电商优惠券实战设计

运营侧配置发放优惠券

核心表设计

1、优惠券模版表

create table coupon_template
(
    id            bigint unsigned auto_increment comment '模板ID'
        primary key,
    title         varchar(64)                        not null comment '优惠券名称',
    type          tinyint                            not null comment '类型(1-满减 2-折扣 3-现金券)',
    rule_json     text                               not null comment '规则配置(JSON格式)
// 满减券
{
  "full_amount": 10000,  // 单位分
  "discount_amount": 5000
}

// 折扣券 
{
  "max_discount": 10000, // 最高优惠金额(分)
  "discount_rate": 0.8    // 8折
}',
    platform      tinyint                            not null comment '使用平台(0-全平台 1-App 2-PC)',
    source_type   int                                not null comment '来源类型(1-平台券,2-店铺券)',
    total         int unsigned                       not null comment '发放总量',
    remaining     int unsigned                       not null comment '剩余数量',
    valid_type    tinyint                            not null comment '有效期类型(1-固定日期 2-领取后生效)',
    valid_start   datetime                           null comment '有效期开始时间',
    valid_end     datetime                           null comment '有效期结束时间',
    valid_days    smallint unsigned                  null comment '领取后有效天数',
    apply_range   tinyint                            not null comment '适用范围(0-全场 1-指定品类 2-指定商品)',
    coupon_status int      default 1                 not null comment '状态(1待发放 2发放中 3已暂停发放)',
    create_time   datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time   datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '编辑时间',
    publish_time  datetime                           not null comment '发放时间',
    tantent_uuid  varchar(36)                        not null comment '租户uuid'
)
    comment '优惠券模板表';

create index idx_remaining
    on coupon_template (remaining);

create index idx_valid_time
    on coupon_template (valid_end, valid_start);

几个时间说明

创建/编辑时间 不说了 

发放时间 指什么时候可以领劵 一般来说 券创建好了 用户就可以看到了 但是还没到发放时间 还不能领 就像12306一样 一般用定时任务控制

有效开始/结束时间 指的是这个券在这个时间段还能用

这几个时间是有先后顺序的 前端页面表单和后端接口都要验证一下这些时间

rule的json示例

// 满减券(满100减20)
{
  "full_amount": 10000,  // 单位分
  "discount_amount": 2000
}

// 折扣券 (打8折 最高减100)
{
  "max_discount": 10000, // 最高优惠金额(分)
  "discount_rate": 0.8    // 8折
}

2、优惠券使用限制表

create table coupon_apply_limit
(
    id              bigint unsigned auto_increment
        primary key,
    coupon_id       bigint unsigned        not null comment '模板ID',
    is_stackable    char       default '0' not null comment '是否可叠加(可叠加的券才可叠加 0不可 1可)',
    priority        tinyint(2) default 1   null comment '使用优先级(如果使用了多张可叠加的券 按照他们的优先级依次使用)(1-20 数字越大优先级越高)',
    limit_rule_json text                   null comment '使用限制规则json
(只给 某种商品用 productId = ''商品id''
只给 某类商品用 categoryId = ''类型id''
只给 某店铺商品用 storeId = ''店铺id''
价格限制 orderPrice = ''值''
数量限制 productNum = ''值''
用户等级限制 userVip = ''值'')',
is_cross    char       default '0' not null comment '是否可跨店(0不可 1可)',
    remark          varchar(1024)          null comment '说明'
)
    comment '优惠券使用限制';

create index idx_coupon
    on coupon_apply_limit (coupon_id);

相当与优惠券的拓展表 记录优惠券在使用上与业务主体(此处就是商品)的关系 一对一关系 后面在使用优惠券时查本表校验

模拟一些场景 

