Java开发微信扫码支付

请注意,本文编写于 95 天前,最后修改于 95 天前,其中某些信息可能已经过时。

微信开发官方文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5
本文采用微信支付二模式,不依赖于平台设置的回调URL

准备工作

在微信扫码支付功能开发之前,需要准备以下信息 公众号Appid,公众号appsecret,商户号mer_id,支付Key

参数配置

在properties中配置好参数,wxpay.callback回调地址也可以一并配置,方便后续使用

#公众号
wxpay.appid=wxw7836z093232q12mx4
wxpay.appsecret=3792368247281dsds32323243422

#支付配置
#微信商户平台
wxpay.mer_id=2832647297
wxpay.key=37R0Fj8d93272vK4RM72937230F2V8i8C3J1
wxpay.callback=https://laomao.utools.club/api/v1/wechat/order/callback

封装配置类

把Properties中的微信相关的参数封装成一个WechatConfig配置类,方便后续使用,开发逻辑更清晰,配置类使用了lombok的Data方法可以自动添加set,get方法,另外除了上面properties中的配置需要封装,还需要在配置类中增加一个统一下单地址的配置

@Data
@Configuration
@PropertySource(value = "classpath:application.properties")
public class WeChatConfig {

    /**
     * 公众号appid
     */
    @Value("${wxpay.appid}")
    private String appId;

    /**
     * 公众号秘钥
     */
    @Value("${wxpay.appsecret}")
    private String appsecret;
    
    /**
     * 商户号ID
     */
    @Value("${wxpay.mer_id}")
    private String machId;

    /**
     * 支付KEY
     */
    @Value("${wxpay.key}")
    private String key;

    /**
     * 支付回调URL
     */
    @Value("${wxpay.callback}")
    private String payCallbackUrl;
    /**
     * 统一下单URL
     */
    private static final String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
    public String getUnifiedOrderUrl() {
        return UNIFIED_ORDER_URL;
    }

}

WXPayUtil工具类封装

封装工具类,为了之后的具体业务实现做准备

public class WXPayUtil {

    /**
     * XML格式字符串转换为Map
     *
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<String, String>();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            throw ex;
        }

    }

    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
        org.w3c.dom.Document document = documentBuilder.newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key : data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        } catch (Exception ex) {
        }
        return output;
    }
    /**
     * 生成微信支付sign
     *
     * @return
     */
    public static String createSign(SortedMap<String, String> params, String key) {
        StringBuilder sb = new StringBuilder();
        Set<Map.Entry<String, String>> es = params.entrySet();
        Iterator<Map.Entry<String, String>> it = es.iterator();

        //生成 stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
        while (it.hasNext()) {
            Map.Entry<String, String> entry = (Map.Entry<String, String>) it.next();
            String k = (String) entry.getKey();
            String v = (String) entry.getValue();
            if (null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)) {
                sb.append(k + "=" + v + "&");
            }
        }

        sb.append("key=").append(key);
        String sign = CommonUtils.MD5(sb.toString()).toUpperCase();
        return sign;
    }

    /**
     * 校验签名
     *
     * @param params
     * @param key
     * @return
     */
    public static boolean isCorrectSign(SortedMap<String, String> params, String key) {
        String sign = createSign(params, key);
        String weixinPaySign = params.get("sign").toUpperCase();
        return weixinPaySign.equals(sign);
    }

    /**
     * 获取有序MAP
     *
     * @param map
     * @return
     */
    public static SortedMap<String, String> getSortedMap(Map<String, String> map) {
        SortedMap<String, String> sortedMap = new TreeMap<>();
        Iterator<String> it = map.keySet().iterator();
        while (it.hasNext()) {
            String key = it.next();
            String value = map.get(key);
            String temp = "";
            if (value != null) {
                temp = value.trim();
            }
            sortedMap.put(key, temp);
        }
        return sortedMap;
    }



}

Controller下单接口开发

通过下单接口接受购买的商品ID,用户ID,生成订单获取codeurl生成二维码返回二维码给用户支付

/**
 * @Description 订单接口
 * @Author laomao
 **/
@RestController
@RequestMapping("/api/v1/order")
public class VideoOrderController {

    @Autowired
    private VideoOrderService videoOrderService;

