Page tree
Skip to end of metadata
Go to start of metadata

Конфигурация ЛК состоит из трех файлов, расположенных в папке WEB-INF: основного файла конфигурации mybgbilling-conf.groovy, файла конфигурации меню mybgbilling-menu.groovy, файла конфигурации платежных систем mybgbilling-payment.groovy. Файлы конфигурации созданы с использованием синтаксиса Groovy.

Описание синтаксиса

Группы параметров в конфиге разделяются не точкой, а с помощью вложенных блоков. Значение параметра должно быть правильным Groovy/Java-объектом - в простом случае строкой, заключенной в одинарные или двойные кавычки, или числом, например:

one {
	two {
		parameterA = 'value1'
		three {
			parameterA = 'value2'
			parameterB = 100
		}
	}
}

Т.е. параметр конфигурации - это один или несколько вложенных блоков, имя параметра и значение после знака =. Данный пример в конфигурации модулей биллинга выглядел бы так:

one.two.parameterA=value1
one.two.three.parameterA=value2
one.two.three.parameterB=100

Некоторые значения параметров должны быть списками или массивами определенных объектов. Объекты списка заключены в квадратные скобки [] и разделены между собой символом запятой. Например:

authentication {

    modes = [
    	authenticationMode {
 			mode = 'contract'
    	}, 
 
    	authenticationMode {
     		mode = 'login'
     		module = 'inet'
     		moduleId = 1
     	}
    ]
}

Некоторые значения параметров могут быть ассоциативными массивами (список ключ:значение, map). Связки ключ:значение заключены в квадратные скобки [] и разделены между собой символом запятой. Например:

example {
    map = [
    	key: 'value',
    	key2: 200
    ]
}

Также параметры могут быть прописаны как ассоциативный массив, заключенный в круглые скобки (значение в этом случае прописывается через ':' (двеоточие), а не через символ '='):

authentication {

    modes = [
    	authenticationMode( mode: 'contract' ), 
    	authenticationMode( mode: 'login', module: 'inet', moduleId: 1 )
     	}
    ]
}

Некоторые значения могут быть динамическими, если использовать замыкания (closure). Т.е., грубо говоря, значением может быть функция, которая будет возвращать нужное значение:

status {
			// возможность изменения статуса договора
			//statusChange = { contract -> return contractInGroup( contract, [1, 2, 3, 4, 20] ) && isCustomer(); }
			//statusChange = { contract -> contractInGroup( contract, [1, 2, 3, 4, 20] ) && isCustomer() }
			statusChange = { isCustomer() }
}

В mybgbilling-conf.groovy и mybgbilling-menu.groovy в таких замыканиях можно использовать определенный набор методов, аргумент объект-contract, а также дополнительные аргументы, специфичные для определенного параметра конфигурации (например, параметры content.kernel.customerTitle и content.kernel.subContractGroup):

content {
	kernel {
		// название контрагента, отображаемое на странице
		customerTitle = { contract, contractParameterMap ->
		
			// ID параметров договоров названия физ. лиц (для customerTitle)
			def individualCustomerTitleParamIds = [0, 0, 0, 0, 0];
			// ID параметров договоров названия юр. лиц (для customerTitle)
			def corporationCustomerParamIds = [0, 0, 0, 0, 0];
	
			def paramIds = contract.personType == 1 ? corporationCustomerParamIds : individualCustomerTitleParamIds;
	
			String result = contractParameterMap.values().stream()
				.filter{ v -> paramIds.contains( v.entitySpecAttrId ) && notBlankString( v.toString() ) }
				.findFirst()
				.map{ v -> v.toString() }
				.orElse( null );
			
			// можно отобразить и просто комментарий договора
			//if( result == null ) {
			//	result = contract.comment;
			//}
			
			return result;
		}
	}
}

В замыканиях можно использовать методы:

  • isCustomer() или isUserInRole('customer') - возвращает true, если в режиме аутентификации, которым воспользовался абонент, не указан параметр role = 'unauthCustomer';
  • contractInGroup( contract, groupIds ) - возвращает true, если переданный в первый аргумент объект-contract содержит в себе одну из групп, указанных списке второго аргумента, например: contractInGroup( contract, [2, 3, 8, 13] ).

Основная конфигурация (mybgbilling-conf.groovy)

