package handler import ( "strconv" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "gorm.io/gorm/clause" "git.sundynix.cn/Blizzard/sundynix-site/server/internal/model" "git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/resp" ) // VisitHandler 访问足迹:用户端埋点 + 管理端统计。 type VisitHandler struct { db *gorm.DB } func NewVisitHandler(db *gorm.DB) *VisitHandler { return &VisitHandler{db: db} } // Track POST /api/track — 用户端埋点上报(免登录)。 // 按 (今天, ClientIP) upsert:已存在则 pv+1,否则插入 pv=1。 func (h *VisitHandler) Track(c *gin.Context) { ip := c.ClientIP() if ip == "" { ip = "unknown" } visit := model.Visit{ Date: time.Now().Format("2006-01-02"), IP: ip, PV: 1, } h.db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "date"}, {Name: "ip"}}, DoUpdates: clause.Assignments(map[string]any{ "pv": gorm.Expr("pv + 1"), "updated_at": time.Now(), }), }).Create(&visit) resp.OK(c, nil) } type dailyRow struct { Date string `json:"date"` UV int64 `json:"uv"` PV int64 `json:"pv"` } // Stats GET /api/admin/visits?days=30 — 近 N 天每日 UV/PV。 func (h *VisitHandler) Stats(c *gin.Context) { days := 30 if v := c.Query("days"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 365 { days = n } } since := time.Now().AddDate(0, 0, -(days - 1)).Format("2006-01-02") today := time.Now().Format("2006-01-02") var rows []dailyRow h.db.Model(&model.Visit{}). Select("date, count(*) as uv, coalesce(sum(pv), 0) as pv"). Where("date >= ?", since). Group("date"). Order("date"). Scan(&rows) var totalUV, totalPV, todayUV, todayPV int64 for _, r := range rows { totalUV += r.UV totalPV += r.PV if r.Date == today { todayUV = r.UV todayPV = r.PV } } resp.OK(c, gin.H{ "daily": rows, "total_uv": totalUV, "total_pv": totalPV, "today_uv": todayUV, "today_pv": todayPV, "days": days, }) }