Files
Bridge-and-Join-s/internal/lkgateway/fancydb_handlers.go
T

339 lines
13 KiB
Go

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/detect", h.detectFancyRequisites) // реквизиты НРД/организации из DEPO
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/setup/fancy/depo-login/script.sql", h.fancyDepoLoginScript)
mux.HandleFunc("/admin/setup/fancy/depo-job/script.sql", h.fancyDepoJobScript)
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 !requireDepo(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 !requireDepo(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 !requireDepo(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 !requireDepo(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)
pmsg, pok := depoProcCheck(ctx, h.rc)
s := h.rc.Snapshot().Fancy
s.DepoJobCheck, s.DepoJobCheckOK, s.DepoJobCheckAt = msg, ok, time.Now()
s.DepoProcCheck, s.DepoProcCheckOK, s.DepoProcCheckAt = pmsg, pok, time.Now()
_ = h.rc.UpdateFancy(s)
run := "НЕ готово — "
if ok {
run = "готово — "
}
proc := "НЕ работает — "
if pok {
proc = "работает — "
}
setupFlash(w, r, "Запуск обработки (джоба): "+run+msg+" · Обработка событий (процедуры): "+proc+pmsg)
}
// createFancyDepoJob — POST /admin/setup/fancy/depo-job/create: создать джобу SQL Agent от имени bj.
func (h *setupHandlers) createFancyDepoJob(w http.ResponseWriter, r *http.Request) {
if !requireDepo(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, r.FormValue("admin_user"), r.FormValue("admin_password")); 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 !requireDepo(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"
}
// fancyDepoJobScript — GET /admin/setup/fancy/depo-job/script.sql: скрипт джобы SQL Agent для администратора.
func (h *setupHandlers) fancyDepoJobScript(w http.ResponseWriter, r *http.Request) {
s := h.rc.Snapshot().Fancy
w.Header().Set("Content-Type", "application/sql; charset=utf-8")
w.Header().Set("Content-Disposition", `attachment; filename="bj-depo-job-mssql.sql"`)
_, _ = w.Write([]byte("\uFEFF" + depoJobAgentAdminScript(s))) // SSMS без BOM читает кириллицу как ANSI
}