1. Day14 订单处理

2. 超时未支付订单处理

超过限定时间并未支付的订单,我们需要进行超时订单的处理先调用微信支付api,查询该订单的支付状态。如果未支付调用关闭订单的api,并修改订单状态为已关闭,并回滚库存数。如果该订单已经支付,则做补偿操作(修改订单状态和记录)。

2.1. 延迟消息队列(死信队列)

延迟消息队列,就是消息的生产者发送的消息并不会立刻被消费,而是在设定的时间之后才可以消费。

我们可以在订单创建时发送一个延迟消息,消息为订单号,系统会在限定时间之后取出这个消息,然后查询订单的支付状态,根据结果做出相应的处理。

rabbitmq中并没有提供延迟队列功能 但是我们可以通过使用 TTL+死信队列 组合实现延迟队列的效果

2.1.1. 消息的TTL(Time To Live)

消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。

2.1.2. 死信交换器 Dead Letter Exchanges

一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。

(1) 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false(不把消息重新放入原目标队列.requeue=false)。也就是说不会被再次放在队列里,被其他消费者使用。

(2)上面的消息的TTL到了,消息过期了

(3)队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信交换机上。

2.1.3. 创建死信队列

进入MQ web管理页面http://192.168.130.128:15672/

  1. 创建死信交换器 exchange.ordertimeoutfanout
    14. Day14 订单处理 - 图1

  2. 创建队列queue.ordertimeout
    14. Day14 订单处理 - 图2

  3. 建立死信交换器 exchange.ordertimeout 与队列queue.ordertimeout 之间的绑定
    14. Day14 订单处理 - 图3

  4. 创建队列queue.ordercreate,Arguments添加

  1. `x-message-ttl`=`10000`
  2. `x-dead-letter-exchange` `exchange.ordertimeout`
  3. ![](https://cdn.jsdelivr.net/gh/Iekrwh/images/md-images/1648290034111.png#alt=1648290034111.png)
  1. 测试:向queue.ordercreate队列添加消息,等待10秒后消息从queue.ordercreate队列消失

2.2. 代码实现

14. Day14 订单处理 - 图4

2.2.1. 微信支付-查询订单

在changgou_service_pay的pay下controller包的WxPayController类添加查询订单方法

  1. //基于微信支付查询订单
  2. @GetMapping("/query/{orderId}")
  3. public Result queryOrder(@PathVariable("orderId") String orderId) {
  4. Map map = wxPayService.queryOrder(orderId);
  5. return new Result(true, StatusCode.OK, "查询订单成功", map);
  6. }

将查询订单方法暴露给feign

在changgou_service_pay_api里的feign包的PayFeign添加查询方法

  1. //基于微信支付查询订单
  2. @GetMapping("/wxpay/query/{orderId}")
  3. public Result queryOrder(@PathVariable("orderId") String orderId);

2.2.2. 微信支付-关闭订单

在changgou_service_pay的pay下service包的WxpayService接口 添加关闭订单方法定义

  1. //基于微信支付关闭订单
  2. Map closeOrder(String orderId);

实现方法 调用微信支付sdk中的closeOrder方法

  1. //基于微信支付关闭订单
  2. @Override
  3. public Map closeOrder(String orderId) {
  4. try {
  5. Map<String,String> map = new HashMap();
  6. map.put("out_trade_no",orderId);
  7. Map<String, String> resultMap = wxPay.closeOrder(map);
  8. return resultMap;
  9. }catch (Exception e){
  10. e.printStackTrace();
  11. return null;
  12. }
  13. }

collection层

  1. //基于微信支付关闭订单
  2. @PutMapping("/close/{orderID}")
  3. public Result closeOrder(@PathVariable("orderId") String orderID){
  4. Map map = wxPayService.closeOrder(orderID);
  5. return new Result(true,StatusCode.OK,"关闭订单成功",map);
  6. }

同样以feign形式声明

  1. //基于微信支付关闭订单
  2. @PutMapping("/wxpay/close/{orderID}")
  3. public Result closeOrder(@PathVariable("orderId") String orderID);

2.2.3. 回滚库存

在changgou_service_goods包dao层的SkuMapper实体接口 添加回滚库存的自定义sql

  1. //回滚库存(增加库存并扣减销量)
  2. @Update("update tb_suk set num=num+#{num},sale_num=sale_num-#{num} where id=#{skuId}")
  3. void resumeStockNum(@Param("skuId") String skuId,@Param("num") Integer num);

在SkuService中添加方法声明

  1. //回滚库存
  2. void resumeStockNum(String skuId, Integer num);

impl实现方法

  1. //回滚库存
  2. @Override
  3. @Transactional
  4. public void resumeStockNum(String skuId, Integer num) {
  5. skuMapper.resumeStockNum(skuId,num);
  6. }

collection层

  1. //回滚库存
  2. @RequestMapping("/resumeStockNum")
  3. public Result resumeStockNum(@RequestParam("skuId") String skuId,@RequestParam("num") Integer num){
  4. skuService.resumeStockNum(skuId,num);
  5. return new Result(true,StatusCode.OK,"回滚库存成功");
  6. }

在changgou_service_goods_api包下声明feign

  1. //回滚库存
  2. @RequestMapping("/sku/resumeStockNum")
  3. public Result resumeStockNum(@RequestParam("skuId") String skuId,@RequestParam("num") Integer num);

2.2.4. 消息队列发送和接受

在changgou_service_order项目下的service.impl包下的OrderServiceImpl

注入rabbitmq模块

  1. @Autowired
  2. private RabbitTemplate rabbitTemplate;

在add方法最后一行添加代码

  1. //发送延时消息
  2. rabbitTemplate.convertAndSend("","queue.ordercreate",orderId);

14. Day14 订单处理 - 图5

在此项目的listener包添加OrderTimeOutListener

  1. @Component
  2. public class OrderTimeOutListener {
  3. @Autowired
  4. private OrderService orderService;
  5. //监听死信队列
  6. @RabbitListener(queues = "queue.ordertimeout")
  7. public void receiveCloseOrderMessage(String orderId){
  8. System.out.println("接受到关闭订单的消息"+orderId);
  9. try {
  10. //调用业务层完成关闭订单操作
  11. orderService.closeOrder(orderId);
  12. } catch (Exception e){
  13. e.printStackTrace();
  14. }
  15. }
  16. }

在OrderService 声明closeOrder方法

  1. //关闭订单
  2. void closeOrder(String oderId);

在changgou_server_order添加 pay的feign依赖导入 在pom.xml添加依赖

  1. <dependency>
  2. <groupId>com.changgou</groupId>
  3. <artifactId>changgou_service_pay_api</artifactId>
  4. <version>1.0-SNAPSHOT</version>
  5. </dependency>

在启动类中扫描feign包注解添加pay包

  1. @EnableFeignClients(basePackages = {"com.changgou.goods.feign" , "com.changgou.pay.feign"})

impl实现接口声明的方法

  1. @Autowired
  2. private PayFeign payFeign;
  3. @Override
  4. @Transactional
  5. public void closeOrder(String orderId) {
  6. System.out.println("关闭订单的业务开启" + orderId);
  7. Order order = orderMapper.selectByPrimaryKey(orderId);
  8. if (order == null) {
  9. throw new RuntimeException("订单不存在");
  10. }
  11. //1.根据订单id查询mysql的订单信息 判断订单是否存在 判断订单的支付状态
  12. //判断是否为已支付
  13. if (!"0".equals(order.getPayStatus())) {
  14. System.out.println("当前订单已支付,无需关闭");
  15. return;
  16. }
  17. System.out.println("关闭订单校验通过" + orderId);
  18. //2.基于微信查询订单信息
  19. Map wxQueryMap = (Map) payFeign.queryOrder(orderId).getData();
  20. System.out.println("查询微信支付订单成功" + wxQueryMap);
  21. //如果订单支付状态为已支付,进行数据补偿
  22. if ("SUCCESS".equals(wxQueryMap.get("trade_state"))) {
  23. String transactionId = String.valueOf(wxQueryMap.get("transaction_id"));
  24. this.updatePayStatus(orderId, transactionId); //订单支付成功 进行减少库存 添加销量操作
  25. System.out.println("完成数据补偿");
  26. }
  27. //如果订单支付状态为未支付 则回滚mysql中的订单信息 新增订单日志 恢复商品的库存 基于微信支付关闭订单
  28. if ("NOTPAY".equals(wxQueryMap.get("trade_state"))) {
  29. System.out.println("执行关闭操作");
  30. order.setUpdateTime(new Date());
  31. order.setOrderStatus("4"); //订单已关闭
  32. orderMapper.updateByPrimaryKeySelective(order); //修改mysql订单信息
  33. //订单日志
  34. OrderLog orderLog = new OrderLog();
  35. orderLog.setId(idWorker.nextId() + "");
  36. orderLog.setOperater("system");
  37. orderLog.setOperateTime(new Date());
  38. orderLog.setOrderStatus("4"); //订单已关闭
  39. orderLog.setOrderId(orderId);
  40. orderLogMapper.insert(orderLog);//写入日志
  41. //恢复商品库存
  42. OrderItem _orderItem = new OrderItem();
  43. _orderItem.setOrderId(orderId);
  44. List<OrderItem> orderItemList = orderItemMapper.select(_orderItem); //根据orderId查询orderItem
  45. for (OrderItem orderItem : orderItemList) {
  46. skuFeign.resumeStockNum(orderItem.getSkuId(), orderItem.getNum());
  47. }
  48. //基于微信关闭订单
  49. payFeign.closeOrder(orderId);
  50. }
  51. }

3. 订单批量发货

在changgou_service_order项目的Service包下的OrderService类添加方法声明

  1. //批量发货
  2. void batchSend(List<Order> orders);

OrderServiceImpl实现该方法

  1. @Override
  2. @Transactional
  3. public void batchSend(List<Order> orders) {
  4. //判断每个订单和物流公司的值是否存在
  5. for (Order order : orders) {
  6. if (order.getId() == null){
  7. throw new RuntimeException("订单号不存在")
  8. }
  9. if (order.getShippingCode() == null || order.getShippingName() == null) {
  10. throw new RuntimeException("请输入快递单号或物流公司名称");
  11. }
  12. }
  13. //进行订单状态的校验
  14. for (Order order : orders) {
  15. Order order1 = orderMapper.selectByPrimaryKey(order.getId());
  16. //发货状态 是否存在
  17. if (!"0".equals(order1.getConsignStatus()) || !"1".equals(order1.getConsignStatus())){
  18. throw new RuntimeException("订单状态不合法");
  19. }
  20. }
  21. //修改订单状态为已发货
  22. for (Order order : orders) {
  23. order.setOrderStatus("2"); // 已发货状态
  24. order.setConsignStatus("1"); //已发货
  25. order.setConsignTime(new Date());
  26. order.setUpdateTime(new Date());
  27. orderMapper.updateByPrimaryKeySelective(order); //写入数据
  28. //记录订单日志
  29. OrderLog orderLog =new OrderLog();
  30. orderLog.setId(String.valueOf(idWorker.nextId()));
  31. orderLog.setOperater("admin");
  32. orderLog.setOperateTime(new Date());
  33. orderLog.setOrderId(order.getId());
  34. orderLog.setOrderStatus("2");
  35. orderLog.setConsignStatus("1");
  36. orderLogMapper.insert(orderLog); //写入日志
  37. }
  38. }

OrderController新增方法

  1. @PostMapping("/batchSend")
  2. public Result batchSend(@RequestBody List<Order> orders){
  3. orderService.batchSend(orders);
  4. return new Result(true,StatusCode.OK,"发货成功");
  5. }

3.1. 对接第三方物流查询

第三方的物流系统。比较常用的有菜鸟物流、快递鸟等。我们这里推荐使用快递鸟 http://www.kdniao.com

4. 确认收货和自动收货

当物流公司将货物送到了用户收货地址之后,需要用户点击确认收货,当用户点击了确认收货之后,会修改订单状态为已完成

4.1. 手动确认收货

在changgou_service_order项目的Service包下的OrderService类添加方法声明

  1. //手动确认收货
  2. void confirmTask(String orderId,String operator);

impl实现

  1. //手动确认收货
  2. @Override
  3. public void confirmTask(String orderId, String operator) {
  4. Order order = orderMapper.selectByPrimaryKey(orderId);
  5. if (order == null) {
  6. throw new RuntimeException("订单不存在");
  7. }
  8. if (!"1".equals(order.getConsignStatus())){
  9. throw new RuntimeException("订单未发货");
  10. }
  11. order.setConsignStatus("2"); //已送达
  12. order.setOrderStatus("3"); //已完成
  13. order.setUpdateTime(new Date());
  14. order.setEndTime(new Date());
  15. orderMapper.updateByPrimaryKeySelective(order);
  16. //订单日志
  17. OrderLog orderLog =new OrderLog();
  18. orderLog.setId(String.valueOf(idWorker.nextId()));
  19. orderLog.setOperater(operator);
  20. orderLog.setOperateTime(new Date());
  21. orderLog.setOrderId(order.getId());
  22. orderLog.setOrderStatus("3"); //订单已完成
  23. orderLog.setConsignStatus("2"); //订单已送达
  24. orderLogMapper.insert(orderLog); //写入日志
  25. }

4.2. 自动收货

如果用户在15天(可以在订单配置表中配置)没有确认收货,系统将自动收货。如何实现?我们这里采用定时任务springTask来实现.

4.2.1. Cron表达式

Cron表达式是一个字符串,字符串分为七个部分,每一个域代表一个含义。

Cron表达式7个域格式为: 秒 分 小时 日 月 星期几 年

Cron表达式6个域格式为: 秒 分 小时 日 月 周

序号 说明 是否必填 允许填写的值 允许的通配符
1 0-59 , - * /
2 0-59 , - * /
3 小时 0-23 , - * /
4 1-31 , - * ? / L W
5 1-12或JAN-DEC , - * /
6 星期几 1-7或SUN-SAT , - * ? / L W
7 empty 或1970-2099 , - * /

通配符

  1. 通配符说明:
  2. * 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。
  3. ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。
  4. 例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?
  5. - 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。
  6. , 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发
  7. / 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。
  8. L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7""SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"
  9. W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-").
  10. # 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;

常用表达式

  1. 0 0 10,14,16 * * ? 每天上午10点,下午2点,4
  2. 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
  3. 0 0 12 ? * WED 表示每个星期三中午12
  4. "0 0 12 * * ?" 每天中午12点触发
  5. "0 15 10 ? * *" 每天上午10:15触发
  6. "0 15 10 * * ?" 每天上午10:15触发
  7. "0 15 10 * * ? *" 每天上午10:15触发
  8. "0 15 10 * * ? 2005" 2005年的每天上午10:15触发
  9. "0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发
  10. "0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发
  11. "0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
  12. "0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发
  13. "0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:102:44触发
  14. "0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发
  15. "0 15 10 15 * ?" 每月15日上午10:15触发
  16. "0 15 10 L * ?" 每月最后一日的上午10:15触发
  17. "0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发
  18. "0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发
  19. "0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发

2.2. 代码实现

4.2.2.1. 发送消息

创建工程changgou_task,引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.amqp</groupId>
  7. <artifactId>spring-rabbit</artifactId>
  8. </dependency>

创建配置文件

  1. server:
  2. port: 9202
  3. spring:
  4. application:
  5. name: task
  6. rabbitmq:
  7. host: 192.168.130.128

创建com.changgou包 再创建启动类 TaskApplication

  1. @SpringBootApplication
  2. @EnableScheduling //开启定时任务
  3. public class TaskApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(TaskApplication.class,args);
  6. }
  7. }

