diff --git a/deploy/depo/fancy-m2m-firebird/00_FTV_M2M_TABLES.sql b/deploy/depo/fancy-m2m-firebird/00_FTV_M2M_TABLES.sql index 7f47f1c..92e5377 100644 --- a/deploy/depo/fancy-m2m-firebird/00_FTV_M2M_TABLES.sql +++ b/deploy/depo/fancy-m2m-firebird/00_FTV_M2M_TABLES.sql @@ -3,7 +3,7 @@ - ID — генератор + триггер (в 2.5 нет IDENTITY); - GUID — CHAR(36) в верхнем регистре (как отдаёт MSSQL uniqueidentifier); - COMMENT / M2M_REJECT_CODE varchar(MAX) — BLOB SUB_TYPE TEXT; - - ACTIVE bit — SMALLINT; + - ACTIVE bit — SMALLINT; AMOUNT numeric(25,7) — DOUBLE PRECISION, как количества в Fancy; - FTV_M2M_SETTINGS — привязки к справочникам клиента (в MSSQL-версии они зашиты в тексты процедур). Скрипт можно выполнять повторно: существующие объекты не пересоздаются (кроме триггеров и исключения). */ @@ -39,7 +39,7 @@ BEGIN TIMEEND TIMESTAMP, ISIN VARCHAR(12), SEC_CODE VARCHAR(255), - AMOUNT NUMERIC(18,7), + AMOUNT DOUBLE PRECISION, PLACE_NUMBER VARCHAR(128), LINK_NUMBER VARCHAR(64), DEPO_NUMBER VARCHAR(128), diff --git a/deploy/depo/fancy-m2m-firebird/10_FTV_M2M_PROCS.sql b/deploy/depo/fancy-m2m-firebird/10_FTV_M2M_PROCS.sql index d56b4c4..87790a0 100644 --- a/deploy/depo/fancy-m2m-firebird/10_FTV_M2M_PROCS.sql +++ b/deploy/depo/fancy-m2m-firebird/10_FTV_M2M_PROCS.sql @@ -12,7 +12,9 @@ - в цикле по строкам поручения бумага/счета сбрасываются перед поиском (в MSSQL ненайденное значение оставалось от предыдущей строки). Тексты работают в обоих диалектах: база Fancy — диалект 1 (DATE хранит и время, "…" — строка, а не имя), - поэтому COMMENT без кавычек, а дата без времени — через FTV_M2M_DAY. Проверено 27.10.2026 на обезличенной + поэтому COMMENT без кавычек, а дата без времени — через FTV_M2M_DAY; количества — DOUBLE PRECISION, как в + Fancy (NUMERIC(18,7) в диалекте 1 и так DOUBLE, а bj подключается в диалекте 3 и создать в базе диалекта 1 + процедуру с NUMERIC точнее 9 знаков не может). Проверено 27.10.2026 на обезличенной базе Fancy (Firebird 2.5, диалект 1, кодировка NONE) с UDF ib_Fansy и штатными триггерами. Порядок создания: вспомогательные, WIRING, OPERATION, UPDATE_HANDBOOK, EVENTS, CREATE, JOB. */ @@ -63,7 +65,7 @@ DECLARE VARIABLE D_CAT INTEGER; DECLARE VARIABLE SHARE INTEGER; DECLARE VARIABLE PLACE INTEGER; DECLARE VARIABLE DEPO INTEGER; -DECLARE VARIABLE AMOUNT NUMERIC(18,7); +DECLARE VARIABLE AMOUNT DOUBLE PRECISION; DECLARE VARIABLE CMT VARCHAR(255); DECLARE VARIABLE B_TYPE INTEGER; DECLARE VARIABLE B_NUM VARCHAR(255); @@ -165,7 +167,7 @@ DECLARE VARIABLE DIRECTION VARCHAR(64); DECLARE VARIABLE DD_DATE TIMESTAMP; DECLARE VARIABLE DD_NUM VARCHAR(64); DECLARE VARIABLE SHARE INTEGER; -DECLARE VARIABLE AMOUNT NUMERIC(18,7); +DECLARE VARIABLE AMOUNT DOUBLE PRECISION; DECLARE VARIABLE CLIENT INTEGER; DECLARE VARIABLE FACE INTEGER; DECLARE VARIABLE PLACE INTEGER; @@ -374,7 +376,7 @@ DECLARE VARIABLE PARAMETERS VARCHAR(4000); DECLARE VARIABLE DIRECTION VARCHAR(64); DECLARE VARIABLE CMT BLOB SUB_TYPE TEXT; DECLARE VARIABLE SHARE INTEGER; -DECLARE VARIABLE AMOUNT NUMERIC(18,7); +DECLARE VARIABLE AMOUNT DOUBLE PRECISION; DECLARE VARIABLE PLACE INTEGER; DECLARE VARIABLE CLIENT INTEGER; DECLARE VARIABLE FACE INTEGER; @@ -474,7 +476,7 @@ DECLARE VARIABLE DEPONENT_DOC VARCHAR(255); DECLARE VARIABLE REFERENCE VARCHAR(36); DECLARE VARIABLE ISIN VARCHAR(12); DECLARE VARIABLE SEC_CODE VARCHAR(255); -DECLARE VARIABLE AMOUNT NUMERIC(18,7); +DECLARE VARIABLE AMOUNT DOUBLE PRECISION; DECLARE VARIABLE PLACE_NUMBER VARCHAR(255); DECLARE VARIABLE LINK_NUMBER VARCHAR(128); DECLARE VARIABLE DEPO_NUMBER VARCHAR(128); diff --git a/deploy/depo/fancy-m2m-firebird/README.md b/deploy/depo/fancy-m2m-firebird/README.md index da19bfa..8422687 100644 --- a/deploy/depo/fancy-m2m-firebird/README.md +++ b/deploy/depo/fancy-m2m-firebird/README.md @@ -10,6 +10,24 @@ | `20_FTV_M2M_GRANTS.sql` | права процедурам на таблицы Fancy и логину bj (`:BJ_USER`) на `FTV_M2M_JOB` и таблицы обмена | | `test/` | заглушки таблиц Fancy (по схеме MSSQL), прогон всех веток на встроенном Firebird 2.5.9 | +## Установка из bj + +Мастер настройки bj, шаг «Учёт DEPO» → СУБД «Firebird» (27.10.2026): адрес сервера, порт 3050, путь к файлу базы +на сервере (как в подключении Fancy) или псевдоним. «Дать логин администратора» — SYSDBA, разово, не сохраняется: +bj проверит базу и функции Fancy (UDF), создаст логин bj `SYSDBA_BJ`, таблицы обмена, оператора Fancy для bj, +кодировщик M2M, определит привязки `FTV_M2M_SETTINGS`, поставит процедуры и права и проверит обработку +(`BJ_PING`). «Получить скрипт» — то же одним файлом для isql (WIN1251). Встроенные в bj копии этих файлов — +`internal/lkgateway/depoprocs/firebird` (тест сверяет их с этим каталогом). + +- Логин bj — `SYSDBA_BJ`: Fancy даёт права на документы только логинам `SYSDBA%` (`FU_HAVE_RIGHT`); для сервера + Firebird это обычный пользователь — у него только выданные права: чтение учёта, таблицы обмена, `FTV_M2M_JOB`. + Он же — оператор Fancy (`OD_OPERATORS`, «робот», `U_FACE` = депозитарий): по оператору Fancy нумерует документы. +- Обработку запускает bj (SQL Server Agent нет). +- Запросы bj к DEPO (T-SQL) подключение к Firebird переводит само: `internal/lkgateway/fancyfb.go`, точные + переводы — `fsql/fb.go`. Проверка — `go test -run FBLive` с `BJ_FB_TEST_DSN` на копии базы клиента. + +## Установка вручную + Порядок установки: 00 → заполнить `FTV_M2M_SETTINGS` → 10 → 20 (под SYSDBA или владельцем базы). Файлы — в UTF-8, а база Fancy обычно в кодировке NONE с данными в WIN1251. Ставить в WIN1251, иначе русские @@ -35,7 +53,13 @@ isql -ch WIN1251 -user SYSDBA -i 10.sql <сервер>:<путь к базе> `D_FILER`; состояние `WAIT_RESP_DECIS`; кодировщик НДЦ; таблицы `D_CLISER_M2M*`, `U_OP_INV_ZACH/SPIS`. Исправлено по итогам: генераторы `OD_DOCS_ID_GEN`/`OD_DOLS_ID_GEN`; `COMMENT` без кавычек (в диалекте 1 `"…"` — -строка); дата без времени — через `FTV_M2M_DAY` (в диалекте 1 `CAST(… AS DATE)` время не отрезает). +строка); дата без времени — через `FTV_M2M_DAY` (в диалекте 1 `CAST(… AS DATE)` время не отрезает); количества — +`DOUBLE PRECISION`, как в Fancy (bj подключается в диалекте 3, а создать в базе диалекта 1 процедуру с `NUMERIC` +точнее 9 знаков из диалекта 3 нельзя). + +Через bj (отдельный экземпляр на копии базы): мастер с нуля — все проверки пройдены; тестовый режим DEPO — К1 +Бубликов, зачисление и списание SBER: поручение, события до `BJ_EXECUTED`, операции и проводки — без ошибок; +очередь обмена, справочник НРД (171 строка) — работают. Справочник участников — на настоящем справочнике НРД (169 участников, 2 места расчётов), записанном так же, как bj пишет его в MSSQL (полная перезапись `FTV_M2M_HANDBOOK` + событие `HANDBOOK`): первый прогон — 168 юрлиц с @@ -51,16 +75,20 @@ bj пишет его в MSSQL (полная перезапись `FTV_M2M_HANDBO поля и триггеры Fancy). На сервере Fancy она есть всегда. Тестовый стенд без Windows — Firebird 2.5 для Windows под wine с `ib_Fansy.dll` той же разрядности. 2. **Кодировщик M2M** — юрлицо `OD_FACES` с `MNEM = 'M2M'`. В проверенной базе его не было; без него HANDBOOK и - создание поручений падают с понятной ошибкой. Завести в Fancy или так (остальные поля заполнят триггеры Fancy): + создание поручений падают с понятной ошибкой. Установка из bj заводит его сама; вручную (остальные поля + заполнят триггеры Fancy): `INSERT INTO OD_FACES (ID, S_TYPE, NAME, MNEM) VALUES (GEN_ID(OD_FACES_ID_GEN, 1), 1, 'Кодировщик M2M (справочник участников НРД)', 'M2M');` -3. **Оператор**: логин, под которым bj вызывает `FTV_M2M_JOB`, должен быть в `OD_OPERATORS` (`USER_ID` = логин, - `U_FACE` = `OD_SETS.WHOS`). `FU_NEXT_DOC_NUM` берёт депозитарий и префикс номера по текущему пользователю; - триггеры Fancy проверяют права оператора (кроме `UPDATE … ID = 0`). Проверено под SYSDBA. +3. **Оператор**: логин, под которым bj вызывает `FTV_M2M_JOB`, должен начинаться с `SYSDBA` (иначе триггер + `OD_DOCS_BI`: «Создание документа запрещено для данного пользователя») и быть в `OD_OPERATORS` (`USER_ID` = логин, + `U_FACE` = `OD_SETS.WHOS`): `FU_NEXT_DOC_NUM` берёт депозитарий и префикс номера по текущему пользователю. + Установка из bj заводит `SYSDBA_BJ` оператором-роботом сама. 4. **Привязки `FTV_M2M_SETTINGS`** — ID из справочников клиента: `WHOS` (депозитарий), `OPER_USER`, `FILER_CAT` (категория `D_FILER`), `D_NUM`/`K_NUM` (балансовые счета 98010 и 98070), `MARKET` (Мосбиржа), `B_TYPE` («Отчёт депозитария» в `DEPO96_DCLA`), `CORR_FACE` (НКО АО НРД), `FILER_RUBRICA`. В проверенной базе счета, категория, биржа и тип основания совпали с Фонтвьель (313, 836, 255, 8106, 31) — справочники поставки Fancy; - `WHOS` и `CORR_FACE` — свои (`OD_SETS.WHOS`; НРД — юрлицо с `MNEM = 'НДЦ'`). + `WHOS` и `CORR_FACE` — свои (`OD_SETS.WHOS`; НРД — юрлицо с `MNEM = 'НДЦ'`). Установка из bj определяет все + привязки сама (оператор bj, `D_FILER`, счета `98010`/`98070.04`, мнемоники `ММВБ` и `НДЦ`, «Отчет депозитария», + частая рубрика `D_FILER`); неопределённые показывает в итоге установки. 5. **Кодировка и диалект**: `RDB$DATABASE.RDB$CHARACTER_SET_NAME`, `MON$DATABASE.MON$SQL_DIALECT`. При NONE или WIN1251 — ставить в WIN1251 (см. выше); текст ошибки в `FTV_M2M_JOB` приводится к WIN1251. 6. **Числа состояний** в `PROCESS_EVENTS` (`STATE = 1, 2, 3, 8`) — как у Фонтвьель; в проверенной базе поручения diff --git a/go.mod b/go.mod index 9881920..b06c224 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,19 @@ go 1.25.7 require ( github.com/jackc/pgx/v5 v5.7.4 github.com/microsoft/go-mssqldb v1.10.0 + github.com/nakagami/firebirdsql v0.9.23 golang.org/x/crypto v0.54.0 golang.org/x/text v0.40.0 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 ) +require ( + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect + github.com/nakagami/chacha20 v0.1.0 // indirect + gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect +) + require ( github.com/Azure/go-ntlmssp v0.1.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect diff --git a/go.sum b/go.sum index 3499083..adda5ad 100644 --- a/go.sum +++ b/go.sum @@ -61,10 +61,16 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/microsoft/go-mssqldb v1.10.0 h1:pHEt+Qz6YFPWqREq10mqSE524QQo+/QremwTCQht7TY= github.com/microsoft/go-mssqldb v1.10.0/go.mod h1:mnG7lGa9iYJbzJqGCXyuQCegStKMr3kogDLD6+bmggg= +github.com/nakagami/chacha20 v0.1.0 h1:2fbf5KeVUw7oRpAe6/A7DqvBJLYYu0ka5WstFbnkEVo= +github.com/nakagami/chacha20 v0.1.0/go.mod h1:xpoujepNFA7MvYLvX5xKHzlOHimDrLI9Ll8zfOJ0l2E= +github.com/nakagami/firebirdsql v0.9.23 h1:2MPuhkrIqQzso1PrQCcXhkVmItc9nZJpMNrw1/SThmQ= +github.com/nakagami/firebirdsql v0.9.23/go.mod h1:HsxjwNJ2Xr4MIppGCJheXkvaOFcvsj3UIYaveiQb6rQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -76,6 +82,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= diff --git a/internal/lkgateway/depo_procs.go b/internal/lkgateway/depo_procs.go index 0f24db3..aafa115 100644 --- a/internal/lkgateway/depo_procs.go +++ b/internal/lkgateway/depo_procs.go @@ -126,6 +126,10 @@ var depoSchemaFixes = []struct{ Name, SQL string }{ // depoProcsHash — отпечаток набора процедур, который bj установит (с учётом правки под клиента). func depoProcsHash(f FancyMSSQLSettings) string { h := sha256.New() + if f.IsFirebird() { // таблицы, привязки, процедуры и права Firebird (depo_setup_fb.go) + h.Write(depoFBHash()) + return hex.EncodeToString(h.Sum(nil))[:16] + } for _, fx := range depoSchemaFixes { h.Write([]byte(fx.Name + "\n" + fx.SQL + "\n")) } @@ -154,6 +158,9 @@ func depoProcList() []struct{ Name, File string } { return depoProcInstallOrder // depoProcsCombined — единый скрипт установки (для скачивания администратору): USE, процедуры, GRANT. func depoProcsCombined(f FancyMSSQLSettings) string { + if f.IsFirebird() { + return depoSetupScriptFB(f, f.DepoDB(), depoSetupLogin(f), "") + } return depoProcsScriptFor(f, f.DepoDB(), depoSetupLogin(f)) } @@ -233,7 +240,10 @@ func quoteIdent(s string) string { return "[" + strings.ReplaceAll(s, "]", "]]") // для экрана, признак «можно применять» и ошибку подключения/проверки. 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") + return "", false, fmt.Errorf("установка процедур bj2depo поддержана только для MSSQL и Firebird") + } + if f.IsFirebird() { + return depoProcsPreflightFB(ctx, f, adminUser, adminPass) } conn := f admin := strings.TrimSpace(adminUser) @@ -314,6 +324,9 @@ func depoProcErrHint(msg string) string { // depoProcsInstall — установить процедуры логином администратора. Сначала preflight; при отсутствии // прав — отказ без изменений. Возвращает итоговое сообщение. func depoProcsInstall(ctx context.Context, f FancyMSSQLSettings, adminUser, adminPass string) (string, error) { + if f.IsFirebird() { + return depoProcsInstallFB(ctx, f, adminUser, adminPass) + } report, canApply, err := depoProcsPreflight(ctx, f, adminUser, adminPass) if err != nil { return "", err @@ -401,6 +414,12 @@ func (h *setupHandlers) installFancyDepoProcs(w http.ResponseWriter, r *http.Req // fancyDepoProcsScript — GET /admin/setup/fancy/depo-procs/script.sql: единый скрипт для администратора. func (h *setupHandlers) fancyDepoProcsScript(w http.ResponseWriter, r *http.Request) { s := h.rc.Snapshot().Fancy + if s.IsFirebird() { // isql -ch WIN1251 + w.Header().Set("Content-Type", "application/sql; charset=windows-1251") + w.Header().Set("Content-Disposition", `attachment; filename="bj-depo-procs-firebird.sql"`) + _, _ = w.Write(win1251(depoProcsCombined(s))) + return + } 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. diff --git a/internal/lkgateway/depo_setup.go b/internal/lkgateway/depo_setup.go index b113706..7aad582 100644 --- a/internal/lkgateway/depo_setup.go +++ b/internal/lkgateway/depo_setup.go @@ -20,8 +20,14 @@ import ( "time" ) -// depoSetupLogin — логин bj в DEPO (если в настройках уже есть допустимый — он). +// depoSetupLogin — логин bj в DEPO (если в настройках уже есть допустимый — он). Firebird — SYSDBA_BJ. func depoSetupLogin(f FancyMSSQLSettings) string { + if f.IsFirebird() { + if u := strings.TrimSpace(f.User); fbLoginRe.MatchString(u) { + return strings.ToUpper(u) + } + return depoFBDefaultLogin + } if u := strings.TrimSpace(f.User); u != "" && depoLoginRe.MatchString(u) { return u } @@ -56,6 +62,9 @@ func depoSetupScript(f FancyMSSQLSettings, db, login, pass string, agentJob bool if f.IsPostgres() { return depoSetupScriptPG(f, db, login, pass) } + if f.IsFirebird() { + return depoSetupScriptFB(f, db, login, pass) + } var b strings.Builder job := "вызывает сам bj" if agentJob { @@ -217,6 +226,9 @@ func depoSetupFileName(f FancyMSSQLSettings) string { if f.IsPostgres() { return "bj-depo-setup-postgres.sql" } + if f.IsFirebird() { + return "bj-depo-setup-firebird.sql" + } return "bj-depo-setup-mssql.sql" } @@ -228,6 +240,12 @@ type depoStep func(key, state, text string) func depoSetupRun(ctx context.Context, f FancyMSSQLSettings, db, adminUser, adminPass, login, pass string, agentJob bool, step depoStep) error { adm := f adm.User, adm.Password = strings.TrimSpace(adminUser), adminPass + if f.IsFirebird() { + if !validFBPath(db) { + return fmt.Errorf("укажите путь к базе DEPO на сервере Firebird") + } + return depoFBSetup(ctx, adm, db, login, pass, step) + } if !validDBName(db) { return fmt.Errorf("укажите имя базы DEPO") } diff --git a/internal/lkgateway/depo_setup_fb.go b/internal/lkgateway/depo_setup_fb.go new file mode 100644 index 0000000..0483ba0 --- /dev/null +++ b/internal/lkgateway/depo_setup_fb.go @@ -0,0 +1,464 @@ +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 +} diff --git a/internal/lkgateway/depo_snapshot.go b/internal/lkgateway/depo_snapshot.go index e2af8c1..4fb8e46 100644 --- a/internal/lkgateway/depo_snapshot.go +++ b/internal/lkgateway/depo_snapshot.go @@ -75,6 +75,9 @@ func depoSnapshotTake(ctx context.Context, fs FancyMSSQLSettings) (*DepoSnapshot if strings.TrimSpace(fs.Server) == "" { return nil, fmt.Errorf("подключение к DEPO не настроено") } + if fs.IsFirebird() { // снимок читает каталог SQL Server (sys.objects, sys.sql_modules) + return nil, fmt.Errorf("снимок объектов DEPO снимается только с Microsoft SQL Server") + } db, err := fancyOpen(fs, fs.DepoDB()) if err != nil { return nil, err diff --git a/internal/lkgateway/depoevents.go b/internal/lkgateway/depoevents.go index ea36141..8256bab 100644 --- a/internal/lkgateway/depoevents.go +++ b/internal/lkgateway/depoevents.go @@ -158,7 +158,7 @@ func depoInPosted(ctx context.Context, rc *RuntimeConfig, guid string) (gate, po func depoHasColumn(ctx context.Context, db *sql.DB, table, col string) bool { var n int - _ = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME=@t AND COLUMN_NAME=@c", sql.Named("t", table), sql.Named("c", col)).Scan(&n) + _ = db.QueryRowContext(ctx, fsql.Cat04, sql.Named("t", table), sql.Named("c", col)).Scan(&n) return n > 0 } @@ -706,6 +706,9 @@ func depoWriteHandbookCtx(pctx context.Context, rc *RuntimeConfig) error { ctx, cancel := context.WithTimeout(pctx, 60*time.Second) defer cancel() if !depoTableExists(ctx, db, "FTV_M2M_HANDBOOK") { + if rc.Snapshot().Fancy.IsFirebird() { // у логина bj в Firebird нет права создавать таблицы — их ставит подготовка базы + return fmt.Errorf("нет таблицы FTV_M2M_HANDBOOK — подготовьте базу DEPO: мастер, шаг «Учёт DEPO»") + } if _, err := db.ExecContext(ctx, depoTableSpecByName("FTV_M2M_HANDBOOK").createSQL(false)); err != nil { return fmt.Errorf("создание FTV_M2M_HANDBOOK: %w — создайте таблицы: Настройка → Обмен Fancy → «Трансферные таблицы DEPO»", err) } @@ -827,6 +830,9 @@ func depoJobProcName(f FancyMSSQLSettings) string { if p := strings.TrimSpace(f.DepoJobProc); p != "" { return p } + if f.IsFirebird() { // схем у Firebird нет + return "FTV_M2M_JOB" + } return "dbo.FTV_M2M_JOB" } @@ -1012,7 +1018,7 @@ func depoJobRunnerCheck(ctx context.Context, rc *RuntimeConfig, adminUser, admin } defer db.Close() var n int - _ = db.QueryRowContext(ctx, "SELECT COUNT(*) FROM fn_my_permissions(@p,'OBJECT') WHERE permission_name='EXECUTE'", sql.Named("p", depoJobProcName(f))).Scan(&n) + _ = db.QueryRowContext(ctx, fsql.Cat05, sql.Named("p", depoJobProcName(f))).Scan(&n) if n == 0 { return "у логина " + f.User + " нет права EXECUTE на " + depoJobProcName(f) + " — выполните на сервере: " + strings.ReplaceAll(depoJobGrantScript(f), "\n", " "), false } @@ -1610,7 +1616,7 @@ func depoMissing(ctx context.Context, db *sql.DB, names []string, typ string) [] var out []string for _, n := range names { var ok int - if err := db.QueryRowContext(ctx, "SELECT CASE WHEN OBJECT_ID(@n, @t) IS NULL THEN 0 ELSE 1 END", sql.Named("n", "dbo."+n), sql.Named("t", typ)).Scan(&ok); err == nil && ok == 0 { + if err := db.QueryRowContext(ctx, fsql.Cat06, sql.Named("n", "dbo."+n), sql.Named("t", typ)).Scan(&ok); err == nil && ok == 0 { out = append(out, n) } } diff --git a/internal/lkgateway/depohealth.go b/internal/lkgateway/depohealth.go index 9ad565d..88baac8 100644 --- a/internal/lkgateway/depohealth.go +++ b/internal/lkgateway/depohealth.go @@ -31,7 +31,7 @@ func depoJobRunAs(f FancyMSSQLSettings) string { func depoRunAsCheck(ctx context.Context, db *sql.DB, f FancyMSSQLSettings) (bool, string) { name := depoJobRunAs(f) var id sql.NullInt64 - if err := db.QueryRowContext(ctx, "SELECT USER_ID(@n)", sql.Named("n", name)).Scan(&id); err != nil { + if err := db.QueryRowContext(ctx, fsql.Cat07, sql.Named("n", name)).Scan(&id); err != nil { return false, "не удалось проверить пользователя " + name + ": " + quikErrorDetail(err) } if !id.Valid { diff --git a/internal/lkgateway/depoprocs/firebird/00_FTV_M2M_TABLES.sql b/internal/lkgateway/depoprocs/firebird/00_FTV_M2M_TABLES.sql new file mode 100644 index 0000000..92e5377 --- /dev/null +++ b/internal/lkgateway/depoprocs/firebird/00_FTV_M2M_TABLES.sql @@ -0,0 +1,168 @@ +/* Bridge&Join — трансферные таблицы обмена M2M для Fancy DEPO на Firebird 2.5. + Аналог таблиц FTV_M2M* версии MSSQL. Отличия: + - ID — генератор + триггер (в 2.5 нет IDENTITY); + - GUID — CHAR(36) в верхнем регистре (как отдаёт MSSQL uniqueidentifier); + - COMMENT / M2M_REJECT_CODE varchar(MAX) — BLOB SUB_TYPE TEXT; + - ACTIVE bit — SMALLINT; AMOUNT numeric(25,7) — DOUBLE PRECISION, как количества в Fancy; + - FTV_M2M_SETTINGS — привязки к справочникам клиента (в MSSQL-версии они зашиты в тексты процедур). + Скрипт можно выполнять повторно: существующие объекты не пересоздаются (кроме триггеров и исключения). */ + +SET TERM ^ ; + +EXECUTE BLOCK AS +BEGIN + IF (NOT EXISTS (SELECT 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'FTV_M2M')) THEN + EXECUTE STATEMENT 'CREATE TABLE FTV_M2M ( + ID INTEGER NOT NULL PRIMARY KEY, + GUID CHAR(36) NOT NULL, + STATE VARCHAR(64) DEFAULT ''TODO'' NOT NULL, + TIMENEW TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + TIMEBEG TIMESTAMP, + TIMEEND TIMESTAMP, + COMMENT BLOB SUB_TYPE TEXT, + DIRECTION VARCHAR(4) NOT NULL, + CONTRAGENT_M2M_CODE VARCHAR(255) NOT NULL, + DEPONENT_NAME VARCHAR(255) NOT NULL, + DEPONENT_DOC VARCHAR(255) NOT NULL, + DOC INTEGER, + CONTRAGENT INTEGER, + DEPONENT INTEGER)'; + + IF (NOT EXISTS (SELECT 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'FTV_M2M_LINES')) THEN + EXECUTE STATEMENT 'CREATE TABLE FTV_M2M_LINES ( + ID INTEGER NOT NULL PRIMARY KEY, + GUID CHAR(36) NOT NULL, + REFERENCE VARCHAR(36), + STATE VARCHAR(64) DEFAULT ''TODO'' NOT NULL, + TIMENEW TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + TIMEBEG TIMESTAMP, + TIMEEND TIMESTAMP, + ISIN VARCHAR(12), + SEC_CODE VARCHAR(255), + AMOUNT DOUBLE PRECISION, + PLACE_NUMBER VARCHAR(128), + LINK_NUMBER VARCHAR(64), + DEPO_NUMBER VARCHAR(128), + DOC INTEGER, + DOL INTEGER, + SHARE INTEGER, + PLACE INTEGER, + DEPO INTEGER, + COMMENT BLOB SUB_TYPE TEXT)'; + + IF (NOT EXISTS (SELECT 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'FTV_M2M_EVENTS')) THEN + EXECUTE STATEMENT 'CREATE TABLE FTV_M2M_EVENTS ( + ID INTEGER NOT NULL PRIMARY KEY, + GUID CHAR(36), + REFERENCE VARCHAR(36), + STATE VARCHAR(64) DEFAULT ''TODO'' NOT NULL, + TIMENEW TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + TIMEBEG TIMESTAMP, + TIMEEND TIMESTAMP, + COMMENT BLOB SUB_TYPE TEXT, + EVENT_TYPE VARCHAR(64) NOT NULL, + EVENT_DATE TIMESTAMP NOT NULL, + M2M_REJECT_CODE VARCHAR(1000), + PARAMETERS VARCHAR(4000))'; + + IF (NOT EXISTS (SELECT 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'FTV_M2M_HANDBOOK')) THEN + EXECUTE STATEMENT 'CREATE TABLE FTV_M2M_HANDBOOK ( + ID INTEGER NOT NULL PRIMARY KEY, + INN VARCHAR(12) NOT NULL, + PARTICIPANT_CODE VARCHAR(12), + KIND VARCHAR(8) NOT NULL, + NAME_FULL VARCHAR(255), + NAME_SHORT VARCHAR(128), + NAME_DISPLAY VARCHAR(255), + NAME_ENG VARCHAR(255), + HANDBOOK_AT TIMESTAMP, + UPDATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + ACTIVE SMALLINT DEFAULT 1 NOT NULL)'; + + /* Привязки к справочникам Fancy клиента: в MSSQL-версии (Фонтвьель) — числа в текстах процедур. + Заполняются при установке; пустое значение — процедура останавливается с понятной ошибкой. */ + IF (NOT EXISTS (SELECT 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'FTV_M2M_SETTINGS')) THEN + EXECUTE STATEMENT 'CREATE TABLE FTV_M2M_SETTINGS ( + NAME VARCHAR(64) NOT NULL PRIMARY KEY, + VAL INTEGER, + COMMENT VARCHAR(255))'; + + IF (NOT EXISTS (SELECT 1 FROM RDB$GENERATORS WHERE RDB$GENERATOR_NAME = 'FTV_M2M_GEN')) THEN + EXECUTE STATEMENT 'CREATE GENERATOR FTV_M2M_GEN'; + IF (NOT EXISTS (SELECT 1 FROM RDB$GENERATORS WHERE RDB$GENERATOR_NAME = 'FTV_M2M_LINES_GEN')) THEN + EXECUTE STATEMENT 'CREATE GENERATOR FTV_M2M_LINES_GEN'; + IF (NOT EXISTS (SELECT 1 FROM RDB$GENERATORS WHERE RDB$GENERATOR_NAME = 'FTV_M2M_EVENTS_GEN')) THEN + EXECUTE STATEMENT 'CREATE GENERATOR FTV_M2M_EVENTS_GEN'; + IF (NOT EXISTS (SELECT 1 FROM RDB$GENERATORS WHERE RDB$GENERATOR_NAME = 'FTV_M2M_HANDBOOK_GEN')) THEN + EXECUTE STATEMENT 'CREATE GENERATOR FTV_M2M_HANDBOOK_GEN'; + + IF (NOT EXISTS (SELECT 1 FROM RDB$INDICES WHERE RDB$INDEX_NAME = 'FTV_M2M_GUID')) THEN + EXECUTE STATEMENT 'CREATE INDEX FTV_M2M_GUID ON FTV_M2M (GUID)'; + IF (NOT EXISTS (SELECT 1 FROM RDB$INDICES WHERE RDB$INDEX_NAME = 'FTV_M2M_LINES_GUID')) THEN + EXECUTE STATEMENT 'CREATE INDEX FTV_M2M_LINES_GUID ON FTV_M2M_LINES (GUID)'; + IF (NOT EXISTS (SELECT 1 FROM RDB$INDICES WHERE RDB$INDEX_NAME = 'FTV_M2M_EVENTS_STATE')) THEN + EXECUTE STATEMENT 'CREATE INDEX FTV_M2M_EVENTS_STATE ON FTV_M2M_EVENTS (STATE)'; + IF (NOT EXISTS (SELECT 1 FROM RDB$INDICES WHERE RDB$INDEX_NAME = 'FTV_M2M_EVENTS_GUID')) THEN + EXECUTE STATEMENT 'CREATE INDEX FTV_M2M_EVENTS_GUID ON FTV_M2M_EVENTS (GUID)'; + IF (NOT EXISTS (SELECT 1 FROM RDB$INDICES WHERE RDB$INDEX_NAME = 'FTV_M2M_HANDBOOK_INN')) THEN + EXECUTE STATEMENT 'CREATE INDEX FTV_M2M_HANDBOOK_INN ON FTV_M2M_HANDBOOK (INN)'; +END^ + +/* таблицы — до триггеров: DDL блока выполняется в его транзакции */ +COMMIT^ + +CREATE OR ALTER TRIGGER FTV_M2M_BI FOR FTV_M2M ACTIVE BEFORE INSERT POSITION 0 AS +BEGIN + IF (NEW.ID IS NULL OR NEW.ID = 0) THEN NEW.ID = GEN_ID(FTV_M2M_GEN, 1); + NEW.GUID = UPPER(NEW.GUID); +END^ + +CREATE OR ALTER TRIGGER FTV_M2M_LINES_BI FOR FTV_M2M_LINES ACTIVE BEFORE INSERT POSITION 0 AS +BEGIN + IF (NEW.ID IS NULL OR NEW.ID = 0) THEN NEW.ID = GEN_ID(FTV_M2M_LINES_GEN, 1); + NEW.GUID = UPPER(NEW.GUID); +END^ + +CREATE OR ALTER TRIGGER FTV_M2M_EVENTS_BI FOR FTV_M2M_EVENTS ACTIVE BEFORE INSERT POSITION 0 AS +BEGIN + IF (NEW.ID IS NULL OR NEW.ID = 0) THEN NEW.ID = GEN_ID(FTV_M2M_EVENTS_GEN, 1); + NEW.GUID = UPPER(NEW.GUID); +END^ + +CREATE OR ALTER TRIGGER FTV_M2M_HANDBOOK_BI FOR FTV_M2M_HANDBOOK ACTIVE BEFORE INSERT POSITION 0 AS +BEGIN + IF (NEW.ID IS NULL OR NEW.ID = 0) THEN NEW.ID = GEN_ID(FTV_M2M_HANDBOOK_GEN, 1); +END^ + +SET TERM ; ^ + +/* Ошибка обработки: процедуры поднимают её с текстом причины (аналог raiserror MSSQL-версии). */ +CREATE OR ALTER EXCEPTION FTV_M2M_ERROR 'Ошибка обработки M2M'; + +/* Строки настроек без значений: ID у каждого клиента свои, в комментарии — значение у Фонтвьель (MSSQL) для + ориентира. Вставляются только отсутствующие строки — повторный запуск не затирает заполненные значения. */ +SET TERM ^ ; +EXECUTE BLOCK AS +BEGIN + IF (NOT EXISTS (SELECT 1 FROM FTV_M2M_SETTINGS WHERE NAME = 'WHOS')) THEN + INSERT INTO FTV_M2M_SETTINGS (NAME, VAL, COMMENT) VALUES ('WHOS', NULL, 'OD_DOCS.WHOS — юрлицо-депозитарий (Фонтвьель: 383118)'); + IF (NOT EXISTS (SELECT 1 FROM FTV_M2M_SETTINGS WHERE NAME = 'OPER_USER')) THEN + INSERT INTO FTV_M2M_SETTINGS (NAME, VAL, COMMENT) VALUES ('OPER_USER', NULL, 'OD_OPERATORS.ID — оператор проводок (Фонтвьель: 84, SYSDBA)'); + IF (NOT EXISTS (SELECT 1 FROM FTV_M2M_SETTINGS WHERE NAME = 'FILER_CAT')) THEN + INSERT INTO FTV_M2M_SETTINGS (NAME, VAL, COMMENT) VALUES ('FILER_CAT', NULL, 'OD_DOC_CATS.ID категории D_FILER — входящая запись (Фонтвьель: 255)'); + IF (NOT EXISTS (SELECT 1 FROM FTV_M2M_SETTINGS WHERE NAME = 'D_NUM')) THEN + INSERT INTO FTV_M2M_SETTINGS (NAME, VAL, COMMENT) VALUES ('D_NUM', NULL, 'OD_BALANCES.ID — 98010 НОСТРО депо базовый (Фонтвьель: 313)'); + IF (NOT EXISTS (SELECT 1 FROM FTV_M2M_SETTINGS WHERE NAME = 'K_NUM')) THEN + INSERT INTO FTV_M2M_SETTINGS (NAME, VAL, COMMENT) VALUES ('K_NUM', NULL, 'OD_BALANCES.ID — 98070 ЦБ на торгах (Фонтвьель: 836)'); + IF (NOT EXISTS (SELECT 1 FROM FTV_M2M_SETTINGS WHERE NAME = 'MARKET')) THEN + INSERT INTO FTV_M2M_SETTINGS (NAME, VAL, COMMENT) VALUES ('MARKET', NULL, 'OD_FACES.SELF_ID — ПАО Московская Биржа (Фонтвьель: 8106)'); + IF (NOT EXISTS (SELECT 1 FROM FTV_M2M_SETTINGS WHERE NAME = 'B_TYPE')) THEN + INSERT INTO FTV_M2M_SETTINGS (NAME, VAL, COMMENT) VALUES ('B_TYPE', NULL, 'OD_SYS_TABS DEPO96_DCLA.NUM — «Отчёт депозитария» (Фонтвьель: 31)'); + IF (NOT EXISTS (SELECT 1 FROM FTV_M2M_SETTINGS WHERE NAME = 'CORR_FACE')) THEN + INSERT INTO FTV_M2M_SETTINGS (NAME, VAL, COMMENT) VALUES ('CORR_FACE', NULL, 'OD_FACES.SELF_ID — НКО АО НРД, корреспондент входящей записи (Фонтвьель: 18174)'); + IF (NOT EXISTS (SELECT 1 FROM FTV_M2M_SETTINGS WHERE NAME = 'FILER_RUBRICA')) THEN + INSERT INTO FTV_M2M_SETTINGS (NAME, VAL, COMMENT) VALUES ('FILER_RUBRICA', NULL, 'D_FILER.RUBRICA для отчёта НРД (Фонтвьель: 5, «уточнить в депозитарии»)'); +END^ +SET TERM ; ^ + +COMMIT; diff --git a/internal/lkgateway/depoprocs/firebird/10_FTV_M2M_PROCS.sql b/internal/lkgateway/depoprocs/firebird/10_FTV_M2M_PROCS.sql new file mode 100644 index 0000000..87790a0 --- /dev/null +++ b/internal/lkgateway/depoprocs/firebird/10_FTV_M2M_PROCS.sql @@ -0,0 +1,649 @@ +/* Bridge&Join — процедуры обработки M2M для Fancy DEPO на Firebird 2.5. + Перенос процедур MSSQL-версии (Фонтвьель, тексты от 22.10.2026, OPERATION — от 24.10.2026) на PSQL. Логика та же; отличия: + - raiserror — FTV_M2M_FAIL: текст причины кладётся в контекст транзакции и поднимается исключение FTV_M2M_ERROR; + FTV_M2M_JOB пишет этот текст в COMMENT (в Firebird 2.5 текст исключения в WHEN не доступен; контекст + возвращает строку без кодировки — приводится к WIN1251, кодировке базы Fancy); + - ошибка записи откатывает только её изменения (WHEN ANY = точка сохранения), в MSSQL-версии изменения + до ошибки оставались; + - числа-привязки Фонтвьель (WHOS, счета, биржа, тип основания, НРД, оператор) — из FTV_M2M_SETTINGS; + - «execute as user = 'SYSDBA'» нет: процедуры работают правами, выданными им самим (GRANT … TO PROCEDURE, + 20_FTV_M2M_GRANTS.sql); логину bj нужно только EXECUTE на FTV_M2M_JOB; + - SELECT … INTO, который в MSSQL молча брал последнюю из нескольких строк, — SELECT FIRST 1; + - в цикле по строкам поручения бумага/счета сбрасываются перед поиском (в MSSQL ненайденное значение + оставалось от предыдущей строки). + Тексты работают в обоих диалектах: база Fancy — диалект 1 (DATE хранит и время, "…" — строка, а не имя), + поэтому COMMENT без кавычек, а дата без времени — через FTV_M2M_DAY; количества — DOUBLE PRECISION, как в + Fancy (NUMERIC(18,7) в диалекте 1 и так DOUBLE, а bj подключается в диалекте 3 и создать в базе диалекта 1 + процедуру с NUMERIC точнее 9 знаков не может). Проверено 27.10.2026 на обезличенной + базе Fancy (Firebird 2.5, диалект 1, кодировка NONE) с UDF ib_Fansy и штатными триггерами. + Порядок создания: вспомогательные, WIRING, OPERATION, UPDATE_HANDBOOK, EVENTS, CREATE, JOB. */ + +SET TERM ^ ; + +/* Ошибка обработки с текстом причины. */ +CREATE OR ALTER PROCEDURE FTV_M2M_FAIL (MSG VARCHAR(1000)) AS +DECLARE VARIABLE R INTEGER; +BEGIN + R = RDB$SET_CONTEXT('USER_TRANSACTION', 'FTV_M2M_ERR', SUBSTRING(MSG FROM 1 FOR 255)); + EXCEPTION FTV_M2M_ERROR MSG; +END^ + +/* Привязка из FTV_M2M_SETTINGS; не заполнена — ошибка с именем настройки. */ +CREATE OR ALTER PROCEDURE FTV_M2M_GET (NAME VARCHAR(64)) RETURNS (VAL INTEGER) AS +BEGIN + VAL = NULL; + SELECT S.VAL FROM FTV_M2M_SETTINGS S WHERE S.NAME = :NAME INTO :VAL; + IF (VAL IS NULL) THEN + EXECUTE PROCEDURE FTV_M2M_FAIL('Не заполнена настройка FTV_M2M_SETTINGS.' || NAME || ' (привязка к справочнику Fancy)'); +END^ + +/* «дд.мм.гггг чч:мм:сс» (FORMAT(…, 'dd.MM.yyyy HH:mm:ss') MSSQL-версии). */ +CREATE OR ALTER PROCEDURE FTV_M2M_DT (T TIMESTAMP) RETURNS (S VARCHAR(19)) AS +BEGIN + S = NULL; + IF (T IS NOT NULL) THEN + S = LPAD(EXTRACT(DAY FROM T), 2, '0') || '.' || LPAD(EXTRACT(MONTH FROM T), 2, '0') || '.' || EXTRACT(YEAR FROM T) || ' ' || + LPAD(EXTRACT(HOUR FROM T), 2, '0') || ':' || LPAD(EXTRACT(MINUTE FROM T), 2, '0') || ':' || + LPAD(CAST(FLOOR(EXTRACT(SECOND FROM T)) AS INTEGER), 2, '0'); +END^ + +/* Дата без времени (CONVERT(DATE, …) MSSQL-версии). В диалекте 1 CAST(… AS DATE) время не отрезает. */ +CREATE OR ALTER PROCEDURE FTV_M2M_DAY (T TIMESTAMP) RETURNS (D TIMESTAMP) AS +BEGIN + D = NULL; + IF (T IS NOT NULL) THEN + D = CAST(EXTRACT(YEAR FROM T) || '-' || EXTRACT(MONTH FROM T) || '-' || EXTRACT(DAY FROM T) AS TIMESTAMP); +END^ + + +/* Директива «Обработать» для документа «Зачисление» или «Списание». Значения берутся только из самого + документа; M2M-поручение и трансферные таблицы не используются. */ +CREATE OR ALTER PROCEDURE FTV_M2M_PROCESS_WIRING (OPER_ID INTEGER) AS +DECLARE VARIABLE USER_ID INTEGER; +DECLARE VARIABLE DIRECTION VARCHAR(3); +DECLARE VARIABLE D_CAT INTEGER; +DECLARE VARIABLE SHARE INTEGER; +DECLARE VARIABLE PLACE INTEGER; +DECLARE VARIABLE DEPO INTEGER; +DECLARE VARIABLE AMOUNT DOUBLE PRECISION; +DECLARE VARIABLE CMT VARCHAR(255); +DECLARE VARIABLE B_TYPE INTEGER; +DECLARE VARIABLE B_NUM VARCHAR(255); +DECLARE VARIABLE B_DATE TIMESTAMP; +DECLARE VARIABLE WIR_DATE TIMESTAMP; +DECLARE VARIABLE D_NUM INTEGER; +DECLARE VARIABLE K_NUM INTEGER; +DECLARE VARIABLE MARKET INTEGER; +DECLARE VARIABLE OP_NAME VARCHAR(255); +DECLARE VARIABLE TEXT_B_TYPE VARCHAR(255); +DECLARE VARIABLE B_DATE_S VARCHAR(19); +DECLARE VARIABLE WIR_ID INTEGER; +DECLARE VARIABLE WIR_LIST VARCHAR(255); +DECLARE VARIABLE DEB_ID INTEGER; +DECLARE VARIABLE KRD_ID INTEGER; +DECLARE VARIABLE D_REG_1 INTEGER; DECLARE VARIABLE K_REG_1 INTEGER; +DECLARE VARIABLE D_REG_2 INTEGER; DECLARE VARIABLE K_REG_2 INTEGER; +DECLARE VARIABLE D_REG_3 INTEGER; DECLARE VARIABLE K_REG_3 INTEGER; +DECLARE VARIABLE D_REG_4 INTEGER; DECLARE VARIABLE K_REG_4 INTEGER; +DECLARE VARIABLE D_REG_5 INTEGER; DECLARE VARIABLE K_REG_5 INTEGER; +DECLARE VARIABLE D_REG_6 INTEGER; DECLARE VARIABLE K_REG_6 INTEGER; +DECLARE VARIABLE DIR_ID INTEGER; +DECLARE VARIABLE OUT_STATE INTEGER; +DECLARE VARIABLE STEP_ID INTEGER; +BEGIN + EXECUTE PROCEDURE FTV_M2M_GET('OPER_USER') RETURNING_VALUES :USER_ID; + + /* НАПРАВЛЕНИЕ (MSSQL-версия при ненайденном документе молча выходила — здесь ошибка, видно в COMMENT) */ + IF (EXISTS (SELECT 1 FROM U_OP_INV_ZACH WHERE DOC = :OPER_ID)) THEN DIRECTION = 'IN'; + ELSE IF (EXISTS (SELECT 1 FROM U_OP_INV_SPIS WHERE DOC = :OPER_ID)) THEN DIRECTION = 'OUT'; + ELSE EXECUTE PROCEDURE FTV_M2M_FAIL('Не найден документ операции ID = ' || OPER_ID); + + /* ЗНАЧЕНИЯ ПЕРЕМЕННЫХ */ + IF (DIRECTION = 'IN') THEN + BEGIN + SELECT D.D_CAT, H.D_NUM, H.K_NUM, H.B_TYPE, H.B_NUM, H.B_DATE, H.SHARE, H.AMOUNT, H.PLACE, H.DEPO, H.MARKET, H.WIR_DATE + FROM OD_DOCS D LEFT JOIN U_OP_INV_ZACH H ON H.DOC = D.ID + WHERE D.ID = :OPER_ID + INTO :D_CAT, :D_NUM, :K_NUM, :B_TYPE, :B_NUM, :B_DATE, :SHARE, :AMOUNT, :PLACE, :DEPO, :MARKET, :WIR_DATE; + OP_NAME = 'Зачисление'; + SELECT C.SELF_ID, C.FACE, 0, 0, 0, :PLACE FROM OD_CLIENTS C WHERE C.ID = :PLACE + INTO :D_REG_1, :D_REG_2, :D_REG_3, :D_REG_4, :D_REG_5, :D_REG_6; + SELECT C.SELF_ID, :MARKET, 0, :D_REG_1, :PLACE, :DEPO FROM OD_CLIENTS C WHERE C.ID = :DEPO + INTO :K_REG_1, :K_REG_2, :K_REG_3, :K_REG_4, :K_REG_5, :K_REG_6; + END + ELSE + BEGIN + SELECT D.D_CAT, H.K_NUM /* реверс */, H.D_NUM /* реверс */, H.B_TYPE, H.B_NUM, H.B_DATE, H.SHARE, H.AMOUNT, H.PLACE, H.DEPO, H.MARKET, H.WIR_DATE + FROM OD_DOCS D LEFT JOIN U_OP_INV_SPIS H ON H.DOC = D.ID + WHERE D.ID = :OPER_ID + INTO :D_CAT, :D_NUM, :K_NUM, :B_TYPE, :B_NUM, :B_DATE, :SHARE, :AMOUNT, :PLACE, :DEPO, :MARKET, :WIR_DATE; + OP_NAME = 'Списание'; + SELECT C.SELF_ID, C.FACE, 0, 0, 0, :PLACE FROM OD_CLIENTS C WHERE C.ID = :PLACE + INTO :K_REG_1, :K_REG_2, :K_REG_3, :K_REG_4, :K_REG_5, :K_REG_6; + SELECT C.SELF_ID, :MARKET, 0, :K_REG_1, :PLACE, :DEPO FROM OD_CLIENTS C WHERE C.ID = :DEPO + INTO :D_REG_1, :D_REG_2, :D_REG_3, :D_REG_4, :D_REG_5, :D_REG_6; + END + + /* ДОПОЛНИТЕЛЬНЫЕ ПАРАМЕТРЫ: «<тип основания> N<номер> от <дата>» + (в MSSQL-версии FORMATMESSAGE начинал строку с пустого @COMMENT — «(null); …») */ + SELECT FIRST 1 T.NAME FROM OD_SYS_TABS T WHERE T.CODE = 'DEPO96_DCLA' AND T.NUM = :B_TYPE INTO :TEXT_B_TYPE; + EXECUTE PROCEDURE FTV_M2M_DT(:B_DATE) RETURNING_VALUES :B_DATE_S; + CMT = SUBSTRING((COALESCE(TEXT_B_TYPE, '') || ' N' || COALESCE(B_NUM, '') || ' от ' || COALESCE(SUBSTRING(B_DATE_S FROM 1 FOR 10), '')) FROM 1 FOR 255); + + /* ДИРЕКТИВА */ + SELECT FIRST 1 T.ID, T.OUT_STATE FROM OD_STEP_TYPES T WHERE T.SYS_NAME = 'PROCESS' AND T.D_CAT = :D_CAT INTO :DIR_ID, :OUT_STATE; + STEP_ID = GEN_ID(OD_STEPS_ID_GEN, 1); + INSERT INTO OD_STEPS (ID, DOC, S_TYPE, S_DATE, ERR_CODE, OUT_STATE, OPER) + VALUES (:STEP_ID, :OPER_ID, :DIR_ID, CURRENT_TIMESTAMP, 0, :OUT_STATE, :USER_ID); + + /* ПРОВОДКА */ + EXECUTE PROCEDURE OD_PR_INS_WIRS ( + NULL, 4, :WIR_DATE, :OP_NAME, :CMT, :AMOUNT, :AMOUNT, NULL, 0, 0, NULL, :STEP_ID, NULL, :USER_ID, 'F', + :D_NUM, :SHARE, :D_REG_1, :D_REG_2, :D_REG_3, :D_REG_4, :D_REG_5, :D_REG_6, /* дебет */ + :K_NUM, :SHARE, :K_REG_1, :K_REG_2, :K_REG_3, :K_REG_4, :K_REG_5, :K_REG_6) /* кредит */ + RETURNING_VALUES :WIR_ID, :WIR_LIST, :DEB_ID, :KRD_ID; + + /* СОСТОЯНИЕ ДОКУМЕНТА */ + UPDATE OD_DOCS SET STATE = :OUT_STATE WHERE ID = :OPER_ID; +END^ + + +/* Создаёт операцию «Зачисление»/«Списание» по строке M2M-поручения, входящую запись (отчёт НРД) и проводит + операцию. Возвращает OPER_ID. */ +CREATE OR ALTER PROCEDURE FTV_M2M_PROCESS_OPERATION (GUID CHAR(36), REFERENCE VARCHAR(255), OPER_DATE TIMESTAMP, PARAMETERS VARCHAR(4000)) +RETURNS (OPER_ID INTEGER) AS +DECLARE VARIABLE WHOS INTEGER; +DECLARE VARIABLE ACC_D INTEGER; +DECLARE VARIABLE ACC_K INTEGER; +DECLARE VARIABLE MARKET INTEGER; +DECLARE VARIABLE B_TYPE INTEGER; +DECLARE VARIABLE CORR_FACE INTEGER; +DECLARE VARIABLE FILER_CAT INTEGER; +DECLARE VARIABLE RUBRICA INTEGER; +DECLARE VARIABLE CLISER_ID INTEGER; +DECLARE VARIABLE CLISER_NUM VARCHAR(64); +DECLARE VARIABLE CLISER_DATE TIMESTAMP; +DECLARE VARIABLE DIRECTION VARCHAR(64); +DECLARE VARIABLE DD_DATE TIMESTAMP; +DECLARE VARIABLE DD_NUM VARCHAR(64); +DECLARE VARIABLE SHARE INTEGER; +DECLARE VARIABLE AMOUNT DOUBLE PRECISION; +DECLARE VARIABLE CLIENT INTEGER; +DECLARE VARIABLE FACE INTEGER; +DECLARE VARIABLE PLACE INTEGER; +DECLARE VARIABLE D_CAT INTEGER; +DECLARE VARIABLE NUM VARCHAR(255); +DECLARE VARIABLE NEW_DATE TIMESTAMP; +DECLARE VARIABLE FILER_ID INTEGER; +DECLARE VARIABLE FILER_NUM VARCHAR(255); +DECLARE VARIABLE FILER_DATE TIMESTAMP; +DECLARE VARIABLE STEP_ID INTEGER; +DECLARE VARIABLE P VARCHAR(4000); +DECLARE VARIABLE REP_NUM VARCHAR(255); +DECLARE VARIABLE REP_DATE TIMESTAMP; +DECLARE VARIABLE CMT VARCHAR(4000); +BEGIN + EXECUTE PROCEDURE FTV_M2M_GET('WHOS') RETURNING_VALUES :WHOS; + EXECUTE PROCEDURE FTV_M2M_GET('D_NUM') RETURNING_VALUES :ACC_D; + EXECUTE PROCEDURE FTV_M2M_GET('K_NUM') RETURNING_VALUES :ACC_K; + EXECUTE PROCEDURE FTV_M2M_GET('MARKET') RETURNING_VALUES :MARKET; + EXECUTE PROCEDURE FTV_M2M_GET('B_TYPE') RETURNING_VALUES :B_TYPE; + EXECUTE PROCEDURE FTV_M2M_GET('CORR_FACE') RETURNING_VALUES :CORR_FACE; + EXECUTE PROCEDURE FTV_M2M_GET('FILER_CAT') RETURNING_VALUES :FILER_CAT; + EXECUTE PROCEDURE FTV_M2M_GET('FILER_RUBRICA') RETURNING_VALUES :RUBRICA; + + SELECT FIRST 1 H.DOC, D.NUM, D.D_DATE, + CASE WHEN DC.SYS_NAME = 'I_CLISER_M2M_ZACH' THEN 'IN' WHEN DC.SYS_NAME = 'I_CLISER_M2M_SPIS' THEN 'OUT' END, + D.D_DATE, D.NUM, F.SHARE, F.AMOUNT, F.CLIENT, H.DEPONENT, F.PLACE + FROM D_CLISER_M2M_DOL F + LEFT JOIN OD_DOLS L ON L.ID = F.DOL + LEFT JOIN OD_DOCS D ON D.ID = L.DOC + LEFT JOIN D_CLISER_M2M H ON H.DOC = D.ID + LEFT JOIN OD_DOC_CATS DC ON DC.ID = D.D_CAT + WHERE H.DOC_GUID = :GUID AND F.DOL_REF = :REFERENCE + INTO :CLISER_ID, :CLISER_NUM, :CLISER_DATE, :DIRECTION, :DD_DATE, :DD_NUM, :SHARE, :AMOUNT, :CLIENT, :FACE, :PLACE; + + IF (CLISER_ID IS NULL) THEN + EXECUTE PROCEDURE FTV_M2M_FAIL('Не найдено M2M-поручение по GUID + REFERENCE'); + EXECUTE PROCEDURE FTV_M2M_DAY(:DD_DATE) RETURNING_VALUES :DD_DATE; + + /* Направление */ + SELECT FIRST 1 DC.ID FROM OD_DOC_CATS DC + WHERE DC.SYS_NAME = CASE WHEN :DIRECTION = 'IN' THEN 'OP_INV_ZACH' ELSE 'OP_INV_SPIS' END + INTO :D_CAT; + + /* ID и номер операции */ + OPER_ID = GEN_ID(OD_DOCS_ID_GEN, 1); + EXECUTE PROCEDURE FU_NEXT_DOC_NUM (:D_CAT, :OPER_DATE, NULL, :OPER_ID) RETURNING_VALUES :NUM, :NEW_DATE; + NUM = NUM || '_' || REFERENCE; + + /* Документ */ + INSERT INTO OD_DOCS (ID, NUM, PARAM, D_CAT, D_DATE, STATE, DIRECTION, WHOS, KVIT) + VALUES (:OPER_ID, :NUM, NULL, :D_CAT, :OPER_DATE, 0, 0, :WHOS, :CLISER_ID); + + /* Отчёт НРД из PARAMETERS — «<номер отчёта> от <дд.мм.гггг>»: номер — до первого пробела, дата — после + последнего. Основание операции (B_NUM, B_DATE) и входящая запись — по отчёту НРД, а не по M2M-поручению + (MSSQL-версия, правка программиста 24.10.2026). */ + P = TRIM(PARAMETERS); + REP_NUM = LEFT(P, POSITION(' ' IN P || ' ') - 1); + REP_DATE = CAST(RIGHT(P, POSITION(' ' IN REVERSE(P)) - 1) AS TIMESTAMP); + + IF (DIRECTION = 'IN') THEN + BEGIN + CMT = 'Перевод M2M (без поручения), GUID ' || GUID || ', референс ' || REFERENCE; + INSERT INTO U_OP_INV_ZACH + (DOC, DD_TYPE, DD_DATE, DD_NUM, SHARE, AMOUNT, BLANKS, PLACE, D_NUM, DEPO, K_NUM, MARKET, + CONDITION, IS_REG, TAX_R, B_TYPE, B_DATE, B_NUM, WIR_DATE, NOTIFY_DATE, REG_DATE, IS_BLOCK, FROM_FACE, REJECT_REASON, COMMENT) + VALUES + (:OPER_ID, 1 /* клиентское */, :DD_DATE, :DD_NUM, :SHARE, :AMOUNT, NULL, :PLACE, :ACC_D, :CLIENT, :ACC_K, :MARKET, + NULL, NULL, NULL, :B_TYPE, :REP_DATE, :REP_NUM, :OPER_DATE, :OPER_DATE, :OPER_DATE, 0, :FACE, NULL, :CMT); + END + ELSE IF (DIRECTION = 'OUT') THEN + BEGIN + CMT = 'Перевод M2M, GUID ' || GUID || ', референс ' || REFERENCE; + INSERT INTO U_OP_INV_SPIS + (DOC, DD_TYPE, DD_DATE, DD_NUM, SHARE, AMOUNT, BLANKS, PLACE, D_NUM, DEPO, K_NUM, MARKET, + OLD_CONDITIONS, IS_REG, TAX_R, B_TYPE, B_DATE, B_NUM, WIR_DATE, NOTIFY_DATE, REG_DATE, IS_BLOCK, FROM_FACE, REJECT_REASON, COMMENT) + VALUES + (:OPER_ID, 1 /* клиентское */, :DD_DATE, :DD_NUM, :SHARE, :AMOUNT, NULL, :PLACE, :ACC_D, :CLIENT, :ACC_K, :MARKET, + NULL, NULL, NULL, :B_TYPE, :REP_DATE, :REP_NUM, :OPER_DATE, :OPER_DATE, :OPER_DATE, 0, :FACE, NULL, :CMT); + END + + /* Входящая запись (отчёт НРД) */ + FILER_ID = GEN_ID(OD_DOCS_ID_GEN, 1); + FILER_DATE = CURRENT_TIMESTAMP; + EXECUTE PROCEDURE FU_NEXT_DOC_NUM ('D_FILER', :FILER_DATE, 1, :FILER_ID) RETURNING_VALUES :FILER_NUM, :NEW_DATE; + INSERT INTO OD_DOCS (ID, NUM, D_CAT, D_DATE, STATE, DIRECTION, WHOS) + VALUES (:FILER_ID, :FILER_NUM, :FILER_CAT, :FILER_DATE, 2, 1, :WHOS); + + INSERT INTO D_FILER (DOC, PASS_KIND, RUBRICA, COMMENT, DOC_NUM, DOC_DATE, CORR_FACE, FUND, ACCOUNT, PLACE, ORI_DATE, CHILD_CAT, MESS) + VALUES (:FILER_ID, 8, :RUBRICA, 'Отчет (перевод через MOEX-МОСТ)', :REP_NUM, :REP_DATE, :CORR_FACE, + :FACE, :CLIENT, :PLACE, :FILER_DATE, NULL, 'MS010 / ' || :GUID || ' / ' || :REFERENCE); + + /* Связь входящей записи с операцией */ + STEP_ID = GEN_ID(OD_STEPS_ID_GEN, 1); + INSERT INTO OD_STEPS (ID, DOC, A_DOC, ERR_CODE, S_TYPE, OUT_STATE, S_DATE, OPER) + VALUES (:STEP_ID, :FILER_ID, :OPER_ID, 2004, 0, 0, CURRENT_TIMESTAMP, NULL); + + /* И сразу проводим */ + EXECUTE PROCEDURE FTV_M2M_PROCESS_WIRING (:OPER_ID); +END^ + + +/* Обновление справочника участников сервиса: недостающие юрлица и коды M2M в Fancy, предупреждения о + двойниках по ИНН и расхождении кодов. Возвращает текст отчёта (пишется в COMMENT события HANDBOOK). */ +CREATE OR ALTER PROCEDURE FTV_M2M_UPDATE_HANDBOOK RETURNS (CMT BLOB SUB_TYPE TEXT) AS +DECLARE VARIABLE M2M_CODER INTEGER; +DECLARE VARIABLE CNT INTEGER; +DECLARE VARIABLE FACE_ID INTEGER; +DECLARE VARIABLE HB_ID INTEGER; +DECLARE VARIABLE RW INTEGER = 0; +DECLARE VARIABLE INN VARCHAR(12); +DECLARE VARIABLE CODE VARCHAR(64); +DECLARE VARIABLE CODE2 VARCHAR(64); +DECLARE VARIABLE NAME_SHORT VARCHAR(255); +DECLARE VARIABLE NAME_FULL VARCHAR(255); +DECLARE VARIABLE CRLF CHAR(2); +BEGIN + CRLF = ASCII_CHAR(13) || ASCII_CHAR(10); + SELECT FIRST 1 F.SELF_ID FROM OD_FACES F WHERE F.LAST_FLAG = 1 AND F.MNEM = 'M2M' INTO :M2M_CODER; + IF (M2M_CODER IS NULL) THEN + EXECUTE PROCEDURE FTV_M2M_FAIL('В Fancy нет кодировщика M2M (OD_FACES с MNEM = ''M2M'')'); + + /* 1. Статистика по участникам */ + SELECT COUNT(*) FROM OD_FACE_CODERS C WHERE C.CODER = :M2M_CODER INTO :CNT; + CMT = 'УЧАСТНИКИ СЕРВИСА: в справочнике юрлиц Фэнси - ' || CNT; + SELECT COUNT(*) FROM FTV_M2M_HANDBOOK WHERE ACTIVE = 1 AND PARTICIPANT_CODE IS NOT NULL INTO :CNT; + CMT = CMT || ', по данным НРД - ' || CNT; + + /* 2. Принудительно добавляем отсутствующих юрлиц */ + FOR SELECT HB.ID, HB.PARTICIPANT_CODE, HB.INN, HB.NAME_SHORT, HB.NAME_FULL + FROM FTV_M2M_HANDBOOK HB + WHERE HB.ACTIVE = 1 + AND HB.INN NOT IN (SELECT F.INN FROM OD_FACES F WHERE F.LAST_FLAG = 1 AND F.E_DATE = '9999-01-01' AND F.INN IS NOT NULL) + ORDER BY HB.NAME_SHORT + INTO :HB_ID, :CODE, :INN, :NAME_SHORT, :NAME_FULL DO + /* один ИНН бывает в справочнике дважды (СПБ Банк — место расчётов и депозитарий): второе юрлицо не создаём, + код к созданному добавит шаг 3 (в MSSQL-версии — два одинаковых юрлица) */ + IF (NOT EXISTS (SELECT 1 FROM OD_FACES F WHERE F.INN = :INN AND F.LAST_FLAG = 1 AND F.E_DATE = '9999-01-01')) THEN + BEGIN + RW = RW + 1; + FACE_ID = GEN_ID(OD_FACES_ID_GEN, 1); + INSERT INTO OD_FACES (ID, S_TYPE, INN, NAME) VALUES (:FACE_ID, 1, :INN, :NAME_SHORT); + INSERT INTO OD_U_FACES (FACE, FULL_NAME) VALUES (:FACE_ID, :NAME_FULL); + /* у мест расчётов (KIND = 'PLACE') кода участника нет, а OD_FACE_CODERS.CODE — NOT NULL: в MSSQL-версии + такой справочник падает целиком, если места расчётов ещё нет в юрлицах Fancy */ + IF (CODE IS NOT NULL) THEN + INSERT INTO OD_FACE_CODERS (CODE, CODER, FACE) VALUES (:CODE, :M2M_CODER, :FACE_ID); + CMT = CMT || CRLF || RW || '. Добавлено юрлицо: ' || COALESCE(CODE, '') || ', ' || INN || ', ' || COALESCE(NAME_SHORT, ''); + END + + /* 3. Недостающие коды участников к существующим юрлицам, с предупреждением о двойниках по ИНН + (в подзапросе ИНН без NULL: иначе NOT IN не находит ни одной строки) */ + FOR SELECT HB.ID, HB.PARTICIPANT_CODE, HB.INN, HB.NAME_SHORT, MIN(F.SELF_ID), COUNT(*) + FROM FTV_M2M_HANDBOOK HB + LEFT JOIN OD_FACES F ON F.INN = HB.INN + LEFT JOIN OD_FACE_CODERS C ON C.CODER = :M2M_CODER AND F.SELF_ID = C.FACE + WHERE HB.ACTIVE = 1 + AND HB.PARTICIPANT_CODE IS NOT NULL + AND F.S_TYPE = 1 + AND F.LAST_FLAG = 1 + AND F.E_DATE = '9999-01-01' + AND C.CODE IS NULL + AND F.INN NOT IN (SELECT F2.INN FROM OD_FACE_CODERS C2 + JOIN OD_FACES F2 ON F2.SELF_ID = C2.FACE AND F2.LAST_FLAG = 1 AND F2.E_DATE = '9999-01-01' + WHERE C2.CODER = :M2M_CODER AND F2.INN IS NOT NULL) + GROUP BY HB.ID, HB.PARTICIPANT_CODE, HB.INN, HB.NAME_SHORT + ORDER BY HB.NAME_SHORT + INTO :HB_ID, :CODE, :INN, :NAME_SHORT, :FACE_ID, :CNT DO + BEGIN + RW = RW + 1; + INSERT INTO OD_FACE_CODERS (CODE, CODER, FACE) VALUES (:CODE, :M2M_CODER, :FACE_ID); + CMT = CMT || CRLF || RW || '. Добавлен код участника ' || CODE || ' к юрлицу: ' || INN || ', ' || COALESCE(NAME_SHORT, ''); + IF (CNT > 1) THEN + CMT = CMT || ' (ПРЕДУПРЕЖДЕНИЕ: в Фэнси найдено несколько юрлиц с ИНН ' || INN || ')'; + END + + IF (RW = 0) THEN + CMT = CMT || ' (при обработке нового справочника НРД никаких изменений не обнаружено)'; + + /* 4. Отличающиеся коды в Fancy и НРД для одного юрлица (по ИНН) или лишние коды в Fancy */ + FOR SELECT F.INN, F.NAME, C.CODE, HB.PARTICIPANT_CODE + FROM OD_FACE_CODERS C + LEFT JOIN OD_FACES F ON F.SELF_ID = C.FACE AND F.LAST_FLAG = 1 AND F.E_DATE = '9999-01-01' + LEFT JOIN FTV_M2M_HANDBOOK HB ON HB.INN = F.INN AND HB.ACTIVE = 1 AND HB.PARTICIPANT_CODE IS NOT NULL + WHERE C.CODER = :M2M_CODER + AND (C.CODE <> HB.PARTICIPANT_CODE OR HB.PARTICIPANT_CODE IS NULL) + INTO :INN, :NAME_SHORT, :CODE, :CODE2 DO + BEGIN + RW = RW + 1; + IF (CODE2 IS NULL) THEN + CMT = CMT || CRLF || 'КРИТИЧЕСКОЕ ПРЕДУПРЕЖДЕНИЕ: для юрлица ' || COALESCE(INN, '?') || ' (' || COALESCE(NAME_SHORT, '') || ') в Фэнси указан код ' || CODE || ', отсутствующий в НРД'; + ELSE + CMT = CMT || CRLF || 'КРИТИЧЕСКОЕ ПРЕДУПРЕЖДЕНИЕ: для юрлица ' || COALESCE(INN, '?') || ' (' || COALESCE(NAME_SHORT, '') || ') в Фэнси указан код ' || CODE || ', отличающийся от кода в НРД ' || CODE2; + END +END^ + + +/* Обработка одного события FTV_M2M_EVENTS. Ссылки — https://lic.zetit.ru/docs/fancy-events */ +CREATE OR ALTER PROCEDURE FTV_M2M_PROCESS_EVENTS (ID INTEGER) AS +DECLARE VARIABLE GUID CHAR(36); +DECLARE VARIABLE REFERENCE VARCHAR(255); +DECLARE VARIABLE EVENT_TYPE VARCHAR(64); +DECLARE VARIABLE EVENT_DATE TIMESTAMP; +DECLARE VARIABLE M2M_REJECT_CODE VARCHAR(1000); +DECLARE VARIABLE PARAMETERS VARCHAR(4000); +DECLARE VARIABLE DIRECTION VARCHAR(64); +DECLARE VARIABLE CMT BLOB SUB_TYPE TEXT; +DECLARE VARIABLE SHARE INTEGER; +DECLARE VARIABLE AMOUNT DOUBLE PRECISION; +DECLARE VARIABLE PLACE INTEGER; +DECLARE VARIABLE CLIENT INTEGER; +DECLARE VARIABLE FACE INTEGER; +DECLARE VARIABLE CLISER_ID INTEGER; +DECLARE VARIABLE DOL_ID INTEGER; +DECLARE VARIABLE DD_DATE TIMESTAMP; +DECLARE VARIABLE DD_NUM VARCHAR(64); +DECLARE VARIABLE OPER_ID INTEGER; +DECLARE VARIABLE ST INTEGER; +DECLARE VARIABLE EV_S VARCHAR(19); +DECLARE VARIABLE NOW_S VARCHAR(19); +BEGIN + SELECT FIRST 1 ME.GUID, ME.REFERENCE, ME.EVENT_TYPE, ME.EVENT_DATE, ME.M2M_REJECT_CODE, ME.PARAMETERS, M.DIRECTION, + H.DOC, F.DOL, D.D_DATE, D.NUM, F.SHARE, F.AMOUNT, F.PLACE, F.CLIENT, H.DEPONENT + FROM FTV_M2M_EVENTS ME + LEFT JOIN FTV_M2M M ON M.GUID = ME.GUID + LEFT JOIN D_CLISER_M2M_DOL F ON F.DOL_REF = ME.REFERENCE + LEFT JOIN D_CLISER_M2M H ON H.DOC_GUID = ME.GUID + LEFT JOIN OD_DOCS D ON D.ID = H.DOC + WHERE ME.ID = :ID + INTO :GUID, :REFERENCE, :EVENT_TYPE, :EVENT_DATE, :M2M_REJECT_CODE, :PARAMETERS, :DIRECTION, + :CLISER_ID, :DOL_ID, :DD_DATE, :DD_NUM, :SHARE, :AMOUNT, :PLACE, :CLIENT, :FACE; + + /* 1. Обновление справочника участников сервиса */ + IF (EVENT_TYPE = 'HANDBOOK') THEN + BEGIN + EXECUTE PROCEDURE FTV_M2M_UPDATE_HANDBOOK RETURNING_VALUES :CMT; + UPDATE FTV_M2M_EVENTS SET COMMENT = :CMT WHERE ID = :ID; + EXIT; + END + + /* 1a. BJ_PING — проверочное событие bj (без GUID и REFERENCE): отметить обработанным, документ не трогать */ + IF (EVENT_TYPE = 'BJ_PING') THEN EXIT; + + /* 2. Критические ошибки */ + IF (GUID IS NULL) THEN EXECUTE PROCEDURE FTV_M2M_FAIL('Не указан GUID'); + IF (REFERENCE IS NULL) THEN EXECUTE PROCEDURE FTV_M2M_FAIL('Не указан REFERENCE'); + IF (DOL_ID IS NULL) THEN EXECUTE PROCEDURE FTV_M2M_FAIL('Не найдена строка M2M-поручения по GUID + REFERENCE'); + + /* 3. Признак документа */ + EXECUTE PROCEDURE FTV_M2M_DT(:EVENT_DATE) RETURNING_VALUES :EV_S; + EXECUTE PROCEDURE FTV_M2M_DT(CURRENT_TIMESTAMP) RETURNING_VALUES :NOW_S; + INSERT INTO OD_INVALIDS (DOC, STATUS, COMMENT, DO_ING) + VALUES (:CLISER_ID, 0, + :EV_S || ' - ' || :EVENT_TYPE || COALESCE(' (референс - ' || :REFERENCE || ')', '') || COALESCE(' (параметры - ' || :PARAMETERS || ')', ''), + 'Время начала обработки: ' || :NOW_S); + + /* 4. Обработка каждого типа события. ID = 0 в UPDATE OD_DOCS — служебное изменение Fancy (триггер оставляет + прежний ID и не проверяет права оператора). */ + IF (EVENT_TYPE = 'BJ_RECEIVED') THEN + UPDATE OD_DOCS SET ID = 0, STATE = 1 WHERE ID = :CLISER_ID; /* START — начало обработки */ + ELSE IF (EVENT_TYPE = 'BJ_SENT') THEN + BEGIN + ST = NULL; + SELECT FIRST 1 S.ID FROM OD_DOC_STATES S WHERE S.SYS_NAME = 'WAIT_RESP_DECIS' INTO :ST; + UPDATE OD_DOCS SET ID = 0, STATE = :ST WHERE ID = :CLISER_ID; /* ожидание ответа Response/Decision */ + END + ELSE IF (EVENT_TYPE IN ('BJ_DECISION_CONFIRM', 'BJ_CP_CONFIRM')) THEN + UPDATE OD_DOCS SET ID = 0, STATE = 8 WHERE ID = :CLISER_ID; /* RECIEVED — принят, ожидает обработки */ + ELSE IF (EVENT_TYPE IN ('BJ_DECISION_REJECT', 'BJ_CP_REJECT')) THEN + UPDATE OD_DOCS SET ID = 0, STATE = 3 WHERE ID = :CLISER_ID; /* REJECT — отмена обработки */ + ELSE IF (EVENT_TYPE = 'BJ_SERVICE_ERROR') THEN + UPDATE OD_DOCS SET ID = 0, STATE = 3 WHERE ID = :CLISER_ID; + ELSE IF (EVENT_TYPE = 'BJ_ORDER_SENT') THEN + BEGIN + ST = NULL; + SELECT FIRST 1 S.ID FROM OD_DOC_STATES S WHERE S.SYS_NAME = 'WAIT_OPER' INTO :ST; + UPDATE OD_DOCS SET ID = 0, STATE = :ST WHERE ID = :CLISER_ID; /* ожидание исполнения операции */ + END + ELSE IF (EVENT_TYPE = 'BJ_ORDER_ACCEPTED') THEN + BEGIN + EXECUTE PROCEDURE FTV_M2M_PROCESS_OPERATION (:GUID, :REFERENCE, :EVENT_DATE, :PARAMETERS) RETURNING_VALUES :OPER_ID; + UPDATE OD_DOCS SET ID = 0, STATE = 2 WHERE ID = :CLISER_ID; /* FINAL — конец обработки */ + END + ELSE IF (EVENT_TYPE = 'BJ_EXECUTED') THEN + EXIT; /* уже исполнено по BJ_ORDER_ACCEPTED */ + ELSE IF (EVENT_TYPE = 'BJ_CANCELLED') THEN + UPDATE OD_DOCS SET ID = 0, STATE = 3 WHERE ID = :CLISER_ID; + ELSE IF (EVENT_TYPE = 'BJ_GATEWAY_REJECT') THEN + UPDATE OD_DOCS SET ID = 0, STATE = 3 WHERE ID = :CLISER_ID; + ELSE IF (EVENT_TYPE IN ('BJ_ORDER_LATE', 'BJ_QUIK_BLOCKED', 'BJ_QUIK_RELEASED', 'BJ_QUIK_SIGNED', 'BJ_QUIK_DEPOSITED')) THEN + EXIT; /* ничего не делать */ + ELSE + EXECUTE PROCEDURE FTV_M2M_FAIL('От сервиса BJ получено неизвестное событие ' || EVENT_TYPE); +END^ + + +/* Создаёт M2M-поручение вместе с подвалом документа по записи FTV_M2M. */ +CREATE OR ALTER PROCEDURE FTV_M2M_PROCESS_CREATE (ID INTEGER) AS +DECLARE VARIABLE WHOS INTEGER; +DECLARE VARIABLE FILER_CAT INTEGER; +DECLARE VARIABLE GUID CHAR(36); +DECLARE VARIABLE DIRECTION VARCHAR(64); +DECLARE VARIABLE CONTRAGENT_M2M_CODE VARCHAR(255); +DECLARE VARIABLE DEPONENT_NAME VARCHAR(255); +DECLARE VARIABLE DEPONENT_DOC VARCHAR(255); +DECLARE VARIABLE REFERENCE VARCHAR(36); +DECLARE VARIABLE ISIN VARCHAR(12); +DECLARE VARIABLE SEC_CODE VARCHAR(255); +DECLARE VARIABLE AMOUNT DOUBLE PRECISION; +DECLARE VARIABLE PLACE_NUMBER VARCHAR(255); +DECLARE VARIABLE LINK_NUMBER VARCHAR(128); +DECLARE VARIABLE DEPO_NUMBER VARCHAR(128); +DECLARE VARIABLE D_CAT INTEGER; +DECLARE VARIABLE NUM VARCHAR(255); +DECLARE VARIABLE NEW_DATE TIMESTAMP; +DECLARE VARIABLE D_DATE TIMESTAMP; +DECLARE VARIABLE CONTRAGENT INTEGER; +DECLARE VARIABLE DEPONENT INTEGER; +DECLARE VARIABLE SHARE INTEGER; +DECLARE VARIABLE PLACE INTEGER; +DECLARE VARIABLE DEPO INTEGER; +DECLARE VARIABLE CLISER_ID INTEGER; +DECLARE VARIABLE FILER_ID INTEGER; +DECLARE VARIABLE FILER_NUM VARCHAR(255); +DECLARE VARIABLE FILER_DATE TIMESTAMP; +DECLARE VARIABLE DOL_ID INTEGER; +DECLARE VARIABLE D_DAY TIMESTAMP; +BEGIN + EXECUTE PROCEDURE FTV_M2M_GET('WHOS') RETURNING_VALUES :WHOS; + + SELECT M.GUID, M.DIRECTION, M.CONTRAGENT_M2M_CODE, M.DEPONENT_NAME, M.DEPONENT_DOC FROM FTV_M2M M WHERE M.ID = :ID + INTO :GUID, :DIRECTION, :CONTRAGENT_M2M_CODE, :DEPONENT_NAME, :DEPONENT_DOC; + + /* 1.1. Неуникальный GUID */ + IF (EXISTS (SELECT 1 FROM D_CLISER_M2M WHERE DOC_GUID = :GUID)) THEN + EXECUTE PROCEDURE FTV_M2M_FAIL('Неуникальный GUID'); + + /* 1.2. Направление перевода */ + IF (DIRECTION = 'IN') THEN + SELECT FIRST 1 C.ID FROM OD_DOC_CATS C WHERE C.SYS_NAME = 'I_CLISER_M2M_ZACH' INTO :D_CAT; + ELSE IF (DIRECTION = 'OUT') THEN + SELECT FIRST 1 C.ID FROM OD_DOC_CATS C WHERE C.SYS_NAME = 'I_CLISER_M2M_SPIS' INTO :D_CAT; + ELSE + EXECUTE PROCEDURE FTV_M2M_FAIL('Ошибочное направление DIRECTION (должно быть IN, OUT)'); + + /* 1.3. Контрагент */ + SELECT FIRST 1 C.FACE FROM OD_FACE_CODERS C + LEFT JOIN OD_FACES F ON F.SELF_ID = C.CODER AND F.LAST_FLAG = 1 + WHERE F.MNEM = 'M2M' AND C.CODE = :CONTRAGENT_M2M_CODE + INTO :CONTRAGENT; + IF (CONTRAGENT IS NULL) THEN + EXECUTE PROCEDURE FTV_M2M_FAIL('Не найден депозитарий-контрагент'); + + /* 1.4. Депонент (как в MSSQL-версии: не найден — поручение создаётся без депонента) */ + SELECT FIRST 1 F.SELF_ID FROM OD_FACES F + WHERE F.LAST_FLAG = 1 + AND REPLACE(F.NAME, ' ', '') = REPLACE(:DEPONENT_NAME, ' ', '') + AND REPLACE(F.DOC_SER || F.DOC_NUM, ' ', '') = REPLACE(:DEPONENT_DOC, ' ', '') + INTO :DEPONENT; + + /* 1.5. Шапка документа */ + CLISER_ID = GEN_ID(OD_DOCS_ID_GEN, 1); + D_DATE = CURRENT_TIMESTAMP; + EXECUTE PROCEDURE FU_NEXT_DOC_NUM (:D_CAT, :D_DATE, NULL, :CLISER_ID) RETURNING_VALUES :NUM, :NEW_DATE; + + INSERT INTO OD_DOCS (ID, NUM, PARAM, D_CAT, D_DATE, STATE, DIRECTION, WHOS, KVIT) + VALUES (:CLISER_ID, :NUM, NULL, :D_CAT, :D_DATE, 0, 0, :WHOS, NULL); + INSERT INTO D_CLISER_M2M (DOC, DEPONENT, DOC_GUID, DOC_STATUS, IIS_TYPE, IIS_INN, IIS_NUM, IIS_DATE) + VALUES (:CLISER_ID, :DEPONENT, :GUID, 0, 0, NULL, NULL, NULL); + IF (DIRECTION = 'IN') THEN + INSERT INTO OD_IN_DOCS (DOC, FROM_FACE, PASS_KIND) VALUES (:CLISER_ID, :CONTRAGENT, 8); + ELSE + INSERT INTO OD_OUT_DOCS (DOC, TO_FACE, PASS_KIND) VALUES (:CLISER_ID, :CONTRAGENT, 8); + + /* 1.6. Обогащаем FTV_M2M */ + UPDATE FTV_M2M SET DOC = :CLISER_ID, CONTRAGENT = :CONTRAGENT, DEPONENT = :DEPONENT WHERE GUID = :GUID; + + /* 1.7. Подвал документа */ + FOR SELECT ML.REFERENCE, ML.ISIN, ML.SEC_CODE, ML.AMOUNT, ML.PLACE_NUMBER, ML.LINK_NUMBER, ML.DEPO_NUMBER + FROM FTV_M2M_LINES ML JOIN FTV_M2M M ON M.GUID = ML.GUID + WHERE ML.GUID = :GUID AND ML.STATE = 'TODO' + ORDER BY ML.ID + INTO :REFERENCE, :ISIN, :SEC_CODE, :AMOUNT, :PLACE_NUMBER, :LINK_NUMBER, :DEPO_NUMBER DO + BEGIN + SHARE = NULL; PLACE = NULL; DEPO = NULL; + + /* Бумага */ + SELECT FIRST 1 V.ID FROM OD_VALUES V + LEFT JOIN OD_VALUE_CODERS C ON C.SHARE = V.ID + LEFT JOIN OD_FACES F ON F.SELF_ID = C.CODER AND F.LAST_FLAG = 1 + WHERE V.ISIN = :ISIN AND F.MNEM = 'НДЦ' AND C.CODE = :SEC_CODE + INTO :SHARE; + + /* Счёт места хранения */ + SELECT FIRST 1 C.ID FROM OD_CLIENTS C + LEFT JOIN OD_CLIENTS_REC CR ON CR.CLIENT = C.SELF_ID AND CR.E_DATE = '9999-01-01' + WHERE C.NUMBER LIKE '%' || :PLACE_NUMBER || '%' AND CR.LINK_NUMBER = :LINK_NUMBER + INTO :PLACE; + + /* Счёт клиента */ + SELECT FIRST 1 C.ID FROM OD_CLIENTS C WHERE C.NUMBER = :DEPO_NUMBER INTO :DEPO; + + DOL_ID = GEN_ID(OD_DOLS_ID_GEN, 1); + INSERT INTO OD_DOLS (ID, DOC) VALUES (:DOL_ID, :CLISER_ID); + INSERT INTO D_CLISER_M2M_DOL (DOL, SHARE, AMOUNT, DOL_REF, PLACE, CLIENT) VALUES (:DOL_ID, :SHARE, :AMOUNT, :REFERENCE, :PLACE, :DEPO); + + UPDATE FTV_M2M_LINES SET STATE = 'SUCCESS', DOC = :CLISER_ID, DOL = :DOL_ID, SHARE = :SHARE, PLACE = :PLACE, DEPO = :DEPO + WHERE GUID = :GUID AND REFERENCE = :REFERENCE; + END + + /* 1.8. Входящая запись — только для списания (исходящий перевод OUT) */ + IF (DIRECTION = 'OUT') THEN + BEGIN + EXECUTE PROCEDURE FTV_M2M_GET('FILER_CAT') RETURNING_VALUES :FILER_CAT; + FILER_ID = GEN_ID(OD_DOCS_ID_GEN, 1); + FILER_DATE = CURRENT_TIMESTAMP; + EXECUTE PROCEDURE FU_NEXT_DOC_NUM ('D_FILER', :FILER_DATE, 1, :FILER_ID) RETURNING_VALUES :FILER_NUM, :NEW_DATE; + EXECUTE PROCEDURE FTV_M2M_DAY(:D_DATE) RETURNING_VALUES :D_DAY; + INSERT INTO OD_DOCS (ID, NUM, D_CAT, D_DATE, STATE, DIRECTION, WHOS) + VALUES (:FILER_ID, :FILER_NUM, :FILER_CAT, :FILER_DATE, 2, 1, :WHOS); + INSERT INTO D_FILER (DOC, PASS_KIND, RUBRICA, COMMENT, DOC_NUM, DOC_DATE, CORR_FACE, FUND, ACCOUNT, PLACE, ORI_DATE, CHILD_CAT, MESS) + VALUES (:FILER_ID, 7, 8, 'M2M поручение на списание через MOEX-МОСТ', :NUM, :D_DAY, :DEPONENT, :DEPONENT, + :DEPO, :PLACE, :FILER_DATE, :D_CAT, :GUID); + /* Связываем с M2M-поручением */ + UPDATE OD_DOCS SET ID = 0, KVIT = :FILER_ID WHERE ID = :CLISER_ID; + END +END^ + + +/* Запуск обработки: новые поручения (FTV_M2M) и события (FTV_M2M_EVENTS) со STATE = 'TODO'. Вызывает bj + (в Firebird нет SQL Server Agent). Ошибка одной записи не останавливает остальные. */ +CREATE OR ALTER PROCEDURE FTV_M2M_JOB AS +DECLARE VARIABLE ID INTEGER; +DECLARE VARIABLE ST VARCHAR(64); +DECLARE VARIABLE CMT VARCHAR(1000); +DECLARE VARIABLE R INTEGER; +BEGIN + /* НОВЫЕ ПОРУЧЕНИЯ */ + FOR SELECT M.ID FROM FTV_M2M M WHERE M.STATE = 'TODO' ORDER BY M.TIMENEW, M.ID INTO :ID DO + BEGIN + UPDATE FTV_M2M SET TIMEBEG = CURRENT_TIMESTAMP WHERE ID = :ID; + ST = 'SUCCESS'; CMT = NULL; + R = RDB$SET_CONTEXT('USER_TRANSACTION', 'FTV_M2M_ERR', NULL); + BEGIN + EXECUTE PROCEDURE FTV_M2M_PROCESS_CREATE (:ID); + WHEN ANY DO + BEGIN + ST = 'ERROR'; + CMT = COALESCE(CAST(RDB$GET_CONTEXT('USER_TRANSACTION', 'FTV_M2M_ERR') AS VARCHAR(255) CHARACTER SET WIN1251), 'ошибка Fancy') || ' // FTV_M2M_PROCESS_CREATE // SQLCODE ' || SQLCODE || ', GDSCODE ' || GDSCODE; + END + END + UPDATE FTV_M2M SET STATE = :ST, TIMEEND = CURRENT_TIMESTAMP, COMMENT = :CMT WHERE ID = :ID; + END + + /* НОВЫЕ СОБЫТИЯ */ + FOR SELECT E.ID FROM FTV_M2M_EVENTS E WHERE E.STATE = 'TODO' ORDER BY E.TIMENEW, E.ID INTO :ID DO + BEGIN + UPDATE FTV_M2M_EVENTS SET TIMEBEG = CURRENT_TIMESTAMP WHERE ID = :ID; + ST = 'SUCCESS'; CMT = NULL; + R = RDB$SET_CONTEXT('USER_TRANSACTION', 'FTV_M2M_ERR', NULL); + BEGIN + EXECUTE PROCEDURE FTV_M2M_PROCESS_EVENTS (:ID); + WHEN ANY DO + BEGIN + ST = 'ERROR'; + CMT = COALESCE(CAST(RDB$GET_CONTEXT('USER_TRANSACTION', 'FTV_M2M_ERR') AS VARCHAR(255) CHARACTER SET WIN1251), 'ошибка Fancy') || ' // FTV_M2M_PROCESS_EVENTS // SQLCODE ' || SQLCODE || ', GDSCODE ' || GDSCODE; + END + END + /* без ошибки COMMENT не трогаем: в нём отчёт справочника (HANDBOOK) */ + IF (CMT IS NOT NULL) THEN + UPDATE FTV_M2M_EVENTS SET STATE = :ST, TIMEEND = CURRENT_TIMESTAMP, COMMENT = :CMT WHERE ID = :ID; + ELSE + UPDATE FTV_M2M_EVENTS SET STATE = :ST, TIMEEND = CURRENT_TIMESTAMP WHERE ID = :ID; + END +END^ + +SET TERM ; ^ + +COMMIT; diff --git a/internal/lkgateway/depoprocs/firebird/20_FTV_M2M_GRANTS.sql b/internal/lkgateway/depoprocs/firebird/20_FTV_M2M_GRANTS.sql new file mode 100644 index 0000000..14cbb64 --- /dev/null +++ b/internal/lkgateway/depoprocs/firebird/20_FTV_M2M_GRANTS.sql @@ -0,0 +1,354 @@ +/* Bridge&Join — права для процедур обработки M2M (Firebird 2.5). + Процедуры работают правами, выданными им самим: логину bj нужно только EXECUTE на FTV_M2M_JOB и доступ к + таблицам обмена FTV_M2M*. Выполнять под SYSDBA или владельцем базы. :BJ_USER — логин bj (подставляет bj). */ + +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_LINES TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_EVENTS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_HANDBOOK TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_SETTINGS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON OD_DOCS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON OD_FACES TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON OD_FACE_CODERS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON OD_U_FACES TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON OD_DOLS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON OD_STEPS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON OD_IN_DOCS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON OD_OUT_DOCS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON OD_INVALIDS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M_DOL TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON D_FILER TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_ZACH TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_SPIS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT ON OD_CLIENTS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT ON OD_CLIENTS_REC TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT ON OD_DOC_CATS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT ON OD_DOC_STATES TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT ON OD_VALUES TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT ON OD_VALUE_CODERS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT ON OD_SYS_TABS TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT ON OD_STEP_TYPES TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE FU_NEXT_DOC_NUM TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE OD_PR_INS_WIRS TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE FTV_M2M_GET TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DT TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DAY TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_WIRING TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_OPERATION TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE FTV_M2M_UPDATE_HANDBOOK TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_EVENTS TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_CREATE TO PROCEDURE FTV_M2M_FAIL; +GRANT EXECUTE ON PROCEDURE FTV_M2M_JOB TO PROCEDURE FTV_M2M_FAIL; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_LINES TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_EVENTS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_HANDBOOK TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_SETTINGS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON OD_DOCS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON OD_FACES TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON OD_FACE_CODERS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON OD_U_FACES TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON OD_DOLS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON OD_STEPS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON OD_IN_DOCS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON OD_OUT_DOCS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON OD_INVALIDS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M_DOL TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON D_FILER TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_ZACH TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_SPIS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT ON OD_CLIENTS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT ON OD_CLIENTS_REC TO PROCEDURE FTV_M2M_GET; +GRANT SELECT ON OD_DOC_CATS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT ON OD_DOC_STATES TO PROCEDURE FTV_M2M_GET; +GRANT SELECT ON OD_VALUES TO PROCEDURE FTV_M2M_GET; +GRANT SELECT ON OD_VALUE_CODERS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT ON OD_SYS_TABS TO PROCEDURE FTV_M2M_GET; +GRANT SELECT ON OD_STEP_TYPES TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE FU_NEXT_DOC_NUM TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE OD_PR_INS_WIRS TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE FTV_M2M_FAIL TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DT TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DAY TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_WIRING TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_OPERATION TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE FTV_M2M_UPDATE_HANDBOOK TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_EVENTS TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_CREATE TO PROCEDURE FTV_M2M_GET; +GRANT EXECUTE ON PROCEDURE FTV_M2M_JOB TO PROCEDURE FTV_M2M_GET; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_LINES TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_EVENTS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_HANDBOOK TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_SETTINGS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON OD_DOCS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON OD_FACES TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON OD_FACE_CODERS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON OD_U_FACES TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON OD_DOLS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON OD_STEPS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON OD_IN_DOCS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON OD_OUT_DOCS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON OD_INVALIDS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M_DOL TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON D_FILER TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_ZACH TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_SPIS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT ON OD_CLIENTS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT ON OD_CLIENTS_REC TO PROCEDURE FTV_M2M_DT; +GRANT SELECT ON OD_DOC_CATS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT ON OD_DOC_STATES TO PROCEDURE FTV_M2M_DT; +GRANT SELECT ON OD_VALUES TO PROCEDURE FTV_M2M_DT; +GRANT SELECT ON OD_VALUE_CODERS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT ON OD_SYS_TABS TO PROCEDURE FTV_M2M_DT; +GRANT SELECT ON OD_STEP_TYPES TO PROCEDURE FTV_M2M_DT; +GRANT EXECUTE ON PROCEDURE FU_NEXT_DOC_NUM TO PROCEDURE FTV_M2M_DT; +GRANT EXECUTE ON PROCEDURE OD_PR_INS_WIRS TO PROCEDURE FTV_M2M_DT; +GRANT EXECUTE ON PROCEDURE FTV_M2M_FAIL TO PROCEDURE FTV_M2M_DT; +GRANT EXECUTE ON PROCEDURE FTV_M2M_GET TO PROCEDURE FTV_M2M_DT; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_WIRING TO PROCEDURE FTV_M2M_DT; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_OPERATION TO PROCEDURE FTV_M2M_DT; +GRANT EXECUTE ON PROCEDURE FTV_M2M_UPDATE_HANDBOOK TO PROCEDURE FTV_M2M_DT; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_EVENTS TO PROCEDURE FTV_M2M_DT; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_CREATE TO PROCEDURE FTV_M2M_DT; +GRANT EXECUTE ON PROCEDURE FTV_M2M_JOB TO PROCEDURE FTV_M2M_DT; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_LINES TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_EVENTS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_HANDBOOK TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_SETTINGS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON OD_DOCS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON OD_FACES TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON OD_FACE_CODERS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON OD_U_FACES TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON OD_DOLS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON OD_STEPS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON OD_IN_DOCS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON OD_OUT_DOCS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON OD_INVALIDS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M_DOL TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON D_FILER TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_ZACH TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_SPIS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT ON OD_CLIENTS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT ON OD_CLIENTS_REC TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT ON OD_DOC_CATS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT ON OD_DOC_STATES TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT ON OD_VALUES TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT ON OD_VALUE_CODERS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT ON OD_SYS_TABS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT ON OD_STEP_TYPES TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE FU_NEXT_DOC_NUM TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE OD_PR_INS_WIRS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE FTV_M2M_FAIL TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE FTV_M2M_GET TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DT TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DAY TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_OPERATION TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE FTV_M2M_UPDATE_HANDBOOK TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_EVENTS TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_CREATE TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT EXECUTE ON PROCEDURE FTV_M2M_JOB TO PROCEDURE FTV_M2M_PROCESS_WIRING; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_LINES TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_EVENTS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_HANDBOOK TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_SETTINGS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON OD_DOCS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON OD_FACES TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON OD_FACE_CODERS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON OD_U_FACES TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON OD_DOLS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON OD_STEPS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON OD_IN_DOCS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON OD_OUT_DOCS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON OD_INVALIDS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M_DOL TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON D_FILER TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_ZACH TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_SPIS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT ON OD_CLIENTS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT ON OD_CLIENTS_REC TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT ON OD_DOC_CATS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT ON OD_DOC_STATES TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT ON OD_VALUES TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT ON OD_VALUE_CODERS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT ON OD_SYS_TABS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT ON OD_STEP_TYPES TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE FU_NEXT_DOC_NUM TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE OD_PR_INS_WIRS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE FTV_M2M_FAIL TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE FTV_M2M_GET TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DT TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DAY TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_WIRING TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE FTV_M2M_UPDATE_HANDBOOK TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_EVENTS TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_CREATE TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT EXECUTE ON PROCEDURE FTV_M2M_JOB TO PROCEDURE FTV_M2M_PROCESS_OPERATION; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_LINES TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_EVENTS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_HANDBOOK TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_SETTINGS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON OD_DOCS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON OD_FACES TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON OD_FACE_CODERS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON OD_U_FACES TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON OD_DOLS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON OD_STEPS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON OD_IN_DOCS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON OD_OUT_DOCS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON OD_INVALIDS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M_DOL TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON D_FILER TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_ZACH TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_SPIS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT ON OD_CLIENTS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT ON OD_CLIENTS_REC TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT ON OD_DOC_CATS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT ON OD_DOC_STATES TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT ON OD_VALUES TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT ON OD_VALUE_CODERS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT ON OD_SYS_TABS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT ON OD_STEP_TYPES TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE FU_NEXT_DOC_NUM TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE OD_PR_INS_WIRS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE FTV_M2M_FAIL TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE FTV_M2M_GET TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DT TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DAY TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_WIRING TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_OPERATION TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_EVENTS TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_CREATE TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT EXECUTE ON PROCEDURE FTV_M2M_JOB TO PROCEDURE FTV_M2M_UPDATE_HANDBOOK; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_LINES TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_EVENTS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_HANDBOOK TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_SETTINGS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON OD_DOCS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON OD_FACES TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON OD_FACE_CODERS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON OD_U_FACES TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON OD_DOLS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON OD_STEPS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON OD_IN_DOCS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON OD_OUT_DOCS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON OD_INVALIDS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M_DOL TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON D_FILER TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_ZACH TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_SPIS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT ON OD_CLIENTS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT ON OD_CLIENTS_REC TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT ON OD_DOC_CATS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT ON OD_DOC_STATES TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT ON OD_VALUES TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT ON OD_VALUE_CODERS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT ON OD_SYS_TABS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT ON OD_STEP_TYPES TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE FU_NEXT_DOC_NUM TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE OD_PR_INS_WIRS TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE FTV_M2M_FAIL TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE FTV_M2M_GET TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DT TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DAY TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_WIRING TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_OPERATION TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE FTV_M2M_UPDATE_HANDBOOK TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_CREATE TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT EXECUTE ON PROCEDURE FTV_M2M_JOB TO PROCEDURE FTV_M2M_PROCESS_EVENTS; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_LINES TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_EVENTS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_HANDBOOK TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_SETTINGS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON OD_DOCS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON OD_FACES TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON OD_FACE_CODERS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON OD_U_FACES TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON OD_DOLS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON OD_STEPS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON OD_IN_DOCS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON OD_OUT_DOCS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON OD_INVALIDS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M_DOL TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON D_FILER TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_ZACH TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_SPIS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT ON OD_CLIENTS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT ON OD_CLIENTS_REC TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT ON OD_DOC_CATS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT ON OD_DOC_STATES TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT ON OD_VALUES TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT ON OD_VALUE_CODERS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT ON OD_SYS_TABS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT ON OD_STEP_TYPES TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE FU_NEXT_DOC_NUM TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE OD_PR_INS_WIRS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE FTV_M2M_FAIL TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE FTV_M2M_GET TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DT TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DAY TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_WIRING TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_OPERATION TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE FTV_M2M_UPDATE_HANDBOOK TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_EVENTS TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT EXECUTE ON PROCEDURE FTV_M2M_JOB TO PROCEDURE FTV_M2M_PROCESS_CREATE; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_LINES TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_EVENTS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_HANDBOOK TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_SETTINGS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON OD_DOCS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON OD_FACES TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON OD_FACE_CODERS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON OD_U_FACES TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON OD_DOLS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON OD_STEPS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON OD_IN_DOCS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON OD_OUT_DOCS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON OD_INVALIDS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON D_CLISER_M2M_DOL TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON D_FILER TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_ZACH TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT, INSERT, UPDATE ON U_OP_INV_SPIS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT ON OD_CLIENTS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT ON OD_CLIENTS_REC TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT ON OD_DOC_CATS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT ON OD_DOC_STATES TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT ON OD_VALUES TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT ON OD_VALUE_CODERS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT ON OD_SYS_TABS TO PROCEDURE FTV_M2M_JOB; +GRANT SELECT ON OD_STEP_TYPES TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE FU_NEXT_DOC_NUM TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE OD_PR_INS_WIRS TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE FTV_M2M_FAIL TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE FTV_M2M_GET TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DT TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE FTV_M2M_DAY TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_WIRING TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_OPERATION TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE FTV_M2M_UPDATE_HANDBOOK TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_EVENTS TO PROCEDURE FTV_M2M_JOB; +GRANT EXECUTE ON PROCEDURE FTV_M2M_PROCESS_CREATE TO PROCEDURE FTV_M2M_JOB; + +/* логин bj: запуск обработки и запись в таблицы обмена */ +GRANT EXECUTE ON PROCEDURE FTV_M2M_JOB TO :BJ_USER; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M TO :BJ_USER; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_LINES TO :BJ_USER; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_EVENTS TO :BJ_USER; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_HANDBOOK TO :BJ_USER; +GRANT SELECT, INSERT, UPDATE, DELETE ON FTV_M2M_SETTINGS TO :BJ_USER; +COMMIT; diff --git a/internal/lkgateway/depotables.go b/internal/lkgateway/depotables.go index 10b0c58..eb6d2c2 100644 --- a/internal/lkgateway/depotables.go +++ b/internal/lkgateway/depotables.go @@ -250,11 +250,17 @@ func depoTablesFileName(f FancyMSSQLSettings) string { if f.IsPostgres() { return "bj-depo-tables-postgres.sql" } + if f.IsFirebird() { + return "bj-depo-tables-firebird.sql" + } return "bj-depo-tables-mssql.sql" } // depoTablesScript — скрипт для администратора СУБД (вариант «по скрипту»). func depoTablesScript(f FancyMSSQLSettings) string { + if f.IsFirebird() { // таблицы у Firebird — вместе с привязками, процедурами и правами (depo_setup_fb.go) + return depoSetupScriptFB(f, f.DepoDB(), depoSetupLogin(f), "") + } pg := f.IsPostgres() db, user := f.DepoDB(), strings.TrimSpace(f.User) sep := "\nGO\n\n" @@ -285,7 +291,7 @@ func depoTablesScript(f FancyMSSQLSettings) string { } func depoTableVisible(ctx context.Context, db *sql.DB, pg bool, name string) bool { - q, arg := "SELECT CASE WHEN OBJECT_ID(@p1, 'U') IS NULL THEN 0 ELSE 1 END", "dbo."+name + q, arg := fsql.Cat01, "dbo."+name if pg { q, arg = "SELECT to_regclass($1) IS NOT NULL", name } @@ -294,7 +300,7 @@ func depoTableVisible(ctx context.Context, db *sql.DB, pg bool, name string) boo } func depoMissingColumns(ctx context.Context, db *sql.DB, pg bool, t depoTableSpec) []string { - q, arg := "SELECT name FROM sys.columns WHERE object_id = OBJECT_ID(@p1)", "dbo."+t.Name + q, arg := fsql.Cat02, "dbo."+t.Name if pg { q, arg = "SELECT column_name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = lower($1)", t.Name } @@ -342,6 +348,9 @@ func depoTablesRightsHint(f FancyMSSQLSettings, login string, err error) string // (используется один раз, не сохраняется); пусто — логин bj из настроек. Если логин // другой, bj выдаёт своему логину права на таблицы. func depoTablesCreate(ctx context.Context, f FancyMSSQLSettings, adminUser, adminPass string) (string, error) { + if f.IsFirebird() { // у Firebird таблицы ставятся вместе с привязками, процедурами и правами + return depoProcsInstallFB(ctx, f, adminUser, adminPass) + } pg := f.IsPostgres() conn, grantTo := f, "" if u := strings.TrimSpace(adminUser); u != "" { @@ -404,7 +413,7 @@ func depoTablesCheck(ctx context.Context, f FancyMSSQLSettings) (string, bool) { } var lacks []string for _, p := range []string{"SELECT", "INSERT", "UPDATE", "DELETE"} { - q, arg := "SELECT ISNULL(HAS_PERMS_BY_NAME(@p1, 'OBJECT', @p2), 0)", "dbo."+t.Name + q, arg := fsql.Cat03, "dbo."+t.Name if pg { q, arg = "SELECT has_table_privilege($1, $2)", t.Name } @@ -429,9 +438,9 @@ func depoTablesCheck(ctx context.Context, f FancyMSSQLSettings) (string, bool) { // DepoTablesView — данные карточки «Трансферные таблицы DEPO». type DepoTablesView struct { DB, User, Script, ScriptFile string - Postgres bool + Postgres, Firebird bool } func depoTablesView(f FancyMSSQLSettings) DepoTablesView { - return DepoTablesView{DB: f.DepoDB(), User: f.User, Script: depoTablesScript(f), ScriptFile: depoTablesFileName(f), Postgres: f.IsPostgres()} + return DepoTablesView{DB: f.DepoDB(), User: f.User, Script: depoTablesScript(f), ScriptFile: depoTablesFileName(f), Postgres: f.IsPostgres(), Firebird: f.IsFirebird()} } diff --git a/internal/lkgateway/fancydb.go b/internal/lkgateway/fancydb.go index f7daee0..71fe0f4 100644 --- a/internal/lkgateway/fancydb.go +++ b/internal/lkgateway/fancydb.go @@ -10,9 +10,10 @@ import ( _ "github.com/jackc/pgx/v5/stdlib" // драйвер "pgx" для PostgreSQL _ "github.com/microsoft/go-mssqldb" // драйвер "sqlserver" для MSSQL + _ "github.com/nakagami/firebirdsql" // драйвер "firebirdsql" для Firebird ) -// Подключение к серверу учётной базы клиента Fancy DEPO — MSSQL или PostgreSQL, +// Подключение к серверу учётной базы клиента Fancy DEPO — MSSQL, PostgreSQL или Firebird, // SQL-логин. База выбирается в настройках (DepoDB). // validDBName — безопасное имя БД/идентификатора MSSQL (буквы/цифры/_), чтобы @@ -35,6 +36,9 @@ func fancyDriver(s FancyMSSQLSettings) string { if s.IsPostgres() { return "pgx" } + if s.IsFirebird() { + return "firebirdsql" + } return "sqlserver" } @@ -49,6 +53,9 @@ func fancyAdminDB(s FancyMSSQLSettings) string { // fancyDSN строит строку подключения. db="" → к служебной БД (для CREATE DATABASE). func fancyDSN(s FancyMSSQLSettings, db string) string { + if s.IsFirebird() { + return fbDSN(s, db) + } port := s.DefaultPort() if s.IsPostgres() { ssl := s.Encrypt @@ -95,6 +102,9 @@ func fancyOpen(s FancyMSSQLSettings, db string) (*sql.DB, error) { if s.IsPostgres() && db == "" { db = fancyAdminDB(s) } + if s.IsFirebird() && db == "" { // служебной базы у Firebird нет: подключение к серверу — это подключение к базе + db = s.DepoDB() + } conn, err := fancyOpenGated(s, fancyDSN(s, db)) // после неудачного входа — пауза, чтобы не заблокировать логин if err != nil { return nil, err @@ -121,6 +131,9 @@ func fancyPing(ctx context.Context, s FancyMSSQLSettings, db string) (string, er if s.IsPostgres() { verQuery = "SELECT version()" } + if s.IsFirebird() { + verQuery = "SELECT 'Firebird ' || RDB$GET_CONTEXT('SYSTEM', 'ENGINE_VERSION') FROM RDB$DATABASE" + } _ = conn.QueryRowContext(cctx, verQuery).Scan(&ver) ver = strings.TrimSpace(ver) if i := strings.IndexAny(ver, "\r\n"); i > 0 { diff --git a/internal/lkgateway/fancydb_handlers.go b/internal/lkgateway/fancydb_handlers.go index a0d8a7f..4afaa0a 100644 --- a/internal/lkgateway/fancydb_handlers.go +++ b/internal/lkgateway/fancydb_handlers.go @@ -39,16 +39,12 @@ func (h *setupHandlers) registerFancy(mux *http.ServeMux) { func (h *setupHandlers) fancyFromForm(r *http.Request) FancyMSSQLSettings { cur := h.rc.Snapshot().Fancy dbType := strings.TrimSpace(r.FormValue("db_type")) - if dbType != "postgres" { + if dbType != "postgres" && dbType != "firebird" { dbType = "mssql" } port, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("port"))) if port == 0 { - if dbType == "postgres" { - port = 5432 - } else { - port = 1433 - } + port = map[string]int{"mssql": 1433, "postgres": 5432, "firebird": 3050}[dbType] } pass := r.FormValue("password") if pass == "" { @@ -178,6 +174,12 @@ func (h *setupHandlers) checkFancyDepoTables(w http.ResponseWriter, r *http.Requ func (h *setupHandlers) fancyDepoTablesScript(w http.ResponseWriter, r *http.Request) { s := h.rc.Snapshot().Fancy body := depoTablesScript(s) + if s.IsFirebird() { // isql -ch WIN1251 + w.Header().Set("Content-Type", "application/sql; charset=windows-1251") + w.Header().Set("Content-Disposition", `attachment; filename="`+depoTablesFileName(s)+`"`) + _, _ = w.Write(win1251(body)) + return + } if !s.IsPostgres() { body = "\uFEFF" + body // SSMS без BOM читает кириллицу в комментариях как ANSI } @@ -291,10 +293,7 @@ func (h *setupHandlers) saveFancy(w http.ResponseWriter, r *http.Request) { return } s := h.fancyFromForm(r) - dbLabel := "MSSQL" - if s.IsPostgres() { - dbLabel = "PostgreSQL" - } + dbLabel := fancyDBLabel(s) if s.Server == "" || s.User == "" { setupFlash(w, r, dbLabel+": укажите сервер и пользователя") return @@ -317,8 +316,13 @@ func (h *setupHandlers) testFancy(w http.ResponseWriter, r *http.Request) { setupFlash(w, r, "MSSQL: сначала сохраните параметры подключения") return } - // Проверяем соединение к серверу (master) — БД может ещё не существовать. - ver, err := fancyPing(depoLoginForce(r.Context()), s, "master") + // Проверяем соединение к серверу (master) — БД может ещё не существовать. У Firebird служебной базы нет — + // подключение к базе DEPO. + db := "master" + if s.IsFirebird() { + db = s.DepoDB() + } + ver, err := fancyPing(depoLoginForce(r.Context()), s, db) if err != nil { setupFlash(w, r, fancyDBLabel(s)+": соединение не удалось — "+err.Error()) return @@ -331,6 +335,9 @@ func fancyDBLabel(s FancyMSSQLSettings) string { if s.IsPostgres() { return "PostgreSQL" } + if s.IsFirebird() { + return "Firebird" + } return "MSSQL" } diff --git a/internal/lkgateway/fancyfb.go b/internal/lkgateway/fancyfb.go new file mode 100644 index 0000000..ef32d7d --- /dev/null +++ b/internal/lkgateway/fancyfb.go @@ -0,0 +1,470 @@ +package lkgateway + +// Fancy DEPO на Firebird (27.10.2026). Fancy работает и на Firebird 2.5 — те же таблицы и процедуры ядра, что в +// MSSQL-версии (MSSQL-версия Fancy — перевод Firebird-версии). Весь SQL bj к DEPO написан на T-SQL; чтобы не +// держать второй текст в каждом месте вызова, подключение к Firebird переводит запросы само (fbConn): +// - точный текст Firebird — для запросов, которые механически не переводятся (fsql.FB, ключ — текст T-SQL); +// - остальное механически, вне строк и комментариев: без dbo., TOP n — FIRST n, ISNULL — COALESCE, +// GETDATE() — CURRENT_TIMESTAMP, LEN — CHAR_LENGTH, N'…' — '…', «exec имя» — EXECUTE PROCEDURE имя; +// - именованные параметры @имя (sql.Named) — позиционные ? в порядке появления в тексте; +// - GUID в параметрах — в верхнем регистре (в таблицах обмена Firebird GUID — строка в верхнем регистре, +// uniqueidentifier MSSQL сравнивается без учёта регистра); время «2006-01-02T15:04:05» — с пробелом вместо T. +// База Fancy на Firebird обычно в диалекте 1 и кодировке NONE с данными в WIN1251 — подключение в WIN1251. +// Таблицы, процедуры и права для Firebird ставит depo_setup_fb.go; DDL сюда не попадает. + +import ( + "context" + "database/sql/driver" + "errors" + "fmt" + "log" + "net/url" + "regexp" + "strconv" + "strings" + "sync" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" + "github.com/nakagami/firebirdsql" +) + +// fbDSN — строка подключения драйвера firebirdsql: путь к файлу базы на сервере Firebird или псевдоним. +func fbDSN(s FancyMSSQLSettings, db string) string { + q := url.Values{} + q.Set("charset", "WIN1251") + u := &url.URL{ + Scheme: "firebird", + User: url.UserPassword(s.User, s.Password), + Host: fmt.Sprintf("%s:%d", s.Server, s.DefaultPort()), + Path: "/" + db, + RawQuery: q.Encode(), + } + return u.String() +} + +// validFBPath — путь к файлу базы Firebird на сервере (D:\Fancy\DEPO.GDB, /var/db/depo.fdb) или псевдоним +// из aliases.conf: без кавычек, точки с запятой и управляющих символов (путь уходит в строку подключения). +func validFBPath(s string) bool { + if strings.TrimSpace(s) == "" || len(s) > 255 { + return false + } + for _, c := range s { + if c < ' ' || strings.ContainsRune(`'";?#`, c) { + return false + } + } + return true +} + +// fbIsLoginFailure — Firebird отверг логин или пароль. +func fbIsLoginFailure(err error) bool { + var fe *firebirdsql.FbError + if !errors.As(err, &fe) { + return false + } + for _, c := range fe.GDSCodes { + if c == firebirdsql.ISCLogin || c == firebirdsql.ISCLoginError { + return true + } + } + return false +} + +// fbConnector — подключения к Firebird с переводом запросов. +type fbConnector struct{ base driver.Connector } + +func (c fbConnector) Driver() driver.Driver { return c.base.Driver() } + +func (c fbConnector) Connect(ctx context.Context) (driver.Conn, error) { + conn, err := c.base.Connect(ctx) + if err != nil { + return nil, err + } + return &fbConn{c: conn}, nil +} + +// fbConn — подключение драйвера firebirdsql, которому database/sql отдаёт T-SQL bj: текст и параметры +// переводятся в fbRewrite. Остальные методы — как у драйвера. +type fbConn struct{ c driver.Conn } + +func (f *fbConn) Prepare(query string) (driver.Stmt, error) { + return f.PrepareContext(context.Background(), query) +} + +func (f *fbConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { + q, names := query, []string(nil) + if !fbNative(query) { + q, names = fbParams(fbSQL(query)) + } + var st driver.Stmt + var err error + if pc, ok := f.c.(driver.ConnPrepareContext); ok { + st, err = pc.PrepareContext(ctx, q) + } else { + st, err = f.c.Prepare(q) + } + if err != nil { + return nil, fbErr(query, q, err) + } + return &fbStmt{st: st, names: names}, nil +} + +func (f *fbConn) Close() error { return f.c.Close() } + +func (f *fbConn) Begin() (driver.Tx, error) { + return f.BeginTx(context.Background(), driver.TxOptions{}) +} + +func (f *fbConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { + if bt, ok := f.c.(driver.ConnBeginTx); ok { + return bt.BeginTx(ctx, opts) + } + return f.c.Begin() //nolint:staticcheck // у драйвера без ConnBeginTx другого способа нет +} + +func (f *fbConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { + ex, ok := f.c.(driver.ExecerContext) + if !ok { + return nil, driver.ErrSkip + } + q, a, err := fbRewrite(query, args) + if err != nil { + return nil, err + } + res, err := ex.ExecContext(ctx, q, a) + return res, fbErr(query, q, err) +} + +func (f *fbConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { + qr, ok := f.c.(driver.QueryerContext) + if !ok { + return nil, driver.ErrSkip + } + q, a, err := fbRewrite(query, args) + if err != nil { + return nil, err + } + rows, err := qr.QueryContext(ctx, q, a) + return rows, fbErr(query, q, err) +} + +func (f *fbConn) Ping(ctx context.Context) error { + if p, ok := f.c.(driver.Pinger); ok { + return p.Ping(ctx) + } + return nil +} + +func (f *fbConn) CheckNamedValue(nv *driver.NamedValue) error { + if c, ok := f.c.(driver.NamedValueChecker); ok { + return c.CheckNamedValue(nv) + } + return driver.ErrSkip +} + +func (f *fbConn) IsValid() bool { + if v, ok := f.c.(driver.Validator); ok { + return v.IsValid() + } + return true +} + +func (f *fbConn) ResetSession(ctx context.Context) error { + if r, ok := f.c.(driver.SessionResetter); ok { + return r.ResetSession(ctx) + } + return nil +} + +// fbStmt — подготовленный запрос: параметры при выполнении расставляются по именам из текста. +type fbStmt struct { + st driver.Stmt + names []string +} + +func (s *fbStmt) Close() error { return s.st.Close() } +func (s *fbStmt) NumInput() int { return -1 } + +func (s *fbStmt) Exec(args []driver.Value) (driver.Result, error) { + return s.ExecContext(context.Background(), fbValues(args)) +} + +func (s *fbStmt) Query(args []driver.Value) (driver.Rows, error) { + return s.QueryContext(context.Background(), fbValues(args)) +} + +func (s *fbStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { + a, err := fbArgs(s.names, args) + if err != nil { + return nil, err + } + if ex, ok := s.st.(driver.StmtExecContext); ok { + return ex.ExecContext(ctx, a) + } + return s.st.Exec(fbPlain(a)) //nolint:staticcheck // драйвер без StmtExecContext +} + +func (s *fbStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { + a, err := fbArgs(s.names, args) + if err != nil { + return nil, err + } + if q, ok := s.st.(driver.StmtQueryContext); ok { + return q.QueryContext(ctx, a) + } + return s.st.Query(fbPlain(a)) //nolint:staticcheck // драйвер без StmtQueryContext +} + +func fbValues(v []driver.Value) []driver.NamedValue { + out := make([]driver.NamedValue, len(v)) + for i, x := range v { + out[i] = driver.NamedValue{Ordinal: i + 1, Value: x} + } + return out +} + +func fbPlain(a []driver.NamedValue) []driver.Value { + out := make([]driver.Value, len(a)) + for i, x := range a { + out[i] = x.Value + } + return out +} + +// fbRewrite — текст и параметры запроса T-SQL для Firebird. DDL и блоки PSQL (установка, depo_setup_fb.go) +// написаны для Firebird — уходят как есть. +func fbRewrite(query string, args []driver.NamedValue) (string, []driver.NamedValue, error) { + if fbNative(query) { + return query, args, nil + } + q, names := fbParams(fbSQL(query)) + a, err := fbArgs(names, args) + return q, a, err +} + +var fbNativeRe = regexp.MustCompile(`(?i)^\s*(CREATE|ALTER|RECREATE|DROP|GRANT|REVOKE|EXECUTE\s+BLOCK|COMMENT\s+ON)\b`) + +// fbNative — команда уже на Firebird (установка таблиц, процедур, прав). +func fbNative(q string) bool { return fbNativeRe.MatchString(fbStripComments(q)) } + +var ( + fbExecRe = regexp.MustCompile(`(?is)^\s*exec(?:ute)?\s+(?:\[?dbo\]?\.)?\[?([A-Za-z0-9_]+)\]?\s*;?\s*$`) + fbObjectIDRe = regexp.MustCompile(`(?i)^SELECT OBJECT_ID\('(?:dbo\.)?([A-Za-z0-9_]+)'\)$`) + fbDboRe = regexp.MustCompile(`(?i)\[?\bdbo\]?\.`) + fbIsNullRe = regexp.MustCompile(`(?i)\bISNULL\s*\(`) + fbGetDateRe = regexp.MustCompile(`(?i)\bGETDATE\s*\(\s*\)`) + fbLenRe = regexp.MustCompile(`(?i)\bLEN\s*\(`) + fbTopRe = regexp.MustCompile(`(?i)\bSELECT(\s+DISTINCT)?\s+TOP\s*(\(\s*@?\w+\s*\)|\d+)`) + fbGUIDRe = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) + fbISOTimeRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?$`) +) + +// fbSQL — текст запроса для Firebird: точный перевод из fsql.FB, иначе механический. +func fbSQL(q string) string { + if t, ok := fsql.FB[q]; ok { + return t + } + if m := fbExecRe.FindStringSubmatch(q); m != nil { + return "EXECUTE PROCEDURE " + m[1] + } + if m := fbObjectIDRe.FindStringSubmatch(q); m != nil { // depoTableExists: NULL — таблицы нет + return "SELECT (SELECT FIRST 1 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = '" + strings.ToUpper(m[1]) + "') FROM RDB$DATABASE" + } + return fbOutsideLiterals(q, func(code string) string { + code = fbDboRe.ReplaceAllString(code, "") + code = fbIsNullRe.ReplaceAllString(code, "COALESCE(") + code = fbGetDateRe.ReplaceAllString(code, "CURRENT_TIMESTAMP") + code = fbLenRe.ReplaceAllString(code, "CHAR_LENGTH(") + return fbTopRe.ReplaceAllStringFunc(code, func(s string) string { + m := fbTopRe.FindStringSubmatch(s) + return "SELECT FIRST " + m[2] + m[1] // в Firebird FIRST — до DISTINCT + }) + }) +} + +// fbOutsideLiterals — замены только в коде: строки, имена в кавычках и комментарии не трогаются; N перед +// строкой (N'…' — национальная строка T-SQL) убирается. +func fbOutsideLiterals(q string, fix func(string) string) string { + var b, code strings.Builder + emit := func(lit string) { + c := code.String() + code.Reset() + if strings.HasPrefix(lit, "'") && (strings.HasSuffix(c, "N") || strings.HasSuffix(c, "n")) && + (len(c) == 1 || !fbIdentChar(c[len(c)-2])) { + c = c[:len(c)-1] + } + b.WriteString(fix(c)) + b.WriteString(lit) + } + for i := 0; i < len(q); i++ { + c := q[i] + switch { + case c == '\'' || c == '"': + j := i + 1 + for j < len(q) { + if q[j] == c { + if j+1 < len(q) && q[j+1] == c { + j += 2 + continue + } + break + } + j++ + } + if j >= len(q) { + j = len(q) - 1 + } + emit(q[i : j+1]) + i = j + case c == '-' && i+1 < len(q) && q[i+1] == '-': + j := strings.IndexByte(q[i:], '\n') + if j < 0 { + j = len(q) - i + } + emit(q[i : i+j]) + i += j - 1 + case c == '/' && i+1 < len(q) && q[i+1] == '*': + j := strings.Index(q[i+2:], "*/") + end := len(q) + if j >= 0 { + end = i + 2 + j + 2 + } + emit(q[i:end]) + i = end - 1 + default: + code.WriteByte(c) + } + } + b.WriteString(fix(code.String())) + return b.String() +} + +// fbParams — @имя вне строк и комментариев заменяются на ?; возвращает имена по порядку (повторы — повторно). +func fbParams(q string) (string, []string) { + if !strings.Contains(q, "@") { + return q, nil + } + var b strings.Builder + var names []string + for i := 0; i < len(q); i++ { + c := q[i] + switch { + case c == '\'' || c == '"': // строка или имя в кавычках — как есть до закрывающей + j := i + 1 + for j < len(q) { + if q[j] == c { + if j+1 < len(q) && q[j+1] == c { + j += 2 + continue + } + break + } + j++ + } + if j >= len(q) { + j = len(q) - 1 + } + b.WriteString(q[i : j+1]) + i = j + case c == '-' && i+1 < len(q) && q[i+1] == '-': + j := strings.IndexByte(q[i:], '\n') + if j < 0 { + j = len(q) - i + } + b.WriteString(q[i : i+j]) + i += j - 1 + case c == '/' && i+1 < len(q) && q[i+1] == '*': + j := strings.Index(q[i+2:], "*/") + end := len(q) + if j >= 0 { + end = i + 2 + j + 2 + } + b.WriteString(q[i:end]) + i = end - 1 + case c == '@' && i+1 < len(q) && fbIdentStart(q[i+1]): + j := i + 1 + for j < len(q) && fbIdentChar(q[j]) { + j++ + } + names = append(names, q[i+1:j]) + b.WriteByte('?') + i = j - 1 + default: + b.WriteByte(c) + } + } + return b.String(), names +} + +func fbIdentStart(c byte) bool { return c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' } +func fbIdentChar(c byte) bool { return fbIdentStart(c) || c >= '0' && c <= '9' } + +// fbArgs — значения для ? по именам из текста: sql.Named — по имени, @p1…@pN без имени — по номеру. +// Без имён в тексте параметры передаются как есть (запрос уже с ?). +func fbArgs(names []string, args []driver.NamedValue) ([]driver.NamedValue, error) { + if len(names) == 0 { + out := make([]driver.NamedValue, len(args)) + for i, a := range args { + out[i] = driver.NamedValue{Ordinal: i + 1, Value: fbValue(a.Value)} + } + return out, nil + } + out := make([]driver.NamedValue, 0, len(names)) + for _, n := range names { + v, ok := fbArg(n, args) + if !ok { + return nil, fmt.Errorf("параметр @%s не передан", n) + } + out = append(out, driver.NamedValue{Ordinal: len(out) + 1, Value: fbValue(v)}) + } + return out, nil +} + +func fbArg(name string, args []driver.NamedValue) (driver.Value, bool) { + for _, a := range args { + if a.Name != "" && strings.EqualFold(a.Name, name) { + return a.Value, true + } + } + if len(name) > 1 && (name[0] == 'p' || name[0] == 'P') { + if n, err := strconv.Atoi(name[1:]); err == nil && n >= 1 { + for _, a := range args { + if a.Name == "" && a.Ordinal == n { + return a.Value, true + } + } + } + } + return nil, false +} + +// fbValue — GUID в верхнем регистре, время ISO 8601 — с пробелом вместо T (Firebird «T» не разбирает). +func fbValue(v driver.Value) driver.Value { + s, ok := v.(string) + if !ok { + return v + } + switch { + case fbGUIDRe.MatchString(s): + return strings.ToUpper(s) + case fbISOTimeRe.MatchString(s): + return strings.Replace(s, "T", " ", 1) + } + return v +} + +var fbErrLogged sync.Map + +// fbErr — ошибка Firebird; для разбора в журнал (раз на запрос) — исходный текст T-SQL и что ушло в Firebird. +func fbErr(orig, sent string, err error) error { + if err == nil { + return nil + } + var fe *firebirdsql.FbError + if errors.As(err, &fe) && fe.SQLCode == -104 { // синтаксис: перевод запроса не подошёл + if _, seen := fbErrLogged.LoadOrStore(orig, true); !seen { + log.Printf("depo-fb: запрос не принят Firebird: %v\n T-SQL: %s\n Firebird: %s", err, depoStmtHead(orig), sent) + } + } + return err +} diff --git a/internal/lkgateway/fancyfb_live_test.go b/internal/lkgateway/fancyfb_live_test.go new file mode 100644 index 0000000..6bd26f5 --- /dev/null +++ b/internal/lkgateway/fancyfb_live_test.go @@ -0,0 +1,137 @@ +package lkgateway + +// Живая проверка перевода запросов на Firebird: каждый запрос bj к DEPO (fsql) готовится (PREPARE, без +// выполнения) на настоящей базе Fancy. Запуск — с подключением к копии базы: +// BJ_FB_TEST_DSN='firebird://SYSDBA:пароль@127.0.0.1:3050/путь_к_базе?charset=WIN1251' go test -run FBLive ./internal/lkgateway + +import ( + "context" + "database/sql" + "database/sql/driver" + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" + "time" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" +) + +func readRepoFile(p string) ([]byte, error) { return os.ReadFile(filepath.Join("..", "..", p)) } + +// fbLiveTodo — личный кабинет и ручной ввод: на Firebird — следующий этап (YEAR(), %, сложение строк). +var fbLiveTodo = map[string]bool{"Arch02": true, "Arch03": true, "Arch07": true, "Arch08": true, "Arch10": true, "Hist02": true, "Man01": true} + +// fbLiveSkip — не к DEPO (PostgreSQL bj и экспорт QUIK, база МНП QUIK, SPECTRE) или куски составных запросов. +var fbLiveSkip = map[string]bool{ + "Doc02": true, "Doc03": true, "Doc04": true, "Sn01": true, "Sn02": true, "Sn03": true, "Sn04": true, "Sn05": true, + "On01": true, "On02": true, "On03": true, "Pr01": true, "Qk01": true, "Cl01": true, "Tbl01": true, "Tbl02": true, "QTb01": true, + "Ev05": true, // DEPONENT_ID — пишется, только если колонка есть (depoHasColumn) + "Que04": true, "Que05": true, + "Ev06": true, "Ev07": true, "Ev08": true, "Ev09": true, "Que01": true, "Que02": true, "Que03": true, "Flow01": true, "Flow02": true, +} + +// fbLiveQueries — строки-запросы пакета fsql по именам; шаблоны заполнены так, как их заполняет код. +func fbLiveQueries(t *testing.T) map[string]string { + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, "fsql", nil, 0) + if err != nil { + t.Fatal(err) + } + out := map[string]string{} + for _, p := range pkgs { + for _, f := range p.Files { + for _, d := range f.Decls { + g, ok := d.(*ast.GenDecl) + if !ok || g.Tok != token.VAR { + continue + } + for _, s := range g.Specs { + vs := s.(*ast.ValueSpec) + for i, n := range vs.Names { + if i >= len(vs.Values) { + continue + } + lit, ok := vs.Values[i].(*ast.BasicLit) + if !ok || lit.Kind != token.STRING || fbLiveSkip[n.Name] { + continue + } + q, err := strconv.Unquote(lit.Value) + if err != nil { + continue + } + q = strings.ReplaceAll(q, "{ACC}", "ВТНКЦ") + q = strings.ReplaceAll(q, "%d", "10") + q = strings.ReplaceAll(q, "%s", "1") + out[n.Name] = q + } + } + } + } + } + doc := "CAST(m.DOC AS varchar(32))" + out["Ev07+"] = fsql.Ev07 + doc + fsql.Ev08 + doc + fsql.Ev09 + doc + `) + FROM dbo.FTV_M2M m WHERE m.TimeNew > DATEADD(day,-45,GETDATE()) ORDER BY m.ID DESC` + out["Que01+"] = fsql.Que01 + "m.TimeNew" + `, ISNULL(` + doc + fsql.Que02 + doc + fsql.Que03 + doc + `) + FROM dbo.FTV_M2M m ORDER BY m.TimeNew DESC` + out["Flow01+"] = fsql.Flow01 + doc + fsql.Flow02 + out["Que04+"] = fsql.Que04 + "TimeNew DESC)" + out["Que05+"] = fsql.Que05 + "TimeNew DESC) ORDER BY e.EVENT_DATE" + out["Ev06+"] = fsql.Ev06 + "GUID, REFERENCE, ISIN, SEC_CODE, AMOUNT, PLACE_NUMBER, LINK_NUMBER, SHARE, DEPO_NUMBER, TimeNew) VALUES (@g, @r, @i, @c, @a, @p, @lnk, @sh, @acc, GETDATE())" + return out +} + +func TestFBLivePrepare(t *testing.T) { + dsn := os.Getenv("BJ_FB_TEST_DSN") + if dsn == "" { + t.Skip("BJ_FB_TEST_DSN не задан") + } + probe, err := sql.Open("firebirdsql", dsn) + if err != nil { + t.Fatal(err) + } + base, err := probe.Driver().(driver.DriverContext).OpenConnector(dsn) + if err != nil { + t.Fatal(err) + } + db := sql.OpenDB(fbConnector{base: base}) + defer db.Close() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + qs := fbLiveQueries(t) + names := make([]string, 0, len(qs)) + for n := range qs { + names = append(names, n) + } + sort.Strings(names) + var bad []string + todo := 0 + for _, n := range names { + st, err := db.PrepareContext(ctx, qs[n]) + if err != nil { + if fbLiveTodo[n] { + todo++ + t.Logf("(следующий этап) %s: %v", n, strings.ReplaceAll(err.Error(), "\n", " ")) + continue + } + bad = append(bad, fmt.Sprintf("%s: %v\n %s", n, err, depoStmtHead(fbSQL(qs[n])))) + continue + } + st.Close() + } + t.Logf("подготовлено %d из %d запросов, отложено %d", len(names)-len(bad)-todo, len(names), todo) + for _, b := range bad { + t.Log(b) + } + if len(bad) > 0 { + t.Fail() + } +} + +var _ driver.Connector = fbConnector{} diff --git a/internal/lkgateway/fancyfb_test.go b/internal/lkgateway/fancyfb_test.go new file mode 100644 index 0000000..9b0cd07 --- /dev/null +++ b/internal/lkgateway/fancyfb_test.go @@ -0,0 +1,117 @@ +package lkgateway + +import ( + "database/sql/driver" + "reflect" + "strings" + "testing" + + "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" +) + +func TestFBSQLMechanical(t *testing.T) { + cases := []struct{ in, want string }{ + {"SELECT TOP 1 ID FROM OD_VALUES WHERE ISIN=@i ORDER BY ID", "SELECT FIRST 1 ID FROM OD_VALUES WHERE ISIN=@i ORDER BY ID"}, + {"SELECT DISTINCT TOP (5000) PORT_NUM FROM OD_CLIENTS_PORTS", "SELECT FIRST (5000) DISTINCT PORT_NUM FROM OD_CLIENTS_PORTS"}, + {"SELECT ISNULL(STATE,'') FROM dbo.FTV_M2M_EVENTS WHERE GUID=@g", "SELECT COALESCE(STATE,'') FROM FTV_M2M_EVENTS WHERE GUID=@g"}, + {"INSERT INTO [dbo].FTV_M2M_EVENTS (EVENT_TYPE, EVENT_DATE) VALUES ('HANDBOOK', GETDATE())", "INSERT INTO FTV_M2M_EVENTS (EVENT_TYPE, EVENT_DATE) VALUES ('HANDBOOK', CURRENT_TIMESTAMP)"}, + // N'…' — без N; внутри строк и комментариев ничего не меняется + {"SELECT 1 FROM OD_FACES WHERE MNEM = N'НДЦ' AND NAME <> ' N' /* ISNULL( dbo.x */", "SELECT 1 FROM OD_FACES WHERE MNEM = 'НДЦ' AND NAME <> ' N' /* ISNULL( dbo.x */"}, + {"SELECT 'TOP 1 ISNULL(' FROM RDB$DATABASE", "SELECT 'TOP 1 ISNULL(' FROM RDB$DATABASE"}, + {"exec dbo.FTV_M2M_JOB", "EXECUTE PROCEDURE FTV_M2M_JOB"}, + {"SELECT OBJECT_ID('dbo.FTV_M2M')", "SELECT (SELECT FIRST 1 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'FTV_M2M') FROM RDB$DATABASE"}, + {fsql.Hlth03, fsql.FB[fsql.Hlth03]}, + } + for _, c := range cases { + if got := fbSQL(c.in); got != c.want { + t.Errorf("fbSQL(%q)\n got %q\nwant %q", c.in, got, c.want) + } + } +} + +func TestFBParamsAndArgs(t *testing.T) { + q, names := fbParams("SELECT @g, '@x', @r /* @y */, @g -- @z\nFROM T WHERE A=@p1") + if q != "SELECT ?, '@x', ? /* @y */, ? -- @z\nFROM T WHERE A=?" { + t.Fatalf("q = %q", q) + } + if !reflect.DeepEqual(names, []string{"g", "r", "g", "p1"}) { + t.Fatalf("names = %v", names) + } + args := []driver.NamedValue{ + {Name: "r", Ordinal: 1, Value: "ref"}, + {Name: "g", Ordinal: 2, Value: "bae1fa60-eb0a-4fee-b12c-018d1eafcbd2"}, + {Ordinal: 3, Value: int64(7)}, + {Name: "unused", Ordinal: 4, Value: "x"}, + } + // @p1 — первый параметр без имени (Ordinal 1 в порядке вызова): здесь его нет — ошибка + if _, err := fbArgs(names, args); err == nil { + t.Fatal("ожидалась ошибка: @p1 без позиционного параметра") + } + got, err := fbArgs([]string{"g", "r", "g"}, args) + if err != nil { + t.Fatal(err) + } + want := []any{"BAE1FA60-EB0A-4FEE-B12C-018D1EAFCBD2", "ref", "BAE1FA60-EB0A-4FEE-B12C-018D1EAFCBD2"} + for i, a := range got { + if a.Value != want[i] || a.Ordinal != i+1 { + t.Fatalf("arg %d = %+v, want %v", i, a, want[i]) + } + } + pos, err := fbArgs([]string{"p1", "p2"}, []driver.NamedValue{{Ordinal: 1, Value: "dbo.FTV_M2M"}, {Ordinal: 2, Value: "2026-10-27T15:00:00.000"}}) + if err != nil || pos[0].Value != "dbo.FTV_M2M" || pos[1].Value != "2026-10-27 15:00:00.000" { + t.Fatalf("позиционные: %+v %v", pos, err) + } +} + +func TestFBNativePassThrough(t *testing.T) { + ddl := "CREATE OR ALTER PROCEDURE X AS BEGIN CMT = ' N' || 'ISNULL(dbo.' ; END" + q, a, err := fbRewrite(ddl, nil) + if err != nil || q != ddl || a != nil { + t.Fatalf("DDL изменён: %q %v", q, err) + } + if !fbNative("/* шапка */\nEXECUTE BLOCK AS BEGIN END") || fbNative("EXECUTE PROCEDURE FTV_M2M_JOB") { + t.Fatal("fbNative") + } +} + +func TestFBStatements(t *testing.T) { + script := `/* шапка; с точкой с запятой */ +SET TERM ^ ; +EXECUTE BLOCK AS +BEGIN + IF (1 = 1) THEN EXECUTE STATEMENT 'CREATE TABLE T (A VARCHAR(10) DEFAULT ''x;y'')'; +END^ +COMMIT^ +CREATE OR ALTER TRIGGER T_BI FOR T BEFORE INSERT AS BEGIN NEW.A = 'a^b'; END^ +SET TERM ; ^ +CREATE OR ALTER EXCEPTION E 'Ошибка; текст'; +-- комментарий без команды +COMMIT; +` + got := fbStatements(script) + if len(got) != 3 { + t.Fatalf("команд %d: %q", len(got), got) + } + if !strings.HasPrefix(strings.TrimSpace(fbStripComments(got[0])), "EXECUTE BLOCK") || !strings.Contains(got[0], "''x;y''") { + t.Errorf("блок: %q", got[0]) + } + if !strings.Contains(got[1], "'a^b'") || !strings.HasPrefix(got[2], "CREATE OR ALTER EXCEPTION") { + t.Errorf("команды: %q", got) + } +} + +// Встроенные скрипты Firebird — те же, что в deploy/depo/fancy-m2m-firebird (их ставят вручную по README). +func TestFBScriptsMatchDeploy(t *testing.T) { + for _, n := range depoFBScriptFiles { + b, err := readRepoFile("deploy/depo/fancy-m2m-firebird/" + n) + if err != nil { + t.Fatal(err) + } + if string(b) != depoFBScript(n) { + t.Errorf("%s: встроенный текст отличается от deploy/depo/fancy-m2m-firebird — скопируйте изменения", n) + } + if len(fbStatements(depoFBScript(n))) == 0 { + t.Errorf("%s: нет команд", n) + } + } +} diff --git a/internal/lkgateway/fancygate.go b/internal/lkgateway/fancygate.go index 7a03401..92127e5 100644 --- a/internal/lkgateway/fancygate.go +++ b/internal/lkgateway/fancygate.go @@ -64,7 +64,7 @@ func depoIsLoginFailure(err error) bool { if errors.As(err, &pe) { return pe.Code == "28P01" || pe.Code == "28000" } - return false + return fbIsLoginFailure(err) } // fancyConnector — подключение к DEPO через паузу после неудачного входа. @@ -121,5 +121,8 @@ func fancyOpenGated(s FancyMSSQLSettings, dsn string) (*sql.DB, error) { if err != nil { return nil, err } + if s.IsFirebird() { // запросы bj (T-SQL) — в диалект Firebird + base = fbConnector{base: base} + } return sql.OpenDB(&fancyConnector{base: base, key: depoLoginKey(s), user: s.User, srv: s.Server}), nil } diff --git a/internal/lkgateway/fsql/depocatalog.go b/internal/lkgateway/fsql/depocatalog.go new file mode 100644 index 0000000..a56ff4d --- /dev/null +++ b/internal/lkgateway/fsql/depocatalog.go @@ -0,0 +1,25 @@ +package fsql + +// Системные запросы к базе DEPO (MSSQL): есть ли таблица, колонки, права логина bj, процедуры. +// Варианты для Firebird — в fb.go. + +// Cat01 — таблица видна логину bj (@p1 — dbo.ИМЯ). +var Cat01 = "SELECT CASE WHEN OBJECT_ID(@p1, 'U') IS NULL THEN 0 ELSE 1 END" + +// Cat02 — колонки таблицы (@p1 — dbo.ИМЯ). +var Cat02 = "SELECT name FROM sys.columns WHERE object_id = OBJECT_ID(@p1)" + +// Cat03 — у логина есть право @p2 (SELECT/INSERT/UPDATE/DELETE) на таблицу @p1. +var Cat03 = "SELECT ISNULL(HAS_PERMS_BY_NAME(@p1, 'OBJECT', @p2), 0)" + +// Cat04 — в таблице @t есть колонка @c. +var Cat04 = "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME=@t AND COLUMN_NAME=@c" + +// Cat05 — у логина есть право запускать процедуру @p. +var Cat05 = "SELECT COUNT(*) FROM fn_my_permissions(@p,'OBJECT') WHERE permission_name='EXECUTE'" + +// Cat06 — объект @n типа @t (U — таблица, P — процедура) есть в базе. +var Cat06 = "SELECT CASE WHEN OBJECT_ID(@n, @t) IS NULL THEN 0 ELSE 1 END" + +// Cat07 — пользователь базы @n (для EXECUTE AS в процедурах обработки). +var Cat07 = "SELECT USER_ID(@n)" diff --git a/internal/lkgateway/fsql/fb.go b/internal/lkgateway/fsql/fb.go new file mode 100644 index 0000000..d4952eb --- /dev/null +++ b/internal/lkgateway/fsql/fb.go @@ -0,0 +1,71 @@ +package fsql + +// FB — запросы к DEPO на Firebird (Fancy на Firebird 2.5, 27.10.2026): ключ — текст T-SQL, значение — текст +// Firebird с теми же @именами параметров. Здесь только то, что подключение к Firebird не переводит само +// (lkgateway/fancyfb.go переводит dbo., TOP, ISNULL, GETDATE(), N'…', exec): системные каталоги, LIKE с [_], +// оконные функции, OFFSET…FETCH, DATEADD с параметром. Параметр внутри функции — с CAST: Firebird иначе не +// знает его типа. +var FB = map[string]string{ + Cat01: `SELECT CASE WHEN EXISTS (SELECT 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = UPPER(REPLACE(CAST(@p1 AS VARCHAR(63)), 'dbo.', ''))) THEN 1 ELSE 0 END FROM RDB$DATABASE`, + + Cat02: `SELECT TRIM(RDB$FIELD_NAME) FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = UPPER(REPLACE(CAST(@p1 AS VARCHAR(63)), 'dbo.', ''))`, + + // права: SYSDBA и владелец таблицы — всё; иначе — выданное логину или PUBLIC (RDB$PRIVILEGE: S, I, U, D) + Cat03: `SELECT CASE WHEN CURRENT_USER = 'SYSDBA' + OR EXISTS (SELECT 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = UPPER(REPLACE(CAST(@p1 AS VARCHAR(63)), 'dbo.', '')) AND RDB$OWNER_NAME = CURRENT_USER) + OR EXISTS (SELECT 1 FROM RDB$USER_PRIVILEGES WHERE RDB$USER IN (CURRENT_USER, 'PUBLIC') + AND RDB$RELATION_NAME = UPPER(REPLACE(CAST(@p1 AS VARCHAR(63)), 'dbo.', '')) AND RDB$PRIVILEGE = SUBSTRING(CAST(@p2 AS VARCHAR(10)) FROM 1 FOR 1)) + THEN 1 ELSE 0 END FROM RDB$DATABASE`, + + Cat04: `SELECT COUNT(*) FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = UPPER(CAST(@t AS VARCHAR(63))) AND RDB$FIELD_NAME = UPPER(CAST(@c AS VARCHAR(63)))`, + + Cat05: `SELECT COUNT(*) FROM RDB$PROCEDURES p WHERE p.RDB$PROCEDURE_NAME = UPPER(REPLACE(CAST(@p AS VARCHAR(63)), 'dbo.', '')) + AND (CURRENT_USER = 'SYSDBA' OR p.RDB$OWNER_NAME = CURRENT_USER OR EXISTS (SELECT 1 FROM RDB$USER_PRIVILEGES u + WHERE u.RDB$USER IN (CURRENT_USER, 'PUBLIC') AND u.RDB$RELATION_NAME = p.RDB$PROCEDURE_NAME AND u.RDB$PRIVILEGE = 'X'))`, + + Cat06: `SELECT CASE WHEN EXISTS (SELECT 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = UPPER(REPLACE(CAST(@n AS VARCHAR(63)), 'dbo.', ''))) + OR EXISTS (SELECT 1 FROM RDB$PROCEDURES WHERE RDB$PROCEDURE_NAME = UPPER(REPLACE(CAST(@n AS VARCHAR(63)), 'dbo.', ''))) THEN 1 ELSE 0 END FROM RDB$DATABASE`, + + // в Firebird нет EXECUTE AS: процедуры работают правами, выданными им самим, — пользователь не нужен + Cat07: `SELECT 1 FROM RDB$DATABASE`, + + QEv09: `SELECT REFERENCE FROM FTV_M2M_LINES WHERE GUID=@g AND (CAST(@i AS VARCHAR(12)) = '' OR UPPER(ISIN) = CAST(@i AS VARCHAR(12))) ORDER BY ID`, + + QEv11: `SELECT FIRST 1 EVENT_DATE, COALESCE(STATE,''), COALESCE(COMMENT,'') FROM FTV_M2M_EVENTS + WHERE EVENT_TYPE='HANDBOOK' AND (COMMENT IS NOT NULL OR TRIM(COALESCE(STATE,'')) <> '') ORDER BY EVENT_DATE DESC`, + + Hlth01: `SELECT FIRST (@n) * FROM ( + SELECT CAST('FTV_M2M' AS VARCHAR(16)) AS T, ID, CAST(GUID AS VARCHAR(36)) AS G, CAST('' AS VARCHAR(36)) AS R, CAST(COALESCE(DIRECTION,'') AS VARCHAR(64)) AS K, COALESCE(TimeEnd, TimeNew) AS A, COALESCE(COMMENT,'') AS C + FROM FTV_M2M WHERE STATE='ERROR' AND COALESCE(TimeEnd, TimeNew) > DATEADD(DAY, -CAST(@d AS INTEGER), CURRENT_TIMESTAMP) + UNION ALL + SELECT 'FTV_M2M_EVENTS', ID, CAST(GUID AS VARCHAR(36)), COALESCE(REFERENCE,''), EVENT_TYPE, COALESCE(TimeEnd, TimeNew), COALESCE(COMMENT,'') + FROM FTV_M2M_EVENTS WHERE STATE='ERROR' AND EVENT_TYPE STARTING WITH 'BJ_' AND EVENT_TYPE <> 'BJ_PING' AND COALESCE(TimeEnd, TimeNew) > DATEADD(DAY, -CAST(@d AS INTEGER), CURRENT_TIMESTAMP) + ) x ORDER BY A DESC`, + + // COUNT(*) OVER () и OFFSET…FETCH в Firebird 2.5 нет: число — подзапросом, первая строка — FIRST 1 + Hlth02: `SELECT FIRST 1 (SELECT COUNT(*) FROM FTV_M2M_EVENTS e WHERE e.STATE='ERROR' AND e.EVENT_TYPE STARTING WITH 'BJ_' AND e.ID > @last AND COALESCE(e.TimeEnd, e.TimeNew) > DATEADD(DAY, -1, CURRENT_TIMESTAMP)), + ID, EVENT_TYPE, CAST(GUID AS VARCHAR(36)), COALESCE(COMMENT,'') + FROM FTV_M2M_EVENTS WHERE STATE='ERROR' AND EVENT_TYPE STARTING WITH 'BJ_' AND ID > @last AND COALESCE(TimeEnd, TimeNew) > DATEADD(DAY, -1, CURRENT_TIMESTAMP) + ORDER BY ID DESC`, + + Hlth03: `SELECT COALESCE(MAX(ID),0) FROM FTV_M2M_EVENTS WHERE EVENT_TYPE STARTING WITH 'BJ_'`, + + // SELECT без FROM в Firebird нет + QEvPosted: `SELECT (SELECT COUNT(*) FROM FTV_M2M WHERE GUID=@g), + (SELECT COUNT(*) FROM FTV_M2M_EVENTS WHERE GUID=@g AND EVENT_TYPE='BJ_ORDER_ACCEPTED' AND STATE='SUCCESS'), + (SELECT COUNT(*) FROM FTV_M2M_EVENTS WHERE GUID=@g AND EVENT_TYPE='BJ_ORDER_ACCEPTED' AND STATE<>'SUCCESS'), + COALESCE((SELECT FIRST 1 STATE FROM FTV_M2M WHERE GUID=@g ORDER BY ID DESC),''), + COALESCE((SELECT FIRST 1 COMMENT FROM FTV_M2M WHERE GUID=@g AND STATE='ERROR' ORDER BY ID DESC), + (SELECT FIRST 1 COMMENT FROM FTV_M2M_EVENTS WHERE GUID=@g AND EVENT_TYPE='BJ_ORDER_ACCEPTED' AND STATE='ERROR' ORDER BY ID DESC), '') + FROM RDB$DATABASE`, + + // PATINDEX в Firebird 2.5 нет: начало номера до первой цифры — по ближайшей из позиций цифр + Acc01: `SELECT FIRST 8 p, COUNT(*) FROM (SELECT LEFT(NUMBER, MINVALUE(POSITION('0' IN NUMBER || '0'), + COALESCE(NULLIF(POSITION('1' IN NUMBER), 0), 9999), COALESCE(NULLIF(POSITION('2' IN NUMBER), 0), 9999), + COALESCE(NULLIF(POSITION('3' IN NUMBER), 0), 9999), COALESCE(NULLIF(POSITION('4' IN NUMBER), 0), 9999), + COALESCE(NULLIF(POSITION('5' IN NUMBER), 0), 9999), COALESCE(NULLIF(POSITION('6' IN NUMBER), 0), 9999), + COALESCE(NULLIF(POSITION('7' IN NUMBER), 0), 9999), COALESCE(NULLIF(POSITION('8' IN NUMBER), 0), 9999), + COALESCE(NULLIF(POSITION('9' IN NUMBER), 0), 9999)) - 1) AS p + FROM OD_CLIENTS WHERE STATE=0 AND D_CLOSE IS NULL AND NUMBER IS NOT NULL) x + WHERE p <> '' GROUP BY p ORDER BY COUNT(*) DESC`, +} diff --git a/internal/lkgateway/letters.go b/internal/lkgateway/letters.go index c488dde..dc2e173 100644 --- a/internal/lkgateway/letters.go +++ b/internal/lkgateway/letters.go @@ -72,6 +72,12 @@ func letterFor(s Settings, key string) (Letter, bool) { case "dba": ip := pickStr(outboundIP(s.Fancy.Server, s.Fancy.Port), firstServerIP()) f := s.Fancy + if f.IsFirebird() { + return Letter{Key: key, Title: "Письмо администратору базы DEPO", To: "Администратор сервера Firebird учёта DEPO (Fancy)", + Subject: "Bridge&Join: подготовка базы DEPO на сервере " + orBracket(f.Server, "сервер DEPO"), + Body: fmt.Sprintf("Коллеги, добрый день!\n\nДля подключения системы Bridge&Join к учёту DEPO (Fancy на Firebird) нужно на сервере %s выполнить один скрипт %s под SYSDBA утилитой isql с кодировкой WIN1251 — команда запуска в начале скрипта (файл прилагается, он в кодировке WIN1251). Скрипт:\n— создаёт пользователя сервера Firebird для bj (%s) с паролем из скрипта; права у него — только выданные скриптом: чтение учётных таблиц, запись в таблицы обмена FTV_M2M*, запуск процедуры обработки FTV_M2M_JOB;\n— заводит этот логин в Fancy оператором-роботом (по нему Fancy нумерует документы) и кодировщик M2M, если его нет;\n— создаёт таблицы обмена FTV_M2M*, процедуры обработки FTV_M2M_* и заполняет привязки к справочникам Fancy (таблица FTV_M2M_SETTINGS); учётные данные не меняет.\nВ конце скрипт показывает привязки: пустые значения нужно вписать вручную.\nСкрипт можно запускать повторно. В нём пароль логина bj — после выполнения файл удалите.\n\nТакже нужно разрешить подключение к серверу Firebird (порт %d) с сервера Bridge&Join (адрес %s).%s", + orBracket(f.Server, "сервер DEPO"), depoSetupFileName(f), depoSetupLogin(f), f.DefaultPort(), orBracket(ip, "IP-адрес сервера bj"), sign)}, true + } what := "— создаёт задание SQL Server Agent «" + depoJobName(f) + "» для обработки обмена;\n" run := "под логином с правами sysadmin (SSMS или sqlcmd)" if f.IsPostgres() { diff --git a/internal/lkgateway/runtimeconfig.go b/internal/lkgateway/runtimeconfig.go index 711abba..e9d7bae 100644 --- a/internal/lkgateway/runtimeconfig.go +++ b/internal/lkgateway/runtimeconfig.go @@ -208,9 +208,9 @@ type PostgresSettings struct { // Fancy (очередь запросов из ЛК + ответы «крана») и журнал обмена с НРД. // Аутентификация — SQL-логин (SQL Server Authentication). type FancyMSSQLSettings struct { - DBType string `json:"db_type"` // "mssql" | "postgres" (пусто = mssql) + DBType string `json:"db_type"` // "mssql" | "postgres" | "firebird" (пусто = mssql) Server string `json:"server"` // host или host\instance - Port int `json:"port"` // 1433 (MSSQL) / 5432 (Postgres) + Port int `json:"port"` // 1433 (MSSQL) / 5432 (Postgres) / 3050 (Firebird) User string `json:"user"` // логин Password string `json:"password"` // пароль Encrypt string `json:"encrypt"` // MSSQL: disable|false|true; Postgres: sslmode @@ -261,6 +261,9 @@ type FancyMSSQLSettings struct { // IsPostgres сообщает, выбран ли PostgreSQL (иначе — MSSQL по умолчанию). func (s FancyMSSQLSettings) IsPostgres() bool { return s.DBType == "postgres" } +// IsFirebird — Fancy на Firebird (DepoDatabase — путь к файлу базы на сервере или псевдоним, fancyfb.go). +func (s FancyMSSQLSettings) IsFirebird() bool { return s.DBType == "firebird" } + // DefaultPort возвращает порт по типу СУБД, если он не задан явно. func (s FancyMSSQLSettings) DefaultPort() int { if s.Port != 0 { @@ -269,6 +272,9 @@ func (s FancyMSSQLSettings) DefaultPort() int { if s.IsPostgres() { return 5432 } + if s.IsFirebird() { + return 3050 + } return 1433 } diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index a352aaa..e39396f 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -380,7 +380,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) { // Есть ли у сервера SQL Agent: у Express его нет, у PostgreSQL — тоже; тогда обработку // запускает сам bj. Иначе по умолчанию джобу SQL Agent создаёт bj. d.DepoJobDef = "bj" - if !s.Fancy.IsPostgres() { + if !s.Fancy.IsPostgres() && !s.Fancy.IsFirebird() { ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second) d.DepoEdition, d.DepoAgent = depoServerEdition(ctx, s.Fancy) cancel() diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html index af8f376..8dd2476 100644 --- a/internal/lkgateway/web/templates/admin_wizard.html +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -140,12 +140,13 @@ {{$sub := .Sub}} {{if eq $sub "setup"}}
bj сверяет каждый перевод с учётом DEPO и сам создаёт в нём поручения. Четыре вопроса по порядку — из ответов bj соберёт подготовку базы.
- {{$job := .Settings.Fancy.DepoJobMode}}{{$pg := eq .Settings.Fancy.DBType "postgres"}} -