feat(admin): авто-сброс эмуляторов НРД/МНП при окончании режима разработки
Если оператор временно получил dev-режим, включил эмулятор и забыл — при окончании dev-режима флаги LK.EmulateNSD и QuikMNP.Emulate сбрасываются насовсем (а не просто перестают действовать): при следующей выдаче dev-режима эмуляторы молча не оживут. Проверка disableEmulatorsIfDevOff вызывается при старте сервера (после продления лицензии) и на каждом проходе цикла продления — ловит истечение dev и по времени, без продления. При сбросе — запись в ленту событий: «Эмуляторы отключены: режим разработки закончился» с перечнем выключенного; если был включён эмулятор МНП при WritesEnabled=false — предупреждение, что цикл МНП остановлен и для боевой работы нужно включить запись.
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Обновление 07.10.2026: dev-страница «Эмуляторы»
|
## Обновление 07.10.2026 (доп. 08.10.2026): dev-страница «Эмуляторы»
|
||||||
|
|
||||||
В режиме разработки (лицензия с `dev_until`) появилась страница `/admin/emulators`
|
В режиме разработки (лицензия с `dev_until`) появилась страница `/admin/emulators`
|
||||||
(пункт меню «Эмуляторы», плитки на дашборде) — переключатели и настройки эмуляции:
|
(пункт меню «Эмуляторы», плитки на дашборде) — переключатели и настройки эмуляции:
|
||||||
@@ -23,7 +23,11 @@
|
|||||||
повтор на следующем проходе.
|
повтор на следующем проходе.
|
||||||
|
|
||||||
Баннер «Режим эмуляции» теперь честен и при настроенном ИШ: при ручном включении
|
Баннер «Режим эмуляции» теперь честен и при настроенном ИШ: при ручном включении
|
||||||
эмуляторов причина указывается явно.
|
эмуляторов причина указывается явно. При окончании режима разработки (по времени
|
||||||
|
`dev_until` или снятии на сервере лицензий) флаги эмуляции автоматически
|
||||||
|
сбрасываются из настроек (проверка при старте и в цикле продления лицензии,
|
||||||
|
новость «Эмуляторы отключены») — при следующей выдаче dev-режима эмуляторы
|
||||||
|
молча не оживут.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ package lkgateway
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// emuRejectOption — вариант кода отказа для эмулятора НРД (value — «07», подпись — с названием).
|
// emuRejectOption — вариант кода отказа для эмулятора НРД (value — «07», подпись — с названием).
|
||||||
@@ -159,3 +161,51 @@ func emuOutcomeRu(outcome string) string {
|
|||||||
}
|
}
|
||||||
return "подтверждение"
|
return "подтверждение"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// disableEmulatorsIfDevOff — безопасность: эмуляторы НРД/МНП действуют только в режиме
|
||||||
|
// разработки; когда он заканчивается (по времени dev_until или снят на сервере лицензий),
|
||||||
|
// флаги сбрасываются из настроек — иначе при следующей выдаче dev-режима эмуляция
|
||||||
|
// молча оживёт. Вызывается при старте и в цикле продления лицензии.
|
||||||
|
func disableEmulatorsIfDevOff(rc *RuntimeConfig) {
|
||||||
|
if rc == nil || devMode(rc) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s := rc.Snapshot()
|
||||||
|
if !s.LK.EmulateNSD && !s.Quik.Emulate {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var off []string
|
||||||
|
if s.LK.EmulateNSD {
|
||||||
|
lk := s.LK
|
||||||
|
lk.EmulateNSD = false
|
||||||
|
if err := rc.UpdateLK(lk); err != nil {
|
||||||
|
log.Printf("lk-gateway: авто-сброс эмулятора НРД: %v", err)
|
||||||
|
} else {
|
||||||
|
off = append(off, "эмулятор НРД (заявки ЛК)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.Quik.Emulate {
|
||||||
|
q := s.Quik
|
||||||
|
q.Emulate = false
|
||||||
|
if err := rc.UpdateQuik(q); err != nil {
|
||||||
|
log.Printf("lk-gateway: авто-сброс эмулятора МНП: %v", err)
|
||||||
|
} else {
|
||||||
|
off = append(off, "эмулятор МНП QUIK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(off) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("lk-gateway: режим разработки закончился — эмуляторы отключены: %s", strings.Join(off, ", "))
|
||||||
|
body := "Выключены: " + strings.Join(off, ", ") + ". Флаги эмуляции сброшены из настроек: при следующем включении режима разработки эмуляторы не включатся сами — их нужно будет включить заново на странице «Эмуляторы»."
|
||||||
|
if s.Quik.Emulate && !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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/license"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Настройки эмуляторов: валидация и обратная совместимость (старый setup.json без полей = дефолты).
|
// Настройки эмуляторов: валидация и обратная совместимость (старый setup.json без полей = дефолты).
|
||||||
@@ -206,3 +208,96 @@ func TestMNPEmuStepDelayCtx(t *testing.T) {
|
|||||||
t.Fatal("задержка не прервалась по контексту")
|
t.Fatal("задержка не прервалась по контексту")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rcWithExpiredDev — действующая лицензия с истёкшим dev_until: devMode уже false.
|
||||||
|
func rcWithExpiredDev(t *testing.T) *RuntimeConfig {
|
||||||
|
t.Helper()
|
||||||
|
priv := testLicenseKey(t)
|
||||||
|
rc := testRC(t)
|
||||||
|
past := time.Now().UTC().Add(-time.Hour)
|
||||||
|
tok := signTestLicense(t, priv, &license.License{Schema: 1, ID: "lic-dev", Tenant: "Тест", Product: "bj-server", Plan: license.PlanPro,
|
||||||
|
IssuedAt: past.Add(-24 * time.Hour), ExpiresAt: time.Now().UTC().Add(60 * 24 * time.Hour), DevUntil: &past})
|
||||||
|
if err := rc.SaveLicense(LicenseSettings{Key: tok}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if devMode(rc) {
|
||||||
|
t.Fatal("devMode с истёкшим dev_until должен быть выключен")
|
||||||
|
}
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// dev-режим закончился: включённые эмуляторы сбрасываются из настроек, про это — новость.
|
||||||
|
func TestDisableEmulatorsIfDevOff(t *testing.T) {
|
||||||
|
rc := rcWithExpiredDev(t)
|
||||||
|
s := rc.Snapshot()
|
||||||
|
s.LK.EmulateNSD = true
|
||||||
|
if err := rc.UpdateLK(s.LK); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s.Quik.Emulate = true
|
||||||
|
if err := rc.UpdateQuik(s.Quik); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
disableEmulatorsIfDevOff(rc)
|
||||||
|
if s := rc.Snapshot(); s.LK.EmulateNSD || s.Quik.Emulate {
|
||||||
|
t.Fatalf("флаги не сброшены: EmulateNSD=%v Quik.Emulate=%v", s.LK.EmulateNSD, s.Quik.Emulate)
|
||||||
|
}
|
||||||
|
// setup.json на диске тоже без флагов — при следующем dev-режиме эмуляторы не оживут.
|
||||||
|
rc2, err := NewRuntimeConfig(rc.path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if s := rc2.Snapshot(); s.LK.EmulateNSD || s.Quik.Emulate {
|
||||||
|
t.Fatal("в setup.json флаги остались включёнными")
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, n := range rc.Snapshot().News.Items {
|
||||||
|
if strings.Contains(n.Title, "Эмуляторы отключены") {
|
||||||
|
found = true
|
||||||
|
if !strings.Contains(n.Body, "эмулятор НРД") || !strings.Contains(n.Body, "эмулятор МНП") {
|
||||||
|
t.Errorf("в новости не перечислено, что выключено: %s", n.Body)
|
||||||
|
}
|
||||||
|
// WritesEnabled=false — новость предупреждает об остановленном цикле МНП.
|
||||||
|
if !strings.Contains(n.Body, "Цикл M2M в МНП остановлен") {
|
||||||
|
t.Errorf("нет предупреждения об остановленном цикле МНП: %s", n.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("новость об отключении эмуляторов не добавлена")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dev-режим действует: авто-сброс ничего не трогает.
|
||||||
|
func TestDisableEmulatorsIfDevOffDevOn(t *testing.T) {
|
||||||
|
devModeForced = true
|
||||||
|
defer func() { devModeForced = false }()
|
||||||
|
rc := testRC(t)
|
||||||
|
s := rc.Snapshot()
|
||||||
|
s.LK.EmulateNSD = true
|
||||||
|
s.Quik.Emulate = true
|
||||||
|
if err := rc.UpdateLK(s.LK); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := rc.UpdateQuik(s.Quik); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
disableEmulatorsIfDevOff(rc)
|
||||||
|
if s := rc.Snapshot(); !s.LK.EmulateNSD || !s.Quik.Emulate {
|
||||||
|
t.Fatal("при действующем dev-режиме флаги не должны сбрасываться")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dev выключен, но флаги уже сняты: no-op — настройки не переписываются, новости нет.
|
||||||
|
func TestDisableEmulatorsIfDevOffNoOp(t *testing.T) {
|
||||||
|
rc := rcWithExpiredDev(t)
|
||||||
|
before := rc.Snapshot().UpdatedAt
|
||||||
|
disableEmulatorsIfDevOff(rc)
|
||||||
|
after := rc.Snapshot()
|
||||||
|
if !after.UpdatedAt.Equal(before) {
|
||||||
|
t.Error("no-op не должен переписывать setup.json")
|
||||||
|
}
|
||||||
|
if len(after.News.Items) != 0 {
|
||||||
|
t.Error("no-op не должен добавлять новость")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ func (s *Server) licenseLoop(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
wait = 5 * time.Minute
|
wait = 5 * time.Minute
|
||||||
licenseExpiryWarn(s.rc, time.Now())
|
licenseExpiryWarn(s.rc, time.Now())
|
||||||
|
disableEmulatorsIfDevOff(s.rc) // dev-режим может истечь и просто по времени — эмуляторы гасим
|
||||||
if !licenseNeedsRenew(s.rc) {
|
if !licenseNeedsRenew(s.rc) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ func (s *Server) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
disableEmulatorsIfDevOff(s.rc) // dev-режим закончился, пока сервер стоял — эмуляторы гасим
|
||||||
go s.superviseHealth(ctx)
|
go s.superviseHealth(ctx)
|
||||||
|
|
||||||
// Данные модуля личного кабинета — схема lk в PostgreSQL bj.
|
// Данные модуля личного кабинета — схема lk в PostgreSQL bj.
|
||||||
|
|||||||
Reference in New Issue
Block a user