feat(igw): REST-клиент ИШ НРД по DOC/instr-ish-rest-api.pdf + упаковщик ZIP
Полный клиент Интеграционного шлюза НРД в internal/nsdadapter/igw/:
client.go — REST endpoint'ы по свежей спецификации НРД:
- POST /api/package/{channel}/file — отправка ZIP (Type=archive, File=base64)
возвращает id пакета (поддерживаются варианты id|package_id|ID)
- GET /api/package/status/{id} — статус NEW|SENT|ERROR (с error-полем)
- GET /api/package?channel=&type=M2MTD|M2MER&date=&id=&count=&excludeErrors=
— список входящих от НРД, с files[] и signs[] (ИШ сам проверяет ЭП и
выдаёт VALID|INVALID)
- GET /api/package/{id} — скачать ZIP (raw или base64-в-JSON, авто-детект
по сигнатуре PK\x03\x04)
- Ретраи только на 5xx/сетевые ошибки (4xx — сразу ошибка)
- HTTP-клиент через options, кастомный таймаут, ретраи
pack.go — упаковщик/распаковщик ZIP по разделу 2.3 инструкции:
- PackRequest(req, docName) — M2MTransferRequest→ZIP с config.xml
- PackXML(xml, docName, packageType) — для эталонных сообщений
- UnpackPackage(zip) → {DocXML, WinfXML, Signature, Filenames}
- ParseDecision / ParseResponse через nsdxml.Unmarshal
Покрыто тестами (10/10 PASS):
- send happy path с проверкой формата JSON-body
- retry на 5xx, без ретраев на 4xx
- GetStatus с числовым id
- ListIncoming как массив (новый формат) и как {items:[]} (старый)
- GetPackage raw ZIP + GetPackage с base64-в-JSON
- упаковка/распаковка: 2 файла в ZIP, имена, содержимое config.xml
- распаковка с .sgn и winf.xml
cmd/bj-server/main.go — NSD-poller адаптирован под новый API
(client.ListIncoming(ctx, ListFilter{}) вместо позиционных параметров;
поля Package.ID/Name/Type/State вместо PackageID/PackageType).
Скачана и положена в DOC/ свежая спецификация (798 KB, 15 стр):
DOC/instr-ish-rest-api.pdf — это исходный документ для нашей реализации.
REPORT.md обновлён:
- общая готовность 65% → 70%
- готовность к роботу 80% → 85%
- добавлен раздел про REST-клиент ИШ
- блокер #6 — отсутствие «Руководства по установке ИШ»
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
// pack.go — упаковщик/распаковщик ZIP-пакетов для ИШ НРД.
|
||||
//
|
||||
// Формат отправляемого пакета (раздел 2.3 инструкции):
|
||||
// ZIP-архив содержит:
|
||||
// - <doc>.xml — сам документ (M2MTransferRequest.xml)
|
||||
// - config.xml — настроечный файл с указанием name и package
|
||||
//
|
||||
// Пример config.xml:
|
||||
// <config>
|
||||
// <name>doc.xml</name>
|
||||
// <package>#M2MTR</package>
|
||||
// </config>
|
||||
//
|
||||
// Формат входящего пакета (раздел 2.4 + раздел 2.6):
|
||||
// ZIP-архив содержит:
|
||||
// - <doc>.xml — сам документ (M2MTransferDecision.xml или M2MTransferResponse.xml)
|
||||
// - winf.xml — транзитный конверт ЭДО НРД
|
||||
// - <doc>.xml.sgn — отсоединённая подпись НРД (опц.)
|
||||
//
|
||||
// ИШ сам формирует пакет ЭДО (подписывает, добавляет winf.xml и т.д.).
|
||||
// Наша задача — собрать ZIP с XML+config.xml и отправить.
|
||||
|
||||
package igw
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml"
|
||||
)
|
||||
|
||||
// config — содержимое config.xml внутри пакета.
|
||||
type config struct {
|
||||
XMLName xml.Name `xml:"config"`
|
||||
Name string `xml:"name"`
|
||||
Package string `xml:"package"`
|
||||
}
|
||||
|
||||
// PackRequest упаковывает M2MTransferRequest в ZIP-архив для ИШ.
|
||||
// docFileName — имя XML внутри архива (например "M2MTransferRequest.xml").
|
||||
// Возвращает байты ZIP, готовые к отправке через POST /api/package/{channel}/file.
|
||||
func PackRequest(req *m2m.M2MTransferRequest, docFileName string) ([]byte, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("igw: PackRequest: req=nil")
|
||||
}
|
||||
if docFileName == "" {
|
||||
docFileName = "M2MTransferRequest.xml"
|
||||
}
|
||||
xmlBytes, err := nsdxml.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("igw: marshal M2MTransferRequest: %w", err)
|
||||
}
|
||||
return packZIP(xmlBytes, docFileName, "#M2MTR")
|
||||
}
|
||||
|
||||
// PackXML упаковывает произвольный XML в ZIP с config.xml. Тип пакета
|
||||
// (например "#M2MTR" / "#M2MTD") задаётся явно. Полезно когда XML уже
|
||||
// собран снаружи (тесты, эталонные сообщения из инструкции).
|
||||
func PackXML(xmlBytes []byte, docFileName, packageType string) ([]byte, error) {
|
||||
if len(xmlBytes) == 0 {
|
||||
return nil, errors.New("igw: PackXML: xmlBytes пустой")
|
||||
}
|
||||
if !strings.HasPrefix(packageType, "#") {
|
||||
return nil, fmt.Errorf("igw: packageType должен начинаться с #, получено %q", packageType)
|
||||
}
|
||||
if docFileName == "" {
|
||||
docFileName = "doc.xml"
|
||||
}
|
||||
return packZIP(xmlBytes, docFileName, packageType)
|
||||
}
|
||||
|
||||
func packZIP(xmlBytes []byte, docFileName, packageType string) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
w := zip.NewWriter(&buf)
|
||||
|
||||
// 1. сам документ
|
||||
fw, err := w.Create(docFileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("igw: create %s in zip: %w", docFileName, err)
|
||||
}
|
||||
if _, err := fw.Write(xmlBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. config.xml
|
||||
cfg := config{Name: docFileName, Package: packageType}
|
||||
cfgBytes, err := xml.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("igw: marshal config.xml: %w", err)
|
||||
}
|
||||
cfgWriter, err := w.Create("config.xml")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("igw: create config.xml in zip: %w", err)
|
||||
}
|
||||
if _, err := cfgWriter.Write(cfgBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, fmt.Errorf("igw: close zip: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnpackedPackage — содержимое распакованного ZIP'а с входящим пакетом.
|
||||
type UnpackedPackage struct {
|
||||
DocXML []byte // первый XML, который не winf.xml и не config.xml
|
||||
WinfXML []byte // транзитный конверт ЭДО (опц., присутствует у входящих от НРД)
|
||||
Signature []byte // .sgn файл (отсоединённая подпись), опц.
|
||||
Filenames []string
|
||||
}
|
||||
|
||||
// UnpackPackage распаковывает ZIP-архив от ИШ и возвращает структурированно
|
||||
// тело документа + winf.xml + отсоединённую подпись.
|
||||
func UnpackPackage(zipBytes []byte) (*UnpackedPackage, error) {
|
||||
r, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("igw: zip reader: %w", err)
|
||||
}
|
||||
out := &UnpackedPackage{}
|
||||
for _, f := range r.File {
|
||||
out.Filenames = append(out.Filenames, f.Name)
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("igw: open %s in zip: %w", f.Name, err)
|
||||
}
|
||||
data, err := io.ReadAll(rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("igw: read %s from zip: %w", f.Name, err)
|
||||
}
|
||||
low := strings.ToLower(filepath.Base(f.Name))
|
||||
switch {
|
||||
case low == "winf.xml":
|
||||
out.WinfXML = data
|
||||
case low == "config.xml":
|
||||
// config.xml в исходящих, во входящих обычно отсутствует — игнорируем
|
||||
case strings.HasSuffix(low, ".sgn"):
|
||||
out.Signature = data
|
||||
case strings.HasSuffix(low, ".xml") && out.DocXML == nil:
|
||||
out.DocXML = data
|
||||
}
|
||||
}
|
||||
if out.DocXML == nil {
|
||||
return nil, fmt.Errorf("igw: в ZIP нет основного .xml документа (файлы: %v)", out.Filenames)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ParseDecision разбирает DocXML входящего пакета M2MTD в m2m.M2MTransferDecision.
|
||||
func ParseDecision(docXML []byte) (*m2m.M2MTransferDecision, error) {
|
||||
var d m2m.M2MTransferDecision
|
||||
if err := nsdxml.Unmarshal(docXML, &d); err != nil {
|
||||
return nil, fmt.Errorf("igw: parse M2MTransferDecision: %w", err)
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
// ParseResponse разбирает DocXML входящего пакета M2MER в m2m.M2MTransferResponse.
|
||||
func ParseResponse(docXML []byte) (*m2m.M2MTransferResponse, error) {
|
||||
var r m2m.M2MTransferResponse
|
||||
if err := nsdxml.Unmarshal(docXML, &r); err != nil {
|
||||
return nil, fmt.Errorf("igw: parse M2MTransferResponse: %w", err)
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
Reference in New Issue
Block a user