【基于SpringBoot的图书购买系统】深度讲解 分页查询用户信息,分析前后端交互的原理

发布于:2025-05-24 ⋅ 阅读:(16) ⋅ 点赞:(0)

引言📚

在企业级应用开发中,用户管理系统是几乎所有后台管理系统的核心模块之一。它不仅需要实现用户数据的增删改查,还需要考虑数据分页展示、状态管理、前后端交互效率等问题。本文将以一个实际的用户管理系统为例,详细讲解基于Spring Boot后端与jQuery前端的全栈开发过程,涵盖接口设计、业务逻辑实现、代码分层架构以及前后端联调等关键环节。通过本文的实践,读者将掌握如何使用主流框架构建一个结构清晰、可维护性强的管理系统,并深入理解前后端分离开发模式的核心思想。

在这里插入图片描述
在这里插入图片描述

1. 前后端交互接口设计📡

1.1 接口定义与功能说明

本系统的核心接口为用户列表分页查询接口,其设计遵循RESTful风格,主要负责返回指定页码的用户数据及分页信息。接口的基本信息如下:

  • URL/user/getUserInfoList

  • 请求方法GET

  • 请求参数:通过查询字符串传递分页参数,参数封装在PageRequest对象中,具体字段包括:

    • currentPage:当前页码,默认值为1,需满足currentPage > 0
    • pageSize:每页显示条数,默认值为10,需满足pageSize ≥ 1
    • offset:数据偏移量,由(currentPage-1)*pageSize自动计算得出
  • 响应数据:统一通过Result对象返回,结构如下:

    {
      "status": "SUCCESS", // 状态码(SUCCESS/FAIL/UNLOGIN等)
      "data": {
        "total": 100, // 总记录数
        "bookInfoList": [ // 用户数据列表(注:此处字段名可能存在语义偏差,实际应为userInfoList)
          {
            "id": 1,
            "userName": "admin",
            "deleteStatus": "存在", // 由deleteFlag转换而来的状态描述
            "createTime": "2025-05-19T12:00:00"
          }
        ],
        "pageRequest": { // 分页请求参数
          "currentPage": 1,
          "pageSize": 10,
          "offset": 0
        }
      }
    }
    

1.2 接口设计原则

  • 参数校验:在后端接口入口(Controller层)对currentPagepageSize进行合法性校验,防止非法参数导致数据库查询异常。
  • 数据封装:通过PageResult对象统一封装分页数据,避免在Controller层直接操作集合数据,提升代码复用性。
  • 状态转换:在Service层将数据库字段deleteFlag(0/1)转换为前端易读的deleteStatus(“存在”/“删除”),实现业务逻辑与数据持久化的解耦。

2. 后端代码逻辑架构🏗️

2.1 三层架构设计理念

本系统采用经典的MVC三层架构,将业务逻辑划分为Controller层(控制层)、Service层(服务层)和Mapper层(数据持久层),各层职责明确,依赖关系清晰:

  • Controller层:负责接收前端请求,进行参数校验和格式转换,调用Service层接口,并将结果封装为统一响应格式返回给前端。
  • Service层:实现核心业务逻辑,包括数据处理、事务管理、业务规则校验等,通过依赖注入调用Mapper层完成数据库操作。
  • Mapper层:基于MyBatis框架实现数据库访问,通过SQL语句操作数据表,返回原始数据或集合。
    在这里插入图片描述

2.2 关键技术栈

  • 框架:Spring Boot 2.6.0(提供快速开发脚手架)
  • 持久化:MyBatis(简化数据库操作)+ MySQL(存储用户数据)
  • 日志:Slf4j(统一日志管理)
  • 依赖管理:Maven(管理项目依赖)

3. 后端代码详解

3.1 Controller层:请求入口与响应封装

3.1.1 代码实现

@Slf4j
@RestController
@RequestMapping("/user")
public class UserInfoController {

    @Autowired
    private UserInfoService userInfoService;

