feat(v2): crypto 孤儿到账发现——OrphanScanner 扫链核对 + orphan_payments 落表告警(对账兜底)
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package reconcile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/wangjia/pay/config"
|
||||
"github.com/wangjia/pay/internal/accounts"
|
||||
"github.com/wangjia/pay/internal/model"
|
||||
"github.com/wangjia/pay/internal/provider"
|
||||
"github.com/wangjia/pay/internal/provider/crypto"
|
||||
"github.com/wangjia/pay/internal/store"
|
||||
)
|
||||
|
||||
// OrphanScanTask 对每个 enabled crypto 账户扫链找孤儿到账(不匹配任何近期 attempt),
|
||||
// 落 OrphanStore + 首次记入时告警。渠道须实现 provider.OrphanScanner(crypto 实现)。
|
||||
func OrphanScanTask(providers *provider.Registry, accts *accounts.Registry, orders *store.OrderStore,
|
||||
orphans *store.OrphanStore, window time.Duration, now func() time.Time) func(ctx context.Context) error {
|
||||
return func(ctx context.Context) error {
|
||||
prov, err := providers.Get("crypto")
|
||||
if err != nil {
|
||||
return nil // 未启用 crypto:无事可做
|
||||
}
|
||||
scanner, ok := prov.(provider.OrphanScanner)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
since := now().Add(-window)
|
||||
for _, acc := range accts.EnabledFor("crypto", "") {
|
||||
atts, err := orders.ListAttemptsByChannelSince("crypto", since, 200)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
known := make([]provider.KnownAttempt, 0, len(atts))
|
||||
for i := range atts {
|
||||
if atts[i].AccountID != acc.AccountID {
|
||||
continue
|
||||
}
|
||||
known = append(known, provider.KnownAttempt{AmountMinor: atts[i].AmountMinor, ProviderRef: atts[i].ProviderRef})
|
||||
}
|
||||
found, err := scanner.ScanOrphans(ctx, provider.OrphanScanRequest{
|
||||
AccountID: acc.AccountID, Since: since, Known: known,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[reconcile] 孤儿扫描 account=%s: %v", acc.AccountID, err)
|
||||
continue // 单账户失败不阻断其它
|
||||
}
|
||||
for _, o := range found {
|
||||
isNew, rerr := orphans.Record(&model.OrphanPayment{
|
||||
Channel: "crypto", AccountID: acc.AccountID, TxID: o.TxID,
|
||||
AmountMinor: o.AmountMinor, Currency: o.Currency, DetectedAt: o.At,
|
||||
Note: "到账无主:不匹配任何近期 attempt 期望金额",
|
||||
})
|
||||
if rerr != nil {
|
||||
log.Printf("[reconcile] 记录孤儿失败 tx=%s: %v", o.TxID, rerr)
|
||||
continue
|
||||
}
|
||||
if isNew {
|
||||
log.Printf("[reconcile][孤儿告警] channel=crypto account=%s tx=%s amount=%d %s",
|
||||
acc.AccountID, o.TxID, o.AmountMinor, o.Currency)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// AddCryptoJobs 装配 crypto 相关后台任务:① 冷启动 Warm(注入 loader 后立即重建预留);
|
||||
// ② 周期孤儿扫描。若未启用 crypto 渠道则安全跳过。
|
||||
func AddCryptoJobs(runner *Runner, providers *provider.Registry, accts *accounts.Registry,
|
||||
orders *store.OrderStore, orphans *store.OrphanStore, cfg config.ReconcileConfig) {
|
||||
prov, err := providers.Get("crypto")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if cp, ok := prov.(*crypto.Provider); ok {
|
||||
cp.SetReservationLoader(CryptoReservationLoader(orders))
|
||||
if werr := cp.Warm(context.Background()); werr != nil { // 起服务前重建预留
|
||||
log.Printf("[reconcile] crypto 预留冷启动重建: %v", werr)
|
||||
}
|
||||
}
|
||||
runner.Add("crypto-orphan-scan", time.Duration(cfg.OrphanEverySec)*time.Second,
|
||||
OrphanScanTask(providers, accts, orders, orphans,
|
||||
time.Duration(cfg.OrphanWindowMin)*time.Minute, time.Now))
|
||||
}
|
||||
Reference in New Issue
Block a user