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.
 
 
 
 
 
 

278 lines
8.7 KiB

package api
import (
"yunyan/comm"
"yunyan/lego/sys/log"
"yunyan/pb"
"sort"
"strings"
"time"
)
// RebuildStats 全量重建 App 综合统计(仅超管)
//
// 清空 app_stat_daily/monthly/yearly/global 后,从以下源表全量重算:
// - payorder:order_count(create_time)、failed_order_count(create_time, status=FAILED)、
// pay_amount/pay_count/pay_user_count(pay_time, status=PAID)
// - user:new_user_count(createtime)
// - useruselog:add_vip_days/add_ai_integral/add_trade_second/add_meet_second(ts)
// - userstatistics(仅 global 行):ai/trade/meet 累计消耗
// - userdevice(仅 global 行):bind_device_count
//
// 无法重建的字段(无历史时间戳/审计)将留 0:
//
// login_count、active_user_count、active_device_count;
// ai_chat/trade/meet 消耗的日/月/年分布(只能汇总到 global 行)。
//
// @Summary 重建统计数据
// @Description 清空并从源表重算 App 综合统计(仅超管)
// @Tags API
// @Accept json
// @Produce json
// @Param user body pb.ApiRebuildStatsReq true "重建请求"
// @Success 200 {object} comm.HttpResult{data=pb.ApiRebuildStatsResp} "成功"
// @Router /web/api/api_rebuildstats [post]
func (this *apiComp) RebuildStats(session comm.IUserSession, req *pb.ApiRebuildStatsReq) (resp *pb.ApiRebuildStatsResp, errdata *pb.ErrorData) {
start := time.Now()
resp = &pb.ApiRebuildStatsResp{}
// 0. 先从消耗日志重建 userstatistics(排行榜 + 后续 global 汇总均依赖此表)
uRows, uErr := this.module.model.rebuildUserStatisticsFromLogs()
if uErr != nil {
errdata = dbErr(uErr, "rebuildUserStatisticsFromLogs")
return
}
this.module.Info("RebuildStats: userstatistics rebuilt from logs",
log.Field{Key: "rows", Value: uRows},
)
// 1. 各源表分组聚合(DB 端 GROUP BY,避免拉全表到 Go)
orderCreate, err := this.module.model.aggOrderCreate()
if err != nil {
errdata = dbErr(err, "aggOrderCreate")
return
}
orderFailed, err := this.module.model.aggOrderFailed()
if err != nil {
errdata = dbErr(err, "aggOrderFailed")
return
}
orderPaid, err := this.module.model.aggOrderPaid()
if err != nil {
errdata = dbErr(err, "aggOrderPaid")
return
}
userSignup, err := this.module.model.aggUserSignup()
if err != nil {
errdata = dbErr(err, "aggUserSignup")
return
}
useLog, err := this.module.model.aggUseLogGrant()
if err != nil {
errdata = dbErr(err, "aggUseLogGrant")
return
}
// 2. 合并到 daily map(key=YYYY-MM-DD)
daily := make(map[string]*pb.DBAppStat)
getOrInit := func(d string) *pb.DBAppStat {
row, ok := daily[d]
if !ok {
row = &pb.DBAppStat{StatDate: d}
daily[d] = row
}
return row
}
for _, r := range orderCreate {
getOrInit(r.D).OrderCount += int32(r.Count)
}
for _, r := range orderFailed {
getOrInit(r.D).FailedOrderCount += int32(r.Count)
}
for _, r := range orderPaid {
row := getOrInit(r.D)
row.PayAmount += r.Amount
row.PayCount += int32(r.Count)
row.PayUserCount += int32(r.UserCount)
}
for _, r := range userSignup {
getOrInit(r.D).NewUserCount += int32(r.Count)
}
for _, r := range useLog {
row := getOrInit(r.D)
row.AddVipDays += r.AddVipDay
row.AddAiIntegral += r.AddAgentInt
row.AddTradeSecond += r.AddTradeSec
row.AddMeetSecond += r.AddMeetSec
}
// 3. 按日期排序,并累计 total_user_count 快照
dailyRows := make([]*pb.DBAppStat, 0, len(daily))
for _, r := range daily {
dailyRows = append(dailyRows, r)
}
sort.Slice(dailyRows, func(i, j int) bool { return dailyRows[i].StatDate < dailyRows[j].StatDate })
var cumUser int64
now := time.Now().Unix()
for _, r := range dailyRows {
cumUser += int64(r.NewUserCount)
r.TotalUserCount = cumUser
r.UpdateTime = now
}
// 4. 月 / 年汇总(基于已修正 total_user_count 的日行;快照字段取该期末值)
monthlyRows := rollupByPrefix(dailyRows, 7) // YYYY-MM
yearlyRows := rollupByPrefix(dailyRows, 4) // YYYY
// 5. global 行:日聚合无法覆盖的字段从生命周期 SQL 单独取
gPayAmount, gPayCount, gPayUserCount, gOrderCount, gFailedCount, err := this.module.model.sumLifetimePayments()
if err != nil {
errdata = dbErr(err, "sumLifetimePayments")
return
}
gVipDay, gAiInt, gTradeSec, gMeetSec, err2 := this.module.model.sumLifetimeUseLog()
if err2 != nil {
errdata = dbErr(err2, "sumLifetimeUseLog")
return
}
gAiChat, gAiUp, gAiDown, gTradeCount, gTradeTime, gTradeWords, gMeetCount, gMeetTime, err3 := this.module.model.sumLifetimeUserStats()
if err3 != nil {
errdata = dbErr(err3, "sumLifetimeUserStats")
return
}
gTotalUsers, err := this.module.model.countTotalUsers()
if err != nil {
errdata = dbErr(err, "countTotalUsers")
return
}
gDevices, err := this.module.model.countUserDevices()
if err != nil {
errdata = dbErr(err, "countUserDevices")
return
}
gVipUsers, err := this.module.model.countActiveVipUsers(now)
if err != nil {
errdata = dbErr(err, "countActiveVipUsers")
return
}
global := &pb.DBAppStat{
StatDate: GlobalStatDate,
PayAmount: gPayAmount,
OrderCount: int32(gOrderCount),
PayCount: int32(gPayCount),
FailedOrderCount: int32(gFailedCount),
PayUserCount: int32(gPayUserCount),
AddVipDays: gVipDay,
AddAiIntegral: gAiInt,
AddTradeSecond: gTradeSec,
AddMeetSecond: gMeetSec,
AiChatCount: gAiChat,
AiUpToken: gAiUp,
AiDownToken: gAiDown,
TradeCount: gTradeCount,
TradeTime: gTradeTime,
TradeWords: gTradeWords,
MeetCount: gMeetCount,
MeetTime: gMeetTime,
NewUserCount: int32(gTotalUsers),
TotalUserCount: gTotalUsers,
VipUserCount: int32(gVipUsers),
BindDeviceCount: int32(gDevices),
UpdateTime: now,
}
// 6. 原子改写:在 stat consumer 锁内 TRUNCATE + INSERT + ReloadCache,
// 避免消费者拿着旧的内存缓存覆盖刚重建的数据。
werr := this.module.statConsumer.WithLock(func() error {
if e := this.module.model.truncateStatTables(); e != nil {
return e
}
if e := this.module.model.insertStatRows(comm.TableAppStatDaily, dailyRows); e != nil {
return e
}
if e := this.module.model.insertStatRows(comm.TableAppStatMonthly, monthlyRows); e != nil {
return e
}
if e := this.module.model.insertStatRows(comm.TableAppStatYearly, yearlyRows); e != nil {
return e
}
if e := this.module.model.insertStatRows(comm.TableAppStatGlobal, []*pb.DBAppStat{global}); e != nil {
return e
}
// 仍在锁内:直接刷新 consumer 内存缓存
this.module.statConsumer.loadCache(time.Now())
return nil
})
if werr != nil {
errdata = dbErr(werr, "rebuild atomic")
return
}
// 7. 重建产品激活/绑定统计:全量扫描 userdevice 落库 product_stat,
// 供代理仪表盘(api_getagentdashboard)按产品读取激活数 / 绑定用户数。
psRows, psErr := this.module.model.rebuildProductStats()
if psErr != nil {
errdata = dbErr(psErr, "rebuildProductStats")
return
}
this.module.Info("RebuildStats: product_stat rebuilt",
log.Field{Key: "rows", Value: psRows},
)
resp.DailyRows = int32(len(dailyRows))
resp.MonthlyRows = int32(len(monthlyRows))
resp.YearlyRows = int32(len(yearlyRows))
resp.CostMs = time.Since(start).Milliseconds()
this.module.Info("RebuildStats done",
log.Field{Key: "daily", Value: resp.DailyRows},
log.Field{Key: "monthly", Value: resp.MonthlyRows},
log.Field{Key: "yearly", Value: resp.YearlyRows},
log.Field{Key: "cost_ms", Value: resp.CostMs},
)
return
}
// rollupByPrefix 把 daily 行按日期前缀 (7=YYYY-MM, 4=YYYY) 聚合成月/年行。
// 快照字段 total_user_count 取该期内最大值(= 期末累计用户数)。
func rollupByPrefix(daily []*pb.DBAppStat, prefixLen int) []*pb.DBAppStat {
bucket := make(map[string]*pb.DBAppStat)
for _, d := range daily {
if len(d.StatDate) < prefixLen {
continue
}
key := d.StatDate[:prefixLen]
row, ok := bucket[key]
if !ok {
row = &pb.DBAppStat{StatDate: key, UpdateTime: d.UpdateTime}
bucket[key] = row
}
row.PayAmount += d.PayAmount
row.OrderCount += d.OrderCount
row.PayCount += d.PayCount
row.FailedOrderCount += d.FailedOrderCount
row.PayUserCount += d.PayUserCount
row.AddVipDays += d.AddVipDays
row.AddAiIntegral += d.AddAiIntegral
row.AddTradeSecond += d.AddTradeSecond
row.AddMeetSecond += d.AddMeetSecond
row.NewUserCount += d.NewUserCount
if d.TotalUserCount > row.TotalUserCount {
row.TotalUserCount = d.TotalUserCount
}
}
out := make([]*pb.DBAppStat, 0, len(bucket))
for _, r := range bucket {
out = append(out, r)
}
sort.Slice(out, func(i, j int) bool { return out[i].StatDate < out[j].StatDate })
return out
}
func dbErr(err error, where string) *pb.ErrorData {
return &pb.ErrorData{
Code: pb.ErrorCode_DBError,
Message: where + ": " + strings.TrimSpace(err.Error()),
}
}