    @RequestMapping("/getUserInfoList")
    public Result userInfoList(PageRequest pageRequest) {
        // 参数校验:页码和每页条数必须合法
        if (pageRequest.getPageSize() < 1 || pageRequest.getCurrentPage() <= 0) {
            log.warn("分页参数不合法:currentPage={}, pageSize={}", 
                     pageRequest.getCurrentPage(), pageRequest.getPageSize());
            return Result.fail("参数验证失败"); // 使用Result工具方法快速构建失败响应
        }

        PageResult<UserInfo> pageResult = null;
        try {
            pageResult = userInfoService.queryNormalUserInfoList(pageRequest);
        } catch (Exception e) {
            log.error("用户列表查询失败:{}", e.getMessage(), e); // 记录完整异常堆栈
            return Result.fail("服务器内部错误");
        }

        // 使用Result.success()封装成功响应
        return Result.success(pageResult);
    }
}

3.1.2 核心逻辑分析

  1. 参数自动绑定:Spring MVC会自动将前端传递的查询参数(如currentPage=1&pageSize=10)绑定到PageRequest对象,无需手动解析。
  2. 校验逻辑:通过判断pageSizecurrentPage的合法性,防止负数或零值导致数据库查询出错(如LIMIT -1, 10会引发SQL语法错误)。
  3. 异常处理:使用try-catch捕获Service层可能抛出的异常,避免原始异常直接返回给前端,提升接口安全性和用户体验。
  4. 响应封装:通过Result工具类的静态方法success()fail()统一响应格式,确保前端接收的数据结构一致。

3.2 Service层:业务逻辑核心

3.2.1 代码实现

@Slf4j
@Service
public class UserInfoService {

    @Autowired
    private UserInfoMapper userInfoMapper;

    /**
     * 查询普通用户列表(分页)
     * @param pageRequest 分页参数
     * @return 分页结果对象
     */
    @Transactional // 声明式事务管理(注:查询操作通常无需事务,此处可优化)
    public PageResult<UserInfo> queryNormalUserInfoList(PageRequest pageRequest) {
        if (pageRequest == null) {
            log.warn("接收到空的分页请求参数");
            return null;
        }

        // 1. 查询总记录数
        Integer total = userInfoMapper.queryNormalUserInfoCount();
        if (total == null || total < 0) {
            log.error("总记录数查询失败,返回非法值:{}", total);
            throw new RuntimeException("数据查询异常");
        }

        // 2. 查询当前页数据
        List<UserInfo> userInfoList = userInfoMapper.queryNormalUserInfoList(pageRequest);
        if (userInfoList == null) {
            log.warn("用户列表为空,currentPage={}, pageSize={}", 
                     pageRequest.getCurrentPage(), pageRequest.getPageSize());
            userInfoList = Collections.emptyList(); // 避免返回null引发前端空指针
        }

        // 3. 状态转换:数据库字段转业务描述
        for (UserInfo user : userInfoList) {
            user.setDeleteStatus(user.getDeleteFlag() == 0 ? "存在" : "删除");
        }

        // 4. 封装分页结果
        return new PageResult<>(total, userInfoList, pageRequest);
    }
}

3.2.2 核心逻辑分析

  1. 事务注解@Transactional用于声明事务边界,通常用于增删改操作。此处查询操作无需事务,可移除该注解以提升性能(避免开启不必要的数据库事务)。
  2. 业务规则
    • 确保总记录数total为非负数,防止分页计算异常(如总页数为负数)。
    • userInfoList转换为非空集合(Collections.emptyList()),避免前端处理null值。
  3. 状态转换:通过遍历集合将数据库中的deleteFlag(0/1)转换为deleteStatus(“存在”/“删除”),这种转换应在业务层完成,确保数据库字段与业务逻辑解耦。
  4. 参数校验:对pageRequest进行非空校验,防止空指针异常,增强方法的健壮性。

3.3 Mapper层:数据库操作实现

3.3.1 代码实现

@Mapper
public interface UserInfoMapper {

    /**
     * 查询普通用户总数
     * @return 总记录数
     */
    @Select("SELECT COUNT(*) FROM normal_user_info")
    Integer queryNormalUserInfoCount();

