11.前后端分页功能详解

文章目录

    • 1. 分页功能基础概念
      • 1.1 什么是分页
      • 1.2 为什么需要分页
      • 1.3 分页的关键参数
      • 1.4 分页的实现方式
    • 2. MyBatis PageHelper详解
      • 2.1 PageHelper简介
      • 2.2 添加PageHelper依赖
      • 2.3 PageHelper配置
      • 2.4 创建实体类
      • 2.5 创建Mapper接口
      • 2.6 创建Mapper XML文件
    • 3. 封装分页请求和响应参数
      • 3.1 分页请求参数封装
      • 3.2 用户查询请求参数
      • 3.3 分页响应结果封装
    • 4. Service层分页实现
      • 4.1 创建用户服务接口
      • 4.2 实现用户服务
    • 5. Controller层分页接口
      • 5.1 创建用户控制器
      • 5.2 分页接口使用示例
      • 5.3 分页响应示例
    • 6. 数据库表设计
      • 6.1 用户表结构
      • 6.2 插入测试数据
    • 7. 前端分页实现
      • 7.1 Vue.js分页组件
      • 7.2 用户列表页面组件
      • 7.3 API请求封装
      • 7.4 请求工具封装
    • 8. 高级分页功能
      • 8.1 虚拟滚动分页
      • 8.2 无限滚动分页
      • 8.3 本地存储分页状态
    • 9. 性能优化和最佳实践
      • 9.1 数据库查询优化
      • 9.2 缓存策略
      • 9.3 前端性能优化
      • 9.4 最佳实践总结
    • 10. 总结

1. 分页功能基础概念

1.1 什么是分页

分页是指将大量数据分割成多个小的数据块(页),每次只返回其中一页的数据,用户可以通过翻页来浏览所有数据。这样可以避免一次性加载过多数据导致的性能问题。

形象比喻:
想象分页就像阅读一本厚厚的书:

  • 整本书(全部数据):包含所有内容
  • 章节目录(分页导航):告诉你有多少页,现在在第几页
  • 当前页(当前页数据):你正在阅读的这一页
  • 翻页(分页操作):点击下一页、上一页、跳转到某页

1.2 为什么需要分页

  1. 性能优化:避免一次性查询和传输大量数据
  2. 用户体验:页面加载更快,操作更流畅
  3. 内存控制:减少服务器和客户端的内存占用
  4. 网络带宽:减少网络传输量
  5. 数据库压力:降低数据库查询压力

1.3 分页的关键参数

/**
 * 分页核心参数
 */
public class PageParams {
    
    // 当前页码(从1开始)
    private Integer pageNum = 1;
    
    // 每页显示数量
    private Integer pageSize = 10;
    
    // 总记录数
    private Long total;
    
    // 总页数
    private Integer pages;
    
    // 是否有下一页
    private Boolean hasNext;
    
    // 是否有上一页
    private Boolean hasPrev;
}

1.4 分页的实现方式

  1. 物理分页:在数据库层面限制查询结果数量(推荐)
  2. 逻辑分页:查询全部数据后在内存中分页(不推荐)
  3. 前端分页:一次性获取数据,在前端进行分页显示

2. MyBatis PageHelper详解

2.1 PageHelper简介

PageHelper是MyBatis的分页插件,它通过拦截器机制在SQL执行前自动添加分页相关的SQL语句,实现物理分页。

2.2 添加PageHelper依赖

<dependencies>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-boot-starterartifactId>
        <version>3.5.3version>
    dependency>
    
    
    <dependency>
        <groupId>com.github.pagehelpergroupId>
        <artifactId>pagehelper-spring-boot-starterartifactId>
        <version>1.4.6version>
    dependency>
    
    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <scope>runtimescope>
    dependency>
dependencies>

2.3 PageHelper配置

application.yml中配置PageHelper:

# 数据库配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.entity

# PageHelper分页插件配置
pagehelper:
  # 指定数据库类型
  helper-dialect: mysql
  # 启用合理化,pageNum<=0时查询第一页,pageNum>总页数时查询最后一页
  reasonable: true
  # 支持通过Mapper接口参数来传递分页参数
  support-methods-arguments: true
  # 分页参数合理化,当pageNum<=0时查询第一页,pageNum>pages时查询最后一页
  page-size-zero: false
  # 为了支持startPage(Object params)方法,增加了该参数来配置参数映射
  params: count=countSql

2.4 创建实体类

package com.example.entity;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDateTime;

/**
 * 用户实体类
 */
public class User {
    
    private Long id;
    
    private String username;
    
    private String email;
    
    private String phone;
    
    private Integer age;
    
    private String status;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    
    // 构造方法
    public User() {}
    
    public User(String username, String email, String phone, Integer age) {
        this.username = username;
        this.email = email;
        this.phone = phone;
        this.age = age;
        this.status = "ACTIVE";
        this.createTime = LocalDateTime.now();
        this.updateTime = LocalDateTime.now();
    }
    
    // getter和setter方法
    public Long getId() {
        return id;
    }
    
