Files
dudu/server/internal/quota/reconcile.go
T
wangjia 40760aa884
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s
dudu MVP:五端语音输入法初始提交
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调
- desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导)
- android:Compose 主 App + IME(键盘内录音直传)
- ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写)
- design/design-pipeline:设计系统 + token 导出 iOS/Android 主题
- doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:38:37 +08:00

68 lines
2.0 KiB
Go

package quota
import (
"context"
"log/slog"
"time"
"dudu/server/internal/store"
)
// InvalidateBalance 删除 Redis 余额键 → 下次读取从 DB 冗余列懒加载(购买入账后调用)。
func (m *Manager) InvalidateBalance(ctx context.Context, uid string) error {
return m.rdb.Del(ctx, store.KeyQuotaBalance(uid)).Err()
}
// Reconcile 对账:users.balance_seconds 必须等于 SUM(balance_ledger.delta_seconds)。
// fix=true 时以 ledger 为准修正冗余列并失效 Redis 键。返回发现的偏差数。
func (m *Manager) Reconcile(ctx context.Context, fix bool) (int, error) {
type row struct {
ID string
Balance int64
Ledger int64
}
var rows []row
err := m.db.WithContext(ctx).Raw(`
SELECT u.id, u.balance_seconds AS balance, COALESCE(SUM(l.delta_seconds), 0) AS ledger
FROM users u LEFT JOIN balance_ledgers l ON l.user_id = u.id
GROUP BY u.id, u.balance_seconds
HAVING u.balance_seconds <> COALESCE(SUM(l.delta_seconds), 0)`).Scan(&rows).Error
if err != nil {
return 0, err
}
for _, r := range rows {
slog.Error("balance drift detected", "user", r.ID, "redundant", r.Balance, "ledger", r.Ledger)
if fix {
if err := m.db.WithContext(ctx).Model(&store.User{}).
Where("id = ?", r.ID).UpdateColumn("balance_seconds", r.Ledger).Error; err != nil {
return len(rows), err
}
_ = m.InvalidateBalance(ctx, r.ID)
}
}
return len(rows), nil
}
// StartDailyReconcile 每日对账任务(凌晨 4 点 CST)。
func (m *Manager) StartDailyReconcile(ctx context.Context) {
go func() {
for {
now := time.Now().In(time.FixedZone("CST", 8*3600))
next := time.Date(now.Year(), now.Month(), now.Day(), 4, 0, 0, 0, now.Location())
if !next.After(now) {
next = next.Add(24 * time.Hour)
}
select {
case <-ctx.Done():
return
case <-time.After(time.Until(next)):
if n, err := m.Reconcile(ctx, true); err != nil {
slog.Error("reconcile failed", "err", err)
} else if n > 0 {
slog.Warn("reconcile fixed drifts", "count", n)
}
}
}
}()
}