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>
This commit is contained in:
+185
-2
@@ -48,17 +48,189 @@ func CharsetReader(charset string, input io.Reader) (io.Reader, error) {
|
||||
return nil, fmt.Errorf("nsdxml: неизвестная кодировка %q", charset)
|
||||
}
|
||||
|
||||
// nsPrefixes — пространства имён НРД и их короткие префиксы. Объявляются один
|
||||
// раз на корневом элементе; вложенные используют префикс (req:Header, typ:GUID).
|
||||
// Так требует НРД: повторное xmlns="…" в каждом теге (как делает стандартный
|
||||
// encoding/xml) НРД считает плохо читаемым и потенциально ломающим парсинг.
|
||||
// Порядок — для детерминированного объявления на корне.
|
||||
var nsPrefixOrder = []struct{ uri, prefix string }{
|
||||
{"http://nsd.ru/schemas/m2m/request", "req"},
|
||||
{"http://nsd.ru/schemas/m2m/decision", "dec"},
|
||||
{"http://nsd.ru/schemas/m2m/response", "resp"},
|
||||
{"http://nsd.ru/schemas/m2m/handbook", "hb"},
|
||||
{"http://nsd.ru/schemas/m2m/handbook/request", "hbreq"},
|
||||
{"http://nsd.ru/schemas/m2m/participant/form", "pf"},
|
||||
{"http://nsd.ru/schemas/m2m/types", "typ"},
|
||||
}
|
||||
|
||||
const xsiURI = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
func nsPrefixFor(uri string) string {
|
||||
for _, p := range nsPrefixOrder {
|
||||
if p.uri == uri {
|
||||
return p.prefix
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// rootSchemaLocation — значение xsi:schemaLocation для корневого элемента
|
||||
// исходящих сообщений (как в эталонных пакетах НРД). Пусто — не добавляем.
|
||||
func rootSchemaLocation(rootSpace string) string {
|
||||
switch rootSpace {
|
||||
case "http://nsd.ru/schemas/m2m/request":
|
||||
return "http://nsd.ru/schemas/m2m/request M2MTransferRequest.xsd"
|
||||
case "http://nsd.ru/schemas/m2m/handbook/request":
|
||||
return "http://nsd.ru/schemas/m2m/handbook/request M2MTransferHandbookRequest.xsd"
|
||||
case "http://nsd.ru/schemas/m2m/participant/form":
|
||||
return "http://nsd.ru/schemas/m2m/participant/form M2MTransferParticipantForm.xsd"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Marshal сериализует v в XML, добавляет пролог с encoding="windows-1251"
|
||||
// и преобразует поток в windows-1251.
|
||||
// и преобразует поток в windows-1251. Пространства имён выносятся в префиксы
|
||||
// на корне (prefixize).
|
||||
func Marshal(v any) ([]byte, error) {
|
||||
utf8Body, err := xml.MarshalIndent(v, "", "\t")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
withProlog := append([]byte(xmlProlog), utf8Body...)
|
||||
prefixed, err := prefixize(utf8Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
withProlog := append([]byte(xmlProlog), prefixed...)
|
||||
return EncodeWindows1251(withProlog)
|
||||
}
|
||||
|
||||
// prefixize переписывает XML, сгенерированный стандартным encoding/xml
|
||||
// (с xmlns="…" в каждом элементе), в форму с префиксами, объявленными один раз
|
||||
// на корне: <M2MTransferRequest xmlns:req="…" xmlns:typ="…"> … <req:Header>
|
||||
// <typ:GUID>…</typ:GUID> … </req:Header>. Работает на уровне xml-токенов
|
||||
// (не строк) — надёжно к структуре. Сохраняет отступы из MarshalIndent.
|
||||
func prefixize(body []byte) ([]byte, error) {
|
||||
// Проход 1: какие namespace реально встречаются (для объявления на корне).
|
||||
used := map[string]bool{}
|
||||
xsiUsed := false
|
||||
{
|
||||
dec := xml.NewDecoder(bytes.NewReader(body))
|
||||
for {
|
||||
tok, err := dec.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if se, ok := tok.(xml.StartElement); ok {
|
||||
if se.Name.Space != "" {
|
||||
used[se.Name.Space] = true
|
||||
}
|
||||
for _, a := range se.Attr {
|
||||
if a.Name.Space == xsiURI {
|
||||
xsiUsed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dec := xml.NewDecoder(bytes.NewReader(body))
|
||||
var out bytes.Buffer
|
||||
depth := 0
|
||||
for {
|
||||
tok, err := dec.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch t := tok.(type) {
|
||||
case xml.StartElement:
|
||||
name := qname(t.Name)
|
||||
out.WriteByte('<')
|
||||
out.WriteString(name)
|
||||
if depth == 0 {
|
||||
// Объявляем префиксы используемых namespace один раз на корне —
|
||||
// каждое объявление на отдельной строке с отступом (как в
|
||||
// эталонных пакетах НРД, для читаемости).
|
||||
const nsIndent = "\n "
|
||||
for _, p := range nsPrefixOrder {
|
||||
if used[p.uri] {
|
||||
out.WriteString(nsIndent + `xmlns:` + p.prefix + `="` + p.uri + `"`)
|
||||
}
|
||||
}
|
||||
// xsi объявляем, если он используется в атрибутах ИЛИ если для
|
||||
// корня есть schemaLocation (как в эталонных пакетах НРД).
|
||||
schemaLoc := rootSchemaLocation(t.Name.Space)
|
||||
if xsiUsed || schemaLoc != "" {
|
||||
out.WriteString(nsIndent + `xmlns:xsi="` + xsiURI + `"`)
|
||||
}
|
||||
if schemaLoc != "" {
|
||||
out.WriteString(nsIndent + `xsi:schemaLocation="` + schemaLoc + `"`)
|
||||
}
|
||||
}
|
||||
// Атрибуты, кроме объявлений namespace (xmlns / xmlns:*).
|
||||
for _, a := range t.Attr {
|
||||
if a.Name.Local == "xmlns" || a.Name.Space == "xmlns" {
|
||||
continue
|
||||
}
|
||||
an := a.Name.Local
|
||||
if a.Name.Space == xsiURI {
|
||||
an = "xsi:" + a.Name.Local
|
||||
} else if pfx := nsPrefixFor(a.Name.Space); pfx != "" {
|
||||
an = pfx + ":" + a.Name.Local
|
||||
}
|
||||
out.WriteString(" " + an + `="`)
|
||||
writeXMLValue(&out, []byte(a.Value))
|
||||
out.WriteByte('"')
|
||||
}
|
||||
out.WriteByte('>')
|
||||
depth++
|
||||
case xml.EndElement:
|
||||
depth--
|
||||
out.WriteString("</")
|
||||
out.WriteString(qname(t.Name))
|
||||
out.WriteByte('>')
|
||||
case xml.CharData:
|
||||
writeXMLValue(&out, t)
|
||||
case xml.Comment:
|
||||
out.WriteString("<!--")
|
||||
out.Write(t)
|
||||
out.WriteString("-->")
|
||||
}
|
||||
}
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
// qname возвращает имя элемента с префиксом НРД (typ:GUID) либо без префикса
|
||||
// (корень/неизвестный namespace).
|
||||
func qname(n xml.Name) string {
|
||||
if pfx := nsPrefixFor(n.Space); pfx != "" {
|
||||
return pfx + ":" + n.Local
|
||||
}
|
||||
return n.Local
|
||||
}
|
||||
|
||||
// writeXMLValue пишет текст элемента/атрибута, экранируя XML-спецсимволы.
|
||||
// Whitespace (отступы MarshalIndent) проходит как есть.
|
||||
func writeXMLValue(buf *bytes.Buffer, b []byte) {
|
||||
for _, c := range b {
|
||||
switch c {
|
||||
case '&':
|
||||
buf.WriteString("&")
|
||||
case '<':
|
||||
buf.WriteString("<")
|
||||
case '>':
|
||||
buf.WriteString(">")
|
||||
default:
|
||||
buf.WriteByte(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unmarshal разбирает XML (windows-1251 или UTF-8) в v. Использует
|
||||
// CharsetReader для перекодирования входа, если в прологе указана
|
||||
// windows-1251.
|
||||
@@ -67,3 +239,14 @@ func Unmarshal(data []byte, v any) error {
|
||||
dec.CharsetReader = CharsetReader
|
||||
return dec.Decode(v)
|
||||
}
|
||||
|
||||
// UnmarshalDefaultNS разбирает XML, подставляя defaultNS как пространство имён
|
||||
// для элементов БЕЗ префикса и без xmlns. Нужно для входящих сообщений НРД,
|
||||
// где корневой элемент идёт без namespace (напр. <M2MTransferRequest> с
|
||||
// префиксами только на детях), а наша структура ждёт корень в своём namespace.
|
||||
func UnmarshalDefaultNS(data []byte, v any, defaultNS string) error {
|
||||
dec := xml.NewDecoder(bytes.NewReader(data))
|
||||
dec.CharsetReader = CharsetReader
|
||||
dec.DefaultSpace = defaultNS
|
||||
return dec.Decode(v)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user