7981105cec
Процедуры Фонтвьель, вызванные bj (FTV_M2M_JOB → EVENTS → UPDATE_HANDBOOK, CREATE, OPERATION), пишут в таблицы Fancy правами логина bj. На 9 у bj_user был только db_datareader — «Запрещено разрешение INSERT на объект OD_OBJECTS» (событие HANDBOOK, 22.10.2026); упали бы и поручения M2M. На тесте — db_datareader + db_datawriter. Роль выдаётся в наборе «Применить скрипты» (права администратора БД; новый отпечаток — плашка на установках), в скачиваемом скрипте процедур, в скрипте логина и при подготовке трансферных таблиц (первичная установка). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
413 lines
21 KiB
Go
413 lines
21 KiB
Go
package lkgateway
|
|
|
|
// depo_procs.go — установка процедур обработки bj2depo (FTV_M2M_PROCESS_* и FTV_M2M_JOB)
|
|
// в базу DEPO прямо из bj: «Проверить возможности» (предпросмотр без изменений) и
|
|
// «Установить» (выполняется логином администратора SQL — разово, не сохраняется).
|
|
// Процедуры встроены в bj (internal/lkgateway/depoprocs/*.sql), порядок установки —
|
|
// WIRING → OPERATION → UPDATE_HANDBOOK → EVENTS → CREATE → JOB; затем GRANT EXECUTE логину bj.
|
|
// Тексты — с тестовой DEPO Фонтвьель на 22.10.2026 (правки программиста 09–21.10; EVENTS вызывает
|
|
// UPDATE_HANDBOOK — без неё обработка событий падает).
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"embed"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
//go:embed depoprocs/*.sql
|
|
var depoProcFS embed.FS
|
|
|
|
// depoProcInstallOrder — имя процедуры и файл; порядок важен для зависимостей.
|
|
var depoProcInstallOrder = []struct{ Name, File string }{
|
|
{"FTV_M2M_PROCESS_WIRING", "depoprocs/FTV_M2M_PROCESS_WIRING.sql"},
|
|
{"FTV_M2M_PROCESS_OPERATION", "depoprocs/FTV_M2M_PROCESS_OPERATION.sql"},
|
|
{"FTV_M2M_UPDATE_HANDBOOK", "depoprocs/FTV_M2M_UPDATE_HANDBOOK.sql"}, // вызывается из EVENTS (HANDBOOK)
|
|
{"FTV_M2M_PROCESS_EVENTS", "depoprocs/FTV_M2M_PROCESS_EVENTS.sql"},
|
|
{"FTV_M2M_PROCESS_CREATE", "depoprocs/FTV_M2M_PROCESS_CREATE.sql"},
|
|
{"FTV_M2M_JOB", "depoprocs/FTV_M2M_JOB.sql"},
|
|
}
|
|
|
|
var goBatchRe = regexp.MustCompile(`(?im)^\s*GO\s*$`)
|
|
|
|
// splitSQLBatches — разбить скрипт на батчи по строкам «GO» (директива клиента, не T-SQL).
|
|
func splitSQLBatches(s string) []string {
|
|
var out []string
|
|
for _, part := range goBatchRe.Split(s, -1) {
|
|
if strings.TrimSpace(part) != "" {
|
|
out = append(out, part)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// depoProcFile — тело встроенной (исходной) процедуры из файла.
|
|
func depoProcFile(file string) (string, error) {
|
|
b, err := depoProcFS.ReadFile(file)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
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)
|
|
}
|
|
|
|
// depoProcOrderText — порядок установки коротко: WIRING→OPERATION→UPDATE_HANDBOOK→…
|
|
func depoProcOrderText(sep string) string {
|
|
var parts []string
|
|
for _, p := range depoProcInstallOrder {
|
|
parts = append(parts, strings.TrimPrefix(strings.TrimPrefix(p.Name, "FTV_M2M_PROCESS_"), "FTV_M2M_"))
|
|
}
|
|
return strings.Join(parts, sep)
|
|
}
|
|
|
|
// depoSchemaFixes — правки структуры трансферных таблиц, которые логин bj сделать не может (нужны права
|
|
// администратора БД); выполняются вместе с процедурами. Повторный запуск безопасен.
|
|
var depoSchemaFixes = []struct{ Name, SQL string }{
|
|
// 22.10.2026: служебные события (HANDBOOK) — без GUID; в таблицах, созданных bj до этого, GUID NOT NULL
|
|
{"FTV_M2M_EVENTS.GUID NULL", `IF EXISTS (SELECT 1 FROM sys.columns WHERE object_id=OBJECT_ID(N'dbo.FTV_M2M_EVENTS') AND name=N'GUID' AND is_nullable=0)
|
|
ALTER TABLE dbo.FTV_M2M_EVENTS ALTER COLUMN GUID uniqueidentifier NULL`},
|
|
}
|
|
|
|
// depoProcsHash — отпечаток набора процедур, который bj установит (с учётом правки под клиента).
|
|
func depoProcsHash(f FancyMSSQLSettings) string {
|
|
h := sha256.New()
|
|
for _, fx := range depoSchemaFixes {
|
|
h.Write([]byte(fx.Name + "\n" + fx.SQL + "\n"))
|
|
}
|
|
h.Write([]byte("grant db_datawriter\n")) // 22.10.2026: роль записи логину bj — новый отпечаток, плашка на установках
|
|
for _, p := range depoProcInstallOrder {
|
|
h.Write([]byte(p.Name + "\n" + strings.TrimSpace(depoProcEffective(f, p.Name)) + "\n"))
|
|
}
|
|
return hex.EncodeToString(h.Sum(nil))[:16]
|
|
}
|
|
|
|
// depoProcsPending — обновление bj принесло процедуры DEPO, которые ещё не применены в базе: bj работает с
|
|
// DEPO (подключение есть, обработка включена), а отпечаток установленного набора не совпадает со встроенным.
|
|
func depoProcsPending() bool {
|
|
if globalRC == nil {
|
|
return false
|
|
}
|
|
f := globalRC.Snapshot().Fancy
|
|
if strings.TrimSpace(f.Server) == "" || (!f.DepoJobRun && f.DepoJobMode == "") {
|
|
return false
|
|
}
|
|
return f.DepoProcsApplied != depoProcsHash(f)
|
|
}
|
|
|
|
// 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
|
|
b.WriteString("/* Bridge&Join: процедуры обработки bj2depo для базы DEPO.\n")
|
|
b.WriteString(" Порядок " + depoProcOrderText(" -> ") + ". Запуск под sysadmin в SSMS.\n")
|
|
b.WriteString(" CREATE OR ALTER — повторный запуск безопасен (данные не меняются). */\n")
|
|
fmt.Fprintf(&b, "USE %s;\nGO\n\n", quoteIdent(db))
|
|
for _, fx := range depoSchemaFixes {
|
|
fmt.Fprintf(&b, "/* ===== структура: %s ===== */\n%s;\nGO\n\n", fx.Name, fx.SQL)
|
|
}
|
|
for _, p := range depoProcInstallOrder {
|
|
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"))
|
|
}
|
|
if depoLoginRe.MatchString(login) {
|
|
b.WriteString("/* Запись в таблицы Fancy: процедуры обработки M2M работают правами логина bj (без неё — «Запрещено разрешение INSERT на объект OD_OBJECTS») */\n")
|
|
fmt.Fprintf(&b, "IF IS_ROLEMEMBER(N'db_datawriter', N'%[1]s') = 0 ALTER ROLE db_datawriter ADD MEMBER %[2]s;\nGO\n", login, quoteIdent(login))
|
|
b.WriteString("/* Права логину bj на все процедуры: без прав OBJECT_ID не видит их и проверка пишет «нет процедур» */\n")
|
|
for _, p := range depoProcInstallOrder {
|
|
fmt.Fprintf(&b, "GRANT EXECUTE ON dbo.%s TO %s;\n", p.Name, quoteIdent(login))
|
|
}
|
|
b.WriteString("GO\n")
|
|
}
|
|
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 _, fx := range depoSchemaFixes {
|
|
if _, err := db.ExecContext(ctx, fx.SQL); err != nil {
|
|
return fmt.Errorf("структура таблиц (%s): %s", fx.Name, quikErrorDetail(err))
|
|
}
|
|
}
|
|
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) {
|
|
// Запись в таблицы Fancy: процедуры Фонтвьель, вызванные bj, работают правами логина bj.
|
|
if _, err := db.ExecContext(ctx, "IF DATABASE_PRINCIPAL_ID(N'"+grantLogin+"') IS NOT NULL AND IS_ROLEMEMBER(N'db_datawriter', N'"+grantLogin+"') = 0 ALTER ROLE db_datawriter ADD MEMBER "+quoteIdent(grantLogin)); err != nil {
|
|
return fmt.Errorf("роль db_datawriter логину %s: %s", grantLogin, quikErrorDetail(err))
|
|
}
|
|
// Право на ВСЕ процедуры: без него OBJECT_ID под bj_user = NULL, и проверка пишет «нет процедур».
|
|
for _, p := range depoProcInstallOrder {
|
|
if _, err := db.ExecContext(ctx, "GRANT EXECUTE ON dbo."+p.Name+" TO "+quoteIdent(grantLogin)); err != nil {
|
|
return fmt.Errorf("право EXECUTE логину %s на %s: %s", grantLogin, p.Name, quikErrorDetail(err))
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// quoteIdent — квадратные скобки для идентификатора T-SQL.
|
|
func quoteIdent(s string) string { return "[" + strings.ReplaceAll(s, "]", "]]") + "]" }
|
|
|
|
// depoProcsPreflight — проверка возможности установки (без изменений в базе). Возвращает отчёт
|
|
// для экрана, признак «можно применять» и ошибку подключения/проверки.
|
|
func depoProcsPreflight(ctx context.Context, f FancyMSSQLSettings, adminUser, adminPass string) (report string, canApply bool, err error) {
|
|
if f.IsPostgres() {
|
|
return "", false, fmt.Errorf("установка процедур bj2depo поддержана только для MSSQL")
|
|
}
|
|
conn := f
|
|
admin := strings.TrimSpace(adminUser)
|
|
if admin != "" {
|
|
conn.User, conn.Password = admin, adminPass
|
|
}
|
|
db, err := fancyOpen(conn, f.DepoDB())
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
defer db.Close()
|
|
if err := db.PingContext(ctx); err != nil {
|
|
return "", false, fmt.Errorf("подключение к базе %s логином %s: %s", f.DepoDB(), conn.User, quikErrorDetail(err))
|
|
}
|
|
if err := depoCheckIsDepo(ctx, conn, f.DepoDB()); err != nil {
|
|
return "", false, err
|
|
}
|
|
missing := depoMissing(ctx, db, depoBj2depoProcs, "P")
|
|
login := depoSetupLogin(f)
|
|
execAs := depoProcExecAsUser(f) // пользователь для EXECUTE AS в теле процедур (обычно SYSDBA)
|
|
var canCreate, sysadmin, bjUser, execAsOK int
|
|
_ = db.QueryRowContext(ctx,
|
|
`SELECT CAST(ISNULL(HAS_PERMS_BY_NAME(NULL,'DATABASE','CREATE PROCEDURE'),0) AS int),
|
|
CAST(ISNULL(IS_SRVROLEMEMBER('sysadmin'),0) AS int),
|
|
CASE WHEN DATABASE_PRINCIPAL_ID(@u) IS NULL THEN 0 ELSE 1 END,
|
|
CASE WHEN DATABASE_PRINCIPAL_ID(@e) IS NULL THEN 0 ELSE 1 END`,
|
|
sql.Named("u", login), sql.Named("e", execAs)).Scan(&canCreate, &sysadmin, &bjUser, &execAsOK)
|
|
canApply = canCreate == 1 || sysadmin == 1
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "Сервер %s, база %s, логин %s.\n", f.Server, f.DepoDB(), conn.User)
|
|
if len(missing) == 0 {
|
|
fmt.Fprintf(&b, "Процедуры: все %d уже установлены (будут обновлены CREATE OR ALTER).\n", len(depoProcInstallOrder))
|
|
} else {
|
|
fmt.Fprintf(&b, "Процедуры: нет %d из %d (%s).\n", len(missing), len(depoProcInstallOrder), strings.Join(missing, ", "))
|
|
}
|
|
if canApply {
|
|
b.WriteString("Право создавать процедуры: есть.\n")
|
|
} else {
|
|
b.WriteString("Право создавать процедуры: НЕТ — введите логин администратора SQL (sysadmin, обычно sa).\n")
|
|
}
|
|
if bjUser == 1 {
|
|
fmt.Fprintf(&b, "Логин %s в базе: есть — выдам ему EXECUTE на FTV_M2M_JOB.\n", login)
|
|
} else {
|
|
fmt.Fprintf(&b, "Логин %s в базе: нет — сначала подготовьте базу (логин/таблицы), GRANT будет пропущен.\n", login)
|
|
}
|
|
if execAsOK == 1 {
|
|
fmt.Fprintf(&b, "Пользователь %s (для EXECUTE AS в процедурах): есть.\n", execAs)
|
|
} else {
|
|
fmt.Fprintf(&b, "Пользователь %s (для EXECUTE AS в процедурах): НЕТ — процедуры FTV_M2M_PROCESS_* используют EXECUTE AS USER='%s' и не создадутся без него. Создайте этого пользователя в базе (как в рабочей базе Fancy) или замените имя в «Тексты процедур».\n", execAs, execAs)
|
|
}
|
|
fmt.Fprintf(&b, "Будет выполнено: CREATE OR ALTER %d процедур (%s) + GRANT EXECUTE логину bj.", len(depoProcInstallOrder), depoProcOrderText("→"))
|
|
return b.String(), canApply, nil
|
|
}
|
|
|
|
// depoProcExecAsUser — пользователь, под которым процедуры выполняют EXECUTE AS (из их текста,
|
|
// строка «execute as user = '...'»). По умолчанию SYSDBA.
|
|
func depoProcExecAsUser(f FancyMSSQLSettings) string {
|
|
body := depoProcEffective(f, "FTV_M2M_PROCESS_WIRING")
|
|
if m := execAsUserRe.FindStringSubmatch(body); len(m) == 2 {
|
|
return m[1]
|
|
}
|
|
return "SYSDBA"
|
|
}
|
|
|
|
var execAsUserRe = regexp.MustCompile(`(?i)execute\s+as\s+user\s*=\s*'([^']+)'`)
|
|
|
|
// depoProcErrHint — подсказка по типичной причине отказа CREATE процедур.
|
|
func depoProcErrHint(msg string) string {
|
|
l := strings.ToLower(msg)
|
|
if strings.Contains(l, "sysdba") || strings.Contains(l, "principal") || strings.Contains(l, "15517") ||
|
|
strings.Contains(l, "execute as") || strings.Contains(l, "не удается олицетворить") || strings.Contains(l, "олицетвор") {
|
|
return " Процедуры FTV_M2M_PROCESS_* используют EXECUTE AS USER='SYSDBA' — в базе DEPO должен быть пользователь SYSDBA (как в рабочей базе Fancy). Создайте его или замените имя пользователя в «Тексты процедур» и повторите."
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// depoProcsInstall — установить процедуры логином администратора. Сначала preflight; при отсутствии
|
|
// прав — отказ без изменений. Возвращает итоговое сообщение.
|
|
func depoProcsInstall(ctx context.Context, f FancyMSSQLSettings, adminUser, adminPass string) (string, error) {
|
|
report, canApply, err := depoProcsPreflight(ctx, f, adminUser, adminPass)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !canApply {
|
|
return "", fmt.Errorf("нет прав на установку. %s", report)
|
|
}
|
|
adm := f
|
|
if u := strings.TrimSpace(adminUser); u != "" {
|
|
adm.User, adm.Password = u, adminPass
|
|
}
|
|
login := depoSetupLogin(f)
|
|
if err := depoProcsInstallInto(ctx, adm, f, f.DepoDB(), login); err != nil {
|
|
return "", err
|
|
}
|
|
msg := fmt.Sprintf("структура таблиц проверена; установлено процедур: %d (%s); логину %s — EXECUTE и роль db_datawriter", len(depoProcInstallOrder), depoProcOrderText(", "), login)
|
|
log.Printf("depo-procs: %s", msg)
|
|
return msg, nil
|
|
}
|
|
|
|
// ---- хендлеры ----
|
|
|
|
// checkFancyDepoProcs — POST /admin/setup/fancy/depo-procs/check: проверка возможностей (без изменений).
|
|
func (h *setupHandlers) checkFancyDepoProcs(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
|
|
if s.Server == "" {
|
|
setupFlash(w, r, "Процедуры DEPO: сначала сохраните подключение к серверу БД")
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(depoLoginForce(r.Context()), time.Minute)
|
|
defer cancel()
|
|
report, canApply, err := depoProcsPreflight(ctx, s, r.FormValue("admin_user"), r.FormValue("admin_password"))
|
|
if err != nil {
|
|
setupFlash(w, r, "Процедуры DEPO: проверка не удалась — "+err.Error())
|
|
return
|
|
}
|
|
verdict := "можно устанавливать"
|
|
if !canApply {
|
|
verdict = "установка невозможна"
|
|
}
|
|
setupFlash(w, r, "Процедуры DEPO ("+verdict+"): "+report)
|
|
}
|
|
|
|
// installFancyDepoProcs — POST /admin/setup/fancy/depo-procs/install: установить процедуры.
|
|
func (h *setupHandlers) installFancyDepoProcs(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
|
|
if s.Server == "" {
|
|
setupFlash(w, r, "Процедуры DEPO: сначала сохраните подключение к серверу БД")
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(depoLoginForce(r.Context()), 2*time.Minute)
|
|
defer cancel()
|
|
msg, err := depoProcsInstall(ctx, s, r.FormValue("admin_user"), r.FormValue("admin_password"))
|
|
if err != nil {
|
|
log.Printf("depo-procs: установка не удалась: %v", err)
|
|
txt := "установка не удалась: " + err.Error() + depoProcErrHint(err.Error())
|
|
s2 := h.rc.Snapshot().Fancy // сохранить ошибку, чтобы не мелькала во флеше
|
|
s2.DepoProcCheck, s2.DepoProcCheckOK, s2.DepoProcCheckAt = txt, false, time.Now()
|
|
_ = h.rc.UpdateFancy(s2)
|
|
setupFlash(w, r, "Процедуры DEPO: "+txt)
|
|
return
|
|
}
|
|
pmsg, pok := depoProcCheck(ctx, h.rc)
|
|
s = h.rc.Snapshot().Fancy
|
|
s.DepoProcCheck, s.DepoProcCheckOK, s.DepoProcCheckAt = pmsg, pok, time.Now()
|
|
s.DepoProcsApplied, s.DepoProcsAppliedAt = depoProcsHash(s), time.Now() // применено — плашка обновления уходит
|
|
_ = h.rc.UpdateFancy(s)
|
|
setupFlash(w, r, "Процедуры DEPO: "+msg+". Проверка обработки: "+pmsg)
|
|
}
|
|
|
|
// fancyDepoProcsScript — GET /admin/setup/fancy/depo-procs/script.sql: единый скрипт для администратора.
|
|
func (h *setupHandlers) fancyDepoProcsScript(w http.ResponseWriter, r *http.Request) {
|
|
s := h.rc.Snapshot().Fancy
|
|
w.Header().Set("Content-Type", "application/sql; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", `attachment; filename="bj-depo-procs-mssql.sql"`)
|
|
// 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))
|
|
}
|