Files
Bridge-and-Join-s/internal/lkgateway/lkdata.go
zuevav 510a34b7bc Кабинет: курсы ЦБ, история стоимости портфеля, быстрые операции, контакты в профиле
Курсы валют. Оценивать замещающие и еврооблигации по курсу из копии учётной
системы было нельзя: в DEPO он от 31.05.2026, в SPECTRE — от 18.07.2026.
Берём официальный курс Банка России (открытый XML, без ключа — тем же каналом
пользуется Инвест-хаб) и складываем по дням в lk.fx_rates: без интернета
кабинет считает по последнему известному курсу, а не отказывается оценивать.
Источник переключается в настройках («Данные кабинета»), там же кнопка
«Обновить курсы сейчас». Портфель БР-719: 560,3 млн ₽ против 522 млн по
старому курсу.

История стоимости. Ни QUIK, ни SPECTRE её не хранят, поэтому раз в день после
закрытия торгов считаем стоимость каждого портфеля по экспорту QUIK и пишем
строку в lk.portfolio_daily (257 портфелей за 14 секунд). Кабинет рисует по
ним график на странице портфеля — SVG собирается на сервере, скриптов в
кабинете по-прежнему нет. Режим по умолчанию — «автоматически»: ведём, если
настроен экспорт QUIK, потому что задним числом историю не восстановить.

Операции. Связка «JOIN OD_RESTS ON sr.ID = CASE WHEN w.D_REST IN (…)»
заставляла SQL Server разбирать список счетов на каждой строке журнала
проводок: год данных читался 6–7 секунд. Обе стороны проводки выбираются
заранее двумя ветвями UNION ALL, индексы IX_WIRING_D_REST_WIRDATE и
IX_WIRING_K_REST_WIRDATE отрабатывают поиском — 89 мс на тех же 64 строках.
Период по умолчанию вернули к году, ответ кэшируется на пять минут.

Профиль: почта, телефон и прошлый вход из анкеты — клиент видит, куда с ним
свяжется депозитарий, и заметит устаревшие данные. Правка только через
менеджера. Заказ документов из кабинета убран совсем (форма была убрана
раньше, теперь нет и обработчика).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
2026-09-18 19:25:18 +03:00

118 lines
4.8 KiB
Go

package lkgateway
// lkdata.go — карточка «Данные кабинета» в настройках: откуда берём курсы валют
// и ведём ли ежедневный срез стоимости портфелей (для графика у клиента).
import (
"context"
"fmt"
"net/http"
"strings"
"time"
)
// LKDataView — состояние источников данных кабинета для страницы настроек.
type LKDataView struct {
FXSource string // cbr | accounting
FXAt string // дата последнего известного курса
FXCount int // сколько валют в последнем комплекте
FXNote string // пояснение/ошибка связи с Банком России
SnapshotMode string // "" (авто) | on | off
SnapshotOn bool // ведём ли по факту
SnapshotAt string // ЧЧ:ММ
Snapshots int // сколько дней истории накоплено
SnapLast string // последний снятый срез
SnapRows int // строк в последнем срезе
}
// lkDataView — собрать состояние без обращения в интернет (карточку открывают часто).
func lkDataView(rc *RuntimeConfig) LKDataView {
s := rc.Snapshot().LK
hour, min := lkSnapshotTime(s)
v := LKDataView{FXSource: strings.TrimSpace(s.FXSource), SnapshotMode: strings.TrimSpace(s.SnapshotMode),
SnapshotOn: lkSnapshotEnabled(rc), SnapshotAt: fmt.Sprintf("%02d:%02d", hour, min)}
if v.FXSource == "" {
v.FXSource = "cbr"
}
db, err := lkDB(rc)
if err != nil {
v.FXNote = "база bj недоступна: " + err.Error()
return v
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var day *time.Time
var n int
if db.QueryRowContext(ctx, `SELECT max(day) FROM lk.fx_rates`).Scan(&day) == nil && day != nil {
v.FXAt = day.Format("02.01.2006")
_ = db.QueryRowContext(ctx, `SELECT count(*) FROM lk.fx_rates WHERE day = $1`, *day).Scan(&n)
v.FXCount = n
}
var sday *time.Time
if db.QueryRowContext(ctx, `SELECT count(DISTINCT day), max(day) FROM lk.portfolio_daily`).Scan(&v.Snapshots, &sday) == nil && sday != nil {
v.SnapLast = sday.Format("02.01.2006")
_ = db.QueryRowContext(ctx, `SELECT count(*) FROM lk.portfolio_daily WHERE day = $1`, *sday).Scan(&v.SnapRows)
}
return v
}
// lkDataSave — POST /admin/setup/lk/data: источник курсов и ежедневный срез.
func (h *setupHandlers) lkDataSave(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
switch r.FormValue("op") {
case "fx_now": // проверить связь с Банком России и обновить курсы
at, rates, err := cbrFetch(r.Context(), time.Time{})
if err != nil {
setupFlash(w, r, "Банк России недоступен: "+err.Error()+". Курсы останутся прежними.")
return
}
lkFxSave(r.Context(), h.rc, at, "cbr", rates)
lkFxReset()
setupFlash(w, r, fmt.Sprintf("Курсы Банка России на %s получены: валют %d.", at.Format("02.01.2006"), len(rates)))
return
case "snapshot_now": // снять срез стоимости прямо сейчас
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
n, err := lkSnapshotRun(ctx, h.rc, time.Now())
if err != nil {
return
}
_ = n
}()
setupFlash(w, r, "Срез стоимости портфелей снимается — на 1300 договоров это несколько минут; обновите страницу.")
return
}
s := h.rc.Snapshot().LK
if v := strings.TrimSpace(r.FormValue("fx_source")); v == "cbr" || v == "accounting" {
s.FXSource = v
}
switch v := strings.TrimSpace(r.FormValue("snapshot_mode")); v {
case "", "on", "off":
s.SnapshotMode = v
}
if v := strings.TrimSpace(r.FormValue("snapshot_at")); len(v) == 5 && v[2] == ':' {
s.SnapshotAt = v
}
if err := h.rc.UpdateLK(s); err != nil {
setupFlash(w, r, "Данные кабинета: ошибка сохранения: "+err.Error())
return
}
lkFxReset()
src := "Банк России"
if s.FXSource == "accounting" {
src = "учётная система"
}
msg := "Курсы валют: " + src + "."
if s.SnapshotMode == "off" {
msg += " Ежедневный срез выключен — график в кабинете обновляться не будет."
} else {
hour, min := lkSnapshotTime(s)
msg += fmt.Sprintf(" Срез стоимости портфелей — ежедневно в %02d:%02d.", hour, min)
}
setupFlash(w, r, msg)
}