Flask+Vue图书管理系统:从数据库初始化到答辩演示的完整实践
2026/9/15 13:40:59 网站建设 项目流程

简介:面向毕业设计场景的Flask+Vue图书管理系统完整源码包,适合需要快速搭建前后端分离项目的计算机专业学生,解决从环境配置、数据库初始化到生产部署的完整落地问题。压缩包共含70个文件,约14.97MB,以Vue组件、Python后端脚本、TypeScript与JSON配置为主,另含PNG/JPG界面截图、Markdown说明及部署Shell脚本,可直观了解页面效果和项目结构。资源内置开发与生产两套环境配置,后端支持SQLite/MySQL切换,附带requirements依赖、Gunicorn/uWSGI部署配置和前端Nginx模板,初始化脚本与README、DEPLOYMENT文档可帮助逐项排查配置问题。项目还包含管理员账号等基础数据,便于直接启动演示,目前已有45人浏览学习,适合作为毕业设计参考或二次开发基础。

1. 从课程设计到可答辩:Flask+Vue图书管理系统的技术选型与工作量分布

答辩现场最能看出一套图书管理系统功课的瞬间,是临时换一台没装任何工具的电脑,当着老师的面把数据库重建、依赖安装、服务启动这整套流程走一遍。很多仓库功能没问题,却经不起这一步:换环境就挂,前端端口起不来,数据库缺表报空指针异常,最后只能用“在我电脑上是好的”搪塞过去。这个基于Flask+Vue的图书管理系统,把后端接口、前端页面和数据库初始化拆成三块独立代码。Flask框架负责为图书、读者、借阅记录提供增删改查接口;Vue页面通过axios与后端通信;数据表与预置数据全部交给一个初始化脚本完成,源码包里附带的使用说明只解决一个问题——让项目在别人的机器上三分钟内跑起来。这篇文章顺着这套代码的落地顺序,把模型设计、接口约定、前端联调、脚本执行和答辩前置的小功能逐层讲清楚。适合第一次做前后端分离课程设计的在校生,也适合需要快速搭基线的开发者。

2. 后端先行:Flask蓝图与SQLAlchemy模型如何支撑图书管理核心表

2.1 为什么不用单文件app.py组织路由

最常见的课程设计级Flask后端是单文件app.py,里面注册了所有路由。接口少的时候跑得欢,路由一多就不好维护:登录路由、图书路由、借阅路由全挤在同一个文件里,排错时不得不上下翻找装饰器。后来我改用Blueprint按业务域拆分模块,books模块管图书、auth模块管登录,每个模块只暴露自己的路由和视图函数。

from flask import Flask from .views.books import books_bp from .views.auth import auth_bp def create_app(): app = Flask(__name__) app.config.from_object('app.config.Config') app.register_blueprint(books_bp, url_prefix='/api/books') app.register_blueprint(auth_bp, url_prefix='/api/auth') return app

url_prefix参数决定了这组接口的公共前缀,前端axios请求路径直接照这个前缀拼接,不用在每个装饰器里重复写。把业务相关的路由收敛到视图包里之后,app包只负责装配,init_db.py也能直接调用create_app()拿应用实例,避免在初始化脚本里重复配置。注册顺序不敏感,但前缀设计要在写接口前想好,后面改路径会牵连前端所有请求地址。

提示:课程设计级别的项目不要为了“显得规范”引入太多扩展,Flask、Flask-SQLAlchemy、Flask-CORS三个就够;与其维护迁移脚本,不如让初始化脚本重建表更可控。

2.2 图书、读者、借阅三张表的模型定义与字段取舍

初始化数据库之前,先把模型定义清楚。图书管理系统里最值得设计的关联是借阅记录:一本图书可以先后被多个读者借走,一个读者也能同时借多本,所以借阅记录单独建一张表,用book_id和user_id做外键,而不是在Book表上放一个is_borrowed布尔值。用布尔值表示“是否被借出”在并发和续借场景下都会出问题,查历史记录时更是无从下手。

