feat(backend): 授权订单列表接口——分页/筛选/汇总,支撑订单管理 tab
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user