    /**
     * 查询普通用户列表(分页)
     * @param pageRequest 分页参数(包含offset和pageSize)
     * @return 用户列表
     */
    @Select("SELECT id, user_name, password, delete_flag, create_time, update_time " +
            "FROM normal_user_info " +
            "ORDER BY id ASC " +
            "LIMIT #{offset}, #{pageSize}")
    List<UserInfo> queryNormalUserInfoList(PageRequest pageRequest);
}

3.3.2 核心逻辑分析

  1. SQL写法
    • 使用LIMIT #{offset}, #{pageSize}实现分页查询,其中offset为数据偏移量,pageSize为每页条数。
    • 通过ORDER BY id ASC确保数据按主键排序,避免分页结果不稳定(若不排序,数据库可能返回无序结果,导致分页错乱)。
  2. 参数映射:MyBatis会自动将PageRequest对象中的offsetpageSize字段映射到SQL参数,无需手动设置@Param注解(前提是字段名与参数名一致)。
  3. 字段映射:数据库表字段user_namedelete_flag等通过MyBatis的自动映射机制填充到UserInfo对象的对应属性(需注意驼峰命名规则,如create_time映射为createTime)。

3.4 分页的逻辑和封装的类

(1)假设每页10条数据,逻辑如下:
第一页,第1-10的数据
第二页,第11-20的数据
第三页,第21-30的数据…

(2)数据库中数据的索引是从0开始的,更新逻辑(假设每页10条数据):
第一页,索引为0-9的数据
第二页,索引为10-19的数据
第三页,索引为20-29的数据…

(3)要想实现这个功能, 从数据库中进行分页查询,我们要使用 LIMIT 关键字,格式为:limit 开始索引每页显示的条数(开始索引从0开始)

limit a,b关键字:a是开始的索引,b是从a开始显示的条数

--第一页
select * from book_info limit 0,10;

--第二页
select * from book_info limit 10,10;

--第三页
select * from book_info limit 20,10;

观察以上SQL语句,发现: 开始索引⼀直在改变, 每每页显示条数是固定的
开始索引的计算公式: 开始索引 = (当前页码 - 1) * 每页显示条数

(3)从上述的分析得出,要实现一个分页的查询需要三个属性:当前页码、查询的条数、起始的索引

(4)创建一个类PageRequest,封装这三个属性。这三个属性的逻辑关系还需要另外的处理(开始索引 = (当前页码 - 1) * 每页显示条数),为了方便调用,直接在构造方法中实现三者的逻辑关系

@Data
public class PageRequest {
    // 请求的页数
    private int currentPage=1;

    //请求的该页条数
    private int pageSize=10;

    //开始的索引
    private int offset;

    public PageRequest(int currentPage){
        this.currentPage = currentPage;
        this.offset=(this.currentPage-1)* this.pageSize;
    }

    public PageRequest(){
        this.offset=(this.currentPage-1)*this.pageSize;
    }

    public PageRequest(int currentPage, int pageSize){
        this.currentPage=currentPage;
        this.pageSize=pageSize;
        this.offset=(this.currentPage-1)* this.pageSize;

    }

    //获取开始的索引
    public int getOffset(){
        return (this.currentPage-1)* this.pageSize;
    }
}

构造函数说明:
(1)当前端没有传来页码和该页的条数时,默认使用该类的缺省值
(2)当前端只传来页码而没有该页的条数时,默认使用每页10条数据。
(3)当前端传来页码和该页的条数时,进行逻辑处理求offset。

4. 前端代码实现

4.1 静态页面:基于Bootstrap的UI设计

4.1.1 HTML结构

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用户管理列表</title>
    <!-- 引入Bootstrap样式 -->
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/list.css">
    <!-- 引入jQuery和分页插件 -->
    <script src="js/jquery.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
    <script src="js/jq-paginator.js"></script>