Основная конфигурация личного кабинета состоит из нескольких блоков:

  • bgbilling - конфигурация подключения к BGBillingServer,
  • authentication - параметры аутентификации абонента,
  • mail - параметры почтовой подсистемы (чтобы ЛК мог отправлять письма при необходимости),
  • content - параметры содержимого страниц.

Конфигурация подключения к BGBillingServer

// Параметры подключения к BGBillingServer.
// ЛК является пользователем биллинга, общается с ним также, как BGBillingClient
bgbilling {
    // URL доступа к BGBilling
    url = 'http://127.0.0.1:8080/bgbilling/executer'
    // Логин
    user = 'customer'
    // Пароль
    password = '123456'
}

Параметры идентификации HTTP-соединения

Личному кабинету в некоторых случаях требуется знать базовый URL, по которому абоненты получают доступ к нему. Личный кабинет биллинга может получить это значение из запроса, однако при использовании NGINX значение из запроса может быть не правильным. Поэтому базовый URL следует указать в конфигурации в параметре baseUrl.

Также личному кабинету требуется знать IP-адрес абонента, который пользуется им в текущий момент (например, для авторизации по IP-адресу или блокировке при переборе логинов/паролей). Поэтому при использовании NGINX требуется указать HTTP-заголовок в параметре context.hostHttpRequestHeader, из которого получать реальный IP-адрес вместо физического IP-адреса HTTP-соединения.

context {
	
    // Базовый адрес сервера (через который абоненты получают доступ к ЛК). По умолчанию используется значение из запроса
    //baseUrl = 'https://provider.ru/selfcare'
    baseUrl = 'https://my.provider.ru'
    
    // Идентификатор хоста по HTTP-заголовку, например, X-Real-IP. По умолчанию используется IP-адрес хоста
    hostHttpRequestHeader = 'X-Real-IP'
}

Параметры аутентификации абонента

// Параметры аутентификации абонента
authentication {
    // Кол-во ошибок аутентификации, после которого будет отображаться captcha для этого логина
    captchaLoginErrorCount = 5
    // Кол-во ошибок аутентификации, после которого будет отображаться captcha для хоста
    captchaHostErrorCount = 20
    // Кол-во ошибок аутентификации, после которых будут заблокированы попытки этого хоста
    blockHostErrorCount = 30
    
    // Режимы аутентификации для входа в ЛК
    modes = [
		// аутентификация по номеру договора
    	authenticationMode {
 			mode = 'contract'
    	}
 	]
}

Режимов аутентификации может быть несколько - в этом случае в окне логина можно выбрать необходимый. На данный момент поддерживаются 6 режимов аутентификации:

  • по номеру договора:

    authenticationMode {
    	mode = 'contract'
    }
  • по логину модуля Inet:

    authenticationMode {
    	module = 'inet'
    	mode = 'login'
    	// ID модуля
    	moduleId = 1
    }
  • по IP-адресу сессии модуля Inet (вход без пароля):

    authenticationMode {
    	module = 'inet'
    	mode = 'ip'
    	// ID модуля
    	moduleId = 1
    	// ограниченный доступ
    	role = 'unauthCustomer'
    }
  • по параметру договора "телефон":

    authenticationMode {
    	module = 'kernel'
    	mode = 'phoneParam'
    	// ID параметра
    	parameterId = 1
    	// преобразование введенного номера договора
    	username = { s -> s.replaceAll( /^8(.+)$/,'7$1' ) }
    }
  • по параметру договора "Email":

    authenticationMode {
    	module = 'kernel'
    	mode = 'emailParam'
    	// ID параметра
    	parameterId = 2
    	// преобразование введенного номера телефона
    	username = { s -> s.replaceAll( /^8(.+)$/,'7$1' ) }
    }
  • по текстовому параметру договора:

    authenticationMode {
    	module = 'kernel'
    	mode = 'textParam'
    	// ID параметра
    	parameterId = 3
    }

При аутентификации по параметру договора в качестве пароля используется пароль к личному кабинету (как и при аутентификации номеру договора). При аутентификации по номеру телефона идет поиск только по введенным цифрам, при этом можно задать преобразование введенного номера в другой вид с помощью параметра username; если параметр username не указан, то по умолчанию 8 в начале строки заменяется на 7.

