feat(admin): 独立后台壳、站点分析与聊天监管,公开站拆出主题切换与访问埋点

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 04:00:14 +08:00
parent cda5fad2e5
commit 9a55c2f6f2
41 changed files with 5655 additions and 1810 deletions

View File

@@ -28,6 +28,8 @@ type Handlers struct {
AdminUser *service.AdminUserService
Moderation *service.ModerationService
Chat *service.ChatService
Telemetry *service.TelemetryService
Analytics *service.AnalyticsService
}
// resolvePublishStatus 决定新帖/新评的初始状态:

View File

@@ -34,6 +34,7 @@ type updateSettingsRequest struct {
AnimCodeFold *bool `json:"anim_code_fold"`
AnimSmoothScroll *bool `json:"anim_smooth_scroll"`
AnimChrome *bool `json:"anim_chrome"`
PostLinkNewTab *bool `json:"post_link_new_tab"`
}
func settingsPayload(saved service.PublicSiteSettings) gin.H {
@@ -51,6 +52,7 @@ func settingsPayload(saved service.PublicSiteSettings) gin.H {
"anim_code_fold": saved.AnimCodeFold,
"anim_smooth_scroll": saved.AnimSmoothScroll,
"anim_chrome": saved.AnimChrome,
"post_link_new_tab": saved.PostLinkNewTab,
}
}
@@ -65,7 +67,7 @@ func (h *Handlers) UpdateSettings(c *gin.Context) {
req.SiteDescription == nil && req.AllowRegister == nil && req.AllowComments == nil &&
req.PostCooldownHours == nil && req.CodeBlockAutoFold == nil && req.CodeBlockFoldLines == nil &&
req.UIAnimations == nil && req.AnimCodeFold == nil && req.AnimSmoothScroll == nil &&
req.AnimChrome == nil {
req.AnimChrome == nil && req.PostLinkNewTab == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "未提供任何可更新的设置项"})
return
}
@@ -168,6 +170,12 @@ func (h *Handlers) UpdateSettings(c *gin.Context) {
return
}
}
if req.PostLinkNewTab != nil {
if err := h.Setting.SetPostLinkNewTab(*req.PostLinkNewTab); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存站点设置失败"})
return
}
}
saved, err := h.Setting.Public()
if err != nil {

View File

@@ -0,0 +1,56 @@
package handler
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"github.com/freefire/jiang13-bbs/middleware"
"github.com/freefire/jiang13-bbs/service"
"github.com/gin-gonic/gin"
)
const visitorCookie = "j13_vid"
func newVisitorID() string {
var b [16]byte
_, _ = rand.Read(b[:])
// 伪 UUID 形态,满足 ValidVisitorID
return fmt.Sprintf("%s-%s-%s-%s-%s",
hex.EncodeToString(b[0:4]),
hex.EncodeToString(b[4:6]),
hex.EncodeToString(b[6:8]),
hex.EncodeToString(b[8:10]),
hex.EncodeToString(b[10:16]),
)
}
// POST /api/telemetry/pageview — 轻量 PV/UV 埋点(公开,需 CSRF)
func (h *Handlers) TelemetryPageView(c *gin.Context) {
vid, _ := c.Cookie(visitorCookie)
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)
}
loggedIn := middleware.CurrentUser(c) != nil
if err := h.Telemetry.RecordPageView(vid, loggedIn); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "记录失败"})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// GET /api/admin/analytics/overview?range=7d|30d
func (h *Handlers) AdminAnalyticsOverview(c *gin.Context) {
rangeQ := c.DefaultQuery("range", "7d")
data, err := h.Analytics.Overview(rangeQ)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取分析数据失败"})
return
}
c.JSON(http.StatusOK, data)
}