简介:基于 Python + Django + Bootstrap + MySQL 的图书管理系统完整项目源码,面向 Web 开发初学者与相关课程设计者。系统围绕出版社、作者、图书三类核心信息,实现了注册登录、数据增删改查、多条件检索与借阅状态记录,配套界面经 Bootstrap 构建,具备响应式展示效果。资源共 63 个文件,压缩包约 1.33MB,主体为 13 个 py 源码文件,包含 views、models、settings、urls 等后端模块;另有 11 个 html 模板构成页面层,7 个 css、4 个 js 与字体图标文件支撑前端样式,10 张 png 截图可直观了解各功能页面效果。项目目录按 Django 应用拆分,模板与静态资源分离,整体结构清晰,适合用来学习 MTV 模式、ORM 数据库操作及完整项目整合流程。目前已有 323 人学习,适合希望获得可运行参考项目并快速上手 Django 开发的人群。
1. 图书管理系统book_system:Python + Django + bootstrap + MySQL能解决什么问题
很多人第一次碰图书管理系统,会觉得不就是写几个页面吗?真正上手才发现:出版社、作者、图书这三类数据的关系理不顺,后面每一步都在返工。标题里的book_system正是这一类内部系统的典型:实体不多,但增删改查全都要,前端还要能看得过去。用Python的Django做后端,bootstrap做界面,MySQL存数据,是在中小型业务里最稳的组合。这个方向适合谁?适合要做课程设计、给某公司做内部书库、或者刚入门Web开发的读者。接下来我会按一个我实际做过的模拟项目X的流程,从建库到上线检查挨个拆开讲,代码可以直接抄,坑也会单独列出来。
2. 从零搭建Django项目并设计出版社、作者、图书三张核心表
2.1 环境准备与项目初始化:创建虚拟环境和book_system工程
我一般会先建一个独立目录,把项目和环境都隔离在里面。这样做的好处是后面换机器、换版本不会互相污染。下面这段命令在Linux和macOS上通用,Windows把source换成venv\Scripts\activate就行。
mkdir book_system && cd book_system python3 -m venv venv source venv/bin/activate pip install django # 安装当前稳定的Django LTS版本 django-admin startproject book_system . python manage.py startapp books这里第一行的目录名和项目名都叫book_system,容易记。startproject book_system .最后的那个点很重要,表示在当前目录生成manage.py和配置包,而不是再套一层同名目录。startapp books是业务模块,负责出版社、作者、图书的所有模型和视图。装完Django后建议顺手用pip freeze > requirements.txt固定依赖,这个文件后面部署要用的。
接下来先不着急写代码,得确认项目能跑起来。执行python manage.py runserver,浏览器访问本地地址能看到Django的默认启动页,再往下才做配置。如果这一步出现报错,绝大多数是Python环境或端口占用问题,把虚拟环境重新激活、换一个端口再试。
2.2 用Django ORM定义出版社、作者、图书的模型关系
books/models.py是所有业务逻辑的地基。我的习惯是先想清楚实体关系:一个出版社可以出版多本图书,一本图书可以有多个作者,一个作者也可以写多本图书。也就是说出版社和图书是典型的一对多,通过外键ForeignKey实现;作者和图书是多对多,通过ManyToManyField实现。下面是核心模型代码:
# books/models.py from django.db import models class Publisher(models.Model): name = models.CharField('出版社名称', max_length=128, unique=True) address = models.CharField('地址', max_length=255, blank=True) website = models.URLField('官网', blank=True) class Meta: ordering = ['name'] def __str__(self): return self.name class Author(models.Model): name = models.CharField('作者姓名', max_length=64) bio = models.TextField('简介', blank=True) class Meta: ordering = ['name'] def __str__(self): return self.name class Book(models.Model): title = models.CharField('书名', max_length=200) isbn = models.CharField('ISBN', max_length=32, unique=True, db_index=True) publisher = models.ForeignKey(Publisher, on_delete=models.PROTECT, verbose_name='出版社', related_name='books') authors = models.ManyToManyField(Author, verbose_name='作者', related_name='books') pub_date = models.DateField('出版日期', null=True, blank=True) price = models.DecimalField('定价', max_digits=8, decimal_places=2, default=0, db_index=True) class Meta: ordering = ['-pub_date', 'title'] indexes = [ models.Index(fields=['title'], name='book_title_idx'), ] def __str__(self): return self.title字段类型上重点说明几处。isbn用db_index=True,因为图书检索基本靠它和书名。价格为DecimalField而不是FloatField,原因是用浮点数存钱容易出现0.1+0.2的精度偏差,财务相关的数据从模型层就要避免。publisher这个外键用on_delete=models.PROTECT,不是最常用的CASCADE,后面避坑章节会专门讲为什么。related_name='books'让反向查询更顺手,比如拿到一个publisher对象后,publisher.books.all()就是这家出版社出的书。
多对多关系在数据库里会自动生成一张中间表,Django会管好它。你要做的只是在Book上定义authors,然后反向查询时用author.books.all()。Book.title加了db_index吗?没有,但我在Meta里显式建了一个联合场景够用的普通索引。其实CharField的模糊查询不一定走索引,真正搜索优化放到第6章讨论。
2.3 配置MySQL并执行迁移
默认项目用的是SQLite,要切到MySQL需要改settings。先安装驱动,我建议用mysqlclient,它对配置好一点,如果装不上就换pymysql。后者是纯Python实现,省去编译依赖,但要在项目包的__init__.py里做一个适配:
# book_system/__init__.py import pymysql pymysql.install_as_MySQLdb()然后修改settings.py:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'book_system', 'USER': 'root', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", }, } }这里有个容易忽略的点:NAME是MySQL里的数据库名,必须先创建,Django不会帮你建库。进MySQL执行CREATE DATABASE book_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。OPTIONS里写utf8mb4才能存生僻字和特殊符号,只写utf8在MySQL 5.7之后会有警告。init_command里的STRICT_TRANS_TABLES是我个人的偏好,它让不合规数据直接报错而不是被静默截断,开发期能省掉很多排查成本。
配置完成后执行迁移:
python manage.py makemigrations books python manage.py migratemakemigrations会读取models.py并生成一个迁移文件,migrate才会真正把表建到MySQL里。如果中途报错,多半是连接问题,回到上面检查HOST/PORT/用户权限。迁移完成后可以进MySQL里用SHOW TABLES;看一下,至少会有books_publisher、books_author、books_book和自动生成的books_book_authors四张表。
3. 把出版社、作者、图书的增删改查做成可用的功能页
3.1 路由设计与视图函数:先画URL再写处理逻辑
很多初学者一上来就写视图,写完了才发现URL也不知道该叫什么。我习惯先用注释把URL清单列出来,再根据路径做视图。这个系统的核心是四个资源:出版社、作者、图书,以及它们各自的增删改查。我在这里用函数视图演示,因为逻辑直接、适合新手改;要换成类视图也容易,后面6.1会提到。
在book_system/urls.py里先配置一个应用入口:
# book_system/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('books.urls')), ]然后在books/urls.py里写具体的路由。以图书为例:
# books/urls.py from django.urls import path from . import views app_name = 'books' urlpatterns = [ path('books/', views.book_list, name='book_list'), path('books/add/', views.book_create, name='book_create'), path('books/<int:pk>/edit/', views.book_update, name='book_update'), path('books/<int:pk>/delete/', views.book_delete, name='book_delete'), ]<int:pk>是Django的路径转换器,它会把URL里的数字捕获到视图函数的关键字参数pk里。用int而不是字符串,能直接过滤掉非数字的参数,避免视图里再类型判断。视图里做三件事:查询数据库、处理表单、决定跳转。下面是一个列表视图加删除视图的最小实现:
# books/views.py from django.shortcuts import render, get_object_or_404, redirect from django.contrib import messages from .models import Book from .forms import BookForm def book_list(request): books = Book.objects.all().select_related('publisher').prefetch_related('authors') return render(request, 'books/book_list.html', {'books': books}) def book_delete(request, pk): book = get_object_or_404(Book, pk=pk) if request.method == 'POST': book.delete() messages.success(request, '图书已删除') return redirect('books:book_list') return render(request, 'books/book_confirm_delete.html', {'book': book})select_related和prefetch_related一起用,是为了把模型里外键和ManyToMany关联的数据一次性查出来,避免循环里一条一条查数据库。这个在列表页尤其关键,不然30本书会出现几十次SQL查询。
3.2 用ModelForm减少表单校验代码
Django最值得用的就是ModelForm。它可以根据模型自动生成表单字段,自带校验,帮我们把安全性和重复劳动都解决掉。对图书表单,我这样定义:
# books/forms.py from django import forms from .models import Book class BookForm(forms.ModelForm): class Meta: model = Book fields = ['title', 'isbn', 'publisher', 'authors', 'pub_date', 'price'] widgets = { 'pub_date': forms.DateInput(attrs={'type': 'date'}), 'authors': forms.CheckboxSelectMultiple, }fields列表顺序控制了页面显示顺序。pub_date用type:'date'的控件,浏览器直接弹出日期选择器,不用手动引入前端日历库。authors用复选框渲染,比默认的多选框直观。如果想让出版社下拉框有搜索过滤能力,后续可以扩展成一个自定义控件,这里先不展开。
视图里,创建和更新共用同一个表单类,只是处理逻辑略有不同:
# books/views.py def book_create(request): if request.method == 'POST': form = BookForm(request.POST) if form.is_valid(): book = form.save() messages.success(request, '图书创建成功') return redirect('books:book_list') else: form = BookForm() return render(request, 'books/book_form.html', {'form': form}) def book_update(request, pk): book = get_object_or_404(Book, pk=pk) if request.method == 'POST': form = BookForm(request.POST, instance=book) if form.is_valid(): book = form.save() messages.success(request, '图书更新成功') return redirect('books:book_list') else: form = BookForm(instance=book) return render(request, 'books/book_form.html', {'form': form})form.save()在创建时自动执行INSERT,更新时自动执行UPDATE。这里有个关键字参数instance的差别需要记牢:创建时没传,更新时传了现有的对象,ModelForm才知道该新建还是覆盖。模板里渲染表单时,统一用一个循环输出所有字段,样式会在第4章统一处理。
3.3 实现图书列表分页与多条件筛选
如果数据量到了几百条,不分页的列表页根本不能看。Django自带分页组件,我封装成一个小函数:
# books/views.py from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger def _paginate(request, queryset, per_page=10): paginator = Paginator(queryset, per_page) page = request.GET.get('page') try: items = paginator.page(page) except PageNotAnInteger: items = paginator.page(1) except EmptyPage: items = paginator.page(paginator.num_pages) return items然后改造book_list支持搜索参数:
# books/views.py def book_list(request): queryset = Book.objects.select_related('publisher').prefetch_related('authors') keyword = request.GET.get('q', '').strip() pub_id = request.GET.get('publisher', '').strip() if keyword: queryset = queryset.filter(title__icontains=keyword) if pub_id.isdigit(): queryset = queryset.filter(publisher_id=int(pub_id)) books = _paginate(request, queryset) return render(request, 'books/book_list.html', { 'books': books, 'keyword': keyword, 'pub_id': pub_id, })title__icontains会生成LIKE %keyword%,不区分大小写。当搜索条件是数字或URL编码的内容时需要小心SQL注入,但Django的ORM会做参数化,这里可以放心。分页参数用page这个通用名,前端翻页时可以带上其它筛选参数,否则点击第2页后搜索条件就丢了。
4. 用Bootstrap把book_system的前端页面变成能直接用的界面
4.1 集成Bootstrap到Django模板:静态文件路径与CDN选择
接入Bootstrap有两种常见做法:一个是直接在模板里引用CDN,另一个是把Bootstrap的css和js下到本地,走Django静态文件。我推荐内网部署或系统开发用本地文件,避免生产环境没有外网时页面裸奔。先在settings.py里配好静态目录:
# settings.py STATIC_URL = 'static/' STATICFILES_DIRS = [ BASE_DIR / 'static', ]在项目根目录建static/,把bootstrap的css/bootstrap.min.css和js/bootstrap.bundle.min.js放进去。模板里的引用方式要写成Django模板标签,不要写死路径:
{% load static %} <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> <script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>{% static %}会按STATIC_URL拼出正确URL。这样做的另一个好处是部署时文件带版本指纹,浏览器缓存不容易失效。不要直接写href="/static/...",一旦你改了STATIC_URL就全挂。
4.2 用Bootstrap表格、表单和模态框提升操作体验
列表页我通常会先搭一个带导航的框架,再塞表格。表格用table table-striped table-hover,操作列放编辑和删除按钮。核心代码片段:
{% load static %} <table class="table table-striped table-hover align-middle"> <thead> <tr> <th>书名</th><th>ISBN</th><th>出版社</th><th>作者</th><th>价格</th><th>操作</th> </tr> </thead> <tbody> {% for book in books %} <tr> <td>{{ book.title }}</td> <td>{{ book.isbn }}</td> <td>{{ book.publisher.name }}</td> <td>{% for author in book.authors.all %}{{ author.name }}{% if not forloop.last %}, {% endif %}{% endfor %}</td> <td>{{ book.price }}</td> <td> <a class="btn btn-sm btn-outline-primary" href="{% url 'books:book_update' book.pk %}">编辑</a> <a class="btn btn-sm btn-outline-danger" href="{% url 'books:book_delete' book.pk %}">删除</a> </td> </tr> {% empty %} <tr><td colspan="6" class="text-center">暂无图书</td></tr> {% endfor %} </tbody> </table>表单页面用Bootstrap的栅格和表单类来做。我倾向于把字段包装成统一结构,避免每个页面重复写:
<form method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="mb-3"> {{ field.label_tag }} {{ field }} {% if field.errors %} <div class="text-danger">{{ field.errors }}</div> {% endif %} </div> {% endfor %} <button type="submit" class="btn btn-primary">保存</button> <a class="btn btn-secondary" href="{% url 'books:book_list' %}">取消</a> </form>注意这里给{{ field }}加一个form-control类会让输入框统一宽度和圆角,我一般通过ModelForm的widget参数统一设置'class': 'form-control'。如果直接在模板里用{{ field }},Bootstrap样式不会自动生效,页面会很丑。
4.3 模板继承与分页组件的复用
不要在每个页面里复制导航栏和CSS链接,用模板继承把它们收敛到一个base.html。大致结构如下:
<!-- templates/base.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{% block title %}图书管理系统{% endblock %}</title> {% load static %} <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container-fluid"> <span class="navbar-brand">book_system</span> <div class="navbar-nav"> <a class="nav-link" href="{% url 'books:book_list' %}">图书</a> <a class="nav-link" href="{% url 'books:publisher_list' %}">出版社</a> <a class="nav-link" href="{% url 'books:author_list' %}">作者</a> </div> </div> </nav> <main class="container mt-3"> {% if messages %} {% for message in messages %} <div class="alert alert-info">{{ message }}</div> {% endfor %} {% endif %} {% block content %}{% endblock %} </main> <script src="{% static 'js/bootstrap.bundle.min.js' %}"></script> </body> </html>子模板只需写{% extends 'base.html' %}并把内容放进{% block content %}里。分页部分也可以抽成一个_pagination.html片段,然后在列表模板里{% include 'books/_pagination.html' %}。这样做的好处是导航、消息提示、分页样式只需要维护一份,新增出版社列表和作者列表时基本是复制改查。
5. book_system开发中的5个常见问题排查与避坑
5.1 连接MySQL报错:驱动缺失与连接参数不匹配
做这个模拟项目X时,我把book_system的settings配好后runserver,结果启动页面报错:No module named 'MySQLdb',随后数据库连接失败。原因是我只pip安装了pymysql,却忘了在__init__.py里执行install_as_MySQLdb(),Django按默认的MySQLdb来找驱动,自然找不到。解决方法是补上前面2.3节那段适配代码。另一个同族坑是NAME对应的数据库创建时用了默认字符集,导致中文写入后变成问号,需要确保建库语句用utf8mb4。排查时先看控制台完整报错,再进MySQL执行SELECT * FROM book_book;看数据真实编码。
5.2 静态文件404,页面变成纯文本
我第一次接入Bootstrap时,页面能打开但完全没有样式,F12里css文件状态码404。原因有两个:一是STATICFILES_DIRS配错了路径,Django找不到Bootstrap文件;二是部署环境里DEBUG=False后runserver不再处理静态文件。开发期解决方案是检查BASE_DIR / 'static'目录是否存在,以及模板里用的{% static %}标签是否正确;部署期老老实实执行python manage.py collectstatic并把静态目录交给Nginx处理。这里最玄学的地方是浏览器缓存,改了css后页面没变,先Ctrl+Shift+R强制刷新再排查。
5.3 表单提交报403 CSRF验证失败
这是新手最常见的问题。现象是填写表单点保存,页面跳转到403。原因很直白:Django默认开启CSRF防护,模板里的表单没有输出{% csrf_token %},导致POST请求验证失败。解决方式是在所有<form>标签内、提交按钮之前加一句{% csrf_token %}。如果你用AJAX提交数据,需要从cookie里读取csrftoken并在请求头带上。这个防护别为了省事关掉,否则系统会处于风险之中。
5.4 删除作者时图书被连带删除
我在做作者删除功能时,测试发现删掉一个作者后某本图书也跟着没了。追查代码,原来图书和出版社的外键用了on_delete=models.CASCADE,出版社被删时所有图书被级联删除。而作者那块是ManyToManyField,不会删掉Book记录,但出版社删除确实是真实存在的风险。解决方式是按业务语义调整on_delete:图书是业务核心,不应因为出版社被删就消失,所以改成on_delete=models.PROTECT,让系统有外键约束,想删必须先处理关联图书。这个教训让我明白,外键的on_delete不是随便选的,它直接决定数据完整性。
5.5 多对多关系保存后作者列表为空
用ModelForm做到后面时,我以为form.save()会自动把作者写进关联表,结果创建图书后,详情页里作者是空的。原因在于ModelForm对ManyToManyField的处理分两步:第一步先保存主表,第二步才保存多对多关联数据。如果你在保存后又手动改了对象属性,或者图省事调了两次save(),很容易把关系丢在中间。解决方法是改用下面这种标准写法:
if form.is_valid(): book = form.save(commit=False) # 这里可以对 book 做额外赋值,例如 created_by book.save() form.save_m2m()commit=False让ModelForm先不写库,等你补完字段再save()主记录,最后用save_m2m()把多对多关系写进中间表。记住:如果列表页或详情页展示作者时发现为空,优先怀疑是不是漏了这条。
6. 最后一步:给book_system加权限、搜索优化与部署前检查
6.1 用Django内置权限做基于角色的访问控制
图书管理系统的增删改查不能对所有登录用户放开。Django自带的auth应用已经提供了用户、分组和三个基础权限,我通常直接在视图上套装饰器。例如:
# books/views.py from django.contrib.auth.decorators import login_required, permission_required @login_required @permission_required('books.delete_book', raise_exception=True) def book_delete(request, pk): ...这样普通员工登录后只能看列表,有权限的编辑才能删。如果你需要更细的“出版社只能看自己的书”,就得在视图里加一层request.user与出版社的关联判断,这里不展开。
6.2 搜索模块的查询优化与索引使用
图书列表页的搜索如果数据量大了,title__icontains会变成LIKE '%关键词%',普通B+树索引基本用不上。我会做两件事:一是把两个固定长度且高频查询的字段用db_index=True,例如ISBN;二是把列表查询的select_related和prefetch_related写齐,避免N+1查询。更进阶的办法是用MySQL的全文索引,但ORM写法会变复杂,日常中小规模系统先不做。
6.3 部署前的检查清单
| 检查项 | 期望结果 | 常见问题 |
|---|---|---|
python manage.py check --deploy | 无红级警告 | DEBUG未关闭、ALLOWED_HOSTS为空 |
python manage.py collectstatic | 静态文件收集到staticfiles | STATIC_ROOT未设置 |
python manage.py migrate | 数据库迁移全部执行 | 漏了哪次迁移、MySQL权限不足 |
| 表单提交与删除操作 | 不报错、有消息提示 | CSRF、级联删除、多对多保存遗漏 |
| 分页与搜索组合 | 翻页后筛选条件仍在 | 前端链接未带原有GET参数 |
当年我把这个系统部署到生产环境前,就是因为没检查ALLOWED_HOSTS,结果访问被拒,排查了半个小时。后来我养成了一个习惯:每次上线前先把上面的清单过一遍,瓶瓶罐罐的都列成命令,跑完再发布。这也是我今天还在用的方法。希望帮到你。
本文还有配套的精品资源,点击获取