Java 项目实战: 外卖平台-MyBatis-Plus分页插件与员工分页查询

MyBatis-Plus 分页插件与员工信息分页查询

纲要

本篇引入 MyBatis-Plus 分页插件,实现带条件过滤的员工列表查询。

  • 分页插件配置MybatisPlusInterceptor + PaginationInnerInterceptor
  • 接口契约GET /employee/page?page=1&pageSize=10&name=xxx
  • 返回值类型R<Page> ------ 由前端需要的 recordstotal 决定
  • 三步实现:构造分页构造器 → 构造条件构造器 → 执行查询
  • 条件构造器like 模糊查询 + StringUtils.isNotEmpty 动态拼接 + orderByDesc 排序
  • 前端状态渲染ElementUI 作用域插槽把 status0/1 转成「已禁用 / 正常」

功能需求

X外卖菜单

  • 订单明细
  • 分类管理
  • 菜品管理
  • 套餐管理
  • 员工管理(当前选中)

员工管理

修改密码\] \[退出登录

请输入员工姓名\] \[搜索\]             \[添加员工

员工姓名 账号 手机号 操作
管理员 13333333333 13333333333 编辑 禁用
管理员 13333333333 13333333333 编辑 禁用
管理员 13333333333 13333333333 编辑 禁用
管理员 13333333333 13333333333 编辑 禁用
管理员 13333333333 13333333333 编辑 禁用
... ... ... ...

分页:总共 85 个项目 < 1 2 3 4 5 >

交互说明

  1. 搜索

    • 1.1 输入员工姓名进行搜索,支持模糊搜索
  2. 添加员工

    • 2.1 点击添加员工跳转至添加员工页面
    • 2.2 字段:
      • 2.2.1 账号:必填,限制输入3-20个字符,限制输入字母及阿拉伯数字,验证唯一性
      • 2.2.2 默认密码:123456,系统生成,仅展示,不可编辑
      • 2.2.3 姓名:必填,限制输入1-12个字,验证唯一性
      • 2.2.4 性别:默认男性,可选择
      • 2.2.5 身份证号:验证身份证规则
  3. 用户列表

    • 3.1 显示字段:账号、姓名、手机号、操作
    • 3.2 排序:创建时间倒序排列,单页最多显示10条
  4. 操作

    • 4.1 编辑:可以修改员工信息
    • 4.2 禁用/启用:用户被禁用后,不可登录

需求分析

员工数据量大时,一次性全量展示既慢又乱。标准做法是分页展示:
#mermaid-svg-2OAdmJIxhUeuELIU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2OAdmJIxhUeuELIU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2OAdmJIxhUeuELIU .error-icon{fill:#552222;}#mermaid-svg-2OAdmJIxhUeuELIU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2OAdmJIxhUeuELIU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2OAdmJIxhUeuELIU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2OAdmJIxhUeuELIU .marker.cross{stroke:#333333;}#mermaid-svg-2OAdmJIxhUeuELIU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2OAdmJIxhUeuELIU p{margin:0;}#mermaid-svg-2OAdmJIxhUeuELIU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2OAdmJIxhUeuELIU .cluster-label text{fill:#333;}#mermaid-svg-2OAdmJIxhUeuELIU .cluster-label span{color:#333;}#mermaid-svg-2OAdmJIxhUeuELIU .cluster-label span p{background-color:transparent;}#mermaid-svg-2OAdmJIxhUeuELIU .label text,#mermaid-svg-2OAdmJIxhUeuELIU span{fill:#333;color:#333;}#mermaid-svg-2OAdmJIxhUeuELIU .node rect,#mermaid-svg-2OAdmJIxhUeuELIU .node circle,#mermaid-svg-2OAdmJIxhUeuELIU .node ellipse,#mermaid-svg-2OAdmJIxhUeuELIU .node polygon,#mermaid-svg-2OAdmJIxhUeuELIU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2OAdmJIxhUeuELIU .rough-node .label text,#mermaid-svg-2OAdmJIxhUeuELIU .node .label text,#mermaid-svg-2OAdmJIxhUeuELIU .image-shape .label,#mermaid-svg-2OAdmJIxhUeuELIU .icon-shape .label{text-anchor:middle;}#mermaid-svg-2OAdmJIxhUeuELIU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2OAdmJIxhUeuELIU .rough-node .label,#mermaid-svg-2OAdmJIxhUeuELIU .node .label,#mermaid-svg-2OAdmJIxhUeuELIU .image-shape .label,#mermaid-svg-2OAdmJIxhUeuELIU .icon-shape .label{text-align:center;}#mermaid-svg-2OAdmJIxhUeuELIU .node.clickable{cursor:pointer;}#mermaid-svg-2OAdmJIxhUeuELIU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2OAdmJIxhUeuELIU .arrowheadPath{fill:#333333;}#mermaid-svg-2OAdmJIxhUeuELIU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2OAdmJIxhUeuELIU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2OAdmJIxhUeuELIU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2OAdmJIxhUeuELIU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2OAdmJIxhUeuELIU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2OAdmJIxhUeuELIU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2OAdmJIxhUeuELIU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2OAdmJIxhUeuELIU .cluster text{fill:#333;}#mermaid-svg-2OAdmJIxhUeuELIU .cluster span{color:#333;}#mermaid-svg-2OAdmJIxhUeuELIU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2OAdmJIxhUeuELIU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2OAdmJIxhUeuELIU rect.text{fill:none;stroke-width:0;}#mermaid-svg-2OAdmJIxhUeuELIU .icon-shape,#mermaid-svg-2OAdmJIxhUeuELIU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2OAdmJIxhUeuELIU .icon-shape p,#mermaid-svg-2OAdmJIxhUeuELIU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2OAdmJIxhUeuELIU .icon-shape .label rect,#mermaid-svg-2OAdmJIxhUeuELIU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2OAdmJIxhUeuELIU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2OAdmJIxhUeuELIU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2OAdmJIxhUeuELIU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 工具条
分页条 el-pagination
每页条数下拉:10 / 20 / 30 / 40
页码:1 2 3 ...
跳页输入框
数据表格 el-table
账号 / 姓名 / 手机号 / 性别 / 身份证号 / 账号状态 / 操作
姓名输入框 + 查询按钮

GET /employee/page?name=xxx

三个交互点都需要后端支持:

交互 传递的参数变化
首次进入页面 page=1&pageSize=10
切换每页条数 page=1&pageSize=20(重置回第一页)
点击页码 / 输入姓名查询 page=2&pageSize=10&name=张三

关键点 :分页与过滤是叠加关系。输入姓名后如果结果超过一页,必须同时按姓名过滤并分页,不能只做其一。

分页插件配置

MyBatis-Plus 的分页能力不是默认开启的,必须注册拦截器。

src/main/java/com/itheima/reggie/config/MybatisPlusConfig.java

java 复制代码
package com.itheima.reggie.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置MP的分页插件
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

工作原理

MySQL PaginationInnerInterceptor MybatisPlusInterceptor IService.page Controller MySQL PaginationInnerInterceptor MybatisPlusInterceptor IService.page Controller #mermaid-svg-in7qj4BbCs1gfbzA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-in7qj4BbCs1gfbzA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-in7qj4BbCs1gfbzA .error-icon{fill:#552222;}#mermaid-svg-in7qj4BbCs1gfbzA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-in7qj4BbCs1gfbzA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-in7qj4BbCs1gfbzA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-in7qj4BbCs1gfbzA .marker.cross{stroke:#333333;}#mermaid-svg-in7qj4BbCs1gfbzA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-in7qj4BbCs1gfbzA p{margin:0;}#mermaid-svg-in7qj4BbCs1gfbzA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-in7qj4BbCs1gfbzA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-in7qj4BbCs1gfbzA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-in7qj4BbCs1gfbzA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-in7qj4BbCs1gfbzA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-in7qj4BbCs1gfbzA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-in7qj4BbCs1gfbzA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-in7qj4BbCs1gfbzA .sequenceNumber{fill:white;}#mermaid-svg-in7qj4BbCs1gfbzA #sequencenumber{fill:#333;}#mermaid-svg-in7qj4BbCs1gfbzA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-in7qj4BbCs1gfbzA .messageText{fill:#333;stroke:none;}#mermaid-svg-in7qj4BbCs1gfbzA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-in7qj4BbCs1gfbzA .labelText,#mermaid-svg-in7qj4BbCs1gfbzA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-in7qj4BbCs1gfbzA .loopText,#mermaid-svg-in7qj4BbCs1gfbzA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-in7qj4BbCs1gfbzA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-in7qj4BbCs1gfbzA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-in7qj4BbCs1gfbzA .noteText,#mermaid-svg-in7qj4BbCs1gfbzA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-in7qj4BbCs1gfbzA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-in7qj4BbCs1gfbzA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-in7qj4BbCs1gfbzA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-in7qj4BbCs1gfbzA .actorPopupMenu{position:absolute;}#mermaid-svg-in7qj4BbCs1gfbzA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-in7qj4BbCs1gfbzA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-in7qj4BbCs1gfbzA .actor-man circle,#mermaid-svg-in7qj4BbCs1gfbzA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-in7qj4BbCs1gfbzA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} page(pageInfo, queryWrapper) 执行查询,被拦截器拦截 交给分页拦截器 SELECT COUNT(*) FROM employee WHERE ... total = 25 SELECT * FROM employee WHERE ... LIMIT 0,10 当前页 10 条 把结果塞回 pageInfo 对象 返回 pageInfo(已含 records 与 total)

插件做了两件事:

  • 自动执行一条 COUNT(*) 查询,把总数写入 Page.total
  • 自动在业务 SQL 后追加 LIMIT offset, size,把当前页数据写入 Page.records

版本差异提示

MyBatis-Plus 3.4.0 前后 API 有变化:

版本 写法
3.4.0 之前 new PaginationInterceptor()
3.4.0 及之后 new MybatisPlusInterceptor() + addInnerInterceptor(new PaginationInnerInterceptor())

本项目用 3.4.2,必须用新写法。用旧类会编译失败。

接口契约分析

请求

F12 观察首次进入员工管理页的请求:

text 复制代码
GET http://localhost:8080/employee/page?page=1&pageSize=10

在姓名框输入「张三」点查询:

text 复制代码
GET http://localhost:8080/employee/page?page=1&pageSize=10&name=张三

三个参数都是普通 key=value 的查询串,不是 JSON ,因此不能用 @RequestBody,直接声明方法参数即可。

返回类型为什么是 Page

前端处理响应的代码:

javascript 复制代码
getMemberList().then(res => {
  if (String(res.code) === '1') {
    this.tableData = res.data.records || []
    this.counts = res.data.total
  }
})

前端从 res.data 中读取 recordstotal 两个属性。这决定了后端 data 必须是一个含有这两个属性的对象 ------ 而 MyBatis-PlusPage 类恰好具备:

Page 属性 含义
records 当前页数据列表
total 总记录数
size 每页条数
current 当前页码
pages 总页数

所以方法签名是 R<Page> 而不是 R<Employee>R<List<Employee>>返回类型由前端需要的数据结构倒推得出,这是前后端分离开发的基本方法。

Controller 实现

三步实现

#mermaid-svg-IOBp8m8qij42OvEp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IOBp8m8qij42OvEp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IOBp8m8qij42OvEp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IOBp8m8qij42OvEp .error-icon{fill:#552222;}#mermaid-svg-IOBp8m8qij42OvEp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IOBp8m8qij42OvEp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IOBp8m8qij42OvEp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IOBp8m8qij42OvEp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IOBp8m8qij42OvEp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IOBp8m8qij42OvEp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IOBp8m8qij42OvEp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IOBp8m8qij42OvEp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IOBp8m8qij42OvEp .marker.cross{stroke:#333333;}#mermaid-svg-IOBp8m8qij42OvEp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IOBp8m8qij42OvEp p{margin:0;}#mermaid-svg-IOBp8m8qij42OvEp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IOBp8m8qij42OvEp .cluster-label text{fill:#333;}#mermaid-svg-IOBp8m8qij42OvEp .cluster-label span{color:#333;}#mermaid-svg-IOBp8m8qij42OvEp .cluster-label span p{background-color:transparent;}#mermaid-svg-IOBp8m8qij42OvEp .label text,#mermaid-svg-IOBp8m8qij42OvEp span{fill:#333;color:#333;}#mermaid-svg-IOBp8m8qij42OvEp .node rect,#mermaid-svg-IOBp8m8qij42OvEp .node circle,#mermaid-svg-IOBp8m8qij42OvEp .node ellipse,#mermaid-svg-IOBp8m8qij42OvEp .node polygon,#mermaid-svg-IOBp8m8qij42OvEp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IOBp8m8qij42OvEp .rough-node .label text,#mermaid-svg-IOBp8m8qij42OvEp .node .label text,#mermaid-svg-IOBp8m8qij42OvEp .image-shape .label,#mermaid-svg-IOBp8m8qij42OvEp .icon-shape .label{text-anchor:middle;}#mermaid-svg-IOBp8m8qij42OvEp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IOBp8m8qij42OvEp .rough-node .label,#mermaid-svg-IOBp8m8qij42OvEp .node .label,#mermaid-svg-IOBp8m8qij42OvEp .image-shape .label,#mermaid-svg-IOBp8m8qij42OvEp .icon-shape .label{text-align:center;}#mermaid-svg-IOBp8m8qij42OvEp .node.clickable{cursor:pointer;}#mermaid-svg-IOBp8m8qij42OvEp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IOBp8m8qij42OvEp .arrowheadPath{fill:#333333;}#mermaid-svg-IOBp8m8qij42OvEp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IOBp8m8qij42OvEp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IOBp8m8qij42OvEp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IOBp8m8qij42OvEp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IOBp8m8qij42OvEp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IOBp8m8qij42OvEp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IOBp8m8qij42OvEp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IOBp8m8qij42OvEp .cluster text{fill:#333;}#mermaid-svg-IOBp8m8qij42OvEp .cluster span{color:#333;}#mermaid-svg-IOBp8m8qij42OvEp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IOBp8m8qij42OvEp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IOBp8m8qij42OvEp rect.text{fill:none;stroke-width:0;}#mermaid-svg-IOBp8m8qij42OvEp .icon-shape,#mermaid-svg-IOBp8m8qij42OvEp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IOBp8m8qij42OvEp .icon-shape p,#mermaid-svg-IOBp8m8qij42OvEp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IOBp8m8qij42OvEp .icon-shape .label rect,#mermaid-svg-IOBp8m8qij42OvEp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IOBp8m8qij42OvEp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IOBp8m8qij42OvEp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IOBp8m8qij42OvEp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} page / pageSize / name 入参
① 构造分页构造器

new Page(page, pageSize)
② 构造条件构造器

LambdaQueryWrapper
③ 执行查询

employeeService.page(pageInfo, queryWrapper)
返回 R.success(pageInfo)

完整代码

java 复制代码
/**
 * 员工信息分页查询
 * @param page     当前页码
 * @param pageSize 每页条数
 * @param name     员工姓名(可选过滤条件)
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
    log.info("page = {},pageSize = {},name = {}", page, pageSize, name);

    // 构造分页构造器
    Page pageInfo = new Page(page, pageSize);

    // 构造条件构造器
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
    // 添加过滤条件:name 非空时才拼接 LIKE
    queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
    // 添加排序条件
    queryWrapper.orderByDesc(Employee::getUpdateTime);

    // 执行查询
    employeeService.page(pageInfo, queryWrapper);

    return R.success(pageInfo);
}

逐行解析

参数接收int page, int pageSize, String name

Spring MVC 会按参数名从请求参数中取值。这依赖于编译时保留参数名(-parameters),Spring Boot 默认已开启。若参数名对不上,会报 500 而非静默置 null ------ 这类错误容易定位。

new Page(page, pageSize) :第一个参数是当前页(从 1 开始),第二个是每页条数。PageMyBatis-Plus 的分页对象,泛型可省略(省略时是原始类型)。

动态条件 like(condition, column, value)

java 复制代码
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);

like 的第一个参数 boolean condition条件开关 :为 true 时才把这个 LIKE 拼进 SQL。等价于:

java 复制代码
// 等价的手工写法,更啰嗦
if (StringUtils.isNotEmpty(name)) {
    queryWrapper.like(Employee::getName, name);
}

这是 MyBatis-Plus 的条件构造器惯例:所有方法都提供带 condition 首参的重载,用于动态 SQL

为什么用 like 而不是 eq?姓名查询通常是模糊匹配,用户输入「张」希望查出「张三」「张四」。like 生成的 SQLWHERE name LIKE '%张%'

StringUtils.isNotEmpty :来自 org.apache.commons.lang.StringUtilscommons-lang 2.6)。注意与 org.springframework.util.StringUtils.hasLength 的区别 ------ 两者都能判空,但导入错了会编译失败。

orderByDesc(Employee::getUpdateTime) :按更新时间倒序,最近修改的排最前。生成的 SQLORDER BY update_time DESC

为什么不需要接收返回值

java 复制代码
employeeService.page(pageInfo, queryWrapper);   // 没有赋值给新变量

page 方法内部会把查询结果回填到传入的 pageInfo 对象 中。所以直接返回 pageInfo 即可。这是 MyBatis-Plus 的一个约定,容易误以为要写 Page result = service.page(...)

生成的 SQL

无姓名条件时:

sql 复制代码
SELECT COUNT(*) FROM employee;
SELECT id, name, username, phone, sex, id_number, status, create_time, update_time, create_user, update_user
FROM employee
ORDER BY update_time DESC
LIMIT 0,10;

有姓名条件时:

sql 复制代码
SELECT COUNT(*) FROM employee WHERE (name LIKE '%张三%');
SELECT id, name, username, phone, sex, id_number, status, create_time, update_time, create_user, update_user
FROM employee
WHERE (name LIKE '%张三%')
ORDER BY update_time DESC
LIMIT 0,10;

前端状态列的渲染处理

现象

后端返回的 status 是数字 1,页面上却显示「正常」。这不是后端做的转换,而是前端处理的。

ElementUI 作用域插槽

html 复制代码
<el-table-column label="账号状态">
  <template slot-scope="scope">
    {{ String(scope.row.status) === '0' ? '已禁用' : '正常' }}
  </template>
</el-table-column>

slot-scope="scope"ElementUI 的作用域插槽,用于获取当前行数据:

表达式 含义
scope 插槽作用域对象
scope.row 当前行的数据对象
scope.row.status 当前行的 status 字段值

三元表达式把 0 映射成「已禁用」,其他值映射成「正常」。

对比:不做处理会怎样

如果改成普通的 prop 绑定:

html 复制代码
<el-table-column prop="status" label="账号状态"></el-table-column>

页面上会直接显示原始数字 1。用户看到 1 无法理解,所以前端做这层映射是必要的。

这种「后端存状态码、前端做展示映射」是常见模式。另一种做法是在后端用 @JsonFormat 或自定义序列化器转换,但会增加后端与前端展示的耦合,通常不推荐。

完整可运行代码

目录结构

text 复制代码
src/main/java/com/itheima/reggie/
├── ReggieApplication.java
├── common/
│   ├── R.java
│   ├── BaseContext.java
│   ├── GlobalExceptionHandler.java
│   └── JacksonObjectMapper.java
├── config/
│   ├── MybatisPlusConfig.java        # 本篇新增:分页插件
│   └── WebMvcConfig.java
├── controller/
│   └── EmployeeController.java       # login / logout / save / page
├── entity/
│   └── Employee.java
├── filter/
│   └── LoginCheckFilter.java
├── mapper/
│   └── EmployeeMapper.java
└── service/
    ├── EmployeeService.java
    └── impl/
        └── EmployeeServiceImpl.java

MybatisPlusConfig.java

java 复制代码
package com.itheima.reggie.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置MP的分页插件
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

EmployeeController.java(分页部分)

java 复制代码
package com.itheima.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.Employee;
import com.itheima.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    /**
     * 员工信息分页查询
     * @param page     当前页码
     * @param pageSize 每页条数
     * @param name     员工姓名,用于模糊查询,可为空
     * @return 分页对象,含 records(当前页数据)与 total(总记录数)
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize, String name) {
        log.info("page = {},pageSize = {},name = {}", page, pageSize, name);

        // 构造分页构造器
        Page pageInfo = new Page(page, pageSize);

        // 构造条件构造器
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
        // 添加过滤条件:仅当 name 非空时拼接 LIKE
        queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
        // 添加排序条件:按更新时间倒序
        queryWrapper.orderByDesc(Employee::getUpdateTime);

        // 执行查询,结果回填到 pageInfo
        employeeService.page(pageInfo, queryWrapper);

        return R.success(pageInfo);
    }
}

Employee.java

java 复制代码
package com.itheima.reggie.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 员工实体
 */
@Data
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

    private String password;

    private String phone;

    private String sex;

    private String idNumber;//身份证号码

    private Integer status;

    @TableField(fill = FieldFill.INSERT) //插入时填充字段
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT) //插入时填充字段
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
    private Long updateUser;
}

