Files
Bridge-and-Join-s/internal/lkgateway/quikdb_handlers.go
T
fontvielle b654c70d49 QUIK: автоподбор торгового счёта фирмы из БД МНП
Кнопка «Определить из QUIK» у поля «Торговый счёт для поручений» (режим
чтение+запись). bj читает distinct Account из Actions/Export по коду фирмы
(QuikFirm/FirmID = код участника): один счёт — подставляет, несколько —
показывает на выбор, нет истории — просит ввести вручную. Только чтение.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-20 15:47:35 +03:00

345 lines
16 KiB
Go

package lkgateway
import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
// registerQuik — маршруты вкладки «QUIK МНП» (подключение к БД Модуля
// неторговых поручений). Только сохранение реквизитов и тест-подключение
// (read-only). БД боевая — запись не делаем.
func (h *setupHandlers) registerQuik(mux *http.ServeMux) {
mux.HandleFunc("/admin/setup/quik", h.saveQuik)
mux.HandleFunc("/admin/setup/quik/test", h.testQuik)
mux.HandleFunc("/admin/quik", h.quikInstructions)
mux.HandleFunc("/admin/setup/quik/cycle", h.saveQuikCycle)
mux.HandleFunc("/admin/setup/quik/mode", h.saveQuikMode)
mux.HandleFunc("/admin/setup/quik/detect-account", h.detectQuikAccount)
}
// quikInstructions — GET /admin/quik. Поручения M2M из БД МНП QUIK.
// ТОЛЬКО ЧТЕНИЕ (боевая БД): SELECT из экспортной таблицы Export.
func (h *setupHandlers) quikInstructions(w http.ResponseWriter, r *http.Request) {
s := h.rc.Snapshot().Quik
onlyActive := r.URL.Query().Get("all") == ""
var items []QuikInstruction
var loadErr string
if s.Server == "" {
loadErr = "Подключение к QUIK не настроено — Настройка → QUIK МНП"
} else if list, err := quikListM2MInstructions(r.Context(), s, 200, onlyActive); err != nil {
loadErr = "не удалось прочитать поручения: " + err.Error()
} else {
items = list
}
data := struct {
page
Items []QuikInstruction
Error string
OnlyActive bool
DBName string
Links []*QuikLink
Cycle QuikMNPSettings
Flash string
}{nowPage("Поручения QUIK", "quik"), items, loadErr, onlyActive, s.Database, globalQuikLinks.List(100), s, r.URL.Query().Get("flash")}
render(w, h.tpl.a.quikInstr, data)
}
func (h *setupHandlers) quikFromForm(r *http.Request) QuikMNPSettings {
cur := h.rc.Snapshot().Quik
port, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("port")))
if port == 0 {
port = cur.Port
}
pass := r.FormValue("password")
if pass == "" {
pass = cur.Password // не затираем сохранённый пароль пустым полем
}
enc := strings.TrimSpace(r.FormValue("encrypt"))
if enc == "" {
enc = "true"
}
return QuikMNPSettings{
Server: strings.TrimSpace(r.FormValue("server")),
Port: port,
Database: strings.TrimSpace(r.FormValue("database")),
User: strings.TrimSpace(r.FormValue("user")),
Password: pass,
Encrypt: enc,
Trust: r.FormValue("trust") != "",
Verified: cur.Verified,
// поля автоматического цикла M2M правятся отдельной формой — не затираем
WritesEnabled: cur.WritesEnabled, Emulate: cur.Emulate, RealWrite: cur.RealWrite, RealClientCode: cur.RealClientCode,
AutoOut: cur.AutoOut, AutoIn: cur.AutoIn, PollTerminal: cur.PollTerminal,
Account: cur.Account, SecOut: cur.SecOut, SecIn: cur.SecIn, ClientCodeMap: cur.ClientCodeMap,
}
}
// saveQuikCycle — POST /admin/setup/quik/cycle. Настройки автоматического цикла M2M в МНП.
func (h *setupHandlers) saveQuikCycle(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Quik
s.WritesEnabled = r.FormValue("writes_enabled") != ""
if devMode(h.rc) { // флажок имитации есть только в режиме разработки
s.Emulate = r.FormValue("emulate") != ""
}
s.AutoOut = r.FormValue("auto_out") != ""
s.AutoIn = r.FormValue("auto_in") != ""
if r.PostForm.Has("poll_terminal_set") { // галочка теперь в простой карточке режима; здесь — только если форма её прислала
s.PollTerminal = r.FormValue("poll_terminal") != ""
}
if r.PostForm.Has("account") { // счёт задаётся в карточке режима; здесь — только если форма его прислала
s.Account = strings.TrimSpace(r.FormValue("account"))
}
s.SecOut = strings.TrimSpace(r.FormValue("sec_out"))
s.SecIn = strings.TrimSpace(r.FormValue("sec_in"))
m := map[string]string{}
for _, line := range strings.Split(r.FormValue("client_code_map"), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
kv := strings.SplitN(line, "=", 2)
if len(kv) == 2 && strings.TrimSpace(kv[0]) != "" {
m[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
}
}
s.ClientCodeMap = m
if err := h.rc.UpdateQuik(s); err != nil {
setupFlash(w, r, "QUIK МНП: ошибка сохранения: "+err.Error())
return
}
mode := "записи в МНП ВЫКЛЮЧЕНЫ"
if s.Emulate && !devMode(h.rc) {
mode = "цикл остановлен — записи в МНП не выполняются"
} else if s.Emulate {
mode = fmt.Sprintf("ИМИТАЦИЯ (в МНП ничего не пишется): исходящие %s, входящие %s, поручения из терминала %s", onOff(s.AutoOut), onOff(s.AutoIn), onOff(s.PollTerminal))
} else if s.WritesEnabled {
mode = fmt.Sprintf("записи включены: исходящие %s, входящие %s, поручения из терминала %s", onOff(s.AutoOut), onOff(s.AutoIn), onOff(s.PollTerminal))
}
setupFlash(w, r, "QUIK МНП: цикл M2M сохранён — "+mode)
}
// saveQuikMode — POST /admin/setup/quik/mode (мастер): как bj работает с QUIK — одним выбором вместо набора
// флажков. check — только чтение (bj читает, в МНП ничего не меняет); auto — чтение и запись, боевая работа: bj ведёт поручения
// сам: исходящие (блокировка, списание или снятие) и входящие (зачисление). Тонкая настройка (инструменты,
// коды клиентов, поручения из терминала, имитация) — в «Настройке», здесь не трогается. Экран сохраняет
// выбор сразу (X-BJ-Ajax — ответ строкой), без JavaScript — обычной формой.
func (h *setupHandlers) saveQuikMode(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Quik
auto := r.FormValue("mode") == "auto"
s.WritesEnabled, s.AutoOut, s.AutoIn = auto, auto, auto
if r.PostForm.Has("account") {
s.Account = strings.TrimSpace(r.FormValue("account"))
}
if r.PostForm.Has("terminal_set") { // галочка «клиенты подают поручения из QUIK» — в той же карточке
s.PollTerminal = r.FormValue("terminal") != ""
}
msg := "только чтение — bj ничего в QUIK не меняет"
if auto {
msg = "чтение и запись — боевая работа: bj сам блокирует, списывает и зачисляет в QUIK"
if s.Account == "" {
msg += "; укажите торговый счёт"
}
}
if s.PollTerminal {
msg += "; поручения клиентов из QUIK (терминал и мобильное приложение) bj замечает и сообщает оператору"
}
if err := h.rc.UpdateQuik(s); err != nil {
msg = "не сохранилось: " + err.Error()
}
if r.Header.Get("X-BJ-Ajax") != "" {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte(msg))
return
}
setupFlash(w, r, "QUIK: "+msg)
}
func onOff(b bool) string {
if b {
return "авто"
}
return "вручную"
}
// quikClientCodeMapText — карта исключений в виде «ключ=значение» построчно (для формы).
func quikClientCodeMapText(m map[string]string) string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
b.WriteString(k + "=" + m[k] + "\n")
}
return b.String()
}
// saveQuik — POST /admin/setup/quik. Сохраняет реквизиты подключения.
func (h *setupHandlers) saveQuik(w http.ResponseWriter, r *http.Request) {
if !requireAdvanced(w, r) {
return
}
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.quikFromForm(r)
if s.Server == "" || s.User == "" {
setupFlash(w, r, "QUIK МНП: укажите адрес сервера и логин")
return
}
if s.Database == "" { // без имени базы модуль не подключается (база логина по умолчанию — не она)
setupFlash(w, r, "QUIK МНП: укажите имя базы модуля неторговых поручений — его сообщает администратор QUIK")
return
}
if err := h.rc.UpdateQuik(s); err != nil {
setupFlash(w, r, "QUIK МНП: ошибка сохранения: "+err.Error())
return
}
if r.FormValue("check") != "" { // «Сохранить и проверить» — одной кнопкой
h.testQuik(w, r)
return
}
setupFlash(w, r, fmt.Sprintf("QUIK МНП: настройки сохранены (%s:%d, логин %s)", s.Server, s.Port, s.User))
}
// quikCertHint — что делать, когда сервер МНП не прошёл проверку сертификата: у QUIK сертификат обычно свой
// (самоподписанный или на имя сервера, а подключаемся по IP).
const quikCertHint = "сертификат сервера МНП выписан не на этот адрес или самоподписанный (так у QUIK обычно и бывает). Включите «Доверять сертификату сервера» в «Дополнительно: база и шифрование» и проверьте ещё раз — соединение останется зашифрованным."
// quikCertError — подключение не прошло из-за проверки сертификата сервера (не из-за сети или логина).
func quikCertError(err error) bool {
m := strings.ToLower(err.Error())
return strings.Contains(m, "x509:") || strings.Contains(m, "failed to verify certificate") || strings.Contains(m, "certificate signed by unknown authority")
}
// quikNeedsTrust — последняя проверка упала на сертификате, а доверие выключено: раскрыть «Дополнительно».
func quikNeedsTrust(s QuikMNPSettings) bool {
return !s.Verified && !s.Trust && strings.Contains(s.LastReport, "Что сделать: сертификат")
}
// testQuik — POST /admin/setup/quik/test. Тест-подключение (только чтение).
// Формирует ПОЛНЫЙ отчёт (параметры + ответ сервера) для копирования коллегам.
func (h *setupHandlers) testQuik(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().Quik
if s.Server == "" {
setupFlash(w, r, "QUIK МНП: сначала сохраните реквизиты подключения")
return
}
now := time.Now()
var b strings.Builder
fmt.Fprintf(&b, "=== Проверка подключения к БД МНП QUIK ===\n")
fmt.Fprintf(&b, "Время: %s\n", now.Format("2006-01-02 15:04:05 MST"))
fmt.Fprintf(&b, "Сервер: %s:%d\n", s.Server, s.Port)
fmt.Fprintf(&b, "БД: %s\n", dfltDB(s.Database))
fmt.Fprintf(&b, "Логин: %s\n", s.User)
fmt.Fprintf(&b, "Шифрование: encrypt=%s, TrustServerCertificate=%v\n", s.Encrypt, s.Trust)
fmt.Fprintf(&b, "Клиент (bj): исходящий IP %s, драйвер go-mssqldb\n", pickStr(outboundIP(s.Server, s.Port), "не определён"))
fmt.Fprintf(&b, "----------------------------------------\n")
res, err := quikProbe(r.Context(), s)
go refreshVersions(h.rc) // строка версий над разделом — по новому подключению, а не через 10 минут
if err != nil {
fmt.Fprintf(&b, "РЕЗУЛЬТАТ: ОШИБКА ПОДКЛЮЧЕНИЯ\n\n")
b.WriteString(quikErrorDetail(err))
cert := quikCertError(err) && !s.Trust
if cert {
b.WriteString("\n\nЧто сделать: " + quikCertHint)
}
s.Verified = false
s.LastReport = b.String()
s.LastAt = now.UTC()
_ = h.rc.UpdateQuik(s)
if cert {
setupFlash(w, r, "QUIK МНП: "+quikCertHint)
return
}
setupFlash(w, r, "QUIK МНП: подключение не удалось — полный ответ сервера ниже (можно скопировать для коллег)")
return
}
// успех
fmt.Fprintf(&b, "РЕЗУЛЬТАТ: ПОДКЛЮЧЕНИЕ OK\n\n")
fmt.Fprintf(&b, "Версия сервера: %s\n", res.Version)
fmt.Fprintf(&b, "Текущая БД: %s\n", res.DBName)
fmt.Fprintf(&b, "Логин на серв.: %s\n", res.Login)
fmt.Fprintf(&b, "M2M-готовность: %v — %s\n\n", res.M2MReady, res.Note)
fmt.Fprintf(&b, "Объекты МНП/M2M (видимость под логином):\n")
keys := make([]string, 0, len(res.Objects))
for k := range res.Objects {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
mark := "нет"
if res.Objects[k] {
mark = "ДА"
}
fmt.Fprintf(&b, " %-28s %s\n", k, mark)
}
s.Verified = true
s.M2MMissing = ""
if !res.M2MReady {
var miss []string
for _, k := range []string{"table:Export", "table:Contracts", "proc:sp_NewInstrSec"} {
if !res.Objects[k] {
miss = append(miss, strings.SplitN(k, ":", 2)[1])
}
}
s.M2MMissing = strings.Join(miss, ", ")
}
s.LastReport = b.String()
s.LastAt = now.UTC()
_ = h.rc.UpdateQuik(s)
if s.M2MMissing != "" {
setupFlash(w, r, fmt.Sprintf("QUIK МНП: подключение есть (БД=%s), но под этим логином не видно объектов M2M: %s. Проверьте имя базы и права логина (роль InstrEu). Полный отчёт ниже.", res.DBName, s.M2MMissing))
return
}
setupFlash(w, r, fmt.Sprintf("QUIK МНП: ПОДКЛЮЧЕНИЕ OK (БД=%s). Полный отчёт ниже.", res.DBName))
}
func dfltDB(db string) string {
if db == "" {
return "(по умолчанию для логина)"
}
return db
}
// quikModeCustom — режим отличается от двух простых вариантов (записи включены, но исходящие или входящие
// выключены; или наоборот): карточка режима говорит об этом, а «Расширенные настройки» открыты.
func quikModeCustom(q QuikMNPSettings) string {
switch {
case q.WritesEnabled && !q.AutoOut && !q.AutoIn:
return "Запись в QUIK разрешена, но исходящие и входящие ведутся вручную"
case q.WritesEnabled && !q.AutoOut:
return "Исходящие переводы ведутся вручную"
case q.WritesEnabled && !q.AutoIn:
return "Входящие переводы зачисляются вручную"
case !q.WritesEnabled && (q.AutoOut || q.AutoIn):
return "Автоматика включена, но запись в QUIK запрещена главным выключателем"
}
return ""
}