接上文,本篇主要介绍支付分的支付、支付回调、退款等流程以及在这过程中需要注意的坑。
步骤四:完结支付分订单
用户结束服务,商户根据实际情况调用《完结支付分订单》接口,微信根据《完结支付分订单》接口中传递的扣款金额完成扣款。
这步没什么难度,POST接口,调用微信,我这里说几个注意的点。
1.url中要传入商户侧订单号,即创建订单时的订单号。(这里埋个伏笔先)
2.服务时间段中,服务结束时间必填,且有硬性要求,建议根据业务需求来传递。
调用示例:
//生成签名
String completeUrl = "https://api.mch.weixin.qq.com/v3/payscore/serviceorder/"+ oi.getRefrenceId() +"/complete";
HttpUrl completeHttpUrl = HttpUrl.parse(completeUrl);
String completeToken = WxAPIV3SignUtils.getToken("POST",completeHttpUrl,JSON.toJSONString(completePayScoreIn),payScoreConfig.getMchId(),payScoreConfig.getPrivateKey(),payScoreConfig.getSerialNo());
RequestBody completeRequestBody = RequestBody.create(MediaType_JSON,JSON.toJSONString(completePayScoreIn));
Request completeRequest = new Request.Builder()
.url(completeUrl)
.addHeader("Content-Type","application/json")
.addHeader("Accept","application/json")
.addHeader("Authorization",completeToken)
.post(completeRequestBody)
.build();
Response completeResponse = okHttpClient.newCall(completeRequest).execute();
logger.debug("【完结支付分订单API】接口返回:{},code={},message={},body={}",JSON.toJSONString(completeResponse),completeResponse.code(),completeResponse.message(),completeResponse.body().string());
if (completeResponse.isSuccessful()){//调用成功
}else {//调用失败
}
接收微信支付成功回调通知
接收回调通知的接口,有几个点需要注意。
1.微信通知机制【如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知】,所以在实现逻辑上,注意防重复处理。
2.通知url必须为直接可访问的url,不能携带参数。
先看一下实战下处理逻辑:
public ResponseEntity payScoreCallBack(HttpServletRequest request) throws Exception
{
boolean isOk = true;
String message = null;
String callBackIn = "";//json字符串
String callBackOut = "";
PayOrderIn orderIn = null;
String shopId = (String) request.getAttribute("shopId");
String mchKey = "";//商户APIv3密钥
try {
Date now = DateTime.Now();
ServletInputStream servletInputStream = request.getInputStream();
int contentLength = request.getContentLength();
byte[] callBackInBytes = new byte[contentLength];
servletInputStream.read(callBackInBytes, 0, contentLength);
callBackIn = new String(callBackInBytes, "UTF-8");
logger.debug("【微信支付分免密支付回调】:" + callBackIn);
WxPayScoreNotifyIn notifyIn = JSON.parseObject(callBackIn,WxPayScoreNotifyIn.class);
if(notifyIn == null){
throw new Exception("参数不正确,反序列化失败");
}
if(!"PAYSCORE.USER_PAID".equals(notifyIn.getEvent_type())) {
logger.debug("通知类型非支付成功");
throw new Exception(message);
}
//解密回调信息
byte[] key = mchKey.getBytes();
WxAPIV3AesUtil aesUtil = new WxAPIV3AesUtil(key);
String decryptToString = aesUtil.decryptToString(notifyIn.getResource().getAssociated_data().getBytes(),notifyIn.getResource().getNonce().getBytes(),notifyIn.getResource().getCiphertext());
logger.debug("【支付分支付回调解密结果:】" + decryptToString);
WxPayScoreNotify_Detail wxPayScoreNotify_detail = JSON.parseObject(decryptToString,WxPayScoreNotify_Detail.class);
//todo:处理业务逻辑
}catch (Exception e){
isOk = false;
message = e.getMessage();
LoggerUtils.logDebug(logger, "微信支付回调处理异常,"+e.toString());
}finally {
if(isOk){
}
try{
return new ResponseEntity(HttpStatus.OK);
} catch (Exception e){
//记录日志
LoggerUtils.logDebug(logger, "微信支付回调响应异常,"+e.toString());
return new ResponseEntity(HttpStatus.EXPECTATION_FAILED);
}
}
}
3.对微信返回的密文进行解密,解密算法,篇幅原因,参考我的另一篇文章:
https://blog.csdn.net/w1170384758/article/details/105414860
4.这边微信给我们遗留了一个坑,返回的内容都是密文,解密时要用到商户密钥。如果是单一商户,没问题,因为就一个密钥。如果是SAAS平台,多商户入驻呢,怎么解密?我问了一下对应的腾讯技术,得到的答案是:轮询解密,拿密钥一个一个试,知道成功为止。
看到这个结果的时候,我就呵呵了。
!!!我这里提供一个思路,也是点一下前面埋的伏笔:
我们完全可以采用这种思路,在支付回调url中,拼接上我们需要的信息,我这里放我们的商户id,接收到通知后,截取url,把这个作为参数重新进行路由转发。
先看下单时通知地址的设置:
String notifyUrl = "https://url/{shopId}/payScoreCallBack";
notifyUrl = notifyUrl.replaceAll("\\{shopId}",oi.getShopId());
wxApplyPayScoreIn.setNotify_url(notifyUrl);
路由转发逻辑(spring框架,自定义拦截器):
/**
* 动态拦截请求的所有参数
* <p>
* 通过"op"参数自动路由到具体的控制方法
* </p>
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
String uri = request.getRequestURI();
if (uri.contains("/payScoreCallBack")){//微信支付分回调通知
String process = (String) request.getAttribute("process");
if (!org.springframework.util.StringUtils.isEmpty(process)) return true;//已转发的消息透传
String[] params = uri.split("/");
String shopId = params[params.length - 2];//shopId存储在倒数第二节
//重新拼接url,并将mchId放在参数里
request.setAttribute("shopId",shopId);
request.setAttribute("process", "true");
uri = uri.replaceFirst("/" + shopId,"");
String vPath = request.getContextPath();
if(!org.springframework.util.StringUtils.isEmpty(vPath))
uri = uri.replaceFirst(vPath, "");
request.getRequestDispatcher(uri).forward(request, response);
return false;
}
return true;
}
这样,既不违背微信的接口规范,又能满足传递我们需要的参数。
支付分退款申请
看过官方文档后发现,支付分退款申请与退款回调与其他收款方式(H5支付、小程序支付等)采用同一个API接口,原本以为会很轻松,但是实测下来发现,呵呵了!
先上代码:
//微信申请退款
private boolean wxApplyRefund(ApplyRefundIn refundIn){
Date wxReqDate = new Date();
boolean result = false;
String wxApplyRefundInXml="",wxApplyRefundOutXml="";
try{
//获取商家支付信息
GetPayInfoOut payInfoOut = customerFacade.getPayInfo(new GetPayInfoIn(refundIn.getShopMchId(), refundIn.getPayType()));
//创建请求对象
BigDecimal handred = new BigDecimal(100);
WxApplyRefundIn wxApplyRefundIn = new WxApplyRefundIn(payInfoOut.getAppId(),payInfoOut.getMchId()
, SerialnoUtils.buildUUID(), "MD5", refundIn.getPayOrderId()/*, refundIn.getOrderId()*/, refundIn.getRefundId()
, refundIn.getOrderAmount().multiply(handred).setScale(2, RoundingMode.HALF_EVEN).intValue()
, refundIn.getRefundAmount().multiply(handred).setScale(2, RoundingMode.HALF_EVEN).intValue()
, "CNY", "order error", SystemConst.WEIXIN_NOTIFY_URL + "/refund");//refundIn.getRefundDesc()
wxApplyRefundIn.setSign(MapUtils.getObjectMD5Sign(wxApplyRefundIn, payInfoOut.getMchKey()));
//调用接口
wxApplyRefundInXml = XmlUtils.beanToXml(wxApplyRefundIn);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
FileInputStream instream = new FileInputStream(new File(SystemConst.WX_Cert_URL+payInfoOut.getCertFile()));
try {
keyStore.load(instream,payInfoOut.getMchId().toCharArray());
}finally {
instream.close();
}
CloseableHttpClient httpclient = null;
CloseableHttpResponse response = null;
try{
// Trust own CA and all self-signed certs
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore
, payInfoOut.getMchId().toCharArray()).build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslcsf = new SSLConnectionSocketFactory(
sslcontext, new String[] { "TLSv1" }, null,
SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
httpclient = HttpClients.custom().setSSLSocketFactory(sslcsf).build();
HttpPost httpPost = new HttpPost(SystemConst.WX_Refund_URL);
httpPost.setEntity(new StringEntity(wxApplyRefundInXml));
response = httpclient.execute(httpPost);
int contentLength = (int) response.getEntity().getContentLength();
byte[] wxApplyPayOutBytes = new byte[contentLength];
InputStream outStream = response.getEntity().getContent();
outStream.read(wxApplyPayOutBytes, 0, contentLength);
wxApplyRefundOutXml = new String(wxApplyPayOutBytes, "UTF-8");
}finally {
response.close();
httpclient.close();
}
WxApplyRefundOut wxApplyRefundOut = XmlUtils.xmlContentToBean(wxApplyRefundOutXml, WxApplyRefundOut.class);
if (wxApplyRefundOut == null) {
throw new Exception("微信支付响应异常");
}
if ("FAIL".equals(wxApplyRefundOut.getReturn_code())) {
throw new Exception(wxApplyRefundOut.getReturn_msg());
}
//验证签名
/*String sign = MapUtils.getObjectMD5Sign(wxApplyRefundOut, payInfoOut.getMchKey());
if (!sign.equals(wxApplyRefundOut.getSign())) {
throw new BusinessException("验证签名失败");
}*/
if ("FAIL".equals(wxApplyRefundOut.getResult_code())) {
throw new Exception("err_code:" + wxApplyRefundOut.getErr_code() + ",err_code_des:" + wxApplyRefundOut.getErr_code_des());
}
result = true;
}catch (Exception e){
refundIn.setErrorMsg(e.getMessage());
logger.error("微信退款申请异常", e);
}finally {
//记录退款请求日志
logger.debug("微信退款申请,请求("+DateUtils.formatDateTime(wxReqDate)+"):"+wxApplyRefundInXml
+"响应("+DateUtils.formatDateTime(new Date())+"):"+wxApplyRefundOutXml);
}
return result;
}
下面来说说坑点:
1.先看看微信支付成功后,普通用户和微信商户后台是怎么显示这笔订单的。
没错,你没看错,微信支付分的,商户侧订单号,他变了,都是字母了,哈哈,再看看微信官方给出的回复。
它不支持,哈哈,笑死我了,微信的商户后台不支持微信的产品,反正我是觉的真的恶心。
!!!我们开发的时候要注意的点就出来了
支付分退款时,必须使用transaction_id,且不能使用out_trade_no,也不要传递out_trade_no,因为会报错,说订单不存在。退款回调通知中,也是同理的,注意兼容处理。(估计微信后续会更新的)
再来看一下退款结果回调通知的处理逻辑:
/**
* 微信退款回调
*
* @param request
* @param response
* @return
* @throws Exception
*/
public void refund(HttpServletRequest request, HttpServletResponse response) throws Exception {
boolean isOk = false;
String message = null;
String reqXml = "";String resXml = "";
PayOrderIn orderIn = null;
WxRefundNotifyIn reqParam = null;
WxRefundNotifyReqInfo reqInfo = null;
try {
//解析报文
ServletInputStream servletInputStream = request.getInputStream();
int contentLength = request.getContentLength();
byte[] callBackInBytes = new byte[contentLength];
servletInputStream.read(callBackInBytes, 0, contentLength);
reqXml = new String(callBackInBytes, "UTF-8");
logger.info("【微信退款回调开始】请求报文:" + reqXml);
//记录回调日志
//xml转object
reqParam = XmlUtils.xmlContentToBean(reqXml, WxRefundNotifyIn.class);
if (reqParam == null) {
logger.error("回调参数反序列化失败");
return;
}
if (WXPayConstants.FAIL.equals(reqParam.getReturn_code())) {
message = reqParam.getReturn_msg();
logger.error(message);
return;
}
PayConfig payConfig = null;
if (StringUtils.isNotBlank(reqParam.getMch_id())) { //订单有收款信息,获取商户收款账户信息
GetPayInfoIn payInfoIn = new GetPayInfoIn(reqParam.getMch_id(),PayTypeEnum.wx_payscore.getVal());
GetPayInfoOut payInfoOut = customerFacade.getPayInfo(payInfoIn);
}
if (payConfig == null) {
WxConfig config = SpringUtils.getBean(WxConfig.class);
payConfig = new PayConfig(config.getAppID(), config.getMchID(),config.getKey());
}
//aes解密
String reqInfoXml = AESUtils.decryptData(reqParam.getReq_info(), payConfig.getApiSecret());
reqInfo = XmlUtils.xmlContentToBean(reqInfoXml, WxRefundNotifyReqInfo.class);
if (reqInfo == null) {
logger.error("回调参数“reqInfo”反序列化失败");
return;
}
logger.debug("【微信退款回调,解密后参数:】{}",JSON.toJSONString(reqInfo));
//boolean isValid = WXPayUtil.isSignatureValid(reqXml, payConfig.getApiSecret());//验证签名
//if (!isValid) {
// logger.error("签名验证失败");
// return;
//}
if (!WXPayConstants.SUCCESS.equals(reqInfo.getRefund_status())) {
logger.error("退款失败");
return;
}
isOk = true;
logger.debug("微信退款回调成功");
} catch (Exception e) {
isOk = false;
message = e.getMessage();
logger.info("微信退款回调处理异常:", e);
} finally {
try{
//处理业务逻辑
}catch (Exception e){
logger.error("微信退款回调逻辑处理异常", e);
}
try {
WxRefundNotifyOut resParam = new WxRefundNotifyOut();
if (isOk) {
resParam.setReturn_code(WXPayConstants.SUCCESS);
} else {
resParam.setReturn_code(WXPayConstants.FAIL);
resParam.setReturn_msg(message);
}
resXml = XmlUtils.beanToXml(resParam);
byte[] resBytes = resXml.getBytes(Charset.forName("UTF-8"));
ServletOutputStream servletOutputStream = response.getOutputStream();
servletOutputStream.write(resBytes);
} catch (Exception e) {
logger.error("微信退款回调应答异常,", e);
}
logger.info("【微信退款回调结束】应答报文:" + resXml);
}
}
相信这块对于对接过微信的小伙伴来说,没有什么难度了,里面有两个工具类:xml转Javabean和AES对称解密,需要的小伙伴可以私信我。
哈哈,至此,我们的微信支付分对接就完成了。希望大家多多支持,有问题的地方也欢迎各位大神指正。