一般来说  

  1. storeId = ''店铺id''  适用于 店铺发券只能买自己店铺的东西用。 或者新店铺开业 平台发该新店铺券

  2. categoryId = ''类型id''  比如6.1儿童节 对儿童玩具 发券 店铺/平台都可以发

  3. product = “商品id” 平台/店铺为促销某种商品

  4. orderPrice = ''值'' 这个其实更像满减卷 必须满这个多才能用 但是不一定直减也可能打折 主要用拼商品 比如买一个钱不够 再买一个凑单 (凑单满减后再退款 懂得都懂)

  5. productNum = ''值'' 走量的券 可以是同商品 也可以是不同商品 

  6. userVip = ''值'' 一些东西只能会员买 

跨店说明

  1. 店铺劵一般不可跨店 此时is_cross字段为0  storeId = 该店铺id  coupon_template.tantent_uuid为该租户的id   
  2. 平台劵一般为可跨店的 此时is_cross字段为0 

其他说明

  1. 对应1 2 3一般来说用等于关系;  4 5 6大于/等于/小于关系都可以

  2. 可以 多个商品用一张券 也可以一个商品用多张券 还可以多个商品用多张券 当然还有最基本的一个商品用一张券 只要符合规则

  3. 对应普通的值的大小判断 有计算工具 但对于一下特殊情况 需要特殊处理

  4. 多个商品 使用类型为 1 2 3类型的券 但是不匹配  比如 多个商品分别属于 类型 A B C 但是用了类型为A的券 肯定不给过 代码逻辑是 遍历券看看有无对 1 2 3种的限制 如果有遍历每一个商品看是否符合 不符合就报错 结束(仅多个商品需要提前走这一步判断 对应一个产品 直接等值判断就行有工具类)

  5. 多个优惠券 里面有是否可叠加属性 有一个不可叠加的就报错 结束

3、优惠券流水记录表

CREATE TABLE `coupon_log` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` BIGINT UNSIGNED NOT NULL,
  `coupon_id` BIGINT UNSIGNED NOT NULL,
  `action` TINYINT NOT NULL COMMENT '操作类型(1-领取 2-使用 3-过期)',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `extra_info` VARCHAR(255) COMMENT '附加信息(如订单ID)',
  PRIMARY KEY (`id`),
  INDEX `idx_user_coupon` (`user_id`, `coupon_id`)
) ENGINE=InnoDB COMMENT='优惠券操作日志表';

记录优惠券的流水 平台可以基于流水进行统计 看哪些商品的优惠券领取/使用的人多 extra_info使用了才存对应订单的信息 这个和之前做的积分流水差不多(可以看我之前的博客)

4、用户优惠券表

CREATE TABLE `user_coupon` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
  `coupon_id` BIGINT UNSIGNED NOT NULL COMMENT '模板ID',
  `status` TINYINT NOT NULL COMMENT '状态(0-未使用 1-已使用 2-已过期)',
  `obtain_type` TINYINT NOT NULL COMMENT '获取方式(1-主动领取 2-系统发放)',
  `obtain_time` DATETIME NOT NULL COMMENT '领取时间',
  `start_time` DATETIME NOT NULL COMMENT '有效期开始',
  `end_time` DATETIME NOT NULL COMMENT '有效期结束',
  `order_id` BIGINT UNSIGNED COMMENT '使用订单ID',
  `use_time` DATETIME COMMENT '使用时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_code` (`coupon_code`),
  INDEX `idx_user_status` (`user_id`, `status`),
  INDEX `idx_endtime` (`end_time`)
) ENGINE=InnoDB COMMENT='用户持有优惠券表';

这个表看似和流水表很像 但这个从业务上来说 是给用户自己看的 方便管理自己的优惠券 而且他的字段更多

核心接口设计

运营侧

优惠券模版crud  发放/停止

统计券领取/使用情况

商家侧

同运营侧

用户侧

查看所有券 领取发放中的券 查看自己领取券

其他

运营侧通过RBAC权限模式 为租户开放优惠券的权限 包括租户侧的申请 运营侧的审核 这次不细说了 主要针对优惠券业务聊

核心业务

1、优惠券领取防止超卖

