21d87fb530
На одном сервере бывает несколько баз DEPO (боевая и тестовая): общее имя bj2depo давало ложную зелёную проверку — тестовой базе засчитывалось боевое задание, а своё не создавалось. - Имя задания — настройка DepoJobName; в мастере, пункт 3, поле «Имя задания SQL Server Agent», по умолчанию bj2depo_<имя базы> (меняется вместе с базой, пока его не поправили). Установки старше — bj2depo, как было. - Скрипт до любых изменений останавливается, если задание с этим именем обрабатывает другую базу или эту базу уже обрабатывает задание с другим именем (иначе двойная обработка событий). - Проверка задания сравнивает его базу (sp_help_jobstep) с базой bj. Синтаксис проверен на SQL Server без выполнения (SET PARSEONLY); проверка задания на боевом сервере — зелёная (bj2depo ↔ BJ_DEPO_DATA). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
135 lines
11 KiB
Go
135 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
|
||
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
|
||
}
|
||
|
||
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))
|
||
}
|