一、环境搭建
1、静态页面
二、订单确认页
1、OrderWebController
@Controller
public class OrderWebController {
@Autowired
OrderService orderService;
// 点击购物车的去结算按钮,跳转到订单确认页面
@GetMapping("/toTrade")
public String toTrade(Model model){
OrderConfirmVo confirmVo = orderService.confirmOrder();
// 展示订单确认的数据
model.addAttribute("orderConfirmData",confirmVo);
return "confirm";
}
}
2、OrderConfirmVo
package com.atguigu.gulimall.order.vo;
public class OrderConfirmVo {
// 会员的收货地址列表
@Setter @Getter
List<MemberAddressVo> address;
// 所有选中的购物项
@Setter @Getter
List<OrderItemVo> items;
// 发票记录...
// 优惠券信息...
// 会员的积分信息
@Setter @Getter
Integer integration;
// 商品的库存信息,键为skuId,值为是否有库存
@Setter @Getter
Map<Long,Boolean> stocks;
//唯一令牌,多次提交订单防重(如网络问题用户多次提交订单)
@Getter @Setter
String token;
public Integer getCount() {
Integer i = 0;
if(items!=null){
for (OrderItemVo item : items){
i+=item.getCount();
}
}
return i;
}
// 订单总额
// BigDecimal total;
public BigDecimal getTotal() {
BigDecimal sum = new BigDecimal("0");
if(items!=null){
for (OrderItemVo item : items){
// 当前价格乘以数量
BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
sum = sum.add(multiply);
}
}
return sum;
}
// 应付总额
// BigDecimal payPrice;
public BigDecimal getPayPrice() {
BigDecimal sum = new BigDecimal("0");
if(items!=null){
for (OrderItemVo item : items){
// 当前价格乘以数量
BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
sum = sum.add(multiply);
}
}
return sum;
}
}
3、OrderConstant
package com.atguigu.gulimall.order.constant;
public class OrderConstant {
// 用户订单令牌前置
public static final String USER_ORDER_TOKEN_PREFIX = "order:token";
}
4、OrderServiceImpl
/**
* 给订单确认页返回需要用的数据
* @return
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
// 登录拦截器,获取当前登录用户信息
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
// 获取主线程中的requestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 异步处理
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
// 每个子线程共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
// 1、远程查询所有的收获地址列表
List<MemberAddressVo> address = memberFeginService.getAddress(memberRespVo.getId());
orderConfirmVo.setAddress(address);
}, executor);
// 异步处理
CompletableFuture<Void> getCartItemsFuture = CompletableFuture.runAsync(() -> {
// 每个子线程共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
// 2、远程查询购物车所有选中的购物项,要注意获取最新的商品价格,而不是先前存到redis中的商品价格
// 利用fegin的RequestInterceptor拦截器功能,远程调用其他服务时,其他服务也能感知当前登录的用户
List<OrderItemVo> cartItems = cartFeginService.getCurrentUserCartItems();
orderConfirmVo.setItems(cartItems);
}, executor).thenRunAsync(() -> {
// 获取购物项中的商品skuId
List<OrderItemVo> items = orderConfirmVo.getItems();
List<Long> collect = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());
// 远程调用库存服务,获取商品的库存信息
R skuHasStock = wmsFeignService.getSkuHasStock(collect);
List<SkuStockVo> stockVos = skuHasStock.getData(new TypeReference<List<SkuStockVo>>() { });
if(stockVos != null){
Map<Long, Boolean> map = stockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
// 设置购物车中商品的库存信息
orderConfirmVo.setStocks(map);
}
},executor);
// 3、查询用户积分信息
Integer integration = memberRespVo.getIntegration();
orderConfirmVo.setIntegration(integration);
// 4、其他数据,如订单总价,商品总价自动计算
// TODO 5、防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
// 防重令牌保存至服务器
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), token, 30 , TimeUnit.MINUTES);
// 防重令牌发送给页面
orderConfirmVo.setToken(token);
CompletableFuture.allOf(getAddressFuture,getCartItemsFuture).get();
return orderConfirmVo;
}
5、Feign远程调用丢失请求头问题
1)feign远程调用的请求头中没有含有JSESSIONID的cookie,所以也就不能得到服务端的session数据,cart认为没登录,获取不了用户信息
2)但是在feign的调用过程中,会使用容器中的RequestInterceptor对RequestTemplate进行处理,因此我们可以通过向容器中导入定制的RequestInterceptor为请求加上cookie。
3)RequestContextHolder为SpingMVC中共享request数据的上下文,底层由ThreadLocal实现。经过RequestInterceptor处理后的请求如下,已经加上了请求头的Cookie信息
package com.atguigu.gulimall.order.config;
/**
* feign在远程调用之前都会先经过每个拦截器的apply(RequestTemplate template)方法,RequestTemplate相当于真正要发出去的请求
* feign在远程调用之前要构造请求,调用很多拦截器:RequestInterceptor interceptor :requestInterceptors
*
* 浏览器发controller请求,service要远程调用feign,feign要创建一个新的对象来发请求,在创建对象的时候会调用拦截器
* 拦截器、controller、service都在同一个线程。
* 拦截器若想获取到原生的请求数据,原始办法是controller中可以传递HttpServletRequest到拦截器
*
* RequestContextHolder:上下文环境保持器,利用ThreadLocal帮我们从请求一开始就把当前的请求数据放到ThreadLocal,随用随取
* RequestContextHolder.getRequestAttributes():可以获取到当前请求的所有属性
*/
@Configuration
public class GuliFeignConfig {
// feign远程调用的请求拦截器
@Bean("requestInterceptor") // requestInterceptor拦截器名称。默认为方法名
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override // RequestTemplate新请求
public void apply(RequestTemplate template) {
// 1、RequestContextHolder拿到刚进来的request请求(@GetMapping("/toTrade"):里面带有cookie)
// 原理是通过threadLocal获取
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(attributes != null){
// 获取当前请求对象(老请求)。相当于controller中可以传递HttpServletRequest
HttpServletRequest request = attributes.getRequest();
if (request != null) {
// 2、同步请求头信息:Cookie
String cookie = request.getHeader("Cookie");
// 给新请求(远程调用请求)同步老请求的cookie
template.header("Cookie", cookie);
// System.out.println("feign远程之前先执行RequestInterceptor.apply方法");
}
}
}
};
}
}
6、Feign异步情况丢失上下文问题
6.1 原因
1)查询购物项、库存和收货地址都要调用远程服务,串行会浪费大量时间,因此我们使用CompletableFuture进行异步编排
2)由于RequestContextHolder使用ThreadLocal共享数据,所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie了。在这种情况下,我们需要在开启异步的时候将老请求的RequestContextHolder的数据设置进去
6.2 解决方案
7、订单确认页流程
8、创建防重令牌
三、提交订单
1、下单流程
2、OrderSubmitVo
package com.atguigu.gulimall.order.vo;
// 封装订单提交的数据
@Data
public class OrderSubmitVo {
// 收货地址的ID
private Long addrId;
// 支付方式
private Integer payType;
// 防重令牌
private String orderToken;
// 无需提交订单确认页需要购买的商品,直接去购物车再获取一遍
// 应付总额,验价(商品价格与购物车价格是否一致)
private BigDecimal payPrice;
// 备注信息
private String note;
// 用户相关信息从session中取出
}
3、SubmitOrderResponseVo
package com.atguigu.gulimall.order.vo;
// 下单操作后的返回信息
@Data
public class SubmitOrderResponseVo {
// 订单的实体类
private OrderEntity order;
// 下单失败的错误状态码,0表示成功
private Integer code;
}
4、OrderWebController
/**
* 下单功能,提交订单。 需要去创建订单、验令牌、验价格、锁库存
* @param vo 订单提交的数据
* @param model
* @param redirectAttributes
* @return 下单成功来到支付选择页,下单失败回到订单确认页重新确认订单信息
*/
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model,
RedirectAttributes redirectAttributes) {
try {
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
if (responseVo.getCode() == 0) {
model.addAttribute("submitOrderResp", responseVo); // 返回页面数据
// 下单成功来到支付选择页
return "pay";
} else {
String msg = "下单失败";
switch (responseVo.getCode()) {
case 1:
msg += "订单信息过期,请刷新重新提交";
break;
case 2:
msg += "订单商品价格发生变化,请确认后再次提交";
break;
case 3:
msg += "商品库存不足";
}
redirectAttributes.addFlashAttribute("msg", msg); // 返回页面数据
// 下单失败回到订单确认页重新确认订单信息
return "redirect:http://order.gulimall.com/toTrade";
}
}catch (Exception e){
if(e instanceof NoStockException){
String msg = ((NoStockException) e).getMessage();
redirectAttributes.addFlashAttribute("msg",msg);
}
return "redirect:http://order.gulimall.com/toTrade";
}
}
4.1 下单失败
5、OrderServiceImpl
5.1 验证令牌
5.2 构造订单数据
1、OrderCreateTo
package com.atguigu.gulimall.order.to;
@Data
public class OrderCreateTo {
// 订单信息
private OrderEntity order;
// 每个订单项
private List<OrderItemEntity> items;
// 计算的应付订单总额
private BigDecimal payPrice;
// 运费
private BigDecimal fare;
}
2、创建订单createOrder
// 创建订单
private OrderCreateTo createOrder() {
OrderCreateTo orderCreateTo = new OrderCreateTo();
// 1、构建订单
String orderSn = IdWorker.getTimeId();
OrderEntity orderEntity = buildOrder(orderSn);
// 2、获取所有的订单项
List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
// 3、验价
computePrice(orderEntity, itemEntities);
orderCreateTo.setItems(itemEntities);
orderCreateTo.setOrder(orderEntity);
return orderCreateTo;
}
5.3、锁定库存
1、锁库存逻辑
2、WareSkuController
/**
* 为当前订单锁定库存
*/
@PostMapping("/lock/order")
public R orderLockStock(@RequestBody WareSkuLockVo vo) {
try{
Boolean stock = wareSkuService.orderLockStock(vo);
return R.ok();
}catch (NoStockException e){
return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(),BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());
}
}