简介:基于Django与MySQL的图书管理系统源码包,面向需要课程设计、毕业设计或实际项目参考的Web开发者,提供了从数据库设计到前端交互的完整实现方案。系统涵盖图书增删改查、批量入库、多条件排序、借阅续借及归还、用户注册登录等核心功能,结合Django自带的认证机制确保操作安全,能帮助读者深入理解MVT架构、ORM映射、模板渲染等关键技术。压缩包共81个文件,既包含22个Python源码文件用于业务逻辑与配置,也有16个HTML模板负责页面展示,4个JavaScript与4个CSS文件优化交互样式,另附项目说明docx与README等文档,整体大小2.71MB,目录层次清晰,方便按功能模块专项阅读。目前已有187人学习下载,参考者可获得可直接运行的系统源码、数据库交互示例、部署配置详解,并可作为二次开发的基础项目使用。
1. 为什么图书管理系统是Django+MySQL最合适的入门战场
图书管理系统的业务边界足够清晰,却又覆盖了一整套 Web 项目必须面对的完整链路:数据建模、ORM 操作、用户交互、借阅状态流转、权限控制、部署上线。用 Django 做后端,MySQL 做持久化,正好把“框架怎么管数据”和“数据库怎么存数据”之间的接缝暴露得明明白白。新手可以通过它理解 MVT 和关系型数据库的配合方式,老手则能在借阅并发、索引优化、表单校验这些点上重新审视自己的习惯。这个标题看起来像课程作业,实际是检验一个人能否把 Django 的 ORM 和 MySQL 的表结构设计串起来的最低成本载体。你不需要理解分布式,不需要微服务,只需要一台能跑 Python 和 MySQL 的机器。
2. 设计与建模:把图书管理系统拆成 Django 模型和 MySQL 表
2.1 数据模型设计:图书、读者、借阅记录的关系
一个图书管理系统的核心实体是图书、读者和借阅记录。绝大多数“基于 Web 的图书管理系统”都逃不开这三张表,差别只在于读者是否复用 Django 自带的 User 模型。常见的做法是新建一个 Profile 关联 User,或者直接自己建 Reader 表,便于记录学号/工号和借阅额度。
我一般会这样设计:
| 模型 | 关键字段 | 说明 |
|---|---|---|
| Book | title, author, isbn, total_count, available_count | available_count 是可用副本数,而不是查询时动态计算,避免借出后不好统计 |
| Reader | user(OneToOne), student_id, max_borrow | 关联 Django 用户,同时保存业务编号 |
| BorrowRecord | book, reader, borrow_date, due_date, return_date | return_date 为 NULL 表示未归还 |
为什么要把 available_count 冗余在 Book 表里?因为在借阅事务里,你需要一个原子操作来扣减存量。如果每次都 COUNT(BorrowRecord),在高并发下很容易出现超借。冗余字段配合 SELECT FOR UPDATE 或者F()表达式,能在单库场景下把冲突概率降到最低。
2.1.1 模型代码示例
from django.db import models from django.contrib.auth.models import User class Book(models.Model): title = models.CharField(max_length=200, db_index=True) author = models.CharField(max_length=100) isbn = models.CharField(max_length=20, unique=True, null=True) total_count = models.PositiveIntegerField(default=1) available_count = models.PositiveIntegerField(default=1) class Meta: ordering = ['title'] class Reader(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) student_id = models.CharField(max_length=20, unique=True) max_borrow = models.PositiveIntegerField(default=5) class BorrowRecord(models.Model): book = models.ForeignKey(Book, on_delete=models.PROTECT) reader = models.ForeignKey(Reader, on_delete=models.CASCADE) borrow_date = models.DateField(auto_now_add=True) due_date = models.DateField() return_date = models.DateField(null=True, blank=True) def is_returned(self): return self.return_date is not None逻辑说明:Book.available_count不是派生字段,而是业务冗余,必须在借书和还书事务里手动维护。BorrowRecord.book使用PROTECT,防止借阅记录存在时删除图书,导致历史数据悬空。one_to_one字段在点击回收站图标把整个用户删除时,会把关联 Reader 一起删掉,这是常见预期,但如果你要保留读者历史,就需要改成外键并加related_name。
参数说明:db_index=True告诉 MySQL 为 title 创建普通索引,unique=True会为 isbn 创建唯一索引。注意 unique 同时隐含db_index=True,不要重复写。max_length必须显式指定,否则 MySQL 会报错,因为 Django 对 CharField 默认长度是 255,但显式写出来能提醒你业务意义。
2.2 Django 模型到 MySQL 的迁移过程
模型定义后,真正让 MySQL 出现表的是 Django 的迁移系统。这个过程有三步:makemigrations、migrate和sqlmigrate。其中sqlmigrate不执行 SQL,而是打印将要执行的语句,很适合在敏感环境里确认操作。
python manage.py makemigrations library python manage.py sqlmigrate library 0001 python manage.py migrate第一行根据library应用下的 models.py 生成迁移文件,不连数据库,因此语法错误会在这一层暴露。第二行把 0001 迁移转成 MySQL 的 DDL,你可以检查索引名、外键名和字符集是否带utf8mb4。第三行真正执行。如果迁移卡住,先看 MySQL 是否有长事务锁住了django_migrations表,而不是盲目删表。
另外,如果你的项目之前用的 SQLite,后来换成 MySQL,常见做法是清空所有迁移记录、保留模型定义,然后重新生成初始迁移。千万不要以为直接改 DATABASES 就完事,因为内置的 auth 迁移和你的业务迁移之间有依赖顺序,依赖表如果已经存在,migrate 会跳过造成不一致。
2.3 模型里容易被忽略的参数:on_delete、db_index、unique
这几个参数决定数据完整性,也直接影响 MySQL 的行为。很多人只记得on_delete=models.CASCADE,但图书管理系统里,删除一个读者不应该把他的借阅历史清空。PROTECT会阻止删除,SET_NULL需要外键字段设为null=True。下面是三种常见场景:
- 删除图书时,借阅记录应该被阻止:用
PROTECT - 删除读者时,保留历史借阅但不再关联用户:把外键改成
SET_NULL并允许空 - 删除用户时,读者档案和借阅记录全部清理:用
CASCADE
db_index要加在where和join频繁出现的字段上,但不要给所有字段都加,因为 MySQL 的联合索引可以覆盖多个查询路径。比如借阅记录经常按book_id和return_date查询,那么应该在模型里声明Meta.indexes,而不是分别加两个单列索引。
class Meta: indexes = [ models.Index(fields=['book', 'return_date'], name='idx_book_return'), ]这样生成的 MySQL 索引名为idx_book_return,覆盖“查某本书所有未还记录”和“查某本书所有历史记录”两类查询。注意顺序:等值查询字段放前面,范围查询字段放后面,这是 MySQL 最基础的原则。
3. 环境搭建与数据库连接:让 Django 和 MySQL 实际跑通
3.1 mysqlclient 安装的常见坑与 Python 版本对应
Django 连接 MySQL 最常见的驱动是mysqlclient,它比pymysql的性能更接近 C 客户端,且支持较新的 MySQL 8.0 认证插件。安装时最常见的坑是缺少系统依赖,导致pip install mysqlclient编译失败。在 Debian/Ubuntu 上需要先装libmysqlclient-dev,在 CentOS 上是mysql-devel,Windows 则建议直接下载预编译的 wheel 文件。
sudo apt install default-libmysqlclient-dev -y pip install mysqlclient如果你的项目用的是 Python 3.12 以上,官方 wheel 可能还没覆盖当前版本,这时可以退一步用pymysql,但需要在__init__.py里加上pymysql.install_as_MySQLdb()。我一般推荐团队统一 Python 3.10 或 3.11,这部分 Python 版本相关的问题会少很多。
mysqlclient安装成功后,import MySQLdb不会报错。如果仍然报ModuleNotFoundError,检查当前终端激活的是不是同一个虚拟环境。很多人在全局环境和 venv 之间反复切换,导致驱装置装到了另一个环境里。
3.2 settings.py 中 DATABASES 配置参数详解
Django 的 DATABASES 配置不止 host、port、user、password。实际生产环境还需要考虑CONN_MAX_AGE、OPTIONS和TIME_ZONE。下面是一份可直接抄的配置:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'library', 'USER': 'library_user', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'CONN_MAX_AGE': 60, 'OPTIONS': { 'charset': 'utf8mb4', 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", 'connect_timeout': 5, }, } }说明:CONN_MAX_AGE表示连接复用 60 秒,避免每次请求都建立新 TCP 连接,这对 MySQL 的max_connections压力改善明显。charset必须设成utf8mb4,否则 emoji 和生僻字会存不进去。init_command里的 sql_mode 会让插入超长字符串时直接报错,而不是被 MySQL 静默截断,开发阶段能帮你尽早发现问题。
connect_timeout很短,是为了在 MySQL 宕机时让 Django 请求快速失败,而不是占用进程等待默认的 30 秒。如果你用了连接池中间件,这里的CONN_MAX_AGE一般设成 0,因为连接池自己管理生命周期。
3.3 用 Django 原生 ORM 执行图书查询与删除对象
连接数据库后,最应该掌握的是 ORM 的惰性查询和删除行为。图书管理系统里,“删除对象”有几种不同写法,效果差别很大。直接objects.all().delete()会返回删除条数,而objects.filter(...).delete()只删除筛选结果。
# 查询所有可用图书 available_books = Book.objects.filter(available_count__gt=0) # 精确查询 ISBN,没有则抛出异常 book = Book.objects.get(isbn='9787111213826') # 删除单个对象 book.delete() # 批量删除可用数量为 0 的图书,注意会触发数据库级 DELETE deleted_count = Book.objects.filter(available_count=0).delete()这里最容易踩的坑是:delete()返回的是一个元组,第一个值是删除总数,第二个值是每个模型删除数量的字典。如果你在代码里只取deleted_count = Book.objects.filter(...).delete(),拿到的是一个(total, dict)结构,而不是整数。正确写法是total, details = Book.objects.filter(...).delete()。
删除图书时,外键字段如果是默认的CASCADE,关联的 BorrowRecord 会被连带删除,这通常是业务事故。在 2.1 里我们把 BorrowRecord.book 设为PROTECT,所以删除动作会抛出ProtectedError。你需要捕获这个异常,提示“该书有借阅记录,无法删除”。
4. 图书管理系统的核心功能实现:增删改查与借阅闭环
4.1 基于类的视图和表单实现图书录入与编辑
图书管理系统的增删改查用FormView或CreateView都可以。我更习惯用CreateView和UpdateView组合,配合 Django 的 ModelForm,能把字段校验和数据库写入绑成一条链。下面是一个典型实现:
# forms.py from django import forms from .models import Book class BookForm(forms.ModelForm): class Meta: model = Book fields = ['title', 'author', 'isbn', 'total_count', 'available_count'] widgets = { 'isbn': forms.TextInput(attrs={'placeholder': '可选'}) } def clean_isbn(self): isbn = self.cleaned_data.get('isbn') if isbn and Book.objects.filter(isbn=isbn).exclude(pk=self.instance.pk).exists(): raise forms.ValidationError('该 ISBN 已经存在') return isbn# views.py from django.urls import reverse_lazy from django.views.generic import CreateView, UpdateView from .models import Book from .forms import BookForm class BookCreateView(CreateView): model = Book form_class = BookForm template_name = 'library/book_form.html' success_url = reverse_lazy('book_list') class BookUpdateView(UpdateView): model = Book form_class = BookForm template_name = 'library/book_form.html' success_url = reverse_lazy('book_list')逻辑说明:BookForm里重写了clean_isbn,在exclude(pk=self.instance.pk)的前提下做唯一性校验,这样编辑时不会因为 ISBN 没有变化而报错。CreateView和UpdateView共用同一个模板,Django 会自动传入form和object,编辑时object有值,创建时为空,这不影响表单渲染。
参数说明:reverse_lazy是给类视图使用的,因为类属性在模块导入时就会计算,使用reverse会导致 URLConf 尚未加载而报错。fields里没有available_count,因为它是业务状态,不应该由手动录入。管理员可以在 admin 后台调整,但普通表单不能碰。
4.2 借阅/归还功能的事务处理
借书动作包含两个操作:在 BorrowRecord 中插入一条记录,并把 Book.available_count 减 1。这两个操作必须在一个数据库事务里执行,否则如果插入成功但扣减失败,数据就会不一致。Django 的transaction.atomic()是解决这个问题的最轻量方案。
from django.db import transaction from django.utils import timezone from datetime import timedelta from .models import Book, BorrowRecord, Reader def borrow_book(request, book_id): reader = Reader.objects.select_for_update().get(user=request.user) if reader.max_borrow <= reader.borrowrecord_set.filter(return_date__isnull=True).count(): return JsonResponse({'error': '超过可借数量'}, status=400) with transaction.atomic(): book = Book.objects.select_for_update().get(pk=book_id) if book.available_count <= 0: return JsonResponse({'error': '库存不足'}, status=400) book.available_count = book.available_count - 1 book.save(update_fields=['available_count']) BorrowRecord.objects.create( book=book, reader=reader, due_date=timezone.now().date() + timedelta(days=30) ) return JsonResponse({'status': 'ok'})说明:select_for_update()会对 MySQL 的行级锁,两个并发请求同时借同一本书时,后一个会阻塞到前一个事务提交,然后重新读取到新的available_count,从而避免超借。save(update_fields=[...])只更新该字段,在 MySQL 里生成的 SQL 只有available_count = ...,减少锁范围。
注意:transaction.atomic()里的return并不是直接提交,而是会先退出atomic代码块,此时如果前面的操作都没有异常,才会提交。上面的代码把库存检查和插入放在同一个事务里,但return JsonResponse本身不影响事务状态。如果你在事务里捕获了异常但不重新抛出,Django 会强制回滚,这点要看懂。
归还功能更简单:把对应 BorrowRecord 的return_date设为今天,同时把 Book.atomic_count + 1。归还不要重新启用select_for_update,
因为可能跨很久,但为了安全,最好仍然在事务里做,并检查return_date是否为空,防止重复归还。
4.3 Django admin 界面美化与列表定制
admin 是图书管理系统展示给管理员的核心操作界面。默认情况下表格很简陋,只显示__str__的结果,搜索和筛选都没有。在 admin.py 里做以下配置,能立刻把可用性提升一个量级:
from django.contrib import admin from .models import Book, Reader, BorrowRecord @admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display = ['id', 'title', 'author', 'isbn', 'total_count', 'available_count'] search_fields = ['title', 'author', 'isbn'] list_filter = ['author'] list_editable = ['available_count'] ordering = ['-id'] readonly_fields = ['id'] @admin.register(BorrowRecord) class BorrowRecordAdmin(admin.ModelAdmin): list_display = ['id', 'book', 'reader', 'borrow_date', 'due_date', 'return_date'] list_filter = ['return_date'] date_hierarchy = 'borrow_date'list_display 里使用模型字段直接展示,如果有is_returned这样的方法,也可以加进去,但注意方法不能接受参数。list_editable 允许在列表页直接修改 available_count,省去进入详情页的点击。date_hierarchy 会在列表页生成按月份快速导航的日历条,对借阅记录这种带日期字段的表很有用。
如果你希望 admin 导航栏皮肤更好看,可以使用 django-admin-interface 库,但注意它需要放在 Django 的 INSTALLED_APPS 首位。对图书管理系统来说,默认 admin 的定制已经完全够用,过度美化反而耽误时间。
5. 部署与优化:宝塔部署 Django 项目和 MySQL 索引调优
5.1 宝塔面板部署 Django 项目的完整步骤
宝塔面板是目前在 VPS 上部署 Django 项目最常用的图形化方式,尤其适合小型团队。整个流程可以归纳为:创建站点、装 Python 环境、配 gunicorn、绑定 MySQL 库。下面是一套我在 Ubuntu 22.04 上常用的操作组合。
# 1. 在宝塔文件管理里上传代码并创建虚拟环境 cd /www/wwwroot/library python3 -m venv venv source venv/bin/activate pip install -r requirements.txt # 2. 收集静态文件 python manage.py collectstatic --noinput # 3. 用 gunicorn 启动 gunicorn library.wsgi:application \ --bind 127.0.0.1:8001 \ --workers 3 \ --threads 2 \ --timeout 60逻辑说明:绑定地址是127.0.0.1,因为面板的 Nginx 会监听公网端口并把请求转发到8001这个内部端口。workers 数量通常按 CPU 核心数 * 2 + 1 来定,但这只是一个经验值。timeout 60表示超过 60 秒未完成的请求会被重试,对借书事务来说不应该超时,但如果 MySQL 有锁等待,这个值可以放宽到 120。
然后到宝塔面板的“网站”里新建一个反向代理站点:域名或 IP 端口 80,目标 URL 为http://127.0.0.1:8001,并开启proxy_set_header Host $host。这一步直接决定 Django 的ALLOWED_HOSTS是否需要包含你的域名。如果忘了配置,Django 会返回Bad Request (400)。
根目录下的静态文件入口不要指向 Django 的/static/,而是到宝塔面板文件中创建软链,指向collectstatic的输出目录。生产效率最高的是直接用面板自带的“自定义配置”粘贴 Nginx 伪静态规则,把静态请求直接交给 Nginx,把其他请求交给 gunicorn。
5.2 MySQL 连接池与查询优化参数
MySQL 默认的单连接开销不小,Django 的CONN_MAX_AGE只能复用同一进程内连接。当使用 gunicorn 多进程时,每个 worker 都维护自己的连接池。为了减少 3306 端口握手次数,可以在 MySQL 端调整部分参数。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| max_connections | 150~300 | 按 worker 数和并发数估算 |
| wait_timeout | 60 | 空闲连接秒数,避免占用 |
| innodb_buffer_pool_size | 内存的 60% | 缓存表数据与索引 |
| max_allowed_packet | 64M | 防止大字段写入失败 |
| log_bin_trust_function_creators | 1 | 开启 binlog 时避免导入存储过程报错 |
在 Django 应用层,借用django-db-connection-pool是很多生产项目的选择,但它需要在 wsgi 启动时替换数据库后端。对于图书管理系统这种并发量不高的场景,我更建议先调CONN_MAX_AGE和 MySQL 的max_connections,不要过早引入连接池中间件,因为中间件的连接回收和事务交互会引入额外复杂度。
索引优化方面,除了前面设置的idx_book_return,借阅记录经常按due_date筛选“即将到期未还”的图书,所以再给due_date加单列索引用处很大。在迁移文件里新增:
operations = [ migrations.RunSQL( 'ALTER TABLE library_borrowrecord ADD INDEX idx_due_date (due_date)', reverse_sql='ALTER TABLE library_borrowrecord DROP INDEX idx_due_date' ) ]RunSQL的好处是手动控制索引名,避免 Django 自动名后缀改变。注意在borrow_date上已经有date_hierarchy使用的索引,不要重复加。
5.3 验证系统的可用性与备份策略
部署完成后需要验证两件事:首页是否能打开、借书功能在 MySQL 重启后是否还健壮。Django 自带的check命令可以快速检查数据库连接和应用配置:
python manage.py check --deploy mysql -ulibrary_user -p -e "SHOW TABLES;" librarycheck --deploy会提示比如 DEBUG 是否开启、静态文件是否配置正确。如果它报安全问题但不影响业务,可以逐条核对。第二条命令直接确认表是否存在。
备份策略上,图书管理系统的数据总量不会很大,用 mysqldump 每天全量备份即可。备份脚本放在宝塔计划任务中,每天凌晨 3 点执行,并保留最近 7 天的文件。
mysqldump -ulibrary_user -p'password' --single-transaction --routines library | gzip > /backup/library_$(date +\%Y\%m\%d).sql.gz--single-transaction可以在不锁表的情况下备份 InnoDB 数据,避免备份期间借书请求被阻塞。恢复时先建库,再用 gunzip 管道导入,注意不要使用 root 账号,单独建一个带SELECT, LOCK TABLES权限的备份账号会让权限更清楚。验证备份可用性的最佳技巧是固定每周在临时库里恢复一次,并用 Django 的showmigrations确认数据可以正常迁移。
本文还有配套的精品资源,点击获取