Учёт DEPO: задание SQL Server Agent — своё у каждой базы (bj2depo_<база>), имя в мастере
На одном сервере бывает несколько баз 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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 != "<nil>" && !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, банк Зенит / робот) в трансферные таблицы ----
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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 знаков") {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 @@
|
||||
<label class="dq-tile"><input type="radio" name="depo_job" value="agent" {{if ne $job "bj"}}checked{{end}}><span><b>На SQL Server</b><small>Задание SQL Server Agent. Рекомендуется: работает, даже когда bj выключен.</small></span></label>
|
||||
<label class="dq-tile"><input type="radio" name="depo_job" value="bj" {{if eq $job "bj"}}checked{{end}}><span><b>На сервере bj</b><small>Если SQL Server Agent нет — например, SQL Server Express.</small></span></label>
|
||||
</div>
|
||||
<div class="dq-jobname" id="wiz-depo-jobbox" data-ms>
|
||||
<label>Имя задания SQL Server Agent<input type="text" name="depo_job_name" value="{{.DepoJobName}}" data-auto="{{if .DepoJobName}}0{{else}}1{{end}}" pattern="[A-Za-z0-9_.\-]{1,100}" title="латинские буквы, цифры, точка, дефис и _" placeholder="bj2depo_имя_базы" spellcheck="false" class="dq-mono"></label>
|
||||
<p class="dq-note">Задание называется по базе: у каждой базы DEPO на сервере — своё. Если обработку этой базы уже запускает задание, впишите его имя — второе создано не будет.</p>
|
||||
</div>
|
||||
<p class="dq-note" data-pg>Для PostgreSQL обработку запускает сервер bj — выбирать не нужно.</p>
|
||||
</section>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 может запускать обработку обмена")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user