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>
136 lines
4.9 KiB
Go
136 lines
4.9 KiB
Go
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_id(UUID + 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
|
||
//
|
||
// 內容:
|
||
// - 一個示範 device(KL520)
|
||
// - 一個示範 model(YOLOv5 Face)
|
||
// - 一個示範 pairing token(log 出來方便手動 copy)
|
||
//
|
||
// 注意:
|
||
// - 失敗只 log warning,不阻擋啟動
|
||
// - 重複呼叫會產生重複資料;本函式只該被呼叫一次(main 已保證)
|
||
// - **不要**在生產環境啟用此 flag
|
||
//
|
||
// dbBacked 為 true 表 repository 已切到 Postgres(塊 1+):此時 seed 的 model/device 必須用
|
||
// 合法 UUID 與已存在的 owner_user_id(UUID + FK),故先 upsert demo user、改用 demoSeedUserID。
|
||
// dbBacked 為 false(in-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 的 owner(user_id):塊 3 起 pairing token 也落 DB,user_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.Upsert(DB-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 時走 Postgres;owner 對齊 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 token(log plaintext 方便開發 — 雛形 demo 用,生產禁用)
|
||
// owner 用 pairingOwnerID:DB-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
|
||
}
|