ruoyi-cloud分页是怎么回事

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实现,别看若依写的这么复杂,其实本质就三件事情:

  1. 指定筛选哪些字段
  2. 查询条件
  3. 默认排序规则

注意:这里面没有分页处理

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 &gt;= #{params.beginTime}
    </if>
    <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
      AND oper_time &lt;= #{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包,查询资料显示:

  1. com.github.pagehelper.PageHelper.startPage是PageHelper分页插件提供的一个静态方法,在MyBatis项目里用于开启分页功能
  2. PageHelper.startPage方法的核心原理是利用MyBatis的拦截器机制。在调用startPage方法之后PageHelper会在当前线程里保存分页信息,当执行SQL查询时,PageHelper拦截器会对SQL进行拦截,自动在SQL语句后面添加分页查询的相关语句(如LIMIT关键字),从而实现分页查询的功能。

这也就解释了为什么我们不需要在xml写分页条件。那么,这条语句都对哪些方面做了增强呢?

  1. pageNum、pageSize:分页查询方面的增强;
  2. orderBy:源码里就是StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc,比如update_date desc,用于排序方式的增强;
  3. 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&params%5BbeginTime%5D=2025-04-01%2000%3A00%3A00&params%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 &gt;= #{params.beginTime}
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
  AND oper_time &lt;= #{params.endTime}
</if>
相关推荐
zm10 分钟前
服务器多客户端连接核心要点(1)
java·开发语言
FuckPatience19 分钟前
关于C#项目中 服务层使用接口的问题
java·开发语言·c#
天上掉下来个程小白39 分钟前
缓存套餐-01.Spring Cache介绍和常用注解
java·redis·spring·缓存·spring cache·苍穹外卖
揣晓丹1 小时前
JAVA实战开源项目:健身房管理系统 (Vue+SpringBoot) 附源码
java·vue.js·spring boot·后端·开源
编程轨迹_1 小时前
使用 Spring 和 Redis 创建处理敏感数据的服务
java·开发语言·restful
奔驰的小野码1 小时前
SpringAI实现AI应用-自定义顾问(Advisor)
java·人工智能·spring boot·spring
奔驰的小野码1 小时前
SpringAI实现AI应用-使用redis持久化聊天记忆
java·数据库·人工智能·redis·spring
裁二尺秋风1 小时前
k8s(11) — 探针和钩子
java·容器·kubernetes
一方~2 小时前
XML语言
xml·java·web
LSL666_2 小时前
Java——多态
java·开发语言·多态·内存图