Files
nl-game-api/internal/handler/analytics.go
2026-08-14 13:17:03 +08:00

130 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
})
}