Files
Bridge-and-Join-s/internal/lkgateway/quikready.go
T
zuevav 47b183a072 QUIK: режимы «Только чтение» / «Чтение и запись» вместо «сверка / автоматически»
Система работает автоматически, ручного режима нет: выбор — только читать QUIK
(настройка и проверка: ничего не блокирует, не разблокирует, не меняет лимиты)
или читать и писать (боевая работа). В проверке МНП строка «Режим работы с QUIK»:
только чтение — сведение (ℹ), а не предупреждение, с объяснением, как включить
боевую работу.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-06 17:16:14 +03:00

177 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")})
})
}