</head>
<body>
<div class="bookContainer">
    <h2>用户管理列表</h2>
    <div class="navbar-justify-between">
        <div>
            <!-- 批量删除和添加用户按钮 -->
            <button class="btn btn-outline-info" onclick="batchDeleteUser()">批量删除</button>
            <button class="btn btn-outline-info" onclick="location.href='normal_user_add.html'">添加用户</button>
        </div>
    </div>

    <table class="table table-hover">
        <thead>
        <tr>
            <th><input type="checkbox" id="selectAll"></th>
            <th>用户ID</th>
            <th>用户名</th>
            <th>密码</th>
            <th>状态</th>
            <th>创建时间</th>
            <th>更新时间</th>
            <th>操作</th>
        </tr>
        </thead>
        <tbody></tbody>
    </table>

    <!-- 分页容器 -->
    <div class="demo">
        <ul id="pageContainer" class="pagination justify-content-center"></ul>
    </div>
</div>
</body>
</html>

4.1.2 设计要点

  1. 响应式布局:通过Bootstrap的栅格系统和响应式类(如containertable-responsive)确保页面在不同设备上正常显示。
  2. 交互组件
    • 批量删除按钮:通过onclick事件绑定batchDeleteUser函数,实现多选删除功能。
    • 添加用户按钮:直接跳转至添加页面normal_user_add.html
  3. 表格设计:使用Bootstrap的.table-hover类实现鼠标悬停高亮效果,提升可读性;操作列包含修改和删除按钮,通过JavaScript绑定事件。

4.2 JavaScript逻辑:前后端交互与页面渲染

4.2.1 核心函数:获取用户列表并渲染

$(function() {
    getUserList(); // 页面加载完成后立即请求数据
});

function getUserList() {
    $.ajax({
        url: "/user/getUserInfoList" + location.search, // 拼接URL参数(如?currentPage=1)
        type: "GET",
        dataType: "json",
        success: function(result) {
            if (result.status === "FAIL" || result.status === "UNLOGIN") {
                // 处理失败或未登录状态,跳转至登录页
                location.href = "login_test.html";
                return;
            }

            const userList = result.data.bookInfoList; // 注意:此处字段名应为userInfoList(可能为后端代码笔误)
            const total = result.data.total;
            const pageRequest = result.data.pageRequest;

            // 渲染用户列表
            renderUserList(userList);
            // 初始化分页组件
            initPagination(total, pageRequest);
        },
        error: function(xhr, status, error) {
            console.error("请求失败:", status, error);
            alert("网络请求失败,请检查网络连接");
        }
    });
}

function renderUserList(users) {
    let html = "";
    users.forEach(user => {
        html += `
            <tr>
                <td><input type="checkbox" name="selectUser" value="${user.id}" class="user-select"></td>
                <td>${user.id}</td>
                <td>${user.userName}</td>
                <td>${user.password}</td>
                <td>${user.deleteStatus}</td>
                <td>${formatDate(user.createTime)}</td>
                <td>${formatDate(user.updateTime)}</td>
                <td>
                    <button class="btn btn-primary btn-sm" 
                            onclick="location.href='normal_user_update.html?userId=${user.id}'">
                        修改密码
                    </button>
                    <button class="btn btn-danger btn-sm ml-2" 
                            onclick="deleteUser(${user.id})">
                        删除
                    </button>
                </td>
            </tr>
        `;
    });
    $("tbody").html(html);
}

// 日期格式化函数
function formatDate(dateStr) {
    if (!dateStr) return "-";
    return new Date(dateStr).toLocaleString();
}

4.2.2 分页功能实现

function initPagination(total, pageRequest) {
    $("#pageContainer").jqPaginator({
        totalCounts: total, // 总记录数
        pageSize: pageRequest.pageSize, // 每页条数
        visiblePages: 5, // 显示5个页码
        currentPage: pageRequest.currentPage, // 当前页码
        // 自定义分页按钮样式(基于Bootstrap)
        first: '<li class="page-item"><a class="page-link">首页</a></li>',
        prev: '<li class="page-item"><a class="page-link">上一页</a></li>',
        next: '<li class="page-item"><a class="page-link">下一页</a></li>',
        last: '<li class="page-item"><a class="page-link">末页</a></li>',
        page: '<li class="page-item"><a class="page-link">{{page}}</a></li>',
        // 页码变化时的回调函数
        onPageChange: function(page, type) {
            if (type === "change") {
                // 跳转至指定页码,通过URL传递参数触发页面刷新
                location.href = `normal_user_list.html?currentPage=${page}`;
            }
        }
    });
}