Для режима аутентификации можно назначить, чтобы доступ после аутентификации через него был ограничен. Для этого указывается параметр role = 'unauthCustomer'. В этом случае, вызов isUserInRole( "customer" ) будет возвращать false. Ограниченный доступ может быть указан, например, для режима аутентификации по IP-адресу модуля Inet.

Можно разрешить аутентификацию только для определенных групп договоров, указав условие в параметре filter:

authenticationMode {
	module = 'inet'
	mode = 'ip'
	//ID модуля
	moduleId = 1
	// ограниченный доступ
	role = 'unauthCustomer'
    // фильтр по группам договоров
	filter = { contract -> contractInGroup( contract, [1, 2, 3, 4, 20] ) }
}

Или наоборот, запретить для определенных групп договоров:

	filter = { contract -> !contractInGroup( contract, [1, 2, 3, 4, 20] ) }

Или разрешить по номеру договора:

	filter = { contract -> contract.title.startsWith( "NK" ) }

Или использовать регулярное выражение:

	filter = { contract -> contract.title.matches( "NK.*" ) }

Параметры почтовой подсистемы

// Параметры SMTP, чтобы ЛК мог отправлять письма
mail {
	smtp {
		host = 'smtp.provider.ru'
	}
	
	from {
		email = 'support@provider.ru'
		name = 'BGBilling'
	}
}

Параметры содержимого страниц

Разрешенные фрагменты

Данный блок конфигурации позволяет настраивать, какие фрагменты страницы или какие действия доступны абонентам или группам абонентов. Например, в коде страницы статусов договора есть фрагмент смены статуса:

<ui:fragment rendered="#{configuration.get('content.kernel.status.statusChange', true)}">
	...
</ui:fragment>

Соответственно можно в конфигурации запретить всем менять статус договора из личного кабинета:

content {
	kernel {
		...
	
		// status.xhtml
		status {
			// возможность изменения статуса договора
			statusChange = false
		}
		
		...
	}
	
	...
}

Можно разрешить только тем, кто был аутентифицирован по логину/паролю (в конфигурации по умолчанию установлен этот вариант):

statusChange = { isUserInRole( "customer" ) }

Разрешить только аутентифицированным по логину/паролю физ. лицам:

statusChange = { contract -> isUserInRole( "customer" ) && contract.getPersonType() == 0 }

Или разрешить только аутентифицированным по логину/паролю определенным группам договоров:

statusChange = { contract -> isUserInRole( "customer" ) && contractInGroup( contract, [1, 2, 3, 4, 20] ) }

Название контрагента в верхней части страницы

По умолчанию в шапке страницы название или имя контрагента не отображается. За отображение названия (или имени) отвечает параметр content.kernel.customerTitle. В конфигурации можно указать, чтобы отображался комментарий договора:

