Sfoglia il codice sorgente

fix(app):
1.bug修复

wzq 3 settimane fa
parent
commit
c7b9f243a9

+ 182 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/serverPay/PartnerAppPrepay.java

@@ -0,0 +1,182 @@
+package org.jeecg.modules.pay.serverPay;
+ 
+import com.google.gson.annotations.SerializedName;
+import okhttp3.*;
+ 
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.List;
+ 
+/**
+ * App下单
+ */
+public class PartnerAppPrepay {
+  private static String HOST = "https://api.mch.weixin.qq.com";
+  private static String METHOD = "POST";
+  private static String PATH = "/v3/pay/partner/transactions/app";
+ 
+  public PartnerAPIv3AppPrepayResponse run(PartnerAPIv3CommonPrepayRequest request) {
+    String uri = PATH;
+    String reqBody = WXPayUtility.toJson(request);
+ 
+    Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
+    reqBuilder.addHeader("Accept", "application/json");
+    reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
+    reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, reqBody));
+    reqBuilder.addHeader("Content-Type", "application/json");
+    RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
+    reqBuilder.method(METHOD, requestBody);
+    Request httpRequest = reqBuilder.build();
+ 
+    // 发送HTTP请求
+    OkHttpClient client = new OkHttpClient.Builder().build();
+    try (Response httpResponse = client.newCall(httpRequest).execute()) {
+      String respBody = WXPayUtility.extractBody(httpResponse);
+      if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
+        // 2XX 成功,验证应答签名
+        WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
+                                      httpResponse.headers(), respBody);
+        // 从HTTP应答报文构建返回数据
+        return WXPayUtility.fromJson(respBody, PartnerAPIv3AppPrepayResponse.class);
+      } else {
+        throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
+      }
+    } catch (IOException e) {
+      throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
+    }
+  }
+ 
+  private final String mchid;
+  private final String certificateSerialNo;
+  private final PrivateKey privateKey;
+  private final String wechatPayPublicKeyId;
+  private final PublicKey wechatPayPublicKey;
+ 
+  public PartnerAppPrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
+    this.mchid = mchid;
+    this.certificateSerialNo = certificateSerialNo;
+    this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
+    this.wechatPayPublicKeyId = wechatPayPublicKeyId;
+    this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
+  }
+ 
+  public static class PartnerSettleInfo {
+    @SerializedName("profit_sharing")
+    public Boolean profitSharing;
+  }
+ 
+  public static class CommonAmountInfo {
+    @SerializedName("total")
+    public Long total;
+ 
+    @SerializedName("currency")
+    public String currency;
+  }
+ 
+  public static class CommonSceneInfo {
+    @SerializedName("payer_client_ip")
+    public String payerClientIp;
+ 
+    @SerializedName("device_id")
+    public String deviceId;
+ 
+    @SerializedName("store_info")
+    public StoreInfo storeInfo;
+  }
+ 
+  public static class GoodsDetail {
+    @SerializedName("merchant_goods_id")
+    public String merchantGoodsId;
+ 
+    @SerializedName("wechatpay_goods_id")
+    public String wechatpayGoodsId;
+ 
+    @SerializedName("goods_name")
+    public String goodsName;
+ 
+    @SerializedName("quantity")
+    public Long quantity;
+ 
+    @SerializedName("unit_price")
+    public Long unitPrice;
+  }
+ 
+  public static class PartnerAPIv3AppPrepayResponse {
+    @SerializedName("prepay_id")
+    public String prepayId;
+  }
+ 
+  public static class PartnerAPIv3CommonPrepayRequest {
+    @SerializedName("sp_appid")
+    public String spAppid;
+ 
+    @SerializedName("sp_mchid")
+    public String spMchid;
+ 
+    @SerializedName("sub_appid")
+    public String subAppid;
+ 
+    @SerializedName("sub_mchid")
+    public String subMchid;
+ 
+    @SerializedName("description")
+    public String description;
+ 
+    @SerializedName("out_trade_no")
+    public String outTradeNo;
+ 
+    @SerializedName("time_expire")
+    public String timeExpire;
+ 
+    @SerializedName("attach")
+    public String attach;
+ 
+    @SerializedName("notify_url")
+    public String notifyUrl;
+ 
+    @SerializedName("goods_tag")
+    public String goodsTag;
+ 
+    @SerializedName("settle_info")
+    public PartnerSettleInfo settleInfo;
+ 
+    @SerializedName("support_fapiao")
+    public Boolean supportFapiao;
+ 
+    @SerializedName("amount")
+    public CommonAmountInfo amount;
+ 
+    @SerializedName("detail")
+    public CouponInfo detail;
+ 
+    @SerializedName("scene_info")
+    public CommonSceneInfo sceneInfo;
+  }
+ 
+  public static class CouponInfo {
+    @SerializedName("cost_price")
+    public Long costPrice;
+ 
+    @SerializedName("invoice_id")
+    public String invoiceId;
+ 
+    @SerializedName("goods_detail")
+    public List<GoodsDetail> goodsDetail;
+  }
+ 
+  public static class StoreInfo {
+    @SerializedName("id")
+    public String id;
+ 
+    @SerializedName("name")
+    public String name;
+ 
+    @SerializedName("area_code")
+    public String areaCode;
+ 
+    @SerializedName("address")
+    public String address;
+  }
+}

