perf: reduce topic list queries
This commit is contained in:
@@ -539,27 +539,78 @@ func (s *content) ListTopics(ctx context.Context, tenantID int64) ([]content_dto
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
var topics []content_dto.Topic
|
||||
if len(results) == 0 {
|
||||
return []content_dto.Topic{}, nil
|
||||
}
|
||||
|
||||
genres := make([]string, 0, len(results))
|
||||
for _, r := range results {
|
||||
if r.Genre == "" {
|
||||
continue
|
||||
}
|
||||
genres = append(genres, r.Genre)
|
||||
}
|
||||
|
||||
contentIDByGenre := make(map[string]int64, len(genres))
|
||||
contentMap := make(map[int64]*models.Content, len(genres))
|
||||
if len(genres) > 0 {
|
||||
// 批量获取每个分类最新内容,避免逐条查询导致 N+1。
|
||||
var rows []struct {
|
||||
Genre string `gorm:"column:genre"`
|
||||
ContentID int64 `gorm:"column:content_id"`
|
||||
}
|
||||
contentQuery := models.ContentQuery.WithContext(ctx).UnderlyingDB().
|
||||
Model(&models.Content{}).
|
||||
Select("distinct on (genre) genre, id as content_id").
|
||||
Where("status = ?", consts.ContentStatusPublished).
|
||||
Where("genre IN ?", genres).
|
||||
Order("genre, published_at desc, id desc")
|
||||
if tenantID > 0 {
|
||||
contentQuery = contentQuery.Where("tenant_id = ?", tenantID)
|
||||
}
|
||||
if err := contentQuery.Scan(&rows).Error; err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
contentIDs := make([]int64, 0, len(rows))
|
||||
seen := make(map[int64]struct{}, len(rows))
|
||||
for _, row := range rows {
|
||||
if row.ContentID == 0 {
|
||||
continue
|
||||
}
|
||||
contentIDByGenre[row.Genre] = row.ContentID
|
||||
if _, ok := seen[row.ContentID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[row.ContentID] = struct{}{}
|
||||
contentIDs = append(contentIDs, row.ContentID)
|
||||
}
|
||||
|
||||
if len(contentIDs) > 0 {
|
||||
var contents []*models.Content
|
||||
if err := models.ContentQuery.WithContext(ctx).
|
||||
UnderlyingDB().
|
||||
Preload("ContentAssets").
|
||||
Preload("ContentAssets.Asset").
|
||||
Where("id IN ?", contentIDs).
|
||||
Find(&contents).Error; err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, c := range contents {
|
||||
contentMap[c.ID] = c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
topics := make([]content_dto.Topic, 0, len(results))
|
||||
for i, r := range results {
|
||||
if r.Genre == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Fetch latest content in this genre to get a cover
|
||||
var c models.Content
|
||||
query := models.ContentQuery.WithContext(ctx).
|
||||
Where(models.ContentQuery.Genre.Eq(r.Genre), models.ContentQuery.Status.Eq(consts.ContentStatusPublished))
|
||||
if tenantID > 0 {
|
||||
query = query.Where(models.ContentQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
query.
|
||||
Order(models.ContentQuery.PublishedAt.Desc()).
|
||||
UnderlyingDB().
|
||||
Preload("ContentAssets").
|
||||
Preload("ContentAssets.Asset").
|
||||
First(&c)
|
||||
|
||||
cover := ""
|
||||
if contentID, ok := contentIDByGenre[r.Genre]; ok {
|
||||
if c := contentMap[contentID]; c != nil {
|
||||
for _, ca := range c.ContentAssets {
|
||||
if (ca.Role == consts.ContentAssetRoleCover || ca.Role == consts.ContentAssetRoleMain) && ca.Asset != nil {
|
||||
cover = Common.GetAssetURL(ca.Asset.ObjectKey)
|
||||
@@ -568,6 +619,8 @@ func (s *content) ListTopics(ctx context.Context, tenantID int64) ([]content_dto
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
topics = append(topics, content_dto.Topic{
|
||||
ID: int64(i + 1), // Use index as ID for aggregation results
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
- 审计操作显式传入操作者信息(服务层不再依赖 ctx 读取)。
|
||||
- 运营统计报表(overview + CSV 导出基础版)。
|
||||
- 超管后台治理能力(健康度/异常监控/内容审核)。
|
||||
- 性能优化(避免 N+1:topics 聚合批量查询)。
|
||||
|
||||
## 里程碑建议
|
||||
- M1:完成 P0
|
||||
|
||||
Reference in New Issue
Block a user