mirror of
https://gitee.com/binary/weixin-java-tools.git
synced 2026-03-10 00:13:40 +08:00
微信支付相关代码独立成一个子模块
This commit is contained in:
@@ -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;
|
||||
}
|
||||
+560
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user