使用 OpenCode + DeepSeek flash 对对ruoyi-vue项目架构的分析
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-system,ruoyi-system 依赖 ruoyi-common。
2.2 实际代码中的反向依赖(CodeGraph 发现)
CodeGraph 的调用链分析揭示了 多个违反 POM 声明的反向依赖:
问题 1:ruoyi-common → Spring Security(反向依赖)
SecurityUtils(位于 ruoyi-common)直接引用:
org.springframework.security.core.Authenticationorg.springframework.security.core.context.SecurityContextHolderorg.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
SysUser、SysRole、SysMenu、SysDept 等核心域实体全部位于 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 |
ISysConfigService, ISysUserService |
web/service/SysPermissionService.java |
ISysMenuService, ISysRoleService |
web/service/SysLoginService.java |
ISysConfigService, ISysUserService |
manager/factory/AsyncFactory.java |
SysLogininfor, SysOperLog, ISysLogininforService, ISysOperLogService |
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.javaR<T>—ruoyi-common/.../core/domain/R.java
问题描述:
项目中同时存在两套响应封装。AjaxResult 被所有生产 Controller 使用,而 R<T> 仅在 TestController 中使用。两个类的 API 风格完全不同:
AjaxResult.success(data)vsR.ok(data)AjaxResult.error(msg)vsR.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 承担了太多职责:
- 数据库实体 —
userId,userName,password,email,phonenumber,sex,avatar等 - 表单验证 — getter 上标注
@Xss,@Size,@NotBlank,@Email - JSON 序列化控制 —
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) - 业务逻辑 —
isAdmin()调用SecurityUtils.isAdmin(this.userId) - 关联查询载体 —
SysDept dept,List<SysRole> roles,Long[] roleIds/postIds - Excel 导出注解 — 字段上有
@Excel注解 - 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.class, ISysOperLogService.class |
ShutdownManager |
ruoyi-framework | 线程池 Bean |
CronUtils / JobInvokeUtil |
ruoyi-quartz | 调度相关 Bean |
GenUtils |
ruoyi-generator | 生成器配置 Bean |
建议改造:将 DictUtils、MessageUtils 改为 Spring Bean,通过 @Autowired 注入依赖。AsyncManager/AsyncFactory 待用 Spring Events 替换后删除。
4.7 P1-7:日志系统紧耦合 — AsyncManager + AsyncFactory
当前日志机制的三个核心问题:
- AsyncFactory 是静态工厂 + SpringUtils.getBean() 服务定位器,彻底绕过 DI
- 所有日志类型硬编码在一个类中,加新日志类型必须改 AsyncFactory
- 使用过时的
java.util.TimerTask,而非Runnable/Callable - 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,于是将 SysUser, SysRole, SysDept, SysMenu, SysDictData, SysDictType 等业务实体全部塞入 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 核心做法
-
SysLoginService、UserDetailsServiceImpl、SysPermissionService→ 归入ruoyi-admin(或ruoyi-web的登录子包),因为它们是登录流程的编排器,需要同时调用 system(DB)和 security(Redis/JWT)。它们不属于 framework 层。 -
PermissionService(@ss) 留在ruoyi-security,它只读 RedisLoginUser,不调用任何 system 的服务。 -
LoginUser中的SysUser user字段 → 逐步替换为UserProfileBO,放在ruoyi-security或ruoyi-common中,只包含鉴权需要的字段(userId,userName,deptId,roles,permissions),不含密码、手机号、邮箱等敏感/无关字段。
// 当前: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 等
}
- 角色/菜单权限变更后刷新在线用户:不在
SysRoleController/SysMenuController中直接调用TokenService,而是发布PermissionChangeEvent,由 system 层的监听器处理在线用户权限刷新。
5.5 与 DIP 方案(方案 A)的对比
| 方案 A(纯 DIP 接口) | 方案 C(Redis 缓存 + 登录桥接) | |
|---|---|---|
| 运行时 security → system | 通过 DTO 接口间接调用 | 零调用(全 Redis) |
| 需要的新接口数量 | 4+(IUserService, IRoleService, IConfigurationService, IMenuService)+ 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 层关注点 |
SysUser, SysRole, SysDept, SysMenu |
common/core/domain/entity/ |
ruoyi-system |
业务实体 |
SysDictData, SysDictType |
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, SysLoginService, SysRegisterService, UserDetailsServiceImpl, SysPermissionService(权限加载部分) |
ruoyi-system |
扩大 | 业务实体 + Service + Mapper + 事件监听器 | SysUser, SysRole, SysDept, SysMenu, SysDictData, SysDictType 等实体(从 common 迁入); ISysXxxService / SysXxxServiceImpl / SysXxxMapper; LoginEventListener, OperationLogEventListener, ConfigChangeEventListener |
ruoyi-web |
新(从 framework 拆分) | AOP + Web 配置 | LogAspect, DataScopeAspect, RateLimiterAspect, RepeatSubmitInterceptor, XssFilter, BaseController, GlobalExceptionHandler, CaptchaConfig, AsyncEventConfig(@EnableAsync) |
ruoyi-security |
新(从 framework 拆分) | 安全认证与鉴权 | SecurityConfig, JwtAuthenticationTokenFilter, PermissionService(@ss), SecurityUtils, TokenService, LoginUser + UserProfile BO, JWT 工具, SysPasswordService |
ruoyi-datasource |
新(从 framework 拆分) | 多数据源配置 | DynamicDataSource, DataSourceAspect, DruidConfig, DruidProperties |
ruoyi-common |
瘦身 | 纯工具库 + 事件 POJO | 见第 6.2 节 |
ruoyi-quartz |
保留 | Quartz 调度 | SysJob, SysJobLog, ScheduleConfig |
ruoyi-generator |
保留 | 代码生成 | GenTable, GenTableColumn, 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 → ISysConfigService, ISysUserService |
归入 ruoyi-admin,合法依赖 system |
SysPermissionService → ISysMenuService, ISysRoleService |
归入 ruoyi-admin,合法依赖 system |
AsyncFactory → ISysLogininforService, ISysOperLogService |
删除 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 SysLogininfor, SysOperLog 等 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 架构原则总结
-
依赖规则:
ruoyi-security、ruoyi-web、ruoyi-datasource、ruoyi-quartz、ruoyi-generator都不能依赖ruoyi-system。只有ruoyi-admin可以同时依赖ruoyi-system和框架模块。 -
运行时零调用:
ruoyi-security在运行时(每次请求鉴权)不需要调用ruoyi-system——所有鉴权数据在登录时一次性加载到 RedisLoginUser中。 -
组合根模式:
ruoyi-admin作为组合根,负责编排 system 层和 security/web 层的交互。登录时调用 system 查 DB,组装LoginUser存入 Redis。 -
事件驱动解耦:框架模块(web/security)与业务模块(system)之间的通信通过 Spring Events 完成。框架发布事件 POJO(定义在 common),业务模块监听并持久化。
-
DTO/BO 隔离:框架模块不直接引用数据库实体(
SysUser,SysRole等),而是使用轻量的 BO(如UserProfile,RoleInfo),仅在登录时从实体转换一次。
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中的SecurityUtils、RedisCache实际依赖了 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 官方实践(UserDetails、Page、Authentication 等),使用描述性英语名词而非 DTO/VO/BO 后缀:
Controller 层 Service 层 Mapper 层 Database
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────┐
│ Request/Query│ → │ Transfer/DTO │ → │ Entity │ → │ DB │
│ (入参验证) │ │ (跨层传输) │ │ (数据库映射) │ │ │
└──────────────┘ └──────────────┘ └──────────────┘ └──────┘
↑ │
│ Response/View │ 转换发生在层边界
│ (前端展示) │ MapStruct / 手动映射
└───────────────────────────────────────┘
各层对象命名规范(参考 Spring 官方惯例):
| 层次 | 包路径 | 命名规范 | 示例 | 说明 |
|---|---|---|---|---|
| Controller 接收 | system/request/ |
{Entity}{Action}Request |
UserCreateRequest, UserUpdateRequest |
创建/更新请求体 |
| 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 的
UserDetails、GrantedAuthority;Spring Data 的Page<T>、Pageable;Spring HATEOAS 的EntityModel<T>;Spring PetClinic 示例的PetRequest、PetSummary。
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 包命名规范
不使用 dto、vo、po、bo 这种中式包名,而是使用英文语义包名:
| 不推荐 | 推荐 | 对应 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 |
UserDetails、GrantedAuthority、Authentication |
| spring-data-commons | spring-data-commons |
Page、Pageable、Sort、Repository |
| spring-tx | spring-tx |
TransactionDefinition、PlatformTransactionManager |
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 同时操作 user、user_role、user_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 中的服务(TokenService、SysLoginService、PermissionService、SysPasswordService)全部是直接类,没有接口定义。
对比: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_STR、JOB_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 天 |
更多推荐

所有评论(0)