1. 项目概览

1.1 基本信息

属性
Group ID com.ruoyi
Artifact ID ruoyi
版本 3.9.2
Spring Boot 4.0.6
JDK 17
构建工具 Maven(阿里云镜像)
ORM MyBatis 4.0.1
连接池 Alibaba Druid 1.2.28
安全框架 Spring Security + JWT(jjwt 0.9.1)
API 文档 SpringDoc 3.0.3
分页 PageHelper 4.1.0

1.2 模块构成

ruoyi-vue (父 POM)
├── ruoyi-admin        ← Web 入口层(Controller、启动类、application.yml)
├── ruoyi-framework    ← 安全 + AOP + 配置层
├── ruoyi-system       ← 业务层(Domain、Mapper、Service)
├── ruoyi-common       ← 公共组件(注解、枚举、异常、工具类、Redis 缓存)
├── ruoyi-quartz       ← Quartz 定时任务
└── ruoyi-generator    ← Velocity 代码生成器

1.3 CodeGraph 统计

指标 数值
扫描文件数 300+
符号总数 1000+
控制器数 18
服务接口数 15
Mapper 接口数 12
实体类数 22
AOP 切面数 6
单元测试数 0

2. 模块架构与依赖关系

2.1 当前依赖方向(存在问题)

ruoyi-admin
  └─ ruoyi-framework
       └─ ruoyi-system
            └─ ruoyi-common
       └─ ruoyi-quartz      ← 同级依赖
       └─ ruoyi-generator   ← 同级依赖

这是 Maven POM 中声明的依赖关系。ruoyi-framework 依赖 ruoyi-systemruoyi-system 依赖 ruoyi-common

2.2 实际代码中的反向依赖(CodeGraph 发现)

CodeGraph 的调用链分析揭示了 多个违反 POM 声明的反向依赖

问题 1:ruoyi-common → Spring Security(反向依赖)

SecurityUtils(位于 ruoyi-common)直接引用:

  • org.springframework.security.core.Authentication
  • org.springframework.security.core.context.SecurityContextHolder
  • org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder

CodeGraph 数据:SecurityUtils 有 18 个调用者

问题 2:ruoyi-common → Redis(基础设施反向依赖)

RedisCache(位于 ruoyi-common)直接依赖 org.springframework.data.redis.core.RedisTemplate,而 RedisTemplate 的 Bean 定义在 ruoyi-framework 的 RedisConfig 中配置。

CodeGraph 数据:RedisCache 有 17 个调用者

问题 3:域实体放在 ruoyi-common

SysUserSysRoleSysMenuSysDept 等核心域实体全部位于 ruoyi-common 模块,但它们本质上是业务领域对象,应该属于 ruoyi-system

CodeGraph 数据:SysUser 有 106 个调用者

问题 4:ruoyi-framework 强依赖 ruoyi-system

CodeGraph 追踪到 6 个 framework 文件 import 了 ruoyi-system 的 Service 和实体:

framework 源文件 导入的 ruoyi-system 类
web/service/UserDetailsServiceImpl.java ISysUserService
web/service/SysRegisterService.java ISysConfigServiceISysUserService
web/service/SysPermissionService.java ISysMenuServiceISysRoleService
web/service/SysLoginService.java ISysConfigServiceISysUserService
manager/factory/AsyncFactory.java SysLogininforSysOperLogISysLogininforServiceISysOperLogService
aspectj/LogAspect.java SysOperLog

3. 核心架构问题清单

3.1 问题总览(按严重程度排序)

