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

更多最新文章欢迎大家访问我的个人博客😄:豆腐别馆

一、前言

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

ps:关于支付模式,对官网文档表述感觉不清晰的可参看该篇文章:微信支付模式一与模式二的区别

二、准备工作

  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";
	}
	
}

三、相关代码

1. 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>
2. 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;
	}

}

3. 扫码支付异步回调通知
/**
 * 扫码支付异步回调通知
 */
@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();
}
4. 退款申请异步回调通知

注:需对该接口返回数据进行解密才能拿到想要的数据,代码中的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();
}
5. 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位密钥加解密异常处理

6. 生成带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="https://img-blog.csdnimg.cn/2022010709280168189.png"/>
				// 其中${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. 签名验证失败
(1)沙箱环境下签名验证失败

请先查阅官网文档签名算法,解决的基本步骤如下:

  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;
	}
}
(2)正式环境下签名验证失败

首先正式环境下的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

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

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

<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}

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

3. 支付金额参数错误

(1)异常如下:

<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}

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

五、后话

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值