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