Files
toutoukan/controllers/article/votearticle.go

257 lines
7.8 KiB
Go
Raw Normal View History

2025-09-24 01:07:21 +08:00
package article
import (
2025-09-24 20:57:23 +08:00
"fmt"
2025-09-24 01:07:21 +08:00
"net/http"
"time"
"toutoukan/init/databaseInit"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
2025-09-24 20:57:23 +08:00
// VoteArticleRequest 投票请求结构体,同时支持单选(optionId)和多选(optionIds)
2025-09-24 01:07:21 +08:00
type VoteArticleRequest struct {
2025-09-24 15:34:09 +08:00
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多选时使用
}
2025-09-24 20:57:23 +08:00
// UserVote 用户投票记录表结构
2025-09-24 15:34:09 +08:00
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"`
2025-09-24 01:07:21 +08:00
}
2025-09-24 20:57:23 +08:00
// 用户积分记录表结构
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
2025-09-24 01:07:21 +08:00
func VoteArticle(c *gin.Context) {
2025-09-24 20:57:23 +08:00
const (
// 定义积分常量
VOTER_POINTS = 1 // 投票者获得的积分
POSTER_POINTS = 3 // 文章发布者获得的积分
)
2025-09-24 01:07:21 +08:00
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()
}
}()
2025-09-24 15:34:09 +08:00
// 3. 检查文章状态和投票类型
2025-09-24 01:07:21 +08:00
var articleStatus struct {
2025-09-24 15:34:09 +08:00
IsEnded bool `gorm:"column:is_ended"`
EndTime time.Time `gorm:"column:end_time"`
2025-09-24 20:57:23 +08:00
VoteType string `gorm:"column:vote_type"` // 投票类型single-单选multiple-多选
AuthorID string `gorm:"column:publish_user_id"` // 文章发布者ID
2025-09-24 01:07:21 +08:00
}
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
}
2025-09-24 15:34:09 +08:00
// 3.1 检查投票是否已结束
2025-09-24 01:07:21 +08:00
now := time.Now()
if articleStatus.IsEnded || articleStatus.EndTime.Before(now) {
tx.Rollback()
2025-09-24 15:34:09 +08:00
c.JSON(http.StatusForbidden, gin.H{"error": "该文评投票已结束或已过截止时间,无法投票"})
return
}
2025-09-24 01:07:21 +08:00
2025-09-24 15:34:09 +08:00
// 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
2025-09-24 01:07:21 +08:00
}
2025-09-24 15:34:09 +08:00
selectedOptions = req.OptionIDs
}
2025-09-24 01:07:21 +08:00
2025-09-24 15:34:09 +08:00
// 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()})
2025-09-24 01:07:21 +08:00
return
}
2025-09-24 15:34:09 +08:00
if voteCount > 0 {
tx.Rollback()
c.JSON(http.StatusForbidden, gin.H{"error": "您已对该文评投过票,无法重复投票"})
return
2025-09-24 01:07:21 +08:00
}
2025-09-24 15:34:09 +08:00
// 6. 批量检查所有选项是否属于该文评
var existingOptionsCount int64
2025-09-24 01:07:21 +08:00
if err := tx.Table("article_options").
2025-09-24 15:34:09 +08:00
Where("vote_article_id = ? AND id IN ?", req.ArticleID, selectedOptions).
Count(&existingOptionsCount).Error; err != nil {
2025-09-24 01:07:21 +08:00
tx.Rollback()
2025-09-24 15:34:09 +08:00
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不存在或不属于该文评"})
2025-09-24 01:07:21 +08:00
return
}
2025-09-24 15:34:09 +08:00
// 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 {
2025-09-24 01:07:21 +08:00
tx.Rollback()
2025-09-24 15:34:09 +08:00
c.JSON(http.StatusInternalServerError, gin.H{"error": "批量记录投票失败: " + err.Error()})
2025-09-24 01:07:21 +08:00
return
}
2025-09-24 15:34:09 +08:00
// 7.2 批量更新选项票数
2025-09-24 01:07:21 +08:00
if err := tx.Table("article_options").
2025-09-24 15:34:09 +08:00
Where("id IN ?", selectedOptions).
2025-09-24 01:07:21 +08:00
Update("option_votes_num", gorm.Expr("option_votes_num + 1")).Error; err != nil {
tx.Rollback()
2025-09-24 15:34:09 +08:00
c.JSON(http.StatusInternalServerError, gin.H{"error": "批量更新选项票数失败: " + err.Error()})
2025-09-24 01:07:21 +08:00
return
}
2025-09-24 15:34:09 +08:00
// 7.3 更新文评总投票人数
2025-09-24 01:07:21 +08:00
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
}
2025-09-24 20:57:23 +08:00
// 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
}
}
2025-09-24 15:34:09 +08:00
// 8. 提交事务
2025-09-24 01:07:21 +08:00
if err := tx.Commit().Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "提交投票失败: " + err.Error()})
return
}
2025-09-24 15:34:09 +08:00
// 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
}
2025-09-24 01:07:21 +08:00
c.JSON(http.StatusOK, gin.H{
"success": true,
2025-09-24 20:57:23 +08:00
"message": "投票成功,您获得了" + fmt.Sprintf("%d", VOTER_POINTS) + "积分",
2025-09-24 15:34:09 +08:00
"data": responseData,
2025-09-24 01:07:21 +08:00
})
}