package article import ( "fmt" "net/http" "time" "toutoukan/init/databaseInit" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type VoteArticleRequest struct { Uid string `json:"uid" binding:"required,min=1,max=50"` // 用户ID,非空且长度限制 ArticleID int64 `json:"articleId" binding:"required,min=1"` // 文评ID,非空且为正整数 OptionID int64 `json:"optionId" binding:"required,min=1"` // 选项ID,投票必需 } func VoteArticle(c *gin.Context) { var req VoteArticleRequest // 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() } }() // 3. 检查用户是否已投票(防重复投票) var existingVote struct{ ID int64 } voteCheckErr := tx.Table("user_votes"). Where("user_id = ? AND vote_article_id = ?", req.Uid, req.ArticleID). First(&existingVote).Error if voteCheckErr == nil { // 已存在投票记录 tx.Rollback() c.JSON(http.StatusForbidden, gin.H{ "error": "您已对该文评投过票,无法重复投票", }) return } else if voteCheckErr != gorm.ErrRecordNotFound { // 其他查询错误 tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{ "error": "检查投票记录失败: " + voteCheckErr.Error(), }) return } var articleStatus struct { IsEnded bool `gorm:"column:is_ended"` EndTime time.Time `gorm:"column:end_time"` // 新增:获取截止时间 } if err := tx.Table("article_list"). Where("articleId = ?", req.ArticleID). First(&articleStatus).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 } // 双重检查:已标记结束 或 已过截止时间,均禁止投票 now := time.Now() if articleStatus.IsEnded || articleStatus.EndTime.Before(now) { tx.Rollback() // 区分提示信息(可选,增强用户体验) var errMsg string if articleStatus.IsEnded { errMsg = "该文评投票已结束,无法投票" } else { errMsg = "该文评投票已过截止时间,无法投票" } c.JSON(http.StatusForbidden, gin.H{"error": errMsg}) return } // 额外优化:如果已过截止时间但is_ended未更新,主动更新状态(可选) if !articleStatus.IsEnded && articleStatus.EndTime.Before(now) { if err := tx.Table("article_list"). Where("articleId = ?", req.ArticleID). Update("is_ended", true).Error; err != nil { // 此处不回滚,仅记录警告,不影响主要逻辑 fmt.Printf("警告:更新过期文评状态失败,articleId=%d, err=%v\n", req.ArticleID, err) } } // 5. 检查选项是否属于该文评 var optionCheck struct{ ID int64 } if err := tx.Table("article_options"). Where("id = ? AND vote_article_id = ?", req.OptionID, req.ArticleID). First(&optionCheck).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 } // 6. 执行投票事务(三表操作) // 6.1 记录用户投票记录 if err := tx.Table("user_votes").Create(map[string]interface{}{ "user_id": req.Uid, "vote_article_id": req.ArticleID, "option_id": req.OptionID, "vote_time": time.Now(), }).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "记录投票失败: " + err.Error()}) return } // 6.2 更新选项票数 if err := tx.Table("article_options"). Where("id = ?", req.OptionID). Update("option_votes_num", gorm.Expr("option_votes_num + 1")).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "更新选项票数失败: " + err.Error()}) return } // 6.3 更新文评总投票人数 if err := tx.Table("article_list"). Where("articleId = ?", req.ArticleID). Update("total_voters_num", gorm.Expr("total_voters_num + 1")).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "更新总投票人数失败: " + err.Error()}) return } // 7. 提交事务 if err := tx.Commit().Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "提交投票失败: " + err.Error()}) return } // 8. 返回成功响应 c.JSON(http.StatusOK, gin.H{ "success": true, "message": "投票成功", "data": gin.H{ "articleId": req.ArticleID, "optionId": req.OptionID, "voteTime": time.Now().Format("2006-01-02 15:04:05"), }, }) }