SpringBoot+Vue图书馆管理系统实战:从零搭建到部署详解(附源码+文档)

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

如果你正在寻找一个能快速上手、技术栈主流、并且能完整覆盖前后端开发的实战项目,那这个SpringBoot+Vue的图书馆管理系统绝对是你的不二之选。我做了这么多年全栈开发,带过不少新人,发现很多朋友学了一堆零散的技术点,但一到自己动手做项目就懵了,前后端怎么对接?数据库怎么设计?部署上线又该怎么办?这个项目就能帮你把这些问题一次性全打通。

简单来说,这个系统就是一个模拟真实图书馆运作的Web应用。管理员可以在后台管理图书、用户、处理借阅和归还;普通用户则可以在网页前端浏览图书、借书还书、查看自己的记录。听起来是不是挺熟悉的?对,这就是一个典型的CRUD(增删改查)应用,但它麻雀虽小五脏俱全,涵盖了用户认证、权限管理、数据关联、前后端分离通信等核心企业级开发场景。

那为什么偏偏是SpringBoot和Vue呢?我跟你打个比方,SpringBoot就像是一个已经帮你配好所有调料和厨具的智能厨房。你想炒个菜(开发个后端服务),不用再自己去买锅碗瓢盆(配置各种XML、部署服务器),直接开火就行。它内嵌了Tomcat服务器,用几个简单的注解就能快速构建出稳定可靠的RESTful API,大大降低了Java后端开发的门槛。而Vue.js呢,就像是一套乐高积木。它用组件化的思想,让你可以像搭积木一样构建用户界面。一个搜索框、一个图书列表、一个表单,都可以做成独立的组件,拼装起来既灵活又高效。而且它的数据绑定特性特别友好,你改了下层的数据,页面视图会自动更新,不用再手动去操作那些繁琐的DOM。

把这两个技术结合起来,采用前后端分离的架构,优势就非常明显了。后端SpringBoot专注提供数据接口,做好业务逻辑和数据库安全;前端Vue专注呈现交互友好的界面。两者通过HTTP API(主要是JSON格式)通信,分工明确,耦合度低。这意味着前端可以独立开发调试,后端接口也可以被小程序、APP等其他客户端复用。这种架构正是现在企业里最流行的做法。接下来,我就带你从零开始,一步步把这个系统搭建起来,我会把每个环节的细节、容易踩的坑都讲清楚,保证你跟着做就能跑通。

2. 手把手搭建开发环境与项目骨架

工欲善其事,必先利其器。在开始写代码之前,咱们得先把“战场”布置好。别担心,我用的都是最主流的免费工具,跟着我的步骤来,半小时内准能搞定。

2.1 后端开发环境准备

后端我们使用Java和SpringBoot。首先,确保你的电脑上安装了JDK 1.8或以上版本。打开命令行,输入 java -versionjavac -version 检查一下。如果没有,去Oracle官网或者AdoptOpenJDK下载安装就行。

接下来是项目管理和构建工具Maven。它负责帮你下载项目依赖的第三方库(Jar包)。去Apache官网下载Maven,解压后,需要配置一下系统的环境变量 MAVEN_HOME,并把 %MAVEN_HOME%\bin 添加到Path里。完成后,在命令行输入 mvn -v,能显示版本号就成功了。

我最推荐的集成开发环境(IDE)是 IntelliJ IDEA 的社区版,完全免费,对Java和SpringBoot的支持是顶级的。当然,你用Eclipse或者VS Code也可以,但IDEA的智能提示和快捷键能让你效率翻倍。安装好IDEA后,我们开始创建SpringBoot项目。不用去官网那个Initializr网站,IDEA内置了创建向导。点击 New Project,选择 Spring Initializr,这里有几个关键点要填对:

  • Project SDK:选择你刚安装的JDK 1.8。
  • Type:选择 Maven Project
  • Group:填你的组织域名倒写,比如 com.yourname
  • Artifact:填项目名,比如 library-backend
  • Dependencies 里,我们勾选几个核心依赖:Spring Web(用于构建Web接口)、Spring Data JPA(简化数据库操作)、MySQL Driver(连接MySQL数据库)、Lombok(用注解自动生成getter/setter等方法,让代码更简洁)。

