Django图书管理系统实战:从ORM模型到部署避坑的完整开发复盘
简介这是一套面向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 图书表的字段选型下面是我最终确定的图书信息表核心字段以及每个字段这么设计的原因字段名类型说明titleCharField(max_length200)书名必须有索引搜索高频字段authorCharField(max_length100)作者isbnCharField(max_length13)ISBN号用CharField不用IntegerField因为ISBN可能有X结尾publisherCharField(max_length100)出版社publish_dateDateField出版日期前端展示和排序用categoryForeignKey(Category)分类外键total_countPositiveIntegerField库存总量available_countPositiveIntegerField当前可借数量coverImageField(upload_tocovers/)封面图ImageField依赖PillowdescriptionTextField简介created_atDateTimeField(auto_now_addTrue)创建时间有一点值得单独说available_count千万别省。有些设计只在借阅记录里标记谁借了书然后通过统计“多少条未归还记录”来算可借数量。这种方案在数据量小的时候没问题但数据一多每次列表页都要做聚合查询页面会越来越慢。冗余一个字段借书时减一、还书时加一配合事务保护性能好得多。2.2 借阅记录表业务逻辑最密集的地方借阅记录表的字段是这样设计的字段名类型说明readerForeignKey(Reader, on_deletemodels.CASCADE)关联读者bookForeignKey(Book, on_deletemodels.CASCADE)关联图书borrow_dateDateTimeField(auto_now_addTrue)借出时间due_dateDateTimeField应还时间一般是借出时间30天return_dateDateTimeField(nullTrue, blankTrue)实际归还时间renew_countPositiveIntegerField(default0)续借次数statusSmallIntegerField(choices...)状态这里有两个关键设计决定。第一on_delete我用了CASCADE。这样做的好处是删数据不会报错但生产环境下管理员误删一本被借出的书时借阅记录也会被删掉。教学项目这样搞没问题生产环境建议用PROTECT让系统拦住删除操作。这是我在长期实践里的个人取舍。第二due_date的默认值不能用auto_now_add做加30天的逻辑。我一开始天真地以为可以用default写成date.today()timedelta(days30)这确实可以但要注意它是模块加载时计算的。真正稳妥的做法是在创建借阅记录的视图里手动算好这个日期再存进去。2.3 用Django的ORM怎么建多表关联查询关联设计上没什么花活Book外键到CategoryBorrowRecord外键到Reader和Book。但查询的时候一定要会用select_related否则代码一复杂N1查询问题会让你怀疑人生。# 错误的做法每查一条借阅记录都要额外查一次图书和读者 records BorrowRecord.objects.filter(status0) for record in records: print(record.book.title, record.reader.name) # 正确的做法连表一次查出所有关联数据 records BorrowRecord.objects.select_related(book, reader).filter(status0)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__icontainskeyword) | Q(author__icontainskeyword) ) if category_id: books books.filter(category_idcategory_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(pkbook_id) if book.available_count 0: # 可借数量不足返回错误提示 return JsonResponse({code: 1, msg: 库存不足}) # 生成借阅记录 BorrowRecord.objects.create( readerrequest.user.reader, bookbook, due_datetimezone.now() timedelta(days30) ) # 更新库存用F表达式保证数据库层面的原子操作 Book.objects.filter(pkbook_id).update( available_countF(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(totalSum(total_count))[total] or 0 total_readers Reader.objects.count() borrowed_count BorrowRecord.objects.filter(status0).count() overdue_count BorrowRecord.objects.filter( status0, due_date__lttimezone.now() ).count()多表关联时用annotate给查询结果加聚合字段比拆成多个查询再手动配对更靠谱。比如查询“每个分类下的图书数量”可以这样写Category.objects.annotate(book_countCount(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.xPyMySQL是Python操作MySQL的驱动。为什么不装mysqlclient因为它依赖系统编译环境Windows和macOS上安装经常报错PyMySQL是纯Python实现安装简单兼容性更好对小型项目性能差距可以忽略。安装完PyMySQL后必须做一件事在项目的__init__.py里加上两行代码否则Django无法识别MySQL数据库。import pymysql pymysql.install_as_MySQLdb()这行代码的作用是把PyMySQL伪装成MySQLdbDjango只会去调用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_passwordPyMySQL在旧版本上连接会报错。解决办法要么在创建用户时指定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_count1都判断可以借然后都执行了减一操作。后来我把借书逻辑全部包裹进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/ShanghaiUSE_TZ保持True。Django在写入MySQL时会把本地时间转成UTC存进去读取时再转回本地时区逻辑完全自洽。如果你不想有任何时区转换可以把USE_TZ设为False但这样Django的部分时间处理比如datetime.now()会以服务器时间为准反而容易出问题。个人建议保持USE_TZTrue只改TIME_ZONE。5.4 查询性能的优化思路图书列表页在最开始的时候每打开一次要一秒多百思不得其解。后来用Django的connection.queries看了实际执行的SQL才发现列表页加载20条记录居然执行了21条查询——因为每次都去联查了分类表。加了select_related之后降到1条查询页面打开时间从一秒降到几十毫秒。这个优化几乎不需要改任何业务代码就是加一行效果立竿见影。另外一个性能相关的优化是给外键和经常查询的字段加索引。Django的ForeignKey字段默认会建索引CharField则不会。如果你经常按title搜索可以给title加db_indexTrue这对大数据量下的查询性能影响很大。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章的步骤配置环境十分钟就能跑起来。本文还有配套的精品资源点击获取