958d777751
Добавлена вкладка «Настройка» в admin-панели lk-gateway. Позволяет ввести параметры каждой подсистемы прямо через веб-интерфейс, проверить подключение и запустить тестовую заявку в один клик. internal/lkgateway/runtimeconfig.go: - Runtime-конфиг с персистом в JSON (BJ_SETUP_PATH или ~/.bj/setup.json) - Поля: PostgresDSN, Crypto (provider/socket/jcp_path/license_key), NSD (profile/igw_base_url/key_container), LK (callback_url), LastTestRun (результат последнего тестового прогона) - ReadinessSummary() для блока «Готовность системы: X из Y» internal/lkgateway/setup.go: - GET /admin/setup — страница настройки - POST /admin/setup/postgres — DSN + sql.Ping (без pgx-драйвера упадёт на «unknown driver postgres», что покажет пользователю) - POST /admin/setup/crypto — provider/socket/jcp.jar/лицензия, проверка существования файла jcp.jar - POST /admin/setup/nsd — профиль/URL ИШ/контейнер, GET /healthz ИШ - POST /admin/setup/lk — callback URL + GET /healthz эмулятора/ЛК - POST /admin/setup/test-run — пробная сквозная заявка с предзаполнением (Иванов, 1500 акций Газпрома, ИИС T03), опрос статуса до финального internal/lkgateway/web/templates/admin_setup.html: - 4 карточки подсистем со статус-индикаторами (зелёная/красная точка) - Inline-формы через <details>/<summary>: открыты для не настроенных, свёрнуты для уже настроенных - Карточка «Тестовый прогон» с историей последнего результата - Прогресс «Готовность системы: X из Y» в верхней части internal/lkgateway/server.go: - Server.rc *RuntimeConfig — поднимается при NewServer - CheckOptions для admin-дашборда теперь берутся из runtime-конфига, а не только из ENV — изменения в /admin/setup сразу видны в /admin/ и /admin/status без перезапуска В layout.html добавлена nav-ссылка «Настройка», между «Дашборд» и «Заявки». Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
230 lines
6.7 KiB
Go
230 lines
6.7 KiB
Go
package lkgateway
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log"
|
||
"net/http"
|
||
"time"
|
||
|
||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
|
||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/mock"
|
||
)
|
||
|
||
// ServerConfig — конфигурация HTTP-сервера lk-gateway.
|
||
type ServerConfig struct {
|
||
Addr string
|
||
DefaultSender m2m.DeponentCode
|
||
DefaultReceiver m2m.DeponentCode
|
||
CheckOptions func() CheckOptions
|
||
MockDecisionDelay time.Duration // 0 = дефолт 3 секунды
|
||
SetupPath string // путь к JSON-файлу runtime-конфига (пусто = ~/.bj/setup.json)
|
||
}
|
||
|
||
// Server — обвязка HTTP + сервис + workers.
|
||
type Server struct {
|
||
cfg ServerConfig
|
||
svc *Service
|
||
mock *mock.Sender
|
||
store *SeedStore
|
||
rc *RuntimeConfig
|
||
mux *http.ServeMux
|
||
server *http.Server
|
||
}
|
||
|
||
// NewServer собирает Server с in-memory репозиторием, mock NSDSender,
|
||
// SeedStore и REST + Admin маршрутами.
|
||
func NewServer(cfg ServerConfig) (*Server, error) {
|
||
store := NewSeedStore()
|
||
mockCfg := mock.DefaultConfig()
|
||
mockCfg.NSDSenderCode = "MC0010300000"
|
||
if cfg.MockDecisionDelay > 0 {
|
||
mockCfg.DecisionDelay = cfg.MockDecisionDelay
|
||
}
|
||
sender := mock.NewSender(mockCfg)
|
||
|
||
svc := NewService(Config{
|
||
Repository: m2mcore.NewMemoryRepository(),
|
||
Sender: sender,
|
||
Store: store,
|
||
Recorder: m2mcore.NewMemoryRecorder(),
|
||
DefaultSender: cfg.DefaultSender,
|
||
DefaultReceiver: cfg.DefaultReceiver,
|
||
})
|
||
|
||
rc, err := NewRuntimeConfig(cfg.SetupPath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// Если runtime-конфиг уже содержит callback URL — применяем его.
|
||
if s := rc.Snapshot(); s.LK.CallbackURL != "" {
|
||
svc.callbackURL = s.LK.CallbackURL
|
||
}
|
||
|
||
mux := http.NewServeMux()
|
||
RegisterAPI(mux, svc)
|
||
|
||
// CheckOptions берётся из runtime-конфига при каждом запросе на дашборд.
|
||
checkOpts := func() CheckOptions {
|
||
s := rc.Snapshot()
|
||
profile := "demo (mock NSD)"
|
||
if s.NSD.Profile != "" {
|
||
profile = s.NSD.Profile
|
||
}
|
||
return CheckOptions{
|
||
PostgresDSN: s.Postgres.DSN,
|
||
CryptoSocket: s.Crypto.SocketPath,
|
||
NSDAdapterURL: s.NSD.IGWBaseURL,
|
||
LKCallbackURL: s.LK.CallbackURL,
|
||
Profile: profile,
|
||
CryptoProvider: nonEmpty(s.Crypto.Provider, "stub"),
|
||
Timeout: 2 * time.Second,
|
||
}
|
||
}
|
||
if cfg.CheckOptions != nil {
|
||
// Опциональный override (например, из cmd/lk-gateway для override ENV-перетягивания).
|
||
checkOpts = cfg.CheckOptions
|
||
}
|
||
|
||
adminTpl, err := RegisterAdmin(mux, svc, checkOpts)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
registerSetup(mux, adminTpl, rc, svc)
|
||
registerHealth(mux)
|
||
registerSetCallback(mux, svc, rc)
|
||
registerSeedListing(mux, store)
|
||
|
||
return &Server{
|
||
cfg: cfg,
|
||
svc: svc,
|
||
mock: sender,
|
||
store: store,
|
||
rc: rc,
|
||
mux: mux,
|
||
server: &http.Server{
|
||
Addr: cfg.Addr,
|
||
Handler: mux,
|
||
ReadHeaderTimeout: 5 * time.Second,
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
// RuntimeConfig возвращает текущий runtime-конфиг (для тестов).
|
||
func (s *Server) RuntimeConfig() *RuntimeConfig { return s.rc }
|
||
|
||
func nonEmpty(s, def string) string {
|
||
if s == "" {
|
||
return def
|
||
}
|
||
return s
|
||
}
|
||
|
||
// SetCallbackURL обновляет адрес, куда отправлять PATCH callback'и в ЛК.
|
||
func (s *Server) SetCallbackURL(url string) { s.svc.callbackURL = url }
|
||
|
||
// Service возвращает Service для тестов.
|
||
func (s *Server) Service() *Service { return s.svc }
|
||
|
||
// Mock возвращает mock-сендер.
|
||
func (s *Server) Mock() *mock.Sender { return s.mock }
|
||
|
||
// Store возвращает SeedStore.
|
||
func (s *Server) Store() *SeedStore { return s.store }
|
||
|
||
// Mux возвращает обработчик (для httptest).
|
||
func (s *Server) Mux() http.Handler { return s.mux }
|
||
|
||
// Run поднимает HTTP-сервер и фоновый Decisions-consumer.
|
||
// Блокируется до ctx.Done().
|
||
func (s *Server) Run(ctx context.Context) error {
|
||
go s.consumeDecisions(ctx)
|
||
|
||
errCh := make(chan error, 1)
|
||
go func() {
|
||
log.Printf("lk-gateway: listen %s", s.cfg.Addr)
|
||
errCh <- s.server.ListenAndServe()
|
||
}()
|
||
|
||
select {
|
||
case <-ctx.Done():
|
||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
_ = s.server.Shutdown(shutdownCtx)
|
||
return nil
|
||
case err := <-errCh:
|
||
if errors.Is(err, http.ErrServerClosed) {
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
}
|
||
|
||
// consumeDecisions слушает Decisions от mock и обновляет соответствующие сделки.
|
||
func (s *Server) consumeDecisions(ctx context.Context) {
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case d := <-s.mock.Decisions():
|
||
if d == nil {
|
||
continue
|
||
}
|
||
if err := s.svc.ApplyDecision(ctx, d); err != nil {
|
||
log.Printf("lk-gateway: ApplyDecision GUID=%s: %v", d.Header.GUID, err)
|
||
} else {
|
||
log.Printf("lk-gateway: Decision применён GUID=%s, callback в %s", d.Header.GUID, s.svc.callbackURL)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func registerHealth(mux *http.ServeMux) {
|
||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||
w.WriteHeader(http.StatusOK)
|
||
_, _ = w.Write([]byte("ok\n"))
|
||
})
|
||
}
|
||
|
||
// registerSetCallback — служебный POST /admin/api/callback-url для
|
||
// эмулятора ЛК, чтобы сообщить gateway свой URL. Если URL уже сохранён
|
||
// в runtime-конфиге (пользователь явно настроил его через UI), запрос
|
||
// эмулятора игнорируется — приоритет у явно настроенного.
|
||
func registerSetCallback(mux *http.ServeMux, svc *Service, rc *RuntimeConfig) {
|
||
mux.HandleFunc("/admin/api/callback-url", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
url := r.URL.Query().Get("url")
|
||
if url == "" {
|
||
http.Error(w, "url required", http.StatusBadRequest)
|
||
return
|
||
}
|
||
if rc != nil {
|
||
if s := rc.Snapshot(); s.LK.CallbackURL != "" {
|
||
w.WriteHeader(http.StatusOK)
|
||
_, _ = w.Write([]byte("kept-user-configured"))
|
||
return
|
||
}
|
||
}
|
||
svc.callbackURL = url
|
||
w.WriteHeader(http.StatusOK)
|
||
_, _ = w.Write([]byte("ok"))
|
||
})
|
||
}
|
||
|
||
func registerSeedListing(mux *http.ServeMux, store *SeedStore) {
|
||
mux.HandleFunc("/admin/api/clients", func(w http.ResponseWriter, _ *http.Request) {
|
||
type c struct {
|
||
ID, LastName, FirstName, MiddleName string
|
||
}
|
||
out := make([]c, 0)
|
||
for _, cl := range store.Clients() {
|
||
out = append(out, c{ID: cl.ID, LastName: cl.LastName, FirstName: cl.FirstName, MiddleName: cl.MiddleName})
|
||
}
|
||
writeJSON(w, http.StatusOK, out)
|
||
})
|
||
}
|