Zhangbw 3 miesięcy temu
rodzic
commit
2e818fee5d
17 zmienionych plików z 401 dodań i 13 usunięć
  1. 1 1
      ruoyi-admin/src/main/java/org/dromara/web/domain/vo/TenantListVo.java
  2. 2 0
      ruoyi-admin/src/main/resources/application.yml
  3. 25 0
      ruoyi-admin/src/main/resources/cert/apiclient_cert.pem
  4. 28 0
      ruoyi-admin/src/main/resources/cert/apiclient_key.pem
  5. 9 0
      ruoyi-admin/src/main/resources/cert/pub_key.pem
  6. 61 0
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/controller/AgreementConfigController.java
  7. 23 0
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/controller/PaymentConfigController.java
  8. 20 0
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/bo/AgreementConfigBo.java
  9. 3 0
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/bo/PaymentConfigBo.java
  10. 20 0
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/vo/AgreementConfigVo.java
  11. 4 0
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/vo/PaymentConfigVo.java
  12. 20 0
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/service/IAgreementConfigService.java
  13. 5 0
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/service/IPaymentConfigService.java
  14. 73 0
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/service/impl/AgreementConfigServiceImpl.java
  15. 98 11
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/service/impl/PaymentConfigServiceImpl.java
  16. 9 0
      ruoyi-modules/yp-miniapp/src/main/resources/sql/agreement_menu.sql
  17. 0 1
      ruoyi-modules/yp-stock/src/main/resources/application.properties

+ 1 - 1
ruoyi-admin/src/main/java/org/dromara/web/domain/vo/TenantListVo.java

@@ -10,7 +10,7 @@ import lombok.Data;
  * @author Lion Li
  */
 @Data
