简介:一套基于Springboot+Vue的志同道合交友网站毕业设计项目,包含完整前后端源码、数据库脚本、使用文档和演示视频,面向需要完成毕设或期末项目的计算机专业学生,可用于选题参考、功能借鉴和答辩准备。项目已通过导师审核,答辩评分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_user | id, username, password, nickname, gender, birthday, city, avatar, bio, last_active_time | 保存账号与基础画像 |
| t_tag | id, tag_name, tag_category | 兴趣标签字典,例如“健身”“摄影” |
| t_user_tag | id, user_id, tag_id | 用户与兴趣标签的多对多关联 |
| t_like_record | id, 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(Set<Long> userTags, Set<Long> targetTags) { if (userTags.isEmpty() || targetTags.isEmpty()) { return 0.0; } Set<Long> union = new HashSet<>(userTags); union.addAll(targetTags); Set<Long> 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 Result<Page<CandidateVO>> candidates(@Valid CandidateQuery query) { Page<CandidateVO> 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必须给默认值,前端传进来异常数据时接口依然能返回正常结果。统一返回Result<T>的原因是为了让前端只做一次错误拦截,而不是为每个接口写一遍状态码判断。
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 vite@latest soulmate-front -- --template vue cd soulmate-front npm install npm install vue-router@4 axios element-plus pinia npm run dev命令执行完毕后就是一个可运行的 Vue 3 工程。选择 Vue Router 4 是因为它和 Vue 3 的响应式模型完全匹配。这里有一个细节值得注意:用户详情页的地址需要携带目标用户 ID,我会把它设计成路由参数而不是 query:
const 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(Map<Long, Double> userWeight, Map<Long, Double> targetWeight) { Set<Long> 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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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 日志,这时候在视频里指出来,评委能明确看到前后端之间的真实联动。视频里出现的数据库密码、环境变量和本地路径要提前打码或改成虚拟地址,不要为了录视频而把本机真实配置暴露进画面。
本文还有配套的精品资源,点击获取