简介:这是一套面向计算机及相关专业毕业设计的图书馆管理系统完整项目,基于Python+Django+Vue+MySQL开发,采用B/S架构,覆盖管理员端、用户端与前台展示页面。功能包含图书分类、图书信息、借阅归还、罚金缴纳、留言板、个人中心、我的收藏与系统管理等模块,适合需要毕业设计源码、论文和答辩支撑材料的本科生作为参考与二次开发基础。资源以zip压缩包形式交付,整体约65.38MB;上游未提供文件总数和类型清单,暂不展开。包内通常整合源码、数据库脚本、毕业论文、答辩PPT及演示视频,可据此快速搭建环境并理解图书借阅全流程,节省从零搭建和撰写文档的时间。已有320人参与学习。整体内容偏重实践落地,既有前后端代码与数据表结构,也有论文框架和操作演示,能帮助读者梳理系统设计思路,完善毕业设计文档与答辩准备。
1. 如果图书馆管理系统毕业设计让你卡在联调这一步:先把项目跑起来再说
图书馆管理系统(基于Python+Django+Vue+MySql)是一套非常标准的Web全栈毕业设计项目,后端用Django做业务逻辑和API,前端用Vue 3做页面交互,MySQL 8存数据。整套资源本身自带源码、数据库脚本、毕业论文、答辩PPT和视频演示,大概覆盖了从开题到答辩的完整链路。这套东西最适合两类人:一是课程设计时间紧、需要在两周内交付能演示的完整系统,二是打算二次开发但不想从零写增删改查的Django入门者。我拆完这套代码之后最大的感受是——技术栈不炫技,但每一层都踩在毕业设计的评分点上,能演示借书、还书、逾期统计、读者管理、图书检索这套完整闭环。别急着改代码,先把MySQL跑起来、把数据库脚本导进去、用Django自带的开发服务器把后端启动,再用npm把Vue开发环境拉起来,整套系统能在半小时内跑通。
2. 整体架构与模块划分:为什么这套系统选Django + Vue而不是其他组合
2.1 技术栈选型:Django + Vue 各自的边界在哪里
毕业设计选技术栈,最怕的不是用错,而是用了自己讲不清楚的东西。这套系统选Django做后端,核心原因是Django自带Admin后台、ORM和认证系统,这三个能力刚好对应图书馆管理系统的三个硬需求:图书和读者的增删改查、数据库表之间的关联查询、管理员和普通读者的权限区分。如果用Flask,Admin后台要自己写;如果用Node.js的Express,ORM要另外选型。而Django把这块全部内置了,写代码的工作量至少省掉三分之一。
Vue这边负责的是页面交互。图书馆管理系统虽然有后端渲染的能力,但借书还书这类操作需要即时反馈,比如输入读者编号后立刻显示读者信息、选择图书后实时显示馆藏数量,这类体验用Vue的响应式数据绑定做起来最顺手。拿这套系统的前端来说,图书检索页面用到了一个实时过滤功能:读者在搜索框输入书名关键字,列表立刻过滤匹配结果,这个交互如果用Django的模板渲染来做,每次输入都要刷新页面,答辩时演示效果会差不少。
前端和后端的通信方式是HTTP + JSON,这也是最常见的做法。后端把业务逻辑封装成符合RESTful风格的API接口,前端的axios负责发起请求并把数据填充到页面。整个项目虽然分了三个部分(Django后端、Vue前端、MySQL数据库),但实际跑起来是两条线:一条是浏览器请求Vue开发服务器,另一条是Vue发请求到Django,Django再查MySQL。
2.2 数据库表设计:馆藏、读者、借阅记录是核心三张表
图书馆管理系统的数据库设计,核心表就三张:图书表(books)、读者表(readers)、借阅记录表(borrow_records)。其他的表都是围绕这三张做辅助。
我先说图书表,字段设计上要区分“书目信息”和“馆藏信息”。很多初写毕业设计的人会把这两件事混在一张表里,导致的情况是:书架上有5本《Python编程》,数据库里却只有一条记录,还书的时候不知道还的是哪一本。这套系统的做法是book表存统一的图书信息,用isbn做唯一标识;另外再单独维护库存数量,借书时库存减一,还书时库存加一。
借阅记录表是整个系统的核心,也是答辩时评委最可能追问的地方。这张表至少要有这么几个字段:借书时间(borrow_time)、应还时间(due_time)、实际归还时间(return_time)、借阅状态(status)。状态字段建议用整数表示而不是字符串,0表示借出、1表示已还、2表示逾期未还,这样在做统计的时候写SQL条件非常直接,不需要字符串模糊匹配。
下面这张表是数据库脚本里主要表的字段清单,实际实现的MySQL脚本里都能找到对应定义。
| 表名 | 主要字段 | 说明 |
|---|---|---|
| book | id, isbn, title, author, publisher, category, total_stock, current_stock, location | 图书书目信息,isbn唯一 |
| reader | id, reader_no, name, gender, phone, email, max_borrow, status | 读者信息,reader_no用于借书识别 |
| borrow_record | id, book_id, reader_id, borrow_time, due_time, return_time, status, renew_count | 借阅记录,status控制流程状态 |
| book_category | id, category_name, sort_order | 图书分类,与book表category字段关联 |
| user | id, username, password_hash, role, name | 登录用户信息,区分管理员与读者 |
数据库脚本导入之后,包里自带了一些演示数据,大概包括几十本图书和十几个读者账号,这个数据量对答辩演示来说足够了——正好能展示分页和检索功能,又不会因为数据太多导致页面加载慢。如果用这套系统做二次开发,最好保留这些初始数据,不要清空重来,省得后面演示时还要手工往数据库里塞数据。
3. Django后端:借书、还书、逾期判断的业务逻辑实现
3.1 项目目录结构与App划分:单App还是多App
Django项目的目录结构,直接决定了后续维护的时候想不想砸电脑。这套系统的后端拆成了两个App:books负责图书和借阅管理,users负责读者、登录、权限。拆两个App而不是把所有模型丢在一个App里,好处有两点:第一,models.py、views.py文件不会膨胀到几千行;第二,答辩的时候可以单独讲“我是按业务模块划分的,books处理图书业务,users处理用户业务”,这种组织方式本身就是一个加分点。
这种时候最忌讳把所有代码堆在一个app里,一个views.py写了2000行,看似工作量很大,实际上评审老师随便翻一下就觉得没有工程组织能力。目录结构大致是这样:books/models.py放图书和借阅记录的模型,books/views.py放借还书的视图函数,books/serializers.py放API序列化器,users那边管理读者和登录。
3.2 借书接口实现:事务回滚是防止库存负数的关键
借书这个功能是整个系统的命脉,代码实现上要保证两件事同时成功:把借阅记录插入borrow_records表,把books表的库存current_stock减一。如果第一条执行成功、第二条失败,就会出现“数据库里有一条借阅记录,但书架上还有这本书”的脏数据。
下面是借书视图的代码核心逻辑,用的是Django的transaction.atomic装饰器来保证事务一致性:
from django.db import transaction from rest_framework.decorators import api_view from rest_framework.response import Response from books.models import Book, BorrowRecord from books.serializers import BorrowSerializer from django.utils import timezone from datetime import timedelta @api_view(['POST']) @transaction.atomic def borrow_book(request): # 前端传参格式: {"book_id": 1, "reader_no": "RD2024001"} book_id = request.data.get('book_id') reader_no = request.data.get('reader_no') # step1: 检查读者是否存在且状态正常 reader = Reader.objects.filter(reader_no=reader_no, status=1).first() if not reader: return Response({'code': 400, 'msg': '读者不存在或已被禁用'}) # step2: 检查图书库存是否大于0 book = Book.objects.select_for_update().get(id=book_id) if book.current_stock <= 0: return Response({'code': 400, 'msg': '库存不足'}) # step3: 扣减库存,创建借阅记录 book.current_stock -= 1 book.save() # 借期默认30天,计算应还时间 due_time = timezone.now() + timedelta(days=30) BorrowRecord.objects.create( book=book, reader=reader, borrow_time=timezone.now(), due_time=due_time, status=0, # 0表示借出 ) return Response({'code': 200, 'msg': '借书成功', 'due_time': due_time.strftime('%Y-%m-%d %H:%M:%S')})这里有两个参数细节要说明。第一个是select_for_update(),这个方法会对book那条记录加行级锁,防止了两个并发请求同时读到库存为1然后都通过校验,结果把库存扣成负数。毕业设计虽然不需要应付高并发,但答辩时被问到“多人同时借同一本书怎么办”时,这行代码就是有力的回答。第二个是timedelta(days=30)直接算应还时间,而不是把到期时间硬编码,这样以后如果要改成14天借期,只改这里一处就够了。
3.3 还书与逾期判断:状态字段的演进规则
还书的逻辑比借书多了一个分支判断——这本书是按时归还还是逾期归还。逾期判断不能靠管理员肉眼对比日期,必须由后端自动计算。代码层面是这样做的:
@api_view(['POST']) def return_book(request): record_id = request.data.get('record_id') record = BorrowRecord.objects.select_related('book', 'reader').get(id=record_id) # 先判断当前时间是否晚于应还时间 now = timezone.now() if now > record.due_time: record.status = 2 # 逾期归还 else: record.status = 1 # 正常归还 record.return_time = now record.save() # 归还后库存加一 book = record.book book.current_stock += 1 book.save() # 逾期天数,用于前端显示 overdue_days = (now - record.due_time).days if record.status == 2 else 0 return Response({'code': 200, 'overdue_days': overdue_days})状态字段在这里的设计值得说一句:0是借出,1是已还,2是逾期。很多人会想用is_overdue这种布尔字段来表示是否逾期,但这样遇到一个场景就露馅了——一本书逾期归还之后,布尔字段倒底是False还是True?作为历史记录,它应该保留“曾经逾期”这个事实。用status整数状态机,借阅记录从借出到归还的过程中,状态只会单向变化,不会产生歧义。
逾期天数这里可以看到,因为使用了(now - record.due_time).days,而日期对象相减会返回一个timedelta对象,取.days就是整数天数。实际开发时要注意,这里如果时区没配置正确,得出的天数可能会相差1,这是我后面第5章联调避坑里要展开详细说的问题。
3.4 API路由与视图集:ModelViewSet替你做一半的CRUD
图书管理、读者管理这两块的增删改查接口,代码量其实很少。原因是用到了Django REST Framework自带的ModelViewSet。这套系统里图书的视图集就是这么写的:
from rest_framework.viewsets import ModelViewSet from books.models import Book from books.serializers import BookSerializer class BookViewSet(ModelViewSet): queryset = Book.objects.all().order_by('-id') serializer_class = BookSerializer pagination_class = None # 默认不分页,分页逻辑在search接口中实现只要这个类,图书的新增、修改、删除、列表查询就全齐了。路由注册也只需要两行:
from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register(r'books', BookViewSet) router.register(r'readers', ReaderViewSet) urlpatterns += router.urls默认情况下,ModelViewSet会生成这些接口:POST到/api/books/新增,GET到/api/books/列表,GET到/api/books/{id}/详情,PUT和DELETE到/api/books/{id}/更新和删除。这套RESTful风格的接口设计,答辩时是能直接讲的:我是用REST Framework的ModelViewSet统一封装了资源的增删改查,所以接口风格一致、开发效率快。
图书检索这里是可以稍微展示点技术细节的地方。图书馆系统的搜索需求不只是书名模糊查询,还要支持“按类检索”和“按作者检索”,用DRF自带的filter_backends实现起来很干净:
from rest_framework import filters from django_filters.rest_framework import DjangoFilterBackend class BookViewSet(ModelViewSet): queryset = Book.objects.all().order_by('-id') serializer_class = BookSerializer filter_backends = [DjangoFilterBackend, filters.SearchFilter] filterset_fields = ['category', 'status'] search_fields = ['title', 'author', 'isbn', 'publisher']前端只要请求GET /api/books/?search=python&category=1,后端就会自动执行模糊搜索并叠加分类过滤,不需要自己写SQL拼字符串。filterset_fields和search_fields都在后端定义了可过滤字段清单,前端传超出范围的参数也不会生效,这块在答辩演示时可以现场改URL给评委看效果变化。
4. Vue前端:从路由配置到Axios联调
4.1 前端工程结构与Vue Router路由设计
后端接口准备就绪后,前端的工作量集中在页面和联调。这套系统的Vue部分用的是Vue 3 + Vite,目录结构是典型的Vite脚手架布局:src/views存放页面组件,src/router存放路由配置,src/api统一封装请求方法。重点看路由的设计思路——路由表分两层是有意安排的。
const routes = [ { path: '/', component: () => import('../layouts/MainLayout.vue'), children: [ { path: 'book', component: () => import('../views/BookList.vue'), name: '图书列表' }, { path: 'borrow', component: () => import('../views/BorrowReturn.vue'), name: '借还管理' }, { path: 'reader', component: () => import('../views/ReaderList.vue'), name: '读者管理' }, { path: 'stats/overdue', component: () => import('../views/OverdueStats.vue'), name: '逾期统计' } ] }, { path: '/login', component: () => import('../views/Login.vue') } ]外层套一个MainLayout.vue的好处是,左侧导航栏和顶部状态栏只需要写一次,所有子页面只负责内容区域。这种布局模式在Vue后台管理项目里很常见,答辩时如果被问到“重复的导航栏是怎么处理的”,直接说这是嵌套路由的父级组件效果,页面响应时子路由出口会自动切换内容。
动态导入需要说明一下作用:() => import(...)会让指定的页面组件在浏览器真正路由到该路径时才加载。好处是首屏只加载登录页和主框架的代码,图书列表页的几百行Vue代码不会被一次性下载,对开发环境的影响不大,但对构建之后的生产包体积有明显优化。
这套前端路由的设计还有一层业务考虑——把“图书列表”和“借还管理”拆成两个独立路由,因为这两个页面的交互模式完全不同:图书列表是纯查询展示,借还管理是输入信息和提交。如果硬塞进一个页面,事件处理的逻辑会牵扯不清。
4.2 Axios请求封装与拦截器:统一的BaseURL与Token鉴权
前端和后端联调时,最大的问题不是接口报错,而是几十个组件里每写一次请求就复制粘贴一份完整的axios配置。这套系统的src/api/request.js中做了统一封装,拦截器同时处理了请求头和错误状态。
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: 'http://127.0.0.1:8000/api', timeout: 8000 }) // 请求拦截器:自动带上token request.interceptors.request.use(config => { const token = localStorage.getItem('library_admin_token') if (token) { config.headers.Authorization = `JWT ${token}` } return config }) // 响应拦截器:统一处理业务错误码 request.interceptors.response.use( response => { const res = response.data // 后端业务code约定: 200为成功, 其他为失败 if (res.code && res.code !== 200) { ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { ElMessage.error('登录已过期,请重新登录') router.push('/login') } else { ElMessage.error('网络异常,请检查后端服务是否已启动') } return Promise.reject(error) } ) export default request关于baseURL这个参数,写的是http://127.0.0.1:8000/api,开发时Django后端就运行在8000端口,前端Vite开发服务器默认5173端口,这样两个服务是跨域的。跨域问题的解决不是在前端改什么配置,而是在后端Django那边配置CORS_ALLOWED_ORIGINS,这个我在第5章避坑部分会展开详细说。
JWT ${token}这种写法使用的是Django REST Framework的JWT认证标准。JWT流程可以这样理解:登录成功后后端返回一个字符串token,前端保存在localStorage本地缓存中,后续每次请求都在HTTP头里带上。后端通过校验这个token判断当前访问者是哪个用户、是否有权限执行当前操作。
拦截器里同时响应了两个层面的错误:请求抛出的网络错误和HTTP状态码错误。401是后端明确返回的“未认证”,说明token缺失或者过期,那就自动踢回登录页;网络其他异常则提示检查后端有没有启动。这两条兜底逻辑能让联调时的报错快速定位到问题层面,不至于前端黑屏后还要自己开浏览器开发者工具抓XHR请求一条条看。
4.3 图书列表中一个值得复用的表格操作模式
图书列表页是整个前端里最典型的CRUD组件——它把Element Plus的表格、分页、搜索框的组合用得非常常规,但这种常规反而是毕业设计最需要的:代码可读性高、每行逻辑都能解释、删改起来不费劲。
<template> <el-table :data="bookList" v-loading="loading"> <el-table-column prop="title" label="书名" width="200"></el-table-column> <el-table-column prop="author" label="作者" width="150"></el-table-column> <el-table-column prop="current_stock" label="可借数" width="100"></el-table-column> <el-table-column label="操作" width="150"> <template #default="scope"> <el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table> <el-pagination @current-change="loadBookList" :current-page="page" :page-size="pageSize" :total="total" layout="total, prev, pager, next"> </el-pagination> </template> <script setup> import { ref, onMounted } from 'vue' import { ElMessageBox, ElMessage } from 'element-plus' import request from '../api/request' const bookList = ref([]) const page = ref(1) const pageSize = ref(10) const total = ref(0) const loadBookList = async () => { const res = await request.get('/books/', { params: { page: page.value, page_size: pageSize.value } }) bookList.value = res.data.rows total.value = res.data.total } const handleDelete = async (row) => { // 先弹出确认框,防止误删 await ElMessageBox.confirm(`确定删除《${row.title}》吗?删除后不可恢复`, '警告', { type: 'warning' }) await request.delete(`/books/${row.id}/`) ElMessage.success('删除成功') loadBookList() } onMounted(loadBookList) </script>loadBookList函数设计时带了一个async/await,原因是删除操作和弹窗提示都是异步任务。delete接口调用成功后会重新执行loadBookList刷新列表,这样页面上的数据始终和后端数据库保持同步。这几行代码值得照抄到自己的项目里——图书删除这种危险操作前一定要加确认弹窗,答辩现场如果误删了一条数据,页面会留下非常明显的翻车现场。
分页参数这块,后端Django使用page和page_size两个参数来接收,前端绑定到分页组件的el-pagination事件上。每次切换页码时,组件触发事件重新请求数据,表格自动更新。读者管理页基本就是这个页面的复制逻辑,只改了字段和接口路径。
5. 联调避坑:Django 4.2 + Vue 3 + MySQL 8 的五个真实坑
5.1 坑一:MySQL 8 报错 caching_sha2_password 认证失败
用MySQL Workbench或Navicat连接数据库时能连上,但Django的python manage.py migrate或者系统启动后首次请求数据库时直接报错,报错内容一般是Authentication plugin 'caching_sha2_password' cannot be loaded。MySQL 8.0默认的认证插件改成了caching_sha2_password,而某些Python驱动(尤其是通过系统包管理器装的mysqlclient)版本过旧,还不认识这个插件。
解决方式有两种,推荐第一种:在MySQL中创建系统专用账号时,手动指定使用旧的mysql_native_password认证插件。
CREATE USER 'library_user'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; GRANT ALL PRIVILEGES ON library_db.* TO 'library_user'@'localhost'; FLUSH PRIVILEGES;# settings.py 中对应的数据库配置 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'library_db', 'USER': 'library_user', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'} } }charset这里强烈建议选utf8mb4而不是utf8。utf8在MySQL里其实只支持最多三个字节的字符,像生僻字和多数符号都会被截断或显示成乱码,utf8mb4才是完整版UTF-8。另外HOST写成127.0.0.1不要写localhost,某些Windows环境下Python的MySQL驱动解析localhost会去走IPv6导致连接超时,这类偶发问题非常折腾人。
5.2 坑二:跨域配置写了但前端还是报CORS错误
前端页面能打开,但一调用接口,浏览器控制台报错has been blocked by CORS policy,于是去网上搜到解决方案——在Django里安装django-cors-headers,然后把中间件加上,把CORS_ALLOW_ALL_ORIGINS = True配上。配完之后依然报错没变化。
原因在于配置的顺序和位置。django-cors-headers要求中间件加在CommonMiddleware之前,而且CORS_ALLOWED_ORIGINS和CORS_ALLOW_ALL_ORIGINS只需要设一个,如果两个都写了,CORS_ALLOWED_ORIGINS的空列表仍然会生效。同时还要检查这个配置是在整个Django运行中生效的,如果改完settings没有重启runserver,那当然是白改。
# settings.py 中按这个顺序和写法来 INSTALLED_APPS = [ ... 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # 必须放在最上面 'django.middleware.common.CommonMiddleware', ... ] CORS_ALLOWED_ORIGINS = [ 'http://localhost:5173', 'http://127.0.0.1:5173', ]只开放Vite开发服务器的两个地址,不要用CORS_ALLOW_ALL_ORIGINS = True图省事。生产环境如果前后端部署在同一台机器的不同端口,跨域配置也是用白名单方式,开放指定域名的权限。
5.3 坑三:逾期天数永远比实际少一天
还书页面显示的逾期天数,读者明明逾期还了2天,页面却显示1天;有时候上午还书还显示逾期0天,实际已经超期了。排查了好一阵代码逻辑都没问题,最后发现是时区问题。数据库里存的时间是UTC时间,Django的TIME_ZONE设置成了'UTC',而中国时间比UTC早8小时,导致timezone.now()取到的时间比本地时间晚了8小时。
# settings.py TIME_ZONE = 'Asia/Shanghai' USE_TZ = TrueUSE_TZ = True时Django内部会用UTC时间做逻辑运算,但写入数据库时会自动转换到Asia/Shanghai时区。改了这一个配置后,后端接口返回的due_time如果还是UTC字符串,做时间比较时也要注意。全套系统如果前后端都在本机运行,前端直接用后端格式化好的'%Y-%m-%d %H:%M:%S'字符串就好,不要自己在前端手动new Date()再转换时区,容易二次翻车。
5.4 坑四:Django后台加载不到CSS样式
输入http://127.0.0.1:8000/admin/之后能打开登录框,但页面完全没有样式,只有一堆堆叠的文字链接,浏览器开发者工具里满屏的404错误,找不到/static/admin/css/下的CSS文件。原因在于Django的静态文件服务默认只在DEBUG=True时开启,但依赖的django.contrib.staticfiles没被正确配置到INSTALLED_APPS里,或者STATIC_URL和项目实际的静态文件目录不一致。
# settings.py 中检查这几项 INSTALLED_APPS = [ 'django.contrib.staticfiles', # 必须存在 ... ] STATIC_URL = '/static/' STATICFILES_DIRS = [ BASE_DIR / 'static', # 如果项目有自定义static目录 ]如果用的是开发模式跑runserver,只要INSTALLED_APPS里有django.contrib.staticfiles,Django会自动帮你处理/static/admin/下的文件,不需要再用额外的whitenoise库。如果还是404,检查一下是不是runserver没有把端口开在正确位置,或者浏览器缓存了旧的404响应,强制刷新一次再试。
5.5 坑五:ORM建表后新增字段报已存在
改模型加了一个字段,执行python manage.py makemigrations和migrate后提示Duplicate column name,去MySQL里看那张表,发现字段已经加上了,但migration记录里没写进去,导致下次迁移重复执行。由于数据库脚本是直接用SQL文件导入的,Django的migration表里没有任何记录,Django不知道这个表已经存在了。
进入MySQL删除这张表的僵尸记录:
-- 把library_db换成你自己项目的库名 USE library_db; -- 查看migration记录中是否有对应app的迁移 SELECT * FROM django_migrations WHERE app = 'books'; -- 如果没有,手动插入一条最新的迁移记录(替换migration文件名) INSERT INTO django_migrations (app, name, applied) VALUES ('books', '0001_initial', NOW());最省事的做法是,如果系统里还没有业务数据,直接删库重建,重新执行包里的library.sql,再执行python manage.py migrate --run-syncdb让Django根据模型补建不存在的表,最后把migration记录对齐。有过一次半路续接的migration状态之后,后面的每次模型修改都要先确保makemigrations能执行。
5.6 避坑经验汇总:联调前必过的自查清单
| 检查项 | 正确配置 | 出错现象 |
|---|---|---|
| MySQL认证插件 | 使用mysql_native_password | Python连接时报caching_sha2_password |
| CORS中间件顺序 | CorsMiddleware在CommonMiddleware前 | 前端请求被CORS拦截 |
| 时区设置 | TIME_ZONE = Asia/Shanghai | 逾期天数差8小时 |
| 静态文件 | INSTALLED_APPS含staticfiles | Admin后台无样式 |
| 迁移记录 | django_migrations与模型一致 | migrate报Duplicate column |
自查清单主要解决的是“资源跑起来之后,换到了新环境/新电脑上能不能重复运行”的问题。答辩前进行一次这些检查项的走查,能节省大量现场排错的时间,也能在演示前自己发现大部分翻车风险。第6章我将讲已验证没问题的系统在答辩现场的呈现顺序和讲解节奏。
6. 答辩演示前的最后验证:拿这套项目走一遍完整说辞
6.1 全流程走查:从浏览器到数据库的一条龙演示
答辩演示时,最怕的不是评委问技术问题,而是演示到一半现场翻车——比如借书时报库存不足,或者页面加载失败。我自己每次都用一条固定的路径走查:启动顺序固定、数据准备固定、说辞固定。先从MySQL开始启动,数据库如果是Windows服务就确认服务已在运行;接着启动Django后端,确认http://127.0.0.1:8000/admin/能打开;最后启动Vite开发服务器,用管理员账号登录系统。
登录成功后的演示顺序也很关键。建议先打开图书列表页,演示分页和搜索功能;然后新开一个标签页打开读者列表,用到系统里的预置读者账号;再回到借还管理页,输入读者编号调出读者信息,输入图书编号完成一次借书;最后进入逾期统计页,展示当前逾期记录的列表。整个流程覆盖了系统最核心的增删改查和统计功能,前后不过五分钟。
数据准备上有一点自己踩过坑:演示前故意准备一本库存只有1的图书用来演示借书,演示后立刻还回去恢复正常数据。如果你选了一本库存较多的大众图书展示借书,那么借完也要在下一个环节把它还掉,否则后续演示库存列表时要注意那本书显示少了1本,答辩评委会认为数据对不上。这套系统预置的演示数据平衡了借出和未借出的馆藏状态,直接拿来走查即可。
6.2 用自己的话说清一段评委最可能追问的代码
答辩时评委大概率会指着借阅记录表的status字段问:“你这个逾期是怎么判断的?系统根据哪个字段来识别?”此时直接回答:borrow_record表里有一个due_time字段,是借书时后端用当前时间加30天计算出来的;还书时不直接改due_time,而是对比当前时间和due_time的大小关系,把比较结果写进status字段,2就是逾期归还、1是正常归还。同时描述一下库存在这个过程中怎么保持一致:借书时current_stock减一,还书时加一,加减放在同一个数据库事务里,不会出现只减不加或只加不减的状态不一致异常。
这类儒雅随和的表述方式比直接背代码的效果好很多。讲完一段之后还可以补一句:如果要扩展成预约借书功能,则可以在status中追加一个3代表预约待取状态,并在借书接口里增加预约信息的校验。这样把当前设计和可扩展性同时说清楚了,比单纯讲“这里我用了一个整数来表示状态”要有说服力得多。
从那以后我每次拿到一套类似的项目,都会先强制自己走一遍完整流程:确认环境能跑、确认关键业务链路能通、确认重要字段在答辩时能讲清,然后才去动代码。这套流程习惯省掉了我很多次踩坑后半夜查配置的折腾。希望帮到你。
本文还有配套的精品资源,点击获取