ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django图书管理系统实战:从MySQL配置到借阅逻辑的避坑指南

Django图书管理系统实战:从MySQL配置到借阅逻辑的避坑指南 简介这是一套面向Python Web开发初学者与课程设计需求的图书管理系统完整源码基于Django框架与MySQL数据库实现适合作为毕业设计、实训项目或自学Django与数据库结合的练手案例。压缩包共2000个文件以1609个js脚本、271个html页面、50个css样式及少量json、py、xml配置为主整体约5.79MB涵盖前端界面资源与后端项目代码并附带独立的MySQL数据库文件。系统按模块化思路组织包含图书信息管理、用户注册登录与权限分配、借书还书及借阅记录查询等核心功能数据库负责存储图书、用户与借阅数据Django项目代码则承载模型定义、视图逻辑、模板渲染与表单处理。资源包内还提供界面截图与图标素材便于理解页面结构与交互效果。目前已有196人学习源码完整、结构清晰可帮助读者快速理清Django项目目录与数据表设计在此基础上进行功能迭代与部署实践。1. 图书管理系统为什么成了 Django 新手的第一道坎很多人学完 Python 基础语法后第一个想动手的项目就是图书管理系统。原因很直接业务逻辑不复杂但增删改查、用户权限、数据关联、页面渲染这些 Web 开发的核心环节一个不少。用 Django 做图书管理系统本质上是在练一套完整的 MVC 思维——模型定义、视图调度、模板渲染、路由分发四件事全都能在一个项目里跑通。但真正动手时翻车点往往不在 Django 本身而在 MySQL 的安装配置和 Django 与 MySQL 的对接上。我见过太多人卡在django.db.utils.OperationalError这个报错上一卡就是一整天。这篇内容就是把这个项目从环境搭建到功能落地的完整路径拆开每一步都给出可复现的命令和配置同时把那些容易踩的坑提前标出来。适合刚学完 Python、想用 Django 做第一个完整项目的人也适合需要快速交付一个课程设计或练手项目的开发者。2. 环境搭建Python、Django、MySQL 三件套的安装与配置2.1 Python 与 Django 的版本选择及安装Python 安装本身不复杂但版本选择有讲究。Django 4.x 系列要求 Python 3.8 以上Django 5.x 要求 Python 3.10 以上。如果你用的是较老的教程可能会看到 Django 2.x 的写法那些url()路由配置在新版本里已经被path()和re_path()取代了。我一般建议直接用 Python 3.10 或 3.11 搭配 Django 4.2 LTS 版本LTS 意味着长期支持不会做一半发现某个库不兼容。安装完 Python 后确认 pip 可用然后安装 Django# 确认 Python 版本建议 3.10 以上 python --version # 安装 Django 4.2 LTS 版本 pip install django4.2 # 验证安装 django-admin --version这里有个细节Windows 上如果同时装了多个 Python 版本python命令可能指向的不是你预期的那个。用where python确认路径必要时用python -m pip install来确保装到正确的解释器里。Mac 和 Linux 用户如果用python3命令后续所有命令里的python都要替换成python3。创建项目和应用# 创建项目项目名用 library_sys django-admin startproject library_sys # 进入项目目录 cd library_sys # 创建图书管理应用 python manage.py startapp books创建完成后目录结构应该是library_sys/下面有一个同名的library_sys/配置目录和一个books/应用目录。很多人第一次看到两层同名目录会懵外层是项目根目录内层是配置包settings.py、urls.py、wsgi.py都在内层。2.2 MySQL 安装与数据库创建MySQL 的安装是新手翻车最集中的环节。Windows 上用官方 installer 安装时记得勾选 “MySQL Server” 和 “MySQL Workbench”后者是图形化管理工具后面建库建表会方便很多。安装过程中会要求设置 root 密码这个密码记牢Django 连接数据库时要用。Linux 上安装 MySQL 用包管理器# Ubuntu/Debian 系 sudo apt update sudo apt install mysql-server # 启动服务 sudo systemctl start mysql sudo systemctl enable mysql # 安全初始化按提示设置 root 密码 sudo mysql_secure_installation安装完成后登录 MySQL 创建项目专用的数据库-- 登录 MySQL -- mysql -u root -p -- 创建数据库字符集用 utf8mb4 支持中文和 emoji CREATE DATABASE library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建专用用户避免直接用 root 连接 CREATE USER lib_userlocalhost IDENTIFIED BY YourPassword123; -- 授权 GRANT ALL PRIVILEGES ON library_db.* TO lib_userlocalhost; FLUSH PRIVILEGES;字符集这里必须用utf8mb4不要用utf8。MySQL 里的utf8实际上是阉割版最多只支持 3 字节字符存中文书名没问题但一旦遇到某些特殊字符就会报错。这个坑我在早期项目里踩过后来统一用utf8mb4再没出过问题。2.3 Django 连接 MySQL 的驱动配置Django 连接 MySQL 需要安装驱动。常见的做法是用mysqlclient它性能好但安装时依赖系统库Windows 上可能需要额外装 Visual C Build Tools。如果安装mysqlclient反复失败可以退而用pymysql纯 Python 实现安装无依赖代价是性能略低。# 方案一mysqlclient推荐 pip install mysqlclient # 方案二pymysql安装失败时的备选 pip install pymysql如果用pymysql需要在项目的__init__.py里加一行伪装# library_sys/__init__.py import pymysql pymysql.install_as_MySQLdb()然后在settings.py里配置数据库连接# library_sys/settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: library_db, USER: lib_user, PASSWORD: YourPassword123, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }HOST这里写127.0.0.1而不是localhost是因为在某些系统上localhost会走 Unix socket 而不是 TCP导致连接报错Cant connect to local MySQL server through socket。用127.0.0.1强制走 TCP能避开这个玄学问题。init_command设置严格模式让数据校验更严格避免存进去的数据格式不对却不报错。配置完成后把books应用注册到INSTALLED_APPSINSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, books, # 注册图书应用 ]到这里环境就搭好了下一步是设计数据模型。3. 数据模型设计图书、借阅、用户三张核心表怎么建3.1 图书与分类模型的定义图书管理系统的核心模型有三个图书、分类、借阅记录。用户模型可以直接用 Django 自带的User不用自己造轮子。先看图书和分类# books/models.py from django.db import models from django.contrib.auth.models import User class Category(models.Model): 图书分类 name models.CharField(分类名称, max_length50, uniqueTrue) description models.TextField(描述, blankTrue) class Meta: verbose_name 分类 verbose_name_plural verbose_name 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_length13, uniqueTrue) category models.ForeignKey( Category, on_deletemodels.SET_NULL, nullTrue, blankTrue, verbose_name分类 ) publisher models.CharField(出版社, max_length100, blankTrue) publish_date models.DateField(出版日期, nullTrue, blankTrue) total_copies models.PositiveIntegerField(总册数, default1) available_copies models.PositiveIntegerField(可借册数, default1) cover models.ImageField(封面, upload_tocovers/, blankTrue) created_at models.DateTimeField(入库时间, auto_now_addTrue) class Meta: verbose_name 图书 verbose_name_plural verbose_name ordering [-created_at] def __str__(self): return self.titleon_deletemodels.SET_NULL表示分类被删除时图书的分类字段置空而不是级联删除图书。这个选择很关键——如果用了CASCADE删一个分类会把该分类下所有图书全删掉这在图书管理系统里是灾难性的。available_copies和total_copies分开存是为了支持多册图书的借阅场景借出一本available_copies减一归还加一但总册数不变。3.2 借阅记录模型与状态管理借阅记录要跟踪谁借了哪本书、什么时候借的、什么时候还、当前状态是什么# books/models.py 继续追加 class BorrowRecord(models.Model): 借阅记录 STATUS_CHOICES ( (borrowed, 借阅中), (returned, 已归还), (overdue, 已逾期), ) user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name借阅人) book models.ForeignKey(Book, on_deletemodels.CASCADE, verbose_name图书) borrow_date models.DateField(借阅日期, auto_now_addTrue) due_date models.DateField(应还日期) return_date models.DateField(实际归还日期, nullTrue, blankTrue) status models.CharField(状态, max_length10, choicesSTATUS_CHOICES, defaultborrowed) class Meta: verbose_name 借阅记录 verbose_name_plural verbose_name ordering [-borrow_date] def __str__(self): return f{self.user.username} - {self.book.title}due_date不设auto_now_add因为应还日期是根据借阅日期加固定天数算出来的需要在视图层计算后写入。状态字段用choices约束避免写入非法值。这里有个设计决策逾期状态是存库还是实时计算我一般倾向于存库用一个定时任务每天扫描一次把超过due_date还没归还的记录状态改成overdue。实时计算虽然不用定时任务但每次查询都要做日期比较数据量大了性能会受影响。模型定义完成后生成迁移并执行# 生成迁移文件 python manage.py makemigrations books # 执行迁移在 MySQL 中建表 python manage.py migrate # 创建超级用户用于登录 admin 后台 python manage.py createsuperuser执行migrate时如果报错django.db.utils.OperationalError: (2002, Cant connect to MySQL server)先检查 MySQL 服务是否启动再检查settings.py里的HOST和PORT是否正确。如果报Access denied for user说明用户名或密码不对回到 MySQL 里用ALTER USER重置密码。3.3 把模型注册到 Admin 后台快速验证在写视图之前先把模型注册到 admin这样能快速验证数据模型是否正确# books/admin.py from django.contrib import admin from .models import Category, Book, BorrowRecord admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display (name, description) search_fields (name,) admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display (title, author, isbn, category, available_copies, total_copies) list_filter (category,) search_fields (title, author, isbn) admin.register(BorrowRecord) class BorrowRecordAdmin(admin.ModelAdmin): list_display (user, book, borrow_date, due_date, status) list_filter (status,) search_fields (user__username, book__title)启动开发服务器python manage.py runserver访问http://127.0.0.1:8000/admin/用超级用户登录就能看到三个模型的管理界面。在这里手动添加几条分类和图书数据确认字段类型和约束都符合预期。这一步看起来简单但能提前发现模型设计的问题比写完视图再回头改模型要省事得多。4. 视图与模板借阅归还逻辑的完整实现4.1 图书列表与搜索视图图书列表页需要支持分页和关键词搜索这是图书管理系统最基础也最常用的功能# books/views.py from django.shortcuts import render, get_object_or_404, redirect from django.core.paginator import Paginator from django.db.models import Q from django.contrib.auth.decorators import login_required from django.utils import timezone from datetime import timedelta from .models import Book, Category, BorrowRecord def book_list(request): 图书列表支持搜索和分类筛选 query request.GET.get(q, ) category_id request.GET.get(category, ) books Book.objects.select_related(category).all() if query: books books.filter( Q(title__icontainsquery) | Q(author__icontainsquery) | Q(isbn__icontainsquery) ) if category_id: books books.filter(category_idcategory_id) paginator Paginator(books, 10) # 每页 10 条 page_number request.GET.get(page) page_obj paginator.get_page(page_number) categories Category.objects.all() context { page_obj: page_obj, categories: categories, query: query, current_category: category_id, } return render(request, books/book_list.html, context)select_related(category)是关键优化。图书列表要显示分类名称如果不加这个每渲染一本书就会查一次分类表10 本书就是 10 次额外查询这就是典型的 N1 问题。加上select_related后Django 会用 JOIN 一次性把分类数据取出来。搜索用Q对象做 OR 组合icontains表示不区分大小写的包含匹配书名、作者、ISBN 三个字段任意一个命中就返回。4.2 借书与还书的业务逻辑借书和还书涉及库存变更和记录创建必须用事务保证一致性# books/views.py 继续追加 from django.db import transaction login_required transaction.atomic def borrow_book(request, book_id): 借书 book get_object_or_404(Book, idbook_id) # 检查是否可借 if book.available_copies 0: return render(request, books/error.html, {message: 该书暂无可借副本}) # 检查用户是否已借同一本书且未归还 existing BorrowRecord.objects.filter( userrequest.user, bookbook, statusborrowed ).exists() if existing: return render(request, books/error.html, {message: 您已借阅此书请先归还}) # 扣减库存 book.available_copies - 1 book.save(update_fields[available_copies]) # 创建借阅记录默认借期 30 天 due_date timezone.now().date() timedelta(days30) BorrowRecord.objects.create( userrequest.user, bookbook, due_datedue_date, statusborrowed ) return redirect(book_list) login_required transaction.atomic def return_book(request, record_id): 还书 record get_object_or_404(BorrowRecord, idrecord_id, userrequest.user) if record.status returned: return render(request, books/error.html, {message: 该记录已归还}) # 更新记录 record.return_date timezone.now().date() record.status returned record.save(update_fields[return_date, status]) # 恢复库存 book record.book book.available_copies 1 book.save(update_fields[available_copies]) return redirect(my_borrows)transaction.atomic装饰器保证借书操作要么全成功要么全回滚。如果扣库存成功但创建记录失败事务会回滚库存不会被错误扣减。update_fields指定只更新变化的字段避免全字段 UPDATE 带来的并发覆盖问题。检查重复借阅的逻辑不能省——没有这个检查用户可以对同一本书反复点借阅库存被扣光但书只有一本。4.3 模板中静态文件与图片的正确引用模板里引用静态文件和上传的图片是新手容易翻车的地方。Django 的静态文件分两种项目级的static/目录和应用级的books/static/目录。图片上传后存在MEDIA_ROOT下需要通过MEDIA_URL访问。!-- templates/books/book_list.html -- {% load static %} !DOCTYPE html html head title图书列表/title link relstylesheet href{% static css/style.css %} /head body form methodget input typetext nameq value{{ query }} placeholder搜索书名、作者或 ISBN button typesubmit搜索/button /form {% for book in page_obj %} div classbook-item {% if book.cover %} img src{{ book.cover.url }} alt{{ book.title }} {% else %} img src{% static images/default_cover.png %} alt默认封面 {% endif %} h3{{ book.title }}/h3 p{{ book.author }} | {{ book.category.name|default:未分类 }}/p p可借{{ book.available_copies }} / {{ book.total_copies }}/p a href{% url borrow_book book.id %}借阅/a /div {% endfor %} !-- 分页 -- div classpagination {% if page_obj.has_previous %} a href?page{{ page_obj.previous_page_number }}q{{ query }}上一页/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 }}q{{ query }}下一页/a {% endif %} /div /body /html图片显示不出来九成是settings.py里没配MEDIA_URL和MEDIA_ROOT或者urls.py里没加 media 的路由。开发环境下需要这样配# settings.py MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media # library_sys/urls.py from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... 其他路由 ] static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)注意static()只在DEBUGTrue时生效生产环境需要用 Nginx 来服务静态文件和媒体文件。分页链接里要带上q{{ query }}否则翻到第二页搜索条件就丢了这是很常见的体验问题。5. 避坑与排查MySQL 连接、静态文件、迁移报错的真实案例5.1 MySQL 连接报错 2002 和 1045 的排查路径现象执行python manage.py migrate时报django.db.utils.OperationalError: (2002, Cant connect to local MySQL server through socket /tmp/mysql.sock)。原因Django 配置的HOST是localhostMySQL 客户端库把localhost解析为 Unix socket 连接但 socket 文件路径不对或 MySQL 没在监听 socket。解决把settings.py里的HOST改成127.0.0.1强制走 TCP。同时确认 MySQL 服务已启动sudo systemctl status mysql。如果报 1045Access denied说明密码错误用mysql -u root -p登录后执行ALTER USER lib_userlocalhost IDENTIFIED BY 新密码;重置。5.2 迁移时表已存在的冲突处理现象migrate时报Table django_migrations already exists或某个业务表已存在。原因之前手动在 MySQL 里建过表或者迁移记录和实际表结构不一致。解决不要直接删表。先看django_migrations表里记录了哪些迁移如果迁移记录缺失但表存在可以用python manage.py migrate --fake books 0001假装执行过。如果确认是测试数据可以丢弃删掉整个数据库重建DROP DATABASE library_db; CREATE DATABASE library_db CHARACTER SET utf8mb4;然后重新migrate。5.3 静态文件 404 与图片不显示现象CSS 样式不生效或者上传的图书封面显示为裂图。原因静态文件目录没配置或者DEBUG模式下 media 路由没加。解决确认settings.py里STATIC_URL /static/和STATICFILES_DIRS配置正确。项目根目录下建static/文件夹放 CSS 和 JS。图片问题检查MEDIA_URL、MEDIA_ROOT和urls.py里的static()路由。还有一个容易忽略的点模板里{% load static %}必须写在文件开头写在中间不生效。5.4 借阅并发导致库存扣成负数现象两个用户同时借同一本书库存显示 1 本但两个人都借成功了available_copies变成 -1。原因视图里先查询库存再扣减两个请求同时读到库存为 1都判断可借然后都执行扣减。解决用数据库层面的原子更新代替先查后改from django.db.models import F # 原子扣减只有 available_copies 0 时才更新 updated Book.objects.filter( idbook_id, available_copies__gt0 ).update(available_copiesF(available_copies) - 1) if not updated: return render(request, books/error.html, {message: 库存不足})F()表达式让数据库直接做字段减法filter条件保证只有库存大于 0 才更新update返回受影响行数为 0 说明库存不足。这个写法在并发下是安全的因为数据库的行锁会保证同一行不会被两个事务同时更新。5.5 Django 版本升级后 url() 报错现象从旧教程抄的url(r^book/(?Pid\d)/$, views.detail)在新版 Django 里报NameError: name url is not defined。原因Django 4.0 起移除了django.conf.urls.url()改用re_path()或path()。解决简单路径用path(book/int:id/, views.detail, namebook_detail)需要正则的用re_path(r^book/(?Pid\d)/$, views.detail)。path的转换器int:id比正则更直观能用path就别用re_path。6. 从能跑到好用分页优化、权限控制和部署前检查项目能跑起来只是第一步要让它真正好用还有几个地方值得花时间打磨。分页优化方面Paginator默认会执行一次COUNT(*)查询来算总数数据量大了会慢。如果图书数量在几千条以内这个开销可以接受。超过万条时可以考虑用cursor分页或者缓存总数。另一个细节是get_page()方法会自动处理页码越界和非法页码比手动切片安全不要用books[offset:offset10]这种写法。权限控制上普通用户只能借书还书和查看自己的借阅记录管理员才能增删图书。Django 的login_required只能判断是否登录要判断是否是管理员用user_passes_test或者自定义装饰器from django.contrib.auth.decorators import user_passes_test def is_librarian(user): return user.is_staff user_passes_test(is_librarian) def book_create(request): # 只有管理员能创建图书 ...部署前检查清单里有几项必须确认DEBUG改成FalseALLOWED_HOSTS填上实际域名或 IPSECRET_KEY从环境变量读取而不是硬编码在settings.py里数据库密码同样走环境变量。静态文件用python manage.py collectstatic收集到统一目录交给 Nginx 服务。MySQL 连接池方面Django 默认每个请求新建连接并发高了会撑爆 MySQL 的max_connections。可以在settings.py里配CONN_MAX_AGE让连接复用DATABASES[default][CONN_MAX_AGE] 600 # 连接保持 10 分钟这个值不要设太大否则 MySQL 那边可能因为wait_timeout先断开Django 这边还以为连接可用导致MySQL server has gone away报错。600 秒是个比较稳妥的折中。最后说一个我自己的习惯每次改完模型或者视图先跑python manage.py check做静态检查再跑python manage.py test跑一遍测试。图书管理系统的核心逻辑就是借书还书的库存增减写两个测试用例覆盖并发借阅和重复借阅的场景比手动点页面靠谱得多。这个项目不大但把这几处细节做到位它就不只是一个练手 demo而是一个能真正拿去用的系统。希望帮到你。本文还有配套的精品资源点击获取
返回列表