- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal - 新增 PostEditedMark 编辑标记展示 - 新增内容锁定机制(content_lock)防止并发编辑冲突 - 审核与通知服务适配
431 lines
22 KiB
Go
431 lines
22 KiB
Go
package router
|
||
|
||
import (
|
||
"context"
|
||
"log"
|
||
"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"
|
||
)
|
||
|
||
// Setup 初始化路由
|
||
func Setup(cfg *config.Config) (*gin.Engine, error) {
|
||
if cfg.DevMode {
|
||
gin.SetMode(gin.DebugMode)
|
||
} else {
|
||
gin.SetMode(gin.ReleaseMode)
|
||
}
|
||
|
||
r := gin.New()
|
||
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
|
||
}
|
||
// 大附件落盘到临时文件,避免 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)
|
||
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)
|
||
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)
|
||
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)
|
||
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
|
||
}
|
||
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,
|
||
Hub: realtime.NewHub(),
|
||
Auth: authSvc,
|
||
Board: boardSvc,
|
||
Post: postSvc,
|
||
Comment: commentSvc,
|
||
Like: likeSvc,
|
||
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 红点(点赞/评论/审核/@ 等共用)
|
||
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"))
|
||
|
||
// 健康检查(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("/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("/users/:id", h.UserProfile)
|
||
pubAPI.GET("/users/:id/comments", h.UserComments)
|
||
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)
|
||
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)
|
||
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/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)
|
||
|
||
// 群聊(二期):建群/成员/消息/未读;全站关闭消息时前台一律 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)
|
||
|
||
// 广告位管理(管理员及以上)
|
||
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)
|
||
|
||
// 站点设置(超级管理员/站长)
|
||
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)
|
||
|
||
// 用户与权限管理(超级管理员/站长):列表、角色授权、封禁、登录历史
|
||
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
|
||
}
|