From 5412122b48fc30c9443788a6cb8e28a4d9f7ebb1 Mon Sep 17 00:00:00 2001 From: JACKYMYPERSON Date: Thu, 25 Sep 2025 18:17:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7=E8=AF=84?= =?UTF-8?q?=E8=AE=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/comments/getcomments.go | 7 - .../comments/getcomments/getcomments.go | 118 ++++++++++ .../comments/publishComments/publish.go | 211 ++++++++++++++++++ router/setupRouter.go | 18 +- 4 files changed, 340 insertions(+), 14 deletions(-) delete mode 100644 controllers/comments/getcomments.go create mode 100644 controllers/comments/getcomments/getcomments.go create mode 100644 controllers/comments/publishComments/publish.go diff --git a/controllers/comments/getcomments.go b/controllers/comments/getcomments.go deleted file mode 100644 index 429a94b..0000000 --- a/controllers/comments/getcomments.go +++ /dev/null @@ -1,7 +0,0 @@ -package comments - -import "github.com/gin-gonic/gin" - -func GetComments(c *gin.Context) { - -} diff --git a/controllers/comments/getcomments/getcomments.go b/controllers/comments/getcomments/getcomments.go new file mode 100644 index 0000000..e687ca4 --- /dev/null +++ b/controllers/comments/getcomments/getcomments.go @@ -0,0 +1,118 @@ +package getcomments + +import ( + "fmt" + "net/http" + "time" + "toutoukan/init/databaseInit" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// ArticleReq 定义请求中文章ID的结构体 +type ArticleReq struct { + ArticleID int64 `json:"articleId" binding:"required,min=1"` +} + +// UserDetail 用户的关键信息,用于嵌入到评论响应中 +type UserDetail struct { + UserID string `gorm:"column:user_id" json:"user_id"` // user_id 来自 comments 表 + Username string `gorm:"column:username" json:"username"` // username 来自 user_info 表 + AvatarURL string `gorm:"column:avatar_url" json:"avatar_url"` // avatar_url 来自 user_info 表 +} + +// CommentResponse 最终的评论响应结构体,包含了用户信息 +type CommentResponse struct { + ID int64 `gorm:"column:id" json:"id"` + ArticleID int64 `gorm:"column:article_id" json:"article_id"` + ParentID int64 `gorm:"column:parent_id" json:"parent_id"` + Content string `gorm:"column:content" json:"content"` + LikesCount int `gorm:"column:likes_count" json:"likes_count"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` + + // 嵌入发布者的信息 + Username string `gorm:"column:username" json:"username"` // 直接映射 user_info.username + AvatarURL string `gorm:"column:avatar_url" json:"avatar_url"` // 直接映射 user_info.avatar_url + + Replies []*CommentResponse `gorm:"-" json:"replies,omitempty"` // 子评论列表,忽略GORM +} + +// TableName 为 Comment 结构体指定数据库表名 (仅供GORM模型参考,实际查询使用原始表名) +func (CommentResponse) TableName() string { + return "article_comments" +} + +// GetComments 获取特定文章的评论列表 +func GetComments(c *gin.Context) { + var req ArticleReq + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "参数解析失败", + "detail": err.Error(), + }) + return + } + + // 1. 联表查询文章的所有评论及其发布者信息 + // 注意:我们直接将结果映射到 CommentResponse 结构体 + var allComments []*CommentResponse + + // GORM 的 JOIN 查询 + // SELECT 语句手动指定了字段,以避免字段名冲突 (如 user_id vs uid) + // 假设 user_info.uid 对应 article_comments.user_id + query := ` + SELECT + c.id, c.articleId, c.user_id, c.parent_id, c.content, c.likes_count, c.created_time, c.update_time, + u.username, u.avatar_url + FROM article_comments c + JOIN user_info u ON c.user_id = u.uid + WHERE c.articleId = ? + ORDER BY c.created_time ASC + ` + + if err := databaseInit.UserDB.Raw(query, req.ArticleID).Scan(&allComments).Error; err != nil { + if err == gorm.ErrRecordNotFound { + // 如果没有找到评论,返回空列表 + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "文章暂无评论", + "data": []CommentResponse{}, + }) + return + } + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("查询评论失败: %s", err.Error()), + }) + return + } + + // 2. 构建评论树(父子关系) + commentMap := make(map[int64]*CommentResponse) + for _, comment := range allComments { + commentMap[comment.ID] = comment + } + + var topLevelComments []*CommentResponse + for _, comment := range allComments { + // ParentID 默认为0表示顶级评论 + if comment.ParentID == 0 { + topLevelComments = append(topLevelComments, comment) + } else { + // 找到父评论,并将其加入子评论列表 + if parent, ok := commentMap[comment.ParentID]; ok { + parent.Replies = append(parent.Replies, comment) + } + // 如果找不到父评论,可能是脏数据,则忽略 + } + } + + // 3. 返回成功响应 + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "获取评论成功", + "data": topLevelComments, + }) +} diff --git a/controllers/comments/publishComments/publish.go b/controllers/comments/publishComments/publish.go new file mode 100644 index 0000000..5fb5335 --- /dev/null +++ b/controllers/comments/publishComments/publish.go @@ -0,0 +1,211 @@ +package publishComments + +import ( + "database/sql" + "fmt" + "net/http" + "time" + "toutoukan/init/databaseInit" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// PublishReq 定义发布评论的请求结构体 +type PublishReq struct { + ArticleID int64 `json:"articleId" binding:"required,min=1"` // 必须是有效的文章ID + ParentID *int64 `json:"parent_id"` // 父评论ID,指针类型,nil表示NULL + Content string `json:"content" binding:"required,min=1,max=500"` + UserID string `json:"user_id" binding:"required,min=1,max=40"` // 用户ID +} + +// CommentModel 对应 article_comments 表的 GORM 模型 +type CommentModel struct { + ID int64 `gorm:"column:id;primaryKey;autoIncrement"` + ArticleID int64 `gorm:"column:articleId"` + UserID string `gorm:"column:user_id"` + ParentID sql.NullInt64 `gorm:"column:parent_id"` + Content string `gorm:"column:content"` + LikesCount int `gorm:"column:likes_count"` + CreatedAt time.Time `gorm:"column:created_time"` + UpdatedAt time.Time `gorm:"column:update_time"` +} + +// UserPoint 用户积分记录表结构 +type UserPoint struct { + UserID string `gorm:"column:user_id"` + PointsChange int `gorm:"column:points_change"` + Source string `gorm:"column:source"` + CreateTime time.Time `gorm:"column:create_time"` +} + +// TableName 为 CommentModel 结构体指定数据库表名 +func (CommentModel) TableName() string { + return "article_comments" +} + +// TableName 为 UserPoint 结构体指定数据库表名 +func (UserPoint) TableName() string { + return "user_points" +} + +// PublishComment 处理发布新评论的请求 +func PublishComment(c *gin.Context) { + const ( + COMMENTER_POINTS = 1 // 评论者获得的积分 + AUTHOR_COMMENT_POINTS = 2 // 文章发布者获得的积分 + ) + + var req PublishReq + + // 1. 解析并验证请求参数 + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "参数校验失败", + "detail": err.Error(), + }) + return + } + + // 2. 开启数据库事务 + tx := databaseInit.UserDB.Begin() + if tx.Error != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "开启事务失败: " + tx.Error.Error()}) + return + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + now := time.Now() + + // 3. 检查文章发布者是否存在,并获取发布者ID + var articleInfo struct { + AuthorID string `gorm:"column:publish_user_id"` + } + if err := tx.Table("article_list"). + Where("articleId = ?", req.ArticleID). + Select("publish_user_id"). + First(&articleInfo).Error; err != nil { + tx.Rollback() + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "文章不存在"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询文章失败: " + err.Error()}) + } + return + } + authorID := articleInfo.AuthorID + + // 3.1 检查评论者UID是否存在(避免外键约束失败) + // 这里的检查只需要确保 req.UserID 存在即可,articleInfo.AuthorID 存在性已在上面检查 + var userCheck struct{ UID string } + if err := tx.Table("user_info").Select("uid").Where("uid = ?", req.UserID).First(&userCheck).Error; err != nil { + tx.Rollback() + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "评论者UID不存在"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询用户失败: " + err.Error()}) + } + return + } + + // 4. 插入评论主记录 + newComment := CommentModel{ + ArticleID: req.ArticleID, + UserID: req.UserID, + Content: req.Content, + LikesCount: 0, + CreatedAt: now, + UpdatedAt: now, + } + + // 处理 ParentID 的 NULL 值 + if req.ParentID != nil && *req.ParentID > 0 { + newComment.ParentID = sql.NullInt64{Int64: *req.ParentID, Valid: true} + } else { + newComment.ParentID = sql.NullInt64{Valid: false} // 设为 NULL + } + + if err := tx.Create(&newComment).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("评论发布失败: %s", err.Error()), + }) + return + } + + // --- 5. 积分处理 (评论者 & 文章作者) --- + + // 5.1 记录评论者积分变动 (user_points) + commenterPoint := UserPoint{ + UserID: req.UserID, + PointsChange: COMMENTER_POINTS, + Source: "publish_comment", + CreateTime: now, + } + if err := tx.Table("user_points").Create(&commenterPoint).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "记录评论者积分变动失败: " + err.Error()}) + return + } + + // 5.2 更新评论者总积分 + if err := tx.Table("user_info"). + Where("uid = ?", req.UserID). + Update("total_points", gorm.Expr("total_points + ?", COMMENTER_POINTS)).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新评论者总积分失败: " + err.Error()}) + return + } + + // 5.3 记录并更新文章发布者积分(如果不是自己评论自己) + if authorID != req.UserID { + // 记录作者积分变动 + authorPoint := UserPoint{ + UserID: authorID, + PointsChange: AUTHOR_COMMENT_POINTS, + Source: "comment_received", + CreateTime: now, + } + if err := tx.Table("user_points").Create(&authorPoint).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "记录作者积分变动失败: " + err.Error()}) + return + } + + // 更新作者总积分 + if err := tx.Table("user_info"). + Where("uid = ?", authorID). + Update("total_points", gorm.Expr("total_points + ?", AUTHOR_COMMENT_POINTS)).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新作者总积分失败: " + err.Error()}) + return + } + } + + // 6. 提交事务 + if err := tx.Commit().Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "提交评论失败: " + err.Error()}) + return + } + + // 7. 返回成功响应 + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": fmt.Sprintf("评论发布成功,您获得了 %d 积分", COMMENTER_POINTS), + "data": gin.H{ + "commentId": newComment.ID, + "articleId": newComment.ArticleID, + "author_points_awarded": func() int { + if authorID != req.UserID { + return AUTHOR_COMMENT_POINTS + } + return 0 + }(), + }, + }) +} diff --git a/router/setupRouter.go b/router/setupRouter.go index 3abdfba..77df8fb 100644 --- a/router/setupRouter.go +++ b/router/setupRouter.go @@ -1,17 +1,16 @@ package router import ( + "github.com/gin-gonic/gin" "toutoukan/controllers/article" + "toutoukan/controllers/comments/getcomments" + "toutoukan/controllers/comments/publishComments" "toutoukan/controllers/goods" "toutoukan/controllers/kills" "toutoukan/controllers/search" "toutoukan/controllers/system" "toutoukan/controllers/user" "toutoukan/init/ratelimit" - "toutoukan/socket" - "toutoukan/utill/jwt" - - "github.com/gin-gonic/gin" ) func SetupRouter() *gin.Engine { @@ -25,9 +24,9 @@ func SetupRouter() *gin.Engine { apiGroup.POST("/getInfo", user.GetUserInfo) } - r.GET("/socket", jwt.JWTAuthMiddleware(), func(c *gin.Context) { - socket.WebsocketHandler(c) - }) + //r.GET("/socket", jwt.JWTAuthMiddleware(), func(c *gin.Context) { + // socket.WebsocketHandler(c) + //}) systemGroup := r.Group("/system") { systemGroup.POST("/sendMsg", system.SendMsg) @@ -49,6 +48,11 @@ func SetupRouter() *gin.Engine { { goodsGroup.GET("/getgoodslist", goods.GetGoods) } + commentGroup := r.Group("/comment") + { + commentGroup.POST("/get", getcomments.GetComments) + commentGroup.POST("/publish", publishComments.PublishComment) + } return r }