前后端分离的实战项目,网上一抓一大把,但我见过太多人下载了源码却根本跑不起来,更别提部署上线了。今天分享的这个“志同道合交友网站系统”,是我花了不少业余时间打磨出来的一套完整作品,技术栈就是最主流的 SpringBoot + Vue + MyBatis + MySQL,没有复杂到劝退的框架,也没有从零教环境的注水内容,重点放在“业务怎么设计、代码怎么写、项目怎么部署”这三件事上。系统内核是兴趣标签驱动的交友匹配,通过用户给自己打的兴趣标签,计算彼此之间的契合度,把真正“志同道合”的人互相推荐。刚开始学前后端分离的同学,可以把这套系统当作教科书级的练手项目;已经准备找工作的朋友,也能从里面提炼出简历上拿得出手的技术亮点。接下来我从需求拆解开始,一路讲到线上部署踩过的坑。
1. 项目整体设计与核心思路
做这类偏应用型的系统,最怕一开始就埋头写代码。前后端分离不是说前端放一个 Vue 工程、后端放一个 SpringBoot 工程就算完事,关键在于职责边界、数据交互方式、接口约定和部署形态,这些在设计阶段就得定清楚。
1.1 交友系统的功能定位与用户价值
虽然叫“交友网站”,但这个系统并不是做成陌陌或者探探那种“附近的人”模式,核心差异化在“志同道合”四个字上。传统的社交软件推荐逻辑偏向地理位置和热度,而这里我们要解决的是兴趣连接的问题。
具体落到功能上,我划分了6个核心模块:
- 用户模块:注册、登录、个人信息管理、头像上传、密码修改。
- 兴趣标签模块:预设标签库 + 用户自选标签,用户最多选12个标签,作为匹配基础。
- 推荐匹配模块:根据标签重叠度给用户推荐“可能感兴趣的人”,支持按契合度排序。
- 互动模块:关注/取消关注、查看访客、点亮喜欢、好友申请与处理。
- 私信模块:支持好友之间的一对一实时聊天,用轮询实现(没有引入 WebSocket,别急,后面我会解释为什么)。
- 个人动态模块:发布文字心情、查看好友动态、给动态点赞。
对于学习用途来说,这套功能量级正好。太少显得单薄,太多像聊天室、直播那种又超出个人项目的能力范围,而且核心技术点重复度高。
1.2 为什么坚持前后端分离架构
从开发模式和部署形态两个维度看,前后端分离是这套系统的必选项,不是可选项。
| 项目对比维度 | 传统单体JSP项目 | 前后端分离项目 |
|---|---|---|
| 职责分工 | 前端页面和后端逻辑耦合在同一个工程里 | 前端只管界面交互,后端只提供JSON接口 |
| 开发效率 | 前端改样式需要重启应用 | 前后端可并行开发,使用Mock数据联调 |
| 部署方式 | 打成一个 WAR 包丢进 Tomcat | 前端静态资源由 Nginx 托管,后端独立部署 |
| 扩展能力 | 难以承接移动端、小程序等新端 | 同一套后端API可同时服务Web、App、小程序 |
SpringBoot 天然适合做后端 API 服务,内嵌 Tomcat 让部署非常干净;Vue 则负责把页面组件化,配合 Vue Router 做前端路由、Pinia 做状态管理,单页应用的体验和无刷新交互都很成熟。前后端之间通过 JSON 交换数据,接口就是唯一的契约,这也是我现在做任何项目都默认采用的方式。
1.3 技术选型背后的取舍逻辑
你是不是好奇,用人气更高的 MyBatis-Plus 不是更方便吗?为什么选 MyBatis?
坦白说,这个选择我是故意的。MyBatis-Plus 确实把单表 CRUD 简化到了极致,但这也成了很多人“只会用不会写”的根源。这个交友系统里有大量多表关联和动态 SQL 场景,比如标签匹配、好友列表带用户信息、动态联表查询作者信息,用原生 MyBatis 能把 XML 映射、动态 SQL、resultMap 关联映射这些核心能力吃透。面试时这些恰好是高频考点。
MySQL 8.0 作为唯一的存储层,承担用户数据、标签关系、互动记录、消息记录和动态数据。搞笑的是,很多人在环境准备阶段就栽了跟头,MySQL 安装配置反而不是应用代码的难点。后面部署章节我会专门讲。
2. 数据库设计:交友系统的地基工程
数据库设计是整套系统里我最看重的部分。交友系统业务看起来简单,但只要涉及到标签匹配、好友关系、消息记录这几个场景,表结构设计稍有不慎,后边查询会写得痛不欲生。
2.1 核心表结构与字段设计
整个系统一共6张核心表,外加1张标签字典表。数据库名我建议用soulmate_db,字符集选 utf8mb4,因为用户昵称和心情动态里可能出现 emoji,utf8mb4 才能完整存储。
用户表user:
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `gender` tinyint(4) DEFAULT '0' COMMENT '性别 0未知 1男 2女', `birthday` date DEFAULT NULL COMMENT '出生日期', `city` varchar(50) DEFAULT NULL COMMENT '所在城市', `signature` varchar(200) DEFAULT NULL COMMENT '个性签名', `status` tinyint(4) DEFAULT '1' COMMENT '账号状态 0禁用 1正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';USER_ID 一定要是雪花ID或者自增主键,这里我用自增是为了演示简单。但注意 username 必须加唯一索引。
标签字典表tag和用户标签关联表user_tag:
CREATE TABLE `tag` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL COMMENT '标签名称', `category` varchar(20) DEFAULT NULL COMMENT '标签分类:运动/音乐/阅读/旅行等', PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='兴趣标签字典表'; CREATE TABLE `user_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '用户ID', `tag_id` int(11) NOT NULL COMMENT '标签ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_tag_id` (`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户兴趣标签关联表';user_tag 就是经典的“用户—标签”多对多映射表,匹配推荐全靠这张表能查出来。
好友关系表user_friend、喜欢表user_like、私信表message和动态表post的设计也需要注意。好友关系表我用的是双行冗余设计,避免查“我关注了谁”和“谁关注了我”时都要做一次反向查询。
CREATE TABLE `user_friend` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '发起方用户ID', `friend_id` bigint(20) NOT NULL COMMENT '被关注用户ID', `status` tinyint(4) DEFAULT '0' COMMENT '状态 0待处理 1已通过 2已拒绝', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `handle_time` datetime DEFAULT NULL COMMENT '处理时间', PRIMARY KEY (`id`), KEY `idx_user_friend` (`user_id`, `friend_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友关注关系表';私信表message设计上需要存 sender_id、receiver_id、content、is_read、create_time,重点在查询未读消息数和会话列表的 SQL。动态表post需要冗余一个 author_id 和 content、image_url、like_count,方便列表页直接展示,不用每次联查 count。
2.2 兴趣匹配背后的数学逻辑
“志同道合”在系统里不是一个口号,它本质上是一个简化版的推荐算法。我采用的是基于标签集合的 Jaccard 相似度。
假设用户 A 的标签集合是 TA,用户 B 的标签集合是 TB,那他们的契合度就是:
similarity(A, B) = |TA ∩ TB| / |TA ∪ TB|也就是交集元素个数除以并集元素个数。两个人都有“跑步”和“摄影”标签,其中一个人还有“阅读”,那重叠度就是 2/3 ≈ 0.67,匹配度非常高了。
考虑到前端推荐列表要按契合度排序,我不能在内存里把所有用户两两算一遍。实际实现上,我先把当前用户的标签ID集合查出来,然后用 SQL 统计出至少有一个共同标签的候选用户,再在 Service 层精确计算 Jaccard 值,最后按值排序取前20个。数据量在十万级以内这种方案完全够用,比引入 Redis 做实时推荐少一大截复杂度。
2.3 数据库设计里我踩过的坑
三个回看时很重要的经验:
第一,字符串类型的枚举字段控制好长度。gender 用 tinyint,status 用 tinyint,不要真的在数据库里存“男”“女”这种字符串。Java 枚举转 JSON 输出时处理一下即可,数据库层保持干净的数值语义。
第二,所有联表查询字段都加了索引。最典型的是 user_tag 表的 user_id、tag_id 必须分别建索引,否则匹配推荐接口在数据量上来之后会慢得非常明显。早年我犯过只建联合索引、不建单列索引的错,结果某个查询用不上索引,排查半天。
第三,时间字段统一用 datetime,不要混合 timestamp。订单、消息、日志类表对时间精度要求其实没那么高,但不同 MySQL 版本对 timestamp 的时区处理不一致,很容易在前后端时间显示上出现8小时的偏移。
3. 后端核心实现:SpringBoot + MyBatis 的完整落地
后端工程我按标准的四层结构来拆分:Controller 层负责参数接收与响应封装,Service 层处理业务逻辑,Mapper 接口 + XML 负责数据访问,Entity 实体与数据库表对应。包名用com.soulmate.*,类命名没有奇技淫巧,清晰第一。
3.1 工程结构一览
soulmate-backend/ ├── src/main/java/com/soulmate/ │ ├── SoulmateApplication.java // SpringBoot 启动类 │ ├── config/ │ │ ├── CorsConfig.java // 跨域配置 │ │ ├── WebMvcConfig.java // 拦截器注册 │ │ └── JwtInterceptor.java // JWT 登录拦截器 │ ├── controller/ │ │ ├── UserController.java │ │ ├── MatchController.java │ │ ├── MessageController.java │ │ └── PostController.java │ ├── service/ │ │ ├── UserService.java │ │ ├── MatchService.java │ │ └── ... │ ├── mapper/ │ │ ├── UserMapper.java │ │ ├── UserTagMapper.java │ │ └── ... │ ├── entity/ │ │ ├── User.java │ │ ├── UserTag.java │ │ └── ... │ ├── common/ │ │ ├── Result.java // 统一响应体 │ │ └── BusinessException.java │ └── util/ │ └── JwtUtil.java └── src/main/resources/ ├── application.yml └── mapper/ ├── UserMapper.xml ├── UserTagMapper.xml └── ...这个结构没有把 controller/service 拆出无数个包,适合中小型项目,也让刚接触的人不容易迷路。
3.2 用户注册登录与 JWT 权限控制
用户密码一律用 BCrypt 加密存储,这是 Spring Security 内置的加密算法,比 MD5 安全得多。MD5 加盐虽然有改善,但 BCrypt 在暴力破解防护上明显更专业。我把 spring-security-crypto 单独引进来,只用了它的 BCryptPasswordEncoder,没有引入整套 Spring Security,避免过滤器链对接口造成额外干扰。
注册逻辑的核心代码:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); @Override public Result register(UserRegisterDTO dto) { // 1. 检查用户名是否存在 User exist = userMapper.selectByUsername(dto.getUsername()); if (exist != null) { return Result.error("用户名已存在"); } // 2. 构造用户对象,密码加密 User user = new User(); user.setUsername(dto.getUsername()); user.setPassword(encoder.encode(dto.getPassword())); user.setNickname(dto.getNickname()); // 3. 保存并返回 userMapper.insert(user); return Result.success(); } }登录成功之后,后端生成 JWT token 返回给前端。JWT 的好处是服务端无需保存会话状态,token 自带过期时间,非常适合前后端分离的部署结构。我的 JwtUtil 里把过期时间设置为 24 小时,密钥放在 application.yml 中通过 @Value 注入,自然不会在代码里写死。
拦截器里只拦截/api/**下需要登录的接口,对/api/user/login、/api/user/register放行。token 校验失败时直接返回 401,前端收到 401 就跳到登录页。
3.3 匹配推荐模块的核心实现
这是全系统灵魂所在。我的实现分三步:查候选用户、算相似度、排序取前 N。
第一步,查出至少有一个共同标签的候选用户:
<!-- UserTagMapper.xml --> <select id="selectCandidateUserIds" resultType="java.lang.Long"> SELECT DISTINCT ut1.user_id FROM user_tag ut1 JOIN user_tag ut2 ON ut1.tag_id = ut2.tag_id WHERE ut2.user_id = #{currentUserId} AND ut1.user_id != #{currentUserId} </select>第二步,在 Service 里计算每个候选用户的 Jaccard 相似度:
public List<MatchUserVO> matchUsers(Long currentUserId, int limit) { // 1. 当前用户的标签集合 Set<Long> myTagIds = userTagMapper.selectTagIdsByUserId(currentUserId); if (myTagIds.isEmpty()) { return Collections.emptyList(); } // 2. 候选用户ID List<Long> candidateIds = userTagMapper.selectCandidateUserIds(currentUserId); if (candidateIds.isEmpty()) { return Collections.emptyList(); } // 3. 批量查候选用户的标签集合 List<MatchUserVO> resultList = new ArrayList<>(); for (Long candidateId : candidateIds) { Set<Long> candidateTags = userTagMapper.selectTagIdsByUserId(candidateId); // 交集和并集计算 Jaccard 相似度 Set<Long> union = new HashSet<>(myTagIds); union.addAll(candidateTags); Set<Long> intersection = new HashSet<>(myTagIds); intersection.retainAll(candidateTags); double score = (double) intersection.size() / union.size(); MatchUserVO vo = new MatchUserVO(); vo.setUserId(candidateId); vo.setMatchScore(score); resultList.add(vo); } // 4. 按契合度降序排序,取前N个 resultList.sort((a, b) -> Double.compare(b.getMatchScore(), a.getMatchScore())); return resultList.stream().limit(limit).collect(Collectors.toList()); }这段代码信息量不小。候选人先通过 SQL 粗筛,缩小范围,然后逐个精确计算,排序取前 20,整体性能在个人项目场景下毫无压力。
3.4 接口设计与统一返回格式约定
所有接口统一返回 JSON:
{ "code": 200, "message": "success", "data": { } }Result 类是一个泛型对象,code 为 200 表示成功,401 表示未登录,500 表示业务失败。这样约定以后,前端 Axios 拦截器只需要判断 code 字段,不需要对 HTTP 状态码做各种特判。
这里补充一个大多数人忽略的点:跨域配置。SpringBoot 后端如果直接让 Vue 的开发服务器代理转发,其实不需要额外处理 CORS,我开发阶段用 Vite 的 proxy 来转发/api请求,生产环境用 Nginx 反向代理,都不会产生跨域问题。但如果前后端分离部署在不同的域名下,就必须在 CorsConfig 里配置allowedOrigins。
4. 前端实战:Vue 3 + Vite + Pinia 的组件化实现
前端工程我用 Vue 3 + Vite 构建,在开发体验上比 Vue CLI 明显轻快。Vite 的依赖预构建和热更新速度,用过的都知道,这里不再安利,直接讲工程结构和关键实现。
4.1 前端工程结构与路由设计
页面划分为:登录/注册页、首页推荐卡片流、用户详情页、标签选择页、私信会话列表页、聊天窗、个人中心、动态发布与浏览页。
// router/index.js const routes = [ { path: '/', component: HomePage, meta: { requiresAuth: true } }, { path: '/login', component: LoginPage }, { path: '/register', component: RegisterPage }, { path: '/tags', component: TagSelectPage, meta: { requiresAuth: true } }, { path: '/messages', component: MessageListPage, meta: { requiresAuth: true } }, { path: '/chat/:friendId', component: ChatPage, meta: { requiresAuth: true } }, { path: '/profile', component: ProfilePage, meta: { requiresAuth: true } }, { path: '/posts', component: PostPage, meta: { requiresAuth: true } } ];路由守卫的作用是拦截未登录用户。我在beforeEach里检查 localStorage 是否存在 token,不存在就跳/login,存在就放行。这个逻辑花了五分钟写完,但避免了每个页面都做重复的登录判断。
4.2 首页推荐卡片流与标签选择器
首页是整站的门面。推荐卡片流我用了纯 CSS 的 flex 布局 + 卡片组件,每一张卡片展示用户头像、昵称、城市、共同标签和契合度百分比。契合度低于 30% 的卡片,在视觉上置灰展示,用户一看就知道匹配度高的在顶部。
标签选择器是这个系统的关键交互组件。标签库按分类分组展示,用户点击切换选中状态,上限12个。如果超过12个,给一个轻提示。选择结束后保存到后端,后端返回推荐列表时就能直接使用。
这里有个设计细节:标签选择页在首次登录后强制跳转一次,用户没选完标签不允许进入推荐页。目的是保证系统冷启动阶段所有用户都有标签数据,不至于出现匹配接口查不到候选人的情况。
4.3 Axios 封装与接口联调
Axios 封装是前端工程的重头戏。我把它拆成三层:请求实例配置、请求拦截器、响应拦截器。
// api/request.js import axios from 'axios'; import { ElMessage } from 'element-plus'; const request = axios.create({ baseURL: '/api', timeout: 10000 }); // 请求拦截器:自动携带 token request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 响应拦截器:统一处理业务错误 request.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.message); if (res.code === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(new Error(res.message)); } return res.data; }, error => { ElMessage.error('网络异常,请稍后重试'); return Promise.reject(error); } );baseURL 设成/api之后,开发阶段 Vite 的 proxy 可以把/api开头的请求转发到http://localhost:8080,生产阶段 Nginx 也可以把/api反向代理到后端服务。这套设计让我在本地和服务器上运行项目时,前端代码一个字都不用改。
5. 部署上线:从本地到云服务器的完整链路
整套系统最关键也最容易出问题的是部署环节。我真见过有人代码写得很漂亮,结果卡在 Nginx 配置上,白白浪费了很多时间。部署方案我用的是最通用、成本最低的方式:后端 SpringBoot jar 包部署在服务器上,前端 Vue 打包成静态文件交给 Nginx 托管,Nginx 反向代理/api接口给后端。
5.1 本地构建与打包
后端打包前需要在 application.yml 里把数据库连接改成服务器的内网或公网地址:
server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://your-server-ip:3306/soulmate_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: your-password driver-class-name: com.mysql.cj.jdbc.Driver注意 MySQL 8.0 必须带serverTimezone参数,否则数据库连接会报时区错误。这行配置在本地和服务器上我都踩过坑。
执行打包:
mvn clean package -DskipTests打包完成后,target 目录下会生成soulmate-0.0.1-SNAPSHOT.jar。这个 jar 内置了 Tomcat,不用再装额外容器,直接java -jar就能启动。
前端打包:
npm install npm run build构建完成后会在 dist 目录生成静态文件,下一步把整个 dist 目录上传到服务器即可。
5.2 服务器环境准备
服务器我用的 CentOS 7 系统,如果是从零开始,需要依次装好 JDK、MySQL、Nginx。
JDK 安装最省事的方式:
yum install -y java-1.8.0-openjdk如果不想用系统自带的 JDK,也可以下载 JDK 8 或 11 的 tar 包解压后配置环境变量。注意 SpringBoot 2.5 以下版本对高版本 JDK 支持不太好,SpringBoot 2.7 可以配 JDK 8 或 11,这个组合非常稳。
MySQL 8.0 安装一定要看官方仓库的方式,别用系统自带的 mariadb 替代,否则容易出现驱动不兼容:
# 下载官方 MySQL rpm 仓库 wget https://repo.mysql.com//mysql80-community-release-el7-3.noarch.rpm rpm -ivh mysql80-community-release-el7-3.noarch.rpm yum install -y mysql-server systemctl start mysqld初次安装后,MySQL 会生成一个临时 root 密码,在日志里找:
grep 'temporary password' /var/log/mysqld.log用临时密码登录后,必须立刻修改密码,然后创建数据库,导入我提供的 init.sql 即可。这里提醒一句,MySQL 8.0 默认的密码校验策略比较严格,如果密码太简单(比如只含数字),会提示不符合策略。可以在 MySQL 里修改 validate_password 策略,但不建议在生产环境这样做。
Nginx 安装:
yum install -y nginx启动后,访问服务器 IP 看到默认页面,说明 Nginx 正常工作。
5.3 Nginx 反向代理配置全解析
下面是我线上正在用的 nginx 配置,注释都写好了,可以直接照着改。
server { listen 80; server_name your-domain.com; # 改成自己的域名或服务器IP # 前端静态资源 root /usr/share/nginx/dist; index index.html; # 解决 Vue Router history 模式刷新404的问题 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1: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 /upload/ { alias /usr/share/nginx/upload/; } # 静态资源缓存,提升加载速度 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 7d; add_header Cache-Control "public, no-transform"; } }nginx 配置里最容易踩坑的是try_files $uri $uri/ /index.html这一行。如果前端用了 Vue Router 的 history 模式,刷新非根路径页面(比如/profile)时会向 Nginx 请求这个路径,找不到真实文件就会出现 404。加上这行,请求会回退到 index.html,由前端路由接管,问题就解决了。
5.4 部署过程常见问题排查速查表
这个表我整理自真实排障经验,几乎覆盖了常见的部署事故。
| 问题现象 | 排查方向 | 解决方案 |
|---|---|---|
| 前端页面能打开,接口 404 | 看 Nginx location 匹配规则 | 确认接口路径以 /api 开头且 proxy_pass 转发地址正确 |
| 后端启动报端口被占用 | 检查8080端口 | netstat -tlnp | grep 8080找到PID,kill 后重启 |
| 数据库连接拒绝 | 检查 MySQL 状态和端口 | systemctl status mysqld,确认3306端口监听,检查防火墙 |
| 前端页面白屏 | 看浏览器 Console 报错 | 确认 dist 文件是否正确上传,Nginx root 路径是否指向 dist 目录 |
| 上传头像失败 | 看 Nginx 错误日志 | 检查 upload 目录的读写权限,路径是否挂载正确 |
| 页面刷新后 404 | 前端路由模式与 Nginx 配置冲突 | 确认 try_files 配置存在且写法正确 |
| 接口返回 401 后无限跳转登录页 | 检查 token 过期和 JWT 密钥 | 确认前后端密钥一致,过期时间合理,Axios 拦截器不要重复跳转 |
部署完整流程走通之后,整套系统在云服务器上跑起来,那一刻你会发现,之前在本地改代码、调样式、捋接口的每一分钟,都没白费。
6. 我对这套系统的复盘与扩展建议
回顾整个开发过程,我最想强调的一点是:任何看起来“高大上”的系统,落到底层就是数据的增删改查加合理的业务逻辑。交友网站的亮点在于“志同道合”这个匹配场景,而技术上真正考验人的,是能不能把标签体系、关系链和消息模块的边界理顺。
这个项目后续扩展空间非常大。比如把私信的轮询机制升级成 WebSocket 或 SSE,聊天体验会有一个质的飞跃;再比如引入 Redis 做推荐结果的缓存,不用每次请求都重算 Jaccard 相似度;还可以在部署上换成 Docker Compose,一套命令同时拉起 MySQL、后端、Nginx,排障和维护成本进一步降低。
我个人踩过几次坑之后最大的体会是:部署环境永远比代码本身更容易出意外。MySQL 8.0 的时区、Nginx 的 try_files、防火墙端口、jar 包启动参数,任何一个环节出问题都能卡一整个晚上。所以如果这个项目对你的价值是“跑通+部署”,我强烈建议你在本地把 Docker 环境、传统环境两种部署方式各走一遍,体验过完整的链路之后,你再遇到线上环境问题,心里就有底了。最后再分享一个小技巧:写完接口之后,在浏览器里用 Postman 或 Apifox 把每个接口的请求和响应格式记录下来,再跟前端联调。接口文档就是你的设计依据,也是项目做完之后最有说服力的交付物。这套方法论,比代码本身值钱得多。