点击创建,IDEA会自动生成项目结构并下载依赖。第一次可能会慢一点,耐心等待。项目创建好后,你会在 pom.xml 文件里看到我们刚才选的依赖。我建议再加一个 spring-boot-starter-validation 依赖,用于接口参数校验,非常实用。直接在 pom.xml<dependencies> 标签里加上这段:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2.2 前端开发环境准备

前端我们使用Vue.js。首先需要安装 Node.js,它自带了包管理工具npm。去Node.js官网下载LTS(长期支持版)安装。安装完成后,打开命令行,输入 node -vnpm -v 检查是否成功。

接下来安装Vue的官方脚手架工具 Vue CLI,它能快速生成标准化的Vue项目结构。在命令行运行:npm install -g @vue/cli。安装完成后,输入 vue --version 确认。

现在,在你喜欢的位置(比如和backend项目同级目录),打开命令行,运行 vue create library-frontend。Vue CLI会交互式地让你选择配置。对于这个项目,我建议:

  • 选择 Manually select features(手动选择特性)。
  • 勾选 Babel(ES6语法转换)、Router(路由管理)、Vuex(状态管理)、CSS Pre-processors(CSS预处理器,比如Sass)。
  • 选择 Sass/SCSS (with dart-sass)
  • 路由模式选择 History mode,输入 Y
  • 配置文件位置选择 In dedicated config files
  • 是否保存为未来项目的模板,可以选 N

等待创建完成,进入项目目录 cd library-frontend,先运行 npm run serve 试试。如果浏览器打开 http://localhost:8080 能看到Vue的欢迎页,说明前端环境也OK了。

2.3 数据库设计与初始化

系统离不开数据存储,我们选用最流行的开源数据库 MySQL。你可以安装MySQL 5.7或8.0版本,也可以使用更轻量的Docker来运行,看个人习惯。安装好后,用命令行或者Navicat、MySQL Workbench这样的图形化工具登录。

首先创建一个专门给这个项目用的数据库:CREATE DATABASE library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。这里用 utf8mb4 字符集是为了支持存储Emoji等特殊字符。

这个图书馆系统核心的表大概有这几张:

  1. 用户表 (user):存用户ID、用户名、密码(加密后)、角色(管理员/普通用户)、联系方式等。
  2. 图书分类表 (book_category):存分类ID、分类名称。
  3. 图书信息表 (book):存图书ID、书名、作者、出版社、ISBN、分类ID(外键关联分类表)、库存数量、封面图片地址等。
  4. 图书借阅记录表 (borrow_record):存记录ID、用户ID、图书ID、借阅时间、应还时间、实际归还时间、状态(借出/已归还/超期)等。
  5. 罚金记录表 (fine_record):存记录ID、借阅记录ID、罚金金额、是否缴纳等。

表之间的关系是:一个分类有多本图书,一个用户可以有多条借阅记录,一本图书也可以被多次借阅(通过记录关联),一条借阅记录可能产生一条罚金记录。你可以在数据库中手动建表,但我强烈建议把建表SQL语句写成文件(比如 init.sql),这样便于版本管理和重置。然后在SpringBoot的 application.yml 配置文件中,配置数据库连接信息:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root # 换成你的数据库用户名
    password: yourpassword # 换成你的数据库密码
  jpa:
    hibernate:
      ddl-auto: update # 启动时根据实体类自动更新表结构,生产环境建议用none或validate
    show-sql: true # 在控制台打印SQL语句,调试时非常有用

3. 后端核心功能实现与代码精讲

环境搭好,骨架建完,现在该注入“灵魂”了。后端代码我们采用经典的MVC分层架构:Controller接收请求,Service处理业务逻辑,Repository(DAO)操作数据库。这样职责清晰,方便维护。

3.1 实体类与数据访问层