// Redis + Lua脚本保证原子性
String script = 
  "if redis.call('exists', KEYS[1]) == 1 then " +
  "   local remaining = tonumber(redis.call('get', KEYS[1])) " +
  "   if remaining <= 0 then return 0 end " +
  "   redis.call('decr', KEYS[1]) " +
  "   return 1 " +
  "end " +
  "return -1";

Long result = redisTemplate.execute(
  new DefaultRedisScript<>(script, Long.class),
  Collections.singletonList("coupon:stock:" + couponId)
);

if (result == 1) {
  // 执行数据库领取操作
} else if (result == 0) {
  throw new BusinessException("优惠券已领完");
} else {
  // 初始化Redis库存
}

直接使用lua保证原子性  也可以使用Redisson 底层都是lua   之前发过分布式锁相关的内容 可以看我之前的博客

2、优惠券限量领取

也用lua脚本实现 大概逻辑 首先先看看是否满足库存(就是上一步的逻辑)然后去流水表里面看该用户是否已经领取了 此时锁对象应该锁 用户id拼接优惠券id

加锁 加事务 看是否能满足条件 操作数据 提交事务 释放锁 

3、统计使用率

SELECT 
    t.id AS coupon_id,
    t.title,
    COUNT(DISTINCT l.user_id) AS receive_users,
    SUM(CASE WHEN uc.status=1 THEN 1 ELSE 0 END) AS used_count,
    CONCAT(ROUND(SUM(CASE WHEN uc.status=1 THEN 1 ELSE 0 END)/COUNT(*)*100,2),'%') AS use_rate
FROM coupon_template t
LEFT JOIN user_coupon uc ON t.id = uc.coupon_id
LEFT JOIN coupon_log l ON t.id = l.coupon_id
GROUP BY t.id;

缓存结构设计

缓存类型存储内容过期策略
Redis优惠券模板基本信息固定过期
Local高频访问的优惠券规则LRU自动淘汰
Redis用户可用优惠券列表(user_id)30分钟 + 延迟双删

流程图

用户使用优惠券

用券流程图

核心逻辑

建议多看看 优惠券使用限制 表里面的说明

接收 OrderContext 对象 
优惠券是否 过期 coupon_template
是否满足 使用条件 coupon_apply_limit
过滤出可用的优惠券

按照 priority 依次算钱


核心代码

规则条件拓展

public class RuleCondition {
    private String field;
    private Object value;
    private Operator operator;
    private LogicType logic;
    private List<RuleCondition> subConditions;

    public boolean isLogicalNode() {
        return logic != null && subConditions != null && !subConditions.isEmpty();
    }
}

enum Operator {
    GT, EQ, LT
}

enum LogicType {
    AND, OR
}

//实例
//已知有规则条件[用户等级为VIP OR 订单金额>1000) AND 商品类目为200]的商品
        RuleCondition rootCondition = new RuleCondition(
                LogicType.AND,
                Arrays.asList(
                        new RuleCondition(
                                LogicType.OR,
                                Arrays.asList(
                                        new RuleCondition("userVipLevel", Operator.EQ, 1),
                                        new RuleCondition("orderPrice", Operator.GT, 1000)
                                )
                        ),
                        new RuleCondition("categoryId", Operator.EQ, 200)
                )
        );


//满足规则条件 {(用户等级为VIP==3 AND 订单金额>5000) OR 店铺ID=1001)] AND 商品类目为200} 的商品
        RuleCondition rootCondition2 = new RuleCondition(
                LogicType.AND,
                Arrays.asList(
                        new RuleCondition(
                                LogicType.OR,
                                Arrays.asList(
                                        new RuleCondition(LogicType.AND, Arrays.asList(
                                                new RuleCondition("userVipLevel", Operator.EQ, 3),
                                                new RuleCondition("orderPrice", Operator.GT, 5000L)
                                        )),
                                        new RuleCondition("storeId", Operator.EQ, 1001)
                                )
                        ),
                        new RuleCondition("categoryId", Operator.EQ, 200)
                )
        );

