Мастер, шаг 3 DEPO: подключение → трансферные таблицы → обработка (job); по умолчанию джобу SQL Agent создаёт bj разовым логином администратора

- редакция SQL Server определяется (Express/PostgreSQL — обработку запускает bj);
- создание джобы и таблиц — логином администратора, который не сохраняется;
- настройки обмена с DEPO открыты без пароля перспективных модулей, если QUIK МНП в лицензии.

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-09-24 22:11:42 +03:00
parent 775b4214b6
commit e043e7c371
5 changed files with 123 additions and 11 deletions
+9
View File
@@ -50,6 +50,15 @@ func advancedUnlocked(r *http.Request) bool {
// requireAdvanced — защита ручек сохранения перспективных модулей: если доступ
// не открыт, отвечает 403. true — можно продолжать.
// requireDepo — настройки обмена с DEPO: открыты, если модуль «QUIK: неторговые поручения»
// входит в лицензию (мастер настройки, шаг 3), иначе — как перспективный модуль, по паролю.
func requireDepo(w http.ResponseWriter, r *http.Request) bool {
if globalRC != nil && moduleAllowed(globalRC, "quik-mnp") {
return true
}
return requireAdvanced(w, r)
}
func requireAdvanced(w http.ResponseWriter, r *http.Request) bool {
if advancedUnlocked(r) {
return true
+8 -2
View File
@@ -822,9 +822,15 @@ func depoJobCheck(ctx context.Context, rc *RuntimeConfig) (string, bool) {
}
// depoJobCreateAgent — создать джобу SQL Agent от имени логина bj (нужна роль SQLAgentOperatorRole).
func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig) error {
// depoJobCreateAgent — создать джобу SQL Agent. adminUser/adminPass — логин администратора SQL
// (sysadmin или SQLAgentOperatorRole в msdb): используется один раз и не сохраняется; пусто — логином bj.
func depoJobCreateAgent(ctx context.Context, rc *RuntimeConfig, adminUser, adminPass string) error {
f := rc.Snapshot().Fancy
mdb, err := fancyOpen(f, "msdb")
conn := f
if strings.TrimSpace(adminUser) != "" {
conn.User, conn.Password = strings.TrimSpace(adminUser), adminPass
}
mdb, err := fancyOpen(conn, "msdb")
if err != nil {
return fmt.Errorf("подключение к msdb: %w", err)
}
+7 -7
View File
@@ -65,7 +65,7 @@ func (h *setupHandlers) fancyFromForm(r *http.Request) FancyMSSQLSettings {
// saveFancyDepoTables — POST /admin/setup/fancy/depo-tables: база DEPO и способ создания таблиц.
func (h *setupHandlers) saveFancyDepoTables(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
if !requireDepo(w, r) {
return
}
if r.Method != http.MethodPost {
@@ -112,7 +112,7 @@ func (h *setupHandlers) saveDepoTablesCheck(msg string, ok bool) {
// createFancyDepoTables — POST /admin/setup/fancy/depo-tables/create: bj создаёт таблицы сам.
// Логин с правами берётся из формы и нигде не сохраняется.
func (h *setupHandlers) createFancyDepoTables(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
if !requireDepo(w, r) {
return
}
if r.Method != http.MethodPost {
@@ -181,7 +181,7 @@ func (h *setupHandlers) fancyDepoTablesScript(w http.ResponseWriter, r *http.Req
// saveFancyDepoJob — POST /admin/setup/fancy/depo-job: обработка трансферных таблиц силами bj.
func (h *setupHandlers) saveFancyDepoJob(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
if !requireDepo(w, r) {
return
}
if r.Method != http.MethodPost {
@@ -226,7 +226,7 @@ func (h *setupHandlers) saveFancyDepoJob(w http.ResponseWriter, r *http.Request)
// checkFancyDepoJob — POST /admin/setup/fancy/depo-job/check: проверка готовности выбранного способа.
func (h *setupHandlers) checkFancyDepoJob(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
if !requireDepo(w, r) {
return
}
if r.Method != http.MethodPost {
@@ -248,7 +248,7 @@ func (h *setupHandlers) checkFancyDepoJob(w http.ResponseWriter, r *http.Request
// createFancyDepoJob — POST /admin/setup/fancy/depo-job/create: создать джобу SQL Agent от имени bj.
func (h *setupHandlers) createFancyDepoJob(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
if !requireDepo(w, r) {
return
}
if r.Method != http.MethodPost {
@@ -257,7 +257,7 @@ func (h *setupHandlers) createFancyDepoJob(w http.ResponseWriter, r *http.Reques
}
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
defer cancel()
if err := depoJobCreateAgent(ctx, h.rc); err != nil {
if err := depoJobCreateAgent(ctx, h.rc, r.FormValue("admin_user"), r.FormValue("admin_password")); err != nil {
setupFlash(w, r, "Создание джобы: "+err.Error())
return
}
@@ -270,7 +270,7 @@ func (h *setupHandlers) createFancyDepoJob(w http.ResponseWriter, r *http.Reques
// saveFancy — POST /admin/setup/fancy. Сохраняет параметры подключения.
func (h *setupHandlers) saveFancy(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
if !requireDepo(w, r) {
return
}
if r.Method != http.MethodPost {
+37 -1
View File
@@ -170,6 +170,11 @@ type WizardData struct {
Lic LicenseStatus
DepoLicensed bool
DepoDB string
DepoTables DepoTablesView
DepoJob DepoJobView
DepoEdition string // редакция SQL Server клиента (пусто — не определена)
DepoAgent bool // у SQL Server есть SQL Agent (не Express, не PostgreSQL)
DepoJobDef string // способ обработки по умолчанию для мастера
ValidataVer string // версия установленной «Валидаты Клиент L» (пусто — не установлена)
CryptoSvc string // состояние службы bj-crypto (systemctl is-active)
PGSummary string // куда подключена база bj, без пароля
@@ -208,7 +213,10 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
d.Done.License = d.Lic.Valid
d.DepoLicensed = moduleAllowed(h.rc, "quik-mnp")
d.DepoDB = s.Fancy.DepoDB()
d.Done.Depo = strings.TrimSpace(s.Fancy.Server) != "" || !d.DepoLicensed
d.DepoTables = depoTablesView(s.Fancy)
d.DepoJob = depoJobView(s.Fancy)
// Шаг 3 выполнен, когда есть подключение, трансферные таблицы и работающая обработка (job).
d.Done.Depo = !d.DepoLicensed || (strings.TrimSpace(s.Fancy.Server) != "" && s.Fancy.DepoTablesCheckOK && s.Fancy.DepoJobCheckOK)
d.Done.Postgres = s.Postgres.DSN != ""
{
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
@@ -256,6 +264,19 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
step = 7
}
d.Step = step
if step == 3 && d.DepoLicensed && strings.TrimSpace(s.Fancy.Server) != "" {
// Есть ли у сервера SQL Agent: у Express его нет, у PostgreSQL — тоже; тогда обработку
// запускает сам bj. Иначе по умолчанию джобу SQL Agent создаёт bj.
d.DepoJobDef = "bj"
if !s.Fancy.IsPostgres() {
ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
d.DepoEdition, d.DepoAgent = depoServerEdition(ctx, s.Fancy)
cancel()
if d.DepoAgent || d.DepoEdition == "" {
d.DepoJobDef = "sql-auto"
}
}
}
if step == 1 && d.Done.License {
// Образ ставит ядро той версии, с которой собран; всё, что вышло позже (исправления ядра,
// установщика и служб), приезжает обновлением — предлагаем его сразу после ввода ключа.
@@ -268,6 +289,21 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
render(w, h.tpl.a.wizard, d)
}
// depoServerEdition — редакция SQL Server и есть ли у неё SQL Agent (EngineEdition 4 — Express).
func depoServerEdition(ctx context.Context, f FancyMSSQLSettings) (string, bool) {
db, err := fancyOpen(f, "master")
if err != nil {
return "", false
}
defer db.Close()
var eng int
var ed string
if err := db.QueryRowContext(ctx, "SELECT CAST(SERVERPROPERTY('EngineEdition') AS int), CAST(SERVERPROPERTY('Edition') AS nvarchar(128))").Scan(&eng, &ed); err != nil {
return "", false
}
return ed, eng != 4
}
// wizardProgressFile — номер последнего пройденного шага мастера (переход «Далее» на шаг N
// означает, что шаги до N пройдены).
const wizardProgressFile = "/var/lib/bj/.bj/wizard-progress"
@@ -90,7 +90,9 @@
{{if not .DepoLicensed}}
<p class="muted">Модуль обмена с DEPO и QUIK не входит в текущую лицензию — шаг пропускается. После добавления модуля в лицензию он станет доступен здесь и в «Настройка → Обмен Fancy».</p>
{{else}}
{{if .Done.Depo}}<p>✓ Подключение сохранено: {{.Settings.Fancy.Server}}, база {{.DepoDB}}. {{if .Settings.Fancy.DepoTablesCheckOK}}Трансферные таблицы на месте.{{else}}Трансферные таблицы ещё не проверены — «Настройка → Обмен Fancy → Трансферные таблицы DEPO».{{end}}</p>{{end}}
<p class="muted" style="margin:6px 0 0">Три части: подключение к базе, трансферные таблицы обмена, обработка (job), которая превращает записи bj в документы DEPO.</p>
<h3 style="margin-top:16px">1. Подключение к базе DEPO {{if .Settings.Fancy.Server}}<span style="color:var(--ok);font-size:14px">✓ {{.Settings.Fancy.Server}}</span>{{end}}</h3>
<form method="post" action="/admin/setup/fancy" style="margin-top:12px;display:grid;gap:10px">
<input type="hidden" name="return_to" value="/admin/wizard?step=3">
<div><label>Тип СУБД</label><select name="db_type" style="padding:8px;width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px"><option value="mssql" {{if ne .Settings.Fancy.DBType "postgres"}}selected{{end}}>MS SQL Server</option><option value="postgres" {{if eq .Settings.Fancy.DBType "postgres"}}selected{{end}}>PostgreSQL</option></select></div>
@@ -102,6 +104,65 @@
<div style="display:flex;gap:10px;flex-wrap:wrap"><button type="submit" class="btn">Сохранить</button>
{{if .Settings.Fancy.Server}}<button type="submit" class="btn" formaction="/admin/setup/fancy/test" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Проверить соединение</button>{{end}}</div>
</form>
{{if .Settings.Fancy.Server}}
{{$btn2 := "background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)"}}
<h3 style="margin-top:22px">2. Трансферные таблицы FTV_M2M {{if .Settings.Fancy.DepoTablesCheckOK}}<span style="color:var(--ok);font-size:14px">✓ на месте</span>{{end}}</h3>
<div class="help-block" style="font-size:13px"><strong>Что это.</strong> Четыре таблицы в базе DEPO, через которые идёт обмен: поручения <code>FTV_M2M</code> и их строки, события <code>FTV_M2M_EVENTS</code>, справочник участников НРД. Данные учёта они не трогают; создание можно повторять — добавляется только недостающее.</div>
<form method="post" action="/admin/setup/fancy/depo-tables" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap">
<input type="hidden" name="return_to" value="/admin/wizard?step=3">
<input type="hidden" name="depo_tables_mode" value="auto">
<input type="hidden" name="iis_contract_marker" value="{{.Settings.Fancy.IISContractMarker}}">
<label style="flex:1;min-width:220px">База DEPO на сервере <input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="{{.DepoTables.DB}}" pattern="[A-Za-z][A-Za-z0-9_]*" style="width:100%;padding:8px"></label>
<button type="submit" class="btn" style="{{$btn2}}">Сохранить имя базы</button>
</form>
{{if not .Settings.Fancy.DepoTablesCheckOK}}
<form method="post" action="/admin/setup/fancy/depo-tables/create" style="margin-top:10px;display:grid;gap:8px">
<input type="hidden" name="return_to" value="/admin/wizard?step=3">
<p style="margin:0"><b>Создать таблицы в базе {{.DepoTables.DB}}.</b> <span class="muted" style="font-size:13px">Логин администратора СУБД ({{if .DepoTables.Postgres}}владелец базы{{else}}db_owner в базе или sa{{end}}) нужен один раз и не сохраняется; bj заодно выдаст права своему логину {{.DepoTables.User}}. Пусто — попробовать логином bj.</span></p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px">
<input type="text" name="admin_user" autocomplete="off" placeholder="логин, например {{if .DepoTables.Postgres}}postgres{{else}}sa{{end}}" style="padding:8px">
<input type="password" name="admin_password" autocomplete="new-password" placeholder="пароль" style="padding:8px">
</div>
<div><button type="submit" class="btn">Создать таблицы</button> <a href="/admin/setup/fancy/depo-tables/script.sql" style="font-size:13px;margin-left:8px">или скачать скрипт для администратора</a></div>
</form>
{{end}}
<form method="post" action="/admin/setup/fancy/depo-tables/check" style="margin-top:8px"><input type="hidden" name="return_to" value="/admin/wizard?step=3"><button type="submit" class="btn" style="{{$btn2}}">Проверить таблицы</button></form>
{{if .Settings.Fancy.DepoTablesCheck}}<p style="font-size:13px;margin-top:6px;{{if .Settings.Fancy.DepoTablesCheckOK}}color:var(--ok){{else}}color:var(--err){{end}}">{{if .Settings.Fancy.DepoTablesCheckOK}}✓{{else}}✗{{end}} {{.Settings.Fancy.DepoTablesCheck}}</p>{{end}}
<h3 style="margin-top:22px">3. Обработка обмена (job) {{if .Settings.Fancy.DepoJobCheckOK}}<span style="color:var(--ok);font-size:14px">✓ работает</span>{{end}}</h3>
<div class="help-block" style="font-size:13px"><strong>Что это.</strong> Процедура <code>{{.DepoJob.Proc}}</code> в базе DEPO каждые {{.DepoJob.Every}} с забирает записи bj из трансферных таблиц и создаёт по ним документы и операции DEPO. Кто-то должен её запускать.<br>
{{if .DepoEdition}}<strong>Ваш сервер:</strong> {{.DepoEdition}} — {{if .DepoAgent}}есть SQL Server Agent, рекомендуем джобу Agent, её создаст bj.{{else}}редакция без SQL Server Agent, процедуру будет запускать сам bj.{{end}}{{else if .DepoTables.Postgres}}<strong>Ваш сервер:</strong> PostgreSQL — процедуру будет запускать сам bj.{{end}}</div>
{{$mode := .Settings.Fancy.DepoJobMode}}{{if not $mode}}{{$mode = .DepoJobDef}}{{end}}
<form method="post" action="/admin/setup/fancy/depo-job" style="display:grid;gap:6px">
<input type="hidden" name="return_to" value="/admin/wizard?step=3">
<input type="hidden" name="depo_job_every_sec" value="{{.DepoJob.Every}}">
<input type="hidden" name="depo_job_proc" value="{{.Settings.Fancy.DepoJobProc}}">
<input type="hidden" name="depo_job_run_as" value="{{.Settings.Fancy.DepoJobRunAs}}">
{{if not .DepoTables.Postgres}}<label style="display:flex;gap:8px;align-items:flex-start"><input type="radio" name="depo_job_mode" value="sql-auto" {{if eq $mode "sql-auto"}}checked{{end}}> <span><b>Джоба SQL Server Agent, создаёт bj</b> <span class="muted" style="font-size:13px">— рекомендуется: работает, даже когда bj перезапускается; нужен разовый логин администратора SQL.</span></span></label>{{end}}
<label style="display:flex;gap:8px;align-items:flex-start"><input type="radio" name="depo_job_mode" value="bj" {{if eq $mode "bj"}}checked{{end}}> <span><b>Запускает сам bj</b> <span class="muted" style="font-size:13px">— для SQL Server Express и PostgreSQL; нужно одно право на процедуру.</span></span></label>
{{if not .DepoTables.Postgres}}<label style="display:flex;gap:8px;align-items:flex-start"><input type="radio" name="depo_job_mode" value="sql-manual" {{if eq $mode "sql-manual"}}checked{{end}}> <span><b>Джоба SQL Server Agent, создаёт администратор</b> <span class="muted" style="font-size:13px">— bj даст готовый скрипт.</span></span></label>{{end}}
<div><button type="submit" class="btn" style="{{$btn2}}">{{if .Settings.Fancy.DepoJobMode}}Сменить способ{{else}}Выбрать способ{{end}}</button></div>
</form>
{{if eq .Settings.Fancy.DepoJobMode "sql-auto"}}
<form method="post" action="/admin/setup/fancy/depo-job/create" style="margin-top:10px;display:grid;gap:8px">
<input type="hidden" name="return_to" value="/admin/wizard?step=3">
<p style="margin:0"><b>Создать джобу {{.DepoJob.Name}}.</b> <span class="muted" style="font-size:13px">Логин администратора SQL (sysadmin, например sa) нужен один раз и не сохраняется. SQL Server Agent должен быть запущен.</span></p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px">
<input type="text" name="admin_user" autocomplete="off" placeholder="логин, например sa" style="padding:8px">
<input type="password" name="admin_password" autocomplete="new-password" placeholder="пароль" style="padding:8px">
</div>
<div><button type="submit" class="btn">Создать джобу и проверить</button></div>
</form>
{{else if eq .Settings.Fancy.DepoJobMode "bj"}}
<p style="margin:10px 0 4px"><b>Администратору SQL — выполнить один раз:</b></p><pre class="copyable" style="font-size:12px">{{.DepoJob.GrantScript}}</pre>
{{else if eq .Settings.Fancy.DepoJobMode "sql-manual"}}
<p style="margin:10px 0 4px"><b>Скрипт для администратора SQL:</b></p><pre class="copyable" style="font-size:12px;max-height:260px;overflow:auto">{{.DepoJob.AgentScript}}</pre>
{{end}}
{{if .Settings.Fancy.DepoJobMode}}
<form method="post" action="/admin/setup/fancy/depo-job/check" style="margin-top:8px"><input type="hidden" name="return_to" value="/admin/wizard?step=3"><button type="submit" class="btn" style="{{$btn2}}">Проверить обработку</button> <span class="muted" style="font-size:12px">bj кладёт служебное событие и ждёт до 40 с, пока его обработают</span></form>
{{if .Settings.Fancy.DepoJobCheck}}<p style="font-size:13px;margin-top:6px;{{if .Settings.Fancy.DepoJobCheckOK}}color:var(--ok){{else}}color:var(--err){{end}}">{{if .Settings.Fancy.DepoJobCheckOK}}✓{{else}}✗{{end}} {{.Settings.Fancy.DepoJobCheck}}</p>{{end}}
{{end}}
{{end}}
{{end}}
<div style="margin-top:20px;display:flex;justify-content:space-between">
<a href="/admin/wizard?step=2" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">← К шагу 2</a>