1. 搭建订单工程

完成购物车页面之后,点击购物车页面的“去结算”按钮,跳转到订单结算页。

接下来,先搭建订单系统:

1570802000914

1590836152299

1570959587143

pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.atguigu</groupId>
<artifactId>gmall</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.atguigu</groupId>
<artifactId>gmall-order</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gmall-order</name>
<description>谷粒商城订单系统</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>gmall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>gmall-pms-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>gmall-sms-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>gmall-wms-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>gmall-ums-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

1.1. 基础配置

bootstrap.yml:

1
2
3
4
5
6
7
spring:
application:
name: order-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848

application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
server:
port: 18091
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
zipkin:
base-url: http://localhost:9411
discovery-client-enabled: false
sender:
type: web
sleuth:
sampler:
probability: 1
redis:
host: 172.16.116.100
rabbitmq:
host: 172.16.116.100
virtual-host: /fengge
username: fengge
password: fengge
listener:
simple:
acknowledge-mode: manual
prefetch: 1
thymeleaf:
cache: false
feign:
sentinel:
enabled: true
logging:
level:
com.atguigu.gmall: debug

启动类:

1
2
3
4
5
6
7
8
9
10
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class GmallOrderApplication {

public static void main(String[] args) {
SpringApplication.run(GmallOrderApplication.class, args);
}

}

gmall-gateway网关配置添加订单路由:

1590908732590

nginx配置:

1591104423676

hosts文件中添加order.gmall.com映射。

1.2. 统一获取登录信息

参照gmall-cart购物车中的统一验证

1591103950152

LoginInterceptor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Component
public class LoginInterceptor implements HandlerInterceptor {

private static final ThreadLocal<UserInfo> THREAD_LOCAL = new ThreadLocal<>();

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

UserInfo userInfo = new UserInfo();
// 获取请求头信息
Long userId = Long.valueOf(request.getHeader("userId"));
userInfo.setUserId(userId);
// 传递给后续业务
THREAD_LOCAL.set(userInfo);
return true;
}

/**
* 封装了一个获取线程局部变量值的静态方法
*
* @return
*/
public static UserInfo getUserInfo() {
return THREAD_LOCAL.get();
}

/**
* 在视图渲染完成之后执行,经常在完成方法中释放资源
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

// 调用删除方法,是必须选项。因为使用的是tomcat线程池,请求结束后,线程不会结束。
// 如果不手动删除线程变量,可能会导致内存泄漏
THREAD_LOCAL.remove();
}
}

WebMvcConfig:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Autowired
private LoginInterceptor loginInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/order/pay/**");
}
}

UserInfo:

1
2
3
4
5
6
@Data
public class UserInfo {

private Long userId;
private String userKey;
}

1.3. 线程池配置

创建订单也是一个很复杂的业务功能,关系到很多远程调用,这里也可以通过异步调用优化业务。

1591104168103

添加线程池配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ThreadPoolConfig {

@Bean
public ThreadPoolExecutor threadPoolExecutor(
@Value("${thread.pool.coreSize}")Integer coreSize,
@Value("${thread.pool.maxSize}")Integer maxSize,
@Value("${thread.pool.keepalive}")Integer keepalive,
@Value("${thread.pool.blockQueueSize}")Integer blockQueueSize
){
return new ThreadPoolExecutor(coreSize, maxSize, keepalive, TimeUnit.SECONDS, new ArrayBlockingQueue<>(blockQueueSize));
}
}

在application.yml中添加线程池配置:

1
2
3
4
5
6
thread:
pool:
coreSize: 100
maxSize: 500
keepalive: 60
blockQueueSize: 1000

2. 订单结算页

购物车页面点击去结算按钮,应该发送请求到controller获取结算页需要的数据。

需要获取的数据模型,可以参照jd结算页,如下:

1570961558320

1570965591825

可以发现订单结算页,包含以下信息:

  1. 收货人信息:有更多地址,即有多个收货地址,其中有一个默认收货地址
  2. 支付方式:货到付款、在线支付,不需要后台提供
  3. 送货清单:配送方式(不做)及商品列表(根据购物车选中的skuId到数据库中查询)
  4. 发票:不做
  5. 优惠:查询用户领取的优惠券(不做)及可用积分(京豆)

2.1. 数据模型

1591199241196

OrderConfirmVO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class OrderConfirmVo {

// 收货地址列表
private List<UserAddressEntity> addresses;

// 送货清单,根据购物车页面传递过来的skuIds查询
private List<OrderItemVo> items;

// 用户的购物积分信息,ums_member表中的integration字段
private Integer bounds;

// 防重的唯一标识
private String orderToken;
}

注意:需要引入gmall-ums-interface依赖,并把UserAddressEntity对象移到gmall-ums-interface工程中去。

OrderItemVO:(参照Cart对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
public class OrderItemVo {

private Long skuId;
private String title;
private String defaultImage;
private BigDecimal price;
private Integer count;
private BigDecimal weight;
private List<SkuAttrValueEntity> saleAttrs; // 销售属性
private List<ItemSaleVo> sales; // 营销信息
private Boolean store = false; // 库存信息
}

2.2. 远程数据接口

结合数据模型,需要的远程接口如下:

  1. 根据当前用户的id查询收货地址(ums_user_address表)
  2. 查询用户选中的购物车记录
  3. 根据skuId查询sku(已有)
  4. 根据skuId查询销售属性(已有)
  5. 根据skuId查询营销信息(已有)
  6. 根据skuId查询库存信息(已有)
  7. 根据当前用户的id查询用户信息(包含积分)

2.2.1. 根据用户id查询收货地址

在gmall-ums中的UserAddressController中添加根据用户id查询收货地址的数据接口:

1
2
3
4
5
@GetMapping("user/{userId}")
public ResponseVo<List<UserAddressEntity>> queryAddressesByUserId(@RequestParam("userId")Long userId){
List<UserAddressEntity> addressEntities = this.userAddressService.list(new QueryWrapper<UserAddressEntity>().eq("user_id", userId));
return ResponseVo.ok(addressEntities);
}

在gmall-ums-interface工程中的GmallUmsApi添加feign方法:

1
2
3
4
5
6
7
/**
* 根据用户id查询收货地址
* @param userId
* @return
*/
@GetMapping("ums/useraddress/user/{userId}")
public ResponseVo<List<UserAddressEntity>> queryAddressesByUserId(@RequestParam("userId")Long userId);

