
简介基于Django的校园Chat在线聊天系统是一份适合毕设、课程设计或工程实训的完整项目源码包面向有一定Python基础、希望快速上手Web开发的学习者。系统分为管理员与普通用户两种角色管理员可管理注册用户、审核、维护交友/学习/生活服务等主题场景并查看问答统计普通用户可修改资料、按场景聊天、添加好友并私信互动。压缩包共392个文件187.27MB以Python源码、SQL数据库文件、HTML/CSS/JS前端页面为主另含说明文档、配置文件与少量资源文件可直接在python3.8djangomysql5.7环境下部署运行。已有93人学习下载。资源附可运行源码、SQL脚本和LW文档涵盖用户注册审核、主题场景锁定、问答统计等关键模块的实现目录结构清晰便于对照学习和二次开发。1. 校园chat在线聊天系统用Django写不酷但你能交付校园场景里做聊天最不缺的是炫酷方案最缺的是能交付、能维护的那一个。这个名为 5p050 校园chat在线聊天系统(django) 的源码包走的正是务实路线用 Django 的账号体系、Admin 后台和 ORM 把用户、会话、消息、未读全部管起来聊天本身用轻量轮询实现。反直觉的是在几千人规模的校园场景下它比一上来就上 WebSocket 更稳、更好改、更好部署。它适合 Django 项目实战新手拿来当练手对象也适合学生会、实验室要一套内部即时通讯工具。它不追求百万并发追求的是学生拿手机能登录、能发消息、能翻历史记录——这一套跑通了你对 Django 的模型设计、请求周期和部署的底子就都有了。2. 把校园chat源码包跑起来解压、虚拟环境与启动三连拿到 zip 后的第一件事不是找功能亮点而是让它在本地跑起来。源码包和纯教程最大的区别是你必须先处理环境、依赖、数据库迁移这三关之后才能看到界面。这三关也是判断一个 Django 项目写没写规范的最佳窗口——requirements.txt 在不在、migrations 目录全不全、settings 里有没有把本地配置写死全在这一步暴露。2.1 先认清 zip 里是什么标准 Django 项目布局这类校园聊天系统的源码包目录结构基本是教科书式的 Django 布局。解压后你会看到manage.py躺在根目录旁边应该有一个requirements.txt一个存放项目配置的目录通常叫config或project以及一个或几个 app 目录。聊天系统的核心 app 一般叫chat用户相关的可能叫accounts或users。如果你以后想加功能python manage.py startapp chat就是 Django 创建 app 的标准命令新 app 的views.py、models.py、migrations/会自动生成剩下的活就是往里面填代码。真正决定你能不能跑起来的是settings.py里的三处配置数据库、静态文件路径、以及INSTALLED_APPS里注册了哪些 app。源码包如果自带db.sqlite3你也许能省掉迁移步骤但我强烈建议别用别人提交的数据库——里面大概率有测试账号和脏数据而且你不知道对方的密码自己在本地重建一个更干净。先看目录别急着执行命令。2.2 启动最小路径venv、依赖、迁移、runserver这一套命令是所有 Django 项目的通用起手式。先解压创建虚拟环境装依赖再做迁移最后启动开发服务器# 解压后进入项目根目录 cd 5p050校园chat在线聊天系统 # 创建并激活虚拟环境隔离项目依赖避免污染系统 Python python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate # 如果没有 requirements.txt先手动安装 Django有就直接装全部依赖 pip install -r requirements.txt # 生成数据库表结构如果项目里改了 User 模型这一步会顺带建对应的表 python manage.py migrate # 启动开发服务器0.0.0.0 允许局域网内其他设备访问 python manage.py runserver 0.0.0.0:8000第一行解压命令里zip 文件名带括号在 bash 里必须用反斜杠转义或加引号。虚拟环境是血泪经验跳过它直接pip install的下场是半年后系统里装了一堆互相打架的包你这个项目要的 Django 版本跟另一个项目冲突最后谁跑不起来都不知道。runserver 0.0.0.0:8000的0.0.0.0不是让你上生产用的是为了让同寝室的人拿手机直接访问你的电脑 IP 测试聊天功能——校园聊天系统如果不支持局域网访问测试起来会非常别扭。提示如果migrate报错说表已存在多半是 zip 里带了旧的 db.sqlite3。删掉它再跑一次 migrate干净起步。装依赖时常遇到的问题有两个。一是pip install -r requirements.txt下载慢可以临时加-i https://pypi.tuna.tsinghua.edu.cn/simple换国内镜像源二是 requirements.txt 里锁了过旧的 Django 版本和当前 Python 版本不兼容报错信息最后几行会出现ModuleNotFoundError。这种情况不要硬刚把 Django 升到与 Python 兼容的版本再装。跑通runserver之后浏览器访问http://127.0.0.1:8000看到登录页或聊天页第一步就结束了。2.3 准备测试账号超级用户与两个学生账号聊天系统必须有两个以上账号才能测出双向收发。先用 Django 自带的创建超级用户命令建一个管理员再用 shell 批量造两个普通学生账号# 交互式创建管理员用于登录 Django Admin 后台 python manage.py createsuperuser# 用 shell 批量创建测试用户密码要在代码里指定 python manage.py shellfrom django.contrib.auth.models import User # 如果项目用了自定义用户模型这里换成对应模型 u1 User.objects.create_user(zhangsan, passwordstu123456) u2 User.objects.create_user(lisi, passwordstu123456) u1.first_name 张三 u2.first_name 李四 u1.save() u2.save()create_user会自动处理密码哈希不要直接用User.objects.create()然后明文存密码那会毁掉整个登录体系。如果项目里定义了自定义用户模型继承AbstractUser上面这段导入要改成from django.contrib.auth import get_user_model然后User get_user_model()否则会因为模型不一致报出莫名其妙的字段错误。测试用的密码别设太复杂但也不要弱到123456这种有些 Django 版本开了密码强度校验会在create_user阶段直接抛异常。账号建好后开两个浏览器隐身窗口一个登录 zhangsan一个登录 lisi互相发一条消息本地联调的核心链路就通了。到了这一步你才会真正理解校园聊天系统最基础的功能不是实时推送而是两个用户都能稳定登录、能确定对方是谁、消息能落库。这套链路通了后面聊实时性才有意义。3. 实时聊天的实现选型Django 同步生态里怎么做出“在线”效果Django 默认是同步框架一个请求占一个 worker 线程直到返回响应才释放。拿它做聊天最大的争议点在于“实时性怎么实现”。在校园这种几万人规模、同时在线可能只有一两千的场景里实时不一定非要 WebSocket。方案选型如果脱离规模谈先进性大概率会把自己拖进部署深渊。这里把三种常见做法的代价讲清楚你就明白为什么很多校园聊天项目最终都选了轮询。3.1 三种方案对比短轮询、长轮询、WebSocket 的取舍聊天实时性本质是一个问题别人发了消息我怎么知道三种方案的回答方式完全不同成本和体验也截然不同。对校园项目来说选型的第一原则是“团队里最不熟练的那个人能不能维护”其次才是延迟。方案实时性服务器成本实现复杂度典型适用场景短轮询2-3 秒延迟高但可控极低原生 Django 就能做小型群聊、通知、校园内部工具长轮询接近实时连接挂起占 worker 线程中需要处理超时和重连几十人同时聊天的私聊场景WebSocket毫秒级需要 ASGI、channel layer高涉及 Redis、通道管理直播弹幕、在线协作、万人聊天室短轮询的请求量可以做一笔非常直观的估算假设 1000 人同时在线每 3 秒刷新一次新消息接口峰值 QPS 大约是 333。Django 配合 Gunicorn 多 worker一台 4 核的服务器扛住这个量毫无压力。真正值得注意的是响应体要做小只返回新消息而不是全量历史记录流量和数据库压力都能压下去。如果这个项目最终要跑在校园的普通机房机器上短轮询是最省心的选择。长轮询在延迟上确实优于短轮询但它的模型是“挂住一个请求直到有消息才返回”。Django 的同步 worker 在处理长轮询时每个挂起的连接都会占用一个 worker 线程1000 人挂着就意味着 1000 个线程被占用后续普通请求可能排队到超时。WebSocket 则要引入 Channels改 ASGI 部署还要处理 Redis 做跨进程消息广播项目部署形态彻底改变。这两种方案的维护成本不适合一个要快速交付的校园源码包。3.2 先读懂消息模型会话、消息、未读是怎么关联的不管选哪种传输方案落库的模型设计都差不多。聊天系统最核心的是两张表会话表和消息表。会话表记录“谁和谁在聊”消息表记录“聊了什么”。下面这组模型是校园聊天系统里最常见的结构简单但够用# chat/models.py from django.db import models from django.contrib.auth.models import User class Conversation(models.Model): # 一个会话对应一段聊天关系单聊就是两个参与者群聊可复用同一张表 participants models.ManyToManyField(User, related_nameconversations) created_at models.DateTimeField(auto_now_addTrue) class Message(models.Model): # CASCADE 表示会话删除时消息跟着全部删除这是聊天记录最常见的处理方式 conversation models.ForeignKey(Conversation, on_deletemodels.CASCADE, related_namemessages) sender models.ForeignKey(User, on_deletemodels.CASCADE, related_namesent_messages) content models.TextField() is_read models.BooleanField(defaultFalse) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at] # 高频查询是“某个会话里按时间翻页”这个复合索引必须加 indexes [ models.Index(fields[conversation, created_at]), ]on_deletemodels.CASCADE在外键上的含义是主表记录删除时关联的子表记录连带删除。这就是 django 执行查询-删除对象时最经典的场景——一条conversation.delete()会沿着外键把该会话下的所有Message一次性清掉你不需要手动循环删除DRF 或 ORM 层面也不会报外键约束错误。如果某些业务要求会话删除但消息保留就把外键改成on_deletemodels.PROTECT或models.SET_NULL后者要求消息表里该字段加nullTrue。indexes里的复合索引是给“翻历史消息”这个高频查询用的。聊天记录只增不改用户滚动查看历史时SQL 会按conversation过滤再按created_at排序没有索引的话数据量到几万条就开始明显卡顿。索引不是越多越好但conversation 时间这组复合索引是聊天系统的标配别省。3.3 最小收发接口一个 POST 发消息一个 GET 拉新消息在短轮询方案下后端只需要两个接口发消息和历史增量拉取。增量拉取的关键是客户端记住最后一条消息的 ID下次请求把 ID 传回来后端只返回比这个 ID 大的消息# chat/views.py from django.http import JsonResponse from django.views.decorators.http import require_POST, require_GET from django.contrib.auth.decorators import login_required from .models import Conversation, Message login_required require_POST def send_message(request, conversation_id): # 只允许登录用户访问发消息必须 POST防止浏览器预加载触发 content request.POST.get(content, ).strip() if not content: return JsonResponse({ok: False, error: 消息不能为空}, status400) conversation Conversation.objects.get(idconversation_id, participantsrequest.user) msg Message.objects.create(conversationconversation, senderrequest.user, contentcontent) return JsonResponse({ok: True, id: msg.id, created_at: msg.created_at.isoformat()}) login_required require_GET def fetch_messages(request, conversation_id): # after 是客户端传来的最后一条消息 ID第一次进入页面时传 0 after int(request.GET.get(after, 0)) conversation Conversation.objects.get(idconversation_id, participantsrequest.user) messages ( conversation.messages .filter(id__gtafter) .select_related(sender) .order_by(id)[:50] ) 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})增量拉取用id__gt而不是按时间比较这是个容易忽略的细节。数据库的时间字段存在精度问题同一秒内多条消息时按时间过滤容易漏消息或重复拉取而消息 ID 是自增主键严格递增after游标天然不会漏。[:50]是单次拉取的上限防止某次请求把上万条历史全拉出去把浏览器卡死。select_related(sender)解决的是 N1 查询问题——不加它的话返回 50 条消息会额外执行 50 次用户表查询加了之后一次 JOIN 全部带出来。require_POST和require_GET不只是语义化装饰器它们同时承担了方法校验。require_POST还会让 Django 的 CSRF 中间件对请求做校验这也是为什么很多新手在 postman 里测试发消息接口时报 403——CSRF 校验默认只针对 POST 请求你需要在请求头里带上 CSRF token或者测试阶段在 settings 里临时注释掉CsrfViewMiddleware但上线之前一定记得加回来。登录保护用login_required就行它会把未登录请求重定向到登录页聊天系统的会话列表、消息记录都不该暴露给未登录用户。4. 数据模型与接口的扩展设计从双人单聊到班级群聊一个只能双人私聊的校园聊天系统功能上是残缺的。班级群、课程群、社团群才是校园场景的高频需求。把单聊模型扩展成群聊并不需要推翻重来而是在会话表上加类型字段、把参与关系改成带附加信息的成员表。这一章的扩展点也是这类项目从“能交差”到“能真正用起来”的分水岭。4.1 用户体系直接用 Django User 还是自建学生模型校园系统的用户天然带学号、院系、班级这些属性而 Django 内置的User表只有用户名、密码、邮箱、姓名这些通用字段。选型上有一个基本判断原则项目启动前如果还没做过任何一次migrate自定义用户模型是首选如果数据库已经建好、线上已有数据追加Profile扩展表是风险更低的方案。方案优点缺点适用阶段内置 User Profile 扩展表不动原表兼容所有现成代码获取学生信息要多查一次表项目已上线数据已存在继承 AbstractUser 自定义 User学号、院系直接挂在用户表上必须在首次 migrate 前设置 AUTH_USER_MODEL全新项目还没建过库重写 AbstractBaseUser完全掌控认证逻辑登录、权限都要自己实现极少需要不推荐大多数校园聊天源码包用的是第一种方案因为项目的起点往往是半成品作者不想动原有用户表。继承AbstractUser的方式则是在settings.py里写一行AUTH_USER_MODEL accounts.User然后自定义模型里加上student_id、department字段。这个配置必须在第一次migrate之前完成否则 Django 会提示AUTH_USER_MODEL不能后期更改。我自己带的项目里统一用第二种因为学号查出来就在用户表上写消息接口时少一层关联查询代码更直白。4.2 群聊怎么改会话加类型成员表带 last_read_id单聊的Conversation用ManyToManyField直接关联用户就够但群聊不行每个成员在群里的“已读位置”是独立的张三读到第 50 条李四可能只读到第 20 条。如果把这个状态放在消息表上每条消息要为每个成员维护一个is_read群里有 50 人就产生 50 行状态查询和更新都会爆炸。正确的做法是引入中间成员表把last_read_id挂在成员上# chat/models.py class Conversation(models.Model): TYPE_CHOICES [(direct, 单聊), (group, 群聊)] type models.CharField(max_length10, choicesTYPE_CHOICES, defaultdirect) name models.CharField(max_length100, blankTrue) # 群聊名称单聊时留空 created_at models.DateTimeField(auto_now_addTrue) participants models.ManyToManyField(User, throughConversationMember) class ConversationMember(models.Model): conversation models.ForeignKey(Conversation, on_deletemodels.CASCADE, related_namemembers) user models.ForeignKey(User, on_deletemodels.CASCADE, related_namechat_memberships) joined_at models.DateTimeField(auto_now_addTrue) last_read_id models.IntegerField(default0) # 该用户在这个会话里读到的最大消息 ID注意ManyToManyField加了throughConversationMember之后conversation.participants.all()这种查询语法仍然有效Django 会通过中间表自动关联。但此时你不能直接conversation.participants.add(user)了——所有改动都要通过ConversationMember.objects.create(conversation..., user..., last_read_id当前最大消息ID)。这个设计把“哪条消息算已读”的判定从消息侧移到了成员侧群聊场景下才不卡顿。last_read_id用 IntegerField 而不是外键到 Message是为了查询时少做一次关联。判断未读数量只需要一条 SQLMessage.objects.filter(conversationconv, id__gtmember.last_read_id, sender__in对话内除自己之外的用户).count()。这个字段在用户点开聊天窗口时更新一次不需要每读一条消息都写库写库频率低性能压力小。4.3 历史消息与未读计数两个高频查询怎么写群聊模型的查询复杂度和单聊不在一个量级但有两个接口是任何聊天系统都逃不掉的未读计数和历史消息分页。写不好这两个查询前端在页面右上角那个红点就是转不动的# chat/services.py from django.db.models import Count, Q from .models import Message, ConversationMember def unread_count_for_user(user): 统计用户在所有会话中的未读消息总数用于导航栏红点 # 先找出该用户加入的所有会话及其 last_read_id memberships ConversationMember.objects.filter(useruser) total 0 for m in memberships: total Message.objects.filter( conversationm.conversation, id__gtm.last_read_id, ).exclude(senderuser).count() return total def history_messages(conversation, user, page1, size20): 按 id 倒序翻页返回一页消息列表 # 校验用户是这个会话的成员 ConversationMember.objects.get(conversationconversation, useruser) start (page - 1) * size return list( conversation.messages .select_related(sender) .order_by(-id)[start:start size] )未读计数的性能瓶颈在于每个会话各执行一次count()。如果用户加入了 20 个群登录时就要执行 20 条 count 语句。数据量不大时能忍几千条消息量级完全没问题真到了需要优化的时候可以把last_read_id和消息最大 ID 的对比改到数据库层用 JOIN 一次算完但对校园项目来说先把功能做正确比做极致更重要。历史分页我刻意用了order_by(-id)而不是order_by(-created_at)原因和增量游标一样——ID 严格递增id排序结果等价于时间排序而且id是主键索引排序成本比普通字段低一个数量级。这里要特别留意切片分页的一个坑用[start:start size]做分页翻到很深的页码时比如第 100 页数据库会先扫过前 2000 条再丢弃这是查询慢的直接原因。聊天系统里用户很少会翻到那么深够用但如果你预测某个群的消息量会超过几万条就改用“上一页最后一条消息的 ID”做游标分页性能稳定得多。校园场景里群聊消息数量级通常可控先别过度设计。5. 校园chat部署运行避坑让我翻过车的五个必查点源码包在本机能跑不代表换个环境也能跑。部署阶段暴露的问题和本地开发完全两回事静态文件、时区、CSRF、在线状态、消息安全每个都能让你白屏半天或者被同事追着骂。这里整理的是我在部署这类 Django 聊天项目时真正翻过车的五个位置每一条都是现象、原因、解决三步讲完照着查能省下大量排查时间。5.1 静态文件全挂DEBUGFalse 之后白屏现象本地runserver一切正常一关掉DEBUG部署到服务器或局域网页面只剩下光秃秃的 HTMLCSS 和 JS 全部 404。原因Django 开发服务器自带静态文件托管这只活在 DEBUG 模式里。关掉 DEBUG 后Django 不再处理静态文件请求全部交给 STATIC_ROOT 收集后的目录而这个目录默认不存在也没人去收集。解决在settings.py里配置STATIC_ROOT os.path.join(BASE_DIR, staticfiles)然后执行收集命令# 把各 app 和项目里的静态文件统一收集到 STATIC_ROOT 指向的目录 python manage.py collectstatic --noinput收集完成后确认STATIC_ROOT目录下出现了 css、js、images 等文件夹。在局域网环境里最简单的做法是让 Django 暂时继续托管静态文件——但这等于把 DEBUG 打开不要在生产环境这么干。正经做法是用 Nginx 指一个/static/路由到staticfiles目录location /static/ { alias /path/to/your/staticfiles/; }我踩过的版本坑是项目本身的static/目录和STATICFILES_DIRS没配置对collectstatic收集出来的文件不全页面样式半残。检查时用python manage.py findstatic css/base.css这种命令逐文件定位比肉眼猜快得多。5.2 消息时间“穿越”TIME_ZONE 没设对现象用户发消息前端显示的时间和系统本地时间差了 8 个小时隔了半天才出现。原因Django 默认TIME_ZONE UTCUSE_TZ True模型里的DateTimeField以 UTC 存储。前端如果不做时区转换直接拿后端返回的字符串渲染就会把 UTC 时间当成本地时间展示。解决把TIME_ZONE改成Asia/ShanghaiUSE_TZ保持True。这里很多人有个误解以为设了TIME_ZONE之后存库时间也变了——不会数据库里仍然是 UTCTIME_ZONE影响的是模板和表单渲染时的自动转换。API 返回的时间建议统一用 ISO 格式带时区偏移由前端 JS 的new Date()做本地化展示。我遇到的实际坑是改了TIME_ZONE之后历史消息全部“变了时间”。这是因为存量数据本身是按旧时区显示逻辑写入的改了配置后模板渲染拿同一批 UTC 时间转成了新时区观感上像数据错乱实际数据没坏。解决方式是前端对时间字段统一处理不要一部分用模板渲染、一部分用接口返回的字符串两套逻辑早晚会打架。5.3 局域网访问ALLOWED_HOSTS 和 CSRF 的双重拦截现象手机在浏览器输入http://192.168.1.5:8000页面能打开登录按钮一点就报 403 Forbidden。原因ALLOWED_HOSTS默认只允许本机 host访问方的 IP 不在名单里Django 直接拒绝请求登录是 POST 请求CSRF 校验会比对请求的 Origin 和当前 host两者不一致同样抛 403。解决本地局域网测试阶段在settings.py里做两处修改# 内网测试可用通配符生产环境必须换成具体域名/IP不要照抄 ALLOWED_HOSTS [*] # 新版 Django 会校验 Origin 头把局域网 IP 和端口加进信任名单 CSRF_TRUSTED_ORIGINS [http://192.168.1.5:8000, http://localhost:8000]ALLOWED_HOSTS [*]只应该出现在内网和开发环境。如果服务器有公网 IP用通配符意味着任何域名都能指向你的服务会引来扫描和注入尝试。CSRF_TRUSTED_ORIGINS 是 Django 4.0 之后才有的配置老项目里如果找不到这个配置说明 Django 版本较老或 CSRF 校验逻辑不同需要优先确认版本而不是硬套新写法。这个配置漏掉的现象很迷惑页面加载、GET 拉消息都正常唯独登录和发消息报 403因为它们是 POST。5.4 在线状态不可信last_login 不是“当前在线”现象聊天页面上的头像绿点一直亮着但那人明明已经两个小时没动静了。原因很多聊天项目用User.last_login判断在线状态这个字段只在用户登录时更新一次跟“现在是否在线”没半毛钱关系。用户登录后一直挂着不关浏览器last_login 是几小时前的值但页面把他显示成在线。解决引入心跳机制。前端定时上报“我还在”后端在内存或数据库里记录最后心跳时间超过阈值就算离线。最简单的实现是在 Redis 里存user:{id}:last_seen没有 Redis 就在数据库里建一张心跳表查询在线列表时扫一遍最近 60 秒内有心跳的用户。这个方案的核心逻辑是“没有心跳就是离线”而不是“有没有登录”语义完全不同。校场景里可以在在线状态旁标注“最后活跃时间”这比一个不准确的绿点更有价值也避免用户因为状态不准产生误解。5.5 聊天内容 XSS消息里的脚本不能当 HTML 渲染现象一条消息内容写成img srcx onerroralert(1)对方点开聊天窗口立刻弹窗页面被注入。原因这是聊天系统最典型的安全漏洞。后端把用户输入原样存库前端用innerHTML把它渲染到页面上浏览器当成 HTML 解析执行。消息内容本质是文本任何把它当 HTML 处理的地方都是注入点。解决两层防线。第一层在入库或输出时清洗内容第二层前端渲染一律用textContent而不是innerHTML。后端做一个清洗函数import html def clean_message_content(raw): # 转义 HTML 特殊字符把 script 变成纯文本浏览器不会执行 return html.escape(raw, quoteTrue)html.escape会把、、、引号全部转义成实体。前端获取消息后用document.createTextNode(m.content)创建文本节点或者给innerHTML赋值前手动转义——最简单可靠的做法是 Vue/React 这类框架默认的插值语法它天然转义 XSS。真正危险的是用v-html或innerHTML手动渲染富文本。校园聊天系统不需要富文本一律纯文本渲染安全性和实现成本都能兼顾。我之前接过一个历史项目后端清洗了但前端还是innerHTML等于白洗两条腿必须同时改。6. 让它更像“能上线”的聊天系统Gunicorn、心跳与消息撤回把 runserver 换成真正的 WSGI 服务器是这套系统从“开发模式”走向“可用状态”的第一步也是最后一步里最不该跳过的环节。runserver 自带自动重载和静态托管但它是单进程开发服务器扛不住真实请求。生产部署我一般用 Gunicorn 加 Nginx命令很简短# 4 核机器的常见配置worker 数取 CPU 核数 × 2 1 gunicorn config.wsgi:application -b 0.0.0.0:8000 -w 9 --timeout 60注意 Gunicorn 只能在 Linux 和 macOS 上跑Windows 部署要换 waitress。worker 数不是越大越好开太多反而会因为频繁切换上下文拖慢请求实测 CPU 核数×21 是经验值。在线状态我习惯在项目里建一张UserOnline心跳表字段就三个用户、最后心跳时间、IP。前端每 30 秒向后端发一次心跳请求后端执行一次 upsert 更新last_seen查询在线列表时过滤last_seen now - 60s的用户这就是一个准实时在线状态。这个方案比 any 中间件都简单而且完全没有跨进程通信问题。消息撤回则是另一个校园聊天的高频需求我的做法是不真正删记录给 Message 表加一个is_deleted布尔字段撤回时把 content 替换成占位符再置位标记——这样已读位置、分页游标、历史消息数量都不受影响只是前端渲染时判断到标记就显示“消息已撤回”。我拿到任何聊天项目的源码第一件事永远是做越权测试登录 A 账号尝试读取 B 账号的会话列表和消息内容。这个习惯帮我拦下过好几次线上事故——很多校园聊天系统的接口只做了登录校验没做数据归属校验改一下 URL 里的 ID 就能看别人的聊天记录。在自己动手改架构之前先把权限边界钉死。希望帮到你。本文还有配套的精品资源点击获取