优惠决策算价

一、简介

1、优惠共享时商品处理

        方式1: 折后价  -> A商品用了1号活动后,将以A的优惠后价格去算2号活动。

        方式2: 商品互斥 -> A 商品用了1号活动后,将不能用2号活动。 

2、优惠策略

        策略计算的方法是贪心法,以最小的门槛代价获取最高的优惠。

a、单品折扣

        针对指定的商品进行打折促销。

b、订单满减

        与其它金额类活动互斥,当订单达到门槛时,可以享用对应优惠,金额需要分摊到全部商品。

c、组合满减

       当购买指定的商品组合后,会享用对应优惠。将金额分摊到参与商品上。

c.1 获取满足金额门槛的最优排列

     基于动态规划算法,递推关系式为:  dp[i][j] = Math.min(dp[i-1][j], dp[i-1][j-p[i].price] + 1);

    根据dp结果,反向推出排列;

public List<Product> bestPath(List<Product> products, int totalAmount) {
        var size = products.size();

        int[][] dp = new int[size][totalAmount + 1];
        // init
        for (int i = 0; i < size; i++) {
            Arrays.fill(dp[i], Integer.MAX_VALUE);
            dp[i][0] = 0;
        }
        // calc
        for (int i = 0; i < size; i++) {
            var unitPrice = products.get(i).getUnitPrice();
            for (int j = 1; j <= Math.min(totalAmount, unitPrice); j++) {
                if (j <= unitPrice) {
                    dp[i][j] = 1;
                    continue;
                }
                if (i > 0) {
                    if (dp[i - 1][j] != Integer.MAX_VALUE) {
                        dp[i][j] = dp[i - 1][j];
                    }
                    if (dp[i - 1][j - unitPrice] != Integer.MAX_VALUE) {
                        dp[i][j] = Math.min(dp[i][j], dp[i - 1][j - unitPrice] + 1);
                    }
                }

            }
        }
        // 反向推出排列
        List<Product> result = new ArrayList<>();
        int i = size - 1;
        if (dp[i][totalAmount] == Integer.MAX_VALUE) { // 没有满足的方案
            return result;
        }
        int amt = totalAmount;
        while (i >= 0) {
            if (dp[i][amt] == dp[i - 1][amt]) { //当前product 没有加
                i--;
                continue;
            }
            result.add(0, products.get(i)); // 当前product有加
            amt -= products.get(i).getUnitPrice();
            i--;
        }
        return result;
    }

tips: 这里product不是sku,而是具体每一件。

