Учёт 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:
zuevav
2026-10-06 16:09:14 +03:00
parent 42524f6a4f
commit 21d87fb530
8 changed files with 135 additions and 25 deletions
+33 -5
View File
@@ -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() {
+41 -15
View File
@@ -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, банк Зенит / робот) в трансферные таблицы ----
+1 -1
View File
@@ -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 = ""
+28
View File
@@ -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 знаков") {
+2
View File
@@ -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"`
}
+5
View File
@@ -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);
+14 -4
View File
@@ -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 может запускать обработку обмена")
}