193 lines
5.6 KiB
Go
193 lines
5.6 KiB
Go
package article
|
||
|
||
import (
|
||
"net/http"
|
||
"time"
|
||
"toutoukan/init/databaseInit"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// 投票请求结构体,同时支持单选(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
|
||
OptionID int64 `json:"optionId" binding:"omitempty,min=1"` // 单个选项ID(单选时使用)
|
||
OptionIDs []int64 `json:"optionIds" binding:"omitempty,dive,min=1"` // 多个选项ID(多选时使用)
|
||
}
|
||
|
||
// 用户投票记录表结构
|
||
type UserVote struct {
|
||
UserID string `gorm:"column:user_id"`
|
||
VoteArticleID int64 `gorm:"column:vote_article_id"`
|
||
OptionID int64 `gorm:"column:option_id"`
|
||
VoteTime time.Time `gorm:"column:vote_time"`
|
||
}
|
||
|
||
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 articleStatus struct {
|
||
IsEnded bool `gorm:"column:is_ended"`
|
||
EndTime time.Time `gorm:"column:end_time"`
|
||
VoteType string `gorm:"column:vote_type"` // 投票类型:single-单选,multiple-多选
|
||
}
|
||
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
|
||
}
|
||
|
||
// 3.1 检查投票是否已结束
|
||
now := time.Now()
|
||
if articleStatus.IsEnded || articleStatus.EndTime.Before(now) {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusForbidden, gin.H{"error": "该文评投票已结束或已过截止时间,无法投票"})
|
||
return
|
||
}
|
||
|
||
// 4. 处理投票选项,统一转换为选项ID列表
|
||
var selectedOptions []int64
|
||
if articleStatus.VoteType == "single" {
|
||
if req.OptionID == 0 || len(req.OptionIDs) > 0 {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusForbidden, gin.H{
|
||
"error": "该文评为单选投票,请使用optionId提交单个选项",
|
||
})
|
||
return
|
||
}
|
||
selectedOptions = []int64{req.OptionID}
|
||
} else {
|
||
if len(req.OptionIDs) == 0 || req.OptionID != 0 {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusForbidden, gin.H{
|
||
"error": "该文评为多选投票,请使用optionIds提交多个选项",
|
||
})
|
||
return
|
||
}
|
||
selectedOptions = req.OptionIDs
|
||
}
|
||
|
||
// 5. 检查用户是否已投票
|
||
var voteCount int64
|
||
if err := tx.Table("user_votes").
|
||
Where("user_id = ? AND vote_article_id = ?", req.Uid, req.ArticleID).
|
||
Count(&voteCount).Error; err != nil {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "检查投票记录失败: " + err.Error()})
|
||
return
|
||
}
|
||
if voteCount > 0 {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusForbidden, gin.H{"error": "您已对该文评投过票,无法重复投票"})
|
||
return
|
||
}
|
||
|
||
// 6. 批量检查所有选项是否属于该文评
|
||
var existingOptionsCount int64
|
||
if err := tx.Table("article_options").
|
||
Where("vote_article_id = ? AND id IN ?", req.ArticleID, selectedOptions).
|
||
Count(&existingOptionsCount).Error; err != nil {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询选项失败: " + err.Error()})
|
||
return
|
||
}
|
||
if existingOptionsCount != int64(len(selectedOptions)) {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusNotFound, gin.H{"error": "部分选项ID不存在或不属于该文评"})
|
||
return
|
||
}
|
||
|
||
// 7. 执行批量投票事务
|
||
voteTime := time.Now()
|
||
|
||
// 7.1 批量插入用户投票记录
|
||
userVotes := make([]UserVote, len(selectedOptions))
|
||
for i, optionID := range selectedOptions {
|
||
userVotes[i] = UserVote{
|
||
UserID: req.Uid,
|
||
VoteArticleID: req.ArticleID,
|
||
OptionID: optionID,
|
||
VoteTime: voteTime,
|
||
}
|
||
}
|
||
if err := tx.Table("user_votes").Create(&userVotes).Error; err != nil {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "批量记录投票失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 7.2 批量更新选项票数
|
||
if err := tx.Table("article_options").
|
||
Where("id IN ?", selectedOptions).
|
||
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
|
||
}
|
||
|
||
// 7.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
|
||
}
|
||
|
||
// 8. 提交事务
|
||
if err := tx.Commit().Error; err != nil {
|
||
tx.Rollback()
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": "提交投票失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 9. 返回成功响应
|
||
responseData := gin.H{
|
||
"articleId": req.ArticleID,
|
||
"voteTime": voteTime.Format("2006-01-02 15:04:05"),
|
||
}
|
||
if articleStatus.VoteType == "single" {
|
||
responseData["optionId"] = req.OptionID
|
||
} else {
|
||
responseData["optionIds"] = req.OptionIDs
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "投票成功",
|
||
"data": responseData,
|
||
})
|
||
}
|