feat(admin): эмулятор МНП — режим реальной записи в МНП на подставного клиента
Третий режим МНП (между «внутренняя имитация» и «боевая запись»): в bj цикл идёт по обычному пользователю, а поручение в QUIK МНП уходит РЕАЛЬНО, но с кодом другого, реально существующего в QUIK клиента (обобщение ручного теста realtest «боевой ЛК → TEST3»). - QuikMNPSettings: RealWrite + RealClientCode (валидация формата кода). - Подмена кода — единая точка quikClientCodeFromPort (после ClientCodeMap); портфель не трогаем. quikActive: RealWrite только при devMode и непустом коде; Emulate (внутренняя имитация) в приоритете. - Шаги боевого пути помечаются «[на подставного клиента <код>]» — видно в «Поручениях QUIK». - Карточка МНП на /admin/emulators — три взаимоисключающих режима (выключено / имитация / реальная запись), поле кода, крупное предупреждение; отдельный красный баннер «Реальные поручения в МНП на клиента <код> (режим разработки)» — не смешиваем с баннером эмуляции НРД. - disableEmulatorsIfDevOff сбрасывает и RealWrite (код остаётся, инертен), новость явно фиксирует прекращение реальных поручений.
This commit is contained in:
@@ -27,7 +27,10 @@
|
||||
`dev_until` или снятии на сервере лицензий) флаги эмуляции автоматически
|
||||
сбрасываются из настроек (проверка при старте и в цикле продления лицензии,
|
||||
новость «Эмуляторы отключены») — при следующей выдаче dev-режима эмуляторы
|
||||
молча не оживут.
|
||||
молча не оживут. У эмулятора МНП есть и третий режим — реальная запись поручений
|
||||
в боевую МНП на подставного клиента (`QuikMNP.RealWrite` + `RealClientCode`:
|
||||
цикл идёт по обычному клиенту, код в поручениях подменяется на указанный;
|
||||
отдельный баннер-предупреждение, сбрасывается вместе с остальными эмуляторами).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -450,6 +450,7 @@ type page struct {
|
||||
Now string
|
||||
IsMockMode bool // true если ИШ или СКЗИ не настроены — обмена с НРД нет
|
||||
MockReason string // короткое описание причины (у клиента — без «mock»)
|
||||
MNPReal string // режим разработки: в боевую МНП пишутся РЕАЛЬНЫЕ поручения на подставного клиента (его код; пусто — режим выключен)
|
||||
NPR bool // модуль «НПР / УДС» включён и разрешён лицензией
|
||||
IGWDown string // ИШ НРД не отвечает: «N мин» (пусто — отвечает или не проверялся)
|
||||
IGWDetail string
|
||||
@@ -486,8 +487,9 @@ type homeData struct {
|
||||
LKUrl string
|
||||
|
||||
// Состояние эмуляторов (dev-плитки дашборда, страница «Эмуляторы»).
|
||||
NSDEmuOn bool
|
||||
QuikEmuOn bool
|
||||
NSDEmuOn bool
|
||||
QuikEmuOn bool
|
||||
QuikRealCode string // режим «реальная запись в МНП на подставного клиента» (код; пусто — выключен)
|
||||
}
|
||||
|
||||
// claimsData — данные журнала.
|
||||
@@ -615,6 +617,9 @@ func (a *admin) renderHome(w http.ResponseWriter, r *http.Request, svc *Service,
|
||||
}
|
||||
data.NSDEmuOn, _ = lkEmulateNSD(rc)
|
||||
data.QuikEmuOn = rc.Snapshot().Quik.Emulate && devMode(rc)
|
||||
if q := rc.Snapshot().Quik; q.RealWrite && devMode(rc) {
|
||||
data.QuikRealCode = strings.TrimSpace(q.RealClientCode)
|
||||
}
|
||||
// Готовность — по тем же шагам, что в мастере настройки (с учётом лицензии).
|
||||
for _, st := range setupSteps(rc) {
|
||||
data.TotalCount++
|
||||
@@ -916,6 +921,9 @@ func nowPage(title, active string) page {
|
||||
}
|
||||
p.MockReason += "эмулятор МНП QUIK включён — в боевую базу МНП ничего не пишется"
|
||||
}
|
||||
if s.Quik.RealWrite && p.DevMode {
|
||||
p.MNPReal = strings.TrimSpace(s.Quik.RealClientCode)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -34,26 +34,36 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
|
||||
}
|
||||
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
|
||||
NSD NSDEmuSettings
|
||||
RejectCodes []emuRejectOption
|
||||
QuikEmuOn bool
|
||||
MNP MNPEmuSettings
|
||||
Flash string
|
||||
NSDEmuOn bool
|
||||
NSDEmuWhy string
|
||||
NSDFlag bool
|
||||
NSD NSDEmuSettings
|
||||
RejectCodes []emuRejectOption
|
||||
QuikEmuOn bool
|
||||
QuikMode string
|
||||
QuikRealCode string
|
||||
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"),
|
||||
page: nowPage("Эмуляторы", "emulators"),
|
||||
NSDEmuOn: nsdOn,
|
||||
NSDEmuWhy: nsdWhy,
|
||||
NSDFlag: s.LK.EmulateNSD,
|
||||
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)
|
||||
})
|
||||
@@ -99,7 +109,18 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
|
||||
return
|
||||
}
|
||||
q := rc.Snapshot().Quik
|
||||
q.Emulate = r.FormValue("emulate") != ""
|
||||
// Режим взаимоисключающий: 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, "Эмулятор МНП: задержка — целое число секунд")
|
||||
@@ -117,12 +138,13 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
|
||||
return
|
||||
}
|
||||
if err := rc.UpdateQuik(q); err != nil {
|
||||
emuAjaxReply(w, r, http.StatusInternalServerError, "Эмулятор МНП: не сохранилось: "+err.Error())
|
||||
emuAjaxReply(w, r, http.StatusBadRequest, "Эмулятор МНП: "+err.Error())
|
||||
return
|
||||
}
|
||||
msg := "выключен — шаги цикла МНП выполняются в QUIK по-настоящему"
|
||||
if q.Emulate && devMode(rc) {
|
||||
msg = "включён — шаги цикла (блокировка, подпись, снятие, зачисление) проходят без записи в МНП"
|
||||
switch {
|
||||
case q.Emulate && devMode(rc):
|
||||
msg = "внутренняя имитация — шаги цикла (блокировка, подпись, снятие, зачисление) проходят без записи в МНП"
|
||||
var fails []string
|
||||
for _, f := range []struct {
|
||||
on bool
|
||||
@@ -135,6 +157,8 @@ func registerEmulators(mux *http.ServeMux, a *admin, rc *RuntimeConfig) {
|
||||
if len(fails) > 0 {
|
||||
msg += "; отказ на шагах: " + strings.Join(fails, ", ")
|
||||
}
|
||||
case q.RealWrite && devMode(rc):
|
||||
msg = "РЕАЛЬНАЯ запись в боевую МНП на подставного клиента " + q.RealClientCode + " — поручения уходят по-настоящему, код клиента подменяется"
|
||||
}
|
||||
emuAjaxReply(w, r, http.StatusOK, "Эмулятор МНП: "+msg)
|
||||
})
|
||||
@@ -171,7 +195,7 @@ func disableEmulatorsIfDevOff(rc *RuntimeConfig) {
|
||||
return
|
||||
}
|
||||
s := rc.Snapshot()
|
||||
if !s.LK.EmulateNSD && !s.Quik.Emulate {
|
||||
if !s.LK.EmulateNSD && !s.Quik.Emulate && !s.Quik.RealWrite {
|
||||
return
|
||||
}
|
||||
var off []string
|
||||
@@ -184,13 +208,18 @@ func disableEmulatorsIfDevOff(rc *RuntimeConfig) {
|
||||
off = append(off, "эмулятор НРД (заявки ЛК)")
|
||||
}
|
||||
}
|
||||
if s.Quik.Emulate {
|
||||
if s.Quik.Emulate || s.Quik.RealWrite {
|
||||
q := s.Quik
|
||||
q.Emulate = false
|
||||
q.Emulate, q.RealWrite = false, false
|
||||
if err := rc.UpdateQuik(q); err != nil {
|
||||
log.Printf("lk-gateway: авто-сброс эмулятора МНП: %v", err)
|
||||
} else {
|
||||
off = append(off, "эмулятор МНП QUIK")
|
||||
if s.Quik.Emulate {
|
||||
off = append(off, "эмулятор МНП QUIK (внутренняя имитация)")
|
||||
}
|
||||
if s.Quik.RealWrite {
|
||||
off = append(off, "реальная запись в МНП на подставного клиента "+s.Quik.RealClientCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(off) == 0 {
|
||||
@@ -198,7 +227,10 @@ func disableEmulatorsIfDevOff(rc *RuntimeConfig) {
|
||||
}
|
||||
log.Printf("lk-gateway: режим разработки закончился — эмуляторы отключены: %s", strings.Join(off, ", "))
|
||||
body := "Выключены: " + strings.Join(off, ", ") + ". Флаги эмуляции сброшены из настроек: при следующем включении режима разработки эмуляторы не включатся сами — их нужно будет включить заново на странице «Эмуляторы»."
|
||||
if s.Quik.Emulate && !s.Quik.WritesEnabled {
|
||||
if s.Quik.RealWrite {
|
||||
body += " Реальные поручения в боевую МНП на подставного клиента " + s.Quik.RealClientCode + " прекращены."
|
||||
}
|
||||
if (s.Quik.Emulate || s.Quik.RealWrite) && !s.Quik.WritesEnabled {
|
||||
body += " Цикл M2M в МНП остановлен: записи в МНП не разрешены — для боевой работы выберите «Чтение и запись» в настройках QUIK."
|
||||
}
|
||||
_ = rc.AddNews(NewsItem{
|
||||
|
||||
@@ -116,14 +116,14 @@ func TestEmulatorsSave(t *testing.T) {
|
||||
}
|
||||
|
||||
rec = post("/admin/emulators/quik", url.Values{
|
||||
"emulate": {"1"}, "step_delay_sec": {"7"}, "fail_register": {"1"}, "fail_deposit": {"1"},
|
||||
"mode": {"emulate"}, "step_delay_sec": {"7"}, "fail_register": {"1"}, "fail_deposit": {"1"},
|
||||
})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("quik: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
s = rc.Snapshot()
|
||||
if !s.Quik.Emulate || s.MNPEmu.StepDelaySec != 7 || !s.MNPEmu.FailRegister || !s.MNPEmu.FailDeposit || s.MNPEmu.FailSign {
|
||||
t.Errorf("после сохранения МНП: emulate=%v emu=%+v", s.Quik.Emulate, s.MNPEmu)
|
||||
if !s.Quik.Emulate || s.Quik.RealWrite || s.MNPEmu.StepDelaySec != 7 || !s.MNPEmu.FailRegister || !s.MNPEmu.FailDeposit || s.MNPEmu.FailSign {
|
||||
t.Errorf("после сохранения МНП: emulate=%v real=%v emu=%+v", s.Quik.Emulate, s.Quik.RealWrite, s.MNPEmu)
|
||||
}
|
||||
|
||||
// Валидация на хендлере: плохой код отказа — 400, настройки не тронуты.
|
||||
@@ -301,3 +301,116 @@ func TestDisableEmulatorsIfDevOffNoOp(t *testing.T) {
|
||||
t.Error("no-op не должен добавлять новость")
|
||||
}
|
||||
}
|
||||
|
||||
// Режимы МНП взаимоисключающие: хендлер выставляет Emulate/RealWrite по выбранному режиму.
|
||||
func TestEmulatorsQuikModes(t *testing.T) {
|
||||
devModeForced = true
|
||||
defer func() { devModeForced = false }()
|
||||
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
registerEmulators(mux, nil, rc)
|
||||
post := func(form url.Values) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(http.MethodPost, "/admin/emulators/quik", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("X-BJ-Ajax", "1")
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
// Реальная запись на подставного клиента.
|
||||
rec := post(url.Values{"mode": {"real"}, "real_client_code": {"BR859"}})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("real: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if s := rc.Snapshot().Quik; !s.RealWrite || s.Emulate || s.RealClientCode != "BR859" {
|
||||
t.Fatalf("режим real: %+v", s)
|
||||
}
|
||||
// Внутренняя имитация — наоборот, код сохраняется (инертен без флага).
|
||||
rec = post(url.Values{"mode": {"emulate"}, "real_client_code": {"BR859"}})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("emulate: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if s := rc.Snapshot().Quik; !s.Emulate || s.RealWrite {
|
||||
t.Fatalf("режим emulate: %+v", s)
|
||||
}
|
||||
// real без кода — 400.
|
||||
if rec := post(url.Values{"mode": {"real"}}); rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("real без кода: %d, ожидали 400", rec.Code)
|
||||
}
|
||||
// Код с недопустимыми символами — 400 (валидация UpdateQuik).
|
||||
if rec := post(url.Values{"mode": {"real"}, "real_client_code": {"БР 859!"}}); rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("плохой код: %d, ожидали 400", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Подмена кода клиента QUIK в режиме реальной записи — после обычной логики (ClientCodeMap не мешает).
|
||||
func TestQuikClientCodeRealWrite(t *testing.T) {
|
||||
s := QuikMNPSettings{ClientCodeMap: map[string]string{"БР-7-7": "BR777"}}
|
||||
if c := quikClientCodeFromPort(s, "БР-859 Баранкин"); c != "BR859" {
|
||||
t.Errorf("обычный режим: %q", c)
|
||||
}
|
||||
if c := quikClientCodeFromPort(s, "БР-7-7 Погонщиков"); c != "BR777" {
|
||||
t.Errorf("ClientCodeMap: %q", c)
|
||||
}
|
||||
s.RealWrite, s.RealClientCode = true, "BR859"
|
||||
if c := quikClientCodeFromPort(s, "БР-7-7 Погонщиков"); c != "BR859" {
|
||||
t.Errorf("подмена имеет приоритет над ClientCodeMap: %q", c)
|
||||
}
|
||||
if c := quikClientCodeFromPort(s, "БР-859 Баранкин"); c != "BR859" {
|
||||
t.Errorf("подмена: %q", c)
|
||||
}
|
||||
// Флаг включён, но код не задан — обычная логика.
|
||||
s.RealClientCode = ""
|
||||
if c := quikClientCodeFromPort(s, "БР-7-7 Погонщиков"); c != "BR777" {
|
||||
t.Errorf("real без кода: %q", c)
|
||||
}
|
||||
}
|
||||
|
||||
// Цикл МНП активен в режиме реальной записи только в dev и с указанным кодом.
|
||||
func TestQuikActiveRealWrite(t *testing.T) {
|
||||
if quikActive(QuikMNPSettings{RealWrite: true, RealClientCode: "BR859"}) {
|
||||
t.Error("вне dev-режима цикл стоит")
|
||||
}
|
||||
devModeForced = true
|
||||
defer func() { devModeForced = false }()
|
||||
if !quikActive(QuikMNPSettings{RealWrite: true, RealClientCode: "BR859"}) {
|
||||
t.Error("dev + код: цикл должен работать")
|
||||
}
|
||||
if quikActive(QuikMNPSettings{RealWrite: true}) {
|
||||
t.Error("dev без кода: цикл стоит")
|
||||
}
|
||||
// Имитация имеет приоритет над real-записью.
|
||||
if !quikActive(QuikMNPSettings{Emulate: true, RealWrite: true, RealClientCode: "BR859"}) {
|
||||
t.Error("emulate + real: работает имитация (цикл активен)")
|
||||
}
|
||||
}
|
||||
|
||||
// Авто-сброс при окончании dev-режима гасит и реальную запись на подставного клиента.
|
||||
func TestDisableEmulatorsIfDevOffRealWrite(t *testing.T) {
|
||||
rc := rcWithExpiredDev(t)
|
||||
q := rc.Snapshot().Quik
|
||||
q.RealWrite, q.RealClientCode = true, "BR859"
|
||||
if err := rc.UpdateQuik(q); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
disableEmulatorsIfDevOff(rc)
|
||||
if s := rc.Snapshot().Quik; s.RealWrite {
|
||||
t.Fatal("RealWrite не сброшен")
|
||||
}
|
||||
found := false
|
||||
for _, n := range rc.Snapshot().News.Items {
|
||||
if strings.Contains(n.Title, "Эмуляторы отключены") {
|
||||
found = true
|
||||
if !strings.Contains(n.Body, "подставного клиента BR859") || !strings.Contains(n.Body, "прекращены") {
|
||||
t.Errorf("новость без упоминания реальных поручений: %s", n.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("новость не добавлена")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,22 +412,32 @@ func quikExecDeny(ctx context.Context, s QuikMNPSettings, queryNo float64, reaso
|
||||
var quikPortRe = regexp.MustCompile(`^\s*(\S+)`)
|
||||
|
||||
// quikClientCodeFromPort: «БР-859 Иванов…» → «BR859»; исключения — из настроек.
|
||||
// В режиме «реальная запись на подставного клиента» (RealWrite, страница «Эмуляторы»)
|
||||
// код подменяется в самом конце — поручения в МНП уходят на RealClientCode.
|
||||
func quikClientCodeFromPort(s QuikMNPSettings, portNum string) string {
|
||||
m := quikPortRe.FindStringSubmatch(portNum)
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
tok := m[1]
|
||||
code := ""
|
||||
if s.ClientCodeMap != nil {
|
||||
if v, ok := s.ClientCodeMap[tok]; ok && strings.TrimSpace(v) != "" {
|
||||
return strings.TrimSpace(v)
|
||||
if v, ok := s.ClientCodeMap[tok]; ok {
|
||||
code = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
up := strings.ToUpper(tok)
|
||||
if strings.HasPrefix(up, "БР-") {
|
||||
return "BR" + strings.TrimPrefix(up, "БР-")
|
||||
if code == "" {
|
||||
up := strings.ToUpper(tok)
|
||||
if strings.HasPrefix(up, "БР-") {
|
||||
code = "BR" + strings.TrimPrefix(up, "БР-")
|
||||
} else {
|
||||
code = tok
|
||||
}
|
||||
}
|
||||
return tok
|
||||
if s.RealWrite && strings.TrimSpace(s.RealClientCode) != "" {
|
||||
return strings.TrimSpace(s.RealClientCode)
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// quikResolveClient находит в DEPO клиента по паспорту и возвращает код QUIK и номер портфеля.
|
||||
@@ -463,12 +473,16 @@ func quikResolveClient(ctx context.Context, rc *RuntimeConfig, ser, num string)
|
||||
|
||||
// ---- высокоуровневые операции цикла ----
|
||||
|
||||
// quikActive — цикл M2M в МНП работает: записи разрешены или включена имитация. Имитация — только в
|
||||
// режиме разработки; если он закончился при включённой имитации, цикл стоит (в МНП не пишем).
|
||||
// quikActive — цикл M2M в МНП работает: записи разрешены, включена имитация или режим реальной
|
||||
// записи на подставного клиента. Имитация и real-запись — только в режиме разработки; если он
|
||||
// закончился при включённом режиме, цикл стоит (в МНП не пишем). Имитация имеет приоритет.
|
||||
func quikActive(s QuikMNPSettings) bool {
|
||||
if s.Emulate {
|
||||
return devMode(globalRC)
|
||||
}
|
||||
if s.RealWrite {
|
||||
return devMode(globalRC) && strings.TrimSpace(s.RealClientCode) != ""
|
||||
}
|
||||
return s.WritesEnabled
|
||||
}
|
||||
|
||||
@@ -512,6 +526,16 @@ func quikStep(action string, aid int64, r QuikActionResult, ok bool) QuikStep {
|
||||
return QuikStep{At: time.Now(), Action: action, ActionID: aid, Status: r.QueryStatus, Reply: r.ReplyText, OK: ok}
|
||||
}
|
||||
|
||||
// quikStepReal — шаг боевого пути с пометкой режима «реальная запись на подставного
|
||||
// клиента»: в UI «Поручения QUIK» видно, что поручение ушло не на своего клиента.
|
||||
func quikStepReal(s QuikMNPSettings, action string, aid int64, r QuikActionResult, ok bool) QuikStep {
|
||||
st := quikStep(action, aid, r, ok)
|
||||
if s.RealWrite && strings.TrimSpace(s.RealClientCode) != "" {
|
||||
st.Reply = strings.TrimSpace(st.Reply + " [на подставного клиента " + strings.TrimSpace(s.RealClientCode) + "]")
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
// Повтор регистрации, пока модуль МНП не подхватил нового получателя из M2MDepoRecipients.
|
||||
var (
|
||||
quikRecipientRetries = 3
|
||||
@@ -556,12 +580,12 @@ func QuikOutRegister(ctx context.Context, rc *RuntimeConfig, guid, clientID, con
|
||||
}
|
||||
if r.QueryStatus != 2 || r.InstrNo == "" || r.InstrNo == "0" {
|
||||
err := fmt.Errorf("МНП отверг регистрацию: %s", r.ReplyText)
|
||||
globalQuikLinks.update(guid, isin, func(l *QuikLink) { l.Steps = append(l.Steps, quikStep("register", aid, r, false)) })
|
||||
globalQuikLinks.update(guid, isin, func(l *QuikLink) { l.Steps = append(l.Steps, quikStepReal(s, "register", aid, r, false)) })
|
||||
return quikFail(rc, guid, isin, "register", err), err
|
||||
}
|
||||
l = globalQuikLinks.update(guid, isin, func(l *QuikLink) {
|
||||
l.InstrNo, l.State, l.Err = r.InstrNo, "registered", ""
|
||||
l.Steps = append(l.Steps, quikStep("register", aid, r, true))
|
||||
l.Steps = append(l.Steps, quikStepReal(s, "register", aid, r, true))
|
||||
})
|
||||
log.Printf("quik: %s вывод зарегистрирован, поручение %s (%s, %s × %v)", shortGUID(guid), r.InstrNo, clientID, isin, qty)
|
||||
go depoOnQuik(rc, guid, isin, "register", r.InstrNo)
|
||||
@@ -590,12 +614,12 @@ func QuikOutSign(ctx context.Context, rc *RuntimeConfig, guid, isin, comment str
|
||||
r := quikWaitAction(ctx, s, aid, quikActionWait)
|
||||
if r.QueryStatus != 2 {
|
||||
err := fmt.Errorf("МНП отверг подпись: %s", r.ReplyText)
|
||||
globalQuikLinks.update(guid, isin, func(l *QuikLink) { l.Steps = append(l.Steps, quikStep("sign", aid, r, false)) })
|
||||
globalQuikLinks.update(guid, isin, func(l *QuikLink) { l.Steps = append(l.Steps, quikStepReal(s, "sign", aid, r, false)) })
|
||||
return quikFail(rc, guid, isin, "sign", err), err
|
||||
}
|
||||
l = globalQuikLinks.update(guid, isin, func(l *QuikLink) {
|
||||
l.State, l.Err = "signed", ""
|
||||
l.Steps = append(l.Steps, quikStep("sign", aid, r, true))
|
||||
l.Steps = append(l.Steps, quikStepReal(s, "sign", aid, r, true))
|
||||
})
|
||||
log.Printf("quik: %s поручение %s подписано и исполнено", shortGUID(guid), l.InstrNo)
|
||||
go depoOnQuik(rc, guid, isin, "sign", l.InstrNo)
|
||||
@@ -624,12 +648,12 @@ func QuikOutDeny(ctx context.Context, rc *RuntimeConfig, guid, isin, reason stri
|
||||
r := quikWaitAction(ctx, s, aid, quikActionWait)
|
||||
if r.QueryStatus != 2 {
|
||||
err := fmt.Errorf("МНП отверг снятие: %s", r.ReplyText)
|
||||
globalQuikLinks.update(guid, isin, func(l *QuikLink) { l.Steps = append(l.Steps, quikStep("deny", aid, r, false)) })
|
||||
globalQuikLinks.update(guid, isin, func(l *QuikLink) { l.Steps = append(l.Steps, quikStepReal(s, "deny", aid, r, false)) })
|
||||
return quikFail(rc, guid, isin, "deny", err), err
|
||||
}
|
||||
l = globalQuikLinks.update(guid, isin, func(l *QuikLink) {
|
||||
l.State, l.Err = "denied", ""
|
||||
l.Steps = append(l.Steps, quikStep("deny", aid, r, true))
|
||||
l.Steps = append(l.Steps, quikStepReal(s, "deny", aid, r, true))
|
||||
})
|
||||
log.Printf("quik: %s поручение %s снято: %s", shortGUID(guid), l.InstrNo, reason)
|
||||
go depoOnQuik(rc, guid, isin, "deny", l.InstrNo)
|
||||
@@ -658,12 +682,12 @@ func QuikInDeposit(ctx context.Context, rc *RuntimeConfig, guid, clientID, contr
|
||||
r := quikWaitAction(ctx, s, aid, quikActionWait)
|
||||
if r.QueryStatus != 2 || r.InstrNo == "" || r.InstrNo == "0" {
|
||||
err := fmt.Errorf("МНП отверг зачисление: %s", r.ReplyText)
|
||||
globalQuikLinks.update(guid, isin, func(l *QuikLink) { l.Steps = append(l.Steps, quikStep("deposit", aid, r, false)) })
|
||||
globalQuikLinks.update(guid, isin, func(l *QuikLink) { l.Steps = append(l.Steps, quikStepReal(s, "deposit", aid, r, false)) })
|
||||
return quikFail(rc, guid, isin, "deposit", err), err
|
||||
}
|
||||
l := globalQuikLinks.update(guid, isin, func(l *QuikLink) {
|
||||
l.InstrNo, l.State, l.Err = r.InstrNo, "deposited", ""
|
||||
l.Steps = append(l.Steps, quikStep("deposit", aid, r, true))
|
||||
l.Steps = append(l.Steps, quikStepReal(s, "deposit", aid, r, true))
|
||||
})
|
||||
log.Printf("quik: %s ввод зарегистрирован и исполнен, поручение %s (%s, %s × %v)", shortGUID(guid), r.InstrNo, clientID, isin, qty)
|
||||
go depoOnQuik(rc, guid, isin, "deposit", r.InstrNo)
|
||||
|
||||
@@ -71,7 +71,8 @@ func (h *setupHandlers) quikFromForm(r *http.Request) QuikMNPSettings {
|
||||
Trust: r.FormValue("trust") != "",
|
||||
Verified: cur.Verified,
|
||||
// поля автоматического цикла M2M правятся отдельной формой — не затираем
|
||||
WritesEnabled: cur.WritesEnabled, Emulate: cur.Emulate, AutoOut: cur.AutoOut, AutoIn: cur.AutoIn, PollTerminal: cur.PollTerminal,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"log"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -69,15 +70,17 @@ type QuikMNPSettings struct {
|
||||
M2MMissing string `json:"m2m_missing,omitempty"` // ключевые объекты M2M, не видные под логином при последней проверке
|
||||
|
||||
// Автоматический цикл M2M в МНП (пишущие операции к боевой БД).
|
||||
WritesEnabled bool `json:"writes_enabled"` // главный предохранитель: без него ни одной записи в МНП
|
||||
Emulate bool `json:"emulate"` // имитация: шаги цикла проходят без записи в МНП (нет пользователя QUIK для блокировок)
|
||||
AutoOut bool `json:"auto_out"` // исходящие: регистрировать «Вывод ЦБ (M2M)» при отправке, подписывать/снимать по ответу
|
||||
AutoIn bool `json:"auto_in"` // входящие: регистрировать «Ввод ЦБ (M2M)» после приёма поручения НРД
|
||||
PollTerminal bool `json:"poll_terminal"` // подхватывать поручения, поданные клиентами из QUIK (Export)
|
||||
Account string `json:"account"` // торговый счёт для регистрации (напр. L01+00000F00)
|
||||
SecOut string `json:"sec_out"` // код МНП-инструмента вывода (пусто = определить по классу INSTROUTM2M)
|
||||
SecIn string `json:"sec_in"` // код МНП-инструмента ввода (пусто = по классу INSTRINM2M)
|
||||
ClientCodeMap map[string]string `json:"client_code_map"` // исключения: номер портфеля DEPO («БР-4-4») → код клиента QUIK
|
||||
WritesEnabled bool `json:"writes_enabled"` // главный предохранитель: без него ни одной записи в МНП
|
||||
Emulate bool `json:"emulate"` // имитация: шаги цикла проходят без записи в МНП (нет пользователя QUIK для блокировок)
|
||||
RealWrite bool `json:"real_write"` // режим разработки: РЕАЛЬНАЯ запись поручений в боевую МНП, но на подставного клиента
|
||||
RealClientCode string `json:"real_client_code,omitempty"` // код реального клиента QUIK, на которого уходят поручения в режиме real_write
|
||||
AutoOut bool `json:"auto_out"` // исходящие: регистрировать «Вывод ЦБ (M2M)» при отправке, подписывать/снимать по ответу
|
||||
AutoIn bool `json:"auto_in"` // входящие: регистрировать «Ввод ЦБ (M2M)» после приёма поручения НРД
|
||||
PollTerminal bool `json:"poll_terminal"` // подхватывать поручения, поданные клиентами из QUIK (Export)
|
||||
Account string `json:"account"` // торговый счёт для регистрации (напр. L01+00000F00)
|
||||
SecOut string `json:"sec_out"` // код МНП-инструмента вывода (пусто = определить по классу INSTROUTM2M)
|
||||
SecIn string `json:"sec_in"` // код МНП-инструмента ввода (пусто = по классу INSTRINM2M)
|
||||
ClientCodeMap map[string]string `json:"client_code_map"` // исключения: номер портфеля DEPO («БР-4-4») → код клиента QUIK
|
||||
}
|
||||
|
||||
// LicenseSettings — лицензионный ключ (подписанный токен).
|
||||
@@ -516,8 +519,14 @@ func (r *RuntimeConfig) UpdateFancy(s FancyMSSQLSettings) error {
|
||||
return r.save()
|
||||
}
|
||||
|
||||
// quikRealClientCodeRe — код подставного клиента QUIK (режим real_write): латиница/цифры/дефис/подчёркивание.
|
||||
var quikRealClientCodeRe = regexp.MustCompile(`^[A-Za-z0-9_-]{2,32}$`)
|
||||
|
||||
// UpdateQuik сохраняет настройки подключения к БД МНП QUIK.
|
||||
func (r *RuntimeConfig) UpdateQuik(s QuikMNPSettings) error {
|
||||
if c := strings.TrimSpace(s.RealClientCode); c != "" && !quikRealClientCodeRe.MatchString(c) {
|
||||
return errors.New("код подставного клиента QUIK — 2..32 символа: латиница, цифры, дефис, подчёркивание")
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.data.Quik = s
|
||||
r.data.UpdatedAt = time.Now().UTC()
|
||||
|
||||
@@ -52,39 +52,55 @@
|
||||
|
||||
{{/* ===== Эмулятор МНП QUIK ===== */}}
|
||||
<div class="card" id="emu-quik-card">
|
||||
<h2><span class="dot {{if .QuikEmuOn}}warn{{else}}ok{{end}}"></span>Эмулятор МНП QUIK
|
||||
{{if .QuikEmuOn}}<span class="hero-status warn">включён</span>{{end}}</h2>
|
||||
<p class="muted">Шаги цикла M2M в Модуле неторговых поручений (регистрация вывода и блокировка бумаг, подпись, снятие поручения,
|
||||
зачисление входящих) проходят и отмечаются «имитация», но <b>в боевую базу МНП ничего не записывается</b>.
|
||||
Имитация имеет приоритет над «записями в МНП».</p>
|
||||
<h2><span class="dot {{if ne .QuikMode "off"}}warn{{else}}ok{{end}}"></span>Эмулятор МНП QUIK
|
||||
{{if eq .QuikMode "emulate"}}<span class="hero-status warn">имитация</span>{{end}}
|
||||
{{if eq .QuikMode "real"}}<span class="hero-status warn">запись на подставного</span>{{end}}</h2>
|
||||
<p class="muted">Режим цикла M2M в Модуле неторговых поручений на время разработки.
|
||||
Имитация имеет приоритет над «записями в МНП»; оба dev-режима сбрасываются автоматически, когда режим разработки заканчивается.</p>
|
||||
{{if eq .QuikMode "real"}}
|
||||
<p class="dq-warn" style="font-weight:700">ВНИМАНИЕ: в боевую МНП будут записываться РЕАЛЬНЫЕ поручения на клиента {{.QuikRealCode}}.</p>
|
||||
{{end}}
|
||||
<form method="post" action="/admin/emulators/quik" id="emu-quik-form" class="dq">
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:600">
|
||||
<input type="checkbox" name="emulate" value="1" {{if .QuikEmuOn}}checked{{end}}>
|
||||
Эмулировать МНП (в QUIK ничего не пишется)</label>
|
||||
<div style="margin-top:10px;max-width:280px">
|
||||
<label>Задержка шага, сек <span class="muted">(0..60, повторяет ожидание ответа МНП)</span>
|
||||
<input type="number" name="step_delay_sec" min="0" max="60" value="{{.MNP.StepDelaySec}}" style="width:100%"></label>
|
||||
<div class="dq-tiles">
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="off" {{if eq .QuikMode "off"}}checked{{end}}><span><b>Выключено</b><small>Шаги цикла выполняются в QUIK по-настоящему (при разрешённых записях в МНП) — как в боевой работе.</small></span></label>
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="emulate" {{if eq .QuikMode "emulate"}}checked{{end}}><span><b>Внутренняя имитация</b><small>Шаги цикла (регистрация и блокировка, подпись, снятие, зачисление) проходят и отмечаются «имитация», в боевую базу МНП ничего не записывается.</small></span></label>
|
||||
<label class="dq-tile"><input type="radio" name="mode" value="real" {{if eq .QuikMode "real"}}checked{{end}}><span><b>Реальная запись на подставного клиента</b><small>Цикл идёт по обычному клиенту, но поручения в боевую МНП уходят РЕАЛЬНО — с кодом другого, реально существующего в QUIK клиента.</small></span></label>
|
||||
</div>
|
||||
<fieldset style="border:1px solid var(--border);border-radius:8px;margin-top:10px;padding:8px 12px">
|
||||
<legend class="muted" style="font-size:12px">Имитировать отказ МНП на шаге (состояние не продвигается, повтор — как при боевом отказе)</legend>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:6px">
|
||||
<label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="fail_register" value="1" {{if .MNP.FailRegister}}checked{{end}}> Регистрация (вывод)</label>
|
||||
<label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="fail_sign" value="1" {{if .MNP.FailSign}}checked{{end}}> Подпись (исполнение)</label>
|
||||
<label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="fail_deny" value="1" {{if .MNP.FailDeny}}checked{{end}}> Снятие поручения</label>
|
||||
<label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="fail_deposit" value="1" {{if .MNP.FailDeposit}}checked{{end}}> Зачисление (ввод)</label>
|
||||
<div id="emu-quik-code" style="margin-top:10px;max-width:280px">
|
||||
<label>Код клиента в QUIK <span class="muted">(код реального клиента QUIK, напр. BR859 — поручения уйдут на него)</span>
|
||||
<input type="text" name="real_client_code" value="{{.QuikRealCode}}" spellcheck="false" class="dq-mono" style="width:100%"></label>
|
||||
</div>
|
||||
<p class="dq-warn" id="emu-quik-warn" style="font-weight:700" hidden>ВНИМАНИЕ: в боевую МНП будут записываться РЕАЛЬНЫЕ поручения на указанного клиента.</p>
|
||||
<div id="emu-quik-emu">
|
||||
<div style="margin-top:10px;max-width:280px">
|
||||
<label>Задержка шага, сек <span class="muted">(0..60, повторяет ожидание ответа МНП)</span>
|
||||
<input type="number" name="step_delay_sec" min="0" max="60" value="{{.MNP.StepDelaySec}}" style="width:100%"></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="border:1px solid var(--border);border-radius:8px;margin-top:10px;padding:8px 12px">
|
||||
<legend class="muted" style="font-size:12px">Имитировать отказ МНП на шаге (состояние не продвигается, повтор — как при боевом отказе)</legend>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:6px">
|
||||
<label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="fail_register" value="1" {{if .MNP.FailRegister}}checked{{end}}> Регистрация (вывод)</label>
|
||||
<label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="fail_sign" value="1" {{if .MNP.FailSign}}checked{{end}}> Подпись (исполнение)</label>
|
||||
<label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="fail_deny" value="1" {{if .MNP.FailDeny}}checked{{end}}> Снятие поручения</label>
|
||||
<label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="fail_deposit" value="1" {{if .MNP.FailDeposit}}checked{{end}}> Зачисление (ввод)</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<p class="dq-note" id="emu-quik-st" aria-live="polite">Изменения сохраняются сразу.</p>
|
||||
<noscript><button type="submit" class="btn">Сохранить</button></noscript>
|
||||
</form>
|
||||
</div>
|
||||
<script>(function(){
|
||||
var f=document.getElementById('emu-quik-form'), st=document.getElementById('emu-quik-st');
|
||||
f.addEventListener('change', function(){
|
||||
var f=document.getElementById('emu-quik-form'), st=document.getElementById('emu-quik-st'),
|
||||
code=document.getElementById('emu-quik-code'), warn=document.getElementById('emu-quik-warn'),
|
||||
emu=document.getElementById('emu-quik-emu');
|
||||
function sync(){ var m=f.mode.value; code.hidden=(m!=='real'); warn.hidden=(m!=='real'); emu.hidden=(m==='real'); }
|
||||
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>
|
||||
{{end}}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<a href="/admin/emulators" class="tile">
|
||||
<span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M7 9h4M7 13h7"/></svg></span>
|
||||
<span class="t-title">Эмулятор МНП QUIK</span>
|
||||
<span class="t-sub">{{if .QuikEmuOn}}ВКЛЮЧЁН — в МНП ничего не пишется{{else}}выключен{{end}}</span>
|
||||
<span class="t-sub">{{if .QuikRealCode}}ЗАПИСЬ в МНП на {{.QuikRealCode}} (реальные поручения){{else if .QuikEmuOn}}ВКЛЮЧЁН — в МНП ничего не пишется{{else}}выключен{{end}}</span>
|
||||
<span class="t-arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -605,6 +605,12 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd
|
||||
{{if ne .Active "wizard"}}<a href="/admin/wizard" style="margin-left:auto">Открыть мастер настройки →</a>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .MNPReal}}
|
||||
<div class="banner-mock" style="border-color:var(--err);background:var(--err-weak,rgba(200,40,40,.08))">
|
||||
<span class="dot err" style="width:9px;height:9px;margin:0"></span>
|
||||
<div><strong style="color:var(--err)">Реальные поручения в МНП на клиента {{.MNPReal}}</strong><span class="muted"> (режим разработки) — в боевую базу МНП пишутся настоящие поручения с подменой кода клиента. Управляется на странице <a href="/admin/emulators">«Эмуляторы»</a>, сбрасывается автоматически по окончании режима разработки.</span></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<main>
|
||||
{{template "content" .}}
|
||||
|
||||
Reference in New Issue
Block a user