Files
Bridge-and-Join-s/internal/lkgateway/lkdocs.go
T
zuevav 7ce4c2c79f Защита рецептов: SQL DEPO/SPECTRE/QUIK вынесен в пакет fsql под garble
Реализация варианта B из аудита защиты знаний. Прямой garble всего bj-server
ломает html/template (он обращается к полям структур по имени, garble их
переименовывает). Решение — селективная обфускация: GOGARBLE обфусцирует ТОЛЬКО
пакет с секретами, шаблоны и обработчики не затрагиваются.

95 SQL-запросов к DEPO/SPECTRE/QUIK и трансферным таблицам FTV_M2M вынесены из
lkgateway в отдельный пакет internal/lkgateway/fsql как переменные (именно var,
не const: const компилятор вставил бы обратно в необфусцируемый пакет, и
шифрование бы не сработало — проверено). Сами обработчики, типы и параметры
sql.Named остались на месте; пакет держит только строки запросов.

build-release.sh собирает bj-server с `GOGARBLE=.../fsql garble -literals`
(нужен отдельный SDK go1.26 — модуль-кэшный тулчейн garble не подходит); если
garble/go1.26 не найдены — обычная сборка с предупреждением.

Проверено на реальном garble-бинаре (запускается, /admin и страницы на
структурах работают): рецепт чтения чужих систем из бинаря исчез —
OD_RESTS 13→0, OD_WIRING 5→0, OD_TURNS 1→0, OD_BALANCES 3→0, D_B_CONTRACTS 1→0,
OD_CLIENTS_PORTS 10→0, DepoLimits/MoneyLimits →0. Остаток FTV_M2M — упоминания
имён таблиц в операторских embed-шаблонах и логах, не сам рецепт.

Стенд собран обычной сборкой (без garble) — трассировки остаются читаемыми.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
2026-09-18 20:29:47 +03:00

256 lines
9.4 KiB
Go

