首页 / 资讯中心 / 文章详情

SpringBoot+Vue3相亲网站系统实战:从架构设计到前后端分离部署

SpringBoot+Vue3相亲网站系统实战:从架构设计到前后端分离部署 ★ FEATURED ARTICLE
自己折腾相亲网站系统也有一阵子了从最开始用 JSPServlet 手写页面到现在前后端分离的 SpringBootVue3MyBatisMySQL 整套中间踩了不少坑也沉淀了一些实战经验。这篇就围绕我实际开发的一套相亲网站系统源码把架构设计、数据库模型、核心业务逻辑、接口实现到前端联调通通捋一遍重点讲我在做会员匹配、实名认证、私信聊天这些模块时的细节取舍。如果你正准备用 Java 技术栈做社交类项目或者想学前后端分离项目怎么写这篇应该能省你不少时间。1. 需求分析与功能模块梳理1.1 相亲网站的核心需求定位做相亲系统跟做普通电商、博客不一样用户对隐私和匹配准确度的要求极高。我一开始就给自己定了几个硬性指标信息展示必须区分“公开”和“脱敏”两套匹配逻辑不能只靠简单 SQL 条件拼凑要有一定权重聊天模块必须能控制敏感词和图片安全。这套系统定位自用学习和二次开发所以我在设计时没有堆砌复杂微服务而是用一个单体 SpringBoot 项目加合理模块划分搞定保证部署简单、理解成本低。从功能范围看我把它拆成了四条主线用户端注册、登录、完善资料、上传照片、浏览推荐、条件搜索、查看意向列表。匹配端基于地域、年龄、学历、身高等多维度打分输出匹配指数。互动端意向记录、私信聊天、查看访客、加入黑名单。管理端用户审核、照片审核、数据统计、会员管理、举报处理。控制好这个范围整个系统在代码层面能保持清爽业务上也能覆盖相亲网站最核心的玩法。我见过不少项目一开始就加直播、送礼物结果后端撑不住前端也乱成一锅粥这种教训不必重复。1.2 角色权限与业务边界这套系统一共有三种核心角色普通用户、会员用户、管理员。我把角色语义直接压进 JWT Token 里配合 Spring Security 做接口级校验。普通用户能看基础资料、搜索、发意向但不能查看对方精确联系方式。会员用户在普通用户基础上能看完整隐私信息、使用高级筛选、消息免打扰。管理员后台审核、禁用违规账号、查看统计报表。角色之间的权限边界必须后置在服务端控制不能只靠前端隐藏按钮。比如用户传入“查看手机号”的接口后端要先判断当前用户是否为会员且是否通过实名认证两项都满足才返回明文号码否则一律脱敏。1.3 系统使用场景说明这套系统适合三类场景一是个人学习 SpringBootVue3 整合最佳实践二是毕业设计或课程项目需要一套完整前后端分离代码三是小范围创业试点比如本地相亲角、园区内部联谊的线上化。我的部署环境是一台 4核8G 的云服务器跑 CentOS 7.9MySQL 8.0 和 Redis 都放在同一台机器上演示规模下压力不大一万会员量级完全撑得住。2. 技术选型与环境准备2.1 前后端分离的技术选型逻辑我为什么选 SpringBoot Vue3 而不是用若依这类现成脚手架因为相亲系统有大量个性化表单、图片上传、实时聊天前端需要比较强的组件灵活性Vue3 的组合式 API 更适合这种复杂状态管理。后端用 SpringBoot 2.7.x理由很简单生态成熟、资料多、出问题好排查而且与 MyBatis 配合非常顺。组件版本方面我列一个实测可用的组合避免新手踩版本不兼容的坑组件版本说明JDK1.8稳定兼容性最好SpringBoot2.7.14基于 JDK8 的最终版本MyBatis2.2.2与 SpringBoot2.7 对应MyBatis-Plus3.5.2简化单表 CRUDMySQL8.0.32支持 JSON 字段Vue33.3.4组合式APIElement Plus2.4.0后台/用户端组件库JWT0.11.5无状态登录认证Redis6.2.7验证码、在线状态缓存这里单独说下 MyBatis-Plus 的定位它和原生 MyBatis 不冲突我是在原生基础上引入了便捷能力复杂查询仍然手写 XML。很多人担心 Plus 会把 MyBatis 的“手写 SQL 灵魂”丢掉实际上只要你自己控制好滥用边界项目既快又能满足复杂业务。2.2 本地开发环境搭建这一步看起来基础但坑不少我按自己的操作顺序整理一下。先装 JDK8配置好JAVA_HOME再装 IDEA安装 Lombok 插件。数据库我用 Docker 起了个 MySQLdocker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e TZAsia/Shanghai \ mysql:8.0.32 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci \ --default-authentication-pluginmysql_native_password注意mysql_native_password这个参数很重要如果不指定SpringBoot 连接早年版本的驱动时可能出现认证插件报错。前端我用 Vite 创建 Vue3 工程npm create vitelatest 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) ) ENGINEInnoDB 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 是 userIdvalue 是候选用户IDscore 是匹配分。用户刷新推荐时直接从 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) ) ENGINEInnoDB 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 个推荐仍没有意向前端“换个推荐”按钮会带isShuffletrue请求后端从候选池随机抽取新的 30 人。实测这个细节对提升留存帮助很大。4.3 个人中心与资料编辑体验优化个人中心资料编辑有两个容易让用户放弃的重灾区一是填写项太多二是照片上传交互太差。我的处理策略是分步骤表单分四步基础信息、个人介绍、择偶要求、照片上传。每一步都有进度条后端profileStore实时保存草稿用户下次进来直接看到上次填到哪一步不需要从头再来。这里分享一个前端细节手机号验证码输入后倒计时按钮用的是 sessionStorage 记录剩余秒数而不是变量这样刷新页面不会丢失倒计时。这个逻辑看似小但用户切走再回来体验非常自然。4.4 聊天页面与实时消息推送聊天页面用了虚拟滚动列表来容纳超长消息记录。进入聊天页时通过 WebSocket 建立长连接服务端推送新消息后前端把未读计数清零并把消息插入到底部如果用户正在向上翻阅历史记录则显示“有新消息”按钮点击再跳到新消息位置而不是粗暴打断阅读位置。这里还需要处理一个特殊场景用户 A 给用户 B 发消息B 在聊天列表页但还没进具体聊天室需要显示未读红点。这个我维护了一个 Redis ZSetkey 是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_timeuser_blacklist黑名单user_id, target_user_idmember_order会员订单user_id, plan_id, pay_amount, status, expire_timephoto_audit照片审核流水photo_id, audit_status, audit_remark, operator_idreport_record举报记录report_user_id, target_user_id, reason, statusoperation_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 PageUserProfileVO page Page.of(1, 10); LambdaQueryWrapperUserProfile 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 --frombuilder /app/target/matchmaking.jar /app.jar EXPOSE 8080 ENTRYPOINT [java,-jar,/app.jar,--spring.profiles.activeprod]前端镜像我直接用 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-purgedOFF --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属性没设置浏览器无法把焦点维持在滚动区域内。修复方法是给容器加上tabindex0并监听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 断线重连状态没有完全清理干净的隐患。项目就是这样永远不会“做完”但只要骨架搭得够稳后续就是一层一层往上面加血肉。如果你手头也在做类似社交或交友类系统希望这篇的数据库表设计和踩坑记录能帮你少走几趟弯路。
阅读完成 · 觉得有帮助?
咨询建站