Мастер, QUIK: короткое письмо в ARQA с внешним адресом, памятка администратору, понятный выбор работы с QUIK
- Письмо в ARQA: организация просит подключить модуль неторговых поручений, выдать логин и пароль и разрешить подключение с нашего внешнего адреса — о bj и внутреннем устройстве ни слова. Внешний адрес bj узнаёт у сервера лицензий (GET /v1/ip, раз в час); пометка — убедиться, что адрес постоянный. - Под письмом — «Памятка администратору QUIK» (классы, логин InstrEu, справочник, перезапуск модуля) со ссылкой на подробную инструкцию. - Вместо свёрнутого «Автоматический цикл M2M — выключен · настроить» и справочника под раскрытием: после проверки подключения — «Как bj работает с QUIK»: «Только сверка» или «Автоматически» (+ торговый счёт), сохраняется сразу; справочник — строкой состояния с напоминанием о перезапуске. До проверки — одна строка. Тонкая настройка — в «Настройка → QUIK», мастер её не трогает. - Проверка МНП: ошибка сертификата (x509, «doesn't contain any IP SANs») — подсказка включить «Доверять сертификату сервера», «Дополнительно» раскрывается. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -55,6 +55,7 @@ var templateFuncs = template.FuncMap{
|
||||
"realTest": realTestGet,
|
||||
"realNSDAs": realNSDAsText,
|
||||
"quikRecipientsInfo": quikRecipientsInfo,
|
||||
"quikNeedsTrust": quikNeedsTrust,
|
||||
"lkPortfolioStates": lkPortfolioStates,
|
||||
"backupStatus": backupStatus,
|
||||
"backupWhen": backupWhen,
|
||||
|
||||
@@ -64,12 +64,11 @@ func letterFor(s Settings, key string) (Letter, bool) {
|
||||
Subject: "Заявка на подключение к промышленному контуру для переводов M2M — " + name,
|
||||
Body: fmt.Sprintf("В НКО АО НРД\n(служба поддержки электронного документооборота)\n\n%s (ИНН %s, код участника %s) завершила тестирование обмена сообщениями о переводах ценных бумаг M2M на контуре TEST3 и просит подключить промышленный контур электронного документооборота НРД.\n\nСведения для подключения:\n%s\n— ключ электронной подписи для промышленного контура: выдан УЦ Московской биржи [номер сертификата, срок действия]\n\nЕсли для подключения установлена форма заявки — просим её направить, а это письмо считать сопроводительным.%s",
|
||||
name, inn, code, req, sign)}, true
|
||||
case "arqa":
|
||||
ip := pickStr(outboundIP(s.Quik.Server, s.Quik.Port), firstServerIP())
|
||||
case "arqa": // коротко и по делу: о внутреннем устройстве (bj, процедуры, классы) поставщику знать не нужно
|
||||
return Letter{Key: key, Title: "Письмо в ARQA Technologies: модуль неторговых поручений", To: "ARQA Technologies, служба поддержки QUIK",
|
||||
Subject: "Подключение модуля неторговых поручений QUIK для переводов M2M — " + name,
|
||||
Body: fmt.Sprintf("В ARQA Technologies\n(служба поддержки QUIK)\n\n%s (ИНН %s) подключает систему Bridge&Join для переводов ценных бумаг M2M через НРД. Система работает с модулем неторговых поручений (МНП) QUIK через его базу данных. Просим:\n\n1. Подключить и настроить модуль неторговых поручений для переводов M2M:\n — классы инструментов INSTROUTM2M (вывод ценных бумаг) и INSTRINM2M (ввод ценных бумаг);\n — типы неторговых поручений для M2M (100/101) в QAdmin;\n — справочник депозитариев-получателей M2MDepoRecipients: его заполняет внешняя система.\n2. Выдать SQL-логин для внешней системы с правами API модуля (роль InstrEu): процедуры sp_NewInstrSec, sp_SignInstrSec, cancel_instruction; таблицы Export, Contracts, M2MDepoRecipients.\n3. Разрешить подключение к базе данных МНП с сервера Bridge&Join (адрес %s) и сообщить адрес сервера, порт и имя базы.\n4. После первого заполнения справочника M2MDepoRecipients перезапустить модуль неторговых поручений: справочник читается при запуске.%s",
|
||||
name, inn, orBracket(ip, "IP-адрес сервера bj"), sign)}, true
|
||||
Subject: "Подключение модуля неторговых поручений QUIK — " + name,
|
||||
Body: fmt.Sprintf("В ARQA Technologies\n(служба поддержки QUIK)\n\n%s (ИНН %s) просит:\n\n1. Подключить модуль неторговых поручений QUIK (переводы ценных бумаг M2M).\n2. Выдать логин и пароль для подключения к базе данных модуля неторговых поручений.\n3. Разрешить подключение к базе данных модуля с нашего адреса %s и сообщить адрес сервера, порт и имя базы.%s",
|
||||
name, inn, arqaAddr(publicIP()), sign)}, true
|
||||
case "dba":
|
||||
ip := pickStr(outboundIP(s.Fancy.Server, s.Fancy.Port), firstServerIP())
|
||||
f := s.Fancy
|
||||
@@ -89,6 +88,16 @@ func letterFor(s Settings, key string) (Letter, bool) {
|
||||
return Letter{}, false
|
||||
}
|
||||
|
||||
// arqaAddr — наш внешний адрес для письма в ARQA с пометкой для отправителя: подключение разрешат только
|
||||
// с этого адреса, поэтому он должен быть постоянным (у провайдера адрес может меняться). Пометку перед
|
||||
// отправкой удаляют — как и всё в квадратных скобках.
|
||||
func arqaAddr(ip string) string {
|
||||
if ip == "" {
|
||||
return "[постоянный внешний IP-адрес вашего сервера — уточните у сетевого администратора]"
|
||||
}
|
||||
return ip + " [убедитесь, что это постоянный внешний адрес вашей организации и он не меняется — уточните у сетевого администратора; пометку удалите]"
|
||||
}
|
||||
|
||||
func firstServerIP() string {
|
||||
_, ips := localAddrs()
|
||||
for _, ip := range ips {
|
||||
|
||||
@@ -462,6 +462,13 @@ func TestDumpOnboardingPages(t *testing.T) {
|
||||
h.renderWizard(w, httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/wizard?step=%d", step), nil))
|
||||
_ = os.WriteFile(filepath.Join(dir, fmt.Sprintf("wizard-%02d.html", step)), w.Body.Bytes(), 0o644)
|
||||
}
|
||||
// шаг QUIK после проверки подключения — выбор «Как bj работает с QUIK»
|
||||
q := rc.Snapshot().Quik
|
||||
q.Server, q.User, q.Verified = "91.209.122.88", "BROKER_88", true
|
||||
_ = rc.UpdateQuik(q)
|
||||
w := httptest.NewRecorder()
|
||||
h.renderWizard(w, httptest.NewRequest(http.MethodGet, "/admin/wizard?step=5", nil))
|
||||
_ = os.WriteFile(filepath.Join(dir, "wizard-05-verified.html"), w.Body.Bytes(), 0o644)
|
||||
var b strings.Builder
|
||||
d := PrepareData{page: page{Title: "Подготовка модулей", Active: "wizard"}, Tenant: "АО «Пример Инвест»", Plan: "enterprise", Until: "поддержка до 06.10.2027", Next: "/admin/wizard?step=12", Flash: "Администратор создан."}
|
||||
if err := a.prepare.ExecuteTemplate(&b, "layout", d); err != nil {
|
||||
@@ -644,3 +651,82 @@ func TestSaveOrgWithRequisites(t *testing.T) {
|
||||
t.Fatalf("неверный счёт — ничего не сохраняется: %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
// Письмо в ARQA — коротко: организация просит подключить модуль, выдать логин и пароль и разрешить подключение
|
||||
// с нашего внешнего адреса; о bj и внутреннем устройстве ни слова. Внешний адрес неизвестен — место для него.
|
||||
func TestArqaLetter(t *testing.T) {
|
||||
publicIPState.Lock()
|
||||
publicIPState.ip, publicIPState.at = "195.239.71.22", time.Now()
|
||||
publicIPState.Unlock()
|
||||
defer func() {
|
||||
publicIPState.Lock()
|
||||
publicIPState.ip, publicIPState.at = "", time.Time{}
|
||||
publicIPState.Unlock()
|
||||
}()
|
||||
var s Settings
|
||||
s.NSD.OrgName, s.NSD.OrgINN, s.NSD.OrgCode = "ООО \"ИК \"Фонтвьель\"", "7703807489", "MC0413600000"
|
||||
l, ok := letterFor(s, "arqa")
|
||||
if !ok {
|
||||
t.Fatal("нет письма arqa")
|
||||
}
|
||||
for _, bad := range []string{"Bridge", "bj", "InstrEu", "sp_NewInstrSec", "M2MDepoRecipients", "10.10."} {
|
||||
if strings.Contains(l.Body, bad) {
|
||||
t.Fatalf("в письме лишнее %q:\n%s", bad, l.Body)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"7703807489", "просит", "логин и пароль", "с нашего адреса 195.239.71.22 [убедитесь, что это постоянный внешний адрес"} {
|
||||
if !strings.Contains(l.Body, want) {
|
||||
t.Fatalf("в письме нет %q:\n%s", want, l.Body)
|
||||
}
|
||||
}
|
||||
publicIPState.Lock()
|
||||
publicIPState.ip = ""
|
||||
publicIPState.Unlock()
|
||||
if l, _ := letterFor(s, "arqa"); !strings.Contains(l.Body, "[постоянный внешний IP-адрес вашего сервера") {
|
||||
t.Fatalf("внешний адрес неизвестен — место для него:\n%s", l.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// Мастер, QUIK: один выбор вместо набора флажков; тонкая настройка не трогается. Ошибка сертификата — подсказка.
|
||||
func TestQuikModeAndCertHint(t *testing.T) {
|
||||
priv := testLicenseKey(t)
|
||||
rc := testRC(t)
|
||||
now := time.Now().UTC()
|
||||
tok := signTestLicense(t, priv, &license.License{Schema: 1, ID: "lic-1", Tenant: "Тест", Product: "bj-server", Plan: license.PlanPro,
|
||||
IssuedAt: now, ExpiresAt: now.Add(30 * 24 * time.Hour), ContractUntil: now.AddDate(1, 0, 0), Features: []string{"updates", "quik-mnp"}})
|
||||
if err := rc.SaveLicense(LicenseSettings{Key: tok}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old := globalRC
|
||||
globalRC = rc
|
||||
defer func() { globalRC = old }()
|
||||
q := rc.Snapshot().Quik
|
||||
q.PollTerminal, q.SecOut, q.ClientCodeMap = true, "RUROUTM2M", map[string]string{"БР-1": "BR1"}
|
||||
if err := rc.UpdateQuik(q); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := &setupHandlers{rc: rc}
|
||||
post := func(form url.Values) string {
|
||||
r := httptest.NewRequest(http.MethodPost, "/admin/setup/quik/mode", strings.NewReader(form.Encode()))
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
r.Header.Set("X-BJ-Ajax", "1")
|
||||
w := httptest.NewRecorder()
|
||||
h.saveQuikMode(w, r)
|
||||
return w.Body.String()
|
||||
}
|
||||
if msg := post(url.Values{"mode": {"auto"}, "account": {"L01-00000F00"}}); !strings.Contains(msg, "автоматически") {
|
||||
t.Fatalf("ответ: %q", msg)
|
||||
}
|
||||
s := rc.Snapshot().Quik
|
||||
if !s.WritesEnabled || !s.AutoOut || !s.AutoIn || s.Account != "L01-00000F00" || !s.PollTerminal || s.SecOut != "RUROUTM2M" || s.ClientCodeMap["БР-1"] != "BR1" {
|
||||
t.Fatalf("автоматически: %+v", s)
|
||||
}
|
||||
post(url.Values{"mode": {"check"}})
|
||||
if s := rc.Snapshot().Quik; s.WritesEnabled || s.AutoOut || s.AutoIn || s.Account != "L01-00000F00" {
|
||||
t.Fatalf("только сверка (счёт не стирается): %+v", s)
|
||||
}
|
||||
if !quikCertError(errors.New("TLS Handshake failed: tls: failed to verify certificate: x509: cannot validate certificate for 91.209.122.88 because it doesn't contain any IP SANs")) ||
|
||||
quikCertError(errors.New("mssql: login error: Login failed for user 'BROKER_88'")) {
|
||||
t.Fatal("ошибка сертификата распознаётся, ошибка логина — нет")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package lkgateway
|
||||
|
||||
// Внешний адрес сервера bj (06.10.2026) — каким его видят снаружи. Нужен для письма в ARQA: поставщика просят
|
||||
// разрешить подключение к модулю неторговых поручений с нашего адреса, и внутренний адрес (10.x, 192.168.x)
|
||||
// ему ничего не даёт. Изнутри сети внешний адрес не узнать, поэтому спрашиваем наш же сервер лицензий
|
||||
// (GET /v1/ip — он видит, откуда пришёл запрос); сторонние «узнать мой IP» для сервера финансовой
|
||||
// организации не годятся. Раз в час, в фоне: письмо строится при показе страницы и сеть не ждёт.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var publicIPState struct {
|
||||
sync.Mutex
|
||||
ip string
|
||||
at time.Time
|
||||
}
|
||||
|
||||
// publicIP — последний известный внешний адрес (пусто — сервер лицензий недоступен, например закрытый контур).
|
||||
func publicIP() string {
|
||||
publicIPState.Lock()
|
||||
defer publicIPState.Unlock()
|
||||
if time.Since(publicIPState.at) > 24*time.Hour {
|
||||
return ""
|
||||
}
|
||||
return publicIPState.ip
|
||||
}
|
||||
|
||||
// publicIPRefresh — спросить внешний адрес у сервера лицензий.
|
||||
func publicIPRefresh(ctx context.Context) error {
|
||||
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(cctx, http.MethodGet, licenseServer+"/v1/ip", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := licenseHTTP.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(io.LimitReader(resp.Body, 64))
|
||||
ip := net.ParseIP(strings.TrimSpace(string(b)))
|
||||
if resp.StatusCode != http.StatusOK || ip == nil {
|
||||
return &net.AddrError{Err: "сервер лицензий не сообщил внешний адрес", Addr: resp.Status}
|
||||
}
|
||||
publicIPState.Lock()
|
||||
changed := publicIPState.ip != ip.String()
|
||||
publicIPState.ip, publicIPState.at = ip.String(), time.Now()
|
||||
publicIPState.Unlock()
|
||||
if changed {
|
||||
log.Printf("lk-gateway: внешний адрес сервера (по серверу лицензий): %s", ip)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// publicIPLoop — внешний адрес при запуске и раз в час (адрес у провайдера может смениться).
|
||||
func publicIPLoop(ctx context.Context) {
|
||||
for {
|
||||
_ = publicIPRefresh(ctx)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Hour):
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ func (h *setupHandlers) registerQuik(mux *http.ServeMux) {
|
||||
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)
|
||||
}
|
||||
|
||||
// quikInstructions — GET /admin/quik. Поручения M2M из БД МНП QUIK.
|
||||
@@ -122,6 +123,43 @@ func (h *setupHandlers) saveQuikCycle(w http.ResponseWriter, r *http.Request) {
|
||||
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"))
|
||||
}
|
||||
msg := "только сверка — в QUIK bj ничего не пишет"
|
||||
if auto {
|
||||
msg = "автоматически — bj ведёт поручения в QUIK сам"
|
||||
if s.Account == "" {
|
||||
msg += "; укажите торговый счёт"
|
||||
}
|
||||
}
|
||||
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 "авто"
|
||||
@@ -168,6 +206,21 @@ func (h *setupHandlers) saveQuik(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
@@ -195,10 +248,18 @@ func (h *setupHandlers) testQuik(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -365,6 +365,7 @@ func (s *Server) Run(ctx context.Context) error {
|
||||
|
||||
// Лицензия: ежесуточное продление ключа на сервере лицензий lic.zetit.ru.
|
||||
go superviseLoop(ctx, "license", s.rc, s.licenseLoop)
|
||||
go publicIPLoop(ctx) // внешний адрес сервера — для письма в ARQA (у сервера лицензий, раз в час)
|
||||
|
||||
// Авто-обновление сертификатов УЦ раз в сутки (если оператор включил).
|
||||
stopCACerts := StartCACertsAutoUpdater(s.rc)
|
||||
|
||||
@@ -86,6 +86,10 @@
|
||||
.dq-jobname { margin-top:14px; max-width:520px; }
|
||||
.dq-jobname label { display:block; margin:0; font-size:13.5px; font-weight:500; color:var(--text-2); }
|
||||
.dq-jobname input { display:block; width:100%; box-sizing:border-box; margin-top:5px; }
|
||||
.wiz-memo { margin:10px 0 0; font-size:13.5px; }
|
||||
.wiz-memo summary { cursor:pointer; color:var(--accent); }
|
||||
.wiz-memo ul { margin:6px 0 8px; padding-left:20px; }
|
||||
.wiz-memo li { margin:3px 0; }
|
||||
.dq-box { border:1px solid var(--border); border-radius:12px; padding:14px 16px 16px; }
|
||||
.dq-box-bj { background:color-mix(in srgb, var(--accent-weak) 45%, transparent); }
|
||||
.dq-box-h { font-weight:600; font-size:14.5px; margin:0 0 10px; }
|
||||
@@ -462,13 +466,49 @@
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Done.Quik}}ok{{else}}err{{end}}"></span>QUIK</h2>
|
||||
<p>Адрес и логин модуля неторговых поручений — у вашего администратора QUIK. Если модуля ещё нет, его подключает ARQA Technologies — письмо для них готово ниже.</p>
|
||||
{{if .DepoLicensed}}{{template "letterCard" (letter "arqa")}}{{end}}
|
||||
{{if .DepoLicensed}}{{template "letterCard" (letter "arqa")}}
|
||||
<details class="wiz-memo"><summary>Памятка администратору QUIK</summary>
|
||||
<p>Всё это описано в документации ARQA на модуль неторговых поручений — администратору QUIK она знакома:</p>
|
||||
<ul>
|
||||
<li>в QAdmin — классы <code>INSTROUTM2M</code> (вывод ценных бумаг) и <code>INSTRINM2M</code> (ввод), типы неторговых поручений M2M;</li>
|
||||
<li>SQL-логин для bj с ролью <code>InstrEu</code> — его логин и пароль вписываются ниже;</li>
|
||||
<li>справочник депозитариев-получателей <code>M2MDepoRecipients</code> заполняет bj; после первого заполнения модуль нужно перезапустить.</li>
|
||||
</ul>
|
||||
<a href="/admin/help/quik">Подробная инструкция для администратора QUIK →</a>
|
||||
</details>{{end}}
|
||||
{{if not .DepoLicensed}}<p class="muted">Модуль неторговых поручений не входит в текущую лицензию — шаг пропускается.</p>
|
||||
{{else}}{{with .Fancy}}{{template "quikSection" .}}{{end}}
|
||||
{{if .Settings.Quik.Server}}{{template "mnpCheck" .}}{{end}}
|
||||
{{if .Settings.Quik.M2MMissing}}<p style="color:var(--err);margin:8px 0">✗ Подключение есть, но под этим логином не видно объектов M2M: {{.Settings.Quik.M2MMissing}}. Проверьте имя базы МНП и права логина (роль InstrEu) и нажмите «Проверить» ещё раз.</p>{{end}}
|
||||
<details style="margin-top:12px;font-size:13px"><summary style="cursor:pointer;color:var(--accent)">Справочник депозитариев-получателей — bj заполняет сам</summary>
|
||||
<p style="margin:6px 0 0">Чтобы QUIK принимал поручения M2M, bj ведёт в МНП справочник депозитариев-получателей (<code>M2MDepoRecipients</code>) по справочнику участников НРД. <b>МНП читает его только при запуске</b> — после первого заполнения попросите администратора QUIK перезапустить модуль, иначе QUIK ответит «Неизвестный депозитарий получателя».<br><span class="muted">{{quikRecipientsInfo}}</span></p></details>{{end}}
|
||||
{{if .Settings.Quik.Verified}}
|
||||
<div class="card" id="quik-mode">
|
||||
<h2>Как bj работает с QUIK</h2>
|
||||
<form method="post" action="/admin/setup/quik/mode" id="quik-mode-form" class="dq">
|
||||
<input type="hidden" name="return_to" value="/admin/wizard?step=5">
|
||||
<div class="dq-tiles">
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="check" {{if not .Settings.Quik.WritesEnabled}}checked{{end}}><span><b>Только сверка</b><small>bj читает QUIK: проверяет клиента и остатки по каждому переводу. Поручения в QUIK вводят ваши сотрудники.</small></span></label>
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="auto" {{if .Settings.Quik.WritesEnabled}}checked{{end}}><span><b>Автоматически</b><small>bj сам ведёт поручения в QUIK: блокирует бумаги при отправке перевода, списывает или снимает по ответу НРД, зачисляет входящие.</small></span></label>
|
||||
</div>
|
||||
<div class="dq-jobname" id="quik-acc">
|
||||
<label>Торговый счёт для поручений<input type="text" name="account" value="{{.Settings.Quik.Account}}" placeholder="торговый счёт фирмы в QUIK" spellcheck="false" class="dq-mono"></label>
|
||||
</div>
|
||||
<p class="dq-note" id="quik-mode-st" aria-live="polite">Выбор сохраняется сразу. Тонкая настройка — инструменты МНП, коды клиентов, поручения из терминала — в «Настройка → QUIK».</p>
|
||||
<noscript><button type="submit" class="btn">Сохранить</button></noscript>
|
||||
</form>
|
||||
<p class="dq-note">{{quikRecipientsInfo}} После первого заполнения справочника депозитариев-получателей попросите администратора QUIK перезапустить модуль неторговых поручений — QUIK читает справочник только при запуске.</p>
|
||||
</div>
|
||||
<script>(function(){
|
||||
var f=document.getElementById('quik-mode-form'), st=document.getElementById('quik-mode-st'), acc=document.getElementById('quik-acc');
|
||||
function sync(){ acc.hidden=f.mode.value!=='auto'; }
|
||||
f.addEventListener('change', function(){ sync();
|
||||
fetch(f.action,{method:'POST',body:new URLSearchParams(new FormData(f)),headers:{'Content-Type':'application/x-www-form-urlencoded','X-BJ-Ajax':'1'},cache:'no-store'})
|
||||
.then(function(r){ return r.text().then(function(t){ st.textContent=(r.ok?'✓ Сохранено: ':'✗ ')+t; }); })
|
||||
.catch(function(){ st.textContent='✗ Не сохранилось — обновите страницу и выберите ещё раз.'; });
|
||||
});
|
||||
f.addEventListener('submit', function(e){ if(window.fetch) e.preventDefault(); });
|
||||
sync();
|
||||
})();</script>
|
||||
{{else}}<p class="muted" style="margin-top:12px">Когда подключение проверено, здесь появится выбор, как bj работает с QUIK.</p>{{end}}{{end}}
|
||||
{{template "wizNav" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<label>Пароль
|
||||
<input type="password" name="password" placeholder="{{if .Settings.Quik.Password}}сохранён — пусто, чтобы не менять{{end}}" autocomplete="new-password" style="width:100%"></label>
|
||||
</div>
|
||||
<details><summary style="cursor:pointer;color:var(--accent);font-size:13px">Дополнительно: база и шифрование (обычно не нужно)</summary>
|
||||
<details {{if quikNeedsTrust .Settings.Quik}}open{{end}}><summary style="cursor:pointer;color:var(--accent);font-size:13px">Дополнительно: база и шифрование</summary>
|
||||
<div style="display:grid;gap:10px;margin-top:8px">
|
||||
<label>База МНП <span class="muted" style="font-weight:400">— пусто: база логина по умолчанию</span>
|
||||
<input type="text" name="database" value="{{.Settings.Quik.Database}}" style="width:100%"></label>
|
||||
@@ -37,8 +37,9 @@
|
||||
<p class="muted" style="margin-top:12px">Статус: {{if .Settings.Quik.Verified}}<span style="color:var(--ok)">✓ подключение проверено</span>{{else}}<span class="muted">не проверено</span>{{end}}</p>
|
||||
</div>
|
||||
|
||||
{{if not .InWizard}}{{/* в мастере вместо этой карточки — выбор «Как bj работает с QUIK» (шаг QUIK) */}}
|
||||
<div class="card" id="sec-quik-cycle">
|
||||
{{if .InWizard}}<details><summary style="cursor:pointer;list-style:none"><h2 style="display:inline">{{else}}<h2>{{end}}<span class="dot {{if .Settings.Quik.Emulate}}warn{{else if not .Settings.Quik.WritesEnabled}}warn{{else if or .Settings.Quik.AutoOut .Settings.Quik.AutoIn}}ok{{else}}warn{{end}}"></span>Автоматический цикл M2M в МНП{{if .Settings.Quik.Emulate}} <span class="hero-status warn">{{if devOn}}имитация{{else}}остановлен{{end}}</span>{{end}}</h2>{{if .InWizard}} <span class="muted" style="font-size:13px">— {{if .Settings.Quik.WritesEnabled}}включён{{else}}выключен: bj ничего не пишет в QUIK{{end}} · настроить ▸</span></summary>{{end}}
|
||||
<h2><span class="dot {{if .Settings.Quik.Emulate}}warn{{else if not .Settings.Quik.WritesEnabled}}warn{{else if or .Settings.Quik.AutoOut .Settings.Quik.AutoIn}}ok{{else}}warn{{end}}"></span>Автоматический цикл M2M в МНП{{if .Settings.Quik.Emulate}} <span class="hero-status warn">{{if devOn}}имитация{{else}}остановлен{{end}}</span>{{end}}</h2>
|
||||
<p class="muted">Что делает bj в Модуле неторговых поручений по ходу перевода: <strong>исходящий</strong> — при отправке в НРД регистрирует «Вывод ЦБ (M2M)» (QUIK блокирует бумаги), по подтверждению подписывает (списание), по отказу снимает с причиной; <strong>входящий</strong> — после приёма нашего поручения НРД регистрирует «Ввод ЦБ (M2M)» (зачисление сразу). Каждый шаг виден в карточке перевода и на странице <a href="/admin/quik">QUIK МНП</a>.</p>
|
||||
<form method="post" action="/admin/setup/quik/cycle" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
|
||||
<input type="hidden" name="section" value="sec-quik-cycle">
|
||||
@@ -69,8 +70,8 @@
|
||||
<textarea name="client_code_map" rows="3" style="width:100%;font-family:ui-monospace,monospace;font-size:12px" placeholder="БР-12-3=BR123">{{quikCodeMap .Settings.Quik.ClientCodeMap}}</textarea></label>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="submit" class="btn">Сохранить</button></div>
|
||||
</form>
|
||||
{{if .InWizard}}</details>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Settings.Quik.LastReport}}
|
||||
<div class="card">
|
||||
|
||||
Reference in New Issue
Block a user