2.2.2. 根据用户id查询用户积分

在gmall-ums中的UserController已有根据用户id查询用户信息的方法(用户信息中包含积分信息):

1586691829955

在gmall-ums-interface工程中的GmallUmsApi添加feign方法:

1
2
3
4
5
6
7
/**
* 根据id查询用户信息
* @param id
* @return
*/
@GetMapping("ums/user/{id}")
public ResponseVo<UserEntity> queryUserById(@PathVariable("id") Long id);

2.2.3. 获取登录用户勾选的购物车

在gmall-carts工程的CartController方法中添加如下方法:

1
2
3
4
5
6
@GetMapping("check/{userId}")
@ResponseBody
public ResponseVo<List<Cart>> queryCheckedCarts(@PathVariable("userId")Long userId){
List<Cart> carts = this.cartService.queryCheckedCarts(userId);
return ResponseVo.ok(carts);
}

在CartService中添加如下方法:

1
2
3
4
5
6
7
8
9
10
public List<Cart> queryCheckedCarts(Long userId) {

String key = KEY_PREFIX + userId;
BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
List<Object> cartJsons = hashOps.values();
if (CollectionUtils.isEmpty(cartJsons)) {
return null;
}
return cartJsons.stream().map(cartJson -> JSON.parseObject(cartJson.toString(), Cart.class)).filter(cart -> cart.getCheck()).collect(Collectors.toList());
}

创建gmall-cart-interface工程,并添加gmallCartApi接口及Cart实体类:

1586693981657

1
2
3
4
5
public interface GmallCartApi {

@GetMapping("check/{userId}")
public ResponseVo<List<Cart>> queryCheckedCarts(@PathVariable("userId")Long userId);
}

2.3. 编写feign接口

在gmall-order工程中添加feign接口:

1591199352461

1
2
3
@FeignClient("cart-service")
public interface GmallCartClient extends GmallCartApi {
}
1
2
3
@FeignClient("pms-service")
public interface GmallPmsClient extends GmallPmsApi {
}
1
2
3
@FeignClient("sms-service")
public interface GmallSmsClient extends GmallSmsApi {
}
1
2
3
@FeignClient("ums-service")
public interface GmallUmsClient extends GmallUmsApi {
}
1
2
3
@FeignClient("wms-service")
public interface GmallWmsClient extends GmallWmsApi {
}

2.4. 完成订单结算页数据查询接口

1591199503412

OrderController:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class OrderController {

@Autowired
private OrderService orderService;

@GetMapping("confirm")
public ResponseVo<OrderConfirmVo> confirm(){

OrderConfirmVo confirmVo = this.orderService.confirm();
return ResponseVo.ok(confirmVo);
}
}

OrderService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@Service
public class OrderService {

@Autowired
private GmallPmsClient pmsClient;

@Autowired
private GmallSmsClient smsClient;

@Autowired
private GmallUmsClient umsClient;

@Autowired
private GmallCartClient cartClient;

@Autowired
private GmallWmsClient wmsClient;

@Autowired
private StringRedisTemplate redisTemplate;

private static final String KEY_PREFIX = "order:token:";

@Autowired
private ThreadPoolExecutor threadPoolExecutor;

/**
* 订单确认页
* 由于存在大量的远程调用,这里使用异步编排做优化
* @return
*/
public OrderConfirmVo confirm() {
OrderConfirmVo confirmVo = new OrderConfirmVo();

UserInfo userInfo = LoginInterceptor.getUserInfo();
Long userId = userInfo.getUserId();

// 查询送货清单
CompletableFuture<List<Cart>> cartCompletableFuture = CompletableFuture.supplyAsync(() -> {
ResponseVo<List<Cart>> listResponseVo = this.cartClient.queryCheckedCarts(userId);
List<Cart> carts = listResponseVo.getData();
if (CollectionUtils.isEmpty(carts)) {
throw new OrderException("没有选中的购物车信息!");
}
return carts;
}, threadPoolExecutor);
CompletableFuture<Void> itemCompletableFuture = cartCompletableFuture.thenAcceptAsync(carts -> {
List<OrderItemVo> items = carts.stream().map(cart -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(cart.getSkuId());
orderItemVo.setCount(cart.getCount().intValue());
// 根据skuId查询sku
CompletableFuture<Void> skuCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<SkuEntity> skuEntityResponseVo = this.pmsClient.querySkuById(cart.getSkuId());
SkuEntity skuEntity = skuEntityResponseVo.getData();
orderItemVo.setTitle(skuEntity.getTitle());
orderItemVo.setPrice(skuEntity.getPrice());
orderItemVo.setDefaultImage(skuEntity.getDefaultImage());
orderItemVo.setWeight(new BigDecimal(skuEntity.getWeight()));
}, threadPoolExecutor);
// 查询销售属性
CompletableFuture<Void> saleAttrCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<List<SkuAttrValueEntity>> skuAttrValueResponseVo = this.pmsClient.querySkuAttrValuesBySkuId(cart.getSkuId());
List<SkuAttrValueEntity> skuAttrValueEntities = skuAttrValueResponseVo.getData();
orderItemVo.setSaleAttrs(skuAttrValueEntities);
}, threadPoolExecutor);

// 根据skuId查询营销信息
CompletableFuture<Void> saleCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<List<ItemSaleVo>> itemSaleVoResponseVo = this.smsClient.querySalesBySkuId(cart.getSkuId());
List<ItemSaleVo> itemSaleVos = itemSaleVoResponseVo.getData();
orderItemVo.setSales(itemSaleVos);
}, threadPoolExecutor);

