Мониторинг связи с НРД: участки Шлюз/НРД/M2M/Отправка, сквозная проверка справочником раз в 10 мин, история за сутки на «Статусе», цепочка в шапке (30 с), автоповтор недоставленных до 24 ч, баннер и статус «ожидает отправки» в кабинете клиента

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-09-25 16:17:25 +03:00
parent a0f6ce9f7b
commit 4cbefa41d5
18 changed files with 1075 additions and 62 deletions
+72 -6
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"log"
"os"
"sort"
"sync"
"time"
@@ -26,8 +27,10 @@ type SendFailure struct {
PackageID string `json:"package_id,omitempty"`
Code string `json:"code,omitempty"`
Error string `json:"error"`
At time.Time `json:"at"`
At time.Time `json:"at"` // последняя попытка
FirstAt time.Time `json:"first_at"` // первая попытка: автоповтор — не дольше суток от неё
Attempts int `json:"attempts"`
GaveUp bool `json:"gave_up,omitempty"` // сутки прошли — автоповтор остановлен
}
var sendFailFile = "/var/lib/bj/.bj/send-failures.json"
@@ -72,6 +75,9 @@ func sendFailNote(guid, dealID string, err error) SendFailure {
sendFailLoad()
f := sendFails.m[guid]
f.DealID, f.Error, f.At, f.Attempts = dealID, err.Error(), time.Now(), f.Attempts+1
if f.FirstAt.IsZero() {
f.FirstAt = f.At
}
f.PackageID, f.Code = "", ""
var nd *igw.NotDeliveredError
if errors.As(err, &nd) {
@@ -82,14 +88,50 @@ func sendFailNote(guid, dealID string, err error) SendFailure {
return f
}
// sendFailClear — перевод доставлен. Если до этого он не доходил — заявка клиента из «ожидает
// отправки» переходит в «отправлена».
func sendFailClear(guid string) {
sendFails.Lock()
defer sendFails.Unlock()
sendFailLoad()
if _, ok := sendFails.m[guid]; ok {
_, ok := sendFails.m[guid]
if !sendFails.loaded {
sendFailLoad()
_, ok = sendFails.m[guid]
}
if ok {
delete(sendFails.m, guid)
sendFailSave()
}
sendFails.Unlock()
if ok && globalRC != nil {
go lkQueuedToSent(globalRC, guid)
}
}
// sendFailList — копия всех недоставленных (GUID → неудача).
func sendFailList() map[string]SendFailure {
sendFails.Lock()
defer sendFails.Unlock()
sendFailLoad()
out := make(map[string]SendFailure, len(sendFails.m))
for k, v := range sendFails.m {
out[k] = v
}
return out
}
// sendFailGiveUp — остановить автоповтор (true — только что остановлен, нужно сообщить).
func sendFailGiveUp(guid string) bool {
sendFails.Lock()
defer sendFails.Unlock()
sendFailLoad()
f, ok := sendFails.m[guid]
if !ok || f.GaveUp {
return false
}
f.GaveUp = true
sendFails.m[guid] = f
sendFailSave()
return true
}
// noteSendFailure — записать неудачу, событие сделки и оповещение.
@@ -97,10 +139,13 @@ func (s *Service) noteSendFailure(ctx context.Context, deal *m2mcore.Deal, req *
guid := string(req.Header.GUID)
f := sendFailNote(guid, deal.ID, err)
_ = s.repo.AppendEvent(ctx, deal.ID, m2mcore.Event{Type: "send_failed", Payload: f, CreatedAt: time.Now().UTC(), Actor: "system"})
log.Printf("lk-gateway: перевод %s (заявка %s) НЕ доставлен в НРД: %v", shortGUID(guid), shortID(deal.ID), err)
log.Printf("lk-gateway: перевод %s (заявка %s) НЕ доставлен в НРД (попытка %d): %v", shortGUID(guid), shortID(deal.ID), f.Attempts, err)
if f.Attempts > 1 { // о повторных неудачах автоповтора не шумим — сообщили при первой
return
}
inv := req.Data.InvestorInformation
raiseSystemAlert(globalRC, "Перевод не доставлен в НРД",
fmt.Sprintf("Перевод %s (%s %s): %v. Заявка осталась «Не отправлена», в DEPO отправка не отмечена. Когда НРД снова будет доступен — «Отправить повторно» в карточке заявки.",
fmt.Sprintf("Перевод %s (%s %s): %v. Заявка осталась «Не отправлена», в DEPO отправка не отмечена. bj отправит её повторно сам, как только НРД снова ответит (в течение суток); можно и вручную — «Отправить повторно» в карточке заявки.",
shortGUID(guid), inv.LastName, inv.FirstName, err))
}
@@ -162,3 +207,24 @@ func sendFailText(f *SendFailure) string {
}
return f.Error
}
// SendFailView — строка «недоставленные переводы» на странице «Статус».
type SendFailView struct {
GUID, DealID, Text string
At, FirstAt time.Time
Attempts int
GaveUp bool
}
func sendFailViews() []SendFailView {
var out []SendFailView
for guid, f := range sendFailList() {
first := f.FirstAt
if first.IsZero() {
first = f.At
}
out = append(out, SendFailView{GUID: guid, DealID: f.DealID, Text: sendFailText(&f), At: f.At, FirstAt: first, Attempts: f.Attempts, GaveUp: f.GaveUp})
}
sort.Slice(out, func(i, j int) bool { return out[i].FirstAt.Before(out[j].FirstAt) })
return out
}