content {
	kernel {
 
	// название контрагента, отображаемое на странице
	customerTitle = { contract, contractParameterMap -> contract.comment }
 
	...
}

Или же отобразить параметр договора, в зависимости от типа лица договора (физ. лицо или юр. лицо):

content {
	kernel {

		// название контрагента, отображаемое на странице
		customerTitle = { contract, contractParameterMap ->

			// ID параметров договоров названия физ. лиц (для customerTitle)
			def individualCustomerTitleParamIds = [33, 0, 0, 0, 0];
			// ID параметров договоров названия юр. лиц (для customerTitle)
			def corporationCustomerParamIds = [10, 0, 0, 0, 0];

			def paramIds = contract.personType == 1 ? corporationCustomerParamIds : individualCustomerTitleParamIds;

			String result = contractParameterMap.values().stream()
				.filter{ v -> paramIds.contains( v.entitySpecAttrId ) && notBlankString( v.toString() ) }
				.findFirst()
				.map{ v -> v.toString() }
				.orElse( null );

			return result;
		}
 
		...
	}
	
	...
}

Группировка субдоговоров в меню

Если субдоговоров у данного договора меньше 10 - они отображаются прямо в меню. В этом случае можно сортировать и группировать список субдоговоров:

content {
	kernel {

		// группировка субдоговоров (для меню)
		subContractGroup = { subContractList ->
		
			subContractList
			.stream()
			.sorted({ a,b -> a.title.compareTo(b.title) })
			.collect( Collectors.groupingBy{ contract ->

				// можно группировать субдоговора по группам договоров
				if( contractInGroup( contract, [1, 2, 3, 4, 20] ) ) {
					return "contract.sub.group.01.internet";
				}else if( contractInGroup( contract, [5, 6, 7, 8, 9] ) ) {
					return "contract.sub.group.02.phone";
				} else {
					return "contract.sub.group.99.other";
				}

				// если всем возвращать пустую строку - то группировки не будет
				return "";
			})
			.entrySet()
			.stream()
			.sorted({ a,b -> a.key.compareTo(b.key) })
			.collect( Collectors.toList() );
		}
 
		...
	}
 
	...
}

В примере при группировке используются строки вида "contract.sub.group.01.internet". Число в данном случае используется для сортировки групп, а само название группы должно быть прописано в Locale_ru_RU.properties по ключу:

contract.sub.group.01.internet=Интернет
contract.sub.group.02.phone=Телефония
contract.sub.group.99.other=Другое

Конфигурация меню (mybgbilling-menu.groovy)

Данный файл конфигурации возвращает дерево пунктов меню для договора. Выглядит конфигурация, например, так:

menu {
	// список пунктов верхнего уровня
	children = [
	
		// Новости
		menu( page: "kernel/news", icon: "fa-newspaper-o", title: "menu.news" ),
		
		// Уведомления + Рассылки
		menu( page: "kernel/notificationsEx", subPage: "notifications", icon: "fa-envelope-o", 
						title: "menu.notifications", badge: "#{notificationBean.getUnreadCount()}", badgeUpdate: "#{notificationBean.populate()}",
						show: isCustomer() ),
	
		// Уведомления (отдельно от рассылок)
		menu( page: "kernel/notifications", icon: "fa-envelope-o", title: "menu.notifications",
						show: !isCustomer() ),
							
		// Баланс
		menu( page: "kernel/balance", icon: "fa-rub", title: "menu.balance" ),
	
		// Лимит
		menu( page: "kernel/limit", icon: "fa-umbrella", title: "menu.limit" ),
		
		// Тарифные опции
		menu( page: "kernel/tariffOptions", icon: "fa-cogs", title: "menu.tariffOptions", show: isCustomer() ),

		// Договор
		menu( icon: "fa-briefcase", title: "menu.contract" ) {
				children = [
					// Статус
					menu( page: "kernel/status", title: "menu.status" ),
					// Тарифы
					menu( page: "kernel/tariffs", title: "menu.tariffs", show: isCustomer() ),
					// Действия
					menu( page: "kernel/additionalActions", title: "menu.additionalActions", show: isCustomer() ),
					// Документы
					menu( page: "kernel/documents", title: "menu.documents", show: isCustomer() ),
					// Документы (включены в предыдущий пункт)
					//menu( page: "plugins/documents/documents", title: "menu.documents" ),
					// Бухгалтерия
					menu( module: "bill", page: "modules/bill/bill", title: "menu.bill", show: isCustomer() ),
					// Примечания
					menu( page: "kernel/notes", title: "menu.notes", show: isCustomer() ),
					// Смена пароля
					menu( page: "kernel/password", title: "menu.password", show: isCustomer() )
				]
			},

		// Интернет
		menu( module:"inet", icon:"fa-globe", title:"menu.inet" ) {
				children = [
					// Сессии
					menu( page: "modules/inet/sessions", title: "menu.inet.sessions" ),
					// Трафик
					menu( page: "modules/inet/traffics", title: "menu.inet.traffics" ),
					// Смена пароля
					menu( page: "modules/inet/password", title: "menu.inet.password", show: isCustomer() )
				]
			},
			
		// ТВ
		menu( module:"tv", page:"modules/tv/tv", icon:"fa-tv", title:"menu.tv" ),
	
		// Поддержка
		menu( page: "plugins/helpdesk/helpdesk", icon: "fa-wrench", title: "menu.helpdesk",
							badge: "#{helpdeskBean.getUnreadTopicCount()}", badgeUpdate: "#{helpdeskBean.populateTopics()}",
							show: isCustomer() )
	
	]
}

У каждого объекта-пункта меню есть набор параметров:

  • module - модуль, если данный пункт относится к модулю, наследуется дочерними пунктами;
  • moduleId - ID модуля (необязательно, если указан module, то подставляется автоматически), наследуется дочерними пунктами. Можно использовать, если одинаковые модули нужно показывать по разному;
  • page - страница, без .xhtml;
  • subPage - подстраница;
  • icon - иконка;
  • title - название пункта меню (ключ для Locale.properties);
  • badge - счетчик, указывается JSF-вызов метода, который вернет число;
  • badgeUpdate - JSF-вызов метода, который нужно произвести для обновления счетчика
  • show - показывать пункт или нет (если не указан, то показывать)
  • children - список дочерних пунктов меню

Используя параметр show, можно ограничивать использование пунктов меню для групп договоров:

 menu( moduleId: 210, page: "modules/tv/tv", icon: "fa-tv", title: "menu.tv",
							show: contractInGroup( contract, [1, 2, 3, 4, 20] ) )

При необходимости список дочерних пунктов меню можно определить как переменную и добавлять пункты в этот список, используя условия:

 
menu {
	// список пунктов верхнего уровня
	def firstLevel = [];
	children = firstLevel;
	
	// Новости
	firstLevel << menu( page: "kernel/news", icon: "fa-newspaper-o", title: "menu.news" )
	
	// если авторизован по логину/паролю
	if( isCustomer() ) {
	
		// Уведомления + Рассылки
		firstLevel << menu( page: "kernel/notificationsEx", subPage: "notifications", icon: "fa-envelope-o", 
							title: "menu.notifications", badge: "#{notificationBean.getUnreadCount()}", badgeUpdate: "#{notificationBean.populate()}" )
	
	} else {
	
		// Уведомления
		firstLevel << menu( page: "kernel/notifications", subPage: "", icon: "fa-envelope-o", title: "menu.notifications" )
	
	}
	// Баланс
	firstLevel << menu( page: "kernel/balance", icon: "fa-rub", title: "menu.balance" )
	
	// если авторизован по логину/паролю
	if( isCustomer() ) {
	
		// Лимит
		firstLevel << menu( page: "kernel/limit", icon: "fa-umbrella", title: "menu.limit" )
		
		// Тарифные опции
		firstLevel << menu( page: "kernel/tariffOptions", icon: "fa-cogs", title: "menu.tariffOptions" )
 
...

Конфигурация приема платежей (mybgbilling-payment.groovy)

В файле mybgbilling-payment.groovy настраивается, какие платежные системы будут присутствовать при проведении оплаты из личного кабинета. Часто значения по умолчанию не требуют изменений. ЛК сам создаст список провайдеров из платежных модулей и при проведении оплаты отобразит те из них, модули которых подключены к данному договору.

paymentConfig {

	/* Если true - то используются только провайдеры/модули, которые указаны в providers. */
    replaceProviders = false
    /* Показывать оплату через модуль Card */
    showCard = false
    /* Нужно ли указывать email/телефон при оплате */
    needReceiptContacts = true

    providers = [
    
    ]

	/* Список возможных способов пополнения можно указать вручную, но также нужно учитывать, что если модуль отсутствует на договоре - 
		то пункт с соответствующим модулем будет исключен из списка.  */
    payments = [

    ]
}

Если вы хотите, чтобы дополнительно отображался вариант с активацией карты оплаты модуля Card, укажите в конфигурации showCard=true

paymentConfig {

	...

    /* Показывать оплату через модуль Card */
    showCard = true

    ...
}

При необходимости к текущим платежным системам вы можете добавить кнопку-ссылку:

paymentConfig {

	/* Если true - то используются только провайдеры/модули, которые указаны в providers. */
    replaceProviders = false
    /* Показывать оплату через модуль Card */
    showCard = false
    /* Нужно ли указывать email/телефон при оплате */
    needReceiptContacts = true

    providers = [

    	// внешняя ссылка
		provider {
        	id = "externalLinkQiwi"
        	title = "Qiwi"
        	image = 'static/images/logos/qiwi.png'
			config = config {
				url = 'https://qiwi.com/payment/form.action?provider=297'
			}
    	}
    ]

    payments = [

    ]
}
Обратите внимание на параметр replaceProviders. Если он указан true, то не будет автоматической подгрузки провайдеров из платежных модулей - будут только те провайдеры, которые указаны в providers. Т.е. если его установить в true в конфигурации, что представлена выше, то при проведении платежа будет доступна только эта внешняя ссылка, даже если к договору подключены какие-то платежные модули.

Также для некоторых случаев с помощью providers можно переопределить конфигурацию провайдера, например, указать для Яндекс.Денег, какие именно типы платежей можно принимать:

paymentConfig {

	/* Если true - то используются только провайдеры/модули, которые указаны в providers. */
    replaceProviders = false
    /* Показывать оплату через модуль Card */
    showCard = false
    /* Нужно ли указывать email/телефон при оплате */
    needReceiptContacts = true

    providers = [
    
    	// для Яндекс.Деньги возможно нужно указать типы платежей
        provider {
        	// код модуля Яндекс.Деньги
			moduleId = кодмодуля
			config = config {
				//Список доступных типов оплаты (Из в ЦПП). Пример: PC:Оплата со счета Яндекс.Денег;AC:Оплата с банковской карты;MC:Платеж со счета мобильного телефона;GP:Оплата наличными через кассы и терминалы;WM:Оплата с кошелька в системе WebMoney;SB:Оплата через Сбербанк Онлайн
				//paymentTypes = 'PC:payment.yamoney.PC;AC:payment.yamoney.AC;MC:payment.yamoney.MC;GP:payment.yamoney.GP;WM:payment.yamoney.WM;SB:payment.yamoney.SB'
				paymentTypes = 'PC:payment.yamoney.PC'
			}
    	}
    ]

    payments = [

    ]
}

Параметр payments предназначен для указания режимов оплаты и их порядка на странице вручную. Если в этом параметре указаны какие-то режимы, то при проведении платежа будут доступны только они (при дополнительном условии, что соответствующий модуль подключен к договору):

paymentConfig {

	/* Если true - то используются только провайдеры/модули, которые указаны в providers. */
    replaceProviders = false
    /* Показывать оплату через модуль Card */
    showCard = false
    /* Нужно ли указывать email/телефон при оплате */
    needReceiptContacts = true

    providers = [

    ]

    payments = [

        payment {
            title = 'payment.bankCard'
            image = 'static/images/logos/visa_mastercard.png'
            providerId = 'yamoney'
            type = 'bankCard'
            config = config {
                paymentTypes = 'AC:payment.yamoney.AC'
            }
        },

        payment {
            title = 'payment.yamoney'
            image = 'static/images/logos/yamoney.png'
            providerId = 'yamoney'
            type = 'bankCard'
            config = config {
                paymentTypes = 'PC:payment.yamoney.PC'
            }
        }, 

        payment {
            title = 'payment.card'
            image = 'static/images/logos/card.png'
            providerId = 'card'
        }
    ]
}

Еще один пример:

paymentConfig {

	/* Если true - то используются только провайдеры/модули, которые указаны в providers. */
    replaceProviders = false
    /* Показывать оплату через модуль Card */
    showCard = false
    /* Нужно ли указывать email/телефон при оплате */
    needReceiptContacts = true

    providers = [

        provider {
             id = "externalLinkQiwi"
             title = "Qiwi"
             image = 'static/images/logos/qiwi.png'
             config = config {
                 url = 'https://qiwi.com/payment/form.action?provider=297'
             }
        }
    ]

    payments = [

        payment {
            title = 'payment.bankCard'
            image = 'static/images/logos/visa_mastercard.png'
            providerId = 'yamoney'
            type = 'bankCard'
            config = config {
                paymentTypes = 'AC:payment.yamoney.AC'
            }
        },

        payment {
            title = 'payment.yamoney'
            image = 'static/images/logos/yamoney.png'
            providerId = 'yamoney'
            type = 'bankCard'
            config = config {
                paymentTypes = 'PC:payment.yamoney.PC'
            }
        }, 
        payment {
            providerId = ’externalLinkQiwi’
        }
    ]
}

Параметр needReceiptContacts указывает, нужно ли абоненту указывать email или телефон перед проведением оплаты (введенную информацию далее можно использовать при создании электронного чека).

Обновление личного кабинета

Для обновления личного кабинета запустите скрипт mybgbilling-update.sh:

/opt/wildfly/bin/mybgbilling-update.sh

При обновлении файлы, рядом с которыми есть файл с таким же именем плюс суффикс(расширение) .orig, не будут перезаписаны файлом из сборки, вместо них обновяться .orig-файлы (см. Кастомизация нового личного кабинета). Также при обновлении полностью удаляется и перезаписывается директория MyBGBilling.war/WEB-INF/classes/ru/bitel.

  • No labels