Bläddra i källkod

Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/db

# Conflicts:
#	sql/tools/README.md
YunaiV 11 månader sedan
förälder
incheckning
6c7f1e8220
22 ändrade filer med 686 tillägg och 141 borttagningar
  1. 4 4
      sql/tools/README.md
  2. 4 4
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/area.csv
  3. 7 3
      yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/util/CronUtils.java
  4. 8 1
      yudao-framework/yudao-spring-boot-starter-protection/pom.xml
  5. 28 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java
  6. 59 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java
  7. 169 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java
  8. 57 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java
  9. 6 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java
  10. 2 1
      yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  11. 75 0
      yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java
  12. 2 1
      yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessStatusDO.java
  13. 2 2
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileContentDO.java
  14. 0 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiErrorLogDO.java
  15. 2 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
  16. 42 4
      yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java
  17. 159 116
      yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql
  18. 1 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/common/SexEnum.java
  19. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
  20. 1 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java
  21. 8 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuService.java
  22. 49 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java

+ 4 - 4
sql/tools/README.md

@@ -67,8 +67,8 @@ exit
 
 ① 下载人大金仓 Docker 镜像:
 
-> x86_64版本: https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/V009R001C001B0025-安装包-docker/x86_64/kdb_x86_64_V009R001C001B0025.tar 
-> aarch64版本:https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/V009R001C001B0025-安装包-docker/aarch64/kdb_aarch64_V009R001C001B0025.tar
+> x86_64 版本: https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/V009R001C001B0025-安装包-docker/x86_64/kdb_x86_64_V009R001C001B0025.tar 
+> aarch64 版本:https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/V009R001C001B0025-安装包-docker/aarch64/kdb_aarch64_V009R001C001B0025.tar
 
 ② 加载镜像文件,在镜像 tar 文件所在目录运行:
 
@@ -80,11 +80,11 @@ docker load -i x86_64/kdb_x86_64_V009R001C001B0025.tar
 
 ```Bash
 docker compose up -d kingbase
-# 注意:启动完 dm 后,需要手动再执行如下命令,因为 dm 不支持初始化脚本
+# 注意:启动完 kingbase 后,需要手动再执行如下命令,因为 kingbase 不支持初始化脚本
 docker compose exec kingbase bash -c 'ksql -U $DB_USER -d test -f /tmp/schema.sql'
 ```
 
-**注意**: MyBatis, MyBatis Plus 目前不兼容人大金仓,推荐直接使用PostgreSQL JDBC 驱动,已经 url 配置方式连接数据库。
+**注意**: MyBatisMyBatis Plus 目前不兼容人大金仓,推荐直接使用 PostgreSQL JDBC 驱动,已经 url 配置方式连接数据库。
 
 ### 1.7 华为 OpenGauss
 

+ 4 - 4
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/resources/area.csv

@@ -247,15 +247,15 @@ id,name,type,parentId
 246,英属印度洋领地,1,0
 247,东萨摩亚,1,0
 248,诺福克岛,1,0
-110000,北京,2,1
-120000,天津,2,1
+110000,北京,2,1
+120000,天津,2,1
 130000,河北省,2,1
 140000,山西省,2,1
 150000,内蒙古自治区,2,1
 210000,辽宁省,2,1
 220000,吉林省,2,1
 230000,黑龙江省,2,1
-310000,上海,2,1
+310000,上海,2,1
 320000,江苏省,2,1
 330000,浙江省,2,1
 340000,安徽省,2,1
@@ -268,7 +268,7 @@ id,name,type,parentId
 440000,广东省,2,1
 450000,广西壮族自治区,2,1
 460000,海南省,2,1
-500000,重庆,2,1
+500000,重庆,2,1
 510000,四川省,2,1
 520000,贵州省,2,1
 530000,云南省,2,1

+ 7 - 3
yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/util/CronUtils.java

