Files
Bridge-and-Join-s/internal/lkgateway/letters.go
T
zuevav a7cfbb2e74 Мастер, QUIK: короткое письмо в ARQA с внешним адресом, памятка администратору, понятный выбор работы с QUIK
- Письмо в ARQA: организация просит подключить модуль неторговых поручений,
  выдать логин и пароль и разрешить подключение с нашего внешнего адреса —
  о bj и внутреннем устройстве ни слова. Внешний адрес bj узнаёт у сервера
  лицензий (GET /v1/ip, раз в час); пометка — убедиться, что адрес постоянный.
- Под письмом — «Памятка администратору QUIK» (классы, логин InstrEu, справочник,
  перезапуск модуля) со ссылкой на подробную инструкцию.
- Вместо свёрнутого «Автоматический цикл M2M — выключен · настроить» и справочника
  под раскрытием: после проверки подключения — «Как bj работает с QUIK»: «Только
  сверка» или «Автоматически» (+ торговый счёт), сохраняется сразу; справочник —
  строкой состояния с напоминанием о перезапуске. До проверки — одна строка.
  Тонкая настройка — в «Настройка → QUIK», мастер её не трогает.
- Проверка МНП: ошибка сертификата (x509, «doesn't contain any IP SANs») —
  подсказка включить «Доверять сертификату сервера», «Дополнительно» раскрывается.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-06 16:59:21 +03:00

144 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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": // коротко и по делу: о внутреннем устройстве (bj, процедуры, классы) поставщику знать не нужно
return Letter{Key: key, Title: "Письмо в ARQA Technologies: модуль неторговых поручений", To: "ARQA Technologies, служба поддержки QUIK",
Subject: "Подключение модуля неторговых поручений QUIK — " + name,
Body: fmt.Sprintf("В ARQA Technologies\n(служба поддержки QUIK)\n\n%s (ИНН %s) просит:\n\n1. Подключить модуль неторговых поручений QUIK (переводы ценных бумаг M2M).\n2. Выдать логин и пароль для подключения к базе данных модуля неторговых поручений.\n3. Разрешить подключение к базе данных модуля с нашего адреса %s и сообщить адрес сервера, порт и имя базы.%s",
name, inn, arqaAddr(publicIP()), sign)}, true
case "dba":
ip := pickStr(outboundIP(s.Fancy.Server, s.Fancy.Port), firstServerIP())
f := s.Fancy
what := "— создаёт задание SQL Server Agent «" + depoJobName(f) + "» для обработки обмена;\n"
run := "под логином с правами sysadmin (SSMS или sqlcmd)"
if f.IsPostgres() {
what = ""
run = "суперпользователем: psql -h <сервер> -U postgres -f " + depoSetupFileName(f)
} else if f.DepoJobMode == "bj" {
what = "— даёт логину bj право запускать процедуру обработки обмена;\n"
}
return Letter{Key: key, Title: "Письмо администратору базы DEPO", To: "Администратор СУБД учёта DEPO",
Subject: "Bridge&Join: подготовка базы DEPO на сервере " + orBracket(f.Server, "сервер DEPO"),
Body: fmt.Sprintf("Коллеги, добрый день!\n\nДля подключения системы Bridge&Join к учёту DEPO нужно на сервере %s выполнить один скрипт %s %s (файл прилагается — он скачивается в мастере настройки bj). Скрипт:\n— в начале — имя базы DEPO и пароль логина bj: проверьте их; скрипт сначала убедится, что это база DEPO (есть учётные таблицы OD_FACES и OD_RESTS), иначе остановится, ничего не меняя;\n— создаёт логин bj: права — чтение учётных таблиц DEPO и запись только в таблицы обмена FTV_M2M*;\n— создаёт таблицы обмена FTV_M2M*; учётные данные он не меняет;\n%s\nСкрипт можно запускать повторно. В нём пароль логина bj — после выполнения файл удалите.\n\nТакже нужно разрешить подключение к серверу базы с сервера Bridge&Join (адрес %s). Процедуры обработки обмена FTV_M2M_* поставляются отдельно.%s",
orBracket(f.Server, "сервер DEPO"), depoSetupFileName(f), run, what, orBracket(ip, "IP-адрес сервера bj"), sign)}, true
}
return Letter{}, false
}
// arqaAddr — наш внешний адрес для письма в ARQA с пометкой для отправителя: подключение разрешат только
// с этого адреса, поэтому он должен быть постоянным (у провайдера адрес может меняться). Пометку перед
// отправкой удаляют — как и всё в квадратных скобках.
func arqaAddr(ip string) string {
if ip == "" {
return "[постоянный внешний IP-адрес вашего сервера — уточните у сетевого администратора]"
}
return ip + " [убедитесь, что это постоянный внешний адрес вашей организации и он не меняется — уточните у сетевого администратора; пометку удалите]"
}
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))
}