feat: 站点访问统计与可配置侧边栏组件

- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面
- 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件
- 新增 widget 嵌入代码生成(widgetCode)与 widgetApi
- 遥测逻辑迁移至 handler 层,删除 service/telemetry
- env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
2026-09-24 18:53:45 +08:00
parent 3e55b5d230
commit 7b327bc8cc
60 changed files with 6539 additions and 572 deletions

View File

@@ -0,0 +1,62 @@
package middleware
import (
"strings"
"github.com/freefire/jiang13-bbs/model"
"github.com/freefire/jiang13-bbs/service"
"github.com/gin-gonic/gin"
)
// VisitCapture 服务端访问捕获:只记「UA 命中爬虫库」与「响应状态 ≥ 400」两类请求,
// 普通人类浏览由客户端埋点上报(互补、不重复)。事件非阻塞入队,不影响响应。
func VisitCapture(svc *service.VisitStatsService) gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if svc == nil || !svc.Enabled() {
return
}
// 静态资源与健康检查/WS 无统计意义
path := c.Request.URL.Path
if strings.HasPrefix(path, "/uploads") || path == "/health" || path == "/api/ws" {
return
}
ua := c.Request.UserAgent()
bot := service.ClassifyBot(ua)
status := c.Writer.Status()
if bot == "" && status < 400 {
return // 正常人类请求:不记
}
ev := model.VisitEvent{}
if bot != "" {
ev.Kind = model.VisitKindBot
ev.Bot = bot
} else {
ev.Kind = model.VisitKindProbe
ev.Status = status
}
if q := c.Request.URL.RawQuery; q != "" {
ev.Path = path + "?" + q
} else {
ev.Path = path
}
if len(ev.Path) > 512 {
ev.Path = ev.Path[:512]
}
ev.IP = service.CanonicalIP(c.ClientIP())
if vid, err := c.Cookie(service.VisitorCookieName); err == nil && service.ValidVisitorID(vid) {
ev.VidHash = service.HashVisitorID(vid)
}
if u := CurrentUser(c); u != nil {
ev.UserID = u.ID
}
p := service.ParseUserAgent(ua)
ev.Device = p.Kind
ev.OS = p.OS
ev.Browser = p.Browser
svc.Enqueue(ev)
}
}