代码 微信支付服务商模式:只改签名密钥就报 PARAM_ERROR,sp_appid 和 sub_appid 下的 openid 怎么配对

2026-10-24 09:01:26

微信支付服务商模式:只改签名密钥就报 PARAM_ERROR,sp_appid 和 sub_appid 下的 openid 怎么配对

一个直连商户支付模块切到服务商模式(普通服务商 + 特约商户)后,只改了签名密钥,下单全部返回 PARAM_ERROR。根因不在签名:服务商模式多了一套平行的 ID,商户号分服务商和子商户,AppID 也分服务商和子商户,openid 跟着 AppID 走,两个 AppID 下的 openid 不是一回事。

接入背景

直连模式只有一组:appid + mchid + openid。服务商模式的参数名带 sp_/sub_ 前缀:

  • sp_appid:服务商在开放平台/公众平台申请的 AppID
  • sp_mchid:服务商商户号
  • sp_openid:用户在该 sp_appid 下的标识
  • sub_appid:子商户的 AppID(选填)
  • sub_mchid:子商户号(必填)
  • sub_openid:用户在该 sub_appid 下的标识

下单接口:POST /v3/pay/partner/transactions/jsapi(小程序同一个地址)。文档:微信支付服务商 JSAPI 下单。

核心规则:appid 和 openid 必须成对

官方文档的原话:

  • 使用 sp_appid 时传 payer.sp_openid,openid 必须由 sp_appid 换取
  • 使用 sub_appid 时传 payer.sub_openid,openid 必须由 sub_appid 换取
  • payer.sp_openid 和 payer.sub_openid 二选一必填
  • 若传 sub_openid,则 sub_appid 必填

工程上常见三种错:

  1. 小程序 wx.login 拿 code,用子商户小程序换 openid,却填进 payer.sp_openid → 「appid与openid不匹配」
  2. 传了 sub_appid 但填了 sp_appid 换来的 openid → 「sub_appid和sub_openid不匹配」
  3. 只传 payer.openid(照抄直连模式字段名)→ 参数校验失败或 openid 无效

取舍:sub_appid 到底传不传

场景 A:收款方是子商户,但用户在你自己的小程序/公众号里下单(平台型 SaaS 常见)。不传 sub_appid,传 sp_appid + payer.sp_openid,sub_mchid 照样必填。钱进子商户账户,用户身份挂在你自己的 AppID 下。好处是只要维护一套登录/openid;代价是子商户看不到自己品牌 AppID 下的用户,营销触达能力归你。

场景 B:用户从子商户自己的小程序进来。sub_appid 必传,payer.sub_openid 必须是从这个 sub_appid 换来的。这要求 sub_appid 已在服务商平台与 sub_mchid 绑定,否则报「sub_mch_id与sub_appid不匹配」。代价是每个子商户要单独绑 AppID、单独做网页授权域名配置,运营成本随商户数线性增长。

能用自己的 AppID 收就用 sp_appid + sp_openid,把 sub_appid 当作「必须支持子商户自有小程序」时才付的成本。别为了对齐字段两边都传——两者都传时以哪个为准,官方没有承诺,也别赌。

下单请求示例

curl -s -X POST https://api.mch.weixin.qq.com/v3/pay/partner/transactions/jsapi \
  -H "Authorization: WECHATPAY2-SHA256-RSA2048 mchid=\"1900000001\",nonce_str=\"...\",signature=\"...\",timestamp=\"...\",serial_no=\"...\"" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{
    "sp_appid":  "wx服务商appid",
    "sp_mchid":  "1900000001",
    "sub_mchid": "1900000109",
    "description": "年度会员",
    "out_trade_no": "T20261024001",
    "notify_url": "https://example.com/pay/notify/wx",
    "amount": {"total": 100, "currency": "CNY"},
    "payer": {"sp_openid": "oXXXX"}
  }'

注意点:

  • 签名用的是服务商商户号的 API 证书私钥与序列号,不是子商户的。回调验签/解密用的也是服务商维度配置的 APIv3 密钥。
  • amount.total 单位是分,整数。
  • prepay_id 有效期 2 小时;过期要用原 out_trade_no 和原参数重新下单,不是换个单号。
  • 不传 time_expire 默认 7 天有效,超期订单需关单。