-@AutoMapper(target = SysTenantVo.class)
+@AutoMapper(target = SysTenantVo.class, reverseConvertGenerate = true)
 public class TenantListVo {
 
     /**

+ 2 - 0
ruoyi-admin/src/main/resources/application.yml

@@ -20,6 +20,7 @@ server:
       # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
       worker: 256
 
+
 captcha:
   # 是否启用验证码校验
   enable: true
@@ -222,6 +223,7 @@ xss:
   # 排除链接
   excludeUrls:
     - /system/notice
+    - /miniapp/agreement/*
 
 --- # 分布式锁 lock4j 全局配置
 lock4j:

+ 25 - 0
ruoyi-admin/src/main/resources/cert/apiclient_cert.pem

@@ -0,0 +1,25 @@
+-----BEGIN CERTIFICATE-----
+MIIEITCCAwmgAwIBAgIUUw/sQukkqUsAHzkSHFh7m51W35kwDQYJKoZIhvcNAQEL
+BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
+FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
+Q0EwHhcNMjYwMTA0MDIzODMxWhcNMzEwMTAzMDIzODMxWjB7MRMwEQYDVQQDDAox
+NzM3NjU0OTQ5MRswGQYDVQQKDBLlvq7kv6HllYbmiLfns7vnu58xJzAlBgNVBAsM
+HuatpuaxieaOjOWNh+enkeaKgOaciemZkOWFrOWPuDELMAkGA1UEBhMCQ04xETAP
+BgNVBAcMCFNoZW5aaGVuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+y+1mq05wjtZ1BU0HsjVT6ruekEXq4r24yfB01qPZI8hzubgHFDka0uclLL69IwPc
+4ymhkitHH1QvrBFQnoazhinQUN3WpQWSdd9w/5Uy0ZfVcJz05hbZI+L3KiZqNGua
+9EsYID0KpuWMkRLw2Yws8Vm08mdFEqTHWCCtObbzOIa6cjCnQGvQfZy2CYkyOWPF
+lQu3wMjsmhIvfl3pFxLb4hp1SMuApJvqxOtqclfLAV346ChPxchiRsN66BYdgPiT
+YpX25UIei3JJSR0FMd45mpam3Skb19SLB2kasKcfR4D6iD/BTIw+QIQRkgOeqARP
+mmLCup9AZFypmH5Qa30DmQIDAQABo4G5MIG2MAkGA1UdEwQCMAAwCwYDVR0PBAQD
+AgP4MIGbBgNVHR8EgZMwgZAwgY2ggYqggYeGgYRodHRwOi8vZXZjYS5pdHJ1cy5j
+b20uY24vcHVibGljL2l0cnVzY3JsP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFEMzk3
+NTQ5ODQ2QzAxQzNFOEVCRDImc2c9SEFDQzQ3MUI2NTQyMkUxMkIyN0E5RDMzQTg3
+QUQxQ0RGNTkyNkUxNDAzNzEwDQYJKoZIhvcNAQELBQADggEBABOsvECcd7zwQqvQ
+yj9cdaFreKQQgQV5qf8cn2DSC42zMKRqPph60yGcDlOx0Vv4h4R8YwkQwxbofxQ3
+adJnOQOVeSwUtBTfmMdT7/m3v4MCeLU3PXfqdbrQ6v8/qy6ugaAeCxBzpRShMnEe
+oavL910JC+oKIdJAhrPvsiXhOlsAv1cP+NJjAgDAYBCMZBoNlrV4XU4Ijdrz4B32
+twMDnf4v0ClBxvTDwwxJ9OFovdrARw2qkNvgY8uKQq6lr6TAlFMjfzjgRKiJho/P
+qHvHhagoEV4xZPiXYdaD2h2UjqCHUxm4XZwhuU0GlLzC52vTtjuXvTt6ftK7lFw3
+vR/aMO0=
+-----END CERTIFICATE-----

+ 28 - 0
ruoyi-admin/src/main/resources/cert/apiclient_key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDL7WarTnCO1nUF
+TQeyNVPqu56QRerivbjJ8HTWo9kjyHO5uAcUORrS5yUsvr0jA9zjKaGSK0cfVC+s
+EVCehrOGKdBQ3dalBZJ133D/lTLRl9VwnPTmFtkj4vcqJmo0a5r0SxggPQqm5YyR
+EvDZjCzxWbTyZ0USpMdYIK05tvM4hrpyMKdAa9B9nLYJiTI5Y8WVC7fAyOyaEi9+
+XekXEtviGnVIy4Ckm+rE62pyV8sBXfjoKE/FyGJGw3roFh2A+JNilfblQh6LcklJ
+HQUx3jmalqbdKRvX1IsHaRqwpx9HgPqIP8FMjD5AhBGSA56oBE+aYsK6n0BkXKmY
+flBrfQOZAgMBAAECggEATxN2nXTkNq84X17YXiVJrhskyGMkStowvrWsSNLuT27a
+WSjgLdpa6W9SPgCUjPBhhgDNs0Vu5doaRknMjjxDfwHV0pXDNuhrO6PySFlNhHKx
+BGMIn/Q6LS1ElKaixkL3qkU4TL/+0mru0A6dMG6nussDXH/pC+qzlhvNsDSeKct6
+02MrO0pTKXeJlJMDmuP99EZrhzG0IoIhjCrEF4LO7m+jkmSesL13VB4LLVBNmFlE
+HoVazqKyh+hIIqj47DEeHfs8nbu/IlWFdC9YxM3gl6kKleUjznnD6avtyhr5uTuz
+RzhG77CgwgFbPJLx27QyJe3anO91NBMpr4lfb0L4gQKBgQD3/l6My5ABnSiB0JwT
+zpvJI1EYb5jvhrXIEilwbUhcD1rrVEmmsdowll1gc80PzP2wTxikcOEAaZWa8me5
+FXcL1f0mOYAgK0GNp6KcrdMq2XVvp1KVhqAJ+VGbNv/XByydL7tgZG0upDyH7xor
+zvw+rT8hwSznr34hrKpjKpdgyQKBgQDSgtSV1UcBszn/k6IEuGmMPfJulcQeCMVc
+MAhEDmMHcQIvhjMVzUgMOa6x06I5SgFn5g8U6bnT7mBvCQ/MRuXuBttBJI56irwN
+gHLp+t/wTn78+b3jP/k3mBTS5OtZ5eo0PHyezJWkiC52SST1gXYXX0Xad2qHK6xl
+qfnqH9JEUQKBgQCV7j2vlap8r008Hqkd+H2K/0UaJu4pciIY0ZIoHInwzn/DYvQc
+Sc6GLYJKu/gwI1kE7O0TDgOaQjtmsEMxTN7qUvd30q+5wy0gvUa8vwK6Fzq+xERT
+jk7XeNpzoq7Oi9HXPnJkdpLj7fuCm3YRIlSyd/+BIs9JS3pQVZm11pwqsQKBgQC8
+wJhy3LB1YsnPUwXEK8JgAC9/XoRh1cV42oye0TOUOY0XJcJqew1uAM6WHkfoDGZk
+TdEaFFQOJkCLbtmedbe3DKjNqUSjtf/WL2VaTRs8cnXEyP2tZPBGG/QYATDSA6hT
+PCUVkiVfSMg4QW+FPpavcDrHH6oCF3PWGGnAttWncQKBgCehkBIKWN/7UidW9hWC
+PITfKDHHjMd2T/g+hLv8BUATIPRGjuluGEsyoAiM7R6PKL24+EheSip3pNEAaO2g
+X2TpKLFLDBVClm8b2nsh+U6FXfv9+VRGDrR64RiFaxnVYIoGhGSDD+N/Lhg605LP
+JlO/OxISeBD0IpR6q6TK8YSh
+-----END PRIVATE KEY-----

+ 9 - 0
ruoyi-admin/src/main/resources/cert/pub_key.pem

@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuECosqB07V6LOzss3lqu
+9PqMqBPwdSTV+GDhZp3n5rkG6ksWrI65YWF/4zNRD8CfoyOxsywow7y7k09mn3HS
+D8lLoS3bRxRCN8OdM8RuFgsiiyk63EpkZ1kgahIuacJLZlypj9Y/bY9bvpz6A9J2
+Qs/tytWSrwjhoRloqPf17SBCmhuDcsEuY1giAyopg2WvdxBdhrgFdjyXGvztJfaj
+m6Rb3Hf60aq/VAi82P7RJ8+zi2TGI46rcIw/BcShFLTbHpGOdhT3ymhDOWoO81yL
+ETD92wvOozipJPcb7NrWIhFHBAyRbAFijr0pQFB7hnlBqX7vqmzaQw8LL5R0Y+hT
++wIDAQAB
+-----END PUBLIC KEY-----

+ 61 - 0
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/controller/AgreementConfigController.java

@@ -0,0 +1,61 @@
+package com.yingpai.miniapp.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.yingpai.miniapp.domain.bo.AgreementConfigBo;
+import com.yingpai.miniapp.domain.vo.AgreementConfigVo;
+import com.yingpai.miniapp.service.IAgreementConfigService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.web.core.BaseController;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 协议配置管理
+ */
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/miniapp/agreement")
+public class AgreementConfigController extends BaseController {
+
+    private final IAgreementConfigService agreementConfigService;
+
+    /**
+     * 获取协议配置(后台管理用)
+     */
+    @SaCheckPermission("miniapp:agreement:list")
+    @GetMapping("/list")
+    public R<AgreementConfigVo> list() {
+        return R.ok(agreementConfigService.getConfig());
+    }
+
+    /**
+     * 更新用户协议
+     */
+    @SaCheckPermission("miniapp:agreement:edit")
+    @Log(title = "用户协议配置", businessType = BusinessType.UPDATE)
+    @PutMapping("/userAgreement")
+    public R<Void> updateUserAgreement(@RequestBody AgreementConfigBo bo) {
+        AgreementConfigBo updateBo = new AgreementConfigBo();
+        updateBo.setUserAgreement(bo.getUserAgreement());
+        return toAjax(agreementConfigService.updateConfig(updateBo));
+    }
+
+    /**
+     * 更新隐私政策
+     */
+    @SaCheckPermission("miniapp:agreement:edit")
+    @Log(title = "隐私政策配置", businessType = BusinessType.UPDATE)
+    @PutMapping("/privacyPolicy")
+    public R<Void> updatePrivacyPolicy(@RequestBody AgreementConfigBo bo) {
+        AgreementConfigBo updateBo = new AgreementConfigBo();
+        updateBo.setPrivacyPolicy(bo.getPrivacyPolicy());
+        return toAjax(agreementConfigService.updateConfig(updateBo));
+    }
+
+}

+ 23 - 0
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/controller/PaymentConfigController.java

@@ -57,6 +57,7 @@ public class PaymentConfigController extends BaseController {
         wxpayBo.setMchId(bo.getMchId());
         wxpayBo.setApiV3Key(bo.getApiV3Key());
         wxpayBo.setNotifyUrl(bo.getNotifyUrl());
+        wxpayBo.setPublicKeyId(bo.getPublicKeyId());
         return toAjax(paymentConfigService.updateConfig(wxpayBo));
     }
 
@@ -117,4 +118,26 @@ public class PaymentConfigController extends BaseController {
             return R.fail("上传失败:" + e.getMessage());
         }
     }