@@ -34,20 +34,24 @@ public class CronUtils {
      * @return 满足条件的执行时间
      */
     public static List<LocalDateTime> getNextTimes(String cronExpression, int n) {
-        // 获得 CronExpression 对象
+        // 1. 获得 CronExpression 对象
         CronExpression cron;
         try {
             cron = new CronExpression(cronExpression);
         } catch (ParseException e) {
             throw new IllegalArgumentException(e.getMessage());
         }
-        // 从当前开始计算,n 个满足条件的
+        // 2. 从当前开始计算,n 个满足条件的
         Date now = new Date();
         List<LocalDateTime> nextTimes = new ArrayList<>(n);
         for (int i = 0; i < n; i++) {
             Date nextTime = cron.getNextValidTimeAfter(now);
+            // 2.1 如果 nextTime 为 null,说明没有更多的有效时间,退出循环
+            if (nextTime == null) {
+                break;
+            }
             nextTimes.add(LocalDateTimeUtil.of(nextTime));
-            // 切换现在,为下一个触发时间;
+            // 2.2 切换现在,为下一个触发时间;
             now = nextTime;
         }
         return nextTimes;

+ 8 - 1
yudao-framework/yudao-spring-boot-starter-protection/pom.xml

@@ -12,7 +12,7 @@
     <packaging>jar</packaging>
 
     <name>${project.artifactId}</name>
-    <description>服务保证,提供分布式锁、幂等、限流、熔断等等功能</description>
+    <description>服务保证,提供分布式锁、幂等、限流、熔断、API 签名等等功能</description>
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <dependencies>
@@ -35,6 +35,13 @@
             <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
             <optional>true</optional>
         </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>

+ 28 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.signature.config;
+
+import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
+import cn.iocoder.yudao.framework.signature.core.aop.ApiSignatureAspect;
+import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * HTTP API 签名的自动配置类
+ *
+ * @author Zhougang
+ */
+@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
+public class YudaoApiSignatureAutoConfiguration {
+
+    @Bean
+    public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) {
+        return new ApiSignatureAspect(signatureRedisDAO);
+    }
+
+    @Bean
+    public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) {
+        return new ApiSignatureRedisDAO(stringRedisTemplate);
+    }
+
+}

+ 59 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.framework.signature.core.annotation;
+
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * HTTP API 签名注解
+ *
+ * @author Zhougang
+ */
+@Inherited
+@Documented
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiSignature {
+
+    /**
+     * 同一个请求多长时间内有效 默认 60 秒
+     */
+    int timeout() default 60;
+
+    /**
+     * 时间单位,默认为 SECONDS 秒
+     */
+    TimeUnit timeUnit() default TimeUnit.SECONDS;
+
+    // ========================== 签名参数 ==========================
+
+    /**
+     * 提示信息,签名失败的提示
+     *
+     * @see GlobalErrorCodeConstants#BAD_REQUEST
+     */
+    String message() default "签名不正确"; // 为空时,使用 BAD_REQUEST 错误提示
+
+    /**
+     * 签名字段:appId 应用ID
+     */
+    String appId() default "appId";
+
+    /**
+     * 签名字段:timestamp 时间戳
+     */
+    String timestamp() default "timestamp";
+
+    /**
+     * 签名字段:nonce 随机数,10 位以上
+     */
+    String nonce() default "nonce";
+
+    /**
+     * sign 客户端签名
+     */
+    String sign() default "sign";
+
+}

+ 169 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java

@@ -0,0 +1,169 @@
+package cn.iocoder.yudao.framework.signature.core.aop;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature;
+import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
+
+/**
+ * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名
+ *
+ * @author Zhougang
+ */
+@Aspect
+@Slf4j
+@AllArgsConstructor
+public class ApiSignatureAspect {
+
+    private final ApiSignatureRedisDAO signatureRedisDAO;
+
+    @Before("@annotation(signature)")
+    public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) {
+        // 1. 验证通过,直接结束
+        if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) {
+            return;
+        }
+
+        // 2. 验证不通过,抛出异常
+        log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(),
+                joinPoint.getArgs());
+        throw new ServiceException(BAD_REQUEST.getCode(),
+                StrUtil.blankToDefault(signature.message(), BAD_REQUEST.getMsg()));
+    }
+
+    public boolean verifySignature(ApiSignature signature, HttpServletRequest request) {
+        // 1.1 校验 Header
+        if (!verifyHeaders(signature, request)) {
+            return false;
+        }
+        // 1.2 校验 appId 是否能获取到对应的 appSecret
+        String appId = request.getHeader(signature.appId());
+        String appSecret = signatureRedisDAO.getAppSecret(appId);
+        Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId);
+
+        // 2. 校验签名【重要!】
+        String clientSignature = request.getHeader(signature.sign()); // 客户端签名
+        String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串
+        String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名
+        if (ObjUtil.notEqual(clientSignature, serverSignature)) {
+            return false;
+        }
+
+        // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )
+        String nonce = request.getHeader(signature.nonce());
+        signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit());
+        return true;
+    }
+
+    /**
+     * 校验请求头加签参数
+     *
+     * 1. appId 是否为空
+     * 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟
+     * 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了
+     * 4. sign 是否为空
+     *
+     * @param signature signature
+     * @param request   request
+     * @return 是否校验 Header 通过
+     */
+    private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {
+        // 1. 非空校验
+        String appId = request.getHeader(signature.appId());
+        if (StrUtil.isBlank(appId)) {
+            return false;
+        }
+        String timestamp = request.getHeader(signature.timestamp());
+        if (StrUtil.isBlank(timestamp)) {
+            return false;
+        }
+        String nonce = request.getHeader(signature.nonce());
+        if (StrUtil.length(nonce) < 10) {
+            return false;
+        }
+        String sign = request.getHeader(signature.sign());
+        if (StrUtil.isBlank(sign)) {
+            return false;
+        }
+
+        // 2. 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值)
+        long expireTime = signature.timeUnit().toMillis(signature.timeout());
+        long requestTimestamp = Long.parseLong(timestamp);
+        long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp);
+        if (timestampDisparity > expireTime) {
+            return false;
+        }
+
+        // 3. 检查 nonce 是否存在,有且仅能使用一次
+        return signatureRedisDAO.getNonce(nonce) == null;
+    }
+
+    /**
+     * 构建签名字符串
+     *
+     * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
+     *
+     * @param signature signature
+     * @param request   request
+     * @param appSecret appSecret
+     * @return 签名字符串
+     */
+    private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) {
+        SortedMap<String, String> parameterMap = getRequestParameterMap(request); // 请求头
+        SortedMap<String, String> headerMap = getRequestHeaderMap(signature, request); // 请求参数
+        String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); // 请求体
+        return MapUtil.join(parameterMap, "&", "=")
+                + requestBody
+                + MapUtil.join(headerMap, "&", "=")
+                + appSecret;
+    }
+
+    /**
+     * 获取请求头加签参数 Map
+     *
+     * @param request 请求
+     * @param signature 签名注解
+     * @return signature params
+     */
+    private static SortedMap<String, String> getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) {
+        SortedMap<String, String> sortedMap = new TreeMap<>();
+        sortedMap.put(signature.appId(), request.getHeader(signature.appId()));
+        sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp()));
+        sortedMap.put(signature.nonce(), request.getHeader(signature.nonce()));
+        return sortedMap;
+    }
+
+    /**
+     * 获取请求参数 Map
+     *
+     * @param request 请求
+     * @return queryParams
+     */
+    private static SortedMap<String, String> getRequestParameterMap(HttpServletRequest request) {
+        SortedMap<String, String> sortedMap = new TreeMap<>();
+        for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
+            sortedMap.put(entry.getKey(), entry.getValue()[0]);
+        }
+        return sortedMap;
+    }
+
+}
+

