Портфель ДЕПО в ЛК: режим «для всех клиентов» + автозаполнение именования

- Кнопка «Включить для всех пользователей» с предупреждением: заполнение
  портфеля запускается для ВСЕХ открытых торговых счетов DEPO (по префиксу),
  договор ЛК выводится из счёта по шаблону — так же как в тесте с одним клиентом,
  только по торговым счетам. Поле AllClients; tick берёт пары из
  lkPortfolioAllPairs. Прежний режим «по списку» (Pairs) сохранён.
- Поля именования (префикс счёта, шаблон договора, раздел, место хранения)
  предзаполняются из базы DEPO (префикс — depoAccPrefix) и значений по умолчанию,
  остаются редактируемыми.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
fontvielle
2026-10-21 17:24:11 +03:00
parent 7947808b26
commit bd1fe202f7
3 changed files with 67 additions and 13 deletions
+2 -2
View File
@@ -20,8 +20,8 @@
"version": "2026.10.21.6" "version": "2026.10.21.6"
}, },
"bj-server": { "bj-server": {
"fingerprint": "e8cd8c67b83a419297ca4cbcf5b488f11b8776a032908eca08f0df254b7c0488", "fingerprint": "6cf5639b24a45d8c39e1896ab11301d00de8eb4d40f4364be70c31fb2b5bb6df",
"version": "2026.10.21.9" "version": "2026.10.21.10"
}, },
"crypto-service.jar": { "crypto-service.jar": {
"fingerprint": "2c4fd536bc9b1c20c69a70e8dcd374a61847b263aeb2e474c45debb01fbd8233", "fingerprint": "2c4fd536bc9b1c20c69a70e8dcd374a61847b263aeb2e474c45debb01fbd8233",
+56 -4
View File
@@ -173,6 +173,9 @@ type LKPortfolioSyncSettings struct {
ContractTemplate string `json:"contract_template,omitempty"` // номер договора депо в ЛК: «ДД-{n}», {n} — номер счёта без префикса и ведущих нулей ContractTemplate string `json:"contract_template,omitempty"` // номер договора депо в ЛК: «ДД-{n}», {n} — номер счёта без префикса и ведущих нулей
Section string `json:"section,omitempty"` // раздел счёта депо в ЛК: «Основной» Section string `json:"section,omitempty"` // раздел счёта депо в ЛК: «Основной»
Storage string `json:"storage,omitempty"` // место хранения: «НРД» Storage string `json:"storage,omitempty"` // место хранения: «НРД»
// AllClients — заполнять портфель ВСЕМ клиентам: все открытые торговые счета DEPO
// (по префиксу), договор ЛК выводится из счёта по шаблону. Иначе — только Pairs.
AllClients bool `json:"all_clients,omitempty"`
} }
func (s LKPortfolioSyncSettings) accountPrefix() string { func (s LKPortfolioSyncSettings) accountPrefix() string {
@@ -223,7 +226,16 @@ func lkPortfolioSyncTick(ctx context.Context, rc *RuntimeConfig, force bool) {
if !st.Enabled { if !st.Enabled {
return return
} }
for _, pr := range st.pairs() { prs := st.pairs()
if st.AllClients {
all, err := lkPortfolioAllPairs(ctx, rc)
if err != nil {
log.Printf("lk-portfolio: список клиентов (все): %v", err)
return
}
prs = all
}
for _, pr := range prs {
lines, skipped, err := lkPortfolioLines(ctx, rc, pr.Depo, nil) lines, skipped, err := lkPortfolioLines(ctx, rc, pr.Depo, nil)
lkPortfolioState.Lock() lkPortfolioState.Lock()
s := lkPortfolioState.m[pr.LK] s := lkPortfolioState.m[pr.LK]
@@ -310,9 +322,11 @@ func registerLKPortfolioSync(mux *http.ServeMux, rc *RuntimeConfig) {
} }
switch r.FormValue("action") { switch r.FormValue("action") {
case "on": case "on":
lk.PortfolioSync.Enabled = lk.PortfolioSync.Pairs != "" lk.PortfolioSync.Enabled = lk.PortfolioSync.Pairs != "" || lk.PortfolioSync.AllClients
case "on_all":
lk.PortfolioSync.AllClients, lk.PortfolioSync.Enabled = true, true
case "off": case "off":
lk.PortfolioSync.Enabled = false lk.PortfolioSync.Enabled, lk.PortfolioSync.AllClients = false, false
} }
if err := rc.UpdateLK(lk); err != nil { if err := rc.UpdateLK(lk); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -339,7 +353,7 @@ func registerLKPortfolioSync(mux *http.ServeMux, rc *RuntimeConfig) {
http.Redirect(w, r, "/admin/setup#lk-portfolio", http.StatusSeeOther) http.Redirect(w, r, "/admin/setup#lk-portfolio", http.StatusSeeOther)
return return
} }
if a := r.FormValue("action"); a == "sync" || a == "on" { // включили или «Заполнить сейчас» — сразу отправить if a := r.FormValue("action"); a == "sync" || a == "on" || a == "on_all" { // включили или «Заполнить сейчас» — сразу отправить
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute) ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
lkPortfolioSyncTick(ctx, rc, true) lkPortfolioSyncTick(ctx, rc, true)
cancel() cancel()
@@ -405,6 +419,44 @@ func lkPortfolioClients(ctx context.Context, rc *RuntimeConfig, q string) ([]LKP
return out, rows.Err() return out, rows.Err()
} }
// lkPortfolioAllPairs — все открытые торговые счета DEPO с портфелем (режим «для всех
// клиентов»): фильтр по префиксу счёта, договор ЛК выводится из счёта по шаблону,
// портфель — PORT_NUM. Только MSSQL-база DEPO (как и поиск одного клиента).
func lkPortfolioAllPairs(ctx context.Context, rc *RuntimeConfig) ([]lkPortfolioPair, error) {
fs := rc.Snapshot().Fancy
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
FROM OD_CLIENTS c LEFT JOIN OD_CLIENTS_PORTS p ON p.CLIENT=c.ID
WHERE c.NUMBER LIKE @pre AND c.STATE=0 AND c.D_CLOSE IS NULL AND p.PORT_NUM IS NOT NULL`,
sql.Named("pre", likeEscape(ps.accountPrefix())+"%"))
if err != nil {
return nil, err
}
defer rows.Close()
var out []lkPortfolioPair
seen := map[string]bool{}
for rows.Next() {
var port, acc string
if rows.Scan(&port, &acc) != nil || strings.TrimSpace(port) == "" {
continue
}
lk := lkContractByAccount(ps, acc)
if lk == "" || seen[lk] {
continue
}
seen[lk] = true
out = append(out, lkPortfolioPair{LK: lk, Depo: strings.TrimSpace(port)})
}
return out, rows.Err()
}
// lkPortfolioPushPair — залить портфель одного клиента сейчас (тест / «Залить сейчас»). // lkPortfolioPushPair — залить портфель одного клиента сейчас (тест / «Залить сейчас»).
func lkPortfolioPushPair(ctx context.Context, rc *RuntimeConfig, pr lkPortfolioPair) { func lkPortfolioPushPair(ctx context.Context, rc *RuntimeConfig, pr lkPortfolioPair) {
lines, skipped, err := lkPortfolioLines(ctx, rc, pr.Depo, nil) lines, skipped, err := lkPortfolioLines(ctx, rc, pr.Depo, nil)
@@ -681,12 +681,14 @@
<label style="display:flex;align-items:flex-start;gap:8px;font-weight:400;flex-basis:100%;margin-top:6px"><input type="checkbox" name="emulate_nsd" style="width:auto;margin-top:3px" {{if .Settings.LK.EmulateNSD}}checked{{end}} onchange="this.form.submit()"> <span><b>Имитация НРД</b> — заявки ЛК проходят сверку с DEPO, блокировку в QUIK и получают статус в ЛК, но в НРД не отправляются: ответ контрагента имитирует bj.{{if and (eq .Settings.LK.ActiveEnv "prod") (not .Settings.LK.EmulateNSD)}} <span style="color:var(--warn)">Сейчас включена принудительно: боевой ЛК при тестовом профиле ИШ ({{.Settings.NSD.Profile}}).</span>{{end}}</span></label>{{end}} <label style="display:flex;align-items:flex-start;gap:8px;font-weight:400;flex-basis:100%;margin-top:6px"><input type="checkbox" name="emulate_nsd" style="width:auto;margin-top:3px" {{if .Settings.LK.EmulateNSD}}checked{{end}} onchange="this.form.submit()"> <span><b>Имитация НРД</b> — заявки ЛК проходят сверку с DEPO, блокировку в QUIK и получают статус в ЛК, но в НРД не отправляются: ответ контрагента имитирует bj.{{if and (eq .Settings.LK.ActiveEnv "prod") (not .Settings.LK.EmulateNSD)}} <span style="color:var(--warn)">Сейчас включена принудительно: боевой ЛК при тестовом профиле ИШ ({{.Settings.NSD.Profile}}).</span>{{end}}</span></label>{{end}}
</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">включено</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 секунд, в том же проходе, что и приём заявок, 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>
{{else}}<button class="btn btn-ok" type="submit" name="action" value="on" onclick="if(!this.form.pairs.value.trim()){alert('Укажите договор ЛК и портфель DEPO, например ДД-816=БР-859');return false}">Включить заполнение портфеля ДЕПО бумагами</button>{{end}} {{if not .Settings.LK.PortfolioSync.AllClients}}<button class="btn" type="submit" name="action" value="on_all" onclick="return confirm('Включаю заполнение портфеля ДЕПО для ВСЕХ клиентов — все открытые торговые счёта. Портфели всех пользователей начнут отправляться в ЛК. Продолжить?')">Включить для всех пользователей</button>{{end}}
{{else}}<button class="btn btn-ok" type="submit" name="action" value="on_all" onclick="return confirm('Включаю заполнение портфеля ДЕПО для ВСЕХ клиентов — все открытые торговые счёта. Портфели всех пользователей начнут отправляться в ЛК. Продолжить?')">Включить для всех пользователей</button>
<button class="btn" type="submit" name="action" value="on" onclick="if(!this.form.pairs.value.trim()){alert('Укажите договор ЛК и портфель DEPO, например ДД-816=БР-859');return false}">Включить по списку</button>{{end}}
<div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;margin-top:8px"> <div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;margin-top:8px">
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px;min-width:260px">Каких клиентов заполнять: договор ЛК = портфель DEPO (через «;») <label style="display:flex;flex-direction:column;gap:3px;font-size:12px;min-width:260px">Каких клиентов заполнять: договор ЛК = портфель DEPO (через «;»)
<input name="pairs" value="{{.Settings.LK.PortfolioSync.Pairs}}" placeholder="ДД-816=БР-859"></label> <input name="pairs" value="{{.Settings.LK.PortfolioSync.Pairs}}" placeholder="ДД-816=БР-859"></label>
@@ -697,16 +699,16 @@
<form method="post" action="/admin/setup/lk-portfolio" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;margin-top:8px"> <form method="post" action="/admin/setup/lk-portfolio" style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;margin-top:8px">
<input type="hidden" name="naming" value="1"> <input type="hidden" name="naming" value="1">
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Префикс торгового счёта в DEPO <label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Префикс торгового счёта в DEPO
<input name="account_prefix" value="{{.Settings.LK.PortfolioSync.AccountPrefix}}" placeholder="{{depoAccPrefix}}" style="width:120px"></label> <input name="account_prefix" value="{{if .Settings.LK.PortfolioSync.AccountPrefix}}{{.Settings.LK.PortfolioSync.AccountPrefix}}{{else}}{{depoAccPrefix}}{{end}}" placeholder="{{depoAccPrefix}}" style="width:120px"></label>
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Номер договора депо в ЛК ({n} — номер счёта) <label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Номер договора депо в ЛК ({n} — номер счёта)
<input name="contract_template" value="{{.Settings.LK.PortfolioSync.ContractTemplate}}" placeholder="ДД-{n}" style="width:160px"></label> <input name="contract_template" value="{{if .Settings.LK.PortfolioSync.ContractTemplate}}{{.Settings.LK.PortfolioSync.ContractTemplate}}{{else}}ДД-{n}{{end}}" placeholder="ДД-{n}" style="width:160px"></label>
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Раздел счёта <label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Раздел счёта
<input name="section" value="{{.Settings.LK.PortfolioSync.Section}}" placeholder="Основной" style="width:130px"></label> <input name="section" value="{{if .Settings.LK.PortfolioSync.Section}}{{.Settings.LK.PortfolioSync.Section}}{{else}}Основной{{end}}" placeholder="Основной" style="width:130px"></label>
<label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Место хранения <label style="display:flex;flex-direction:column;gap:3px;font-size:12px">Место хранения
<input name="storage" value="{{.Settings.LK.PortfolioSync.Storage}}" placeholder="НРД" style="width:110px"></label> <input name="storage" value="{{if .Settings.LK.PortfolioSync.Storage}}{{.Settings.LK.PortfolioSync.Storage}}{{else}}НРД{{end}}" placeholder="НРД" style="width:110px"></label>
<button class="btn" type="submit" name="action" value="save">Сохранить</button> <button class="btn" type="submit" name="action" value="save">Сохранить</button>
</form> </form>
<p class="muted" style="font-size:12px;margin:4px 0 0">Пусто — значение по умолчанию (серым). Пример: счёт ВТНКЦ00000816 и шаблон ДД-{n} → договор ДД-816.</p> <p class="muted" style="font-size:12px;margin:4px 0 0">Заполнено из базы DEPO (префикс счёта) и значений по умолчанию — можно изменить. Пример: счёт ВТНКЦ00000816 и шаблон ДД-{n} → договор ДД-816.</p>
</details> </details>
<form method="post" action="/admin/setup/lk-portfolio" id="lkpf-client" style="margin:10px 0 0;padding-top:10px;border-top:1px dashed var(--border)"> <form method="post" action="/admin/setup/lk-portfolio" id="lkpf-client" style="margin:10px 0 0;padding-top:10px;border-top:1px dashed var(--border)">
<div style="font-size:13px;font-weight:600;margin-bottom:4px">Залить одному клиенту (для теста)</div> <div style="font-size:13px;font-weight:600;margin-bottom:4px">Залить одному клиенту (для теста)</div>