    public void setId(Long id) {
        this.id = id;
    }
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        this.email = email;
    }
    
    public String getPhone() {
        return phone;
    }
    
    public void setPhone(String phone) {
        this.phone = phone;
    }
    
    public Integer getAge() {
        return age;
    }
    
    public void setAge(Integer age) {
        this.age = age;
    }
    
    public String getStatus() {
        return status;
    }
    
    public void setStatus(String status) {
        this.status = status;
    }
    
    public LocalDateTime getCreateTime() {
        return createTime;
    }
    
    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }
    
    public LocalDateTime getUpdateTime() {
        return updateTime;
    }
    
    public void setUpdateTime(LocalDateTime updateTime) {
        this.updateTime = updateTime;
    }
}

2.5 创建Mapper接口

package com.example.mapper;

import com.example.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 用户Mapper接口
 */
@Mapper
public interface UserMapper {
    
    /**
     * 查询所有用户
     */
    List<User> selectAllUsers();
    
    /**
     * 根据条件查询用户
     */
    List<User> selectUsersByCondition(@Param("username") String username,
                                     @Param("email") String email,
                                     @Param("status") String status);
    
    /**
     * 根据年龄范围查询用户
     */
    List<User> selectUsersByAgeRange(@Param("minAge") Integer minAge,
                                    @Param("maxAge") Integer maxAge);
    
    /**
     * 根据ID查询用户
     */
    User selectUserById(@Param("id") Long id);
    
    /**
     * 插入用户
     */
    int insertUser(User user);
    
    /**
     * 更新用户
     */
    int updateUser(User user);
    
    /**
     * 删除用户
     */
    int deleteUserById(@Param("id") Long id);
}

2.6 创建Mapper XML文件

src/main/resources/mapper/UserMapper.xml中:


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mapper.UserMapper">
    
    
    <resultMap id="UserResultMap" type="com.example.entity.User">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="email" property="email"/>
        <result column="phone" property="phone"/>
        <result column="age" property="age"/>
        <result column="status" property="status"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
    resultMap>
    
    
    <select id="selectAllUsers" resultMap="UserResultMap">
        SELECT id, username, email, phone, age, status, create_time, update_time
        FROM users
        ORDER BY create_time DESC
    select>
    
    
    <select id="selectUsersByCondition" resultMap="UserResultMap">
        SELECT id, username, email, phone, age, status, create_time, update_time
        FROM users
        <where>
            <if test="username != null and username != ''">
                AND username LIKE CONCAT('%', #{username}, '%')
            if>
            <if test="email != null and email != ''">
                AND email LIKE CONCAT('%', #{email}, '%')
            if>
            <if test="status != null and status != ''">
                AND status = #{status}
            if>
        where>
        ORDER BY create_time DESC
    select>
    
    
    <select id="selectUsersByAgeRange" resultMap="UserResultMap">
        SELECT id, username, email, phone, age, status, create_time, update_time
        FROM users
        <where>
            <if test="minAge != null">
                AND age >= #{minAge}
            if>
            <if test="maxAge != null">
                AND age <= #{maxAge}
            if>
        where>
        ORDER BY age ASC
    select>
    
    
    <select id="selectUserById" resultMap="UserResultMap">
        SELECT id, username, email, phone, age, status, create_time, update_time
        FROM users
        WHERE id = #{id}
    select>
    
    
    <insert id="insertUser" parameterType="com.example.entity.User" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO users (username, email, phone, age, status, create_time, update_time)
        VALUES (#{username}, #{email}, #{phone}, #{age}, #{status}, #{createTime}, #{updateTime})
    insert>
    
    
    <update id="updateUser" parameterType="com.example.entity.User">
        UPDATE users SET
            username = #{username},
            email = #{email},
            phone = #{phone},
            age = #{age},
            status = #{status},
            update_time = #{updateTime}
        WHERE id = #{id}
    update>
    
    
    <delete id="deleteUserById">
        DELETE FROM users WHERE id = #{id}
    delete>
    
mapper>

3. 封装分页请求和响应参数

3.1 分页请求参数封装

package com.example.dto;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;

/**
 * 分页请求参数基类
 */
public class PageRequest {
    
    /**
     * 当前页码,从1开始
     */
    @Min(value = 1, message = "页码不能小于1")
    private Integer pageNum = 1;
    
    /**
     * 每页显示数量
     */
    @Min(value = 1, message = "每页显示数量不能小于1")
    @Max(value = 100, message = "每页显示数量不能大于100")
    private Integer pageSize = 10;
    
    /**
     * 排序字段
     */
    private String orderBy;
    
    /**
     * 排序方向:ASC(升序)、DESC(降序)
     */
    private String sortOrder = "DESC";
    
    public PageRequest() {}
    
    public PageRequest(Integer pageNum, Integer pageSize) {
        this.pageNum = pageNum;
        this.pageSize = pageSize;
    }
    
    // getter和setter方法
    public Integer getPageNum() {
        return pageNum;
    }
    
    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }
    
    public Integer getPageSize() {
        return pageSize;
    }
    
    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }
    
    public String getOrderBy() {
        return orderBy;
    }
    
    public void setOrderBy(String orderBy) {
        this.orderBy = orderBy;
    }
    
    public String getSortOrder() {
        return sortOrder;
    }
    
    public void setSortOrder(String sortOrder) {
        this.sortOrder = sortOrder;
    }
}

3.2 用户查询请求参数

package com.example.dto;

/**
 * 用户查询请求参数
 * 继承分页参数,添加用户特有的查询条件
 */
public class UserQueryRequest extends PageRequest {
    
    /**
     * 用户名(模糊查询)
     */
    private String username;
    
    /**
     * 邮箱(模糊查询)
     */
    private String email;
    
    /**
     * 手机号
     */
    private String phone;
    
    /**
     * 状态
     */
    private String status;
    
    /**
     * 最小年龄
     */
    private Integer minAge;
    
    /**
     * 最大年龄
     */
    private Integer maxAge;
    
    /**
     * 开始时间(创建时间范围查询)
     */
    private String startTime;
    
    /**
     * 结束时间(创建时间范围查询)
     */
    private String endTime;
    
    public UserQueryRequest() {}
    
    // getter和setter方法
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        this.email = email;
    }
    
    public String getPhone() {
        return phone;
    }
    
    public void setPhone(String phone) {
        this.phone = phone;
    }
    
    public String getStatus() {
        return status;
    }
    
    public void setStatus(String status) {
        this.status = status;
    }
    
    public Integer getMinAge() {
        return minAge;
    }
    
    public void setMinAge(Integer minAge) {
        this.minAge = minAge;
    }
    
    public Integer getMaxAge() {
        return maxAge;
    }
    
    public void setMaxAge(Integer maxAge) {
        this.maxAge = maxAge;
    }
    
    public String getStartTime() {
        return startTime;
    }
    
    public void setStartTime(String startTime) {
        this.startTime = startTime;
    }
    
    public String getEndTime() {
        return endTime;
    }
    
    public void setEndTime(String endTime) {
        this.endTime = endTime;
    }
}

