Spring EL实战:多对象入参实现优惠券动态可用规则校验

一、业务背景:传统优惠券规则有多痛苦?

电商系统优惠券场景,规则五花八门、迭代极快:

  • 满减门槛:订单金额≥99 元可用、≥199 元可用
  • 用户限制:仅新用户 / 会员等级≥3 级可用、黑名单用户禁用
  • 时间限制:仅限活动期、下单时间在券有效期内
  • 商品限制:优惠券限定类目,订单商品需匹配类目
  • 叠加 / 库存限制:优惠券剩余使用次数大于 0、不可与其他券叠加

传统硬编码方案痛点

业务规则写死在if/else、枚举、业务代码中:

  • 新增优惠券规则、修改门槛必须改代码、重启服务、灰度发布
  • 规则耦合业务代码,代码臃肿,大量重复判断逻辑
  • 运营配置优惠券、临时调整活动规则,依赖研发排期,效率极低
  • 规则版本难追溯,线上 bug 修复成本高

最优解:Spring EL 表达式实现动态规则

无需 Drools、QLExpress 重型规则引擎,基于 Spring 原生 Spring EL 表达式,支持同时传入User、Coupon、Order多个业务对象,规则存入数据库,运行时动态解析判断优惠券是否可用,改规则只改数据库,服务无需重启、无需改代码,轻量、零依赖、适配 SpringBoot 项目。

二、Spring EL 多对象模式核心优势(优惠券场景)

Spring Expression Language(Spring 表达式语言),Spring 原生内置,无需引入第三方依赖:

  • 原生支持 Spring 全家桶,无额外 jar 包、无版本冲突
  • 支持同时传入User/Coupon/Order多个业务对象,直接对象.属性取值,表达式语义贴合业务
  • 支持数值比较、逻辑运算、集合匹配、时间区间、空安全运算符
  • 规则持久化 MySQL,支持热更新、动态生效
  • 性能足够支撑优惠券高并发下单校验,缓存表达式后性能大幅提升
  • 语法简单,运营可快速编写基础规则

三、优惠券规则数据库表设计

优惠券主表简化设计

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| sql CREATE TABLE coupon ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '优惠券ID', coupon_name VARCHAR(64) NOT NULL COMMENT '优惠券名称', coupon_type TINYINT NOT NULL COMMENT '1满减券 2折扣券', rule_expression VARCHAR(1024) NOT NULL COMMENT 'SpEL可用校验表达式,返回布尔值,支持user/coupon/order多对象属性', discount_amount DECIMAL(10,2) COMMENT '优惠金额', valid_start_time DATETIME NOT NULL COMMENT '券生效时间', valid_end_time DATETIME NOT NULL COMMENT '券失效时间', status TINYINT DEFAULT 1 COMMENT '状态 1正常 0下架', rule_desc VARCHAR(512) COMMENT '规则中文说明,便于运营查看', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券表'; |

多对象场景示例规则表达式(直接存入数据库)

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| spel -- 示例1:订单满199、会员3级以上、非黑名单、下单在券有效期、商品匹配类目 order.totalAmount >= 199 and user.level >= 3 and !user.isBlack and order.createTime between coupon.validStartTime and coupon.validEndTime and coupon.category in order.goodsCategories -- 示例2:新用户专享满99券,券剩余次数>0 order.totalAmount >= 99 and user.isNewUser and coupon.maxUseCount > 0 -- 示例3:空安全写法,防止对象null空指针报错 user?.level >= 2 and order?.totalAmount >= 50 |

四、核心业务实体(多对象入参载体)