调起支付:appId 必须和下单时一致

前端 wx.requestPayment 的 appId 要填下单时实际用的那个 appid(sp_appid 或 sub_appid),而且当前实际调起支付的公众号/小程序必须就是它。填错报「商户传入的APPID参数不正确」。package 的值是 "prepay_id=" + 下单返回的 prepay_id,前缀不能省。

支付授权目录在服务商平台配置(可以给全体子商户配,也可以给单个子商户配),网页授权域名在公众平台配——这两个是两个地方,只配一个就会出现「能调起来但拿不到 openid」或者「拿到 openid 但调不起来」。paySign 的构造与排查站内 #8142 已单独写过,这里不重复。

报错对照表

报错原文出现阶段实际原因
PARAM_ERROR 受理机构必须传入sub_mch_id下单用的是服务商商户号却调了普通商户模式接口,路径少了 /partner
appid和mch_id不匹配,请检查后再试下单sp_appid 未与服务商商户号绑定
sub_mch_id与sub_appid不匹配下单sub_appid 未在服务商平台与 sub_mchid 绑定
appid与openid不匹配 / sub_appid和sub_openid不匹配下单openid 与本次使用的 appid 不配对
无效的openid下单openid 不存在(常见于串了测试号或存储被截断)
下单账号与支付账号不一致,请核实后再支付调起下单的 openid 属于 A 用户,实际支付的是 B 用户(同一浏览器切换账号、或前端缓存了旧 openid)

最后一条最容易被误判成签名问题。它是用户维度的校验,跟证书没关系。

回调里怎么取用户标识

支付成功通知是 TRANSACTION.SUCCESS,resource.ciphertext 用 APIv3 密钥 AES-256-GCM 解密后,字段是 sp_appid / sp_mchid / sub_appid / sub_mchid / out_trade_no / transaction_id / trade_state / payer 等。关键:payer 下面有两个 key,sp_openid 和 sub_openid。文档把 sp_openid 标为必填、sub_openid 标为选填,且写明「若下单未传 sub_appid 参数,则不会返回」。

所以不要在回调里写死取 payer.sub_openid,也不要默认 payer.openid 存在——服务商模式回调日志里没有 payer.openid 这个 key(那是直连模式的字段)。稳妥做法:用 out_trade_no 回本地订单表取本单实际使用的 appid 和 openid,回调里的 openid 只用来做一致性校验,不一致就告警不要直接改库。

适用范围

以上规则适用于普通服务商 + JSAPI/小程序支付的 v3 接口。电商收付通(平台商户)、境内服务商跨境、以及 v2 的统一下单在字段命名上有差异,不要直接套。文中字段与报错文案摘自微信支付合作伙伴文档(下单接口 4012738519、支付结果通知 4012090195、常见问题 4013334850/4013352071),未在真实商户号上逐条复现;报错文案以你实际收到的响应为准,微信会改字。

数据模型:openid 不是用户主键

踩过的坑基本都是因为表结构假设了「一个用户一个 openid」。openid 是 AppID 维度的,同一个微信用户在不同 AppID 下是不同的字符串。服务商模式下这个维度至少有三个:你的 sp_appid、子商户的 sub_appid、以及未来可能接入的其它小程序。

最小可用结构:

CREATE TABLE user_wechat (
  id           BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id      BIGINT NOT NULL COMMENT '本地用户',
  appid        VARCHAR(32) NOT NULL COMMENT 'openid 所属 AppID',
  openid       VARCHAR(128) NOT NULL,
  unionid      VARCHAR(128) NULL,
  created_at   DATETIME NOT NULL,
  UNIQUE KEY uk_appid_openid (appid, openid),
  KEY idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单表另存 pay_appid 和 pay_openid(下单时使用的,不是查询时再算),以及 sp_mchid/sub_mchid 两个商户号。这样对账、退款、分账时不用再猜这单是哪个 AppID 下的用户,也不会出现「退款退给错误的 openid」这种不可逆事故。

推荐文章

程序员茄子在线接单