소스 검색

新增支付宝支付配置项

jialuyu 1 개월 전
부모
커밋
31df56ad2e

+ 73 - 12
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/config/WxPayConfig.java

@@ -144,21 +144,51 @@ public class WxPayConfig {
     }
 
     /**
-     * 生成 JSAPI 支付参数
+     * 生成 JSAPI 支付参数(每次直接从数据库读取最新配置)
      */
     public Map<String, String> createJsapiPayParams(String openid, BigDecimal payPrice, String remark, String orderCode) {
-        if (jsapiService == null) {
-            throw new RuntimeException("微信支付服务未初始化,请检查配置");
-        }
-
         try {
+            // 每次从数据库读取最新配置
+            PaymentConfig dbConfig = paymentConfigService.getEnabledWechatConfig();
+            if (dbConfig == null) {
+                throw new RuntimeException("微信支付配置不存在,请前往【系统配置 → 微信支付配置】完成配置");
+            }
+
+            String dbAppId      = dbConfig.getAppId();
+            String dbMchId      = dbConfig.getMchId();
+            String dbApiV3Key   = dbConfig.getApiV3Key();
+            String dbSerialNo   = dbConfig.getSerialNo();
+            String dbNotifyUrl  = dbConfig.getPayNotifyUrl();
+            // 直接从数据库读取已存储的私钥文本内容(上传时已写入 private_key 字段)
+            String pemContent   = dbConfig.getPrivateKey();
+
+            if (StringUtils.isBlank(dbMchId) || StringUtils.isBlank(dbApiV3Key) || StringUtils.isBlank(dbSerialNo)) {
+                throw new RuntimeException("微信支付配置不完整,请前往【系统配置 → 微信支付配置】补全商户号、API密钥、证书序列号");
+            }
+            if (StringUtils.isBlank(pemContent) || !pemContent.contains("-----BEGIN")) {
+                throw new RuntimeException("商户私钥未配置或格式无效,请前往【系统配置 → 微信支付配置】重新上传私钥文件");
+            }
+
+            // 动态构建微信支付服务(SDK 直接接收 PEM 字符串)
+            Config config = new RSAAutoCertificateConfig.Builder()
+                .merchantId(dbMchId)
+                .privateKey(pemContent)
+                .merchantSerialNumber(dbSerialNo)
+                .apiV3Key(dbApiV3Key)
+                .build();
+            JsapiService currentJsapiService = new JsapiService.Builder().config(config).build();
+
+            // 解析私钥对象(用于签名),兼容 PKCS#8 和 PKCS#1 格式
+            PrivateKey currentPrivateKey = parsePrivateKey(pemContent);
+
+
             // 构建预支付请求
             PrepayRequest request = new PrepayRequest();
-            request.setAppid(appId);
-            request.setMchid(mchId);
+            request.setAppid(dbAppId);
+            request.setMchid(dbMchId);
             request.setDescription(StringUtils.isEmpty(remark) ? "订单支付" : remark);
             request.setOutTradeNo(orderCode);
-            request.setNotifyUrl(notifyUrl);
+            request.setNotifyUrl(dbNotifyUrl);
 
             // 金额设置(转换为分)
             Amount amount = new Amount();
@@ -173,19 +203,49 @@ public class WxPayConfig {
             request.setPayer(payer);
 
             // 调用预支付接口
-            PrepayResponse response = jsapiService.prepay(request);
+            PrepayResponse response = currentJsapiService.prepay(request);
 
             // 生成前端支付参数
-            return generatePaySign(response.getPrepayId());
+            return generatePaySign(response.getPrepayId(), dbAppId, dbMchId, currentPrivateKey);
+        } catch (RuntimeException e) {
+            throw e;
         } catch (Exception e) {
             throw new RuntimeException("生成JSAPI支付参数失败", e);
         }
     }
 
+    /**
+     * 从 PEM 字符串解析私钥,兼容 PKCS#8(BEGIN PRIVATE KEY)和 PKCS#1(BEGIN RSA PRIVATE KEY)格式
+     */
+    private PrivateKey parsePrivateKey(String pemContent) {
+        try {
+            // 去除 PEM 头、尾及所有空白字符,得到纯 Base64 内容
+            String base64 = pemContent
+                .replaceAll("-----[^-]+-----", "")
+                .replaceAll("\\s", "");
+            byte[] decoded = java.util.Base64.getDecoder().decode(base64);
+
+            // 优先尝试 PKCS#8 格式(BEGIN PRIVATE KEY,微信支付标准格式)
+            try {
+                java.security.spec.PKCS8EncodedKeySpec spec = new java.security.spec.PKCS8EncodedKeySpec(decoded);
+                return java.security.KeyFactory.getInstance("RSA").generatePrivate(spec);
+            } catch (Exception e1) {
+                // 降级尝试 PKCS#1 格式(BEGIN RSA PRIVATE KEY),借助 BouncyCastle 解析
+                org.bouncycastle.asn1.pkcs.RSAPrivateKey rsa =
+                    org.bouncycastle.asn1.pkcs.RSAPrivateKey.getInstance(decoded);
+                java.security.spec.RSAPrivateKeySpec spec = new java.security.spec.RSAPrivateKeySpec(
+                    rsa.getModulus(), rsa.getPrivateExponent());
+                return java.security.KeyFactory.getInstance("RSA").generatePrivate(spec);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("解析商户私钥失败,请检查私钥文件是否为标准 PEM 格式(PKCS#8 或 PKCS#1)", e);
+        }
+    }
+
     /**
      * 生成支付签名
      */
-    private Map<String, String> generatePaySign(String prepayId) {
+    private Map<String, String> generatePaySign(String prepayId, String appId, String mchId, PrivateKey currentPrivateKey) {
         try {
             String timeStamp = String.valueOf(Instant.now().getEpochSecond());
             String nonceStr = UUID.randomUUID().toString().replaceAll("-", "");
@@ -195,7 +255,7 @@ public class WxPayConfig {
             String signMessage = appId + "\n" + timeStamp + "\n" + nonceStr + "\n" + packageStr + "\n";
 
             // 使用商户私钥签名
-            String paySign = WXPayUtility.sign(signMessage, "SHA256withRSA", privateKey);
+            String paySign = WXPayUtility.sign(signMessage, "SHA256withRSA", currentPrivateKey);
 
             // 返回支付参数
             Map<String, String> payParams = new HashMap<>();
@@ -216,3 +276,4 @@ public class WxPayConfig {
     public JsapiService getJsapiService() { return jsapiService; }
     public RefundService getRefundService() { return refundService; }
 }
+

+ 61 - 5
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/miniapp/MiniappPaymentConfigController.java

@@ -19,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestPart;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.nio.charset.StandardCharsets;
+
 @SaIgnore
 @RequiredArgsConstructor
 @RestController
@@ -42,25 +44,79 @@ public class MiniappPaymentConfigController {
 
     @RepeatSubmit
     @PostMapping(value = "/uploadPrivateKey", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
-    public R<Void> uploadPrivateKey(@RequestPart("file") MultipartFile file) {
+    public R<String> uploadPrivateKey(@RequestPart("file") MultipartFile file) throws Exception {
+        // 读取私鑰文件内容并存入数据库(避免运行时访问 OSS URL 权限问题)
+        String privateKeyContent = new String(file.getBytes(), StandardCharsets.UTF_8).trim();
+        if (!privateKeyContent.contains("-----BEGIN")) {
+            return R.fail("上传的文件不是合法的 PEM 格式私鑰文件,请重新选择");
+        }
+        paymentConfigService.updateWechatPrivateKey(privateKeyContent);
+        // 同时仍上传到 OSS 备份,并存储路径(不再用于运行时加载)
         SysOssVo oss = ossService.upload(file);
         paymentConfigService.updateWechatPrivateKeyPath(oss.getUrl());
-        return R.ok();
+        return R.ok(oss.getUrl());
     }
 
     @RepeatSubmit
     @PostMapping(value = "/uploadCert", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
-    public R<Void> uploadCert(@RequestPart("file") MultipartFile file) {
+    public R<String> uploadCert(@RequestPart("file") MultipartFile file) {
         SysOssVo oss = ossService.upload(file);
         paymentConfigService.updateWechatCertPath(oss.getUrl());
-        return R.ok();
+        return R.ok(oss.getUrl());
     }
 
     @RepeatSubmit
     @PostMapping(value = "/uploadPublicKey", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
-    public R<Void> uploadPublicKey(@RequestPart("file") MultipartFile file) {
+    public R<String> uploadPublicKey(@RequestPart("file") MultipartFile file) {
         SysOssVo oss = ossService.upload(file);
         paymentConfigService.updateWechatPublicKeyPath(oss.getUrl());
+        return R.ok(oss.getUrl());
+    }
+
+    @GetMapping("/alipay")
+    public R<PaymentConfigVo> getAlipayConfig() {
+        return R.ok(paymentConfigService.getAlipayConfig());
+    }
+
+    @RepeatSubmit
+    @PutMapping("/alipay")
+    public R<Void> saveAlipayConfig(@RequestBody PaymentConfigBo bo) {
+        paymentConfigService.saveAlipayConfig(bo);
         return R.ok();
     }
+
+    @RepeatSubmit
+    @PostMapping(value = "/uploadAlipayAppPublicKey", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<String> uploadAlipayAppPublicKey(@RequestPart("file") MultipartFile file) throws Exception {
+        // 读取应用公钥内容存入数据库(备用,证书模式需要)
+        String content = new String(file.getBytes(), StandardCharsets.UTF_8).trim();
+        paymentConfigService.updateAlipayAppPublicKeyPath(content);
+        // 同时上传到 OSS 备份
+        SysOssVo oss = ossService.upload(file);
+        return R.ok(oss.getUrl());
+    }
+
+    @RepeatSubmit
+    @PostMapping(value = "/uploadAlipayPublicKey", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<String> uploadAlipayPublicKey(@RequestPart("file") MultipartFile file) throws Exception {
+        // 读取支付宝公钥文件内容,存入 public_key 字段(DefaultAlipayClient 直接需要公钥字符串,非 OSS URL)
+        String content = new String(file.getBytes(), StandardCharsets.UTF_8).trim();
+        paymentConfigService.updateAlipayPublicKey(content);
+        // 同时存 OSS 路径备份,并更新 public_key_path_zfb
+        SysOssVo oss = ossService.upload(file);
+        paymentConfigService.updateAlipayPublicKeyPath(oss.getUrl());
+        return R.ok(oss.getUrl());
+    }
+
+    @RepeatSubmit
+    @PostMapping(value = "/uploadAlipayRootCert", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public R<String> uploadAlipayRootCert(@RequestPart("file") MultipartFile file) throws Exception {
+        // 读取根证书内容存入数据库(证书模式需要)
+        String content = new String(file.getBytes(), StandardCharsets.UTF_8).trim();
+        paymentConfigService.updateAlipayRootCertPath(content);
+        // 同时上传到 OSS 备份
+        SysOssVo oss = ossService.upload(file);
+        return R.ok(oss.getUrl());
+    }
 }
+

+ 6 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/PaymentConfig.java

@@ -42,6 +42,12 @@ public class PaymentConfig extends BaseEntity {
 
     private String publicKey;
 
+    private String applicationPublicKeyPath;
+
+    private String publicKeyPathZfb;
+
+    private String rootCert;
+
     private String signType;
 
     private String format;

+ 20 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IPaymentConfigService.java

@@ -26,7 +26,27 @@ public interface IPaymentConfigService {
 
     void updateWechatPrivateKeyPath(String privateKeyPath);
 
+    /**
+     * 将私钥文本内容直接存入 private_key 字段(避免运行时访问 OSS)
+     */
+    void updateWechatPrivateKey(String privateKeyContent);
+
     void updateWechatCertPath(String certPath);
 
     void updateWechatPublicKeyPath(String publicKeyPath);
+
+    PaymentConfigVo getAlipayConfig();
+
+    void saveAlipayConfig(PaymentConfigBo bo);
+
+    void updateAlipayAppPublicKeyPath(String path);
+
+    void updateAlipayPublicKeyPath(String path);
+
+    /**
+     * 将支付宝公鑰文本内容存入 public_key 字段(DefaultAlipayClient 直接需要公鑰字符串)
+     */
+    void updateAlipayPublicKey(String publicKeyContent);
+
+    void updateAlipayRootCertPath(String path);
 }

+ 8 - 4
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainBackOrderServiceImpl.java

@@ -921,8 +921,10 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
             .distinct()
             .toList();
         Map<Long, MainBackCandidate> candidateMap = new HashMap<>();
-        for (MainBackCandidate candidate : mainBackCandidateMapper.selectBatchIds(candidateIds)) {
-            candidateMap.put(candidate.getId(), candidate);
+        if (candidateIds != null && !candidateIds.isEmpty()) {
+            for (MainBackCandidate candidate : mainBackCandidateMapper.selectBatchIds(candidateIds)) {
+                candidateMap.put(candidate.getId(), candidate);
+            }
         }
 
         List<Long> studentIds = candidateMap.values().stream()
@@ -931,8 +933,10 @@ public class MainBackOrderServiceImpl implements IMainBackOrderService {
             .distinct()
             .toList();
         Map<Long, MainStudent> studentMap = new HashMap<>();
-        for (MainStudent student : mainStudentMapper.selectBatchIds(studentIds)) {
-            studentMap.put(student.getId(), student);
+        if (studentIds != null && !studentIds.isEmpty()) {
+            for (MainStudent student : mainStudentMapper.selectBatchIds(studentIds)) {
+                studentMap.put(student.getId(), student);
+            }
         }
 
         List<MainBackOrderCandidateVo> result = new ArrayList<>(records.size());

+ 90 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/PaymentConfigServiceImpl.java

@@ -110,6 +110,15 @@ public class PaymentConfigServiceImpl implements IPaymentConfigService {
         saveOrUpdate(config);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateWechatPrivateKey(String privateKeyContent) {
+        // 将私钥文本内容直接存入 private_key 字段,读取时无需再访问 OSS
+        PaymentConfig config = getOrCreateWechatConfig();
+        config.setPrivateKey(privateKeyContent);
+        saveOrUpdate(config);
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updateWechatCertPath(String certPath) {
@@ -153,4 +162,85 @@ public class PaymentConfigServiceImpl implements IPaymentConfigService {
         }
         paymentConfigMapper.updateById(config);
     }
+
+    @Override
+    public PaymentConfigVo getAlipayConfig() {
+        PaymentConfig config = getOrCreateAlipayConfig();
+        PaymentConfigVo vo = new PaymentConfigVo();
+        vo.setAppId(config.getAppId());
+        vo.setAppPrivateKey(config.getPrivateKey());
+        vo.setAppPublicKeyPath(config.getApplicationPublicKeyPath());
+        vo.setAlipayPublicKeyPath(config.getPublicKeyPathZfb());
+        vo.setAlipayRootCertPath(config.getRootCert());
+        
+        vo.setAppPublicKeyUploaded(config.getApplicationPublicKeyPath() != null && !config.getApplicationPublicKeyPath().isEmpty());
+        vo.setAlipayPublicKeyUploaded(config.getPublicKeyPathZfb() != null && !config.getPublicKeyPathZfb().isEmpty());
+        vo.setAlipayRootCertUploaded(config.getRootCert() != null && !config.getRootCert().isEmpty());
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void saveAlipayConfig(PaymentConfigBo bo) {
+        PaymentConfig config = getOrCreateAlipayConfig();
+        config.setAppId(bo.getAppId());
+        config.setPrivateKey(bo.getAppPrivateKey());
+        config.setApplicationPublicKeyPath(bo.getAppPublicKeyPath());
+        config.setPublicKeyPathZfb(bo.getAlipayPublicKeyPath());
+        config.setRootCert(bo.getAlipayRootCertPath());
+        saveOrUpdate(config);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateAlipayAppPublicKeyPath(String path) {
+        PaymentConfig config = getOrCreateAlipayConfig();
+        config.setApplicationPublicKeyPath(path);
+        saveOrUpdate(config);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateAlipayPublicKeyPath(String path) {
+        PaymentConfig config = getOrCreateAlipayConfig();
+        config.setPublicKeyPathZfb(path);
+        saveOrUpdate(config);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateAlipayPublicKey(String publicKeyContent) {
+        // 将支付宝公钥文本内容存入 public_key 字段,DefaultAlipayClient 直接使用此内容验签
+        PaymentConfig config = getOrCreateAlipayConfig();
+        config.setPublicKey(publicKeyContent);
+        saveOrUpdate(config);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateAlipayRootCertPath(String path) {
+        PaymentConfig config = getOrCreateAlipayConfig();
+        config.setRootCert(path);
+        saveOrUpdate(config);
+    }
+
+    private PaymentConfig getOrCreateAlipayConfig() {
+        PaymentConfig config = paymentConfigMapper.selectOne(
+            Wrappers.<PaymentConfig>lambdaQuery()
+                .eq(PaymentConfig::getConfigType, 1)
+                .eq(PaymentConfig::getPaymentType, 1)
+                .orderByDesc(PaymentConfig::getCreateTime)
+                .last("limit 1")
+        );
+        if (config != null) {
+            return config;
+        }
+        PaymentConfig add = new PaymentConfig();
+        add.setConfigName("支付宝支付配置");
+        add.setConfigType(1);
+        add.setPaymentType(1);
+        add.setIsEnabled(1);
+        paymentConfigMapper.insert(add);
+        return add;
+    }
 }

+ 6 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/PaymentConfigBo.java

@@ -22,6 +22,12 @@ public class PaymentConfigBo {
     // 微信支付公钥配置(新商户需要)
     private String publicKeyId;
 
+    // 支付宝支付配置
+    private String appPrivateKey;
+    private String appPublicKeyPath;
+    private String alipayPublicKeyPath;
+    private String alipayRootCertPath;
+
     // 价格配置
     private BigDecimal shortPrice;
     private BigDecimal strongPrice;

+ 9 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/PaymentConfigVo.java

@@ -25,6 +25,15 @@ public class PaymentConfigVo {
     private String publicKeyId;
     private Boolean publicKeyUploaded;
 
+    // 支付宝支付配置
+    private String appPrivateKey;
+    private String appPublicKeyPath;
+    private String alipayPublicKeyPath;
+    private String alipayRootCertPath;
+    private Boolean appPublicKeyUploaded;
+    private Boolean alipayPublicKeyUploaded;
+    private Boolean alipayRootCertUploaded;
+
     // 价格配置
     private BigDecimal shortPrice;
     private BigDecimal strongPrice;