看到这个标题的时候,我脑子里第一个想法是:这不就是一套完整的商业化交友小程序吗?上一轮交付的是2048小程序的源码工程,那个项目核心是前端交互,后端基本是固定关卡数据,做起来相对轻量。这次从Python后端到uniapp前端、再到微信小程序打包,整条链路的复杂度完全不是同一个量级。我按自己实际做过的项目经验,把婚恋交友系统从选型、建模、匹配接口、小程序端开发到最后打包联调的完整过程写下来,希望能帮到正在做类似项目的朋友。适合什么人看?懂一点Python基础、接触过Vue语法、正准备做第一个前后端分离小程序的开发者。
1. 整体方案选型:为什么是 Python + uniapp + 微信小程序
1.1 项目定位与技术栈拆解
婚恋交友系统说白了就是一个“陌生人社交 + 兴趣匹配 + 私信互动”的业务闭环。和普通电商小程序不同,它的核心不是商品,而是人。因此业务上最吃功夫的是两件事:用户画像的丰富程度、匹配推荐的准确性。我最终选择的技术栈是:
- 后端:Python 3.10 + Flask + SQLAlchemy + MySQL + Redis
- 前端:uniapp(Vue 3 语法 + TypeScript 模板),编译为微信小程序
- 中间件:Redis 做在线状态与匹配队列,MySQL 存用户和关系数据
为什么不用 Django?Flask 更轻,适合一个人维护的中小型项目,路由和模型划分自由度高;Django 自带 Admin 后台成熟,但那套 model 迁移和中间件体系对新手来说反而容易绕晕。我用 Flask 的原因很简单:项目初期只有两三个模块,Flask 的路由组织方式足够清晰,写起来也没有额外心智负担。
uniapp 的选择更直接。婚恋交友这种产品,如果只做微信小程序,原生一套也没问题,但后续大概率要扩展到 H5 或者抖音小程序。uniapp 的底层是 Vue,写一遍业务代码,微信端、支付宝端、H5 端都能编译出来。尤其是做私信这类高频页面,uniapp 的组件化写法可以让一套逻辑复用到多个平台。
1.2 婚恋交友的核心业务模块梳理
在动手写代码前,我先把业务拆成下面几个模块,每个模块对应一组页面和一组后端接口:
| 模块 | 包含功能 | 涉及页面 |
|---|---|---|
| 用户体系 | 微信授权登录、个人资料填写、兴趣标签、城市/职业设置 | 登录页、资料编辑页 |
| 匹配推荐 | 基于标签、城市、活跃度的打分推荐 | 首页推荐卡片 |
| 互动 | 喜欢/不喜欢、互相喜欢后解锁私信 | 卡片操作、匹配成功弹窗 |
| 消息 | 私信列表、聊天窗口、已读状态 | 消息页、聊天页 |
| 会员 | 查看谁喜欢我、每天更多推荐次数 | 会员页、订单页 |
这个表看起来简单,但实际开发时容易踩坑的地方不在业务逻辑本身,而在数据交互。比如“互相喜欢后解锁私信”这个动作,前后端至少涉及三个接口:用户A点击喜欢、用户B点击喜欢、触发匹配回调生成会话。如果接口顺序没设计好,很容易出现A喜欢B之后B还在推荐流里看到A,造成体验割裂。我的做法是在后端统一维护一个like_relation表,每次点击喜欢都先查反向记录,存在就直接生成会话,不存在则只插入一条待匹配记录,前端再通过轮询或消息推送通知结果。
1.3 为什么先定接口协议再写页面
这里我想多说一句选型之外的经验:定好接口返回结构是这类项目最值得提前做的事。我在项目初期把后端所有接口都统一成{code: 0, data: {...}, msg: "success"}的格式,前端封装一个通用的 request 方法,后续所有页面都走同一个入口。这样做的直接好处是,小程序端做 token 过期处理、错误提示、加载状态只需要在一个文件里改,不用每个页面重复写。
2. 后端设计:用 Python 搭出一个能跑的服务
2.1 环境准备:Python 安装、虚拟环境与依赖管理
这一步看起来基础,但实际项目里很多人栽在第一句命令上。先说 Python 的安装。我建议直接到官网下载 3.10 以上的安装包,安装时勾选“Add Python to PATH”,不然到封装依赖那一步会出现python命令找不到的情况。Linux 服务器上更简单,直接apt install python3就行,不过系统自带的版本可能偏低,我建议还是用pyenv或者conda管理版本,避免污染系统环境。
装好之后,第一步永远是创建虚拟环境:
python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate虚拟环境这个动作容易被新手忽略,觉得“反正我就一个项目,装全局不行吗”。实际做婚恋交友这种小项目的时候,你还得同时维护老项目的代码,Python 依赖版本冲突是家常便饭。比如 Flask 2.x 和 Flask 1.x 的路由规则就有差异,全装在一个环境里迟早出问题。
依赖文件我习惯手动维护,不用pip freeze直接导出,因为 freeze 会把一些无关依赖也带进来。我自己用的是手写 requirements.txt:
flask==3.0.0 flask-cors==4.0.0 flask-sqlalchemy==3.1.1 PyMySQL==1.1.0 redis==5.0.0 pyjwt==2.8.02.2 数据库表结构设计:用户、标签与匹配关系
婚恋交友系统的表结构核心就是围绕“用户”和“两个用户之间的关系”来设计。我建表时重点考虑了查询效率,因为推荐流的接口会被高频调用,不可能每次都去算全表匹配。
用户表主要字段:
class User(db.Model): id = db.Column(db.Integer, primary_key=True) openid = db.Column(db.String(64), unique=True, nullable=False) nickname = db.Column(db.String(32)) avatar = db.Column(db.String(255)) gender = db.Column(db.Integer) # 1男 2女 city = db.Column(db.String(32)) birthday = db.Column(db.Date) intro = db.Column(db.String(255)) activity_score = db.Column(db.Integer, default=0)兴趣标签我单独拆了一张表,然后通过中间表做多对多关系。为什么不直接用一个逗号分隔的字段?因为匹配算法要按标签取交集,如果存成字符串,每次匹配都要做拆分和重组,索引也用不上。老老实实用中间表:
class UserTag(db.Model): user_id = db.Column(db.Integer, primary_key=True) tag_id = db.Column(db.Integer, primary_key=True)关系表的设计就要稍微多想一层。喜欢和不喜欢本质上是同一种操作,只是方向不同,所以我只建了一张user_relation表:
class UserRelation(db.Model): id = db.Column(db.Integer, primary_key=True) from_user = db.Column(db.Integer, index=True) to_user = db.Column(db.Integer, index=True) action = db.Column(db.Integer) # 1喜欢 2不喜欢 3互相喜欢 created_at = db.Column(db.DateTime)action这个字段看起来有点冗余,其实很方便:当 A 喜欢 B、B 也喜欢 A 时,我直接把两条记录的 action 改成 3,匹配会话生成后,推荐流里这两个人就互不出现了。用联合查询from_user和to_user双向过滤,写起来清晰,跑起来也有索引支撑。
2.3 匹配评分接口:一段可以直接用的 Python 代码
匹配算法不用一开始就上机器学习,先用规则打分就能支撑几千人的日活。我的规则比较简单:标签重合度占大头,同城加分,活跃度影响权重。核心函数长这样:
def match_score(user_a, user_b): score = 0 tags_a = get_user_tags(user_a.id) tags_b = get_user_tags(user_b.id) overlap = set(tags_a) & set(tags_b) score += len(overlap) * 10 if user_a.city and user_a.city == user_b.city: score += 20 # 活跃度梯度:最近登录时间越近分越高 score += user_b.activity_score # 性别互斥:默认男看女、女看男 if user_a.gender == user_b.gender: score = -1 return score这里要注意,用户自己不喜欢的对象需要提前过滤,不能等打分出来再筛。我是在 SQL 层先排除掉所有user_relation里存在的反向记录,只把候选集缩小到 100 条以内,再用 Python 循环打分。道理很简单:全表算分的数据量越来越大,与其优化评分函数,不如先把候选集缩得足够小。
推荐接口的响应我刻意做了分页,一次只给 20 个用户,前端滑到底再加载下一批。这里有个很多人踩过的坑:分页的游标不能简单用页码,因为在看推荐的过程中,其他用户的 relation 状态可能已经变了,下一批的 offset 会出现重复或遗漏。我直接用 id 作为游标,返回时带上last_id,前端下次请求把它带回来。
3. 小程序端实现:uniapp 从创建到微信打包
3.1 从 HBuilderX 创建 TypeScript 项目开始
uniapp 项目的创建有两条路:Vue CLI 和 HBuilderX。我个人推荐 HBuilderX,原因很简单——微信小程序打包、真机调试、App 云打包这些操作都有图形界面,对新手友好太多了。用 Vue CLI 创建的项目在配置上更灵活,但搞不好光环境配置就得折腾半天。
创建项目的时候选择“Vue 3 + TypeScript”模板。我见过很多人后来想给项目加 TS,结果改造时要给每个组件补类型声明,工作量非常大,不如一开始就选 TS。
创建完成后,需要动两个关键文件:
manifest.json:填微信小程序的 AppID,配定位权限、分享参数pages.json:配置页面路由、导航栏标题和底部 tabBar
这两个文件是小程序的“身份证”和“地图”。比如你要开定位权限,必须在manifest.json的 mp-weixin 节点下声明permission,只在前端代码里调用uni.getLocation是不够的,不然真机一跑就报错。
3.2 核心页面:推荐卡片、个人资料与消息列表
推荐页是整个产品的门面。我用的是 swiper 组件做上下滑卡片,每张卡片展示头像、昵称、年龄、城市、兴趣标签,底部放两个按钮:不喜欢和喜欢。UI 组件我没有全部手写,直接引入了 uview-plus 组件库,按按钮、标签、弹窗这些常用组件足够用了。
uview-plus 的导入有一个细节:不要在插件市场下载 ZIP 之后直接扔进项目,正确的做法是在 HBuilderX 插件市场点“使用 HBuilderX 导入插件”,它会自动处理好依赖。手动拷贝往往会导致 sass 变量没编译,页面样式乱掉,这个问题我最初排查了很久。
个人资料页就是一堆表单,但要注意 uni 表单组件和普通 Vue 表单不同,v-model在部分组件上可能不生效。我用的方式是手动监听@input事件,把值同步到一个全局 store 里,提交时再统一从 store 取值。
3.3 请求封装:token 处理、登录态刷新
小程序没有浏览器的 Cookie 概念,登录态全靠后端返回的 token。我封装了一个全局的request.ts,所有接口都走它:
const request = <T>(url: string, method: 'GET' | 'POST' = 'GET', data?: any): Promise<T> => { return new Promise((resolve, reject) => { uni.request({ url: baseUrl + url, method, data, header: { Authorization: uni.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode === 401) { refreshToken().then(() => { request<T>(url, method, data).then(resolve).catch(reject) }) return } resolve(res.data as T) }, fail: reject }) }) }这段代码里我特意处理了 401:token 过期时先静默刷新,刷完把刚才失败的请求重新发一遍。用户是无感知的,不会突然被踢回登录页。
3.4 微信小程序打包:2MB 限制与分包方案
微信小程序最让人头疼的限制就是主包不超过 2MB,超过一点就报source size 2612kb exceed max limit 2mb。我第一次打包的时候正好踩到这个坑。
解决方案有两条路:
第一,压缩静态资源。图片和字体不要本地放,一律传到对象存储,代码里用 URL 引用。项目里那些背景图、默认头像,我全部换成了 CDN 地址,主包立刻小了几百 KB。
第二,分包加载。把聊天相关的页面单独放到一个 subpackage,用户的首次加载只下载主包,进入聊天功能时再加载分包。uniapp 里配置很简单,在pages.json里加subPackages字段:
{ "subPackages": [ { "root": "pages/chat", "pages": ["chat-list/chat-list", "chat-room/chat-room"] } ] }这两个方案组合起来,主包基本能压到 1.5MB 以内。实测下来很稳,后面再追加功能也有操作空间。
4. 联调排坑实录:日志、定位、标题与分享
4.1 uniapp 不打印日志信息?换一种调试思路
你在小程序控制台里console.log不输出,这是 uniapp 开发中最常见的问题之一。我试过在 HBuilderX 的控制台和微信开发者工具的控制台同时看,结果一个只显示编译日志,一个只显示用户代码日志,非常容易看错地方。
我的经验是:优先编译到 H5 端调试业务逻辑。H5 端直接用 Chrome DevTools 的 Network 面板看请求、用 Console 看日志,效率高得多。等 H5 端稳定了,再切到微信开发者工具做真机验证。如果必须在微信端看日志,可以引入 vconsole,在页面上悬浮一个小按钮点开就能看到 console 内容,比反复切工具窗口强很多。
4.2 页面列表加载更多:前后端配合的分页方案
消息列表和推荐列表都要做“上滑加载更多”。前端用onReachBottom触发分页请求,这个生命周期函数在小程序里对应滚动到底部的事件,在 H5 端则模拟为窗口滚动到底部。
这里有个经典的重复请求 bug:用户快速滑到底部,onReachBottom短时间内触发多次,后端接口被重复调用。我的处理方式是在方法入口加一个锁:
onReachBottom() { if (this.loading) return if (this.page * this.pageSize >= this.total) return this.loading = true this.page++ fetchList().finally(() => this.loading = false) }同时后端要保证分页参数校验好page和pageSize的上限,防止有人用page=99999把整个数据库捞走。这种接口安全问题,前端管不了,必须在后端兜底。
4.3 后台定位与隐私授权的正确姿势
婚恋交友需要基于距离筛选附近的人,所以定位功能是刚需。uniapp 里定位有两个 API 容易混淆:uni.getLocation获取一次坐标,uni.startLocation开启持续定位。如果要实现后台监测定位,需要同时使用plus.geolocation.watchPosition配合uni.startLocation一起作用。
但这里最大的坑不是接口调用,而是权限声明。微信小程序必须在manifest.json中声明:
"permission": { "scope.userLocation": { "desc": "用于推荐附近交友用户" } }另外,一旦要用后台定位,微信审核会要求你有明确的隐私协议弹窗,并且让用户手动开启位置授权。不要试图绕开这些限制,把“定位开关”做成设置页里的一个独立项,用户可以随时关闭,这样对审核和用户体验都更友好。
4.4 动态设置标题与自定义分享
每个聊天对象的昵称就是聊天页的标题,不能写死。uniapp 里动态修改导航栏标题用uni.setNavigationBarTitle({ title: user.nickname }),这个调用需要在页面onLoad或者收到对方消息时触发。
自定义分享是婚恋系统拉新的重要入口。我在onShareAppMessage里动态生成分享文案,把用户自己的头像拼进去:
onShareAppMessage() { return { title: '我在婚恋交友等你来匹配', path: `/pages/index/index?inviter=${this.openid}`, imageUrl: this.shareImage } }这个inviter参数就是一套简单的邀请返利机制,新用户注册后给邀请人加积分。后端解析到路径参数后存进用户表,做个邀请来源标记,数据统计时就能看出哪条渠道带来的用户多。
5. 常见问题速查与个人心得
5.1 高频问题速查表
我在整个开发过程中遇到的问题不少,整理成一张速查表,方便你直接对照解决:
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 打包报 source size 2612kb exceed max limit | 主包体积超过微信 2MB 限制 | 静态资源转 CDN,聊天页加重分包 |
| 真机 console.log 不打印 | uniapp 编译模式和调试工具不匹配 | 优先 H5 端调试,必要时引入 vconsole |
| 自定义分享后点开空白页 | 分享 path 没有携带必要参数,页面未正确处理 | 分享路径显式带上 inviteId,页面 onLoad 解析 |
| uni.getLocation 调用失败 | manifest.json 缺少 permission 声明 | 配置 scope.userLocation 并写好隐私描述 |
| 列表加载更多重复请求 | onReachBottom 高频触发未加锁 | 添加 loading 锁和分页终止条件 |
| 导入 uview-plus 后样式全乱 | 手动复制插件包导致 sass 依赖缺失 | 在 HBuilderX 插件市场一键导入 |
5.2 踩过几次坑之后总结的经验
第一,接口签名设计一定要早。我最初用 RESTful 风格给喜欢接口命名为POST /user/like,后来又要支持“B 已经喜欢 A,A 点击喜欢直接匹配”的场景,接口语义就变得混乱,不得不加一个source参数区分。如果重来一次,我会直接用 RPC 风格,比如POST /api/interaction/like,把动作放在 URL 里,而不是靠 HTTP 方法去猜意图。
第二,Redis 的用途不要只局限在缓存。我做运营后台时发现按标签算匹配度很容易,但线上用户多了之后,每次都要从 MySQL 取候选集再算分,延迟很高。后来我把活跃用户的 id 列表放在 Redis 的 ZSET 里,按最近活跃时间排序,推荐接口先从这里取一批在线用户再做详细打分,响应时间直接从 800ms 降到 200ms 以内。
第三,匿名聊天加“敏感词拦截”。婚恋产品的聊天内容不能裸奔,我加了一层关键词过滤,后端在存储前对文本做替换。uniapp 端也要在输入时做长度限制,防止用户一次发几千个字。这个功能不复杂,但能在审核和用户举报环节省掉很多麻烦。
5.3 一些后续可以扩展的方向
这套系统跑通之后,剩下的就是运营层面的优化。我觉得最值得做的是“互相喜欢后的推荐排序”。现在只用标签重合度和城市距离,用户量大的时候明显感觉推荐结果太同质化。后续想加入用户行为权重,比如被点击喜欢多的人权重提高、刚注册的新用户给一波短时流量扶持,甚至用简单的内容推荐思路做“新用户优先推荐池”。
这些东西听起来很玄,但落到代码上,其实就是在算分函数里多加几个加权项而已。我实际做下来最大的感受是:婚恋交友系统的技术难点从来不在单点功能,而在数据流转的通畅程度。只要后端接口语义清晰、前端页面组件的复用关系理清楚,这个项目一个人做完是完全可以做到的。