3.3 分页响应结果封装

package com.example.common;

import com.github.pagehelper.PageInfo;

import java.util.List;

/**
 * 分页响应结果封装
 * @param  数据类型
 */
public class PageResult<T> {
    
    /**
     * 当前页码
     */
    private Integer pageNum;
    
    /**
     * 每页显示数量
     */
    private Integer pageSize;
    
    /**
     * 总记录数
     */
    private Long total;
    
    /**
     * 总页数
     */
    private Integer pages;
    
    /**
     * 当前页数据
     */
    private List<T> list;
    
    /**
     * 是否有下一页
     */
    private Boolean hasNext;
    
    /**
     * 是否有上一页
     */
    private Boolean hasPrev;
    
    /**
     * 是否是第一页
     */
    private Boolean isFirst;
    
    /**
     * 是否是最后一页
     */
    private Boolean isLast;
    
    public PageResult() {}
    
    /**
     * 基于PageInfo构造分页结果
     */
    public PageResult(PageInfo<T> pageInfo) {
        this.pageNum = pageInfo.getPageNum();
        this.pageSize = pageInfo.getPageSize();
        this.total = pageInfo.getTotal();
        this.pages = pageInfo.getPages();
        this.list = pageInfo.getList();
        this.hasNext = pageInfo.isHasNextPage();
        this.hasPrev = pageInfo.isHasPreviousPage();
        this.isFirst = pageInfo.isIsFirstPage();
        this.isLast = pageInfo.isIsLastPage();
    }
    
    /**
     * 创建分页结果的静态方法
     */
    public static <T> PageResult<T> of(PageInfo<T> pageInfo) {
        return new PageResult<>(pageInfo);
    }
    
    /**
     * 创建空的分页结果
     */
    public static <T> PageResult<T> empty() {
        PageResult<T> result = new PageResult<>();
        result.pageNum = 1;
        result.pageSize = 10;
        result.total = 0L;
        result.pages = 0;
        result.list = List.of();
        result.hasNext = false;
        result.hasPrev = false;
        result.isFirst = true;
        result.isLast = true;
        return result;
    }
    
    // getter和setter方法
    public Integer getPageNum() {
        return pageNum;
    }
    
    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }
    
    public Integer getPageSize() {
        return pageSize;
    }
    
    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }
    
    public Long getTotal() {
        return total;
    }
    
    public void setTotal(Long total) {
        this.total = total;
    }
    
    public Integer getPages() {
        return pages;
    }
    
    public void setPages(Integer pages) {
        this.pages = pages;
    }
    
    public List<T> getList() {
        return list;
    }
    
    public void setList(List<T> list) {
        this.list = list;
    }
    
    public Boolean getHasNext() {
        return hasNext;
    }
    
    public void setHasNext(Boolean hasNext) {
        this.hasNext = hasNext;
    }
    
    public Boolean getHasPrev() {
        return hasPrev;
    }
    
    public void setHasPrev(Boolean hasPrev) {
        this.hasPrev = hasPrev;
    }
    
    public Boolean getIsFirst() {
        return isFirst;
    }
    
    public void setIsFirst(Boolean isFirst) {
        this.isFirst = isFirst;
    }
    
    public Boolean getIsLast() {
        return isLast;
    }
    
    public void setIsLast(Boolean isLast) {
        this.isLast = isLast;
    }
} 