from datetime import datetime from .extensions import db class Book(db.Model): __tablename__ = 'books' id = db.Column(db.Integer, primary_key=True) isbn = db.Column(db.String(20), unique=True, index=True) title = db.Column(db.String(200), nullable=False) author = db.Column(db.String(100)) publisher = db.Column(db.String(100)) total_stock = db.Column(db.Integer, default=1) available = db.Column(db.Integer, default=1) class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) student_no = db.Column(db.String(20), unique=True) real_name = db.Column(db.String(50)) password = db.Column(db.String(128)) class BorrowRecord(db.Model): __tablename__ = 'borrow_records' id = db.Column(db.Integer, primary_key=True) book_id = db.Column(db.Integer, db.ForeignKey('books.id')) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) borrow_date = db.Column(db.DateTime, default=datetime.now) due_date = db.Column(db.DateTime) return_date = db.Column(db.DateTime, nullable=True) book = db.relationship('Book') user = db.relationship('User')

available字段是一个有意的冗余设计,它保存当前可借数量,在借出时减一、归还时加一,查询“可借数量”时避免对借阅记录做count聚合,演示时响应更快,代码也更直观。total_stock与available两个字段配合,答辩时可以解释为库存逻辑。password字段存的是Werkzeug生成的哈希值,不是明文,这个细节在答辩现场经常被反复问到。due_date在借出时计算,默认借期30天,超期判断不依赖定时任务,查询时比较当前时间即可。

2.3 图书列表接口与借阅接口的约定

后端接口统一返回JSON结构,成功时data字段携带数据,失败时message字段给出原因,前端axios拦截器才能统一弹提示。下面是图书列表接口的实现:

from flask import Blueprint, request, jsonify from app.models import Book from ..extensions import db books_bp = Blueprint('books', __name__) @books_bp.route('', methods=['GET']) def list_books(): page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 10, type=int) keyword = request.args.get('keyword', '', type=str) query = Book.query if keyword: like = f'%{keyword}%' query = query.filter(db.or_(Book.title.like(like), Book.author.like(like))) pagination = query.paginate(page=page, per_page=per_page, error_out=False) items = [{ 'id': b.id, 'isbn': b.isbn, 'title': b.title, 'author': b.author, 'available': b.available } for b in pagination.items] return jsonify({'data': items, 'total': pagination.total, 'page': page})

request.args.get带type=int做类型转换,比手动int()更稳,参数缺失时直接走默认值,不会抛异常。paginate是Flask-SQLAlchemy自带的简单分页,total字段供前端表格渲染总页数。查询条件这里用的是or_,同时匹配书名字段和作者字段,适合演示时按关键词搜索。

下表是前后端联调时的接口约定,后端写完按这个表自测,再去对接Vue页面。

方法路径功能鉴权
POST/api/auth/login登录换取token
GET/api/books图书分页列表
GET/api/books/图书详情
POST/api/borrow借阅
PUT/api/borrow/return归还

POST /api/borrow的请求体是{"book_id": 1},后端在事务里做三件事:检查可借数量、扣减available、插入一条BorrowRecord。鉴权列表示该接口是否要求请求头携带Authorization字段,登录成功后端返回token,前端保存到localStorage,后续请求在拦截器里自动带上。

3. 前端对屏:Vue组件与Axios封装把借阅流程串起来

3.1 Vue项目结构与依赖版本选择

Vue侧的项目结构直接影响联调效率。我习惯用Vue CLI创建项目,在frontend目录下安装依赖,命令如下:

npx @vue/cli create frontend cd frontend npm install axios element-ui vue-router@3

创建向导里选择Manually select features并勾选Router即可。版本匹配是个容易踩的坑:Vue 2项目配Element UI和vue-router@3,Vue 3项目配Element Plus和vue-router@4,混用会在运行时出现Unknown custom element或router-view不渲染的问题。依赖对照如下:

依赖包Vue 2项目Vue 3项目
vue-routervue-router@3vue-router@4
UI组件库element-uielement-plus
@vue/cli4.x5.x

工程根目录建议把frontend和backend分开,node_modules与Python虚拟环境各归各,互不干扰。src目录内部按api、router、views、utils划分,视图里不直接写axios请求,统一走api层,这样后端接口路径变更时只改一处文件。

3.2 Axios拦截器与开发环境代理

axios封装是前后端分离项目的关键基础设施。拦截器把token注入、错误提示、登录失效跳转集中到一处,页面组件里只关心业务数据。下面是我常用的request.js写法:

