ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

若依RuoYiApp版前后端项目结构解析:从目录到二开实战

若依RuoYiApp版前后端项目结构解析:从目录到二开实战

拿到一个开源项目,第一件事干什么?我最先看的永远是项目结构。若依(RuoYi)这类经典后台管理系统,版本又多,目录长得还不一样,RuoYiApp版更是把前端从PC端换成了移动端,很多人一clone下来就晕了。这次想从做二次开发的老兵视角,把RuoYiApp版的前后端结构逐层拆开,说清楚每个目录到底解决什么问题、改动业务时该从哪一层下手,以及那些官方文档里不会明说的坑。适合刚接触若依、或者准备基于它做移动端管理类项目的朋友。看完你能自己画出一张“改哪个菜单、找哪个文件、影响哪条链路”的地图。

1. 项目结构分析的切入点

1.1 为什么需要专门拆App版结构

很多人习惯了PC端若依的目录,拿到App版第一反应是“这不还是那套Spring Boot吗”。这句话对了一半。后端确实是同一套骨架,但App版在前端工程、接口返回、权限校验、登录状态维持上都做了不少针对移动端的调整。如果只盯着自己熟悉的模块,不去理解整体结构,后期改需求会非常被动。

拆结构这件事,本质上是在做两件事:第一,把“依赖关系”理清楚,知道哪个模块不能反过来依赖哪个模块;第二,把“运行链路”标记出来,知道一次请求从入口进入后依次穿过哪些类、哪些配置文件。RuoYiApp版的后端依然是由几个Maven模块组成的多模块工程,前端则可能是基于uni-app或类似Vue技术栈的移动端工程。这两部分加在一起,才构成一个完整的“App版”。

还有一个容易被忽略的点:项目结构不仅是代码目录,还包含配置中心、数据库脚本、打包方式、环境切换手段。很多刚入门的人只盯着src目录看,忽视了pom.xml、application.yml、sql脚本的位置,结果改了半天启动报错。所以这里我会把“结构”的范围放宽一点,按实战视角来讲。

1.2 从标题看到的两个关键问题

标题带“项目结构分析”字样,通常背后有两类需求。一类是完全没接触过若依的人,想通过目录先建立整体认知,知道这个项目有哪些零件、怎么咬合;另一类是已经在用PC版,准备做移动端改造的人,他们真正关心的是App版和标准版差异在哪里、新增业务模块该放在哪个位置。

这两类人看文章的重点完全不同。第一类人需要的是分层逻辑和依赖方向,第二类人需要的是“新增一个订单管理功能,从后端的Controller写到前端的页面,完整路径是什么”。所以我后面每一节都会先讲原理,再给一个可以直接照着做的落点。两者兼顾,但都不做教科书式罗列。

这里先给一张整体印象表,后面再逐个展开:

结构层面主要承载内容改动时最常涉及
后端公共模块工具类、常量、通用注解、统一返回体小,通常只读
后端框架模块安全、token校验、切面、拦截器权限相关才动
后端系统模块用户、角色、菜单、部门等基础业务权限、用户体系
后端管理模块Controller、启动类、配置入口几乎每天都会碰
前端移动端工程页面、请求封装、状态管理、静态资源业务页面改动
数据库脚本初始化表、菜单SQL、定时任务SQL新菜单、新字段

2. 后端工程结构逐层解析

2.1 根目录与Maven模块关系

用Maven多模块方式组织工程,是若依App版后端最显著的特征。根目录下通常有一个父pom.xml,负责统一管理依赖版本、插件版本和公共属性。下面再挂若干个相对独立的子模块。切成多模块的好处是依赖边界清晰,比如业务模块只能依赖框架模块暴露的能力,不能直接去操作数据库底层细节。

典型的模块组合大致长这样:

ruoyi-admin // 启动入口、Controller层、配置文件 ruoyi-framework // 安全认证、AOP、拦截器、配置类 ruoyi-system // 系统核心业务Service、Mapper、实体类 ruoyi-common // 工具类、常量、统一返回对象 ruoyi-quartz // 定时任务(按需保留) ruoyi-generator // 代码生成(按需保留)

App版通常不会做太多稀奇古怪的改名,所以看到这四个直接依赖关系就能明白大半:admin是门面,framework是骨架,system是内脏,common是公共零件。依赖方向是单向的,越往上越靠近业务入口,越往下越通用。如果有人把ruoyi-admin里的工具类放到ruoyi-system里调用,就破坏了这种结构,后期维护会很痛苦。

