更新投票功能

This commit is contained in:
2025-09-24 01:07:21 +08:00
parent 3182008eef
commit b499fe633e
5 changed files with 517 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
package article
import (
"net/http"
"time"
"toutoukan/init/databaseInit"
"github.com/gin-gonic/gin"
)
// OptionItem 选项子结构
type OptionItem struct {
Content string `json:"content" binding:"required,min=1,max=200"` // 选项内容
SortOrder int `json:"sort_order" binding:"required,min=0"` // 排序值
}
// CreateArticleRequest 创建文章的请求参数结构
type CreateArticleRequest struct {
PublishUserID string `json:"publish_user_id" binding:"required,min=1,max=40"`
Title string `json:"title" binding:"required,min=1,max=255"`
VoteType string `json:"vote_type" binding:"required,min=1,max=60"`
EndTime time.Time `json:"end_time" binding:"required"`
Options []OptionItem `json:"options" binding:"required,min=1,max=3"`
}
// 数据库文章记录结构体(与表结构完全对应)
type ArticleList struct {
ArticleID int64 `gorm:"column:articleId;primaryKey;autoIncrement"` // 明确主键和自增
PublishUserID string `gorm:"column:publish_user_id"`
Title string `gorm:"column:title"`
VoteType string `gorm:"column:vote_type"`
EndTime time.Time `gorm:"column:end_time"`
IsEnded bool `gorm:"column:is_ended"`
TotalVotersNum int `gorm:"column:total_voters_num"`
CreateTime time.Time `gorm:"column:create_time"`
}
// 自定义表名(如果结构体名与表名不一致)
func (ArticleList) TableName() string {
return "article_list"
}
// CreateArticle 创建文章(包含选项)
func CreateArticle(c *gin.Context) {
var req CreateArticleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "参数解析失败",
"detail": err.Error(),
})
return
}
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()
}
}()
// 1. 创建文章主记录
newArticle := ArticleList{
PublishUserID: req.PublishUserID,
Title: req.Title,
VoteType: req.VoteType,
EndTime: req.EndTime,
IsEnded: false,
TotalVotersNum: 0,
CreateTime: time.Now(),
}
// 插入文章并获取ID使用GORM的Create方法自动填充自增ID
if err := tx.Create(&newArticle).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建文章失败: " + err.Error()})
return
}
// 验证文章ID是否有效必须大于0
if newArticle.ArticleID <= 0 {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取文章ID创建失败"})
return
}
// 2. 批量创建选项确保vote_article_id正确关联
var options []map[string]interface{}
for _, opt := range req.Options {
options = append(options, map[string]interface{}{
"vote_article_id": newArticle.ArticleID, // 使用刚创建的文章ID
"option_content": opt.Content,
"option_votes_num": 0,
"sort_order": opt.SortOrder,
})
}
// 插入选项前先验证文章ID是否存在额外保险
var count int64
if err := tx.Model(&ArticleList{}).Where("articleId = ?", newArticle.ArticleID).Count(&count).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "验证文章ID失败: " + err.Error()})
return
}
if count == 0 {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "文章ID不存在外键约束失败"})
return
}
// 批量插入选项
if err := tx.Table("article_options").CreateInBatches(options, len(options)).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建选项失败: " + err.Error()})
return
}
// 提交事务
if err := tx.Commit().Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "提交数据失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "文章及选项创建成功",
"data": gin.H{
"article_id": newArticle.ArticleID,
"option_count": len(req.Options),
},
})
}

View File

@@ -0,0 +1,92 @@
package article
import (
"net/http"
"toutoukan/init/databaseInit"
"github.com/gin-gonic/gin"
)
// DeleteArticleRequest 删除文章的请求参数
type DeleteArticleRequest struct {
ArticleID int64 `json:"article_id" binding:"required,min=1"` // 要删除的文章ID
}
// DeleteArticle 删除文章(处理外键关联)
func DeleteArticle(c *gin.Context) {
var req DeleteArticleRequest
// 解析请求参数
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "参数解析失败",
"detail": err.Error(),
})
return
}
// 开启事务(确保所有删除操作要么全部成功,要么全部失败)
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()
}
}()
// 步骤1先删除用户投票记录因为依赖文章ID和选项ID
if err := tx.Table("user_votes").
Where("vote_article_id = ?", req.ArticleID).
Delete(nil).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{
"error": "删除用户投票记录失败: " + err.Error(),
})
return
}
// 步骤2再删除文章选项因为依赖文章ID
if err := tx.Table("article_options").
Where("vote_article_id = ?", req.ArticleID).
Delete(nil).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{
"error": "删除文章选项失败: " + err.Error(),
})
return
}
// 步骤3最后删除文章主记录
if err := tx.Table("article_list").
Where("articleId = ?", req.ArticleID).
Delete(nil).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{
"error": "删除文章失败: " + err.Error(),
})
return
}
// 提交事务
if err := tx.Commit().Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{
"error": "提交删除操作失败: " + err.Error(),
})
return
}
// 返回成功响应
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "文章及关联数据已全部删除",
"data": gin.H{
"deleted_article_id": req.ArticleID,
},
})
}

