SpringBoot+Vue图书管理系统实战:从零搭建前后端分离架构

1. 为什么选择SpringBoot+Vue来搭建图书管理系统?

如果你是一个刚学完Java基础或者Vue基础,想找个项目练手,又或者你正在为毕业设计发愁,不知道从何下手,那我强烈建议你试试这个SpringBoot+Vue图书管理系统。我当年带新人,或者自己做一些内部工具,也经常从这个项目开始。为什么?因为它太经典了,几乎涵盖了企业级应用开发的所有核心环节:用户认证、数据增删改查、前后端数据交互、权限控制,麻雀虽小,五脏俱全。

先说说技术选型。后端用SpringBoot,这简直是Java开发者的福音。以前用Spring MVC,光配置文件就能写一大堆,现在SpringBoot主打一个“开箱即用”。你不需要再为复杂的XML配置头疼,它内置了Tomcat服务器,你写个@SpringBootApplication注解的主类,点一下运行,一个Web服务就起来了。这让我们能把精力完全集中在业务逻辑上,而不是环境搭建上。

前端用Vue,特别是Vue 3现在越来越普及,它的响应式系统和组件化开发体验非常流畅。对于图书管理系统这种以数据展示和操作为主的后台管理页面,Vue配合Element Plus(或者Element UI)组件库,拖拖拽拽就能搭出一个像模像样的界面,开发效率极高。而且Vue的学习曲线相对平缓,文档是中文的,社区活跃,遇到问题很容易找到解决方案。

最关键的是前后端分离架构。这意味着前端和后端可以并行开发。后端同学只需要定义好API接口(比如/api/books返回图书列表),返回JSON数据;前端同学也不管你后端是Java还是Python,他只需要调用这个接口,把数据渲染到页面上。这种模式在现代Web开发中已经是绝对的主流,能让你提前熟悉公司里的真实协作流程。

所以,无论你是想巩固技术栈,还是完成一个能写进简历的实战项目,这个组合都是性价比极高的选择。接下来,我就带你从零开始,一步步把它搭起来。

2. 动手之前:你的开发环境准备好了吗?

工欲善其事,必先利其器。在写第一行代码之前,咱们得先把“厨房”收拾好。别担心,我列了个清单,你跟着一步步安装配置,半小时内绝对搞定。

后端环境(Java阵营):

  1. JDK 1.8+:这是Java的基石。建议直接用JDK 11或17(LTS长期支持版),但为了兼容性,很多教程仍用1.8。去Oracle官网或AdoptOpenJDK下载安装,然后记得配置JAVA_HOME环境变量。
  2. Maven 3.6+:项目依赖管理工具。它就像一个智能管家,你告诉它项目需要什么库(比如MyBatis、MySQL驱动),它就去中央仓库帮你下载,不用自己手动找Jar包。安装后配置MAVEN_HOME
  3. IntelliJ IDEA:首推的Java IDE。社区版就够用,但终极版(Ultimate)对Spring和数据库支持更好。用它来写SpringBoot项目,代码提示和自动补全能让你爽到飞起。
  4. MySQL 5.7/8.0:我们的数据仓库。用Docker安装最方便,一条命令的事。或者直接下载安装包。记得安装完后,用命令行或Navicat这类图形化工具创建一个叫book_manager的数据库。

前端环境(JavaScript阵营):

  1. Node.js:前端项目的运行环境,自带npm包管理工具。务必安装LTS版本(如18.x或20.x)。安装后,在命令行输入 node -vnpm -v 能显示版本号就成功了。
  2. Vue CLI 或 Vite:项目脚手架。Vue CLI更成熟稳定,Vite更快更现代。新手我建议先用Vue CLI,命令更直观。全局安装它:npm install -g @vue/cli
  3. Visual Studio Code:前端开发神器。轻量、插件丰富,配合Vue官方插件,写起来行云流水。

一个关键步骤:配置Maven镜像 因为网络原因,从国外仓库下载依赖可能会很慢甚至失败。我们必须把Maven的源换成国内的。找到你的Maven安装目录下的 conf/settings.xml 文件,在 <mirrors> 标签内添加阿里云的镜像:

