
简介面向Python初学者与计算机专业学生的校园在线聊天系统Django项目源码包完整覆盖从后台管理到前端交互的实现链路属于典型的课程级/毕业设计类完整项目。项目基于Python 3.8、Django与MySQL 5.7前端采用HTML、CSS、JavaScript系统围绕管理员与普通用户双角色展开管理员可审核用户、管理交友/学习/生活服务三类主题场景、设置锁定并查看问答统计普通用户则可修改资料、进入未锁定主题聊天、在用户列表中添加好友并互相收发消息。资源共392个文件核心代码包括46个Python源文件、44个JavaScript脚本、25个CSS样式、11个HTML页面和36个pyc编译文件附带SQL数据库与LW文档另有78个gif动图和41个png图片压缩包整体约187MB目录结构清晰、可直接运行适合快速部署与二次开发。已有93人浏览学习尤其适合作为毕业设计、课程设计、大作业或工程实训的完整项目参考。1. 校园chat在线聊天系统(django)这个压缩包解压后到底能拿来干什么拿到5p050校园chat在线聊天系统(django).zip这个名字第一反应是这多半是一个用 Django 写的、面向校园场景的即时聊天项目。解压后你会看到 manage.py、一个主项目目录以及若干个 app再往下翻就是用户注册、好友/房间列表、消息记录这类模块。它不是一个追求高并发的分布式聊天中台而是一个把「用户、会话、消息」三件事讲清楚的 Django 项目实战样本适合两类人一类是刚学完 Django 基础、想照着完整项目复现一遍的实战新手另一类是手里有校园信息化需求想评估「用 Django 做聊天到底划不划算」的开发者。本文就从拆结构开始一路讲到本地跑通、实时性改造、上线部署和排坑保证每一步都能直接照做。2. 拆开项目的技术骨架Django建模、路由与会话是怎么组织的2.1 项目根目录与app划分先看清startproject之后多出来的部分任何 Django 项目入口都是manage.py。校园聊天系统也不会例外它会在项目根目录下放一个主配置目录通常叫chat_project或直接用项目名里面是settings.py、urls.py、wsgi.py。真正干活的是各个 app 目录。聊天类项目常见的 app 划分是users用户注册、登录、资料、chat会话窗口、消息发送、历史记录有时还有一个room或friends负责好友和群组关系。收到压缩包后先别急着runserver花两分钟把目录结构读一遍。# 解压后先做一次只读检查不修改任何文件 unzip 5p050校园chat在线聊天系统\(django\).zip -d campus_chat cd campus_chat ls -la find . -maxdepth 2 -type d | sort | head -50逻辑说明第一个ls -la是看有没有隐藏文件比如.env、.gitignore有时候数据库连接串、SECRET_KEY 会被随手写进.env。find限定两层目录深度是为了看出 app 划分而不被 migrations、static 里的大量文件淹没。如果项目结构杂乱比如多个 app 的职责混在一起后面改消息模型时会非常痛苦。参数说明压缩包解压后的目录名带括号和中文命令行里必须用反斜杠转义空格和括号或者干脆先mv改成一个纯英文目录名避免 Windows 和 Linux 下 shell 解析出错。这一步虽然简单但很多人都在这上面浪费过时间。2.2 用户模型与消息模型用Django自带的User还是扩展Profile聊天系统最核心的建模是消息表。用户可以直接用 Django 内置的auth.User因为校园场景下登录名就是学号或工号密码交给 Django 的认证体系管理就够了。但如果你需要存班级、院系、入学年份就不要硬改内置用户表而是用OneToOneField扩展一个Profile。消息表的设计直接决定后面查询聊天记录顺不顺手。# chat/models.py from django.db import models from django.conf import settings class Room(models.Model): name models.CharField(max_length64, uniqueTrue, verbose_name会话名称) is_group models.BooleanField(defaultFalse, verbose_name是否群聊) members models.ManyToManyField( settings.AUTH_USER_MODEL, related_namechat_rooms, blankTrue, ) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.name class Message(models.Model): room models.ForeignKey( Room, on_deletemodels.CASCADE, related_namemessages, verbose_name所属会话 ) sender models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namesent_messages, verbose_name发送人 ) content models.TextField(verbose_name消息内容) created_at models.DateTimeField(auto_now_addTrue, db_indexTrue, verbose_name发送时间) is_read models.BooleanField(defaultFalse, verbose_name是否已读) class Meta: ordering [created_at] def __str__(self): return f{self.sender} 在 {self.room} 发送的消息逻辑说明把Message.room做成ForeignKey而不是直接存一个recipient是因为这样既能支持单聊也能支持群聊——单聊就是is_groupFalse且members只有两个人的房间群聊则把多人拉进同一个 Room。用related_namemessages后想取某个会话所有消息直接写room.messages.all()就行不用再写Message.objects.filter(roomroom)。这是聊天项目里最常用的关联查询方式。参数说明db_indexTrue加在created_at上是因为聊天记录翻页基本都按时间排序这个索引在消息量到几万条时能明显降低查询耗时。on_deletemodels.CASCADE的含义是删除用户或会话时连带删除消息避免留下无主的脏数据。这里顺便说一个热门的 Django 删除查询如果要做「清理 90 天前消息」的定时任务正确写法是Message.objects.filter(created_at__ltcutoff).delete()不要用for msg in Message.objects.all(): msg.delete()后者会逐条触发信号慢且容易把数据库锁死。2.3 路由与视图页面路由和接口路由分开还是参数化聊天的页面路由一般用 Django 的模板渲染体系接口路由则负责返回 JSON 给前端轮询或者收 WebSocket 消息。校园项目通常会把两者都放在chat/urls.py里用path()做参数化路由而不是写死每一个聊天室。# chat/urls.py from django.urls import path from . import views app_name chat urlpatterns [ path(rooms/, views.room_list, nameroom_list), path(room/int:room_id/, views.room_detail, nameroom_detail), path(api/messages/int:room_id/, views.message_list_api, namemessage_list_api), path(api/messages/create/, views.message_create_api, namemessage_create_api), ]# chat/views.py 节选 from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.shortcuts import get_object_or_404, render from .models import Room, Message login_required def room_detail(request, room_id): room get_object_or_404(Room, pkroom_id, membersrequest.user) messages room.messages.select_related(sender)[:200] return render(request, chat/room_detail.html, {room: room, messages: messages}) login_required def message_list_api(request, room_id): room get_object_or_404(Room, pkroom_id, membersrequest.user) after request.GET.get(after, 0) messages room.messages.filter(id__gtafter).select_related(sender) data [ {id: m.id, sender: m.sender.username, content: m.content, created_at: m.created_at.strftime(%H:%M:%S)} for m in messages ] return JsonResponse({messages: data})逻辑说明room_detail负责渲染聊天窗口页面message_list_api给前端做增量拉取。filter(id__gtafter)是聊天轮询里最常见的增量查询前端把当前页面最大消息 ID 传进来后端只返回比它新的消息避免每次把全量聊天记录重传一遍。select_related(sender)是这里最值得记住的优化点它用一条 SQL 把消息和发送人两张表 join 出来避免在循环里逐条查用户表造成 N1 查询。参数说明路由里的int:room_id是 Django 2.0 之后推荐的写法替代了老项目里常见的(?Proom_id\d)正则。login_required没写异常处理未登录用户会被重定向到登录页这是在校园内网环境下的合理默认。membersrequest.user这个过滤条件很多人会漏掉它保证了用户只能访问自己加入的会话是聊天系统权限控制里最简单的防线。3. 把zip跑成本地服务从解压到两个人开始聊天的全套操作3.1 环境准备python版本、虚拟环境与依赖安装的顺序拿到项目先别急着装依赖。我建议按「Python 版本 → venv → pip 安装 → 版本校准」这个顺序来。校园聊天这种打包项目大概率基于 Django 3.2 或 4.x 编写的所以先用python3 --version确认本机是 3.8 到 3.12 之间太新的 Python 有时会遇上依赖轮子还没适配的情况。cd campus_chat python3 -m venv venv source venv/bin/activate # 有 requirements.txt 就按它装没有就先把 Django 装好再跑起来缺什么补什么 pip install --upgrade pip if [ -f requirements.txt ]; then pip install -r requirements.txt else pip install django fi pip list | grep -i -E django|channels|redis|pillow逻辑说明pip list这一步非常重要它直接告诉你这个项目的技术栈里有没有channelsWebSocket 支持、有没有redis消息队列从而判断项目本身到底做没做实时推送。如果requirements.txt里 Django 版本是Django3.2.18里面却说channels3.0.5那就说明作者用的是 ASGI 模式后面启动用的命令不是runserver而是runserver加 ASGI 支持。版本对不上时最常见的现象是python manage.py migrate报TypeError或者字段相关错误。参数说明虚拟环境目录venv不要提交到 git所以压缩包里一般也不应该带venv。如果解压后看到一个巨大的venv/或node_modules/优先怀疑是作者打包失误正确做法是删掉重建而不是直接复用因为别人的虚拟环境里的路径和 Python 小版本大概率和你机器不匹配。3.2 数据库迁移与超级用户第一次启动前必须做的三件事跑通 Django 项目的标准动作是makemigrations→migrate→createsuperuser顺序不能乱。如果项目自带了db.sqlite3文件我建议先把它改名备份重新生成一份干净的数据库避免作者开发时留下的测试数据污染你的环境。# 如果压缩包自带 sqlite 数据库先备份再新建 if [ -f db.sqlite3 ]; then mv db.sqlite3 db.sqlite3.bak_$(date %Y%m%d) fi python manage.py makemigrations python manage.py migrate python manage.py createsuperuser逻辑说明makemigrations只做一件事——对比模型定义和迁移记录生成新的迁移文件。如果你运行后提示No changes detected先不要慌检查这个 app 有没有写进settings.py的INSTALLED_APPS里。很多新手自己新建了chat目录却没有在settings.py里注册Django 根本不知道它的存在。# settings.py 里的 INSTALLED_APPS 至少要有这些 INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, users, # 用户模块自己定义的 app chat, # 聊天核心 app ]参数说明createsuperuser会依次询问用户名、邮箱、密码。校园场景下建议用户名直接采用学号或工号密码强度要求可以临时放宽但上线前必须强制改成强密码。这一步创建的超级用户用来登录 Django admin也用来在后端手动给某个用户加好友、建会话。另外SQLite 的数据库文件路径是相对manage.py所在目录的所以后续执行命令不要cd到别的目录再调用python manage.py否则会生成一个全新的空数据库让你以为迁移白做了。3.3 启动runserver并用双浏览器验证聊天闭环环境就绪后本地启动开发服务器然后打开两个浏览器窗口一个用超级用户登录一个重新注册一个普通账号互发一条消息看能不能出现在对方的会话列表里。python manage.py runserver 0.0.0.0:8000# 在另一个终端窗口验证消息是否入库 python manage.py shell -c from chat.models import Room, Message from django.contrib.auth.models import User u1 User.objects.get(username你的账号) u2 User.objects.get(username第二个账号) room Room.objects.filter(membersu1).filter(membersu2).first() if room: print(找到了共同会话:, room.name, 消息数:, room.messages.count()) else: print(还没有共同会话先去网页上建一个) 逻辑说明runserver 0.0.0.0:8000里的0.0.0.0表示监听所有网卡这样同一局域网内的其他电脑可以通过http://你的IP:8000访问。注意 Django 的runserver自带自动重载改代码会重启但改settings.py里的数据库配置时偶尔会失效这时候手动按CtrlC再重启最保险。参数说明双浏览器验证的核心不是看页面好不好看而是确认两个关键点第一Room.objects.filter(membersu1).filter(membersu2)能同时匹配两个用户说明多对多关系的查询条件写对了第二消息写入后room.messages.count()随发送次数递增说明外键关联没有断裂。很多所谓「可以聊天」的 demo实际上消息只是存在前端变量里一刷新就丢这个验证步骤就是用来戳穿这种假聊天的。如果前端用了localStorage存消息数据库里永远是空表这种项目上线后没有任何历史记录价值属于只能演示不能用的性质。4. 消息实时性与上线部署从轮询到WebSocket再到NginxGunicorn4.1 校园聊天为什么先用轮询也够用HTTP轮询的最小实现校园聊天场景和互联网大厂的高并发 IM 有本质区别——同一时刻在线人数基本就是几栋教学楼的人消息频率也远低于双十一客服系统。在这种前提下直接用 HTTP 轮询反而是最稳的方案不需要额外引入消息队列和 WebSocket 网关部署结构简单排错容易。前端每隔几秒请求一次增量接口就能达到「看起来实时」的效果。// chat/static/js/polling.js let lastMessageId 0; async function fetchNewMessages(roomId) { try { const resp await fetch(/api/messages/${roomId}/?after${lastMessageId}, { headers: { X-Requested-With: XMLHttpRequest } }); const data await resp.json(); if (data.messages.length 0) { lastMessageId data.messages[data.messages.length - 1].id; appendMessages(data.messages); // 把新消息渲染到页面 } } catch (err) { console.warn(轮询失败下次继续, err); } } // 每 3 秒拉一次增量消息 setInterval(() { const roomId document.querySelector(#room-id).value; fetchNewMessages(roomId); }, 3000);逻辑说明这个轮询代码的巧妙之处在after参数。前端永远只关心比lastMessageId新的消息后端过滤掉历史数据网络传输量就很小。X-Requested-With头是告知后端这是一个 AJAX 请求方便中间件区分普通页面访问和接口调用。3 秒的间隔在校园网环境下体验尚可而且请求体极小并不会给 Django 开发服务器造成压力。参数说明轮询间隔是个需要反复调的数——间隔越短实时性越好但请求量线性增长。对校园项目来说3 秒是甜点值如果要求秒开可以改成 1 秒但要确认后端能扛住如果只是班级公告场景5 秒甚至 10 秒都行。这是典型的「参数跟着场景走」的例子。轮询最大的坑是接口返回的data.messages顺序必须与数据库排序一致否则lastMessageId取最大值会出错导致消息漏拉。后端ordering [created_at]只能保证同一秒内排序稳定如果出现两条消息 ID 相同的时间戳需要把排序改成[id]或[created_at, id]。4.2 改造成WebSocket的正确姿势channels、ASGI与consumer如果你要做的不是课程作业而是真正上线轮询在超过一两百人同时在线时响应速度会明显下降。这时就该引入 Django Channels把长轮询或短轮询升级成 WebSocket。Django 原生是 WSGI 同步模型WebSocket 需要 ASGI 异步模型所以改造的第一步是安装 channels 并配置 ASGI 应用。pip install channels3.0.5 channels-redis3.4.1# settings.py 增加 ASGI 配置与 channel layer INSTALLED_APPS INSTALLED_APPS [channels] ASGI_APPLICATION chat_project.asgi.application CHANNEL_LAYERS { default: { BACKEND: channels_redis.core.RedisChannelLayer, CONFIG: { hosts: [(127.0.0.1, 6379)], }, }, }# chat/consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): self.room_id self.scope[url_route][kwargs][room_id] self.group_name froom_{self.room_id} await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def receive(self, text_data): data json.loads(text_data) content data[content] # 异步写库避免阻塞事件循环 await self.channel_layer.group_send( self.group_name, {type: chat.message, content: content}, ) async def chat_message(self, event): await self.send(text_datajson.dumps({content: event[content]}))逻辑说明AsyncWebsocketConsumer是 Channels 提供的异步基类connect里先把当前连接加入room_xxx这个 channel group这样同一个会话里其他人发消息你都能收到。receive收到前端文本后直接广播给同组所有人。这里刻意省掉了数据库写入是为了先跑通实时链路避免思路被 IO 打断实际项目里应该在receive里同步或异步写入Message表。参数说明Channels 3.x 对应 Django 3.2 和 4.x但如果你用的 Django 5.0建议直接上 Channels 4.x接口变化不大底层对 asyncio 的支持更好。CHANNEL_LAYERS里的hosts可以填多个 Redis 地址校园部署时一个 Redis 实例足够。最关键的坑是没有 Redis 时不要硬上 channels-redis可以先用channels.layers.InMemoryChannelLayer替代它不需要任何外部服务但多个 Django 进程之间无法共享消息只能用于单机开发调试。前端连接 WebSocket 的地址也有讲究。浏览器里的ws://协议对应后端 ASGI 路由不能用fetch去 POST 这个地址否则就会遇到网上一搜一大把的报错[error] unexpected endpoint or method. (POST /chat/completions). returning 2这类问题——本质上是把 HTTP 请求发到了只接受 WebSocket 握手的地方。# 启动 ASGI 开发服务器不要用 runserver它不是 ASGI 的完整实现 daphne -b 0.0.0.0 -p 8000 chat_project.asgi:application4.3 部署到公网或校内服务器Gunicorn、Nginx与静态文件的顺序开发环境下runserver能托管静态文件和 Python 代码但上线意味着你必须切换成「Gunicorn 跑 Django 逻辑 Nginx 托管静态文件 反向代理 WebSocket」的三层结构。这个顺序是无数血泪经验攒出来的先改settings.py再配 Gunicorn最后写 Nginx每一步都要单独验证。# settings.py 生产环境必须改的配置 DEBUG False ALLOWED_HOSTS [chat.example.edu.cn, 你的服务器IP] CSRF_TRUSTED_ORIGINS [https://chat.example.edu.cn] STATIC_ROOT /var/www/campus_chat/static# 收集静态文件这一步经常被漏掉导致上线后 CSS 全部 404 python manage.py collectstatic --noinput # 用 gunicorn 启动 Django 应用 pip install gunicorn gunicorn chat_project.wsgi:application \ --bind 127.0.0.1:8000 \ --workers 3 \ --timeout 60 \ --access-logfile -# /etc/nginx/sites-available/campus_chat server { listen 80; server_name chat.example.edu.cn; location /static/ { alias /var/www/campus_chat/static/; } 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; } # WebSocket 升级端点 location /ws/ { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 3600s; } }逻辑说明DEBUG False之后Django 默认不再托管静态文件所以必须先collectstatic把各 app 的静态资源汇总到STATIC_ROOT。Nginx 里/static/直接走磁盘文件不经过 Python响应速度快得多。WebSocket 的location /ws/是最容易出错的地方——Nginx 默认 HTTP/1.0 不支持长连接必须显式设置Connection upgrade和Upgrade $http_upgradeproxy_read_timeout也要调到几分钟级别否则连接会在默认 60 秒后被 Nginx 掐断。参数说明Gunicorn 的--workers 3对一个小型校园聊天系统够用了但注意 WebSocket 不走 Gunicorn而是走 Daphne 或 Uvicorn所以生产环境需要同时跑两个进程Gunicorn 处理普通 HTTPDaphne 处理/ws/流量。Nginx 里写location /ws/时proxy_pass末尾不要加路径否则会把/ws/前缀吞掉导致后端 ASGI 路由匹配不到room_id。这个细节能排查掉一多半「WebSocket 握手成功但发不了消息」的诡异现象。组件端口职责启动方式Nginx80/443静态文件、反向代理、WebSocket 升级systemctl start nginxGunicorn127.0.0.1:8000普通 HTTP 请求gunicorn 命令Daphne127.0.0.1:8001WebSocket 连接daphne 命令Redis6379Channel layer 消息中转systemctl start redis5. 高频踩坑排查从迁移报错到聊天历史丢失的修复记录5.1 迁移与模型类坑No changes detected、删除对象被外键拦住坑 1python manage.py makemigrations提示No changes detected但模型文件里明明写了新字段。现象model 里加了字段执行迁移却提示没有变化数据库表结构一直不更新。原因chatapp 没有写进settings.py的INSTALLED_APPS。Django 只扫描已注册的 app你写的模型文件躺在目录里但它根本没被认领。解决去settings.py的INSTALLED_APPS里补上chat然后重新执行makemigrations chat。如果确认已注册还报这个问题就检查chat/migrations/目录是否存在且里面有__init__.py文件没有__init__.py的 migrations 目录会被 Django 直接忽略。坑 2执行Message.objects.filter(roomroom).delete()删除旧消息结果报ProtectedError删不掉。现象想清理一个会话的历史消息调用delete()时抛异常提示某些外键关联阻止了删除。原因消息表本身没问题但其他表比如MessageRead已读记录表用on_deletemodels.PROTECT或DO_NOTHING指向了Message。Django 宁可给你报错也不愿意留下指向不存在记录的孤儿数据。解决不要硬删先把已读记录等关联数据清理掉再删消息。或者干脆把相关外键改成on_deletemodels.CASCADE并重新迁移让数据库自动级联删除。但如果后面要接数据分析历史消息建议做软删除——加一个is_deleted字段查询时过滤掉即可给自己留个后悔药。5.2 会话、CSRF与WebSocket类坑发不出消息、403、端点歧义坑 3前端点击发送后端runserver报 403 CSRF verification failed消息发不出去。现象接口用fetchPOST 到/api/messages/create/表单页面正常但 AJAX 请求全部被 Django 的 CSRF 中间件拦下。原因Django 默认要求所有 POST 请求携带csrftoken。模板渲染时{% csrf_token %}会生成隐藏字段但fetch不会自动带上它于是每个 AJAX 请求都撞在 CSRF 校验上。很多人的第一反应是给视图加csrf_exempt这是典型的饮鸩止渴——解除保护后任何人都可以伪装成登录用户灌消息。解决在模板里读取 cookie 中的csrftoken手动加到请求头。Django 官方文档推荐的方式是// 从 cookie 里拿到 csrf token function getCookie(name) { let cookieValue null; if (document.cookie document.cookie ! ) { document.cookie.split(;).forEach(cookie { const parts cookie.split(, 2); if (parts[0].trim() name) { cookieValue decodeURIComponent(parts[1]); } }); } return cookieValue; } // 发送消息时带上 CSRF header fetch(/api/messages/create/, { method: POST, headers: { Content-Type: application/json, X-CSRFToken: getCookie(csrftoken), }, body: JSON.stringify({ room_id: 1, content: 你好 }), });逻辑说明getCookie(csrftoken)从浏览器 cookie 读取 Django 种下的令牌X-CSRFToken是 Django 中间件认可的请求头名称。前提是模板里渲染过一次{% csrf_token %}或者你通过ensure_csrf_cookie装饰器强制在页面响应里种 cookie。如果你为了图省事把 Response 改成JsonResponse直接返回消息数据别忘了响应也要经过csrf_exempt或正确带 token否则前端又会遇到莫名其妙的 403。坑 4WebSocket 握手成功但发送消息后收不到广播服务器日志里出现unexpected endpoint or method之类的字样。现象网页把ws://地址换成http://或者把 POST 请求发到/ws/room/1/地址上Channels 的消费者直接报错。原因WebSocket 的握手是 HTTP GET 请求带Upgrade头不是普通的 POST。如果你用fetch或XMLHttpRequest去 POST 一个为 WebSocket 设计的端点ASGI 服务器会把它当成非法请求拒绝掉。解决前端必须用new WebSocket(ws://你的域名/ws/room/1/)建立连接不能走 AJAX。如果你是在调试接口时在 POST 工具里看到的这个报错那只是把它当成 HTTP 接口调了重新确认是不是走错了协议。坑 5上线后聊天记录隔几天丢一次最后发现 SQLite 文件被覆盖了。现象数据库文件时而正常时而缺失检查发现db.sqlite3经常被重置聊天历史全部清空。原因SQLite 是单文件数据库如果有人把项目目录打包带走在另一台机器上跑migrate那次操作可能重建了数据库结构把所有历史数据冲掉。另一个常见原因是你执行了python manage.py migrate时db.sqlite3不在当前目录Django 自动新建了一个空库。解决给数据库做定时备份是唯一的后悔药。Django 的dumpdata可以把所有数据导出成 JSON 或 XML配合系统 crontab 每天备份一次。我在处理聊天类项目时还会额外做一层防护——把db.sqlite3路径改成绝对路径避免因为启动目录不同而误建空库。如果你接手项目后什么都没改数据库经常丢数据先看看有没有人手动在 server 上跑过python manage.py migrate那个命令的副作用就是重建数据表结构。6. 上线前验证十分钟自检清单与批量灌消息的小技巧最后一个环节不是写代码而是养成交付前的验证习惯。我现在拿到任何 Django 聊天项目第一件事不是点开页面而是先看settings.py里DEBUG是不是False、ALLOWED_HOSTS有没有配全、数据库是不是还是默认的 SQLite。这五个检查项能在十分钟内拦住绝大多数低级事故检查项命令或操作通过标准静态文件python manage.py collectstatic --noinput无报错且STATIC_ROOT目录非空WebSocket 握手浏览器 Console 执行new WebSocket(ws://你的域名/ws/room/1/)状态码 101无 404CSRF 与登录态双浏览器互发 3 条消息双方均能实时看到数据库Message表增加 6 条记录数据库备份执行python manage.py dumpdata backup.json文件大小大于 1KB 且能loaddata回读并发基础python manage.py shell批量造数据后再刷新页面页面加载时间无明显卡顿批量灌消息这个技巧特别实用手动一条条点发送太慢也无法验证增量拉取逻辑。我一般会用django shell直接造几百条消息把场景拉满再去看页面滚动流畅度和接口耗时python manage.py shell -c from chat.models import Room, Message from django.contrib.auth.models import User from django.utils import timezone import random room Room.objects.filter(is_groupTrue).first() sender User.objects.filter(is_superuserTrue).first() for i in range(200): Message.objects.create( roomroom, sendersender, contentf压测消息 {i} 号, created_attimezone.now() - timezone.timedelta(seconds300 - i * 3) ) print(当前会话消息总数:, room.messages.count()) 逻辑说明created_at手动往前拨是为了让消息保持「先旧后新」的时间分布模拟真实聊天场景。灌入 200 条后再刷新聊天页面如果你的消息列表是分页或只取最近 200 条就能看出有没有漏发、重复加载的问题。这比自己手动点几十次发送高效得多。参数说明这里的timezone.timedelta(seconds300 - i * 3)让第i条消息生成时间递增 3 秒正好对应轮询间隔。如果轮询逻辑写死了after参数但数据库时间字段不准增量拉取就会错乱这个灌数据脚本能直接暴露这类问题。我自己的一个执念是聊天系统上线前一定要在垃圾网络环境下试一次——把浏览器 Developer Tools 的 Network 改成Slow 3G看页面上的消息是不是还能按序到达。很多校园项目的无线网络并不稳定轮询失败后的重试逻辑、消息乱序恢复能力都得在这种环境下才能暴露出来。这个习惯救过我两次一次是发现轮询接口在断网重连后拿不到全量数据另一次是发现 WebSocket 掉线后前端没有自动重连。聊天这种东西用户对「消息丢了」和「消息延迟」的容忍度都非常低宁可多看几个坑也别等上线后变成黑匣子让人猜。希望帮到你。本文还有配套的精品资源点击获取