import axios from 'axios' import { Message } from 'element-ui' import router from '../router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== undefined && res.code !== 0) { Message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } else { Message.error(error.response ? error.response.data.message : '网络异常') } return Promise.reject(error) } ) export default service

baseURL设为/api,配合开发服务器的代理,前端代码里不需要写完整的后端地址。vue.config.js里配置如下:

module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:5000', changeOrigin: true } } } }

这里要说明为什么配代理:开发时前端跑在8080端口,后端Flask跑在5000端口,浏览器同源策略会拦截跨端口请求。devServer.proxy把/api开头的请求转发到5000,前端开发时看到的是同源请求,部署到服务器后再由Nginx做同样的转发,前端代码一行不用改。

3.3 图书列表页与借阅操作的组件写法

图书列表页用el-table渲染数据,配合el-pagination做分页。组件模板里最关键的是操作列的作用域插槽,scoped变量row能拿到当前行数据,根据available字段控制借阅按钮的禁用状态。

<template> <div> <el-input v-model="keyword" placeholder="按书名/作者搜索" clearable style="width: 280px" @keyup.enter="loadBooks" @clear="loadBooks" /> <el-table :data="books" v-loading="loading" border> <el-table-column prop="isbn" label="ISBN" width="160" /> <el-table-column prop="title" label="书名" /> <el-table-column prop="author" label="作者" /> <el-table-column prop="available" label="可借数量" width="100" /> <el-table-column label="操作" width="150"> <template slot-scope="scope"> <el-button type="primary" size="small" :disabled="scope.row.available <= 0" @click="borrowBook(scope.row)">借阅</el-button> </template> </el-table-column> </el-table> <el-pagination :page-size="perPage" :total="total" :current-page.sync="page" layout="total, prev, pager, next" @current-change="loadBooks" /> </div> </template>
import { getBooks, borrowBookApi } from '../api/book' export default { data() { return { books: [], keyword: '', page: 1, perPage: 10, total: 0, loading: false } }, created() { this.loadBooks() }, methods: { async loadBooks() { this.loading = true try { const res = await getBooks({ page: this.page, per_page: this.perPage, keyword: this.keyword }) this.books = res.data this.total = res.total } finally { this.loading = false } }, async borrowBook(row) { await borrowBookApi(row.id) this.$message.success('借阅成功') this.loadBooks() } } }

created生命周期里调用loadBooks,保证页面一打开就有数据。分页组件用.sync修饰符同步当前页,翻页后重新请求列表。借阅成功后再刷新列表,available会从后端重新拉取,不需要手动改前端状态。

4. 数据库初始化与使用说明:init_db脚本给答辩老师的运行流程

4.1 为什么数据库初始化要把drop和insert写在同一个脚本里

很多教程让你在flask shell里手动建表,再逐个插入测试数据。这套流程在开发机上没问题,答辩换机时就麻烦了:老师不可能陪你在终端里敲create_all。独立的数据库初始化脚本把删表、建表、写入预置数据三步合并成一次执行,跑完就能得到一个干净的演示库。这个脚本同时还是“数据库课程设计”答辩中说明表结构设计的最好素材,老师在代码里能看到每张表和字段的含义。

4.2 init_db.py的最小完整实现

from app import create_app, db from app.models import Book, User from werkzeug.security import generate_password_hash app = create_app() def init(): with app.app_context(): db.drop_all() db.create_all() admin = User(student_no='admin', real_name='系统管理员', password=generate_password_hash('123456')) reader = User(student_no='2021001', real_name='张三', password=generate_password_hash('123456')) db.session.add_all([admin, reader]) books = [ Book(isbn='978-7-111-11111-1', title='计算机网络', author='Tanenbaum', publisher='机械工业出版社', total_stock=5, available=5), Book(isbn='978-7-111-22222-2', title='深入理解计算机系统', author='Bryant', publisher='电子工业出版社', total_stock=3, available=3), ] db.session.add_all(books) db.session.commit() print('数据库初始化完成') if __name__ == '__main__': init()

