ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

三端婚恋相亲系统源码实战:统一用户身份与消息通道设计

三端婚恋相亲系统源码实战:统一用户身份与消息通道设计

简介:这是一套面向婚恋交友行业创业者、PHP开发者及红娘运营团队的2024年最新相亲系统源码,基于红娘金媒10.3版本,支持PC网站、微信小程序与公众号三端接入,可快速搭建覆盖多终端的婚恋平台。系统核心模块包括红娘服务、相亲活动、交友匹配与付费获取联系方式:红娘可依据用户资料与偏好提供精准配对建议,活动模块支持报名审核与通知推送,匹配模块借助大数据与智能算法推荐潜在对象,付费解锁联系方式兼顾隐私保护与平台收益。压缩包共2008个文件,以895个PHP后端文件、321个JavaScript脚本、152个CSS样式及78个WXSS、77个WXML小程序页面文件为主,另含GIF、PNG等图片素材与JSON配置,整体约32.03MB,目录结构完整。目前已有464人学习下载,适合需要三端一体化婚恋解决方案的开发者参考与二次开发。

1. 三端婚恋相亲系统源码到底在解决什么问题

去年帮一个做本地相亲社群的朋友看系统,他手里那套东西是典型的「网页能跑、小程序半残、公众号丢消息」——用户在小程序里划到心动对象,点「打招呼」没反应;切到公众号菜单进去,消息列表是空的;只有网页端勉强能用,但网页端又没法做微信支付。这不是个例,市面上大量所谓「三端婚恋相亲系统源码」都是这个状态:一套后端硬撑三个前端,接口没做端侧适配,消息通道各走各的,最后运营方只能让用户「去网页端操作」,转化率直接腰斩。

2024 年做婚恋相亲系统,三端(网页、小程序、公众号)已经不是「要不要做」的问题,而是「怎么让三端共用一套用户体系和消息通道」的问题。用户不会管你后端怎么设计,他今天在公众号看到推送点进来,明天在小程序里刷人,后天用电脑网页填资料,任何一端体验断裂,他就流失。这套源码要解决的核心就三件事:统一用户身份(一个 uid 打通三端)、统一消息通道(站内信、订阅消息、公众号模板消息按端分发)、统一资料与匹配逻辑(三端读同一份画像数据)。适合谁用?适合有运营能力但缺技术团队的婚恋工作室、本地相亲社群、垂直行业交友平台,不适合想直接上线就日活过万的幻想型选手。

2. 三端身份打通:从微信 unionid 到本地 uid 的映射设计

2.1 为什么不能只用 openid 做用户主键

新手最容易翻车的地方:小程序拿 openid,公众号拿 openid,网页端用手机号注册,三套用户表各存各的。结果同一个用户在三个端有三个账号,匹配池被稀释,运营发消息要发三遍。正确做法是:微信生态内用 unionid 做跨应用唯一标识,网页端用手机号 + 验证码,最终在业务层统一映射到一个本地 uid。

前提是你的小程序和公众号绑定在同一个微信开放平台账号下,否则拿不到 unionid。很多源码包不提醒这一点,买回来发现 unionid 全是空,就是因为没绑开放平台。绑定之后,小程序端wx.login换 code,后端用 code + appid + secret 调jscode2session,返回里就有 unionid;公众号网页授权走snsapi_userinfo,拿到的用户信息里也有 unionid。

2.2 后端统一登录接口的实现

下面是一个典型的统一登录接口,用 Python FastAPI 写,三端都调这一个入口,靠platform字段区分来源:

from fastapi import FastAPI, HTTPException from pydantic import BaseModel import httpx app = FastAPI() class LoginReq(BaseModel): platform: str # web / miniapp / official_account code: str = None # 小程序或公众号授权 code phone: str = None # 网页端手机号 sms_code: str = None # 网页端短信验证码 @app.post("/api/auth/unified_login") async def unified_login(req: LoginReq): if req.platform in ("miniapp", "official_account"): # 微信侧:用 code 换 unionid if not req.code: raise HTTPException(400, "missing code") appid = MINI_APPID if req.platform == "miniapp" else OA_APPID secret = MINI_SECRET if req.platform == "miniapp" else OA_SECRET async with httpx.AsyncClient() as client: resp = await client.get( "https://api.weixin.qq.com/sns/jscode2session", params={"appid": appid, "secret": secret, "js_code": req.code, "grant_type": "authorization_code"} ) data = resp.json() unionid = data.get("unionid") if not unionid: raise HTTPException(401, "unionid missing, check open platform binding") uid = get_or_create_uid_by_unionid(unionid) else: # 网页端:手机号 + 短信验证码 if not verify_sms(req.phone, req.sms_code): raise HTTPException(401, "sms code invalid") uid = get_or_create_uid_by_phone(req.phone) token = issue_jwt(uid) return {"uid": uid, "token": token}

逻辑说明:platform决定走哪条认证链路,微信侧只认 unionid,网页侧只认手机号。get_or_create_uid_by_unionid内部先查映射表,没有就插入新用户并返回自增 uid。参数上,MINI_APPID、OA_APPID这些从环境变量读,不要硬编码在代码里。issue_jwt签发的 token 里带 uid 和过期时间,三端后续请求都带这个 token,后端不再关心请求来自哪个端。