条件判断

public class RuleEvaluator {



    /**
     * 根据给定的规则条件和用户订单上下文来评估规则条件
     * 此方法首先检查条件节点是否有效,如果无效,则抛出异常
     * 然后,根据条件节点是逻辑节点还是叶节点来调用不同的评估方法
     *
     * @param condition 规则条件节点,用于评估的条件
     * @param context 用户订单上下文,包含评估条件所需的信息
     * @return boolean 评估结果,如果条件为真,则返回true;否则返回false
     * @throws IllegalArgumentException 如果节点规则无效
     */
    public boolean evaluate(RuleCondition condition, UserOrderContext context) {
        // 检查节点是否无效,如果无效则抛出异常
        if (isInvalidNode(condition)) {
            Assert.isTrue(false,"非法节点规则");
        }

        // 根据节点类型调用相应的评估方法
        if (condition.isLogicalNode()) {
            return evaluateLogicalCondition(condition, context);
        } else {
            return evaluateLeafCondition(condition, context);
        }
    }

    private boolean isInvalidNode(RuleCondition condition) {
        // 同时存在逻辑条件和字段条件属于非法节点
        return condition.getLogic() != null && condition.getField() != null;
    }

    /**
     * 评估逻辑条件是否满足
     * 此方法根据逻辑类型(AND或OR)评估一组子条件是否满足对于AND逻辑,所有子条件都必须满足才返回true;
     * 对于OR逻辑,至少有一个子条件满足就返回true
     *
     * @param condition 规则条件,包含逻辑类型和子条件
     * @param context 用户订单上下文,提供评估条件所需的信息
     * @return boolean 返回条件评估结果,满足则为true,否则为false
     */
    private boolean evaluateLogicalCondition(RuleCondition condition, UserOrderContext context) {
        // 获取逻辑类型
        LogicType logic = condition.getLogic();
        // 获取子条件列表
        List<RuleCondition> subConditions = condition.getSubConditions();

        // 根据逻辑类型进行条件评估
        if (logic == LogicType.AND) {
            // 对于AND逻辑,所有子条件都必须满足
            for (RuleCondition sub : subConditions) {
                // 如果有任一子条件不满足,则整体条件不满足,返回false
                if (!evaluate(sub, context)) {
                    return false;
                }
            }
            // 所有子条件都满足,返回true
            return true;
        } else { // OR逻辑
            // 对于OR逻辑,至少有一个子条件满足即可
            for (RuleCondition sub : subConditions) {
                // 如果有任一子条件满足,则整体条件满足,返回true
                if (evaluate(sub, context)) {
                    return true;
                }
            }
            // 所有子条件都不满足,返回false
            return false;
        }
    }

