1. 搭建订单工程

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

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

13.订单、库存 - 图1

13.订单、库存 - 图2

13.订单、库存 - 图3

pom.xml:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <groupId>com.atguigu</groupId>
  7. <artifactId>gmall</artifactId>
  8. <version>0.0.1-SNAPSHOT</version>
  9. </parent>
  10. <groupId>com.atguigu</groupId>
  11. <artifactId>gmall-order</artifactId>
  12. <version>0.0.1-SNAPSHOT</version>
  13. <name>gmall-order</name>
  14. <description>谷粒商城订单系统</description>
  15. <properties>
  16. <java.version>1.8</java.version>
  17. </properties>
  18. <dependencies>
  19. <dependency>
  20. <groupId>com.atguigu</groupId>
  21. <artifactId>gmall-common</artifactId>
  22. <version>0.0.1-SNAPSHOT</version>
  23. </dependency>
  24. <dependency>
  25. <groupId>com.atguigu</groupId>
  26. <artifactId>gmall-pms-interface</artifactId>
  27. <version>0.0.1-SNAPSHOT</version>
  28. </dependency>
  29. <dependency>
  30. <groupId>com.atguigu</groupId>
  31. <artifactId>gmall-sms-interface</artifactId>
  32. <version>0.0.1-SNAPSHOT</version>
  33. </dependency>
  34. <dependency>
  35. <groupId>com.atguigu</groupId>
  36. <artifactId>gmall-wms-interface</artifactId>
  37. <version>0.0.1-SNAPSHOT</version>
  38. </dependency>
  39. <dependency>
  40. <groupId>com.atguigu</groupId>
  41. <artifactId>gmall-ums-interface</artifactId>
  42. <version>0.0.1-SNAPSHOT</version>
  43. </dependency>
  44. <dependency>
  45. <groupId>org.springframework.boot</groupId>
  46. <artifactId>spring-boot-starter-amqp</artifactId>
  47. </dependency>
  48. <dependency>
  49. <groupId>org.springframework.boot</groupId>
  50. <artifactId>spring-boot-starter-data-redis</artifactId>
  51. </dependency>
  52. <dependency>
  53. <groupId>org.springframework.boot</groupId>
  54. <artifactId>spring-boot-starter-web</artifactId>
  55. </dependency>
  56. <dependency>
  57. <groupId>org.springframework.boot</groupId>
  58. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  59. </dependency>
  60. <dependency>
  61. <groupId>com.alibaba.cloud</groupId>
  62. <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  63. </dependency>
  64. <dependency>
  65. <groupId>com.alibaba.cloud</groupId>
  66. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  67. </dependency>
  68. <dependency>
  69. <groupId>com.alibaba.cloud</groupId>
  70. <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
  71. </dependency>
  72. <dependency>
  73. <groupId>org.springframework.cloud</groupId>
  74. <artifactId>spring-cloud-starter-openfeign</artifactId>
  75. </dependency>
  76. <dependency>
  77. <groupId>org.springframework.cloud</groupId>
  78. <artifactId>spring-cloud-starter-zipkin</artifactId>
  79. </dependency>
  80. <dependency>
  81. <groupId>org.springframework.boot</groupId>
  82. <artifactId>spring-boot-starter-test</artifactId>
  83. <scope>test</scope>
  84. <exclusions>
  85. <exclusion>
  86. <groupId>org.junit.vintage</groupId>
  87. <artifactId>junit-vintage-engine</artifactId>
  88. </exclusion>
  89. </exclusions>
  90. </dependency>
  91. <dependency>
  92. <groupId>org.springframework.amqp</groupId>
  93. <artifactId>spring-rabbit-test</artifactId>
  94. <scope>test</scope>
  95. </dependency>
  96. </dependencies>
  97. <build>
  98. <plugins>
  99. <plugin>
  100. <groupId>org.springframework.boot</groupId>
  101. <artifactId>spring-boot-maven-plugin</artifactId>
  102. </plugin>
  103. </plugins>
  104. </build>
  105. </project>

1.1. 基础配置

bootstrap.yml:

spring:
  application:
    name: order-service
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848

application.yml:

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

启动类:

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class GmallOrderApplication {

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

}

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

