package handler import ( "fmt" "time" "github.com/gin-gonic/gin" "nl-game-api-gin/internal/database" "nl-game-api-gin/internal/model" "nl-game-api-gin/internal/room" "nl-game-api-gin/pkg/resp" ) // dayStart 返回某时刻所在自然日的零点时间戳(本地时区) func dayStart(t time.Time) int64 { return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).Unix() } // AdminAnalytics 运营数据分析:日活趋势、游玩时长、平均时长、最火游戏、当前在线 // 日活口径:当天有登录 / 游玩 / 签到 / 对战任一行为的去重用户数 func AdminAnalytics(c *gin.Context) { now := time.Now() todayStart := dayStart(now) const trendDays = 14 since := todayStart - int64(trendDays-1)*86400 // ---- 日活趋势:四张行为表按天去重合并 ---- type dayRow struct { D string `gorm:"column:d"` N int `gorm:"column:n"` } var rows []dayRow database.DB.Raw(`SELECT d, COUNT(DISTINCT uid) AS n FROM ( SELECT user_id AS uid, FROM_UNIXTIME(created_at, '%Y-%m-%d') AS d FROM login_logs WHERE created_at >= ? UNION ALL SELECT user_id, FROM_UNIXTIME(created_at, '%Y-%m-%d') FROM game_records WHERE created_at >= ? UNION ALL SELECT user_id, FROM_UNIXTIME(created_at, '%Y-%m-%d') FROM sign_ins WHERE created_at >= ? UNION ALL SELECT user_id, FROM_UNIXTIME(created_at, '%Y-%m-%d') FROM battle_records WHERE created_at >= ? ) t GROUP BY d ORDER BY d`, since, since, since, since).Scan(&rows) dauMap := map[string]int{} for _, r := range rows { dauMap[r.D] = r.N } // 补齐没有数据的日期(趋势图连续) trend := make([]gin.H, 0, trendDays) for i := 0; i < trendDays; i++ { day := time.Unix(todayStart-int64(trendDays-1-i)*86400, 0) key := day.Format("2006-01-02") trend = append(trend, gin.H{"date": day.Format("01-02"), "full": key, "count": dauMap[key]}) } dauToday := dauMap[now.Format("2006-01-02")] dauYesterday := dauMap[time.Unix(todayStart-86400, 0).Format("2006-01-02")] // ---- 游玩时长与局数(单机 + 联机对战合并) ---- type sumRow struct { Dur int64 `gorm:"column:dur"` Cnt int64 `gorm:"column:cnt"` } sumOf := func(table string, from int64) sumRow { var r sumRow database.DB.Raw(fmt.Sprintf( `SELECT COALESCE(SUM(duration),0) AS dur, COUNT(*) AS cnt FROM %s WHERE created_at >= ?`, table), from).Scan(&r) return r } gToday, gTotal := sumOf("game_records", todayStart), sumOf("game_records", 0) bToday, bTotal := sumOf("battle_records", todayStart), sumOf("battle_records", 0) secToday := gToday.Dur + bToday.Dur secTotal := gTotal.Dur + bTotal.Dur playsToday := gToday.Cnt + bToday.Cnt playsTotal := gTotal.Cnt + bTotal.Cnt avgSec := int64(0) if playsTotal > 0 { avgSec = secTotal / playsTotal } avgSecToday := int64(0) if playsToday > 0 { avgSecToday = secToday / playsToday } // ---- 最火游戏:近 7 天局数排序(单机+对战合并),附平均时长与历史总局数 ---- weekAgo := now.Unix() - 7*86400 type hotRow struct { GameID int `gorm:"column:game_id"` Plays int `gorm:"column:plays"` Dur int64 `gorm:"column:dur"` } var hot []hotRow database.DB.Raw(`SELECT game_id, COUNT(*) AS plays, COALESCE(SUM(duration),0) AS dur FROM ( SELECT game_id, duration FROM game_records WHERE created_at >= ? UNION ALL SELECT game_id, duration FROM battle_records WHERE created_at >= ? ) t GROUP BY game_id ORDER BY plays DESC LIMIT 8`, weekAgo, weekAgo).Scan(&hot) ids := make([]int, 0, len(hot)) for _, h := range hot { ids = append(ids, h.GameID) } gameMap := map[int]model.Game{} if len(ids) > 0 { var games []model.Game database.DB.Where("id IN ?", ids).Find(&games) for _, g := range games { gameMap[g.ID] = g } } hotGames := make([]gin.H, 0, len(hot)) for _, h := range hot { g, ok := gameMap[h.GameID] if !ok { continue } avg := int64(0) if h.Plays > 0 { avg = h.Dur / int64(h.Plays) } hotGames = append(hotGames, gin.H{ "game_id": g.ID, "code": g.Code, "name": g.Name, "icon": g.Icon, "category": g.Category, "plays_7d": h.Plays, "avg_seconds": avg, "play_count": g.PlayCount, }) } resp.OK(c, gin.H{ "dau_today": dauToday, // 今日日活 "dau_yesterday": dauYesterday, // 昨日日活(对比) "dau_trend": trend, // 近 14 天日活趋势 "online_now": room.OnlineCount(), // 当前在线人数(WebSocket 连接数) "play_seconds_today": secToday, // 今日游玩总时长(秒) "play_seconds_total": secTotal, // 累计游玩总时长(秒) "plays_today": playsToday, // 今日总局数 "plays_total": playsTotal, // 累计总局数 "avg_seconds": avgSec, // 平均单局时长(秒,历史) "avg_seconds_today": avgSecToday, // 平均单局时长(秒,今日) "hot_games": hotGames, // 近 7 天最火游戏 Top8 }) }