feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР

- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk)
- трансферные таблицы FTV_M2M: создание при установке, события, справочник участников
- сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO
- боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК
- онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML)
- модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-15 19:41:12 +03:00
parent 59fc774f08
commit da12a9ec1c
132 changed files with 29969 additions and 1089 deletions
+305 -25
View File
@@ -5,6 +5,7 @@ import (
"errors"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
@@ -20,22 +21,65 @@ type RuntimeConfig struct {
// Settings — сериализуемое представление настроек.
type Settings struct {
Postgres PostgresSettings `json:"postgres"`
Crypto CryptoSettings `json:"crypto"`
NSD NSDSettings `json:"nsd"`
LK LKSettings `json:"lk"`
CACerts CACertsSettings `json:"ca_certs"`
News NewsSettings `json:"news"`
Update UpdateSettings `json:"update"`
License LicenseSettings `json:"license"`
LastTest *TestRunResult `json:"last_test,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
Postgres PostgresSettings `json:"postgres"`
Fancy FancyMSSQLSettings `json:"fancy_mssql"`
Quik QuikMNPSettings `json:"quik_mnp"`
Crypto CryptoSettings `json:"crypto"`
NSD NSDSettings `json:"nsd"`
LK LKSettings `json:"lk"`
CACerts CACertsSettings `json:"ca_certs"`
NSDCerts NSDCertsSettings `json:"nsd_certs"`
News NewsSettings `json:"news"`
Alerts AlertSettings `json:"alerts"`
Incoming IncomingNotifySettings `json:"incoming_notify"`
Update UpdateSettings `json:"update"`
License LicenseSettings `json:"license"`
Branding BrandingSettings `json:"branding"`
NPR NPRSettings `json:"npr"`
QuikExport QuikExportSettings `json:"quik_export"`
Auth AuthSettings `json:"auth"`
LastTest *TestRunResult `json:"last_test,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
// QuikMNPSettings — подключение к БД Модуля неторговых поручений QUIK (MSSQL).
// Используется для блокировки бумаг при выводе (M2M). БД боевая — работаем на
// чтение + вызовы штатных хранимых процедур API (роль InstrEu). Прямых INSERT/
// UPDATE в таблицы НЕ делаем.
type QuikMNPSettings struct {
Server string `json:"server"` // host или host\instance
Port int `json:"port"` // напр. 8849 (нестандартный)
Database string `json:"database"` // имя БД МНП (если требуется)
User string `json:"user"` // SQL-логин (роль InstrEu), напр. BROKER_88
Password string `json:"password"` // пароль
Encrypt string `json:"encrypt"` // disable | true (по умолчанию true+trust для боевой)
Trust bool `json:"trust"` // TrustServerCertificate (самоподписанный серт)
Verified bool `json:"verified"` // последнее тест-подключение прошло
LastReport string `json:"last_report"` // полный текст последнего теста (для копирования коллегам)
LastAt time.Time `json:"last_at"` // время последнего теста
// Автоматический цикл M2M в МНП (пишущие операции к боевой БД).
WritesEnabled bool `json:"writes_enabled"` // главный предохранитель: без него ни одной записи в МНП
AutoOut bool `json:"auto_out"` // исходящие: регистрировать «Вывод ЦБ (M2M)» при отправке, подписывать/снимать по ответу
AutoIn bool `json:"auto_in"` // входящие: регистрировать «Ввод ЦБ (M2M)» после приёма поручения НРД
PollTerminal bool `json:"poll_terminal"` // подхватывать поручения, поданные клиентами из QUIK (Export)
Account string `json:"account"` // торговый счёт для регистрации (напр. L01+00000F00)
SecOut string `json:"sec_out"` // код МНП-инструмента вывода (пусто = определить по классу INSTROUTM2M)
SecIn string `json:"sec_in"` // код МНП-инструмента ввода (пусто = по классу INSTRINM2M)
ClientCodeMap map[string]string `json:"client_code_map"` // исключения: номер портфеля DEPO («БР-4-4») → код клиента QUIK
}
// LicenseSettings — лицензионный ключ (подписанный токен).
type LicenseSettings struct {
Key string `json:"key"` // компактный токен payload.signature.keyid
PublicKey string `json:"public_key"` // base64 (если не зашит в бинарь)
Key string `json:"key"` // компактный токен payload.signature.keyid
PublicKey string `json:"public_key,omitempty"` // устарело: ключ проверки зашит в bj, поле не используется
// Онлайн-лицензирование (licenseclient.go).
InstallID string `json:"install_id,omitempty"` // идентификатор этой установки на сервере лицензий
LastSync time.Time `json:"last_sync,omitempty"` // последнее успешное обращение к серверу лицензий
LastAttempt time.Time `json:"last_attempt,omitempty"` // последняя попытка
LastOK bool `json:"last_ok,omitempty"`
LastMsg string `json:"last_msg,omitempty"`
}
// UpdateSettings — авто-обновления из артефактории (#18/#20).
@@ -55,10 +99,10 @@ type UpdateSettings struct {
// doc-watcher'ом и cron-задачами. Каждое событие может быть скрыто (Dismissed)
// оператором, но не удалено — лента служит «журналом» для аудита.
type NewsSettings struct {
Items []NewsItem `json:"items"`
DocSources []DocSource `json:"doc_sources"` // URL'ы для авто-проверки (NSD pages)
LastDocCheck time.Time `json:"last_doc_check"`
DocCheckResult string `json:"doc_check_result"`
Items []NewsItem `json:"items"`
DocSources []DocSource `json:"doc_sources"` // URL'ы для авто-проверки (NSD pages)
LastDocCheck time.Time `json:"last_doc_check"`
DocCheckResult string `json:"doc_check_result"`
}
// NewsItem — одно событие в ленте.
@@ -76,21 +120,21 @@ type NewsItem struct {
// DocSource — страница НРД, которую doc-watcher периодически проверяет.
type DocSource struct {
URL string `json:"url"`
Name string `json:"name"` // человекочитаемое имя
LastChecked time.Time `json:"last_checked"`
KnownPDFs map[string]string `json:"known_pdfs"` // url → sha256
URL string `json:"url"`
Name string `json:"name"` // человекочитаемое имя
LastChecked time.Time `json:"last_checked"`
KnownPDFs map[string]string `json:"known_pdfs"` // url → sha256
}
// CACertsSettings — URL'ы для авто-загрузки сертификатов УЦ НРД и нашего
// УЦ. Список редактируется пользователем; раз в сутки фоновая горутина
// перекачивает каждый URL и сохраняет сертификат, если он поменялся.
type CACertsSettings struct {
URLs []string `json:"urls"`
AutoUpdate bool `json:"auto_update"`
LastFetch time.Time `json:"last_fetch"`
LastFetchLog string `json:"last_fetch_log"`
FetchedCerts []FetchedCACert `json:"fetched_certs"`
URLs []string `json:"urls"`
AutoUpdate bool `json:"auto_update"`
LastFetch time.Time `json:"last_fetch"`
LastFetchLog string `json:"last_fetch_log"`
FetchedCerts []FetchedCACert `json:"fetched_certs"`
}
// FetchedCACert — информация о последнем удачно скачанном сертификате.
@@ -110,6 +154,53 @@ type PostgresSettings struct {
DSN string `json:"dsn"`
}
// FancyMSSQLSettings — подключение к MSSQL-серверу клиента: канал обмена с
// Fancy (очередь запросов из ЛК + ответы «крана») и журнал обмена с НРД.
// Аутентификация — SQL-логин (SQL Server Authentication).
type FancyMSSQLSettings struct {
DBType string `json:"db_type"` // "mssql" | "postgres" (пусто = mssql)
Server string `json:"server"` // host или host\instance
Port int `json:"port"` // 1433 (MSSQL) / 5432 (Postgres)
User string `json:"user"` // логин
Password string `json:"password"` // пароль
Encrypt string `json:"encrypt"` // MSSQL: disable|false|true; Postgres: sslmode
// Обработка трансферных таблиц DEPO силами bj (вместо джобы SQL Agent bj2depo):
// bj сам вызывает процедуру DepoJobProc каждые DepoJobEverySec секунд.
DepoJobRun bool `json:"depo_job_run"`
DepoJobMode string `json:"depo_job_mode"` // bj | sql-auto | sql-manual (пусто = не выбран)
DepoJobCheck string `json:"depo_job_check"` // результат последней проверки готовности
DepoJobCheckOK bool `json:"depo_job_check_ok"`
DepoJobCheckAt time.Time `json:"depo_job_check_at"`
DepoJobEverySec int `json:"depo_job_every_sec"`
DepoJobProc string `json:"depo_job_proc"`
DepoJobRunAs string `json:"depo_job_run_as,omitempty"` // пользователь базы DEPO для EXECUTE AS процедуры (пусто = SYSDBA)
DepoJobLast string `json:"depo_job_last"` // последний результат (для карточки)
DepoJobLastAt time.Time `json:"depo_job_last_at"`
// Трансферные таблицы FTV_M2M* в базе DEPO (depotables.go).
DepoDatabase string `json:"depo_database"` // база DEPO; пусто = BJ_DEPO_DATA
IISContractMarker string `json:"iis_contract_marker,omitempty"` // начало названия договора ИИС в DEPO (через запятую); пусто = БРИИС
DepoTablesMode string `json:"depo_tables_mode"` // auto (создаёт bj) | script (администратор СУБД)
DepoTablesCheck string `json:"depo_tables_check"`
DepoTablesCheckOK bool `json:"depo_tables_check_ok"`
DepoTablesCheckAt time.Time `json:"depo_tables_check_at"`
}
// IsPostgres сообщает, выбран ли PostgreSQL (иначе — MSSQL по умолчанию).
func (s FancyMSSQLSettings) IsPostgres() bool { return s.DBType == "postgres" }
// DefaultPort возвращает порт по типу СУБД, если он не задан явно.
func (s FancyMSSQLSettings) DefaultPort() int {
if s.Port != 0 {
return s.Port
}
if s.IsPostgres() {
return 5432
}
return 1433
}
// CryptoSettings — путь к PKCS#11 модулю и тип провайдера.
type CryptoSettings struct {
Provider string `json:"provider"` // "stub" | "validata"
@@ -129,11 +220,44 @@ type NSDSettings struct {
DeponentCode string `json:"deponent_code"` // депкод, напр. MC0413600000
AccountID string `json:"account_id"` // депозитарный счёт
SectionID string `json:"section_id"` // раздел депозитарного счёта
// IGWv904 — ИШ обновлён до версии 107.0.0.904+ (Astra 107.0.907+): с этой
// версии в config.xml ОБЯЗАТЕЛЬНО указывать businessLine=MOST для M2M; на
// версиях младше — НЕ допускается. По умолчанию false (совместимо со старым
// ИШ — элемент не добавляется).
IGWv904 bool `json:"igw_v904"`
}
// LKSettings — настройки callback в ЛК клиента.
type LKSettings struct {
CallbackURL string `json:"callback_url"`
// PublicURL — адрес личного кабинета клиента (стенд/бой), куда оператор
// заходит подать тестовую заявку. Показывается ссылкой на дашборде.
PublicURL string `json:"lk_public_url"`
// APIKey — ключ доступа для модуля Личного кабинета клиента (bj-lk).
// Модуль ставится на веб-сервер клиента и обращается к bj-server по этому
// ключу (заголовок X-BJ-Key). Пустой ключ = модульный API выключен.
APIKey string `json:"lk_api_key"`
// Интеграция с ЛК (потребление back_office API платформы): два окружения.
ActiveEnv string `json:"lk_active_env"` // "test" | "prod"
Test LKEndpoint `json:"lk_test"`
Prod LKEndpoint `json:"lk_prod"`
// EmulateNSD — заявки ЛК проходят весь путь (сверка DEPO, блокировка QUIK, сделка,
// статус в ЛК), но в НРД не уходят: отправку и ответ контрагента имитирует bj.
// Для боевого ЛК при тестовом профиле ИШ имитация включается принудительно.
EmulateNSD bool `json:"lk_emulate_nsd"`
}
// LKEndpoint — реквизиты подключения к API ЛК (HTTP Basic) для одного окружения.
type LKEndpoint struct {
BaseURL string `json:"base_url"` // напр. https://fontvielle.ef.winr.pro/manager/api
Login string `json:"login"` // Basic-логин
Password string `json:"password"` // Basic-пароль
LastOK bool `json:"last_ok"` // результат последней «Проверить связь»
LastMsg string `json:"last_msg"` // текст результата
LastAt time.Time `json:"last_at"` // время последней проверки
}
// TestRunResult — результат последнего тестового прогона.
@@ -181,6 +305,21 @@ func (r *RuntimeConfig) Snapshot() Settings {
return out
}
// BrandingSettings — оформление отчётов (логотип организации в шапке).
type BrandingSettings struct {
LogoDataURI string `json:"logo_data_uri,omitempty"` // data:image/...;base64,... (пусто = встроенный логотип по умолчанию)
LogoName string `json:"logo_name,omitempty"` // имя загруженного файла (для UI)
}
// UpdateBranding сохраняет настройки оформления (логотип отчётов).
func (r *RuntimeConfig) UpdateBranding(s BrandingSettings) error {
r.mu.Lock()
r.data.Branding = s
r.data.UpdatedAt = time.Now().UTC()
r.mu.Unlock()
return r.save()
}
// UpdatePostgres сохраняет postgres-настройки.
func (r *RuntimeConfig) UpdatePostgres(s PostgresSettings) error {
r.mu.Lock()
@@ -190,6 +329,24 @@ func (r *RuntimeConfig) UpdatePostgres(s PostgresSettings) error {
return r.save()
}
// UpdateFancy сохраняет настройки подключения к MSSQL (канал обмена с Fancy).
func (r *RuntimeConfig) UpdateFancy(s FancyMSSQLSettings) error {
r.mu.Lock()
r.data.Fancy = s
r.data.UpdatedAt = time.Now().UTC()
r.mu.Unlock()
return r.save()
}
// UpdateQuik сохраняет настройки подключения к БД МНП QUIK.
func (r *RuntimeConfig) UpdateQuik(s QuikMNPSettings) error {
r.mu.Lock()
r.data.Quik = s
r.data.UpdatedAt = time.Now().UTC()
r.mu.Unlock()
return r.save()
}
// SaveLicense сохраняет лицензионные настройки.
func (r *RuntimeConfig) SaveLicense(s LicenseSettings) error {
r.mu.Lock()
@@ -223,6 +380,7 @@ func (r *RuntimeConfig) UpdateNSD(s NSDSettings) error {
r.data.NSD = s
r.data.UpdatedAt = time.Now().UTC()
r.mu.Unlock()
applyBusinessLine(s.IGWv904) // сразу применяем businessLine к генератору config.xml
return r.save()
}
@@ -285,6 +443,128 @@ func (r *RuntimeConfig) DismissNews(id string) error {
return r.save()
}
// AlertSettings — каналы внешней доставки системных оповещений (сверх ленты
// «Новости»). SMTP — почта оператору; Zabbix — trapper-хук в мониторинг.
type AlertSettings struct {
SMTP SMTPSettings `json:"smtp"`
Zabbix ZabbixSettings `json:"zabbix"`
}
// SMTPSettings — параметры отправки уведомлений по e-mail.
type SMTPSettings struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"` // 587 (STARTTLS), 465 (SSL), 25
Security string `json:"security"` // "starttls" | "ssl" | "none"
Username string `json:"username"`
Password string `json:"password"`
From string `json:"from"`
To string `json:"to"` // получатели через запятую
LastMsg string `json:"last_msg"`
LastAt time.Time `json:"last_at"`
}
// ZabbixSettings — trapper-хук: bj шлёт значение элемента данных на сервер/прокси
// Zabbix (протокол zabbix_sender). В Zabbix заводится host + item типа
// «Zabbix trapper» с указанным ключом; триггер по значению шлёт уведомления.
type ZabbixSettings struct {
Enabled bool `json:"enabled"`
Server string `json:"server"` // адрес сервера/прокси Zabbix
Port int `json:"port"` // 10051 по умолчанию
Host string `json:"host"` // имя хоста в Zabbix (host.host)
Key string `json:"key"` // ключ элемента-trapper, напр. bj.alert
LastMsg string `json:"last_msg"`
LastAt time.Time `json:"last_at"`
}
func (r *RuntimeConfig) UpdateAlerts(s AlertSettings) error {
r.mu.Lock()
r.data.Alerts = s
r.data.UpdatedAt = time.Now().UTC()
r.mu.Unlock()
return r.save()
}
// IncomingNotifySettings — уведомления депозитарию о входящих бумагах (перевод
// НАМ по M2M). Пока — письмо на почту «к вам переведена бумага, требует учёта»;
// позже здесь же сформируем пакет для загрузки в учётную систему.
type IncomingNotifySettings struct {
Enabled bool `json:"enabled"`
Emails string `json:"emails"` // депозитарий (через запятую)
EmailsBack string `json:"emails_back"` // бэк-офис
EmailsMiddle string `json:"emails_middle"` // мидл-офис
// Маршрутизация: тип уведомления → группы через запятую (depo,back,middle).
// Типы: incoming (входящие переводы), orders (сроки депо-поручений), quik (QUIK МНП), system (прочие).
Routes map[string]string `json:"routes"`
}
// Группы получателей и типы уведомлений (порядок — для формы).
var notifyGroups = []struct{ Key, Title string }{{"depo", "Депозитарий"}, {"back", "Бэк-офис"}, {"middle", "Мидл-офис"}}
var notifyKinds = []struct{ Key, Title string }{
{"incoming", "Входящие переводы (получен / принят)"},
{"orders", "Сроки депо-поручений НРД (MS010 5 мин / 30 дней)"},
{"quik", "QUIK МНП (ошибки блокировки / списания / зачисления)"},
{"system", "Прочие системные оповещения"},
}
// defaultNotifyRoutes — маршруты по умолчанию, пока оператор не настроил свои.
var defaultNotifyRoutes = map[string]string{"incoming": "depo,back", "orders": "depo,back", "quik": "back,middle", "system": "back"}
// RouteHas — получает ли группа уведомления данного типа.
func (s IncomingNotifySettings) RouteHas(kind, group string) bool {
r := s.Routes
if r == nil {
r = defaultNotifyRoutes
}
for _, g := range strings.Split(r[kind], ",") {
if strings.TrimSpace(g) == group {
return true
}
}
return false
}
// GroupEmails — адреса группы.
func (s IncomingNotifySettings) GroupEmails(group string) string {
switch group {
case "depo":
return s.Emails
case "back":
return s.EmailsBack
case "middle":
return s.EmailsMiddle
}
return ""
}
// Recipients — адреса всех групп, на которые направлен тип уведомления (без дублей).
func (s IncomingNotifySettings) Recipients(kind string) string {
seen := map[string]bool{}
var out []string
for _, g := range notifyGroups {
if !s.RouteHas(kind, g.Key) {
continue
}
for _, e := range strings.Split(s.GroupEmails(g.Key), ",") {
e = strings.TrimSpace(e)
if e == "" || seen[strings.ToLower(e)] {
continue
}
seen[strings.ToLower(e)] = true
out = append(out, e)
}
}
return strings.Join(out, ", ")
}
func (r *RuntimeConfig) UpdateIncoming(s IncomingNotifySettings) error {
r.mu.Lock()
r.data.Incoming = s
r.data.UpdatedAt = time.Now().UTC()
r.mu.Unlock()
return r.save()
}
func (r *RuntimeConfig) UpdateLK(s LKSettings) error {
r.mu.Lock()
r.data.LK = s