首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >JavaWeb——基于 Spring Boot + Vue 的图书管理系统的设计与实现

JavaWeb——基于 Spring Boot + Vue 的图书管理系统的设计与实现

原创
作者头像
用户12759075
修改2026-09-12 12:46:27
修改2026-09-12 12:46:27
480
举报

课程设计总结 | 前后端分离 | Spring Boot 2.5.6 + Vue 2 + Element UI + MySQL


1 概述

1.1 项目背景

高校图书馆的日常业务围绕"图书"和"借阅"展开:图书信息需要按类别归档、读者需要能自助查询、借还书需要留下可追溯的记录。传统的人工登记方式在图书数量增长后会出现两个问题:一是查询效率低,管理员很难快速定位某位读者当前借了哪些书;二是状态不同步,书架上的书被借走后,台账上往往不能及时更新,导致"显示在馆、实际已借出"的情况。

本系统正是为了解决这两个问题而设计的。系统面向管理员读者两类角色:管理员负责图书、图书类型、用户和借阅记录的维护;读者可以查询图书、自助借还、查看自己的借阅记录。系统采用前后端分离架构,后端以 RESTful 接口对外提供服务,前端通过 Axios 调用接口渲染页面,二者可以独立开发、独立部署。

1.2 开发工具与环境

本项目采用 Windows 10 操作系统和 IntelliJ IDEA 2021.2.1(Ultimate Edition) 进行系统开发,具体环境如下表所示。

表 1-1 开发环境

名称

版本 / 说明

操作系统

Windows 10 home

开发工具

IntelliJ IDEA 2021.2.1(Ultimate Edition)

Java 版本

JDK 1.8.0

构建工具

Maven 3.3.9

Web 服务器

Tomcat 8.5(Spring Boot 内嵌)

数据库

MySQL 5.7

缓存

Redis(存放登录 Token)

接口测试

Postman / curl

1.3 技术栈

表 1-2 技术栈选型

层次

技术

前端框架

Vue 2 + Vue Router + Vuex

前端 UI 库

Element UI

前端网络库

Axios

后端框架

Spring Boot 2.5.6

持久层

MyBatis(XML 映射 + 动态 SQL)

数据库

MySQL 5.7(InnoDB)

缓存

Redis(Token 存储,有效期 1 小时)

构建

Maven

1.4 项目成员

表 1-3 角色分工

姓名

职位

负责范围

liwo

项目经理

进度把控、任务拆分、需求确认

liwo

系统分析师

需求分析、功能模块划分、接口约定

liwo

设计师

页面原型、交互设计、UI 风格统一

liwo

前端开发

页面开发、Axios 接口调用、路由与状态管理

liwo

后端开发

接口开发、数据库设计、业务逻辑实现

liwo

测试工程师

编写测试用例、接口测试、缺陷跟踪

liwo

运维工程师

环境搭建、项目部署、备份与恢复


2 系统分析

2.1 需求分析

通过对图书馆日常业务的梳理,系统需要满足以下需求:

  1. 身份与权限:系统存在管理员和读者两种角色,登录时需要选择身份,不同角色进入不同的功能界面,读者不能访问管理员的功能入口。
  2. 图书信息管理:图书包含编号、名称、作者、价格、类型、描述、封面、借阅状态等属性,需要支持增加、修改、删除、按条件搜索。
  3. 图书类型管理:图书按类型归档,类型可以被新增与修改,图书与类型之间是多对一关系。
  4. 借阅管理:读者借书后,图书的借阅状态需要立即变为"已借出",同时生成一条借阅记录;还书后状态回到"未借",记录补上归还时间。
  5. 用户管理:管理员可以维护读者账号,可以重置密码,可以删除账号。
  6. 数据统计:需要一个信息看板,展示用户数量、图书数量、借阅数量等关键指标。

