一、前言
- 微信支付的沙箱环境不似支付宝可近乎完全模拟生产环境,其仅仅只是个验收环境,测试中必须使用官网指定案例。
- 官方文档零散,异步回调、参数解密等官网无示例代码,部分异常也无文档说明,官方代码埋坑未提示…直观感受只有一个:
辣鸡,诚彼娘之非悦牛逼牛逼~ But don’t worry,该篇文章将给出相关代码。 - 关于支付模式,对官网文档表述感觉不清晰的可参看该篇文章:微信支付模式一与模式二的区别
吐槽归吐槽,发现问题并解决问题才是正道,下面进入正题:
二、准备工作
- 支付申请流程
- 参数配置及证书下载
以支付模式二而言,需要准备的有:
- 公众平台: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}
异常解析:
该异常通常是因为传入的价格参数不对,微信支付的默认价格为正整数,单位为分。该异常细心些即可避免。
五、后话
- 微信支付的文档虽然编写得很烂,但该看的还是得细看,熟读完文档,至少你可以避免掉很多的错误。
- 请新手细看微信支付时序图,对你业务的理解有帮助:模式二业务流程时序图
- 2018/07/03号左右微信爆出外部实体注入漏洞,该漏洞于4号微信团队已补上,在这之前已开发好的朋友记得更换官方最新SDK,具体应对方案请参阅官方给出的文档:关于XML解析存在的安全问题指引
- 代码及文章编写过程中部分参阅到其它资料,仓促间忘记录来源,如有侵权请与本人联系
…
好了,就到这,欢迎探讨交流。