Отправка в НРД с подтверждением доставки: 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:
@@ -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} ---------------
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -18,6 +19,10 @@ import (
|
||||
// Сценарий по DOC/instr-ish-rest-api.pdf раздел 2.5.1.
|
||||
func TestSendPackageHappyPath(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/package/status/123" {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 123, "status": "SENT"})
|
||||
return
|
||||
}
|
||||
if r.URL.Path != "/api/package/CH1/file" {
|
||||
t.Errorf("неожиданный путь %q", r.URL.Path)
|
||||
}
|
||||
@@ -206,3 +211,27 @@ func TestGetPackageBase64InJSON(t *testing.T) {
|
||||
t.Errorf("decoded = %q, ожидалось %q", body, zipBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// Пакет принят ИШ, но в НРД не ушёл (контур недоступен) — отправка должна вернуть ошибку.
|
||||
func TestSendPackageNotDelivered(t *testing.T) {
|
||||
polls := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/package/status/") {
|
||||
polls++
|
||||
st := map[string]any{"id": 187, "status": "NEW"}
|
||||
if polls > 1 {
|
||||
st = map[string]any{"id": 187, "status": "ERROR", "error": "ERRORXML_UPLOAD_FAILED"}
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(st)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 187})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
|
||||
_, err := c.SendPackage(context.Background(), "CH1", "#M2MTR", []byte("PK"))
|
||||
var nd *igw.NotDeliveredError
|
||||
if !errors.As(err, &nd) || nd.PackageID != "187" || nd.Code != "ERRORXML_UPLOAD_FAILED" {
|
||||
t.Fatalf("ожидалась ошибка доставки, получено %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user