From 5beb726e5bf842c0456b93b4ce2c3dd3b36f654b Mon Sep 17 00:00:00 2001 From: fontvielle Date: Thu, 15 Oct 2026 11:18:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=20M2M:=20=D1=80=D1=83=D1=87=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D0=B0=20=D0=B2=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D0=B0=D1=82=D0=B5=20=D0=9D=D0=A0=D0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Форма «Загрузка справочника из файла» на /admin/handbook принимает подлинный формат НРД (hk:M2MTransferHandbook, windows-1251 — тот же, что приходит пакетом #M2MTH и который отдаёт кнопка «XML»). Разбор и сохранение идут тем же путём, что у поллера при ответе НРД: nsdxml.Unmarshal → writeHandbookRawXML (сырой XML для байт-в-байт выгрузки) → saveHandbookEx (плоский JSON) → перезапись FTV_M2M_HANDBOOK в DEPO → синхронизация получателей в МНП. На сервер лицензий ручной файл НЕ рассылается (saveHandbookEx push=false): справочник одного стенда не должен становиться облачным для всех клиентов. Нужно для закрытого контура и когда шлюз к НРД недоступен. - handbook_import.go: обработчик importHandbook (POST /admin/handbook/import) - nsdpoller.go: saveHandbook → saveHandbookEx(hb, push) - admin.go: маршрут import, поле Flash, renderHandbook читает ?flash= - admin_handbook.html: карточка загрузки + вывод flash Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XG3zNMMf2LMoDpXo22mwMX --- internal/lkgateway/admin.go | 12 ++- internal/lkgateway/handbook_import.go | 83 +++++++++++++++++++ internal/lkgateway/nsdpoller.go | 11 ++- .../web/templates/admin_handbook.html | 14 ++++ 4 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 internal/lkgateway/handbook_import.go diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index 067e265..307d53a 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -511,7 +511,7 @@ type claimData struct { // клиента. SubstAs — под чьими данными ушло; этот XML в карточке — наша запись с реальным клиентом. SubstAs string Flash string - Fail *SendFailure // последняя неудачная отправка в НРД (sendfail.go) + Fail *SendFailure // последняя неудачная отправка в НРД (sendfail.go) } // statusData — данные страницы статуса. @@ -570,6 +570,8 @@ func RegisterAdmin(mux *http.ServeMux, svc *Service, rc *RuntimeConfig, getOpts a.exportHandbook(w, "json") case p == "handbook/export.xml": a.exportHandbook(w, "xml") + case p == "handbook/import": + a.importHandbook(w, r) case p == "handbook": a.renderHandbook(w, r) case p == "help": @@ -773,8 +775,8 @@ func (a *admin) renderClaim(w http.ResponseWriter, r *http.Request, svc *Service } render(w, a.claim, claimData{ Quik: quik, Watch: watch, DepoL: depoL, Fail: fail, - page: nowPage("Заявка", "claims"), - Claim: view, Depo: depo, SubstAs: substAs, Flash: r.URL.Query().Get("flash"), + page: nowPage("Заявка", "claims"), + Claim: view, Depo: depo, SubstAs: substAs, Flash: r.URL.Query().Get("flash"), }) } @@ -848,6 +850,7 @@ func (a *admin) downloadClaimXML(w http.ResponseWriter, r *http.Request, svc *Se type handbookData struct { page At string + Flash string Participants []HandbookEntry CodesHTML template.HTML // справка по кодам ошибок/отказов M2M Result HandbookResult // итог обработки справочника джобой Fancy bj2depo @@ -855,8 +858,9 @@ type handbookData struct { // renderHandbook показывает сохранённый справочник участников M2M с поиском. // Места расчётов добавляются к участникам как строки с типом "place". -func (a *admin) renderHandbook(w http.ResponseWriter, _ *http.Request) { +func (a *admin) renderHandbook(w http.ResponseWriter, r *http.Request) { data := handbookData{page: nowPage("Справочник участников", "handbook")} + data.Flash = r.URL.Query().Get("flash") data.CodesHTML = template.HTML(m2mSpravkaHTML(false)) // справка по кодам ошибок data.Result = depoHandbookResult(globalRC) if raw, err := readHandbook(); err == nil { diff --git a/internal/lkgateway/handbook_import.go b/internal/lkgateway/handbook_import.go new file mode 100644 index 0000000..8938d6b --- /dev/null +++ b/internal/lkgateway/handbook_import.go @@ -0,0 +1,83 @@ +package lkgateway + +// handbook_import.go — ручная загрузка справочника участников M2M со страницы +// /admin/handbook файлом в ПОДЛИННОМ формате НРД (hk:M2MTransferHandbook, +// windows-1251 — тот же, что НРД присылает пакетом #M2MTH и который отдаёт +// кнопка «XML»). Нужна для закрытого контура и когда шлюз к НРД недоступен. +// +// Обрабатываем ровно тем же путём, что и поллер при ответе НРД (nsdpoller.go): +// writeHandbookRawXML → saveHandbookEx(push=false) → перезапись FTV_M2M_HANDBOOK +// в DEPO → синхронизация получателей в МНП. +// На сервер лицензий файл НЕ рассылаем: ручная загрузка одного стенда не должна +// становиться облачным справочником для всех клиентов (push=false). Отметку живой +// связи с НРД тоже не ставим — это действие человека, а не ответ шлюза. + +import ( + "fmt" + "io" + "log" + "net/http" + "net/url" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m" + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml" +) + +// handbookUploadMaxBytes — предел размера загружаемого файла. Справочник НРД — +// сотни КБ; с запасом. +const handbookUploadMaxBytes = 16 << 20 + +// importHandbook — POST /admin/handbook/import: принять файл справочника в формате НРД. +func (a *admin) importHandbook(w http.ResponseWriter, r *http.Request) { + back := func(msg string) { + http.Redirect(w, r, "/admin/handbook?flash="+url.QueryEscape(msg), http.StatusSeeOther) + } + if r.Method != http.MethodPost { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + r.Body = http.MaxBytesReader(w, r.Body, handbookUploadMaxBytes) + f, hdr, err := r.FormFile("handbook") + if err != nil { + back("Загрузка справочника: файл не получен (" + err.Error() + ")") + return + } + defer f.Close() + raw, err := io.ReadAll(f) + if err != nil { + back("Загрузка справочника: не удалось прочитать файл: " + err.Error()) + return + } + if len(raw) == 0 { + back("Загрузка справочника: файл пустой") + return + } + // Разбираем тем же парсером НРД, что и ответ шлюза (поддерживает windows-1251 по прологу). + var hb m2m.M2MTransferHandbook + if err := nsdxml.Unmarshal(raw, &hb); err != nil { + back("Загрузка справочника: это не справочник НРД (hk:M2MTransferHandbook) или файл повреждён: " + err.Error()) + return + } + nP, nPl := len(hb.Participants.Participants), len(hb.SettlementPlaces.Places) + if nP == 0 { + back("Загрузка справочника: в файле нет ни одного участника — проверьте, тот ли это файл") + return + } + writeHandbookRawXML(raw) // подлинный XML — для выгрузки кнопкой «XML» байт в байт + changed := saveHandbookEx(&hb, false) // плоский JSON; на сервер лицензий НЕ рассылаем + if globalRC != nil { + go func(rc *RuntimeConfig) { + if err := depoWriteHandbook(rc); err != nil { + log.Printf("depo-events: справочник после ручной загрузки: %v", err) + } + }(globalRC) + quikRecipientsSyncAsync(globalRC) // депозитарии-получатели в МНП + } + note := "" + if !changed { + note = " (состав не изменился)" + } + log.Printf("lk-gateway: справочник M2M загружен ВРУЧНУЮ из файла %q: участников %d, мест расчётов %d, изменился=%v", + hdr.Filename, nP, nPl, changed) + back(fmt.Sprintf("Справочник загружен из файла %s: участников %d, мест расчётов %d%s.", hdr.Filename, nP, nPl, note)) +} diff --git a/internal/lkgateway/nsdpoller.go b/internal/lkgateway/nsdpoller.go index b5960c5..2604667 100644 --- a/internal/lkgateway/nsdpoller.go +++ b/internal/lkgateway/nsdpoller.go @@ -305,7 +305,12 @@ type HandbookEntry struct { // saveHandbook раскладывает справочник в плоский список и пишет в файл. Возвращает, изменился ли // состав: справочник запрашивается раз в 10 минут (сквозная проверка связи, nsdhealth.go), и без // изменений его не нужно ни рассылать, ни переписывать в DEPO. -func saveHandbook(hb *m2m.M2MTransferHandbook) (changed bool) { +func saveHandbook(hb *m2m.M2MTransferHandbook) (changed bool) { return saveHandbookEx(hb, true) } + +// saveHandbookEx — как saveHandbook, но push управляет рассылкой справочника на сервер +// лицензий. Ручная загрузка файла (handbook_import.go) идёт с push=false: справочник +// одного стенда не должен становиться облачным для всех клиентов. +func saveHandbookEx(hb *m2m.M2MTransferHandbook, push bool) (changed bool) { out := struct { At string `json:"at"` Participants []HandbookEntry `json:"participants"` @@ -350,7 +355,9 @@ func saveHandbook(hb *m2m.M2MTransferHandbook) (changed bool) { for _, e := range out.Participants { log.Printf("lk-gateway: справочник M2M: ИНН %s → %s [%s] %s", e.INN, e.Code, e.Kind, e.Name) } - pushHandbookAsync(globalRC, raw) // свежий справочник — на сервер лицензий для всех клиентов + if push { + pushHandbookAsync(globalRC, raw) // свежий справочник — на сервер лицензий для всех клиентов + } } } return changed diff --git a/internal/lkgateway/web/templates/admin_handbook.html b/internal/lkgateway/web/templates/admin_handbook.html index 6e022e9..9244080 100644 --- a/internal/lkgateway/web/templates/admin_handbook.html +++ b/internal/lkgateway/web/templates/admin_handbook.html @@ -1,4 +1,9 @@ {{define "content"}} +{{if .Flash}} +
+
{{.Flash}}
+
+{{end}}

Справочник участников M2M

Актуальная карта участников сервиса MOEX МОСТ (получена от НРД пакетом #M2MTH): ИНН, код участника (MC/EC/…), тип и наименование. Используется для сопоставления контрагента с кодом участника. @@ -12,6 +17,15 @@ {{end}}

+
+

Загрузка справочника из файла

+

Если шлюз к НРД недоступен, справочник можно загрузить файлом в формате НРД (hk:M2MTransferHandbook, windows-1251 — тот же, что приходит пакетом #M2MTH и который отдаёт кнопка «XML»). Файл заменит текущий справочник, перезапишет таблицу в DEPO и обновит получателей в МНП.

+
+ + +
+
+ {{if or .Result.Found .Result.Err}}

Результат обработки в Fancy