From 1034d1a69891370002cac20cd92e558fe832d783 Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 8 Jan 2026 14:44:07 +0800 Subject: [PATCH] perf: reduce list query n+1 --- backend/app/services/order.go | 123 +++++++++++++++++++++++++++++-- backend/app/services/tenant.go | 128 +++++++++++++++++++++++++++------ 2 files changed, 226 insertions(+), 25 deletions(-) diff --git a/backend/app/services/order.go b/backend/app/services/order.go index b6dd150..8a38acf 100644 --- a/backend/app/services/order.go +++ b/backend/app/services/order.go @@ -38,10 +38,9 @@ func (s *order) ListUserOrders(ctx context.Context, userID int64, status string) return nil, errorx.ErrDatabaseError.WithCause(err) } - var data []user_dto.Order - for _, v := range list { - dto, _ := s.composeOrderDTO(ctx, v) - data = append(data, dto) + data, err := s.composeOrderListDTO(ctx, list) + if err != nil { + return nil, err } return data, nil } @@ -411,6 +410,122 @@ func (s *order) composeOrderDTO(ctx context.Context, o *models.Order) (user_dto. return dto, nil } +func (s *order) composeOrderListDTO(ctx context.Context, orders []*models.Order) ([]user_dto.Order, error) { + if len(orders) == 0 { + return []user_dto.Order{}, nil + } + + // 批量收集订单、租户、内容 ID,避免逐条查询。 + orderIDs := make([]int64, 0, len(orders)) + tenantIDSet := make(map[int64]struct{}, len(orders)) + for _, o := range orders { + orderIDs = append(orderIDs, o.ID) + if o.TenantID > 0 { + tenantIDSet[o.TenantID] = struct{}{} + } + } + + tenantIDs := make([]int64, 0, len(tenantIDSet)) + for id := range tenantIDSet { + tenantIDs = append(tenantIDs, id) + } + + tenantMap := make(map[int64]*models.Tenant, len(tenantIDs)) + if len(tenantIDs) > 0 { + tbl, q := models.TenantQuery.QueryContext(ctx) + list, err := q.Where(tbl.ID.In(tenantIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, t := range list { + tenantMap[t.ID] = t + } + } + + itemsByOrder := make(map[int64][]*models.OrderItem, len(orderIDs)) + contentIDSet := make(map[int64]struct{}, len(orderIDs)) + if len(orderIDs) > 0 { + tbl, q := models.OrderItemQuery.QueryContext(ctx) + items, err := q.Where(tbl.OrderID.In(orderIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, item := range items { + itemsByOrder[item.OrderID] = append(itemsByOrder[item.OrderID], item) + if item.ContentID > 0 { + contentIDSet[item.ContentID] = struct{}{} + } + } + } + + contentIDs := make([]int64, 0, len(contentIDSet)) + for id := range contentIDSet { + contentIDs = append(contentIDs, id) + } + + contentMap := make(map[int64]*models.Content, len(contentIDs)) + if len(contentIDs) > 0 { + var contents []*models.Content + tbl, q := models.ContentQuery.QueryContext(ctx) + err := q.Where(tbl.ID.In(contentIDs...)). + UnderlyingDB(). + Preload("ContentAssets"). + Preload("ContentAssets.Asset"). + Find(&contents).Error + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, c := range contents { + contentMap[c.ID] = c + } + } + + data := make([]user_dto.Order, 0, len(orders)) + for _, o := range orders { + dto := user_dto.Order{ + ID: o.ID, + Type: string(o.Type), + TypeDescription: o.Type.Description(), + Status: string(o.Status), + StatusDescription: o.Status.Description(), + Amount: float64(o.AmountPaid) / 100.0, + CreateTime: o.CreatedAt.Format(time.RFC3339), + TenantID: o.TenantID, + } + + if t, ok := tenantMap[o.TenantID]; ok { + dto.TenantName = t.Name + } + + items := itemsByOrder[o.ID] + dto.Quantity = len(items) + for _, item := range items { + c := contentMap[item.ContentID] + if c == nil { + continue + } + ci := transaction_dto.ContentItem{ + ID: c.ID, + Title: c.Title, + Genre: c.Genre, + AuthorID: c.UserID, + Price: float64(item.AmountPaid) / 100.0, + } + for _, asset := range c.ContentAssets { + if asset.Role == consts.ContentAssetRoleCover && asset.Asset != nil { + ci.Cover = Common.GetAssetURL(asset.Asset.ObjectKey) + break + } + } + dto.Items = append(dto.Items, ci) + } + + data = append(data, dto) + } + + return data, nil +} + func (s *order) toUserOrderDTO(o *models.Order) user_dto.Order { return user_dto.Order{ ID: o.ID, diff --git a/backend/app/services/tenant.go b/backend/app/services/tenant.go index a3cc6ed..563910c 100644 --- a/backend/app/services/tenant.go +++ b/backend/app/services/tenant.go @@ -36,13 +36,23 @@ func (s *tenant) List(ctx context.Context, filter *dto.TenantListFilter) (*reque return nil, errorx.ErrDatabaseError.WithCause(err) } + tenantIDs := make([]int64, 0, len(list)) + for _, t := range list { + tenantIDs = append(tenantIDs, t.ID) + } + + // 批量统计关注数与内容数,避免逐条 Count。 + followerMap, err := s.countFollowers(ctx, tenantIDs) + if err != nil { + return nil, err + } + contentMap, err := s.countPublishedContents(ctx, tenantIDs) + if err != nil { + return nil, err + } + var data []dto.TenantProfile for _, t := range list { - followers, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(t.ID)).Count() - contents, _ := models.ContentQuery.WithContext(ctx). - Where(models.ContentQuery.TenantID.Eq(t.ID), models.ContentQuery.Status.Eq(consts.ContentStatusPublished)). - Count() - cfg := t.Config.Data() data = append(data, dto.TenantProfile{ ID: t.ID, @@ -50,8 +60,8 @@ func (s *tenant) List(ctx context.Context, filter *dto.TenantListFilter) (*reque Avatar: cfg.Avatar, Bio: cfg.Bio, Stats: dto.Stats{ - Followers: int(followers), - Contents: int(contents), + Followers: int(followerMap[t.ID]), + Contents: int(contentMap[t.ID]), }, }) } @@ -159,29 +169,56 @@ func (s *tenant) ListFollowed(ctx context.Context, userID int64) ([]dto.TenantPr return nil, errorx.ErrDatabaseError.WithCause(err) } + if len(list) == 0 { + return []dto.TenantProfile{}, nil + } + + tenantIDs := make([]int64, 0, len(list)) + tenantIDSet := make(map[int64]struct{}, len(list)) + for _, tu := range list { + if _, ok := tenantIDSet[tu.TenantID]; ok { + continue + } + tenantIDs = append(tenantIDs, tu.TenantID) + tenantIDSet[tu.TenantID] = struct{}{} + } + + tblTenant, qTenant := models.TenantQuery.QueryContext(ctx) + tenants, err := qTenant.Where(tblTenant.ID.In(tenantIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + tenantMap := make(map[int64]*models.Tenant, len(tenants)) + for _, t := range tenants { + tenantMap[t.ID] = t + } + + // 批量统计关注数与内容数,避免逐条 Count。 + followerMap, err := s.countFollowers(ctx, tenantIDs) + if err != nil { + return nil, err + } + contentMap, err := s.countPublishedContents(ctx, tenantIDs) + if err != nil { + return nil, err + } + var data []dto.TenantProfile for _, tu := range list { - // Fetch Tenant - t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tu.TenantID)).First() - if err != nil { + t, ok := tenantMap[tu.TenantID] + if !ok { continue } - // Stats - followers, _ := models.TenantUserQuery.WithContext(ctx). - Where(models.TenantUserQuery.TenantID.Eq(tu.TenantID)). - Count() - contents, _ := models.ContentQuery.WithContext(ctx). - Where(models.ContentQuery.TenantID.Eq(tu.TenantID), models.ContentQuery.Status.Eq(consts.ContentStatusPublished)). - Count() - + cfg := t.Config.Data() data = append(data, dto.TenantProfile{ ID: t.ID, Name: t.Name, - Avatar: "", + Avatar: cfg.Avatar, Stats: dto.Stats{ - Followers: int(followers), - Contents: int(contents), + Followers: int(followerMap[tu.TenantID]), + Contents: int(contentMap[tu.TenantID]), }, IsFollowing: true, }) @@ -202,3 +239,52 @@ func (s *tenant) GetModelByID(ctx context.Context, id int64) (*models.Tenant, er } return u, nil } + +type tenantStatRow struct { + TenantID int64 `gorm:"column:tenant_id"` + Total int64 `gorm:"column:total"` +} + +func (s *tenant) countFollowers(ctx context.Context, tenantIDs []int64) (map[int64]int64, error) { + if len(tenantIDs) == 0 { + return map[int64]int64{}, nil + } + + tbl, q := models.TenantUserQuery.QueryContext(ctx) + var rows []tenantStatRow + err := q.Where(tbl.TenantID.In(tenantIDs...)). + Select(tbl.TenantID, tbl.ALL.Count().As("total")). + Group(tbl.TenantID). + Scan(&rows) + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + result := make(map[int64]int64, len(rows)) + for _, row := range rows { + result[row.TenantID] = row.Total + } + return result, nil +} + +func (s *tenant) countPublishedContents(ctx context.Context, tenantIDs []int64) (map[int64]int64, error) { + if len(tenantIDs) == 0 { + return map[int64]int64{}, nil + } + + tbl, q := models.ContentQuery.QueryContext(ctx) + var rows []tenantStatRow + err := q.Where(tbl.TenantID.In(tenantIDs...), tbl.Status.Eq(consts.ContentStatusPublished)). + Select(tbl.TenantID, tbl.ALL.Count().As("total")). + Group(tbl.TenantID). + Scan(&rows) + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + result := make(map[int64]int64, len(rows)) + for _, row := range rows { + result[row.TenantID] = row.Total + } + return result, nil +}