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(`` + "\n") b.WriteString(`` + "\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(" " + xmlEsc(ts) + "\n") b.WriteString(" \n") for _, p := range hb.Places { b.WriteString(" \n") b.WriteString(" " + xmlEsc(p.INN) + "\n") if p.Name != "" { b.WriteString(" " + xmlEsc(p.Name) + "\n") } if p.DisplayName != "" { b.WriteString(" " + xmlEsc(p.DisplayName) + "\n") } b.WriteString(" \n") } b.WriteString(" \n") b.WriteString(" \n") for _, p := range hb.Participants { b.WriteString(" \n") b.WriteString(" " + xmlEsc(p.INN) + "\n") b.WriteString(" \n") b.WriteString(" \n") if p.FullName != "" { b.WriteString(" " + xmlEsc(p.FullName) + "\n") } if p.Name != "" { b.WriteString(" " + xmlEsc(p.Name) + "\n") } if p.DisplayName != "" { b.WriteString(" " + xmlEsc(p.DisplayName) + "\n") } b.WriteString(" \n") if p.EngName != "" { b.WriteString(" \n") b.WriteString(" " + xmlEsc(p.EngName) + "\n") b.WriteString(" \n") } b.WriteString(" \n") switch p.Kind { case "broker": b.WriteString(" \n") b.WriteString(" " + xmlEsc(p.Code) + "\n") b.WriteString(" \n") default: // depo и прочее if p.Code != "" { b.WriteString(" \n") b.WriteString(" " + xmlEsc(p.Code) + "\n") b.WriteString(" \n") } } b.WriteString(" \n") } b.WriteString(" \n") b.WriteString("\n") return []byte(b.String()) } // xmlEsc — XML-экранирование текстового значения. func xmlEsc(s string) string { var b bytes.Buffer _ = xml.EscapeText(&b, []byte(s)) return b.String() }