VO 对象转换、数据翻译
这篇解决什么
接口出入参用 VO / DTO,表映射用 DO,中间要做对象拷贝,还常要把 userId、deptId 翻译成姓名。读完能改 convert 包、BeanUtils,以及 @Trans / @DictFormat 的导出字段。
这里的 VO 泛指 POJO,也包括 DTO、BO。管理端前缀仍是 /admin-api。

示意图:ReqVO / RespVO 与 DO 分开,中间用 Convert 或 BeanUtils。
对象分层
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| ReqVO / RespVO | 管理端入参、出参,挂校验和 Swagger | 例如 UserRespVO |
| DTO | 模块间 RPC 契约 | yudao-module-xxx-api |
| DO | 映表,不对外当接口 IO | 例如 AdminUserDO |
| Convert | MapStruct 接口,复杂拼接放 default 方法 | yudao-module-xxx-server/.../convert/ |
不要用 DO 当接口出入参
创建用户只该收账号、昵称、部门。用 DO 会把 createTime、creator 也暴露给入参。校验和文档挂在 VO 上,DO 只负责映表。
对象转换
同名字段拷贝即可时,用 BeanUtils。字段要忽略、改名或表达式时,用 MapStruct。还要拼部门名、角色名时,放 Convert 的 default 方法,或给 toBean 传 Consumer。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| MapStruct | 编译期生成映射;接口上标 @Mapper | 例如 UserConvert |
BeanUtils | 包一层 Hutool,支持 List、PageResult、Consumer | ruoyi-office/yudao-framework/yudao-common/.../BeanUtils.java |
相对一次数据库查询,两种拷贝的耗时都可以忽略。多数 Controller 直接 BeanUtils.toBean。
MapStruct
每个 yudao-module-xxx-server 的 convert 包放业务 Convert。UserConvert 在 ruoyi-office/yudao-module-system/yudao-module-system-server/:
@Mapper
public interface UserConvert {
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
default UserRespVO convert(AdminUserDO user, DeptDO dept) {
UserRespVO userVO = BeanUtils.toBean(user, UserRespVO.class);
if (dept != null) {
userVO.setDeptName(dept.getName());
}
return userVO;
}
}UserController 查完用户和部门后调用 UserConvert.INSTANCE.convertList(...),把角色、岗位一并回填。字段映射要 ignore 或表达式时,看 PayChannelConvert 的 @Mapping。
BeanUtils
封装在 cn.iocoder.yudao.framework.common.util.object.BeanUtils。只改这一处,就能换底层实现。
public static <T> T toBean(Object source, Class<T> targetClass) {
return BeanUtil.toBean(source, targetClass);
}
public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType) {
return toBean(source, targetType, null);
}TenantController 分页直接转 VO:
@GetMapping("/page")
public CommonResult<PageResult<TenantRespVO>> getTenantPage(@Valid TenantPageReqVO pageVO) {
PageResult<TenantDO> pageResult = tenantService.getTenantPage(pageVO);
return success(BeanUtils.toBean(pageResult, TenantRespVO.class));
}MapStruct 还是 BeanUtils
同名字段、列表和分页:用 BeanUtils。字段名对不上、要丢掉敏感配置:用 MapStruct @Mapping。两种可以同时出现在一个 Convert 里。
复杂拼接
toBean 第三个参数是 Consumer,拷贝完再补翻译字段。CRM 客户列表先批量查用户和部门,再回填:
return BeanUtils.toBean(list, CrmCustomerRespVO.class, customerVO -> {
MapUtils.findAndThen(userMap, customerVO.getOwnerUserId(), user -> {
customerVO.setOwnerUserName(user.getNickname());
});
});这段在 yudao-module-crm-server 的 CrmCustomerController。逻辑再长,就挪到对应 Convert,避免 Controller 膨胀。
数据翻译
把 A 对象的编号,写成 B 对象上的名称。userId → AdminUserDO.nickname → userName。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 手工拼接 | 单表查出 Map,再 setXxxName | 见上一节 UserConvert、Consumer |
| 联表 SQL | 一条 SQL 带出名称 | 见 MyBatis |
| easy-trans | VO 标 @Trans,接口标 @TransMethodResult | 操作日志、CRM 产品 |
列表接口多数走手工拼接:SQL 简单,压力小,后续改字段也只动 Java。注解翻译适合「一个编号对应一张本模块表」或「走 *-api 查用户」。

