package main import ( "context" "log/slog" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "visiona-backend/internal/auth" "visiona-backend/internal/device" "visiona-backend/internal/model" ) // 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 // // dbPool 非 nil 表 model repo 已切到 Postgres(塊 1):此時 seed 的 model 必須用合法 UUID // 與已存在的 owner_user_id(UUID + FK),故先 upsert demo user、改用 demoSeedUserID。 // dbPool 為 nil(in-memory fallback)時行為與雛形完全相同。 func seedDemoData( devRepo device.Repository, mdlRepo model.Repository, pairings auth.PairingStore, userID string, dbPool *pgxpool.Pool, 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 dbPool != nil { 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)。 if _, err := dbPool.Exec(ctx, `INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING`, demoSeedUserID, "demo@visiona.local", "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 }