ЛК: портфель ДЕПО «для всех клиентов» — одно соединение и один запрос к DEPO, свой цикл раз в 5 мин; итог в настройках свёрнут
На 9 после включения «для всех» (23.10.2026) каждый проход открывал по два соединения с DEPO на каждый из 1158 договоров — около 50 входов в MSSQL в секунду без перерыва, а проход шёл в цикле опроса заявок ЛК и задерживал его. - Остатки всех клиентов — одним запросом (fsql.Dp03), одно соединение с DEPO на проход; по списку — запрос по клиенту на том же соединении. На боевой DEPO: общий запрос 127 мс, совпадает с запросом по клиенту (58 договоров). - Свой цикл lkPortfolioLoop (здоровье «lk-portfolio»): по списку — каждые 10 с, для всех клиентов — раз в 5 мин. Опрос заявок ЛК больше не ждёт. - Проходы по очереди: фоновый и «Заполнить сейчас» не шлют один портфель дважды; принудительный проход пропускает отправленные за последнюю минуту. - ЛК не отвечает — проход прерывается; одна и та же ошибка отправки не повторяется в журнале каждый проход. Ошибка чтения DEPO снимается, когда портфель снова читается без изменений. - Настройки: итог (договоров, отправлено, нечего отправлять, с ошибкой), последние 5 отправок; договоры с ошибкой и все договоры — под раскрытием. «Отправлять нечего» — не ошибка (поле Nothing). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
@@ -72,16 +72,23 @@ func lkPaperType(cfi, name string) string {
|
||||
// (пусто — все). skipped — бумаги, которые не отправляются, с причиной.
|
||||
func lkPortfolioLines(ctx context.Context, rc *RuntimeConfig, depoPort string, onlyISIN map[string]bool) (lines []LKPortfolioLine, skipped []string, err error) {
|
||||
fs := rc.Snapshot().Fancy
|
||||
ps := rc.Snapshot().LK.PortfolioSync
|
||||
pos, err := depoClientPositions(ctx, fs, depoPort)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("DEPO: %w", err)
|
||||
}
|
||||
db, err := fancyOpen(fs, fs.DepoDB())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("DEPO: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
pos, err := depoClientPositionsDB(ctx, db, depoPort)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("DEPO: %w", err)
|
||||
}
|
||||
lines, skipped = lkPortfolioLinesFrom(ctx, rc, db, pos, onlyISIN)
|
||||
return lines, skipped, nil
|
||||
}
|
||||
|
||||
// lkPortfolioLinesFrom — строки для ЛК из уже прочитанных остатков; db — открытая база DEPO (вид бумаги
|
||||
// и номер гос. регистрации, кэш на час).
|
||||
func lkPortfolioLinesFrom(ctx context.Context, rc *RuntimeConfig, db *sql.DB, pos []DepoPosition, onlyISIN map[string]bool) (lines []LKPortfolioLine, skipped []string) {
|
||||
ps := rc.Snapshot().LK.PortfolioSync
|
||||
qe, qerr := nprDB(rc) // экспорт QUIK (только чтение) — номер гос. регистрации
|
||||
for _, p := range pos {
|
||||
isin := strings.ToUpper(strings.TrimSpace(p.ISIN))
|
||||
@@ -114,7 +121,40 @@ func lkPortfolioLines(ctx context.Context, rc *RuntimeConfig, depoPort string, o
|
||||
DepoAccount: p.AccountNum, Section: ps.section(), Storage: ps.storage(), PaperType: lkPaperType(cfi, p.SecurityName),
|
||||
})
|
||||
}
|
||||
return lines, skipped, nil
|
||||
return lines, skipped
|
||||
}
|
||||
|
||||
// lkPortfolioByPort — остатки из общего запроса (depoAllPositions) по портфелям пар, как в Dp01: портфель
|
||||
// совпадает или начинается с него и пробела («БР-1001» → «БР-1001 Гравс Геннадий»). Порядок строк — по
|
||||
// бумаге, счёту, ISIN: от него зависит отпечаток портфеля, ЛК повторно не получает тот же портфель.
|
||||
func lkPortfolioByPort(all map[string][]DepoPosition, prs []lkPortfolioPair) map[string][]DepoPosition {
|
||||
want := map[string]bool{}
|
||||
for _, pr := range prs {
|
||||
want[pr.Depo] = true
|
||||
}
|
||||
out := map[string][]DepoPosition{}
|
||||
for port, pos := range all {
|
||||
for i := 0; i <= len(port); i++ {
|
||||
if i == len(port) || port[i] == ' ' {
|
||||
if k := port[:i]; want[k] {
|
||||
out[k] = append(out[k], pos...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, pos := range out {
|
||||
sort.SliceStable(pos, func(i, j int) bool {
|
||||
a, b := pos[i], pos[j]
|
||||
if a.SecurityName != b.SecurityName {
|
||||
return a.SecurityName < b.SecurityName
|
||||
}
|
||||
if a.AccountNum != b.AccountNum {
|
||||
return a.AccountNum < b.AccountNum
|
||||
}
|
||||
return a.ISIN < b.ISIN
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// lkPortfolioPush — отправить портфель ДЕПО договора ЛК (номер договора депо в ЛК, напр. «ДД-816»).
|
||||
@@ -216,6 +256,7 @@ type LKPortfolioSyncState struct {
|
||||
Lines int
|
||||
Skipped []string
|
||||
Err string
|
||||
Nothing string // отправлять нечего (нет остатков или все бумаги без номера гос. регистрации) — не ошибка
|
||||
hash string
|
||||
manual bool // ручная заливка («Залить сейчас»), не постоянное заполнение
|
||||
}
|
||||
@@ -225,23 +266,53 @@ var lkPortfolioState = struct {
|
||||
m map[string]*LKPortfolioSyncState
|
||||
}{m: map[string]*LKPortfolioSyncState{}}
|
||||
|
||||
// lkPortfolioSyncTick — сверка портфелей с DEPO; в ЛК уходит только изменившийся (или при force).
|
||||
// lkPortfolioRun — проходы по очереди: фоновый и «Заполнить сейчас» не отправляют один портфель дважды
|
||||
// (23.10.2026 при включении «для всех» 481 договор ушёл дважды).
|
||||
var lkPortfolioRun sync.Mutex
|
||||
|
||||
// lkPortfolioSyncTick — сверка портфелей с DEPO; в ЛК уходит только изменившийся (или при force — кроме
|
||||
// отправленных в последнюю минуту). Одно соединение с DEPO на проход; «для всех клиентов» — остатки одним
|
||||
// запросом (23.10.2026: по два новых соединения и запросу на каждого из ~600 клиентов — ~50 входов в DEPO в секунду).
|
||||
func lkPortfolioSyncTick(ctx context.Context, rc *RuntimeConfig, force bool) {
|
||||
st := rc.Snapshot().LK.PortfolioSync
|
||||
if !st.Enabled {
|
||||
return
|
||||
}
|
||||
lkPortfolioRun.Lock()
|
||||
defer lkPortfolioRun.Unlock()
|
||||
fs := rc.Snapshot().Fancy
|
||||
db, err := fancyOpen(fs, fs.DepoDB())
|
||||
if err != nil {
|
||||
log.Printf("lk-portfolio: DEPO: %v", err)
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
prs := st.pairs()
|
||||
var byPort map[string][]DepoPosition
|
||||
if st.AllClients {
|
||||
all, err := lkPortfolioAllPairs(ctx, rc)
|
||||
all, err := lkPortfolioAllPairs(ctx, rc, db)
|
||||
if err != nil {
|
||||
log.Printf("lk-portfolio: список клиентов (все): %v", err)
|
||||
return
|
||||
}
|
||||
prs = all
|
||||
pos, err := depoAllPositions(ctx, db)
|
||||
if err != nil {
|
||||
log.Printf("lk-portfolio: остатки DEPO (все клиенты): %v", err)
|
||||
return
|
||||
}
|
||||
prs, byPort = all, lkPortfolioByPort(pos, all)
|
||||
}
|
||||
for _, pr := range prs {
|
||||
lines, skipped, err := lkPortfolioLines(ctx, rc, pr.Depo, nil)
|
||||
var lines []LKPortfolioLine
|
||||
var skipped []string
|
||||
var err error
|
||||
if byPort != nil {
|
||||
lines, skipped = lkPortfolioLinesFrom(ctx, rc, db, byPort[pr.Depo], nil)
|
||||
} else if pos, perr := depoClientPositionsDB(ctx, db, pr.Depo); perr != nil {
|
||||
err = fmt.Errorf("DEPO: %w", perr)
|
||||
} else {
|
||||
lines, skipped = lkPortfolioLinesFrom(ctx, rc, db, pos, nil)
|
||||
}
|
||||
lkPortfolioState.Lock()
|
||||
s := lkPortfolioState.m[pr.LK]
|
||||
if s == nil {
|
||||
@@ -260,20 +331,25 @@ func lkPortfolioSyncTick(ctx context.Context, rc *RuntimeConfig, force bool) {
|
||||
raw, _ := json.Marshal(lines)
|
||||
sum := sha256.Sum256(raw)
|
||||
h := hex.EncodeToString(sum[:])
|
||||
same := h == s.hash
|
||||
same := h == s.hash // отпечаток ставится только после принятой отправки или «нечего отправлять»
|
||||
if same {
|
||||
s.Err = "" // DEPO снова читается, портфель тот же — прежняя ошибка чтения снята
|
||||
}
|
||||
recent := !s.PushedAt.IsZero() && time.Since(s.PushedAt) < time.Minute
|
||||
lkPortfolioState.Unlock()
|
||||
if same && !force {
|
||||
if same && (!force || recent) {
|
||||
continue
|
||||
}
|
||||
if len(lines) == 0 { // ЛК отвечает 500 на пустой импорт — не шлём
|
||||
lkPortfolioState.Lock()
|
||||
s.Skipped, s.Err, s.hash = skipped, lkPortfolioNothing(skipped), h
|
||||
s.Skipped, s.Err, s.Nothing, s.hash = skipped, "", lkPortfolioNothing(skipped), h
|
||||
lkPortfolioState.Unlock()
|
||||
continue
|
||||
}
|
||||
code, resp, perr := lkPortfolioPush(ctx, rc, pr.LK, lines)
|
||||
lkPortfolioState.Lock()
|
||||
s.Skipped = skipped
|
||||
prev := s.Err
|
||||
s.Skipped, s.Nothing = skipped, ""
|
||||
switch {
|
||||
case perr != nil:
|
||||
s.Err = perr.Error()
|
||||
@@ -284,14 +360,53 @@ func lkPortfolioSyncTick(ctx context.Context, rc *RuntimeConfig, force bool) {
|
||||
}
|
||||
msg := s.Err
|
||||
lkPortfolioState.Unlock()
|
||||
if msg != "" {
|
||||
switch {
|
||||
case perr != nil: // ЛК не отвечает — остальным договорам этого прохода тоже не уйти
|
||||
log.Printf("lk-portfolio: %s ← %s: отправка не прошла, проход прерван: %s", pr.LK, pr.Depo, msg)
|
||||
return
|
||||
case msg != "" && msg != prev: // та же ошибка в следующих проходах — без повтора в журнале
|
||||
log.Printf("lk-portfolio: %s ← %s: отправка не прошла: %s", pr.LK, pr.Depo, msg)
|
||||
} else {
|
||||
case msg == "":
|
||||
log.Printf("lk-portfolio: %s ← %s: портфель ДЕПО обновлён в ЛК (%d бумаг, пропущено %d)", pr.LK, pr.Depo, len(lines), len(skipped))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lkPortfolioAllEvery — как часто сверять портфели «для всех клиентов» (по списку — каждые 10 секунд).
|
||||
const lkPortfolioAllEvery = 5 * time.Minute
|
||||
|
||||
// lkPortfolioLoop — заполнение портфеля ДЕПО в ЛК, отдельно от опроса заявок ЛК (23.10.2026: проход по всем
|
||||
// клиентам задерживал приём заявок). По списку — каждые 10 секунд, для всех клиентов — раз в 5 минут.
|
||||
func (s *Server) lkPortfolioLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
var lastAll time.Time
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
healthBeat("lk-portfolio")
|
||||
ps := s.rc.Snapshot().LK.PortfolioSync
|
||||
if !ps.Enabled {
|
||||
continue
|
||||
}
|
||||
if _, ok := lkActiveEndpoint(s.rc); !ok || !moduleAllowed(s.rc, "esia-finance") {
|
||||
continue
|
||||
}
|
||||
if ps.AllClients {
|
||||
if time.Since(lastAll) < lkPortfolioAllEvery {
|
||||
continue
|
||||
}
|
||||
lastAll = time.Now()
|
||||
}
|
||||
pctx, cancel := context.WithTimeout(ctx, 4*time.Minute)
|
||||
lkPortfolioSyncTick(pctx, s.rc, false)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lkPortfolioStates — состояние для страницы настроек: последняя ручная заливка сверху, затем клиенты
|
||||
// постоянного заполнения.
|
||||
func lkPortfolioStates() []LKPortfolioSyncState {
|
||||
@@ -310,6 +425,55 @@ func lkPortfolioStates() []LKPortfolioSyncState {
|
||||
return out
|
||||
}
|
||||
|
||||
// lkPortfolioRecentN — сколько последних отправок видно в настройках без раскрытия списка.
|
||||
const lkPortfolioRecentN = 5
|
||||
|
||||
// LKPortfolioSummary — итог заполнения для настроек (23.10.2026: «для всех» — сотни договоров, список
|
||||
// целиком на странице мешает): счётчики, последние отправки; ошибки и все договоры — под раскрытием.
|
||||
type LKPortfolioSummary struct {
|
||||
Total, Sent, Nothing, Errors, Skipped int
|
||||
LastPush, LastCheck time.Time
|
||||
Manual *LKPortfolioSyncState // последняя ручная заливка («Залить сейчас»)
|
||||
Recent, Failed, All []LKPortfolioSyncState
|
||||
}
|
||||
|
||||
func lkPortfolioSummary() LKPortfolioSummary {
|
||||
var sm LKPortfolioSummary
|
||||
for _, s := range lkPortfolioStates() {
|
||||
if s.manual {
|
||||
m := s
|
||||
sm.Manual = &m
|
||||
continue
|
||||
}
|
||||
sm.Total++
|
||||
sm.Skipped += len(s.Skipped)
|
||||
switch {
|
||||
case s.Err != "":
|
||||
sm.Errors++
|
||||
sm.Failed = append(sm.Failed, s)
|
||||
case s.Nothing != "":
|
||||
sm.Nothing++
|
||||
case !s.PushedAt.IsZero():
|
||||
sm.Sent++
|
||||
}
|
||||
if s.PushedAt.After(sm.LastPush) {
|
||||
sm.LastPush = s.PushedAt
|
||||
}
|
||||
if s.CheckedAt.After(sm.LastCheck) {
|
||||
sm.LastCheck = s.CheckedAt
|
||||
}
|
||||
sm.All = append(sm.All, s)
|
||||
if !s.PushedAt.IsZero() {
|
||||
sm.Recent = append(sm.Recent, s)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(sm.Recent, func(i, j int) bool { return sm.Recent[i].PushedAt.After(sm.Recent[j].PushedAt) })
|
||||
if len(sm.Recent) > lkPortfolioRecentN {
|
||||
sm.Recent = sm.Recent[:lkPortfolioRecentN]
|
||||
}
|
||||
return sm
|
||||
}
|
||||
|
||||
// lkPortfolioInSync — договор в постоянном заполнении (по списку или «для всех»).
|
||||
func lkPortfolioInSync(rc *RuntimeConfig, lk string) bool {
|
||||
ps := rc.Snapshot().LK.PortfolioSync
|
||||
@@ -479,14 +643,8 @@ func lkPortfolioClients(ctx context.Context, rc *RuntimeConfig, q string) ([]LKP
|
||||
// lkPortfolioAllPairs — все открытые торговые счета DEPO с портфелем (режим «для всех
|
||||
// клиентов»): фильтр по префиксу счёта, договор ЛК выводится из счёта по шаблону,
|
||||
// портфель — PORT_NUM. Только MSSQL-база DEPO (как и поиск одного клиента).
|
||||
func lkPortfolioAllPairs(ctx context.Context, rc *RuntimeConfig) ([]lkPortfolioPair, error) {
|
||||
fs := rc.Snapshot().Fancy
|
||||
func lkPortfolioAllPairs(ctx context.Context, rc *RuntimeConfig, db *sql.DB) ([]lkPortfolioPair, error) {
|
||||
ps := rc.Snapshot().LK.PortfolioSync
|
||||
db, err := fancyOpen(fs, fs.DepoDB())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
rows, err := db.QueryContext(cctx, `SELECT ISNULL(p.PORT_NUM,''), c.NUMBER
|
||||
@@ -534,18 +692,22 @@ func lkPortfolioPushPair(ctx context.Context, rc *RuntimeConfig, pr lkPortfolioP
|
||||
}
|
||||
}
|
||||
lkPortfolioState.Unlock()
|
||||
msg := ""
|
||||
msg, nothing := "", ""
|
||||
if err != nil {
|
||||
msg = err.Error()
|
||||
} else if len(lines) == 0 {
|
||||
msg = lkPortfolioNothing(skipped)
|
||||
nothing = lkPortfolioNothing(skipped)
|
||||
msg = nothing
|
||||
} else if code, resp, perr := lkPortfolioPush(ctx, rc, pr.LK, lines); perr != nil {
|
||||
msg = perr.Error()
|
||||
} else if code != 200 || !strings.Contains(resp, `"success":true`) {
|
||||
msg = fmt.Sprintf("HTTP %d: %s", code, resp)
|
||||
}
|
||||
lkPortfolioState.Lock()
|
||||
s.Err = msg
|
||||
s.Err, s.Nothing = msg, nothing
|
||||
if nothing != "" {
|
||||
s.Err = ""
|
||||
}
|
||||
if msg == "" {
|
||||
raw, _ := json.Marshal(lines)
|
||||
sum := sha256.Sum256(raw)
|
||||
|
||||
Reference in New Issue
Block a user