Учёт DEPO: имя базы обязательно, bj базу по серверу не ищет

Имя базы DEPO задаёт клиент — скрипт и подготовка логином администратора
работают только с ней. Автопоиск по серверу убран (экран, скрипт, подготовка,
проверка подключения, старый экран conn), выбор из нескольких найденных баз —
тоже. Вместо поиска — защита от опечатки: скрипт (SQL Server и PostgreSQL),
подготовка и проверка подключения сначала убеждаются, что в названной базе
есть учётные таблицы OD_FACES и OD_RESTS, иначе останавливаются, ничего не
меняя.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-06 15:18:15 +03:00
parent 237203f29a
commit 35f70a1b49
8 changed files with 118 additions and 219 deletions
+40
View File
@@ -0,0 +1,40 @@
package lkgateway
// Проверка, что указанная база — учёт DEPO (06.10.2026). Имя базы клиент задаёт сам: bj по серверу базы не
// ищет. Чтобы опечатка в имени не привела bj в чужую базу, перед подготовкой и при проверке подключения
// bj смотрит, есть ли в ней учётные таблицы Fancy DEPO — лица OD_FACES и остатки OD_RESTS.
import (
"context"
"fmt"
"strings"
"time"
)
// depoMarkerTables — по каким таблицам база опознаётся как DEPO (Fancy).
var depoMarkerTables = []string{"OD_FACES", "OD_RESTS"}
// depoCheckIsDepo — в базе db есть учётные таблицы DEPO, видимые логину s.User.
func depoCheckIsDepo(ctx context.Context, s FancyMSSQLSettings, db string) error {
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
conn, err := fancyOpen(s, db)
if err != nil {
return err
}
defer conn.Close()
if err := conn.PingContext(cctx); err != nil {
return fmt.Errorf("база %s недоступна логину %s: %s", db, s.User, quikErrorDetail(err))
}
var miss []string
for _, t := range depoMarkerTables {
if !depoTableVisible(cctx, conn, s.IsPostgres(), t) {
miss = append(miss, t)
}
}
if len(miss) > 0 {
return fmt.Errorf("в базе %s нет учётных таблиц DEPO (%s) — проверьте имя базы: это не база учёта DEPO или у логина %s нет к ней доступа",
db, strings.Join(miss, ", "), s.User)
}
return nil
}
+33 -32
View File
@@ -2,8 +2,8 @@ package lkgateway
// Подготовка базы учёта DEPO одним действием (06.10.2026). Раньше это были три экрана мастера — логин bj,
// таблицы обмена, задание SQL Agent — и три скрипта. Теперь один скрипт делает всё:
// - находит базу DEPO на сервере сам (по учётным таблицам OD_FACES и OD_RESTS) — имя базы клиенту знать
// не нужно (SQL Server; для PostgreSQL базу назвать придётся: psql не умеет искать по чужим базам);
// - работает с базой, которую назвал клиент (сам по серверу не ищет), и сначала проверяет, что в ней есть
// учётные таблицы DEPO OD_FACES и OD_RESTS — опечатка в имени не уведёт в чужую базу;
// - создаёт логин bj (пароль придумывает bj и запоминает) или обновляет его пароль;
// - создаёт таблицы обмена FTV_M2M* и выдаёт логину bj права: чтение учёта, запись в таблицы обмена;
// - по галочке «обработку запускает SQL Server Agent» — создаёт задание bj2depo, иначе обработку
@@ -50,8 +50,8 @@ func depoGenPassword() string {
// sqlStr — строковый литерал T-SQL / PostgreSQL (одинарная кавычка удваивается).
func sqlStr(s string) string { return strings.ReplaceAll(s, "'", "''") }
// depoSetupScript — один скрипт подготовки базы DEPO. db — имя базы (пусто — найти на сервере; только SQL
// Server), agentJob — создать задание SQL Server Agent (иначе обработку вызывает bj).
// depoSetupScript — один скрипт подготовки базы DEPO. db — имя базы DEPO (задаёт клиент), agentJob — создать
// задание SQL Server Agent (иначе обработку вызывает bj).
func depoSetupScript(f FancyMSSQLSettings, db, login, pass string, agentJob bool) string {
if f.IsPostgres() {
return depoSetupScriptPG(f, db, login, pass)
@@ -65,13 +65,13 @@ func depoSetupScript(f FancyMSSQLSettings, db, login, pass string, agentJob bool
SET NOCOUNT ON;
-- ===== Проверьте перед запуском ==========================================
-- База DEPO: пусто — скрипт найдёт её сам по таблицам OD_FACES и OD_RESTS.
-- База DEPO — точное имя базы учёта на этом сервере.
DECLARE @db sysname = N'%[3]s';
-- Пароль логина bj (%[1]s). Меняете — впишите тот же пароль в bj, шаг «Учёт DEPO».
DECLARE @pass nvarchar(128) = N'%[4]s';
-- =========================================================================
-- Что делает: находит базу DEPO, создаёт логин bj и таблицы обмена FTV_M2M*, выдаёт права
-- Что делает: проверяет, что @db — база DEPO, создаёт логин bj и таблицы обмена FTV_M2M*, выдаёт права
-- (чтение учёта, запись только в таблицы обмена). Обработка обмена: %[2]s.
-- Учётные данные DEPO не меняет. Запускать в SSMS или sqlcmd под логином с правами sysadmin.
-- Можно запускать повторно. В скрипте пароль логина bj — после выполнения файл удалите.
@@ -81,18 +81,19 @@ BEGIN
RAISERROR(N'Впишите пароль логина bj в @pass в начале скрипта и запустите снова.', 16, 1);
RETURN;
END
DECLARE @found int;
IF @db = N''
BEGIN
SELECT @found = COUNT(*) FROM sys.databases WHERE state = 0 AND database_id > 4
AND OBJECT_ID(QUOTENAME(name) + N'.dbo.OD_FACES') IS NOT NULL AND OBJECT_ID(QUOTENAME(name) + N'.dbo.OD_RESTS') IS NOT NULL;
IF @found = 1
SELECT @db = name FROM sys.databases WHERE state = 0 AND database_id > 4
AND OBJECT_ID(QUOTENAME(name) + N'.dbo.OD_FACES') IS NOT NULL AND OBJECT_ID(QUOTENAME(name) + N'.dbo.OD_RESTS') IS NOT NULL;
RAISERROR(N'Впишите имя базы DEPO в @db в начале скрипта и запустите снова.', 16, 1);
RETURN;
END
IF @db = N'' OR DB_ID(@db) IS NULL
IF DB_ID(@db) IS NULL
BEGIN
RAISERROR(N'База DEPO не найдена или на сервере их несколько. Впишите имя базы DEPO в @db в начале скрипта и запустите снова.', 16, 1);
RAISERROR(N'Базы %%s на этом сервере нет. Проверьте имя в @db в начале скрипта.', 16, 1, @db);
RETURN;
END
IF OBJECT_ID(QUOTENAME(@db) + N'.dbo.OD_FACES', N'U') IS NULL OR OBJECT_ID(QUOTENAME(@db) + N'.dbo.OD_RESTS', N'U') IS NULL
BEGIN
RAISERROR(N'В базе %%s нет учётных таблиц DEPO (OD_FACES, OD_RESTS) — это не база DEPO. Проверьте имя в @db.', 16, 1, @db);
RETURN;
END
PRINT N'База DEPO: ' + @db;
@@ -162,6 +163,12 @@ END $$;
GRANT CONNECT ON DATABASE "%[1]s" TO "%[2]s";
\c "%[1]s"
-- это база DEPO? (учётные таблицы OD_FACES и OD_RESTS) — иначе остановиться, ничего не создавая
DO $$ BEGIN
IF to_regclass('od_faces') IS NULL OR to_regclass('od_rests') IS NULL THEN
RAISE EXCEPTION 'В базе %% нет учётных таблиц DEPO (OD_FACES, OD_RESTS) — проверьте имя базы', current_database();
END IF;
END $$;
`, db, login, sqlStr(pass))
for _, s := range depoTablesDDL(true) {
b.WriteString(s + ";\n")
@@ -182,37 +189,31 @@ func depoSetupFileName(f FancyMSSQLSettings) string {
return "bj-depo-setup-mssql.sql"
}
// depoSetupRun — «bj сделает сам»: тот же скрипт выполняется логином администратора (не сохраняется).
// Возвращает найденную базу DEPO.
func depoSetupRun(ctx context.Context, f FancyMSSQLSettings, db, adminUser, adminPass, login, pass string, agentJob bool) (string, error) {
// depoSetupRun — «bj сделает сам»: тот же скрипт выполняется логином администратора (не сохраняется) в базе
// db, которую назвал клиент; сначала — проверка, что это база DEPO.
func depoSetupRun(ctx context.Context, f FancyMSSQLSettings, db, adminUser, adminPass, login, pass string, agentJob bool) error {
adm := f
adm.User, adm.Password = strings.TrimSpace(adminUser), adminPass
if db == "" {
found, err := depoDiscover(ctx, adm)
switch {
case err != nil:
return "", fmt.Errorf("список баз сервера логином %s: %s", adm.User, quikErrorDetail(err))
case len(found) == 0:
return "", fmt.Errorf("на сервере нет базы учёта DEPO (с таблицами %s), доступной логину %s", strings.Join(depoMarkerTables, " и "), adm.User)
case len(found) > 1:
return "", fmt.Errorf("на сервере несколько баз DEPO (%s) — укажите нужную в поле «База DEPO»", strings.Join(found, ", "))
}
db = found[0]
if !validDBName(db) {
return fmt.Errorf("укажите имя базы DEPO")
}
if err := depoCheckIsDepo(ctx, adm, db); err != nil {
return err
}
if f.IsPostgres() {
return db, depoSetupRunPG(ctx, adm, db, login, pass)
return depoSetupRunPG(ctx, adm, db, login, pass)
}
conn, err := fancyOpen(adm, "")
if err != nil {
return db, err
return err
}
defer conn.Close()
cctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
if _, err := conn.ExecContext(cctx, depoSetupScript(f, db, login, pass, agentJob)); err != nil {
return db, fmt.Errorf("подготовка базы %s логином %s: %s%s", db, adm.User, quikErrorDetail(err), depoTablesRightsHint(f, adm.User, err))
return fmt.Errorf("подготовка базы %s логином %s: %s%s", db, adm.User, quikErrorDetail(err), depoTablesRightsHint(f, adm.User, err))
}
return db, nil
return nil
}
func depoSetupRunPG(ctx context.Context, adm FancyMSSQLSettings, db, login, pass string) error {
-98
View File
@@ -1,98 +0,0 @@
package lkgateway
// Поиск базы DEPO на сервере (05.10.2026). Клиент не знает и не должен знать, как называется база учёта у
// него на сервере: на шаге «Подключение» он вводит только адрес, логин и пароль, а bj сам находит базы с
// учётными таблицами Fancy DEPO (лица OD_FACES и остатки OD_RESTS), к которым у этого логина есть доступ.
// Одна база — выбирается сразу, несколько — мастер предлагает выбрать, ни одной — понятная ошибка.
import (
"context"
"fmt"
"strings"
"time"
)
// depoMarkerTables — по каким таблицам база опознаётся как DEPO (Fancy).
var depoMarkerTables = []string{"OD_FACES", "OD_RESTS"}
// depoDiscoverMax — сколько баз сервера просматривать (на сервере могут быть десятки баз).
const depoDiscoverMax = 60
// depoDiscover — базы DEPO на сервере, доступные этому логину.
func depoDiscover(ctx context.Context, s FancyMSSQLSettings) ([]string, error) {
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
if s.IsPostgres() {
return depoDiscoverPG(cctx, s)
}
conn, err := fancyOpen(s, "")
if err != nil {
return nil, err
}
defer conn.Close()
rows, err := conn.QueryContext(cctx, `SELECT name FROM sys.databases
WHERE state = 0 AND database_id > 4 AND HAS_DBACCESS(name) = 1 ORDER BY name`)
if err != nil {
return nil, fmt.Errorf("список баз сервера: %w", err)
}
var names []string
for rows.Next() {
var n string
if rows.Scan(&n) == nil && validDBName(n) && len(names) < depoDiscoverMax {
names = append(names, n)
}
}
rows.Close()
var found []string
for _, n := range names {
// имя проверено validDBName — подставлять в [ ] безопасно
q := "SELECT CASE WHEN " + strings.Join(func() []string {
var c []string
for _, t := range depoMarkerTables {
c = append(c, "OBJECT_ID(N'["+n+"].dbo."+t+"') IS NOT NULL")
}
return c
}(), " AND ") + " THEN 1 ELSE 0 END"
var ok int
if conn.QueryRowContext(cctx, q).Scan(&ok) == nil && ok == 1 {
found = append(found, n)
}
}
return found, nil
}
func depoDiscoverPG(ctx context.Context, s FancyMSSQLSettings) ([]string, error) {
conn, err := fancyOpen(s, "")
if err != nil {
return nil, err
}
defer conn.Close()
rows, err := conn.QueryContext(ctx, `SELECT datname FROM pg_database
WHERE datallowconn AND NOT datistemplate AND has_database_privilege(datname, 'CONNECT') ORDER BY datname`)
if err != nil {
return nil, fmt.Errorf("список баз сервера: %w", err)
}
var names []string
for rows.Next() {
var n string
if rows.Scan(&n) == nil && validDBName(n) && n != "postgres" && len(names) < depoDiscoverMax {
names = append(names, n)
}
}
rows.Close()
var found []string
for _, n := range names {
db, err := fancyOpen(s, n)
if err != nil {
continue
}
var cnt int
err = db.QueryRowContext(ctx, `SELECT count(DISTINCT lower(table_name)) FROM information_schema.tables
WHERE lower(table_name) = ANY($1::text[])`, "{"+strings.ToLower(strings.Join(depoMarkerTables, ","))+"}").Scan(&cnt)
db.Close()
if err == nil && cnt == len(depoMarkerTables) {
found = append(found, n)
}
}
return found, nil
}
+1 -1
View File
@@ -83,7 +83,7 @@ func letterFor(s Settings, key string) (Letter, bool) {
}
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: их можно поправить;\n— создаёт логин bj: права — чтение учётных таблиц DEPO и запись только в таблицы обмена FTV_M2M*;\n— создаёт таблицы обмена FTV_M2M*; учётные данные он не меняет;\n%s\nСкрипт можно запускать повторно. В нём пароль логина bj — после выполнения файл удалите.\n\nТакже нужно разрешить подключение к серверу базы с сервера Bridge&Join (адрес %s). Процедуры обработки обмена FTV_M2M_* поставляются отдельно.%s",
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
+13 -9
View File
@@ -347,17 +347,18 @@ func TestFromLocalBundle(t *testing.T) {
}
}
// Один скрипт подготовки базы DEPO: сам ищет базу, создаёт логин, таблицы, права и (по выбору) задание.
// Один скрипт подготовки базы DEPO: в названной базе (сам не ищет, но проверяет, что это DEPO) создаёт логин,
// таблицы, права и (по выбору) задание.
func TestDepoSetupScript(t *testing.T) {
f := FancyMSSQLSettings{}
s := depoSetupScript(f, "", "bj_user", "Pass1word", true)
for _, want := range []string{"DECLARE @db sysname = N'';", "DECLARE @pass nvarchar(128) = N'Pass1word';", "OD_FACES", "CREATE LOGIN [bj_user] WITH PASSWORD = N''' + REPLACE(@pass, N'''', N'''''')", "FTV_M2M_EVENTS", "db_datareader", "sp_add_job @job_name = N'bj2depo'", "@database_name = @db", "SQLAgentReaderRole"} {
s := depoSetupScript(f, "DEPO_DATA", "bj_user", "Pass1word", true)
for _, want := range []string{"DECLARE @db sysname = N'DEPO_DATA';", "RAISERROR(N'Базы %s на этом сервере нет.", "OBJECT_ID(QUOTENAME(@db) + N'.dbo.OD_FACES', N'U') IS NULL", "DECLARE @pass nvarchar(128) = N'Pass1word';", "OD_FACES", "CREATE LOGIN [bj_user] WITH PASSWORD = N''' + REPLACE(@pass, N'''', N'''''')", "FTV_M2M_EVENTS", "db_datareader", "sp_add_job @job_name = N'bj2depo'", "@database_name = @db", "SQLAgentReaderRole"} {
if !strings.Contains(s, want) {
t.Fatalf("в скрипте нет %q", want)
}
}
if strings.Contains(s, "BJ_DEPO_DATA") || strings.Contains(s, "\nGO") {
t.Fatal("в скрипте — имя нашей базы или GO (переменные @db между пакетами не живут)")
if strings.Contains(s, "BJ_DEPO_DATA") || strings.Contains(s, "\nGO") || strings.Contains(s, "sys.databases") || strings.Contains(s, "%!") {
t.Fatal("в скрипте — имя нашей базы, GO (переменные @db между пакетами не живут), поиск базы по серверу или сбой подстановки")
}
// каждая строка EXEC(@in + N'…') — закрытый литерал: чётное число кавычек
for _, line := range strings.Split(s, "\n") {
@@ -370,7 +371,7 @@ func TestDepoSetupScript(t *testing.T) {
t.Fatal("обработка силами bj: без задания, с правом на процедуру и указанной базой")
}
pg := depoSetupScript(FancyMSSQLSettings{DBType: "postgres"}, "depo", "bj_user", "p'w", false)
if !strings.Contains(pg, `\c "depo"`) || !strings.Contains(pg, "PASSWORD 'p''w'") {
if !strings.Contains(pg, `\c "depo"`) || !strings.Contains(pg, "PASSWORD 'p''w'") || !strings.Contains(pg, "RAISE EXCEPTION 'В базе % нет учётных таблиц DEPO") {
t.Fatal("PostgreSQL: подключение к базе и экранирование пароля")
}
if p := depoGenPassword(); len(p) != 24 || !strings.ContainsAny(p, "ABCDEFGHJKLMNPQRSTUVWXYZ") || !strings.ContainsAny(p, "23456789") {
@@ -443,7 +444,7 @@ func TestDumpOnboardingPages(t *testing.T) {
t.Fatal(err)
}
_ = os.WriteFile(filepath.Join(dir, "prepare.html"), []byte(b.String()), 0o644)
_ = os.WriteFile(filepath.Join(dir, "depo-script.sql"), []byte(depoSetupScript(FancyMSSQLSettings{}, "", "bj_user", "Xk7pQ2mN9vR4tL8wZ3hB6cJd", true)), 0o644)
_ = os.WriteFile(filepath.Join(dir, "depo-script.sql"), []byte(depoSetupScript(FancyMSSQLSettings{}, "DEPO_DATA", "bj_user", "Xk7pQ2mN9vR4tL8wZ3hB6cJd", true)), 0o644)
}
// Первая страница (администратор) — для снимка в TestDumpOnboardingPages.
@@ -503,9 +504,12 @@ func TestDepoScriptView(t *testing.T) {
if f := rc.Snapshot().Fancy; f.Server != "10.0.0.5" || f.Password != "Secret123x" {
t.Fatalf("без адреса сервера настройки не должны меняться: %q", f.Server)
}
// PostgreSQL — свой скрипт; без имени базы — понятная причина вместо скрипта
form.Set("db_type", "postgres")
// имя базы обязательно (bj по серверу не ищет) — без него вместо скрипта понятная причина
form.Set("depo_database", "")
if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "имя базы") {
t.Fatalf("SQL Server без базы: %d %s", w.Code, w.Body.String())
}
form.Set("db_type", "postgres")
if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "имя базы") {
t.Fatalf("PostgreSQL без базы: %d %s", w.Code, w.Body.String())
}
-2
View File
@@ -174,7 +174,6 @@ type WizardData struct {
DepoJobDef string // способ обработки по умолчанию для мастера
Fancy *SetupData // шаг 3: общий блок «Обмен Fancy» (тот же, что в настройках)
Sub string // экран шага 3 (учёт DEPO): intro|conn|tables|job|done
DepoCands []string // шаг 3: найдено несколько баз DEPO — выбор
DepoUser string // шаг 3, «скрипт администратору»: логин bj, который создаст скрипт
DepoPass string // и его пароль (сохранённый или придуманный сейчас — сохраняется со скриптом)
DepoMode string // выбранный способ подготовки базы: auto|script|existing
@@ -324,7 +323,6 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
}
if step == 3 {
d.Sub = depoWizardSub(r.URL.Query().Get("sub"))
d.DepoCands = depoCands(r.URL.Query().Get("cands"))
d.DepoUser, d.DepoPass = depoSetupLogin(s.Fancy), s.Fancy.Password
if s.Fancy.User != d.DepoUser || d.DepoPass == "" {
d.DepoPass = depoGenPassword()
@@ -78,7 +78,7 @@
.dq-sql { margin-top:14px; }
.dq-sql-head { display:flex; align-items:center; gap:8px; margin:0 0 6px; justify-content:flex-start; }
.dq-sql-st { flex:1; font-size:12.5px; color:var(--muted); }
.dq-sql-st.bad { color:var(--warn); }
.dq-sql pre:empty::before { content:attr(data-msg); color:var(--muted); font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; font-size:13px; }
.dq-sql pre { max-height:380px; min-height:140px; font-size:12px; margin:0; transition:opacity .2s; }
.dq-sql pre.dq-busy { opacity:.5; }
.dq.is-pg [data-ms], .dq:not(.is-pg) [data-pg] { display:none !important; }
@@ -179,10 +179,9 @@
<div class="dq-grid dq-srv">
<label>Адрес сервера<input type="text" name="server" value="{{.Settings.Fancy.Server}}" placeholder="IP-адрес или имя\экземпляр" required autofocus spellcheck="false"></label>
<label>Порт<input type="text" name="port" value="{{if .Settings.Fancy.Port}}{{.Settings.Fancy.Port}}{{else if $pg}}5432{{else}}1433{{end}}" inputmode="numeric"></label>
{{if .DepoCands}}<label>Имя базы <span class="dq-opt">— выберите</span><select name="depo_database">{{range .DepoCands}}<option>{{.}}</option>{{end}}</select></label>
{{else}}<label>Имя базы <span class="dq-opt" data-ms>— необязательно</span><input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="bj найдёт сам" pattern="[A-Za-z][A-Za-z0-9_]*" title="латинские буквы, цифры и _, начинается с буквы" spellcheck="false" {{if $pg}}required{{end}}></label>{{end}}
<label>Имя базы<input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="как называется на сервере" required pattern="[A-Za-z][A-Za-z0-9_]*" title="латинские буквы, цифры и _, начинается с буквы" spellcheck="false"></label>
</div>
<p class="dq-note">{{if .DepoCands}}Логину bj видны несколько баз DEPO — выберите нужную.{{else}}<span data-ms>Не знаете имя базы — оставьте пустым: bj найдёт её сам по учётным таблицам OD_FACES и OD_RESTS.</span><span data-pg>Для PostgreSQL имя базы нужно указать.</span>{{end}}</p>
<p class="dq-note">Точное имя базы учёта DEPO — bj будет работать только с ней. Перед подготовкой bj проверит, что в ней есть учётные таблицы DEPO, и в чужую базу ничего не запишет.</p>
</section>
<section class="dq-s">
@@ -221,9 +220,9 @@
<div class="dq-sql-head copy-bar">
<span class="dq-sql-st" id="wiz-depo-st">Скрипт собирается из ответов выше и обновляется сам.</span>
<button type="button" id="wiz-depo-copy">Скопировать</button>
<button type="submit" formaction="/admin/setup/wizard-depo/script.sql" formnovalidate>Скачать файлом</button>
<button type="submit" formaction="/admin/setup/wizard-depo/script.sql">Скачать файлом</button>
</div>
<pre class="copyable" id="wiz-depo-sql"></pre>
<pre class="copyable" id="wiz-depo-sql" data-msg="Впишите имя базы DEPO — скрипт появится здесь."></pre>
</div>
<p class="dq-note"><span data-ms>Администратор выполняет скрипт в SSMS или sqlcmd под логином с правами sysadmin.</span><span data-pg>Администратор выполняет скрипт в psql суперпользователем (postgres).</span> Когда выполнит — нажмите «Проверить подключение».</p>
{{template "letterCard" (letter "dba")}}
@@ -240,13 +239,12 @@
</section>
</form>
<script>(function(){
var f=document.getElementById('wiz-depo'), pre=document.getElementById('wiz-depo-sql'), st=document.getElementById('wiz-depo-st');
var f=document.getElementById('wiz-depo'), pre=document.getElementById('wiz-depo-sql');
var timer=0, seq=0, last='';
function sync(){ // СУБД меняет подсказки, порт и обязательность имени базы; способ — какую панель показать
var pg=f.db_type.value==='postgres', m=f.mode.value, db=f.querySelector('input[name=depo_database]');
var pg=f.db_type.value==='postgres', m=f.mode.value;
f.classList.toggle('is-pg', pg);
if(pg&&f.port.value==='1433') f.port.value='5432'; else if(!pg&&f.port.value==='5432') f.port.value='1433';
if(db){ db.required=pg; db.placeholder=pg?'обязательно':'bj найдёт сам'; }
f.admin_user.placeholder=pg?'postgres':'sa';
['auto','script','existing'].forEach(function(k){ document.getElementById('wiz-depo-'+k).hidden=m!==k; });
}
@@ -259,9 +257,9 @@
fetch('/admin/setup/wizard-depo/script.sql',{method:'POST',body:body,headers:{'Content-Type':'application/x-www-form-urlencoded'},cache:'no-store'})
.then(function(r){return r.text().then(function(t){return {ok:r.ok,t:t}})})
.then(function(x){ if(my!==seq) return; pre.classList.remove('dq-busy');
if(x.ok){ pre.textContent=x.t; st.textContent='Скрипт собирается из ответов выше и обновляется сам.'; st.classList.remove('bad'); }
else { st.textContent=x.t.trim().replace(/^./,function(c){return c.toUpperCase()})+' — скрипт обновится, когда поправите.'; st.classList.add('bad'); } })
.catch(function(){ if(my!==seq) return; last=''; pre.classList.remove('dq-busy'); st.textContent='Не получилось собрать скрипт — обновите страницу.'; st.classList.add('bad'); });
if(x.ok){ pre.textContent=x.t; }
else { pre.textContent=''; pre.dataset.msg=x.t.trim().replace(/^./,function(c){return c.toUpperCase()})+' — скрипт появится здесь.'; } })
.catch(function(){ if(my!==seq) return; last=''; pre.classList.remove('dq-busy'); pre.textContent=''; pre.dataset.msg='Не получилось собрать скрипт — обновите страницу.'; });
}
function changed(){ sync(); clearTimeout(timer); timer=setTimeout(load, 350); }
f.addEventListener('input', changed); f.addEventListener('change', changed);
+21 -65
View File
@@ -49,15 +49,15 @@ func wizDepoGo(w http.ResponseWriter, r *http.Request, sub, msg string, ok bool)
}
// wizardDepoConn — POST /admin/setup/wizard-depo/conn: сохранить подключение и сразу проверить его. Имя базы
// DEPO клиент не вводит: bj находит её на сервере сам (depoDiscover); вручную — только в «Дополнительно».
// DEPO клиент задаёт сам — bj по серверу не ищет, только проверяет, что это база DEPO.
func (h *setupHandlers) wizardDepoConn(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || !requireDepo(w, r) {
return
}
s := h.fancyFromForm(r)
manual := strings.TrimSpace(r.FormValue("depo_database"))
if manual != "" && !validDBName(manual) {
wizDepoGo(w, r, "conn", "имя базы DEPO — только латинские буквы, цифры и _, начинается с буквы", false)
if !validDBName(manual) {
wizDepoGo(w, r, "conn", "укажите имя базы DEPO — латинские буквы, цифры и _, начинается с буквы", false)
return
}
if s.Server == "" || s.User == "" {
@@ -74,23 +74,10 @@ func (h *setupHandlers) wizardDepoConn(w http.ResponseWriter, r *http.Request) {
wizDepoGo(w, r, "conn", "к серверу подключиться не удалось — "+err.Error()+". Проверьте адрес, порт, логин и пароль.", false)
return
}
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], "найдена на сервере"
db := manual
if err := depoCheckIsDepo(ctx, s, db); err != nil {
wizDepoGo(w, r, "conn", err.Error(), false)
return
}
if db != s.DepoDatabase {
s.DepoTablesCheck, s.DepoTablesCheckOK = "", false
@@ -100,22 +87,7 @@ func (h *setupHandlers) wizardDepoConn(w http.ResponseWriter, r *http.Request) {
wizDepoGo(w, r, "conn", "не сохранилось: "+err.Error(), false)
return
}
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
wizDepoGo(w, r, "tables", "подключение к "+s.Server+" работает; база DEPO — "+db, true)
}
// wizardDepoTables — POST /admin/setup/wizard-depo/tables: создать таблицы (bj) или проверить созданные
@@ -236,8 +208,8 @@ func (h *setupHandlers) wizardDepoRecheck(w http.ResponseWriter, r *http.Request
wizDepoGo(w, r, "done", "всё перепроверено", true)
}
// depoSetupForm — сервер DEPO и способ обработки из формы экрана «Подготовка базы» (логин bj не трогается;
// пустой адрес сервера не ошибка — скрипту он не нужен, подключению проверяют вызывающие).
// depoSetupForm — сервер DEPO, имя базы (обязательно) и способ обработки из формы экрана «Подготовка базы»
// (логин bj не трогается; пустой адрес сервера не ошибка — скрипту он не нужен, подключению проверяют вызывающие).
func (h *setupHandlers) depoSetupForm(r *http.Request) (FancyMSSQLSettings, string, bool, string) {
s := h.rc.Snapshot().Fancy
dbType := "mssql"
@@ -263,15 +235,12 @@ func (h *setupHandlers) depoSetupForm(r *http.Request) (FancyMSSQLSettings, stri
s.DepoJobMode = "sql-auto"
}
db := strings.TrimSpace(r.FormValue("depo_database"))
if db != "" && !validDBName(db) {
if db == "" { // bj по серверу базу не ищет: с какой базой работать, клиент говорит сам
return s, "", agent, "укажите имя базы DEPO — как она называется на сервере"
}
if !validDBName(db) {
return s, "", agent, "имя базы DEPO — только латинские буквы, цифры и _, начинается с буквы"
}
if db == "" {
db = s.DepoDatabase
}
if dbType == "postgres" && db == "" {
return s, "", agent, "для PostgreSQL укажите имя базы DEPO"
}
return s, db, agent, ""
}
@@ -374,13 +343,12 @@ func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request)
return
}
depoEnsureLogin(&s)
found, err := depoSetupRun(ctx, s, db, au, r.FormValue("admin_password"), s.User, s.Password, agent)
if err != nil {
if err := depoSetupRun(ctx, s, db, au, r.FormValue("admin_password"), s.User, s.Password, agent); err != nil {
wizDepoGo(w, r, "setup", "база не подготовлена — "+err.Error(), false)
return
}
s.DepoDatabase, s.DepoTablesMode = found, "auto"
note = "база " + found + " подготовлена: логин " + s.User + ", таблицы обмена, права"
s.DepoTablesMode = "auto"
note = "база " + db + " подготовлена: логин " + s.User + ", таблицы обмена, права"
if agent {
note += ", задание " + depoJobName
}
@@ -405,9 +373,7 @@ func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request)
return
}
}
if db != "" {
s.DepoDatabase = db
}
s.DepoDatabase = db
if err := h.rc.UpdateFancy(s); err != nil {
wizDepoGo(w, r, "setup", "не сохранилось: "+err.Error(), false)
return
@@ -416,19 +382,9 @@ func (h *setupHandlers) wizardDepoSetup(w http.ResponseWriter, r *http.Request)
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)
if err := depoCheckIsDepo(ctx, s, db); err != nil { // имя базы задал клиент — та ли это база
wizDepoGo(w, r, "setup", note+err.Error(), false)
return
}
msg, ok := depoTablesCheck(ctx, s)
h.saveDepoTablesCheck(msg, ok)