13.订单、库存 - 图4

nginx配置:

13.订单、库存 - 图5

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

1.2. 统一获取登录信息

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

13.订单、库存 - 图6

LoginInterceptor:

@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:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

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

UserInfo:

@Data
public class UserInfo {

    private Long userId;
    private String userKey;
}

1.3. 线程池配置

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

13.订单、库存 - 图7

添加线程池配置类:

@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中添加线程池配置:

thread:
  pool:
    coreSize: 100
    maxSize: 500
    keepalive: 60
    blockQueueSize: 1000

2. 订单结算页

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

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

13.订单、库存 - 图8

13.订单、库存 - 图9

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

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

2.1. 数据模型

13.订单、库存 - 图10

OrderConfirmVO:

@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对象)

@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查询收货地址的数据接口:

@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方法:

/**
     * 根据用户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查询用户信息的方法(用户信息中包含积分信息):

13.订单、库存 - 图11

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

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

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

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

@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中添加如下方法:

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实体类:

13.订单、库存 - 图12

public interface GmallCartApi {

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

2.3. 编写feign接口

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

13.订单、库存 - 图13

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

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

13.订单、库存 - 图14

OrderController:

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

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

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

OrderService:

@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

响应数据:

{
    "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页面:

@GetMapping("confirm")
public String confirm(Model model){

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

trade.html总体结构:

13.订单、库存 - 图15

包含

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

3.1. 主内容

主内容结构如下:

13.订单、库存 - 图16

3.1.1. 收件人信息

页面模板:

<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处理:

    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. 支付方式

13.订单、库存 - 图17

html模板如下:

<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数据模型如下:

13.订单、库存 - 图18

3.1.3. 送货清单

<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数据模型处理:

13.订单、库存 - 图19

3.1.4. 积分优惠

html模板内容:

<div class="cardInfo">
    <div class="step-tit">
        <h5>使用优惠/抵用</h5>
    </div>
    <div class="step-cont">
        <span>购买积分:{{order.bounds}}</span>
    </div>
</div>

对应vue数据模型:

13.订单、库存 - 图20

3.2. 汇总信息

html模板如下:

<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数据模型:

13.订单、库存 - 图21

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

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

13.订单、库存 - 图22

3.3. 送货信息

html模板:

<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对应的数据模型:

13.订单、库存 - 图23

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

13.订单、库存 - 图24

3.4. 提交订单

13.订单、库存 - 图25

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

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

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

13.订单、库存 - 图26

4.1. 数据模型

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

@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的依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.11.2</version>
</dependency>

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

13.订单、库存 - 图27

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

@Data
public class SkuLockVO {

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

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

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

在添加WareSkuService的接口方法:

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

在WareSkuServiceImpl实现类中添加方法:

@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中添加方法:

@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中添加映射:

<?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中添加接口方法

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

4.2.2. 创建订单接口

搭建订单接口工程:

13.订单、库存 - 图28

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

13.订单、库存 - 图29

添加api接口:

public interface GmallOmsApi {

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

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

给OrderController添加如下方法:

@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接口添加如下方法:

OrderEntity saveOrder(OrderSubmitVO orderSubmitVO, Long userId);

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

@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添加方法:

/**
     * 提交订单返回订单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中添加方法:

@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的链接信息略。。。。

13.订单、库存 - 图30

@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. 库存解锁的监听器

13.订单、库存 - 图31

@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. 利用定时任务轮询数据库

     缺点:消耗系统内存,增加了数据库的压力,存在较大的时间误差
    
  2. rabbitmq的延时队列和死信队列结合(推荐)

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

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

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

13.订单、库存 - 图32

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

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

4.4.1. 配置延时队列

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

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

13.订单、库存 - 图33

@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实现类保存订单的最后:

13.订单、库存 - 图34

2.监听器处理消息

13.订单、库存 - 图35

@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接口添加接口方法:

@Mapper
public interface OrderMapper extends BaseMapper<OrderEntity> {

    public int closeOrder(String orderToken);

}

给OrderMapper.xml添加映射:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.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验库存并锁库存方法中,库存锁定成功,在返回之前发送一个延时消息。

13.订单、库存 - 图36

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

13.订单、库存 - 图37

@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章节中已实现