首先,根据数据库表设计,创建对应的Java实体类。以 Book(图书)实体为例,我们使用JPA注解来映射数据库字段。这里强烈推荐使用Lombok,它能用 @Data 注解省去一大堆getter、setter、toString方法的编写。

package com.yourname.library.entity;

import lombok.Data;
import javax.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "book")
@Data
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title; // 书名

    private String author; // 作者
    private String publisher; // 出版社
    private String isbn; // ISBN号

    @ManyToOne
    @JoinColumn(name = "category_id")
    private BookCategory category; // 关联分类

    private Integer totalStock; // 总库存
    private Integer availableStock; // 可借库存
    private String coverImage; // 封面图片URL

    @Column(precision = 10, scale = 2)
    private BigDecimal price; // 价格

    // 省略构造方法,Lombok @Data 会默认生成
}

注意 @ManyToOne 注解,它定义了图书与分类的“多对一”关系。对应的,在 BookCategory 实体中,可以用 @OneToMany(mappedBy = "category") 定义反向的“一对多”关系。

实体类建好后,创建数据访问接口。Spring Data JPA的强大之处在于,你只需要定义一个继承 JpaRepository 的接口,基本的增删改查方法它就自动给你实现了。比如 BookRepository

package com.yourname.library.repository;

import com.yourname.library.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;

public interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> {
    // 根据书名模糊查询
    List<Book> findByTitleContaining(String title);

    // 根据作者查询
    List<Book> findByAuthor(String author);

    // 使用JPQL自定义复杂查询:查询某个分类下库存大于0的图书
    @Query("SELECT b FROM Book b WHERE b.category.id = :categoryId AND b.availableStock > 0")
    List<Book> findAvailableBooksByCategory(@Param("categoryId") Long categoryId);
}

继承 JpaSpecificationExecutor 接口后,你还可以使用更灵活的动态查询条件,这在做多条件组合搜索时特别好用。

3.2 业务逻辑层与服务

Service层是业务逻辑的核心。它调用Repository获取数据,进行业务规则校验和处理,然后返回结果给Controller。这里以 BookService 和借书业务为例:

package com.yourname.library.service;

import com.yourname.library.entity.Book;
import com.yourname.library.entity.BorrowRecord;
import com.yourname.library.entity.User;
import com.yourname.library.exception.BusinessException;
import com.yourname.library.repository.BookRepository;
import com.yourname.library.repository.BorrowRecordRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;

@Service
@RequiredArgsConstructor // Lombok注解,为final字段生成构造函数,实现依赖注入
public class BookService {
    private final BookRepository bookRepository;
    private final BorrowRecordRepository borrowRecordRepository;

    // 借书业务,使用事务保证数据一致性
    @Transactional
    public BorrowRecord borrowBook(Long bookId, User user) {
        // 1. 检查图书是否存在且可借
        Book book = bookRepository.findById(bookId)
                .orElseThrow(() -> new BusinessException("图书不存在"));
        if (book.getAvailableStock() <= 0) {
            throw new BusinessException("该图书暂无库存");
        }

        // 2. 检查用户是否有超期未还或罚金未缴记录(这里简化,实际可能需单独查询)
        // ...

        // 3. 创建借阅记录
        BorrowRecord record = new BorrowRecord();
        record.setBook(book);
        record.setUser(user);
        record.setBorrowTime(LocalDateTime.now());
        record.setDueTime(LocalDateTime.now().plusDays(30)); // 假设借期30天
        record.setStatus("BORROWED");

        // 4. 更新图书库存
        book.setAvailableStock(book.getAvailableStock() - 1);
        bookRepository.save(book); // 保存库存更新

        // 5. 保存借阅记录
        return borrowRecordRepository.save(record);
    }

    // 其他方法:还书、查询图书列表、添加图书等...
}

这里有几个关键点:第一,使用了 @Transactional 注解,这意味着如果借书过程中任何一步失败(比如库存更新了但记录没存进去),整个操作会回滚,数据不会错乱。第二,我们自定义了一个 BusinessException(运行时异常),用于抛出业务错误,方便Controller层统一捕获并返回友好的错误信息给前端。第三,使用了Lombok的 @RequiredArgsConstructor 进行依赖注入,代码比传统的 @Autowired 更简洁。

