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) } // 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, 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") != "" s.AutoOut = r.FormValue("auto_out") != "" s.AutoIn = r.FormValue("auto_in") != "" s.PollTerminal = r.FormValue("poll_terminal") != "" 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.WritesEnabled { mode = fmt.Sprintf("записи включены: исходящие %s, входящие %s, поручения из терминала %s", onOff(s.AutoOut), onOff(s.AutoIn), onOff(s.PollTerminal)) } setupFlash(w, r, "QUIK МНП: цикл M2M сохранён — "+mode) } 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 err := h.rc.UpdateQuik(s); err != nil { setupFlash(w, r, "QUIK МНП: ошибка сохранения: "+err.Error()) return } setupFlash(w, r, fmt.Sprintf("QUIK МНП: настройки сохранены (%s:%d, логин %s)", s.Server, s.Port, s.User)) } // 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 195.239.71.22, драйвер go-mssqldb\n") fmt.Fprintf(&b, "----------------------------------------\n") res, err := quikProbe(r.Context(), s) if err != nil { fmt.Fprintf(&b, "РЕЗУЛЬТАТ: ОШИБКА ПОДКЛЮЧЕНИЯ\n\n") b.WriteString(quikErrorDetail(err)) s.Verified = false s.LastReport = b.String() s.LastAt = now.UTC() _ = h.rc.UpdateQuik(s) 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.LastReport = b.String() s.LastAt = now.UTC() _ = h.rc.UpdateQuik(s) setupFlash(w, r, fmt.Sprintf("QUIK МНП: ПОДКЛЮЧЕНИЕ OK (БД=%s). Полный отчёт ниже.", res.DBName)) } func dfltDB(db string) string { if db == "" { return "(по умолчанию для логина)" } return db }