// 根据 skuId查询库存信息
CompletableFuture<Void> storeCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<List<WareSkuEntity>> wareSkuResponseVo = this.wmsClient.queryWareSkusBySkuId(cart.getSkuId());
List<WareSkuEntity> wareSkuEntities = wareSkuResponseVo.getData();
if (!CollectionUtils.isEmpty(wareSkuEntities)) {
orderItemVo.setStore(wareSkuEntities.stream().anyMatch(wareSkuEntity -> wareSkuEntity.getStock() - wareSkuEntity.getStockLocked() > 0));
}
}, threadPoolExecutor);
CompletableFuture.allOf(skuCompletableFuture, saleAttrCompletableFuture, saleCompletableFuture, storeCompletableFuture).join();
return orderItemVo;
}).collect(Collectors.toList());
confirmVo.setItems(items);
}, threadPoolExecutor);

// 查询收货地址列表
CompletableFuture<Void> addressCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<List<UserAddressEntity>> addressesResponseVo = this.umsClient.queryAddressesByUserId(userId);
List<UserAddressEntity> addresses = addressesResponseVo.getData();
confirmVo.setAddresses(addresses);
}, threadPoolExecutor);

// 查询用户的积分信息
CompletableFuture<Void> boundsCompletableFuture = CompletableFuture.runAsync(() -> {
ResponseVo<UserEntity> userEntityResponseVo = this.umsClient.queryUserById(userId);
UserEntity userEntity = userEntityResponseVo.getData();
if (userEntity != null) {
confirmVo.setBounds(userEntity.getIntegration());
}
}, threadPoolExecutor);

// 防重的唯一标识
CompletableFuture<Void> tokenCompletableFuture = CompletableFuture.runAsync(() -> {
String timeId = IdWorker.getTimeId();
this.redisTemplate.opsForValue().set(KEY_PREFIX + timeId, timeId);
confirmVo.setOrderToken(timeId);
}, threadPoolExecutor);

CompletableFuture.allOf(itemCompletableFuture, addressCompletableFuture, boundsCompletableFuture, tokenCompletableFuture).join();

return confirmVo;
}

}

2.5. 测试订单确认页

访问:http://order.gmall.com/confirm

响应数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
{
"code": 0,
"msg": null,
"data": {
"addresses": [
{
"id": 1,
"userId": 2,
"name": "柳岩",
"phone": "13812345678",
"postCode": "200122",
"province": "上海",
"city": "上海市",
"region": "松江区",
"address": "大江商厦6层",
"defaultStatus": 1
},
{
"id": 2,
"userId": 2,
"name": "锋哥",
"phone": "18612345678",
"postCode": "200112",
"province": "北京",
"city": "北京市",
"region": "昌平区",
"address": "宏福科技园",
"defaultStatus": 0
}
],
"orderItems": [
{
"skuId": 5,
"title": "华为 HUAWEI Mate 30 5G 麒麟990 4000万超感光徕卡影像双超级快充8GB+128GB亮黑色5G全网通游戏手机",
"defaultImage": "https://ggmall.oss-cn-shanghai.aliyuncs.com/2020-03-21/a3a0a224-caad-4af2-8eec-f62c0e49a51f_e9ad9735fc3f0686.jpg",
"price": 5000.0000,
"count": 3,
"skuAttrValue": [
{
"id": 13,
"skuId": 5,
"attrId": 3,
"attrName": "机身颜色",
"attrValue": "黑色",
"sort": 0
},
{
"id": 14,
"skuId": 5,
"attrId": 4,
"attrName": "运行内存",
"attrValue": "8G",
"sort": 0
},
{
"id": 15,
"skuId": 5,
"attrId": 5,
"attrName": "机身存储",
"attrValue": "128G",
"sort": 0
}
],
"itemSaleVo": [
{
"type": "积分",
"desc": "成长积分赠送1000.0000,购物积分赠送1000.0000"
},
{
"type": "满减",
"desc": "满5000.0000减100.0000"
},
{
"type": "打折",
"desc": "满2件打0.90折"
}
],
"weight": 500,
"store": null
},
{
"skuId": 6,
"title": "华为 HUAWEI Mate 30 5G 麒麟990 4000万超感光徕卡影像双超级快充8GB+256GB亮黑色5G全网通游戏手机",
"defaultImage": "https://ggmall.oss-cn-shanghai.aliyuncs.com/2020-03-21/f053e068-e43d-46e7-8d67-4964abb240eb_802254cca298ae79 (1).jpg",
"price": 6000.0000,
"count": 2,
"skuAttrValue": [
{
"id": 16,
"skuId": 6,
"attrId": 3,
"attrName": "机身颜色",
"attrValue": "黑色",
"sort": 0
},
{
"id": 17,
"skuId": 6,
"attrId": 4,
"attrName": "运行内存",
"attrValue": "8G",
"sort": 0
},
{
"id": 18,
"skuId": 6,
"attrId": 5,
"attrName": "机身存储",
"attrValue": "256G",
"sort": 0
}
],
"itemSaleVo": [
{
"type": "积分",
"desc": "成长积分赠送2000.0000,购物积分赠送2000.0000"
},
{
"type": "满减",
"desc": "满6000.0000减1500.0000"
},
{
"type": "打折",
"desc": "满2件打8.00折"
}
],
"weight": 510,
"store": null
}
],
"bounds": 2000,
"orderToken": "202004122044380421249317500785655809"
}
}

