环境/框架:windows7+Intellij Idea+jdk8+tomcat+Spring
支付类型:扫码支付模式二(统一下单接口)
只是希望少一点人踩同样坑的列表(未完):
测试接口问题:4月初开始写的时候看到开发文档的最佳实践-支付验收一节,一直以为测试时需要走先沙盒路径,结果报的错和这个帖子(微信支付公众号支付提示验证签名失败、报错“请调用getsignkey生成沙箱密钥”)一模一样,查完参数又查签名,始终找不到原因。后来百度了几篇教程才突然发现:用统一下单接口的都没有提到沙盒……马上试了下正式的unifiedorder接口、果然一次就拿到二维码链接……再后来总算给账号让登公众平台,结果找了半天似乎也只能配置扫码支付模式一的测试url。所以虽然明知会污染对账单,为了赶进度、目前还是直接拿正式付款的账号和接口进行开发测试的。
appid:微信支付需要提前申请商户号
mch_id
和公众账号IDappid
,这是所有请求的必带参数,商户号无歧义,但一个企业可能会同时有企业号corpid和公众号openid,样子也都是以wx
2个字符开头,填错可能报“mchid和appid不符”。http返回没有消息体:记得检查是否漏了必填参数。微信只要正常收到请求,http response status code就都是
200 ok
这一点上也真是省力…、client只需解析body string。但遇到一次消息体为null的异常,检查到最后发现是因为读取数据库账号id表时出了问题,漏掉了appid根本没写进xml。签名校验问题
微信支付文档中关于签名计算流程写的挺明白的,因为是Java没有直接例程、抄起来就自己写了、结果还是遇到了一下细节问题:Checksum需要用UTF-8:开始时图省力调用了别人写的md5 util方法、结果里面使用了系统默认编码(tomcat在windows下是GBK),而用Idea直接跑static main时用的却是utf-8(猜测是自动检测了源文件编码)。最终现象是明明单元测试时sign生成方法和微信的签名验证工具一致、一放到tomcat跑起来就签名失败、调试很久才找到原因。所以后来整理了下相关的utf-8配置问题、写在了这篇。
除NULL外,值为空字串的参数也要剔除:微信返回的xml里可能包含whitespace node,比如
<sub_mch_id></sub_mch_id>
,而我当时使用的默认的Jackson xml mapper将它解析成了length=0的空string""
,拼接signStr时就变成了...&sub_mch_id=&...
,导致校验失败。网上说的“一定要确认参数名称和大小写”确实要注意,但另外一些“有中文就不行”、“凡string都要包在CDATA内”等,似乎是没什么大关系的。
补一段签名计算代码吧:
public static String computeSign(Map<String, String> params, String key) { if (params == null || key == null) throw new NullPointerException("param map or key is null"); String signSrc = params.entrySet().stream() .filter(e -> e.getValue() != null) .filter(e -> !(e.getValue().trim().length() == 0)) .filter(e -> !"sign".equals(e.getKey())) .map(e -> e.getKey() + "=" + e.getValue()) .sorted() .collect(Collectors.joining("&")); signSrc = signSrc + "&key=" + key; String sign = null; try { sign = compute("MD5", signSrc, UTF_8).toUpperCase(); } catch (NoSuchAlgorithmException e) { LOGGER.error("computeSign", e); } return sign; } // checksum public static String compute(String checksumAlg, String src, Charset charSet) throws NoSuchAlgorithmException { final byte[] hashBytes = MessageDigest.getInstance(checksumAlg).digest(src.getBytes(charSet)); return DatatypeConverter.printHexBinary(hashBytes); }
退款
- 商户证书相关写在了这篇
返回结果格式 :申请退款和查询退款的返回结果里有很多
out_refund_no_$n
(甚至coupon_refund_fee_$n_$m
)这样带后缀的参数,初次看文档时整个人也是“$$”了。直到测试时收到的实际结果……我并没有很多年的经验里是第一次看到竟然有人会想到用改xml标签名的方式来表示一个列表的(设计一个复杂一点的element、甚至直接用attribute不好吗)。(总之因为强烈地repel)退款这块数据并没有很好处理……这里只是附贴一下结果样例(2017/04,省略了一些通用项):
申请退款返回:<xml> <transaction_id><![CDATA[40007***76642]]></transaction_id> <out_trade_no><![CDATA[1492844-5410-000-002-929]]></out_trade_no> <out_refund_no><![CDATA[1493007-4988-0018]]></out_refund_no> <refund_id><![CDATA[50000***05548]]></refund_id> <refund_channel><![CDATA[]]></refund_channel> <refund_fee>1</refund_fee> <coupon_refund_fee>0</coupon_refund_fee> <total_fee>2</total_fee> <cash_fee>2</cash_fee> <coupon_refund_count>0</coupon_refund_count> <cash_refund_fee>1</cash_refund_fee> </xml>
查询退款返回:
<xml> <cash_fee><![CDATA[2]]></cash_fee> <out_refund_no_0><![CDATA[1493007-4988-0018]]></out_refund_no_0> <out_trade_no><![CDATA[1492844-5410-000-002-929]]></out_trade_no> <refund_account_0><![CDATA[REFUND_SOURCE_UNSETTLED_FUNDS]]></refund_account_0> <refund_channel_0><![CDATA[ORIGINAL]]></refund_channel_0> <refund_count>1</refund_count> <refund_fee>1</refund_fee> <refund_fee_0>1</refund_fee_0> <refund_id_0><![CDATA[50000***05548]]></refund_id_0> <refund_recv_accout_0><![CDATA[支付用户的零钱]]></refund_recv_accout_0> <refund_status_0><![CDATA[SUCCESS]]></refund_status_0> <refund_success_time_0><![CDATA[2017-04-24 12:18:26]]></refund_success_time_0> <total_fee><![CDATA[2]]></total_fee> <transaction_id><![CDATA[40007***76642]]></transaction_id> </xml>
开发文档阅读:不是很重要但是真挺让人抓狂的一点,微信支付开发文档网站风格设计完全一样、内容却像是根据不同支付方式各自维护的,比如请比较:
安全规范:
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3
统一下单:
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1
百度来的真的很容易走错啊。