赞
踩
在服务器的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/
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
- id: gulimall_order_route
uri: lb://gulimall-order
predicates:
- Host=order.gulimall.cn
已导入依赖
主启动类加上 @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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
spring:
thymeleaf:
cache: false
**第一步、**导入依赖
<!--属性配置的提示工具-->
<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;
}
第四步、页面调整
订单生成 -> 支付订单 -> 卖家发货 -> 确认收货 -> 交易成功
需求:去结算、查看订单必须是登录用户之后的,这里编写一个拦截器。
- 用户登录 放行
- 用户未登录:跳转到登录页面
Gulimall-order服务中com.atguigu.gulimall.order.web
路径下
package com.atguigu.gulimall.order.web;
@Controller
public class OrderWebController {
@GetMapping("/toTrade")
public String toTrade(){
return "confirm";
}
}
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;
}
}
}
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("/**");
}
}
订单确认页需要用的数据
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;
}
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层实现类方法编写
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;
}
}
编写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);
}
第一步、编写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();
}
在 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);
}
}
};
}
}
此时:查询购物项、库存和收货地址都要调用远程服务,串行会浪费大量时间,因此我们进行异步编排优化
ThreadLocal<RequestAttributes>
,我们知道线程共享数据的域是 当前线程下,线程之间是不共享的。所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie
了。修改 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;
}
修改 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>
需求:有货、无货
在远程查询购物车所有选中的购物项之后进行 批量查询库存
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 库存服务中提供 查询库存的接口
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);
}
//....
}
com.atguigu.gulimall.order.feign
路径下:WareFeignServicepackage 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>
需求:选择收货地址,计算物流费
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1paZHi3c-1650101180086)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220412183318234.png)]
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);
})
}
2、gulimall-ware仓储服务编写 根据用户地址,返回详细地址并计算物流费h
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);
}
//...
}
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;
}
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);
}
com.atguigu.gulimall.ware.vo
路径下的 Vo@Data
public class FareVo {
private MemberAddressVo addressVo;
private BigDecimal fare;
}
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的。
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";
}
}
@Data
@ToString
public class OrderSubmitVo {
/**
* 收货地址Id
*/
private Long addrId;
/**
* 支付方式
*/
private Integer payType;
// 无需提交需要购买的商品,去购物车再获取一遍
// 优惠发票
/**
* 防重令牌
*/
private String orderToken;
/**
* 应付价格,验价
*/
private BigDecimal payPrice;
/**
* 订单备注
*/
private String note;
/**
* 用户相关信息,直接去Session取出登录的用户
*/
}
<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);
})
}
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;
}
}
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;
}
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;
}
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信息接口
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);
}
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;
}
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);
}
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;
}
/**
* 计算价格
* @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 未删除
}
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;
}
}
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 (仓储服务)中编写 锁定库存 的接口
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());
}
}
//....
}
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;
}
}
<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>
<p class="p1">填写并核对订单信息 <span style="color: red" th:value="${msg!=null}" th:text="${msg}"></span></p>
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 未删除
}
}
问题:
SpringBoot事务的坑:
在同一个类里面,编写两个方法,内部调用的的时候,会导致事务设置失效。原因没有用到代理对象的缘故。
spring-boot-starter-aop
,(帮我们引入了aspectj)cap定理
CAP 原则又称 CAP 定理,指的是在一个分布式系统中
CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾
BASE理论
是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可以适当的采取若一致性,即 最终一致性。
BASE 是指:
从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。
* 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概述:
我们只需要使用一个 @GlobalTransactional
注解在业务方法上:
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
......
}
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"
}
hgw@HGWdeMacBook-Air bin # sh seata-server.sh
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延时队列的目的是为了解决事务最终一致性。
死信队列&死信交换器:,称之为死信交换器,当消息变成一个死信之后,如果这个消息所在的队列存在x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换器上,这个交换器就称之为死信交换器,与这个死信交换器绑定的队列就是死信队列。
死信消息:
过期消息:
在 rabbitmq 中存在2种方可设置消息的过期时间,
如果同时使用这2种方法,那么以过期时间小的那个数值为准。当消息达到过期时间还没有被消费,那么那个消息就成为了一个 死信 消息。
**延时队列:**在rabbitmq中不存在延时队列,但是我们可以通过设置消息的过期时间和死信队列来模拟出延时队列。消费者监听死信交换器绑定的队列,而不要监听消息发送的队列。
Order-event-exchange 交换机 绑定了两个队列
**第一步、**创建相应的交换机、队列、以及交换机和队列的绑定 并 编写一个队列监听
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";
}
}
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);
}
}
创建相应的交换机、队列、以及交换机和队列的绑定
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<>());
}
}
库存解锁的场景
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nuTf7P7D-1650102697212)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220414195547133.png)]
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>
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 类,代码如下
@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
}
库存自动解锁
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;
}
}
}
1)、创建一个类监听 stock.release.stock.queue
队列
gulimall-ware服务的 com.atguigu.gulimall.ware.listener
路径 StockReleaseListener 类,接收到消息之后调用 Service层 WareSkuServiceImpl.java 实现类的 unlockStock 方法实现解锁库存:
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);
}
order.create.order
order.release.order
order.release.order.queue
队列,进行释放订单服务order.release.other
stock.release.stock.queue
,编写一个重载方法,进行判断
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);
}
}
为了防止因为其他原因,订单的关闭延期了
/**
* 订单的关闭
* @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);
}
}
order.release.order.queue
队列,释放订单服务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);
}
}
}
/**
* 订单的关闭
* @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);
}
}
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;
}
柔性事务-可靠消息+最终一致性方案(异步确保型)
防止消息丢失
* 1、做好消息确认机制(pulisher、consumer[手动ACK])
* 2、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
此时:
获取支付宝的沙箱环境中商户的公钥匙、私钥、以及支付宝的公钥
续断:https://www.zhexi.tech/
第一步:登录
第三步:登录哲西云控制台,到“客户端”菜单,确认客户端已上线;
第四步、新建隧道
后面操作的时候会说
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v6n7UuOx-1650103370483)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220415140932328.png)]
续断:https://www.zhexi.tech/
第一步:登录
第三步:登录哲西云控制台,到“客户端”菜单,确认客户端已上线;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qziWABOV-1650103370483)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220415141404548.png)]
第四步、新建隧道
第一步:导入依赖
<!--阿里支付模块-->
<!-- 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;
}
测试成功:
第五步、订单列表页渲染完成
首先将资料中 订单页 静态资源部署到服务器中,让页面放到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("/**");
}
}
第一步、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";
}
}
修改 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;
}
}
}
进行测试接口:测试成功
主体代码,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";
}
}
}
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>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。