实战二:文章发布 API(用户鉴权 + 标签多对多 + 分页列表)
第二个实战叠加三块:JWT 鉴权(复用鉴权章节)、文章与标签多对多、分页列表规范。目标:登录用户发文、标签自动建并关联、公开接口分页浏览。
1. 模型
type Article struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"size:200;not null"`
Content string `json:"content" gorm:"type:text"`
AuthorID uint `json:"author_id" gorm:"index"`
Tags []Tag `json:"tags" gorm:"many2many:article_tags;"`
CreatedAt time.Time `json:"created_at"`
}
type Tag struct {
ID uint `json:"id"`
Name string `json:"name" gorm:"size:32;uniqueIndex"`
}
AuthorID 必须来自 Token 而不是客户端传参——这是防「冒充他人发文」的关键。
2. 发布:事务内建标签并关联
标签「有则用、无则建」,两步放进一个事务:
func (s *ArticleService) Create(a *Article, tagNames []string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(a).Error; err != nil { return err }
var tags []Tag
for _, name := range tagNames {
name = strings.TrimSpace(name)
if name == "" { continue }
var t Tag
if err := tx.Where("name = ?", name).
FirstOrCreate(&t, Tag{Name: name}).Error; err != nil { return err }
tags = append(tags, t)
}
if len(tags) == 0 { return nil }
return tx.Model(a).Association("Tags").Append(&tags)
})
}
3. 鉴权接入 handler
路由:api.Group("/articles", AuthRequired()).POST("", h.Create);作者取自中间件注入的 userID:
func (h *ArticleHandler) Create(c *gin.Context) {
var in struct {
Title string `json:"title" binding:"required,max=200"`
Body string `json:"content" binding:"required,min=10"`
Tags []string `json:"tags"`
}
if err := c.ShouldBindJSON(&in); err != nil { fail(c, 400, "INVALID_ARG", "参数有误"); return }
art := &model.Article{Title: in.Title, Content: in.Body, AuthorID: c.GetUint("userID")}
if err := h.svc.Create(art, in.Tags); err != nil { fail(c, 500, "CREATE_FAIL", "发布失败"); return }
c.JSON(http.StatusCreated, art)
}
4. 分页列表带标签
列表公开,读时 Preload("Tags") 避免 N+1:
func (s *ArticleService) List(page, size int) ([]Article, int64, error) {
var arts []Article
var total int64
s.db.Model(&Article{}).Count(&total)
err := s.db.Preload("Tags").
Order("created_at DESC").
Offset((page - 1) * size).Limit(size).
Find(&arts).Error
return arts, total, err
}
按标签过滤可追加 JOIN:Joins 到 article_tags、tags 表后 Where("tags.name = ?", tag),细节以 gorm.io 关联文档为准。
5. 接口清单与测试要点
POST /api/v1/articles(需登录)创建;列表GET公开(?page=&page_size=&tag=);GET /:id详情带 Tags。- 测试要点:未带 Token 401;标签重复创建不报错;分页 total 正确;非作者更新/删除 403。
注意点
- 更新/删除前校验
AuthorID == 当前 userID,否则 403。 - 事务里先 Create 再 Append 关联,顺序不能反;分页 Count 与 Find 各自独立执行。
小结
实战二把无状态鉴权、多对多关联、分页与事务拼在一起:中间件给身份,service 管事务,Preload 解 N+1。文件上传实战在下一章继续。