3. 订单确认页面联调

改造controller方法跳转到trade.html页面:

1
2
3
4
5
6
7
@GetMapping("confirm")
public String confirm(Model model){

OrderConfirmVo confirmVo = this.orderService.confirm();
model.addAttribute("confirmVo", confirmVo);
return "trade";
}

trade.html总体结构:

1591256657805

包含

  1. 主内容:收件人信息、支付和送货清单、发票信息(略)、积分优惠
  2. 汇总信息:商品总数、总金额、返现、运费
  3. 送货信息:应付金额、送货地址等
  4. 提交订单按钮等

3.1. 主内容

主内容结构如下:

1591256947967

3.1.1. 收件人信息

页面模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="step-cont">
<div class="addressInfo">
<ul class="addr-detail">
<li class="addr-item">
<div class="choose-address" v-for="address in addresses" :key="address.id">
<div class="con name" :class="address.selected ? 'selected' : ''"><a href="javascript:;" @click="selectAddress(address)"><em>{{address.name}}</em><span
title="点击取消选择"></span></a></div>
<div class="con address">
<span class="place">{{address.province}}</span>
<span class="place">{{address.city}}</span>
<span class="place">{{address.region}}</span>
<span class="place">{{address.address}}</span>
<span class="phone">{{address.phone}}</span>
<span class="base" v-if="address.defaultStatus == 1">默认地址</span>
</div>
<div class="clearfix"></div>
</div>
</li>
</ul>
<!--确认地址-->
</div>
</div>

vuejs处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let app = new Vue({
el: "#app",
data: {
addresses: [[${confirmVo.addresses}]], // 收获地址列表
order: { // 收集订单提交信息
orderToken: [[${confirmVo.orderToken}]], // 订单编号
address: {}, // 用户选中的收货地址
bounds: [[${confirmVo.bounds}]], // 积分信息
items: [[${confirmVo.items}]] // 送货清单
}
},
created(){
// 处理默认地址选中状态
let addresses = this.addresses
addresses.forEach(address => {
address.selected = false
if (address.defaultStatus) {
address.selected = true
this.order.address = address
}
})
this.addresses = addresses
},
methods: {
selectAddress(address){ // 选择地址方法
this.addresses.forEach(address => { // 重置所有地址的选中状态
address.selected = false
})
address.selected = true
this.order.address = address
}
}
})

3.1.2. 支付方式

1591257723743

html模板如下:

1
2
3
4
5
6
7
<div class="step-cont">
<ul class="payType">
<li :class="order.payType == 1 ? 'selected' : ''" typeValue="1" @click="order.payType=1">在线支付<span title="点击取消选择"></span></li>
<li :class="order.payType == 2 ? 'selected' : ''" typeValue="0" @click="order.payType=2">货到付款<span title="点击取消选择"></span></li>
<input type="hidden" id="payType" value="1">
</ul>
</div>

vuejs数据模型如下:

1591257817329

3.1.3. 送货清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<div class="step-cont">
<ul class="send-detail">
<li>
<div class="sendType">
<span>配送方式:</span>
<ul>
<li>
<div class="con express">{{order.deliveryCompany}}</div>
<div class="con delivery">配送时间:预计8月10日(周三)09:00-15:00送达</div>
</li>
</ul>
</div>
<div class="sendGoods">
<span>商品清单:</span>
<ul class="yui3-g" v-for="item in order.items" :key="item.skuId">
<li class="yui3-u-1-6">
<span><img :src="item.defaultImage" width="150px" height="200px"/></span>
</li>
<li class="yui3-u-7-12">
<div class="desc">
<span>{{item.title}}</span><br>
<span v-for="saleAttr in item.saleAttrs">{{saleAttr.attrName}}:{{saleAttr.attrValue}}&emsp;</span>
</div>
<div class="seven">7天无理由退货</div>
</li>
<li class="yui3-u-1-12">
<div class="price">¥{{item.price.toFixed(2)}}</div>
</li>
<li class="yui3-u-1-12">
<div class="num">X{{item.count}}</div>
</li>
<li class="yui3-u-1-12">
<div class="exit" v-text="item.store ? '有货' : '无货'">有货</div>
</li>
</ul>
</div>
<div class="buyMessage">
<span>买家留言:</span>
<textarea placeholder="建议留言前先与商家沟通确认" class="remarks-cont"></textarea>
</div>
</li>
</ul>
</div>