编号 问题 严重程度 影响范围 CodeGraph 证据
P0-1 AjaxResult extends HashMap 类型不安全 🔴 严重 全局 所有 Controller 和异常处理器均使用
P0-2 两套响应封装并存(AjaxResult + R<T> 🔴 严重 全局 TestController 用 R<T>,其他用 AjaxResult
P0-3 零测试覆盖 🔴 严重 全局 CodeGraph 每个符号都标注 "⚠️ no covering tests"
P0-4 ruoyi-common 模块职责过重、反向依赖 🔴 严重 所有模块 SecurityUtils(18调用者)、RedisCache(17调用者)
P0-5 依赖方向错误:业务实体错放 common 🔴 严重 全局 SysUser(106调用者) 等实体在 common 中
P0-6 ruoyi-framework → ruoyi-system 业务依赖 🔴 严重 framework 6 个文件直接调用 system 的 Service
P1-1 SysUser 上帝对象(62成员, 106调用者) 🟠 重要 多模块 CodeGraph 统计 106 个调用者
P1-2 SpringUtils 服务定位器反模式(13处使用) 🟠 重要 跨模块 CodeGraph 统计 12 处 getBean() 调用
P1-3 BaseController 继承反模式(18子类) 🟠 重要 所有 Controller CodeGraph 统计 18 个调用者
P1-4 DataScopeAspect SQL 拼接注入风险 🟠 重要 ruoyi-framework 字符串拼接构建 SQL WHERE 子句
P1-5 RuoYiConfig 静态字段与实例 setter 混用 🟠 重要 全局 16 个调用者
P1-6 PageHelper ThreadLocal 分页泄漏风险 🟠 重要 所有分页查询 startPage() 未配对 clearPage()
P1-7 日志系统紧耦合:AsyncManager + AsyncFactory 🟠 重要 framework→system 静态方法 + SpringUtils.getBean + TimerTask
P2-1 JWT 密钥硬编码 🟡 一般 ruoyi-framework secret: abcdefghijklmnopqrstuvwxyz
P2-2 Druid 控制台密码硬编码 🟡 一般 ruoyi-framework login-username: ruoyi
P2-3 TestController 存在于生产代码中 🟡 一般 ruoyi-admin 含硬编码测试数据
P2-4 ServiceException 可变状态 + 覆盖父类字段 🟡 一般 全局 40 个调用者
P2-5 线程池参数硬编码 🟡 一般 ruoyi-framework ThreadPoolConfig 中写死核心参数
P2-6 Validator 注解放在 getter 上 🟡 一般 ruoyi-common 非标准 Spring 实践
P2-7 ISysXxxService 接口命名不规范 🟡 一般 ruoyi-system 不符合 Spring 生态惯例
P2-8 缺少 API 版本化 🟡 一般 ruoyi-admin URL 无版本前缀
P2-9 代码质量工具缺失 🟡 一般 全局 无 Checkstyle/PMD/SpotBugs 配置

4. 问题深度分析

4.1 P0-1:AjaxResult extends HashMap — 类型不安全

位置ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java:13

问题描述
AjaxResult 继承 HashMap<String, Object>,通过 put(key, value) 链式调用添加响应字段。这意味着:

  • 所有值的类型都是 Object,完全丧失编译期类型检查
  • 调用方不知道 data 字段里是什么类型,必须强制转换
  • 字段名("code""msg""data")是魔法字符串
  • Controller 之间没有统一的数据契约

CodeGraph 证据:所有 18 个 Controller 和 GlobalExceptionHandler 的 8 个异常处理方法全部返回 AjaxResult

建议改造:使用泛型响应类(如 ApiResponse<T>),定义强类型的 data 字段。


4.2 P0-2:两套响应封装并存

位置

  • AjaxResult extends HashMap<String, Object> — ruoyi-common/.../core/domain/AjaxResult.java
  • R<T> — ruoyi-common/.../core/domain/R.java

问题描述
项目中同时存在两套响应封装。AjaxResult 被所有生产 Controller 使用,而 R<T> 仅在 TestController 中使用。两个类的 API 风格完全不同:

  • AjaxResult.success(data) vs R.ok(data)
  • AjaxResult.error(msg) vs R.fail(msg)
  • AjaxResult 返回 this 用于链式 put()R<T> 是泛型不可变类

建议改造:统一为泛型响应类 ApiResponse<T>,废弃 AjaxResult 和 R<T>


4.3 P0-3:零测试覆盖

CodeGraph 证据:CodeGraph 分析中每个符号都标注了 "⚠️ no covering tests found"。src/test 目录不存在任何测试文件。

影响

  • 重构风险极高——无法验证改动是否破坏现有功能
  • DataScopeAspect 的 SQL 拼接逻辑完全无覆盖
  • 认证流程(TokenService → JwtAuthenticationTokenFilter → SecurityConfig)无集成测试
  • SysUser(106 调用者)的任何修改都可能产生连锁故障

建议改造:优先添加关键路径的集成测试(登录认证、数据权限 SQL 生成、分页查询)。


4.4 P0-4~P0-6:模块依赖方向错误与职责混乱(详见第 5~7 节)

这三个问题是相互关联的核心架构问题,将在后续章节中详细展开分析。


4.5 P1-1:SysUser 上帝对象

位置ruoyi-common/.../entity/SysUser.java:23

CodeGraph 数据:106 个调用者,62 个成员。

SysUser 承担了太多职责:

  1. 数据库实体 — userIduserNamepasswordemailphonenumbersexavatar 等
  2. 表单验证 — getter 上标注 @Xss@Size@NotBlank@Email
  3. JSON 序列化控制 — @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
  4. 业务逻辑 — isAdmin() 调用 SecurityUtils.isAdmin(this.userId)
  5. 关联查询载体 — SysDept deptList<SysRole> rolesLong[] roleIds/postIds
  6. Excel 导出注解 — 字段上有 @Excel 注解
  7. MyBatis 查询参数 — params 字段(继承自 BaseEntity

建议改造:拆分为 User(持久化实体)、UserCreateRequest(创建请求)、UserUpdateRequest(更新请求)、UserProfile(详情响应)、UserSummary(列表摘要)。


4.6 P1-2:SpringUtils 服务定位器反模式

位置ruoyi-common/.../utils/spring/SpringUtils.java:19

CodeGraph 数据:getBean() 方法被 12 处调用。

调用者 所在模块 获取的 Bean
DictUtils ruoyi-common RedisCache.class(4处)
MessageUtils ruoyi-common MessageSource.class
AsyncManager ruoyi-framework "scheduledExecutorService"
DruidConfig ruoyi-framework "slaveDataSource"
AsyncFactory ruoyi-framework ISysLogininforService.classISysOperLogService.class
ShutdownManager ruoyi-framework 线程池 Bean
CronUtils / JobInvokeUtil ruoyi-quartz 调度相关 Bean
GenUtils ruoyi-generator 生成器配置 Bean

建议改造:将 DictUtilsMessageUtils 改为 Spring Bean,通过 @Autowired 注入依赖。AsyncManager/AsyncFactory 待用 Spring Events 替换后删除。


4.7 P1-7:日志系统紧耦合 — AsyncManager + AsyncFactory

当前日志机制的三个核心问题:

  1. AsyncFactory 是静态工厂 + SpringUtils.getBean() 服务定位器,彻底绕过 DI
  2. 所有日志类型硬编码在一个类中,加新日志类型必须改 AsyncFactory
  3. 使用过时的 java.util.TimerTask,而非 Runnable/Callable
  4. AsyncFactory 直接依赖 system 的实体和 Service,是 framework→system 违规依赖的一部分

详细解决方案见 第 8 节


4.8 其他 P1/P2 问题

P1-3 BaseController 继承反模式:18 个 Controller 被迫继承一个包含分页、用户、响应、跳转等混杂职责的父类。建议将分页逻辑抽取为 PageService,用户信息通过 CurrentUserProvider 注入。

P1-4 DataScopeAspect SQL 注入风险StringUtils.format 将 {} 替换为参数值,不对 SQL 特殊字符转义。建议使用 MyBatis 参数化方式构建数据权限 SQL。

P1-5 RuoYiConfig 静态字段:实例 setter 修改静态字段,违反 Spring Bean 作用域语义。建议所有字段改为实例字段。

P1-6 PageHelper ThreadLocal 泄漏startPage() 未在 finally 块中配对 clearPage()

P2 级问题:JWT 密钥硬编码、Druid 密码硬编码、TestController 污染生产代码、ServiceException 可变状态、线程池硬编码、命名/版本化/质量工具缺失。


5. 依赖方向错误与业务实体错放 — 解决方案对比

5.1 问题根源

当前依赖方向:admin → framework → system → common。由于 framework 需要引用实体但无法反向依赖 system,于是将 SysUserSysRoleSysDeptSysMenuSysDictDataSysDictType 等业务实体全部塞入 common,导致 common 成为事实上的"业务中心"而非"公共工具库"。

5.2 方案对比

方案 A:DIP 接口方案 方案 B:独立 domain 模块 方案 C:Redis 缓存 + 登录桥接模式(推荐)
依赖方向 admin → system → framework → common admin → framework → system → domain ← common admin 是组合根,system 和 security/web 互不知晓
实体放哪 ruoyi-system 新建 ruoyi-domain ruoyi-system
framework 如何获取用户/角色数据 通过 DTO 接口(IUserService 等)间接调用 直接依赖 ruoyi-domain 中的实体 运行时零调用 — 登录时全部塞入 Redis LoginUser
需要新接口 4+ + 对应 DTO/BO 类 0 0(但需要 BO 替换 LoginUser 中的实体引用)
实现复杂度 中高 低(但新增模块)
根本解决问题 ✅ 彻底解耦 ⚠️ 模块增多,依赖方向仍不对 ✅ 利用 Redis 现有的 LoginUser 机制

5.3 推荐方案:方案 C(Redis 缓存 + 登录桥接)为主,辅以方案 A 的 DTO/BO 设计

CodeGraph 追踪发现了一个关键事实ruoyi-framework 对 ruoyi-system 的所有调用全部发生在登录/注册时,运行时(每次鉴权请求)是零调用的!

                            登录时(一次性)                        运行时(每次请求)
┌──────────────┐         ┌──────────────────────┐         ┌──────────────────────┐
│ ruoyi-admin  │ ──────→ │ UserDetailsServiceImpl│         │ JwtAuthTokenFilter   │
│ (登录编排器)  │         │ SysPermissionService  │         │ PermissionService(@ss)│
│              │         │ (查DB, 组装LoginUser)  │         │ DataScopeAspect      │
│              │         │ 存入 Redis            │         │ (全部读Redis LoginUser)│
└──────────────┘         └──────────────────────┘         └──────────────────────┘
       ↓                           ↓                              ↓
  调用 system.Service        调用 system.Service            零调用 system!
  + security.TokenService     + Redis                       纯 Redis 读取

结论:运行时 ruoyi-security 完全不需要调用 ruoyi-system——所有鉴权所需的数据已经全量存储在 Redis 的 LoginUser 中。

5.4 核心做法

  1. SysLoginServiceUserDetailsServiceImplSysPermissionService → 归入 ruoyi-admin(或 ruoyi-web 的登录子包),因为它们是登录流程的编排器,需要同时调用 system(DB)和 security(Redis/JWT)。它们不属于 framework 层。

  2. PermissionService@ss 留在 ruoyi-security,它只读 Redis LoginUser,不调用任何 system 的服务。

  3. LoginUser 中的 SysUser user 字段 → 逐步替换为 UserProfile BO,放在 ruoyi-security 或 ruoyi-common 中,只包含鉴权需要的字段(userIduserNamedeptIdrolespermissions),不含密码、手机号、邮箱等敏感/无关字段。

// 当前:LoginUser 持有 SysUser(实体,含密码、手机等敏感字段)
public class LoginUser {
    private SysUser user;  // 问题:security 模块因此依赖 SysUser 实体
}

// 改造后:LoginUser 持有 UserProfile(BO,仅鉴权所需字段)
public class UserProfile {
    private Long userId;
    private String userName;
    private Long deptId;
    private List<RoleInfo> roles;        // RoleInfo 也是 BO
    private Set<String> permissions;     // 权限字符串集合
    // 不包含:password, email, phonenumber, loginIp, avatar 等
}
  1. 角色/菜单权限变更后刷新在线用户:不在 SysRoleController/SysMenuController 中直接调用 TokenService,而是发布 PermissionChangeEvent,由 system 层的监听器处理在线用户权限刷新。

5.5 与 DIP 方案(方案 A)的对比

方案 A(纯 DIP 接口) 方案 C(Redis 缓存 + 登录桥接)
运行时 security → system 通过 DTO 接口间接调用 零调用(全 Redis)
需要的新接口数量 4+(IUserServiceIRoleServiceIConfigurationServiceIMenuService)+ DTO 类 无需新 Service 接口
依赖方向 system → security(反向,system 实现 security 的接口) admin 作为组合根,system 和 security 互不知晓
LoginUser 承载 DTO 需要单独从实体转换 现有 LoginUser 即可,按需替换字段为 BO
实现复杂度

推荐选择:方案 C 的主路径 + DTO/BO 替换 LoginUser 中的实体引用。不需要在 framework 定义额外的 Service 接口。


6. ruoyi-common 模块依赖倒置 — Maven 依赖瘦身方案

6.1 当前 common/pom.xml 中的问题依赖

通过分析 ruoyi-common/pom.xml,以下依赖不应该存在于底层通用模块中:

当前依赖 说明 问题 应移至
spring-boot-starter-security Spring Security 安全框架,不应在底层 ruoyi-security
spring-boot-starter-data-redis Redis 客户端 缓存基础设施,不应在底层 ruoyi-security(暂)/ 新模块
jjwt (0.9.1) JWT Token 认证实现细节 ruoyi-security
pagehelper-spring-boot-starter 分页插件 数据访问层关注点 ruoyi-framework
poi-ooxml (4.1.2) Excel 处理 重量级依赖(~10MB),特定功能 保留 common 但改为 optional,或移至 system

6.2 瘦身后的 common/pom.xml

<!-- ruoyi-common/pom.xml 瘦身后 —— 仅保留轻量工具依赖 -->
<dependencies>
    <!-- Spring 核心(轻量) -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
    </dependency>

    <!-- 参数校验 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- 轻量工具库 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
    </dependency>

    <!-- JSON 处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>

    <!-- Servlet API(编译期) -->
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <scope>provided</scope>
    </dependency>

    <!-- UserAgent 解析 -->
    <dependency>
        <groupId>nl.basjes.parse.useragent</groupId>
        <artifactId>yauaa</artifactId>
    </dependency>

    <!-- POI 改为 optional -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

6.3 代码迁移清单

从 ruoyi-common 移至 原因
SecurityUtils common/utils/ ruoyi-security 依赖 Spring Security
RedisCache common/core/redis/ ruoyi-security(暂) 依赖 RedisTemplate
BaseController common/core/controller/ ruoyi-web 依赖 PageHelper + Spring MVC
PageDomain / PageUtils common/core/page/ ruoyi-web 分页 = Web 层关注点
SysUserSysRoleSysDeptSysMenu common/core/domain/entity/ ruoyi-system 业务实体
SysDictDataSysDictType common/core/domain/entity/ ruoyi-system 业务实体
DictUtils common/utils/ ruoyi-system(改为 Bean) 字典缓存工具(依赖 RedisCache)
MessageUtils common/utils/ 改为 Spring Bean 依赖 MessageSource

6.4 依赖的正确分层(改造后)

Layer 组       模块             应包含的依赖(外部)
──────────────────────────────────────────────────────────
通用工具层     ruoyi-common      spring-context-support, spring-web,
                                 spring-boot-starter-validation,
                                 commons-lang3, commons-io,
                                 jackson-databind, yauaa,
                                 jakarta.servlet-api (provided),
                                 poi-ooxml (optional)
                                 [零 安全/缓存/持久化依赖]

安全层         ruoyi-security    spring-boot-starter-security,
 (依赖 common)                   spring-boot-starter-data-redis,
                                 jjwt, commons-pool2

Web 层         ruoyi-web         spring-boot-starter-webmvc,
 (依赖 security)                 spring-boot-starter-aspectj,
                                 pagehelper, kaptcha, oshi-core

数据源层        ruoyi-datasource  druid-spring-boot-4-starter
 (依赖 common)

业务层         ruoyi-system      [直接依赖 common]
 (依赖 common)

定时任务        ruoyi-quartz      spring-boot-starter-quartz
 (依赖 common)

代码生成        ruoyi-generator   velocity-engine-core
 (依赖 common)

入口层         ruoyi-admin       mysql-connector-j,
 (依赖 system+web+security)      springdoc-openapi,
                                 spring-boot-devtools

7. ruoyi-framework 模块拆分方案

7.1 拆分原则

ruoyi-framework 当前承担了安全认证、Web 基础设施、数据源配置三类完全不相关的职责。拆分后的各个模块不能依赖 ruoyi-system——system 是业务功能模块,framework 拆出的模块是开发框架性质的,不应有业务功能依赖。

7.2 推荐拆分:8 模块架构

ruoyi-admin          ← 入口 + Controller + 登录编排器
  ├── ruoyi-system   ← 实体 + Service + Mapper + 事件监听器
  │     └── ruoyi-common
  ├── ruoyi-web      ← LogAspect、DataScopeAspect、XssFilter、BaseController、全局异常
  │     └── ruoyi-security
  │           └── ruoyi-common
  ├── ruoyi-datasource ← DynamicDataSource、DruidConfig、DataSourceAspect
  │     └── ruoyi-common
  ├── ruoyi-quartz    ← 独立,仅依赖 ruoyi-common
  └── ruoyi-generator ← 独立,仅依赖 ruoyi-common

7.3 各模块职责与内容

模块 来源 职责 包含内容
ruoyi-admin 原 admin 启动入口 + Controller + 登录编排器 RuoYiApplication, 18 个 Controller, SysLoginServiceSysRegisterServiceUserDetailsServiceImplSysPermissionService(权限加载部分)
ruoyi-system 扩大 业务实体 + Service + Mapper + 事件监听器 SysUserSysRoleSysDeptSysMenuSysDictDataSysDictType 等实体(从 common 迁入); ISysXxxService / SysXxxServiceImpl / SysXxxMapper; LoginEventListenerOperationLogEventListenerConfigChangeEventListener
ruoyi-web (从 framework 拆分) AOP + Web 配置 LogAspectDataScopeAspectRateLimiterAspectRepeatSubmitInterceptorXssFilterBaseControllerGlobalExceptionHandlerCaptchaConfigAsyncEventConfig(@EnableAsync)
ruoyi-security (从 framework 拆分) 安全认证与鉴权 SecurityConfigJwtAuthenticationTokenFilterPermissionService(@ss), SecurityUtilsTokenServiceLoginUser + UserProfile BO, JWT 工具, SysPasswordService
ruoyi-datasource (从 framework 拆分) 多数据源配置 DynamicDataSourceDataSourceAspectDruidConfigDruidProperties
ruoyi-common 瘦身 纯工具库 + 事件 POJO 见第 6.2 节
ruoyi-quartz 保留 Quartz 调度 SysJobSysJobLogScheduleConfig
ruoyi-generator 保留 代码生成 GenTableGenTableColumn, Velocity 模板

7.4 关键类归属分析

SysLoginService / UserDetailsServiceImpl / SysPermissionService → ruoyi-admin

这三个类需要同时调用 ISysUserService(system 层)和 TokenService(security 层),是登录流程的编排器,属于组合根。放入 ruoyi-admin 的登录包:

ruoyi-admin/src/main/java/com/ruoyi/web/service/
├── SysLoginService.java           # 登录编排器
├── SysRegisterService.java        # 注册编排器
├── UserDetailsServiceImpl.java    # UserDetails 加载编排器
└── SysPermissionService.java      # 权限加载编排器(登录时一次性调)
PermissionService → ruoyi-security

PermissionService@Service("ss"))只读 Redis LoginUser零调用 system,安全留在 ruoyi-security

当前 framework → system 的依赖——全部通过事件或归属变更解除
当前调用 改造方式
UserDetailsServiceImpl → ISysUserService 归入 ruoyi-admin,合法依赖 system
SysLoginService → ISysConfigServiceISysUserService 归入 ruoyi-admin,合法依赖 system
SysPermissionService → ISysMenuServiceISysRoleService 归入 ruoyi-admin,合法依赖 system
AsyncFactory → ISysLogininforServiceISysOperLogService 删除 AsyncFactory,改用 Spring Events(见第 8 节)
LogAspect → SysOperLog 改用 OperationLogEvent 发布,不再直接引用实体

7.5 依赖规则总结

✅ 允许的依赖:
   ruoyi-admin → ruoyi-system(Controller 调 Service)
   ruoyi-admin → ruoyi-web(使用 AOP、Web 配置)
   ruoyi-admin → ruoyi-security(登录编排器调 TokenService)
   ruoyi-web → ruoyi-security(AOP 中需要 SecurityUtils)
   ruoyi-security → ruoyi-common
   ruoyi-web → ruoyi-common
   ruoyi-datasource → ruoyi-common
   ruoyi-system → ruoyi-common
   ruoyi-quartz → ruoyi-common
   ruoyi-generator → ruoyi-common

❌ 禁止的依赖:
   ruoyi-web → ruoyi-system            # 框架不依赖业务
   ruoyi-security → ruoyi-system       # 安全不依赖业务
   ruoyi-datasource → ruoyi-system     # 数据源不依赖业务
   ruoyi-common → 任何业务/框架模块     # common 保持纯净

8. Spring Events 解耦日志与配置变更

8.1 当前日志机制的缺陷

当前使用 AsyncManager + AsyncFactory + TimerTask

登录日志:
SysLoginService.login()
  → AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, status, msg))
  → [异步线程] IP定位 → UA解析 → new SysLogininfor()
  → SpringUtils.getBean(ISysLogininforService.class).insertLogininfor()
  → SysLogininforMapper

