ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Java毕业设计:Spring Boot+Vue志同道合交友网站匹配算法与实现

Java毕业设计:Spring Boot+Vue志同道合交友网站匹配算法与实现 简介一套基于SpringbootVue的志同道合交友网站毕业设计项目包含完整前后端源码、数据库脚本、使用文档和演示视频面向需要完成毕设或期末项目的计算机专业学生可用于选题参考、功能借鉴和答辩准备。项目已通过导师审核答辩评分97分并在Windows10/11环境完成调试部署教程齐全下载后按脚本即可运行便于快速上手和二次开发。资源包共781个文件压缩后约37.21MB主体是Java后端与Vue前端代码JavaScript、CSS、SVG/PNG等负责页面表现SQL脚本用于初始化数据库Bat脚本提供一键部署另有图片、GIF动图、配置文件及Maven依赖管理文件结构清晰易检索。目前已有102人学习下载。除了直接运行之外通过阅读源码还能掌握前后端分离项目中的模块划分、接口联调、页面交互和环境配置方法对提升全栈开发能力也有帮助。1. 志同道合交友网站在毕业设计里到底该做成什么“志同道合交友网站”这类 Java 毕业设计拿到手之后第一反应往往是“做一个带聊天室的社交系统”但如果真按这个思路去写很容易掉进即时通讯、在线状态维护的深坑里。这个题目的评分重点其实是基于 Spring Boot Vue 的前后端分离完整度后端把“用户画像、标签、匹配、喜欢”做成可计算的接口前端把推荐列表和交互状态呈现出来再把数据库设计和使用文档补齐。它适合已经有 Java 基础和一点前端经验、想在一个月内跑通完整链路的同学。我把这套工程从表结构、匹配算法、接口参数到前端联调再到交付时的演示视频脚本按一线开发的做法拆开讲照着做就能答上“为什么这么设计”。2. Spring Boot 后端数据库表、匹配算法与接口参数2.1 先把用户画像拆成表用户、标签、点赞关系“志同道合”落到工程里不是一句口号而是一组数据库字段。设计表结构时我的做法是先拆四张核心表用户表、标签字典表、用户标签关联表、点赞记录表。如果还要展示“互相喜欢”和私信入口再加一张消息表就够了。毕业设计最忌讳一上来拆十几张表评委更看重每一张表都有存在意义、每一个关联字段都在查询里被用到。下面这张表结构可以直接作为数据库课程设计的初始版本表名字段要点用途t_userid, username, password, nickname, gender, birthday, city, avatar, bio, last_active_time保存账号与基础画像t_tagid, tag_name, tag_category兴趣标签字典例如“健身”“摄影”t_user_tagid, user_id, tag_id用户与兴趣标签的多对多关联t_like_recordid, from_user_id, to_user_id, status, create_time记录喜欢、跳过和互相喜欢状态使用 MyBatis-Plus 时实体类的写法最直接TableName、TableId注解能把实体和表正确映射这套写法在 Spring Boot 2.7.x 和 3.x 中都能正常使用Data TableName(t_user) public class User { TableId(type IdType.AUTO) private Long id; private String username; private String password; private String nickname; private String gender; private LocalDate birthday; private String city; private String avatar; private String bio; private LocalDateTime lastActiveTime; }代码里IdType.AUTO表示主键由数据库自增开发阶段最省事。如果项目改成 JPA只需要把TableName换成Entity(name t_user)业务逻辑不受影响。真正建议你留意的字段是last_active_time推荐排序时“最近是否活跃”是重要加权因子没有它连“过滤掉 90 天未登录用户”这种基础查询都写不干净。t_user_tag是匹配逻辑的命脉查询时先用它取出当前用户的标签 ID 集合再和候选用户的标签集合做相似度计算。标签不能设计成用户自由填写的字符串否则“健身”和“运动健身”会变成两个标签数据一致性很难维护。常见做法是初始化脚本里预置一批标签注册时让用户从中勾选。提示数据库脚本里给 t_tag 预置“音乐、电竞、户外、读书、摄影、美食”等标签后端算相似度时标签 ID 是连续可枚举的查询条件也更好写。2.2 匹配分数怎么算从 Jaccard 到加权评分“志同道合”在工程上最简单的落地算法是 Jaccard 相似度也就是两个用户选择标签集合的交集大小除以并集大小。这个公式虽然基础但能清楚解释“为什么这两个人排在前面”答辩时不会卡壳。public double jaccardSimilarity(SetLong userTags, SetLong targetTags) { if (userTags.isEmpty() || targetTags.isEmpty()) { return 0.0; } SetLong union new HashSet(userTags); union.addAll(targetTags); SetLong intersection new HashSet(userTags); intersection.retainAll(targetTags); return (double) intersection.size() / union.size(); }这段代码的执行顺序是先复制集合算并集再复制一次算交集。注意retainAll会修改原集合所以交集必须基于new HashSet(userTags)计算否则会把当前用户的标签集合弄丢。一个人没选任何标签时直接返回 0.0避免除零异常。实际推荐不能只看标签重合度我一般会把年龄、活跃度一起加进来形成一个加权公式score 0.5 * tagSimilarity 0.3 * ageSimilarity 0.2 * activityScore年龄相似度的计算不要做成“差几岁就扣几分”的严格线性函数可以设置一个容差区间年龄差小于等于 3 岁时取 1.0每超过 1 年减 0.1最低不低于 0。活跃度则可以按last_active_time换算成 0 到 1 的小数30 天内活跃为 1.0之后每天衰减 0.01。这三个权重是面试时必被追问的细节所以要把计算逻辑放到独立的MatchStrategy类里不要散落在UserService各处。2.3 Controller 参数与分页对外暴露什么才够用推荐页面的核心接口是“获取候选人列表”。它至少需要这五个参数当前用户 ID、页码、每页条数、性别偏好、城市过滤。写 Controller 的时候参数校验要放在 DTO 里不要用一堆 if 堆在方法内。GetMapping(/candidates) public ResultPageCandidateVO candidates(Valid CandidateQuery query) { PageCandidateVO page userService.recommend(query); return Result.ok(page); }Data public class CandidateQuery { NotNull private Long userId; Min(value 1, message 页码不能小于1) private Integer pageNo 1; Min(value 5, message 每页不能少于5条) Max(value 50, message 每页不能超过50条) private Integer pageSize 10; private String gender; private String city; }Valid配合Min、Max可以拦截大部分非法输入。pageNo和pageSize必须给默认值前端传进来异常数据时接口依然能返回正常结果。统一返回ResultT的原因是为了让前端只做一次错误拦截而不是为每个接口写一遍状态码判断。Service 里的分页逻辑用 MyBatis-Plus 的Page对象和LambdaQueryWrapper组合实现先按性别和城市过滤再在内存里计算匹配分并排序。按毕业设计的数据量级这种“先过滤再内存排序”的做法完全够用。如果你更倾向写原生 SQL必须注意LIMIT偏移量计算页码从 1 开始offset (pageNo - 1) * pageSize这是最容易出错的地方。3. Vue 前端路由、Axios 状态管理与匹配页联动3.1 Vue 安装、依赖准备和路由参数传递前端推荐走 Vite Vue 3 Vue Router 4 Pinia 这条链路。执行下面的命令前先确认本机 Node.js 版本不低于 16避免 vue 安装依赖时出现兼容性报错。npm create vitelatest soulmate-front -- --template vue cd soulmate-front npm install npm install vue-router4 axios element-plus pinia npm run dev命令执行完毕后就是一个可运行的 Vue 3 工程。选择 Vue Router 4 是因为它和 Vue 3 的响应式模型完全匹配。这里有一个细节值得注意用户详情页的地址需要携带目标用户 ID我会把它设计成路由参数而不是 queryconst routes [ { path: /, component: Home }, { path: /match, component: MatchList }, { path: /profile/:userId, component: UserProfile, props: true } ]:userId是动态路由参数props: true的作用是把userId作为属性传给组件。这样在UserProfile.vue里直接使用defineProps([userId])即可拿到参数比this.$route.query更规范也方便组件复用。3.2 获取候选人列表Axios 封装与接口联调页面里的每一个请求都要经过统一封装不能直接在组件里散写axios.get。封装的核心是baseURL、超时时间和两个拦截器import axios from axios const http axios.create({ baseURL: /api, timeout: 10000 }) http.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) http.interceptors.response.use( res res.data, err Promise.reject(err) ) export default http请求拦截器统一注入 token响应拦截器直接把data返回业务代码里就不需要再写response.data.data这种重复结构。候选人接口的请求方法单独放到api/match.js中import http from /utils/http export function fetchCandidates(params) { return http.get(/candidates, { params }) }联调阶段最常见的坑是跨域。开发环境不要在 Spring Boot 里把 CORS 全部放开较好的做法是在 Vite 的vite.config.js里配置代理让页面把请求发给同源地址再由 Vite 转发到后端。export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })提示代理配置里rewrite会去掉/api前缀。所以 Spring Boot 接口不要加/api开始路径否则会出现 404。更稳妥的约定是后端保持/candidates前端代理统一加前缀。3.3 喜欢、跳过操作Pinia 状态管理与列表更新匹配列表页的交互通常是“喜欢”“跳过”两个按钮。每次操作后当前这张卡片要从候选列表移除同时要把已处理的用户 ID 记录下来防止后一次推荐重复出现。这里的状态如果只放在组件的ref里一旦切到详情页再返回状态就丢失了所以我用 Pinia 来统一管理。import { defineStore } from pinia export const useMatchStore defineStore(match, { state: () ({ candidates: [], likedIds: [], skippedIds: [], matchedCount: 0 }), actions: { async loadCandidates(params) { const data await fetchCandidates(params) this.candidates data.records }, async like(userId) { this.candidates this.candidates.filter(item item.id ! userId) this.likedIds.push(userId) }, async skip(userId) { this.candidates this.candidates.filter(item item.id ! userId) this.skippedIds.push(userId) } } })filter的目的是让前端状态立即变化不需要再发一次/candidates请求。likedIds和skippedIds单独维护是为了在后端返回“互相喜欢”时快速判断当前用户是否已经出现在对方的喜欢列表里。每次操作后还要把userIds通过localStorage备份一份防止刷新页面后重复操作同一个人。4. 把匹配调得更准余弦相似度、SQL 过滤与配置安全4.1 余弦相似度和标签权重的落地计算Jaccard 对“标签集合重合比例”很敏感但它没有区分标签的重要性。比如用户 A 选了 {健身, 阅读}用户 B 选了 {健身, 阅读, 游戏, 追剧}Jaccard 计算出来只有 0.33但两个人共同喜欢的核心标签已经在其中。换成余弦相似度会更合理它可以结合“主动选择”和“批量勾选”的权重差异。public double cosineSimilarity(MapLong, Double userWeight, MapLong, Double targetWeight) { SetLong allTags new HashSet(userWeight.keySet()); allTags.addAll(targetWeight.keySet()); double dot 0.0; double normA 0.0; double normB 0.0; for (Long tagId : allTags) { double a userWeight.getOrDefault(tagId, 0.0); double b targetWeight.getOrDefault(tagId, 0.0); dot a * b; normA a * a; normB b * b; } if (normA 0.0 || normB 0.0) { return 0.0; } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); }代码中getOrDefault很关键某一方没有这个标签时权重按 0 处理不会抛空指针。权重从哪里来注册时手动选择的标签我给 1.0批量推荐标签给 0.6。这个差异解决了“标签是用户主动点出来的还是系统默认塞给他的”这一问题。4.2 带标签过滤的 SQL 与分页优化当推荐规则变成“找出和我至少有 3 个共同标签的用户”时用 QueryWrapper 拼条件会很别扭直接写原生 SQL 更清晰SELECT u.id, u.nickname, u.city, COUNT(ut.tag_id) AS common_tag_count FROM t_user u JOIN t_user_tag ut ON ut.user_id u.id WHERE ut.tag_id IN (1, 3, 5, 8, 12) AND u.id ! #{userId} GROUP BY u.id, u.nickname, u.city HAVING common_tag_count 3 ORDER BY common_tag_count DESC LIMIT #{offset}, #{pageSize}这条 SQL 的执行顺序是先用IN把用户范围缩小到“拥有这些标签之一”的人GROUP BY聚合出共同标签数再用HAVING过滤掉重合数不足 3 的用户。注意AND u.id ! #{userId}是必须的否则自己会出现在自己的推荐列表里。LIMIT的 offset 在数据量超过万行后会有性能退化到时可以把分页改成“基于上一页最后一条 ID”的键值翻页。这条 SQL 本身也可以作为数据库课程设计里“复杂查询”部分的展示内容。4.3 Spring Boot 版本选择与配置文件加密答辩时被问到“项目用什么版本”的概率很高。Spring Boot 2.7.x 是比较稳妥的选择依赖生态兼容性好如果非要用 3.x必须把javax.*换成jakarta.*MyBatis-Plus 也要升级到 3.5.3 以上。用 IDEA 创建 Spring Boot 项目时默认会生成一个完整的 Maven 目录结构但要注意maven-wrapper.properties里的镜像地址改成国内镜像后第一次构建会快很多。另一个高分细节是敏感配置加密。application.yml里的数据库密码如果明文写在源码包里演示视频一录就全部暴露。常见做法是引入 Jasypt把密码替换成加密串spring: datasource: url: jdbc:mysql://localhost:3306/soulmate?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ENC(k5VmfG3NnOe1r0fJlE99a1hK0Q) jasypt: encryptor: algorithm: PBEWithMD5AndDES password: ${JASYPT_SECRET}ENC(...)里的值不是明文密码运行时通过环境变量JASYPT_SECRET注入解密密钥。这样源码包就算被传阅数据库密码也不会泄露。需要注意的是演示时要保留生成密文的命令行工具参数否则评委现场让你生成新密文时会答不上来。5. 高分交付的核心验证链路、文档脚本与演示视频5.1 端到端功能验证清单演示前按“注册 → 补全标签 → 查看推荐 → 点喜欢 → 触发互相喜欢 → 查看消息”这条链路完整走一遍。最容易出错的三个点第一注册时勾选的标签在刷新页面后必须保留第二点击喜欢后推荐列表不能再次出现同一个候选人第三互相喜欢的弹窗只出现一次。检查时打开浏览器 DevTools 的 Network 面板确认每个请求的状态码和耗时。点击喜欢后如果返回 4xx绝大多数情况是请求头 token 没带全或者userId参数类型传成了字符串前端调用接口前要对参数做一次Number()转换。5.2 数据库初始化脚本与使用文档的写作重点数据库脚本建议拆成schema.sql和data.sql两份一份建表一份灌基础标签数据。脚本要保证重复执行不报错data.sql里的插入语句可以用INSERT IGNORE或者ON DUPLICATE KEY UPDATE保证幂等。使用文档不要从“打开 IDEA”开始写按“环境要求 → 导入数据库 → 修改 application.yml → 启动后端 → 启动前端 → 访问地址”的顺序来每一步附上完整命令。文档的目的不是给导师看而是让演示现场换一台电脑也能在 10 分钟内把项目跑起来。5.3 演示视频的时间安排与录制技巧演示视频总时长控制在 6 到 8 分钟开场 30 秒展示项目结构和启动过程中间 2 分钟演示注册登录2 分钟演示推荐列表和标签筛选90 秒做互相喜欢和消息联动最后留 1 分钟展示数据库表和关键 SQL 日志。录制时不要把后端控制台隐藏当推荐接口返回数据时控制台会打出一条带HAVING common_tag_count的 SQL 日志这时候在视频里指出来评委能明确看到前后端之间的真实联动。视频里出现的数据库密码、环境变量和本地路径要提前打码或改成虚拟地址不要为了录视频而把本机真实配置暴露进画面。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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