Анкета участника: ловить ответ НРД (M2MER с нулевым GUID) и показывать отправку/ответ
Ответ НРД на анкету приходит как M2MTransferResponse (M2MER) с нулевым GUID — раньше он уходил в обработку перевода, «сделка не найдена», и отбрасывался. Теперь нулевой GUID распознаётся как ответ на регистрацию участника: - сырой XML сохраняется (captureNSDReply → nsd-replies); - отправка анкеты и ответ НРД пишутся в participant-reg.json (время, пакет, наименование, статус, коды, текст НРД); - на странице «Регистрация участника M2M» показываем: когда отправили и вердикт НРД («принял»/«отклонил» с текстом, напр. M2M37 «Некорректно указано наименование участника в анкете»). Текст берётся из самого ответа (serviceErrorTexts) — карту кодов не трогаем. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
@@ -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) —
|
||||
// логируем, но помечаем обработанным, чтобы не зациклиться.
|
||||
|
||||
@@ -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(`<div class="note">Анкета отправлена через ИШ ` + 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(`<b style="color:#b00">НРД отклонил</b> ` + reg.RespAt.Format("02.01 15:04") + `: ` + html.EscapeString(reg.RespText) + `. Исправьте указанное и отправьте снова.`)
|
||||
default:
|
||||
out.WriteString(`<b style="color:#2a7a2a">НРД принял</b> ` + reg.RespAt.Format("02.01 15:04") + `. ` + html.EscapeString(reg.RespText))
|
||||
}
|
||||
out.WriteString(`</div>`)
|
||||
}
|
||||
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 `<div class="ok">В справочнике участников M2M ` + contour + ` контура: <b>есть</b> — ` +
|
||||
out.WriteString(`<div class="ok">В справочнике участников M2M ` + contour + ` контура: <b>есть</b> — ` +
|
||||
html.EscapeString(pickStr(hb.Name, hb.FullName)) + `, код ` + html.EscapeString(hb.Code) +
|
||||
`, ИНН ` + html.EscapeString(hb.INN) + `. Регистрация подтверждена.</div>`
|
||||
`, ИНН ` + html.EscapeString(hb.INN) + `. Регистрация подтверждена.</div>`)
|
||||
return out.String()
|
||||
}
|
||||
return `<div class="note">В справочнике участников M2M ` + contour + ` контура нашей организации пока <b>нет</b>. ` +
|
||||
out.WriteString(`<div class="note">В справочнике участников M2M ` + contour + ` контура нашей организации пока <b>нет</b>. ` +
|
||||
`После приёма анкеты НРД организация появится в справочнике (обновляется каждые ~10 минут) — тогда здесь будет «есть». ` +
|
||||
`Регистрацию подтверждает также ответный документ от НРД.</div>`
|
||||
`Регистрацию подтверждает также ответный документ от НРД.</div>`)
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func participantFormRender(w http.ResponseWriter, d participantFormData, errs []string, action, flash, flashKind, xmlPreview, regHTML string, igwConfigured, igwProd bool) {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user