添加用户评论功能
This commit is contained in:
@@ -1,7 +0,0 @@
|
|||||||
package comments
|
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
func GetComments(c *gin.Context) {
|
|
||||||
|
|
||||||
}
|
|
||||||
118
controllers/comments/getcomments/getcomments.go
Normal file
118
controllers/comments/getcomments/getcomments.go
Normal file
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
211
controllers/comments/publishComments/publish.go
Normal file
211
controllers/comments/publishComments/publish.go
Normal file
@@ -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
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"toutoukan/controllers/article"
|
"toutoukan/controllers/article"
|
||||||
|
"toutoukan/controllers/comments/getcomments"
|
||||||
|
"toutoukan/controllers/comments/publishComments"
|
||||||
"toutoukan/controllers/goods"
|
"toutoukan/controllers/goods"
|
||||||
"toutoukan/controllers/kills"
|
"toutoukan/controllers/kills"
|
||||||
"toutoukan/controllers/search"
|
"toutoukan/controllers/search"
|
||||||
"toutoukan/controllers/system"
|
"toutoukan/controllers/system"
|
||||||
"toutoukan/controllers/user"
|
"toutoukan/controllers/user"
|
||||||
"toutoukan/init/ratelimit"
|
"toutoukan/init/ratelimit"
|
||||||
"toutoukan/socket"
|
|
||||||
"toutoukan/utill/jwt"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupRouter() *gin.Engine {
|
func SetupRouter() *gin.Engine {
|
||||||
@@ -25,9 +24,9 @@ func SetupRouter() *gin.Engine {
|
|||||||
apiGroup.POST("/getInfo", user.GetUserInfo)
|
apiGroup.POST("/getInfo", user.GetUserInfo)
|
||||||
|
|
||||||
}
|
}
|
||||||
r.GET("/socket", jwt.JWTAuthMiddleware(), func(c *gin.Context) {
|
//r.GET("/socket", jwt.JWTAuthMiddleware(), func(c *gin.Context) {
|
||||||
socket.WebsocketHandler(c)
|
// socket.WebsocketHandler(c)
|
||||||
})
|
//})
|
||||||
systemGroup := r.Group("/system")
|
systemGroup := r.Group("/system")
|
||||||
{
|
{
|
||||||
systemGroup.POST("/sendMsg", system.SendMsg)
|
systemGroup.POST("/sendMsg", system.SendMsg)
|
||||||
@@ -49,6 +48,11 @@ func SetupRouter() *gin.Engine {
|
|||||||
{
|
{
|
||||||
goodsGroup.GET("/getgoodslist", goods.GetGoods)
|
goodsGroup.GET("/getgoodslist", goods.GetGoods)
|
||||||
}
|
}
|
||||||
|
commentGroup := r.Group("/comment")
|
||||||
|
{
|
||||||
|
commentGroup.POST("/get", getcomments.GetComments)
|
||||||
|
commentGroup.POST("/publish", publishComments.PublishComment)
|
||||||
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user