fix(distribution): 人工批次窗口期防退款穿透与退款中途金额单调性加固
This commit is contained in:
@@ -784,6 +784,8 @@ func writeError(c *gin.Context, err error) {
|
||||
response.Error(c, http.StatusForbidden, "admin_disabled", "分销管理员账号已被禁用")
|
||||
case errors.Is(err, ErrInvalidAmount), errors.Is(err, ErrInvalidRate), errors.Is(err, ErrInvalidServiceUnit), errors.Is(err, ErrUnsupportedMode), errors.Is(err, ErrInvalidOrderKind), errors.Is(err, ErrInvalidPeriod), errors.Is(err, ErrBatchAlreadySettled), errors.Is(err, ErrSettlementBatchEmpty), errors.Is(err, ErrSettlementTotalMismatch):
|
||||
response.BadRequest(c, "分销规则参数不正确")
|
||||
case errors.Is(err, ErrSettlementBatchStale):
|
||||
response.BadRequest(c, "批次内订单发生退款变动或维权,该批次已失效,请作废后重新生成")
|
||||
case errors.Is(err, ErrInvalidSettlementHold):
|
||||
response.BadRequest(c, "结算冷却天数须在 0 到 365 之间")
|
||||
case errors.Is(err, ErrBatchNotCancellable):
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -47,6 +48,7 @@ var (
|
||||
ErrInvalidListingStatus = errors.New("invalid listing status")
|
||||
ErrInvalidSettlementHold = errors.New("settlement hold days must be between 0 and 365")
|
||||
ErrBatchNotCancellable = errors.New("distribution settlement batch cannot be cancelled")
|
||||
ErrSettlementBatchStale = errors.New("distribution settlement batch stale")
|
||||
)
|
||||
|
||||
var settlementReadyOrderStatuses = []string{"completed", "closed"}
|
||||
@@ -1019,6 +1021,8 @@ func (r *Repository) CreateSettlementBatch(ctx context.Context, adminID uint64,
|
||||
Joins("JOIN rental_orders ro ON ro.id = f.order_id").
|
||||
Where("f.settlement_status = 'pending'").
|
||||
Where("ro.status IN ?", settlementReadyOrderStatuses).
|
||||
Where("ro.refund_status NOT IN ?", autoReleaseBlockedRefundStatuses).
|
||||
Where("NOT EXISTS (SELECT 1 FROM disputes d WHERE d.order_id = ro.id AND d.status IN ?)", autoReleaseBlockedDisputeStatuses).
|
||||
Where("f.created_at >= ? AND f.created_at < ?", req.PeriodStart, req.PeriodEnd).
|
||||
Order("f.order_id ASC").
|
||||
Scan(&pendingOrderIDs).Error; err != nil {
|
||||
@@ -1033,7 +1037,7 @@ func (r *Repository) CreateSettlementBatch(ctx context.Context, adminID uint64,
|
||||
}
|
||||
}
|
||||
// 只收本站 site party。平台份不进批次,三方订单按 party 分别结算。
|
||||
// 仅纳入已完结订单,避免租赁中订单被提前打入人工批次。
|
||||
// 仅纳入已完结且未处于退款中/维权中的订单,与自动释放口径对齐。
|
||||
var parties []model.DistributionOrderParty
|
||||
if err := tx.Where("party_type = ? AND party_id = ?", PartyTypeSite, req.SiteID).
|
||||
Where("NOT EXISTS (SELECT 1 FROM distribution_settlement_items i WHERE i.party_id = distribution_order_parties.id)").
|
||||
@@ -1043,6 +1047,8 @@ func (r *Repository) CreateSettlementBatch(ctx context.Context, adminID uint64,
|
||||
Joins("JOIN rental_orders ro ON ro.id = f.order_id").
|
||||
Where("f.settlement_status = 'pending'").
|
||||
Where("ro.status IN ?", settlementReadyOrderStatuses).
|
||||
Where("ro.refund_status NOT IN ?", autoReleaseBlockedRefundStatuses).
|
||||
Where("NOT EXISTS (SELECT 1 FROM disputes d WHERE d.order_id = ro.id AND d.status IN ?)", autoReleaseBlockedDisputeStatuses).
|
||||
Where("f.created_at >= ? AND f.created_at < ?", req.PeriodStart, req.PeriodEnd),
|
||||
).
|
||||
Order("id ASC").
|
||||
@@ -1099,12 +1105,74 @@ func (r *Repository) MarkSettlementBatch(ctx context.Context, adminID, id uint64
|
||||
if total != row.TotalAmountCent {
|
||||
return ErrSettlementTotalMismatch
|
||||
}
|
||||
|
||||
// 复核批次内每个参与方与订单的最新实时状态,确保未发生退款变更、无进行中退款/维权:
|
||||
orderIDs := uniqueSettlementOrderIDs(items)
|
||||
var lockedFinancials []model.DistributionOrderFinancial
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("order_id IN ?", orderIDs).
|
||||
Order("id ASC").Find(&lockedFinancials).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var orders []model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id IN ?", orderIDs).
|
||||
Order("id ASC").Find(&orders).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(orders) != len(orderIDs) {
|
||||
return ErrSettlementBatchStale
|
||||
}
|
||||
orderMap := make(map[uint64]model.RentalOrder, len(orders))
|
||||
for _, o := range orders {
|
||||
orderMap[o.ID] = o
|
||||
}
|
||||
|
||||
var disputedCount int64
|
||||
if err := tx.Table("disputes").
|
||||
Where("order_id IN ? AND status IN ?", orderIDs, autoReleaseBlockedDisputeStatuses).
|
||||
Count(&disputedCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if disputedCount > 0 {
|
||||
return ErrSettlementBatchStale
|
||||
}
|
||||
|
||||
partyIDs := make([]uint64, 0, len(items))
|
||||
for _, it := range items {
|
||||
partyIDs = append(partyIDs, it.PartyID)
|
||||
}
|
||||
var parties []model.DistributionOrderParty
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id IN ?", partyIDs).
|
||||
Order("id ASC").Find(&parties).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(parties) != len(partyIDs) {
|
||||
return ErrSettlementBatchStale
|
||||
}
|
||||
partyMap := make(map[uint64]model.DistributionOrderParty, len(parties))
|
||||
for _, p := range parties {
|
||||
partyMap[p.ID] = p
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
order, ok := orderMap[item.OrderID]
|
||||
if !ok || !orderStatusAllowsRelease(order.Status) || slices.Contains(autoReleaseBlockedRefundStatuses, order.RefundStatus) {
|
||||
return ErrSettlementBatchStale
|
||||
}
|
||||
party, ok := partyMap[item.PartyID]
|
||||
if !ok || party.IncomeAmountCent != item.AmountCent {
|
||||
return ErrSettlementBatchStale
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err := tx.Model(&model.DistributionSettlementItem{}).Where("batch_id = ?", row.ID).
|
||||
Update("status", "settled").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
orderIDs := uniqueSettlementOrderIDs(items)
|
||||
if err := markFinancialsSettledIfSitePartiesDone(tx, orderIDs, now); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
)
|
||||
|
||||
// TestMarkSettlementBatchRejectsWhenOrderRefunded 断言人工批次创建后若发生退款调减,
|
||||
// MarkSettlementBatch 必须拦截并返回 ErrSettlementBatchStale,不得将过时金额入账;
|
||||
// 管理员作废批次后,新批次能按最新退款后金额成功生成与结算。
|
||||
func TestMarkSettlementBatchRejectsWhenOrderRefunded(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
now := time.Now()
|
||||
|
||||
site := mustCreateDistSite(t, db, "stale_site", "退款防穿透站", "stale.dist.com")
|
||||
rule := mustCreateActiveRule(t, db, site.ID, ModeTwoPartyDistributorReceive, now)
|
||||
order, _, party := addSettlementOrder(t, db, site.ID, rule.ID, "ORD-STALE-1", 1400)
|
||||
|
||||
// 管理员创建草稿人工批次:批次总额 1400,明细 1400 pending。
|
||||
batch, err := repo.CreateSettlementBatch(t.Context(), 0, CreateSettlementBatchRequest{
|
||||
SiteID: site.ID, PeriodStart: now.Add(-time.Hour), PeriodEnd: now.Add(time.Hour),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if batch.TotalAmountCent != 1400 {
|
||||
t.Fatalf("batch total = %d, want 1400", batch.TotalAmountCent)
|
||||
}
|
||||
|
||||
// 窗口期内订单发生退款,参与方收入被调减(例如从 1400 降为 400)。
|
||||
if err := db.Model(&party).Update("income_amount_cent", 400).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 此时管理员点击确认结算:必须被拦截阻断,不得超额结算 1400。
|
||||
_, err = repo.MarkSettlementBatch(t.Context(), 0, batch.ID)
|
||||
if err != ErrSettlementBatchStale {
|
||||
t.Fatalf("MarkSettlementBatch err = %v, want ErrSettlementBatchStale", err)
|
||||
}
|
||||
|
||||
// 断言站点钱包未入账任何资金。
|
||||
var wallet model.WalletAccount
|
||||
if err := db.Where("user_id = 0 AND distribution_site_id = ?", site.ID).First(&wallet).Error; err == nil {
|
||||
if wallet.AvailableBalanceCent != 0 {
|
||||
t.Fatalf("被拦截的批次不得向钱包入账,当前余额 = %d", wallet.AvailableBalanceCent)
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员作废陈旧批次。
|
||||
cancelled, err := repo.CancelSettlementBatch(t.Context(), 0, batch.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("CancelSettlementBatch failed: %v", err)
|
||||
}
|
||||
if cancelled.Status != "cancelled" {
|
||||
t.Fatalf("cancelled status = %s, want cancelled", cancelled.Status)
|
||||
}
|
||||
|
||||
// 重新生成批次:按最新参与方收入 400 生成。
|
||||
newBatch, err := repo.CreateSettlementBatch(t.Context(), 0, CreateSettlementBatchRequest{
|
||||
SiteID: site.ID, PeriodStart: now.Add(-time.Hour), PeriodEnd: now.Add(time.Hour),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if newBatch.TotalAmountCent != 400 {
|
||||
t.Fatalf("new batch total = %d, want 400(最新收入)", newBatch.TotalAmountCent)
|
||||
}
|
||||
|
||||
// 确认结算新批次:成功入账 400。
|
||||
settled, err := repo.MarkSettlementBatch(t.Context(), 0, newBatch.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("MarkSettlementBatch on new batch failed: %v", err)
|
||||
}
|
||||
if settled.Status != "settled" {
|
||||
t.Fatalf("settled status = %s, want settled", settled.Status)
|
||||
}
|
||||
|
||||
if err := db.Where("user_id = 0 AND distribution_site_id = ?", site.ID).First(&wallet).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if wallet.AvailableBalanceCent != 400 {
|
||||
t.Fatalf("最终钱包余额 = %d, want 400", wallet.AvailableBalanceCent)
|
||||
}
|
||||
_ = order
|
||||
}
|
||||
|
||||
// TestMarkSettlementBatchRejectsWhenOrderDisputedOrRefunding 断言批次内订单处于维权中或退款中时,
|
||||
// MarkSettlementBatch 同样拒绝入账。
|
||||
func TestMarkSettlementBatchRejectsWhenOrderDisputedOrRefunding(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
now := time.Now()
|
||||
|
||||
site := mustCreateDistSite(t, db, "block_site", "状态拦截站", "block.dist.com")
|
||||
rule := mustCreateActiveRule(t, db, site.ID, ModeTwoPartyDistributorReceive, now)
|
||||
order, _, _ := addSettlementOrder(t, db, site.ID, rule.ID, "ORD-BLOCK-1", 1400)
|
||||
|
||||
batch, err := repo.CreateSettlementBatch(t.Context(), 0, CreateSettlementBatchRequest{
|
||||
SiteID: site.ID, PeriodStart: now.Add(-time.Hour), PeriodEnd: now.Add(time.Hour),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 模拟订单发起纠纷维权中。
|
||||
dispute := model.Dispute{OrderID: order.ID, InitiatorID: 4, TargetUserID: 3, Type: "account_error", Status: "open"}
|
||||
if err := db.Create(&dispute).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := repo.MarkSettlementBatch(t.Context(), 0, batch.ID); err != ErrSettlementBatchStale {
|
||||
t.Fatalf("维权中批次结算 err = %v, want ErrSettlementBatchStale", err)
|
||||
}
|
||||
|
||||
// 结案纠纷,但订单进入 refunding 状态。
|
||||
if err := db.Model(&dispute).Update("status", "closed").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Model(&order).Update("refund_status", "refunding").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := repo.MarkSettlementBatch(t.Context(), 0, batch.ID); err != ErrSettlementBatchStale {
|
||||
t.Fatalf("退款中批次结算 err = %v, want ErrSettlementBatchStale", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateSettlementBatchExcludesRefundingAndDisputedOrders 断言创建人工批次时,
|
||||
// 退款中或维权中的订单不会被打包进入批次。
|
||||
func TestCreateSettlementBatchExcludesRefundingAndDisputedOrders(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
now := time.Now()
|
||||
|
||||
site := mustCreateDistSite(t, db, "excl_site", "批次排除站", "excl.dist.com")
|
||||
rule := mustCreateActiveRule(t, db, site.ID, ModeTwoPartyDistributorReceive, now)
|
||||
|
||||
// 订单 1:正常可结算。
|
||||
_, _, _ = addSettlementOrder(t, db, site.ID, rule.ID, "ORD-EXCL-NORMAL", 1000)
|
||||
|
||||
// 订单 2:退款中。
|
||||
orderRefunding, _, _ := addSettlementOrder(t, db, site.ID, rule.ID, "ORD-EXCL-REFUNDING", 2000)
|
||||
if err := db.Model(&orderRefunding).Update("refund_status", "refunding").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 订单 3:维权中。
|
||||
orderDisputed, _, _ := addSettlementOrder(t, db, site.ID, rule.ID, "ORD-EXCL-DISPUTED", 3000)
|
||||
if err := db.Create(&model.Dispute{OrderID: orderDisputed.ID, InitiatorID: 4, TargetUserID: 3, Type: "cheat", Status: "open"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
batch, err := repo.CreateSettlementBatch(t.Context(), 0, CreateSettlementBatchRequest{
|
||||
SiteID: site.ID, PeriodStart: now.Add(-time.Hour), PeriodEnd: now.Add(time.Hour),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 只有正常订单 1000 被纳入,退款中 2000 和维权中 3000 被排除。
|
||||
if batch.TotalAmountCent != 1000 {
|
||||
t.Fatalf("批次总额 = %d, want 1000(排除退款与纠纷订单)", batch.TotalAmountCent)
|
||||
}
|
||||
}
|
||||
@@ -415,6 +415,15 @@ func (r *Repository) markRefundFailedOrderStatus(ctx context.Context, orderID ui
|
||||
return r.markOrderRefundFailed(ctx, orderID, refundAmountCent)
|
||||
}
|
||||
|
||||
func refundedTotalTx(tx *gorm.DB, orderID uint64) (int64, error) {
|
||||
var refundedTotal int64
|
||||
err := tx.Model(&model.PaymentOrder{}).
|
||||
Where("order_id = ? AND status = ?", orderID, "refunded").
|
||||
Where("biz_type IN ?", refundBizTypes).
|
||||
Select("COALESCE(SUM(amount_cent), 0)").Scan(&refundedTotal).Error
|
||||
return refundedTotal, err
|
||||
}
|
||||
|
||||
func (r *Repository) updateOrderRefundStatus(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
@@ -424,11 +433,8 @@ func (r *Repository) updateOrderRefundStatus(ctx context.Context, orderID uint64
|
||||
// 同一订单可能多笔退款,且退款结果查询会重复回调:
|
||||
// 以已退款成功流水合计为口径,并取本次金额与累计额的较大值,
|
||||
// 保证 order.RefundAmountCent 单调不回退、重复回调幂等。
|
||||
var refundedTotal int64
|
||||
if err := tx.Model(&model.PaymentOrder{}).
|
||||
Where("order_id = ? AND status = ?", orderID, "refunded").
|
||||
Where("biz_type IN ?", refundBizTypes).
|
||||
Select("COALESCE(SUM(amount_cent), 0)").Scan(&refundedTotal).Error; err != nil {
|
||||
refundedTotal, err := refundedTotalTx(tx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cumulative := max(refundAmountCent, refundedTotal)
|
||||
@@ -448,18 +454,31 @@ func (r *Repository) updateOrderRefundStatus(ctx context.Context, orderID uint64
|
||||
})
|
||||
}
|
||||
func (r *Repository) markOrderRefunding(ctx context.Context, orderID uint64, refundAmountCent int64) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
|
||||
"refund_status": "refunding",
|
||||
"refund_amount_cent": refundAmountCent,
|
||||
"refunded_at": nil,
|
||||
}).Error
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
refundedTotal, err := refundedTotalTx(tx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cumulative := max(refundAmountCent, refundedTotal)
|
||||
return tx.Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
|
||||
"refund_status": "refunding",
|
||||
"refund_amount_cent": cumulative,
|
||||
"refunded_at": nil,
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
func (r *Repository) markOrderRefundFailed(ctx context.Context, orderID uint64, refundAmountCent int64) error {
|
||||
return r.db.WithContext(ctx).Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
|
||||
"refund_status": "failed",
|
||||
"refund_amount_cent": refundAmountCent,
|
||||
"refunded_at": nil,
|
||||
}).Error
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
refundedTotal, err := refundedTotalTx(tx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{
|
||||
"refund_status": "failed",
|
||||
"refund_amount_cent": refundedTotal,
|
||||
"refunded_at": nil,
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
func (r *Repository) updateRefundRawRequest(ctx context.Context, paymentID uint64, rawRequest map[string]string) error {
|
||||
if rawRequest == nil {
|
||||
|
||||
@@ -94,3 +94,50 @@ func TestUpdateOrderRefundStatusUsesCumulativeRefundedAmount(t *testing.T) {
|
||||
t.Fatalf("重复回调后 order.RefundAmountCent = %d, want 1000(单调不回退)", saved.RefundAmountCent)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMarkOrderRefundingAndFailedPreserveCumulativeRefund 断言已成功退款的订单
|
||||
// 发生后续退款中或退款失败时,累计退款额不会被单笔处理金额冲减缩小。
|
||||
func TestMarkOrderRefundingAndFailedPreserveCumulativeRefund(t *testing.T) {
|
||||
db := setupPaymentTestDB(t)
|
||||
repo := NewRepository(db, nil, nil)
|
||||
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-REF-PRESERVE", ListingID: 1, AccountID: 1, OwnerID: 1, RenterID: 2,
|
||||
Status: "completed", RentAmountCent: 10000, OwnerRentAmountCent: 6000,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 先前已成功退款 5000。
|
||||
prevRefund := model.PaymentOrder{
|
||||
PaymentNo: "REF-PREV-1", OrderID: order.ID, OrderNo: order.OrderNo, UserID: order.RenterID,
|
||||
Provider: "mock", ThirdOrderID: "REF-PREV-1", AmountCent: 5000, BizType: "admin_refund", Status: "refunded",
|
||||
}
|
||||
if err := db.Create(&prevRefund).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 此时发起了另一笔 2000 的退款中:累计额应为 max(2000, 5000) = 5000。
|
||||
if err := repo.markOrderRefunding(t.Context(), order.ID, 2000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.RefundStatus != "refunding" || saved.RefundAmountCent != 5000 {
|
||||
t.Fatalf("markOrderRefunding 后状态 = %s/%d, want refunding/5000", saved.RefundStatus, saved.RefundAmountCent)
|
||||
}
|
||||
|
||||
// 该笔 2000 退款最终失败:累计退款额还原为已成功流水合计 5000,而不是单笔 2000。
|
||||
if err := repo.markOrderRefundFailed(t.Context(), order.ID, 2000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.RefundStatus != "failed" || saved.RefundAmountCent != 5000 {
|
||||
t.Fatalf("markOrderRefundFailed 后状态 = %s/%d, want failed/5000", saved.RefundStatus, saved.RefundAmountCent)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user