Przeglądaj źródła

feat(pay): 添加分账功能并优化支付流程

- 新增分账相关实体类和接口- 实现分账接收方添加功能
- 优化订单支付流程,支持微信支付
- 重构部分代码以提高可维护性
wzq 1 miesiąc temu
rodzic
commit
a15f5bbb64

+ 4 - 6
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/service/impl/OrderServiceImpl.java

@@ -540,9 +540,11 @@ public class OrderServiceImpl implements IOrderService {
 
                     // 查询库存
                     Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
+                    //使用人
+                    List<String> ids = Arrays.stream(createOrderForm.getFamilyIds().split(",")).collect(Collectors.toList());
                     log.info("学校场地预约商品库存数量:{}", stock);
                     // 缓存没有商品库存,查询数据库
-                    if (stock == null) {
+                    if (stock == null || stock == 0 || stock < ids.size()) {
                         AppSitePriceRules product = appSitePriceRulesMapper.selectById(productId);
                         if (Objects.isNull(product)) {
                             throw new JeecgBootException("订单提交失败,商品已下架");
@@ -555,14 +557,10 @@ public class OrderServiceImpl implements IOrderService {
                         stock = product.getTicketNum();
                         redisTemplate.opsForValue().set(stockKey, stock, 60 * 60 * 24, TimeUnit.SECONDS);
                     }
-                    //使用人
-                    List<String> ids = Arrays.stream(createOrderForm.getFamilyIds().split(",")).collect(Collectors.toList());
-
                     // 检查库存是否足够
                     if (stock < ids.size()) {
                         throw new JeecgBootException("订单提交失败,库存不足");
                     }
-
                     // 更新数据库中的库存数据
                     log.info("更新学校场地数据库中的库存数据:{}", stock - ids.size());
                     int row = appSitePriceRulesMapper.update(null, Wrappers.<AppSitePriceRules>lambdaUpdate()
@@ -1132,7 +1130,7 @@ public class OrderServiceImpl implements IOrderService {
         payForm.setOrderId(appOrder.getId()).setOrderCode(orderCode);
 
         //判断是否试听课(试听课不走订单)
-        if (ObjectUtil.isNotEmpty(appOrder.getOrderOrFree()) && appOrder.getOrderOrFree() == 1 && appOrder.getPrice().compareTo(BigDecimal.ZERO)== 0) {
+        if ((ObjectUtil.isNotEmpty(appOrder.getOrderOrFree()) && appOrder.getOrderOrFree() == 1) || appOrder.getPrice().compareTo(BigDecimal.ZERO)== 0) {
             payForm.setOrPayOrder(0);
         } else {
             Map<String, String> result = payment(appOrder.getId());

+ 2 - 1
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/config/WechatPayV3Utils.java

@@ -28,6 +28,7 @@ import java.nio.charset.StandardCharsets;
 import java.security.PrivateKey;
 import java.security.Signature;
 import java.util.Base64;
+import java.util.Locale;
 
 /**
  * @author wangzhiqiang
@@ -142,7 +143,7 @@ public class WechatPayV3Utils {
             HttpPost httpPost = new HttpPost(url);
             httpPost.addHeader("Accept", "application/json");
             httpPost.addHeader("Content-type", "application/json; charset=utf-8");
-            String WechatPaySerial = verifier.getValidCertificate().getSerialNumber().toString(16);
+            String WechatPaySerial = verifier.getValidCertificate().getSerialNumber().toString(16).toUpperCase(Locale.ROOT);
             httpPost.addHeader("Wechatpay-Serial", WechatPaySerial);
             httpPost.setEntity(new StringEntity(params.toJSONString(), StandardCharsets.UTF_8));
             CloseableHttpResponse response = httpClient.execute(httpPost);

+ 0 - 1
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/entity/Receiver.java

@@ -13,7 +13,6 @@ public class Receiver implements Serializable {
 
     private String type;          // MERCHANT_ID/PERSONAL_OPENID
     private String account;       // 接收方账号
-    private String name;       // 接收方名称
     private Integer amount;       // 分账金额(分)
     private String description;   // 描述
 }

+ 65 - 61
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/paytest/payController.java

@@ -1,11 +1,18 @@
 package org.jeecg.modules.pay.paytest;
 
 
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson2.JSONObject;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+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.util.EntityUtils;
 import org.jeecg.modules.pay.config.WechatConstants;
 import org.jeecg.modules.pay.config.WechatPayV3Utils;
 import org.jeecg.modules.pay.config.WechatUrlConstants;
@@ -13,23 +20,21 @@ import org.jeecg.modules.pay.entity.ProfitSharingRequest;
 import org.jeecg.modules.pay.entity.Receiver;
 import org.jeecg.modules.pay.entity.ReceiverAddForm;
 import org.jeecg.modules.pay.routing.WeChatProfitSharingService;
+import org.jeecg.modules.pay.serverPay.HttpClientUtil;
+import org.jeecg.modules.pay.serverPay.PayKit;
+import org.jeecg.modules.pay.serverPay.RsaKit;
 import org.jeecg.modules.system.app.entity.AppOrder;
 import org.jeecg.modules.system.app.service.IAppOrderService;
-import org.springframework.core.io.ClassPathResource;
 import org.springframework.web.bind.annotation.*;
 
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.*;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 
+
 @Slf4j
 @RestController
 @AllArgsConstructor
@@ -43,7 +48,7 @@ public class payController {
     private final WeChatProfitSharingService weChatProfitSharingService;
 
     @GetMapping(value = "/getPay")
-    public String getPay(String orderSn,int total , String description) {
+    public String getPay(String orderSn, int total, String description) {
         PayMerchantUtil payMerchantUtil = new PayMerchantUtil();
         try {
             return payMerchantUtil.requestwxChatPay(orderSn, total, description, "oYgFI91D00GpCwccdnKDR4KNxI4k");
@@ -82,6 +87,54 @@ public class payController {
 //        return null;
 //    }
 
+    @GetMapping(value = "/test666")
+    public String test() throws Exception {
+        //时间戳
+        long timestamp = System.currentTimeMillis() / 1000;
+        //随机字符串(用UUID去掉-就行)
+        String nonce = IdUtil.fastSimpleUUID().toUpperCase();
+        String body = "";
+        //拼接要签名的字符串
+        PrivateKey privateKey = PayKit.getPrivateKey("这里填商户平台私钥证书");
+        //拼接要签名的字符串
+        String orgSignText = "GET\n"
+                + "/v3/certificates\n"
+                + timestamp + "\n"
+                + nonce + "\n"
+                + body + "\n";
+        // 生成签名
+        String sign = RsaKit.encryptByPrivateKey(orgSignText, privateKey);
+        //要放在HttpHeader中的auth信息
+        // 获取商户证书序列号  这里填写公钥路径 也就是apiclient_cert.pem这个文件的路径
+        //证书序列号
+        X509Certificate certificate = PayKit.getCertificate(FileUtil.getInputStream("填你的apiclient_cert.pem"));
+        String serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
+        String auth = "WECHATPAY2-SHA256-RSA2048 "
+                + "mchid=\"商户号\",nonce_str=\""
+                + nonce + "\",timestamp=\"" + timestamp
+                + "\",serial_no=\"" + serialNo + "\",signature=\"" + sign + "\"";
+        String url = "https://api.mch.weixin.qq.com/v3/certificates";
+
+        HashMap<String, Object> tmap = new HashMap<String, Object>();
+        tmap.put("Authorization", auth);//tmap.put("token","tonken值");
+        tmap.put("Accept", "application/json");
+        tmap.put("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
+
+        HttpGet httpGet = new HttpGet(url);
+        httpGet.addHeader("Authorization", auth);
+        httpGet.addHeader("Accept", "application/json");
+        httpGet.addHeader("Content-type", "application/json; charset=utf-8");
+        httpGet.addHeader("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
+
+        String bodyAsString = HttpClientUtil.sendGetRequest(url, null);
+
+        log.info("微信返回的内容:" + bodyAsString);
+
+        String rs = HttpClientUtil.sendGetRequest(url, null);
+        System.out.println("获取平台证书" + rs);
+        return "success";
+    }
+
     @GetMapping(value = "/test1")
     public JSONObject test(@RequestParam("out_order_no") String out_order_no) throws Exception {
 
@@ -97,18 +150,11 @@ public class payController {
             String orgCode = appOrder.getOrgCode();
             //通过orgCode查询订单的分账发起方及接收方
         }
-        String str = "贵州帝侍天健康管理有限公司";
-        ClassPathResource classPathResource = new ClassPathResource("cert/apiclient_cert.pem");
-        InputStream certStream = classPathResource.getInputStream();
-        X509Certificate x509Certificate = getCertificate(certStream);
-
-        String name = rsaEncryptOAEP(str,x509Certificate);
         //查询当前订单的分账接收方列表
         for (int i = 0; i < 1; i++) {
             Receiver receiver = new Receiver();
             receiver.setType("MERCHANT_ID")
                     .setAccount("1723757626")
-                    .setName(name)
                     .setAmount(1)
                     .setDescription("分给帝释天");
             receivers.add(receiver);
@@ -131,51 +177,9 @@ public class payController {
         return null;
     }
 
-    /**
-     * 获取证书
-     *
-     * @param inputStream 证书文件
-     * @return {@link X509Certificate} 获取证书
-     */
-    public static X509Certificate getCertificate(InputStream inputStream) {
-        try {
-            CertificateFactory cf = CertificateFactory.getInstance("X509");
-            X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
-            cert.checkValidity();
-            return cert;
-        } catch (CertificateExpiredException e) {
-            throw new RuntimeException("证书已过期", e);
-        } catch (CertificateNotYetValidException e) {
-            throw new RuntimeException("证书尚未生效", e);
-        } catch (CertificateException e) {
-            throw new RuntimeException("无效的证书", e);
-        }
-    }
+    //获取 获取平台证书列表  测试成功
+    public static void main(String[] args) throws Exception {
 
-    /**
-     * 公钥加密   加密隐私信息数据
-     *
-     * @param data        待加密数据
-     * @param certificate 平台公钥证书
-     * @return 加密后的数据
-     * @throws Exception 异常信息
-     */
-    public static String rsaEncryptOAEP(String data, X509Certificate certificate) throws Exception {
-        try {
-            Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
-            cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
-            byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
-            byte[] cipherData = cipher.doFinal(dataByte);
-            // String s = new String(cipherData);
-
-            return java.util.Base64.getEncoder().encodeToString(cipherData);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
-        } catch (InvalidKeyException e) {
-            throw new IllegalArgumentException("无效的证书", e);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
-        }
     }
 }
  

+ 9 - 8
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/routing/WeChatProfitSharingService.java

@@ -13,11 +13,13 @@ import javax.crypto.BadPaddingException;
 import javax.crypto.Cipher;
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.NoSuchPaddingException;
+import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.*;
+import java.util.Base64;
 
 @Slf4j
 @Service
@@ -38,7 +40,7 @@ public class WeChatProfitSharingService {
         receiverAddForm.setName(name);
         JSONObject params = JSONObject.from(receiverAddForm);
         log.info("分账接收方:{}",receiverAddForm);
-        JSONObject body = wechatPayV3Utils.sendPost(WechatUrlConstants.PAY_V3_RECEIVERS_ADD,params);
+        JSONObject body = wechatPayV3Utils.profitSharingSendPost(WechatUrlConstants.PAY_V3_RECEIVERS_ADD,params);
         log.info("添加分账接收方结果:{}",body);
         return body;
     }
@@ -67,20 +69,19 @@ public class WeChatProfitSharingService {
     /**
      * 公钥加密   加密隐私信息数据
      *
-     * @param data        待加密数据
+
      * @param certificate 平台公钥证书
      * @return 加密后的数据
      * @throws Exception 异常信息
      */
-    public static String rsaEncryptOAEP(String data, X509Certificate certificate) throws Exception {
+    public static String rsaEncryptOAEP(String message, X509Certificate certificate)
+            throws IllegalBlockSizeException, IOException {
         try {
             Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
             cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
-            byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
-            byte[] cipherData = cipher.doFinal(dataByte);
-            // String s = new String(cipherData);
-
-            return java.util.Base64.getEncoder().encodeToString(cipherData);
+            byte[] data = message.getBytes(StandardCharsets.UTF_8);
+            byte[] cipherdata = cipher.doFinal(data);
+            return Base64.getEncoder().encodeToString(cipherdata);
         } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
             throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
         } catch (InvalidKeyException e) {

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

@@ -0,0 +1,354 @@
+package org.jeecg.modules.pay.serverPay;
+
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.ParseException;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+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.DefaultHttpClient;
+import org.apache.http.params.CoreConnectionPNames;
+import org.apache.http.protocol.HTTP;
+import org.apache.http.util.EntityUtils;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.HashMap;
+import java.util.Map;
+
+public class HttpClientUtil
+ {
+   public static final String SunX509 = "SunX509";
+   public static final String JKS = "JKS";
+   public static final String PKCS12 = "PKCS12";
+   public static final String TLS = "TLS";
+   
+   public static HttpURLConnection getHttpURLConnection(String strUrl)
+     throws IOException
+   {
+     URL url = new URL(strUrl);
+     HttpURLConnection httpURLConnection = (HttpURLConnection)url
+       .openConnection();
+     return httpURLConnection;
+   }
+   
+ 
+ 
+   /**
+	 * 发送HTTP_GET请求
+	 * 
+	 * @see 该方法会自动关闭连接,释放资源
+	 * @param reqURL
+	 *            请求地址(含参数)
+	 * @param decodeCharset
+	 *            解码字符集,解析响应数据时用之,其为null时默认采用UTF-8解码
+	 * @return 远程主机响应正文
+	 */
+	public static String sendGetRequest(String reqURL, String decodeCharset) {
+		long responseLength = 0; // 响应长度
+		String responseContent = null; // 响应内容
+		HttpClient httpClient = new DefaultHttpClient(); // 创建默认的httpClient实例
+		HttpGet httpGet = new HttpGet(reqURL); // 创建org.apache.http.client.methods.HttpGet
+		try {
+			HttpResponse response = httpClient.execute(httpGet); // 执行GET请求
+			HttpEntity entity = response.getEntity(); // 获取响应实体
+			if (null != entity) {
+				responseLength = entity.getContentLength();
+				responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset);
+				EntityUtils.consume(entity); // Consume response content
+			}
+			System.out.println("请求地址: " + httpGet.getURI());
+			System.out.println("响应状态: " + response.getStatusLine());
+			System.out.println("响应长度: " + responseLength);
+			System.out.println("响应内容: " + responseContent);
+		} catch (ClientProtocolException e) {
+			System.out.println("该异常通常是协议错误导致,比如构造HttpGet对象时传入的协议不对(将'http'写成'htp')或者服务器端返回的内容不符合HTTP协议要求等,堆栈信息如下");
+		} catch (ParseException e) {
+			System.out.println(e.getMessage());
+		} catch (IOException e) {
+			System.out.println("该异常通常是网络原因引起的,如HTTP服务器未启动等,堆栈信息如下");
+		} finally {
+			httpClient.getConnectionManager().shutdown(); // 关闭连接,释放资源
+		}
+		return responseContent;
+	}
+ 
+	/**
+	 * 发送HTTP_POST请求
+	 * 
+	 * @see 该方法为
+	 *      <code>sendPostRequest(String,String,boolean,String,String)</code>
+	 *      的简化方法
+	 * @see 该方法在对请求数据的编码和响应数据的解码时,所采用的字符集均为UTF-8
+	 * @see 当<code>isEncoder=true</code>时,其会自动对<code>sendData</code>中的[中文][|][
+	 *      ]等特殊字符进行<code>URLEncoder.encode(string,"UTF-8")</code>
+	 * @param isEncoder
+	 *            用于指明请求数据是否需要UTF-8编码,true为需要
+	 */
+	public static String sendPostRequest(String reqURL, String sendData, boolean isEncoder) {
+		return sendPostRequest(reqURL, sendData, isEncoder, null, null,"application/json");
+	}
+	
+	/**
+	 * 发送HTTP_POST请求
+	 * 
+	 * @see 该方法会自动关闭连接,释放资源
+	 * @see 当<code>isEncoder=true</code>时,其会自动对<code>sendData</code>中的[中文][|][
+	 *      ]等特殊字符进行<code>URLEncoder.encode(string,encodeCharset)</code>
+	 * @param reqURL
+	 *            请求地址
+	 * @param sendData
+	 *            请求参数,若有多个参数则应拼接成param11=value11¶m22=value22¶m33=value33的形式后,
+	 *            传入该参数中
+	 * @param isEncoder
+	 *            请求数据是否需要encodeCharset编码,true为需要
+	 * @param encodeCharset
+	 *            编码字符集,编码请求数据时用之,其为null时默认采用UTF-8解码
+	 * @param decodeCharset
+	 *            解码字符集,解析响应数据时用之,其为null时默认采用UTF-8解码
+	 * @return 远程主机响应正文
+	 */
+	public static String sendPostRequest(String reqURL, String sendData, boolean isEncoder, String encodeCharset,
+			String decodeCharset,String contentType) {
+		String responseContent = null;
+		HttpClient httpClient = new DefaultHttpClient();
+ 
+		HttpPost httpPost = new HttpPost(reqURL);
+		// httpPost.setHeader(HTTP.CONTENT_TYPE,
+		// "application/x-www-form-urlencoded; charset=UTF-8");
+		httpPost.setHeader(HTTP.CONTENT_TYPE, contentType);
+		try {
+			if (isEncoder) {
+				httpPost.setEntity(new StringEntity(sendData, encodeCharset == null ? "UTF-8" : encodeCharset));
+			} else {
+				httpPost.setEntity(new StringEntity(sendData));
+			}
+			// 设置请求超时时间
+			httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 5000);
+			HttpResponse response = httpClient.execute(httpPost);
+			HttpEntity entity = response.getEntity();
+			if (null != entity) {
+				responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset);
+				EntityUtils.consume(entity);
+			}
+		} catch (Exception e) {
+			System.out.println("与[" + reqURL + "]通信过程中发生异常,堆栈信息如下");
+			e.printStackTrace();
+		} finally {
+			httpClient.getConnectionManager().shutdown();
+		}
+		return responseContent;
+	}
+ 
+ 
+   public static HttpsURLConnection getHttpsURLConnection(String strUrl)
+     throws IOException
+   {
+     URL url = new URL(strUrl);
+     HttpsURLConnection httpsURLConnection = (HttpsURLConnection)url
+       .openConnection();
+     return httpsURLConnection;
+   }
+   
+ 
+ 
+ 
+   public static String getURL(String strUrl)
+   {
+     if (strUrl != null) {
+       int indexOf = strUrl.indexOf("?");
+       if (-1 != indexOf) {
+         return strUrl.substring(0, indexOf);
+       }
+       
+       return strUrl;
+     }
+     
+     return strUrl;
+   }
+ 
+ 
+ 
+   public static String getQueryString(String strUrl)
+   {
+     if (strUrl != null) {
+       int indexOf = strUrl.indexOf("?");
+       if (-1 != indexOf) {
+         return strUrl.substring(indexOf + 1, strUrl.length());
+       }
+       
+       return "";
+     }
+     
+     return strUrl;
+   }
+   
+ 
+ 
+   public static Map queryString2Map(String queryString)
+   {
+     if ((queryString == null) || ("".equals(queryString))) {
+       return null;
+     }
+     
+     Map m = new HashMap();
+     String[] strArray = queryString.split("&");
+     for (int index = 0; index < strArray.length; index++) {
+       String pair = strArray[index];
+       putMapByPair(pair, m);
+     }
+     
+     return m;
+   }
+   
+ 
+ 
+   public static void putMapByPair(String pair, Map m)
+   {
+     if ((pair == null) || ("".equals(pair))) {
+       return;
+     }
+     
+     int indexOf = pair.indexOf("=");
+     if (-1 != indexOf) {
+       String k = pair.substring(0, indexOf);
+       String v = pair.substring(indexOf + 1, pair.length());
+       if ((k != null) && (!"".equals(k))) {
+         m.put(k, v);
+       }
+     } else {
+       m.put(pair, "");
+     }
+   }
+   
+ 
+ 
+ 
+   public static String bufferedReader2String(BufferedReader reader)
+     throws IOException
+   {
+     StringBuffer buf = new StringBuffer();
+     String line = null;
+     while ((line = reader.readLine()) != null) {
+       buf.append(line);
+       buf.append("\r\n");
+     }
+     
+     return buf.toString();
+   }
+   
+ 
+ 
+ 
+   public static void doOutput(OutputStream out, byte[] data, int len)
+     throws IOException
+   {
+     int dataLen = data.length;
+     int off = 0;
+     while (off < dataLen) {
+       if (len >= dataLen) {
+         out.write(data, off, dataLen);
+       } else {
+         out.write(data, off, len);
+       }
+       
+ 
+       out.flush();
+       
+       off += len;
+       
+       dataLen -= len;
+     }
+   }
+   
+ 
+ 
+ 
+   public static SSLContext getSSLContext(FileInputStream trustFileInputStream, String trustPasswd, FileInputStream keyFileInputStream, String keyPasswd)
+     throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException, KeyManagementException
+   {
+     TrustManagerFactory tmf =
+       TrustManagerFactory.getInstance("SunX509");
+     KeyStore trustKeyStore = KeyStore.getInstance("JKS");
+     trustKeyStore.load(trustFileInputStream, 
+       str2CharArray(trustPasswd));
+     tmf.init(trustKeyStore);
+     
+     char[] kp = str2CharArray(keyPasswd);
+     KeyManagerFactory kmf =
+       KeyManagerFactory.getInstance("SunX509");
+     KeyStore ks = KeyStore.getInstance("PKCS12");
+     ks.load(keyFileInputStream, kp);
+     kmf.init(ks, kp);
+     
+     SecureRandom rand = new SecureRandom();
+     SSLContext ctx = SSLContext.getInstance("TLS");
+     ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), rand);
+     
+     return ctx;
+   }
+   
+ 
+ 
+ 
+ 
+ 
+   public static Certificate getCertificate(File cafile)
+     throws CertificateException, IOException
+   {
+     CertificateFactory cf = CertificateFactory.getInstance("X.509");
+     FileInputStream in = new FileInputStream(cafile);
+     Certificate cert = cf.generateCertificate(in);
+     in.close();
+     return cert;
+   }
+   
+ 
+ 
+ 
+ 
+ 
+   public static char[] str2CharArray(String str)
+   {
+     if (str == null) {
+       return null;
+     }
+     return str.toCharArray();
+   }
+   
+ 
+ 
+ 
+ 
+ 
+ 
+   public static void storeCACert(Certificate cert, String alias, String password, OutputStream out)
+     throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException
+   {
+     KeyStore ks = KeyStore.getInstance("JKS");
+     
+     ks.load(null, null);
+     
+     ks.setCertificateEntry(alias, cert);
+     
+ 
+     ks.store(out, str2CharArray(password));
+   }
+   
+   public static InputStream String2Inputstream(String str)
+   {
+     return new ByteArrayInputStream(str.getBytes());
+   }
+ }

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

@@ -0,0 +1,269 @@
+package org.jeecg.modules.pay.serverPay;
+
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.*;
+import java.util.*;
+
+public class PayKit {
+ 
+    /**
+     * 获取证书
+     *
+     * @param inputStream 证书文件
+     * @return {@link X509Certificate} 获取证书
+     */
+    public static X509Certificate getCertificate(InputStream inputStream) {
+        try {
+            CertificateFactory cf = CertificateFactory.getInstance("X509");
+            X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
+            cert.checkValidity();
+            return cert;
+        } catch (CertificateExpiredException e) {
+            throw new RuntimeException("证书已过期", e);
+        } catch (CertificateNotYetValidException e) {
+            throw new RuntimeException("证书尚未生效", e);
+        } catch (CertificateException e) {
+            throw new RuntimeException("无效的证书", e);
+        }
+    }
+    /**
+     * 简化的UUID,去掉了横线,使用性能更好的 ThreadLocalRandom 生成UUID
+     *
+     * @return 简化的 UUID,去掉了横线
+     */
+    public static String generateStr() {
+        return IdUtil.fastSimpleUUID();
+    }
+ 
+    /**
+     * 构造签名串
+     *
+     * @param method    {@link RequestMethod} GET,POST,PUT等
+     * @param url       请求接口 /v3/certificates
+     * @param timestamp 获取发起请求时的系统当前时间戳
+     * @param nonceStr  随机字符串
+     * @param body      请求报文主体
+     * @return 待签名字符串
+     */
+    public static String buildSignMessage(String method, String url, String timestamp, String nonceStr, String body) {
+        ArrayList<String> arrayList = new ArrayList<>();
+        arrayList.add(method);
+        arrayList.add(url);
+        arrayList.add(String.valueOf(timestamp));
+        arrayList.add(nonceStr);
+        arrayList.add(body);
+        return buildSignMessage(arrayList);
+    }
+    /**
+     * 构造签名串
+     *
+     * @param signMessage 待签名的参数
+     * @return 构造后带待签名串
+     */
+    public static String buildSignMessage(ArrayList<String> signMessage) {
+        if (signMessage == null || signMessage.size() <= 0) {
+            return null;
+        }
+        StringBuilder sbf = new StringBuilder();
+        for (String str : signMessage) {
+            sbf.append(str).append("\n");
+        }
+        System.out.println("待签名字符串内容:" + sbf.toString());
+        System.out.println("待签名字符串长度:" + sbf.toString().length());
+        return sbf.toString();
+    }
+    /**
+     * v3 接口创建签名
+     *
+     * @param signMessage 待签名的参数
+     * @param keyPath     key.pem 证书路径
+     * @return 生成 v3 签名
+     * @throws Exception 异常信息
+     */
+    public static String createSign(String signMessage, String keyPath) throws Exception {
+        if (StrUtil.isEmpty(signMessage)) {
+            return null;
+        }
+        // 获取商户私钥
+        PrivateKey privateKey = PayKit.getPrivateKey(keyPath);
+        // 生成签名
+        return RsaKit.encryptByPrivateKey(signMessage, privateKey);
+    }
+ 
+    /**
+     * 获取商户私钥
+     *
+     * @param keyPath 商户私钥证书路径
+     * @return {@link PrivateKey} 商户私钥
+     * @throws Exception 异常信息
+     */
+    public static PrivateKey getPrivateKey(String keyPath) throws Exception {
+        String originalKey = FileUtil.readUtf8String(keyPath);
+        String privateKey = originalKey
+                .replace("-----BEGIN PRIVATE KEY-----", "")
+                .replace("-----END PRIVATE KEY-----", "")
+                .replaceAll("\\s+", "");
+ 
+        return RsaKit.loadPrivateKey(privateKey);
+    }
+ 
+    /**
+     * 获取授权认证信息
+     *
+     * @param mchId     商户号
+     * @param serialNo  商户API证书序列号
+     * @param nonceStr  请求随机串
+     * @param timestamp 时间戳
+     * @param signature 签名值
+     * @param authType  认证类型,目前为WECHATPAY2-SHA256-RSA2048
+     * @return 请求头 Authorization
+     */
+    public static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) {
+        Map<String, String> params = new HashMap<>(5);
+        params.put("mchid", mchId);
+        params.put("serial_no", serialNo);
+        params.put("nonce_str", nonceStr);
+        params.put("timestamp", timestamp);
+        params.put("signature", signature);
+        return authType.concat(" ").concat(createLinkString(params, ",", false, true));
+    }
+    /**
+     * @param params 需要排序并参与字符拼接的参数组
+     * @param encode 是否进行URLEncoder
+     * @return 拼接后字符串
+     */
+    public static String createLinkString(Map<String, String> params, boolean encode) {
+        return createLinkString(params, "&", encode);
+    }
+    /**
+     * @param params  需要排序并参与字符拼接的参数组
+     * @param connStr 连接符号
+     * @param encode  是否进行URLEncoder
+     * @return 拼接后字符串
+     */
+    public static String createLinkString(Map<String, String> params, String connStr, boolean encode) {
+        return createLinkString(params, connStr, encode, false);
+    }
+ 
+    public static String createLinkString(Map<String, String> params, String connStr, boolean encode, boolean quotes) {
+        List<String> keys = new ArrayList<>(params.keySet());
+        Collections.sort(keys);
+        StringBuilder content = new StringBuilder();
+        for (int i = 0; i < keys.size(); i++) {
+            String key = keys.get(i);
+            String value = params.get(key);
+            // 拼接时,不包括最后一个&字符
+            if (i == keys.size() - 1) {
+                if (quotes) {
+                    content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"');
+                } else {
+                    content.append(key).append("=").append(encode ? urlEncode(value) : value);
+                }
+            } else {
+                if (quotes) {
+                    content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr);
+                } else {
+                    content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr);
+                }
+            }
+        }
+        return content.toString();
+    }
+ 
+    /**
+     * URL 编码
+     *
+     * @param src 需要编码的字符串
+     * @return 编码后的字符串
+     */
+    public static String urlEncode(String src) {
+        try {
+            return URLEncoder.encode(src, CharsetUtil.UTF_8).replace("+", "%20");
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+ 
+    /**
+     * 构造签名串
+     *
+     * @param timestamp 应答时间戳
+     * @param nonceStr  应答随机串
+     * @param body      应答报文主体
+     * @return 应答待签名字符串
+     */
+    public static String buildSignMessage(String timestamp, String nonceStr, String body) {
+        ArrayList<String> arrayList = new ArrayList<>();
+        arrayList.add(timestamp);
+        arrayList.add(nonceStr);
+        arrayList.add(body);
+        return buildSignMessage(arrayList);
+    }
+ 
+    /**
+     * 公钥加密
+     *
+     * @param data        待加密数据
+     * @param certificate 平台公钥证书
+     * @return 加密后的数据
+     * @throws Exception 异常信息
+     */
+    public static String rsaEncryptOAEP(String data, X509Certificate certificate) throws Exception {
+        try {
+            Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
+            cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
+ 
+            byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
+            byte[] cipherData = cipher.doFinal(dataByte);
+            return Base64.encode(cipherData);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("无效的证书", e);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
+        }
+    }
+ 
+    /**
+     * 私钥解密
+     *
+     * @param cipherText 加密字符
+     * @param privateKey 私钥
+     * @return 解密后的数据
+     * @throws Exception 异常信息
+     */
+    public static String rsaDecryptOAEP(String cipherText, PrivateKey privateKey) throws Exception {
+        try {
+            Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
+            cipher.init(Cipher.DECRYPT_MODE, privateKey);
+            byte[] data = Base64.decode(cipherText);
+            return new String(cipher.doFinal(data), StandardCharsets.UTF_8);
+        } catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
+            throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("无效的私钥", e);
+        } catch (BadPaddingException | IllegalBlockSizeException e) {
+            throw new BadPaddingException("解密失败");
+        }
+    }
+}

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

@@ -0,0 +1,74 @@
+package org.jeecg.modules.pay.serverPay;
+
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.util.StrUtil;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+public class RsaKit {
+ 
+    /**
+     * 加密算法RSA
+     */
+    private static final String KEY_ALGORITHM = "RSA";
+ 
+    /**
+     * 私钥签名
+     *
+     * @param data       需要加密的数据
+     * @param privateKey 私钥
+     * @return 加密后的数据
+     * @throws Exception 异常信息
+     */
+    public static String encryptByPrivateKey(String data, PrivateKey privateKey) throws Exception {
+        java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA");
+        signature.initSign(privateKey);
+        signature.update(data.getBytes(StandardCharsets.UTF_8));
+        byte[] signed = signature.sign();
+        return StrUtil.str(Base64.encode(signed));
+    }
+ 
+    /**
+     * 从字符串中加载私钥<br>
+     * 加载时使用的是PKCS8EncodedKeySpec(PKCS#8编码的Key指令)。
+     *
+     * @param privateKeyStr 私钥
+     * @return {@link PrivateKey}
+     * @throws Exception 异常信息
+     */
+    public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception {
+        try {
+            byte[] buffer = Base64.decode(privateKeyStr);
+            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
+            KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
+            return keyFactory.generatePrivate(keySpec);
+        } catch (NoSuchAlgorithmException e) {
+            throw new Exception("无此算法");
+        } catch (InvalidKeySpecException e) {
+            throw new Exception("私钥非法");
+        } catch (NullPointerException e) {
+            throw new Exception("私钥数据为空");
+        }
+    }
+    /**
+     * 公钥验证签名
+     *
+     * @param data      需要加密的数据
+     * @param sign      签名
+     * @param publicKey 公钥
+     * @return 验证结果
+     * @throws Exception 异常信息
+     */
+    public static boolean checkByPublicKey(String data, String sign, PublicKey publicKey) throws Exception {
+        java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA");
+        signature.initVerify(publicKey);
+        signature.update(data.getBytes(StandardCharsets.UTF_8));
+        return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8)));
+    }
+}

+ 14 - 9
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/AppCoursesVerificationRecordServiceImpl.java

@@ -5,6 +5,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.shiro.SecurityUtils;
 import org.jeecg.common.constant.CommonConstant;
 import org.jeecg.common.exception.JeecgBootException;
@@ -34,6 +35,7 @@ import java.util.Objects;
  * @Version: V1.0
  */
 @Service
+@Slf4j
 public class AppCoursesVerificationRecordServiceImpl extends ServiceImpl<AppCoursesVerificationRecordMapper, AppCoursesVerificationRecord> implements IAppCoursesVerificationRecordService {
 
     @Resource
@@ -76,17 +78,20 @@ public class AppCoursesVerificationRecordServiceImpl extends ServiceImpl<AppCour
                 if (ObjectUtil.isNotEmpty(orderProInfo)){
                     orderProInfo.setOrderStatus(CommonConstant.ORDER_STATUS_2);
                     appOrderProInfoMapper.updateById(orderProInfo);
-                }
-                 String orderId = orderProInfo.getOrderId();
-                AppOrder appOrder = appOrderMapper.selectById(orderId);
-                if (ObjectUtil.isNotEmpty(appOrder)){
-                    List<AppOrderProInfo> proInfoList = appOrderProInfoMapper.selectList(Wrappers.<AppOrderProInfo>lambdaQuery().eq(AppOrderProInfo::getOrderId, orderId).eq(AppOrderProInfo::getType, CommonConstant.ORDER_PRO_INFO_TYPE_5));
-                    long count = proInfoList.stream().filter(info -> Objects.equals(info.getOrderStatus(), CommonConstant.ORDER_STATUS_2)).count();
-                    if(count == proInfoList.size()){
-                        appOrder.setOrderStatus(CommonConstant.ORDER_STATUS_2);
-                        appOrderMapper.updateById(appOrder);
+                    String orderId = orderProInfo.getOrderId();
+                    AppOrder appOrder = appOrderMapper.selectById(orderId);
+                    if (ObjectUtil.isNotEmpty(appOrder)){
+                        List<AppOrderProInfo> proInfoList = appOrderProInfoMapper.selectList(Wrappers.<AppOrderProInfo>lambdaQuery().eq(AppOrderProInfo::getOrderId, orderId).eq(AppOrderProInfo::getType, CommonConstant.ORDER_PRO_INFO_TYPE_5));
+                        long count = proInfoList.stream().filter(info -> Objects.equals(info.getOrderStatus(), CommonConstant.ORDER_STATUS_2)).count();
+                        if(count == proInfoList.size()){
+                            appOrder.setOrderStatus(CommonConstant.ORDER_STATUS_2);
+                            appOrderMapper.updateById(appOrder);
+                        }
                     }
+                }else {
+                    log.info("未查询到订单信息,表示当前人员属于延课人员不走订单");
                 }
+
             });
         }
         return Boolean.TRUE;