package lkgateway import ( "context" "fmt" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql" "strings" "sync" "time" ) // Чтение РЕАЛЬНЫХ портфелей клиентов из депозитарной базы Fancy (BJ_DEPO_DATA). // Только SELECT — боевая база учёта, ничего не меняем. // // Найденная цепочка связи: // // OD_CLIENTS_PORTS.PORT_NUM договор клиента («БР-1001 Гравс Геннадий») // ↓ CLIENT // OD_CLIENTS.ID счёт депо клиента (В00000957 / ВТНКЦ00000957) // ↓ = OD_RESTS.REG_1 ← ключ связи остатков со счётом // OD_RESTS K = количество, BAL_ACC (836 рос. / 318 иностр.), // BLOCK_DATE/BLOCK_REASON = блокировка // ↓ VALUE_ID // OD_VALUES бумага: NAME + ISIN (V_TYPE=2 — ценные бумаги) // // Важно: numeric-колонки читаем через CAST(... AS float) — иначе драйвер отдаёт 0. // DepoPosition — позиция клиента по данным депозитарного учёта. type DepoPosition struct { Contract string // номер договора (PORT_NUM) ClientName string // ФИО из договора AccountNum string // счёт депо (OD_CLIENTS.NUMBER) ISIN string // SecurityName string // Quantity float64 // остаток (OD_RESTS.K) Blocked float64 // из них заблокировано (по BLOCK_REASON) BlockReason string // BalAcc int // балансовый счёт (836 — рос., 318 — иностр.) } // depoContractExists проверяет, есть ли такой договор в депозитарии. func depoContractExists(ctx context.Context, s FancyMSSQLSettings, contract string) (string, bool) { conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return "", false } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 12*time.Second) defer cancel() var full string err = conn.QueryRowContext(cctx, fsql.QDb01, contract, likeEscape(contract)+" %").Scan(&full) if err != nil { return "", false } return full, true } // depoClientPositions читает реальный портфель клиента по номеру договора. // contract — «БР-1001» либо полная строка «БР-1001 Гравс Геннадий». func depoClientPositions(ctx context.Context, s FancyMSSQLSettings, contract string) ([]DepoPosition, error) { 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 := fsql.Dp01 rows, err := conn.QueryContext(cctx, q, contract, likeEscape(contract)+" %") if err != nil { return nil, err } defer rows.Close() var out []DepoPosition for rows.Next() { var p DepoPosition if err := rows.Scan(&p.Contract, &p.AccountNum, &p.ISIN, &p.SecurityName, &p.Quantity, &p.BlockReason, &p.BalAcc); err != nil { return out, err } // ФИО зашито в номер договора: «БР-1001 Гравс Геннадий» if i := strings.Index(p.Contract, " "); i > 0 { p.ClientName = strings.TrimSpace(p.Contract[i:]) } if p.BlockReason != "" { p.Blocked = p.Quantity } out = append(out, p) } return out, rows.Err() } // depoPositionsAsClient переводит позиции депозитария в вид, который отдаётся в // Личный кабинет (агрегируя одинаковые бумаги по разным счетам клиента). func depoPositionsAsClient(pos []DepoPosition) []ClientPosition { type agg struct { p ClientPosition accts map[string]bool } byISIN := map[string]*agg{} var order []string for _, d := range pos { key := strings.ToUpper(d.ISIN) a, ok := byISIN[key] if !ok { a = &agg{p: ClientPosition{ ISIN: d.ISIN, SecurityCode: d.ISIN, SecurityName: d.SecurityName, ClientName: d.ClientName, }, accts: map[string]bool{}} byISIN[key] = a order = append(order, key) } a.p.Quantity += d.Quantity a.p.Blocked += d.Blocked if d.AccountNum != "" && !a.accts[d.AccountNum] { a.accts[d.AccountNum] = true if a.p.AccountID == "" { a.p.AccountID = d.AccountNum } else { a.p.AccountID += ", " + d.AccountNum } } } out := make([]ClientPosition, 0, len(order)) for _, k := range order { a := byISIN[k] a.p.Available = a.p.Quantity - a.p.Blocked a.p.UpdatedAt = time.Now().Format("02.01.2006 15:04") out = append(out, a.p) } // Иностранные бумаги видны в портфеле, но к переводу недоступны. return markTransferable(out) } // DepoContract — реальный клиент депозитария для списка в админке ЛК. type DepoContract struct { Contract string `json:"contract"` // «БР-1001 Гравс Геннадий» ClientName string `json:"name"` // ФИО (если есть в номере договора) Securities int `json:"securities"` // сколько бумаг с ненулевым остатком } // depoListContracts — реальные клиенты депозитария с ненулевыми остатками. // search — часть номера договора или ФИО (пусто = первые limit по алфавиту). func depoListContracts(ctx context.Context, s FancyMSSQLSettings, search string, limit int) ([]DepoContract, error) { conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return nil, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() if limit <= 0 || limit > 500 { limit = 100 } q := fmt.Sprintf(fsql.Dp02, limit) rows, err := conn.QueryContext(cctx, q, search, "%"+likeEscape(search)+"%") if err != nil { return nil, err } defer rows.Close() var out []DepoContract for rows.Next() { var c DepoContract if err := rows.Scan(&c.Contract, &c.Securities); err != nil { return out, err } if i := strings.Index(c.Contract, " "); i > 0 { c.ClientName = strings.TrimSpace(c.Contract[i:]) } out = append(out, c) } return out, rows.Err() } // depoFindContracts ищет договоры клиентов по части номера/ФИО (для админки). func depoFindContracts(ctx context.Context, s FancyMSSQLSettings, like string, limit int) ([]string, error) { conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return nil, err } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() if limit <= 0 || limit > 200 { limit = 50 } rows, err := conn.QueryContext(cctx, fmt.Sprintf( fsql.QDb02, limit), "%"+like+"%") if err != nil { return nil, err } defer rows.Close() var out []string for rows.Next() { var s string if err := rows.Scan(&s); err != nil { return out, err } out = append(out, s) } return out, rows.Err() } // likeEscape — экранирование шаблона LIKE. Номер договора приходит от клиента, и // знаки % _ [ в нём должны означать сами себя, а не подстановку: иначе договор // вида «БР-1%» совпал бы с чужими счетами. func likeEscape(v string) string { r := strings.NewReplacer("[", "[[]", "%", "[%]", "_", "[_]") return r.Replace(v) } // ---- подсказки в поиске клиентов (админка ЛК) ---- // Список договоров нужен на каждой загрузке страницы «Клиенты»: браузер сам // показывает выпадающий список по мере ввода (), без скриптов на // странице. Запрос дешёвый, но их 1300+, поэтому держим ответ в памяти. var depoSuggestCache = struct { mu sync.Mutex list []string at time.Time }{} // depoSuggestContracts — все номера договоров депозитария для подсказок поиска. func depoSuggestContracts(ctx context.Context, s FancyMSSQLSettings) []string { if strings.TrimSpace(s.Server) == "" { return nil } depoSuggestCache.mu.Lock() if time.Since(depoSuggestCache.at) < 10*time.Minute && depoSuggestCache.list != nil { list := depoSuggestCache.list depoSuggestCache.mu.Unlock() return list } depoSuggestCache.mu.Unlock() conn, err := fancyOpen(s, s.DepoDB()) if err != nil { return nil } defer conn.Close() cctx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() rows, err := conn.QueryContext(cctx, fsql.QDb03) if err != nil { return nil } defer rows.Close() var out []string for rows.Next() { var v string if rows.Scan(&v) != nil { break } if v = strings.TrimSpace(v); v != "" { out = append(out, v) } } if rows.Err() != nil && len(out) == 0 { return nil } depoSuggestCache.mu.Lock() depoSuggestCache.list, depoSuggestCache.at = out, time.Now() depoSuggestCache.mu.Unlock() return out }