
简介Python结合Django构建的图书管理系统源码包面向刚接触Web框架的Python学习者、计算机专业课程设计与毕业设计人群聚焦图书信息录入、分类检索、借还管理等典型后台业务场景。资源共51个文件核心代码以py源文件为主覆盖models数据模型、views视图函数、admin后台、urls路由配置及migrations数据迁移脚本同时附带sqlite3数据库文件可免去额外配置直接运行调试少量图片和md文档用于项目说明与界面素材压缩包整体仅711KB轻量易部署。已有2878人学习下载多用于课程设计参考与复现Django MTV开发流程。通过阅读这套源码可使用Django ORM完成数据建模与增删改查理解settings配置、模板渲染、静态资源挂载等关键环节还可参考其目录组织方式为独立开发资讯管理、学生管理等相似信息管理系统打下扎实基础。1. 从一份图书管理系统的压缩包说起拿到Python基于Django的图书管理系统源码.zip这类压缩包时很多人第一反应是“老掉牙的课设项目”但它实际上把 Django 开发里最常被问到的知识点全部串起来了——ORM 关联查询、分页、模板渲染、后台管理、事务与并发扣减、部署上线。图书管理系统不像电商秒杀那样高并发但它覆盖的业务边界恰好是 CRUD 之外的那层库存字段的原子操作、借阅状态的状态机流转、超期归还的批量清理。这正是中级工程师日常会写、会维护的那类业务代码。对 Python 新手而言这份源码最好的打开方式不是“跑起来截图交作业”而是沿着模型定义、视图函数、模板标签、管理后台、部署脚本这条线去改造它。对 5 年以上的开发来说值得关注的反而是那些参数select_related用在哪条查询链、transaction.atomic包住哪段逻辑、F()表达式如何防止并发超借。本文就按这个顺序把能从标题里拆出来的技术点逐个说透。2. Django图书管理系统的数据建模与ORM查询口径2.1 应用拆分与模型字段设计用 Django 写图书管理系统第一步往往不是写代码而是决定应用怎么拆。常见做法是拆成books和borrow两个 app前者管图书档案后者管借阅记录。小项目合成一个 app 也能跑但拆分之后权限控制、admin 配置、后续加预约和续借功能会清晰很多。创建应用的命令是python manage.py startapp books python manage.py startapp borrow创建之后要把 app 名写进settings.py的INSTALLED_APPS否则makemigrations会找不到模型。图书模型建议这样定义class Book(models.Model): isbn models.CharField(ISBN, max_length13, uniqueTrue) title models.CharField(书名, max_length200, db_indexTrue) author models.CharField(作者, max_length100) publisher models.CharField(出版社, max_length100) publish_date models.DateField(出版日期, nullTrue, blankTrue) category models.CharField(分类, max_length50, blankTrue) total_copies models.PositiveIntegerField(总册数, default1) available_copies models.PositiveIntegerField(可借册数, default1) location models.CharField(馆藏位置, max_length50, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: ordering [-created_at] def __str__(self): return f{self.title} ({self.isbn})db_indexTrue加在需要频繁检索的字段上。total_copies和available_copies用正整数而不是布尔值表达“在馆/借出”是为了支持同一本书多册副本。排序在Meta.ordering里声明后默认查询就会按创建时间倒序避免每个视图里重复写order_by。2.1.1 借阅记录模型与状态字段借阅记录是整个系统里最容易把关系搞错的表。正确的做法是让BorrowRecord外键指向Book和用户而不是让Book去记录“谁借了这本书”。因为一本书有多个副本同一本书可以被不同人同时借不同的册。记录模型这样写class BorrowRecord(models.Model): BORROW_STATUS ( (borrowed, 借出中), (returned, 已归还), (overdue, 已超期), ) book models.ForeignKey(Book, on_deletemodels.CASCADE, related_nameborrow_records) borrower models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_nameborrow_records) status models.CharField(状态, max_length10, choicesBORROW_STATUS, defaultborrowed) borrowed_at models.DateTimeField(借出时间, auto_now_addTrue) due_date models.DateTimeField(应还时间) returned_at models.DateTimeField(归还时间, nullTrue, blankTrue) class Meta: ordering [-borrowed_at]状态用一个CharField加choices即可不要拆成多个布尔字段。due_date是借出时根据settings.BORROW_DAYS计算出来的快照而不是归还时实时计算这样即使后来调整借期规则历史记录的应还时间仍然准确。returned_at保留原始时间戳便于统计借阅时长。2.2 ORM 查询常用口径与参数说明系统里出现频率最高的查询是“查某本书是否可借”“查当前用户借了哪些书”“按分类过滤图书”。这三条分别对应 ORM 的三种典型写法# 查可借图书过滤可借册数大于 0 available_books Book.objects.filter(available_copies__gt0) # 查用户当前借出中的记录 current_borrows BorrowRecord.objects.filter( borrowerrequest.user, statusborrowed ).select_related(book) # 按分类过滤并按可借数倒序 math_books Book.objects.filter(category计算机).order_by(-available_copies)filter里的available_copies__gt0是字段查找语法__gt表示大于。select_related(book)会在 SQL 层面用JOIN把BorrowRecord和Book一次查出来避免在模板里访问record.book.title时逐条触发查询。这是最常见的一类 N1 问题源码里如果看到 views 里没有加select_related这就是改造点。2.2.1 聚合、分组与复杂条件查询图书列表页的筛选条件多了以后filter链会变得很长这时用Q对象组合条件更清晰from django.db.models import Q, Count # 标题或作者匹配并且可借 results Book.objects.filter( Q(title__icontainskeyword) | Q(author__icontainskeyword), available_copies__gt0 ) # 每个分类的图书数量和可借总量 category_stats Book.objects.values(category).annotate( totalCount(id), availableCount(id, filterQ(available_copies__gt0)) )Q对象用|表示 OR多个条件放在同一个filter里是 AND 关系。values(category)之后接annotateSQL 会按category分组。Count(id, filterQ(...))是 Django 2.x 之后才有的条件聚合写法老代码里往往先过滤再聚合性能差一截。3. 视图、URL路由与模板渲染的落地写法3.1 函数视图和类视图如何取舍图书管理系统的视图层有两种组织方式函数视图FBV和类视图CBV。源码里两种都常见我的建议是——列表页用ListView增删改用函数视图。因为列表页的逻辑高度雷同ListView直接给分页而借阅和归还涉及状态变更写进函数视图里更容易用transaction.atomic包住。from django.views.generic import ListView from .models import Book class BookListView(ListView): model Book template_name books/book_list.html context_object_name books paginate_by 20 def get_queryset(self): queryset super().get_queryset() keyword self.request.GET.get(q, ) if keyword: queryset queryset.filter( Q(title__icontainskeyword) | Q(author__icontainskeyword) ) return querysetpaginate_by 20控制每页数量。模板里通过page_obj获取分页对象page_obj.has_previous、page_obj.next_page_number控制上下翻页。context_object_name books是给模板用的变量名不设置的话默认是book_list容易在模板里写错。3.2 URL路由的 name 与 reverse 设计URL 配置虽然简单但最容易踩坑的是硬编码路径。模板里href/book/{{ book.id }}/写多了之后一旦路径调整全站模板都要改。正确的做法是在urls.py里给每个路由起name模板用{% url books:detail book.id %}反向解析。reverse和resolve这一对方法在测试和重定向场景里非常有用。app_name books urlpatterns [ path(, views.BookListView.as_view(), namelist), path(book/int:pk/, views.book_detail, namedetail), path(book/int:pk/borrow/, views.borrow_book, nameborrow), path(book/int:pk/return/, views.return_book, namereturn), ]app_name books设置了命名空间让多个 app 里的同名路由不冲突。视图里做重定向时用reverse(books:detail, args[book.id])测试里用resolve(/book/1/)校验路由匹配是否正常。这样改路径只动urls.py不动业务代码。3.3 模板变量与模板继承模板层要解决的问题有三个重复的公共头部、列表渲染、空数据提示。公共部分用{% extends base.html %}解决列表渲染用{% for %}空数据用{% empty %}。{% extends base.html %} {% block content %} div classbook-grid {% for book in books %} div classbook-card h3a href{% url books:detail book.id %}{{ book.title }}/a/h3 p{{ book.author }} | {{ book.category }}/p p可借 {{ book.available_copies }} / 共 {{ book.total_copies }} 册/p /div {% empty %} p没有找到匹配的图书/p {% endfor %} /div {% if is_paginated %} div classpagination {% if page_obj.has_previous %} a href?page{{ page_obj.previous_page_number }}上一页/a {% endif %} span第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页/span {% if page_obj.has_next %} a href?page{{ page_obj.next_page_number }}下一页/a {% endif %} /div {% endif %} {% endblock %}{% empty %}比在视图里判断books是否为空再传一个empty_flag干净得多。分页链接要注意保留已有的查询参数直接写?page会丢掉?qxx的搜索条件正确写法是用request.GET.urlencode()把原参数带进去。这是源码改造里很常见的一个坑。4. 图书管理系统的核心业务逻辑4.1 借阅流程与事务原子性借阅动作本质上是一次“扣减库存 创建借阅记录”的组合操作。扣库存和写记录必须在一个事务里否则可能出现记录创建成功但库存没扣或者库存被扣但记录失败的脏数据。Django 里用transaction.atomic()包住这两步from django.db import transaction from django.utils import timezone from datetime import timedelta transaction.atomic def borrow_book(request, book_id): book Book.objects.select_for_update().get(pkbook_id) if book.available_copies 1: return JsonResponse({error: 该图书暂时无可借副本}, status400) book.available_copies - 1 book.save() BorrowRecord.objects.create( bookbook, borrowerrequest.user, due_datetimezone.now() timedelta(dayssettings.BORROW_DAYS), statusborrowed, ) return JsonResponse({message: 借阅成功})select_for_update()是这里的关键参数它会在数据库层面给这一行记录加锁直到事务提交。多人同时借同一本书的最后一册时这条语句能有效避免超借。没有加锁的话两个请求都读到available_copies1各自减一库存就变成 -1。timedelta(dayssettings.BORROW_DAYS)里的天数建议提到配置里方便管理员调整。4.2 归还、续借与状态流转归还逻辑与借阅相反但多了“检查是否超期”的分支。状态机只有三条边borrowed - returned、borrowed - overdue、overdue - returned。归还时统一处理transaction.atomic def return_book(request, record_id): record BorrowRecord.objects.select_for_update().get(pkrecord_id) if record.status returned: return JsonResponse({error: 该记录已归还}, status400) book record.book book.available_copies 1 book.save() record.status returned record.returned_at timezone.now() record.save() return JsonResponse({message: 归还成功})这里有一个细节归还时要先把record查出来再操作record.book。不要通过book.borrow_records.filter(statusborrowed)反查记录因为同一本书的多册副本可能分属不同用户反查很容易改错记录。续借的实现可以复用借阅的一段逻辑但不能直接新建记录而是要更新due_date并检查累计借阅次数防止无限续借。4.3 超期记录的后台清理与定时任务超期状态在真实的系统里通常不是借出时立刻判断的而是通过定时任务批量修正。用 Django 的 management command 写一个清理命令class Command(BaseCommand): help 将逾期未还的借阅记录标记为 overdue def handle(self, *args, **options): now timezone.now() overdue_records BorrowRecord.objects.filter( statusborrowed, due_date__ltnow ) count overdue_records.update(statusoverdue) self.stdout.write(f标记超期记录 {count} 条)update()是批量操作直接生成UPDATE语句不会触发模型的save()方法适合这种不涉及业务钩子的状态同步。需要注意的是update()不会更新auto_now字段所以超期修正不会覆盖returned_at这类时间戳。清理命令的调用可以挂在系统 crontab 里每天凌晨执行一次比在视图里临时判断due_date now更稳妥。4.4 Django Admin 后台的列表配置与界面优化图书管理系统的后台管理是 Django Admin 的强项但默认界面信息密度太低。用list_display、list_filter、search_fields三个参数就够把后台变成可用的管理界面admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display (title, author, category, total_copies, available_copies) list_filter (category, publish_date) search_fields (title, author, isbn) list_editable (available_copies,)list_display控制列表显示哪些列list_filter在右侧生成过滤面板search_fields生成搜索框。list_editable允许在列表页直接修改可借册数适合管理员盘点后快速调整库存。如果嫌默认 Admin 界面样式老气常见做法有两个一是写admin/base_site.html模板覆盖改掉标题和样式表二是接入django-simpleui这类第三方皮肤。前者不需要额外依赖后者开箱即用但要注意版本兼容。5. 部署上线与常见排错参数拿到源码压缩包最常遇到的是本地能跑、服务器上跑不起来。问题集中在 MySQL 驱动、静态文件收集、ALLOWED_HOSTS三个地方。先把产物准备齐全再谈部署。# 1. 创建并激活虚拟环境 python3 -m venv venv source venv/bin/activate # 2. 安装依赖并检查 django 版本 pip install -r requirements.txt python -m django --version # 3. 如果使用 mysqlclient 且安装失败 sudo apt install libmysqlclient-dev default-libmysqlclient-dev pip install mysqlclientMySQL 驱动的安装是 Django 部署里被问得最多的一步。mysqlclient依赖系统库安装时报错mysql_config not found就是因为少了libmysqlclient-dev。如果不想装系统依赖用纯 Python 的PyMySQL也能跑但PyMySQL的 C 扩展性能不如mysqlclient压测时会差几个百分点。数据库连接信息集中放在settings.py或环境变量里DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: library_db, USER: library_user, PASSWORD: os.environ.get(DB_PASSWORD), HOST: 127.0.0.1, PORT: 3306, CONN_MAX_AGE: 60, } }CONN_MAX_AGE60让数据库连接在 60 秒内复用减少频繁建连的握手开销设为0则每个请求都重新连接性能会差很多。部署上线时把DEBUG设为FalseALLOWED_HOSTS填域名或服务器 IPSECRET_KEY从环境变量读取禁止明文写在源码里。静态文件用python manage.py collectstatic收集到指定目录然后交给 Nginx 直接托管。使用宝塔面板部署时常见的做法是先在面板里装好 Nginx 和 MySQL再用 Python 项目管理器创建虚拟环境并运行迁移脚本python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py collectstatic --noinput迁移失败时先看报错里的 SQL 语句多数情况是数据表字符集不是utf8mb4中文索引长度超限执行ALTER DATABASE library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci后重新迁移。浏览器访问 500 错误时先看/var/log/nginx/error.log和后端进程的 stderr 日志ALLOWED_HOSTS配置错误会直接提示Invalid HTTP_HOST header。验证部署是否成功最后用一行命令确认首页响应码即可curl -I -H Host: your-domain.com http://127.0.0.1:8000/curl -I只拉取响应头而不下载整个页面200 OK说明 Django 进程正常然后再检查静态文件是否由 Nginx 直接返回而不是经过 Django 的runserver。静态文件 404 时优先确认STATIC_ROOT和STATIC_URL是否配对以及 Nginx 里location /static/的alias路径是否指向collectstatic的输出目录这两处不匹配是部署后最常见的问题。本文还有配套的精品资源点击获取