第一次看这个结构,建议先用mvn dependency:tree把依赖树跑出来,看看实际生效的依赖关系是什么,比单纯翻pom.xml更直观。实际项目里可能有开发者私自加了某些本地依赖,导致编译环境跟干净仓库不一致,依赖树一眼就能看出问题。

2.2 common模块的核心内容

common在四个模块里看起来最不起眼,却是被引用最多的。它承担的是“地基”职能:所有模块都要用到它,但它不应该反向依赖任何业务模块。实际代码里,它主要包含这几类东西。

第一类是基础返回体AjaxResult,App版通常还会封装一个R对象,用于统一返回状态码、消息和数据体。移动端请求不像PC端那样随时能看Network面板,返回体里的code语义必须一致,否则前端request封装会判断错。第二类是工具类,比如String、Date、Excel工具,身份证号校验、文件上传命名处理,都扔在这里。第三类是常量类,例如通用状态标识、用户状态、删除标记,很多还有Constants和SecurityConstants两个类,分别存放普通常量和安全相关常量。

还有一类很关键但容易被忽略的:异常体系。很多初学者在业务里到处try...catch,吞掉了异常,还找不到问题。正确做法是业务里抛出自定义的ServiceException,由框架统一捕获并转成标准错误返回体。这套异常类在common里定义,具体捕获逻辑在framework里实现,这也是“结构决定行为”的典型案例。

说一个实操经验:不要为了省事在common里引入大量第三方组件,比如把某个图片处理的SDK直接放在common依赖里。一旦common的依赖变得臃肿,所有模块都会被拖累,每次打包体积和编译时间都会上涨。保持它足够干净,是最划算的投资。

2.3 framework模块的功能边界

framework是整个若依App版最体现架构水平的地方。它的职责可以概括成三句话:管好“谁进来”、管好“谁能做什么”、管好“进来了以后能留下什么痕迹”。对应到代码就是安全认证、权限校验、操作日志和异常处理。

安全认证这一块,App版通常采用令牌机制而不是Session。用户在App端登录后,后端签发一个token,前端把token存起来,后续每次请求都带上。token的生成、解析、校验、续期逻辑就落在framework里。一般会看到一个类似SecurityConfig的配置类,负责放行哪些URL、拦截哪些URL。这里有个非常容易踩的坑:App端的接口路径和PC端的接口路径往往不同,比如多了/app前缀,配置放行规则时如果不小心把整段前缀放行,等于给未登录用户开了一扇门。

Permissions和Roles的注解也在framework里定义并使用。常见用法是在Controller方法上挂注解,标注需要什么权限码。很多人疑惑权限码是怎么匹配到用户的,其实逻辑集中在framework的PermissionService中,它会去用户对象里找已有的权限集合,然后做包含判断。理解了这一点,你给角色配权限时就能明白sys_menu里的perms字符串为什么必须和注解里写的完全一致。

framework里还有一堆Config结尾的类,比如MyBatis配置、Redis配置、线程池配置、文件上传配置。说句实话,我看着这些配置类也头大,但它们决定系统能不能跑起来。改动前最好先搞清楚哪个配置对应哪段逻辑,不要一上来就改连接池参数。

2.4 system模块与admin模块的区别

对很多做业务开发的人来说,system和admin是最容易混淆的两个模块。简单区分:system管业务数据和底层服务,admin管接口暴露和启动入口。

继续拆细一点。system模块里躺着用户、角色、菜单、部门、岗位、字典、参数、通知公告等基础业务的实体类、Mapper接口、XML文件、Service接口和实现类。你要是新增一张“订单表”,想跟系统权限打通,标准做法是在system模块里新增一套domain、mapper、service,Controller层可以放在admin里,或者按App版约定放在单独的controller/app包下。

admin模块承担的是Controller、启动类、配置文件。它依赖framework和system,是整个后端启动的入口。你会在这里看到Application类,通过@SpringBootApplication启动Spring Boot。配置文件也放在admin模块的资源目录下,如application.yml、application-druid.yml、logback.xml。

这里有个很常见的困惑:为什么Controller放在admin而不是system?原因很简单:Controller是外部接口,要靠近入口层,避免业务模块直接暴露给网络层。如果哪天你要把组件拆出去做微服务,后面就能体会到这种分层的价值——system模块直接变成公共依赖,admin模块则成为独立服务。

