|
|
@@ -0,0 +1,720 @@
|
|
|
+# 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` 的 `<dependencyManagement>` 中添加 API 模块依赖:
|
|
|
+ ```xml
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.dromara</groupId>
|
|
|
+ <artifactId>ruoyi-api-{模块名}</artifactId>
|
|
|
+ <version>${revision}</version>
|
|
|
+ </dependency>
|
|
|
+ ```
|
|
|
+
|
|
|
+### 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<Vo> 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<Vo> list = service.queryList(bo);
|
|
|
+ ExcelUtil.exportExcel(list, "导出标题", Vo.class, response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取详情
|
|
|
+ */
|
|
|
+ @SaCheckPermission("{模块名}:{功能名}:query")
|
|
|
+ @GetMapping("/{id}")
|
|
|
+ public R<Vo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
|
|
|
+ return R.ok(service.queryById(id));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 新增数据
|
|
|
+ */
|
|
|
+ @SaCheckPermission("{模块名}:{功能名}:add")
|
|
|
+ @Log(title = "功能标题", businessType = BusinessType.INSERT)
|
|
|
+ @RepeatSubmit()
|
|
|
+ @PostMapping()
|
|
|
+ public R<Void> add(@Validated(AddGroup.class) @RequestBody Bo bo) {
|
|
|
+ return toAjax(service.insertByBo(bo));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 修改数据
|
|
|
+ */
|
|
|
+ @SaCheckPermission("{模块名}:{功能名}:edit")
|
|
|
+ @Log(title = "功能标题", businessType = BusinessType.UPDATE)
|
|
|
+ @RepeatSubmit()
|
|
|
+ @PutMapping()
|
|
|
+ public R<Void> edit(@Validated(EditGroup.class) @RequestBody Bo bo) {
|
|
|
+ return toAjax(service.updateByBo(bo));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除数据
|
|
|
+ */
|
|
|
+ @SaCheckPermission("{模块名}:{功能名}:remove")
|
|
|
+ @Log(title = "功能标题", businessType = BusinessType.DELETE)
|
|
|
+ @DeleteMapping("/{ids}")
|
|
|
+ public R<Void> 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<ImportVo> 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<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport) throws Exception {
|
|
|
+ ExcelResult<ImportVo> result = ExcelUtil.importExcel(
|
|
|
+ file.getInputStream(),
|
|
|
+ ImportVo.class,
|
|
|
+ new ImportListener(updateSupport)
|
|
|
+ );
|
|
|
+ return R.ok(result.getAnalysis());
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+4. **导入功能相关文件**:
|
|
|
+ - **ImportVo**:导入专用的VO类,使用 `@ExcelProperty` 注解标记Excel列
|
|
|
+ - **ImportListener**:继承 `AnalysisEventListener<ImportVo>` 实现 `ExcelListener<ImportVo>` 接口
|
|
|
+ - 位置:`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<Domain>`
|
|
|
+
|
|
|
+5. **创建 Service 接口和实现**
|
|
|
+ - 接口继承 `IService<Domain>`
|
|
|
+ - 实现类使用 `@RequiredArgsConstructor` 注入依赖
|
|
|
+
|
|
|
+6. **创建 Controller**
|
|
|
+ - 使用 `@RestController` 和 `@RequestMapping`
|
|
|
+ - 使用 `@SaCheckPermission` 配置权限
|
|
|
+ - 继承 `BaseController` 获取通用方法
|
|
|
+
|
|
|
+### 场景2:Dubbo 接口暴露
|
|
|
+
|
|
|
+1. **在 ruoyi-api 模块定义接口**
|
|
|
+ ```java
|
|
|
+ public interface RemoteXxxService {
|
|
|
+ R<RemoteXxxDto> getById(Long id);
|
|
|
+ TableDataInfo<RemoteXxxDto> 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<RemoteXxxDto> result = remoteXxxService.getById(id);
|
|
|
+ ```
|
|
|
+
|
|
|
+## 关键转换模式
|
|
|
+
|
|
|
+### DTO → BO 转换(Dubbo接口实现层)
|
|
|
+
|
|
|
+```java
|
|
|
+@Override
|
|
|
+public TableDataInfo<RemoteTraineeDto> 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<PtUserAccountVo> result = userAccountService.queryPageList(bo, pageQuery);
|
|
|
+
|
|
|
+ // 4. VO → DTO 转换
|
|
|
+ List<RemoteTraineeDto> dtoList = result.getRows().stream()
|
|
|
+ .map(this::convertToTraineeDto)
|
|
|
+ .toList();
|
|
|
+
|
|
|
+ // 5. 构建返回结果
|
|
|
+ TableDataInfo<RemoteTraineeDto> 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<RemoteUserAccountVo> 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<RemoteXxxDto> 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<XxxVo> result = xxxService.queryPageList(bo, pageQuery);
|
|
|
+
|
|
|
+ // 4. 转换并返回
|
|
|
+ List<RemoteXxxDto> dtoList = MapstructUtils.convert(result.getRows(), RemoteXxxDto.class);
|
|
|
+ TableDataInfo<RemoteXxxDto> 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<EcsTerm> lqw = buildQueryWrapper(bo);
|
|
|
+return baseMapper.selectVoList(lqw); // 列表查询
|
|
|
+Page<EcsTermVo> 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<T>` 包装,便于错误处理
|
|
|
+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<Domain>` |
|
|
|
+| Service | `service/I{Name}Service.java` | 继承 `IService<Domain>` |
|
|
|
+| 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<ImportVo>` |
|
|
|
+
|
|
|
+#### 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. **包名大小写**:类名首字母大写,包路径全小写
|