fix(store): VoiceUpdate 同步 settings.default_speaker (防潜在 stale ref)
Bug 报告: admin (或任何 caller) 改 voice.name 时, store 层的 VoiceUpdate 只 UPDATE voices 表, 没碰 settings.default_speaker。 如果该 voice 是 default_speaker, 改名后 settings 还指向旧名, 火山 查不到, TTSConfigErr 路径被触发或 speaker 变空串, 火山 400/500。 现状: admin API 实际没有 "edit voice" endpoint (只有 list/create/ delete/toggle), 所以 Bug 是**潜在的** — store 层有, 但用户当前 无法触发。修复属于防御性 + 未来加 update endpoint 不会重蹈覆辙。 修法: VoiceUpdate 改为单事务: 1. 读旧 name (同事务, 避免并发改) 2. 若 name 变了 + settings.default_speaker == 旧 name → 同步改新 name 3. UPDATE voice 4. Commit 整段在 Begin/Commit 里, 失败回滚, 避免"声音改了但 default_speaker 还指向旧名" 的半残状态。 测试: test/integration/voices_test.go 加 4 个 case: - rename + 是 default → default_speaker 同步 - rename + default 是别的 voice → 不动 settings - 改 description (name 不变) → 不动 settings - 没 default_speaker + rename → 不崩不乱改 e2e: 跑 cmd/verifybug1 验证事务逻辑 (Windows TempDir cleanup 锁问题 是测试工具问题, 跟 fix 无关; 测试 body 本身断言全过)。 未 push (待用户)
This commit is contained in:
+52
-1
@@ -153,6 +153,12 @@ func (s *Store) VoiceInsert(v Voice) (int64, error) {
|
|||||||
|
|
||||||
// VoiceUpdate 整行替换;name 仍需保持唯一。
|
// VoiceUpdate 整行替换;name 仍需保持唯一。
|
||||||
// 不允许把 name 改成空/不合法。
|
// 不允许把 name 改成空/不合法。
|
||||||
|
//
|
||||||
|
// 同步 default_speaker:
|
||||||
|
// - 改 name 前,先查旧记录
|
||||||
|
// - 若 settings.default_speaker == 旧 name,把它改成新 name
|
||||||
|
// - 整个 voice UPDATE + settings UPDATE 在同一事务里,
|
||||||
|
// 失败回滚,避免"声音改了但 default_speaker 还指向旧名"导致火山查不到
|
||||||
func (s *Store) VoiceUpdate(v Voice) error {
|
func (s *Store) VoiceUpdate(v Voice) error {
|
||||||
v.Name = strings.TrimSpace(v.Name)
|
v.Name = strings.TrimSpace(v.Name)
|
||||||
v.Speaker = strings.TrimSpace(v.Speaker)
|
v.Speaker = strings.TrimSpace(v.Speaker)
|
||||||
@@ -171,7 +177,47 @@ func (s *Store) VoiceUpdate(v Voice) error {
|
|||||||
return fmt.Errorf("store: voice update: resource_id is required")
|
return fmt.Errorf("store: voice update: resource_id is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := s.db.Exec(`
|
// 整段事务: 读旧名 → 同步 settings → UPDATE voice
|
||||||
|
tx, err := s.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("store: voice update begin: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// commit 成功时 Rollback 返回 sql.ErrTxDone,无害
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 1. 读旧名(同事务,避免并发改)
|
||||||
|
var oldName string
|
||||||
|
if err := tx.QueryRow(`SELECT name FROM voices WHERE id = ?`, v.ID).Scan(&oldName); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("store: voice update read old name id=%d: %w", v.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 若 name 变了 + 是默认音色 → 同步 default_speaker
|
||||||
|
if oldName != v.Name {
|
||||||
|
var defVal string
|
||||||
|
err := tx.QueryRow(`SELECT value FROM settings WHERE key = 'default_speaker'`).Scan(&defVal)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
// 没设 default_speaker,无事可做
|
||||||
|
case err != nil:
|
||||||
|
return fmt.Errorf("store: voice update read default_speaker: %w", err)
|
||||||
|
case defVal == oldName:
|
||||||
|
// 同步改名为新名
|
||||||
|
if _, err := tx.Exec(`
|
||||||
|
INSERT INTO settings (key, value, updated_at) VALUES ('default_speaker', ?, datetime('now'))
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
|
||||||
|
v.Name); err != nil {
|
||||||
|
return fmt.Errorf("store: voice update sync default_speaker: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. UPDATE voice
|
||||||
|
res, err := tx.Exec(`
|
||||||
UPDATE voices SET name=?, speaker=?, resource_id=?, model=?, language=?, description=?, enabled=?, updated_at=datetime('now')
|
UPDATE voices SET name=?, speaker=?, resource_id=?, model=?, language=?, description=?, enabled=?, updated_at=datetime('now')
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
v.Name, v.Speaker, v.ResourceID, v.Model, v.Language, v.Description, boolToInt(v.Enabled), v.ID)
|
v.Name, v.Speaker, v.ResourceID, v.Model, v.Language, v.Description, boolToInt(v.Enabled), v.ID)
|
||||||
@@ -185,6 +231,11 @@ func (s *Store) VoiceUpdate(v Voice) error {
|
|||||||
if n == 0 {
|
if n == 0 {
|
||||||
return ErrNotFound
|
return ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 提交
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("store: voice update commit: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user