记录一下踩过的坑吧。
项目里对接微信特约商户支付差不多是一年前了吧,很多东西都忘了,刚好今天在新项目里调试这一块,然后掉坑里了。
比较久的一些坑以后再说,就先记录一下今天的坑吧。
这里以php,微信最新的sdk为例。
先说说第一个坑。
微信支付开发文档(境内)分普通商户版、服务商版、银行服务商版,这里只讨论前两个,两者区别的话就是在接口上的一些参数不同,比如服务商版在调用统一支付API的时候需要填写sub_app_id,sub_mch_id之类的,这没什么,加上就是了,如果自己从0开始写的话那没什么,如果使用微信的SDK的话就要注意了,普通商户版和服务商版提供的SDK同样都是普通商户版的SDK,这就意味着,如果你要对接服务商版,缺失的参数以及对应的设置方法获取方法需要你自己在SDK里添加好,否则不成功,比如前面提到的sub_app_id,sub_mch_id。
贴上一段代码:
public static function unifiedOrder($config, $inputObj, $timeOut = 6)
{
$url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
//检测必填参数
if(!$inputObj->IsOut_trade_noSet()) {
throw new WxPayException("缺少统一支付接口必填参数out_trade_no!");
}else if(!$inputObj->IsBodySet()){
throw new WxPayException("缺少统一支付接口必填参数body!");
}else if(!$inputObj->IsTotal_feeSet()) {
throw new WxPayException("缺少统一支付接口必填参数total_fee!");
}else if(!$inputObj->IsTrade_typeSet()) {
throw new WxPayException("缺少统一支付接口必填参数trade_type!");
}
//关联参数
if($inputObj->GetTrade_type() == "JSAPI" && !$inputObj->IsOpenidSet()){
throw new WxPayException("统一支付接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!");
}
if($inputObj->GetTrade_type() == "NATIVE" && !$inputObj->IsProduct_idSet()){
throw new WxPayException("统一支付接口中,缺少必填参数product_id!trade_type为JSAPI时,product_id为必填参数!");
}
//异步通知url未设置,则使用配置文件中的url
if(!$inputObj->IsNotify_urlSet() && $config->GetNotifyUrl() != ""){
$inputObj->SetNotify_url($config->GetNotifyUrl());//异步通知url
}
$inputObj->SetAppid($config->GetAppId());//公众账号ID
$inputObj->SetMch_id($config->GetMerchantId());//商户号
$inputObj->SetSpbill_create_ip($_SERVER['REMOTE_ADDR']);//终端ip
$inputObj->SetNonce_str(self::getNonceStr());//随机字符串
//签名
$inputObj->SetSign($config);
$xml = $inputObj->ToXml();
$startTimeStamp = self::getMillisecond();//请求开始时间
$response = self::postXmlCurl($config, $xml, $url, false, $timeOut);
$result = WxPayResults::Init($config, $response);
self::reportCostTime($config, $url, $startTimeStamp, $result);//上报请求花费时间
return $result;
}
这是微信支付SDK中统一下单接口方法,其中$inputObj在普通商户下这样用是没问题的,但是到了服务商版本,有了如下区别:
先来看看服务商支付所需要的参数(摘自微信支付服务商版统一支付API):
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
服务商的APPID | appid | 是 | String(32) | wx8888888888888888 | 服务商商户的APPID |
商户号 | mch_id | 是 | String(32) | 1900000109 | 微信支付分配的商户号 |
子商户公众账号ID | sub_appid | 否 | String(32) | wx8888888888888888 | 微信分配的子商户公众账号ID,如需在支付完成后获取sub_openid则此参数必传。 |
子商户号 | sub_mch_id | 是 | String(32) | 1900000109 | 微信支付分配的子商户号 |
设备号 | device_info | 否 | String(32) | 013467007045764 | 终端设备号(门店号或收银设备ID),注意:PC网页或JSAPI支付请传"WEB" |
开发票入口开放标识 | receipt | 否 | String(8) | Y | Y,传入Y时,支付成功消息和支付详情页将出现开票入口。需要在微信支付商户平台或微信公众平台开通电子发票功能,传此字段才可生效 |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位。推荐随机数生成算法 |
签名 | sign | 是 | String(64) | C380BEC2BFD727A4B6845133519F3AD6 | 签名,详见签名生成算法 |
签名类型 | sign_type | 否 | String(32) | HMAC-SHA256 | 签名类型,目前支持HMAC-SHA256和MD5,默认为MD5 |
商品描述 | body | 是 | String(128) | 腾讯充值中心-QQ会员充值 | 商品描述交易字段格式根据不同的应用场景建议按照以下格式上传: (1)PC网站——传入浏览器打开的网站主页title名-实际商品名称,例如:腾讯充值中心-QQ会员充值; (2) 公众号——传入公众号名称-实际商品名称,例如:腾讯形象店- image-QQ公仔; (3) H5——应用在浏览器网页上的场景,传入浏览器打开的移动网页的主页title名-实际商品名称,例如:腾讯充值中心-QQ会员充值; (4) 线下门店——门店品牌名-城市分店名-实际商品名称,例如: image形象店-深圳腾大- QQ公仔) (5) APP——需传入应用市场上的APP名字-实际商品名称,天天爱消除-游戏充值。 |
商品详情 | detail | 否 | String(6000) | 商品详情描述 | 商品详细描述,对于使用单品优惠的商户,改字段必须按照规范上传,详见“单品优惠参数说明” |
附加数据 | attach | 否 | String(127) | 说明 | 附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据 |
商户订单号 | out_trade_no | 是 | String(32) | 1217752501201407033233368018 | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一。详见商户订单号 |
货币类型 | fee_type | 否 | String(16) | CNY | 符合ISO 4217标准的三位字母代码,默认人民币:CNY,其他值列表详见货币类型 |
总金额 | total_fee | 是 | Int | 888 | 订单总金额,只能为整数,详见支付金额 |
终端IP | spbill_create_ip | 是 | String(64) | 123.12.12.123 | 支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP |
交易起始时间 | time_start | 否 | String(14) | 20091225091010 | 订单生成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010。其他详见时间规则 |
交易结束时间 | time_expire | 否 | String(14) | 20091227091010 | 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。订单失效时间是针对订单号而言的,由于在请求支付的时候有一个必传参数prepay_id只有两小时的有效期,所以在重入时间超过2小时的时候需要重新请求下单接口获取新的prepay_id。其他详见时间规则 time_expire只能第一次下单传值,不允许二次修改,二次修改系统将报错。如用户支付失败后,需再次支付,需更换原订单号重新下单。 建议:最短失效时间间隔大于1分钟 |
订单优惠标记 | goods_tag | 否 | String(32) | WXG | 订单优惠标记,代金券或立减优惠功能的参数,说明详见代金券或立减优惠 |
通知地址 | notify_url | 是 | String(256) | http://www.weixin.qq.com/wxpay/pay.php | 接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数。 |
交易类型 | trade_type | 是 | String(16) | JSAPI | JSAPI -JSAPI支付 NATIVE -Native支付 APP -APP支付 说明详见参数规定 |
商品ID | product_id | 否 | String(32) | 12235413214070356458058 | trade_type=NATIVE时,此参数必传。此id为二维码中包含的商品ID,商户自行定义。 |
指定支付方式 | limit_pay | 否 | String(32) | no_credit | no_credit--指定不能使用信用卡支付 |
用户标识 | openid | 否 | String(128) | oUpF8uMuAJO_M2pxb1Q9zNjWeS6o | trade_type=JSAPI,此参数必传,用户在主商户appid下的唯一标识。openid和sub_openid可以选传其中之一,如果选择传sub_openid,则必须传sub_appid。下单前需要调用【网页授权获取用户信息】接口获取到用户的Openid。 |
用户子标识 | sub_openid | 否 | String(128) | oUpF8uMuAJO_M2pxb1Q9zNjWeS6o | trade_type=JSAPI,此参数必传,用户在子商户appid下的唯一标识。openid和sub_openid可以选传其中之一,如果选择传sub_openid,则必须传sub_appid。下单前需要调用【网页授权获取用户信息】接口获取到用户的Openid。 |
电子发票入口开放标识 | receipt | 否 | String(8) | Y | Y,传入Y时,支付成功消息和支付详情页将出现开票入口。需要在微信支付商户平台或微信公众平台开通电子发票功能,传此字段才可生效 |
+场景信息 | scene_info | 否 | String(256) | {"store_info" : {"id": "SZTX001","name": "腾大餐厅","area_code": "440305","address": "科技园中一路腾讯大厦" }} | 该字段常用于线下活动时的场景信息上报,支持上报实际门店信息,商户也可以按需求自己上报相关信息。该字段为JSON对象数据,对象格式为{"store_info":{"id": "门店ID","name": "名称","area_code": "编码","address": "地址" }} ,字段详细说明请点击行前的+展开 |
再来看看普通商户所需要的参数(摘自微信支付普通商户版统一支付API):
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | 是 | String(32) | wxd678efh567hg6787 | 微信支付分配的公众账号ID(企业号corpid即为此appId) |
商户号 | mch_id | 是 | String(32) | 1230000109 | 微信支付分配的商户号 |
设备号 | device_info | 否 | String(32) | 013467007045764 | 自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB" |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,长度要求在32位以内。推荐随机数生成算法 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 通过签名算法计算得出的签名值,详见签名生成算法 |
签名类型 | sign_type | 否 | String(32) | MD5 | 签名类型,默认为MD5,支持HMAC-SHA256和MD5。 |
商品描述 | body | 是 | String(128) | 腾讯充值中心-QQ会员充值 | 商品简单描述,该字段请按照规范传递,具体请见参数规定 |
商品详情 | detail | 否 | String(6000) | 商品描述 | 商品详细描述,对于使用单品优惠的商户,该字段必须按照规范上传,详见“单品优惠参数说明” |
附加数据 | attach | 否 | String(127) | 深圳分店 | 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 |
商户订单号 | out_trade_no | 是 | String(32) | 20150806125346 | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|* 且在同一个商户号下唯一。详见商户订单号 |
标价币种 | fee_type | 否 | String(16) | CNY | 符合ISO 4217标准的三位字母代码,默认人民币:CNY,详细列表请参见货币类型 |
标价金额 | total_fee | 是 | Int | 88 | 订单总金额,单位为分,详见支付金额 |
终端IP | spbill_create_ip | 是 | String(64) | 123.12.12.123 | 支持IPV4和IPV6两种格式的IP地址。用户的客户端IP |
交易起始时间 | time_start | 否 | String(14) | 20091225091010 | 订单生成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010。其他详见时间规则 |
交易结束时间 | time_expire | 否 | String(14) | 20091227091010 | 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。订单失效时间是针对订单号而言的,由于在请求支付的时候有一个必传参数prepay_id只有两小时的有效期,所以在重入时间超过2小时的时候需要重新请求下单接口获取新的prepay_id。其他详见时间规则 time_expire只能第一次下单传值,不允许二次修改,二次修改系统将报错。如用户支付失败后,需再次支付,需更换原订单号重新下单。 建议:最短失效时间间隔大于1分钟 |
订单优惠标记 | goods_tag | 否 | String(32) | WXG | 订单优惠标记,使用代金券或立减优惠功能时需要的参数,说明详见代金券或立减优惠 |
通知地址 | notify_url | 是 | String(256) | http://www.weixin.qq.com/wxpay/pay.php | 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 |
交易类型 | trade_type | 是 | String(16) | JSAPI | JSAPI -JSAPI支付 NATIVE -Native支付 APP -APP支付 说明详见参数规定 |
商品ID | product_id | 否 | String(32) | 12235413214070356458058 | trade_type=NATIVE时,此参数必传。此参数为二维码中包含的商品ID,商户自行定义。 |
指定支付方式 | limit_pay | 否 | String(32) | no_credit | 上传此参数no_credit--可限制用户不能使用信用卡支付 |
用户标识 | openid | 否 | String(128) | oUpF8uMuAJO_M2pxb1Q9zNjWeS6o | trade_type=JSAPI时(即JSAPI支付),此参数必传,此参数为微信用户在商户对应appid下的唯一标识。openid如何获取,可参考【获取openid】。企业号请使用【企业号OAuth2.0接口】获取企业号内成员userid,再调用【企业号userid转openid接口】进行转换 |
电子发票入口开放标识 | receipt | 否 | String(8) | Y | Y,传入Y时,支付成功消息和支付详情页将出现开票入口。需要在微信支付商户平台或微信公众平台开通电子发票功能,传此字段才可生效 |
+场景信息 | scene_info | 否 | String(256) | {"store_info" : {"id": "SZTX001","name": "腾大餐厅","area_code": "440305","address": "科技园中一路腾讯大厦" }} | 该字段常用于线下活动时的场景信息上报,支持上报实际门店信息,商户也可以按需求自己上报相关信息。该字段为JSON对象数据,对象格式为{"store_info":{"id": "门店ID","name": "名称","area_code": "编码","address": "地址" }} ,字段详细说明请点击行前的+展开 |
这里就很明显看得出来二者的区别,服务商支付需要服务商的appid和服务商的mch_id,然后多了sub_app_id、sub_mch_id、sub_openid。
按照SDK的写法,这三个值应该有对应的设置方法和读取方法:
$inputObj->SetSubAppid();
$inputObj->SetSubMch_id();
$inputObj->SetSubOpen_id();
然而,并没有。甚至我在整个SDK里搜索sub这三个字母都无结果。所以这里需要我们自己去添加对应的方法。emmmmmm这算是个小坑吧,自己改改就完。
然后说回这仨值,这仨值是必填!必填!必填!否则报错,而文档里sub_app_id、sub_openid的必填属性为否。
嗯,好,以上的都没啥问题了,后边签名又过不去了,看文档(服务商版):
嗯,没毛病,不过怎么看着就面熟呢?再看看普通商户版:
这特么不一个样么?不都是按照ASCII码从小到大排序拼接然后拼上商户密钥key最后md5运算转换大写么?为什么我就是不通过?
那么,坑来了,这个商户密钥key,服务商版的需要用服务商的商户密钥key,而不是子商户的。
好的,换上了服务商的key,通过了,也获取到了前端调起支付需要的xml数据了,然后兴冲冲拿着这些数据去进行签名处理好发给前端调起支付了,然后前端也确实得到了timeStamp、nonceStr、package、signType、paySign这五个参数(以小程序为例),然后前端也兴冲冲的拿起手机扫码付款了,这时候突然手机上一个弹窗:支付签名验证失败。然后一下子就从天上掉下来了,为什么?不是签名通过了么?参数也都全啊?怎么就又签名失败了?
于是跑回去各种检查各种尝试,然后对比检查到了签名这一块:appid、timeStamp、nonceStr、package、signType都设置了啊,package也按要求prepay_id=的方式填写了啊,到底怎么回事?恭喜又踩坑了,这里的appid不能使用服务商的,而是使用子商户的,也就是sub_app_id。
改完参数后,emmmmmmm成了。
以上是付款遇到的坑,emmmmm还有退款,年代久远都忘了,等新项目对接到了再说吧,又得头疼一阵子。