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
146 lines
5.6 KiB
Go
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
|
|
}
|
|
}
|
|
}
|