电商促销后台设计,写得太好了!

简介: 电商促销后台设计,写得太好了!

电商所谓营销,归根结底都是订单金额的变化;如果我们清楚的知道订单金额的计算流程是怎样的,那么我们只需要顺着系统的计算流程做促销,就不用担心各种促销类型之间产生重叠或者冲突的情况了。


当我们知道这个关系后,就可以将营销活动区分为三种类型:改商品价格、改商品小计价格、改订单价格,因为无论什么营销归根结底都是可以描述成改价格。


购物车中任何增删查改都要重新计算促销,所以促销的计算变得尤为重要,感觉京东已经把促销做到了极致。


从模式上来讲,我们公司的促销就相当于京东自营,所以很多也都是参考京东自营的,但我们还没法做到像京东促销那样强大。


这里,将我们做的促销跟大家分享一下,只涉及后台接口逻辑部分。


接口的功能就是输入商品列表,返回加了促销分组后的商品列表。


首先要声明几点:


一、不是通用的促销设计,只是我们公司目前支持的促销设计及逻辑;


二、作者水平有限,不会画图,所以图画得比较丑,也很粗,希望大家不要介意;


三、不谈性能


废话就不多说了,下面正式开始。。。


促销类型image.pngimage.png

同类型通过实体进行互斥、不同类型可以相互叠加。”这是别人总结的设计电商促销系统的基本原则,我也比较认同。

上面接口主流程就是先应用单品促销,再应用条件促销。稍微再细化一点儿就是这样的:

image.png先处理赠品促销,将赠品挂载到主商品(原先用户添加的购物车中的商品我称之为主商品)上,再应用单品促销。


在进行单品促销的时候,很有可能同一个商品命中多个单品促销。这个时候只能取一个促销,此处的计算逻辑是这样的:


优惠力度最大的优先

优惠力度相同时,取最新创建的那个(创建时间最新)

例如:


商品A命中四条促销,分别是:【促销1】直降2元,【促销2】折扣8折,【促销3】直降1元。假设A的原价时10元,那么经过计算【促销1】8元,【促销2】8元,【促销3】9元。这个时候,【促销3】应该被剔除,假设【促销2】的创建时间比【促销1】要晚,那么应该取【促销2】。即商品A最终命中【促销2】。原价10元,促销价8元。


计算商品价格流程image.png稍微解释一下:

  • 特价:商品A原价12元,今日特价9.9元。
  • 折扣:商品打几折。
  • 直降:商品A原价12元,今日直降3元,所以最终9元。且当促销价低于原价的70%时恢复原价。

限购流程image.png这里有两点需要说明:


限购的话需要查订单系统,但是刚才说了购物车中的任意增删查改都要重新计算促销,所以如果这里直接调订单的话可能订单的顶不住(技术实力还比较薄弱,无奈!!!),考虑到这里我们冗余了订单数据,每次从本地数据库去查。当然,这样肯定不准,但是我们只保证90%的情况就可以了,所以这里我们采用这种方式。

拆商品行。还是用上面的例子,商品A命中了【促销2】,假设【促销2】限购每人每单1件,而现在A 的数量时3,那么我们会拆成2行,第一行商品A售价8元数量1件,第二行商品A售价10元数量2件。

条件促销分组

同一个商品可能会命中多个条件促销,而最终每个商品只能应用一个条件促销(即每个商品最终只能属于一个组)


我们说,同种类型的促销不能叠加,不同类型的促销可以叠加。在我们这里,单品促销和单品促销不能叠加,条件促销与条件促销不能叠加,单品与条件可以叠加。


程序走到这里,我们已经完成了单品促销的处理,接下来处理条件促销。在决定商品应该最终应用哪个条件促销时,我们的原则是这样的:


1、优先考虑满足条件的促销


这句话的意思是,假设商品A,商品B满足【促销1】满100减20这个阶梯,同时A和B又都命中了【促销2】但是不满足【促销2】的条件,因为假设【促销2】的最小阶梯是满150减30。那么这个时候,虽然A和B都同时命中【促销1】和【促销2】,但A和B一起正好符合【促销1】满100减20的条件,所以这个时候促销A和B应该最终取【促销2】


2、同时满足多个条件促销时,取后创建的那个(创建时间最近)