前端 member.js 接口封装

javascript 复制代码
// src/main/resources/backend/api/member.js
function getMemberList(params) {
  return $axios({
    url: '/employee/page',
    method: 'get',
    params
  })
}

前端分页与查询逻辑

javascript 复制代码
// src/main/resources/backend/page/member/list.html
new Vue({
  data() {
    return {
      input: '',        // 姓名输入框
      counts: 0,        // 总记录数
      page: 1,          // 当前页
      pageSize: 10,     // 每页条数
      tableData: []     // 表格数据
    }
  },
  created() {
    this.init()
  },
  methods: {
    async init() {
      const params = {
        page: this.page,
        pageSize: this.pageSize,
        name: this.input ? this.input : undefined
      }
      await getMemberList(params).then(res => {
        if (String(res.code) === '1') {
          this.tableData = res.data.records || []
          this.counts = res.data.total
        }
      })
    },
    // 每页条数变化
    handleSizeChange(val) {
      this.pageSize = val
      this.init()
    },
    // 页码变化
    handleCurrentChange(val) {
      this.page = val
      this.init()
    },
    // 点击查询按钮
    handleQuery() {
      this.page = 1
      this.init()
    }
  }
})

功能测试

场景 操作 预期结果
首次进入 登录后进员工管理页 显示第 1 页,每页 10 条,总条数正确
切换每页条数 下拉改选 20 重新查询,LIMIT 0,20
翻页 点击第 2 page=2LIMIT 10,10
姓名查询 输入「张」点查询 LIKE '%张%',结果被过滤
空姓名 清空输入框点查询 LIKE 不拼接,返回全部

