Анкета участника M2M: отправка в НРД через ИШ пакетом #M2MPF

НРД открыл приём анкеты (M2MTransferParticipantForm) в пром-контуре.
По формату НРД анкета — отдельный тип ЭД M2MPF: тег <package> = "#" +
первые 5 символов имени типа ЭД → "#M2MPF", businessLine=MOST (как и
остальные M2M-сообщения; на ИШ <107.0.0.904 businessLine опускается).
Подпись — боевым ГОСТ-контейнером на стороне ИШ.

- nsdadapter/router.go: тип PackageM2MParticipantForm "#M2MPF";
  KindParticipantForm теперь маппится на него (был ошибочно
  Assets_investment).
- lkgateway/participant_form.go: действие send_igw — упаковка анкеты
  через igw.PackXML(..., "#M2MPF") и отправка igwClient.SendPackage в
  канал профиля (в боевом контуре — prod-gost). Кнопка «Отправить через
  шлюз (боевой ИШ)»; письмо на M2MOST@nsd.ru оставлено запасным каналом.
  Статус готовности ИШ на странице.
- lkgateway/server.go: проброс igwClient/igwChannel в registerParticipantForm.

Образец — sendHandbookRequest (#M2MHR) через тот же ИШ.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XG3zNMMf2LMoDpXo22mwMX
This commit is contained in:
fontvielle
2026-10-16 13:07:40 +03:00
parent 22eb6c6f11
commit af60277411
3 changed files with 58 additions and 17 deletions
+50 -10
View File
@@ -5,10 +5,12 @@ package lkgateway
// участников M2M, иначе сервис МОСТ отбивает запросы кодом M2M14 «Код ЭДО НРД
// отправителя отсутствует в справочнике участников M2M». Здесь bj формирует
// заполненную анкету (предзаполнение из автоопределения DEPO), даёт проверить
// (валидация + показ XML в windows-1251) и предлагает отправить её в НРД обычным
// каналом — письмом на M2MOST@nsd.ru с вложением (не через M2M-шлюз).
// (валидация + показ XML в windows-1251) и отправляет её в НРД: в боевом контуре —
// через ИШ пакетом #M2MPF (businessLine=MOST, подпись боевым контейнером на стороне
// ИШ), либо запасным каналом — письмом на M2MOST@nsd.ru с вложением.
import (
"context"
"encoding/base64"
"fmt"
"html"
@@ -18,6 +20,8 @@ import (
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml"
)
@@ -215,7 +219,7 @@ func sendSMTPAttach(s SMTPSettings, subject, body, filename string, attach []byt
// registerParticipantForm — страница «Регистрация участника M2M»: форма, проверка,
// скачивание анкеты, отправка письмом на M2MOST@nsd.ru.
func registerParticipantForm(mux *http.ServeMux, rc *RuntimeConfig) {
func registerParticipantForm(mux *http.ServeMux, rc *RuntimeConfig, igwClient *igw.Client, igwChannel string) {
mux.HandleFunc("/admin/setup/participant-form", func(w http.ResponseWriter, r *http.Request) {
d := participantFormPrefill(rc)
action := ""
@@ -277,12 +281,41 @@ func registerParticipantForm(mux *http.ServeMux, rc *RuntimeConfig) {
}
}
if action == "send_igw" {
switch {
case len(errs) > 0:
flash, flashKind = "Анкета не прошла проверку — исправьте поля ниже.", "err"
case igwClient == nil || igwChannel == "":
flash, flashKind = "Боевой ИШ не настроен (раздел «НРД», профиль prod-*). Отправка через шлюз недоступна — переключитесь в боевой контур.", "err"
default:
body, encErr := participantFormBytes(d)
if encErr != nil {
flash, flashKind = "кодировка windows-1251: "+encErr.Error(), "err"
break
}
pkg, _ := nsdadapter.RouteToPackageType(nsdadapter.KindParticipantForm)
zipBody, zErr := igw.PackXML(body, "M2MTransferParticipantForm.xml", string(pkg))
if zErr != nil {
flash, flashKind = "упаковка пакета: "+zErr.Error(), "err"
break
}
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
id, sErr := igwClient.SendPackage(ctx, igwChannel, string(pkg), zipBody)
cancel()
if sErr != nil {
flash, flashKind = "отправка через ИШ не удалась: "+sErr.Error(), "err"
break
}
flash, flashKind = "Анкета отправлена через ИШ ("+string(pkg)+", пакет id="+id+"). Ждём подтверждения регистрации от НРД.", "ok"
}
}
xmlPreview := participantFormXML(d)
participantFormRender(w, d, errs, action, flash, flashKind, xmlPreview)
participantFormRender(w, d, errs, action, flash, flashKind, xmlPreview, igwClient != nil && igwChannel != "")
})
}
func participantFormRender(w http.ResponseWriter, d participantFormData, errs []string, action, flash, flashKind, xmlPreview string) {
func participantFormRender(w http.ResponseWriter, d participantFormData, errs []string, action, flash, flashKind, xmlPreview string, igwReady bool) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
inp := func(name, label, val, ph string) string {
return `<label>` + label + `<input name="` + name + `" value="` + html.EscapeString(val) + `" placeholder="` + ph + `"></label>`
@@ -301,6 +334,11 @@ func participantFormRender(w http.ResponseWriter, d participantFormData, errs []
}
flashHTML = `<div class="` + cls + `">` + html.EscapeString(flash) + `</div>`
}
igwBtn := `<button type="submit" name="action" value="send_igw" class="primary">Отправить через шлюз (боевой ИШ)</button>`
igwStatus := `<p class="note">Боевой ИШ: <b style="color:#2a7a2a">настроен</b> — анкету можно отправить через шлюз (#M2MPF).</p>`
if !igwReady {
igwStatus = `<p class="note">Боевой ИШ: <b style="color:#b00">не настроен</b> (раздел «НРД», профиль <code>prod-*</code>). Пока доступны «Скачать» и «Отправить письмом».</p>`
}
page := `<!doctype html><html lang="ru"><head><meta charset="utf-8"><title>Регистрация участника M2M</title>
<style>body{font:14px/1.5 system-ui,Arial;margin:24px;max-width:900px;color:#111}
h1{font-size:20px}label{display:block;margin:8px 0}label input{display:block;width:100%;padding:6px;margin-top:2px;box-sizing:border-box}
@@ -314,9 +352,10 @@ pre{background:#f6f8fa;border:1px solid #ddd;border-radius:8px;padding:12px;over
.note{color:#555;font-size:13px}</style></head><body>
<h1>Регистрация участника сервиса M2M (МОСТ)</h1>
<p class="note">Требование НРД при подключении: зарегистрировать организацию в справочнике участников M2M.
Без регистрации сервис отбивает запросы кодом <b>M2M14</b>. Отправляется <b>обычным каналом</b> — письмом
на <code>` + participantFormRecipient + `</code> с вложенной анкетой (не через M2M-шлюз). Поля предзаполнены
из автоопределения DEPO — проверьте юридические реквизиты.</p>
Без регистрации сервис отбивает запросы кодом <b>M2M14</b>. В боевом контуре анкета подаётся <b>через ИШ</b>
пакетом <code>#M2MPF</code> (businessLine=MOST, подпись боевым контейнером на стороне ИШ); письмо на
<code>` + participantFormRecipient + `</code> оставлено запасным каналом. Поля предзаполнены из
автоопределения DEPO — проверьте юридические реквизиты.</p>
` + flashHTML + vErr + `
<form method="post">
<fieldset><legend>Организация</legend>
@@ -332,12 +371,13 @@ pre{background:#f6f8fa;border:1px solid #ddd;border-radius:8px;padding:12px;over
<div class="row">` + inp("depo", "Код депозитария (DepositoryPlace)", d.DepoCode, "MC0000000000") + inp("broker", "Код брокера (BrokerPlace, если брокер)", d.BrokerCode, "оставьте пустым, если не брокер") + `</div>
</fieldset>
<fieldset><legend>Отправка</legend>
` + inp("to", "Кому (НРД)", participantFormRecipient, participantFormRecipient) + `
` + igwStatus + inp("to", "Кому (НРД), запасной канал", participantFormRecipient, participantFormRecipient) + `
</fieldset>
<div class="btns">
<button type="submit" name="action" value="preview">Проверить</button>
<button type="submit" name="action" value="download" formtarget="_blank">Скачать анкету (XML)</button>
<button type="submit" name="action" value="send" class="primary">Отправить в НРД (письмом)</button>
` + igwBtn + `
<button type="submit" name="action" value="send">Отправить письмом (запасной канал)</button>
</div>
</form>
<h3>Предпросмотр анкеты (windows-1251)</h3>
+6 -6
View File
@@ -232,9 +232,9 @@ func NewServer(cfg ServerConfig) (*Server, error) {
registerSetCallback(mux, svc, rc)
registerSeedListing(mux, store)
registerHandbookRequest(mux, igwClient, igwChannel)
registerChainTest(mux, rc, igwClient != nil) // «Проверить всю цепочку» (мастер, глава «Запуск»)
registerParticipantForm(mux, rc) // анкета участника M2M (регистрация в НРД, participant_form.go)
registerClientProfile(mux, rc) // профиль клиента: выгрузка эталона и предзаполнение (client_profile.go)
registerChainTest(mux, rc, igwClient != nil) // «Проверить всю цепочку» (мастер, глава «Запуск»)
registerParticipantForm(mux, rc, igwClient, igwChannel) // анкета участника M2M (регистрация в НРД, participant_form.go)
registerClientProfile(mux, rc) // профиль клиента: выгрузка эталона и предзаполнение (client_profile.go)
return &Server{
cfg: cfg,
@@ -305,10 +305,10 @@ func (s *Server) Run(ctx context.Context) error {
// Все фоновые горутины — под supervise: паника ловится recover(), пишется
// в лог и в ленту, горутина перезапускается. Один сбой не роняет сервис.
go superviseLoop(ctx, "consume-decisions", s.rc, s.consumeDecisions)
go superviseLoop(ctx, "igw-watch", s.rc, s.igwWatchLoop) // сторож ИШ НРД: оповещения и автоперезапуск
go superviseLoop(ctx, "units-watch", s.rc, s.unitsWatchLoop) // службы модулей и ключей Валидаты: письмо администратору
go superviseLoop(ctx, "igw-watch", s.rc, s.igwWatchLoop) // сторож ИШ НРД: оповещения и автоперезапуск
go superviseLoop(ctx, "units-watch", s.rc, s.unitsWatchLoop) // службы модулей и ключей Валидаты: письмо администратору
go superviseLoop(ctx, "igate-share", s.rc, s.igateShareSyncLoop) // папки шлюза «через домен»: список доступа = группы и сотрудники bj
go handbookLoop(ctx, s.rc) // справочник участников M2M с сервера лицензий (для установок без шлюза НРД)
go handbookLoop(ctx, s.rc) // справочник участников M2M с сервера лицензий (для установок без шлюза НРД)
// Поллер входящих от НРД (только в реальном режиме ИШ): забирает
// ответы робота/контрагента и применяет их через ApplyDecision.
+2 -1
View File
@@ -10,6 +10,7 @@ const (
PackageM2MTransferRequest PackageType = "#M2MTR"
PackageM2MTransferDecision PackageType = "#M2MTD"
PackageM2MTransferError PackageType = "#M2MER"
PackageM2MParticipantForm PackageType = "#M2MPF"
PackageSUBBR PackageType = "SUBBR"
PackageSUBER PackageType = "SUBER"
PackageSUB16 PackageType = "SUB16"
@@ -40,7 +41,7 @@ func RouteToPackageType(kind MessageKind) (PackageType, error) {
case KindHandbookRequest:
return PackageAssetsInvestment, nil
case KindParticipantForm:
return PackageAssetsInvestment, nil
return PackageM2MParticipantForm, nil
}
return "", fmt.Errorf("nsdadapter: не задан PackageType для kind=%q", kind)
}