2.2 可行性分析

  • 技术可行性:Spring Boot 与 Vue 均为成熟框架,社区资料充足;MyBatis 的动态 SQL 足以应对本系统的条件查询需求;系统数据量小(图书量级为百条),单库单表即可支撑。
  • 经济可行性:全部采用开源技术栈,无商业授权成本;开发阶段使用本机 MySQL 与 Redis,无需额外服务器投入。
  • 操作可行性:界面采用 Element UI 标准组件,操作方式与常见的后台管理系统一致,管理员无需专门培训即可上手。

2.3 功能分析

系统功能可归纳为四大模块:

  • 账号管理:包含普通用户(读者)账号管理与管理员账号管理。读者可以自助注册;管理员可以登录后查看全部账号、修改用户信息、重置密码、删除账号。
  • 图书管理:包含图书信息管理与图书类型管理。支持添加、删除、修改图书;查询分为单独查询(按书名、按作者、按类型)和批量查询(显示全部)两类;图书封面的上传由独立的上传接口完成。
  • 借阅管理:包含读者借书、管理员代借书、还书、续借(修改借阅时间)、删除借阅记录。借书时系统会校验图书是否已被借出,重复借阅会被拒绝。
  • 信息看板:展示用户数量、图书数量、借阅数量、访问次数等统计数据,便于管理员快速了解系统使用情况。

3 系统设计

3.1 功能概述

系统整体分为管理员模块读者模块两大部分,两者共用同一套后端接口,通过登录时选择的身份(isAdmin 字段)区分权限。

  • 管理员模块:注册、登录、图书管理(增删改查)、图书类型管理、用户管理、借阅管理、修改个人信息、修改密码。
  • 读者模块:注册、登录、查询图书信息、借阅图书、归还图书、查看个人借阅记录、修改个人信息、修改密码。

系统功能结构如图 3-1 所示。

图 3-1 系统结构图

3.2 界面设计

本系统为前后端分离架构,前端为单页应用(SPA),使用 Vue Router 的 hash 模式组织页面跳转,主要路由如下表。

表 3-1 前端路由表

路由

对应页面

可访问角色

/login

登录页

全部

/register

注册页

全部

/dashboard

信息看板

全部

/bookmanage/bookinfo

图书信息管理

全部(读者只读 + 借阅)

/bookmanage/booktype

图书类型管理

管理员

/bookmanage/borrow

借阅信息管理

全部(读者仅本人记录)

/other/user

用户管理

管理员

/other/password

修改密码

全部

3.2.1 登录页面

登录页面如图 3-2 所示,包含用户名密码身份选择三个输入项和登录、注册两个按钮。身份选择为下拉框,可选"管理员"或"读者",系统会将该值一并提交到登录接口,与数据库中的 isAdmin 字段比对,从而保证"同一个用户名不会被错误地以另一种身份登录"。

图 3-2 登录页面

3.2.2 注册页面

注册页面如图 3-3 所示,包含用户名、密码、确认密码三个输入框。点击"确认"后调用 /user/register 接口;若用户名已存在,接口返回 0,前端提示注册失败。本系统面向校内使用,注册默认创建读者账号,管理员账号由已有管理员在用户管理中新增。

图 3-3 注册页面

3.2.3 信息看板页面

信息看板页面如图 3-4 所示,登录成功后默认跳转至此。页面顶部展示系统欢迎信息,下方是项目开发环境说明。看板数据来自 /user/getCount/bookInfo/getCount/bookType/getCount/borrow/getCount 四个统计接口。

图 3-4 信息看板页面

3.2.4 图书信息管理页面

图书信息管理页面如图 3-5 所示,是管理员使用频率最高的页面。页面顶部是查询区,支持按书名作者类型三个条件组合查询,另有"显示全部"按钮用于清空条件;下方是图书列表,列出序号、图书名称、图书作者、图书价格、图书类型名、图书描述、图书借阅状态(未借出为绿色标签、已借出为红色标签)等信息,每行提供"编辑""删除""借阅登记"三个操作按钮。

列表底部是分页控件,支持每页 10 / 20 / 50 条的切换与页码跳转,当前数据共 60 条图书记录,分为 6 页展示。

图 3-5 图书信息管理页面

3.2.5 图书类型管理页面

