feat: 站点媒体库与移动端底部导航,服务测试补强
- 新增媒体库:media_library/media_thumbs 服务(WebP 缩略图)、管理端 media 页面 - 移动端底部导航 MobileTabBar 替换 MobilePostBar - 旧数据导入增强与测试、路由与登录会话/板块侧边栏测试补强 - site-doc 组件精简(移除 Breadcrumb),文档新增迁移公告说明
This commit is contained in:
@@ -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))
|
||||
|
||||
33
backend/router/router_test.go
Normal file
33
backend/router/router_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestParseTrustedProxies 覆盖:合法 CIDR/IP/localhost、空白容忍、空项、非法条目跳过
|
||||
func TestParseTrustedProxies(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want []string
|
||||
}{
|
||||
{"未配置", "", nil},
|
||||
{"单个网段", "172.19.0.0/16", []string{"172.19.0.0/16"}},
|
||||
// 真实事故:尾随空格曾导致 invalid CIDR 整进程退出
|
||||
{"尾随空格", "172.19.0.0/16 ", []string{"172.19.0.0/16"}},
|
||||
{"两侧空白与空项", " 172.19.0.0/16 , 10.0.0.5 ,", []string{"172.19.0.0/16", "10.0.0.5"}},
|
||||
{"单个 IP", "10.0.0.5", []string{"10.0.0.5"}},
|
||||
{"localhost", "localhost", []string{"localhost"}},
|
||||
{"非法条目跳过", "172.19.0.0/16,bad-cidr", []string{"172.19.0.0/16"}},
|
||||
{"全部非法", "bad,,worse", nil},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := parseTrustedProxies(c.in)
|
||||
if !reflect.DeepEqual(got, c.want) {
|
||||
t.Errorf("parseTrustedProxies(%q) = %v, want %v", c.in, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user