Skip to content

场景 1:订单模块 - “能否取消订单”的复杂业务规则判断

业务背景: 用户下单后,想取消订单。但取消不是随时都能取消的,得满足一堆业务规则:

  • 订单状态必须是“已支付”或“待发货”
  • 未超过可取消时间窗口(比如支付后7天内)
  • 如果是虚拟商品(如门票),超过活动开始前24小时就不能取消
  • 如果订单包含“赠品”,且赠品已发货,就不能取消
  • VIP用户有特权,可以延长取消窗口到15天

不用 Specification 的传统写法(痛点)

Java
public boolean canCancel(Order order, User user) {
    if (order.getStatus() != PAID && order.getStatus() != PENDING_SHIP) {
        return false;
    }
    if (order.getCreateTime().plusDays(7).isBefore(now())) {
        return false;
    }
    if (order.isVirtual() && order.getActivityStartTime().minusHours(24).isBefore(now())) {
        return false;
    }
    if (order.hasGift() && order.getGiftShipped()) {
        return false;
    }
    if (user.isVip()) {
        // VIP 特权逻辑
        if (order.getCreateTime().plusDays(15).isBefore(now())) {
            return false;
        }
    }
    return true;
}

痛点

  • 规则全堆在一个大 if-else 方法里,代码越来越长,可读性差
  • 新增一个规则(如“节日活动订单不可取消”),就要改这个方法,容易漏判
  • 想复用“VIP可延长取消时间”这个子规则,在其他地方(如退款判断)用不了
  • 测试麻烦:要测所有组合,得写一大堆测试用例覆盖分支

用 Specification 的优雅写法

java
// 原子规则(每个规则独立、可复用、可单独测试)
public class PaidOrPendingShipSpec implements Specification<Order> {
    public boolean isSatisfiedBy(Order order) {
        return order.getStatus() == PAID || order.getStatus() == PENDING_SHIP;
    }
}

public class WithinCancelWindowSpec implements Specification<Order> {
    private final int days;
    public WithinCancelWindowSpec(int days) { this.days = days; }
    public boolean isSatisfiedBy(Order order) {
        return order.getCreateTime().plusDays(days).isAfter(now());
    }
}

public class NotStartedVirtualActivitySpec implements Specification<Order> {
    public boolean isSatisfiedBy(Order order) {
        if (!order.isVirtual()) return true;
        return order.getActivityStartTime().minusHours(24).isAfter(now());
    }
}

public class GiftNotShippedSpec implements Specification<Order> {
    public boolean isSatisfiedBy(Order order) {
        return !order.hasGift() || !order.getGiftShipped();
    }
}

public class VipExtendedCancelSpec implements Specification<Order> {
    public boolean isSatisfiedBy(Order order, User user) {  // 可传入上下文
        if (!user.isVip()) return true;  // 非VIP不影响
        return order.getCreateTime().plusDays(15).isAfter(now());
    }
}

// 使用时动态组合(超级清晰!)
Specification<Order> cancelSpec = new PaidOrPendingShipSpec()
    .and(new WithinCancelWindowSpec(7))
    .and(new NotStartedVirtualActivitySpec())
    .and(new GiftNotShippedSpec());

if (user.isVip()) {
    cancelSpec = cancelSpec.and(new VipExtendedCancelSpec());
}

if (cancelSpec.isSatisfiedBy(order)) {
    order.cancel();
}

好处

  • 每个规则独立成类,命名就是业务语言(通用语言)
  • 新增规则只需加一个新类,不会改动原有代码(开闭原则)
  • 子规则可复用(比如 WithinCancelWindowSpec 还能用于退款判断)
  • 测试简单:每个 Spec 可以单独单元测试

场景 2:商品模块 - “商品能否发布/上架”的多规则校验

