“基于Spring Boot的交友平台”这个题目,在毕业设计里出现频率不算低,但真正能把它做得完整、不糊弄的并不多。这个项目如果用一句话概括,就是一套整合了用户注册、资料展示、心动匹配、私信聊天这些核心场景的后端系统。老实说,这个题目比普通的单表CRUD管理系统要舒服很多,因为它的业务逻辑更丰富,能展示的东西更多,但又不至于复杂到让人在一个学期里做不完。这篇文章就围绕这个毕设项目,从整体设计、功能拆解、关键实现到部署排坑,完整记录一遍实操过程,希望能给正在做类似题目的朋友省下不少时间。
1. 项目定位与整体设计思路
1.1 这个项目到底在解决什么问题
很多同学看到“交友平台”四个字,第一反应是“这不是要做一个探探/陌陌吗?”,然后就开始焦虑推荐算法、LBS定位这些东西。实际上,作为毕业设计,这类项目要解决的从来不是一个完整的商业产品,而是以下几个具体问题:
- 用户如何注册、登录、完善自己的交友资料;
- 用户如何看到其他用户,并表达喜欢或不感兴趣;
- 两个人互相喜欢之后,怎么建立匹配关系;
- 匹配成功后如何发起聊天,消息如何存储和推送。
把这些问题做好,项目的完成度就已经很扎实了。反而是那些想着堆砌“智能推荐算法”“实时音视频”的同学,最后往往连基本流程都没跑通。我经手这个项目的时候,最初的需求文档也很简单,就六张表、四个页面、八个接口,但真正落地时,每一条都值得仔细打磨。
1.2 后端为什么选Spring Boot
先说结论:这个题目选择Spring Boot,不是因为它是最潮的技术,而是因为它最稳、最合适。后端用Spring Boot,有几个不可替代的优势:
- 自动配置极大降低了上手门槛,一个
spring-boot-starter-web就能把Web环境搭起来; - 生态成熟,配合MyBatis-Plus操作数据库非常省事,CRUD基本不用自己写SQL;
- 后续如果要扩展Redis缓存、定时任务、消息推送,starter都是一个坐标的事,不会遇到写C++那种“什么都要自己造轮子”的尴尬。
真正动手之前,我给项目定了一个很克制的技术方案:Spring Boot 2.7 + MyBatis-Plus + MySQL + Redis(只做验证码和Session存储)+ WebSocket。前端部分直接用Vue3 + Element Plus,如果你前端不太熟,用Thymeleaf加Boostrap也能做出来,只是交互体验会弱一点。方案里没有引入微服务、消息队列这些重东西,核心原因是:这个项目体量用单机加单库完全够,引入复杂技术只会给自己增加部署和排查的负担。
1.3 项目目录结构怎么规划
目录结构是很多人容易忽略的地方,但它直接决定后面开发是否顺手。我用的是经典三层结构加模块分包:
src/main/java/com/example/dating ├── controller // 接收请求,返回统一结果 ├── service // 业务逻辑层,接口加实现 ├── mapper // 数据库操作层 ├── entity // 数据库实体 ├── dto // 请求参数封装 ├── vo // 返回视图对象 ├── config // 配置类:拦截器、跨域、WebSocket等 ├── utils // 通用工具类:JWT、加密、日期处理 └── common // 统一返回结果、全局异常、常量这个结构的核心思路是:controller层越薄越好,业务逻辑集中在service,数据访问收敛在mapper。很多同学容易犯的毛病是controller里写一堆业务判断,结果一个接口几百行,后面改需求时痛苦得要命。我后期调试项目时,只要看到controller里的代码超过30行,就会直接重构。
2. 核心功能拆解与数据库设计
2.1 用户模块:注册、登录、资料完整度
用户模块是所有功能的地基。注册这里有一个很多资料里都不细说的点:交友平台必须要让用户建立资料,而不是只注册一个账号。所以我在设计时把注册和信息完善分成了两步。
第一步是注册账号,字段只有手机号、密码、昵称。密码用BCrypt加密存储,这一点不能省,明文存密码不仅答辩时会被老师diss,也显得很不专业。第二步是完善资料,包括性别、生日、城市、头像、个人介绍、兴趣爱好标签。这里引入了一个“资料完整度”概念:每填写一个字段,完整度就加对应分数。之所以这么做,是因为后面做匹配推荐时,可以要求资料完整度达到80%以上的用户才进入推荐池,这样既减少了垃圾账号,又让匹配结果的质量高很多。
// 资料完整度计算 public Integer calculateProfileCompletion(Long userId) { Profile profile = profileMapper.selectById(userId); int rate = 0; if (StringUtils.hasText(profile.getNickname())) rate += 10; if (StringUtils.hasText(profile.getAvatar())) rate += 20; if (profile.getGender() != null) rate += 10; if (profile.getBirthday() != null) rate += 10; if (StringUtils.hasText(profile.getCity())) rate += 10; if (StringUtils.hasText(profile.getBio())) rate += 10; if (profile.getTags() != null && !profile.getTags().isEmpty()) rate += 30; return rate; }注意:这里的标签字段在前端展示时是多个可选标签,在后端数据库里我直接用了JSON字符串存储,没有单独建一张标签表。优点是实现简单、查询一次就能拿全;缺点是无法按标签做高效SQL查询。对毕设体量来说,这个取舍完全没问题。
2.2 交友匹配模块:推荐、喜欢、互相喜欢
交友匹配是这个项目的业务灵魂。最开始我差点把它做成“列表页+筛选条件”,后来仔细想了想,筛性别筛年龄那是最原始的相亲网站思路,显得目光短浅。所以最终做成了三个子功能:
- 每日推荐:系统根据当前用户的资料和标签,推荐一批可能感兴趣的异性用户;
- 喜欢与不喜欢:用户对推荐卡片左滑右滑或点击按钮,表达态度;
- 互相喜欢即匹配:如果A喜欢了B,而B在此之前已经喜欢过A,则双方建立匹配关系,并在双方的匹配列表中可见。
这里有个容易忽略的逻辑:匹配的判定是“双向同时成立”。A喜欢B时,系统要先查一下B是否已经喜欢了A,如果成立则通知双方“你们互相喜欢了”,否则保存Like记录,等待对方回应即可。我用一张user_like表存储所有喜欢行为,并加了唯一索引(from_user_id, to_user_id),从源头避免重复记录。
2.3 消息聊天模块:会话、消息、已读
匹配成功之后,用户需要能聊天。毕设级别的聊天模块不需要做到微信那种复杂度,但至少要支持两点:发消息、看消息列表。我额外加了已读/未读状态,这个对于交友场景特别重要,因为用户很在意“对方到底有没有看过我发的消息”。
聊天这里我用了WebSocket做实时推送。用户A给用户B发消息时,后端收到消息后先存入chat_message表,然后通过WebSocket把消息推给在线用户B,同时更新会话表的最新消息和未读数量。这里有一个设计细节:会话表chat_session并不在用户匹配成功后立刻创建,而是在第一条消息发出时懒创建。为什么这么做?因为实际使用中很多人匹配了却不会立刻聊天,如果预创建大量会话,会造成“空会话“垃圾数据,列表页还要专门过滤,得不偿失。
2.4 数据表关系设计
数据库是整个项目的核心,我最终设计了八张核心表,梳理清楚靠这一张结构说明就够了:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| user | 用户账号 | id, phone, password, status |
| user_profile | 用户资料 | user_id, nickname, gender, birthday, city, avatar, bio, tags |
| user_like | 喜欢/不喜欢记录 | from_user_id, to_user_id, is_like, create_time |
| match_record | 匹配成功记录 | user_id_a, user_id_b, matched_time |
| chat_session | 聊天会话 | id, user_a, user_b, last_message, last_time |
| chat_message | 聊天消息 | session_id, sender_id, content, msg_type, read_status |
| user_blacklist | 拉黑列表 | user_id, target_user_id |
| feedback | 用户反馈 | user_id, content, contact |
表之间的关系也很直接:user和user_profile一对一;user_like和match_record都是从用户衍生出来的一对多;chat_session关联两个用户,chat_message归属于一个会话。我画E-R图的时候就按这个关系描述,答辩老师看了也不会追问太多细节问题。
3. 关键接口与核心实现
3.1 统一返回结构与全局异常处理
前后端分离的项目里,接口风格统一非常影响开发效率。我定义了一个Result类,所有接口都返回这种结构:
public class Result<T> { private Integer code; private String message; private T data; }同时配合了全局异常处理器,业务异常、参数校验异常、未知异常都有对应的反馈。比如参数校验失败,前端拿到的code是400,message是具体字段错误;系统内部出错,返回500,但不会把异常堆栈直接抛给前端。这么做的好处是前端可以只依赖code判断状态,不需要每个接口各写一套错误处理逻辑。我实际开发中前后端联调的速度提升非常明显,很多接口根本不需要看后端日志就能定位问题。
3.2 登录态校验与跨域问题
用了JWT做登录令牌。用户登录成功后生成一个token,前端将其放在Authorization请求头里。后端通过一个拦截器解析token,并把当前用户ID放入ThreadLocal,业务层直接通过工具类获取当前登录用户,不需要每个方法的入参里都传userId。
不过JWT有一个需要注意的坑:token失效时间不要设置太长,我一开始设置了7天,结果用户修改密码后旧token还是有效的,存在安全隐患。后来改成2小时过期,前端在检测到401时自动跳转登录页重新登录。为了不让用户频繁重新登录,我在拦截器里实现了一个简单的token续期机制:只要请求还在有效期内,就刷新过期时间并返回新token。
跨域问题也是前后端分离必踩的坑。在开发环境我直接通过一个CorsConfig放行所有请求,生产环境则配置了指定域名白名单。这里放一张代码片段:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }3.3 匹配推荐算法的实现思路
推荐算法这个词听起来高深,但在毕设项目中完全可以做“基于规则的推荐”,效果也不会差。我的实现逻辑是这样的:从当前用户资料中取出性别、城市、标签,然后从推荐池中过滤掉自己、已喜欢的人、已不喜欢的人、已拉黑的人,再计算一个匹配分,按分数排序。
匹配分由两个部分组成,一部分是标签重合度,另一部分是活跃度。标签重合度的计算不复杂:把用户的标签列表和候选人的标签列表都拆出来求交集,交集越多分越高。活跃度则为最近登录过的用户加分。促销一下公式:
匹配分 = 标签重合数量 * 30 + 城市相同 ? 20 : 0 + 资料完整度 * 0.2 + 最近登录时间系数这套规则跑起来效果相当直观:两个人如果同城、标签相似度又高,排名必然靠前。题目如果偏向“智能”一点,还可以在这个基础上加入“看过我和喜欢我的人优先”,但这做好后反而推荐列表会被少数人霸占,对体验不一定是好事。
3.4 聊天消息的已读状态处理
聊天消息的已读逻辑看似简单,实际上特别容易出错。我最开始的方案是每条消息加一个is_read布尔字段,但后来发现一个问题:如果A连续发了10条消息,B看了其中一条,其他9条也应该是已读状态。按条更新会导致更新次数非常多,而且逻辑容易乱。
最后借鉴了即时通讯里常见的做法:会话表里记录“最后已读消息id”。用户在A和B的聊天页里打开时,前端带上最后一条消息的id,后端把这个id写入会话的last_read_msg_id。查询未读消息时,只要判断消息id是否大于last_read_msg_id即可。这个方案更新次数少、查询效率高,而且从头做到尾没有出现数据不一致的情况。
public Integer unreadCount(Long sessionId, Long userId) { Session session = sessionMapper.selectById(sessionId); Long lastReadMsgId = session.getLastReadMsgId(); LambdaQueryWrapper<Message> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Message::getSessionId, sessionId); wrapper.ne(Message::getSenderId, userId); // 不算自己发的 wrapper.gt(Message::getId, lastReadMsgId); // 大于最后已读ID就是未读 return messageMapper.selectCount(wrapper); }4. 部署配置与性能优化
4.1 本地开发环境搭建
先列一下我本地开发时使用的软件环境,都是目前比较主流稳定的版本组合:
- JDK 1.8(也可以用JDK 17,看Spring Boot版本兼容性);
- Maven 3.8+
- MySQL 8.0
- Redis 6.x
- Node.js 16(前端部分,如果不做前端可忽略)
环境搭建本身不难,但很多同学会在连接MySQL时遇到时区问题。我先说明一个关键配置项,在application.yml里这样写:
spring: datasource: url: jdbc:mysql://localhost:3306/dating_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 redis: host: localhost port: 6379重点强调一下serverTimezone=Asia/Shanghai:如果不设置时区,操作数据库时日期数据经常会差8个小时,查出来明明中午12点存进去,读出来却变成了凌晨4点。characterEncoding=utf8mb4也不能漏,否则中文昵称和表情符号存入数据库会乱码。
4.2 文件上传配置与静态资源映射
用户头像上传是交友平台的必备功能,我一开始用了一个很笨的方案:把图片存到数据库的LONGBLOB字段里。结果一张头像的JSON返回就几十KB,接口响应慢得不行,还不利于前端直接通过URL展示。后来改成了经典的本地磁盘存储:
file: upload-path: /data/upload/后端接收上传的文件后,生成一个新的文件名(比如用UUID加后缀),保存到磁盘,然后把拼接好的访问URL存到数据库。为了让前端能通过URL访问到这个图片,需要配置一个静态资源映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath); }这里还有一个小细节:上传文件时要限制大小,否则别人上传一个1GB的视频文件,磁盘很快就满了。我在配置里把单个文件限制在5MB以内,图片压缩交给前端处理。
4.3 常见问题与排查实录
把整个项目从搭建到测试的过程中遇到的问题汇总一下,这些都是高频问题,基本每条都能对上号。
问题一:启动报“Table doesn't exist”。原因通常是对应数据库里根本没有建表,或者表名与实体类映射不一致。排查办法很简单:先看数据库里有没有表,再看实体类@TableName注解指定的表名是否一致。MyBatis-Plus默认会把驼峰命名的类名转成下划线表名,比如ChatMessage会去找chat_message表,如果创建表时用了chatmessage这种名字,就会报这个错。
问题二:接口能调通,但返回给前端的中文全部变成了问号。这是数据库连接字符串里的字符集配置不对。确保改成characterEncoding=utf8mb4后,还需要确认数据库本身是utf8mb4编码。在MySQL里执行show create table user\G直接看表定义,不是就必须改表编码。
问题三:WebSocket连接失败,前端一直报403。通常不是端口问题,而是WebSocket握手被拦截器拦住了。因为JWT拦截器默认拦截所有/api/**路径,而WebSocket端点是/ws/**,如果在拦截器配置里没有把/ws/**放行,连接就会被拦截返回403。方案是把WebSocket路径单独放行,并在WebSocket握手时通过token完成鉴权。
问题四:Redis连接不上导致登录接口直接抛异常。这个问题比较常见,很多同学把Redis当成可选项,其实只要依赖中引入了spring-boot-starter-data-redis,项目启动时就会自动尝试连接。如果本地没装Redis或服务没启动,接口就会报错。不妨把Redis的启动命令redis-server加到开发环境启动脚本里,或者干脆在本地安装一个服务。
5. 踩坑记录与实操心得
5.1 那些看起来简单却绕了很久的坑
第一个坑是缩略图导致的头像路径丢失。我在做前端页面时发现,用户上传头像后,访问图片经常404,但图片又确实存在。排查到最后才发现是上传时为了展示方便对图片做了压缩处理,压缩后的缩略图文件名和原图不一致,而前端拿到的还是原图URL。后来我把缩略图逻辑移除,统一用原图URL,问题就消失了。
第二个坑是聊天消息的重复推送。用WebSocket推送消息时,如果前端监听逻辑写得不严谨,同一个消息页面可能会显示两遍。原因是后端在消息入库后先走WebSocket推送了一次,前端刷新页面又重新拉取了消息列表,两个来源的数据没有去重。解决方式是前端把消息渲染和增量推送区分开:页面打开时拉一次完整历史,之后进来的全部走WebSocket增量追加,不重复拉列表。
第三个坑是跨天之后的“今日推荐”缓存没有刷新。用户反馈说推荐列表一直不变化,一开始以为是推荐规则写错了,后来发现是计算推荐结果之后被缓存到了Redis,缓存key是按天拼的,例如recommend:user:1001:20250117。但因为Redis的过期时间没设置好,第二天的数据还会沿用前一天的缓存。最后一刀切地改成了每天零点缓存过期,或者直接把缓存时间设为当日剩余秒数。
5.2 给毕业设计答辩的加分建议
项目做完之后的答辩阶段,很多同学只注重演示功能,忽略了展示设计思路,其实比较吃亏。我给几个实际验证过的建议:
- 准备一个“数据流讲解”:从用户打开App首页开始,依次说明推荐列表怎么拿到的、点击喜欢以后后端做了什么、匹配成功后通知从哪里来。答辩老师喜欢听这种有流程感的讲解,会觉得你真正理解了业务。
- 把数据库设计图打印出来,讲到匹配模块时指着user_like和match_record两张表说清“双向匹配是怎么实现的“,这个环节几乎都会得到好评。
- 准备好至少一个“真实踩坑复盘”:讲一个开发中遇到的bug,从原因、排查到解决完整描述,体现的是问题定位能力,这个比代码行数更有说服力。
5.3 后续扩展方向
这个项目的底子其实挺开放,后面想继续完善的话可以从三个方向加:
- 加一个“动态广场”:用户可以在平台发布图文内容,其他人可以点赞评论。这个功能本质上是内容设计的变形,复用现有的用户体系,做起来也不难。
- 加“系统通知”:比如互相喜欢的通知、新消息提醒,统一走消息中心。这个在现有WebSocket的架构上很容易扩展。
- 推荐策略升级:把基于规则的推荐改成加权融合,加入用户行为数据(浏览时长、点击次数),收益更高。
如果在实际使用中需要更多数据支撑,也可以给推荐逻辑加一点统计功能,看看用户更倾向于哪种匹配方式,再反过来调权重权重,效果会越来越接近商业产品的体验。
我个人做这个项目到后期的体会是:交友平台这种题目,听着偏互动偏娱乐,但技术上基本功考察一点不少,从多表查询、登录鉴权到实时通信都覆盖到了,很适合作为毕业设计练手。如果你正在做类似项目,别被眼花缭乱的功能绑架,先把账号、资料、喜欢、匹配、聊天这条主线走通,再考虑锦上添花的事。主线稳了,项目就已经成功了百分之八十。