案例 代金券回调新增 appid/openid:多公众号共商户号时怎么路由、校验与限频

2026-10-20 21:05:24

代金券回调新增 appid/openid:多公众号共商户号时怎么路由、校验与限频

信源:微信支付文档中心 2026-10 更新日志,更新时间 2026.10.14。

  • 服务商文档:https://pay.wechatpay.cn/doc/v3/partner/4012085442
  • 商户文档:https://pay.weixin.qq.com/doc/v3/merchant/4012078749

下面按更新日志条目顺序整理,重点标出会影响已有对接代码的部分。

变更原文条目

12. 代金券

  • 《设置代金券消息通知地址》新增选填请求参数 appid【公众账号ID】。
  • 《查询代金券消息通知地址》同步新增对应的选填返回参数。
  • 设置正确的 appid 后,《核销事件回调通知》新增选填返回参数 openid【核销用户 openid】。

4. 商家名片会员——《创建会员卡模板》

  • 新增选填请求参数:support_join_component【是否支持入会组件】、purchase_membership【是否为付费会员】。
  • 必填改为选填:card_color【卡背景颜色】、card_picture_url【卡图片】、code_mode【会员卡 code 分配类型】、code_type【会员码展示类型】、benefits【会员权益】、notify_url【回调地址】、legal_agreement【商家法务协议】、valid_date_information【会员卡有效期】。

6. 商品券——《创建商品券(单券/多次优惠)》

  • name【商品券名称】字符上限 15 → 25,《修改商品券》同步。
  • available_begin_time / available_end_time 支持范围 365 天 → 366 天。
  • 新增选填请求参数:show_as_exchange【是否展示兑换价】、available_product_no_list【可用商品编码列表】,各接口对应新增返回参数。
  • scope【优惠范围】新增枚举值 CATEGORY:品类券。
  • 下线选填请求参数 entrance_finder【视频号入口】。

7. 商品券——《修改商品券》

  • display_info【展示信息】由必填改选填,且必须与 available_product_no_list 二选一传参。

8. 商品券——《向用户发放商品券》《向用户发放商品券批次组》

  • coupon_tag_list 新增枚举 MEMBER_ACTIVITY:会员活动标签。
  • member_card_id 必填改选填。
  • send_channel 新增枚举 STICKER_GIFT:贴图有礼。

9. 商品券——《核销用户商品券》

  • 新增选填请求参数 use_product_no_list【核销商品编码列表】,各接口新增返回参数。

10. 商家名片会员限频

  • 各接口调用限频放宽至 100/秒;《根据 OPENID 导入用户会员卡》限频放宽至 200/秒。

11. 商家名片会员——《修改会员卡模板信息》

  • 新增选填请求参数:support_join_component、purchase_membership、legal_agreement、init_level【会员初始等级】。

1–3、5. 进件类《提交申请单》(商户开户意愿确认、平台收付通(商户进件)、特约商户进件、平台收付通(修改主体信息))

  • finance_type 新增枚举 SMALL_LOAN,小额贷款机构不再归入 OTHER。

代金券回调:appid 与 openid 带来的路由变化

代金券消息通知地址是「每个商户号一份」的配置,即回调 URL 挂在商户号维度。过去的核销回调里没有 appid,也没有 openid:一个商户号同时服务多个公众号/小程序时,收到回调无法判断这是哪个 appid 下的用户,更拿不到核销人。

现在的链路是:先调《设置代金券消息通知地址》带上 appid,再通过《查询代金券消息通知地址》确认已生效,之后的《核销事件回调通知》才会带上 openid。也就是说 openid 是配置 appid 之后才出现的可选返回参数,不是默认就有。历史配置没补 appid 的商户号,回调行为不变。

路由不要硬编码。落一张 mchid + appid → 业务库/租户 的映射表,回调进来先查表:

CREATE TABLE wxpay_appid_route (
  mchid  VARCHAR(32) NOT NULL,
  appid  VARCHAR(64) NOT NULL,
  tenant VARCHAR(64) NOT NULL,
  PRIMARY KEY (mchid, appid)
);
// body 已完成 APIv3 验签,appid/openid 仍以验签后的报文为准
type CouponUseNotify struct {
    Appid      string `json:"appid"`
    Openid     string `json:"openid"`
    OutTradeNo string `json:"out_trade_no"`
}

