1. 这不是一个普通的交友网站,而是一次Django全栈练手
先说说为什么做这个项目。我是做后端开发的,平时工作主要围绕业务系统、接口设计这类偏“务实”的东西,说实话有点腻了。一直想找个能覆盖全栈、又有真实业务复杂度、做完还能拿去演示的项目练手。看了一圈,电商系统太俗,博客系统太简单,最后选了“相亲系统”——你别说,这东西看起来只是“注册、筛选、聊天”,实际上涉及的模块比想象中多得多:会员体系、个人资料、照片管理、匹配规则、即时聊天、支付(会员服务)、后台审核、行为风控……几乎把一个互联网产品的常见模块都包圆了。
至于“帅小伙网络相亲系统”这个名字,纯属朋友起哄。当时项目做完要起个项目名,大家说既然是演示项目,不如起得生动点,“帅小伙”三个字反而成了记忆点,每次给别人演示都被调侃一嘴“这系统是不是只允许帅小伙注册”。后来我也懒得改了,反正Gitee上的项目名挺重要,一个好记的名字确实更容易被点开。实际做的时候我加了性别字段,男女都能用,核心逻辑是一样的。
这个系统适合谁?如果你是刚学完Django基础、想找个有深度的实战项目的学生,或者工作中只用Django做各种内部管理系统、想试试面向公开用户的产品型项目怎么做,这个项目的技术选型和实现思路都很有参考价值。文章里我会把所有踩过的坑、取舍逻辑都讲一遍,不是那种照着教程敲一遍就完事的水文。
技术栈先摆这里:Python 3.10 + Django 4.1 + MySQL 5.7 + Redis(缓存和在线状态)+ Vue 3(前端,但核心逻辑放在后端,前端只是展示层)。开发环境是Win11,生产环境用CentOS 7 + Nginx + Gunicorn。整套东西从零到能对外访问,我一个业余时间断断续续做了大概三周。
2. 为什么选了Django而不是Spring Boot或Flask,以及整体架构怎么搭
2.1 技术选型:三个框架里Django最合适
这个项目立项之前,我认真比较过Python系的三条路:Flask、FastAPI、Django,顺带也看了下Java那边用Spring Boot做同类系统的路子。
先排除Flask。相亲系统的核心痛点是数据模型之间关系复杂:用户有Profile、有照片、有身高体重学历收入这些标签、有关注列表、有匹配记录、有聊天记录,还要有浏览日志。Flask的ORM虽然可以用SQLAlchemy凑合,但双方都得自己拼,折腾下来一半时间都在写基础设施。FastAPI适合纯API服务,异步性能是亮点,但相亲系统的后台管理、模板渲染、用户认证这些它都不直接管,得自己搭,等于从零造轮子。
Django最舒服的地方是“全家桶”自带的这些能力恰好都是这个项目需要的:自带Admin后台(给运营和审核用,不用自己写管理端)、自带User模型和认证体系(扩展一下就能当会员系统用)、自带ORM(迁移管理、外键关系直接省掉一大半SQL)、自带的CSRF防护和XSS过滤(对面向公众的产品太重要了)。有个老哥跟我说Spring Boot做这种系统也很成熟,Java生态确实强,但用Python做原型验证、改业务逻辑的速度快太多,而且我这个项目本身也有个人学习价值,没必要为了所谓的“企业级”去上Java那套重装备。至于SQLite,开发时用没问题,上线前我换成了MySQL,原因后面部署章节会说。
2.2 项目结构:Django的应用拆分思路
很多Django新手喜欢把啥都塞进一个app里,models、views、forms全堆一起,刚开始很爽,等到要加功能时就是一场灾难。我这次按业务域拆成了四个应用:
sjmatch/ # 项目配置目录(settings/urls/wsgi) apps/ ├── accounts/ # 会员注册、登录、资料管理、相册 ├── matching/ # 匹配推荐、搜索、关注、喜欢 ├── chat/ # 私信聊天、在线状态、消息通知 └── admin_ops/ # 后台运营辅助、审核任务、数据报表accounts这个app是整个系统的地基,因为它管的是User模型和Profile模型。Django自带的User模型字段太少,只有用户名、邮箱、密码这些,相亲系统需要的生日、性别、城市、职业这些都没有。我的做法是扩展一个UserProfile模型,跟User做OneToOne关联,然后在User的post_save信号里自动创建对应Profile。
matching管匹配推荐逻辑,是整个系统的核心卖点。chat管私信和在线状态。admin_ops管审核,因为实名制相关的项目必须有人工审核环节,不能全靠机器。
settings.py里我把AUTH_USER_MODEL配置成了自定义的User(继承AbstractUser),这个时机很重要——如果你已经执行过第一次migrate再改,会比较麻烦。所以建议做新项目时第一时间配这个,后面所有多对多、外键关系都以自定义用户模型为准。
3. 核心数据模型设计:相亲系统的“骨架”怎么搭
3.1 用户资料模型:每个字段都带着业务逻辑
相亲系统和普通社交网站最大的不同是:用户资料的信息密度高,而且这些字段直接决定匹配和搜索能不能做。我设计UserProfile时是这么想的——既要够用,又不能堆一堆用不上的字段。
class UserProfile(models.Model): GENDER_CHOICES = (('M', 'Male'), ('F', 'Female')) MARITAL_CHOICES = (('S', '未婚'), ('D', '离异'), ('W', '丧偶'), ('O', '其他')) user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile' ) gender = models.CharField(max_length=2, choices=GENDER_CHOICES) birth_date = models.DateField(null=True, blank=True) city = models.CharField(max_length=64, db_index=True) height = models.PositiveSmallIntegerField(null=True, blank=True) weight = models.PositiveSmallIntegerField(null=True, blank=True) education = models.CharField(max_length=32, blank=True) # 学历 occupation = models.CharField(max_length=64, blank=True) # 职业 income_level = models.CharField(max_length=16, blank=True) # 收入档位 marital_status = models.CharField(max_length=8, choices=MARITAL_CHOICES, default='S') self_intro = models.TextField(max_length=500, blank=True) # 自我介绍 ...身高体重为什么用PositiveSmallIntegerField而不是CharField?因为后面要支持筛选,比如“身高175到185之间”,用整数才能走范围查询。存成字符串“175cm”这种,筛选时就得写一堆乱七八糟的字符串解析逻辑,纯粹给自己找事。
还有两个重要字段:is_verified和profile_score。前者是实名认证标记(后台人工审核通过后置True,只有认证用户在匹配列表里才靠前),后者是资料完整度评分。这两个字段在匹配推荐、排序时都会用到。
3.2 匹配与互动模型:多对多关系别偷懒
“喜欢”这个动作我用一个专门的LikeRecord模型来实现,而不是在用户表里搞个ManyToManyField完事。为什么?因为业务上我需要记录“谁在什么时候喜欢了谁”,还要防止重复喜欢。ManyToManyField虽然方便,但中间表是自动生成的,没法挂额外的业务字段。
class LikeRecord(models.Model): from_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='likes_sent') to_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='likes_received') is_mutual = models.BooleanField(default=False) # 是否互相喜欢 created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('from_user', 'to_user') # 防止重复喜欢 indexes = [models.Index(fields=['to_user', 'is_mutual'])] # 查询“谁喜欢了我”时提速is_mutual这个字段是整张表的核心,当两个人的喜欢记录互为反向时,它会被置成True,同时系统自动通知双方“你们互相喜欢了,可以开始聊天”,这就是相亲场景里的“配对成功”。这个操作在写LikeRecord的时候要做一次事务内检查,保证一致性。
聊天模型我偷了个懒,没有做成复杂的会话列表,而是直接用“私信+会话Room”的方式。Room表记录对话双方,Message表记录每一条消息。为什么不用WebSocket做实时聊天?因为初期为了控制复杂度,我用了轮询(每10秒拉一次新消息),后面优化时再换Django Channels,这个取舍后面单说。
3.3 为什么说信号(Signal)是Django的隐形功臣
这个项目里信号用得比预期多,但都控制得很克制。最典型的例子就是“创建用户就自动创建UserProfile”:
@receiver(post_save, sender=User) def create_profile(sender, instance, created, **kwargs): if created: UserProfile.objects.create(user=instance)很多人说信号会让代码“隐式执行”,不好调试。但用在这个场景里是合理的——你不能保证每个注册入口(前台注册、后台新增运营账号)都记得去手动创建Profile,信号就是一个兜底机制。生产环境踩过坑后我才发现,真正的炸点不在信号里面,而在信号里面写了耗时操作。有一次我在post_save信号里顺手发了邮件验证,结果注册接口慢了两秒多。后来把所有邮件、短信这类外部IO改成走Celery异步任务队列,接口响应时间才回到正常水平。所以信号可以用,但千万不要在信号里做耗时的网络请求。
4. 核心功能模块的实现:注册登录到匹配推荐,每个环节的取舍
4.1 注册登录:session还是JWT?
这是个绕不开的问题。Django默认用session做登录状态,模板渲染配起来确实简单。但这个项目的前后端是分离的(前端Vue调RESTful API),所以认证机制选哪个直接决定前端怎么存登录态。
我最终选的是DRF(Django REST Framework)+ JWT认证。理由有两个:一是移动端和Web端共用一套API,JWT无状态,扩展起来方便;二是JWT的payload里可以塞用户ID和昵称,前端不用每次请求都去查一遍用户资料。
实现上用djangorestframework-simplejwt这个库,配置也不复杂:
# settings.py from datetime import timedelta SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, 'UPDATE_LAST_LOGIN': True, }前端登录后拿到access token存localStorage,每次请求带在Authorization头里。这里有个安全细节是我后来才想明白的:access token生命周期必须短(30分钟够了),refresh token才能给7天,不然token泄露等于账号被盗。
密码加密这块不用操心,Django自带PBKDF2-HMAC-SHA256。注册时还要接一个验证码逻辑,我用的腾讯云短信,开发阶段用django-debug-toolbar直接看邮件内容(配置EMAIL_BACKEND为console backend),连短信都不用真发,调试效率高很多。
4.2 匹配推荐:从最简单的到真正能用的
匹配算法是我在这个项目里花时间最多的部分。“帅小伙网络相亲系统”听起来是个玩票项目,但匹配推荐的核心体验直接决定用户留不留得住。第一版我做得极其简单:按城市+年龄+性别筛选,按最近登录时间排序,SQL里一条order_by就搞定,上线一礼拜用户反馈“推荐的人不精准”、“翻几页就没新人了”。
第二版我重新设计了分数评分体系。每个候选用户计算一个“匹配分”,按分数降序排列:
def calculate_match_score(user_a, user_b): score = 0 # 基础条件分:目标城市 + 年龄区间 if user_a.profile.city == user_b.profile.city: score += 30 age_gap = abs(user_a.profile.age() - user_b.profile.age()) if age_gap <= 3: score += 20 elif age_gap <= 6: score += 10 # 互补条件:身高差(女矮男高组合加分),学历匹配度 if user_a.profile.gender == 'M' and user_a.profile.height > user_b.profile.height: score += 15 if user_a.profile.education == user_b.profile.education: score += 10 # 资料完整度加成 score += min(user_a.profile.profile_score, 100) // 10 score += min(user_b.profile.profile_score, 100) // 10 # 热度微调(最近活跃加分) if user_b.is_recently_active(): score += 5 return score这个打分逻辑明显不是最优的,但它有个好处:每一分都能解释清楚“为什么给你推荐这个人”,用户端的推荐理由就写“你们同城,年龄差2岁,学历相当”。后来我研究了一下真实相亲App的推荐机制,主流做法是协同过滤或者向量召回,但那种方案需要大量行为数据和离线计算。我这个项目数据量撑不起协同过滤,规则打分反而是最可靠的。
实现上有个细节:如果对每个候选人实时计算分数,100个候选人就要算100次,再叠加数据库查询,页面会很慢。我的优化思路是预计算——每天凌晨用Celery定时任务把所有用户的两两匹配分算好存进一张MatchScore表,推荐时直接查表。当天注册的新用户算不出来就先按基础排序,第二次刷新再走预计算表。这个方案在几万用户量级下完全够用,真正的大厂方案是向量相似度召回+粗排+精排,系统复杂度完全不是一个量级。
4.3 搜索筛选:直接上Q对象组合查询
搜索页的条件有:城市、性别、年龄范围、身高范围、学历、收入、最近活跃时间。这些条件都是可选的,我用Django ORM的Q对象动态拼接:
def search_users(request): params = [] if request.query_params.get('city'): params.append(Q(profile__city=request.query_params['city'])) if request.query_params.get('gender'): params.append(Q(profile__gender=request.query_params['gender'])) age_min = request.query_params.get('age_min') age_max = request.query_params.get('age_max') if age_min: params.append(Q(profile__birth_date__lte=calculate_birthday(int(age_min)))) if age_max: params.append(Q(profile__birth_date__gte=calculate_birthday(int(age_max)))) ... combined_q = params.pop(0) if params else Q() for p in params: combined_q &= p results = get_user_model().objects.filter( combined_q, is_active=True, profile__is_verified=True ).select_related('profile').order_by('-last_login')[:100]search_users这个函数唯一容易出错的地方是年龄转生日的逻辑。用户筛“25到30岁”,你不能直接拿出生年份去比,因为边界年份还涉及月份,最稳妥的做法是以当前年份减目标年龄得到出生年份区间,再用__lte和__gte两个方向做范围过滤——注意这里方向容易搞反,我踩过一次:小于最小年龄的人出生日期更晚,所以birth_date__lte对应的是年龄下限。
搜索性能方面,profile表的city和birth_date都建了索引,这个量级下走索引很快。如果城市数据量再大,可以换Elasticsearch,但那是后话。
4.4 聊天:短期轮询的长远代价
聊天的第一版我用了“短轮询”——前端每10秒请求一次新消息接口。优点是Django原生的视图就能搞定,不用引入WebSocket协议栈;缺点是延迟10秒,体验一般,而且大量轮询对服务器压力不小。
我估了一下:按1000个在线用户、每人10秒拉一次来算,平均每秒100个请求。每个请求查一次数据库,MySQL扛得住,但通信量和服务器负载都会指数增长。如果用户量到1万,这个方案肯定先崩。
这就是为什么我先用轮询再做Channels的原因——把功能跑通的关键不是技术选型多炫,而是控制每一步的风险。轮询版本上线后实际上也发现了一个体验问题:当用户A给用户B发消息,B必须等下一轮轮询到来才能知道有新消息,碰上轮询正好在请求间隙,延迟就是10秒。后来我升级了WS协议,用Django Channels的WebSocket做实时推送,体验一下到了毫秒级。但Channels的部署比普通Django复杂——需要ASGI服务器(Daphne或Uvicorn)、Redis做channel layer,和Nginx的WebSocket代理配置也有讲究。如果只想跑通流程,轮询完全可以;想拿去演示,还是建议上Channels。
5. 安全与审核机制:相亲系统不能踩的雷区
5.1 实名与审核:机器审核为主,人工兜底
相亲系统的特殊性在于,一旦出现虚假信息或者诈骗贴子,用户的信任感会瞬间崩塌。我做了一个简单的实名认证流程:用户上传身份证正面照+手持照,后台审核列表里可以看到待审图片,管理员人工判断后在admin_ops里做“通过/驳回”操作。这个流程没法靠纯自动判断,只能机器辅助+人工兜底。
图片上传这块我用了Django自带的FileField加自定义上传路径:
def upload_id_photo(instance, filename): ext = filename.split('.')[-1] return f'id_photos/{instance.user.id}/id_{time.time()}.{ext}'注意一个安全问题:上传的证件照属于敏感隐私,不能放到静态资源目录直接通过URL访问。我的做法是把media根目录配置成不参与Nginx静态服务,而是通过一个带身份校验的视图来读取照片,只有本人和管理员才能访问。
5.2 敏感操作防滥用:限流与验证
注册接口、短信验证码接口、搜索接口这些都是容易被薅的攻击面。我用django-ratelimit做了接口级限流:
@ratelimit(key='ip', rate='5/m', method='POST') def send_sms_code(request): ...这里的key是按IP限,注册接口限制5次/分钟。如果同一台设备有多个账号注册(羊毛党操作),光肉眼看request body里的手机号容易漏。所以我还加了“同IP注册量检测”:连续30分钟内同一IP注册超过3个账号,自动进到审核冻结名单,需要人工解封。这个功能是我上线后被人薅了一波之后才加的。
CSRF防护这块,DRF默认在SessionAuthentication下有CSRF强制,但我用的是JWT认证,JWT不会自动带CSRF Token——准确说,CSRF的问题在JWT下主要是防“自动携带Cookie”的攻击方式,JWT放Header里不自动带,CSRF风险天然就低。实际做的时候完全没处理也还行,但为了稳妥,还是保留每次写操作校验Content-Type和Origin头的习惯。
5.3 Photo类的版权与隐私问题
相册模块本来是让用户传生活照的,结果上线第一天就有用户传了别人(可能是明星?)的照片。我直接加了一条机器审核逻辑:图片先存OSS,然后提交到腾讯云的“内容安全接口”做检测,检测不通过的直接不发到公开相册,审核结果标记在记录里。个人开发者的项目没有条件上大厂的审核系统的话,至少也要做“上传后先不公开、管理员在后台审核后才展示”的流程,否则光靠举报,迟早出问题。这条是给所有做UGC(用户生成内容)产品的人提个醒。
6. 部署与性能优化:从Dev环境到对外可访问,完整复盘
6.1 本地开发环境与生产环境分离
开发阶段我用的SQLite,生产换成MySQL,这中间其实有一个坑:Django的ORM会在Python层面处理很多数据库差异,但有些地方不会——比如MySQL的JSONField用法和SQLite不一样,日期范围查询的边界条件也不同。所以我建议如果你是用Django做新品,开发机就装个MySQL,用Docker起一个MySQL 8容器,数据库统一,绝大多数跨库的“怪问题”可以提前规避。
配置分离我用的是几套settings模块:
settings/ ├── base.py # 公共配置:日志、缓存、中间件 ├── dev.py # 开发:DEBUG=True,本地DB,邮件后端打印 └── prod.py # 生产:DEBUG=False,MySQL,Redis,邮件真实发送环境变量管理是Python + Django项目的死角。很多初学者把数据库密码直接写settings.py里提交到GitHub,属实名泄露现场。我用的是django-environ这个库,生产环境变量放在服务器.env文件里,settings.py读取环境变量。这样代码库里只有模板,没有实际密码。
6.2 Gunicorn + Nginx部署经典三件套
生产部署组合是Nginx反向代理 + Gunicorn + Redis(缓存和Channel Layer)。核心配置:
# gunicorn.conf.py bind = "127.0.0.1:8000" workers = 4 # 不是越多越好,一般 = CPU核数×2+1 worker_class = "gthread" # 同步worker并发量太低,用线程模式 threads = 8 timeout = 120worker数量我之前犯过错,拿到一台4核机器直接写16个worker,结果内存直接拉满。Gunicorn每个worker会加载完整的Django应用,一个worker大约占100MB内存,4核8G机器开4个worker + 每个worker开8个线程就够了。
Nginx需要关注两个端口的转发:80端口转发到Django API,880端口转发到前端Vue项目的静态文件。但更关键的是静态文件和media文件不能走Django——Nginx直接alias指向磁盘目录,否则每个图片请求都要走一遍Python进程,性能差得离谱。还有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_read_timeout 3600s; }这个proxy_read_timeout 3600s是Channels部署最常见的坑:不配这个,WebSocket连接每60秒被Nginx掐断,表现就是聊天一会儿断线一会儿重连,状态极其诡异。
6.3 数据库瓶颈与缓存策略
搜索接口在数据量过1万条以后,还是会出现偶尔的慢查询。我给这几个高频接口加了Redis缓存,缓存策略很简单:key为“用户ID+筛选条件”,value为搜索结果的用户ID列表,过期时间5分钟。这个策略解决了80%的重复查询压力。
MySQL本身的优化就没那么麻烦了,在my.cnf里调整innodb_buffer_pool_size到机器物理内存的一半左右,重启后综合查询性能明显变化。还有一个被我忽略很久的坑是Django ORM的懒加载——查出来的UserProfile如果没有用select_related('user'),循环打印每个人时会触发N+1次查询,日志里一堆重复SQL。我统一在列表接口加了select_related和prefetch_related,这个是Django性能优化最简单有效的一步,很多人挂在N+1上还以为是网络问题。
7. 踩坑实录:排查过程中我学到的那些教训
7.1 “删不掉的外键”:约束冲突与数据一致性
注册时我已经做了UniqueConstraint防止重复Like,但后来出现了让人头疼的情况:运营后台想删掉某个违规用户,结果外键关联了太多表(LikeRecord、ChatRoom、Photo都是外键指向User),删除时直接报IntegrityError。第一反应是给外键加on_delete=CASCADE,这样删用户时所有关联数据自动清理。
但CASCADE也有反噬:有一次手滑删了一个运营测试账号,结果这个账号名下上百条聊天记录一瞬间全没了,用户投诉说消息消失了。后来痛定思痛,把聊天记录这类“可能需要审计找回”的数据改成了“软删除”。
实现软删除最简单的方式是给User模型加一个is_deleted字段,默认False。删除时只是置True,不清数据;同时登录逻辑里过滤掉is_deleted=True的用户。真正的物理清理放到一个月一次的定时任务里做。这套方案既满足了合规删除的要求,又保留了数据可追溯性。
7.2 Django版本和第三方库的兼容性地狱
2023年做项目时用的是Django 4.1,但Channels的最新版本已经更新到4.x,而Channels 4要求Django 4.2以上,于是pip install时自动把Django升级到了4.2,结果数据库迁移文件全乱套。具体表现是:makemigrations检测到一堆模型变更,一migrate就报字段不存在或者表已存在。
排查链路是这样的——先看django版本:
pip show Django发现版本变了。再检查迁移文件有没有被自动rebase。解决方案不复杂但也踩了半天:把全局Python环境清理掉,改用virtualenv或者Poetry做项目隔离,然后在requirements.txt里锁死版本:
Django==4.1.7 djangorestframework==3.14.0 channels==4.0.0 django-channels-redis==4.2.0很多人对requirements.txt只有“记录用什么包”这个认识,实际上它最大的价值是“锁定版本”。一旦有人跑pip install -U django把版本升上去,项目可能直接崩掉。从那次以后,我所有项目的依赖都用pip freeze > requirements.txt固定,并且用一个独立的Python 3.10虚拟环境管理,不跟系统的Py环境混用。
另一个类似的坑是MySQLdb驱动。Django连MySQL需要mysqlclient或者PyMySQL,Windows上mysqlclient编译经常出问题,很多人转投PyMySQL。但PyMySQL在Django 4.2以后兼容性开始出岔子,我最后选择了在服务器上用mysqlclient(CentOS上编译顺利),开发环境Windows上干脆用Docker里的MySQL加本机mysqlclient,省去动态库编译的麻烦。这个选择背后没有高深的技术,只有一句经验:能用编译好的包就别自己编译,尤其是在Windows上。
8. 产品化思考:从“能跑”到“像那么回事”
8.1 为什么需要会员等级和积分体系
一个真正能叫“系统”的产品,除了功能,还得有运营抓手。我给这个相亲系统做了两级会员:普通会员和VIP会员。VIP可以看到谁喜欢了我、浏览记录、无限次解锁照片。这些都是很标准的“增值服务”逻辑,但重点在于Django里怎么设计这个权益模型。
我把VIP状态定义为UserProfile的一个字段:vip_expire_date。当这个日期大于当前时间时,接口会放行VIP权限;过期以后走回基础功能。这里有个业务边界要注意:充值记录和到期时间不能靠定时任务去更新,而是在每次请求时动态判断,这样即使定时任务挂了,权限也不会出问题。支付对接我用的是微信H5支付,回调接口是Django视图,收到回调后校验签名、更新订单状态、给用户加VIP天数。
8.2 运营后台:Django Admin的二次开发
自带的Admin确实是最快的管理界面,但直接套用到“会员列表”这种场景会把敏感字段(密码哈希)暴露出来。我的做法是自定义了一个Admin类,重写了list_display、list_filter、search_fields,并关掉了密码修改入口:
@admin.register(User) class CustomUserAdmin(UserAdmin): list_display = ('username', 'email', 'profile_gender', 'profile_city', 'is_verified', 'is_active') list_filter = ('is_active', 'is_staff', 'profile__is_verified') search_fields = ('username', 'email', 'profile__city') def profile_gender(self, obj): return obj.profile.gender if hasattr(obj, 'profile') else '-' profile_gender.short_description = '性别'用Profile的关联字段做list_display,需要额外查询,但因为Admin页面是内部使用、访问量低,问题不大。Admin里还加了一个“发送系统通知”的按钮,这个是为了给用户推“有人喜欢你了”这种消息通知用的,直接在Admin里配置了模板消息,运营人员操作起来零门槛。
8.3 前端展示层的血泪经验
前端我用的Vue 3 + Element Plus,没有用任何复杂的工程化框架。前端的核心页面就是注册、登录、用户列表、用户详情、聊天窗口、个人中心、管理后台。但开发到一半时我发现,即使前端只是个“展示层”,很多错误仍然出在这层。
比如跨域问题。开发时Vue跑在localhost:5173,Django跑在localhost:8000,请求必跨域,我加django-cors-headers解决:
CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "https://yourdomain.com", ]这个库第一次配置容易漏掉CORS_ALLOW_CREDENTIALS = True,虽然JWT方案不需要带Cookie,但为了兼容后续可能要带Cookie的场景,还是配置上。为了查跨域报错耽误了整整半天,最后发现不是Django响应头问题,而是前端Vite代理没配好——这是前后端分离开发最折磨人的地方。
给新手的建议是:开发阶段用Vite的proxy机制,让dev server把/api/请求代理到后端8000端口,这样浏览器里看到的请求是同源的,既省了跨域配置,也更好调试。
9. 如果重来一次:想做这个系统,先避免这五件事
踩了这么多坑,最后整理一份“如果让我重新做”会改变的五件事,对新人尤其有用。
第一件事:先画清楚数据模型图再写代码。我第一版表结构是在开发过程中改出来的,结果用户表改了三次、聊天表重建了两次。建议动手前用draw.io或者纸笔把User、Profile、Photo、Like、ChatRoom、Message之间的关系理清楚,把OneToOne、ForeignKey、ManyToMany标明白,后面切表结构的成本会小很多。
第二件事:任何上传文件必须限大小和类型。第一版我根本没限,发布后有人在个人资料里传了个1.2GB的视频(虽然前端限制了图片格式,但移动端直接跨UI上传被绕过了),服务器磁盘直接爆掉。现在我在Nginx层就拦:client_max_body_size 10m,同时在Django验证文件扩展名和MIME类型。
第三件事:起步就该用Docker Compose。数据库、Redis、后端服务、前端,全部用容器化部署。我开发时Windows本地装MySQL和Redis折腾了整整一个晚上,是当前项目里最没技术含量又最影响心情的部分。Docker Compose写好以后换机器一键起全套环境,效率完全不一样。
第四件事:前端接口文档用drf-spectacular自动生成。我第一次给前端同事写API文档是手写Markdown,字段变化后文档就过期了。DRF本身就有OpenAPI支持,配好spectacular库以后自动生成Swagger文档,每次代码更新文档也同步更新,省掉无数「为什么这里字段没有了」的沟通成本。
第五件事:日志和错误监控从第一天就接好。我上线第一周根本没配日志,有一次线上用户反馈“收不到验证码”,我查了半天才发现是短信服务商的单日限额用完了,而应用日志里什么都没有。后来接了sentry-sdk,错误自动上报,线上问题能第一时间定位到具体代码行。这个习惯如果一开始就有,估计能省掉三分之一排查时间。
10. 最后补充:一些关于项目的后续设想
前面聊的都是已经实现的内容。其实在相亲系统这个方向上,还有很多值得玩味的扩展点没做,写出来供有志者参考。
第一个是“匿名片”功能——用户可以在不知道对方真实身份的情况下先交换片面的兴趣爱好标签,双方都点亮了某个共同标签后,系统才解锁真实头像和资料。这个功能对保护隐私和提升互动率很有帮助,表现在匹配逻辑上就是多了一个“中间状态”:从普通陌生人到互相关注,再到显真实身份。
第二个是把匹配算法升级到“向量化”。当用户量超过5万以后,规则打分顶多算“能看”,真正的个性化需要把用户标签(身高、收入、爱好、星座、学历、城市等)做embedding,用内积相似度或余弦相似度做推荐。Django生态里没有现成轮子,要接Faiss或者Milvus,这是一个很有意思的优化方向,但数据没上规模时别搞这套,纯属浪费服务器。
第三个是客服系统和举报闭环。相亲产品用户之间矛盾频发,没有客服介入机制,用户投诉会直接变差评。我做了最简单的站内举报按钮,举报记录进后台,运营人员处理完后回传处理结果——流程闭环了,但由于是个人项目,没有给运营留太多时间维护。
我在整个过程中最大的体会是:项目不怕名字土,就怕逻辑不清。你给系统起名叫“帅小伙网络相亲系统”,听着随意,但内部的模块划分、数据关系、权限边界都得严肃对待。从账号体系到匹配算法到聊天通信到安全风控,这个项目几乎把Web开发该有的骨架全走了一遍。如果手头有这份文章,再结合Django官方文档,从头复刻一个类似的系统,快则两周、慢则一个月,你就能把一个“能跑的项目”做成“能讲的项目”。别人的介绍都是静态的Demo,我的建议是做完以后把这个项目部署到公网,哪怕只开放测试账号,面试或者交流时直接演示给对方看,点击、登录、配对、聊天,全程跑通,比你说一百句“我会Django”都管用。