Модуль "Профиль пользователя"
Описание
Страница профиля пользователя: бэкенд на Kotlin в пакете ru.bgcrm.core.profile,
интерфейс на Vue + TypeScript в components/user/profile.
Открывается из меню пользователя (пункт "Профиль", адрес /user/profile) и по ссылкам вида
userProfile#<id>, которые ведут на /user/userProfile.do?userId=<id>.
Состав страницы
Свой профиль:
-
"Учётная запись" — фото, значки связи, имя, логин, комментарий, смена пароля с правилами политики и индикатором сложности;
-
"Параметры" — параметры пользователя с поиском по названию и ID и постраничным выводом, сохраняются сразу; вкладка "История" — журнал изменений;
-
"Опции интерфейса" — персональные настройки интерфейса и уведомлений ядра, с поиском и сворачиваемыми разделами;
-
"Настройки плагинов" — опции плагинов, по разделу на плагин; карточки нет, если у плагинов нет опций;
-
"Сеансы" — свои сеансы на текущем сервере: браузер, IP, время входа и активности, завершение остальных;
-
карточки плагинов, например "Telegram" (см. [profile-cards]).
Чужой профиль (/user/userProfile.do?userId=<id>):
-
карточка пользователя — фото, имя, под ним отметки "Заблокирован" и отсутствия (отпуск, больничный и т. п.) с датой окончания или смена на сегодня, справа ID и значки связи, справа после значков связи значок "Послать персональную новость", действующие группы с периодами (по 5 на странице);
-
"Открытые процессы" — отдельная карточка со списком процессов, где пользователь исполнитель, с постраничным выводом;
-
"Параметры" — только просмотр.
Если userId не передан или совпадает с текущим пользователем, открывается свой профиль.
Структура модуля
ru.bgcrm.core.profile
├── IProfileOptionService опции интерфейса
├── IProfileAccountService учётная запись
├── IProfileCardService карточка чужого профиля
├── ProfilePluginSettings настройки плагина: префикс ключей и опции
├── ProfileOptionCatalog регистрация настроек плагинов
├── model/ ProfileOption, ProfileAccount, ProfileCard
├── impl/ реализации сервисов, опции ядра (CoreProfileOptionProvider)
├── impl/repository/ запись в таблицу user
└── struts/
├── action/UserProfileAction
└── model/ DTO ответов
Сервисы зарегистрированы в ServiceModule, репозитории — в RepositoryModule.
Действие зарегистрировано в struts-config.xml по пути /user/userProfile.
Действия и права
| Действие | Право | Что делает |
|---|---|---|
без |
|
Отдаёт страницу |
|
всем |
Опции ядра ( |
|
всем |
Сохраняет опции. Тело запроса — JSON-словарь "ключ → значение" |
|
всем |
Имя, логин и комментарий текущего пользователя |
|
по праву "Изменение учётной записи" |
Сохраняет учётную запись. Тело — JSON с полями |
|
всем |
Карточка пользователя |
|
всем |
Почта и телефоны пользователя |
|
всем |
Открытые процессы пользователя |
|
всем |
Своё фото и признак, что фото можно загружать |
|
всем |
Загружает своё фото из поля формы |
|
всем |
Удаляет своё фото |
|
всем |
Свои сеансы на текущем сервере |
|
всем |
Завершает свои сеансы на текущем сервере, кроме текущего |
|
всем |
Сохраняет произвольные ключи персонализации плагинов, см. Персонализация плагинов |
Права описаны в kernel.xml в разделе "Профиль пользователя".
Право "Изменение учётной записи" объявлено в kernel.xml синонимом
ru.bgcrm.struts.action.ProfileAction:updateSettings;ru.bgcrm.core.profile.struts.action.UserProfileAction:updateAccount:
выдача хранится под первым именем, в таблицах user_permission и user_permset_permission
его нужно искать по нему.
Учётная запись
Пароль меняется, только если передан непустой password. Без него сохраняются имя, логин и
комментарий, а пароль не меняется.
Проверки на сервере:
-
пустое имя — "Не указано имя.";
-
пустой логин — "Не указан логин.";
-
логин занят другим активным пользователем — отказ с текстом про занятый логин;
-
пароль проверяется политикой
PswdUtilс префиксомuser., включая совпадение двух полей.
Запись идёт только в поля title, login, description и отдельно в password
(через EncryptorFactory), наборы прав, группы и конфиг пользователя не трогаются. После
записи сбрасывается UserCache.
Опции интерфейса
Опции хранятся в поле user.personalization, JSP читают их через u:getFromPers.
Каждая опция описывается моделью ProfileOption: ключ, раздел, подпись, тип (switch,
select, number), значение по умолчанию, варианты выбора, границы для чисел и значения
переключателя (onValue/offValue, по умолчанию 1/0). Значение по умолчанию задаётся в
одном месте — в описании опции. Сохранённое значение переключателя приводится к его
onValue/offValue, поэтому true, 1 и yes читаются одинаково.
Сервер принимает только известные ключи и проверяет значение по типу опции.
У опции есть способ применения (ProfileOptionApply):
-
immediate— опцию читает JS изbgcrm.pers, после сохранения страница обновляет значение сама; -
nextOpen— опцию читают JSP при отрисовке, она применится при следующем открытии экрана; -
reload— опцию читаетshell.jspпри загрузке оболочки, нужна перезагрузка страницы.
Опция iface.startPage задаёт стартовую страницу: если в адресе только /user/, оболочка
открывает выбранный пункт меню, при условии что он пользователю разрешён.
Уведомления о процессах
Опции раздела "Уведомления" (notify.process.telegram, notify.process.email,
notify.process.news) по умолчанию включены. Отправки уведомлений о процессах пользователям
проверяют их через isProcessNotificationEnabled / filterProcessNotificationRecipients
из ru.bgcrm.core.profile:
-
команды
telegramNotify*,emailNotify*,newsNotifyExecutorsвProcessCommandExecutor; -
действия конструктора правил: Telegram исполнителям, группам и пользователям, e-mail пользователям, новость исполнителям.
Отправки в произвольный чат и рассылки контрагентам не фильтруются.
Настройки плагинов
Плагин описывает свои настройки одним классом — наследником ProfilePluginSettings — и
регистрирует его при инициализации:
ProfileOptionCatalog.register(HolidaysProfileSettings())
ProfilePluginSettings задаёт:
-
pluginIdиtitle— id плагина и название раздела в карточке "Настройки плагинов"; -
keyPrefix— префикс ключей плагина, по умолчаниюplugin.<pluginId>.; -
options()— опции плагина, описанные так же, как опции ядра; по умолчанию опций нет; -
legacyKeys— ключи вне префикса, которые плагину разрешены для совместимости.
Плагин владеет ключами под своим префиксом и ключами из legacyKeys (owns). Опцию с чужим
ключом сервер не показывает и пишет предупреждение в лог. Повторная регистрация с тем же
pluginId заменяет предыдущую, разделы плагинов идут по названию.
Новые ключи плагинов заводятся только под plugin.<pluginId>.. Единственное исключение —
iface.bgbilling.contractOpenMode у BGBilling: ключ появился раньше правила, переименование
потребовало бы переноса персонализации всех пользователей.
Примеры:
-
ru.bgcrm.plugin.bgbilling.BgbillingProfileSettings— одна опция-список и ключ вlegacyKeys; -
ru.bgcrm.plugin.holidays.HolidaysProfileSettings— эффекты текущего праздника как переключатели. Праздник берётся изplugin.holidays.event, эффекты и их значения по умолчанию — изwebapps/plugin/holidays/events.json(файл перечитывается, когда меняется). Значения переключателей —true/false, как их пишет меню праздника, поэтому профиль и меню показывают одно и то же. Применяются после перезагрузки страницы: эффекты читаетscript.jspпри отрисовке оболочки. Если праздник не задан или не найден, опций нет. -
ru.bgcrm.plugin.oktell.OktellProfileSettings— два переключателя, оба по умолчанию включены.plugin.oktell.profile.call— кнопка звонка у телефонов в профиле:oktell-loader.jsпосле загрузки оболочки читаетbgcrm.persи регистрируетPhoneCall.vueна путиuser.profile.card.phone.action, поэтому применяется после перезагрузки страницы.plugin.oktell.call.confirm— окно подтверждения номера перед звонком.PhoneCall.vueпроверяет его при каждом нажатии, и в профиле, и в списке параметров, поэтому применяется сразу.
Персонализация плагинов
Данные, которые не описать опцией (положение перетаскиваемых элементов и подобное), плагин
пишет через /user/userProfile.do?action=updatePersonalization. Значения передаются
параметрами запроса, подчёркивания в именах заменяются точками: ключ
plugin_holidays_event_newyear_effect_tree_position сохранится как
plugin.holidays.event.newyear.effect.tree.position. Сервер принимает только ключи,
которыми владеет какой-либо зарегистрированный плагин (см. Настройки плагинов); отдельно
регистрировать префикс не нужно.
Параметры
Карточка "Параметры" использует общий компонент ParametersWidget.vue с типом объекта
user и отдаёт данные через существующий /user/parameter.do. В чужом профиле компонент
открывается с disabled: редакторы параметров не раскрываются, кроме параметров типа file,
которые можно открыть для просмотра.
Вкладка "История" берёт журнал из /user/parameter.do?action=parameterLog без разбиения на
страницы. Нужно право "Просмотр истории изменения параметров", без него вкладка показывает
сообщение об ошибке.
Расширения списка параметров
ParametersWidget передаёт в компонент расширения контекст с полями object, user,
parameter и parameters — весь список параметров объекта. Расширение регистрируется через
$loader.load({ extensions }) на путь parameter.list.<тип>.item.action.
Так сделан значок Telegram у параметров "Telegram chat id" (1799) и "Telegram username" (6507):
компонент components/ufanet/TelegramParameterLink.vue, регистрация в js/ufanet.js. Значок
показывается только у пользователя и только у непустого параметра. У username ссылка ведёт на
https://t.me/<username>, у chat id — туда же, если username заполнен, иначе на
tg://user?id=<chat id>.
Чужой профиль: процессы, смена, отсутствие, фото
-
Открытые процессы — те, где пользователь исполнитель и
close_dtпуст, сортировка по id по убыванию. Карточка "Открытые процессы" (ProfileProcesses.vue) запрашивает их постранично отдельным вызовомprocesses, карточка пользователя их не загружает. -
Группы приходят в карточке целиком,
ProfileCard.vueлистает их на клиенте по 5 на странице. -
Смена — записи
callboard_shift_userна текущую дату приложения, время в минутах от начала суток. -
Отсутствие — тоже из графика смен. Смены, которые означают отсутствие, перечисляются ключом
user.profile.absenceShiftIdsв конфигурации через запятую (id изcallboard_shift, например отпуска, больничные, командировки). Без ключа отметки нет. Если на сегодня у пользователя такая смена, период расширяется в обе стороны по дням с такими же сменами: дни без записей в графике (выходные) период не прерывают, день с другой сменой прерывает. Поиск ограничен 62 днями в каждую сторону. Подпись — название сегодняшней смены без хвоста с временем вида0:00-23:59, карточка показывает её как "<подпись> по <дата окончания>" и в этот день не выводит строку смены.
-
Фото — параметр-файл пользователя, его id задаётся ключом
user.profile.photoParamIdв конфигурации. Без ключа показываются инициалы.
Значки связи
ProfileContacts.vue показывает круглые значки справа вверху: в чужом профиле под ID, в
карточке учётной записи — в строке с фото. Данные отдаёт contacts:
-
почта — поле
emailпользователя и значения всех параметров пользователя типаemail, одинаковые адреса без учёта регистра выводятся один раз, значок открываетmailto:; -
телефоны — значения всех параметров пользователя типа
phone, в подсказке 11-значный номер на 7 или 8 выводится как+7. Ядро не звонит: в значок встраиваются компоненты на путиuser.profile.card.phone.action, им передаётсяcontextв формате кнопок телефонных параметров (object,user,parameter.value). Плагинoktellрегистрирует тамPhoneCall.vue, если у пользователя включена опцияplugin.oktell.profile.call. Без такого компонента значок только показывает номер.
Плагины добавляют свои значки компонентами на пути user.profile.card.contacts, им передаётся
user-id. Так ufanet добавляет Telegram (components/ufanet/TelegramContactButton.vue): ссылку отдаёт
/user/plugin/ufanet/telegramLink.do?action=contact&userId=<id> — https://t.me/<username>
по параметру 6507, иначе tg://user?id=<chat id> по параметру 1799. Без обоих значка нет.
Своё фото
Фото загружается в карточке учётной записи (ProfilePhoto.vue) и хранится в том же
параметре-файле user.profile.photoParamId, поэтому сразу видно в чужом профиле. Если ключ не
задан или параметр не типа file, кнопок нет.
Сервер (ProfilePhotoService) принимает JPG, PNG, GIF и WebP до 5 МБ и проверяет и расширение,
и сигнатуру содержимого. Перед записью все прежние версии удаляются, файл сохраняется под именем
photo.<расширение>. Удаление версии параметра удаляет и сам файл, восстановить прежнее фото
нельзя.
Сеансы
Сеансы берутся из LoginStat того сервера, на который пришёл запрос. На других серверах
кластера сеансы не видны и не завершаются, интерфейс прямо об этом пишет. Завершённый сеанс
выходит из системы при следующем запросе, в списке он не показывается сразу после завершения.
IP и User-Agent запоминаются при входе (AuthFilter → LoginStat.userLoggedIn). IP берётся
из заголовка X-Real-IP, который ставит nginx, иначе — адрес соединения. Название браузера и
системы фронт разбирает из User-Agent сам.
Карточки плагинов
UserProfile.vue выводит в своём профиле компоненты, зарегистрированные на путь
расширения user.profile.cards, и передаёт им user-id:
Vue.prototype.$loader.load({
extensions: {user: {profile: {cards: [{name: 'my-card', component: '/path/MyCard.vue'}]}}}
});
Telegram в один клик
Карточка components/ufanet/TelegramLinkCard.vue, регистрация в js/ufanet.js, бэкенд
ru.bgcrm.plugin.ufanet.telegram.link.
-
Кнопка "Подключить Telegram" вызывает
/user/plugin/ufanet/telegramLink.do?action=link. Сервер выдаёт одноразовый токен на 15 минут и ссылкуhttps://t.me/<бот>?start=<server.instance.id>-<токен>. -
Пользователь нажимает в боте "Запустить". telegram-service превращает
/startвCallbackRequestс командой0;linkTelegram:<токен>и кладёт в очередьtg.telegrabсto=<server.instance.id>— на тот сервер, который выдал токен. -
TeleGrabAPIобрабатывает команду до поиска пользователя по Telegram id: по токену находит пользователя ERP и записывает параметры 1799 (chat id) и 6507 (username). Если этот Telegram уже привязан к другому пользователю, привязка отклоняется. -
Бот уведомлений присылает подтверждение, карточка в профиле видит новый chat id и показывает "Telegram подключён".
Имя бота задаётся ключом telegram.link.botUsername. Без него кнопки нет, карточка подсказывает
команду бота /get_id. Токены хранятся в памяти сервера, после перезапуска
ссылку нужно получить заново.
Фронтенд
| Файл | Назначение |
|---|---|
|
Обёртка страницы, передаёт id текущего и запрошенного пользователя, заголовок оболочки |
|
Выбор режима: свой или чужой профиль |
|
Карточка "Учётная запись" |
|
Своё фото: загрузка, замена, удаление |
|
Значки почты, телефона и плагинов |
|
Карточка "Параметры" |
|
Карточка "Опции интерфейса" |
|
Карточка чужого профиля |
|
Открытые процессы в чужом профиле |
|
Карточка "Сеансы" |
|
Репозиторий запросов к |
|
Типы ответов и запросов |
Заголовок оболочки задаётся через shell_title.jsp, который вставляет его в JS-строку
в двойных кавычках. Поэтому в заголовке нельзя использовать разметку с атрибутами в двойных
кавычках, имя пользователя выводится через c:out.
Версия скриптов плагинов
js/ufanet.js и другие скрипты плагинов, подключённые через endpoint js с путём под
/js/, получают в адрес суффикс версии по времени изменения файла (script_css.jsp).
Суффикс снимает UrlRewriteFilter, который замаплен только на /js/*. Скрипты вне /js/
(например, /plugin/oktell/js/oktell-loader.js) подключаются без версии.