简介:这是一套面向Python初学者与Web开发入门者的完整图书管理系统实战项目,基于Django框架与MySQL数据库构建,适用于高校课程设计、小型图书馆或图书室数字化管理场景。资源包含2000个文件,以1609个JavaScript脚本(实现前端交互与表单校验)、271个HTML模板(承载图书增删改查、用户登录、借阅记录等核心页面)、50个CSS样式文件(含bootstrap、font-awesome、datetimepicker等主流UI组件)为主干,辅以少量Python后端逻辑(5个.py文件)与JSON配置数据,整体压缩包仅5.79MB,轻量易部署。已有187人学习下载。读者可直接运行完整Django项目,获得从数据库建模(含用户、图书、借阅三张主表)、管理员/普通用户双角色权限控制、到借还书全流程闭环的全栈开发范例,代码结构清晰、注释充分,是掌握Django MTV模式与MySQL集成实践的优质学习素材。 写这篇东西之前,先说说我为什么要选这个项目。Django的官方教程做的是投票应用,很多人跟着敲完一遍之后,面对“独立开发一个完整项目”这件事还是心里没底——投票应用只有一张表、两个页面,离真正的业务系统差得太远。图书管理系统恰好是Django入门的经典实战场景:它包含多张数据表的关联、前后台分离的管理需求、借阅流程里的状态变更、搜索分页统计这些最常见的业务动作,一套做下来,Django的ORM、Admin后台、视图层、模板层这些核心能力基本都能覆盖到。这篇博客我会从需求拆解、数据库设计、核心功能实现、环境配置到踩坑记录,完整复盘这个项目的开发过程,代码和数据库文件也已经整理好,拿到就能跑。
1. 项目的真实边界:图书管理系统到底要管什么
很多人拿到“图书管理系统”这个题目就开始写代码,结果做着做着发现功能越加越多,最后连自己都绕晕了。动手之前把需求边界划清楚,比写代码本身重要得多。
1.1 三个核心角色和六张业务表
图书管理系统本质上解决的是三类人的问题:普通读者要查书、借书、还书;图书管理员要维护书籍信息、处理借还、管理读者;系统管理员要管分类、管账号、看统计。围绕这三个角色,数据模型可以收敛成六个核心部分:
- 图书分类表:一级分类即可,比如文学、计算机、历史、经济,不需要搞无限极分类,徒增复杂度。
- 图书信息表:书名、作者、ISBN、出版社、出版日期、定价、库存总量、当前可借数量、封面图、简介。
- 读者表:读者号、姓名、性别、联系方式、注册时间、状态(正常/挂失/冻结)。
- 借阅记录表:哪本书、哪个读者、借出时间、应还时间、实际归还时间、续借次数、状态(借阅中/已归还/逾期已还)。
- 管理员表:Django自带的User表可以直接复用,加上is_staff、is_superuser权限控制。
- 公告/轮播图表(可选):如果做前台展示,可以加一张公告表,发布图书馆的最新通知。
对于教学级和中小型实际场景来说,这个边界已经足够了。不要一上来就想着做预约系统、罚款系统、座位预约、图书荐购,需求一多,项目周期和出错概率都会成倍上涨。
1.2 借阅流程里的状态机
图书管理系统最容易乱的地方就是借阅状态,我习惯先把状态流转画出来再写代码。一张借阅记录只有四种状态:
- 借阅中:书已借出,尚未归还。
- 已归还:书已还回,记录留档。
- 已续借:在借阅中基础上发生续借动作,应还日期向后顺延。
- 逾期未还:超过应还日期且未归还,系统自动标记。
回到代码层面,借阅记录表的status字段可以用整数存储,0表示借阅中、1表示已归还、2表示续借过。逾期状态不用单独落库,因为它是通过“应还时间大于当前时间且status为0”动态算出来的,单独字段存储会很麻烦——每天要跑定时任务去更新状态,一旦漏跑数据就全乱了。
1.3 为什么是Django + MySQL,而不是别的组合
这个组合在图书管理系统这个场景下几乎是“不用想”的答案。Django自带Admin后台,意味着你的图书信息维护、读者管理页面不用从零手写——把模型注册进Admin,一个可用的后台管理系统基本就出来了,这对项目的开发效率提升是实打实的。ORM让多表关联的操作从手写JOIN变成了Python对象属性访问,心智负担小很多。
数据库选MySQL而不选SQLite,是因为SQLite在并发写入场景下表现不好,图书系统虽然并发量不大,但如果是课程设计或者小团队内部使用,后续扩展到局域网多人访问,SQLite很可能会成为瓶颈。MySQL的部署和维护成本虽然比SQLite高,但它稳定、生态成熟、遇到问题网上的解决方案一搜一大堆。
提示:如果只是本机跑通玩一玩,SQLite确实更省事,但既然项目的定位是“完整源码+数据库”,果断上MySQL,不要半途换库。
2. 数据模型设计:把地基打牢,后面才不返工
数据库表结构设计我走了两版。第一版图省事,借阅记录表里只存了读者ID和图书ID,结果想要查询“某个读者正在借的书”时,要跨三张表联查,麻烦得要死。第二版把冗余设计加了进去,查询一下子简单很多。
2.1 图书表的字段选型
下面是我最终确定的图书信息表核心字段,以及每个字段这么设计的原因:
| 字段名 | 类型 | 说明 |
|---|---|---|
| title | CharField(max_length=200) | 书名,必须有索引,搜索高频字段 |
| author | CharField(max_length=100) | 作者 |
| isbn | CharField(max_length=13) | ISBN号,用CharField不用IntegerField,因为ISBN可能有X结尾 |
| publisher | CharField(max_length=100) | 出版社 |
| publish_date | DateField | 出版日期,前端展示和排序用 |
| category | ForeignKey(Category) | 分类外键 |
| total_count | PositiveIntegerField | 库存总量 |
| available_count | PositiveIntegerField | 当前可借数量 |
| cover | ImageField(upload_to='covers/') | 封面图,ImageField依赖Pillow |
| description | TextField | 简介 |
| created_at | DateTimeField(auto_now_add=True) | 创建时间 |
有一点值得单独说:available_count千万别省。有些设计只在借阅记录里标记谁借了书,然后通过统计“多少条未归还记录”来算可借数量。这种方案在数据量小的时候没问题,但数据一多,每次列表页都要做聚合查询,页面会越来越慢。冗余一个字段,借书时减一、还书时加一,配合事务保护,性能好得多。
2.2 借阅记录表:业务逻辑最密集的地方
借阅记录表的字段是这样设计的:
| 字段名 | 类型 | 说明 |
|---|---|---|
| reader | ForeignKey(Reader, on_delete=models.CASCADE) | 关联读者 |
| book | ForeignKey(Book, on_delete=models.CASCADE) | 关联图书 |
| borrow_date | DateTimeField(auto_now_add=True) | 借出时间 |
| due_date | DateTimeField | 应还时间,一般是借出时间+30天 |
| return_date | DateTimeField(null=True, blank=True) | 实际归还时间 |
| renew_count | PositiveIntegerField(default=0) | 续借次数 |
| status | SmallIntegerField(choices=...) | 状态 |
这里有两个关键设计决定。
第一,on_delete我用了CASCADE。这样做的好处是删数据不会报错,但生产环境下管理员误删一本被借出的书时,借阅记录也会被删掉。教学项目这样搞没问题,生产环境建议用PROTECT,让系统拦住删除操作。这是我在长期实践里的个人取舍。
第二,due_date的默认值不能用auto_now_add做加30天的逻辑。我一开始天真地以为可以用default写成date.today()+timedelta(days=30),这确实可以,但要注意它是模块加载时计算的。真正稳妥的做法是在创建借阅记录的视图里手动算好这个日期再存进去。
2.3 用Django的ORM怎么建多表关联查询
关联设计上没什么花活:Book外键到Category,BorrowRecord外键到Reader和Book。但查询的时候一定要会用select_related,否则代码一复杂,N+1查询问题会让你怀疑人生。
# 错误的做法:每查一条借阅记录,都要额外查一次图书和读者 records = BorrowRecord.objects.filter(status=0) for record in records: print(record.book.title, record.reader.name) # 正确的做法:连表一次查出所有关联数据 records = BorrowRecord.objects.select_related('book', 'reader').filter(status=0)select_related适合处理一对一和一对一的外键关系,它会生成SQL JOIN,把关联表的字段一并查出来。如果遇到Book外键Category这样的多级关联,还能链式调用select_related('book__category')。一篇文章里只要出现列表页,就必须养成习惯先想一下要不要加这个优化。
3. 核心功能实现:从增删改查到借阅闭环
模型设计好之后,功能实现就是按部就班地写视图、模板、URL。但有几处逻辑值得展开说,因为它们隐藏着很多人容易踩的坑。
3.1 图书列表页的分页与多条件检索
列表页大家都写过,但图书管理系统里的检索条件比较多:按书名模糊搜、按作者模糊搜、按分类筛选、按出版时间排序。如果直接堆if判断,代码会变得很啰嗦。我建议用Django的Q对象统一处理搜索逻辑。
from django.db.models import Q def book_list(request): keyword = request.GET.get('keyword', '') category_id = request.GET.get('category', '') order_by = request.GET.get('order', 'id') books = Book.objects.select_related('category').all() if keyword: books = books.filter( Q(title__icontains=keyword) | Q(author__icontains=keyword) ) if category_id: books = books.filter(category_id=category_id) books = books.order_by(order_by) paginator = Paginator(books, 10) # 每页10条 page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) return render(request, 'book/book_list.html', {'page_obj': page_obj})需要注意Q对象里的icontains在MySQL里最终会转成LIKE '%keyword%',只要字段建了索引,依然无法命中索引,数据量大之后会慢。对于学习项目和几千条数据来说,问题不大,但你要知道这个瓶颈的根源在哪。
分页用的是Django自带的Paginator,它足够应付这个场景。自定义分页器的好处是显示效果更灵活,但对于图书管理系统这种场景,先把自带的用熟比什么都重要。
3.2 借书和还书的业务逻辑:事务和并发控制
借书的动作说起来很简单:检查这本书可借数量是否大于0,生成一条借阅记录,图书表可借数量减一。但如果不做并发控制,两个读者同时点击借阅同一本书的最后库存时,会产生超借问题。
用Django的F表达式加transaction.atomic()事务可以优雅地解决:
from django.db import transaction from django.db.models import F @transaction.atomic def borrow_book(request, book_id): book = Book.objects.select_for_update().get(pk=book_id) if book.available_count <= 0: # 可借数量不足,返回错误提示 return JsonResponse({'code': 1, 'msg': '库存不足'}) # 生成借阅记录 BorrowRecord.objects.create( reader=request.user.reader, book=book, due_date=timezone.now() + timedelta(days=30) ) # 更新库存,用F表达式保证数据库层面的原子操作 Book.objects.filter(pk=book_id).update( available_count=F('available_count') - 1 ) return JsonResponse({'code': 0, 'msg': '借阅成功'})这里有两个关键点我必须强调。
第一是select_for_update()。它在事务内给这一行数据加了行级锁,保证两个请求并发进来时,一个请求执行完,另一个请求才读到最新数据。没有这行代码,两个请求同时检查available_count时都大于0,就会造成超借。
第二是F('available_count') - 1。用F表达式做减法而不是先取出来再减,是为了避免并发下数据覆盖问题。先取出值再更新,在并发场景下可能丢失更新。这种细节很难从教程里学到,都是在真实项目里踩出来的经验。
还书的逻辑是镜像操作:把记录状态改成已归还,填上实际归还时间,图书表可借数量加一。但这里有一个“逾期判断”要处理,归还时检查due_date是否早于当前时间,如果早于,就标记逾期。
3.3 Admin后台定制:图书管理系统的最佳辅助
Django Admin是这个项目的隐藏主角。几乎不用写前端页面,只要注册模型,后台就能完成90%的管理操作。但默认的Admin交互比较朴素,我做了两个优化。
第一个是列表页展示字段和搜索框:
@admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display = ('title', 'author', 'publisher', 'available_count', 'total_count') search_fields = ('title', 'author', 'isbn') list_filter = ('category', 'publisher') ordering = ('-created_at',)第二个是借阅记录的批量操作:图书管理员经常会遇到“某个读者逾期未还书要发通知”的场景,我给借阅记录注册了自定义操作,一键标记选定记录为已催还。这个功能纯粹是通过Admin的actions机制实现的,不用写任何前端页面。
3.4 统计看板的实现思路
图书管理系统一般都要有一个首页统计看板,展示总藏书量、总读者数、当前借出数量、逾期未还数量。这些数据用ORM聚合函数一条条写出来:
from django.db.models import Count, Sum total_books = Book.objects.aggregate(total=Sum('total_count'))['total'] or 0 total_readers = Reader.objects.count() borrowed_count = BorrowRecord.objects.filter(status=0).count() overdue_count = BorrowRecord.objects.filter( status=0, due_date__lt=timezone.now() ).count()多表关联时,用annotate给查询结果加聚合字段,比拆成多个查询再手动配对更靠谱。比如查询“每个分类下的图书数量”,可以这样写:
Category.objects.annotate(book_count=Count('book')).order_by('-book_count')一句话就能拿到每个分类的图书数量,模板里直接循环渲染,列表页、图表页都能用。
4. 把项目跑起来:环境配置与部署避坑
源码和数据库文件都整理好了,但很多初学者在拿到代码后跑不起来,问题大多出在环境配置上。我按照自己从零搭建的顺序,把完整步骤写在这里。
4.1 创建虚拟环境并安装依赖
我建议所有Python项目都使用虚拟环境,否则不同项目的依赖版本会互相污染。在项目根目录执行:
python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install django pymysql pillowDjango版本我用的是4.x,PyMySQL是Python操作MySQL的驱动。为什么不装mysqlclient?因为它依赖系统编译环境,Windows和macOS上安装经常报错,PyMySQL是纯Python实现,安装简单,兼容性更好,对小型项目性能差距可以忽略。
安装完PyMySQL后,必须做一件事:在项目的__init__.py里加上两行代码,否则Django无法识别MySQL数据库。
import pymysql pymysql.install_as_MySQLdb()这行代码的作用是把PyMySQL伪装成MySQLdb,Django只会去调用MySQLdb接口,通过这个方式就能绕过驱动兼容性问题。
4.2 settings.py中数据库配置的细节
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'library_db', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } }这里有两个坑我需要单独提醒。
第一个坑:MySQL 8.0默认的密码认证插件是caching_sha2_password,PyMySQL在旧版本上连接会报错。解决办法要么在创建用户时指定mysql_native_password认证插件,要么直接升级PyMySQL到最新版本,新版本已经兼容了这个问题。
第二个坑:一定要显式指定OPTIONS里的charset: utf8mb4。如果不指定,默认字符集可能在插入中文时出现乱码,而且utf8mb4比utf8多支持emoji,当前主流的MySQL版本已经完全支持,直接用utf8mb4就对了。
4.3 数据库导入与超级管理员创建
拿到我提供的library_db.sql文件后,先创建数据库再导入:
CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE library_db; SOURCE /path/to/library_db.sql;然后回到项目目录执行迁移和初始化:
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver这里要注意一下执行顺序:如果直接导入sql文件,再执行migrate,有时候会因为表已存在而报错。稳妥的做法是:先migrate创建好所有表结构,再导入数据,或者导入sql后执行migrate --fake-initial跳过已存在的表。我在使用中更倾向于直接导入sql后,再在Django里做几次makemigrations和migrate --fake-initial,这样不会报错。说到底是哪种方式更顺手,按你的偏好来。
4.4 源码的目录结构速览
在项目根目录下看一下关键内容:
library/ ├── manage.py ├── library/ # 项目配置 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── book/ # 图书模块 │ ├── models.py # 数据模型 │ ├── views.py # 业务逻辑 │ ├── admin.py # Admin配置 │ └── templates/book/ # 模板文件 ├── reader/ # 读者模块 ├── borrow/ # 借阅模块 ├── static/ # 静态资源 ├── media/ # 上传的封面图 └── requirements.txt拿到代码后,先看requirements.txt把依赖装好,再看settings.py改数据库账号密码,最后启动项目访问http://127.0.0.1:8000。一个月前我把自己电脑上的项目迁移到一台新机器上时,全程只花了十分钟,就是按照这个流程走的。
5. 开发过程中踩过的坑和优化建议
最后这部分我要把自己反复踩过的坑原原本本写出来。这些坑几乎不会出现在官方文档里,但你在实际开发中早晚会遇到。
5.1 关于借书超卖的问题
这是我在初版代码里踩过的最典型的坑。当时没加select_for_update(),本地单个用户测试一切正常,但用两个浏览器同时操作同一本书的最后库存时,系统的可借数量变成了负数。原因是两个请求同时读到了available_count=1,都判断可以借,然后都执行了减一操作。
后来我把借书逻辑全部包裹进transaction.atomic(),并且给图书查询加了行级锁,这个问题彻底解决。并发问题的排查思路很简单:涉及读改写三个步骤的数据库操作,一律要考虑并发;安全做法是加事务和锁,而不是祈祷用户不会同时操作。
5.2 中文乱码问题
有段时间系统里新写入的图书简介里的引号变成了问号,排查发现是数据库连接字符集的问题。我的MySQL字符集是utf8mb4,但连接时没有指定,导致部分字符被截断。
你在settings.py中已经看到我写了OPTIONS里的charset,这里再补充一下:MySQL数据库本身的字符集最好在一开始建库时就指定,否则后期改库表字符集需要执行一堆ALTER语句,很麻烦。如果是已有数据库,可以用下面的SQL检查并修正:
SHOW CREATE DATABASE library_db; ALTER DATABASE library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.3 时区问题带来的假逾期
Django的TIME_ZONE默认是UTC,而USE_TZ默认是True。如果你不做任何修改,用户在中国时区访问系统,借阅记录的due_date是用UTC时间算的。一个8月1号借的书,加30天变成8月31号,但前端显示时Django会转成本地时间,显示成9月1号,而逾期判断用的又是UTC时间,就会出现“明明还没到日期但系统判定为逾期”的诡异现象。
我的处理方式是把TIME_ZONE设为'Asia/Shanghai',USE_TZ保持True。Django在写入MySQL时会把本地时间转成UTC存进去,读取时再转回本地时区,逻辑完全自洽。如果你不想有任何时区转换,可以把USE_TZ设为False,但这样Django的部分时间处理(比如datetime.now())会以服务器时间为准,反而容易出问题。个人建议:保持USE_TZ=True,只改TIME_ZONE。
5.4 查询性能的优化思路
图书列表页在最开始的时候每打开一次要一秒多,百思不得其解。后来用Django的connection.queries看了实际执行的SQL,才发现列表页加载20条记录,居然执行了21条查询——因为每次都去联查了分类表。
加了select_related之后,降到1条查询,页面打开时间从一秒降到几十毫秒。这个优化几乎不需要改任何业务代码,就是加一行,效果立竿见影。
另外一个性能相关的优化是给外键和经常查询的字段加索引。Django的ForeignKey字段默认会建索引,CharField则不会。如果你经常按title搜索,可以给title加db_index=True,这对大数据量下的查询性能影响很大。
5.5 备份与部署建议
开发完不是终点,部署上线才是。我个人的建议是:小规模内网使用可以部署在云服务器上,用python manage.py runserver只能用于开发,真正对外服务要用gunicorn + nginx的组合;再配上MySQL的数据定期备份,数据库文件用mysqldump导出,定时任务自动备份到其他目录。
mysqldump -u root -p library_db > backup_$(date +%Y%m%d).sql图书管理系统虽然功能不算复杂,但麻雀虽小五脏俱全:多表关联、事务、并发控制、分页搜索、权限管理、部署上线,这些知识点覆盖了一个Web开发者的核心技能树。把这套代码吃透,你再去学Django Rest Framework做前后端分离项目,或者去写其他业务系统,会发现思路是相通的。
最后分享一个我自己的习惯:代码写完之后,一定要做一遍完整的黑盒测试,模拟一个读者从注册、登录、找书、借书、续借、还书的完整流程,中间任何一步报错都要当场解决,不要攒着。我当年做这个项目时,就是因为在测试阶段偷了个懒,结果部署后才发现借书成功但没有跳转到详情页,这种小问题在开发环境里看日志半天找不出来,特别浪费时间。项目源码和数据库我都已经打包好,你拿到之后直接按照第4章的步骤配置环境,十分钟就能跑起来。
本文还有配套的精品资源,点击获取