合单支付
合单支付是指可以在一个订单中包含多个商家的多个商品,一次性支付。
-
关于JSAPI、APP、小程序等支付类型的区别:https://pay.weixin.qq.com/wiki/doc/api/sl.html
我们是在小程序中嵌入支付功能的,最开始用的是APP支付一直提示“支付场景非法”,换成JSAPI支付后才成功调出支付确认框并成功支付。
-
如果是公众号支付、扫码支付和H5支付还需要配置支付域名或回调链接,请参考:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3
-
在申请开通电商收付通时如果选择的是平台代扣手续费,那么建议在正式支付分账前向手续费账户充值至少100块钱,否则可能会导致分账失败;
-
回调通知地址必须为域名并且为https协议,微信会以POST方式访问URL,以json格式通过body携带数据;
预支付
此处的测试方法只设置了一个子单,即只在一家店铺下了一个商品的订单;
/**
* 预支付
*/
@Test
public void prepay() {
JSONObject reqJsonObject = new JSONObject();
//合单商户appid
reqJsonObject.put("combine_appid", WxPayConfig.merchantAppId);
//合单商户
reqJsonObject.put("combine_mchid", WxPayConfig.merchantId);
//合单商户订单号
reqJsonObject.put("combine_out_trade_no", "202005260952_test001");
//回调地址
reqJsonObject.put("notify_url", WxPayConfig.callback_notify_pay_address);
//支付者
JSONObject combine_payer_info = new JSONObject();
combine_payer_info.put("openid", "oaKTs4vq93MDDmCFKQO123456789");
reqJsonObject.put("combine_payer_info", combine_payer_info);
//子单信息数组
JSONArray sub_orders = new JSONArray();
//子单信息
JSONObject sub_order = new JSONObject();
//子单商户号,必须与发起方appid有绑定关系
sub_order.put("mchid", WxPayConfig.merchantId);
//附加数据,在查询API和支付通知中原样返回
sub_order.put("attach", "订单号为202005260952_test001");
//子单商户订单号
sub_order.put("out_trade_no", "202005260952_test001");
//二级商户号
sub_order.put("sub_mchid", WxPayConfig.testMerchantId);
//商品描述
sub_order.put("description", "高端茶叶500g装");
//子单信息-订单金额
JSONObject amount = new JSONObject();
//标价金额,子单金额,单位为分
amount.put("total_amount", 100);
//标价币种
amount.put("currency", WxPayConfig.currency_cny);
sub_order.put("amount", amount);
//子单信息-结算信息
JSONObject settle_info = new JSONObject();
//是否指定分账,true:是,false:否
settle_info.put("profit_sharing", true);
sub_order.put("settle_info", settle_info);
sub_orders.set(0, sub_order);
reqJsonObject.put("sub_orders", sub_orders);
System.out.println("请求的body:" + reqJsonObject + "\n\n");
String headerToken = WxPayUtils.getHeaderAuthorization("POST",
HttpUrl.parse(WxPayConfig.transactions_prepay_app_url), reqJsonObject.toJSONString());
System.out.println("请求接口携带的 header: \n" + headerToken + "\n\n");
Map<String, String> headersMap = new HashMap<>();
headersMap.put("User-Agent", WxPayConfig.userAgent);
headersMap.put("Accept", "application/json");
headersMap.put("Authorization", headerToken);
headersMap.put("Wechatpay-Serial", WxPayConfig.api_v3_cert_serial_no);
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqJsonObject.toJSONString());
ResponseAndStatusAndHeaders response = ClientUtil.getAndPostJson("POST", WxPayConfig.transactions_prepay_app_url, requestBody, headersMap);
if (response.getStatus() != Status.SUCCESS || response.getResponseData() == null) {
LogUtil.printErrorLog(LogUtil.log_front_wxpay + "合单下单接口请求错误,返回信息为:\n" + response.toString() + "\n请求的参数为:\n" + reqJsonObject + "\n\n");
return;
}
System.out.println(response);
JSONObject jsonObject = JSON.parseObject(response.getResponseData().toString());
String prepay_id = jsonObject.get("prepay_id").toString();
System.out.println("合单下单成功,预支付id为:" + prepay_id);
//{"prepay_id":"up_wx26112728792815875842ded51824410800"}
}
调起支付
后端处理好数据获得到预支付id后,需要将各种支付参数进行签名计算后的值返回给前端,由小程序方调起支付框(也就是输入支付密码的界面)。
/**
* 继续支付
* @param requestJsonStr 接受的参数
* @return 操作结果
*/
@PostMapping("/continuePay")
public ResultBO continuePay(@RequestBody String requestJsonStr) {
JSONObject requestJson = JSONObject.parseObject(requestJsonStr);
String orderNo = requestJson.getString("orderNo");
//是否为有效订单
MallOrderDO mallOrder = mallOrderService.getByOrderNo(orderNo);
if (mallOrder == null) {
return Results.result(HttpStatusEnum.PARAMTER_ERROE, "订单不存在");
}
if (mallOrder.getOrderStatus() != 1 || StrUtil.isBlank(mallOrder.getWxPrepayId())) {
return Results.result(HttpStatusEnum.PARAMTER_ERROE, "该订单已经支付过了,请勿重复支付");
}
JSONObject payObject = WxPayUtils.getPayObject(mallOrder.getWxPrepayId());
JSONObject data = new JSONObject();
data.put("payObject", payObject);
return Results.success(HttpStatusEnum.SUCCESS, data);
}
支付回调通知
- 微信回调通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m
- 回调处理必须校验签名,避免出现恶意调用攻击。
- 使用APIv3密钥对回调数据中的ciphertext进行AEAD_AES_256_GCM算法解密,得到原文。
- 回调处理必须检查订单状态,避免重复处理。
- 处理回调通知后做好日志记录,当回调正常接受返回的HTTP状态码为200或204,当回调处理失败返回的HTTP状态码应为500。
/**
* 微信支付回调通知
*/
@PostMapping("payCallBack")
public JSONObject callBackNotify(@RequestBody String requestJsonStr, @RequestHeader HttpHeaders headers, HttpServletResponse response) {
LogUtil.printErrorLog(LogUtil.log_front_wxpay + "微信支付结果通知接受成功,header为:" + headers + "\n body为:" + requestJsonStr);
//返回通知的应答报文,code(32):SUCCESS为清算机构接收成功;message(64):错误原因
JSONObject responseJson = new JSONObject();
responseJson.put("code", "FAIL");
//支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
if (!headers.containsKey("Wechatpay-Serial") || !headers.containsKey("Wechatpay-Timestamp")
|| !headers.containsKey("Wechatpay-Nonce") || !headers.containsKey("Wechatpay-Signature")) {
LogUtil.printErrorLog(LogUtil.log_front_wxpay + "回调请求header缺失");
responseJson.put("message", "回调请求header缺失");
return responseJson;
}
String WechatpaySerial = headers.getFirst("Wechatpay-Serial");//平台证书序列号
String WechatpayTimestamp = headers.getFirst("Wechatpay-Timestamp");//应答时间戳
String WechatpayNonce = headers.getFirst("Wechatpay-Nonce");//应答随机串
String WechatpaySignature = headers.getFirst("Wechatpay-Signature"); //应答签名
if (!WechatpaySerial.equals(WxPayConfig.api_v3_cert_serial_no)) {
LogUtil.printErrorLog(LogUtil.log_front_wxpay + "回调请求证书序列化不一致");
responseJson.put("message", "回调请求证书序列化不一致");
return responseJson;
}
//获取签名串,验签
String srcData = WechatpayTimestamp + "\n" + WechatpayNonce + "\n" + requestJsonStr + "\n";
if (!WxPayUtils.verify(srcData, WechatpaySignature)) {
LogUtil.printErrorLog(LogUtil.log_front_wxpay + "验签失败");
responseJson.put("message", "验签失败");
return responseJson;
}
JSONObject requestJson = JSONObject.parseObject(requestJsonStr);
//通知的类型,支付成功通知的类型为 TRANSACTION.SUCCESS
String event_type = requestJson.get("event_type").toString();
//通知数据
JSONObject resource = JSONObject.parseObject(requestJson.get("resource").toString());
//数据密文,Base64编码后的开启/停用结果数据密文
String ciphertext = resource.get("ciphertext").toString();
String associated_data = resource.get("associated_data").toString();
String nonce = resource.get("nonce").toString();
//对密文串进行解密
String verify = WxPayUtils.getNotifyData(associated_data, nonce, ciphertext);
JSONObject paySuccess = JSONObject.parseObject(verify);
JSONArray sub_orders = JSON.parseArray(paySuccess.get("sub_orders").toString());
JSONObject sub_order = JSONObject.parseObject(sub_orders.get(0).toString());
//微信子单订单号
String wxOrderNo = sub_order.get("transaction_id").toString();
if (StrUtil.isBlank(wxOrderNo)) {
responseJson.put("message", "微信支付成功,未接受到微信订单号");
return responseJson;
}
//合单商户订单号
String orderNo = paySuccess.get("combine_out_trade_no").toString();
if (StrUtil.isBlank(orderNo)) {
responseJson.put("message", "微信支付成功,未接受到合单商户订单号");
return responseJson;
}
MallOrderDO mallOrder = mallOrderService.getByOrderNo(orderNo);
if (mallOrder == null) {
responseJson.put("message", "操作失败,未查询到合单商户订单号");
return responseJson;
}
//支付失败
if (!event_type.equals("TRANSACTION.SUCCESS")) {
boolean result = mallOrderService.payFail(orderNo, wxOrderNo);
if (result) {
LogUtil.printErrorLog(LogUtil.log_front_wxpay + "支付结果通知:支付失败,处理成功。订单号为:" + orderNo);
response.setStatus(HttpServletResponse.SC_OK);
responseJson.put("code", "SUCCESS");
responseJson.put("message", "微信支付失败,商户处理成功");
} else {
LogUtil.printErrorLog(LogUtil.log_front_wxpay + "支付结果通知:支付失败,处理失败。订单号为:" + orderNo);
responseJson.put("message", "微信支付失败,商户处理失败");
}
return responseJson;
}
//支付成功
boolean result = mallOrderService.paySuccess(orderNo, wxOrderNo);
if (result) {
LogUtil.printErrorLog(LogUtil.log_front_wxpay + "支付结果通知:支付成功,处理成功。订单号为:" + orderNo);
response.setStatus(HttpServletResponse.SC_OK);
responseJson.put("code", "SUCCESS");
responseJson.put("message", "微信支付成功,商户处理成功");
} else {
LogUtil.printErrorLog(LogUtil.log_front_wxpay + "支付结果通知:支付成功,处理失败。订单号为:" + orderNo);
responseJson.put("message", "微信支付成功,商户处理失败");
}
return responseJson;
}
查询合单订单
- 我们系统设定用户下订单后一定时间内(24h)未付款会自动取消订单,为防止状态不同步,在正式取消订单前调用合单查询接口,确认最终支付状态。
- 当超时(4h)未接受到支付结果通知时,手动调用查询接口确认最终支付状态。
/**
* 合单查询订单
* @param orderNo 订单号
* @return 支付是否成功
*/
@Override
public Map<String, String> queryOrder(String orderNo) {
Map<String, String> resultMap = new HashMap<>();
Map<String, String> headersMap = WxPayUtils.getHeaderMap("GET", WxPayConfig.transactions_order_query_url + orderNo, "");
ResponseAndStatusAndHeaders response = ClientUtil.getAndPostJson("GET", WxPayConfig.transactions_order_query_url + orderNo, null, headersMap);
if (response.getStatus() != Status.SUCCESS || response.getResponseData() == null) {
LogUtil.printErrorLog(LogUtil.log_front_wxpay + "合单查询订单接口请求错误,返回信息为:\n" + response.toString() + ",请求的参数:\n" + orderNo);
return null;
}
JSONObject jsonObject = JSON.parseObject(response.getResponseData().toString());
/*{
"combine_appid": "wx4fcb17899329a2a1",
"combine_mchid": "1586786671",
"combine_out_trade_no": "20200528807139",
"combine_payer_info": {
"openid": "oaKTs4vq93MDDmCFKQO123456789"
},
"scene_info": {
"device_id": ""
},
"sub_orders": [{
"amount": {
"payer_amount": 100,
"payer_currency": "CNY",
"total_amount": 100
},
"attach": "商户平台订单号为20200528807139",
"bank_type": "CFT",
"mchid": "1586786671",
"out_trade_no": "20200528807139",
"sub_mchid": "1596741381",
"success_time": "2020-05-28T22:11:13+08:00",
"trade_state": "SUCCESS",
"trade_type": "JSAPI",
"transaction_id": "4325300104202005287616306099"
}]
}*/
JSONArray subOrders = JSON.parseArray(jsonObject.get("sub_orders").toString());
JSONObject subOrder = JSON.parseObject(subOrders.get(0).toString());
//交易状态,SUCCESS:支付成功
//REFUND:转入退款
//NOTPAY:未支付
//CLOSED:已关闭
//USERPAYING:用户支付中
//PAYERROR:支付失败(其他原因,如银行返回失败)
String trade_state = subOrder.getString("trade_state");
String transaction_id = subOrder.getString("transaction_id");
resultMap.put("trade_state", trade_state);
resultMap.put("transaction_id", transaction_id);
return resultMap;
}
参考链接
微信电商收付通支付接口文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/combine/chapter3_1.shtml
微信API-v3回调报文解密文档:https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/zheng-shu-he-hui-tiao-bao-wen-jie-mi