ЛК на Fancy с Firebird: все запросы к DEPO, курсы, база SPECTRE

ЛК (bj-lk берёт данные у bj-server) сам работает с той СУБД, что выбрана для DEPO.
- Перевод на Firebird: YEAR/MONTH/DAY(x) — EXTRACT, AS float — AS DOUBLE PRECISION (float T-SQL — 8 байт,
  FLOAT Firebird — 4: точность сумм и цен в ЛК); шаблоны со сложением строк (история операций Hist02, поиск
  клиента Man01) — Firebird-тексты до подстановки (depoSQL); UPPER по кириллице — в WIN1251 (база NONE).
- Курсы валют (Fx01): на Firebird запрос от всех курсов шёл больше 200 с (лимит 15 с — курсов нет) — для
  Firebird от 20 валют, 0,2 с.
- База SPECTRE (архив портфеля и операций ЛК): поле в «Трансферные таблицы DEPO»; у Firebird — путь или строка
  подключения Fancy (может быть на другом компьютере); не задана — ЛК пишет, где указать. На данных SPECTRE
  не проверено: базы нет.
- Тест FBLiveLK: функции ЛК, ручного ввода, сверки переводов и QUIK на копии базы по договору К1 Бубликова —
  все работают, 0,07–0,26 с каждая; FBLivePrepare — 101 из 101 запроса.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
fontvielle
2026-10-27 20:00:28 +03:00
parent 1d8bb0c032
commit 312e4f70f1
12 changed files with 301 additions and 10 deletions
+7 -1
View File
@@ -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 пока не
проверен.
## Установка вручную
+19
View File
@@ -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 = ""
+18 -1
View File
@@ -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
+9 -3
View File
@@ -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
}
}
+165
View File
@@ -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)
}
}
+44
View File
@@ -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'),
+6
View File
@@ -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))
+24 -1
View File
@@ -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 != "" {
+5 -2
View File
@@ -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
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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()
}
@@ -36,6 +36,8 @@
<input type="hidden" name="section" value="sec-fancy-depo-tables">
<label>База DEPO
<input type="text" name="depo_database" value="{{.Settings.Fancy.DepoDatabase}}" placeholder="{{if .Settings.Fancy.IsFirebird}}D:\Fancy\Bases\DEPO.GDB или 10.0.0.5:D:\Fancy\Bases\DEPO.GDB{{else}}{{.DepoTables.DB}}{{end}}" {{if not .Settings.Fancy.IsFirebird}}pattern="[A-Za-z][A-Za-z0-9_]*" {{end}}style="width:100%"></label>
<label>База брокерского учёта SPECTRE <span class="muted" style="font-weight:400">(архив портфеля и операций в личном кабинете{{if .Settings.Fancy.IsFirebird}}; путь к базе или строка подключения Fancy — SPECTRE может быть на другом компьютере{{end}})</span>
<input type="text" name="spectre_database" value="{{.Settings.Fancy.SpectreDatabase}}" placeholder="{{if .Settings.Fancy.IsFirebird}}D:\Fancy\Bases\SPECTRE.GDB или 10.0.0.5:D:\Fancy\Bases\SPECTRE.GDB{{else}}BJ_SPECTRE_DATA{{end}}" style="width:100%"></label>
<label>Признак ИИС в названии брокерского договора <span class="muted" style="font-weight:400">(с чего начинается номер договора ИИС в DEPO; несколько — через запятую)</span>
<input type="text" name="iis_contract_marker" value="{{.Settings.Fancy.IISContractMarker}}" placeholder="БРИИС" style="width:100%"></label>
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:600"><input type="radio" name="depo_tables_mode" value="auto" {{if eq .Settings.Fancy.DepoTablesMode "auto"}}checked{{end}}> <span>Создаёт bj <span class="muted" style="font-weight:400">— вводите логин с правами на создание таблиц в базе DEPO. Он используется один раз и не сохраняется; bj заодно выдаст права своему логину.</span></span></label>