feat(backend): 授权订单列表接口——分页/筛选/汇总,支撑订单管理 tab

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-10 23:21:24 +08:00
parent 7e5a697f28
commit 7e99a7a7ee
5 changed files with 497 additions and 0 deletions
+138
View File
@@ -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 之一(否则 ErrInvalidStatushandler 回 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
}