把 visionA-backend 6 個 in-memory store 接到資料庫持久化,範圍=完整 (PG 全接 + session 接 Redis + 交易韌性)。interface / handler 不動, 只加 DB 實作 + 換 wiring,config 未設 DB 時保留 in-memory fallback。 - 塊 0 基礎建設:pgx/v5 連線池 + DatabaseConfig/RedisConfig + golang-migrate runner(embed)+ cmd/migrate + testcontainers 測試基礎建設 - 塊 1 model → Postgres:array 映射、upsert 保留 CreatedAt、faa_object_key、 三維 filter(owner/chip/source)、soft-delete partial index - 塊 2 device → Postgres:partial unique(已刪 serial 可重註冊)、雙狀態欄位 - 塊 3 token → Postgres:pairing_tokens + session_tokens 分表、token_hash 當 PK - 塊 4 userSession → Redis:idle + absolute 雙 TTL 取代 cleanup goroutine (tunnel session 維持 in-memory,yamux handle 不可序列化) - 塊 5 交易/韌性:WithTx helper + 刪 device cascade 撤銷 token(同 tx 原子) + /healthz ping PG/Redis(fail-fast 503)+ pgx error 統一映射(不洩漏 raw error) 降級策略(fail-fast):PG 掉 → 持久資料 API 回 503;Redis 掉 → session 失敗 不自動 fallback in-memory(避免多機 session 不同步)。 DB:PostgreSQL 14.23(gen_random_uuid 內建、無 citext → email 用 lower() unique index)。每塊經 Reviewer 審查 + 真 PG/Redis testcontainers 全量 dbtest 綠燈, in-memory fallback 未受影響。 docs: 同步更新 database.md(schema/config/migration 清單)+ api-spec.md (409/503 錯誤碼、/healthz 新行為、device unpair cascade)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
2.2 KiB
Go
77 lines
2.2 KiB
Go
// Command migrate 是 visionA-backend 的獨立 migration 工具。
|
||
//
|
||
// DB 接入塊 0:提供「不啟動整個 api-server 也能跑 migration」的入口,
|
||
// 供 CI / 部署流程 / 手動操作使用。api-server 啟動時的 auto-migrate(VISIONA_DB_AUTO_MIGRATE)
|
||
// 與本工具共用同一份嵌入式 migration(migrations.FS)+ internal/db.Migrator,行為一致。
|
||
//
|
||
// 用法:
|
||
//
|
||
// go run ./cmd/migrate up # 套用所有未執行的 migration(預設)
|
||
// go run ./cmd/migrate down # 回退一個版本
|
||
// go run ./cmd/migrate version # 顯示目前 schema 版本
|
||
//
|
||
// 連線資訊一律走 VISIONA_DB_* 環境變數(與 api-server 相同),不接受密碼當 CLI 參數
|
||
// (避免密碼出現在 shell history / process list)。
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
|
||
"visiona-backend/internal/config"
|
||
"visiona-backend/internal/db"
|
||
"visiona-backend/internal/logger"
|
||
)
|
||
|
||
func main() {
|
||
cfg := config.Load()
|
||
log := logger.New(cfg.Logger.Level).With("service", "migrate")
|
||
|
||
if !cfg.Database.Enabled() {
|
||
log.Error("database not configured",
|
||
"hint", "set VISIONA_DB_HOST + VISIONA_DB_USER + VISIONA_DB_NAME")
|
||
os.Exit(1)
|
||
}
|
||
|
||
cmd := "up"
|
||
if len(os.Args) > 1 {
|
||
cmd = os.Args[1]
|
||
}
|
||
|
||
mg, err := db.NewMigrator(cfg.Database, log)
|
||
if err != nil {
|
||
log.Error("failed to init migrator", "error", err, "target", db.SafeTarget(cfg.Database))
|
||
os.Exit(1)
|
||
}
|
||
defer func() {
|
||
if cerr := mg.Close(); cerr != nil {
|
||
log.Warn("migrator close error", "error", cerr)
|
||
}
|
||
}()
|
||
|
||
switch cmd {
|
||
case "up":
|
||
if err := mg.Up(); err != nil {
|
||
log.Error("migrate up failed", "error", err)
|
||
os.Exit(1)
|
||
}
|
||
log.Info("migrate up complete", "target", db.SafeTarget(cfg.Database))
|
||
case "down":
|
||
if err := mg.Down(); err != nil {
|
||
log.Error("migrate down failed", "error", err)
|
||
os.Exit(1)
|
||
}
|
||
log.Info("migrate down (one step) complete", "target", db.SafeTarget(cfg.Database))
|
||
case "version":
|
||
ver, dirty, err := mg.Version()
|
||
if err != nil {
|
||
log.Error("read version failed", "error", err)
|
||
os.Exit(1)
|
||
}
|
||
log.Info("schema version", "version", ver, "dirty", dirty)
|
||
default:
|
||
fmt.Fprintf(os.Stderr, "unknown command %q; use one of: up | down | version\n", cmd)
|
||
os.Exit(2)
|
||
}
|
||
}
|