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/
创建死信交换器
exchange.ordertimeout
(fanout)创建队列
queue.ordertimeout
建立死信交换器
exchange.ordertimeout
与队列queue.ordertimeout
之间的绑定创建队列
queue.ordercreate
,Arguments添加
`x-message-ttl`=`10000`
`x-dead-letter-exchange`: `exchange.ordertimeout`
![](https://cdn.jsdelivr.net/gh/Iekrwh/images/md-images/1648290034111.png#alt=1648290034111.png)
- 测试:向queue.ordercreate队列添加消息,等待10秒后消息从queue.ordercreate队列消失
2.2. 代码实现
2.2.1. 微信支付-查询订单
在changgou_service_pay的pay下controller包的WxPayController类添加查询订单方法
//基于微信支付查询订单
@GetMapping("/query/{orderId}")
public Result queryOrder(@PathVariable("orderId") String orderId) {
Map map = wxPayService.queryOrder(orderId);
return new Result(true, StatusCode.OK, "查询订单成功", map);
}
将查询订单方法暴露给feign
在changgou_service_pay_api里的feign包的PayFeign添加查询方法
//基于微信支付查询订单
@GetMapping("/wxpay/query/{orderId}")
public Result queryOrder(@PathVariable("orderId") String orderId);
2.2.2. 微信支付-关闭订单
在changgou_service_pay的pay下service包的WxpayService接口 添加关闭订单方法定义
//基于微信支付关闭订单
Map closeOrder(String orderId);
实现方法 调用微信支付sdk中的closeOrder方法
//基于微信支付关闭订单
@Override
public Map closeOrder(String orderId) {
try {
Map<String,String> map = new HashMap();
map.put("out_trade_no",orderId);
Map<String, String> resultMap = wxPay.closeOrder(map);
return resultMap;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
collection层
//基于微信支付关闭订单
@PutMapping("/close/{orderID}")
public Result closeOrder(@PathVariable("orderId") String orderID){
Map map = wxPayService.closeOrder(orderID);
return new Result(true,StatusCode.OK,"关闭订单成功",map);
}
同样以feign形式声明
//基于微信支付关闭订单
@PutMapping("/wxpay/close/{orderID}")
public Result closeOrder(@PathVariable("orderId") String orderID);
2.2.3. 回滚库存
在changgou_service_goods包dao层的SkuMapper实体接口 添加回滚库存的自定义sql
//回滚库存(增加库存并扣减销量)
@Update("update tb_suk set num=num+#{num},sale_num=sale_num-#{num} where id=#{skuId}")
void resumeStockNum(@Param("skuId") String skuId,@Param("num") Integer num);
在SkuService中添加方法声明
//回滚库存
void resumeStockNum(String skuId, Integer num);
impl实现方法
//回滚库存
@Override
@Transactional
public void resumeStockNum(String skuId, Integer num) {
skuMapper.resumeStockNum(skuId,num);
}
collection层
//回滚库存
@RequestMapping("/resumeStockNum")
public Result resumeStockNum(@RequestParam("skuId") String skuId,@RequestParam("num") Integer num){
skuService.resumeStockNum(skuId,num);
return new Result(true,StatusCode.OK,"回滚库存成功");
}
在changgou_service_goods_api包下声明feign
//回滚库存
@RequestMapping("/sku/resumeStockNum")
public Result resumeStockNum(@RequestParam("skuId") String skuId,@RequestParam("num") Integer num);
2.2.4. 消息队列发送和接受
在changgou_service_order项目下的service.impl包下的OrderServiceImpl
注入rabbitmq模块
@Autowired
private RabbitTemplate rabbitTemplate;
在add方法最后一行添加代码
//发送延时消息
rabbitTemplate.convertAndSend("","queue.ordercreate",orderId);
在此项目的listener包添加OrderTimeOutListener
@Component
public class OrderTimeOutListener {
@Autowired
private OrderService orderService;
//监听死信队列
@RabbitListener(queues = "queue.ordertimeout")
public void receiveCloseOrderMessage(String orderId){
System.out.println("接受到关闭订单的消息"+orderId);
try {
//调用业务层完成关闭订单操作
orderService.closeOrder(orderId);
} catch (Exception e){
e.printStackTrace();
}
}
}
在OrderService 声明closeOrder方法
//关闭订单
void closeOrder(String oderId);
在changgou_server_order添加 pay的feign依赖导入 在pom.xml添加依赖
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_service_pay_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在启动类中扫描feign包注解添加pay包
@EnableFeignClients(basePackages = {"com.changgou.goods.feign" , "com.changgou.pay.feign"})
impl实现接口声明的方法
@Autowired
private PayFeign payFeign;
@Override
@Transactional
public void closeOrder(String orderId) {
System.out.println("关闭订单的业务开启" + orderId);
Order order = orderMapper.selectByPrimaryKey(orderId);
if (order == null) {
throw new RuntimeException("订单不存在");
}
//1.根据订单id查询mysql的订单信息 判断订单是否存在 判断订单的支付状态
//判断是否为已支付
if (!"0".equals(order.getPayStatus())) {
System.out.println("当前订单已支付,无需关闭");
return;
}
System.out.println("关闭订单校验通过" + orderId);
//2.基于微信查询订单信息
Map wxQueryMap = (Map) payFeign.queryOrder(orderId).getData();
System.out.println("查询微信支付订单成功" + wxQueryMap);
//如果订单支付状态为已支付,进行数据补偿
if ("SUCCESS".equals(wxQueryMap.get("trade_state"))) {
String transactionId = String.valueOf(wxQueryMap.get("transaction_id"));
this.updatePayStatus(orderId, transactionId); //订单支付成功 进行减少库存 添加销量操作
System.out.println("完成数据补偿");
}
//如果订单支付状态为未支付 则回滚mysql中的订单信息 新增订单日志 恢复商品的库存 基于微信支付关闭订单
if ("NOTPAY".equals(wxQueryMap.get("trade_state"))) {
System.out.println("执行关闭操作");
order.setUpdateTime(new Date());
order.setOrderStatus("4"); //订单已关闭
orderMapper.updateByPrimaryKeySelective(order); //修改mysql订单信息
//订单日志
OrderLog orderLog = new OrderLog();
orderLog.setId(idWorker.nextId() + "");
orderLog.setOperater("system");
orderLog.setOperateTime(new Date());
orderLog.setOrderStatus("4"); //订单已关闭
orderLog.setOrderId(orderId);
orderLogMapper.insert(orderLog);//写入日志
//恢复商品库存
OrderItem _orderItem = new OrderItem();
_orderItem.setOrderId(orderId);
List<OrderItem> orderItemList = orderItemMapper.select(_orderItem); //根据orderId查询orderItem
for (OrderItem orderItem : orderItemList) {
skuFeign.resumeStockNum(orderItem.getSkuId(), orderItem.getNum());
}
//基于微信关闭订单
payFeign.closeOrder(orderId);
}
}
3. 订单批量发货
在changgou_service_order项目的Service包下的OrderService类添加方法声明
//批量发货
void batchSend(List<Order> orders);
OrderServiceImpl实现该方法
@Override
@Transactional
public void batchSend(List<Order> orders) {
//判断每个订单和物流公司的值是否存在
for (Order order : orders) {
if (order.getId() == null){
throw new RuntimeException("订单号不存在")
}
if (order.getShippingCode() == null || order.getShippingName() == null) {
throw new RuntimeException("请输入快递单号或物流公司名称");
}
}
//进行订单状态的校验
for (Order order : orders) {
Order order1 = orderMapper.selectByPrimaryKey(order.getId());
//发货状态 是否存在
if (!"0".equals(order1.getConsignStatus()) || !"1".equals(order1.getConsignStatus())){
throw new RuntimeException("订单状态不合法");
}
}
//修改订单状态为已发货
for (Order order : orders) {
order.setOrderStatus("2"); // 已发货状态
order.setConsignStatus("1"); //已发货
order.setConsignTime(new Date());
order.setUpdateTime(new Date());
orderMapper.updateByPrimaryKeySelective(order); //写入数据
//记录订单日志
OrderLog orderLog =new OrderLog();
orderLog.setId(String.valueOf(idWorker.nextId()));
orderLog.setOperater("admin");
orderLog.setOperateTime(new Date());
orderLog.setOrderId(order.getId());
orderLog.setOrderStatus("2");
orderLog.setConsignStatus("1");
orderLogMapper.insert(orderLog); //写入日志
}
}
OrderController新增方法
@PostMapping("/batchSend")
public Result batchSend(@RequestBody List<Order> orders){
orderService.batchSend(orders);
return new Result(true,StatusCode.OK,"发货成功");
}
3.1. 对接第三方物流查询
第三方的物流系统。比较常用的有菜鸟物流、快递鸟等。我们这里推荐使用快递鸟 http://www.kdniao.com
4. 确认收货和自动收货
当物流公司将货物送到了用户收货地址之后,需要用户点击确认收货,当用户点击了确认收货之后,会修改订单状态为已完成
4.1. 手动确认收货
在changgou_service_order项目的Service包下的OrderService类添加方法声明
//手动确认收货
void confirmTask(String orderId,String operator);
impl实现
//手动确认收货
@Override
public void confirmTask(String orderId, String operator) {
Order order = orderMapper.selectByPrimaryKey(orderId);
if (order == null) {
throw new RuntimeException("订单不存在");
}
if (!"1".equals(order.getConsignStatus())){
throw new RuntimeException("订单未发货");
}
order.setConsignStatus("2"); //已送达
order.setOrderStatus("3"); //已完成
order.setUpdateTime(new Date());
order.setEndTime(new Date());
orderMapper.updateByPrimaryKeySelective(order);
//订单日志
OrderLog orderLog =new OrderLog();
orderLog.setId(String.valueOf(idWorker.nextId()));
orderLog.setOperater(operator);
orderLog.setOperateTime(new Date());
orderLog.setOrderId(order.getId());
orderLog.setOrderStatus("3"); //订单已完成
orderLog.setConsignStatus("2"); //订单已送达
orderLogMapper.insert(orderLog); //写入日志
}
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 | , - * / |
通配符
通配符说明:
* 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。
? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。
例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?
- 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。
, 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发
/ 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。
L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"
W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-").
# 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;
常用表达式
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 12 ? * WED 表示每个星期三中午12点
"0 0 12 * * ?" 每天中午12点触发
"0 15 10 ? * *" 每天上午10:15触发
"0 15 10 * * ?" 每天上午10:15触发
"0 15 10 * * ? *" 每天上午10:15触发
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发
"0 15 10 15 * ?" 每月15日上午10:15触发
"0 15 10 L * ?" 每月最后一日的上午10:15触发
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
2.2. 代码实现
4.2.2.1. 发送消息
创建工程changgou_task
,引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
</dependency>
创建配置文件
server:
port: 9202
spring:
application:
name: task
rabbitmq:
host: 192.168.130.128
创建com.changgou包 再创建启动类 TaskApplication
@SpringBootApplication
@EnableScheduling //开启定时任务
public class TaskApplication {
public static void main(String[] args) {
SpringApplication.run(TaskApplication.class,args);
}
}
创建com.changgou.config包 创建RabbitMQConfig
类
注意Queue为org.springframework.amqp.core.Queue下的Queue
@Configuration
public class RabbitMQConfig {
public static final String ORDER_TACK="order_tack";
@Bean
public Queue queue(){
return new Queue(ORDER_TACK);
}
}
创建com.changgou.task包 创建OrderTask
类
@Configuration
public class RabbitMQConfig {
public static final String ORDER_TACK="order_tack";
@Bean
public Queue queue(){
return new Queue(ORDER_TACK);
}
}
4.2.2.2. 接受消息
在changgou_service_order项目 order.config包 的RabbitMQConfig 添加自动收货消息队列
//自动收货消息队列
public static final String ORDER_TACK="order_tack";
@Bean
public Queue ORDER_TASK() {
return new Queue(ORDER_TACK);
}
在OrderService添加方法声明
//自动确认收货
void autoTack();
impl实现
@Autowired
private OrderConfigMapper orderConfigMapper;
@Override
@Transactional
public void autoTack() {
//1.从订单配置表获取到订单自动确认的时间点
OrderConfig orderConfig = orderConfigMapper.selectByPrimaryKey(1); //目前只有1条写死为获取 id为1的
//2.得到当前时间节点向前数 15(默认为) 天 作为过期的时间节点
LocalDate now = LocalDate.now();
LocalDate date = now.plusDays(-orderConfig.getTakeTimeout());
//3.按条件查询 获取订单列表
Example example = new Example(Order.class);
Example.Criteria criteria = example.createCriteria();
criteria.andLessThan("consignTime",date); //发货时间小于过期时间
criteria.andEqualTo("orderStatus","2"); //收货状态为未确认
List<Order> orderList = orderMapper.selectByExample(example);
for (Order order : orderList) {
this.confirmTask(order.getId(),"system"); //调用手动确认方法
}
}
在listener包 创建OrderTackListener
类
@Component
public class OrderTackListener {
@Autowired
private OrderService orderService;
@RabbitListener(queues = RabbitMQConfig.ORDER_TACK)
public void receiveOrderTaskMessage(String message){
System.out.println("收到自动确认收货消息");
orderService.autoTack();
}
}