基于若依框架的动态分页逻辑的实现分析
如果让我们自己写分页查询的逻辑,应该怎么写呢?
在前端要完成分页的逻辑实际要做的工作还是挺多的。
- 分页查询应该支持查询参数的输入,对于一个有众多属性的列表,可能有很多查询参数,对于不同的参数类型,有的需要like模糊查找、有的需要equals相等、有的必须是一个between时间范围。
- 分页查询要有一个查询区的页面组件提供查询参数的输入,这个要考虑查询区的形式,提供平铺式的,还是提供一个简单的搜索输入框,要考虑是否记录历史值,使用get还是post发送请求。
- 分页组件做页面切换时需要实时改变分页参数,当页数较小时应该简便显示,当页数较多时应该详细显示组件。同时也要选择合适的条数。
- 表格的某个字段需要做筛选、排序、过滤时,也需要分页查询接口提供支持。
因此、分页查询功能的完成其实也不简单。
后端对于分页查询的逻辑编写,其实也不简单。
例如,对于众多变化的查询参数应该用什么请求对象来接收。用HashMap
?还是专门的DTO
请求对象?
支持众多可选查询参数的接口,有的属性会传入查询值,有的又没有传,难道我要写很多个if条件来判断?
分页参数又是怎么影响sql语句做到分页分段查询的?
接下来,我们分析下在若依框架中的分页逻辑是怎么完成的。
一、前端调用实现
(一)分页变量定义
将分页变量放入查询参数中,并将查询参数提取出来,防止嵌套过深。
const data = reactive({form: {},// 一般在查询参数中定义分页变量queryParams: {pageNum: 1,pageSize: 10,//userName: "", 其它查询参数},rules: {}
});
// 提取出变量,防止嵌套过深
const { queryParams, form, rules } = toRefs(data);
(二)查询区
一般分页查询是支持传入参数后进行搜索的,因此在前端页面有个填写查询参数的查询区。
<el-form :model="queryParams" ref="queryRef" v-show="showSearch" :inline="true" label-width="68px"><el-form-item label="用户名称" prop="roleName"><el-inputv-model="queryParams.userName"placeholder="请输入用户名称"clearablestyle="width: 240px"@keyup.enter="handleQuery"/></el-form-item><!-- 其它查询参数项--><el-form-item><el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button><el-button icon="Refresh" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
(三)分页组件
分页组件可以更改分页参数,并接收后台返回的总条数total,若total为0则不显示分页组件。
// 页面添加分页组件,传入分页变量
<paginationv-show="total>0":total="total":page.sync="queryParams.pageNum":limit.sync="queryParams.pageSize"@pagination="getList"
/>
(四)调用方法
/** 查询用户列表 */
function getList() {loading.value = true;// 调用后台方法,传入参数 获取结果listUser(proxy.addDateRange(queryParams.value, dateRange.value)).then(res => {loading.value = false;// 接受数据集合userList.value = res.rows;// 接受数据总条数total.value = res.total;});
};
(五)前端接口方法
这里可以看到分页查询的请求是GET请求,而不是POST请求。
查询参数以?name1=value1&name2=value2
的形式发送请求,优点是可以进行浏览器缓存,方便记录查询日志。缺点是若查询参数很长很复杂,或需要保密,则还是用POST请求比较好。
import request from '@/utils/request'
import { parseStrEmpty } from "@/utils/ruoyi";// 查询用户列表
export function listUser(query) {return request({url: '/system/user/list',method: 'get',params: query})
}
二、后端逻辑实现
(一)控制器Controller
对接前端接口的请求路径、方法的内容在Controller文件中。
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController@GetMapping("/list")public TableDataInfo list(SysUser user){startPage(); // 此方法配合前端完成自动分页List<SysUser> list = userService.selectUserList(user);return getDataTable(list);}
}
可以看到,前端控制器继承了一个基类控制器BaseController
,该基类控制器委托给分页工具类PageUtils
进行分页功能的处理。
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;public class BaseController
{protected final Logger logger = LoggerFactory.getLogger(this.getClass());/*** 设置请求分页数据*/protected void startPage(){PageUtils.startPage();}/*** 设置请求排序数据*/protected void startOrderBy(){PageDomain pageDomain = TableSupport.buildPageRequest();if (StringUtils.isNotEmpty(pageDomain.getOrderBy())){String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());PageHelper.orderBy(orderBy);}}/*** 清理分页的线程变量*/protected void clearPage(){PageUtils.clearPage();}
}
(二)分页工具类
实际上,分页工具类又是委托给PageHelper插件进行分页处理。
import com.github.pagehelper.PageHelper;
import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.utils.sql.SqlUtil;public class PageUtils extends PageHelper
{/*** 设置请求分页数据*/public static void startPage(){PageDomain pageDomain = TableSupport.buildPageRequest();Integer pageNum = pageDomain.getPageNum();Integer pageSize = pageDomain.getPageSize();String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());Boolean reasonable = pageDomain.getReasonable();// 委托给分页插件处理PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);}/*** 清理分页的线程变量*/public static void clearPage(){PageHelper.clearPage();}
}
分页插件中内部深层调用了PageMethod
中的startPage
方法
public abstract class PageMethod {// 线程变量protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();public static <E> Page<E> startPage(Object params) {Page<E> page = PageObjectUtil.getPageFromObject(params, true);Page<E> oldPage = getLocalPage();if (oldPage != null && oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());}setLocalPage(page);return page;}// 设置线程变量public static void setLocalPage(Page page) {LOCAL_PAGE.set(page);}public static <T> Page<T> getLocalPage() {return (Page)LOCAL_PAGE.get();}
}
(三)表格数据处理
由于表格列表的按字段排序的功能也需要调用分页查询接口,这里封装了分页的属性名称的默认值。
public class TableSupport
{/** 当前记录起始索引 */public static final String PAGE_NUM = "pageNum";/** 每页显示记录数 */public static final String PAGE_SIZE = "pageSize";/** 排序列 */public static final String ORDER_BY_COLUMN = "orderByColumn";/** 排序的方向 "desc" 或者 "asc". */public static final String IS_ASC = "isAsc";/** 分页参数合理化 */public static final String REASONABLE = "reasonable";/** 封装分页对象 */public static PageDomain getPageDomain(){PageDomain pageDomain = new PageDomain();pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));return pageDomain;}public static PageDomain buildPageRequest(){return getPageDomain();}
}
然后创建分页对象PageDomain
交给分页插件PageHelper
使用。
// 分页参数的封装
public class PageDomain
{/** 当前记录起始索引 */private Integer pageNum;/** 每页显示记录数 */private Integer pageSize;/** 排序列 */private String orderByColumn;/** 排序的方向desc或者asc */private String isAsc = "asc";/** 分页参数合理化,例如:前端传递的参数是:pageNum=-1是不合理的 */private Boolean reasonable = true;
}
由于pagehelper的插件只对mybatis有用,如果自己需要对别的数据来源进行分页。
可以参照框架使用一部分功能,例如以下是对list集合进行分页:
public TableDataInfo getTodoItems(@RequestParam String searchValue) {/**第一步:pageNum和pageSize是从前端数据里传进来的分页对象的属性**/PageDomain pageDomain = TableSupport.buildPageRequest();Integer pageNum = pageDomain.getPageNum();Integer pageSize = pageDomain.getPageSize();String userName = SecurityUtils.getUsername();/**第二步:过滤并获取数据**/List<TodoTaskDTO> result = taskCenterService.getTodoItem(searchValue);/**第四步:获取处理好的list集合**/int num = result.size();// 对列表进行分页result = result.stream().skip((pageNum - 1) * pageSize).limit(pageSize).collect(Collectors.toList());// 组装响应数据TableDataInfo rspData = new TableDataInfo();rspData.setCode(0);rspData.setRows(result);rspData.setTotal(num);return rspData;}
(四)实体基类
注意到实体类,例如SysUser
类既作为请求对象,又是ORM映射的数据库表实体对象。
这里共用了同一个对象,简化了类的数量。但是否会出现功能不适配呢?
public class SysUserController extends BaseController@GetMapping("/list")public TableDataInfo list(SysUser user){startPage(); // 此方法配合前端完成自动分页List<SysUser> list = userService.selectUserList(user);return getDataTable(list);}
}public class SysUser extends BaseEntity{private Long userId;private String userName;// 省略其它...
}
可以在请求URL的携带参数中看到,日期范围是含在params的数组里的。
http://localhost:8811/dev-api/system/user/list?pageNum=1&pageSize=10&userName=admin¶ms%5BbeginTime%5D=2025-01-01¶ms%5BendTime%5D=2025-01-03
SysUser
类继承了BaseEntity
基类,基类中包含如下属性:
- searchValue :搜索值
- createBy :创建人,记录创建该实体的对象或用户的标识符,可以是用户名、用户ID等。
- createTime :创建时间,使用 @JsonFormat 注解来指定日期时间格式,以便于JSON序列化/反序列化时保持一致的格式。
- updateBy :更新人,记录最后一次更新该实体的对象或用户的标识符。
- updateTime :更新时间,使用 @JsonFormat 注解来指定日期时间格式,以便于JSON序列化/反序列化时保持一致的格式。
- remark :提供额外的备注信息,可以用于描述实体的任何相关信息。
- params :请求参数,用于传递额外的参数或非结构化数据
public class BaseEntity implements Serializable
{private static final long serialVersionUID = 1L;/** 搜索值 */@JsonIgnoreprivate String searchValue;/** 创建者 */private String createBy;/** 创建时间 */@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date createTime;/** 更新者 */private String updateBy;/** 更新时间 */@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date updateTime;/** 备注 */private String remark;/** 请求参数 */@JsonInclude(JsonInclude.Include.NON_EMPTY)private Map<String, Object> params;
}
(五)动态SQL
这里接收各种参数查询的功能是通过使用mybatis的动态sql的方式完成的。
应该通过代码生成工具自动生成以下代码,否则会有点繁琐。
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user uleft join sys_dept d on u.dept_id = d.dept_idwhere u.del_flag = '0'<if test="userId != null and userId != 0">AND u.user_id = #{userId}</if><if test="userName != null and userName != ''">AND u.user_name like concat('%', #{userName}, '%')</if><if test="status != null and status != ''">AND u.status = #{status}</if><if test="phonenumber != null and phonenumber != ''">AND u.phonenumber like concat('%', #{phonenumber}, '%')</if><if test="params.beginTime != null and params.beginTime != ''"> <!-- 开始时间检索 -->AND date_format(u.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')</if><if test="params.endTime != null and params.endTime != ''"> <!-- 结束时间检索 -->AND date_format(u.create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')</if><if test="deptId != null and deptId != 0">AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) ))</if><!-- 数据范围过滤 -->${params.dataScope}
</select>
若使用MyBatis-Plus 的功能,那么不用生成动态sql的代码,但是可能需要用以下方式实现功能。
QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
// 使用 lambda 表达式安全地添加条件
wrapper.like(StringUtils.isNotEmpty(userName), SysUser::getUserName, userName);
// 如果有其他条件,继续添加...
if (StringUtils.isNotEmpty(phonenumber)) {wrapper.like(SysUser::getPhonenumber, phonenumber);
}
if (status != null && !"".equals(status)) {wrapper.eq(SysUser::getStatus, status);
}// 处理日期范围查询
if (params.getBeginTime() != null) {wrapper.ge(SysUser::getCreateTime, params.getBeginTime());
}
if (params.getEndTime() != null) {wrapper.le(SysUser::getCreateTime, params.getEndTime());
}
// 执行查询
List<SysUser> userList = mapper.selectList(wrapper);
对于复杂查询,特别是涉及到多表关联、分页、排序、数据权限等功能时,MyBatis-Plus 虽然提供了很多便利的方法,但在某些情况下还是需要结合 XML 配置或手写 SQL 来满足特定需求。
(六)返回响应数据
获取到的数据是通过封装TableDataInfo
返回给前端的。
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;public class BaseController
{/*** 响应请求分页数据*/@SuppressWarnings({ "rawtypes", "unchecked" })protected TableDataInfo getDataTable(List<?> list){TableDataInfo rspData = new TableDataInfo();rspData.setCode(HttpStatus.SUCCESS);rspData.setMsg("查询成功");rspData.setRows(list);rspData.setTotal(new PageInfo(list).getTotal());return rspData;}
}@Schema(title = "表格分页数据对象")
public class TableDataInfo implements Serializable
{/** 总记录数 */@Schema(title = "总记录数")private long total;/** 列表数据 */@Schema(title = "列表数据")private List<?> rows;/** 消息状态码 */@Schema(title = "消息状态码")private int code;/** 消息内容 */@Schema(title = "消息内容")private String msg;
}
相关文章:

基于若依框架的动态分页逻辑的实现分析
如果让我们自己写分页查询的逻辑,应该怎么写呢? 在前端要完成分页的逻辑实际要做的工作还是挺多的。 分页查询应该支持查询参数的输入,对于一个有众多属性的列表,可能有很多查询参数,对于不同的参数类型,…...

51c~ONNX~合集1
我自己的原文哦~ https://blog.51cto.com/whaosoft/11608027 一、使用Pytorch进行简单的自定义图像分类 ~ONNX 推理 图像分类是计算机视觉中的一项基本任务,涉及训练模型将图像分类为预定义类别。本文中,我们将探讨如何使用 PyTorch 构建一个简单的自定…...

【数据结构篇】顺序表 超详细
目录 一.顺序表的定义 1.顺序表的概念及结构 1.1线性表 2.顺序表的分类 2.1静态顺序表 2.2动态顺序表 二.动态顺序表的实现 1.准备工作和注意事项 2.顺序表的基本接口: 2.0 创建一个顺序表 2.1 顺序表的初始化 2.2 顺序表的销毁 2.3 顺序表的打印 3.顺序…...
kubernetes 集群搭建(二进制方式)
Kubernetes 作为当今最流行的容器编排平台,提供了强大的功能来管理和扩展容器化应用。除了使用 kubeadm 等工具简化集群的创建过程外,直接通过二进制文件安装 Kubernetes 组件也是一种常见的方法。这种方式给予用户更多的控制权,并且适用于那…...

linux平台RTMP|RTSP播放器如何回调SEI数据?
我们在对接Linux平台RTMP|RTSP播放的时候,有遇到这样的技术需求,合作企业在做无人机视觉分析场景的时候,除了需要低延迟的拿到解码后的RGB|YUV数据,然后投递给他们自己的视觉算法处理模块外,还需要播放器支持SEI的回调…...
Vue uni-app免手动import
unplugin-auto-import 是一个流行的 JavaScript/TypeScript 插件,可以自动导入常用的库、API 或自定义函数,减少手动书写 import 语句的繁琐操作。它常用于 Vue、React 等框架,帮助开发者提高效率和减少样板代码。 核心功能: 自…...
7. 计算机视觉
计算机视觉(Computer Vision,简称 CV)是人工智能(AI)领域中的一个重要分支,旨在使计算机能够像人类一样“看”并理解数字图像或视频。它结合了计算机科学、数学、图像处理、模式识别、机器学习等多个学科&a…...
在服务器进行docker部署频繁提示permission denied
当你频繁遇到permission denied,证明当前用户的权限不够 可以参考如下操作: 1.创建用户组docker sudo groupadd docker把当前用户添加到docker用户组中 sudo usermod -aG docker $USER优点: 可以在不使用sudo的情况下运行docker命令...
c/c++ static
定义 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。修饰普通函数,表明函数的作…...
C#中System.Text.Json:从入门到精通的实用指南
一、引言 在当今数字化时代,数据的高效交换与处理成为软件开发的核心环节。JSON(JavaScript Object Notation)凭借其简洁、轻量且易于读写的特性,已然成为数据交换领域的中流砥柱。无论是前后端数据交互,还是配置文件…...

内存故障原因与诊断(Reasons and Diagnosis of Memory Failure)
内存故障原因与诊断 您是否曾遇到过电脑无法启动、黑屏、死机,或者系统卡顿的情况?这些问题看起来很复杂,实际上大多数都是内存故障引起的。内存是电脑的核心组成部分之一,任何小东西问题都可能导致系统死机,严重时甚…...

[操作系统] 进程状态详解
在操作系统中,进程是程序执行的基本单位,操作系统负责管理进程的生命周期。为了高效地管理进程,操作系统通过定义不同的进程状态来表示进程在不同时间点的行为。本文将详细介绍常见的进程状态及其相互之间的转换过程。 进程状态概述 在kerne…...

[论文阅读] (36)CS22 MPSAutodetect:基于自编码器的恶意Powershell脚本检测模型
《娜璋带你读论文》系列主要是督促自己阅读优秀论文及听取学术讲座,并分享给大家,希望您喜欢。由于作者的英文水平和学术能力不高,需要不断提升,所以还请大家批评指正,非常欢迎大家给我留言评论,学术路上期…...

【Maui】下拉框的实现,绑定键值对
文章目录 前言一、问题描述二、解决方案三、软件开发(源码)3.1 创建模型3.2 视图界面3.3 控制器逻辑层 四、项目展示 前言 .NET 多平台应用 UI (.NET MA…...

Oracle 深入学习 Part 14:Managing Password Security and Resources(管理密码安全性和资源)
Profiles Profile 是一个以名称标识的集合,用于管理 密码 和 资源限制。 每个用户都对应一个profiles,可以通过 CREATE USER 或 ALTER USER 命令分配给用户。 Profiles 可以启用或禁用。 Profiles 可以关联到默认的 DEFAULT Profile。 密码管理&…...

C语言:位段
位段的内存分配: 1. 位段的成员可以是 int unsigned int signed int 或者是char (属于整形家族)类型 2. 位段的空间上是按照需要以4个字节( 类型 int )或者1个字节( char )的方式来开辟的。 3. 位段涉及…...

MPLS VPN 原理与配置
一.简介 MPLS,称之为多协议标签交换,在九十年代中期被提出来,用于解决传统IP报文依赖查表转发而产生的瓶颈,现多用于VPN技术,MPLS报头封装在数据链路层之上,网络层之下。本文为结合了华为技术和新华三技术…...

稳定的通信桥梁,CCLINKIE转ModbusTCP网关实现AGV运输的光速效应
三菱PLC与AGV机器人搬运车通过稳联技术协议转换网关建立通信 一、现场情况概述 - 三菱PLC:使用CC-Link IE协议进行通信。 - AGV机器人搬运车:使用Modbus TCP协议进行通信。 - 协议转换网关:使用稳联技术的协议转换网关将PLC和AGV连接…...
Leetcode 3428. Maximum and Minimum Sums of at Most Size K Subsequences
Leetcode 3428. Maximum and Minimum Sums of at Most Size K Subsequences 1. 解题思路2. 代码实现 题目链接:3428. Maximum and Minimum Sums of at Most Size K Subsequences 1. 解题思路 这一题不需要连续性,因此我们就是考虑取得子串长度为别为1…...
第2章:Python TDD构建Dollar类基础
写在前面 这本书是我们老板推荐过的,我在《价值心法》的推荐书单里也看到了它。用了一段时间 Cursor 软件后,我突然思考,对于测试开发工程师来说,什么才更有价值呢?如何让 AI 工具更好地辅助自己写代码,或许…...

idea大量爆红问题解决
问题描述 在学习和工作中,idea是程序员不可缺少的一个工具,但是突然在有些时候就会出现大量爆红的问题,发现无法跳转,无论是关机重启或者是替换root都无法解决 就是如上所展示的问题,但是程序依然可以启动。 问题解决…...

基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...

shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...

.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
Oracle查询表空间大小
1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...

大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...