Первичная настройка, часть 2: шаг 9 «Оповещения и резервные копии», шаг 10 — итоговая проверка готовности (живые проверки со ссылкой «Исправить»); проверка модуля неторговых поручений QUIK (объекты M2M, инструменты, справочник получателей, цикл, ошибки операций); восстановление из копии на /first-run по одноразовому коду; возврат на тот же шаг мастера после «Сохранить» (return_to с ?step); пароль SMTP не выводится в страницу; карточки почты, адресатов и копий — общие для настроек и мастера

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-05 16:18:28 +03:00
parent fdf0ddd498
commit 17df53f25b
15 changed files with 847 additions and 155 deletions
+175
View File
@@ -0,0 +1,175 @@
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 | 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:
add("Автоматический цикл M2M", "warn", "запись в МНП выключена — bj не ставит и не снимает блокировки (Настройка → QUIK МНП)")
case s.Emulate:
add("Автоматический цикл M2M", "warn", "включена имитация — шаги проходят без записи в МНП")
case !s.AutoOut && !s.AutoIn:
add("Автоматический цикл M2M", "warn", "запись разрешена, но исходящие и входящие поручения в МНП не регистрируются автоматически")
default:
var on []string
if s.AutoOut {
on = append(on, "исходящие")
}
if s.AutoIn {
on = append(on, "входящие")
}
if s.PollTerminal {
on = append(on, "поручения из терминала")
}
add("Автоматический цикл M2M", "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")})
})
}