仲裁退款失败补偿: refundretry 自动补建仲裁孤儿退款单
问题: dispute/arbitration.go 事务提交后 startRefundBestEffort 失败只 log.Printf 吞错, 号主收入已入账但租客退款单未建, 订单卡在 refund_status=pending 的孤儿状态。原 refundretry 的 warnMissingRefundOrders 只告警不补建, 导致资金口径不平需人工兜底。 修复: 升级 warnMissingRefundOrders, 对 settlement_status="arbitrated" 的孤儿订单自动调 StartRefund 补建 (biz_type 固定 arbitration_refund, 金额用 order.RefundAmountCent)。 幂等保证: 复用 StartRefund 内部 prepareRefundOrder 的 (order_id, biz_type) 查重作为天然幂等闸, 重复触发走 existing 分支只同步状态不重复建单。渠道失败 → markRefundFailed 留记录 + 订单 refund_status=failed → 下一轮不再视为孤儿。 范围限定: 仅仲裁孤儿自动补建 (biz_type 无歧义)。非仲裁孤儿 (取消/客服关闭/结账) biz_type 无法从订单状态可靠区分, 维持只告警。 新增测试: - TestAutoRebuildArbitrationOrphan: 仲裁孤儿补建成功 - TestNoRebuildForNonArbitrationOrphan: 非仲裁孤儿不补建 - TestRebuildArbitrationOrphanIdempotent: 连续扫描幂等 防御: payments==nil 时跳过补建并告警, 避免单测/未来构造 Job 时 panic。 go build / vet / test 通过。
This commit is contained in:
@@ -119,12 +119,16 @@ func (j *Job) run(ctx context.Context) {
|
||||
if err != nil {
|
||||
j.logger.Warn("refund retry job sync payments failed", zap.Error(err))
|
||||
}
|
||||
missing, err := j.warnMissingRefundOrders(ctx, now)
|
||||
missing, rebuilt, err := j.warnMissingRefundOrders(ctx, now)
|
||||
if err != nil {
|
||||
j.logger.Warn("refund retry job scan missing refund orders failed", zap.Error(err))
|
||||
}
|
||||
if processed > 0 || missing > 0 {
|
||||
j.logger.Info("refund retry job finished", zap.Int("processed", processed), zap.Int("missing_refund_orders", missing))
|
||||
j.logger.Info("refund retry job finished",
|
||||
zap.Int("processed", processed),
|
||||
zap.Int("missing_refund_orders", missing),
|
||||
zap.Int("rebuilt_arbitration_refunds", rebuilt),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +236,7 @@ func (j *Job) warnMaxRetryRefunds(ctx context.Context, now time.Time) {
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Job) warnMissingRefundOrders(ctx context.Context, now time.Time) (int, error) {
|
||||
func (j *Job) warnMissingRefundOrders(ctx context.Context, now time.Time) (int, int, error) {
|
||||
var rows []model.RentalOrder
|
||||
err := j.db.WithContext(ctx).
|
||||
Where("refund_status IN ? AND refund_amount_cent > 0 AND updated_at <= ?", []string{"pending", "refunding"}, now.Add(-10*time.Minute)).
|
||||
@@ -240,26 +244,54 @@ func (j *Job) warnMissingRefundOrders(ctx context.Context, now time.Time) (int,
|
||||
Limit(100).
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, 0, err
|
||||
}
|
||||
missing := 0
|
||||
rebuilt := 0
|
||||
for _, row := range rows {
|
||||
var count int64
|
||||
if err := j.db.WithContext(ctx).Model(&model.PaymentOrder{}).
|
||||
Where("order_id = ? AND biz_type IN ?", row.ID, payment.RefundBizTypes()).
|
||||
Count(&count).Error; err != nil {
|
||||
return missing, err
|
||||
return missing, rebuilt, err
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
missing++
|
||||
// 仲裁孤儿:settlement_status="arbitrated" 是仲裁事务无条件写入的标志
|
||||
// (arbitration.go:61),biz_type 可确定还原为 arbitration_refund,自动补建。
|
||||
// 非仲裁孤儿 biz_type 无法从订单状态可靠区分,维持只告警。
|
||||
if row.SettlementStatus == "arbitrated" {
|
||||
if j.payments == nil {
|
||||
j.logger.Warn("auto rebuild arbitration refund skipped, payment repository unavailable",
|
||||
zap.Uint64("order_id", row.ID),
|
||||
zap.Int64("refund_amount_cent", row.RefundAmountCent),
|
||||
)
|
||||
continue
|
||||
}
|
||||
if _, err := j.payments.StartRefund(ctx, row.ID, row.RefundAmountCent, "arbitration_refund", "仲裁退款补偿补建"); err != nil {
|
||||
j.logger.Warn("auto rebuild arbitration refund failed, need manual check",
|
||||
zap.Uint64("order_id", row.ID),
|
||||
zap.Int64("refund_amount_cent", row.RefundAmountCent),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
rebuilt++
|
||||
j.logger.Info("auto rebuild arbitration refund submitted",
|
||||
zap.Uint64("order_id", row.ID),
|
||||
zap.Int64("refund_amount_cent", row.RefundAmountCent),
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
j.logger.Warn("refund order missing payment record, need manual check",
|
||||
zap.Uint64("order_id", row.ID),
|
||||
zap.String("order_no", row.OrderNo),
|
||||
zap.String("refund_status", row.RefundStatus),
|
||||
zap.String("settlement_status", row.SettlementStatus),
|
||||
zap.Int64("refund_amount_cent", row.RefundAmountCent),
|
||||
)
|
||||
}
|
||||
return missing, nil
|
||||
return missing, rebuilt, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user