脚本开头从app包导入create_app和db,避免在models和app之间产生循环导入。db对象建议单独放在extensions.py里实例化,models.py只负责定义表结构,app/init.py负责装配视图和调用db.init_app(app)。with app.app_context()包裹是必须的,create_all和查询操作都需要应用上下文。drop_all先清空旧表,保证重新执行不会因为表结构变化而报错。预置读者时用student_no作为学号,后续登录借阅都用这个字段。

4.3 默认账号与预置数据设计

初始化脚本里预置的数据要覆盖演示路径:管理员登录后台、读者登录借书、归还时超期判断。账号信息如下:

角色账号密码用途
管理员admin123456图书录入、读者管理
普通读者2021001123456借阅、归还、查看记录

密码统一用generate_password_hash生成哈希再入库,登录时用check_password_hash比对。预置图书选两本经典教材,库存分别设为5和3,一是贴近真实场景,二是方便演示“库存不足”时按钮禁用的效果。如果需要演示超期场景,可以在BorrowRecord里直接插入一条due_date为昨天、return_date为空的记录,前端列表就能立刻显示逾期状态。

4.4 使用说明文档必须交代清楚的四件事

使用说明文档是这套源码能不能在陌生机器上跑起来的关键,写到四件事就够:环境版本、依赖安装、初始化顺序、常见故障。环境版本要具体,Python 3.8+、Node 14+、npm版本都写清楚。依赖安装和启动顺序固定成下面两条命令链:

cd backend pip install -r requirements.txt python init_db.py python app.py
cd frontend npm install npm run serve

顺序上必须先初始化数据库再启动后端,否则首次启动时表还不存在。前端在后端启动之后再npm run serve,端口冲突时先看5000是否被占用,再看8080是否被占用。使用说明里顺手写一句“首次跑通预计耗时3分钟”,比写长篇环境搭建教程实用得多。

注意:不要把SQLite文件提交到git仓库里,init_db.py能重建一切,data.db应该留在.gitignore里。

5. 答辩前值得补的小功能:CSV导出、超期天数与借阅状态联动

5.1 CSV导出图书列表

答辩时演示数据导出是个加分项,实现成本很低。后端新增一个导出接口,用csv标准库输出文本流,前端放一个下载按钮就行。

import csv import io from flask import Response from app.models import Book @app.get('/api/books/export') def export_books(): books = Book.query.all() buf = io.StringIO() writer = csv.writer(buf) writer.writerow(['isbn', 'title', 'author', 'available']) for b in books: writer.writerow([b.isbn, b.title, b.author, b.available]) return Response(buf.getvalue(), mimetype='text/csv', headers={'Content-Disposition': 'attachment; filename=books.csv'})

接口返回的是纯文本流,浏览器会把Content-Disposition解析为下载。前端直接window.open('/api/books/export')即可触发下载,不需要经过axios,因为拦截器会去读JSON的message字段,对文件流反而会误报。

5.2 超期天数计算与借阅状态标红

超期天数不要用前端时间戳相减,统一由后端传入当前时间计算,避免客户端时钟不准。工具函数放在models层:

def calc_overdue(record): if record.return_date: return 0 now = datetime.now() if now > record.due_date: return (now - record.due_date).days return 0

前端拿到overdue_days后,大于0时把表格行标红,el-table的行样式可以通过cell-class-name或row-class-name动态绑定。状态联动逻辑是:available大于0才能借,due_date超过当前时间显示“逾期”,return_date不为空显示“已还”。这一条数据链路把借阅记录表的价值完整展现出来。

5.3 答辩前对照代码自查的三个问题

评委最常问的三个问题:为什么available单独存一列而不是实时count?答案是因为演示时需要快速展示可借数量,冗余字段用业务逻辑保证一致性;为什么密码存哈希不是明文?因为数据库泄露时明文密码会连累用户其他平台账号,Werkzeug的哈希算法自带盐;跨域是怎么解决的?开发环境用vue.config.js的proxy代理,生产环境由Nginx反向代理,前后端代码不需要写CORS头。这三个问题在代码里都有对应位置,回答时指着文件讲比背概念更有说服力。到这一步,导出、超期和借阅状态已经联动起来了:列表页显示可借数量,借出后available减一,逾期后行标红,CSV导出的是同一份数据源。答辩时顺着这条链路讲,比逐行念代码清楚得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询