vuejs数据模型处理:

1591257988402

3.1.4. 积分优惠

html模板内容:

1
2
3
4
5
6
7
8
<div class="cardInfo">
<div class="step-tit">
<h5>使用优惠/抵用</h5>
</div>
<div class="step-cont">
<span>购买积分:{{order.bounds}}</span>
</div>
</div>

对应vue数据模型:

1591258145873

3.2. 汇总信息

html模板如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="order-summary">
<div class="static fr">
<div class="list">
<span><i class="number">{{total}}</i>件商品,总商品金额</span>
<em class="allprice">¥{{order.totalPrice.toFixed(2)}}</em>
</div>
<div class="list">
<span>返现:</span>
<em class="money">{{returnMoney.toFixed(2)}}</em>
</div>
<div class="list">
<span>运费:</span>
<em class="transport">{{postFee.toFixed(2)}}</em>
</div>
</div>
</div>

金额都保留了两位小数。

vuejs数据模型:

1591258254015

totalPrice总价格:在created方法中进行了计算

total总件数:在computed计算属性中进行了计算

1591258410515

3.3. 送货信息

html模板:

1
2
3
4
5
6
7
8
9
<div class="clearfix trade">
<div class="fc-price">应付金额: <span class="price">¥{{payAmount.toFixed(2)}}</span></div>
<div class="fc-receiverInfo">
寄送至:
<span id="receive-address">{{order.address.city}}{{order.address.region}}{{order.address.address}}</span>
收货人:<span id="receive-name">{{order.address.name}}</span>
<span id="receive-phone">{{order.address.phone}}</span>
</div>
</div>

vuejs对应的数据模型:

1591258650403

收货地址在收件人信息已经处理过了,这里可以直接使用。这里还有个应付金额通过计算属性完成计算:

1591258748137

3.4. 提交订单

1591258852666

vuejs中提供了提交订单的方法:

1
2
3
4
5
6
7
8
9
10
11
submitOrder(){
axios.post('http://order.gmall.com/submit', this.order).then(({data}) => {
if (data.code === 0) {
window.location = 'http://payment.gmall.com/pay.html?orderToken=' + data.data
} else {
alert(data.data)
}
}).catch(({response}) => { // 提交订单失败,弹出错误信息
alert(response.data.message);
})
}

4. 提交订单

当用户点击提交订单按钮,应该收集页面数据提交到后台并生成订单数据。

请求路径:http://order.gmall.com/submit

需要把该路径添加到网关配置中,进行登录拦截

1591266058255

4.1. 数据模型

订单确认页,需要提交的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class OrderSubmitVO {

private String orderToken; // 防重
private BigDecimal totalPrice; // 总价,校验价格变化
private UserAddressEntity address; // 收货人信息
private Integer payType; // 支付方式
private String deliveryCompany; // 配送方式
private List<OrderItemVO> items; // 订单详情信息
private Integer bounds; // 积分信息

// 发票信息TODO

// 营销信息TODO
}

4.2. 远程接口

4.2.1. 验证库存并锁库存

为了保证验库存和锁库存的原子性,这里直接把验证和锁定库存封装到一个方法中,并在方法中使用分布式锁,防止多人同时锁库存。

gmall-wms工程的pom.xml中引入redisson的依赖:

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.2</version>
</dependency>

gmall-wms工程的config目录下需要添加redisson的配置文件,参照gmall-index工程:

1586699712599

给gmall-wms-interface工程添加实体类:

1
2
3
4
5
6
7
8
9
@Data
public class SkuLockVO {

private Long skuId; // 锁定的商品id
private Integer count; // 购买的数量
private Boolean lock; // 锁定状态
private Long wareSkuId; // 锁定成功时,锁定的仓库id
private String orderToken; // 方便以订单为单位缓存订单的锁定信息
}

gmall-wms工程的WareSkuController添加方法:

1
2
3
4
5
@PostMapping("check/lock")
public ResponseVo<List<SkuLockVO>> checkAndLock(@RequestBody List<SkuLockVO> lockVOS){
List<SkuLockVO> skuLockVOS = this.wareSkuService.checkAndLock(lockVOS);
return ResponseVo.ok(skuLockVOS);
}

在添加WareSkuService的接口方法:

1
List<SkuLockVO> checkAndLock(List<SkuLockVO> lockVOS);

在WareSkuServiceImpl实现类中添加方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Autowired
private WareSkuMapper wareSkuMapper;

@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
private RedissonClient redissonClient;

private static final String KEY_PREFIX = "store:lock:";

@Transactional
@Override
public List<SkuLockVO> checkAndLock(List<SkuLockVO> lockVOS) {

if (CollectionUtils.isEmpty(lockVOS)) {
return null;
}

lockVOS.forEach(lockVO -> {
// 每一个商品验库存并锁库存
this.checkLock(lockVO);
});

// 如果有一个商品锁定失败了,所有已经成功锁定的商品要解库存
List<SkuLockVO> successLockVO = lockVOS.stream().filter(SkuLockVO::getLock).collect(Collectors.toList());
List<SkuLockVO> errorLockVO = lockVOS.stream().filter(skuLockVO -> !skuLockVO.getLock()).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(errorLockVO)) {
successLockVO.forEach(lockVO -> {
this.wareSkuMapper.unlockStock(lockVO.getWareSkuId(), lockVO.getCount());
});
return lockVOS;
}