3.3 控制层与RESTful API设计

Controller层负责接收HTTP请求,调用Service,并返回HTTP响应。我们遵循RESTful风格设计API,让接口清晰易懂。同时,使用 @Valid 注解配合实体类中的校验注解(如 @NotBlank)来做参数校验。

package com.yourname.library.controller;

import com.yourname.library.entity.Book;
import com.yourname.library.service.BookService;
import com.yourname.library.vo.BookVO;
import com.yourname.library.vo.ResultVO;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;

@RestController
@RequestMapping("/api/books")
@RequiredArgsConstructor
public class BookController {
    private final BookService bookService;

    // 分页查询图书列表
    @GetMapping
    public ResultVO<Page<BookVO>> getBooks(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy) {
        Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy).descending());
        Page<BookVO> bookPage = bookService.getBooks(pageable);
        return ResultVO.success(bookPage);
    }

    // 根据ID获取图书详情
    @GetMapping("/{id}")
    public ResultVO<BookVO> getBookById(@PathVariable Long id) {
        BookVO book = bookService.getBookById(id);
        return ResultVO.success(book);
    }

    // 添加新图书
    @PostMapping
    public ResultVO<Book> addBook(@RequestBody @Valid Book book) { // @Valid 触发校验
        Book savedBook = bookService.addBook(book);
        return ResultVO.success(savedBook);
    }

    // 借书接口
    @PostMapping("/{bookId}/borrow")
    public ResultVO borrowBook(@PathVariable Long bookId, @RequestParam Long userId) {
        // 这里需要从请求中获取当前登录用户,简化起见用userId参数
        bookService.borrowBook(bookId, userId);
        return ResultVO.success("借书成功");
    }

    // 统一异常处理(可以放在一个单独的@ControllerAdvice类中)
    @ExceptionHandler(BusinessException.class)
    public ResultVO handleBusinessException(BusinessException e) {
        return ResultVO.error(400, e.getMessage());
    }
}

注意,我们返回给前端的数据通常不是原始的实体对象,而是封装了一层 ResultVO(包含code、message、data),并且实体对象也经常转换为 BookVO(View Object)来过滤掉一些敏感或不必要的字段(如密码),或者组合一些关联数据,这能更好地控制API的输出。分页查询使用了Spring Data JPA的 PagePageable 对象,前端只需要传页码和每页大小,后端就能返回带分页信息的结果,非常方便。

4. 前端Vue组件开发与界面交互

后端API准备好了,现在我们来构建用户能看到和操作的界面。Vue的核心思想是组件化,我们将系统拆分成一个个可复用的组件。

4.1 项目结构规划与路由配置

首先,规划一下 src 目录下的结构。我习惯这样组织:

src/
├── api/          # 所有与后端交互的请求函数,使用axios封装
├── assets/       # 静态资源(图片、字体等)
├── components/   # 公共组件(如导航栏、分页器、搜索框)
├── router/       # Vue Router路由配置
├── store/        # Vuex状态管理
├── utils/        # 工具函数(如请求拦截器、日期格式化)
├── views/        # 页面级组件(如首页、图书列表页、个人中心页)
└── App.vue       # 根组件

router/index.js 中配置路由。我们需要根据用户角色(管理员/普通用户)来动态加载路由,这可以通过路由守卫(beforeEach)来实现。

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import { getToken } from '@/utils/auth' // 假设从本地存储获取token

Vue.use(VueRouter)

// 公共路由(无需登录)
const constantRoutes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue')
  },
  {
    path: '/books',
    name: 'BookList',
    component: () => import('@/views/book/BookList.vue')
  }
]

// 需要登录才能访问的动态路由
const asyncRoutes = [
  {
    path: '/user/borrow',
    name: 'MyBorrow',
    component: () => import('@/views/user/MyBorrow.vue'),
    meta: { roles: ['user', 'admin'] } // 用户和管理员都能访问
  },
  {
    path: '/admin/book-manage',
    name: 'BookManage',
    component: () => import('@/views/admin/BookManage.vue'),
    meta: { roles: ['admin'] } // 仅管理员
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: constantRoutes
})

