Files
Bridge-and-Join-s/internal/lkgateway/participant_reg.go
T
fontvielle f296ac5ad8 Справочник участников — своего контура; регистрация — по справочнику НРД; поиск клиентов портфеля
readHandbook брал самый свежий из своего, общего (с сервера лицензий) и
встроенного — общий был тестовым (TEST3, 166 участников, прислан с 28), и на
боевом 9 bj считал «регистрация подтверждена», хотя в боевом справочнике
НРД (89) нас нет. Теперь при своём справочнике от НРД — только он; на сервер
лицензий справочник уходит только с боевого контура.

Регистрация участника проверяется по справочнику, полученному от НРД;
появились в нём — анкета отмечается принятой автоматически, плашка уходит.

Портфель ЛК: клиенты DEPO списком по клику (первые 50 по алфавиту), «не
найдено» вместо «поиск недоступен» (пустой ответ был null).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-22 11:28:13 +03:00

233 lines
10 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
// participant_reg.go — учёт регистрации участника M2M: что и когда отправили
// анкетой (#M2MPF) и какой ответ дал НРД (сервис МОСТ, M2MTransferResponse с
// нулевым GUID — он не относится к переводу). Показывается на странице анкеты.
import (
"encoding/json"
"html/template"
"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"`
// Квитанция ЭДО НРД о получении пакета (пакет C): НРД получил анкету, первичная обработка прошла.
DeliveredAt time.Time `json:"delivered_at,omitempty"`
Package string `json:"package,omitempty"` // имя пакета из квитанции (#M2MPF….ZIP)
}
var participantRegMu sync.Mutex
// participantRegsFile — все отправки анкеты (строки в «Исходящих» и «Журнале»); participantRegFile —
// прежний файл с одной последней отправкой, из него история начинается при первом чтении.
const participantRegsFile = "/var/lib/bj/.bj/participant-regs.json"
func readParticipantRegs() []participantReg {
var list []participantReg
if b, err := os.ReadFile(participantRegsFile); err == nil {
_ = json.Unmarshal(b, &list)
return list
}
var r participantReg
if b, err := os.ReadFile(participantRegFile); err == nil && json.Unmarshal(b, &r) == nil && !r.SentAt.IsZero() {
list = append(list, r)
}
return list
}
func writeParticipantRegs(list []participantReg) {
b, err := json.MarshalIndent(list, "", " ")
if err != nil {
return
}
tmp := participantRegsFile + ".tmp"
if os.WriteFile(tmp, b, 0o640) == nil {
_ = os.Rename(tmp, participantRegsFile)
}
}
// loadParticipantRegs — все отправки анкеты, старые первыми.
func loadParticipantRegs() []participantReg {
participantRegMu.Lock()
defer participantRegMu.Unlock()
return readParticipantRegs()
}
// loadParticipantReg — последняя отправка анкеты.
func loadParticipantReg() participantReg {
list := loadParticipantRegs()
if len(list) == 0 {
return participantReg{}
}
return list[len(list)-1]
}
// addParticipantReg — новая отправка анкеты.
func addParticipantReg(r participantReg) {
participantRegMu.Lock()
defer participantRegMu.Unlock()
writeParticipantRegs(append(readParticipantRegs(), r))
}
// saveParticipantReg — изменить последнюю отправку (ответ НРД приходит на неё).
func saveParticipantReg(mut func(*participantReg)) {
participantRegMu.Lock()
defer participantRegMu.Unlock()
list := readParticipantRegs()
if len(list) == 0 {
list = append(list, participantReg{})
}
mut(&list[len(list)-1])
writeParticipantRegs(list)
}
// StatusClass, StatusText — статус отправки анкеты для строк списков (класс бейджа и текст).
func (r participantReg) StatusClass() string {
switch {
case r.RespAt.IsZero(), strings.EqualFold(r.Status, "INFO"):
return "awaiting_decision"
case strings.EqualFold(r.Status, "ERROR"):
return "rejected"
}
return "confirmed"
}
func (r participantReg) StatusText() string {
switch {
case r.RespAt.IsZero() && !r.DeliveredAt.IsZero():
return "Доставлена в НРД · ждём ответ"
case r.RespAt.IsZero():
return "Отправлена · ждём квитанцию НРД"
case strings.EqualFold(r.Status, "INFO"):
return "Принята НРД в обработку"
case strings.EqualFold(r.Status, "ERROR"):
if len(r.Codes) > 0 {
return "Отклонена НРД · " + strings.Join(r.Codes, ", ")
}
return "Отклонена НРД"
}
return "Принята НРД"
}
// 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
})
}
// participantAccepted — последняя отправленная анкета принята НРД.
func participantAccepted() bool {
if in, _, _, _ := nsdHandbookHasUs(globalRC); in {
return true // уже в справочнике участников НРД — зарегистрированы (в т.ч. без анкеты из bj)
}
r := loadParticipantReg()
return !r.RespAt.IsZero() && !strings.EqualFold(r.Status, "ERROR") && !strings.EqualFold(r.Status, "INFO")
}
// recordParticipantFormReply — НРД прислал документ-ответ на анкету (M2MTransferParticipantForm):
// регистрация подтверждена.
func recordParticipantFormReply(rawFile string) {
saveParticipantReg(func(r *participantReg) {
r.RespAt, r.Status, r.Codes = time.Now(), "OK", nil
r.RespText, r.RawFile = "НРД подтвердил регистрацию участника", rawFile
})
}
// participantBanner — плашка наверху раздела НРД: анкета не отправлена, ждёт ответа или отклонена.
// Пропадает, когда НРД принял анкету. Анкету шлём только в боевом контуре — в тестовом плашки нет.
func participantBanner() template.HTML {
if participantAccepted() || contourOf(currentNSDProfile()) != "prod" {
return ""
}
esc := template.HTMLEscapeString
r := loadParticipantReg()
cls, head, body := "stc-err", "Анкета участника M2M не отправлена",
"Без регистрации участника НРД не принимает запросы на переводы. Проверьте анкету и отправьте её."
switch {
case r.SentAt.IsZero():
case r.RespAt.IsZero() && !r.DeliveredAt.IsZero():
cls, head, body = "stc-run", "Анкета участника M2M доставлена в НРД — ждём ответ", "Квитанция НРД "+r.DeliveredAt.In(listMSK).Format("02.01.2006 15:04:05")+": пакет получен, ошибок нет. Плашка пропадёт, когда НРД примет анкету."
case r.RespAt.IsZero(), strings.EqualFold(r.Status, "INFO"):
cls, head, body = "stc-run", "Анкета участника M2M ждёт ответа НРД", "Отправлена "+r.SentAt.In(listMSK).Format("02.01.2006 15:04")+". Плашка пропадёт, когда НРД примет анкету."
case strings.EqualFold(r.Status, "ERROR"):
cls, head = "stc-err", "НРД отклонил анкету участника M2M"
body = r.RespAt.In(listMSK).Format("02.01.2006 15:04") + ": " + r.RespText + ". Исправьте и отправьте снова."
}
return template.HTML(`<div class="stc ` + cls + `" style="margin:0 0 14px"><span class="stc-ic"></span><div class="stc-b"><b>` + esc(head) +
`</b><small>` + esc(body) + ` <a href="#nsd-participant" onclick="var d=document.getElementById('pf-box');if(d)d.open=true">Открыть анкету →</a></small></div></div>`)
}
// nsdHandbookHasUs — наша организация в справочнике участников M2M, который этот bj получил от НРД
// (handbookFile, текущий контур). known=false — справочника от НРД ещё нет. Общий справочник с
// сервера лицензий здесь не годится: он собран с других установок и контуров.
func nsdHandbookHasUs(rc *RuntimeConfig) (in, known bool, at time.Time, n int) {
b, err := os.ReadFile(handbookFile)
if err != nil || rc == nil {
return false, false, time.Time{}, 0
}
var h struct {
At time.Time `json:"at"`
Participants []HandbookEntry `json:"participants"`
}
if json.Unmarshal(b, &h) != nil {
return false, false, time.Time{}, 0
}
s := rc.Snapshot()
code, inn := strings.ToUpper(strings.TrimSpace(orgCode(s))), strings.TrimSpace(orgINN(s))
for _, p := range h.Participants {
if (code != "" && strings.EqualFold(p.Code, code)) || (code == "" && inn != "" && p.INN == inn) {
return true, true, h.At, len(h.Participants)
}
}
return false, true, h.At, len(h.Participants)
}
// participantHandbookCheck — после каждого справочника от НРД: появились в нём, а анкета ещё не
// отмечена принятой — регистрация подтверждена справочником.
func participantHandbookCheck(rc *RuntimeConfig) {
in, known, at, _ := nsdHandbookHasUs(rc)
if !in || !known {
return
}
r := loadParticipantReg()
if r.SentAt.IsZero() || (!r.RespAt.IsZero() && !strings.EqualFold(r.Status, "INFO") && !strings.EqualFold(r.Status, "ERROR")) {
return
}
saveParticipantReg(func(r *participantReg) {
r.RespAt, r.Status, r.Codes = at, "OK", nil
r.RespText = "организация появилась в справочнике участников M2M НРД"
})
}