微信支付V3集成过程(Native)
长话短说,微信支付V3版本相比V2版本,简化许多接口,大大降低了商户的接入代价,只不过由于相关的集成说明文档写的过于笼统,细节描述不够具体,demo版本不够完善等等原因,被大家诟病。
最近基于业务需要,完成了微信支付的接入,选择的是公众号扫码支付(Native模式)。在此把集成的大致过程贴出来,希望能对正在困扰的各位有所帮助。
整个交互过程分为四步:
1、商户:生成微信支付二维码
2、微信:扫描二维码,获取商户订单信息
3、微信:确认支付,调起微信支付模块,完成支付,回调商户通知接口
4、商户:接收微信支付成功后的通知信息,根据通知结果进行业务处理
交互时序图如下(参考微信官方说明):
这四步中,商户需要做的有三块工作,明确指出的1和4需要商户完成,还有一块工作隐藏在2中,提供微信回调接口,将商户订单信息返回给微信,该接口在申请微信支付后需要在公众平台进行配置。
在进行详细说明之前,先做几点说明:
1、涉及传参及返回结果,均为xml格式
2、默认编码为UTF-8
3、签名算法
重点说下签名算法,官方文档中对签名算法做了比较详细的说明,可以直接参考。不过没有说明每次交互中用到的签名sign,都有哪些字段参与生成了签名。
这里明确下:微信支付每次交互中的签名字段sign,由本次交互所有参数(sign和空值除外)参与签名生成,这点也是经常多次尝试后确认的。
贴出源码:
/**
* 创建签名
*
* @param packageParams
* @return
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static String createSign(SortedMap packageParams) {
StringBuffer sb = new StringBuffer();
Set<Entry> es = packageParams.entrySet();
Iterator<Entry> it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = entry.getValue() != null ? entry.getValue().toString() : null;
if (null != v
&& !"".equals(v)
&& !"sign".equals(k)
&& !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + WxPayConfig.APP_KEY);
return MD5Util.MD5Encode(sb.toString(), WxPayConfig.CHAR_SET).toUpperCase();
}
/**
* 签名校验
*
* @return
* @throws UnsupportedEncodingException
*/
@SuppressWarnings("rawtypes")
public static boolean verifyPaySign(SortedMap paras, String sign) throws UnsupportedEncodingException {
String paySign = Tools.createSign(paras);
return StringUtil.isNotEmpty(paySign) ? paySign.equalsIgnoreCase(sign) : false;
}
下面对每一步进行详细说明:
1、商户:生成微信支付二维码
微信支付链接格式为:weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXX&time_stamp=XXXXX&nonce_str=XXXXX
sign为签名字段,参与签名的字段为appid、mch_id、product_id、time_stamp、nonce_str
其中XXXXX为商户需要填写的内容,商户将该链接生成二维码,如需要打印发布二维码,需要采用此格式。商户可调用第三方库(zxing 、qrcode)生成二维码图片。
举例:weixin://wxpay/bizpayurl?appid=wx2421b1c4370ec43b&mch_id=10000100&nonce_str=f6808210402125e30663234f94c87a8c&product_id=1&time_stamp=1415949957&sign=512F68131DD251DA4A45DA79CC7EFE9D
贴出源码:略
2、微信:扫描二维码,获取商户订单信息
这里商户需要做的是,给微信提供获取订单信息的接口。接口需要完成三部分工作:
获取微信回调接口时传过来的参数,进行签名校验
输入参数说明如下:
名称 | 变量名 | 类型 | 必填 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | String(32) | 是 | wx8888888888888888 | 微信分配的公众账号ID |
用户标识 | openid | String(128) | 是 | o8GeHuLAsgefS_80exEr1cTqekUs | 用户在商户appid下的唯一标识 |
商户号 | mch_id | String(32) | 是 | 1900000109 | 微信支付分配的商户号 |
是否关注公众账号 | is_subscribe | String(1) | 是 | Y | 用户是否关注公众账号,仅在公众账号类型支付有效,取值范围:Y或N;Y-关注;N-未关注 |
随机字符串 | nonce_str | String(32) | 是 | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位 |
商品ID | product_id | String(32) | 是 | 88888 | 商户定义的商品id 或者订单号 |
签名 | sign | String(32) | 是 | C380BEC2BFD727A4B6845133519F3AD6 | 返回数据签名 |
sign为签名字段,参与签名的字段为appid、openid、mch_id、is_subscribe、nonce_str、product_id。
调用微信统一下单接口,生成预支付订单,获取预支付订单prepay_id
统一下单URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder
输入参数说明如下:
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | 是 | String(32) | wx8888888888888888 | 微信分配的公众账号ID |
商户号 | mch_id | 是 | String(32) | 1900000109 | 微信支付分配的商户号 |
设备号 | device_info | 否 | String(32) | 013467007045764 | 微信支付分配的终端设备号,商户自定义 |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 签名 |
商品描述 | body | 是 | String(32) | Ipad mini 16G 白色 | 商品或支付单简要描述 |
商品详情 | detail | 否 | String(8192) | Ipad mini 16G 白色 | 商品名称明细列表 |
附加数据 | 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(16) | 8.8.8.8 | APP和网页支付提交用户端ip,Native支付填调用微信支付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 |
商品标记 | goods_tag | 否 | String(32) | WXG | 商品标记,代金券或立减优惠功能的参数 |
通知地址 | notify_url | 是 | String(256) | http://www.baidu.com/ | 接收微信支付异步通知回调地址 |
交易类型 | trade_type | 是 | String(16) | JSAPI | 取值如下:JSAPI,NATIVE,APP |
商品ID | product_id | 否 | String(32) | 12235413214070356458058 | trade_type=NATIVE,此参数必传。此id为二维码中包含的商品ID,商户自行定义。 |
用户标识 | openid | 否 | String(128) | oUpF8uMuAJO_M2pxb1Q9zNjWeS6o | trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。 |
sign为签名字段,参与签名的字段包括上面除sign之外的所有非空字段。
返回结果说明如下:
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 |
返回信息 | return_msg | 否 | String(128) | 签名失败
| 返回信息,如非空,为错误原因 签名失败 参数格式校验错误 |
以下字段在return_code为SUCCESS的时候有返回
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | 是 | String(32) | wx8888888888888888 | 调用接口提交的公众账号ID |
商户号 | mch_id | 是 | String(32) | 1900000109 | 调用接口提交的商户号 |
设备号 | device_info | 否 | String(32) | 013467007045764 | 调用接口提交的终端设备号, |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 微信返回的随机字符串 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 微信返回的签名 |
业务结果 | result_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL |
错误代码 | err_code | 否 | String(32) | SYSTEMERROR | 详细参见第6节错误列表 |
错误代码描述 | err_code_des | 否 | String(128) | 系统错误 | 错误返回的信息描述 |
以下字段在return_code 和result_code都为SUCCESS的时候有返回
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
交易类型 | trade_type | 是 | String(16) | JSAPI | 调用接口提交的交易类型,取值如下:JSAPI,NATIVE,APP |
预支付交易会话标识 | prepay_id | 是 | String(64) | wx201410272009395522657a690389285100
| 微信生成的预支付回话标识,用于后续接口调用中使用,该值有效期为2小时 |
二维码链接 | code_url | 否 | String(64) | URl: weixin://wxpay/s/An4baqw | trade_type为NATIVE是有返回,可将该参数值生成二维码展示出来进行扫码支付 |
向微信回写商户订单信息
参数说明如下:
名称 | 变量名 | 类型 | 必填 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | String(16) | 是 | SUCCESS | SUCCESS/FAIL,此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 |
返回信息 | return_msg | String(128) | 否 | 签名失败 | 返回信息,如非空,为错误原因;签名失败;具体某个参数格式校验错误. |
公众账号ID | appid | String(32) | 是 | wx8888888888888888 | 微信分配的公众账号ID |
商户号 | mch_id | String(32) | 是 | 1900000109 | 微信支付分配的商户号 |
随机字符串 | nonce_str | String(32) | 是 | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 微信返回的随机字符串 |
预支付ID | prepay_id | String(64) | 是 | wx201410272009395522657a690389285100 | 调用统一下单接口生成的预支付ID |
业务结果 | result_code | String(16) | 是 | SUCCESS | SUCCESS/FAIL |
错误描述 | err_code_des | String(128) | 否 | 当result_code为FAIL时,商户展示给用户的错误提 | |
签名 | sign | String(32) | 是 | C380BEC2BFD727A4B6845133519F3AD6 | 返回数据签名 |
sign为签名字段,参与签名的字段包括上面除sign之外的所有非空字段。
贴出源码:
// ************************************************************************************
// 1、参数校验
// 微信版本&支付签名&订单信息
// ************************************************************************************
// 预定义订单信息
Order order = null;
// 解析微信post过来的xml数据
Map<String, Object> postData = Tools.getXmlDataFromWx(request);
if (postData != null
&& postData.get("product_id") != null) {
// 校验支付签名是否一致
boolean isPaySignValid = Tools.verifyPaySign(Tools.convertMap2SortedMap(postData),
postData.get("sign").toString());
if (isPaySignValid) {
// 获取订单信息
order = orderService.get(postData.get("product_id").toString());
if (order == null) {
RetCode = WxPayRetCodeEnum.ORDERNOTEXIST.getCode();
RetErrMsg = WxPayRetCodeEnum.ORDERNOTEXIST.getMsg();
} else if (order.getOrderStatus() == OrderStatus.CANCELED.getStatus()) {
RetCode = WxPayRetCodeEnum.ORDEREXPIRED.getCode();
RetErrMsg = WxPayRetCodeEnum.ORDEREXPIRED.getMsg();
} else if (order.getPayStatus() == PayStatus.PAID.getStatus()
|| order.getPayAmount() <= order.getBalanceAmount()) {
RetCode = WxPayRetCodeEnum.ORDERPAYED.getCode();
RetErrMsg = WxPayRetCodeEnum.ORDERPAYED.getMsg();
}
} else {
RetCode = WxPayRetCodeEnum.PAYSIGNWRONG.getCode();
RetErrMsg = WxPayRetCodeEnum.PAYSIGNWRONG.getMsg();
}
} else {
RetCode = WxPayRetCodeEnum.WXPOSTXMLERROR.getCode();
RetErrMsg = WxPayRetCodeEnum.WXPOSTXMLERROR.getMsg();
}
// ************************************************************************************
// 2、调用微信统一下单接口,生成预支付订单
// ************************************************************************************
// 计算商品总金额,精确到分,取整
String totalFee = order != null ? String.valueOf(
NumberCaculator.multiply(NumberCaculator.substract(order.getPayAmount(), order.getBalanceAmount()), 100)) : "0";
totalFee = totalFee.indexOf(".") > -1 ? totalFee.substring(0, totalFee.indexOf(".")) : totalFee;
String outTradeNo = order != null ? order.getOrderSn() : null;
// 调用微信统一下单接口,生成微信预支付订单,获取交易会话标识
RequestHandler reqHandler = new RequestHandler();
reqHandler.setParameter("appid", WxPayConfig.APP_ID);
reqHandler.setParameter("mch_id", WxPayConfig.MCH_ID);
reqHandler.setParameter("nonce_str", Tools.getNonceStr());
reqHandler.setParameter("body", PurchaseTypeEnum.Course.getName());
reqHandler.setParameter("detail", PurchaseTypeEnum.Course.getDesc());
reqHandler.setParameter("out_trade_no", outTradeNo);
reqHandler.setParameter("total_fee", totalFee);
reqHandler.setParameter("spbill_create_ip", InetAddress.getLocalHost().getHostAddress().toString());
reqHandler.setParameter("notify_url", WxPayConfig.NOTIFY_URL);
reqHandler.setParameter("trade_type", WxPayTradeTypeEnum.NATIVE.getMsg());
reqHandler.setParameter("sign", Tools.createSign(reqHandler.getParameters()));
// 发起请求,调用统一下单接口,获取统一下单接口返回结果
String result = reqHandler.sendReq(WxPayConfig.UNIFIED_ORDER_API, reqHandler.getRequestParams4Xml());
Map<String, String> apiResult = XMLUtil.doXMLParse(result);
// ************************************************************************************
// 3、向微信回写订单信息
// ************************************************************************************
// 设置支付参数-为了返回Package 数据,回调URL 必须返回一个xml 格式的返回数据
SortedMap<String, Object> signParams = new TreeMap<String, Object>();
signParams.put("return_code", apiResult != null ? apiResult.get("return_code") : "FAIL");
signParams.put("return_msg", apiResult != null ? apiResult.get("return_msg") : "下单失败");
signParams.put("appid", WxPayConfig.APP_ID);
signParams.put("mch_id", WxPayConfig.MCH_ID);
signParams.put("nonce_str", Tools.getNonceStr());
signParams.put("prepay_id", apiResult != null ? apiResult.get("prepay_id") : "");
signParams.put("result_code", RetCode);
signParams.put("err_code_des", RetErrMsg);
signParams.put("sign", Tools.createSign(signParams));
// 回写订单信息
PrintWriter writer = null;
response.setHeader("ContentType", "text/xml");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
try {
writer = response.getWriter();
writer.flush();
writer.print(XMLUtil.parseXML(signParams));
// writer.print(JSONObject.fromObject(signParams).toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
writer.close();
}
3、微信:确认支付,调起微信支付模块,完成支付,回调商户通知接口
这一步不涉及商户
4、商户:接收微信支付成功后的通知信息,根据通知结果进行业务处理
微信支付成功后,会将支付结果通过统一下单接口中的notify_url参数回传通知给商户。商户需要在通知接口中完成签名校验、业务处理、回写处理结果等操作。
输入参数说明如下:
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 |
返回信息 | return_msg | 否 | String(128) | 签名失败
| 返回信息,如非空,为错误原因 签名失败 参数格式校验错误 |
以下字段在return_code为SUCCESS的时候有返回
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | 是 | String(32) | wx8888888888888888 | 微信分配的公众账号ID |
商户号 | mch_id | 是 | String(32) | 1900000109 | 微信支付分配的商户号 |
设备号 | device_info | 否 | String(32) | 013467007045764 | 微信支付分配的终端设备号, |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 签名 |
业务结果 | result_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL |
错误代码 | err_code | 否 | String(32) | SYSTEMERROR | 详细参见第6节错误列表 |
错误代码描述 | err_code_des | 否 | String(128) | 系统错误 | 错误返回的信息描述 |
用户标识 | openid | 是 | String(128) | wxd930ea5d5a258f4f | 用户在商户appid下的唯一标识 |
是否关注公众账号 | is_subscribe | 是 | String(1) | Y | 用户是否关注公众账号,Y-关注,N-未关注,仅在公众账号类型支付有效 |
交易类型 | trade_type | 是 | String(16) | JSAPI | JSAPI、NATIVE、APP |
付款银行 | bank_type | 是 | String(16) | CMC | 银行类型,采用字符串类型的银行标识,银行类型见附表 |
总金额 | total_fee | 是 | Int | 100 | 订单总金额,单位为分 |
货币种类 | fee_type | 否 | String(8) | CNY | 货币类型,符合ISO 4217标准的三位字母代码,默认人民币:CNY |
现金支付金额 | cash_fee | 是 | Int | 100 | 现金支付金额订单现金支付金额 |
现金支付货币类型 | cash_fee_type | 否 | String(16) | CNY | 货币类型,符合ISO 4217标准的三位字母代码,默认人民币:CNY |
代金券或立减优惠金额 | coupon_fee | 否 | Int | 10 | 代金券或立减优惠金额<=订单总金额,订单总金额-代金券或立减优惠金额=现金支付金额 |
代金券或立减优惠使用数量 | coupon_count | 否 | Int | 1 | 代金券或立减优惠使用数量 |
代金券或立减优惠批次ID | coupon_batch_id_$n | 否 | String(20) | 100 | 代金券或立减优惠批次ID ,$n为下标,从1开始编号 |
代金券或立减优惠ID | coupon_id_$n | 否 | String(20) | 10000 | 代金券或立减优惠ID, $n为下标,从1开始编号 |
单个代金券或立减优惠支付金额 | coupon_fee_$n | 否 | Int | 100 | 单个代金券或立减优惠支付金额, $n为下标,从1开始编号 |
微信支付订单号 | transaction_id | 是 | String(32) | 1217752501201407033233368018 | 微信支付订单号 |
商户订单号 | out_trade_no | 是 | String(32) | 1212321211201407033568112322 | 商户系统的订单号,与请求一致。 |
商家数据包 | attach | 否 | String(128) | 123456 | 商家数据包,原样返回 |
支付完成时间 | time_end | 是 | String(14) | 20141030133525 | 支付完成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010 |
sign为签名字段,参与签名的字段包括上面除sign之外的所有非空字段。
贴出源码:
// 1、验证返回通知的合法性
Map<String, String> params = paymentService.getNotifyParamsMap(request);// 解析返回参数
boolean verifyStatus = paymentService.isNotifyLegal(params, null);
if (log.isInfoEnabled()) {
log.info("************WxpayNotifyAction**********Wxpay params Log: " + params.toString());
log.info("************WxpayNotifyAction**********verifyStatus: " + verifyStatus);
}
// 2、根据验证结果进行业务逻辑的操作
String returnCode = WxpayTradeStateEnum.FAIL.getCode();
String returnMsg = WxpayTradeStateEnum.FAIL.getMsg();
if (verifyStatus) {
// *****************************************************
// TODO:业务处理
// *****************************************************
// 向支付平台返回
returnCode = WxpayTradeStateEnum.SUCCESS.getCode();
returnMsg = WxpayTradeStateEnum.SUCCESS.getMsg();
} else {
returnCode = WxpayTradeStateEnum.FAIL.getCode();
returnMsg = "无效签名";
}
// 3、向微信回写处理结果
SortedMap<String, Object> resMap = new TreeMap<String, Object>();
resMap.put("return_code", returnCode);
resMap.put("return_msg", returnMsg);
PrintWriter writer = null;
response.setHeader("ContentType", "text/xml");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
try {
writer = response.getWriter();
writer.flush();
writer.print(XMLUtil.parseXML(resMap));
} catch (IOException e) {
e.printStackTrace();
} finally {
writer.close();
}
OK,可以系统联调了,祝你顺利!