Description or Example
# 核心源码
```java
/**
* 在购物车中添加购物项
* @param redirectAttributes
* @return
*/
@GetMapping("/addCartItem")
public String addCartItem(RedirectAttributes redirectAttributes,
@RequestParam Long skuId,
@RequestParam Integer num) {
cartService.addSignalCartItem(skuId, num);
redirectAttributes.addAttribute("skuId", skuId);
return "redirect:http://cart.bitmall.com/success";
}
/**
* 跳转成功页
* @return
*/
@GetMapping("/success")
public ModelAndView cartSuccessIndex(@RequestParam Long skuId, ModelAndView mv) {
CartItemVO cartItemVO = cartService.getSignalCartItem(skuId);
mv.setViewName("success");
mv.addObject("cart", cartItemVO);
return mv;
}
```
```java
@Override
public void addSignalCartItem(Long skuId, Integer num) {
// 1. 获取对应的hash操作
BoundHashOperations<String, Object, Object> hashHandle = getHashHandle();
// 1.1 首先判断其是否已经存在, 如果已经存在可以直接累加, 不需要后续封装查询操作(这个需要写在前面)
String cartItemJson = (String) hashHandle.get(skuId.toString());
if (StringUtils.isNotBlank(cartItemJson)) { // 原来已经有了
CartItemVO cartItemVO = JSON.parseObject(cartItemJson, CartItemVO.class);
cartItemVO.setCount(cartItemVO.getCount() + num);
// 保存并离开
hashHandle.put(skuId.toString(), JSON.toJSONString(cartItemVO));
return;
}
// 2. 创建购物项视图对象
CartItemVO cartItemVO = new CartItemVO();
// 3.1 异步编排~查找Sku基本信息
CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
R info = skuService.info(skuId);
SkuInfoTO skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoTO>() {
});
cartItemVO.setImage(skuInfo.getSkuDefaultImg())
.setPrice(skuInfo.getPrice())
.setTitle(skuInfo.getSkuTitle());
}, threadPool);
// 3.2 异步编排~查找Sku销售属性信息
CompletableFuture<Void> getSkuAttrsTask = CompletableFuture.runAsync(() -> {
List<String> allSkuAttrValuesInCart = skuService.getAllSkuAttrValuesInCart(skuId);
cartItemVO.setSkuAttr(allSkuAttrValuesInCart);
}, threadPool);
// 3.3 当前线程~赋值剩余已知
cartItemVO.setCheck(true).setCount(num).setSkuId(skuId);
// 需要等待所有任务的完成
CompletableFuture.allOf(getSkuInfoTask, getSkuAttrsTask).join();
// 4. 保存单个购物项
hashHandle.put(skuId.toString(), JSON.toJSONString(cartItemVO));
}
@Override
public CartItemVO getSignalCartItem(Long skuId) {
// 获取hash操作
BoundHashOperations<String, Object, Object> hashHandle = getHashHandle();
String cartItemJson = (String) hashHandle.get(skuId.toString());
return JSON.parseObject(cartItemJson, CartItemVO.class);
}
private BoundHashOperations<String, Object, Object> getHashHandle() {
// 判断是否登录, 如果登录了使用在线购物车, 否则使用离线购物车
UserBO userBO = CartInterceptor.USER_INFO_ID.get();
Long userId = userBO.getUserId();
String userKey = userBO.getUserKey();
String cartKey = userId != null ? CART_PREFIX + userId : CART_PREFIX + userKey;
return redisTemplate.boundHashOps(cartKey);
}
```
```java
@RequestMapping("/cart/allAttrs/{skuId}")
public List<String> getAllSkuAttrValuesInCart(@PathVariable Long skuId) {
return skuSaleAttrValueService.getAllSkuAttrValuesInCart(skuId);
}
```
```java
@Override
public List<String> getAllSkuAttrValuesInCart(Long skuId) {
return skuSaleAttrValueDao.getAllSkuAttrValuesInCart(skuId);
}
```
```java
List<String> getAllSkuAttrValuesInCart(@Param("skuId") Long skuId);
```
```java
<select id="getAllSkuAttrValuesInCart" resultType="java.lang.String">
SELECT CONCAT(attr_name, ':', attr_value)
FROM pms_sku_sale_attr_value
WHERE sku_id = #{skuId};
</select>
```
# 核心要点
## 重复提交问题
### 什么是重复提交问题?
> 当我们将商品加入购物车的时候, 如果成功了, 我们最终会跳转到成功页面, 如果我们采取了渲染的策略, 类似于转发, 那么, 请求的地址仍然是添加购物车的地址, 如果我们刷新成功页面, 那么就会发生商品重复提交购物车的问题
### 怎么解决重复提交问题?
> 为了彻底解决上述的问题, 最好的方式是重定向, 因为重定向了就彻底解决浏览器的地址问题, 地址都改了, 怎么刷新都不怕
## 老师存在的问题
### 没有等待结果
> 没有等待结果, 即没有调用`join`或`get`方法, 这回有一个非常严重的问题, 就是, 异步线程没有处理完任务, 主线程就将没有成功赋值完全的对象存储到Redis, 造成数据的错误或异常
> 因此, 使用异步编排的时候, 一定要等待最终结果, 不能一直执行下去, 除非异步任务和主任务没有任何的关系
### 代码亮点
> 代码中, 我采取的策略是首先判断该商品在购物车是否存在, 而老师是最后才判断, 这会导致无论是否存在都会调用远程微服务都手机信息, 效率很低, 因此, 首先判断可以节省调用远程微服务的时间, 提高吞吐量
> 而在, 在代码中, 明确确认了每个异步任务需要做的点, 将一些已知要点放于主线程执行, 而老师是将一个公共点放在异步任务执行, 这样会导致主线程空闲的时间更多, 整体的执行效率更差
## `RedirectAttributes`的说明
### `redirectAttributes.addAttribute()`方法
> 这个方法并不是放在会话域里面, 这个方法是将里面的参数直接拼接在URL地址后面作为参数
### `redirectAttributes.addFlashAttribute()`方法
> 这个方法是放在会话域里面, 使用过一次直接删除