Files
toutoukan/controllers/article/votearticle.go
2025-09-24 20:57:23 +08:00

257 lines
7.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package article
import (
"fmt"
"net/http"
"time"
"toutoukan/init/databaseInit"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// 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
OptionID int64 `json:"optionId" binding:"omitempty,min=1"` // 单个选项ID单选时使用
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"`
OptionID int64 `gorm:"column:option_id"`
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. 解析并验证请求参数
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-多选
AuthorID string `gorm:"column:publish_user_id"` // 文章发布者ID
}
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
}
// 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()
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": "投票成功,您获得了" + fmt.Sprintf("%d", VOTER_POINTS) + "积分",
"data": responseData,
})
}