所有对话、说明、注释均使用中文。 与用户的交互全程中文,代码注释也使用中文。
本技能指导基于 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 | - |
ruoyi-modules/{module}/domain/TenantEntity(多租户)或 BaseEntity@TableName 指定表名,@TableId 指定主键职责:与数据库表一一对应
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_pt_room")
public class PtRoom extends TenantEntity {
@TableId(value = "room_id")
private Long roomId;
private String roomName;
// ...
}
ruoyi-modules/{module}/domain/bo/${parentEntity}(主表为 TenantEntity,从表为 BaseEntity)@AutoMapper(target = Domain.class, reverseConvertGenerate = false) 配置与Domain的映射(主表、从表都一样)职责:用于Service层参数传递,包含校验注解
@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;
// ...
}
ruoyi-modules/{module}/domain/vo/Serializable@AutoMapper(target = Domain.class) 配置与Domain的映射(主表、从表都一样)职责:用于Controller层返回给前端的数据
@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;
// ...
}
ruoyi-api/{api-module}/domain/dto/Serializable职责:用于跨服务(Dubbo)传输的数据对象
@Data
public class RemoteRoomDto implements Serializable {
private Long roomId;
private String roomName;
// ...
}
位置:ruoyi-modules/ruoyi-{模块名}/
生成内容:
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 模块依赖:
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api-{模块名}</artifactId>
<version>${revision}</version>
</dependency>
位置:controller/{子模块名}/
生成内容:
功能创建有两个路径:
路径1:模块下直接创建功能
controller/{功能名}Controller.java路径2:子模块下创建功能
controller/{子模块名}/{功能名}Controller.java生成具体文件: Domain、Bo、Vo、Mapper、Service、ServiceImpl、Controller (如有Dubbo:RemoteService接口 + RemoteServiceImpl实现)
Controller 层规范:
请求路径格式:/{子模块名}/{功能名} 或 /{功能名}
/basicParameter/ptParameter权限字符格式:{模块名}:{功能名}:{操作名}
basicParameter)ptParameter)list(列表)、query(详情)、add(新增)、edit(修改)、remove(删除)、export(导出)basicParameter:ptParameter:add标准方法模板:
/**
* 查询列表
*/
@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());
}
导入功能相关文件:
@ExcelProperty 注解标记Excel列AnalysisEventListener<ImportVo> 实现 ExcelListener<ImportVo> 接口
listener/{功能名}ImportListener.javaSpringUtils.getBean() 获取Serviceinvoke() 方法中处理每行数据getExcelResult() 中返回导入结果创建 Domain 实体类
TenantEntity 或 BaseEntity@TableName 和 @TableId 注解创建 BO 业务对象
${parentEntity}(主表 TenantEntity,从表 BaseEntity)@AutoMapper(target = Domain.class, reverseConvertGenerate = false) 注解(主表、从表都一样)@NotNull, @NotBlank 等)创建 VO 视图对象
Serializable@AutoMapper(target = Domain.class) 注解(主表、从表都一样)创建 Mapper 接口
BaseMapper<Domain>创建 Service 接口和实现
IService<Domain>@RequiredArgsConstructor 注入依赖创建 Controller
@RestController 和 @RequestMapping@SaCheckPermission 配置权限BaseController 获取通用方法在 ruoyi-api 模块定义接口
public interface RemoteXxxService {
R<RemoteXxxDto> getById(Long id);
TableDataInfo<RemoteXxxDto> selectPage(RemoteXxxQueryDto query);
}
创建 DTO 和 QueryDTO
在 ruoyi-modules 模块实现接口
@DubboService
public class RemoteXxxServiceImpl implements RemoteXxxService {
// 实现方法
}
注册 Dubbo 服务
@DubboService 注解在 Consumer 端注入远程服务
@DubboReference
private RemoteXxxService remoteXxxService;
直接调用远程方法
R<RemoteXxxDto> result = remoteXxxService.getById(id);
@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;
}
// 简单对象转换
RemoteUserAccountVo vo = userAccountService.queryById(userId);
RemoteUserAccountVo result = MapstructUtils.convert(vo, RemoteUserAccountVo.class);
// 列表转换
List<RemoteUserAccountVo> voList = userAccountService.queryList(bo);
return MapstructUtils.convert(voList, RemoteUserAccountVo.class);
// BO → Domain 转换(在Service层)
PtUserAccountBo ptUserAccountBo = BeanUtil.copyProperties(bo, PtUserAccountBo.class);
@Data
public class RemoteXxxQueryDto implements Serializable {
private Integer pageNum = 1; // 页码,默认1
private Integer pageSize = 10; // 每页条数,默认10
private String keyword; // 查询关键字
// ... 其他查询条件
}
@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 |
正则表达式校验 |
| 注解 | 用途 |
|---|---|
@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 必须使用 LambdaQueryWrapper 模式,禁止直接传递 Bo 对象给 Mapper。
// ❌ 错误写法(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 生成规则:
lqw.like(StringUtils.isNotBlank(bo.getXxx()), Entity::getXxx, bo.getXxx())lqw.eq(StringUtils.isNotBlank(bo.getXxx()), Entity::getXxx, bo.getXxx())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 规范:
validEntityBeforeSavedeleteWithValidByIds 规范:
isValid=true 时预留校验逻辑位置MapstructUtils,复杂场景手动转换AddGroup, EditGroup)R<T> 包装,便于错误处理TenantEntity,单表继承 BaseEntitycourse_type 而非 ecs_course_type,用 data_source 而非 ecs_data_sourcevarchar(16),Java 实体/Bo/Vo 中对应类型为 String(而非 Integer)。字典值虽然常为数字,但存储和传输统一使用字符串。建表语句中字典字段用 character varying(16 char) 或 varchar(16)@ExcelProperty 注解,不参与 Excel 导出。Vo 中主键仅作为标识,不需要在导出文件中展示本章节定义基于需求规格说明书自动生成后端代码的完整流程。由
/dev指令触发。
需求规格说明书位于:d:/dt_ykt/ykt_server/doc/{模块}-{功能名}-requirements.md
文件不存在时,提示用户先执行 /require {模块} {功能名}。
依次读取需求规格说明书的以下章节:
| 章节 | 用途 |
|---|---|
| 基础信息 | 确定包路径、表名、多租户标记 |
| 字段清单 | 生成 Domain/Bo/Vo 各字段 |
| VO 结构 | 生成列表VO、明细VO的类型定义 |
| 接口清单 | 生成 Controller 方法 |
| 特殊需求 | 判断是否生成 ImportVo/Listener、Dubbo接口 |
| 字典项 | 生成 VO 的 @ExcelDictFormat 注解 |
根据需求文档内容,按需生成以下文件:
| 文件 | 路径 | 说明 |
|---|---|---|
| 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,含权限注解 |
| 文件 | 路径 | 说明 |
|---|---|---|
| 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 |
— |
| 文件 | 路径 | 说明 |
|---|---|---|
| ImportVo | domain/vo/{Name}ImportVo.java |
导入专用VO,含 @ExcelProperty |
| ImportListener | listener/{Name}ImportListener.java |
继承 AnalysisEventListener<ImportVo> |
| 文件 | 路径 | 说明 |
|---|---|---|
| RemoteService | dubbo/Remote{Name}Service.java |
Dubbo 接口定义 |
| RemoteDto | dto/Remote{Name}Dto.java |
Dubbo 传输对象 |
| RemoteServiceImpl | dubbo/Remote{Name}ServiceImpl.java |
@DubboService 实现 |
使用技能目录下 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
| 需求字段属性 | 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; |
— | — |
主表 Domain 中不直接存储讲师ID,而是:
{Name}Teacher.java 存储 {xxxId}、{teacherId}、{teacherName}GROUP_CONCAT 拼接 {teacherNames},返回到 Vo 的 inDb=false 字段| 资源 | 路径 |
|---|---|
| 后端项目根 | 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/ |
格式:{子模块名(如basics)}:{功能名}:{操作名}
示例:对于 ruoyi-backstage/basics/room/CourseController.java → basics:course:list
controller/{子模块}/{功能名}Controller.javaTenantEntity,单表继承 BaseEntity