Java实现Native微信支付 超完整流程!!!

前言:最近公司准备在平板“***App”订单页面新建收款页完成收款功能,实现 微信扫码、支付宝扫码、网银扫码 收款功能。

  1. 项目环境介绍
  2. 微信支付文档
    2.1 业务流程说明
  3. 前期准备
    3.1 微信公众账号如何获取?
    3.2 商户号如何获取?
    3.3 API密钥如何获取?
  4. 后端接口编写
    4.1 依赖和工具类
    4.2 生成二维码链接接口
    4.3 查询订单支付状态接口
    4.4 关闭交易订单接口
    4.5 统一下单支付成功之后回调通知接口
    总结

1.项目环境介绍

jdk 1.8

postgresql(远程)

maven 3.6.2

微信商城java代码下载 java 微信支付 实现步骤_java


备注(该文章只展示后端实现流程)

2. 微信支付文档

官方文档:https://pay.weixin.qq.com/wiki/doc/api/index.html 选择Native支付

微信商城java代码下载 java 微信支付 实现步骤_前端_02

2.1 业务流程说明

1.根据公司系统订单,新建收款,触发后端接口“生成微信支付二维码”,后端返回二维码链接code_url给前端,前端转成二维码,用户通过微信扫码支付。
2.通过接口生成支付链接后,触发后端接口“查询订单支付状态”该接口在指定的一个时间内重复校验,在规定时间内状态都为未支付状态,则系统判定支付失效,给前端返回"二维码失效",触发后端“关闭订单接口”。
3.二维码展示给客户,客户支付完成之后完成业务逻辑,修改订单状态,生成订单信息等等。调“支付成功回调接口”

3. 前期准备

3.1 获取微信公众账号

公众账号ID是什么:微信支付分配的公众账号ID(公众号appid)
申请地址:https://mp.weixin.qq.com/ 该地址申请公众号,完成之后会分配公众号appId.公众号可关联

3.2 商户号如何获取?

商户号是什么:微信支付分配的商户号,关联公众号
申请地址:https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal

3.3 API密钥如何获取?

获取地址:https://mp.weixin.qq.com/ 该地址申请公众号,完成之后会分配AppSecret

微信商城java代码下载 java 微信支付 实现步骤_xml_03

4. 后端接口编写

4.1 依赖和工具类

在pom 文件引入微信支付 SDK 依赖

<!-- 微信支付 SDK -->
        <dependency>
            <groupId>com.github.wxpay</groupId>
            <artifactId>wxpay-sdk</artifactId>
             <version>0.0.3</version>
        </dependency>

自定义微信支付工具类

import org.apache.commons.lang.StringUtils;

/**
 * 自定义微信支付工具类
 *
 * @author: fuqin
 * @date: 2022/1/7
 */
