Настройка нового личного кабинета

Конфигурация ЛК состоит из трех файлов, расположенных в папке 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-соединения

Личному кабинету в некоторых случаях требуется знать 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" )
 
...