Files
Bridge-and-Join-s/internal/lkgateway/nsdreceipt.go
T
fontvielle 6fc0ff5d3d Анкета участника: ответ НРД принимается автоматически — квитанция и ответ сервиса
Квитанции ЭДО НРД (пакеты C, «Уведомление о получении Пакетов…»)
разбираются: для нашей анкеты (#M2MPF…) — отметка «доставлена в НРД»
с временем, ошибка первичной обработки — отказ с текстом квитанции.
Ответ сервиса: INFO — «принята в обработку», OK — принята, ERROR —
отказ с кодом; документ-ответ M2MTransferParticipantForm — регистрация
подтверждена. Всё видно в строке «Исходящих», на странице анкеты и в
плашке раздела НРД. После запуска bj перечитывает квитанции за день
отправки, если отметки ещё нет (22.10 на 9 квитанция пришла раньше).

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

146 lines
5.6 KiB
Go

package lkgateway
// nsdreceipt.go — квитанции ЭДО НРД («Уведомление о получении Пакетов электронных документов», пакеты
// типа C: HTML + подпись, без M2M-XML). По ним видно, что НРД получил наш пакет и как прошла первичная
// обработка (тип, расшифровка, распаковка). Для анкеты участника это первая отметка «доставлена»;
// ответ по существу (принята / M2M37) приходит отдельно.
import (
"archive/zip"
"bytes"
"context"
"io"
"log"
"regexp"
"strings"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
"golang.org/x/text/encoding/charmap"
)
// nsdReceipt — разобранная квитанция о получении пакета.
type nsdReceipt struct {
Package string // имя нашего пакета, напр. #M2MPF2210260000000000071.ZIP
ReceivedAt time.Time // когда НРД получил пакет
OK bool // первичная обработка без ошибок
Text string // кратко — что не прошло (если не OK)
}
var (
rcptTagRe = regexp.MustCompile(`(?s)<[^>]*>`)
rcptPkgRe = regexp.MustCompile(`#[A-Z0-9]+\.ZIP`)
rcptTimeRe = regexp.MustCompile(`Дата и время получения\s+(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2})`)
rcptBadRe = regexp.MustCompile(`(?i)(ошибк\S*|не\s*успешно|неуспешно|отказ\S*|не\s+выполнен\S*)`)
)
// parseNSDReceipt — квитанция из ZIP пакета C. false — это не квитанция о получении.
func parseNSDReceipt(zipBytes []byte) (nsdReceipt, bool) {
zr, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
if err != nil {
return nsdReceipt{}, false
}
for _, f := range zr.File {
if !strings.HasSuffix(strings.ToLower(f.Name), ".html") && !strings.HasSuffix(strings.ToLower(f.Name), ".htm") {
continue
}
rc, err := f.Open()
if err != nil {
continue
}
raw, _ := io.ReadAll(io.LimitReader(rc, 1<<20))
rc.Close()
s := string(raw)
if !strings.Contains(s, "Уведомление") { // квитанции НРД — в windows-1251
if d, err := charmap.Windows1251.NewDecoder().Bytes(raw); err == nil {
s = string(d)
}
}
return parseNSDReceiptText(s)
}
return nsdReceipt{}, false
}
func parseNSDReceiptText(html string) (nsdReceipt, bool) {
text := strings.Join(strings.Fields(rcptTagRe.ReplaceAllString(html, " ")), " ")
if !strings.Contains(text, "Уведомление о получении") {
return nsdReceipt{}, false
}
r := nsdReceipt{Package: rcptPkgRe.FindString(text)}
if m := rcptTimeRe.FindStringSubmatch(text); m != nil {
r.ReceivedAt, _ = time.ParseInLocation("02.01.2006 15:04:05", m[1], listMSK)
}
res := text
if i := strings.Index(text, "Результаты первичной обработки"); i >= 0 {
res = text[i:]
}
if j := strings.Index(res, "Составлено"); j > 0 {
res = res[:j]
}
if m := rcptBadRe.FindStringIndex(res); m != nil {
a, b := m[0]-80, m[1]+120
if a < 0 {
a = 0
}
if b > len(res) {
b = len(res)
}
r.Text = strings.TrimSpace(strings.ToValidUTF8(res[a:b], ""))
} else {
r.OK = strings.Contains(res, "Успешно")
}
return r, r.Package != ""
}
// applyNSDReceipt — квитанция на нашу анкету (#M2MPF…): отметка «доставлена» в последней отправке;
// ошибка первичной обработки — отказ с текстом квитанции.
func applyNSDReceipt(r nsdReceipt) bool {
if !strings.HasPrefix(r.Package, "#M2MPF") {
return false
}
applied := false
saveParticipantReg(func(rg *participantReg) {
if rg.SentAt.IsZero() || !rg.DeliveredAt.IsZero() || (!r.ReceivedAt.IsZero() && r.ReceivedAt.Before(rg.SentAt.Add(-time.Minute))) {
return // квитанция на прежнюю отправку
}
applied = true
rg.DeliveredAt, rg.Package = r.ReceivedAt, r.Package
if rg.DeliveredAt.IsZero() {
rg.DeliveredAt = time.Now()
}
if !r.OK && rg.RespAt.IsZero() {
rg.RespAt, rg.Status, rg.RespText = time.Now(), "ERROR", "квитанция НРД: "+pick(r.Text, "пакет не прошёл первичную обработку")
}
})
return applied
}
// participantReceiptBackfill — после запуска: квитанция на анкету могла прийти, когда bj ещё
// пропускал квитанции (или был остановлен) — перечитываем входящие за день отправки.
func (s *Server) participantReceiptBackfill(ctx context.Context) {
rg := loadParticipantReg()
if rg.SentAt.IsZero() || !rg.DeliveredAt.IsZero() || time.Since(rg.SentAt) > 72*time.Hour || s.igwClient == nil {
return
}
cctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
pkgs, err := s.igwClient.ListIncoming(cctx, igw.ListFilter{Channel: s.igwChannel, Date: rg.SentAt})
if err != nil {
log.Printf("lk-gateway: квитанция на анкету: ListIncoming: %v", err)
return
}
for _, p := range pkgs {
if !strings.EqualFold(p.Type, "C") {
continue
}
b, err := s.igwClient.GetPackage(cctx, p.ID)
if err != nil {
continue
}
if r, ok := parseNSDReceipt(b); ok && applyNSDReceipt(r) {
log.Printf("lk-gateway: квитанция на анкету найдена id=%d: %s получен НРД %s, без ошибок=%v", p.ID, r.Package, r.ReceivedAt.Format("02.01 15:04:05"), r.OK)
return
}
}
}