feat: 站点访问统计与可配置侧边栏组件
- 新增访问统计:visitstats 服务与查询、visit_capture 中间件、爬虫/来源识别、管理端 analytics 页面 - 新增侧边栏:sidebar 服务与 handler、管理端 sidebar 配置页、前端侧边栏组件 - 新增 widget 嵌入代码生成(widgetCode)与 widgetApi - 遥测逻辑迁移至 handler 层,删除 service/telemetry - env 示例补充 Docker 部署可信代理 IP 说明
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user