DEPO: процедуры ставятся в мастере «Подготовить» + правка под клиента

- «Подготовить» в мастере (step 3, режим «дать логин администратора») теперь ставит
  и процедуры обработки bj2depo (CREATE OR ALTER) + GRANT, не только логин/таблицы/джобу.
- Скачиваемый скрипт мастера включает процедуры.
- Тексты процедур редактируются под клиента (Настройка → Обмен DEPO → Процедуры):
  сохранённый вариант используется при установке и при «Подготовить». Пусто/совпадение
  со встроенным — исходная версия.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
fontvielle
2026-10-20 14:12:54 +03:00
parent 45edbdbae6
commit eef6c9d996
6 changed files with 125 additions and 32 deletions
+2
View File
@@ -101,6 +101,8 @@ var templateFuncs = template.FuncMap{
"whForm": func(w WorkHoursSettings, ret string) map[string]any { return map[string]any{"W": w, "Ret": ret} },
"weekdayNames": func() []string { return weekdayShort },
"depoAccPrefix": depoAccPrefix,
"depoProcText": depoProcEffective,
"depoProcList": depoProcList,
"certDaysLeft": certDaysLeft,
"currentContour": func() string { return contourOf(currentNSDProfile()) },
"httpsCert": func() map[string]any {
+102 -32
View File
@@ -43,7 +43,7 @@ func splitSQLBatches(s string) []string {
return out
}
// depoProcFile — тело встроенной процедуры.
// depoProcFile — тело встроенной (исходной) процедуры из файла.
func depoProcFile(file string) (string, error) {
b, err := depoProcFS.ReadFile(file)
if err != nil {
@@ -52,25 +52,83 @@ func depoProcFile(file string) (string, error) {
return string(b), nil
}
// embeddedProcBody — встроенный текст процедуры по имени.
func embeddedProcBody(name string) string {
for _, p := range depoProcInstallOrder {
if p.Name == name {
b, _ := depoProcFile(p.File)
return b
}
}
return ""
}
// depoProcEffective — текст процедуры с учётом правки под клиента: сохранённый
// в настройках вариант, иначе встроенный. Используется в предпросмотре, установке
// и скачиваемом скрипте.
func depoProcEffective(f FancyMSSQLSettings, name string) string {
if v, ok := f.DepoProcOverrides[name]; ok && strings.TrimSpace(v) != "" {
return v
}
return embeddedProcBody(name)
}
// depoProcList — порядок процедур для шаблона (редактор текстов).
func depoProcList() []struct{ Name, File string } { return depoProcInstallOrder }
// depoProcsCombined — единый скрипт установки (для скачивания администратору): USE, процедуры, GRANT.
func depoProcsCombined(f FancyMSSQLSettings) string {
return depoProcsScriptFor(f, f.DepoDB(), depoSetupLogin(f))
}
// depoProcsScriptFor — скрипт установки процедур в базу db с выдачей EXECUTE логину login.
// Тексты процедур — с учётом правки под клиента (depoProcEffective).
func depoProcsScriptFor(f FancyMSSQLSettings, db, login string) string {
var b strings.Builder
login := depoSetupLogin(f)
b.WriteString("/* Bridge&Join: установка процедур обработки bj2depo в базу DEPO.\n")
b.WriteString("/* Bridge&Join: процедуры обработки bj2depo для базы DEPO.\n")
b.WriteString(" Порядок WIRING -> OPERATION -> EVENTS -> CREATE -> JOB. Запуск под sysadmin в SSMS.\n")
b.WriteString(" CREATE OR ALTER — повторный запуск безопасен (данные не меняются). */\n")
fmt.Fprintf(&b, "USE %s;\nGO\n\n", quoteIdent(f.DepoDB()))
fmt.Fprintf(&b, "USE %s;\nGO\n\n", quoteIdent(db))
for _, p := range depoProcInstallOrder {
body, err := depoProcFile(p.File)
if err != nil {
body := depoProcEffective(f, p.Name)
if strings.TrimSpace(body) == "" {
continue
}
fmt.Fprintf(&b, "/* ===== %s ===== */\n%s\nGO\n\n", p.Name, strings.TrimRight(body, "\n"))
}
fmt.Fprintf(&b, "/* Право запуска задания логину bj */\nGRANT EXECUTE ON dbo.FTV_M2M_JOB TO %s;\nGO\n", quoteIdent(login))
if depoLoginRe.MatchString(login) {
fmt.Fprintf(&b, "/* Право запуска задания логину bj */\nGRANT EXECUTE ON dbo.FTV_M2M_JOB TO %s;\nGO\n", quoteIdent(login))
}
return b.String()
}
// depoProcsInstallInto — установить процедуры (CREATE OR ALTER, с учётом правки под клиента) в базу
// dbName соединением adm (логин администратора) и выдать EXECUTE логину grantLogin.
func depoProcsInstallInto(ctx context.Context, adm, f FancyMSSQLSettings, dbName, grantLogin string) error {
db, err := fancyOpen(adm, dbName)
if err != nil {
return err
}
defer db.Close()
for _, p := range depoProcInstallOrder {
body := depoProcEffective(f, p.Name)
if strings.TrimSpace(body) == "" {
return fmt.Errorf("пустой текст процедуры %s", p.Name)
}
for _, batch := range splitSQLBatches(body) {
if _, err := db.ExecContext(ctx, batch); err != nil {
return fmt.Errorf("установка %s: %s", p.Name, quikErrorDetail(err))
}
}
}
if depoLoginRe.MatchString(grantLogin) {
if _, err := db.ExecContext(ctx, "GRANT EXECUTE ON dbo.FTV_M2M_JOB TO "+quoteIdent(grantLogin)); err != nil {
return fmt.Errorf("право EXECUTE логину %s: %s", grantLogin, quikErrorDetail(err))
}
}
return nil
}
// quoteIdent — квадратные скобки для идентификатора T-SQL.
func quoteIdent(s string) string { return "[" + strings.ReplaceAll(s, "]", "]]") + "]" }
@@ -137,35 +195,15 @@ func depoProcsInstall(ctx context.Context, f FancyMSSQLSettings, adminUser, admi
if !canApply {
return "", fmt.Errorf("нет прав на установку. %s", report)
}
conn := f
adm := f
if u := strings.TrimSpace(adminUser); u != "" {
conn.User, conn.Password = u, adminPass
adm.User, adm.Password = u, adminPass
}
db, err := fancyOpen(conn, f.DepoDB())
if err != nil {
login := depoSetupLogin(f)
if err := depoProcsInstallInto(ctx, adm, f, f.DepoDB(), login); err != nil {
return "", err
}
defer db.Close()
for _, p := range depoProcInstallOrder {
body, err := depoProcFile(p.File)
if err != nil {
return "", fmt.Errorf("встроенный файл %s: %w", p.File, err)
}
for _, batch := range splitSQLBatches(body) {
if _, err := db.ExecContext(ctx, batch); err != nil {
return "", fmt.Errorf("установка %s: %s", p.Name, quikErrorDetail(err))
}
}
}
msg := "установлено процедур: 5 (WIRING, OPERATION, EVENTS, CREATE, JOB)"
login := depoSetupLogin(f)
if depoLoginRe.MatchString(login) {
if _, err := db.ExecContext(ctx, "GRANT EXECUTE ON dbo.FTV_M2M_JOB TO "+quoteIdent(login)); err != nil {
msg += "; право EXECUTE логину " + login + " не выдано: " + quikErrorDetail(err)
} else {
msg += "; право EXECUTE логину " + login + " выдано"
}
}
msg := "установлено процедур: 5 (WIRING, OPERATION, EVENTS, CREATE, JOB); право EXECUTE логину " + login + " выдано"
log.Printf("depo-procs: %s", msg)
return msg, nil
}
@@ -237,3 +275,35 @@ func (h *setupHandlers) fancyDepoProcsScript(w http.ResponseWriter, r *http.Requ
// UTF-8 BOM (0xEF 0xBB 0xBF): SSMS без него читает кириллицу в комментариях как ANSI.
_, _ = w.Write(append([]byte{0xEF, 0xBB, 0xBF}, depoProcsCombined(s)...))
}
// saveFancyDepoProcsText — POST /admin/setup/fancy/depo-procs/save-text: сохранить правку текстов процедур
// под клиента. Пустое поле или совпадение со встроенным текстом — вернуть встроенную версию (убрать правку).
func (h *setupHandlers) saveFancyDepoProcsText(w http.ResponseWriter, r *http.Request) {
if !requireDepo(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Fancy
ov := map[string]string{}
changed := 0
for _, p := range depoProcInstallOrder {
v := strings.ReplaceAll(r.FormValue("proc_"+p.Name), "\r\n", "\n")
if strings.TrimSpace(v) != "" && strings.TrimSpace(v) != strings.TrimSpace(embeddedProcBody(p.Name)) {
ov[p.Name] = v
changed++
}
}
if changed == 0 {
s.DepoProcOverrides = nil
} else {
s.DepoProcOverrides = ov
}
if err := h.rc.UpdateFancy(s); err != nil {
setupFlash(w, r, "Тексты процедур: ошибка сохранения — "+err.Error())
return
}
setupFlash(w, r, fmt.Sprintf("Тексты процедур сохранены. Изменённых под клиента: %d (пустые или совпадающие со встроенными — используется встроенная версия).", changed))
}
+1
View File
@@ -29,6 +29,7 @@ func (h *setupHandlers) registerFancy(mux *http.ServeMux) {
mux.HandleFunc("/admin/setup/fancy/depo-procs/check", h.checkFancyDepoProcs)
mux.HandleFunc("/admin/setup/fancy/depo-procs/install", h.installFancyDepoProcs)
mux.HandleFunc("/admin/setup/fancy/depo-procs/script.sql", h.fancyDepoProcsScript)
mux.HandleFunc("/admin/setup/fancy/depo-procs/save-text", h.saveFancyDepoProcsText)
mux.HandleFunc("/admin/fancy-queue", h.fancyQueue)
mux.HandleFunc("/admin/fancy-queue/requeue", h.fancyQueueRequeue)
mux.HandleFunc("/admin/fancy-queue/dismiss", h.fancyQueueDismiss)
+2
View File
@@ -250,6 +250,8 @@ type FancyMSSQLSettings struct {
DepoJobName string `json:"depo_job_name,omitempty"`
DepoNewUser string `json:"depo_new_user,omitempty"`
DepoNewPass string `json:"depo_new_password,omitempty"`
// DepoProcOverrides — правка текстов процедур bj2depo под клиента (имя процедуры -> SQL); пусто = встроенный текст.
DepoProcOverrides map[string]string `json:"depo_proc_overrides,omitempty"`
}
// IsPostgres сообщает, выбран ли PostgreSQL (иначе — MSSQL по умолчанию).
@@ -130,6 +130,12 @@
</div>
</form>
<p style="margin-top:8px"><a href="/admin/setup/fancy/depo-procs/script.sql">Скачать скрипт bj-depo-procs-mssql.sql</a> <span class="muted" style="font-size:12px">— для ручной установки администратором в SSMS</span></p>
<details style="margin-top:10px"><summary class="muted" style="cursor:pointer">Тексты процедур — изменить под клиента</summary>
<form method="post" action="/admin/setup/fancy/depo-procs/save-text" style="display:grid;gap:10px;margin-top:10px">
<input type="hidden" name="section" value="sec-fancy-depo-procs">
{{range depoProcList}}<label style="font-weight:600;font-size:13px">{{.Name}}<textarea name="proc_{{.Name}}" rows="10" spellcheck="false" style="width:100%;font-family:ui-monospace,monospace;font-size:11.5px">{{depoProcText $.Settings.Fancy .Name}}</textarea></label>{{end}}
<div><button type="submit" class="btn">Сохранить тексты</button> <span class="muted" style="font-size:12px">пустое поле или совпадение со встроенным — вернуть исходную версию; изменённые ставятся при «Установить» и при «Подготовить» в мастере</span></div>
</form></details>
{{if .Settings.Fancy.DepoProcCheck}}<p style="margin-top:6px;font-size:13px;{{if .Settings.Fancy.DepoProcCheckOK}}color:var(--ok){{else}}color:var(--warn){{end}}">{{if .Settings.Fancy.DepoProcCheckOK}}✓{{else}}⚠{{end}} {{.Settings.Fancy.DepoProcCheck}}</p>{{end}}
</div>
{{end}}
+12
View File
@@ -323,6 +323,9 @@ func (h *setupHandlers) wizardDepoScript(w http.ResponseWriter, r *http.Request)
}
}
body := depoSetupScript(s, db, s.User, s.Password, agent)
if !s.IsPostgres() { // процедуры обработки bj2depo — тем же файлом
body += "\nGO\n" + depoProcsScriptFor(s, db, s.User)
}
w.Header().Set("Cache-Control", "no-store")
if view {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -410,6 +413,15 @@ func (h *setupHandlers) depoSetupDo(ctx context.Context, r *http.Request, step d
if err := depoSetupRun(ctx, s, db, au, r.FormValue("admin_password"), s.User, s.Password, agent, step); err != nil {
return fail("база не подготовлена — " + err.Error())
}
if !s.IsPostgres() { // процедуры обработки bj2depo (CREATE OR ALTER, с учётом правки под клиента)
step("procs", "run", "Устанавливаю процедуры обработки bj2depo")
adm := s
adm.User, adm.Password = au, r.FormValue("admin_password")
if err := depoProcsInstallInto(ctx, adm, s, db, s.User); err != nil {
return fail("процедуры обработки не установлены — " + err.Error())
}
step("procs", "ok", "")
}
s.DepoTablesMode = "auto"
note = "база " + db + " подготовлена: логин " + s.User + ", таблицы обмена, права"
if agent {