跳到主要内容

积分兑换

一、功能说明

1、所谓积分兑换就是用户可以用消费积分来兑换商家发布的积分商品。

2、目前只有自营店铺可以发布积分商品。

3、积分商品兑换条件有两种形式:

纯积分兑换:商家发布积分商品时,可以设置兑换条件为0元+X积分。

积分+价钱兑换:商家在发布积分商品时,可以设置兑换条件为X元+X积分。

4、积分商品有单独的商品分类,在平台管理端可添加积分分类。

积分分类暂时只支持一级分类。

5、对积分商品申请售后,用户积分不做退还处理。

二、数据库设计

1、表结构设计

  1. 积分兑换表—es_exchange

    字段名类型与长度说明
    exchange_idbigint(20)主键ID
    goods_idbigint(20)商品ID(关联es_goods表)
    category_idbigint(20)积分分类ID(关联es_exchange_cat表)
    enable_exchangeint(11)是否允许兑换 0:否,1:是
    exchange_moneydecimal(20,2)兑换所需金额
    exchange_pointint(11)兑换所需积分
    goods_namevarchar(100)商品名称
    goods_pricedecimal(20,2)商品原价
    goods_imgvarchar(255)商品图片
  1. 积分商品分类表—es_exchange_cat

    字段名类型与长度说明
    category_idbigint(20)主键ID
    namevarchar(255)分类名称
    parent_idbigint(20)父分类ID(预留字段,暂时无用)
    category_pathvarchar(255)分类ID路径(预留字段,暂时无用)
    goods_countint(11)此分类下的积分商品数量
    category_orderint(11)分类排序(值越小排序越靠前)
    list_showint(10)是否显示 0:否,1:是
    imagevarchar(255)分类图片

2、表关联说明

  • 积分兑换表与其他表之间的关联图

    image-20201105113646394

  • 积分兑换表与其他表之间的关联字段对照

    积分兑换表(es_exchange)积分商品分类表(es_exchange_cat)
    category_idcategory_id
    积分兑换表(es_exchange)商品表(es_goods)
    goods_idgoods_id
    积分兑换表(es_exchange)促销活动商品表(es_promotion_goods)
    exchange_idactivity_id
    无字段,促销类型值为EXCHANGEpromotion_type

三、缓存设计

1、商家在发布积分商品时,在将积分商品信息入库的同时,也会将积分兑换信息放入缓存中。

缓存key值为:{STOREID_EXCHANGE_KEY}积分兑换ID。

缓存value值为:ExchangeDO.java这个实体对象信息。

2、积分兑换脚本引擎

  • 脚本引擎缓存结构:

    积分兑换活动的促销脚本引擎缓存结构只有一种:SKU级别的缓存结构。

  • 脚本引擎生成和删除时机:

    • 生成:商家发布积分商品,通过发送消息,在消费者中生成。
    • 删除:商家删除积分商品或者将积分商品修改为普通商品时删除。

关于促销脚本引擎缓存结构可参考《促销活动脚本引擎生成架构》这篇文档。

四、代码设计

1、接口调用流程图

  • 平台和商家发布积分商品

    image-20201105142841929