3. 前端App端的目录与职责

3.1 前端框架选型与目录总览

RuoYiApp版的前端最大的特点是“一套代码,多端运行”。在不纠结具体市场活动的情况下,通常采用类uni-app的Vue技术栈。这样做可以只保留一套页面,编译成H5、小程序和原生App安装包。目录规划和PC端后台管理项目有很大区别,更强调平台差异和移动端性能。

典型目录结构大概长这样:

├── api // 接口请求定义,按业务模块分文件 ├── components // 公共组件 ├── pages // 页面文件 ├── static // 静态资源 ├── store // Vuex状态管理 ├── utils // 工具库,含request请求封装 ├── App.vue // 根组件 ├── main.js // 入口 ├── manifest.json // 应用配置 ├── pages.json // 页面路由与底部tabBar配置

看这个结构的时候,不要只盯着目录名。关键要理解:pages.json的地位类似于后端路由表,每个页面注册在哪、是否需要在tabBar中展示,都通过它控制。manifest.json则是真正面向App平台的配置,比如应用图标、启动页、SDK参数,等操作原生能力时才需要改它。

移动端前端和PC后台管理系统最大的差异在性能预算上。PC端可以懒加载一大堆组件,App端如果页面上一次引入几十个组件,首屏会明显卡顿。所以实际项目中尽量让页面轻量化,独立的弹窗和复杂组件放到components里再按需引用。

3.2 api层、store层、utils层的分工

api目录是给后端接口做“客户端代理”的地方。通常一个业务模块一个文件,比如login.js、user.js、order.js。每个文件导出若干方法,方法内部调用request封装的请求函数。这里建议和Controller路径保持一致,比如后端是/app/order/list,前端就建一个order.js并导出listOrder方法,回头排查问题省一半时间。

store目录是全局状态的中心。登录状态、用户基本信息、权限点、应用配置这些需要跨页面共享的数据,放进store里。像临时表单数据这种页面私有不全局共享的数据,则不应该放进去。很多初学者把什么都往store里塞,结果页面一多,store变得比业务代码还复杂,改一个数据字段要全局搜索。App端的store应该更克制,只放真正的全局状态。

utils目录里的request.js是整个前端网络层的核心,也是新手最容易忽略但又最应该读懂的文件。它负责统一拼接BASE_URL,在header里自动附加token,根据返回码做集中处理,比如401强制跳转登录、code=500直接弹出错误提示。改这个文件会影响所有接口的行为,所以动它之前体会一下“牵一发动全身”的感觉。

这里分享一个小技巧:给request.js里的请求增加一个可选的isForm参数,用于兼容表单提交和JSON提交。App端部分第三方SDK回调会强制要求Content-Type为表单格式,没有这个参数你会被坑到怀疑人生。这属于常见的二次开发场景,不是若依本身的标准功能。

3.3 页面与组件的组织方式

pages目录按业务域划分页面,比如pages/login、pages/order、pages/user。每个页面通常由四个文件组成:.vue页面文件、.js逻辑文件、.json页面配置、.css/scss样式文件。如果使用了组合式API,.js里也可能以<script setup>的形式写在.vue里,实际以项目代码为准。

页面之间跳转建议用uni.navigateTo,不要随便用uni.redirectTo。前者可以保持页面栈,用户能返回上一级;后者关闭当前页再跳转,经常导致业务流断裂。组件则按粒度区分:跨页面复用的放components公共目录,业务内私有组件直接放在页面子目录下,这样既方便复用,又不至于在公共目录里堆满一次性组件。

App版还有个特殊页面:登录页。它通常不进入tabBar,路由配置上要做特殊标记。登录后的页面和未登录页面分开管理,登录状态挂在store或本地Storage中。这里有个细节:不要只在request.js里做401跳转,还应该在路由拦截器里做一次前置判断,否则用户手动进入未登录页面时会出现白屏,而不是被引导到登录页。

4. 核心业务流转与配置关联

4.1 一次登录请求的后端处理链路

理解了前后端目录,接下来要串起来看一条完整的请求链路。我们从登录接口开始。

用户在App端输入账号密码,点击登录后,前端login.js里的login方法调用request.post('/login', data)。请求进入后端,先被Spring Security的过滤器链拦下。通常在SecurityConfig里,登录接口会被配置为匿名可访问。接着请求到达LoginController,这里可能是App版单独的入口,也可能是复用公共的LoginController,区别在于返回的数据结构和登录成功后的落地处理。

