feat(admin): dev-страница «Эмуляторы» — переключатели и настройки эмуляции НРД и МНП QUIK
В режиме разработки новая страница /admin/emulators с двумя клавишами: - «Эмулятор НРД» (флаг LK.EmulateNSD): заявки ЛК и ручного ввода идут через внутренний имитатор, в НРД ничего не отправляется. Настройки: задержка решения (0–300 с), исход (подтвердить/отклонить/не отвечать), код отказа M2M01–M2M09/18. Сендер перенастраивается в рантайме через потокобезопасный mock.Sender.Reconfigure — без перезапуска. - «Эмулятор МНП QUIK» (флаг QuikMNP.Emulate): шаги цикла без записи в боевую БД МНП. Настройки: задержка шага (0–60 с), точечные отказы по шагам (регистрация/подпись/отказ/зачисление) — эмулированный отказ ведёт себя как боевой: шаг OK=false, состояние не продвигается, повтор на следующем проходе. Настройки хранятся в setup.json (nsd_emu/mnp_emu), обратная совместимость — дефолты. Баннер «Режим эмуляции» показывается и при ручном включении эмуляторов (раньше зависел только от отсутствия ИШ). Пункт меню «Эмуляторы» и две плитки на дашборде — только в dev. Пути занесены в devOnlyPath. Попутно: исправлен race в TestLicenseActivateAndRenew (cleanup восстанавливал DefaultLicensePublicKey без moduleEnforceMu — падал под -race и на чистом HEAD).
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
package lkgateway
|
||||
|
||||
// emulators.go — dev-страница «Эмуляторы» (/admin/emulators, только режим разработки):
|
||||
// переключатели и настройки эмуляции НРД (имитатор контрагента для заявок ЛК) и МНП QUIK
|
||||
// (шаги цикла без записи в боевую БД). Переключатели пишут существующие флаги
|
||||
// LK.EmulateNSD и QuikMNP.Emulate — чекбоксы в «Настройке» показывают то же состояние.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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)
|
||||
data := struct {
|
||||
page
|
||||
NSDEmuOn bool
|
||||
NSDEmuWhy string
|
||||
NSDFlag bool
|
||||
NSD NSDEmuSettings
|
||||
RejectCodes []emuRejectOption
|
||||
QuikEmuOn bool
|
||||
MNP MNPEmuSettings
|
||||
Flash string
|
||||
}{
|
||||
page: nowPage("Эмуляторы", "emulators"),
|
||||
NSDEmuOn: nsdOn,
|
||||
NSDEmuWhy: nsdWhy,
|
||||
NSDFlag: s.LK.EmulateNSD,
|
||||
NSD: s.NSDEmu.Defaults(),
|
||||
RejectCodes: emuRejectOptions(),
|
||||
QuikEmuOn: s.Quik.Emulate && devMode(rc),
|
||||
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
|
||||
lk.EmulateNSD = r.FormValue("emulate") != ""
|
||||
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)
|
||||
msg := "выключен — заявки ЛК идут в НРД по обычному пути"
|
||||
if on, why := lkEmulateNSD(rc); on {
|
||||
eff := rc.Snapshot().NSDEmu.Defaults()
|
||||
msg = fmt.Sprintf("включён (%s) — в НРД ничего не отправляется; решение контрагента: %s через %d сек",
|
||||
why, emuOutcomeRu(eff.Outcome), eff.DelaySec)
|
||||
}
|
||||
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
|
||||
q.Emulate = r.FormValue("emulate") != ""
|
||||
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.StatusInternalServerError, "Эмулятор МНП: не сохранилось: "+err.Error())
|
||||
return
|
||||
}
|
||||
msg := "выключен — шаги цикла МНП выполняются в QUIK по-настоящему"
|
||||
if 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, ", ")
|
||||
}
|
||||
}
|
||||
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 "подтверждение"
|
||||
}
|
||||
Reference in New Issue
Block a user