package reconcile import ( "context" "log" "time" "github.com/wangjia/pay/internal/gateway" "github.com/wangjia/pay/internal/provider" "github.com/wangjia/pay/internal/store" ) // SyncPendingTask 调度 P2 gateway.SyncPendingAttempts:逐 pending attempt 查单收敛(防掉单)。 // 其内部已对 not_found/amount_mismatch/failed 打日志(settle-sync,764ed55),此处不重复。 func SyncPendingTask(gw *gateway.Gateway, limit int) func(ctx context.Context) error { return func(ctx context.Context) error { n, err := gw.SyncPendingAttempts(ctx, limit) if err != nil { return err } if n > 0 { log.Printf("[reconcile] 查单对账收敛 %d 笔待支付 → paid", n) } return nil } } // PaidSpotCheckTask 已付订单抽查:对近 window 内已付 attempt 反查渠道,金额/币种漂移即告警 // (如渠道侧已退款/拒付而本地仍 paid)。只发现不改状态——状态机翻转属 P4。 // crypto 之类 query-only 渠道:paid 后再查若命中同额即一致;查不到(链上历史滚出窗口)不报错跳过。 func PaidSpotCheckTask(orders *store.OrderStore, providers *provider.Registry, window time.Duration, now func() time.Time) func(ctx context.Context) error { return func(ctx context.Context) error { atts, err := orders.ListRecentlyPaidAttempts(now().Add(-window), 100) if err != nil { return err } for i := range atts { a := &atts[i] prov, err := providers.Get(a.Channel) if err != nil { continue } created := a.CreatedAt ev, err := prov.Query(ctx, provider.QueryRequest{ ProviderRef: a.ProviderRef, OutTradeNo: a.OutTradeNo, AccountID: a.AccountID, AmountMinor: a.AmountMinor, Currency: a.Currency, CreatedAt: created, ExpiresAt: a.ExpiresAt, }) if err != nil || ev == nil { continue // 查不到/瞬时错:抽查尽力而为,不阻断 } // 本地 paid,渠道却报非成功,或金额/币种对不上 → 对账差异,必须可见。 if ev.Status != provider.PaidSucceeded || ev.PaidCurrency != a.Currency || ev.PaidAmountMinor < a.AmountMinor { log.Printf("[reconcile][对账差异] attempt=%s channel=%s 本地 paid 但渠道 status=%s amount=%d/%s(本地 %d/%s)", a.ProviderRef, a.Channel, ev.Status, ev.PaidAmountMinor, ev.PaidCurrency, a.AmountMinor, a.Currency) } } return nil } }