【实战】 对接微信支付分(前端小程序、后端java) 【二】

接上文,本篇主要介绍支付分的支付、支付回调、退款等流程以及在这过程中需要注意的坑。

步骤四:完结支付分订单

用户结束服务,商户根据实际情况调用《完结支付分订单》接口,微信根据《完结支付分订单》接口中传递的扣款金额完成扣款。

这步没什么难度,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对称解密,需要的小伙伴可以私信我。

哈哈,至此,我们的微信支付分对接就完成了。希望大家多多支持,有问题的地方也欢迎各位大神指正。

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值