package handler import ( "context" "log" "time" "github.com/sundynix/sundynix-gateway/internal/payment" ) // 掉单补偿(P5.3,设计见 PAYMENT_DESIGN.md §5): // 前端轮询只在「用户开着账单页」时才查单确认——用户扫完码就关页面的话,钱付了、 // 订单却永远挂 pending、积分永远不到账。这个后台定时器把「用户在不在场」从入账链路 // 里摘掉:周期扫 pending 微信单,逐单 reconcileOrder(与前端轮询同一份幂等落态逻辑)。 const reconcileInterval = 1 * time.Minute // StartReconcile 启动掉单补偿定时器(微信渠道未配置时空转,几乎零成本)。随进程生命周期运行, // ctx 取消即退出。返回给调用方保存以便优雅停机时取消。 func (h *Handler) StartReconcile(ctx context.Context) { go func() { t := time.NewTicker(reconcileInterval) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: h.reconcilePending(ctx) } } }() log.Printf("[payment] 掉单补偿定时器已启动(每 %s 扫一次 pending 微信单)", reconcileInterval) } // reconcilePending 扫一轮待补偿的 pending 微信单。渠道未配置时直接返回(不打扰)。 func (h *Handler) reconcilePending(ctx context.Context) { if h.pay.Get(payment.ChannelWechat) == nil { return } orders, err := h.db.PendingWechatOrders(ctx, 200) if err != nil { log.Printf("[payment] 补偿扫描取 pending 单失败: %v", err) return } var paid, expired, mismatch int for i := range orders { o := &orders[i] updated, mm := h.reconcileOrder(ctx, o) switch { case mm: mismatch++ log.Printf("[payment] ⚠️ 订单 %s 支付金额与订单不符,已挂起待人工对账", o.ID) case updated.Status == "paid": paid++ case updated.Status == "expired": expired++ } } // 只在有变化时记一行,避免空转刷屏。 if paid+expired+mismatch > 0 { log.Printf("[payment] 补偿扫描:入账 %d、过期 %d、金额不符 %d(本轮 %d 单)", paid, expired, mismatch, len(orders)) } }