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 } } }