工单实体与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去做。

避坑指南:我曾经在PO里直接用了枚举类型,结果MyBatis映射时死活报错。后来查了半天,发现是枚举的ordinal()和数据库值对不上。从那以后,PO里我只用基本类型和String。

二、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的改动会影响所有使用它的地方,风险太大。

我的习惯:VO里只放前端需要的数据,不多不少。多一个字段,前端就要多处理一个;少一个字段,前端就得再调一次接口。所以设计VO时,最好跟前端同学对一下原型图。

三、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就是接口的“守门员”,该放行的放行,该拦截的拦截。

核心原则:PO只管数据库,VO只管前端展示,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);
    }
}

有了枚举,状态流转的逻辑就清晰多了。比如工单只能从“待生产”流转到“生产中”,不能从“已完成”跳回“待生产”。这些规则可以写在枚举里,也可以写在服务层。我个人习惯把流转规则放在枚举里,因为这样更内聚。

避坑指南:我曾经在项目里用字符串表示状态,比如"0"、"1"。结果有一次数据库里混入了"01",程序直接崩了。从那以后,我坚持用枚举+数字编码,并且在枚举里提供fromCode方法做校验。

五、整体结构图

下面这张图展示了PO、VO、DTO和枚举之间的关系。你可以看到,数据从数据库到前端,经过了层层转换,每一层都有自己的职责。

工单模块数据流转结构图 数据库 work_order 表 PO 实体类 WorkOrderPO DTO 数据传输对象 WorkOrderCreateDTO VO 视图对象 WorkOrderVO 前端页面 工单列表 / 工单详情 状态枚举 WorkOrderStatus 图例: 持久层 实体层 传输层 视图层 枚举层 依赖关系

六、总结一下

这一章的内容其实就一句话:各层各司其职,别越界。PO只管数据库映射,VO只管前端展示,DTO只管接口传输,枚举管好状态流转。这样做的好处是,将来改数据库表结构,只需要改PO;改前端展示,只需要改VO;改接口参数,只需要改DTO。互不影响,维护成本直线下降。

嗯,我记得有一次重构一个老项目,发现PO里居然有前端展示用的字段,DTO里又混了数据库字段,整个代码乱成一锅粥。后来花了整整两周才理清楚。所以,从一开始就设计好分层,后面能省不少事。

小技巧:如果你用的是MyBatis-Plus,可以在PO里加@TableField(exist = false)注解来排除非数据库字段。但我不建议这么做——非数据库字段多了,PO就变味了。还是老老实实分层吧。

专注资料整理