资讯动态

订单状态字段改造:枚举、校验与MyBatis Plus条件构造器的实战指南

发布时间:2026/9/8 2:25:37 来源:尧图企业网站定制
最近在做订单模块的改造遇到了一个特别典型的组合枚举、校验、MyBatis Plus 条件构造器。这三样东西单独拎出来每一个都简单但放在一个接口里配合不好就会不断踩坑。后台管理系统里最常见的操作就是前端通过 status 字段筛选订单后端要校验这个值是不是合法然后用 MyBatis Plus 的条件构造器把它拼进查询。一开始我用 Integer 字段从头写到尾代码倒也能跑可越到后面越难受到处是魔法值状态拿错、参数不过滤、SQL 里莫名多出status null的情况也出现过。下面这块就把我在这个过程中的设计方案、踩坑记录和优化思路完整梳理一遍适合正在用 Spring Boot MyBatis Plus 做后台系统的开发者参考。1. 为什么订单状态字段必须用枚举我被魔法值坑了三次之后才明白1.1 魔法值代码留下的定时炸弹很多项目初期为了赶进度订单状态直接写成 Integer于是代码里到处都是这种操作if (order.getStatus() 1) { // 1 表示已支付 // 执行发货逻辑 } // 又或者查询的时候 queryWrapper.eq(status, 2);第一次看到这样的代码你会觉得挺清爽不就是个数字吗可等需求做大了问题就来了1、2、3、4 分别代表什么状态没人记得住只能去翻数据库注释或问老同事。新旧需求交替时数字含义发生变动比如原来的已取消是 4后来插了一个售后中进去有的地方用 4有的地方用 5整个系统就乱了。前后端联调时前端问已支付传几后端翻代码看一眼回答传 1这种沟通成本一次两次还好时间长了就是在消耗团队耐心。我甚至遇到过一种情况某个老接口里把已支付硬编码成了 2而新需求里已支付是 1前端拿着新文档传了 1后端还是按 2 去查线上查出来少了一大批订单。定位了很久才找到是魔法值不一致。这种问题靠 code review 很难完全避免最根本的解决办法就是让状态字段变成一个「带语义的类型」也就是枚举。1.2 用枚举定义状态代码立刻变得能说话我们把订单状态设计成枚举每个常量带一个业务码和一个描述public enum OrderStatus { PENDING(0, 待支付), PAID(1, 已支付), SHIPPED(2, 已发货), COMPLETED(3, 已完成), CANCELLED(4, 已取消); private final Integer code; private final String desc; OrderStatus(Integer code, String desc) { this.code code; this.desc desc; } public Integer getCode() { return code; } public String getDesc() { return desc; } public static OrderStatus fromCode(Integer code) { for (OrderStatus status : values()) { if (status.code.equals(code)) { return status; } } return null; } }这样再去看业务代码if (OrderStatus.PAID.equals(order.getStatus())) { // 执行发货逻辑 }哪怕是一个没参与开发的人看到OrderStatus.PAID也知道这段逻辑是针对已支付订单的根本不需要去翻数字的含义。平时写 switch、写 if、做状态机判断全都是基于枚举类型编译器会在写代码阶段就拦截掉很多低级错误。1.3 枚举设计要避开 useOrdinal 的坑有一类新手设计习惯很危险直接用枚举的ordinal()映射数据库。比如PAID.ordinal()是 1SHIPPED.ordinal()是 2。这样做问题很大只要你在枚举中间插入一个新状态比如在PAID和SHIPPED之间加一个PARTIALLY_SHIPPED所有已发货订单的ordinal()就自动从 2 变成了 3而数据库里老数据的 status 还是 2整个数据含义直接错位。更不要提多个环境枚举定义顺序不一致造成的同步问题。所以枚举里的业务码一定要独立维护。上面代码里的code字段就是干这个用的。即使以后调整枚举顺序只要 code 不变数据库里的数据就不会乱。如果老项目里已经有 int 状态值我更建议在枚举的 code 里直接写成老数字这样迁移时不用改历史数据只在代码层把 Integer 换成枚举成本会小很多。2. 让枚举在数据库和前端之间畅通无阻MyBatis-Plus 的 EnumValue 与 Jackson 序列化2.1 数据库存 codeJava 用枚举EnumValue 的核心逻辑枚举定义好了但数据库不会自动认识它默认情况下 MyBatis-Plus 会把枚举当成对象处理存到数据库时要么报类型转换异常要么存成一个毫无意义的字符串。要解决这个问题MyBatis-Plus 提供了EnumValue注解作用是告诉 MyBatis-Plus这个枚举在数据库里应该存哪一个字段。我们回到上面的OrderStatus在 code 字段上加上EnumValuepublic enum OrderStatus { EnumValue PENDING(0, 待支付), EnumValue PAID(1, 已支付), EnumValue SHIPPED(2, 已发货), EnumValue COMPLETED(3, 已完成), EnumValue CANCELLED(4, 已取消); // 其他代码不变 }实际项目里我更推荐另一种写法枚举实现 MyBatis-Plus 的IEnum接口重写getValue()方法。不过EnumValue更直观、侵入性更低普通项目用它就够了。加了注解之后插入记录时MyBatis-Plus 会把枚举的 code 写入数据库字段查询记录时又会自动把数据库的数字映射成对应的枚举对象。2.2 配置 type-enums-package 和常见的坑要让上面的映射生效还需要在 application.yml 里配置枚举扫描包mybatis-plus: type-enums-package: com.example.order.enums configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.CompositeEnumTypeHandlertype-enums-package的作用是告诉 MyBatis-Plus 去哪里扫描枚举类型扫描到之后统一注册类型处理器。如果你配置了依旧不生效可以检查一下枚举是否真的放在了扫描包路径下或者有没有误写成了别的包。这里还有一个很容易被忽略的细节如果某个实体字段上用了TableField(typeHandler ...)自定义类型处理器它会优先于全局枚举类型处理器所以遇到字段类型转换异常时可以先看是不是实体类上硬指定了 handler。2.3 前端传参的反序列化JsonCreator 和 JsonValue 配合使用枚举在数据库这边通了但前端还是直接面对 JSON。如果不做任何处理Jackson 默认把枚举序列化成它的 name比如PAID。前端拿到的是个字符串PAID展示的时候要自己去映射描述查询时还要把PAID传回来。这体验太差了。更好的方案是前后端统一约定状态字段传业务码Integer例如已支付就传 1。为了实现这个约定需要在枚举中同时配置JsonValue和JsonCreatorpublic enum OrderStatus { EnumValue PENDING(0, 待支付), EnumValue PAID(1, 已支付), EnumValue SHIPPED(2, 已发货), EnumValue COMPLETED(3, 已完成), EnumValue CANCELLED(4, 已取消); private final Integer code; private final String desc; OrderStatus(Integer code, String desc) { this.code code; this.desc desc; } JsonValue public Integer getCode() { return code; } JsonCreator public static OrderStatus fromJson(Integer code) { if (code null) { return null; } return fromCode(code); } public String getDesc() { return desc; } public static OrderStatus fromCode(Integer code) { for (OrderStatus status : values()) { if (status.code.equals(code)) { return status; } } return null; } }JsonValue用于序列化告诉 Jackson 输出codeJsonCreator用于反序列化告诉 Jacksoncode可以构造出枚举对象。这样前端拿到的是 1、2、3传回来的也是 1、2、3不会再出现PAID这种英文名称。2.4 对比Integer 手动转换 vs 枚举自动映射对比维度传统 Integer 手动转换枚举 MyBatis-Plus 自动映射字段类型IntegerOrderStatus可读性低到处是魔法值高POJO 里一眼看懂序列化天然是数字但无约束通过 JsonValue 定义输出反序列化任意数字都能进来通过 JsonCreator 校验拦截数据库映射手写 set/get 转换EnumValue 自动处理插入越界值可能插入未知数字枚举校验拦截代码层面控制其实最核心的价值是类型安全。用了枚举之后非法状态值在进入 Service 之前就会被拦截而 Integer 字段你根本没办法保证它里面装的是 1 还是 99。3. 参数校验别再 if-else 了用自定义注解一次管住枚举值3.1 后端校验为什么是必须的不能只听前端很多前端同学会在页面上做下拉校验但后端接口是对外暴露的永远不要假设调用方会按前端页面来玩。有人手写接口传一个status 99后端如果不校验拿着 99 去查库可能查不到任何数据也可能因为未知状态让后续逻辑出现空指针。最要命的是如果某个状态值在历史版本里存在、新版本已经废弃前端老页面还在传接口需要在后端做兼容性处理。所以后端校验是最后一道防线它保护的是数据库和业务逻辑的完整性。校验分两层第一层是参数是否合法第二层才是业务逻辑校验。枚举值属于第一层应该在参数接收阶段就处理掉。3.2 自定义 EnumValid 注解的基本实现以前我的代码里经常出现这种手写 ifif (dto.getStatus() ! null) { OrderStatus status OrderStatus.fromCode(dto.getStatus()); if (status null) { throw new BusinessException(无效的订单状态); } }一个字段写一遍还好订单模块有十几处接口都要校验状态就变成了复制粘贴的重灾区。后来我改成了 JSR 303 自定义校验注解声明式地做这件事。先定义一个注解Target({ElementType.FIELD, ElementType.PARAMETER}) Retention(RetentionPolicy.RUNTIME) Constraint(validatedBy EnumValidValidator.class) public interface EnumValid { String message() default 枚举值不合法; Class?[] groups() default {}; Class? extends Payload[] payload() default {}; // 指定要校验的枚举类 Class? extends Enum? enumClass(); // 指定枚举里取值的方法名默认 getCode String method() default getCode; }再写校验器public class EnumValidValidator implements ConstraintValidatorEnumValid, Object { private Class? extends Enum? enumClass; private String method; Override public void initialize(EnumValid annotation) { this.enumClass annotation.enumClass(); this.method annotation.method(); } Override public boolean isValid(Object value, ConstraintValidatorContext context) { if (value null) { return true; } try { Object[] enums enumClass.getEnumConstants(); Method m enumClass.getMethod(method); for (Object e : enums) { Object code m.invoke(e); if (code ! null code.equals(value)) { return true; } } } catch (Exception ex) { return false; } return false; } }这里有一个关键设计value null时返回 true让NotNull注解去负责空值校验避免两个注解职责重复。自定义校验器只管枚举值是不是存在。3.3 通用化改造用 enumClass 和 method 指定任意枚举这样设计的好处是通用。不只订单状态能用支付渠道、用户类型、商品上下架状态只要是传一个 code 进来需要校验是否是合法枚举的场景都能复用。使用时只需要在 DTO 字段上标注public class OrderQueryDTO { EnumValid(enumClass OrderStatus.class, method getCode, message 订单状态不合法) private Integer status; }Controller 这里要注意Validated要放在参数上PostMapping(/page) public ResultPageResultOrderVO page(Validated RequestBody OrderQueryDTO dto) { return orderService.pageQuery(dto); }这样 Spring 在执行方法之前就会校验 DTO 里的 status。如果传了 99直接抛出 MethodArgumentNotValidException完全没有进入 Service 的机会。3.4 空值和默认值的处理细节很多人在状态筛选场景里会犯一个错误把未传状态和传了非法状态混为一谈。未传 status我们期望查询所有状态的订单传了非法的 status我们期望返回参数错误。这里必须分开处理status为 null校验器返回 true不拦截。status不为 null 但是枚举里不存在校验器返回 false抛出异常。还要注意有些枚举的 code 是字符串类型比如PAY_SUCCESS、PAY_FAIL使用方式完全一样只要把 DTO 里字段类型改成 String注解配置的method还是指向字符串类型的getCode就行。我习惯把枚举的 code 统一成 Integer因为数据库 int 类型查询性能更好、存储空间更小除非业务上必须用英文字符串做对外标识才考虑 String。4. LambdaQueryWrapper 条件构造器枚举筛选的正确打开方式4.1 从 QueryWrapper 到 LambdaQueryWrapper 的升级理由校验通过之后真正干活的是 MyBatis Plus 条件构造器。老项目里常见写法是QueryWrapperOrder wrapper new QueryWrapper(); wrapper.eq(status, dto.getStatus());字段名写字符串一旦数据库字段改了名或者实体字段改名导致列名变化这段代码在编译期完全不会报错运行起来才发现 SQL 出问题。尤其是枚举接入之后实体里是OrderStatus status数据库列叫status手工写错一个字母排查起来特别费劲。LambdaQueryWrapper 的好处就是类型安全。它用方法引用代替字段名例如LambdaQueryWrapperOrder wrapper Wrappers.lambdaQuery(); wrapper.eq(Order::getStatus, dto.getStatus());这样在编译期间就能确认 Order 类确实有status字段重构的时候编译器会直接告诉你哪里没改。4.2 条件构造器与枚举 code 的几种组合用法在实际查询里我很少直接把枚举对象传给 wrapper而是传 code。因为数据库里存的就是 code类型是 Integer传枚举对象虽然 MyBatis-Plus 也能处理但会额外走一遍枚举转换没这个必要。单个状态精确匹配wrapper.eq(dto.getStatus() ! null, Order::getStatus, dto.getStatus());多个状态范围匹配比如查询待支付 已支付的订单ListInteger statusList Arrays.asList(0, 1); wrapper.in(CollectionUtils.isNotEmpty(statusList), Order::getStatus, statusList);排除某个状态wrapper.ne(dto.getStatus() ! null, Order::getStatus, dto.getStatus());枚举本身也可以参与构造条件比如OrderStatus.PAID.getCode()只要不把 null 传进去就行。4.3 eq、in、orderBy、page 的配合避免 null 条件污染 SQL这里最容易出问题的点是条件构造器会无条件拼接。看下面的错误示例wrapper.eq(Order::getStatus, dto.getStatus());如果dto.getStatus()为 nullMyBatis-Plus 生成的 SQL 会是WHERE status null。虽然 MySQL 里这个条件永远为 false不会把数据查坏但它会让接口结果变成空列表而且排查问题的人会一脸懵为什么没传状态就查不到数据正确写法是使用 condition 重载wrapper.eq(dto.getStatus() ! null, Order::getStatus, dto.getStatus());第一个参数 condition 为 true 时才拼接这个条件为 false 时自动忽略。这个方法在很多条件构造器方法里都有重载比如like、ge、le、in强烈建议大家习惯它。分页配合PageOrder page new Page(dto.getPageNum(), dto.getPageSize()); wrapper.orderByDesc(Order::getCreateTime); IPageOrder result orderMapper.selectPage(page, wrapper);这样 page 和 wrapper 就结合起来了不需要自己写 count 查询。4.4 设计一个简单的 Wrapper 构建工具方法当筛选条件越来越多Controller、Service 里堆一堆 wrapper 代码会很啰嗦。我习惯在 Service 里抽一个私有方法private LambdaQueryWrapperOrder buildQueryWrapper(OrderQueryDTO dto) { LambdaQueryWrapperOrder wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.isNotBlank(dto.getOrderNo()), Order::getOrderNo, dto.getOrderNo()); wrapper.eq(dto.getStatus() ! null, Order::getStatus, dto.getStatus()); wrapper.ge(dto.getStartTime() ! null, Order::getCreateTime, dto.getStartTime()); wrapper.le(dto.getEndTime() ! null, Order::getCreateTime, dto.getEndTime()); wrapper.orderByDesc(Order::getCreateTime); return wrapper; }这样 Service 里就干净很多public PageResultOrderVO pageQuery(OrderQueryDTO dto) { PageOrder page orderMapper.selectPage(new Page(dto.getPageNum(), dto.getPageSize()), buildQueryWrapper(dto)); return PageResult.of(page); }如果你们的项目有大量类似查询甚至可以把这种buildQueryWrapper的方法进一步模板化配合泛型抽取成一个基础方法。不过大多数场景下一个私有方法已经够用过度抽象反而影响阅读。5. 完整实操一个订单筛选接口的演进过程5.1 需求描述与接口设计假设我们有这样一个后台需求订单管理页需要分页查询订单支持按订单号模糊搜索、按订单状态筛选、按下单时间范围筛选。订单状态有五种待支付 0、已支付 1、已发货 2、已完成 3、已取消 4。接口设计如下URLPOST /order/page请求体{ orderNo: 20241201, status: 1, startTime: 2024-12-01 00:00:00, endTime: 2024-12-31 23:59:59, pageNum: 1, pageSize: 10 }响应分页结果包含订单信息。接口要求status不传时查询全部状态status传了非法值比如 99时直接返回参数错误。5.2 Controller 层DTO 校验是怎么被触发的首先定义枚举OrderStatus参照上面的代码然后定义 DTOData public class OrderQueryDTO { private String orderNo; EnumValid(enumClass OrderStatus.class, method getCode, message 订单状态不合法) private Integer status; DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss) private LocalDateTime startTime; DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss) private LocalDateTime endTime; private Integer pageNum 1; private Integer pageSize 10; }Controller 很简单PostMapping(/page) public ResultPageResultOrderVO page(Validated RequestBody OrderQueryDTO dto) { return Result.success(orderService.pageQuery(dto)); }Spring 接收到 JSON 后先做 Jackson 反序列化把 JSON 里的status: 1放到 DTO 的 Integer 字段里然后执行Validated校验。校验器遍历 OrderStatus 枚举的 code发现 1 对应的是PAID校验通过。如果传的是 99校验失败抛出异常由全局异常处理器包装成错误响应返回给前端。5.3 Service 层用 Wrappers.lambdaQuery 完成动态查询Service 实现里按照我们前面说的方式构建查询条件Override public PageResultOrderVO pageQuery(OrderQueryDTO dto) { LambdaQueryWrapperOrder wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.isNotBlank(dto.getOrderNo()), Order::getOrderNo, dto.getOrderNo()); wrapper.eq(dto.getStatus() ! null, Order::getStatus, dto.getStatus()); wrapper.ge(dto.getStartTime() ! null, Order::getCreateTime, dto.getStartTime()); wrapper.le(dto.getEndTime() ! null, Order::getCreateTime, dto.getEndTime()); wrapper.orderByDesc(Order::getCreateTime); PageOrder page orderMapper.selectPage(new Page(dto.getPageNum(), dto.getPageSize()), wrapper); // 转 VO 返回前端 ListOrderVO voList page.getRecords().stream().map(order - { OrderVO vo new OrderVO(); BeanUtils.copyProperties(order, vo); vo.setStatusDesc(order.getStatus() null ? : order.getStatus().getDesc()); return vo; }).collect(Collectors.toList()); PageResultOrderVO result new PageResult(); result.setRecords(voList); result.setTotal(page.getTotal()); return result; }这里有一个容易被初学者忽略的点Order实体的status字段类型是OrderStatus而dto.getStatus()是 Integer。在wrapper.eq(dto.getStatus() ! null, Order::getStatus, dto.getStatus())中传入的第三个参数是 Integer但 MyBatis-Plus 在底层生成 SQL 时会比较实体字段的数据库列和这个 Integer 值数据库列其实就是枚举的 code能直接匹配。实际上更多项目里会直接把 DTO 里的 status 也定义成OrderStatus类型这样反序列化时JsonCreator已经帮我们转换成了枚举对象。此时 wrapper 里可以直接写wrapper.eq(dto.getStatus() ! null, Order::getStatus, dto.getStatus());但这样就变成了传枚举对象给 wrapperMyBatis-Plus 一样能处理。从代码直观性来说DTO 字段类型用 Integer 反而更清晰因为它明确表示前端传过来的只是一个业务码校验器和 wrapper 只需要处理 Integer 即可。5.4 关键点复盘枚举、校验、Wrapper 是怎么闭环的整个链路其实是一个闭环前端传status业务码JSON 反序列化后成为 DTO 里的 Integer。自定义校验注解EnumValid判断这个 Integer 是否是 OrderStatus 枚举中的合法 code非法则直接拦截。校验通过后Service 层拿到 Integer 类型的status通过 LambdaQueryWrapper 动态拼接到 SQL 条件中。MyBatis-Plus 在查询时把数据库 int 字段自动映射成 OrderStatus 枚举业务代码里可以直接用枚举做判断。这个闭环里如果缺了校验非法值会直接污染 SQL如果缺了枚举映射查询结果里 status 是 Integer代码里没法安全地调用PAID等语义如果缺了条件构造器的高级用法动态条件会很容易写出冗余甚至错误的 SQL。6. 枚举 校验 条件构造器联动的常见坑与优化6.1 枚举反序列化失败前端传的是描述不是 code这种坑非常典型。前端同学可能会直接把状态描述传给后端比如已支付而不是1。如果 DTO 里 status 是 IntegerJackson 根本没法把已支付转成数字如果 status 是 OrderStatus 类型且只有EnumValid但没有JsonCreatorJackson 默认会去找枚举的name()也就是PAID也还是对不上。解决办法前后端约定使用 code并在枚举上通过JsonCreator静态工厂接收 Integer。如果历史接口里前端一直传的是描述字符串也可以临时在JsonCreator里增加对 desc 的匹配但这属于兼容写法最好还是尽快推动前端改成传 code。6.2 MyBatis-Plus 枚举映射未生效查询结果 status 为 null有段时间我在本地跑联调明明数据库里 status 是 1查询出来 Order 对象的 status 却是 null。排查了一圈发现是type-enums-package配置的包路径不对枚举类和 mapper 不在同一个模块导致 MyBatis-Plus 没有自动扫描到这个枚举。后来把包路径改成正确的根包重新启动就好了。还有一个可能实体类字段上没有加EnumValue的枚举MyBatis-Plus 可能直接按枚举 name 转换比如把 1 映射成OrderStatus.PAID可能会失败最终返回 null。所以一定要确保至少有一处枚举字段全局扫描注解或者实体字段上的 typeHandler配置正确。建议写一个单元测试专门验证枚举插入和查询别等联调时再发现。6.3 Wrapper 拼接条件时 null 参与 eq 导致 SQL 异常这一点我在前面强调过但实在太多人踩了单独拎出来再说一次。错误的写法wrapper.eq(Order::getStatus, dto.getStatus());正确的写法wrapper.eq(dto.getStatus() ! null, Order::getStatus, dto.getStatus());如果你使用的是 MyBatis-Plus 3.5.x 以上版本还有一个小技巧可以直接用wrapper.eq(condition, Order::getStatus, value)condition 可以是一个 boolean 表达式也可以是一个 Boolean 对象。MyBatis-Plus 也提供了StringUtils.isNotBlank和ObjectUtils.isNotEmpty等判断工具。我习惯在 condition 里只判断前端有没有传这个参数比如wrapper.eq(Objects.nonNull(dto.getStatus()), Order::getStatus, dto.getStatus());这样写一眼就能看出逻辑也不容易漏。6.4 自定义校验器的 NPE 与异常消息不够友好自定义校验器看起来简单但有一个隐藏的 NPE 点如果某个枚举的getCode()返回 null而前端传的 value 不是 nullcode.equals(value)就会触发空指针。所以一定要在代码里先判断code ! null。再看错误消息默认的枚举值不合法对前端同学太不友好了他们根本不知道什么值合法。更好的做法是在校验失败时把允许的 code 列表拼进去Override public boolean isValid(Object value, ConstraintValidatorContext context) { if (value null) { return true; } SetObject codeSet EnumCodeCache.get(enumClass, method); if (codeSet.contains(value)) { return true; } context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate( String.format(参数值 %s 不合法允许范围: %s, value, codeSet) ).addConstraintViolation(); return false; }这里用到了一个缓存工具类EnumCodeCache也就是我下面要说的性能优化。6.5 用缓存和字典接口提升枚举使用体验枚举数量通常很少不过在高并发接口里每次校验都用method.invoke(e)反射遍历一遍终究不是最优解。我抽了一个简单的缓存工具public class EnumCodeCache { private static final MapClass?, SetObject CACHE new ConcurrentHashMap(); public static SetObject get(Class? enumClass, String method) { return CACHE.computeIfAbsent(enumClass, clazz - { SetObject set new HashSet(); try { Object[] enums clazz.getEnumConstants(); Method m clazz.getMethod(method); for (Object e : enums) { set.add(m.invoke(e)); } } catch (Exception ignored) { } return set; }); } }在校验器里直接调用EnumCodeCache.get(...)第一次反射构建集合之后就是O(1)的contains判断。顺便提一句后台管理系统通常还会把所有枚举的 code/desc 暴露成一个字典接口给前端做下拉框。这个功能可以写一个通用接口扫描项目里的枚举类返回[{code: 0, desc: 待支付}, {code: 1, desc: 已支付}]这样的结构。有了字典接口前端下拉选项和后端枚举定义始终来自同一个数据源就不容易出现前端下拉显示已支付、实际传值传成 2 这种乌龙。6.6 让所有枚举实现统一接口少写重复代码做到后面你会发现每个业务枚举都要写 code、desc、fromCode、JsonValue、JsonCreator非常重复。我后来在项目里定了一个规则所有状态类枚举统一实现一个IdDescEnum接口public interface IdDescEnum { Integer getCode(); String getDesc(); static T extends EnumT IdDescEnum T fromCode(ClassT enumType, Integer code) { if (code null) { return null; } for (T e : enumType.getEnumConstants()) { if (e.getCode().equals(code)) { return e; } } return null; } }这样每个枚举就只需要写字段和构造器通用逻辑统一到接口的静态方法里。自定义校验器里也可以把method默认指向getCode使用更简洁。这个习惯帮我省掉了大量重复代码也让团队里的状态枚举风格高度统一。最后一个个人体会枚举、校验、条件构造器这三件事看起来是三个独立的知识点但在真实业务里是强耦合的。枚举定义不好校验代码就会写得啰嗦校验不到位条件构造器就会接到脏参数条件构造器不会动态拼接再好的枚举也发挥不出作用。把这条链路理顺后台系统的代码会清爽一大截你在联调和排查问题时也会发现很多莫名其妙的 bug 其实从源头就能避免。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价