控制台日志验证

text 复制代码
==>  Preparing: SELECT COUNT(*) FROM employee
==> Parameters:
<==    Columns: COUNT(*)
<==        Row: 3
==>  Preparing: SELECT id,name,username,phone,sex,id_number,status,create_time,update_time,create_user,update_user FROM employee ORDER BY update_time DESC LIMIT ?
==> Parameters: 10(Long)
<==      Total: 3

带姓名时:

text 复制代码
==>  Preparing: SELECT COUNT(*) FROM employee WHERE (name LIKE ?)
==> Parameters: %张%(String)

注意 COUNT(*) 与数据查询是两条独立的 SQL ------ 这是分页插件的工作方式,也是分页查询在大表上性能敏感的原因(COUNT(*) 需要扫全表)。

API 速览

API / 注解 / 类 所属 作用
@Configuration Spring 声明配置类
@Bean Spring 把方法返回值注册为容器 Bean
MybatisPlusInterceptor MyBatis-Plus 3.4+ MP 插件总拦截器
PaginationInnerInterceptor MyBatis-Plus 3.4+ 分页插件,自动 COUNTLIMIT
Page<T> MyBatis-Plus 分页对象,含 records / total / size / current
IService.page(Page, Wrapper) MyBatis-Plus 分页查询,结果回填到传入的 Page
LambdaQueryWrapper<T> MyBatis-Plus Lambda 条件构造器
like(boolean, SFunction, Object) MyBatis-Plus 条件为 true 时拼接 LIKE
orderByDesc(SFunction) MyBatis-Plus 降序排序
@GetMapping Spring MVC 映射 GET 请求
StringUtils.isNotEmpty(String) commons-lang 判断字符串非 null 且非空串
log.info("{} {} {}", a, b, c) SLF4J 多占位符日志,按顺序对应
slot-scope ElementUI 作用域插槽,获取当前行数据