示意图:模块内查本模块 DO;跨模块走 AdminUserApi。
模块内翻译
OperateLogRespVO 实现 org.dromara.core.trans.vo.VO,用 userId 读本模块 AdminUserDO:
@Trans(type = TransType.SIMPLE, target = AdminUserDO.class, fields = "nickname", ref = "userName")
private Long userId;
private String userName;| 名称 | 说明 | 仓库路径 |
|---|---|---|
type | TransType.SIMPLE,走 MyBatis Plus | OperateLogRespVO |
target | 目标 DO | AdminUserDO.class |
fields / ref | 读哪个字段、写到 VO 哪一列 | nickname → userName |
对应 Controller 必须加 @TransMethodResult,否则注解不生效:
@RestController
@RequestMapping("/system/operate-log")
public class OperateLogController {
@GetMapping("/page")
@TransMethodResult
public CommonResult<PageResult<OperateLogRespVO>> pageOperateLog(@Valid OperateLogPageReqVO pageReqVO) {
PageResult<OperateLogDO> pageResult = operateLogService.getOperateLogPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, OperateLogRespVO.class));
}
}@Trans 属性说明见 easy-trans Trans 注解。
跨模块翻译
别的模块不能直接碰 AdminUserDO。AdminUserApi 做成翻译器,CRM 产品只依赖 system-api:
@AutoTrans(namespace = PREFIX, fields = {"nickname"})
public interface AdminUserApi extends AutoTransable<AdminUserRespDTO> {
String PREFIX = ApiConstants.PREFIX + "/user";
@Override
@FeignIgnore
default AdminUserRespDTO selectById(Object id) {
return getUser(Convert.toLong(id)).getCheckedData();
}
}CrmProductRespVO 用 AUTO_TRANS,key 必须等于上面的 namespace:
@Trans(type = TransType.AUTO_TRANS, key = AdminUserApi.PREFIX,
fields = "nickname", ref = "ownerUserName")
private Long ownerUserId;
private String ownerUserName;CrmProductController 的 /get、/page 同样加 @TransMethodResult。selectById / selectByIds 要 @FeignIgnore,避免被当成 Feign 接口导致启动失败。
Excel 导出
导出绕过 Controller 返回值切面时,要先手动翻译,再写文件。
@GetMapping("/export-excel")
@TransMethodResult
public void exportOperateLog(HttpServletResponse response, @Valid OperateLogPageReqVO exportReqVO) {
List<OperateLogDO> list = operateLogService.getOperateLogPage(exportReqVO).getList();
ExcelUtils.write(response, "操作日志.xls", "数据列表", OperateLogRespVO.class,
TranslateUtils.translate(BeanUtils.toBean(list, OperateLogRespVO.class)));
}TranslateUtils 在 ruoyi-office/yudao-framework/yudao-spring-boot-starter-mybatis/,内部调用 easy-trans 的 transBatch。
字典值(性别、状态)走另一条线:字段同时标 @ExcelProperty(converter = DictConvert.class) 和 @DictFormat。
@ExcelProperty(value = "用户性别", converter = DictConvert.class)
@DictFormat(DictTypeConstants.USER_SEX)
private Integer sex;| 名称 | 说明 | 仓库路径 |
|---|---|---|
@DictFormat | 声明字典类型 | yudao-spring-boot-starter-excel/.../DictFormat.java |
DictConvert | EasyExcel 读写时查字典标签 | 同模块 .../convert/DictConvert.java |
TranslateUtils | 导出前把 @Trans 字段填上 | yudao-spring-boot-starter-mybatis/.../TranslateUtils.java |
配置与操作
复杂转换写各模块 convert 的 MapStruct;简单拷贝用 BeanUtils。导出翻译加 @Trans / @DictFormat,不要把 DO 当接口出入参。
开启见 框架层。示意图见上文。
