ЛК на 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:
@@ -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 пока не
|
||||
проверен.
|
||||
|
||||
## Установка вручную
|
||||
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user