一、分析购物车vo
(1)添加成功页
public class CartItemVo implements Serializable {
/**
* 商品id
*/
private Long skuId;
/**
* 是否选中
*/
private Boolean check = true;
/**
* 商品标题
*/
private String title;
/**
* 商品图片
*/
private String image;
/**
* 商品属性
*/
private List<String> skuAttrValues;
/**
* 商品价格
*/
private BigDecimal price;
/**
* 商品数量
*/
private Integer count;
/**
* 商品总价 = 价格 * 数量
*/
private BigDecimal totalPrice;
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
public Boolean getCheck() {
return check;
}
public void setCheck(Boolean check) {
this.check = check;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public List<String> getSkuAttrValues() {
return skuAttrValues;
}
public void setSkuAttrValues(List<String> skuAttrValues) {
this.skuAttrValues = skuAttrValues;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
/**
* 计算当前购物项总价
* @return
*/
public BigDecimal getTotalPrice() {
return this.price.multiply(new BigDecimal(this.count.toString()));
}
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
@Override
public String toString() {
return "CartItemVo{" +
"skuId=" + skuId +
", check=" + check +
", title='" + title + '\'' +
", image='" + image + '\'' +
", skuAttrValues=" + skuAttrValues +
", price=" + price +
", count=" + count +
", totalPrice=" + totalPrice +
'}';
}
}
(2)结算页
public class Cart {
/**
* 购物车子项信息
*/
List<CartItemVo> items;
/**
* 购物车商品总数
*/
private Integer countNum;
/**
* 商品类型数量,有几种商品
*/
private Integer countType;
/**
* 结算价格 = 选中的商品价格 - 优惠价格
*/
private BigDecimal totalAmount;
/**
* 减免价格
*/
private BigDecimal reduce = new BigDecimal("0.00");;
public List<CartItemVo> getItems() {
return items;
}
public void setItems(List<CartItemVo> items) {
this.items = items;
}
public Integer getCountNum() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItemVo item : items) {
count += item.getCount();
}
}
return count;
}
public Integer getCountType() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItemVo item : items) {
count += 1;
}
}
return count;
}
public BigDecimal getTotalAmount() {
BigDecimal amount = new BigDecimal("0");
// 计算购物项总价
if (!CollectionUtils.isEmpty(items)) {
for (CartItemVo cartItem : items) {
if (cartItem.getCheck()) {
amount = amount.add(cartItem.getTotalPrice());
}
}
}
// 计算优惠后的价格
return amount.subtract(getReduce());
}
public BigDecimal getReduce() {
return reduce;
}
public void setReduce(BigDecimal reduce) {
this.reduce = reduce;
}
}
二、添加商品到购物车
购物车存储结构: Map<(redis的key)用户id, Map<商品skuId, 商品信息>>
1.通过ThreadLocal获取cookie中保存的用户信息,判断用户是否登陆,分别配置redis的key(boundHashOps)
2.如果购物车中已经存在该商品,只需要修改商品件数,不需要重新添加
3.feign获取sku商品信息、属性列表 ,使用CompletableFuture异步编排提升效率
为什么线程池不能放在common中?
为什么使用同一个线程池?
如果不添加自定义线程池会怎么样? 如果不指定线程池,默认使用 ForkJoinPool,是守护线程,主线程结束,线程池关闭,自定义线程池的需要手动关闭,
为什么通过调用get方法来完成操作,CompletableFuture.anyOf(skuRunAsync,skuAttrRunAsync).get(); ? 等待所有线程执行结束
为什么自定义线程后,调用线程却是调用 ThreadPoolExecutor ?
user-key是什么时候生成的?
此方法是将商品数据存入redis,这里选用redis是因为添加购物车操作会频繁的交互数据库,但是redis的持久性有没有数据库好,所有需要增强redis的持久性,redis存数据会使用序列化操作转成字符流进行保存,我们查看数据和获取都不方便,这里直接转换为JSON格式进行存储
/**
* 加入购物车
*/
@Override
public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
//绑定key值,不用要一直set key值,使用hash存储方便查询数据
CartItemVo cartItemVo = new CartItemVo();
BoundHashOperations<String, Object, Object> boundHashOperations = extracted();
/**
* 1.如果redis中已经保存了该商品,则只需要添加件数
*/
String res = (String) boundHashOperations.get(Long.toString(skuId));
if (!StringUtils.isEmpty(res)){
CartItemVo itemVo = JSON.parseObject(res, CartItemVo.class);
itemVo.setCount(itemVo.getCount() + num);
String jsonString = JSON.toJSONString(itemVo);
//这里是直接覆盖?
boundHashOperations.put(Long.toString(skuId),jsonString);
return itemVo;
}
/**
* 2.如果redis中未保存该商品
*/
//查询sku商品信息
CompletableFuture<Void> skuRunAsync = CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
R<SkuInfoVo> info = productFeignService.info(skuId);
if (info.getCode() == 0) {
SkuInfoVo skuInfoVo = info.getData(new TypeReference<SkuInfoVo>() {
});
cartItemVo.setSkuId(skuId);
cartItemVo.setCheck(true);
cartItemVo.setTitle(skuInfoVo.getSkuTitle());
cartItemVo.setImage(skuInfoVo.getSkuDefaultImg());
cartItemVo.setPrice(skuInfoVo.getPrice());
cartItemVo.setCount(num);
}
}
}, executor);
//查询sku属性列表
CompletableFuture<Void> skuAttrRunAsync = CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
R<List<String>> skuAttr = productFeignService.getSkuAttr(skuId);
if (skuAttr.getCode() == 0) {
List<String> skuAttrData = skuAttr.getData(new TypeReference<List<String>>() {
});
cartItemVo.setSkuAttrValues(skuAttrData);
}
}
}, executor);
//等待所有的异步任务全部完成
CompletableFuture.allOf(skuRunAsync,skuAttrRunAsync).get();
/**
* redis存数据会使用序列号操作转成字符流进行保存,我们查询数据和获取都不方便,这里直接转换为JSON格式进行存储
*/
//存入redis
String cartJson = JSON.toJSONString(cartItemVo);
boundHashOperations.put(Long.toString(skuId),cartJson);
System.out.println();
return cartItemVo;
}
封装redis的prefix前缀
/**
* 封装redis的prefix前缀
*/
private BoundHashOperations<String, Object, Object> extracted() {
UserInfoTo userInfoTo = CartIntercept.toThreadLocal.get();
String redisKey = "";
//判断用户是否登陆->封装前缀
if (userInfoTo.getUserId() != null){
//gulimall:cart:1
redisKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();
}else {
redisKey = CartConstant.CART_PREFIX + userInfoTo.getUserKey();
}
//绑定key值
BoundHashOperations<String, Object, Object> boundHashOperations = redisTemplate.boundHashOps(redisKey);
return boundHashOperations;
}
三、合并购物车(离线购物车与在线购物车合并)
(1)已登录
1. 获取离线购物车数据
如果有离线购物数据,有就取出数据,将离线购物数据与已登录购物数据合并,直接将离线数据添加到已登录的redis中,删除离线的redis
(1)如果离线购物车没数据,则只需要返回在线购物车的数据
(2)如果离线购物车有数据就合并数据
上面两种判断都涉及到离线购物车的数据,所以应该先做离线操作,
并且我们只需要返回在线购物车的数据,所以获取在线购物车的操作更因为放在最后执行
总结:以后再碰见 if(xx == null){}else(xx != null){} 的操作应该直接走 if(xx != null){}
2.获取已登录购物车数据,返回
(2)离线
获取离线购物车数据,返回
@Override
public Cart getCart() throws ExecutionException, InterruptedException {
Cart cart = new Cart();
UserInfoTo userInfoTo = CartIntercept.toThreadLocal.get();
//在线购物车(已登录用户的redis的key)
String onlineKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();
//离线购物车(临时用户的redis的key)
String offlineKey = CartConstant.CART_PREFIX + userInfoTo.getUserKey();
/**
* 已登录
*/
if (userInfoTo.getUserId() != null){
/**
* 这里是要做两次判断的
* (1)如果离线购物车没数据,则只需要返回在线购物车的数据
* (2)如果离线购物车有数据就合并数据
* 上面两种判断都涉及到离线购物车的数据,所以应该先做离线操作,
* 并且我们只需要返回在线购物车的数据,所以获取在线购物车的操作更因为放在最后执行
* 总结:以后再碰见 if(xx == null){}else(xx != null){} 的操作应该直接走 if(xx != null){}
*/
//判断离线是否有购物数据
List<CartItemVo> offlineVos = getCartFromRedis(offlineKey);
if (offlineVos != null){
//如果离线购物车有数据,就合并数据->将离线数据添加到已登录的数据中
for (CartItemVo offlineVo : offlineVos) {
/**
* 因为上面已经判断了用户已经登录,证明UserInfo的userId是存在的
* addToCart()也会通过extracted()得到已登录的redisKey
* 所有这里执行添加购物车的操作会直接添加到已登录的购物车中
*/
addToCart(offlineVo.getSkuId(),offlineVo.getCount());
//删除离线数据
redisTemplate.delete(offlineKey);
}
}
//再次查询在线购物车的数据,并返回
cart.setItems(getCartFromRedis(onlineKey));
}else {
/**
* 离线
*/
cart.setItems(getCartFromRedis(offlineKey));
}
return cart;
}
根据key获取redis中存的值
/**
* 根据key获取redis中存的值
*/
@Override
public List<CartItemVo> getCartFromRedis(String redisKey) {
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(redisKey);
List<Object> values = ops.values();
if (values != null && values.size() > 0){
List<CartItemVo> collect = values.stream().map(obj -> {
//因为我们存购物数据是使用的JSON格式,所有取数据时直接json字符串转java对象
String str = (String) obj;
CartItemVo itemVo = JSON.parseObject(str, CartItemVo.class);
return itemVo;
}).collect(Collectors.toList());
return collect;
}
return null;
}