Отправка в НРД с подтверждением доставки: ERROR от ИШ — заявка «Не отправлена», без BJ_SENT; «Отправить повторно» тем же GUID; сторож связи ИШ↔НРД по журналу ИШ

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 15:57:07 +03:00
parent eecb3b7923
commit c2aaa09fe3
15 changed files with 651 additions and 33 deletions
+77 -10
View File
@@ -11,10 +11,11 @@
// - выдаёт всё это клиенту через REST API.
//
// REST-эндпоинты (все по 200/JSON):
// POST /api/package/{channel}/file — отправить ZIP, вернёт id
// GET /api/package/status/{id} — статус: NEW | SENT | ERROR
// GET /api/package?channel=&type=... — список входящих
// GET /api/package/{id} — тело пакета (ZIP в base64)
//
// POST /api/package/{channel}/file — отправить ZIP, вернёт id
// GET /api/package/status/{id} — статус: NEW | SENT | ERROR
// GET /api/package?channel=&type=... — список входящих
// GET /api/package/{id} — тело пакета (ZIP в base64)
package igw
import (
@@ -39,6 +40,8 @@ type Client struct {
httpClient *http.Client
retryMax int
retryWait time.Duration
// confirmWait — сколько ждать от ИШ итога передачи пакета в НРД (SENT/ERROR). 0 — не ждать.
confirmWait time.Duration
}
// Option настраивает Client.
@@ -55,13 +58,34 @@ func WithRetry(max int, wait time.Duration) Option {
return func(cl *Client) { cl.retryMax = max; cl.retryWait = wait }
}
// WithConfirmWait задаёт ожидание подтверждения передачи пакета в НРД (0 — не ждать).
func WithConfirmWait(d time.Duration) Option {
return func(cl *Client) { cl.confirmWait = d }
}
// NotDeliveredError — ИШ принял пакет, но передать его в НРД не смог (статус ERROR): например,
// контур НРД недоступен (ERRORXML_UPLOAD_FAILED). Пакет в НРД не ушёл — отправку нужно повторить.
type NotDeliveredError struct {
PackageID string
Code string
}
func (e *NotDeliveredError) Error() string {
code := e.Code
if code == "" {
code = "без кода"
}
return fmt.Sprintf("НРД не принял пакет (ИШ, пакет %s: %s) — пакет не доставлен, отправку нужно повторить", e.PackageID, code)
}
// NewClient собирает клиента к ИШ по URL.
func NewClient(baseURL string, opts ...Option) *Client {
c := &Client{
baseURL: strings.TrimRight(baseURL, "/"),
httpClient: &http.Client{Timeout: 30 * time.Second},
retryMax: 3,
retryWait: time.Second,
baseURL: strings.TrimRight(baseURL, "/"),
httpClient: &http.Client{Timeout: 30 * time.Second},
retryMax: 3,
retryWait: time.Second,
confirmWait: 20 * time.Second,
}
for _, o := range opts {
o(c)
@@ -131,12 +155,55 @@ func (c *Client) SendPackage(ctx context.Context, channel, packageType string, z
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return "", fmt.Errorf("igw: decode SendPackage response: %w", err)
}
id := ""
for _, v := range []json.Number{out.ID, out.PackageID, out.IDAlt} {
if s := string(v); s != "" {
return s, nil
id = s
break
}
}
if id == "" {
return "", errors.New("igw: пустой id в ответе ИШ")
}
if err := c.waitDelivered(ctx, id); err != nil {
return "", err
}
return id, nil
}
// waitDelivered — ИШ отвечает id сразу, а в НРД передаёт пакет следом (обычно за доли секунды).
// Ждём итог: SENT — доставлен; ERROR — не доставлен (*NotDeliveredError). Если статус узнать
// не удалось или пакет всё ещё в очереди ИШ — считаем отправленным, как раньше.
func (c *Client) waitDelivered(ctx context.Context, id string) error {
if c.confirmWait <= 0 {
return nil
}
deadline := time.Now().Add(c.confirmWait)
for wait := 300 * time.Millisecond; ; {
st, err := c.GetStatus(ctx, id)
if err != nil {
return nil
}
switch strings.ToUpper(st.Status) {
case StatusSent:
return nil
case StatusError:
return &NotDeliveredError{PackageID: id, Code: st.Error}
case "": // ИШ не сообщает статус — ждать нечего
return nil
}
if time.Now().Add(wait).After(deadline) {
return nil
}
select {
case <-ctx.Done():
return nil
case <-time.After(wait):
}
if wait < 2*time.Second {
wait *= 2
}
}
return "", errors.New("igw: пустой id в ответе ИШ")
}
// --------------- GET /api/package/status/{id} ---------------