Java微信扫码支付(模式二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/stilll123456/article/details/80830004

一、前言

  1. 微信支付的沙箱环境不似支付宝可近乎完全模拟生产环境,其仅仅只是个验收环境,测试中必须使用官网指定案例。
  2. 官方文档零散,异步回调、参数解密等官网无示例代码,部分异常也无文档说明,官方代码埋坑未提示…直观感受只有一个:辣鸡,诚彼娘之非悦 牛逼牛逼~ But don’t worry,该篇文章将给出相关代码。
  3. 关于支付模式,对官网文档表述感觉不清晰的可参看该篇文章:微信支付模式一与模式二的区别

吐槽归吐槽,发现问题并解决问题才是正道,下面进入正题:


二、准备工作

  1. 支付申请流程
  2. 参数配置及证书下载

以支付模式二而言,需要准备的有:
- 公众平台:APP_ID、商户证书
- 商户平台:MCH_ID、API_KEY

配置参数如下:该类从官网下载即可,如getPayNotifyUrl()等需自己定义

package com.yby.api.weixin.pay;

import java.io.InputStream;

public abstract class WXPayConfig {

    /**
     * 获取 App ID
     *
     * @return App ID
     */
    abstract String getAppID();

    /**
     * 获取 Mch ID
     *
     * @return Mch ID
     */
    abstract String getMchID();

    /**
     * 获取 API 密钥
     *
     * @return API密钥
     */
    abstract String getKey();

    /**
     * 获取商户证书内容
     *
     * @return 商户证书内容
     */
    abstract InputStream getCertStream();

    /**
     * HTTP(S) 连接超时时间,单位毫秒
     *
     * @return
     */
    abstract int getHttpConnectTimeoutMs();

    /**
     * HTTP(S) 读数据超时时间,单位毫秒
     *
     * @return
     */
    abstract int getHttpReadTimeoutMs();

    /**
     * 获取WXPayDomain, 用于多域名容灾自动切换
     * 
     * @return
     */
    abstract IWXPayDomain getWXPayDomain();

    /**
     * 是否自动上报。 若要关闭自动上报,子类中实现该函数返回 false 即可。
     *
     * @return
     */
    abstract boolean shouldAutoReport();

    /**
     * 进行健康上报的线程的数量
     *
     * @return
     */
    abstract int getReportWorkerNum();

    /**
     * 健康上报缓存消息的最大数量。会有线程去独立上报 粗略计算:加入一条消息200B,10000消息占用空间 2000 KB,约为2MB,可以接受
     *
     * @return
     */
    abstract int getReportQueueMaxSize();

    /**
     * 批量上报,一次最多上报多个数据
     *
     * @return
     */
    abstract int getReportBatchSize();

    /**
     * 扫码支付回调地址
     */
    abstract String getPayNotifyUrl();

    /**
     * 退款申请回调地址
     */
    abstract String getRefundNotifyUrl();

}

配置实现类,该类需自己重写,注意星号部分需替换为自己的参数:

package com.yby.api.weixin.pay;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class WXPayConfigImpl extends WXPayConfig {

    private byte[] certData;
    private static WXPayConfigImpl INSTANCE;

    public WXPayConfigImpl() {
        // TODO 1、此处最好修改为外界不可访问路径 2、名称复杂化
        String certPath = "D://*****************/apiclient_cert.p12";
        File file = new File(certPath);
        try {
            InputStream certStream = new FileInputStream(file);
            this.certData = new byte[(int) file.length()];
            certStream.read(this.certData);
            certStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static WXPayConfigImpl getInstance() throws Exception {
        if (INSTANCE == null) {
            synchronized (WXPayConfigImpl.class) {
                if (INSTANCE == null) {
                    INSTANCE = new WXPayConfigImpl();
                }
            }
        }
        return INSTANCE;
    }

    public String getAppID() {
        return "*****************";
    }

    public String getMchID() {
        return "*****************";
    }

    public String getKey() {
        String key = "*****************";
        // String sandBoxKey = "*****************";
        return key;
    }

    public InputStream getCertStream() {
        ByteArrayInputStream certBis;
        certBis = new ByteArrayInputStream(this.certData);
        return certBis;
    }

    public int getHttpConnectTimeoutMs() {
        return 6 * 1000;
    }

    public int getHttpReadTimeoutMs() {
        return 8 * 1000;
    }

    public IWXPayDomain getWXPayDomain() {
        return WXPayDomainSimpleImpl.instance();
    }

    public String getPrimaryDomain() {
        return "api.mch.weixin.qq.com";
    }

    public String getAlternateDomain() {
        return "api2.mch.weixin.qq.com";
    }

    @Override
    public int getReportWorkerNum() {
        return 5;
    }

    @Override
    public int getReportBatchSize() {
        return 10;
    }

    @Override
    public boolean shouldAutoReport() {
        return true;
    }

    @Override
    public int getReportQueueMaxSize() {
        return 10000;
    }

    @Override
    public String getPayNotifyUrl() {
        return "https://*****************/wxpay/notify";
    }

    @Override
    public String getRefundNotifyUrl() {
        return "https://*****************/wxpay/refund/notify";
    }

}

三、相关代码

Maven依赖

注:此处需注意httpclient版本不适用触发的NoClassDefFoundError异常

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

<!-- httpclient:接口调用时使用,此处注意jar包版本,0.03的微信支付版本在调用接口时,如报类似以下错误的请使用4.4版本的httpclient。
org.springframework.web.util.NestedServletException: 
Handler processing failed; nested exception is java.lang.NoClassDefFoundError: org/apache/http/conn/ssl/DefaultHostnameVerifier
-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.4</version>
</dependency>

<!-- 日志 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.21</version>
</dependency>

<!-- google二维码生成工具,微信不提供收款二维码工具类,需自己处理 -->
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.3.3</version>
</dependency>

<!-- BouncyCastle库,用于微信支付退款回调通知解密 -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk16</artifactId>
    <version>1.45</version>
</dependency>

API调用

注:建议先上官网下底层代码,接口封装调用可参考如下代码

package com.yby.api.service.impl;

import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Service;

import com.yby.api.service.WXPayService;
import com.yby.api.weixin.pay.WXPay;
import com.yby.api.weixin.pay.WXPayConfigImpl;

/**
 * 微信支付
 * 
 * @author lwx
 */
@Service
public class WXPayServiceImpl implements WXPayService {

    /**
     * 微信扫码支付 - 简单参数
     * 
     * @param body
     *            商品描述
     * @param product_id
     *            商品ID
     * @param out_trade_no
     *            商户订单号
     * @param device_info
     *            设备号(自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB")
     * @param total_fee
     *            标价金额(分)
     * @param spbill_create_ip
     *            终端IP(APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。)
     * @param time_expire
     *            订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。
     *            订单失效时间是针对订单号而言的,由于在请求支付的时候有一个必传参数prepay_id只有两小时的有效期,所以在重入时间超过2小时的时候需要重新请求下单接口获取新的prepay_id。
     */
    @Override
    public Map<String, String> simpleParamUnifiedOrder(String body, String product_id, String out_trade_no,
            String device_info, String total_fee, String spbill_create_ip, String time_expire) {

        WXPayConfigImpl config = new WXPayConfigImpl();
        WXPay wxpay = null;
        try {
            wxpay = new WXPay(config);
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        if (wxpay == null) {
            return null;
        }

        Map<String, String> data = new HashMap<String, String>();
        data.put("body", body);
        data.put("out_trade_no", out_trade_no);
        data.put("device_info", device_info);
        data.put("fee_type", "CNY");
        data.put("total_fee", total_fee);
        data.put("spbill_create_ip", spbill_create_ip);
        data.put("notify_url", config.getPayNotifyUrl());
        // 此处指定为扫码支付
        data.put("trade_type", "NATIVE");
        data.put("product_id", product_id);
        data.put("time_expire", time_expire);

        Map<String, String> map = null;
        try {
            map = wxpay.unifiedOrder(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (map == null) {
            map = new HashMap<String, String>();
        }
        return map;
    }

    /**
     * 订单交易查询
     * 
     * @param out_trade_no
     *            商户订单号
     */
    @Override
    public Map<String, String> tradeQuery(String out_trade_no) {
        WXPayConfigImpl config = new WXPayConfigImpl();
        WXPay wxpay = null;
        try {
            wxpay = new WXPay(config);
        } catch (Exception e1) {
            e1.printStackTrace();
        }

        Map<String, String> data = new HashMap<String, String>();
        data.put("out_trade_no", out_trade_no);

        Map<String, String> map = new HashMap<String, String>();
        try {
            map = wxpay.orderQuery(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (map == null) {
            map = new HashMap<String, String>();
        }
        return map;
    }

    /**
     * 关闭订单
     * 
     * @param out_trade_no
     *            商户订单号
     */
    @Override
    public Map<String, String> close(String out_trade_no) {
        WXPayConfigImpl config = new WXPayConfigImpl();
        WXPay wxpay = null;
        try {
            wxpay = new WXPay(config);
        } catch (Exception e1) {
            e1.printStackTrace();
        }

        Map<String, String> data = new HashMap<String, String>();
        data.put("out_trade_no", out_trade_no);

        Map<String, String> map = new HashMap<String, String>();
        try {
            map = wxpay.closeOrder(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (map == null) {
            map = new HashMap<String, String>();
        }
        return map;
    }

    /**
     * 申请退款
     * 
     * @param out_trade_no
     *            商户订单号,与微信订单号二选一设置,若两者都传,则以微信订单号为准
     * @param transaction_id
     *            微信订单号,与商户订单号二选一设置,若两者都传,则以微信订单号为准
     * @param out_refund_no
     *            商户退款单号
     * @param total_fee
     *            订单总金额,单位为分,只能为整数
     * @param refund_fee
     *            退款总金额,单位为分,只能为整数
     * @param refund_desc
     *            退款原因
     */
    @Override
    public Map<String, String> refund(String out_trade_no, String transaction_id, String out_refund_no,
            String total_fee, String refund_fee, String refund_desc) {
        WXPayConfigImpl config = new WXPayConfigImpl();
        WXPay wxpay = null;
        try {
            wxpay = new WXPay(config);
        } catch (Exception e1) {
            e1.printStackTrace();
        }

        Map<String, String> data = new HashMap<String, String>();
        if (transaction_id != null && !"".equals(transaction_id)) {
            data.put("transaction_id", transaction_id);
        } else {
            data.put("out_trade_no", out_trade_no);
        }
        data.put("out_refund_no", out_refund_no);
        data.put("total_fee", total_fee);
        data.put("refund_fee", refund_fee);
        data.put("refund_desc", refund_desc);
        data.put("notify_url", config.getRefundNotifyUrl());

        Map<String, String> map = new HashMap<String, String>();
        try {
            map = wxpay.refund(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (map == null) {
            map = new HashMap<String, String>();
        }
        return map;
    }

    /**
     * 退款查询
     * 
     * @param out_trade_no
     *            商户订单号
     */
    @Override
    public Map<String, String> refundQuery(String out_trade_no) {
        WXPayConfigImpl config = new WXPayConfigImpl();
        WXPay wxpay = null;
        try {
            wxpay = new WXPay(config);
        } catch (Exception e1) {
            e1.printStackTrace();
        }

        Map<String, String> data = new HashMap<String, String>();
        data.put("out_trade_no", out_trade_no);

        Map<String, String> map = new HashMap<String, String>();
        try {
            map = wxpay.refundQuery(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (map == null) {
            map = new HashMap<String, String>();
        }
        return map;
    }

}

扫码支付异步回调通知

/**
 * 扫码支付异步回调通知
 */
@RequestMapping("/notify")
public void payNotify(HttpServletResponse response) throws Exception {

    // 读取参数
    InputStream inputStream;
    StringBuffer buffer = new StringBuffer();
    inputStream = request.getInputStream();
    String str;
    BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
    while ((str = in.readLine()) != null) {
        buffer.append(str);
    }
    in.close();
    inputStream.close();

    // 解析xml成map
    Map<String, String> notifyMap = new HashMap<String, String>();
    notifyMap = WXPayUtil.xmlToMap(buffer.toString());

    WXPayConfigImpl config = new WXPayConfigImpl();
    WXPay wxpay = new WXPay(config);
    // 通知微信xml
    String resXml = "";
    // 通知微信标记
    boolean resFlag = false;
    // 验证签名
    if (wxpay.isPayResultNotifySignatureValid(notifyMap)) {
        if (WXPayConstants.SUCCESS.equals((String) notifyMap.get(WXPayConstants.Param.RESULT_CODE))) {
            resFlag = true;
            // 支付成功,执行商户操作
            orderService.notifyOperation(notifyMap, false, DictionaryCode.Payment.WX_PAY,
                    ClientUtil.getClientIP(request), getUser());
        } else {
            // 重新查询,如果待支付订单实际已支付,将调用notifyOperation()执行正常商户操作
            boolean isSuccess = orderService.wxAffirmTradeQuery(notifyMap.get(WXPayConstants.Param.OUT_TRADE_NO),
                    ClientUtil.getClientIP(request), getUser());
            // 支付成功
            if (isSuccess) {
                resFlag = true;
            } else {
                log.info("【异常】微信扫码支付失败,错误代码:" + notifyMap.get(WXPayConstants.Param.ERR_CODE) + ",错误描述:"
                        + notifyMap.get(WXPayConstants.Param.ERR_CODE_DES));
            }
        }
    } else {
        log.info("【异常】微信扫码支付异步通知签名验证失败");
    }
    // 通知微信异步确认成功。(必写,不然会一直通知后台,八次之后就认为交易失败了)
    if (resFlag) {
        resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
                + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
    } else {
        resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
                + "<return_msg><![CDATA[签名失败]]></return_msg>" + "</xml> ";
    }
    // 处理业务完毕
    BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
    out.write(resXml.getBytes());
    out.flush();
    out.close();
}

退款申请异步回调通知

注:需对该接口返回数据进行解密才能拿到想要的数据,代码中的AESUtil即为已封装好的解密工具类。
解密步骤如下:
(1)对加密串A做base64解码,得到加密串B
(2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)–>账户设置–>API安全–>密钥设置 )
(3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding)

/**
 * 退款申请异步回调通知
 */
@RequestMapping("/refund/notify")
public void refundNotify(HttpServletResponse response) throws Exception {

    // 读取参数
    InputStream inputStream;
    StringBuffer buffer = new StringBuffer();
    inputStream = request.getInputStream();
    String str;
    BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
    while ((str = in.readLine()) != null) {
        buffer.append(str);
    }
    in.close();
    inputStream.close();

    // 通知微信xml
    String resXml = "";
    // 通知微信标记
    boolean resFlag = false;
    // 解析xml成map
    Map<String, String> notifyMap = new HashMap<String, String>();
    notifyMap = WXPayUtil.xmlToMap(buffer.toString());
    if (!notifyMap.isEmpty() && WXPayConstants.SUCCESS.equals(notifyMap.get(WXPayConstants.Param.RETURN_CODE))) {
        WXPayConfigImpl config = new WXPayConfigImpl();
        // 加密信息
        String req_info = notifyMap.get(WXPayConstants.Param.REQ_INFO);
        // 解密
        String decodeXml = AESUtil.decryptData(req_info, config.getKey());
        Map<String, String> resultMap = WXPayUtil.xmlToMap(decodeXml);
        if (resultMap == null) {
            resultMap = new HashMap<String, String>();
        }
        log.info(resultMap);
        // 商户订单号
        String out_trade_no = resultMap.get(WXPayConstants.Param.OUT_TRADE_NO);
        // 退款成功
        if (WXPayConstants.REFUND_STATUS.SUCCESS.equals(resultMap.get(WXPayConstants.Param.REFUND_STATUS))) {
            resFlag = true;
            // 微信订单号
            String transaction_id = resultMap.get(WXPayConstants.Param.TRANSACTION_ID);
            // 微信退款单号
            String refund_id = resultMap.get(WXPayConstants.Param.REFUND_ID);
            // 商户退款单号
            String out_refund_no = resultMap.get(WXPayConstants.Param.OUT_REFUND_NO);
            // 退款金额
            BigDecimal bdl = new BigDecimal(resultMap.get(WXPayConstants.Param.REFUND_FEE));
            BigDecimal bd = new BigDecimal("100");
            String settlement_refund_fee =bdl.divide(bd).toString() ;
            // 退款入账账户
            String refund_recv_accout = resultMap.get(WXPayConstants.Param.REFUND_RECV_ACCOUT);

            DzOrder dzOrder = orderService.getBySn(out_refund_no);
            refundAuditService.common(refund_recv_accout, transaction_id, settlement_refund_fee, dzOrder,
                    out_trade_no);

        } else {
            // 调用退款查询接口重新查询并根据结果做相应操作
            orderService.wxAffirmRefundQuery(out_trade_no, resultMap.get(WXPayConstants.Param.OUT_REFUND_NO));

            log.info("【异常】微信退款失败,退款状态:" + resultMap.get(WXPayConstants.Param.REFUND_STATUS) + ",申请退款金额:"
                    + resultMap.get(WXPayConstants.Param.REFUND_FEE));
        }
    }
    // 通知微信
    if (resFlag) {
        resXml = "<xml>" + "  <return_code><![CDATA[SUCCESS]]></return_code>"
                + "  <return_msg><![CDATA[OK]]></return_msg>" + "</xml>";
    } else {
        resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
                + "<return_msg><![CDATA[解密失败]]></return_msg>" + "</xml> ";
    }
    // 处理业务完毕
    BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
    out.write(resXml.getBytes());
    out.flush();
    out.close();
}

AES加密解密工具类

package com.yby.api.common;

import java.security.Security;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

/**
 * AES加密工具类
 * 
 * @author lwx
 * @data 2018/07/03
 */
public class AESUtil {

    /**
     * 密钥算法
     */
    private static final String ALGORITHM = "AES";

    /**
     * 加解密算法/工作模式/填充方式
     */
    private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";

    /**
     * AES加密
     * 
     * @param data
     * @return
     * @throws Exception
     */
    public static String encryptData(String data, String key) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        // 创建密码器
        Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
        SecretKeySpec secretKeySpec = new SecretKeySpec(MD5Util.encode(key).toLowerCase().getBytes(), ALGORITHM);
        // 初始化
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        return Base64.encodeBase64String(cipher.doFinal(data.getBytes()));
    }

    /**
     * AES解密
     * 
     * @param base64Data
     * @return
     * @throws Exception
     */
    public static String decryptData(String base64Data, String key) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
        SecretKeySpec secretKeySpec = new SecretKeySpec(MD5Util.encode(key).toLowerCase().getBytes(), ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
        return new String(cipher.doFinal(Base64.decodeBase64(base64Data)));
    }
}

△ 注:此处如报java.security.InvalidKeyException: Illegal key size or default parameters异常,可参考该篇博文解决:AES的256位密钥加解密异常处理

生成带logo二维码工具类

package com.yby.api.common;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.imageio.ImageIO;

import org.apache.commons.codec.binary.Base64;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix; 
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

/**
 * <pre>
 * 二维码工具类
 *      使用zxing生成带logo的二维码图片,自动调节logo图片相对二维码图片的大小,可选是否带logo、是否保存二维码图片。
 *      结果返回base64编码的图片数据字符串
 * </pre>
 * 
 * @author lwx
 * @date 2018/06/28
 */
public class QRCodeUtil {

    /**
     * 二维码颜色,默认黑色
     */
    private static final int QRCOLOR = 0xFF000000;

    /**
     * 背景颜色
     */
    private static final int BGWHITE = 0xFFFFFFFF;

    /**
     * 二维码宽度
     */
    private static int WIDTH = 400;

    /**
     * 二维码高度
     */
    private static int HEIGHT = 400;

    /**
     * 创建带logo二维码
     * 
     * @param logoPath
     *            logo路径
     * @param content
     *            二维码内容
     */
    public static String createQRCode(String logoPath, String content) {
        try {
            File logoFile = new File(logoPath);
            QRCodeUtil zp = new QRCodeUtil();
            // 生成二维码bufferedImage图片
            BufferedImage bim = zp.getQRCODEBufferedImage(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT,
                    zp.getDecodeHintType());

            // 给二维码图片添加Logo并保存到指定位置,返回base64编码的图片数据字符串
            return zp.createLogoQRCode(null, WIDTH, HEIGHT, bim, logoFile, new LogoConfig(), null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 创建带logo二维码
     * 
     * @param logoPath
     *            二维码图片中间包含的logo图片文件,如果不存在,则生成不带logo图片的二维码
     * @param content
     *            内容或跳转路径
     * @param outPath
     *            二维码输出路径,如果为""则表示不输出图片到指定位置,只返回base64图片字符串
     * @param qrImgWidth
     *            二维码图片宽度
     * @param qrImgHeight
     *            二维码图片高度(有文字的话会加高45px)
     * @param productName
     *            二维码图片下的文字
     * @return
     */
    public static String createQRCode(File logoFile, String content, String outPath, int qrImgWidth, int qrImgHeight,
            String productName) {
        try {
            QRCodeUtil zp = new QRCodeUtil();
            // 生成二维码bufferedImage图片
            BufferedImage bim = zp.getQRCODEBufferedImage(content, BarcodeFormat.QR_CODE, qrImgWidth, qrImgHeight,
                    zp.getDecodeHintType());

            // 如果有文字,则二维码图片高度增加45px
            if (!"".equals(productName)) {
                qrImgHeight += 45;
            }
            // 给二维码图片添加Logo并保存到指定位置,返回base64编码的图片数据字符串
            return zp.createLogoQRCode(outPath, qrImgWidth, qrImgHeight, bim, logoFile, new LogoConfig(), productName);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 给二维码图片添加Logo图片并生成最终二维码图片
     * 
     * @param outPath
     *            输出二维码图片的路径,如果为""则表示不输出图片到指定位置,只返回base64图片字符串
     * @param qrImgWidth
     *            生成二维码图片的宽度
     * @param qrImgHeight
     *            生成二维码图片的高度
     * @param bim
     *            读取二维码图片BufferedImage对象
     * @param logoPic
     *            logo图片File文件
     * @param logoConfig
     *            logo配置
     * @param productName
     *            二维码图片下的文字
     * @return 返回图片base64编码后的字符串
     */
    public String createLogoQRCode(String outPath, int qrImgWidth, int qrImgHeight, BufferedImage bim, File logoPic,
            LogoConfig logoConfig, String productName) {
        try {
            /**
             * 读取二维码图片,并构建绘图对象
             */
            BufferedImage image = bim;

            // 如果logo图片存在,则加入到二维码图片中
            if (logoPic != null && logoPic.exists()) {
                Graphics2D g = image.createGraphics();

                /**
                 * 读取Logo图片
                 */
                BufferedImage logo = ImageIO.read(logoPic);
                /**
                 * 设置logo的大小,本人设置为二维码图片的20%,因为过大会盖掉二维码
                 */
                int widthLogo = logo.getWidth(null) > image.getWidth() * 3 / 10 ? (image.getWidth() * 3 / 10)
                        : logo.getWidth(null),
                        heightLogo = logo.getHeight(null) > image.getHeight() * 3 / 10 ? (image.getHeight() * 3 / 10)
                                : logo.getWidth(null);

                /**
                 * logo放在中心
                 */
                int x = (image.getWidth() - widthLogo) / 2;
                int y = (image.getHeight() - heightLogo) / 2;
                /**
                 * logo放在右下角 int x = (image.getWidth() - widthLogo); int y = (image.getHeight()
                 * - heightLogo);
                 */

                // 开始绘制图片
                g.drawImage(logo, x, y, widthLogo, heightLogo, null);
                // g.drawRoundRect(x, y, widthLogo, heightLogo, 15, 15);
                // g.setStroke(new BasicStroke(logoConfig.getBorder()));
                // g.setColor(logoConfig.getBorderColor());
                // g.drawRect(x, y, widthLogo, heightLogo);
                g.dispose();

                logo.flush();
            }

            // 把商品名称添加上去,商品名称不要太长,这里最多支持两行。太长就会自动截取
            if (productName != null && !productName.equals("")) {
                // 新的图片,把带logo的二维码下面加上文字
                BufferedImage outImage = new BufferedImage(qrImgWidth, qrImgHeight, BufferedImage.TYPE_4BYTE_ABGR);
                Graphics2D outg = outImage.createGraphics();
                // 画二维码到新的面板
                outg.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null);
                // 画文字到新的面板
                outg.setColor(Color.BLACK);
                outg.setFont(new Font("宋体", Font.BOLD, 26)); // 字体、字型、字号
                int strWidth = outg.getFontMetrics().stringWidth(productName);
                if (strWidth > 399) {
                    // //长度过长就截取前面部分
                    // outg.drawString(productName, 0, image.getHeight() + (outImage.getHeight() -
                    // image.getHeight())/2 + 5 ); //画文字
                    // 长度过长就换行
                    String productName1 = productName.substring(0, productName.length() / 2);
                    String productName2 = productName.substring(productName.length() / 2, productName.length());
                    int strWidth1 = outg.getFontMetrics().stringWidth(productName1);
                    int strWidth2 = outg.getFontMetrics().stringWidth(productName2);
                    outg.drawString(productName1, 200 - strWidth1 / 2,
                            image.getHeight() + (outImage.getHeight() - image.getHeight()) / 2 + 12);
                    BufferedImage outImage2 = new BufferedImage(400, 485, BufferedImage.TYPE_4BYTE_ABGR);
                    Graphics2D outg2 = outImage2.createGraphics();
                    outg2.drawImage(outImage, 0, 0, outImage.getWidth(), outImage.getHeight(), null);
                    outg2.setColor(Color.BLACK);
                    outg2.setFont(new Font("宋体", Font.BOLD, 26)); // 字体、字型、字号
                    outg2.drawString(productName2, 200 - strWidth2 / 2,
                            outImage.getHeight() + (outImage2.getHeight() - outImage.getHeight()) / 2 + 5);
                    outg2.dispose();
                    outImage2.flush();
                    outImage = outImage2;
                } else {
                    outg.drawString(productName, 200 - strWidth / 2,
                            image.getHeight() + (outImage.getHeight() - image.getHeight()) / 2 + 12); // 画文字
                }
                outg.dispose();
                outImage.flush();
                image = outImage;
            }
            // logo.flush();
            image.flush();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            baos.flush();
            ImageIO.write(image, "png", baos);

            // 如果输出路径为空,则不保存二维码图片到指定路径下
            if (outPath != null && !"".equals(outPath.trim())) {
                // 二维码生成的路径,但是实际项目中,我们是把这生成的二维码显示到界面上的,因此下面的折行代码可以注释掉
                // 可以看到这个方法最终返回的是这个二维码的imageBase64字符串
                // 前端用 <img src="data:image/png;base64,${imageBase64QRCode}"/>
                // 其中${imageBase64QRCode}对应二维码的imageBase64字符串
                ImageIO.write(image, "png", new File(outPath + "\\" + new Date().getTime() + ".png"));
            }

            // 获取base64编码的二维码图片字符串
            String imageBase64QRCode = Base64.encodeBase64String(baos.toByteArray());
            baos.close();
            return imageBase64QRCode;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 构建初始化二维码
     */
    public BufferedImage fileToBufferedImage(BitMatrix bm) {
        BufferedImage image = null;
        try {
            int w = bm.getWidth(), h = bm.getHeight();
            image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);

            for (int x = 0; x < w; x++) {
                for (int y = 0; y < h; y++) {
                    image.setRGB(x, y, bm.get(x, y) ? 0xFF000000 : 0xFFCCDDEE);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return image;
    }

    /**
     * 生成二维码bufferedImage图片
     *
     * @param content
     *            编码内容
     * @param barcodeFormat
     *            编码类型
     * @param width
     *            图片宽度
     * @param height
     *            图片高度
     * @param hints
     *            设置参数
     */
    public BufferedImage getQRCODEBufferedImage(String content, BarcodeFormat barcodeFormat, int width, int height,
            Map<EncodeHintType, ?> hints) {
        MultiFormatWriter multiFormatWriter = null;
        BitMatrix bm = null;
        BufferedImage image = null;
        try {
            multiFormatWriter = new MultiFormatWriter();
            // 参数顺序分别为:编码内容,编码类型,生成图片宽度,生成图片高度,设置参数
            bm = multiFormatWriter.encode(content, barcodeFormat, width, height, hints);
            int w = bm.getWidth();
            int h = bm.getHeight();
            image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);

            // 开始利用二维码数据创建Bitmap图片,分别设为黑(0xFFFFFFFF)白(0xFF000000)两色
            for (int x = 0; x < w; x++) {
                for (int y = 0; y < h; y++) {
                    image.setRGB(x, y, bm.get(x, y) ? QRCOLOR : BGWHITE);
                }
            }
        } catch (WriterException e) {
            e.printStackTrace();
        }
        return image;
    }

    /**
     * 设置二维码的格式参数
     */
    @SuppressWarnings("deprecation")
    public Map<EncodeHintType, Object> getDecodeHintType() {
        // 用于设置QR二维码参数
        Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>();
        // 设置QR二维码的纠错级别(H为最高级别)具体级别信息
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        // 设置编码方式
        hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
        hints.put(EncodeHintType.MARGIN, 0);
        hints.put(EncodeHintType.MAX_SIZE, 350);
        hints.put(EncodeHintType.MIN_SIZE, 100);

        return hints;
    }

    // 接口测试
    public static void main(String[] args) throws WriterException {
        try {
            // filePath是二维码logo的路径,但是实际中我们是放在项目的某个路径下面的,所以路径用上面的,把下面的注释就好
            String logoPath = "F:\\logo.jpg";
            File logoFile = new File(logoPath);
            String outPath = "F:\\QRCode\\";

            String content = "weixin://wxpay/bizpayurl?pr=GC7nRDJ";

            String createQRCode = createQRCode(logoFile, content, outPath, 400, 400, "");
            System.out.println("createQRCode:" + createQRCode);

            String QRCode = createQRCode(logoPath, content);
            System.out.println("QRCode:" + QRCode);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

/**
 * logo的配置
 */
class LogoConfig {

    /**
     * logo默认边框颜色
     */
    public static final Color DEFAULT_BORDERCOLOR = Color.WHITE;

    /**
     * logo默认边框宽度
     */
    public static final int DEFAULT_BORDER = 2;

    /**
     * logo大小默认为照片的1/5
     */
    public static final int DEFAULT_LOGOPART = 5;

    private final int border = DEFAULT_BORDER;
    private final Color borderColor;
    private final int logoPart;

    /**
     * Creates a default config with on color {@link #BLACK} and off color
     * {@link #WHITE}, generating normal black-on-white barcodes.
     */
    public LogoConfig() {
        this(DEFAULT_BORDERCOLOR, DEFAULT_LOGOPART);
    }

    public LogoConfig(Color borderColor, int logoPart) {
        this.borderColor = borderColor;
        this.logoPart = logoPart;
    }

    public Color getBorderColor() {
        return borderColor;
    }

    public int getBorder() {
        return border;
    }

    public int getLogoPart() {
        return logoPart;
    }
}

四、相关异常

签名验证失败

沙箱环境下签名验证失败

请先查阅官网文档签名算法,解决的基本步骤如下:
1. 正常做签名
2. 用带有正常签名的数据 去 https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey 获得一个 signKey。注意这是个key,不是签名,不能拿来用的。
3. 把你正常做签名中的key,替换成 这个signkey,再次做签名,就是那个字符串MD5 以后的。
4. 这个带有新签名的post data 提交到带有 sandboxnew 的地址,比如说 unifiedorder 一类的

至于如何获取沙箱密钥,官网依旧无示例,可参考如下代码:

/**
 * 下面接口中使用到的常量:沙箱验签密钥api
 */
public static final String SANDBOX_SIGNKEY = "/sandboxnew/pay/getsignkey";

/**
 - 获取沙箱验签密钥
 - 
 - @param config
 - @param wxPay
 */
public static String retrieveSandboxSignKey(WXPayConfig config, WXPay wxPay) {
    try {
        Map<String, String> params = new HashMap<String, String>();
        params.put("mch_id", config.getMchID());
        params.put("nonce_str", WXPayUtil.generateNonceStr());
        params.put("sign", WXPayUtil.generateSignature(params, config.getKey()));
        String strXML = wxPay.requestWithoutCert(WXPayConstants.SANDBOX_SIGNKEY, params,
                config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs());
        if (strXML == null || "".equals(strXML)) {
            return null;
        }
        Map<String, String> result = WXPayUtil.xmlToMap(strXML);
        System.out.println("retrieveSandboxSignKey:" + result);
        if ("SUCCESS".equals(result.get("return_code"))) {
            return result.get("sandbox_signkey");
        }
        return null;
    } catch (Exception e) {
        System.out.println("获取sandbox_signkey异常");
        e.printStackTrace();
        return null;
    }
}
正式环境下签名验证失败

首先正式环境下的key并不需要像沙箱那样需要调用接口生成,那怎么还会验证失败呢?
请看官网给的代码(WXPay -> 第50行左右):

public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox)
            throws Exception {
    this.config = config;
    this.notifyUrl = notifyUrl;
    this.autoReport = autoReport;
    this.useSandbox = useSandbox;
    if (useSandbox) {
        this.signType = SignType.MD5; // 沙箱环境
    } else {
        this.signType = SignType.HMACSHA256;
    }
    this.wxPayRequest = new WXPayRequest(config);
}

emmm,相信你已经看到了,微信在这里加了一个判断:

if (useSandbox) {
    this.signType = SignType.MD5; // 沙箱环境
} else {
    this.signType = SignType.HMACSHA256;
}

我们知道,调用支付接口时可以有两种加密方式,其默认是Md5,但由于此处的判断,导致其在执行到签名验证时又改为了HMACSHA256加密,因此这里死活是验证不通过了。知道原因了相信要解决已经很简单,要么去掉该判断,使用默认Md5加密,要么更改默认加密方式为HMACSHA256。至于微信团队为何加上如此前后矛盾的判断??Oh shit,I don’t know

沙箱支付金额(1)无效,请检查需要验收的case

在沙箱环境中调用支付接口时返回如下异常:

<xml>
  <return_code><![CDATA[FAIL]]></return_code>
  <return_msg><![CDATA[沙箱支付金额(1)无效,请检查需要验收的case]]></return_msg>
</xml>

// 调用WXPay -> processResponseXml(String xmlStr)方法得到:
{return_msg=沙箱支付金额(1)无效,请检查需要验收的case, return_code=FAIL}

异常解析:
该异常是因为微信支付在沙箱环境中,其订单支付金额必须是微信支付验收指引-扫码支付验收用例
中指定的金额,也就是说:官方给出的示例金额是多少那就必须是多少,无法修改。

支付金额参数错误

异常如下:

<xml>
  <return_code><![CDATA[FAIL]]></return_code>
  <retmsg><![CDATA[请确认请求参数是否正确total_fee]]></retmsg>
  <retcode><![CDATA[1]]></retcode>
</xml>
{retcode=1, retmsg=请确认请求参数是否正确total_fee, return_code=FAIL}

异常解析:
该异常通常是因为传入的价格参数不对,微信支付的默认价格为正整数,单位为分。该异常细心些即可避免。


五、后话

  1. 微信支付的文档虽然编写得很烂,但该看的还是得细看,熟读完文档,至少你可以避免掉很多的错误。
  2. 请新手细看微信支付时序图,对你业务的理解有帮助:模式二业务流程时序图
  3. 2018/07/03号左右微信爆出外部实体注入漏洞,该漏洞于4号微信团队已补上,在这之前已开发好的朋友记得更换官方最新SDK,具体应对方案请参阅官方给出的文档:关于XML解析存在的安全问题指引
  4. 代码及文章编写过程中部分参阅到其它资料,仓促间忘记录来源,如有侵权请与本人联系


好了,就到这,欢迎探讨交流。

猜你喜欢

转载自blog.csdn.net/stilll123456/article/details/80830004