找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

319

积分

0

好友

41

主题
发表于 昨天 23:07 | 查看: 1| 回复: 0

概述

在内容型应用中,文章的阅读量、点赞与收藏是衡量内容热度与用户互动的重要指标,也是后续实现热榜等功能的基础。本次实战将在一个全栈项目中,详细设计与实现这三个核心功能。

我们将从数据库表设计开始,重点讨论在高并发访问下如何保证数据的一致性与查询性能,并最终通过Go后端提供完整的API接口。下图展示了本次需要实现的核心接口与数据表关系:

接口与表设计概览

数据库表设计

1. 点赞记录表 (like_record) 此表用于记录用户与文章间的点赞关系。为了避免用户频繁点赞/取消点赞时数据库的频繁插入与删除操作,我们采用“更新”而非“增删”的策略:用户点赞时插入记录或将like字段置为1,取消点赞时则将like字段更新为0。这种单字段更新在数据库/中间件操作中通常比增删更高效。

为确保用户对同一文章只能点赞一次,并为Upsert(存在则更新,不存在则插入)操作提供高效支撑,我们为aid(文章ID)和uid(用户ID)创建了唯一联合索引。在Golang的Gorm框架中,可以方便地利用此索引实现原子性的Upsert

like_record表索引设计

关于联合索引顺序的思考:联合索引遵循最左前缀匹配原则。对于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,获得最佳性能。

collect_record表索引设计

3. 交互统计表 (interactive) 这是本文设计的一个关键优化点。如果每次需要获取文章的阅读、点赞、收藏总数时,都去like_recordcollect_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_recordinteractive两张表的更新,必须使用数据库事务保证原子性。逻辑步骤如下:

  • 首先查询用户对该文章的历史点赞状态。
  • 比较历史状态与当前请求:状态不变则忽略;由赞→取消,则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

前端实现则相对直观,只需将按钮的显示状态(如颜色、图标)与接口返回的likedcollected等布尔值进行绑定,并通过点击事件调用相应API即可。这些基础互动功能是构建用户粘性和内容分发系统的基石,为后续实现更复杂的热榜、推荐算法打下了坚实的数据基础。

您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-3 13:45 , Processed in 1.087620 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表