Первичная настройка: ключ спрашивается один раз, затем обновления, модули и «Время и сеть»; DEPO одним шагом
- Первая страница — только администратор (и восстановление из копии); ключ продукта — после создания администратора, на шаге «Лицензия». Консоль первой загрузки ключ больше не спрашивает (для образа с комплектом bj). - «Подготовка модулей» начинает с проверки обновлений: есть — ставит (bj перезапускается, страница ждёт и продолжает), нет — дальше; выпуск, который уже ставили, по кругу не ставится. Затем модули лицензии и переход на «Время и сеть» (шаг 12) — часовой пояс клиент выбирает сам, шаг больше не проскакивает. - Учёт DEPO одним шагом: bj сам находит базу Fancy DEPO на сервере (по таблицам OD_FACES/OD_RESTS), имя базы не спрашивается. Один скрипт создаёт логин bj, таблицы обмена и права; по выбору — задание обработки в SQL Agent или обработка на сервере bj. Выполнить сразу (логин администратора СУБД не сохраняется), скачать скрипт для администратора или подключиться готовым логином. - Восстановление из копии — только на первой странице, в мастере его больше нет. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -9,17 +9,17 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// depoWizardSubs — экраны шага 3 по порядку.
|
||||
// Шаг 3 — два экрана (06.10.2026): подготовка базы одним действием и проверка. Прежние адреса экранов
|
||||
// (intro, conn, tables, job) ведут на подготовку.
|
||||
var depoWizardSubs = []struct{ Key, Title string }{
|
||||
{"intro", "Что это"},
|
||||
{"conn", "Подключение"},
|
||||
{"tables", "Таблицы обмена"},
|
||||
{"job", "Обработка"},
|
||||
{"done", "Готово"},
|
||||
{"setup", "Подключение и подготовка базы"},
|
||||
{"done", "Проверка"},
|
||||
}
|
||||
|
||||
func depoWizardSub(v string) string {
|
||||
@@ -28,11 +28,14 @@ func depoWizardSub(v string) string {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "intro"
|
||||
return "setup"
|
||||
}
|
||||
|
||||
// wizDepoGo — перейти на экран шага 3 с сообщением (ok=false — сообщение об ошибке).
|
||||
func wizDepoGo(w http.ResponseWriter, r *http.Request, sub, msg string, ok bool) {
|
||||
if sub == "intro" || sub == "conn" || sub == "tables" || sub == "job" {
|
||||
sub = "setup"
|
||||
}
|
||||
u := "/admin/wizard?step=3&sub=" + sub
|
||||
if msg != "" {
|
||||
if ok {
|
||||
@@ -45,21 +48,17 @@ func wizDepoGo(w http.ResponseWriter, r *http.Request, sub, msg string, ok bool)
|
||||
http.Redirect(w, r, u, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// wizardDepoConn — POST /admin/setup/wizard-depo/conn: сохранить подключение и сразу проверить его.
|
||||
// wizardDepoConn — POST /admin/setup/wizard-depo/conn: сохранить подключение и сразу проверить его. Имя базы
|
||||
// DEPO клиент не вводит: bj находит её на сервере сам (depoDiscover); вручную — только в «Дополнительно».
|
||||
func (h *setupHandlers) wizardDepoConn(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || !requireDepo(w, r) {
|
||||
return
|
||||
}
|
||||
s := h.fancyFromForm(r)
|
||||
if db := strings.TrimSpace(r.FormValue("depo_database")); db != "" {
|
||||
if !validDBName(db) {
|
||||
wizDepoGo(w, r, "conn", "имя базы DEPO — только латинские буквы, цифры и _, начинается с буквы", false)
|
||||
return
|
||||
}
|
||||
if db != s.DepoDatabase {
|
||||
s.DepoTablesCheck, s.DepoTablesCheckOK = "", false
|
||||
}
|
||||
s.DepoDatabase = db
|
||||
manual := strings.TrimSpace(r.FormValue("depo_database"))
|
||||
if manual != "" && !validDBName(manual) {
|
||||
wizDepoGo(w, r, "conn", "имя базы DEPO — только латинские буквы, цифры и _, начинается с буквы", false)
|
||||
return
|
||||
}
|
||||
if s.Server == "" || s.User == "" {
|
||||
wizDepoGo(w, r, "conn", "укажите адрес сервера и логин", false)
|
||||
@@ -69,17 +68,54 @@ func (h *setupHandlers) wizardDepoConn(w http.ResponseWriter, r *http.Request) {
|
||||
wizDepoGo(w, r, "conn", "не сохранилось: "+err.Error(), false)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 45*time.Second)
|
||||
defer cancel()
|
||||
if _, err := fancyPing(ctx, s, "master"); err != nil {
|
||||
if _, err := fancyPing(ctx, s, ""); err != nil {
|
||||
wizDepoGo(w, r, "conn", "к серверу подключиться не удалось — "+err.Error()+". Проверьте адрес, порт, логин и пароль.", false)
|
||||
return
|
||||
}
|
||||
if _, err := fancyPing(ctx, s, s.DepoDB()); err != nil {
|
||||
wizDepoGo(w, r, "conn", "сервер отвечает, но база "+s.DepoDB()+" недоступна этим логином — "+err.Error(), false)
|
||||
db, how := manual, "указана вручную"
|
||||
if db == "" {
|
||||
found, err := depoDiscover(ctx, s)
|
||||
switch {
|
||||
case err != nil:
|
||||
wizDepoGo(w, r, "conn", "сервер отвечает, но список баз получить не удалось — "+err.Error()+". Укажите базу DEPO вручную (ниже, «Дополнительно»).", false)
|
||||
return
|
||||
case len(found) == 0:
|
||||
wizDepoGo(w, r, "conn", "на сервере не найдена база учёта DEPO (Fancy), доступная логину "+s.User+
|
||||
": нет базы с таблицами "+strings.Join(depoMarkerTables, " и ")+". Проверьте, что логину выдан доступ к базе DEPO, или укажите её вручную («Дополнительно»).", false)
|
||||
return
|
||||
case len(found) > 1:
|
||||
http.Redirect(w, r, "/admin/wizard?step=3&sub=conn&cands="+url.QueryEscape(strings.Join(found, ","))+
|
||||
"&flash="+url.QueryEscape("На сервере несколько баз DEPO — выберите, с какой работает bj."), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
db, how = found[0], "найдена на сервере"
|
||||
}
|
||||
if db != s.DepoDatabase {
|
||||
s.DepoTablesCheck, s.DepoTablesCheckOK = "", false
|
||||
}
|
||||
s.DepoDatabase = db
|
||||
if err := h.rc.UpdateFancy(s); err != nil {
|
||||
wizDepoGo(w, r, "conn", "не сохранилось: "+err.Error(), false)
|
||||
return
|
||||
}
|
||||
wizDepoGo(w, r, "tables", "подключение к "+s.Server+", база "+s.DepoDB()+" — работает", true)
|
||||
if _, err := fancyPing(ctx, s, db); err != nil {
|
||||
wizDepoGo(w, r, "conn", "сервер отвечает, но база "+db+" недоступна этим логином — "+err.Error(), false)
|
||||
return
|
||||
}
|
||||
wizDepoGo(w, r, "tables", "подключение к "+s.Server+" работает; база DEPO — "+db+" ("+how+")", true)
|
||||
}
|
||||
|
||||
// depoCands — базы DEPO для выбора на экране «Подключение» (из адреса после поиска на сервере).
|
||||
func depoCands(v string) []string {
|
||||
var out []string
|
||||
for _, n := range strings.Split(v, ",") {
|
||||
if n = strings.TrimSpace(n); validDBName(n) {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// wizardDepoTables — POST /admin/setup/wizard-depo/tables: создать таблицы (bj) или проверить созданные
|
||||
@@ -178,13 +214,13 @@ func (h *setupHandlers) wizardDepoRecheck(w http.ResponseWriter, r *http.Request
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 90*time.Second)
|
||||
defer cancel()
|
||||
if _, err := fancyPing(ctx, s, s.DepoDB()); err != nil {
|
||||
wizDepoGo(w, r, "conn", "подключение не работает — "+err.Error(), false)
|
||||
wizDepoGo(w, r, "setup", "подключение не работает — "+err.Error(), false)
|
||||
return
|
||||
}
|
||||
msg, ok := depoTablesCheck(ctx, s)
|
||||
h.saveDepoTablesCheck(msg, ok)
|
||||
if !ok {
|
||||
wizDepoGo(w, r, "tables", msg, false)
|
||||
wizDepoGo(w, r, "setup", msg, false)
|
||||
return
|
||||
}
|
||||
jmsg, jok := depoJobCheck(ctx, h.rc)
|
||||
@@ -194,8 +230,180 @@ func (h *setupHandlers) wizardDepoRecheck(w http.ResponseWriter, r *http.Request
|
||||
s.DepoProcCheck, s.DepoProcCheckOK, s.DepoProcCheckAt = pmsg, pok, time.Now()
|
||||
_ = h.rc.UpdateFancy(s)
|
||||
if !jok {
|
||||
wizDepoGo(w, r, "job", jmsg, false)
|
||||
wizDepoGo(w, r, "done", "обработка не запускается — "+jmsg, false)
|
||||
return
|
||||
}
|
||||
wizDepoGo(w, r, "done", "всё перепроверено", true)
|
||||
}
|
||||
|
||||
// depoSetupForm — сервер DEPO и способ обработки из формы экрана «Подготовка базы» (логин bj не трогается).
|
||||
func (h *setupHandlers) depoSetupForm(r *http.Request) (FancyMSSQLSettings, string, bool, string) {
|
||||
s := h.rc.Snapshot().Fancy
|
||||
dbType := "mssql"
|
||||
if r.FormValue("db_type") == "postgres" {
|
||||
dbType = "postgres"
|
||||
}
|
||||
port, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("port")))
|
||||
if port == 0 {
|
||||
port = map[string]int{"mssql": 1433, "postgres": 5432}[dbType]
|
||||
}
|
||||
server := strings.TrimSpace(r.FormValue("server"))
|
||||
if server != s.Server || dbType != s.DBType {
|
||||
s.DepoTablesCheck, s.DepoTablesCheckOK, s.DepoJobCheck, s.DepoJobCheckOK = "", false, "", false
|
||||
s.DepoDatabase = ""
|
||||
}
|
||||
s.DBType, s.Port, s.Server = dbType, port, server
|
||||
if s.Encrypt == "" {
|
||||
s.Encrypt = "disable"
|
||||
}
|
||||
agent := r.FormValue("depo_job") != "bj" && dbType != "postgres"
|
||||
s.DepoJobMode, s.DepoJobRun = "bj", !agent
|
||||
if agent {
|
||||
s.DepoJobMode = "sql-auto"
|
||||
}
|
||||
db := strings.TrimSpace(r.FormValue("depo_database"))
|
||||
if db != "" && !validDBName(db) {
|
||||
return s, "", agent, "имя базы DEPO — только латинские буквы, цифры и _, начинается с буквы"
|
||||
}
|
||||
if db == "" {
|
||||
db = s.DepoDatabase
|
||||
}
|
||||
if dbType == "postgres" && db == "" {
|
||||
return s, "", agent, "для PostgreSQL укажите имя базы DEPO"
|
||||
}
|
||||
if server == "" {
|
||||
return s, db, agent, "укажите адрес сервера DEPO"
|
||||
}
|
||||
return s, db, agent, ""
|
||||
}
|
||||
|
||||
// depoEnsureLogin — логин bj и его пароль для скрипта (пароль придумывает bj и запоминает: после
|
||||
// выполнения скрипта bj входит с ним; повторная выдача скрипта — с тем же паролем).
|
||||
func depoEnsureLogin(s *FancyMSSQLSettings) {
|
||||
login := depoSetupLogin(*s)
|
||||
if s.User != login || s.Password == "" {
|
||||
s.User, s.Password = login, depoGenPassword()
|
||||
}
|
||||
}
|
||||
|
||||
// wizardDepoScript — POST /admin/setup/wizard-depo/script.sql: один скрипт подготовки базы файлом.
|
||||
func (h *setupHandlers) wizardDepoScript(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || !requireDepo(w, r) {
|
||||
return
|
||||
}
|
||||
s, db, agent, bad := h.depoSetupForm(r)
|
||||
if bad != "" {
|
||||
wizDepoGo(w, r, "setup", bad, false)
|
||||
return
|
||||
}
|
||||
depoEnsureLogin(&s)
|
||||
s.DepoTablesMode = "script"
|
||||
if err := h.rc.UpdateFancy(s); err != nil {
|
||||
wizDepoGo(w, r, "setup", "не сохранилось: "+err.Error(), false)
|
||||
return
|
||||
}
|
||||
body := depoSetupScript(s, db, s.User, s.Password, agent)
|
||||
if !s.IsPostgres() {
|
||||
body = "\uFEFF" + body // SSMS без BOM читает кириллицу в комментариях как ANSI
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Content-Type", "application/sql; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+depoSetupFileName(s)+`"`)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}
|
||||
|
||||
// wizardDepoSetup — POST /admin/setup/wizard-depo/setup: подготовить базу (mode=auto — bj сам логином
|
||||
// администратора), проверить после скрипта (mode=script) или с готовым логином (mode=existing).
|
||||
func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || !requireDepo(w, r) {
|
||||
return
|
||||
}
|
||||
s, db, agent, bad := h.depoSetupForm(r)
|
||||
if bad != "" {
|
||||
wizDepoGo(w, r, "setup", bad, false)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
|
||||
defer cancel()
|
||||
note := ""
|
||||
switch r.FormValue("mode") {
|
||||
case "auto":
|
||||
au := strings.TrimSpace(r.FormValue("admin_user"))
|
||||
if au == "" {
|
||||
wizDepoGo(w, r, "setup", "введите логин и пароль администратора SQL (они используются один раз и не сохраняются)", false)
|
||||
return
|
||||
}
|
||||
depoEnsureLogin(&s)
|
||||
found, err := depoSetupRun(ctx, s, db, au, r.FormValue("admin_password"), s.User, s.Password, agent)
|
||||
if err != nil {
|
||||
wizDepoGo(w, r, "setup", "база не подготовлена — "+err.Error(), false)
|
||||
return
|
||||
}
|
||||
s.DepoDatabase, s.DepoTablesMode = found, "auto"
|
||||
note = "база " + found + " подготовлена: логин " + s.User + ", таблицы обмена, права"
|
||||
if agent {
|
||||
note += ", задание " + depoJobName
|
||||
}
|
||||
note += ". "
|
||||
case "existing":
|
||||
u := strings.TrimSpace(r.FormValue("user"))
|
||||
if u == "" {
|
||||
wizDepoGo(w, r, "setup", "введите логин bj и пароль", false)
|
||||
return
|
||||
}
|
||||
if u != s.User || r.FormValue("password") != "" {
|
||||
s.User, s.Password = u, r.FormValue("password")
|
||||
}
|
||||
default: // script — администратор выполнил скрипт: логин и пароль bj уже в настройках
|
||||
if s.User == "" || s.Password == "" {
|
||||
wizDepoGo(w, r, "setup", "сначала скачайте скрипт и передайте администратору SQL", false)
|
||||
return
|
||||
}
|
||||
}
|
||||
if db != "" {
|
||||
s.DepoDatabase = db
|
||||
}
|
||||
if err := h.rc.UpdateFancy(s); err != nil {
|
||||
wizDepoGo(w, r, "setup", "не сохранилось: "+err.Error(), false)
|
||||
return
|
||||
}
|
||||
if _, err := fancyPing(ctx, s, ""); err != nil {
|
||||
wizDepoGo(w, r, "setup", note+"войти логином "+s.User+" не удалось — "+quikErrorDetail(err)+". Выполнен ли скрипт на сервере "+s.Server+"?", false)
|
||||
return
|
||||
}
|
||||
if s.DepoDatabase == "" { // база не указана и скрипт её нашёл сам — найти и нам
|
||||
found, err := depoDiscover(ctx, s)
|
||||
switch {
|
||||
case err != nil || len(found) == 0:
|
||||
wizDepoGo(w, r, "setup", "логину "+s.User+" не видна ни одна база DEPO — выполнен ли скрипт? Можно указать базу вручную («Дополнительно»)", false)
|
||||
return
|
||||
case len(found) > 1:
|
||||
http.Redirect(w, r, "/admin/wizard?step=3&sub=setup&cands="+url.QueryEscape(strings.Join(found, ","))+
|
||||
"&flash="+url.QueryEscape("Логину bj видны несколько баз DEPO — выберите нужную и проверьте ещё раз."), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
s.DepoDatabase = found[0]
|
||||
_ = h.rc.UpdateFancy(s)
|
||||
}
|
||||
msg, ok := depoTablesCheck(ctx, s)
|
||||
h.saveDepoTablesCheck(msg, ok)
|
||||
if !ok {
|
||||
wizDepoGo(w, r, "setup", note+msg, false)
|
||||
return
|
||||
}
|
||||
if !s.IsPostgres() {
|
||||
_ = depoWriteHandbook(h.rc)
|
||||
}
|
||||
// запуск обработки: только что созданное задание запускается в течение интервала — подождать
|
||||
jmsg, jok := depoJobRunnerCheck(ctx, h.rc, "", "")
|
||||
for i := 0; !jok && i < 6 && strings.Contains(jmsg, "ни разу не запускалась"); i++ {
|
||||
time.Sleep(5 * time.Second)
|
||||
jmsg, jok = depoJobRunnerCheck(ctx, h.rc, "", "")
|
||||
}
|
||||
pmsg, pok := depoProcCheck(ctx, h.rc) // процедуры обработки ставятся отдельно — шаг не держат
|
||||
cur := h.rc.Snapshot().Fancy
|
||||
cur.DepoJobCheck, cur.DepoJobCheckOK, cur.DepoJobCheckAt = jmsg, jok, time.Now()
|
||||
cur.DepoProcCheck, cur.DepoProcCheckOK, cur.DepoProcCheckAt = pmsg, pok, time.Now()
|
||||
_ = h.rc.UpdateFancy(cur)
|
||||
wizDepoGo(w, r, "done", note+"подключение и таблицы обмена проверены", true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user