    /**
     * 评估叶子条件
     * 此方法用于评估规则条件中的叶子节点,即最简单的条件判断它通过反射机制获取用户订单上下文中指定字段的值,
     * 并根据条件操作符(等于或比较)与规则条件中的值进行对比
     *
     * @param condition 规则条件,包含字段名、操作符和值
     * @param context 用户订单上下文,包含各种字段信息
     * @return 返回条件评估的结果,如果满足条件则为true,否则为false
     */
    private boolean evaluateLeafCondition(RuleCondition condition, UserOrderContext context) {
        // 获取条件中的字段名、值和操作符
        String fieldName = condition.getField();
        Object conditionValue = condition.getValue();
        Operator operator = condition.getOperator();

        try {
            // 通过反射获取UserOrderContext类中与条件字段名对应的字段
            Field field = UserOrderContext.class.getDeclaredField(fieldName);
            // 设置字段可访问,以绕过访问修饰符限制
            field.setAccessible(true);
            // 获取字段的实际值
            Object fieldValue = field.get(context);

            // 根据操作符类型执行相应的比较逻辑
            if (operator == Operator.EQ) {
                // 如果操作符是等于,则调用checkEquals方法比较字段值和条件值是否相等
                return checkEquals(fieldValue, conditionValue);
            } else {
                // 如果操作符是数值比较,则调用compareNumbers方法进行数值比较
                return compareNumbers(fieldValue, conditionValue, operator);
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            // 如果发生字段不存在或访问异常,则认为条件不满足,返回false
            return false;
        }
    }

    /**
     * 检查字段值是否等于条件值
     * 首先使用Objects.equals进行比较,如果相等则返回true
     * 如果不相等,尝试将字段值和条件值转换为数字,并进行比较
     * 如果转换成功且值相等,则返回true,否则返回false
     *
     * @param fieldValue   字段值,可以是任何类型,但通常为数字类型
     * @param conditionValue   条件值,可以是任何类型,但通常为数字类型
     * @return 如果字段值等于条件值,则返回true;否则返回false
     */
    private boolean checkEquals(Object fieldValue, Object conditionValue) {
        // 使用Objects.equals进行比较,以处理null值和相同类型值的比较
        if (Objects.equals(fieldValue, conditionValue)) {
            return true;
        }
        // 尝试将字段值和条件值转换为数字
        Number fieldNumber = convertToNumber(fieldValue);
        Number conditionNumber = convertToNumber(conditionValue);
        // 如果转换成功,则比较两个数字的double值
        if (fieldNumber != null && conditionNumber != null) {
            return fieldNumber.doubleValue() == conditionNumber.doubleValue();
        }
        // 如果转换失败,或者值不相等,则返回false
        return false;
    }

    /**
     * 比较两个数值的大小
     * 此方法主要用于在给定的操作符下,比较两个可能具有不同包装类型的数值
     * 它首先将两个对象转换为Number类型,然后根据操作符判断两者之间的大小关系
     *
     * @param fieldValue   字段值,可以是任何包装类型的数值
     * @param conditionValue   条件值,可以是任何包装类型的数值
     * @param operator   操作符,用于指定比较的方式(大于或小于)
     * @return 根据操作符比较两个数值后的布尔结果如果输入不是有效的数值类型,则返回false
     */
    private boolean compareNumbers(Object fieldValue, Object conditionValue, Operator operator) {
        // 将字段值和条件值转换为Number类型
        Number fieldNumber = convertToNumber(fieldValue);
        Number conditionNumber = convertToNumber(conditionValue);

        // 如果任一数值转换失败,则返回false
        if (fieldNumber == null || conditionNumber == null) {
            return false;
        }

        // 将Number类型转换为double类型,以便进行比较
        double fieldVal = fieldNumber.doubleValue();
        double conditionVal = conditionNumber.doubleValue();

        // 根据操作符判断两个数值的大小关系
        switch (operator) {
            case GT: return fieldVal > conditionVal;
            case LT: return fieldVal < conditionVal;
            default: return false;
        }
    }

    /**
 * 将对象转换为数字类型。
 *
 * 该方法用于将输入对象转换为数字类型。如果输入对象已经是Number类型,则直接返回;
 * 如果是字符串类型,尝试将其解析为Double类型;如果解析失败或输入对象为其他类型,则返回null。
 *
 * @param value 需要转换的对象,可以是Number或String类型。
 * @return 如果转换成功,返回对应的Number类型;否则返回null。
 */
private Number convertToNumber(Object value) {
    // 如果输入对象是Number类型,直接返回
    if (value instanceof Number) {
        return (Number) value;
    } else if (value instanceof String) {
        try {
            // 如果输入对象是字符串类型,尝试将其解析为Double类型
            return Double.parseDouble((String) value);
        } catch (NumberFormatException e) {
            // 如果解析失败,返回null
            return null;
        }
    }
    // 如果输入对象既不是Number也不是字符串类型,返回null
    return null;
}

}

enum Operator {
    GT, EQ, LT
}

enum LogicType {
    AND, OR
}

// Assume Lombok's @Data generates getters/setters
@Data
class RuleCondition {
    private String field;
    private Object value;
    private Operator operator;
    private LogicType logic;
    private List<RuleCondition> subConditions;

