整体概要
在实现购物车选中商品结算页面时,会查询购物车中的商品所能使用的优惠活动和优惠券(前提是当前用户要具有此优惠券),并自动进行额度减免计算,默认为最优减免选择,此文详细描述如何实现此业务逻辑:
前置信息说明
现在有一个实体类 CartInfo 描述的是购物车中每个商品的详细信息
@Data
@ApiModel(description = "CartInfo")
public class CartInfo extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "用户id")
private Long userId;
@ApiModelProperty(value = "分类id")
private Long categoryId;
@ApiModelProperty(value = "商品类型:0->普通商品 1->秒杀商品")
private Integer skuType;
@ApiModelProperty(value = "是否新人专享:0->否;1->是")
private Integer isNewPerson;
@ApiModelProperty(value = "sku名称 (冗余)")
private String skuName;
@ApiModelProperty(value = "skuid")
private Long skuId;
@ApiModelProperty(value = "放入购物车时价格")
private BigDecimal cartPrice;
@ApiModelProperty(value = "数量")
private Integer skuNum;
@ApiModelProperty(value = "限购个数")
private Integer perLimit;
@ApiModelProperty(value = "图片文件")
private String imgUrl;
@ApiModelProperty(value = "isChecked")
private Integer isChecked;
@ApiModelProperty(value = "状态")
private Integer status;
@ApiModelProperty(value = "秒杀开始时间")
@JsonFormat(pattern = "HH:mm:ss")
private Date startTime;
@ApiModelProperty(value = "秒杀结束时间")
@JsonFormat(pattern = "HH:mm:ss")
private Date endTime;
@ApiModelProperty(value = "仓库")
private Long wareId;
@ApiModelProperty(value = "当天已购买个数")
private Integer currentBuyNum = 0;
}
加入购物车时我们的实现是将CartInfo列表放入redis中 ,以Hash结构存储,用户id为key 商品id为feild,CartInfo为value,设有过期时间,很好的满足了购物车这一特性
使用到的数据库表
商品表
sku_Info(描述商品必要信息,商品有不同的分类)
优惠活动:一种商品可以对应多个优惠活动,一个活动可以对应多个优惠规则,(一种商品可以对应多种规则)现实中购物也是如此。
activity_info(优惠活动表如:五一促销 国庆促销等不同活动)
activity_rule(活动规则表,如满5减2,满多少打几折等优惠规则)
activity_sku (优惠活动和商品之间的对应关系)
优惠券:一个用户可以有多个优惠券,一张优惠券有多种使用范围(一个优惠卷可以全场通用,可以固定分类使用,可以固定商品使用)
coupon_info(优惠券表)
coupon_range(优惠券使用范围)
coupon_use(用户和优惠券之间的对应关系)
详细逻辑分析
获取购物车中每个购物项参与的活动,一个活动有多种规则,根据活动规则进行分组,因为活动规则才能确定优惠信息,根据分组计算商品参与活动之后的金额是多少,之后获取购物车中可以使用的优惠券列表,根据优惠券进行分组,计算商品使用优惠券之后的金额(一次只能使用一张优惠券)计算没有参与活动,没有使用优惠券的原始金额 ,计算参与活动,使用优惠券之后的金额,封装需要的数据返回。
业务实现细节
特别说明。本文是基于项目的实现,可能代码和数据库中有与本文主要实现无关的部分,存在过度设计,请谅解
接口调用
说明从当前线程中获取当前用户id,根据当前用户id从redis中获取购物车列表CartList,远程调用方法实现带优惠活动和优惠券的购物车列表,返回数据类型为OrderConfirmVo (本文最终封装目的)
/**
* 查询带优惠卷的购物车
*
* @return
*/
@GetMapping("activityCartList")
public Result activityCartList() {
// 获取用户Id
Long userId = AuthContextHolder.getUserId();
List<CartInfo> cartInfoList = cartInfoService.getCartList(userId);
OrderConfirmVo orderTradeVo =
activityFeignClient.findCartActivityAndCoupon(cartInfoList, userId);
return Result.ok(orderTradeVo);
}
@Data
public class OrderConfirmVo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "预生产订单号")
private String orderNo;
@ApiModelProperty(value = "用户对应的团长地址")
private LeaderAddressVo leaderAddressVo;
@ApiModelProperty(value = "购物项列表")
private List<CartInfoVo> carInfoVoList;
@ApiModelProperty(value = "订单优惠券列表")
private List<CouponInfo> couponInfoList;
@ApiModelProperty(value = "促销优惠金额")
private BigDecimal activityReduceAmount;
@ApiModelProperty(value = "优惠券优惠金额")
private BigDecimal couponReduceAmount;
@ApiModelProperty(value = "购物车原始总金额")
private BigDecimal originalTotalAmount;
@ApiModelProperty(value = "最终总金额")
private BigDecimal totalAmount;
}
远程方法定义
//获取购物车里面满足条件优惠卷和活动的信息
@PostMapping("inner/findCartActivityAndCoupon/{userId}")
public OrderConfirmVo findCartActivityAndCoupon(@RequestBody List<CartInfo> cartInfoList,
@PathVariable("userId") Long userId) {
return activityInfoService.findCartActivityAndCoupon(cartInfoList,userId);
}
接口定义
//获取购物车里面满足条件优惠卷和活动的信息
OrderConfirmVo findCartActivityAndCoupon(List<CartInfo> cartInfoList, Long userId);
方法实现:
1.先获取购物车数据,查询商品参与的活动,根据活动规则分组,把一种活动规则对应的商品集合封装,再整体封装
2.参加活动之后的减免金额计算
3.获取购物车中可以使用的优惠券列表
4.计算使用优惠券之后可以减免的金额
5.计算原始金额,和优惠之后的金额,封装数据返回
//获取购物车里面满足条件优惠卷和活动的信息
@Override
public OrderConfirmVo findCartActivityAndCoupon(List<CartInfo> cartInfoList,
Long userId) {
//1 获取购物车,查询每个商品参与活动,根据活动规则分组,
//一个活动规则对应多个商品
//cartInfoVoList封装一个规则对应的购物项信息,并封装成集合
List<CartInfoVo> cartInfoVoList = this.findCartActivityList(cartInfoList);
//2 计算参与活动之后优惠的金额
BigDecimal activityReduceAmount = cartInfoVoList
.stream()
.filter(cartInfoVo -> cartInfoVo.getActivityRule() != null)
.map(cartInfoVo -> cartInfoVo.getActivityRule().getReduceAmount())
.reduce(BigDecimal.ZERO, BigDecimal::add);
//3 获取购物车中的商品可以使用优惠卷列表
List<CouponInfo> couponInfoList =
couponInfoService.findCartCouponInfo(cartInfoList,userId);
//4 计算商品使用优惠卷之后金额,一次只能使用一张优惠卷
BigDecimal couponReduceAmount = new BigDecimal(0);
if(!CollectionUtils.isEmpty(couponInfoList)) {
//优惠券可以优惠的总金额
couponReduceAmount = couponInfoList.stream()
.filter(couponInfo -> couponInfo.getIsOptimal().intValue() == 1)
.map(couponInfo -> couponInfo.getAmount())
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
//5 计算没有参与活动,没有使用优惠卷原始金额
BigDecimal originalTotalAmount = cartInfoList.stream()
.filter(cartInfo -> cartInfo.getIsChecked() == 1)
.map(cartInfo -> cartInfo.getCartPrice().multiply(new BigDecimal(cartInfo.getSkuNum())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
//6 最终金额
BigDecimal totalAmount =
originalTotalAmount.subtract(activityReduceAmount).subtract(couponReduceAmount);
//7 封装需要数据到OrderConfirmVo,返回 用于生成订单数据
OrderConfirmVo orderTradeVo = new OrderConfirmVo();
orderTradeVo.setCarInfoVoList(cartInfoVoList);
orderTradeVo.setActivityReduceAmount(activityReduceAmount);
orderTradeVo.setCouponInfoList(couponInfoList);
orderTradeVo.setCouponReduceAmount(couponReduceAmount);
orderTradeVo.setOriginalTotalAmount(originalTotalAmount);
orderTradeVo.setTotalAmount(totalAmount); //最终总金额
return orderTradeVo;
}
获取购物车对应的活动规则
返回结果类CartInfoVo 用于封装一种活动的规则对应的商品列表有哪些 一个商品对应多个活动,一个活动有多个规则
@Data
public class CartInfoVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 购物项凑单,同一活动对应的最优活动规则
*/
@ApiModelProperty(value = "cartInfoList")
private List<CartInfo> cartInfoList;
@ApiModelProperty(value = "活动规则")
private ActivityRule activityRule;
}
获取购物车对应规则数据
@Override
public List<CartInfoVo> findCartActivityList(List<CartInfo> cartInfoList) {
//创建最终返回集合
List<CartInfoVo> cartInfoVoList = new ArrayList<>();
//获取所有skuId
List<Long> skuIdList = cartInfoList
.stream()
.map(CartInfo::getSkuId)
.collect(Collectors.toList());
//根据所有skuId列表获取参与活动
List<ActivitySku> activitySkuList = baseMapper.selectCartActivity(skuIdList);
//根据活动进行分组,每个活动里面有哪些skuId信息
//map里面key是分组字段 活动id
// value是每组里面sku列表数据,set集合
Map<Long, Set<Long>> activityIdToSkuIdListMap = activitySkuList
.stream()
.collect(
Collectors.groupingBy(
ActivitySku::getActivityId,
Collectors.mapping(ActivitySku::getSkuId, Collectors.toSet())
)
);
//获取活动里面规则数据
//key是活动id value是活动里面规则列表数据
Map<Long,List<ActivityRule>> activityIdToActivityRuleListMap
= new HashMap<>();
//所有活动id
Set<Long> activityIdSet = activitySkuList
.stream()
.map(ActivitySku::getActivityId)
.collect(Collectors.toSet());
if(!CollectionUtils.isEmpty(activityIdSet)) {
//activity_rule表
LambdaQueryWrapper<ActivityRule> wrapper = new LambdaQueryWrapper<>();
//排序规则
wrapper.orderByDesc(ActivityRule::getConditionAmount,ActivityRule::getConditionNum);
//选择条件
wrapper.in(ActivityRule::getActivityId,activityIdSet);
//规则数据库列表
List<ActivityRule> activityRuleList = activityRuleMapper.selectList(wrapper);
//封装到activityIdToActivityRuleListMap里面
//根据活动id进行分组(一个活动可以对应多个满减规则)
activityIdToActivityRuleListMap = activityRuleList
.stream()
.collect(Collectors.groupingBy(activityRule -> activityRule.getActivityId())
);
}
//有活动的购物项skuId
Set<Long> activitySkuIdSet = new HashSet<>();
if(!CollectionUtils.isEmpty(activityIdToSkuIdListMap)) {
//遍历activityIdToSkuIdListMap集合
Iterator<Map.Entry<Long, Set<Long>>> iterator
= activityIdToSkuIdListMap.entrySet().iterator();
while(iterator.hasNext()) {
Map.Entry<Long, Set<Long>> entry = iterator.next();
//活动id
Long activityId = entry.getKey();
//每个活动对应skuId列表
Set<Long> currentActivitySkuIdSet = entry.getValue();
//获取当前活动对应的购物项列表
//参加活动的购物项的列表
List<CartInfo> currentActivityCartInfoList = cartInfoList
.stream()
.filter(cartInfo ->
currentActivitySkuIdSet.contains(cartInfo.getSkuId()))
.collect(Collectors.toList());
//计数购物项总金额和总数量
BigDecimal activityTotalAmount =
this.computeTotalAmount(currentActivityCartInfoList);
int activityTotalNum =
this.computeCartNum(currentActivityCartInfoList);
//计算活动对应规则
//根据activityId获取活动对应规则
List<ActivityRule> currentActivityRuleList =
activityIdToActivityRuleListMap.get(activityId);
//活动类型
ActivityType activityType = currentActivityRuleList.get(0).getActivityType();
//判断活动类型:满减和打折
ActivityRule activityRule = null;
if(activityType == ActivityType.FULL_REDUCTION) {//满减"
activityRule =
this.computeFullReduction(activityTotalAmount,
currentActivityRuleList);
} else {//满量
activityRule
= this.computeFullDiscount(activityTotalNum,
activityTotalAmount,
currentActivityRuleList);
}
//CartInfoVo封装
CartInfoVo cartInfoVo = new CartInfoVo();
cartInfoVo.setActivityRule(activityRule);
cartInfoVo.setCartInfoList(currentActivityCartInfoList);
cartInfoVoList.add(cartInfoVo);
//记录哪些购物项参与活动
activitySkuIdSet.addAll(currentActivitySkuIdSet);
}
}
//没有活动购物项skuId
//获取哪些skuId没有参加活动
skuIdList.removeAll(activitySkuIdSet);
if(!CollectionUtils.isEmpty(skuIdList)) {
//skuId对应购物项
Map<Long, CartInfo> skuIdCartInfoMap = cartInfoList.stream().collect(
Collectors.toMap(CartInfo::getSkuId, CartInfo -> CartInfo)
);
for(Long skuId : skuIdList) {
CartInfoVo cartInfoVo = new CartInfoVo();
cartInfoVo.setActivityRule(null);//没有活动
List<CartInfo> cartInfos = new ArrayList<>();
CartInfo cartInfo = skuIdCartInfoMap.get(skuId);
cartInfos.add(cartInfo);
cartInfoVo.setCartInfoList(cartInfos);
cartInfoVoList.add(cartInfoVo);
}
}
return cartInfoVoList;
}
根据所有skuId列表获取参与活动
<!-- //根据所有skuId列表获取参与活动-->
<select id="selectCartActivity" resultMap="ActivitySkuMap">
select
info.id as activityId,
sku.sku_id as skuId
from activity_info info
inner join activity_sku sku on info.id = sku.activity_id
<where>
and sku.sku_id in
<foreach collection="skuIdList" item="item" open="(" close=")" separator=",">
#{item}
</foreach>
</where>
and now() between info.start_time and info.end_time
</select>
购物车可以使用的优惠卷列表
获取购物车可以使用的优惠券列表
//3 获取购物车可以使用优惠卷列表
@Override
public List<CouponInfo> findCartCouponInfo(List<CartInfo> cartInfoList,
Long userId) {
//1 根据userId获取当前用户全部优惠卷
//coupon_use coupon_info
List<CouponInfo> userAllCouponInfoList = baseMapper.selectCartCouponInfoList(userId);
if(CollectionUtils.isEmpty(userAllCouponInfoList)) {
return new ArrayList<CouponInfo>();
}
//2 从第一步返回list集合中,获取所有优惠卷id列表
List<Long> couponIdList = userAllCouponInfoList
.stream().map(couponInfo -> couponInfo.getId())
.collect(Collectors.toList());
//3 查询优惠卷对应的范围 coupon_range
//couponRangeList
LambdaQueryWrapper<CouponRange> wrapper = new LambdaQueryWrapper<>();
// id in (1,2,3)
wrapper.in(CouponRange::getCouponId,couponIdList);
List<CouponRange> couponRangeList = couponRangeMapper.selectList(wrapper);
//4 获取优惠卷id 对应skuId列表(一个优惠券可以多种商品使用)
//优惠卷id进行分组,得到map集合
// Map<Long,List<Long>>
Map<Long,List<Long>> couponIdToSkuIdMap =
this.findCouponIdToSkuIdMap(cartInfoList,couponRangeList);
//5 遍历全部优惠卷集合,判断优惠卷类型
//全场通用 sku和分类
BigDecimal reduceAmount = new BigDecimal(0);
CouponInfo optimalCouponInfo = null;
for(CouponInfo couponInfo:userAllCouponInfoList) {
//全场通用
if(CouponRangeType.ALL == couponInfo.getRangeType()) {
//全场通用
//判断是否满足优惠使用门槛
//计算购物车商品的总价
BigDecimal totalAmount = computeTotalAmount(cartInfoList);
if(totalAmount.subtract(couponInfo.getConditionAmount()).doubleValue() >= 0){
couponInfo.setIsSelect(1); //是否选择该优惠券
}
} else {
//优惠卷id获取对应skuId列表
List<Long> skuIdList
= couponIdToSkuIdMap.get(couponInfo.getId());
//查询满足使用范围购物项
//(可使用优惠券的购物单集合)
List<CartInfo> currentCartInfoList = cartInfoList.stream()
.filter(cartInfo -> skuIdList.contains(cartInfo.getSkuId()))
.collect(Collectors.toList());
BigDecimal totalAmount = computeTotalAmount(currentCartInfoList);
if(totalAmount.subtract(couponInfo.getConditionAmount()).doubleValue() >= 0){
couponInfo.setIsSelect(1);
}
}
if (couponInfo.getIsSelect().intValue() == 1 &&
couponInfo.getAmount().subtract(reduceAmount).doubleValue() > 0) {
reduceAmount = couponInfo.getAmount();
optimalCouponInfo = couponInfo;
}
//优惠券已经排序。第一个已经是额度最大
}
//6 返回List<CouponInfo>
if(null != optimalCouponInfo) {
optimalCouponInfo.setIsOptimal(1);
}
return userAllCouponInfoList;
}
获取优惠卷Id 对应购物车中skuId列表(一个优惠券可以多种商品使用)
//4 获取优惠卷id 对应skuId列表(一个优惠券可以有多种商品使用)
//优惠卷id进行分组,得到map集合
private Map<Long, List<Long>> findCouponIdToSkuIdMap(List<CartInfo> cartInfoList,
List<CouponRange> couponRangeList) {
Map<Long, List<Long>> couponIdToSkuIdMap = new HashMap<>();
//couponRangeList数据处理,根据优惠卷id分组
//Map<CouponId, List<CouponRange>>
Map<Long, List<CouponRange>> couponRangeToRangeListMap = couponRangeList.stream()
.collect(
Collectors.groupingBy(couponRange -> couponRange.getCouponId())
);
//遍历map集合
Iterator<Map.Entry<Long, List<CouponRange>>> iterator =
couponRangeToRangeListMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, List<CouponRange>> entry = iterator.next();
Long couponId = entry.getKey();
List<CouponRange> rangeList = entry.getValue();
//创建集合 set
Set<Long> skuIdSet = new HashSet<>();
for (CartInfo cartInfo:cartInfoList) {
for (CouponRange couponRange:rangeList) {
//判断
if(couponRange.getRangeType() == CouponRangeType.SKU //商品优惠规则
&& couponRange.getRangeId().longValue() == cartInfo.getSkuId().longValue()) {
skuIdSet.add(cartInfo.getSkuId());
} else if(couponRange.getRangeType() == CouponRangeType.CATEGORY //分类优惠规则
&& couponRange.getRangeId().longValue() == cartInfo.getCategoryId().longValue()) {
//说明此规则属于该商品,可以使用 ,加入集合
skuIdSet.add(cartInfo.getSkuId());
} else {
//全场通用
}
}
}
couponIdToSkuIdMap.put(couponId,new ArrayList<>(skuIdSet));
}
return couponIdToSkuIdMap;
}