package lkgateway // Подготовка базы DEPO на Firebird (Fancy на Firebird 2.5, 27.10.2026) — то же, что depo_setup.go для MSSQL: // одним действием логином администратора (обычно SYSDBA, не сохраняется) или скриптом для администратора. // Отличия от MSSQL: // - логин bj — SYSDBA_BJ: Fancy даёт логинам SYSDBA% все права на документы (FU_HAVE_RIGHT: USER like 'SYSDBA%'), // а EXECUTE AS, которым MSSQL-версия переходит на SYSDBA, в Firebird нет. Для Firebird это обычный // пользователь сервера: права только выданные — чтение учёта, таблицы обмена, запуск FTV_M2M_JOB; // - логин bj заводится оператором Fancy (OD_OPERATORS, признак «робот»): по оператору текущего пользователя // Fancy выбирает депозитарий и нумерует документы (FU_NEXT_DOC_NUM); // - обработку запускает bj (SQL Server Agent нет); // - таблицы, процедуры и права — встроенные скрипты depoprocs/firebird (копия — deploy/depo/fancy-m2m-firebird); // привязки к справочникам клиента (FTV_M2M_SETTINGS) bj определяет сам по признакам Fancy. // Пароль Firebird 2.5 различает только по первым 8 знакам. import ( "context" "database/sql" "embed" "fmt" "regexp" "strings" "time" "golang.org/x/text/encoding" "golang.org/x/text/encoding/charmap" ) //go:embed depoprocs/firebird/*.sql var depoFBFS embed.FS // depoFBScriptFiles — скрипты установки по порядку: таблицы, процедуры, права. var depoFBScriptFiles = []string{"00_FTV_M2M_TABLES.sql", "10_FTV_M2M_PROCS.sql", "20_FTV_M2M_GRANTS.sql"} // depoFBDefaultLogin — логин bj в Firebird (имя на SYSDBA — условие прав Fancy). const depoFBDefaultLogin = "SYSDBA_BJ" // fbLoginRe — имя пользователя Firebird без кавычек, на SYSDBA (иначе Fancy запретит bj создавать документы). var fbLoginRe = regexp.MustCompile(`^(?i:SYSDBA)[A-Za-z0-9_$]{1,25}$`) func depoFBScript(name string) string { b, _ := depoFBFS.ReadFile("depoprocs/firebird/" + name) return string(b) } // depoFBGrants — права (20) для логина bj. func depoFBGrants(login string) string { return strings.ReplaceAll(depoFBScript("20_FTV_M2M_GRANTS.sql"), ":BJ_USER", login) } // depoFBBindings — оператор Fancy для логина bj, кодировщик M2M и привязки FTV_M2M_SETTINGS по признакам Fancy // (заполняются только пустые: исправленное вручную повторная установка не трогает). Один блок PSQL — и для // установки из bj, и в скрипте администратора. func depoFBBindings(login string) string { return fmt.Sprintf(`EXECUTE BLOCK AS DECLARE VARIABLE V INTEGER; BEGIN /* логин bj — оператор Fancy (робот): по оператору Fancy выбирает депозитарий и нумерует документы */ IF (NOT EXISTS (SELECT 1 FROM OD_OPERATORS WHERE USER_ID = '%[1]s')) THEN INSERT INTO OD_OPERATORS (ID, USER_ID, NAME, U_FACE, PLAN_ID, USER_DOL, IS_ROBOT) SELECT FIRST 1 GEN_ID(OD_OPERATORS_ID_GEN, 1), '%[1]s', 'Bridge&Join (переводы M2M)', O.U_FACE, O.PLAN_ID, 'Робот M2M', 1 FROM OD_OPERATORS O WHERE O.U_FACE IN (SELECT S.WHOS FROM OD_SETS S) ORDER BY CASE WHEN O.USER_ID = 'SYSDBA' THEN 0 ELSE 1 END, O.ID; /* кодировщик M2M — коды участников сервиса из справочника НРД (событие HANDBOOK) */ IF (NOT EXISTS (SELECT 1 FROM OD_FACES WHERE LAST_FLAG = 1 AND MNEM = 'M2M')) THEN INSERT INTO OD_FACES (ID, S_TYPE, NAME, MNEM) VALUES (GEN_ID(OD_FACES_ID_GEN, 1), 1, 'Кодировщик M2M (справочник участников НРД)', 'M2M'); V = NULL; SELECT FIRST 1 U_FACE FROM OD_OPERATORS WHERE USER_ID = '%[1]s' INTO :V; UPDATE FTV_M2M_SETTINGS SET VAL = :V WHERE NAME = 'WHOS' AND VAL IS NULL; V = NULL; SELECT FIRST 1 ID FROM OD_OPERATORS WHERE USER_ID = '%[1]s' INTO :V; UPDATE FTV_M2M_SETTINGS SET VAL = :V WHERE NAME = 'OPER_USER' AND VAL IS NULL; V = NULL; SELECT FIRST 1 ID FROM OD_DOC_CATS WHERE SYS_NAME = 'D_FILER' ORDER BY ID INTO :V; UPDATE FTV_M2M_SETTINGS SET VAL = :V WHERE NAME = 'FILER_CAT' AND VAL IS NULL; V = NULL; SELECT FIRST 1 ID FROM OD_BALANCES WHERE NUM_S = '98010' ORDER BY ID INTO :V; UPDATE FTV_M2M_SETTINGS SET VAL = :V WHERE NAME = 'D_NUM' AND VAL IS NULL; V = NULL; SELECT FIRST 1 ID FROM OD_BALANCES WHERE NUM_S = '98070.04' ORDER BY ID INTO :V; UPDATE FTV_M2M_SETTINGS SET VAL = :V WHERE NAME = 'K_NUM' AND VAL IS NULL; V = NULL; SELECT FIRST 1 SELF_ID FROM OD_FACES WHERE LAST_FLAG = 1 AND (MNEM = 'ММВБ' OR INN = '7702077840') ORDER BY CASE WHEN MNEM = 'ММВБ' THEN 0 ELSE 1 END, ID INTO :V; UPDATE FTV_M2M_SETTINGS SET VAL = :V WHERE NAME = 'MARKET' AND VAL IS NULL; V = NULL; SELECT FIRST 1 NUM FROM OD_SYS_TABS WHERE CODE = 'DEPO96_DCLA' AND NAME IN ('Отчет депозитария', 'Отчёт депозитария') INTO :V; UPDATE FTV_M2M_SETTINGS SET VAL = :V WHERE NAME = 'B_TYPE' AND VAL IS NULL; V = NULL; SELECT FIRST 1 SELF_ID FROM OD_FACES WHERE LAST_FLAG = 1 AND (MNEM = 'НДЦ' OR INN = '7702165310') ORDER BY CASE WHEN MNEM = 'НДЦ' THEN 0 ELSE 1 END, ID INTO :V; UPDATE FTV_M2M_SETTINGS SET VAL = :V WHERE NAME = 'CORR_FACE' AND VAL IS NULL; V = NULL; SELECT FIRST 1 RUBRICA FROM D_FILER WHERE RUBRICA IS NOT NULL GROUP BY RUBRICA ORDER BY COUNT(*) DESC INTO :V; UPDATE FTV_M2M_SETTINGS SET VAL = COALESCE(:V, 0) WHERE NAME = 'FILER_RUBRICA' AND VAL IS NULL; END`, login) } // depoFBReadAll — право логина bj читать учёт: все таблицы Fancy, кроме таблиц обмена (на них — свои права). // Имена без кавычек: так команда работает и в диалекте 1 (isql по скрипту), где "…" — строка. func depoFBReadAll(login string) string { return fmt.Sprintf(`EXECUTE BLOCK AS DECLARE VARIABLE N VARCHAR(31); BEGIN FOR SELECT TRIM(RDB$RELATION_NAME) FROM RDB$RELATIONS WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0 AND RDB$RELATION_NAME NOT STARTING WITH 'FTV_M2M' INTO :N DO EXECUTE STATEMENT 'GRANT SELECT ON ' || N || ' TO %s'; END`, login) } var ( fbSetTermRe = regexp.MustCompile(`(?i)^SET\s+TERM\s+(\S+)$`) fbIsqlCmdRe = regexp.MustCompile(`(?i)^(COMMIT(\s+WORK)?|SET\s+\w+.*)$`) ) // fbStatements — isql-скрипт на команды для драйвера: SET TERM меняет разделитель, COMMIT и прочие SET — команды // isql (у драйвера каждая команда фиксируется сама), комментарии вне команд отбрасываются. func fbStatements(script string) []string { term := ";" var out []string var cur strings.Builder flush := func() { s := strings.TrimSpace(cur.String()) cur.Reset() if body := strings.TrimSpace(fbStripComments(s)); body != "" { if m := fbSetTermRe.FindStringSubmatch(body); m != nil { term = m[1] return } if fbIsqlCmdRe.MatchString(body) { return } out = append(out, s) } } for i := 0; i < len(script); i++ { c := script[i] switch { case c == '\'' || c == '"': j := i + 1 for j < len(script) { if script[j] == c { if j+1 < len(script) && script[j+1] == c { j += 2 continue } break } j++ } if j >= len(script) { j = len(script) - 1 } cur.WriteString(script[i : j+1]) i = j case c == '/' && i+1 < len(script) && script[i+1] == '*': j := strings.Index(script[i+2:], "*/") end := len(script) if j >= 0 { end = i + 2 + j + 2 } cur.WriteString(script[i:end]) i = end - 1 case c == '-' && i+1 < len(script) && script[i+1] == '-': j := strings.IndexByte(script[i:], '\n') if j < 0 { j = len(script) - i } cur.WriteString(script[i : i+j]) i += j - 1 case strings.HasPrefix(script[i:], term): i += len(term) - 1 flush() default: cur.WriteByte(c) } } flush() return out } // fbStripComments — текст без комментариев (чтобы узнать команду isql или пустой кусок). func fbStripComments(s string) string { var b strings.Builder for i := 0; i < len(s); i++ { switch { case s[i] == '/' && i+1 < len(s) && s[i+1] == '*': j := strings.Index(s[i+2:], "*/") if j < 0 { return b.String() } i += j + 3 case s[i] == '-' && i+1 < len(s) && s[i+1] == '-': j := strings.IndexByte(s[i:], '\n') if j < 0 { return b.String() } i += j default: b.WriteByte(s[i]) } } return b.String() } // fbExecScript — выполнить скрипт по командам; ошибка — с началом команды. func fbExecScript(ctx context.Context, db *sql.DB, what, script string) error { for _, st := range fbStatements(script) { if _, err := db.ExecContext(ctx, st); err != nil { return fmt.Errorf("%s: %s → %s", what, depoStmtHead(fbStripComments(st)), quikErrorDetail(err)) } } return nil } // fbUser — создать пользователя сервера Firebird или сменить ему пароль (повторная подготовка). Firebird 2.5 // меняет пользователей при фиксации транзакции: неудачный CREATE USER (логин уже есть) — откат, иначе ошибка // фиксации остаётся в подключении и повторяется на каждой следующей команде. func fbUser(ctx context.Context, db *sql.DB, login, pass string) error { err := fbExecTx(ctx, db, "CREATE USER "+login+" PASSWORD '"+sqlStr(pass)+"'") if err == nil { return nil } m := strings.ToLower(err.Error()) if !strings.Contains(m, "rdb$users") && !strings.Contains(m, "already") && !strings.Contains(m, "уже") { return err } return fbExecTx(ctx, db, "ALTER USER "+login+" PASSWORD '"+sqlStr(pass)+"'") } // fbExecTx — команда в своей транзакции; ошибка выполнения или фиксации — откат. func fbExecTx(ctx context.Context, db *sql.DB, q string) error { tx, err := db.BeginTx(ctx, nil) if err != nil { return err } if _, err := tx.ExecContext(ctx, q); err != nil { _ = tx.Rollback() return err } if err := tx.Commit(); err != nil { _ = tx.Rollback() return err } return nil } // depoFBUDFCheck — функции Fancy (UDF ib_Fansy) на сервере: без них учётные таблицы не читаются. func depoFBUDFCheck(ctx context.Context, db *sql.DB) error { var s sql.NullString err := db.QueryRowContext(ctx, "SELECT FIRST 1 NUM_DATE FROM OD_DOCS").Scan(&s) if err == nil || err == sql.ErrNoRows { return nil } m := err.Error() if strings.Contains(m, "is not defined") || strings.Contains(m, "entrypoint") { return fmt.Errorf("сервер Firebird не находит функции Fancy (UDF ib_Fansy): %s. Библиотека ib_Fansy должна лежать в папке UDF сервера Firebird — так же, как для работы самой Fancy", quikErrorDetail(err)) } return fmt.Errorf("чтение OD_DOCS: %s", quikErrorDetail(err)) } // depoFBSettings — привязки FTV_M2M_SETTINGS после установки: «ИМЯ=значение» и список незаполненных. func depoFBSettings(ctx context.Context, db *sql.DB) (set, empty []string) { rows, err := db.QueryContext(ctx, "SELECT NAME, VAL FROM FTV_M2M_SETTINGS ORDER BY NAME") if err != nil { return nil, nil } defer rows.Close() for rows.Next() { var n string var v sql.NullInt64 if rows.Scan(&n, &v) != nil { continue } n = strings.TrimSpace(n) if v.Valid { set = append(set, fmt.Sprintf("%s=%d", n, v.Int64)) } else { empty = append(empty, n) } } return set, empty } // depoFBSetup — «bj сделает сам» для Firebird логином администратора adm: проверка базы, логин bj, таблицы // обмена, оператор и привязки Fancy, процедуры, права. Возвращает итог привязок (что не определилось — warn). func depoFBSetup(ctx context.Context, adm FancyMSSQLSettings, db, login, pass string, step depoStep) error { if !fbLoginRe.MatchString(login) { return fmt.Errorf("логин bj для Firebird — латинские буквы, цифры и _, начинается с SYSDBA (например, %s): Fancy даёт права на документы только логинам SYSDBA…", depoFBDefaultLogin) } step("connect", "run", "Подключаюсь к базе "+db+" на "+adm.Server+" логином администратора «"+adm.User+"»") conn, err := fancyOpen(adm, db) if err != nil { return err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 5*time.Minute) defer cancel() if err := conn.PingContext(cctx); err != nil { return fmt.Errorf("к базе %s на %s логином %s подключиться не удалось — %s", db, adm.Server, adm.User, quikErrorDetail(err)) } step("connect", "ok", "") step("isdepo", "run", "Проверяю, что это база учёта DEPO и функции Fancy на сервере доступны") if err := depoCheckIsDepo(cctx, adm, db); err != nil { return err } if err := depoFBUDFCheck(cctx, conn); err != nil { return err } step("isdepo", "ok", "") step("apply", "run", "Создаю логин bj «"+login+"», таблицы обмена FTV_M2M* и оператора Fancy для bj") if err := fbUser(cctx, conn, login, pass); err != nil { return fmt.Errorf("логин %s: %s (создавать пользователей сервера Firebird может только SYSDBA)", login, quikErrorDetail(err)) } if err := fbExecScript(cctx, conn, "таблицы обмена", depoFBScript("00_FTV_M2M_TABLES.sql")); err != nil { return err } if _, err := conn.ExecContext(cctx, depoFBBindings(login)); err != nil { return fmt.Errorf("оператор и привязки Fancy: %s", quikErrorDetail(err)) } step("apply", "ok", "") step("procs", "run", "Устанавливаю процедуры обработки и выдаю права логину bj") if err := fbExecScript(cctx, conn, "процедуры", depoFBScript("10_FTV_M2M_PROCS.sql")); err != nil { return err } if err := fbExecScript(cctx, conn, "права", depoFBGrants(login)); err != nil { return err } if _, err := conn.ExecContext(cctx, depoFBReadAll(login)); err != nil { return fmt.Errorf("право чтения учёта логину %s: %s", login, quikErrorDetail(err)) } step("procs", "ok", "") set, empty := depoFBSettings(cctx, conn) if len(empty) > 0 { step("bindings", "warn", "Не определены по справочникам Fancy: "+strings.Join(empty, ", ")+ " — впишите значения в таблицу FTV_M2M_SETTINGS (UPDATE FTV_M2M_SETTINGS SET VAL = … WHERE NAME = '…'), иначе обработка остановится с этой ошибкой. Определены: "+strings.Join(set, ", ")) } else { step("bindings", "ok", "Привязки к справочникам Fancy: "+strings.Join(set, ", ")) } return nil } // win1251 — текст скрипта для isql -ch WIN1251 (знаки вне WIN1251 — заменяются). func win1251(s string) []byte { b, err := encoding.ReplaceUnsupported(charmap.Windows1251.NewEncoder()).Bytes([]byte(s)) if err != nil { return []byte(s) } return b } // depoSetupScriptFB — скрипт для администратора (isql, файл в WIN1251): то же, что depoFBSetup. pass="" — без // создания логина (обновление процедур: логин bj уже есть). func depoSetupScriptFB(f FancyMSSQLSettings, db, login, pass string) string { var b strings.Builder if pass == "" { fmt.Fprintf(&b, `/* Bridge&Join: таблицы обмена, привязки, процедуры обработки и права логину bj %[3]s (Fancy на Firebird). Запуск под SYSDBA утилитой isql с кодировкой WIN1251: isql -ch WIN1251 -user SYSDBA -password <пароль> -i bj-depo-procs-firebird.sql %[1]s:%[2]s Можно запускать повторно: недостающее добавляется, процедуры заменяются, заполненные привязки не меняются. */ `, f.Server, db, login) depoFBScriptBody(&b, login) return b.String() } fmt.Fprintf(&b, `/* Bridge&Join: подготовка базы учёта DEPO (Fancy на Firebird) для bj. Запуск под SYSDBA утилитой isql с кодировкой WIN1251: isql -ch WIN1251 -user SYSDBA -password <пароль> -i bj-depo-setup-firebird.sql %[1]s:%[2]s Создаёт логин bj %[3]s (пользователь сервера Firebird: только выданные ниже права; Fancy даёт логинам SYSDBA… права на документы, а по оператору Fancy — депозитарий и нумерацию), таблицы обмена FTV_M2M*, оператора Fancy для bj, кодировщик M2M и привязки к справочникам Fancy (FTV_M2M_SETTINGS — только пустые), процедуры обработки и права. Учётные данные не меняет. Можно запускать повторно. Если логин %[3]s на сервере уже есть, CREATE USER выдаст ошибку — это нормально, следующая строка ставит пароль. В скрипте пароль логина bj — после выполнения файл удалите. */ CREATE USER %[3]s PASSWORD '%[4]s'; ALTER USER %[3]s PASSWORD '%[4]s'; COMMIT; `, f.Server, db, login, sqlStr(pass)) depoFBScriptBody(&b, login) return b.String() } // depoFBScriptBody — таблицы, привязки, процедуры и права в скрипт isql. func depoFBScriptBody(b *strings.Builder, login string) { b.WriteString(depoFBScript("00_FTV_M2M_TABLES.sql")) b.WriteString("\n/* Оператор Fancy для bj, кодировщик M2M, привязки к справочникам */\nSET TERM ^ ;\n") b.WriteString(depoFBBindings(login)) b.WriteString("^\nSET TERM ; ^\nCOMMIT;\n\n") b.WriteString(depoFBScript("10_FTV_M2M_PROCS.sql")) b.WriteString("\n") b.WriteString(depoFBGrants(login)) b.WriteString("\n/* Чтение учёта логином bj */\nSET TERM ^ ;\n") b.WriteString(depoFBReadAll(login)) b.WriteString("^\nSET TERM ; ^\nCOMMIT;\n\n/* Привязки: пустые значения нужно вписать вручную */\nSELECT NAME, VAL FROM FTV_M2M_SETTINGS;\n") } // depoFBHash — отпечаток установки для Firebird (плашка «процедуры DEPO обновились»). func depoFBHash() []byte { var b strings.Builder for _, n := range depoFBScriptFiles { b.WriteString(n + "\n" + depoFBScript(n) + "\n") } b.WriteString(depoFBBindings("") + depoFBReadAll("")) return []byte(b.String()) } // depoProcsPreflightFB — «Проверить возможности» для Firebird: подключение администратора, база DEPO, функции // Fancy, какие процедуры уже есть. func depoProcsPreflightFB(ctx context.Context, f FancyMSSQLSettings, adminUser, adminPass string) (string, bool, error) { adm := f if u := strings.TrimSpace(adminUser); u != "" { adm.User, adm.Password = u, adminPass } db, err := fancyOpen(adm, 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(), adm.User, quikErrorDetail(err)) } if err := depoCheckIsDepo(ctx, adm, f.DepoDB()); err != nil { return "", false, err } if err := depoFBUDFCheck(ctx, db); err != nil { return "", false, err } missing := depoMissing(ctx, db, depoBj2depoProcs, "P") var b strings.Builder fmt.Fprintf(&b, "Сервер %s, база %s, логин %s.\n", f.Server, f.DepoDB(), adm.User) if len(missing) == 0 { b.WriteString("Процедуры обработки: все на месте (будут заменены CREATE OR ALTER).\n") } else { fmt.Fprintf(&b, "Процедуры обработки: нет %s.\n", strings.Join(missing, ", ")) } canApply := strings.EqualFold(adm.User, "SYSDBA") if !canApply { b.WriteString("Нужен логин SYSDBA: процедуры и права в базе Fancy ставит администратор сервера Firebird.\n") } fmt.Fprintf(&b, "Будет выполнено: таблицы обмена (недостающее), привязки к справочникам (только пустые), процедуры обработки, права логину %s.", depoSetupLogin(f)) return b.String(), canApply, nil } // depoProcsInstallFB — «Установить» для Firebird: та же подготовка, что в мастере, с прежним логином и паролем bj. func depoProcsInstallFB(ctx context.Context, f FancyMSSQLSettings, adminUser, adminPass string) (string, error) { adm := f if u := strings.TrimSpace(adminUser); u != "" { adm.User, adm.Password = u, adminPass } if !strings.EqualFold(adm.User, "SYSDBA") { return "", fmt.Errorf("введите логин и пароль SYSDBA: процедуры и права в базе Fancy ставит администратор сервера Firebird") } login := depoSetupLogin(f) if strings.TrimSpace(f.Password) == "" { return "", fmt.Errorf("у bj нет пароля логина %s — подготовьте базу в мастере (шаг «Учёт DEPO»)", login) } var notes []string step := func(key, state, text string) { if state == "warn" { notes = append(notes, text) } } if err := depoFBSetup(ctx, adm, f.DepoDB(), login, f.Password, step); err != nil { return "", err } msg := "таблицы обмена, привязки, процедуры обработки и права логину " + login + " установлены" if len(notes) > 0 { msg += ". " + strings.Join(notes, " ") } return msg, nil }