package router import ( "context" "log" "net" "os" "path/filepath" "strings" "time" "github.com/freefire/jiang13-bbs/config" "github.com/freefire/jiang13-bbs/handler" "github.com/freefire/jiang13-bbs/middleware" "github.com/freefire/jiang13-bbs/model" "github.com/freefire/jiang13-bbs/realtime" "github.com/freefire/jiang13-bbs/service" "github.com/gin-contrib/cors" "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 { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } r := gin.New() 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") } if err := r.SetTrustedProxies(proxies); err != nil { return nil, err } // 大附件落盘到临时文件,避免 multipart 整文件进内存(默认 32MiB) r.MaxMultipartMemory = 4 << 20 r.Use(gin.Recovery()) r.Use(gin.Logger()) // 全局安全响应头 r.Use(middleware.SecurityHeaders()) // CORS:开发放行 localhost:3000;生产用 SITE_URL / CORS_ORIGINS(同源反代时浏览器不走跨域) r.Use(cors.New(cors.Config{ AllowOriginFunc: cfg.AllowOrigin, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "X-CSRF-Token"}, AllowCredentials: true, MaxAge: 12 * time.Hour, })) // 服务 authSvc := service.NewAuthService(model.DB, cfg.JWTSecret).WithDevMode(cfg.DevMode) boardSvc := service.NewBoardService(model.DB) settingSvc := service.NewSettingService(model.DB) // 等级体系快照随进程启动加载(后台保存时经 SetLevels 刷新,无需重启) model.ApplyLevelDefs(settingSvc.Levels()) ops := service.NewOperations(model.DB, cfg) go ops.Run(context.Background()) postSvc := service.NewPostService(model.DB).WithSetting(settingSvc).WithBoard(boardSvc).WithDevMode(cfg.DevMode) commentSvc := service.NewCommentService(model.DB).WithSetting(settingSvc) likeSvc := service.NewLikeService(model.DB) notifSvc := service.NewNotificationService(model.DB) favoriteSvc := service.NewFavoriteService(model.DB).WithPosts(postSvc) followSvc := service.NewFollowService(model.DB).WithNotification(notifSvc) overviewSvc := service.NewOverviewService(model.DB) checkinSvc := service.NewCheckinService(model.DB).WithSetting(settingSvc) announcementSvc := service.NewAnnouncementService(model.DB) sitePageSvc := service.NewSitePageService(model.DB) uploadSvc := service.NewUploadService(model.DB, filepath.Join(cfg.DataDir, "uploads")).WithSetting(settingSvc) postFileSvc := service.NewPostFileService(model.DB, filepath.Join(cfg.DataDir, "private")).WithSetting(settingSvc) uploadSvc.WithOperations(ops) postFileSvc.WithOperations(ops) pointsSvc := service.NewPointsService(model.DB) leaderboardSvc := service.NewLeaderboardService(model.DB) librarySvc := service.NewLibraryService(model.DB, cfg.DataDir).WithSetting(settingSvc) adminUserSvc := service.NewAdminUserService(model.DB) 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) adSvc := service.NewAdService(model.DB, service.NewCaptchaStore()) overviewSvc.WithAds(adSvc) if err := uploadSvc.EnsureDir(); err != nil { return nil, err } if err := postFileSvc.EnsureDir(); err != nil { return nil, err } if err := librarySvc.EnsureDir(); err != nil { return nil, err } limiter := service.DefaultRateLimiter() // 访问统计管道:攒批落库 + 保留期滚动清理(随进程生命周期)。 // 不用 WithCancel+defer:Setup 返回即 cancel 会让落库协程在启动阶段就退出,事件永远不落库 pipeCtx := context.Background() go visitSvc.StartFlusher(pipeCtx) 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), FriendLink: service.NewFriendLinkService(model.DB), Badge: badgeSvc, LeaderboardSvc: leaderboardSvc, LibrarySvc: librarySvc, } // 通知落库后统一推 WS 红点(点赞/评论/审核/@ 等共用) notifSvc.OnNotifyNew = func(userID uint) { if h.Hub == nil { return } h.Hub.BroadcastUser(userID, realtime.Envelope{ Type: realtime.EventNotificationNew, }) } // 点赞通知延迟落库(冷却期内取消点赞则不通知) service.StartLikeNotifyFlusher(notifSvc) 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")) // 品牌图固定地址(友链等外部引用):按站点设置解析到当前图,换图不变链 r.GET("/api/brand/:slot", h.BrandImage) // 健康检查(sitemap / robots 由 Next.js Metadata Route 对外提供) r.GET("/health", h.Health) // 实时通信总线(WebSocket,cookie 鉴权 + Origin 校验,处理器内部完成鉴权升级) r.GET("/api/ws", h.RealtimeWS) // 公开 API(可选登录);EnsureCSRF 让游客也能拿到双提交 token(密码解锁等) pubAPI := r.Group("/api", authMW.OptionalAuth(), middleware.EnsureCSRFCookie(!cfg.DevMode)) { pubAPI.GET("/me", h.Me) pubAPI.GET("/boards", h.Boards) pubAPI.GET("/boards/:id/sidebar", h.BoardSidebar) pubAPI.GET("/overview", h.Overview) pubAPI.GET("/sidebar", h.GetSidebar) pubAPI.GET("/friend-links", h.GetFriendLinks) 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) pubAPI.GET("/posts/:id/attachments/:aid/download", h.DownloadPostAttachment) pubAPI.GET("/posts/:id/comments", h.PostComments) pubAPI.GET("/posts/:id/comments/:cid", h.CommentLocation) pubAPI.GET("/users/:id", h.UserProfile) pubAPI.GET("/users/:id/comments", h.UserComments) pubAPI.GET("/users/:id/favorites", h.UserFavorites) pubAPI.GET("/users/:id/following", h.UserFollowing) pubAPI.GET("/announcements", h.AnnouncementsList) pubAPI.GET("/announcements/:id", h.AnnouncementDetail) pubAPI.GET("/pages", h.SitePagesList) pubAPI.GET("/pages/:slug", h.SitePageDetail) pubAPI.GET("/settings", h.PublicSettings) pubAPI.GET("/leaderboard", h.Leaderboard) pubAPI.GET("/leaderboard/me", h.LeaderboardMe) // 书库(公开浏览/下载;文件经专用端点消费,不进 /uploads 静态目录) pubAPI.GET("/library", h.LibraryList) pubAPI.GET("/library/:slug", h.LibraryDetail) pubAPI.GET("/library/files/:fid/download", h.LibraryFileDownload) pubAPI.GET("/site-state", h.SiteState) 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(), 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 防护) pubAPI.POST("/auth/refresh", middleware.CSRFMiddleware(), h.Refresh) // 登出不依赖有效登录态:封禁/登录态失效后前端仍需凭它清除 cookie(CSRF 仍校验) pubAPI.POST("/logout", middleware.CSRFMiddleware(), h.Logout) pubAPI.GET("/captcha", middleware.RateLimitMiddleware(limiter, service.RateCaptcha), h.CaptchaIssue) pubAPI.GET("/ads/config", h.AdsPublicConfig) pubAPI.POST("/ads", middleware.CSRFMiddleware(), middleware.RateLimitMiddleware(limiter, service.RateAdSubmit), h.AdsSubmit) } // 需登录 API(先鉴权,再 CSRF 防护) api := r.Group("/api", authMW.RequireAuth(), middleware.CSRFMiddleware()) { api.POST("/change-password", h.ChangePassword) api.GET("/me/login-devices", h.MyLoginDevices) api.DELETE("/me/login-devices/:id", h.RevokeLoginDevice) api.PUT("/profile", h.UpdateProfile) // 友情链接申请(登录用户;提交后进 pending 待审批) api.POST("/friend-links/apply", h.ApplyFriendLink) api.GET("/friend-links/mine", h.MyFriendLinks) api.PUT("/friend-links/:id", h.OwnerUpdateFriendLink) api.POST("/posts", middleware.StaffExempt(middleware.RateLimitMiddleware(limiter, service.RatePost)), h.CreatePost) api.PUT("/posts/:id", h.UpdatePost) api.GET("/posts/:id/history", h.PostEditHistory) // 评论编辑历史(登录即可见;顶层路由:管理后台无 post_id 上下文) api.GET("/comments/:cid/history", h.CommentEditHistory) api.POST("/posts/:id/unlock", middleware.StaffExempt(middleware.RateLimitUserMiddleware(limiter, service.RateInteract)), h.UnlockPostContent) api.POST("/posts/:id/poll/vote", middleware.StaffExempt(middleware.RateLimitUserMiddleware(limiter, service.RateInteract)), h.VotePoll) api.POST("/posts/:id/poll/close", h.ClosePoll) api.POST("/posts/:id/question/accept", middleware.StaffExempt(middleware.RateLimitUserMiddleware(limiter, service.RateInteract)), h.AcceptQuestion) api.POST("/posts/:id/question/solve", middleware.StaffExempt(middleware.RateLimitUserMiddleware(limiter, service.RateInteract)), h.SolveQuestion) api.POST("/posts/:id/question/reopen", middleware.StaffExempt(middleware.RateLimitUserMiddleware(limiter, service.RateInteract)), h.ReopenQuestion) api.POST("/posts/:id/bounty/accept", middleware.StaffExempt(middleware.RateLimitUserMiddleware(limiter, service.RateInteract)), h.AcceptBounty) api.POST("/posts/:id/lottery/draw", h.DrawLottery) api.DELETE("/posts/:id", h.DeletePost) api.PUT("/posts/:id/pin", h.TogglePin) api.PUT("/posts/:id/recommend", h.ToggleRecommend) api.PUT("/posts/:id/lock", h.TogglePostLock) api.POST("/posts/:id/like", h.ToggleLike) api.POST("/posts/:id/favorite", middleware.StaffExempt(middleware.RateLimitUserMiddleware(limiter, service.RateInteract)), h.ToggleFavorite) api.POST("/users/:id/follow", middleware.StaffExempt(middleware.RateLimitUserMiddleware(limiter, service.RateInteract)), h.ToggleFollow) api.POST("/posts/:id/comments", middleware.StaffExempt(middleware.RateLimitMiddleware(limiter, service.RateComment)), h.CreateComment) api.PUT("/posts/:id/comments/:cid", middleware.StaffExempt(middleware.RateLimitMiddleware(limiter, service.RateComment)), h.UpdateComment) api.DELETE("/posts/:id/comments/:cid", h.DeleteComment) api.PUT("/posts/:id/comments/:cid/restore", h.RestoreComment) api.DELETE("/posts/:id/comments/:cid/purge", h.PurgeComment) // 通知 api.GET("/notifications", h.Notifications) api.GET("/notifications/unread-count", h.UnreadCount) api.PUT("/notifications/:id/read", h.MarkRead) api.PUT("/notifications/read-all", h.MarkAllRead) // 每日签到 / 积分 api.GET("/checkin", h.GetCheckin) api.POST("/checkin", middleware.StaffExempt(middleware.RateLimitMiddleware(limiter, service.RateComment)), h.DoCheckin) api.GET("/points", h.GetPointsBalance) api.GET("/points/ledger", h.GetPointsLedger) // 头像上传(裁剪后的 WebP)/ 帖子插图 / 媒体库 / 附件删除 api.POST("/upload/avatar", h.UploadAvatar) api.POST("/upload/image", middleware.StaffExempt(middleware.RateLimitMiddleware(limiter, service.RateUpload)), h.UploadImage) api.POST("/upload/file", middleware.StaffExempt(middleware.RateLimitMiddleware(limiter, service.RateUpload)), h.UploadPostFile) api.PUT("/upload/file/:id/price", h.UpdatePostFilePrice) api.DELETE("/upload/file/:id", h.DeletePostFile) api.PUT("/avatar/use", h.UseAvatar) api.GET("/my/media", h.MyMedia) api.DELETE("/my/attachments/:id", h.DeleteAttachment) // 时间线:从 Git commits 页导入(登录用户;适配器由超管配置) api.POST("/timeline/from-git", middleware.StaffExempt(middleware.RateLimitUserMiddleware(limiter, service.RateTimelineGit)), h.TimelineFromGit) // 时间线:从 Git releases 页导入更新日志 api.POST("/timeline/from-releases", middleware.StaffExempt(middleware.RateLimitUserMiddleware(limiter, service.RateTimelineGit)), h.TimelineFromReleases) // 群聊(二期):建群/成员/消息/未读;全站关闭消息时前台一律 403 chatAPI := api.Group("/chat", h.RequireMessagesOpen) { chatAPI.GET("/rooms", h.ListChatRooms) chatAPI.POST("/rooms", h.CreateChatRoom) chatAPI.POST("/direct", h.OpenDirectChat) chatAPI.GET("/users", h.SearchChatUsers) chatAPI.GET("/rooms/:id", h.GetChatRoom) chatAPI.PUT("/rooms/:id", h.UpdateChatRoom) chatAPI.PUT("/rooms/:id/pin", h.PinChatRoom) chatAPI.DELETE("/rooms/:id", h.DissolveChatRoom) chatAPI.POST("/rooms/:id/join", h.JoinChatRoom) chatAPI.POST("/rooms/:id/leave", h.LeaveChatRoom) chatAPI.GET("/rooms/:id/members", h.ListChatMembers) chatAPI.POST("/rooms/:id/members", h.InviteChatMembers) chatAPI.DELETE("/rooms/:id/members/:uid", h.KickChatMember) chatAPI.PUT("/rooms/:id/members/:uid/mute", h.SetChatMemberMute) chatAPI.PUT("/rooms/:id/members/:uid/role", h.SetChatMemberRole) chatAPI.GET("/rooms/:id/messages", h.ListChatMessages) chatAPI.POST("/rooms/:id/messages", middleware.StaffExempt(middleware.RateLimitMiddleware(limiter, service.RateChat)), h.SendChatMessage) chatAPI.DELETE("/rooms/:id/messages/:mid", h.RecallChatMessage) chatAPI.PUT("/rooms/:id/read", h.MarkChatRead) chatAPI.GET("/unread-summary", h.ChatUnreadSummary) } } // 管理后台 API:RequireStaff 基础鉴权(板块管理员及以上,Actor 实时从 DB 现取), // 各功能点再由 RequirePerm 按角色放行;板块范围在 service 层按 Actor.BoardIDs 隔离 staffAPI := r.Group("/api/admin", authMW.RequireStaff(), middleware.CSRFMiddleware()) { // 内容审核队列(任意管理角色;板块管理员只见授权板块) staffAPI.GET("/moderation/pending-posts", h.AdminPendingPosts) staffAPI.GET("/moderation/pending-comments", h.AdminPendingComments) staffAPI.GET("/moderation/counts", h.AdminPendingCounts) staffAPI.PUT("/moderation/posts/:id/approve", h.AdminApprovePost) staffAPI.PUT("/moderation/posts/:id/reject", h.AdminRejectPost) staffAPI.PUT("/moderation/comments/:id/approve", h.AdminApproveComment) staffAPI.PUT("/moderation/comments/:id/reject", h.AdminRejectComment) // 内容库:帖子/评论全量 + 回收站(恢复 / 彻底删除) staffAPI.GET("/content/counts", h.AdminContentCounts) staffAPI.GET("/content/posts", h.AdminListPosts) staffAPI.GET("/content/comments", h.AdminListComments) staffAPI.DELETE("/content/posts/:id", h.AdminSoftDeletePost) staffAPI.PUT("/content/posts/:id/restore", h.AdminRestorePost) staffAPI.DELETE("/content/posts/:id/purge", h.AdminPurgePost) staffAPI.DELETE("/content/comments/:id", h.AdminSoftDeleteComment) staffAPI.PUT("/content/comments/:id/restore", h.AdminRestoreComment) staffAPI.DELETE("/content/comments/:id/purge", h.AdminPurgeComment) // 积分与类型帖概览(管理员及以上) staffAPI.GET("/economy", authMW.RequirePerm(service.PermAnnouncements), h.AdminPointsStats) // 站点分析概览(任意 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) // 站点公告文章管理(管理员及以上) announceAPI := staffAPI.Group("", authMW.RequirePerm(service.PermAnnouncements)) announceAPI.GET("/announcements", h.AdminListAnnouncements) announceAPI.POST("/announcements", h.AdminCreateAnnouncement) announceAPI.PUT("/announcements/:id", h.AdminUpdateAnnouncement) announceAPI.POST("/announcements/:id/pin", h.AdminToggleAnnouncementPin) announceAPI.DELETE("/announcements/:id", h.AdminDeleteAnnouncement) announceAPI.GET("/pages", h.AdminListSitePages) announceAPI.POST("/pages", h.AdminCreateSitePage) announceAPI.PUT("/pages/:id", h.AdminUpdateSitePage) announceAPI.DELETE("/pages/:id", h.AdminDeleteSitePage) announceAPI.DELETE("/pages/:id/purge", h.AdminPurgeSitePage) // 书库管理(管理员及以上,同公告/单页权限) libraryAPI := staffAPI.Group("/library", authMW.RequirePerm(service.PermAnnouncements)) { libraryAPI.GET("/docs", h.AdminListLibraryDocs) libraryAPI.POST("/docs", h.AdminCreateLibraryDoc) libraryAPI.PUT("/docs/:id", h.AdminUpdateLibraryDoc) libraryAPI.DELETE("/docs/:id", h.AdminDeleteLibraryDoc) libraryAPI.DELETE("/docs/:id/purge", h.AdminPurgeLibraryDoc) libraryAPI.POST("/docs/:id/files", middleware.RateLimitMiddleware(limiter, service.RateUpload), h.AdminUploadLibraryFile) libraryAPI.DELETE("/files/:fid", h.AdminDeleteLibraryFile) // 章节(两级树:章 → 节) libraryAPI.POST("/docs/:id/sections", h.AdminCreateLibrarySection) libraryAPI.PUT("/sections/:sid", h.AdminUpdateLibrarySection) libraryAPI.DELETE("/sections/:sid", h.AdminDeleteLibrarySection) libraryAPI.POST("/sections/:sid/move", h.AdminMoveLibrarySection) libraryAPI.POST("/docs/:id/import-md", middleware.RateLimitMiddleware(limiter, service.RateUpload), h.AdminImportLibraryMarkdown) libraryAPI.POST("/docs/:id/import-md-batch", middleware.RateLimitMiddleware(limiter, service.RateUpload), h.AdminImportLibraryMarkdownBatch) libraryAPI.POST("/cover/from-media", middleware.RateLimitMiddleware(limiter, service.RateUpload), h.UploadLibraryCoverFromMedia) } // 广告位管理(管理员及以上) adsAPI := staffAPI.Group("", authMW.RequirePerm(service.PermAnnouncements)) adsAPI.GET("/ads/config", h.AdminGetAdsConfig) adsAPI.PUT("/ads/config", h.AdminSaveAdsConfig) adsAPI.GET("/ads", h.AdminListAds) adsAPI.PUT("/ads/:id/approve", h.AdminApproveAd) adsAPI.PUT("/ads/:id/reject", h.AdminRejectAd) adsAPI.PUT("/ads/:id/sort", h.AdminSortAd) adsAPI.DELETE("/ads/:id", h.AdminDeleteAd) adsAPI.POST("/upload/ad-qr", middleware.RateLimitMiddleware(limiter, service.RateUpload), h.AdminUploadAdQR) 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) // 徽章管理(超级管理员/站长):定义 CRUD + 授予/撤销 badgeAPI := staffAPI.Group("/badges", authMW.RequirePerm(service.PermSettings)) { badgeAPI.GET("", h.AdminListBadges) badgeAPI.POST("", h.AdminCreateBadge) badgeAPI.PUT("/:id", h.AdminUpdateBadge) badgeAPI.DELETE("/:id", h.AdminDeleteBadge) badgeAPI.GET("/:id/holders", h.AdminBadgeHolders) badgeAPI.POST("/:id/grant", h.AdminGrantBadge) badgeAPI.DELETE("/:id/grant/:userId", h.AdminRevokeBadge) } staffAPI.PUT("/sidebar/widgets/:id", authMW.RequirePerm(service.PermSettings), h.AdminUpdateSidebarWidget) staffAPI.DELETE("/sidebar/widgets/:id", authMW.RequirePerm(service.PermSettings), h.AdminDeleteSidebarWidget) // 友情链接管理(超级管理员/站长):CRUD + 审批 friendLinkAPI := staffAPI.Group("/friend-links", authMW.RequirePerm(service.PermSettings)) { friendLinkAPI.GET("", h.AdminListFriendLinks) friendLinkAPI.POST("", h.AdminCreateFriendLink) friendLinkAPI.PUT("/:id", h.AdminUpdateFriendLink) friendLinkAPI.POST("/:id/status", h.AdminSetFriendLinkStatus) friendLinkAPI.DELETE("/:id", h.AdminDeleteFriendLink) } // 站点设置(超级管理员/站长) staffAPI.GET("/settings", authMW.RequirePerm(service.PermSettings), h.AdminGetSettings) opsAPI := staffAPI.Group("/settings/modules", authMW.RequirePerm(service.PermSettings)) opsAPI.GET("/:module", h.ReadModule) opsAPI.PUT("/:module", h.SaveModule) opsAPI.POST("/:module/test", h.TestModule) opsAPI.GET("/:module/records", h.ModuleRecords) staffAPI.GET("/diagnostics", authMW.RequirePerm(service.PermSettings), h.Diagnostics) staffAPI.POST("/maintenance/actions", authMW.RequirePerm(service.PermSettings), h.MaintenanceAction) // 数据导入(旧站 / WordPress / Typecho 等,source 白名单见 service.LegacyImportSource) staffAPI.POST("/import/:source", authMW.RequirePerm(service.PermSettings), h.AdminImportLegacy) staffAPI.PUT("/settings", authMW.RequirePerm(service.PermSettings), h.UpdateSettings) staffAPI.POST("/upload/background", authMW.RequirePerm(service.PermSettings), middleware.RateLimitMiddleware(limiter, service.RateUpload), h.UploadBackground) 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)) { usersAPI.GET("/users", h.AdminListUsers) usersAPI.PUT("/users/:id/role", h.AdminUpdateUserRole) usersAPI.PUT("/users/:id/messages", h.AdminSetUserMessages) usersAPI.PUT("/users/:id/ban", h.AdminSetUserBan) usersAPI.GET("/users/:id/login-logs", h.AdminUserLoginLogs) // 积分管理:手动调整(加分计累计/扣分仅扣余额)与积分流水分页 usersAPI.PUT("/users/:id/points", h.AdminAdjustUserPoints) usersAPI.GET("/users/:id/points/ledger", h.AdminUserPointLedger) // 站长内容档案(handler 内再校验 owner) usersAPI.GET("/users/:id", h.AdminGetUserAudit) usersAPI.GET("/users/:id/posts", h.AdminUserAuditPosts) usersAPI.GET("/users/:id/comments", h.AdminUserAuditComments) usersAPI.GET("/users/:id/messages", h.AdminUserAuditMessages) } // 板块管理(仅站长):增删改、排序、版主授权 boardsAPI := staffAPI.Group("", authMW.RequirePerm(service.PermBoards)) { boardsAPI.GET("/boards", h.AdminListBoards) boardsAPI.POST("/boards", h.AdminCreateBoard) boardsAPI.PUT("/boards/:id", h.AdminUpdateBoard) boardsAPI.DELETE("/boards/:id", h.AdminDeleteBoard) boardsAPI.PUT("/boards/reorder", h.AdminReorderBoards) boardsAPI.GET("/boards/:id/moderators", h.AdminListBoardModerators) boardsAPI.POST("/boards/:id/moderators", h.AdminAddBoardModerator) boardsAPI.DELETE("/boards/:id/moderators/:uid", h.AdminRemoveBoardModerator) } } // 消息管理后台:登录即可(handler 内校验站点消息权或群管理员) msgAPI := r.Group("/api/admin", authMW.RequireAuth(), middleware.CSRFMiddleware()) { msgAPI.GET("/chat/messages", h.AdminChatMessages) msgAPI.DELETE("/chat/rooms/:roomId/messages/:mid", h.AdminRecallChatMessage) } r.NoRoute(func(c *gin.Context) { c.JSON(404, gin.H{"error": "not found"}) }) return r, nil }