订单服务的实现流程(确认订单->提交订单->支付)
1、整合SpringSession
使用SpringSession的目的是来解决分布式session不同步不共享的问题,其实就是为了让登录信息在订单微服务里共享
注意:由于这里使用springsession的用的类型是redis,所以这springsession和redis都要一起加入依赖和配置
(1)导入依赖
<!-- 整合springsession 来解决分布式session不同步不共享的问题--> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <!-- 整合redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
(2)在application.properties配置文件里配置springsession
#配置springsession spring.session.store-type=redis server.servlet.session.timeout=30m #配置redis的ip地址 spring.redis.host=192.168.241.128
(3)在config配置中加入springSession配置类
package com.saodai.saodaimall.order.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.DefaultCookieSerializer; /** * springSession配置类(所有要使用session的服务的session配置要一致) */ @Configuration public class GulimallSessionConfig { /** * 配置session(主要是为了放大session作用域) * @return */ @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); //放大作用域 cookieSerializer.setDomainName("saodaimall.com"); cookieSerializer.setCookieName("SAODAISESSION"); return cookieSerializer; } /** * 配置Session放到redis存储的格式为json(其实就是json序列化) * @return */ @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } }
(4)在启动类上添加@EnableRedisHttpSession注解
package com.saodai.saodaimall.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; /** * 订单服务启动类 */ @EnableFeignClients @EnableRedisHttpSession @EnableDiscoveryClient @SpringBootApplication public class SaodaimallOrderApplication { public static void main(String[] args) { SpringApplication.run(SaodaimallOrderApplication.class, args); } }
2、增加登录拦截器
(1)点击去结算后会去订单详情确认页面,这个时候需要用户登录才可以去结算
package com.saodai.saodaimall.order.interceptor; import com.saodai.common.vo.MemberResponseVo; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import static com.saodai.common.constant.AuthServerConstant.LOGIN_USER; /** * 登录拦截器 */ @Component public class LoginUserInterceptor implements HandlerInterceptor { public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { /** *直接放行的路径(也就是下面的两个路径不需要登录用于库存解锁) */ String uri = request.getRequestURI(); AntPathMatcher antPathMatcher = new AntPathMatcher(); //根据订单号查询订单实体类 boolean match = antPathMatcher.match("/order/order/status/**", uri); //支付宝支付成功后的异步回调 boolean match1 = antPathMatcher.match("/payed/notify", uri); if (match || match1) { return true; } //获取登录的用户信息 MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(LOGIN_USER); if (attribute != null) { //把登录后用户的信息放在ThreadLocal里面进行保存 loginUser.set(attribute); return true; } else { //未登录,返回登录页面 request.getSession().setAttribute("msg", "请先进行登录"); response.sendRedirect("http://auth.saodaimall.com/login.html"); return false; } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
(2)一定要记得在SpringMVC的配置文件里注册拦截器,不然拦截器不会生效
package com.saodai.saodaimall.order.config; import com.saodai.saodaimall.order.interceptor.LoginUserInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * springmvc配置类 **/ @Configuration public class OrderWebConfig implements WebMvcConfigurer { @Autowired private LoginUserInterceptor loginUserInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**"); } }
3、根据订单业务需求抽取模型
(1)订单确认页类OrderConfirmVo(也就是订单确认页需要用的数据)
package com.saodai.saodaimall.order.vo; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; import java.util.List; import java.util.Map; /** * 订单确认页类(订单确认页需要用的数据) **/ public class OrderConfirmVo { @Getter @Setter /** 会员收获地址列表 **/ List<MemberAddressVo> memberAddressVos; @Getter @Setter /** 所有选中的购物项 **/ List<OrderItemVo> items; /** 发票记录 **/ @Getter @Setter /** 优惠券(会员积分) **/ private Integer integration; /** 防止重复提交的令牌 **/ @Getter @Setter private String orderToken; @Getter @Setter Map<Long,Boolean> stocks; public Integer getCount() { Integer count = 0; if (items != null && items.size() > 0) { for (OrderItemVo item : items) { count += item.getCount(); } } return count; } /** 订单总额 **/ //BigDecimal total; //计算订单总额 public BigDecimal getTotal() { BigDecimal totalNum = BigDecimal.ZERO; if (items != null && items.size() > 0) { for (OrderItemVo item : items) { //计算当前商品的总价格 BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString())); //再计算全部商品的总价格 totalNum = totalNum.add(itemPrice); } } return totalNum; } /** 应付价格 **/ //BigDecimal payPrice; public BigDecimal getPayPrice() { return getTotal(); } }
(2)订单项类OrderItemVo
package com.saodai.saodaimall.order.vo; import lombok.Data; import java.math.BigDecimal; import java.util.List; /** * 订单项类(其实就是购物项) **/ @Data public class OrderItemVo { private Long skuId; // private Boolean check; private String title; private String image; /** * 商品套餐属性 */ private List<String> skuAttrValues; private BigDecimal price; private Integer count; private BigDecimal totalPrice; private Boolean hasStock; /** 商品重量 **/ private BigDecimal weight = new BigDecimal("0.085"); }
(3)用户收货信息类MemberAddressVo
package com.saodai.saodaimall.order.vo; import lombok.Data; /** * 用户订单的地址 **/ @Data public class MemberAddressVo { /** * 地址id */ private Long id; /** * member_id */ private Long memberId; /** * 收货人姓名 */ private String name; /** * 电话 */ private String phone; /** * 邮政编码 */ private String postCode; /** * 省份/直辖市 */ private String province; /** * 城市 */ private String city; /** * 区 */ private String region; /** * 详细地址(街道) */ private String detailAddress; /** * 省市区代码 */ private String areacode; /** * 是否默认 */ private Integer defaultStatus; }
4、点击去结算按钮(确认订单)
(1)订单服务的web的OrderWebController控制器来处理/toTrade请求
@Autowired private OrderService orderService; /** * 去结算确认页 * @param model * @param request * @return * @throws ExecutionException * @throws InterruptedException */ @GetMapping(value = "/toTrade") public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException { OrderConfirmVo confirmVo = orderService.confirmOrder(); model.addAttribute("confirmOrderData",confirmVo); //展示订单确认的数据 return "confirm"; }
(2)处理/toTrade请求的confirmOrder方法具体实现
流程:
1、远程调用会员服务来查询所有的收获地址列表
2、远程调用购物车服务来查询购物车所有选中的购物项
3、远程调用库存服务来批量查询所有商品的库存是否有货
4、查询用户积分
5、生成一个防重令牌并放到redis中(防止表单重复提交),这格令牌是会过期的,过期时间为30分钟(注意是结算的时候服务器生成一个防重令牌,然后把这个令牌隐藏在订单页面,点击去结算只是生成这个令牌并存到redis中和订单页面上,下面的点击提交订单按钮才会做校验令牌,保证原子性,防刷这些操作)
格式为key:order:token:+用户id,value:防重令牌
/** * 去结算确认页时封装订单确认页返回需要用的数据 * @return * @throws ExecutionException * @throws InterruptedException */ @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { //构建OrderConfirmVo OrderConfirmVo confirmVo = new OrderConfirmVo(); //获取当前用户登录的信息(直接获取) MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get(); //获取当前线程请求头信息(用于解决Feign异步调用丢失上下文的问题) RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); /**开启第一个异步任务来远程查询所有的收获地址列表**/ CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> { //每一个线程都来共享之前的请求数据(用于解决Feign异步调用丢失上下文的问题) RequestContextHolder.setRequestAttributes(requestAttributes); //1、远程查询所有的收获地址列表 List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId()); confirmVo.setMemberAddressVos(address); }, threadPoolExecutor); /**开启第二个异步任务来远程查询购物车所有选中的购物项**/ CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> { //每一个线程都来共享之前的请求数据(用于解决Feign异步调用丢失上下文的问题) RequestContextHolder.setRequestAttributes(requestAttributes); //2、远程查询购物车所有选中的购物项 List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems(); confirmVo.setItems(currentCartItems); //feign在远程调用之前要构造请求,调用很多的拦截器 }, threadPoolExecutor).thenRunAsync(() -> { /** 开启第三个异步任务来远程批量查询所有商品的库存是否有货**/ List<OrderItemVo> items = confirmVo.getItems(); //获取全部商品的id List<Long> skuIds = items.stream().map((itemVo -> itemVo.getSkuId())).collect(Collectors.toList()); //远程查询商品库存信息 R skuHasStock = wmsFeignService.getSkuHasStock(skuIds); //SkuStockVo就是下面的SkuHasStockVo类 List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {}); if (skuStockVos != null && skuStockVos.size() > 0) { //将skuStockVos集合转换为map Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock)); confirmVo.setStocks(skuHasStockMap); } },threadPoolExecutor); //3、查询用户积分 Integer integration = memberResponseVo.getIntegration(); confirmVo.setIntegration(integration); //4、价格数据由OrderConfirmVo的getTotal方法自动计算 //TODO 5、防重令牌(防止表单重复提交) //为用户设置一个token,三十分钟过期时间(存在redis) String token = UUID.randomUUID().toString().replace("-", ""); //防重令牌一个放到redis里 USER_ORDER_TOKEN_PREFIX = "order:token" redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES); //防重令牌一个放到前台页面(不过隐藏了) confirmVo.setOrderToken(token); //阻塞异步线程,只有两个异步都完成了才可以进行下一步 CompletableFuture.allOf(addressFuture,cartInfoFuture).get(); return confirmVo; }
package com.saodai.common.vo; import lombok.Data; import lombok.ToString; import java.io.Serializable; import java.util.Date; /** *会员信息 **/ @ToString @Data public class MemberResponseVo implements Serializable { private static final long serialVersionUID = 5573669251256409786L; private Long id; /** * 会员等级id */ private Long levelId; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 昵称 */ private String nickname; /** * 手机号码 */ private String mobile; /** * 邮箱 */ private String email; /** * 头像 */ private String header; /** * 性别 */ private Integer gender; /** * 生日 */ private Date birth; /** * 所在城市 */ private String city; /** * 职业 */ private String job; /** * 个性签名 */ private String sign; /** * 用户来源 */ private Integer sourceType; /** * 积分 */ private Integer integration; /** * 成长值 */ private Integer growth; /** * 启用状态 */ private Integer status; /** * 注册时间 */ private Date createTime; /** * 社交登录用户的ID */ private String socialId; /** * 社交登录用户的名称 */ private String socialName; /** * 社交登录用户的自我介绍 */ private String socialBio; }
1>远程调用会员服务来查询所有的收获地址列表
@Autowired private MemberReceiveAddressService memberReceiveAddressService; /** * 根据会员id查询会员的所有地址(用于获取订单时远程的查询地址) * @param memberId * @return */ @GetMapping(value = "/{memberId}/address") public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) { List<MemberReceiveAddressEntity> addressList = memberReceiveAddressService.getAddress(memberId); return addressList; } //根据会员id查询会员的所有地址(用于获取订单时远程的查询地址) @Override public List<MemberReceiveAddressEntity> getAddress(Long memberId) { //MemberReceiveAddressEntity对象就是上面的用户收货信息类MemberAddressVo List<MemberReceiveAddressEntity> addressList = this.baseMapper.selectList (new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId)); return addressList; }
2>远程调用购物车服务来查询购物车所有选中的购物项
package com.saodai.saodaimall.order.feign; import com.saodai.saodaimall.order.vo.OrderItemVo; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import java.util.List; /** * 远程调用购物车服务 **/ @FeignClient("saodaimall-cart") public interface CartFeignService { /** * 查询当前用户购物车选中的商品项 * @return */ @GetMapping(value = "/currentUserCartItems") List<OrderItemVo> getCurrentCartItems(); } @Resource private CartService cartService; /** * 获取当前用户的购物车商品项(订单生成需要查询用户购物车中选择的购物项) * @return */ @GetMapping(value = "/currentUserCartItems") @ResponseBody public List<CartItemVo> getCurrentCartItems() { List<CartItemVo> cartItemVoList = cartService.getUserCartItems(); return cartItemVoList; }
查询购物车所有选中的购物项分流程:
1、获取当前用户登录的信息来判断登录没
2、组装Redis中的HashMap结构的Hash值,用来绑定Hash操作(这里的购物车和购物项的数据都是存到Reids缓存里的,格式为Hash值:saodaimall:cart:10290038,key:39,value:CartItemVo对象的String类型,其中10290038是用户id,39是skuId)
3、调用getCartItems方法来把所有的value值取出来并都封装成CartItemVo对象(注意每一个Hash值对应一个用户的购物车,所以其中的key和value合在一起表示每个购物项)
4、远程调用商品服务来查询每个商品的最新价格(由于redis缓存中的数据可能不是最新的数据)
/** * 获取当前用户的购物车商品项(订单生成需要查询用户购物车中选择的购物项) * @return */ @Override public List<CartItemVo> getUserCartItems() { List<CartItemVo> cartItemVoList = new ArrayList<>(); //获取当前用户登录的信息 UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get(); //如果用户未登录直接返回null // userInfoTo.getUserId()== null是因为拦截器判断用户登录了就设置其userId // 没有登录就设置userKey,所以不能像以前那样直接就用userInfoTo==null来判断 if (userInfoTo.getUserId() == null) { return null; } else { //获取购物车项 String CART_PREFIX = "saodaimall:cart:"是Hash值 String cartKey = CART_PREFIX + userInfoTo.getUserId(); //把Hash值传给getCartItems来获取购物车的所有购物项 List<CartItemVo> cartItems = getCartItems(cartKey); if (cartItems == null) { throw new CartExceptionHandler(); } //由于redis缓存中的数据可能不是最新的数据,所以要远程在查一次价格 cartItemVoList = cartItems.stream() .filter(items -> items.getCheck()) .map(item -> { //更新为最新的价格(远程调用商品服务来查询数据库) BigDecimal price = productFeignService.getPrice(item.getSkuId()); item.setPrice(price); return item; }) .collect(Collectors.toList()); } return cartItemVoList; } /** * 获取购物车里面的数据 * @param cartKey redis中的外围map的key值 * @return */ private List<CartItemVo> getCartItems(String cartKey) { //获取购物车里面的所有商品 BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey); //注意这里从Reids中取出来的是Object类型 List<Object> values = operations.values(); if (values != null && values.size() > 0) { List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> { //这里要转为String类型是因为reids中取出来的是Obhect类型 String str = (String) obj; //在把String转为CartItemVo对象 CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class); return cartItem; }).collect(Collectors.toList()); return cartItemVoStream; } return null; } @Autowired private SkuInfoService skuInfoService; /** * 根据skuId查询当前商品的价格(订单生成需要查询用户购物车中选择的购物项时的最新价格) * @param skuId * @return */ @GetMapping(value = "/{skuId}/price") public BigDecimal getPrice(@PathVariable("skuId") Long skuId) { //获取当前商品的信息(skuInfoService.getById()这个方法是代码构造器自动生成的代码里有的) SkuInfoEntity skuInfo = skuInfoService.getById(skuId); //获取商品的价格 BigDecimal price = skuInfo.getPrice(); return price; }