- 谷粒商城-分布式基础篇【环境准备】
- 谷粒商城-分布式基础【业务编写】
- 谷粒商城-分布式高级篇【业务编写】持续更新
- 谷粒商城-分布式高级篇-ElasticSearch
- 谷粒商城-分布式高级篇-分布式锁与缓存
- 项目托管于gitee
一、页面环境搭建
1、配置动静环境
在服务器的mydata/nginx/html/static
路径下创建一个 order 文件夹,在order路径下分别创建以下几个文件夹,用来存放对应的静态资源
-
detail 文件夹下存放 等待付款的静态资源,
并将等待付款文件夹下的页面复制到 gulimall-order服务中并命名为detail.html
href=" ==> href="/static/order/detail/ src=" ==> src="/static/order/detail/
-
list 文件夹下存放 订单页的静态资源,并将订单页文件夹下的页面复制到 gulimall-order服务中并命名为
list.html
href=" ==> href="/static/order/list/ src=" ==> src="/static/order/list/
-
confirm 文件夹下存放 结算页的静态资源,并将结算页文件夹下的页面复制到 gulimall-order服务中并命名为
confirm.html
src=" ==> src="/static/order/confirm/ href=" ==> href="/static/order/confirm/
-
pay 文件夹下存放 收银页的静态资源,并将收银页文件夹下的页面复制到 gulimall-order服务中并命名为
pay.html
href=" ==> href="/static/order/pay/ src=" ==> src="/static/order/pay/
2、网关路由配置
- 修改文件,添加新的域名
vim /etc/hosts
# Gulimall Host Start
127.0.0.1 gulimall.cn
127.0.0.1 search.gulimall.cn
127.0.0.1 item.gulimall.cn
127.0.0.1 auth.gulimall.cn
127.0.0.1 cart.gulimall.cn
127.0.0.1 order.gulimall.cn
# Gulimall Host End
- 配置网关路由 gulimall-gateway
- id: gulimall_order_route
uri: lb://gulimall-order
predicates:
- Host=order.gulimall.cn
3、配置加入注册中心Nacos
-
已导入依赖
-
主启动类加上
@EnableDiscoveryClient
注解@EnableDiscoveryClient @EnableRabbit @SpringBootApplication public class GulimallOrderApplication { public static void main(String[] args) { SpringApplication.run(GulimallOrderApplication.class, args); } }
-
配置注册中心信息
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-order
4、页面渲染
- 导入 thymeleaf的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 配置清除缓存
spring:
thymeleaf:
cache: false
二、整合SpringSession
**第一步、**导入依赖
<!--属性配置的提示工具-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 整合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>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
第二步、修改配置
spring:
#SpringSession的存储类型
session:
store-type: redis
#reidis地址
redis:
host: 124.222.223.222
# 配置线程池
gulimall:
thread:
core-size: 20
max-size: 200
keep-alive-time: 10
主启动类是上添加SpingSession自动启动的注解
@EnableRedisHttpSession
@EnableDiscoveryClient
@EnableRabbit
@SpringBootApplication
public class GulimallOrderApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallOrderApplication.class, args);
}
}
第三步、导入SpringSession、线程池配置类
-
添加SpringSession的配置,添加“com.atguigu.gulimall.order.config.GulimallSessionConfig”类,代码如下
@Configuration public class GulimallSessionConfig { @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); cookieSerializer.setDomainName("gulimall.cn"); cookieSerializer.setCookieName("GULISESSION"); return cookieSerializer; } @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } }
-
添加线程池的配置,添加“com.atguigu.gulimall.order.config.MyThreadConfig”类,代码如下
@Configuration public class MyThreadConfig { @Bean public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) { return new ThreadPoolExecutor(pool.getCoreSize(), pool.getMaxSize(), pool.getKeepAliveTime(), TimeUnit.SECONDS, new LinkedBlockingDeque<>(100000), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); } }
-
线程池配置需要的属性
添加“com.atguigu.gulimall.order.config.ThreadPoolConfigProperties”类,代码如下
@ConfigurationProperties(prefix = "gulimall.thread") @Component @Data public class ThreadPoolConfigProperties { private Integer coreSize; private Integer maxSize; private Integer keepAliveTime; }
第四步、页面调整
- 修改商城首页、商品页我的订单地链接地址
- 获取用户信息
三、订单基本概念
1、订单中心
1.1、订单构成
1.2、订单状态
- 代付款
- 已付款/待发货
- 待收货/已发货
- 已完成
- 已取消
- 售后中
2、订单流程
订单生成 -> 支付订单 -> 卖家发货 -> 确认收货 -> 交易成功
四、订单登录拦截
需求:去结算、查看订单必须是登录用户之后的,这里编写一个拦截器。
- 用户登录 放行
- 用户未登录:跳转到登录页面
1)、修改cartList.html 页面的**“去结算”**的链接地址
2)、编写Controller 层
Gulimall-order服务中com.atguigu.gulimall.order.web
路径下
package com.atguigu.gulimall.order.web;
@Controller
public class OrderWebController {
@GetMapping("/toTrade")
public String toTrade(){
return "confirm";
}
}
3)、编写拦截器
Gulimall-order服务中com.atguigu.gulimall.order.interceptoe
路径下
package com.atguigu.gulimall.order.interceptoe;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
/**
* 用户登录拦截器
* @param request
* @param response
* @param handler
* @return
* 用户登录:放行
* 用户未登录:跳转到登录页面
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute!=null){
loginUser.set(attribute);
return true;
} else {
// 没登录就去登录
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.gulimall.cn/login.html");
return false;
}
}
}
4)、添加拦截器的配置
Gulimall-order服务中com.atguigu.gulimall.order.config
路径下
package com.atguigu.gulimall.order.config;
import com.atguigu.gulimall.order.interceptoe.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;
/**
* Data time:2022/4/11 22:21
* StudentID:2019112118
* Author:hgw
* Description:
*/
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}
5)、修改gulimall-auth-server的login.html页面接收提醒信息
五、订单确认页
1、订单确认页的模型抽取
订单确认页需要用的数据
- 因为存在网路延迟等问题,若一直点下单会下许多。所以我们需要防重令牌
gulimall-order 服务中 com.atguigu.gulimall.order.vo
路径下 VO类:
/**
* Data time:2022/4/12 09:31
* StudentID:2019112118
* Author:hgw
* Description: 订单确认页需要用的数据
*/
public class OrderConfirmVo {
/**
* 收货地址,ums_member_receive_address 表
*/
@Setter@Getter
List<MemberAddressVo> addressVos;
/**
* 所有选中的购物车项
*/
@Setter@Getter
List<OrderItemVo> items;
// 发票记录。。。
/**
* 优惠券信息
*/
@Setter@Getter
Integer integration;
/**
* 是否有库存
*/
@Setter@Getter
Map<Long,Boolean> stocks;
/**
* 防重令牌
*/
@Setter@Getter
String OrderToken;
/**
* @return 订单总额
* 所有选中商品项的价格 * 其数量
*/
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 pryPrice;
public BigDecimal getPryPrice() {
return getTotal();
}
public Integer getCount(){
Integer i =0;
if (items!=null){
for (OrderItemVo item : items) {
i+=item.getCount();
}
}
return i;
}
}
收货地址,ums_member_receive_address 表
package com.atguigu.gulimall.order.vo;
@Data
public class OrderConfirmVo {
/**
* 收货地址,ums_member_receive_address 表
*/
List<MemberAddressVo> addressVos;
/**
* 所有选中的购物车项
*/
List<OrderItemVo> items;
// 发票记录。。。
/**
* 优惠券信息
*/
Integer integration;
/**
* 订单总额
*/
BigDecimal total;
/**
* 应付价格
*/
BigDecimal pryPrice;
}
商品项信息
package com.atguigu.gulimall.order.vo;
@Data
public class OrderItemVo {
/**
* 商品Id
*/
private Long skuId;
/**
* 商品标题
*/
private String title;
/**
* 商品图片
*/
private String image;
/**
* 商品套餐信
*/
private List<String> skuAttr;
/**
* 商品价格
*/
private BigDecimal price;
/**
* 数量
*/
private Integer count;
/**
* 小计价格
*/
private BigDecimal totalPrice;
}
2、订单确认页数据获取
2.1、gulimall-order 订单确认页数据获取 接口编写
- Controller 层方法编写
Gulimall-product 服务中com.atguigu.gulimall.order.web
路径下 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";
}
}
-
Service层实现类方法编写
- 1、远程查询所有的地址列表
- 2、远程查询购物车所有选中的购物项
- 3、查询用户积分
- 4、其他数据自动计算
- 5、防重令牌
Gulimall-product 服务中
com.atguigu.gulimall.order.service.impl
路径下 OrderServiceImpl
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
@Autowired
MemberFeignService memberFeignService;
@Autowired
CartFeignService cartFeignService;
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 1、远程查询所有的地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddressVos(address);
// 2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
// 3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 4、其他数据自动计算
// 5、防重令牌
return confirmVo;
}
}
2.2、编写Gulimall-common获取会员所有收货地址接口
-
编写Controller层接口方法
Gulimall-member 服务中com.atguigu.gulimall.member.controller
路径下 MemberReceiveAddressController 类package com.atguigu.gulimall.member.controller; @RestController @RequestMapping("member/memberreceiveaddress") public class MemberReceiveAddressController { @Autowired private MemberReceiveAddressService memberReceiveAddressService; @GetMapping("/{memberId}/address") public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) { return memberReceiveAddressService.getAddress(memberId); }
-
Service层实现类 编写 获取会有收货地址列表 方法
Gulimall-member 服务中com.atguigu.gulimall.member.service.impl
路径下 MemberReceiveAddressServiceImpl 实现类@Override public List<MemberReceiveAddressEntity> getAddress(Long memberId) { return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId)); }
-
Gulimall-order服务中编写远程调用 gulimall-member服务 feign接口
Gulimall-order服务中com.atguigu.gulimall.order.feign
路径下 MemberFeignService 接口package com.atguigu.gulimall.order.feign; @FeignClient("gulimall-member") public interface MemberFeignService { /** * 返回会员所有的收货地址列表 * @param memberId 会员ID * @return */ @GetMapping("/member/memberreceiveaddress/{memberId}/address") List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId); }
2.3、编写GuliMall-cart 购物车服务中用户选择的所有购物项
- 首先通过用户ID在Redis中查询到购物车中的所有的购物项
- 通过 filter 过滤 用户购物车中被选择的购物项
- 查询数据库中当前购物项的价格,不能使用之前加入购物车的价格
- 编写远程 gulimall-product 服务中的 查询sku价格接口
第一步、编写Controller层接口
编写 gulimall-cart 服务中 package com.atguigu.cart.controller;
路径下的 CartController 类:
package com.atguigu.cart.controller;
@Controller
public class CartController {
@Autowired
CartService cartService;
@GetMapping("/currentUserCartItems")
@ResponseBody
public List<CartItem> getCurrentUserCartItems(){
return cartService.getUserCartItems();
}
//....
}
第二步、Service层实现类 获取用户选择的所有购物项方法编写
编写 gulimall-cart 服务中 com.atguigu.cart.service.impl
路径中 CartServiceImpl 类
@Autowired
ProductFeignService productFeignService;
/**
* 获取用户选择的所有购物项
* @return
*/
@Override
public List<CartItem> getUserCartItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if (userInfoTo.getUserId() == null) {
return null;
} else {
String cartKey = CART_PREFIX + userInfoTo.getUserId();
// 获取所有用户选择的购物项
List<CartItem> collect = getCartItems(cartKey).stream()
.filter(item -> item.getCheck())
.map(item->{
// TODO 1、更新为最新价格
R price = productFeignService.getPrice(item.getSkuId());
String data = (String) price.get("data");
item.setPrice(new BigDecimal(data));
return item;
})
.collect(Collectors.toList());
return collect;
}
}
第三步、编写Gulimall-product 服务中获取指定商品的价格接口
Gulimall-product 服务中 com.atguigu.gulimall.product.app
路径下的 SkuInfoController
package com.atguigu.gulimall.product.app;
@RestController
@RequestMapping("product/skuinfo")
public class SkuInfoController {
@Autowired
private SkuInfoService skuInfoService;
/**
* 获取指定商品的价格
* @param skuId
* @return
*/
@GetMapping("/{skuId}/price")
public R getPrice(@PathVariable("skuId") Long skuId){
SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
return R.ok().setData(skuInfoEntity.getPrice().toString());
}
Gulimall-cart 服务中的 com.atguigu.cart.feign
路径下的远程调用接口 ProductFeignService
package com.atguigu.cart.feign;
@FeignClient("gulimall-product")
public interface ProductFeignService {
//.....
@GetMapping("/product/skuinfo/{skuId}/price")
R getPrice(@PathVariable("skuId") Long skuId);
}
第四步、Gulimall-order服务中编写远程调用 gulimall-cart服务 feign接口
Gulimall-order服务中com.atguigu.gulimall.order.feign
路径下的 CartFeignService接口
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-cart")
public interface CartFeignService {
@GetMapping("/currentUserCartItems")
List<OrderItemVo> getCurrentUserCartItems();
}
3、Feign远程调用丢失请求头问题
- 问题 :Feign远程调用的时候会丢失请求头
- 解决:加上feign远程调用的请求拦截器。(RequestInterceptor)
- 因为feign在远程调用之前会执行所有的RequestInterceptor拦截器
在 gulimall-order 服务中 com.atguigu.gulimall.order.config
路径下编写Feign配置类:GulimallFeignConfig类 并编写请求拦截器
package com.atguigu.gulimall.order.config;
@Configuration
public class GulimallFeignConfig {
/**
* feign在远程调用之前会执行所有的RequestInterceptor拦截器
* @return
*/
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
// 1、使用 RequestContextHolder 拿到请求数据,RequestContextHolder底层使用过线程共享数据 ThreadLocal<RequestAttributes>
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes!=null){
HttpServletRequest request = attributes.getRequest();
// 2、同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
// 给新请求同步了老请求的cookie
requestTemplate.header("Cookie",cookie);
}
}
};
}
}
4、Feign异步调用丢失请求头问题
此时:查询购物项、库存和收货地址都要调用远程服务,串行会浪费大量时间,因此我们进行异步编排优化
- 问题:
由于 RequestContextHolder底层使用的是线程共享数据ThreadLocal<RequestAttributes>
,我们知道线程共享数据的域是 当前线程下,线程之间是不共享的。所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie
了。 - 解决:
向 RequestContextHolder 线程域中放主线程的域。
修改 gulimall-order 服务中 com.atguigu.gulimall.order.service.impl
目录下的 OrderServiceImpl 类
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 获取主线程的域
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 1、远程查询所有的地址列表
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 将主线程的域放在该线程的域中
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddressVos(address);
}, executor);
// 2、远程查询购物车所有选中的购物项
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
// 将老请求的域放在该线程的域中
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);
// feign在远程调用请求之前要构造
// 3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 4、其他数据自动计算
// TODO 5、防重令牌
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
5、订单确认页渲染
修改 gulimall-order 服务中,src/main/resources/templates/
路径下的 confirm.html
<!--主体部分-->
<p class="p1">填写并核对订单信息</p>
<div class="section">
<!--收货人信息-->
<div class="top-2">
<span>收货人信息</span>
<span>新增收货地址</span>
</div>
<!--地址-->
<div class="top-3" th:each="addr:${orderConfirmData.addressVos}">
<p>[[${addr.name}]]</p><span>[[${addr.name}]] [[${addr.province}]] [[${addr.city}]] [[${addr.detailAddress}]] [[${addr.phone}]]</span>
</div>
<p class="p2">更多地址︾</p>
<div class="hh1"/></div>
<div class="xia">
<div class="qian">
<p class="qian_y">
<span>[[${orderConfirmData.count}]]</span>
<span>件商品,总商品金额:</span>
<span class="rmb">¥[[${#numbers.formatDecimal(orderConfirmData.total,1,2)}]]</span>
</p>
<p class="qian_y">
<span>返现:</span>
<span class="rmb"> -¥0.00</span>
</p>
<p class="qian_y">
<span>运费: </span>
<span class="rmb">   ¥0.00</span>
</p>
<p class="qian_y">
<span>服务费: </span>
<span class="rmb">   ¥0.00</span>
</p>
<p class="qian_y">
<span>退换无忧: </span>
<span class="rmb">   ¥0.00</span>
</p>
</div>
<div class="yfze">
<p class="yfze_a"><span class="z">应付总额:</span><span class="hq">¥[[${#numbers.formatDecimal(orderConfirmData.pryPrice,1,2)}]]</span></p>
<p class="yfze_b">寄送至: IT-中心研发二部 收货人:</p>
</div>
<button class="tijiao">提交订单</button>
</div>
6、订单确认页库存查询
需求:有货、无货
在远程查询购物车所有选中的购物项之后进行 批量查询库存
1)、在订单确认页数据获取 Service层实现类 OrderServiceImpl 方法中进行批量查询库存
1、修改Gulimall-order 服务中 com.atguigu.gulimall.order.service.impl
路径下的 OrderServiceImpl 类
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 获取主线程的请求域
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 1、远程查询所有的地址列表
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 将主线程的请求域放在该线程请求域中
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddressVos(address);
}, executor);
// 2、远程查询购物车所有选中的购物项
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
// 将主线程的请求域放在该线程请求域中
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor).thenRunAsync(()->{
// 批量查询商品项库存
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R hasStock = wareFeignService.getSkusHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data != null) {
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
}, executor);
// feign在远程调用请求之前要构造
// 3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 4、其他数据自动计算
// TODO 5、防重令牌
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
2)、在gulimall-order 服务中创建商品是否有库存的VO类
在 Gulimall-order 服务中 package com.atguigu.gulimall.order.vo
路径下创建 SkuStockVo 类
package com.atguigu.gulimall.order.vo;
@Data
public class SkuStockVo {
private Long skuId;
private Boolean hasStock;
}
3)、gulimall-ware 库存服务中提供 查询库存的接口
- gulimall-ware 服务中
com.atguigu.gulimall.ware.controller
路径下的 WareSkuController 类,之前编写过。
package com.atguigu.gulimall.ware.controller;
@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
@Autowired
private WareSkuService wareSkuService;
// 查询sku是否有库存
@PostMapping("/hasstock")
public R getSkusHasStock(@RequestBody List<Long> skuIds){
// sku_id,stock
List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);
return R.ok().setData(vos);
}
//....
}
- gulimall-order 服务中编写远程调用 gulimall-ware 库存服务中 查询库存 feign接口
gulimall-order 服务下com.atguigu.gulimall.order.feign
路径下:WareFeignService
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-ware")
public interface WareFeignService {
@PostMapping("/ware/waresku/hasstock")
R getSkusHasStock(@RequestBody List<Long> skuIds);
}
4)、页面效果
[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]
<div class="mi">
<p>[[${item.title}]]<span style="color: red;"> ¥ [[${#numbers.formatDecimal(item.price,1,2)}]]</span> <span> x[[${item.count}]]</span> <span>[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]</span></p>
<p><span>0.095kg</span></p>
<p class="tui-1"><img src="/static/order/confirm/img/i_07.png" />支持7天无理由退货</p>
</div>
7、模拟运费效果
需求:选择收货地址,计算物流费
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1paZHi3c-1650101180086)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220412183318234.png)]
7.1、选择收货地址页面效果
function highlight(){
$(".addr-item p").css({"border": "2px solid gray"});
$(".addr-item p[def='1']").css({"border": "2px solid red"});
}
$(".addr-item p").click(function () {
$(".addr-item p").attr("def","0");
$(this).attr("def","1");
highlight();
// 获取当前地址id
var addrId = $(this).attr("addrId");
// 发送ajax获取运费信息
getFare(addrId);
});
function getFare(addrId) {
$.get("http://gulimall.cn/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
console.log(resp);
$("#fareEle").text(resp.data.fare);
var total = [[${orderConfirmData.total}]]
// 设置运费信息
$("#payPriceEle").text(total*1 + resp.data.fare*1);
// 设置收货人信息
$("#reciveAddressEle").text(resp.data.address.province+" " + resp.data.address.region+ "" + resp.data.address.detailAddress);
$("#reveiverEle").text(resp.data.address.name);
})
}
7.2、后端接口提供根据用户地址ID,返回详细地址并计算物流费h
2、gulimall-ware仓储服务编写 根据用户地址,返回详细地址并计算物流费h
- gulimall-ware 服务中
com.atguigu.gulimall.ware.controller
路径下 WareInfoController 类
package com.atguigu.gulimall.ware.controller;
@RestController
@RequestMapping("ware/wareinfo")
public class WareInfoController {
@Autowired
private WareInfoService wareInfoService;
@GetMapping("/fare")
public R getFare(@RequestParam("addrId") Long addrId){
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
//...
}
- gulimall-ware 服务中
com.atguigu.gulimall.ware.service.impl
路径下 WareInfoServiceImpl 类
@Override
public FareVo getFare(Long addrId) {
FareVo fareVo = new FareVo();
R r = memberFeignService.addrInfo(addrId);
MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
});
if (data!=null) {
// 简单处理:截取手机号最后一位作为邮费
String phone = data.getPhone();
String substring = phone.substring(phone.length() - 1, phone.length());
BigDecimal bigDecimal = new BigDecimal(substring);
fareVo.setAddressVo(data);
fareVo.setFare(bigDecimal);
return fareVo;
}
return null;
}
- gulimall-ware 服务中
com.atguigu.gulimall.ware.feign
路径下 MemberFeignService远程查询地址详细信息feign接口
package com.atguigu.gulimall.ware.feign;
@FeignClient("gulimall-member")
public interface MemberFeignService {
/**
* 根据地址id查询地址的详细信息
* @param id
* @return
*/
@RequestMapping("/member/memberreceiveaddress/info/{id}")
R addrInfo(@PathVariable("id") Long id);
}
- gulimall-ware 服务中
com.atguigu.gulimall.ware.vo
路径下的 Vo
@Data
public class FareVo {
private MemberAddressVo addressVo;
private BigDecimal fare;
}
六、接口幂等性讨论
1、幂等性概述
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的。
- 接口幂等性:
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用,比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也交成了两条这就没有保证接口的幂等性。 - 哪些情况需要防止:
- 用户多次点击按钮
- 用户页面回退再次提交
- 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
其他业务情況
- 幂等性解决方案
- 1、token机制(令牌机制)本项目采用令牌机制
- 2、各种锁机制
- 3、各种唯一性约束
- 4、防重表
- 5、全球请求唯一id
2、添加防重令牌
gulimall-order服务 com.atguigu.gulimall.order.service.impl
路径下的 OrderServiceImpl
package com.atguigu.gulimall.order.service.impl;
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
@Autowired
MemberFeignService memberFeignService;
@Autowired
CartFeignService cartFeignService;
@Autowired
ThreadPoolExecutor executor;
@Autowired
WareFeignService wareFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<OrderEntity> page = this.page(
new Query<OrderEntity>().getPage(params),
new QueryWrapper<OrderEntity>()
);
return new PageUtils(page);
}
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 获取主线程的请求域
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 1、远程查询所有的地址列表
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 将主线程的请求域放在该线程请求域中
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddressVos(address);
}, executor);
// 2、远程查询购物车所有选中的购物项
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
// 将主线程的请求域放在该线程请求域中
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor).thenRunAsync(()->{
// 批量查询商品项库存
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R hasStock = wareFeignService.getSkusHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data != null) {
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
}, executor);
// feign在远程调用请求之前要构造
// 3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.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);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
}
七、订单提交
Controller 层编写下单功能接口
gulimall-order 服务 com.atguigu.gulimall.order.web
路径下的 OrderWebController 类,代码如下
/**
* 下单功能
* @param vo
* @return
*/
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes){
// 1、创建订单、验令牌、验价格、验库存
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+="商品库存不足";break;
}
redirectAttributes.addAttribute("msg",msg);
return "redirect:http://order.gulimall.cn/toTrade";
}
} catch (Exception e){
if (e instanceof NoStockException) {
String message = e.getMessage();
redirectAttributes.addFlashAttribute("msg", message);
}
return "redirect:http://order.gulimall.cn/toTrade";
}
}
7.1、封装订单提交的VO
- 页面提交数据 添加“com.atguigu.gulimall.order.vo.OrderSubmitVo”类,代码如下:
@Data
@ToString
public class OrderSubmitVo {
/**
* 收货地址Id
*/
private Long addrId;
/**
* 支付方式
*/
private Integer payType;
// 无需提交需要购买的商品,去购物车再获取一遍
// 优惠发票
/**
* 防重令牌
*/
private String orderToken;
/**
* 应付价格,验价
*/
private BigDecimal payPrice;
/**
* 订单备注
*/
private String note;
/**
* 用户相关信息,直接去Session取出登录的用户
*/
}
- 前端页面 confirm.html 提供数据
<form action="http://order.gulimall.cn/submitOrder" method="post">
<input id="addrIdInput" type="hidden" name="addrId">
<input id="payPriceInput" type="hidden" name="payPrice">
<input type="hidden" name="orderToken" th:value="${orderConfirmData.orderToken}">
<button class="tijiao" type="submit">提交订单</button>
</form>
function getFare(addrId) {
// 给表单回填的地址
$("#addrIdInput").val(addrId);
$.get("http://gulimall.cn/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
console.log(resp);
$("#fareEle").text(resp.data.fare);
var total = [[${orderConfirmData.total}]]
// 设置运费信息
var pryPrice = total*1 + resp.data.fare*1;
$("#payPriceEle").text(pryPrice);
$("#payPriceInput").val(pryPrice);
// 设置收货人信息
$("#reciveAddressEle").text(resp.data.address.province+" " + resp.data.address.region+ "" + resp.data.address.detailAddress);
$("#reveiverEle").text(resp.data.address.name);
})
}
7.2、原子验令牌
- 问题:存在网路延时,同时提交从Redis拿到的令牌一直,导致重复提交
- 解决:令牌的对比和删除必须保证原子性
1)、封装提交订单数据
package com.atguigu.gulimall.order.vo;
@Data
public class SubmitOrderResponseVo {
private OrderEntity order;
private Integer code; //0成功,错误状态码
}
2)、修改 SubmitOrderResponseVo 类编写验证令牌操作
/**
* 下单操作:验令牌、创建订单、验价格、验库存
* @param vo
* @return
*/
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
// 从拦截器中拿到当前的用户
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
// 原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(),
OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), orderToken);
if (result == 0L) {
// 令牌验证失败
response.setCode(1);
return response;
} else {
// 令牌验证成功
return response;
}
}
7.3、创建订单、订单项等信息
gulimall-order服务中 com.atguigu.gulimall.order.service.impl
路径下的 OrderServiceImpl 类
/**
* 创建订单、订单项等信息
* @return
*/
private OrderCreateTo createOrder(){
OrderCreateTo createTo = new OrderCreateTo();
// 1、生成一个订单号
String orderSn = IdWorker.getTimeId();
// 2、构建一个订单
OrderEntity orderEntity = buildOrder(orderSn);
// 3、获取到所有的订单项
List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
// 4、计算价格、积分等相关信息
computePrice(orderEntity,itemEntities);
createTo.setOrder(orderEntity);
createTo.setOrderItems(itemEntities);
return createTo;
}
7.3.1、创建订单
gulimall-order服务中 com.atguigu.gulimall.order.service.impl
路径下的 OrderServiceImpl 类
/**
* 构建订单
* @param orderSn
* @return
*/
private OrderEntity buildOrder(String orderSn) {
MemberRespVo respVp = LoginUserInterceptor.loginUser.get();
OrderEntity entity = new OrderEntity();
entity.setOrderSn(orderSn);
entity.setMemberId(respVp.getId());
OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
// 1、获取运费 和 收货信息
R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
});
// 2、设置运费
entity.setFreightAmount(fareResp.getFare());
// 3、设置收货人信息
entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
entity.setReceiverProvince(fareResp.getAddress().getProvince());
entity.setReceiverRegion(fareResp.getAddress().getRegion());
entity.setReceiverCity(fareResp.getAddress().getCity());
entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
entity.setReceiverName(fareResp.getAddress().getName());
entity.setReceiverPhone(fareResp.getAddress().getPhone());
// 4、设置订单的相关状态信息
entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
// 5、默认取消信息
entity.setAutoConfirmDay(7);
return entity;
}
1)、创建远程调用 gulimall-ware 服务 计算运费和详细地址方法的接口
gulimall-order服务中 com.atguigu.gulimall.order.feign
路径下的 WareFeignService 类
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-ware")
public interface WareFeignService {
@PostMapping("/ware/waresku/hasstock")
R getSkusHasStock(@RequestBody List<Long> skuIds);
/**
* 计算运费和详细地址
* @param addrId
* @return
*/
@GetMapping("/ware/wareinfo/fare")
R getFare(@RequestParam("addrId") Long addrId);
}
2)、创建 计算运费和详细地址方法 信息封装VO
gulimall-order服务中 com.atguigu.gulimall.order.vo
路径下的 FareVo 类
package com.atguigu.gulimall.order.vo;
@Data
public class FareVo {
private MemberAddressVo address;
private BigDecimal fare;
}
7.3.2、构造订单项数据
1)、构建订单项数据
gulimall-order服务中 com.atguigu.gulimall.order.service.impl
路径下的 OrderServiceImpl 类
/**
* 构建所有订单项数据
* @return
*/
private List<OrderItemEntity> buildOrderItems(String orderSn) {
// 最后确定每个购物项的价格
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
if (currentUserCartItems != null && currentUserCartItems.size()>0){
List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
OrderItemEntity itemEntity = buildOrderItem(cartItem);
itemEntity.setOrderSn(orderSn);
return itemEntity;
}).collect(Collectors.toList());
return itemEntities;
}
return null;
}
/**
* 构建某一个订单项
* @param cartItem
* @return
*/
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
OrderItemEntity itemEntity = new OrderItemEntity();
// 1、订单信息:订单号 v
// 2、商品的spu信息
Long skuId = cartItem.getSkuId();
R r = productFeignService.getSpuInfoBySkuId(skuId);
SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
});
itemEntity.setSpuId(data.getId());
itemEntity.setSpuBrand(data.getBrandId().toString());
itemEntity.setSpuName(data.getSpuName());
itemEntity.setCategoryId(data.getCatalogId());
// 3、商品的sku信息 v
itemEntity.setSkuId(cartItem.getSkuId());
itemEntity.setSkuName(cartItem.getTitle());
itemEntity.setSkuPic(cartItem.getImage());
itemEntity.setSkuPrice(cartItem.getPrice());
itemEntity.setSkuQuantity(cartItem.getCount());
itemEntity.setSkuAttrsVals(StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(),";"));
// 4、优惠信息【不做】
// 5、积分信息
itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
// 6、订单项的价格信息
itemEntity.setPromotionAmount(new BigDecimal("0"));
itemEntity.setCouponAmount(new BigDecimal("0"));
itemEntity.setIntegrationAmount(new BigDecimal("0"));
// 当前订单项的实际金额 总额-各种优惠
BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount()).
subtract(itemEntity.getCouponAmount()).
subtract(itemEntity.getIntegrationAmount());
itemEntity.setRealAmount(subtract);
return itemEntity;
}
2)、gulimall-product服务中编写通过skuId查询spu信息接口
- gulimall-product服务
com.atguigu.gulimall.product.app
路径下 SpuInfoController 类,代码如下:
package com.atguigu.gulimall.product.app;
@RestController
@RequestMapping("product/spuinfo")
public class SpuInfoController {
@Autowired
private SpuInfoService spuInfoService;
/**
* 查询指定sku的spu信息
* @param skuId
* @return
*/
@GetMapping("/skuId/{id}")
public R getSpuInfoBySkuId(@PathVariable("id") Long skuId) {
SpuInfoEntity entity = spuInfoService.getSpuInfoBySkuId(skuId);
return R.ok().setData(entity);
}
- gulimall-product服务
com.atguigu.gulimall.product.service.impl
路径下 SpuInfoServiceImpl 类,代码如下:
package com.atguigu.gulimall.product.service.impl;
@Override
public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
SkuInfoEntity byId = skuInfoService.getById(skuId);
Long spuId = byId.getSpuId();
SpuInfoEntity spuInfoEntity = getById(spuId);
return spuInfoEntity;
}
- gulimall-order服务
com.atguigu.gulimall.order.feign
路径下 ProductFeignService 类,代码如下:
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-product")
public interface ProductFeignService {
@GetMapping("/product/spuinfo/skuId/{id}")
R getSpuInfoBySkuId(@PathVariable("id") Long skuId);
}
- gulimall-order服务
com.atguigu.gulimall.order.vo
路径下 SpuInfoVo 类,用来接收查询过来的Spu信息;代码如下:
package com.atguigu.gulimall.order.vo;
@Data
public class SpuInfoVo {
/**
* 商品id
*/
@TableId
private Long id;
/**
* 商品名称
*/
private String spuName;
/**
* 商品描述
*/
private String spuDescription;
/**
* 所属分类id
*/
private Long catalogId;
/**
* 品牌id
*/
private Long brandId;
/**
*
*/
private BigDecimal weight;
/**
* 上架状态[0 - 新建,1 - 上架,2-下架]
*/
private Integer publishStatus;
/**
*
*/
private Date createTime;
/**
*
*/
private Date updateTime;
}
7.3.3、计算价格
/**
* 计算价格
* @param orderEntity
* @param itemEntities
*/
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
BigDecimal total = new BigDecimal("0.0");
BigDecimal coupon = new BigDecimal("0.0");
BigDecimal integration = new BigDecimal("0.0");
BigDecimal promotion = new BigDecimal("0.0");
BigDecimal gift = new BigDecimal("0.0");
BigDecimal growth = new BigDecimal("0.0");
// 1、订单的总额,叠加每一个订单项的总额信息
for (OrderItemEntity entity : itemEntities) {
total = total.add(entity.getRealAmount());
coupon = coupon.add(entity.getCouponAmount());
integration = integration.add(entity.getIntegrationAmount());
promotion = promotion.add(entity.getPromotionAmount());
gift = gift.add(new BigDecimal(entity.getGiftIntegration()));
growth = growth.add(new BigDecimal(entity.getGiftGrowth()));
}
// 订单总额
orderEntity.setTotalAmount(total);
// 应付总额
orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
orderEntity.setCouponAmount(coupon);
orderEntity.setIntegrationAmount(integration);
orderEntity.setPromotionAmount(promotion);
// 设置积分等信息
orderEntity.setIntegration(gift.intValue());
orderEntity.setGrowth(growth.intValue());
orderEntity.setDeleteStatus(0);//0 未删除
}
7.4、保存订单数据、锁定库存
7.4.1、保存订单数据并锁定库存
1)、编写 保存订单数据并锁定库存 逻辑实现代码
/**
* 下单操作:验令牌、创建订单、验价格、验库存
* @param vo
* @return
*/
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
// 在当前线程共享 OrderSubmitVo
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
// 从拦截器中拿到当前的用户
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
response.setCode(0);
// 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
// 原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
// 令牌验证失败
response.setCode(1);
return response;
} else {
// 令牌验证成功
// 2、创建订单、订单项等信息
OrderCreateTo order = createOrder();
// 3、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
// 金额对比成功
// 4、保存订单;
saveOrder(order);
// 5、库存锁定,只要有异常回滚订单数据
// 订单号,所有订单项(skuId,skuName,num)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);
// TODO 远程锁库存
R r = wareFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
// 锁成功了
response.setOrder(order.getOrder());
return response;
}else {
// 锁定失败
throw new NoStockException((String) r.get("msg"));
}
} else {
// 金额对比失败
response.setCode(2);
return response;
}
}
}
2)、编写超时异常类
gulimall-common服务中com.atguigu.common.exception
路径下的 NoStockException 接口:
package com.atguigu.common.exception;
public class NoStockException extends RuntimeException{
private Long skuId;
public NoStockException(Long skuId){
super("商品id:"+skuId+";没有足够的库存了!");
}
public NoStockException(String message) {
super(message);
}
@Override
public String getMessage() {
return super.getMessage();
}
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
}
7.4.2、锁定库存
1)、gulimall-order服务中编写远程调用 gulimall-ware (仓储服务) 锁定库存方法 的接口
gulimall-order服务中com.atguigu.gulimall.order.feign
路径下的 WareFeignService 接口:
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-ware")
public interface WareFeignService {
//....
/**
* 锁定指定订单的库存
* @param vo
* @return
*/
@PostMapping("/ware/waresku/lock/order")
R orderLockStock(@RequestBody WareSkuLockVo vo);
}
2)、gulimall-ware (仓储服务)中编写 锁定库存 的接口
- gulimall-ware服务中
com.atguigu.gulimall.ware.controller
路径下的 WareSkuController 类:
package com.atguigu.gulimall.ware.controller;
@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
@Autowired
private WareSkuService wareSkuService;
/**
* 锁定订单项库存
* @param vo
* @return
*/
@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());
}
}
//....
}
- gulimall-ware服务中
com.atguigu.gulimall.ware.service.impl
路径下的 WareSkuServiceImpl 类:
package com.atguigu.gulimall.ware.service.impl;
@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {
@Autowired
WareSkuDao wareSkuDao;
@Autowired
ProductFeignService productFeignService;
//......
/**
* 锁定指定订单的库存
* @param vo
* @return
* (rollbackFor = NoStockException.class)
* 默认只要是运行时异常都会回滚
*/
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
// 1、每个商品在哪个库存里有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHashStock> collect = locks.stream().map(item -> {
SkuWareHashStock stock = new SkuWareHashStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
// 查询这个商品在哪里有库存
List<Long> wareIds = wareSkuDao.listWareIdHashSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList());
// 2、锁定库存
for (SkuWareHashStock hashStock : collect) {
Boolean skuStocked = false;
Long skuId = hashStock.getSkuId();
List<Long> wareIds = hashStock.getWareId();
if (wareIds == null || wareIds.size()==0){
// 没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
for (Long wareId : wareIds) {
// 成功就返回1,否则就返回0
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hashStock.getNum());
if (count == 1){
skuStocked = true;
break;
} else {
// 当前仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false){
// 当前商品所有仓库都没有锁住,其他商品也不需要锁了,直接返回没有库存了
throw new NoStockException(skuId);
}
}
// 3、运行到这,全部都是锁定成功的
return true;
}
@Data
class SkuWareHashStock{
private Long skuId; // skuid
private Integer num; // 锁定件数
private List<Long> wareId; // 锁定仓库id
}
}
- 查询这个商品在哪里有库存
gulimall-ware服务中com.atguigu.gulimall.ware.dao
路径下的 WareSkuDao 类:
package com.atguigu.gulimall.ware.dao;
@Mapper
public interface WareSkuDao extends BaseMapper<WareSkuEntity> {
/**
* 通过skuId查询在哪个仓库有库存
* @param skuId
* @return 仓库的编号
*/
List<Long> listWareIdHashSkuStock(@Param("skuId") Long skuId);
/**
* 锁库存
* @param skuId
* @param wareId
* @param num
* @return
*/
Long lockSkuStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);
}
gulimall-ware服务中gulimall-ware/src/main/resources/mapper/ware
路径下的 WareSkuDao.xml:
<update id="addStock">
UPDATE `wms_ware_sku` SET stock=stock+#{skuNum} WHERE sku_id=#{skuId} AND ware_id=#{wareId}
</update>
<select id="listWareIdHashSkuStock" resultType="java.lang.Long">
SELECT ware_id FROM wms_ware_sku WHERE sku_id=#{skuId} and stock-stock_locked>0;
</select>
- 编写异常返回类
gulimall-ware服务中com.atguigu.gulimall.ware.exception
路径下的 NoStockException:
package com.atguigu.gulimall.ware.exception;
public class NoStockException extends RuntimeException{
private Long skuId;
public NoStockException(Long skuId){
super("商品id:"+skuId+";没有足够的库存了!");
}
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
}
3)、在 错误码和错误信息定义类 BizCodeEnume枚举类中新增 库存 错误码和信息
gulimall-common服务中com.atguigu.common.exception
路径下的 BizCodeEnume:
21: 库存
package com.atguigu.common.exception;
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败"),
SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试"),
PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
USER_EXIST_EXCEPTION(15001,"用户名已存在"),
PHONE_EXIST_EXCEPTION(15002,"手机号已被注册"),
NO_STOCK_EXCEPTION(21000,"商品库存不足"),
LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"账号或密码错误");
private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
7.5、前端页面的修改
- 订单提交成功,跳转到支付页面 pay.html
<div class="Jdbox_BuySuc">
<dl>
<dt><img src="/static/order/pay/img/saoyisao.png" alt=""></dt>
<dd>
<span>订单提交成功,请尽快付款!订单号:[[${submitOrderResp.order.orderSn}]]</span>
<span>应付金额<font>[[${#numbers.formatDecimal(submitOrderResp.order.payAmount,1,2)}]]</font>元</span>
</dd>
<dd>
<span>推荐使用</span>
<span>扫码支付请您在<font>24小时</font>内完成支付,否则订单会被自动取消(库存紧订单请参见详情页时限)</span>
<span>订单详细</span>
</dd>
</dl>
</div>
- 订单提交失败,重定项到confirm.html 并回显 失败原因
<p class="p1">填写并核对订单信息 <span style="color: red" th:value="${msg!=null}" th:text="${msg}"></span></p>
7.6、主体代码
1、Controller层接口编写
gulimall-order服务中com.atguigu.gulimall.order.web
路径下的 OrderWebController:
package com.atguigu.gulimall.order.web;
@Controller
public class OrderWebController {
@Autowired
OrderService orderService;
@GetMapping("/toTrade")
public String toTrade(Model model) throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = orderService.confirmOrder();
model.addAttribute("orderConfirmData",confirmVo);
return "confirm";
}
/**
* 下单功能
* @param vo
* @return
*/
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes){
// 1、创建订单、验令牌、验价格、验库存
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+="商品库存不足";break;
}
redirectAttributes.addAttribute("msg",msg);
return "redirect:http://order.gulimall.cn/toTrade";
}
} catch (Exception e){
if (e instanceof NoStockException) {
String message = e.getMessage();
redirectAttributes.addFlashAttribute("msg", message);
}
return "redirect:http://order.gulimall.cn/toTrade";
}
}
}
2、Service层代码
gulimall-order服务中com.atguigu.gulimall.order.service.impl
路径下的 OrderServiceImpl:
package com.atguigu.gulimall.order.service.impl;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.exception.NoStockException;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberRespVo;
import com.atguigu.gulimall.order.constant.OrderConstant;
import com.atguigu.gulimall.order.entity.OrderItemEntity;
import com.atguigu.gulimall.order.enume.OrderStatusEnum;
import com.atguigu.gulimall.order.feign.CartFeignService;
import com.atguigu.gulimall.order.feign.MemberFeignService;
import com.atguigu.gulimall.order.feign.ProductFeignService;
import com.atguigu.gulimall.order.feign.WareFeignService;
import com.atguigu.gulimall.order.interceptoe.LoginUserInterceptor;
import com.atguigu.gulimall.order.service.OrderItemService;
import com.atguigu.gulimall.order.to.OrderCreateTo;
import com.atguigu.gulimall.order.vo.*;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.order.dao.OrderDao;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.service.OrderService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
@Autowired
MemberFeignService memberFeignService;
@Autowired
CartFeignService cartFeignService;
@Autowired
ThreadPoolExecutor executor;
@Autowired
WareFeignService wareFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
ProductFeignService productFeignService;
@Autowired
OrderItemService orderItemService;
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<OrderEntity> page = this.page(
new Query<OrderEntity>().getPage(params),
new QueryWrapper<OrderEntity>()
);
return new PageUtils(page);
}
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
// 获取主线程的请求域
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 1、远程查询所有的地址列表
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 将主线程的请求域放在该线程请求域中
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddressVos(address);
}, executor);
// 2、远程查询购物车所有选中的购物项
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
// 将主线程的请求域放在该线程请求域中
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor).thenRunAsync(()->{
// 批量查询商品项库存
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R hasStock = wareFeignService.getSkusHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data != null) {
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
}, executor);
// feign在远程调用请求之前要构造
// 3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.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);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
/**
* 下单操作:验令牌、创建订单、验价格、验库存
* @param vo
* @return
*/
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
// 在当前线程共享 OrderSubmitVo
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
// 从拦截器中拿到当前的用户
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
response.setCode(0);
// 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
// 原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
// 令牌验证失败
response.setCode(1);
return response;
} else {
// 令牌验证成功
// 2、创建订单、订单项等信息
OrderCreateTo order = createOrder();
// 3、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
// 金额对比成功
// 4、保存订单;
saveOrder(order);
// 5、库存锁定,只要有异常回滚订单数据
// 订单号,所有订单项(skuId,skuName,num)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);
// TODO 远程锁库存
R r = wareFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
// 锁成功了
response.setOrder(order.getOrder());
return response;
}else {
// 锁定失败
throw new NoStockException((String) r.get("msg"));
}
} else {
// 金额对比失败
response.setCode(2);
return response;
}
}
}
/**
* 保存订单、订单项数据
* @param order
*/
private void saveOrder(OrderCreateTo order) {
OrderEntity orderEntity = order.getOrder();
orderEntity.setModifyTime(new Date());
this.save(orderEntity);
List<OrderItemEntity> orderItems = order.getOrderItems();
orderItemService.saveBatch(orderItems);
}
/**
* 创建订单、订单项等信息
* @return
*/
private OrderCreateTo createOrder(){
OrderCreateTo createTo = new OrderCreateTo();
// 1、生成一个订单号
String orderSn = IdWorker.getTimeId();
// 2、构建一个订单
OrderEntity orderEntity = buildOrder(orderSn);
// 3、获取到所有的订单项
List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
// 4、计算价格、积分等相关信息
computePrice(orderEntity,itemEntities);
createTo.setOrder(orderEntity);
createTo.setOrderItems(itemEntities);
return createTo;
}
/**
* 构建订单
* @param orderSn
* @return
*/
private OrderEntity buildOrder(String orderSn) {
MemberRespVo respVp = LoginUserInterceptor.loginUser.get();
OrderEntity entity = new OrderEntity();
entity.setOrderSn(orderSn);
entity.setMemberId(respVp.getId());
OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
// 1、获取运费 和 收货信息
R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
});
// 2、设置运费
entity.setFreightAmount(fareResp.getFare());
// 3、设置收货人信息
entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
entity.setReceiverProvince(fareResp.getAddress().getProvince());
entity.setReceiverRegion(fareResp.getAddress().getRegion());
entity.setReceiverCity(fareResp.getAddress().getCity());
entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
entity.setReceiverName(fareResp.getAddress().getName());
entity.setReceiverPhone(fareResp.getAddress().getPhone());
// 4、设置订单的相关状态信息
entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
// 5、默认取消信息
entity.setAutoConfirmDay(7);
return entity;
}
/**
* 构建所有订单项数据
* @return
*/
private List<OrderItemEntity> buildOrderItems(String orderSn) {
// 最后确定每个购物项的价格
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
if (currentUserCartItems != null && currentUserCartItems.size()>0){
List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
OrderItemEntity itemEntity = buildOrderItem(cartItem);
itemEntity.setOrderSn(orderSn);
return itemEntity;
}).collect(Collectors.toList());
return itemEntities;
}
return null;
}
/**
* 构建某一个订单项
* @param cartItem
* @return
*/
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
OrderItemEntity itemEntity = new OrderItemEntity();
// 1、订单信息:订单号 v
// 2、商品的spu信息
Long skuId = cartItem.getSkuId();
R r = productFeignService.getSpuInfoBySkuId(skuId);
SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
});
itemEntity.setSpuId(data.getId());
itemEntity.setSpuBrand(data.getBrandId().toString());
itemEntity.setSpuName(data.getSpuName());
itemEntity.setCategoryId(data.getCatalogId());
// 3、商品的sku信息 v
itemEntity.setSkuId(cartItem.getSkuId());
itemEntity.setSkuName(cartItem.getTitle());
itemEntity.setSkuPic(cartItem.getImage());
itemEntity.setSkuPrice(cartItem.getPrice());
itemEntity.setSkuQuantity(cartItem.getCount());
itemEntity.setSkuAttrsVals(StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(),";"));
// 4、优惠信息【不做】
// 5、积分信息
itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
// 6、订单项的价格信息
itemEntity.setPromotionAmount(new BigDecimal("0"));
itemEntity.setCouponAmount(new BigDecimal("0"));
itemEntity.setIntegrationAmount(new BigDecimal("0"));
// 当前订单项的实际金额 总额-各种优惠
BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount()).
subtract(itemEntity.getCouponAmount()).
subtract(itemEntity.getIntegrationAmount());
itemEntity.setRealAmount(subtract);
return itemEntity;
}
/**
* 计算价格
* @param orderEntity
* @param itemEntities
*/
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
BigDecimal total = new BigDecimal("0.0");
BigDecimal coupon = new BigDecimal("0.0");
BigDecimal integration = new BigDecimal("0.0");
BigDecimal promotion = new BigDecimal("0.0");
BigDecimal gift = new BigDecimal("0.0");
BigDecimal growth = new BigDecimal("0.0");
// 1、订单的总额,叠加每一个订单项的总额信息
for (OrderItemEntity entity : itemEntities) {
total = total.add(entity.getRealAmount());
coupon = coupon.add(entity.getCouponAmount());
integration = integration.add(entity.getIntegrationAmount());
promotion = promotion.add(entity.getPromotionAmount());
gift = gift.add(new BigDecimal(entity.getGiftIntegration()));
growth = growth.add(new BigDecimal(entity.getGiftGrowth()));
}
// 订单总额
orderEntity.setTotalAmount(total);
// 应付总额
orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
orderEntity.setCouponAmount(coupon);
orderEntity.setIntegrationAmount(integration);
orderEntity.setPromotionAmount(promotion);
// 设置积分等信息
orderEntity.setIntegration(gift.intValue());
orderEntity.setGrowth(growth.intValue());
orderEntity.setDeleteStatus(0);//0 未删除
}
}
八、分布式事务
8.1、本地事务在分布式下的问题
问题:
- 远程服务假失败:
远程服务其实成功了,由于网络故障等没有返回。
导致:订单回滚,库存却扣减 - 远程服务执行完成,下面的其他方法出现问题
导致:已执行的远程请求,肯定不能回滚。数据不一致问题
SpringBoot事务的坑:
在同一个类里面,编写两个方法,内部调用的的时候,会导致事务设置失效。原因没有用到代理对象的缘故。
- 概括:同一个对象内事务方法互调默认失效(事务是加上的,但是事务的设置失效。比如说:设置超时时间),原因:绕过了代理对象
- 解决:使用代理对象来调用事务方法
- 引入
spring-boot-starter-aop
,(帮我们引入了aspectj) - @EnableTransactionManagement(proxyTargetClass = true) :对外暴露代理对象
- @EnableAspectJAutoProxy(exposeProxy = true) :开启 aspectj 动态代理功能。
- AopContext.currentProxy() : 调用方法
- 引入
8.2、分布式事务
- 分布式系统经常出现以下异常:
- 机器宕机、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失……
8.2.1、分布式cap定理 和 BASE理论
cap定理
CAP 原则又称 CAP 定理,指的是在一个分布式系统中
- 一致性 (Consistency):
- 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
- 可用性 (Availability):
- 在集群中一部分节点故障后,集群整体是否还能享用客户端的读写请求。(对数据更新具备高可用性)
- 分区容错性 (Partition tolerance):
- 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。
分区容错的意思是:区间通信可能失败。
比如:一台服务器放在中国,另一台服务器放在美国,这就是两个区,他们之间可能无法通信。
- 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。
CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾
- CP
- AP
BASE理论
是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可以适当的采取若一致性,即 最终一致性。
BASE 是指:
- 基本可用(Basically Available)
- 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如:响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。
- 响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了 1~2秒。
- 功能上的损失:购物网站在购物高峰(如双十一时),为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
- 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如:响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。
- 软状态(Soft State)
- 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。mysql replication 的异步复制也是一种体现。
- 最终一致性(Nventual Consistency)
- 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。
- 对于关系型数据库,要求更新过的数据能够被后续的访问都能看到,这是强一致性。
- 如果能容忍后续的部分或者全部访问不到,则是弱一致性。
- 如果经过一段时间后要求能够访问到更新后的数据,则是最终一致性。
8.2.2、分布式事务几种方案
1、2PC 模式
2、柔性事务-TCC事务补偿型方案
3、柔性事务-最大努力通知方案
4、柔性事务-可靠消息+最终一致性方案(异步确保型)
九、Seata
* 6、Seata控制分布式事务
* 1)、每一个微服务先必须创建ubdo_log回滚日志表;
* 2)、安装事务协调器:seata-server: https://github.com/seata/seata/releases
* 3)、整合
* 1、导入依赖 :spring-cloud-starter-alibaba-seata seata-all-0.7.1
* 2、解压启动seata-server;
* registry.conf :注册中心配置 修改它: registry type = "nacos"
* file.conf:
* 3、所有想用到分布式事务的微服务 使用 seata DataSourceProxy代理自己的数据源
* 4、每个微服务都必须要导入
* registry.conf
* file.conf vgroup_mapping.{application.name}-fescar-service-group = "default"
* 5、给分布式大事务的入口标注: @GlobalTransactional
* 6、每一个小事务标注: @Transactional
提前透露:本项目没有采用!
Satia概述:
- Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
- TC (Transaction Coordinator) - 事务协调者
- 维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器
- 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器
- 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
我们只需要使用一个 @GlobalTransactional
注解在业务方法上:
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
......
}
9.1、Seata 环境准备
1、在每个微服务数据库里创建一个undo_log(回滚日志表)
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2、安装事务协调器:seata-server: https://github.com/seata/seata/releases
-
解压启动seata-server
-
修改 registry.conf :注册中心配置
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 指定注册中心
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = "public"
cluster = "default"
}
- 启动 seata-server
hgw@HGWdeMacBook-Air bin # sh seata-server.sh
9.2、整合
1、导入依赖 :spring-cloud-starter-alibaba-seata seata-all-0.7.1
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
2、给分布式大事务的路口标注@GlobalTransactional; 每一个远程的小事务用 @Transactional
在 gulimall-order服务中com/atguigu/gulimall/order/service/impl/OrderServiceImpl.java
的 SubmitOrderResponseVo方法加上@GlobalTransactional
注解
@GlobalTransactional
@Transactional // 本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚。
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//.....
}
3、配置代理数据源 使用 seata DataSourceProxy代理自己的数据源
因为 Seata 通过代理数据源实现分支事务,如果没有注入,事务无法回滚
添加“com.atguigu.gulimall.order.config.MySeataConfig”类,代码如下:
package com.atguigu.gulimall.order.config;
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
//得到数据源
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())){
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
4、每个微服务都必须要导入
-
registry.conf
-
file.conf
分别给gulimall-order和gulimall-ware加上file.conf和registry.conf这两个配置,并修改file.conf
5、给所有还不使用seata的服务排除掉,修改其pom.xml文件
<exclusion>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</exclusion>
十、RabbitMQ 延时队列
[本项目通过RabbitMQ延时队列 实现 柔性事务+可靠消息+最终一致性发难]
引入:RabbitMq延时队列的目的是为了解决事务最终一致性。
- 场景:
比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。 - 常见解决方案:
spirng的 schedule 定时任务轮训数据库- 缺点:
消耗系统内存、增加了数据库的压力、存在较大的时间误差 - 解决:
RabbitMQ的消息TTL和死信Exchange结合
- 缺点:
10.1、RabbitMQ相关概念
10.1.1、消息的TTL(Time To Live)
- 消息的TTL就是消息的存活时间。
- RabbitMQ 可以对队列 和 消息分别设置TTL.
- 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
- 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。
10.1.2、死信队列 DLX 全称(Dead-Letter-Exchange)
-
死信队列&死信交换器:,称之为死信交换器,当消息变成一个死信之后,如果这个消息所在的队列存在x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换器上,这个交换器就称之为死信交换器,与这个死信交换器绑定的队列就是死信队列。
-
死信消息:
- 消息被拒绝(Basic.Reject或Basic.Nack)并且设置 requeue 参数的值为 false
- 消息过期了
- 队列达到最大的长度
-
过期消息:
在 rabbitmq 中存在2种方可设置消息的过期时间,- 第一种通过对队列进行设置,这种设置后,该队列中所有的消息都存在相同的过期时间,
- 队列设置:在队列申明的时候使用 x-message-ttl 参数,单位为 毫秒
- 第二种通过对消息本身进行设置,那么每条消息的过期时间都不一样。
- 单个消息设置:是设置消息属性的 expiration 参数的值,单位为 毫秒
如果同时使用这2种方法,那么以过期时间小的那个数值为准。当消息达到过期时间还没有被消费,那么那个消息就成为了一个 死信 消息。
- 第一种通过对队列进行设置,这种设置后,该队列中所有的消息都存在相同的过期时间,
**延时队列:**在rabbitmq中不存在延时队列,但是我们可以通过设置消息的过期时间和死信队列来模拟出延时队列。消费者监听死信交换器绑定的队列,而不要监听消息发送的队列。
10.2、延时队列定时关单模拟
Order-event-exchange 交换机 绑定了两个队列
- order.delay.queue队列,绑定的路由键是:order.creare.order
- order.release.order.queue 队列,绑定的路由键是:order.release.order
- 下订单,发送到服务器,消息的路由键是:order.creare.order
- 消息发送到 order.delay.queue队列
- 一分钟后,消息TTL已过期,发送到 Order-event-exchange 交换机,携带路由键是:order.release.order
- 消息发送到order.release.order.queue队列,监听该队列
**第一步、**创建相应的交换机、队列、以及交换机和队列的绑定 并 编写一个队列监听
package com.atguigu.gulimall.order.config;
@Configuration
public class MyMQConfig {
@RabbitListener(queues = "order.release.order.queue")
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息:准备关闭订单!" + entity.getOrderSn());
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
/**
* Spring中注入Bean之后,容器中的Binding、Queue、Exchange 都会自动创建(前提是RabbitMQ中没有)
* RabbitMQ 只要有,@Bean属性发生变化也不会覆盖
* @return
* Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
*/
@Bean
public Queue orderDelayQueue(){
HashMap<String, Object> arguments = new HashMap<>();
/**
* x-dead-letter-exchange :order-event-exchange 设置死信路由
* x-dead-letter-routing-key : order.release.order 设置死信路由键
* x-message-ttl :60000
*/
arguments.put("x-dead-letter-exchange","order-event-exchange");
arguments.put("x-dead-letter-routing-key","order.release.order");
arguments.put("x-message-ttl",30000);
Queue queue = new Queue("order.delay.queue", true, false, false,arguments);
return queue;
}
@Bean
public Queue orderReleaseOrderQueue(){
return new Queue("order.release.order.queue", true, false, false);
}
@Bean
public Exchange orderEventExchange(){
// TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
public Binding orderCreateOrder(){
// Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseOrder(){
// Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
}
第二步、编写个Controller用来发送消息
修改“com.atguigu.gulimall.order.web.HelloController”类代码如下:
package com.atguigu.gulimall.order.web;
@Controller
public class HelloController {
@Autowired
RabbitTemplate rabbitTemplate;
// ......
@ResponseBody
@GetMapping("/test/createOrder")
public String createOrderTest(){
// 此处模拟:省略订单下单成功,并保存到数据库
OrderEntity entity = new OrderEntity();
entity.setOrderSn(UUID.randomUUID().toString());
entity.setModifyTime(new Date());
// 给MQ发送消息
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",entity);
return "ok";
}
}
十一、库存自动解锁
11.1、gulimall-ware 服务添加RabbitMQ
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、添加配置
spring:
rabbitmq:
host: 124.222.223.222
virtual-host: /
username: guest
password: guest
listener:
simple:
acknowledge-mode: manual
3、主启动类添加注解
@EnableRabbit
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallWareApplication.class, args);
}
}
11.2、创建业务交换机 & 队列 以及之间的绑定
创建相应的交换机、队列、以及交换机和队列的绑定
gulimall-ware服务中添加“com.atguigu.gulimall.ware.config.MyRabbitConfig”类,代码如下:
package com.atguigu.gulimall.ware.config;
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
@RabbitListener(queues = "stock.release.stock.queue")
public void handle(Message message){
}
/**
* 使用JSON序列化机制,进行消息转换
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
@Bean
public Exchange exchange(){
return new TopicExchange("stock-event-exchange", true, false);
}
@Bean
public Queue stockReleaseStockQueue() {
return new Queue("stock.release.stock.queue", true, false, false);
}
@Bean
public Queue stockDelayQueue() {
// String name, boolean durable, boolean exclusive, boolean autoDelete,
// @Nullable Map<String, Object> arguments
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
arguments.put("x-message-ttl", 120000);
return new Queue("stock.delay.queue", true, false, false, arguments);
}
@Bean
public Binding stockReleaseStockBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
new HashMap<>());
}
@Bean
public Binding orderLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
new HashMap<>());
}
}
11.3、监听库存解锁
库存解锁的场景
- 下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
- 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚;之前锁定的库存就要自动解锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nuTf7P7D-1650102697212)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220414195547133.png)]
11.3.1、环境修改
1、修改gulimall-ware 仓储服务数据库的wms_ware_order_task_detail表结构
2、修改“com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity”类,代码 如下:
package com.atguigu.gulimall.ware.entity;
@Data
@TableName("wms_ware_order_task_detail")
public class WareOrderTaskDetailEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* sku_id
*/
private Long skuId;
/**
* sku_name
*/
private String skuName;
/**
* 购买个数
*/
private Integer skuNum;
/**
* 工作单id
*/
private Long taskId;
/**
* 仓库id
*/
private Long wareId;
/**
* 锁定状态,1-已锁定 2-已解锁 3-扣减
*/
private Integer lockStatus;
}
3、修改 Mapper文件
修改resources/mapper/ware/WareOrderTaskDetailDao.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.gulimall.ware.dao.WareOrderTaskDetailDao">
<!-- 可根据自己的需求,是否要使用 -->
<resultMap type="com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity" id="wareOrderTaskDetailMap">
<result property="id" column="id"/>
<result property="skuId" column="sku_id"/>
<result property="skuName" column="sku_name"/>
<result property="skuNum" column="sku_num"/>
<result property="taskId" column="task_id"/>
<result property="wareId" column="ware_id"/>
<result property="lockStatus" column="lock_status"/>
</resultMap>
</mapper>
11.3.2、告诉MQ库存锁定成功
1)、封装给MQ发送的数据 To类
gulimall-conmmon服务 com.atguigu.common.to.mq
路径下编写:StockLockedTo类、StockDetailTo类
package com.atguigu.common.to.mq;
@Data
public class StockLockedTo {
/**
* 库存工作单的id
*/
private Long id;
/**
* 工作单详情
*/
private StockDetailTo detailTo;
}
package com.atguigu.common.to.mq;
/**
* Data time:2022/4/14 20:21
* StudentID:2019112118
* Author:hgw
* Description: 详情单
*/
@Data
public class StockDetailTo {
private Long id;
/**
* sku_id
*/
private Long skuId;
/**
* sku_name
*/
private String skuName;
/**
* 购买个数
*/
private Integer skuNum;
/**
* 工作单id
*/
private Long taskId;
/**
* 仓库id
*/
private Long wareId;
/**
* 锁定状态,1-已锁定 2-已解锁 3-扣减
*/
private Integer lockStatus;
}
2)、编写 告诉MQ库存锁定成功
修改gulimall-ware 服务 com.atguigu.gulimall.ware.service.imp
路径下的 WareSkuServiceImpl 类,代码如下
- 保存库存工作单
- 保存库存工作单详情
- 给MQ发送锁定库存以及详情消息
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
/**
* 保存库存工作单的性情
* 追溯
*/
// 1、保存库存工作单
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(vo.getOrderSn());
orderTaskService.save(taskEntity);
// 1、每个商品在哪个库存里有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHashStock> collect = locks.stream().map(item -> {
SkuWareHashStock stock = new SkuWareHashStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
// 查询这个商品在哪里有库存
List<Long> wareIds = wareSkuDao.listWareIdHashSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList());
// 2、锁定库存
for (SkuWareHashStock hashStock : collect) {
Boolean skuStocked = false;
Long skuId = hashStock.getSkuId();
List<Long> wareIds = hashStock.getWareId();
if (wareIds == null || wareIds.size()==0){
// 没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
// 1、如果每一个商品都锁定成功,将当前商品锁定了几件的的工作单记录发送给MQ
// 2、如果有一个商品锁定失败,则前面锁定的就回滚了。发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
// 1、
for (Long wareId : wareIds) {
// 成功就返回1,否则就返回0
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hashStock.getNum());
if (count == 1){
skuStocked = true;
// TODO 告诉MQ库存锁定成功
// 2、保存库存工作单详情
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null,skuId,"",hashStock.getNum(),taskEntity.getId(),wareId,1);
orderTaskDetailService.save(entity);
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
StockDetailTo stockDetailTo = new StockDetailTo();
BeanUtils.copyProperties(entity,stockDetailTo);
// 只发id不行,防止回滚以后找不到数据
lockedTo.setDetailTo(stockDetailTo);
rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
break;
} else {
// 当前仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false){
// 当前商品所有仓库都没有锁住,其他商品也不需要锁了,直接返回没有库存了
throw new NoStockException(skuId);
}
}
// 3、运行到这,全部都是锁定成功的
return true;
}
@Data
class SkuWareHashStock{
private Long skuId; // skuid
private Integer num; // 锁定件数
private List<Long> wareId; // 锁定仓库id
}
11.3.3、监听库存自动解锁
-
库存自动解锁
- 1、查询数据库关于这个订单的锁库存消息
- 有,证明库存锁定成功了。
- 1、没有这个订单。必须解锁
- 2、有这个订单。不是解锁库存。
- 订单状态:已取消:解锁库存
- 订单状态:没取消:不能解锁
- 没有,库存锁定失败了,库存回滚了。这种情况无需解锁
- 有,证明库存锁定成功了。
- 1、查询数据库关于这个订单的锁库存消息
1)、主体代码封装
gulimall-ware 服务中 com.atguigu.gulimall.ware.listener
路径下 StockReleaseListener
@Slf4j
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("收到解锁库存的消息");
try {
wareSkuService.unlockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
/**
* 1、库存自动解锁
* 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
* 2、订单失败
* 锁库存失败,则库存回滚了,这种情况无需解锁
* 如何判断库存是否锁定失败呢?查询数据库关于这个订单的锁库存消息即可
* 自动ACK机制:只要解决库存的消息失败,一定要告诉服务器解锁是失败的。启动手动ACK机制
* @param to
*
*/
@Override
public void unlockStock(StockLockedTo to) {
StockDetailTo detail = to.getDetailTo();
Long detailId = detail.getId();
/**
* 1、查询数据库关于这个订单的锁库存消息
* 有,证明库存锁定成功了。
* 1、没有这个订单。必须解锁
* 2、有这个订单。不是解锁库存。
* 订单状态:已取消:解锁库存
* 订单状态:没取消:不能解锁
* 没有,库存锁定失败了,库存回滚了。这种情况无需解锁
*/
WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
if (byId != null) {
Long id = to.getId(); // 库存工作单的Id,拿到订单号
WareOrderTaskEntity taskEntity = orderTaskService.getById(id);
String orderSn = taskEntity.getOrderSn(); // 根据订单号查询订单的状态
R r = orderFeignService.getOrderStatus(orderSn);
if (r.getCode() == 0) {
// 订单数据返回成功
OrderVo data = r.getData(new TypeReference<OrderVo>() {
});
if (data == null || data.getStatus() == 4) {
// 订单不存在、订单已经被取消了,才能解锁库存
if (byId.getLockStatus() == 1){
// 当前库存工作单详情,状态1 已锁定但是未解锁才可以解锁
unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
}
} else {
// 消息拒绝以后重新放到队列里面,让别人继续消费解锁
throw new RuntimeException("远程服务失败");
}
}
} else {
// 无需解锁
}
}
/**
* 解库存锁
*
* @param skuId 商品id
* @param wareId 仓库id
* @param num 解锁数量
* @param taskDetailId 库存工作单ID
*/
private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
// 库存解锁
wareSkuDao.unlockStock(skuId, wareId, num);
// 更新库存工作单的状态
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
entity.setId(taskDetailId);
entity.setLockStatus(2);// 变为已解锁
orderTaskDetailService.updateById(entity);
}
2)、编写一个远程方法查询订单的状态
1、编写远程调用 gulimall-order 服务feign接口
gulimall-ware服务中 com.atguigu.gulimall.ware.feign
路径下的 OrderFeignService类,代码如下:
package com.atguigu.gulimall.ware.feign;
@FeignClient("gulimall-order")
public interface OrderFeignService {
@GetMapping("/order/order/status/{orderSn}")
R getOrderStatus(@PathVariable("orderSn") String orderSn);
}
2、gulimall-order服务中提供接口
gulimall-order服务中 com.atguigu.gulimall.order.controller
路径下的 OrderController类,代码如下:
@RestController
@RequestMapping("order/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 通过订单号获取订单的详细信息
* @param orderSn
* @return
*/
@GetMapping("/status/{orderSn}")
public R getOrderStatus(@PathVariable("orderSn") String orderSn){
OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
return R.ok().setData(orderEntity);
}
gulimall-order服务中 com.atguigu.gulimall.order.service.impl
路径下的 OrderServiceImpl类,代码如下:
@Override
public OrderEntity getOrderByOrderSn(String orderSn) {
OrderEntity order_sn = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
return order_sn;
}
3、本地编写接收信息的VO
gulimall-ware服务中 com.atguigu.gulimall.ware.vo
路径下的 OrderVo类,代码如下:
package com.atguigu.gulimall.ware.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* Data time:2022/4/14 21:05
* StudentID:2019112118
* Author:hgw
* Description:
*/
@Data
public class OrderVo {
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 订单号
*/
private String orderSn;
/**
* 使用的优惠券
*/
private Long couponId;
/**
* create_time
*/
private Date createTime;
/**
* 用户名
*/
private String memberUsername;
/**
* 订单总额
*/
private BigDecimal totalAmount;
/**
* 应付总额
*/
private BigDecimal payAmount;
/**
* 运费金额
*/
private BigDecimal freightAmount;
/**
* 促销优化金额(促销价、满减、阶梯价)
*/
private BigDecimal promotionAmount;
/**
* 积分抵扣金额
*/
private BigDecimal integrationAmount;
/**
* 优惠券抵扣金额
*/
private BigDecimal couponAmount;
/**
* 后台调整订单使用的折扣金额
*/
private BigDecimal discountAmount;
/**
* 支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
*/
private Integer payType;
/**
* 订单来源[0->PC订单;1->app订单]
*/
private Integer sourceType;
/**
* 订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
*/
private Integer status;
/**
* 物流公司(配送方式)
*/
private String deliveryCompany;
/**
* 物流单号
*/
private String deliverySn;
/**
* 自动确认时间(天)
*/
private Integer autoConfirmDay;
/**
* 可以获得的积分
*/
private Integer integration;
/**
* 可以获得的成长值
*/
private Integer growth;
/**
* 发票类型[0->不开发票;1->电子发票;2->纸质发票]
*/
private Integer billType;
/**
* 发票抬头
*/
private String billHeader;
/**
* 发票内容
*/
private String billContent;
/**
* 收票人电话
*/
private String billReceiverPhone;
/**
* 收票人邮箱
*/
private String billReceiverEmail;
/**
* 收货人姓名
*/
private String receiverName;
/**
* 收货人电话
*/
private String receiverPhone;
/**
* 收货人邮编
*/
private String receiverPostCode;
/**
* 省份/直辖市
*/
private String receiverProvince;
/**
* 城市
*/
private String receiverCity;
/**
* 区
*/
private String receiverRegion;
/**
* 详细地址
*/
private String receiverDetailAddress;
/**
* 订单备注
*/
private String note;
/**
* 确认收货状态[0->未确认;1->已确认]
*/
private Integer confirmStatus;
/**
* 删除状态【0->未删除;1->已删除】
*/
private Integer deleteStatus;
/**
* 下单时使用的积分
*/
private Integer useIntegration;
/**
* 支付时间
*/
private Date paymentTime;
/**
* 发货时间
*/
private Date deliveryTime;
/**
* 确认收货时间
*/
private Date receiveTime;
/**
* 评价时间
*/
private Date commentTime;
/**
* 修改时间
*/
private Date modifyTime;
}
3)、解锁库存方法编写详情
gulimall-ware服务中的 /com/atguigu/gulimall/ware/service/impl/WareSkuServiceImpl.java
路径下 WareSkuServiceImpl.java类的方法
/**
* 解库存锁
* @param skuId 商品id
* @param wareId 仓库id
* @param num 解锁数量
* @param taskDetailId 库存工作单ID
*/
private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
wareSkuDao.unlockStock(skuId,wareId,num);
}
void unlockStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);
gulimall-ware服务中的 resources/mapper/ware/WareSkuDao.xml
文件
<update id="unlockStock">
UPDATE wms_ware_sku SET stock_locked=stock_locked-#{num} WHERE sku_id=#{skuId} AND ware_id=#{wareId}
</update>
4)、由于gulimall-order添加了拦截器,只要使用该服务必须登录才行。因为这边需要远程调用订单,但不需要登录,所以给这个路径放行
修改gulimall-order 服务的 com.atguigu.gulimall.order.interceptoe
路径下 LoginUserInterceptor类
package com.atguigu.gulimall.order.interceptoe;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
/**
* 用户登录拦截器
* @param request
* @param response
* @param handler
* @return
* 用户登录:放行
* 用户未登录:跳转到登录页面
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// /order/order/status/222222222
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
if (match){
return true;
}
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute!=null){
loginUser.set(attribute);
return true;
} else {
// 没登录就去登录
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.gulimall.cn/login.html");
return false;
}
}
}
11.4、代码整理
1)、创建一个类监听 stock.release.stock.queue
队列
gulimall-ware服务的 com.atguigu.gulimall.ware.listener
路径 StockReleaseListener 类,接收到消息之后调用 Service层 WareSkuServiceImpl.java 实现类的 unlockStock 方法实现解锁库存:
- 没有异常捕捉,则成功解锁消息。手动ACK
- 捕捉到异常,则 消息拒绝以后重新放到队列里面,让别人继续消费解锁
package com.atguigu.gulimall.ware.listener;
@Slf4j
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("收到解锁库存的消息");
try {
wareSkuService.unlockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
2)、service层业务方法
gulimall-ware服务的 com.atguigu.gulimall.ware.service.impl
路径 WareSkuServiceImpl 类
/**
* 1、库存自动解锁
* 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
* 2、订单失败
* 锁库存失败,则库存回滚了,这种情况无需解锁
* 如何判断库存是否锁定失败呢?查询数据库关于这个订单的锁库存消息即可
* 自动ACK机制:只要解决库存的消息失败,一定要告诉服务器解锁是失败的。启动手动ACK机制
* @param to
*
*/
@Override
public void unlockStock(StockLockedTo to) {
StockDetailTo detail = to.getDetailTo();
Long detailId = detail.getId();
/**
* 1、查询数据库关于这个订单的锁库存消息
* 有,证明库存锁定成功了。
* 1、没有这个订单。必须解锁
* 2、有这个订单。不是解锁库存。
* 订单状态:已取消:解锁库存
* 订单状态:没取消:不能解锁
* 没有,库存锁定失败了,库存回滚了。这种情况无需解锁
*/
WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
if (byId != null) {
Long id = to.getId(); // 库存工作单的Id,拿到订单号
WareOrderTaskEntity taskEntity = orderTaskService.getById(id);
String orderSn = taskEntity.getOrderSn(); // 根据订单号查询订单的状态
R r = orderFeignService.getOrderStatus(orderSn);
if (r.getCode() == 0) {
// 订单数据返回成功
OrderVo data = r.getData(new TypeReference<OrderVo>() {
});
if (data == null || data.getStatus() == 4) {
// 订单不存在、订单已经被取消了,才能解锁库存
if (byId.getLockStatus() == 1){
// 当前库存工作单详情,状态1 已锁定但是未解锁才可以解锁
unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
}
} else {
// 消息拒绝以后重新放到队列里面,让别人继续消费解锁
throw new RuntimeException("远程服务失败");
}
}
} else {
// 无需解锁
}
}
/**
* 解库存锁
*
* @param skuId 商品id
* @param wareId 仓库id
* @param num 解锁数量
* @param taskDetailId 库存工作单ID
*/
private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
// 库存解锁
wareSkuDao.unlockStock(skuId, wareId, num);
// 更新库存工作单的状态
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
entity.setId(taskDetailId);
entity.setLockStatus(2);// 变为已解锁
orderTaskDetailService.updateById(entity);
}
十二、定时关单
- 在订单创建成功时向MQ中 延时队列发送消息,携带路由键:
order.create.order
- 30分钟后未支付,则释放订单服务 向MQ中发送消息,携带路由键:
order.release.order
- 监听该
order.release.order.queue
队列,进行释放订单服务
- 监听该
- 30分钟后未支付,则释放订单服务 向MQ中发送消息,携带路由键:
- 此时存在一种情况,存在订单创建成功之后出现延时卡顿,消息延迟,导致订单解锁在库存解锁之后完成
- 则每次库存解锁之后 向MQ中发送消息,携带路由键:
order.release.other
- 监听
stock.release.stock.queue
,编写一个重载方法,进行判断- 查一下最新库存的状态,防止重复解锁库存
- 按照工作单找到所有 没有解锁的库存,进行解锁
- 查一下最新库存的状态,防止重复解锁库存
- 监听
- 则每次库存解锁之后 向MQ中发送消息,携带路由键:
12.1、创建交换机、队列以及之间的绑定
package com.atguigu.gulimall.order.config;
@Configuration
public class MyMQConfig {
/**
* Spring中注入Bean之后,容器中的Binding、Queue、Exchange 都会自动创建(前提是RabbitMQ中没有)
* RabbitMQ 只要有,@Bean属性发生变化也不会覆盖
* @return
* Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
*/
@Bean
public Queue orderDelayQueue(){
HashMap<String, Object> arguments = new HashMap<>();
/**
* x-dead-letter-exchange :order-event-exchange 设置死信路由
* x-dead-letter-routing-key : order.release.order 设置死信路由键
* x-message-ttl :60000
*/
arguments.put("x-dead-letter-exchange","order-event-exchange");
arguments.put("x-dead-letter-routing-key","order.release.order");
arguments.put("x-message-ttl",30000);
Queue queue = new Queue("order.delay.queue", true, false, false,arguments);
return queue;
}
@Bean
public Queue orderReleaseOrderQueue(){
return new Queue("order.release.order.queue", true, false, false);
}
@Bean
public Exchange orderEventExchange(){
// TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
public Binding orderCreateOrder(){
// Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseOrder(){
// Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
/**
* 订单释放直接和库存释放进行绑定
* @return
*/
@Bean
public Binding orderReleaseOtherBingding(){
// Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
}
12.2、在订单创建成功时向MQ中 延时队列发送消息
12.3、在订单的关闭之后时向MQ发送消息
为了防止因为其他原因,订单的关闭延期了
/**
* 订单的关闭
* @param entity
*/
@Override
public void closeOrder(OrderEntity entity) {
// 1、查询当前这个订单的最新状态
OrderEntity orderEntity = this.getById(entity.getId());
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
// 2、关单
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
update.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(update);
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity, orderTo);
// 3、发给MQ一个
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
}
}
12.4、监听 order.release.order.queue
队列,释放订单服务
- gulimall-order 服务的
com.atguigu.gulimall.order.listener
路径下的 OrderClassListener类。
package com.atguigu.gulimall.order.listener;
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderClassListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息:准备关闭订单!" + entity.getOrderSn());
try {
orderService.closeOrder(entity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
- Service层中 OrderServiceImpl.java 实现类进行订单的关闭
/**
* 订单的关闭
* @param entity
*/
@Override
public void closeOrder(OrderEntity entity) {
// 1、查询当前这个订单的最新状态
OrderEntity orderEntity = this.getById(entity.getId());
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
// 2、关单
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
update.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(update);
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity, orderTo);
// 3、发给MQ一个
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderEntity);
}
}
12.5、监听 stock.release.stock.queue
队列,进行解锁
在 gulimall-ware 服务中,进行监听处理
1)、编写 StockReleaseListener 进行监听队列
package com.atguigu.gulimall.ware.listener;
import com.atguigu.common.to.mq.OrderTo;
import com.atguigu.common.to.mq.StockLockedTo;
import com.atguigu.gulimall.ware.service.WareSkuService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
* Data time:2022/4/14 21:47
* StudentID:2019112118
* Author:hgw
* Description:
*/
@Slf4j
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/**
* 库存自己过期处理
* @param to
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("收到解锁库存的消息");
try {
wareSkuService.unlockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
/**
* 订单关闭处理
* @param orderTo
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
System.out.println("订单关闭准备解锁库存");
try {
wareSkuService.unlockStock(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
2、Service层 WareSkuServiceImpl 实现类中,进行方法处理
/**
* 防止订单服务卡顿,导致订单状态一直修改不了,库存消息优先到期。查订单状态肯定是新建状态,什么都不做就走了
* 导致卡顿的订单,永远不能解锁库存
* @param orderTo
*/
@Transactional
@Override
public void unlockStock(OrderTo orderTo) {
String orderSn = orderTo.getOrderSn();
// 查一下最新库存的状态,防止重复解锁库存
WareOrderTaskEntity task = orderTaskService.getOrderTeskByOrderSn(orderSn);
Long taskId = task.getId();
// 按照工作单找到所有 没有解锁的库存,进行解锁
List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
.eq("task_id", taskId)
.eq("lock_status", 1));
// 进行解锁
for (WareOrderTaskDetailEntity entity : entities) {
unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
}
3、编写查询最新库存的状态,防止重复解锁库存
package com.atguigu.gulimall.ware.service.impl;
@Service("wareOrderTaskService")
public class WareOrderTaskServiceImpl extends ServiceImpl<WareOrderTaskDao, WareOrderTaskEntity> implements WareOrderTaskService {
//.....
@Override
public WareOrderTaskEntity getOrderTeskByOrderSn(String orderSn) {
WareOrderTaskEntity one = this.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
return one;
}
}
4、消息共享封装To
package com.atguigu.common.to.mq;
@Data
public class OrderTo {
/**
* id
*/
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 订单号
*/
private String orderSn;
/**
* 使用的优惠券
*/
private Long couponId;
/**
* create_time
*/
private Date createTime;
/**
* 用户名
*/
private String memberUsername;
/**
* 订单总额
*/
private BigDecimal totalAmount;
/**
* 应付总额
*/
private BigDecimal payAmount;
/**
* 运费金额
*/
private BigDecimal freightAmount;
/**
* 促销优化金额(促销价、满减、阶梯价)
*/
private BigDecimal promotionAmount;
/**
* 积分抵扣金额
*/
private BigDecimal integrationAmount;
/**
* 优惠券抵扣金额
*/
private BigDecimal couponAmount;
/**
* 后台调整订单使用的折扣金额
*/
private BigDecimal discountAmount;
/**
* 支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
*/
private Integer payType;
/**
* 订单来源[0->PC订单;1->app订单]
*/
private Integer sourceType;
/**
* 订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
*/
private Integer status;
/**
* 物流公司(配送方式)
*/
private String deliveryCompany;
/**
* 物流单号
*/
private String deliverySn;
/**
* 自动确认时间(天)
*/
private Integer autoConfirmDay;
/**
* 可以获得的积分
*/
private Integer integration;
/**
* 可以获得的成长值
*/
private Integer growth;
/**
* 发票类型[0->不开发票;1->电子发票;2->纸质发票]
*/
private Integer billType;
/**
* 发票抬头
*/
private String billHeader;
/**
* 发票内容
*/
private String billContent;
/**
* 收票人电话
*/
private String billReceiverPhone;
/**
* 收票人邮箱
*/
private String billReceiverEmail;
/**
* 收货人姓名
*/
private String receiverName;
/**
* 收货人电话
*/
private String receiverPhone;
/**
* 收货人邮编
*/
private String receiverPostCode;
/**
* 省份/直辖市
*/
private String receiverProvince;
/**
* 城市
*/
private String receiverCity;
/**
* 区
*/
private String receiverRegion;
/**
* 详细地址
*/
private String receiverDetailAddress;
/**
* 订单备注
*/
private String note;
/**
* 确认收货状态[0->未确认;1->已确认]
*/
private Integer confirmStatus;
/**
* 删除状态【0->未删除;1->已删除】
*/
private Integer deleteStatus;
/**
* 下单时使用的积分
*/
private Integer useIntegration;
/**
* 支付时间
*/
private Date paymentTime;
/**
* 发货时间
*/
private Date deliveryTime;
/**
* 确认收货时间
*/
private Date receiveTime;
/**
* 评价时间
*/
private Date commentTime;
/**
* 修改时间
*/
private Date modifyTime;
}
十三、消息丢失、积压、重复等解决方案
柔性事务-可靠消息+最终一致性方案(异步确保型)
-
防止消息丢失
- 做好消息确认机制(pulisher、consumer[手动ACK])
- 每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
13.1、如何保证消息可靠性-消息丢失
- 消息发送出去,由于网络问题没有抵达服务器
- 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式;
- 做好日志记录,每个消息状态是否都被服务器收到都应该记录;
- 做好定期重发,如果消息没有发生成,定期去数据库扫描未成功的消息进行重发;
- 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机
- Publisher 也必须加入确认回调机制,确认成功的消息,修改数据库消息状态
- 自动ACK的状态下。消费者收到消息,但没来得及消费然后宕机
- 一定开启手动ACK,消费成功才移除,失败或者没来得处理就noAck并重新入队
* 1、做好消息确认机制(pulisher、consumer[手动ACK])
* 2、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
13.2、如何保证消息可靠性-消息重复
13.3、如何保证消息可靠性-消息积压
十四、支付[支付宝]
14.1、支付宝配置相关概念
- 公钥 私钥
- 公钥和私钥时一个相对概念
- 他们的公私性是相对于生产者来说的
- 一对密钥生成后,保存在生产者手里的就是私钥
- 生成者发布出去大家用的就是公钥
此时:
- 密钥A 和 密钥C 就是私钥
- 密钥B 和 密钥D 就是公钥
- 加密 和 数字签名
获取支付宝的沙箱环境中商户的公钥匙、私钥、以及支付宝的公钥
14.2、环境准备-内网穿透常用软件和安装
续断:https://www.zhexi.tech/
第一步:登录
第三步:登录哲西云控制台,到“客户端”菜单,确认客户端已上线;
第四步、新建隧道
后面操作的时候会说
14.2、环境准备-内网穿透常用软件和安装
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v6n7UuOx-1650103370483)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220415140932328.png)]
续断:https://www.zhexi.tech/
第一步:登录
第三步:登录哲西云控制台,到“客户端”菜单,确认客户端已上线;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qziWABOV-1650103370483)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220415141404548.png)]
第四步、新建隧道
14.3、支付整合服务
第一步、导入依赖
第一步:导入依赖
<!--阿里支付模块-->
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.28.ALL</version>
</dependency>
第二步、抽取支付工具类并进行配置
第二步、抽取支付工具类并进行配置
成功调用该接口后,返回的数据就是支付页面的html,因此后续会使用@ResponseBody
1)、编写配置类
gulimall-order 服务的 com.atguigu.gulimall.order.config
路径下的 AlipayTemplate 配置类:
package com.atguigu.gulimall.order.config;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.atguigu.gulimall.order.vo.PayVo;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Data time:2022/4/15 14:52
* StudentID:2019112118
* Author:hgw
* Description: 支付宝沙箱测试配置类
*/
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
//在支付宝创建的应用的id
private String app_id = "2021000119667766";
// 商户私钥,您的PKCS8格式RSA2私钥
private String merchant_private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpzlIqpnBAEWCTIr4SaNrpQId9P2KTpJ7Pa2aGoElXYefW9BSlqv+oGz5hLn8VANNJAwjwazgDbIuaaTJ8yYYjWh1wxNf+c4iRDNpBb1qf9GZhrW4l/HGH6XCzFlRpo40CCCDBfgl+U7DLbI4h+4KXokEue6ALWsXBVTYLFTyxpsBC3UOauZUzccvHeczD746psV1oiYounxs4QWJrTBJtxWgRnD+mLPPtk79WjwYJAr0dYLACZVC6Bdi06khEgxnrbu3NJYH2qMts623cygQ0SWk2ZtRWhEwmpQ/Dd2ilsQKGQErkK5mVUmsuo87K4Z6Rq333B5Fleb4TLTVuWd+ZAgMBAAECggEAY/3a5MKd1xxkgkAzLSQRxMj7AAYTRl3qJrpX5W79wTcmDq4semH3qkZgtVlr/DJAOP5QhUKd+WYxzvujf1gsZSTrsTw49N2TzdaDr4SjGQ4SO/Kkqjm9oQsWEl9T1eE5Z7jhkQ9nB7zAnwmNqPUyMZiaSYUC+ay6Rt6mtGANHY7cFmLeBoz93W8V0ClhY2EwKMnyF/QlgR1no3qbjCMRsMHjkJoQBbswhoGpRSIbQidekB9cO71EFW/5dJV87W42UfoNQe4K61yhgWgrCHpVn9rFSdGIX0n8A48X/cPgYVu9Cm3GURoT71ePg1P++SwQGYnkO6xkR8PFMnpqpmDjAQKBgQDwkptaa+vWaFpGH6eSucYEZGMmdwyMiOfZo5kXgphMbd/nEyG0U0o2dtgWG1WbeS714PZgwgNnlidIOeNs0JDocamIAYSVgXdfZTPPJuyvUFVFbQGftPWGKSj90vWxHNS3oph6Yw41Del7UkqxFt1knDwycd4Amfo5nSEoeGSR0wKBgQC0sfZrPjn5hgSAkOazX+cugMfhYpZ69YLEVZkD9yVadV2D19xlwMSpf713g77ux2zSVKDIERbEgVkopjssGM64DgpkH7gwN18v+F6YcbCOB7gIX4hScykgVASXKTXyuUjGLaqs2aEuH/ULRxj+n8cH00x/RxGURROY52ciZRy5YwKBgH51cnh7loMkY5/M7/du9CpG4t/LYKtXJBkBqG31Vj2G3FXJdsQlrDMpEbm9MKkDcK4LTTfbhJKlGY0b8PK4SBQH+4fk1F8KqUdaGXvhCDW30rsl696Z7x5Q8J1MkZ5Ce4b0T5a2DzfQUlVjEqQ4UrSadAJIXNyQFDrI4C836hXFAoGAP0jo3gyQL3UhlImrUv1usVnHJ4fo3i2oW+0Cx2HCwljCpM9wUG7gMeEcUYRh1a0gztV27jsV90K6IEOAC+SwWcQJHaICV1i9TMa3ErsWs9e+O6iBzSaqK7lhVjPHwjfkZgxOb3VVPxtQLl/7QApjobj+XMFeRcifoXjCJUi2c7MCgYEAjpVdOAsZwrHI0YCuUU6xLHTagP9u80EMCr6D7xvfpimS1EL2wHxNwhZGzpTitFj25e8yTA27S2SYeHLvKBIaejclT1TY/Y9PSqmcvl2CCSB2hBw8gbxuBt607eObuR4fxt+/C11K9GAj6Dca+1pmoGfV0a0OPTfcESHQxXM89hg=";
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
private String alipay_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyay7fjd6Yf2pyGWQK/efrZggwUBE2fEvDoZ1+q6P21d86Rcbzz4lL2qrl4UPgChOE29SNRCKDnWIthXCBMTtRcf6SxCtaldp7D+1uXBmzxoEzKQ2lDjep+pnFMpoA+3CB4nOArL7B2hThh/F2ofbEDK0IJJVXWksZSSjfsKQm0+BXcrYMWe6khcf2S9NnBSnMB1bnHmnMK69oObsg8/dBp6cHruFoMu8OGCMIDO0Z6W7hoywzkf3K08VrqxGOhM5p94oGSBQJcD2CclK8c5wvHFMZm0wtmxWkyY2zdQ84stGJnLhX9ORmfHo/HBeXX8xPGF91SZU1yZ0gmatQZK91QIDAQAB";
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
private String notify_url="http://**.natappfree.cc/payed/notify";
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//同步通知,支付成功,一般跳转到成功页
private String return_url="http://member.gulimall.cn/memberOrder.html";
// 签名方式
private String sign_type = "RSA2";
// 字符编码格式
private String charset = "utf-8";
// 支付宝网关; https://openapi.alipaydev.com/gateway.do
private String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";
public String pay(PayVo vo) throws AlipayApiException {
//AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
//1、根据支付宝的配置生成一个支付客户端
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
app_id, merchant_private_key, "json",
charset, alipay_public_key, sign_type);
//2、创建一个支付请求 //设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(return_url);
alipayRequest.setNotifyUrl(notify_url);
//商户订单号,商户网站订单系统中唯一订单号,必填
String out_trade_no = vo.getOut_trade_no();
//付款金额,必填
String total_amount = vo.getTotal_amount();
//订单名称,必填
String subject = vo.getSubject();
//商品描述,可空
String body = vo.getBody();
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
String result = alipayClient.pageExecute(alipayRequest).getBody();
//会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
System.out.println("支付宝的响应:"+result);
return result;
}
}
2)、提交信息封装VO
package com.atguigu.gulimall.order.vo;
import lombok.Data;
@Data
public class PayVo {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private String total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}
3)、因为加上了@ConfigurationProperties(prefix = "alipay")
,是一个配置类
我们可以在application.yaml 配置文件中编写相关的配置
# 支付宝相关的配置
alipay:
app-id: 2021000119667766
第三步、修改支付页的支付宝按钮
第三步、修改支付页的支付宝按钮
修改 gulimall-order 服务中的 pay.html 页面
<li>
<img src="/static/order/pay/img/zhifubao.png" style="weight:auto;height:30px;" alt="">
<a th:href="'http://order.gulimall.cn/payOrder?orderSn='+${submitOrderResp.order.orderSn}">支付宝</a>
</li>
第四步、订单支付与同步通知
第四步、订单支付与同步通知
1)、编写Controller层接口调用
gulimall-ordert 服务的 com.atguigu.gulimall.order.web
路径下的 PayWebController 类,映射/payOrder
package com.atguigu.gulimall.order.web;
@Controller
public class PayWebController {
@Autowired
AlipayTemplate alipayTemplate;
@Autowired
OrderService orderService;
/**
* 1、将支付页让浏览器展示
* 2、支付成功后,跳转到用户的订单列表项
* @param orderSn
* @return
* @throws AlipayApiException
*/
@ResponseBody
@GetMapping(value = "/payOrder",produces = "text/html")
public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
PayVo payVo = orderService.getOrderPay(orderSn);
// 返回的是一个页面。将此页面交给浏览器就行
String pay = alipayTemplate.pay(payVo);
System.out.println(pay);
return pay;
}
}
2)、Service层实现类 OrderServiceImpl.java 编写获取当前订单的支付信息 方法
/**
* 获取当前订单的支付信息
* @param orderSn
* @return
*/
@Override
public PayVo getOrderPay(String orderSn) {
PayVo payVo = new PayVo();
OrderEntity order = this.getOrderByOrderSn(orderSn);
BigDecimal decimal = order.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
payVo.setTotal_amount(decimal.toString());
payVo.setOut_trade_no(order.getOrderSn());
List<OrderItemEntity> order_sn = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
OrderItemEntity itemEntity = order_sn.get(0);
payVo.setSubject(itemEntity.getSkuName());
payVo.setBody(itemEntity.getSkuAttrsVals());
return payVo;
}
测试成功:
第五步、订单列表页渲染完成
第五步、订单列表页渲染完成
5.1、环境准备
-
首先将资料中 订单页 静态资源部署到服务器中,让页面放到gulimall-member服务中。
-
配置网关
- id: gulimall_member_route uri: lb://gulimall-member predicates: - Host=member.gulimall.cn
-
添加域名映射
# Gulimall Host Start 127.0.0.1 gulimall.cn 127.0.0.1 search.gulimall.cn 127.0.0.1 item.gulimall.cn 127.0.0.1 auth.gulimall.cn 127.0.0.1 cart.gulimall.cn 127.0.0.1 order.gulimall.cn 127.0.0.1 member.gulimall.cn "/etc/hosts"
-
整合SpringSession
-
导入依赖
<!-- 整合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> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
-
编写配置
spring: session: store-type: redis redis: host: 124.222.223.222
-
启动类加上注解
@EnableRedisHttpSession @EnableFeignClients(basePackages = "com.atguigu.gulimall.member.feign") @EnableDiscoveryClient @SpringBootApplication public class GulimallMemberApplication { public static void main(String[] args) { SpringApplication.run(GulimallMemberApplication.class, args); } }
-
-
配置拦截器(这里复制的同时一定要修改 放行:/member/member/**
-
用户登录拦截器
package com.atguigu.gulimall.member.interceptoe; import com.atguigu.common.constant.AuthServerConstant; import com.atguigu.common.vo.MemberRespVo; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Data time:2022/4/11 22:21 * StudentID:2019112118 * Author:hgw * Description: 用户登录拦截器 */ @Component public class LoginUserInterceptor implements HandlerInterceptor { public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>(); /** * 用户登录拦截器 * @param request * @param response * @param handler * @return * 用户登录:放行 * 用户未登录:跳转到登录页面 * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // /order/order/status/222222222 String uri = request.getRequestURI(); boolean match = new AntPathMatcher().match("/member/**", uri); if (match){ return true; } MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER); if (attribute!=null){ loginUser.set(attribute); return true; } else { // 没登录就去登录 request.getSession().setAttribute("msg", "请先进行登录"); response.sendRedirect("http://auth.gulimall.cn/login.html"); return false; } } }
-
编写Web配置类,指定用户登录拦截器
package com.atguigu.gulimall.member.config; import com.atguigu.gulimall.member.interceptoe.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; /** * Data time:2022/4/15 17:03 * StudentID:2019112118 * Author:hgw * Description: Web配置 */ @Configuration public class MemberWebConfig implements WebMvcConfigurer { @Autowired LoginUserInterceptor loginUserInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**"); } }
-
5.2、接口编写
第一步、gulimall-order 服务中编写 分页查询当前登录用户的所有订单 接口方法
1)、在Controller层编写
gulimall-order 服务中/src/main/java/com/atguigu/gulimall/order/controller
OrderController.java
package com.atguigu.gulimall.order.controller;
@RestController
@RequestMapping("order/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 分页查询当前登录用户的所有订单
*/
@PostMapping("/listWithItem")
public R listWithItem(@RequestBody Map<String, Object> params){
PageUtils page = orderService.queryPageWithItem(params);
return R.ok().put("page", page);
}
2)、Service层 OrderServiceImpl.java实现类方法编写:
gulimall-order 服务中 com/atguigu/gulimall/order/service/impl
OrderServiceImpl.java
@Override
public PageUtils queryPageWithItem(Map<String, Object> params) {
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
IPage<OrderEntity> page = this.page(
new Query<OrderEntity>().getPage(params),
new QueryWrapper<OrderEntity>().eq("member_id", memberRespVo.getId()).orderByDesc("modify_time")
);
List<OrderEntity> order_sn = page.getRecords().stream().map(order -> {
List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", order.getOrderSn()));
order.setItemEntities(itemEntities);
return order;
}).collect(Collectors.toList());
page.setRecords(order_sn);
return new PageUtils(page);
}
3)、修改 OrderEntity.java 实体类,为其加上一个属性
gulimall-order 服务 com.atguigu.gulimall.order.entity
路径下的 OrderEntity类,添加以下属性:
@TableField(exist = false)
private List<OrderItemEntity> itemEntities;
第二步、在gulimall-member服务中调用 gulimall-order 服务接口
gulimall-member 服务的 com.atguigu.gulimall.member.feign
路径下的 OrderFeignService接口进行远程调用:
package com.atguigu.gulimall.member.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Map;
/**
* Data time:2022/4/15 20:46
* StudentID:2019112118
* Author:hgw
* Description:
*/
@FeignClient("gulimall-order")
public interface OrderFeignService {
@PostMapping("/order/order/listWithItem")
R listWithItem(@RequestBody Map<String, Object> params);
}
第三步、编写过滤器
package com.atguigu.gulimall.member.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* Data time:2022/4/12 11:20
* StudentID:2019112118
* Author:hgw
* Description:
*/
@Configuration
public class GulimallFeignConfig {
/**
* feign在远程调用之前会执行所有的RequestInterceptor拦截器
* @return
*/
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
// 1、使用 RequestContextHolder 拿到请求数据,RequestContextHolder底层使用过线程共享数据 ThreadLocal<RequestAttributes>
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes!=null){
HttpServletRequest request = attributes.getRequest();
// 2、同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
// 给新请求同步了老请求的cookie
requestTemplate.header("Cookie",cookie);
}
}
};
}
}
第四步、Controller 层 MemberWebController 编写
package com.atguigu.gulimall.member.web;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.member.feign.OrderFeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.HashMap;
import java.util.Map;
/**
* Data time:2022/4/15 17:00
* StudentID:2019112118
* Author:hgw
* Description:
*/
@Controller
public class MemberWebController {
@Autowired
OrderFeignService orderFeignService;
@GetMapping("/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum,
Model model){
// 查处当前登录的用户的所有订单列表数据
Map<String,Object> page = new HashMap<>();
page.put("page",pageNum.toString());
R r = orderFeignService.listWithItem(page);
System.out.println(JSON.toJSONString(r));
model.addAttribute("orders",r);
return "orderList";
}
}
5.3、前端页面接收渲染
修改 orderList.html 页面的部分内容,渲染页面
<table class="table" th:each="order:${orders.page.list}">
<tr>
<td colspan="7" style="background:#F7F7F7">
<span style="color:#AAAAAA">2017-12-09 20:50:10</span>
<span><ruby style="color:#AAAAAA">订单号:</ruby> [[${order.orderSn}]]</span>
<span>谷粒商城<i class="table_i"></i></span>
<i class="table_i5 isShow"></i>
</td>
</tr>
<tr class="tr" th:each="item,itemStat:${order.itemEntities}">
<td colspan="3">
<img style="height: 60px; width: 60px;" th:src="${item.skuPic}" alt="" class="img">
<div>
<p style="width: 242px; height: auto; overflow: auto">
[[${item.skuName}]]
</p>
<div><i class="table_i4"></i>找搭配</div>
</div>
<div style="margin-left:15px;">x[[${item.skuQuantity}]]</div>
<div style="clear:both"></div>
</td>
<td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}">[[${order.receiverName}]]<i><i class="table_i1"></i></i></td>
<td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}" style="padding-left:10px;color:#AAAAB1;">
<p style="margin-bottom:5px;">总额 ¥[[${order.payAmount}]]</p>
<hr style="width:90%;">
<p>在线支付</p>
</td>
<td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}">
<ul>
<li style="color:#71B247;" th:if="${order.status==0}">待付款</li>
<li style="color:#71B247;" th:if="${order.status==1}">已付款</li>
<li style="color:#71B247;" th:if="${order.status==2}">已发货</li>
<li style="color:#71B247;" th:if="${order.status==3}">已完成</li>
<li style="color:#71B247;" th:if="${order.status==4}">已取消</li>
<li style="color:#71B247;" th:if="${order.status==5}">售后中</li>
<li style="color:#71B247;" th:if="${order.status==6}">售后完成</li>
<li style="margin:4px 0;" class="hide"><i class="table_i2"></i>跟踪<i class="table_i3"></i>
<div class="hi">
<div class="p-tit">
普通快递 运单号:390085324974
</div>
<div class="hideList">
<ul>
<li>
[北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
的快件已签收,感谢您使用韵达快递)签收
</li>
<li>
[北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
的快件已签收,感谢您使用韵达快递)签收
</li>
<li>
[北京昌平区南口公司] 在北京昌平区南口公司进行派件扫描
</li>
<li>
[北京市] 在北京昌平区南口公司进行派件扫描;派送业务员:业务员;联系电话:17319268636
</li>
</ul>
</div>
</div>
</li>
<li class="tdLi">订单详情</li>
</ul>
</td>
<td>
<button>确认收货</button>
<p style="margin:4px 0; ">取消订单</p>
<p>催单</p>
</td>
</tr>
</table>
第六步、异步通知内网穿透环境搭建
- 订单支付成功后支付宝会回调商户接口,这个时候需要修改订单状态
- 由于同步跳转可能由于网络问题失败,所以使用异步通知
- 支付宝使用的是最大努力通知方案,保障数据一致性,隔一段时间会通知商户支付成功,直到返回
success
1)、建立内网穿透
2)内网穿透设置异步通知地址
-
将外网映射到本地的
order.gulimall.cn:80
-
由于回调的请求头不是
order.gulimall.cn
,因此nginx转发到网关后找不到对应的服务,所以需要对nginx进行设置将
/payed/notify
异步通知转发至订单服务
设置异步通知的地址:
3)、内网穿透联调
通过工具进行内网穿透,第三方并不是从浏览器发送过来请求,即使是从浏览中发送过来,那host也不对,故我们需要修改nginx的配置,来监听 /payed/notify
请求,设置默认的host
服务器的 mydata/nginx/conf/conf.d
目录下
hgw@HGWdeAir conf.d % vim gulimall.conf
server {
listen 80;
server_name gulimall.cn *.gulimall.cn mvaophzk6b.51xd.pub;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location /static/ {
root /usr/share/nginx/html;
}
location /payed/ {
proxy_set_header Host order.gulimall.cn;
proxy_pass http://gulimall;
}
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
4)、编写登录拦截器方法,放行
/payed/notify
修改gulimall-order服务 com.atguigu.gulimall.order.interceptoe
路径的 LoginUserInterceptor 类,代码如下:
package com.atguigu.gulimall.order.interceptoe;
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// /order/order/status/222222222
String uri = request.getRequestURI();
AntPathMatcher matcher = new AntPathMatcher();
boolean match = matcher.match("/order/order/status/**", uri);
boolean match1 = matcher.match("/payed/notify", uri);
if (match || match1 ){
return true;
}
MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute!=null){
loginUser.set(attribute);
return true;
} else {
// 没登录就去登录
request.getSession().setAttribute("msg", "请先进行登录");
response.sendRedirect("http://auth.gulimall.cn/login.html");
return false;
}
}
}
进行测试接口:测试成功
第七步、验证签名,支付成功
- 验证签名
- 验签通过,即是支付宝发过来的数据,处理支付结果
- 保存交易流水 oms_payment_info
- 修改订单的状态信息 oms_order
- 返回"success"
- 验签通过,即不是支付宝发送过来的数据
- 返回非 “success”,即可
- 验签通过,即是支付宝发过来的数据,处理支付结果
1、主体代码,Controller层接口编写
主体代码,Controller层接口编写
package com.atguigu.gulimall.order.listener;
import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import com.atguigu.gulimall.order.config.AlipayTemplate;
import com.atguigu.gulimall.order.service.OrderService;
import com.atguigu.gulimall.order.vo.PayAsyncVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* Data time:2022/4/15 21:54
* StudentID:2019112118
* Author:hgw
* Description: 接收支付宝的异步通知
*/
@RestController
public class OrderPayedListener {
@Autowired
OrderService orderService;
@Autowired
AlipayTemplate alipayTemplate;
@PostMapping("/payed/notify")
public String handleAliPayed(PayAsyncVo vo,HttpServletRequest request) throws AlipayApiException {
// 只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success,支付宝就再也不通知
// 验签
Map<String, String> params = new HashMap<>();
Map<String, String[]> requestParams = request.getParameterMap();
for (String name : requestParams.keySet()) {
String[] values = requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名
if (signVerified) {
System.out.println("签名验证成功....");
String result = orderService.handlePayRequest(vo);
return result;
} else {
System.out.println("签名验证失败....");
return "error";
}
}
}
2、处理支付结果
1、编写一个实体类Vo 用来映射支付宝异步通知回来的数据
gulimall-order 服务的 com.atguigu.gulimall.order.vo
路径下的 PayAsyncVo 类
package com.atguigu.gulimall.order.vo;
import lombok.Data;
import lombok.ToString;
@ToString
@Data
public class PayAsyncVo {
private String gmt_create;
private String charset;
private String gmt_payment;
private String notify_time;
private String subject;
private String sign;
private String buyer_id;//支付者的id
private String body;//订单的信息
private String invoice_amount;//支付金额
private String version;
private String notify_id;//通知id
private String fund_bill_list;
private String notify_type;//通知类型; trade_status_sync
private String out_trade_no;//订单号
private String total_amount;//支付的总额
private String trade_status;//交易状态 TRADE_SUCCESS
private String trade_no;//流水号
private String auth_app_id;//
private String receipt_amount;//商家收到的款
private String point_amount;//
private String app_id;//应用id
private String buyer_pay_amount;//最终支付的金额
private String sign_type;//签名类型
private String seller_id;//商家的id
}
2)、设置 表oms_payment_info 的索引
因为一个订单对应一个流水号,所以我们给订单号和支付流水号加上两个唯一索引:
并修改 order_sn 属性的长度为 64位
3)、Service 层实现类 OrderServiceImpl.java 类编写 处理支付宝的支付结果 方法
gulimall-order 服务的 com/atguigu/gulimall/order/service/impl/OrderServiceImpl.java
路径下的 OrderServiceImpl.java
/**
* 处理支付宝的支付结果
* @param vo
* @return
*/
@Override
public String handlePayRequest(PayAsyncVo vo) {
// 1、保存交易流水 oms_payment_info
PaymentInfoEntity infoEntity = new PaymentInfoEntity();
infoEntity.setAlipayTradeNo(vo.getTrade_no());
infoEntity.setOrderSn(vo.getOut_trade_no());
infoEntity.setPaymentStatus(vo.getTrade_status());
infoEntity.setCallbackTime(vo.getNotify_time());
paymentInfoService.save(infoEntity);
// 2、修改订单的状态信息 oms_order
// 判断支付是否成功:支付宝返回 TRADE_SUCCESS、TRADE_FINISHED 都表示成功
if (vo.getTrade_status().equals("TRADE_SUCCESS") || vo.getTrade_status().equals("TRADE_FINISHED")) {
// 支付成功状态,则修改订单的状态
String outTradeNo = vo.getOut_trade_no();
this.baseMapper.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());
}
return "success";
}
4)、修改订单的状态信息方法编写 oms_order
gulimall-order 服务的 com.atguigu.gulimall.order.dao
路径下的 OrderDao
package com.atguigu.gulimall.order.dao;
@Mapper
public interface OrderDao extends BaseMapper<OrderEntity> {
void updateOrderStatus(@Param("outTradeNo") String outTradeNo, @Param("code") Integer code);
}
gulimall-order 服务的 gulimall-order/src/main/resources/mapper/order/OrderDao.xml
<update id="updateOrderStatus">
UPDATE oms_order SET `status`=#{code} WHERE order_sn=#{outTradeNo};
</update>
第八步、关单处理
- 由于买家的特殊原因,没能在订单过期前完成支付,等到订单状态过期了才支付,这时库存已进行解库存,并将订单状态改为已支付
- 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
- 由于延时等问题。订单解锁完成,正在解锁库存的时候,异步通知才到
- 订单解锁,手动调用收单
- 网络阻塞问题,订单支付成功的异步通知一直不到达
- 查询订单列表时,Ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝此订单的状态。
- 其他各种问题
- 每天晚上闲时下载支付宝对账单——进行对账