mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-03 15:56:22 +00:00
✨ feat: 添加评论功能,包括评论输入、评论列表和评论项组件,支持层级深度和私密评论
This commit is contained in:
@ -89,6 +89,11 @@ func (cc *CommentController) GetComment(ctx context.Context, c *app.RequestConte
|
||||
}
|
||||
|
||||
func (cc *CommentController) GetCommentList(ctx context.Context, c *app.RequestContext) {
|
||||
depth := c.Query("depth")
|
||||
depthInt, err := strconv.Atoi(depth)
|
||||
if err != nil || depthInt < 0 {
|
||||
depthInt = 1
|
||||
}
|
||||
pagination := ctxutils.GetPaginationParams(c)
|
||||
if pagination.OrderBy == "" {
|
||||
pagination.OrderBy = constant.OrderByUpdatedAt
|
||||
@ -107,6 +112,7 @@ func (cc *CommentController) GetCommentList(ctx context.Context, c *app.RequestC
|
||||
OrderBy: pagination.OrderBy,
|
||||
Page: pagination.Page,
|
||||
Size: pagination.Size,
|
||||
Depth: depthInt,
|
||||
TargetID: uint(targetID),
|
||||
TargetType: c.Query("target_type"),
|
||||
}
|
||||
|
@ -17,19 +17,21 @@ type CreateCommentReq struct {
|
||||
TargetType string `json:"target_type" binding:"required"` // 目标类型,如 "post", "page"
|
||||
Content string `json:"content" binding:"required"` // 评论内容
|
||||
ReplyID uint `json:"reply_id"` // 回复的评论ID
|
||||
IsPrivate bool `json:"is_private" binding:"required"` // 是否私密
|
||||
IsPrivate bool `json:"is_private"` // 是否私密评论,默认false
|
||||
}
|
||||
|
||||
type UpdateCommentReq struct {
|
||||
CommentID uint `json:"comment_id" binding:"required"` // 评论ID
|
||||
Content string `json:"content" binding:"required"` // 评论内容
|
||||
IsPrivate bool `json:"is_private"` // 是否私密
|
||||
}
|
||||
|
||||
type GetCommentListReq struct {
|
||||
TargetID uint `json:"target_id" binding:"required"`
|
||||
TargetID uint `json:"target_id" binding:"required"`
|
||||
TargetType string `json:"target_type" binding:"required"`
|
||||
OrderBy string `json:"order_by"` // 排序方式
|
||||
Page uint64 `json:"page"` // 页码
|
||||
Size uint64 `json:"size"`
|
||||
Desc bool `json:"desc"`
|
||||
}
|
||||
OrderBy string `json:"order_by"` // 排序方式
|
||||
Page uint64 `json:"page"` // 页码
|
||||
Size uint64 `json:"size"`
|
||||
Desc bool `json:"desc"`
|
||||
Depth int `json:"depth"` // 评论的层级深度
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/snowykami/neo-blog/internal/ctxutils"
|
||||
"github.com/snowykami/neo-blog/internal/repo"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
@ -15,7 +16,7 @@ import (
|
||||
func UseAuth(block bool) app.HandlerFunc {
|
||||
return func(ctx context.Context, c *app.RequestContext) {
|
||||
// For cookie
|
||||
tokenFromCookie := string(c.Cookie("tokenFromCookie"))
|
||||
tokenFromCookie := string(c.Cookie("token"))
|
||||
tokenFromHeader := strings.TrimPrefix(string(c.GetHeader("Authorization")), "Bearer ")
|
||||
refreshToken := string(c.Cookie("refresh_token"))
|
||||
|
||||
@ -25,8 +26,10 @@ func UseAuth(block bool) app.HandlerFunc {
|
||||
if err == nil && tokenClaims != nil {
|
||||
ctx = context.WithValue(ctx, constant.ContextKeyUserID, tokenClaims.UserID)
|
||||
c.Next(ctx)
|
||||
logrus.Debugf("UseAuth: tokenFromCookie authenticated successfully, userID: %d", tokenClaims.UserID)
|
||||
return
|
||||
}
|
||||
logrus.Debugf("UseAuth: tokenFromCookie authentication failed, error: %v", err)
|
||||
}
|
||||
// tokenFromCookie 认证失败,尝试用 Bearer tokenFromHeader 认证
|
||||
if tokenFromHeader != "" {
|
||||
@ -34,8 +37,10 @@ func UseAuth(block bool) app.HandlerFunc {
|
||||
if err == nil && tokenClaims != nil {
|
||||
ctx = context.WithValue(ctx, constant.ContextKeyUserID, tokenClaims.UserID)
|
||||
c.Next(ctx)
|
||||
logrus.Debugf("UseAuth: tokenFromHeader authenticated successfully, userID: %d", tokenClaims.UserID)
|
||||
return
|
||||
}
|
||||
logrus.Debugf("UseAuth: tokenFromHeader authentication failed, error: %v", err)
|
||||
}
|
||||
// tokenFromCookie 失效 使用 refresh tokenFromCookie 重新签发和鉴权
|
||||
refreshTokenClaims, err := utils.Jwt.ParseJsonWebTokenWithoutState(refreshToken)
|
||||
@ -56,19 +61,19 @@ func UseAuth(block bool) app.HandlerFunc {
|
||||
} else {
|
||||
resps.InternalServerError(c, resps.ErrInternalServerError)
|
||||
}
|
||||
|
||||
c.Next(ctx)
|
||||
logrus.Debugf("UseAuth: refreshToken authenticated successfully, userID: %d", refreshTokenClaims.UserID)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 所有认证方式都失败
|
||||
if block {
|
||||
// 若需要阻断,返回未授权错误并中止请求
|
||||
logrus.Debug("UseAuth: all authentication methods failed, blocking request")
|
||||
resps.Unauthorized(c, resps.ErrUnauthorized)
|
||||
c.Abort()
|
||||
} else {
|
||||
// 若不需要阻断,继续请求但不设置用户ID
|
||||
logrus.Debug("UseAuth: all authentication methods failed, blocking request")
|
||||
c.Next(ctx)
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ type Comment struct {
|
||||
TargetType string `gorm:"index"` // 目标类型,如 "post", "page"
|
||||
ReplyID uint `gorm:"index"` // 回复的评论ID
|
||||
Content string `gorm:"type:text"` // 评论内容
|
||||
Depth int `gorm:"default:0"` // 评论的层级深度
|
||||
Depth int `gorm:"default:0"` // 评论的层级深度,从0开始计数
|
||||
IsPrivate bool `gorm:"default:false"` // 是否为私密评论,私密评论只有评论者和被评论对象所有者可见
|
||||
LikeCount uint64
|
||||
CommentCount uint64
|
||||
|
@ -48,7 +48,6 @@ func (cr *CommentRepo) isCircularReference(tx *gorm.DB, commentID, parentID uint
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
// 递归删除子评论的辅助函数
|
||||
func (cr *CommentRepo) deleteChildren(tx *gorm.DB, parentID uint) error {
|
||||
var children []*model.Comment
|
||||
@ -99,7 +98,7 @@ func (cr *CommentRepo) CreateComment(comment *model.Comment) error {
|
||||
if err := tx.Create(comment).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -173,7 +172,7 @@ func (cr *CommentRepo) GetComment(commentID string) (*model.Comment, error) {
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
func (cr *CommentRepo) ListComments(currentUserID uint, targetID uint, targetType string, page, size uint64, orderBy string, desc bool) ([]model.Comment, error) {
|
||||
func (cr *CommentRepo) ListComments(currentUserID uint, targetID uint, targetType string, page, size uint64, orderBy string, desc bool, depth int) ([]model.Comment, error) {
|
||||
if !slices.Contains(constant.OrderByEnumComment, orderBy) {
|
||||
return nil, errs.New(http.StatusBadRequest, "invalid order_by parameter", nil)
|
||||
}
|
||||
@ -196,9 +195,13 @@ func (cr *CommentRepo) ListComments(currentUserID uint, targetID uint, targetTyp
|
||||
query = query.Where("is_private = ?", false)
|
||||
}
|
||||
|
||||
query = query.Where("target_id = ? AND target_type = ?", targetID, targetType)
|
||||
|
||||
if depth > 0 {
|
||||
query = query.Where("target_id = ? AND target_type = ? AND depth = ?", targetID, targetType, depth)
|
||||
} else {
|
||||
query = query.Where("target_id = ? AND target_type = ?", targetID, targetType)
|
||||
}
|
||||
items, _, err := PaginateQuery[model.Comment](query, page, size, orderBy, desc)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -8,14 +8,14 @@ import (
|
||||
|
||||
func registerCommentRoutes(group *route.RouterGroup) {
|
||||
commentController := v1.NewCommentController()
|
||||
commentGroup := group.Group("/comments").Use(middleware.UseAuth(true))
|
||||
commentGroupWithoutAuth := group.Group("/comments").Use(middleware.UseAuth(false))
|
||||
commentGroup := group.Group("/comment").Use(middleware.UseAuth(true))
|
||||
commentGroupWithoutAuth := group.Group("/comment").Use(middleware.UseAuth(false))
|
||||
{
|
||||
commentGroup.POST("/c", commentController.CreateComment)
|
||||
commentGroup.PUT("/c/:id", commentController.UpdateComment)
|
||||
commentGroup.DELETE("/c/:id", commentController.DeleteComment)
|
||||
commentGroup.PUT("/c/:id/react", commentController.ReactComment) // 暂时先不写
|
||||
commentGroupWithoutAuth.GET("/c/:id", commentController.GetComment)
|
||||
commentGroupWithoutAuth.GET("/c/list", commentController.GetCommentList)
|
||||
commentGroupWithoutAuth.GET("/list", commentController.GetCommentList)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/snowykami/neo-blog/pkg/constant"
|
||||
"strconv"
|
||||
|
||||
"github.com/snowykami/neo-blog/internal/ctxutils"
|
||||
@ -23,6 +24,13 @@ func (cs *CommentService) CreateComment(ctx context.Context, req *dto.CreateComm
|
||||
return errs.ErrUnauthorized
|
||||
}
|
||||
|
||||
if ok, err := cs.checkTargetExists(req.TargetID, req.TargetType); !ok {
|
||||
if err != nil {
|
||||
return errs.New(errs.ErrBadRequest.Code, "target not found", err)
|
||||
}
|
||||
return errs.ErrBadRequest
|
||||
}
|
||||
|
||||
comment := &model.Comment{
|
||||
Content: req.Content,
|
||||
ReplyID: req.ReplyID,
|
||||
@ -57,6 +65,7 @@ func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateComm
|
||||
}
|
||||
|
||||
comment.Content = req.Content
|
||||
comment.IsPrivate = req.IsPrivate
|
||||
|
||||
err = repo.Comment.UpdateComment(comment)
|
||||
|
||||
@ -117,7 +126,7 @@ func (cs *CommentService) GetComment(ctx context.Context, commentID string) (*dt
|
||||
func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommentListReq) ([]dto.CommentDto, error) {
|
||||
currentUser, _ := ctxutils.GetCurrentUser(ctx)
|
||||
|
||||
comments, err := repo.Comment.ListComments(currentUser.ID, req.TargetID, req.TargetType, req.Page, req.Size, req.OrderBy, req.Desc)
|
||||
comments, err := repo.Comment.ListComments(currentUser.ID, req.TargetID, req.TargetType, req.Page, req.Size, req.OrderBy, req.Desc, req.Depth)
|
||||
if err != nil {
|
||||
return nil, errs.New(errs.ErrInternalServer.Code, "failed to list comments", err)
|
||||
}
|
||||
@ -139,3 +148,15 @@ func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommen
|
||||
}
|
||||
return commentDtos, nil
|
||||
}
|
||||
|
||||
func (cs *CommentService) checkTargetExists(targetID uint, targetType string) (bool, error) {
|
||||
switch targetType {
|
||||
case constant.TargetTypePost:
|
||||
if _, err := repo.Post.GetPostByID(strconv.Itoa(int(targetID))); err != nil {
|
||||
return false, errs.New(errs.ErrNotFound.Code, "post not found", err)
|
||||
}
|
||||
default:
|
||||
return false, errs.New(errs.ErrBadRequest.Code, "invalid target type", nil)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
@ -352,12 +352,12 @@ func (s *UserService) UpdateUser(req *dto.UpdateUserReq) (*dto.UpdateUserResp, e
|
||||
}
|
||||
|
||||
func (s *UserService) generate2Token(userID uint) (string, string, error) {
|
||||
token := utils.Jwt.NewClaims(userID, "", false, time.Duration(utils.Env.GetAsInt(constant.EnvKeyTokenDuration, constant.EnvKeyTokenDurationDefault)*int(time.Second)))
|
||||
token := utils.Jwt.NewClaims(userID, "", false, time.Duration(utils.Env.GetAsInt(constant.EnvKeyTokenDuration, constant.EnvKeyTokenDurationDefault))*time.Second)
|
||||
tokenString, err := token.ToString()
|
||||
if err != nil {
|
||||
return "", "", errs.ErrInternalServer
|
||||
}
|
||||
refreshToken := utils.Jwt.NewClaims(userID, utils.Strings.GenerateRandomString(64), true, time.Duration(utils.Env.GetAsInt(constant.EnvKeyRefreshTokenDuration, constant.EnvKeyRefreshTokenDurationDefault)*int(time.Second)))
|
||||
refreshToken := utils.Jwt.NewClaims(userID, utils.Strings.GenerateRandomString(64), true, time.Duration(utils.Env.GetAsInt(constant.EnvKeyRefreshTokenDuration, constant.EnvKeyRefreshTokenDurationDefault))*time.Second)
|
||||
refreshTokenString, err := refreshToken.ToString()
|
||||
if err != nil {
|
||||
return "", "", errs.ErrInternalServer
|
||||
|
Reference in New Issue
Block a user