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 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), 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 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 // Режим взаимоисключающий: 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, }) }