func handleCouponUse(ctx context.Context, n CouponUseNotify) error {
    tenant, err := route.Lookup(ctx, n.Appid) // 按 appid 查路由,不写死
    if err != nil {
        return fmt.Errorf("no route for appid %s: %w", n.Appid, err)
    }
    // 幂等键:核销流水号
    if done, _ := dedup.Seen(ctx, n.OutTradeNo); done {
        return nil
    }
    return biz.Apply(ctx, tenant, n)
}

openid 是可选返回参数,取不到时不要直接 panic,按「无法定位用户」走降级分支并记录报文字段。

必填改选填之后的字段校验

《创建会员卡模板》里 card_color、card_picture_url、code_mode、code_type、benefits、notify_url、legal_agreement、valid_date_information 全部由必填改为选填。微信侧不会因为少传而拒绝,但两侧的本地代码要注意:

  • 请求侧:如果本地 JSON Schema 或参数校验器仍把这几个字段标成 required,会自己把请求拦下来,看起来像「升级后接口调不通」。
  • 响应侧:如果组装本地对象时假定「必填字段一定存在」,比如直接拿 card_color 去做渲染或写库,新版本可能拿到 null 或空串。
// 旧写法:默认字段一定非空
tmpl.Color = resp.CardColor

// 改成可空 + 渲染侧兜底
type CardTemplate struct {
    Color        *string `json:"card_color"`
    PictureURL   *string `json:"card_picture_url"`
    NotifyURL    *string `json:"notify_url"`
}

商品券侧同理:《修改商品券》的 display_info 由必填改选填,且必须与 available_product_no_list 二选一——两个都不传会被拒,都传也会被拒,调用前要显式分支:

switch {
case req.DisplayInfo != nil && len(req.AvailableProductNoList) == 0:
    // 走展示信息
case req.DisplayInfo == nil && len(req.AvailableProductNoList) > 0:
    // 走可用商品编码列表
default:
    return errors.New("display_info 与 available_product_no_list 必须二选一")
}

entrance_finder 已下线,请求里还留着这个字段可能被忽略、也可能报参数错误,按新文档删掉最稳。scope 新增的 CATEGORY(品类券)、coupon_tag_list 新增的 MEMBER_ACTIVITY(会员活动标签)、send_channel 新增的 STICKER_GIFT(贴图有礼)需要在枚举映射表里补上,否则落库时会落到 default 分支。

限频放宽到 100/秒后的批量同步

商家名片会员各接口从原来的保守值放宽到 100/秒,《根据 OPENID 导入用户会员卡》到 200/秒。批量导入会员卡时可以据此提高并发,但要注意两点:其他接口仍按各自的限频走,不要用一个全局并发数套所有接口;腾讯侧限频和你自己数据库的写入压力是两件事,压测要以库为准。

按接口分别配置限流器,留一点余量:

// 单接口限流:目标 80/s,低于 100/s 的上限
limiter := rate.NewLimiter(rate.Limit(80), 80)

// 控制同时在飞的请求数,避免瞬时打满连接池
sem := make(chan struct{}, 16)

for _, item := range batch {
    if err := limiter.Wait(ctx); err != nil {
        return err
    }
    sem <- struct{}{}
    go func(it Item) {
        defer func() { <-sem }()
        importOne(ctx, it)
    }(item)
}

导入接口按 200/秒单独建一个 limiter,和普通查询接口分开计数。

回调处理的几个固定动作

  • 幂等:微信支付回调会重试,用 out_trade_no / 核销流水号做去重键。
  • 验签:用平台证书/公钥走 APIv3 验证,appid 来自 body,不能当成可信来源直接用于路由。
  • 路由:appid 到业务库的映射落库维护,一个商户号接多个 appid 时按 appid 查记录。
  • 留痕:记录原始回调报文(脱敏后),排查「某个 appid 的用户核销没同步」时直接比对字段。
复制全文 生成海报 支付对接 微信支付 接口变更 Go

推荐文章

程序员茄子在线接单