da12a9ec1c
- 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>
226 lines
8.9 KiB
Go
226 lines
8.9 KiB
Go
// 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
|
||
}
|