+ 441 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/serverPay/WXPayUtility.java

@@ -0,0 +1,441 @@
+package org.jeecg.modules.pay.serverPay;
+
+import com.google.gson.*;
+import com.google.gson.annotations.Expose;
+import com.wechat.pay.java.core.util.GsonUtil;
+import okhttp3.Headers;
+import okhttp3.Response;
+import okio.BufferedSource;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Objects;
+ 
+public class WXPayUtility {
+  private static final Gson gson = new GsonBuilder()
+      .disableHtmlEscaping()
+      .addSerializationExclusionStrategy(new ExclusionStrategy() {
+        @Override
+        public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+          final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+          return expose != null && !expose.serialize();
+        }
+ 
+        @Override
+        public boolean shouldSkipClass(Class<?> aClass) {
+          return false;
+        }
+      })
+      .addDeserializationExclusionStrategy(new ExclusionStrategy() {
+        @Override
+        public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+          final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+          return expose != null && !expose.deserialize();
+        }
+ 
+        @Override
+        public boolean shouldSkipClass(Class<?> aClass) {
+          return false;
+        }
+      })
+      .create();
+  private static final char[] SYMBOLS =
+      "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
+  private static final SecureRandom random = new SecureRandom();
+ 
+  /**
+   * 将 Object 转换为 JSON 字符串
+   */
+  public static String toJson(Object object) {
+    return gson.toJson(object);
+  }
+ 
+  /**
+   * 将 JSON 字符串解析为特定类型的实例
+   */
+  public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
+    return gson.fromJson(json, classOfT);
+  }
+ 
+  /**
+   * 从公私钥文件路径中读取文件内容
+   *
+   * @param keyPath 文件路径
+   * @return 文件内容
+   */
+  private static String readKeyStringFromPath(String keyPath) {
+    try {
+      return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+ 
+  /**
+   * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象
+   *
+   * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头
+   * @return PrivateKey 对象
+   */
+  public static PrivateKey loadPrivateKeyFromString(String keyString) {
+    try {
+      keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
+          .replace("-----END PRIVATE KEY-----", "")
+          .replaceAll("\\s+", "");
+      return KeyFactory.getInstance("RSA").generatePrivate(
+          new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+    } catch (NoSuchAlgorithmException e) {
+      throw new UnsupportedOperationException(e);
+    } catch (InvalidKeySpecException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+ 
+  /**
+   * 从 PKCS#8 格式的私钥文件中加载私钥
+   *
+   * @param keyPath 私钥文件路径
+   * @return PrivateKey 对象
+   */
+  public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
+    return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
+  }
+ 
+  /**
+   * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象
+   *
+   * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头
+   * @return PublicKey 对象
+   */
+  public static PublicKey loadPublicKeyFromString(String keyString) {
+    try {
+      keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
+          .replace("-----END PUBLIC KEY-----", "")
+          .replaceAll("\\s+", "");
+      return KeyFactory.getInstance("RSA").generatePublic(
+          new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+    } catch (NoSuchAlgorithmException e) {
+      throw new UnsupportedOperationException(e);
+    } catch (InvalidKeySpecException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+ 
+  /**
+   * 从 PKCS#8 格式的公钥文件中加载公钥
+   *
+   * @param keyPath 公钥文件路径
+   * @return PublicKey 对象
+   */
+  public static PublicKey loadPublicKeyFromPath(String keyPath) {
+    return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
+  }
+ 
+  /**
+   * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途
+   */
+  public static String createNonce(int length) {
+    char[] buf = new char[length];
+    for (int i = 0; i < length; ++i) {
+      buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
+    }
+    return new String(buf);
+  }
+ 
+  /**
+   * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密
+   *
+   * @param publicKey 加密用公钥对象
+   * @param plaintext 待加密明文
+   * @return 加密后密文
+   */
+  public static String encrypt(PublicKey publicKey, String plaintext) {
+    final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
+ 
+    try {
+      Cipher cipher = Cipher.getInstance(transformation);
+      cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+      return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
+    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+      throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
+    } catch (InvalidKeyException e) {
+      throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
+    } catch (BadPaddingException | IllegalBlockSizeException e) {
+      throw new IllegalArgumentException("Plaintext is too long", e);
+    }
+  }
+ 
+  /**
+   * 使用私钥按照指定算法进行签名
+   *
+   * @param message 待签名串
+   * @param algorithm 签名算法,如 SHA256withRSA
+   * @param privateKey 签名用私钥对象
+   * @return 签名结果
+   */
+  public static String sign(String message, String algorithm, PrivateKey privateKey) {
+    byte[] sign;
+    try {
+      Signature signature = Signature.getInstance(algorithm);
+      signature.initSign(privateKey);
+      signature.update(message.getBytes(StandardCharsets.UTF_8));
+      sign = signature.sign();
+    } catch (NoSuchAlgorithmException e) {
+      throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
+    } catch (InvalidKeyException e) {
+      throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
+    } catch (SignatureException e) {
+      throw new RuntimeException("An error occurred during the sign process.", e);
+    }
+    return Base64.getEncoder().encodeToString(sign);
+  }
+ 
+  /**
+   * 使用公钥按照特定算法验证签名
+   *
+   * @param message 待签名串
+   * @param signature 待验证的签名内容
+   * @param algorithm 签名算法,如:SHA256withRSA
+   * @param publicKey 验签用公钥对象
+   * @return 签名验证是否通过
+   */
+  public static boolean verify(String message, String signature, String algorithm,
+                               PublicKey publicKey) {
+    try {
+      Signature sign = Signature.getInstance(algorithm);
+      sign.initVerify(publicKey);
+      sign.update(message.getBytes(StandardCharsets.UTF_8));
+      return sign.verify(Base64.getDecoder().decode(signature));
+    } catch (SignatureException e) {
+      return false;
+    } catch (InvalidKeyException e) {
+      throw new IllegalArgumentException("verify uses an illegal publickey.", e);
+    } catch (NoSuchAlgorithmException e) {
+      throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
+    }
+  }
+ 
+  /**
+   * 根据微信支付APIv3请求签名规则构造 Authorization 签名
+   *
+   * @param mchid 商户号
+   * @param certificateSerialNo 商户API证书序列号
+   * @param privateKey 商户API证书私钥
+   * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE
+   * @param uri 请求接口的URL
+   * @param body 请求接口的Body
+   * @return 构造好的微信支付APIv3 Authorization 头
+   */
+  public static String buildAuthorization(String mchid, String certificateSerialNo,
+                                          PrivateKey privateKey,
+                                          String method, String uri, String body) {
+    String nonce = createNonce(32);
+    long timestamp = Instant.now().getEpochSecond();
+ 
+    String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
+        body == null ? "" : body);
+ 
+    String signature = sign(message, "SHA256withRSA", privateKey);
+ 
+    return String.format(
+        "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
+            "timestamp=\"%d\",serial_no=\"%s\"",
+        mchid, nonce, signature, timestamp, certificateSerialNo);
+  }
+ 
+  /**
+   * 对参数进行 URL 编码
+   *
+   * @param content 参数内容
+   * @return 编码后的内容
+   */
+  public static String urlEncode(String content) {
+    try {
+      return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+ 
+  /**
+   * 对参数Map进行 URL 编码,生成 QueryString
+   *
+   * @param params Query参数Map
+   * @return QueryString
+   */
+  public static String urlEncode(Map<String, Object> params) {
+    if (params == null || params.isEmpty()) {
+      return "";
+    }
+ 
+    int index = 0;
+    StringBuilder result = new StringBuilder();
+    for (Map.Entry<String, Object> entry : params.entrySet()) {
+      result.append(entry.getKey())
+          .append("=")
+          .append(urlEncode(entry.getValue().toString()));
+      index++;
+      if (index < params.size()) {
+        result.append("&");
+      }
+    }
+    return result.toString();
+  }
+ 
+  /**
+   * 从应答中提取 Body
+   *
+   * @param response HTTP 请求应答对象
+   * @return 应答中的Body内容,Body为空时返回空字符串
+   */
+  public static String extractBody(Response response) {
+    if (response.body() == null) {
+      return "";
+    }
+ 
+    try {
+      BufferedSource source = response.body().source();
+      return source.readUtf8();
+    } catch (IOException e) {
+      throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e);
+    }
+  }
+ 
+  /**
+   * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常
+   *
+   * @param wechatpayPublicKeyId 微信支付公钥ID
+   * @param wechatpayPublicKey 微信支付公钥对象
+   * @param headers 微信支付应答 Header 列表
+   * @param body 微信支付应答 Body
+   */
+  public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
+                                      Headers headers,
+                                      String body) {
+    String timestamp = headers.get("Wechatpay-Timestamp");
+    try {
+      Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
+      // 拒绝过期请求
+      if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
+        throw new IllegalArgumentException(
+            String.format("Validate http response,timestamp[%s] of httpResponse is expires, "
+                    + "request-id[%s]",
+                timestamp, headers.get("Request-ID")));
+      }
+    } catch (DateTimeException | NumberFormatException e) {
+      throw new IllegalArgumentException(
+          String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " +
+                  "request-id[%s]", timestamp,
+              headers.get("Request-ID")));
+    }
+    String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
+        body == null ? "" : body);
+    String serialNumber = headers.get("Wechatpay-Serial");
+    if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
+      throw new IllegalArgumentException(
+          String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId,
+              serialNumber));
+    }
+    String signature = headers.get("Wechatpay-Signature");
+ 
+    boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
+    if (!success) {
+      throw new IllegalArgumentException(
+          String.format("Validate response failed,the WechatPay signature is incorrect.%n"
+                  + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
+              headers.get("Request-ID"), headers, body));
+    }
+  }
+ 
+  /**
+   * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
+   */
+  public static class ApiException extends RuntimeException {
+    private static final long serialVersionUID = 2261086748874802175L;
+ 
+    private final int statusCode;
+    private final String body;
+    private final Headers headers;
+    private final String errorCode;
+    private final String errorMessage;
+ 
+    public ApiException(int statusCode, String body, Headers headers) {
+      super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, body, headers));
+      this.statusCode = statusCode;
+      this.body = body;
+      this.headers = headers;
+ 
+      if (body != null && !body.isEmpty()) {
+        JsonElement code;
+        JsonElement message;
+ 
+        try {
+          JsonObject jsonObject = GsonUtil.getGson().fromJson(body, JsonObject.class);
+          code = jsonObject.get("code");
+          message = jsonObject.get("message");
+        } catch (JsonSyntaxException ignored) {
+          code = null;
+          message = null;
+        }
+        this.errorCode = code == null ? null : code.getAsString();
+        this.errorMessage = message == null ? null : message.getAsString();
+      } else {
+        this.errorCode = null;
+        this.errorMessage = null;
+      }
+    }
+ 
+    /**
+     * 获取 HTTP 应答状态码
+     */
+    public int getStatusCode() {
+      return statusCode;
+    }
+ 
+    /**
+     * 获取 HTTP 应答包体内容
+     */
+    public String getBody() {
+      return body;
+    }
+ 
+    /**
+     * 获取 HTTP 应答 Header
+     */
+    public Headers getHeaders() {
+      return headers;
+    }
+ 
+    /**
+     * 获取 错误码 (错误应答中的 code 字段)
+     */
+    public String getErrorCode() {
+      return errorCode;
+    }
+ 
+    /**
+     * 获取 错误消息 (错误应答中的 message 字段)
+     */
+    public String getErrorMessage() {
+      return errorMessage;
+    }
+  }
+}