// 把库存的锁定信息保存到redis中,以方便将来解锁库存
String orderToken = lockVOS.get(0).getOrderToken();
this.redisTemplate.opsForValue().set(KEY_PREFIX + orderToken, JSON.toJSONString(lockVOS));

return null; // 如果都锁定成功,不需要展示锁定情况
}

private void checkLock(SkuLockVO skuLockVO){
RLock fairLock = this.redissonClient.getFairLock("lock:" + skuLockVO.getSkuId());
fairLock.lock();
// 验库存
List<WareSkuEntity> wareSkuEntities = this.wareSkuMapper.checkStock(skuLockVO.getSkuId(), skuLockVO.getCount());
if (CollectionUtils.isEmpty(wareSkuEntities)) {
skuLockVO.setLock(false); // 库存不足,锁定失败
fairLock.unlock(); // 程序返回之前,一定要释放锁
return ;
}
// 锁库存。一般会根据运输距离,就近调配。这里就锁定第一个仓库的库存
if(this.wareSkuMapper.lockStock(wareSkuEntities.get(0).getId(), skuLockVO.getCount()) == 1){
skuLockVO.setLock(true); // 锁定成功
skuLockVO.setWareSkuId(wareSkuEntities.get(0).getId());
} else {
skuLockVO.setLock(false);
}
fairLock.unlock();
}

在WareSkuMapper中添加方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper
public interface WareSkuMapper extends BaseMapper<WareSkuEntity> {

// 验库存方法
List<WareSkuEntity> checkStock(@Param("skuId") Long skuId, @Param("count") Integer count);

// 锁库存
int lockStock(@Param("id") Long id, @Param("count") Integer count);

// 解库存
int unlockStock(@Param("id") Long id, @Param("count") Integer count);

}

在WareSkuMapper对应的xml中添加映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.gmall.wms.mapper.WareSkuMapper">

<select id="checkStock" resultMap="WareSkuEntity">
select * from wms_ware_sku where stock-stock_locked>=#{count} and sku_id=#{skuId}
</select>

<update id="lockStock">
update wms_ware_sku set stock_locked = stock_locked + #{count} where id = #{id}
</update>

<update id="unlockStock">
update wms_ware_sku set stock_locked = stock_locked - #{count} where id = #{id}
</update>

</mapper>

在gmall-wms-interface中的GmallWmsApi中添加接口方法

1
2
@PostMapping("wms/waresku/check/lock")
public ResponseVo<List<SkuLockVO>> checkAndLock(@RequestBody List<SkuLockVO> lockVOS);

4.2.2. 创建订单接口

搭建订单接口工程:

1571288035143

把OrderSubmitVO和OrderItemVO数据模型移动到gmall-oms-interface中

1571303781294

添加api接口:

1
2
3
4
5
public interface GmallOmsApi {

@PostMapping("oms/order/{userId}")
public ResponseVo<OrderEntity> saveOrder(@RequestBody OrderSubmitVO orderSubmitVO, @PathVariable("userId")Long userId);
}

该api接口对应的实现如下:

给OrderController添加如下方法:

1
2
3
4
5
6
7
@PostMapping("{userId}")
public ResponseVo<OrderEntity> saveOrder(@RequestBody OrderSubmitVO orderSubmitVO, @PathVariable("userId")Long userId){

OrderEntity orderEntity = this.orderService.saveOrder(orderSubmitVO, userId);

return ResponseVo.ok(orderEntity);
}

给OrderService接口添加如下方法:

1
OrderEntity saveOrder(OrderSubmitVO orderSubmitVO, Long userId);

给OrderServiceImpl实现类添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Autowired
private OrderItemMapper orderItemMapper;

@Transactional
@Override
public OrderEntity saveOrder(OrderSubmitVO orderSubmitVO, Long userId) {

// 保存订单
OrderEntity orderEntity = new OrderEntity();
BeanUtils.copyProperties(orderSubmitVO, orderEntity);
orderEntity.setOrderSn(orderSubmitVO.getOrderToken());
orderEntity.setUserId(userId);
orderEntity.setCreateTime(new Date());
orderEntity.setTotalAmount(orderSubmitVO.getTotalPrice());
orderEntity.setPayAmount(orderSubmitVO.getTotalPrice());
orderEntity.setPayType(orderSubmitVO.getPayType());
orderEntity.setStatus(0);
orderEntity.setDeliveryCompany(orderSubmitVO.getDeliveryCompany());

this.save(orderEntity);

// 保存订单详情
List<OrderItemVO> orderItems = orderSubmitVO.getItems();
for (OrderItemVO orderItem : orderItems) {
OrderItemEntity itemEntity = new OrderItemEntity();

// 订单信息
itemEntity.setOrderId(orderEntity.getId());
itemEntity.setOrderSn(orderEntity.getOrderSn());

// 需要远程查询spu信息 TODO

// 设置sku信息
itemEntity.setSkuId(orderItem.getSkuId());
itemEntity.setSkuName(orderItem.getTitle());
itemEntity.setSkuPrice(orderItem.getPrice());
itemEntity.setSkuQuantity(orderItem.getCount().intValue());

//需要远程查询优惠信息 TODO

this.orderItemMapper.insert(itemEntity);
}

return orderEntity;
}

4.3. 提交订单基本代码实现

4.3.1. 下单的基本实现

提交订单分以下几个基本步骤:

  1. 验证令牌防止重复提交
  2. 验证价格
  3. 验证库存,并锁定库存
  4. 生成订单
  5. 删购物车中对应的记录(消息队列)

