概述
在内容型应用中,文章的阅读量、点赞与收藏是衡量内容热度与用户互动的重要指标,也是后续实现热榜等功能的基础。本次实战将在一个全栈项目中,详细设计与实现这三个核心功能。
我们将从数据库表设计开始,重点讨论在高并发访问下如何保证数据的一致性与查询性能,并最终通过Go后端提供完整的API接口。下图展示了本次需要实现的核心接口与数据表关系:

数据库表设计
1. 点赞记录表 (like_record)
此表用于记录用户与文章间的点赞关系。为了避免用户频繁点赞/取消点赞时数据库的频繁插入与删除操作,我们采用“更新”而非“增删”的策略:用户点赞时插入记录或将like字段置为1,取消点赞时则将like字段更新为0。这种单字段更新在数据库/中间件操作中通常比增删更高效。
为确保用户对同一文章只能点赞一次,并为Upsert(存在则更新,不存在则插入)操作提供高效支撑,我们为aid(文章ID)和uid(用户ID)创建了唯一联合索引。在Golang的Gorm框架中,可以方便地利用此索引实现原子性的Upsert。

关于联合索引顺序的思考:联合索引遵循最左前缀匹配原则。对于like_record表,查询条件多为WHERE aid = ? AND uid = ?,因此(aid, uid)或(uid, aid)的顺序对唯一性约束无影响。但考虑到“查询某篇文章的点赞用户列表”(WHERE aid = ?)也是常见场景,将aid放在首位能直接利用索引,提升查询效率。
2. 收藏记录表 (collect_record)
此表结构与like_record类似,但业务场景侧重不同。用户查看“我的收藏夹”(WHERE uid = ?)的频率远高于查看“某篇文章的收藏列表”(WHERE aid = ?)。因此,我们为collect_record创建了(uid, aid)的联合唯一索引,让个人收藏夹查询能直接命中索引的最左前缀uid,获得最佳性能。