+
+    /**
+     * 上传微信支付公钥文件
+     */
+    @SaCheckPermission("miniapp:paymentConfig:edit")
+    @Log(title = "支付配置-上传公钥", businessType = BusinessType.UPDATE)
+    @PostMapping("/uploadPublicKey")
+    public R<Void> uploadPublicKey(@RequestParam("file") MultipartFile file) {
+        try {
+            if (file.isEmpty()) {
+                return R.fail("文件不能为空");
+            }
+            String filename = file.getOriginalFilename();
+            if (filename == null || !filename.endsWith(".pem")) {
+                return R.fail("请上传.pem格式的公钥文件");
+            }
+            return toAjax(paymentConfigService.savePublicKey(file));
+        } catch (Exception e) {
+            log.error("上传公钥失败", e);
+            return R.fail("上传失败:" + e.getMessage());
+        }
+    }
 }

+ 20 - 0
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/bo/AgreementConfigBo.java

@@ -0,0 +1,20 @@
+package com.yingpai.miniapp.domain.bo;
+
+import lombok.Data;
+
+/**
+ * 协议配置BO
+ */
+@Data
+public class AgreementConfigBo {
+    
+    /**
+     * 用户协议内容(HTML)
+     */
+    private String userAgreement;
+    
+    /**
+     * 隐私政策内容(HTML)
+     */
+    private String privacyPolicy;
+}

