自己折腾相亲网站系统也有一阵子了,从最开始用 JSP+Servlet 手写页面,到现在前后端分离的 SpringBoot+Vue3+MyBatis+MySQL 整套,中间踩了不少坑,也沉淀了一些实战经验。这篇就围绕我实际开发的一套相亲网站系统源码,把架构设计、数据库模型、核心业务逻辑、接口实现到前端联调通通捋一遍,重点讲我在做会员匹配、实名认证、私信聊天这些模块时的细节取舍。如果你正准备用 Java 技术栈做社交类项目,或者想学前后端分离项目怎么写,这篇应该能省你不少时间。
1. 需求分析与功能模块梳理
1.1 相亲网站的核心需求定位
做相亲系统跟做普通电商、博客不一样,用户对隐私和匹配准确度的要求极高。我一开始就给自己定了几个硬性指标:信息展示必须区分“公开”和“脱敏”两套;匹配逻辑不能只靠简单 SQL 条件拼凑,要有一定权重;聊天模块必须能控制敏感词和图片安全。这套系统定位自用学习和二次开发,所以我在设计时没有堆砌复杂微服务,而是用一个单体 SpringBoot 项目加合理模块划分搞定,保证部署简单、理解成本低。
从功能范围看,我把它拆成了四条主线:
- 用户端:注册、登录、完善资料、上传照片、浏览推荐、条件搜索、查看意向列表。
- 匹配端:基于地域、年龄、学历、身高等多维度打分,输出匹配指数。
- 互动端:意向记录、私信聊天、查看访客、加入黑名单。
- 管理端:用户审核、照片审核、数据统计、会员管理、举报处理。
控制好这个范围,整个系统在代码层面能保持清爽,业务上也能覆盖相亲网站最核心的玩法。我见过不少项目一开始就加直播、送礼物,结果后端撑不住,前端也乱成一锅粥,这种教训不必重复。
1.2 角色权限与业务边界
这套系统一共有三种核心角色:普通用户、会员用户、管理员。我把角色语义直接压进 JWT Token 里,配合 Spring Security 做接口级校验。
- 普通用户:能看基础资料、搜索、发意向,但不能查看对方精确联系方式。
- 会员用户:在普通用户基础上,能看完整隐私信息、使用高级筛选、消息免打扰。
- 管理员:后台审核、禁用违规账号、查看统计报表。
角色之间的权限边界必须后置在服务端控制,不能只靠前端隐藏按钮。比如用户传入“查看手机号”的接口,后端要先判断当前用户是否为会员,且是否通过实名认证,两项都满足才返回明文号码,否则一律脱敏。
1.3 系统使用场景说明
这套系统适合三类场景:一是个人学习 SpringBoot+Vue3 整合最佳实践,二是毕业设计或课程项目需要一套完整前后端分离代码,三是小范围创业试点,比如本地相亲角、园区内部联谊的线上化。我的部署环境是一台 4核8G 的云服务器,跑 CentOS 7.9,MySQL 8.0 和 Redis 都放在同一台机器上,演示规模下压力不大,一万会员量级完全撑得住。
2. 技术选型与环境准备
2.1 前后端分离的技术选型逻辑
我为什么选 SpringBoot + Vue3 而不是用若依这类现成脚手架?因为相亲系统有大量个性化表单、图片上传、实时聊天,前端需要比较强的组件灵活性,Vue3 的组合式 API 更适合这种复杂状态管理。后端用 SpringBoot 2.7.x,理由很简单:生态成熟、资料多、出问题好排查,而且与 MyBatis 配合非常顺。
组件版本方面,我列一个实测可用的组合,避免新手踩版本不兼容的坑:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 稳定,兼容性最好 |
| SpringBoot | 2.7.14 | 基于 JDK8 的最终版本 |
| MyBatis | 2.2.2 | 与 SpringBoot2.7 对应 |
| MyBatis-Plus | 3.5.2 | 简化单表 CRUD |
| MySQL | 8.0.32 | 支持 JSON 字段 |
| Vue3 | 3.3.4 | 组合式API |
| Element Plus | 2.4.0 | 后台/用户端组件库 |
| JWT | 0.11.5 | 无状态登录认证 |
| Redis | 6.2.7 | 验证码、在线状态缓存 |
这里单独说下 MyBatis-Plus 的定位:它和原生 MyBatis 不冲突,我是在原生基础上引入了便捷能力,复杂查询仍然手写 XML。很多人担心 Plus 会把 MyBatis 的“手写 SQL 灵魂”丢掉,实际上只要你自己控制好滥用边界,项目既快又能满足复杂业务。
2.2 本地开发环境搭建
这一步看起来基础,但坑不少,我按自己的操作顺序整理一下。先装 JDK8,配置好JAVA_HOME,再装 IDEA,安装 Lombok 插件。数据库我用 Docker 起了个 MySQL:
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -e TZ=Asia/Shanghai \ mysql:8.0.32 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci \ --default-authentication-plugin=mysql_native_password注意mysql_native_password这个参数很重要,如果不指定,SpringBoot 连接早年版本的驱动时可能出现认证插件报错。
前端我用 Vite 创建 Vue3 工程:
npm create vite@latest match-frontend -- --template vue cd match-frontend npm install npm install element-plus axios pinia vue-router实际开发中,我习惯给 axios 封装一个统一的请求实例,设置baseURL指向后端网关地址,同时在请求拦截器里自动带上Authorization头,响应拦截器里统一处理登录过期状态码 401。这个封装是所有前后端分离项目的第一步,没做好后面联调能把你逼疯。
2.3 MySQL 库表初始化
我建库名matchmaking_db,统一使用utf8mb4字符集。以下是用户主表的 DDL,我把核心字段都贴出来:
CREATE TABLE `user_profile` ( `id` bigint NOT NULL AUTO_INCREMENT, `uuid` varchar(32) NOT NULL COMMENT '对外唯一ID', `phone` varchar(20) DEFAULT NULL COMMENT '手机号(注册凭证)', `password` varchar(100) DEFAULT NULL COMMENT '加密密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `gender` tinyint DEFAULT '0' COMMENT '0保密 1男 2女', `birth_date` date DEFAULT NULL COMMENT '出生日期', `province` varchar(30) DEFAULT NULL COMMENT '省份', `city` varchar(30) DEFAULT NULL COMMENT '城市', `height_cm` int DEFAULT NULL COMMENT '身高(厘米)', `education` varchar(20) DEFAULT NULL COMMENT '学历', `marital_status` varchar(10) DEFAULT NULL COMMENT '婚姻状况', `income_range` varchar(20) DEFAULT NULL COMMENT '收入范围', `avatar_url` varchar(200) DEFAULT NULL, `self_intro` text COMMENT '自我介绍', `require_text` text COMMENT '择偶要求', `real_name` varchar(30) DEFAULT NULL COMMENT '真实姓名(脱敏存储)', `id_card` varchar(18) DEFAULT NULL COMMENT '身份证号(加密存储)', `is_real_auth` tinyint DEFAULT '0' COMMENT '是否实名认证', `member_level` tinyint DEFAULT '0' COMMENT '0普通 1会员', `member_expire_time` datetime DEFAULT NULL, `status` tinyint DEFAULT '1' COMMENT '1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_phone` (`phone`), KEY `idx_city_gender` (`city`,`gender`) ) ENGINE=InnoDB COMMENT='用户资料表';这里有个设计细节值得说明:phone和uuid分开。手机号是敏感信息,内部定位用户用uuid,对外暴露的链接分享、聊天消息都是传的uuid,这样即使接口被扒也不至于泄露真实手机号。
3. 后端核心架构与接口实现
3.1 整体包结构与分层思路
后端工程我按“横向分层 + 纵向模块化”双维度组织:
com.matchmaking ├── common // 通用响应体、异常处理、工具类 ├── config // 安全配置、Redis配置、跨域配置、MyBatis配置 ├── controller // 接口层 ├── service // 业务层,接口与实现分离 ├── mapper // MyBatis Mapper接口 ├── model // 实体类、DTO、VO ├── security // JWT过滤链、认证逻辑 ├── interceptor // 自定义拦截器 └── job // 定时任务,如会员过期扫描分层思想不新鲜,但每层职责必须划清楚。我踩过的一个典型坑是:开发图快,把 SQL 拼接逻辑直接写在 Controller 里,导致后面改一个需求要同时改接口、业务和数据库三处。正确的做法是 Controller 只管参数校验和返回包装,所有业务判断进 Service,数据操作全部收敛到 Mapper。
Controller 层返回统一结构,我定义为这样:
{ "code": 200, "message": "success", "data": {} }所有异常由全局异常处理器兜底,业务异常自定义BizException,参数错误直接绑定MethodArgumentNotValidException。这使得前端联调相当省心,不用纠结各种奇奇怪怪的返回格式。
3.2 JWT 认证与权限控制
登录接口收到手机号密码后,我用BCrypt校验密码,用户名密码都通过后生成 JWT Token。Token 里放三个关键字段:userId、role、memberLevel。续期策略采用的是“Redis 存储 token,过期时间 2 小时”,每次请求由过滤器刷新剩余时间,用户连续操作不用反复登录。
核心过滤器逻辑简述:
OncePerRequestFilter 实现类的 doFilterInternal() 中: 1. 获取请求头 Authorization = "Bearer xxx" 2. 用 KeyResolver 从 Redis 取用户会话 3. 校验 token 签名和过期时间 4. 向 SecurityContextHolder 写入 Authentication 5. 放行后续责任链如果一个接口要求会员权限,我在方法上打@PreAuthorize("hasRole('VIP')"),配合 Spring Security 的@EnableGlobalMethodSecurity(prePostEnabled = true)开启注解驱动。这里提醒一点:如果你的项目里没有引入 Spring Security 的 starter,单纯依赖拦截器手写角色判断也可以,但要对每个接口做遗漏检查。我最终选择 Security 而不是自研,是因为它有完善的会话管理、CSRF 防护和权限表达式,虽然学习成本高一些,但后续扩展管理端、运营后台时省心太多。
3.3 用户注册与实名认证的实现细节
注册接口的流程是:前端传手机号 + 短信验证码 + 密码,后端先验验证码(Redis key 为sms:code:{phone}),再校验手机号未注册,然后 BCrypt 加密密码落库。短信服务我接的是阿里云验证码接口,生产环境建议做“每分钟同手机号最多发送 5 次”的限制,防止短信轰炸。
实名认证是相亲系统里一个绕不开的合规点。我的实现方式是:用户在个人中心提交真实姓名 + 身份证号 + 手持身份证照片,后端先 OCR 识别姓名和证件号,再比对用户填写的资料是否一致,最后走身份证二要素验证接口确认真伪。实名信息在库里用 AES 加密存储,密钥放在环境变量,不写死在配置文件里。加密后的身份证号即使数据库被拖走,也无法还原。
这里有个非常容易踩的坑:身份证号脱敏展示时,要保留前 3 后 4,而不是中间几位。比如110***********1234,不然用户无法核对自己填的是不是那张卡。很多新手在这里低级出错,后台审核员没法对用户反馈。
3.4 匹配推荐算法的思考
匹配度是相亲系统的核心体验。我一开始也想用复杂的协同过滤、Personality 性格测试来打分,但落地时发现问题不少:冷启动、特征稀疏、正反馈数据太少。后来我改用“规则权重 + 部分排序”的方式,配合定期离线算好的匹配指数来优化性能。
匹配指数计算主要考虑这些因子:
// 年龄差,越小分越高,最多20分 // 学历匹配,例如同档次+15分,跨一档+10分 // 城市同城+15分,同省+8分 // 身高差,男生比女生高5-15cm最佳,满分15分 // 收入稳定度,按收入区间差异化打分,满分10分 // 择偶要求关键词匹配,命中越多分越高,满分15分 // 头像审核通过且有高清照+10分算法虽然逻辑写起来简单,但我做了一层很关键的处理:在用户搜索“推荐列表”时,不是实时每次去全表扫描,而是每天凌晨跑定时任务把每个用户的匹配对象存到 Redis ZSet 中,key 是 userId,value 是候选用户ID,score 是匹配分。用户刷新推荐时,直接从 Redis 按分数倒序取 50 个 id,再查数据库秒回。
为了冷启动,系统对刚注册尚未填完整资料的用户,前端强制引导完善资料,同时后端提供“资料完整度得分”,低于 60 分不参与匹配排序,避免一堆空壳用户污染推荐池。
3.5 私信系统的设计与实现
私信聊天最早我用的是简单的“发消息 + 对方刷新拉取”模式,但用户体验很差。后来升级为 WebSocket 在线推送,离线消息落库,用户上线时去拉取未读消息。WebSocket 握手时前端把 JWT 传过一个token参数,后端拦截器校验通过后绑定 userId 与 Channel。
关键消息表结构设计如下:
CREATE TABLE `message` ( `id` bigint NOT NULL AUTO_INCREMENT, `from_user_id` bigint NOT NULL, `to_user_id` bigint NOT NULL, `content` text COMMENT '消息内容(已过滤敏感词)', `msg_type` tinyint DEFAULT '0' COMMENT '0文字 1图片 2语音', `is_read` tinyint DEFAULT '0', `need_vip_read` tinyint DEFAULT '0' COMMENT '对方未读时是否需会员可见', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_two_user` (`from_user_id`,`to_user_id`,`create_time`) ) ENGINE=InnoDB COMMENT='私信消息表';这里有个产品层面的坑:非会员用户之间发私信,如果对方没回,容易产生骚扰问题。我的方案是限制非会员每天主动发私信 10 条,超过则引导开通会员;同时引入举报机制,接到举报后人工审核,一旦确认骚扰就禁用账号。
3.6 图片上传与照片审核
照片上传我用的方案是前端把图片压缩到 1080p 以下,通过预签名 URL 直传阿里 OSS,避免后端做大二进制流中转。后端收到回调通知后,把图片 URL 入库并设置状态为“待审核”。管理后台照片审核通过后,这条照片才在用户主页可见。
这里的一个难点是:用户头像被审核通过后,推荐列表里会显示;但相册里的生活照如果包含明显广告、二维码、非法内容,必须第一时间拦截。我做了一个异步审核流程:上传成功立即丢消息队列(不引重型 MQ,用 Redis List 模拟消息队列),后台定时拉取审核。同时联合第三方图片内容安全接口做机审,高危图片直接置为“不通过”,普通图片进入人工队列。
4. 前端 Vue3 实现与交互细节
4.1 前端项目结构与状态管理
前端与后端的接口对话完全走 REST API。工程结构上,我按“页面-组件-状态-API-路由”五个维度管理。存储方案用的 Pinia,拆分出userStore、profileStore、messageStore。如果用 Vuex 不是不行,但 Vue3 组合式 API 环境下 Pinia 的 TypeScript 支持和模块化程度更好,代码量也更少。
Vue Router 我采用了两种路由模式:用户端普通访问,管理端用懒加载路由import()拆包,控制首屏体积。所有需要登录的页面配置beforeEach守卫,无 token 自动跳转登录页,同时记录跳转前地址,登录成功后可以完美回到原来的页面。这个体验细节很多人忽略,但实际用户非常在意。
4.2 推荐页与条件筛选组件的实现
首页推荐列表我用了“卡片式”展示,每个卡片展示头像、昵称、年龄、城市、匹配指数,点击卡片进入详情页。卡片滑过时触发一个动效,展示资料完整度和最近活跃时间。筛选组件用 Element Plus 的el-select、el-slider、el-date-picker组合,选择条件后直接调/api/match/filter,后端返回分页数据。
筛选条件里有几个特殊项:
- 年龄区间:前端滑杆是单个范围,后端参数是
minAge和maxAge - 身高区间:同理
- 学历:下拉枚举,按“不限、大专、本科、硕士、博士”排序
- 是否只看实名认证:单选开关
为了让推荐结果保持新鲜感,我在接口里加了“随机游览”参数。如果用户连续滑动超过 30 个推荐仍没有意向,前端“换个推荐”按钮会带isShuffle=true请求,后端从候选池随机抽取新的 30 人。实测这个细节对提升留存帮助很大。
4.3 个人中心与资料编辑体验优化
个人中心资料编辑有两个容易让用户放弃的重灾区:一是填写项太多,二是照片上传交互太差。我的处理策略是分步骤表单,分四步:基础信息、个人介绍、择偶要求、照片上传。每一步都有进度条,后端profileStore实时保存草稿,用户下次进来直接看到上次填到哪一步,不需要从头再来。
这里分享一个前端细节:手机号验证码输入后,倒计时按钮用的是 sessionStorage 记录剩余秒数,而不是变量,这样刷新页面不会丢失倒计时。这个逻辑看似小,但用户切走再回来体验非常自然。
4.4 聊天页面与实时消息推送
聊天页面用了虚拟滚动列表来容纳超长消息记录。进入聊天页时,通过 WebSocket 建立长连接,服务端推送新消息后,前端把未读计数清零,并把消息插入到底部,如果用户正在向上翻阅历史记录,则显示“有新消息”按钮,点击再跳到新消息位置,而不是粗暴打断阅读位置。
这里还需要处理一个特殊场景:用户 A 给用户 B 发消息,B 在聊天列表页但还没进具体聊天室,需要显示未读红点。这个我维护了一个 Redis ZSet,key 是unread:{userId},score 是最后一条消息的时间戳,value 是对端 userId。前端每隔 3 秒轮询拉一次未读总数,性能和实时性平衡得不错。
5. 数据库设计与性能优化
5.1 核心表结构全览
除用户表和消息表外,还有几张关键业务表。我把它们的分工用表说明:
| 表名 | 用途 | 核心字段 |
|---|---|---|
user_like | 用户之间的意向记录 | from_user_id, to_user_id, status(1喜欢 2超级喜欢 3取消) |
user_vistor | 谁看过我 | visitor_id, visited_user_id, view_time |
user_blacklist | 黑名单 | user_id, target_user_id |
member_order | 会员订单 | user_id, plan_id, pay_amount, status, expire_time |
photo_audit | 照片审核流水 | photo_id, audit_status, audit_remark, operator_id |
report_record | 举报记录 | report_user_id, target_user_id, reason, status |
operation_log | 管理员操作日志 | admin_id, module, action, ip, detail |
这些表是相亲系统的基础骨架。设计时几乎每张表都带了 status 字段,方便后续做逻辑删除和审核流,而不是用物理删除。逻辑删除的好处是方便数据追溯,比如“取消喜欢”并不真正删除记录,而是把 status 改成 3,这样统计用户行为时可以回溯。
5.2 索引设计与慢查询优化
用户量到了一定规模,搜索接口最容易出的性能问题就是全表扫描。我的搜索接口组合条件很多,不能为每个字段建单列索引,否则 MySQL 优化器会选错索引。我用了联合索引覆盖高频组合:
ALTER TABLE `user_profile` ADD INDEX idx_match_query (city, gender, birth_date, status);但注意:低频组合不要瞎建索引。比如“收入范围”只有少数人筛选,建索引收益极低,还拖慢插入性能。针对这类字段,我借助 MySQL 8.0 的降序索引特性,把birth_date放到联合索引里,同时查询时按出生日期倒序。
慢查询日志是必须开的:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;上线初期我就靠慢 SQL 日志抓到了两个大问题:一个是推荐列表里关联user_like表判断是否已喜欢过,导致NOT IN子查询扫描全量,改成LEFT JOIN + IS NULL后快了十几倍;另一个是列表页用函数处理时间字段,导致索引失效,改成传入时间范围参数后解决。
5.3 Redis 缓存使用策略
我缓存的场景主要有三类:
- 首页推荐候选池,key 如
match:candidate:{userId},24小时过期 - 在线状态,key 如
user:online:{userId},存心跳时间戳,命中说明在线 - 验证码,key 如
sms:code:{phone},5分钟过期
这里想重点讲一下缓存一致性。用户资料表更新时,最怕用户主页展示的还是旧数据。我的做法是“更新数据库后主动删除缓存 Key”,而不是直接更新缓存。下次请求再查库回填,简单可靠。如果你采用“先更新缓存再更新库”的顺序,万一缓存成功而数据库失败,那么缓存会一直留存错误数据,反而不如删除缓存来得干净。
5.4 分页插件用法经验谈
网上铺天盖地是 PageHelper 的教程,我在这套系统里用的是 MyBatis-Plus 自带的分页插件。因为它和 SpringBoot2.7 整合更丝滑,只需配置一个PaginationInnerInterceptor,写法和普通 Mapper 查询几乎一致:
// 伪代码,实际项目是 Java Page<UserProfileVO> page = Page.of(1, 10); LambdaQueryWrapper<UserProfile> wrapper = Wrappers.lambdaQuery(); wrapper.eq(条件...).orderByDesc(UserProfile::getCreateTime); userMapper.selectPage(page, wrapper);但注意分页插件在自定义多表关联查询时不会自动帮你统计数据量。如果你在 XML 里写了自定义selectMatchList,它能够通过 MyBatis-Plus 的PaginationInnerInterceptor自动拦截生成 count 查询,但这个 count 逻辑有时不准,特别是用了GROUP BY和DISTINCT时。遇到这种情况,我自己写一条精确 count SQL,放进 XML 里通过@Select注解或 mapper 方法单独调用,以便保证总条数真实。
6. 项目部署与运维实战
6.1 云服务器与 Docker 部署方案
我一直用的部署方案是 Docker Compose 编排所有服务,这样换服务器时简直是无痛迁移。整个 compose 文件包含四个服务:db、redis、backend、frontend。
后端镜像构建时有个关键的优化点:多阶段构建。第一阶段用 maven 打包,第二阶段只放jar包和JRE,镜像体积能控制在 200MB 左右。如果不做多阶段构建,镜像体积动辄 500MB,而且在低配服务器上拉取镜像超级慢。
# 第一阶段:编译 FROM maven:3.8.6-jdk8 AS builder COPY . /app WORKDIR /app RUN mvn clean package -DskipTests # 第二阶段:运行 FROM openjdk:8-jre-alpine COPY --from=builder /app/target/matchmaking.jar /app.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","/app.jar","--spring.profiles.active=prod"]前端镜像我直接用 Nginx 作为基础镜像,构建时把dist目录拷进去。Nginx 里我还配了/api反向代理到后端容器,避免跨域问题。
6.2 Nginx 配置与 HTTPS 证书
前后端分离项目,最理想的是把静态资源和 API 放在同一个域下,通过 Nginx 路径区分。这样既省了浏览器的跨域限制,也能统一使用 Cookie 或 Header 传递 Token。我的配置核心块如下:
server { listen 443 ssl; server_name match.example.com; ssl_certificate /etc/nginx/cert/fullchain.pem; ssl_certificate_key /etc/nginx/cert/privkey.pem; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://backend:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /ws { proxy_pass http://backend:8080/ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } location / { try_files $uri $uri/ /index.html; } }这里location /ws必须开启 WebSocket Upgrade 头,否则聊天实时推送功能会失效。我最初没配好,前端一直报 404,排查了半天才发现是 Nginx 把 WebSocket 请求按普通 HTTP 转发导致的。
HTTPS 证书我用的是 certbot 申请的免费证书,脚本支持自动续期,每两个月会收到续期通知。对个人项目来说性价比极高。
6.3 上线后的监控与日志
上线后不能光看功能跑通就完事。我接入了两个轻量监控:一是 SpringBoot Actuator 暴露健康接口,用 aapanel(宝塔面板)做进程守护;二是日志用 logback 输出成 JSON 格式,方便后期做日志分析。关键异常通过邮件发送告警,这样半夜出问题也能第一时间收到邮件。
对于新人,我的建议是先把日志级别调成 INFO,不要一上来就 DEBUG,否则线上日志文件一天撑爆磁盘。我自己的日志按天滚动,保留 7 天,单文件上限 200MB,磁盘 50GB 扛住公司项目的规模也没问题。
6.4 数据库备份与恢复
相亲系统涉及实名信息和聊天记录,数据备份是底线要求。我写了一个每日自动备份脚本,凌晨 3 点执行mysqldump导出全量 SQL,并上传到 OSS 保留 30 天。恢复时用mysql -u root -p < backup.sql即可,但要注意备份前必须关闭外键检查,否则由于导出顺序问题恢复可能失败。
mysqldump -uroot -p --single-transaction --set-gtid-purged=OFF --triggers --routines --events matchmaking_db > /backup/match_$(date +%Y%m%d).sql这个脚本里--single-transaction是 InnoDB 表的在线备份关键参数,不加会导致备份期间数据写入被锁表,用户端会出现页面卡顿。
7. 常见问题与避坑指南
7.1 后端开发中踩过的典型坑
跨域问题:前后端分离项目在开发环境常见,配置 SpringBoot 的CorsFilter即可,但要注意前端请求带Authorization头时,allowedHeaders必须显式放行,否则浏览器会先发一个 OPTIONS 预检请求,后端如果没处理 OPTIONS 就会一直报跨域错误。
MyBatis 空值更新:很多新手用updateById时发现参数为 null 的字段被覆盖成空。MyBatis-Plus 默认字段策略是NOT_NULL,也就是说只有非空字段才更新,这是项目初期最容易忽略的隐式约定。当你需要“主动把字段置空”时,要在实体字段上标注@TableField(updateStrategy = FieldStrategy.IGNORED),否则会白调试半天。
时间精度问题:MySQL 8.0 的datetime默认精度到秒,但 Java 的LocalDateTime可以带毫秒,如果两边没对齐,查询“某个时间点之后的记录”会出现边界漏数据。我的解法是统一用秒级时间戳比较,或者数据库datetime(3)指定毫秒精度。
大文件上传内存溢出:SpringBoot 默认max-file-size是 1MB,照片如果超了直接报错。一定要在配置里调大:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB但不要贪心,限制太大容易被打流量攻击。我线上设的是 10MB,前端先压缩再上传,基本够用。
事务不回滚问题:Service 方法里调用同类内部方法时,如果事务注解只加在内部方法上,会因为代理失效导致事务不生效。解决办法是事务注解加在外部入口方法上,或者拆成两个 Bean 再注入调用。
7.2 前端开发中的关键坑
路由守卫与 pinia 初始化顺序问题:路由守卫里使用 Pinia 的 store,如果 Pinia 实例尚未初始化,会报 “getActivePinia() was called but there was no active Pinia”。解决方式是先const pinia = createPinia()然后app.use(pinia),再安装 router。顺序不能反,很多人在这里卡住。
图片懒加载白屏:Vue3 中使用v-lazy指令加载图片,如果图片 URL 售后被审核删除,会导致加载失败白屏。我加了一个@error兜底事件,失败时自动替换为默认占位图,这样列表页不会出现一堆撕裂图片。
虚拟滚动键盘事件丢失:聊天列表做了虚拟滚动后发现 input 框失焦,排查后发现滚动容器的tabindex属性没设置,浏览器无法把焦点维持在滚动区域内。修复方法是给容器加上tabindex="0",并监听focus/blur事件。
定时器清理:很多前端页面用setInterval轮询未读数,但在组件卸载时没有清理导致内存泄漏,页面切换几次后浏览器明显变卡。我的写法是const timer = setInterval(...); onBeforeUnmount(() => clearInterval(timer)),并用 ref 保存 timer 引用,避免多实例互相覆盖。
7.3 排查问题的一般方法论
线上出问题时,我的排查顺序基本固定,这套流程对新人很友好:先看 Nginx 访问日志和错误日志,确认请求有没有到达后端;再看后端日志的异常堆栈,不要看第一行就下结论,要从Caused by开始找根因;然后看 SQL 日志,确认是否走到了慢查询分支;最后如果是数据问题,直接查库看数据状态。
定位 WebSocket 问题时,浏览器 F12 的 Network 面板能看到 WS 帧,如果握手失败会有明显的 401 或 404。这时候先检查 Nginx 的 Upgrade 配置,再检查拦截器是否正确读取 token,通常问题出在 token 传递位置不一致。
7.4 会员过期与数据清理定时任务
我后台定时任务用的是 Spring 自带的@Scheduled,每天凌晨两点执行会员过期扫描,把member_expire_time < NOW()的用户批量降为普通用户。批量更新时要控制每次处理数量,比如LIMIT 1000,避免一次锁上千条记录导致其他查询阻塞。
数据清理上,超过两年的聊天记录我迁移到冷表message_history,主表只保留最近两年,这样消息查询性能能保持稳定。我每个月跑一次归档脚本,从未影响过线上服务。
8. 实操经验与项目复盘
8.1 从零到上线的主要时间线
我把项目开发时间线列一下,给准备做类似系统的人一个预期参考:
- 第1周:需求梳理、原型设计、数据库表设计
- 第2-4周:后端基础框架搭建、用户登录注册、资料管理
- 第5-6周:匹配推荐、意向、私信模块
- 第7周:管理后台接口与前端管理页面
- 第8周:前后端联调、接口测试
- 第9周:部署上线、压测、修 bug
实际速度取决于你对 SpringBoot 和 Vue3 的熟悉度。我第一次做这个项目用了两个月,第二次复用这套代码一周就能搭起新站点。所以源码的可复用性非常重要,设计时我特意把业务与平台逻辑解耦,换一个垂直行业(比如宠物交友、租房找室友)时,只需替换匹配指标和表单字段。
8.2 我沉淀下来的一些设计习惯
习惯一:所有接口的 VO 类不要裸奔返回实体类。比如用户表里有password、idCard这些字段,我用了专门的UserVO做字段裁剪,避免因疏忽把敏感数据暴露。
习惯二:接口层尽量幂等。前端聊天消息发送失败会自动重发,可能导致同一消息插入两次。我给message表加了client_msg_id字段,前端生成一次性的 UUID,后端通过唯一索引去重,实测重发导致的重复消息直接消失。
习惯三:配置统一用application-prod.yml和application-dev.yml区分环境,密码、密钥通过环境变量注入,不写死在代码中。这样即使源码泄露,线上数据库密码也不会暴露。
习惯四:写 SQL 时养成习惯先EXPLAIN看执行计划。我对自己定的规矩是:多表连接和子查询必须 EXPLAIN 过才允许上线,单表慢查询阈值是 500ms 以上必须优化。
8.3 关于“后端管理”思路的思考
管理后台我之前用过若依脚手架,后来还是换成了自己手写,原因不是若依不好,而是相亲系统的管理后台高度定制化,比如照片审核页面需要大图预览和标记,会员订单页需要人工退款按钮,访客统计需要图表,这些在通用脚手架里改起来反而比从零写更费劲。但我仍然建议新手先拿若依跑通一遍流程,理解权限管理、代码生成器的写法,再迭代出自己的方案,学习曲线会更平滑。
管理端我用到的主要页面有:用户管理、会员管理、照片审核、举报处理、数据看板。每个页面都有导出 Excel 的能力,用的是阿里 EasyExcel 组件,解决了 POI 写大数据量 Excel 容易内存溢出的问题。
8.4 代码版本管理与协作提效
个人项目也建议用 Git 做版本管理,我习惯用主干开发加特性分支的方式。每个功能模块建一个分支,开发完再合并到 master,合并前必须过一遍mvn test和npm run build。这样即使某个功能写坏了,也能快速回滚到上一个稳定版本。
接口文档我用的 Postman,把所有接口按模块分组,每个接口写清楚入参、出参和鉴权需求。前端同事直接导入 Postman 就能联调,不需要反复口头沟通字段含义。
9. 扩展功能与未来改进建议
9.1 聊天消息已读回执的改进
当前系统的已读状态是轮询拉取未读数,实时感还是差点意思。下一步我计划加“已读回执”事件,在 WebSocket 内部推送已读状态,类似 IM 的已读功能。具体做法是:消息表中增加read_time字段,对方打开聊天室后,前端把当前会话的lastReadTime发给后端,后端批量更新该时间前的所有未读消息为已读,并通过 WS 通知对方发送端更新消息状态。
9.2 推荐算法升级到向量化
下一步如果数据规模上来,我准备引入向量数据库和 Embedding 模型,把用户资料文本向量化,做语义召回而不是单纯规则过滤。比如用户写“希望对方喜欢旅行”,传统 SQL 很难精确召回,但向量相似度可以做到。然后规则打分作为精排,形成“召回-粗排-精排”的完整流程,这与现代推荐系统思路一致。不过这套对硬件和运维要求高,个人项目可以先在本地 GPU 机器上实验。
9.3 前后端联调的自动测试
目前联调靠手工点页面,后面我计划引入 Postman + Newman 做接口自动化回归,前端引入 Vitest 做核心组件测试,让迭代更稳。接口测试的关键点是把会员逻辑、黑名单逻辑、敏感词过滤逻辑这些容易回归出 bug 的场景覆盖住,每次改动一键执行全量回归。
9.4 多端适配的思考
现在系统只有 Web 端,移动端是很多相亲产品的强需求。好消息是,后端接口全是 REST 风格,前端 Vue3 的 store 和 api 层也跟 UI 解耦,未来可以开发小程序或者 App。我建议优先做小程序版本,因为微信生态的登录和分享能力对社交产品加成很大,只需写一套新的前端壳子,复用现有后端即可。
9.5 敏感内容过滤与合规设计
相亲平台最怕灰色内容,敏感词过滤我用的开源库sensitive-word,同时接入了图片鉴黄接口。敏感词库我维护了两份:一份通用库,一份相亲场景自扩充库,比如“拉人进群”“一夜情”这类高频异常词。聊天接口发送前过一道过滤,命中可疑词直接拦截并记录审计日志,供管理端分析处理。这个设计在合规层面非常必要,上线前建议咨询平台内容安全规范,做好风控措施。
最后说点我实际的体感。做这个相亲系统,最大的收获不是学会了 SpringBoot 或者 Vue3 某个 API,而是把一套完整业务从前到后串起来的思维。很多人每天背 java 面试题、抄 springboot 配置,但没有完整地做出过一个项目,面试时一深问就露馅。这套系统的代码我至今仍在迭代,每次加需求都还会遇到新问题,比如最近在改消息模块的已读回执,又发现了 WebSocket 断线重连状态没有完全清理干净的隐患。项目就是这样,永远不会“做完”,但只要骨架搭得够稳,后续就是一层一层往上面加血肉。如果你手头也在做类似社交或交友类系统,希望这篇的数据库表设计和踩坑记录能帮你少走几趟弯路。