2.3 数据库映射表的最小字段设计

不要一上来就设计几十个字段的用户表,先把映射关系跑通。核心就一张user_identity表:

字段类型说明
uidbigint 自增业务主键,三端共用
unionidvarchar(64)微信跨应用标识,可空
phonevarchar(20)网页端手机号,可空
nicknamevarchar(64)昵称,三端可各自覆盖
avatarvarchar(255)头像 URL
created_atdatetime注册时间

unionid 和 phone 都建唯一索引,但允许 null。这样微信用户先注册、后绑手机,或者手机用户先注册、后绑微信,都能合并到同一个 uid。合并逻辑写在get_or_create_uid_by_unionid里:如果 unionid 查不到但 phone 能匹配上已有用户,就把 unionid 补写到那条记录,而不是新建。

3. 消息通道分发:站内信、订阅消息、模板消息怎么各走各的

3.1 三端消息能力对比与选型

这是最容易做错的部分。很多人以为「发消息」就是一个接口的事,结果小程序端用户收不到,公众号端用户被骚扰。先把三端的能力边界列清楚:

通道触发条件到达率限制
站内信用户主动打开端100%(打开才看到)无
小程序订阅消息用户授权一次中一次授权只能发一条,需反复引导
公众号模板消息用户关注公众号高需用户与公众号有交互,2024 年规则收紧
短信有手机号高成本高,仅关键节点用

常见做法是:打招呼、新消息提醒走站内信 + 小程序订阅消息;匹配成功、资料被查看走公众号模板消息;验证码、付费成功走短信。不要所有消息都往公众号推,用户会取关。

3.2 消息分发服务的代码骨架

下面是一个消息分发器,根据用户当前活跃端和消息类型决定走哪些通道:

async def dispatch_message(uid: int, msg_type: str, payload: dict): # 1. 站内信永远写一份,保证用户打开任意端都能看到 await save_inbox(uid, msg_type, payload) # 2. 查用户绑定的端 binds = await get_user_binds(uid) # {"miniapp": True, "oa": True, "phone": "138..."} # 3. 按消息类型分发 if msg_type in ("greeting", "new_message"): if binds.get("miniapp"): await send_subscribe_msg(uid, payload) # 订阅消息 elif msg_type in ("match_success", "profile_viewed"): if binds.get("oa"): await send_template_msg(uid, payload) # 公众号模板消息 elif msg_type in ("verify_code", "pay_success"): if binds.get("phone"): await send_sms(binds["phone"], payload)

逻辑说明:站内信是兜底,先写库再分发。get_user_binds从user_identity表读该用户绑定了哪些端。send_subscribe_msg需要用户之前授权过对应模板 ID,没授权就跳过,不要报错中断。参数上,msg_type用枚举字符串,不要用数字,方便排查日志。每个发送函数内部都要 try/except,单个通道失败不影响其他通道。

3.3 订阅消息的授权次数管理

小程序订阅消息是「一次授权一条」,用户点一次「允许」你只能发一条。很多源码包不处理这个,发完一条后续全失败。正确做法是在user_identity表加一个subscribe_quota字段,用户每次授权 +1,发送成功 -1,发之前检查 quota > 0。引导授权的时机也有讲究:不要在用户刚注册就弹,要在用户点击「打招呼」按钮时弹,这时候他有明确预期,通过率高。

4. 资料与匹配逻辑:三端读同一份画像数据

4.1 用户画像字段的取舍

婚恋系统的资料字段不是越多越好。字段太多,用户填到一半就跑了;字段太少,匹配没依据。我一般会分三层:必填层(性别、年龄、所在城市、婚姻状况、学历)、选填层(身高、收入区间、房车情况、兴趣爱好)、扩展层(自我介绍、择偶要求、生活照)。必填层决定能不能进匹配池,选填层影响匹配权重,扩展层影响用户点进主页后的转化。

数据库设计上,必填和选填放user_profile主表,扩展层放user_profile_ext附表,避免主表过宽。三端提交资料都调同一个POST /api/profile/update,后端按字段白名单过滤,防止小程序端传了网页端不该传的字段。

4.2 匹配打分的可解释实现

匹配算法不要一上来就上协同过滤,冷启动阶段没数据,跑出来全是噪声。先用规则打分,可解释、可调参、运营能看懂:

def match_score(me: dict, other: dict) -> float: score = 0.0 # 硬性条件:性别互补、年龄差在范围内 if me["gender"] == other["gender"]: return 0.0 age_diff = abs(me["age"] - other["age"]) if age_diff > 10: return 0.0 score += (10 - age_diff) * 2 # 年龄越近分越高,最高 20 # 同城加分 if me["city"] == other["city"]: score += 15 # 学历匹配 if me["education"] == other["education"]: score += 10 # 择偶要求双向匹配 if meets_requirement(me, other) and meets_requirement(other, me): score += 25 # 活跃度加权 score += min(other["active_score"], 10) return score