操作日志:
LogAspect (@Log拦截)
  → 组装 SysOperLog → AsyncManager.me().execute(AsyncFactory.recordOper(operLog))
  → [异步线程] 地址反查 → SpringUtils.getBean(ISysOperLogService.class).insertOperlog()
  → SysOperLogMapper
缺陷 具体表现
服务定位器 AsyncFactory 用 SpringUtils.getBean() 获取 Bean,违反 DI
不可测试 静态方法无法 mock
耦合集中 所有日志逻辑硬编码在 AsyncFactory 一个类中
TimerTask 过时 使用 java.util.TimerTask 而非 Runnable/Callable
冗余延迟 固定 10ms 延迟,所有日志类型一刀切
跨模块硬依赖 AsyncFactory 直接 import SysLogininforSysOperLog 等 system 实体

8.2 目标架构:Spring ApplicationEvent + @EventListener + @Async

发布者 (admin/web)             事件 POJO (common)           监听器 (system)
┌────────────────┐           ┌─────────────────┐          ┌─────────────────────┐
│ SysLoginService │ ─publish→  LoginEvent       │ ─listen→ LoginEventListener   │
│ LogAspect       │ ─publish→ OperationLogEvent  │ ─listen→ OplogEventListener    │
│ SysConfigContr. │ ─publish→ ConfigChangeEvent  │ ─listen→ ConfigEventListener   │
│ SysRoleContr.   │ ─publish→ PermissionChangeEv │ ─listen→ PermissionRefreshList │
└────────────────┘           └─────────────────┘          └─────────────────────┘
                                      ↑
                               ruoyi-common/event/
                               (纯 POJO,零依赖)

8.3 代码示例

事件 POJO → ruoyi-common/event/
// LoginEvent.java
@Getter
public class LoginEvent extends ApplicationEvent {
    private final String username;
    private final Long userId;
    private final String status;   // LOGIN_SUCCESS / LOGIN_FAIL / LOGOUT / REGISTER
    private final String message;

