Files
jiang13-bbs/backend/router/router.go
freefire 42c395a864 feat: 实时消息与管理端运营能力
新增群聊/私信与公共 WebSocket 总线,站长用户内容档案,并将通知与待审角标改为推送驱动;同步精简管理仪表盘并修复主题换肤 DOM 冲突。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 22:58:16 +08:00

218 lines
8.6 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()
r.Use(gin.Recovery())
r.Use(gin.Logger())
// 全局安全响应头
r.Use(middleware.SecurityHeaders())
// CORS
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000", "http://127.0.0.1:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "X-CSRF-Token"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// 服务
authSvc := service.NewAuthService(model.DB, cfg.JWTSecret)
boardSvc := service.NewBoardService(model.DB)
postSvc := service.NewPostService(model.DB)
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)
uploadSvc := service.NewUploadService(model.DB, filepath.Join(cfg.DataDir, "uploads"))
settingSvc := service.NewSettingService(model.DB)
adminUserSvc := service.NewAdminUserService(model.DB)
moderationSvc := service.NewModerationService(model.DB, notifSvc)
chatSvc := service.NewChatService(model.DB, notifSvc)
if err := uploadSvc.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,
Upload: uploadSvc,
Setting: settingSvc,
AdminUser: adminUserSvc,
Moderation: moderationSvc,
Chat: chatSvc,
}
// 通知落库后统一推 WS 红点(点赞/评论/审核/@ 等共用)
notifSvc.OnNotifyNew = func(userID uint) {
if h.Hub == nil {
return
}
h.Hub.BroadcastUser(userID, realtime.Envelope{
Type: realtime.EventNotificationNew,
})
}
authMW := middleware.NewAuthMiddleware(authSvc)
// 上传文件静态服务(data/uploads → /uploads)
r.Static("/uploads", filepath.Join(cfg.DataDir, "uploads"))
// 健康检查 & SEO
r.GET("/health", h.Health)
r.GET("/robots.txt", h.RobotsTxt)
r.GET("/sitemap.xml", h.SitemapXML)
// 实时通信总线(WebSocket,cookie 鉴权 + Origin 校验,处理器内部完成鉴权升级)
r.GET("/api/ws", h.RealtimeWS)
// 公开 API(可选登录)
pubAPI := r.Group("/api", authMW.OptionalAuth())
{
pubAPI.GET("/me", h.Me)
pubAPI.GET("/boards", h.Boards)
pubAPI.GET("/overview", h.Overview)
pubAPI.GET("/posts", h.Posts)
pubAPI.GET("/posts/:id", h.PostDetail)
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("/settings", h.PublicSettings)
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.PUT("/profile", h.UpdateProfile)
api.POST("/posts", middleware.RateLimitMiddleware(limiter, service.RatePost), h.CreatePost)
api.PUT("/posts/:id", h.UpdatePost)
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.DELETE("/posts/:id/comments/:cid", h.DeleteComment)
// 通知
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)
// 头像上传(裁剪后的 WebP)/ 媒体库 / 附件删除
api.POST("/upload/avatar", h.UploadAvatar)
api.PUT("/avatar/use", h.UseAvatar)
api.GET("/my/media", h.MyMedia)
api.DELETE("/my/attachments/:id", h.DeleteAttachment)
// 群聊(二期):建群/成员/消息/未读
api.GET("/chat/rooms", h.ListChatRooms)
api.POST("/chat/rooms", h.CreateChatRoom)
api.POST("/chat/direct", h.OpenDirectChat)
api.GET("/chat/users", h.SearchChatUsers)
api.GET("/chat/rooms/:id", h.GetChatRoom)
api.PUT("/chat/rooms/:id", h.UpdateChatRoom)
api.PUT("/chat/rooms/:id/pin", h.PinChatRoom)
api.DELETE("/chat/rooms/:id", h.DissolveChatRoom)
api.POST("/chat/rooms/:id/join", h.JoinChatRoom)
api.POST("/chat/rooms/:id/leave", h.LeaveChatRoom)
api.GET("/chat/rooms/:id/members", h.ListChatMembers)
api.POST("/chat/rooms/:id/members", h.InviteChatMembers)
api.DELETE("/chat/rooms/:id/members/:uid", h.KickChatMember)
api.GET("/chat/rooms/:id/messages", h.ListChatMessages)
api.POST("/chat/rooms/:id/messages", middleware.RateLimitMiddleware(limiter, service.RateChat), h.SendChatMessage)
api.DELETE("/chat/rooms/:id/messages/:mid", h.RecallChatMessage)
api.PUT("/chat/rooms/:id/read", h.MarkChatRead)
api.GET("/chat/unread-summary", h.ChatUnreadSummary)
}
// 管理后台 API:RequireStaff 基础鉴权(板块管理员及以上,Actor 实时从 DB 现取),
// 各功能点再由 RequirePerm 按角色放行;板块范围在 service 层按 Actor.BoardIDs 隔离
staffAPI := r.Group("/api/admin", authMW.RequireStaff(), middleware.CSRFMiddleware())
{
staffAPI.GET("/dashboard", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "admin dashboard"})
})
// 内容审核队列(任意管理角色;板块管理员只见授权板块)
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)
// 站点公告文章管理(管理员及以上)
announceAPI := staffAPI.Group("", authMW.RequirePerm(service.PermAnnouncements))
announceAPI.GET("/announcements", h.AdminListAnnouncements)
announceAPI.POST("/announcements", h.AdminCreateAnnouncement)
announceAPI.PUT("/announcements/:id", h.AdminUpdateAnnouncement)
announceAPI.DELETE("/announcements/:id", h.AdminDeleteAnnouncement)
// 站点外观设置(超级管理员/站长)
staffAPI.PUT("/settings", authMW.RequirePerm(service.PermSettings), h.UpdateSettings)
// 用户与权限管理(超级管理员/站长):列表、角色授权、封禁、登录历史
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
}