3. 交互统计表 (interactive)
这是本文设计的一个关键优化点。如果每次需要获取文章的阅读、点赞、收藏总数时,都去like_record、collect_record等关系表中进行COUNT(*)聚合查询,在数据量大时性能开销巨大。
因此,我们引入interactive表,专门用于冗余存储每篇文章的实时统计计数(read_count, like_count, collect_count)。当用户执行点赞、收藏等动作时,在事务中同时更新关系表和本表的计数字段。这样,前端获取文章热度数据时,仅需一次简单的单表查询,极大地提升了接口响应速度,是一种典型的以空间换时间的优化策略。
DAO层核心实现
1. 阅读量统计
阅读量统计相对简单,用户进入文章页面即触发计数。在实际生产环境中,通常会加入用户ID或IP频次限制等防刷机制。这里我们直接对interactive表进行计数更新:
now := time.Now().Format("2006-01-02 15:04:05")
if err = i.db.Model(domain.Interactive{}).Table("interactive").Clauses(clause.OnConflict{
DoUpdates: clause.Assignments(map[string]any{
"read_count": gorm.Expr("read_count + 1"),
"utime": now,
}),
}).Create(&domain.Interactive{
Aid: aid,
ReadCount: 1,
CTime: now,
UTime: now,
}).Error; err != nil {
return err
}
return nil
2. 点赞/取消点赞
点赞操作涉及like_record和interactive两张表的更新,必须使用数据库事务保证原子性。逻辑步骤如下:
- 首先查询用户对该文章的历史点赞状态。
- 比较历史状态与当前请求:状态不变则忽略;由赞→取消,则
interactive.like_count - 1;由取消→赞,则interactive.like_count + 1。
- 最后,利用唯一联合索引,对
like_record表执行Upsert操作,更新或插入点赞状态。
now := time.Now().Format("2006-01-02 15:04:05")
return i.db.Transaction(func(tx *gorm.DB) error {
// 1. 查询历史点赞状态
var likeRec domain.LikeRecord
err = tx.Model(domain.LikeRecord{}).Table("like_record").Where("aid = ? AND uid = ?", aid, uid).First(&likeRec).Error
// 2. 根据状态变化,更新interactive表计数
switch {
case like == 1 && likeRec.Like == 0: // 新增点赞
if err = tx.Model(domain.Interactive{}).Table("interactive").Where("aid = ?", aid).
UpdateColumn("like_count", gorm.Expr("like_count + ?", 1)).Error; err != nil {
return err
}
case like == 0 && likeRec.Like == 1: // 取消点赞
if err = tx.Model(domain.Interactive{}).Table("interactive").Where("aid = ?", aid).
UpdateColumn("like_count", gorm.Expr("like_count + ?", -1)).Error; err != nil {
return err
}
default:
// 状态无变化,不更新计数
}
// 3. Upsert更新like_record表
if err = tx.Model(domain.LikeRecord{}).Table("like_record").Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "aid"}, {Name: "uid"}},
DoUpdates: clause.Assignments(map[string]any{
"like": like,
"utime": now,
}),
}).Create(&domain.LikeRecord{
Uid: uid,
Aid: aid,
Like: like,
CTime: now,
UTime: now,
}).Error; err != nil {
return err
}
return nil
})
收藏功能的实现逻辑与此类似,此处不再赘述。
3. 获取文章互动状态与数据
此接口需返回目标文章的总阅读、点赞、收藏数,以及当前用户是否已点赞、收藏。通过一次事务内的查询,联查interactive表与两个关系表即可高效完成。
if err = i.db.Transaction(func(tx *gorm.DB) error {
if err = tx.Model(&domain.CollectRecord{}).Table("collect_record").Select("collect_record.collected", "like_record.like").
Joins("LEFT JOIN like_record ON collect_record.aid = like_record.aid AND collect_record.uid = like_record.uid").
Where("collect_record.uid = ? AND collect_record.aid = ?", uid, aid).Scan(&personRes).Error; err != nil {
return err
}
if err = tx.Model(domain.Interactive{}).Table("interactive").Where("aid = ?", aid).Scan(&interRes).Error; err != nil {
return err
}
return nil
}); err != nil {
return res, err
}
4. 获取用户收藏夹(支持分页)
查询用户收藏列表时,需要与文章表进行JOIN,以获取文章标题、作者等详细信息,并支持分页参数。
if err = i.db.Transaction(func(tx *gorm.DB) error {
// 获取收藏总数,用于分页计算
if err = tx.Model(&domain.CollectRecord{}).Where("uid = ?", uid).Count(&count).Error; err != nil {
return err
}
// 分页查询收藏的文章详情
if err = tx.Model(&domain.CollectRecord{}).Limit(pageSize).Offset(offset).
Select("article.title", "article.content", "article.author_name", "collect_record.collected", "collect_record.aid").
Joins("JOIN article ON article.id = aid").
Where("uid = ? AND collected = 1", uid).Scan(&rows).Error; err != nil {
return err
}
return nil
}); err != nil {
return res, err
}
总结
至此,我们完成了文章阅读、点赞、收藏功能的后端核心设计与Go实现。总结几个关键点:
- 索引优化:根据最常访问的查询模式(个人收藏夹 vs 文章点赞列表)设计联合索引顺序。
- 性能与一致性:通过
interactive统计表聚合数据,用空间换时间,大幅提升查询性能;利用数据库事务保证计数与关系记录的一致性。
- 原子操作:使用唯一索引与Gorm的
Clause.OnConflict实现高效的Upsert。
前端实现则相对直观,只需将按钮的显示状态(如颜色、图标)与接口返回的liked、collected等布尔值进行绑定,并通过点击事件调用相应API即可。这些基础互动功能是构建用户粘性和内容分发系统的基石,为后续实现更复杂的热榜、推荐算法打下了坚实的数据基础。