简介:这是一套基于Python与Django框架开发的完整小说网站项目源码,面向计算机专业本科生、Web开发初学者及毕业设计需求者,提供可直接运行的小说阅读平台实现方案,涵盖用户注册登录、小说分类展示、章节阅读、搜索收藏等核心功能。压缩包共851个文件,包含61个Python后端逻辑文件(含Django模型、视图与路由)、252个JavaScript前端交互脚本、115个Vue组件(支持动态渲染与状态管理)、69个CSS/SCSS/LESS样式文件及84个PNG等静态资源,整体体积达460.8MB,结构清晰、模块解耦,便于理解MVT架构落地细节。目前已有388人学习下载,资源附带完整项目目录、基础部署说明及多端适配样式,开箱即用,适合用于课程设计实践、求职作品集构建或Django全栈开发能力进阶训练。 在社区分享源码项目的时间不算短了,几乎每个月都会碰到有人问起:有没有一套比较完整的 Python + Django 小说网站源码,既能拿来学 Django 的 MVT 流程,又真的能在本地跑起来当毕设或者个人站点。问的人多了,我也发现一个普遍现象:不少人下载的源码要么缺依赖文件,要么数据库配置写死,要么跑起来之后一堆诡异报错,最后只能放弃。所以这次我把自己整理过的一套“基于 Python 和 Django 开发的小说网站项目源码”整个拆开,从功能范围、模型设计、核心模块实现,到本地部署、生产环境上线、常见坑位,一次说清楚。如果你是第一次接触 Django,或者正打算用 Django 做一个内容型站点,这篇应该能帮你省下不少走弯路的时间。
1. 这套小说站源码到底包含哪些能力
看一套源码值不值得下载,我习惯先不看代码,而是先看功能清单和目录结构。功能决定它能拿来干什么,目录结构决定它好不好改。这套小说网站项目不是那种“只有一个列表页和详情页”的玩具 Demo,而是一个相对完整的在线小说阅读站点,前台用户端和后台管理端都有,拿来当课程设计、毕业设计,或者在此基础上做二次开发,都是够用的。
先列一下前台的功能清单:
- 用户注册、登录、退出,支持简单的个人中心
- 小说分类浏览,按大类(玄幻、都市、历史、科幻等)筛选
- 小说列表页,支持按点击量、更新时间、收藏数排序
- 小说详情页,展示封面、作者、简介、最新章节、总字数
- 全文检索,支持按书名、作者名模糊搜索
- 小说阅读页,支持分章阅读、上一章/下一章跳转
- 书架功能,用户可以收藏/取消收藏小说
- 阅读历史,自动记录用户阅读到哪一章,下次继续阅读
后台管理端的功能也按内容型站点的常规需求做了:
- 管理小说分类
- 发布、编辑、下架小说
- 管理小说章节,支持批量导入章节内容
- 管理用户、查看用户收藏和阅读记录
- 基础数据统计(小说数量、用户数量、章节总数)
从功能范围就能看出来,这套项目覆盖了内容型网站几乎所有核心场景:内容建模、列表分页、详情聚合、用户体系、搜索、用户行为记录。这些功能点对应的不是某个单独技术点,而是一整套 Django 开发思维——Model 设计、ORM 查询优化、CBV 和 FBV 的取舍、模板继承、分页器、中间件、上下文处理器,全都有涉及。
再看目录结构,我拆包之后大概是这样的:
novel_site/ ├── manage.py ├── requirements.txt ├── config/ # 项目配置 │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── novels/ # 小说模块 │ ├── users/ # 用户模块 │ └── operations/ # 书架、阅读记录 ├── static/ # 静态文件 ├── media/ # 用户上传/封面 ├── templates/ # 模板文件 │ ├── base.html │ ├── novels/ │ ├── users/ │ └── operations/ └── scripts/ # 辅助脚本 ├── import_novels.py └── generate_data.py把业务放到apps包下按模块拆分的做法,是我比较推荐的一种结构。原因很简单:Django 的 App 本质上是可复用的业务单元,按模块拆分之后,后续加新功能只需要在apps下新建一个包,不需要改动主工程的 URL 和设置,维护成本会低很多。很多新手喜欢把所有 model 堆在同一个 models.py 里,项目小的时候没问题,到了小说、章节、用户、收藏、历史记录这些实体多起来之后,文件一长,改起来就非常痛苦。
这套源码适合的人群,我总结下来有三类。第一类是学完 Django 基础教程、想找一个完整项目巩固 MVT 流程的人;第二类是准备做毕设、需要一个能演示能跑通、又不想从零写业务逻辑的学生;第三类是有点 Django 基础、想自己搭一个阅读类站点、需要一个干净底子的开发者。当然,如果你完全没学过 Python,直接拿这套源码硬啃,会有点吃力,建议先补一下 Python 语法和 Django 基础再回来。
2. 技术选型与数据模型设计:核心是内容建模
一套源码值不值得读,最关键的部分其实是数据模型。功能是可以堆的,但 Model 设计一旦定下来,后面所有业务逻辑都建立在它之上,想改就得大动。这套小说网站走的是一条经典的 Django 内容站点路线,技术栈不花哨,但每个选型都有它的理由。
2.1 为什么是 Django 而不是 Flask
很多人纠结写内容站到底用 Flask 还是 Django,我的看法很直接:追求快速交付、需要自带后台、需要完整 ORM 和迁移机制的场景,Django 的效率远高于 Flask。Python 后端里,Flask 的定位是微框架,适合做 API 服务、原型验证,但到了内容型网站这种场景,用户认证、Admin 后台、ORM、模板引擎、表单处理、分页这些需求几乎一个不少,Flask 都需要自己拼装。Django 的哲学是“batteries included”,官方已经把常用组件都集成了,开发时不需要在每个项目里重新造一遍轮子。
国内使用 Django 做内容管理系统、企业信息化后台、数据平台的项目其实一直不少。小说网站这种典型的内容站,天然就是 Django 的舒适区——它有自带的 Admin 后台,可以直接管理小说和章节;有完善的 ORM,查询、过滤、聚合都很方便;有模板继承机制,做整站统一布局非常舒服;还有自带的用户认证体系,注册登录这些功能不需要从零写。
2.2 核心数据模型拆解
这套项目的数据模型主要围绕五个实体:分类、小说、章节、用户、书架/阅读记录。我直接把核心字段列出来,并说明为什么这样设计。
先看小说的 Model:
from django.db import models from django.utils import timezone class Category(models.Model): name = models.CharField('分类名', max_length=32, unique=True) sort = models.IntegerField('排序', default=0) class Meta: ordering = ['sort', 'id'] verbose_name = '小说分类' verbose_name_plural = verbose_name def __str__(self): return self.name class Novel(models.Model): category = models.ForeignKey(Category, on_delete=models.PROTECT, verbose_name='分类') name = models.CharField('书名', max_length=128, db_index=True) author = models.CharField('作者', max_length=64, db_index=True) intro = models.TextField('简介', blank=True) cover = models.ImageField('封面', upload_to='covers/', blank=True, null=True) status = models.CharField('状态', max_length=16, choices=(('serial', '连载中'), ('finish', '已完结')), default='serial') clicks = models.PositiveIntegerField('点击量', default=0) favorites = models.PositiveIntegerField('收藏数', default=0) created_at = models.DateTimeField('创建时间', default=timezone.now) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: ordering = ['-updated_at'] verbose_name = '小说' verbose_name_plural = verbose_name def __str__(self): return self.name class Chapter(models.Model): novel = models.ForeignKey(Novel, on_delete=models.CASCADE, related_name='chapters', verbose_name='所属小说') title = models.CharField('章节名', max_length=128) content = models.TextField('正文') volume = models.IntegerField('卷', default=1) sort = models.IntegerField('章节序号', default=0) word_count = models.PositiveIntegerField('字数', default=0) created_at = models.DateTimeField('创建时间', default=timezone.now) class Meta: ordering = ['sort', 'id'] unique_together = ('novel', 'sort') verbose_name = '章节' verbose_name_plural = verbose_name def __str__(self): return f'{self.novel.name} {self.title}'有几个设计点我想专门说明一下,这些都是实际开发中容易踩坑的地方:
第一,Novel.category用的是on_delete=models.PROTECT而不是CASCADE。网上很多教程示例默认 CASCADE,但小说分类如果被小说引用,一旦删除会用书的记录全部被连带删除,这个在内容站里是很危险的操作。用PROTECT之后,如果分类下还有小说,删除分类会被 Django 拦截,能避免很多误操作。业务上需要删除分类时,应该先转移或删除分类下的小说。
第二,Chapter的排序字段用的是sort,而不是直接用id自增主键排序。原因很现实:章节内容有时候需要调整顺序,比如作者插了一章番外,如果排序依赖 id,插入就会很麻烦;有了独立的sort字段,改顺序只改数字就行。unique_together = ('novel', 'sort')保证同一本小说下章节序号不能重复,避免数据错乱。
第三,Chapter.content直接用了TextField,正文很长时这个字段会比较大。MySQL 下它的底层类型是longtext,优点是简单直接,查一章节就是一次主键查询,速度很快。后面我会专门讲这个大字段在部署时的配置坑。
再来看用户侧的数据模型:
from django.contrib.auth.models import AbstractUser class User(AbstractUser): nickname = models.CharField('昵称', max_length=32, blank=True) avatar = models.ImageField('头像', upload_to='avatars/', blank=True, null=True) class Shelf(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shelf_items', verbose_name='用户') novel = models.ForeignKey(Novel, on_delete=models.CASCADE, related_name='shelf_items', verbose_name='小说') created_at = models.DateTimeField('加入时间', default=timezone.now) class Meta: unique_together = ('user', 'novel') verbose_name = '书架' verbose_name_plural = verbose_name class ReadHistory(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='read_history', verbose_name='用户') novel = models.ForeignKey(Novel, on_delete=models.CASCADE, verbose_name='小说') chapter = models.ForeignKey(Chapter, on_delete=models.CASCADE, verbose_name='最后阅读章节') read_at = models.DateTimeField('阅读时间', auto_now=True) class Meta: ordering = ['-read_at'] unique_together = ('user', 'novel') verbose_name = '阅读记录' verbose_name_plural = verbose_name用户模型继承了 Django 自带的AbstractUser,扩展了昵称和头像两个字段。这样做的好处是能无缝复用 Django 自带的认证、密码管理、Admin 关联,不需要自己写登录注册的核心逻辑。Shelf和ReadHistory都加上了unique_together,保证同一个用户对同一本小说只有一条记录。书架去重很合理,但阅读历史为什么要唯一?因为阅读记录最关心的不是“读过多少次”,而是“最后一次读到哪”,只保留一条记录、每次阅读时更新章节序号和更新时间,查询最新阅读列表会非常高效。
2.3 ORM 级联删除和查询边界
这套项目很容易遇到的问题就是 ORM 的级联行为。Django 的默认策略是,如果一个对象被其他对象通过外键引用,删除父对象时子对象的行为取决于on_delete参数。之前说过的PROTECT、CASCADE之外,还有一个常用的SET_NULL,比如某些评论场景删用户后保留评论,但把外键置空。
举个具体场景:删除一本小说时,因为Chapter的novel外键用了CASCADE,这本书的所有章节会被自动删除,这个符合业务预期。但如果Chapter的novel外键误用了PROTECT,那你删除小说时就会报ProtectedError,必须先把章节删掉。所以设计 Model 时要想清楚“谁是父、谁是子、删了之后子怎么办”,这套关系一旦上线,后面再想改迁移就很麻烦。
3. 核心功能模块实现:书架、阅读记录与搜索方案
这部分是整套源码的精华所在。很多 Django 初学者看得懂 Model,看得懂视图,但一遇到“这个功能具体怎么组织代码”就卡住了。我挑三个有代表性的核心功能展开讲,它们几乎覆盖了内容站开发的常用套路。
3.1 书架:去重、状态判断和性能
书架功能最核心的就是两件事:判断一本书是否已经被当前用户收藏,以及在个人中心展示收藏列表。
先看加入收藏的视图逻辑:
from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.views.decorators.http import require_POST from apps.novels.models import Novel from apps.operations.models import Shelf @login_required @require_POST def toggle_shelf(request, novel_id): novel = get_object_or_404(Novel, pk=novel_id) shelf_item, created = Shelf.objects.get_or_create( user=request.user, novel=novel, ) if created: Novel.objects.filter(pk=novel.pk).update(favorites=models.F('favorites') + 1) return JsonResponse({'status': 'added', 'favorites': novel.favorites + 1}) else: shelf_item.delete() Novel.objects.filter(pk=novel.pk).update(favorites=models.F('favorites') - 1) return JsonResponse({'status': 'removed', 'favorites': novel.favorites - 1})这里用到了get_or_create,它本质上先尝试查一条记录,不存在则创建。加了unique_together约束后,并发情况下也不会出现重复的收藏记录。favorites字段用F()表达式做原子加减,避免多用户同时收藏时读到旧值覆盖新值,这个细节在高并发场景下很重要。
在模板里判断“这本书是否已加入书架”,最直接的做法是给模板传入一个novel_ids集合:
shelf_novel_ids = Shelf.objects.filter(user=request.user).values_list('novel_id', flat=True)然后模板里判断{% if novel.id in shelf_novel_ids %}。这里要注意,values_list返回的是一个 QuerySet,判断是否包含时 Django 会执行一次 SQL,但因为它只查novel_id一列,且走的是唯一性索引,性能完全可以接受。不建议在模板里对每个小说单独执行一次Shelf.objects.filter(user=request.user, novel=novel).exists(),那样会有 N+1 查询。
3.2 阅读记录:断点续读如何实现
阅读记录的核心不是“记一条日志”,而是“断点续读”。用户今天看到第 100 章,明天打开详情页,要能直接显示“继续阅读第 101 章”,而不是从头找。
我的做法是,在阅读页渲染的时候同时更新ReadHistory:
@login_required def chapter_read(request, novel_id, chapter_id): novel = get_object_or_404(Novel, pk=novel_id) chapter = get_object_or_404(Chapter, pk=chapter_id, novel=novel) # 更新阅读记录:同一本小说只保留一条,指向当前章节 ReadHistory.objects.update_or_create( user=request.user, novel=novel, defaults={'chapter': chapter}, ) next_chapter = Chapter.objects.filter(novel=novel, sort__gt=chapter.sort).order_by('sort').first() prev_chapter = Chapter.objects.filter(novel=novel, sort__lt=chapter.sort).order_by('-sort').first() return render(request, 'novels/chapter_read.html', { 'novel': novel, 'chapter': chapter, 'prev_chapter': prev_chapter, 'next_chapter': next_chapter, })update_or_create是 Django 里非常实用的一个方法:如果存在就更新,不存在就创建。配合unique_together,可以保证每个用户每本书只有一条阅读历史,且一直指向最近阅读的那一章。上一章和下一章用sort字段来查,注意两条查询的方向不同,一条是“大于当前 sort 的最小值”,另一条是“小于当前 sort 的最大值”,这个逻辑要记牢。
个人中心的“最近阅读”列表,直接查ReadHistory并按read_at倒序就行,每个小说取最近一条。如果有“展示最近看过的 10 本书”这种需求,最省事的办法是查ReadHistory后做去重,但为了性能,也可以在ReadHistory表里额外存一个冗余的novel_name,列表页直接展示冗余字段,避免连表查询。不过冗余字段会有数据一致性问题,小项目里我更推荐直接查询去重,代码简单、出错概率低。
3.3 搜索方案:从 LIKE 到 Whoosh
小说网站最常见的搜索需求是搜书名和作者名。最粗暴但最简单的方案是用 Django ORM 的icontains:
from django.db.models import Q def search(request): keyword = request.GET.get('q', '').strip() if keyword: novels = Novel.objects.filter( Q(name__icontains=keyword) | Q(author__icontains=keyword) )[:20] else: novels = Novel.objects.none() return render(request, 'novels/search_result.html', {'novels': novels, 'keyword': keyword})icontains对应的 SQL 是LIKE '%keyword%',数据量小的时候没问题,但小说站的书目量上来之后,LIKE '%xxx%'没法走索引,全表扫描会越来越慢。所以这套源码里我做了一个折中:搜索走一个独立的search.py模块,小数据量默认用数据库查询,同时预留了接入 Whoosh 的接口。
Whoosh 是纯 Python 实现的全文检索引擎,相比 Elasticsearch,它不需要额外部署服务,非常适合中小型 Django 项目。基本思路是启动时把所有小说(或更新时异步触发)写入索引,搜索时查索引得到 id 列表,再回数据库取详情。这套方案对 10 万级书目完全够用,部署成本也低。如果你未来书目量涨到百万级,再考虑把索引层换成 Elasticsearch,那是后话。
我个人建议:刚开始跑这套源码时先用icontains,重点理解业务逻辑;确认有性能瓶颈之后,再引入 Whoosh。不要在项目一开始就上重搜索方案,避免复杂度喧宾夺主。
4. 从源码包到本地跑起来:部署操作全流程
源码下载下来,第一件事当然是让它跑起来。我按步骤拆一遍,包括环境准备、依赖安装、数据库配置、迁移、初始化数据和启动服务。这套流程我自己在各个环境里跑过很多次,按下面顺序操作一般不会出问题。
4.1 环境准备与依赖安装
建议用 Python 3.8 到 3.11 之间的版本,太新的版本偶尔会遇到第三方库还没有预编译 wheel 的问题。先建虚拟环境再装依赖:
python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install -r requirements.txtrequirements.txt里主要的依赖有这些:
Django>=3.2,<4.0 PyMySQL>=1.0.2 Pillow>=9.0.0 django-redis>=5.2.0 gunicorn>=20.1.0PyMySQL是让 Django 连 MySQL 的驱动,Pillow是图片处理库,django-redis用于缓存和 Session 存储,gunicorn是生产环境的 WSGI 服务器。本地调试时可以用 Django 自带的开发服务器,但依赖里先装好 Gunicorn,后面部署直接用,不用再装一遍。
4.2 数据库配置与初始化
默认配置里数据库用的是 SQLite,方便本地零配置启动。如果你要接 MySQL,在config/settings.py里改成这样:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'novel_site', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'CONN_MAX_AGE': 60, } }注意,用 PyMySQL 时,一般还需要在项目的__init__.py里加一句:
import pymysql pymysql.install_as_MySQLdb()否则 Django 会去找 MySQLdb,而 MySQLdb 在 Python 3 环境下安装比较麻烦。这个细节是初学者最容易忽略的,报错往往长这样:ModuleNotFoundError: No module named 'MySQLdb'。
初始化数据库和创建超级管理员:
python manage.py makemigrations python manage.py migrate python manage.py createsuperusermakemigrations会扫描 Model 和已有迁移文件的差异,生成新的迁移文件;migrate把这些迁移应用到数据库。首次执行时,Django 会帮我们建好所有内置表(auth、sessions、admin 等)和业务表。如果migrate时报外键相关错误,通常是数据库字符集或引擎问题,后面避坑部分会专门讲。
4.3 导入测试数据和启动
为了让你能看到效果,源码包里的scripts/generate_data.py可以生成一批测试小说和章节数据:
python scripts/generate_data.py --novels 50 --chapters-per-novel 30这个脚本做了几件事:创建分类(玄幻、都市、历史、科幻等)、为每个分类生成随机小说、为每本小说生成指定数量的章节内容。生成的内容是假的,但结构是真的,足够你体验完整功能。
然后启动开发服务器:
python manage.py runserver 0.0.0.0:8000浏览器打开http://127.0.0.1:8000,就能看到小说首页。后台管理地址是http://127.0.0.1:8000/admin/,用刚才创建的超级管理员账号登录。
4.4 启动后优先检查的三件事
第一步跑通之后,不要急着改代码,先做三个检查:
第一,打开小说详情页,看看封面图片能否正常显示。如果显示不了,很可能是MEDIA_URL和STATIC_URL没配对。开发环境里 Django 不会帮你托管 media 文件,需要在项目的urls.py里临时加一段:
from django.conf import settings from django.conf.urls.static import static if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)第二,注册一个新账号,测试收藏和阅读历史功能。如果登录状态失效或者 CSRF 报错,检查settings.py里的CSRF_TRUSTED_ORIGINS,开发环境下一般是空列表没问题,但如果你用了别的主机名访问,需要把它加进去。
第三,清空一次浏览器缓存,确认静态文件(CSS、JS)正常加载。跑起来之后页面光秃秃没有样式,99% 是STATIC_URL配置和模板里静态文件引用的路径对不上,这个问题在生产部署的踩坑里还会再遇到一次。
5. 从本地到线上:生产部署和性能优化
本地跑通只是第一步。小说网站一旦上线,就会面临静态文件托管、并发连接、数据库连接耗尽、缓存策略这些问题。这套源码不只是在本地能跑,按照下面的方式部署到云服务器上,也能撑住日常流量。
5.1 静态文件和媒体文件的处理
开发环境下 Django 自己托管静态文件,但生产环境绝不能这么做,正确姿势是:用collectstatic把静态文件收集到一个目录,交给 Nginx 托管。
python manage.py collectstatic --noinput在settings.py里:
DEBUG = False ALLOWED_HOSTS = ['your_domain.com', 'www.your_domain.com'] STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'Nginx 配置的关键片段:
location /static/ { alias /path/to/novel_site/staticfiles/; } location /media/ { alias /path/to/novel_site/media/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }这里有个很常见的坑:本地DEBUG=True时媒体文件正常显示,一旦切到DEBUG=False,Django 不再处理媒体文件,如果 Nginx 没配好/media/的 alias,所有用户上传的封面就会全部 404。所以上线前一定要先检查 Nginx 的媒体文件路由。
5.2 Gunicorn 与反向代理
WSGI 服务器我选 Gunicorn,多进程模型,稳定可靠。启动命令建议写成:
gunicorn config.wsgi:application \ --bind 0.0.0.0:8000 \ --workers 3 \ --threads 2 \ --timeout 60 \ --access-logfile -workers数量没有绝对标准,一般建议2 x CPU 核数 + 1。如果服务器是 2 核,3 个 worker 是比较合理的起点。threads可以让每个 worker 内再跑几个线程,适合 IO 密集型的 Django 应用。timeout设置成 60 秒,防止某些慢接口被 Gunicorn 提前杀掉。
还有个容易踩的坑:Django 默认每请求创建一个数据库连接,流量稍微上来,MySQL 的max_connections很容易被打满。所以数据库配置里CONN_MAX_AGE建议设置成 60 到 600 之间的值,让连接在进程内复用,减少频繁创建和断开连接的开销。但要注意,设了CONN_MAX_AGE之后,如果 MySQL 的wait_timeout比较短,空闲连接会被 MySQL 服务端断开,Django 在检测到连接失效后会重连,这个机制是自动的,只是在日志里偶尔能看到MySQL server has gone away,不用慌。
5.3 Redis 缓存和页面级缓存
小说站的访问热点非常集中:首页、分类列表页、小说详情页。这些页面每次请求都查数据库,即使加索引,QPS 高了之后还是会有压力。我的做法是引入 Redis 做两层缓存。
第一层是 Django 的视图缓存。以小说详情页为例,一本书的信息基本是固定的,只有点击量会变。点击量其实没必要实时写数据库,可以先用 Redis 计数,定期批量回写。详情页缓存可以设置几分钟的过期时间:
from django.views.decorators.cache import cache_page urlpatterns = [ path('novel/<int:pk>/', cache_page(60 * 5)(NovelDetailView.as_view()), name='novel_detail'), ]第二层是从数据库查询结果缓存。列表页的分页数据,比如“玄幻分类按更新时间排序第 3 页”,这种查询结果可以缓存 10 分钟左右,只要 KEY 设计合理,包含分类 id、页码、排序方式,就能显著减少数据库压力。
Redis 缓存配置:
CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': 'redis://127.0.0.1:6379/1', 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', }, 'TIMEOUT': 300, } }页面级缓存最适合小说详情、列表这种读多写少的场景。对于阅读页,因为每个用户看到的章节内容是一样的,也可以做,但要注意登录用户的阅读记录还是会实时更新的,所以要么把阅读记录接口和阅读页分开,要么对未登录用户做缓存、登录用户跳过缓存,这个需要根据实际情况取舍。
5.4 数据库慢查询和索引优化
项目里我提前在几个高频查询字段上加了db_index=True:小说的name、author,章节的sort。这些字段在搜索、排序、翻页时都会用到,加了索引之后查询计划会走索引扫描而不是全表扫描。
如果你后续自己加字段,可以用 Django 的connection.queries或数据库慢查询日志来定位问题。MySQL 下可以开慢查询日志:
slow_query_log = ON slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1long_query_time设置成 1 秒,超过 1 秒的 SQL 会被记录。Django 的 ORM 生成的 SQL 虽然可读性一般,但通过日志能大致定位是哪张表、哪个条件的查询慢,再针对性加索引或者改写查询方式。
6. 实际踩过的坑和二次开发建议
最后这部分,我把实际使用过程中踩过的一些坑按“问题现象、原因、解决方案”的方式列出来。这些坑不是我编的,而是这套源码在多个环境、多个版本组合下真实遇到过的。你如果跑的时候遇到类似问题,直接对照排查会快很多。
6.1 时区问题:发布时间差 8 小时
如果你在settings.py里看到USE_TZ = True、TIME_ZONE = 'UTC',然后发现后台创建的小说时间比本地北京时间慢 8 小时,这就是时区配置的典型问题。
原因很简单:Django 在USE_TZ=True时,数据库里存的是 UTC 时间,模板渲染时会根据TIME_ZONE转成当地时区。如果TIME_ZONE没设为Asia/Shanghai,展示出来的就是 UTC 时间。
解决方案:
USE_TZ = True TIME_ZONE = 'Asia/Shanghai'注意,USE_TZ=True和TIME_ZONE是配合工作的,比较稳妥的做法是数据库统一存 UTC,展示层统一转北京时间。直接在应用层用datetime.now()拿本地时间反而会在跨时区部署时出问题。
6.2 MySQL 保存大章节内容失败
小说章节正文是TextField,一本书如果章节很多、单章字数很大,保存的时候容易遇到Packet too large或者max_allowed_packet相关的报错。这是 MySQL 的一个硬限制,默认max_allowed_packet通常是 4MB 或 64MB,如果单章内容超过这个值,写入就会被拒绝。
查一下当前值:
SHOW VARIABLES LIKE 'max_allowed_packet';一般建议改成 64MB 或 128MB,在 MySQL 配置文件里设置:
[mysqld] max_allowed_packet = 128M character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ciutf8mb4也值得单独说一句:Django 在 MySQL 下默认用的字符集如果不对,中文可能存不了生僻字,所以连接串里最好显式指定'OPTIONS': {'charset': 'utf8mb4'}。
6.3 collectstatic 后样式丢失
本地开发一切正常,collectstatic之后部署到服务器,页面 CSS 全部失效。这个坑几乎每个人都踩过。
排查思路:先确认 Nginx 的/static/路径是否能访问到STATIC_ROOT目录下的文件;再确认STATIC_ROOT和STATIC_URL是否配对;最后看模板里是用{% static 'css/base.css' %}还是硬编码/static/css/base.css,前者在STATIC_URL改变时能自动适配,后者就是写死路径,换配置就失效。
6.4 迁移文件冲突
这套源码如果被人 fork 之后各自改 Model,再合并的时候很容易出现“多个迁移文件修改同一个字段”的冲突。Django 迁移框架能处理简单的依赖,但遇到冲突时,需要用python manage.py makemigrations --merge生成一个合并迁移。
我的建议是:如果你在已有数据的基础上改 Model,最好不要直接删迁移文件重建。迁移文件一旦应用到数据库,就和库里的django_migrations表对应上了,删了重建往往会让migrate认为迁移没有执行,导致重复建表报错。正确的做法是新建一个迁移文件,让它只包含本次的变更。
6.5 二次开发建议
这套源码最合适的用法是当基底,而不是当终点。按我现在做内容站的经验,以下几个方向是最常被问到的:
第一,加评论和评分模块。小说站天然需要读者互动,评论模块可以复用 Django 自带用户体系,新增一个Comment模型,关联小说和用户。要注意的是防刷和内容过滤,至少做个简单的频率限制,比如同一个用户 30 秒内只能评论一次。
第二,接入第三方登录。当前只支持用户名密码注册,对普通用户来说门槛偏高。接入django-allauth是成本很低的做法,支持微信、QQ、微博等国内常用方式,配置文档也很完善。
第三,改造成 API 接口供小程序或 App 使用。目前源码是模板渲染,如果你想做个小说 App,可以只保留 Model 层和业务逻辑,用 Django REST Framework 重写视图,返回 JSON 数据。开发时注意不要为了 DRF 而 DRF,模板渲染和 API 的适用场景不同,数据量小、以 Web 为主的话,模板渲染反而简单直接。
第四,部署到 NAS 或内网服务器。现在很多人喜欢在内网 NAS 上跑一个个人阅读站,Django 项目完全可以做到。因为 NAS 的 CPU 通常不强,部署时可以只跑一个 Gunicorn worker,把数据库换成 SQLite 也够用。要留意的是 Python 版本和系统依赖,NAS 厂商自带的 Python 环境往往比较旧,建议用 Docker 跑,把 Python 版本、依赖、代码全部打成镜像,迁移起来会省很多心。
我实际跑这套源码到现在,最大的体会是:看源码和学习框架是两回事。源码给你的是完整的上下文,从 Model 到 URL 到模板是一个闭环,比零散看文档更容易建立全局观。拿到源码之后,建议先不要急着跑,把models.py从头读一遍,理顺表之间的关系,再对照页面看每个功能是怎么从数据库里拿数据的。这个过程走完,你对 Django 的理解会比刷十篇教程都深。如果你在这个基础上有自己的改造思路,照着上面的方向去扩展就行,代码结构已经给你留好了余地。
本文还有配套的精品资源,点击获取