图书类型管理页面如图 3-6 所示,列出类型编号、类型名称、类型描述,并提供新增、编辑、删除操作。当前系统内置 10 个类型:计算机科学、历史、文学、科幻、小说、外语、数学、经济管理、心理学、艺术设计。由于图书表中保存的是类型 ID,删除类型前需要确认该类型下没有图书,避免出现"孤儿数据"。

图 3-6 图书类型管理页面

3.2.6 借阅信息管理页面

借阅信息管理页面如图 3-7 所示,这是系统的核心业务页面。列表展示借阅记录编号、用户名、图书名、借书时间、还书时间,并提供"删除"和"归还图书"操作。

页面支持按用户名图书名模糊查询。管理员可以看到全部读者的借阅记录,而读者登录后进入同一路由时,前端会自动带上当前用户的 userid 作为查询条件,从而只能看到自己的借阅记录

图 3-7 借阅信息管理页面

3.2.7 用户管理页面

用户管理页面如图 3-8 所示,用于维护读者与管理员账号。列表展示用户编号、用户名、密码、是否为管理员(0 表示读者、1 表示管理员),并提供编辑、删除、批量删除操作。

需要注意的是,超级管理员账号(userId = 1)在服务端做了保护——删除请求会被直接拒绝并返回 0,防止误删导致系统无法登录。

图 3-8 用户管理页面

3.2.8 修改密码页面

修改密码页面如图 3-9 所示,包含原密码、新密码、确认新密码三项。提交时会先校验原密码是否正确:若原密码错误,接口返回 0,前端提示"原密码错误";校验通过后写入新密码并返回 1。

图 3-9 修改密码页面

3.2.9 读者端图书查询页面

读者登录后进入的图书页面如图 3-10 所示,与管理员的图书管理页面使用同一套列表组件,但隐藏了新增、编辑、删除等管理员操作,仅保留查询与借阅入口。读者可以按书名或作者检索图书,点击"借阅图书"即可完成借书。

图 3-10 读者端图书查询页面

3.2.10 读者端借阅记录页面

读者端的借阅信息管理页面如图 3-11 所示,列出当前读者本人的借阅历史,包含图书名、借书时间、还书时间,并提供"归还图书"操作。还书时间列为空表示该书尚未归还,这是判断借阅状态最直接的方式。

图 3-11 读者端借阅记录页面

3.3 接口设计

系统后端采用 RESTful 风格,统一以 /BookManager 作为上下文根,端口为 8092。所有列表类接口返回统一的 JSON 结构,所有写操作返回受影响的行数。

表 3-2 主要接口清单

模块

请求方式

接口路径

说明

用户

POST

/user/login

登录,返回 Token

用户

GET

/user/info

按 Token 获取当前用户

用户

POST

/user/logout

退出登录,清除 Token

用户

POST

/user/register

注册读者账号

用户

POST

/user/alterPassword

修改密码

用户

GET

/user/queryUsersByPage

分页查询用户

用户

POST / DELETE / PUT

/user/addUser、/user/deleteUser、/user/deleteUsers、/user/updateUser

用户增删改

图书

GET

/bookInfo/queryBookInfosByPage

分页 + 条件查询图书

图书

GET

/bookInfo/getCount

图书总数

图书

POST / DELETE / PUT

/bookInfo/addBookInfo、/bookInfo/deleteBookInfo、/bookInfo/deleteBookInfos、/bookInfo/updateBookInfo

图书增删改

类型

GET

/bookType/queryBookTypes

查询全部类型

类型

GET

/bookType/queryBookTypesByPage

分页查询类型

借阅

POST

/borrow/borrowBook

借书(含状态校验与事务)

借阅

POST

/borrow/returnBook

还书(含状态校验与事务)

借阅

GET

/borrow/queryBorrowsByPage

分页 + 条件查询借阅记录

借阅

GET

/borrow/getCount

借阅总数

上传

POST

/update/updateImg

上传图书封面

3.4 数据库设计

系统共设计 4 张数据表user(用户表)、book_info(图书信息表)、book_type(图书类型表)、borrow(借阅记录表)。表之间的关系如图 3-12 所示。

