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

136 lines
4.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.

package main
import (
"context"
"log/slog"
"time"
"github.com/google/uuid"
"visiona-backend/internal/auth"
"visiona-backend/internal/device"
"visiona-backend/internal/model"
"visiona-backend/internal/user"
)
// demoSeedUserID 是 DB 啟用時 seed 用的固定 demo user UUID。
//
// 雛形的 StaticUserID"demo-user")不是合法 UUID無法當 models.owner_user_idUUID + FK
// DB-backed seed 改用此固定 UUID並先 upsert 一筆對應 users 列以滿足 FK
// in-memory seed 不受影響(仍用 cfg.Auth.StaticUserID
const demoSeedUserID = "00000000-0000-0000-0000-0000000000d3"
// seedDemoData 在啟動時塞入示範資料,方便本機開發 / demo 不必跑完整 pairing。
//
// 觸發條件VISIONA_SEED_DEMO_DATA=true
//
// 內容:
// - 一個示範 deviceKL520
// - 一個示範 modelYOLOv5 Face
// - 一個示範 pairing tokenlog 出來方便手動 copy
//
// 注意:
// - 失敗只 log warning不阻擋啟動
// - 重複呼叫會產生重複資料本函式只該被呼叫一次main 已保證)
// - **不要**在生產環境啟用此 flag
//
// dbBacked 為 true 表 repository 已切到 Postgres塊 1+):此時 seed 的 model/device 必須用
// 合法 UUID 與已存在的 owner_user_idUUID + FK故先 upsert demo user、改用 demoSeedUserID。
// dbBacked 為 falsein-memory fallback時行為與雛形完全相同。
//
// demo user 透過注入的 userStore.Upsert 落地DB-on 時 = PostgresStore滿足 models/devices/
// pairing_tokens 的 owner_user_id FK——與 OIDC callback 走同一條 provision 路徑,避免 seed
// 自己手拼 INSERT users 與 store 行為不一致(問題 #1 的 seed 對齊)。
func seedDemoData(
devRepo device.Repository,
mdlRepo model.Repository,
pairings auth.PairingStore,
userStore user.Store,
userID string,
dbBacked bool,
log *slog.Logger,
) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
now := time.Now().UTC()
// owner / id 視 backend 決定DB-backed 需合法 UUID + 存在的 user 列。
// 塊 2 起 device 也落 DB故 model 與 device 的 demo owner 必須一致(同一 demoSeedUserID
// 否則 owner 對不上device.owner_user_id 也是 UUID + FK
modelOwnerID := userID
deviceOwnerID := userID
// pairing token 的 owneruser_id塊 3 起 pairing token 也落 DBuser_id 是
// REFERENCES users(id) 的 FK + UUID故 DB-backed 時必須與 model/device 一致用
// demoSeedUserID已 upsert 的合法 user不可用非-UUID 的 StaticUserID。
pairingOwnerID := userID
modelID := "demo-model-" + uuid.NewString()[:8]
deviceID := "demo-device-" + uuid.NewString()[:8]
if dbBacked {
modelOwnerID = demoSeedUserID
deviceOwnerID = demoSeedUserID
pairingOwnerID = demoSeedUserID
modelID = uuid.NewString()
deviceID = uuid.NewString() // device.id 為 UUID + FK 對齊;不可用非-UUID 字串
// 先確保 owner user 存在(滿足 models / devices owner_user_id FK
// 走 userStore.UpsertDB-on = PostgresStore與 OIDC callback 同一條 provision 路徑。
if err := userStore.Upsert(ctx, &user.User{
ID: demoSeedUserID,
Email: "demo@visiona.local",
Name: "Demo User (seeded)",
}); err != nil {
log.Warn("seed: ensure demo user failed", "error", err)
}
}
// 1. Demo device塊 2 起 DB-backed 時走 Postgresowner 對齊 demoSeedUserID
dev := &device.Device{
ID: deviceID,
OwnerUserID: deviceOwnerID,
Name: "Demo KL520 (seeded)",
DeviceType: "kl520",
SerialNumber: "DEMO-SN-001",
RemoteStatus: device.RemoteStatusOffline,
Status: device.USBStatusUnknown,
CreatedAt: now,
UpdatedAt: now,
}
if err := devRepo.Save(ctx, dev); err != nil {
log.Warn("seed: device save failed", "error", err)
} else {
log.Info("seed: demo device created", "id", dev.ID, "name", dev.Name)
}
// 2. Demo model
mdl := &model.Model{
ID: modelID,
OwnerUserID: modelOwnerID,
Name: "YOLOv5 Face (seeded)",
TargetChip: "kl520",
FileSize: 1024 * 1024, // 1 MB
Source: model.SourceUploaded,
StorageKey: "models/" + modelOwnerID + "/demo.nef",
CreatedAt: now,
UpdatedAt: now,
UploadedAt: &now,
}
if err := mdlRepo.Save(ctx, mdl); err != nil {
log.Warn("seed: model save failed", "error", err)
} else {
log.Info("seed: demo model created", "id", mdl.ID, "name", mdl.Name)
}
// 3. Demo pairing tokenlog plaintext 方便開發 — 雛形 demo 用,生產禁用)
// owner 用 pairingOwnerIDDB-backed 時為 demoSeedUserID滿足 user_id FK
pt, _, err := pairings.Create(ctx, pairingOwnerID, 24*time.Hour)
if err != nil {
log.Warn("seed: pairing token create failed", "error", err)
} else {
log.Info("seed: demo pairing token created (use for local-tool tunnel)",
"token", pt,
"ttl", "24h")
}
return nil
}