+ 57 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java

@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.framework.signature.core.redis;
+
+import lombok.AllArgsConstructor;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * HTTP API 签名 Redis DAO
+ *
+ * @author Zhougang
+ */
+@AllArgsConstructor
+public class ApiSignatureRedisDAO {
+
+    private final StringRedisTemplate stringRedisTemplate;
+
+    /**
+     * 验签随机数
+     *
+     * KEY 格式:signature_nonce:%s // 参数为 随机数
+     * VALUE 格式:String
+     * 过期时间:不固定
+     */
+    private static final String SIGNATURE_NONCE = "api_signature_nonce:%s";
+
+    /**
+     * 签名密钥
+     *
+     * HASH 结构
+     * KEY 格式:%s // 参数为 appid
+     * VALUE 格式:String
+     * 过期时间:永不过期(预加载到 Redis)
+     */
+    private static final String SIGNATURE_APPID = "api_signature_app";
+
+    // ========== 验签随机数 ==========
+
+    public String getNonce(String nonce) {
+        return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce));
+    }
+
+    public void setNonce(String nonce, int time, TimeUnit timeUnit) {
+        stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), "", time, timeUnit);
+    }
+
+    private static String formatNonceKey(String key) {
+        return String.format(SIGNATURE_NONCE, key);
+    }
+
+    // ========== 签名密钥 ==========
+
+    public String getAppSecret(String appId) {
+        return (String) stringRedisTemplate.opsForHash().get(SIGNATURE_APPID, appId);
+    }
+
+}

+ 6 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * HTTP API 签名,校验安全性
+ *
+ * @see <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3>微信支付 —— 安全规范</a>
+ */
+package cn.iocoder.yudao.framework.signature;

+ 2 - 1
yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -1,3 +1,4 @@
 cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration
 cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration
-cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration
+cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration
+cn.iocoder.yudao.framework.signature.config.YudaoApiSignatureAutoConfiguration

+ 75 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java

