diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 818d9cf..fc1edce 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -25,7 +25,7 @@ com.github.wechatpay-apiv3 wechatpay-java - 0.2.12 + 0.2.17 diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/PayApi.java b/ruoyi-admin/src/main/java/com/ruoyi/api/PayApi.java index 3d4ff01..ef6607c 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/api/PayApi.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/api/PayApi.java @@ -11,10 +11,15 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.member.domain.MemberOrder; import com.ruoyi.member.service.IMemberOrderService; 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.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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -34,8 +39,7 @@ import java.util.Map; public class PayApi { @Resource private WechatPayConfig wechatPayConfig; - @Resource - private WechatPayRequest wechatPayRequest; + @Autowired private IMemberOrderService memberOrderService; /** @@ -45,41 +49,39 @@ public class PayApi { */ @ApiOperation(value = "统一下单-统一接口", notes = "统一下单-统一接口") @GetMapping("/prepayment") - public Map transactions(String type, Long orderNo) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, IOException { + public PrepayWithRequestPaymentResponse transactions(String type, String orderNo) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException, IOException { LambdaQueryWrapper queryWrapper =new LambdaQueryWrapper<>(); queryWrapper.eq(MemberOrder::getOrderNo,orderNo).last("limit 1"); MemberOrder memberOrder = memberOrderService.getOne(queryWrapper); SysUser user = SecurityUtils.getLoginUser().getUser(); - // 统一参数封装 - Map params = new HashMap<>(8); - params.put("appid", wechatPayConfig.getAppId()); - params.put("mchid", wechatPayConfig.getMchId()); - params.put("description", "开通会员"); - params.put("out_trade_no", orderNo.toString()); - params.put("notify_url", wechatPayConfig.getNotifyUrl()); - Map amountMap = new HashMap<>(4); - BigDecimal goodsPrice = memberOrder.getGoodsPrice(); - // 金额单位为分 - amountMap.put("total", goodsPrice.multiply(new BigDecimal(100)).setScale(0, RoundingMode.HALF_UP).intValue()); - //人民币 - amountMap.put("currency", "CNY"); - params.put("amount", amountMap); - - // 场景信息 - Map sceneInfoMap = new HashMap<>(4); - // 客户端IP - sceneInfoMap.put("payer_client_ip", "127.0.0.1"); - // 商户端设备号(门店号或收银设备ID) - sceneInfoMap.put("device_id", "127.0.0.1"); - // 除H5与JSAPI有特殊参数外,其他的支付方式都一样 - Map payerMap = new HashMap<>(4); - payerMap.put("openid", user.getWxOpenId()); - params.put("payer", payerMap); - params.put("scene_info", sceneInfoMap); - String paramsStr = JSON.toJSONString(params); - String resStr = wechatPayRequest.wechatHttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi",paramsStr); - Map resMap = JSONObject.parseObject(resStr, new TypeReference>(){}); - return paySignMsg(resMap.get("prepay_id").toString(), wechatPayConfig.getAppId(),null); + // 使用微信支付公钥的RSA配置 + Config config = + new RSAPublicKeyConfig.Builder() + .merchantId(wechatPayConfig.getMchId()) + .privateKeyFromPath(wechatPayConfig.getPrivateKeyPath()) + .publicKeyFromPath(wechatPayConfig.getPublicKeyPath()) + .publicKeyId(wechatPayConfig.getPublicKeyId()) + .merchantSerialNumber(wechatPayConfig.getSerialNo()) + .apiV3Key(wechatPayConfig.getApiV3Key()) + .build(); + // 构建service + JsapiServiceExtension service = new JsapiServiceExtension .Builder().config(config).build(); + // request.setXxx(val)设置所需参数,具体参数可见Request定义 + PrepayRequest request = new PrepayRequest(); + Amount amount = new Amount(); + Payer payer = new Payer(); + amount.setTotal(100); + payer.setOpenid(user.getWxOpenId()); + request.setAmount(amount); + request.setAppid(wechatPayConfig.getAppId()); + request.setMchid(wechatPayConfig.getMchId()); + request.setDescription("开通会员"); + request.setNotifyUrl(wechatPayConfig.getNotifyUrl()); + request.setOutTradeNo(orderNo); + request.setPayer(payer); + // 调用下单方法,得到应答 + // response包含了调起支付所需的所有参数,可直接用于前端调起支付 + return service.prepayWithRequestPayment(request); } @ApiOperation(value = "支付回调", notes = "支付回调") @@ -95,56 +97,13 @@ public class PayApi { JSONObject decryptDataObj = JSONObject.parseObject(decryptData, JSONObject.class); String orderNo = decryptDataObj.get("out_trade_no").toString(); //调用业务系统 + memberOrderService.payCallback(orderNo); Map res = new HashMap<>(); res.put("code", "SUCCESS"); res.put("message", "成功"); 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 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 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; - } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayConfig.java b/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayConfig.java index 71d4927..554d94a 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayConfig.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayConfig.java @@ -64,7 +64,9 @@ public class WechatPayConfig { /** * 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; - - - /** - * 获取商户的私钥文件 - * @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(); - } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayRequest.java b/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayRequest.java deleted file mode 100644 index 252c57b..0000000 --- a/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayRequest.java +++ /dev/null @@ -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; - } -} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayUrlEnum.java b/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayUrlEnum.java deleted file mode 100644 index 9f13d73..0000000 --- a/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayUrlEnum.java +++ /dev/null @@ -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; -} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayValidator.java deleted file mode 100644 index c24565d..0000000 --- a/ruoyi-admin/src/main/java/com/ruoyi/payConfig/WechatPayValidator.java +++ /dev/null @@ -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 decryptFromResource(String resource,String apiV3Key,Integer type) { - - String msg = type==1?"支付成功":"退款成功"; - log.info(msg+",回调通知,密文解密"); - try { - //通知数据 - Map resourceMap = JSONObject.parseObject(resource, new TypeReference>() { - }); - //数据密文 - 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>(){}); - }catch (Exception e){ - throw new RuntimeException("回调参数,解密失败!"); - } - } -} diff --git a/ruoyi-admin/src/main/resources/pub_key.pem b/ruoyi-admin/src/main/resources/pub_key.pem new file mode 100644 index 0000000..e44d2a4 --- /dev/null +++ b/ruoyi-admin/src/main/resources/pub_key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnyLw8B8x3B+jWpCxiSeU +L+7NjFjx34Q44zlyRq6RXnz9xMFJXZq7HLv4YA9GcKyniF6aq7TvelKV8NUE8Ogd +fdOEZr2Gc/W15nz1RAo5Fu2K9q9IsZlQ4pM+HT9oqu4qVCrsPZEPbr11szQZjqtf +WdpZhRmiFzQRui0V0xzOQcd0GhicVhN5uMOekFqTALCq9JcWUl6Ti0fF4I4wH4kZ +iZVEkPTcXQACEUWRCH7hbRvluF1dEr87I/hdp98C1lo1UzQWHimVmFD8t0wHgnyO +qgSog353Hu59zkPB6qdraNNAvvOScOD8S/U57Nc4NXPXHOua1ZekwbE7lw/AvOcJ +zQIDAQAB +-----END PUBLIC KEY-----