还是上面的例子,假设A和B的总金额加起来是160元,那么它们都满足【促销1】和【促销2】,假设【促销2】是后创建的,所以此时它们最终命中的条件促销应该取【促销2】。并且,之后应该讲它们从【促销1】的商品组中剔除(PS:因为一个商品只能属于一个组,即只能应用一个条件促销)。京东在这里对每种促销做了计算,把最终用哪个促销的决定权交给用户去选,我们这里不搞这么复杂。


说了这么多,可能有点晕,下面举个例子

image.png假设有A,B,C,D四个商品,促销1234是四个促销


如图,【促销1】是所有商品,所有A,B,C,D四个都命中【促销1】,换句话说【促销1】的商品组中有A,B,C,D


【促销2】的商品组中有A,C


【促销3】的商品组中有A,B


【促销4】的商品组中有A,B,C


假设促销1,2,3,4是依次创建的,也就是说4是最晚创建的,1是最早创建的


再假设,A+B+C符合【促销4】的其中一个阶梯条件,A+B符合【促销3】中的其中一个阶梯条件,A+B+C+D符合【促销1】的其中最低一级的阶梯条件


那么,最终的促销分组应该是这样的:


【促销4】的商品组有:A,B,C


【促销3】的商品组为空


【促销2】的商品组为空


【促销1】的商品组中有:D,而且不满足最低的阶梯,因为原来A+B+C+D满足最低一级的阶梯,现在只剩下D了当然不满足最低一个的阶梯


条件促销分组计算image.png在代码实现上,这里是两层循环:

  • 第一层是条件促销列表
  • 第二层是某个条件促销中的商品组

部分代码实现

代码可能是这样的,下面贴出条件促销部分的代码片段:

//  处理条件促销
//  算小计
for (PromotionProductDTO promotionProductDTO : promotionProductDTOList) {
    promotionProductDTO.setSubtotal(promotionProductDTO.getPromotionPrice().multiply(new BigDecimal(promotionProductDTO.getQuantity())));
}
List<PromotionInfoDTO> conditionPromotionInfoDTOList = promotionInfoMap.get(PromotionTypeEnum.TIAOJIAN.getType());
//  限购
List<PromotionInfoDTO> validConditionPromotionInfoDTOList = new ArrayList<>();
for (PromotionInfoDTO promotionInfoDTO : conditionPromotionInfoDTOList) {
    if (isMaxConditionPromotionLimit(promotionInfoDTO, userId)) {
        continue;
    }
    validConditionPromotionInfoDTOList.add(promotionInfoDTO);
}
conditionPromotionInfoDTOList = validConditionPromotionInfoDTOList;
//  按范围初步将商品归到各个条件促销下(撒网)
for (PromotionInfoDTO promotionInfoDTO : conditionPromotionInfoDTOList) {
    List<PromotionProductDTO> matchedPromotionProductDTOList = new ArrayList<>();
    List<PromotionProductEntity> promotionProductEntityList = promotionInfoDTO.getDefinitiveProductEntityList();
    for (PromotionProductDTO promotionProductDTO : promotionProductDTOList) {
        //  商品匹配到的促销
        if (promotionInfoDTO.getProductRange() == PromotionPruductRangeEnum.ALL.getValue()) {
            matchedPromotionProductDTOList.add(promotionProductDTO);
        }else if (promotionInfoDTO.getProductRange() == PromotionPruductRangeEnum.CATEGORY.getValue()) {
            Set<String> secondCategorySet = promotionProductEntityList.stream().map(PromotionProductEntity::getProCategorySecond).collect(Collectors.toSet());
            if (secondCategorySet.contains(promotionProductDTO.getCategoryCode())) {
                matchedPromotionProductDTOList.add(promotionProductDTO);
            }
        }else if (promotionInfoDTO.getProductRange() == PromotionPruductRangeEnum.SPECIFIED.getValue()) {
            Set<Long> specialProductIdSet = promotionProductEntityList.stream().map(PromotionProductEntity::getProductId).collect(Collectors.toSet());
            if (specialProductIdSet.contains(promotionProductDTO.getId())) {
                matchedPromotionProductDTOList.add(promotionProductDTO);
            }
        }
    }
    //  促销匹配到的商品
    promotionInfoDTO.setMatchedProductDTOList(matchedPromotionProductDTOList);
    //  判断促销匹配的这些商品是否满足条件
    BigDecimal totalAmount = BigDecimal.ZERO;
    for (PromotionProductDTO promotionProductDTO : matchedPromotionProductDTOList) {
        totalAmount = totalAmount.add(promotionProductDTO.getSubtotal());
    }
    PromotionStairEntity promotionStairEntity = matchStair(promotionInfoDTO.getDefinitiveStairEntityList(), totalAmount);
    if (null != promotionStairEntity) {
        promotionInfoDTO.setPromotionStairEntity(promotionStairEntity);
    }
}
//  按满足条件与否以及促销创建的先后顺序进一步归档商品(即分组)
//  挑选出满足条件的促销,并按照创建时间降序排序
List<PromotionInfoDTO> matchedConditionPromotionInfoDTOList = conditionPromotionInfoDTOList.stream()
        .filter(x->null != x.getPromotionStairEntity())
        .sorted(Comparator.comparing(PromotionInfoDTO::getCreateTime).reversed())
        .collect(Collectors.toList());
