微信支付相关代码独立成一个子模块

This commit is contained in:
Binary Wang
2017-03-05 22:49:17 +08:00
parent 8a270c8f7e
commit eee954b736
49 changed files with 1596 additions and 1308 deletions
@@ -0,0 +1,314 @@
package com.github.binarywang.wxpay.service;
import com.github.binarywang.wxpay.bean.request.*;
import com.github.binarywang.wxpay.bean.result.*;
import com.github.binarywang.wxpay.config.WxPayConfig;
import me.chanjar.weixin.common.exception.WxErrorException;
import java.io.File;
import java.util.Map;
/**
* <pre>
* 微信支付相关接口
* Created by Binary Wang on 2016/7/28.
* </pre>
*
* @author binarywang (https://github.com/binarywang)
*/
public interface WxPayService {
/**
* <pre>
* 查询订单(详见https://com.github.binarywang.wechat.pay.bean.pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2)
* 该接口提供所有微信支付订单的查询,商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑。
* 需要调用查询接口的情况:
* ◆ 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知;
* ◆ 调用支付接口后,返回系统错误或未知交易状态情况;
* ◆ 调用被扫支付API,返回USERPAYING的状态;
* ◆ 调用关单或撤销接口API之前,需确认支付状态;
* 接口地址:https://api.mch.weixin.qq.com/pay/orderquery
* </pre>
*
* @param transactionId 微信订单号
* @param outTradeNo 商户系统内部的订单号,当没提供transactionId时需要传这个。
*/
WxPayOrderQueryResult queryOrder(String transactionId, String outTradeNo) throws WxErrorException;
/**
* <pre>
* 关闭订单
* 应用场景
* 以下情况需要调用关单接口:
* 1. 商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
* 2. 系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
* 注意:订单生成后不能马上调用关单接口,最短调用时间间隔为5分钟。
* 接口地址:https://api.mch.weixin.qq.com/pay/closeorder
* 是否需要证书: 不需要。
* </pre>
*
* @param outTradeNo 商户系统内部的订单号
*/
WxPayOrderCloseResult closeOrder(String outTradeNo) throws WxErrorException;
/**
* 统一下单(详见http://com.github.binarywang.wechat.pay.bean.pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1)
* 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识"
* 接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder
*
* @param request 请求对象,注意一些参数如appid、mchid等不用设置,方法内会自动从配置对象中获取到(前提是对应配置中已经设置)
*/
WxPayUnifiedOrderResult unifiedOrder(WxPayUnifiedOrderRequest request) throws WxErrorException;
/**
* 该接口调用“统一下单”接口,并拼装发起支付请求需要的参数
* 详见http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
*
* @param request 请求对象,注意一些参数如appid、mchid等不用设置,方法内会自动从配置对象中获取到(前提是对应配置中已经设置)
*/
Map<String, String> getPayInfo(WxPayUnifiedOrderRequest request) throws WxErrorException;
/**
* 获取配置
*/
WxPayConfig getConfig();
/**
* 设置配置对象
*/
void setConfig(WxPayConfig config);
/**
* <pre>
* 微信支付-申请退款
* 详见 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
* 接口链接:https://api.mch.weixin.qq.com/secapi/pay/refund
* </pre>
*
* @param request 请求对象
* @return 退款操作结果
*/
WxPayRefundResult refund(WxPayRefundRequest request) throws WxErrorException;
/**
* <pre>
* 微信支付-查询退款
* 应用场景:
* 提交退款申请后,通过调用该接口查询退款状态。退款有一定延时,用零钱支付的退款20分钟内到账,
* 银行卡支付的退款3个工作日后重新查询退款状态。
* 详见 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_5
* 接口链接:https://api.mch.weixin.qq.com/pay/refundquery
* </pre>
* 以下四个参数四选一
*
* @param transactionId 微信订单号
* @param outTradeNo 商户订单号
* @param outRefundNo 商户退款单号
* @param refundId 微信退款单号
* @return 退款信息
*/
WxPayRefundQueryResult refundQuery(String transactionId, String outTradeNo, String outRefundNo, String refundId)
throws WxErrorException;
/**
* 读取支付结果通知
* 详见http://com.github.binarywang.wechat.pay.bean.pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7
*/
WxPayOrderNotifyResult getOrderNotifyResult(String xmlData) throws WxErrorException;
/**
* 微信公众号支付签名算法(详见:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=4_3)
*
* @param xmlbean Bean需要标记有XML注解,默认使用配置中的PartnerKey进行签名
* @return 签名字符串
* @see #createSign(Map, String)
* @since 2.5.0
*/
String createSign(Object xmlbean);
/**
* 微信公众号支付签名算法(详见:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=4_3)
*
* @param xmlbean Bean需要标记有XML注解
* @param signKey 签名Key
* @return 签名字符串
* @see #createSign(Map, String)
*/
String createSign(Object xmlbean, String signKey);
/**
* 微信公众号支付签名算法(详见:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=4_3)
*
* @param prams 参数信息,默认使用配置中的PartnerKey进行签名
* @return 签名字符串
* @see #createSign(Map, String)
*/
String createSign(Map<String, String> prams);
/**
* 微信公众号支付签名算法(详见:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=4_3)
*
* @param prams 参数信息
* @param signKey 签名Key
* @return 签名字符串
*/
String createSign(Map<String, String> prams, String signKey);
/**
* 校验签名是否正确,默认使用配置中的PartnerKey进行签名
*
* @param xmlbean Bean需要标记有XML注解
* @return true - 签名校验成功,false - 签名校验失败
* @see #checkSign(Map, String)
*/
boolean checkSign(Object xmlbean);
/**
* 校验签名是否正确
*
* @param xmlbean Bean需要标记有XML注解
* @param signKey 校验的签名Key
* @return true - 签名校验成功,false - 签名校验失败
* @see #checkSign(Map, String)
*/
boolean checkSign(Object xmlbean, String signKey);
/**
* 校验签名是否正确,默认使用配置中的PartnerKey进行签名
*
* @param prams 需要校验的参数Map
* @return true - 签名校验成功,false - 签名校验失败
* @see #checkSign(Map, String)
*/
boolean checkSign(Map<String, String> prams);
/**
* 校验签名是否正确
*
* @param params 需要校验的参数Map
* @param signKey 校验的签名Key
* @return true - 签名校验成功,false - 签名校验失败
* @see #checkSign(Map, String)
*/
boolean checkSign(Map<String, String> params, String signKey);
/**
* 发送微信红包给个人用户
* <pre>
* 文档详见:
* 发送普通红包 https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_4&index=3
* 接口地址:https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack
* 发送裂变红包 https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_5&index=4
* 接口地址:https://api.mch.weixin.qq.com/mmpaymkttransfers/sendgroupredpack
* </pre>
*
* @param request 请求对象
*/
WxPaySendRedpackResult sendRedpack(WxPaySendRedpackRequest request) throws WxErrorException;
/**
* <pre>
* 查询红包记录
* 用于商户对已发放的红包进行查询红包的具体信息,可支持普通红包和裂变包。
* 请求Url https://api.mch.weixin.qq.com/mmpaymkttransfers/gethbinfo
* 是否需要证书 是(证书及使用说明详见商户证书)
* 请求方式 POST
* </pre>
*
* @param mchBillNo 商户发放红包的商户订单号,比如10000098201411111234567890
*/
WxPayRedpackQueryResult queryRedpack(String mchBillNo) throws WxErrorException;
/**
* <pre>
* 企业付款业务是基于微信支付商户平台的资金管理能力,为了协助商户方便地实现企业向个人付款,针对部分有开发能力的商户,提供通过API完成企业付款的功能。
* 比如目前的保险行业向客户退保、给付、理赔。
* 企业付款将使用商户的可用余额,需确保可用余额充足。查看可用余额、充值、提现请登录商户平台“资金管理”https://pay.weixin.qq.com/进行操作。
* 注意:与商户微信支付收款资金并非同一账户,需要单独充值。
* 文档详见:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2
* 接口链接:https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers
* </pre>
*
* @param request 请求对象
*/
WxEntPayResult entPay(WxEntPayRequest request) throws WxErrorException;
/**
* <pre>
* 查询企业付款API
* 用于商户的企业付款操作进行结果查询,返回付款操作详细结果。
* 文档详见:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_3
* 接口链接:https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo
* </pre>
*
* @param partnerTradeNo 商户订单号
*/
WxEntPayQueryResult queryEntPay(String partnerTradeNo) throws WxErrorException;
/**
* <pre>
* 扫码支付模式一生成二维码的方法
* 二维码中的内容为链接,形式为:
* weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX
* 其中XXXXX为商户需要填写的内容,商户将该链接生成二维码,如需要打印发布二维码,需要采用此格式。商户可调用第三方库生成二维码图片。
* 文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4
* </pre>
*
* @param productId 产品Id
* @param sideLength 要生成的二维码的边长,如果为空,则取默认值400
* @param logoFile 商户logo图片的文件对象,可以为空
* @return 生成的二维码的字节数组
*/
byte[] createScanPayQrcodeMode1(String productId, File logoFile, Integer sideLength);
/**
* <pre>
* 扫码支付模式二生成二维码的方法
* 对应链接格式:weixin://wxpay/bizpayurl?sr=XXXXX。请商户调用第三方库将code_url生成二维码图片。
* 该模式链接较短,生成的二维码打印到结账小票上的识别率较高。
* 文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5
* </pre>
*
* @param codeUrl 微信返回的交易会话的二维码链接
* @param logoFile 商户logo图片的文件对象,可以为空
* @param sideLength 要生成的二维码的边长,如果为空,则取默认值400
* @return 生成的二维码的字节数组
*/
byte[] createScanPayQrcodeMode2(String codeUrl, File logoFile, Integer sideLength);
/**
* <pre>
* 交易保障
* 应用场景:
* 商户在调用微信支付提供的相关接口时,会得到微信支付返回的相关信息以及获得整个接口的响应时间。
* 为提高整体的服务水平,协助商户一起提高服务质量,微信支付提供了相关接口调用耗时和返回信息的主动上报接口,
* 微信支付可以根据商户侧上报的数据进一步优化网络部署,完善服务监控,和商户更好的协作为用户提供更好的业务体验。
* 接口地址: https://api.mch.weixin.qq.com/payitil/report
* 是否需要证书:不需要
* </pre>
*/
void report(WxPayReportRequest request) throws WxErrorException;
/**
* <pre>
* 下载对账单
* 商户可以通过该接口下载历史交易清单。比如掉单、系统错误等导致商户侧和微信侧数据不一致,通过对账单核对后可校正支付状态。
* 注意:
* 1、微信侧未成功下单的交易不会出现在对账单中。支付成功后撤销的交易会出现在对账单中,跟原支付单订单号一致,bill_type为REVOKED;
* 2、微信在次日9点启动生成前一天的对账单,建议商户10点后再获取;
* 3、对账单中涉及金额的字段单位为“元”。
* 4、对账单接口只能下载三个月以内的账单。
* 接口链接:https://api.mch.weixin.qq.com/pay/downloadbill
* 详情请见: <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_6">下载对账单</a>
* </pre>
*
* @param billDate 对账单日期 bill_date 下载对账单的日期,格式:20140603
* @param billType 账单类型 bill_type ALL,返回当日所有订单信息,默认值,SUCCESS,返回当日成功支付的订单,REFUND,返回当日退款订单
* @param tarType 压缩账单 tar_type 非必传参数,固定值:GZIP,返回格式为.gzip的压缩包账单。不传则默认为数据流形式。
* @param deviceInfo 设备号 device_info 非必传参数,终端设备号
* @return 保存到本地的临时文件
*/
File downloadBill(String billDate, String billType, String tarType, String deviceInfo) throws WxErrorException;
}
@@ -0,0 +1,560 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.utils.qrcode.QrcodeUtils;
import com.github.binarywang.wxpay.bean.request.*;
import com.github.binarywang.wxpay.bean.result.*;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.service.WxPayService;
import com.google.common.collect.Maps;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import me.chanjar.weixin.common.bean.result.WxError;
import me.chanjar.weixin.common.exception.WxErrorException;
import me.chanjar.weixin.common.util.BeanUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.CharEncoding;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Consts;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.*;
/**
* Created by Binary Wang on 2016/7/28.
*
* @author binarywang (https://github.com/binarywang)
*/
public class WxPayServiceImpl implements WxPayService {
private static final String PAY_BASE_URL = "https://api.mch.weixin.qq.com";
private static final String[] TRADE_TYPES = new String[]{"JSAPI", "NATIVE", "APP"};
private static final String[] REFUND_ACCOUNT = new String[]{"REFUND_SOURCE_RECHARGE_FUNDS", "REFUND_SOURCE_UNSETTLED_FUNDS"};
private static final String[] BILL_TYPE = new String[]{"ALL", "REFUND", "SUCCESS"};
private final Logger log = LoggerFactory.getLogger(this.getClass());
private WxPayConfig config;
@Override
public WxPayConfig getConfig() {
return this.config;
}
@Override
public void setConfig(WxPayConfig config) {
this.config = config;
}
private String getPayBaseUrl() {
if (this.getConfig().useSandboxForWxPay()) {
return PAY_BASE_URL + "/sandboxnew";
}
return PAY_BASE_URL;
}
@Override
public WxPayRefundResult refund(WxPayRefundRequest request) throws WxErrorException {
this.initRequest(request);
if (StringUtils.isBlank(request.getOpUserId())) {
request.setOpUserId(this.getConfig().getMchId());
}
this.checkParameters(request);
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/secapi/pay/refund";
String responseContent = this.postWithKey(url, request.toXML());
WxPayRefundResult result = WxPayRefundResult.fromXML(responseContent, WxPayRefundResult.class);
this.checkResult(result);
return result;
}
@Override
public WxPayRefundQueryResult refundQuery(String transactionId, String outTradeNo,
String outRefundNo, String refundId)
throws WxErrorException {
if ((StringUtils.isBlank(transactionId) && StringUtils.isBlank(outTradeNo)
&& StringUtils.isBlank(outRefundNo) && StringUtils.isBlank(refundId)) ||
(StringUtils.isNotBlank(transactionId) && StringUtils.isNotBlank(outTradeNo)
&& StringUtils.isNotBlank(outRefundNo) && StringUtils.isNotBlank(refundId))) {
throw new IllegalArgumentException("transaction_id , out_trade_no,out_refund_no, refund_id 必须四选一");
}
WxPayRefundQueryRequest request = new WxPayRefundQueryRequest();
this.initRequest(request);
request.setOutTradeNo(StringUtils.trimToNull(outTradeNo));
request.setTransactionId(StringUtils.trimToNull(transactionId));
request.setOutRefundNo(StringUtils.trimToNull(outRefundNo));
request.setRefundId(StringUtils.trimToNull(refundId));
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/pay/refundquery";
String responseContent = this.post(url, request.toXML());
WxPayRefundQueryResult result = WxPayRefundQueryResult.fromXML(responseContent, WxPayRefundQueryResult.class);
result.composeRefundRecords();
this.checkResult(result);
return result;
}
private void checkResult(WxPayBaseResult result) throws WxErrorException {
//校验返回结果签名
Map<String, String> map = result.toMap();
if (result.getSign() != null && !this.checkSign(map)) {
log.debug("校验结果签名失败,参数:{}", map);
throw new WxErrorException(WxError.newBuilder().setErrorCode(-1).setErrorMsg("参数格式校验错误!").build());
}
//校验结果是否成功
if (!"SUCCESS".equalsIgnoreCase(result.getReturnCode())
|| !"SUCCESS".equalsIgnoreCase(result.getResultCode())) {
StringBuilder errorMsg = new StringBuilder();
if (result.getReturnCode() != null) {
errorMsg.append("返回代码:" + result.getReturnCode());
}
if (result.getReturnMsg() != null) {
errorMsg.append(",返回信息:" + result.getReturnMsg());
}
if (result.getResultCode() != null) {
errorMsg.append(",结果代码:" + result.getResultCode());
}
if (result.getErrCode() != null) {
errorMsg.append(",错误代码:" + result.getErrCode());
}
if (result.getErrCodeDes() != null) {
errorMsg.append(",错误详情:" + result.getErrCodeDes());
}
WxError error = WxError.newBuilder()
.setErrorCode(-1)
.setErrorMsg(errorMsg.toString())
.build();
log.error("\n结果业务代码异常,返回結果:{},\n{}", map, error);
throw new WxErrorException(error);
}
}
private void checkParameters(WxPayDownloadBillRequest request) throws WxErrorException {
BeanUtils.checkRequiredFields(request);
if (StringUtils.isNotBlank(request.getTarType()) && !"GZIP".equals(request.getTarType())) {
throw new IllegalArgumentException("tar_type值如果存在,只能为GZIP");
}
if (!ArrayUtils.contains(BILL_TYPE, request.getBillType())) {
throw new IllegalArgumentException("bill_tpye目前必须为" + Arrays.toString(BILL_TYPE)
+ "其中之一,实际值:" + request.getBillType());
}
}
private void checkParameters(WxPayRefundRequest request) throws WxErrorException {
BeanUtils.checkRequiredFields(request);
if (StringUtils.isNotBlank(request.getRefundAccount())) {
if (!ArrayUtils.contains(REFUND_ACCOUNT, request.getRefundAccount())) {
throw new IllegalArgumentException("refund_account目前必须为" + Arrays.toString(REFUND_ACCOUNT)
+ "其中之一,实际值:" + request.getRefundAccount());
}
}
if (StringUtils.isBlank(request.getOutTradeNo()) && StringUtils.isBlank(request.getTransactionId())) {
throw new IllegalArgumentException("transaction_id 和 out_trade_no 不能同时为空,必须提供一个");
}
}
@Override
public WxPayOrderNotifyResult getOrderNotifyResult(String xmlData) throws WxErrorException {
try {
log.debug("微信支付回调参数详细:{}", xmlData);
WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xmlData);
log.debug("微信支付回调结果对象:{}", result);
this.checkResult(result);
return result;
} catch (WxErrorException e) {
log.error(e.getMessage(), e);
throw e;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new WxErrorException(WxError.newBuilder().setErrorMsg("发生异常" + e.getMessage()).build());
}
}
@Override
public WxPaySendRedpackResult sendRedpack(WxPaySendRedpackRequest request)
throws WxErrorException {
this.initRequest(request);
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/mmpaymkttransfers/sendredpack";
if (request.getAmtType() != null) {
//裂变红包
url = this.getPayBaseUrl() + "/mmpaymkttransfers/sendgroupredpack";
}
String responseContent = this.postWithKey(url, request.toXML());
WxPaySendRedpackResult result = WxPaySendRedpackResult.fromXML(responseContent, WxPaySendRedpackResult.class);
//毋须校验,因为没有返回签名信息
// this.checkResult(result);
return result;
}
@Override
public WxPayRedpackQueryResult queryRedpack(String mchBillNo) throws WxErrorException {
WxPayRedpackQueryRequest request = new WxPayRedpackQueryRequest();
request.setMchBillNo(mchBillNo);
request.setBillType("MCHT");
initRequest(request);
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/mmpaymkttransfers/gethbinfo";
String responseContent = this.postWithKey(url, request.toXML());
WxPayRedpackQueryResult result = WxPayRedpackQueryResult.fromXML(responseContent, WxPayRedpackQueryResult.class);
this.checkResult(result);
return result;
}
@Override
public WxPayOrderQueryResult queryOrder(String transactionId, String outTradeNo) throws WxErrorException {
if ((StringUtils.isBlank(transactionId) && StringUtils.isBlank(outTradeNo)) ||
(StringUtils.isNotBlank(transactionId) && StringUtils.isNotBlank(outTradeNo))) {
throw new IllegalArgumentException("transaction_id 和 out_trade_no 不能同时存在或同时为空,必须二选一");
}
WxPayOrderQueryRequest request = new WxPayOrderQueryRequest();
request.setOutTradeNo(StringUtils.trimToNull(outTradeNo));
request.setTransactionId(StringUtils.trimToNull(transactionId));
initRequest(request);
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/pay/orderquery";
String responseContent = this.post(url, request.toXML());
if (StringUtils.isBlank(responseContent)) {
throw new WxErrorException(WxError.newBuilder().setErrorMsg("无响应结果").build());
}
WxPayOrderQueryResult result = WxPayOrderQueryResult.fromXML(responseContent, WxPayOrderQueryResult.class);
result.composeCoupons();
this.checkResult(result);
return result;
}
@Override
public WxPayOrderCloseResult closeOrder(String outTradeNo) throws WxErrorException {
if (StringUtils.isBlank(outTradeNo)) {
throw new IllegalArgumentException("out_trade_no不能为空");
}
WxPayOrderCloseRequest request = new WxPayOrderCloseRequest();
request.setOutTradeNo(StringUtils.trimToNull(outTradeNo));
this.initRequest(request);
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/pay/closeorder";
String responseContent = this.post(url, request.toXML());
WxPayOrderCloseResult result = WxPayBaseResult.fromXML(responseContent, WxPayOrderCloseResult.class);
this.checkResult(result);
return result;
}
@Override
public WxPayUnifiedOrderResult unifiedOrder(WxPayUnifiedOrderRequest request)
throws WxErrorException {
this.initRequest(request);
if (StringUtils.isBlank(request.getNotifyURL())) {
request.setNotifyURL(getConfig().getNotifyUrl());
}
if (StringUtils.isBlank(request.getTradeType())) {
request.setTradeType(getConfig().getTradeType());
}
this.checkParameters(request);//校验参数
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/pay/unifiedorder";
String responseContent = this.post(url, request.toXML());
WxPayUnifiedOrderResult result = WxPayUnifiedOrderResult.fromXML(responseContent, WxPayUnifiedOrderResult.class);
this.checkResult(result);
return result;
}
private void initRequest(WxPayBaseRequest request) {
if (StringUtils.isBlank(request.getAppid())) {
request.setAppid(this.getConfig().getAppId());
}
if (StringUtils.isBlank(request.getMchId())) {
request.setMchId(this.getConfig().getMchId());
}
if (StringUtils.isBlank(request.getSubAppId())) {
request.setSubAppId(this.getConfig().getSubAppId());
}
if (StringUtils.isBlank(request.getSubMchId())) {
request.setSubMchId(this.getConfig().getSubMchId());
}
if (StringUtils.isBlank(request.getNonceStr())) {
request.setNonceStr(String.valueOf(System.currentTimeMillis()));
}
}
private void checkParameters(WxPayUnifiedOrderRequest request) throws WxErrorException {
BeanUtils.checkRequiredFields(request);
if (!ArrayUtils.contains(TRADE_TYPES, request.getTradeType())) {
throw new IllegalArgumentException("trade_type目前必须为" + Arrays.toString(TRADE_TYPES) + "其中之一,实际值:" + request.getTradeType());
}
if ("JSAPI".equals(request.getTradeType()) && request.getOpenid() == null) {
throw new IllegalArgumentException("当 trade_type是'JSAPI'时未指定openid");
}
if ("NATIVE".equals(request.getTradeType()) && request.getProductId() == null) {
throw new IllegalArgumentException("当 trade_type是'NATIVE'时未指定product_id");
}
}
@Override
public Map<String, String> getPayInfo(WxPayUnifiedOrderRequest request) throws WxErrorException {
WxPayUnifiedOrderResult unifiedOrderResult = this.unifiedOrder(request);
String prepayId = unifiedOrderResult.getPrepayId();
if (StringUtils.isBlank(prepayId)) {
throw new RuntimeException(String.format("无法获取prepay id,错误代码: '%s',信息:%s。",
unifiedOrderResult.getErrCode(), unifiedOrderResult.getErrCodeDes()));
}
Map<String, String> payInfo = new HashMap<>();
payInfo.put("appId", getConfig().getAppId());
// 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。
// 但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
payInfo.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
payInfo.put("nonceStr", String.valueOf(System.currentTimeMillis()));
payInfo.put("package", "prepay_id=" + prepayId);
payInfo.put("signType", "MD5");
if ("NATIVE".equals(request.getTradeType())) {
payInfo.put("codeUrl", unifiedOrderResult.getCodeURL());
}
payInfo.put("paySign", this.createSign(payInfo));
return payInfo;
}
@Override
public WxEntPayResult entPay(WxEntPayRequest request) throws WxErrorException {
this.initRequest(request);
BeanUtils.checkRequiredFields(request);
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/mmpaymkttransfers/promotion/transfers";
String responseContent = this.postWithKey(url, request.toXML());
WxEntPayResult result = WxEntPayResult.fromXML(responseContent, WxEntPayResult.class);
this.checkResult(result);
return result;
}
@Override
public WxEntPayQueryResult queryEntPay(String partnerTradeNo) throws WxErrorException {
WxEntPayQueryRequest request = new WxEntPayQueryRequest();
this.initRequest(request);
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/mmpaymkttransfers/gettransferinfo";
String responseContent = this.postWithKey(url, request.toXML());
WxEntPayQueryResult result = WxEntPayQueryResult.fromXML(responseContent, WxEntPayQueryResult.class);
this.checkResult(result);
return result;
}
@Override
public byte[] createScanPayQrcodeMode1(String productId, File logoFile, Integer sideLength) {
//weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX
StringBuilder codeUrl = new StringBuilder("weixin://wxpay/bizpayurl?");
Map<String, String> params = Maps.newHashMap();
params.put("appid", this.getConfig().getAppId());
params.put("mch_id", this.getConfig().getMchId());
params.put("product_id", productId);
params.put("time_stamp", String.valueOf(System.currentTimeMillis()));
params.put("nonce_str", String.valueOf(System.currentTimeMillis()));
String sign = this.createSign(params);
params.put("sign", sign);
for (String key : params.keySet()) {
codeUrl.append(key + "=" + params.get(key) + "&");
}
String content = codeUrl.toString().substring(0, codeUrl.length() - 1);
if (sideLength == null || sideLength < 1) {
return QrcodeUtils.createQrcode(content, logoFile);
}
return QrcodeUtils.createQrcode(content, sideLength, logoFile);
}
@Override
public byte[] createScanPayQrcodeMode2(String codeUrl, File logoFile, Integer sideLength) {
if (sideLength == null || sideLength < 1) {
return QrcodeUtils.createQrcode(codeUrl, logoFile);
}
return QrcodeUtils.createQrcode(codeUrl, sideLength, logoFile);
}
public void report(WxPayReportRequest request) throws WxErrorException {
BeanUtils.checkRequiredFields(request);
this.initRequest(request);
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/payitil/report";
String responseContent = this.post(url, request.toXML());
WxPayCommonResult result = WxPayBaseResult.fromXML(responseContent, WxPayCommonResult.class);
this.checkResult(result);
}
@Override
public File downloadBill(String billDate, String billType, String tarType, String deviceInfo) throws WxErrorException {
WxPayDownloadBillRequest request = new WxPayDownloadBillRequest();
this.initRequest(request);
request.setBillType(billType);
request.setBillDate(billDate);
request.setTarType(tarType);
request.setDeviceInfo(deviceInfo);
this.checkParameters(request);
request.setSign(this.createSign(request));
String url = this.getPayBaseUrl() + "/pay/downloadbill";
//TODO 返回的内容可能是文件流,也有可能是xml,需要区分对待
String responseContent = this.post(url, request.toXML());
WxPayCommonResult result = WxPayBaseResult.fromXML(responseContent, WxPayCommonResult.class);
this.checkResult(result);
//TODO 待实现,暂时无测试帐号,无法调试
return null;
}
private String post(String url, String xmlParam) {
String requestString = null;
try {
requestString = new String(xmlParam.getBytes(CharEncoding.UTF_8), CharEncoding.ISO_8859_1);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
HttpRequest request = HttpRequest.post(url).body(requestString);
HttpResponse response = request.send();
String responseString = null;
try {
responseString = new String(response.bodyText().getBytes(CharEncoding.ISO_8859_1), CharEncoding.UTF_8);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
this.log.debug("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}", url, xmlParam, responseString);
return responseString;
}
/**
* 由于暂时未找到使用jodd-http实现证书配置的办法,故而暂时使用httpclient
*/
private String postWithKey(String url, String requestStr) throws WxErrorException {
try {
SSLContext sslContext = this.getConfig().getSslContext();
if (null == sslContext) {
sslContext = this.getConfig().initSSLContext();
}
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,
new String[]{"TLSv1"}, null, new DefaultHostnameVerifier());
HttpPost httpPost = new HttpPost(url);
try (CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build()) {
httpPost.setEntity(new StringEntity(new String(requestStr.getBytes("UTF-8"), "ISO-8859-1")));
try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
String result = EntityUtils.toString(response.getEntity(), Consts.UTF_8);
this.log.debug("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}", url, requestStr, result);
return result;
}
} finally {
httpPost.releaseConnection();
}
} catch (Exception e) {
this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[EXCEPTION]: {}", url, requestStr, e.getMessage());
throw new WxErrorException(WxError.newBuilder().setErrorCode(-1).setErrorMsg(e.getMessage()).build(), e);
}
}
@Override
public String createSign(Object xmlBean) {
return this.createSign(BeanUtils.xmlBean2Map(xmlBean), this.getConfig().getMchKey());
}
@Override
public String createSign(Object xmlBean, String signKey) {
return this.createSign(BeanUtils.xmlBean2Map(xmlBean), signKey);
}
@Override
public String createSign(Map<String, String> params) {
return this.createSign(params, this.getConfig().getMchKey());
}
@Override
public String createSign(Map<String, String> params, String signKey) {
if (this.getConfig().useSandboxForWxPay()) {
//使用仿真测试环境
//TODO 目前测试发现,以下两行代码都会出问题,所以暂不建议使用仿真测试环境
signKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456";
//return "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456";
}
SortedMap<String, String> sortedMap = new TreeMap<>(params);
StringBuilder toSign = new StringBuilder();
for (String key : sortedMap.keySet()) {
String value = params.get(key);
if (StringUtils.isNotEmpty(value) && !"sign".equals(key) && !"key".equals(key)) {
toSign.append(key + "=" + value + "&");
}
}
toSign.append("key=" + signKey);
return DigestUtils.md5Hex(toSign.toString()).toUpperCase();
}
@Override
public boolean checkSign(Object xmlBean) {
return this.checkSign(BeanUtils.xmlBean2Map(xmlBean), getConfig().getMchKey());
}
@Override
public boolean checkSign(Object xmlBean, String signKey) {
return this.checkSign(BeanUtils.xmlBean2Map(xmlBean), signKey);
}
@Override
public boolean checkSign(Map<String, String> params) {
return this.checkSign(params, getConfig().getMchKey());
}
@Override
public boolean checkSign(Map<String, String> params, String signKey) {
String sign = this.createSign(params, signKey);
return sign.equals(params.get("sign"));
}
}