Мониторинг связи с НРД: участки Шлюз/НРД/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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user