package lkgateway
// lkdocs.go — корпоративные действия по бумагам клиента и документы депозитария:
// готовые выписки из учётной системы (U_OP_INF_EXTRACT) плюс заявки клиента на
// документ, которые кабинет кладёт в схему lk и показывает сотрудникам.
//
// Кабинет не создаёт документы в учётной системе: заявка — это поручение сотруднику,
// он готовит документ у себя и выкладывает клиенту.
import (
"context"
"database/sql"
"fmt"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
"strings"
"time"
)
// LKCorpAction — корпоративное действие по бумаге клиента.
type LKCorpAction struct {
FixDate string `json:"fix_date,omitempty"` // дата фиксации
OperDate string `json:"oper_date,omitempty"` // дата операции
Kind string `json:"kind"` // расшифровка вида
Code string `json:"code,omitempty"` // код вида в учёте
ISIN string `json:"isin,omitempty"`
Security string `json:"security,omitempty"`
Note string `json:"note,omitempty"`
}
// caKindRu — вид корпоративного действия по коду учёта (коды ISO 20022).
func caKindRu(code string) string {
switch strings.ToUpper(strings.TrimSpace(code)) {
case "DVCA":
return "дивиденды"
case "INTR":
return "выплата купона"
case "REDM":
return "погашение"
case "MCAL":
return "досрочное погашение"
case "PCAL":
return "частичное погашение"
case "BPUT":
return "оферта: предъявление к выкупу"
case "TEND":
return "добровольное предложение о выкупе"
case "MEET", "XMET", "OMET":
return "собрание владельцев"
case "CONV":
return "конвертация"
case "SPLF", "SPLR":
return "дробление или консолидация"
case "MRGR":
return "реорганизация"
case "EXOF":
return "обмен бумаг"
case "DRCA":
return "выплата по депозитарной расписке"
}
return "корпоративное действие"
}
// lkCorpActions — ближайшие корпоративные действия по бумагам клиента.
func lkCorpActions(ctx context.Context, s FancyMSSQLSettings, isins []string) ([]LKCorpAction, error) {
if strings.TrimSpace(s.Server) == "" || len(isins) == 0 {
return nil, nil
}
conn, err := fancyOpen(s, s.DepoDB())
if err != nil {
return nil, err
}
defer conn.Close()
cctx, cancel := context.WithTimeout(ctx, 25*time.Second)
defer cancel()
// Список ISIN подставляем параметрами: значения приходят из портфеля клиента.
names := make([]string, 0, len(isins))
args := make([]any, 0, len(isins))
for i, isin := range isins {
names = append(names, fmt.Sprintf("@p%d", i+1))
args = append(args, sql.Named(fmt.Sprintf("p%d", i+1), strings.ToUpper(strings.TrimSpace(isin))))
}
q := fmt.Sprintf(fsql.Doc01, strings.Join(names, ","))
rows, err := conn.QueryContext(cctx, q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var out []LKCorpAction
for rows.Next() {
var code, isin, name, offer string
var fix, oper sql.NullTime
if rows.Scan(&code, &isin, &name, &fix, &oper, &offer) != nil {
continue
}
a := LKCorpAction{Code: strings.TrimSpace(code), Kind: caKindRu(code), ISIN: isin, Security: name, Note: strings.TrimSpace(offer)}
if fix.Valid && fix.Time.Year() < 9000 {
a.FixDate = fix.Time.Format("02.01.2006")
}
if oper.Valid && oper.Time.Year() > 2000 && oper.Time.Year() < 9000 {
a.OperDate = oper.Time.Format("02.01.2006")
}
out = append(out, a)
}
return out, rows.Err()
}
// LKDocument — документ депозитария, готовый или заказанный клиентом.
type LKDocument struct {
ID int64 `json:"id,omitempty"`
Date string `json:"date"` // дата готовности или заказа
Title string `json:"title"` // что за документ
Period string `json:"period,omitempty"` // за какой период
Account string `json:"account,omitempty"`
State string `json:"state"` // готов | в работе | отклонён
Note string `json:"note,omitempty"` // комментарий депозитария
Ordered bool `json:"ordered"` // заявка клиента (а не готовый документ учёта)
Kind string `json:"kind,omitempty"` // вид заявки
}
// lkDocSchema — таблица заявок на документы (создаётся вместе со схемой lk).
var lkDocSchema = fsql.Doc02
// lkDocKinds — виды документов, которые клиент может заказать в кабинете.
var lkDocKinds = []string{
"Выписка по счёту депо",
"Справка об остатках на дату",
"Отчёт об операциях за период",
"Справка о доходах",
}
// lkDocKindAllowed — вид заявки из списка (чужие значения не принимаем).
func lkDocKindAllowed(kind string) bool {
for _, k := range lkDocKinds {
if k == kind {
return true
}
}
return false
}
// lkDocRequestAdd — заявка клиента на документ.
func lkDocRequestAdd(ctx context.Context, rc *RuntimeConfig, contract, clientName, kind, from, to, account, comment string) error {
if !lkDocKindAllowed(kind) {
return fmt.Errorf("неизвестный вид документа")
}
db, err := lkDB(rc)
if err != nil {
return err
}
var fromP, toP any
if t, err := time.Parse("2006-01-02", strings.TrimSpace(from)); err == nil {
fromP = t
}
if t, err := time.Parse("2006-01-02", strings.TrimSpace(to)); err == nil {
toP = t
}
_, err = db.ExecContext(ctx, fsql.Doc03, contract, clientName, kind, fromP, toP, strings.TrimSpace(account), strings.TrimSpace(comment))
return err
}
// lkDocRequests — заявки клиента на документы.
func lkDocRequests(ctx context.Context, rc *RuntimeConfig, contract string) ([]LKDocument, error) {
db, err := lkDB(rc)
if err != nil {
return nil, err
}
rows, err := db.QueryContext(ctx, fsql.Doc04, contract)
if err != nil {
return nil, err
}
defer rows.Close()
var out []LKDocument
for rows.Next() {
var d LKDocument
var created time.Time
var from, to *time.Time
var status string
if err := rows.Scan(&d.ID, &created, &d.Kind, &from, &to, &d.Account, &status, &d.Note); err != nil {
continue
}
d.Ordered, d.Title, d.Date = true, d.Kind, created.Format("02.01.2006")
switch status {
case "done":
d.State = "готов"
case "rejected":
d.State = "отклонён"
default:
d.State = "в работе"
}
if from != nil && to != nil {
d.Period = from.Format("02.01.2006") + " — " + to.Format("02.01.2006")
}
out = append(out, d)
}
return out, rows.Err()
}
// lkReadyDocuments — готовые выписки по счетам клиента из учётной системы.
func lkReadyDocuments(ctx context.Context, s FancyMSSQLSettings, contract string) ([]LKDocument, error) {
if strings.TrimSpace(s.Server) == "" {
return nil, nil
}
ids, _, accounts, err := depoClientIDs(ctx, s, contract)
if err != nil || len(ids) == 0 {
return nil, err
}
conn, err := fancyOpen(s, s.DepoDB())
if err != nil {
return nil, err
}
defer conn.Close()
cctx, cancel := context.WithTimeout(ctx, 25*time.Second)
defer cancel()
q := fmt.Sprintf(fsql.Doc05, inClause(ids))
rows, err := conn.QueryContext(cctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
var out []LKDocument
for rows.Next() {
var rd, bd, ed sql.NullTime
var num string
var acc int64
var state, turns sql.NullInt64
if rows.Scan(&rd, &num, &bd, &ed, &acc, &state, &turns) != nil {
continue
}
d := LKDocument{Title: "Выписка по счёту депо", Account: accounts[acc], State: docStateRu(state, sql.NullTime{}), Note: num}
if turns.Valid && turns.Int64 == 1 {
d.Title = "Выписка по счёту депо с оборотами"
}
if rd.Valid {
d.Date = rd.Time.Format("02.01.2006")
}
if bd.Valid && ed.Valid {
d.Period = bd.Time.Format("02.01.2006") + " — " + ed.Time.Format("02.01.2006")
} else if ed.Valid {
d.Period = "на " + ed.Time.Format("02.01.2006")
}
out = append(out, d)
}
return out, rows.Err()
}
// notifyDocRequest — сообщить сотрудникам депозитария, что клиент заказал документ.
func notifyDocRequest(rc *RuntimeConfig, u LKUser, kind, from, to string) {
period := ""
if from != "" || to != "" {
period = fmt.Sprintf(" за период %s — %s", pick(from, "…"), pick(to, "…"))
}
go raiseSystemAlert(rc, "Кабинет: заказан документ",
fmt.Sprintf("Клиент %s (договор %s) заказал документ «%s»%s. Заявка видна в кабинете клиента и в журнале lk.doc_requests.",
pick(u.FullName, u.Login), u.Contract, kind, period))
}