+ 3 - 0
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/bo/PaymentConfigBo.java

@@ -15,6 +15,9 @@ public class PaymentConfigBo {
     private String apiV3Key;
     private String notifyUrl;
     
+    // 微信支付公钥配置(新商户需要)
+    private String publicKeyId;
+    
     // 价格配置
     private BigDecimal shortPrice;
     private BigDecimal strongPrice;

+ 20 - 0
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/vo/AgreementConfigVo.java

@@ -0,0 +1,20 @@
+package com.yingpai.miniapp.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 协议配置VO
+ */
+@Data
+public class AgreementConfigVo {
+    
+    /**
+     * 用户协议内容(HTML)
+     */
+    private String userAgreement;
+    
+    /**
+     * 隐私政策内容(HTML)
+     */
+    private String privacyPolicy;
+}

+ 4 - 0
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/vo/PaymentConfigVo.java

@@ -17,6 +17,10 @@ public class PaymentConfigVo {
     private Boolean privateKeyUploaded;
     private Boolean certUploaded;
     
+    // 微信支付公钥配置(新商户需要)
+    private String publicKeyId;
+    private Boolean publicKeyUploaded;
+    
     // 价格配置
     private BigDecimal shortPrice;
     private BigDecimal strongPrice;

+ 20 - 0
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/service/IAgreementConfigService.java

@@ -0,0 +1,20 @@
+package com.yingpai.miniapp.service;
+
+import com.yingpai.miniapp.domain.bo.AgreementConfigBo;
+import com.yingpai.miniapp.domain.vo.AgreementConfigVo;
+
+/**
+ * 协议配置服务接口
+ */
+public interface IAgreementConfigService {
+
+    /**
+     * 获取协议配置
+     */
+    AgreementConfigVo getConfig();
+
+    /**
+     * 更新协议配置
+     */
+    boolean updateConfig(AgreementConfigBo bo);
+}

+ 5 - 0
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/service/IPaymentConfigService.java

@@ -28,4 +28,9 @@ public interface IPaymentConfigService {
      * 保存支付证书文件
      */
     boolean saveCert(MultipartFile file);
+    
+    /**
+     * 保存微信支付公钥文件
+     */
+    boolean savePublicKey(MultipartFile file);
 }

+ 73 - 0
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/service/impl/AgreementConfigServiceImpl.java

@@ -0,0 +1,73 @@
+package com.yingpai.miniapp.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.yingpai.miniapp.domain.bo.AgreementConfigBo;
+import com.yingpai.miniapp.domain.vo.AgreementConfigVo;
+import com.yingpai.miniapp.service.IAgreementConfigService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.system.domain.SysConfig;
+import org.dromara.system.mapper.SysConfigMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 协议配置服务实现
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AgreementConfigServiceImpl implements IAgreementConfigService {
+
+    private final SysConfigMapper sysConfigMapper;
+
+    private static final String USER_AGREEMENT_KEY = "miniapp.user.agreement";
+    private static final String PRIVACY_POLICY_KEY = "miniapp.privacy.policy";
+
+    @Override
+    public AgreementConfigVo getConfig() {
+        AgreementConfigVo vo = new AgreementConfigVo();
+        vo.setUserAgreement(getConfigString(USER_AGREEMENT_KEY));
+        vo.setPrivacyPolicy(getConfigString(PRIVACY_POLICY_KEY));
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateConfig(AgreementConfigBo bo) {
+        if (bo.getUserAgreement() != null) {
+            updateOrInsertConfig(USER_AGREEMENT_KEY, bo.getUserAgreement(), "用户协议内容");
+        }
+        if (bo.getPrivacyPolicy() != null) {
+            updateOrInsertConfig(PRIVACY_POLICY_KEY, bo.getPrivacyPolicy(), "隐私政策内容");
+        }
+        return true;
+    }
+
+    private String getConfigString(String key) {
+        List<SysConfig> list = sysConfigMapper.selectList(
+            new LambdaQueryWrapper<SysConfig>().eq(SysConfig::getConfigKey, key).last("LIMIT 1"));
+        return !list.isEmpty() ? list.get(0).getConfigValue() : "";
+    }
+
+    private void updateOrInsertConfig(String key, String value, String name) {
+        List<SysConfig> list = sysConfigMapper.selectList(
+            new LambdaQueryWrapper<SysConfig>().eq(SysConfig::getConfigKey, key).last("LIMIT 1"));
+
+        if (!list.isEmpty()) {
+            sysConfigMapper.update(null, new LambdaUpdateWrapper<SysConfig>()
+                .eq(SysConfig::getConfigKey, key)
+                .set(SysConfig::getConfigValue, value));
+        } else {
+            SysConfig config = new SysConfig();
+            config.setConfigName(name);
+            config.setConfigKey(key);
+            config.setConfigValue(value);
+            config.setConfigType("N");
+            sysConfigMapper.insert(config);
+        }
+    }
+}

+ 98 - 11
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/service/impl/PaymentConfigServiceImpl.java

@@ -9,13 +9,14 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.system.domain.SysConfig;
 import org.dromara.system.mapper.SysConfigMapper;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.math.BigDecimal;
 import java.util.List;
 
@@ -29,8 +30,25 @@ public class PaymentConfigServiceImpl implements IPaymentConfigService {
 
     private final SysConfigMapper sysConfigMapper;
 
-    @Value("${ruoyi.profile:/data/upload}")
-    private String uploadPath;
+    /** 证书保存目录(模块resources/cert下) */
+    private String getCertDir() {
+        try {
+            // 获取当前模块的resources目录
+            String resourcePath = this.getClass().getClassLoader().getResource("").getPath();
+            // Windows路径处理
+            if (resourcePath.startsWith("/")) {
+                resourcePath = resourcePath.substring(1);
+            }
+            // 开发环境:target/classes -> src/main/resources
+            if (resourcePath.contains("target/classes")) {
+                resourcePath = resourcePath.replace("target/classes", "src/main/resources");
+            }
+            return resourcePath + "cert";
+        } catch (Exception e) {
+            log.error("获取resources路径失败", e);
+            throw new RuntimeException("获取证书目录失败", e);
+        }
+    }
 
     // 配置key
     private static final String SHORT_PRICE_KEY = "payment.short.price";
@@ -40,6 +58,8 @@ public class PaymentConfigServiceImpl implements IPaymentConfigService {
     private static final String NOTIFY_URL_KEY = "payment.notify.url";
     private static final String PRIVATE_KEY_PATH_KEY = "payment.private.key.path";
     private static final String CERT_PATH_KEY = "payment.cert.path";
+    private static final String PUBLIC_KEY_ID_KEY = "payment.public.key.id";
+    private static final String PUBLIC_KEY_PATH_KEY = "payment.public.key.path";
 
     @Override
     public PaymentConfigVo getConfig() {
@@ -50,6 +70,13 @@ public class PaymentConfigServiceImpl implements IPaymentConfigService {
         vo.setMchId(getConfigString(MCH_ID_KEY));
         vo.setApiV3Key(getConfigString(API_V3_KEY));
         vo.setNotifyUrl(getConfigString(NOTIFY_URL_KEY));
+        
+        // 微信支付公钥配置
+        vo.setPublicKeyId(getConfigString(PUBLIC_KEY_ID_KEY));
+        
+        // 检查公钥是否已上传
+        String publicKeyPath = getConfigString(PUBLIC_KEY_PATH_KEY);
+        vo.setPublicKeyUploaded(publicKeyPath != null && !publicKeyPath.isEmpty() && new File(publicKeyPath).exists());
 
         // 检查私钥是否已上传
         String privateKeyPath = getConfigString(PRIVATE_KEY_PATH_KEY);
@@ -80,21 +107,35 @@ public class PaymentConfigServiceImpl implements IPaymentConfigService {
         if (bo.getNotifyUrl() != null) {
             updateOrInsertConfig(NOTIFY_URL_KEY, bo.getNotifyUrl(), "支付回调地址");
         }
+        if (bo.getPublicKeyId() != null) {
+            updateOrInsertConfig(PUBLIC_KEY_ID_KEY, bo.getPublicKeyId(), "微信支付公钥ID");
+        }
         return true;
     }
 
     @Override
     public boolean savePrivateKey(MultipartFile file) {
         try {
-            // 保存到cert目录
-            String certDir = uploadPath + "/cert";
+            // 保存到resources/cert目录
+            String certDir = getCertDir();
             File dir = new File(certDir);
             if (!dir.exists()) {
-                dir.mkdirs();
+                boolean created = dir.mkdirs();
+                log.info("创建目录 {}: {}", certDir, created);
             }
 
             String filePath = certDir + "/apiclient_key.pem";
-            file.transferTo(new File(filePath));
+            File destFile = new File(filePath);
+            
+            // 使用 InputStream 写入文件,避免 transferTo 在 Windows 上的问题
+            try (InputStream is = file.getInputStream();
+                 FileOutputStream fos = new FileOutputStream(destFile)) {
+                byte[] buffer = new byte[1024];
+                int len;
+                while ((len = is.read(buffer)) > 0) {
+                    fos.write(buffer, 0, len);
+                }
+            }
 
             // 保存路径到配置
             updateOrInsertConfig(PRIVATE_KEY_PATH_KEY, filePath, "商户私钥文件路径");
@@ -110,15 +151,26 @@ public class PaymentConfigServiceImpl implements IPaymentConfigService {
     @Override
     public boolean saveCert(MultipartFile file) {
         try {
-            // 保存到cert目录
-            String certDir = uploadPath + "/cert";
+            // 保存到resources/cert目录
+            String certDir = getCertDir();
             File dir = new File(certDir);
             if (!dir.exists()) {
-                dir.mkdirs();
+                boolean created = dir.mkdirs();
+                log.info("创建目录 {}: {}", certDir, created);
             }
 
             String filePath = certDir + "/apiclient_cert.pem";
-            file.transferTo(new File(filePath));
+            File destFile = new File(filePath);
+            
+            // 使用 InputStream 写入文件,避免 transferTo 在 Windows 上的问题
+            try (InputStream is = file.getInputStream();
+                 FileOutputStream fos = new FileOutputStream(destFile)) {
+                byte[] buffer = new byte[1024];
+                int len;
+                while ((len = is.read(buffer)) > 0) {
+                    fos.write(buffer, 0, len);
+                }
+            }
 
             // 保存路径到配置
             updateOrInsertConfig(CERT_PATH_KEY, filePath, "支付证书文件路径");
@@ -131,6 +183,41 @@ public class PaymentConfigServiceImpl implements IPaymentConfigService {
         }
     }
 
+    @Override
+    public boolean savePublicKey(MultipartFile file) {
+        try {
+            // 保存到resources/cert目录
+            String certDir = getCertDir();
+            File dir = new File(certDir);
+            if (!dir.exists()) {
+                boolean created = dir.mkdirs();
+                log.info("创建目录 {}: {}", certDir, created);
+            }
+
+            String filePath = certDir + "/pub_key.pem";
+            File destFile = new File(filePath);
+            
+            // 使用 InputStream 写入文件
+            try (InputStream is = file.getInputStream();
+                 FileOutputStream fos = new FileOutputStream(destFile)) {
+                byte[] buffer = new byte[1024];
+                int len;
+                while ((len = is.read(buffer)) > 0) {
+                    fos.write(buffer, 0, len);
+                }
+            }
+
+            // 保存路径到配置
+            updateOrInsertConfig(PUBLIC_KEY_PATH_KEY, filePath, "微信支付公钥文件路径");
+
+            log.info("公钥文件保存成功: {}", filePath);
+            return true;
+        } catch (IOException e) {
+            log.error("保存公钥文件失败", e);
+            throw new RuntimeException("保存公钥文件失败", e);
+        }
+    }
+
     private BigDecimal getConfigValue(String key, String defaultValue) {
         List<SysConfig> list = sysConfigMapper.selectList(
             new LambdaQueryWrapper<SysConfig>().eq(SysConfig::getConfigKey, key).last("LIMIT 1"));

+ 9 - 0
ruoyi-modules/yp-miniapp/src/main/resources/sql/agreement_menu.sql

@@ -0,0 +1,9 @@
+-- 协议配置菜单SQL
+-- 父菜单:配置管理 (2004074990147751937)
+
+-- 协议配置目录
+INSERT INTO `sys_menu` VALUES (2005310040, '协议配置', 2004074990147751937, 2, 'agreement', 'settings/agreement/index', '', 1, 0, 'C', '0', '0', 'miniapp:agreement:list', 'documentation', 103, 1, NOW(), NULL, NULL, '用户协议与隐私政策配置');
+
+-- 协议配置权限按钮
+INSERT INTO `sys_menu` VALUES (2005310041, '协议查询', 2005310040, 1, '', '', '', 1, 0, 'F', '0', '0', 'miniapp:agreement:query', '#', 103, 1, NOW(), NULL, NULL, '');
+INSERT INTO `sys_menu` VALUES (2005310042, '协议修改', 2005310040, 2, '', '', '', 1, 0, 'F', '0', '0', 'miniapp:agreement:edit', '#', 103, 1, NOW(), NULL, NULL, '');

+ 0 - 1
ruoyi-modules/yp-stock/src/main/resources/application.properties

@@ -1 +0,0 @@
-spring.application.name=yp-stock