// 路由守卫:检查登录状态和权限
router.beforeEach((to, from, next) => {
  const hasToken = getToken()
  if (to.meta && to.meta.roles) {
    // 需要权限的路由
    if (!hasToken) {
      next('/login')
    } else {
      // 这里需要从Vuex或API获取当前用户角色
      const userRole = 'user' // 假设当前角色
      if (to.meta.roles.includes(userRole)) {
        next()
      } else {
        next('/403') // 跳转到无权限页面
      }
    }
  } else {
    next() // 公共路由直接放行
  }
})

export default router

4.2 核心页面组件开发

以用户端的“图书列表页” (BookList.vue) 为例,它通常包含搜索框、图书卡片列表和分页组件。

<template>
  <div class="book-list-page">
    <!-- 搜索区域 -->
    <div class="search-bar">
      <el-input
        v-model="searchParams.keyword"
        placeholder="输入书名或作者"
        @keyup.enter="handleSearch"
      >
        <el-button slot="append" icon="el-icon-search" @click="handleSearch"></el-button>
      </el-input>
      <el-select v-model="searchParams.categoryId" placeholder="全部分类" @change="handleSearch">
        <el-option label="全部分类" :value="null"></el-option>
        <el-option
          v-for="cat in categories"
          :key="cat.id"
          :label="cat.name"
          :value="cat.id"
        ></el-option>
      </el-select>
    </div>

    <!-- 图书列表 -->
    <div class="book-list">
      <el-row :gutter="20">
        <el-col
          v-for="book in bookList"
          :key="book.id"
          :xs="24"
          :sm="12"
          :md="8"
          :lg="6"
          class="book-col"
        >
          <book-card :book="book" @borrow="handleBorrow(book.id)" />
        </el-col>
      </el-row>
    </div>

    <!-- 分页 -->
    <div class="pagination">
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="pagination.current"
        :page-sizes="[10, 20, 50, 100]"
        :page-size="pagination.size"
        layout="total, sizes, prev, pager, next, jumper"
        :total="pagination.total"
      >
      </el-pagination>
    </div>
  </div>
</template>

<script>
import { getBookList, getCategories } from '@/api/book'
import BookCard from '@/components/BookCard.vue'

