
简介一套基于Python与Django的图书馆管理系统源码包采用Django、MySQL5.7与Vue构建使用B/S模式面向毕业设计、课程设计、工程实训或初期项目立项适合正在学习前后端分离开发的初、中级学习者。系统完整覆盖首页、个人中心、用户管理、图书馆管理、图书信息与类型管理、借阅信息管理、归还信息管理、续借信息管理、罚金信息管理、留言板管理以及系统管理等模块能够清楚呈现管理员与用户两类角色的不同操作链路。压缩包共783个文件大小约20.13MB主要包含Python后端源码、Vue前端组件、HTML与JS和CSS静态资源、SQL数据库脚本以及docx、md说明文档和启动辅助脚本目录结构清晰便于分层查阅。目前已有1880人浏览学习适合作为项目参考。资源附带可运行源码与SQL文件可直接部署并快速查看图书建档、借阅、归还、续借、罚金等完整业务流程文档中的需求分析、数据库设计和模块图也可辅助进行毕业设计写作或二次开发。1. 图书馆管理系统不是简单增删改查而是一套状态机图书馆管理系统在 Django 项目里看着最人畜无害图书表、读者表、借书还书表不过七八张。真正让它翻车的往往不是界面而是还书那一刻——库存没回补、借阅记录被删掉、同一个人重复借同一本没有拦截。这个标题的本质不是“给图书建表”而是在 Django 的 ORM、事务和 Admin 之上把借阅状态流转写成一套经得起并发敲打的状态机。下面按一条可落地的路径展开先搭项目和模型再写借书还书视图用 Admin 和模板补齐操作界面最后处理查询、删除对象和并发。适合准备毕业设计、想把手头 pythondjango 图书馆源码捋顺的人也适合刚学完 Django 基础需要完整闭环的开发者。2. 搭 django 项目骨架图书馆管理系统的最小模型与迁移2.1 pip install django 与 startapp 创建项目的最小命令先说你一定会遇到的环境问题。如果本机 Python 版本较旧先去官网装 Python 3.8 以上再用python -m venv venv建虚拟环境。VSCode 里做 python 环境配置时选择解释器指向刚建好的 venv命令行再执行python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install django django-admin startproject library cd library python manage.py startapp booksstartproject library生成manage.py和项目包library里面是 settings、urls、wsgi 这些面向启动和部署的文件。startapp books生成面向业务的models.py、views.py、admin.py。两条命令的分工要区分清楚很多人把业务直接写在项目包里后面迁移和复用都会别扭。安装 Django 之后不要手动到处建目录用命令行生成是官方推荐路径。然后把books注册进INSTALLED_APPS。settings.py 里默认已有 7 个内置 appadmin、auth、sessions 这些都是图书馆系统要用的不要图干净把django.contrib.auth删掉读者登录权限依赖它。INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, books, ]2.2 图书馆核心模型馆藏、读者、借阅流水图书馆管理系统需要的表可以压缩成四张分类、图书、扩展读者信息、借阅记录。建模时的一个标准是“馆藏数量与在馆数量分开存”。代码# books/models.py from django.conf import settings from django.db import models class Category(models.Model): name models.CharField(分类名, max_length50, uniqueTrue) def __str__(self): return self.name class Book(models.Model): title models.CharField(书名, max_length200) author models.CharField(作者, max_length100) isbn models.CharField(ISBN, max_length20, uniqueTrue) category models.ForeignKey( Category, on_deletemodels.SET_NULL, nullTrue, blankTrue ) total models.PositiveIntegerField(馆藏总量, default1) available models.PositiveIntegerField(在馆数量, default1) is_deleted models.BooleanField(下架标记, defaultFalse) created_at models.DateTimeField(录入时间, auto_now_addTrue) class Meta: ordering [-created_at] verbose_name 图书 def __str__(self): return self.title class BorrowRecord(models.Model): BORROW_STATUS [ (borrowing, 借出中), (returned, 已归还), (overdue, 已逾期), ] book models.ForeignKey(Book, on_deletemodels.PROTECT, verbose_name图书) borrower models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.PROTECT, verbose_name借阅人 ) borrow_date models.DateTimeField(借出时间, auto_now_addTrue) due_date models.DateTimeField(应还时间, nullTrue, blankTrue) return_date models.DateTimeField(实际归还时间, nullTrue, blankTrue) status models.CharField(状态, max_length20, choicesBORROW_STATUS, defaultborrowing) class Meta: ordering [-borrow_date] verbose_name 借阅记录 def __str__(self): return f{self.borrower} - {self.book}available是冗余字段故意和total分开。每次借书时对它减一还书时加一比每次统计“total 减去未还记录数”快一个数量级也更容易在列表页直接展示“在馆”。BorrowRecord是所有业务的核心它同时外键到 Book 和 Useron_deletemodels.PROTECT表示有借阅记录时不允许直接把书或读者删掉这比 CASCADE 更适合图书这种有历史含义的业务。模型关键字段作用Categoryname分类名唯一约束Bookisbn, total, availableISBN 保证同一本书唯一total/available 区隔馆藏与在馆BorrowRecordbook, borrower, status一本书一次借阅一条流水status 标识状态机当前节点2.3 读者信息用 Profile 扩展 User而不是另建表常见的错误是在 books 里再建一个 Reader 表把 username、password 又存一遍。Django 自带auth.User登录、session、权限都已接好图书馆系统需要额外部字段学号、联系方式时用 OneToOneField 挂一个 Profileclass ReaderProfile(models.Model): user models.OneToOneField(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE) student_id models.CharField(学号, max_length20, blankTrue) phone models.CharField(手机号, max_length20, blankTrue) max_borrow models.PositiveIntegerField(最大可借数, default5) def __str__(self): return f{self.user.username}的读者资料OneToOneField保证一个用户只对应一份读者扩展信息max_borrow是额度借书时会把它和当前未还数量做比较。为什么这里用 CASCADE因为 Profile 是附属数据用户注销跟着删掉是合理行为。这和 BorrowRecord 上的 PROTECT 是两种语义别混用。2.4 迁移与初始数据python manage.py makemigrations books python manage.py migrate python manage.py createsuperuser三行命令含义不同makemigrations把模型变化生成迁移文件migrate真正去建表和改字段createsuperuser创建管理后台账号。顺序不能颠倒也不要在 migrate 之前急着写测试数据表还没建。迁移文件建议提交进版本库团队协作时其他人直接migrate就能把表结构拉齐。3. 图书馆管理系统的借书还书视图、事务与状态判断3.1 借书视图要过的四道检查借书不是“存一条记录”那么简单它在进入BorrowRecord.objects.create()之前要做完整判断。同一本书同一个读者上一次还没还就再次借出这是校园图书系统里最常见的投诉。我一般会把检查写成四个串行条件一旦失败就带 message 重定向回去# books/views.py from django.contrib import messages from django.db import transaction from django.db.models import F from django.shortcuts import get_object_or_404, redirect transaction.atomic def borrow_book(request, book_id): book get_object_or_404(Book.objects.select_for_update(), pkbook_id) if not request.user.is_authenticated: return redirect(login) if book.available 1: messages.error(request, f《{book.title}》没有在馆库存) return redirect(books:book_list) exists BorrowRecord.objects.filter( borrowerrequest.user, bookbook, return_date__isnullTrue, ).exists() if exists: messages.warning(request, 这本书还没还不能重复借) return redirect(books:book_list) profile request.user.readerprofile borrowing_count BorrowRecord.objects.filter( borrowerrequest.user, return_date__isnullTrue ).count() if borrowing_count profile.max_borrow: messages.error(request, 借阅额度已用满) return redirect(books:book_list) BorrowRecord.objects.create(bookbook, borrowerrequest.user, statusborrowing) Book.objects.filter(pkbook.pk).update(availableF(available) - 1) messages.success(request, 借阅成功) return redirect(books:book_list)select_for_update()必须在事务里生效这里用transaction.atomic包住整个函数保证两个动作创建记录、扣减库存要么都成功要么都回滚。用Book...update(availableF(available) - 1)而不是book.available - 1原因是后者“先读出再写回”两个请求并到同一个值时会把库存多扣F 表达式在数据库端完成减法数据库行锁会排队。四道检查的顺序有讲究库存不足要先拦截重复借要在检查额度之前不然一个已经超额的读者会先进到额度分支看不出真正的问题。messages 把错误送到下一次请求配合后面的重定向使用。提示select_for_update()只有在事务内才会走 SELECT ... FOR UPDATE没有transaction.atomic时会抛 TransactionManagementError。3.2 还书更新记录而不是删除对象见过不少项目把还书写成BorrowRecord.objects.get(...).delete()这是把流水账当临时单处理了。图书馆系统要留住“谁在哪天借过、什么时候还”的完整历史所以还书动作的本质是给已经存在的 BorrowRecord 填上 return_date 并改状态。transaction.atomic def return_book(request, record_id): record get_object_or_404( BorrowRecord.objects.select_for_update(), idrecord_id, borrowerrequest.user, statusborrowing, ) record.return_date timezone.now() record.status returned record.save(update_fields[return_date, status]) Book.objects.filter(pkrecord.book_id).update(availableF(available) 1) messages.success(request, 归还成功库存已回补) return redirect(books:my_borrows)记录选出来时加了statusborrowing条件等于在查询层做了一次状态检查已经还过的记录进不来也不会被重复归还。update_fields只更新两个字段避免整行写入把其他字段覆盖成旧值。库存回补仍然用 F 表达式和借书对称。状态跳转可以做成一张简单的表当前状态动作目标状态库存变化borrowing还书returnedavailable 1borrowing超期未还overdue无overdue还书returnedavailable 13.3 超期记录怎么查、怎么批量更新due_date建议在创建借阅记录时就算好borrow_date timedelta(days30)。Django 里查询超期记录用两个条件同时满足from datetime import timedelta from django.utils import timezone overdue_records BorrowRecord.objects.filter( return_date__isnullTrue, due_date__lttimezone.now(), )due_date__lttimezone.now()表示应还时间早于当前时间且没有归还时间这两个条件同时成立就是逾期。批量把逾期记录的状态从 borrowing 改成 overdue不要用循环逐条 save直接updateoverdue_records.update(statusoverdue)update()生成一条 UPDATE 语句不会触发模型的 save 方法也不会写 auto_now 字段这就是批量操作和单条 save 的差别。4. 用 admin 和模板把图书馆管理系统变成能用的后台4.1 admin 注册与列表界面美化Django admin 是这种管理系统的现成后台。注册模型时不要只写一行admin.site.register(Book)那会浪费掉 Django 自动生成列表页的能力。按下面这样写图书列表可以直接按分类筛选、按书名或作者搜索# books/admin.py from django.contrib import admin from .models import Book, BorrowRecord, Category, ReaderProfile admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display [title, author, isbn, category, total, available] list_filter [category, created_at] search_fields [title, author, isbn] list_editable [total, available] ordering [-created_at] admin.register(BorrowRecord) class BorrowRecordAdmin(admin.ModelAdmin): list_display [book, borrower, borrow_date, due_date, return_date, status] list_filter [status, borrow_date] search_fields [book__title, borrower__username] date_hierarchy borrow_datelist_display决定列表页展示出来的列list_filter在右侧生成筛选图书按分类、借阅记录按状态筛search_fields里的book__title是跨表搜索语法list_editable让 total/available 可以在列表页直接改比进详情页快。date_hierarchy是借阅记录时间轴点年份、月份逐层下钻。只做这些还不够再改两个全局入口admin.site.site_header 图书馆管理系统后台 admin.site.site_title 图书管理这两行放在 admin.py 底部替换默认 “Django administration” 字样界面看起来完整很多。4.2 模板渲染读者借阅记录清单前端部分用 Django 模板就够了。读者的“我的借阅”页面在视图里取数据时就要把关联表一次性查出来def my_borrows(request): records BorrowRecord.objects.filter(borrowerrequest.user) records records.select_related(book) return render(request, books/my_borrows.html, {records: records})模板里这样渲染!-- books/templates/books/my_borrows.html -- table trth书名/thth借出时间/thth状态/thth操作/th/tr {% for record in records %} tr td{{ record.book.title }}/td td{{ record.borrow_date|date:Y-m-d }}/td td{{ record.get_status_display }}/td td {% if record.status borrowing %} a href{% url books:return_book record.id %}归还/a {% endif %} /td /tr {% endfor %} /tableselect_related(book)在查询阶段做 JOIN模板里每次访问record.book.title不会再发一次 SQL避免“N1 查询”。get_status_display是 choices 字段自带的方法把borrowing转成人话“借出中”。{% url %}用视图名反查地址改 URL 路径时模板不用跟着改。4.3 重定向传递数据redirect、messages 与 URL 参数借书成功后的跳转最稳妥的做法是redirect messages。messages 框架把消息写进 session下一次请求读出来渲染页面刷新后自动清空。如果要在跳转 URL 上带业务参数用它也可以return redirect(books:book_detail, book_idbook.pk)这种写法对应 urls.py 里的int:book_id路由。重定向里带查询串的场景也有比如还书后定位到自己的借阅列表并带个页码return redirect(f{reverse(books:my_borrows)}?page1)用reverse而不是硬编码/borrows/因为 urls.py 里改名以后所有视图不用逐个改。redirect(reverse(...))当目标 URL 里需要参数时reverse可以接 kwargs比字符串拼接安全。写法适用场景说明redirect(books:book_detail, book_idbook.pk)带主键参数的路由视图名反查地址参数自动拼接reverse(books:my_borrows)构造查询串 URL路由改名时不用改视图代码字符串拼接/borrows/?page1不推荐路由一旦调整这里很容易漏改5. 查询、删除对象与并发图书馆管理系统里的硬骨头5.1 借阅相关查询的几种标准写法日常运营里反复要用到几类查询某本书的借阅历史、某读者当前未还列表、超期未还提醒。它们都可以用 ORM 在同一个模型上完成# 某本书的完整借阅历史 book Book.objects.get(pk1) history book.borrowrecord_set.all().select_related(borrower) # 某读者未还记录 borrowing BorrowRecord.objects.filter( borrowerrequest.user, return_date__isnullTrue ) # 从记录反向拿书对象避免再查一次 for rec in borrowing: print(rec.book.title)这些查询的共同点是都用到了外键的“正向/反向”关系。record.book是正向查询book.borrowrecord_set是反向查询反向名可以通过 ForeignKey 的related_name参数改掉例如models.ForeignKey(..., related_nameassets)之后写book.assets.all()。更复杂的聚合放到数据库端查询目标写法关键点统计每个读者当前借了几本BorrowRecord.objects.filter(return_date__isnullTrue).values(borrower__username).annotate(cntCount(id))annotate 分组统计按借阅次数排热门图书BorrowRecord.objects.values(book__title).annotate(cntCount(id)).order_by(-cnt)[:10]注意字段名是 book__title删除对象前查关联BorrowRecord.objects.filter(bookbook).exists()先判断再删5.2 删除对象的三层检查“django 执行查询-删除对象”是这个项目里最容易出事的地方。图书可以删但删除前必须考虑借阅记录。因为 BorrowRecord 的 book 外键设的是PROTECT直接book.delete()会抛 ProtectedError数据库层已经拦住第一道。更完整的做法是三段检查from django.shortcuts import get_object_or_404, redirect def delete_book(request, book_id): book get_object_or_404(Book, pkbook_id) # 第一层有没有未还记录 if book.borrowrecord_set.filter(return_date__isnullTrue).exists(): messages.error(request, 存在未归还的借阅记录不能删除) return redirect(books:book_detail, book_idbook.id) # 第二层有没有历史流水 history_count book.borrowrecord_set.count() if history_count 0: book.is_deleted True # 软删除 book.save(update_fields[is_deleted]) messages.warning(request, 图书已下架) return redirect(books:book_list) # 第三层干净记录才物理删除 book.delete() return redirect(books:book_list)第一层拦截未还记录避免删了一个读者还挂着的书第二层对有过历史流水的书转为软删除把is_deleted置为 True列表页过滤掉即可只有从未被借过的书才真正delete()。这三层对应数据库的完整性问题、历史数据问题和磁盘空间问题优先级从高到低。5.3 并发借书与 select_for_update 的前提图书还剩一本两个读者同时点击借阅传统的“先查 available 再判断”会出现两个请求都读到 1都认为自己能借最后记录两条、库存变成 -1。前面借书视图里的select_for_update()解决的就是这个查询那本书时对数据库行加锁第二个事务必须等第一个事务提交或回滚后才会读到新的库存值。注意三个前提数据库必须支持行锁SQLite 在部分场景下会退化、外部一定要有transaction.atomic、被锁的查询不能再调用.values()或.annotate()否则锁可能不生效。注意SQLite 是文件锁模型select_for_update在高并发下可能退化为整库锁生产环境换 MySQL 或 PostgreSQL。5.4 宝塔部署 django 的稳妥顺序本地跑通之后要部署常见做法是“宝塔面板 gunicorn Nginx”。基本顺序是先装 Python 环境再用pip install gunicorn在项目目录执行gunicorn library.wsgi:application --bind 127.0.0.1:8000 --workers 3然后在宝塔的 Nginx 站点配置里把动态请求反向代理到 8000 端口静态文件交给 Nginx 直接返回。层工具职责静态资源Nginx直接返回 css/js/图片避免 Python 进程处理动态请求gunicorn跑 Django WSGI 应用多 worker 进程数据库SQLite 或 MySQL存业务数据生产环境推荐 MySQL部署时最容易漏的是ALLOWED_HOSTS和STATIC_ROOT前者没配上部署域名会报 Bad Request后者没执行collectstatic页面会没有样式。线上不要用 Django 自带的 runserver。6. 用 Django 测试客户端验证借书还书全流程功能写完最后一步用 Django 测试客户端把整条链路串起来验证。测试客户端可以模拟登录、请求视图、断言响应和数据库状态不依赖浏览器# books/tests.py from django.contrib.auth.models import User from django.test import TestCase from django.urls import reverse from .models import Book, BorrowRecord, Category class BorrowFlowTest(TestCase): def setUp(self): self.user User.objects.create_user(stu01, passwordpass12345) category Category.objects.create(name计算机) self.book Book.objects.create( titleDjango实战, author张三, isbn9787000000001, categorycategory, total1, available1, ) self.client.login(usernamestu01, passwordpass12345) def test_borrow_and_return(self): resp self.client.post(reverse(books:borrow_book, args[self.book.id])) self.assertEqual(resp.status_code, 302) self.book.refresh_from_db() self.assertEqual(self.book.available, 0) self.assertEqual(BorrowRecord.objects.count(), 1) record BorrowRecord.objects.first() resp self.client.post(reverse(books:return_book, args[record.id])) self.assertEqual(resp.status_code, 302) self.book.refresh_from_db() self.assertEqual(self.book.available, 1) record.refresh_from_db() self.assertEqual(record.return_date is not None, True) def test_cannot_borrow_twice(self): self.client.post(reverse(books:borrow_book, args[self.book.id])) book Book.objects.get(pkself.book.id) book.available 0 book.save() resp self.client.post(reverse(books:borrow_book, args[self.book.id])) self.assertEqual(BorrowRecord.objects.filter(return_date__isnullTrue).count(), 1)setUp在每个测试前创建读者、分类、图书并完成登录。第一个断言语义是“借书后跳转成功”第二个断言检查库存从 1 变 0第三个断言检查借阅记录存在。还书后再次刷新数据库断言库存回补到 1状态字段更新。refresh_from_db()是 Django 提供的重读方法因为视图是在数据库端用 F 表达式改的值当前实例里的旧值不会自动同步。跑测试的命令python manage.py test books.tests.BorrowFlowTest -v 2用-v 2能看到每个测试的执行明细。执行完这条命令之后打开 admin 后台再补一次手工借还确认available字段从 0 回到 1这条链路的查询、更新、状态跳转就算全部覆盖到了。本文还有配套的精品资源点击获取