ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RuoYi分离版代码生成器原理与契约式开发指南

RuoYi分离版代码生成器原理与契约式开发指南 1. 为什么在 RuoYi 分离版里“加个子模块”会卡住三天你刚接手一个基于 RuoYi 分离版Spring Boot Vue的项目产品经理甩过来一句“这个新需求要加个‘设备巡检管理’模块参考现有‘用户管理’的结构下周上线。”你信心满满点开 IDEA打开代码生成器填完表名sys_inspect_task勾选“生成前端”点击“生成”然后——IDEA 卡住、生成目录空空如也、控制台刷出一串红色报错、src/main/java/com/ruoyi/xxx下连个包都没建出来。这不是你一个人的遭遇。我在三个不同团队做技术支撑时平均每周都会接到两通类似电话“代码生成器点了没反应”“生成的 Controller 缺了 PreAuthorize 注解”“前端页面里 el-form-item 的 label 宽度和 input 不对齐”。RuoYi 官方文档写的是“一键生成”但现实是它不是傻瓜式按钮而是一套需要你理解其契约关系的模板引擎系统。它不生成“功能”只生成“符合 RuoYi 约定结构的骨架代码”。一旦你没对齐它的约定——比如数据库字段命名没加sys_前缀、没在generator.yml里配好模块路径、或者 IDEA 的 Maven 配置指向了错误的 JDK 版本——生成器就立刻沉默连个像样的错误提示都不给。这背后的核心矛盾在于RuoYi 分离版的代码生成器本质是 Velocity 模板 MyBatis-Plus 反射 Spring Boot 自动装配三者强耦合的产物。它默认假设你已完全内化了 RuoYi 的三层包结构com.ruoyi.project.xxx、前端路由注册方式router/index.js的动态 import、以及权限注解的注入逻辑PreAuthorize(ss.hasPermi(xxx:list))。当你试图添加一个“子模块”你真正要做的不是点按钮而是在 RuoYi 的整个契约体系里为新模块预留并注册四个关键锚点后端包路径、前端路由、菜单权限标识、以及数据库表与实体类的映射关系。漏掉任何一个生成器就会在某个环节断链而它不会告诉你断在哪——它只会返回一个null或抛出NullPointerException然后让你对着日志里一行at com.ruoyi.generator.util.GenUtils.generateFile(GenUtils.java:128)发呆。所以这篇文章不叫“手把手教你用代码生成器”因为那只是表象。我要带你拆开 RuoYi 分离版的生成器外壳看清它内部的齿轮如何咬合为什么generator.yml里author字段必须和pom.xml的groupId一致为什么前端生成的api/inspect.js里baseURL是/dev-api而不是/prod-api为什么GenTableController.java里RequestMapping(/tool/gen)这个路径不能改搞懂这些你才能把“加子模块”从玄学操作变成可预测、可调试、可复用的标准化流程。接下来我们就从最基础的环境校验开始一环扣一环地重建这套契约。2. 环境校验IDEA 里那些“看起来正常”的配置90% 都藏着致命陷阱很多人以为只要 IDEA 装好了、JDK 装好了、Maven 装好了就能跑 RuoYi。错。RuoYi 分离版对开发环境的“一致性”要求极高它不像普通 Spring Boot 项目那样宽容。我见过太多案例开发机上能跑通打包部署到测试服务器就报ClassNotFoundException或者本地生成器能用换台新电脑重装 IDEA 就彻底失效。问题几乎都出在环境校验这一步——而校验的关键不是看软件有没有装而是看它们之间是否形成了 RuoYi 所需的精确版本契约。2.1 JDK 版本与 Maven 编译目标的隐性绑定RuoYi V4.7.x当前主流分离版的pom.xml中明确指定了java.version11/java.version和maven.compiler.source11/maven.compiler.source。这意味着你的 IDEA 必须使用 JDK 11 作为 Project SDK且 Maven 的settings.xml中的jdk配置也必须指向同一份 JDK 11。注意这里有两个独立的配置点IDEA Project SDKFile → Project Structure → Project → Project SDK必须选择11 (java version 11.0.x)。如果选了 JDK 17即使你强制设置了maven.compiler.source11IDEA 在编译generator模块时仍会因var关键字或switch表达式语法报错因为 IDEA 的实时语法检查器会按 Project SDK 版本解析代码。Maven 的 JDK 绑定打开~/.m2/settings.xmlWindows 是C:\Users\{用户名}\.m2\settings.xml检查profiles节点下是否有类似配置profile idjdk-11/id activation activeByDefaulttrue/activeByDefault /activation properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target maven.compiler.compilerVersion11/maven.compiler.compilerVersion /properties /profile更关键的是确保profiles外层的activeProfiles启用了这个 profile。如果settings.xml里没配Maven 默认会用自己内置的 JDK通常是 JDK 8导致mvn clean install时编译通过但运行generator模块的main方法时因反射调用java.lang.Class.getDeclaredMethods()等 API 版本不兼容而崩溃。提示验证方法是在 IDEA 终端执行mvn -v输出中Java version必须与Project SDK一致再执行mvn compile -X | grep compiler确认maven.compiler.source和target输出为11。2.2 IDEA 的 Maven 导入策略别让“自动导入”毁掉生成器RuoYi 分离版的generator模块是一个独立的 Maven 子模块但它依赖于ruoyi-framework和ruoyi-common的编译产物。默认情况下IDEA 的Build → Build Project只编译当前模块而generator模块的pom.xml里scopecompile/scope的依赖项需要ruoyi-framework的 class 文件已存在target/classes目录下。如果你直接File → Open打开ruoyi根目录IDEA 会自动导入所有模块但它的默认行为是“延迟编译”——即只加载.iml文件不立即编译依赖模块。这就导致一个经典陷阱你右键generator模块的GenTableController.java点击Run GenTableController.main()IDEA 报错java.lang.NoClassDefFoundError: com/ruoyi/common/core/domain/AjaxResult。原因很简单ruoyi-common模块还没被编译过target/classes下没有AjaxResult.class。此时你本能地去点Build → Build Project但 IDEA 依然可能只编译了generator模块本身因为它没识别出这个main方法需要整个依赖树。正确做法是强制全量编译在 IDEA 右侧Maven工具窗口若未显示View → Tool Windows → Maven展开根项目ruoyi双击ruoyi → Lifecycle → clean等待完成再双击ruoyi → Lifecycle → install注意是install不是compile这会触发ruoyi-common、ruoyi-framework、ruoyi-system等所有模块的编译并将 jar 包安装到本地 Maven 仓库~/.m2/repository此时再运行generator的main方法才能确保所有依赖类都已就位。注意install操作耗时较长通常 2-5 分钟但这是不可跳过的步骤。我曾帮一位同事排查他坚持用compile反复重试 7 次最后发现ruoyi-common的target/classes目录下确实缺少AjaxResult.class而install后该文件立刻出现。2.3 数据库连接池与生成器的“心跳检测”机制RuoYi 的代码生成器在启动时会执行一次数据库元数据查询SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA ruoyi AND TABLE_NAME sys_user以获取表结构。这个查询由DruidDataSource执行而 Druid 有一个鲜为人知的“心跳检测”特性当initialSize设为 0 时连接池在首次获取连接前会先执行validationQuery默认是SELECT 1来验证连接有效性。如果数据库服务未启动或application.yml中的url、username、password配置有误生成器会在GenTableController.java的第 89 行dataSource.getConnection()处抛出SQLException但堆栈信息被try-catch吞掉只留下一行log.error(获取数据库连接失败, e)而日志级别设为ERROR如果你没打开logback-spring.xml的DEBUG级别根本看不到这条日志。快速定位法打开ruoyi-generator/src/main/resources/application.yml找到spring.datasource节点确认url是否包含正确的数据库名如jdbc:mysql://localhost:3306/ruoyi?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai将initialSize: 1而非 0并添加testWhileIdle: true和validationQuery: SELECT 1在logback-spring.xml中将com.ruoyi.generator的 logger level 改为DEBUG重新运行main方法控制台会清晰打印出连接失败的具体原因例如Access denied for user rootlocalhost或Unknown database ruoyi。这三点校验看似琐碎却是所有生成器故障的根源。它们不是“环境配置”而是 RuoYi 生成器运行的“物理定律”。跳过任何一条后续所有操作都是在流沙上盖楼。3. generator.yml 的深层契约你以为在填表单其实是在签署一份法律协议当你打开ruoyi-generator/src/main/resources/generator.yml看到author: ruoyi、packageName: com.ruoyi.project.tool这些字段时很容易把它当成一个简单的配置表单。但事实上这份 YAML 文件是 RuoYi 生成器的“宪法”每一个字段都对应着代码生成过程中一个不可绕过的契约点。修改它不是在调整参数而是在重写 RuoYi 的基因序列。我曾见过最典型的错误开发人员为了“个性化”把packageName改成com.mycompany.inspection结果生成的 Java 类里MapperScan(com.mycompany.inspection.mapper)与ruoyi-system模块的MapperScan(com.ruoyi.*.mapper)冲突导致 MyBatis 扫描不到新 mapper接口永远返回空列表。3.1packageName不只是包名它是 Spring Boot 的组件扫描边界RuoYi 的后端采用多模块结构其中ruoyi-system模块负责核心业务它的SpringBootApplication注解默认扫描com.ruoyi包下的所有组件。而generator.yml中的packageName决定了生成的 Controller、Service、Mapper 等类的顶层包路径。这个路径必须以com.ruoyi.project.开头且其后的层级必须与 RuoYi 的模块划分逻辑一致。例如标准的sys_user表生成在com.ruoyi.project.system下对应ruoyi-system模块而gen_table表生成在com.ruoyi.project.tool下对应ruoyi-generator模块。如果你要添加“设备巡检”子模块正确的packageName应该是com.ruoyi.project.inspection而不是com.ruoyi.inspection或com.mycompany.inspection。为什么com.ruoyi.project.inspection会被ruoyi-system的MapperScan(com.ruoyi.*.mapper)扫描到因为*匹配project.inspectioncom.ruoyi.inspection则不会被扫描因为ruoyi-system的MapperScan规则不覆盖com.ruoyi.inspection.mappercom.mycompany.inspection更是完全游离于 RuoYi 的扫描体系之外。更进一步packageName还决定了前端路由的模块归属。生成的inspection/index.vue页面在router/index.js中会被注册为children: [{ path: inspect, component: () import(/views/inspection/index) }]而/views/inspection/这个路径正是由packageName的最后一级inspection映射而来。如果packageName是com.ruoyi.project.device.inspection那么前端路径就会变成/views/device/inspection/这会导致路由注册失败因为router/index.js的import语句是硬编码的import(/views/${moduleName}/index)其中${moduleName}直接取自packageName的最后一级。3.2author字段一个被严重低估的“版权签名”generator.yml中的author: ruoyi看似只是生成代码头部的作者注释如author ruoyi。但它的作用远不止于此。RuoYi 的GenUtils.java在生成 Java 文件时会调用Velocity引擎渲染模板而 Velocity 模板如controller.java.vm中有一行关键代码#set($author $!{config.author})。这个$author变量不仅出现在author注释里还参与了RequestMapping路径的拼接逻辑。查看controller.java.vm模板你会发现RestController RequestMapping(/${packageName?replace(com.ruoyi.project., )}/${className?uncapitalize}) public class ${className}Controller extends BaseController这里的${packageName?replace(com.ruoyi.project., )}就是把com.ruoyi.project.inspection替换成inspection作为 URL 的一级路径。但如果author字段被改成mycompany而packageName没同步修改模板里的replace操作就会失效导致RequestMapping变成/com.ruoyi.project.inspection/inspectTask这显然不符合 RESTful 规范前端调用时会 404。更重要的是author字段还与权限注解强绑定。生成的 Controller 方法里有PreAuthorize(ss.hasPermi(${packageName?replace(com.ruoyi.project., )}:${functionName}:list))。如果packageName是com.ruoyi.project.inspectionfunctionName是inspectTask那么权限标识就是inspection:inspectTask:list。这个标识会被存入sys_menu表的perms字段用于 Shiro 权限校验。如果author被乱改导致packageName替换失败权限标识就会变成com.ruoyi.project.inspection:inspectTask:list而菜单管理界面里你手动添加的菜单权限却只写了inspection:inspectTask:list两者永远不匹配用户点了菜单就提示“无权限”。3.3tablePrefix数据库表名与 Java 类名的“翻译官”tablePrefix: sys_这个配置是 RuoYi 生成器最精妙的设计之一。它定义了数据库表名到 Java 实体类名的映射规则。RuoYi 约定所有业务表名必须以sys_开头如sys_user,sys_role而生成的实体类名则去掉这个前缀变成User,Role。这个规则由GenTableServiceImpl.java中的convertClassName(String tableName)方法实现public static String convertClassName(String tableName) { if (StringUtils.isNotEmpty(tableName)) { // 移除表前缀 tableName tableName.replaceFirst(config.getTablePrefix(), ); // 下划线转驼峰 return StringUtils.convertToCamelCase(tableName); } return null; }如果你的巡检表名是sys_inspect_tasktablePrefix设为sys_生成的实体类就是InspectTask如果设成t_就会变成InspectTask因为t_sys_inspect_task去掉t_后是sys_inspect_task再转驼峰还是SysInspectTask这显然不对。但问题在于tablePrefix还影响着Mapper.xml文件中的 SQL 语句。查看mapper.xml.vm模板你会发现select idselect${className}List resultType${packageName}.domain.${className} select * from ${tableName} /select这里的${tableName}直接取自数据库元数据不会被tablePrefix修改。也就是说tablePrefix只影响 Java 类名和包路径不影响 SQL 中的表名。因此tablePrefix必须与你实际的数据库表名前缀严格一致。如果数据库里表名是inspection_task没有sys_前缀而你把tablePrefix设为sys_生成的InspectTaskMapper.java里Select(select * from inspection_task)是对的但MapperScan扫描不到这个 mapper因为InspectionTaskMapper类被生成在com.ruoyi.project.inspection.mapper包下而ruoyi-system的MapperScan规则是com.ruoyi.*.mapper它能扫描到但InspectionTaskMapper.xml文件里的mapper namespacecom.ruoyi.project.inspection.mapper.InspectionTaskMapper与InspectionTaskMapper.java的Mapper注解冲突导致 MyBatis 初始化失败。实操心得tablePrefix的唯一安全值就是你数据库中所有业务表的实际前缀。RuoYi 官方示例用sys_是因为它的sys_user、sys_menu等表都带这个前缀。如果你的巡检表是inspection_task那就把tablePrefix改成空字符串并确保tableName在数据库里就是inspection_task这样生成的实体类名才是InspectionTask一切才对得上。4. 生成器源码级调试从GenTableController.java的第 1 行开始逐行追踪生成链路当生成器“没反应”或“生成不全”时90% 的开发者会重启 IDEA、清缓存、重装插件。这些操作治标不治本。真正的解决之道是像外科医生一样打开生成器的源码用断点一步步跟踪它的执行流看清数据在哪个环节丢失、哪个对象为null、哪个条件判断被跳过。RuoYi 的生成器入口非常清晰ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenTableController.java的main方法。我们以此为起点逐层下钻。4.1main方法启动一个微型 Spring Boot 应用GenTableController.java的main方法本质上是启动了一个极简的 Spring Boot 应用只为运行生成逻辑public static void main(String[] args) { ConfigurableApplicationContext context SpringApplication.run(GenTableApplication.class, args); GenTableService genTableService context.getBean(GenTableService.class); // ... 调用生成方法 }这里的关键是GenTableApplication.class。它不是一个普通的SpringBootApplication而是ruoyi-generator/src/main/java/com/ruoyi/generator/GenTableApplication.java其内容极其精简SpringBootApplication(exclude {DataSourceAutoConfiguration.class, MybatisAutoConfiguration.class}) public class GenTableApplication { public static void main(String[] args) { SpringApplication.run(GenTableApplication.class, args); } }exclude参数排除了DataSourceAutoConfiguration和MybatisAutoConfiguration意味着这个应用不加载任何数据库连接和 MyBatis 配置。它只加载generator模块自己的 Bean如GenTableService、GenUtils、VelocityConfig。所以当你在main方法里调用genTableService.generatorCode(tableName)时它走的是纯内存逻辑不访问数据库——这解释了为什么有时你改了application.yml的数据库配置main方法却依然报错因为main方法根本不用那个配置。调试技巧在main方法第一行ConfigurableApplicationContext context ...处打断点F8 单步进入你会看到 Spring Boot 的启动日志确认GenTableApplication是否成功初始化。如果卡在这里说明pom.xml依赖有冲突或resources目录下有非法的application.properties文件干扰了启动。4.2GenTableService.generatorCode()生成逻辑的中枢神经generatorCode方法是整个生成过程的中枢。它接收表名tableName如sys_inspect_task然后执行一系列操作查询表元数据调用genTableMapper.selectGenTableByName(tableName)从sys_gen_table表中查出该表的配置如果已存在构建 GenTable 对象如果sys_gen_table中没有记录则调用GenTableServiceImpl.initTableField()通过 JDBC 查询information_schema.COLUMNS构建GenTable实体生成代码文件调用GenUtils.generatorCode(genTable, writer)这才是真正的生成动作。重点在第 2 步。initTableField()方法里有一段关键代码ListGenTableColumn columns dataScopeMapper.selectTableColumnsByTableName(tableName);这里的dataScopeMapper是ruoyi-common模块的DataScopeMapper.java它依赖于ruoyi-common的DataSource。但GenTableApplication排除了DataSourceAutoConfiguration所以dataScopeMapper的DataSource是nullRuoYi 的解决方案是在GenTableService的PostConstruct方法中手动创建了一个HikariDataSourcePostConstruct public void init() { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE); config.setUsername(sa); config.setPassword(); config.setDriverClassName(org.h2.Driver); dataSource new HikariDataSource(config); }它用 H2 内存数据库模拟了元数据查询。所以initTableField()实际查询的是 H2 数据库而不是你本地的 MySQL。这就是为什么有时你改了 MySQL 的表结构生成器却没反应——因为它查的是 H2 里预置的information_schema。调试技巧在initTableField()方法里dataScopeMapper.selectTableColumnsByTableName(tableName)这行打断点F7 进入DataScopeMapper.selectTableColumnsByTableName再 F7 进入DataScopeMapper.xml的 SQL你会看到它执行的是SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME ?。此时观察dataSource的 URL确认它是jdbc:h2:mem:testdb而非你的 MySQL URL。这提醒你生成器的元数据来源是 H2不是你的生产库。4.3GenUtils.generatorCode()Velocity 模板的最终执行者generatorCode方法的最后一行是GenUtils.generatorCode(genTable, writer)。GenUtils是生成器的“发动机”它封装了所有模板渲染逻辑。其核心是Velocity引擎的初始化private static VelocityEngine velocityEngine; static { Properties p new Properties(); p.setProperty(resource.loader, class); p.setProperty(class.resource.loader.class, org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader); p.setProperty(input.encoding, UTF-8); p.setProperty(output.encoding, UTF-8); velocityEngine new VelocityEngine(p); }velocityEngine从classpath加载模板文件路径是templates/目录下的.vm文件。generatorCode方法会遍历genTable.getColumns()为每个 Java 类型controller.java.vm,service.java.vm,mapper.java.vm,mapper.xml.vm,vue.vm调用velocityEngine.mergeTemplate()将genTable对象的数据模型Model注入模板生成最终代码。这里最容易出错的是模板路径。ruoyi-generator/src/main/resources/templates/目录下必须有完整的.vm文件。如果vue.vm文件缺失前端代码就永远不会生成。而vue.vm模板里有一行关键代码import { list${className} } from /api/${packageName?replace(com.ruoyi.project., )}这个/api/...路径必须与ruoyi-ui/src/api/目录下的实际文件结构一致。如果packageName是com.ruoyi.project.inspectionclassName是InspectTask那么vue.vm会生成import { listInspectTask } from /api/inspection这就要求ruoyi-ui/src/api/inspection.js文件必须存在。而这个文件正是由generator模块生成的。调试技巧在GenUtils.generatorCode()方法里velocityEngine.mergeTemplate(...)这行打断点F7 进入mergeTemplate观察templateName参数确认它是否是controller.java.vm、vue.vm等。如果templateName是xxx.vm但找不到文件IDEA 会抛出ResourceNotFoundException这就是前端代码不生成的直接原因。5. 前端生成的“隐形战场”Vue 文件里的el-form-item宽度为何总不一致后端代码生成后你兴冲冲地打开ruoyi-ui/src/views/inspection/index.vue准备调试却发现一个扎眼的问题表单里的el-form-item有的 label 宽度是 120px有的却是 80px输入框长度也不统一整个页面看起来像被狗啃过。你翻遍index.vue的代码发现所有el-form-item都写了label-width120px但效果就是不生效。这不是 CSS 错误而是 RuoYi 前端生成器的一个深层设计它生成的 Vue 文件只是一个“半成品”必须经过ruoyi-ui项目的全局样式和组件注册体系才能获得完整渲染能力。5.1el-form-item的宽度逻辑来自ruoyi-ui/src/styles/element-ui.scssRuoYi 的ruoyi-ui项目在src/styles/element-ui.scss中对el-form-item做了全局覆盖.el-form-item { margin-bottom: 15px; .el-form-item__label { font-weight: bold; padding-right: 12px; } .el-form-item__content { line-height: 32px; } }而label-width属性是 Element UI 的原生属性它控制的是.el-form-item__label的width。但 RuoYi 的element-ui.scss里没有设置.el-form-item__label的width而是依赖label-width的传入值。问题在于vue.vm模板生成的el-form-item其label-width是硬编码的el-form-item label任务名称 proptaskName label-width120px el-input v-modelform.taskName placeholder请输入任务名称 / /el-form-item这个120px是写死的但它只对当前el-form-item生效。而 RuoYi 的ruoyi-ui项目还有一个全局的form组件封装src/components/Form/index.vue。这个组件里定义了propsprops: { labelWidth: { type: String, default: 120px } }并且在模板中它把labelWidth透传给了内部的el-formel-form :label-widthlabelWidth ...所以真正的label-width控制权在Form组件的labelWidthprop 上而不是单个el-form-item的label-width属性。vue.vm模板生成的代码忽略了这一层封装直接写了el-form-item导致样式无法继承全局设置。5.2 解决方案修改vue.vm模板拥抱Form组件要让所有el-form-item的宽度一致必须修改ruoyi-generator/src/main/resources/templates/vue.vm模板。找到生成表单的代码块将原来的el-form-item label任务名称 proptaskName label-width120px el-input v-modelform.taskName placeholder请输入任务名称 / /el-form-item替换为Form :label-widthlabelWidth el-form-item label任务名称 proptaskName el-input v-modelform.taskName placeholder请输入任务名称 / /el-form-item /Form同时在vue.vm的script区域顶部添加Form组件的导入import Form from /components/Form并在export default的components选项中注册components: { Form }这样labelWidth就变成了一个响应式变量可以在data()中统一定义data() { return { labelWidth: 120px, form: {...}, rules: {...} } }所有el-form-item的宽度就由这个labelWidth变量统一控制再也不用在每个el-form-item上重复写label-width120px。5.3treeselect下拉框与日期框的宽度对齐CSS 变量的终极方案treeselect是 RuoYi 封装的树形选择器组件el-date-picker是 Element UI 的日期选择器。它们的宽度不一致根源在于treeselect组件的style里写了width: 200px而el-date-picker默认是width: 100%。vue.vm模板生成的代码对treeselect是treeselect v-modelform.deptId :optionsdeptOptions placeholder请选择部门 /对el-date-picker是el-date-picker v-modelform.startTime typedate placeholder请选择开始日期 /treeselect没有style属性el-date-picker也没有。解决方案是在ruoyi-ui/src/styles/element-ui.scss中为所有表单控件设置统一的width.el-form-item__content .el-input__inner, .el-form-item__content .el-textarea__inner, .el-form-item__content .el-select .el-input__inner, .el-form-item__content .el-date-editor .el-input__inner, .el-form-item__content .treeselect .el-input__inner { width: 200px !important; }这里用!important是为了覆盖treeselect组件内部的width: 200px。同时el-date-picker的typedate会渲染成el-date-editor所以选择器必须包含.el-date-editor。实操心得不要在vue.vm模板里给每个控件加stylewidth:200px因为那会污染生成的代码且无法统一维护。RuoYi 的设计哲学是“约定优于配置”所有样式应该在ruoyi-ui的全局样式中定义generator只负责生成符合约定的结构。这才是可持续的维护方式。6. 权限与菜单的闭环为什么生成的模块在后台看不到却能在前端直接访问生成器跑通了后端接口能调前端页面能打开但当你登录 RuoYi 后台管理系统却找不到“设备巡检”这个菜单。你去系统管理 → 菜单管理里手动添加填了菜单名称、路径、组件保存后刷新菜单还是不显示。你打开浏览器开发者工具Network 标签页里getRouters接口返回的路由数组里根本没有inspection这一项。这说明**RuoYi 的菜单系统不是靠“添加菜单”就生效的而是靠“权限标识”与“用户角色”的双向绑定形成
返回列表