1. 项目概览

1.1 基本信息

属性值
Group IDcom.ruoyi
Artifact IDruoyi
版本3.9.2
Spring Boot4.0.6
JDK17
构建工具Maven(阿里云镜像)
ORMMyBatis 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.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

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.javaISysUserService
web/service/SysRegisterService.javaISysConfigService, ISysUserService
web/service/SysPermissionService.javaISysMenuService, ISysRoleService
web/service/SysLoginService.javaISysConfigService, ISysUserService
manager/factory/AsyncFactory.javaSysLogininfor, SysOperLog, ISysLogininforService, ISysOperLogService
aspectj/LogAspect.javaSysOperLog

3. 核心架构问题清单

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

编号问题严重程度影响范围CodeGraph 证据
P0-1AjaxResult extends HashMap 类型不安全🔴 严重全局所有 Controller 和异常处理器均使用
P0-2两套响应封装并存(AjaxResult + R<T>)🔴 严重全局TestController 用 R<T>,其他用 AjaxResult
P0-3零测试覆盖🔴 严重全局CodeGraph 每个符号都标注 "⚠️ no covering tests"
P0-4ruoyi-common 模块职责过重、反向依赖🔴 严重所有模块SecurityUtils(18调用者)、RedisCache(17调用者)
P0-5依赖方向错误:业务实体错放 common🔴 严重全局SysUser(106调用者) 等实体在 common 中
P0-6ruoyi-framework → ruoyi-system 业务依赖🔴 严重framework6 个文件直接调用 system 的 Service
P1-1SysUser 上帝对象(62成员, 106调用者)🟠 重要多模块CodeGraph 统计 106 个调用者
P1-2SpringUtils 服务定位器反模式(13处使用)🟠 重要跨模块CodeGraph 统计 12 处 getBean() 调用
P1-3BaseController 继承反模式(18子类)🟠 重要所有 ControllerCodeGraph 统计 18 个调用者
P1-4DataScopeAspect SQL 拼接注入风险🟠 重要ruoyi-framework字符串拼接构建 SQL WHERE 子句
P1-5RuoYiConfig 静态字段与实例 setter 混用🟠 重要全局16 个调用者
P1-6PageHelper ThreadLocal 分页泄漏风险🟠 重要所有分页查询startPage() 未配对 clearPage()
P1-7日志系统紧耦合:AsyncManager + AsyncFactory🟠 重要framework→system静态方法 + SpringUtils.getBean + TimerTask
P2-1JWT 密钥硬编码🟡 一般ruoyi-frameworksecret: abcdefghijklmnopqrstuvwxyz
P2-2Druid 控制台密码硬编码🟡 一般ruoyi-frameworklogin-username: ruoyi
P2-3TestController 存在于生产代码中🟡 一般ruoyi-admin含硬编码测试数据
P2-4ServiceException 可变状态 + 覆盖父类字段🟡 一般全局40 个调用者
P2-5线程池参数硬编码🟡 一般ruoyi-frameworkThreadPoolConfig 中写死核心参数
P2-6Validator 注解放在 getter 上🟡 一般ruoyi-common非标准 Spring 实践
P2-7ISysXxxService 接口命名不规范🟡 一般ruoyi-system不符合 Spring 生态惯例
P2-8缺少 API 版本化🟡 一般ruoyi-adminURL 无版本前缀
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. 数据库实体 — userId, userName, password, email, phonenumber, sex, avatar 等
  2. 表单验证 — getter 上标注 @Xss, @Size, @NotBlank, @Email
  3. JSON 序列化控制 — @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
  4. 业务逻辑 — isAdmin() 调用 SecurityUtils.isAdmin(this.userId)
  5. 关联查询载体 — SysDept dept, List<SysRole> roles, Long[] 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
DictUtilsruoyi-commonRedisCache.class(4处)
MessageUtilsruoyi-commonMessageSource.class
AsyncManagerruoyi-framework"scheduledExecutorService"
DruidConfigruoyi-framework"slaveDataSource"
AsyncFactoryruoyi-frameworkISysLogininforService.class, ISysOperLogService.class
ShutdownManagerruoyi-framework线程池 Bean
CronUtils / JobInvokeUtilruoyi-quartz调度相关 Bean
GenUtilsruoyi-generator生成器配置 Bean

建议改造:将 DictUtils、MessageUtils 改为 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,于是将 SysUser, SysRole, SysDept, SysMenu, SysDictData, SysDictType 等业务实体全部塞入 common,导致 common 成为事实上的"业务中心"而非"公共工具库"。

5.2 方案对比