创建com.changgou.config包 创建RabbitMQConfig

注意Queue为org.springframework.amqp.core.Queue下的Queue

  1. @Configuration
  2. public class RabbitMQConfig {
  3. public static final String ORDER_TACK="order_tack";
  4. @Bean
  5. public Queue queue(){
  6. return new Queue(ORDER_TACK);
  7. }
  8. }

创建com.changgou.task包 创建OrderTask

  1. @Configuration
  2. public class RabbitMQConfig {
  3. public static final String ORDER_TACK="order_tack";
  4. @Bean
  5. public Queue queue(){
  6. return new Queue(ORDER_TACK);
  7. }
  8. }

4.2.2.2. 接受消息

在changgou_service_order项目 order.config包 的RabbitMQConfig 添加自动收货消息队列

  1. //自动收货消息队列
  2. public static final String ORDER_TACK="order_tack";
  1. @Bean
  2. public Queue ORDER_TASK() {
  3. return new Queue(ORDER_TACK);
  4. }

在OrderService添加方法声明

  1. //自动确认收货
  2. void autoTack();

impl实现

  1. @Autowired
  2. private OrderConfigMapper orderConfigMapper;
  3. @Override
  4. @Transactional
  5. public void autoTack() {
  6. //1.从订单配置表获取到订单自动确认的时间点
  7. OrderConfig orderConfig = orderConfigMapper.selectByPrimaryKey(1); //目前只有1条写死为获取 id为1的
  8. //2.得到当前时间节点向前数 15(默认为) 天 作为过期的时间节点
  9. LocalDate now = LocalDate.now();
  10. LocalDate date = now.plusDays(-orderConfig.getTakeTimeout());
  11. //3.按条件查询 获取订单列表
  12. Example example = new Example(Order.class);
  13. Example.Criteria criteria = example.createCriteria();
  14. criteria.andLessThan("consignTime",date); //发货时间小于过期时间
  15. criteria.andEqualTo("orderStatus","2"); //收货状态为未确认
  16. List<Order> orderList = orderMapper.selectByExample(example);
  17. for (Order order : orderList) {
  18. this.confirmTask(order.getId(),"system"); //调用手动确认方法
  19. }
  20. }

在listener包 创建OrderTackListener

  1. @Component
  2. public class OrderTackListener {
  3. @Autowired
  4. private OrderService orderService;
  5. @RabbitListener(queues = RabbitMQConfig.ORDER_TACK)
  6. public void receiveOrderTaskMessage(String message){
  7. System.out.println("收到自动确认收货消息");
  8. orderService.autoTack();
  9. }
  10. }