Version: 0.9.78.dev.260506

This commit is contained in:
Losita
2026-05-06 00:30:08 +08:00
parent 3b6fca44a6
commit 33227e48a7
71 changed files with 13137 additions and 62 deletions

View File

@@ -0,0 +1,87 @@
package forumapi
import (
"context"
"errors"
"net/http"
"time"
"github.com/LoveLosita/smartflow/backend/services/runtime/dao"
gatewaymiddleware "github.com/LoveLosita/smartflow/backend/gateway/middleware"
rootmiddleware "github.com/LoveLosita/smartflow/backend/gateway/middleware"
"github.com/LoveLosita/smartflow/backend/gateway/shared/respond"
ratelimit "github.com/LoveLosita/smartflow/backend/shared/infra/ratelimit"
"github.com/LoveLosita/smartflow/backend/shared/ports"
"github.com/gin-gonic/gin"
)
// RegisterRoutes 把计划广场 HTTP 入口挂到 gateway 路由组。
//
// 职责边界:
// 1. 只注册 /plan-square 下的边缘路由,不承载论坛业务规则;
// 2. 公开读接口允许匿名访问,若携带 token 则补齐 viewer_state
// 3. 写接口必须登录,并按既有 Redis 幂等中间件保护重复提交。
func RegisterRoutes(apiGroup *gin.RouterGroup, handler *Handler, authClient ports.AccessTokenValidator, cache *dao.CacheDAO, limiter *ratelimit.RateLimiter) {
if apiGroup == nil || handler == nil {
return
}
planSquare := apiGroup.Group("/plan-square")
{
publicGroup := planSquare.Group("")
publicGroup.Use(optionalJWTTokenAuth(authClient), rootmiddleware.RateLimitMiddleware(limiter, 40, 1))
publicGroup.GET("/posts", handler.ListPosts)
publicGroup.GET("/tags", handler.ListTags)
publicGroup.GET("/posts/:post_id", handler.GetPost)
publicGroup.GET("/posts/:post_id/comments", handler.ListComments)
writeGroup := planSquare.Group("")
writeGroup.Use(gatewaymiddleware.JWTTokenAuth(authClient), rootmiddleware.RateLimitMiddleware(limiter, 20, 1))
writeGroup.POST("/posts", rootmiddleware.IdempotencyMiddleware(cache), handler.CreatePost)
writeGroup.POST("/posts/:post_id/like", handler.LikePost)
writeGroup.DELETE("/posts/:post_id/like", handler.UnlikePost)
writeGroup.POST("/posts/:post_id/comments", rootmiddleware.IdempotencyMiddleware(cache), handler.CreateComment)
writeGroup.DELETE("/comments/:comment_id", handler.DeleteComment)
writeGroup.POST("/posts/:post_id/import", rootmiddleware.IdempotencyMiddleware(cache), handler.ImportPost)
}
}
// optionalJWTTokenAuth 为计划广场公开读接口提供“可登录增强”。
//
// 步骤说明:
// 1. 没有 Authorization 时直接放行,让匿名用户也能浏览计划广场;
// 2. 有 Authorization 时复用 user/auth 校验,并把 user_id 写入上下文;
// 3. token 非法时按正常鉴权失败返回,避免前端误以为已登录状态仍可用。
func optionalJWTTokenAuth(validator ports.AccessTokenValidator) gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := gatewaymiddleware.ExtractTokenFromAuthorization(c.GetHeader("Authorization"))
if tokenString == "" {
c.Next()
return
}
if validator == nil {
c.JSON(http.StatusInternalServerError, respond.InternalError(errors.New("计划广场可选鉴权依赖未初始化")))
c.Abort()
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
defer cancel()
resp, err := validator.ValidateAccessToken(ctx, tokenString)
if err != nil {
respond.DealWithError(c, err)
c.Abort()
return
}
if resp == nil || !resp.Valid || resp.UserID <= 0 {
c.JSON(http.StatusUnauthorized, respond.InvalidClaims)
c.Abort()
return
}
c.Set("user_id", resp.UserID)
c.Set("claims", resp)
c.Next()
}
}