From e268176af527bc3a324c3d0092eae2b07dee68fa Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 18 Dec 2025 16:46:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8C=89=E5=86=85=E5=AE=B9ID=E3=80=81=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E8=8C=83=E5=9B=B4=E5=92=8C=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E9=87=91=E9=A2=9D=E8=8C=83=E5=9B=B4=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/http/tenant/dto/order_admin.go | 12 ++ backend/app/http/tenant/dto/order_me.go | 8 + backend/app/services/order.go | 134 ++++++++++++----- backend/app/services/order_test.go | 161 ++++++++++++++++++++- backend/docs/docs.go | 48 ++++++ backend/docs/swagger.json | 48 ++++++ backend/docs/swagger.yaml | 34 +++++ backend/tests/tenant.http | 10 ++ 8 files changed, 418 insertions(+), 37 deletions(-) diff --git a/backend/app/http/tenant/dto/order_admin.go b/backend/app/http/tenant/dto/order_admin.go index 96fb898..69c75bf 100644 --- a/backend/app/http/tenant/dto/order_admin.go +++ b/backend/app/http/tenant/dto/order_admin.go @@ -1,6 +1,8 @@ package dto import ( + "time" + "quyun/v2/app/requests" "quyun/v2/database/models" "quyun/v2/pkg/consts" @@ -12,8 +14,18 @@ type AdminOrderListFilter struct { requests.Pagination `json:",inline" query:",inline"` // UserID filters orders by buyer user id. UserID *int64 `json:"user_id,omitempty" query:"user_id"` + // ContentID filters orders by purchased content id (via order_items join). + ContentID *int64 `json:"content_id,omitempty" query:"content_id"` // Status filters orders by order status. Status *consts.OrderStatus `json:"status,omitempty" query:"status"` + // PaidAtFrom filters orders by paid_at >= this time. + PaidAtFrom *time.Time `json:"paid_at_from,omitempty" query:"paid_at_from"` + // PaidAtTo filters orders by paid_at <= this time. + PaidAtTo *time.Time `json:"paid_at_to,omitempty" query:"paid_at_to"` + // AmountPaidMin filters orders by amount_paid >= this amount (cents). + AmountPaidMin *int64 `json:"amount_paid_min,omitempty" query:"amount_paid_min"` + // AmountPaidMax filters orders by amount_paid <= this amount (cents). + AmountPaidMax *int64 `json:"amount_paid_max,omitempty" query:"amount_paid_max"` } // AdminOrderRefundForm defines payload for tenant-admin to refund an order. diff --git a/backend/app/http/tenant/dto/order_me.go b/backend/app/http/tenant/dto/order_me.go index e5528cc..2188bbd 100644 --- a/backend/app/http/tenant/dto/order_me.go +++ b/backend/app/http/tenant/dto/order_me.go @@ -1,6 +1,8 @@ package dto import ( + "time" + "quyun/v2/app/requests" "quyun/v2/pkg/consts" ) @@ -11,4 +13,10 @@ type MyOrderListFilter struct { requests.Pagination `json:",inline" query:",inline"` // Status filters orders by order status. Status *consts.OrderStatus `json:"status,omitempty" query:"status"` + // PaidAtFrom filters orders by paid_at >= this time. + PaidAtFrom *time.Time `json:"paid_at_from,omitempty" query:"paid_at_from"` + // PaidAtTo filters orders by paid_at <= this time. + PaidAtTo *time.Time `json:"paid_at_to,omitempty" query:"paid_at_to"` + // ContentID filters orders by purchased content id (via order_items join). + ContentID *int64 `json:"content_id,omitempty" query:"content_id"` } diff --git a/backend/app/services/order.go b/backend/app/services/order.go index e11cdf3..82e5209 100644 --- a/backend/app/services/order.go +++ b/backend/app/services/order.go @@ -23,42 +23,74 @@ import ( "go.ipao.vip/gen/types" ) +// PurchaseOrderSnapshot 为“内容购买订单”的下单快照(用于历史展示与争议审计)。 type PurchaseOrderSnapshot struct { - ContentID int64 `json:"content_id"` - ContentTitle string `json:"content_title"` - ContentUserID int64 `json:"content_user_id"` - ContentVisibility consts.ContentVisibility `json:"content_visibility"` - PreviewSeconds int32 `json:"preview_seconds"` - PreviewDownloadable bool `json:"preview_downloadable"` - Currency consts.Currency `json:"currency"` - PriceAmount int64 `json:"price_amount"` - DiscountType consts.DiscountType `json:"discount_type"` - DiscountValue int64 `json:"discount_value"` - DiscountStartAt *time.Time `json:"discount_start_at,omitempty"` - DiscountEndAt *time.Time `json:"discount_end_at,omitempty"` - AmountOriginal int64 `json:"amount_original"` - AmountDiscount int64 `json:"amount_discount"` - AmountPaid int64 `json:"amount_paid"` - PurchaseAt time.Time `json:"purchase_at"` - PurchaseIdempotency string `json:"purchase_idempotency_key,omitempty"` - PurchasePricingNotes string `json:"purchase_pricing_notes,omitempty"` + // ContentID 内容ID。 + ContentID int64 `json:"content_id"` + // ContentTitle 内容标题(下单时快照,避免事后改名影响历史订单展示)。 + ContentTitle string `json:"content_title"` + // ContentUserID 内容作者用户ID(用于审计与后续分成扩展)。 + ContentUserID int64 `json:"content_user_id"` + // ContentVisibility 下单时的可见性快照。 + ContentVisibility consts.ContentVisibility `json:"content_visibility"` + // PreviewSeconds 下单时的试看秒数快照。 + PreviewSeconds int32 `json:"preview_seconds"` + // PreviewDownloadable 下单时的试看是否可下载快照(当前固定为 false)。 + PreviewDownloadable bool `json:"preview_downloadable"` + // Currency 币种:当前固定 CNY(金额单位为分)。 + Currency consts.Currency `json:"currency"` + // PriceAmount 基础价格(分)。 + PriceAmount int64 `json:"price_amount"` + // DiscountType 折扣类型(none/percent/amount)。 + DiscountType consts.DiscountType `json:"discount_type"` + // DiscountValue 折扣值(percent=0-100;amount=分)。 + DiscountValue int64 `json:"discount_value"` + // DiscountStartAt 折扣开始时间(可选)。 + DiscountStartAt *time.Time `json:"discount_start_at,omitempty"` + // DiscountEndAt 折扣结束时间(可选)。 + DiscountEndAt *time.Time `json:"discount_end_at,omitempty"` + // AmountOriginal 原价金额(分)。 + AmountOriginal int64 `json:"amount_original"` + // AmountDiscount 优惠金额(分)。 + AmountDiscount int64 `json:"amount_discount"` + // AmountPaid 实付金额(分)。 + AmountPaid int64 `json:"amount_paid"` + // PurchaseAt 下单时间(逻辑时间)。 + PurchaseAt time.Time `json:"purchase_at"` + // PurchaseIdempotency 幂等键(可选)。 + PurchaseIdempotency string `json:"purchase_idempotency_key,omitempty"` + // PurchasePricingNotes 价格计算补充说明(可选,便于排查争议)。 + PurchasePricingNotes string `json:"purchase_pricing_notes,omitempty"` } +// OrderItemSnapshot 为“订单明细”的内容快照。 type OrderItemSnapshot struct { - ContentID int64 `json:"content_id"` - ContentTitle string `json:"content_title"` - ContentUserID int64 `json:"content_user_id"` - AmountPaid int64 `json:"amount_paid"` + // ContentID 内容ID。 + ContentID int64 `json:"content_id"` + // ContentTitle 内容标题快照。 + ContentTitle string `json:"content_title"` + // ContentUserID 内容作者用户ID。 + ContentUserID int64 `json:"content_user_id"` + // AmountPaid 该行实付金额(分)。 + AmountPaid int64 `json:"amount_paid"` } +// TopupOrderSnapshot 为“后台充值订单”的快照(用于审计与追责)。 type TopupOrderSnapshot struct { - OperatorUserID int64 `json:"operator_user_id"` - TargetUserID int64 `json:"target_user_id"` - Amount int64 `json:"amount"` - Currency consts.Currency `json:"currency"` - Reason string `json:"reason,omitempty"` - IdempotencyKey string `json:"idempotency_key,omitempty"` - TopupAt time.Time `json:"topup_at"` + // OperatorUserID 充值操作人用户ID(租户管理员)。 + OperatorUserID int64 `json:"operator_user_id"` + // TargetUserID 充值目标用户ID(租户成员)。 + TargetUserID int64 `json:"target_user_id"` + // Amount 充值金额(分)。 + Amount int64 `json:"amount"` + // Currency 币种:当前固定 CNY(金额单位为分)。 + Currency consts.Currency `json:"currency"` + // Reason 充值原因(可选,强烈建议填写用于审计)。 + Reason string `json:"reason,omitempty"` + // IdempotencyKey 幂等键(可选)。 + IdempotencyKey string `json:"idempotency_key,omitempty"` + // TopupAt 充值时间(逻辑时间)。 + TopupAt time.Time `json:"topup_at"` } // PurchaseContentParams 定义“租户内使用余额购买内容”的入参。 @@ -237,9 +269,10 @@ func (s *order) MyOrderPage( } logrus.WithFields(logrus.Fields{ - "tenant_id": tenantID, - "user_id": userID, - "status": lo.FromPtr(filter.Status), + "tenant_id": tenantID, + "user_id": userID, + "status": lo.FromPtr(filter.Status), + "content_id": lo.FromPtr(filter.ContentID), }).Info("services.order.me.page") filter.Pagination.Format() @@ -254,6 +287,18 @@ func (s *order) MyOrderPage( if filter.Status != nil { conds = append(conds, tbl.Status.Eq(*filter.Status)) } + if filter.PaidAtFrom != nil { + conds = append(conds, tbl.PaidAt.Gte(*filter.PaidAtFrom)) + } + if filter.PaidAtTo != nil { + conds = append(conds, tbl.PaidAt.Lte(*filter.PaidAtTo)) + } + if filter.ContentID != nil && *filter.ContentID > 0 { + oiTbl, _ := models.OrderItemQuery.QueryContext(ctx) + query = query.LeftJoin(oiTbl, oiTbl.OrderID.EqCol(tbl.ID)) + conds = append(conds, oiTbl.ContentID.Eq(*filter.ContentID)) + query = query.Group(tbl.ID) + } items, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit)) if err != nil { @@ -305,9 +350,10 @@ func (s *order) AdminOrderPage( } logrus.WithFields(logrus.Fields{ - "tenant_id": tenantID, - "user_id": lo.FromPtr(filter.UserID), - "status": lo.FromPtr(filter.Status), + "tenant_id": tenantID, + "user_id": lo.FromPtr(filter.UserID), + "status": lo.FromPtr(filter.Status), + "content_id": lo.FromPtr(filter.ContentID), }).Info("services.order.admin.page") filter.Pagination.Format() @@ -322,6 +368,24 @@ func (s *order) AdminOrderPage( if filter.Status != nil { conds = append(conds, tbl.Status.Eq(*filter.Status)) } + if filter.PaidAtFrom != nil { + conds = append(conds, tbl.PaidAt.Gte(*filter.PaidAtFrom)) + } + if filter.PaidAtTo != nil { + conds = append(conds, tbl.PaidAt.Lte(*filter.PaidAtTo)) + } + if filter.AmountPaidMin != nil { + conds = append(conds, tbl.AmountPaid.Gte(*filter.AmountPaidMin)) + } + if filter.AmountPaidMax != nil { + conds = append(conds, tbl.AmountPaid.Lte(*filter.AmountPaidMax)) + } + if filter.ContentID != nil && *filter.ContentID > 0 { + oiTbl, _ := models.OrderItemQuery.QueryContext(ctx) + query = query.LeftJoin(oiTbl, oiTbl.OrderID.EqCol(tbl.ID)) + conds = append(conds, oiTbl.ContentID.Eq(*filter.ContentID)) + query = query.Group(tbl.ID) + } items, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit)) if err != nil { diff --git a/backend/app/services/order_test.go b/backend/app/services/order_test.go index 983735f..7d8ba6a 100644 --- a/backend/app/services/order_test.go +++ b/backend/app/services/order_test.go @@ -16,6 +16,7 @@ import ( "quyun/v2/database/models" "quyun/v2/pkg/consts" + "github.com/samber/lo" . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/suite" @@ -184,10 +185,11 @@ func (s *OrderTestSuite) Test_AdminTopupUser() { func (s *OrderTestSuite) Test_MyOrderPage() { Convey("Order.MyOrderPage", s.T(), func() { ctx := s.T().Context() + now := time.Now().UTC() tenantID := int64(1) userID := int64(2) - s.truncate(ctx, models.TableNameOrder, models.TableNameOrderItem) + s.truncate(ctx, models.TableNameOrderItem, models.TableNameOrder) Convey("参数非法应返回错误", func() { _, err := Order.MyOrderPage(ctx, 0, userID, &dto.MyOrderListFilter{}) @@ -199,6 +201,64 @@ func (s *OrderTestSuite) Test_MyOrderPage() { So(err, ShouldBeNil) So(pager.Total, ShouldEqual, 0) }) + + Convey("按 content_id 过滤", func() { + o1 := &models.Order{ + TenantID: tenantID, + UserID: userID, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusPaid, + Currency: consts.CurrencyCNY, + AmountPaid: 100, + Snapshot: types.JSON([]byte("{}")), + PaidAt: now, + CreatedAt: now, + UpdatedAt: now, + } + So(o1.Create(ctx), ShouldBeNil) + So((&models.OrderItem{ + TenantID: tenantID, + UserID: userID, + OrderID: o1.ID, + ContentID: 111, + ContentUserID: 1, + AmountPaid: 100, + Snapshot: types.JSON([]byte("{}")), + CreatedAt: now, + UpdatedAt: now, + }).Create(ctx), ShouldBeNil) + + o2 := &models.Order{ + TenantID: tenantID, + UserID: userID, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusPaid, + Currency: consts.CurrencyCNY, + AmountPaid: 200, + Snapshot: types.JSON([]byte("{}")), + PaidAt: now.Add(time.Minute), + CreatedAt: now.Add(time.Minute), + UpdatedAt: now.Add(time.Minute), + } + So(o2.Create(ctx), ShouldBeNil) + So((&models.OrderItem{ + TenantID: tenantID, + UserID: userID, + OrderID: o2.ID, + ContentID: 222, + ContentUserID: 1, + AmountPaid: 200, + Snapshot: types.JSON([]byte("{}")), + CreatedAt: now.Add(time.Minute), + UpdatedAt: now.Add(time.Minute), + }).Create(ctx), ShouldBeNil) + + pager, err := Order.MyOrderPage(ctx, tenantID, userID, &dto.MyOrderListFilter{ + ContentID: lo.ToPtr(int64(111)), + }) + So(err, ShouldBeNil) + So(pager.Total, ShouldEqual, 1) + }) }) } @@ -225,9 +285,10 @@ func (s *OrderTestSuite) Test_MyOrderDetail() { func (s *OrderTestSuite) Test_AdminOrderPage() { Convey("Order.AdminOrderPage", s.T(), func() { ctx := s.T().Context() + now := time.Now().UTC() tenantID := int64(1) - s.truncate(ctx, models.TableNameOrder, models.TableNameOrderItem) + s.truncate(ctx, models.TableNameOrderItem, models.TableNameOrder) Convey("参数非法应返回错误", func() { _, err := Order.AdminOrderPage(ctx, 0, &dto.AdminOrderListFilter{}) @@ -239,6 +300,102 @@ func (s *OrderTestSuite) Test_AdminOrderPage() { So(err, ShouldBeNil) So(pager.Total, ShouldEqual, 0) }) + + Convey("按 paid_at 时间窗过滤", func() { + o1 := &models.Order{ + TenantID: tenantID, + UserID: 2, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusPaid, + Currency: consts.CurrencyCNY, + AmountPaid: 100, + Snapshot: types.JSON([]byte("{}")), + PaidAt: now.Add(-time.Hour), + CreatedAt: now.Add(-time.Hour), + UpdatedAt: now.Add(-time.Hour), + } + So(o1.Create(ctx), ShouldBeNil) + So((&models.OrderItem{ + TenantID: tenantID, + UserID: 2, + OrderID: o1.ID, + ContentID: 333, + ContentUserID: 1, + AmountPaid: 100, + Snapshot: types.JSON([]byte("{}")), + CreatedAt: now.Add(-time.Hour), + UpdatedAt: now.Add(-time.Hour), + }).Create(ctx), ShouldBeNil) + + o2 := &models.Order{ + TenantID: tenantID, + UserID: 3, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusPaid, + Currency: consts.CurrencyCNY, + AmountPaid: 200, + Snapshot: types.JSON([]byte("{}")), + PaidAt: now, + CreatedAt: now, + UpdatedAt: now, + } + So(o2.Create(ctx), ShouldBeNil) + So((&models.OrderItem{ + TenantID: tenantID, + UserID: 3, + OrderID: o2.ID, + ContentID: 444, + ContentUserID: 1, + AmountPaid: 200, + Snapshot: types.JSON([]byte("{}")), + CreatedAt: now, + UpdatedAt: now, + }).Create(ctx), ShouldBeNil) + + from := now.Add(-10 * time.Minute) + to := now.Add(10 * time.Minute) + pager, err := Order.AdminOrderPage(ctx, tenantID, &dto.AdminOrderListFilter{ + PaidAtFrom: &from, + PaidAtTo: &to, + }) + So(err, ShouldBeNil) + So(pager.Total, ShouldEqual, 1) + }) + + Convey("按 content_id 过滤", func() { + s.truncate(ctx, models.TableNameOrderItem, models.TableNameOrder) + + o1 := &models.Order{ + TenantID: tenantID, + UserID: 2, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusPaid, + Currency: consts.CurrencyCNY, + AmountPaid: 100, + Snapshot: types.JSON([]byte("{}")), + PaidAt: now, + CreatedAt: now, + UpdatedAt: now, + } + So(o1.Create(ctx), ShouldBeNil) + So((&models.OrderItem{ + TenantID: tenantID, + UserID: 2, + OrderID: o1.ID, + ContentID: 555, + ContentUserID: 1, + AmountPaid: 100, + Snapshot: types.JSON([]byte("{}")), + CreatedAt: now, + UpdatedAt: now, + }).Create(ctx), ShouldBeNil) + + pager, err := Order.AdminOrderPage(ctx, tenantID, &dto.AdminOrderListFilter{ + ContentID: lo.ToPtr(int64(555)), + }) + So(err, ShouldBeNil) + So(pager.Total, ShouldEqual, 1) + }) }) } diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 9a04643..0e1a65a 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -623,6 +623,24 @@ const docTemplate = `{ "in": "path", "required": true }, + { + "type": "integer", + "description": "AmountPaidMax filters orders by amount_paid \u003c= this amount (cents).", + "name": "amount_paid_max", + "in": "query" + }, + { + "type": "integer", + "description": "AmountPaidMin filters orders by amount_paid \u003e= this amount (cents).", + "name": "amount_paid_min", + "in": "query" + }, + { + "type": "integer", + "description": "ContentID filters orders by purchased content id (via order_items join).", + "name": "content_id", + "in": "query" + }, { "type": "integer", "description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).", @@ -635,6 +653,18 @@ const docTemplate = `{ "name": "page", "in": "query" }, + { + "type": "string", + "description": "PaidAtFrom filters orders by paid_at \u003e= this time.", + "name": "paid_at_from", + "in": "query" + }, + { + "type": "string", + "description": "PaidAtTo filters orders by paid_at \u003c= this time.", + "name": "paid_at_to", + "in": "query" + }, { "enum": [ "created", @@ -1172,6 +1202,12 @@ const docTemplate = `{ "in": "path", "required": true }, + { + "type": "integer", + "description": "ContentID filters orders by purchased content id (via order_items join).", + "name": "content_id", + "in": "query" + }, { "type": "integer", "description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).", @@ -1184,6 +1220,18 @@ const docTemplate = `{ "name": "page", "in": "query" }, + { + "type": "string", + "description": "PaidAtFrom filters orders by paid_at \u003e= this time.", + "name": "paid_at_from", + "in": "query" + }, + { + "type": "string", + "description": "PaidAtTo filters orders by paid_at \u003c= this time.", + "name": "paid_at_to", + "in": "query" + }, { "enum": [ "created", diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index c7983bb..d816b6d 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -617,6 +617,24 @@ "in": "path", "required": true }, + { + "type": "integer", + "description": "AmountPaidMax filters orders by amount_paid \u003c= this amount (cents).", + "name": "amount_paid_max", + "in": "query" + }, + { + "type": "integer", + "description": "AmountPaidMin filters orders by amount_paid \u003e= this amount (cents).", + "name": "amount_paid_min", + "in": "query" + }, + { + "type": "integer", + "description": "ContentID filters orders by purchased content id (via order_items join).", + "name": "content_id", + "in": "query" + }, { "type": "integer", "description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).", @@ -629,6 +647,18 @@ "name": "page", "in": "query" }, + { + "type": "string", + "description": "PaidAtFrom filters orders by paid_at \u003e= this time.", + "name": "paid_at_from", + "in": "query" + }, + { + "type": "string", + "description": "PaidAtTo filters orders by paid_at \u003c= this time.", + "name": "paid_at_to", + "in": "query" + }, { "enum": [ "created", @@ -1166,6 +1196,12 @@ "in": "path", "required": true }, + { + "type": "integer", + "description": "ContentID filters orders by purchased content id (via order_items join).", + "name": "content_id", + "in": "query" + }, { "type": "integer", "description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).", @@ -1178,6 +1214,18 @@ "name": "page", "in": "query" }, + { + "type": "string", + "description": "PaidAtFrom filters orders by paid_at \u003e= this time.", + "name": "paid_at_from", + "in": "query" + }, + { + "type": "string", + "description": "PaidAtTo filters orders by paid_at \u003c= this time.", + "name": "paid_at_to", + "in": "query" + }, { "enum": [ "created", diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 9fc4e2f..6871cee 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1386,6 +1386,19 @@ paths: name: tenantCode required: true type: string + - description: AmountPaidMax filters orders by amount_paid <= this amount (cents). + in: query + name: amount_paid_max + type: integer + - description: AmountPaidMin filters orders by amount_paid >= this amount (cents). + in: query + name: amount_paid_min + type: integer + - description: ContentID filters orders by purchased content id (via order_items + join). + in: query + name: content_id + type: integer - description: Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10). in: query @@ -1395,6 +1408,14 @@ paths: in: query name: page type: integer + - description: PaidAtFrom filters orders by paid_at >= this time. + in: query + name: paid_at_from + type: string + - description: PaidAtTo filters orders by paid_at <= this time. + in: query + name: paid_at_to + type: string - description: Status filters orders by order status. enum: - created @@ -1745,6 +1766,11 @@ paths: name: tenantCode required: true type: string + - description: ContentID filters orders by purchased content id (via order_items + join). + in: query + name: content_id + type: integer - description: Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10). in: query @@ -1754,6 +1780,14 @@ paths: in: query name: page type: integer + - description: PaidAtFrom filters orders by paid_at >= this time. + in: query + name: paid_at_from + type: string + - description: PaidAtTo filters orders by paid_at <= this time. + in: query + name: paid_at_to + type: string - description: Status filters orders by order status. enum: - created diff --git a/backend/tests/tenant.http b/backend/tests/tenant.http index d75e3ff..05ab1df 100644 --- a/backend/tests/tenant.http +++ b/backend/tests/tenant.http @@ -57,6 +57,11 @@ GET {{ host }}/t/{{ tenantCode }}/v1/orders?page=1&limit=10 Content-Type: application/json Authorization: Bearer {{ token }} +### Tenant - My orders list (filter by status/content_id/paid_at window) +GET {{ host }}/t/{{ tenantCode }}/v1/orders?page=1&limit=10&status=paid&content_id={{ contentID }} +Content-Type: application/json +Authorization: Bearer {{ token }} + ### Tenant - My order detail GET {{ host }}/t/{{ tenantCode }}/v1/orders/{{ orderID }} Content-Type: application/json @@ -113,6 +118,11 @@ GET {{ host }}/t/{{ tenantCode }}/v1/admin/orders?page=1&limit=10 Content-Type: application/json Authorization: Bearer {{ token }} +### Tenant Admin - Orders list (filter by user_id/content_id/paid_at range/amount_paid range) +GET {{ host }}/t/{{ tenantCode }}/v1/admin/orders?page=1&limit=10&user_id=2&content_id={{ contentID }}&paid_at_from=2025-01-01T00:00:00Z&paid_at_to=2026-01-01T00:00:00Z&amount_paid_min=1&amount_paid_max=99999999 +Content-Type: application/json +Authorization: Bearer {{ token }} + ### Tenant Admin - Order detail @orderID = 1 GET {{ host }}/t/{{ tenantCode }}/v1/admin/orders/{{ orderID }}