逻辑说明:先做硬过滤(同性、年龄差过大直接 0 分),再累加软性权重。meets_requirement检查对方的年龄、身高、收入是否落在我的择偶要求区间内,双向都满足才加分,避免单向匹配。active_score是用户近 7 天登录、发消息、完善资料的加权分,防止推荐一堆僵尸号。参数上,各项权重写在配置表里,运营可以调,不要硬编码。

4.3 三端资料展示的差异处理

同一份资料,三端展示要适配。网页端可以展示完整信息,小程序端受限于屏幕,只展示核心字段 + 「展开更多」,公众号端基本只做消息触达,点进去跳小程序或网页。后端返回资料时带一个platform参数,按端裁剪字段:

def get_profile(uid: int, viewer_platform: str): profile = query_profile(uid) if viewer_platform == "miniapp": return {k: profile[k] for k in ("nickname", "age", "city", "avatar", "tags")} elif viewer_platform == "web": return profile # 全量 else: return {"nickname": profile["nickname"], "avatar": profile["avatar"]}

这样三端读的是同一份数据,只是展示层做裁剪,不会出现「小程序改了资料网页端没变」的问题。

5. 避坑与排查:三端联调最容易翻车的 5 个点

5.1 坑一:unionid 拿不到,用户三端分裂

现象:小程序和公众号各自注册了账号,同一个用户在两个端看到的是两套匹配池。原因:小程序和公众号没有绑定在同一个微信开放平台账号下,或者绑定了但jscode2session返回里没有 unionid 字段。解决:先确认开放平台绑定关系,再检查后端是否把 unionid 存进了user_identity表。如果历史数据已经分裂,写一个合并脚本,按手机号把两条记录合并,unionid 补写到保留的那条上。

5.2 坑二:订阅消息发不出去,日志显示 43101

现象:调用订阅消息接口返回errcode: 43101,提示用户拒绝接收。原因:用户没有授权对应模板,或者授权次数已经用完。解决:发送前查subscribe_quota,为 0 就跳过并记录,不要反复重试。同时在用户点击关键按钮时重新引导授权,补充 quota。注意模板 ID 要和小程序后台配置的一致,测试环境和正式环境容易搞混。

5.3 坑三:网页端登录态和小程序登录态互相覆盖

现象:用户在网页端登录后,小程序端 token 失效,被踢下线。原因:后端用同一个 token 存储 key,后登录的覆盖了先登录的。解决:token 存储按uid + platform做 key,各端独立。JWT 里带 platform 字段,校验时也按 platform 校验。如果业务要求单点登录,那要明确告知用户,不要默默踢人。

5.4 坑四:资料图片三端存储路径不一致

现象:小程序上传的头像,网页端显示裂图。原因:小程序上传到本地磁盘,网页端上传到对象存储,返回的 URL 前缀不同,前端拼接逻辑各写各的。解决:统一走一个上传接口,后端根据配置决定存本地还是对象存储,返回完整可访问 URL。前端不要自己拼路径,直接用后端返回的 URL。

5.5 坑五:匹配结果分页在三端表现不一致

现象:网页端第一页和第二页有重复用户,小程序端正常。原因:网页端分页参数从 0 开始,小程序端从 1 开始,后端没统一。解决:后端统一用offset + limit,前端传之前做转换,或者在接口文档里写死从 0 开始。这种问题排查起来很费时间,建议在接口层加参数校验,offset 小于 0 直接报错。

6. 上线前必做的三端一致性验证与灰度技巧

系统跑通不等于能上线。上线前我会做一轮三端一致性验证,核心是确认「同一个用户在三端看到的数据和能做的操作完全一致」。具体做法:准备一个测试账号,在三端分别登录,依次执行注册、完善资料、浏览匹配、打招呼、发消息、查看站内信六个动作,每步截图对比。任何一端出现数据不一致,先查接口返回,再查数据库,最后查前端渲染。

验证通过后不要全量放开,先做灰度。灰度策略按 uid 取模,比如uid % 100 < 10的用户走新系统,其余走旧系统。这样出问题影响面可控。灰度期间重点看三个指标:登录成功率(三端分别统计)、消息到达率(站内信写入量 vs 订阅消息发送成功量)、匹配点击率(推荐列表的点击/曝光)。任何一个指标比旧系统低 20% 以上,先回滚再排查。

还有一个血泪经验:上线前一定要用真实微信账号测试,不要只用开发者工具。开发者工具里订阅消息、模板消息、支付都能模拟成功,真机上各种权限和网络问题才会暴露。我一般会找三台不同型号的手机,分别测小程序和公众号,网页端用 Chrome 和 Safari 各测一遍。测试用例不用多,把核心链路走通就行,但每端都要走。

最后说一个我自己的习惯:每次改完消息分发逻辑,先在一个测试 uid 上把三种消息类型各触发一遍,看日志里站内信、订阅消息、模板消息、短信四条记录是否按预期出现。这个习惯帮我省了很多次上线后才发现「某端收不到消息」的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进