package handler import ( "context" "fmt" "log" "time" "github.com/sundynix/sundynix-gateway/internal/store" "github.com/sundynix/sundynix-gateway/internal/wechat" ) // 支付到账后给下单人推一条微信客服消息(回执)。 // // 为什么只做「支付回执」不做「周期刷新提醒」:客服消息只能在用户 48h 内与公众号 // 互动过时下发。支付回执时用户刚扫码付完款,稳在窗口内;而订阅的周期刷新由定时器 // 触发,那一刻用户多半早已超出 48h → 客服消息必然失败。隔天的提醒须用模板消息(暂缓)。 const wxNotifyTimeout = 10 * time.Second // notifyOrderPaid 尽力而为地给下单人推支付回执:异步、超时隔离,任何失败只记日志, // 绝不影响入账主流程(钱已收、账已记,通知发不发都不能回滚)。 // 仅在 MarkOrderPaid 返回 changed=true(首次到账)时调用,避免重复回调重复推送。 func (h *Handler) notifyOrderPaid(orderID string) { go func() { ctx, cancel := context.WithTimeout(context.Background(), wxNotifyTimeout) defer cancel() o, err := h.db.GetOrder(ctx, orderID) if err != nil || o == nil || o.UserID == "" { return } u, err := h.db.GetUserByID(ctx, o.UserID) if err != nil || u == nil || u.WechatOpenID == "" { return // 非微信用户(邮箱注册等)无从推送,静默跳过 } cfg := h.loadWechatMP(ctx) if !cfg.Enabled() { return } token, err := h.accessToken(ctx, cfg) if err != nil { log.Printf("[wxnotify] 取 access_token 失败 order=%s: %v", orderID, err) return } if err := wechat.SendCustomText(ctx, token, u.WechatOpenID, h.composePaidMessage(ctx, o)); err != nil { log.Printf("[wxnotify] 支付回执推送失败 order=%s: %v", orderID, err) } }() } // composePaidMessage 按订单类型拼回执文案,并带上当前积分余额。 func (h *Handler) composePaidMessage(ctx context.Context, o *store.PaymentOrder) string { bal := credits(h.db.TenantBalance(ctx, o.TenantID)) if o.Kind == store.OrderKindSub { name := "订阅" if pl := h.db.GetSubPlan(ctx, o.PlanID); pl != nil && pl.Name != "" { name = pl.Name } return fmt.Sprintf("✅ 订阅开通成功\n套餐:%s\n积分将在有效期内按周期自动发放。\n当前余额:%s 积分", name, bal) } return fmt.Sprintf("✅ 充值成功\n本次到账:%s 积分\n当前余额:%s 积分", credits(o.CreditsMicro), bal) } // credits 把 micro 积分格式化成人类可读数(去掉多余小数)。 func credits(micro int64) string { const unit = 1_000_000 if micro%unit == 0 { return fmt.Sprintf("%d", micro/unit) } return fmt.Sprintf("%.2f", float64(micro)/float64(unit)) }