Files
Bridge-and-Join-s/internal/nsdadapter/igw/pack.go
T
zuevav da12a9ec1c feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР
- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk)
- трансферные таблицы FTV_M2M: создание при установке, события, справочник участников
- сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO
- боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК
- онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML)
- модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 19:41:12 +03:00

226 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"`
BusinessLine string `xml:"businessLine,omitempty"`
}
// BusinessLine — бизнес-линия для config.xml. С версии ИШ 107.0.0.904 при M2M
// ОБЯЗАТЕЛЬНО указывать "MOST"; на версиях младше — НЕ допускается. Пусто (по
// умолчанию) → элемент опускается (omitempty), т.е. совместимо со старым ИШ.
// Оператор ставит "MOST" в настройках НРД после обновления ИШ до 107.0.0.904+.
var BusinessLine string
// 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)
}
// PackDepoOrder упаковывает XML депозитарного поручения (форма MF010, операции
// 16/16-1) в пакет ЭД типа ORDXM для отправки в НРД: config.xml с
// <package>K</package> и БЕЗ businessLine (это не M2M, а классическое поручение
// депо; см. «Инструкция по взаимодействию с НРД через Web-сервис», разд. 8.1).
// ИШ сам подписывает документ ГОСТ-подписью при отправке через REST API.
func PackDepoOrder(xmlBytes []byte, docFileName string) ([]byte, error) {
if len(xmlBytes) == 0 {
return nil, errors.New("igw: PackDepoOrder: xmlBytes пустой")
}
if docFileName == "" {
docFileName = "Poruchenie.xml"
}
return packZIPWith(xmlBytes, docFileName, "K", "")
}
// packZIPWith — как packZIP, но с явными package и businessLine (без глобальной
// BusinessLine), чтобы слать не-M2M документы (напр. поручение депо, package=K).
func packZIPWith(xmlBytes []byte, docFileName, pkg, businessLine string) ([]byte, error) {
var buf bytes.Buffer
w := zip.NewWriter(&buf)
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
}
cfg := config{Name: docFileName, Package: pkg, BusinessLine: businessLine}
cfgBytes, err := xml.MarshalIndent(cfg, "", " ")
if err != nil {
return nil, fmt.Errorf("igw: marshal config.xml: %w", err)
}
cw, err := w.Create("config.xml")
if err != nil {
return nil, fmt.Errorf("igw: create config.xml in zip: %w", err)
}
if _, err := cw.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
}
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 (businessLine добавляется только если задан — см. BusinessLine)
cfg := config{Name: docFileName, Package: packageType, BusinessLine: BusinessLine}
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
}