//  去重,以保证每个组中的商品之间无交集
int len = matchedConditionPromotionInfoDTOList.size();
for (int i = 0; i < len - 1; i++) {
    PromotionInfoDTO majorPromotionInfoDTO = matchedConditionPromotionInfoDTOList.get(i);
    for (int j = i + 1; j < len; j++) {
        PromotionInfoDTO minorPromotionInfoDTO = matchedConditionPromotionInfoDTOList.get(j);
        for (PromotionProductDTO majorMatchedPromotionProductDTO : majorPromotionInfoDTO.getMatchedProductDTOList()) {
            minorPromotionInfoDTO.setMatchedProductDTOList(minorPromotionInfoDTO.getMatchedProductDTOList()
                    .stream()
                    .filter(x -> !x.getId().equals(majorMatchedPromotionProductDTO.getId()))
                    .collect(Collectors.toList()));
        }
    }
}
//  最终命中的促销
List<PromotionInfoDTO> ultimatePromotionInfoDTOList = new ArrayList<>();
//  重新计算各组匹配的阶梯规则
for (PromotionInfoDTO promotionInfoDTO : matchedConditionPromotionInfoDTOList) {
    List<PromotionProductDTO> promotionProductDTOS = promotionInfoDTO.getMatchedProductDTOList();
    //  过滤掉空的促销
    if (null == promotionProductDTOS || promotionProductDTOS.size() < 1) {
        continue;
    }
    ultimatePromotionInfoDTOList.add(promotionInfoDTO);
    BigDecimal totalAmount = BigDecimal.ZERO;
    for (PromotionProductDTO promotionProductDTO : promotionProductDTOS) {
        totalAmount = totalAmount.add(promotionProductDTO.getSubtotal());
    }
    //  查询该组商品满足的最高阶梯
    PromotionStairEntity promotionStairEntity = matchStair(promotionInfoDTO.getDefinitiveStairEntityList(), totalAmount);
    if (null != promotionStairEntity) {
        //  设置这组商品命中的促销的哪一个阶梯
        promotionInfoDTO.setPromotionStairEntity(promotionStairEntity);
        //  设置每个商品最终命中的唯一的条件促销
        for (PromotionProductDTO promotionProductDTO : promotionProductDTOS) {
            promotionProductDTO.setConditionpromotionInfoDTO(promotionInfoDTO);
        }
    }else {
        //  计算还差多少钱满足最低阶梯
        List<PromotionStairEntity> promotionStairList = promotionInfoDTO.getDefinitiveStairEntityList().stream().sorted(Comparator.comparing(PromotionStairEntity::getMinimumCharge)).collect(Collectors.toList());
        PromotionStairEntity promotionStairEntity2 = promotionStairList.get(0);
        BigDecimal minimumCharge = promotionStairEntity2.getMinimumCharge();
        BigDecimal balance = minimumCharge.subtract(totalAmount);
        promotionInfoDTO.setBalance(balance);
    }
} 

返回的数据接口

最终返回的应该是一个列表,列表中的每一个元素代表一个条件促销(即分组)

接口看起来可能是这样的:image.pngimage.pngimage.png参考:


