Настройка нового личного кабинета
Конфигурация ЛК состоит из трех файлов, расположенных в папке 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=value1one.two.three.parameterA=value2one.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.// ЛК является пользователем биллинга, общается с ним также, как BGBillingClientbgbilling {    // URL доступа к BGBilling    url = 'http://127.0.0.1:8080/bgbilling/executer'    // Логин    user = 'customer'    // Пароль    password = '123456'}Параметры идентификации HTTP-соединения
Личному кабинету в некоторых случаях требуется знать IP-адрес абонента, который пользуется им в текущий момент (например, для авторизации по IP-адресу или блокировке при переборе логинов/паролей). Поэтому при использовании NGINX требуется указать HTTP-заголовок в параметре context.hostHttpRequestHeader, из которого получать реальный IP-адрес вместо физического IP-адреса HTTP-соединения.
context {		...        // Идентификатор хоста по HTTP-заголовку, например, X-Real-IP. По умолчанию используется IP-адрес хоста    hostHttpRequestHeader = 'X-Real-IP'}Параметры аутентификации абонента
// Параметры аутентификации абонентаauthentication {    // Кол-во ошибок аутентификации, после которого будет отображаться captcha для этого логина    captchaLoginErrorCount = 5    // Кол-во ошибок аутентификации, после которого будет отображаться captcha для хоста    captchaHostErrorCount = 20    // Кол-во ошибок аутентификации, после которых будут заблокированы попытки этого хоста    blockHostErrorCount = 30        // Режимы аутентификации для входа в ЛК    modes = [		// аутентификация по номеру договора    	authenticationMode { 			mode = 'contract'    	} 	]}Режимов аутентификации может быть несколько - в этом случае в окне логина можно выбрать необходимый. На данный момент поддерживаются три режима аутентификации:
по номеру договора
authenticationMode {mode='contract'}по логину модуля Inet
authenticationMode {module='inet'mode='login'// ID модуляmoduleId=1}по IP-адресу сессии модуля Inet (вход без пароля)
authenticationMode {module='inet'mode='ip'// ID модуляmoduleId=1// ограниченный доступrole='unauthCustomer'}
Для режима аутентификации можно назначить, чтобы доступ после аутентификации через него был ограничен. Для этого указывается параметр 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" ) ...