简介: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_length=13, unique=True) title = models.CharField('书名', max_length=200, db_index=True) author = models.CharField('作者', max_length=100) publisher = models.CharField('出版社', max_length=100) publish_date = models.DateField('出版日期', null=True, blank=True) category = models.CharField('分类', max_length=50, blank=True) total_copies = models.PositiveIntegerField('总册数', default=1) available_copies = models.PositiveIntegerField('可借册数', default=1) location = models.CharField('馆藏位置', max_length=50, blank=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) class Meta: ordering = ['-created_at'] def __str__(self): return f'{self.title} ({self.isbn})'db_index=True加在需要频繁检索的字段上。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_delete=models.CASCADE, related_name='borrow_records') borrower = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='borrow_records') status = models.CharField('状态', max_length=10, choices=BORROW_STATUS, default='borrowed') borrowed_at = models.DateTimeField('借出时间', auto_now_add=True) due_date = models.DateTimeField('应还时间') returned_at = models.DateTimeField('归还时间', null=True, blank=True) 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__gt=0) # 查用户当前借出中的记录 current_borrows = BorrowRecord.objects.filter( borrower=request.user, status='borrowed' ).select_related('book') # 按分类过滤并按可借数倒序 math_books = Book.objects.filter(category='计算机').order_by('-available_copies')filter里的available_copies__gt=0是字段查找语法,__gt表示大于。select_related('book')会在 SQL 层面用JOIN把BorrowRecord和Book一次查出来,避免在模板里访问record.book.title时逐条触发查询。这是最常见的一类 N+1 问题,源码里如果看到 views 里没有加select_related,这就是改造点。
2.2.1 聚合、分组与复杂条件查询
图书列表页的筛选条件多了以后,filter链会变得很长,这时用Q对象组合条件更清晰:
from django.db.models import Q, Count # 标题或作者匹配,并且可借 results = Book.objects.filter( Q(title__icontains=keyword) | Q(author__icontains=keyword), available_copies__gt=0 ) # 每个分类的图书数量和可借总量 category_stats = Book.objects.values('category').annotate( total=Count('id'), available=Count('id', filter=Q(available_copies__gt=0)) )Q对象用|表示 OR,多个条件放在同一个filter里是 AND 关系。values('category')之后接annotate,SQL 会按category分组。Count('id', filter=Q(...))是 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__icontains=keyword) | Q(author__icontains=keyword) ) 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(), name='list'), path('book/<int:pk>/', views.book_detail, name='detail'), path('book/<int:pk>/borrow/', views.borrow_book, name='borrow'), path('book/<int:pk>/return/', views.return_book, name='return'), ]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 class="book-grid"> {% for book in books %} <div class="book-card"> <h3><a 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 class="pagination"> {% 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=会丢掉?q=xx的搜索条件,正确写法是用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(pk=book_id) if book.available_copies < 1: return JsonResponse({'error': '该图书暂时无可借副本'}, status=400) book.available_copies -= 1 book.save() BorrowRecord.objects.create( book=book, borrower=request.user, due_date=timezone.now() + timedelta(days=settings.BORROW_DAYS), status='borrowed', ) return JsonResponse({'message': '借阅成功'})select_for_update()是这里的关键参数,它会在数据库层面给这一行记录加锁,直到事务提交。多人同时借同一本书的最后一册时,这条语句能有效避免超借。没有加锁的话,两个请求都读到available_copies=1,各自减一,库存就变成 -1。timedelta(days=settings.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(pk=record_id) if record.status == 'returned': return JsonResponse({'error': '该记录已归还'}, status=400) 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(status='borrowed')反查记录,因为同一本书的多册副本可能分属不同用户,反查很容易改错记录。续借的实现可以复用借阅的一段逻辑,但不能直接新建记录,而是要更新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( status='borrowed', due_date__lt=now ) count = overdue_records.update(status='overdue') 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_AGE=60让数据库连接在 60 秒内复用,减少频繁建连的握手开销;设为0则每个请求都重新连接,性能会差很多。部署上线时把DEBUG设为False,ALLOWED_HOSTS填域名或服务器 IP,SECRET_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的输出目录,这两处不匹配是部署后最常见的问题。
本文还有配套的精品资源,点击获取