官方文档

总结

这篇博客围绕「员工管理」的分页查询,把 MyBatis-Plus 分页插件的用法完整走了一遍。核心就三步:先注册 MybatisPlusInterceptor 拦截器,再在 Controller 里构造 PageLambdaQueryWrapper,最后调用 employeeService.page() 把结果回填进 Page 返回给前端。

几个容易踩的坑也顺带提了:3.4.0 前后分页插件类名不一样、like 的条件开关怎么用、page() 不需要接收返回值、前端 status 数字要靠作用域插槽转成文字。把这些点记住,分页查询基本就不会卡壳了。

如果还想深入,建议去翻一下官方文档里条件构造器的其他方法(eqgebetween 等),组合起来能覆盖绝大多数查询场景。

相关推荐
MetaLite1 小时前
全网最好的SpringBoot接口请求对象设计
java·spring boot·后端
蜗牛互联网1 小时前
AI给面试打分不够,求职者更需要可核对的证据
java·人工智能·后端
此时不提桶,更待何时1 小时前
02-05-B-虚拟线程面试与生产事故实战
java·面试
名字还没想好☜1 小时前
Spring Boot 3.2 RestClient 实战:替代 RestTemplate 调外部接口,超时、连接池与错误处理
java·后端·spring
Wang's Blog2 小时前
Java 项目实战: 外卖平台-新增分类与type标志位设计
服务器·项目开发
摇滚侠2 小时前
《Spring Boot 3:高级与架构设计》第 2 章 IOC容器的高级机制 BeanFactoryPostProcessor 个人理解 6
java·spring boot·笔记·后端
程序员阿鹏2 小时前
简单介绍一下软引用
java·开发语言·jvm·数据结构·后端
SL_staff3 小时前
项目延期时如何用四类角色机制实现责任锚定?一线技术实践拆解
java·低代码·团队管理
wno7043 小时前
Spring Security Session管理
java·后端·spring