Files
Bridge-and-Join-s/internal/lkgateway/handbook_export.go
T
fontvielle 567706e0a1 Справочник: выгрузка XML в подлинном формате НРД; фикс ошибочного FIFO-сопоставления ответа
1) XML-выгрузка справочника (/admin/handbook → XML) теперь в формате НРД
   (hk:M2MTransferHandbook: SettlementPlaces/Participants, Rus/Eng,
   Depository/BrokerPlace). Поллер сохраняет подлинный XML от НРД
   (writeHandbookRawXML → m2m-handbook.xml) и выгрузка отдаёт его как есть
   (windows-1251, байт в байт). Для установок без своего шлюза — реконструкция
   того же формата из плоского JSON (reconstructHandbookXML), проверено
   парсером НРД.

2) findDealForResponse: ответ сервиса МОСТ с конкретным GUID, которого нет
   среди ИСХОДЯЩИХ сделок (например M2M31 на наше решение по ВХОДЯЩЕМУ
   переводу), больше НЕ пришивается по эвристике FIFO к чужой исходящей
   заявке и не отклоняет её ошибочно. FIFO остаётся только для ответа с
   нулевым/пустым GUID (поведение НРД при M2M14).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-14 19:10:18 +03:00

176 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// handbook_export.go — выгрузка справочника участников M2M со страницы
// /admin/handbook в файлах JSON и XML.
//
// JSON — наш плоский вид (readHandbook: от НРД #M2MTH, копия с сервера лицензий
// или встроенный снимок).
// XML — в ПОДЛИННОМ формате НРД (hk:M2MTransferHandbook). Источник по порядку:
// 1) сырой XML, полученный этим bj от НРД (handbookXMLFile) — отдаём как есть
// (он уже в кодировке windows-1251, с SettlementPlaces/Participants,
// Rus/Eng, Depository/BrokerPlace — байт в байт как прислал НРД);
// 2) если сырого нет (установка без своего шлюза) — реконструкция того же
// формата из плоского JSON (с потерями: Eng — только DisplayName, место —
// по типу записи), UTF-8.
// Доступ — под админ-авторизацией /admin/*.
import (
"bytes"
"encoding/json"
"encoding/xml"
"net/http"
"os"
"strings"
"time"
)
// handbookXMLFile — сырой XML справочника, как его прислал НРД (#M2MTH). Пишется
// поллером при получении (writeHandbookRawXML).
const handbookXMLFile = "/var/lib/bj/.bj/m2m-handbook.xml"
// writeHandbookRawXML сохраняет подлинный XML справочника от НРД (для выгрузки в
// исходном формате). Перезаписывает только при изменении содержимого.
func writeHandbookRawXML(raw []byte) {
if len(raw) == 0 {
return
}
if old, err := os.ReadFile(handbookXMLFile); err == nil && bytes.Equal(old, raw) {
return
}
_ = os.WriteFile(handbookXMLFile, raw, 0o600)
}
// exportHandbook отдаёт сохранённый справочник участников M2M файлом JSON или XML.
func (a *admin) exportHandbook(w http.ResponseWriter, format string) {
day := time.Now().Format("2006-01-02")
if format == "xml" {
// 1) Подлинный XML от НРД — отдаём как есть (windows-1251, формат НРД).
if raw, err := os.ReadFile(handbookXMLFile); err == nil && len(raw) > 0 {
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("Content-Disposition", `attachment; filename="m2m-handbook-`+day+`.xml"`)
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(raw)
return
}
// 2) Сырого нет — реконструируем формат НРД из плоского JSON.
raw, err := readHandbook()
if err != nil || len(raw) == 0 {
http.Error(w, "Справочник ещё не загружен — запросите его у НРД.", http.StatusNotFound)
return
}
var hb flatHandbook
if err := json.Unmarshal(raw, &hb); err != nil {
http.Error(w, "Справочник повреждён: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="m2m-handbook-`+day+`.xml"`)
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(reconstructHandbookXML(hb))
return
}
// JSON — отдаём сохранённый справочник как есть, с отступами для читаемости.
raw, err := readHandbook()
if err != nil || len(raw) == 0 {
http.Error(w, "Справочник ещё не загружен — запросите его у НРД.", http.StatusNotFound)
return
}
var buf bytes.Buffer
if json.Indent(&buf, raw, "", " ") != nil {
buf.Reset()
buf.Write(raw)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="m2m-handbook-`+day+`.json"`)
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(buf.Bytes())
}
// flatHandbook — наш плоский формат хранения (handbookFile).
type flatHandbook struct {
At string `json:"at"`
Participants []HandbookEntry `json:"participants"`
Places []HandbookEntry `json:"settlement_places"`
}
// reconstructHandbookXML собирает XML в формате НРД (hk:M2MTransferHandbook) из
// плоского JSON — запасной путь, когда подлинного XML от НРД нет. С потерями:
// у участника восстанавливается одно место (Depository или Broker — по типу
// записи), из английских наименований — только DisplayName.
func reconstructHandbookXML(hb flatHandbook) []byte {
var b strings.Builder
b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + "\n")
b.WriteString(`<hk:M2MTransferHandbook xmlns:hk="http://nsd.ru/schemas/m2m/handbook" xmlns:m2m="http://nsd.ru/schemas/m2m/types" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">` + "\n")
msk := time.FixedZone("МСК", 3*3600)
ts := time.Now().In(msk).Format("2006-01-02T15:04:05") + "(МСК)" // формат метки времени НРД
if hb.At != "" {
if t, err := time.Parse(time.RFC3339, hb.At); err == nil {
ts = t.In(msk).Format("2006-01-02T15:04:05") + "(МСК)"
}
}
b.WriteString(" <hk:CreationTimestamp>" + xmlEsc(ts) + "</hk:CreationTimestamp>\n")
b.WriteString(" <hk:SettlementPlaces>\n")
for _, p := range hb.Places {
b.WriteString(" <m2m:Place>\n")
b.WriteString(" <m2m:INN>" + xmlEsc(p.INN) + "</m2m:INN>\n")
if p.Name != "" {
b.WriteString(" <m2m:ShortName>" + xmlEsc(p.Name) + "</m2m:ShortName>\n")
}
if p.DisplayName != "" {
b.WriteString(" <m2m:DisplayName>" + xmlEsc(p.DisplayName) + "</m2m:DisplayName>\n")
}
b.WriteString(" </m2m:Place>\n")
}
b.WriteString(" </hk:SettlementPlaces>\n")
b.WriteString(" <hk:Participants>\n")
for _, p := range hb.Participants {
b.WriteString(" <m2m:Participant>\n")
b.WriteString(" <m2m:INN>" + xmlEsc(p.INN) + "</m2m:INN>\n")
b.WriteString(" <m2m:Names>\n")
b.WriteString(" <m2m:Rus>\n")
if p.FullName != "" {
b.WriteString(" <m2m:FullName>" + xmlEsc(p.FullName) + "</m2m:FullName>\n")
}
if p.Name != "" {
b.WriteString(" <m2m:ShortName>" + xmlEsc(p.Name) + "</m2m:ShortName>\n")
}
if p.DisplayName != "" {
b.WriteString(" <m2m:DisplayName>" + xmlEsc(p.DisplayName) + "</m2m:DisplayName>\n")
}
b.WriteString(" </m2m:Rus>\n")
if p.EngName != "" {
b.WriteString(" <m2m:Eng>\n")
b.WriteString(" <m2m:DisplayName>" + xmlEsc(p.EngName) + "</m2m:DisplayName>\n")
b.WriteString(" </m2m:Eng>\n")
}
b.WriteString(" </m2m:Names>\n")
switch p.Kind {
case "broker":
b.WriteString(" <m2m:BrokerPlace>\n")
b.WriteString(" <m2m:ParticipantCode>" + xmlEsc(p.Code) + "</m2m:ParticipantCode>\n")
b.WriteString(" </m2m:BrokerPlace>\n")
default: // depo и прочее
if p.Code != "" {
b.WriteString(" <m2m:DepositoryPlace>\n")
b.WriteString(" <m2m:ParticipantCode>" + xmlEsc(p.Code) + "</m2m:ParticipantCode>\n")
b.WriteString(" </m2m:DepositoryPlace>\n")
}
}
b.WriteString(" </m2m:Participant>\n")
}
b.WriteString(" </hk:Participants>\n")
b.WriteString("</hk:M2MTransferHandbook>\n")
return []byte(b.String())
}
// xmlEsc — XML-экранирование текстового значения.
func xmlEsc(s string) string {
var b bytes.Buffer
_ = xml.EscapeText(&b, []byte(s))
return b.String()
}