Files
Bridge-and-Join-s/internal/lkgateway/fancydb_handlers.go
T
zuevav da12a9ec1c 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>
2026-09-15 19:41:12 +03:00

322 lines
12 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
import (
"context"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
)
// Хендлеры вкладки «Обмен Fancy (MSSQL)»: подключение, тест, создание БД,
// наливка схемы. Маршруты вешаются в registerSetup (см. registerFancy).
func (h *setupHandlers) registerFancy(mux *http.ServeMux) {
mux.HandleFunc("/admin/setup/fancy", h.saveFancy)
mux.HandleFunc("/admin/setup/fancy/test", h.testFancy)
mux.HandleFunc("/admin/setup/fancy/depo-job", h.saveFancyDepoJob)
mux.HandleFunc("/admin/setup/fancy/depo-job/check", h.checkFancyDepoJob)
mux.HandleFunc("/admin/setup/fancy/depo-job/create", h.createFancyDepoJob)
mux.HandleFunc("/admin/setup/fancy/depo-tables", h.saveFancyDepoTables)
mux.HandleFunc("/admin/setup/fancy/depo-tables/create", h.createFancyDepoTables)
mux.HandleFunc("/admin/setup/fancy/depo-tables/check", h.checkFancyDepoTables)
mux.HandleFunc("/admin/setup/fancy/depo-tables/script.sql", h.fancyDepoTablesScript)
mux.HandleFunc("/admin/fancy-queue", h.fancyQueue)
mux.HandleFunc("/admin/fancy-queue/requeue", h.fancyQueueRequeue)
}
// fancyFromForm собирает настройки из формы (пароль пустой = оставить прежний).
func (h *setupHandlers) fancyFromForm(r *http.Request) FancyMSSQLSettings {
cur := h.rc.Snapshot().Fancy
dbType := strings.TrimSpace(r.FormValue("db_type"))
if dbType != "postgres" {
dbType = "mssql"
}
port, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("port")))
if port == 0 {
if dbType == "postgres" {
port = 5432
} else {
port = 1433
}
}
pass := r.FormValue("password")
if pass == "" {
pass = cur.Password // не затираем сохранённый пароль пустым полем
}
enc := strings.TrimSpace(r.FormValue("encrypt"))
if enc == "" {
enc = "disable"
}
// остальные настройки (обработка DEPO, трансферные таблицы) сохраняем как были
s := cur
s.DBType, s.Port, s.Password, s.Encrypt = dbType, port, pass, enc
s.Server = strings.TrimSpace(r.FormValue("server"))
s.User = strings.TrimSpace(r.FormValue("user"))
if s.Server != cur.Server || s.DBType != cur.DBType || s.User != cur.User {
s.DepoTablesCheck, s.DepoTablesCheckOK = "", false
s.DepoJobCheck, s.DepoJobCheckOK = "", false
}
return s
}
// saveFancyDepoTables — POST /admin/setup/fancy/depo-tables: база DEPO и способ создания таблиц.
func (h *setupHandlers) saveFancyDepoTables(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Fancy
db := strings.TrimSpace(r.FormValue("depo_database"))
if db != "" && !validDBName(db) {
setupFlash(w, r, "Таблицы DEPO: имя базы — только латинские буквы/цифры/_, начинать с буквы")
return
}
if m := strings.TrimSpace(r.FormValue("iis_contract_marker")); len([]rune(m)) <= 100 {
s.IISContractMarker = m
}
mode := r.FormValue("depo_tables_mode")
if mode != "auto" && mode != "script" {
mode = ""
}
if db != s.DepoDatabase {
s.DepoTablesCheck, s.DepoTablesCheckOK = "", false
}
s.DepoDatabase, s.DepoTablesMode = db, mode
if err := h.rc.UpdateFancy(s); err != nil {
setupFlash(w, r, "Таблицы DEPO: ошибка сохранения: "+err.Error())
return
}
switch mode {
case "auto":
setupFlash(w, r, "Таблицы DEPO: bj создаст их в базе "+s.DepoDB()+" — введите логин с правами и нажмите «Создать таблицы».")
case "script":
setupFlash(w, r, "Таблицы DEPO: передайте скрипт администратору СУБД, после выполнения нажмите «Проверить таблицы».")
default:
setupFlash(w, r, "Таблицы DEPO: база "+s.DepoDB()+" сохранена, способ создания не выбран")
}
}
func (h *setupHandlers) saveDepoTablesCheck(msg string, ok bool) {
s := h.rc.Snapshot().Fancy
s.DepoTablesCheck, s.DepoTablesCheckOK, s.DepoTablesCheckAt = msg, ok, time.Now()
_ = h.rc.UpdateFancy(s)
}
// createFancyDepoTables — POST /admin/setup/fancy/depo-tables/create: bj создаёт таблицы сам.
// Логин с правами берётся из формы и нигде не сохраняется.
func (h *setupHandlers) createFancyDepoTables(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Fancy
if s.Server == "" {
setupFlash(w, r, "Таблицы DEPO: сначала сохраните подключение к серверу БД")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
defer cancel()
msg, err := depoTablesCreate(ctx, s, r.FormValue("admin_user"), r.FormValue("admin_password"))
if err != nil {
log.Printf("depo-tables: создание не удалось: %v", err)
setupFlash(w, r, "Таблицы DEPO: не созданы — "+err.Error())
return
}
log.Printf("depo-tables: %s", msg)
check, ok := depoTablesCheck(ctx, s)
h.saveDepoTablesCheck(check, ok)
if ok && !s.IsPostgres() {
if err := depoWriteHandbook(h.rc); err == nil {
msg += "; справочник участников записан"
} else {
log.Printf("depo-tables: справочник: %v", err)
}
}
setupFlash(w, r, "Таблицы DEPO: "+msg+". Проверка: "+check)
}
// checkFancyDepoTables — POST /admin/setup/fancy/depo-tables/check.
func (h *setupHandlers) checkFancyDepoTables(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Fancy
if s.Server == "" {
setupFlash(w, r, "Таблицы DEPO: сначала сохраните подключение к серверу БД")
return
}
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
msg, ok := depoTablesCheck(ctx, s)
h.saveDepoTablesCheck(msg, ok)
if ok {
setupFlash(w, r, "Таблицы DEPO: готово — "+msg)
} else {
setupFlash(w, r, "Таблицы DEPO: НЕ готово — "+msg)
}
}
// fancyDepoTablesScript — GET /admin/setup/fancy/depo-tables/script.sql: скрипт для администратора СУБД.
func (h *setupHandlers) fancyDepoTablesScript(w http.ResponseWriter, r *http.Request) {
s := h.rc.Snapshot().Fancy
body := depoTablesScript(s)
if !s.IsPostgres() {
body = "\uFEFF" + body // SSMS без BOM читает кириллицу в комментариях как ANSI
}
w.Header().Set("Content-Type", "application/sql; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="`+depoTablesFileName(s)+`"`)
_, _ = w.Write([]byte(body))
}
// saveFancyDepoJob — POST /admin/setup/fancy/depo-job: обработка трансферных таблиц силами bj.
func (h *setupHandlers) saveFancyDepoJob(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Fancy
mode := r.FormValue("depo_job_mode")
if mode != "bj" && mode != "sql-auto" && mode != "sql-manual" {
mode = ""
}
if s.DepoJobMode != mode {
s.DepoJobCheck, s.DepoJobCheckOK = "", false
}
s.DepoJobMode = mode
s.DepoJobRun = mode == "bj"
if v, err := strconv.Atoi(strings.TrimSpace(r.FormValue("depo_job_every_sec"))); err == nil && v >= 5 && v <= 3600 {
s.DepoJobEverySec = v
}
s.DepoJobProc = strings.TrimSpace(r.FormValue("depo_job_proc"))
if u := strings.TrimSpace(r.FormValue("depo_job_run_as")); u == "" || depoRunAsRe.MatchString(u) {
if u != s.DepoJobRunAs {
s.DepoJobCheck, s.DepoJobCheckOK = "", false
}
s.DepoJobRunAs = u
}
if err := h.rc.UpdateFancy(s); err != nil {
setupFlash(w, r, "Обработка DEPO: ошибка сохранения: "+err.Error())
return
}
switch mode {
case "bj":
setupFlash(w, r, fmt.Sprintf("Обработка DEPO: bj сам вызывает %s каждые %d с. Нажмите «Проверить готовность».", depoJobProcName(s), depoJobEvery(s)))
case "sql-auto":
setupFlash(w, r, "Обработка DEPO: джоба SQL Agent, создаёт bj. Выдайте права по инструкции и нажмите «Создать джобу».")
case "sql-manual":
setupFlash(w, r, "Обработка DEPO: джоба SQL Agent, настраивает администратор. Передайте инструкцию и нажмите «Проверить готовность».")
default:
setupFlash(w, r, "Обработка DEPO: способ не выбран")
}
}
// checkFancyDepoJob — POST /admin/setup/fancy/depo-job/check: проверка готовности выбранного способа.
func (h *setupHandlers) checkFancyDepoJob(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 90*time.Second)
defer cancel()
msg, ok := depoJobCheck(ctx, h.rc)
s := h.rc.Snapshot().Fancy
s.DepoJobCheck, s.DepoJobCheckOK, s.DepoJobCheckAt = msg, ok, time.Now()
_ = h.rc.UpdateFancy(s)
if ok {
setupFlash(w, r, "Обработка DEPO: готово — "+msg)
} else {
setupFlash(w, r, "Обработка DEPO: НЕ готово — "+msg)
}
}
// createFancyDepoJob — POST /admin/setup/fancy/depo-job/create: создать джобу SQL Agent от имени bj.
func (h *setupHandlers) createFancyDepoJob(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
defer cancel()
if err := depoJobCreateAgent(ctx, h.rc); err != nil {
setupFlash(w, r, "Создание джобы: "+err.Error())
return
}
msg, ok := depoJobCheck(ctx, h.rc)
s := h.rc.Snapshot().Fancy
s.DepoJobCheck, s.DepoJobCheckOK, s.DepoJobCheckAt = msg, ok, time.Now()
_ = h.rc.UpdateFancy(s)
setupFlash(w, r, "Джоба SQL Agent создана. Проверка: "+msg)
}
// saveFancy — POST /admin/setup/fancy. Сохраняет параметры подключения.
func (h *setupHandlers) saveFancy(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.fancyFromForm(r)
dbLabel := "MSSQL"
if s.IsPostgres() {
dbLabel = "PostgreSQL"
}
if s.Server == "" || s.User == "" {
setupFlash(w, r, dbLabel+": укажите сервер и пользователя")
return
}
if err := h.rc.UpdateFancy(s); err != nil {
setupFlash(w, r, dbLabel+": ошибка сохранения: "+err.Error())
return
}
setupFlash(w, r, fmt.Sprintf("%s: настройки сохранены (%s:%d)", dbLabel, s.Server, s.Port))
}
// testFancy — POST /admin/setup/fancy/test. Проверяет соединение к серверу.
func (h *setupHandlers) testFancy(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Fancy
if s.Server == "" {
setupFlash(w, r, "MSSQL: сначала сохраните параметры подключения")
return
}
// Проверяем соединение к серверу (master) — БД может ещё не существовать.
ver, err := fancyPing(r.Context(), s, "master")
if err != nil {
setupFlash(w, r, fancyDBLabel(s)+": соединение не удалось — "+err.Error())
return
}
setupFlash(w, r, fmt.Sprintf("%s: соединение OK. %s", fancyDBLabel(s), strings.TrimSpace(ver)))
}
// fancyDBLabel — «MSSQL» или «PostgreSQL» для сообщений.
func fancyDBLabel(s FancyMSSQLSettings) string {
if s.IsPostgres() {
return "PostgreSQL"
}
return "MSSQL"
}