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")}) }) }