You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

512 lines
20 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 订单模块
# 一、资源整合
  我们需要把相关的静态资源拷贝到nginx然后动态模板文件拷贝到order项目的templates目录下然后调整资源的路径。在网关中设置对应的路由即可。
# 二、整合SpringSession
  结合官网导入对应的依赖然后添加对应的配置信息redis配置信息Cookie的配置一级域名和二级域名。
# 三、订单中心
  订单中心涉及到的模块
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/50facb2b205e4e57a7759e89cc1b0b52.png)
订单的状态:
1. 待付款:提交订单,订单预下单
2. 已付款/待发货完成支付订单系统需要记录支付时间支付流水号便于对账订单下放到wms系统仓库进行调拨配货分拣出库等操作
3. 待收款/已发货:仓库将商品出库,订单进入物流环节
4. 已完成:用户确认收货,订单交易完成,后续支付侧进行结算,如果订单存在问题就进入售后状态
5. 已取消:付款之前取消订单。
6. 售后中:用户在付款后申请退款,或商家发货后用户申请退换货。
订单流程:
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/563690b8538a499fb75be45db0a16524.png)
# 四、认证拦截
  订单服务中的所有的请求都必须是在认证的状态下处理的,所有我们需要添加一个校验是否认证的拦截器
```java
package com.msb.mall.order.interceptor;
import com.msb.common.constant.AuthConstant;
import com.msb.common.vo.MemberVO;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class AuthInterceptor implements HandlerInterceptor {
public static ThreadLocal threadLocal = new ThreadLocal();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 通过HttpSession获取当前登录的用户信息
HttpSession session = request.getSession();
Object attribute = session.getAttribute(AuthConstant.AUTH_SESSION_REDIS);
if(attribute != null){
MemberVO memberVO = (MemberVO) attribute;
threadLocal.set(memberVO);
return true;
}
// 如果 attribute == null 说明没有登录,那么我们就需要重定向到登录页面
session.setAttribute(AuthConstant.AUTH_SESSION_MSG,"请先登录");
response.sendRedirect("http://auth.msb.com/login.html");
return false;
}
}
```
然后注册该拦截器即可
```
@Configuration
public class MyWebInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/**");
}
}
```
# 五、订单确认页
## 1.订单确认页VO抽取
```java
public class OrderConfirmVo {
// 订单的收货人 及 收货地址信息
@Getter @Setter
List<MemberAddressVo> address;
// 购物车中选中的商品信息
@Getter @Setter
List<OrderItemVo> items;
// 支付方式
// 发票信息
// 优惠信息
//Integer countNum;
public Integer getCountNum(){
int count = 0;
if(items != null){
for (OrderItemVo item : items) {
count += item.getCount();
}
}
return count;
}
// BigDecimal total ;// 总的金额
public BigDecimal getTotal(){
BigDecimal sum = new BigDecimal(0);
if(items != null ){
for (OrderItemVo item : items) {
BigDecimal totalPrice = item.getPrice().multiply(new BigDecimal(item.getCount()));
sum = sum.add(totalPrice);
}
}
return sum;
}
// BigDecimal payTotal;// 需要支付的总金额
public BigDecimal getPayTotal(){
return getTotal();
}
}
```
## 2.确认页数据获取
&emsp;&emsp;通过Fegin远程调用对应的服务获取会员的数据和购物车中的商品信息。
```java
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo vo = new OrderConfirmVo();
MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get();
// 获取到 RequestContextHolder 的相关信息
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
// 同步主线程中的 RequestContextHolder
RequestContextHolder.setRequestAttributes(requestAttributes);
// 1.查询当前登录用户对应的会员的地址信息
Long id = memberVO.getId();
List<MemberAddressVo> addresses = memberFeginService.getAddress(id);
vo.setAddress(addresses);
}, executor);
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 2.查询购物车中选中的商品信息
List<OrderItemVo> userCartItems = cartFeginService.getUserCartItems();
vo.setItems(userCartItems);
}, executor);
try {
CompletableFuture.allOf(future1,future2).get();
} catch (Exception e) {
e.printStackTrace();
}
// 3.计算订单的总金额和需要支付的总金额 VO自动计算
return vo;
}
```
在Fegin调用远程服务的时候会出现请求Header丢失的问题。
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/12fab57056b84296b2e370c07b8835d6.png)
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/abdf9be8827644fbabc0b8d84c291d74.png)
首先我们创建 `RequestInterceptor`的实现来绑定Header信息同时在异步处理的时候我们需要从主线程中获取Request信息然后绑定在子线程中。
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/03b15a2ea3754fab8c19d466bb6edeb1.png)
然后在订单确认页中渲染数据的展示
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/474288fa974c417c9dcddb34bcc53424.png)
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/c03fe7d9450b4abab9f8e9cca4863105.png)
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/af9331a66a104e39901173c8517707ad.png)
最后的页面效果
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/53b4910decb94a2eb86564168b127c07.png)
# 六、接口幂等性处理
幂等性: 多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。
## 1.天然的幂等行为
以SQL语句为例
```sql
select * from t_user where id = 1;
update t_user set age = 18 where id = 2;
delete from t_user where id = 1
insert into (userid,username)values(1,'波哥') ; # userid 唯一主键
```
不具备幂等行为的
```SQL
update t_user set age = age + 1 where id = 1;
insert into (userid,username)values(1,'波哥'); # userid 不是主键 可以重复
```
## 2.**需要使用幂等的场景**
需要使用幂等的场景
* 前端重复提交
* 接口超时重试
* 消息队列重复消费
## 3.解决方案
1. **token机制** ①客户端请求获取token服务端生成一个唯一ID作为token存在redis中②客户端第二次请求时携带token服务端校验token成功则执行业务操作并删除token服务端校验token失败则表示重复操作。
2. **基于mysql** :①新建去重表;②服务端将客户端请求时提交的部分信息放入表中,其中有唯一索引字段;③成功插入则没有重复请求,插入失败则重复请求。
3. **基于redis** ①客户端请求服务端拿本次请求的标识字段②服务端将标识字段以setnx方式存入redis并设置过期时间③设置成功则说明非重复操作设置失败则表示重复操作。
4. 状态机、悲观锁、乐观锁等。
# 七、提交订单
## 1.防重提交
&emsp;&emsp;在订单提交的时候我们通过防重Token来保证请求的幂等性
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/e9b7179e6bdf4ad992183b461cfff5d4.png)
## 2.生成Token
&emsp;&emsp;我们在获取订单结算页数据的service中我们需要生成对应的Token并且保存到Redis中同时绑定到页面。
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/87dedfea617d4ccab41db943c44c3a45.png)
页面中的处理
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/336759f08fe24878b085cb201d29c127.png)
## 3.提交订单
&emsp;&emsp;然后在提交订单的逻辑中我们先创建对应的VO
```java
@Data
public class OrderSubmitVO {
// 收获地址的id
private Long addrId;
// 支付方式
private Integer payType;
// 防重Token
private String orderToken;
// 买家备注
private String note;
}
```
然后在订单确认页中创建对应的form表单
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/14c2e5ea197944b493c88d15e604f0d1.png)
然后把数据提交到后端服务中。
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/bfc4432db59a455e8be85ffcc1abda52.png)
## 4.防重检查
&emsp;&emsp;订单数据提交到后端服务,我们在下订单前需要做防重提交的校验。
```java
try{
lock.lock();//加锁
String redisToken = redisTemplate.opsForValue().get(key);
if(redisToken != null && redisToken.equals(vo.getOrderToken())){
// 表示是第一次提交
// 需要删除Token
redisTemplate.delete(key);
}else{
// 表示是重复提交
return responseVO;
}
}finally {
lock.unlock(); //释放锁
}
```
上面我们是通过Lock加锁的方式来实现Redis中的查询和删除操作的原子性我们同时可以使用Redis中脚本来实现原子性处理。
```
// 获取当前登录的用户信息
MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get();
// 1.验证是否重复提交 保证Redis中的token 的查询和删除是一个原子性操作
String key = OrderConstant.ORDER_TOKEN_PREFIX+":"+memberVO.getId();
String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0";
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList(key)
, vo.getOrderToken());
if(result == 0){
// 表示验证失败 说明是重复提交
return responseVO;
}
```
# 八、生成订单
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/1150ae4c71d94b92bfa1404921227016.png)
一个是需要生成订单信息一个是需要生成订单项信息。具体的核心代码为
```java
/**
* 创建订单的方法
* @param vo
* @return
*/
private OrderCreateTO createOrder(OrderSubmitVO vo) {
OrderCreateTO createTO = new OrderCreateTO();
// 创建订单
OrderEntity orderEntity = buildOrder(vo);
createTO.setOrderEntity(orderEntity);
// 创建OrderItemEntity 订单项
List<OrderItemEntity> orderItemEntitys = buildOrderItems(orderEntity.getOrderSn());
createTO.setOrderItemEntitys(orderItemEntitys);
return createTO;
}
/**
* 通过购物车中选中的商品来创建对应的购物项信息
* @return
*/
private List<OrderItemEntity> buildOrderItems(String orderSN) {
List<OrderItemEntity> orderItemEntitys = new ArrayList<>();
// 获取购物车中的商品信息 选中的
List<OrderItemVo> userCartItems = cartFeginService.getUserCartItems();
if(userCartItems != null && userCartItems.size() > 0){
// 统一根据SKUID查询出对应的SPU的信息
List<Long> spuIds = new ArrayList<>();
for (OrderItemEntity orderItemEntity : orderItemEntitys) {
if(!spuIds.contains(orderItemEntity.getSpuId())){
spuIds.add(orderItemEntity.getOrderId());
}
}
// 远程调用商品服务获取到对应的SPU信息
List<OrderItemSpuInfoVO> spuInfos = productService.getOrderItemSpuInfoBySpuId((Long[]) spuIds.toArray());
Map<Long, OrderItemSpuInfoVO> map = spuInfos.stream().collect(Collectors.toMap(OrderItemSpuInfoVO::getId, item -> item));
for (OrderItemVo userCartItem : userCartItems) {
// 获取到商品信息对应的 SPU信息
OrderItemSpuInfoVO spuInfo = map.get(userCartItem.getSpuId());
OrderItemEntity orderItemEntity = buildOrderItem(userCartItem,spuInfo);
// 绑定对应的订单编号
orderItemEntity.setOrderSn(orderSN);
orderItemEntitys.add(orderItemEntity);
}
}
return orderItemEntitys;
}
/**
* 根据一个购物车中的商品创建对应的 订单项
* @param userCartItem
* @return
*/
private OrderItemEntity buildOrderItem(OrderItemVo userCartItem,OrderItemSpuInfoVO spuInfo) {
OrderItemEntity entity = new OrderItemEntity();
// SKU信息
entity.setSkuId(userCartItem.getSkuId());
entity.setSkuName(userCartItem.getTitle());
entity.setSkuPic(userCartItem.getImage());
entity.setSkuQuantity(userCartItem.getCount());
List<String> skuAttr = userCartItem.getSkuAttr();
String skuAttrStr = StringUtils.collectionToDelimitedString(skuAttr, ";");
entity.setSkuAttrsVals(skuAttrStr);
// SPU信息
entity.setSpuId(spuInfo.getId());
entity.setSpuBrand(spuInfo.getBrandName());
entity.setCategoryId(spuInfo.getCatalogId());
entity.setSpuPic(spuInfo.getImg());
// 优惠信息 忽略
// 积分信息
entity.setGiftGrowth(userCartItem.getPrice().intValue());
entity.setGiftIntegration(userCartItem.getPrice().intValue());
return entity;
}
private OrderEntity buildOrder(OrderSubmitVO vo) {
// 创建OrderEntity
OrderEntity orderEntity = new OrderEntity();
// 创建订单编号
String orderSn = IdWorker.getTimeId();
orderEntity.setOrderSn(orderSn);
MemberVO memberVO = (MemberVO) AuthInterceptor.threadLocal.get();
// 设置会员相关的信息
orderEntity.setMemberId(memberVO.getId());
orderEntity.setMemberUsername(memberVO.getUsername());
// 根据收获地址ID获取收获地址的详细信息
MemberAddressVo memberAddressVo = memberFeginService.getAddressById(vo.getAddrId());
orderEntity.setReceiverCity(memberAddressVo.getCity());
orderEntity.setReceiverDetailAddress(memberAddressVo.getDetailAddress());
orderEntity.setReceiverName(memberAddressVo.getName());
orderEntity.setReceiverPhone(memberAddressVo.getPhone());
orderEntity.setReceiverPostCode(memberAddressVo.getPostCode());
orderEntity.setReceiverRegion(memberAddressVo.getRegion());
orderEntity.setReceiverProvince(memberAddressVo.getProvince());
// 设置订单的状态
orderEntity.setStatus(OrderConstant.OrderStateEnum.FOR_THE_PAYMENT.getCode());
return orderEntity;
}
```
锁定库存的操作需要操作ware仓储服务。
```java
/**
* 锁定库存的操作
* @param vo
* @return
*/
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVO vo) {
List<OrderItemVo> items = vo.getItems();
// 首先找到具有库存的仓库
List<SkuWareHasStock> collect = items.stream().map(item -> {
SkuWareHasStock skuWareHasStock = new SkuWareHasStock();
skuWareHasStock.setSkuId(item.getSkuId());
List<WareSkuEntity> wareSkuEntities = this.baseMapper.listHashStock(item.getSkuId());
skuWareHasStock.setWareSkuEntities(wareSkuEntities);
skuWareHasStock.setNum(item.getCount());
return skuWareHasStock;
}).collect(Collectors.toList());
// 尝试锁定库存
for (SkuWareHasStock skuWareHasStock : collect) {
Long skuId = skuWareHasStock.getSkuId();
List<WareSkuEntity> wareSkuEntities = skuWareHasStock.wareSkuEntities;
if(wareSkuEntities == null && wareSkuEntities.size() == 0){
// 当前商品没有库存了
throw new NoStockExecption(skuId);
}
// 当前需要锁定的商品的梳理
Integer count = skuWareHasStock.getNum();
Boolean skuStocked = false; // 表示当前SkuId的库存没有锁定完成
for (WareSkuEntity wareSkuEntity : wareSkuEntities) {
// 循环获取到对应的 仓库,然后需要锁定库存
// 获取当前仓库能够锁定的库存数
Integer canStock = wareSkuEntity.getStock() - wareSkuEntity.getStockLocked();
if(count <= canStock){
// 表示当前的skuId的商品的数量小于等于需要锁定的数量
Integer i = this.baseMapper.lockSkuStock(skuId,wareSkuEntity.getWareId(),count);
count = 0;
skuStocked = true;
}else{
// 需要锁定的库存大于 可以锁定的库存 就按照已有的库存来锁定
Integer i = this.baseMapper.lockSkuStock(skuId,wareSkuEntity.getWareId(),canStock);
count = count - canStock;
}
if(count <= 0 ){
// 表示所有的商品都锁定了
break;
}
}
if(count > 0){
// 说明库存没有锁定完
throw new NoStockExecption(skuId);
}
if(skuStocked == false){
// 表示上一个商品的没有锁定库存成功
throw new NoStockExecption(skuId);
}
}
return true;
}
```
没有库存或者锁定库存失败我们通过自定义的异常抛出
```java
/**
* 自定义异常:锁定库存失败的情况下产生的异常信
*/
public class NoStockExecption extends RuntimeException{
private Long skuId;
public NoStockExecption(Long skuId){
super("当前商品["+skuId+"]没有库存了");
this.skuId = skuId;
}
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
}
```
如果下订单操作成功(订单数据和订单项数据)我们就会操作锁库存的行为
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/afe0338ac81c4328aa67e1065ccc7318.png)
锁定库存失败通过抛异常来使订单操作回滚
![image.png](https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/1462/1652947845035/b19db8f073bd4f07be7652ac167ef026.png)