
简介基于JAVASpringBootMySQL微信小程序的高校校园交友小程序毕业设计项目面向计算机专业毕业生及需要完成课程设计的学生。系统提供完整源码、数据库脚本与论文文档前后端代码齐全覆盖用户登录、个人主页、匹配交友、消息管理等核心功能可直接导入开发环境运行。资源包共696个文件压缩后15.97MB包含77个Java后端文件、113个Vue后台管理页面、24个WXML小程序页面以及JSON/WXSS/JS等前端配置另有SQL数据库脚本和项目说明文档便于按模块查阅。项目基于Maven构建推荐使用IDEA与微信开发者工具配合Navicat进行调试部署。目前已有41人学习下载。这套资料适合作为毕设项目模板也可作为学习SpringBoot微信小程序前后端分离开发的完整范例。附带的论文和数据库设计能帮助理解业务逻辑经过严格调试确保开箱即用具有较高的参考和复用价值。1. 开学第三周交友小程序的服务器为什么总是崩每年九月新生入学后的第三周是这类校园交友小程序最刺激的时候。不是功能不够炫而是几百人同时刷动态、打招呼、匹配MySQL 的 CPU 直接飙到 100%写代码的人手机里全是报警群的消息。这个基于 JAVA SpringBoot MySQL 微信小程序的高校校园交友项目之所以被称为高分毕设项目不是因为它用了多冷门的技术而是它把一套标准的学生认证、LBS 匹配、动态发布、好友聊天全流程用最常见的框架落到了实处。拆开看这就是一个典型的「老技术解决新场景」的案例。微信小程序承担 C 端展示和交互SpringBoot 提供 API 和业务规则的执行MySQL 存储所有结构化数据。所谓交友在这里更多是一个复杂的状态管理和匹配算法问题而不是一个 UI 问题。有人追求左滑右滑的流畅感有人看重「同专业」「同楼栋」带来的安全感这两类需求背后的数据结构和查询逻辑完全不同。作为毕设它够完整作为工程它还有不少可以打磨的细节。下面从数据设计、匹配引擎、状态一致性到部署验证把这套方案里值得写进论文和面试回答的东西全部摊开讲。2. 数据模型先行学生认证、动态流与关系链的表设计2.1 为什么用 MySQL 而不是 MongoDB 这类 NoSQL交友类业务在原型阶段最容易踩的坑是一上来就引入 Redis、Elasticsearch、MongoDB 一整套组合拳。主要业务诉求是学生认证信息的持久化、动态内容的用户关联查询、匹配关系的状态流转这些都是强事务和强一致性场景。MySQL 在这里最大的优势是单库单表就能支撑前期的核心链路而且对事务和行级锁的支持非常成熟。比如两个用户建立好友关系时需要同时更新关系表和对方的粉丝计数这种操作在 MySQL 中用一条带FOR UPDATE的查询加上一个事务就能做到不需要引入分布式事务。技术上牺牲的是「附近的人」这类地理查询的效率但后面会讲通过经纬度网格 楼栋编码可以绕开对 MongoDB 的地理索引需求在 MySQL 里用整型字段也能获得很快的查询速度。2.2 用户档案表把学生证号写进业务主键里建表的第一个核心决策是用户表的主键设计。不要用自增主键直接暴露在 API 返回里因为userId1001很容易被遍历这在校园场景里会导致个人隐私泄露。常见的做法是使用微信小程序端调用 login 接口后拿到的openid关联本地用户 ID。下面是用户表的一个最小可用设计。CREATE TABLE t_user ( id BIGINT UNSIGNED AUTO_INCREMENT COMMENT 用户内部ID不对外暴露, student_no VARCHAR(20) NOT NULL COMMENT 学号绑定学生身份, school_id INT UNSIGNED NOT NULL COMMENT 学校ID关联学校表, openid VARCHAR(64) NOT NULL COMMENT 小程序openid, nickname VARCHAR(32) NOT NULL DEFAULT COMMENT 昵称, avatar_url VARCHAR(255) NOT NULL DEFAULT COMMENT 头像地址, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, grade_year SMALLINT UNSIGNED NOT NULL COMMENT 入学年份如2023, college VARCHAR(64) NOT NULL DEFAULT COMMENT 学院, major VARCHAR(64) NOT NULL DEFAULT COMMENT 专业, building_no VARCHAR(16) NOT NULL DEFAULT COMMENT 宿舍楼栋编码如A3-205, latitude DECIMAL(10, 7) NOT NULL DEFAULT 0 COMMENT 纬度, longitude DECIMAL(10, 7) NOT NULL DEFAULT 0 COMMENT 经度, profile_text VARCHAR(500) NOT NULL DEFAULT COMMENT 个性签名, role TINYINT NOT NULL DEFAULT 1 COMMENT 1普通用户 2管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, last_login_time DATETIME DEFAULT NULL COMMENT 最后登录时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid), UNIQUE KEY uk_student_school (school_id, student_no) ) ENGINE InnoDB COMMENT 用户主表;一个容易忽视的细节是uk_student_school联合唯一键。同一个学校内学号应该是唯一的但不同学校之间可能重复所以把两个字段做联合唯一才能保证学生身份的唯一性。另外student_no不要设置为全局唯一否则多所学校接入时会导致数据冲突这也是在业务设计上容易扣分的地方。2.3 动态表和关系表到底要不要冗余计数动态内容通常是交友小程序里访问频率最高的数据。用户打开小程序先看到的是校园动态流然后是「可能感兴趣的人」最后才是聊天会话列表。动态表的核心字段包括动态内容、图片列表、发布者 ID、可见范围公开、同校、同专业、经纬度。这里展示一个关键设计。CREATE TABLE t_feed ( id BIGINT UNSIGNED AUTO_INCREMENT COMMENT 动态ID, user_id BIGINT UNSIGNED NOT NULL COMMENT 发布用户ID, content VARCHAR(2000) NOT NULL DEFAULT COMMENT 文本内容, image_urls JSON NOT NULL COMMENT 图片URL数组如[url1,url2], topic_id INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 话题ID0表示无, visibility TINYINT NOT NULL DEFAULT 1 COMMENT 1公开 2同校 3同专业, latitude DECIMAL(10, 7) NOT NULL DEFAULT 0 COMMENT 发布时纬度, longitude DECIMAL(10, 7) NOT NULL DEFAULT 0 COMMENT 发布时经度, building_no VARCHAR(16) NOT NULL DEFAULT COMMENT 发布时所在楼栋, like_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 点赞数冗余计数, comment_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 评论数冗余计数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_create (user_id, create_time), KEY idx_visibility_create (visibility, create_time) ) ENGINE InnoDB COMMENT 校园动态表;like_count和comment_count是典型的冗余计数。每次点赞不实时去t_feed_like里 count而是先插入点赞明细表再执行UPDATE t_feed SET like_count like_count 1 WHERE id ?。这样动态流的列表查询不需要子查询扫描速度会快很多代价是数据一致性需要靠事务来保证。把 image_urls 设计为 JSON 类型一方面方便小程序端直接读取后wx.previewImage识别数组另一方面也避免了单独建一个图片附表带来的查询开销。在 MySQL 5.7 以上版本中 JSON 字段可以建虚拟列索引但在这个场景里基本用不到因为图片 URL 不会作为查询条件。2.4 好友关系表交友小程序的核心关系链是双向好友关系以及关注和拉黑。关系表的难点在于状态机设计要能表示「待验证-已通过-已拉黑-已删除」等多种状态。CREATE TABLE t_relationship ( id BIGINT UNSIGNED AUTO_INCREMENT COMMENT 关系ID, user_id BIGINT UNSIGNED NOT NULL COMMENT 关系发起方, target_user_id BIGINT UNSIGNED NOT NULL COMMENT 关系接收方, relation_type TINYINT NOT NULL DEFAULT 1 COMMENT 1好友 2关注 3拉黑, status TINYINT NOT NULL DEFAULT 1 COMMENT 1待验证 2已通过 3已拒绝 4已解除, request_msg VARCHAR(100) NOT NULL DEFAULT COMMENT 验证消息, source TINYINT NOT NULL DEFAULT 1 COMMENT 1同专业 2同兴趣 3附近 4扫码, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_target (user_id, target_user_id, relation_type), KEY idx_target_status (target_user_id, status) ) ENGINE InnoDB COMMENT 用户关系表;这个表的查询模式通常是「当前用户和另一个用户的关系是什么」「谁申请加我为好友」以及「我的好友列表」。source字段值得保留它记录用户是通过什么渠道结识的这在论文中可以作为「社交归因分析」的数据来源比如验证「同专业加好友的通过率远高于附近的人随机打招呼」。在 SpringBoot 层面MyBatis 对应的 Mapper 里查询好友列表时需要注意user_id和target_user_id的双向扫描。如果只查WHERE user_id #{userId}会漏掉对方主动加我为好友的情况需要做一次合并查询或者直接按固定规则把「小 ID 在前大 ID 在后」存入两个字段。3. 匹配引擎的实现从 SQL 查询到加权随机推荐3.1 同校同专业优先的 LBS 匹配查询交友匹配的第一版实现不需要引入复杂的推荐算法把用户可选择的条件落成 SQL 查询即可。先看一个按专业匹配的例子。SELECT u.id, u.nickname, u.avatar_url, u.grade_year, u.major, ROUND(ST_Distance_Sphere( POINT(u.longitude, u.latitude), POINT(#{myLng}, #{myLat}) ) / 1000, 1) AS distance_km FROM t_user u LEFT JOIN t_relationship r ON r.user_id u.id AND r.target_user_id #{myId} WHERE u.school_id #{schoolId} AND u.status 1 AND u.id ! #{myId} AND u.major #{myMajor} AND r.id IS NULL AND NOT EXISTS ( SELECT 1 FROM t_relationship r2 WHERE r2.user_id #{myId} AND r2.target_user_id u.id AND r2.relation_type 3 ) ORDER BY u.last_login_time DESC LIMIT 20;这个查询的关键在NOT EXISTS子查询排除已经拉黑过的人。ST_Distance_Sphere是 MySQL 5.7 以上提供的球面距离计算函数单位是米除以 1000 转成千米它比直接计算欧几里得距离更准确。在两个经纬度都是 DECIMAL 类型存储时这个函数可以正确执行。对于「同专业」这种高频筛选条件前期数据量在几千人时不需要额外加索引直接走 school_id major 的联合索引就能在毫秒级返回结果。真正影响性能的是ORDER BY last_login_time DESC因为last_login_time索引在这里帮不上忙所以可能出现 filesort但数据量在 10 万以内时影响不大不需要过早优化。3.2 加权随机拒绝「每次都是同一批人」如果推荐列表每次都用同样的排序规则用户会发现刷来刷去就那几个人这是交友类产品最影响体验的问题之一。第二个常见做法是引入随机因子但随机的粒度需要控制。在 Java 代码里实现加权随机的思路是这样的给每个候选用户分配一个权重权重由基本属性同专业加 30 分同学院加 15 分和行为属性最近活跃加 10 分新注册用户加 20 分组成然后把这些权重放进一个累计区间做随机抽取。public ListUserCardVO recommendUsers(Long userId, int size) { ListUserScore candidates userMapper.findCandidates(userId, 100); int totalWeight candidates.stream().mapToInt(UserScore::getWeight).sum(); ListUserCardVO result new ArrayList(size); Random random new Random(); for (int i 0; i size; i) { int r random.nextInt(totalWeight); int cursor 0; for (UserScore cand : candidates) { cursor cand.getWeight(); if (cursor r) { result.add(toVO(cand)); break; } } candidates.removeIf(c - c.getUserId().equals(result.get(result.size() - 1).getUserId())); totalWeight candidates.stream().mapToInt(UserScore::getWeight).sum(); } return result; }这里的findCandidates不应该在 SQL 里直接排好序返回而是查出 100 个基础候选后在 Java 内存中做过滤和权重分配。好处是避免 SQL 里写复杂的 CASE WHEN 加权语句导致后续维护困难同时随机逻辑在 Java 代码里可测试、可加日志。从分页策略上两种方式各有取舍。MySQL 端ORDER BY RAND()是绝对要避免的因为它会对全表做随机排序数据量一大就卡死。3.3 负反馈闭环不感兴趣的人和举报的人必须沉底匹配系统如果没有负反馈就会反复向用户展示那个「看过三次但完全不喜欢」的人。这里需要在关系表的基础上增加一个动作类型比如relation_type 4表示「不感兴趣」。在拉取候选用户时需要把这类用户从结果中排除。更好的做法是建一张独立的t_user_negative_feedback表记录负反馈因为不感兴趣和拉黑不同拉黑需要通知对方并解除关系而不感兴趣只是单向隐藏。CREATE TABLE t_user_negative ( id BIGINT UNSIGNED AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL COMMENT 操作者, target_user_id BIGINT UNSIGNED NOT NULL COMMENT 被隐藏的人, reason TINYINT NOT NULL DEFAULT 0 COMMENT 0手动隐藏 1举报, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_target (user_id, target_user_id) ) ENGINE InnoDB COMMENT 负反馈记录表;查询匹配列表时一条LEFT JOIN t_user_negative n ON n.user_id #{myId} AND n.target_user_id u.id WHERE n.id IS NULL就可以把负反馈用户过滤掉。这里隐藏了一个细节t_relationship的uk_user_target里包含relation_type而t_user_negative不包含因为一个用户对另一个用户只可能有一条不感兴趣记录重复点击只更新 reason 字段。当用户在「附近的人」页面连续点击不感兴趣超过 20 次时匹配结果池会迅速收缩。常见做法是记录用户负反馈的分布情况比如根据用户活跃度动态扩大或缩小候选池。在这个项目中把每天负反馈占比超过 60% 的用户视为「高冷用户」在推荐权重中额外增加 10 分给活跃用户这样能有效避免匹配池因为负反馈而收敛到空。4. 恋爱情感的逻辑状态机从打招呼到好友关系的流转4.1 好友验证的状态流交友小程序的业务闭环是用户 A 看到用户 B 的卡片或动态发起打招呼用户 B 收到验证消息通过后双方进入好友列表。这个流程里最需要避免的问题是状态混乱对方已经通过了请求但 A 端还显示「待验证」。状态流转的设计要统一收口。在t_relationship表中status字段的取值应该有明确语义1 待验证、2 已通过、3 已拒绝、4 已解除。所有针对关系的操作都必须经过 Service 层校验后更新状态不允许直接 UPDATE。在 SpringBoot 服务中这段逻辑会落在 service 层Transactional(rollbackFor Exception.class) public boolean acceptFriendRequest(Long requestId, Long currentUserId) { Relationship relation relationshipMapper.selectByIdForUpdate(requestId); if (relation null || !relation.getTargetUserId().equals(currentUserId)) { throw new BizException(ErrorCode.ILLEGAL_OPERATION); } // 当前状态必须是待验证 if (relation.getStatus() ! 1) { throw new BizException(ErrorCode.STATUS_ALREADY_PROCESSED); } int row relationshipMapper.updateStatus(requestId, 2); if (row 0) { throw new BizException(ErrorCode.STATUS_ALREADY_PROCESSED); } // 同时插入一条反向关系因为好友是双向的 relationshipMapper.insertReverseRelation(relation.getUserId(), currentUserId, 2); return true; }selectByIdForUpdate使用了SELECT ... FOR UPDATE锁行保证两个请求同时操作同一条关系记录时只有一个能成功修改状态。另一个请求在锁释放后读取到最新状态发现 status 已经变成 2直接抛出「已处理」异常。4.2 MySQL 行锁在多端登录场景下的表现校园场景里很多学生有多台设备或者会在手机上频繁切换账号。并发问题集中体现在同一用户在两台设备上同时操作「添加好友」「点赞」「取消匹配」。这里需要先理解 InnoDB 行锁的行为。selectByIdForUpdate加的是排他锁同一行的其他更新操作会被阻塞而不是报错。所以两个请求同时点「通过验证」后一个请求会先阻塞在锁上等前一个请求提交后才执行查询然后发现 status 不是 1返回友好提示这个流程是安全的。但有一个并发坑是反向关系的插入。insertReverseRelation如果只依赖数据库唯一键uk_user_target那么在两个不同的小伙伴同时互相添加对方时可能因为主键顺序问题出现锁等待超时。解决办法是在业务侧做排序插入关系记录前约定 user_id 为值较小的 IDtarget_user_id 为值较大的 ID插入和查询都用这条规则这样同一个好友对只会对应唯一一行不需要担心重复插入。4.3 消息已读未读与「正在输入」的隐式状态聊天功能如果做全量实时推送会引入 WebSocket 或定时轮询的复杂度。这个项目的常见做法是进入聊天页面时拉取最近 50 条记录页面停留期间用微信小程序的wx.request做 3 秒一次的轮询拉取新消息用last_read_msg_id记录已读位置。消息表的设计可以简单一点。CREATE TABLE t_message ( id BIGINT UNSIGNED AUTO_INCREMENT, from_user_id BIGINT UNSIGNED NOT NULL COMMENT 发送者, to_user_id BIGINT UNSIGNED NOT NULL COMMENT 接收者, msg_type TINYINT NOT NULL DEFAULT 1 COMMENT 1文本 2图片 3语音, content VARCHAR(2000) NOT NULL COMMENT 文本内容或媒体URL, biz_id VARCHAR(64) NOT NULL COMMENT 客户端生成的唯一ID用于去重, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_from_to_create (from_user_id, to_user_id, create_time) ) ENGINE InnoDB COMMENT 聊天消息表;biz_id是客户端生成的消息唯一 ID比如UUID.randomUUID().toString()。服务端收到消息时先查biz_id是否已存在存在就直接返回旧消息记录避免小程序端因为超时重试导致消息重复发送。未读数的维护同样采用冗余手段在会话维度记录未读计数。如果收件箱模型比较复杂也可以用类似UPDATE t_message SET read_flag 1的批量回执但在这个数据量上 t_message 表加一个read_status字段就够了。5. 上线前必做的几件事接口安全、隐私遮蔽与压测验证5.1 学生认证学号 姓名 入学年份的三要素校验交友类小程序上架时最容易被要求整改的就是身份认证环节。校园场景不能只靠微信授权拿到头像昵称就允许用户逛动态必须对「在校学生」身份做一个校验闭环。常见的方案是接入学校的数据中心做学号验证但大多数毕设项目没有这个权限。替代方案是采用「学生证照片 管理员审核」的人工兜底用户在认证页面上传学生证照片后台管理员在小程序的管理端进行人工核对通过后status置为 1可以正常使用全部功能。同时对接wx.login获取的openid绑定设备。所有 API 在返回用户信息时都需要做字段脱敏处理学号只显示前 3 位和后 2 位手机号直接不返回。避免把student_no完整字段通过接口透出。5.2 敏感词过滤为什么不建议自己造轮子交友类内容的审核重点集中在动态发布、打招呼消息和头像昵称上。这里建议策略是文本走接口、图片走人工两层配套。文本过滤不要自己用正则去匹配敏感词列表因为校园场景里的黑话变体太快。比较可靠的做法是直接使用现成的 DFA确定性有限状态机敏感词库或者在服务端调用内容安全接口。下面是一个基于 DFA 的简化版 Java 实现思路。PostMapping(/feed) public ResultLong createFeed(RequestBody FeedCreateRequest req) { // 数字、字母、常见谐音词的变体需要先归一化 String normalized SensitiveWordNormalizer.normalize(req.getContent()); if (sensitiveFilter.contains(normalized)) { return Result.error(ErrorCode.CONTENT_BLOCKED, 内容包含违规词汇); } // 生僻字使用 Unicode 转义检查避免漏掉 Feed feed new Feed(); feed.setUserId(LoginContext.getUserId()); feed.setContent(req.getContent()); feed.setImageUrls(JSON.toJSONString(req.getImageUrls())); feed.setLatitude(req.getLatitude()); feed.setLongitude(req.getLongitude()); feedService.create(feed); return Result.success(feed.getId()); }normalize方法把全角字符转半角、大小写归一化、去掉空白符这样用户发「f u c k」或者「f-u-c-k」都能被同一个敏感词规则命中。对于那些无法用 DFA 覆盖的图片内容可以做一个举报按钮用户举报后在管理后台集中处理。5.3 性能验证用模拟数据打一次 500 并发上线前需要做一轮快速压测确认数据库连接池和接口响应时间在校园网环境下的表现。Apache JMeter 或者简单的 Java 并发脚本都可以。关注三个指标接口 P99 响应时间、数据库连接池的等待时长、错误率。在 SpringBoot 的application.yml里连接池参数建议这样起步spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 3000 max-lifetime: 1800000 validation-timeout: 1000对于交友动态流的查询接口开启 MySQL 慢查询日志阈值设为 500ms。如果发现某个接口频繁超过阈值可以用EXPLAIN看执行计划中是否出现Using temporary或Using filesort。候选用户的列表接口核心优化是去掉所有OR条件因为 OR 在 MySQL 中很难走索引实际执行时做一次UNION ALL反而更优。5.4 不做过度设计但要有灰度开关很多做过完整项目的开发者在毕设阶段容易陷入过度优化。这里想提醒的是不要在这个阶段引入 Redis 缓存动态流、不要用 MQ 做异步点赞、也不要在 MySQL 前再加一层 Elasticsearch。这套 SpringBoot MySQL 的单体应用在 5000 人同时在线的校园场景下完全撑得住。最后建议保留一个开关。在数据库配置表里记录「测试模式」和「正式模式」正式模式下隐藏未认证用户的动态流内容测试模式下放开权限。这样毕业答辩时演示流畅真实部署时校园用户更安全。本文还有配套的精品资源点击获取