LoginController调用LoginService,先校验验证码。App版如果关闭了本地验证码,至少会做账号密码校验和用户状态校验,比如是否锁定、是否被停用。校验通过后,生成token,把用户信息、权限信息写入Redis,然后把token和用户基本信息返回给前端。这里的核心动作是“无状态认证”,后端不再保存Session,所有请求靠token识别身份。

前端拿到token后,会把它存起来,并解析出用户信息放store。后续所有需要身份验证的接口,request.js都会自动在请求头上加Authorization。后端的token过滤器先解析header里的token,取出登录用户ID,再把用户信息放到当前线程上下文里。业务代码里通过工具方法就能拿到当前登录用户,不用每次在Controller参数里手动传用户ID。

4.2 权限注解与token校验的实现位置

权限校验的“大脑”在framework模块。你只需要在Controller方法上写好注解,框架会自动执行校验。它的执行逻辑并不神秘:请求进入后,权限切面拦截带有权限注解的方法,解析注解里要求的一个或多个权限码,再和当前登录用户拥有的权限集合做匹配。匹配通过才放行,不通过就抛出异常。

token校验则由一个自定义过滤器完成,处理逻辑大致如下:

// 伪代码,展示思路 String token = getTokenFromRequest(request); if (StringUtils.isNotBlank(token)) { String userId = jwtUtils.parseToken(token); if (userId != null && SecurityUtils.getAuthentication() == null) { LoginUser loginUser = loginUserService.getLoginUser(userId); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authToken); } }

很多人在二次开发时一发现某个接口没有权限控制,第一反应是去Controller里加注解。其实如果加了注解还不生效,要优先检查两件事:一是这个接口是不是被配置成匿名访问了,二是当前用户有没有对应的权限码,而不是权限判断本身坏了。这属于排查思路上的结构性问题。

4.3 数据权限与多租户的取舍

若依App版在后端结构上为多部门、多层级数据隔离预留了数据权限方案。常见做法是用@DataScope注解加AOP拦截,根据部门ID和用户角色自动给SQL拼接数据过滤条件。举个例子,用户A只看本部门数据,用户B能看所有部门数据,这些规则不是写死在每个Mapper里的,而是通过数据权限机制统一处理。

不过这玩意也不是完美的。实际开发中,一旦表格查询用了复杂的自定义SQL,比如多表关联后又加了子查询,数据权限自动拼接的SQL可能跑出意料之外的结果。我的建议是:基础模块尽量保留数据权限支撑,但新开发的业务模块如果数据模型简单,可以直接在业务层手动过滤,反而更可控。

多租户则是另一个话题。若依本身并不是天然的多租户架构,它更像“单库单应用+权限控制”的模式。如果要做SaaS化多租户,项目结构上要增加的通常是租户标识字段、租户上下文解析、以及SQL拦截器强制附加租户条件。这属于较大的架构改造,不要指望靠配置能解决。

5. 改动项目时最容易被忽略的细节

5.1 打包与多环境配置

后端打包一般就是mavenpackage,但App版经常要面对不同环境。开发环境连测试库,生产环境连正式库,配置文件切换靠application.yml里的spring.profiles.active。容易忽略的点在于:改环境时不只是改数据库地址,还有Redis地址、文件存储路径、日志级别。这些内容分布在多个配置片段里,建议在修改前先全局搜一遍当前环境名,确认是否全都改到位。

前端的多环境更麻烦。如果是H5方式运行,接口地址可以放在单独的配置文件里,比如config.js或者.env文件;如果是原生App包,则要注意打包时有没有把接口地址正确编译进去。很多人在真机上调试一切正常,打成正式包后请求全部失败,原因就是接口地址还写着局域网IP。

数据库脚本的位置也容易被忽略。一般在项目根目录的sql文件夹下,文件名带日期,比如ry_2024xxxx.sql。首次部署要先执行菜单表、字典表、用户表的基础脚本,再执行可选模块的SQL。改动字段时不要直接在测试库里加列而不更新脚本,否则一个新人拉最新代码后连库都建不起来。

5.2 日志、缓存与异步任务的实际落点

日志配置通常在logback.xml或logback-plus.xml里管理。很多开发者习惯用System.out.print调试,这在小规模项目没问题,但在若依这种工程里不建议。统一用日志框架打印,并且对应到具体模块的logger名,这样线上排查问题时按模块关键字过滤日志,比在一堆输出里大海捞针高效得多。

