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

127 lines
5.9 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.

// errors_db.go — 統一把 DBpgx錯誤映射成對外 API 錯誤DB 接入塊 5.4)。
//
// 兩個目的:
// 1. **fail-fast 落地**PG 連線失敗 / context 逾時 → 503 SERVICE_UNAVAILABLE不回假資料、
// 不誤報 500讓 load balancer 把這台拉出。對齊使用者拍板的「PG 都 fail-fast、不自動降級」。
// 2. **收掉塊 3 M1 技術債**handler 過去把 raw DB error 字串串進 response
// `"... failed: "+err.Error()`)會洩漏 schema / SQL / 連線細節。本檔統一映射,
// 對外只給穩定的 error code + 通用 messageraw error 只進 server log含 request_id
//
// 映射規則(依優先序):
//
// | 來源 | HTTP | code |
// |----------------------------------------|------|-----------------------|
// | context 逾時 / 取消DeadlineExceeded / Canceled| 503 | SERVICE_UNAVAILABLE |
// | 連線層失敗pgconn 無 SQLSTATErefused/reset/EOF| 503 | SERVICE_UNAVAILABLE |
// | unique violationSQLSTATE 23505 | 409 | CONFLICT |
// | 其餘有 SQLSTATE 的 PG 錯誤(語法/約束等)| 500 | INTERNAL_ERROR |
// | 非 DB 錯誤 / 未知 | 500 | INTERNAL_ERROR |
//
// ⚠️ not found 不在此映射domain 層device.ErrNotFound / auth.ErrInvalidToken 等)已把
// pgx.ErrNoRows 轉成自己的 sentinelhandler 應先用 errors.Is 比對那些 sentinel 回 404
// **再**把剩下的「真 DB 錯誤」交給 WriteDBError。這樣「正常的 not found」絕不會被誤判成 503。
package api
import (
"context"
"errors"
"log/slog"
"net/http"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgconn"
)
// dbErrorClass 是 DB 錯誤分類結果。
type dbErrorClass struct {
status int
code string
message string // 對外通用訊息(不含 raw DB 細節)
}
// classifyDBError 把一個 error 分類為對外的 (HTTP status, code, message)。
//
// 不洩漏 raw DB error回傳的 message 是固定通用字串raw error 由 WriteDBError 寫進 server log。
func classifyDBError(err error) dbErrorClass {
// 1) context 逾時 / 取消 → 503依賴慢/不可用fail-fast
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return dbErrorClass{http.StatusServiceUnavailable, ErrCodeServiceUnavailable,
"service temporarily unavailable"}
}
// 2) pgconn.PgErrorPG server 端回的錯誤(有 SQLSTATE
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "23505": // unique_violation
return dbErrorClass{http.StatusConflict, ErrCodeConflict, "resource already exists"}
default:
// 其餘 PG 錯誤(語法、約束、權限…)對外一律 500不洩漏 SQLSTATE / 欄位名。
return dbErrorClass{http.StatusInternalServerError, ErrCodeInternalError,
"internal error"}
}
}
// 3) pgconn.ConnectError 等「連不上 / 連線中斷」(無 SQLSTATE→ 503。
// pgxpool 連線層失敗connection refused / reset / EOF多半包成 *pgconn.ConnectError
// 或為底層 net error。用 errors.As 抓 ConnectError抓不到再保守視為連線問題前先看下一步。
var connErr *pgconn.ConnectError
if errors.As(err, &connErr) {
return dbErrorClass{http.StatusServiceUnavailable, ErrCodeServiceUnavailable,
"service temporarily unavailable"}
}
// 4) 其餘未知錯誤 → 500保守不假設是 DB down 以免把程式 bug 也報成 503
return dbErrorClass{http.StatusInternalServerError, ErrCodeInternalError, "internal error"}
}
// WriteDBError 把一個 DB 操作錯誤映射成對外 API 錯誤並寫回 response同時把 raw error 進 log。
//
// op 是操作描述(如 "get device" / "list models"),只進 log、不對外。
// 對外只給 classifyDBError 算出的穩定 code + 通用 message不洩漏 raw DB error
//
// 用法handler 內,已先處理過 domain sentinel 如 ErrNotFound 後):
//
// if err != nil {
// WriteDBError(c, deps.Logger, "list devices", err)
// return
// }
func WriteDBError(c *gin.Context, log *slog.Logger, op string, err error) {
cls := classifyDBError(err)
// raw error 只進 server log附 request_id + 對外 code方便對照不進 response。
logOrDefault(log).Error("db operation failed",
"op", op,
"error", err,
"http_status", cls.status,
"code", cls.code,
"request_id", RequestIDFrom(c))
WriteError(c, cls.status, cls.code, cls.message, nil)
}
// WriteStorageError 把一個 storage物件儲存操作錯誤映射成對外 API 錯誤並寫回 response
// 同時把 raw error 進 server log含 request_id
//
// 動機(塊 5 Minor-1presigned URL / Stat 等 storage 操作失敗時handler 過去把
// err.Error() 直接串進 response message可能洩漏 storage 後端細節bucket / endpoint /
// 內部路徑 / 簽章參數)給前端。本函式比照 WriteDBError對外只給穩定 code + 通用 message
// raw error 只進 server log。
//
// 對外一律 500 INTERNAL_ERRORstorage 後端不可用 / 設定錯誤對前端而言都是「伺服器端問題」,
// 不細分(與 WriteDBError 的 503 區隔開——DB 是 fail-fast 拉機制的依賴、storage 不是)。
// 呼叫端應先處理 storage sentinel如 storage.ErrNotFound回對應的 4xx再把剩下的
// 「真 storage 錯誤」交給本函式。
//
// op 是操作描述(如 "presigned put url" / "stat object"),只進 log、不對外。
func WriteStorageError(c *gin.Context, log *slog.Logger, op string, err error) {
logOrDefault(log).Error("storage operation failed",
"op", op,
"error", err,
"http_status", http.StatusInternalServerError,
"code", ErrCodeInternalError,
"request_id", RequestIDFrom(c))
WriteError(c, http.StatusInternalServerError, ErrCodeInternalError, "internal error", nil)
}