4.2.3 数据交互细节

  1. URL参数处理:通过location.search获取当前URL的查询参数(如?currentPage=2),确保分页切换时参数正确传递给后端。
  2. 跨域问题:若前后端部署在不同域名下,需在后端配置@CrossOrigin注解或全局CORS配置,否则会触发浏览器跨域限制。
  3. 性能优化
    • 分页组件使用jqPaginator实现前端分页渲染,避免重复请求全量数据。
    • 日期格式化通过toLocaleString()方法处理,确保时间格式统一且易读。

5. 系统优化与扩展建议

5.1 现有代码改进点

  1. 参数校验增强

    • PageRequest类中添加JSR-303校验注解(如@Min(1)),利用Spring MVC的自动校验功能,简化Controller层的手动校验逻辑。
    public class PageRequest {
        @Min(value = 1, message = "当前页码不能小于1")
        private int currentPage = 1;
        
        @Min(value = 1, message = "每页条数不能小于1")
        private int pageSize = 10;
        // ...其他字段
    }
    
    • 在Controller方法中添加@Valid注解,自动捕获校验异常并返回错误信息。
  2. Service层事务优化

    • 查询操作无需开启事务,移除@Transactional注解,提升数据库查询性能。
  3. 前端安全增强

    • 对用户输入进行XSS过滤,避免恶意代码注入(如使用DOMPurify库)。
    • 在批量删除功能中,对URL传递的idList参数长度进行限制,防止SQL注入(后端可通过正则校验参数格式)。

5.2 功能扩展方向

  1. 搜索与过滤

    • 添加用户名搜索框,后端接口支持userName模糊查询,修改SQL为:
      SELECT ... FROM normal_user_info 
      WHERE user_name LIKE CONCAT('%', #{keyword}, '%') 
      ORDER BY id ASC LIMIT #{offset}, #{pageSize}
      
    • 前端增加搜索按钮,通过URL传递keyword参数,与分页参数结合使用。
  2. 权限管理

    • 添加角色表(role)和用户角色关联表(user_role),实现基于角色的访问控制(RBAC)。
    • 在Controller层添加权限校验注解(如@PreAuthorize("hasRole('ADMIN')")),限制普通用户访问敏感操作。
  3. 前端框架升级

    • 将jQuery替换为Vue.js或React.js,采用单页面应用(SPA)架构,提升用户体验。
    • 使用Axios替代原生AJAX,简化异步请求处理,并集成请求拦截器处理Token验证。

总结🌟

本文通过一个完整的用户管理系统案例,详细讲解了Spring Boot后端与jQuery前端的开发流程,涵盖接口设计、三层架构实现、分页功能开发及前后端联调等核心环节。通过该实践,我们可以总结出以下关键经验:

  • 后端设计:三层架构确保代码职责分离,参数校验和异常处理是接口健壮性的基础,MyBatis的动态SQL和分页功能简化了数据库操作。
  • 前端开发:利用Bootstrap快速构建响应式界面,通过jQuery实现异步数据交互,分页插件的合理使用提升了用户体验。
  • 交互设计:统一的响应格式(Result对象)和清晰的参数传递规则(分页参数通过URL传递)是前后端高效协作的关键。

在实际项目中,还需根据需求进一步优化代码,如添加单元测试、集成日志监控、实现接口文档(Swagger)等。全栈开发不仅需要掌握各层技术,更要理解前后端之间的数据流动和业务逻辑衔接,通过不断实践积累,才能构建出高质量的企业级应用系统💪。

结语🌈

技术的发展日新月异,但分层架构的设计思想和前后端交互的核心逻辑始终是软件开发的基石。希望本文能为读者提供一个可落地的全栈开发参考案例,在实际项目中灵活运用所学知识,不断优化代码结构,提升系统的可维护性和扩展性。未来,可以进一步探索微服务架构、分布式缓存、前端组件化等技术,持续提升技术能力,迎接更复杂的开发挑战🚀。


网站公告

今日签到

点亮在社区的每一天
去签到