news 2026/10/20 4:38:41

基于Spring Boot的交友平台毕设实战:从数据库设计到WebSocket聊天

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Spring Boot的交友平台毕设实战:从数据库设计到WebSocket聊天

“基于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的架构上很容易扩展。
  • 推荐策略升级:把基于规则的推荐改成加权融合,加入用户行为数据(浏览时长、点击次数),收益更高。

如果在实际使用中需要更多数据支撑,也可以给推荐逻辑加一点统计功能,看看用户更倾向于哪种匹配方式,再反过来调权重权重,效果会越来越接近商业产品的体验。

我个人做这个项目到后期的体会是:交友平台这种题目,听着偏互动偏娱乐,但技术上基本功考察一点不少,从多表查询、登录鉴权到实时通信都覆盖到了,很适合作为毕业设计练手。如果你正在做类似项目,别被眼花缭乱的功能绑架,先把账号、资料、喜欢、匹配、聊天这条主线走通,再考虑锦上添花的事。主线稳了,项目就已经成功了百分之八十。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 4:38:36

AI Agent 时代的生物计算:AlphaFold 3 MCP Server 搭建与本地部署

本文收录于专栏 AI Agent智能体系列 —— 专栏系统覆盖 AI Agent 记忆、工具、插件与实战&#xff0c;点击订阅可跟踪后续更新。 自建 af3-mcp&#xff0c;通过Agent调用AlphaFold3。这篇把过程摊开&#xff1a;怎么从一个能启动的 server 走到 4 步链路全打通、能读AlphaFold3…

作者头像 李华
网站建设 2026/10/20 4:38:29

sklearn Pipeline 实战:从数据泄露到自动化建模流水线

1. 从一段“意大利面条式”的建模代码说起如果你写过一段时间的机器学习代码&#xff0c;大概率见过或者亲手写过下面这种“流水账”&#xff1a;先读数据&#xff0c;然后手动填充缺失值&#xff0c;接着对类别特征做独热编码&#xff0c;再标准化数值特征&#xff0c;最后把处…

作者头像 李华
网站建设 2026/10/20 4:38:12

Java图书管理系统源码实战:从能跑到能改的完整项目

简介&#xff1a;这份Java图书管理系统源代码面向Java初学者与课程设计开发者&#xff0c;提供一套可直接参考的图书馆业务实现方案&#xff0c;涵盖图书录入、查询、借阅、归还、续借及状态跟踪等核心流程&#xff0c;适合用于毕业设计、实训项目或框架学习。资源包共212个文件…

作者头像 李华
网站建设 2026/10/20 4:37:49

LiteLLM:生产级LLM API协议转换与路由中间件

1. 项目概述&#xff1a;一个被严重低估的API抽象层&#xff0c;不是“另一个LLM框架”“litellm”这三个字最近在开发者群、技术论坛和内部分享里出现频率陡增&#xff0c;但很多人第一次看到时下意识会想&#xff1a;“又一个大模型调用库&#xff1f;是不是跟llama-cpp-pyth…

作者头像 李华
网站建设 2026/10/20 4:36:50

小米商城HTML实战:语义化结构与响应式骨架设计

1. 项目概述&#xff1a;为什么从“小米商城网页”切入HTML实战做前端开发的同行应该都经历过这个阶段&#xff1a;学完HTML基础标签&#xff0c;打开编辑器写了个带标题、段落和图片的页面&#xff0c;自我感觉良好&#xff0c;结果一打开浏览器——布局错乱、文字堆叠、按钮点…

作者头像 李华
网站建设 2026/10/20 4:36:35

使用JFMRFVU3P多通道同步相位测试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华