苍穹外卖07——来单提醒和客户催单(涉及SpringTask、WebSocket协议、苍穹外卖跳过微信支付同时保证可以收到订单功能)
Spring Task介绍
应用场景:
- 信用卡每月还款提醒
- 银行贷款每月还款提醒
- 火车票销售系统处理未付款订单
- 入职纪念日为用户发送通知
cron表达式
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间。
构成规则:分为6或7个域,由空格分隔,每个域代表一个含义。
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)。
秒 分钟 小时 日 月 周 年
2022年10月12日上午9点整对应的cron表达式为:0 0 9 12 10 ? 2022
。
cron表达式在线生成器:Cron在线生成器 | 程序员导航网
Spring Task 使用步骤:
- 导入 Maven 坐标 spring-context(已存在)
- 在启动类添加注解 @EnableScheduling 开启任务调度
- 自定义定时任务类,新建一个Task包
@Component @Slf4j public class MyTask {@Scheduled(cron = "0/5 * * * * ?") // 每五秒触发public void executeTask(){log.info("定时任务开始执行:{}", new Date());} }
订单状态定时处理
@Component // 将该类标记为Spring组件,以便自动扫描和管理
@Slf4j // 使用Lombok的@Slf4j注解,自动生成日志记录器
public class OrderTask { @Autowired // 自动注入OrderMapper依赖 private OrderMapper orderMapper; @Scheduled(cron = "0 * * * * ?") // 每分钟执行一次 public void processTimeoutOrder() { log.info("定时处理超时订单 {}", LocalDateTime.now()); // 记录当前时间的日志 // 当前时间减去15分钟 LocalDateTime time = LocalDateTime.now().plusMinutes(-15); // 查询状态为待付款且订单时间早于当前时间减去15分钟的订单 List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time); // 如果查询到的订单列表不为空 if (ordersList != null && ordersList.size() > 0) { for (Orders orders : ordersList) { // 将订单状态设置为已取消 orders.setStatus(Orders.CANCELLED); // 设置取消原因 orders.setCancelReason("订单超时,自动取消"); // 设置取消时间为当前时间 orders.setCancelTime(LocalDateTime.now()); // 更新订单信息 orderMapper.update(orders); } } } @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行 public void processDeliveryOrder() { log.info("定时处理配送中的订单 {}", LocalDateTime.now()); // 记录当前时间的日志 // 当前时间减去60分钟 LocalDateTime time = LocalDateTime.now().plusMinutes(-60); // 查询状态为配送中且订单时间早于当前时间减去60分钟的订单 List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time); // 如果查询到的订单列表不为空 if (ordersList != null && ordersList.size() > 0) { for (Orders orders : ordersList) { // 将订单状态设置为已完成 orders.setStatus(Orders.COMPLETED); // 更新订单信息 orderMapper.update(orders); } } }
}
WebSocket
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
HTTP协议和WebSocket协议对比:
- HTTP是短连接
- WebSocket是长连接
- HTTP通信是单向的,基于请求响应模式
- WebSocket支持双向通信
- HTTP和WebSocket底层都是TCP连接
选择使用 HTTP 协议还是 WebSocket 协议?
使用 HTTP 协议的情况
-
请求-响应结构:
- 如果你的应用主要以传统的请求-响应方式工作(例如,加载网页、提交表单等),那么 HTTP 是更合适的。
-
无持续连接:
- HTTP 适合于短暂的连接需求,每个请求和响应都是独立的。
-
安全和缓存:
- HTTP 支持缓存机制,可以提高静态资源的加载速度,同时可以通过 HTTPS 实现安全传输。
-
简单性:
- 对于相对简单的应用,使用 HTTP 通信更为直观和易于实现。
使用 WebSocket 协议的情况
-
实时双向通信:
- 当应用需要实时数据交换,如在线聊天、游戏、股票行情更新等,WebSocket 是更好的选择。
-
低延迟和高频率数据传输:
- WebSocket 允许在客户端和服务器之间建立持久连接,适用于需要频繁更新或低延迟的数据传输场景。
-
高效数据传输:
- WebSocket 在数据传输上比 HTTP 更为高效,因为它减少了开销,适合大规模应用。
-
无需频繁连接和断开:
- 如果应用需要保持长连接,WebSocket 可以避免频繁的连接和断开,节省资源和时间。
总结
- HTTP:适用于传统请求-响应式的应用,简单和一次性的通信。
- WebSocket:适用于需要实时、双向且高效沟通的应用。
WebSocket入门案例
导入坐标
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>
导完坐标之后新建配置类,使WebSocket的第三方bean由ioc容器管理
@Configuration
public class WebSocketConfiguration {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}
添加websocket的实体类
/*** WebSocket服务*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {//存放会话对象private static Map<String, Session> sessionMap = new HashMap();/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("sid") String sid) {System.out.println("客户端:" + sid + "建立连接");sessionMap.put(sid, session);}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, @PathParam("sid") String sid) {System.out.println("收到来自客户端:" + sid + "的信息:" + message);}/*** 连接关闭调用的方法** @param sid*/@OnClosepublic void onClose(@PathParam("sid") String sid) {System.out.println("连接断开:" + sid);sessionMap.remove(sid);}/*** 群发,服务器给客户端发送** @param message*/
public void sendToAllClient(String message) { Collection<Session> sessions = sessionMap.values(); // 获取所有客户端会话 for (Session session : sessions) { // 遍历每个会话 try { // 服务器向客户端发送消息 session.getBasicRemote().sendText(message); // 发送文本消息 } catch (Exception e) { e.printStackTrace(); // 捕获并打印异常 } } }}
苍穹外卖跳过微信支付同时保证可以收到订单功能
苍穹外卖遇到问题(包括跳过微信支付、nodejs不兼容等)-CSDN博客
来单提醒功能代码开发
需求分析和设计
- 通过WebSocket实现管理端页面和服务端保持长连接状态
- 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
- 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
- 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type, orderId, content
- type 为消息类型,1为来单提醒 2为客户催单
- orderId 为订单id
- content 为消息内容
/*** 订单支付* @param ordersPaymentDTO* @return*/public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {// 当前登录用户idLong userId = BaseContext.getCurrentId();User user = userMapper.getById(userId);JSONObject jsonObject = new JSONObject();jsonObject.put("code","ORDERPAID");OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);vo.setPackageStr(jsonObject.getString("package"));Integer OrderPaidStatus = Orders.PAID;//支付状态,已支付Integer OrderStatus = Orders.TO_BE_CONFIRMED; //订单状态,待接单LocalDateTime check_out_time = LocalDateTime.now();//更新支付时间orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time, this.os.getId());//通过websocket向客户端浏览器推送消息 type orderId contentMap map = new HashMap();map.put("type",1);map.put("orderId",this.os.getId());map.put("content","订单号:"+this.os.getNumber());String json = JSON.toJSONString(map);webSocketServer.sendToAllClient(json);return vo;}
如上面的代码,用户支付成功之后修改数据库中记录的订单的状态为已支付,然后发送websocket响应给管理端admin,然后在controller层发送vo实体类的响应给小程序用户端。
//通过websocket向客户端浏览器推送消息 type orderId content Map map = new HashMap(); map.put("type",1); map.put("orderId",this.os.getId()); map.put("content","订单号:"+this.os.getNumber()); String json = JSON.toJSONString(map); webSocketServer.sendToAllClient(json); return vo; // 发送给小程序端管理员的浏览器端接收websocket响应。
深入了解websocket
- websocket使用前一定要建立通信的信道,这样就可以实现双方的相互通信。
- WebSocket通信通常是由前端负责打开和关闭的,一旦连接建立,前端和后端可以通过这个通道双向发送和接收消息。
客户催单
// controller层
//客户催单@GetMapping("/reminder/{id}")@ApiOperation("客户端提醒")public Result reminder(@PathVariable("id") Long id){orderService.reminder(id);return Result.success();}// service层
public void reminder(Long id) {// 根据id查询订单Orders ordersDB = orderMapper.getById(id);// 校验订单是否存在if (ordersDB == null) {throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);}Map map = new HashMap();map.put("type", 2); // 类型map.put("orderId", id);map.put("content", "订单号: " + ordersDB.getNumber());// 发送websocket消息webSocketServer.sendToAllClient(JSON.toJSONString(map));}
相关文章:
![](https://i-blog.csdnimg.cn/direct/5122388d58524b12b2a10b56e71a0535.png)
苍穹外卖07——来单提醒和客户催单(涉及SpringTask、WebSocket协议、苍穹外卖跳过微信支付同时保证可以收到订单功能)
Spring Task介绍 应用场景: 信用卡每月还款提醒银行贷款每月还款提醒火车票销售系统处理未付款订单入职纪念日为用户发送通知 cron表达式 cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间。 构成规则:分为6或7个域&…...
![](https://www.ngui.cc/images/no-images.jpg)
C语言二级考试
你必须知道的 二级考试不是编写程序,或者说不只是编程的考核,它还会考核计算机C语言相关语言还有内涵等基础知识,比较全面综合(说人话,要看最新考纲具备一定的基础知识) 考试时间 120 分钟 分值 100 分&…...
![](https://i-blog.csdnimg.cn/img_convert/dafcf22ad2e0e0df2f57de264d7a8ab6.png)
IDEA Maven构建时报错:无效的目标发行版17
报错分析 报错原因:Maven 构建时,Java 版本配置不匹配 我安装的JDK版本是1.8,但由于种种原因,Maven构建时指定了 Java 17 作为目标发行版,从而导致错误 解决方案 首先,java -version,查看环…...
![](https://www.ngui.cc/images/no-images.jpg)
javafx 将项目打包为 Windows 的可执行文件exe
要将 JavaFX 项目打包为 .exe 文件,你可以使用一些工具将你的应用程序封装为 Windows 可执行文件。以下是两种常用的方法: 方法 1:使用 jpackage(适用于 JDK 14 及更高版本) jpackage 是 JDK 内置的工具,…...
![](https://www.ngui.cc/images/no-images.jpg)
Python操作Excel的库openpyxl使用入门
openpyxl 是一个用于读写 Excel 2010 xlsx/xlsm/xltx/xltm 文件的 Python 库。以下是一些 openpyxl 的基本使用方法: 安装 openpyxl 首先,确保已经安装了 openpyxl。如果没有安装,可以使用以下命令进行安装: pip install openp…...
![](https://www.ngui.cc/images/no-images.jpg)
数据通过canal 同步es,存在延迟问题,解决方案
当使用 Canal 同步数据到 Elasticsearch(ES)时,出现延迟问题通常源于多个因素,如 Canal 配置、网络延迟、ES 的负载和性能瓶颈等。以下是一些解决方案,帮助减少和解决延迟问题: 1. 优化 Canal 配置 Canal…...
![](https://i-blog.csdnimg.cn/direct/ee79747754594d1c8334ad1e70528a93.jpeg)
了解Node.js
Node.js是一个基于V8引擎的JavaScript运行时环境,它允许JavaScript代码在服务器端运行,从而实现后端开发。Node.js的出现,使得前端开发人员可以利用他们已经掌握的JavaScript技能,扩展技能树并成为全栈开发人员。本文将深入浅出地…...
![](https://i-blog.csdnimg.cn/direct/1e8805c6babe4ac4ba1e7b5a576b8409.png)
Android Studio创建新项目并引入第三方jar、aar库驱动NFC读写器读写IC卡
本示例使用设备:https://item.taobao.com/item.htm?spma21dvs.23580594.0.0.52de2c1bbW3AUC&ftt&id615391857885 一、打开Android Studio,点击 File> New>New project 菜单,选择 要创建的项目模版,点击 Next 二、输入项目名称…...
![](https://www.ngui.cc/images/no-images.jpg)
Oracle Dataguard(主库为双节点集群)配置详解(4):配置备库
Oracle Dataguard(主库为双节点集群)配置详解(4):配置备库 目录 Oracle Dataguard(主库为双节点集群)配置详解(4):配置备库一、为备库配置静态监听1、配置 li…...
![](https://i-blog.csdnimg.cn/direct/0bf4356aeb2f415db03578b0ac5854aa.png)
前端炫酷动画--文字(二)
目录 一、弧形边框选项卡 二、零宽字符 三、目录滚动时自动高亮 四、高亮关键字 五、文字描边 六、按钮边框的旋转动画 七、视频文字特效 八、立体文字特效让文字立起来 九、文字连续光影特效 十、重复渐变的边框 十一、磨砂玻璃效果 十二、FLIP动画 一、弧形边框…...
![](https://www.ngui.cc/images/no-images.jpg)
ceph 数据均衡
实现数据均衡的主要方法 在 Ceph 集群中,实现 OSD(对象存储守护进程)之间的数据均衡对于提升性能和资源利用率至关重要。以下是实现数据均衡的主要方法: 1. 调整 OSD 权重(Reweight) 通过调整 OSD 的权重,可以控制数据在各个 OSD 之间的分布。Ceph 提供了根据利用率或…...
![](https://i-blog.csdnimg.cn/direct/e69a5fe92be34f3f91b1a89271c88761.png)
代码随想录算法训练营day29
代码随想录算法训练营 —day29 文章目录 代码随想录算法训练营前言一、134. 加油站暴力解法贪心算法 二、135. 分发糖果三、860. 柠檬水找零四、406.根据身高重建队列vector版list版 总结 前言 今天是算法营的第29天,希望自己能够坚持下来! 今日任务&a…...
![](https://i-blog.csdnimg.cn/direct/68a9e0e0eb684b63a1987e05f3fac106.png)
android studio根据包名获取当前安装包信息
package com.example.myapplication2;import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.util.Log;/**** 获取版本信息*/ public class SystemHelper {/*** 获取本地软件版本号*/public stat…...
![](https://www.ngui.cc/images/no-images.jpg)
学习第六十五行
仔细观察键盘,会发现一个$符号,其实是有含义的。 在 shell 脚本中,美元符号 $ 有几种重要的含义: 变量引用:$ 用于引用变量的值。例如,如果你有一个变量 name,可以通过 $name 来获取它的值。 n…...
![](https://www.ngui.cc/images/no-images.jpg)
零碎的知识点(七):线性二次调节器(LQR)是什么?
线性二次调节器(LQR)是什么? 1. LQR的定义与目标2. LQR的原理性能指标 J J J最优解的计算控制律 3. LQR的性质4. 举例说明问题描述解步骤仿真结果 5. 实际应用总结 线性二次调节器(LQR) 是一种经典的最优控制方法&…...
![](https://i-blog.csdnimg.cn/direct/ed5cabed0f024fa69634cc062b5011b7.png#pic_center)
Matlab一些使用技巧
代码分段 两个百分号就可以实现代码的分段,不同段之间会以不同的背景色显示,方便调试 如下: %% 腐蚀 stlen TimeWidth*Fs/50; %线性算子的长度,1/100的脉宽,对应0.5us,15个采样点 stlen 100; SE strel…...
![](https://i-blog.csdnimg.cn/direct/9c9e8ecc0e3643d1abb20ce638fe06b7.png)
Linux 发行版介绍与对比:Red Hat、Ubuntu、Kylin、Debian
Linux 操作系统有众多发行版(Distros),每个发行版的设计目标、目标用户、应用场景和使用方式有所不同。常见的 Linux 发行版包括 Red Hat、Ubuntu、Kylin 和 Debian。以下是这些发行版的详细介绍与对比,以及它们的应用场景和使用方…...
![](https://i-blog.csdnimg.cn/direct/e91cf11d5d504c6cba1a1aa78a92aea9.png)
从CentOS到龙蜥:企业级Linux迁移实践记录(龙蜥开局)
引言: 在我们之前的文章中,我们详细探讨了从CentOS迁移到龙蜥操作系统的基本过程和考虑因素。今天,我们将继续这个系列,重点关注龙蜥系统的实际应用——特别是常用软件的安装和配置。 龙蜥操作系统(OpenAnolis&#…...
![](https://www.ngui.cc/images/no-images.jpg)
java1-相对路径与绝对路径
注意注意~开始新部分啦! 开始正式分享java前,先为大家分享一下一个常用的概念---文件的相对路径与绝对路径. 开篇明义: 相对路径是指一个文件或目录相对于当前工作目录的路径。相对路径不包含根目录,而是从当前目录开始计算。 绝对路径是指一个文件或目录从根目录…...
![](https://i-blog.csdnimg.cn/img_convert/4a75672b3887975020c693e15af73674.png)
iChainfo 品牌升級為 ichaingo,打造 Web3 數據基礎設施新標杆
Web3 數據基礎設施服務商 iChainfo 今⽇正式宣佈,全新名稱 「ichaingo」 重磅登場,新的官⽅網站 ichaingo.com 正式上線。此次品牌升級基於 Web3 ⾏業的發展趨勢和公司⾃⾝的戰略布局,旨在為全 球⽤戶提供更準確、即時、全⾯、深⼊的 Web3 數…...
![](https://www.ngui.cc/images/no-images.jpg)
Flink概念知识讲解之:Restart重启策略配置
Flink概念知识讲解之:Restart重启策略配置 当 Task 发生故障时,Flink 需要重启出错的 Task 以及其他受到影响的 Task ,以使得作业恢复到正常执行状态。 Flink 通过重启策略和故障恢复策略来控制 Task 重启:重启策略决定是否可以…...
![](https://i-blog.csdnimg.cn/direct/e00bf11a77034ac79b072d470b9c3e0b.png)
[java基础-集合篇]LinkedList源码粗析
LinkedList 的数据结构 实现List、Deque 接口,基于 双向链表实现的列表。与基于数组的 ArrayList 不同,基于链表的LinkedList 允许在列表的任何位置快速地插入和删除元素。 Java中LinkedList实现了Deque,它提供了 add, offer, remove, poll, …...
![](https://www.ngui.cc/images/no-images.jpg)
面试:C++类成员初始化顺序
1、非静态数据成员:按它们在类定义的声明顺序初始化,不会按它们在初始化列表的顺序。 2、静态数据成员:在main函数启动之前,并且只初始化一次 3、基类构造函数:如果类从一个或多个基类继承而来,基类的构造…...
![](https://i-blog.csdnimg.cn/direct/f553c49a863b4e419f922653ccba7722.png#pic_center)
【Python】Python与C的区别
文章目录 语句结束符代码块表示变量声明函数定义注释格式Python的标识符数据输入input()函数数据输出print()函数 语句结束符 C 语言 C 语言中每条语句必须以分号;结束。例如,int a 10;、printf("Hello, World!");。分号是语句的一部分,用于…...
![](https://i-blog.csdnimg.cn/direct/6b25cc4ca1ad4f9a90b32efe06f070f1.png)
[开源]自动化定位建图系统(视频)
系统状态机: 效果展示: 1、 机器人建图定位系统-基础重定位,定位功能演示 2、 机器人建图定位系统-增量地图构建,手动回环检测演示 3、… 开源链接: https://gitee.com/li-wenhao-lwh/lifelong-backend Qt人机交互…...
![](https://i-blog.csdnimg.cn/img_convert/caa166256384118b9bdbf370f47bb601.png)
ISP流程--去马赛克详解
前言 本期我们将深入讨论ISP流程中的去马赛克处理。我们熟知,彩色图像由一个个像元组成,每个像元又由红、绿、蓝(RGB)三通道构成。而相机传感器只能感知光的强度,无法直接感知光谱信息,即只有亮暗而没有颜色…...
![](https://www.ngui.cc/images/no-images.jpg)
Objective-C语言的软件工程
Objective-C语言的软件工程探讨 引言 在软件工程的领域中,编程语言的选择是至关重要的。Objective-C,作为一种为苹果公司的macOS和iOS操作系统而开发的编程语言,凭借其灵活性和强大的功能被广泛应用于应用开发。然而,随着Swift等…...
![](https://www.ngui.cc/images/no-images.jpg)
Objective-C语言的语法糖
Objective-C语言的语法糖探秘 在编程语言的发展历程中,语法糖(Syntactic Sugar)是一个颇具趣味性和重要性的概念。它让编程的表达更加简洁直观,同时提高了代码的可读性和可维护性。Objective-C 作为一种面向对象的编程语言&#…...
![](https://www.ngui.cc/images/no-images.jpg)
设计模式中的代理模式
在Java中,代理模式(Proxy Pattern)可以通过静态代理和动态代理两种主要方式实现。 一、静态代理模式 在编译时就已经确定了代理类和被代理类的关系。 代理类和目标对象通常实现相同的接口或继承相同父类。 缺点是对于每个需要代理的目标对象…...
![](https://www.ngui.cc/images/no-images.jpg)
15个学习Python 的编程游戏网站
从小很多人都会在想,那些枯燥的教学课程要是全部变成游戏就好了,这样的话那期末成绩不得立即起飞了嘛?那对于编程很多人也有这样的想法,边玩边学就好了 这不已经有很多程序员开发了多款边玩边学的编程游戏供大家使用,…...
![](/images/no-images.jpg)
选择锦州网站建设/网站是怎么做出来的
1.无外部包引用时用通常方法即可; 2.有外部包引用时,最好使用 fat jar eclipse plug-in,可以自己去下,zip文件解压后,将net.sf.fjep.fatjar_0.0.25文件夹放入eclipse的plugins文件 夹中,然后将里面的fatjar…...
![](/images/no-images.jpg)
wordpress云落主题/软文广告平台
看到很多朋友问起Eclipse插件问题,所以索性就将自己所用过的总结一下。呵呵,说是全攻略只当胀胀眼球,俺能用过的插件会有多少呀,虽然才疏学浅,为了不让自己的痛苦重复在似俺当年般的菜虫身上…… tomcatPluginV31beta…...
![](https://www.oschina.net/img/hot3.png)
dedecms制作网站地图/厦门百度关键词推广
2019独角兽企业重金招聘Python工程师标准>>> 1.运行时常量池属于线程共享区中的方法区。 2.运行时常量池用于编译期生成的各种自变量,符号引用,这部分内用将在类加载后接入方法区的运行时常量池中存放。 看如下代码所示,如图&…...
![](/images/no-images.jpg)
php动态网站开发简答题/百度seo推广怎么收费
测试原理,停掉源端和目标端的OGG, 在源端进行DML操作,然后切换日志。然后开启源端和目标端的OGG。看看是否会同步数据到目标端。另外查看源端的ext进程日志,看里面的提示。 结论: OGG 在停掉后,进行大批量的…...
![](https://img-blog.csdnimg.cn/img_convert/0437b7b2f7fcaa444e71a49765f319a1.png)
wordpress冷门二次元主题/接推广怎么收费
博客园加密登录--jsencrypt问题由来前几天在做项目的时候,发现一般做登录的时候只是一个非常简单的form表单,但是这样肯定是不安全的!所以想去看看其他比较流行的网站是怎么实现的。说到安全,我第一个想到的就是去看支付宝&#x…...
![](/images/no-images.jpg)
平台类网站做多久/企业网站推广方案策划
题目 给你n(n<5e4)个数,第i个数为ai(1<ai<1e6) 以下q(q<5e4)个修改,第j次把pj改为vj(1<vj<1e6) 每次询问问修改之后,[1,n]间有多少种不同的gcd的值 思路来源 归神代码 题解 网上搜题解都看不懂,只好硬啃…...