缓存方面,RuoYiApp版最常用的缓存载体是Redis。验证码、token、登录用户信息、参数配置都缓存在Redis里。这里有一个旧项目里很典型的坑:改了用户权限后用户没立即生效,卡在Redis里的旧权限缓存上。处理办法是刷掉对应用户的缓存,或者修改权限保存逻辑时主动清理。目录结构里找到对应缓存管理类,这个动作就变得很轻量。

异步任务主要依赖线程池或定时任务模块。若依自带定时任务模块,支持通过后台界面动态管理任务,任务类放哪个包需要看项目的约定。我个人建议把任务类单独放在业务模块的task包中,和Controller、Service区分开,否则新增任务时一直找不到文件。

5.3 数据库脚本与菜单表的初始化

菜单表sys_menu是整个权限系统的“源数据”。新增菜单前,必须搞清楚几个字段:menu_name是菜单名,path是前端路由路径,component是页面文件路径,perms是权限标识。很多新手加了菜单但不加组件,或者组件路径写错一个字母,结果前端页面空白,后端接口调通了却无法从菜单点进去。

这里补充一个我在实际项目中的习惯:新增一个菜单时,先用SQL查一下最大排序号,避免菜单位置被插到前面导致排序混乱。如果新增“订单管理”模块,要从父菜单开始插,再插入二级菜单和按钮权限。不要一次插一堆不填parent_id的菜单,否则前端渲染出来一团乱。

数据库初始化还有个容易被忽略的点:类型不同的数据库,比如MySQL和Oracle,SQL脚本语法有区别。若依App版实际部署中,MySQL用户占多数,但只要你切换过数据库就明白“结构兼容”有多重要。写字段长度、默认值、注释时尽量遵循兼容原则,减少切换成本。

6. 关于RuoYiApp版的几点实践心得

6.1 适合直接二次开发的模块组合

对大多数业务场景,我建议把项目结构看作“骨架 + 业务扩展”的组合。用户、角色、菜单、部门、字典这些模块直接复用它提供的就好,尽量不要为了炫技重写。真正需要花心思的是新业务模块。

新业务模块放到哪里是有讲究的。如果业务和系统管理功能定位接近,比如增加一个“客户管理”,可以考虑直接写在system模块里,这样和用户模块的数据交互最方便。如果业务模块非常大,比如电商、工单、财务,建议另起一个独立Maven模块,比如ruoyi-business,再让admin模块依赖它。这样不会和基础系统代码混在一起,后续拆服务也更有底气。

还有一点,项目结构虽然是固定的,但使用方式可以灵活。RuoYiApp版很多接口返回结构是专为App端设计的,但如果你在开发过程中发现某个接口在PC端也要复用,不要硬塞,尽量单独封装一个方法或Controller。结构是给人服务的,不是给人制造约束的。

6.2 我踩过的典型坑

讲几个真实遇到过的位置,给后面接手的人提个醒。

第一个是登录token过期后,App端表现很诡异。PC端可能跳转登录页就算了,App端如果不处理401,用户会一直停留在当前页面,反复点击也没有反馈。解决方案不是只改后端,还要在前端request.js的拦截器里做好401统一处理,并给出“登录已过期,请重新登录”的提示。

第二个是LocalStorage的使用。App端很多开发者习惯沿用Web思维,把用户信息整块塞进LocalStorage。这种做法在H5上没毛病,但在小程序和原生App上会踩兼容坑。建议把关键用户信息放到store里,持久化只保留token和少量必要信息,再需要时按接口拉取。

第三个是文件上传路径问题。本地开发时上传文件通常存在项目服务器目录,打包部署时这个路径往往不在应用的目录下,导致上传成功但访问404。能定位到配置文件是一方面,更重要的是看项目结构里关于静态资源映射的配置,把上传目录和访问URL的映射关系事先理清,否则上线后才改就很被动。

最后再分享一个小技巧:接到这种结构分析任务,别急着看源码。先执行mvn dependency:tree看后端,再打开前端根目录下的manifest.json和pages.json看平台配置。这两步大概花十分钟,但能帮你快速判断当前版本裁剪了哪些模块、哪些页面才是真正的入口。结构分析的价值从来不是背目录,而是让后续每一个改动都“知道自己在哪、改完会影响谁”。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进