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:
@@ -3,8 +3,10 @@ package lkgateway
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter"
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/mock"
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml"
|
||||
)
|
||||
|
||||
// igwClientAdapter адаптирует igw.Client под узкий nsdadapter.IGWClient:
|
||||
@@ -51,6 +54,10 @@ type Server struct {
|
||||
// (M2MTransferDecision/Response) и применяет через svc.ApplyDecision.
|
||||
igwClient *igw.Client
|
||||
igwChannel string
|
||||
|
||||
// incoming — входящие переводы (M2MTransferRequest от робота/контрагента,
|
||||
// сценарий 3333), на которые мы отвечаем M2MTransferDecision.
|
||||
incoming *IncomingStore
|
||||
}
|
||||
|
||||
// NewServer собирает Server с репозиторием, mock NSDSender, SeedStore
|
||||
@@ -81,6 +88,7 @@ func NewServer(cfg ServerConfig) (*Server, error) {
|
||||
var igwChannel string
|
||||
{
|
||||
s := rc.Snapshot()
|
||||
applyBusinessLine(s.NSD.IGWv904) // config.xml businessLine=MOST для ИШ 107.0.0.904+
|
||||
if s.NSD.IGWBaseURL != "" && s.NSD.Profile != "" {
|
||||
prof, perr := nsdadapter.LookupProfile(s.NSD.Profile)
|
||||
if perr != nil {
|
||||
@@ -161,9 +169,32 @@ func NewServer(cfg ServerConfig) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
registerSetup(mux, adminTpl, rc, svc)
|
||||
registerM2MTest(mux, adminTpl, &setupHandlers{rc: rc, tpl: &adminTemplates{a: adminTpl}, svc: svc})
|
||||
incomingStore := NewIncomingStore()
|
||||
globalIncomingStore = incomingStore // для сводной выгрузки отчётов тест-плана
|
||||
registerIncoming(mux, adminTpl, incomingDeps{
|
||||
store: incomingStore,
|
||||
igwClient: igwClient,
|
||||
igwChan: igwChannel,
|
||||
rc: rc,
|
||||
ourCode: string(cfg.DefaultSender),
|
||||
})
|
||||
globalOutgoingStore = NewOutgoingStore()
|
||||
registerDepoSim(mux, adminTpl) // песочница сверки с DEPO (имитация ответа Fancy)
|
||||
registerOutgoing(mux, adminTpl, outgoingDeps{
|
||||
store: globalOutgoingStore,
|
||||
igwClient: igwClient,
|
||||
igwChan: igwChannel,
|
||||
})
|
||||
registerJournal(mux, adminTpl, svc, incomingStore)
|
||||
registerLKAPI(mux, rc, svc) // API для модуля личного кабинета клиента (bj-lk)
|
||||
registerHealth(mux)
|
||||
registerQuikOps(mux, rc, svc)
|
||||
registerNPR(mux, adminTpl, rc)
|
||||
registerAuth(mux, rc)
|
||||
registerSetCallback(mux, svc, rc)
|
||||
registerSeedListing(mux, store)
|
||||
registerHandbookRequest(mux, igwClient, igwChannel)
|
||||
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
@@ -174,9 +205,10 @@ func NewServer(cfg ServerConfig) (*Server, error) {
|
||||
mux: mux,
|
||||
igwClient: igwClient,
|
||||
igwChannel: igwChannel,
|
||||
incoming: incomingStore,
|
||||
server: &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
Handler: mux,
|
||||
Handler: authMiddleware(rc, mux),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
},
|
||||
}, nil
|
||||
@@ -210,21 +242,80 @@ func (s *Server) Mux() http.Handler { return s.mux }
|
||||
// Run поднимает HTTP-сервер и фоновый Decisions-consumer.
|
||||
// Блокируется до ctx.Done().
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
go s.consumeDecisions(ctx)
|
||||
// Монитор здоровья + systemd watchdog: следит за фоновыми горутинами,
|
||||
// поднимает алерт при зависании и «пинает» systemd, пока всё живо.
|
||||
healthRegister("poll-lk", time.Minute)
|
||||
go s.superviseHealth(ctx)
|
||||
|
||||
// Данные модуля личного кабинета — схема lk в PostgreSQL bj.
|
||||
if err := ensureLKSchema(ctx, s.rc); err != nil {
|
||||
log.Printf("lk-gateway: схема lk в PostgreSQL: %v", err)
|
||||
}
|
||||
|
||||
// Все фоновые горутины — под supervise: паника ловится recover(), пишется
|
||||
// в лог и в ленту, горутина перезапускается. Один сбой не роняет сервис.
|
||||
go superviseLoop(ctx, "consume-decisions", s.rc, s.consumeDecisions)
|
||||
|
||||
// Поллер входящих от НРД (только в реальном режиме ИШ): забирает
|
||||
// ответы робота/контрагента и применяет их через ApplyDecision.
|
||||
if s.igwClient != nil && s.igwChannel != "" {
|
||||
go s.pollIncoming(ctx)
|
||||
healthRegister("poll-incoming", 30*time.Second)
|
||||
go superviseLoop(ctx, "poll-incoming", s.rc, s.pollIncoming)
|
||||
}
|
||||
|
||||
// Имитатор НРД для заявок ЛК в тестовом режиме (lkemulate.go): решения контрагента
|
||||
// применяются тем же обработчиком, что настоящие ответы из ИШ.
|
||||
lkEmuSender = newLKEmuSender()
|
||||
go superviseLoop(ctx, "lk-emulator", s.rc, s.consumeEmulatedDecisions)
|
||||
// Отказы шлюза по заявкам ЛК: ждём ответ Fancy (DEPO) и только тогда ставим «Отклонена» в ЛК.
|
||||
go superviseLoop(ctx, "lk-flow", s.rc, s.lkFlowLoop)
|
||||
|
||||
// Поллер заявок ЛК ЕСИА Финанс: раз в минуту забирает новые depo_m2m и
|
||||
// отправляет в НРД (или имитатору). Интервал 1 мин — время ответов регламентировано.
|
||||
go superviseLoop(ctx, "poll-lk", s.rc, s.pollLKClaims)
|
||||
|
||||
// Авто-обновление справочника участников M2M раз в сутки (только реальный
|
||||
// ИШ). Тик суточный — в heartbeat-монитор не регистрируем, только recover.
|
||||
go superviseLoop(ctx, "handbook-refresh", s.rc, s.handbookRefreshLoop)
|
||||
|
||||
// QUIK МНП: автоматический цикл M2M (регистрация до отправки — хук в
|
||||
// SendBuiltRequest; подпись/снятие/зачисление по MS010 — опросчик).
|
||||
quikSendHook = func(ctx context.Context, req *m2m.M2MTransferRequest) error {
|
||||
return quikOutgoingBeforeSend(ctx, s.rc, req)
|
||||
}
|
||||
healthRegister("poll-quik", 30*time.Second)
|
||||
go superviseLoop(ctx, "poll-quik", s.rc, s.quikCycleLoop)
|
||||
|
||||
// Контроль сроков депо-поручений 16/16-1: MS010 за 5 мин, исполнение до 30 дней.
|
||||
healthRegister("order-watch", time.Minute)
|
||||
go superviseLoop(ctx, "order-watch", s.rc, s.orderWatchLoop)
|
||||
|
||||
// DEPO (bj2depo): события BJ_* в трансферные таблицы, чтение DEPO_*, справочник участников.
|
||||
sentHook = func(req *m2m.M2MTransferRequest) { depoOnOutgoingSent(s.rc, req) }
|
||||
healthRegister("depo-events", time.Minute)
|
||||
go superviseLoop(ctx, "depo-events", s.rc, s.depoEventsLoop)
|
||||
healthRegister("depo-job", 2*time.Minute)
|
||||
go superviseLoop(ctx, "depo-job", s.rc, s.depoJobLoop)
|
||||
|
||||
// Перепроверка групп AD раз в 15 мин (если задана сервисная учётная запись).
|
||||
// Тик длинный, контроллер может отвечать медленно — только recover, без heartbeat.
|
||||
go superviseLoop(ctx, "ad-recheck", s.rc, s.adRecheckLoop)
|
||||
|
||||
// Фоновая авто-проверка обновлений из артефактории (если включена).
|
||||
go NewUpdater(s.rc).updateLoop(ctx)
|
||||
|
||||
// Лицензия: ежесуточное продление ключа на сервере лицензий lic.zetit.ru.
|
||||
go superviseLoop(ctx, "license", s.rc, s.licenseLoop)
|
||||
|
||||
// Авто-обновление сертификатов УЦ раз в сутки (если оператор включил).
|
||||
stopCACerts := StartCACertsAutoUpdater(s.rc)
|
||||
defer stopCACerts()
|
||||
|
||||
// Доверенные сертификаты НРД: вшитый комплект → справочник активного
|
||||
// профиля ИШ (добавление/плановое удаление, с бэкапом и откатом).
|
||||
stopNSDCerts := StartNSDCertsSync(s.rc)
|
||||
defer stopNSDCerts()
|
||||
|
||||
// Doc-watcher: раз в сутки проверяет сайт НРД на новые PDF и
|
||||
// эмитирует новости в ленту. Дефолтные источники + дефолтные
|
||||
// новости (окно техработ TEST3, появление робота) сеются один раз.
|
||||
@@ -278,6 +369,16 @@ func (s *Server) consumeDecisions(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// applyBusinessLine синхронизирует флаг «ИШ 107.0.0.904+» с генератором
|
||||
// config.xml: v904 → businessLine=MOST, иначе элемент опускается.
|
||||
func applyBusinessLine(v904 bool) {
|
||||
if v904 {
|
||||
igw.BusinessLine = "MOST"
|
||||
} else {
|
||||
igw.BusinessLine = ""
|
||||
}
|
||||
}
|
||||
|
||||
func registerHealth(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -285,6 +386,74 @@ func registerHealth(mux *http.ServeMux) {
|
||||
})
|
||||
}
|
||||
|
||||
// sendHandbookRequest собирает и отправляет в ИШ запрос справочника (#M2MHR).
|
||||
func sendHandbookRequest(ctx context.Context, igwClient *igw.Client, channel string) (string, error) {
|
||||
if igwClient == nil || channel == "" {
|
||||
return "", fmt.Errorf("ИШ не настроен (реальный режим НРД выключен)")
|
||||
}
|
||||
xmlBytes, err := nsdxml.Marshal(m2m.M2MTransferHandbookRequest{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("сборка XML: %w", err)
|
||||
}
|
||||
zip, err := igw.PackXML(xmlBytes, "M2MTransferHandbookRequest.xml", "#M2MHR")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("упаковка: %w", err)
|
||||
}
|
||||
return igwClient.SendPackage(ctx, channel, "#M2MHR", zip)
|
||||
}
|
||||
|
||||
// registerHandbookRequest — POST /admin/setup/handbook-request: ручной запрос
|
||||
// актуального справочника участников M2M. Ответ #M2MTH разберёт поллер.
|
||||
func registerHandbookRequest(mux *http.ServeMux, igwClient *igw.Client, channel string) {
|
||||
mux.HandleFunc("/admin/setup/handbook-request", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
||||
defer cancel()
|
||||
id, err := sendHandbookRequest(ctx, igwClient, channel)
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Запрос справочника: "+err.Error())
|
||||
return
|
||||
}
|
||||
setupFlash(w, r, "Запрос справочника участников M2M отправлен (#M2MHR, пакет id="+id+"). Ответ придёт во входящие через ~30-60с — список появится в логе и в файле m2m-handbook.json.")
|
||||
})
|
||||
}
|
||||
|
||||
// handbookRefreshLoop раз в сутки обновляет справочник участников M2M, чтобы он
|
||||
// оставался актуальным. Первый запрос — вскоре после старта, если справочника
|
||||
// ещё нет или он старше суток. Работает только в реальном режиме ИШ.
|
||||
func (s *Server) handbookRefreshLoop(ctx context.Context) {
|
||||
if s.igwClient == nil || s.igwChannel == "" {
|
||||
<-ctx.Done() // нечего делать в mock-режиме, но не выходим (иначе supervise перезапустит)
|
||||
return
|
||||
}
|
||||
const interval = 24 * time.Hour
|
||||
initial := time.Minute
|
||||
if fi, err := os.Stat(handbookFile); err == nil && time.Since(fi.ModTime()) < interval {
|
||||
initial = interval - time.Since(fi.ModTime()) // справочник свежий — ждём до суток
|
||||
}
|
||||
timer := time.NewTimer(initial)
|
||||
defer timer.Stop()
|
||||
log.Printf("lk-gateway: авто-обновление справочника M2M включено (интервал %s, первый запрос через %s)", interval, initial.Round(time.Second))
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
cctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
if id, err := sendHandbookRequest(cctx, s.igwClient, s.igwChannel); err != nil {
|
||||
log.Printf("lk-gateway: авто-обновление справочника: %v", err)
|
||||
} else {
|
||||
log.Printf("lk-gateway: авто-обновление справочника: запрос отправлен (#M2MHR id=%s)", id)
|
||||
}
|
||||
cancel()
|
||||
timer.Reset(interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// registerSetCallback — служебный POST /admin/api/callback-url для
|
||||
// эмулятора ЛК, чтобы сообщить gateway свой URL. Если URL уже сохранён
|
||||
// в runtime-конфиге (пользователь явно настроил его через UI), запрос
|
||||
|
||||
Reference in New Issue
Block a user