业务背景: 商家发布商品,上架前要通过一系列审核规则:

  • 主图必须上传
  • 商品标题不能含敏感词
  • 价格必须合理(不能低于成本价的80%)
  • 库存必须大于0
  • 必须选择正确的类目
  • 如果是食品类,必须上传质检报告
  • 管理员手动审核通过

不用 Specification:一个巨大的 validate() 方法,里面一堆 if,维护噩梦。

用 Specification

java
Specification<Product> publishSpec = new HasMainImageSpec()
    .and(new NoSensitiveWordsInTitleSpec())
    .and(new ReasonablePriceSpec(0.8))  // 价格不低于成本80%
    .and(new HasStockSpec())
    .and(new ValidCategorySpec());

if (product.isFood()) {
    publishSpec = publishSpec.and(new HasQualityReportSpec());
}

publishSpec = publishSpec.and(new AdminApprovedSpec());

if (!publishSpec.isSatisfiedBy(product)) {
    throw new BusinessException("商品不满足上架条件: " + publishSpec.getFailedReasons());
}

好处:规则清晰可组合,食品类目自动加额外规则,超级灵活。

场景 3:权限模块 - 复杂权限判断

业务背景: 用户访问某个功能,需要判断是否有权限。权限规则可能很复杂:

  • 有角色“ADMIN”就能访问
  • 或者属于某个组织
  • 或者是被邀请的嘉宾
  • 或者付费开通了高级功能

用 Specification

java
Specification<User> accessSpec = new HasRoleSpec("ADMIN")
    .or(new InOrganizationSpec(orgId))
    .or(new IsInvitedGuestSpec(eventId))
    .or(new HasPaidFeatureSpec("PREMIUM"));

if (accessSpec.isSatisfiedBy(currentUser)) {
    grantAccess();
}

好处:OR 组合非常自然,新增一种权限方式只需加一个 Spec。

场景 4:风控模块 - 用户注册/登录风控拦截

业务背景: 防止刷号、恶意注册。规则:

  • 同IP 1小时内注册不能超过3个
  • 同设备ID 24小时内不能超过5个
  • 手机号不能是黑名单
  • 不能短时间内频繁尝试登录

用 Specification(内存执行,速度快):

java
Specification<RegisterRequest> riskSpec = new NotBlacklistedPhoneSpec()
    .and(new IpRegisterFrequencySpec(3, Duration.ofHours(1)))
    .and(new DeviceRegisterFrequencySpec(5, Duration.ofDays(1)));

if (riskSpec.isSatisfiedBy(request)) {
    allowRegister();
} else {
    reject("风控拦截");
}

好处:风控规则经常调整,用 Specification 改起来特别方便。

场景 5:报表/统计模块 - 数据过滤(结合 JPA Specification)

业务背景: 后台运营看报表,想按多种条件过滤订单数据:时间段、订单状态、支付方式、用户类型、金额范围等。

这其实和你现在的活动搜索一模一样! 用 Specification:完全复用你现在的 Criteria + JPA Specification 模式,在数据库层面过滤,性能最优。

总结:Specification 到底解决了什么业务痛点?

痛点传统写法Specification 解决方式
规则越来越多,代码膨胀一个大方法堆满 if-else每个规则一个独立类
新增/修改规则要改核心代码改动风险高新增类或组合方式,不改旧代码
规则难以复用复制粘贴代码原子 Spec 可跨场景复用
可读性差一长串条件判断组合后像自然语言:paid.and(within7Days).and(notVirtualStarted)
测试困难覆盖所有分支难每个 Spec 单独单元测试

核心价值:把“业务规则”从“过程式代码”变成“一等公民的可组合对象”。

你现在的活动搜索是用 Specification 的“数据库动态查询”能力, 而上面这些场景是用它的“纯业务规则校验 + 可组合”能力。

两者都是 Specification 的正确用法,只是执行位置不同(内存 vs 数据库)。

明白了吗?如果还有哪个场景不清晰,或者想看某个具体业务的完整代码示例,我可以再详细展开!🚀

Powered by VitePress