@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.framework.signature.core;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature;
+import cn.iocoder.yudao.framework.signature.core.aop.ApiSignatureAspect;
+import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
+import jakarta.servlet.http.HttpServletRequest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link ApiSignatureTest} 的单元测试
+ */
+@ExtendWith(MockitoExtension.class)
+public class ApiSignatureTest {
+
+    @InjectMocks
+    private ApiSignatureAspect apiSignatureAspect;
+
+    @Mock
+    private ApiSignatureRedisDAO signatureRedisDAO;
+
+    @Test
+    public void testSignatureGet() throws IOException {
+        // 搞一个签名
+        Long timestamp = System.currentTimeMillis();
+        String nonce = IdUtil.randomUUID();
+        String appId = "xxxxxx";
+        String appSecret = "yyyyyy";
+        String signString = "k1=v1&v1=k1testappId=xxxxxx&nonce=" + nonce + "&timestamp=" + timestamp + "yyyyyy";
+        String sign = DigestUtil.sha256Hex(signString);
+
+        // 准备参数
+        ApiSignature apiSignature = mock(ApiSignature.class);
+        when(apiSignature.appId()).thenReturn("appId");
+        when(apiSignature.timestamp()).thenReturn("timestamp");
+        when(apiSignature.nonce()).thenReturn("nonce");
+        when(apiSignature.sign()).thenReturn("sign");
+        when(apiSignature.timeout()).thenReturn(60);
+        when(apiSignature.timeUnit()).thenReturn(TimeUnit.SECONDS);
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getHeader(eq("appId"))).thenReturn(appId);
+        when(request.getHeader(eq("timestamp"))).thenReturn(String.valueOf(timestamp));
+        when(request.getHeader(eq("nonce"))).thenReturn(nonce);
+        when(request.getHeader(eq("sign"))).thenReturn(sign);
+        when(request.getParameterMap()).thenReturn(MapUtil.<String, String[]>builder()
+                .put("v1", new String[]{"k1"}).put("k1", new String[]{"v1"}).build());
+        when(request.getContentType()).thenReturn("application/json");
+        when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test")));
+        // mock 方法
+        when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret);
+
+        // 调用
+        boolean result = apiSignatureAspect.verifySignature(apiSignature, request);
+        // 断言结果
+        assertTrue(result);
+        // 断言调用
+        verify(signatureRedisDAO).setNonce(eq(nonce), eq(120), eq(TimeUnit.SECONDS));
+    }
+
+}

