Режим разработки: служебные разделы, робот НРД и имитации — только по признаку из ключа лицензии
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,8 +53,16 @@ type License struct {
|
||||
// пока действует договор (ContractUntil). У офлайн-ключей оба поля пустые.
|
||||
InstallID string `json:"install_id,omitempty"`
|
||||
ContractUntil time.Time `json:"contract_until,omitempty"`
|
||||
|
||||
// DevUntil — режим разработки до указанного момента: сервер лицензий временно включает его
|
||||
// установке, чтобы разработчики bj подключились к серверу клиента (тестовые разделы и
|
||||
// инструменты). Указатель — чтобы у ключей без режима разработки подписанный JSON не менялся.
|
||||
DevUntil *time.Time `json:"dev_until,omitempty"`
|
||||
}
|
||||
|
||||
// DevMode — действует ли режим разработки на момент now.
|
||||
func (l *License) DevMode(now time.Time) bool { return l.DevUntil != nil && now.Before(*l.DevUntil) }
|
||||
|
||||
// Token — лицензия + подпись. Именно это вводит клиент (одна base64-строка
|
||||
// или JSON-файл). Формат: base64url(payload).base64url(sig) — компактно.
|
||||
type Token struct {
|
||||
|
||||
@@ -346,6 +346,7 @@ type page struct {
|
||||
IsMockMode bool // true если ИШ не настроен — bj-server в режиме эмуляции
|
||||
MockReason string // короткое описание почему mock
|
||||
NPR bool // модуль «НПР / УДС» включён и разрешён лицензией
|
||||
DevMode bool // режим разработки (сервер лицензий): видны служебные разделы
|
||||
}
|
||||
|
||||
// globalRC — ссылка на runtime-конфиг для template-funcs/page helpers.
|
||||
@@ -704,6 +705,7 @@ func nowPage(title, active string) page {
|
||||
p := page{Title: title, Active: active, Now: time.Now().Format("02.01.2006 15:04:05")}
|
||||
if globalRC != nil {
|
||||
p.NPR = nprAvailable(globalRC)
|
||||
p.DevMode = devMode(globalRC)
|
||||
s := globalRC.Snapshot()
|
||||
switch {
|
||||
case s.NSD.IGWBaseURL == "":
|
||||
|
||||
@@ -37,6 +37,9 @@ func advancedUnlocked(r *http.Request) bool {
|
||||
if globalRC != nil && moduleAllowed(globalRC, "quik-mnp") {
|
||||
return true
|
||||
}
|
||||
if !devMode(globalRC) { // пароль на модули — инструмент разработчиков
|
||||
return false
|
||||
}
|
||||
c, err := r.Cookie(advancedCookie)
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
@@ -180,6 +180,9 @@ type User struct {
|
||||
}
|
||||
|
||||
func (u *User) Can(perm string) bool {
|
||||
if perm == "dev" { // режим разработки: включает сервер лицензий, внутри — права настройки
|
||||
return u != nil && devMode(globalRC) && u.Can("settings")
|
||||
}
|
||||
if u == nil || !permLicensed(perm) {
|
||||
return false
|
||||
}
|
||||
@@ -809,16 +812,30 @@ func authExempt(p string) bool {
|
||||
return true
|
||||
case strings.HasPrefix(p, "/api/"):
|
||||
return true
|
||||
case p == "/admin/api/callback-url": // служебный вызов эмулятора ЛК
|
||||
return true
|
||||
case strings.HasPrefix(p, "/admin/npr/static/"):
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// devOnlyPath — служебные разделы разработчиков (тестирование, имитации, прогоны с роботом НРД):
|
||||
// доступны только в режиме разработки.
|
||||
func devOnlyPath(p string) bool {
|
||||
for _, pre := range []string{"/admin/m2m-testing", "/admin/incoming/depo-sim", "/admin/setup/test-nsd", "/admin/setup/test-run",
|
||||
"/admin/setup/lk-simulate", "/admin/setup/postgres/quick-start", "/admin/help/robot", "/admin/api/clients", "/admin/api/callback-url",
|
||||
"/admin/setup/advanced-unlock", "/admin/setup/advanced-lock"} {
|
||||
if p == pre || strings.HasPrefix(p, pre+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// requiredPerm — какое право нужно для пути и метода.
|
||||
func requiredPerm(p, method string) string {
|
||||
if devOnlyPath(p) {
|
||||
return "dev"
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(p, "/admin/setup") || p == "/admin/wizard":
|
||||
return "settings"
|
||||
@@ -853,6 +870,10 @@ func clientIP(r *http.Request) string {
|
||||
func authMiddleware(rc *RuntimeConfig, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.URL.Path
|
||||
if devOnlyPath(p) && !devMode(rc) { // вне режима разработки служебных разделов нет вовсе
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if authExempt(p) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
@@ -1272,6 +1293,9 @@ func renderFor(w http.ResponseWriter, t *template.Template, data any) error {
|
||||
t2.Funcs(template.FuncMap{
|
||||
"curUser": func() *User { return u },
|
||||
"can": func(perm string) bool {
|
||||
if perm == "dev" {
|
||||
return devMode(globalRC) && (authOpen() || u.Can("settings"))
|
||||
}
|
||||
if authOpen() {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Служебные разделы разработчиков вне режима разработки отвечают 404 — даже при открытом входе.
|
||||
func TestDevOnlyPaths(t *testing.T) {
|
||||
for _, p := range []string{"/admin/m2m-testing", "/admin/m2m-testing/preview", "/admin/setup/test-run", "/admin/api/callback-url", "/admin/help/robot"} {
|
||||
if !devOnlyPath(p) || requiredPerm(p, http.MethodGet) != "dev" {
|
||||
t.Errorf("%s должен быть служебным", p)
|
||||
}
|
||||
}
|
||||
for _, p := range []string{"/admin/m2m-testingx", "/admin/incoming", "/admin/setup", "/admin/help"} {
|
||||
if devOnlyPath(p) {
|
||||
t.Errorf("%s не служебный", p)
|
||||
}
|
||||
}
|
||||
rc, err := NewRuntimeConfig(t.TempDir() + "/setup.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) })
|
||||
h := authMiddleware(rc, next)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/admin/m2m-testing", nil))
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("вне режима разработки: %d", rec.Code)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ type LicenseStatus struct {
|
||||
DaysLeft int
|
||||
AllowsUpdates bool
|
||||
Features []string // признаки (модули), разрешённые ключом
|
||||
DevUntil time.Time // режим разработки до (выдаёт сервер лицензий; нулевое — выключен)
|
||||
Message string
|
||||
}
|
||||
|
||||
@@ -71,6 +72,22 @@ func modulesAllowed(rc *RuntimeConfig) map[string]bool {
|
||||
return m
|
||||
}
|
||||
|
||||
// devModeForced — режим разработки в тестах пакета.
|
||||
var devModeForced bool
|
||||
|
||||
// devMode — режим разработки: сервер лицензий временно открыл разработчикам bj тестовые разделы и
|
||||
// служебные инструменты на этой установке. Без него у клиента их нет (адреса отвечают 404).
|
||||
func devMode(rc *RuntimeConfig) bool {
|
||||
if devModeForced {
|
||||
return true
|
||||
}
|
||||
if rc == nil {
|
||||
return false
|
||||
}
|
||||
st := licenseStatus(rc)
|
||||
return st.Valid && time.Now().Before(st.DevUntil)
|
||||
}
|
||||
|
||||
// moduleAllowed — разрешён ли модуль (лицензионный признак). План enterprise — все модули.
|
||||
func moduleAllowed(rc *RuntimeConfig, feature string) bool {
|
||||
st := licenseStatus(rc)
|
||||
@@ -127,6 +144,9 @@ func licenseStatus(rc *RuntimeConfig) LicenseStatus {
|
||||
st.DaysLeft = lic.DaysLeft(now)
|
||||
st.AllowsUpdates = lic.AllowsUpdates()
|
||||
st.Features = lic.Features
|
||||
if lic.DevUntil != nil {
|
||||
st.DevUntil = *lic.DevUntil
|
||||
}
|
||||
st.Online = lic.InstallID != ""
|
||||
if lic.Product != "" && lic.Product != "bj-server" {
|
||||
st.Message = "ключ выпущен для другого продукта (" + lic.Product + ")"
|
||||
|
||||
@@ -171,17 +171,21 @@ func LicenseRenew(ctx context.Context, rc *RuntimeConfig) error {
|
||||
return licenseAccept(rc, r.Token, "продлена")
|
||||
}
|
||||
|
||||
// licenseNeedsRenew — онлайн-ключ, и сутки без успешного продления.
|
||||
// licenseNeedsRenew — онлайн-ключ и 15 минут без продления: так режим разработки, включённый или
|
||||
// выключенный на сервере лицензий, доходит до установки быстро. После неудачи — не чаще раза в час.
|
||||
func licenseNeedsRenew(rc *RuntimeConfig) bool {
|
||||
s := rc.Snapshot().License
|
||||
lic, err := verifyLicenseToken(s.Key)
|
||||
if err != nil || lic.InstallID == "" {
|
||||
return false
|
||||
}
|
||||
return time.Since(s.LastSync) >= 23*time.Hour
|
||||
if !s.LastOK && !s.LastAttempt.IsZero() && time.Since(s.LastAttempt) < time.Hour && time.Since(s.LastSync) < 23*time.Hour {
|
||||
return false
|
||||
}
|
||||
return time.Since(s.LastSync) >= 15*time.Minute
|
||||
}
|
||||
|
||||
// licenseLoop — раз в час предупреждает о скором окончании лицензии и проверяет, пора ли продлить
|
||||
// licenseLoop — раз в 5 минут предупреждает о скором окончании лицензии и проверяет, пора ли продлить
|
||||
// ключ (продление — раз в сутки, при сбое — повтор через час).
|
||||
func (s *Server) licenseLoop(ctx context.Context) {
|
||||
ensureInstallID(s.rc)
|
||||
@@ -192,7 +196,7 @@ func (s *Server) licenseLoop(ctx context.Context) {
|
||||
return
|
||||
case <-time.After(wait):
|
||||
}
|
||||
wait = time.Hour
|
||||
wait = 5 * time.Minute
|
||||
licenseExpiryWarn(s.rc, time.Now())
|
||||
if !licenseNeedsRenew(s.rc) {
|
||||
continue
|
||||
|
||||
@@ -506,7 +506,8 @@ func lkClaimToRequest(ctx context.Context, rc *RuntimeConfig, ep LKEndpoint, cl,
|
||||
// MC0012500000, а серия/номер ДУЛ переопределяются на сценарий (робот
|
||||
// читает сценарий из DocumentSeries). Прод-заявки этим маркером не
|
||||
// затрагиваются. Позволяет прогонять все сценарии через путь ЛК.
|
||||
if sc := lkDetectRobotScenario(str(data["contragent_name"])); sc != "" {
|
||||
// Только в режиме разработки: у клиентов маршрута к роботу нет.
|
||||
if sc := lkDetectRobotScenario(str(data["contragent_name"])); sc != "" && devMode(rc) {
|
||||
doc := Document{DocumentType: "21", Series: sc, Number: robotDocumentNumbers[sc]}
|
||||
req.Investor.Document = doc
|
||||
req.InvestorDocumentOverride = &doc
|
||||
|
||||
@@ -35,7 +35,7 @@ var lkEmuSender *mock.Sender
|
||||
// lkEmulateNSD — идут ли заявки ЛК через имитатор НРД и почему.
|
||||
func lkEmulateNSD(rc *RuntimeConfig) (bool, string) {
|
||||
s := rc.Snapshot()
|
||||
if s.LK.EmulateNSD {
|
||||
if s.LK.EmulateNSD && devMode(rc) {
|
||||
return true, "включена в настройках"
|
||||
}
|
||||
if s.LK.ActiveEnv == "prod" && strings.HasPrefix(strings.ToLower(s.NSD.Profile), "test") {
|
||||
|
||||
@@ -55,6 +55,11 @@ func TestLKEmulateNSD(t *testing.T) {
|
||||
}
|
||||
s.LK.EmulateNSD = true
|
||||
_ = rc.UpdateLK(s.LK)
|
||||
if on, _ := lkEmulateNSD(rc); on {
|
||||
t.Error("флаг в настройках без режима разработки не действует")
|
||||
}
|
||||
devModeForced = true
|
||||
defer func() { devModeForced = false }()
|
||||
if on, why := lkEmulateNSD(rc); !on || why != "включена в настройках" {
|
||||
t.Errorf("флаг в настройках: %v %q", on, why)
|
||||
}
|
||||
|
||||
@@ -204,7 +204,8 @@ func (s *Service) CreateLKTransfer(ctx context.Context, rc *RuntimeConfig, u LKU
|
||||
// ТЕСТОВЫЙ КЛИЕНТ: если серия ДУЛ совпадает с кодом сценария робота НРД
|
||||
// (1111/2001/2002/3333), заявка адресуется роботу-тестировщику, а серия/номер
|
||||
// документа задают сценарий. Так из ЛК можно прогнать полный цикл с НРД.
|
||||
if _, isRobot := robotScenarios[u.DocSeries]; isRobot {
|
||||
// Только в режиме разработки: у реальных паспортов такие серии тоже встречаются.
|
||||
if _, isRobot := robotScenarios[u.DocSeries]; isRobot && devMode(rc) {
|
||||
claim.ReceiverCodeOverride = NSDRobotCode
|
||||
claim.InvestorDocumentOverride = &doc
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ var m2mRejectCatalog = []m2mCodeInfo{
|
||||
{"M2M08", "Операция нарушит законодательство РФ",
|
||||
"Проведение перевода приведёт к нарушению закона: например, бумага предназначена только для квалифицированных инвесторов, а инвестор не квалифицирован."},
|
||||
{"M2M09", "Отказ принимающей стороны (доп.)",
|
||||
"Дополнительный код отказа зачисляющей стороны (используется в тест-сценариях)."},
|
||||
"Дополнительный код отказа зачисляющей стороны."},
|
||||
}
|
||||
|
||||
// m2mServiceCatalog — коды сервиса МОСТ (ошибки/информирование). Приходят в ответе
|
||||
@@ -107,9 +107,9 @@ var m2mCodeTexts = map[string]string{
|
||||
"M2M04": "Инвестор не найден",
|
||||
"M2M05": "Зачисляющий депозитарий не оказывает услуги по учёту прав на ценную бумагу",
|
||||
"M2M06": "Приостановление операций по счёту депо зачисления",
|
||||
"M2M07": "Отказ принимающей стороны (тест 07)",
|
||||
"M2M07": "Отказ принимающей стороны",
|
||||
"M2M08": "Проведение операции приведёт к нарушению законодательства РФ",
|
||||
"M2M09": "Отказ принимающей стороны (тест 09)",
|
||||
"M2M09": "Отказ принимающей стороны (дополнительный код)",
|
||||
"M2M10": "Получено невалидное сообщение",
|
||||
"M2M14": "Отправитель не зарегистрирован в справочнике участников M2M",
|
||||
"M2M15": "Получатель (зачисляющий депозитарий) не зарегистрирован в справочнике участников M2M", // предположительно (пара к M2M14); уточнить по Спецификации ЭД
|
||||
|
||||
@@ -466,8 +466,14 @@ func quikResolveClient(ctx context.Context, rc *RuntimeConfig, ser, num string)
|
||||
|
||||
// ---- высокоуровневые операции цикла ----
|
||||
|
||||
// quikActive — цикл M2M в МНП работает: записи разрешены или включена имитация.
|
||||
func quikActive(s QuikMNPSettings) bool { return s.WritesEnabled || s.Emulate }
|
||||
// quikActive — цикл M2M в МНП работает: записи разрешены или включена имитация. Имитация — только в
|
||||
// режиме разработки; если он закончился при включённой имитации, цикл стоит (в МНП не пишем).
|
||||
func quikActive(s QuikMNPSettings) bool {
|
||||
if s.Emulate {
|
||||
return devMode(globalRC)
|
||||
}
|
||||
return s.WritesEnabled
|
||||
}
|
||||
|
||||
// quikEmulated — шаг цикла в режиме имитации: связка переходит в состояние state, в МНП ничего не
|
||||
// пишется (нет пользователя QUIK для блокировок и лимитов); события в DEPO — как при реальной работе.
|
||||
|
||||
@@ -86,7 +86,9 @@ func (h *setupHandlers) saveQuikCycle(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
s := h.rc.Snapshot().Quik
|
||||
s.WritesEnabled = r.FormValue("writes_enabled") != ""
|
||||
if devMode(h.rc) { // флажок имитации есть только в режиме разработки
|
||||
s.Emulate = r.FormValue("emulate") != ""
|
||||
}
|
||||
s.AutoOut = r.FormValue("auto_out") != ""
|
||||
s.AutoIn = r.FormValue("auto_in") != ""
|
||||
s.PollTerminal = r.FormValue("poll_terminal") != ""
|
||||
|
||||
@@ -249,6 +249,15 @@ func (s *Server) Run(ctx context.Context) error {
|
||||
// Монитор здоровья + systemd watchdog: следит за фоновыми горутинами,
|
||||
// поднимает алерт при зависании и «пинает» systemd, пока всё живо.
|
||||
healthRegister("poll-lk", time.Minute)
|
||||
// Ключ продлеваем до запуска фоновых циклов: режим разработки (имитации и т.п.) должен быть
|
||||
// известен им с первого прохода.
|
||||
if licenseNeedsRenew(s.rc) {
|
||||
lctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
if err := LicenseRenew(lctx, s.rc); err != nil {
|
||||
log.Printf("lk-gateway: лицензия при запуске: %v", err)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
go s.superviseHealth(ctx)
|
||||
|
||||
// Данные модуля личного кабинета — схема lk в PostgreSQL bj.
|
||||
|
||||
@@ -522,7 +522,9 @@ func dealToView(d *m2mcore.Deal) ClaimView {
|
||||
out.LastCallback = &cb
|
||||
}
|
||||
// Самопроверка тестовой заявки роботу: серия документа = код сценария.
|
||||
if devMode(globalRC) { // сценарии робота — только в режиме разработки: такие серии бывают у реальных паспортов
|
||||
out.TestResult = buildTestResult(out.Investor.Document.Series, out.M2MDecision, out.M2MResponse)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
|
||||
d.Done.Crypto = s.Crypto.Provider != "" && s.Crypto.Provider != "stub" && s.Crypto.ModulePath != ""
|
||||
d.Done.Certs = len(d.Certs) > 0
|
||||
d.Done.NSD = s.NSD.IGWBaseURL != "" && s.NSD.Profile != ""
|
||||
d.Done.TestRun = s.LastTest != nil
|
||||
d.Done.TestRun = s.LastTest != nil || (!d.DevMode && d.Done.NSD) // вне режима разработки тестовой заявки нет
|
||||
|
||||
// Определяем текущий шаг.
|
||||
step := 1
|
||||
@@ -612,7 +612,7 @@ func (h *setupHandlers) saveLKIntegration(w http.ResponseWriter, r *http.Request
|
||||
if a := r.FormValue("active_env"); a == "test" || a == "prod" {
|
||||
s.ActiveEnv = a
|
||||
}
|
||||
if r.FormValue("emulate_nsd_form") != "" { // флажок есть только в форме активного окружения
|
||||
if r.FormValue("emulate_nsd_form") != "" && devMode(h.rc) { // флажок — в форме активного окружения, в режиме разработки
|
||||
s.EmulateNSD = r.FormValue("emulate_nsd") != ""
|
||||
}
|
||||
if err := h.rc.UpdateLK(s); err != nil {
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
<p class="muted">ИШ НРД (профили GUEST/TEST3/PROD), команда Fansy (ETL в staging), уведомления (e-mail, Yandex Messenger, Telegram), порядок согласования.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/admin/help/robot" style="text-decoration:none">
|
||||
{{if .DevMode}}<a href="/admin/help/robot" style="text-decoration:none">
|
||||
<div class="card" style="height:100%">
|
||||
<h2 style="color:var(--accent)">Тестирование с роботом MOEX МОСТ →</h2>
|
||||
<p class="muted">Робот НРД на TEST3 (код <code>MC0012500000</code>), 4 тестовых сценария (отказ / принять все / частично / встречный перевод), управление через DocumentSeries и DocumentNumber, тестовые наборы депозитариев и кодов ошибок.</p>
|
||||
</div>
|
||||
</a>
|
||||
</a>{{end}}
|
||||
<a href="/admin/help/architecture" style="text-decoration:none">
|
||||
<div class="card" style="height:100%">
|
||||
<h2 style="color:var(--accent)">Архитектура обмена с НРД →</h2>
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<p>Ключ установлен на флешку, справочник сертификатов сформирован, Валидата проверена.</p>
|
||||
{{if $s.Backup}}<p class="muted">Бэкап прежнего носителя: <code>{{$s.Backup}}</code></p>{{end}}
|
||||
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
|
||||
<form method="post" action="/admin/setup/test-nsd" style="margin:0">
|
||||
{{if .DevMode}}<form method="post" action="/admin/setup/test-nsd" style="margin:0">
|
||||
<input type="hidden" name="scenario" value="2001">
|
||||
<button type="submit" class="btn btn-ok">→ Отправить тестовый документ роботу</button>
|
||||
</form>
|
||||
</form>{{else}}<a href="/admin/status" class="btn btn-ok">→ Проверить подключения</a>{{end}}
|
||||
<form method="post" action="/admin/setup/keywizard/reset" style="margin:0">
|
||||
<button type="submit" class="btn btn-secondary">Установить ещё один ключ</button>
|
||||
</form>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<button data-sec="update"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V8M6 12l6-6 6 6"/><path d="M5 4h14"/></svg></span>Обновления{{if .Settings.Update.Available}}<span class="ind warn"></span>{{end}}</button>
|
||||
<button data-sec="license"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="12" r="4"/><path d="M12 12h9M18 12v3M15 12v2"/></svg></span>Лицензия{{if .License.Present}}<span class="ind {{if .License.Valid}}ok{{else}}err{{end}}"></span>{{end}}</button>
|
||||
<button data-sec="branding"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="10" r="1.5"/><path d="M21 16l-5-5L5 19"/></svg></span>Оформление{{if .Settings.Branding.LogoDataURI}}<span class="ind ok"></span>{{end}}</button>
|
||||
<button data-sec="advanced"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span>Перспективные{{if .AdvancedUnlocked}}<span class="ind ok"></span>{{end}}</button>
|
||||
{{if .DevMode}} <button data-sec="advanced"><span class="nico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg></span>Перспективные{{if .AdvancedUnlocked}}<span class="ind ok"></span>{{end}}</button>{{end}}
|
||||
</nav>
|
||||
|
||||
<div class="settings-body">
|
||||
@@ -59,7 +59,7 @@
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Settings.Postgres.DSN}}ok{{else}}err{{end}}"></span>PostgreSQL</h2>
|
||||
<p class="muted">Журнал сделок m2m-core и принимающая БД. Сейчас: {{if .Settings.Postgres.DSN}}<code>подключено</code>{{else}}<code>in-memory</code> (данные не сохраняются){{end}}.</p>
|
||||
{{if not .Settings.Postgres.DSN}}
|
||||
{{if and .DevMode (not .Settings.Postgres.DSN)}}
|
||||
<div style="background:var(--card-2);border:1px solid var(--accent);border-radius:10px;padding:16px;margin-top:12px">
|
||||
<h3 style="margin:0 0 8px">Подключить автоматически</h3>
|
||||
<p class="muted" style="margin:0 0 12px">Поднимем локальный PostgreSQL в контейнере, применим миграции и запишем DSN. Для дев-стенда. Для прода — укажите свой DSN ниже.</p>
|
||||
@@ -227,16 +227,16 @@
|
||||
</div>
|
||||
|
||||
<div class="card" id="sec-quik-cycle">
|
||||
<h2><span class="dot {{if .Settings.Quik.Emulate}}warn{{else if not .Settings.Quik.WritesEnabled}}warn{{else if or .Settings.Quik.AutoOut .Settings.Quik.AutoIn}}ok{{else}}warn{{end}}"></span>Автоматический цикл M2M в МНП{{if .Settings.Quik.Emulate}} <span class="hero-status warn">имитация</span>{{end}}</h2>
|
||||
<h2><span class="dot {{if .Settings.Quik.Emulate}}warn{{else if not .Settings.Quik.WritesEnabled}}warn{{else if or .Settings.Quik.AutoOut .Settings.Quik.AutoIn}}ok{{else}}warn{{end}}"></span>Автоматический цикл M2M в МНП{{if .Settings.Quik.Emulate}} <span class="hero-status warn">{{if .DevMode}}имитация{{else}}остановлен{{end}}</span>{{end}}</h2>
|
||||
<p class="muted">Что делает bj в Модуле неторговых поручений по ходу перевода: <strong>исходящий</strong> — при отправке в НРД регистрирует «Вывод ЦБ (M2M)» (QUIK блокирует бумаги), по подтверждению подписывает (списание), по отказу снимает с причиной; <strong>входящий</strong> — после приёма нашего поручения НРД регистрирует «Ввод ЦБ (M2M)» (зачисление сразу). Проверено на боевом МНП 09.09.2026. Каждый шаг виден в карточке перевода и на странице <a href="/admin/quik">QUIK МНП</a>.</p>
|
||||
<form method="post" action="/admin/setup/quik/cycle" style="margin-top:12px;display:grid;gap:10px;max-width:640px">
|
||||
<input type="hidden" name="section" value="sec-quik-cycle">
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:600">
|
||||
<input type="checkbox" name="writes_enabled" value="1" {{if .Settings.Quik.WritesEnabled}}checked{{end}}>
|
||||
Разрешить записи в МНП (главный предохранитель — база боевая)</label>
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:600">
|
||||
{{if .DevMode}} <label style="display:flex;gap:8px;align-items:center;font-weight:600">
|
||||
<input type="checkbox" name="emulate" value="1" {{if .Settings.Quik.Emulate}}checked{{end}}>
|
||||
Имитация: шаги цикла (блокировка, подпись, снятие, зачисление) проходят и отмечаются «имитация», в МНП ничего не пишется — пока нет пользователя QUIK для блокировок и лимитов. Имеет приоритет над записью.</label>
|
||||
Имитация: шаги цикла (блокировка, подпись, снятие, зачисление) проходят и отмечаются «имитация», в МНП ничего не пишется — пока нет пользователя QUIK для блокировок и лимитов. Имеет приоритет над записью.</label>{{end}}
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:400;margin-left:22px">
|
||||
<input type="checkbox" name="auto_out" value="1" {{if .Settings.Quik.AutoOut}}checked{{end}}>
|
||||
Исходящие переводы: регистрировать при отправке, подписывать / снимать по ответу автоматически</label>
|
||||
@@ -616,12 +616,12 @@
|
||||
<option value="prod" {{if eq .Settings.LK.ActiveEnv "prod"}}selected{{end}}>Бой</option>
|
||||
</select>
|
||||
<span class="muted" style="font-size:12px">bj работает с ЛК через выбранное окружение.</span>
|
||||
<input type="hidden" name="emulate_nsd_form" value="1">
|
||||
<label style="display:flex;align-items:flex-start;gap:8px;font-weight:400;flex-basis:100%;margin-top:6px"><input type="checkbox" name="emulate_nsd" style="width:auto;margin-top:3px" {{if .Settings.LK.EmulateNSD}}checked{{end}} onchange="this.form.submit()"> <span><b>Имитация НРД</b> — заявки ЛК проходят сверку с DEPO, блокировку в QUIK и получают статус в ЛК, но в НРД не отправляются: ответ контрагента имитирует bj.{{if and (eq .Settings.LK.ActiveEnv "prod") (not .Settings.LK.EmulateNSD)}} <span style="color:var(--warn)">Сейчас включена принудительно: боевой ЛК при тестовом профиле ИШ ({{.Settings.NSD.Profile}}).</span>{{end}}</span></label>
|
||||
{{if .DevMode}} <input type="hidden" name="emulate_nsd_form" value="1">
|
||||
<label style="display:flex;align-items:flex-start;gap:8px;font-weight:400;flex-basis:100%;margin-top:6px"><input type="checkbox" name="emulate_nsd" style="width:auto;margin-top:3px" {{if .Settings.LK.EmulateNSD}}checked{{end}} onchange="this.form.submit()"> <span><b>Имитация НРД</b> — заявки ЛК проходят сверку с DEPO, блокировку в QUIK и получают статус в ЛК, но в НРД не отправляются: ответ контрагента имитирует bj.{{if and (eq .Settings.LK.ActiveEnv "prod") (not .Settings.LK.EmulateNSD)}} <span style="color:var(--warn)">Сейчас включена принудительно: боевой ЛК при тестовом профиле ИШ ({{.Settings.NSD.Profile}}).</span>{{end}}</span></label>{{end}}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{{if .DevMode}} <div class="card">
|
||||
<h2>Симулятор заявки M2M</h2>
|
||||
<p class="muted">Пока подача через веб-форму у ЕСИА Финанс не готова — bj сам собирает заявку <code>depo_m2m</code> и <strong>отправляет её в API ЛК</strong> (<code>POST /claims/</code>) через активное окружение. Ответ показывается ниже; затем заявку ловим опросом в «Переводы». Поля предзаполнены рабочим тестовым договором.</p>
|
||||
<form method="post" action="/admin/setup/lk-simulate" style="margin-top:12px">
|
||||
@@ -657,7 +657,7 @@
|
||||
<div style="margin-top:16px"><button type="submit" class="btn">Собрать и отправить в ЛК</button></div>
|
||||
</form>
|
||||
<p class="muted" style="font-size:12px;margin-top:10px">Результат (сырой ответ API) появится вверху страницы. Если «Раздел счёта не доступен для быстрых переводов» — Финансы должны включить признак M2M на разделе счёта.</p>
|
||||
</div>
|
||||
</div>{{end}}
|
||||
|
||||
<div class="card">
|
||||
<h2>Забрать заявки из ЛК</h2>
|
||||
@@ -702,7 +702,7 @@
|
||||
<p class="muted" style="font-size:12px;margin-top:8px">Ответ придёт во входящие ИШ (~30-60с). Список появится в логе bj-server и в <code>m2m-handbook.json</code>.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{{if .DevMode}} <div class="card">
|
||||
<h2>Тестовый пакет роботу НРД</h2>
|
||||
<p class="muted">Робот <code>MC0012500000</code> эмулирует вторую сторону перевода. Выберите сценарий — bj-server отправит эталонный запрос через ИШ, ответ придёт во входящие. Требуется настроенный ИШ + профиль Валидаты.</p>
|
||||
<form method="post" action="/admin/setup/test-nsd" style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||||
@@ -734,7 +734,7 @@
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>{{end}}
|
||||
</section>
|
||||
|
||||
{{/* ============ ОПОВЕЩЕНИЯ ============ */}}
|
||||
@@ -961,7 +961,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{/* ============ ПЕРСПЕКТИВНЫЕ МОДУЛИ (под паролем) ============ */}}
|
||||
{{if .DevMode}} {{/* ============ ПЕРСПЕКТИВНЫЕ МОДУЛИ (под паролем) ============ */}}
|
||||
<section class="settings-section" id="sec-advanced">
|
||||
<h1>Перспективные модули</h1>
|
||||
<p class="muted" style="max-width:64ch">Модули для будущих профилей внедрения <strong>«автомат»</strong> и <strong>«полуавтомат»</strong> (когда используется собственный личный кабинет, а не ЕСИА Финанс). Пока не в проде — скрыты, чтобы не мешать текущей работе. В профиле «через Финансы» они не нужны: проверку и блокировку бумаг закрывает сам ЕСИА Финанс.</p>
|
||||
@@ -992,7 +992,7 @@
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
</section>{{end}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<strong>Что выбрать?</strong> Если у вас уже есть рабочий PostgreSQL — нажмите «У меня уже есть PostgreSQL» и введите DSN. Если впервые настраиваете — выберите «Поднять автоматически», система сама развернёт контейнер с PostgreSQL и накатит миграции.
|
||||
</div>
|
||||
|
||||
{{if not .Settings.Postgres.DSN}}
|
||||
{{if and .DevMode (not .Settings.Postgres.DSN)}}
|
||||
<div style="background:var(--bg);border:1px solid var(--accent);border-radius:6px;padding:14px;margin-top:12px">
|
||||
<h3 style="margin:0 0 8px 0;font-size:15px">Вариант А — для тех, у кого нет своего PostgreSQL</h3>
|
||||
<p class="muted" style="margin:0 0 10px 0">Bridge-and-Join-s сам поднимет PostgreSQL в контейнере (podman-compose), создаст БД <code>bj</code> и накатит миграции. Подходит для дев-стенда. Для продакшена лучше указать свой DSN.</p>
|
||||
|
||||
@@ -287,7 +287,7 @@ label { font-size:13px; font-weight:500; }
|
||||
<a href="/admin/incoming">Входящие<span id="nav-incoming-badge" style="display:none;margin-left:5px;background:var(--err);color:#fff;border-radius:10px;padding:1px 7px;font-size:11px;font-weight:700"></span></a>
|
||||
</span>
|
||||
</span>
|
||||
{{end}}{{if can "operate"}}<a href="/admin/m2m-testing" class="{{if eq .Active "m2mtest"}}active{{end}}">Тестирование M2M</a>{{end}}
|
||||
{{end}}{{if and .DevMode (can "operate")}}<a href="/admin/m2m-testing" class="{{if eq .Active "m2mtest"}}active{{end}}">Тестирование M2M</a>{{end}}
|
||||
{{if and .NPR (can "npr")}}<a href="/admin/npr" class="{{if eq .Active "npr"}}active{{end}}">НПР / УДС</a>{{end}}
|
||||
{{if or (can "view") (can "quik")}}<span class="dd{{if or (eq .Active "fancy") (eq .Active "quik") (eq .Active "news") (eq .Active "handbook")}} active{{end}}">
|
||||
<span class="ddbtn">Обмен ▾</span>
|
||||
|
||||
Reference in New Issue
Block a user