Files
Bridge-and-Join-s/internal/lkgateway/depo_setup.go
T
zuevav 8905ebc6fb Первичная настройка: ключ спрашивается один раз, затем обновления, модули и «Время и сеть»; 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
2026-10-06 11:42:10 +03:00

224 lines
12 KiB
Go

package lkgateway
// Подготовка базы учёта DEPO одним действием (06.10.2026). Раньше это были три экрана мастера — логин bj,
// таблицы обмена, задание SQL Agent — и три скрипта. Теперь один скрипт делает всё:
// - находит базу DEPO на сервере сам (по учётным таблицам OD_FACES и OD_RESTS) — имя базы клиенту знать
// не нужно (SQL Server; для PostgreSQL базу назвать придётся: psql не умеет искать по чужим базам);
// - создаёт логин bj (пароль придумывает bj и запоминает) или обновляет его пароль;
// - создаёт таблицы обмена FTV_M2M* и выдаёт логину bj права: чтение учёта, запись в таблицы обмена;
// - по галочке «обработку запускает SQL Server Agent» — создаёт задание bj2depo, иначе обработку
// вызывает сам bj (тогда логину bj — право запускать процедуру обработки).
// Вариант «bj сделает сам» выполняет тот же скрипт логином администратора SQL (он не сохраняется), вариант
// «скрипт для администратора» отдаёт его файлом, вариант «логин уже есть» только проверяет.
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"strings"
"time"
)
// depoSetupLogin — логин bj в DEPO (если в настройках уже есть допустимый — он).
func depoSetupLogin(f FancyMSSQLSettings) string {
if u := strings.TrimSpace(f.User); u != "" && depoLoginRe.MatchString(u) {
return u
}
return "bj_user"
}
// depoGenPassword — пароль логина bj: 24 знака, есть заглавные, строчные и цифры (политика паролей SQL Server).
func depoGenPassword() string {
const up, low, dig = "ABCDEFGHJKLMNPQRSTUVWXYZ", "abcdefghijkmnpqrstuvwxyz", "23456789"
all := up + low + dig
pick := func(set string) byte {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(set))))
return set[n.Int64()]
}
b := []byte{pick(up), pick(low), pick(dig)}
for len(b) < 24 {
b = append(b, pick(all))
}
for i := len(b) - 1; i > 0; i-- { // перемешать, чтобы обязательные знаки не стояли в начале
j, _ := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
b[i], b[j.Int64()] = b[j.Int64()], b[i]
}
return string(b)
}
// sqlStr — строковый литерал T-SQL / PostgreSQL (одинарная кавычка удваивается).
func sqlStr(s string) string { return strings.ReplaceAll(s, "'", "''") }
// depoSetupScript — один скрипт подготовки базы DEPO. db — имя базы (пусто — найти на сервере; только SQL
// Server), 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)
}
var b strings.Builder
job := "обработку вызывает сам bj"
if agentJob {
job = "задание SQL Server Agent «" + depoJobName + "» для обработки обмена"
}
fmt.Fprintf(&b, `-- Bridge&Join: подготовка базы учёта DEPO для bj. Microsoft SQL Server.
-- Один скрипт: находит базу DEPO на сервере, создаёт логин bj (%[1]s), таблицы обмена FTV_M2M*, выдаёт
-- права (чтение учёта, запись только в таблицы обмена) и %[2]s.
-- Учётные данные DEPO скрипт не меняет. Запускать в SSMS или sqlcmd под логином с правами sysadmin.
-- Можно запускать повторно: уже сделанное не повторяется. Скрипт содержит пароль логина bj — после
-- выполнения файл удалите.
SET NOCOUNT ON;
DECLARE @db sysname = N'%[3]s'; -- база DEPO; пусто — найти на сервере по таблицам OD_FACES и OD_RESTS
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;
END
IF @db = N'' OR DB_ID(@db) IS NULL
BEGIN
RAISERROR(N'База DEPO не найдена или на сервере их несколько. Впишите имя базы DEPO в @db в начале скрипта и запустите снова.', 16, 1);
RETURN;
END
PRINT N'База DEPO: ' + @db;
DECLARE @in nvarchar(300) = N'USE ' + QUOTENAME(@db) + N'; ';
-- 1. Логин bj
IF SUSER_ID(N'%[1]s') IS NULL
EXEC(N'CREATE LOGIN [%[1]s] WITH PASSWORD = N''%[4]s'', CHECK_POLICY = ON, DEFAULT_DATABASE = ' + QUOTENAME(@db))
ELSE
EXEC(N'ALTER LOGIN [%[1]s] WITH PASSWORD = N''%[4]s''');
-- 2. Таблицы обмена FTV_M2M* (только недостающие таблицы, колонки и индексы — данные не меняются)
`, login, job, sqlStr(db), sqlStr(sqlStr(pass)))
for _, s := range depoTablesDDL(false) {
fmt.Fprintf(&b, "EXEC(@in + N'%s');\n", sqlStr(s))
}
b.WriteString("\n-- 3. Права логину bj в базе DEPO: чтение учёта (сверка переводов), запись в таблицы обмена\n")
for _, s := range depoTablesGrantSQL(false, login) {
fmt.Fprintf(&b, "EXEC(@in + N'%s');\n", sqlStr(s))
}
proc := depoJobProcName(f)
if agentJob {
fmt.Fprintf(&b, `
-- 4. Задание SQL Server Agent «%[1]s»: каждые %[3]d с вызывает %[2]s — записи bj становятся документами учёта
IF NOT EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = N'%[1]s')
BEGIN
EXEC msdb.dbo.sp_add_job @job_name = N'%[1]s', @enabled = 1, @description = N'bj2depo: обработка таблиц обмена M2M (Bridge&Join)';
EXEC msdb.dbo.sp_add_jobstep @job_name = N'%[1]s', @step_name = N'%[1]s', @subsystem = N'TSQL', @database_name = @db, @command = N'exec %[2]s;';
EXEC msdb.dbo.sp_add_jobschedule @job_name = N'%[1]s', @name = N'%[1]s', @enabled = 1, @freq_type = 4, @freq_interval = 1, @freq_subday_type = 2, @freq_subday_interval = %[3]d, @active_start_time = 0;
EXEC msdb.dbo.sp_add_jobserver @job_name = N'%[1]s';
PRINT N'Задание %[1]s создано.';
END
ELSE
PRINT N'Задание %[1]s уже есть — не изменялось.';
-- логину bj — чтение списка заданий и их истории: bj проверяет, что задание работает
EXEC(N'USE msdb; IF USER_ID(N''%[4]s'') IS NULL CREATE USER [%[4]s] FOR LOGIN [%[4]s]; ELSE IF EXISTS (SELECT 1 FROM sys.database_principals WHERE name = N''%[4]s'' AND sid <> SUSER_SID(N''%[4]s'')) ALTER USER [%[4]s] WITH LOGIN = [%[4]s]; IF IS_ROLEMEMBER(N''SQLAgentReaderRole'', N''%[4]s'') = 0 ALTER ROLE SQLAgentReaderRole ADD MEMBER [%[4]s];');
`, depoJobName, proc, depoJobEvery(f), login)
} else {
fmt.Fprintf(&b, `
-- 4. Обработку вызывает bj: логину bj — право запускать процедуру %[1]s
EXEC(@in + N'IF OBJECT_ID(N''%[1]s'', N''P'') IS NOT NULL GRANT EXECUTE ON %[1]s TO [%[2]s] ELSE PRINT N''Процедуры обработки ещё не установлены: после их установки выполните GRANT EXECUTE ON %[1]s TO [%[2]s]''');
`, proc, login)
}
b.WriteString("\nPRINT N'Готово: база DEPO подготовлена для bj.';\n")
return b.String()
}
// depoSetupScriptPG — то же для PostgreSQL (psql: роль, подключение к базе DEPO, таблицы, права).
func depoSetupScriptPG(f FancyMSSQLSettings, db, login, pass string) string {
var b strings.Builder
fmt.Fprintf(&b, `-- Bridge&Join: подготовка базы учёта DEPO для bj. PostgreSQL 10 и новее.
-- Запуск суперпользователем или владельцем базы: psql -h <сервер> -U postgres -f bj-depo-setup-postgres.sql
-- Создаёт роль bj (%[2]s), таблицы обмена FTV_M2M* и права (чтение учёта, запись только в таблицы обмена).
-- Обработку обмена вызывает сам bj. Учётные данные скрипт не меняет. Можно запускать повторно.
-- Скрипт содержит пароль роли bj — после выполнения файл удалите.
\set ON_ERROR_STOP on
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '%[2]s') THEN
CREATE ROLE "%[2]s" LOGIN PASSWORD '%[3]s';
ELSE
ALTER ROLE "%[2]s" LOGIN PASSWORD '%[3]s';
END IF;
END $$;
GRANT CONNECT ON DATABASE "%[1]s" TO "%[2]s";
\c "%[1]s"
`, db, login, sqlStr(pass))
for _, s := range depoTablesDDL(true) {
b.WriteString(s + ";\n")
}
for _, s := range depoTablesGrantSQL(true, login) {
b.WriteString(s + ";\n")
}
proc := depoJobProcName(f)
fmt.Fprintf(&b, "DO $$ BEGIN IF to_regproc('%[1]s') IS NOT NULL THEN EXECUTE 'GRANT EXECUTE ON FUNCTION %[1]s TO \"%[2]s\"'; END IF; END $$;\n", strings.ToLower(proc), login)
return b.String()
}
// depoSetupFileName — имя файла скрипта.
func depoSetupFileName(f FancyMSSQLSettings) string {
if f.IsPostgres() {
return "bj-depo-setup-postgres.sql"
}
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) {
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 f.IsPostgres() {
return db, depoSetupRunPG(ctx, adm, db, login, pass)
}
conn, err := fancyOpen(adm, "")
if err != nil {
return db, 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 db, nil
}
func depoSetupRunPG(ctx context.Context, adm FancyMSSQLSettings, db, login, pass string) error {
conn, err := fancyOpen(adm, db)
if err != nil {
return err
}
defer conn.Close()
stmts := []string{
fmt.Sprintf(`DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '%[1]s') THEN CREATE ROLE "%[1]s" LOGIN PASSWORD '%[2]s'; ELSE ALTER ROLE "%[1]s" LOGIN PASSWORD '%[2]s'; END IF; END $$`, login, sqlStr(pass)),
fmt.Sprintf(`GRANT CONNECT ON DATABASE "%s" TO "%s"`, db, login),
}
stmts = append(stmts, depoTablesDDL(true)...)
stmts = append(stmts, depoTablesGrantSQL(true, login)...)
for _, s := range stmts {
if _, err := conn.ExecContext(ctx, s); err != nil {
return fmt.Errorf("%s → %s", depoStmtHead(s), quikErrorDetail(err))
}
}
return nil
}