diff --git a/backend/app/http/super/v1/dto/super.go b/backend/app/http/super/v1/dto/super.go index 555af0f..1e73e89 100644 --- a/backend/app/http/super/v1/dto/super.go +++ b/backend/app/http/super/v1/dto/super.go @@ -19,12 +19,80 @@ type TenantListFilter struct { type SuperContentListFilter struct { requests.Pagination - // Add filters if needed, currently list signature is just pagination in super.go service + // ID 内容ID,精确匹配。 + ID *int64 `query:"id"` + // TenantID 租户ID,精确匹配。 + TenantID *int64 `query:"tenant_id"` + // TenantCode 租户编码,模糊匹配。 + TenantCode *string `query:"tenant_code"` + // TenantName 租户名称,模糊匹配。 + TenantName *string `query:"tenant_name"` + // UserID 作者用户ID,精确匹配。 + UserID *int64 `query:"user_id"` + // Username 作者用户名/昵称,模糊匹配。 + Username *string `query:"username"` + // Keyword 标题或摘要关键字,模糊匹配。 + Keyword *string `query:"keyword"` + // Status 内容状态过滤。 + Status *consts.ContentStatus `query:"status"` + // Visibility 内容可见性过滤。 + Visibility *consts.ContentVisibility `query:"visibility"` + // PublishedAtFrom 发布时间起始(RFC3339)。 + PublishedAtFrom *string `query:"published_at_from"` + // PublishedAtTo 发布时间结束(RFC3339)。 + PublishedAtTo *string `query:"published_at_to"` + // CreatedAtFrom 创建时间起始(RFC3339)。 + CreatedAtFrom *string `query:"created_at_from"` + // CreatedAtTo 创建时间结束(RFC3339)。 + CreatedAtTo *string `query:"created_at_to"` + // PriceAmountMin 价格下限(分)。 + PriceAmountMin *int64 `query:"price_amount_min"` + // PriceAmountMax 价格上限(分)。 + PriceAmountMax *int64 `query:"price_amount_max"` + // Asc 升序字段(id/title/published_at/created_at 等)。 + Asc *string `query:"asc"` + // Desc 降序字段(id/title/published_at/created_at 等)。 + Desc *string `query:"desc"` } type SuperOrderListFilter struct { requests.Pagination - // Add filters if needed + // ID 订单ID,精确匹配。 + ID *int64 `query:"id"` + // TenantID 租户ID,精确匹配。 + TenantID *int64 `query:"tenant_id"` + // TenantCode 租户编码,模糊匹配。 + TenantCode *string `query:"tenant_code"` + // TenantName 租户名称,模糊匹配。 + TenantName *string `query:"tenant_name"` + // UserID 买家用户ID,精确匹配。 + UserID *int64 `query:"user_id"` + // Username 买家用户名/昵称,模糊匹配。 + Username *string `query:"username"` + // ContentID 内容ID,精确匹配。 + ContentID *int64 `query:"content_id"` + // ContentTitle 内容标题关键字,模糊匹配。 + ContentTitle *string `query:"content_title"` + // Type 订单类型过滤。 + Type *consts.OrderType `query:"type"` + // Status 订单状态过滤。 + Status *consts.OrderStatus `query:"status"` + // CreatedAtFrom 创建时间起始(RFC3339)。 + CreatedAtFrom *string `query:"created_at_from"` + // CreatedAtTo 创建时间结束(RFC3339)。 + CreatedAtTo *string `query:"created_at_to"` + // PaidAtFrom 支付时间起始(RFC3339)。 + PaidAtFrom *string `query:"paid_at_from"` + // PaidAtTo 支付时间结束(RFC3339)。 + PaidAtTo *string `query:"paid_at_to"` + // AmountPaidMin 实付金额下限(分)。 + AmountPaidMin *int64 `query:"amount_paid_min"` + // AmountPaidMax 实付金额上限(分)。 + AmountPaidMax *int64 `query:"amount_paid_max"` + // Asc 升序字段(id/created_at/paid_at/amount_paid 等)。 + Asc *string `query:"asc"` + // Desc 降序字段(id/created_at/paid_at/amount_paid 等)。 + Desc *string `query:"desc"` } // SuperUserLite 用于平台用户列表的轻量级用户信息 diff --git a/backend/app/services/super.go b/backend/app/services/super.go index 346eb33..4bf902c 100644 --- a/backend/app/services/super.go +++ b/backend/app/services/super.go @@ -3,6 +3,7 @@ package services import ( "context" "errors" + "strings" "time" "quyun/v2/app/errorx" @@ -274,13 +275,165 @@ func (s *super) UpdateTenantExpire(ctx context.Context, id int64, form *super_dt func (s *super) ListContents(ctx context.Context, filter *super_dto.SuperContentListFilter) (*requests.Pager, error) { tbl, q := models.ContentQuery.QueryContext(ctx) + if filter.ID != nil && *filter.ID > 0 { + q = q.Where(tbl.ID.Eq(*filter.ID)) + } + if filter.TenantID != nil && *filter.TenantID > 0 { + q = q.Where(tbl.TenantID.Eq(*filter.TenantID)) + } + if filter.UserID != nil && *filter.UserID > 0 { + q = q.Where(tbl.UserID.Eq(*filter.UserID)) + } + if filter.Status != nil && *filter.Status != "" { + q = q.Where(tbl.Status.Eq(*filter.Status)) + } + if filter.Visibility != nil && *filter.Visibility != "" { + q = q.Where(tbl.Visibility.Eq(*filter.Visibility)) + } + if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" { + keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%" + q = q.Where(tbl.Title.Like(keyword)).Or(tbl.Description.Like(keyword)).Or(tbl.Summary.Like(keyword)) + } + + tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName) + if err != nil { + return nil, err + } + if tenantFilter { + if len(tenantIDs) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.TenantID.In(tenantIDs...)) + } + } + + userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username) + if err != nil { + return nil, err + } + if userFilter { + if len(userIDs) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.UserID.In(userIDs...)) + } + } + + if filter.PublishedAtFrom != nil { + from, err := s.parseFilterTime(filter.PublishedAtFrom) + if err != nil { + return nil, err + } + if from != nil { + q = q.Where(tbl.PublishedAt.Gte(*from)) + } + } + if filter.PublishedAtTo != nil { + to, err := s.parseFilterTime(filter.PublishedAtTo) + if err != nil { + return nil, err + } + if to != nil { + q = q.Where(tbl.PublishedAt.Lte(*to)) + } + } + if filter.CreatedAtFrom != nil { + from, err := s.parseFilterTime(filter.CreatedAtFrom) + if err != nil { + return nil, err + } + if from != nil { + q = q.Where(tbl.CreatedAt.Gte(*from)) + } + } + if filter.CreatedAtTo != nil { + to, err := s.parseFilterTime(filter.CreatedAtTo) + if err != nil { + return nil, err + } + if to != nil { + q = q.Where(tbl.CreatedAt.Lte(*to)) + } + } + + if filter.PriceAmountMin != nil || filter.PriceAmountMax != nil { + pTbl, pQ := models.ContentPriceQuery.QueryContext(ctx) + pq := pQ + if filter.PriceAmountMin != nil { + pq = pq.Where(pTbl.PriceAmount.Gte(*filter.PriceAmountMin)) + } + if filter.PriceAmountMax != nil { + pq = pq.Where(pTbl.PriceAmount.Lte(*filter.PriceAmountMax)) + } + prices, err := pq.Select(pTbl.ContentID).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + ids := make([]int64, 0, len(prices)) + for _, price := range prices { + ids = append(ids, price.ContentID) + } + if len(ids) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.ID.In(ids...)) + } + } + + orderApplied := false + if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" { + switch strings.TrimSpace(*filter.Desc) { + case "id": + q = q.Order(tbl.ID.Desc()) + case "title": + q = q.Order(tbl.Title.Desc()) + case "tenant_id": + q = q.Order(tbl.TenantID.Desc()) + case "user_id": + q = q.Order(tbl.UserID.Desc()) + case "status": + q = q.Order(tbl.Status.Desc()) + case "visibility": + q = q.Order(tbl.Visibility.Desc()) + case "published_at": + q = q.Order(tbl.PublishedAt.Desc()) + case "created_at": + q = q.Order(tbl.CreatedAt.Desc()) + } + orderApplied = true + } else if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" { + switch strings.TrimSpace(*filter.Asc) { + case "id": + q = q.Order(tbl.ID) + case "title": + q = q.Order(tbl.Title) + case "tenant_id": + q = q.Order(tbl.TenantID) + case "user_id": + q = q.Order(tbl.UserID) + case "status": + q = q.Order(tbl.Status) + case "visibility": + q = q.Order(tbl.Visibility) + case "published_at": + q = q.Order(tbl.PublishedAt) + case "created_at": + q = q.Order(tbl.CreatedAt) + } + orderApplied = true + } + + if !orderApplied { + q = q.Order(tbl.ID.Desc()) + } + filter.Pagination.Format() total, err := q.Count() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } var list []*models.Content - err = q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Order(tbl.ID.Desc()). + err = q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)). UnderlyingDB(). Preload("Author"). Preload("ContentAssets.Asset"). @@ -322,12 +475,153 @@ func (s *super) UpdateContentStatus(ctx context.Context, tenantID, contentID int func (s *super) ListOrders(ctx context.Context, filter *super_dto.SuperOrderListFilter) (*requests.Pager, error) { tbl, q := models.OrderQuery.QueryContext(ctx) + if filter.ID != nil && *filter.ID > 0 { + q = q.Where(tbl.ID.Eq(*filter.ID)) + } + if filter.TenantID != nil && *filter.TenantID > 0 { + q = q.Where(tbl.TenantID.Eq(*filter.TenantID)) + } + if filter.UserID != nil && *filter.UserID > 0 { + q = q.Where(tbl.UserID.Eq(*filter.UserID)) + } + if filter.Type != nil && *filter.Type != "" { + q = q.Where(tbl.Type.Eq(*filter.Type)) + } + if filter.Status != nil && *filter.Status != "" { + q = q.Where(tbl.Status.Eq(*filter.Status)) + } + if filter.AmountPaidMin != nil { + q = q.Where(tbl.AmountPaid.Gte(*filter.AmountPaidMin)) + } + if filter.AmountPaidMax != nil { + q = q.Where(tbl.AmountPaid.Lte(*filter.AmountPaidMax)) + } + + tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName) + if err != nil { + return nil, err + } + if tenantFilter { + if len(tenantIDs) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.TenantID.In(tenantIDs...)) + } + } + + userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username) + if err != nil { + return nil, err + } + if userFilter { + if len(userIDs) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.UserID.In(userIDs...)) + } + } + + orderIDs, contentFilter, err := s.lookupOrderIDsByContent(ctx, filter.ContentID, filter.ContentTitle) + if err != nil { + return nil, err + } + if contentFilter { + if len(orderIDs) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.ID.In(orderIDs...)) + } + } + + if filter.CreatedAtFrom != nil { + from, err := s.parseFilterTime(filter.CreatedAtFrom) + if err != nil { + return nil, err + } + if from != nil { + q = q.Where(tbl.CreatedAt.Gte(*from)) + } + } + if filter.CreatedAtTo != nil { + to, err := s.parseFilterTime(filter.CreatedAtTo) + if err != nil { + return nil, err + } + if to != nil { + q = q.Where(tbl.CreatedAt.Lte(*to)) + } + } + if filter.PaidAtFrom != nil { + from, err := s.parseFilterTime(filter.PaidAtFrom) + if err != nil { + return nil, err + } + if from != nil { + q = q.Where(tbl.PaidAt.Gte(*from)) + } + } + if filter.PaidAtTo != nil { + to, err := s.parseFilterTime(filter.PaidAtTo) + if err != nil { + return nil, err + } + if to != nil { + q = q.Where(tbl.PaidAt.Lte(*to)) + } + } + + orderApplied := false + if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" { + switch strings.TrimSpace(*filter.Desc) { + case "id": + q = q.Order(tbl.ID.Desc()) + case "created_at": + q = q.Order(tbl.CreatedAt.Desc()) + case "paid_at": + q = q.Order(tbl.PaidAt.Desc()) + case "amount_paid": + q = q.Order(tbl.AmountPaid.Desc()) + case "amount_original": + q = q.Order(tbl.AmountOriginal.Desc()) + case "amount_discount": + q = q.Order(tbl.AmountDiscount.Desc()) + case "status": + q = q.Order(tbl.Status.Desc()) + case "type": + q = q.Order(tbl.Type.Desc()) + } + orderApplied = true + } else if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" { + switch strings.TrimSpace(*filter.Asc) { + case "id": + q = q.Order(tbl.ID) + case "created_at": + q = q.Order(tbl.CreatedAt) + case "paid_at": + q = q.Order(tbl.PaidAt) + case "amount_paid": + q = q.Order(tbl.AmountPaid) + case "amount_original": + q = q.Order(tbl.AmountOriginal) + case "amount_discount": + q = q.Order(tbl.AmountDiscount) + case "status": + q = q.Order(tbl.Status) + case "type": + q = q.Order(tbl.Type) + } + orderApplied = true + } + if !orderApplied { + q = q.Order(tbl.ID.Desc()) + } + filter.Pagination.Format() total, err := q.Count() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } - list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Order(tbl.ID.Desc()).Find() + list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } @@ -617,6 +911,128 @@ func (s *super) toSuperOrderItemLine(item *models.OrderItem) super_dto.SuperOrde } } +func (s *super) parseFilterTime(value *string) (*time.Time, error) { + if value == nil { + return nil, nil + } + text := strings.TrimSpace(*value) + if text == "" { + return nil, nil + } + if t, err := time.Parse(time.RFC3339, text); err == nil { + return &t, nil + } + t, err := time.Parse("2006-01-02", text) + if err != nil { + return nil, errorx.ErrInvalidFormat.WithCause(err) + } + return &t, nil +} + +func (s *super) lookupTenantIDs(ctx context.Context, code, name *string) ([]int64, bool, error) { + codeText := "" + if code != nil { + codeText = strings.TrimSpace(*code) + } + nameText := "" + if name != nil { + nameText = strings.TrimSpace(*name) + } + if codeText == "" && nameText == "" { + return nil, false, nil + } + + tbl, q := models.TenantQuery.QueryContext(ctx) + if codeText != "" { + q = q.Where(tbl.Code.Like("%" + codeText + "%")) + } + if nameText != "" { + q = q.Where(tbl.Name.Like("%" + nameText + "%")) + } + tenants, err := q.Select(tbl.ID).Find() + if err != nil { + return nil, true, errorx.ErrDatabaseError.WithCause(err) + } + + ids := make([]int64, 0, len(tenants)) + for _, tenant := range tenants { + ids = append(ids, tenant.ID) + } + return ids, true, nil +} + +func (s *super) lookupUserIDs(ctx context.Context, username *string) ([]int64, bool, error) { + text := "" + if username != nil { + text = strings.TrimSpace(*username) + } + if text == "" { + return nil, false, nil + } + + tbl, q := models.UserQuery.QueryContext(ctx) + keyword := "%" + text + "%" + q = q.Where(tbl.Username.Like(keyword)).Or(tbl.Nickname.Like(keyword)) + users, err := q.Select(tbl.ID).Find() + if err != nil { + return nil, true, errorx.ErrDatabaseError.WithCause(err) + } + + ids := make([]int64, 0, len(users)) + for _, user := range users { + ids = append(ids, user.ID) + } + return ids, true, nil +} + +func (s *super) lookupOrderIDsByContent(ctx context.Context, contentID *int64, contentTitle *string) ([]int64, bool, error) { + var id int64 + if contentID != nil { + id = *contentID + } + title := "" + if contentTitle != nil { + title = strings.TrimSpace(*contentTitle) + } + if id <= 0 && title == "" { + return nil, false, nil + } + + tbl, q := models.OrderItemQuery.QueryContext(ctx) + if id > 0 { + q = q.Where(tbl.ContentID.Eq(id)) + } + + var items []*models.OrderItem + if title != "" { + // JSONB 字段需要使用 UnderlyingDB 做模糊查询。 + keyword := "%" + title + "%" + err := q.UnderlyingDB(). + Where("snapshot ->> 'content_title' ILIKE ?", keyword). + Select("order_id"). + Find(&items).Error + if err != nil { + return nil, true, errorx.ErrDatabaseError.WithCause(err) + } + } else { + list, err := q.Select(tbl.OrderID).Find() + if err != nil { + return nil, true, errorx.ErrDatabaseError.WithCause(err) + } + items = list + } + + idMap := make(map[int64]struct{}, len(items)) + for _, item := range items { + idMap[item.OrderID] = struct{}{} + } + ids := make([]int64, 0, len(idMap)) + for orderID := range idMap { + ids = append(ids, orderID) + } + return ids, true, nil +} + func (s *super) contentPriceMap(ctx context.Context, list []*models.Content) (map[int64]*models.ContentPrice, error) { if len(list) == 0 { return map[int64]*models.ContentPrice{}, nil