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

@@ -2,6 +2,7 @@ package router
import (
"context"
"log"
"os"
"path/filepath"
"strings"
@@ -29,6 +30,9 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
var proxies []string
if v := os.Getenv("TRUSTED_PROXIES"); v != "" {
proxies = strings.Split(v, ",")
} else {
// 未配置信任代理时 ClientIP 退化为 TCP 直连对端:反代/容器部署会记录内网地址
log.Println("警告: TRUSTED_PROXIES 未配置,ClientIP 将记录反代/容器内网地址(如 172.19.0.4),请设置为实际反代网段,如 172.16.0.0/12")
}
if err := r.SetTrustedProxies(proxies); err != nil {
return nil, err
@@ -73,7 +77,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
legacyImportSvc := service.NewLegacyImportService(model.DB, filepath.Join(cfg.DataDir, "uploads"))
moderationSvc := service.NewModerationService(model.DB, notifSvc)
chatSvc := service.NewChatService(model.DB, notifSvc)
telemetrySvc := service.NewTelemetryService(model.DB)
visitSvc := service.NewVisitStatsService(model.DB, settingSvc)
analyticsSvc := service.NewAnalyticsService(model.DB)
adSvc := service.NewAdService(model.DB, service.NewCaptchaStore())
overviewSvc.WithAds(adSvc)
@@ -85,6 +89,12 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
}
limiter := service.DefaultRateLimiter()
// 访问统计管道:攒批落库 + 保留期滚动清理(随进程生命周期)
pipeCtx, pipeCancel := context.WithCancel(context.Background())
defer pipeCancel()
go visitSvc.StartFlusher(pipeCtx)
go visitSvc.RunRetention(pipeCtx)
h := &handler.Handlers{
Ops: ops,
Cfg: cfg,
@@ -107,10 +117,11 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
LegacyImport: legacyImportSvc,
Moderation: moderationSvc,
Chat: chatSvc,
Telemetry: telemetrySvc,
Visit: visitSvc,
Analytics: analyticsSvc,
HidePwd: service.NewHidePasswordCookie(cfg.JWTSecret, !cfg.DevMode),
Ads: adSvc,
Sidebar: service.NewSidebarService(model.DB),
}
// 通知落库后统一推 WS 红点(点赞/评论/审核/@ 等共用)
notifSvc.OnNotifyNew = func(userID uint) {
@@ -127,6 +138,10 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
authMW := middleware.NewAuthMiddleware(authSvc)
r.Use(authMW.OptionalAuth(), h.RuntimeGuard, h.BusinessQuota)
// 访问捕获:仅记爬虫 UA 与 ≥400 响应(人类浏览走客户端埋点);
// 挂在 OptionalAuth 之后可顺带拿到登录用户
r.Use(middleware.VisitCapture(visitSvc))
// 上传文件静态服务(data/uploads → /uploads)
r.Static("/uploads", filepath.Join(cfg.DataDir, "uploads"))
@@ -143,6 +158,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
pubAPI.GET("/boards", h.Boards)
pubAPI.GET("/boards/:id/sidebar", h.BoardSidebar)
pubAPI.GET("/overview", h.Overview)
pubAPI.GET("/sidebar", h.GetSidebar)
pubAPI.GET("/posts", h.Posts)
pubAPI.GET("/posts/:id", h.PostDetail)
pubAPI.POST("/posts/:id/unlock-password", middleware.CSRFMiddleware(), middleware.RateLimitMiddleware(limiter, service.RateHidePassword), h.UnlockPostPassword)
@@ -159,7 +175,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
pubAPI.GET("/media/:object", h.PublicObject)
pubAPI.POST("/auth/code", middleware.CSRFMiddleware(), h.SendEmailCode)
pubAPI.POST("/auth/reset-password", middleware.CSRFMiddleware(), h.ResetPassword)
pubAPI.POST("/telemetry/pageview", middleware.CSRFMiddleware(), h.TelemetryPageView)
pubAPI.POST("/telemetry/pageview", middleware.CSRFMiddleware(), middleware.RateLimitMiddleware(limiter, service.RateTelemetry), h.TelemetryPageView)
pubAPI.POST("/register", middleware.CSRFMiddleware(), middleware.RateLimitMiddleware(limiter, service.RateRegister), h.Register)
pubAPI.POST("/login", middleware.CSRFMiddleware(), middleware.RateLimitMiddleware(limiter, service.RateLogin), h.Login)
// refresh token 端点:access 过期后用 refresh 换新 token(需 CSRF 防护)
@@ -277,6 +293,21 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
// 站点分析概览(任意 staff;板管按授权板过滤)
staffAPI.GET("/analytics/overview", h.AdminAnalyticsOverview)
// 访问统计(仅超管/站长):8 个 Tab 的数据源
statsAPI := staffAPI.Group("/stats", authMW.RequirePerm(service.PermSettings))
{
statsAPI.GET("/overview", h.StatsOverview)
statsAPI.GET("/realtime", h.StatsRealtime)
statsAPI.GET("/sources", h.StatsSources)
statsAPI.GET("/pages", h.StatsPages)
statsAPI.GET("/visitors", h.StatsVisitors)
statsAPI.GET("/security", h.StatsSecurity)
statsAPI.GET("/events", h.StatsEvents)
statsAPI.GET("/settings", h.StatsGetSettings)
statsAPI.PUT("/settings", h.StatsSaveSettings)
statsAPI.POST("/purge", h.StatsPurge)
}
// 群聊监管:移出后挂到 RequireAuth 组,允许「仅群管/消息 flag」用户访问
// (见下方 msgAPI)
@@ -305,6 +336,13 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
adsAPI.GET("/sponsors/config", h.AdminGetSponsorsConfig)
adsAPI.PUT("/sponsors/config", h.AdminSaveSponsorsConfig)
// 侧栏管理(超级管理员/站长):布局配置 + 自定义 HTML 工具
staffAPI.GET("/sidebar", authMW.RequirePerm(service.PermSettings), h.AdminGetSidebar)
staffAPI.PUT("/sidebar/config", authMW.RequirePerm(service.PermSettings), h.AdminSaveSidebarConfig)
staffAPI.POST("/sidebar/widgets", authMW.RequirePerm(service.PermSettings), h.AdminCreateSidebarWidget)
staffAPI.PUT("/sidebar/widgets/:id", authMW.RequirePerm(service.PermSettings), h.AdminUpdateSidebarWidget)
staffAPI.DELETE("/sidebar/widgets/:id", authMW.RequirePerm(service.PermSettings), h.AdminDeleteSidebarWidget)
// 站点设置(超级管理员/站长)
staffAPI.GET("/settings", authMW.RequirePerm(service.PermSettings), h.AdminGetSettings)
opsAPI := staffAPI.Group("/settings/modules", authMW.RequirePerm(service.PermSettings))