View File

@@ -0,0 +1,108 @@
package article
import (
"fmt"
"net/http"
"toutoukan/init/databaseInit"
"github.com/gin-gonic/gin"
)
// ArticleOption 文评选项结构增加了ID字段
type ArticleOption struct {
ID int64 `json:"id"` // 选项ID
Name string `json:"name"` // 选项名称
Votes int `json:"votes"` // 该选项的投票数
}
// ArticleResponse 单个文评的响应结构(保持不变)
type ArticleResponse struct {
ID int64 `json:"文评ID"` // 文评唯一ID
Title string `json:"文评标题"` // 文评标题
VoteType string `json:"投票类型"` // 投票类型
TotalVoters int `json:"总投票人数"` // 总投票人数
EndTime string `json:"结束时间"` // 结束时间
IsEnded bool `json:"是否结束"` // 是否结束
PublisherID string `json:"发布者ID"` // 发布者ID
CreateTime string `json:"创建时间"` // 创建时间
Options []ArticleOption `json:"选项"` // 按顺序排列的选项列表
}
// ArticleList 获取所有文评及选项信息(带排序)
func ArticleListget(c *gin.Context) {
// 1. 查询所有文评,获取表中所有字段
var articles []struct {
ArticleID int64 `gorm:"column:articleId"`
Title string `gorm:"column:title"`
VoteType string `gorm:"column:vote_type"`
TotalVotersNum int `gorm:"column:total_voters_num"`
EndTime string `gorm:"column:end_time"`
IsEnded bool `gorm:"column:is_ended"`
PublishUserID string `gorm:"column:publish_user_id"`
CreateTime string `gorm:"column:create_time"`
}
// 查询文评主表,选择所有字段
if err := databaseInit.UserDB.Table("article_list").
Select("articleId, title, vote_type, total_voters_num, end_time, is_ended, publish_user_id, create_time").
Find(&articles).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "查询文评失败: " + err.Error(),
})
return
}
fmt.Println("所有文评列表:", articles)
// 2. 构建结果映射
result := make(map[string]interface{})
// 3. 逐个查询每个文评的选项
for _, article := range articles {
var options []struct {
ID int64 `gorm:"column:id"` // 新增获取选项ID
OptionName string `gorm:"column:option_content"`
VoteCount int `gorm:"column:option_votes_num"`
SortOrder int `gorm:"column:sort_order"` // 用于排序的字段
}
// 查询当前文评的所有选项并按sort_order排序
if err := databaseInit.UserDB.Table("article_options").
Where("vote_article_id = ?", article.ArticleID).
Select("id, option_content, option_votes_num, sort_order"). // 新增选择id字段
Order("sort_order ASC").
Find(&options).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "查询文评选项失败: " + err.Error(),
})
return
}
// 4. 格式化选项数据为有序切片包含ID
var optionList []ArticleOption
for _, opt := range options {
optionList = append(optionList, ArticleOption{
ID: opt.ID, // 新增赋值选项ID
Name: opt.OptionName,
Votes: opt.VoteCount,
})
}
// 5. 组装单个文评的响应数据
articleData := ArticleResponse{
ID: article.ArticleID,
Title: article.Title,
VoteType: article.VoteType,
TotalVoters: article.TotalVotersNum,
EndTime: article.EndTime,
IsEnded: article.IsEnded,
PublisherID: article.PublishUserID,
CreateTime: article.CreateTime,
Options: optionList,
}
// 6. 加入结果集
result[article.Title] = articleData
}
c.JSON(http.StatusOK, result)
}

View File

@@ -0,0 +1,172 @@
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"),
},
})
}

View File

@@ -1,6 +1,7 @@
package router package router
import ( import (
"toutoukan/controllers/article"
"toutoukan/controllers/kills" "toutoukan/controllers/kills"
"toutoukan/controllers/search" "toutoukan/controllers/search"
"toutoukan/controllers/system" "toutoukan/controllers/system"
@@ -35,6 +36,13 @@ func SetupRouter() *gin.Engine {
searchGroup.POST("/usersearch", search.DataSearch) searchGroup.POST("/usersearch", search.DataSearch)
searchGroup.POST("/insert", search.InsertDocument) searchGroup.POST("/insert", search.InsertDocument)
} }
articleGroup := r.Group("/article")
{
articleGroup.POST("/get", article.ArticleListget)
articleGroup.POST("/vote", article.VoteArticle)
articleGroup.POST("/create", article.CreateArticle)
articleGroup.POST("/delete", article.DeleteArticle)
}
return r return r
} }