Python Django Vue MySQL前后端分离:图书馆管理系统毕设全攻略
发布时间:2026/9/28 1:46:39来源:尧图网络
简介一套基于PythonDjangoVueMySql的前后端分离图书馆管理系统毕业设计资源面向计算机相关专业学生、需要快速完成毕设原型或二次开发的开发者。系统采用B/S架构包含管理员端与用户端覆盖图书管理、借阅归还、罚金缴纳、留言板、收藏及前台首页展示等核心业务模块可支撑图书馆日常事务流程便于理解前后端分离开发与DjangoVue的协作方式。压缩包约65.38MB内含完整源代码、毕业论文、答辩PPT、任务书及视频操作教程可从环境配置、代码阅读到答辩演示全流程参考。当前已有148人学习目录结构清晰适合直接导入VSCode配合MySQL运行可作为课程设计或二次开发的基础能有效节省毕业设计开发与文档撰写时间。1. 毕业设计选图书馆管理系统前后端分离方案的投入产出比最高毕业设计分到“图书馆管理系统”这个题目第一反应别是“太简单”。业务虽然不复杂但要把 Python、Django、Vue、MySQL 四个技术栈揉成一个可演示的前后端分离系统踩坑点一点不少。这套方案里Django 负责出 REST 接口管数据Vue 负责页面和交互MySQL 存图书、读者、借阅记录前端通过 axios 调接口身份用 JWT 维护。它适合两类人想展示完整 Web 开发能力、又不想在业务建模上耗太久的应届生答辩时想从架构、数据库、权限、部署四个角度讲明白的实践型选手。我按从建库到上线避坑的顺序把这一整套拆给你看。2. 架构与数据库设计用四张表把图书借阅的边界画清楚动手写代码之前先把架构边界和数据模型定下来。网上搜“前后端分离项目实战”十个有八个是 Spring Boot Vue 或若依框架改造的但你的题目已经锁定 Python Django Vue MySQL就按这条栈走到底中途切栈会让答辩很难交代。先定边界再建表后面写接口和页面都是在给这套模型补肉。2.1 前后端分离的架构边界Vue 负责渲染Django 只出接口数据前后端分离的边界只有一句话前端拿数据后端出数据谁都不直接碰对方的地盘。Vue 跑在 5173 端口负责渲染表格、表单、路由跳转Django 跑在 8000 端口只提供 JSON 接口不返回任何 HTML 页面MySQL 只接受 Django 的连接前端永不直连数据库。身份认证是这套架构里最容易被做成 session 的地方我建议直接上 JWT。原因是 session 依赖浏览器 Cookie 和服务器端会话记录在分离架构里前端是独立应用Cookie 跨端口会被各种拦截JWT 把用户身份放进 token 里前端每次请求在 Authorization 头带上就行后端无状态校验。对应到毕业设计答辩JWT 比 session 好讲理由也站得住前端不存登录态后端不存会话扩展成小程序或 App 时不用改接口。2.2 图书、读者、借阅记录三张核心表的字段设计与约束取舍图书馆业务可以压缩成三张表图书表、读者表、借阅记录表。管理员不需要单独建业务表直接复用 Django 内置的 auth_user 就行权限用 Group 和 Permission 控制这属于 RBAC 的简化落地答辩时提一句“用 Django 内置权限模型实现角色控制”比自造一张管理员表专业得多。图书表的核心是库存。我一般把 total_count 和 stock 分开total_count 表示馆藏总册数借还永远不会变stock 表示当前可借数量每次借书减一、还书加一。很多毕设只存一个 total_count借书时靠查询借阅记录去反推库存代码能跑但一旦有逾期不还或手工改库的记录数据就对不上了。借阅记录表是整张数据库设计里最值得花时间的地方。due_date 用 DATE 类型不用 DATETIME因为还书按天算不按时刻算return_date 允许为空空就代表“未归还”status 字段存 0/1/2分别代表在借、已还、逾期未还。status 属于冗余字段因为理论上用 return_date 和 due_date 比较就能算出是否逾期但列表页和统计页都需要频繁按状态过滤冗余一个字段可以少写一堆条件判断。外键必须建。借阅记录表要同时指向 book 和 reader这样查“某人借了哪些书”和“某本书被谁借走”都是直接 join 或 ORM 关联查询。最后加一条约束建议category 字段不要单独建分类表毕设的图书量撑不起一张分类主数据表字符串字段足够。2.3 初始化 MySQL 数据库utf8mb4 建库与可直接抄的建表 SQL先用命令行或 Navicat 建库。字符集选 utf8mb4 而不是 utf8因为 utf8mb4 能存 emoji 和生僻字图书书名里偶尔会出现特殊字符选错字符集导入数据时会报 Incorrect string value。下面是完整的建表 SQL。CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE library; CREATE TABLE book ( id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) NOT NULL UNIQUE COMMENT ISBN同一本书唯一, title VARCHAR(128) NOT NULL COMMENT 书名, author VARCHAR(64) NOT NULL, publisher VARCHAR(128) NOT NULL DEFAULT , category VARCHAR(32) NOT NULL DEFAULT , total_count INT NOT NULL DEFAULT 5 COMMENT 馆藏总册数, stock INT NOT NULL DEFAULT 5 COMMENT 当前可借库存默认等于总册数, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category) ) ENGINEInnoDB COMMENT图书表; CREATE TABLE reader ( id INT AUTO_INCREMENT PRIMARY KEY, reader_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号/工号, name VARCHAR(64) NOT NULL, phone VARCHAR(20) NOT NULL DEFAULT , max_borrow INT NOT NULL DEFAULT 5 COMMENT 最大可借数量, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT读者表; CREATE TABLE borrow_record ( id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATE NOT NULL COMMENT 借出日期, due_date DATE NOT NULL COMMENT 应还日期, return_date DATE NULL COMMENT 实际归还日期为空表示未归还, status TINYINT NOT NULL DEFAULT 0 COMMENT 0在借 1已还 2逾期未还, CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(id), KEY idx_status (status) ) ENGINEInnoDB COMMENT借阅记录表; INSERT INTO book (isbn, title, author, publisher, category, total_count, stock) VALUES (9787121000012, Python编程从入门到实践, Eric Matthes, 人民邮电出版社, 编程, 5, 5), (9787121000029, Vue.js设计与实现, 霍春阳, 人民邮电出版社, 前端, 3, 3);这段 SQL 里每一条都有明确取舍。isbn 设 UNIQUE 是为了防止同一本书被重复建档因为同名书不同版次用 ISBN 区分stock 默认等于 total_count 是保证新书入库时“全量可借”borrow_record 的外键约束写在数据库层而不是只靠 Django 维护是为了删数据时不产生孤儿记录。建议把 isbn 里的示例值替换成你自己手边真实图书的编号答辩时被问到“你这个数据哪来的”能接得住。2.4 用 Navicat 与命令行核对初始数据答辩数据字典从哪来建完表先别急着写后端用图形工具核对一遍。Navicat 连接 MySQL 后看三张表的字段注释和索引重点确认外键有没有生效没有 Navicat 就用 MySQL Workbench两者效果一样。命令行核对的姿势是mysql -u root -p USE library; SHOW CREATE TABLE borrow_record; SELECT COUNT(*) FROM book;这里我一般会顺手导出一份数据字典。不是让你手写 Word 文档而是用 Navicat 的“导出表结构”功能生成一份含字段名、类型、注释、约束的表格答辩 PPT 里放一页就是最直观的数据库设计说明。这个动作成本五分钟答辩时能挡住“你这个表设计有什么考虑”的追问。核对无误后进入后端开发。3. Django 后端用 DRF 序列化器与 JWT 认证实现借书还书接口数据库定了后端就好写了。Django 后端我建议用 Django REST FrameworkDRF而不是原生 Django 的 View JsonResponse。原生写法也能出接口但序列化、分页、认证、路由要全手写DRF 把这些都封装好了对毕业设计来说省下的是实打实的调试时间。选 DRF 的理由答辩也讲得通它是 Django 生态里事实标准的 API 框架不是冷门玩具。3.1 创建 Django 项目与环境venv、startapp、settings 关键配置先在项目根目录建虚拟环境并激活依赖全部装进 venv不要装到全局 Python。这一步被很多人跳过直接进全局环境装包装到一半发现系统 Python 版本不对再后悔就麻烦了。mkdir library_project cd library_project python -m venv venv source venv/bin/activate pip install django djangorestframework django-cors-headers djangorestframework-simplejwt mysqlclient django-admin startproject library_backend . python manage.py startapp books python manage.py startapp borrows python manage.py startapp readers虚拟环境激活后终端提示符前面会出现 (venv)看到这个再装依赖否则容易装错解释器。mysqlclient 在 Windows 上偶尔装不上装不上就换 PyMySQL在项目__init__.py里写import pymysql; pymysql.install_as_MySQLdb()效果一样。然后把 settings.py 里几处关键配置改掉。INSTALLED_APPS 要加 DRF、corsheaders 和你新建的 app数据库换成 MySQL时区用 Asia/Shanghai再补上 DRF 的 JWT 认证配置。# settings.py 关键片段 INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, corsheaders, books, borrows, readers, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, django.middleware.security.SecurityMiddleware, # 其余默认 middleware 保持不动 ] DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: library, USER: root, PASSWORD: 改成你的密码, HOST: 127.0.0.1, PORT: 3306, CONN_MAX_AGE: 600, } } CORS_ALLOWED_ORIGINS [ http://localhost:5173, ] REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], } SIMPLE_JWT { ACCESS_TOKEN_LIFETIME: timedelta(hours2), }注意几个参数的含义。DATABASES 里 HOST 写 127.0.0.1 而不是 localhost是为了强制走 TCP 连接而不是 Unix socket配合第 5 章的 MySQL 连接坑CONN_MAX_AGE 设为 600 表示连接复用 10 分钟避免每次请求都新建连接接近连接池的效果。SIMPLE_JWT 的 ACCESS_TOKEN_LIFETIME 给 2 小时答辩演示时不会中途过期2 小时也足够说明你考虑了安全性而不是永远不失效。3.2 从 Model 到 Serializer借阅记录里带书名和读者名的写法Django 的 Model 字段要跟第 2 章的 SQL 对得上。我建议这一章里的字段由 Django 的 migrate 来自动建表而不是用第 2 章手写的 SQL 建表。因为手写 SQL 建表后要让 Django 识别得设置 managed False 或手工保证字段一一对应很折腾。正确姿势是第 2 章的 SQL 用来理解结构开发时把表删掉用 Django 的 migrate 重新生成。# books/models.py from django.db import models class Book(models.Model): isbn models.CharField(ISBN, max_length20, uniqueTrue) title models.CharField(书名, max_length128) author models.CharField(作者, max_length64) publisher models.CharField(出版社, max_length128, blankTrue, default) category models.CharField(分类, max_length32, blankTrue, default) total_count models.PositiveIntegerField(馆藏总数, default5) stock models.PositiveIntegerField(当前库存, default5) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: db_table book ordering [-id] def __str__(self): return f{self.title} ({self.stock}/{self.total_count})# borrows/models.py from django.db import models from books.models import Book from readers.models import Reader class BorrowRecord(models.Model): STATUS_CHOICES ( (0, 在借), (1, 已还), (2, 逾期未还), ) book models.ForeignKey(Book, on_deletemodels.CASCADE, verbose_name图书) reader models.ForeignKey(Reader, on_deletemodels.CASCADE, verbose_name读者) borrow_date models.DateField(借出日期, auto_now_addTrue) due_date models.DateField(应还日期) return_date models.DateField(归还日期, nullTrue, blankTrue) status models.SmallIntegerField(状态, choicesSTATUS_CHOICES, default0) class Meta: db_table borrow_record ordering [-id]Serializer 是 DRF 里把数据变成 JSON 的关键层。借阅列表页需要显示书名和读者名但前端拿 book_id 再调接口查询效率低直接在序列化器里加两个只读字段让后端联表查好一次性返回。# borrows/serializers.py from rest_framework import serializers from .models import BorrowRecord class BorrowRecordSerializer(serializers.ModelSerializer): book_title serializers.CharField(sourcebook.title, read_onlyTrue) reader_name serializers.CharField(sourcereader.name, read_onlyTrue) class Meta: model BorrowRecord fields [ id, book_id, book_title, reader_id, reader_name, borrow_date, due_date, return_date, status, ]source 参数告诉 DRF 从关联对象的哪个字段取值read_onlyTrue 表示这个字段只用来输出不参与写入校验。这样前端表格里直接用 book_title 和 reader_name 渲染少写两个接口。3.3 ViewSet 与自定义 action借书扣库存与还书算逾期的完整逻辑借书和还书是这套系统的核心动作不能用 DRF 默认的 create 和 update因为默认写法不会动库存。我建议用 GenericViewSet 加自定义 action把“借书”和“还书”设计成两个明确的接口POST /api/borrows/borrow/ 和 POST /api/borrows/{id}/return_book/。路由语义清晰答辩时也好逐个讲逻辑。# borrows/views.py from datetime import timedelta from django.db import transaction from django.utils import timezone from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.response import Response from books.models import Book from .models import BorrowRecord from .serializers import BorrowRecordSerializer class BorrowViewSet(viewsets.GenericViewSet): queryset BorrowRecord.objects.select_related(book, reader).all() serializer_class BorrowRecordSerializer action(detailFalse, methods[post]) def borrow(self, request): book_id request.data.get(book_id) reader_id request.data.get(reader_id) if not book_id or not reader_id: return Response({detail: 缺少book_id或reader_id}, statusstatus.HTTP_400_BAD_REQUEST) with transaction.atomic(): book Book.objects.select_for_update().get(pkbook_id) if book.stock 0: return Response({detail: 库存不足}, statusstatus.HTTP_400_BAD_REQUEST) due_date timezone.localdate() timedelta(days30) record BorrowRecord.objects.create( book_idbook_id, reader_idreader_id, due_datedue_date, ) book.stock - 1 book.save() return Response(BorrowRecordSerializer(record).data, statusstatus.HTTP_201_CREATED) action(detailTrue, methods[post]) def return_book(self, request, pkNone): record self.get_object() if record.status ! 0: return Response({detail: 这条记录已处理}, statusstatus.HTTP_400_BAD_REQUEST) record.return_date timezone.localdate() record.status 1 if record.return_date record.due_date else 2 record.save() book record.book book.stock 1 book.save(update_fields[stock]) return Response(BorrowRecordSerializer(record).data)这段代码有三个关键点。一是借书时用 transaction.atomic() 包住查询扣减和创建记录再用 select_for_update() 锁住图书行防止两个并发请求同时读到 stock1 然后各自扣减成 -1二是还书时 status 的判断规则是 return_date due_date 为正常归还否则标记为逾期未还三是还书时给 book.stock 加回一本但注意记录本身的 status 已经变成 1 或 2不会重复加。这里如果把 return_date 和 due_date 都当成带时间的 datetime 比较当天归还就会被误判逾期所以第 2 章在第 5 章时区部分会重点讲为什么用 DATE 类型。再把路由接上。图书列表直接用 ModelViewSet借阅用上面自定义的 BorrowViewSet。# library_backend/urls.py from django.contrib import admin from django.urls import path, include from rest_framework.routers import DefaultRouter from rest_framework_simplejwt.views import TokenObtainPairView from books.views import BookViewSet from borrows.views import BorrowViewSet router DefaultRouter() router.register(books, BookViewSet) router.register(borrows, BorrowViewSet) urlpatterns [ path(admin/, admin.site.urls), path(api/, include(router.urls)), path(api/auth/login/, TokenObtainPairView.as_view(), namelogin), ]TokenObtainPairView 是 simplejwt 提供的登录接口接受用户名和密码返回 access 和 refresh 两个 token。前端拿 access 存起来每次请求带上即可。调试阶段如果嫌命令行 curl 麻烦直接在 Django admin 里建一个超级用户再用这个登录接口换 token。4. Vue 前端用 axios 拦截器与路由参数把借阅页面串起来后端接口齐了前端的工作就是把这些 JSON 数据变成能看、能点的页面。Vue 3 现在的主流写法是用 Vite 初始化工程加script setup组合式 API比 vue-cli 和选项式 API 都更接近当前团队的开发习惯。要注意的是 Vue 本身只负责渲染所有数据都得通过 axios 向后端要不要在组件里直接拼 SQL 或连数据库那是前后端分离的大忌。4.1 用 Vite 创建前端工程node 版本与依赖安装的注意点Vite 要求 Node.js 版本在 16 以上安装前先node -v看一下版本。如果版本过低Vite 启动会直接报错官方文档也不再兼容旧版本。创建项目时选 Vue 模板然后安装运行时依赖。npm create vitelatest library_frontend -- --template vue cd library_frontend npm install npm install vue-router4 axios element-plus npm run devnpm run dev启动后默认端口是 5173后端 CORS 配置里已经放行过这个地址。Element Plus 是 UI 组件库表格、表单、分页、消息提示都直接用它自己手写样式做出来的效果在答辩时很难看。依赖装好后把src/main.js里挂上 router 和 Element Plus就能开始写页面。4.2 路由与跨页传参/book/:id 详情页与列表跳转的实现仓库管理系统的页面不需要很复杂列表页、详情页、登录页三个就够。路由配置里最需要注意的是:id这种动态参数列表页跳详情页时把图书 id 塞进 URL详情页再读出来。// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(../views/BookList.vue) }, { path: /book/:id, component: () import(../views/BookDetail.vue) }, { path: /login, component: () import(../views/LoginView.vue) }, ] const router createRouter({ history: createWebHistory(), routes, }) export default router列表页跳详情页时用router.push带上参数详情页用useRoute()读取。这是 Vue Router 最基础的传参方式params 刷新页面不会丢因为参数在 URL 里。!-- BookList.vue 里的跳转按钮 -- el-button typeprimary click$router.push(/book/${row.id}) 详情 /el-button!-- BookDetail.vue 里读取参数 -- script setup import { useRoute } from vue-router const route useRoute() console.log(route.params.id) /scriptcreateWebHistory 是 HTML5 History 模式URL 干净好看但部署到 nginx 后刷新会 404需要配置 try_files 回退到 index.html这个坑在第 5 章专门讲。嫌麻烦可以直接用 createWebHashHistoryURL 带 # 号但胜在刷新永远正常。我一般建议毕设用 hash 模式保平安答辩演示时刷新页面是高频动作。4.3 axios 实例与拦截器统一携带 token 并在 401 时跳回登录前端所有接口请求都应该走同一个 axios 实例而不是每个组件里各自import axios from axios。统一实例的目的有两个一是 baseURL 只配一处后端地址换了只改一个文件二是拦截器可以在请求发出前统一加 token在响应返回后统一处理 401。// src/utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: http://localhost:8000/api, timeout: 5000, }) request.interceptors.request.use( (config) { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, (error) Promise.reject(error) ) request.interceptors.response.use( (response) response.data, (error) { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } else { ElMessage.error(error.response?.data?.detail || 请求失败) } return Promise.reject(error) } ) export default requestAuthorization 头的格式Bearer ${token}必须和后端 simplejwt 的校验方式完全一致大小写都不能错。拦截器里处理 401 的意思是token 过期或无效时自动清掉本地 token 并跳回登录页这个逻辑保证了演示时拖很久也不会卡在报错页面上。登录页拿到 token 后的处理也走这个实例async function handleLogin() { const data await request.post(/auth/login/, { username: username.value, password: password.value, }) localStorage.setItem(token, data.access) window.location.href / }这里用的是 data.access因为 simplejwt 登录接口返回的字段名就叫 access。存好之后刷新页面拦截器会自动带上 token图书列表、借阅操作就都能调通了。组件里调用接口时只关心业务数据不用每个请求都写一遍 token 逻辑。5. 部署与避坑CORS 跨域、error 2002、时区逾期等五个高频问题这一章是我做这类项目时翻车最多的五个位置。前四个是开发期就能踩到的最后一个是上线部署期的问题。每一条都按“现象 → 原因 → 解决”写照着排查就行。这些坑单独看都不难但它们都长着一副“明明是代码问题”的脸实际却是环境或配置问题最容易把人带偏。5.1 CORS 跨域被拦截django-cors-headers 配置与现象确认现象前端页面上跨域报错信息里出现 CORS policy浏览器控制台一片红接口在 Postman 里却完全正常。原因浏览器安全机制阻止了跨端口请求。Vue 页面跑在 5173Django 接口在 8000端口不同就是跨域。Postman 没有浏览器同源策略所以表现完全不一样。解决确认 settings.py 里装了 corsheaders、中间件里加了 CorsMiddleware并且 CORS_ALLOWED_ORIGINS 配置正确。这里最容易漏的是中间件的位置——CorsMiddleware 要放在 CommonMiddleware 之前否则部分请求仍然会被拦截。改完配置重启 Django不要只刷新页面。5.2 MySQL 连不上error 2002 (HY000) through socket 与连接池参数现象Django 启动或跑迁移时报django.db.utils.OperationalError: (2002, Cant connect to local MySQL server through socket /tmp/mysql.sock)。原因MySQL 服务没启动或者 Django 配置里用了 localhost 导致驱动尝试走 Unix socket 而不是 TCP。macOS 上经常是 MySQL 装了但没跑起来Linux 上则可能是 mysqld 服务挂了。解决先确认服务状态macOS 用brew services start mysqlLinux 用systemctl start mysqld。然后把 Django 数据库配置里的 HOST 从 localhost 改成 127.0.0.1强制走 TCP。这两步做完基本能连上如果还有问题再用mysqladmin ping验证服务是否真的在监听。另外连接池方面Django 默认每次请求新建数据库连接性能不好但毕设不会暴露。如果你想在答辩时展示一点性能意识把 CONN_MAX_AGE 设成 600 即可含义是连接复用 10 分钟这就够了。5.3 时区不一致逾期天数算错的根源与 TIME_ZONE 统一方案现象读者当天借书当天还还书后状态却变成“逾期未还”逾期天数也总是差一天。原因Django 默认 USE_TZTrue时间按 UTC 存储MySQL 里存的也是 UTC。timezone.localdate() 取到的是本地日期而数据库里的 due_date 是 UTC 日期两者在凌晨时段会差一天。解决在 settings.py 里设置TIME_ZONE Asia/Shanghai和USE_TZ False。关闭时区支持后Django 存进 MySQL 的时间就是本地时间due_date 和 return_date 比较不再有时区偏移。毕业设计不涉及多时区用户关闭时区是最省心的选择答辩时说明“系统面向国内单一图书馆场景不考虑跨时区”也站得住。5.4 并发借书超卖select_for_update 与事务兜底现象两个人同时借同一本只剩库存 1 的书两个请求都显示借书成功最后库存变成 -1。原因两个请求同时读到 stock1各自在内存里减一再分别写回。先写回的覆盖后写回的库存被扣成负数却没有任何报错。解决借书逻辑里用transaction.atomic()包住事务并用select_for_update()锁住那本图书的行记录。锁的含义是第一个事务提交前第二个事务的查询会阻塞等待。这样第二个请求读到的是已经减过库存的结果stock 0 的判断就能拦住超借。这段代码在第 3.3 节的 borrow 接口里已经写过答辩时这段是加分点。5.5 前端刷新 404 与 Django 静态文件显示不了两处部署配置现象开发环境一切正常部署到服务器后前端页面刷新任意子路由变成 404同时 Django 后台样式丢失或页面上 img 标签里的静态图片显示不出来。原因两个问题都是部署配置没跟上。前端用的是 HTML5 History 模式nginx 收到 /book/1 这种路径时找不到对应文件直接返回 404Django 的 DEBUG 一旦设为 False静态文件不再由开发服务器托管需要专门配静态文件服务。解决前端交给 nginx 时在 location / 里加 try_files 回退到 index.html让前端路由接管一切未知路径后端用 whitenoise 托管静态文件配置好之后执行 collectstatic 收集一次。server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /path/to/library_backend/staticfiles/; } location / { root /path/to/library_frontend/dist; try_files $uri $uri/ /index.html; } }部署 Django 用 Gunicorn 而不是python manage.py runserverrunserver 只适合开发调试。Java 项目那套丢进 Tomcat 的思路在这里不通用Django 的部署形态是 WSGI 应用加反向代理别套错经验。nginx 配置里最关键的是最后一行 try_files没有它前端刷新必 404。6. 答辩前自查用一条完整借阅链路验收系统再造一屏演示数据答辩前一天别急着加新功能先把数据库重置一遍导入干净数据然后从头走一遍完整链路登录 → 图书列表 → 详情 → 借书 → 查看借阅列表 → 还书 → 确认库存回填。这条链路能一次性走通系统就及格了。我见过太多人在展示库存扣减时卡住原因不是代码错了而是之前调试时手工改过数据库库存早就不对了。演示数据要提前准备足。三本书的数据在投影仪上撑不起一屏造数 SQL 可以帮你快速铺满页面INSERT INTO borrow_record (book_id, reader_id, borrow_date, due_date, return_date, status) SELECT 1, 1, DATE_SUB(CURDATE(), INTERVAL n DAY), DATE_ADD(CURDATE(), INTERVAL 30 - n DAY), IF(n % 3 0, DATE_ADD(CURDATE(), INTERVAL 10 DAY), NULL), IF(n % 3 0, 1, IF(n 20, 2, 0)) FROM ( SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10 ) AS nums;这是递归造数的一种稳妥写法先生成 1 到 10 的数字序列再按规律生成借还日期和状态一次插入 10 条记录。想造更多就扩展 UNION 的数字。这套数据让列表页有内容可看图表页有状态可统计答辩时不至于对着空表讲。答辩时被问到权限控制直接讲 Django 内置的 Group 和 Permission不需要展示代码说清楚“管理员和读者走不同的权限组”就够了。被问到前后端分离的优势标准答案是三个前端独立迭代不影响后端、接口可复用给其他客户端、部署上静态资源和 API 可以分开扛压力。我现在的习惯是每做完一个项目就把运行命令和初始化 SQL 写进 README答辩前一晚照着 README 从零跑一遍。这套图书馆管理系统完整建库、后端启动、前端启动、走通借还链路全程应该控制在十分钟以内。希望你答辩那天也这么顺如果这篇笔记能帮你避开那几个坑就值得了希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网