工单实体与DTO设计:PO、VO、DTO与状态枚举
好,咱们进入第四章。这一章我打算聊聊工单模块里最基础、也最容易踩坑的部分——实体与DTO设计。
你想想看,一个工单从创建到归档,数据要在不同层之间流转。数据库里存一套结构,前端展示一套结构,接口传输又是一套结构。如果全混在一起写,后期维护起来那叫一个酸爽。我在项目里见过不少同学图省事,直接把数据库实体丢给前端,结果字段暴露、性能浪费、改个需求还得动数据库表结构……嗯,咱们今天就把这事捋清楚。
一、PO实体类:跟数据库表一一对应
PO,全称Persistent Object,说白了就是数据库表的映射。每个字段、每个类型,都得跟表结构严丝合缝。
我个人习惯在PO里只放数据库字段,不掺杂任何业务逻辑。举个例子:
public class WorkOrderPO {
private Long id;
private String orderNo; // 工单编号
private String productCode; // 产品编码
private Integer quantity; // 计划数量
private Integer status; // 状态:0-待生产 1-生产中 2-已完成
private LocalDateTime createTime;
private LocalDateTime updateTime;
// getter/setter 省略
}
这里有个细节——状态字段我用了Integer,而不是直接写枚举。为什么?因为数据库里存的是数字,PO作为映射层,保持原始类型更安全。枚举转换的事,交给DTO去做。
二、VO视图对象:前端要什么,我就给什么
VO,View Object,是给前端看的。前端不关心你数据库里存了几个字段,它只关心页面上要展示什么。
比如工单列表页,前端需要显示状态的中文名称、创建时间格式化后的字符串,甚至还要拼接产品名称和规格。这些在PO里都没有,那就得在VO里组装。
public class WorkOrderVO {
private Long id;
private String orderNo;
private String productName; // 产品名称(关联查询)
private String productSpec; // 产品规格
private Integer quantity;
private String statusName; // 状态中文名
private String createTimeStr; // 格式化后的时间
// getter/setter 省略
}
你可能会问:为什么不直接在PO里加这些字段?嗯,一旦PO里混入了非数据库字段,MyBatis的自动映射就会出问题。而且,PO的改动会影响所有使用它的地方,风险太大。
三、DTO数据传输对象:接口的“守门员”
DTO,Data Transfer Object,是服务层和接口层之间的桥梁。它承担两个职责:一是接收前端传过来的参数,二是返回给前端需要的数据。
我举个例子。工单创建接口,前端传的参数可能包括产品编码、数量、计划开始时间等。这些参数可能跟PO字段不完全一致,也可能需要做校验。DTO就是干这个的。
public class WorkOrderCreateDTO {
@NotBlank(message = "产品编码不能为空")
private String productCode;
@Min(value = 1, message = "数量必须大于0")
private Integer quantity;
private String planStartTime; // 前端传的是字符串,DTO里先收着
// getter/setter 省略
}
DTO还有一个重要用途——屏蔽敏感字段。比如PO里有createTime、updateTime,这些前端不需要知道,DTO里就不放。说白了,DTO就是接口的“守门员”,该放行的放行,该拦截的拦截。
四、状态枚举定义:用枚举管理状态流转
工单状态是MES系统里最核心的业务字段之一。从待生产到生产中,再到已完成,中间还可能穿插暂停、取消等状态。如果状态值散落在代码里,用魔法数字到处写,那维护起来就是一场噩梦。
我建议用枚举来统一管理:
public enum WorkOrderStatus {
PENDING(0, "待生产"),
IN_PROGRESS(1, "生产中"),
COMPLETED(2, "已完成"),
PAUSED(3, "已暂停"),
CANCELLED(4, "已取消");
private final int code;
private final String desc;
WorkOrderStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static WorkOrderStatus fromCode(int code) {
for (WorkOrderStatus status : values()) {
if (status.code == code) {
return status;
}
}
throw new IllegalArgumentException("未知状态码: " + code);
}
}
有了枚举,状态流转的逻辑就清晰多了。比如工单只能从“待生产”流转到“生产中”,不能从“已完成”跳回“待生产”。这些规则可以写在枚举里,也可以写在服务层。我个人习惯把流转规则放在枚举里,因为这样更内聚。
五、整体结构图
下面这张图展示了PO、VO、DTO和枚举之间的关系。你可以看到,数据从数据库到前端,经过了层层转换,每一层都有自己的职责。
六、总结一下
这一章的内容其实就一句话:各层各司其职,别越界。PO只管数据库映射,VO只管前端展示,DTO只管接口传输,枚举管好状态流转。这样做的好处是,将来改数据库表结构,只需要改PO;改前端展示,只需要改VO;改接口参数,只需要改DTO。互不影响,维护成本直线下降。
嗯,我记得有一次重构一个老项目,发现PO里居然有前端展示用的字段,DTO里又混了数据库字段,整个代码乱成一锅粥。后来花了整整两周才理清楚。所以,从一开始就设计好分层,后面能省不少事。