package creator import ( "context" "sort" "time" ) // WorkGrowth 是作品在观察窗口内的点赞增量,用于爆品筛选。 // Baseline 为窗口起点前最后一个采样点;无基线时以最早采样点近似。 type WorkGrowth struct { Likes *int64 `json:"likes_growth"` Coverage string `json:"likes_growth_coverage"` // full=有窗口起点基线;partial=以首点近似 } // WorkGrowthFilter 增速筛选条件。 type WorkGrowthFilter struct { Hours int64 // 观察窗口长度,默认 24 MinLikes *int64 } // normalizeGrowthFilter 校验并补默认值。 func normalizeGrowthFilter(filter WorkGrowthFilter) (WorkGrowthFilter, error) { if filter.Hours == 0 { filter.Hours = 24 } if filter.Hours < 0 || filter.Hours > 24*30 { return WorkGrowthFilter{}, ErrInvalid } if filter.MinLikes != nil && *filter.MinLikes < 0 { return WorkGrowthFilter{}, ErrInvalid } return filter, nil } // computeWorkGrowth 从按时间升序的采样点计算观察窗口内的点赞增量。 // points 为空返回零值(无数据);不足两个点或无基线时 coverage=partial。 func computeWorkGrowth(points []MetricPoint, now time.Time, hours int64) WorkGrowth { // hours 为小时数(接口参数同名同义),窗口起点 = now - hours 小时。 cutoff := now.Add(-time.Duration(hours) * time.Hour) var baseline, latest *MetricPoint for index := range points { point := &points[index] if point.Likes == nil { continue } if !point.CollectedAt.Before(cutoff) { // 窗口起点及之后的点属于窗口内;升序遍历取最晚点作为 latest。 latest = point continue } // 窗口起点前最近点作为基线(升序遍历,最后赋值即最近)。 baseline = point } if latest == nil { // 窗口内无采样点:以最后一个窗口外点近似(数据稀疏时的降级)。 for index := range points { point := &points[index] if point.Likes == nil { continue } latest = point } if latest == nil { return WorkGrowth{} } } if baseline == nil { // 无窗口起点前基线:以窗口内最早采样点近似基线(partial)。 for index := range points { point := &points[index] if point.Likes == nil { continue } if !point.CollectedAt.Before(cutoff) { baseline = point break } } if baseline == nil || baseline == latest { // 仅有单点:无法计算增量。 return WorkGrowth{Likes: int64Ptr(0), Coverage: "partial"} } return WorkGrowth{Likes: int64Ptr(*latest.Likes - *baseline.Likes), Coverage: "partial"} } if baseline == latest { return WorkGrowth{Likes: int64Ptr(0), Coverage: "partial"} } return WorkGrowth{Likes: int64Ptr(*latest.Likes - *baseline.Likes), Coverage: "full"} } // LoadWorksGrowth 批量加载作品的点赞增量。返回 map[workID]WorkGrowth;无采样点的作品不在 map 中。 func (s *Store) LoadWorksGrowth(ctx context.Context, workIDs []string, filter WorkGrowthFilter) (map[string]WorkGrowth, error) { filter, err := normalizeGrowthFilter(filter) if err != nil { return nil, err } if len(workIDs) == 0 { return map[string]WorkGrowth{}, nil } rows, err := s.db.QueryContext(ctx, `SELECT work_id, collected_at, likes FROM creator_work_metric WHERE work_id = ANY($1) AND likes IS NOT NULL ORDER BY work_id, collected_at`, workIDs) if err != nil { return nil, databaseError(err) } defer rows.Close() byWork := make(map[string][]MetricPoint) for rows.Next() { var workID string var point MetricPoint var likes int64 if err := rows.Scan(&workID, &point.CollectedAt, &likes); err != nil { return nil, databaseError(err) } point.CollectedAt = point.CollectedAt.UTC() value := likes point.Likes = &value byWork[workID] = append(byWork[workID], point) } if err := rows.Err(); err != nil { return nil, databaseError(err) } now := time.Now().UTC() result := make(map[string]WorkGrowth, len(byWork)) for workID, points := range byWork { result[workID] = computeWorkGrowth(points, now, filter.Hours) } return result, nil } // ApplyWorkGrowthFilter 为作品列表附加增速并按条件过滤、排序(增速降序)。 // 只有设置了 MinLikes 才改变排序;否则保持原顺序仅附加增速。 func ApplyWorkGrowthFilter(works []Work, growth map[string]WorkGrowth, filter WorkGrowthFilter) ([]Work, error) { filter, err := normalizeGrowthFilter(filter) if err != nil { return nil, err } result := make([]Work, 0, len(works)) for _, work := range works { if stat, ok := growth[work.ID]; ok { work.LikesGrowth = stat.Likes work.LikesGrowthCoverage = stat.Coverage } if filter.MinLikes != nil { if work.LikesGrowth == nil || *work.LikesGrowth < *filter.MinLikes { continue } } result = append(result, work) } if filter.MinLikes != nil { sort.SliceStable(result, func(i, j int) bool { gi, gj := result[i].LikesGrowth, result[j].LikesGrowth if gi == nil || gj == nil { return gj == nil && gi != nil } return *gi > *gj }) } return result, nil } func int64Ptr(value int64) *int64 { return &value }