feat: 站点媒体库与移动端底部导航,服务测试补强

- 新增媒体库:media_library/media_thumbs 服务(WebP 缩略图)、管理端 media 页面
- 移动端底部导航 MobileTabBar 替换 MobilePostBar
- 旧数据导入增强与测试、路由与登录会话/板块侧边栏测试补强
- site-doc 组件精简(移除 Breadcrumb),文档新增迁移公告说明
This commit is contained in:
2026-09-27 02:35:38 +08:00
parent 734afbc39f
commit 1d862a0dd3
61 changed files with 4757 additions and 1129 deletions

View File

@@ -3,6 +3,7 @@ package router
import (
"context"
"log"
"net"
"os"
"path/filepath"
"strings"
@@ -18,6 +19,30 @@ import (
"github.com/gin-gonic/gin"
)
// parseTrustedProxies 解析 TRUSTED_PROXIES(逗号分隔的 IP/CIDR/localhost)。
// 容忍条目两侧空白与空项,无法解析的条目跳过并告警:编排环境变量多一个空格
// 不应让整个服务拒绝启动(真实事故:尾随空格致 invalid CIDR 秒退)。
// 全部非法或未配置时返回 nil,退化为「ClientIP 记 TCP 对端」并触发启动警告。
func parseTrustedProxies(v string) []string {
var out []string
for _, part := range strings.Split(v, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
if part == "localhost" {
out = append(out, part)
continue
}
if _, _, err := net.ParseCIDR(part); err != nil && net.ParseIP(part) == nil {
log.Printf("警告: TRUSTED_PROXIES 条目 %q 无法解析,已忽略", part)
continue
}
out = append(out, part)
}
return out
}
// Setup 初始化路由
func Setup(cfg *config.Config) (*gin.Engine, error) {
if cfg.DevMode {
@@ -27,12 +52,10 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
}
r := gin.New()
var proxies []string
if v := os.Getenv("TRUSTED_PROXIES"); v != "" {
proxies = strings.Split(v, ",")
} else {
proxies := parseTrustedProxies(os.Getenv("TRUSTED_PROXIES"))
if len(proxies) == 0 {
// 未配置信任代理时 ClientIP 退化为 TCP 直连对端:反代/容器部署会记录内网地址
log.Println("警告: TRUSTED_PROXIES 未配置,ClientIP 将记录反代/容器内网地址(如 172.19.0.4),请设置为实际反代网段,如 172.16.0.0/12")
log.Println("警告: TRUSTED_PROXIES 未配置或无有效条目,ClientIP 将记录反代/容器内网地址(如 172.19.0.4),请设置为实际反代网段,如 172.16.0.0/12")
}
if err := r.SetTrustedProxies(proxies); err != nil {
return nil, err
@@ -55,7 +78,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
}))
// 服务
authSvc := service.NewAuthService(model.DB, cfg.JWTSecret)
authSvc := service.NewAuthService(model.DB, cfg.JWTSecret).WithDevMode(cfg.DevMode)
boardSvc := service.NewBoardService(model.DB)
settingSvc := service.NewSettingService(model.DB)
// 等级体系快照随进程启动加载(后台保存时经 SetLevels 刷新,无需重启)
@@ -82,6 +105,8 @@ 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)
// 导入建号绕过注册链路,显式接线:导入用户同样加入默认全站大厅
legacyImportSvc.WithHallMembership(chatSvc.EnsureDefaultMembership)
badgeSvc := service.NewBadgeService(model.DB).WithNotification(notifSvc)
visitSvc := service.NewVisitStatsService(model.DB, settingSvc)
analyticsSvc := service.NewAnalyticsService(model.DB)
@@ -102,35 +127,35 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
go visitSvc.RunRetention(pipeCtx)
h := &handler.Handlers{
Ops: ops,
Cfg: cfg,
Hub: realtime.NewHub(),
Auth: authSvc,
Board: boardSvc,
Post: postSvc,
Comment: commentSvc,
Like: likeSvc,
Favorite: favoriteSvc,
Follow: followSvc,
Notification: notifSvc,
OverviewSvc: overviewSvc,
Checkin: checkinSvc,
Announcement: announcementSvc,
SitePage: sitePageSvc,
Upload: uploadSvc,
PostFile: postFileSvc,
Points: pointsSvc,
Setting: settingSvc,
AdminUser: adminUserSvc,
LegacyImport: legacyImportSvc,
Moderation: moderationSvc,
Chat: chatSvc,
Visit: visitSvc,
Analytics: analyticsSvc,
HidePwd: service.NewHidePasswordCookie(cfg.JWTSecret, !cfg.DevMode),
Ads: adSvc,
Sidebar: service.NewSidebarService(model.DB),
Badge: badgeSvc,
Ops: ops,
Cfg: cfg,
Hub: realtime.NewHub(),
Auth: authSvc,
Board: boardSvc,
Post: postSvc,
Comment: commentSvc,
Like: likeSvc,
Favorite: favoriteSvc,
Follow: followSvc,
Notification: notifSvc,
OverviewSvc: overviewSvc,
Checkin: checkinSvc,
Announcement: announcementSvc,
SitePage: sitePageSvc,
Upload: uploadSvc,
PostFile: postFileSvc,
Points: pointsSvc,
Setting: settingSvc,
AdminUser: adminUserSvc,
LegacyImport: legacyImportSvc,
Moderation: moderationSvc,
Chat: chatSvc,
Visit: visitSvc,
Analytics: analyticsSvc,
HidePwd: service.NewHidePasswordCookie(cfg.JWTSecret, !cfg.DevMode),
Ads: adSvc,
Sidebar: service.NewSidebarService(model.DB),
Badge: badgeSvc,
LeaderboardSvc: leaderboardSvc,
}
// 通知落库后统一推 WS 红点(点赞/评论/审核/@ 等共用)
@@ -391,6 +416,9 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
staffAPI.POST("/upload/background/from-media", authMW.RequirePerm(service.PermSettings), middleware.RateLimitMiddleware(limiter, service.RateUpload), h.UploadBackgroundFromMedia)
staffAPI.POST("/upload/brand", authMW.RequirePerm(service.PermSettings), middleware.RateLimitMiddleware(limiter, service.RateUpload), h.UploadBrand)
staffAPI.POST("/upload/brand/from-media", authMW.RequirePerm(service.PermSettings), middleware.RateLimitMiddleware(limiter, service.RateUpload), h.UploadBrandFromMedia)
// 媒体库:全站图片盘点(只读展示)+ 网格缩略图
staffAPI.GET("/media-library", authMW.RequirePerm(service.PermSettings), h.AdminMediaLibrary)
staffAPI.GET("/media-library/thumb", authMW.RequirePerm(service.PermSettings), h.MediaLibraryThumb)
// 用户与权限管理(超级管理员/站长):列表、角色授权、封禁、登录历史
usersAPI := staffAPI.Group("", authMW.RequirePerm(service.PermUsers))