ЛК: портфель ДЕПО «для всех клиентов» — одно соединение и один запрос к 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:
@@ -66,7 +66,7 @@ var templateFuncs = template.FuncMap{
|
|||||||
"realNSDAs": realNSDAsText,
|
"realNSDAs": realNSDAsText,
|
||||||
"quikRecipientsInfo": quikRecipientsInfo,
|
"quikRecipientsInfo": quikRecipientsInfo,
|
||||||
"quikNeedsTrust": quikNeedsTrust,
|
"quikNeedsTrust": quikNeedsTrust,
|
||||||
"lkPortfolioStates": lkPortfolioStates,
|
"lkPortfolioSummary": lkPortfolioSummary,
|
||||||
"backupStatus": backupStatus,
|
"backupStatus": backupStatus,
|
||||||
"backupWhen": backupWhen,
|
"backupWhen": backupWhen,
|
||||||
"smbWin": smbWin,
|
"smbWin": smbWin,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package lkgateway
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -65,6 +66,11 @@ func depoClientPositions(ctx context.Context, s FancyMSSQLSettings, contract str
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
return depoClientPositionsDB(ctx, conn, contract)
|
||||||
|
}
|
||||||
|
|
||||||
|
// depoClientPositionsDB — то же на открытом соединении (заполнение портфеля в ЛК: одно соединение на проход).
|
||||||
|
func depoClientPositionsDB(ctx context.Context, conn *sql.DB, contract string) ([]DepoPosition, error) {
|
||||||
cctx, cancel := context.WithTimeout(ctx, 25*time.Second)
|
cctx, cancel := context.WithTimeout(ctx, 25*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -73,6 +79,31 @@ func depoClientPositions(ctx context.Context, s FancyMSSQLSettings, contract str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return depoScanPositions(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// depoAllPositions — остатки всех клиентов одним запросом (заполнение портфеля «для всех клиентов»),
|
||||||
|
// по портфелю (PORT_NUM без пробелов по краям).
|
||||||
|
func depoAllPositions(ctx context.Context, conn *sql.DB) (map[string][]DepoPosition, error) {
|
||||||
|
cctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
rows, err := conn.QueryContext(cctx, fsql.Dp03)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pos, err := depoScanPositions(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := map[string][]DepoPosition{}
|
||||||
|
for _, p := range pos {
|
||||||
|
k := strings.TrimSpace(p.Contract)
|
||||||
|
out[k] = append(out[k], p)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func depoScanPositions(rows *sql.Rows) ([]DepoPosition, error) {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var out []DepoPosition
|
var out []DepoPosition
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
|||||||
@@ -17,3 +17,13 @@ var Dp02 = `SELECT TOP (%d) p.PORT_NUM, COUNT(DISTINCT rst.VALUE_ID)
|
|||||||
JOIN OD_VALUES v ON v.ID = rst.VALUE_ID AND v.V_TYPE = 2
|
JOIN OD_VALUES v ON v.ID = rst.VALUE_ID AND v.V_TYPE = 2
|
||||||
WHERE CAST(rst.K AS float) <> 0 AND (@p1 = '' OR p.PORT_NUM LIKE @p2)
|
WHERE CAST(rst.K AS float) <> 0 AND (@p1 = '' OR p.PORT_NUM LIKE @p2)
|
||||||
GROUP BY p.PORT_NUM ORDER BY p.PORT_NUM`
|
GROUP BY p.PORT_NUM ORDER BY p.PORT_NUM`
|
||||||
|
|
||||||
|
// Dp03 — Dp01 для всех портфелей сразу (заполнение портфеля ДЕПО в ЛК «для всех клиентов»).
|
||||||
|
var Dp03 = `SELECT ISNULL(p.PORT_NUM,''), ISNULL(c.NUMBER,''), ISNULL(v.ISIN,''), ISNULL(v.NAME,''),
|
||||||
|
CAST(rst.K AS float), ISNULL(rst.BLOCK_REASON,''), ISNULL(rst.BAL_ACC,0)
|
||||||
|
FROM OD_RESTS rst
|
||||||
|
JOIN OD_CLIENTS c ON c.ID = rst.REG_1
|
||||||
|
JOIN OD_CLIENTS_PORTS p ON p.CLIENT = c.ID
|
||||||
|
JOIN OD_VALUES v ON v.ID = rst.VALUE_ID AND v.V_TYPE = 2
|
||||||
|
WHERE CAST(rst.K AS float) <> 0
|
||||||
|
ORDER BY p.PORT_NUM, v.NAME, c.NUMBER, v.ISIN`
|
||||||
|
|||||||
@@ -295,10 +295,7 @@ func (s *Server) pollLKClaims(ctx context.Context) {
|
|||||||
log.Printf("lk-gateway: поллер ЛК: связь с ЛК восстановлена")
|
log.Printf("lk-gateway: поллер ЛК: связь с ЛК восстановлена")
|
||||||
lastErr = ""
|
lastErr = ""
|
||||||
}
|
}
|
||||||
// тот же проход: портфели ДЕПО из DEPO в ЛК (только изменившиеся, lkportfolio.go)
|
// портфели ДЕПО из DEPO в ЛК — отдельный цикл lkPortfolioLoop (lkportfolio.go)
|
||||||
pctx, pcancel := context.WithTimeout(ctx, time.Minute)
|
|
||||||
lkPortfolioSyncTick(pctx, s.rc, false)
|
|
||||||
pcancel()
|
|
||||||
if res.Fetched > 0 || len(res.Errors) > 0 {
|
if res.Fetched > 0 || len(res.Errors) > 0 {
|
||||||
log.Printf("lk-gateway: поллер ЛК: получено %d, отправлено %d, отклонено %d, ошибок %d (имитация НРД: %v)",
|
log.Printf("lk-gateway: поллер ЛК: получено %d, отправлено %d, отклонено %d, ошибок %d (имитация НРД: %v)",
|
||||||
res.Fetched, res.Sent, res.Rejected, len(res.Errors), res.Emulated)
|
res.Fetched, res.Sent, res.Rejected, len(res.Errors), res.Emulated)
|
||||||
|
|||||||
@@ -72,16 +72,23 @@ func lkPaperType(cfi, name string) string {
|
|||||||
// (пусто — все). skipped — бумаги, которые не отправляются, с причиной.
|
// (пусто — все). skipped — бумаги, которые не отправляются, с причиной.
|
||||||
func lkPortfolioLines(ctx context.Context, rc *RuntimeConfig, depoPort string, onlyISIN map[string]bool) (lines []LKPortfolioLine, skipped []string, err error) {
|
func lkPortfolioLines(ctx context.Context, rc *RuntimeConfig, depoPort string, onlyISIN map[string]bool) (lines []LKPortfolioLine, skipped []string, err error) {
|
||||||
fs := rc.Snapshot().Fancy
|
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())
|
db, err := fancyOpen(fs, fs.DepoDB())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("DEPO: %w", err)
|
return nil, nil, fmt.Errorf("DEPO: %w", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
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 (только чтение) — номер гос. регистрации
|
qe, qerr := nprDB(rc) // экспорт QUIK (только чтение) — номер гос. регистрации
|
||||||
for _, p := range pos {
|
for _, p := range pos {
|
||||||
isin := strings.ToUpper(strings.TrimSpace(p.ISIN))
|
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),
|
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»).
|
// lkPortfolioPush — отправить портфель ДЕПО договора ЛК (номер договора депо в ЛК, напр. «ДД-816»).
|
||||||
@@ -216,6 +256,7 @@ type LKPortfolioSyncState struct {
|
|||||||
Lines int
|
Lines int
|
||||||
Skipped []string
|
Skipped []string
|
||||||
Err string
|
Err string
|
||||||
|
Nothing string // отправлять нечего (нет остатков или все бумаги без номера гос. регистрации) — не ошибка
|
||||||
hash string
|
hash string
|
||||||
manual bool // ручная заливка («Залить сейчас»), не постоянное заполнение
|
manual bool // ручная заливка («Залить сейчас»), не постоянное заполнение
|
||||||
}
|
}
|
||||||
@@ -225,23 +266,53 @@ var lkPortfolioState = struct {
|
|||||||
m map[string]*LKPortfolioSyncState
|
m map[string]*LKPortfolioSyncState
|
||||||
}{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) {
|
func lkPortfolioSyncTick(ctx context.Context, rc *RuntimeConfig, force bool) {
|
||||||
st := rc.Snapshot().LK.PortfolioSync
|
st := rc.Snapshot().LK.PortfolioSync
|
||||||
if !st.Enabled {
|
if !st.Enabled {
|
||||||
return
|
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()
|
prs := st.pairs()
|
||||||
|
var byPort map[string][]DepoPosition
|
||||||
if st.AllClients {
|
if st.AllClients {
|
||||||
all, err := lkPortfolioAllPairs(ctx, rc)
|
all, err := lkPortfolioAllPairs(ctx, rc, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("lk-portfolio: список клиентов (все): %v", err)
|
log.Printf("lk-portfolio: список клиентов (все): %v", err)
|
||||||
return
|
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 {
|
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()
|
lkPortfolioState.Lock()
|
||||||
s := lkPortfolioState.m[pr.LK]
|
s := lkPortfolioState.m[pr.LK]
|
||||||
if s == nil {
|
if s == nil {
|
||||||
@@ -260,20 +331,25 @@ func lkPortfolioSyncTick(ctx context.Context, rc *RuntimeConfig, force bool) {
|
|||||||
raw, _ := json.Marshal(lines)
|
raw, _ := json.Marshal(lines)
|
||||||
sum := sha256.Sum256(raw)
|
sum := sha256.Sum256(raw)
|
||||||
h := hex.EncodeToString(sum[:])
|
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()
|
lkPortfolioState.Unlock()
|
||||||
if same && !force {
|
if same && (!force || recent) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(lines) == 0 { // ЛК отвечает 500 на пустой импорт — не шлём
|
if len(lines) == 0 { // ЛК отвечает 500 на пустой импорт — не шлём
|
||||||
lkPortfolioState.Lock()
|
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()
|
lkPortfolioState.Unlock()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
code, resp, perr := lkPortfolioPush(ctx, rc, pr.LK, lines)
|
code, resp, perr := lkPortfolioPush(ctx, rc, pr.LK, lines)
|
||||||
lkPortfolioState.Lock()
|
lkPortfolioState.Lock()
|
||||||
s.Skipped = skipped
|
prev := s.Err
|
||||||
|
s.Skipped, s.Nothing = skipped, ""
|
||||||
switch {
|
switch {
|
||||||
case perr != nil:
|
case perr != nil:
|
||||||
s.Err = perr.Error()
|
s.Err = perr.Error()
|
||||||
@@ -284,14 +360,53 @@ func lkPortfolioSyncTick(ctx context.Context, rc *RuntimeConfig, force bool) {
|
|||||||
}
|
}
|
||||||
msg := s.Err
|
msg := s.Err
|
||||||
lkPortfolioState.Unlock()
|
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)
|
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))
|
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 — состояние для страницы настроек: последняя ручная заливка сверху, затем клиенты
|
// lkPortfolioStates — состояние для страницы настроек: последняя ручная заливка сверху, затем клиенты
|
||||||
// постоянного заполнения.
|
// постоянного заполнения.
|
||||||
func lkPortfolioStates() []LKPortfolioSyncState {
|
func lkPortfolioStates() []LKPortfolioSyncState {
|
||||||
@@ -310,6 +425,55 @@ func lkPortfolioStates() []LKPortfolioSyncState {
|
|||||||
return out
|
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 — договор в постоянном заполнении (по списку или «для всех»).
|
// lkPortfolioInSync — договор в постоянном заполнении (по списку или «для всех»).
|
||||||
func lkPortfolioInSync(rc *RuntimeConfig, lk string) bool {
|
func lkPortfolioInSync(rc *RuntimeConfig, lk string) bool {
|
||||||
ps := rc.Snapshot().LK.PortfolioSync
|
ps := rc.Snapshot().LK.PortfolioSync
|
||||||
@@ -479,14 +643,8 @@ func lkPortfolioClients(ctx context.Context, rc *RuntimeConfig, q string) ([]LKP
|
|||||||
// lkPortfolioAllPairs — все открытые торговые счета DEPO с портфелем (режим «для всех
|
// lkPortfolioAllPairs — все открытые торговые счета DEPO с портфелем (режим «для всех
|
||||||
// клиентов»): фильтр по префиксу счёта, договор ЛК выводится из счёта по шаблону,
|
// клиентов»): фильтр по префиксу счёта, договор ЛК выводится из счёта по шаблону,
|
||||||
// портфель — PORT_NUM. Только MSSQL-база DEPO (как и поиск одного клиента).
|
// портфель — PORT_NUM. Только MSSQL-база DEPO (как и поиск одного клиента).
|
||||||
func lkPortfolioAllPairs(ctx context.Context, rc *RuntimeConfig) ([]lkPortfolioPair, error) {
|
func lkPortfolioAllPairs(ctx context.Context, rc *RuntimeConfig, db *sql.DB) ([]lkPortfolioPair, error) {
|
||||||
fs := rc.Snapshot().Fancy
|
|
||||||
ps := rc.Snapshot().LK.PortfolioSync
|
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)
|
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
rows, err := db.QueryContext(cctx, `SELECT ISNULL(p.PORT_NUM,''), c.NUMBER
|
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()
|
lkPortfolioState.Unlock()
|
||||||
msg := ""
|
msg, nothing := "", ""
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg = err.Error()
|
msg = err.Error()
|
||||||
} else if len(lines) == 0 {
|
} 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 {
|
} else if code, resp, perr := lkPortfolioPush(ctx, rc, pr.LK, lines); perr != nil {
|
||||||
msg = perr.Error()
|
msg = perr.Error()
|
||||||
} else if code != 200 || !strings.Contains(resp, `"success":true`) {
|
} else if code != 200 || !strings.Contains(resp, `"success":true`) {
|
||||||
msg = fmt.Sprintf("HTTP %d: %s", code, resp)
|
msg = fmt.Sprintf("HTTP %d: %s", code, resp)
|
||||||
}
|
}
|
||||||
lkPortfolioState.Lock()
|
lkPortfolioState.Lock()
|
||||||
s.Err = msg
|
s.Err, s.Nothing = msg, nothing
|
||||||
|
if nothing != "" {
|
||||||
|
s.Err = ""
|
||||||
|
}
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
raw, _ := json.Marshal(lines)
|
raw, _ := json.Marshal(lines)
|
||||||
sum := sha256.Sum256(raw)
|
sum := sha256.Sum256(raw)
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//go:build live
|
||||||
|
|
||||||
|
package lkgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Остатки «для всех клиентов» одним запросом (Dp03) совпадают с запросом по клиенту (Dp01). Только чтение DEPO.
|
||||||
|
// PF_SETUP — копия setup.json (по умолчанию /var/lib/bj/.bj/setup.json).
|
||||||
|
func TestLKPortfolioAllLive(t *testing.T) {
|
||||||
|
path := os.Getenv("PF_SETUP")
|
||||||
|
if path == "" {
|
||||||
|
path = "/var/lib/bj/.bj/setup.json"
|
||||||
|
}
|
||||||
|
rc, err := NewRuntimeConfig(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
fs := rc.Snapshot().Fancy
|
||||||
|
db, err := fancyOpen(fs, fs.DepoDB())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
t0 := time.Now()
|
||||||
|
prs, err := lkPortfolioAllPairs(ctx, rc, db)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t1 := time.Now()
|
||||||
|
all, err := depoAllPositions(ctx, db)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t2 := time.Now()
|
||||||
|
by := lkPortfolioByPort(all, prs)
|
||||||
|
rows := 0
|
||||||
|
for _, p := range all {
|
||||||
|
rows += len(p)
|
||||||
|
}
|
||||||
|
t.Logf("договоров %d (%s); остатков %d по %d портфелям (%s); с остатками у договоров %d",
|
||||||
|
len(prs), t1.Sub(t0).Round(time.Millisecond), rows, len(all), t2.Sub(t1).Round(time.Millisecond), len(by))
|
||||||
|
key := func(ps []DepoPosition) string {
|
||||||
|
var k []string
|
||||||
|
for _, p := range ps {
|
||||||
|
k = append(k, fmt.Sprintf("%s|%s|%g", strings.TrimSpace(p.AccountNum), strings.TrimSpace(p.ISIN), p.Quantity))
|
||||||
|
}
|
||||||
|
sort.Strings(k)
|
||||||
|
return strings.Join(k, ";")
|
||||||
|
}
|
||||||
|
step, checked := len(prs)/60+1, 0
|
||||||
|
var one time.Duration
|
||||||
|
for i := 0; i < len(prs); i += step {
|
||||||
|
pr := prs[i]
|
||||||
|
s := time.Now()
|
||||||
|
want, err := depoClientPositionsDB(ctx, db, pr.Depo)
|
||||||
|
one += time.Since(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", pr.Depo, err)
|
||||||
|
}
|
||||||
|
if a, b := key(want), key(by[pr.Depo]); a != b {
|
||||||
|
t.Errorf("%s ← %s: по клиенту %d строк, общим запросом %d\n %s\n %s", pr.LK, pr.Depo, len(want), len(by[pr.Depo]), a, b)
|
||||||
|
}
|
||||||
|
checked++
|
||||||
|
}
|
||||||
|
if checked > 0 {
|
||||||
|
t.Logf("сверено договоров %d, запрос по клиенту в среднем %s", checked, (one / time.Duration(checked)).Round(time.Millisecond))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLKPaperTypeAndPairs(t *testing.T) {
|
func TestLKPaperTypeAndPairs(t *testing.T) {
|
||||||
@@ -36,6 +38,57 @@ func TestLKContractByAccount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Остатки общего запроса раскладываются по портфелям пар как в Dp01: совпадение или «портфель + пробел».
|
||||||
|
func TestLKPortfolioByPort(t *testing.T) {
|
||||||
|
all := map[string][]DepoPosition{
|
||||||
|
"БР-1001 Гравс Геннадий": {{AccountNum: "ВТНКЦ01", ISIN: "RU2", SecurityName: "Б"}, {AccountNum: "ВТНКЦ01", ISIN: "RU1", SecurityName: "А"}},
|
||||||
|
"БР-10011 Другой": {{AccountNum: "ВТНКЦ02", ISIN: "RU3", SecurityName: "В"}},
|
||||||
|
"БР-7": {{AccountNum: "ВТНКЦ07", ISIN: "RU4", SecurityName: "Г"}},
|
||||||
|
}
|
||||||
|
by := lkPortfolioByPort(all, []lkPortfolioPair{{LK: "ДД-1", Depo: "БР-1001"}, {LK: "ДД-7", Depo: "БР-7"}, {LK: "ДД-9", Depo: "БР-9"}})
|
||||||
|
if p := by["БР-1001"]; len(p) != 2 || p[0].ISIN != "RU1" || p[1].ISIN != "RU2" {
|
||||||
|
t.Fatalf("БР-1001: %+v", p)
|
||||||
|
}
|
||||||
|
if len(by["БР-7"]) != 1 || len(by["БР-9"]) != 0 || len(by) != 2 {
|
||||||
|
t.Fatalf("%+v", by)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Итог заполнения: «нечего отправлять» — не ошибка, все договоры — под раскрытием.
|
||||||
|
func TestLKPortfolioSummaryView(t *testing.T) {
|
||||||
|
lkPortfolioState.Lock()
|
||||||
|
saved := lkPortfolioState.m
|
||||||
|
now := time.Now()
|
||||||
|
lkPortfolioState.m = map[string]*LKPortfolioSyncState{
|
||||||
|
"ДД-1": {LK: "ДД-1", Depo: "БР-1-1", CheckedAt: now, Nothing: lkPortfolioNothing([]string{"x"}), Skipped: []string{"x"}},
|
||||||
|
"ДД-1006": {LK: "ДД-1006", Depo: "БР-1047", CheckedAt: now, PushedAt: now, Lines: 1},
|
||||||
|
"ДД-1007": {LK: "ДД-1007", Depo: "БР-1048", CheckedAt: now, Err: "DEPO: i/o timeout"},
|
||||||
|
"ДД-816": {LK: "ДД-816", Depo: "БР-859", CheckedAt: now, PushedAt: now, Lines: 3, manual: true},
|
||||||
|
}
|
||||||
|
lkPortfolioState.Unlock()
|
||||||
|
defer func() { lkPortfolioState.Lock(); lkPortfolioState.m = saved; lkPortfolioState.Unlock() }()
|
||||||
|
|
||||||
|
sm := lkPortfolioSummary()
|
||||||
|
if sm.Total != 3 || sm.Sent != 1 || sm.Nothing != 1 || sm.Errors != 1 || sm.Skipped != 1 || sm.Manual == nil || len(sm.Recent) != 1 {
|
||||||
|
t.Fatalf("%+v", sm)
|
||||||
|
}
|
||||||
|
a, err := newAdmin()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
render(rec, a.setup, SetupData{page: nowPage("Настройка", "setup")})
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{"Итог заполнения", "Все договоры (3)", "Договоры с ошибкой (1)", "Последние обновления", "Заливка одному клиенту"} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Errorf("нет %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(body, "ошибка — отправлять нечего") {
|
||||||
|
t.Error("«отправлять нечего» показано как ошибка")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Маршруты карточки «Заполнение портфеля ДЕПО» зарегистрированы (29.09–22.10 их не было — 404).
|
// Маршруты карточки «Заполнение портфеля ДЕПО» зарегистрированы (29.09–22.10 их не было — 404).
|
||||||
func TestLKPortfolioRoutes(t *testing.T) {
|
func TestLKPortfolioRoutes(t *testing.T) {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ func (s *Server) Run(ctx context.Context) error {
|
|||||||
// Монитор здоровья + systemd watchdog: следит за фоновыми горутинами,
|
// Монитор здоровья + systemd watchdog: следит за фоновыми горутинами,
|
||||||
// поднимает алерт при зависании и «пинает» systemd, пока всё живо.
|
// поднимает алерт при зависании и «пинает» systemd, пока всё живо.
|
||||||
healthRegister("poll-lk", time.Minute)
|
healthRegister("poll-lk", time.Minute)
|
||||||
|
healthRegister("lk-portfolio", 2*time.Minute) // проход «для всех клиентов» — до 4 мин без отметки
|
||||||
// Ключ продлеваем до запуска фоновых циклов: режим разработки (имитации и т.п.) должен быть
|
// Ключ продлеваем до запуска фоновых циклов: режим разработки (имитации и т.п.) должен быть
|
||||||
// известен им с первого прохода.
|
// известен им с первого прохода.
|
||||||
if licenseNeedsRenew(s.rc) {
|
if licenseNeedsRenew(s.rc) {
|
||||||
@@ -338,6 +339,8 @@ func (s *Server) Run(ctx context.Context) error {
|
|||||||
// Поллер заявок ЛК ЕСИА Финанс: раз в минуту забирает новые depo_m2m и
|
// Поллер заявок ЛК ЕСИА Финанс: раз в минуту забирает новые depo_m2m и
|
||||||
// отправляет в НРД (или имитатору). Интервал 1 мин — время ответов регламентировано.
|
// отправляет в НРД (или имитатору). Интервал 1 мин — время ответов регламентировано.
|
||||||
go superviseLoop(ctx, "poll-lk", s.rc, s.pollLKClaims)
|
go superviseLoop(ctx, "poll-lk", s.rc, s.pollLKClaims)
|
||||||
|
// Портфели ДЕПО из DEPO в ЛК: по списку — каждые 10 с, для всех клиентов — раз в 5 мин.
|
||||||
|
go superviseLoop(ctx, "lk-portfolio", s.rc, s.lkPortfolioLoop)
|
||||||
|
|
||||||
// Авто-обновление справочника участников M2M раз в сутки (только реальный
|
// Авто-обновление справочника участников M2M раз в сутки (только реальный
|
||||||
// ИШ). Тик суточный — в heartbeat-монитор не регистрируем, только recover.
|
// ИШ). Тик суточный — в heartbeat-монитор не регистрируем, только recover.
|
||||||
|
|||||||
@@ -583,6 +583,8 @@ func HealthLabel(name string) string {
|
|||||||
switch name {
|
switch name {
|
||||||
case "poll-lk":
|
case "poll-lk":
|
||||||
return "Опрос заявок ЛК ЕСИА Финанс"
|
return "Опрос заявок ЛК ЕСИА Финанс"
|
||||||
|
case "lk-portfolio":
|
||||||
|
return "Заполнение портфеля ДЕПО в ЛК ЕСИА Финанс"
|
||||||
case "poll-incoming":
|
case "poll-incoming":
|
||||||
return "Опрос ответов НРД (ИШ)"
|
return "Опрос ответов НРД (ИШ)"
|
||||||
case "consume-decisions":
|
case "consume-decisions":
|
||||||
|
|||||||
@@ -677,7 +677,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<div id="lk-portfolio" style="margin-top:14px;padding:12px 14px;border:1px solid var(--border);border-radius:10px">
|
<div id="lk-portfolio" style="margin-top:14px;padding:12px 14px;border:1px solid var(--border);border-radius:10px">
|
||||||
<b>Заполнение портфеля ДЕПО бумагами из DEPO</b> {{if .Settings.LK.PortfolioSync.Enabled}}<span class="badge confirmed">включено{{if .Settings.LK.PortfolioSync.AllClients}} — все клиенты{{else}} — по списку{{end}}</span>{{else}}<span class="badge draft">выключено</span>{{end}}
|
<b>Заполнение портфеля ДЕПО бумагами из DEPO</b> {{if .Settings.LK.PortfolioSync.Enabled}}<span class="badge confirmed">включено{{if .Settings.LK.PortfolioSync.AllClients}} — все клиенты{{else}} — по списку{{end}}</span>{{else}}<span class="badge draft">выключено</span>{{end}}
|
||||||
<p class="muted" style="margin:6px 0 8px;font-size:13px">Каждые 10 секунд, в том же проходе, что и приём заявок, bj сверяет портфель клиента в DEPO и отправляет его в ЛК (<code>/portfolio/import</code>, портфель ДЕПО) — только если остатки изменились. Номер гос. регистрации — из экспорта QUIK, раздел счёта — «Основной», хранение — НРД. Бумаги без номера гос. регистрации не отправляются (ЛК отвергает весь импорт).</p>
|
<p class="muted" style="margin:6px 0 8px;font-size:13px">Для клиентов из списка — каждые 10 секунд, для всех клиентов — раз в 5 минут (одним запросом к DEPO) bj сверяет портфель клиента в DEPO и отправляет его в ЛК (<code>/portfolio/import</code>, портфель ДЕПО) — только если остатки изменились. Номер гос. регистрации — из экспорта QUIK, раздел счёта — «Основной», хранение — НРД. Бумаги без номера гос. регистрации не отправляются (ЛК отвергает весь импорт).</p>
|
||||||
<form method="post" action="/admin/setup/lk-portfolio" style="margin:0 0 8px">
|
<form method="post" action="/admin/setup/lk-portfolio" style="margin:0 0 8px">
|
||||||
{{if .Settings.LK.PortfolioSync.Enabled}}<button class="btn" type="submit" name="action" value="off" style="background:var(--card);color:var(--err);border:1px solid var(--err)">Выключить заполнение портфеля ДЕПО бумагами</button>
|
{{if .Settings.LK.PortfolioSync.Enabled}}<button class="btn" type="submit" name="action" value="off" style="background:var(--card);color:var(--err);border:1px solid var(--err)">Выключить заполнение портфеля ДЕПО бумагами</button>
|
||||||
<button class="btn" type="submit" name="action" value="sync">Заполнить сейчас</button>
|
<button class="btn" type="submit" name="action" value="sync">Заполнить сейчас</button>
|
||||||
@@ -746,7 +746,14 @@
|
|||||||
q.addEventListener('focus',function(){ if(!list.innerHTML) load(); });
|
q.addEventListener('focus',function(){ if(!list.innerHTML) load(); });
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{{range lkPortfolioStates}}<div class="muted" style="font-size:12.5px;margin-top:8px"><b>{{.LK}}</b> ← {{.Depo}}: {{if .Err}}<span style="color:var(--err)">ошибка — {{.Err}}</span>{{else if .PushedAt.IsZero}}сверено {{.CheckedAt.Format "15:04:05"}}, отправки ещё не было{{else}}отправлено {{.PushedAt.Format "02.01 15:04:05"}} ({{.Lines}} бумаг), сверено {{.CheckedAt.Format "15:04:05"}}{{end}}{{if .Skipped}}<details style="margin-top:2px"><summary style="cursor:pointer">пропущено бумаг: {{len .Skipped}} — без номера гос. регистрации</summary>{{range .Skipped}}<div>{{.}}</div>{{end}}</details>{{end}}</div>{{end}}
|
{{with lkPortfolioSummary}}{{if or .Manual .Total}}<div style="margin-top:10px;padding-top:10px;border-top:1px dashed var(--border);font-size:12.5px">
|
||||||
|
{{with .Manual}}<div class="muted" style="margin-bottom:6px">Заливка одному клиенту: {{template "lkpfRow" .}}</div>{{end}}
|
||||||
|
{{if .Total}}<div><b>Итог заполнения:</b> договоров {{.Total}} · отправлено в ЛК {{.Sent}} · нечего отправлять {{.Nothing}}{{if .Errors}} · <span style="color:var(--err)">с ошибкой {{.Errors}}</span>{{end}}{{if .Skipped}} · бумаг без номера гос. регистрации {{.Skipped}}{{end}}</div>
|
||||||
|
<div class="muted">{{if not .LastPush.IsZero}}последняя отправка {{.LastPush.Format "02.01 15:04:05"}} · {{end}}последняя сверка {{.LastCheck.Format "15:04:05"}}</div>
|
||||||
|
{{if .Recent}}<div style="margin-top:6px">Последние обновления:</div>{{range .Recent}}<div class="muted" style="margin-top:2px">{{template "lkpfRow" .}}</div>{{end}}{{end}}
|
||||||
|
{{if .Failed}}<details style="margin-top:6px"><summary style="cursor:pointer;color:var(--err)">Договоры с ошибкой ({{len .Failed}})</summary>{{range .Failed}}<div class="muted" style="margin-top:4px">{{template "lkpfRow" .}}</div>{{end}}</details>{{end}}
|
||||||
|
<details style="margin-top:6px"><summary style="cursor:pointer">Все договоры ({{.Total}})</summary>{{range .All}}<div class="muted" style="margin-top:4px">{{template "lkpfRow" .}}</div>{{end}}</details>{{end}}
|
||||||
|
</div>{{end}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div id="lk-real-nsd" style="margin-top:14px;padding:12px 14px;border:1px solid var(--border);border-radius:10px;{{if .Settings.LK.RealNSDTest.Enabled}}background:var(--warn-weak){{end}}">
|
<div id="lk-real-nsd" style="margin-top:14px;padding:12px 14px;border:1px solid var(--border);border-radius:10px;{{if .Settings.LK.RealNSDTest.Enabled}}background:var(--warn-weak){{end}}">
|
||||||
<b>Тест «боевой ЛК → тестовый контур НРД»</b> {{if .Settings.LK.RealNSDTest.Enabled}}<span class="badge awaiting_decision">включён</span>{{else}}<span class="badge draft">выключен</span>{{end}}
|
<b>Тест «боевой ЛК → тестовый контур НРД»</b> {{if .Settings.LK.RealNSDTest.Enabled}}<span class="badge awaiting_decision">включён</span>{{else}}<span class="badge draft">выключен</span>{{end}}
|
||||||
@@ -1144,3 +1151,5 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{/* строка договора в итоге заполнения портфеля ДЕПО (LKPortfolioSyncState) */}}
|
||||||
|
{{define "lkpfRow"}}<b>{{.LK}}</b> ← {{.Depo}}: {{if .Err}}<span style="color:var(--err)">ошибка — {{.Err}}</span>{{else if .Nothing}}{{.Nothing}}{{else if .PushedAt.IsZero}}сверено {{.CheckedAt.Format "15:04:05"}}, отправки ещё не было{{else}}отправлено {{.PushedAt.Format "02.01 15:04:05"}} ({{.Lines}} бумаг), сверено {{.CheckedAt.Format "15:04:05"}}{{end}}{{if .Skipped}}<details style="margin-top:2px"><summary style="cursor:pointer">пропущено бумаг: {{len .Skipped}} — без номера гос. регистрации</summary>{{range .Skipped}}<div>{{.}}</div>{{end}}</details>{{end}}{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user