1. User.java 用户对象

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java import lombok.Data; @Data public class User { private Long userId; // 会员等级 1普通 2银卡 3金卡 private Integer level; // 是否黑名单用户 private Boolean isBlack; // 是否新用户 private Boolean isNewUser; } |

2. Coupon.java 优惠券对象

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java import lombok.Data; import java.time.LocalDateTime; @Data public class Coupon { private Long couponId; // 优惠券限定商品类目 private String category; // 券生效时间 private LocalDateTime validStartTime; // 券失效时间 private LocalDateTime validEndTime; // 最大可使用次数 private Integer maxUseCount; } |

3. Order.java 订单对象

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; @Data public class Order { // 订单实付总金额 private BigDecimal totalAmount; // 订单内全部商品类目集合 private List<String> goodsCategories; // 下单时间 private LocalDateTime createTime; } |

五、Spring EL 全局配置类

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; /** * Spring EL 规则引擎全局配置 */ @Configuration public class SpelConfig { /** * 全局表达式解析器,单例复用 */ @Bean public ExpressionParser expressionParser() { return new SpelExpressionParser(); } } |

六、通用多对象优惠券规则校验工具类

支持同时传入User、Coupon、Order三个对象,绑定独立变量名,内置异常捕获、空安全兼容,可直接用于下单校验、领券资格校验。

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; /** * 基于Spring EL 多对象模式优惠券规则校验工具类 * 支持同时传入 user / coupon / order 三个业务对象,表达式直接读取对象属性 */ @Slf4j @Component public class CouponSpelRuleUtil { @Autowired private ExpressionParser expressionParser; /** * 多对象入参统一校验方法 * @param ruleExpression 数据库存储SpEL规则表达式 * @param user 当前操作用户对象 * @param coupon 当前待校验优惠券对象 * @param order 当前下单订单对象 * @return true=优惠券可用 false=不满足规则不可用 */ public boolean checkCouponAvailable(String ruleExpression, User user, Coupon coupon, Order order) { // 无自定义规则,直接放行 if (ruleExpression == null || ruleExpression.trim().isEmpty()) { return true; } try { // 初始化表达式上下文 StandardEvaluationContext evalContext = new StandardEvaluationContext(); // 绑定多个业务对象,指定变量名,表达式中直接使用 evalContext.setVariable("user", user); evalContext.setVariable("coupon", coupon); evalContext.setVariable("order", order); // 解析表达式并执行判断 Expression expression = expressionParser.parseExpression(ruleExpression); Boolean result = expression.getValue(evalContext, Boolean.class); // 空结果默认判定不可用 return Boolean.TRUE.equals(result); } catch (Exception e) { log.error("优惠券SpEL规则解析校验失败,表达式:{},异常信息:{}", ruleExpression, e.getMessage(), e); // 语法错误、对象为空、属性不存在等异常,统一返回不可用 return false; } } } |

七、业务服务层调用实战

1. 优惠券业务 Service

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor public class CouponService { private final CouponMapper couponMapper; private final CouponSpelRuleUtil couponSpelRuleUtil; /** * 下单时校验优惠券是否可使用 * @param couponId 待使用优惠券ID * @param userId 当前用户ID * @param orderId 订单ID * @return 校验结果+提示文案 */ public ResultVO checkCouponUse(Long couponId, Long userId, Long orderId) { // 1. 查询优惠券基础数据 Coupon coupon = couponMapper.selectById(couponId); if (coupon == null || coupon.getStatus() == 0) { return ResultVO.fail("优惠券不存在或已下架"); } // 2. 查询用户、订单完整业务对象 User user = userMapper.selectById(userId); Order order = orderMapper.selectById(orderId); if (user == null || order == null) { return ResultVO.fail("用户或订单信息异常"); } // 3. Spring EL多对象动态校验规则 boolean available = couponSpelRuleUtil.checkCouponAvailable(coupon.getRuleExpression(), user, coupon, order); if (available) { return ResultVO.success("优惠券校验通过,可正常使用"); } return ResultVO.fail("不满足优惠券使用条件,无法使用"); } } |

2. 单元测试验证多对象属性判断

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; @SpringBootTest public class CouponSpelTest { @Autowired private CouponSpelRuleUtil couponSpelRuleUtil; @Test void testMultiObjectRuleCheck() { // 1. 组装用户对象 User user = new User(); user.setLevel(3); user.setIsBlack(false); user.setIsNewUser(false); // 2. 组装优惠券对象 Coupon coupon = new Coupon(); coupon.setCategory("FOOD"); coupon.setMaxUseCount(10); coupon.setValidStartTime(LocalDateTime.of(2026, 6, 1, 0, 0)); coupon.setValidEndTime(LocalDateTime.of(2026, 6, 30, 23, 59)); // 3. 组装订单对象 Order order = new Order(); order.setTotalAmount(new BigDecimal("259")); order.setGoodsCategories(List.of("FOOD", "DRINK")); order.setCreateTime(LocalDateTime.of(2026, 6, 10, 14, 30)); // 4. 数据库存储的SpEL表达式 String ruleExpr = "order.totalAmount >= 199 and user.level >= 3 and !user.isBlack and order.createTime between coupon.validStartTime and coupon.validEndTime and coupon.category in order.goodsCategories"; boolean pass = couponSpelRuleUtil.checkCouponAvailable(ruleExpr, user, coupon, order); System.out.println("优惠券是否可用:" + pass); // 输出 true,校验通过 } } |

八、多对象模式常用 SpEL 语法合集

1. 对象基础属性比较

|----------------------------------------------------------------------------------------------------------------------------------|
| spel // 订单金额门槛 order.totalAmount >= 99 // 用户会员等级限制 user.level >= 2 // 黑名单拦截 !user.isBlack // 优惠券剩余可用次数 coupon.maxUseCount > 0 |

2. 空安全运算符(避免空指针)

|---------------------------------------------------------------------------------|
| spel // user为null时直接返回false,不会抛出NPE user?.level >= 3 order?.totalAmount >= 50 |

3. 集合包含判断(类目匹配)

|--------------------------------------------------------------------|
| spel // 优惠券限定类目在订单商品类目列表中 coupon.category in order.goodsCategories |

4. 时间区间判断

|----------------------------------------------------------------------------------------------|
| spel // 下单时间落在优惠券有效期内 order.createTime between coupon.validStartTime and coupon.validEndTime |

5. 复杂复合规则

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| spel order.totalAmount >= 299 and user.level >= 2 and !user.isBlack and coupon.maxUseCount > 0 and order.createTime between coupon.validStartTime and coupon.validEndTime and coupon.category in order.goodsCategories |

九、方案优缺点分析

✅ 优点

  • 业务语义清晰 :直接传入User/Coupon/Order领域对象,表达式对象.属性贴合业务,可读性远高于单一体封装参数
  • 轻量化无依赖:Spring 原生能力,无需引入第三方规则引擎
  • 规则完全解耦:规则存储数据库,运营修改规则无需改代码、重启服务
  • 扩展性强 :后续新增Shop、Member等对象,仅需新增setVariable绑定,工具类无需大幅改造
  • 适配分层架构:符合项目原有 DO/VO 分层,无需额外封装统一上下文 DTO

❌ 缺点 & 生产优化方案

  • 表达式存在执行安全风险
    风险:恶意表达式可调用对象setter、反射方法篡改数据
    优化:自定义SpelParserConfiguration,限制表达式仅允许读取 getter,屏蔽修改类方法、反射、静态类执行
  • 重复解析表达式损耗性能
    优化:增加本地缓存,key 为表达式字符串,缓存解析后的Expression对象,避免重复解析字符串
  • 复杂长表达式可读性差
    优化:数据库增加rule_desc字段,存储中文规则描述,后台配置页面同时展示表达式 + 说明

十、高阶生产优化补充

1. 表达式本地缓存(高并发下单优化)

使用Caffeine缓存解析完成的Expression,大幅降低下单接口 CPU 消耗,避免重复解析字符串表达式。

2. 自定义安全解析器(线上必备)

重写 SpEL 解析器,禁止表达式调用set、delete、反射、系统静态方法,仅开放属性读取能力,防止注入攻击。

3. 支持工具类静态方法拓展

可绑定自定义工具静态类,在表达式中调用工具方法,拓展复杂判断能力:

|-----------------------------------------------------------------------------------------------------------------------|
| spel // 表达式调用自定义工具类判断是否叠加券 T(com.market.util.CouponUtil).canStackCoupon(coupon.couponId) and order.totalAmount >= 99 |

十一、总结

  • 电商优惠券、营销活动这类规则多变的业务,摒弃硬编码if-else分支,采用 Spring EL 动态规则是中小型项目最优落地方案。
  • 多对象入参模式相比单一上下文 DTO,更贴合领域分层设计,表达式语义直观,新增业务参数无需修改统一上下文实体,维护成本更低。
  • 规则持久化数据库实现热更新,运营自主配置活动规则,完全解放研发人力。
  • 该方案可无缝复用至红包、满减活动、会员权益、积分兑换等全部营销场景。