Django小说网站开发实战:从模型设计到部署优化 简介这是一份利用Django框架打造的小说阅读网站完整工程源码面向需要深入学习Web开发全流程的Python初学者和进阶者。项目以真实小说网为案例覆盖了项目结构搭建、数据库建模与迁移、动态页面渲染、URL映射、用户登录与权限管理等核心环节并集成Vue和JavaScript前端技术帮助读者理解后端与前端如何协作。源码包内共有852个文件包含大量Python源码、Vue组件、样式表、HTML页面以及图片字体等静态资源压缩后整体大小为460.54MB。目录划分清晰按照功能模块组织从配置到业务逻辑都便于逐层对照学习。这套源码已有491人学习下载说明其内容得到一定认可。通过学习这份源码读者可以掌握小说列表展示、章节内容阅读、用户收藏评论等功能的完整实现思路锻炼从零开始构建全栈网站的能力。1. 用Django做小说站这份源码拆开的姿势比“能用”更重要学完Django基础后很多人会遇到一个断档会写模型、视图和模板但没有一个业务能把三者串起来。小说网站正好补上这个断档——它有分类、有长篇内容、有分页阅读、有用户收藏和阅读记录还涉及后台管理和全文检索几乎是Django全栈练手的教科书式场景。你手里这份“利用Django学习并开发的小说网源码.zip”从命名就能看出是面向学习加二次开发的不是那种报名送的空壳模板。拆开这份源码最重要的事情不是急着双击运行而是先看清它的项目如何组织、表结构为什么这样设计、阅读链路在视图里怎么走的然后按自己的需求改。本文按项目骨架、数据模型、阅读链路、后台与部署的顺序把它讲透。2. 先把Django项目骨架搭对再看小说业务怎么落进去2.1 源码包里的目录结构project和app要分清楚一个合格的Django小说项目目录通常长这样novel_website/ ├── manage.py ├── requirements.txt ├── config/ # 项目配置很多教学视频写的是 novel_website │ ├── settings.py │ └── urls.py ├── books/ # 小说核心 app │ ├── models.py │ ├── views.py │ └── urls.py ├── users/ # 用户 app ├── templates/ ├── static/ └── db.sqlite3这个结构最关键的一点是config或novel_website叫项目职责是放 settings、根路由和部署配置books、users是 app职责是放业务代码。很多从源码包里学习的人一开始会犯的错是看到config里也有文件目录就以为 app 必须放在项目同名目录下其实 Django 启动时只要 INSTALLED_APPS 指向正确就能找到。判断一个业务要不要拆成 app标准很简单它是否有独立的数据模型以及是否会被其他模块复用。2.2 用命令重建一套最小骨架确认源码没有缺文件拿到源码先别打开 IDE我一般会在同级目录手动建一个新项目做对照这样能快速发现 zip 包缺了哪些配置文件python -m venv venv source venv/bin/activate pip install Django4.2,5.0 django-admin startproject config . python manage.py startapp books python manage.py startapp users python manage.py migrate参数说明startproject后面的.表示把 manage.py 生成在当前目录这在后面接入宝塔或 Docker 时更省事startapp每执行一次都会在当前目录生成一个业务模块目录并把同名类注册到settings.py的INSTALLED_APPS。对照源码包时重点看三个位置manage.py、config/settings.py、requirements.txt。如果 zip 里这三个文件齐全说明项目主体完整缺失的往往是 static、media、uploads 一类运行时目录需要自行创建并加入.gitignore。这里提醒一下源码包里的 requirements.txt 如果写的是 Django 3.x而你新建项目用了 Django 4.2目录结构上startproject生成的包名会稍有变化不要混用。2.3 settings.py 四件套时区、数据库、静态目录、ALLOWED_HOSTS从源码包里学 Django 项目第一个值得精读的文件一定是 settings.py。小说站这种内容型站点以下四段配置决定项目能不能在别人机器上跑起来# config/settings.py LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True ALLOWED_HOSTS [*] # 部署时再改成具体域名/IP DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static]逻辑说明LANGUAGE_CODE和TIME_ZONE同时影响 admin 后台语言和日期时间写入小说站更新时间字段如果用auto_nowTrue时区设错会导致章节更新列表的时间和服务器时间对不上。ALLOWED_HOSTS [*]只适合本地调试一旦部署到公网必须修改否则会收到“Invalid HTTP_HOST header”的告警。STATICFILES_DIRS不加模板里加载 CSS/JS 会全军覆没。USE_TZ True时生成时间入库会自动带上 UTC 偏移读取时按TIME_ZONE本地化新手最容易踩的坑是直接往模型里写datetime.now()而不是用timezone.now()。2.4 SQLite 换 MySQLmysqlclient 与 utf8mb4 的兼容问题源码包默认使用 SQLite是为了让学习者免装数据库。真要按“小说网”的定位发布用户多了以后 SQLite 写锁是个硬伤。Django 官方推荐的 MySQL 驱动是mysqlclient但它在 Windows 上安装经常报错因为需要 MySQL C 语言客户端库。如果你用的是 Python 3.10 以上版本更省事的方案是安装PyMySQL并在config/__init__.py里做兼容设置# config/__init__.py import pymysql pymysql.install_as_MySQLdb()Django 的设置里只需把 ENGINE 换成django.db.backends.mysql并补上连接参数DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: novel_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }这里OPTIONS的charset必须设为utf8mb4否则小说正文里出现 emoji 或生僻字写入会报Incorrect string value的错误。从源码包学项目建议本地开发还是保留 SQLite等部署前再切 MySQL切换后执行一次python manage.py makemigrations python manage.py migrate即可但要注意数据库迁移本身不能自动拷贝已有数据开发期数据量小可以直接重新录入。提示源码包如果自带了requirements.txt优先按它的版本安装而不是直接装最新版 Django。Django 4.2 以后移除了部分旧版写法版本差距大到一定程度项目启动就会报 import 错误。3. 小说业务核心Django 模型设计、ORM 查询与删除对象3.1 Book、Chapter、Category 三张表的关系与字段选择模型是小说站的根。源码包无论怎么包装最后都会落到几张核心表上。我拆这类项目时重点关注四张表# books/models.py from django.db import models from django.contrib.auth.models import User class Category(models.Model): name models.CharField(max_length20, uniqueTrue) class Book(models.Model): title models.CharField(max_length200) author models.CharField(max_length50) category models.ForeignKey(Category, on_deletemodels.CASCADE, related_namebooks) intro models.TextField(blankTrue) cover models.ImageField(upload_tocover/, blankTrue) is_completed models.BooleanField(defaultFalse) is_active models.BooleanField(defaultTrue) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Chapter(models.Model): book models.ForeignKey(Book, on_deletemodels.CASCADE, related_namechapters) title models.CharField(max_length200) content models.TextField() word_count models.IntegerField(default0) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at] indexes [ models.Index(fields[book, created_at]), ] class Bookmark(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_namebookmarks) chapter models.ForeignKey(Chapter, on_deletemodels.CASCADE, related_namebookmarks) progress models.FloatField(default0) # 0~1 class Meta: unique_together (user, chapter)参数说明related_name决定了反向查询的名字没有它时 Django 默认用book.chapter_set源码包里通常都会写上related_name让代码更可读。on_deletemodels.CASCADE表示删除小说时其章节一起删除这是最简单的级联方式但如果收藏记录也想保留就要考虑models.PROTECT或SET_NULL。Meta.ordering加在模型上会影响所有未显式排序的查询created_at字段一定要建索引小说网的章节列表按创建时间倒序是所有查询里最频繁的。3.2 列表页的 N1 查询select_related 与 prefetch_related第一个写 Django 小说站的人几乎都会在书籍列表页写出 N1 查询先查出 20 本书模板里访问book.category时再发出 20 条 SQL。修复方式就两个方法# books/views.py books (Book.objects .select_related(category) .filter(is_activeTrue) .prefetch_related(chapters)[:20])逻辑说明select_related会把ForeignKey字段通过 JOIN 查出来适用于分类、作者这类“多对一”的关系prefetch_related则适合chapters这类反向外键它会先查书籍列表再用WHERE book_id IN (...)批量查出章节并在 Python 内存里组装。普通小说站章节数不多时可以只在列表页用select_related(category)详情页单独查章节因为章节数量可能上千一次性全查出来在内存里拼装反而比按需查询慢。源码包里如果列表页 SQL 查询数量超过 20 条多半就是这里没处理干净。3.3 更新点击数、删除对象ORM 的三种高频操作小说网会有两个高频操作容易写错。第一个是更新书的最后更新时间正确写法是使用F()表达式避免并发覆盖from django.db.models import F Book.objects.filter(idbook_id).update( updated_attimezone.now(), chapter_countF(chapter_count) 1 )逻辑说明F(chapter_count) 1把加一这个动作下推到 SQL 里执行而不是先取出数值再写回高并发下不会因为两个请求同时读到同一初值而丢失更新。第二个是删除操作。Book.objects.filter(id1).delete()会触发级联删除与之区分的是Book.objects.update(is_activeFalse)这类软删除。源码包如果做用户书架物理删书是有风险的因为 Bookmark 外键可能连带删除用户记录。所以线上小说站通常两种策略并存管理后台用delete()彻底删除垃圾数据用户触发的下架用is_activeFalse让内容不可见但保留数据。还有一个常用写法是按条件删除章节Chapter.objects.filter(book_id1, word_count0).delete()批量删除会返回删除的对象数量调试时可以打印出来确认。3.4 章节正文存 TextField 还是文件章节正文是小说站数据量最大的部分。源码包里最常见的做法是TextField优点是查询、更新、备份都方便。但当单章超过几万字时TextField通过 ORM 传输会给数据库连接造成压力。如果只是学习直接存TextField即可如果面向真实发布常见做法是正文落FileField数据库字段只存路径和字数配合 nginx 的 X-Accel-Redirect 或对象存储 CDN 分发。这两条路线没有绝对对错按你的并发量决定不要一开始就给模型设计过重的存储方案。存储方式读取速度搜索支持维护成本适合阶段TextField高弱只能 LIKE低学习、日 PV 1 万以下FileField 对象存储需二次请求弱中章节内容大、有 CDN 需求独立全文搜索引擎高强高站内搜索要求高4. 从 URL 到页面打通 Django 小说网站的阅读链路4.1 列表页按分类筛选、按更新时间排序、分页列表页要处理的三个需求是分类、排序和分页。视图代码一般这样写# books/views.py from django.shortcuts import render from django.core.paginator import Paginator from .models import Book def book_list(request, category_idNone): qs Book.objects.select_related(category).filter(is_activeTrue) if category_id: qs qs.filter(category_idcategory_id) order request.GET.get(order, -updated_at) qs qs.order_by(order).only( id, title, author, updated_at, category__name ) paginator Paginator(qs, 20) page_obj paginator.get_page(request.GET.get(page)) return render(request, books/list.html, {page_obj: page_obj})参数说明order参数直接接在order_by上等于把排序权原样交给 URL 里的查询字符串这是简化写法。真实项目里排序字段最好加白名单校验否则用户传入不存在的字段会触发 FieldError。分页用Paginator(qs, 20)每页 20 本paginator.get_page()还帮你处理了pageabc这类非法参数它不会抛 500而是返回第一页。模板里则用page_obj.paginator.num_pages渲染页码列表。4.2 阅读一章时上一章和下一章怎么查小说阅读页最容易写错的逻辑是“上一章”查询。很多人会把章节 id 减一这是错误的因为章节可能被删除或者 id 不连续。正确做法是# books/views.py from django.shortcuts import get_object_or_404 def chapter_detail(request, book_id, chapter_id): chapter get_object_or_404( Chapter.objects.select_related(book), book_idbook_id, idchapter_id ) prev_chapter (Chapter.objects .filter(book_idbook_id, created_at__ltchapter.created_at) .order_by(-created_at).first()) next_chapter (Chapter.objects .filter(book_idbook_id, created_at__gtchapter.created_at) .order_by(created_at).first()) return render(request, books/chapter.html, { chapter: chapter, prev_chapter: prev_chapter, next_chapter: next_chapter, })这里用created_at作为排序依据比用id更可靠因为导入章节数据时有时会手动指定 id而且 id 不保证时间先后。first()和last()在未命中时返回None模板里用{% if prev_chapter %}判断即可不需要对查询结果单独判空。如果章节在导入时编号规则固定也可以在 Chapter 模型上存一个chapter_no字段查询时直接用chapter_no__ltchapter.chapter_no效率更高代价是必须保证同一本书里chapter_no唯一。4.3 用 django reverse resolve 做 URL 反向解析与登录回跳小说站有“点击阅读需要登录”的典型场景游客点了“加入书架”被重定向到登录页登录后还要跳回原页面。这里的关键是 reverse 解析回跳参数。# books/views.py from django.urls import reverse from django.shortcuts import redirect def add_to_bookmark(request, chapter_id): if not request.user.is_authenticated: # 把当前页面的绝对路径拼到 next 参数里 login_url f{reverse(account_login)}?next{request.path} return redirect(login_url) obj, created Bookmark.objects.get_or_create( userrequest.user, chapter_idchapter_id) return redirect(request.GET.get(next) or reverse(books:list))逻辑说明reverse(account_login)根据 URL 配置反推出登录地址Django 里和它配套的resolve()则用来把当前路径还原成视图函数或 URL 名称空间常见使用场景是中间件里检查当前请求属于哪个前缀再决定是否要做权限校验。登录回跳必须限定在站内路径不能在跳转时拼接外部站点的域名否则会留下开放重定向漏洞。获取next参数后建议再用resolve校验目标是否属于本域名或者直接用 Django 的url_has_allowed_host_and_scheme函数判断。4.4 模板继承与自定义过滤器小说正文的排版与清理Django 模板引擎的继承用{% block %}小说页一般把正文放在content块里!-- books/templates/books/chapter.html -- {% extends base.html %} {% block content %} article h1{{ chapter.book.title }} {{ chapter.title }}/h1 div classcontent{{ chapter.content|linebreaks }}/div div classpager a href{% url books:chapter chapter.book.id prev_chapter.id %}上一章/a a href{% url books:chapter chapter.book.id next_chapter.id %}下一章/a /div /article {% endblock %}linebreaks把正文里的换行转成p避免小说正文变成一团文字{% url %}是按视图名和参数动态生成链接比硬写/book/1/chapter/2/更抗改版。如果正文里含有大量全角空格和广告行可以注册一个自定义模板过滤器来清洗源码包里常见的是# books/templatetags/novel_filters.py from django import template import re register template.Library() register.filter(nameclean_novel) def clean_novel(value): value re.sub(r^\s*第[一二三四五六七八九十百千0-9]章.*$, , value, flagsre.M) return value.replace(全本小说网, ).strip()在模板里要先{% load novel_filters %}才能用过滤器接收|前面的值当作第一个参数。register.filter的name参数不写时默认使用函数名写上了可以在模板里换个更短的名字。自定义过滤器里不要做数据库查询它的执行频率很高只适合纯文本处理。5. 后台管理、全书搜索与宝塔部署 Django 小说站5.1 把 admin 界面改成本站后台list_display、search_fields、inlines源码包里的小说数据99% 都是靠 admin 维护而不靠前台表单录入。Django 默认 admin 界面比较克制美化它最直接的方式是重写ModelAdmin属性# books/admin.py from django.contrib import admin from .models import Book, Chapter, Category class ChapterInline(admin.TabularInline): model Chapter extra 1 fields (title, word_count) admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display (id, title, author, category, is_completed, updated_at) list_filter (category, is_completed) search_fields (title, author) list_per_page 50 inlines [ChapterInline]参数说明list_display决定列表页显示哪些列search_fields里如果写了titleadmin 搜索框会生成WHERE title LIKE %关键词%需要关联字段时可以写category__nameinlines让小说和章节在同一个编辑页操作比单独开两个页面录入高效。想要更好看的样式常见做法是推第三方主题django-admin-interface或simpleui这两个包都支持 pip 安装后在 INSTALLED_APPS 里置顶导入但我个人更建议先学会原生 admin 配置再上主题否则出了问题不好排查。5.2 正文搜索SQL LIKE、MySQL 全文索引还是第三方搜索小说网站的搜索框是最容易被高估的功能。只要数据量在十万级以内用 MySQL 的全文索引就够了。# books/views.py from django.db.models import Q def search(request): kw request.GET.get(q, ).strip() qs Book.objects.none() if kw: qs Book.objects.filter( Q(title__icontainskw) | Q(author__icontainskw) ) return render(request, books/search.html, {qs: qs, kw: kw})__icontains生成的 SQL 是LIKE %kw%优点是小项目零依赖缺点是无法利用索引、性能差。MySQL 的FULLTEXT索引能用MATCH ... AGAINST做真正的全文搜索但 Django ORM 不直接支持需要写 RawSQL 或多次调用extra()。如果目标是学习 Django 本身不要把时间花在这上面把搜索框先做成icontains版本等用户量起来再迁到 Whoosh、Elasticsearch 都来得及。这里要提醒icontains在 SQLite 和 MySQL 下对大小写的处理不一样中文搜索差别不大但英文书名会踩坑。5.3 宝塔部署 djangouwsgi nginx 的极简步骤源码包在本地跑通后发布到服务器最常见的方案是宝塔面板加 nginx 加 uwsgi。我踩过的经验先在服务器上建好 Python 环境和依赖再启动 uwsgi最后才配 nginx。# 项目目录下依次执行 python3 -m venv /www/wwwroot/novel/venv source /www/wwwroot/novel/venv/bin/activate pip install -r requirements.txt python manage.py migrate python manage.py collectstatic --noinput pip install uwsgi # 启动 uwsgi测试用 uwsgi --http :8000 --module config.wsgi --master --processes 4 --threads 2uwsgi 进程数不能拍脑袋定。一个小型小说站4 进程 2 线程足够每进程占用内存取决于页面复杂度正文页如果做了缓存看到的内存占用大约 60 到 100 MB。宝塔面板里创建一个 Python 项目站点把启动命令填成上面的 uwsgi再把 nginx 反向代理到 8000 端口location /static/ { alias /www/wwwroot/novel/static/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8000; uwsgi_param UWSGI_CHDIR /www/wwwroot/novel; uwsgi_param UWSGI_SCRIPT config.wsgi; }uwsgi_pass的值要和 uwsgi 启动参数里的 socket 一致。如果用--http :8000启动nginx 要写proxy_pass而不是uwsgi_pass用--socket启动才能配合uwsgi_pass。这个区别是部署时最常见的错误之一排错时先确认两边的协议一致。宝塔面板的 Python 项目管理器默认生成的是 gunicorn 配置如果你想换回 uwsgi注意把它的“启动方式”从 gunicorn 改成命令行模式不然互相抢端口。配置项本地开发宝塔上线DEBUGTrueFalse静态目录STATICFILES_DIRSSTATIC_ROOT nginx alias数据库SQLiteMySQL utf8mb4应用服务器runserveruwsgi socket5.4 上线前检查清单DEBUG 关闭、静态收集与日志源码包直接部署的结局通常是打开页面一片空白、admin 后台样式丢失、或者页面报错信息直接暴露给用户。上线前至少要做这几件事python manage.py check --deploy python manage.py collectstatic --noinputcheck --deploy会输出一组安全警告逐条看即可。collectstatic会把每个 app 里的静态文件复制到STATIC_ROOT而STATIC_ROOT需要在 settings 里单独设置不能和STATICFILES_DIRS一样。如果忘记设置部署环境下的静态文件就会全 404。日志方面生产环境要配置LOGGING把 uwsgi 的 stdout 重定向到文件因为正文页出现OperationalError: too many connections这类数据库错误时没有日志连定位入口都没有。还有一步容易被忽略把db.sqlite3从源码包里删掉让服务器重新 migrate 生成空库否则本地测试数据会直接暴露在公网。6. 从“能跑”到“扛得住”Django 小说站的缓存、调试与安全验证6.1 用 django-debug-toolbar 定位慢查询源码包跑通后第一件事不是加功能而是看当前项目的缺陷所在。安装django-debug-toolbar并配置好刷新书籍列表页如果 SQL 查询数量超过 30 条说明 N1 问题没处理干净回到第三章的select_related重新优化。面板上的“SQL”页签会显示出每一条查询花费的时间重点看有没有WHERE book_id IN (...)的大 IN 查询以及排序字段是否走了索引。配置只有三行INSTALLED_APPS [debug_toolbar] MIDDLEWARE [debug_toolbar.middleware.DebugToolbarMiddleware] INTERNAL_IPS [127.0.0.1] # 开发机访问才显示注意调试工具本身会占用内存所以生产环境不要把它留在 INSTALLED_APPS 里用环境变量控制加载。源码包的学习阶段建议始终开着看模板渲染时间也比看视图代码更直观。6.2 给阅读页加页面缓存小说正文基本不变化非常适合整页缓存。用 Django 内置的缓存装饰器# config/urls.py 或 books/urls.py from django.views.decorators.cache import cache_page urlpatterns [ path(book/int:book_id/chapter/int:chapter_id/, cache_page(60 * 30)(views.chapter_detail), namechapter), ]cache_page(60 * 30)表示该页在内存中缓存半小时缓存的 key 默认按完整 URL 拆登录用户读到缓存内容也无妨因为正文对所有用户是相同的。部署后再配一层Cache-Control: public, max-age1800响应头CDN 或浏览器就都能缓存阅读页的并发压力立刻降一个量级。验证方式很简单连续刷两次页面然后看 uwsgi 日志里的请求记录第二次请求如果不再打印 SQL 查询日志说明命中缓存。6.3 安全验证DEBUG 关闭、XSS 转义与用户输入过滤最后要跑一次“坏人视角”检查。第一确认DEBUGFalse时ALLOWED_HOSTS不是空列表第二检查模板是否有|safe用得太随意Django 默认自动转义 HTML但如果模板里写了{{ chapter.content|safe }}就把转义跳过了小说正文若由爬虫录入里面带 script 标签就可能造成 XSS第三验证用户输入的文字在 admin 里能正常存取MySQL 下的utf8mb4已经解决特殊字符问题但 SQLite 下没事不代表迁移到 MySQL 也没事。正文里有代码片段或特殊符号时要在写入前做一次统一清洗而不是等出了弹窗再回头查模板。用curl -I http://127.0.0.1:8000/book/1/chapter/1/看响应头如果能同时看到Cache-Control: max-age1800和Content-Type: text/html; charsetutf-8阅读页的缓存链路就通了。到这里这份源码包的骨架、模型、阅读视图、后台优化和部署验证就全部走完了一遍剩下需要你动手改的是把它从“学习源码”变成“自己维护的项目”。本文还有配套的精品资源点击获取