40760aa884
- 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>
68 lines
2.0 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|