feat(backend): 授权订单列表接口——分页/筛选/汇总,支撑订单管理 tab
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,25 @@ func createPendingPurchase(t *testing.T, db *gorm.DB, shopID uint64, bizCode str
|
||||
return p
|
||||
}
|
||||
|
||||
// createPurchaseFull ListPurchases 测试专用:可控 userID/status/pay_url/created_at/paid_at/renewed_to。
|
||||
func createPurchaseFull(t *testing.T, db *gorm.DB, shopID, userID uint64, bizCode string, amountMinor int64, otn, status, payURL string, createdAt time.Time) *model.LicensePurchase {
|
||||
t.Helper()
|
||||
p := &model.LicensePurchase{
|
||||
ShopID: shopID, UserID: userID, ProductBizCode: bizCode, AmountMinor: amountMinor,
|
||||
Currency: "CNY", OutTradeNo: otn, Status: status, PayURL: payURL,
|
||||
}
|
||||
require.NoError(t, db.Create(p).Error)
|
||||
require.NoError(t, db.Model(&model.LicensePurchase{}).Where("id = ?", p.ID).Update("created_at", createdAt).Error)
|
||||
if status == "paid" {
|
||||
paidAt := createdAt.Add(time.Minute)
|
||||
renewedTo := createdAt.AddDate(0, 0, 30)
|
||||
require.NoError(t, db.Model(&model.LicensePurchase{}).Where("id = ?", p.ID).
|
||||
Updates(map[string]any{"paid_at": paidAt, "renewed_to": renewedTo}).Error)
|
||||
}
|
||||
require.NoError(t, db.First(p, p.ID).Error)
|
||||
return p
|
||||
}
|
||||
|
||||
// createStalePendingPurchase 建一条 created_at 在 5 分钟对账窗口之外的 pending 购买单,
|
||||
// 供 reconcileOnce 测试(该函数只捞 created_at < now-5min 的 pending 单)。
|
||||
func createStalePendingPurchase(t *testing.T, db *gorm.DB, shopID uint64, bizCode string, amountMinor int64, currency, otn string) *model.LicensePurchase {
|
||||
@@ -737,3 +756,189 @@ func TestCancelPurchase_NonPendingNoOpNoExternalCall(t *testing.T) {
|
||||
require.NoError(t, db.Where("out_trade_no = ?", "yanmei-otn-cancel-4").First(&got).Error)
|
||||
assert.Equal(t, "paid", got.Status, "非 pending 单状态不变")
|
||||
}
|
||||
|
||||
// ---------- ⑥ 订单列表(订单管理 tab)----------
|
||||
|
||||
// TestListPurchases_ShopIsolation:3 店共 7 单,A 店列表绝不含 B/C 店的单。
|
||||
func TestListPurchases_ShopIsolation(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
svc := newTestPaySvc(db, "http://pay.invalid")
|
||||
shopA := testutil.CreateTestShop(db, "PAY100")
|
||||
shopB := testutil.CreateTestShop(db, "PAY101")
|
||||
shopC := testutil.CreateTestShop(db, "PAY102")
|
||||
|
||||
base := time.Now().Add(-time.Hour)
|
||||
createPurchaseFull(t, db, shopA.ID, 1, "annual_pro", 599900, "iso-a-1", "paid", "", base)
|
||||
createPurchaseFull(t, db, shopA.ID, 1, "monthly_standard", 29900, "iso-a-2", "pending", "https://pay.example.com/a2", base.Add(time.Minute))
|
||||
createPurchaseFull(t, db, shopA.ID, 1, "monthly_standard", 29900, "iso-a-3", "failed", "", base.Add(2*time.Minute))
|
||||
createPurchaseFull(t, db, shopB.ID, 1, "annual_standard", 199900, "iso-b-1", "paid", "", base)
|
||||
createPurchaseFull(t, db, shopB.ID, 1, "monthly_pro", 99900, "iso-b-2", "pending", "https://pay.example.com/b2", base.Add(time.Minute))
|
||||
createPurchaseFull(t, db, shopC.ID, 1, "monthly_standard", 29900, "iso-c-1", "paid", "", base)
|
||||
createPurchaseFull(t, db, shopC.ID, 1, "monthly_standard", 29900, "iso-c-2", "pending", "https://pay.example.com/c2", base.Add(time.Minute))
|
||||
|
||||
list, err := svc.ListPurchases(shopA.ID, 1, 20, "")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 3, list.Total)
|
||||
assert.Len(t, list.Items, 3)
|
||||
for _, item := range list.Items {
|
||||
assert.Contains(t, []string{"iso-a-1", "iso-a-2", "iso-a-3"}, item.OutTradeNo, "A 店列表不得混入 B/C 店订单")
|
||||
}
|
||||
assert.EqualValues(t, 3, list.Summary.TotalCount)
|
||||
}
|
||||
|
||||
// TestListPurchases_OrderAndPagination:按 created_at DESC 排序,分页边界正确。
|
||||
func TestListPurchases_OrderAndPagination(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
svc := newTestPaySvc(db, "http://pay.invalid")
|
||||
shop := testutil.CreateTestShop(db, "PAY103")
|
||||
|
||||
base := time.Now().Add(-time.Hour)
|
||||
// otn-1 最早,otn-5 最晚
|
||||
for i := 1; i <= 5; i++ {
|
||||
createPurchaseFull(t, db, shop.ID, 1, "monthly_standard", 29900,
|
||||
fmt.Sprintf("order-%d", i), "paid", "", base.Add(time.Duration(i)*time.Minute))
|
||||
}
|
||||
|
||||
// page=1 size=2:最新两条 order-5, order-4
|
||||
p1, err := svc.ListPurchases(shop.ID, 1, 2, "")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 5, p1.Total)
|
||||
require.Len(t, p1.Items, 2)
|
||||
assert.Equal(t, "order-5", p1.Items[0].OutTradeNo)
|
||||
assert.Equal(t, "order-4", p1.Items[1].OutTradeNo)
|
||||
|
||||
// page=2 size=2:order-3, order-2
|
||||
p2, err := svc.ListPurchases(shop.ID, 2, 2, "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, p2.Items, 2)
|
||||
assert.Equal(t, "order-3", p2.Items[0].OutTradeNo)
|
||||
assert.Equal(t, "order-2", p2.Items[1].OutTradeNo)
|
||||
|
||||
// page=3 size=2:仅剩 order-1(边界:末页不满)
|
||||
p3, err := svc.ListPurchases(shop.ID, 3, 2, "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, p3.Items, 1)
|
||||
assert.Equal(t, "order-1", p3.Items[0].OutTradeNo)
|
||||
|
||||
// page=4 size=2:越界返回空
|
||||
p4, err := svc.ListPurchases(shop.ID, 4, 2, "")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, p4.Items)
|
||||
assert.EqualValues(t, 5, p4.Total)
|
||||
}
|
||||
|
||||
// TestListPurchases_StatusFilter:status 筛选生效;非法 status 报 ErrInvalidStatus。
|
||||
func TestListPurchases_StatusFilter(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
svc := newTestPaySvc(db, "http://pay.invalid")
|
||||
shop := testutil.CreateTestShop(db, "PAY104")
|
||||
|
||||
base := time.Now().Add(-time.Hour)
|
||||
createPurchaseFull(t, db, shop.ID, 1, "monthly_standard", 29900, "flt-1", "paid", "", base)
|
||||
createPurchaseFull(t, db, shop.ID, 1, "monthly_standard", 29900, "flt-2", "paid", "", base.Add(time.Minute))
|
||||
createPurchaseFull(t, db, shop.ID, 1, "monthly_standard", 29900, "flt-3", "pending", "https://pay.example.com/flt-3", base.Add(2*time.Minute))
|
||||
createPurchaseFull(t, db, shop.ID, 1, "monthly_standard", 29900, "flt-4", "failed", "", base.Add(3*time.Minute))
|
||||
|
||||
paid, err := svc.ListPurchases(shop.ID, 1, 20, "paid")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 2, paid.Total)
|
||||
for _, item := range paid.Items {
|
||||
assert.Equal(t, "paid", item.Status)
|
||||
}
|
||||
|
||||
pending, err := svc.ListPurchases(shop.ID, 1, 20, "pending")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1, pending.Total)
|
||||
assert.Equal(t, "flt-3", pending.Items[0].OutTradeNo)
|
||||
|
||||
failed, err := svc.ListPurchases(shop.ID, 1, 20, "failed")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 1, failed.Total)
|
||||
|
||||
all, err := svc.ListPurchases(shop.ID, 1, 20, "")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 4, all.Total)
|
||||
|
||||
_, err = svc.ListPurchases(shop.ID, 1, 20, "bogus")
|
||||
assert.ErrorIs(t, err, ErrInvalidStatus)
|
||||
}
|
||||
|
||||
// TestListPurchases_SummaryStableAcrossFilterAndPagination:summary 四值只看店铺全量,
|
||||
// 不随 status 筛选/分页变化。
|
||||
func TestListPurchases_SummaryStableAcrossFilterAndPagination(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
svc := newTestPaySvc(db, "http://pay.invalid")
|
||||
shop := testutil.CreateTestShop(db, "PAY105")
|
||||
|
||||
base := time.Now().Add(-time.Hour)
|
||||
createPurchaseFull(t, db, shop.ID, 1, "annual_pro", 599900, "sum-1", "paid", "", base)
|
||||
createPurchaseFull(t, db, shop.ID, 1, "monthly_standard", 29900, "sum-2", "paid", "", base.Add(time.Minute))
|
||||
createPurchaseFull(t, db, shop.ID, 1, "monthly_standard", 29900, "sum-3", "pending", "https://pay.example.com/sum-3", base.Add(2*time.Minute))
|
||||
createPurchaseFull(t, db, shop.ID, 1, "monthly_standard", 29900, "sum-4", "failed", "", base.Add(3*time.Minute))
|
||||
|
||||
wantPaidTotal := int64(599900 + 29900)
|
||||
wantPaidCount := int64(2)
|
||||
wantPendingCount := int64(1)
|
||||
wantTotalCount := int64(4)
|
||||
|
||||
all, err := svc.ListPurchases(shop.ID, 1, 20, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, wantPaidTotal, all.Summary.PaidTotalMinor)
|
||||
assert.Equal(t, wantPaidCount, all.Summary.PaidCount)
|
||||
assert.Equal(t, wantPendingCount, all.Summary.PendingCount)
|
||||
assert.Equal(t, wantTotalCount, all.Summary.TotalCount)
|
||||
|
||||
// status=paid 筛选 + 分页只影响 items/total,不影响 summary
|
||||
filtered, err := svc.ListPurchases(shop.ID, 1, 1, "paid")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 2, filtered.Total, "total 是筛选后的计数")
|
||||
assert.Len(t, filtered.Items, 1, "分页只影响 items 条数")
|
||||
assert.Equal(t, wantPaidTotal, filtered.Summary.PaidTotalMinor)
|
||||
assert.Equal(t, wantPaidCount, filtered.Summary.PaidCount)
|
||||
assert.Equal(t, wantPendingCount, filtered.Summary.PendingCount)
|
||||
assert.Equal(t, wantTotalCount, filtered.Summary.TotalCount)
|
||||
}
|
||||
|
||||
// TestListPurchases_PayURLOnlyOnPending:pending 单带 pay_url,paid 单抹空。
|
||||
func TestListPurchases_PayURLOnlyOnPending(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
svc := newTestPaySvc(db, "http://pay.invalid")
|
||||
shop := testutil.CreateTestShop(db, "PAY106")
|
||||
|
||||
base := time.Now().Add(-time.Hour)
|
||||
createPurchaseFull(t, db, shop.ID, 1, "monthly_standard", 29900, "url-pending", "pending", "https://pay.example.com/url-pending", base)
|
||||
createPurchaseFull(t, db, shop.ID, 1, "monthly_standard", 29900, "url-paid", "paid", "https://pay.example.com/url-paid-stale", base.Add(time.Minute))
|
||||
|
||||
list, err := svc.ListPurchases(shop.ID, 1, 20, "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 2)
|
||||
|
||||
byOtn := map[string]PurchaseListItem{}
|
||||
for _, item := range list.Items {
|
||||
byOtn[item.OutTradeNo] = item
|
||||
}
|
||||
assert.Equal(t, "https://pay.example.com/url-pending", byOtn["url-pending"].PayURL)
|
||||
assert.Empty(t, byOtn["url-paid"].PayURL, "paid 单 pay_url 必须抹空")
|
||||
}
|
||||
|
||||
// TestListPurchases_UserNameJoin:user_name 取下单人 real_name,查不到留空。
|
||||
func TestListPurchases_UserNameJoin(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
svc := newTestPaySvc(db, "http://pay.invalid")
|
||||
shop := testutil.CreateTestShop(db, "PAY107")
|
||||
user := testutil.CreateTestUser(db, shop.ID, "boss", "pass", "admin")
|
||||
require.NoError(t, db.Model(user).Update("real_name", "王老板").Error)
|
||||
|
||||
base := time.Now().Add(-time.Hour)
|
||||
createPurchaseFull(t, db, shop.ID, user.ID, "annual_pro", 599900, "un-known", "paid", "", base)
|
||||
createPurchaseFull(t, db, shop.ID, 999999, "annual_pro", 599900, "un-missing", "paid", "", base.Add(time.Minute))
|
||||
|
||||
list, err := svc.ListPurchases(shop.ID, 1, 20, "")
|
||||
require.NoError(t, err)
|
||||
byOtn := map[string]PurchaseListItem{}
|
||||
for _, item := range list.Items {
|
||||
byOtn[item.OutTradeNo] = item
|
||||
}
|
||||
assert.Equal(t, "王老板", byOtn["un-known"].UserName)
|
||||
assert.Empty(t, byOtn["un-missing"].UserName, "查不到用户留空")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user