diff --git a/REPORT.md b/REPORT.md index 52c7c05..a3d37e7 100644 --- a/REPORT.md +++ b/REPORT.md @@ -27,7 +27,10 @@ `dev_until` или снятии на сервере лицензий) флаги эмуляции автоматически сбрасываются из настроек (проверка при старте и в цикле продления лицензии, новость «Эмуляторы отключены») — при следующей выдаче dev-режима эмуляторы -молча не оживут. +молча не оживут. У эмулятора МНП есть и третий режим — реальная запись поручений +в боевую МНП на подставного клиента (`QuikMNP.RealWrite` + `RealClientCode`: +цикл идёт по обычному клиенту, код в поручениях подменяется на указанный; +отдельный баннер-предупреждение, сбрасывается вместе с остальными эмуляторами). --- diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index 5da2cab..642bac9 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -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 } diff --git a/internal/lkgateway/emulators.go b/internal/lkgateway/emulators.go index 4210a91..fcf5584 100644 --- a/internal/lkgateway/emulators.go +++ b/internal/lkgateway/emulators.go @@ -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{ diff --git a/internal/lkgateway/emulators_test.go b/internal/lkgateway/emulators_test.go index 5e0119d..bc2bd1a 100644 --- a/internal/lkgateway/emulators_test.go +++ b/internal/lkgateway/emulators_test.go @@ -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("новость не добавлена") + } +} diff --git a/internal/lkgateway/quikcycle.go b/internal/lkgateway/quikcycle.go index cb596eb..aac07ea 100644 --- a/internal/lkgateway/quikcycle.go +++ b/internal/lkgateway/quikcycle.go @@ -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) diff --git a/internal/lkgateway/quikdb_handlers.go b/internal/lkgateway/quikdb_handlers.go index e5fec95..f0471f9 100644 --- a/internal/lkgateway/quikdb_handlers.go +++ b/internal/lkgateway/quikdb_handlers.go @@ -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, } } diff --git a/internal/lkgateway/runtimeconfig.go b/internal/lkgateway/runtimeconfig.go index 6e1b86a..be78518 100644 --- a/internal/lkgateway/runtimeconfig.go +++ b/internal/lkgateway/runtimeconfig.go @@ -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() diff --git a/internal/lkgateway/web/templates/admin_emulators.html b/internal/lkgateway/web/templates/admin_emulators.html index c8bdf7d..871a115 100644 --- a/internal/lkgateway/web/templates/admin_emulators.html +++ b/internal/lkgateway/web/templates/admin_emulators.html @@ -52,39 +52,55 @@ {{/* ===== Эмулятор МНП QUIK ===== */}}
-

Эмулятор МНП QUIK - {{if .QuikEmuOn}}включён{{end}}

-

Шаги цикла M2M в Модуле неторговых поручений (регистрация вывода и блокировка бумаг, подпись, снятие поручения, - зачисление входящих) проходят и отмечаются «имитация», но в боевую базу МНП ничего не записывается. - Имитация имеет приоритет над «записями в МНП».

+

Эмулятор МНП QUIK + {{if eq .QuikMode "emulate"}}имитация{{end}} + {{if eq .QuikMode "real"}}запись на подставного{{end}}

+

Режим цикла M2M в Модуле неторговых поручений на время разработки. + Имитация имеет приоритет над «записями в МНП»; оба dev-режима сбрасываются автоматически, когда режим разработки заканчивается.

+ {{if eq .QuikMode "real"}} +

ВНИМАНИЕ: в боевую МНП будут записываться РЕАЛЬНЫЕ поручения на клиента {{.QuikRealCode}}.

+ {{end}}
- -
- +
+ + +
-
- Имитировать отказ МНП на шаге (состояние не продвигается, повтор — как при боевом отказе) -
- - - - +
+ +
+ +
+
+
-
+
+ Имитировать отказ МНП на шаге (состояние не продвигается, повтор — как при боевом отказе) +
+ + + + +
+
+

Изменения сохраняются сразу.

{{end}} diff --git a/internal/lkgateway/web/templates/admin_home.html b/internal/lkgateway/web/templates/admin_home.html index 8c72ace..7fad5ee 100644 --- a/internal/lkgateway/web/templates/admin_home.html +++ b/internal/lkgateway/web/templates/admin_home.html @@ -68,7 +68,7 @@ Эмулятор МНП QUIK - {{if .QuikEmuOn}}ВКЛЮЧЁН — в МНП ничего не пишется{{else}}выключен{{end}} + {{if .QuikRealCode}}ЗАПИСЬ в МНП на {{.QuikRealCode}} (реальные поручения){{else if .QuikEmuOn}}ВКЛЮЧЁН — в МНП ничего не пишется{{else}}выключен{{end}} diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index a0b6dff..ceb97a4 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -605,6 +605,12 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd {{if ne .Active "wizard"}}Открыть мастер настройки →{{end}} {{end}} +{{if .MNPReal}} + +{{end}}
{{template "content" .}}