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:
zuevav
2026-10-08 00:45:06 +03:00
parent 8e4cb5de19
commit 6c42e0378d
10 changed files with 293 additions and 81 deletions
+40 -16
View File
@@ -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)