    public LoginEvent(Object source, String username, Long userId,
                      String status, String message) {
        super(source);
        this.username = username;
        this.userId = userId;
        this.status = status;
        this.message = message;
    }
}

// OperationLogEvent.java
@Getter
public class OperationLogEvent extends ApplicationEvent {
    private final String title;
    private final Integer businessType;
    private final Integer operatorType;
    private final String method;
    private final String requestMethod;
    private final String operName;
    private final String deptName;
    private final String operUrl;
    private final String operIp;
    private final String operParam;
    private final String jsonResult;
    private final Integer status;
    private final String errorMsg;
    private final Long costTime;

    public OperationLogEvent(Object source, String title, Integer businessType,
            Integer operatorType, String method, String requestMethod,
            String operName, String deptName, String operUrl, String operIp,
            String operParam, String jsonResult, Integer status,
            String errorMsg, Long costTime) {
        super(source);
        this.title = title;
        this.businessType = businessType;
        this.operatorType = operatorType;
        this.method = method;
        this.requestMethod = requestMethod;
        this.operName = operName;
        this.deptName = deptName;
        this.operUrl = operUrl;
        this.operIp = operIp;
        this.operParam = operParam;
        this.jsonResult = jsonResult;
        this.status = status;
        this.errorMsg = errorMsg;
        this.costTime = costTime;
    }
}

// ConfigChangeEvent.java
@Getter
public class ConfigChangeEvent extends ApplicationEvent {
    private final String configKey;
    private final String configValue;

    public ConfigChangeEvent(Object source, String configKey, String configValue) {
        super(source);
        this.configKey = configKey;
        this.configValue = configValue;
    }
}
监听器 → ruoyi-system/event/
// LoginEventListener.java
@Component
public class LoginEventListener {

    @Autowired
    private ISysLogininforService logininforService;

    @Async
    @EventListener
    public void handleLoginEvent(LoginEvent event) {
        SysLogininfor info = new SysLogininfor();
        info.setUserName(event.getUsername());
        info.setStatus(event.getStatus());
        info.setMsg(event.getMessage());

        // 从 RequestContextHolder 获取 IP、UA 等
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        if (ra != null) {
            HttpServletRequest request =
                ((ServletRequestAttributes) ra).getRequest();
            info.setIpaddr(IpUtils.getIpAddr(request));
            info.setLoginLocation(
                AddressUtils.getRealAddressByIP(info.getIpaddr()));
            String ua = request.getHeader("User-Agent");
            info.setBrowser(UserAgentUtils.parseBrowser(ua));
            info.setOs(UserAgentUtils.parseOs(ua));
        }
        logininforService.insertLogininfor(info);
    }
}

// OperationLogEventListener.java
@Component
public class OperationLogEventListener {

