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:
zuevav
2026-09-15 19:41:12 +03:00
parent 59fc774f08
commit da12a9ec1c
132 changed files with 29969 additions and 1089 deletions
+185 -2
View File
@@ -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("&amp;")
case '<':
buf.WriteString("&lt;")
case '>':
buf.WriteString("&gt;")
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)
}