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 }