diff --git a/controllers/article/createarticle.go b/controllers/article/createarticle.go new file mode 100644 index 0000000..ac3a247 --- /dev/null +++ b/controllers/article/createarticle.go @@ -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), + }, + }) +} diff --git a/controllers/article/deletearticle.go b/controllers/article/deletearticle.go new file mode 100644 index 0000000..5eeb9f5 --- /dev/null +++ b/controllers/article/deletearticle.go @@ -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, + }, + }) +} diff --git a/controllers/article/getarticle.go b/controllers/article/getarticle.go new file mode 100644 index 0000000..233628c --- /dev/null +++ b/controllers/article/getarticle.go @@ -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) +} diff --git a/controllers/article/votearticle.go b/controllers/article/votearticle.go new file mode 100644 index 0000000..7117754 --- /dev/null +++ b/controllers/article/votearticle.go @@ -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"), + }, + }) +} diff --git a/router/setupRouter.go b/router/setupRouter.go index 404ce5f..503a490 100644 --- a/router/setupRouter.go +++ b/router/setupRouter.go @@ -1,6 +1,7 @@ package router import ( + "toutoukan/controllers/article" "toutoukan/controllers/kills" "toutoukan/controllers/search" "toutoukan/controllers/system" @@ -35,6 +36,13 @@ func SetupRouter() *gin.Engine { searchGroup.POST("/usersearch", search.DataSearch) 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 }