方案 A:DIP 接口方案方案 B:独立 domain 模块方案 C:Redis 缓存 + 登录桥接模式(推荐)
依赖方向admin → system → framework → commonadmin → framework → system → domain ← commonadmin 是组合根,system 和 security/web 互不知晓
实体放哪ruoyi-system新建 ruoyi-domainruoyi-system
framework 如何获取用户/角色数据通过 DTO 接口(IUserService 等)间接调用直接依赖 ruoyi-domain 中的实体运行时零调用 — 登录时全部塞入 Redis LoginUser
需要新接口4+ + 对应 DTO/BO 类00(但需要 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. SysLoginService、UserDetailsServiceImpl、SysPermissionService → 归入 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 中,只包含鉴权需要的字段(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 等
}
  1. 角色/菜单权限变更后刷新在线用户:不在 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-securitySpring Security安全框架,不应在底层ruoyi-security
spring-boot-starter-data-redisRedis 客户端缓存基础设施,不应在底层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移至原因
SecurityUtilscommon/utils/ruoyi-security依赖 Spring Security
RedisCachecommon/core/redis/ruoyi-security(暂)依赖 RedisTemplate
BaseControllercommon/core/controller/ruoyi-web依赖 PageHelper + Spring MVC
PageDomain / PageUtilscommon/core/page/ruoyi-web分页 = Web 层关注点
SysUser, SysRole, SysDept, SysMenucommon/core/domain/entity/ruoyi-system业务实体
SysDictData, SysDictTypecommon/core/domain/entity/ruoyi-system业务实体
DictUtilscommon/utils/ruoyi-system(改为 Bean)字典缓存工具(依赖 RedisCache)
MessageUtilscommon/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 模块归属总结

组件模块理由
LoginEventruoyi-common/event/纯 POJO,无任何框架依赖
OperationLogEventruoyi-common/event/纯 POJO,无任何框架依赖
ConfigChangeEventruoyi-common/event/纯 POJO,无任何框架依赖
LoginEventListenerruoyi-system/event/需注入 ISysLogininforService,需引用 SysLogininfor 实体
OperationLogEventListenerruoyi-system/event/需注入 ISysOperLogService,需引用 SysOperLog 实体
ConfigChangeEventListenerruoyi-system/event/需注入缓存服务
AsyncEventConfigruoyi-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 解耦的场景

场景事件名发布者(模块)监听者(模块)
登录成功/失败LoginEventSysLoginService (admin)LoginEventListener (system)
操作日志OperationLogEventLogAspect (web)OperationLogEventListener (system)
注册成功RegisterEventSysRegisterService (admin)LoginEventListener (system)
配置变更ConfigChangeEventSysConfigController (admin)ConfigChangeEventListener (system)
角色/菜单权限变更PermissionChangeEventSysRoleController / SysMenuController (admin)PermissionRefreshListener (system)
用户信息变更UserProfileChangeEventSysUserController (admin)UserCacheRefreshListener (system)
字典数据变更DictChangeEventSysDictDataController (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-commonSecurityUtils → ruoyi-security,域实体 → ruoyi-system5 天
T1.4 Maven 依赖瘦身common 移除 security/redis/jjwt/pagehelper 依赖2 天

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

任务描述预期工时
T2.1 ruoyi-framework 拆分拆为 ruoyi-security + ruoyi-web + ruoyi-datasource5 天
T2.2 登录编排器移入 adminSysLoginService/UserDetailsServiceImpl 等归入 ruoyi-admin3 天
T2.3 Spring Events 替换 AsyncManager删除 AsyncManager/AsyncFactory,引入事件机制4 天
T2.4 拆分 SysUser 上帝对象引入 DTO/BO 类,LoginUser 中实体替换为 UserProfile5 天
T2.5 消除 SpringUtilsDictUtils、MessageUtils 改为 Spring Bean3 天
T2.6 BaseController 改为组合抽取 PageService、CurrentUserProvider3 天
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 + SpotBugs1 天

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

任务描述预期工时
T4.1 引入 MapStruct实体 ↔ DTO/BO 转换2 天
T4.2 API 版本化/api/v1/ URL 前缀1 天
T4.3 接口命名规范化移除 I 前缀,改为 UserService / UserServiceImpl2 天
T4.4 Java 17 特性利用record 替代简单 DTO、sealed class、pattern matching3 天
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 个拆分):

#模块来源职责
1ruoyi-admin保留 + 扩大启动入口, 18 个 Controller, 登录/注册编排器(SysLoginService, SysRegisterService, UserDetailsServiceImpl, SysPermissionService 从 framework 迁入)
2ruoyi-system扩大业务实体(从 common 迁入: SysUser, SysRole, SysDept, SysMenu, SysDictData, SysDictType), Service 接口/Impl, Mapper, 事件监听器(LoginEventListener, OperationLogEventListener 等)
3ruoyi-web新增(从 framework 拆分)LogAspect, DataScopeAspect, RateLimiterAspect, XssFilter, BaseController, GlobalExceptionHandler, CaptchaConfig, AsyncEventConfig
4ruoyi-security新增(从 framework 拆分)SecurityConfig, JwtAuthenticationTokenFilter, PermissionService (@ss), SecurityUtils, TokenService, LoginUser + UserProfile BO, SysPasswordService
5ruoyi-datasource新增(从 framework 拆分)DynamicDataSource, DataSourceAspect, DruidConfig, DruidProperties
6ruoyi-common瘦身纯工具类(StringUtils, DateUtils 等), 异常体系, 注解(@Log, @Excel, @Anonymous 等), BaseEntity, AjaxResult → ApiResponse, 事件 POJO(LoginEvent, OperationLogEvent 等), RuoYiConfig
7ruoyi-quartz保留Quartz 调度, SysJob, SysJobLog(业务隔离)
8ruoyi-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-security、ruoyi-web、ruoyi-datasource、ruoyi-quartz、ruoyi-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 隔离:框架模块不直接引用数据库实体(SysUser, SysRole 等),而是使用轻量的 BO(如 UserProfile, RoleInfo),仅在登录时从实体转换一次。


11. 附录:CodeGraph 统计数据

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

符号位置调用者数风险等级
SysUserruoyi-common/.../entity/SysUser.java106🔴
ServiceExceptionruoyi-common/.../exception/ServiceException.java40🟠
SecurityUtilsruoyi-common/.../utils/SecurityUtils.java18🔴
BaseControllerruoyi-common/.../controller/BaseController.java18🟠
RedisCacheruoyi-common/.../redis/RedisCache.java17🟠
RuoYiConfigruoyi-common/.../config/RuoYiConfig.java16🟠
SpringUtilsruoyi-common/.../spring/SpringUtils.java13🟠
SpringUtils.getBean()同上12🟠
TokenServiceruoyi-framework/.../service/TokenService.java11🟠

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

调用者 \ 被调用者adminframeworksystemcommon
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}RequestUserCreateRequest, UserUpdateRequest创建/更新请求体
Controller 接收system/query/{Entity}QueryUserQuery查询条件(分页+筛选)
Controller 返回system/response/{Entity}DetailsUserDetails详情页(含关联信息)
Controller 返回system/response/{Entity}SummaryUserSummary列表摘要(精简字段)
Service 内部system/aggregate/{Entity}With{Roles}UserWithRoles聚合对象(Service 内部使用)
Mapper 层system/entity/{Entity}User数据库实体(直接映射表)

