发布通告功能
This commit is contained in:
parent
194ec6d12d
commit
8cf3786d42
@ -25,7 +25,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.wechatpay-apiv3</groupId>
|
<groupId>com.github.wechatpay-apiv3</groupId>
|
||||||
<artifactId>wechatpay-java</artifactId>
|
<artifactId>wechatpay-java</artifactId>
|
||||||
<version>0.2.12</version>
|
<version>0.2.17</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- spring-boot-devtools -->
|
<!-- spring-boot-devtools -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@ -11,10 +11,15 @@ import com.ruoyi.common.utils.StringUtils;
|
|||||||
import com.ruoyi.member.domain.MemberOrder;
|
import com.ruoyi.member.domain.MemberOrder;
|
||||||
import com.ruoyi.member.service.IMemberOrderService;
|
import com.ruoyi.member.service.IMemberOrderService;
|
||||||
import com.ruoyi.payConfig.WechatPayConfig;
|
import com.ruoyi.payConfig.WechatPayConfig;
|
||||||
import com.ruoyi.payConfig.WechatPayRequest;
|
|
||||||
import com.ruoyi.payConfig.WechatPayUrlEnum;
|
|
||||||
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
|
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
|
||||||
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
|
import com.wechat.pay.java.core.Config;
|
||||||
|
import com.wechat.pay.java.core.RSAPublicKeyConfig;
|
||||||
|
|
||||||
|
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
|
||||||
|
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
|
||||||
|
import com.wechat.pay.java.service.payments.jsapi.model.*;
|
||||||
|
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@ -34,8 +39,7 @@ import java.util.Map;
|
|||||||
public class PayApi {
|
public class PayApi {
|
||||||
@Resource
|
@Resource
|
||||||
private WechatPayConfig wechatPayConfig;
|
private WechatPayConfig wechatPayConfig;
|
||||||
@Resource
|
|
||||||
private WechatPayRequest wechatPayRequest;
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IMemberOrderService memberOrderService;
|
private IMemberOrderService memberOrderService;
|
||||||
/**
|
/**
|
||||||
@ -45,41 +49,39 @@ public class PayApi {
|
|||||||
*/
|
*/
|
||||||
@ApiOperation(value = "统一下单-统一接口", notes = "统一下单-统一接口")
|
@ApiOperation(value = "统一下单-统一接口", notes = "统一下单-统一接口")
|
||||||
@GetMapping("/prepayment")
|
@GetMapping("/prepayment")
|
||||||
public Map<String,Object> transactions(String type, Long orderNo) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, IOException {
|
public PrepayWithRequestPaymentResponse transactions(String type, String orderNo) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||||
LambdaQueryWrapper<MemberOrder> queryWrapper =new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<MemberOrder> queryWrapper =new LambdaQueryWrapper<>();
|
||||||
queryWrapper.eq(MemberOrder::getOrderNo,orderNo).last("limit 1");
|
queryWrapper.eq(MemberOrder::getOrderNo,orderNo).last("limit 1");
|
||||||
MemberOrder memberOrder = memberOrderService.getOne(queryWrapper);
|
MemberOrder memberOrder = memberOrderService.getOne(queryWrapper);
|
||||||
SysUser user = SecurityUtils.getLoginUser().getUser();
|
SysUser user = SecurityUtils.getLoginUser().getUser();
|
||||||
// 统一参数封装
|
// 使用微信支付公钥的RSA配置
|
||||||
Map<String, Object> params = new HashMap<>(8);
|
Config config =
|
||||||
params.put("appid", wechatPayConfig.getAppId());
|
new RSAPublicKeyConfig.Builder()
|
||||||
params.put("mchid", wechatPayConfig.getMchId());
|
.merchantId(wechatPayConfig.getMchId())
|
||||||
params.put("description", "开通会员");
|
.privateKeyFromPath(wechatPayConfig.getPrivateKeyPath())
|
||||||
params.put("out_trade_no", orderNo.toString());
|
.publicKeyFromPath(wechatPayConfig.getPublicKeyPath())
|
||||||
params.put("notify_url", wechatPayConfig.getNotifyUrl());
|
.publicKeyId(wechatPayConfig.getPublicKeyId())
|
||||||
Map<String, Object> amountMap = new HashMap<>(4);
|
.merchantSerialNumber(wechatPayConfig.getSerialNo())
|
||||||
BigDecimal goodsPrice = memberOrder.getGoodsPrice();
|
.apiV3Key(wechatPayConfig.getApiV3Key())
|
||||||
// 金额单位为分
|
.build();
|
||||||
amountMap.put("total", goodsPrice.multiply(new BigDecimal(100)).setScale(0, RoundingMode.HALF_UP).intValue());
|
// 构建service
|
||||||
//人民币
|
JsapiServiceExtension service = new JsapiServiceExtension .Builder().config(config).build();
|
||||||
amountMap.put("currency", "CNY");
|
// request.setXxx(val)设置所需参数,具体参数可见Request定义
|
||||||
params.put("amount", amountMap);
|
PrepayRequest request = new PrepayRequest();
|
||||||
|
Amount amount = new Amount();
|
||||||
// 场景信息
|
Payer payer = new Payer();
|
||||||
Map<String, Object> sceneInfoMap = new HashMap<>(4);
|
amount.setTotal(100);
|
||||||
// 客户端IP
|
payer.setOpenid(user.getWxOpenId());
|
||||||
sceneInfoMap.put("payer_client_ip", "127.0.0.1");
|
request.setAmount(amount);
|
||||||
// 商户端设备号(门店号或收银设备ID)
|
request.setAppid(wechatPayConfig.getAppId());
|
||||||
sceneInfoMap.put("device_id", "127.0.0.1");
|
request.setMchid(wechatPayConfig.getMchId());
|
||||||
// 除H5与JSAPI有特殊参数外,其他的支付方式都一样
|
request.setDescription("开通会员");
|
||||||
Map<String, Object> payerMap = new HashMap<>(4);
|
request.setNotifyUrl(wechatPayConfig.getNotifyUrl());
|
||||||
payerMap.put("openid", user.getWxOpenId());
|
request.setOutTradeNo(orderNo);
|
||||||
params.put("payer", payerMap);
|
request.setPayer(payer);
|
||||||
params.put("scene_info", sceneInfoMap);
|
// 调用下单方法,得到应答
|
||||||
String paramsStr = JSON.toJSONString(params);
|
// response包含了调起支付所需的所有参数,可直接用于前端调起支付
|
||||||
String resStr = wechatPayRequest.wechatHttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi",paramsStr);
|
return service.prepayWithRequestPayment(request);
|
||||||
Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>(){});
|
|
||||||
return paySignMsg(resMap.get("prepay_id").toString(), wechatPayConfig.getAppId(),null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation(value = "支付回调", notes = "支付回调")
|
@ApiOperation(value = "支付回调", notes = "支付回调")
|
||||||
@ -95,56 +97,13 @@ public class PayApi {
|
|||||||
JSONObject decryptDataObj = JSONObject.parseObject(decryptData, JSONObject.class);
|
JSONObject decryptDataObj = JSONObject.parseObject(decryptData, JSONObject.class);
|
||||||
String orderNo = decryptDataObj.get("out_trade_no").toString();
|
String orderNo = decryptDataObj.get("out_trade_no").toString();
|
||||||
//调用业务系统
|
//调用业务系统
|
||||||
|
memberOrderService.payCallback(orderNo);
|
||||||
Map<String, String> res = new HashMap<>();
|
Map<String, String> res = new HashMap<>();
|
||||||
res.put("code", "SUCCESS");
|
res.put("code", "SUCCESS");
|
||||||
res.put("message", "成功");
|
res.put("message", "成功");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
String buildMessage(String appId, String timestamp,String nonceStr,String prepay_id) {
|
|
||||||
|
|
||||||
return appId + "\n"
|
|
||||||
+ timestamp + "\n"
|
|
||||||
+ nonceStr + "\n"
|
|
||||||
+ prepay_id + "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
String sign(byte[] message,String privateKeyStr) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
|
|
||||||
//签名方式
|
|
||||||
Signature sign = Signature.getInstance("SHA256withRSA");
|
|
||||||
//私钥,通过MyPrivateKey来获取,这是个静态类可以接调用方法 ,需要的是_key.pem文件的绝对路径配上文件名
|
|
||||||
PrivateKey privateKey =null;
|
|
||||||
if (StringUtils.isNotEmpty(privateKeyStr)){
|
|
||||||
privateKey = PemUtil.loadPrivateKey(privateKeyStr);
|
|
||||||
}else {
|
|
||||||
privateKey = wechatPayConfig.getPrivateKey(wechatPayConfig.getKeyPemPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
sign.initSign(privateKey);
|
|
||||||
sign.update(message);
|
|
||||||
return Base64.getEncoder().encodeToString(sign.sign());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> paySignMsg(String prepayId,String appId,String privateKeyStr) throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
|
||||||
long timeMillis = System.currentTimeMillis();
|
|
||||||
String timeStamp = timeMillis/1000+"";
|
|
||||||
String nonceStr = timeMillis+"";
|
|
||||||
String packageStr = "prepay_id="+prepayId;
|
|
||||||
// 公共参数
|
|
||||||
Map<String, Object> resMap = new HashMap<>();
|
|
||||||
resMap.put("nonceStr",nonceStr);
|
|
||||||
resMap.put("timeStamp",timeStamp);
|
|
||||||
resMap.put("appId",appId);
|
|
||||||
resMap.put("package", packageStr);
|
|
||||||
// 使用字段appId、timeStamp、nonceStr、package进行签名
|
|
||||||
//从下往上依次生成
|
|
||||||
String message = buildMessage(appId, timeStamp, nonceStr, packageStr);
|
|
||||||
//签名
|
|
||||||
String paySign = sign(message.getBytes("utf-8"), privateKeyStr);
|
|
||||||
resMap.put("paySign", paySign);
|
|
||||||
resMap.put("signType", "RSA");
|
|
||||||
return resMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -64,7 +64,9 @@ public class WechatPayConfig {
|
|||||||
/**
|
/**
|
||||||
* API 证书中的 key.pem
|
* API 证书中的 key.pem
|
||||||
*/
|
*/
|
||||||
private String keyPemPath;
|
private String privateKeyPath;
|
||||||
|
private String publicKeyPath;
|
||||||
|
private String publicKeyId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 商户序列号
|
* 商户序列号
|
||||||
@ -76,87 +78,5 @@ public class WechatPayConfig {
|
|||||||
*/
|
*/
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取商户的私钥文件
|
|
||||||
* @param keyPemPath
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public PrivateKey getPrivateKey(String keyPemPath){
|
|
||||||
log.info("进入获取");
|
|
||||||
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath);
|
|
||||||
if(inputStream==null){
|
|
||||||
log.info("私钥文件不存在");
|
|
||||||
throw new RuntimeException("私钥文件不存在");
|
|
||||||
}
|
|
||||||
log.info("存在");
|
|
||||||
return PemUtil.loadPrivateKey(inputStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取证书管理器实例
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException {
|
|
||||||
|
|
||||||
log.info("获取证书管理器实例");
|
|
||||||
|
|
||||||
//获取商户私钥
|
|
||||||
PrivateKey privateKey = getPrivateKey(keyPemPath);
|
|
||||||
|
|
||||||
//私钥签名对象
|
|
||||||
PrivateKeySigner privateKeySigner = new PrivateKeySigner(serialNo, privateKey);
|
|
||||||
|
|
||||||
//身份认证对象
|
|
||||||
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
|
|
||||||
|
|
||||||
// 使用定时更新的签名验证器,不需要传入证书
|
|
||||||
CertificatesManager certificatesManager = CertificatesManager.getInstance();
|
|
||||||
certificatesManager.putMerchant(mchId,wechatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8));
|
|
||||||
|
|
||||||
return certificatesManager.getVerifier(mchId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取支付http请求对象
|
|
||||||
* @param verifier
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Bean(name = "wxPayClient")
|
|
||||||
public CloseableHttpClient getWxPayClient(Verifier verifier) {
|
|
||||||
|
|
||||||
//获取商户私钥
|
|
||||||
PrivateKey privateKey = getPrivateKey(keyPemPath);
|
|
||||||
|
|
||||||
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
|
|
||||||
.withMerchant(mchId, serialNo, privateKey)
|
|
||||||
.withValidator(new WechatPay2Validator(verifier));
|
|
||||||
|
|
||||||
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程
|
|
||||||
*/
|
|
||||||
@Bean(name = "wxPayNoSignClient")
|
|
||||||
public CloseableHttpClient getWxPayNoSignClient(){
|
|
||||||
|
|
||||||
//获取商户私钥
|
|
||||||
PrivateKey privateKey = getPrivateKey(keyPemPath);
|
|
||||||
|
|
||||||
//用于构造HttpClient
|
|
||||||
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
|
|
||||||
//设置商户信息
|
|
||||||
.withMerchant(mchId, serialNo, privateKey)
|
|
||||||
//无需进行签名验证、通过withValidator((response) -> true)实现
|
|
||||||
.withValidator((response) -> true);
|
|
||||||
|
|
||||||
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
package com.ruoyi.payConfig;
|
|
||||||
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.http.HttpEntity;
|
|
||||||
import org.apache.http.HttpStatus;
|
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
|
||||||
import org.apache.http.client.methods.HttpGet;
|
|
||||||
import org.apache.http.client.methods.HttpPost;
|
|
||||||
import org.apache.http.entity.StringEntity;
|
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
|
||||||
import org.apache.http.util.EntityUtils;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author:
|
|
||||||
* @Description:
|
|
||||||
**/
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
public class WechatPayRequest {
|
|
||||||
@Resource
|
|
||||||
private CloseableHttpClient wxPayClient;
|
|
||||||
public String wechatHttpGet(String url) {
|
|
||||||
try {
|
|
||||||
// 拼接请求参数
|
|
||||||
HttpGet httpGet = new HttpGet(url);
|
|
||||||
httpGet.setHeader("Accept", "application/json");
|
|
||||||
|
|
||||||
//完成签名并执行请求
|
|
||||||
CloseableHttpResponse response = wxPayClient.execute(httpGet);
|
|
||||||
|
|
||||||
return getResponseBody(response);
|
|
||||||
}catch (Exception e){
|
|
||||||
throw new RuntimeException(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String wechatHttpPost(String url,String paramsStr) {
|
|
||||||
try {
|
|
||||||
HttpPost httpPost = new HttpPost(url);
|
|
||||||
StringEntity entity = new StringEntity(paramsStr, "utf-8");
|
|
||||||
entity.setContentType("application/json");
|
|
||||||
httpPost.setEntity(entity);
|
|
||||||
httpPost.setHeader("Accept", "application/json");
|
|
||||||
|
|
||||||
CloseableHttpResponse response = wxPayClient.execute(httpPost);
|
|
||||||
return getResponseBody(response);
|
|
||||||
}catch (Exception e){
|
|
||||||
throw new RuntimeException(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getResponseBody(CloseableHttpResponse response) throws IOException {
|
|
||||||
|
|
||||||
//响应体
|
|
||||||
HttpEntity entity = response.getEntity();
|
|
||||||
String body = entity==null?"":EntityUtils.toString(entity);
|
|
||||||
//响应状态码
|
|
||||||
int statusCode = response.getStatusLine().getStatusCode();
|
|
||||||
|
|
||||||
//处理成功,204是,关闭订单时微信返回的正常状态码
|
|
||||||
if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) {
|
|
||||||
log.info("成功, 返回结果 = " + body);
|
|
||||||
} else {
|
|
||||||
String msg = "微信支付请求失败,响应码 = " + statusCode + ",返回结果 = " + body;
|
|
||||||
log.error(msg);
|
|
||||||
throw new RuntimeException(msg);
|
|
||||||
}
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
package com.ruoyi.payConfig;
|
|
||||||
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Getter
|
|
||||||
public enum WechatPayUrlEnum {
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* native
|
|
||||||
*/
|
|
||||||
NATIVE("native"),
|
|
||||||
/**
|
|
||||||
* app
|
|
||||||
*/
|
|
||||||
APP("app"),
|
|
||||||
/**
|
|
||||||
* h5
|
|
||||||
*/
|
|
||||||
H5("h5"),
|
|
||||||
/**
|
|
||||||
* jsapi
|
|
||||||
*/
|
|
||||||
JSAPI("jsapi"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 小程序jsapi
|
|
||||||
*/
|
|
||||||
SUB_JSAPI("sub_jsapi"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Native下单
|
|
||||||
*/
|
|
||||||
PAY_TRANSACTIONS("/pay/transactions/"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Native下单
|
|
||||||
*/
|
|
||||||
NATIVE_PAY_V2("/pay/unifiedorder"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询订单
|
|
||||||
*/
|
|
||||||
ORDER_QUERY_BY_NO("/pay/transactions/out-trade-no/"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭订单
|
|
||||||
*/
|
|
||||||
CLOSE_ORDER_BY_NO("/pay/transactions/out-trade-no/%s/close"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 申请退款
|
|
||||||
*/
|
|
||||||
DOMESTIC_REFUNDS("/refund/domestic/refunds"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询单笔退款
|
|
||||||
*/
|
|
||||||
DOMESTIC_REFUNDS_QUERY("/refund/domestic/refunds/"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 申请交易账单
|
|
||||||
*/
|
|
||||||
TRADE_BILLS("/bill/tradebill"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 申请资金账单
|
|
||||||
*/
|
|
||||||
FUND_FLOW_BILLS("/bill/fundflowbill");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 类型
|
|
||||||
*/
|
|
||||||
private final String type;
|
|
||||||
}
|
|
||||||
@ -1,150 +0,0 @@
|
|||||||
package com.ruoyi.payConfig;
|
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
|
||||||
import com.alibaba.fastjson2.TypeReference;
|
|
||||||
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
|
|
||||||
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.http.HttpEntity;
|
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
|
||||||
import org.apache.http.util.EntityUtils;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.time.DateTimeException;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author:
|
|
||||||
* @Description:
|
|
||||||
**/
|
|
||||||
@Slf4j
|
|
||||||
public class WechatPayValidator {
|
|
||||||
/**
|
|
||||||
* 应答超时时间,单位为分钟
|
|
||||||
*/
|
|
||||||
private static final long RESPONSE_EXPIRED_MINUTES = 5;
|
|
||||||
private final Verifier verifier;
|
|
||||||
private final String requestId;
|
|
||||||
private final String body;
|
|
||||||
|
|
||||||
|
|
||||||
public WechatPayValidator(Verifier verifier, String requestId, String body) {
|
|
||||||
this.verifier = verifier;
|
|
||||||
this.requestId = requestId;
|
|
||||||
this.body = body;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static IllegalArgumentException parameterError(String message, Object... args) {
|
|
||||||
message = String.format(message, args);
|
|
||||||
return new IllegalArgumentException("parameter error: " + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static IllegalArgumentException verifyFail(String message, Object... args) {
|
|
||||||
message = String.format(message, args);
|
|
||||||
return new IllegalArgumentException("signature verify fail: " + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public final boolean validate(HttpServletRequest request) {
|
|
||||||
try {
|
|
||||||
//处理请求参数
|
|
||||||
validateParameters(request);
|
|
||||||
|
|
||||||
//构造验签名串
|
|
||||||
String message = buildMessage(request);
|
|
||||||
|
|
||||||
String serial = request.getHeader(WECHAT_PAY_SERIAL);
|
|
||||||
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
|
|
||||||
|
|
||||||
//验签
|
|
||||||
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
|
|
||||||
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
|
|
||||||
serial, message, signature, requestId);
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
log.warn(e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateParameters(HttpServletRequest request) {
|
|
||||||
|
|
||||||
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
|
|
||||||
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
|
|
||||||
|
|
||||||
String header = null;
|
|
||||||
for (String headerName : headers) {
|
|
||||||
header = request.getHeader(headerName);
|
|
||||||
if (header == null) {
|
|
||||||
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//判断请求是否过期
|
|
||||||
String timestampStr = header;
|
|
||||||
try {
|
|
||||||
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
|
|
||||||
// 拒绝过期请求
|
|
||||||
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
|
|
||||||
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
|
|
||||||
}
|
|
||||||
} catch (DateTimeException | NumberFormatException e) {
|
|
||||||
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildMessage(HttpServletRequest request) {
|
|
||||||
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
|
|
||||||
String nonce = request.getHeader(WECHAT_PAY_NONCE);
|
|
||||||
return timestamp + "\n"
|
|
||||||
+ nonce + "\n"
|
|
||||||
+ body + "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getResponseBody(CloseableHttpResponse response) throws IOException {
|
|
||||||
HttpEntity entity = response.getEntity();
|
|
||||||
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对称解密,异步通知的加密数据
|
|
||||||
* @param resource 加密数据
|
|
||||||
* @param apiV3Key apiV3密钥
|
|
||||||
* @param type 1-支付,2-退款
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static Map<String, Object> decryptFromResource(String resource,String apiV3Key,Integer type) {
|
|
||||||
|
|
||||||
String msg = type==1?"支付成功":"退款成功";
|
|
||||||
log.info(msg+",回调通知,密文解密");
|
|
||||||
try {
|
|
||||||
//通知数据
|
|
||||||
Map<String, String> resourceMap = JSONObject.parseObject(resource, new TypeReference<Map<String, String>>() {
|
|
||||||
});
|
|
||||||
//数据密文
|
|
||||||
String ciphertext = resourceMap.get("ciphertext");
|
|
||||||
//随机串
|
|
||||||
String nonce = resourceMap.get("nonce");
|
|
||||||
//附加数据
|
|
||||||
String associatedData = resourceMap.get("associated_data");
|
|
||||||
|
|
||||||
log.info("密文: {}", ciphertext);
|
|
||||||
AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));
|
|
||||||
String resourceStr = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
|
|
||||||
nonce.getBytes(StandardCharsets.UTF_8),
|
|
||||||
ciphertext);
|
|
||||||
|
|
||||||
log.info(msg+",回调通知,解密结果 : {}", resourceStr);
|
|
||||||
return JSONObject.parseObject(resourceStr, new TypeReference<Map<String, Object>>(){});
|
|
||||||
}catch (Exception e){
|
|
||||||
throw new RuntimeException("回调参数,解密失败!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
ruoyi-admin/src/main/resources/pub_key.pem
Normal file
9
ruoyi-admin/src/main/resources/pub_key.pem
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnyLw8B8x3B+jWpCxiSeU
|
||||||
|
L+7NjFjx34Q44zlyRq6RXnz9xMFJXZq7HLv4YA9GcKyniF6aq7TvelKV8NUE8Ogd
|
||||||
|
fdOEZr2Gc/W15nz1RAo5Fu2K9q9IsZlQ4pM+HT9oqu4qVCrsPZEPbr11szQZjqtf
|
||||||
|
WdpZhRmiFzQRui0V0xzOQcd0GhicVhN5uMOekFqTALCq9JcWUl6Ti0fF4I4wH4kZ
|
||||||
|
iZVEkPTcXQACEUWRCH7hbRvluF1dEr87I/hdp98C1lo1UzQWHimVmFD8t0wHgnyO
|
||||||
|
qgSog353Hu59zkPB6qdraNNAvvOScOD8S/U57Nc4NXPXHOua1ZekwbE7lw/AvOcJ
|
||||||
|
zQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
Loading…
Reference in New Issue
Block a user