ruoyi-cloud版本:v3.6.5
身为一个CURD Boy,分页是一个绕不开的话题。ruoyi-cloud中分页是怎么做的呢?
我们以系统管理-日志管理-操作日志功能点作为示例来看问题。
一、结论
假设我们现在要研发一个操作日志的分页功能,如何实现呢?
1.1 后端
控制器固定这么写:
less
@RequiresPermissions("system:operlog:list")
@GetMapping("/list")
public TableDataInfo list(SysOperLog operLog)
{
// 分页处理逻辑
startPage();
// 查询接口
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
return getDataTable(list);
}
然后注意查询接口,最底层的xml实现,别看若依写的这么复杂,其实本质就三件事情:
- 指定筛选哪些字段
- 查询条件
- 默认排序规则
注意:这里面没有分页处理
xml
<select id="selectOperLogList" parameterType="SysOperLog" resultMap="SysOperLogResult">
-- 查询列
<include refid="selectOperLogVo"/>
-- 查询条件
<where>
<if test="operIp != null and operIp != ''">
AND oper_ip like concat('%', #{operIp}, '%')
</if>
<if test="title != null and title != ''">
AND title like concat('%', #{title}, '%')
</if>
<if test="businessType != null">
AND business_type = #{businessType}
</if>
<if test="businessTypes != null and businessTypes.length > 0">
AND business_type in
<foreach collection="businessTypes" item="businessType" open="(" separator="," close=")">
#{businessType}
</foreach>
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="operName != null and operName != ''">
AND oper_name like concat('%', #{operName}, '%')
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
AND oper_time >= #{params.beginTime}
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND oper_time <= #{params.endTime}
</if>
</where>
-- 默认排序方式
order by oper_id desc
</select>
然后后端分页接口就ok。
1.2 前端
首先在api/system下的operLog.js中添加接口的调用:
php
export function list(query) {
return request({
url: '/system/operlog/list',
method: 'get',
params: query
})
}
接下来在vue文件中声明查询参数,注意日期范围查询的参数需要单独指定:
javascript
data() {
return {
// 日期范围
dateRange: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
operIp: undefined,
title: undefined,
operName: undefined,
businessType: undefined,
status: undefined
}
}
}
这里可以参考观察一下vue文件,看看这些参数都绑定到了哪些查询条件上。
绑定查询操作时,以下面的方式调用查询接口,即可:
ini
import { list } from "@/api/system/operlog";
getList() {
this.loading = true;
list(this.addDateRange(this.queryParams, this.dateRange)).then( response => {
this.list = response.rows;
this.total = response.total;
this.loading = false;
}
);
},
二、分页接口原理
找到后端接口,我们发现分页接口是一个GET请求:
less
@RequiresPermissions("system:operlog:list")
@GetMapping("/list")
public TableDataInfo list(SysOperLog operLog)
{
// 分页处理逻辑
startPage();
// 查询接口
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
return getDataTable(list);
}
接下来我们打开谷歌浏览器的F12,点击分页控制器的页数,可以看到调用的接口路径:
bash
http://localhost:81/dev-api/system/operlog/list?pageNum=1&pageSize=10
此时有一个非常诡异的事情,后端控制器接收的参数是operLog,这是一个业务domain实体,里面没有pageNum和pageSize,那么是谁处理了这两个参数呢?
没错,就是startPage()
,我们翻一下它的源码。它调用了一个PageUtils.startPage()
方法:
ini
/**
* 设置请求分页数据
*/
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);
}
再往下看TableSupport.buildPageRequest()
,可以看到在这里接收了pageNum和pageSize,以及其他的一些参数:
scss
/**
* 封装分页对象
*/
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));
// 排序的方向desc或者asc,默认asc
pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));
// 分页参数合理化,默认true
pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));
return pageDomain;
}
这样我们就知道pageNum和pageSize是被PageDomain接收了。
在处理完参数之后,注意这行代码:PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
,而PageHelper属于com.github.pagehelper
包,查询资料显示:
com.github.pagehelper.PageHelper.startPage
是PageHelper分页插件提供的一个静态方法,在MyBatis项目里用于开启分页功能PageHelper.startPage
方法的核心原理是利用MyBatis的拦截器机制。在调用startPage
方法之后PageHelper会在当前线程里保存分页信息,当执行SQL查询时,PageHelper拦截器会对SQL进行拦截,自动在SQL语句后面添加分页查询的相关语句(如LIMIT
关键字),从而实现分页查询的功能。
这也就解释了为什么我们不需要在xml写分页条件。那么,这条语句都对哪些方面做了增强呢?
- pageNum、pageSize:分页查询方面的增强;
- orderBy:源码里就是
StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc
,比如update_date desc
,用于排序方式的增强; - reasonable:是一个布尔类型的参数,用于控制分页合理化。
- 当
reasonable
为true
时 :表示开启分页合理化。如果传入的pageNum
小于 1,会自动将pageNum
修正为1;如果传入的pageNum
大于最大页码,会自动将pageNum
修正为最大页码。例如,数据库中只有20条记录,每页显示10条,最大页码为2。如果传入的pageNum = 3
,开启分页合理化后,会将pageNum
修正为2,避免出现查询结果为空的情况。- 当
reasonable
为false
时 :表示不开启分页合理化。如果传入的pageNum
小于1或者大于最大页码,会按照传入的pageNum
进行查询,可能会得到空的查询结果。
三、如何实现排序
如下图所示,ruoyi-cloud支持在表格字段上开启排序条件的功能:

这是一种比较简单的排序,仅能根据某个字段进行正序或倒叙排序。这种排序不是vue前端排序,实际上是调用了后端接口的,打开F12可以看到请求路径:
arduino
// 正序排序
http://localhost:81/dev-api/system/operlog/list?pageNum=1&pageSize=10&orderByColumn=operTime&isAsc=ascending
// 逆序排序
http://localhost:81/dev-api/system/operlog/list?pageNum=1&pageSize=10&orderByColumn=costTime&isAsc=descending
在前端,我们需要在表格需要添加排序功能的字段加上如下sortable="custom" :sort-orders="['descending', 'ascending']"
:
xml
<el-table-column label="操作日期" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.operTime) }}</span>
</template>
</el-table-column>
添加一个排序触发事件:
ini
/** 排序触发事件 */
handleSortChange(column, prop, order) {
this.queryParams.orderByColumn = column.prop;
this.queryParams.isAsc = column.order;
this.getList();
},
前端的配置就ok了。在后端,依然是startPage方法帮我们做了排序方式的增强。所以不用管太多。
四、日期范围查询
在xml中,查询条件是需要我们手动写的。其中日期范围查询比较特殊。
在前端,日期范围绑定的并不是queryParams,而是单独的一个数组变量:
ini
<el-form-item label="操作时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']"
></el-date-picker>
</el-form-item>
// 日期范围
dateRange: [],
在进行分页请求时,通过一个js函数将日期范围拼接到了请求参数上:
kotlin
list(this.addDateRange(this.queryParams, this.dateRange))
观察一下请求路径:
perl
http://localhost:81/dev-api/system/operlog/list?pageNum=1&pageSize=10¶ms%5BbeginTime%5D=2025-04-01%2000%3A00%3A00¶ms%5BendTime%5D=2025-04-30%2023%3A59%3A59
我们发现传参方式是params[beginTime]
和params[endTime]
,因此后端如此处理:
xml
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
AND oper_time >= #{params.beginTime}
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND oper_time <= #{params.endTime}
</if>