fix(distribution): 结算幂等与退款口径加固,补关单释放与回归测试
- 结算明细加唯一键 (order_id, party_id):自动释放与手工批次并发时以唯一键冲突为准跳过该参与方,不再重复入账 - 自动释放先无锁筛出本站未入账参与方,收敛加锁范围;手工批次建批次后先锁周期内财务快照,与自动释放串行化 - 累计退款额改为「本次回调金额与已退款成功流水合计取较大值」,多笔退款与退款结果重复回调不再把口径写小 - 关单三路径(AdminClose / AdminRejectRefund 的 close 分支 / AdminSeal)补写 release_at,接线原死代码 EnsureCompletedReleaseAtTx - 仲裁分销重算改用实际退款额(扣除押金暂扣),避免站点被多扣 - 订单列表与详情的提价分成取 min(收入, 货架加价),退款压缩加价后不再显示负数 - 新增迁移 000070(结算明细唯一键)、000071(补回填 closed 订单释放时间) - 新增 5 个测试文件:结算幂等与展示钳制、结算 job 幂等、累计退款、关单补写释放时间、仲裁押金暂扣口径
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user