chore: 补充后端核心模块的中文注释

This commit is contained in:
2026-07-15 11:54:01 +08:00
parent e3c68dd6d3
commit ffd9b0c8f0
12 changed files with 70 additions and 39 deletions

View File

@@ -168,6 +168,7 @@ func (f *FeedHandler) ListByPopularity(c *gin.Context) {
c.JSON(200, resp)
}
// 在返回的 FeedVideoItem 列表中,如果列表为 nil则返回空切片避免 JSON 序列化为 null
func nonNilFeedVideoItems(items []FeedVideoItem) []FeedVideoItem {
if items == nil {
return []FeedVideoItem{}

View File

@@ -17,6 +17,7 @@ func NewFeedRepository(db *gorm.DB) *FeedRepository {
return &FeedRepository{db: db}
}
// 查询最新视频. limit: 查询数量 latestBefore: 查询早于此时间的视频(零值则不限制)
func (repo *FeedRepository) ListLatest(ctx context.Context, limit int, latestBefore time.Time) ([]*video.Video, error) {
var videos []*video.Video
query := repo.db.WithContext(ctx).Model(&video.Video{}).
@@ -30,6 +31,7 @@ func (repo *FeedRepository) ListLatest(ctx context.Context, limit int, latestBef
return videos, nil
}
// 查询点赞数最多的视频. limit: 查询数量 cursor: 游标
func (repo *FeedRepository) ListLikesCountWithCursor(ctx context.Context, limit int, cursor *LikesCountCursor) ([]*video.Video, error) {
var videos []*video.Video
query := repo.db.WithContext(ctx).Model(&video.Video{}).
@@ -49,6 +51,7 @@ func (repo *FeedRepository) ListLikesCountWithCursor(ctx context.Context, limit
return videos, nil
}
// 查询关注用户的视频. limit: 查询数量 viewerAccountID: 查看者账户ID latestBefore: 查询早于此时间的视频(零值则不限制)
func (repo *FeedRepository) ListByFollowing(ctx context.Context, limit int, viewerAccountID uint, latestBefore time.Time) ([]*video.Video, error) {
var videos []*video.Video
query := repo.db.WithContext(ctx).Model(&video.Video{}).
@@ -69,6 +72,7 @@ func (repo *FeedRepository) ListByFollowing(ctx context.Context, limit int, view
return videos, nil
}
// 查询热门视频. limit: 查询数量 popularityBefore: 查询热度低于此值的视频 timeBefore: 查询早于此时间的视频 idBefore: 查询ID小于此值的视频
func (repo *FeedRepository) ListByPopularity(ctx context.Context, limit int, popularityBefore int64, timeBefore time.Time, idBefore uint) ([]*video.Video, error) {
var videos []*video.Video
query := repo.db.WithContext(ctx).Model(&video.Video{}).

View File

@@ -33,9 +33,9 @@ func NewFeedService(repo *FeedRepository, likeRepo *video.LikeRepository, redisc
return &FeedService{repo: repo, likeRepo: likeRepo, rediscache: rediscache, localcache: cache.New(3*time.Second, 5*time.Second), cacheTTL: 24 * time.Hour}
}
// GetVideoByIDs 批量获取视频信息
// 采用 L1(本地缓存) -> L2(Redis) -> L3(MySQL) 三级架构
func (f *FeedService) GetVideoByIDs(ctx context.Context, videoIDs []uint) ([]*video.Video, error) {
// GetVideoByIDs 批量获取视频信息
// 采用 L1(本地缓存) -> L2(Redis) -> L3(MySQL) 三级架构
if len(videoIDs) == 0 {
return []*video.Video{}, nil
}
@@ -109,6 +109,8 @@ func (f *FeedService) GetVideoByIDs(ctx context.Context, videoIDs []uint) ([]*vi
wg.Add(1)
go func(videoID uint) {
defer wg.Done()
// singleflight 防止缓存击穿
sfKey := f.rediscache.Key("sf:entity:%d", videoID)
v, err, _ := f.requestGroup.Do(sfKey, func() (interface{}, error) {
@@ -160,6 +162,7 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
isZsetEmpty := len(zsetTail) == 0
// ZSet 为空时尝试重建 ZSet
if isZsetEmpty {
//全局静态锁:无视所有用户的不同时间戳游标
sfKey := f.rediscache.Key("sf:fallback:global_timeline_rebuild")
@@ -195,10 +198,11 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
return ListLatestResponse{HasMore: false}, nil
}
// 让所有被阻塞的请求重新查一遍
// 递归调用自己,让所有被阻塞的请求重新查一遍
return f.ListLatest(ctx, limit, latestBefore, viewerAccountID)
}
// watermark 是 ZSET 中最老的一条数据的时间戳; reqTime 是本次请求的时间戳(如果没有传 latestBefore则使用当前时间)
watermark := int64(zsetTail[0].Score)
reqTime := time.Now().UnixMilli()
if !latestBefore.IsZero() {
@@ -228,11 +232,19 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
maxScore = fmt.Sprintf("%d", reqTime-1) // 防重复
}
videoIDsStr, err := f.rediscache.ZRevRangeByScore(ctx, f.rediscache.Key("feed:global_timeline"), maxScore, "-inf", 0, int64(limit))
videoIDsStr, err := f.rediscache.ZRevRangeByScore(
ctx,
f.rediscache.Key("feed:global_timeline"),
maxScore,
"-inf",
0,
int64(limit),
)
if err != nil {
return ListLatestResponse{}, err
}
// 将字符串 ID 转换为 uint
var videoIDs []uint
for _, idStr := range videoIDsStr {
if id, err := strconv.ParseUint(idStr, 10, 64); err == nil {
@@ -247,7 +259,7 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
}
}
// 刚好击穿了冷热边界
// 刚好击穿了冷热边界,从数据库中再拉一些冷数据补齐
if len(baseVideos) < limit {
remainLimit := limit - len(baseVideos) // 计算还差几个
@@ -381,6 +393,7 @@ func (f *FeedService) ListByFollowing(ctx context.Context, limit int, latestBefo
token, locked, _ := f.rediscache.Lock(cacheCtx, lockKey, 500*time.Millisecond)
if locked {
defer func() { _ = f.rediscache.Unlock(context.Background(), lockKey, token) }()
// Double check再次检查缓存是否被其他请求回写
if b, err := f.rediscache.GetBytes(cacheCtx, cacheKey); err == nil {
var cached ListByFollowingResponse
if err := json.Unmarshal(b, &cached); err == nil {
@@ -396,7 +409,7 @@ func (f *FeedService) ListByFollowing(ctx context.Context, limit int, latestBefo
}
return resp, nil
}
} else {
} else { // 加锁失败,循环等待缓存被其他请求回写
for i := 0; i < 5; i++ {
time.Sleep(20 * time.Millisecond)
if b, err := f.rediscache.GetBytes(cacheCtx, cacheKey); err == nil {
@@ -410,11 +423,12 @@ func (f *FeedService) ListByFollowing(ctx context.Context, limit int, latestBefo
}
}
// 缓存未命中或 Redis 不可用,降级成数据库查询
resp, err := doListByFollowingFromDB()
if err != nil {
return ListByFollowingResponse{}, err
}
if cacheKey != "" {
if cacheKey != "" { // 缓存回写
if b, err := json.Marshal(resp); err == nil {
cacheCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
defer cancel()
@@ -427,22 +441,25 @@ func (f *FeedService) ListByFollowing(ctx context.Context, limit int, latestBefo
func (f *FeedService) ListByPopularity(ctx context.Context, limit int, reqAsOf int64, offset int, viewerAccountID uint, latestPopularity int64, latestBefore time.Time, latestIDBefore uint) (ListByPopularityResponse, error) {
// Redis 热榜稳定分页as_of + offset
if f.rediscache != nil {
// 将 as_of 截断到分钟级
asOf := time.Now().UTC().Truncate(time.Minute)
if reqAsOf > 0 {
asOf = time.Unix(reqAsOf, 0).UTC().Truncate(time.Minute)
}
// 创建时间窗口,获取过去 60 分钟的 ZSET
const win = 60
keys := make([]string, 0, win)
for i := 0; i < win; i++ {
keys = append(keys, f.rediscache.Key("hot:video:1m:%s", asOf.Add(-time.Duration(i)*time.Minute).Format("200601021504")))
}
dest := f.rediscache.Key("hot:video:merge:1m:%s", asOf.Format("200601021504")) // 快照key同一个as_of页内复用
// 创建快照key同一个as_of页内复用
dest := f.rediscache.Key("hot:video:merge:1m:%s", asOf.Format("200601021504"))
opCtx, cancel := context.WithTimeout(ctx, 80*time.Millisecond)
defer cancel()
exists, _ := f.rediscache.Exists(opCtx, dest)
exists, _ := f.rediscache.Exists(opCtx, dest) // 检查合并快照是否已存在
if !exists {
_ = f.rediscache.ZUnionStore(opCtx, dest, keys, "SUM")
_ = f.rediscache.Expire(opCtx, dest, 2*time.Minute) // 给翻页留时间
@@ -462,6 +479,7 @@ func (f *FeedService) ListByPopularity(ctx context.Context, limit int, reqAsOf i
}
}
if err == nil && len(members) > 0 {
// 将字符串 ID 转换为 uint
ids := make([]uint, 0, len(members))
for _, m := range members {
u, err := strconv.ParseUint(m, 10, 64)
@@ -470,6 +488,7 @@ func (f *FeedService) ListByPopularity(ctx context.Context, limit int, reqAsOf i
}
}
// 根据 ID 批量获取视频信息
videos, err := f.repo.GetByIDs(ctx, ids)
if err == nil {
byID := make(map[uint]*video.Video, len(videos))
@@ -477,7 +496,7 @@ func (f *FeedService) ListByPopularity(ctx context.Context, limit int, reqAsOf i
byID[v.ID] = v
}
ordered := make([]*video.Video, 0, len(ids))
for _, id := range ids {
for _, id := range ids { // 按 Redis 返回的顺序重新排列
if v := byID[id]; v != nil {
ordered = append(ordered, v)
}
@@ -492,7 +511,7 @@ func (f *FeedService) ListByPopularity(ctx context.Context, limit int, reqAsOf i
NextOffset: offset + len(items),
HasMore: len(items) == limit,
}
if len(ordered) > 0 {
if len(ordered) > 0 { // 准备最后一条视频的游标信息,供 DB fallback 使用
last := ordered[len(ordered)-1]
nextPopularity := last.Popularity
nextBefore := last.CreateTime
@@ -506,6 +525,7 @@ func (f *FeedService) ListByPopularity(ctx context.Context, limit int, reqAsOf i
}
}
// DB fallback游标分页latestPopularity + latestBefore + latestIDBefore
videos, err := f.repo.ListByPopularity(ctx, limit, latestPopularity, latestBefore, latestIDBefore)
if err != nil {
return ListByPopularityResponse{}, err
@@ -532,6 +552,7 @@ func (f *FeedService) ListByPopularity(ctx context.Context, limit int, reqAsOf i
return resp, nil
}
// 将 Video 列表转换为 FeedVideoItem 列表,并批量查询填充当前用户对所有视频的点赞状态
func (f *FeedService) buildFeedVideos(ctx context.Context, videos []*video.Video, viewerAccountID uint) ([]FeedVideoItem, error) {
feedVideos := make([]FeedVideoItem, 0, len(videos))
videoIDs := make([]uint, len(videos))
@@ -558,6 +579,7 @@ func (f *FeedService) buildFeedVideos(ctx context.Context, videos []*video.Video
return feedVideos, nil
}
// 将视频列表按照给定的 ID 顺序(orderedIDs)重新排序
func buildOrderedResult(orderedIDs []uint, dataMap map[uint]*video.Video) []*video.Video {
res := make([]*video.Video, 0, len(orderedIDs))
for _, id := range orderedIDs {