OrderController添加方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 提交订单返回订单id
* @param orderSubmitVO
* @return
*/
@PostMapping("submit")
@ResponseBody
public ResponseVo<Object> submit(@RequestBody OrderSubmitVo submitVo){

OrderEntity orderEntity = this.orderService.submit(submitVo);
return ResponseVo.ok(orderEntity.getOrderSn());
}

OrderService中添加方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@Autowired
private GmallOmsClient omsClient;

@Autowired
private RabbitTemplate rabbitTemplate;

public OrderEntity submit(OrderSubmitVO submitVO) {

// 1.防重
String orderToken = submitVO.getOrderToken();
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
Boolean flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(KEY_PREFIX + orderToken), orderToken);
if (!flag) {
throw new OrderException("您多次提交过快,请稍后再试!");
}

// 2.验价
BigDecimal totalPrice = submitVO.getTotalPrice(); // 获取页面上的价格
List<OrderItemVO> items = submitVO.getItems(); // 订单详情
if (CollectionUtils.isEmpty(items)) {
throw new OrderException("您没有选中的商品,请选择要购买的商品!");
}
// 遍历订单详情,获取数据库价格,计算实时总价
BigDecimal currentTotalPrice = items.stream().map(item -> {
ResponseVo<SkuEntity> skuEntityResp = this.pmsClient.querySkuById(item.getSkuId());
SkuEntity skuEntity = skuEntityResp.getData();
if (skuEntity != null) {
return skuEntity.getPrice().multiply(item.getCount());
}
return new BigDecimal(0);
}).reduce((t1, t2) -> t1.add(t2)).get();
if (totalPrice.compareTo(currentTotalPrice) != 0) {
throw new OrderException("页面已过期,刷新后再试!");
}

// 3.验库存并锁库存
List<SkuLockVO> lockVOS = items.stream().map(item -> {
SkuLockVO skuLockVO = new SkuLockVO();
skuLockVO.setSkuId(item.getSkuId());
skuLockVO.setCount(item.getCount().intValue());
skuLockVO.setOrderToken(submitVO.getOrderToken());
return skuLockVO;
}).collect(Collectors.toList());
ResponseVo<List<SkuLockVO>> skuLockResp = this.wmsClient.checkAndLock(lockVOS);
List<SkuLockVO> skuLockVOS = skuLockResp.getData();
if (!CollectionUtils.isEmpty(skuLockVOS)){
throw new OrderException("手慢了,商品库存不足:" + JSON.toJSONString(skuLockVOS));
}

// order:此时服务器宕机

// 4.下单
UserInfo userInfo = LoginInterceptor.getUserInfo();
Long userId = userInfo.getUserId();
OrderEntity orderEntity = null;
try {
ResponseVo<OrderEntity> orderEntityResp = this.omsClient.saveOrder(submitVO, userId);// feign(请求,响应)超时
orderEntity = orderEntityResp.getData();
} catch (Exception e) {
e.printStackTrace();
// 如果订单创建失败,立马释放库存
this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "stock.unlock", orderToken);
}

// 5.删除购物车。异步发送消息给购物车,删除购物车
Map<String, Object> map = new HashMap<>();
map.put("userId", userId);
List<Long> skuIds = items.stream().map(OrderItemVO::getSkuId).collect(Collectors.toList());
map.put("skuIds", JSON.toJSONString(skuIds));
this.rabbitTemplate.convertAndSend("ORDER-EXCHANGE", "cart.delete", map);

return orderEntity;
}

4.3.2. 删除购物车监听器

引入rabbitmq依赖及配置rabbitmq的链接信息略。。。。

