47b183a072
Система работает автоматически, ручного режима нет: выбор — только читать QUIK (настройка и проверка: ничего не блокирует, не разблокирует, не меняет лимиты) или читать и писать (боевая работа). В проверке МНП строка «Режим работы с QUIK»: только чтение — сведение (ℹ), а не предупреждение, с объяснением, как включить боевую работу. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
177 lines
8.1 KiB
Go
177 lines
8.1 KiB
Go
package lkgateway
|
||
|
||
// Проверка модуля неторговых поручений QUIK (МНП) — только чтение (05.10.2026). QUIK у депозитариев
|
||
// стандартный, поэтому проверяется то, без чего цикл M2M в МНП не работает: объекты API, инструменты
|
||
// классов INSTROUTM2M/INSTRINM2M, справочник депозитариев-получателей, включённый автоматический цикл и
|
||
// ошибки последних операций. Вызывается кнопкой на шаге «QUIK МНП» и в итоговой проверке готовности.
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
||
)
|
||
|
||
// MNPCheck — одна строка проверки МНП.
|
||
type MNPCheck struct {
|
||
Title string `json:"title"`
|
||
State string `json:"state"` // ok | info (сведение, не проблема) | warn | err
|
||
Detail string `json:"detail"`
|
||
}
|
||
|
||
func quikMNPCheck(ctx context.Context, s QuikMNPSettings) []MNPCheck {
|
||
var out []MNPCheck
|
||
add := func(title, state, detail string) { out = append(out, MNPCheck{title, state, detail}) }
|
||
if strings.TrimSpace(s.Server) == "" {
|
||
add("Подключение к базе МНП", "err", "сервер МНП не указан")
|
||
return out
|
||
}
|
||
res, err := quikProbe(ctx, s)
|
||
if err != nil {
|
||
add("Подключение к базе МНП", "err", quikErrorDetail(err))
|
||
return out
|
||
}
|
||
add("Подключение к базе МНП", "ok", fmt.Sprintf("база %s, логин %s", res.DBName, res.Login))
|
||
|
||
var miss []string
|
||
for _, k := range []string{"table:Export", "table:M2MDepoRecipients", "table:Contracts", "proc:sp_NewInstrSec", "proc:sp_SignInstrSec", "proc:cancel_instruction"} {
|
||
if !res.Objects[k] {
|
||
miss = append(miss, strings.SplitN(k, ":", 2)[1])
|
||
}
|
||
}
|
||
if len(miss) > 0 {
|
||
add("Объекты API МНП для M2M", "err", "под этим логином не видно: "+strings.Join(miss, ", ")+". Нужна версия МНП с поддержкой M2M и права логина (роль InstrEu) — к администратору QUIK / ARQA")
|
||
} else {
|
||
add("Объекты API МНП для M2M", "ok", "таблицы и процедуры поручений M2M на месте")
|
||
}
|
||
|
||
conn, err := quikOpen(s)
|
||
if err != nil {
|
||
add("Инструменты M2M", "err", quikErrorDetail(err))
|
||
return out
|
||
}
|
||
defer conn.Close()
|
||
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||
defer cancel()
|
||
|
||
found := map[string]string{}
|
||
if rows, err := conn.QueryContext(cctx, fsql.Qk01); err == nil {
|
||
for rows.Next() {
|
||
var cls, sec string
|
||
if rows.Scan(&cls, &sec) == nil && found[cls] == "" {
|
||
found[cls] = sec
|
||
}
|
||
}
|
||
rows.Close()
|
||
} else {
|
||
add("Инструменты M2M", "err", "справочник инструментов: "+quikErrorDetail(err))
|
||
}
|
||
inst := func(cls, manual, what string) {
|
||
switch {
|
||
case strings.TrimSpace(manual) != "":
|
||
add("Инструмент «"+what+"»", "ok", "задан в настройках: "+manual)
|
||
case found[cls] != "":
|
||
add("Инструмент «"+what+"»", "ok", "класс "+cls+", инструмент "+found[cls])
|
||
default:
|
||
add("Инструмент «"+what+"»", "err", "в МНП нет включённого инструмента класса "+cls+" — администратору QUIK: включить класс и типы поручений M2M в QAdmin")
|
||
}
|
||
}
|
||
inst("INSTROUTM2M", s.SecOut, "Вывод ЦБ (M2M)")
|
||
inst("INSTRINM2M", s.SecIn, "Ввод ЦБ (M2M)")
|
||
|
||
var nrec int
|
||
if err := conn.QueryRowContext(cctx, "SELECT COUNT(*) FROM M2MDepoRecipients").Scan(&nrec); err != nil {
|
||
add("Справочник депозитариев-получателей", "err", quikErrorDetail(err))
|
||
} else {
|
||
quikRecipientsState.Lock()
|
||
total, at, rerr := quikRecipientsState.Total, quikRecipientsState.At, quikRecipientsState.Err
|
||
quikRecipientsState.Unlock()
|
||
switch {
|
||
case nrec == 0:
|
||
add("Справочник депозитариев-получателей", "err", "пуст — bj заполнит его по справочнику НРД (раз в 30 мин до 20:00); после первого заполнения модуль МНП нужно перезапустить")
|
||
case rerr != "":
|
||
add("Справочник депозитариев-получателей", "warn", fmt.Sprintf("в МНП %d записей; последнее обновление с ошибкой: %s", nrec, rerr))
|
||
case total > 0 && nrec < total:
|
||
add("Справочник депозитариев-получателей", "warn", fmt.Sprintf("в МНП %d из %d депозитариев НРД — bj дополнит при следующей сверке", nrec, total))
|
||
default:
|
||
d := fmt.Sprintf("%d депозитариев", nrec)
|
||
if !at.IsZero() {
|
||
d += ", сверен " + at.Format("02.01 15:04")
|
||
}
|
||
add("Справочник депозитариев-получателей", "ok", d+". Модуль МНП читает справочник при запуске — после первого заполнения его перезапускают")
|
||
}
|
||
}
|
||
|
||
switch {
|
||
case !s.WritesEnabled:
|
||
// не проблема, а начальный режим: подключились и читаем, в QUIK ничего не меняем
|
||
add("Режим работы с QUIK", "info", "только чтение: bj подключён к QUIK и читает данные, но ничего в нём не меняет — не блокирует и не разблокирует бумаги, не меняет лимиты. Когда будете готовы к боевой работе, включите «Чтение и запись» (в мастере — «Как bj работает с QUIK», в настройке — «Автоматический цикл M2M в МНП»)")
|
||
case s.Emulate:
|
||
add("Режим работы с QUIK", "warn", "включена имитация — шаги проходят без записи в МНП")
|
||
case !s.AutoOut && !s.AutoIn:
|
||
add("Режим работы с QUIK", "warn", "запись разрешена, но исходящие и входящие поручения в МНП не регистрируются автоматически")
|
||
default:
|
||
var on []string
|
||
if s.AutoOut {
|
||
on = append(on, "исходящие")
|
||
}
|
||
if s.AutoIn {
|
||
on = append(on, "входящие")
|
||
}
|
||
if s.PollTerminal {
|
||
on = append(on, "поручения из терминала")
|
||
}
|
||
add("Режим работы с QUIK", "ok", "чтение и запись (боевая работа): "+strings.Join(on, ", ")+"; торговый счёт "+quikAccount(s))
|
||
}
|
||
|
||
since := time.Now().AddDate(0, 0, -30)
|
||
var n, nerr int
|
||
var lastErr *QuikLink
|
||
for _, l := range globalQuikLinks.List(500) {
|
||
if l.UpdatedAt.Before(since) {
|
||
continue
|
||
}
|
||
n++
|
||
if l.State == "error" {
|
||
nerr++
|
||
if lastErr == nil || l.UpdatedAt.After(lastErr.UpdatedAt) {
|
||
lastErr = l
|
||
}
|
||
}
|
||
}
|
||
switch {
|
||
case n == 0:
|
||
add("Операции в МНП за 30 дней", "ok", "ещё не было")
|
||
case nerr > 0:
|
||
add("Операции в МНП за 30 дней", "warn", fmt.Sprintf("%d, из них с ошибкой %d; последняя (%s): %s", n, nerr, lastErr.UpdatedAt.Format("02.01 15:04"), lastErr.Err))
|
||
default:
|
||
add("Операции в МНП за 30 дней", "ok", fmt.Sprintf("%d, без ошибок", n))
|
||
}
|
||
return out
|
||
}
|
||
|
||
func mnpWorst(cs []MNPCheck) string {
|
||
w := "ok"
|
||
for _, c := range cs {
|
||
if c.State == "err" {
|
||
return "err"
|
||
}
|
||
if c.State == "warn" {
|
||
w = "warn"
|
||
}
|
||
}
|
||
return w
|
||
}
|
||
|
||
func registerQuikReady(mux *http.ServeMux, rc *RuntimeConfig) {
|
||
mux.HandleFunc("/admin/setup/quik/mnp-check", func(w http.ResponseWriter, r *http.Request) {
|
||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||
defer cancel()
|
||
cs := quikMNPCheck(ctx, rc.Snapshot().Quik)
|
||
jsonOut(w, map[string]any{"checks": cs, "state": mnpWorst(cs), "at": time.Now().Format("02.01.2006 15:04:05")})
|
||
})
|
||
}
|