Справочник M2M: ручная загрузка файла в формате НРД
Форма «Загрузка справочника из файла» на /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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XG3zNMMf2LMoDpXo22mwMX
This commit is contained in:
@@ -511,7 +511,7 @@ type claimData struct {
|
|||||||
// клиента. SubstAs — под чьими данными ушло; этот XML в карточке — наша запись с реальным клиентом.
|
// клиента. SubstAs — под чьими данными ушло; этот XML в карточке — наша запись с реальным клиентом.
|
||||||
SubstAs string
|
SubstAs string
|
||||||
Flash string
|
Flash string
|
||||||
Fail *SendFailure // последняя неудачная отправка в НРД (sendfail.go)
|
Fail *SendFailure // последняя неудачная отправка в НРД (sendfail.go)
|
||||||
}
|
}
|
||||||
|
|
||||||
// statusData — данные страницы статуса.
|
// statusData — данные страницы статуса.
|
||||||
@@ -570,6 +570,8 @@ func RegisterAdmin(mux *http.ServeMux, svc *Service, rc *RuntimeConfig, getOpts
|
|||||||
a.exportHandbook(w, "json")
|
a.exportHandbook(w, "json")
|
||||||
case p == "handbook/export.xml":
|
case p == "handbook/export.xml":
|
||||||
a.exportHandbook(w, "xml")
|
a.exportHandbook(w, "xml")
|
||||||
|
case p == "handbook/import":
|
||||||
|
a.importHandbook(w, r)
|
||||||
case p == "handbook":
|
case p == "handbook":
|
||||||
a.renderHandbook(w, r)
|
a.renderHandbook(w, r)
|
||||||
case p == "help":
|
case p == "help":
|
||||||
@@ -773,8 +775,8 @@ func (a *admin) renderClaim(w http.ResponseWriter, r *http.Request, svc *Service
|
|||||||
}
|
}
|
||||||
render(w, a.claim, claimData{
|
render(w, a.claim, claimData{
|
||||||
Quik: quik, Watch: watch, DepoL: depoL, Fail: fail,
|
Quik: quik, Watch: watch, DepoL: depoL, Fail: fail,
|
||||||
page: nowPage("Заявка", "claims"),
|
page: nowPage("Заявка", "claims"),
|
||||||
Claim: view, Depo: depo, SubstAs: substAs, Flash: r.URL.Query().Get("flash"),
|
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 {
|
type handbookData struct {
|
||||||
page
|
page
|
||||||
At string
|
At string
|
||||||
|
Flash string
|
||||||
Participants []HandbookEntry
|
Participants []HandbookEntry
|
||||||
CodesHTML template.HTML // справка по кодам ошибок/отказов M2M
|
CodesHTML template.HTML // справка по кодам ошибок/отказов M2M
|
||||||
Result HandbookResult // итог обработки справочника джобой Fancy bj2depo
|
Result HandbookResult // итог обработки справочника джобой Fancy bj2depo
|
||||||
@@ -855,8 +858,9 @@ type handbookData struct {
|
|||||||
|
|
||||||
// renderHandbook показывает сохранённый справочник участников M2M с поиском.
|
// renderHandbook показывает сохранённый справочник участников M2M с поиском.
|
||||||
// Места расчётов добавляются к участникам как строки с типом "place".
|
// Места расчётов добавляются к участникам как строки с типом "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 := handbookData{page: nowPage("Справочник участников", "handbook")}
|
||||||
|
data.Flash = r.URL.Query().Get("flash")
|
||||||
data.CodesHTML = template.HTML(m2mSpravkaHTML(false)) // справка по кодам ошибок
|
data.CodesHTML = template.HTML(m2mSpravkaHTML(false)) // справка по кодам ошибок
|
||||||
data.Result = depoHandbookResult(globalRC)
|
data.Result = depoHandbookResult(globalRC)
|
||||||
if raw, err := readHandbook(); err == nil {
|
if raw, err := readHandbook(); err == nil {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -305,7 +305,12 @@ type HandbookEntry struct {
|
|||||||
// saveHandbook раскладывает справочник в плоский список и пишет в файл. Возвращает, изменился ли
|
// saveHandbook раскладывает справочник в плоский список и пишет в файл. Возвращает, изменился ли
|
||||||
// состав: справочник запрашивается раз в 10 минут (сквозная проверка связи, nsdhealth.go), и без
|
// состав: справочник запрашивается раз в 10 минут (сквозная проверка связи, nsdhealth.go), и без
|
||||||
// изменений его не нужно ни рассылать, ни переписывать в DEPO.
|
// изменений его не нужно ни рассылать, ни переписывать в 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 {
|
out := struct {
|
||||||
At string `json:"at"`
|
At string `json:"at"`
|
||||||
Participants []HandbookEntry `json:"participants"`
|
Participants []HandbookEntry `json:"participants"`
|
||||||
@@ -350,7 +355,9 @@ func saveHandbook(hb *m2m.M2MTransferHandbook) (changed bool) {
|
|||||||
for _, e := range out.Participants {
|
for _, e := range out.Participants {
|
||||||
log.Printf("lk-gateway: справочник M2M: ИНН %s → %s [%s] %s", e.INN, e.Code, e.Kind, e.Name)
|
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
|
return changed
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
|
{{if .Flash}}
|
||||||
|
<div class="card" style="border-left:4px solid var(--accent,#2d6cdf)">
|
||||||
|
<div style="font-size:14px">{{.Flash}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Справочник участников M2M</h2>
|
<h2>Справочник участников M2M</h2>
|
||||||
<p class="muted">Актуальная карта участников сервиса MOEX МОСТ (получена от НРД пакетом <code>#M2MTH</code>): ИНН, код участника (MC/EC/…), тип и наименование. Используется для сопоставления контрагента с кодом участника.
|
<p class="muted">Актуальная карта участников сервиса MOEX МОСТ (получена от НРД пакетом <code>#M2MTH</code>): ИНН, код участника (MC/EC/…), тип и наименование. Используется для сопоставления контрагента с кодом участника.
|
||||||
@@ -12,6 +17,15 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 style="margin-top:0">Загрузка справочника из файла</h2>
|
||||||
|
<p class="muted" style="margin-top:0">Если шлюз к НРД недоступен, справочник можно загрузить файлом в формате НРД (<code>hk:M2MTransferHandbook</code>, windows-1251 — тот же, что приходит пакетом <code>#M2MTH</code> и который отдаёт кнопка «XML»). Файл заменит текущий справочник, перезапишет таблицу в DEPO и обновит получателей в МНП.</p>
|
||||||
|
<form method="post" action="/admin/handbook/import" enctype="multipart/form-data" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||||
|
<input type="file" name="handbook" accept=".xml,text/xml,application/xml" required>
|
||||||
|
<button class="btn" type="submit">Загрузить</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{if or .Result.Found .Result.Err}}
|
{{if or .Result.Found .Result.Err}}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 style="margin-top:0">Результат обработки в Fancy</h2>
|
<h2 style="margin-top:0">Результат обработки в Fancy</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user