跳到主要内容

促销活动核心逻辑说明

一、功能说明

1、当前商城平台共有8种促销活动,分别为满减满赠、单品立减、第二件半价、团购、限时抢购(秒杀)、拼团、优惠券和积分兑换。

2、促销活动分为平台促销活动和商家促销活动。

活动类型活动种类
平台促销活动团购、限时抢购(秒杀)、优惠券
商家促销活动满减满赠、单品立减、第二件半价、拼团、优惠券和积分兑换

平台促销活动:指的是平台管理端创建活动主体,由商家选择指定商品参与活动,平台来进行审核。

商家促销活动:指的是商家店铺端自行创建活动主体和选择商品参与,无需平台审核。

优惠券:平台可以创建优惠券,商家也可以创建优惠券。

3、促销活动也分为组合促销活动和单品促销活动。

目前商城平台只有满减满赠属于组合促销活动,其余活动都属于单品促销活动。

4、满减满赠、单品立减和第二件半价这三种促销活动,商家可以选择全部商品参与或者部分商品参与。

5、创建促销活动时间段的限制:

  • 满减满赠、单品立减、第二件半价和拼团这四种促销活动,同一个商家在同一时间段内只允许创建一个相同类型的促销活动。例如:某商家新建了一个满减满赠活动,活动时间为2020年11月01日10时00分00秒到2020年11月02日10时00分00秒,那么商家再创建满减满赠活动时,活动的开始时间和结束时间都不能在2020年11月01日10时00分00秒到2020年11月02日10时00分00秒这个时间范围内。
  • 平台管理端在创建团购和限时抢购(秒杀)促销活动时,逻辑同上。
  • 优惠券和积分兑换这两种促销活动,由于自身的特殊性,不受此限制。

6、目前商城平台除了拼团和积分兑换外,其它促销活动一旦生效后,均不可进行修改和删除操作。

二、数据库设计

表结构展示

促销活动商品表--es_promotion_goods

字段名类型与长度说明
pg_idbigint(20)主键ID
goods_idbigint(20)商品ID(关联商品表—es_goods)
sku_idbigint(20)商品SKU(关联商品SKU表—es_goods_sku)
start_timebigint(20)活动开始时间(存放的是以秒为单位的时间戳)
end_timebigint(20)活动结束时间(存放的是以秒为单位的时间戳)
activity_idbigint(20)促销活动ID(关联相关的促销活动表)
promotion_typevarchar(50)促销活动类型 FULL_DISCOUNT:满减满赠,MINUS:单品立减,HALF_PRICE:第二件半价,GROUPBUY:团购,SECKILL:限时抢购(秒杀),EXCHANGE:积分兑换
titlevarchar(50)促销活动标题(活动名称)
numint(10)参与促销活动的商品数量
pricedecimal(20,2)参与促销活动时商品的价格
seller_idbigint(20)活动所属商家ID(关联商家店铺表--es_shop)

此表为促销活动的核心业务表。

除拼团和优惠券之外,参与其它促销活动的商品都会存放在这个表中。

满减满赠、单品立减和第二件半价这三种促销活动,如果选择的是全部商品参与活动,那么也会向此表中存入一条数据,其中的goods_id和sku_id的值用"-1"代替。

