# RuoYi-Cloud-Plus 后端开发技能 ## 语言要求 **所有对话、说明、注释均使用中文。** 与用户的交互全程中文,代码注释也使用中文。 ## 简介 本技能指导基于 RuoYi-Cloud-Plus 微服务框架的 Java 后端开发,涵盖代码结构、分层设计、Dubbo 远程调用等核心开发规范。 ## 项目结构 ``` ykt_server/ ├── ruoyi-api/ # API接口定义层 │ ├── ruoyi-api-backstage/ # backstage模块对外暴露的Dubbo接口 │ ├── ruoyi-api-system/ # system模块对外暴露的Dubbo接口 │ └── ... ├── ruoyi-modules/ # 业务模块实现层 │ ├── ruoyi-backstage/ # 后台管理模块 │ ├── ruoyi-ecs/ # ECS电子班牌模块 │ ├── ruoyi-system/ # 系统管理模块 │ └── ... ├── ruoyi-common/ # 公共组件 │ ├── ruoyi-common-core/ # 核心工具类 │ ├── ruoyi-common-mybatis/ # MyBatis-Plus封装 │ ├── ruoyi-common-dubbo/ # Dubbo公共配置 │ └── ... ├── ruoyi-gateway/ # 网关服务 ├── ruoyi-auth/ # 认证中心 └── pom.xml # 父POM ``` ## 核心框架栈 | 组件 | 技术 | 版本 | |------|------|------| | 基础框架 | Spring Boot | 3.1+ | | 微服务框架 | Spring Cloud Alibaba | - | | RPC框架 | Apache Dubbo | 3.X | | ORM框架 | MyBatis-Plus | - | | 权限认证 | Sa-Token + JWT | - | | 注册中心 | Nacos | - | | 数据库连接池 | HikariCP | - | | 主键生成 | 雪花ID | - | ## 分层对象定义规范 ### 1. Domain(实体类) - 位置:`ruoyi-modules/{module}/domain/` - 继承:`TenantEntity`(多租户)或 `BaseEntity` - 注解:`@TableName` 指定表名,`@TableId` 指定主键 - 职责:与数据库表一一对应 ```java @Data @EqualsAndHashCode(callSuper = true) @TableName("t_pt_room") public class PtRoom extends TenantEntity { @TableId(value = "room_id") private Long roomId; private String roomName; // ... } ``` ### 2. BO(业务对象) - 位置:`ruoyi-modules/{module}/domain/bo/` - 继承:`${parentEntity}`(主表为 `TenantEntity`,从表为 `BaseEntity`) - 注解:**必须有** `@AutoMapper(target = Domain.class, reverseConvertGenerate = false)` 配置与Domain的映射(主表、从表都一样) - 职责:用于Service层参数传递,包含校验注解 ```java @Data @EqualsAndHashCode(callSuper = true) @AutoMapper(target = PtRoom.class, reverseConvertGenerate = false) public class PtRoomBo extends TenantEntity { @NotNull(message = "房间Id不能为空", groups = { EditGroup.class }) private Long roomId; @NotBlank(message = "房间名称不能为空", groups = { AddGroup.class, EditGroup.class }) private String roomName; // ... } ``` ### 3. VO(视图对象) - 位置:`ruoyi-modules/{module}/domain/vo/` - 实现:`Serializable` - 注解:**必须有** `@AutoMapper(target = Domain.class)` 配置与Domain的映射(主表、从表都一样) - 职责:用于Controller层返回给前端的数据 ```java @Data @ExcelIgnoreUnannotated @AutoMapper(target = PtRoom.class) public class PtRoomVo implements Serializable { @ExcelProperty(value = "房间名称") private String roomName; @Translation(type = TransConstant.XXX, mapper = "fieldName") private String fieldName; // ... } ``` ### 4. DTO(数据传输对象) - 位置:`ruoyi-api/{api-module}/domain/dto/` - 实现:`Serializable` - 职责:用于跨服务(Dubbo)传输的数据对象 ```java @Data public class RemoteRoomDto implements Serializable { private Long roomId; private String roomName; // ... } ``` ## 模块生成场景 ### 1. 创建模块 **位置**:`ruoyi-modules/ruoyi-{模块名}/` **生成内容**: - Maven 模块目录结构 - `pom.xml`(引用父POM、添加依赖) - 基础包结构(直接在模块包下): ``` src/main/java/org/dromara/{模块名}/ ├── controller/ # 控制器 ├── domain/ # 实体类 │ ├── bo/ # 业务对象 │ └── vo/ # 视图对象 ├── mapper/ # Mapper接口 ├── service/ # Service接口 │ └── impl/ # Service实现 ├── dubbo/ # Dubbo服务实现(可选) └── listener/ # Excel导入监听器(可选) ``` - 如需Dubbo暴露,同步创建 `ruoyi-api/ruoyi-api-{模块名}/` - **重要**:在 `ruoyi-api/ruoyi-api-bom/pom.xml` 的 `` 中添加 API 模块依赖: ```xml org.dromara ruoyi-api-{模块名} ${revision} ``` ### 2. 创建子模块 **位置**:`controller/{子模块名}/` **生成内容**: - 子模块包目录(仅一层,不再嵌套basics等额外目录) ### 3. 创建功能 功能创建有两个路径: **路径1:模块下直接创建功能** - `controller/{功能名}Controller.java` **路径2:子模块下创建功能** - `controller/{子模块名}/{功能名}Controller.java` **生成具体文件**: Domain、Bo、Vo、Mapper、Service、ServiceImpl、Controller (如有Dubbo:RemoteService接口 + RemoteServiceImpl实现) **Controller 层规范**: 1. **请求路径格式**:`/{子模块名}/{功能名}` 或 `/{功能名}` - 示例:`/basicParameter/ptParameter` 2. **权限字符格式**:`{模块名}:{功能名}:{操作名}` - 模块名:对应子模块目录名(如 `basicParameter`) - 功能名:对应功能Controller名(如 `ptParameter`) - 操作名:`list`(列表)、`query`(详情)、`add`(新增)、`edit`(修改)、`remove`(删除)、`export`(导出) - 示例:`basicParameter:ptParameter:add` 3. **标准方法模板**: ```java /** * 查询列表 */ @SaCheckPermission("{模块名}:{功能名}:list") @GetMapping("/list") public TableDataInfo list(Bo bo, PageQuery pageQuery) { return service.queryPageList(bo, pageQuery); } /** * 导出数据 */ @SaCheckPermission("{模块名}:{功能名}:export") @Log(title = "功能标题", businessType = BusinessType.EXPORT) @PostMapping("/export") public void export(Bo bo, HttpServletResponse response) { List list = service.queryList(bo); ExcelUtil.exportExcel(list, "导出标题", Vo.class, response); } /** * 获取详情 */ @SaCheckPermission("{模块名}:{功能名}:query") @GetMapping("/{id}") public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { return R.ok(service.queryById(id)); } /** * 新增数据 */ @SaCheckPermission("{模块名}:{功能名}:add") @Log(title = "功能标题", businessType = BusinessType.INSERT) @RepeatSubmit() @PostMapping() public R add(@Validated(AddGroup.class) @RequestBody Bo bo) { return toAjax(service.insertByBo(bo)); } /** * 修改数据 */ @SaCheckPermission("{模块名}:{功能名}:edit") @Log(title = "功能标题", businessType = BusinessType.UPDATE) @RepeatSubmit() @PutMapping() public R edit(@Validated(EditGroup.class) @RequestBody Bo bo) { return toAjax(service.updateByBo(bo)); } /** * 删除数据 */ @SaCheckPermission("{模块名}:{功能名}:remove") @Log(title = "功能标题", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { return toAjax(service.deleteWithValidByIds(List.of(ids), true)); } /** * 导出导入模板 */ @SaCheckPermission("{模块名}:{功能名}:export") @Log(title = "导入模板", businessType = BusinessType.EXPORT) @PostMapping("/exportTemplate") public void exportTemplate(HttpServletResponse response) { List list = new ArrayList<>(); ExcelUtil.exportExcel(list, "导入模板", ImportVo.class, response); } /** * 导入数据 */ @SaCheckPermission("{模块名}:{功能名}:import") @Log(title = "数据导入", businessType = BusinessType.IMPORT) @PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public R importData(@RequestPart("file") MultipartFile file, boolean updateSupport) throws Exception { ExcelResult result = ExcelUtil.importExcel( file.getInputStream(), ImportVo.class, new ImportListener(updateSupport) ); return R.ok(result.getAnalysis()); } ``` 4. **导入功能相关文件**: - **ImportVo**:导入专用的VO类,使用 `@ExcelProperty` 注解标记Excel列 - **ImportListener**:继承 `AnalysisEventListener` 实现 `ExcelListener` 接口 - 位置:`listener/{功能名}ImportListener.java` - 通过 `SpringUtils.getBean()` 获取Service - 在 `invoke()` 方法中处理每行数据 - 在 `getExcelResult()` 中返回导入结果 ## 开发工作流程 ### 场景1:基础数据模块开发(CRUD) 1. **创建 Domain 实体类** - 继承 `TenantEntity` 或 `BaseEntity` - 使用 `@TableName` 和 `@TableId` 注解 2. **创建 BO 业务对象** - 继承 `${parentEntity}`(主表 `TenantEntity`,从表 `BaseEntity`) - **必须有** `@AutoMapper(target = Domain.class, reverseConvertGenerate = false)` 注解(主表、从表都一样) - 添加校验注解(`@NotNull`, `@NotBlank` 等) 3. **创建 VO 视图对象** - 实现 `Serializable` - **必须有** `@AutoMapper(target = Domain.class)` 注解(主表、从表都一样) - 添加 Excel 导出注解(可选) 4. **创建 Mapper 接口** - 继承 `BaseMapper` 5. **创建 Service 接口和实现** - 接口继承 `IService` - 实现类使用 `@RequiredArgsConstructor` 注入依赖 6. **创建 Controller** - 使用 `@RestController` 和 `@RequestMapping` - 使用 `@SaCheckPermission` 配置权限 - 继承 `BaseController` 获取通用方法 ### 场景2:Dubbo 接口暴露 1. **在 ruoyi-api 模块定义接口** ```java public interface RemoteXxxService { R getById(Long id); TableDataInfo selectPage(RemoteXxxQueryDto query); } ``` 2. **创建 DTO 和 QueryDTO** - DTO 用于数据传输 - QueryDTO 用于查询条件(包含分页参数) 3. **在 ruoyi-modules 模块实现接口** ```java @DubboService public class RemoteXxxServiceImpl implements RemoteXxxService { // 实现方法 } ``` 4. **注册 Dubbo 服务** - 实现类添加 `@DubboService` 注解 - 框架会自动暴露服务 ### 场景3:跨服务调用 Dubbo 接口 1. **在 Consumer 端注入远程服务** ```java @DubboReference private RemoteXxxService remoteXxxService; ``` 2. **直接调用远程方法** ```java R result = remoteXxxService.getById(id); ``` ## 关键转换模式 ### DTO → BO 转换(Dubbo接口实现层) ```java @Override public TableDataInfo selectTraineePage(RemoteTraineeQueryDto queryDto) { // 1. DTO → BO(手动设置查询条件) PtUserAccountBo bo = new PtUserAccountBo(); bo.setCategory(TRAINEE_CATEGORY); if (queryDto.getRealName() != null) { bo.setRealName(queryDto.getRealName()); } // 2. 构建分页参数 PageQuery pageQuery = new PageQuery(); pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1); pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10); // 3. 调用本地Service TableDataInfo result = userAccountService.queryPageList(bo, pageQuery); // 4. VO → DTO 转换 List dtoList = result.getRows().stream() .map(this::convertToTraineeDto) .toList(); // 5. 构建返回结果 TableDataInfo pageData = TableDataInfo.build(); pageData.setRows(dtoList); pageData.setTotal(result.getTotal()); return pageData; } private RemoteTraineeDto convertToTraineeDto(PtUserAccountVo vo) { if (vo == null) return null; RemoteTraineeDto dto = new RemoteTraineeDto(); dto.setUserId(vo.getUserId()); dto.setRealName(vo.getRealName()); // 手动映射字段... return dto; } ``` ### VO → DTO 转换(使用 MapstructUtils) ```java // 简单对象转换 RemoteUserAccountVo vo = userAccountService.queryById(userId); RemoteUserAccountVo result = MapstructUtils.convert(vo, RemoteUserAccountVo.class); // 列表转换 List voList = userAccountService.queryList(bo); return MapstructUtils.convert(voList, RemoteUserAccountVo.class); ``` ### Bean 拷贝(使用 Hutool) ```java // BO → Domain 转换(在Service层) PtUserAccountBo ptUserAccountBo = BeanUtil.copyProperties(bo, PtUserAccountBo.class); ``` ## 分页处理规范 ### 分页查询参数 ```java @Data public class RemoteXxxQueryDto implements Serializable { private Integer pageNum = 1; // 页码,默认1 private Integer pageSize = 10; // 每页条数,默认10 private String keyword; // 查询关键字 // ... 其他查询条件 } ``` ### 分页查询实现 ```java @Override public TableDataInfo selectPage(RemoteXxxQueryDto queryDto) { // 1. 构建分页参数 PageQuery pageQuery = new PageQuery(); pageQuery.setPageNum(queryDto.getPageNum() != null ? Math.max(queryDto.getPageNum(), 1) : 1); pageQuery.setPageSize(queryDto.getPageSize() != null ? Math.min(Math.max(queryDto.getPageSize(), 1), 500) : 10); // 2. 构建查询条件 XxxBo bo = new XxxBo(); // 设置查询条件... // 3. 执行查询 TableDataInfo result = xxxService.queryPageList(bo, pageQuery); // 4. 转换并返回 List dtoList = MapstructUtils.convert(result.getRows(), RemoteXxxDto.class); TableDataInfo pageData = TableDataInfo.build(); pageData.setRows(dtoList); pageData.setTotal(result.getTotal()); return pageData; } ``` ## 常用注解速查 ### 实体类注解 | 注解 | 用途 | 位置 | |------|------|------| | `@TableName` | 指定数据库表名 | Domain | | `@TableId` | 指定主键字段 | Domain | | `@TableLogic` | 逻辑删除标记 | Domain | | `@AutoMapper` | 配置对象映射(Bo/Vo **都必须有**) | BO/VO | | `@ExcelProperty` | Excel导出配置 | VO | | `@Translation` | 数据翻译(字典/关联) | VO | ### 校验注解 | 注解 | 用途 | |------|------| | `@NotNull` | 非空校验(允许空字符串) | | `@NotBlank` | 非空白校验(不允许null、空字符串、空白字符) | | `@NotEmpty` | 非空校验(用于集合、字符串) | | `@Size` | 长度/大小范围校验 | | `@Pattern` | 正则表达式校验 | ### Dubbo 注解 | 注解 | 用途 | |------|------| | `@DubboService` | 暴露 Dubbo 服务 | | `@DubboReference` | 引用 Dubbo 服务 | ### 权限注解 | 注解 | 用途 | |------|------| | `@SaCheckPermission` | 检查权限标识 | | `@SaCheckRole` | 检查角色 | | `@SaCheckLogin` | 检查登录状态 | ## 代码模板 技能目录下 `templates/` 文件夹包含以下代码模板: - `Domain.java.template` - 实体类模板 - `Bo.java.template` - 业务对象模板 - `Vo.java.template` - 视图对象模板 - `Dto.java.template` - 数据传输对象模板 - `ImportVo.java.template` - 导入专用VO模板 - `Service.java.template` - Service接口模板 - `ServiceImpl.java.template` - Service实现模板 - `Controller.java.template` - 控制器模板 - `Mapper.java.template` - Mapper接口模板 - `RemoteService.java.template` - Dubbo接口模板 - `RemoteServiceImpl.java.template` - Dubbo实现模板 - `ImportListener.java.template` - Excel导入监听器模板 ### 模板变量说明 | 变量 | 说明 | 示例 | |------|------|------| | `${module}` | 模块名(小写) | `ecs` | | `${submodule}` | 子模块包名 | `term` | | `${ClassName}` | 类名(首字母大写) | `EcsTerm` | | `${tableComment}` | 表注释 | `设备信息` | | `${pkField}` | 主键字段名(驼峰) | `termId` | | `${pkType}` | 主键类型 | `Long` | | `${PkCapField}` | 主键字段名(首字母大写,用于getter/setter) | `TermId` | | `${author}` | 作者 | `ruoyi` | | `${date}` | 日期 | `2026-04-20` | | `${parentEntity}` | 父实体类 | `TenantEntity` / `BaseEntity` | | `${queryWrapperConditions}` | 查询条件代码块(由生成器根据字段自动生成) | 见下方说明 | ### ServiceImpl 查询规范(重要) ServiceImpl **必须**使用 `LambdaQueryWrapper` 模式,**禁止**直接传递 Bo 对象给 Mapper。 ```java // ❌ 错误写法(BaseMapperPlus 没有此方法签名) return baseMapper.selectVoList(bo); return baseMapper.selectVoPageList(bo, pageQuery); // ✅ 正确写法 LambdaQueryWrapper lqw = buildQueryWrapper(bo); return baseMapper.selectVoList(lqw); // 列表查询 Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); // 分页查询 return TableDataInfo.build(result); ``` **buildQueryWrapper 生成规则**: - String 类型字段 → `lqw.like(StringUtils.isNotBlank(bo.getXxx()), Entity::getXxx, bo.getXxx())` - 字典字段(dictType,String 类型) → `lqw.eq(StringUtils.isNotBlank(bo.getXxx()), Entity::getXxx, bo.getXxx())` - Integer/Long 类型字段(非字典的精确匹配) → `lqw.eq(bo.getXxx() != null, Entity::getXxx, bo.getXxx())` - 日期范围字段 → `lqw.ge(bo.getBeginTime() != null, Entity::getXxx, bo.getBeginTime())` + `lqw.le(...)` - 分页查询默认按 `createTime` 降序排列:`lqw.orderByDesc(Entity::getCreateTime)` **insertByBo 规范**: - 转换后需调用 `validEntityBeforeSave(Objects.requireNonNull(add))` 进行保存前校验 - 插入成功后回填主键:`bo.set${PkCapField}(add.get${PkCapField}())` **updateByBo 规范**: - 转换后需 null 检查 + 调用 `validEntityBeforeSave` **deleteWithValidByIds 规范**: - 当 `isValid=true` 时预留校验逻辑位置 ## 注意事项 1. **对象转换**:DTO/VO/BO 之间的转换优先使用 `MapstructUtils`,复杂场景手动转换 2. **分页参数**:pageSize 需要限制最大值(通常500),防止内存溢出 3. **校验分组**:新增和编辑使用不同的校验分组(`AddGroup`, `EditGroup`) 4. **Dubbo 调用**:远程接口返回类型建议使用 `R` 包装,便于错误处理 5. **租户隔离**:多租户表必须继承 `TenantEntity`,单表继承 `BaseEntity` 6. **字典命名**:字典类型(dictType)是全局的,命名不加模块前缀。例如用 `course_type` 而非 `ecs_course_type`,用 `data_source` 而非 `ecs_data_source` 7. **字典字段类型**:数据字典(dictType)对应的数据库表字段类型为 `varchar(16)`,Java 实体/Bo/Vo 中对应类型为 `String`(而非 `Integer`)。字典值虽然常为数字,但存储和传输统一使用字符串。建表语句中字典字段用 `character varying(16 char)` 或 `varchar(16)` 8. **主键不导出**:主键字段(Long 类型)不加 `@ExcelProperty` 注解,不参与 Excel 导出。Vo 中主键仅作为标识,不需要在导出文件中展示 --- ## 附录:从需求规格说明书生成代码 > 本章节定义基于需求规格说明书自动生成后端代码的完整流程。由 `/dev` 指令触发。 ### A.1 输入文件 需求规格说明书位于:`d:/dt_ykt/ykt_server/doc/{模块}-{功能名}-requirements.md` 文件不存在时,提示用户先执行 `/require {模块} {功能名}`。 ### A.2 读取与解析 依次读取需求规格说明书的以下章节: | 章节 | 用途 | |------|------| | 基础信息 | 确定包路径、表名、多租户标记 | | 字段清单 | 生成 Domain/Bo/Vo 各字段 | | VO 结构 | 生成列表VO、明细VO的类型定义 | | 接口清单 | 生成 Controller 方法 | | 特殊需求 | 判断是否生成 ImportVo/Listener、Dubbo接口 | | 字典项 | 生成 VO 的 `@ExcelDictFormat` 注解 | ### A.3 后端文件生成清单 根据需求文档内容,按需生成以下文件: #### A.3.1 主表文件(必有) | 文件 | 路径 | 说明 | |------|------|------| | Domain | `domain/{Name}.java` | 表名=基础信息.主表名,字段=字段清单中 inDb=true | | Bo | `domain/bo/{Name}Bo.java` | 字段=字段清单中 inForm=true,含校验注解 | | Vo | `domain/vo/{Name}Vo.java` | 字段=字段清单中 inTable/inForm=true,含Excel注解 | | Mapper | `mapper/{Name}Mapper.java` | 继承 `BaseMapper` | | Service | `service/I{Name}Service.java` | 继承 `IService` | | ServiceImpl | `service/impl/{Name}ServiceImpl.java` | 实现 Service,含 CRUD + 导入导出逻辑 | | Controller | `controller/{Name}Controller.java` | REST API,含权限注解 | #### A.3.2 从表文件(如有 relation 字段) | 文件 | 路径 | 说明 | |------|------|------| | Domain(从表) | `domain/{SubName}.java` | 表名=基础信息.从表名 | | Bo(从表) | `domain/bo/{SubName}Bo.java` | **必须有** `@AutoMapper(target = {SubName}.class, reverseConvertGenerate = false)` + 主表ID外键字段,继承 `BaseEntity` | | Vo(从表) | `domain/vo/{SubName}Vo.java` | **必须有** `@AutoMapper(target = {SubName}.class)` + 主表ID、关联字段 | | Mapper(从表) | `mapper/{SubName}Mapper.java` | — | | Service(从表) | `service/I{SubName}Service.java` | 含批量插入方法 | | ServiceImpl(从表) | `service/impl/{SubName}ServiceImpl.java` | — | #### A.3.3 导入导出文件(特殊需求.导入=true) | 文件 | 路径 | 说明 | |------|------|------| | ImportVo | `domain/vo/{Name}ImportVo.java` | 导入专用VO,含 `@ExcelProperty` | | ImportListener | `listener/{Name}ImportListener.java` | 继承 `AnalysisEventListener` | #### A.3.4 Dubbo 接口文件(特殊需求.Dubbo暴露=true) | 文件 | 路径 | 说明 | |------|------|------| | RemoteService | `dubbo/Remote{Name}Service.java` | Dubbo 接口定义 | | RemoteDto | `dto/Remote{Name}Dto.java` | Dubbo 传输对象 | | RemoteServiceImpl | `dubbo/Remote{Name}ServiceImpl.java` | `@DubboService` 实现 | ### A.4 代码生成模板 使用技能目录下 `templates/` 中的模板文件填充: **主表:** ``` templates/ ├── Domain.java.template → domain/{Name}.java ├── Bo.java.template → domain/bo/{Name}Bo.java ├── Vo.java.template → domain/vo/{Name}Vo.java ├── Mapper.java.template → mapper/{Name}Mapper.java ├── Service.java.template → service/I{Name}Service.java ├── ServiceImpl.java.template → service/impl/{Name}ServiceImpl.java ├── Controller.java.template → controller/{Name}Controller.java ├── ImportVo.java.template → domain/vo/{Name}ImportVo.java ├── ImportListener.java.template → listener/{Name}ImportListener.java ├── RemoteService.java.template → dubbo/Remote{Name}Service.java ├── RemoteServiceImpl.java.template → dubbo/Remote{Name}ServiceImpl.java └── Dto.java.template → dto/Remote{Name}Dto.java ``` **从表(复用主表模板,替换 {Name} → {SubName}):** > ⚠️ 从表模板与主表使用同一套模板,关键差异: > - `${parentEntity}` 变量:主表为 `TenantEntity`,从表为 `BaseEntity` > - 从表 Bo **必须**包含 `@AutoMapper(target = {SubName}.class, reverseConvertGenerate = false)` 注解 > - 从表 Vo **必须**包含 `@AutoMapper(target = {SubName}.class)` 注解 > - **不管主表还是从表,Bo 和 Vo 都必须有 `@AutoMapper` 映射注解,这是框架对象转换(MapstructUtils.convert)的前提** ``` Domain.java.template → domain/{SubName}.java Bo.java.template → domain/bo/{SubName}Bo.java Vo.java.template → domain/vo/{SubName}Vo.java ← 必须!含 @AutoMapper Mapper.java.template → mapper/{SubName}Mapper.java Service.java.template → service/I{SubName}Service.java └── ServiceImpl.java.template → service/impl/{SubName}ServiceImpl.java ``` ### A.5 字段到代码属性的映射 | 需求字段属性 | Domain 字段 | Bo 校验注解 | Vo Excel注解 | |-------------|------------|------------|-------------| | fieldType=Long(主键) | `private Long xxxId;` | `@NotNull(groups=EditGroup.class)` | **不导出**(不加 @ExcelProperty) | | fieldType=String, required=true | `private String xxxName;` | `@NotBlank` | `@ExcelProperty("名称")` | | fieldType=Integer, dictType=xxx | `private Integer xxxType;` | — | `@ExcelProperty("类型"), @ExcelDictFormat(dictType="xxx")` | | **dictType=xxx(字典字段)** | **`private String xxxType;`** | — | **`@ExcelProperty("类型"), @ExcelDictFormat(dictType="xxx")`** | | fieldType=BigDecimal | `private BigDecimal xxxAmount;` | — | `@ExcelProperty("金额")` | | fieldType=Date | `private Date xxxTime;` | — | `@ExcelProperty("时间")` | | inDb=false | —(不出现在 Domain) | — | `private String teacherNames;`(纯计算字段) | | inTable=false, inForm=true | —(不出现在 Domain) | — | `private String remark;`(仅表单) | | **从表关联字段(Bo/Vo)** | **`private List<{SubName}Vo> {Name}{Sub}List;`** | — | — | ### A.6 多对多关联处理 主表 Domain 中不直接存储讲师ID,而是: 1. 从表 `{Name}Teacher.java` 存储 `{xxxId}`、`{teacherId}`、`{teacherName}` 2. 主表列表查询时,通过 SQL `GROUP_CONCAT` 拼接 `{teacherNames}`,返回到 `Vo` 的 `inDb=false` 字段 3. 列表页面通过 teacherNames 直接展示讲师列表 ### A.7 输出路径约定 | 资源 | 路径 | |------|------| | 后端项目根 | `d:/dt_ykt/ykt_server`(可通过 `--backend=` 参数覆盖) | | 主表代码 | `d:/dt_ykt/ykt_server/ruoyi-modules/ruoyi-{模块}/src/main/java/org/dromara/{模块}/` | | Dubbo API | `d:/dt_ykt/ykt_server/ruoyi-api/ruoyi-api-{模块}/src/main/java/org/dromara/{模块}/api/` | ### A.8 权限字符生成规则 格式:`{子模块名(如basics)}:{功能名}:{操作名}` 示例:对于 `ruoyi-backstage/basics/room/CourseController.java` → `basics:course:list` ### A.9 注意事项 1. **覆盖确认**:文件已存在时,提示用户确认是否覆盖 2. **子模块路径**:优先使用 `controller/{子模块}/{功能名}Controller.java` 3. **多租户**:多租户表继承 `TenantEntity`,单表继承 `BaseEntity` 4. **包名大小写**:类名首字母大写,包路径全小写