    @Autowired
    private ISysOperLogService operLogService;

    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @EventListener
    public void handleOperationLog(OperationLogEvent event) {
        SysOperLog log = new SysOperLog();
        log.setTitle(event.getTitle());
        log.setBusinessType(event.getBusinessType());
        log.setOperatorType(event.getOperatorType());
        log.setMethod(event.getMethod());
        log.setRequestMethod(event.getRequestMethod());
        log.setOperName(event.getOperName());
        log.setDeptName(event.getDeptName());
        log.setOperUrl(event.getOperUrl());
        log.setOperIp(event.getOperIp());
        log.setOperLocation(
            AddressUtils.getRealAddressByIP(event.getOperIp()));
        log.setOperParam(event.getOperParam());
        log.setJsonResult(event.getJsonResult());
        log.setStatus(event.getStatus());
        log.setErrorMsg(event.getErrorMsg());
        log.setCostTime(event.getCostTime());
        operLogService.insertOperlog(log);
    }
}
发布者改造示例
// SysLoginService (ruoyi-admin) —— 原 AsyncFactory 调用替换为事件发布
@Component
public class SysLoginService {

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public String login(String username, String password, String code, String uuid) {
        // ... 验证码校验、IP 校验 ...

        try {
            authentication = authenticationManager.authenticate(authenticationToken);
        } catch (BadCredentialsException e) {
            // 原来:AsyncManager.me().execute(AsyncFactory.recordLogininfor(...))
            eventPublisher.publishEvent(new LoginEvent(this, username, null,
                Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }

        // 登录成功
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        eventPublisher.publishEvent(new LoginEvent(this, username, loginUser.getUserId(),
            Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        return tokenService.createToken(loginUser);
    }
}

// LogAspect (ruoyi-web) —— 原 AsyncFactory 调用替换为事件发布
// 原来:AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
// 改为:
eventPublisher.publishEvent(new OperationLogEvent(this,
    operLog.getTitle(), operLog.getBusinessType(), /* ... */));
异步配置 → ruoyi-web/config/
@Configuration
@EnableAsync
public class AsyncEventConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(50);
        executor.setMaxPoolSize(200);
        executor.setQueueCapacity(1000);
        executor.setThreadNamePrefix("event-async-");
        executor.setRejectedExecutionHandler(new CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

8.4 模块归属总结

组件 模块 理由
LoginEvent ruoyi-common/event/ 纯 POJO,无任何框架依赖
OperationLogEvent ruoyi-common/event/ 纯 POJO,无任何框架依赖
ConfigChangeEvent ruoyi-common/event/ 纯 POJO,无任何框架依赖
LoginEventListener ruoyi-system/event/ 需注入 ISysLogininforService,需引用 SysLogininfor 实体
OperationLogEventListener ruoyi-system/event/ 需注入 ISysOperLogService,需引用 SysOperLog 实体
ConfigChangeEventListener ruoyi-system/event/ 需注入缓存服务
AsyncEventConfig ruoyi-web/config/ Web 层基础设施配置

8.5 可删除的文件清单

文件 动作 替换为
AsyncManager.java 删除 Spring @Async + @EventListener
AsyncFactory.java 删除 LoginEvent + OperationLogEvent 等事件 POJO
AsyncFactory.recordLogininfor() 的多处调用 替换 eventPublisher.publishEvent(new LoginEvent(...))
AsyncFactory.recordOper() 的调用 替换 eventPublisher.publishEvent(new OperationLogEvent(...))
ShutdownManager 中对 AsyncManager 的引用 清理 直接管理线程池

8.6 除日志外适合用 Event 解耦的场景

场景 事件名 发布者(模块) 监听者(模块)
登录成功/失败 LoginEvent SysLoginService (admin) LoginEventListener (system)
操作日志 OperationLogEvent LogAspect (web) OperationLogEventListener (system)
注册成功 RegisterEvent SysRegisterService (admin) LoginEventListener (system)
配置变更 ConfigChangeEvent SysConfigController (admin) ConfigChangeEventListener (system)
角色/菜单权限变更 PermissionChangeEvent SysRoleController / SysMenuController (admin) PermissionRefreshListener (system)
用户信息变更 UserProfileChangeEvent SysUserController (admin) UserCacheRefreshListener (system)
字典数据变更 DictChangeEvent SysDictDataController (admin) DictCacheRefreshListener (system)

9. 改造优先级与计划

9.1 第一阶段:关键缺陷修复(P0,约 2-3 周)

任务 描述 预期工时
T1.1 添加关键路径测试 为登录认证、JWT 过滤、数据权限 SQL 生成添加集成测试 4 天
T1.2 统一响应封装 用 ApiResponse<T> 替代 AjaxResult 和 R<T>,全局替换 3 天
T1.3 拆分 ruoyi-common SecurityUtils → ruoyi-security,域实体 → ruoyi-system 5 天
T1.4 Maven 依赖瘦身 common 移除 security/redis/jjwt/pagehelper 依赖 2 天

9.2 第二阶段:架构债务清理(P1,约 2-4 周)

任务 描述 预期工时
T2.1 ruoyi-framework 拆分 拆为 ruoyi-security + ruoyi-web + ruoyi-datasource 5 天
T2.2 登录编排器移入 admin SysLoginService/UserDetailsServiceImpl 等归入 ruoyi-admin 3 天
T2.3 Spring Events 替换 AsyncManager 删除 AsyncManager/AsyncFactory,引入事件机制 4 天
T2.4 拆分 SysUser 上帝对象 引入 DTO/BO 类,LoginUser 中实体替换为 UserProfile 5 天
T2.5 消除 SpringUtils DictUtils、MessageUtils 改为 Spring Bean 3 天
T2.6 BaseController 改为组合 抽取 PageService、CurrentUserProvider 3 天
T2.7 DataScopeAspect SQL 安全加固 参数化查询或 MyBatis <sql> 标签 2 天
T2.8 RuoYiConfig 去静态化 所有字段改为实例字段 2 天

9.3 第三阶段:安全与运维加固(P2,约 1-2 周)

任务 描述 预期工时
T3.1 配置外部化 JWT 密钥、Druid 密码等通过环境变量注入 1 天
T3.2 异常类重构 ServiceException 去可变性 1 天
T3.3 线程池参数外部化 核心参数可配置 1 天
T3.4 移除 TestController 删除生产环境中的测试代码 0.5 天
T3.5 引入代码质量工具 Checkstyle + SpotBugs 1 天

9.4 第四阶段:架构优化(长期,约 2-3 周)

任务 描述 预期工时
T4.1 引入 MapStruct 实体 ↔ DTO/BO 转换 2 天
T4.2 API 版本化 /api/v1/ URL 前缀 1 天
T4.3 接口命名规范化 移除 I 前缀,改为 UserService / UserServiceImpl 2 天
T4.4 Java 17 特性利用 record 替代简单 DTO、sealed class、pattern matching 3 天
T4.5 事件机制扩展 将更多场景(用户变更、部门变更等)纳入事件驱动 3 天

10. 最终目标架构

10.1 模块依赖拓扑

ruoyi-admin (组合根:启动入口 + Controller + 登录编排器)
  ├── ruoyi-system (业务实体 + Service + Mapper + 事件监听器)
  │     └── ruoyi-common (纯工具 + 事件 POJO + 注解)
  ├── ruoyi-web (AOP / Web配置 / 全局异常 / 异步配置)
  │     └── ruoyi-security (JWT + Spring Security + PermissionService)
  │           └── ruoyi-common
  ├── ruoyi-datasource (DynamicDataSource / DruidConfig / DataSourceAspect)
  │     └── ruoyi-common
  ├── ruoyi-quartz (Quartz 调度)
  │     └── ruoyi-common
  └── ruoyi-generator (代码生成)
        └── ruoyi-common

共 8 个模块(从原来 6 个新增 3 个、合并 1 个拆分):

# 模块 来源 职责
1 ruoyi-admin 保留 + 扩大 启动入口, 18 个 Controller, 登录/注册编排器(SysLoginService, SysRegisterService, UserDetailsServiceImpl, SysPermissionService 从 framework 迁入)
2 ruoyi-system 扩大 业务实体(从 common 迁入: SysUser, SysRole, SysDept, SysMenu, SysDictData, SysDictType), Service 接口/Impl, Mapper, 事件监听器(LoginEventListener, OperationLogEventListener 等)
3 ruoyi-web 新增(从 framework 拆分) LogAspect, DataScopeAspect, RateLimiterAspect, XssFilter, BaseController, GlobalExceptionHandler, CaptchaConfig, AsyncEventConfig
4 ruoyi-security 新增(从 framework 拆分) SecurityConfig, JwtAuthenticationTokenFilter, PermissionService (@ss), SecurityUtils, TokenService, LoginUser + UserProfile BO, SysPasswordService
5 ruoyi-datasource 新增(从 framework 拆分) DynamicDataSource, DataSourceAspect, DruidConfig, DruidProperties
6 ruoyi-common 瘦身 纯工具类(StringUtils, DateUtils 等), 异常体系, 注解(@Log, @Excel, @Anonymous 等), BaseEntity, AjaxResult → ApiResponse, 事件 POJO(LoginEvent, OperationLogEvent 等), RuoYiConfig
7 ruoyi-quartz 保留 Quartz 调度, SysJob, SysJobLog(业务隔离)
8 ruoyi-generator 保留 代码生成, GenTable, GenTableColumn

10.2 分层架构示意

┌─────────────────────────────────────────────────────────────────┐
│                    ruoyi-admin (启动入口 + Controller)            │
│  SysUserController  SysConfigController  SysDeptController      │
│  SysRoleController  SysPostController    SysNoticeController    │
│  SysLoginService  SysRegisterService  (登录编排器)               │
│  ... 18 Controllers                                               │
└───────┬────────────────────┬──────────────────┬─────────────────┘
        │                    │                  │
┌───────▼──────────────┐ ┌──▼────────────────┐ │
│  ruoyi-system        │ │  ruoyi-web        │ │
│  (业务实体+服务)      │ │  (AOP/Web配置)    │ │
│                      │ │                   │ │
│ SysUser  SysRole     │ │ LogAspect         │ │
│ SysDept  SysMenu     │ │ DataScopeAspect   │ │
│ SysDictData          │ │ XssFilter         │ │
│ SysConfig            │ │ BaseController    │ │
│                      │ │ CaptchaConfig     │ │
│ ISysXxxService       │ │ AsyncEventConfig  │ │
│ SysXxxServiceImpl    │ └─────┬─────────────┘ │
│ SysXxxMapper         │       │               │
│                      │       │               │
│ LoginEventListener   │ ┌─────▼─────────────┐ │
│ OplogEventListener   │ │  ruoyi-security   │ │
│ ConfigEventListener  │ │  (安全认证/鉴权)   │ │
│                      │ │                   │ │
└───────┬──────────────┘ │ PermissionService │ │
        │                │ SecurityConfig     │ │
        │                │ JwtAuthFilter      │ │
        │                │ SecurityUtils      │ │
        │                │ TokenService       │ │
        │                │ LoginUser + BO     │ │
        │                └─────┬─────────────┘ │
        │                      │               │
        │        ┌─────────────▼──────────────┐ │
        │        │  ruoyi-common             │ │
        │        │  (纯工具库/事件POJO/注解)    │ │
        │        │                           │ │
        │        │ StringUtils  DateUtils    │ │
        │        │ ApiResponse  BaseEntity   │ │
        │        │ @Log  @Excel  @Anonymous  │ │
        │        │ RuoYiConfig               │ │
        │        │ LoginEvent  OperLogEvent  │ │
        │        │ ConfigChangeEvent         │ │
        │        └───────────────────────────┘ │
        │                                      │
        │  ┌──────────────────────────────────┐ │
        ├──│  ruoyi-datasource (独立)          │ │
        │  └──────────────────────────────────┘ │
        │  ruoyi-quartz (独立)                  │
        │  ruoyi-generator (独立)               │
        └──────────────────────────────────────┘

10.3 架构原则总结

  1. 依赖规则ruoyi-securityruoyi-webruoyi-datasourceruoyi-quartzruoyi-generator 都不能依赖 ruoyi-system。只有 ruoyi-admin 可以同时依赖 ruoyi-system 和框架模块。

  2. 运行时零调用ruoyi-security 在运行时(每次请求鉴权)不需要调用 ruoyi-system——所有鉴权数据在登录时一次性加载到 Redis LoginUser 中。

  3. 组合根模式ruoyi-admin 作为组合根,负责编排 system 层和 security/web 层的交互。登录时调用 system 查 DB,组装 LoginUser 存入 Redis。

  4. 事件驱动解耦:框架模块(web/security)与业务模块(system)之间的通信通过 Spring Events 完成。框架发布事件 POJO(定义在 common),业务模块监听并持久化。

  5. DTO/BO 隔离:框架模块不直接引用数据库实体(SysUserSysRole 等),而是使用轻量的 BO(如 UserProfileRoleInfo),仅在登录时从实体转换一次。


11. 附录:CodeGraph 统计数据

11.1 高影响符号(按调用者数量排序)

符号 位置 调用者数 风险等级
SysUser ruoyi-common/.../entity/SysUser.java 106 🔴
ServiceException ruoyi-common/.../exception/ServiceException.java 40 🟠
SecurityUtils ruoyi-common/.../utils/SecurityUtils.java 18 🔴
BaseController ruoyi-common/.../controller/BaseController.java 18 🟠
RedisCache ruoyi-common/.../redis/RedisCache.java 17 🟠
RuoYiConfig ruoyi-common/.../config/RuoYiConfig.java 16 🟠
SpringUtils ruoyi-common/.../spring/SpringUtils.java 13 🟠
SpringUtils.getBean() 同上 12 🟠
TokenService ruoyi-framework/.../service/TokenService.java 11 🟠

11.2 模块依赖矩阵(基于 CodeGraph 调用链)

调用者 \ 被调用者 admin framework system common
ruoyi-admin
ruoyi-framework
ruoyi-system
ruoyi-common ⚠️ * ⚠️ *
ruoyi-quartz
ruoyi-generator

⚠️ * ruoyi-common 中的 SecurityUtilsRedisCache 实际依赖了 framework 层才有的 Bean 配置(Spring Security、RedisTemplate),形成隐式反向依赖。

11.3 ruoyi-framework → ruoyi-system 调用全景图

Spring Security 认证入口
  └─→ UserDetailsServiceImpl.loadUserByUsername()
        ├─→ ISysUserService.selectUserByUserName()        [ruoyi-system]
        ├─→ SysPasswordService.validate()                 [framework 内部]
        └─→ SysPermissionService.getMenuPermission()
              ├─→ ISysMenuService.selectMenuPermsByRoleId()   [ruoyi-system]
              ├─→ ISysMenuService.selectMenuPermsByUserId()   [ruoyi-system]
              └─→ ISysRoleService.selectRolePermissionByUserId() [ruoyi-system]

登录流程
  └─→ SysLoginService.login()
        ├─→ ISysConfigService.selectCaptchaEnabled()      [ruoyi-system]
        ├─→ ISysConfigService.selectConfigByKey()         [ruoyi-system]
        ├─→ ISysUserService.updateLoginInfo()             [ruoyi-system]
        └─→ AsyncFactory.recordLogininfor()
              └─→ ISysLogininforService.insertLogininfor() [ruoyi-system]

操作日志(AOP)
  └─→ LogAspect → AsyncFactory.recordOper()
        └─→ ISysOperLogService.insertOperlog()            [ruoyi-system]

11.4 问题严重程度定义

级别 定义 示例
🔴 P0 严重 影响系统正确性、安全性,或严重阻碍后续开发 零测试覆盖、类型不安全、依赖方向错误
🟠 P1 重要 导致代码难以维护、存在潜在风险 上帝对象、SQL 注入、服务定位器
🟡 P2 一般 不符合最佳实践,但暂不影响运行 命名规范、配置硬编码

12. DTO/VO/BO 分层对象设计与 Spring 风格命名规范

本节综合了 architecture-review.md(第6-10节)、ruoyi-vue架构分析报告.md(第2节)和 ruoyi-vue-architecture-analysis.md(问题10)的合理建议。

12.1 当前问题:Entity 从数据库直通前端

当前所有层次共用同一个 SysUser 实体,从数据库一直传到前端:

Controller ←→ SysUser(62字段,含password)←→ Service ←→ SysUser ←→ Mapper ←→ DB

这导致:

  • 敏感字段(如密码)可能泄露给前端
  • 列表查询加载了不需要的关联对象(roles, dept)
  • 字段变更影响所有层次
  • API 文档无法准确描述返回结构

12.2 目标:五层对象分离

参考 Spring 官方实践(UserDetailsPageAuthentication 等),使用描述性英语名词而非 DTO/VO/BO 后缀:

Controller 层        Service 层           Mapper 层         Database
┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────┐
│ Request/Query│ →  │ Transfer/DTO │ →  │   Entity     │ →  │  DB  │
│ (入参验证)    │    │ (跨层传输)    │    │ (数据库映射)  │    │      │
└──────────────┘    └──────────────┘    └──────────────┘    └──────┘
       ↑                                       │
       │ Response/View                         │ 转换发生在层边界
       │ (前端展示)                             │ MapStruct / 手动映射
       └───────────────────────────────────────┘

各层对象命名规范(参考 Spring 官方惯例)

层次 包路径 命名规范 示例 说明
Controller 接收 system/request/ {Entity}{Action}Request UserCreateRequestUserUpdateRequest 创建/更新请求体
Controller 接收 system/query/ {Entity}Query UserQuery 查询条件(分页+筛选)
Controller 返回 system/response/ {Entity}Details UserDetails 详情页(含关联信息)
Controller 返回 system/response/ {Entity}Summary UserSummary 列表摘要(精简字段)
Service 内部 system/aggregate/ {Entity}With{Roles} UserWithRoles 聚合对象(Service 内部使用)
Mapper 层 system/entity/ {Entity} User 数据库实体(直接映射表)

命名对照表

当前命名(不规范) Spring 风格命名(推荐) 说明
SysUser User 去掉无意义前缀 Sys
SysUserDTO UserProfile 或 UserCreateRequest 描述性语义,不用 DTO 后缀
SysUserVO UserSummary(列表)/ UserDetails(详情) 描述展示场景
SysRole Role 去掉前缀
SysDept Department 用完整单词,不用缩写
SysMenu Menu 去掉前缀
SysDictData / SysDictType DictionaryEntry / DictionaryType 语义清晰
IUserService UserService 去掉 I 前缀
selectUserById findUserById Spring Data 风格

参考来源:Spring Security 的 UserDetailsGrantedAuthority;Spring Data 的 Page<T>Pageable;Spring HATEOAS 的 EntityModel<T>;Spring PetClinic 示例的 PetRequestPetSummary

12.3 完整类设计示例(以 User 为例)

// ============ Entity(数据访问层,system/entity)============
// 直接映射 sys_user 表,纯数据库字段
@TableName("sys_user")
public class User extends BaseEntity {
    private Long userId;
    private Long deptId;
    private String userName;
    private String nickName;
    private String password;       // 保留,但不在 Response 中暴露
    private String email;
    private String phonenumber;
    private String sex;
    private String avatar;
    private String status;
    private String delFlag;
    private String loginIp;
    private Date loginDate;
    private Date pwdUpdateDate;
    // 注意:不含关联对象(roles, dept),关联对象在 aggregate 中组装
}

// ============ Request(Controller 接收参数,system/request)============
public class UserCreateRequest {
    @NotBlank(message = "用户账号不能为空")
    @Size(min = 0, max = 30)
    private String userName;

    @NotBlank(message = "用户昵称不能为空")
    @Size(min = 0, max = 30)
    private String nickName;

    @NotBlank(message = "用户密码不能为空")
    private String password;

    @Email
    @Size(min = 0, max = 50)
    private String email;

    @Size(min = 0, max = 11)
    private String phonenumber;

    private String sex;
    private Long deptId;
    private Long[] roleIds;
    private Long[] postIds;
    private String status;
}

public class UserUpdateRequest {
    @NotNull(message = "用户ID不能为空")
    private Long userId;

    @Size(min = 0, max = 30)
    private String nickName;

    @Email @Size(min = 0, max = 50)
    private String email;

    @Size(min = 0, max = 11)
    private String phonenumber;

    private String sex;
    private Long deptId;
    private Long[] roleIds;
    private Long[] postIds;
    private String status;
}

// ============ Query(查询条件,system/query)============
public class UserQuery {
    private String userName;
    private String phonenumber;
    private String status;
    private Long deptId;
    private String beginTime;
    private String endTime;
    // 分页参数由框架统一处理(PageHelper 或 PageRequest)
}

// ============ Response(Controller 返回,system/response)============
public class UserDetails {           // 详情页用
    private Long userId;
    private String userName;
    private String nickName;
    private String email;
    private String phonenumber;
    private String sex;
    private String avatar;
    private String status;
    private String deptName;         // 关联展示字段(非 DB 字段)
    private List<String> roleNames;  // 关联展示字段
    private List<String> postNames;  // 关联展示字段
    private String createTime;
    // 不含:password, delFlag, loginIp, roleIds, postIds
}

public class UserSummary {           // 列表页用,字段精简
    private Long userId;
    private String userName;
    private String nickName;
    private String deptName;
    private String status;
    private String createTime;
}

// ============ Aggregate(Service 内部组装,system/aggregate)============
public class UserWithRoles {
    private User user;
    private List<Role> roles;
    private Department dept;
    private Set<String> permissions;
    // 仅在 Service 内部使用,不暴露给 Controller
}

12.4 各层转换示例(使用 MapStruct)

// ============ Convert 层(MapStruct)============
@Mapper(componentModel = "spring")
public interface UserConvert {

    UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);

    // Query → Entity(查询条件转实体)
    User fromQuery(UserQuery query);

    // Request → Entity(创建请求转实体)
    User fromCreateRequest(UserCreateRequest request);
    User fromUpdateRequest(UserUpdateRequest request);

    // Entity → Response
    UserSummary toSummary(User user);
    List<UserSummary> toSummaryList(List<User> users);

    // 自定义映射:组装关联信息
    default UserDetails toDetails(User user, List<Role> roles, Department dept) {
        UserDetails details = new UserDetails();
        details.setUserId(user.getUserId());
        details.setUserName(user.getUserName());
        details.setNickName(user.getNickName());
        details.setEmail(user.getEmail());
        details.setPhonenumber(user.getPhonenumber());
        details.setSex(user.getSex());
        details.setAvatar(user.getAvatar());
        details.setStatus(user.getStatus());
        details.setDeptName(dept != null ? dept.getDeptName() : null);
        details.setRoleNames(roles.stream().map(Role::getRoleName).toList());
        details.setCreateTime(DateUtils.parseDateToStr(user.getCreateTime()));
        return details;
    }
}

// ============ Controller 使用示例 ============
@GetMapping("/list")
public Page<UserSummary> list(UserQuery query) {
    return userService.queryUsers(query);
}

@GetMapping("/{userId}")
public UserDetails detail(@PathVariable Long userId) {
    return userService.getUserDetails(userId);
}

@PostMapping
public ApiResponse<Void> create(@Validated @RequestBody UserCreateRequest request) {
    userService.createUser(request);
    return ApiResponse.success();
}

// ============ Service 使用示例 ============
public Page<UserSummary> queryUsers(UserQuery query) {
    User condition = UserConvert.INSTANCE.fromQuery(query);
    startPage();
    List<User> users = userMapper.selectUserList(condition);
    List<UserSummary> summaries = UserConvert.INSTANCE.toSummaryList(users);
    return new Page<>(summaries, total);
}

public UserDetails getUserDetails(Long userId) {
    User user = userMapper.selectUserById(userId);
    List<Role> roles = roleMapper.selectRolesByUserId(userId);
    Department dept = deptMapper.selectDeptById(user.getDeptId());
    return UserConvert.INSTANCE.toDetails(user, roles, dept);
}

public void createUser(UserCreateRequest request) {
    User user = UserConvert.INSTANCE.fromCreateRequest(request);
    user.setPassword(SecurityUtils.encryptPassword(request.getPassword()));
    userMapper.insertUser(user);
    insertUserRoles(user.getUserId(), request.getRoleIds());
}

12.5 包命名规范

不使用 dtovopobo 这种中式包名,而是使用英文语义包名:

不推荐 推荐 对应 Spring 惯例
dto request + query(入参)/ transfer(跨层) Spring MVC @RequestBody
vo response Spring HATEOAS RepresentationModel
po entity JPA @Entity
bo aggregate DDD Aggregate
mapper repository(可选) Spring Data @Repository

13. ruoyi-core 模块方案(进阶可选)

本节综合了 architecture-review.md(第9、12节)关于 ruoyi-core 模块和泛型接口的建议。这是一个进阶可选方案——如果团队希望 framework 和 system 之间的解耦达到 Spring Security 级别的抽象程度,可以采用。否则第 5~7 节的 8 模块方案已经足够。

13.1 设计动机

当前方案(第 7 节的 8 模块架构)通过事件机制和归属变更解决了 framework → system 的直接依赖,但 LoginUser 仍然持有 SysUser(或 UserProfile BO),而且 ruoyi-security 在编译期仍然知道 UserProfile 这个具体类型。

如果需要更彻底的解耦(例如 ruoyi-security 完全不认识任何 system 层的类型),可以引入 ruoyi-core 模块。

13.2 参考 Spring 的做法

Spring 项目 独立模块 内容
spring-security-core spring-security-core UserDetailsGrantedAuthorityAuthentication
spring-data-commons spring-data-commons PagePageableSortRepository
spring-tx spring-tx TransactionDefinitionPlatformTransactionManager

Spring 把跨模块共享的 POJO 和接口放在独立的 *-core 或 *-commons 模块中。

13.3 ruoyi-core 模块设计

ruoyi-vue/
├── ruoyi-common          纯工具类、常量、注解、异常(无任何业务概念)
├── ruoyi-core            ← 新增:通用业务 POJO + 接口定义(跨模块共享)
│   ├── user/
│   │   ├── UserIdentity.java       // 泛型用户标识接口
│   │   └── UserDetailsService.java // 用户加载接口
│   ├── role/
│   │   └── RoleIdentity.java       // 泛型角色标识接口
│   ├── dept/
│   │   └── Department.java         // 泛型部门接口
│   ├── menu/
│   │   └── MenuNode.java           // 泛型菜单节点接口
│   ├── auth/
│   │   ├── Authentication.java     // 认证信息接口
│   │   └── GrantedAuthority.java   // 授权权限接口
│   └── pagination/
│       ├── Page.java               // 分页结果(替代 PageHelper ThreadLocal)
│       ├── Pageable.java           // 分页请求
│       └── Sort.java               // 排序条件
├── ruoyi-framework       通用基础设施:Security、Redis、AOP(用 core 的接口)
├── ruoyi-system          业务模块:实体实现 core 接口,Service 实现 core 的 UserDetailsService
├── ruoyi-quartz / ruoyi-generator
└── ruoyi-admin           启动入口

依赖方向

admin → system → framework → ruoyi-core → common
                       ↑           ↑
                       └───────────┘
                    framework 和 system 都依赖 ruoyi-core

13.4 关键接口设计

// ============ ruoyi-core 中的泛型接口 ============

// 参考 Spring Security 的 UserDetails
public interface UserIdentity<ID> {
    ID getUserId();
    String getUsername();
    String getPassword();
    boolean isEnabled();
}

// 参考 Spring Security 的 GrantedAuthority
public interface RoleIdentity<ID> {
    ID getRoleId();
    String getRoleKey();
    String getDataScope();
}

// 表达"部门"概念,不绑定数据库
public interface Department<ID> {
    ID getDeptId();
    String getDeptName();
    ID getParentId();
}

// 表达"菜单"概念
public interface MenuNode<ID> {
    ID getMenuId();
    String getMenuName();
    String getPerms();
    ID getParentId();
}

// 框架定义接口,system 实现
public interface UserDetailsService<ID> {
    UserIdentity<ID> loadUserByUsername(String username);
}

// 分页结果(替代 PageHelper ThreadLocal 模式)
public class Page<T> {
    private List<T> content;
    private long totalElements;
    private int totalPages;
    private int pageNumber;
    private int pageSize;
}

13.5 system 层实现 core 接口

// ============ ruoyi-system 中实现 core 接口 ============

// 实体实现泛型接口
public class User extends BaseEntity implements UserIdentity<Long> {
    private Long userId;
    private String userName;
    private String password;
    private String status;

    @Override public Long getUserId() { return this.userId; }
    @Override public String getUsername() { return this.userName; }
    @Override public String getPassword() { return this.password; }
    @Override public boolean isEnabled() { return "0".equals(this.status); }
}

public class Role extends BaseEntity implements RoleIdentity<Long> {
    private Long roleId;
    private String roleKey;
    private String dataScope;

    @Override public Long getRoleId() { return this.roleId; }
    @Override public String getRoleKey() { return this.roleKey; }
    @Override public String getDataScope() { return this.dataScope; }
}

// 实现 core 的 UserDetailsService 接口(依赖倒置)
@Service
public class UserDetailsServiceImpl implements UserDetailsService<Long> {
    @Autowired private UserMapper userMapper;

    @Override
    public UserIdentity<Long> loadUserByUsername(String username) {
        User user = userMapper.selectUserByUserName(username);
        // ... 校验逻辑 ...
        Set<String> perms = loadPermissions(user.getUserId());
        return new LoginUser(user, perms);  // LoginUser 也实现 UserIdentity<Long>
    }
}

13.6 framework 层使用 core 接口

// ============ ruoyi-security 中只使用 core 接口 ============

// TokenService 只使用 core 接口,不依赖 system 实体
@Service
public class TokenService {
    public String createToken(UserIdentity<Long> user) {
        return Jwts.builder()
            .setSubject(user.getUserId().toString())
            .claim("username", user.getUsername())
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }
}

// JwtAuthenticationFilter 只使用 core 接口
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    // parseToken 返回 UserIdentity<Long>(core 接口),不是 SysUser
}

13.7 ruoyi-core 方案与 8 模块方案的关系

8 模块方案(第 7 节) + ruoyi-core 方案
依赖方向 admin → {system, web, security} → common admin → system → framework → core → common
解耦程度 高(运行时零调用 + 事件解耦) 最高(编译期 + 运行期完全解耦)
新模块数 3 个新增(web/security/datasource) 4 个新增(+ ruoyi-core)
复杂度 中高
适用场景 中小团队,注重实效 需要框架级复用,或有多套 system 实现
关系 独立可用 在 8 模块基础上叠加

建议:8 模块方案已经解决了最核心的依赖方向问题,对于 RuoYi 项目规模已足够。ruoyi-core 方案作为进阶选项,在有明确的"框架与业务需彻底解耦"需求时再考虑。


14. 补充问题清单(来自其他参考文件)

以下问题是补充。

14.1 校验注解位置不当

位置SysUser.java 的 getter 方法上

// 当前:校验注解在 getter 上
@Xss(message = "用户昵称不能包含脚本字符")
@Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符")
public String getNickName() { return nickName; }

问题:Spring Boot 默认校验字段注解,放在 getter 上需要额外配置,不符合 Java Bean 惯例,容易遗漏。

建议:将校验注解从 getter 移到字段上:

@Xss(message = "用户昵称不能包含脚本字符")
@Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符")
private String nickName;

14.2 Service 层缺少事务管理

CodeGraph 证据:多个 Service 方法同时操作多张表但无 @Transactional 注解。例如 SysUserServiceImpl.insertUser 同时操作 useruser_roleuser_post 表。

建议:关键写操作方法添加 @Transactional(rollbackFor = Exception.class),确保数据一致性。

14.3 缺少 Repository 层

现状:所有 Service 实现类都直接注入 Mapper(@Autowired private SysUserMapper userMapper)。

问题

  • 数据访问与业务逻辑耦合
  • 难以在不修改 Service 的情况下切换数据源或缓存策略
  • 难以进行单元测试(需要启动 MyBatis 环境)

建议:引入 Repository 层封装数据访问,Service 只依赖 Repository 接口。Repository 负责 Entity ↔ 数据库的映射。

14.4 AOP 自调用反模式

位置ruoyi-system/.../SysDeptServiceImpl.java:60

// 通过 getAopProxy(this) 获取自身 AOP 代理
List<SysDept> depts = SpringUtils.getAopProxy(this).selectDeptList(dept);

问题SpringUtils.getAopProxy(this) 暴露了内部实现细节,SysRoleServiceImpl 也有类似代码。这表明类内部方法需要 AOP 增强(如 @DataScope)但通过自调用无法触发。

建议:将需要 AOP 增强的方法抽取到另一个独立的 Service Bean 中通过注入调用。

14.5 Controller 业务逻辑重复 — SysJobController

位置SysJobController.add() 和 edit() 方法

两个方法存在完全相同的 6 段验证逻辑(Cron 表达式校验、RMI/LDAP/HTTP 调用目标校验、错误字符串校验、白名单校验)。修改验证规则时容易遗漏一个方法。

建议:将验证逻辑抽取到 Service 层的 validateJob(SysJob) 方法中。

14.6 框架服务未走接口抽象

现状ruoyi-framework 中的服务(TokenServiceSysLoginServicePermissionServiceSysPasswordService)全部是直接类,没有接口定义。

对比ruoyi-system 中有标准的 ISysXxxService 接口 + SysXxxServiceImpl 实现。

建议:为 framework 的关键服务提取接口,或至少提供 @ConditionalOnProperty 支持替换实现。

14.7 校验注解使用 getter — 见 14.1

(已合并到 14.1)

14.8 RedisCache 泛型丢失

位置ruoyi-common/.../RedisCache.java:26

@Autowired
public RedisTemplate redisTemplate;  // raw type,丢失泛型

RedisTemplate<Object, Object> 的正确泛型参数丢失,导致所有调用处出现 unchecked 警告。

建议:改为 RedisTemplate<String, Object>

14.9 RedisConfig 废弃 API 压制

位置ruoyi-framework/.../RedisConfig.java:17

@SuppressWarnings("deprecation")
public class RedisConfig extends CachingConfigurerSupport

类级别 @SuppressWarnings("deprecation") 隐藏了潜在的废弃 API 问题。CachingConfigurerSupport 在 Spring 6.x 中已废弃。

建议:移除 @SuppressWarnings("deprecation"),使用新的 CachingConfigurer 接口替代。

14.10 Constants 耦合各模块配置

位置ruoyi-common/.../constant/Constants.java

Constants 包含了本应属于各业务模块的常量,如 JOB_ERROR_STRJOB_WHITELIST_STR(Quartz 相关)、LOOKUP_RMI/LDAP/HTTP 等。

建议:将业务常量迁移到对应模块的常量类或用 application.yml 配置管理。Constants 只保留跨模块通用常量。

14.11 Generator / Quartz 特有问题

问题 位置 描述 建议
GenConfig 静态可变字段 ruoyi-generator/.../GenConfig.java 字段声明为 public static 通过 @Value 写入 改为实例字段 + @ConfigurationProperties
${sql} 原生 SQL 替换 ruoyi-generator/.../GenTableMapper.xml createTable 用 MyBatis ${sql} 拼接 加强输入校验或用 PreparedStatement
域模型序列化副作用 ruoyi-quartz/.../SysJob.java:113 getNextValidTime() 被 @JsonFormat,JSON 序列化触发 Cron 计算 声明为独立字段,由 Service 计算填充
scheduler.clear() 破坏性启动 ruoyi-quartz/.../SysJobServiceImpl.java 每次启动清除所有 Quartz Job 只在初次运行或通过配置控制
Job 日志同步写 ruoyi-quartz/.../AbstractQuartzJob.java 任务执行后同步写 sys_job_log 使用 @Async 或事件机制异步写入
表扫描仅支持 MySQL ruoyi-generator/.../GenTableMapper.xml information_schema.tables 是 MySQL 专有 引入数据库方言抽象层

14.12 前端 sessionStorage 防重复提交

位置ruoyi-admin-ui/.../utils/request.ts

前端通过 sessionStorage 记录 POST/PUT 请求标识,1 秒内重复请求被拦截。

问题:分布式部署下不可靠,sessionStorage 无法跨实例共享。

建议:后端 @RepeatSubmit 注解已存在(基于 Redis),应推广使用而非依赖前端方案。

14.13 代码风格与质量

问题 描述 建议
@Autowired / @Resource 混用 项目中同时使用两种注入方式 统一使用构造器注入(Spring 官方推荐)
Java 17 特性未使用 未使用 record、sealed class、switch 表达式、Text Block 逐步引入简化代码
缺少代码质量工具 无 Checkstyle/PMD/SpotBugs/.editorconfig 集成 SonarQube + 配置 Checkstyle + SpotBugs
@Autowired 字段注入 普遍使用字段注入而非构造器注入 改为构造器注入,便于测试和不可变性

15. 事件驱动解耦 — 扩展场景

补充更多可用 Spring Events 解耦的场景。

15.1 Spring Events vs AsyncManager 对比

维度 AsyncManager + TimerTask Spring ApplicationEvent + @Async
依赖注入 SpringUtils.getBean() 服务定位器 构造器注入(标准 DI)
可测试性 差(静态方法无法 mock) 好(普通 Spring Bean)
扩展性 加新日志 = 改 AsyncFactory 加方法 加新监听器,不改发布者
事务控制 独立线程,不可控 @TransactionalEventListener(phase=AFTER_COMMIT)
异步机制 ScheduledThreadPoolExecutor 固定 10ms @Async 线程池,灵活可配
模块耦合 AsyncFactory 直接 import system 实体 事件 POJO 在 common,监听器在 system,符合目标依赖方向
粒度控制 粗粒度(所有日志共用 TimerTask) 细粒度(每类事件独立监听器)

15.2 完整事件解耦场景

事件名 所属模块 发布者 发布时机 监听者(模块) 行为
LoginEvent common SysLoginService (admin) 登录成功/失败 LoginEventListener (system) 异步写登录日志
LogoutEvent common LogoutSuccessHandler (security) 退出登录 LoginEventListener (system) 异步写登出日志
RegisterEvent common SysRegisterService (admin) 注册成功 LoginEventListener (system) 异步写注册日志
OperationLogEvent common LogAspect (web) @Log 方法执行后 OperationLogEventListener (system) 异步写操作日志
ConfigChangeEvent common SysConfigController (admin) 配置修改 ConfigChangeEventListener (system) 同步刷新 Redis 缓存
PermissionChangeEvent common SysRoleController / SysMenuController (admin) 角色/菜单权限变更 PermissionRefreshListener (system) 刷新所有在线用户权限缓存
UserStatusChangeEvent common SysUserController (admin) 用户禁用/启用 UserCacheRefreshListener (system) 踢出/恢复 Redis 中的 LoginUser
DictChangeEvent common SysDictDataController / SysDictTypeController (admin) 字典数据变更 DictCacheRefreshListener (system) 清除字典缓存
DeptChangeEvent common SysDeptController (admin) 部门变更 相关监听器 (system) 清除数据权限相关缓存

15.3 可扩展的监听器模式

// TransactionalEventListener 确保事务提交后才记录日志
// 避免事务回滚后日志仍被写入的问题
@Component
public class OperationLogEventListener {

    @Autowired
    private ISysOperLogService operLogService;

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOperationLog(OperationLogEvent event) {
        // ... 同第 8 节代码
    }
}

16. 架构改进优先级(最终汇总版)

综合各方面的问题,给出最终优先级排序。

优先级 编号 问题 来源文件 建议排期
🔴 P0 1 统一响应封装(AjaxResult + R<T> → ApiResponse<T>) 所有文件 2 周
🔴 P0 2 补充核心模块测试 所有文件 持续
🔴 P0 3 修正依赖方向(业务实体从 common 移至 system) architecture-review, ruoyi-vue架构分析报告 2 周
🔴 P0 4 ruoyi-common Maven 依赖瘦身(移除 security/redis/jjwt 等) 所有文件 2 周
🔴 P0 5 ruoyi-framework 拆分为 security + web + datasource 所有文件 2 周
🔴 P0 6 登录编排器移入 admin(SysLoginService 等从 framework 迁出) architecture-review, ruoyi-vue-architecture-analysis 1 周
🟠 P1 7 Spring Events 替换 AsyncManager/AsyncFactory ruoyi-vue-architecture-analysis, 本文件 1 周
🟠 P1 8 引入 Request/Response/Query 分层 + MapStruct architecture-review, ruoyi-vue架构分析报告 3 周
🟠 P1 9 拆分 SysUser 上帝对象(UserProfile/UserSummary 等) 所有文件 1 周
🟠 P1 10 消除 SpringUtils 服务定位器 本文件, ruoyi-vue架构分析与改造计划 1 周
🟠 P1 11 BaseController 改组合(PageService + CurrentUserProvider) architecture-review 1 周
🟠 P1 12 DataScopeAspect SQL 安全加固 所有文件 2 天
🟠 P1 13 Service 添加 @Transactional ruoyi-vue架构分析报告 2 天
🟠 P1 14 校验注解从 getter 移至字段 architecture-review 1 天
🟢 P2 15 RuoYiConfig 去静态化 所有文件 1 天
🟢 P2 16 配置外部化(JWT、Druid、线程池) 所有文件 1 天
🟢 P2 17 ServiceException 不可变化 architecture-review 0.5 天
🟢 P2 18 移除 TestController 所有文件 0.5 天
🟢 P2 19 框架服务提取接口 architecture-review 1 天
🟢 P2 20 消除 AOP 自调用反模式 ruoyi-vue-architecture-analysis 2 天
🟢 P2 21 Controller 重复逻辑下沉 Service ruoyi-vue-architecture-analysis 1 天
🟢 P2 22 Constants 模块常量清理 ruoyi-vue架构分析与改造计划 1 天
🟢 P2 23 Generator/Quartz P1/P2 问题修复 ruoyi-vue架构分析与改造计划 3 天
🟢 P2 24 Java 17 新特性引入 ruoyi-vue架构分析报告, ruoyi-vue架构分析与改造计划 持续
🟢 P2 25 命名规范化(去 Sys/I 前缀,语义化命名) architecture-review, ruoyi-vue架构分析报告 持续
🟢 P2 26 引入代码质量工具 所有文件 1 天
🟢 P2 27 API 版本化 ruoyi-vue架构分析报告 1 天
🟢 P2 28 RedisCache 泛型修复 + 废弃 API 清理 ruoyi-vue-architecture-analysis 0.5 天
🟢 P2 29 前端防重改用后端 @RepeatSubmit ruoyi-vue架构分析与改造计划 1 天
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