+ 107 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/serverPay/WechatSignUtil.java

@@ -0,0 +1,107 @@
+package org.jeecg.modules.pay.serverPay;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+import java.util.UUID;
+
+public class WechatSignUtil {
+
+    /**
+     * 参考网站 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml
+     * 计算签名值
+     *
+     * @param appId
+     * @param prepay_id
+     * @return
+     * @throws IOException
+     * @throws SignatureException
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeyException
+     */
+    public static WxUnifiedOrderVo getTokenJSAPI(String appId, String prepay_id, String privateKey) throws IOException, SignatureException, NoSuchAlgorithmException, InvalidKeyException {
+        // 获取随机字符串
+        String nonceStr = getNonceStr();
+        // 获取微信小程序支付package
+        String packagestr = "prepay_id=" + prepay_id;
+        long timestamp = System.currentTimeMillis() / 1000;
+        //签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值
+        String message = buildMessageTwo(appId, timestamp, nonceStr, packagestr);
+        //获取对应的签名
+        String signature = sign(message.getBytes("utf-8"),privateKey);
+        // 组装返回
+        WxUnifiedOrderVo vo = new WxUnifiedOrderVo();
+        vo.setAppId(appId);
+        vo.setTimeStamp(String.valueOf(timestamp));
+        vo.setNonceStr(nonceStr);
+        vo.setPackageStr(packagestr);
+        vo.setSignType("RSA");
+        vo.setPaySign(signature);
+        return vo;
+    }
+
+    /**
+     * 生成随机数
+     * @return
+     */
+    public static String getNonceStr(){
+        return UUID.randomUUID().toString()
+                .replaceAll("-", "")
+                .substring(0, 32);
+    }
+    /**
+     * 拼接参数
+     *
+     * @return
+     */
+
+    public static String buildMessageTwo(String appId, long timestamp, String nonceStr, String packag) {
+        return appId + "\n"
+                + timestamp + "\n"
+                + nonceStr + "\n"
+                + packag + "\n";
+    }
+    /**
+     * 生成签名
+     *
+     * @return
+     */
+    public static String sign(byte[] message,String privateKey) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
+        Signature sign = Signature.getInstance("SHA256withRSA"); //SHA256withRSA
+        sign.initSign(getPrivateKey(privateKey));
+        sign.update(message);
+        return Base64.getEncoder().encodeToString(sign.sign());
+    }
+    /**
+     * 获取私钥
+     * @param filename 私钥文件路径  (required)
+     * @return 私钥对象
+     */
+    public static PrivateKey getPrivateKey(String filename) throws IOException {
+        System.out.println("filename:" + filename);
+        filename = filename.replace("classpath:", "");
+        WechatSignUtil wechatSignUtil = new WechatSignUtil();
+        InputStream resourceAsStream = wechatSignUtil.getClass().getClassLoader().getResourceAsStream(filename);
+        byte[] bytes = new byte[0];
+        bytes = new byte[resourceAsStream.available()];
+        resourceAsStream.read(bytes);
+        String content = new String(bytes);
+//        String content = new String(Files.readAllBytes(Paths.get(resource.getPath())), "utf-8");
+        try {
+            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
+                    .replace("-----END PRIVATE KEY-----", "")
+                    .replaceAll("\\s+", "");
+
+            KeyFactory kf = KeyFactory.getInstance("RSA");
+            return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("当前Java环境不支持RSA", e);
+        } catch (InvalidKeySpecException e) {
+            throw new RuntimeException("无效的密钥格式");
+        }
+    }
+}
+

