Files
Bridge-and-Join-s/internal/lkgateway/emulators.go
T
fontvielle cd6e2c424f Эмуляция НРД: один переключатель Выключена/Подмена/Полная имитация; контур отдельно
Было: правило «боевой ЛК + тестовый профиль → принудительно имитатор» не давало выключить
эмуляцию, а подмена личности жила отдельным тумблером на другой странице — пользователь не мог
понять состояние и выключить.

Стало — явная модель (контур НРД и режим эмуляции независимы):
- «Выключена» — заявки идут в выбранный контур (NSD.Profile) с реальными данными клиента;
- «Подмена личности» — уходят в контур, но у клиентов из списка блок инвестора заменяется на
  тест-инвестора (Бубликов/Баранкин); DEPO и QUIK — по реальному клиенту;
- «Полная имитация» — в НРД ничего не уходит, bj сам отвечает (внутренний имитатор).
Режимы взаимоисключающие (ведутся флагами EmulateNSD/RealNSDTest.Enabled), принудительное
правило убрано. Единый 3-радио на /admin/emulators (как у QUIK), с показом контура, тест-инвестора
и списка паспортов; карточка на /admin/m2m-testing — статус + ссылка на «Эмуляторы».
Тесты обновлены под новую модель.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:02:06 +03:00

281 lines
12 KiB
Go

package lkgateway
// emulators.go — dev-страница «Эмуляторы» (/admin/emulators, только режим разработки):
// переключатели и настройки эмуляции НРД (имитатор контрагента для заявок ЛК) и МНП QUIK
// (шаги цикла без записи в боевую БД). Переключатели пишут существующие флаги
// LK.EmulateNSD и QuikMNP.Emulate — чекбоксы в «Настройке» показывают то же состояние.
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
)
// emuRejectOption — вариант кода отказа для эмулятора НРД (value — «07», подпись — с названием).
type emuRejectOption struct{ Code, Label string }
func emuRejectOptions() []emuRejectOption {
out := make([]emuRejectOption, 0, len(nsdEmuRejectCodes))
for _, c := range nsdEmuRejectCodes {
out = append(out, emuRejectOption{c, "M2M" + c + " — " + m2mCodeText("M2M"+c)})
}
return out
}
// registerEmulators вешает /admin/emulators (GET — страница, POST …/nsd и …/quik — сохранение, ajax).
func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
mux.HandleFunc("/admin/emulators", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := rc.Snapshot()
nsdOn, nsdWhy := lkEmulateNSD(rc)
quikMode := "off"
if s.Quik.Emulate {
quikMode = "emulate"
} else if s.Quik.RealWrite {
quikMode = "real"
}
data := struct {
page
NSDEmuOn bool
NSDEmuWhy string
NSDFlag bool
NSDMode string
Profile string
ProfileTest bool
NSD NSDEmuSettings
RejectCodes []emuRejectOption
RealNSDOn bool
RealNSDDocs string
RealNSDAs string
QuikEmuOn bool
QuikMode string
QuikRealCode string
MNP MNPEmuSettings
Flash string
}{
page: nowPage("Эмуляторы", "emulators"),
NSDEmuOn: nsdOn,
NSDEmuWhy: nsdWhy,
NSDFlag: s.LK.EmulateNSD,
NSDMode: nsdEmuMode(rc),
Profile: s.NSD.Profile,
ProfileTest: strings.HasPrefix(strings.ToLower(s.NSD.Profile), "test"),
RealNSDOn: s.LK.RealNSDTest.Enabled,
RealNSDDocs: s.LK.RealNSDTest.Docs,
RealNSDAs: realNSDAsText(),
NSD: s.NSDEmu.Defaults(),
RejectCodes: emuRejectOptions(),
QuikEmuOn: s.Quik.Emulate && devMode(rc),
QuikMode: quikMode,
QuikRealCode: s.Quik.RealClientCode,
MNP: s.MNPEmu,
Flash: r.URL.Query().Get("flash"),
}
render(w, a.emulators, data)
})
mux.HandleFunc("/admin/emulators/nsd", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
lk := rc.Snapshot().LK
// Режим эмуляции НРД — взаимоисключающий: off | substitute | imitate.
switch r.FormValue("mode") {
case "imitate":
lk.EmulateNSD, lk.RealNSDTest.Enabled = true, false
case "substitute":
lk.EmulateNSD, lk.RealNSDTest.Enabled = false, true
if d := strings.TrimSpace(r.FormValue("docs")); d != "" {
lk.RealNSDTest.Docs = d
}
default: // off
lk.EmulateNSD, lk.RealNSDTest.Enabled = false, false
}
delay, err := strconv.Atoi(strings.TrimSpace(r.FormValue("delay_sec")))
if err != nil && strings.TrimSpace(r.FormValue("delay_sec")) != "" {
emuAjaxReply(w, r, http.StatusBadRequest, "Эмулятор НРД: задержка — целое число секунд")
return
}
emu := NSDEmuSettings{
DelaySec: delay,
Outcome: strings.TrimSpace(r.FormValue("outcome")),
RejectCode: strings.TrimSpace(r.FormValue("reject_code")),
}
if err := rc.UpdateNSDEmu(emu); err != nil {
emuAjaxReply(w, r, http.StatusBadRequest, "Эмулятор НРД: "+err.Error())
return
}
if err := rc.UpdateLK(lk); err != nil {
emuAjaxReply(w, r, http.StatusInternalServerError, "Эмулятор НРД: не сохранилось: "+err.Error())
return
}
applyNSDEmuSettings(rc)
prof := rc.Snapshot().NSD.Profile
msg := "выключена — заявки идут в выбранный контур НРД (" + prof + ") с реальными данными клиента"
switch nsdEmuMode(rc) {
case "imitate":
eff := rc.Snapshot().NSDEmu.Defaults()
msg = fmt.Sprintf("полная имитация — в НРД ничего не отправляется; ответ bj: %s через %d сек",
emuOutcomeRu(eff.Outcome), eff.DelaySec)
case "substitute":
msg = "подмена личности — в НРД (" + prof + ") вместо клиента из списка уходит тест-инвестор " + realNSDAsText()
}
emuAjaxReply(w, r, http.StatusOK, "Эмуляция НРД: "+msg)
})
mux.HandleFunc("/admin/emulators/quik", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
q := rc.Snapshot().Quik
// Режим взаимоисключающий: off | emulate (внутренняя имитация) | real (реальная запись на подставного клиента).
mode := r.FormValue("mode")
if mode != "emulate" && mode != "real" {
mode = "off"
}
q.Emulate = mode == "emulate"
q.RealWrite = mode == "real"
q.RealClientCode = strings.TrimSpace(r.FormValue("real_client_code"))
if q.RealWrite && q.RealClientCode == "" {
emuAjaxReply(w, r, http.StatusBadRequest, "Эмулятор МНП: укажите код клиента QUIK — на него уйдут реальные поручения")
return
}
delay, err := strconv.Atoi(strings.TrimSpace(r.FormValue("step_delay_sec")))
if err != nil && strings.TrimSpace(r.FormValue("step_delay_sec")) != "" {
emuAjaxReply(w, r, http.StatusBadRequest, "Эмулятор МНП: задержка — целое число секунд")
return
}
emu := MNPEmuSettings{
StepDelaySec: delay,
FailRegister: r.FormValue("fail_register") != "",
FailSign: r.FormValue("fail_sign") != "",
FailDeny: r.FormValue("fail_deny") != "",
FailDeposit: r.FormValue("fail_deposit") != "",
}
if err := rc.UpdateMNPEmu(emu); err != nil {
emuAjaxReply(w, r, http.StatusBadRequest, "Эмулятор МНП: "+err.Error())
return
}
if err := rc.UpdateQuik(q); err != nil {
emuAjaxReply(w, r, http.StatusBadRequest, "Эмулятор МНП: "+err.Error())
return
}
msg := "выключен — шаги цикла МНП выполняются в QUIK по-настоящему"
switch {
case q.Emulate && devMode(rc):
msg = "внутренняя имитация — шаги цикла (блокировка, подпись, снятие, зачисление) проходят без записи в МНП"
var fails []string
for _, f := range []struct {
on bool
name string
}{{emu.FailRegister, "регистрация"}, {emu.FailSign, "подпись"}, {emu.FailDeny, "снятие"}, {emu.FailDeposit, "зачисление"}} {
if f.on {
fails = append(fails, f.name)
}
}
if len(fails) > 0 {
msg += "; отказ на шагах: " + strings.Join(fails, ", ")
}
case q.RealWrite && devMode(rc):
msg = "РЕАЛЬНАЯ запись в боевую МНП на подставного клиента " + q.RealClientCode + " — поручения уходят по-настоящему, код клиента подменяется"
}
emuAjaxReply(w, r, http.StatusOK, "Эмулятор МНП: "+msg)
})
}
// emuAjaxReply — ответ сохранения: ajax (X-BJ-Ajax) — строкой с кодом, без JS — редирект с flash.
func emuAjaxReply(w http.ResponseWriter, r *http.Request, code int, msg string) {
if r.Header.Get("X-BJ-Ajax") != "" {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(code)
_, _ = w.Write([]byte(msg))
return
}
setupFlash(w, r, msg)
}
// emuOutcomeRu — исход эмулятора НРД по-русски.
func emuOutcomeRu(outcome string) string {
switch outcome {
case "reject":
return "отказ"
case "timeout":
return "не отвечает (таймаут)"
}
return "подтверждение"
}
// disableEmulatorsIfDevOff — безопасность: эмуляторы НРД/МНП и подмена личности в НРД
// («боевой ЛК → тестовый контур») действуют только в режиме разработки; когда он
// заканчивается (по времени dev_until или снят на сервере лицензий), флаги сбрасываются из
// настроек — иначе при следующей выдаче dev-режима эмуляция/подмена молча оживёт, а клиент
// без доступа к dev-вкладкам не смог бы вернуть стандартный режим. Вызывается при старте и в
// цикле продления лицензии.
func disableEmulatorsIfDevOff(rc *RuntimeConfig) {
if rc == nil || devMode(rc) {
return
}
s := rc.Snapshot()
if !s.LK.EmulateNSD && !s.LK.RealNSDTest.Enabled && !s.Quik.Emulate && !s.Quik.RealWrite {
return
}
var off []string
// Эмулятор НРД и подмена личности («боевой ЛК → тестовый контур НРД») — оба в настройках LK;
// сбрасываем одним обновлением, чтобы без режима разработки заявки шли под своими данными.
if s.LK.EmulateNSD || s.LK.RealNSDTest.Enabled {
lk := s.LK
lk.EmulateNSD = false
lk.RealNSDTest.Enabled = false
if err := rc.UpdateLK(lk); err != nil {
log.Printf("lk-gateway: авто-сброс эмулятора/подмены НРД: %v", err)
} else {
if s.LK.EmulateNSD {
off = append(off, "эмулятор НРД (заявки ЛК)")
}
if s.LK.RealNSDTest.Enabled {
off = append(off, "подмена личности в НРД (тест «боевой ЛК → TEST3»)")
}
}
}
if s.Quik.Emulate || s.Quik.RealWrite {
q := s.Quik
q.Emulate, q.RealWrite = false, false
if err := rc.UpdateQuik(q); err != nil {
log.Printf("lk-gateway: авто-сброс эмулятора МНП: %v", err)
} else {
if s.Quik.Emulate {
off = append(off, "эмулятор МНП QUIK (внутренняя имитация)")
}
if s.Quik.RealWrite {
off = append(off, "реальная запись в МНП на подставного клиента "+s.Quik.RealClientCode)
}
}
}
if len(off) == 0 {
return
}
log.Printf("lk-gateway: режим разработки закончился — эмуляторы отключены: %s", strings.Join(off, ", "))
body := "Выключены: " + strings.Join(off, ", ") + ". Флаги эмуляции сброшены из настроек: при следующем включении режима разработки эмуляторы не включатся сами — их нужно будет включить заново на странице «Эмуляторы»."
if s.Quik.RealWrite {
body += " Реальные поручения в боевую МНП на подставного клиента " + s.Quik.RealClientCode + " прекращены."
}
if (s.Quik.Emulate || s.Quik.RealWrite) && !s.Quik.WritesEnabled {
body += " Цикл M2M в МНП остановлен: записи в МНП не разрешены — для боевой работы выберите «Чтение и запись» в настройках QUIK."
}
_ = rc.AddNews(NewsItem{
ID: "sys-" + strconv.FormatInt(time.Now().Unix()/600, 10) + "-emulators-off",
At: time.Now(),
Kind: "system",
Title: "Эмуляторы отключены: режим разработки закончился",
Body: body,
})
}