f0dec0c238
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
134 lines
11 KiB
Go
134 lines
11 KiB
Go
package lkgateway
|
||
|
||
// Готовые письма тем, без кого подключение не закончить (05.10.2026): НРД (доступ к тестовому и боевому
|
||
// контурам), ARQA (модуль неторговых поручений QUIK), администратору базы DEPO. Реквизиты подставляются
|
||
// из настроек; то, чего bj не знает (контактное лицо), — в квадратных скобках. У НРД бывают свои формы
|
||
// заявок — письмо тогда идёт сопроводительным.
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"html/template"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
)
|
||
|
||
// Letter — готовое письмо.
|
||
type Letter struct {
|
||
Key, Title, To, Subject, Body string
|
||
}
|
||
|
||
func letterOrg(s Settings) (name, inn, code string) {
|
||
inn = orgINN(s)
|
||
name = strings.TrimSpace(s.NSD.OrgName)
|
||
if name == "" && inn != "" { // наименование не введено — из справочника участников НРД по ИНН
|
||
raw, _ := readHandbook()
|
||
var hb struct {
|
||
Participants []HandbookEntry `json:"participants"`
|
||
}
|
||
_ = json.Unmarshal(raw, &hb)
|
||
for _, p := range hb.Participants {
|
||
if p.INN == inn {
|
||
name = pickStr(p.FullName, p.Name)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if name == "" && globalRC != nil {
|
||
name = licenseStatus(globalRC).Tenant
|
||
}
|
||
return pickStr(name, "[наименование организации]"), pickStr(inn, "[ИНН]"), pickStr(orgCode(s), "[код участника]")
|
||
}
|
||
|
||
func orBracket(v, what string) string { return pickStr(strings.TrimSpace(v), "["+what+"]") }
|
||
|
||
// letterFor — письмо по ключу: nsd-test | nsd-prod | arqa | dba.
|
||
func letterFor(s Settings, key string) (Letter, bool) {
|
||
name, inn, code := letterOrg(s)
|
||
sign := "\n\nКонтактное лицо: [ФИО, должность, телефон, e-mail]\n\nС уважением,\n[ФИО, должность]\n" + name
|
||
crypto := "ГОСТ"
|
||
if strings.HasSuffix(s.NSD.Profile, "rsa") {
|
||
crypto = "RSA"
|
||
}
|
||
req := fmt.Sprintf("— депозитарный код: %s\n— депозитарный счёт: %s, раздел: %s\n— программа обмена: Интеграционный шлюз НРД (ИШ), канал WSL, криптография %s\n— роль: депозитарий — участник переводов ценных бумаг M2M",
|
||
orBracket(s.NSD.DeponentCode, "депозитарный код"), orBracket(s.NSD.AccountID, "депозитарный счёт"), orBracket(s.NSD.SectionID, "раздел счёта"), crypto)
|
||
switch key {
|
||
case "nsd-test":
|
||
return Letter{Key: key, Title: "Письмо в НРД: доступ к тестовому контуру TEST3", To: "НКО АО НРД, служба поддержки электронного документооборота",
|
||
Subject: "Заявка на подключение к тестовому контуру TEST3 для переводов M2M — " + name,
|
||
Body: fmt.Sprintf("В НКО АО НРД\n(служба поддержки электронного документооборота)\n\n%s (ИНН %s, код участника %s) просит предоставить доступ к тестовому контуру TEST3 электронного документооборота НРД для обмена сообщениями о переводах ценных бумаг M2M.\n\nСведения для подключения:\n%s\n\nПросим также сообщить порядок получения тестового ключа электронной подписи (УЦ Московской биржи) и включить нас в тестирование сценариев переводов M2M.\n\nЕсли для подключения установлена форма заявки — просим её направить, а это письмо считать сопроводительным.%s",
|
||
name, inn, code, req, sign)}, true
|
||
case "nsd-prod":
|
||
return Letter{Key: key, Title: "Письмо в НРД: подключение к боевому контуру", To: "НКО АО НРД, служба поддержки электронного документооборота",
|
||
Subject: "Заявка на подключение к промышленному контуру для переводов M2M — " + name,
|
||
Body: fmt.Sprintf("В НКО АО НРД\n(служба поддержки электронного документооборота)\n\n%s (ИНН %s, код участника %s) завершила тестирование обмена сообщениями о переводах ценных бумаг M2M на контуре TEST3 и просит подключить промышленный контур электронного документооборота НРД.\n\nСведения для подключения:\n%s\n— ключ электронной подписи для промышленного контура: выдан УЦ Московской биржи [номер сертификата, срок действия]\n\nЕсли для подключения установлена форма заявки — просим её направить, а это письмо считать сопроводительным.%s",
|
||
name, inn, code, req, sign)}, true
|
||
case "arqa":
|
||
ip := pickStr(outboundIP(s.Quik.Server, s.Quik.Port), firstServerIP())
|
||
return Letter{Key: key, Title: "Письмо в ARQA Technologies: модуль неторговых поручений", To: "ARQA Technologies, служба поддержки QUIK",
|
||
Subject: "Подключение модуля неторговых поручений QUIK для переводов M2M — " + name,
|
||
Body: fmt.Sprintf("В ARQA Technologies\n(служба поддержки QUIK)\n\n%s (ИНН %s) подключает систему Bridge&Join для переводов ценных бумаг M2M через НРД. Система работает с модулем неторговых поручений (МНП) QUIK через его базу данных. Просим:\n\n1. Подключить и настроить модуль неторговых поручений для переводов M2M:\n — классы инструментов INSTROUTM2M (вывод ценных бумаг) и INSTRINM2M (ввод ценных бумаг);\n — типы неторговых поручений для M2M (100/101) в QAdmin;\n — справочник депозитариев-получателей M2MDepoRecipients: его заполняет внешняя система.\n2. Выдать SQL-логин для внешней системы с правами API модуля (роль InstrEu): процедуры sp_NewInstrSec, sp_SignInstrSec, cancel_instruction; таблицы Export, Contracts, M2MDepoRecipients.\n3. Разрешить подключение к базе данных МНП с сервера Bridge&Join (адрес %s) и сообщить адрес сервера, порт и имя базы.\n4. После первого заполнения справочника M2MDepoRecipients перезапустить модуль неторговых поручений: справочник читается при запуске.%s",
|
||
name, inn, orBracket(ip, "IP-адрес сервера bj"), sign)}, true
|
||
case "dba":
|
||
ip := pickStr(outboundIP(s.Fancy.Server, s.Fancy.Port), firstServerIP())
|
||
f := s.Fancy
|
||
login := "bj-depo-login-mssql.sql"
|
||
job := "3. Создать задание SQL Server Agent обработки обмена — скрипт bj-depo-job-mssql.sql. Задание вызывает процедуры обработки FTV_M2M_JOB и FTV_M2M_PROCESS_* (поставляются отдельно; устанавливаются в базу DEPO в порядке WIRING → OPERATION → EVENTS → CREATE → JOB)."
|
||
dbms := "SQL Server"
|
||
if f.IsPostgres() {
|
||
login, dbms = "bj-depo-login-postgres.sql", "PostgreSQL"
|
||
job = "3. Выдать логину bj право запускать процедуру обработки обмена (её вызывает сам bj) — строка в мастере, раздел «Учёт DEPO» → «Обработка». Процедуры обработки FTV_M2M_* поставляются отдельно."
|
||
}
|
||
return Letter{Key: key, Title: "Письмо администратору базы DEPO", To: "Администратор СУБД учёта DEPO",
|
||
Subject: "Bridge&Join: доступ к базе DEPO " + f.DepoDB(),
|
||
Body: fmt.Sprintf("Коллеги, добрый день!\n\nДля подключения системы переводов ценных бумаг Bridge&Join к учёту DEPO нужно на сервере %s (база %s):\n\n1. Создать логин для bj с минимальными правами — скрипт %s: чтение учётных таблиц и запись только в таблицы обмена.\n2. Создать таблицы обмена FTV_M2M* — скрипт %s. Учётные данные они не меняют.\n%s\n4. Разрешить подключение к %s с сервера Bridge&Join (адрес %s).\n\nСкрипты прилагаются (скачиваются в мастере настройки bj, раздел «Учёт DEPO»). Все скрипты можно запускать повторно: если объект уже есть, он не меняется.%s",
|
||
orBracket(f.Server, "сервер DEPO"), f.DepoDB(), login, depoTablesFileName(f), job, dbms, orBracket(ip, "IP-адрес сервера bj"), sign)}, true
|
||
}
|
||
return Letter{}, false
|
||
}
|
||
|
||
func firstServerIP() string {
|
||
_, ips := localAddrs()
|
||
for _, ip := range ips {
|
||
if ip.To4() != nil && !ip.IsLoopback() {
|
||
return ip.String()
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// letterView — письмо для шаблона (по текущим настройкам).
|
||
func letterView(key string) *Letter {
|
||
if globalRC == nil {
|
||
return nil
|
||
}
|
||
if l, ok := letterFor(globalRC.Snapshot(), key); ok {
|
||
return &l
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GET /admin/setup/letters/<ключ>.txt — письмо файлом.
|
||
func registerLetters(mux *http.ServeMux, rc *RuntimeConfig) {
|
||
mux.HandleFunc("/admin/setup/letters/", func(w http.ResponseWriter, r *http.Request) {
|
||
key := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/admin/setup/letters/"), ".txt")
|
||
l, ok := letterFor(rc.Snapshot(), key)
|
||
if !ok {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||
w.Header().Set("Content-Disposition", `attachment; filename="bj-letter-`+key+`.txt"`)
|
||
_, _ = fmt.Fprintf(w, "Кому: %s\r\nТема: %s\r\n\r\n%s\r\n", l.To, l.Subject, strings.ReplaceAll(l.Body, "\n", "\r\n"))
|
||
})
|
||
}
|
||
|
||
// mailtoLink — ссылка «Открыть в почте» (тема и текст письма; пробелы и переводы строк — %20 и %0A).
|
||
func mailtoLink(l *Letter) template.URL {
|
||
if l == nil {
|
||
return ""
|
||
}
|
||
return template.URL("mailto:?subject=" + url.PathEscape(l.Subject) + "&body=" + url.PathEscape(l.Body))
|
||
}
|