fix(v2): 退款自愈扫描加回溯窗(防 LIMIT 尾部饥饿)+ 清死行

RefundApplyTask 的候选查询按 out_trade_no ASC/id ASC 取前 200,succeeded 退款历史
只增不减、partially_refunded 长期驻留,超过 limit 后最新的崩溃窗口永远排在候选外
扫不到。两路查询都加 since 回溯窗:RefundStore.ListDistinctOutTradeNosByStatusSince
(completed_at>=since)、OrderStore.ListOrdersByStatusSince(updated_at>=since),旧的
无窗方法原样保留。RefundApplyTask 新增 lookback/now 参数,config.ReconcileConfig 新增
RefundApplyLookbackMin(默认 48h)。顺带按 T6 复审补 OrderTTLMin 字段注释(须 ≫ crypto
orderTTL 15min),清 sync_test.go 死行 `_ = time.Second`。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
wangjia
2026-07-10 17:59:57 +08:00
parent 727ed74899
commit 9117de7dcf
9 changed files with 212 additions and 14 deletions
+23
View File
@@ -126,6 +126,29 @@ func TestListDistinctOutTradeNosByStatus(t *testing.T) {
}
}
// TestListDistinctOutTradeNosByStatusSinceExcludesStale 钉住回溯窗:completed_at
// 早于 since 的 succeeded 退款不应入候选(防 LIMIT 尾部饥饿——见 RefundApplyTask)。
func TestListDistinctOutTradeNosByStatusSinceExcludesStale(t *testing.T) {
db := model.OpenTestDB(t)
rs := NewRefundStore(db)
now := time.Now()
stale := now.Add(-72 * time.Hour)
fresh := now.Add(-time.Hour)
_ = rs.CreateRefund(&model.Refund{RefundID: "s-stale", OutTradeNo: "PAY-STALE", AmountMinor: 100, Currency: "CNY",
Status: model.RefundSucceeded, CompletedAt: &stale})
_ = rs.CreateRefund(&model.Refund{RefundID: "s-fresh", OutTradeNo: "PAY-FRESH", AmountMinor: 100, Currency: "CNY",
Status: model.RefundSucceeded, CompletedAt: &fresh})
got, err := rs.ListDistinctOutTradeNosByStatusSince(model.RefundSucceeded, now.Add(-48*time.Hour), 50)
if err != nil {
t.Fatalf("list: %v", err)
}
if len(got) != 1 || got[0] != "PAY-FRESH" {
t.Fatalf("只应命中窗内的 PAY-FRESH, got %+v", got)
}
}
// TestListStuckRefunds 供「卡滞 processing/manual_pending 退款告警」:只挑 updated_at
// 早于阈值的 processing/manual_pending 行,requested/succeeded/failed 不命中。
func TestListStuckRefunds(t *testing.T) {