    @GetMapping("/add")
    public void saveOrder(@RequestParam(value = "video_id", required = true) int vidoeId,
                          HttpServletRequest request,
                          HttpServletResponse response) throws Exception {

        //IP地址和用户ID临时写死方便测试
        //String ipAddr = IpUtils.getIpAddr(request);
        String ipAddr = "125.20.45.1";
        //int id = request.getAttribute("id");
        int id = 1;

        VideoOrderDto videoOrderDto = new VideoOrderDto();
        videoOrderDto.setUserId(id);
        videoOrderDto.setVideoId(vidoeId);
        videoOrderDto.setIp(ipAddr);

        //生成订单获取codeurl
        String code_url = videoOrderService.save(videoOrderDto);
        if (code_url == null) {
            throw new NullPointerException();
        }

        try {
            //生成二维码配置
            Map<EncodeHintType, Object> hints = new HashMap();
            //设置纠错等级
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
            //设置编码类型
            hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");

            BitMatrix bitMatrix = new MultiFormatWriter().encode(code_url, BarcodeFormat.QR_CODE, 400, 400, hints);
            OutputStream out = response.getOutputStream();
            //输出二维码到页面
            MatrixToImageWriter.writeToStream(bitMatrix, "png", out);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

VideoOrderService业务类实现

整个类的业务逻辑
1、通过商品ID查询是否有该商品信息
2、通过用户ID查询是否存在该用户
3、如果前面两个步骤都没有问题,则生成订单信息保存到数据库中
4、然后调用统一下单方法进行签名然后返回payXml,然后通过post协议请求微信统一下单地址传如payXml获取返回值
5、把获取到返回值转换为Map,从中获取到code_url然后进行返回

@Override
@Transactional(propagation = Propagation.REQUIRED)
public String save(VideoOrderDto videoOrderDto) throws Exception {

  //查找视频信息
  Video video = videoMapper.findById(videoOrderDto.getVideoId());
  //查找用户信息
  User user = userMapper.findById(videoOrderDto.getUserId());

  //生成订单
  VideoOrder videoOrder = new VideoOrder();
  videoOrder.setTotalFee(video.getPrice());
  videoOrder.setVideoImg(video.getCoverImg());
  videoOrder.setVideoTitle(video.getTitle());
  videoOrder.setCreateTime(new Date());
  videoOrder.setVideoId(video.getId());

  videoOrder.setState(0);
  videoOrder.setUserId(user.getId());
  videoOrder.setHeadImg(user.getHeadImg());
  videoOrder.setNickname(user.getName());

  videoOrder.setDel(0);
  videoOrder.setIp(videoOrderDto.getIp());
  videoOrder.setOutTradeNo(CommonUtils.generateUUID());
  videoOrderMapper.insert(videoOrder);


  String payXml = unifiedOrder(videoOrder);

  //获取codeurl
  String orderStr = HttpUtils.doPost(weChatConfig.getUnifiedOrderUrl(), payXml);
  if (orderStr == null) {
  return null;
  }
  Map<String, String> unifiedOrderMap = WXPayUtil.xmlToMap(orderStr);
  if (unifiedOrderMap != null) {
  String code_url = unifiedOrderMap.get("code_url");
  return code_url;
  }
  return null;
}

/**
* 统一下单方法
*
* @return
*/
private String unifiedOrder(VideoOrder videoOrder) throws Exception {
  //生成签名
  SortedMap<String, String> params = new TreeMap<>();
  params.put("appid", weChatConfig.getAppId());
  params.put("mch_id", weChatConfig.getMachId());
  params.put("nonce_str", CommonUtils.generateUUID());
  params.put("body", videoOrder.getVideoTitle().replaceAll(" ", ""));
  params.put("out_trade_no", videoOrder.getOutTradeNo());
  params.put("total_fee", videoOrder.getTotalFee().toString());
  params.put("spbill_create_ip", videoOrder.getIp());
  params.put("notify_url", weChatConfig.getPayCallbackUrl());
  params.put("trade_type", "NATIVE");

  //sign签名
  String sign = WXPayUtil.createSign(params, weChatConfig.getKey());
  params.put("sign", sign);

  //map转xml
  String payXml = WXPayUtil.mapToXml(params);
  return payXml;
}

微信支付回调接口开发

上面下单接口获取code_url生成二维码返回给用户,用户扫码支付后,微信开发平台回回调商户接口告知用户支付成功(回调地址的配置就在我们之前在Properties中配置的回调地址)微信会把相关的支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。

对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。
通知频率为15/15/30/180/1800/1800/1800/1800/3600(单位:秒)注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。

推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。
商户系统对于支付结果通知的内容一定要做签名验证,防止数据泄漏导致出现“假通知”,造成资金损失。

效验签名的方法以及写在上面封装的WXPayUtil工具类中了

/**
 * 微信支付回调
 */
@RequestMapping("/order/callback")
public void orderCallback(HttpServletRequest request, HttpServletResponse response) throws Exception {
    InputStream inputStream = request.getInputStream();
    //包装设计模式,性能更高
    BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
    StringBuffer sb = new StringBuffer();
    String line = null;
    while ((line = in.readLine()) != null) {
        sb.append(line);
    }
    in.close();
    inputStream.close();

    Map<String, String> callBackMap = WXPayUtil.xmlToMap(sb.toString());
    SortedMap<String, String> sortedMap = WXPayUtil.getSortedMap(callBackMap);

    //签名效验
    boolean correctSign = WXPayUtil.isCorrectSign(sortedMap, weChatConfig.getKey());
    if (correctSign) {
        if ("SUCCESS".equals(sortedMap.get("return_code"))) {
            String out_trade_no = sortedMap.get("out_trade_no");
            //根据流水号查询订单
            VideoOrder dbVideoOrder = videoOrderService.findByOutTradeNo(out_trade_no);
            //如果状态等于未支付(0)则更新状态
            if (dbVideoOrder.getState() == 0) {
                VideoOrder videoOrder = new VideoOrder();
                videoOrder.setState(1);
                videoOrder.setOpenid(sortedMap.get("openid"));
                videoOrder.setOutTradeNo(out_trade_no);
                videoOrder.setNotifyTime(new Date());
                int rows = videoOrderService.updateVidoeOrderByOutTradeNo(videoOrder);
                //如果等于1通知微信订单处理成功
                if (rows == 1) {
                    response.setContentType("text/xml");
                    response.getWriter().println("success");
                }
            }
        }
    }

    //处理失败
    response.setContentType("text/xml");
    response.getWriter().println("fail");

}

添加新评论

评论列表