图 3-12 数据库表关系图

borrow 表是整个数据模型的关键:它是 userbook_info 之间的关联表,一条借阅记录同时关联"一个读者"和"一本图书",returnTime 字段为空表示尚未归还。这样设计既保留了完整的历史记录,又避免在图书表上重复存储借阅人信息。

表 3-3 user(用户表)

字段名

类型

约束

说明

userId

int(11)

主键、自增

用户编号

userName

varchar(20)

非空

用户名

userPassword

varchar(20)

非空

密码

isAdmin

tinyint(4)

非空

1 为管理员,0 为读者

表 3-4 book_type(图书类型表)

字段名

类型

约束

说明

bookTypeId

int(11)

主键、自增

类型编号

bookTypeName

varchar(20)

非空

类型名称

bookTypeDesc

varchar(255)

非空

类型描述

表 3-5 book_info(图书信息表)

字段名

类型

约束

说明

bookId

int(11)

主键、自增

图书编号

bookName

varchar(50)

非空

图书名称

bookAuthor

varchar(50)

非空

图书作者

bookPrice

decimal(10,2)

非空

图书价格

bookTypeId

int(11)

非空

所属类型(对应 book_type)

bookDesc

varchar(255)

非空

书籍描述

isBorrowed

tinyint(4)

非空

1 表示借出,0 表示已还

bookImg

varchar(255)

可空

书籍图片地址

表 3-6 borrow(借阅记录表)

字段名

类型

约束

说明

borrowId

int(11)

主键、自增

借阅记录编号

userId

int(11)

非空

借阅人(对应 user)

bookId

int(11)

非空

被借图书(对应 book_info)

borrowTime

datetime

非空

借书时间

returnTime

datetime

可空

还书时间,为空表示未归还


4 实现与测试

4.1 关键技术实现

4.1.1 统一返回结构 MyResult

前后端分离项目中,接口返回结构的统一是最先要解决的问题。项目通过 MyResult 工具类提供三种标准返回:普通结果(含 status、message、timestamp)、带数据的结果(额外含 data)、列表结果(含 code、count、data,供分页表格直接使用)。

代码语言:javascript
复制
package com.liwo.bms.utils;

import java.util.HashMap;

public class MyResult {

    public static HashMap<String, Object> getResultMap(Integer status, String message) {
        return new HashMap<String, Object>() {
            {
                put("status", status);
                put("message", message);
                put("timestamp", System.currentTimeMillis());
            }
        };
    }

    public static HashMap<String, Object> getResultMap(Integer status, String message, Object data) {
        return new HashMap<String, Object>() {
            {
                put("status", status);
                put("message", message);
                put("data", data);
                put("timestamp", System.currentTimeMillis());
            }
        };
    }

    public static HashMap<String, Object> getListResultMap(Integer status, String message, Integer count, Object data) {
        return new HashMap<String, Object>() {
            {
                put("code", status);
                put("message", message);
                put("count", count);
                put("data", data);
            }
        };
    }
}

其中列表结果采用 code 而不是 status,是为了对齐 Element UI 表格分页组件的默认字段约定——组件会直接读取 data 渲染表格行、读取 count 计算总页数,无需在前端做额外转换。

4.1.2 分页与条件查询:BookInfoController

图书查询是本系统最典型的"分页 + 多条件"场景。Controller 层只做参数准备与结果组装,具体的分页参数换算由 MyUtils.parsePageParams 完成——它把前端的 page(第几页)和 limit(每页条数)换算成 SQL 的 beginsize

代码语言:javascript
复制
package com.liwo.bms.web;

import com.liwo.bms.model.BookInfo;
import com.liwo.bms.service.BookInfoService;
import com.liwo.bms.utils.MyResult;
import com.liwo.bms.utils.MyUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping(value = "/bookInfo")
public class BookInfoController {

    @Autowired
    BookInfoService bookInfoService;

    // 获取图书数量
    @GetMapping(value = "/getCount")
    public Integer getCount(){
        return bookInfoService.getCount();
    }