+ 53 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/serverPay/WxV3PayConfig.java

@@ -0,0 +1,53 @@
+package org.jeecg.modules.pay.serverPay;
+ 
+import org.springframework.stereotype.Component;
+ 
+import javax.annotation.PostConstruct;
+ 
+@Component
+public class WxV3PayConfig {
+    // 服务商AppId
+    private String appIdValue = "wxXXXXXXXXXXX";
+    public static String APP_ID= "wxXXXXXXXXXX";
+ 
+    // 服务商商户号
+    private String mchIdValue= "168XXXXXXX";
+    public static String Mch_ID= "168XXXXXX";
+ 
+    // 平台收款商户号
+    public static String smidVx= "208XXXXXXXXXXXX";
+    private  String smidVxValue= "2088XXXXXXXXXXX";
+ 
+    // 服务商商户私钥
+    private String apiV3KeyValue= "1skiujhXXXXXXXXXXXXXXXXXXX";
+    public static String apiV3Key= "1skiujhXXXXXXXXXXXXXXXXXX";
+    // 证书序列号
+ 
+    private String mchSerialNoValue= "5571XXXXXXXXXXXXXXXXXX";
+    public static String mchSerialNo= "5571XXXXXXXXXXXXXXXXXXXXX";
+ 
+    private String privateKeyPathValue= "/XXXXXXXXXXXXXX/apiclient_key.pem";
+    public static String privateKeyPath= "/XXXXXXXXXXXXX/apiclient_key.pem";
+ 
+    // 如果需要静态访问,可以使用 @PostConstruct 初始化静态变量
+    @PostConstruct
+    public void init() {
+        APP_ID = this.appIdValue;
+        APP_ID = this.appIdValue;
+        smidVx = this.smidVxValue;
+        apiV3Key = this.apiV3KeyValue;
+        mchSerialNo = this.mchSerialNoValue;
+        privateKeyPath = this.privateKeyPathValue; // WXPaySignatureCertificateUtil 会用到这个路径
+ 
+ 
+        // 可以在这里加一些非空检查
+        if (APP_ID == null || Mch_ID == null || apiV3Key == null || mchSerialNo == null || privateKeyPath == null) {
+             System.err.println("微信支付V3配置加载不完整,请检查配置文件!");
+             // 在实际应用中,这里可能需要抛出异常或采取其他错误处理措施
+        } else {
+             System.out.println("微信支付V3配置加载完成。");
+        }
+    }
+ 
+ 
+}

+ 13 - 0
national-motion-module-system/national-motion-system-start/src/main/resources/application-dev.yml

@@ -342,6 +342,19 @@ wx:
         token: #微信小程序消息服务器配置的token
         aesKey: #微信小程序消息服务器配置的EncodingAESKey
         msgDataFormat: JSON
+# 微信支付配置
+  pay:
+    #服务商id
+    mchId: 12
+    #证书
+    keyPath: apiclient_cert.p12
+    #服务商appid
+    appId: wxe12233
+    #支付回调通知地址
+    notifyUrl:
+    #服务商key的密钥
+    mchKey: 123444
+
 
 baidu:
   map: