130 lines
4.9 KiB
Go
130 lines
4.9 KiB
Go
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
|
||
})
|
||
}
|