feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР

- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk)
- трансферные таблицы FTV_M2M: создание при установке, события, справочник участников
- сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO
- боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК
- онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML)
- модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-15 19:41:12 +03:00
parent 59fc774f08
commit da12a9ec1c
132 changed files with 29969 additions and 1089 deletions
+77 -8
View File
@@ -1,6 +1,7 @@
package lkgateway
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -29,7 +30,6 @@ func urlQ(s string) string { return url.QueryEscape(s) }
// VDCrySvc) делает помощник /usr/local/sbin/bj-keymedia-install через узкий
// sudoers (bj-server работает под непривилегированным bj).
// keyWizardStep — один шаг мастера.
type keyWizardStep struct {
Title string
@@ -232,6 +232,9 @@ func (h *setupHandlers) keyWizardUpload(w http.ResponseWriter, r *http.Request)
}
password := r.FormValue("password")
// Не копим staging: чистим прошлые распаковки/архивы перед новой загрузкой.
sweepStaging()
isoDir := "/var/lib/bj/iso"
if err := os.MkdirAll(isoDir, 0o755); err != nil {
h.keyWizFlash(w, r, "Не удалось создать "+isoDir+": "+err.Error())
@@ -332,8 +335,11 @@ func (h *setupHandlers) keyWizardInstall(w http.ResponseWriter, r *http.Request)
tgt, _ := res["target"].(string)
spr, _ := res["spr"].(string)
h.keyWiz.set(1, "ok", fmt.Sprintf("Профиль «%s» записан на %s. Бэкап: %s", h.keyWiz.Profile, fallback(tgt, "носитель"), h.keyWiz.Backup))
crl, _ := res["crl"].(string)
h.keyWiz.set(2, "ok", fmt.Sprintf("Справочник «%s» сформирован. CRL: %s", fallback(spr, "—"), crlRu(crl)))
rebuild, _ := res["rebuild"].(string)
igate, _ := res["igate"].(string)
igp, _ := res["igate_profile_dir"].(string)
h.keyWiz.set(2, "ok", fmt.Sprintf("Справочник «%s» пересобран (%s); профиль ИШ %s %s",
fallback(spr, "—"), rebuildRu(rebuild), igateRu(igate), igp))
h.keyWiz.set(3, "active", "")
h.keyWiz.mu.Unlock()
@@ -354,6 +360,9 @@ func (h *setupHandlers) keyWizardInstall(w http.ResponseWriter, r *http.Request)
h.keyWiz.Done = true
h.keyWiz.mu.Unlock()
// Ключ уже на флешке и в ИШ — распаковка staging больше не нужна.
_ = os.RemoveAll(staging)
http.Redirect(w, r, "/admin/setup/keywizard?flash="+urlQ("Готово! Ключ на флешке, справочник сформирован, ИШ перезапущен и работает. Финальная проверка — тестовым документом роботу."), http.StatusSeeOther)
}
@@ -370,7 +379,7 @@ func (h *setupHandlers) restartAndVerifyISH(ctx context.Context) (bool, string)
deadline := time.Now().Add(40 * time.Second)
for time.Now().Before(deadline) {
if st := h.ishEngineState(ctx); st != "" {
return true, "engine "+st
return true, "engine " + st
}
select {
case <-ctx.Done():
@@ -418,6 +427,36 @@ func crlRu(s string) string {
}
}
// rebuildRu — статус пересборки справочника сертификатов на флешке.
func rebuildRu(s string) string {
switch {
case s == "ok":
return "совместимый формат, сертификаты извлечены из архива"
case s == "skip":
return "пропущено"
case strings.HasPrefix(s, "fail"):
return "ошибка пересборки (" + strings.TrimPrefix(s, "fail: ") + ")"
default:
return s
}
}
// igateRu — статус формирования профиля для ИШ.
func igateRu(s string) string {
switch s {
case "connected":
return "сформирован, зарегистрирован и подключён к ИШ (cryptography.profile):"
case "ok":
return "сформирован и зарегистрирован (но не выбран в ИШ):"
case "skip":
return "не формировался"
case "ini-fail":
return "сформирован, но регистрация в zpki.ini не удалась:"
default:
return s
}
}
// reactivateCryptoProfile повторно активирует текущий серверный профиль
// bj-crypto (после перезапуска VDCrySvc активация в сайдкаре сбрасывается).
// Best-effort: возвращает true при успехе.
@@ -448,6 +487,24 @@ func boolRu(b bool, yes, no string) string {
return no
}
// sweepStaging удаляет прошлые распаковки (/var/lib/bj/media/iso/*) и архивы
// (/var/lib/bj/iso/*.7z|zip), чтобы staging не копился. Вызывается перед новой
// загрузкой — текущая распаковка создаётся уже после очистки и не страдает.
func sweepStaging() {
if entries, err := os.ReadDir("/var/lib/bj/media/iso"); err == nil {
for _, e := range entries {
_ = os.RemoveAll(filepath.Join("/var/lib/bj/media/iso", e.Name()))
}
}
for _, pat := range []string{"/var/lib/bj/iso/*.7z", "/var/lib/bj/iso/*.zip"} {
if files, err := filepath.Glob(pat); err == nil {
for _, f := range files {
_ = os.Remove(f)
}
}
}
}
const keymediaReqDir = "/var/lib/bj/keymedia-requests"
// runKeymediaHelper передаёт запрос привилегированному воркеру через файловый
@@ -456,15 +513,23 @@ const keymediaReqDir = "/var/lib/bj/keymedia-requests"
// и пишет <id>.res. bj-server опрашивает результат. Так привилегированная
// работа идёт вне mount-namespace песочницы, где доступно перемонтирование USB.
func runKeymediaHelper(ctx context.Context, action, staging, profile, targetDev, targetMnt string) (map[string]any, error) {
return runKeymediaOp(ctx, action, map[string]string{"staging": staging, "profile": profile, "target_dev": targetDev, "target_mnt": targetMnt})
}
// runKeymediaOp — общий вход к root-воркеру: action + произвольные поля
// запроса (staging, profile, target_dev, target_mnt, extra). Используется и
// мастером ключа, и автообновлением сертификатов НРД (nsdcerts.go).
func runKeymediaOp(ctx context.Context, action string, fields map[string]string) (map[string]any, error) {
id := fmt.Sprintf("%s-%d", action, time.Now().UnixNano())
reqPath := filepath.Join(keymediaReqDir, id+".req")
resPath := filepath.Join(keymediaReqDir, id+".res")
defer os.Remove(resPath)
reqBody, _ := json.Marshal(map[string]string{
"action": action, "staging": staging, "profile": profile,
"target_dev": targetDev, "target_mnt": targetMnt,
})
req := map[string]string{"action": action}
for k, v := range fields {
req[k] = v
}
reqBody, _ := json.Marshal(req)
// Пишем атомарно (tmp → rename), чтобы .path не подхватил полупустой файл.
tmp := reqPath + ".tmp"
if err := os.WriteFile(tmp, reqBody, 0o660); err != nil {
@@ -487,6 +552,10 @@ func runKeymediaHelper(ctx context.Context, action, staging, profile, targetDev,
continue // ещё не готово
}
res := map[string]any{}
// systemd/скрипты могут напечатать предупреждение перед JSON — берём с первой «{».
if i := bytes.IndexByte(b, '{'); i > 0 {
b = b[i:]
}
if jerr := json.Unmarshal(b, &res); jerr != nil {
return nil, fmt.Errorf("разбор ответа воркера: %v (%s)", jerr, strings.TrimSpace(string(b)))
}