feat(m2mcore): PgRepository через pgx + интеграция в lk-gateway
PostgreSQL-репозиторий для m2m_core.deals — реальное хранилище сделок вместо in-memory. Выбор Repository происходит автоматически в lkgateway.NewServer: если в runtime-конфиге задан Postgres DSN, поднимается pgxpool и используется PostgresRepository; иначе fallback на MemoryRepository. internal/m2mcore/pgrepo.go: - PostgresRepository: Create (идемпотентный по guid через ON CONFLICT DO NOTHING), GetByGUID, GetByID, Update, List (с фильтрами state/investor/created_*), AppendEvent для журнала deal_events - request_xml/response_xml/decision_xml хранятся как windows-1251 XML через nsdxml, на чтении парсятся обратно в m2m.M2M* структуры - stages — jsonb с историей FSM-переходов migrations/m2m-core/002__stages.sql: - ALTER TABLE deals ADD COLUMN stages jsonb DEFAULT '[]' internal/lkgateway/server.go: - При NewServer проверяется runtime-config: если есть DSN → PostgresRepository, иначе MemoryRepository; ошибка подключения логируется с fallback на in-memory - Тесты используют tempdir SetupPath для изоляции от реальной БД internal/lkgateway/setup.go: - tryPingPostgres переписан с database/sql (требует регистрации драйвера) на pgx.Connect — теперь форма /admin/setup/postgres реально проверяет подключение перед сохранением DSN Проверено сквозным smoke-тестом: введение DSN через UI → сохранение в ~/.bj/setup.json → перезапуск lk-gateway → лог "PostgresRepository подключён (m2m_core.deals)" → сделки реально пишутся в БД. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,8 +33,11 @@ type Server struct {
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// NewServer собирает Server с in-memory репозиторием, mock NSDSender,
|
||||
// SeedStore и REST + Admin маршрутами.
|
||||
// NewServer собирает Server с репозиторием, mock NSDSender, SeedStore
|
||||
// и REST + Admin маршрутами. Выбор Repository:
|
||||
// - если в runtime-конфиге (или ENV-fallback в cfg) задан PostgresDSN
|
||||
// и pgx-Pool успешно создаётся — используется PostgresRepository;
|
||||
// - иначе fallback на MemoryRepository (M2-демо).
|
||||
func NewServer(cfg ServerConfig) (*Server, error) {
|
||||
store := NewSeedStore()
|
||||
mockCfg := mock.DefaultConfig()
|
||||
@@ -44,8 +47,27 @@ func NewServer(cfg ServerConfig) (*Server, error) {
|
||||
}
|
||||
sender := mock.NewSender(mockCfg)
|
||||
|
||||
rc, err := NewRuntimeConfig(cfg.SetupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Repository: pgx если DSN указан, иначе in-memory.
|
||||
var repo m2mcore.Repository = m2mcore.NewMemoryRepository()
|
||||
if dsn := rc.Snapshot().Postgres.DSN; dsn != "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
pgRepo, pgErr := m2mcore.NewPostgresRepository(ctx, dsn)
|
||||
cancel()
|
||||
if pgErr != nil {
|
||||
log.Printf("lk-gateway: PostgresRepository отказал, fallback MemoryRepository: %v", pgErr)
|
||||
} else {
|
||||
repo = pgRepo
|
||||
log.Printf("lk-gateway: PostgresRepository подключён (m2m_core.deals)")
|
||||
}
|
||||
}
|
||||
|
||||
svc := NewService(Config{
|
||||
Repository: m2mcore.NewMemoryRepository(),
|
||||
Repository: repo,
|
||||
Sender: sender,
|
||||
Store: store,
|
||||
Recorder: m2mcore.NewMemoryRecorder(),
|
||||
@@ -53,10 +75,6 @@ func NewServer(cfg ServerConfig) (*Server, error) {
|
||||
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
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -20,6 +21,8 @@ func newServer(t *testing.T) *lkgateway.Server {
|
||||
DefaultSender: "MC0079200000",
|
||||
DefaultReceiver: "MC0010300000",
|
||||
MockDecisionDelay: 50 * time.Millisecond,
|
||||
// Изоляция от ~/.bj/setup.json — каждый тест получает пустой файл.
|
||||
SetupPath: filepath.Join(t.TempDir(), "setup.json"),
|
||||
CheckOptions: func() lkgateway.CheckOptions {
|
||||
return lkgateway.CheckOptions{Profile: "test", CryptoProvider: "stub"}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -10,6 +9,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// setupHandlers — обработчики /admin/setup/*.
|
||||
@@ -278,23 +279,16 @@ func (h *setupHandlers) runTestClaim() {
|
||||
_ = h.rc.RecordTestRun(res)
|
||||
}
|
||||
|
||||
// tryPingPostgres пытается sql.Open + Ping с прокачкой драйвера; без
|
||||
// драйвера вернёт «unknown driver pgx»/«unknown driver postgres» —
|
||||
// тоже считаем ошибкой и показываем пользователю.
|
||||
// tryPingPostgres делает короткое подключение через pgx и Ping.
|
||||
func tryPingPostgres(dsn string) error {
|
||||
// Угадываем имя драйвера по префиксу.
|
||||
driver := "postgres"
|
||||
if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") {
|
||||
driver = "postgres"
|
||||
}
|
||||
db, err := sql.Open(driver, dsn)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
conn, err := pgx.Connect(ctx, dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
return db.PingContext(ctx)
|
||||
defer conn.Close(ctx)
|
||||
return conn.Ping(ctx)
|
||||
}
|
||||
|
||||
// tryHTTPHealth делает GET и ждёт 2xx.
|
||||
|
||||
Reference in New Issue
Block a user