4. Service层分页实现

4.1 创建用户服务接口

package com.example.service;

import com.example.dto.UserQueryRequest;
import com.example.entity.User;
import com.example.common.PageResult;

/**
 * 用户服务接口
 */
public interface UserService {
    
    /**
     * 分页查询所有用户
     */
    PageResult<User> getAllUsers(UserQueryRequest request);
    
    /**
     * 根据条件分页查询用户
     */
    PageResult<User> getUsersByCondition(UserQueryRequest request);
    
    /**
     * 根据年龄范围分页查询用户
     */
    PageResult<User> getUsersByAgeRange(Integer minAge, Integer maxAge, 
                                      Integer pageNum, Integer pageSize);
    
    /**
     * 根据ID查询用户
     */
    User getUserById(Long id);
    
    /**
     * 创建用户
     */
    User createUser(User user);
    
    /**
     * 更新用户
     */
    User updateUser(User user);
    
    /**
     * 删除用户
     */
    boolean deleteUser(Long id);
    
    /**
     * 批量删除用户
     */
    boolean deleteUsers(List<Long> ids);
}

4.2 实现用户服务

package com.example.service.impl;

import com.example.dto.UserQueryRequest;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import com.example.common.PageResult;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.List;

/**
 * 用户服务实现类
 */
@Service
public class UserServiceImpl implements UserService {
    
    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * 分页查询所有用户
     */
    @Override
    public PageResult<User> getAllUsers(UserQueryRequest request) {
        logger.info("分页查询所有用户,页码:{},每页数量:{}", request.getPageNum(), request.getPageSize());
        
        // 设置分页参数
        PageHelper.startPage(request.getPageNum(), request.getPageSize());
        
        // 设置排序
        if (StringUtils.hasText(request.getOrderBy())) {
            String orderBy = request.getOrderBy() + " " + request.getSortOrder();
            PageHelper.orderBy(orderBy);
        }
        
        // 执行查询
        List<User> users = userMapper.selectAllUsers();
        
        // 创建分页信息
        PageInfo<User> pageInfo = new PageInfo<>(users);
        
        logger.info("查询完成,总记录数:{},总页数:{}", pageInfo.getTotal(), pageInfo.getPages());
        
        return PageResult.of(pageInfo);
    }
    
    /**
     * 根据条件分页查询用户
     */
    @Override
    public PageResult<User> getUsersByCondition(UserQueryRequest request) {
        logger.info("根据条件分页查询用户,查询条件:{}", request);
        
        // 设置分页参数
        PageHelper.startPage(request.getPageNum(), request.getPageSize());
        
        // 设置排序
        if (StringUtils.hasText(request.getOrderBy())) {
            String orderBy = request.getOrderBy() + " " + request.getSortOrder();
            PageHelper.orderBy(orderBy);
        }
        
        // 执行条件查询
        List<User> users = userMapper.selectUsersByCondition(
            request.getUsername(),
            request.getEmail(),
            request.getStatus()
        );
        
        // 创建分页信息
        PageInfo<User> pageInfo = new PageInfo<>(users);
        
        logger.info("条件查询完成,总记录数:{},总页数:{}", pageInfo.getTotal(), pageInfo.getPages());
        
        return PageResult.of(pageInfo);
    }
    
    /**
     * 根据年龄范围分页查询用户
     */
    @Override
    public PageResult<User> getUsersByAgeRange(Integer minAge, Integer maxAge, 
                                             Integer pageNum, Integer pageSize) {
        logger.info("根据年龄范围分页查询用户,年龄范围:{}-{},页码:{},每页数量:{}", 
                   minAge, maxAge, pageNum, pageSize);
        
        // 设置分页参数
        PageHelper.startPage(pageNum, pageSize);
        
        // 执行年龄范围查询
        List<User> users = userMapper.selectUsersByAgeRange(minAge, maxAge);
        
        // 创建分页信息
        PageInfo<User> pageInfo = new PageInfo<>(users);
        
        logger.info("年龄范围查询完成,总记录数:{},总页数:{}", pageInfo.getTotal(), pageInfo.getPages());
        
        return PageResult.of(pageInfo);
    }
    
    /**
     * 根据ID查询用户
     */
    @Override
    public User getUserById(Long id) {
        logger.info("根据ID查询用户:{}", id);
        
        User user = userMapper.selectUserById(id);
        
        if (user == null) {
            logger.warn("用户不存在,ID:{}", id);
        } else {
            logger.info("查询用户成功:{}", user.getUsername());
        }
        
        return user;
    }
    
