diff --git a/controllers/article/createarticle.go b/controllers/article/createarticle.go index ac3a247..27c4ae9 100644 --- a/controllers/article/createarticle.go +++ b/controllers/article/createarticle.go @@ -1,11 +1,12 @@ package article import ( + "fmt" + "github.com/gin-gonic/gin" + "gorm.io/gorm" "net/http" "time" "toutoukan/init/databaseInit" - - "github.com/gin-gonic/gin" ) // OptionItem 选项子结构 @@ -23,25 +24,33 @@ type CreateArticleRequest struct { Options []OptionItem `json:"options" binding:"required,min=1,max=3"` } -// 数据库文章记录结构体(与表结构完全对应) -type ArticleList struct { - ArticleID int64 `gorm:"column:articleId;primaryKey;autoIncrement"` // 明确主键和自增 - PublishUserID string `gorm:"column:publish_user_id"` - Title string `gorm:"column:title"` - VoteType string `gorm:"column:vote_type"` - EndTime time.Time `gorm:"column:end_time"` - IsEnded bool `gorm:"column:is_ended"` - TotalVotersNum int `gorm:"column:total_voters_num"` - CreateTime time.Time `gorm:"column:create_time"` +// ArticleList 数据库文章记录结构体 + +// UserPoint 用户积分记录表结构 + +// UserInfo 用户信息表结构 +type UserInfo struct { + UID string `gorm:"column:uid;primaryKey"` + TotalPoints int `gorm:"column:total_points"` } -// 自定义表名(如果结构体名与表名不一致) +// 自定义表名,GORM会根据这些结构体进行操作 func (ArticleList) TableName() string { return "article_list" } +func (UserPoint) TableName() string { + return "user_points" +} +func (UserInfo) TableName() string { + return "user_info" +} // CreateArticle 创建文章(包含选项) func CreateArticle(c *gin.Context) { + const ( + POST_ARTICLE_POINTS = 2 // 发表文章获得的积分 + ) + var req CreateArticleRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -52,6 +61,7 @@ func CreateArticle(c *gin.Context) { return } + // 开启数据库事务 tx := databaseInit.UserDB.Begin() if tx.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "开启事务失败: " + tx.Error.Error()}) @@ -63,7 +73,21 @@ func CreateArticle(c *gin.Context) { } }() - // 1. 创建文章主记录 + // 1. 检查文章发布者是否存在 + var userCheck UserInfo + if err := tx.Table("user_info"). + Where("uid = ?", req.PublishUserID). + 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 + } + + // 2. 创建文章主记录 newArticle := ArticleList{ PublishUserID: req.PublishUserID, Title: req.Title, @@ -73,53 +97,58 @@ func CreateArticle(c *gin.Context) { TotalVotersNum: 0, CreateTime: time.Now(), } - - // 插入文章并获取ID(使用GORM的Create方法,自动填充自增ID) if err := tx.Create(&newArticle).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "创建文章失败: " + err.Error()}) return } - // 验证文章ID是否有效(必须大于0) if newArticle.ArticleID <= 0 { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取文章ID,创建失败"}) return } - // 2. 批量创建选项(确保vote_article_id正确关联) + // 3. 批量创建选项 var options []map[string]interface{} for _, opt := range req.Options { options = append(options, map[string]interface{}{ - "vote_article_id": newArticle.ArticleID, // 使用刚创建的文章ID + "vote_article_id": newArticle.ArticleID, "option_content": opt.Content, "option_votes_num": 0, "sort_order": opt.SortOrder, }) } - - // 插入选项前先验证文章ID是否存在(额外保险) - var count int64 - if err := tx.Model(&ArticleList{}).Where("articleId = ?", newArticle.ArticleID).Count(&count).Error; err != nil { - tx.Rollback() - c.JSON(http.StatusInternalServerError, gin.H{"error": "验证文章ID失败: " + err.Error()}) - return - } - if count == 0 { - tx.Rollback() - c.JSON(http.StatusInternalServerError, gin.H{"error": "文章ID不存在,外键约束失败"}) - return - } - - // 批量插入选项 if err := tx.Table("article_options").CreateInBatches(options, len(options)).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "创建选项失败: " + err.Error()}) return } - // 提交事务 + // 4. 记录文章发布者积分变动 + userPoint := UserPoint{ + UserID: req.PublishUserID, + PointsChange: POST_ARTICLE_POINTS, + Source: "publish_article", + CreateTime: time.Now(), + } + if err := tx.Table("user_points").Create(&userPoint).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "记录积分变动失败: " + err.Error()}) + return + } + + // 5. 更新 user_info 表中的 total_points 字段 + // 使用 UpdateColumn 来确保原子性,并避免 GORM 的其他回调 + if err := tx.Table("user_info"). + Where("uid = ?", req.PublishUserID). + UpdateColumn("total_points", gorm.Expr("total_points + ?", POST_ARTICLE_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()}) @@ -128,7 +157,7 @@ func CreateArticle(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "success": true, - "message": "文章及选项创建成功", + "message": "文章及选项创建成功,您获得了" + fmt.Sprintf("%d", POST_ARTICLE_POINTS) + "积分", "data": gin.H{ "article_id": newArticle.ArticleID, "option_count": len(req.Options), diff --git a/controllers/article/deletearticle.go b/controllers/article/deletearticle.go index 5eeb9f5..158be79 100644 --- a/controllers/article/deletearticle.go +++ b/controllers/article/deletearticle.go @@ -9,14 +9,14 @@ import ( // DeleteArticleRequest 删除文章的请求参数 type DeleteArticleRequest struct { - ArticleID int64 `json:"article_id" binding:"required,min=1"` // 要删除的文章ID + ArticleID int64 `json:"article_id" binding:"required,min=1"` } -// DeleteArticle 删除文章(处理外键关联) +// DeleteArticle 删除文章(如果数据库支持级联删除) func DeleteArticle(c *gin.Context) { var req DeleteArticleRequest - // 解析请求参数 + // 1. 解析请求参数 if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "参数解析失败", @@ -25,7 +25,7 @@ func DeleteArticle(c *gin.Context) { return } - // 开启事务(确保所有删除操作要么全部成功,要么全部失败) + // 2. 开启数据库事务 tx := databaseInit.UserDB.Begin() if tx.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{ @@ -39,29 +39,7 @@ func DeleteArticle(c *gin.Context) { } }() - // 步骤1:先删除用户投票记录(因为依赖文章ID和选项ID) - if err := tx.Table("user_votes"). - Where("vote_article_id = ?", req.ArticleID). - Delete(nil).Error; err != nil { - tx.Rollback() - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "删除用户投票记录失败: " + err.Error(), - }) - return - } - - // 步骤2:再删除文章选项(因为依赖文章ID) - if err := tx.Table("article_options"). - Where("vote_article_id = ?", req.ArticleID). - Delete(nil).Error; err != nil { - tx.Rollback() - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "删除文章选项失败: " + err.Error(), - }) - return - } - - // 步骤3:最后删除文章主记录 + // 3. 只删除文章主记录,由数据库自动处理关联数据 if err := tx.Table("article_list"). Where("articleId = ?", req.ArticleID). Delete(nil).Error; err != nil { @@ -72,7 +50,7 @@ func DeleteArticle(c *gin.Context) { return } - // 提交事务 + // 4. 提交事务 if err := tx.Commit().Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{ @@ -81,7 +59,7 @@ func DeleteArticle(c *gin.Context) { return } - // 返回成功响应 + // 5. 返回成功响应 c.JSON(http.StatusOK, gin.H{ "success": true, "message": "文章及关联数据已全部删除", diff --git a/controllers/article/getarticle.go b/controllers/article/getarticle.go index 6a09609..17020ce 100644 --- a/controllers/article/getarticle.go +++ b/controllers/article/getarticle.go @@ -3,37 +3,64 @@ package article import ( "fmt" "net/http" + "time" "toutoukan/init/databaseInit" "github.com/gin-gonic/gin" "gorm.io/gorm" ) +// ArticleList 数据库文章记录结构体 +// 添加了与 ArticleOption 的关联 +type ArticleList struct { + ArticleID int64 `gorm:"column:articleId;primaryKey;autoIncrement"` + PublishUserID string `gorm:"column:publish_user_id"` + Title string `gorm:"column:title"` + VoteType string `gorm:"column:vote_type"` + EndTime time.Time `gorm:"column:end_time"` + IsEnded bool `gorm:"column:is_ended"` + TotalVotersNum int `gorm:"column:total_voters_num"` + CreateTime time.Time `gorm:"column:create_time"` + // 这是关键:定义 Has Many 关联,表示一篇文章有多个选项 + Options []ArticleOption `gorm:"foreignKey:VoteArticleID;references:ArticleID"` +} + // ArticleOption 文评选项结构 type ArticleOption struct { - ID int64 `json:"id"` // 选项ID - Name string `json:"name"` // 选项名称 - Votes int `json:"votes"` // 该选项的投票数 - IsVotedByUser bool `json:"is_voted"` // 当前用户是否投了该选项 + ID int64 `gorm:"column:id"` + VoteArticleID int64 `gorm:"column:vote_article_id"` // 外键,关联 ArticleList + OptionContent string `gorm:"column:option_content"` + OptionVotesNum int `gorm:"column:option_votes_num"` + SortOrder int `gorm:"column:sort_order"` +} + +// UserVote 用户投票记录表结构 + +// ArticleOptionResp 格式化后的选项响应结构 +type ArticleOptionResp struct { + ID int64 `json:"id"` + Name string `json:"name"` + Votes int `json:"votes"` + IsVotedByUser bool `json:"is_voted"` } // ArticleResponse 单个文评的响应结构 type ArticleResponse struct { - ID int64 `json:"文评ID"` // 文评唯一ID - Title string `json:"文评标题"` // 文评标题 - VoteType string `json:"投票类型"` // 投票类型 - TotalVoters int `json:"总投票人数"` // 总投票人数 - EndTime string `json:"结束时间"` // 结束时间 - IsEnded bool `json:"是否结束"` // 是否结束 - PublisherID string `json:"发布者ID"` // 发布者ID - CreateTime string `json:"创建时间"` // 创建时间 - Options []ArticleOption `json:"选项"` // 按顺序排列的选项列表 - UserHasVoted bool `json:"用户是否已投票"` // 当前用户是否投过票 - VotedOptionIDs []int64 `json:"用户投票的选项ID"` // 若已投票,记录所有选项ID(未投票为空数组) + ID int64 `json:"文评ID"` + Title string `json:"文评标题"` + VoteType string `json:"投票类型"` + TotalVoters int `json:"总投票人数"` + EndTime string `json:"结束时间"` + IsEnded bool `json:"是否结束"` + PublisherID string `json:"发布者ID"` + CreateTime string `json:"创建时间"` + Options []ArticleOptionResp `json:"选项"` + UserHasVoted bool `json:"用户是否已投票"` + VotedOptionIDs []int64 `json:"用户投票的选项ID"` } type UserReq struct { - Uid string `json:"uid"` + Uid string `json:"uid" binding:"required"` } // ArticleListget 获取所有文评及选项信息(包含用户投票状态) @@ -49,20 +76,12 @@ func ArticleListget(c *gin.Context) { return } - // 2. 查询所有文评 - var articles []struct { - ArticleID int64 `gorm:"column:articleId"` - Title string `gorm:"column:title"` - VoteType string `gorm:"column:vote_type"` - TotalVotersNum int `gorm:"column:total_voters_num"` - EndTime string `gorm:"column:end_time"` - IsEnded bool `gorm:"column:is_ended"` - PublishUserID string `gorm:"column:publish_user_id"` - CreateTime string `gorm:"column:create_time"` - } - - if err := databaseInit.UserDB.Table("article_list"). - Select("articleId, title, vote_type, total_voters_num, end_time, is_ended, publish_user_id, create_time"). + // 2. 使用 Preload 一次性查询所有文章和它们的选项 + var articles []ArticleList + if err := databaseInit.UserDB.Model(&ArticleList{}). + Preload("Options", func(db *gorm.DB) *gorm.DB { + return db.Order("sort_order ASC") + }). Find(&articles).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "查询文评失败: " + err.Error(), @@ -70,62 +89,28 @@ func ArticleListget(c *gin.Context) { return } - fmt.Println("所有文评列表:", articles) + // 3. 一次性查询当前用户的所有投票记录,并构建一个 map 方便查找 + var userAllVotes []UserVote + if err := databaseInit.UserDB.Table("user_votes").Where("user_id = ?", req.Uid).Find(&userAllVotes).Error; err != nil && err != gorm.ErrRecordNotFound { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "查询用户投票记录失败: " + err.Error(), + }) + return + } + // 投票记录 map:key: articleId, value: []optionId + userVotesMap := make(map[int64][]int64) + for _, vote := range userAllVotes { + userVotesMap[vote.VoteArticleID] = append(userVotesMap[vote.VoteArticleID], vote.OptionID) + } - // 3. 构建结果映射 - result := make(map[string]interface{}) - - // 4. 逐个处理文评 + // 4. 组装最终响应数据 + var responseList []ArticleResponse for _, article := range articles { - // 4.1 查询当前文评的选项 - var options []struct { - ID int64 `gorm:"column:id"` - OptionName string `gorm:"column:option_content"` - VoteCount int `gorm:"column:option_votes_num"` - SortOrder int `gorm:"column:sort_order"` - } + votedOptionIDs, userHasVoted := userVotesMap[article.ArticleID] - if err := databaseInit.UserDB.Table("article_options"). - Where("vote_article_id = ?", article.ArticleID). - Select("id, option_content, option_votes_num, sort_order"). - Order("sort_order ASC"). - Find(&options).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "查询文评选项失败: " + err.Error(), - }) - return - } - - // 4.2 查询当前用户对该文评的所有投票记录 - var userVotes []struct { - OptionID int64 `gorm:"column:option_id"` - } - voteErr := databaseInit.UserDB.Table("user_votes"). - Where("user_id = ? AND vote_article_id = ?", req.Uid, article.ArticleID). - Find(&userVotes).Error - - // 标记用户是否投票及所有投票的选项ID - userHasVoted := false - var votedOptionIDs []int64 = []int64{} - - if voteErr == nil && len(userVotes) > 0 { - userHasVoted = true - // 收集所有投票的选项ID - for _, vote := range userVotes { - votedOptionIDs = append(votedOptionIDs, vote.OptionID) - } - } else if voteErr != nil && voteErr != gorm.ErrRecordNotFound { - // 处理查询错误(非"未找到"的错误) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "查询用户投票记录失败: " + voteErr.Error(), - }) - return - } - - // 4.3 格式化选项数据(标记用户是否投了该选项) - var optionList []ArticleOption - for _, opt := range options { - // 检查当前选项是否是用户投票的选项之一 + // 格式化选项数据 + var optionList []ArticleOptionResp + for _, opt := range article.Options { isVoted := false for _, votedID := range votedOptionIDs { if opt.ID == votedID { @@ -133,33 +118,33 @@ func ArticleListget(c *gin.Context) { break } } - - optionList = append(optionList, ArticleOption{ + optionList = append(optionList, ArticleOptionResp{ ID: opt.ID, - Name: opt.OptionName, - Votes: opt.VoteCount, - IsVotedByUser: isVoted, // 标记当前用户是否投了这个选项 + Name: opt.OptionContent, + Votes: opt.OptionVotesNum, + IsVotedByUser: isVoted, }) } - // 4.4 组装单个文评的响应数据 articleData := ArticleResponse{ ID: article.ArticleID, Title: article.Title, VoteType: article.VoteType, TotalVoters: article.TotalVotersNum, - EndTime: article.EndTime, + EndTime: article.EndTime.Format("2006-01-02 15:04:05"), IsEnded: article.IsEnded, PublisherID: article.PublishUserID, - CreateTime: article.CreateTime, + CreateTime: article.CreateTime.Format("2006-01-02 15:04:05"), Options: optionList, - UserHasVoted: userHasVoted, // 整体标记用户是否投过票 - VotedOptionIDs: votedOptionIDs, // 记录用户投票的所有选项ID(未投票则为空数组) + UserHasVoted: userHasVoted, + VotedOptionIDs: votedOptionIDs, } - - // 4.5 加入结果集 - result[article.Title] = articleData + responseList = append(responseList, articleData) } - c.JSON(http.StatusOK, result) + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": fmt.Sprintf("查询成功,共 %d 篇文章", len(responseList)), + "data": responseList, + }) } diff --git a/controllers/article/votearticle.go b/controllers/article/votearticle.go index fe3402f..b69851a 100644 --- a/controllers/article/votearticle.go +++ b/controllers/article/votearticle.go @@ -1,6 +1,7 @@ package article import ( + "fmt" "net/http" "time" "toutoukan/init/databaseInit" @@ -9,7 +10,7 @@ import ( "gorm.io/gorm" ) -// 投票请求结构体,同时支持单选(optionId)和多选(optionIds) +// VoteArticleRequest 投票请求结构体,同时支持单选(optionId)和多选(optionIds) type VoteArticleRequest struct { Uid string `json:"uid" binding:"required,min=1,max=50"` // 用户ID ArticleID int64 `json:"articleId" binding:"required,min=1"` // 文章ID @@ -17,7 +18,7 @@ type VoteArticleRequest struct { OptionIDs []int64 `json:"optionIds" binding:"omitempty,dive,min=1"` // 多个选项ID(多选时使用) } -// 用户投票记录表结构 +// UserVote 用户投票记录表结构 type UserVote struct { UserID string `gorm:"column:user_id"` VoteArticleID int64 `gorm:"column:vote_article_id"` @@ -25,7 +26,22 @@ type UserVote struct { VoteTime time.Time `gorm:"column:vote_time"` } +// 用户积分记录表结构 +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"` +} + +// UserInfo 用户信息表结构,用于更新 total_points func VoteArticle(c *gin.Context) { + const ( + // 定义积分常量 + VOTER_POINTS = 1 // 投票者获得的积分 + POSTER_POINTS = 3 // 文章发布者获得的积分 + ) + var req VoteArticleRequest // 1. 解析并验证请求参数 @@ -55,7 +71,8 @@ func VoteArticle(c *gin.Context) { var articleStatus struct { IsEnded bool `gorm:"column:is_ended"` EndTime time.Time `gorm:"column:end_time"` - VoteType string `gorm:"column:vote_type"` // 投票类型:single-单选,multiple-多选 + VoteType string `gorm:"column:vote_type"` // 投票类型:single-单选,multiple-多选 + AuthorID string `gorm:"column:publish_user_id"` // 文章发布者ID } if err := tx.Table("article_list"). Where("articleId = ?", req.ArticleID). @@ -166,6 +183,53 @@ func VoteArticle(c *gin.Context) { return } + // 7.4 记录用户积分变动 + var userPoints []UserPoint + // 投票者积分 + userPoints = append(userPoints, UserPoint{ + UserID: req.Uid, + PointsChange: VOTER_POINTS, + Source: "vote_article", + CreateTime: voteTime, + }) + + // 文章发布者积分,如果不是自己给自己投票 + if articleStatus.AuthorID != req.Uid { + userPoints = append(userPoints, UserPoint{ + UserID: articleStatus.AuthorID, + PointsChange: POSTER_POINTS, + Source: "article_voted", + CreateTime: voteTime, + }) + } + if err := tx.Table("user_points").Create(&userPoints).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "记录积分变动失败: " + err.Error()}) + return + } + + // 7.5 更新user_info表中的total_points字段 + // 这两个更新操作都是原子性的,可以保证数据一致性 + // 更新投票者总积分 + if err := tx.Table("user_info"). + Where("uid = ?", req.Uid). + Update("total_points", gorm.Expr("total_points + ?", VOTER_POINTS)).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新投票者总积分失败: " + err.Error()}) + return + } + + // 更新文章发布者总积分(如果不是自己给自己投票) + if articleStatus.AuthorID != req.Uid { + if err := tx.Table("user_info"). + Where("uid = ?", articleStatus.AuthorID). + Update("total_points", gorm.Expr("total_points + ?", POSTER_POINTS)).Error; err != nil { + tx.Rollback() + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新文章发布者总积分失败: " + err.Error()}) + return + } + } + // 8. 提交事务 if err := tx.Commit().Error; err != nil { tx.Rollback() @@ -186,7 +250,7 @@ func VoteArticle(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "success": true, - "message": "投票成功", + "message": "投票成功,您获得了" + fmt.Sprintf("%d", VOTER_POINTS) + "积分", "data": responseData, }) }