From 07866526eb7d3bf161343c126f929b78c5a976f1 Mon Sep 17 00:00:00 2001 From: fontvielle Date: Wed, 21 Oct 2026 18:01:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=BD=D0=BA=D0=B5=D1=82=D0=B0=20=D1=83?= =?UTF-8?q?=D1=87=D0=B0=D1=81=D1=82=D0=BD=D0=B8=D0=BA=D0=B0:=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=20=D0=9D=D0=A0=D0=94=20(M2MER=20=D1=81=20=D0=BD=D1=83=D0=BB?= =?UTF-8?q?=D0=B5=D0=B2=D1=8B=D0=BC=20GUID)=20=D0=B8=20=D0=BF=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BE=D1=82=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BA=D1=83/=D0=BE=D1=82=D0=B2=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ответ НРД на анкету приходит как M2MTransferResponse (M2MER) с нулевым GUID — раньше он уходил в обработку перевода, «сделка не найдена», и отбрасывался. Теперь нулевой GUID распознаётся как ответ на регистрацию участника: - сырой XML сохраняется (captureNSDReply → nsd-replies); - отправка анкеты и ответ НРД пишутся в participant-reg.json (время, пакет, наименование, статус, коды, текст НРД); - на странице «Регистрация участника M2M» показываем: когда отправили и вердикт НРД («принял»/«отклонил» с текстом, напр. M2M37 «Некорректно указано наименование участника в анкете»). Текст берётся из самого ответа (serviceErrorTexts) — карту кодов не трогаем. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn --- deploy/release/component-versions.json | 4 +- internal/lkgateway/nsdpoller.go | 10 ++++ internal/lkgateway/participant_form.go | 31 ++++++++-- internal/lkgateway/participant_reg.go | 82 ++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 internal/lkgateway/participant_reg.go diff --git a/deploy/release/component-versions.json b/deploy/release/component-versions.json index 488b109..d65446e 100644 --- a/deploy/release/component-versions.json +++ b/deploy/release/component-versions.json @@ -20,8 +20,8 @@ "version": "2026.10.21.6" }, "bj-server": { - "fingerprint": "bbbd830ed300ae81eee05d87ad0fe59d0756a794f1d157c37bc36fe67c2e0f25", - "version": "2026.10.21.12" + "fingerprint": "729ea3d2cdb6ba9d3f5e56d8acb967d7a5620771a6949d723482ee14d882a83c", + "version": "2026.10.21.13" }, "crypto-service.jar": { "fingerprint": "2c4fd536bc9b1c20c69a70e8dcd374a61847b263aeb2e474c45debb01fbd8233", diff --git a/internal/lkgateway/nsdpoller.go b/internal/lkgateway/nsdpoller.go index a4491c9..a640a26 100644 --- a/internal/lkgateway/nsdpoller.go +++ b/internal/lkgateway/nsdpoller.go @@ -176,6 +176,16 @@ func (s *Server) applyIncoming(ctx context.Context, p igw.Package) error { } log.Printf("lk-gateway: поллер M2MTransferResponse id=%d: статус=%s коды=[%s] GUID=%s", p.ID, resp.StatusCode, strings.TrimSpace(codes), resp.GUID) + if gs := string(resp.GUID); gs == "" || gs == zeroGUID { + // Нулевой GUID — ответ не на перевод (сделки нет), а на регистрацию участника + // (анкета #M2MPF) или служебное сообщение сервиса МОСТ. Фиксируем и показываем + // на странице анкеты, а не отбрасываем как «сделка не найдена». + raw, _ := captureNSDReply(p, unpacked.DocXML, "participant-form") + recordParticipantReply(string(resp.StatusCode), resp, raw) + log.Printf("lk-gateway: поллер ОТВЕТ НРД на анкету участника id=%d: %s [%s] %q (сохранён %s)", + p.ID, resp.StatusCode, strings.TrimSpace(codes), strings.Join(serviceErrorTexts(resp), "; "), raw) + return nil + } if err := s.svc.ApplyServiceResponse(ctx, resp, unpacked.DocXML); err != nil { // Сделка может быть не найдена (ответ на чужой/старый GUID) — // логируем, но помечаем обработанным, чтобы не зациклиться. diff --git a/internal/lkgateway/participant_form.go b/internal/lkgateway/participant_form.go index 1038c9f..593ce70 100644 --- a/internal/lkgateway/participant_form.go +++ b/internal/lkgateway/participant_form.go @@ -355,6 +355,9 @@ func registerParticipantForm(mux *http.ServeMux, rc *RuntimeConfig, igwClient *i break } flash, flashKind = "Анкета отправлена через ИШ ("+string(pkg)+", пакет id="+id+"). Ждём подтверждения регистрации от НРД.", "ok" + saveParticipantReg(func(rg *participantReg) { + *rg = participantReg{SentAt: time.Now(), PkgID: id, Name: d.FullName, Channel: igwChannel} + }) } } @@ -370,6 +373,24 @@ func participantRegStatus(rc *RuntimeConfig) string { if rc == nil { return "" } + var out strings.Builder + // Последняя отправка анкеты и ответ НРД на неё (учёт в participant-reg.json). + if reg := loadParticipantReg(); !reg.SentAt.IsZero() { + out.WriteString(`
Анкета отправлена через ИШ ` + reg.SentAt.Format("02.01.2006 15:04") + `, пакет ` + html.EscapeString(reg.PkgID)) + if reg.Name != "" { + out.WriteString(`, наименование «` + html.EscapeString(reg.Name) + `»`) + } + out.WriteString(`. `) + switch { + case reg.RespAt.IsZero(): + out.WriteString(`Ответ НРД пока не получен.`) + case strings.EqualFold(reg.Status, "ERROR"): + out.WriteString(`НРД отклонил ` + reg.RespAt.Format("02.01 15:04") + `: ` + html.EscapeString(reg.RespText) + `. Исправьте указанное и отправьте снова.`) + default: + out.WriteString(`НРД принял ` + reg.RespAt.Format("02.01 15:04") + `. ` + html.EscapeString(reg.RespText)) + } + out.WriteString(`
`) + } s := rc.Snapshot() contour := map[string]string{"prod": "боевого", "guest": "гостевого", "test3": "тестового"}[contourOf(currentNSDProfile())] if contour == "" { @@ -382,13 +403,15 @@ func participantRegStatus(rc *RuntimeConfig) string { } } if ok { - return `
В справочнике участников M2M ` + contour + ` контура: есть — ` + + out.WriteString(`
В справочнике участников M2M ` + contour + ` контура: есть — ` + html.EscapeString(pickStr(hb.Name, hb.FullName)) + `, код ` + html.EscapeString(hb.Code) + - `, ИНН ` + html.EscapeString(hb.INN) + `. Регистрация подтверждена.
` + `, ИНН ` + html.EscapeString(hb.INN) + `. Регистрация подтверждена.
`) + return out.String() } - return `
В справочнике участников M2M ` + contour + ` контура нашей организации пока нет. ` + + out.WriteString(`
В справочнике участников M2M ` + contour + ` контура нашей организации пока нет. ` + `После приёма анкеты НРД организация появится в справочнике (обновляется каждые ~10 минут) — тогда здесь будет «есть». ` + - `Регистрацию подтверждает также ответный документ от НРД.
` + `Регистрацию подтверждает также ответный документ от НРД.
`) + return out.String() } func participantFormRender(w http.ResponseWriter, d participantFormData, errs []string, action, flash, flashKind, xmlPreview, regHTML string, igwConfigured, igwProd bool) { diff --git a/internal/lkgateway/participant_reg.go b/internal/lkgateway/participant_reg.go new file mode 100644 index 0000000..deb53b3 --- /dev/null +++ b/internal/lkgateway/participant_reg.go @@ -0,0 +1,82 @@ +package lkgateway + +// participant_reg.go — учёт регистрации участника M2M: что и когда отправили +// анкетой (#M2MPF) и какой ответ дал НРД (сервис МОСТ, M2MTransferResponse с +// нулевым GUID — он не относится к переводу). Показывается на странице анкеты. + +import ( + "encoding/json" + "os" + "strings" + "sync" + "time" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m" +) + +const participantRegFile = "/var/lib/bj/.bj/participant-reg.json" + +// participantReg — последняя отправка анкеты и ответ НРД на неё. +type participantReg struct { + SentAt time.Time `json:"sent_at,omitempty"` + PkgID string `json:"pkg_id,omitempty"` + Name string `json:"name,omitempty"` // отправленное полное наименование + Channel string `json:"channel,omitempty"` + RespAt time.Time `json:"resp_at,omitempty"` + Status string `json:"status,omitempty"` // OK | ERROR + Codes []string `json:"codes,omitempty"` + RespText string `json:"resp_text,omitempty"` + RawFile string `json:"raw_file,omitempty"` +} + +var participantRegMu sync.Mutex + +func loadParticipantReg() participantReg { + participantRegMu.Lock() + defer participantRegMu.Unlock() + var r participantReg + if b, err := os.ReadFile(participantRegFile); err == nil { + _ = json.Unmarshal(b, &r) + } + return r +} + +// saveParticipantReg читает текущую запись, даёт изменить и атомарно пишет назад. +func saveParticipantReg(mut func(*participantReg)) { + participantRegMu.Lock() + defer participantRegMu.Unlock() + var r participantReg + if b, err := os.ReadFile(participantRegFile); err == nil { + _ = json.Unmarshal(b, &r) + } + mut(&r) + b, err := json.MarshalIndent(&r, "", " ") + if err != nil { + return + } + tmp := participantRegFile + ".tmp" + if os.WriteFile(tmp, b, 0o640) == nil { + _ = os.Rename(tmp, participantRegFile) + } +} + +// recordParticipantReply сохраняет ответ НРД на анкету (статус, коды, тексты, файл XML). +func recordParticipantReply(status string, resp *m2m.M2MTransferResponse, rawFile string) { + var codes []string + seen := map[string]bool{} + for _, rr := range resp.Responses { + c := strings.TrimSpace(rr.Code) + if c != "" && !seen[c] { + seen[c] = true + codes = append(codes, c) + } + } + text := strings.Join(serviceErrorTexts(resp), "; ") + saveParticipantReg(func(r *participantReg) { + r.RespAt = time.Now() + r.Status = status + r.Codes = codes + r.RespText = text + r.RawFile = rawFile + }) +}