package lkgateway // Подготовка базы учёта DEPO одним действием (06.10.2026). Раньше это были три экрана мастера — логин bj, // таблицы обмена, задание SQL Agent — и три скрипта. Теперь один скрипт делает всё: // - работает с базой, которую назвал клиент (сам по серверу не ищет), и сначала проверяет, что в ней есть // учётные таблицы DEPO OD_FACES и OD_RESTS — опечатка в имени не уведёт в чужую базу; // - создаёт логин 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 — имя базы 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) } var b strings.Builder job := "вызывает сам bj" if agentJob { job = "задание SQL Server Agent «" + depoJobName(f) + "»" } fmt.Fprintf(&b, `-- Bridge&Join: подготовка базы учёта DEPO для bj. Microsoft SQL Server. SET NOCOUNT ON; -- ===== Проверьте перед запуском ========================================== -- База DEPO — точное имя базы учёта на этом сервере. DECLARE @db sysname = N'%[3]s'; -- Пароль логина bj (%[1]s). Меняете — впишите тот же пароль в bj, шаг «Учёт DEPO». DECLARE @pass nvarchar(128) = N'%[4]s'; -- ========================================================================= -- Что делает: проверяет, что @db — база DEPO, создаёт логин bj и таблицы обмена FTV_M2M*, выдаёт права -- (чтение учёта, запись только в таблицы обмена). Обработка обмена: %[2]s. -- Учётные данные DEPO не меняет. Запускать в SSMS или sqlcmd под логином с правами sysadmin. -- Можно запускать повторно. В скрипте пароль логина bj — после выполнения файл удалите. IF @pass = N'' BEGIN RAISERROR(N'Впишите пароль логина bj в @pass в начале скрипта и запустите снова.', 16, 1); RETURN; END IF @db = N'' BEGIN RAISERROR(N'Впишите имя базы DEPO в @db в начале скрипта и запустите снова.', 16, 1); RETURN; END IF DB_ID(@db) IS NULL BEGIN 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; DECLARE @in nvarchar(300) = N'USE ' + QUOTENAME(@db) + N'; '; %[5]s -- 1. Логин bj (если такой логин на сервере уже есть — ему ставится пароль из @pass) -- (EXEC(...) склеивает только строки и переменные — команда собирается в @sql заранее) DECLARE @sql nvarchar(max); IF SUSER_ID(N'%[1]s') IS NULL SET @sql = N'CREATE LOGIN [%[1]s] WITH PASSWORD = N''' + REPLACE(@pass, N'''', N'''''') + N''', CHECK_POLICY = ON, DEFAULT_DATABASE = ' + QUOTENAME(@db); ELSE SET @sql = N'ALTER LOGIN [%[1]s] WITH PASSWORD = N''' + REPLACE(@pass, N'''', N'''''') + N''''; EXEC(@sql); -- 2. Таблицы обмена FTV_M2M* (только недостающие таблицы, колонки и индексы — данные не меняются) `, login, job, sqlStr(db), sqlStr(pass), depoJobGuard(f, agentJob)) 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(f), 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 — после выполнения файл удалите. -- Проверьте перед запуском: база DEPO — "%[1]s", роль bj — "%[2]s", её пароль — в строках PASSWORD ниже -- (меняете — впишите тот же пароль в bj, шаг «Учёт DEPO»). \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" -- это база 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") } 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() } // depoJobGuard — проверки задания до любых изменений: у каждой базы DEPO на сервере своё задание. Задание с // этим именем уже обрабатывает другую базу — стоп (иначе тест обработали бы боевым заданием и наоборот); // эту базу уже обрабатывает задание с другим именем — стоп (два задания обрабатывали бы одни события дважды). func depoJobGuard(f FancyMSSQLSettings, agentJob bool) string { if !agentJob { return "" } proc := depoJobProcName(f) if i := strings.LastIndex(proc, "."); i >= 0 { proc = proc[i+1:] } return fmt.Sprintf(` -- Задание обработки «%[1]s» — только для этой базы: у каждой базы DEPO на сервере своё задание IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs j JOIN msdb.dbo.sysjobsteps s ON s.job_id = j.job_id WHERE j.name = N'%[1]s' AND s.database_name <> @db) BEGIN RAISERROR(N'Задание %[1]s уже есть и обрабатывает другую базу. Задайте для базы %%s другое имя задания в bj (шаг «Учёт DEPO», пункт 3) и получите скрипт заново.', 16, 1, @db); RETURN; END DECLARE @other sysname = (SELECT TOP 1 j.name FROM msdb.dbo.sysjobs j JOIN msdb.dbo.sysjobsteps s ON s.job_id = j.job_id WHERE s.database_name = @db AND s.command LIKE N'%%%[2]s%%' AND j.name <> N'%[1]s'); IF @other IS NOT NULL BEGIN RAISERROR(N'Обработку базы %%s уже запускает задание %%s. Впишите в bj это имя задания (шаг «Учёт DEPO», пункт 3) и получите скрипт заново.', 16, 1, @db, @other); RETURN; END `, depoJobName(f), proc) } // depoSetupFileName — имя файла скрипта. func depoSetupFileName(f FancyMSSQLSettings) string { if f.IsPostgres() { return "bj-depo-setup-postgres.sql" } return "bj-depo-setup-mssql.sql" } // depoStep — ход подготовки для экрана: key — шаг, state — run (начат) | ok | warn, text — что делается или итог. type depoStep func(key, state, text string) // depoSetupRun — «bj сделает сам»: тот же скрипт выполняется логином администратора (не сохраняется) в базе // db, которую назвал клиент; сначала — подключение и проверка, что это база DEPO. step — ход для экрана. func depoSetupRun(ctx context.Context, f FancyMSSQLSettings, db, adminUser, adminPass, login, pass string, agentJob bool, step depoStep) error { adm := f adm.User, adm.Password = strings.TrimSpace(adminUser), adminPass if !validDBName(db) { return fmt.Errorf("укажите имя базы DEPO") } step("connect", "run", "Подключаюсь к серверу "+f.Server+" логином администратора «"+adm.User+"»") if _, err := fancyPing(ctx, adm, ""); err != nil { return fmt.Errorf("к серверу %s логином %s подключиться не удалось — %s", f.Server, adm.User, quikErrorDetail(err)) } step("connect", "ok", "") step("isdepo", "run", "Проверяю, что «"+db+"» — база учёта DEPO") if err := depoCheckIsDepo(ctx, adm, db); err != nil { return err } step("isdepo", "ok", "") what := "логин bj «" + login + "», таблицы обмена, права" if agentJob && !f.IsPostgres() { what += ", задание «" + depoJobName(f) + "»" } step("apply", "run", "Применяю скрипт подготовки: "+what) if f.IsPostgres() { if err := depoSetupRunPG(ctx, adm, db, login, pass); err != nil { return err } step("apply", "ok", "") return nil } conn, err := fancyOpen(adm, "") if err != nil { 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 fmt.Errorf("подготовка базы %s логином %s: %s%s", db, adm.User, quikErrorDetail(err), depoTablesRightsHint(f, adm.User, err)) } step("apply", "ok", "") return 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 }