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
|
|
|
|
})
|
|
|
|
|
|
}
|