public class CommonUtil {
    /**
     * 发送 http 请求
     *
     * @param requestUrl    请求路径
     * @param requestMethod 请求方式(GET/POST/PUT/DELETE/...)
     * @param outputStr     请求参数体
     * @return 结果信息
     */
    public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
        try {
            URL url = new URL(requestUrl);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 设置请求方式(GET/POST)
            conn.setRequestMethod(requestMethod);
            conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
            // 当outputStr不为null时向输出流写数据
            if (null != outputStr) {
                OutputStream outputStream = conn.getOutputStream();
                // 注意编码格式
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }
            // 从输入流读取返回内容
            InputStream inputStream = conn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str = null;
            StringBuffer buffer = new StringBuffer();
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            // 释放资源
            bufferedReader.close();
            inputStreamReader.close();
            inputStream.close();
            inputStream = null;
            conn.disconnect();
            return buffer.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取ip
     *
     * @param request 请求
     * @return ip 地址
     */
    public static String getIp(HttpServletRequest request) {
        if (request == null) {
            return "";
        }
        String ip = request.getHeader("X-Requested-For");
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

4.2 生成二维码链接接口

/**
     * 生成微信支付二维码  
     *
     * @param product_id 订单号//商品ID,系统内部订单id
     * @param totalFee   金额(分)
     */
    @RequestMapping(value = "/createNative",method = RequestMethod.GET)
    public Map<String, String> createNative(@RequestParam("product_id") String product_id,
                                            @RequestParam("totalFee") String totalFee,
                                            HttpServletRequest request, HttpServletResponse response) {
        try {
            //todo 创建请求参数
            SortedMap<String, String> req = new TreeMap<String, String>();
            req.put("appid", "***"); //公众号appid
            req.put("mch_id", "***");  // 商户号
            req.put("nonce_str", WXPayUtil.generateNonceStr()); // 32位随机字符串
            req.put("body", "***"); // 商品描述
            String out_trade_no = Randoms.uuid();
            req.put("out_trade_no", out_trade_no);   // 商户订单号
            req.put("product_id",product_id);//商品id,系统内部订单号
            req.put("total_fee", df.format(Double.parseDouble(totalFee) * 100));    // 标价金额(元)
            req.put("spbill_create_ip", CommonUtil.getIp(request));   // 终端IP
            req.put("notify_url", "https://**/mtzx/WeiXinPay/pay/notify");  // 回调地址
            req.put("trade_type", "NATIVE");    // 交易类型
            req.put("sign", WXPayUtil.generateSignature(req, "APPSecret", WXPayConstants.SignType.MD5));  // 签名
            // 生成要发送的 xml
            String xmlBody = WXPayUtil.generateSignedXml(req, "APPSecret");
             logger.info("微信支付预下单请求参数xmlBody={}", WXPayUtil.xmlToMap(xmlBody));
          
            //发送 POST 请求 统一下单 API 并携带 xmlBody 内容,然后获得返回结果
            String result = CommonUtil.httpsRequest("https://api.mch.weixin.qq.com/pay/unifiedorder", "POST", xmlBody);
            logger.info("微信支付预下单请求结果result={}", WXPayUtil.xmlToMap(result));
            //将返回结果从 xml 格式转换为 map 格式
            Map<String, String> resultMap = WXPayUtil.xmlToMap(result);
            Map<String, String> map = new HashMap<>();
            String result_code = resultMap.get("result_code");
            if (result_code.equals("FAIL")){
                map.put("err_code_des",resultMap.get("err_code_des"));
                map.put("status","201");
                return map;
            }
            map.put("code_url", resultMap.get("code_url")); // 支付地址
            map.put("total_fee", totalFee); // 总金额
            map.put("out_trade_no", out_trade_no);// 随机生成商户订单号
            map.put("status","200");
            return map;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

备注:在pom文件引入了微信支付 SDK 就有WXPayUtil工具类,支付地址返回给前端,或者后端转二维码,此文章未写。

4.3 查询订单支付状态接口

/**
     * 查询订单支付状态
     * @author: fuqin
     * @date: 2022/1/7
     * @param outTradeNo 订单号
     * @return 支付状态
     */
    @RequestMapping(value = "/queryOrder", method = RequestMethod.GET)
    public JsonResult queryOrder(@RequestParam("outTradeNo") String outTradeNo) {
        int x = 0;
        while (true) {
            // 调用查询微信支付订单状态方法
            Map<String, String> map = this.queryPayStatus(outTradeNo);
            if (map.isEmpty()) {
                return JsonResult.error("支付出错");
            }
            if (map.get("trade_state").equals("SUCCESS")) {
                return JsonResult.ok("支付成功");
            }
            if (map.get("trade_state").equals("CLOSED")) {
                return JsonResult.ok("订单已关闭");
            }
            try {
                // 睡3秒
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 自定义时间,这期间未支付则提示二维码超时,前端在触发关闭订单接口
            x++;
            if (x >= 20) {
                return JsonResult.error("二维码超时!");
            }
        }
    }

Controller层中 queryPayStatus(String outTradeNo)方法如下:

/**
     * 查询微信支付订单状态
     * @author: fuqin
     * @date: 2022/1/7
     * @param outTradeNo 订单号
     * @return 支付状态
     */
    private Map<String, String> queryPayStatus(String outTradeNo) {
        try {
            //todo 创建请求参数
            SortedMap<String, String> req = new TreeMap<String, String>();
            req.put("appid", "**"); // 公众号ID
            req.put("mch_id", "**");   // 商户号
            req.put("out_trade_no", outTradeNo);    // 订单号
            req.put("nonce_str", WXPayUtil.generateNonceStr()); // 随机字符串
            req.put("sign", WXPayUtil.generateSignature(req, "APPSecret", WXPayConstants.SignType.MD5));
            //生成要发送的 xml
            String xmlBody = WXPayUtil.generateSignedXml(req, "APPSecret");
            logger.info("订单支付状态请求参数={}", WXPayUtil.xmlToMap(xmlBody));
            //调用查询订单支付状态 API
            String result = CommonUtil.httpsRequest("https://api.mch.weixin.qq.com/pay/orderquery", "POST", xmlBody);
            // 返回解析后的 map 数据 转map
            Map<String, String> map = WXPayUtil.xmlToMap(result);
            logger.info("订单支付状态请求结果={}", WXPayUtil.xmlToMap(result));
            return map;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

4.4 关闭交易订单接口

自定义时间,这期间未支付则提示二维码超时,前端在触发该接口

/**
     * 关闭交易订单
     * @author: fuqin
     * @date: 2022/1/7
     * @param outTradeNo
     * @return 交易关闭状态
     */
    @RequestMapping(value = "/closeOrder", method = RequestMethod.GET)
    public Map<String, String> closeOrder(@RequestParam("outTradeNo") String outTradeNo) {
        try {
            SortedMap<String, String> req = new TreeMap<>();
            req.put("appid", "***"); //公众号appid
            req.put("mch_id", "***");  // 商户号
            req.put("nonce_str", WXPayUtil.generateNonceStr()); // 32位随机字符串
            req.put("out_trade_no", outTradeNo);//订单号
            req.put("sign", WXPayUtil.generateSignature(req, "APPSecret", WXPayConstants.SignType.MD5)); // 签名
            //生成发送请求的xml
            String xmlBody = WXPayUtil.generateSignedXml(req, "APPSecret");
            logger.info("关闭交易订单请求的xmlBody={}", WXPayUtil.xmlToMap(xmlBody));
            String result = CommonUtil.httpsRequest("https://api.mch.weixin.qq.com/pay/closeorder", "POST", xmlBody);
            logger.info("关闭交易订单请求结果={}", WXPayUtil.xmlToMap(result));

            // 将返回结果从 xml 格式转换为 map 格式
            Map<String, String> resultMap = WXPayUtil.xmlToMap(result);
            Map<String, String> map = new HashMap<>();

            String result_code = resultMap.get("result_code");
            if (result_code.equals("FAIL")) {
                map.put("err_code_des", resultMap.get("err_code_des"));
                map.put("status", "201");
                return map;
            }
            map.put("status", "200");
            map.put("msg", "交易关闭成功");
            return map;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

4.5 统一下单支付成功之后回调通知接口

客户支付成功,微信调我们接收回调接口,此接口可以做我们需要的业务逻辑,该地址在"4.2生成二维码链接接口"配置。注意地址必须是https协议且无参数
Controller层

/**
     * 统一下单支付成功之后回调通知接口
     * @author: fuqin
     * @date: 2022/1/7
     */
    @RequestMapping(value = "/pay/notify")
    public void payAsyncNotify(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
      
        logger.info("支付回调是否执行");
        /*微信支付异步通知验证签名*/
        String resultXml = wxPayService.payAsyncNotifyVerificationSign(httpServletRequest);
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(httpServletResponse.getOutputStream());
        bufferedOutputStream.write(resultXml.getBytes());
        bufferedOutputStream.flush();
        bufferedOutputStream.close();
    }

WxPayService

public interface WxPayService {
    /**
     * 微信支付异步通知验证签名
     * @param httpServletRequest
     * @return
     */
    String payAsyncNotifyVerificationSign(HttpServletRequest httpServletRequest);

}

WxPayServiceImpl

package net.dunotech.mercury.mtzx.service.WxPay.impl;

import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayUtil;
import net.dunotech.mercury.mtzx.service.WxPay.WxPayService;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;

/**
 * 微信支付成功回调通知
 *
 * @author: fuqin
 * @date: 2022/1/7
 */
@Service
public class WxPayServiceImpl implements WxPayService {
    private static Logger logger = LoggerFactory.getLogger(WxPayServiceImpl.class);

    @Override
    public String payAsyncNotifyVerificationSign(HttpServletRequest httpServletRequest) {
     /*  WXPay wxPay = new WXPay(myWxPayConfig);*/
        String returnXmlMessage = null;
        String notifyXmlData = null;
        try {
            notifyXmlData = readXmlFromStream(httpServletRequest);

            Map<String, String> notifyMapData = WXPayUtil.xmlToMap(notifyXmlData);
            logger.info("微信支付成功回调notifyMapData={}", notifyMapData);
            // 验证签名
           /* boolean signatureValid = wxPay.isPayResultNotifySignatureValid(notifyMapData);*/
            // 验证是否支付成功
            if("SUCCESS".equals(notifyMapData.get( "result_code"))){
                // 获取支付成功的订单号
                String out_trade_no = notifyMapData.get("out_trade_no");
                //订单支付成功之后相关业务逻辑...
                logger.info("支付成功回调订单号out_trade_no={}",out_trade_no);
                // 一切正常返回的xml数据
                returnXmlMessage = setReturnXml(WXPayConstants.SUCCESS, "OK");
                logger.info("[out_trade_no:{}] [支付成功异步消息处理成功:{}]", notifyMapData.get("out_trade_no"),  WXPayUtil.xmlToMap(returnXmlMessage));
            } else {
                returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "Verification sign failed!");
                logger.info("[out_trade_no:{}] [验签失败:{}]", notifyMapData.get("out_trade_no"), WXPayUtil.xmlToMap(returnXmlMessage));
            }
        } catch (IOException e) {
            logger.error("[读取微信服务器返回流中xml数据时发生异常:{}] ", ExceptionUtils.getStackTrace(e));
            returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "An exception occurred while reading the WeChat server returning xml data in the stream.");
        } catch (Exception e) {
            logger.error("[xml数据:{}] [异常:{}] ", notifyXmlData, ExceptionUtils.getStackTrace(e));

            returnXmlMessage = setReturnXml(WXPayConstants.FAIL, "Payment successful, exception occurred during asynchronous notification processing.");
            logger.warn("[支付成功异步消息处理失败:{}]", returnXmlMessage);
        }
        return returnXmlMessage;
    }
    /**
     * 从流中读取微信返回的xml数据
     *
     * @param httpServletRequest
     * @return
     * @throws IOException
     */
    private String readXmlFromStream(HttpServletRequest httpServletRequest) throws IOException {
        InputStream inputStream = httpServletRequest.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        final StringBuffer sb = new StringBuffer();
        String line = null;
        try {
            while ((line = bufferedReader.readLine()) != null) {
                sb.append(line);
            }
        } finally {
            bufferedReader.close();
            inputStream.close();
        }

        return sb.toString();
    }
    /**
     * 设置返回给微信服务器的xml信息
     *
     * @param returnCode
     * @param returnMsg
     * @return
     */
    private String setReturnXml(String returnCode, String returnMsg) {
        return "<xml><return_code><![CDATA[" + returnCode + "]]></return_code><return_msg><![CDATA[" + returnMsg + "]]></return_msg></xml>";
    }
}

总结

写的不好,欢迎大佬们点评。后期有时间还也会更新 支付宝 及 网银支付全流程。