命名对照表:

当前命名(不规范)Spring 风格命名(推荐)说明
SysUserUser去掉无意义前缀 Sys
SysUserDTOUserProfile 或 UserCreateRequest描述性语义,不用 DTO 后缀
SysUserVOUserSummary(列表)/ UserDetails(详情)描述展示场景
SysRoleRole去掉前缀
SysDeptDepartment用完整单词,不用缩写
SysMenuMenu去掉前缀
SysDictData / SysDictTypeDictionaryEntry / DictionaryType语义清晰
IUserServiceUserService去掉 I 前缀
selectUserByIdfindUserByIdSpring 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 惯例
dtorequest + query(入参)/ transfer(跨层)Spring MVC @RequestBody
voresponseSpring HATEOAS RepresentationModel
poentityJPA @Entity
boaggregateDDD Aggregate
mapperrepository(可选)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-corespring-security-coreUserDetails、GrantedAuthority、Authentication
spring-data-commonsspring-data-commonsPage、Pageable、Sort、Repository
spring-txspring-txTransactionDefinition、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} → commonadmin → 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.xmlcreateTable 用 MyBatis ${sql} 拼接加强输入校验或用 PreparedStatement
域模型序列化副作用ruoyi-quartz/.../SysJob.java:113getNextValidTime() 被 @JsonFormat,JSON 序列化触发 Cron 计算声明为独立字段,由 Service 计算填充
scheduler.clear() 破坏性启动ruoyi-quartz/.../SysJobServiceImpl.java每次启动清除所有 Quartz Job只在初次运行或通过配置控制
Job 日志同步写ruoyi-quartz/.../AbstractQuartzJob.java任务执行后同步写 sys_job_log使用 @Async 或事件机制异步写入
表扫描仅支持 MySQLruoyi-generator/.../GenTableMapper.xmlinformation_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 + TimerTaskSpring 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 完整事件解耦场景

事件名所属模块发布者发布时机监听者(模块)行为
LoginEventcommonSysLoginService (admin)登录成功/失败LoginEventListener (system)异步写登录日志
LogoutEventcommonLogoutSuccessHandler (security)退出登录LoginEventListener (system)异步写登出日志
RegisterEventcommonSysRegisterService (admin)注册成功LoginEventListener (system)异步写注册日志
OperationLogEventcommonLogAspect (web)@Log 方法执行后OperationLogEventListener (system)异步写操作日志
ConfigChangeEventcommonSysConfigController (admin)配置修改ConfigChangeEventListener (system)同步刷新 Redis 缓存
PermissionChangeEventcommonSysRoleController / SysMenuController (admin)角色/菜单权限变更PermissionRefreshListener (system)刷新所有在线用户权限缓存
UserStatusChangeEventcommonSysUserController (admin)用户禁用/启用UserCacheRefreshListener (system)踢出/恢复 Redis 中的 LoginUser
DictChangeEventcommonSysDictDataController / SysDictTypeController (admin)字典数据变更DictCacheRefreshListener (system)清除字典缓存
DeptChangeEventcommonSysDeptController (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 节代码
    }
}
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