    // 查询所有图书信息
    @GetMapping(value = "/queryBookInfos")
    public List<BookInfo> queryBookInfos(){
        return bookInfoService.queryBookInfos();
    }

    // 分页搜索查询图书信息 params: {page, limit, bookname, bookauthor, booktypeid}
    @GetMapping(value = "/queryBookInfosByPage")
    public Map<String, Object> queryBookInfosByPage(@RequestParam Map<String, Object> params){
        MyUtils.parsePageParams(params);
        int count = bookInfoService.getSearchCount(params);  // 获得总数
        List<BookInfo> bookInfos = bookInfoService.searchBookInfosByPage(params);  // 分页查询
        return MyResult.getListResultMap(0, "success", count, bookInfos);
    }

    // 添加图书信息
    @PostMapping(value = "/addBookInfo")
    public Integer addBookInfo(@RequestBody BookInfo bookInfo){
        return bookInfoService.addBookInfo(bookInfo);
    }

    // 删除图书信息
    @DeleteMapping(value = "/deleteBookInfo")
    public Integer deleteBookInfo(@RequestBody BookInfo bookInfo){
        return bookInfoService.deleteBookInfo(bookInfo);
    }

    // 删除一些图书信息
    @DeleteMapping(value = "/deleteBookInfos")
    public Integer deleteBookInfos(@RequestBody List<BookInfo> bookInfos){
        return bookInfoService.deleteBookInfos(bookInfos);
    }

    // 更新图书信息
    @PutMapping(value = "/updateBookInfo")
    public Integer updateBookInfo(@RequestBody BookInfo bookInfo){
        return bookInfoService.updateBookInfo(bookInfo);
    }
}

对应的 Mapper 使用 MyBatis 的动态 SQL 实现"条件为空则不加该条件"的语义,并在图书列表查询中用子查询带出类型名称:

代码语言:javascript
复制
<select id="selectBySearch" resultMap="BaseResultMap">
  select bookId, bookName, bookAuthor, bookPrice, bookTypeId, bookDesc, isBorrowed, bookImg,
         (select bookTypeName from book_type where book_type.bookTypeId = book_info.bookTypeId) as bookTypeName
  from book_info
  <where>
    <if test="bookname != null and bookname != '' ">
      and bookName like concat('%',#{bookname},'%')
    </if>
    <if test="bookauthor != null and bookauthor != '' ">
      and bookAuthor like concat('%',#{bookauthor},'%')
    </if>
    <if test="booktypeid != null and booktypeid != '' ">
      and bookTypeId = #{booktypeid}
    </if>
  </where>
  limit #{begin}, #{size}
</select>

这里的 <where> 标签会自动处理第一个 and,避免出现 where and bookName like ... 的语法错误;concat('%', #{bookname}, '%') 则实现了书名与作者名的模糊查询。类型名称不冗余存储在图书表中,而是通过子查询实时获取,保证了类型改名后图书列表能同步更新。

4.1.3 借书业务与事务控制:BorrowController

借书是本系统中最需要保证数据一致性的操作。它同时要改两张表:把 book_infoisBorrowed 置为 1,并向 borrow 表插入一条记录。如果只成功了一半,就会出现"书显示已借出但查不到借阅记录"的脏数据。项目通过 Spring 的 @Transactional 注解配合手动回滚解决这一问题。

代码语言:javascript
复制
// 借书
@RequestMapping(value = {"/borrowBook", "/reader/borrowBook"})
@Transactional
public Integer borrowBook(Integer userid, Integer bookid){
    try{
        // 查询该书的情况
        BookInfo theBook = bookInfoService.queryBookInfoById(bookid);

        if(theBook == null) {  // 图书不存在
            throw new NullPointerException("图书" + bookid + "不存在");
        } else if(theBook.getIsborrowed() == 1) {  // 已经被借
            throw new NotEnoughException("图书" + bookid + "库存不足(已经被借走)");
        }

        // 更新图书表的isBorrowed
        BookInfo bookInfo = new BookInfo();
        bookInfo.setBookid(bookid);
        bookInfo.setIsborrowed((byte) 1);
        Integer res2 = bookInfoService.updateBookInfo(bookInfo);
        if(res2 == 0) throw new OperationFailureException("图书" + bookid + "更新被借信息失败");

        // 添加一条记录到borrow表
        Borrow borrow = new Borrow();
        borrow.setUserid(userid);
        borrow.setBookid(bookid);
        borrow.setBorrowtime(new Date(System.currentTimeMillis()));
        Integer res1 = borrowService.addBorrow2(borrow);
        if(res1 == 0) throw new OperationFailureException("图书" + bookid + "添加借阅记录失败");

    } catch (Exception e) {
        System.out.println("发生异常,进行手动回滚");
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        e.printStackTrace();
        return 0;
    }
    return 1;
}

这段代码有三个设计要点:

  1. 先校验后修改。在动数据之前先判断图书是否存在、是否已被借出,把可预见的业务异常挡在最前面,减少事务内的失败概率。
  2. 每个写操作都校验影响行数updateBookInfoaddBorrow2 都会返回受影响的行数,一旦为 0 说明写库没有生效,立即抛出异常中断流程。
  3. 手动回滚。捕获异常后调用 setRollbackOnly() 显式标记事务回滚,最后统一返回 0 表示失败,前端据此提示用户。

同一 Controller 中的 returnBook 采用相同的模式,只是把方向反过来:校验借阅记录存在且 returnTime 为空(防止重复还书),然后把图书状态置回 0、为借阅记录写入归还时间。

4.1.4 登录与 Token 机制:UserController

登录接口是整个系统的入口,其实现思路是"登录成功即发一个 Token,后续请求凭 Token 换用户信息",Token 存放在 Redis 中并设置 1 小时过期。

代码语言:javascript
复制
// 登录
@RequestMapping(value = "/login")
public Map<String, Object> login(@RequestBody User user) {
    // 登录
    User userObj = userService.login(user);
    if(userObj == null) {   // 账号或密码错误
        // 返回结果对象
        return MyResult.getResultMap(420, "账号或密码错误");
    } else {    // 账号密码正确
        // 创建token
        String token = TokenProcessor.getInstance().makeToken();
        // 保存到Redis
        userService.saveUser(token, userObj);
        // 返回结果对象
        return MyResult.getResultMap(200, "登录成功",
                new HashMap<String, String>(){{ put("token", token); }});
    }
}

其中 Token 由时间戳加随机数经 MD5 摘要再 Base64 编码生成,保证不可预测:

代码语言:javascript
复制
public String makeToken() {
    String token = (System.currentTimeMillis() + new Random().nextInt(999999999)) + "";
    try {
        MessageDigest md = MessageDigest.getInstance("md5");
        byte md5[] = md.digest(token.getBytes());
        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encode(md5);
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    return null;
}

登录时的身份校验是"三条件匹配":用户名、密码、isAdmin 必须同时命中数据库中的同一条记录。这样做的好处是读者无法用正确的账号密码从管理员入口登录,登录页的身份选择不再只是界面装饰,而是参与了一次真实的权限判定。

用户信息则通过 Token 反查:

代码语言:javascript
复制
// 查看用户信息
@RequestMapping(value = "/info")
public Map<String, Object> info(String token) {
    // 从redis中取用户
    User user = userService.getUser(token);
    if(user == null) {  // 获取失败
        return MyResult.getResultMap(420, "获取用户信息失败");
    } else {    // 获取成功
        return MyResult.getResultMap(200, "获取用户信息成功", user);
    }
}
4.1.5 图书封面上传:UpdateController

图书封面需要上传到服务器。上传接口接收 multipart 请求,把文件写入 classes/static/pictures 目录,文件名加上时间戳防止重名,最后返回可访问的网络路径。

代码语言:javascript
复制
@RequestMapping("/updateImg")
@ResponseBody
public Map<String,Object> updateImg(HttpServletRequest req){
    String resPath = myUpdate(req, "pictures");

    Map<String,Object> res = new HashMap<>();
    res.put("code",0);
    res.put("data", resPath);

    return res;
}

前端拿到返回的路径后,把它写入图书记录的 bookImg 字段,列表页即可直接以 <img src> 渲染封面。

4.2 测试

4.2.1 测试环境
  • 后端服务:Spring Boot 内嵌 Tomcat,监听 8092 端口,上下文 /BookManager
  • 数据库:MySQL 5.7,库名 book_manager
  • 测试数据:用户 21 条、图书 60 条、图书类型 10 条、借阅记录 99 条(其中 23 条尚未归还)
  • 测试方式:接口层使用 curl / Postman 直接调用,界面层在 Chrome 浏览器中按角色走完整流程
4.2.2 接口测试

表 4-1 接口测试用例与结果

编号

测试用例

输入

预期结果

实际结果

结论

1

管理员正常登录

admin / admin / 管理员

status=200,返回 token

status=200,返回 token

通过

2

密码错误登录

admin / wrong / 管理员

status=420,提示账号或密码错误

status=420,账号或密码错误

通过

3

借书

读者 14 借图书 3

返回 1

返回 1

通过

4

借书后状态联动

查询图书 3

isBorrowed 变为 1

isBorrowed=1

通过

5

重复借阅同一本书

读者 14 再借图书 3

返回 0(拒绝)

返回 0

通过

6

还书

归还记录 100

返回 1

返回 1

通过

7

重复还书

再次归还记录 100

返回 0(拒绝)

返回 0

通过

8

书名模糊查询

bookname=Java

命中 1 条

count=1,返回 1 条

通过

9

分页越界

page=99

总数为 60,当前页为空

count=60,data 为 0 条

通过

10

统计接口

用户 21 / 图书 60 / 类型 10 / 借阅 100(含用例 3 新增的 1 条)

与预期一致

通过

4.2.3 功能测试结论
  1. 角色权限有效:以读者身份登录后,界面不出现"用户管理""图书类型管理"等管理员入口,且查询借阅记录时被限定为本人数据。
  2. 借还流程闭环:借书后图书状态立即变为"已借出"并生成记录;还书后状态复原、归还时间写入。重复借阅与重复还书均被正确拦截。
  3. 查询与分页正确:单条件、组合条件与模糊查询均返回预期结果;分页越界时返回空列表而不报错,前端分页组件正常显示总页数。
  4. 异常处理完整:密码错误、图书不存在、图书已被借出等异常场景均返回友好提示,未出现 500 错误页面。
  5. 遗留问题:原始版本使用了第三方对象存储保存图书封面,该地址现已不可访问(返回 403),测试时使用本地占位封面替代,不影响借阅主流程。

5 部署与运行

5.1 数据库准备

  1. 安装 MySQL 5.7,创建数据库 book_manager(字符集建议 utf8)。
  2. 导入数据库脚本 book_manager_full.sql:脚本会依次建库、建表并写入业务数据,一次性完成 10 个图书类型、60 本图书、21 个用户、99 条借阅记录的初始化。其中 23 本图书处于"已借出"状态,与借阅记录中未归还的 23 条数据严格对应,可用于直接演示借还流程。
  3. 脚本使用无外键的物理表设计,导入前会执行 DROP TABLE IF EXISTS,可反复导入而不会产生重复数据(生产环境使用前请先备份)。

5.2 后端启动

  1. 修改 src/main/resources/application.properties
代码语言:javascript
复制
server.port=8092
server.servlet.context-path=/BookManager

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/book_manager?useAffectedRows=true
spring.datasource.username=你的数据库账号
spring.datasource.password=你的数据库密码

spring.redis.host=你的Redis地址
spring.redis.port=6379
spring.redis.password=你的Redis密码
  1. 启动 Redis 服务(登录 Token 存储依赖 Redis,未启动会导致登录接口报错)。
  2. 用 IDEA 打开项目,等待 Maven 依赖下载完成后运行 BookManagerApplication
  3. 浏览器访问 http://localhost:8092/BookManager/,使用初始账号登录: - 管理员:admin / admin - 读者:liwo / 123456

5.3 部署常见问题

表 5-1 常见问题与解决办法

现象

原因

解决办法

启动报数据库连接失败

账号密码占位符未替换

检查 application.properties 中的 ? 占位

登录接口 500

Redis 未启动

先启动 Redis,或确认 spring.redis.* 配置正确

页面样式错乱

静态资源路径带上下文前缀

前端打包时 publicPath 需配置为 /BookManager/

页面 404

路由为 hash 模式

访问地址需带 #,如 /#/bookmanage/bookinfo

中文乱码

数据库或连接字符集不一致

建库使用 utf8,JDBC URL 增加字符集参数


6 总结与不足

6.1 完成的工作

本系统实现了图书管理业务的完整闭环:用户可注册登录并按角色进入不同界面,管理员可以维护图书、图书类型、用户和借阅记录,读者可以自助查询图书、借还图书并查看个人借阅历史。系统采用前后端分离架构,后端提供 RESTful 接口,前端通过 Axios 调用,二者解耦,便于后续扩展。

6.2 主要收获

  1. 掌握了 Spring Boot + MyBatis 的分层开发模式:Controller 负责参数与返回、Service 负责业务、Mapper 负责持久化,职责边界清晰。
  2. 理解了事务在业务中的实际意义:借书这种"跨两张表的写操作"必须保证原子性,@Transactional 加手动回滚是可靠的做法。
  3. 学会了分页查询的完整实现链路:前端翻页参数 → 服务端换算 begin/size → SQL limit → 统一列表结构返回。
  4. 体会到接口约定先行的价值:先定好统一返回结构,前后端联调时几乎不需要额外沟通。

6.3 不足与改进方向

  1. 密码明文存储userPassword 直接以明文保存,存在安全隐患,后续应改为 BCrypt 加盐哈希。
  2. 权限控制偏弱:项目中的登录拦截器目前被注释掉了,实际靠前端隐藏入口控制,服务端应当重新启用拦截器或引入 Spring Security,做到接口级鉴权。
  3. 图书库存模型过简isBorrowed 只有 0/1 两态,同一本书无法多副本借阅,可扩展为库存数量字段。
  4. 缺少续借与逾期提醒:目前续借只支持手工修改借阅时间,可以增加自动续借次数限制与逾期罚金计算。
  5. 检索能力有限:仅支持书名与作者模糊匹配,可以引入全文检索或 Elasticsearch 提升体验。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 1 概述
    • 1.1 项目背景
    • 1.2 开发工具与环境
    • 1.3 技术栈
    • 1.4 项目成员
  • 2 系统分析
    • 2.1 需求分析
    • 2.2 可行性分析
    • 2.3 功能分析
  • 3 系统设计
    • 3.1 功能概述
    • 3.2 界面设计
      • 3.2.1 登录页面
      • 3.2.2 注册页面
      • 3.2.3 信息看板页面
      • 3.2.4 图书信息管理页面
      • 3.2.5 图书类型管理页面
      • 3.2.6 借阅信息管理页面
      • 3.2.7 用户管理页面
      • 3.2.8 修改密码页面
      • 3.2.9 读者端图书查询页面
      • 3.2.10 读者端借阅记录页面
    • 3.3 接口设计
    • 3.4 数据库设计
  • 4 实现与测试
    • 4.1 关键技术实现
      • 4.1.1 统一返回结构 MyResult
      • 4.1.2 分页与条件查询:BookInfoController
      • 4.1.3 借书业务与事务控制:BorrowController
      • 4.1.4 登录与 Token 机制:UserController
      • 4.1.5 图书封面上传:UpdateController
    • 4.2 测试
      • 4.2.1 测试环境
      • 4.2.2 接口测试
      • 4.2.3 功能测试结论
  • 5 部署与运行
    • 5.1 数据库准备
    • 5.2 后端启动
    • 5.3 部署常见问题
  • 6 总结与不足
    • 6.1 完成的工作
    • 6.2 主要收获
    • 6.3 不足与改进方向
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档