三、缓存设计

  1. 设计思路

    在当前版本程序中,我们使用了促销脚本引擎的相关方式来实现了促销活动核心价格的计算,具体可参考《促销活动脚本引擎生成架构》这篇文档。

    简单来讲,就是在发布促销活动时,我们将促销活动相关信息(包括促销活动本身信息,如活动ID、活动类型等;还包括直接可调用的JavaScript语言方法,这些方法包含验证活动是否有效、计算出商品参与此活动后的价格等)存放在Redis缓存中,当用户购买了参与促销活动的商品后,程序不会再去数据库中读取相应的数据,而是直接在Redis缓存中拿到数据直接进行价格的计算。

  2. 实现方法

    那我们是如何将促销脚本相关信息放入缓存中的呢?

    在程序中,大部分促销活动都是有开始时间和结束时间的,并且活动在生效之后不允许删除和修改,根据这个特性,我们如果在促销活动创建成功的时候就生成促销脚本,那么如果促销活动还未生效的情况下,就需要考虑在修改和删除时还要对已经生成的促销脚本进行更新和删除操作,所以在生成促销脚本时,我们采用了延时任务执行器

    所谓延时任务执行器,是我们利用Rabbitmq延迟队列来实现的定时任务,就是将消息设置一个过期时间,放入一个没有读取的队列中,让消息过期后自动转入另外一个队列中,监控这个队列消息的监听处来处理定时任务具体的操作。具体操作流程大致如下:

    9181836-ebe66b87ccdfbffc

    如上所述,我们在发布促销活动时,只需要把创建促销脚本的消息以活动开始时间为过期时间放入过期队列中,当活动生效时,这条过期消息会转发到正常消息队列中,然后被对应的消费者执行并创建促销脚本引擎信息。

  3. 接口调用流程

    延时任务接口调用

    TimeTriggerExecuter接口实现类:

    TimeTriggerExecuter接口实现

  4. 相关代码展示

    • 延时任务接口实现类(RabbitmqTimeTrigger)—以添加延时任务为例:

      /**
      * 添加延时任务
      *
      * @param executerName 执行器
      * @param param 执行参数
      * @param triggerTime 执行时间
      * @param uniqueKey 如果是一个 需要有 修改/取消 延时任务功能的延时任务,<br/>
      * 请填写此参数,作为后续删除,修改做为唯一凭证 <br/>
      * 建议参数为:PINTUAZN_{ACTIVITY_ID} 例如 pintuan_123<br/>
      * 业务内全局唯一
      */
      @Override
      public void add(String executerName, Object param, Long triggerTime, String uniqueKey) {

      if (StringUtil.isEmpty(uniqueKey)) {
      uniqueKey = StringUtil.getRandStr(10);
      }
      //标识任务需要执行
      cache.put(RabbitmqTriggerUtil.generate(executerName, triggerTime, uniqueKey), 1);

      TimeTriggerMsg timeTriggerMsg = new TimeTriggerMsg(executerName, param, triggerTime, uniqueKey);
      logger.debug("定时执行在【" + DateUtil.toString(triggerTime, "yyyy-MM-dd HH:mm:ss") + "】,消费【" + param.toString() + "】");
      rabbitTemplate.convertAndSend(TimeTriggerConfig.DELAYED_EXCHANGE_XDELAY, TimeTriggerConfig.DELAY_ROUTING_KEY_XDELAY, timeTriggerMsg, message -> {

      Long current = DateUtil.getDateline();
      //如果执行的延时任务应该是在现在日期之前执行的,那么补救一下,要求系统一秒钟后执行
      if (triggerTime < current) {
      message.getMessageProperties().setDelay(1000);
      } else {
      Long time = (triggerTime - current) * 1000 + 5000 ;
      message.getMessageProperties().setHeader("x-delay", time);
      }
      logger.debug("还有【" + message.getMessageProperties().getExpiration() + "】执行任务");

      return message;
      });
      }
    • 监听并接收延时任务消息的接收者(TimeTriggerConsumer):

      /**
      * 接收消息,监听 CONSUMPTION_QUEUE 队列
      */
      @RabbitListener(queues = TimeTriggerConfig.IMMEDIATE_QUEUE_XDELAY)
      public void consume(TimeTriggerMsg timeTriggerMsg) {

      try {
      String key = RabbitmqTriggerUtil.generate(timeTriggerMsg.getTriggerExecuter(), timeTriggerMsg.getTriggerTime(), timeTriggerMsg.getUniqueKey());

      //如果这个任务被标识不执行
      if (cache.get(key) == null) {

      logger.debug("执行器执行被取消:" + timeTriggerMsg.getTriggerExecuter() + "|任务标识:" + timeTriggerMsg.getUniqueKey());
      return;
      }
      logger.debug("执行器执行:" + timeTriggerMsg.getTriggerExecuter());
      logger.debug("执行器参数:" + JsonUtil.objectToJson(timeTriggerMsg.getParam()));

      //执行任务前 清除标识
      cache.remove(key);

      TimeTriggerExecuter timeTriggerExecuter = (TimeTriggerExecuter) ApplicationContextHolder.getBean(timeTriggerMsg.getTriggerExecuter());
      timeTriggerExecuter.execute(timeTriggerMsg.getParam());

      } catch (Exception e) {
      logger.error("延时任务异常:", e);
      }
      }
    • 延时任务消息的消费者—共有多个实现,这里以优惠券脚本生成和删除消费者(CouponScriptTimeTriggerExecuter)为例:

      @Override
      public void execute(Object object) {

      PromotionScriptMsg promotionScriptMsg = (PromotionScriptMsg) object;

      //获取促销活动ID
      Long promotionId = promotionScriptMsg.getPromotionId();

      //优惠券级别缓存key
      String cacheKey = CachePrefix.COUPON_PROMOTION.getPrefix() + promotionId;

      //如果是优惠券开始生效
      if (ScriptOperationTypeEnum.CREATE.equals(promotionScriptMsg.getOperationType())) {

      //获取优惠券详情
      CouponDO coupon = this.couponClient.getModel(promotionId);

      //渲染并读取优惠券脚本信息
      String script = renderCouponScript(coupon);

      //将优惠券脚本信息放入缓存
      cache.put(cacheKey, script);

      //优惠券生效后,立马设置一个优惠券失效的流程
      promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.DELETE);
      String uniqueKey = "{TIME_TRIGGER_" + promotionScriptMsg.getPromotionType().name() + "}_" + promotionId;
      timeTrigger.add(TimeExecute.COUPON_SCRIPT_EXECUTER, promotionScriptMsg, promotionScriptMsg.getEndTime(), uniqueKey);

      this.logger.debug("优惠券[" + promotionScriptMsg.getPromotionName() + "]开始生效,id=[" + promotionId + "]");
      } else {

      //删除缓存中的促销脚本数据
      cache.remove(cacheKey);

      this.logger.debug("优惠券[" + promotionScriptMsg.getPromotionName() + "]已经失效,id=[" + promotionId + "]");
      }
      }

      /**
      * 渲染并读取优惠券脚本信息
      * @param coupon 优惠券信息
      * @return
      */
      private String renderCouponScript(CouponDO coupon) {
      Map<String, Object> model = new HashMap<>();

      Map<String, Object> params = new HashMap<>();
      params.put("startTime", coupon.getStartTime().toString());
      params.put("endTime", coupon.getEndTime().toString());
      params.put("couponPrice", coupon.getCouponPrice());

      model.put("coupon", params);

      String path = "coupon.ftl";
      String script = ScriptUtil.renderScript(path, model);

      logger.debug("生成优惠券脚本:" + script);

      return script;
      }
  5. 脚本渲染和执行工具类--ScriptUtil

    • 渲染脚本方法:

      /**
      * 渲染并读取脚本内容
      * @param name 脚本模板名称(例:test.js,test.html,test.ftl等)
      * @param model 渲染脚本需要的数据内容
      * @return
      */
      public static String renderScript(String name, Map<String, Object> model) {
      StringWriter stringWriter = new StringWriter();

      try {
      Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);

      cfg.setClassLoaderForTemplateLoading(Thread.currentThread().getContextClassLoader(),"/script_tpl");
      cfg.setDefaultEncoding("UTF-8");
      cfg.setNumberFormat("#.##");

      Template temp = cfg.getTemplate(name);

      temp.process(model, stringWriter);

      stringWriter.flush();

      return stringWriter.toString();

      } catch (Exception e) {
      log.error(e.getMessage());
      } finally {
      try {
      stringWriter.close();
      } catch (IOException ex) {
      log.error(ex.getMessage());
      }
      }

      return null;
      }
    • 执行脚本方法:

      /**
      * @Description:执行script脚本
      * @param method script方法名
      * @param params 参数
      * @param script 脚本
      * @return: 返回执行结果
      * @Author: liuyulei
      * @Date: 2020/1/7
      */
      public static Object executeScript(String method,Map<String,Object> params,String script) {
      if (StringUtil.isEmpty(script)){
      log.debug("script is " + script);
      return new Object();
      }

      try {
      ScriptEngineManager manager = new ScriptEngineManager();
      ScriptEngine engine = manager.getEngineByName("javascript");


      log.debug("脚本参数:");
      for (String key:params.keySet()) {
      log.debug(key + "=" + params.get(key));
      engine.put(key, params.get(key));
      }

      engine.eval(script);
      log.debug("script 脚本 :");
      log.debug(script);

      Invocable invocable = (Invocable) engine;

      return invocable.invokeFunction(method);
      } catch (ScriptException e) {
      log.error(e.getMessage(),e);
      } catch (NoSuchMethodException e) {
      log.error(e.getMessage(),e);
      }
      return new Object();
      }

