fix(distribution): 结算幂等与退款口径加固,补关单释放与回归测试
- 结算明细加唯一键 (order_id, party_id):自动释放与手工批次并发时以唯一键冲突为准跳过该参与方,不再重复入账 - 自动释放先无锁筛出本站未入账参与方,收敛加锁范围;手工批次建批次后先锁周期内财务快照,与自动释放串行化 - 累计退款额改为「本次回调金额与已退款成功流水合计取较大值」,多笔退款与退款结果重复回调不再把口径写小 - 关单三路径(AdminClose / AdminRejectRefund 的 close 分支 / AdminSeal)补写 release_at,接线原死代码 EnsureCompletedReleaseAtTx - 仲裁分销重算改用实际退款额(扣除押金暂扣),避免站点被多扣 - 订单列表与详情的提价分成取 min(收入, 货架加价),退款压缩加价后不再显示负数 - 新增迁移 000070(结算明细唯一键)、000071(补回填 closed 订单释放时间) - 新增 5 个测试文件:结算幂等与展示钳制、结算 job 幂等、累计退款、关单补写释放时间、仲裁押金暂扣口径
This commit is contained in:
@@ -421,11 +421,22 @@ func (r *Repository) updateOrderRefundStatus(ctx context.Context, orderID uint64
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 同一订单可能多笔退款,且退款结果查询会重复回调:
|
||||
// 以已退款成功流水合计为口径,并取本次金额与累计额的较大值,
|
||||
// 保证 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 {
|
||||
return err
|
||||
}
|
||||
cumulative := max(refundAmountCent, refundedTotal)
|
||||
now := time.Now()
|
||||
order.RefundStatus = "refunded"
|
||||
order.RefundAmountCent = refundAmountCent
|
||||
order.RefundAmountCent = cumulative
|
||||
order.RefundedAt = &now
|
||||
if bizType == "admin_refund" && order.Status == "completed" && refundAmountCent >= order.RentAmountCent+order.DepositAmountCent {
|
||||
if bizType == "admin_refund" && order.Status == "completed" && cumulative >= order.RentAmountCent+order.DepositAmountCent {
|
||||
if err := rentergrowth.RevokeOrderCompleted(tx, &order); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -433,7 +444,7 @@ func (r *Repository) updateOrderRefundStatus(ctx context.Context, orderID uint64
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return distribution.ApplyRefundTx(tx, orderID, refundAmountCent)
|
||||
return distribution.ApplyRefundTx(tx, orderID, cumulative)
|
||||
})
|
||||
}
|
||||
func (r *Repository) markOrderRefunding(ctx context.Context, orderID uint64, refundAmountCent int64) error {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/distribution"
|
||||
)
|
||||
|
||||
// TestUpdateOrderRefundStatusUsesCumulativeRefundedAmount 断言同一订单多笔退款时,
|
||||
// 单笔回调传入的金额不会把订单累计退款额写小,分销财务快照按合计口径重算。
|
||||
func TestUpdateOrderRefundStatusUsesCumulativeRefundedAmount(t *testing.T) {
|
||||
db := setupPaymentTestDB(t)
|
||||
repo := NewRepository(db, nil, nil)
|
||||
now := time.Now()
|
||||
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "cum_site", SiteName: "累计退款站", Domain: "cum.dist.com",
|
||||
Status: "active", DisplayAccountEnabled: true,
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rule := model.DistributionRuleVersion{
|
||||
SiteID: &site.ID, Mode: distribution.ModeTwoPartyPlatformReceive, Version: 1, Status: "active",
|
||||
PlatformReceiveRatio: 35, DistributorSellRatio: 40, ServiceFeeValue: 0,
|
||||
ServiceFeeUnit: distribution.ServiceFeeUnitFixedAmount, PaymentFeeBPS: 0,
|
||||
EffectiveFrom: now.Add(-time.Hour),
|
||||
}
|
||||
if err := db.Create(&rule).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-CUM-REFUND", ListingID: 1, AccountID: 1, OwnerID: 1, RenterID: 2,
|
||||
Status: "completed", RentAmountCent: 10000, OwnerRentAmountCent: 6000,
|
||||
DistributionSiteID: &site.ID, DistributionRuleVersionID: &rule.ID,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := distribution.SnapshotPaidOrderTx(db, order.ID, 10000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 两笔已退款成功流水:300 + 700 = 1000。
|
||||
refunds := []model.PaymentOrder{
|
||||
{PaymentNo: "REF-CUM-1", OrderID: order.ID, OrderNo: order.OrderNo, UserID: order.RenterID, Provider: "mock", ThirdOrderID: "REF-CUM-1", AmountCent: 300, BizType: "admin_refund", Status: "refunded"},
|
||||
{PaymentNo: "REF-CUM-2", OrderID: order.ID, OrderNo: order.OrderNo, UserID: order.RenterID, Provider: "mock", ThirdOrderID: "REF-CUM-2", AmountCent: 700, BizType: "rent_refund", Status: "refunded"},
|
||||
}
|
||||
if err := db.Create(&refunds).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 只传其中一笔金额调用:累计口径应取两笔合计 1000。
|
||||
if err := repo.updateOrderRefundStatus(t.Context(), order.ID, 300, "admin_refund"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.RefundAmountCent != 1000 {
|
||||
t.Fatalf("order.RefundAmountCent = %d, want 1000(两笔退款合计)", saved.RefundAmountCent)
|
||||
}
|
||||
if saved.RefundStatus != "refunded" {
|
||||
t.Fatalf("order.RefundStatus = %s, want refunded", saved.RefundStatus)
|
||||
}
|
||||
|
||||
var financial model.DistributionOrderFinancial
|
||||
if err := db.Where("order_id = ?", order.ID).First(&financial).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if financial.RefundAmountCent != 1000 || financial.NetPaidAmountCent != 9000 || financial.SplitBaseAmountCent != 3000 {
|
||||
t.Fatalf("分销快照未按累计退款重算:refund=%d netPaid=%d splitBase=%d",
|
||||
financial.RefundAmountCent, financial.NetPaidAmountCent, financial.SplitBaseAmountCent)
|
||||
}
|
||||
var party model.DistributionOrderParty
|
||||
if err := db.Where("order_id = ? AND party_role = ?", order.ID, "distributor_sell").First(&party).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if party.IncomeAmountCent != 1200 {
|
||||
t.Fatalf("分站份额 = %d, want 1200(利润 3000 的 40%%)", party.IncomeAmountCent)
|
||||
}
|
||||
|
||||
// 退款结果查询重复回调不应把累计额写小。
|
||||
if err := repo.updateOrderRefundStatus(t.Context(), order.ID, 300, "admin_refund"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.RefundAmountCent != 1000 {
|
||||
t.Fatalf("重复回调后 order.RefundAmountCent = %d, want 1000(单调不回退)", saved.RefundAmountCent)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user