<mirror>
    <id>aliyunmaven</id>
    <mirrorOf>*</mirrorOf>
    <name>阿里云公共仓库</name>
    <url>https://maven.aliyun.com/repository/public</url>
</mirror>

同样,npm也可以换源,在终端执行:npm config set registry https://registry.npmmirror.com

环境配好,就像游戏角色装备齐全,可以正式“打怪升级”了。接下来,我们先从后端的“地基”——数据库设计开始。

3. 设计数据库:如何规划系统的“记忆中枢”?

数据库设计是项目的基石,设计得好,后面编码顺风顺水;设计得差,改表结构能改到你怀疑人生。根据图书管理系统的核心业务,我们至少需要六张表。我画个简单的ER图帮你理解:

用户 (User) --- 借阅 (Borrow) --- 图书 (Book)
                     |
                     |
归还 (Return)    分类 (Category)

核心表结构详解:

  1. 用户表 (user):存放读者信息。

    • id (主键):唯一标识。
    • username:登录名,唯一。
    • password:加密后的密码(切记不要明文存储!)。
    • name:真实姓名。
    • phone:联系方式。
    • status:账户状态(0-禁用,1-启用)。
  2. 管理员表 (admin):结构可以和用户表类似,但通常权限更高。有时为了简单,也会和用户表共用,加一个role字段区分角色(如:useradmin)。

  3. 图书表 (book):核心数据表。

    • id (主键)
    • isbn:国际标准书号,唯一标识一本书。
    • name:书名。
    • author:作者。
    • category_id:外键,关联分类表。
    • price:价格。
    • status:在馆状态(0-借出,1-在馆)。
    • cover:封面图片的存储路径。
  4. 图书分类表 (category):让图书管理更有条理。

    • id (主键)
    • name:分类名(如“计算机”、“文学”)。
    • description:分类描述。
  5. 借阅记录表 (borrow):记录每一次借书行为。

    • id (主键)
    • user_id:借阅人ID。
    • book_id:图书ID。
    • borrow_time:借出时间。
    • due_time:应还时间。
    • status:借阅状态(0-借出未还,1-已归还)。
  6. 归还记录表 (return):可以和借阅记录合并,但独立出来更清晰。它记录还书行为,并可与借阅记录关联。

建表SQL示例(以book表为例):

