第14章:种子数据 Seeder
本章分析 internal/seeder 包:为什么账号与默认提示词要作为种子数据、SeedAll 的幂等策略、以及种子与 model 包常量(DefaultVideoProjectPromptID = 1)之间的隐式契约。种子数据决定「初始化完成即可用」的体验,是 envinit 流程的最后一环。
流程
SeedAll(db, logger) 顺序执行两步:SeedAccounts(写入 admin 账号)→ SeedLLMSystemPrompts(写入默认系统提示词)。日志只打「开始/完成」两条,具体项由各子函数自行记录。
种子数据的必要性来自两处依赖:
- 登录需要账号:系统没有开放注册(v1 路由无 register),第一个账号只能靠种子注入,admin/admin 是文档承诺的默认凭据。
- 剪辑项目需要提示词 :
model.DefaultVideoProjectPromptID = 1意味着任何新建项目缺省引用 ID=1 的提示词------种子必须保证这条记录存在,否则「创建项目→AI 切片」链路会在读取提示词时空引用。
因此 seeder 的执行顺序也是依赖顺序:先账号后提示词没有硬约束(无外键),但逻辑上「谁创建提示词」需要 created_by 指向种子账号,所以顺序仍有讲究。
输入是空数据库(或 reinit 后的空库),输出是「登录可用 + 新建项目可用」的最小可用集。幂等性由各子函数的「存在即跳过」策略保证------init 可以重复执行不报错、不重复插入。
实现
seeder 与 migrator 一样是「仅由 envinit 调用」的包(包注释写明),运行时主服务完全不引用它------职责隔离让 webserver 二进制不含种子逻辑。
实现模式是典型的「按唯一键查,存在则跳过」:
SeedAccounts:按 username 查 admin,不存在则创建(密码经utils.HashPassword加密存储,与登录校验路径同一套密码工具,第 18 章)。SeedLLMSystemPrompts:按约定的 ID/名称查默认提示词,不存在则插入。默认提示词的内容是 AI 切片质量的地基------它教 LLM 如何从 ASR 句段中挑高价值内容、如何产出合规的标题/描述/话题;用户后续可在前端 CRUD 自定义(第 34 章),种子只保证第 1 号存在。
配套测试 account_seeder_test.go 与 llm_system_prompt_seeder_test.go 验证:空库插入、重复执行不重复插入------幂等性是被测试固化的行为,不是口头承诺。
一个值得注意的隐式契约链:VideoProject.PromptID 默认 1 ←→ seeder 保证 ID=1 存在 ←→ taskService 创建任务时若项目 PromptID 指向不存在的提示词则报错。三处分散在 model、seeder、service,任何一处改动都要跨包检查------这是弱关联架构的典型维护成本。
📌 设计决策
- 种子而非首次启动引导:CLI 工具明确表达「初始化是显式运维动作」,避免服务首次启动半初始化状态。
- 幂等插入而非 truncate + insert:保护已有数据(
init误跑在存量库上不破坏数据),代价是种子内容更新时旧库不会自动刷新(需 reinit 或手动改)。 - 默认账号写死 admin/admin 并在 README 强警告:可用性与安全的折中,reset-password 子命令提供立即改密的出路。
代码示例
SeedAll 的极简编排:
go
// internal/seeder/seeder.go
func SeedAll(db *gorm.DB, logger *zap.Logger) error {
logger.Info("开始填充种子数据...")
if err := SeedAccounts(db, logger); err != nil {
return err
}
if err := SeedLLMSystemPrompts(db, logger); err != nil {
return err
}
logger.Info("种子数据填充完成")
return nil
}
幂等种子的标准形态(示意,与 account_seeder.go 同构):
go
// 幂等:已存在则跳过,保证 init 可重复执行
func SeedAccounts(db *gorm.DB, logger *zap.Logger) error {
var count int64
if err := db.Model(&model.Account{}).
Where("username = ?", "admin").Count(&count).Error; err != nil {
return err
}
if count > 0 {
logger.Info("种子账号已存在,跳过")
return nil
}
hashed, err := utils.HashPassword("admin")
if err != nil {
return err
}
admin := model.Account{
Username: "admin", Password: hashed,
Nickname: "管理员", Roles: "admin", IsActive: 1,
}
if err := db.Create(&admin).Error; err != nil {
return fmt.Errorf("写入种子账号失败: %w", err)
}
logger.Info("已写入默认账号 admin/admin")
return nil
}
小结
- 种子 = 默认 admin 账号 + 1 号系统提示词,幂等可重复执行。
- PromptID=1 的跨包隐式契约是弱关联架构需要人工维护的典型例子。
- 种子逻辑只进 envinit 二进制,运行时不携带。
思考题
- 若默认提示词需要随版本迭代措辞,如何设计「种子内容版本号」实现存量库升级?
- 种子账号 admin 的首次登录强制改密应实现在哪一层(登录 service / 前端 / 中间件)?
项目信息
- GitHub仓库:github.com/Chyona/live-mixer
- 项目案例:gogoshine.com