四、促销商品价格计算

  1. 渲染促销活动

    • 接口调用流程图:

      image-20201029114901078

      image-20201029114957872

    • 相关代码展示

      渲染促销信息主方法

      com.enation.app.javashop.service.trade.cart.cartbuilder.impl.CartPromotionRendererImpl#renderPromotion

      /**
      * 渲染促销提示信息
      * @param cartList
      */
      private void renderPromotion(List<CartVO> cartList){
      //循环购物车,取出购物车所有sku
      List<CartSkuVO> cartSkuVOS = new ArrayList<>();
      for (CartVO cartVO:cartList) {
      cartSkuVOS.addAll(cartVO.getSkuList());
      }
      //首先检测已使用的活动是否失效,如果失效则移除
      this.cartPromotionManager.checkPromotionInvalid();

      //获取购物车中所有商品参与的促销活动
      List<PromotionScriptVO> promotions = this.scriptProcess.readSkuScript(cartSkuVOS);
      if(promotions == null || promotions.isEmpty()){
      promotions = new ArrayList<>();
      }
      //获取选中的促销活动
      SelectedPromotionVo selectedPromotionVo = cartPromotionManager.getSelectedPromotion();
      //用户选择的组合活动
      Map<Long, CartPromotionVo> groupPromotionMap = selectedPromotionVo.getGroupPromotionMap();
      //用户选择的单品活动
      Map<Long, List<CartPromotionVo>> singlePromotionMap = selectedPromotionVo.getSinglePromotionMap();

      //循环购物车,渲染购物车中促销信息
      for (CartVO cartVO:cartList) {
      //获取当前购物车用户选择的组合活动
      CartPromotionVo groupPromotion = groupPromotionMap.get(cartVO.getSellerId());
      List<PromotionScriptVO> promotionsList = new ArrayList<>(promotions);
      //获取当前购物车用户选择的单品活动
      List<CartPromotionVo> singlePromotions = singlePromotionMap.get(cartVO.getSellerId());
      if(singlePromotions == null){
      singlePromotions = new ArrayList<>();
      }

      //全部商品参与的活动 读取"全部商品"参与的促销活动
      List<PromotionScriptVO> promotionScripts = this.scriptProcess.readCartScript(cartVO.getSellerId());
      //如果不为空,则将全部商品参与的促销活动加入促销活动列表中
      if(promotionScripts != null && !promotionScripts.isEmpty()){
      promotionsList.addAll(promotionScripts);
      }
      //渲染sku促销信息
      renderSkuPromotion(promotionsList, cartVO, groupPromotion, singlePromotions);
      //设置购物车促销提示
      cartVO.setPromotionNotice(this.createNotice(cartVO.getPromotionList()));
      }
      }

      渲染SKU级别促销脚本信息

      com.enation.app.javashop.service.trade.cart.cartbuilder.impl.ScriptProcessImpl#readSkuScript(java.util.List{com.enation.app.javashop.model.trade.cart.vo.CartSkuVO})

      @Override
      public List<PromotionScriptVO> readSkuScript(List<CartSkuVO> skuList){
      List<String> skuKeys = new ArrayList<>();
      for (CartSkuVO cartSkuVO :skuList){
      skuKeys.add(CachePrefix.SKU_PROMOTION.getPrefix() +cartSkuVO.getSkuId());
      }
      List<List<PromotionScriptVO>> skuScripts = cache.multiGet(skuKeys);
      List<PromotionScriptVO> result = new ArrayList<>();
      for (List<PromotionScriptVO> scriptVO:skuScripts) {
      if(scriptVO != null && !scriptVO.isEmpty()){
      result.addAll(scriptVO);
      }
      }
      return result;
      }

      获取选中的促销活动

      com.enation.app.javashop.service.trade.cart.impl.CartPromotionManagerImpl#getSelectedPromotion

      /**
      * 由缓存中读取出用户选择的促销信息
      *
      * @return 用户选择的促销信息
      */
      @Override
      public SelectedPromotionVo getSelectedPromotion() {
      String cacheKey = this.getOriginKey();
      SelectedPromotionVo selectedPromotionVo = (SelectedPromotionVo) cache.get(cacheKey);
      if (selectedPromotionVo == null) {
      selectedPromotionVo = new SelectedPromotionVo();
      cache.put(cacheKey, selectedPromotionVo);
      }

      return selectedPromotionVo;
      }

      渲染店铺(购物车)级别促销脚本信息

      com.enation.app.javashop.service.trade.cart.cartbuilder.impl.ScriptProcessImpl#readCartScript

      @Override
      public List<PromotionScriptVO> readCartScript(Long sellerId){
      List<PromotionScriptVO> scriptVO = (List<PromotionScriptVO>) cache.get(CachePrefix.CART_PROMOTION.getPrefix() + sellerId);
      return scriptVO;
      }
  2. 促销活动商品价格计算

    • 接口调用流程图

      image-20201029115213096

      CartPriceCalculatorImpl内部实现:

      image-20201029114325031

    • 相关代码展示

      价格计算核心代码--CartPriceCalculatorImpl

      @Override
      public PriceDetailVO countPrice(List<CartVO> cartList, Boolean includeCoupon) {

      //如果不包含优惠券计算,则将选中的优惠券信息删除
      if (!includeCoupon) {
      cartPromotionManager.cleanCoupon();
      }

      //根据规则计算价格
      PriceDetailVO priceDetailVO = this.countPriceWithScript(cartList, includeCoupon);

      return priceDetailVO;
      }


      /**
      * 根据促销脚本计算价格
      *
      * @param cartList 购物车列表
      * @param includeCoupon 是否包含优惠券
      * @return
      */
      private PriceDetailVO countPriceWithScript(List<CartVO> cartList, boolean includeCoupon) {

      PriceDetailVO price = new PriceDetailVO();
      //获取选中的促销活动
      SelectedPromotionVo selectedPromotionVo = cartPromotionManager.getSelectedPromotion();
      //用户选择的组合活动
      Map<Long, CartPromotionVo> groupPromotionMap = selectedPromotionVo.getGroupPromotionMap();
      //用户选择的单品活动
      Map<Long, List<CartPromotionVo>> singlePromotionMap = selectedPromotionVo.getSinglePromotionMap();
      //用户选择使用的优惠券
      Map<Long, CouponVO> couponMap = selectedPromotionVo.getCouponMap();

      for (CartVO cart : cartList) {
      //未选中的购物车不参与计算
      if (cart.getChecked() == 0) {
      continue;
      }
      //获取当前购物车用户选择的组合活动
      CartPromotionVo groupPromotion = groupPromotionMap.get(cart.getSellerId());
      //获取当前购物车用户选择的单品活动
      List<CartPromotionVo> singlePromotions = singlePromotionMap.get(cart.getSellerId());

      PriceDetailVO cartPrice = new PriceDetailVO();
      cartPrice.setFreightPrice(cart.getPrice().getFreightPrice());
      List<CartSkuVO> groupSkuList = new ArrayList<>();
      for (CartSkuVO cartSku : cart.getSkuList()) {
      //未选中的商品不参与计算
      if (cartSku.getChecked() == 0) {
      continue;
      }
      //计算单品活动促销优惠
      this.calculatorSingleScript(cart, singlePromotions, cartPrice, cartSku);
      if (cartSku.getGroupList().contains(groupPromotion)) {
      groupSkuList.add(cartSku);
      }
      }
      //计算满减优惠,返回是否免运费
      Boolean freeShipping = calculatorGroupScript(cart, groupPromotion, cartPrice, groupSkuList);

      //单品规则中有免运费或满减里有免运费
      if (freeShipping) {
      cartPrice.setIsFreeFreight(1);
      cartPrice.setFreightPrice(0D);
      }

      //是否计算优惠券优惠价格
      Long sellerId = cart.getSellerId();
      if (includeCoupon) {
      CouponVO couponVO = couponMap.get(sellerId);
      calculatorCoupon(cartPrice, couponVO);

      }

      //计算店铺商品总优惠金额
      double totalDiscount = CurrencyUtil.add(cartPrice.getCashBack(), cartPrice.getCouponPrice());
      cartPrice.setDiscountPrice(totalDiscount);

      //总价为商品价加运费
      double totalPrice = CurrencyUtil.add(cartPrice.getTotalPrice(), cartPrice.getFreightPrice());
      cartPrice.setTotalPrice(totalPrice);

      cart.setPrice(cartPrice);
      price = price.plus(cartPrice);

      }
      //是否计算优惠券价格 此处针对平台优惠券
      if (includeCoupon) {
      CouponVO couponVO = couponMap.get(0L);
      calculatorCoupon(price, couponVO);
      }

      logger.debug("计算完优惠后购物车数据为:");
      logger.debug(cartList);

      logger.debug("价格为:");
      logger.debug(price);

      return price;
      }

      计算组合活动优惠

      /**
      * 计算组合活动优惠
      *
      * @param cart 购物车
      * @param groupPromotion 组合活动
      * @param cartPrice 购物车价格VO
      * @param groupSkuList 参与组合活动的skuVO
      * @return 是否免运费 true 免运费 false不免运费
      */
      private Boolean calculatorGroupScript(CartVO cart, CartPromotionVo groupPromotion, PriceDetailVO cartPrice, List<CartSkuVO> groupSkuList) {
      Boolean freeShipping = false;
      if (groupPromotion != null && groupSkuList.size() > 0) {
      Double cost;
      String giftJson;
      Double totalGroupPrice = 0D;
      for (CartSkuVO cartSku : groupSkuList) {
      //如果商品是积分商品,则不参与满减优惠
      // if (GoodsType.POINT.name().equals(cartSku.getGoodsType())) {
      // continue;
      // }
      totalGroupPrice += cartSku.getSubtotal();
      }
      Map param = new HashedMap();
      param.put("$price", totalGroupPrice);
      cost = scriptProcess.countPrice(groupPromotion.getPromotionScript(), param);
      //获取赠品信息
      giftJson = scriptProcess.giveGift(groupPromotion.getPromotionScript(), param);
      //设置购物车赠品信息
      cart.setGiftJson(giftJson);
      //计算优惠前后差价
      Double diffPrice = CurrencyUtil.sub(totalGroupPrice, cost);
      //设置返现金额
      cartPrice.setCashBack(CurrencyUtil.add(cartPrice.getCashBack(), diffPrice));
      //设置满减优惠金额
      cartPrice.setFullMinus(diffPrice);
      //设置优惠后的金额
      cartPrice.setTotalPrice(CurrencyUtil.sub(cartPrice.getTotalPrice(), diffPrice));
      if (!StringUtil.isEmpty(giftJson)) {
      List<GiveGiftVO> giftList = JsonUtil.jsonToList(giftJson, GiveGiftVO.class);
      for (GiveGiftVO giveGiftVO : giftList) {
      if ("freeShip".equals(giveGiftVO.getType())) {
      freeShipping = (Boolean) giveGiftVO.getValue();
      }
      }
      }
      }
      return freeShipping;
      }

      计算单品活动优惠

      /**
      * 计算单品活动优惠
      *
      * @param cart 购物车
      * @param singlePromotions 参与的单品活动
      * @param cartPrice 当前购物车价格
      * @param cartSku 当前skuVO
      */
      private void calculatorSingleScript(CartVO cart, List<CartPromotionVo> singlePromotions, PriceDetailVO cartPrice, CartSkuVO cartSku) {
      Double cost;
      Integer point = 0;
      if (CartType.CHECKOUT.equals(cart.getCartType()) && cartSku.getChecked() == 0) {
      return;
      }

      CartPromotionVo promotionVo = null;
      if (singlePromotions != null) {
      for (CartPromotionVo cartPromotionVo : singlePromotions) {
      if (cartSku.getSkuId().equals(cartPromotionVo.getSkuId())) {
      promotionVo = cartPromotionVo;
      break;
      }
      }
      }
      //转换脚本参数
      ScriptSkuVO skuVO = new ScriptSkuVO(cartSku);
      Map param = new HashedMap();
      param.put("$sku", skuVO);
      //商品参与了用户选择的活动
      if (promotionVo != null) {
      //获取优惠后的价格
      cost = scriptProcess.countPrice(promotionVo.getPromotionScript(), param);
      //获取积分兑换所需积分
      logger.debug("用户选择参与的促销活动类型:" + promotionVo.getPromotionType());
      if (PromotionTypeEnum.EXCHANGE.name().equals(promotionVo.getPromotionType())) {
      point = scriptProcess.countPoint(promotionVo.getPromotionScript(), param);
      }
      //设置商品单品成交单价
      calculatorPurchasePrice(cartSku, cost, promotionVo.getPromotionType());

      cartSku.setPoint(point);
      cartSku.setSubtotal(cost);
      }

      //未选中的不计入合计中
      if (cartSku.getChecked() == 0) {
      return;
      }

      //购物车全部商品的原价合
      cartPrice.setOriginalPrice(CurrencyUtil.add(cartPrice.getOriginalPrice(), CurrencyUtil.mul(cartSku.getOriginalPrice(), cartSku.getNum())));

      //购物车所有小计合
      cartPrice.setGoodsPrice(CurrencyUtil.add(cartPrice.getGoodsPrice(), cartSku.getSubtotal()));

      //购物车返现合
      cartPrice.setCashBack(CurrencyUtil.add(cartPrice.getCashBack(), CurrencyUtil.sub(CurrencyUtil.mul(cartSku.getOriginalPrice(), cartSku.getNum()), cartSku.getSubtotal())));

      //购物车使用积分
      cartPrice.setExchangePoint(cartPrice.getExchangePoint() + cartSku.getPoint());

      //购物车小计
      cartPrice.setTotalPrice(CurrencyUtil.add(cartPrice.getTotalPrice(), cartSku.getSubtotal()));

      //累计商品重量
      double weight = CurrencyUtil.mul(cartSku.getGoodsWeight(), cartSku.getNum());
      double cartWeight = CurrencyUtil.add(cart.getWeight(), weight);
      cart.setWeight(cartWeight);
      }

      返回结果对象--PriceDetailVO

      @ApiModel(value = "PriceDetailVO", description = "价格明细")
      @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
      public class PriceDetailVO implements Serializable {
      @ApiModelProperty(value = "总价")
      private Double totalPrice;

      @ApiModelProperty(value = "商品原价,没有优惠过的")
      private Double originalPrice;

      @ApiModelProperty(value = "商品价格,优惠后的")
      private Double goodsPrice;

      @ApiModelProperty(value = "配送费")
      private Double freightPrice;

      @ApiModelProperty(value = "优惠金额")
      private Double discountPrice;

      @ApiModelProperty(value = "返现金额,不含优惠券")
      private Double cashBack;

      @ApiModelProperty(value = "优惠券抵扣金额")
      private Double couponPrice;

      @ApiModelProperty(value = "满减金额")
      private Double fullMinus;

      @ApiModelProperty(value = "是否免运费,1为免运费")
      private Integer isFreeFreight;

      @ApiModelProperty(value = "使用的积分")
      private Long exchangePoint;

      /**
      * 构造器,初始化默认值
      */
      public PriceDetailVO() {
      this.goodsPrice = 0.0;
      this.freightPrice = 0.0;
      this.totalPrice = 0.0;
      this.discountPrice = 0.0;
      this.exchangePoint = 0L;
      this.isFreeFreight = 0;
      this.couponPrice = 0D;
      this.cashBack = 0D;
      this.originalPrice = 0D;
      fullMinus = 0d;
      }

      /**
      * 清空功能
      */
      public void clear() {
      this.goodsPrice = 0.0;
      this.freightPrice = 0.0;
      this.totalPrice = 0.0;
      this.discountPrice = 0.0;
      this.exchangePoint = 0L;
      this.isFreeFreight = 0;
      this.cashBack = 0D;
      this.originalPrice=0D;
      this.couponPrice=0D;
      fullMinus = 0d;
      }

      /**
      * 价格累加运算
      * @param price
      * @return
      */
      public PriceDetailVO plus(PriceDetailVO price) {
      double total = CurrencyUtil.add(totalPrice, price.getTotalPrice());
      double original = CurrencyUtil.add(originalPrice, price.getOriginalPrice());
      double goods = CurrencyUtil.add(goodsPrice, price.getGoodsPrice());
      double freight = CurrencyUtil.add(this.freightPrice, price.getFreightPrice());
      double discount = CurrencyUtil.add(this.discountPrice, price.getDiscountPrice());
      double couponPrice = CurrencyUtil.add(this.couponPrice, price.getCouponPrice());
      double cashBack = CurrencyUtil.add(this.cashBack, price.getCashBack());
      double fullMinus = CurrencyUtil.add(this.cashBack, price.getFullMinus());
      Long point = this.exchangePoint + price.getExchangePoint();

      PriceDetailVO newPrice = new PriceDetailVO();
      newPrice.setTotalPrice(total);
      newPrice.setGoodsPrice(goods);
      newPrice.setFreightPrice(freight);
      newPrice.setDiscountPrice(discount);
      newPrice.setExchangePoint(point);
      newPrice.setCouponPrice(couponPrice);
      newPrice.setCashBack(cashBack);
      newPrice.setIsFreeFreight(price.getIsFreeFreight());
      newPrice.setOriginalPrice(original);
      newPrice.setFullMinus(fullMinus);
      return newPrice;
      }

      /**
      * 当前店铺总价计算
      */
      public void countPrice() {
      //购物车内当前商家的商品原价总计
      Double goodsPrice = this.getGoodsPrice();

      //购物车内当前商家的配送金额总计
      Double freightPrice = this.getFreightPrice();

      //购物车内当前商家的应付金额总计
      //运算过程=商品原价总计-返现金额+配送费用
      Double totalPrice = CurrencyUtil.add(CurrencyUtil.sub(goodsPrice, this.getCashBack()), freightPrice);

      //运算过程=商品原价总计-优惠券金额
      totalPrice = CurrencyUtil.sub(totalPrice, this.getCouponPrice());

      //防止金额为负数
      if (totalPrice.doubleValue() <= 0) {
      totalPrice = 0d;
      }
      this.setTotalPrice(totalPrice);
      }

      //get、set、equals、toString等方法略
      }