    public boolean isLogicalNode() {
        return logic != null && subConditions != null && !subConditions.isEmpty();
    }

    public RuleCondition(String field, Operator operator, Object value) {
        this.field = field;
        this.operator = operator;
        this.value = value;
    }

    public RuleCondition(LogicType logic, List<RuleCondition> subConditions) {
        this.logic = logic;
        this.subConditions = subConditions;
    }
}

@Data
class UserOrderContext {
    private Integer productId;
    private Integer categoryId;
    private Integer storeId;
    private Long orderPrice;
    private Integer userVipLevel;
}

重要说明

在构建RuleCondition时 里面的field里面的值必须和UserOrderContext里面的字段保持一致 建议使用枚举

订单上下文

/**
 * 订单计算上下文
 */
public class OrderContext {
    private Long userId;
    private List<OrderItem> items;      // 商品明细列表
    private List<Coupon> coupons;       // 用户选择的优惠券列表
    private BigDecimal originAmount;    // 原始金额
    private BigDecimal shippingFee;     // 运费
    // 其他业务字段...
}

优惠券

public class Coupon {
    private Long id;
    private CouponType type;            // 优惠类型
    private Map<String, RuleCondition> rules;  // 券使用限制规则
    private BigDecimal discountAmount;  // 计算后的优惠金额
    // 其他字段...
}

public enum CouponType {
    FULL_REDUCTION,  // 满减
    DISCOUNT,        // 折扣
    CASH,           // 现金券
    FREE_SHIPPING   // 包邮
}

最终金额计算结果

/**
 * 金额计算结果
 */
public class OrderAmount {
    private BigDecimal currentAmount;   // 当前计算中的金额(原价)
    private BigDecimal finalAmount;     // 最终支付金额(用券后价)
    private List<Coupon> usedCoupons = new ArrayList<>();  //使用劵集合
    //折扣明细 key 券id value 用这个券便宜了多少钱
    private Map<String, BigDecimal> discountDetails = new HashMap<>(); 

}

优惠计算核心

/**
 * 优惠计算引擎核心类
 */
public class CouponEngine {
    // 注入依赖服务
    private CouponTemplateService couponTemplateService;
    private RuleEvaluator ruleEvaluator;

    /**
     * 计算订单最优优惠
     *
     * @param orderContext 订单上下文(包含商品、用户、优惠券等信息)
     * @return 最终订单金额计算结果
     */
    public OrderResult calculate(OrderContext orderContext) {
        List<Coupon> couponList = orderContext.getCoupons();
        List<OrderItem> items = orderContext.getItems();
        // 1. 并行校验所有优惠券有效性(是否过期+用户是否满足使用条件)
        //时间校验通过的数据
        List<Coupon> dbCoupons = couponTemplateService.validateCoupons(couponList);
        //通过coupon集合对象获取对应的使用限制集合对象
        List<couponApplyLimit> couponsLimit = couponTemplateService.getCouponsLimitBy(dbCoupons);
        List<Coupon> usedCoupons = orderContext.getCoupons();
        if (CollUtil.isNotEmpty(usedCoupons) && usedCoupons.size() > 0) {
            //如果券是1 2 3类型
            couponsLimit.forEach(couponApplyLimit -> {
                if (couponApplyLimit.getStackable()==0){
                    Assert.isTrue(false, "券不可叠加");
                }
                if (couponApplyLimit.getCouponType() == 1 || couponApplyLimit.getCouponType() == 2 || couponApplyLimit.getCouponType() == 3) {
                    items.forEach(productItem -> {
                        if (couponApplyLimit.getProductId() != productItem.getProductId()) {
                            Assert.isTrue(false, "商品Id不匹配");
                        }
                        //依次判断商品类型/商品店铺
                    });
                }
            });
        }
        //orderContext转换userOrdercontext对象
        UserOrderContext userOrdercontext = ruleEvaluator.convert(orderContext);
        ArrayList<CouponDTO> finallyCouponDTOS = new ArrayList<CouponDTO>();
        couponsLimit.forEach(couponApplyLimit -> {
            boolean isUseValid = ruleEvaluator.evaluate(couponApplyLimit.getRuleCondition(), userOrdercontext);
            if (isUseValid){
                CouponDTO couponDTO = new CouponDTO();
                //数据塞值转换
                finallyCouponDTOS.add(couponDTO);
            }else {
                Assert.isTrue(false, "券不满足使用条件");
            }

        });


        // 3. 应用优惠规则进行计算
        OrderAmount amount = applyCoupons(orderContext, finallyCouponDTOS);

        // 4. 返回计算结果
        return new OrderResult(
                //用券后金额
                amount.getFinalAmount(),
                //券使用情况
                amount.getUsedCoupons(),
                //用券明细
                amount.getDiscountDetails()
        );
    }

