Files
Bridge-and-Join-s/internal/lkgateway/depo_procs.go
T
fontvielle 045dfac6c7 DEPO: установка процедур bj2depo из bj (проверка возможностей + применение)
Процедуры FTV_M2M_PROCESS_* и FTV_M2M_JOB встроены в bj (depoprocs/*.sql).
В разделе «Учёт DEPO» карточка «Процедуры обработки bj2depo»:
- «Проверить возможности» — подключение, это ли база DEPO, какие процедуры
  установлены, есть ли право CREATE PROCEDURE и логин bj в базе (ничего не меняет);
- «Установить» — логином администратора SQL (разово, не сохраняется) выполняет
  CREATE OR ALTER в порядке WIRING→OPERATION→EVENTS→CREATE→JOB и GRANT EXECUTE
  на FTV_M2M_JOB логину bj; без прав — отказ без изменений;
- скачивание единого скрипта для ручной установки в SSMS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-20 13:56:40 +03:00

240 lines
10 KiB
Go

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)...))
}