export default {
  name: 'BookList',
  components: { BookCard },
  data() {
    return {
      searchParams: {
        keyword: '',
        categoryId: null
      },
      bookList: [],
      categories: [],
      pagination: {
        current: 1,
        size: 10,
        total: 0
      }
    }
  },
  created() {
    this.fetchCategories()
    this.fetchBookList()
  },
  methods: {
    async fetchCategories() {
      try {
        const res = await getCategories()
        this.categories = res.data
      } catch (error) {
        this.$message.error('获取分类失败')
      }
    },
    async fetchBookList() {
      try {
        const params = {
          ...this.searchParams,
          page: this.pagination.current - 1, // 后端页码通常从0开始
          size: this.pagination.size
        }
        const res = await getBookList(params)
        this.bookList = res.data.content
        this.pagination.total = res.data.totalElements
      } catch (error) {
        this.$message.error('获取图书列表失败')
      }
    },
    handleSearch() {
      this.pagination.current = 1 // 搜索时回到第一页
      this.fetchBookList()
    },
    handleSizeChange(size) {
      this.pagination.size = size
      this.fetchBookList()
    },
    handleCurrentChange(current) {
      this.pagination.current = current
      this.fetchBookList()
    },
    async handleBorrow(bookId) {
      try {
        await this.$confirm('确认借阅这本书吗?', '提示', { type: 'warning' })
        await borrowBook(bookId) // 调用借书API
        this.$message.success('借阅成功!')
        this.fetchBookList() // 刷新列表,更新库存显示
      } catch (error) {
        if (error !== 'cancel') {
          this.$message.error(error.message || '借阅失败')
        }
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.book-list-page {
  padding: 20px;
  .search-bar {
    margin-bottom: 20px;
    display: flex;
    gap: 15px;
  }
  .pagination {
    margin-top: 30px;
    text-align: center;
  }
}
</style>

这个组件展示了几个Vue的核心特性:v-model 实现表单双向绑定;v-for 渲染列表;@click@change 事件监听;以及使用 async/await 处理异步的API请求。我们引入了Element UI组件库(需要提前安装:npm i element-ui -S)来快速搭建美观的UI。BookCard 是一个独立的子组件,负责展示单本图书的卡片,这样代码更清晰,也便于复用。

4.3 状态管理与API请求封装

对于跨组件共享的状态,比如用户登录信息,我们使用 Vuex 来管理。在 store 目录下创建模块:

// store/modules/user.js
const state = {
  token: localStorage.getItem('token') || '',
  userInfo: JSON.parse(localStorage.getItem('userInfo')) || null
}

const mutations = {
  SET_TOKEN(state, token) {
    state.token = token
    localStorage.setItem('token', token)
  },
  SET_USER_INFO(state, userInfo) {
    state.userInfo = userInfo
    localStorage.setItem('userInfo', JSON.stringify(userInfo))
  },
  CLEAR_USER(state) {
    state.token = ''
    state.userInfo = null
    localStorage.removeItem('token')
    localStorage.removeItem('userInfo')
  }
}

const actions = {
  login({ commit }, { username, password }) {
    return new Promise((resolve, reject) => {
      // 调用封装的API
      loginApi({ username, password }).then(res => {
        commit('SET_TOKEN', res.data.token)
        commit('SET_USER_INFO', res.data.user)
        resolve(res)
      }).catch(error => {
        reject(error)
      })
    })
  },
  logout({ commit }) {
    commit('CLEAR_USER')
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

所有与后端的HTTP请求,我们都用 axios 进行统一封装,好处是能集中处理请求拦截(如自动添加token)、响应拦截(如统一处理错误)和基础配置。

// utils/request.js
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // 在 .env.development 等环境变量文件中配置
  timeout: 10000
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    if (store.getters.token) {
      config.headers['Authorization'] = 'Bearer ' + store.getters.token
    }
    return config
  },
  error => {
    console.log(error)
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data
    // 假设后端统一返回格式为 { code: 200, message: 'success', data: ... }
    if (res.code !== 200) {
      Message.error(res.message || 'Error')
      // 如果是token过期等特定错误,可以触发登出
      if (res.code === 401) {
        store.dispatch('user/logout').then(() => {
          location.reload() // 重新登录
        })
      }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.log('err' + error)
    Message.error(error.message || '网络请求失败')
    return Promise.reject(error)
  }
)

export default service

然后在 api/book.js 中,引入这个 request 实例,定义具体的接口函数:

import request from '@/utils/request'

export function getBookList(params) {
  return request({
    url: '/api/books',
    method: 'get',
    params
  })
}

export function borrowBook(bookId) {
  return request({
    url: `/api/books/${bookId}/borrow`,
    method: 'post'
  })
}

5. 项目打包与生产环境部署

代码都写好了,在本地跑得也挺顺畅,最后一步就是把它部署到服务器上,让所有人都能访问。这是从开发到上线的临门一脚,同样很重要。

5.1 前后端项目打包

首先,我们需要把源代码转换成生产环境能直接运行的“成品”。

后端打包:在SpringBoot项目的根目录(有 pom.xml 的目录)下,打开命令行,运行 mvn clean package -DskipTests。这个命令会清理旧的构建,跳过测试,然后打包。完成后,你会在 target 目录下找到一个名字类似 library-backend-0.0.1-SNAPSHOT.jar 的文件。这就是一个可执行的 Fat JAR,它里面包含了你的应用代码、依赖库以及内嵌的Tomcat服务器。你可以用 java -jar library-backend-0.0.1-SNAPSHOT.jar 直接运行它,默认端口是8080。

前端打包:进入Vue项目目录,运行 npm run build。这个命令会启动Webpack进行代码压缩、混淆、Tree Shaking等优化,最终在 dist 目录下生成静态资源文件(HTML、CSS、JS)。这里有个关键点:因为我们是前后端分离,前端打包后需要知道后端API的地址。我们之前在 request.js 里用了 process.env.VUE_APP_BASE_API,这个值可以在项目根目录创建 .env.production 文件来设置:VUE_APP_BASE_API = 'http://你的服务器IP或域名/api'。打包命令会自动使用这个生产环境配置。

5.2 服务器环境配置与部署

假设你有一台云服务器(CentOS 7.x 或 Ubuntu 20.04)。部署的典型架构是:Nginx作为反向代理和静态资源服务器,SpringBoot JAR包作为后端应用运行。

  1. 安装Java和Nginx

    # CentOS
    sudo yum install -y java-1.8.0-openjdk nginx
    # Ubuntu
    sudo apt update
    sudo apt install -y openjdk-8-jdk nginx
    
  2. 部署后端:将打包好的 xxx.jar 上传到服务器,比如 /opt/app/ 目录。可以创建一个Systemd服务来管理它,这样能开机自启、方便查看日志。创建文件 /etc/systemd/system/library.service

    [Unit]
    Description=Library Backend Service
    After=network.target
    
    [Service]
    Type=simple
    User=root
    WorkingDirectory=/opt/app
    ExecStart=/usr/bin/java -jar library-backend-0.0.1-SNAPSHOT.jar
    Restart=on-failure
    
    [Install]
    WantedBy=multi-user.target
    

    然后运行 sudo systemctl daemon-reloadsudo systemctl start librarysudo systemctl enable library 启动并设置开机自启。用 sudo systemctl status library 查看状态,用 journalctl -u library -f 跟踪日志。

  3. 部署前端:将 dist 目录下的所有文件上传到服务器,比如 /usr/share/nginx/html/library

  4. 配置Nginx:编辑Nginx配置文件,通常是 /etc/nginx/conf.d/library.conf

    server {
        listen 80;
        server_name your-domain.com; # 你的域名或IP
    
        # 前端静态文件
        location / {
            root /usr/share/nginx/html/library;
            index index.html;
            try_files $uri $uri/ /index.html; # 支持Vue的history模式
        }
    
        # 反向代理到后端API
        location /api/ {
            proxy_pass http://127.0.0.1:8080/; # 后端服务地址
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    

    检查配置语法:sudo nginx -t,无误后重载配置:sudo nginx -s reload

5.3 域名、HTTPS与持续集成

要让项目更专业、更安全,还有几步可以优化:

  • 域名绑定:在域名DNS管理后台,将你的域名A记录解析到服务器公网IP。
  • HTTPS加密:现在网站没有小绿锁(HTTPS)几乎不可接受。你可以使用 Let‘s Encrypt 免费申请SSL证书。安装Certbot工具,运行一条命令就能自动为你的Nginx配置HTTPS,它会自动处理证书申请和续期。
  • 数据库优化:生产环境的MySQL连接建议使用连接池(如HikariCP,SpringBoot默认集成),并在 application-prod.yml 中调整连接参数。数据库最好与应用服务器分离,并定期备份。
  • 持续集成/持续部署:如果你后续代码有更新,每次都手动上传打包太麻烦。可以搭建一套简单的CI/CD流程,比如使用 JenkinsGitHub Actions。当你把代码推送到Git仓库的特定分支时,自动触发脚本完成测试、打包、部署到服务器的全过程,实现自动化运维。

走到这一步,你的SpringBoot+Vue图书馆管理系统就已经从一个本地项目,变成了一个在互联网上可访问的、功能完整的Web应用了。这个过程里,你不仅学会了具体的技术,更重要的是掌握了全栈开发的完整工作流。我建议你把源码和部署过程好好整理一下,这不仅是你的学习成果,未来面试或者接一些小项目,这都是非常拿得出手的实战经验。如果在部署过程中遇到端口冲突、权限问题或者跨域报错,别慌,多看看日志,九成的问题都能在日志里找到答案。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值