    /**
     * 创建用户
     */
    @Override
    public User createUser(User user) {
        logger.info("创建用户:{}", user.getUsername());
        
        // 设置创建时间和更新时间
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        user.setStatus("ACTIVE");
        
        // 插入用户
        int result = userMapper.insertUser(user);
        
        if (result > 0) {
            logger.info("用户创建成功,ID:{}", user.getId());
            return user;
        } else {
            logger.error("用户创建失败:{}", user.getUsername());
            throw new RuntimeException("用户创建失败");
        }
    }
    
    /**
     * 更新用户
     */
    @Override
    public User updateUser(User user) {
        logger.info("更新用户:{}", user.getId());
        
        // 设置更新时间
        user.setUpdateTime(LocalDateTime.now());
        
        // 更新用户
        int result = userMapper.updateUser(user);
        
        if (result > 0) {
            logger.info("用户更新成功,ID:{}", user.getId());
            return user;
        } else {
            logger.error("用户更新失败,ID:{}", user.getId());
            throw new RuntimeException("用户更新失败");
        }
    }
    
    /**
     * 删除用户
     */
    @Override
    public boolean deleteUser(Long id) {
        logger.info("删除用户:{}", id);
        
        int result = userMapper.deleteUserById(id);
        
        if (result > 0) {
            logger.info("用户删除成功,ID:{}", id);
            return true;
        } else {
            logger.warn("用户删除失败,用户可能不存在,ID:{}", id);
            return false;
        }
    }
    
    /**
     * 批量删除用户
     */
    @Override
    public boolean deleteUsers(List<Long> ids) {
        logger.info("批量删除用户,数量:{}", ids.size());
        
        int successCount = 0;
        for (Long id : ids) {
            if (deleteUser(id)) {
                successCount++;
            }
        }
        
        logger.info("批量删除完成,成功删除:{}/{}", successCount, ids.size());
        
        return successCount == ids.size();
    }
}

5. Controller层分页接口

5.1 创建用户控制器

package com.example.controller;

import com.example.common.PageResult;
import com.example.common.Result;
import com.example.dto.UserQueryRequest;
import com.example.entity.User;
import com.example.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import java.util.List;

/**
 * 用户控制器
 * 提供用户相关的分页接口
 */
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
    
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);
    
    @Autowired
    private UserService userService;
    
    /**
     * 分页查询所有用户
     * GET /api/users?pageNum=1&pageSize=10&orderBy=createTime&sortOrder=DESC
     */
    @GetMapping
    public Result<PageResult<User>> getAllUsers(@Valid UserQueryRequest request) {
        logger.info("接收到分页查询请求:页码={}, 每页数量={}", request.getPageNum(), request.getPageSize());
        
        PageResult<User> pageResult = userService.getAllUsers(request);
        
        return Result.success("查询成功", pageResult);
    }
    
    /**
     * 根据条件分页查询用户
     * POST /api/users/search
     */
    @PostMapping("/search")
    public Result<PageResult<User>> searchUsers(@Valid @RequestBody UserQueryRequest request) {
        logger.info("接收到条件查询请求:{}", request);
        
        PageResult<User> pageResult = userService.getUsersByCondition(request);
        
        return Result.success("查询成功", pageResult);
    }
    
    /**
     * 根据年龄范围分页查询用户
     * GET /api/users/age-range?minAge=18&maxAge=30&pageNum=1&pageSize=10
     */
    @GetMapping("/age-range")
    public Result<PageResult<User>> getUsersByAgeRange(
            @RequestParam(required = false) Integer minAge,
            @RequestParam(required = false) Integer maxAge,
            @RequestParam(defaultValue = "1") @Min(value = 1, message = "页码不能小于1") Integer pageNum,
            @RequestParam(defaultValue = "10") @Min(value = 1, message = "每页数量不能小于1") Integer pageSize) {
        
        logger.info("根据年龄范围查询用户:{}-{}, 页码={}, 每页数量={}", minAge, maxAge, pageNum, pageSize);
        
        PageResult<User> pageResult = userService.getUsersByAgeRange(minAge, maxAge, pageNum, pageSize);
        
        return Result.success("查询成功", pageResult);
    }
    
    /**
     * 根据ID查询单个用户
     * GET /api/users/{id}
     */
    @GetMapping("/{id}")
    public Result<User> getUserById(@PathVariable @Min(value = 1, message = "用户ID必须大于0") Long id) {
        logger.info("根据ID查询用户:{}", id);
        
        User user = userService.getUserById(id);
        
        if (user != null) {
            return Result.success("查询成功", user);
        } else {
            return Result.error(404, "用户不存在");
        }
    }
    
    /**
     * 创建用户
     * POST /api/users
     */
    @PostMapping
    public Result<User> createUser(@Valid @RequestBody User user) {
        logger.info("创建用户:{}", user.getUsername());
        
        User createdUser = userService.createUser(user);
        
        return Result.success("用户创建成功", createdUser);
    }
    
    /**
     * 更新用户
     * PUT /api/users/{id}
     */
    @PutMapping("/{id}")
    public Result<User> updateUser(@PathVariable @Min(value = 1, message = "用户ID必须大于0") Long id,
                                  @Valid @RequestBody User user) {
        logger.info("更新用户:{}", id);
        
        user.setId(id);
        User updatedUser = userService.updateUser(user);
        
        return Result.success("用户更新成功", updatedUser);
    }
    
    /**
     * 删除单个用户
     * DELETE /api/users/{id}
     */
    @DeleteMapping("/{id}")
    public Result<Void> deleteUser(@PathVariable @Min(value = 1, message = "用户ID必须大于0") Long id) {
        logger.info("删除用户:{}", id);
        
        boolean success = userService.deleteUser(id);
        
        if (success) {
            return Result.success("用户删除成功");
        } else {
            return Result.error(404, "用户不存在或删除失败");
        }
    }
    
    /**
     * 批量删除用户
     * DELETE /api/users/batch
     */
    @DeleteMapping("/batch")
    public Result<Void> deleteUsers(@RequestBody @NotEmpty(message = "删除的用户ID列表不能为空") List<Long> ids) {
        logger.info("批量删除用户,数量:{}", ids.size());
        
        boolean success = userService.deleteUsers(ids);
        
        if (success) {
            return Result.success("批量删除成功");
        } else {
            return Result.error("批量删除失败,部分用户可能不存在");
        }
    }
}

