积分兑换
一、功能说明
1、所谓积分兑换就是用户可以用消费积分来兑换商家发布的积分商品。
2、目前只有自营店铺可以发布积分商品。
3、积分商品兑换条件有两种形式:
纯积分兑换:商家发布积分商品时,可以设置兑换条件为0元+X积分。
积分+价钱兑换:商家在发布积分商品时,可以设置兑换条件为X元+X积分。
4、积分商品有单独的商品分类,在平台管理端可添加积分分类。
积分分类暂时只支持一级分类。
5、对积分商品申请售后,用户积分不做退还处理。
二、数据库设计
1、表结构设计
积分兑换表—es_exchange
字段名 类型与长度 说明 exchange_id bigint(20) 主键ID goods_id bigint(20) 商品ID(关联es_goods表) category_id bigint(20) 积分分类ID(关联es_exchange_cat表) enable_exchange int(11) 是否允许兑换 0:否,1:是 exchange_money decimal(20,2) 兑换所需金额 exchange_point int(11) 兑换所需积分 goods_name varchar(100) 商品名称 goods_price decimal(20,2) 商品原价 goods_img varchar(255) 商品图片
积分商品分类表—es_exchange_cat
字段名 类型与长度 说明 category_id bigint(20) 主键ID name varchar(255) 分类名称 parent_id bigint(20) 父分类ID(预留字段,暂时无用) category_path varchar(255) 分类ID路径(预留字段,暂时无用) goods_count int(11) 此分类下的积分商品数量 category_order int(11) 分类排序(值越小排序越靠前) list_show int(10) 是否显示 0:否,1:是 image varchar(255) 分类图片
2、表关联说明
积分兑换表与其他表之间的关联图
积分兑换表与其他表之间的关联字段对照
积分兑换表(es_exchange) 积分商品分类表(es_exchange_cat) category_id category_id 积分兑换表(es_exchange) 商品表(es_goods) goods_id goods_id 积分兑换表(es_exchange) 促销活动商品表(es_promotion_goods) exchange_id activity_id 无字段,促销类型值为EXCHANGE promotion_type
三、缓存设计
1、商家在发布积分商品时,在将积分商品信息入库的同时,也会将积分兑换信息放入缓存中。
缓存key值为:{STOREID_EXCHANGE_KEY}积分兑换ID。
缓存value值为:ExchangeDO.java这个实体对象信息。
2、积分兑换脚本引擎
脚本引擎缓存结构:
积分兑换活动的促销脚本引擎缓存结构只有一种:SKU级别的缓存结构。
脚本引擎生成和删除时机:
- 生成:商家发布积分商品,通过发送消息,在消费者中生成。
- 删除:商家删除积分商品或者将积分商品修改为普通商品时删除。
关于促销脚本引擎缓存结构可参考《促销活动脚本引擎生成架构》这篇文档。
四、代码设计
1、接口调用流程图
平台和商家发布积分商品
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();
}