feat(backend): 授权订单列表接口——分页/筛选/汇总,支撑订单管理 tab
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -112,6 +113,39 @@ func (h *PayHandler) Cancel(c *gin.Context) {
|
||||
util.RespondSuccess(c, gin.H{"canceled": canceled})
|
||||
}
|
||||
|
||||
// Purchases GET /api/v1/license/purchases — 订单管理 tab 数据源:分页/status 筛选/汇总。
|
||||
// 仅管理员可查看(handler 内判权,同 Purchase)。
|
||||
func (h *PayHandler) Purchases(c *gin.Context) {
|
||||
role := middleware.GetRole(c)
|
||||
if role != "admin" && role != "superadmin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "仅管理员可查看订单"})
|
||||
return
|
||||
}
|
||||
page, _ := strconv.Atoi(c.Query("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize, _ := strconv.Atoi(c.Query("page_size"))
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
status := c.Query("status")
|
||||
|
||||
list, err := h.svc.ListPurchases(middleware.GetShopID(c), page, pageSize, status)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrInvalidStatus) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
util.RespondSuccess(c, list)
|
||||
}
|
||||
|
||||
// PromoStatus GET /api/v1/license/promo-status — 本店首月特惠是否已享用(前端据此置灰特惠档)。
|
||||
func (h *PayHandler) PromoStatus(c *gin.Context) {
|
||||
used, err := h.svc.PromoUsed(middleware.GetShopID(c))
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/wangjia/jiu/backend/internal/middleware"
|
||||
"github.com/wangjia/jiu/backend/internal/model"
|
||||
"github.com/wangjia/jiu/backend/internal/service"
|
||||
"github.com/wangjia/jiu/backend/testutil"
|
||||
)
|
||||
|
||||
// setupPayRouter 独立路由:仅挂 payH.Purchases(仓里暂无 pay handler 测试先例,
|
||||
// 不复用 setupProtectedRouter 以免污染其他测试的路由表;判权写法与 Purchase/Cancel 同构)。
|
||||
func setupPayRouter(db *gorm.DB) *gin.Engine {
|
||||
payH := NewPayHandler(service.NewPayService(db, "http://pay.invalid", "test-secret", ""))
|
||||
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
api := r.Group("/api/v1")
|
||||
api.Use(middleware.JWT(db))
|
||||
api.GET("/license/purchases", payH.Purchases)
|
||||
return r
|
||||
}
|
||||
|
||||
func seedPurchase(t *testing.T, db *gorm.DB, shopID, userID uint64, otn, status string, amountMinor int64) {
|
||||
t.Helper()
|
||||
require.NoError(t, db.Create(&model.LicensePurchase{
|
||||
ShopID: shopID, UserID: userID, ProductBizCode: "monthly_standard",
|
||||
AmountMinor: amountMinor, Currency: "CNY", OutTradeNo: otn, Status: status,
|
||||
}).Error)
|
||||
}
|
||||
|
||||
func TestPayHandler_Purchases_OperatorForbidden(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "PHDL01")
|
||||
op := testutil.CreateTestUser(db, shop.ID, "op1", "pass", "operator")
|
||||
token := getAuthToken(op.ID, shop.ID, "operator")
|
||||
r := setupPayRouter(db)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/license/purchases", token, nil)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestPayHandler_Purchases_ReadonlyForbidden(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "PHDL02")
|
||||
ro := testutil.CreateTestUser(db, shop.ID, "ro1", "pass", "readonly")
|
||||
token := getAuthToken(ro.ID, shop.ID, "readonly")
|
||||
r := setupPayRouter(db)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/license/purchases", token, nil)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestPayHandler_Purchases_AdminOK(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "PHDL03")
|
||||
admin := testutil.CreateTestUser(db, shop.ID, "admin1", "pass", "admin")
|
||||
token := getAuthToken(admin.ID, shop.ID, "admin")
|
||||
seedPurchase(t, db, shop.ID, admin.ID, "phdl-1", "paid", 29900)
|
||||
seedPurchase(t, db, shop.ID, admin.ID, "phdl-2", "pending", 29900)
|
||||
r := setupPayRouter(db)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/license/purchases", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
resp := parseResponse(w)
|
||||
data, ok := resp["data"].(map[string]interface{})
|
||||
require.True(t, ok, "响应应含 data")
|
||||
assert.EqualValues(t, 2, data["total"])
|
||||
summary, ok := data["summary"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.EqualValues(t, 1, summary["paid_count"])
|
||||
assert.EqualValues(t, 1, summary["pending_count"])
|
||||
assert.EqualValues(t, 2, summary["total_count"])
|
||||
}
|
||||
|
||||
func TestPayHandler_Purchases_SuperadminOK(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "PHDL04")
|
||||
sa := testutil.CreateTestUser(db, shop.ID, "sa1", "pass", "superadmin")
|
||||
token := getAuthToken(sa.ID, shop.ID, "superadmin")
|
||||
r := setupPayRouter(db)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/license/purchases", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestPayHandler_Purchases_InvalidStatusBadRequest(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "PHDL05")
|
||||
admin := testutil.CreateTestUser(db, shop.ID, "admin2", "pass", "admin")
|
||||
token := getAuthToken(admin.ID, shop.ID, "admin")
|
||||
r := setupPayRouter(db)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/license/purchases?status=bogus", token, nil)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestPayHandler_Purchases_PageSizeClamped(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
shop := testutil.CreateTestShop(db, "PHDL06")
|
||||
admin := testutil.CreateTestUser(db, shop.ID, "admin3", "pass", "admin")
|
||||
token := getAuthToken(admin.ID, shop.ID, "admin")
|
||||
for i := 0; i < 3; i++ {
|
||||
seedPurchase(t, db, shop.ID, admin.ID, fmt.Sprintf("phdl6-%d", i), "paid", 100)
|
||||
}
|
||||
r := setupPayRouter(db)
|
||||
|
||||
// page_size 请求 500,服务端应上限 clamp 到 100(不报错,正常 200)
|
||||
w := makeRequest(r, "GET", "/api/v1/license/purchases?page_size=500", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
@@ -124,6 +124,7 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
license.GET("/purchase/:out_trade_no", payH.PurchaseStatus)
|
||||
license.POST("/purchase/:out_trade_no/cancel", payH.Cancel)
|
||||
license.GET("/promo-status", payH.PromoStatus)
|
||||
license.GET("/purchases", payH.Purchases)
|
||||
}
|
||||
|
||||
// 业务路由:ReadOnly + LicenseGuard(过期只读/锁定拦截写操作)
|
||||
|
||||
@@ -40,6 +40,7 @@ var (
|
||||
ErrPayAmount = errors.New("回调金额与订单不符")
|
||||
ErrPurchaseNotFound = errors.New("购买记录不存在")
|
||||
ErrPromoUsed = errors.New("首月特惠每个门店限购一次,本店已享受过")
|
||||
ErrInvalidStatus = errors.New("无效的订单状态")
|
||||
)
|
||||
|
||||
// PromoBizCode 新店首月特惠(¥1/30 天标准版),每个门店仅可购买一次。
|
||||
@@ -657,3 +658,140 @@ func (s *PayService) cancelOrder(orderNo string) (bool, error) {
|
||||
}
|
||||
return r.Data.Canceled, nil
|
||||
}
|
||||
|
||||
// ---------- ⑥ 订单列表(授权管理·订单管理 tab 数据源)----------
|
||||
|
||||
// PurchaseListItem 订单管理 tab 一行。pay_url 仅 pending 单输出(继续支付用),
|
||||
// paid/failed 抹空;amount 是 Deprecated 兼容串(同 formatMinor 口径)。
|
||||
type PurchaseListItem struct {
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
BizCode string `json:"product_biz_code"`
|
||||
AmountMinor int64 `json:"amount_minor"`
|
||||
Currency string `json:"currency"`
|
||||
Amount string `json:"amount"` // Deprecated: formatMinor(AmountMinor) 分转元字符串
|
||||
Status string `json:"status"`
|
||||
PayURL string `json:"pay_url"`
|
||||
UserName string `json:"user_name"` // 下单人显示名(LEFT JOIN users.real_name),查不到留空
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PaidAt *time.Time `json:"paid_at"`
|
||||
RenewedTo *time.Time `json:"renewed_to"`
|
||||
}
|
||||
|
||||
// PurchaseSummary 汇总统计:同店全量(不受分页/status 筛选影响)。
|
||||
type PurchaseSummary struct {
|
||||
PaidTotalMinor int64 `json:"paid_total_minor"`
|
||||
PaidCount int64 `json:"paid_count"`
|
||||
PendingCount int64 `json:"pending_count"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
// PurchaseList ListPurchases 响应体。
|
||||
type PurchaseList struct {
|
||||
Items []PurchaseListItem `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Summary PurchaseSummary `json:"summary"`
|
||||
}
|
||||
|
||||
// ListPurchases 本店订单列表:分页 + status 筛选 + 全量汇总。status 空=全部,
|
||||
// 非空须为 pending/paid/failed 之一(否则 ErrInvalidStatus,handler 回 400)。
|
||||
func (s *PayService) ListPurchases(shopID uint64, page, pageSize int, status string) (*PurchaseList, error) {
|
||||
if status != "" && status != "pending" && status != "paid" && status != "failed" {
|
||||
return nil, ErrInvalidStatus
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
// 每次从 s.db 重新起 query,避免 *gorm.DB 复用累加条件的坑。
|
||||
scope := func() *gorm.DB {
|
||||
q := s.db.Model(&model.LicensePurchase{}).Where("shop_id = ?", shopID)
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := scope().Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rows []model.LicensePurchase
|
||||
if err := scope().Order("created_at DESC, id DESC").
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 下单人显示名:批量查 users,查不到留空。
|
||||
userIDs := make([]uint64, 0, len(rows))
|
||||
seen := map[uint64]bool{}
|
||||
for _, p := range rows {
|
||||
if !seen[p.UserID] {
|
||||
seen[p.UserID] = true
|
||||
userIDs = append(userIDs, p.UserID)
|
||||
}
|
||||
}
|
||||
names := map[uint64]string{}
|
||||
if len(userIDs) > 0 {
|
||||
var users []model.User
|
||||
if err := s.db.Where("id IN ?", userIDs).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, u := range users {
|
||||
names[u.ID] = u.RealName
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]PurchaseListItem, 0, len(rows))
|
||||
for _, p := range rows {
|
||||
amount := formatMinor(p.AmountMinor)
|
||||
if amount == "" {
|
||||
amount = p.Amount
|
||||
}
|
||||
payURL := ""
|
||||
if p.Status == "pending" {
|
||||
payURL = p.PayURL
|
||||
}
|
||||
items = append(items, PurchaseListItem{
|
||||
OutTradeNo: p.OutTradeNo,
|
||||
BizCode: p.ProductBizCode,
|
||||
AmountMinor: p.AmountMinor,
|
||||
Currency: p.Currency,
|
||||
Amount: amount,
|
||||
Status: p.Status,
|
||||
PayURL: payURL,
|
||||
UserName: names[p.UserID],
|
||||
CreatedAt: p.CreatedAt,
|
||||
PaidAt: p.PaidAt,
|
||||
RenewedTo: p.RenewedTo,
|
||||
})
|
||||
}
|
||||
|
||||
// 汇总:同店全量,一次 GROUP BY 扫出后内存汇总,不受分页/status 筛选影响。
|
||||
var groups []struct {
|
||||
Status string
|
||||
Cnt int64
|
||||
Amt int64
|
||||
}
|
||||
if err := s.db.Model(&model.LicensePurchase{}).
|
||||
Select("status, COUNT(*) as cnt, COALESCE(SUM(amount_minor),0) as amt").
|
||||
Where("shop_id = ?", shopID).Group("status").Scan(&groups).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
summary := PurchaseSummary{}
|
||||
for _, g := range groups {
|
||||
summary.TotalCount += g.Cnt
|
||||
switch g.Status {
|
||||
case "paid":
|
||||
summary.PaidCount = g.Cnt
|
||||
summary.PaidTotalMinor = g.Amt
|
||||
case "pending":
|
||||
summary.PendingCount = g.Cnt
|
||||
}
|
||||
}
|
||||
|
||||
return &PurchaseList{Items: items, Total: total, Summary: summary}, nil
|
||||
}
|
||||
|
||||
@@ -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