5.2 分页接口使用示例

1. 基础分页查询

# 查询第1页,每页10条记录
GET /api/users?pageNum=1&pageSize=10

# 查询第2页,每页20条记录,按创建时间倒序排列
GET /api/users?pageNum=2&pageSize=20&orderBy=createTime&sortOrder=DESC

# 查询第1页,每页5条记录,按年龄升序排列
GET /api/users?pageNum=1&pageSize=5&orderBy=age&sortOrder=ASC

2. 条件查询分页

# POST /api/users/search
Content-Type: application/json

{
    "pageNum": 1,
    "pageSize": 10,
    "username": "张",
    "status": "ACTIVE",
    "minAge": 18,
    "maxAge": 30,
    "orderBy": "createTime",
    "sortOrder": "DESC"
}

3. 年龄范围查询

# 查询18-30岁的用户,第1页,每页10条
GET /api/users/age-range?minAge=18&maxAge=30&pageNum=1&pageSize=10

5.3 分页响应示例

{
    "code": 200,
    "message": "查询成功",
    "success": true,
    "timestamp": "2024-01-15 10:30:00",
    "data": {
        "pageNum": 1,
        "pageSize": 10,
        "total": 156,
        "pages": 16,
        "list": [
            {
                "id": 1,
                "username": "张三",
                "email": "[email protected]",
                "phone": "13800138001",
                "age": 25,
                "status": "ACTIVE",
                "createTime": "2024-01-15 10:00:00",
                "updateTime": "2024-01-15 10:00:00"
            },
            {
                "id": 2,
                "username": "李四",
                "email": "[email protected]",
                "phone": "13800138002",
                "age": 28,
                "status": "ACTIVE",
                "createTime": "2024-01-15 09:30:00",
                "updateTime": "2024-01-15 09:30:00"
            }
            // ... 其他8条记录
        ],
        "hasNext": true,
        "hasPrev": false,
        "isFirst": true,
        "isLast": false
    }
}

6. 数据库表设计

6.1 用户表结构

-- 创建数据库
CREATE DATABASE IF NOT EXISTS test_db 
DEFAULT CHARACTER SET utf8mb4 
DEFAULT COLLATE utf8mb4_unicode_ci;

USE test_db;

-- 创建用户表
CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
    username VARCHAR(50) NOT NULL COMMENT '用户名',
    email VARCHAR(100) NOT NULL COMMENT '邮箱',
    phone VARCHAR(20) COMMENT '手机号',
    age INT COMMENT '年龄',
    status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE-活跃,INACTIVE-非活跃',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    
    UNIQUE KEY uk_username (username),
    UNIQUE KEY uk_email (email),
    INDEX idx_age (age),
    INDEX idx_status (status),
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

6.2 插入测试数据

-- 插入测试数据
INSERT INTO users (username, email, phone, age, status) VALUES
('张三', '[email protected]', '13800138001', 25, 'ACTIVE'),
('李四', '[email protected]', '13800138002', 28, 'ACTIVE'),
('王五', '[email protected]', '13800138003', 22, 'ACTIVE'),
('赵六', '[email protected]', '13800138004', 35, 'INACTIVE'),
('钱七', '[email protected]', '13800138005', 29, 'ACTIVE'),
('孙八', '[email protected]', '13800138006', 31, 'ACTIVE'),
('周九', '[email protected]', '13800138007', 27, 'ACTIVE'),
('吴十', '[email protected]', '13800138008', 24, 'INACTIVE'),
('郑十一', '[email protected]', '13800138009', 26, 'ACTIVE'),
('王十二', '[email protected]', '13800138010', 30, 'ACTIVE');

