fix(leonardo): cookie 写回改 CAS,并补上额度对账后的轮换写回

Leonardo 只认加密的 session_data 缓存,轮换值一旦没存住账号就会被判死。写回原先是无条件 update:长任务(生成、慢额度查询)手里的旧 cookie 完成时会盖掉期间 keepalive 存好的新值。改为 SwapValue(WHERE value = 旧值 才写),旧值无法覆盖新值;reconcileLeonardoCredits 原先完全没写回 FetchCreditsBalance 带回的轮换值,补上。
This commit is contained in:
GlossSeaDress
2026-08-10 12:13:11 +08:00
parent b488142495
commit 2c47344ed9
3 changed files with 22 additions and 4 deletions
+17
View File
@@ -87,6 +87,23 @@ func (r *TokenRepository) Update(ctx context.Context, pool, id string, patch map
return r.Get(ctx, pool, id)
}
// SwapValue replaces an account's credential only while the stored one is still
// the value the caller started from. A rotating cookie is minted from whatever
// was in the row, so a goroutine that has been holding an older copy (a long
// render, a slow quota probe) must NOT be allowed to write it back over a newer
// rotation — that older copy no longer authenticates, and the account then looks
// dead. Reports whether the row was updated.
func (r *TokenRepository) SwapValue(ctx context.Context, pool, id, from, to string) (bool, error) {
res := r.db.WithContext(ctx).
Model(&model.TokenAccount{}).
Where("pool = ? AND id = ? AND value = ?", pool, id, from).
Updates(map[string]any{"value": to, "updated_at": time.Now()})
if res.Error != nil {
return false, res.Error
}
return res.RowsAffected > 0, nil
}
// ReserveQuota atomically pre-deducts `amount` from an account's cached image
// token balance under a row lock, so concurrent picks of the same near-empty
// account can never over-commit it. Returns: