一、业务背景:传统优惠券规则有多痛苦?
电商系统优惠券场景,规则五花八门、迭代极快:
- 满减门槛:订单金额≥99 元可用、≥199 元可用
- 用户限制:仅新用户 / 会员等级≥3 级可用、黑名单用户禁用
- 时间限制:仅限活动期、下单时间在券有效期内
- 商品限制:优惠券限定类目,订单商品需匹配类目
- 叠加 / 库存限制:优惠券剩余使用次数大于 0、不可与其他券叠加
传统硬编码方案痛点
业务规则写死在if/else、枚举、业务代码中:
- 新增优惠券规则、修改门槛必须改代码、重启服务、灰度发布
- 运营配置优惠券、临时调整活动规则,依赖研发排期,效率极低
最优解: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,更贴合领域分层设计,表达式语义直观,新增业务参数无需修改统一上下文实体,维护成本更低。
- 规则持久化数据库实现热更新,运营自主配置活动规则,完全解放研发人力。
- 该方案可无缝复用至红包、满减活动、会员权益、积分兑换等全部营销场景。
所有评论(0)