Первичная настройка, часть 1: префикс торгового счёта DEPO — настройка с подсказкой по учёту (был зашит «ВТНКЦ» в 7 запросах); шаг 8 — адрес ИШ вместо ONYX, профили prod-gost/prod-rsa (пункт prod уводил bj в имитацию), без неиспользуемого контейнера, перезапуск bj после смены контура; реквизиты НРД на шаге 4; точная проверка процедур bj2depo и таблиц Fancy M2M; QUIK — объекты M2M и справочник получателей, без зашитого IP; сертификаты из справочника ИШ (FindCertificates — заглушка), уведомления о сроке за 30/14/7/3/1 день; тексты мастера, нумерация, режим разработки; уникальные id оповещений с русскими заголовками

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-05 16:04:43 +03:00
parent 9360543589
commit fdf0ddd498
37 changed files with 807 additions and 129 deletions
+38 -21
View File
@@ -21,6 +21,7 @@ import (
"github.com/jackc/pgx/v5"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/cryptocli"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter"
)
// crypto-сертификаты на текущих токенах (для отображения на странице).
@@ -28,20 +29,7 @@ import (
// Валидаты — здесь временно возвращаем пустой список, пока
// crypto-service не получит метод ListCertificates.
func (h *setupHandlers) listCertsForUI() []cryptocli.Certificate {
s := h.rc.Snapshot()
if s.Crypto.Provider == "" || s.Crypto.Provider == "stub" {
return nil
}
cli := cryptocli.New(cryptocli.Config{
Provider: cryptocli.Provider(s.Crypto.Provider),
SocketPath: s.Crypto.SocketPath,
ModulePath: s.Crypto.ModulePath,
})
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
certs, _ := cli.FindCertificates(ctx)
return certs
return storeCertsForUI(h.rc.Snapshot())
}
// setupHandlers — обработчики /admin/setup/*.
@@ -84,6 +72,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service
mux.HandleFunc("/admin/setup/wizard-depo/tables", h.wizardDepoTables)
mux.HandleFunc("/admin/setup/wizard-depo/job", h.wizardDepoJob)
mux.HandleFunc("/admin/setup/wizard-depo/recheck", h.wizardDepoRecheck)
mux.HandleFunc("/admin/setup/fancy/acc-prefix", h.accPrefix)
mux.HandleFunc("/admin/setup/license", h.saveLicense)
mux.HandleFunc("/admin/setup/license/activate", h.licenseActivate)
mux.HandleFunc("/admin/setup/license/renew", h.licenseRenewNow)
@@ -91,6 +80,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service
mux.HandleFunc("/admin/setup/logo/reset", h.resetLogo)
mux.HandleFunc("/admin/setup/nsd", h.saveNSD)
mux.HandleFunc("/admin/setup/org", h.saveOrg)
mux.HandleFunc("/admin/setup/nsd-requisites", h.saveNSDRequisites)
mux.HandleFunc("/admin/setup/lk", h.saveLK)
mux.HandleFunc("/admin/setup/lk/dist", h.lkSaveDist)
mux.HandleFunc("/admin/setup/lk/dist.tar.gz", h.lkDistDownload)
@@ -251,8 +241,8 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
// Валидата ставится с образа/установщиком: если она на месте, шаг выполнен без ввода параметров
// (bj работает с ней через свой криптосервис, путь к модулю не нужен).
d.Done.Crypto = d.ValidataVer != "" || (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.Certs = len(ownValidCerts(d.Certs)) > 0
d.Done.NSD = doneNSD(s)
d.Done.TestRun = s.LastTest != nil || (!d.DevMode && d.Done.NSD) // вне режима разработки тестовой заявки нет
// Мастер идёт по порядку: каждый шаг показывается, даже если он уже выполнен (установщиком,
@@ -272,7 +262,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
step = wizardSteps
}
d.Step = step
if step == 4 && strings.TrimSpace(s.Fancy.Server) != "" && (orgCode(s) == "" || orgINN(s) == "") && depoDetectResult() == nil {
if step == 4 && strings.TrimSpace(s.Fancy.Server) != "" && !doneOrg(s) && depoDetectResult() == nil {
// организация ещё не задана, а база DEPO подключена — определяем реквизиты сами: карточка
// покажет их заполненными, пользователю остаётся проверить и сохранить
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
@@ -296,8 +286,8 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
d.Sub = "done"
}
}
d.Done.Org = orgCode(s) != "" && orgINN(s) != ""
d.Done.Quik = !d.DepoLicensed || s.Quik.Verified
d.Done.Org = doneOrg(s)
d.Done.Quik = !d.DepoLicensed || doneQuik(s)
if step == 3 && d.DepoLicensed && strings.TrimSpace(s.Fancy.Server) != "" {
// Есть ли у сервера SQL Agent: у Express его нет, у PostgreSQL — тоже; тогда обработку
// запускает сам bj. Иначе по умолчанию джобу SQL Agent создаёт bj.
@@ -721,6 +711,22 @@ func (h *setupHandlers) saveNSD(w http.ResponseWriter, r *http.Request) {
SectionID: strings.TrimSpace(r.FormValue("section_id")),
IGWv904: r.FormValue("igw_v904") == "on",
}
cur := h.rc.Snapshot().NSD
if r.FormValue("keep_requisites") == "1" { // мастер: реквизиты НРД — на шаге «Организация»
s.DeponentCode, s.AccountID, s.SectionID = cur.DeponentCode, cur.AccountID, cur.SectionID
}
if _, ok := r.Form["key_container"]; !ok {
s.KeyContainer = cur.KeyContainer
}
if s.Profile == "prod" { // старый пункт мастера: такого профиля нет — bj ушёл бы в имитацию
s.Profile = "prod-gost"
}
if s.Profile != "" {
if _, err := nsdadapter.LookupProfile(s.Profile); err != nil {
setupFlash(w, r, "НРД: неизвестный контур "+s.Profile)
return
}
}
if s.IGWBaseURL != "" {
if _, err := url.ParseRequestURI(s.IGWBaseURL); err != nil {
setupFlash(w, r, "nsd: невалидный URL: "+err.Error())
@@ -728,7 +734,7 @@ func (h *setupHandlers) saveNSD(w http.ResponseWriter, r *http.Request) {
}
// У ИШ нет /healthz — проверяем рабочий эндпоинт Web API.
if err := tryHTTPHealth(s.IGWBaseURL + "/api/admin/engine/state"); err != nil {
setupFlash(w, r, "nsd: ИШ не отвечает (engine/state): "+err.Error())
setupFlash(w, r, "Шлюз НРД не отвечает по адресу "+s.IGWBaseURL+": "+err.Error()+". Проверьте, что ИШ установлен и запущен (служба igate).")
return
}
}
@@ -736,7 +742,18 @@ func (h *setupHandlers) saveNSD(w http.ResponseWriter, r *http.Request) {
setupFlash(w, r, "nsd: ошибка сохранения: "+err.Error())
return
}
setupFlash(w, r, "nsd-adapter: настройки сохранены")
msg := "Шлюз НРД: настройки сохранены"
if s.Profile != cur.Profile || s.IGWBaseURL != cur.IGWBaseURL {
// отправитель в НРД создаётся при запуске — перезапускаем bj, чтобы контур и адрес применились сразу
msg += ". bj перезапускается, чтобы применить контур и адрес, — обновите страницу через 10–15 секунд."
go func() {
time.Sleep(2 * time.Second)
if _, err := restartUnit(context.Background(), "bj-server"); err != nil {
log.Printf("setup: перезапуск bj-server после смены контура НРД: %v", err)
}
}()
}
setupFlash(w, r, msg)
}
func (h *setupHandlers) saveLK(w http.ResponseWriter, r *http.Request) {