You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

193 lines
6.0 KiB

package api
import (
"context"
"yunyan/comm"
"yunyan/lego/core"
"yunyan/lego/core/cbase"
"yunyan/lego/sys/log"
"yunyan/lego/sys/mysql"
redissys "yunyan/lego/sys/redis"
"yunyan/pb"
"yunyan/sys/appstat"
"encoding/json"
"sync"
"time"
)
// statConsumerComp 消费 Redis 统计队列并写入日/月/年三张 DB 表。
// 内存中缓存当前日/月/年三行数据,启动时从 DB 加载一次;
// 每次 flush 在内存累加后立即写入 DB,日期切换时加载新行。
type statConsumerComp struct {
cbase.ModuleCompBase
module *API
cancel context.CancelFunc
// 内存缓存(三个时间粒度 + 全局累计)
// mu 保护以下四个缓存指针的读写,避免 consumer flush 与外部全量重建并发改写。
mu sync.Mutex
daily *pb.DBAppStat
monthly *pb.DBAppStat
yearly *pb.DBAppStat
global *pb.DBAppStat
}
// 全局累计行固定 stat_date 标识
const GlobalStatDate = "all"
func (this *statConsumerComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) {
this.ModuleCompBase.Init(service, module, comp, opt)
this.module = module.(*API)
return
}
func (this *statConsumerComp) Start() (err error) {
if err = this.ModuleCompBase.Start(); err != nil {
return
}
// 启动时从 DB 加载当前日/月/年缓存行
this.loadCache(time.Now())
ctx, cancel := context.WithCancel(context.Background())
this.cancel = cancel
go this.consume(ctx)
log.Infoln("appstat consumer started")
return
}
func (this *statConsumerComp) Destroy() (err error) {
if this.cancel != nil {
this.cancel()
}
return this.ModuleCompBase.Destroy()
}
// loadCache 从 DB 加载指定时间点对应的日/月/年/全局行(不存在则初始化空行)
func (this *statConsumerComp) loadCache(t time.Time) {
this.daily = this.loadOrNew(comm.TableAppStatDaily, t.Format("2006-01-02"))
this.monthly = this.loadOrNew(comm.TableAppStatMonthly, t.Format("2006-01"))
this.yearly = this.loadOrNew(comm.TableAppStatYearly, t.Format("2006"))
this.global = this.loadOrNew(comm.TableAppStatGlobal, GlobalStatDate)
}
func (this *statConsumerComp) loadOrNew(tbl, date string) *pb.DBAppStat {
var row pb.DBAppStat
if err := mysql.FindOne(tbl, &row, "stat_date=?", date); err != nil {
row = pb.DBAppStat{StatDate: date}
}
return &row
}
// consume 从 Redis 队列消费统计增量
func (this *statConsumerComp) consume(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
}
vals, err := redissys.Conn().BRPop(ctx, 5*time.Second, redissys.RKey(appstat.RedisQueueKey)).Result()
if err != nil {
continue
}
if len(vals) < 2 {
continue
}
var delta pb.DBAppStat
if err = json.Unmarshal([]byte(vals[1]), &delta); err != nil {
this.module.Warn("stat consumer unmarshal", log.Field{Key: "err", Value: err.Error()})
continue
}
this.flush(&delta)
}
}
// checkExpired 各粒度独立检查是否过期,过期则创建新的空统计对象;全局行不过期
func (this *statConsumerComp) checkExpired(now time.Time) {
if today := now.Format("2006-01-02"); this.daily == nil || this.daily.StatDate != today {
this.daily = &pb.DBAppStat{StatDate: today}
}
if month := now.Format("2006-01"); this.monthly == nil || this.monthly.StatDate != month {
this.monthly = &pb.DBAppStat{StatDate: month}
}
if year := now.Format("2006"); this.yearly == nil || this.yearly.StatDate != year {
this.yearly = &pb.DBAppStat{StatDate: year}
}
if this.global == nil {
this.global = this.loadOrNew(comm.TableAppStatGlobal, GlobalStatDate)
}
}
// flush 将 delta 累加到内存缓存后立即写入 DB
func (this *statConsumerComp) flush(delta *pb.DBAppStat) {
if len(delta.StatDate) < 10 {
this.module.Warn("stat consumer flush: invalid StatDate", log.Field{Key: "date", Value: delta.StatDate})
return
}
this.mu.Lock()
defer this.mu.Unlock()
this.checkExpired(time.Now())
accum(this.daily, delta)
accum(this.monthly, delta)
accum(this.yearly, delta)
accum(this.global, delta)
this.saveRow(comm.TableAppStatDaily, this.daily)
this.saveRow(comm.TableAppStatMonthly, this.monthly)
this.saveRow(comm.TableAppStatYearly, this.yearly)
this.saveRow(comm.TableAppStatGlobal, this.global)
}
// ReloadCache 由外部(如全量重建)在改写 DB 后调用,丢弃过期内存缓存重新从 DB 加载。
func (this *statConsumerComp) ReloadCache() {
this.mu.Lock()
defer this.mu.Unlock()
this.loadCache(time.Now())
}
// WithLock 提供给外部对四张统计表执行原子改写(清空+重建)使用,
// 期间 consumer flush 被阻塞,避免被半成品状态覆盖。
func (this *statConsumerComp) WithLock(fn func() error) error {
this.mu.Lock()
defer this.mu.Unlock()
return fn()
}
func (this *statConsumerComp) saveRow(tbl string, row *pb.DBAppStat) {
if row == nil {
return
}
row.UpdateTime = time.Now().Unix()
if err := mysql.Save(tbl, row); err != nil {
this.module.Warn("stat consumer save", log.Field{Key: "table", Value: tbl}, log.Field{Key: "err", Value: err.Error()})
}
}
// accum 将 delta 各字段累加到 row 上
func accum(row, delta *pb.DBAppStat) {
row.PayAmount += delta.PayAmount
row.OrderCount += delta.OrderCount
row.PayCount += delta.PayCount
row.FailedOrderCount += delta.FailedOrderCount
row.PayUserCount += delta.PayUserCount
row.AddVipDays += delta.AddVipDays
row.AddAiIntegral += delta.AddAiIntegral
row.AddTradeSecond += delta.AddTradeSecond
row.AddMeetSecond += delta.AddMeetSecond
row.AiChatCount += delta.AiChatCount
row.AiUpToken += delta.AiUpToken
row.AiDownToken += delta.AiDownToken
row.TradeCount += delta.TradeCount
row.TradeTime += delta.TradeTime
row.TradeWords += delta.TradeWords
row.MeetCount += delta.MeetCount
row.MeetTime += delta.MeetTime
row.NewUserCount += delta.NewUserCount
row.LoginCount += delta.LoginCount
row.ActiveUserCount += delta.ActiveUserCount
row.TotalUserCount = delta.TotalUserCount // 快照值,直接覆盖
row.VipUserCount += delta.VipUserCount
row.BindDeviceCount += delta.BindDeviceCount
row.ActiveDeviceCount += delta.ActiveDeviceCount
}