等待付款 --------->detail
订单页 --------->list
结算页 --------->confirm
收银页 ---------> pay
# gulimall
192.168.157.128 gulimall.com
# search
192.168.157.128 search.gulimall.com
# item 商品详情
192.168.157.128 item.gulimall.com
#商城认证
192.168.157.128 auth.gulimall.com
#购物车
192.168.157.128 cart.gulimall.com
#订单
192.168.157.128 order.gulimall.com
#单点登录
127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
gulimall-gateway/src/main/resources/application.yml
#订单
- id: gulimall_order_route
uri: lb://gulimall-order
predicates:
- Host=order.gulimall.com
@EnableDiscoveryClient
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
src=" ===>src="/static/order/xxx/
herf=" ===>herf="/static/order/xxx/
确认页前端代码:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/confirm.html
order.gulimall.com/confirm.html
订单列表页前端代码:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/list.html
谷粒商城订单 (gulimall.com)
订单详情页前端代码:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/detail.html
order.gulimall.com/detail.html
订单支付页前端代码:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/pay.html
order.gulimall.com/pay.html
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
@EnableRedisHttpSession //整合Redis作为session存储
redis:
host: 192.168.157.128
session:
store-type: redis
package site.zhourui.gulimall.order.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* @author zr
* @date 2021/12/12 10:29
*/
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//放大作用域
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
cookieSerializer.setCookieMaxAge(60*60*24*7);
return cookieSerializer;
}
//session存储对象方式json,默认jdk
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
可以实现登录成功后用户信息共享
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/MyThreadConfig.java
package site.zhourui.gulimall.order.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author zr
* @date 2021/11/28 10:12
*/
@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()
);
}
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/ThreadPoolConfigProperties.java
package site.zhourui.gulimall.order.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
gulimall:
thread:
core-size: 20
max-size: 200
keep-alive-time: 10
电商系统涉及到 3 流, 分别时信息流, 资金流, 物流, 而订单系统作为中枢将三者有机的集合起来。订单模块是电商系统的枢纽, 在订单这个环节上需求获取多个模块的数据和信息, 同时对这些信息进行加工处理后流向下个环节, 这一系列就构成了订单的信息流通。
用户信息包括用户账号、 用户等级、 用户的收货地址、 收货人、 收货人电话等组成, 用户账户需要绑定手机号码, 但是用户绑定的手机号码不一定是收货信息上的电话。 用户可以添加多个收货信息, 用户等级信息可以用来和促销系统进行匹配, 获取商品折扣, 同时用户等级还可以获取积分的奖励等
订单基础信息是订单流转的核心, 其包括订单类型、 父/子订单、 订单编号、 订单状态、 订单流转的时间等。
(1) 订单类型包括实体商品订单和虚拟订单商品等, 这个根据商城商品和服务类型进行区分。
(2) 同时订单都需要做父子订单处理, 之前在初创公司一直只有一个订单, 没有做父子订单处理后期需要进行拆单的时候就比较麻烦, 尤其是多商户商场, 和不同仓库商品的时候,父子订单就是为后期做拆单准备的。
(3) 订单编号不多说了, 需要强调的一点是父子订单都需要有订单编号, 需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。
(4) 订单状态记录订单每次流转过程, 后面会对订单状态进行单独的说明。
(5) 订单流转时间需要记录下单时间, 支付时间, 发货时间, 结束时间/关闭时间等等
商品信息从商品库中获取商品的 SKU 信息、 图片、 名称、 属性规格、 商品单价、 商户信息等, 从用户下单行为记录的用户下单数量, 商品合计价格等。
优惠信息记录用户参与的优惠活动, 包括优惠促销活动, 比如满减、 满赠、 秒杀等, 用户使用的优惠券信息, 优惠券满足条件的优惠券需要默认展示出来, 具体方式已在之前的优惠券篇章做过详细介绍, 另外还虚拟币抵扣信息等进行记录。
因为优惠信息只是记录用户使用的条目, 而支付信息需要加入数据进行计算, 所以做为区分。
( 1) 支付流水单号, 这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号, 财务通过订单号和流水单号与支付通道进行对账使用。
( 2) 支付方式用户使用的支付方式, 比如微信支付、 支付宝支付、 钱包支付、 快捷支付等。支付方式有时候可能有两个——余额支付+第三方支付。
( 3) 商品总金额, 每个商品加总后的金额; 运费, 物流产生的费用; 优惠总金额, 包括促销活动的优惠金额, 优惠券优惠金额, 虚拟积分或者虚拟币抵扣的金额, 会员折扣的金额等之和; 实付金额, 用户实际需要付款的金额。用户实付金额=商品总金额+运费-优惠总金额
物流信息包括配送方式, 物流公司, 物流单号, 物流状态, 物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。
订单流程是指从订单产生到完成整个流转的过程, 从而行程了一套标准流程规则。 而不同的产品类型或业务类型在系统中的流程会千差万别, 比如上面提到的线上实物订单和虚拟订单的流程, 线上实物订单与 O2O 订单等, 所以需要根据不同的类型进行构建订单流程。不管类型如何订单都包括正向流程和逆向流程, 对应的场景就是购买商品和退换货流程, 正向流程就是一个正常的网购步骤: 订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。而每个步骤的背后, 订单是如何在多系统之间交互流转的, 可概括如下图
因为订单系统必然涉及到用户信息,因此进入订单系统的请求必须是已经登录的,所以我们需要通过拦截器对未登录订单请求进行拦截
gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java
package site.zhourui.gulimall.order.interceptor;
/**
* @author zr
* @date 2021/12/21 22:04
*/
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.vo.MemberResponseVo;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;
import static site.zhourui.common.constant.AuthServerConstant.LOGIN_USER;
/**
* 登录拦截器
* 从session中获取了登录信息(redis中),封装到了ThreadLocal中
*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (memberResponseVo != null) {
loginUser.set(memberResponseVo);
return true;
}else {
session.setAttribute("msg","请先登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/OrderWebConfig.java
package site.zhourui.gulimall.order.config;
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;
import site.zhourui.gulimall.order.interceptor.LoginUserInterceptor;
/**
* @author zr
* @date 2021/12/21 22:05
*/
@Configuration
public class OrderWebConfig implements WebMvcConfigurer {
@Autowired
private LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
确认页提交数据
gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/OrderConfirmVo.java
package site.zhourui.gulimall.order.vo;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 订单确认页需要用的数据
* @author zr
* @date 2021/12/21 22:22
*/
public class OrderConfirmVo {
@Getter
@Setter
List<MemberAddressVo> memberAddressVos;/** 会员收获地址列表 **/
@Getter @Setter
List<OrderItemVo> items; /** 所有选中的购物项【购物车中的所有项】 **/
@Getter @Setter
private Integer integration;/** 优惠券(会员积分) **/
/** TODO 防止重复提交的令牌 幂等性**/
@Getter @Setter
private String orderToken;
@Getter @Setter
Map<Long,Boolean> stocks;
public Integer getCount() {
Integer count = 0;
if (items != null && items.size() > 0) {
for (OrderItemVo item : items) {
count += item.getCount();
}
}
return count;
}
/** 总商品金额 **/
//BigDecimal total;
//计算订单总额
public BigDecimal getTotal() {
BigDecimal totalNum = BigDecimal.ZERO;
if (items != null && items.size() > 0) {
for (OrderItemVo item : items) {
//计算当前商品的总价格
BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
//再计算全部商品的总价格
totalNum = totalNum.add(itemPrice);
}
}
return totalNum;
}
/** 应付总额 **/
//BigDecimal payPrice;
public BigDecimal getPayPrice() {
return getTotal();
}
}
确认页提交数据模型还需要地址信息
gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/MemberAddressVo.java
package site.zhourui.gulimall.order.vo;
import lombok.Data;
/**
* 地址信息
* @author zr
* @date 2021/12/21 22:24
*/
@Data
public class MemberAddressVo {
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 收货人姓名
*/
private String name;
/**
* 电话
*/
private String phone;
/**
* 邮政编码
*/
private String postCode;
/**
* 省份/直辖市
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 区
*/
private String region;
/**
* 详细地址(街道)
*/
private String detailAddress;
/**
* 省市区代码
*/
private String areacode;
/**
* 是否默认
*/
private Integer defaultStatus;
}
确认页提交数据模型还需要订单行信息
gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/OrderItemVo.java
package site.zhourui.gulimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 购物项内容
* @author zr
* @date 2021/12/21 22:23
*/
@Data
public class OrderItemVo {
private Long skuId; // skuId
private Boolean check = true; // 是否选中
private String title; // 标题
private String image; // 图片
private List<String> skuAttrValues;// 商品销售属性
private BigDecimal price; // 单价
private Integer count; // 当前商品数量
private BigDecimal totalPrice; // 总价
private BigDecimal weight = new BigDecimal("0.085");// 商品重量
}
1、远程调用:获取所有收货地址【member-ums表】
2、远程调用:所有选中的商品(最新价格-远程调用)【cart-redis中】【product-查询最新价格】
3、查询用户积分【session的用户信息中】
4、订单总额【根据所有选中的价格之和 求得】
5、应付总额【暂时跟订单总额相等】【优惠卡等功能不做,直接用积分】
6、查询每个商品是否有货【批量查询ware服务】
7、收货地址高亮【选中地址调用ajax直接远程调用ware计算运费【远程调用会员服务member传入addrId获取详细地址】
WareInfoController /fare
接口返回运费信息,和地址信息
8、防重令牌【防止用户多次 提交订单】【点击提交订单后,数据库只保存一条订单信息(幂等性,提交1次和多次结果是一致的)】
返回订单确认页所需要的数据OrderConfirmVo
gulimall-order/src/main/java/site/zhourui/gulimall/order/web/OrderWebController.java
/**
* 去结算确认页
* @param model
* @param request
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
@GetMapping(value = "/toTrade")
public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = orderService.confirmOrder();
model.addAttribute("confirmOrderData",confirmVo);
//展示订单确认的数据
return "confirm";
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java
OrderConfirmVo confirmOrder();
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
- 判断用户登录信息
- 远程查询所有的收获地址列表
- 远程查询购物车所有选中的购物项
- 远程查询商品库存信息
- 查询用户积分
- 价格数据自动计算
- 防重令牌(防止表单重复提交)
/**
* 订单确认页返回需要用的数据
* @return
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//构建OrderConfirmVo
OrderConfirmVo confirmVo = new OrderConfirmVo();
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//开启第一个异步任务
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//1、远程查询所有的收获地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
//开启第二个异步任务
CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据【解决异步ThreadLocal 无法共享数据】
RequestContextHolder.setRequestAttributes(requestAttributes);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
confirmVo.setItems(currentCartItems);
//feign在远程调用之前要构造请求,调用很多的拦截器
}, threadPoolExecutor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItems();
//获取全部商品的id
List<Long> skuIds = items.stream()
.map((itemVo -> itemVo.getSkuId()))
.collect(Collectors.toList());
//3、远程查询商品库存信息
R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});
if (skuStockVos != null && skuStockVos.size() > 0) {
//将skuStockVos集合转换为map
Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(skuHasStockMap);
}
},threadPoolExecutor);
//4、、查询用户积分
Integer integration = memberResponseVo.getIntegration();
confirmVo.setIntegration(integration);
//5、、价格数据自动计算
//TODO 5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(addressFuture,cartInfoFuture).get();
return confirmVo;
}
模拟创建两条该用户地址信息
gulimall-member/src/main/java/site/zhourui/gulimall/member/service/MemberReceiveAddressService.java
List<MemberReceiveAddressEntity> getAddress(Long memberId);
gulimall-member/src/main/java/site/zhourui/gulimall/member/service/impl/MemberReceiveAddressServiceImpl.java
@Override
public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
List<MemberReceiveAddressEntity> addressList = this.baseMapper.selectList
(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
return addressList;
}
gulimall-member/src/main/java/site/zhourui/gulimall/member/controller/MemberReceiveAddressController.java
/**
* 根据会员id查询会员的所有地址
* @param memberId
* @return
*/
@GetMapping(value = "/{memberId}/address")
public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {
List<MemberReceiveAddressEntity> addressList = memberReceiveAddressService.getAddress(memberId);
return addressList;
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/MemberFeignService.java
package site.zhourui.gulimall.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.gulimall.order.vo.MemberAddressVo;
import java.util.List;
/**
* @author zr
* @date 2021/12/23 15:05
*/
@FeignClient("gulimall-member")
public interface MemberFeignService {
/**
* 查询当前用户的全部收货地址
* @param memberId
* @return
*/
@GetMapping(value = "/member/memberreceiveaddress/{memberId}/address")
List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
gulimall-cart/src/main/java/site/zhourui/gulimall/cart/service/CartService.java
/**
* 获取当前用户的购物车所有商品项
* @return
*/
List<CartItemVo> getUserCartItems();
gulimall-cart/src/main/java/site/zhourui/gulimall/cart/service/Impl/CartServiceImpl.java
/**
* 远程调用:订单服务调用【更新最新价格】
* 获取当前用户购物车所有选中的商品项check=true【从redis中取】
*/
@Override
public List<CartItemVo> getUserCartItems() {
List<CartItemVo> cartItemVoList = new ArrayList<>();
//获取当前用户登录的信息
UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
//如果用户未登录直接返回null
if (userInfoTo.getUserId() == null) {
return null;
} else {
//获取购物车项
String cartKey =CartConstant.CART_PREFIX + userInfoTo.getUserId();
//获取所有的
List<CartItemVo> cartItems = getCartItems(cartKey);
if (cartItems == null) {
throw new CartExceptionHandler();
}
//筛选出选中的
cartItemVoList = cartItems.stream()
.filter(items -> items.getCheck())
.map(item -> {
//更新为最新的价格(查询数据库)
// redis中的价格不是最新的
BigDecimal price = productFeignService.getPrice(item.getSkuId());
item.setPrice(price);
return item;
})
.collect(Collectors.toList());
}
return cartItemVoList;
}
gulimall-cart/src/main/java/site/zhourui/gulimall/cart/controller/CartController.java
/**
* 订单服务调用:【购物车页面点击确认订单时】
* 返回所有选中的商品项【从redis中取】
* 并且要获取最新的商品价格信息,而不是redis中的数据
*
* 获取当前用户的购物车所有商品项
*/
@GetMapping(value = "/currentUserCartItems")
@ResponseBody
public List<CartItemVo> getCurrentCartItems() {
List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
return cartItemVoList;
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/CartFeignService.java
package site.zhourui.gulimall.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import site.zhourui.gulimall.order.vo.OrderItemVo;
import java.util.List;
/**
* @author zr
* @date 2021/12/23 15:06
*/
@FeignClient("gulimall-cart")
public interface CartFeignService {
/**
* 查询当前用户购物车选中的商品项
* @return
*/
@GetMapping(value = "/currentUserCartItems")
List<OrderItemVo> getCurrentCartItems();
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/SkuStockVo.java
返回库存信息的vo
package site.zhourui.gulimall.order.vo;
import lombok.Data;
/**
* 库存vo
* @author zr
* @date 2021/12/23 15:53
*/
@Data
public class SkuStockVo {
private Long skuId;
private Boolean hasStock;
}
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java
/**
* 判断是否有库存
*/
List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds);
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java
/**
* 检查sku 是否有库存
*/
@Override
public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {
List<SkuHasStockVo> vos = skuIds.stream().map(skuId -> {
SkuHasStockVo vo = new SkuHasStockVo();
// 1、不止一个仓库有,多个仓库都有库存 sum
// 2、锁定库存是别人下单但是还没下完
Long count = baseMapper.getSkuStock(skuId);
vo.setSkuId(skuId);
vo.setHasStock(count == null ? false : count > 0);
return vo;
}).collect(Collectors.toList());
return vos;
}
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/controller/WareSkuController.java
/**
* 查询sku是否有库存
*/
@PostMapping("/hasstock")
// @RequiresPermissions("ware:waresku:list")
public R getSkusHasStock(@RequestBody List<Long> skuIds){
// sku_id stock
List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);
return R.ok().setData(vos);
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/WmsFeignService.java
package site.zhourui.gulimall.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import site.zhourui.common.utils.R;
import java.util.List;
/**
* @author zr
* @date 2021/12/23 15:06
*/
@FeignClient("gulimall-ware")
public interface WmsFeignService {
/**
* 查询sku是否有库存
*/
@PostMapping(value = "/ware/waresku/hasstock")
R getSkuHasStock(@RequestBody List<Long> skuIds);
}
之前在章节3
整合过线程池了,只需导入
@Autowired
private ThreadPoolExecutor threadPoolExecutor;
原因:feign发送请求时构造的RequestTemplate没有请求头(该请求头为空),请求参数等信息【cookie没了】
导致在cart服务中,拦截器拦截获取session中的登录信息,获取不到userId【没有cookie】
解决:同步新、老请求(老请求就是/toTrade请求,带有Cookie数据)的cookie
原理: feign在远程调用之前要构造请求,调用很多的拦截器(DEBUG,查看到会调用 拦截器)
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/GuliFeignConfig.java
package site.zhourui.gulimall.order.config;
/**
* @author zr
* @date 2021/12/23 17:03
*/
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* feign拦截器功能
* 解决feign 远程请求头丢失问题
**/
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
System.out.println("feign远程调用,拦截包装请求头");
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();//老请求
if (request != null) {
//2、同步请求头的数据(主要是cookie)
//把老请求的cookie值放到新请求上来,进行一个同步
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
}
导致拦截器中 空指针异常
1、先在主线程的ThreadLocal中获取 请求头数据
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
2、再在新线程给ThreadLocal设置 请求头数据【否则获取不到数据,不是同一个线程】
//每一个线程都来共享之前的请求数据【解决异步ThreadLocal 无法共享数据】
RequestContextHolder.setRequestAttributes(requestAttributes);
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareInfoService.java
/**
* 获取运费和收货地址信息
* @param addrId
* @return
*/
FareVo getFare(Long addrId);
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareInfoServiceImpl.java
模拟运费,真实情况下需要计算得出
/**
* 计算运费
* @param addrId
* @return
*/
@Override
public FareVo getFare(Long addrId) {
FareVo fareVo = new FareVo();
//收获地址的详细信息
R addrInfo = memberFeignService.info(addrId);
MemberAddressVo memberAddressVo = addrInfo.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {});
if (memberAddressVo != null) {
String phone = memberAddressVo.getPhone();
//截取用户手机号码最后一位作为我们的运费计算
//1558022051
String fare = phone.substring(phone.length() - 1);
BigDecimal bigDecimal = new BigDecimal(fare);
fareVo.setFare(bigDecimal);
fareVo.setAddress(memberAddressVo);
return fareVo;
}
return null;
}
需要获取用户选择的地址信息(远程调用)
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/feign/MemberFeignService.java
/**
* 根据id获取用户地址信息
* @param id
* @return
*/
@RequestMapping("/member/memberreceiveaddress/info/{id}")
R info(@PathVariable("id") Long id);
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/controller/WareInfoController.java
/**
* 获取运费信息,订单服务远程调用
* @return
*/
@GetMapping(value = "/fare")
public R getFare(@RequestParam("addrId") Long addrId) {
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().setData(fare);
}
前端页面当用户地址切换时,查询出运费及订单总金额为用户展示
测试需要创建两条地址信息数据
令牌前缀常量
package site.zhourui.gulimall.order.constant;
/**
* @author zr
* @date 2021/12/23 22:05
*/
public class OrderConstant {
public static final String USER_ORDER_TOKEN_PREFIX = "order:token";
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
//TODO 5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
注意:创建的订单号很长,注意将oms_order
与oms_order_item
数据库表中的order_sn
字段调大至50,否则会报错
gulimall-order/src/main/java/site/zhourui/gulimall/order/web/OrderWebController.java
@RequestMapping("/submitOrder")
public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes attributes) {
try{
SubmitOrderResponseVo responseVo=orderService.submitOrder(submitVo);
Integer code = responseVo.getCode();
if (code==0){
model.addAttribute("order", responseVo.getOrder());
return "pay";
}else {
String msg = "下单失败;";
switch (code) {
case 1:
msg += "防重令牌校验失败";
break;
case 2:
msg += "商品价格发生变化";
break;
}
attributes.addFlashAttribute("msg", msg);
return "redirect:http://order.gulimall.com/toTrade";
}
}catch (Exception e){
if (e instanceof NoStockException){
String msg = "下单失败,商品无库存";
attributes.addFlashAttribute("msg", msg);
}
return "redirect:http://order.gulimall.com/toTrade";
}
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 提交订单
* @param vo
* @return
*/
// @Transactional(isolation = Isolation.READ_COMMITTED) 设置事务的隔离级别
// @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播级别
// @GlobalTransactional(rollbackFor = Exception.class)
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//去创建、下订单、验令牌、验价格、锁定库存...
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
responseVo.setCode(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();
//通过lure脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
if (result == 0L) {
//令牌验证失败
responseVo.setCode(1);
return responseVo;
} else {
//令牌验证成功
//1、创建订单、订单项等信息
OrderCreateTo order = createOrder();
//2、验证价格
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//TODO 3、保存订单
saveOrder(order);
//4、库存锁定,只要有异常,回滚订单数据
//订单号、所有订单项信息(skuId,skuNum,skuName)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
//获取出要锁定的商品数据信息【order里面存储的是Entity】
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
lockVo.setLocks(orderItemVos);
//TODO 调用远程锁定库存的方法
//出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
//为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁定成功
responseVo.setOrder(order.getOrder());
//int i = 10/0;
//TODO 订单创建成功,发送消息给MQ
// rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
//删除购物车里的数据
// redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
return responseVo;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
// responseVo.setCode(3);
// return responseVo;
}
} else {
responseVo.setCode(2);
return responseVo;
}
}
}
//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();
//通过lure脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
if (result == 0L) {
//令牌验证失败
responseVo.setCode(1);
return responseVo;
} else {
//令牌验证成功
//1、创建订单、订单项等信息
//2、验证价格
//3、保存订单
//4、库存锁定,只要有异常,回滚订单数据
}
需要远程调用获取SPU信息
gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/ProductFeignService.java
package site.zhourui.gulimall.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.common.utils.R;
/**
* @author zr
* @date 2021/12/24 9:58
*/
@FeignClient("gulimall-product")
public interface ProductFeignService {
/**
* 根据skuId查询spu的信息
* @param skuId
* @return
*/
@GetMapping(value = "/product/spuinfo/skuId/{skuId}")
R getSpuInfoBySkuId(@PathVariable("skuId") Long skuId);
}
gulimall-product/src/main/java/site/zhourui/gulimall/product/app/SpuInfoController.java
/**
* 提交订单,远程接口
* 根据skuId查询spu的信息
*/
@GetMapping(value = "/skuId/{skuId}")
public R getSpuInfoBySkuId(@PathVariable("skuId") Long skuId) {
SpuInfoEntity spuInfoEntity = spuInfoService.getSpuInfoBySkuId(skuId);
return R.ok().setData(spuInfoEntity);
}
gulimall-product/src/main/java/site/zhourui/gulimall/product/service/SpuInfoService.java
/**
* 根据skuId查询spu的信息
* @param skuId
* @return
*/
SpuInfoEntity getSpuInfoBySkuId(Long skuId);
gulimall-product/src/main/java/site/zhourui/gulimall/product/service/impl/SpuInfoServiceImpl.java
/**
* 根据skuId查询spu的信息
* @param skuId
* @return
*/
@Override
public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
//先查询sku表里的数据
SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
//获得spuId
Long spuId = skuInfoEntity.getSpuId();
//再通过spuId查询spuInfo信息表里的数据
SpuInfoEntity spuInfoEntity = baseMapper.selectById(spuId);
//查询品牌表的数据获取品牌名
BrandEntity brandEntity = brandService.getById(spuInfoEntity.getBrandId());
spuInfoEntity.setBrandName(brandEntity.getName());
return spuInfoEntity;
}
gulimall-product/src/main/java/site/zhourui/gulimall/product/entity/SpuInfoEntity.java
需要加上品牌名称
/**
* 品牌名
*/
@TableField(exist = false)
private String brandName;
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 创建订单
*/
private OrderCreateTo createOrder() {
OrderCreateTo createTo = new OrderCreateTo();
//1、生成订单号
String orderSn = IdWorker.getTimeId();
// 构建订单数据【封装价格】
OrderEntity orderEntity = builderOrder(orderSn);
//2、获取到所有的订单项【封装价格】
List<OrderItemEntity> orderItemEntities = builderOrderItems(orderSn);
//3、验价(计算价格、积分等信息)
computePrice(orderEntity, orderItemEntities);
createTo.setOrder(orderEntity);
createTo.setOrderItems(orderItemEntities);
return createTo;
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 构建订单数据
* @param orderSn
* @return
*/
private OrderEntity builderOrder(String orderSn) {
//获取当前用户登录信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
OrderEntity orderEntity = new OrderEntity();
orderEntity.setMemberId(memberResponseVo.getId());
orderEntity.setOrderSn(orderSn);
orderEntity.setMemberUsername(memberResponseVo.getUsername());
OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
//远程获取收货地址和运费信息
R fareAddressVo = wmsFeignService.getFare(orderSubmitVo.getAddrId());
FareVo fareResp = fareAddressVo.getData("data", new TypeReference<FareVo>() {});
//获取到运费信息
BigDecimal fare = fareResp.getFare();
orderEntity.setFreightAmount(fare);
//获取到收货地址信息
MemberAddressVo address = fareResp.getAddress();
//设置收货人信息
orderEntity.setReceiverName(address.getName());
orderEntity.setReceiverPhone(address.getPhone());
orderEntity.setReceiverPostCode(address.getPostCode());
orderEntity.setReceiverProvince(address.getProvince());
orderEntity.setReceiverCity(address.getCity());
orderEntity.setReceiverRegion(address.getRegion());
orderEntity.setReceiverDetailAddress(address.getDetailAddress());
//设置订单相关的状态信息
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
orderEntity.setAutoConfirmDay(7);
orderEntity.setConfirmStatus(0);
return orderEntity;
}
订单状态枚举
gulimall-order/src/main/java/site/zhourui/gulimall/order/enume/OrderStatusEnum.java
package site.zhourui.gulimall.order.enume;
/**
* @author zr
* @date 2021/12/24 9:52
*/
/**
* 订单状态枚举
*/
public enum OrderStatusEnum {
CREATE_NEW(0,"待付款"),
PAYED(1,"已付款"),
SENDED(2,"已发货"),
RECIEVED(3,"已完成"),
CANCLED(4,"已取消"),
SERVICING(5,"售后中"),
SERVICED(6,"售后完成");
private Integer code;
private String msg;
OrderStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 构建所有订单项数据
* @return
*/
public List<OrderItemEntity> builderOrderItems(String orderSn) {
List<OrderItemEntity> orderItemEntityList = new ArrayList<>();
//最后确定每个购物项的价格
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
if (currentCartItems != null && currentCartItems.size() > 0) {
orderItemEntityList = currentCartItems.stream().map((items) -> {
//构建订单项数据
OrderItemEntity orderItemEntity = builderOrderItem(items);
orderItemEntity.setOrderSn(orderSn);
return orderItemEntity;
}).collect(Collectors.toList());
}
return orderItemEntityList;
}
将页面提交的价格和后台计算的价格进行对比,若不同则提示用户商品价格发生变化
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 计算价格的方法
* @param orderEntity
* @param orderItemEntities
*/
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> orderItemEntities) {
//总价
BigDecimal total = new BigDecimal("0.0");
//优惠价
BigDecimal coupon = new BigDecimal("0.0");
BigDecimal intergration = new BigDecimal("0.0");
BigDecimal promotion = new BigDecimal("0.0");
//积分、成长值
Integer integrationTotal = 0;
Integer growthTotal = 0;
//订单总额,叠加每一个订单项的总额信息
for (OrderItemEntity orderItem : orderItemEntities) {
//优惠价格信息
coupon = coupon.add(orderItem.getCouponAmount());
promotion = promotion.add(orderItem.getPromotionAmount());
intergration = intergration.add(orderItem.getIntegrationAmount());
//总价
total = total.add(orderItem.getRealAmount());
//积分信息和成长值信息
integrationTotal += orderItem.getGiftIntegration();
growthTotal += orderItem.getGiftGrowth();
}
//1、订单价格相关的
orderEntity.setTotalAmount(total);
//设置应付总额(总额+运费)
orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
orderEntity.setCouponAmount(coupon);
orderEntity.setPromotionAmount(promotion);
orderEntity.setIntegrationAmount(intergration);
//设置积分成长值信息
orderEntity.setIntegration(integrationTotal);
orderEntity.setGrowth(growthTotal);
//设置删除状态(0-未删除,1-已删除)
orderEntity.setDeleteStatus(0);
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 保存订单所有数据
*/
private void saveOrder(OrderCreateTo orderCreateTo) {
//获取订单信息
OrderEntity order = orderCreateTo.getOrder();
order.setModifyTime(new Date());
order.setCreateTime(new Date());
//保存订单
this.baseMapper.insert(order);
//获取订单项信息
List<OrderItemEntity> orderItems = orderCreateTo.getOrderItems();
//批量保存订单项数据
orderItemService.saveBatch(orderItems);
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
//TODO 调用远程锁定库存的方法
//出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
//为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
R r = wmsFeignService.orderLockStock(lockVo);
gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/WmsFeignService.java
- 找出所有库存大于商品数的仓库
- 遍历所有满足条件的仓库,逐个尝试锁库存,若锁库存成功则退出遍历
/**
* 锁定库存
*/
@PostMapping(value = "/ware/waresku/lock/order")
R orderLockStock(@RequestBody WareSkuLockVo vo);
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/controller/WareSkuController.java
/**
* 下订单时锁库存
* @param
* @return
*/
@RequestMapping("/lock/order")
public R orderLockStock(@RequestBody WareSkuLockVo lockVo) {
try {
Boolean lock = wareSkuService.orderLockStock(lockVo);
return R.ok();
} catch (NoStockException e) {
return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(), BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());
}
}
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java
/**
* 锁定库存
*/
boolean orderLockStock(WareSkuLockVo vo);
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java
/**
* 为某个订单锁定库存
*/
@Transactional(rollbackFor = Exception.class)
@Override
public boolean orderLockStock(WareSkuLockVo vo) {
/**
* 保存库存工作单详情信息
* 追溯
* 如果没有库存,就不会发送消息给mq
* 【不会进入save(WareOrderTaskDetailEntity)逻辑,也不会发送消息给mq,也不会锁定库存,也不会监听到解锁服务】
*/
WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
wareOrderTaskEntity.setCreateTime(new Date());
wareOrderTaskService.save(wareOrderTaskEntity);
//1、按照下单的收货地址,找到一个就近仓库,锁定库存
//2、找到每个商品在哪个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map((item) -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪个仓库有库存 stock-锁定num > 0
List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIdList);
return stock;
}).collect(Collectors.toList());
//2、锁定库存
for (SkuWareHasStock hasStock : collect) {
boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (CollectionUtils.isEmpty(wareIds)) {
//没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
//1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
//2、锁定失败。前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所有就不用解锁
for (Long wareId : wareIds) {
//锁定成功就返回1,失败就返回0
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
// count==1表示锁定成功
if (count == 1) {
skuStocked = true;
// WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder()
// .skuId(skuId)
// .skuName("")
// .skuNum(hasStock.getNum())
// .taskId(wareOrderTaskEntity.getId())
// .wareId(wareId)
// .lockStatus(1)
// .build();
// wareOrderTaskDetailService.save(taskDetailEntity);
//
// //TODO 告诉MQ库存锁定成功
// StockLockedTo lockedTo = new StockLockedTo();
// lockedTo.setId(wareOrderTaskEntity.getId());
// StockDetailTo detailTo = new StockDetailTo();
// BeanUtils.copyProperties(taskDetailEntity,detailTo);// 这里直接存entity。但是应该存id更好,数据最好来自DB
// lockedTo.setDetailTo(detailTo);
// rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
// 锁定成功返回
break;
} else {
//当前仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false) {
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//3、肯定全部都是锁定成功的
return true;
}
分布式情况下,可能出现一些服务事务不一致的情况
库存扣减成功但是订单业务执行出错,订单业务可以回滚但远程调用的库存服务是办法回滚的
有多种模式:AT、TCC、SAGA 和 XA
doc:http://seata.io/zh-cn/docs/overview/what-is-seata.html
1、TC不会控制各RM回滚,而是调用补偿方案,AT模式是根据 回滚日志表【每个数据库都创建一个回滚日志表】
2、而TCC模式的回滚是根据补偿方法 来回滚
AT模式:Auto Transiaction:自动事务模式,根据回滚日志表自动回滚
TCC模式:就是根据自己手写的事务补偿方法 来回滚
Seata术语
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
不适用高并发场景,适用于商品服务,保存商品的那个接口 SpuInfoController
/save
1、保存spu pms_spu_info
2、保存attr
3、保存描述图片 pms_spu_info_desc
4、保存图片集 pms_spu_images
5、保存当前spu对应的所有sku信息
6、优惠券信息【远程调用】分布式事务【并发不高,可以使用AT模式,@GlobalTransactional】
7、保存积分信息【远程调用】分布式事务【并发不高,可以使用AT模式,@GlobalTransactional】
seata官方文档:https://github.com/seata/seata-samples/blob/master/doc/quick-integration-with-spring-cloud.md
1.创建 UNDO_LOG 表
-- 注意此处0.3.0+ 增加唯一索引 ux_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,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2.导入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>0.7.1version>
dependency>
下载安装事务协调器:seate-server0.7.1Release v0.7.1 · seata/seata · GitHub
版本与seata-all版本对应
3.配置seata的注册中心 registry.conf
4.启动D:\environment\seata-server-0.7.1\bin\seata-server.bat
seata各种属性配置 file.conf
5.所有想要用到分布式事务的微服务使用seata DataSourceProxy 代理自己的数据源
package site.zhourui.gulimall.order.config;
/**
* @author zr
* @date 2021/12/28 10:57
*/
import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
/**
* seata分布式事务
* 配置代理数据源
*/
//@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);
}
}
6.每个使用分布式事务的微服务都需要导入 file.conf registry.conf
注意file.conf:
需要注意的是 service.vgroup_mapping
这个配置,在 Spring Cloud 中默认是${spring.application.name}-fescar-service-group
,可以通过指定application.properties
的 spring.cloud.alibaba.seata.tx-service-group
这个属性覆盖,但是必须要和 file.conf
中的一致,否则会提示 no available server to connect
7.加注解
- 给分布式大事务的入口标注@GlobalTransactional gulimall-order服务
每一个远程的小事务用@Trabsactional gulimall-ware服务
重启服务测试
测试完成后关闭seata
GlobalTransactional
,排除依赖 gulimall-order,gulimall-ware
导入依赖
gulimall-ware/pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
配置Rabbitmq地址端口(虚拟主机,确认发送,抵达确认,手动ack)
spring:
rabbitmq:
host: 192.168.157.128
port: 5672
virtual-host: /
#开启发送端确认
publisher-confirms: true
# 开启发送端消息抵达Queue确认
publisher-returns: true
# 只要消息抵达Queue,就会异步发送优先回调returnfirm
template:
mandatory: true
# 使用手动ack确认模式,关闭自动确认【消息丢失】
listener:
simple:
acknowledge-mode: manual
开启RabbitMQ
主启动类加上
@EnableRabbit
配置确认回调,失败回调
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/config/MyRabbitConfig.java
package site.zhourui.gulimall.ware.config;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @author zr
* @date 2021/12/15 9:56
*/
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 定制RabbitTemplate
* 1、服务收到消息就会回调
* 1、spring.rabbitmq.publisher-confirms: true
* 2、设置确认回调
* 2、消息正确抵达队列就会进行回调
* 1、spring.rabbitmq.publisher-returns: true
* spring.rabbitmq.template.mandatory: true
* 2、设置确认回调ReturnCallback
*
* 3、消费端确认(保证每个消息都被正确消费,此时才可以broker删除这个消息)
*
*/
@PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate() {
/**
* 1、只要消息抵达Broker就ack=true
* correlationData:当前消息的唯一关联数据(这个是消息的唯一id)
* ack:消息是否成功收到
* cause:失败的原因
*/
//设置确认回调
rabbitTemplate.setConfirmCallback((correlationData,ack,cause) -> {
System.out.println("confirm...correlationData["+correlationData+"]==>ack:["+ack+"]==>cause:["+cause+"]");
});
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* message:投递失败的消息详细信息
* replyCode:回复的状态码
* replyText:回复的文本内容
* exchange:当时这个消息发给哪个交换机
* routingKey:当时这个消息用哪个路邮键
*/
rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey) -> {
System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]" +
"==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");
});
}
}
创建库存解锁延时队列及交换机,绑定
package site.zhourui.gulimall.ware.config;
/**
* @author zr
* @date 2021/12/28 16:47
*/
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.util.HashMap;
/**
* 创建队列,交换机,延迟队列,绑定关系 的configuration
* 不会重复创建覆盖
* 1、第一次使用队列【监听】的时候才会创建
* 2、Broker没有队列、交换机才会创建
*/
@Configuration
public class MyRabbitMQConfig {
@RabbitListener(queues = "stock.release.stock.queue")
public void listen( Channel channel, Message message) throws IOException {
System.out.println("收到库存解锁消息,准备解锁库存:------>");
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
}
/**
* 库存服务默认的交换机
* @return
*/
@Bean
public Exchange stockEventExchange() {
//String name, boolean durable, boolean autoDelete, Map arguments
TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
return topicExchange;
}
/**
* 普通队列
* @return
*/
@Bean
public Queue stockReleaseStockQueue() {
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
Queue queue = new Queue("stock.release.stock.queue", true, false, false);
return queue;
}
/**
* 延迟队列
* @return
*/
@Bean
public Queue stockDelay() {
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
// 消息过期时间 2分钟
arguments.put("x-message-ttl", 120000);
Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);
return queue;
}
/**
* 交换机与普通队列绑定
* @return
*/
@Bean
public Binding stockLocked() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map arguments
Binding binding = new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
return binding;
}
/**
* 交换机与延迟队列绑定
* @return
*/
@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
}
启动测试
向
stock.locked
路邮键发送队列,并监听死信队列,两分钟后监听到消息说明成功了
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java
/**
* 解锁库存
* @param to
*/
void unlockStock(StockLockedTo to);
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java
此处需要调用订单服务远程服务根据订单号查询订单信息
/**
* 解锁库存
*/
@Override
public void unlockStock(StockLockedTo to) {
//库存工作单的id
StockDetailTo detail = to.getDetailTo();
Long detailId = detail.getId();
/**
* 解锁
* 1、查询数据库关于这个订单锁定库存信息
* 有:证明库存锁定成功了
* 解锁:订单状况
* 1、没有这个订单,必须解锁库存
* 2、有这个订单,不一定解锁库存
* 订单状态:已取消:解锁库存
* 已支付:不能解锁库存
*/
WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);
if (taskDetailInfo != null) {
//查出wms_ware_order_task工作单的信息
Long id = to.getId();
WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);
//获取订单号查询订单状态
String orderSn = orderTaskInfo.getOrderSn();
//远程查询订单信息
R orderData = orderFeignService.getOrderStatus(orderSn);
if (orderData.getCode() == 0) {
//订单数据返回成功
OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});
//判断订单状态是否已取消或者支付或者订单不存在
// 1、订单不存在:解锁
// 2、订单存在,且订单状态是取消状态:解锁
if (orderInfo == null || orderInfo.getStatus() == 4) {
// 工作单状态必须是 已锁定 才可以解锁【因为解锁方法没有加事务】
if (taskDetailInfo.getLockStatus() == 1) {
unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
}
}
} else {
//消息拒绝以后重新放在队列里面,让别人继续消费解锁
//远程调用服务失败
throw new RuntimeException("远程调用服务失败");
}
} else {
//无需解锁【回滚状态】
}
}
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/feign/OrderFeignService.java
package site.zhourui.gulimall.ware.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.common.utils.R;
/**
* @author zr
* @date 2021/12/29 15:29
*/
@FeignClient("gulimall-order")
public interface OrderFeignService {
@GetMapping(value = "/order/order/status/{orderSn}")
R getOrderStatus(@PathVariable("orderSn") String orderSn);
}
远程订单服务
gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/OrderController.java
/**
* 根据订单编号查询订单状态
* @param orderSn
* @return
*/
@GetMapping(value = "/status/{orderSn}")
public R getOrderStatus(@PathVariable("orderSn") String orderSn) {
OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
return R.ok().setData(orderEntity);
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java
/**
* 按照订单号获取订单信息
* @param orderSn
* @return
*/
OrderEntity getOrderByOrderSn(String orderSn);
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 按照订单号获取订单信息
* @param orderSn
* @return
*/
@Override
public OrderEntity getOrderByOrderSn(String orderSn) {
OrderEntity orderEntity = this.baseMapper.selectOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
return orderEntity;
}
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/entity/WareOrderTaskDetailEntity.java
增加两个字段 仓库id,锁定状态使用
@Builder
package site.zhourui.gulimall.ware.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 库存工作单
*
* @author zr
* @email [email protected]
* @date 2021-09-28 15:47:50
*/
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@TableName("wms_ware_order_task_detail")
public class WareOrderTaskDetailEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 仓库id
*/
private Long wareId;
/**
* 锁定状态
* 1-已锁定 2-已解锁 3-已扣减
*/
private Integer lockStatus;
/**
* id
*/
@TableId
private Long id;
/**
* sku_id
*/
private Long skuId;
/**
* sku_name
*/
private String skuName;
/**
* 购买个数
*/
private Integer skuNum;
/**
* 工作单id
*/
private Long taskId;
}
锁定库存时向库存延时队列发送一条库存工作单记录
库存工作单
gulimall-common/src/main/java/site/zhourui/common/to/mq/StockLockedTo.java
package site.zhourui.common.to.mq;
/**
* 锁定库存成功,往延时队列存入 工作单to 对象
* wms_ware_order_task
* @author zr
* @date 2021/12/29 15:07
*/
import lombok.Data;
/**
*/
@Data
public class StockLockedTo {
/** 库存工作单的id **/
private Long id;
/** 库存单详情 wms_ware_order_task_detail**/
private StockDetailTo detailTo;
}
库存详情单
gulimall-common/src/main/java/site/zhourui/common/to/mq/StockDetailTo.java
package site.zhourui.common.to.mq;
/**
* 库存单详情
* wms_ware_order_task_detail
* @author zr
* @date 2021/12/29 15:07
*/
import lombok.Data;
@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;
}
监听库存死信队列,解锁库存
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/listener/StockReleaseListener.java
package site.zhourui.gulimall.ware.listener;
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 site.zhourui.common.to.mq.StockLockedTo;
import site.zhourui.gulimall.ware.service.WareSkuService;
import java.io.IOException;
/**
* 监听死信队列,解锁库存
* @author zr
* @date 2021/12/29 15:22
*/
@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
@Autowired
private WareSkuService wareSkuService;
/**
* 这个是监听死信消息
* 1、库存自动解锁
* 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
*
* 2、订单失败
* 库存锁定失败
*
* 只要解锁库存的消息失败,一定要告诉服务解锁失败
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("******收到解锁库存的延时信息******,准备解锁" + to.getDetailTo().getId());
try {
//当前消息是否被第二次及以后(重新)派发过来了
// Boolean redelivered = message.getMessageProperties().getRedelivered();
//解锁库存
wareSkuService.unlockStock(to);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
/**
* 客户取消订单,监听到消息
*/
@RabbitHandler
public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
System.out.println("******收到订单关闭,准备解锁库存的信息******订单号:" + orderTo.getOrderSn());
try {
wareSkuService.unlockStock(orderTo);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java
/**
* 解锁库存
* @param to
*/
void unlockStock(StockLockedTo to);
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java
此处需要调用订单服务远程查询订单信息
/**
* 解锁库存
*/
@Override
public void unlockStock(StockLockedTo to) {
//库存工作单的id
StockDetailTo detail = to.getDetailTo();
Long detailId = detail.getId();
/**
* 解锁
* 1、查询数据库关于这个订单锁定库存信息
* 有:证明库存锁定成功了
* 解锁:订单状况
* 1、没有这个订单,必须解锁库存
* 2、有这个订单,不一定解锁库存
* 订单状态:已取消:解锁库存
* 已支付:不能解锁库存
*/
WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);
if (taskDetailInfo != null) {
//查出wms_ware_order_task工作单的信息
Long id = to.getId();
WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);
//获取订单号查询订单状态
String orderSn = orderTaskInfo.getOrderSn();
//远程查询订单信息
R orderData = orderFeignService.getOrderStatus(orderSn);
if (orderData.getCode() == 0) {
//订单数据返回成功
OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});
//判断订单状态是否已取消或者支付或者订单不存在
// 1、订单不存在:解锁
// 2、订单存在,且订单状态是取消状态:解锁
if (orderInfo == null || orderInfo.getStatus() == 4) {
// 工作单状态必须是 已锁定 才可以解锁【因为解锁方法没有加事务】
if (taskDetailInfo.getLockStatus() == 1) {
unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
}
}
} else {
//消息拒绝以后重新放在队列里面,让别人继续消费解锁
//远程调用服务失败
throw new RuntimeException("远程调用服务失败");
}
} else {
//无需解锁【回滚状态】
}
}
/**
* 解锁库存的方法【设计DB,没加事务】
*/
public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
// 1、库存解锁
wareSkuDao.unLockStock(skuId,wareId,num);
// 2、更新工作单的状态 为已解锁 2
WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
taskDetailEntity.setId(taskDetailId);
taskDetailEntity.setLockStatus(2);
wareOrderTaskDetailService.updateById(taskDetailEntity);
}
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/feign/OrderFeignService.java
远程查询订单信息
package site.zhourui.gulimall.ware.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.common.utils.R;
/**
* @author zr
* @date 2021/12/29 15:29
*/
@FeignClient("gulimall-order")
public interface OrderFeignService {
@GetMapping(value = "/order/order/status/{orderSn}")
R getOrderStatus(@PathVariable("orderSn") String orderSn);
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/OrderController.java
/**
* 根据订单编号查询订单状态
* @param orderSn
* @return
*/
@GetMapping(value = "/status/{orderSn}")
public R getOrderStatus(@PathVariable("orderSn") String orderSn) {
OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
return R.ok().setData(orderEntity);
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java
/**
* 按照订单号获取订单信息
* @param orderSn
* @return
*/
OrderEntity getOrderByOrderSn(String orderSn);
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 按照订单号获取订单信息
* @param orderSn
* @return
*/
@Override
public OrderEntity getOrderByOrderSn(String orderSn) {
OrderEntity orderEntity = this.baseMapper.selectOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
return orderEntity;
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java
将该请求放行
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/order/order/status/**", uri);
return match;
在谷粒商城--消息队列--高级篇笔记十
的6.6 延时队列定时关单模拟
已经创建了交换机队列,绑定
https://blog.csdn.net/qq_31745863/article/details/122212434
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java
/**
* 关闭订单
* @param orderEntity
*/
void closeOrder(OrderEntity orderEntity);
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 关闭订单
*/
@Override
public void closeOrder(OrderEntity orderEntity) {
//关闭订单之前先查询一下数据库,判断此订单状态是否已支付
OrderEntity orderInfo = this.getOne(new QueryWrapper<OrderEntity>().
eq("order_sn",orderEntity.getOrderSn()));
if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {
//代付款状态进行关单
OrderEntity orderUpdate = new OrderEntity();
orderUpdate.setId(orderInfo.getId());
orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(orderUpdate);
// 发送消息给MQ
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderInfo, orderTo);
try {
//TODO 确保每个消息发送成功,给每个消息做好日志记录,(给数据库保存每一个详细信息)保存每个消息的详细信息
//TODO 定期扫描数据库,重新发送失败的消息
rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
} catch (Exception e) {
}
}
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/OrderCloseListener.java
package site.zhourui.gulimall.order.interceptor;
import com.rabbitmq.client.Channel;
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 site.zhourui.gulimall.order.entity.OrderEntity;
import site.zhourui.gulimall.order.service.OrderService;
import java.io.IOException;
/**
* @author zr
* @date 2021/12/29 17:22
*/
/**
* 定时关闭订单
*
*/
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
try {
orderService.closeOrder(orderEntity);
// 手动调用支付宝收单【这里省略了,可以参照demo中的代码】
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
清空之前的订单与库存锁定,库存工作单(也可以不清,但需要记住提交此时的状态,这样好看一点)
清空mq消息
下单
mq的订单延时队列(1分钟),库存延时队列(2分钟)
一分钟之内数据库状态
大于一分钟小于两分钟,自动关单
其他数据库表与一分钟一致
大于两分钟,库存自动解锁
防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
导致卡顿的订单,永远都不能解锁库存
解决方案
再往订单死信队列发送消息时,同时也往库存死信队列发送相同消息,通知库存解锁
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/listener/StockReleaseListener.java
/**
* 客户取消订单,监听到消息
*/
@RabbitHandler
public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
System.out.println("******收到订单关闭,准备解锁库存的信息******订单号:" + orderTo.getOrderSn());
try {
wareSkuService.unlockStock(orderTo);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
重载解锁库存
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java
/**
* 解锁订单
*/
void unlockStock(OrderTo orderTo);
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java
/**
* 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
* 导致卡顿的订单,永远都不能解锁库存
* @param orderTo
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void unlockStock(OrderTo orderTo) {
String orderSn = orderTo.getOrderSn();
//查一下最新的库存解锁状态,防止重复解锁库存
WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
//按照工作单的id找到所有 没有解锁的库存,进行解锁
Long id = orderTaskEntity.getId();
List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
.eq("task_id", id).eq("lock_status", 1));
for (WareOrderTaskDetailEntity taskDetailEntity : list) {
unLockStock(taskDetailEntity.getSkuId(),
taskDetailEntity.getWareId(),
taskDetailEntity.getSkuNum(),
taskDetailEntity.getId());
}
}
在库存解锁前查一下最新的库存解锁状态,防止重复解锁库存
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareOrderTaskService.java
WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn);
gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareOrderTaskServiceImpl.java
@Override
public WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn) {
WareOrderTaskEntity orderTaskEntity = this.baseMapper.selectOne(
new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
return orderTaskEntity;
}
消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息
重新由unack变为ready,并发送给其他消费者
消息消费失败,由于重试机制,自动又将消息发送出去
成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送
消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理
CREATE TABLE `mq_message`(
`message_id` char(32) not null ,
`content` text, #json
`to_exchange` char(255) default null ,
`routing_key` char(255) default null ,
`class_type` char(255) default null ,
`message_status` int(1) default '0' comment '0-新建 1-已发送 2-错误抵达 3-已抵达',
`create_time` datetime default null ,
`update_time` datetime default null
)
rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的
加密与解密用的秘钥都是一样的
加密与解密用到的秘钥不一致
商户私钥
加一个对应的签名,支付宝端会使用商户公钥
对签名进行验签,只有数据明文和签名对应的时候才能说明传输正确支付宝私钥
加一个对应的签名,商户端收到支付成功数据之后也会使用支付宝公钥
延签,成功后才能确认公钥和私钥是一个相对概念
它们的公私性是相对于生成者来说的。
一对密钥生成后, 保存在生成者手里的就是私钥,生成者发布出去大家用的就是公钥
官方demo下载地址:https://opendocs.alipay.com/open/54/106682
设置并启用
配置web目录
添加archive
配置Tomcat
添加依赖
配置字符集
启动Tomcat,测试
能够字符成功的话就说明测试成功了
测试账号密码https://open.alipay.com/platform/appDaily.htm?tab=account
支付宝需要回调我们的接口进行异步通知
内网穿透功能可以允许我们使用外网的网址来访问主机;
正常的外网需要访问我们项目的流程是:
1、 开发测试(微信、 支付宝)
2、 智慧互联
3、 远程控制
4、 私有云
1、 natapp: https://natapp.cn/ 优惠码: 022B93FD(9 折) [仅限第一次使用]
2、 续断: www.zhexi.tech 优惠码: SBQMEA(95 折) [仅限第一次使用]
3、 花生壳: https://www.oray.com/
官方文档NATAPP1分钟快速新手图文教程 - NATAPP-内网穿透 基于ngrok的国内高速内网映射工具
注册,实名认证
购买免费隧道后拿到authtoken
window启动命令
natapp -authtoken=你的authtoken
gulimall-order/pom.xml
<dependency>
<groupId>com.alipay.sdkgroupId>
<artifactId>alipay-sdk-javaartifactId>
<version>4.10.111.ALLversion>
dependency>
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/AlipayTemplate.java
package site.zhourui.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 lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author zr
* @date 2021/12/31 10:26
*/
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
// 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
public String app_id;
// 商户私钥,您的PKCS8格式RSA2私钥
public String merchant_private_key;
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
public String alipay_public_key;
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
public String notify_url;
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//同步通知,支付成功,一般跳转到成功页
public String return_url;
// 签名方式
private String sign_type;
// 字符编码格式
private String charset;
//订单超时时间
private String timeout = "1m";
// 支付宝网关; https://openapi.alipaydev.com/gateway.do
public String gatewayUrl;
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 +"\","
+ "\"timeout_express\":\""+timeout+"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
String result = alipayClient.pageExecute(alipayRequest).getBody();
//会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
System.out.println("支付宝响应:登录页面的代码\n"+result);
return result;
}
@Data
public static class PayVo {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private String total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}
}
alipay:
alipay_public_key: xxx
app_id: 2021000117672854
charset: utf-8
gatewayUrl: https://openapi.alipaydev.com/gateway.do
merchant_private_key: xxxx
#此处先使用demo的回调接口页面
notify_url: http://4wa8cx.natappfree.cc/alipay_trade_wap_pay_java_utf_8_Web_exploded/notify_url.jsp
return_url: http://4wa8cx.natappfree.cc/alipay_trade_wap_pay_java_utf_8_Web_exploded/return_url.jsp
sign_type: RSA2
gulimall-order/src/main/java/site/zhourui/gulimall/order/web/PayWebController.java
package site.zhourui.gulimall.order.web;
import com.alipay.api.AlipayApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import site.zhourui.gulimall.order.config.AlipayTemplate;
import site.zhourui.gulimall.order.service.OrderService;
/**
* @author zr
* @date 2021/12/31 10:57
*/
@Slf4j
@Controller
public class PayWebController {
@Autowired
private AlipayTemplate alipayTemplate;
@Autowired
private OrderService orderService;
/**
* 用户下单:支付宝支付
* 1、让支付页让浏览器展示
* 2、支付成功以后,跳转到用户的订单列表页
* @param orderSn
* @return
* @throws AlipayApiException
*/
@ResponseBody
@GetMapping(value = "/aliPayOrder",produces = "text/html")
public String aliPayOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
AlipayTemplate.PayVo payVo = orderService.getOrderPay(orderSn);
// 支付宝返回一个页面【支付宝账户登录的html页面】
String pay = alipayTemplate.pay(payVo);
System.out.println(pay);
return pay;
}
}
获取当前订单的支付信息
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java
/**
* 获取当前订单的支付信息
* @param orderSn
* @return
*/
AlipayTemplate.PayVo getOrderPay(String orderSn);
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 获取当前订单的支付信息
*/
@Override
public AlipayTemplate.PayVo getOrderPay(String orderSn) {
AlipayTemplate.PayVo payVo = new AlipayTemplate.PayVo();
OrderEntity orderInfo = this.getOrderByOrderSn(orderSn);
//保留两位小数点,向上取值
BigDecimal payAmount = orderInfo.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
payVo.setTotal_amount(payAmount.toString());
payVo.setOut_trade_no(orderInfo.getOrderSn());
//查询订单项的数据
List<OrderItemEntity> orderItemInfo = orderItemService.list(
new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
OrderItemEntity orderItemEntity = orderItemInfo.get(0);
payVo.setBody(orderItemEntity.getSkuAttrsVals());
payVo.setSubject(orderItemEntity.getSkuName());
return payVo;
}
https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/pay.html
支付成功后跳转页面,正常情况下应该跳转到订单列表页面
gulimall-gateway/src/main/resources/application.yml
- id: gulimall_member_route
uri: lb://gulimall-member
predicates:
- Host=member.gulimall.com
前端页面https://gitee.com/zhourui815/gulimall/blob/master/gulimall-member/src/main/resources/templates/orderList.html
gulimall-member/src/main/java/site/zhourui/gulimall/member/web/MemberWebController.java
package site.zhourui.gulimall.member.web;
import com.alibaba.fastjson.JSON;
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 site.zhourui.common.utils.R;
import site.zhourui.gulimall.member.feign.OrderFeignService;
import java.util.HashMap;
import java.util.Map;
/**
* @author zr
* @date 2021/12/31 15:23
*/
@Controller
public class MemberWebController {
@Autowired
private OrderFeignService orderFeignService;
@GetMapping(value = "/memberOrder.html")
public String memberOrderPage(@RequestParam(value = "pageNum",required = false,defaultValue = "0") Integer pageNum,
Model model) {
//获取到支付宝给我们转来的所有请求数据
//request,验证签名
//查出当前登录用户的所有订单列表数据
Map<String,Object> page = new HashMap<>();
page.put("page",pageNum.toString());
//远程查询订单服务订单数据
R orderInfo = orderFeignService.listWithItem(page);
System.out.println(JSON.toJSONString(orderInfo));
model.addAttribute("orders",orderInfo);
return "orderList";
}
}
需要调用订单远程服务
gulimall-member/src/main/java/site/zhourui/gulimall/member/feign/OrderFeignService.java
package site.zhourui.gulimall.member.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import site.zhourui.common.utils.R;
import java.util.Map;
/**
* @author zr
* @date 2021/12/31 15:24
*/
@FeignClient("gulimall-order")
public interface OrderFeignService {
/**
* 分页查询当前登录用户的所有订单信息
*/
@PostMapping("/order/order/listWithItem")
R listWithItem(@RequestBody Map<String, Object> params);
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/OrderController.java
/**
* member远程调用:分页查询当前登录用户的所有订单信息
*/
@PostMapping("/listWithItem")
//@RequiresPermissions("order:order:list")
public R listWithItem(@RequestBody Map<String, Object> params){
PageUtils page = orderService.queryPageWithItem(params);
return R.ok().put("page", page);
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java
/**
* 查询当前用户所有订单数据
* @param params
* @return
*/
PageUtils queryPageWithItem(Map<String, Object> params);
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
此处需要设置订单行信息
/**
* 查询当前用户所有订单数据
*/
@Override
public PageUtils queryPageWithItem(Map<String, Object> params) {
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
IPage<OrderEntity> page = this.page(
new Query<OrderEntity>().getPage(params),
new QueryWrapper<OrderEntity>()
.eq("member_id",memberResponseVo.getId()).orderByDesc("create_time")
);
//遍历所有订单集合
List<OrderEntity> orderEntityList = page.getRecords().stream().map(order -> {
//根据订单号查询订单项里的数据
List<OrderItemEntity> orderItemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>()
.eq("order_sn", order.getOrderSn()));
order.setOrderItemEntityList(orderItemEntities);
return order;
}).collect(Collectors.toList());
page.setRecords(orderEntityList);
return new PageUtils(page);
}
为OrderEntity 新增属性
@TableField(exist = false)
private List<OrderItemEntity> orderItemEntityList;
gulimall-member/src/main/java/site/zhourui/gulimall/member/interceptor/LoginUserInterceptor.java
新增拦截器,放行member/**,远程调用接口
package site.zhourui.gulimall.member.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.vo.MemberResponseVo;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;
/**
* @author zr
* @date 2021/12/31 15:34
*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/member/**", uri);
if (match) {
return true;
}
HttpSession session = request.getSession();
//获取登录的用户信息
MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null) {
//把登录后用户的信息放在ThreadLocal里面进行保存
loginUser.set(attribute);
return true;
} else {
//未登录,返回登录页面
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("");
// session.setAttribute("msg", "请先进行登录");
// response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
gulimall-member/src/main/java/site/zhourui/gulimall/member/config/MemberWebConfig.java
package site.zhourui.gulimall.member.config;
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;
import site.zhourui.gulimall.member.interceptor.LoginUserInterceptor;
/**
* @author zr
* @date 2021/12/31 15:36
*/
@Configuration
public class MemberWebConfig implements WebMvcConfigurer {
@Autowired
private LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
依赖
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
配置文件
spring:
application:
name: gulimall-member
redis:
port: 6379
host: 192.168.157.128
jackson:
date-format: yyyy-MM-dd HH:mm:ss
session:
store-type: redis
session自定义配置
gulimall-member/src/main/java/site/zhourui/gulimall/member/config/GulimallSessionConfig.java
package site.zhourui.gulimall.member.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* @author zr
* @date 2021/12/12 10:29
*/
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//放大作用域
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
cookieSerializer.setCookieMaxAge(60*60*24*7);
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
解决feign远程调用请求头丢失问题
gulimall-member/src/main/java/site/zhourui/gulimall/member/config/GuliFeignConfig.java
package site.zhourui.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.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @author zr
* @date 2021/12/31 15:36
*/
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//老请求
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
//2、同步请求头的数据(主要是cookie)
//把老请求的cookie值放到新请求上来,进行一个同步
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
}
修改return_url地址为member服务的订单列表页请求地址
gulimall-order/src/main/resources/application.yaml
return_url: http://member.gulimall.com/memberOrder.html
success
gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/OrderPayedListener.java
package site.zhourui.gulimall.order.listener;
import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import site.zhourui.gulimall.order.config.AlipayTemplate;
import site.zhourui.gulimall.order.service.OrderService;
import site.zhourui.gulimall.order.vo.PayAsyncVo;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
/**
* @author zr
* @date 2021/12/31 17:30
*/
@RestController
public class OrderPayedListener {
@Autowired
private OrderService orderService;
@Autowired
private AlipayTemplate alipayTemplate;
@PostMapping(value = "/payed/notify")
public String handleAlipayed(PayAsyncVo asyncVo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
// 只要收到支付宝的异步通知,返回 success 支付宝便不再通知
// 获取支付宝POST过来反馈信息
//TODO 需要验签
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.handlePayResult(asyncVo);
return result;
} else {
System.out.println("签名验证失败...");
return "error";
}
}
}
处理支付宝支付结果
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
/**
* 处理支付宝的支付结果
*
*/
@Transactional(rollbackFor = Exception.class)
@Override
public String handlePayResult(PayAsyncVo asyncVo) {
//保存交易流水信息
PaymentInfoEntity paymentInfo = new PaymentInfoEntity();
paymentInfo.setOrderSn(asyncVo.getOut_trade_no());
paymentInfo.setAlipayTradeNo(asyncVo.getTrade_no());
paymentInfo.setTotalAmount(new BigDecimal(asyncVo.getBuyer_pay_amount()));
paymentInfo.setSubject(asyncVo.getBody());
paymentInfo.setPaymentStatus(asyncVo.getTrade_status());
paymentInfo.setCreateTime(new Date());
paymentInfo.setCallbackTime(asyncVo.getNotify_time());
//添加到数据库中
this.paymentInfoService.save(paymentInfo);
//修改订单状态
//获取当前状态
String tradeStatus = asyncVo.getTrade_status();
if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) {
//支付成功状态
String orderSn = asyncVo.getOut_trade_no(); //获取订单号
this.updateOrderStatus(orderSn,OrderStatusEnum.PAYED.getCode(), PayConstant.ALIPAY);
}
return "success";
}
/**
* 修改订单状态
* @param orderSn
* @param code
*/
private void updateOrderStatus(String orderSn, Integer code,Integer payType) {
this.baseMapper.updateOrderStatus(orderSn,code,payType);
}
修改订单状态
gulimall-order/src/main/java/site/zhourui/gulimall/order/dao/OrderDao.java
/**
* 修改订单状态
* @param orderSn
* @param code
* @param payType
*/
void updateOrderStatus(@Param("orderSn") String orderSn,
@Param("code") Integer code,
@Param("payType") Integer payType);
gulimall-order/src/main/resources/mapper/order/OrderDao.xml
<update id="updateOrderStatus">
UPDATE oms_order
SET `status` = #{code},modify_time = NOW(),pay_type = #{payType},payment_time = NOW()
WHERE order_sn = #{orderSn}
</update>
修改notify_url地址为订单服务的回调接口地址
gulimall-order/src/main/resources/application.yaml
notify_url: http://4wa8cx.natappfree.cc/payed/notify
gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java
package site.zhourui.gulimall.order.interceptor;
/**
* @author zr
* @date 2021/12/21 22:04
*/
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.vo.MemberResponseVo;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;
import static site.zhourui.common.constant.AuthServerConstant.LOGIN_USER;
/**
* 登录拦截器
* 从session中获取了登录信息(redis中),封装到了ThreadLocal中
*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/order/order/status/**", uri);
boolean match1 = antPathMatcher.match("/payed/notify", uri);
if (match || match1) {
return true;
}
HttpSession session = request.getSession();
MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (memberResponseVo != null) {
loginUser.set(memberResponseVo);
return true;
}else {
session.setAttribute("msg","请先登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
order.gulimall.com:80
order.gulimall.com
,因此nginx转发到网关后找不到对应的服务,所以需要对nginx进行设置修改内网穿透接口
测试
nginx 配置域名转发
listen 80;
server_name gulimall.com *.gulimall.com *.natappfree.cc;
#server_name search.gulimall.com;
#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.com;
proxy_pass http://gulimall;
}
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
拦截器放行通知接口
gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/order/order/status/**", uri);
boolean match1 = antPathMatcher.match("/payed/notify", uri);
if (match || match1) {
return true;
}
HttpSession session = request.getSession();
MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (memberResponseVo != null) {
loginUser.set(memberResponseVo);
return true;
}else {
session.setAttribute("msg","请先登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
Field error in object 'payAsyncVo' on field 'notify_time': rejected value [2022-01-02 10:50:06]; codes [typeMismatch.payAsyncVo.notify_time,typeMismatch.notify_time,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [payAsyncVo.notify_time,notify_time]; arguments []; default message [notify_time]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'notify_time'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2022-01-02 10:50:06'; nested exception is java.lang.IllegalArgumentException]]
解决方案
spring:
mvc:
date-format: yyyy-MM-dd HH:mm:ss
订单号长度报错
修改oms_payment_info 的订单号长度
付款成功后自动跳转到订单列表页
订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库存解锁了。
由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到
网络阻塞问题,订单支付成功的异步通知一直不到达
其他各种问题
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的, 不会因为多次点击而产生了副作用; 比如说支付场景, 用户购买了商品支付扣款成功, 但是返回结果的时候网络异常, 此时钱已经扣了, 用户再次点击按钮, 此时会进行第二次扣款, 返回结果成功, 用户查询余额返发现多扣钱了, 流水记录也变成了两条. . . ,这就没有保证接口的幂等性。
以 SQL 为例, 有些操作是天然幂等的。
危险性:
删除 token 还是后删除 token;
Token 获取、 比较和删除必须是原子性
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
select * from xxxx where id = 1 for update;
悲观锁使用时一般伴随事务一起使用, 数据锁定时间可能会很长, 需要根据实际情况选用。另外要注意的是, id 字段一定是主键或者唯一索引, 不然可能造成锁表的结果, 处理起来会非常麻烦。
这种方法适合在更新的场景中,
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1根据 version 版本, 也就是在操作库存前先获取当前商品的 version 版本号, 然后操作的时候带上此 version 号。 我们梳理下, 我们第一次操作库存时, 得到 version 为 1, 调用库存服务version 变成了 2; 但返回给订单服务出现了问题, 订单服务又一次发起调用库存服务, 当订单服务传如的 version 还是 1, 再执行上面的 sql 语句时, 就不会执行; 因为 version 已经变为 2 了, where 条件就不成立。 这样就保证了不管调用几次, 只会真正的处理一次。乐观锁主要使用于处理读多写少的问题
如果多个机器可能在同一时间同时处理相同的数据, 比如多台机器定时任务都拿到了相同数据处理, 我们就可以加分布式锁, 锁定此数据, 处理完成后释放锁。 获取到锁的必须先判断这个数据是否被处理过。
插入数据, 应该按照唯一索引进行插入, 比如订单号, 相同的订单就不可能有两条记录插入。我们在数据库层面防止重复。
这个机制是利用了数据库的主键唯一约束的特性, 解决了在 insert 场景时幂等问题。 但主键的要求不是自增的主键, 这样就需要业务生成全局唯一的主键。
如果是分库分表场景下, 路由规则要保证相同请求下, 落地在同一个数据库和同一表中, 要不然数据库主键约束就不起效果了, 因为是不同的数据库和表主键不相关。
很多数据需要处理, 只能被处理一次, 比如我们可以计算数据的 MD5 将其放入 redis 的 set,每次处理数据, 先看这个 MD5 是否已经存在, 存在就不处理。
使用订单号 orderNo 做为去重表的唯一索引, 把唯一索引插入去重表, 再进行业务操作, 且他们在同一个事务中。 这个保证了重复请求时, 因为去重表有唯一约束, 导致请求失败, 避免了幂等问题。 这里要注意的是, 去重表和业务表应该在同一库中, 这样就保证了在同一个事务, 即使业务操作失败了, 也会把去重表的数据回滚。 这个很好的保证了数据一致性。之前说的 redis 防重也算
调用接口时, 生成一个唯一 id, redis 将数据保存到集合中(去重) , 存在即处理过。可以使用 nginx 设置每一个请求的唯一 id;
proxy_set_header X-Request-Id $request_id;
数据库事务的几个特性: 原子性(Atomicity )、 一致性( Consistency )、 隔离性或独立性( Isolation)和持久性(Durabilily), 简称就是 ACID;
在以往的单体应用中, 我们多个业务操作使用同一条连接操作不同的数据表, 一旦有异常,我们可以很容易的整体回滚;
TransactionAutoConfiguration
在同一个类里面, 编写两个方法, 内部调用的时候, 会导致事务设置失效。 原因是没有用到代理对象的缘故。
解决办法
- 导入 spring-boot-starter-aop
- @EnableTransactionManagement(proxyTargetClass = true)
- @EnableAspectJAutoProxy(exposeProxy=true)
- AopContext.currentProxy() 调用方法
示例:
1、如果方法a、b、c都在同一个service里面,事务传播行为不生效,共享一个事务
原理:事务是用代理对象来控制的,内部调用b(),c(),就相当于直接调用没有经过事务【绕过了代理对象】
解决:不能使用this.b();也不能注入自己【要使用代理对象来调用事务方法】
@Transactional(timeout=30)
public void a() {
b();// a事务传播给了b事务,并且b事务的设置失效
c();// c单独创建一个新事务
}
@Transactional(propagation = Propagation.REQUIRED, timeout=2)
public void b() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c() {
}
解决步骤
具体步骤:
1、引入aop依赖
<!-- 引入aop,解决本地事务失效问题 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、开启动态代理【默认使用jdk动态代理,需要有接口】
@EnableAspectJAutoProxy(exposeProxy = true) //开启了aspect动态代理模式,对外暴露代理对象
好处:cglib继承的方式完成动态代理
exposeProxy = true:对外暴露代理对象
3、获取动态代理对象
OrderServiceImpl orderService = (OrderServiceImpl)AopContext.currentProxy();
orderService.b();
orderService.c();
分布式系统经常出现的异常机器宕机、 网络异常、 消息丢失、 消息乱序、 数据错误、 不可靠的 TCP、 存储数据丢失…
分布式事务是企业集成中的一个技术难点, 也是每一个分布式系统架构中都会涉及到的一个东西, 特别是在微服务架构中, 几乎可
以说是无法避免。
CAP 原则又称 CAP 定理, 指的是在一个分布式系统中
CAP 原则指的是, 这三个要素最多只能同时实现两点, 不可能三者兼顾。
一般来说, 分区容错无法避免, 因此可以认为 CAP 的 P 总是成立。 CAP 定理告诉我们,剩下的 C 和 A 无法同时做到(CA没有P就是单体应用,没有必要)。
如果满足P,此时要满足A(所有机器都可用包括通信故障那台【数据未同步】),就不能保证一致性【同步数据的通信线故障,无法同步】
如果满足P,此时要满足C,那网络通信故障的节点就不应该继续提供服务(因为他的数据不一致)【宕机的那台机器数据 无法同步】
AP:容易,就算未同步的数据也可用
CP:牺牲可用性
1、算法:raft和paxos算法:http://thesecretlivesofdata.com/raft/【raft算法演示】
具体步骤:
1、选举超时 election timeout
随从变成候选者的时间【150ms and 300ms随机的】【自旋时间,如果没有收到领导的命令变成候选者】
例如:启动集群,3个节点获得随机自旋时间,自旋时间到了就成为候选节点
2、成为候选节点,并给自己投票1,然后给其他随从节点发送选举请求【随从节点的票可能投给更快的候选者】
随从节点的票一旦投出便重新自旋
3、心跳时间(heartbeat timeout):每隔一段时间发送一个心跳,然后随从节点刷新自旋时间【小于300ms,否则大家都成为候选者了】
此时领导网络延时,自旋结束产生候选者,产生新领导
4、有多个候选者,并且票数一样,就自旋重新投
所有节点修改数据,都要通过领导来修改
具体步骤:
1、领导收到后并不会马上给随从节点发送 日志,等待下一次心跳时发送日志
2、然后领导提交并马上返回请求提交成功。然后跟随下一个心跳发送随从 告诉其提交
3、可保证数据一致性【例如选出来两个领导,不同机房。2个和3个组成两个群】
demo:此时2个的那个客户端发请求,一直保存失败,因为不是大多数人成功【所以数据未提交】,但是另外一边3个节点组成的集群可以保存成功【大多数节点】
如果此时两个集群恢复了数据通信,旧领导退位,并且跟着旧领导未提交的数据需要回滚【低轮领导退位,新领导上位】
然后匹配上新领导的日志
对于多数大型互联网应用的场景, 主机众多、 部署分散, 而且现在的集群规模越来越大, 所以节点故障、 网络故障是常态, 而且要保证服务可用性达到 99.99999%(N 个 9) , 即保证P 和 A, 舍弃 C。
是对 CAP 理论的延伸, 思想是即使无法做到强一致性(CAP 的一致性就是强一致性) , 但可以采用适当的采取弱一致性, 即最终一致性。
BASE 是指
从客户端角度, 多进程并发访问时, 更新过的数据在不同进程如何获取的不同策略, 决定了不同的一致性。 对于关系型数据库, 要求更新过的数据能被后续的访问都能看到, 这是强一致性。 如果能容忍后续的部分或者全部访问不到, 则是弱一致性。 如果经过一段时间后要求能访问到更新后的数据, 则是最终一致性
数据库支持的 2PC【 2 phase commit 二阶提交】 , 又叫做 XA Transactions。MySQL 从 5.5 版本开始支持, SQL Server 2005 开始
支持, Oracle 7 开始支持。其中, XA 是一个两阶段提交协议, 该协议分为以下两个阶段:
第一阶段: 事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作, 并反映是否可以提交.
第二阶段: 事务协调器要求每个数据库提交数据。
其中, 如果有任何一个数据库否决此次提交, 那么所有数据库都会被要求回滚它们在此事务中的那部分信息。
刚性事务: 遵循 ACID 原则, 强一致性。
柔性事务: 遵循 BASE 理论, 最终一致性;
与刚性事务不同, 柔性事务允许一定时间内, 不同节点的数据不一致, 但要求最终一致。
一阶段 prepare 行为: 调用 自定义 的 prepare 逻辑。
二阶段 commit 行为: 调用 自定义 的 commit 逻辑。
二阶段 rollback 行为: 调用 自定义 的 rollback 逻辑。
所谓 TCC 模式, 是指支持把 自定义 的分支事务纳入到全局事务的管理中(seata)。
实现:
将业务代码拆成三部分。
1、try锁库存
2、confirm提交数据
3、事务补偿逻辑:一旦出现异常执行cancel来回滚【取消锁定库存】
其实就是2PC的手动实现
按规律进行通知, 不保证数据一定能通知成功, 但会提供可查询操作接口进行核对。 这种方案主要用在与第三方系统通讯时, 比如: 调用微信或支付宝支付后的支付结果通知。 这种方案也是结合 MQ 进行实现, 例如: 通过 MQ 发送 http 请求, 设置最大通知次数。 达到通知次数后即不再通知。
案例: 银行通知、 商户通知等( 各大交易业务平台间的商户通知: 多次通知、 查询校对、 对账文件) , 支付宝的支付成功异步回调
例如支付宝支付成功,往MQ发送消息【隔几秒发一个】
订单订阅topic,一旦订单确认消息,给支付宝发送确认,支付宝就不再通知了