    /**
     * 使用符合条件的优惠券算钱
     */
    private OrderAmount applyCoupons(OrderContext orderContext,
                                     List<CouponDTO> finallyCouponDTOS) {
        OrderAmount orderAmount = new OrderAmount();
        BigDecimal originAmount = orderContext.getOriginAmount();
        orderAmount.setCurrentAmount(originAmount);
        List<Coupon> usedCoupons = new ArrayList<>();
        Map<String, BigDecimal> finalDiscountMap = new HashMap<>();
        // 按优先级排序规则
        finallyCouponDTOS.sort(Comparator.comparingInt(CouponDTO::getPriority).reversed());
        for (CouponDTO applyCoupon : finallyCouponDTOS) {
            Map<String, BigDecimal> discountMap = applyCoupon(applyCoupon,originAmount);
            usedCoupons.add(applyCoupon);
            finalDiscountMap.putAll(discountMap);
            originAmount = originAmount.subtract(applyCoupon.getDiscountAmount());
        }
        orderAmount.setUsedCoupons(usedCoupons);
        orderAmount.setDiscountDetails(finalDiscountMap);
        orderAmount.setFinalAmount(originAmount);
        return orderAmount;
    }


    /**
     * 使用券 算钱逻辑
     */
    public Map<String, BigDecimal>  applyCoupon(Coupon coupon, BigDecimal originAmount) {
        BigDecimal before = originAmount;
        BigDecimal currentAmount = null;
        Map<String, BigDecimal> discountDetails = new HashMap<>();
        switch (coupon.getType()) {
            //满减逻辑
            case FULL_REDUCTION:
                BigDecimal threshold = (BigDecimal) coupon.getRules().get("fullAmount");
                BigDecimal discount = (BigDecimal) coupon.getRules().get("discountAmount");
                //满判断
                if (originAmount.compareTo(threshold) >= 0) {
                    //减操作
                    currentAmount = before.subtract(discount);
                }
                break;
            //折扣逻辑
            case DISCOUNT:
                BigDecimal rate = (BigDecimal) coupon.getRules().get("discountRate");
                BigDecimal maxDiscount = (BigDecimal) coupon.getRules().get("maxDiscount");
                //打折
                BigDecimal discount = currentAmount.multiply(BigDecimal.ONE.subtract(rate));
                //是否超过打折最大值
                discount = discount.min(maxDiscount);
                //减
                currentAmount = before.subtract(discount);
                break;
        }

        // 记录优惠明细
        discountDetails.put(coupon.getId(), before.subtract(currentAmount));
        return discountDetails;

    }
    
}

拓展

本人玩某宝 某东不多 

这是最简单的实现 还有很多功能未涉及

  • 梯度满减
  • 兑换码券
  • 涉及虚拟货币(使用积分)
  • 计算消费券叠加最优解
  • 拼团券
  • 第二份半价
  • .....
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值