+ 2 - 1
yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessStatusDO.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.crm.dal.dataobject.business;
 
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -19,7 +20,7 @@ import lombok.*;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class CrmBusinessStatusDO {
+public class CrmBusinessStatusDO extends BaseDO {
 
     /**
      * 主键

+ 2 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileContentDO.java

@@ -28,8 +28,8 @@ public class FileContentDO extends BaseDO {
     /**
      * 编号,数据库自增
      */
-    @TableId(type = IdType.INPUT)
-    private String id;
+    @TableId
+    private Long id;
     /**
      * 配置编号
      *

+ 0 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/logger/ApiErrorLogDO.java

@@ -25,7 +25,6 @@ import java.time.LocalDateTime;
 @KeySequence(value = "infra_api_error_log_seq")
 public class ApiErrorLogDO extends BaseDO {
 
-
     /**
      * {@link #requestParams} 的最大长度
      */

+ 2 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java

@@ -342,7 +342,8 @@ public class CodegenEngine {
 
         // className 相关
         // 去掉指定前缀,将 TestDictType 转换成 DictType. 因为在 create 等方法后,不需要带上 Test 前缀
-        String simpleClassName = removePrefix(table.getClassName(), upperFirst(table.getModuleName()));
+        String simpleClassName = equalsAnyIgnoreCase(table.getClassName(), table.getModuleName()) ? table.getClassName()
+                : removePrefix(table.getClassName(), upperFirst(table.getModuleName()));
         bindingMap.put("simpleClassName", simpleClassName);
         bindingMap.put("simpleClassName_underlineCase", toUnderlineCase(simpleClassName)); // 将 DictType 转换成 dict_type
         bindingMap.put("classNameVar", lowerFirst(simpleClassName)); // 将 DictType 转换成 dictType,用于变量

+ 42 - 4
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java

@@ -1,33 +1,49 @@
 package cn.iocoder.yudao.module.trade.service.order;
 
+import cn.hutool.core.util.IdUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import cn.iocoder.yudao.module.member.api.address.MemberAddressApi;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
 import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
+import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
 import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
 import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
 import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
+import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
+import cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.express.DeliveryExpressCreateReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
 import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
 import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderItemMapper;
 import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper;
+import cn.iocoder.yudao.module.trade.dal.redis.no.TradeNoRedisDAO;
+import cn.iocoder.yudao.module.trade.enums.delivery.DeliveryTypeEnum;
+import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
 import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderConfig;
 import cn.iocoder.yudao.module.trade.framework.order.config.TradeOrderProperties;
+import cn.iocoder.yudao.module.trade.service.cart.CartServiceImpl;
+import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
+import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressServiceImpl;
+import cn.iocoder.yudao.module.trade.service.message.TradeMessageServiceImpl;
+import cn.iocoder.yudao.module.trade.service.order.handler.TradeOrderHandler;
+import cn.iocoder.yudao.module.trade.service.price.TradePriceServiceImpl;
+import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculator;
+import jakarta.annotation.Resource;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
 
-import jakarta.annotation.Resource;
 import java.time.Duration;
 
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
 
@@ -38,7 +54,9 @@ import static org.mockito.Mockito.when;
  * @since 2022-09-07
  */
 @Disabled // TODO 芋艿:后续 fix 补充的单测
-@Import({TradeOrderUpdateServiceImpl.class, TradeOrderConfig.class})
+@Import({TradeOrderUpdateServiceImpl.class, TradeOrderConfig.class, CartServiceImpl.class, TradePriceServiceImpl.class,
+        DeliveryExpressServiceImpl.class, TradeMessageServiceImpl.class
+})
 public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
 
     @Resource
@@ -55,7 +73,9 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
     private ProductSpuApi productSpuApi;
     @MockBean
     private ProductSkuApi productSkuApi;
-//    @MockBean
+    @MockBean
+    private ProductCommentApi productCommentApi;
+    //    @MockBean
 //    private PriceApi priceApi;
     @MockBean
     private PayOrderApi payOrderApi;
@@ -66,11 +86,22 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
 
     @MockBean
     private TradeOrderProperties tradeOrderProperties;
+    @MockBean
+    private TradeNoRedisDAO tradeNoRedisDAO;
+    @MockBean
+    private TradeOrderHandler tradeOrderHandler;
+    @MockBean
+    private TradePriceCalculator tradePriceCalculator;
+    @MockBean
+    private NotifyMessageSendApi notifyMessageSendApi;
+    @MockBean
+    private DeliveryExpressService deliveryExpressService;
 
     @BeforeEach
     public void setUp() {
         when(tradeOrderProperties.getAppId()).thenReturn(888L);
         when(tradeOrderProperties.getPayExpireTime()).thenReturn(Duration.ofDays(1));
+        when(tradeNoRedisDAO.generate(anyString())).thenReturn(IdUtil.randomUUID());
     }
 
 //    @Test
@@ -259,11 +290,18 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
         TradeOrderDO order = randomPojo(TradeOrderDO.class, o -> {
             o.setId(1L).setStatus(TradeOrderStatusEnum.UNDELIVERED.getStatus());
             o.setLogisticsId(null).setLogisticsNo(null).setDeliveryTime(null);
+            o.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus());
+            o.setDeliveryType(DeliveryTypeEnum.EXPRESS.getType());
         });
         tradeOrderMapper.insert(order);
+
+        DeliveryExpressCreateReqVO expressCreateReqVO = new DeliveryExpressCreateReqVO();
+        expressCreateReqVO.setCode("code").setName("Name").setLogo("logo").setSort(0).setStatus(CommonStatusEnum.ENABLE.getStatus());
+        Long deliveryExpressId = deliveryExpressService.createDeliveryExpress(expressCreateReqVO);
         // 准备参数
         TradeOrderDeliveryReqVO deliveryReqVO = new TradeOrderDeliveryReqVO().setId(1L)
-                .setLogisticsId(10L).setLogisticsNo("100");
+                .setLogisticsId(deliveryExpressId).setLogisticsNo("100");
+
         // mock 方法(支付单)
 
         // 调用

+ 159 - 116
yudao-module-mall/yudao-module-trade-biz/src/test/resources/sql/create_tables.sql

@@ -1,128 +1,155 @@
-CREATE TABLE IF NOT EXISTS "trade_order" (
-     "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-     "no" varchar NOT NULL,
-     "type" int NOT NULL,
-     "terminal" int NOT NULL,
-     "user_id" bigint NOT NULL,
-     "user_ip" varchar NOT NULL,
-     "user_remark" varchar,
-     "status" int NOT NULL,
-     "product_count" int NOT NULL,
-     "cancel_type" int,
-     "remark" varchar,
-     "pay_status" bit NOT NULL,
-     "pay_time" datetime,
-     "finish_time" datetime,
-     "cancel_time" datetime,
-     "original_price" int NOT NULL,
-     "order_price" int NOT NULL,
-     "discount_price" int NOT NULL,
-     "delivery_price" int NOT NULL,
-     "adjust_price" int NOT NULL,
-     "pay_price" int NOT NULL,
-     "pay_order_id" bigint,
-     "pay_channel_code" varchar,
-     "delivery_template_id" bigint,
-     "logistics_id" bigint,
-     "logistics_no" varchar,
-     "delivery_time" datetime,
-     "receive_time" datetime,
-     "receiver_name" varchar NOT NULL,
-     "receiver_mobile" varchar NOT NULL,
-     "receiver_area_id" int NOT NULL,
-     "receiver_post_code" int,
-     "receiver_detail_address" varchar NOT NULL,
-     "after_sale_status" int NOT NULL,
-     "refund_price" int NOT NULL,
-     "coupon_id" bigint NOT NULL,
-     "coupon_price" int NOT NULL,
-     "point_price" int NOT NULL,
-     "creator" varchar DEFAULT '',
-     "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-     "updater" varchar DEFAULT '',
-     "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-     "deleted" bit NOT NULL DEFAULT FALSE,
-     PRIMARY KEY ("id")
+CREATE TABLE IF NOT EXISTS "trade_order"
+(
+    "id"                      bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "no"                      varchar  NOT NULL,
+    "type"                    int      NOT NULL,
+    "terminal"                int      NOT NULL,
+    "user_id"                 bigint   NOT NULL,
+    "user_ip"                 varchar  NOT NULL,
+    "user_remark"             varchar,
+    "status"                  int      NOT NULL,
+    "product_count"           int      NOT NULL,
+    "cancel_type"             int,
+    "remark"                  varchar,
+    "comment_status"          boolean,
+    "brokerage_user_id"       bigint,
+    "pay_status"              bit      NOT NULL,
+    "pay_time"                datetime,
+    "finish_time"             datetime,
+    "cancel_time"             datetime,
+    "total_price"             int      NULL,
+    "order_price"             int      NULL,
+    "discount_price"          int      NOT NULL,
+    "delivery_price"          int      NOT NULL,
+    "adjust_price"            int      NOT NULL,
+    "pay_price"               int      NOT NULL,
+    "delivery_type"           int      NOT NULL,
+    "pay_order_id"            bigint,
+    "pay_channel_code"        varchar,
+    "delivery_template_id"    bigint,
+    "logistics_id"            bigint,
+    "logistics_no"            varchar,
+    "delivery_time"           datetime,
+    "receive_time"            datetime,
+    "receiver_name"           varchar  NOT NULL,
+    "receiver_mobile"         varchar  NOT NULL,
+    "receiver_area_id"        int      NOT NULL,
+    "receiver_post_code"      int,
+    "receiver_detail_address" varchar  NOT NULL,
+    "pick_up_store_id"        long     NULL,
+    "pick_up_verify_code"     varchar  NULL,
+    "refund_status"           int      NULL,
+    "refund_price"            int      NULL,
+    "after_sale_status"       int      NULL,
+    "coupon_id"               bigint   NOT NULL,
+    "coupon_price"            int      NOT NULL,
+    "use_point"               int      NULL,
+    "point_price"             int      NOT NULL,
+    "give_point"              int      NULL,
+    "refund_point"            int      NULL,
+    "vip_price"               int      NULL,
+    "seckill_activity_id"     long     NULL,
+    "bargain_activity_id"     long     NULL,
+    "bargain_record_id"       long     NULL,
+    "combination_activity_id" long     NULL,
+    "combination_head_id"     long     NULL,
+    "combination_record_id"   long     NULL,
+    "creator"                 varchar           DEFAULT '',
+    "create_time"             datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"                 varchar           DEFAULT '',
+    "update_time"             datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"                 bit      NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
 ) COMMENT '交易订单表';
 
-CREATE TABLE IF NOT EXISTS "trade_order_item" (
-      "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-      "user_id" bigint NOT NULL,
-      "order_id" bigint NOT NULL,
-      "spu_id" bigint NOT NULL,
-      "spu_name" varchar NOT NULL,
-      "sku_id" bigint NOT NULL,
-      "properties" varchar,
-      "pic_url" varchar,
-      "count" int NOT NULL,
-      "original_price" int NOT NULL,
-      "original_unit_price" int NOT NULL,
-      "discount_price" int NOT NULL,
-      "pay_price" int NOT NULL,
-      "order_part_price" int NOT NULL,
-      "order_divide_price" int NOT NULL,
-      "after_sale_status" int NOT NULL,
-      "creator" varchar DEFAULT '',
-      "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-      "updater" varchar DEFAULT '',
-      "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-      "deleted" bit NOT NULL DEFAULT FALSE,
-      PRIMARY KEY ("id")
+CREATE TABLE IF NOT EXISTS "trade_order_item"
+(
+    "id"                bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"           bigint   NOT NULL,
+    "order_id"          bigint   NOT NULL,
+    "cart_id"           int      NULL,
+    "spu_id"            bigint   NOT NULL,
+    "spu_name"          varchar  NOT NULL,
+    "sku_id"            bigint   NOT NULL,
+    "properties"        varchar,
+    "pic_url"           varchar,
+    "count"             int      NOT NULL,
+    "comment_status"    boolean  NULL,
+    "price"             int      NOT NULL,
+    "discount_price"    int      NOT NULL,
+    "delivery_price"    int      NULL,
+    "adjust_price"      int      NULL,
+    "pay_price"         int      NOT NULL,
+    "coupon_price"      int      NULL,
+    "point_price"       int      NULL,
+    "use_point"         int      NULL,
+    "give_point"        int      NULL,
+    "vip_price"         int      NULL,
+    "after_sale_id"     long     NULL,
+    "after_sale_status" int      NOT NULL,
+    "creator"           varchar           DEFAULT '',
+    "create_time"       datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"           varchar           DEFAULT '',
+    "update_time"       datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"           bit      NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
 ) COMMENT '交易订单明细表';
 
-CREATE TABLE IF NOT EXISTS "trade_after_sale" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "no" varchar NOT NULL,
-    "status" int NOT NULL,
-    "type" int NOT NULL,
-    "way" int NOT NULL,
-    "user_id" bigint NOT NULL,
-    "apply_reason" varchar NOT NULL,
+CREATE TABLE IF NOT EXISTS "trade_after_sale"
+(
+    "id"             bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "no"             varchar  NOT NULL,
+    "status"         int      NOT NULL,
+    "type"           int      NOT NULL,
+    "way"            int      NOT NULL,
+    "user_id"        bigint   NOT NULL,
+    "apply_reason"   varchar  NOT NULL,
     "apply_description" varchar,
     "apply_pic_urls" varchar,
-    "order_id" bigint NOT NULL,
-    "order_no" varchar NOT NULL,
-    "order_item_id" bigint NOT NULL,
-    "spu_id" bigint NOT NULL,
-    "spu_name" varchar NOT NULL,
-    "sku_id" bigint NOT NULL,
-    "properties" varchar,
-    "pic_url" varchar,
-    "count" int NOT NULL,
-    "audit_time" varchar,
-    "audit_user_id" bigint,
-    "audit_reason" varchar,
-    "refund_price" int NOT NULL,
-    "pay_refund_id" bigint,
-    "refund_time" varchar,
-    "logistics_id" bigint,
-    "logistics_no" varchar,
-    "delivery_time" varchar,
-    "receive_time" varchar,
+    "order_id"       bigint   NOT NULL,
+    "order_no"       varchar  NOT NULL,
+    "order_item_id"  bigint   NOT NULL,
+    "spu_id"         bigint   NOT NULL,
+    "spu_name"       varchar  NOT NULL,
+    "sku_id"         bigint   NOT NULL,
+    "properties"     varchar,
+    "pic_url"        varchar,
+    "count"          int      NOT NULL,
+    "audit_time"     varchar,
+    "audit_user_id"  bigint,
+    "audit_reason"   varchar,
+    "refund_price"   int      NOT NULL,
+    "pay_refund_id"  bigint,
+    "refund_time"    varchar,
+    "logistics_id"   bigint,
+    "logistics_no"   varchar,
+    "delivery_time"  varchar,
+    "receive_time"   varchar,
     "receive_reason" varchar,
-    "creator" varchar DEFAULT '',
-    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar DEFAULT '',
-    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+    "creator"        varchar           DEFAULT '',
+    "create_time"    datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"        varchar           DEFAULT '',
+    "update_time"    datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"        bit      NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
 ) COMMENT '交易售后表';
 
-CREATE TABLE IF NOT EXISTS "trade_after_sale_log" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "user_id" bigint NOT NULL,
-    "user_type" int NOT NULL,
-    "after_sale_id" bigint NOT NULL,
-    "order_id" bigint NOT NULL,
-    "order_item_id" bigint NOT NULL,
+CREATE TABLE IF NOT EXISTS "trade_after_sale_log"
+(
+    "id"            bigint   NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "user_id"       bigint   NOT NULL,
+    "user_type"     int      NOT NULL,
+    "after_sale_id" bigint   NOT NULL,
+    "order_id"      bigint   NOT NULL,
+    "order_item_id" bigint   NOT NULL,
     "before_status" int,
-    "after_status" int NOT NULL,
-    "content" varchar NOT NULL,
-    "creator" varchar DEFAULT '',
-    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updater" varchar DEFAULT '',
-    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    "deleted" bit NOT NULL DEFAULT FALSE,
+    "after_status"  int      NOT NULL,
+    "content"       varchar  NOT NULL,
+    "creator"       varchar           DEFAULT '',
+    "create_time"   datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"       varchar           DEFAULT '',
+    "update_time"   datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"       bit      NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
 ) COMMENT '交易售后日志';
 
@@ -161,7 +188,7 @@ CREATE TABLE IF NOT EXISTS "trade_brokerage_record"
     "updater"       varchar           DEFAULT '',
     "update_time"   datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     "deleted"       bit      NOT NULL DEFAULT FALSE,
-    "tenant_id"      bigint   not null default '0',
+    "tenant_id" bigint not null default '0',
     PRIMARY KEY ("id")
 ) COMMENT '佣金记录';
 CREATE TABLE IF NOT EXISTS "trade_brokerage_withdraw"
@@ -186,6 +213,22 @@ CREATE TABLE IF NOT EXISTS "trade_brokerage_withdraw"
     "updater"             varchar           DEFAULT '',
     "update_time"         datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     "deleted"             bit      NOT NULL DEFAULT FALSE,
-    "tenant_id"      bigint   not null default '0',
+    "tenant_id" bigint not null default '0',
+    PRIMARY KEY ("id")
+) COMMENT '佣金提现';
+
+CREATE TABLE IF NOT EXISTS "trade_delivery_express"
+(
+    "id"          int      NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "code"        varchar  NULL,
+    "name"        varchar,
+    "logo"        varchar  NULL,
+    "sort"        int      NOT NULL,
+    "status"      int      NOT NULL,
+    "creator"     varchar           DEFAULT '',
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater"     varchar           DEFAULT '',
+    "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    "deleted"     bit      NOT NULL DEFAULT FALSE,
     PRIMARY KEY ("id")
 ) COMMENT '佣金提现';

+ 1 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/common/SexEnum.java

@@ -11,6 +11,7 @@ import lombok.Getter;
 @Getter
 @AllArgsConstructor
 public enum SexEnum {
+
     /** 男 */
     MALE(1),
     /** 女 */

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java

@@ -109,7 +109,7 @@ public class AuthController {
         // 1.3 获得菜单列表
         Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId));
         List<MenuDO> menuList = menuService.getMenuList(menuIds);
-        menuList.removeIf(menu -> !CommonStatusEnum.ENABLE.getStatus().equals(menu.getStatus())); // 移除禁用的菜单
+        menuList = menuService.filterDisableMenus(menuList);
 
         // 2. 拼接结果返回
         return success(AuthConvert.INSTANCE.convert(user, roles, menuList));

+ 1 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java

@@ -72,6 +72,7 @@ public class MenuController {
     public CommonResult<List<MenuSimpleRespVO>> getSimpleMenuList() {
         List<MenuDO> list = menuService.getMenuListByTenant(
                 new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        list = menuService.filterDisableMenus(list);
         list.sort(Comparator.comparing(MenuDO::getSort));
         return success(BeanUtils.toBean(list, MenuSimpleRespVO.class));
     }

+ 8 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuService.java

@@ -52,6 +52,14 @@ public interface MenuService {
      */
     List<MenuDO> getMenuListByTenant(MenuListReqVO reqVO);
 
+    /**
+     * 过滤掉关闭的菜单及其子菜单
+     *
+     * @param list 菜单列表
+     * @return 过滤后的菜单列表
+     */
+    List<MenuDO> filterDisableMenus(List<MenuDO> list);
+
     /**
      * 筛选菜单列表
      *

+ 49 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.module.system.service.permission;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
@@ -19,11 +21,11 @@ import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.Collection;
-import java.util.List;
+import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
 import static cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO.ID_ROOT;
 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
 
@@ -106,12 +108,57 @@ public class MenuServiceImpl implements MenuService {
 
     @Override
     public List<MenuDO> getMenuListByTenant(MenuListReqVO reqVO) {
+        // 查询所有菜单,并过滤掉关闭的节点
         List<MenuDO> menus = getMenuList(reqVO);
         // 开启多租户的情况下,需要过滤掉未开通的菜单
         tenantService.handleTenantMenu(menuIds -> menus.removeIf(menu -> !CollUtil.contains(menuIds, menu.getId())));
         return menus;
     }
 
+    @Override
+    public List<MenuDO> filterDisableMenus(List<MenuDO> menuList) {
+        if (CollUtil.isEmpty(menuList)){
+            return Collections.emptyList();
+        }
+        Map<Long, MenuDO> menuMap = convertMap(menuList, MenuDO::getId);
+
+        // 遍历 menu 菜单,查找不是禁用的菜单,添加到 enabledMenus 结果
+        List<MenuDO> enabledMenus = new ArrayList<>();
+        Set<Long> disabledMenuCache = new HashSet<>(); // 存下递归搜索过被禁用的菜单,防止重复的搜索
+        for (MenuDO menu : menuList) {
+            if (isMenuDisabled(menu, menuMap, disabledMenuCache)) {
+                continue;
+            }
+            enabledMenus.add(menu);
+        }
+        return enabledMenus;
+    }
+
+    private boolean isMenuDisabled(MenuDO node, Map<Long, MenuDO> menuMap, Set<Long> disabledMenuCache) {
+        // 如果已经判定是禁用的节点,直接结束
+        if (disabledMenuCache.contains(node.getId())) {
+            return true;
+        }
+
+        // 1. 遍历到 parentId 为根节点,则无需判断
+        Long parentId = node.getParentId();
+        if (ObjUtil.equal(parentId, ID_ROOT)) {
+            if (CommonStatusEnum.isDisable(node.getStatus())) {
+                disabledMenuCache.add(node.getId());
+                return true;
+            }
+            return false;
+        }
+
+        // 2. 继续遍历 parent 节点
+        MenuDO parent = menuMap.get(parentId);
+        if (parent == null || isMenuDisabled(parent, menuMap, disabledMenuCache)) {
+            disabledMenuCache.add(node.getId());
+            return true;
+        }
+        return false;
+    }
+
     @Override
     public List<MenuDO> getMenuList(MenuListReqVO reqVO) {
         return menuMapper.selectList(reqVO);