-- 批量插入更多测试数据(用于测试分页效果)
INSERT INTO users (username, email, phone, age, status)
SELECT 
    CONCAT('用户', LPAD((@row_number:=@row_number+1), 4, '0')) as username,
    CONCAT('user', LPAD(@row_number, 4, '0'), '@example.com') as email,
    CONCAT('138', LPAD(@row_number, 8, '0')) as phone,
    18 + FLOOR(RAND() * 42) as age,
    CASE WHEN RAND() > 0.2 THEN 'ACTIVE' ELSE 'INACTIVE' END as status
FROM 
    (SELECT @row_number:=10) r
CROSS JOIN 
    (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t1
CROSS JOIN 
    (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t2
CROSS JOIN 
    (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t3
LIMIT 150;

7. 前端分页实现

7.1 Vue.js分页组件

创建一个通用的分页组件 Pagination.vue






7.2 用户列表页面组件






7.3 API请求封装

创建 api/user.js 文件:

import request from '@/utils/request'

/**
 * 获取用户列表(分页)
 */
export function getUserList(params) {
  return request({
    url: '/api/users',
    method: 'get',
    params
  })
}

/**
 * 根据条件搜索用户(分页)
 */
export function searchUsers(data) {
  return request({
    url: '/api/users/search',
    method: 'post',
    data
  })
}

/**
 * 根据年龄范围查询用户
 */
export function getUsersByAgeRange(params) {
  return request({
    url: '/api/users/age-range',
    method: 'get',
    params
  })
}

/**
 * 根据ID获取用户详情
 */
export function getUserById(id) {
  return request({
    url: `/api/users/${id}`,
    method: 'get'
  })
}

/**
 * 创建用户
 */
export function createUser(data) {
  return request({
    url: '/api/users',
    method: 'post',
    data
  })
}

/**
 * 更新用户
 */
export function updateUser(id, data) {
  return request({
    url: `/api/users/${id}`,
    method: 'put',
    data
  })
}

/**
 * 删除用户
 */
export function deleteUser(id) {
  return request({
    url: `/api/users/${id}`,
    method: 'delete'
  })
}

/**
 * 批量删除用户
 */
export function batchDeleteUsers(ids) {
  return request({
    url: '/api/users/batch',
    method: 'delete',
    data: ids
  })
}

7.4 请求工具封装

创建 utils/request.js 文件:

import axios from 'axios'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API || 'http://localhost:8080',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 在发送请求之前做些什么
    
    // 添加认证token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`
    }
    
    // 打印请求信息(开发环境)
    if (process.env.NODE_ENV === 'development') {
      console.log('发送请求:', config.method.toUpperCase(), config.url, config.params || config.data)
    }
    
    return config
  },
  error => {
    console.error('请求错误:', error)
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data
    
    // 打印响应信息(开发环境)
    if (process.env.NODE_ENV === 'development') {
      console.log('收到响应:', response.config.url, res)
    }
    
    // 如果响应成功码不是200,则判断为错误
    if (res.code !== 200) {
      console.error('业务错误:', res.message)
      
      // 特殊错误码处理
      if (res.code === 401) {
        // 未授权,跳转到登录页
        localStorage.removeItem('token')
        window.location.href = '/login'
      }
      
      return Promise.reject(new Error(res.message || '请求失败'))
    }
    
    return res
  },
  error => {
    console.error('响应错误:', error)
    
    let message = '网络错误'
    
    if (error.response) {
      // 服务器返回了错误状态码
      const status = error.response.status
      switch (status) {
        case 400:
          message = '请求参数错误'
          break
        case 401:
          message = '未授权,请重新登录'
          localStorage.removeItem('token')
          window.location.href = '/login'
          break
        case 403:
          message = '权限不足'
          break
        case 404:
          message = '请求的资源不存在'
          break
        case 500:
          message = '服务器内部错误'
          break
        default:
          message = `请求失败 (${status})`
      }
    } else if (error.request) {
      // 请求已发出但没有收到响应
      message = '网络连接超时'
    }
    
    return Promise.reject(new Error(message))
  }
)

export default service

8. 高级分页功能

8.1 虚拟滚动分页

对于大量数据的场景,可以使用虚拟滚动来优化性能:




8.2 无限滚动分页




8.3 本地存储分页状态

// utils/pageState.js
export class PageStateManager {
  constructor(key) {
    this.storageKey = `page_state_${key}`
  }
  
  // 保存分页状态
  saveState(state) {
    const pageState = {
      pageNum: state.pageNum,
      pageSize: state.pageSize,
      searchForm: state.searchForm,
      timestamp: Date.now()
    }
    localStorage.setItem(this.storageKey, JSON.stringify(pageState))
  }
  
  // 恢复分页状态
  restoreState() {
    try {
      const stored = localStorage.getItem(this.storageKey)
      if (!stored) return null
      
      const state = JSON.parse(stored)
      
      // 检查状态是否过期(1小时)
      if (Date.now() - state.timestamp > 3600000) {
        this.clearState()
        return null
      }
      
      return state
    } catch (error) {
      console.error('恢复分页状态失败:', error)
      return null
    }
  }
  
