jim800121chen cabbdde495 feat(visionA-backend): DB 接入後續 — OIDC/pairing FK 收尾 + B4 metadata + nginx healthz + 補測試
DB 接入塊 0-5 上主幹後的收尾工作,讓 DB-on 模式可真人使用 + 補齊功能與測試。

OIDC / pairing FK 修復(接 DB 上線必要):
- 新建 internal/user package(User + Store + InMemory + Postgres);OIDC callback
  驗證 id_token 成功後 fail-closed upsert users(sub 直接當 users.id,MC sub 為 UUID)
- pairing exchange 雲端自建 device(不動 local-tool)+ 同 tx 綁 session token;
  自建 device 空 serial 寫 NULL(避免撞 partial unique)
- device.SaveTx / session.CreateTx 新增 tx-aware 版本

B4 model metadata:
- 轉檔 result 的 analysis_info(input_shape/classes/framework)串進 model:
  converter_client → flow → adapter → model.Model → PG → ModelResponse DTO
- input_shape 優先用陣列、後備四維組 NCHW、缺一不亂組;全 optional 防禦性
- 前端詳細頁顯示(另 repo);轉檔端串接交接檔 b4-converter-handoff.md

nginx healthz(部署層):
- 新增 /healthz/deep 轉發 backend(ping PG+Redis、down 回 503)給 LB
- 修掉 default_server return 444 短路 bug(docker healthcheck 長期 unhealthy 真因)

storage error 統一映射(不洩漏 storage 後端細節)。

測試:補 internal/api(storage/errors handler)、cmd/api-server(seed/adapter)、
internal/db(redis)、relay/session 弱處,含 testcontainers integration。
DB 接入相關 package 真環境覆蓋達 88-94%。全程 Reviewer 審查 + 130 真 PG/Redis dbtest 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:36:35 +08:00

90 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package db
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
// Querier 抽象「能跑查詢的東西」——同時被 *pgxpool.Pool 與 pgx.Tx 滿足。
//
// 設計目的DB 接入塊 5.2 跨 store 交易):
//
// repository 方法收 Querier 而非寫死 *pgxpool.Pool就能在「池上直接跑自動 commit
// 或「在某個 tx 內跑(隨 tx commit/rollback」之間自由切換而呼叫端 / handler 一行不改。
// cascade 撤銷(刪 device → 同 tx 撤 pairing + session token正是靠這個介面讓 device repo
// 與 token store 在同一個 pgx.Tx 下操作、達成「整筆成功或整筆回滾」。
//
// 方法集刻意只取 repository 實際用到的三個Exec / Query / QueryRow不暴露 Begin/Commit
// 等交易控制——交易邊界由 WithTx 統一掌控repository 不該自行 commit/rollback。
//
// pgxpool.Pool 與 pgx.Tx 都已實作這三個方法(簽章完全相符),故下方兩個編譯期斷言成立。
type Querier interface {
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}
// WithTx 在一個 pgx 交易內執行 fn成功則 commit、失敗fn 回 error 或 panic則 rollback。
//
// 語意(塊 5.1
// - Begin 失敗 → 直接回 error連線層問題fail-fast
// - fn 回 error → rollback 並回 fn 的 error保留原因呼叫端可 errors.Is 比對 domain error
// - fn 成功 → commitcommit 失敗回 commit error。
// - panic → rollback 後重新 panic不吞讓上層 recovery middleware 處理)。
//
// ctx 取消會讓 Begin / fn 內的查詢 / Commit 各自因 context 失敗而中止——交易不會半開。
//
// 注意fn 內所有 DB 操作都必須用傳入的 qtx不可再用外層 pool否則那些操作不在交易內、
// 失敗時不會被 rollback。
func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(q Querier) error) (err error) {
if pool == nil {
return errors.New("db: WithTx requires non-nil pool")
}
tx, beginErr := pool.Begin(ctx)
if beginErr != nil {
return fmt.Errorf("db: begin tx: %w", beginErr)
}
// committed 避免 commit 後又 rollbackpgx 對已結束的 tx rollback 會回 ErrTxClosed
committed := false
defer func() {
if committed {
return
}
// Rollback 用 background context若是 ctx 已取消才走到這裡,仍要盡力 rollback。
if rbErr := tx.Rollback(context.Background()); rbErr != nil && !errors.Is(rbErr, pgx.ErrTxClosed) {
// 只有在 fn 本身沒回 error亦即 err 為 nilrollback error 才升級成回傳值;
// 否則保留 fn 的原始 error更有診斷價值rollback error 只是次要訊號。
if err == nil {
err = fmt.Errorf("db: rollback tx: %w", rbErr)
}
}
}()
if fnErr := fn(tx); fnErr != nil {
return fnErr // defer 會 rollback
}
if cErr := tx.Commit(ctx); cErr != nil {
return fmt.Errorf("db: commit tx: %w", cErr)
}
committed = true
return nil
}
// 編譯期斷言:*pgxpool.Pool 與 pgx.Tx 都滿足 Querier。
//
// 兩者都顯式斷言,避免任一方未來簽章漂移時只在 runtime 才爆。
// pgx.Tx 本身是 interfacetyped-nil(pgx.Tx)(nil))可作為靜態斷言對象——編譯器只看
// 靜態型別是否滿足 Querier、不解參考該 nil 值,故安全。
var (
_ Querier = (*pgxpool.Pool)(nil)
_ Querier = (pgx.Tx)(nil)
)