http://www.woshipm.com/pd/741573.html\http://www.woshipm.com/pd/594963.html\http://www.woshipm.com/pd/716781.html


近期热文推荐:


1.Java 15 正式发布, 14 个新特性,刷新你的认知!!


2.终于靠开源项目弄到 IntelliJ IDEA 激活码了,真香!


3.我用 Java 8 写了一段逻辑,同事直呼看不懂,你试试看。。


4.吊打 Tomcat ,Undertow 性能很炸!!


5.《Java开发手册(嵩山版)》最新发布,速速下载!


觉得不错,别忘了随手点赞+转发哦!


相关文章
|
9月前
|
存储 人工智能 自然语言处理
无影AgentBay来了!给AI智能体装上“超级大脑”
阿里云在WAIC上发布专为AI Agents打造的“超级大脑”——无影AgentBay。该云端电脑支持多系统切换,集成视觉理解、自然语言控制等多项AI能力,提供高性能算力与企业级安全保障,助力AI开发者高效构建智能应用。
716 1
无影AgentBay来了!给AI智能体装上“超级大脑”
|
人工智能 监控 算法
千星计划如何解决私域裂变难题?
《千星计划:破解私域裂变难题》介绍了通过“零成本裂变引擎”降低参与门槛、构建轻量启动场景,实现低成本高回报的用户增长;借助指数级收益模型激发用户动力,提升裂变效率;清流机制解决流量垄断问题,确保公平分配;视频号变现闭环与自动化运营体系助力降本增效。方案结合实际案例(如众店生活、店商豹等),为高毛利低决策成本品类提供可验证的增长模型,打造私域流量全链路闭环。作者张梅以丰富经验,为企业搭建自驱型增长生态提供参考。
|
人工智能 物联网 开发者
魔搭上线AIGC专区,为开发者提供一站式AI创作开发平台
魔搭上线AIGC专区,首批上架157个风格化大模型,专业文生图全免费~
919 16
|
设计模式 存储 算法
本周推荐 | 设计模式在淘宝营销价格体系的实践
推荐语:本文详细描述责任链、中介者、适配器等多种设计模式在淘宝营销价格服务中的应用,从而实现了一套可扩展性的架构,应对灵活多变营销价格需求。 ——大淘宝技术研发工程师 小枫
1021 0
本周推荐 | 设计模式在淘宝营销价格体系的实践
|
监控 前端开发 网络协议
HTTP - 长连接 & 短连接 & 长轮询 & 短轮询 & 心跳机制
HTTP - 长连接 & 短连接 & 长轮询 & 短轮询 & 心跳机制
3653 0
HTTP - 长连接 & 短连接 & 长轮询 & 短轮询 & 心跳机制
数据一致性-对账
一致性分为强一致性和弱一致性。 强一致性的协议和手段主要有:二阶段提交(2PC)、三阶段提交(3PC)、TCC(Try-Confirm-Cancel)补偿型。这里面经常有人把两阶段提交和TCC补偿型混淆。二阶段提交实际上业务逻辑是在提交之前做的,两阶段只是事务控制的两个阶段。而TCC是将业务逻辑分为try、confirm和cancel三个阶段。举个例子:比如一个人要预售苹果,有两种销售策略。一种让用户先付钱,根据用户需求量准备足够的苹果。另一种是让用户先付钱同时声明到时候先到先得,没抢到的就退款。第一种就是二阶段提交,第二种就是TCC。弱一致性在分布式系统中常用的是一种特例:最终一致性。
1246 0
数据一致性-对账
|
监控 数据可视化 前端开发
高效设计企业营销系统的3种方案实践复盘
高效设计企业营销系统的3种方案实践复盘
489 2
|
Java Go 开发者
Goroutine内存泄漏:原因与避免
【2月更文挑战第23天】
860 0
|
SQL 数据可视化 数据库连接
Grafana图表工具Graph使用教程-通过Grafana绘制折线图
Grafana图表工具Graph使用教程-通过Grafana绘制折线图
|
机器学习/深度学习 数据挖掘 PyTorch
Python sklearn实现K-means鸢尾花聚类
Python sklearn实现K-means鸢尾花聚类
670 0
Python sklearn实现K-means鸢尾花聚类

热门文章

最新文章