From 21d87fb5300588b475bf2c9024a59cb521004fb4 Mon Sep 17 00:00:00 2001 From: zuevav Date: Tue, 6 Oct 2026 16:09:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D1=87=D1=91=D1=82=20DEPO:=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20SQL=20Server=20Agent=20?= =?UTF-8?q?=E2=80=94=20=D1=81=D0=B2=D0=BE=D1=91=20=D1=83=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=B6=D0=B4=D0=BE=D0=B9=20=D0=B1=D0=B0=D0=B7=D1=8B=20(bj2depo?= =?UTF-8?q?=5F<=D0=B1=D0=B0=D0=B7=D0=B0>),=20=D0=B8=D0=BC=D1=8F=20=D0=B2?= =?UTF-8?q?=20=D0=BC=D0=B0=D1=81=D1=82=D0=B5=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На одном сервере бывает несколько баз DEPO (боевая и тестовая): общее имя bj2depo давало ложную зелёную проверку — тестовой базе засчитывалось боевое задание, а своё не создавалось. - Имя задания — настройка DepoJobName; в мастере, пункт 3, поле «Имя задания SQL Server Agent», по умолчанию bj2depo_<имя базы> (меняется вместе с базой, пока его не поправили). Установки старше — bj2depo, как было. - Скрипт до любых изменений останавливается, если задание с этим именем обрабатывает другую базу или эту базу уже обрабатывает задание с другим именем (иначе двойная обработка событий). - Проверка задания сравнивает его базу (sp_help_jobstep) с базой bj. Синтаксис проверен на SQL Server без выполнения (SET PARSEONLY); проверка задания на боевом сервере — зелёная (bj2depo ↔ BJ_DEPO_DATA). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi --- internal/lkgateway/depo_setup.go | 38 +++++++++++-- internal/lkgateway/depoevents.go | 56 ++++++++++++++----- internal/lkgateway/letters.go | 2 +- internal/lkgateway/onboarding_test.go | 28 ++++++++++ internal/lkgateway/runtimeconfig.go | 2 + internal/lkgateway/setup.go | 5 ++ .../lkgateway/web/templates/admin_wizard.html | 11 ++++ internal/lkgateway/wizard_depo.go | 18 ++++-- 8 files changed, 135 insertions(+), 25 deletions(-) diff --git a/internal/lkgateway/depo_setup.go b/internal/lkgateway/depo_setup.go index c732b8a..b113706 100644 --- a/internal/lkgateway/depo_setup.go +++ b/internal/lkgateway/depo_setup.go @@ -59,7 +59,7 @@ func depoSetupScript(f FancyMSSQLSettings, db, login, pass string, agentJob bool var b strings.Builder job := "вызывает сам bj" if agentJob { - job = "задание SQL Server Agent «" + depoJobName + "»" + job = "задание SQL Server Agent «" + depoJobName(f) + "»" } fmt.Fprintf(&b, `-- Bridge&Join: подготовка базы учёта DEPO для bj. Microsoft SQL Server. SET NOCOUNT ON; @@ -98,7 +98,7 @@ BEGIN 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); @@ -109,7 +109,7 @@ ELSE EXEC(@sql); -- 2. Таблицы обмена FTV_M2M* (только недостающие таблицы, колонки и индексы — данные не меняются) -`, login, job, sqlStr(db), sqlStr(pass)) +`, login, job, sqlStr(db), sqlStr(pass), depoJobGuard(f, agentJob)) for _, s := range depoTablesDDL(false) { fmt.Fprintf(&b, "EXEC(@in + N'%s');\n", sqlStr(s)) } @@ -133,7 +133,7 @@ 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) +`, depoJobName(f), proc, depoJobEvery(f), login) } else { fmt.Fprintf(&b, ` -- 4. Обработку вызывает bj: логину bj — право запускать процедуру %[1]s @@ -184,6 +184,34 @@ END $$; 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() { @@ -215,7 +243,7 @@ func depoSetupRun(ctx context.Context, f FancyMSSQLSettings, db, adminUser, admi step("isdepo", "ok", "") what := "логин bj «" + login + "», таблицы обмена, права" if agentJob && !f.IsPostgres() { - what += ", задание «" + depoJobName + "»" + what += ", задание «" + depoJobName(f) + "»" } step("apply", "run", "Применяю скрипт подготовки: "+what) if f.IsPostgres() { diff --git a/internal/lkgateway/depoevents.go b/internal/lkgateway/depoevents.go index 7ce8270..a4ec7d3 100644 --- a/internal/lkgateway/depoevents.go +++ b/internal/lkgateway/depoevents.go @@ -743,7 +743,24 @@ func (s *Server) depoJobLoop(ctx context.Context) { // ---- выбор способа обработки при настройке: проверки и создание джобы ---- -const depoJobName = "bj2depo" +// depoJobNameLegacy — имя задания у установок до 06.10.2026 (одно на сервер). +const depoJobNameLegacy = "bj2depo" + +// depoJobNameRe — допустимое имя задания SQL Server Agent (подставляется в скрипты). +var depoJobNameRe = regexp.MustCompile(`^[A-Za-z0-9_.\-]{1,100}$`) + +// depoJobName — имя задания SQL Server Agent этой установки. С 06.10.2026 задание называется по базе +// (depoJobNameFor): на одном сервере бывает несколько баз DEPO (боевая и тестовая), и у каждой своё +// задание. Пусто в настройках — установка старше, у неё задание bj2depo. +func depoJobName(f FancyMSSQLSettings) string { + if n := strings.TrimSpace(f.DepoJobName); n != "" { + return n + } + return depoJobNameLegacy +} + +// depoJobNameFor — имя задания по умолчанию для базы db. +func depoJobNameFor(db string) string { return "bj2depo_" + db } // depoJobAgentScript — скрипт для администратора SQL (ручной вариант). func depoJobAgentScript(f FancyMSSQLSettings) string { @@ -751,7 +768,7 @@ func depoJobAgentScript(f FancyMSSQLSettings) string { EXEC dbo.sp_add_job @job_name = N'%[1]s', @enabled = 1, @description = N'bj2depo: обработка трансферных таблиц M2M (bj)'; EXEC dbo.sp_add_jobstep @job_name = N'%[1]s', @step_name = N'%[1]s', @subsystem = N'TSQL', @database_name = N'%[2]s', @command = N'exec %[3]s;'; EXEC 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 = %[4]d, @active_start_time = 0; -EXEC dbo.sp_add_jobserver @job_name = N'%[1]s';`, depoJobName, f.DepoDB(), depoJobProcName(f), depoJobEvery(f)) +EXEC dbo.sp_add_jobserver @job_name = N'%[1]s';`, depoJobName(f), f.DepoDB(), depoJobProcName(f), depoJobEvery(f)) } // depoJobAgentAdminScript — тот же набор для администратора SQL (вариант «создаёт администратор по @@ -784,7 +801,7 @@ IF EXISTS (SELECT 1 FROM sys.database_principals WHERE name = N'%[5]s' AND type IF USER_ID(N'%[5]s') IS NULL CREATE USER [%[5]s] FOR LOGIN [%[5]s]; IF IS_ROLEMEMBER(N'SQLAgentReaderRole', N'%[5]s') = 0 ALTER ROLE SQLAgentReaderRole ADD MEMBER [%[5]s]; GO -`, depoJobName, f.DepoDB(), depoJobProcName(f), depoJobEvery(f), pickStr(strings.TrimSpace(f.User), "bj_user")) +`, depoJobName(f), f.DepoDB(), depoJobProcName(f), depoJobEvery(f), pickStr(strings.TrimSpace(f.User), "bj_user")) } // depoJobGrantScript — права для варианта «внутри bj». @@ -865,14 +882,23 @@ func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, ad runMsg sql.NullString now time.Time ) - err = mdb.QueryRowContext(ctx, "SELECT enabled, GETDATE() FROM msdb.dbo.sysjobs_view WHERE name = @n", sql.Named("n", depoJobName)).Scan(&jobEnabled, &now) + err = mdb.QueryRowContext(ctx, "SELECT enabled, GETDATE() FROM msdb.dbo.sysjobs_view WHERE name = @n", sql.Named("n", depoJobName(f))).Scan(&jobEnabled, &now) if err == sql.ErrNoRows { - return "джобы " + depoJobName + " на сервере нет — создайте её (вариант «создаёт bj» с логином администратора или скрипт для администратора)", false + return "джобы " + depoJobName(f) + " на сервере нет — создайте её (вариант «создаёт bj» с логином администратора или скрипт для администратора)", false } if err != nil { return "список джоб недоступен логину " + conn.User + " (" + quikErrorDetail(err) + "). Чтобы bj мог проверять джобу, администратору SQL — один раз: " + strings.ReplaceAll(depoJobReaderGrant(f), "\n", " "), false } - if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobschedule @job_name = @n", sql.Named("n", depoJobName)); err == nil { + // задание с этим именем — той ли базы (на одном сервере у боевой и тестовой баз DEPO свои задания) + if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobstep @job_name = @n", sql.Named("n", depoJobName(f))); err == nil { + for _, r := range rows { + if dbn := strings.TrimSpace(fmt.Sprint(r["database_name"])); dbn != "" && dbn != "" && !strings.EqualFold(dbn, f.DepoDB()) { + return fmt.Sprintf("задание %s обрабатывает базу %s, а bj работает с базой %s — это задание другой базы. Задайте для этой базы своё имя задания (мастер, «Учёт DEPO», пункт 3) и подготовьте базу заново", + depoJobName(f), dbn, f.DepoDB()), false + } + } + } + if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobschedule @job_name = @n", sql.Named("n", depoJobName(f))); err == nil { for _, r := range rows { if mapInt(r, "enabled") == 1 || !schedEnabled.Valid { schedEnabled = sql.NullInt64{Int64: mapInt(r, "enabled"), Valid: true} @@ -881,14 +907,14 @@ func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, ad } } } - if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_job @job_name = @n, @job_aspect = N'JOB'", sql.Named("n", depoJobName)); err == nil && len(rows) > 0 { + if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_job @job_name = @n, @job_aspect = N'JOB'", sql.Named("n", depoJobName(f))); err == nil && len(rows) > 0 { runDate = sql.NullInt64{Int64: mapInt(rows[0], "last_run_date"), Valid: true} runTime = sql.NullInt64{Int64: mapInt(rows[0], "last_run_time"), Valid: true} runStatus = sql.NullInt64{Int64: mapInt(rows[0], "last_run_outcome"), Valid: true} } if runStatus.Valid && runStatus.Int64 == 0 && runDate.Int64 != 0 { // причина — в сообщении последнего выполненного шага (история идёт от новых к старым) - if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobhistory @job_name = @n, @mode = N'FULL'", sql.Named("n", depoJobName)); err == nil { + if rows, err := mssqlRowMaps(ctx, mdb, "EXEC msdb.dbo.sp_help_jobhistory @job_name = @n, @mode = N'FULL'", sql.Named("n", depoJobName(f))); err == nil { for _, r := range rows { if mapInt(r, "step_id") > 0 { runMsg = sql.NullString{String: fmt.Sprint(r["message"]), Valid: true} @@ -898,10 +924,10 @@ func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, ad } } if jobEnabled.Int64 != 1 { - return "джоба " + depoJobName + " есть, но ВЫКЛЮЧЕНА — включите её в SQL Server Agent", false + return "джоба " + depoJobName(f) + " есть, но ВЫКЛЮЧЕНА — включите её в SQL Server Agent", false } if !schedEnabled.Valid || schedEnabled.Int64 != 1 { - return "у джобы " + depoJobName + " нет включённого расписания — она не будет запускаться", false + return "у джобы " + depoJobName(f) + " нет включённого расписания — она не будет запускаться", false } every := time.Duration(depoJobEvery(f)) * time.Second if subType.Int64 == 2 && subInt.Int64 > 0 { // каждые N секунд @@ -909,7 +935,7 @@ func depoAgentJobStatus(ctx context.Context, f FancyMSSQLSettings, adminUser, ad } else if subType.Int64 == 4 && subInt.Int64 > 0 { // каждые N минут every = time.Duration(subInt.Int64) * time.Minute } - sched := fmt.Sprintf("джоба %s включена, запускается каждые %s", depoJobName, humanEvery(every)) + sched := fmt.Sprintf("джоба %s включена, запускается каждые %s", depoJobName(f), humanEvery(every)) if !runDate.Valid || runDate.Int64 == 0 { return sched + ", но ещё ни разу не запускалась — запущена ли служба SQL Server Agent?", false } @@ -992,12 +1018,12 @@ func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig, adminUser, admin } defer mdb.Close() var n int - if err := mdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM msdb.dbo.sysjobs_view WHERE name=@n", sql.Named("n", depoJobName)).Scan(&n); err != nil { + if err := mdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM msdb.dbo.sysjobs_view WHERE name=@n", sql.Named("n", depoJobName(f))).Scan(&n); err != nil { return fmt.Errorf("нет доступа к msdb (%s). Нужны права: %s", quikErrorDetail(err), strings.ReplaceAll(depoJobAgentRightsScript(f), "\n", " ")) } if n > 0 { depoGrantJobReader(ctx, mdb, f) // джоба осталась (msdb не восстанавливали) — права на проверку всё равно выдаём - return fmt.Errorf("джоба %s уже существует", depoJobName) + return fmt.Errorf("джоба %s уже существует", depoJobName(f)) } for _, stmt := range strings.Split(depoJobAgentScript(f), "\n") { stmt = strings.TrimSpace(stmt) @@ -1008,7 +1034,7 @@ func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig, adminUser, admin return fmt.Errorf("%s → %s. Нужны права: %s", stmt[:min(40, len(stmt))], quikErrorDetail(err), strings.ReplaceAll(depoJobAgentRightsScript(f), "\n", " ")) } } - log.Printf("depo-job: джоба SQL Agent %s создана", depoJobName) + log.Printf("depo-job: джоба SQL Agent %s создана", depoJobName(f)) depoGrantJobReader(ctx, mdb, f) return nil } @@ -1037,7 +1063,7 @@ type DepoJobView struct { } func depoJobView(f FancyMSSQLSettings) DepoJobView { - return DepoJobView{Name: depoJobName, Proc: depoJobProcName(f), Every: depoJobEvery(f), GrantScript: depoJobGrantScript(f), AgentRights: depoJobAgentRightsScript(f), AgentScript: depoJobAgentScript(f), AdminScript: depoJobAgentAdminScript(f)} + return DepoJobView{Name: depoJobName(f), Proc: depoJobProcName(f), Every: depoJobEvery(f), GrantScript: depoJobGrantScript(f), AgentRights: depoJobAgentRightsScript(f), AgentScript: depoJobAgentScript(f), AdminScript: depoJobAgentAdminScript(f)} } // ---- выгрузка истории тестов (TEST3, банк Зенит / робот) в трансферные таблицы ---- diff --git a/internal/lkgateway/letters.go b/internal/lkgateway/letters.go index 7cfdc5a..144b726 100644 --- a/internal/lkgateway/letters.go +++ b/internal/lkgateway/letters.go @@ -73,7 +73,7 @@ func letterFor(s Settings, key string) (Letter, bool) { case "dba": ip := pickStr(outboundIP(s.Fancy.Server, s.Fancy.Port), firstServerIP()) f := s.Fancy - what := "— создаёт задание SQL Server Agent «" + depoJobName + "» для обработки обмена;\n" + what := "— создаёт задание SQL Server Agent «" + depoJobName(f) + "» для обработки обмена;\n" run := "под логином с правами sysadmin (SSMS или sqlcmd)" if f.IsPostgres() { what = "" diff --git a/internal/lkgateway/onboarding_test.go b/internal/lkgateway/onboarding_test.go index 64a3579..f48fac2 100644 --- a/internal/lkgateway/onboarding_test.go +++ b/internal/lkgateway/onboarding_test.go @@ -375,6 +375,21 @@ func TestDepoSetupScript(t *testing.T) { } } } + // задание — по базе: своё имя, и до любых изменений — стоп, если имя занято другой базой или эту базу + // уже обрабатывает другое задание + named := depoSetupScript(FancyMSSQLSettings{DepoJobName: "bj2depo_DEPO_DATA"}, "DEPO_DATA", "bj_user", "Pass1word", true) + for _, want := range []string{"sp_add_job @job_name = N'bj2depo_DEPO_DATA'", "WHERE j.name = N'bj2depo_DEPO_DATA' AND s.database_name <> @db", + "s.command LIKE N'%FTV_M2M_JOB%' AND j.name <> N'bj2depo_DEPO_DATA'", "уже запускает задание %s"} { + if !strings.Contains(named, want) { + t.Fatalf("задание по базе: в скрипте нет %q", want) + } + } + if strings.Index(named, "s.database_name <> @db") > strings.Index(named, "-- 1. Логин bj") { + t.Fatal("проверка задания должна стоять до создания логина") + } + if depoJobName(FancyMSSQLSettings{}) != "bj2depo" || depoJobNameFor("BJ2_DEPO_DATA") != "bj2depo_BJ2_DEPO_DATA" { + t.Fatal("имя задания: прежние установки — bj2depo, новые — bj2depo_<база>") + } bj := depoSetupScript(f, "DEPO_X", "bj_user", "Pass1word", false) if strings.Contains(bj, "sp_add_job") || !strings.Contains(bj, "GRANT EXECUTE ON dbo.FTV_M2M_JOB") || !strings.Contains(bj, "N'DEPO_X'") { t.Fatal("обработка силами bj: без задания, с правом на процедуру и указанной базой") @@ -514,6 +529,19 @@ func TestDepoScriptView(t *testing.T) { if f := rc.Snapshot().Fancy; f.User != "bj_user" || f.Password != "Working1x" || f.DepoNewUser != "bj_user" || f.DepoNewPass != "Secret123x" || f.Server != "10.0.0.5" || f.DepoTablesMode != "script" { t.Fatalf("файл: рабочие логин/пароль должны остаться, новые — черновиком: %q/%q, черновик %q", f.User, f.Password, f.DepoNewUser) } + if f := rc.Snapshot().Fancy; f.DepoJobName != "bj2depo_DEPO_X" { + t.Fatalf("имя задания по умолчанию — по базе: %q", f.DepoJobName) + } + form.Set("view", "1") + form.Set("depo_job_name", "bj2depo test;") + if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "имя задания") { + t.Fatalf("недопустимое имя задания: %d %s", w.Code, w.Body.String()) + } + form.Set("depo_job_name", "bj2depo") + if w := post(form); w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "sp_add_job @job_name = N'bj2depo'") { + t.Fatalf("имя задания с экрана: %d", w.Code) + } + form.Del("view") form.Set("view", "1") form.Set("bj_password", "short") if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "8 знаков") { diff --git a/internal/lkgateway/runtimeconfig.go b/internal/lkgateway/runtimeconfig.go index 390044a..75b5313 100644 --- a/internal/lkgateway/runtimeconfig.go +++ b/internal/lkgateway/runtimeconfig.go @@ -214,6 +214,8 @@ type FancyMSSQLSettings struct { // Логин и пароль bj из выданного скрипта, ещё не проверенные входом. В работу (User/Password) — только // после успешного входа: службы bj ходят в DEPO с рабочими, и неверный пароль от них за минуты блокирует // логин на сервере (CHECK_POLICY) — для всех, кто им пользуется. + // Имя задания SQL Server Agent для этой базы (depoJobName); пусто — bj2depo, как до 06.10.2026. + DepoJobName string `json:"depo_job_name,omitempty"` DepoNewUser string `json:"depo_new_user,omitempty"` DepoNewPass string `json:"depo_new_password,omitempty"` } diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index 01a3b90..7a828ca 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -177,6 +177,7 @@ type WizardData struct { DepoUser string // шаг 3, «скрипт администратору»: логин bj, который создаст скрипт DepoPass string // и его пароль (сохранённый или придуманный сейчас — сохраняется со скриптом) DepoMode string // выбранный способ подготовки базы: auto|script|existing + DepoJobName string // имя задания SQL Server Agent для этой базы (пусто — по имени базы, считает экран) ValidataVer string // версия установленной «Валидаты Клиент L» (пусто — не установлена) CryptoSvc string // состояние службы bj-crypto (systemctl is-active) PGSummary string // куда подключена база bj, без пароля @@ -329,6 +330,10 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) { } else if s.Fancy.User != d.DepoUser || d.DepoPass == "" { d.DepoPass = depoGenPassword() } + d.DepoJobName = s.Fancy.DepoJobName + if d.DepoJobName == "" && s.Fancy.DepoJobMode == "sql-auto" && s.Fancy.User != "" { + d.DepoJobName = depoJobNameLegacy // установка старше имён по базе: её задание — bj2depo + } d.DepoMode = "auto" if s.Fancy.DepoTablesMode == "script" { d.DepoMode = "script" diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html index 79c3210..d9a9868 100644 --- a/internal/lkgateway/web/templates/admin_wizard.html +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -83,6 +83,9 @@ .dq-sql pre { max-height:380px; min-height:140px; font-size:12px; margin:0; transition:opacity .2s; } .dq-sql pre.dq-busy { opacity:.5; } .dq.is-pg [data-ms], .dq:not(.is-pg) [data-pg] { display:none !important; } +.dq-jobname { margin-top:14px; max-width:520px; } +.dq-jobname label { display:block; margin:0; font-size:13.5px; font-weight:500; color:var(--text-2); } +.dq-jobname input { display:block; width:100%; box-sizing:border-box; margin-top:5px; } .dq-box { border:1px solid var(--border); border-radius:12px; padding:14px 16px 16px; } .dq-box-bj { background:color-mix(in srgb, var(--accent-weak) 45%, transparent); } .dq-box-h { font-weight:600; font-size:14.5px; margin:0 0 10px; } @@ -221,6 +224,10 @@ +
+ +

Задание называется по базе: у каждой базы DEPO на сервере — своё. Если обработку этой базы уже запускает задание, впишите его имя — второе создано не будет.

+

Для PostgreSQL обработку запускает сервер bj — выбирать не нужно.

@@ -293,6 +300,9 @@ f.classList.toggle('is-pg', pg); if(pg&&f.port.value==='1433') f.port.value='5432'; else if(!pg&&f.port.value==='5432') f.port.value='1433'; f.admin_user.placeholder=pg?'postgres':'sa'; + var jn=f.depo_job_name, dbv=(f.depo_database.value||'').trim(); // имя задания — по базе, пока не поправили руками + if(jn.dataset.auto==='1') jn.value=dbv?'bj2depo_'+dbv:''; + document.getElementById('wiz-depo-jobbox').hidden=f.depo_job.value!=='agent'; var show={auto:['auto','login','autogo'], script:['login','script'], existing:['existing']}[m]||[]; ['auto','login','autogo','script','existing'].forEach(function(k){ document.getElementById('wiz-depo-'+k).hidden=show.indexOf(k)<0; }); document.getElementById('wiz-depo-login-script').hidden=m!=='script'; @@ -311,6 +321,7 @@ .catch(function(){ if(my!==seq) return; last=''; pre.classList.remove('dq-busy'); pre.textContent=''; pre.dataset.msg='Не получилось собрать скрипт — обновите страницу.'; }); } function changed(){ sync(); clearTimeout(timer); timer=setTimeout(load, 350); } + f.depo_job_name.addEventListener('input', function(){ this.dataset.auto=this.value.trim()?'0':'1'; }); f.addEventListener('input', changed); f.addEventListener('change', changed); document.getElementById('wiz-depo-copy').onclick=function(){ var b=this; window.bjCopy(pre.textContent).then(function(ok){ b.textContent=ok?'Скопировано ✓':'Выделите и скопируйте вручную'; b.classList.toggle('done',!!ok); diff --git a/internal/lkgateway/wizard_depo.go b/internal/lkgateway/wizard_depo.go index 8e391a0..a3cd052 100644 --- a/internal/lkgateway/wizard_depo.go +++ b/internal/lkgateway/wizard_depo.go @@ -160,9 +160,9 @@ func (h *setupHandlers) wizardDepoJob(w http.ResponseWriter, r *http.Request) { return } // джобы хранятся в msdb: при восстановлении базы DEPO из копии джоба остаётся — не пересоздаём - jobNote = "джоба " + depoJobName + " на сервере уже есть (bj её не пересоздавал). " + jobNote = "джоба " + depoJobName(h.rc.Snapshot().Fancy) + " на сервере уже есть (bj её не пересоздавал). " } else { - jobNote = "джоба " + depoJobName + " создана. " + jobNote = "джоба " + depoJobName(h.rc.Snapshot().Fancy) + " создана. " } } // проверяем запуск джобы (не процедуры): только что созданная джоба запускается в течение интервала — ждём @@ -247,6 +247,16 @@ func (h *setupHandlers) depoSetupForm(r *http.Request) (FancyMSSQLSettings, stri if !validDBName(db) { return s, "", agent, "имя базы DEPO — только латинские буквы, цифры и _, начинается с буквы" } + if agent { // задание обработки — своё у каждой базы: по умолчанию bj2depo_<база> + jn := strings.TrimSpace(r.FormValue("depo_job_name")) + if jn == "" { + jn = depoJobNameFor(db) + } + if !depoJobNameRe.MatchString(jn) { + return s, db, agent, "имя задания — латинские буквы, цифры, точка, дефис и _" + } + s.DepoJobName = jn + } return s, db, agent, "" } @@ -396,7 +406,7 @@ func (h *setupHandlers) depoSetupDo(ctx context.Context, r *http.Request, step d s.DepoTablesMode = "auto" note = "база " + db + " подготовлена: логин " + s.User + ", таблицы обмена, права" if agent { - note += ", задание " + depoJobName + note += ", задание " + depoJobName(s) } note += ". " case "existing": @@ -448,7 +458,7 @@ func (h *setupHandlers) depoSetupDo(ctx context.Context, r *http.Request, step d step("tables", "ok", "") // запуск обработки: только что созданное задание запускается в течение интервала — подождать if agent { - step("job", "run", "Проверяю задание SQL Server Agent «"+depoJobName+"» — до 30 секунд") + step("job", "run", "Проверяю задание SQL Server Agent «"+depoJobName(s)+"» — до 30 секунд") } else { step("job", "run", "Проверяю, что bj может запускать обработку обмена") }