c.2 每满x元减y元样例
void apply(StrategyContext ctx, List<Product> products) {
        
        var strategy = ctx.getModel().getStrategyList().get(0);
       
        var strategyLocker = ctx.getStrategyLocker();


        var tmpLockQueue = new LockerList();
        
        var acc = 0;
        for (var p : products) {
            // 尝试锁定商品
            var lockResult = strategyLocker.tryLockCombo(ctx, p, tmpLockQueue);
            if (!lockResult.isSuccess()) {
                break;
            }
            acc += p.unitPrice;
            // 加入锁定列表
            tmpLockQueue.add(new Locker(p, ctx);
        }

        var cnt = acc / strategy.getOrderAmount();
  
       // 获取最优排列
        var bestProducts = bestPath(tmpLockQueue.getProducts(), cnt * strategy.getOrderAmount());
        tmpLockQueue = tmpLockQueue.filter(bestProducts);


        if (!tmpLockQueue.isEmpty()){
            rowLockerManager.lockProductsShareByAmount( tmpLockQueue, cnt * strategy.getDeductAmount());
        }
}

d、赠品活动

      赠品活动分为自动推荐赠品和用户手动选择赠品两种形式。

e、换购活动

      当推荐换购活动后,用户可以在换购页加购换购商品。

      关于换购活动,如果购物车变化时候,这种变化可能会产生以下几种情况:

             1、推荐新的换购活动,这个时候需要将购物车中老的换购商品设置为不可选中。

             2、换购活动门槛失效,这个时候把之前加购的换购商品设置为不可选中。

             3、换购活动门槛又生效,这个时候需要把之前不可选中的换购商品变为选中。

二、技术方案设计

1.商品级互斥

a.Locker

        Locker粒度是单个商品的分摊结果。

@Data
public class Locker implements Comparable<Locker>{

    /**
     * 命中的商品编码
     */
    private String skuCode;
   

    /**
    * sku的第几个
    */
    private Integer index;

   
    /**
     * 优惠活动编号
     */
    private String promotionId;

    
    /**
     * 单价
     */
    private Long unitPirce;

    /**
     * 商品优惠分摊金额
     */
    private Long promotionAmoount;

    private boolean isSkuLock;

    private boolean isSkuDiscountLock;

    private boolean isComboLock;

}

b.CartLocker

    CartLocker表示整个试算分摊情况。

@Data
public class CartLocker{
    /**
    * 优惠总金额
    */
    private Long totalPromotionAmount;
    
    /**
    * 试算分摊结果
    */
    private  List<Locker> lockers;

   
}

c.StrategyLocker

        StrategyLocker这个类表示某个策略分摊情况。

public class StrategyLocker{

   
    private CartLocker cartLocker;
    
    /**
    * 当前活动id
    */    
    private String promotionId;
    

    /**
    * 当前活动命中lockers
    */
    private List<Locker> lockers;



    /**
    * 判断当前商品是否能命中
    */
    public boolean tryLock(Product product, List<Product> tmpLockQueue){
        String skuCode = product.getSkuCode();
        int quantity = product.getQuantity();
        // 临时锁定数量
        Long tmpLockQuantity = Lists.filter(tmpLockQueue, v -> v.getSkuCode().equals(skuCode)).size();
        // 已锁定数量
        Long lockQuantity = Lists.filter(lockers, v -> v.getSkuCode().equals(skuCode)).size();
        // 获取购物车已锁定数量
        Long cartLockerQuantity = Lists.filter(cartLocker.lockers, v -> v.getSkuCode().equals(skuCode)).size();
        
        return tmpLockQuantity + lockQuantity + cartLockerQuantity < quantity;
    
    }

    /**
    * 锁定临时锁定队列商品
    */
    public void lockProductsShareByAmount(List<Product> tmpLockQueue, int promotionAmt){
        var totalPrice = Lists.sum(tmpLockQueue, p -> p.getDiscount());
        for (Product product:tmpLockQueue){
            Locker locker = new Locker(product.getSkuCode(), promotionId, product.getDiscount());
            locker.setPromotionAmt(promotionAmt * (product.getDiscount()/totalPrice);
            lockers.add(locker);
            
        }
    }
}

2.优惠编排

a.单品折扣

     上文介绍的Locker 是以策略为粒度的,比方说 A活动 锁定 sku1,sku2, B活动锁定sku1,sku2, 最终最优的是A活动,或者是B活动。。。。。。。而事实上sku1最优的是A活动,sku2最优的是B活动。 

      所以在计算时候,需要将单品活动根据配置的sku进行拆分,比如A配置了sku1,sku2,那就拆成A1,A2;同理B拆成了B1,B2,呢么最优的是 A1 + B2了。

public List<Promotion> splitSkuPromotion(Promotion promotion){
  // 每个sku 对应一个策略
  var strategyList = promotion.getStrategyList();
  var res = new ArrayList<Promotion>();
  for (var strategy:strategyList){
    var split = promotion.copy();
    split.setStrategyList(List.of(strategy));
    res.add(split);
  }
  return res;
   
}

     由于全排列的影响,分裂单品活动后会导致性能下降,可以在试算时候,根据购物车商品,取出其最优的单品活动,如sku1是A1,sku2是B2, 删除A2和B1

b.组合满减

       组合和组合,组合和单品直接计算顺序会影响最终结果,所以采用全排列方式计算。

全排列计算方式

  方式1: 在回溯过程中计算。

  方式2: 先获取所有排列,然后对每个排列进行异步试算。(!性能更好)

   private CartLocker maxCartLocker = null;

    /**
    * 回溯算法计算优惠全排列
    */
    public void dfs(CalculateReq req, CartLocker cartLocker, List<Promotion> pList, int[] visited){
        
        boolean end = true;
        for (int i = 0; i < pList.size(); i++){
            if (visited[i] == 1){
                continue;
            }
            visited[i] = 1;
            end = false;
            CartLocker cpCL= cartLocker.copy(); // 这里是copy的,不好回溯。
            // 互斥组判断通过
            if (StrategyGroup.isNotConflict(cpCl.getHitPromotion(), 
                pList.get(i)
            )){
                // 策略计算,结果存在cartLocker中    
                ApplyResult result =  pList.get(i).apply(req, cpCl);
                cpCl.addAllLockers(result.getRowLockerManager().getLockers());
              }
            // 全排列
            dfs(req, cpCL, pList, visited);
            visited[i] = 0;
        }
        if (end){
            if (!isValid(cartLocker)){return;} // 勾选的优惠券必须使用
            if (maxCartLocker == null || maxCartLocker.getPromotionAmt() < cartLocker.getPromotionAmt()){
               
               maxCartLocker = cartLocker;
           }
        }
    }
    
    
    

    全排列优化:

    1. 如果全是单品活动 -> 降级成线性

    2. 如果有1个组合 ,n个单品 -> 降级成组合。

       组合 ->  x个单品 ,组合 ,n-x个单品。只算x,n-x可以取差集。

        tips: 全排列 是 Ann 种, 组合是 Cn0 + Cn 1 + ... C n/2 种

                下面是组合的代码示例

   private CartLocker maxCartLocker;

   private List<Promotion> skuPromotions;

   private List<Promotion> comboPromotions;

    /**
    * 回溯算法计算获取单品的组合
    */
    public void dfs(CalculateReq req, CartLocker cartLocker, List<Promotion> tmpSkuPromotions, int start, int size){
        if (tmpSkuPromomtions.size() == size){
            for (var comboPromotion: comboPromotons){
                var left = tmpSkuPromotion;
                var middle = comboPromotion;
                var right = Lists.filter(skuPromotions, item -> !left.contains(item));
                {
                    var lmr = new ArrayList<>(left);
                    lmr.add(middle);
                    lmr.addAll(right);
                    apply(lmr, req, cartLocker.copy());
                    // todo setMaxcartlocker
                }
                {
                    var rml = new ArrayList<>(right);
                    lmr.add(middle);
                    lmr.addAll(left);
                    apply(rml, req, cartLocker.copy());
                    // todo setMaxcartLocker
                }   
                 
                 
            }
            return;
        }
        for (int i = start; i < skuPromotions.size(); i++ ){
            // add
            tmpSkuPromotions.add(skuPromotions.get(i));
            dfs(req,cartLocker, tmpSkuPromotions, i + 1 , size);
            // not add
            tmpSkuPromotions.remove(skuPromotions.get(i));
            dfs(req,cartLocker, tmpSkuPromotions, i + 1 , size);
        }
    }
    

    public void apply(List<Promotion> promotions, CalculateRequest request, CartLocker cartLocker){

         for (var promoton:promotions){
            promotion.apply(request, cartLocker);
         }
         if (!isValid(cartLocker)){return;} // 勾选的优惠券必须使用
         if (maxCartLocker == null || maxCartLocker.getPromotionAmount() < cartLocker.getPromotionAmount()){
            maxCartLocker = cartLocker;
        }
    }
    
    

3.折后价

// note 这里面product 是指同一个skuCode,但是在数量上有区别
Long price = product.getUnitPrice() - cartLocker.getDiscountAmt(product);

4.优惠互斥

      通过HitPromotionStack 存储试算过程中已命中的活动,通过CalculateContext中记录互斥配置(用户选了优惠券,会将某些活动互斥)。在判断当前活动是否可用时,判断HitPromotionStack是否有互斥的活动,以及CalculateContext中配置互斥条件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值