- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
167 lines
4.7 KiB
Go
167 lines
4.7 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/freefire/jiang13-bbs/service"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// ============ 访问统计管理端 API(全部 RequirePerm(PermSettings)) ============
|
||
|
||
func statsRange(c *gin.Context) string { return c.DefaultQuery("range", "7d") }
|
||
|
||
// GET /api/admin/stats/overview
|
||
func (h *Handlers) StatsOverview(c *gin.Context) {
|
||
data, err := h.Visit.Overview(statsRange(c))
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取概览失败", "detail": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, data)
|
||
}
|
||
|
||
// GET /api/admin/stats/realtime
|
||
func (h *Handlers) StatsRealtime(c *gin.Context) {
|
||
data, err := h.Visit.Realtime()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取实时数据失败", "detail": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, data)
|
||
}
|
||
|
||
// GET /api/admin/stats/sources
|
||
func (h *Handlers) StatsSources(c *gin.Context) {
|
||
data, err := h.Visit.Sources(statsRange(c))
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取来源分析失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, data)
|
||
}
|
||
|
||
// GET /api/admin/stats/pages?q=&page=&size=
|
||
func (h *Handlers) StatsPages(c *gin.Context) {
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
||
data, err := h.Visit.Pages(statsRange(c), strings.TrimSpace(c.Query("q")), page, size)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取受访页面失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, data)
|
||
}
|
||
|
||
// GET /api/admin/stats/visitors
|
||
func (h *Handlers) StatsVisitors(c *gin.Context) {
|
||
data, err := h.Visit.Visitors(statsRange(c))
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取访客画像失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, data)
|
||
}
|
||
|
||
// GET /api/admin/stats/security
|
||
func (h *Handlers) StatsSecurity(c *gin.Context) {
|
||
data, err := h.Visit.Security(statsRange(c))
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取安全数据失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, data)
|
||
}
|
||
|
||
// GET /api/admin/stats/events?kind=&bot=&path=&ip=&start=&end=&page=&size=
|
||
func (h *Handlers) StatsEvents(c *gin.Context) {
|
||
f := service.VisitEventFilter{
|
||
Kind: strings.TrimSpace(c.Query("kind")),
|
||
Bot: strings.TrimSpace(c.Query("bot")),
|
||
Path: strings.TrimSpace(c.Query("path")),
|
||
IP: strings.TrimSpace(c.Query("ip")),
|
||
}
|
||
if v := c.Query("start"); v != "" {
|
||
if t, err := time.Parse(time.RFC3339, v); err == nil {
|
||
f.Start = t
|
||
}
|
||
}
|
||
if v := c.Query("end"); v != "" {
|
||
if t, err := time.Parse(time.RFC3339, v); err == nil {
|
||
f.End = t
|
||
}
|
||
}
|
||
f.Page, _ = strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
f.Size, _ = strconv.Atoi(c.DefaultQuery("size", "50"))
|
||
data, err := h.Visit.Events(f)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取访问明细失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, data)
|
||
}
|
||
|
||
// GET /api/admin/stats/settings — 开关/保留天数 + 容量信息
|
||
func (h *Handlers) StatsGetSettings(c *gin.Context) {
|
||
data, err := h.Visit.SettingsPayload()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取设置失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, data)
|
||
}
|
||
|
||
// PUT /api/admin/stats/settings
|
||
func (h *Handlers) StatsSaveSettings(c *gin.Context) {
|
||
var body struct {
|
||
Enabled *bool `json:"enabled"`
|
||
RetentionDays *int `json:"retention_days"`
|
||
}
|
||
if err := c.ShouldBindJSON(&body); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"})
|
||
return
|
||
}
|
||
cur, err := h.Visit.SettingsPayload()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取当前设置失败"})
|
||
return
|
||
}
|
||
enabled := cur.Enabled
|
||
if body.Enabled != nil {
|
||
enabled = *body.Enabled
|
||
}
|
||
days := cur.RetentionDays
|
||
if body.RetentionDays != nil {
|
||
days = *body.RetentionDays
|
||
}
|
||
if err := h.Visit.SaveSettings(enabled, days); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "保存失败,保留天数需在 7–365 之间"})
|
||
return
|
||
}
|
||
data, err := h.Visit.SettingsPayload()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取设置失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, data)
|
||
}
|
||
|
||
// POST /api/admin/stats/purge — 立即清理 N 天前明细(异步分批执行,受理即返回)
|
||
func (h *Handlers) StatsPurge(c *gin.Context) {
|
||
var body struct {
|
||
Days int `json:"days"`
|
||
}
|
||
if err := c.ShouldBindJSON(&body); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"})
|
||
return
|
||
}
|
||
if !h.Visit.PurgeBefore(body.Days) {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "天数需在 7–365 之间"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||
}
|