8905ebc6fb
- Первая страница — только администратор (и восстановление из копии); ключ продукта — после создания администратора, на шаге «Лицензия». Консоль первой загрузки ключ больше не спрашивает (для образа с комплектом 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
224 lines
12 KiB
Go
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
|
|
}
|