fix(distribution): 结算幂等与退款口径加固,补关单释放与回归测试

- 结算明细加唯一键 (order_id, party_id):自动释放与手工批次并发时以唯一键冲突为准跳过该参与方,不再重复入账
- 自动释放先无锁筛出本站未入账参与方,收敛加锁范围;手工批次建批次后先锁周期内财务快照,与自动释放串行化
- 累计退款额改为「本次回调金额与已退款成功流水合计取较大值」,多笔退款与退款结果重复回调不再把口径写小
- 关单三路径(AdminClose / AdminRejectRefund 的 close 分支 / AdminSeal)补写 release_at,接线原死代码 EnsureCompletedReleaseAtTx
- 仲裁分销重算改用实际退款额(扣除押金暂扣),避免站点被多扣
- 订单列表与详情的提价分成取 min(收入, 货架加价),退款压缩加价后不再显示负数
- 新增迁移 000070(结算明细唯一键)、000071(补回填 closed 订单释放时间)
- 新增 5 个测试文件:结算幂等与展示钳制、结算 job 幂等、累计退款、关单补写释放时间、仲裁押金暂扣口径
This commit is contained in:
yml2213
2026-09-10 19:13:39 +08:00
parent e3a75fb2d7
commit 36ebe7a5f1
13 changed files with 1017 additions and 10 deletions
@@ -593,8 +593,14 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList
totalEarning += p.IncomeAmountCent
if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" {
roles = append(roles, "sell")
markupCent += items[i].ShelfMarkupCent
splitCent += (p.IncomeAmountCent - items[i].ShelfMarkupCent)
// 退款压缩加价后收入可能小于货架加价:加价部分取两者较小值,
// 避免展示负的提价分成。
markup := items[i].ShelfMarkupCent
if p.IncomeAmountCent < markup {
markup = p.IncomeAmountCent
}
markupCent += markup
splitCent += p.IncomeAmountCent - markup
} else if p.PartyRole == "distributor_receive" || p.PartyRole == "distributor_a" {
roles = append(roles, "receive")
splitCent += p.IncomeAmountCent
@@ -670,8 +676,14 @@ func (r *Repository) FindOrder(ctx context.Context, adminID, id uint64) (*Distri
totalEarning += p.IncomeAmountCent
if p.PartyRole == "distributor_sell" || p.PartyRole == "distributor_b" {
roles = append(roles, "sell")
markupCent += item.ShelfMarkupCent
splitCent += (p.IncomeAmountCent - item.ShelfMarkupCent)
// 退款压缩加价后收入可能小于货架加价:加价部分取两者较小值,
// 避免展示负的提价分成。
markup := item.ShelfMarkupCent
if p.IncomeAmountCent < markup {
markup = p.IncomeAmountCent
}
markupCent += markup
splitCent += p.IncomeAmountCent - markup
} else if p.PartyRole == "distributor_receive" || p.PartyRole == "distributor_a" {
roles = append(roles, "receive")
splitCent += p.IncomeAmountCent
@@ -998,6 +1010,28 @@ func (r *Repository) CreateSettlementBatch(ctx context.Context, adminID uint64,
if err := tx.Create(&batch).Error; err != nil {
return err
}
// 与自动释放路径串行化:先按周期锁定待结算财务快照,再选参与方,
// 避免同一订单同时被手工批次与自动释放纳入明细、重复入账。
// 分两步(先查 ID 再 FOR UPDATE)而不是 JOIN + FOR UPDATE,避免顺带锁住 rental_orders 行。
var pendingOrderIDs []uint64
if err := tx.Table("distribution_order_financials f").
Select("f.order_id").
Joins("JOIN rental_orders ro ON ro.id = f.order_id").
Where("f.settlement_status = 'pending'").
Where("ro.status IN ?", settlementReadyOrderStatuses).
Where("f.created_at >= ? AND f.created_at < ?", req.PeriodStart, req.PeriodEnd).
Order("f.order_id ASC").
Scan(&pendingOrderIDs).Error; err != nil {
return err
}
if len(pendingOrderIDs) > 0 {
var lockedFinancials []model.DistributionOrderFinancial
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_id IN ?", pendingOrderIDs).
Order("id ASC").Find(&lockedFinancials).Error; err != nil {
return err
}
}
// 只收本站 site party。平台份不进批次,三方订单按 party 分别结算。
// 仅纳入已完结订单,避免租赁中订单被提前打入人工批次。
var parties []model.DistributionOrderParty