feat(wechat): 支付成功后微信客服消息推回执(积分包/订阅购买)

用户扫码付款后(在 48h 互动窗口内),主动推一条客服消息回执:
到账积分/开通套餐 + 当前余额。只在 MarkOrderPaid changed=true 首次到账时推,
异步+超时隔离,失败只记日志、绝不影响入账。非微信用户自动跳过。

不做「周期刷新提醒」:客服消息受 48h 窗口限制,定时刷新那刻用户多半已超窗、
必然失败,那类隔天提醒须用模板消息(暂缓)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-21 11:23:41 +08:00
parent 83269e067a
commit 68608c1592
4 changed files with 129 additions and 2 deletions
+29
View File
@@ -179,6 +179,35 @@ func (c Config) CreateLoginQR(ctx context.Context, accessToken, scene string, ex
return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + url.QueryEscape(r.Ticket), nil
}
// SendCustomText 发一条「客服消息」文本给指定 openid(主动推送)。
// 约束:微信只允许在用户 48 小时内与公众号有过互动时下发(否则 45015 errcode)。
// 适合支付回执这类「用户刚操作完」的即时通知;隔天的提醒需改用模板消息。
// 需 access_token(走中控/直连均可)。
func SendCustomText(ctx context.Context, accessToken, openID, content string) error {
reqBody := map[string]any{
"touser": openID,
"msgtype": "text",
"text": map[string]any{"content": content},
}
raw, _ := json.Marshal(reqBody)
endpoint := "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" + url.QueryEscape(accessToken)
body, err := httpPost(ctx, endpoint, raw)
if err != nil {
return err
}
var r struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(body, &r); err != nil {
return fmt.Errorf("解析客服消息响应失败: %s", strings.TrimSpace(string(body)))
}
if r.ErrCode != 0 {
return fmt.Errorf("发送客服消息失败: errcode=%d errmsg=%s", r.ErrCode, r.ErrMsg)
}
return nil
}
// Event 是微信推送的事件(明文 XML)。只取登录/回复需要的字段。
type Event struct {
XMLName xml.Name `xml:"xml"`