Files
jiang13-bbs/backend/router/router.go
freefire d784b0ea7a feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS
站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 05:27:33 +08:00

290 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package router
import (
"path/filepath"
"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()
// 大附件落盘到临时文件,避免 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)
postSvc := service.NewPostService(model.DB).WithSetting(settingSvc).WithDevMode(cfg.DevMode)
commentSvc := service.NewCommentService(model.DB)
likeSvc := service.NewLikeService(model.DB)
notifSvc := service.NewNotificationService(model.DB)
overviewSvc := service.NewOverviewService(model.DB)
checkinSvc := service.NewCheckinService(model.DB)
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)
pointsSvc := service.NewPointsService(model.DB)
adminUserSvc := service.NewAdminUserService(model.DB)
moderationSvc := service.NewModerationService(model.DB, notifSvc)
chatSvc := service.NewChatService(model.DB, notifSvc)
telemetrySvc := service.NewTelemetryService(model.DB)
analyticsSvc := service.NewAnalyticsService(model.DB)
if err := uploadSvc.EnsureDir(); err != nil {
return nil, err
}
if err := postFileSvc.EnsureDir(); err != nil {
return nil, err
}
limiter := service.DefaultRateLimiter()
h := &handler.Handlers{
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,
Moderation: moderationSvc,
Chat: chatSvc,
Telemetry: telemetrySvc,
Analytics: analyticsSvc,
HidePwd: service.NewHidePasswordCookie(cfg.JWTSecret, !cfg.DevMode),
}
// 通知落库后统一推 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)
// 上传文件静态服务(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("/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.POST("/telemetry/pageview", middleware.CSRFMiddleware(), h.TelemetryPageView)
pubAPI.POST("/register", middleware.RateLimitMiddleware(limiter, service.RateRegister), h.Register)
pubAPI.POST("/login", 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)
}
// 需登录 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.RateLimitMiddleware(limiter, service.RatePost), h.CreatePost)
api.PUT("/posts/:id", h.UpdatePost)
api.POST("/posts/:id/unlock", middleware.RateLimitUserMiddleware(limiter, service.RateInteract), h.UnlockPostContent)
api.POST("/posts/:id/poll/vote", middleware.RateLimitUserMiddleware(limiter, service.RateInteract), h.VotePoll)
api.POST("/posts/:id/poll/close", h.ClosePoll)
api.POST("/posts/:id/question/accept", middleware.RateLimitUserMiddleware(limiter, service.RateInteract), h.AcceptQuestion)
api.POST("/posts/:id/question/solve", middleware.RateLimitUserMiddleware(limiter, service.RateInteract), h.SolveQuestion)
api.POST("/posts/:id/question/reopen", middleware.RateLimitUserMiddleware(limiter, service.RateInteract), h.ReopenQuestion)
api.POST("/posts/:id/bounty/accept", 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.POST("/posts/:id/like", h.ToggleLike)
api.POST("/posts/:id/comments", middleware.RateLimitMiddleware(limiter, service.RateComment), h.CreateComment)
api.PUT("/posts/:id/comments/:cid", 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.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.RateLimitMiddleware(limiter, service.RateUpload), h.UploadImage)
api.POST("/upload/file", 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.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.GET("/rooms/:id/messages", h.ListChatMessages)
chatAPI.POST("/rooms/:id/messages", 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("/content/comments/:id/history", h.AdminCommentHistory)
// 积分与类型帖概览(管理员及以上)
staffAPI.GET("/economy", authMW.RequirePerm(service.PermAnnouncements), h.AdminPointsStats)
// 站点分析概览(任意 staff)
staffAPI.GET("/analytics/overview", h.AdminAnalyticsOverview)
// 群聊监管队列(任意 staff;撤回权限仍按 oversee 规则)
staffAPI.GET("/chat/messages", h.AdminChatMessages)
staffAPI.DELETE("/chat/rooms/:roomId/messages/:mid", h.AdminRecallChatMessage)
// 站点公告文章管理(管理员及以上)
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)
// 站点外观设置(超级管理员/站长);含 timeline_git_import
staffAPI.GET("/settings", authMW.RequirePerm(service.PermSettings), h.AdminGetSettings)
staffAPI.PUT("/settings", authMW.RequirePerm(service.PermSettings), h.UpdateSettings)
staffAPI.POST("/upload/background", authMW.RequirePerm(service.PermSettings), middleware.RateLimitMiddleware(limiter, service.RateUpload), h.UploadBackground)
// 用户与权限管理(超级管理员/站长):列表、角色授权、封禁、登录历史
usersAPI := staffAPI.Group("", authMW.RequirePerm(service.PermUsers))
usersAPI.GET("/users", h.AdminListUsers)
usersAPI.PUT("/users/:id/role", h.AdminUpdateUserRole)
usersAPI.PUT("/users/:id/ban", h.AdminSetUserBan)
usersAPI.GET("/users/:id/login-logs", h.AdminUserLoginLogs)
// 站长内容档案(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)
}
r.NoRoute(func(c *gin.Context) {
c.JSON(404, gin.H{"error": "not found"})
})
return r, nil
}