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 }