1571304547666

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Component
public class CartListener {

@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
private GmallPmsClient pmsClient;

private static final String KEY_PREFIX = "cart:info:";

@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "order-cart-queue", durable = "true"),
exchange = @Exchange(value = "order-exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC),
key = {"cart.delete"}
))
public void deleteCart(Map<String, Object> map, Channel channel, Message message) throws IOException {

try {
Long userId = (Long)map.get("userId");
String skuIdString = map.get("skuIds").toString();
List<String> skuIds = JSON.parseArray(skuIdString, String.class);

BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(KEY_PREFIX + userId);
hashOps.delete(skuIds.toArray());

channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
if (message.getMessageProperties().getRedelivered()){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
}

4.3.3. 库存解锁的监听器

1571304682491

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Component
public class StockListener {

@Autowired
private WareSkuMapper wareSkuMapper;

@Autowired
private StringRedisTemplate redisTemplate;

private static final String KEY_PREFIX = "stock:lock:";

@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "order-stock-queue", durable = "true"),
exchange = @Exchange(value = "order-exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC),
key = {"stock.unlock"}
))
public void listener(String orderToken, Channel channel, Message message) throws IOException {

try {
// 获取redis中该订单的锁定库存信息
String json = this.redisTemplate.opsForValue().get(KEY_PREFIX + orderToken);
if (StringUtils.isNotBlank(json)){
// 反序列化获取库存的锁定信息
List<SkuLockVo> skuLockVos = JSON.parseArray(json, SkuLockVo.class);
// 遍历并解锁库存信息
skuLockVos.forEach(skuLockVo -> {
this.wareSkuMapper.unlock(skuLockVo.getWareSkuId(), skuLockVo.getCount());
});
// 删除redis中库存锁定信息
this.redisTemplate.delete(KEY_PREFIX + orderToken);
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
if (message.getMessageProperties().getRedelivered()){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
}

4.4. 延时队列定时关单

如果用户下单后一直不支付,库存处于锁定状态,陷入店家商品卖不出,买家无法购买的情况。所以,需要定时关单。

常用解决方案:

  1. 利用定时任务轮询数据库

​ 缺点:消耗系统内存,增加了数据库的压力,存在较大的时间误差

  1. rabbitmq的延时队列和死信队列结合(推荐)

声明延时队列的方式,使用如下参数:

1
2
3
arguments.put("x-dead-letter-exchange", "dlx名称");
arguments.put("x-dead-letter-routing-key", "routingkey");
arguments.put("x-message-ttl", "过期时间");

使用延时队列完成定时关单的流程如下:

1571363284889

  1. 订单创建成功,发送消息到创建订单的路由
  2. 创建订单的路由转发消息给延时队列,延时队列的延时时间就是订单从创建到支付过程,允许的最大等待时间。延时队列不能有消费者(即消息不能被消费)
  3. 延时时间一到,消息被转入DLX(死信路由)
  4. 死信路由把死信消息转发给死信队列
  5. 订单系统监听死信队列,获取到死信消息后,执行关单解库存操作

为了防止在gmall-oms中订单创建成功,而gmall-order中获取响应时网络故障,或删除购物车时失败导致的关单消息发送失败,我们应该在gmall-oms创建订单的方法中发送消息,和订单创建使用一个本地事务,要么都成功要么都失败。

4.4.1. 配置延时队列

在gmall-oms工程中添加rabbitmq的依赖并添加rabbitmq的配置, 略。。。

在gmall-oms工程的config目录下添加rabbitmq的配置类:

1587045379673

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Configuration
public class RabbitMqConfig {

// 声明延时交换机:order-exchange

// 声明延时队列
@Bean
public Queue ttlQueue(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 60000);
arguments.put("x-dead-letter-exchange", "order-exchange");
arguments.put("x-dead-letter-routing-key", "order.dead");
return new Queue("order-ttl-queue", true, false, false, arguments);
}

// 把延时队列绑定到交换机
@Bean
public Binding ttlBinding(){

return new Binding("order-ttl-queue", Binding.DestinationType.QUEUE, "order-exchange", "order.create", null);
}

// 声明死信交换机:order-exchange

// 声明死信队列
@Bean
public Queue deadQueue(){
return new Queue("order-dead-queue", true, false, false);
}

// 把死信队列绑定到死信交换机
@Bean
public Binding binding(){
return new Binding("order-dead-queue", Binding.DestinationType.QUEUE, "order-exchange", "order.dead", null);
}
}

4.4.2. 实现定时关单

1.发送延时消息

在订单创建成功后,发送消息到延时队列。在gmall-oms工程中的OrderServiceImpl实现类保存订单的最后:

1587045623910

2.监听器处理消息

1571369252203

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
public class OrderListener {

@Autowired
private OrderMapper orderMapper;

@Autowired
private RabbitTemplate rabbitTemplate;

@RabbitListener(queues = {"order-dead-queue"})
public void closeOrder(String orderToken, Channel channel, Message message) throws IOException {

try {
// 更新订单状态,更新为4
// 执行关单操作,如果返回值是1,说明执行关单成功,再去执行解锁库存的操作;如果返回值是0,是说明执行关单失败
if(this.orderMapper.closeOrder(orderToken) == 1){
// 解锁库存
this.rabbitTemplate.convertAndSend("order-exchange", "stock.unlock", orderToken);
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e){
if (message.getMessageProperties().getRedelivered()){
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
}

给OrderMapper接口添加接口方法:

1
2
3
4
5
6
@Mapper
public interface OrderMapper extends BaseMapper<OrderEntity> {

public int closeOrder(String orderToken);

}

给OrderMapper.xml添加映射:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.gmall.oms.mapper.OrderMapper">

<update id="closeOrder">
update oms_order set `status`=4 where order_sn=#{orderToken} and `status`=0
</update>
</mapper>

4.4.3. 定时解锁库存

在下单的5个步骤中:如果验库存并锁库存成功,还没来得及执行下单操作,服务器就宕机了,怎么办?

此时,库存已经被锁住,而下单操作还没有执行,这部分锁定的库存无法解锁。所以库存也需要像订单一样定时解锁。

锁定成功要定时解锁库存,在gmall-wms工程WareSkuServiceImpl的checkAndLock验库存并锁库存方法中,库存锁定成功,在返回之前发送一个延时消息。

1591265154797

在gmall-wms工程中添加RabbitMqConfig的配置类,配置延时队列及死信队列

1591265567821

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class RabbitMqConfig {

// 声明延时交换机:借用order-exchange

// 声明延时队列
@Bean
public Queue ttlQueue(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 90000);
arguments.put("x-dead-letter-exchange", "order-exchange");
arguments.put("x-dead-letter-routing-key", "stock.unlock");
return new Queue("stock-ttl-queue", true, false, false, arguments);
}

// 把延时队列绑定到交换机
@Bean
public Binding ttlBinding(){

return new Binding("stock-ttl-queue", Binding.DestinationType.QUEUE, "order-exchange", "stock.ttl", null);
}

// 声明死信交换机:借用order-exchange

// 声明死信队列:借用order-stock-queue

// 把死信队列绑定到死信交换机:注解中已绑定
}

解锁库存的监听器在4.3.3章节中已实现