  // 清除分页状态
  clearState() {
    localStorage.removeItem(this.storageKey)
  }
}

// 在Vue组件中使用
export default {
  data() {
    return {
      pageStateManager: new PageStateManager('user_list')
    }
  },
  mounted() {
    // 恢复分页状态
    const savedState = this.pageStateManager.restoreState()
    if (savedState) {
      this.pageData.pageNum = savedState.pageNum
      this.pageData.pageSize = savedState.pageSize
      this.searchForm = { ...this.searchForm, ...savedState.searchForm }
    }
    
    this.loadUsers()
  },
  beforeUnmount() {
    // 保存分页状态
    this.pageStateManager.saveState({
      pageNum: this.pageData.pageNum,
      pageSize: this.pageData.pageSize,
      searchForm: this.searchForm
    })
  }
}

9. 性能优化和最佳实践

9.1 数据库查询优化

-- 为分页查询添加合适的索引
CREATE INDEX idx_users_create_time ON users(create_time);
CREATE INDEX idx_users_status_create_time ON users(status, create_time);
CREATE INDEX idx_users_age_status ON users(age, status);

-- 复合索引优化多条件查询
CREATE INDEX idx_users_search ON users(status, age, create_time);

-- 避免使用SELECT *,明确指定需要的字段
SELECT id, username, email, phone, age, status, create_time 
FROM users 
WHERE status = 'ACTIVE' 
ORDER BY create_time DESC 
LIMIT 10 OFFSET 0;

9.2 缓存策略

/**
 * 用户服务缓存实现
 */
@Service
public class CachedUserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 缓存分页查询结果
     */
    public PageResult<User> getUsersWithCache(UserQueryRequest request) {
        // 构建缓存key
        String cacheKey = buildCacheKey(request);
        
        // 尝试从缓存获取
        PageResult<User> cached = (PageResult<User>) redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            logger.info("从缓存获取分页数据: {}", cacheKey);
            return cached;
        }
        
        // 从数据库查询
        PageResult<User> result = getAllUsers(request);
        
        // 缓存结果(5分钟过期)
        redisTemplate.opsForValue().set(cacheKey, result, 5, TimeUnit.MINUTES);
        
        return result;
    }
    
    private String buildCacheKey(UserQueryRequest request) {
        return String.format("user_page_%d_%d_%s_%s_%s", 
            request.getPageNum(), 
            request.getPageSize(),
            Objects.toString(request.getUsername(), ""),
            Objects.toString(request.getStatus(), ""),
            Objects.toString(request.getOrderBy(), ""));
    }
}

9.3 前端性能优化

// 防抖搜索
export default {
  data() {
    return {
      searchDebounceTimer: null
    }
  },
  methods: {
    // 防抖搜索
    onSearchInputChange() {
      clearTimeout(this.searchDebounceTimer)
      this.searchDebounceTimer = setTimeout(() => {
        this.searchUsers()
      }, 300)
    },
    
    // 使用requestAnimationFrame优化大量DOM更新
    updateUserList(users) {
      requestAnimationFrame(() => {
        this.userList = users
      })
    },
    
    // 分批处理大量数据
    processBatchUsers(users) {
      const batchSize = 100
      let index = 0
      
      const processBatch = () => {
        const batch = users.slice(index, index + batchSize)
        this.processUsers(batch)
        
        index += batchSize
        if (index < users.length) {
          setTimeout(processBatch, 0)
        }
      }
      
      processBatch()
    }
  }
}

9.4 最佳实践总结

  1. 后端优化

    • 使用合适的数据库索引
    • 避免N+1查询问题
    • 实现查询结果缓存
    • 限制单页最大数据量
  2. 前端优化

    • 实现虚拟滚动处理大量数据
    • 使用防抖优化搜索功能
    • 缓存分页状态
    • 懒加载图片和其他资源
  3. 用户体验

    • 提供加载状态指示
    • 实现分页状态记忆
    • 支持快速跳转
    • 提供搜索和筛选功能
  4. 安全考虑

    • 限制分页参数范围
    • 验证排序字段合法性
    • 防止SQL注入
    • 实现访问权限控制

10. 总结

前后端分页功能是Web应用的核心功能之一,合理的分页设计能够显著提升应用性能和用户体验。通过本教程的学习,你应该掌握了:

  1. PageHelper的使用:配置和基本用法
  2. 参数封装:请求参数和响应结果的标准化
  3. 后端实现:Service和Controller层的完整实现
  4. 前端实现:Vue.js分页组件和列表页面
  5. 高级功能:虚拟滚动、无限滚动等
  6. 性能优化:数据库优化、缓存策略等

记住分页不仅仅是技术实现,更要考虑用户体验和系统性能的平衡!

你可能感兴趣的:(全栈项目,spring,boot,mybatis,后端,vue.js,java)