最近项目涉及到微信支付的功能,在这里简单分享下整体的开发流程,这里要介绍的是JSAPI支付。
JSAPI网页支付,即日常所说的公众号支付,可在微信公众号、朋友圈、聊天会话中点击页面链接,或者用微信“扫一扫”扫描页面地址二维码在微信中打开商户HTML5页面,在页面内下单完成支付。
上述介绍可以简单理解为,jsapi支付必须在微信浏览器中进行。
微信支付支持多种接入模式,常用的就是直连模式及服务商模式。
直连模式是指商户自行开发系统来对接微信支付进行交易,微信支付将资金直接结算到商户的结算账户,商户给用户提供支付服务。该模式要求商户具备系统开发能力,商户可自行前往商户平台完成入驻。
服务商模式是指针对市面上一些中小型且没有开发能力的商户,由已在微信支付官方注册入驻的系统开发商或解决方案提供商协助这些商户完成入驻,开发及日常运营工作的模式。服务商可前往 服务商平台完成注册入驻。
根据实际项目情况,来选择接入模式。2种模式的api大同小异,其中服务商模式下支持多个子商户的入驻,而且支持点金计划。
以下介绍的普通商户(直连模式)下微信支付的开发流程。
1.申请公众号appId,并配置对应的网页授权域名及ip白名单等
2.申请mchid,入驻普通商户,并开通JSAPI支付。
3.绑定APPID及mchid
4.配置API key以及api证书
5.设置支付目录
商户最后请求拉起微信支付收银台的页面地址我们称之为“支付目录”。
如果支付授权目录设置为顶级域名,那么只校验顶级域名,不校验后缀;如果支付授权目录设置为多级目录,就会进行全匹配。
准备工作完成后,可以愉快的开发接口了。这里要说明的是,微信支付api目前有V2和V3两个版本,都长期有效,签名方式和参数格式可能略有不同,V3还需要商户平台证书(区别于Api证书),但是大体的开发思路以及关键参数都是一致的。
(a)微信下单支付的基本流程是,业务系统后台进行下单,生成微信支付订单(预支付),并将接口返回的相应参数(prepay_id)返回给前台,前台调起微信支付页面,输入密码支付后,微信会将支付结果异步通知给业务系统,系统进行相应业务处理。
(b)微信退款的流程是,业务系统发起退款申请,之后微信会将退款结果异步通知给业务系统,业务系统做相应处理。
注:后台发起下单申请或退款申请后,并不意味付款或退款成功,需要根据异步通知去判断,而且通知可能发起多次,需要业务系统正确处理通知,以下是官方建议。
当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
考虑到多次异步通知处理后仍会出现订单状态未及时更新的情况,可以利用定时任务定期查询订单状态,并更新业务系统中的订单状态。
WxPayApiConfig wxPayApiConfig = WxPayApiConfigKit.getWxPayApiConfig();
Map<String, String> params = UnifiedOrderModel
.builder()
.appid(wxPayApiConfig.getAppId())
.mch_id(wxPayApiConfig.getMchId())
.nonce_str(WxPayKit.generateStr())
.body("公众号支付")
.attach("hello world")
.out_trade_no(WxPayKit.generateStr())
.total_fee("1000")
.spbill_create_ip(ip)
.notify_url(notifyUrl)
.trade_type(TradeType.JSAPI.getTradeType())
.openid(openId)
.build()
.createSign(wxPayApiConfig.getPartnerKey(), SignType.HMACSHA256);
String xmlResult = WxPayApi.pushOrder(false, params);
log.info(xmlResult);
Map<String, String> resultMap = WxPayKit.xmlToMap(xmlResult);
String returnCode = resultMap.get("return_code");
String returnMsg = resultMap.get("return_msg");
if (!WxPayKit.codeIsOk(returnCode)) {
return new AjaxResult().addError(returnMsg);
}
String resultCode = resultMap.get("result_code");
if (!WxPayKit.codeIsOk(resultCode)) {
return new AjaxResult().addError(returnMsg);
}
// 以下字段在 return_code 和 result_code 都为 SUCCESS 的时候有返回
String prepayId = resultMap.get("prepay_id");
Map<String, String> packageParams = WxPayKit.prepayIdCreateSign(prepayId, wxPayApiConfig.getAppId(),
wxPayApiConfig.getPartnerKey(), SignType.MD5);
String jsonStr = JSON.toJSONString(packageParams);
return new AjaxResult().success(jsonStr);
微信下单返回的信息中有一项关键参数,prepay_id,需要返回给前端。
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":"wx2421b1c4370ec43b", //公众号ID,由商户传入
"timeStamp":"1395712654", //时间戳,自1970年以来的秒数
"nonceStr":"e61463f8efa94090b1f366cccfbbb444", //随机串
"package":"prepay_id=u802345jgfjsdfgsdg888",
"signType":"MD5", //微信签名方式:
"paySign":"70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名
},
function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ){
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
}
});
}
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}
发起请求时,package参数的统一格式prepay_id=***,就是统一下单接口返回的prepay_id参数值,就是上文提到的返回的关键参数。考虑签名算法比较麻烦,前端调取js支付所需的所有参数可以统一由后端返回。
@RequestMapping(value = "/payNotify", method = {RequestMethod.POST, RequestMethod.GET})
public String payNotify(HttpServletRequest request) {
String xmlMsg = HttpKit.readData(request);
log.info("支付通知=" + xmlMsg);
Map params = WxPayKit.xmlToMap(xmlMsg);
String returnCode = params.get("return_code");
// 注意重复通知的情况,同一订单号可能收到多次通知,请注意一定先判断订单状态
// 注意此处签名方式需与统一下单的签名类型一致
if (WxPayKit.verifyNotify(params, WxPayApiConfigKit.getWxPayApiConfig().getPartnerKey(), SignType.HMACSHA256)) {
if (WxPayKit.codeIsOk(returnCode)) {
// 更新订单信息(系统的具体业务处理)
// 发送通知等
Map xml = new HashMap(2);
xml.put("return_code", "SUCCESS");
xml.put("return_msg", "OK");
return WxPayKit.toXml(xml);
}
}
return null;
}
通知接口的地址是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。接收到通知后,对订单信息(状态)进行更新。
商户处理后需要返回给微信参数,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止。
@RequestMapping(value = "/refund", method = {RequestMethod.POST, RequestMethod.GET})
public String refund(@RequestParam(value = "transactionId", required = false) String transactionId,
@RequestParam(value = "outTradeNo", required = false) String outTradeNo) {
try {
log.info("transactionId: {} outTradeNo:{}", transactionId, outTradeNo);
if (StrKit.isBlank(outTradeNo) && StrKit.isBlank(transactionId)) {
return "transactionId、out_trade_no二选一";
}
WxPayApiConfig wxPayApiConfig = WxPayApiConfigKit.getWxPayApiConfig();
Map params = RefundModel.builder()
.appid(wxPayApiConfig.getAppId())
.mch_id(wxPayApiConfig.getMchId())
.nonce_str(WxPayKit.generateStr())
.transaction_id(transactionId)
.out_trade_no(outTradeNo)
.out_refund_no(WxPayKit.generateStr())
.total_fee("1")
.refund_fee("1")
.notify_url(refundNotifyUrl)
.build()
.createSign(wxPayApiConfig.getPartnerKey(), SignType.MD5);
String refundStr = WxPayApi.orderRefund(false, params, wxPayApiConfig.getCertPath(), wxPayApiConfig.getMchId());
log.info("refundStr: {}", refundStr);
return refundStr;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
退款时需要api证书,证书应放在有访问权限控制的目录中,防止被他人下载。
@RequestMapping(value = "/refundNotify", method = {RequestMethod.POST, RequestMethod.GET})
public String refundNotify(HttpServletRequest request) {
String xmlMsg = HttpKit.readData(request);
log.info("退款通知=" + xmlMsg);
Map params = WxPayKit.xmlToMap(xmlMsg);
String returnCode = params.get("return_code");
// 注意重复通知的情况,同一订单号可能收到多次通知,请注意一定先判断订单状态
if (WxPayKit.codeIsOk(returnCode)) {
String reqInfo = params.get("req_info");
String decryptData = WxPayKit.decryptData(reqInfo, WxPayApiConfigKit.getWxPayApiConfig().getPartnerKey());
log.info("退款通知解密后的数据=" + decryptData);
// 更新订单信息
// 发送通知等
Map xml = new HashMap(2);
xml.put("return_code", "SUCCESS");
xml.put("return_msg", "OK");
return WxPayKit.toXml(xml);
}
return null;
}
退款结果的异步通知与支付异步通知处理方式类似,这里不再赘述。
这里只是介绍了微信JSAPI支付的大体开发流程,实际开发中还需要考虑避免重复下单,重复退款,业务系统订单的状态与实际付款(退款)状态是否一致等情况;在对业务数据进行状态检查和处理时,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱,以及事务的控制等。
具体的api实现可参照微信支付的官方文档和官方SDK及Demo,微信开放社区也有相应的代码实践供使用。
相关链接:
[1]微信支付官方文档
[2]:官方sdk及DEMO
[3]: 微信开放社区优秀实践源码