前提
是为了获取:标识商户身份的信息、商户的证书和私钥、微信支付的证书、微信支付API的URL
- 获取商户号
微信商户平台:https://pay.weixin.qq.com/步骤:申请成为商户 => 提交资料 => 签署协议 =>
获取商户号 - 获取AppID
微信公众平台:https://mp.weixin.qq.com/步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号 - 申请商户证书
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书包括商户证书和商户私钥 - 获取微信的证书
可以预先下载,也可以通过编程的方式获取。 - 获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥
引依赖
<!--wechatpay-sdk-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.5</version>
</dependency>
配置类
@Configuration
public class WechatpayConfig{
@Value("${wechat-pay.merchant-id}")
private String merchantId;
@Value("${wechat-pay.merchant-serial-number}")
private String merchantSerialNumber;
@Value("${wechat-pay.private-key}")
private String privateKey;
@Value("${wechat-pay.api-v3-key}")
private String apiV3Key;
/**
* 给容器中加入WechatPay的HttpClient,虽然它是WechatPay的,
* 但可以用它给任何外部发请求,因为它只对发给WechatPay的请求做处理而不对发给别的的请求做处理.
*/
@Bean
public HttpClient httpClient(){
//私钥
PrivateKey merchantPrivateKey=PemUtil.loadPrivateKey(privateKey);
//微信证书校验器
Verifier verifier=null;
try{
//获取证书管理器实例
CertificatesManager certificatesManager=CertificatesManager.getInstance();
//向证书管理器增加需要自动更新平台证书的商户信息(默认时间间隔:24小时)
certificatesManager.putMerchant(merchantId,new WechatPay2Credentials(merchantId,new PrivateKeySigner(merchantSerialNumber,merchantPrivateKey)),apiV3Key.getBytes(StandardCharsets.UTF_8));
//从证书管理器中获取verifier
verifier=certificatesManager.getVerifier(merchantId);
}
catch(Exception e){
new RuntimeException("微信证书校验器配置失败");
}
WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create().withMerchant(merchantId,merchantSerialNumber,merchantPrivateKey).withValidator(new WechatPay2Validator(verifier));
CloseableHttpClient httpClient=builder.build();
return httpClient;
}
/**
* 和上面相比只是不需要验证签名了
* @return
*/
@Bean
public HttpClient httpClientWithNoSign(){
//私钥
PrivateKey merchantPrivateKey=PemUtil.loadPrivateKey(privateKey);
//微信证书校验器
Verifier verifier=null;
try{
//获取证书管理器实例
CertificatesManager certificatesManager=CertificatesManager.getInstance();
//向证书管理器增加需要自动更新平台证书的商户信息(默认时间间隔:24小时)
certificatesManager.putMerchant(merchantId,new WechatPay2Credentials(merchantId,new PrivateKeySigner(merchantSerialNumber,merchantPrivateKey)),apiV3Key.getBytes(StandardCharsets.UTF_8));
//从证书管理器中获取verifier
verifier=certificatesManager.getVerifier(merchantId);
}
catch(Exception e){
new RuntimeException("微信证书校验器配置失败");
}
WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create()
.withMerchant(merchantId,merchantSerialNumber,merchantPrivateKey)
.withValidator(response->true);
CloseableHttpClient httpClient=builder.build();
return httpClient;
}
}
写配置
wechat-pay:
#接下来两个用来标识用户
#商户id
merchant-id: xxxxxxxxxxx
#公众号appid(和商户id绑定过)
appid: xxxxxxxxxxx
#接下来两个用来确保SSL(内容未作任何加密,只做了签名.)
#商户证书序列号
merchant-serial-number: xxxxxxxxxxx
#商户私钥
private-key: xxxxxxxxxxx
#APIv3密钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
api-v3-key: xxxxxxxxxxx
#接下来两个是相关地址
#微信服务器地址
domain: https://api.mch.weixin.qq.com
#接收结果通知地址
notify-domain: xxxxxxxxxxx
使用
以下涉及的共有的内容
public class WechatPayConstant{
public static final String CANCEL_PAY_URL="/v3/pay/transactions/out-trade-no/%s/close";
public static final String CREATE_PAY_URL="/v3/pay/transactions/native";
public static final String QUERY_PAY_URL="/v3/pay/transactions/out-trade-no/%s?mchid=%s";
public static final String CREATE_REFUND_URL="/v3/refund/domestic/refunds";
public static final String QUERY_REFUND_URL="/v3/refund/domestic/refunds/%s";
public static final String TRADE_BILL_URL="/v3/bill/tradebill?bill_date=%s&bill_type=%s";
public static final String FLOW_BILL_URL="/v3/bill/fundflowbill?bill_date=%s";
public static final String TRADE_STATE_SUCCESS="SUCCESS";
public static final String REFUND_STATE_SUCCESS="SUCCESS";
}
@Value("${wechat-pay.merchant-id}")
private String merchantId;
@Value("${wechat-pay.merchant-serial-number}")
private String merchantSerialNumber;
@Value("${wechat-pay.api-v3-key}")
private String apiV3Key;
@Value("${wechat-pay.domain}")
private String domain;
@Value("${wechat-pay.appid}")
private String appId;
@Value("${wechat-pay.notify-url}")
private String notifyUrl;
@Autowired
private HttpClient httpClient;
@Autowired
private HttpClient httpClientWithNoSign;
支付
流程图
创建支付
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
private String createPay(OrderInfo orderInfo) throws Exception{
//请求构造
HttpPost httpPost=new HttpPost(domain+WechatPayConstant.CREATE_PAY_URL);
//请求体
//构造数据
HashMap<String,Object> reqData=new HashMap<>();
reqData.put("appid",appId);
reqData.put("mchid",merchantId);
reqData.put("description",orderInfo.getTitle());
reqData.put("out_trade_no",orderInfo.getOrderNo());
reqData.put("notify_url",notifyUrl+"/pay/order/order-signal");
HashMap<String,Integer> amount=new HashMap<>();
//单位是分
amount.put("total",orderInfo.getTotalFee());
reqData.put("amount",amount);
String jsonReqData=new Gson().toJson(reqData);
StringEntity entity=new StringEntity(jsonReqData,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
//请求头
httpPost.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpPost);
Map<String,String> dataMap=null;
try{
int statusCode=response.getStatusLine()
.getStatusCode();
//成功
if(statusCode==200){
String body=EntityUtils.toString(response.getEntity());
dataMap=new Gson().fromJson(body,HashMap.class);
}
//失败
else{
if(statusCode!=204){
String body=EntityUtils.toString(response.getEntity());
log.error(body);
return null;
}
}
}
finally{
response.close();
}
//返回二维码的地址
return dataMap.get("code_url");
}
支付通知
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
@PostMapping("/pay-signal")
public HashMap<String,String> paySignal(@RequestBody Map<String,Object> signalRes,HttpServletResponse response){
log.debug("收到微信回调");
try{
//TODO:验签
//用密文解密出明文
Map<String,String> resource=(Map<String,String>)signalRes.get("resource");
String ciphertext=resource.get("ciphertext");
String associatedData=resource.get("associated_data");
String nonce=resource.get("nonce");
String plainText=new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8)).decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);
//转换
HashMap<String,Object> data=new Gson().fromJson(plainText,HashMap.class);
//从数据库中查出对应的订单
QueryWrapper<OrderInfo> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("order_no",data.get("out_trade_no"));
OrderInfo orderInfo=orderInfoService.getOne(queryWrapper);
synchronized(this){
if(orderInfo.getOrderStatus()
.equals(OrderInfo.NOT_PAIED)){
//将订单设置为已支付状态
orderInfo.setOrderStatus(OrderInfo.PAIED);
//更新订单状态
orderInfoService.updateById(orderInfo);
//添加支付记录
PaymentInfo paymentInfo=new PaymentInfo();
paymentInfo.setOrderNo(orderInfo.getOrderNo());
paymentInfo.setCreateTime(new Date());
paymentInfo.setUpdateTime(new Date());
paymentInfo.setPayerTotal(orderInfo.getTotalFee());
paymentInfo.setPaymentType(PaymentInfo.WECHAT_PAY);
//微信支付中的支付编号
paymentInfo.setTransactionId((String)data.get("transaction_id"));
//交易类型(扫码 刷脸等等)
paymentInfo.setTradeType((String)data.get("trade_type"));
paymentInfo.setTradeState((String)data.get("trade_state"));
//存放全部数据(json)以备不时之需
paymentInfo.setContent(plainText);
paymentInfoService.save(paymentInfo);
log.info("订单{}的支付记录添加成功,支付记录id为{}.",orderInfo.getOrderNo(),paymentInfo.getId());
}
else{
log.debug("订单{}状态为{},回调处理退出.",orderInfo.getOrderNo(),OrderInfo.PAIED);
}
}
return null;
}
catch(Exception e){
log.error(e.getMessage());
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
HashMap<String,String> map=new HashMap<>();
map.put("code","FAIL");
map.put("message","支付失败");
return map;
}
}
查询支付
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml
private Map<String,Object> queryPay(String orderNo) throws Exception{
//请求构造
HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.QUERY_PAY_URL,orderNo,merchantId));
//请求头
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
Map<String,Object> dataMap=null;
try{
int statusCode=response.getStatusLine()
.getStatusCode();
String body=EntityUtils.toString(response.getEntity());
//成功
if(statusCode<400){
dataMap=new Gson().fromJson(body,HashMap.class);
log.info("查询订单支付{}成功",orderNo);
}
//失败
else{
log.error("查询订单支付{}失败,返回数据为{}.",orderNo,body);
throw new IOException("订单查询失败");
}
}
finally{
response.close();
}
return dataMap;
}
取消支付
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml
private boolean cancelPay(String orderNo){
//请求构造
HttpPost httpPost=new HttpPost(String.format(domain+WechatPayConstant.CANCEL_PAY_URL,orderNo));
//请求体
//构造数据
HashMap<String,Object> reqData=new HashMap<>();
reqData.put("mchid",merchantId);
String jsonReqData=new Gson().toJson(reqData);
StringEntity entity=new StringEntity(jsonReqData,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
//请求头
httpPost.setHeader("Accept","application/json");
CloseableHttpResponse response=null;
try{
//完成签名并执行请求
response=(CloseableHttpResponse)httpClient.execute(httpPost);
Map<String,String> dataMap=null;
int statusCode=response.getStatusLine()
.getStatusCode();
//成功
if(statusCode==204){
log.info("取消订单{}成功",orderNo);
return true;
}
//失败
else{
log.error("取消订单{}失败",orderNo);
return false;
}
}
catch(Exception e){
log.error("取消订单{}失败",orderNo);
return false;
}
finally{
try{
response.close();
}
catch(IOException e){
log.error("关闭response失败");
}
}
}
退款
创建退款
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml
private String createRefund(RefundInfo refundInfo) throws Exception{
//请求构造
HttpPost httpPost=new HttpPost(domain+WechatPayConstant.CREATE_REFUND_URL);
//请求体
//构造数据
HashMap<String,Object> reqData=new HashMap<>();
reqData.put("out_trade_no",refundInfo.getOrderNo());//订单编号
reqData.put("out_refund_no",refundInfo.getRefundNo());//退款单编号
reqData.put("reason",refundInfo.getReason());//退款原因
reqData.put("notify_url",notifyUrl+"/pay/order/refund-signal");//退款通知地址
HashMap<String,Object> amount=new HashMap<>();
amount.put("refund",refundInfo.getRefund());//退款金额
amount.put("total",refundInfo.getTotalFee());//原订单金额
amount.put("currency","CNY");//币种
reqData.put("amount",amount);
//将参数转换成json字符串
String jsonData=new Gson().toJson(reqData);
log.info("请求参数 ===> {}"+jsonData);
StringEntity entity=new StringEntity(jsonData,"utf-8");
httpPost.setEntity(entity);//将请求报文放入请求对象
//请求头
httpPost.setHeader("content-type","application/json");
httpPost.setHeader("Accept","application/json");//设置响应报文格式
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpPost);
try{
//解析响应结果
String bodyAsString=EntityUtils.toString(response.getEntity());
int statusCode=response.getStatusLine()
.getStatusCode();
if(statusCode==200){
log.info("成功, 退款返回结果 = "+bodyAsString);
return bodyAsString;
}
else{
if(statusCode!=204){
log.warn("退款异常:"+bodyAsString);
}
return null;
}
}
finally{
response.close();
}
}
退款通知
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml
@PostMapping("/refund-signal")
public HashMap<String,String> refundSignal(@RequestBody Map<String,Object> signalRes,HttpServletResponse response){
log.debug("收到微信回调");
try{
Map<String,String> resource=(Map<String,String>)signalRes.get("resource");
String ciphertext=resource.get("ciphertext");
String associatedData=resource.get("associated_data");
String nonce=resource.get("nonce");
//解密出明文
String plainText=new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8)).decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);
//转换
HashMap<String,Object> data=new Gson().fromJson(plainText,HashMap.class);
//从数据库中查出对应的退款信息
QueryWrapper<RefundInfo> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("refund_no",data.get("out_refund_no"));
RefundInfo refundInfo=refundInfoService.getOne(queryWrapper);
synchronized(this){
if(RefundInfo.REFUND_PROCESSING.equals(refundInfo.getRefundStatus())){
//将退款状态设置为退款成功
refundInfo.setRefundStatus(RefundInfo.REFUND_SUCCESS);
//存放全部数据(json)以备不时之需
refundInfo.setContentNotify(plainText);
//更新退款
refundInfoService.updateById(refundInfo);
//更新订单的状态
String orderNo=refundInfo.getOrderNo();
QueryWrapper<OrderInfo> orderInfoQueryWrapper=new QueryWrapper<>();
orderInfoQueryWrapper.eq("order_no",orderNo);
OrderInfo orderInfoToUpdate=new OrderInfo();
orderInfoToUpdate.setOrderStatus(OrderInfo.REFUNDED);
orderInfoService.update(orderInfoToUpdate,orderInfoQueryWrapper);
log.debug("退款成功,退款单为{},对应的订单为{}.",refundInfo.getRefundNo(),refundInfo.getOrderNo());
}
else{
log.debug("退款单{}状态为{},回调处理退出.",refundInfo.getRefundNo(),refundInfo.getRefundStatus());
}
}
HashMap<String,String> map=new HashMap<>();
map.put("code","SUCCESS");
map.put("message","成功");
return map;
}
catch(Exception e){
log.error(e.getMessage());
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
HashMap<String,String> map=new HashMap<>();
map.put("code","FAIL");
map.put("message","失败");
return map;
}
}
查询退款
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml
private Map<String,Object> queryRefund(String refundNo) throws Exception{
//请求构造
HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.QUERY_REFUND_URL,refundNo));
//请求头
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
Map<String,Object> dataMap=null;
try{
int statusCode=response.getStatusLine()
.getStatusCode();
String body=EntityUtils.toString(response.getEntity());
//成功
if(statusCode<400){
dataMap=new Gson().fromJson(body,HashMap.class);
log.info("查询退款{}成功",refundNo);
}
//失败
else{
log.warn("查询退款{}失败,返回数据为{}.",refundNo,body);
}
}
finally{
response.close();
}
return dataMap;
}
账单
查询交易账单(注重交易双方)下载URL
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_6.shtml
/**
* @param billDate 格式:yyyy-MM-dd 5.6日的账单记录的时间为05-06 9:00到05-07 9:00,并且在05-07 9:00后才能查到.
* @param billType ALL(支付成功且未退款+支付成功且退款) SUCCESS(支付成功且未退款) REFUND(支付成功且退款)
* @return 账单下载url(30s后则失效)
*/
private String queryTradeBillDownloadUrl(String billDate,String billType)throws Exception{
//请求构造
HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.TRADE_BILL_URL,billDate,billType));
//请求头
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
String downloadUrl=null;
try{
int statusCode=response.getStatusLine()
.getStatusCode();
String body=EntityUtils.toString(response.getEntity());
//成功
if(statusCode<400){
downloadUrl=(String)new Gson().fromJson(body,HashMap.class)
.get("download_url");
return downloadUrl;
}
//失败
else{
log.warn("查询downloadUrl失败,返回数据为{}.",body);
return null;
}
}
catch(Exception e){
log.warn("查询downloadUrl失败");
return null;
}
finally{
response.close();
}
}
查询流水账单(只注重商户)下载URL
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_7.shtml
/**
* @param billDate 格式:yyyy-MM-dd 5.6日的账单记录的时间为05-06 9:00到05-07 9:00,并且在05-07 9:00后才能查到.
* @return 账单下载url(30s后则失效)
*/
private String queryFlowBillDownloadUrl(String billDate) throws Exception{
//请求构造
HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.FLOW_BILL_URL,billDate));
//请求头
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
String downloadUrl=null;
try{
int statusCode=response.getStatusLine()
.getStatusCode();
String body=EntityUtils.toString(response.getEntity());
//成功
if(statusCode<400){
downloadUrl=(String)new Gson().fromJson(body,HashMap.class)
.get("download_url");
return downloadUrl;
}
//失败
else{
log.warn("查询downloadUrl失败,返回数据为{}.",body);
return null;
}
}
catch(Exception e){
log.warn("查询downloadUrl失败");
return null;
}
finally{
response.close();
}
}
获取账单(包括交易/流水)数据
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_8.shtml
/**
* 调用微信支付的接口只返回数据,然后我们把数据返回给前端,前端将其转化为excel.
* @param downloadUrl 交易/流水账单的下载URL(即上面两个方法的返回值)
*/
private String downloadBill(String downloadUrl)throws Exception{
//请求构造
HttpGet httpGet=new HttpGet(downloadUrl);
//请求头
httpGet.addHeader("Accept","application/json");
//完成签名并执行请求(注意:这里必须用httpClientWithNoSign,因为这个请求返回的数据是没有签名的.)
CloseableHttpResponse response=(CloseableHttpResponse)httpClientWithNoSign.execute(httpGet);
try{
String bodyAsString=EntityUtils.toString(response.getEntity());
int statusCode=response.getStatusLine()
.getStatusCode();
if(statusCode<400){
log.debug("下载账单成功");
//存放数据
return bodyAsString;
}
else{
log.warn("下载账单失败,返回结果为:{}",bodyAsString);
return null;
}
}
catch(Exception e){
log.warn("下载账单失败");
return null;
}
finally{
response.close();
}
}
完整下载账单的操作
@GetMapping("/download-trade-bill")
public AjaxResult downloadTradeBill(String billDate,@RequestParam(defaultValue="ALL") String billType)throws Exception{
//获取downloadUrl
String downloadUrl=queryTradeBillDownloadUrl(billDate,billType);
log.debug("downloadUrl:{}",downloadUrl);
//访问downloadUrl获取数据
String returnData=downloadBill(downloadUrl);
HashMap<String,String> resultMap=new HashMap<>();
resultMap.put("result",returnData);
return AjaxResult.success(AjaxResult.QUERY_SUCCESS,resultMap);
}
@GetMapping("/download-flow-bill")
public AjaxResult downloadFlowBill(String billDate) throws Exception{
//获取downloadUrl
String downloadUrl=queryFlowBillDownloadUrl(billDate);
log.debug("downloadUrl:{}",downloadUrl);
//访问downloadUrl获取数据
String returnData=downloadBill(downloadUrl);
HashMap<String,String> resultMap=new HashMap<>();
resultMap.put("result",returnData);
return AjaxResult.success(AjaxResult.QUERY_SUCCESS,resultMap);
}