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" }