CREATE TABLE `book` (
  `id` int NOT NULL AUTO_INCREMENT,
  `isbn` varchar(20) DEFAULT NULL,
  `name` varchar(100) NOT NULL,
  `author` varchar(50) DEFAULT NULL,
  `category_id` int DEFAULT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  `status` tinyint DEFAULT '1' COMMENT '0-借出 1-在馆',
  `cover` varchar(255) DEFAULT NULL COMMENT '封面图片URL',
  PRIMARY KEY (`id`),
  UNIQUE KEY `isbn_unique` (`isbn`),
  KEY `fk_book_category` (`category_id`),
  CONSTRAINT `fk_book_category` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计心得:

  • 主外键关系:像book.category_id关联category.id,能保证数据完整性,避免出现“幽灵分类”。
  • 字段注释:一定要写COMMENT,三个月后你自己看代码,会感谢现在的自己。
  • 索引:对经常用于查询条件的字段(如book.name, book.isbn)建立索引,能大幅提升查询速度。
  • 密码加密:这是红线。绝对不能明文存密码。Spring Security的BCryptPasswordEncoder是现成的解决方案。

数据库设计完,我们就可以用SpringBoot来创建连接数据的桥梁了。

4. 构建SpringBoot后端:快速搭建RESTful API

现在进入后端实战环节。我们用SpringBoot快速搭建一套提供JSON数据的RESTful API。

第一步:创建SpringBoot项目 打开IDEA,选择 Spring Initializr(这是官方提供的项目初始化服务)。我习惯用这个,比手动创建省事。

  • Project:选Maven。
  • Language:Java。
  • Spring Boot:选一个稳定的版本,比如2.7.x或3.0.x(注意3.x需要JDK17+)。
  • Project Metadata:填好Group(如com.example)和Artifact(如book-manager)。
  • Dependencies:这是关键!我们勾选:
    • Spring Web:用于构建Web接口。
    • MyBatis Framework:数据库ORM框架。
    • MySQL Driver:连接MySQL。
    • Lombok:代码神器,用注解自动生成Getter/Setter等方法,让实体类非常简洁。

点击创建,IDEA会自动下载依赖并生成项目。

第二步:配置数据库和MyBatissrc/main/resources/application.properties(或application.yml,我更喜欢yml的清晰结构)中配置:

# 应用端口
server:
  port: 8080

# 数据库配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/book_manager?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: your_password # 换成你的密码
    driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml  # 告诉MyBatis我们的SQL映射文件在哪
  type-aliases-package: com.example.bookmanager.entity  # 实体类所在包,这样映射文件里就不用写全类名了
  configuration:
    map-underscore-to-camel-case: true  # 超有用!自动将数据库的user_name映射到Java的userName属性

第三步:编写实体类、Mapper和Service 这是MVC模式中的Model层。

  1. 实体类 (Entity):对应数据库表。在entity包下创建Book.java

    import lombok.Data;
    import java.math.BigDecimal;
    import java.time.LocalDateTime;
    
    @Data // Lombok注解,自动生成getter, setter, toString等方法
    public class Book {
        private Integer id;
        private String isbn;
        private String name;
        private String author;
        private Integer categoryId; // 数据库中是category_id,靠配置自动转
        private BigDecimal price;
        private Integer status;
        private String cover;
        private LocalDateTime createTime;
        // 非数据库字段,用于前端显示分类名
        private String categoryName;
    }
    
  2. Mapper接口:在mapper包下创建BookMapper.java。这里用MyBatis的注解方式写简单SQL,复杂SQL还是推荐用XML。

    import org.apache.ibatis.annotations.*;
    import java.util.List;
    
    @Mapper // 重要!让Spring知道这是一个MyBatis的Mapper
    public interface BookMapper {
        @Select("SELECT * FROM book WHERE name LIKE CONCAT('%', #{name}, '%')")
        List<Book> selectAll(@Param("name") String name);
    
        @Insert("INSERT INTO book(isbn, name, author, category_id, price, status) VALUES(#{isbn}, #{name}, #{author}, #{categoryId}, #{price}, #{status})")
        int insert(Book book);
    
        @Update("UPDATE book SET name=#{name}, author=#{author} WHERE id=#{id}")
        int update(Book book);
    
        @Delete("DELETE FROM book WHERE id = #{id}")
        int deleteById(Integer id);
    }
    
  3. Service层:处理业务逻辑。在service包下创建BookService.java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import java.util.List;
    
    @Service // 声明为Spring的业务组件
    public class BookService {
        @Autowired
        private BookMapper bookMapper;
    
        public List<Book> getBooks(String name) {
            return bookMapper.selectAll(name);
        }
    
        public boolean addBook(Book book) {
            // 这里可以加业务校验,比如ISBN是否重复
            return bookMapper.insert(book) > 0;
        }
        // 其他更新、删除方法...
    }
    

第四步:编写Controller层,提供API 这是前后端交互的桥梁。在controller包下创建BookController.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController // 表示这个类返回的是JSON数据,不是页面
@RequestMapping("/api/book") // 所有这个Controller下的接口路径都以/api/book开头
public class BookController {

    @Autowired
    private BookService bookService;

    // 查询图书列表 GET /api/book?name=xxx
    @GetMapping
    public Map<String, Object> listBooks(@RequestParam(required = false) String name) {
        Map<String, Object> result = new HashMap<>();
        try {
            List<Book> books = bookService.getBooks(name);
            result.put("code", 200);
            result.put("msg", "success");
            result.put("data", books);
        } catch (Exception e) {
            result.put("code", 500);
            result.put("msg", "查询失败:" + e.getMessage());
        }
        return result;
    }

    // 新增图书 POST /api/book
    @PostMapping
    public Map<String, Object> addBook(@RequestBody Book book) { // @RequestBody 接收前端传来的JSON对象
        Map<String, Object> result = new HashMap<>();
        try {
            boolean success = bookService.addBook(book);
            result.put("code", success ? 200 : 500);
            result.put("msg", success ? "添加成功" : "添加失败");
        } catch (Exception e) {
            result.put("code", 500);
            result.put("msg", "系统错误:" + e.getMessage());
        }
        return result;
    }
    // 更新和删除接口类似...
}

启动你的BookManagerApplication主类,访问 http://localhost:8080/api/book,如果返回JSON数据(哪怕是个空数组),恭喜你,后端API骨架已经搭成了!

5. 开发Vue前端:打造用户友好的管理界面

后端API在“嗷嗷待哺”,现在轮到前端登场,给系统装上好看易用的“脸面”。我们用Vue CLI快速创建一个项目。

第一步:创建Vue项目并安装核心依赖 打开终端,进入你的工作目录:

vue create book-manager-frontend

选择Manually select features,然后勾选Babel, Router, Vuex(状态管理,我们这个项目可以不用),CSS Pre-processors(选Sass),Linter可以暂时不选。完成后进入项目并安装Element Plus和Axios:

cd book-manager-frontend
npm install element-plus axios

第二步:配置全局引用和跨域代理src/main.js 中引入Element Plus和样式:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.mount('#app')

前端运行在localhost:8081,后端在8080,直接请求会触发浏览器的跨域限制。在Vue项目根目录创建或修改 vue.config.js 文件,配置开发服务器代理:

module.exports = {
  devServer: {
    proxy: {
      '/api': { // 所有以/api开头的请求
        target: 'http://localhost:8080', // 转发到后端地址
        changeOrigin: true, // 改变请求头中的host为目标地址
        pathRewrite: {
          '^/api': '' // 去掉请求路径中的/api前缀,因为后端接口本身有/api
        }
      }
    }
  }
}

这样,你在前端代码里请求/api/book,实际上会被转发到http://localhost:8080/api/book,完美解决跨域。

第三步:编写图书列表页面src/views 下创建 BookList.vue。我们将使用Element Plus的表格和分页组件。

<template>
  <div class="book-container">
    <!-- 搜索和操作栏 -->
    <div style="margin-bottom: 20px">
      <el-input v-model="searchName" placeholder="请输入书名" style="width: 200px" clearable @clear="loadBooks"/>
      <el-button type="primary" @click="loadBooks" :icon="Search">搜索</el-button>
      <el-button type="success" @click="dialogVisible = true" :icon="Plus">新增图书</el-button>
    </div>

    <!-- 数据表格 -->
    <el-table :data="bookList" border stripe style="width: 100%">
      <el-table-column prop="id" label="ID" width="80" />
      <el-table-column prop="isbn" label="ISBN" width="180" />
      <el-table-column prop="name" label="书名" />
      <el-table-column prop="author" label="作者" width="120" />
      <el-table-column prop="price" label="价格" width="100">
        <template #default="scope">¥{{ scope.row.price }}</template>
      </el-table-column>
      <el-table-column prop="status" label="状态" width="100">
        <template #default="scope">
          <el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
            {{ scope.row.status === 1 ? '在馆' : '借出' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="180">
        <template #default="scope">
          <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
          <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页 -->
    <div style="margin-top: 20px; text-align: center">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :page-sizes="[5, 10, 20]"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
        @size-change="loadBooks"
        @current-change="loadBooks"
      />
    </div>

    <!-- 新增/编辑图书的对话框 -->
    <el-dialog v-model="dialogVisible" :title="formTitle" width="500px">
      <el-form :model="bookForm" label-width="80px">
        <el-form-item label="ISBN">
          <el-input v-model="bookForm.isbn" />
        </el-form-item>
        <el-form-item label="书名">
          <el-input v-model="bookForm.name" />
        </el-form-item>
        <el-form-item label="作者">
          <el-input v-model="bookForm.author" />
        </el-form-item>
        <el-form-item label="价格">
          <el-input-number v-model="bookForm.price" :min="0" :precision="2" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确认</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted, computed } from 'vue'
import { Search, Plus } from '@element-plus/icons-vue'
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'

// 响应式数据
const searchName = ref('')
const bookList = ref([])
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const dialogVisible = ref(false)
const bookForm = ref({ id: null, isbn: '', name: '', author: '', price: 0 })
const isEditMode = ref(false)

// 计算属性:动态对话框标题
const formTitle = computed(() => isEditMode.value ? '编辑图书' : '新增图书')

// 加载图书数据
const loadBooks = async () => {
  try {
    const params = { page: currentPage.value, size: pageSize.value }
    if (searchName.value) params.name = searchName.value
    const response = await axios.get('/api/book', { params })
    if (response.data.code === 200) {
      bookList.value = response.data.data.list || response.data.data // 根据后端返回结构调整
      total.value = response.data.data.total || 0
    } else {
      ElMessage.error(response.data.msg)
    }
  } catch (error) {
    ElMessage.error('网络请求失败:' + error.message)
  }
}

// 提交表单(新增或更新)
const submitForm = async () => {
  const url = isEditMode.value ? `/api/book/${bookForm.value.id}` : '/api/book'
  const method = isEditMode.value ? 'put' : 'post'
  try {
    const response = await axios({ method, url, data: bookForm.value })
    if (response.data.code === 200) {
      ElMessage.success(response.data.msg)
      dialogVisible.value = false
      loadBooks() // 刷新列表
      resetForm()
    } else {
      ElMessage.error(response.data.msg)
    }
  } catch (error) {
    ElMessage.error('操作失败:' + error.message)
  }
}

// 编辑图书
const handleEdit = (row) => {
  isEditMode.value = true
  bookForm.value = { ...row } // 浅拷贝,避免直接修改原数据
  dialogVisible.value = true
}

// 删除图书
const handleDelete = (id) => {
  ElMessageBox.confirm('确定要删除这本图书吗?', '警告', { type: 'warning' })
    .then(async () => {
      const response = await axios.delete(`/api/book/${id}`)
      if (response.data.code === 200) {
        ElMessage.success('删除成功')
        loadBooks()
      }
    })
    .catch(() => {})
}

// 重置表单
const resetForm = () => {
  bookForm.value = { id: null, isbn: '', name: '', author: '', price: 0 }
  isEditMode.value = false
}

// 组件挂载时加载数据
onMounted(() => {
  loadBooks()
})
</script>

<style scoped>
.book-container {
  padding: 20px;
}
</style>

这个页面已经具备了查询、分页、新增、编辑、删除等完整功能。你需要根据后端API返回的实际数据结构(比如分页格式)稍作调整。将BookList.vue配置到路由中,然后运行 npm run serve,访问 http://localhost:8081,就能看到这个管理界面了。看着自己亲手搭建的页面能和后端真实交互,那种成就感是看教程无法比拟的。

6. 实现核心业务模块:借阅与归还逻辑

图书管理,光有CRUD(增删改查)还不够,借阅和归还是真正的业务核心。这里面的逻辑比单纯的增删要复杂一些,涉及到状态判断和多个表的关联操作。

后端实现:借阅接口 首先,在后端创建一个BorrowController和对应的BorrowService

  1. 借书逻辑:用户借书时,我们需要检查:

    • 图书是否存在且状态为“在馆”(status=1)。
    • 用户是否存在且状态正常。
    • 用户是否已有超期未还的图书(这个规则可以自定义)。 检查通过后,执行以下操作(这里要用到数据库事务,确保数据一致性):
    • borrow表插入一条借阅记录。
    • book表中对应图书的状态改为“借出”(status=0)。
    @Transactional // 声明式事务,任何一步失败都会回滚
    public boolean borrowBook(Integer userId, Integer bookId) {
        // 1. 检查图书状态
        Book book = bookMapper.selectById(bookId);
        if (book == null || book.getStatus() == 0) {
            throw new ServiceException("图书不存在或已被借出");
        }
        // 2. 检查用户状态(略)
        // 3. 插入借阅记录
        BorrowRecord record = new BorrowRecord();
        record.setUserId(userId);
        record.setBookId(bookId);
        record.setBorrowTime(LocalDateTime.now());
        record.setDueTime(LocalDateTime.now().plusDays(30)); // 假设借期30天
        record.setStatus(0);
        borrowMapper.insert(record);
        // 4. 更新图书状态
        book.setStatus(0);
        bookMapper.updateStatus(book);
        return true;
    }
    
  2. 还书逻辑:还书时,我们需要:

    • 找到对应的借阅记录(状态为“借出未还”)。
    • return表插入归还记录(或更新借阅记录状态)。
    • book表中对应图书的状态改回“在馆”。

前端实现:借阅操作页面 前端可以创建一个BorrowManagement.vue页面。管理员在此页面,通过选择用户和扫描图书ISBN(或手动选择)来进行借阅操作。

<template>
  <div>
    <el-steps :active="activeStep" finish-status="success" simple style="margin-bottom: 20px">
      <el-step title="选择用户" />
      <el-step title="扫描图书" />
      <el-step title="确认借阅" />
    </el-steps>

    <!-- 步骤1:选择用户 -->
    <div v-if="activeStep === 0">
      <el-input v-model="userSearch" placeholder="输入用户名或手机号搜索" @input="searchUser"/>
      <el-table :data="userList" @row-click="selectUser">
        <el-table-column prop="username" label="用户名"/>
        <el-table-column prop="name" label="姓名"/>
      </el-table>
    </div>

    <!-- 步骤2:扫描图书 -->
    <div v-if="activeStep === 1">
      <el-input v-model="bookIsbn" placeholder="扫描或输入图书ISBN" @keyup.enter="addBookToCart">
        <template #append>
          <el-button @click="addBookToCart">添加</el-button>
        </template>
      </el-input>
      <el-table :data="selectedBooks">
        <el-table-column prop="isbn" label="ISBN"/>
        <el-table-column prop="name" label="书名"/>
        <el-table-column label="操作">
          <template #default="scope">
            <el-button size="small" type="danger" @click="removeBook(scope.index)">移除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>

    <!-- 步骤3:确认并提交 -->
    <div v-if="activeStep === 2">
      <p>用户:{{ selectedUser.name }}</p>
      <p>待借阅图书:{{ selectedBooks.map(b => b.name).join(', ') }}</p>
      <el-button type="primary" @click="submitBorrow">确认借出</el-button>
    </div>

    <div style="margin-top: 20px">
      <el-button v-if="activeStep > 0" @click="activeStep--">上一步</el-button>
      <el-button v-if="activeStep < 2" type="primary" @click="activeStep++" :disabled="!canNext">下一步</el-button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import axios from 'axios'

const activeStep = ref(0)
const userSearch = ref('')
const userList = ref([])
const selectedUser = ref(null)
const bookIsbn = ref('')
const selectedBooks = ref([])

// 计算属性:判断能否进入下一步
const canNext = computed(() => {
  if (activeStep.value === 0) return selectedUser.value !== null
  if (activeStep.value === 1) return selectedBooks.value.length > 0
  return true
})

// 搜索用户
const searchUser = async () => {
  const res = await axios.get(`/api/user/search?keyword=${userSearch.value}`)
  userList.value = res.data.data
}

// 选择用户
const selectUser = (row) => {
  selectedUser.value = row
}

// 通过ISBN添加图书到借阅车
const addBookToCart = async () => {
  const res = await axios.get(`/api/book/isbn/${bookIsbn.value}`)
  if (res.data.code === 200 && res.data.data.status === 1) {
    selectedBooks.value.push(res.data.data)
    bookIsbn.value = ''
  } else {
    ElMessage.error('图书不存在或不可借')
  }
}

// 提交借阅
const submitBorrow = async () => {
  const bookIds = selectedBooks.value.map(b => b.id)
  const res = await axios.post('/api/borrow', {
    userId: selectedUser.value.id,
    bookIds: bookIds
  })
  if (res.data.code === 200) {
    ElMessage.success('借阅成功!')
    // 重置所有状态
    activeStep.value = 0
    selectedUser.value = null
    selectedBooks.value = []
  }
}
</script>

这个多步骤表单模拟了图书馆员实际的操作流程,用户体验更好。还书功能可以类似实现,通常是通过查询用户的未还记录列表,然后点击归还。

7. 项目联调、部署与常见问题排查

前后端都开发完了,但在合到一起时,总会遇到一些“坑”。我把最常见的问题和解决方案列出来,你遇到时可以快速排查。

1. 跨域问题 (CORS) 虽然开发时我们用Vue代理解决了,但项目打包部署到同一域名不同端口,或者用Nginx反向代理时,可能还会遇到。更稳妥的方式是在SpringBoot后端全局配置CORS。

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**") // 对所有/api开头的接口
                .allowedOriginPatterns("*") // 允许所有源,生产环境应指定具体域名
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

2. 前后端数据格式不一致 这是联调中最常见的问题。前端发送的JSON字段名是categoryId,后端实体类期望的是category_id(或反之)。确保:

  • 后端开启map-underscore-to-camel-case
  • 使用@JsonProperty注解指定字段别名。
  • 前后端协商好统一的命名规范(推荐后端实体用驼峰,接口文档也输出驼峰)。

3. 图片上传与访问 图书封面需要上传图片。通常步骤是:

  • 前端用<input type="file">或Element Plus的el-upload组件。
  • 后端用MultipartFile接收,保存到服务器特定目录(如D:/upload)或云存储。
  • 将文件访问路径(如/upload/filename.jpg)存入数据库。
  • 配置静态资源映射,使http://localhost:8080/upload/能访问到保存的图片。
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/upload/**")
                    .addResourceLocations("file:D:/upload/");
        }
    }
    

4. 项目打包与部署

  • 前端打包:运行 npm run build,会在dist目录生成静态文件(HTML, CSS, JS)。
  • 后端打包:使用Maven命令 mvn clean package,会在target目录生成一个可执行的jar文件(如book-manager-0.0.1-SNAPSHOT.jar)。
  • 部署
    • 传统方式:将前端dist目录下的文件,放到后端项目的src/main/resources/static目录下,然后打包成一个Fat Jar。这样访问8080端口就能看到前端页面。简单,但前后端耦合。
    • 分离部署(推荐)
      1. 将后端Jar包上传到服务器,用 java -jar book-manager.jar 运行。
      2. 将前端静态文件(dist目录)用Nginx托管。配置Nginx,将API请求反向代理到后端服务。
      server {
          listen       80;
          server_name  your-domain.com;
          # 前端静态资源
          location / {
              root   /home/www/book-manager;
              index  index.html;
              try_files $uri $uri/ /index.html; # 支持Vue Router的history模式
          }
          # 后端API代理
          location /api/ {
              proxy_pass http://localhost:8080;
              proxy_set_header Host $host;
              proxy_set_header X-Real-IP $remote_addr;
          }
          # 图片资源代理
          location /upload/ {
              proxy_pass http://localhost:8080;
          }
      }
      

5. 遇到问题怎么查?

  • 看浏览器控制台 (F12):看Network标签,请求是否发出、状态码(404接口不存在,500服务器错误)、请求参数和响应体是什么。这是定位前端问题的第一现场。
  • 看后端日志:SpringBoot默认在控制台打印日志。仔细看错误堆栈信息,它能精确告诉你哪一行代码出了问题。
  • 用Postman测试接口:绕过前端,直接用Postman调用后端接口,能快速判断问题是出在前端还是后端。
  • 检查依赖版本冲突:有时候莫名其妙的错误是库版本不兼容导致的。用mvn dependency:tree查看依赖树。

把这个项目从头到尾做一遍,你收获的不仅仅是一个图书管理系统,更是一套完整的、可复用的全栈开发流程和问题解决能力。下次再遇到类似的需求,你就能从容地拆解、设计和实现了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值