Django+Bootstrap+MySQL图书管理系统开发实战

📅 发布时间:2026/10/15 13:28:59
Django+Bootstrap+MySQL图书管理系统开发实战
简介基于 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_systemPython 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_length128, uniqueTrue) address models.CharField(地址, max_length255, blankTrue) website models.URLField(官网, blankTrue) class Meta: ordering [name] def __str__(self): return self.name class Author(models.Model): name models.CharField(作者姓名, max_length64) bio models.TextField(简介, blankTrue) class Meta: ordering [name] def __str__(self): return self.name class Book(models.Model): title models.CharField(书名, max_length200) isbn models.CharField(ISBN, max_length32, uniqueTrue, db_indexTrue) publisher models.ForeignKey(Publisher, on_deletemodels.PROTECT, verbose_name出版社, related_namebooks) authors models.ManyToManyField(Author, verbose_name作者, related_namebooks) pub_date models.DateField(出版日期, nullTrue, blankTrue) price models.DecimalField(定价, max_digits8, decimal_places2, default0, db_indexTrue) class Meta: ordering [-pub_date, title] indexes [ models.Index(fields[title], namebook_title_idx), ] def __str__(self): return self.title字段类型上重点说明几处。isbn用db_indexTrue因为图书检索基本靠它和书名。价格为DecimalField而不是FloatField原因是用浮点数存钱容易出现0.10.2的精度偏差财务相关的数据从模型层就要避免。publisher这个外键用on_deletemodels.PROTECT不是最常用的CASCADE后面避坑章节会专门讲为什么。related_namebooks让反向查询更顺手比如拿到一个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.pyDATABASES { 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_modeSTRICT_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, namebook_list), path(books/add/, views.book_create, namebook_create), path(books/int:pk/edit/, views.book_update, namebook_update), path(books/int:pk/delete/, views.book_delete, namebook_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, pkpk) 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, pkpk) if request.method POST: form BookForm(request.POST, instancebook) if form.is_valid(): book form.save() messages.success(request, 图书更新成功) return redirect(books:book_list) else: form BookForm(instancebook) 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_page10): 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__icontainskeyword) if pub_id.isdigit(): queryset queryset.filter(publisher_idint(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 relstylesheet 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 classtable table-striped table-hover align-middle thead tr th书名/ththISBN/thth出版社/thth作者/thth价格/thth操作/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 classbtn btn-sm btn-outline-primary href{% url books:book_update book.pk %}编辑/a a classbtn btn-sm btn-outline-danger href{% url books:book_delete book.pk %}删除/a /td /tr {% empty %} trtd colspan6 classtext-center暂无图书/td/tr {% endfor %} /tbody /table表单页面用Bootstrap的栅格和表单类来做。我倾向于把字段包装成统一结构避免每个页面重复写form methodpost novalidate {% csrf_token %} {% for field in form %} div classmb-3 {{ field.label_tag }} {{ field }} {% if field.errors %} div classtext-danger{{ field.errors }}/div {% endif %} /div {% endfor %} button typesubmit classbtn btn-primary保存/button a classbtn 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 langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1 title{% block title %}图书管理系统{% endblock %}/title {% load static %} link relstylesheet href{% static css/bootstrap.min.css %} /head body nav classnavbar navbar-expand-lg navbar-dark bg-dark div classcontainer-fluid span classnavbar-brandbook_system/span div classnavbar-nav a classnav-link href{% url books:book_list %}图书/a a classnav-link href{% url books:publisher_list %}出版社/a a classnav-link href{% url books:author_list %}作者/a /div /div /nav main classcontainer mt-3 {% if messages %} {% for message in messages %} div classalert 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文件二是部署环境里DEBUGFalse后runserver不再处理静态文件。开发期解决方案是检查BASE_DIR / static目录是否存在以及模板里用的{% static %}标签是否正确部署期老老实实执行python manage.py collectstatic并把静态目录交给Nginx处理。这里最玄学的地方是浏览器缓存改了css后页面没变先CtrlShiftR强制刷新再排查。5.3 表单提交报403 CSRF验证失败这是新手最常见的问题。现象是填写表单点保存页面跳转到403。原因很直白Django默认开启CSRF防护模板里的表单没有输出{% csrf_token %}导致POST请求验证失败。解决方式是在所有form标签内、提交按钮之前加一句{% csrf_token %}。如果你用AJAX提交数据需要从cookie里读取csrftoken并在请求头带上。这个防护别为了省事关掉否则系统会处于风险之中。5.4 删除作者时图书被连带删除我在做作者删除功能时测试发现删掉一个作者后某本图书也跟着没了。追查代码原来图书和出版社的外键用了on_deletemodels.CASCADE出版社被删时所有图书被级联删除。而作者那块是ManyToManyField不会删掉Book记录但出版社删除确实是真实存在的风险。解决方式是按业务语义调整on_delete图书是业务核心不应因为出版社被删就消失所以改成on_deletemodels.PROTECT让系统有外键约束想删必须先处理关联图书。这个教训让我明白外键的on_delete不是随便选的它直接决定数据完整性。5.5 多对多关系保存后作者列表为空用ModelForm做到后面时我以为form.save()会自动把作者写进关联表结果创建图书后详情页里作者是空的。原因在于ModelForm对ManyToManyField的处理分两步第一步先保存主表第二步才保存多对多关联数据。如果你在保存后又手动改了对象属性或者图省事调了两次save()很容易把关系丢在中间。解决方法是改用下面这种标准写法if form.is_valid(): book form.save(commitFalse) # 这里可以对 book 做额外赋值例如 created_by book.save() form.save_m2m()commitFalse让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_exceptionTrue) def book_delete(request, pk): ...这样普通员工登录后只能看列表有权限的编辑才能删。如果你需要更细的“出版社只能看自己的书”就得在视图里加一层request.user与出版社的关联判断这里不展开。6.2 搜索模块的查询优化与索引使用图书列表页的搜索如果数据量大了title__icontains会变成LIKE %关键词%普通B树索引基本用不上。我会做两件事一是把两个固定长度且高频查询的字段用db_indexTrue例如ISBN二是把列表查询的select_related和prefetch_related写齐避免N1查询。更进阶的办法是用MySQL的全文索引但ORM写法会变复杂日常中小规模系统先不做。6.3 部署前的检查清单检查项期望结果常见问题python manage.py check --deploy无红级警告DEBUG未关闭、ALLOWED_HOSTS为空python manage.py collectstatic静态文件收集到staticfilesSTATIC_ROOT未设置python manage.py migrate数据库迁移全部执行漏了哪次迁移、MySQL权限不足表单提交与删除操作不报错、有消息提示CSRF、级联删除、多对多保存遗漏分页与搜索组合翻页后筛选条件仍在前端链接未带原有GET参数当年我把这个系统部署到生产环境前就是因为没检查ALLOWED_HOSTS结果访问被拒排查了半个小时。后来我养成了一个习惯每次上线前先把上面的清单过一遍瓶瓶罐罐的都列成命令跑完再发布。这也是我今天还在用的方法。希望帮到你。本文还有配套的精品资源点击获取