package usage import ( "context" "log" "time" "gorm.io/gorm" ) // 记账明细的保留期清理。 // // usage_logs 只增不减:长期运行的实例上,渠道统计这类「按时间窗扫描」的查询会 // 随表增长线性变慢,备份也会越来越大。余额流水(balance_logs)不在清理范围内—— // 那是财务记录,需要长期保留。 // // 默认关闭(retention_days=0):删数据是不可逆操作,由部署者显式开启。 // 清理分批进行(每批 2000 行、批间小睡),避免长时间持锁影响在线请求。 const ( retentionBatch = 2000 retentionTick = 24 * time.Hour ) // StartRetention 周期性删除超过 days 天的用量明细;days<=0 时不启动。 // 返回停止函数(进程退出时调用)。 func StartRetention(ctx context.Context, db *gorm.DB, days int) (stop func()) { if days <= 0 || db == nil { return func() {} } done := make(chan struct{}) go func() { purgeOlderThan(ctx, db, days) t := time.NewTicker(retentionTick) defer t.Stop() for { select { case <-ctx.Done(): return case <-done: return case <-t.C: purgeOlderThan(ctx, db, days) } } }() return func() { select { case <-done: default: close(done) } } } // purgeOlderThan 分批删除 cutoff 之前的明细,返回删除行数。 func purgeOlderThan(ctx context.Context, db *gorm.DB, days int) int64 { cutoff := time.Now().UTC().AddDate(0, 0, -days) var total int64 for { if ctx.Err() != nil { return total } res := db.Exec( "DELETE FROM usage_logs WHERE id IN (SELECT id FROM usage_logs WHERE created_at < ? LIMIT ?)", cutoff, retentionBatch) if res.Error != nil { log.Printf("usage: retention purge failed: %v", res.Error) return total } n := res.RowsAffected total += n if n < retentionBatch { break } select { case <-ctx.Done(): return total case <-time.After(50 * time.Millisecond): } } if total > 0 { log.Printf("usage: retention removed %d rows older than %s", total, cutoff.Format("2006-01-02")) } return total }