feat: 站点访问统计与可配置侧边栏组件
- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/middleware"
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
@@ -12,8 +13,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const visitorCookie = "j13_vid"
|
||||
|
||||
func newVisitorID() string {
|
||||
var b [16]byte
|
||||
_, _ = rand.Read(b[:])
|
||||
@@ -27,21 +26,64 @@ func newVisitorID() string {
|
||||
)
|
||||
}
|
||||
|
||||
// POST /api/telemetry/pageview — 轻量 PV/UV 埋点(公开,需 CSRF)
|
||||
// POST /api/telemetry/pageview — 轻量 PV/UV 埋点(公开,需 CSRF + 限流)。
|
||||
// 只组装事件并入队即返回,落库由后台协程攒批完成。
|
||||
func (h *Handlers) TelemetryPageView(c *gin.Context) {
|
||||
vid, _ := c.Cookie(visitorCookie)
|
||||
if !h.Visit.Enabled() {
|
||||
c.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Path string `json:"path"`
|
||||
Ref string `json:"ref"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"})
|
||||
return
|
||||
}
|
||||
path := strings.TrimSpace(body.Path)
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"})
|
||||
return
|
||||
}
|
||||
if len(path) > 512 {
|
||||
path = path[:512]
|
||||
}
|
||||
|
||||
vid, _ := c.Cookie(service.VisitorCookieName)
|
||||
if !service.ValidVisitorID(vid) {
|
||||
vid = newVisitorID()
|
||||
secure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https"
|
||||
// HttpOnly:仅服务端读 UV,前端埋点不依赖可读 cookie
|
||||
c.SetCookie(visitorCookie, vid, 365*24*3600, "/", "", secure, true)
|
||||
c.SetCookie(service.VisitorCookieName, vid, 365*24*3600, "/", "", secure, true)
|
||||
}
|
||||
|
||||
loggedIn := middleware.CurrentUser(c) != nil
|
||||
if err := h.Telemetry.RecordPageView(vid, loggedIn); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "记录失败"})
|
||||
return
|
||||
ua := c.Request.UserAgent()
|
||||
ev := model.VisitEvent{Path: path}
|
||||
if bot := service.ClassifyBot(ua); bot != "" {
|
||||
// UA 命中爬虫:只进明细,不计 PV/UV
|
||||
ev.Kind = model.VisitKindBot
|
||||
ev.Bot = bot
|
||||
} else {
|
||||
ev.Kind = model.VisitKindPageview
|
||||
}
|
||||
host, kind := service.ClassifyReferrer(body.Ref, c.Request.Host)
|
||||
ev.RefHost = host
|
||||
ev.RefKind = kind
|
||||
if service.ValidVisitorID(vid) {
|
||||
ev.VidHash = service.HashVisitorID(vid)
|
||||
}
|
||||
if u := middleware.CurrentUser(c); u != nil {
|
||||
ev.UserID = u.ID
|
||||
}
|
||||
ev.IP = service.CanonicalIP(c.ClientIP())
|
||||
p := service.ParseUserAgent(ua)
|
||||
ev.Device = p.Kind
|
||||
ev.OS = p.OS
|
||||
ev.Browser = p.Browser
|
||||
h.Visit.Enqueue(ev)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user