package lkgateway // depo_procs.go — установка процедур обработки bj2depo (FTV_M2M_PROCESS_* и FTV_M2M_JOB) // в базу DEPO прямо из bj: «Проверить возможности» (предпросмотр без изменений) и // «Установить» (выполняется логином администратора SQL — разово, не сохраняется). // Процедуры встроены в bj (internal/lkgateway/depoprocs/*.sql), порядок установки — // WIRING → OPERATION → EVENTS → CREATE → JOB; затем GRANT EXECUTE на FTV_M2M_JOB логину bj. import ( "context" "database/sql" "embed" "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_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 } // depoProcsCombined — единый скрипт установки (для скачивания администратору): USE, процедуры, GRANT. func depoProcsCombined(f FancyMSSQLSettings) string { var b strings.Builder login := depoSetupLogin(f) 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())) for _, p := range depoProcInstallOrder { body, err := depoProcFile(p.File) if err != nil { 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)) return b.String() } // 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) var canCreate, sysadmin, bjUser 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`, sql.Named("u", login)).Scan(&canCreate, &sysadmin, &bjUser) 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 { b.WriteString("Процедуры: все 5 уже установлены (будут обновлены CREATE OR ALTER).\n") } else { fmt.Fprintf(&b, "Процедуры: нет %d из 5 (%s).\n", len(missing), 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) } b.WriteString("Будет выполнено: CREATE OR ALTER 5 процедур (WIRING→OPERATION→EVENTS→CREATE→JOB) + GRANT EXECUTE на FTV_M2M_JOB.") return b.String(), canApply, nil } // 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) } conn := f if u := strings.TrimSpace(adminUser); u != "" { conn.User, conn.Password = u, adminPass } db, err := fancyOpen(conn, f.DepoDB()) if 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 + " выдано" } } 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) setupFlash(w, r, "Процедуры DEPO: не установлены — "+err.Error()) return } pmsg, pok := depoProcCheck(ctx, h.rc) s = h.rc.Snapshot().Fancy s.DepoProcCheck, s.DepoProcCheckOK, s.DepoProcCheckAt = pmsg, pok, 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)...)) }