2026-03-15 15:57:09 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"soul-api/internal/database"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// AdminTrackStats GET /api/admin/track/stats 管理端-按钮/标签点击统计(按模块+action聚合)
|
|
|
|
|
|
func AdminTrackStats(c *gin.Context) {
|
|
|
|
|
|
period := c.DefaultQuery("period", "today")
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
var since time.Time
|
|
|
|
|
|
switch period {
|
|
|
|
|
|
case "today":
|
|
|
|
|
|
since = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
|
|
|
|
case "week":
|
|
|
|
|
|
since = now.AddDate(0, 0, -7)
|
|
|
|
|
|
case "month":
|
|
|
|
|
|
since = now.AddDate(0, -1, 0)
|
|
|
|
|
|
default:
|
|
|
|
|
|
since = time.Time{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 15:25:26 +08:00
|
|
|
|
type rawRow struct {
|
2026-03-15 15:57:09 +08:00
|
|
|
|
Action string `gorm:"column:action"`
|
|
|
|
|
|
Target string `gorm:"column:target"`
|
|
|
|
|
|
ExtraData []byte `gorm:"column:extra_data"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 15:25:26 +08:00
|
|
|
|
query := db.Table("user_tracks").Select("action, COALESCE(target, '') as target, extra_data")
|
2026-03-15 15:57:09 +08:00
|
|
|
|
if !since.IsZero() {
|
|
|
|
|
|
query = query.Where("created_at >= ?", since)
|
|
|
|
|
|
}
|
2026-03-17 15:25:26 +08:00
|
|
|
|
query = query.Where("action NOT LIKE '%union%' AND action NOT LIKE '%jndi%' AND action NOT LIKE '%SLEEP%'")
|
2026-03-15 15:57:09 +08:00
|
|
|
|
|
2026-03-17 15:25:26 +08:00
|
|
|
|
var rawRows []rawRow
|
|
|
|
|
|
query.Find(&rawRows)
|
2026-03-15 15:57:09 +08:00
|
|
|
|
|
2026-03-17 15:25:26 +08:00
|
|
|
|
type statKey struct {
|
|
|
|
|
|
Module string
|
|
|
|
|
|
Action string
|
|
|
|
|
|
Target string
|
|
|
|
|
|
}
|
2026-03-15 15:57:09 +08:00
|
|
|
|
type statItem struct {
|
|
|
|
|
|
Action string `json:"action"`
|
|
|
|
|
|
Target string `json:"target"`
|
|
|
|
|
|
Module string `json:"module"`
|
|
|
|
|
|
Page string `json:"page"`
|
|
|
|
|
|
Count int64 `json:"count"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 15:25:26 +08:00
|
|
|
|
aggregated := make(map[statKey]*statItem)
|
2026-03-15 15:57:09 +08:00
|
|
|
|
total := int64(0)
|
|
|
|
|
|
|
2026-03-17 15:25:26 +08:00
|
|
|
|
for _, r := range rawRows {
|
2026-03-15 15:57:09 +08:00
|
|
|
|
module := "other"
|
|
|
|
|
|
page := ""
|
|
|
|
|
|
if len(r.ExtraData) > 0 {
|
|
|
|
|
|
var extra map[string]interface{}
|
|
|
|
|
|
if json.Unmarshal(r.ExtraData, &extra) == nil {
|
|
|
|
|
|
if m, ok := extra["module"].(string); ok && m != "" {
|
|
|
|
|
|
module = m
|
|
|
|
|
|
}
|
|
|
|
|
|
if p, ok := extra["page"].(string); ok && p != "" {
|
|
|
|
|
|
page = p
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 15:25:26 +08:00
|
|
|
|
key := statKey{Module: module, Action: r.Action, Target: r.Target}
|
|
|
|
|
|
if existing, ok := aggregated[key]; ok {
|
|
|
|
|
|
existing.Count++
|
|
|
|
|
|
} else {
|
|
|
|
|
|
aggregated[key] = &statItem{
|
|
|
|
|
|
Action: r.Action,
|
|
|
|
|
|
Target: r.Target,
|
|
|
|
|
|
Module: module,
|
|
|
|
|
|
Page: page,
|
|
|
|
|
|
Count: 1,
|
|
|
|
|
|
}
|
2026-03-15 15:57:09 +08:00
|
|
|
|
}
|
2026-03-17 15:25:26 +08:00
|
|
|
|
total++
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
byModule := make(map[string][]statItem)
|
|
|
|
|
|
for _, item := range aggregated {
|
|
|
|
|
|
byModule[item.Module] = append(byModule[item.Module], *item)
|
2026-03-15 15:57:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"period": period,
|
|
|
|
|
|
"total": total,
|
|
|
|
|
|
"byModule": byModule,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|