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

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

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

传统硬编码方案痛点

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

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

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

无需 Drools、QLExpress 重型规则引擎,基于 Spring 原生 Spring EL 表达式,支持同时传入UserCouponOrder多个业务对象,规则存入数据库,运行时动态解析判断优惠券是否可用,改规则只改数据库,服务无需重启、无需改代码,轻量、零依赖、适配 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();
    }
}

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

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

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 原生能力,无需引入第三方规则引擎
  • 规则完全解耦:规则存储数据库,运营修改规则无需改代码、重启服务
  • 扩展性强:后续新增ShopMember等对象,仅需新增setVariable绑定,工具类无需大幅改造
  • 适配分层架构:符合项目原有 DO/VO 分层,无需额外封装统一上下文 DTO

❌ 缺点 & 生产优化方案

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

十、高阶生产优化补充

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

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

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

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

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

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

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

十一、总结

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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