2、相关代码展示

  • 添加和修改积分兑换信息

    @Service
    public class ExchangeGoodsManagerImpl implements ExchangeGoodsManager {

    @Override
    @Transactional(value = "tradeTransactionManager", propagation =
    Propagation.REQUIRED, rollbackFor = {Exception.class})
    public ExchangeDO add(ExchangeDO exchangeSetting, PromotionGoodsDTO goodsDTO) {
    //设置商品ID
    exchangeSetting.setGoodsId(goodsDTO.getGoodsId());
    //设置商品名称
    exchangeSetting.setGoodsName(goodsDTO.getGoodsName());
    //设置商品价格
    exchangeSetting.setGoodsPrice(goodsDTO.getPrice());
    //设置商品图片
    exchangeSetting.setGoodsImg(goodsDTO.getThumbnail());

    //如果允许积分兑换 0:否,1:是
    if (exchangeSetting.getEnableExchange() == 1) {
    //如果积分兑换分类ID等于空,默认设置为0
    if (exchangeSetting.getCategoryId() == null) {
    exchangeSetting.setCategoryId(0L);
    }

    //积分兑换信息入库
    exchangeMapper.insert(exchangeSetting);
    //获取主键ID
    Long settingId = exchangeSetting.getExchangeId();

    //获取当前时间
    long nowTime = DateUtil.getDateline();
    //获取一年后的时间
    long endTime = endOfSomeDay(365);

    List<PromotionGoodsDTO> list = new ArrayList<>();
    list.add(goodsDTO);

    //新建促销活动详情对象
    PromotionDetailDTO detailDTO = new PromotionDetailDTO();
    //设置促销活动标题
    detailDTO.setTitle("积分活动");
    //设置促销活动类型为积分兑换
    detailDTO.setPromotionType(PromotionTypeEnum.EXCHANGE.name());
    //设置促销活动ID
    detailDTO.setActivityId(settingId);
    //设置活动开始时间
    detailDTO.setStartTime(nowTime);
    //设置活动结束时间
    detailDTO.setEndTime(endTime);
    //入库操作
    this.promotionGoodsManager.add(list, detailDTO);
    //将积分兑换信息放入缓存中
    this.cache.put(PromotionCacheKeys.getExchangeKey(settingId),
    exchangeSetting);
    }

    return exchangeSetting;
    }

    @Override
    @Transactional(value = "tradeTransactionManager", propagation =
    Propagation.REQUIRED, rollbackFor = {Exception.class})
    public ExchangeDO edit(ExchangeDO exchangeSetting, PromotionGoodsDTO goodsDTO) {
    //删除之前的相关信息
    this.deleteByGoods(goodsDTO.getGoodsId());

    //如果允许积分兑换 0:否,1:是
    if (exchangeSetting != null && exchangeSetting.getEnableExchange() == 1) {
    //新添加积分兑换信息
    this.add(exchangeSetting, goodsDTO);
    }
    return exchangeSetting;
    }
    }
  • 积分兑换活动脚本生成

    @Service
    public class ExchangeGoodsScriptConsumer implements GoodsChangeEvent {

    @Override
    public void goodsChange(GoodsChangeMsg goodsChangeMsg) {
    //获取商品ID数组
    Long[] goodsIds = goodsChangeMsg.getGoodsIds();
    //获取商品信息集合
    List<GoodsDO> goodsList = goodsClient.queryDo(goodsIds);
    //获取操作类型
    int operationType = goodsChangeMsg.getOperationType();

    //如果商品集合不为空并且集合长度不为0
    if (goodsList != null && goodsList.size() != 0) {
    for (GoodsDO goodsDO : goodsList) {
    //如果商品为积分兑换商品
    if (GoodsType.POINT.name().equals(goodsDO.getGoodsType())) {
    //获取商品ID
    Long goodsId = goodsDO.getGoodsId();
    //获取积分兑换商品信息
    ExchangeDO exchangeDO =
    this.exchangeGoodsClient.getModelByGoods(goodsId);
    //获取商品的SKU信息
    List<GoodsSkuVO> skuList = this.goodsClient.listByGoodsId(goodsId);

    //如果为true 证明商品正在上架销售
    boolean normal = goodsDO.getDisabled() == 1 &&
    goodsDO.getMarketEnable() == 1 && goodsDO.getIsAuth() == 1;
    //如果为true 证明商品已下架但是没有删除
    boolean under = goodsDO.getDisabled() == 1 &&
    goodsDO.getMarketEnable() == 0 && goodsDO.getIsAuth() == 1;
    //新增脚本条件:(操作类型为添加或者审核成功操作) && 商品未删除
    //&& 商品未下架 && 商品审核状态为审核通过
    boolean add = (GoodsChangeMsg.ADD_OPERATION == operationType ||
    GoodsChangeMsg.GOODS_VERIFY_SUCCESS ==
    operationType) && normal;
    //修改脚本条件:操作类型为修改 && 商品未删除 && 商品未下架
    //&& 商品审核状态为审核通过
    boolean edit = GoodsChangeMsg.UPDATE_OPERATION ==
    operationType && normal;
    //删除脚本条件:操作类型为修改 && 商品未删除 && 商品已下架
    //&& 商品审核状态为审核通过
    boolean delete = GoodsChangeMsg.UNDER_OPERATION ==
    operationType && under;

    if (add || edit) {
    for (GoodsSkuVO goodsSkuVO : skuList) {
    //先删除已有的脚本信息
    this.goodsClient.deleteSkuExchangeScript(
    goodsSkuVO.getSkuId());
    //生成脚本信息
    this.goodsClient.createSkuExchangeScript(
    goodsSkuVO.getSkuId(), exchangeDO.getExchangeId(),
    exchangeDO.getExchangeMoney(),
    exchangeDO.getExchangePoint());
    }
    }

    if (delete) {
    for (GoodsSkuVO goodsSkuVO : skuList) {
    //删除已有的脚本信息
    this.goodsClient.deleteSkuExchangeScript(
    goodsSkuVO.getSkuId());
    }
    }
    }
    }
    }
    }
    }
    @Service
    public class GoodsSkuManagerImpl implements GoodsSkuManager {

    @Override
    public void createSkuExchangeScript(Long skuId, Long exchangeId,
    Double price, Integer point) {

    //缓存key
    String cacheKey = CachePrefix.SKU_PROMOTION.getPrefix() + skuId;

    //积分兑换商品脚本信息
    PromotionScriptVO scriptVO = new PromotionScriptVO();

    //渲染并读取积分兑换商品脚本信息
    String script = renderScript(price, point);

    scriptVO.setPromotionScript(script);
    scriptVO.setPromotionId(exchangeId);
    scriptVO.setPromotionType(PromotionTypeEnum.EXCHANGE.name());
    scriptVO.setIsGrouped(false);
    scriptVO.setPromotionName("积分兑换");
    scriptVO.setSkuId(skuId);

    //从缓存中读取脚本信息
    List<PromotionScriptVO> scriptList = (List<PromotionScriptVO>)
    cache.get(cacheKey);
    if (scriptList == null) {
    scriptList = new ArrayList<>();
    }
    scriptList.add(scriptVO);

    cache.put(cacheKey, scriptList);

    }

    @Override
    public void deleteSkuExchangeScript(Long skuId) {
    //缓存key
    String cacheKey = CachePrefix.SKU_PROMOTION.getPrefix() + skuId;
    //从缓存中读取脚本信息
    List<PromotionScriptVO> scriptList = (List<PromotionScriptVO>)
    cache.get(cacheKey);
    if (scriptList != null && scriptList.size() != 0) {
    //循环促销脚本缓存数据集合
    for (PromotionScriptVO script : scriptList) {
    //如果脚本数据的促销活动信息与当前修改的促销活动信息一致,那么就将此信息删除
    if (PromotionTypeEnum.EXCHANGE.name().equals(script.getPromotionType())
    && script.getSkuId().intValue() == skuId.intValue()) {
    scriptList.remove(script);
    break;
    }
    }

    //如果经过上面的处理过后脚本集合长度为0,那么直接删除;如果不为0,
    //那么把剩余的脚本信息重新放入缓存中。
    if (scriptList.size() == 0) {
    cache.remove(cacheKey);
    } else {
    cache.put(cacheKey, scriptList);
    }
    }
    }

    @Override
    public void deleteSkuExchangeScript(Long goodsId, String oldType, String newType) {
    //如果原商品类型为积分商品,先商品类型为普通商品
    if (GoodsType.POINT.name().equals(oldType) &&
    GoodsType.NORMAL.name().equals(newType)) {
    //获取商品的SKU信息
    List<GoodsSkuVO> skuList = this.listByGoodsId(goodsId);
    for (GoodsSkuVO goodsSkuVO : skuList) {
    this.deleteSkuExchangeScript(goodsSkuVO.getSkuId());
    }
    }
    }

    /**
    * 渲染并读取积分兑换商品脚本信息
    *
    * @param price 兑换积分商品所需的价钱
    * @param point 兑换积分商品所需的积分
    * @return
    */
    private String renderScript(Double price, Integer point) {
    Map<String, Object> model = new HashMap<>();

    Map<String, Object> params = new HashMap<>();
    params.put("price", price);
    params.put("point", point);

    model.put("goods", params);

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

    logger.debug("生成积分兑换商品脚本:" + script);

    return script;
    }

    /**
    * 清除缓存中的商品SKU促销信息脚本
    *
    * @param skuList 编辑后的商品SKU集合
    * @param oldSkuList 编辑前的商品SKU集合
    */
    private void cleanSkuPromotionScript(List<GoodsSkuVO> skuList,
    List<GoodsSkuVO> oldSkuList) {
    //将编辑后的商品skuID取出
    List<Long> newSkuList = new ArrayList<Long>();
    for (GoodsSkuVO goodsSkuVO : skuList) {
    if (goodsSkuVO.getSkuId() != null && goodsSkuVO.getSkuId() != 0) {
    newSkuList.add(goodsSkuVO.getSkuId());
    }
    }

    //如果编辑后的skuID集合长度为0,证明之前存在的SKU均已删除,
    //那么编辑前的所有商品SKU的促销脚本信息均要删除
    //长度不为0,证明之前存在的SKU还有部分存在,那么取出不存在的商品SKU删除促销脚本信息
    if (newSkuList.size() == 0) {
    for (GoodsSkuVO goodsSkuVO : oldSkuList) {
    cache.remove(CachePrefix.SKU_PROMOTION.getPrefix()
    + goodsSkuVO.getSkuId());
    }
    } else {
    for (GoodsSkuVO goodsSkuVO : oldSkuList) {
    if (!newSkuList.contains(goodsSkuVO.getSkuId())) {
    cache.remove(CachePrefix.SKU_PROMOTION.getPrefix()
    + goodsSkuVO.getSkuId());
    }
    }
    }
    }

    /**
    * 积分兑换商品修改商品规格更新缓存中的脚本信息
    *
    * @param goodsId 商品id
    * @param goodsType 商品类型
    * @param oldSkuList 原商品SKU信息集合
    */
    private void updateSkuExchangeScript(Long goodsId, String goodsType,
    List<GoodsSkuVO> oldSkuList) {
    //如果商品为积分兑换商品,需要更新缓存中的积分商品脚本信息
    if (GoodsType.POINT.name().equals(goodsType)) {
    //获取积分兑换商品信息
    ExchangeDO exchangeDO = this.exchangeGoodsClient.getModelByGoods(goodsId);

    //先删除规格修改前积分商品sku已有的脚本信息
    for (GoodsSkuVO goodsSkuVO : oldSkuList) {
    this.deleteSkuExchangeScript(goodsSkuVO.getSkuId());
    }

    //获取商品规格变化之后的sku信息集合
    List<GoodsSkuVO> newSkuList = this.listByGoodsId(goodsId);

    //重新生成脚本信息
    for (GoodsSkuVO goodsSkuVO : newSkuList) {
    this.createSkuExchangeScript(
    goodsSkuVO.getSkuId(), exchangeDO.getExchangeId(),
    exchangeDO.getExchangeMoney(), exchangeDO.getExchangePoint());
    }
    }
    }
    }
  • 积分兑换脚本引擎模板文件—exchange.ftl

    <#--
    此方法会直接返回true,积分兑换不涉及有效期,脚本中有此方法是为了脚本内容统一
    @returns {boolean}
    -->
    function validTime(){
    return true;
    }

    <#--
    计算兑换积分商品所需要的金额
    @param goods 积分商品对象(内置常量)
    .price 兑换积分商品所需要的金额
    @param $sku 商品SKU信息对象(变量)
    .$num 商品数量
    @returns {*}
    -->
    function countPrice() {
    var resultPrice = $sku.$num * ${goods.price};
    return resultPrice < 0 ? 0 : resultPrice.toString();
    }

    <#--
    计算兑换积分商品所需要的金额
    @param goods 积分商品对象(内置常量)
    .point 兑换积分商品所需要的积分
    @param $sku 商品SKU信息对象(变量)
    .$num 商品数量
    @returns {*}
    -->
    function countPoint() {
    return ($sku.$num * ${goods.point}).toString();
    }