diff --git a/deploy/depo/fancy-m2m-firebird/README.md b/deploy/depo/fancy-m2m-firebird/README.md index ce8fa2d..ce0df5b 100644 --- a/deploy/depo/fancy-m2m-firebird/README.md +++ b/deploy/depo/fancy-m2m-firebird/README.md @@ -27,7 +27,13 @@ bj проверит базу и функции Fancy (UDF), создаст ло Он же — оператор 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` на копии базы клиента. + переводы — `fsql/fb.go`. Проверка — `go test -run FBLive` с `BJ_FB_TEST_DSN` на копии базы клиента: + `FBLivePrepare` готовит все запросы, `FBLiveLK` выполняет функции ЛК, ручного ввода и сверки переводов по + договору (`BJ_FB_TEST_CONTRACT`, `BJ_FB_TEST_PASSPORT`, `BJ_FB_TEST_FIO`, `BJ_FB_TEST_ACC`). +- Личный кабинет (модуль `bj-lk`) берёт данные у bj-server — с Firebird работает так же, отдельно ничего не + настраивается. Архив портфеля и операций читает базу брокерского учёта SPECTRE: у Firebird путь к ней задаётся + в «Обмен с DEPO» → «Трансферные таблицы DEPO» (не задан — ЛК пишет, где указать); на данных SPECTRE пока не + проверен. ## Установка вручную diff --git a/internal/lkgateway/fancydb_handlers.go b/internal/lkgateway/fancydb_handlers.go index 9cbf07d..1b7e854 100644 --- a/internal/lkgateway/fancydb_handlers.go +++ b/internal/lkgateway/fancydb_handlers.go @@ -102,6 +102,25 @@ func (h *setupHandlers) saveFancyDepoTables(w http.ResponseWriter, r *http.Reque if m := strings.TrimSpace(r.FormValue("iis_contract_marker")); len([]rune(m)) <= 100 { s.IISContractMarker = m } + // база брокерского учёта SPECTRE (архив портфеля и операций в ЛК); у Firebird — путь или строка подключения Fancy + if sp := strings.TrimSpace(r.FormValue("spectre_database")); sp == "" { + s.SpectreDatabase = "" + } else if s.IsFirebird() { + if _, _, path, err := fbSplitConn(sp); err != nil || !validFBPath(path) { + msg := "путь — без кавычек, ; ? и #" + if err != nil { + msg = err.Error() + } + setupFlash(w, r, "База SPECTRE: "+msg) + return + } + s.SpectreDatabase = sp + } else if validDBName(sp) { + s.SpectreDatabase = sp + } else { + setupFlash(w, r, "База SPECTRE: имя — только латинские буквы/цифры/_, начинать с буквы") + return + } mode := r.FormValue("depo_tables_mode") if mode != "auto" && mode != "script" { mode = "" diff --git a/internal/lkgateway/fancyfb.go b/internal/lkgateway/fancyfb.go index 632a4b0..3ef8402 100644 --- a/internal/lkgateway/fancyfb.go +++ b/internal/lkgateway/fancyfb.go @@ -5,7 +5,9 @@ package lkgateway // держать второй текст в каждом месте вызова, подключение к Firebird переводит запросы само (fbConn): // - точный текст Firebird — для запросов, которые механически не переводятся (fsql.FB, ключ — текст T-SQL); // - остальное механически, вне строк и комментариев: без dbo., TOP n — FIRST n, ISNULL — COALESCE, -// GETDATE() — CURRENT_TIMESTAMP, LEN — CHAR_LENGTH, N'…' — '…', «exec имя» — EXECUTE PROCEDURE имя; +// GETDATE() — CURRENT_TIMESTAMP, LEN — CHAR_LENGTH, YEAR(x) — EXTRACT(YEAR FROM x), AS float — AS DOUBLE +// PRECISION (float T-SQL — 8 байт, FLOAT Firebird — 4), N'…' — '…', «exec имя» — EXECUTE PROCEDURE имя; +// - шаблоны (fmt, {ACC}) с тем, что механически не переводится, выбираются до подстановки — depoSQL; // - именованные параметры @имя (sql.Named) — позиционные ? в порядке появления в тексте; // - GUID в параметрах — в верхнем регистре (в таблицах обмена Firebird GUID — строка в верхнем регистре, // uniqueidentifier MSSQL сравнивается без учёта регистра); время «2006-01-02T15:04:05» — с пробелом вместо T. @@ -254,11 +256,24 @@ var ( fbIsNullRe = regexp.MustCompile(`(?i)\bISNULL\s*\(`) fbGetDateRe = regexp.MustCompile(`(?i)\bGETDATE\s*\(\s*\)`) fbLenRe = regexp.MustCompile(`(?i)\bLEN\s*\(`) + fbDatePartRe = regexp.MustCompile(`(?i)\b(YEAR|MONTH|DAY)\s*\(\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\)`) + fbFloatRe = regexp.MustCompile(`(?i)\bAS\s+float\b`) 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+)?)?$`) ) +// depoSQL — текст запроса-шаблона для СУБД базы DEPO: для Firebird — перевод из fsql.FB, если он есть (шаблон с +// fmt-подстановкой или {ACC} подключение к Firebird по тексту уже не узнает). Остальное переводит fbConn. +func depoSQL(f FancyMSSQLSettings, q string) string { + if f.IsFirebird() { + if t, ok := fsql.FB[q]; ok { + return t + } + } + return q +} + // fbSQL — текст запроса для Firebird: точный перевод из fsql.FB, иначе механический. func fbSQL(q string) string { if t, ok := fsql.FB[q]; ok { @@ -275,6 +290,8 @@ func fbSQL(q string) string { code = fbIsNullRe.ReplaceAllString(code, "COALESCE(") code = fbGetDateRe.ReplaceAllString(code, "CURRENT_TIMESTAMP") code = fbLenRe.ReplaceAllString(code, "CHAR_LENGTH(") + code = fbDatePartRe.ReplaceAllString(code, "EXTRACT($1 FROM $2)") + code = fbFloatRe.ReplaceAllString(code, "AS DOUBLE PRECISION") // float T-SQL — 8 байт, FLOAT Firebird — 4 return fbTopRe.ReplaceAllStringFunc(code, func(s string) string { m := fbTopRe.FindStringSubmatch(s) return "SELECT FIRST " + m[2] + m[1] // в Firebird FIRST — до DISTINCT diff --git a/internal/lkgateway/fancyfb_live_test.go b/internal/lkgateway/fancyfb_live_test.go index 6bd26f5..d802b8e 100644 --- a/internal/lkgateway/fancyfb_live_test.go +++ b/internal/lkgateway/fancyfb_live_test.go @@ -14,6 +14,7 @@ import ( "go/token" "os" "path/filepath" + "regexp" "sort" "strconv" "strings" @@ -25,8 +26,10 @@ import ( 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} +// fbLiveTodo — запросы, которые на Firebird пока не готовятся (пусто — все). +var fbLiveTodo = map[string]bool{} + +var fbLiveFmtRe = regexp.MustCompile(`%(\[\d\])?s`) // fbLiveSkip — не к DEPO (PostgreSQL bj и экспорт QUIK, база МНП QUIK, SPECTRE) или куски составных запросов. var fbLiveSkip = map[string]bool{ @@ -66,9 +69,12 @@ func fbLiveQueries(t *testing.T) map[string]string { if err != nil { continue } + if t, ok := fsql.FB[q]; ok { // шаблон: Firebird-вариант выбирается до подстановки (depoSQL) + q = t + } q = strings.ReplaceAll(q, "{ACC}", "ВТНКЦ") q = strings.ReplaceAll(q, "%d", "10") - q = strings.ReplaceAll(q, "%s", "1") + q = fbLiveFmtRe.ReplaceAllString(q, "1") out[n.Name] = q } } diff --git a/internal/lkgateway/fancyfb_lk_live_test.go b/internal/lkgateway/fancyfb_lk_live_test.go new file mode 100644 index 0000000..818d998 --- /dev/null +++ b/internal/lkgateway/fancyfb_lk_live_test.go @@ -0,0 +1,165 @@ +package lkgateway + +// Живая проверка ЛК, ручного ввода и сверки переводов на Fancy на Firebird: функции выполняются на копии базы +// клиента по договору с бумагами. Запуск: +// BJ_FB_TEST_DSN='firebird://SYSDBA:пароль@127.0.0.1:3050/путь?charset=WIN1251' BJ_FB_TEST_CONTRACT='БР-1147' \ +// BJ_FB_TEST_ACC='Б' BJ_FB_TEST_PASSPORT='3453 289090' BJ_FB_TEST_FIO='Бубликов Павел Иванович' \ +// go test -run FBLiveLK -v ./internal/lkgateway + +import ( + "context" + "net/url" + "os" + "strconv" + "strings" + "testing" + "time" +) + +func fbLiveSettings(t *testing.T) FancyMSSQLSettings { + dsn := os.Getenv("BJ_FB_TEST_DSN") + if dsn == "" || os.Getenv("BJ_FB_TEST_CONTRACT") == "" { + t.Skip("BJ_FB_TEST_DSN / BJ_FB_TEST_CONTRACT не заданы") + } + u, err := url.Parse(dsn) + if err != nil { + t.Fatal(err) + } + pass, _ := u.User.Password() + port, _ := strconv.Atoi(u.Port()) + return FancyMSSQLSettings{DBType: "firebird", Server: u.Hostname(), Port: port, User: u.User.Username(), Password: pass, + DepoDatabase: strings.TrimPrefix(u.Path, "/"), AccountPrefix: os.Getenv("BJ_FB_TEST_ACC")} +} + +func TestFBLiveLK(t *testing.T) { + s := fbLiveSettings(t) + rc, err := NewRuntimeConfig(t.TempDir() + "/setup.json") + if err != nil { + t.Fatal(err) + } + if err := rc.UpdateFancy(s); err != nil { + t.Fatal(err) + } + prev := globalRC + globalRC = rc + defer func() { globalRC = prev }() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + contract := os.Getenv("BJ_FB_TEST_CONTRACT") + last := time.Now() + check := func(what string, err error, n int, sample any) { + took := time.Since(last) + last = time.Now() + if took > 5*time.Second { // ЛК ждёт ответа: на MSSQL эти чтения — доли секунды + t.Errorf("%s: %v — медленно", what, took.Round(time.Millisecond)) + } + if err != nil { + t.Errorf("%s: %v", what, err) + return + } + t.Logf("%s (%v): %d — %+v", what, took.Round(time.Millisecond), n, sample) + } + first := func(n int, v any) any { + if n == 0 { + return nil + } + return v + } + + name, ok := depoContractExists(ctx, s, contract) + t.Logf("depoContractExists: %v %q", ok, name) + if !ok { + t.Errorf("договор %s не найден", contract) + } + pos, err := depoClientPositions(ctx, s, contract) + check("depoClientPositions", err, len(pos), pos) + lst, err := depoListContracts(ctx, s, "", 5) + check("depoListContracts", err, len(lst), first(len(lst), lst)) + fnd, err := depoFindContracts(ctx, s, contract[:2], 5) + check("depoFindContracts", err, len(fnd), fnd) + t.Logf("depoSuggestContracts: %v", depoSuggestContracts(ctx, s)) + + ids, face, accs, err := depoClientIDs(ctx, s, contract) + check("depoClientIDs", err, len(ids), map[string]any{"face": face, "accounts": accs}) + from, to := time.Now().AddDate(-1, 0, 0).Format("2006-01-02"), time.Now().Format("2006-01-02") + ops, err := lkOperations(ctx, s, contract, from, to, 50) + check("lkOperations", err, len(ops), first(len(ops), ops)) + inc, tax, err := lkIncome(ctx, s, contract, from, to) + check("lkIncome", err, len(inc), map[string]any{"tax": tax}) + + if s.SpectreDatabase == "" { // архив ЛК — из базы брокерского учёта SPECTRE; без неё — понятная ошибка + _, err := lkArchivePortfolio(ctx, s, contract, time.Now()) + t.Logf("lkArchivePortfolio без SPECTRE: %v", err) + last = time.Now() + } else { + ap, err := lkArchivePortfolio(ctx, s, contract, time.Now()) + check("lkArchivePortfolio", err, 1, ap) + aops, err := lkArchiveOperations(ctx, s, contract, time.Now().AddDate(-1, 0, 0), time.Now()) + check("lkArchiveOperations", err, len(aops), first(len(aops), aops)) + } + + var isins []string + var lkpos []LKPosition + for _, p := range pos { + isins = append(isins, p.ISIN) + lkpos = append(lkpos, LKPosition{ISIN: p.ISIN, Name: p.SecurityName, Quantity: p.Quantity}) + } + ca, err := lkCorpActions(ctx, s, isins) + check("lkCorpActions", err, len(ca), first(len(ca), ca)) + docs, err := lkReadyDocuments(ctx, s, contract) + check("lkReadyDocuments", err, len(docs), first(len(docs), docs)) + up := lkUpcoming(ctx, s, lkpos, 400) + check("lkUpcoming", nil, len(up), first(len(up), up)) + fx := lkFxAccounting(ctx, s) + check("lkFxAccounting", nil, len(fx), fx) + if len(fx) == 0 { + t.Error("lkFxAccounting: нет курсов валют") + } + for _, isin := range isins { + card, err := securityCard(ctx, rc, isin, nil) + check("securityCard "+isin, err, 1, card) + } + + cur, cands, err := depoAccPrefixInfo(ctx, s) + check("depoAccPrefixInfo", err, cur, cands) + if req, err := depoDetectRequisites(ctx, s); err != nil { + t.Errorf("depoDetectRequisites: %v", err) + } else { + t.Logf("depoDetectRequisites: %+v", *req) + } + ver, upd, err := depoVersion(ctx, s) + t.Logf("depoVersion: %q %q %v", ver, upd, err) + + fio := strings.Fields(os.Getenv("BJ_FB_TEST_FIO")) + pp := strings.Fields(os.Getenv("BJ_FB_TEST_PASSPORT")) + if len(fio) == 3 && len(pp) == 2 { + faces, msg := manualDepoSearch(rc, fio[0]) + t.Logf("manualDepoSearch(%s): %d %s — %+v", fio[0], len(faces), msg, first(len(faces), faces)) + if len(faces) == 0 { + t.Errorf("manualDepoSearch: клиент %s не найден (%s)", fio[0], msg) + } + if len(pos) > 0 { + id, msg := manualDepoClientByAccount(rc, pos[0].AccountNum) + t.Logf("manualDepoClientByAccount(%s): %d %s", pos[0].AccountNum, id, msg) + mp, msg := manualDepoPositions(rc, id) + t.Logf("manualDepoPositions: %d %s — %+v", len(mp), msg, first(len(mp), mp)) + } + var secs []IncomingSecurity + var outs []depoOutSec + for i, p := range pos { + ref := "FBREF" + strconv.Itoa(i) + secs = append(secs, IncomingSecurity{ReferenceID: ref, ISIN: p.ISIN, Quantity: "1"}) + outs = append(outs, depoOutSec{ReferenceID: ref, ISIN: p.ISIN, Qty: "1"}) + } + in := runDepoCheck(&IncomingTransfer{InvestorLast: fio[0], InvestorFirst: fio[1], InvestorMiddle: fio[2], DocSeries: pp[0], DocNumber: pp[1], Securities: secs}) + t.Logf("runDepoCheck (зачисление): ran=%v err=%q investor=%q account=%q %s — %s", in.Ran, in.Err, in.Investor, in.Account, in.Recommend, in.Summary) + out := runDepoCheckOut(fio[0], fio[1], fio[2], pp[0], pp[1], outs, "", "") + t.Logf("runDepoCheckOut (списание): ran=%v err=%q investor=%q account=%q %s — %s", out.Ran, out.Err, out.Investor, out.Account, out.Recommend, out.Summary) + if in.Err != "" || out.Err != "" || in.Investor == "" || out.Investor == "" || in.Account == "" || out.Account == "" { + t.Errorf("сверка переводов: зачисление %q / списание %q, инвестор %q / %q", in.Err, out.Err, in.Investor, out.Investor) + } + code, contr, err := quikResolveClient(ctx, rc, pp[0], pp[1]) + t.Logf("quikResolveClient: %q %q %v", code, contr, err) + } +} diff --git a/internal/lkgateway/fsql/fb.go b/internal/lkgateway/fsql/fb.go index d4952eb..35a957b 100644 --- a/internal/lkgateway/fsql/fb.go +++ b/internal/lkgateway/fsql/fb.go @@ -50,6 +50,50 @@ var FB = map[string]string{ Hlth03: `SELECT COALESCE(MAX(ID),0) FROM FTV_M2M_EVENTS WHERE EVENT_TYPE STARTING WITH 'BJ_'`, + // сложение строк (+) — ||; UNION строк разной длины — CAST; параметр в DATEADD — CAST + Hist02: `SELECT FIRST %d * FROM ( + SELECT d.R_DATE AS RD, d.PROC_DATE AS PD, COALESCE(d.NUM,'') AS NUM, d.STATE AS ST, CAST('зачисление' AS VARCHAR(32)) AS KIND, + z.DEPO AS ACC, COALESCE(v.ISIN,'') AS ISIN, COALESCE(v.NAME,'') AS SEC, CAST(COALESCE(z.AMOUNT,0) AS DOUBLE PRECISION) AS QTY, + CAST(TRIM(COALESCE(z.COMMENT,'') || ' ' || COALESCE(z.REJECT_REASON,'')) AS VARCHAR(600)) AS NOTE + FROM U_OP_INV_ZACH z JOIN OD_DOCS d ON d.ID = z.DOC LEFT JOIN OD_VALUES v ON v.ID = z.SHARE + WHERE z.DEPO IN (%s) + UNION ALL + SELECT d.R_DATE, d.PROC_DATE, COALESCE(d.NUM,''), d.STATE, 'списание', + p.DEPO, COALESCE(v.ISIN,''), COALESCE(v.NAME,''), -CAST(COALESCE(p.AMOUNT,0) AS DOUBLE PRECISION), + TRIM(COALESCE(p.COMMENT,'') || ' ' || COALESCE(p.REJECT_REASON,'')) + FROM U_OP_INV_SPIS p JOIN OD_DOCS d ON d.ID = p.DOC LEFT JOIN OD_VALUES v ON v.ID = p.SHARE + WHERE p.DEPO IN (%s) + UNION ALL + SELECT d.R_DATE, d.PROC_DATE, COALESCE(d.NUM,''), d.STATE, 'перевод: списание', + t.D_DEPO, COALESCE(v.ISIN,''), COALESCE(v.NAME,''), -CAST(COALESCE(t.AMOUNT,0) AS DOUBLE PRECISION), + TRIM(COALESCE(t.COMMENT,'') || ' ' || COALESCE(t.REJECT_REASON,'')) + FROM U_OP_INV_PEREVOD t JOIN OD_DOCS d ON d.ID = t.DOC LEFT JOIN OD_VALUES v ON v.ID = t.SHARE + WHERE t.D_DEPO IN (%s) + UNION ALL + SELECT d.R_DATE, d.PROC_DATE, COALESCE(d.NUM,''), d.STATE, 'перевод: зачисление', + t.K_DEPO, COALESCE(v.ISIN,''), COALESCE(v.NAME,''), CAST(COALESCE(t.AMOUNT,0) AS DOUBLE PRECISION), + TRIM(COALESCE(t.COMMENT,'') || ' ' || COALESCE(t.REJECT_REASON,'')) + FROM U_OP_INV_PEREVOD t JOIN OD_DOCS d ON d.ID = t.DOC LEFT JOIN OD_VALUES v ON v.ID = t.SHARE + WHERE t.K_DEPO IN (%s)) x + WHERE x.RD >= @p1 AND x.RD < DATEADD(DAY, 1, CAST(@p2 AS TIMESTAMP)) + ORDER BY x.RD DESC, x.NUM DESC`, + + // сложение строк — ||; UPPER по кириллице — в WIN1251 (база Fancy — NONE, там UPPER меняет только латиницу) + Man01: `SELECT FIRST 20 COALESCE(NULLIF(f.SELF_ID,0),f.ID), COALESCE(f.NAME,''), f.DATE_BIRTH, COALESCE(f.DOC_TYPE,0), COALESCE(f.DOC_SER,''), COALESCE(f.DOC_NUM,''), + f.DOC_DAT, COALESCE(f.DOC_PLACE,''), COALESCE(f.DOC_CODE,''), COALESCE(f.PHONE,'') + FROM OD_FACES f + WHERE COALESCE(f.LAST_FLAG,1)=1 AND (UPPER(CAST(f.NAME AS VARCHAR(255) CHARACTER SET WIN1251)) LIKE UPPER(CAST(@q AS VARCHAR(255) CHARACTER SET WIN1251)) + OR f.DOC_NUM=@d OR REPLACE(COALESCE(f.DOC_SER,''),' ','') || f.DOC_NUM=@d) + AND EXISTS (SELECT 1 FROM OD_CLIENTS c WHERE c.FACE=COALESCE(NULLIF(f.SELF_ID,0),f.ID) AND c.NUMBER LIKE '{ACC}%' AND c.STATE=0 AND c.D_CLOSE IS NULL) + ORDER BY f.NAME`, + + // последние курсы валют: от 20 валют, а не от всех курсов — Firebird иначе перебирает миллионы строк + // с подзапросом на каждую (на MSSQL — доли секунды, на Firebird — больше 200 с) + Fx01: `SELECT UPPER(TRIM(v.SYSNAME)), + (SELECT FIRST 1 x.OFICDATE FROM OD_VALUES_RATES x WHERE x.VALUE_ID = v.ID AND x.MARKET = 0 AND x.RATE > 0 ORDER BY x.OFICDATE DESC), + (SELECT FIRST 1 CAST(x.RATE AS DOUBLE PRECISION) FROM OD_VALUES_RATES x WHERE x.VALUE_ID = v.ID AND x.MARKET = 0 AND x.RATE > 0 ORDER BY x.OFICDATE DESC) + FROM OD_VALUES v WHERE v.V_TYPE = 1`, + // 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'), diff --git a/internal/lkgateway/lkarchive.go b/internal/lkgateway/lkarchive.go index c232487..acaeab1 100644 --- a/internal/lkgateway/lkarchive.go +++ b/internal/lkgateway/lkarchive.go @@ -42,6 +42,9 @@ type spectreRest struct { // lkArchivePortfolio — портфель на дату по SPECTRE. date — в местном времени, конец дня включительно. func lkArchivePortfolio(ctx context.Context, s FancyMSSQLSettings, contract string, date time.Time) (*LKPortfolio, error) { + if err := spectreMissing(s); err != nil { + return nil, err + } c, ok := spectreContractFor(ctx, s, contract) if !ok { return nil, fmt.Errorf("договор %s в брокерском учёте не найден", lkContractNumber(contract)) @@ -329,6 +332,9 @@ func lkArchiveOperations(ctx context.Context, s FancyMSSQLSettings, contract str } func lkArchiveOperationsLoad(ctx context.Context, s FancyMSSQLSettings, contract string, from, to time.Time) ([]LKOperationRow, error) { + if err := spectreMissing(s); err != nil { + return nil, err + } c, ok := spectreContractFor(ctx, s, contract) if !ok { return nil, fmt.Errorf("договор %s в брокерском учёте не найден", lkContractNumber(contract)) diff --git a/internal/lkgateway/lkclient.go b/internal/lkgateway/lkclient.go index 9810537..3b408d7 100644 --- a/internal/lkgateway/lkclient.go +++ b/internal/lkgateway/lkclient.go @@ -9,6 +9,7 @@ package lkgateway import ( "context" "database/sql" + "fmt" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" "strings" "sync" @@ -50,12 +51,34 @@ var spectreContractCache = struct { at map[string]time.Time }{m: map[string]spectreContract{}, at: map[string]time.Time{}} -// spectreOpen — подключение к копии SPECTRE тем же логином, что и к DEPO. +// spectreOpen — подключение к копии SPECTRE тем же логином, что и к DEPO. Firebird: база SPECTRE — свой файл, +// путь или строка подключения Fancy (может быть на другом компьютере, чем DEPO). func spectreOpen(s FancyMSSQLSettings) (*sql.DB, error) { + if err := spectreMissing(s); err != nil { + return nil, err + } db := strings.TrimSpace(s.SpectreDB()) + if s.IsFirebird() { + host, port, path, err := fbSplitConn(db) + if err != nil { + return nil, err + } + if host != "" { + s.Server, s.Port = host, port + } + db = path + } return fancyOpen(s, db) } +// spectreMissing — у Firebird имени базы SPECTRE по умолчанию нет: путь к ней задают в настройке. +func spectreMissing(s FancyMSSQLSettings) error { + if s.IsFirebird() && strings.TrimSpace(s.SpectreDatabase) == "" { + return fmt.Errorf("база брокерского учёта SPECTRE не указана — Настройка → «Обмен с DEPO», карточка «Трансферные таблицы DEPO»") + } + return nil +} + // SpectreDB — база SPECTRE рядом с DEPO: по умолчанию BJ_SPECTRE_DATA. func (s FancyMSSQLSettings) SpectreDB() string { if v := strings.TrimSpace(s.SpectreDatabase); v != "" { diff --git a/internal/lkgateway/lkfx.go b/internal/lkgateway/lkfx.go index 2484827..1a5a679 100644 --- a/internal/lkgateway/lkfx.go +++ b/internal/lkgateway/lkfx.go @@ -70,8 +70,11 @@ func lkFxAccounting(ctx context.Context, s FancyMSSQLSettings) map[string]LKFxRa if strings.TrimSpace(s.Server) == "" { return out } - for _, db := range []string{s.DepoDB(), s.SpectreDB()} { - conn, err := fancyOpen(s, db) + for _, open := range []func() (*sql.DB, error){ + func() (*sql.DB, error) { return fancyOpen(s, s.DepoDB()) }, + func() (*sql.DB, error) { return spectreOpen(s) }, + } { + conn, err := open() if err != nil { continue } diff --git a/internal/lkgateway/lkhistory.go b/internal/lkgateway/lkhistory.go index 8832d6f..d5ade88 100644 --- a/internal/lkgateway/lkhistory.go +++ b/internal/lkgateway/lkhistory.go @@ -136,7 +136,7 @@ func lkOperations(ctx context.Context, s FancyMSSQLSettings, contract, from, to } fromT, toT := parseRange(from, to) // Три вида операций в одном списке: зачисление, списание, перевод между счетами. - q := fmt.Sprintf(fsql.Hist02, limit, inClause(ids), inClause(ids), inClause(ids), inClause(ids)) + q := fmt.Sprintf(depoSQL(s, fsql.Hist02), limit, inClause(ids), inClause(ids), inClause(ids), inClause(ids)) rows, err := conn.QueryContext(cctx, q, fromT, toT) if err != nil { return nil, err diff --git a/internal/lkgateway/manual_depo.go b/internal/lkgateway/manual_depo.go index 60f992d..67283fb 100644 --- a/internal/lkgateway/manual_depo.go +++ b/internal/lkgateway/manual_depo.go @@ -73,7 +73,7 @@ func manualDepoSearch(rc *RuntimeConfig, q string) ([]manualDepoFace, string) { // Действующая версия карточки (LAST_FLAG=1); счета привязаны к исходной записи (SELF_ID). // likeEscape: метасимволы LIKE (% _ [) в имени от пользователя означают сами себя, // иначе поиск «расползается» по всей базе (wildcard-injection). - rows, err := db.QueryContext(ctx, accSQL(fsql.Man01), sql.Named("q", likeEscape(q)+"%"), sql.Named("d", digits)) + rows, err := db.QueryContext(ctx, accSQL(depoSQL(rc.Snapshot().Fancy, fsql.Man01)), sql.Named("q", likeEscape(q)+"%"), sql.Named("d", digits)) if err != nil { return nil, "поиск в DEPO: " + err.Error() } diff --git a/internal/lkgateway/web/templates/part_fancy.html b/internal/lkgateway/web/templates/part_fancy.html index 0d274e5..b3b3bc9 100644 --- a/internal/lkgateway/web/templates/part_fancy.html +++ b/internal/lkgateway/web/templates/part_fancy.html @@ -36,6 +36,8 @@ +