Skip to content

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
ConvertMapStruct 接口,复杂拼接放 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、Consumerruoyi-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/:

java
@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。只改这一处,就能换底层实现。

java
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:

java
@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 客户列表先批量查用户和部门,再回填:

java
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-transVO 标 @Trans,接口标 @TransMethodResult操作日志、CRM 产品

列表接口多数走手工拼接:SQL 简单,压力小,后续改字段也只动 Java。注解翻译适合「一个编号对应一张本模块表」或「走 *-api 查用户」。

数据翻译示意图

示意图:模块内查本模块 DO;跨模块走 AdminUserApi。

模块内翻译 ​

OperateLogRespVO 实现 org.dromara.core.trans.vo.VO,用 userId 读本模块 AdminUserDO:

java
@Trans(type = TransType.SIMPLE, target = AdminUserDO.class, fields = "nickname", ref = "userName")
private Long userId;
private String userName;
名称说明仓库路径
typeTransType.SIMPLE,走 MyBatis PlusOperateLogRespVO
target目标 DOAdminUserDO.class
fields / ref读哪个字段、写到 VO 哪一列nickname → userName

对应 Controller 必须加 @TransMethodResult,否则注解不生效:

java
@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:

java
@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:

java
@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 返回值切面时,要先手动翻译,再写文件。

java
@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。

java
@ExcelProperty(value = "用户性别", converter = DictConvert.class)
@DictFormat(DictTypeConstants.USER_SEX)
private Integer sex;
名称说明仓库路径
@DictFormat声明字典类型yudao-spring-boot-starter-excel/.../DictFormat.java
DictConvertEasyExcel 读写时查字典标签同模块 .../convert/DictConvert.java
TranslateUtils导出前把 @Trans 字段填上yudao-spring-boot-starter-mybatis/.../TranslateUtils.java

配置与操作 ​

复杂转换写各模块 convert 的 MapStruct;简单拷贝用 BeanUtils。导出翻译加 @Trans / @DictFormat,不要把 DO 当接口出入参。

开启见 框架层。示意图见上文。

相关篇 ​

联系我们

获取报价、演示和二开方案

微信咨询二维码

微信咨询

17156169080

添加时备注「RuoYi Office」

在线体验商业版