Files
Bridge-and-Join-s/internal/lkgateway/depo_setup.go
T
fontvielle 50e420f4a0 DEPO на Firebird в мастере: подготовка базы одним действием, как для MSSQL
Мастер, шаг «Учёт DEPO», и «Обмен с DEPO» в настройках: СУБД «Firebird» (Fancy на Firebird 2.5).
- Подключение: драйвер nakagami/firebirdsql, путь к файлу базы или псевдоним, WIN1251. Запросы bj к DEPO
  (T-SQL) подключение переводит само (fancyfb.go): механически (dbo., TOP, ISNULL, GETDATE, LEN, N, exec)
  вне строк и комментариев, точные тексты — fsql/fb.go (каталоги, LIKE [_], COUNT(*) OVER, SELECT без FROM,
  PATINDEX); @имена — позиционные ?, GUID — в верхнем регистре. Системные запросы из кода — в fsql/depocatalog.go.
- «Подготовить» логином SYSDBA (depo_setup_fb.go): проверка базы и UDF Fancy, логин SYSDBA_BJ (Fancy даёт права
  на документы логинам SYSDBA%; для Firebird — обычный пользователь), оператор-робот Fancy, кодировщик M2M,
  привязки FTV_M2M_SETTINGS по признакам Fancy, таблицы, процедуры, права; обработку запускает bj.
  «Получить скрипт» — то же для isql в WIN1251; письмо администратору, установка процедур, плашка обновления.
- Скрипты Firebird встроены (depoprocs/firebird, тест сверяет с deploy/); количества — DOUBLE PRECISION: из
  диалекта 3 в базу диалекта 1 процедуру с NUMERIC(18,7) не создать.
- Проверено на копии базы Fancy (отдельный экземпляр bj): мастер с нуля — все проверки, BJ_PING; тестовый режим
  DEPO — К1 Бубликов, зачисление и списание до исполнения без ошибок; очередь, справочник НРД.
  Тест FBLive готовит на базе все запросы bj к DEPO: 94 из 101, оставшиеся 7 — ЛК и ручной ввод (следующий этап).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-27 18:55:02 +03:00

307 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (если в настройках уже есть допустимый — он). Firebird — SYSDBA_BJ.
func depoSetupLogin(f FancyMSSQLSettings) string {
if f.IsFirebird() {
if u := strings.TrimSpace(f.User); fbLoginRe.MatchString(u) {
return strings.ToUpper(u)
}
return depoFBDefaultLogin
}
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)
}
if f.IsFirebird() {
return depoSetupScriptFB(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"
}
if f.IsFirebird() {
return "bj-depo-setup-firebird.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 f.IsFirebird() {
if !validFBPath(db) {
return fmt.Errorf("укажите путь к базе DEPO на сервере Firebird")
}
return depoFBSetup(ctx, adm, db, login, pass, step)
}
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
}