简介基于uniapp与HBuilderX开发的Java后端交友社交软件完整源码面向需要完成毕业设计或想系统学习跨端社交项目开发的读者。项目支持小程序与App端前后端已实现联通覆盖交友、婚礼等场景并提供页面模板与功能模块的自定义能力便于按业务需求调整。压缩包共480个文件包括215个JavaScript文件、92个Java文件、47个HTML与31个CSS分别对应前端逻辑、后端服务、页面结构与样式另含图片、字体、配置文件等辅助资源整体大小14.53MB目录层级分明可快速定位前后端代码。目前已有413人学习下载。通过这份源码可掌握uniapp多端适配、Java后端接口设计、前后端数据交互的完整流程同时可基于MVC分层理解社交软件中用户展示、搜索匹配、私信等关键功能的具体实现适合直接部署运行并作为毕设展示或二次开发的起点。1. uniapp hbuilderX Java 的交友源码先判断值不值得复现很多人搜“交友app源码”时第一眼想看的是注册、匹配、聊天三个页面长什么样但实际动手之后才会发现真正卡住你的往往不是页面而是消息会不会丢、接口扛不扛得住刷、上架会不会被拒。这个标题把前端框架、构建工具、后端语言一次写全uniapp 负责一套代码出 Android、iOS 和小程序hbuilderX 负责真机调试与打包Java 后端管登录鉴权、IM 私聊、匹配推荐和动态广场。它适合两类人一是想拿完整项目练手或做毕业设计的在校生二是在短周期内要交付一套可演示社交产品的接单开发者。结论先放在这这套骨架值得复现但别原样照抄按下文改完再上线。2. Java 后端这样搭鉴权、IM 与匹配打分的核心代码后端是整套社交源码的心脏前端页面做得再好看一旦登录过期不刷新、私聊消息凭空消失用户两天就会流失。做这类交友后端常用组合是 Spring Boot MySQL Redis WebSocket 的单体应用先别急着上 Spring Cloud业务到了一定规模再拆分不迟。下面按模块给出可复现的代码骨架和参数边界。选型上关系数据用 MySQL在线状态、离线消息、Feed 缓存和接口限流用 Redis实时消息用 WebSocket。很多人纠结要不要换 Netty我一般建议先不换Spring 自带的 WebSocket 在几千并发、单机跑通业务的阶段完全够用等压测真到了瓶颈再把消息网关独立出去。2.1 技术选型和模块划分为什么是 Spring Boot Redis MySQL一个能跑的交友后端最少要拆六个模块user 用户资料、auth 登录鉴权、match 匹配推荐、dynamic 动态广场、message 私聊消息、admin 审核后台。Spring Boot 做单体聚合最省事一个 jar 包部署目录按业务分包配置一套环境变量就行。依赖也简单spring-boot-starter-web、spring-boot-starter-websocket、mybatis-plus、redis、mysql-connector-java再加一个 jjwt 做令牌骨架就能立住。消息表是这套系统里最容易被忽略但又最关键的表。很多人把聊天记录放在内存里觉得重启前用完就行结果线上重启一次用户聊天记录全没了口碑直接崩。私聊表建议按下面的 DDL 来建CREATE TABLE chat_message ( id bigint NOT NULL AUTO_INCREMENT, from_id bigint NOT NULL COMMENT 发送者用户ID, to_id bigint NOT NULL COMMENT 接收者用户ID, content varchar(2000) NOT NULL DEFAULT COMMENT 消息内容, msg_type tinyint NOT NULL DEFAULT 0 COMMENT 0文本 1图片 2语音, status tinyint NOT NULL DEFAULT 0 COMMENT 0未读 1已读, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_to_status (to_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明聊天记录必须落 MySQL只靠内存转发等于把线上业务当测试环境status 字段配合 idx_to_status 索引支撑“未读数”查询msg_type 预留图片语音扩展图片消息的 content 存对象存储 URL。参数说明content 用 varchar(2000) 足够文本消息不要用 text因为 InnoDB 里 text 走行外存储会影响普通查询性能utf8mb4 字符集必须用否则用户发 emoji 直接报错create_time 一定保留后续做消息撤回、漫游、客服侧查询都有时间依据。2.2 登录鉴权JWT 双令牌与刷新流程的具体参数登录鉴权是交友系统里最容易做残的部分。常见错误做法是只发一个 token过期时间设七天用户一旦被拉黑token 在七天内依然有效拉黑形同虚设。正确做法是双令牌access token 短期refresh token 长期服务端通过 Redis 维护黑名单登出、封禁、拉黑都能即时生效。下面是 JWT 工具的关键代码import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Date; public class JwtUtil { // 生产环境从配置中心读取不要硬编码在代码里 private static final String SECRET replace-with-config-secret; private static final long ACCESS_EXPIRE_MS 2 * 60 * 60 * 1000L; // 2小时 private static final long REFRESH_EXPIRE_MS 7 * 24 * 60 * 60 * 1000L; // 7天 public static String createToken(Long userId, String tokenType) { long expire access.equals(tokenType) ? ACCESS_EXPIRE_MS : REFRESH_EXPIRE_MS; return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(type, tokenType) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(new SecretKeySpec( SECRET.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName())) .compact(); } public static Long parseUserId(String token) { Claims claims Jwts.parser() .setSigningKey(new SecretKeySpec( SECRET.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName())) .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); } }逻辑说明createToken 按 tokenType 决定过期时间refresh token 单独签发前端保存时两者分开存parseUserId 解析失败会抛异常拦截器捕获后统一返回 401。参数说明access 2 小时、refresh 7 天是社交类 App 的折中方案太短用户天天重新登录太长封禁难生效拉黑时把 token 的 jti 写进 Redis 黑名单刷新接口也要先查黑名单再签发新 token。注意 SECRET 长度至少 32 字节HS256 才有足够强度而且不要出现在 git 历史里。拦截器负责放行登录和刷新接口其余请求校验 Authorization 头里的 Bearer token再把 userId 塞进 ThreadLocal便于后续业务取用。需要登录的接口都要求前端带 token匿名可访问的只有验证码、登录注册、公开资料页。这个白名单列表务必维护清楚否则会出现“已登录”和“未登录”逻辑混在一起的问题比如某些接口匿名也能调拉黑就形同虚设。2.3 私聊消息先落库再转发WebSocket 与 Redis 的配合交友软件的实时消息链路按顺序做三件事握手鉴权、上线登记、消息转发。握手阶段直接在 query 参数里带 token服务端校验后把 userId 放进 session 属性。上线时把 session 放进 ConcurrentHashMap离线时移除。转发逻辑里最核心的一条原则先写库再发消息。写库成功才算这条消息真的发出去了否则服务端刚重启用户就发现聊天记录断了。下面是服务端 WebSocket 处理器的骨架Component public class ChatServer extends TextWebSocketHandler { private static final MapLong, WebSocketSession ONLINE new ConcurrentHashMap(); Autowired private ChatMessageMapper messageMapper; Autowired private StringRedisTemplate redisTemplate; Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { Long userId (Long) session.getAttributes().get(userId); ONLINE.put(userId, session); // 上线后先补推离线消息 ListString offlineList redisTemplate.opsForList().range(offline: userId, 0, -1); for (String jsonStr : offlineList) { session.sendMessage(new TextMessage(jsonStr)); } redisTemplate.delete(offline: userId); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { JSONObject json JSON.parseObject(message.getPayload()); if (ping.equals(json.getString(type))) { session.sendMessage(new TextMessage({\type\:\pong\})); return; } Long fromId (Long) session.getAttributes().get(userId); Long toId json.getLong(toId); String content json.getString(content); ChatMessage msg new ChatMessage(); msg.setFromId(fromId); msg.setToId(toId); msg.setContent(content); msg.setStatus(0); messageMapper.insert(msg); // 先落库再转发 WebSocketSession target ONLINE.get(toId); if (target ! null target.isOpen()) { target.sendMessage(new TextMessage(JSON.toJSONString(msg))); } else { // 接收方不在线消息进 Redis 离线队列等对方上线补推 redisTemplate.opsForList().leftPush(offline: toId, JSON.toJSONString(msg)); } } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { Long userId (Long) session.getAttributes().get(userId); ONLINE.remove(userId); } }逻辑说明afterConnectionEstablished 里先补推 Redis 离线队列避免用户上线后一条条等ping/pong 心跳在这里直接处理不落库不转发。参数说明离线队列用 Redis 的 Listkey 是 offline:{userId}value 是消息 JSON建议给 key 设置 7 天过期时间避免僵尸账号堆积并发量上来后消息要带上唯一 ID客户端按 ID 去重否则补推和实时推同时到达会重复。注意这个版本的 ONLINE 是单机内存 Map如果后端起了两台机器转发要改成 Redis 发布订阅或者引入消息中间件这是后话。另外容器层的空闲超时参数必须调整否则默认 60 秒空闲连接就会被回收Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container new ServletServerContainerFactoryBean(); container.setMaxSessionIdleTimeout(0L); // 0 由应用层心跳控制 container.setMaxTextMessageBufferSize(8192); return container; }逻辑说明把容器层的空闲超时关掉由客户端 30 秒一次的心跳决定连接是否存活如果连接断了但客户端没感知心跳发送失败会触发重连服务端通过 afterConnectionClosed 清理 ONLINE 表。参数说明maxTextMessageBufferSize 设成 8192 字节匹配普通聊天文本和一张压缩图的 base64 上限超过会抛异常所以图片消息走对象存储 URL 是正路。2.4 匹配与动态先圈人再打分Feed 走 Redis 缓存匹配推荐的常见迷信是“上来就上机器学习”实际这个阶段用规则打分就够了。做法分两步先用 SQL 把候选人圈到几百人以内再在 Java 里算相似度分数。圈人条件一般是同城、近 7 天有活跃、性别取向匹配打分公式可以用兴趣标签重合度加距离和活跃度的加权和。核心打分代码public double matchScore(UserProfile me, UserProfile other) { SetString common new HashSet(me.getTags()); common.retainAll(other.getTags()); double tagScore common.size() / (double) Math.min(me.getTags().size(), other.getTags().size()); double km distanceKm(me.getLat(), me.getLng(), other.getLat(), other.getLng()); double distanceScore Math.max(0.0, 1.0 - km / 50.0); // 50km 内线性衰减 double activeScore Math.min((me.getActiveScore() other.getActiveScore()) / 200.0, 1.0); // 兴趣占比最高距离其次活跃度只做微调 return 0.6 * tagScore 0.3 * distanceScore 0.1 * activeScore; }逻辑说明标签重合度算的是交集除以较少那方的标签数选 3 个标签的用户和选 10 个标签的用户能公平比较distanceKm 用地图服务商坐标系下的 geohash 预计算字段粗筛别在 SQL 里实时算球面距离否则用户一多接口就慢。参数说明分母 50 控制距离衰减速度一线城市可以调到 20让匹配更聚焦同区0.6/0.3/0.1 的权重是经验值上线后用次日回访率和私信率做 A/B 调整。动态广场的 Feed 流不要实时查数据库关注者动态按时间写进 Redis ZSet推荐动态用点赞数和时间衰减排序缓存 5 分钟即可热点动态后置处理。3. uniapp 前端这样写登录、列表、聊天与 hbuilderX 真机调试后端接口就绪后前端在 hbuilderX 里用 uniapp 搭壳。uniapp 的价值在于一套代码编译多端但交友 App 的核心场景还是 App所以真机调试和安卓云打包这两步绕不开。这章按工程骨架、请求封装、登录推荐、聊天联调四个步骤展开代码都是可以直接抄进项目的最小可用版本。3.1 创建项目与页面骨架pages.json 的四个 tab 和路由hbuilderX 新建 uniapp 项目时选默认模板推荐用 Vue 3 版本。工程结构按 pages、static、utils、api 分包。交友软件的最小页面集是四个 tab首页推荐、动态广场、消息列表、个人中心再加登录、个人资料、聊天、匹配详情等二级页面。pages.json 的 tabBar 配置如下{ pages: [ { path: pages/home/home, style: { navigationBarTitleText: 推荐 } }, { path: pages/dynamic/dynamic, style: { navigationBarTitleText: 动态 } }, { path: pages/message/message, style: { navigationBarTitleText: 消息 } }, { path: pages/my/my, style: { navigationBarTitleText: 我的 } } ], tabBar: { color: #999999, selectedColor: #ff4d6a, backgroundColor: #ffffff, list: [ { pagePath: pages/home/home, text: 推荐 }, { pagePath: pages/dynamic/dynamic, text: 动态 }, { pagePath: pages/message/message, text: 消息 }, { pagePath: pages/my/my, text: 我的 } ] } }逻辑说明四个 tab 页面必须在 pages 数组的第一层先注册tabBar 里配的 pagePath 才能被识别不要在 tab 页跳 tab 页时用 navigateTo要用 switchTab。参数说明tabBar 文字最多四个汉字超出会出省略号iconPath 和 selectedIconPath 需要本地静态图不要引网络图云打包时网络图会丢颜色值用十六进制selectedColor 在社交产品里普遍用偏暖的红粉色系视觉上引导点击。注意 Vue 3 模板里生命周期用 uniapp 的 onLoad、onShow 钩子而不是直接写在 setup 里调用混用会踩坑。3.2 请求封装token 注入、401 刷新与防循环重放前端最值得封装的就是 request。BaseURL 在真机调试时用局域网 IP云打包后要换成线上 HTTPS 域名。封装里要处理三件事自动带 token、401 时刷新 token 并重放、登录态失效时回登录页。下面是 utils/request.js 的骨架// utils/request.js const BASE_URL http://192.168.1.100:8080/api // 真机调试用电脑局域网IP上线换HTTPS域名 let refreshing false // 防止多个请求同时触发刷新 function request(path, method, data {}, needAuth true) { return new Promise((resolve, reject) { const accessToken uni.getStorageSync(access_token) uni.request({ url: BASE_URL path, method: method || GET, data, header: { Content-Type: application/json, Authorization: needAuth accessToken ? Bearer accessToken : }, success: async (res) { const body res.data if (body.code 401 needAuth) { if (refreshing) { // 已有刷新在进行等一会儿再重放当前请求 setTimeout(() { request(path, method, data, needAuth).then(resolve).catch(reject) }, 300) return } refreshing true const ok await refreshToken() refreshing false if (ok) { request(path, method, data, needAuth).then(resolve).catch(reject) } else { uni.removeStorageSync(access_token) uni.removeStorageSync(refresh_token) uni.reLaunch({ url: /pages/login/login }) } return } if (body.code 0) { resolve(body.data) } else { uni.showToast({ title: body.msg, icon: none }) reject(body) } }, fail: reject }) }) } function refreshToken() { return new Promise((resolve) { const refreshToken uni.getStorageSync(refresh_token) uni.request({ url: BASE_URL /auth/refresh, method: POST, data: { refreshToken }, success: (res) { if (res.data.code 0) { uni.setStorageSync(access_token, res.data.data.accessToken) uni.setStorageSync(refresh_token, res.data.data.refreshToken) resolve(true) } else { resolve(false) } }, fail: () resolve(false) }) }) } module.exports { request, BASE_URL }逻辑说明refreshing 标志位避免并发请求把 refresh 接口打爆401 后先刷新再重放原请求用户无感续期。参数说明判断登录失效的标准统一用 code 401后端所有需要鉴权的接口都要返回这个码不要有的用 401、有的用 403前端会分不清refreshToken 接口本身不要再走这个封装否则刷新失败会死循环这也是很多源码包的前端翻车点。注意401 重放次数必须限制我一般只重放一次第二次再 401 就直接回登录页用 setTimeout 300ms 等并发刷新落地虽然是个笨办法但简单可靠。3.3 登录与推荐列表从按钮到接口的最小链路登录推荐用手机号验证码起步比账号密码省事也符合社交产品主流。点击发送验证码调验证码接口输入后调登录接口拿到双令牌存 storage然后 switchTab 进首页。核心跳转代码// pages/login/login.vue async onLoginTap() { if (!/^1\d{10}$/.test(this.phone)) { uni.showToast({ title: 手机号格式不对, icon: none }) return } const data await request(/auth/login, POST, { phone: this.phone, code: this.code }, false) uni.setStorageSync(access_token, data.accessToken) uni.setStorageSync(refresh_token, data.refreshToken) uni.switchTab({ url: /pages/home/home }) }逻辑说明登录接口 needAuth 传 false走无鉴权分支校验手机号用的是国内 11 位规则产品如果面向海外正则要改。参数说明验证码接口要做 60 秒倒计时防刷后端同时限制同一手机号一分钟一条登录成功后立刻进首页不要再留在登录页。推荐列表的请求单独放一个 api 文件分页参数 page 和 size 后端必须做校验防止一次拉全表// api/match.js export function getRecommendList(page 1, size 10) { return request(/match/recommend, GET, { page, size }) }逻辑说明统一通过 request 封装走鉴权这里不重复写 token 逻辑。参数说明page 从 1 开始size 默认 10下拉加载时 page1在 onReachBottom 里调用列表页的图片要懒加载uniapp 的 image 标签记得开 lazy-load 属性。3.4 聊天页面与 hbuilderX 真机调试/打包WebSocket 和 manifest 参数聊天页面用 uniapp 的 uni.connectSocket 建连token 走 query 参数传递。重点处理心跳和重连30 秒发一次 ping收到 pong 才能确认连接存活断线后做指数退避重连。核心代码// pages/chat/chat.vue onLoad() { const token uni.getStorageSync(access_token) this.socket uni.connectSocket({ url: ws://192.168.1.100:8080/ws/chat?token token, complete: () {} }) this.socket.onMessage((res) { const msg JSON.parse(res.data) if (msg.type pong) return this.messages.push(msg) }) this.socket.onClose(() this.reconnect()) // 断开后自动重连 this.heartbeat setInterval(() { this.socket.send({ data: JSON.stringify({ type: ping }) }) }, 30000) }逻辑说明心跳时间和服务端 idle 超时对齐服务端不主动断开客户端 30 秒一次 ping 就够onClose 里必须清理心跳定时器再重新连接否则会重复连接导致消息重复。参数说明重连建议指数退避3 秒、6 秒、12 秒封顶聊天页 onShow 时如果连接状态是 closed 要拉起重连App 切后台超过几分钟系统会杀掉 WebSocket回前台必须重建连接。hbuilderX 真机调试有两个高频配置manifest.json 的 App 模块里勾选 WebSocket以及真机运行前先让手机和电脑连同一个局域网。云打包的 manifest 关键配置{ app-plus: { distribute: { android: { minSdkVersion: 21, targetSdkVersion: 30, permissions: [ CAMERA, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE, ACCESS_FINE_LOCATION ] } } } }逻辑说明minSdkVersion 21 覆盖 Android 5.0 以上设备targetSdkVersion 30 对应 Android 11读写外部存储权限还按传统方式申请如果升到 31 以上分区存储限制更严相册上传要改用系统选图能力。参数说明CAMERA 用于头像拍照定位权限用于“附近的人”功能不需要的权限别乱勾应用市场审核会查敏感权限的用途声明。云打包前还要准备签名证书证书的 alias 和密码要和打包配置一致这个不一致是最常见的打包失败原因。4. 避坑清单编译、鉴权、推送与部署的 5 个高频翻车点这套骨架从本地跑通到上架中途能翻车的地方不少。下面 5 个问题是我在排查这类项目时遇到频率最高的每一条按“现象—原因—解决”写清楚你对照自己的环境检查就行。4.1 云打包后接口全部超时真机调试却正常现象hbuilderX 真机调试时接口正常云打包安装到安卓手机上所有 HTTP 请求全部超时或者返回数据为空。很多人第一反应是后端挂了但后端日志里根本没有请求进来。原因Android 9 开始默认禁止明文 HTTP 流量真机调试基座里带了调试配置所以能跑自己云打包的 APK 没有而项目里的接口地址还是 http://192.168.x.x:8080 这种局域网地址两种问题叠加在一起。解决上线接口必须换成 HTTPS 域名如果只是内部测试在 manifest.json 的 App 常用其他设置里勾选“使用安全明文传输”并把 BASE_URL 从局域网 IP 改成线上地址后重新打包。排查顺序从网络请求响应看起先确认 URL 在手机浏览器能打开再加 App 的 cleartext 配置别一上来就怀疑后端。4.2 WebSocket 连上就断聊天消息秒掉线现象聊天页面刚进入时连接成功锁屏或切后台几分钟再回来消息收不到页面显示连接关闭。重新进页面又能连上但过一会儿又断。原因服务端容器默认空闲超时是 60 秒没消息就回收连接App 进后台后系统挂起 WebSocket回前台时没有重连逻辑两边状态就对不上了。解决服务端设 maxSessionIdleTimeout(0) 关闭空闲回收客户端加 30 秒心跳onClose 里指数退避重连onShow 里检测连接状态并补连。这套组合改完后锁屏回来最多 3 秒内重建连接基本不再掉线。排查时看服务端日志如果 afterConnectionClosed 和 afterConnectionEstablished 频繁成对出现就是心跳没生效。4.3 access token 过期后用户被反复踢回登录页现象用户玩着玩着突然弹出登录页重新登录后过一会儿又弹。复审代码发现前端只判断了 401没有区分 token 过期和登录态真的失效。原因前端只存了 access token没有实现 refresh 机制后端 2 小时过期后所有接口都返回 401前端收到 401 就无条件跳登录页。解决按 3.2 的 request 封装把双令牌刷新做进去401 后先刷新再重放后端要保证 refresh token 本身不参与刷新逻辑避免死循环。更隐蔽的坑是多个接口同时 401前端并发触发多次 refresh要在封装里加 refreshing 标志位只允许一个刷新请求生效否则 refresh 接口也会被自己人打爆。4.4 聊天记录一旦后端重启历史消息和离线消息全没现象服务器重启后用户聊天记录消失离线期间收到的消息也补不回来。用户投诉“昨天聊的内容全没了”后端日志里看不到任何报错。原因消息只通过 WebSocket 转发没写库离线消息存在内存 Map 里进程一退出全没了。这类项目最容易出现这个问题因为本地演示时不会重启线上运维一操作就露馅。解决按 2.3 的顺序做先插库再转发离线消息进 Redis Listkey 带 7 天过期。注意补推逻辑要幂等客户端按消息 ID 去重否则上线重连和普通重连同时触发同一批消息可能推两遍。数据库重启后还要确认 binlog 开启这是最简单可靠的消息恢复手段很多源码包图省事不开。4.5 应用市场上架被拒陌生人社交遭重点审核现象在应用商店提交审核被以“涉及陌生人社交缺少安全能力”为由驳回没有给出具体修改意见只让你自查。原因交友类 App 属于社交分类里审核最严的子类商店要求必须具备举报、拉黑、敏感信息过滤等能力纯功能演示的源码包装上去必被拒。解决后端必须实现三件套——敏感词过滤内置词库加脱敏替换用户举报接口点进对应用户资料页可提交举报原因拉黑功能拉黑后双方互相不可见、不可发消息。审核后台至少要有举报列表和封禁操作的表单界面上也要有入口。上架前把用户协议和隐私政策页面准备好收集手机号、定位、相册权限都必须在隐私政策里逐条写明用途。5. 上架前最后检查消息 ack、敏感词过滤与接口限流如果上面都跑通了先别急着上线。有一件事比写代码更早教会我上线前要做什么这类 App 上线初期最常遇到的故障就是聊天接口被脚本刷爆消息表直接被打满最后只能回滚重来。所以压测和防护一定要做在前面。这章说三个最值得做的检查项。5.1 消息必达客户端 ack 与服务端补推消息发送成功后客户端要回一条 ack 给服务端服务端把 status 置为 1如果 3 秒内没收到 ack服务端补推一次。这个机制能把弱网下的消息丢失率降到几乎为零。核心是消息带全局唯一 ID客户端用集合去重。压测可以先打推荐接口看错误率比如用 wrk 起 4 个线程 100 个连接跑 30 秒重点看错误率和响应时间有没有突变错误率明显抬升就先查数据库连接池和 Redis 连接数不要急着加机器。5.2 三道闸门限流、敏感词与举报闭环用户每次请求要过三道闸门一是接口限流按用户维度做计数限流同一用户一秒超过 5 次就返回“操作太频繁”二是敏感词过滤消息发出前先过词库命中替换成 * 并记录日志三是举报拉黑闭环举报进后台队列人工处理结果同步到用户端。这三件事做完审核和运营压力都会小一大截。一个简单可用的计数限流public boolean tryAcquire(String userId, int limit, int windowSeconds) { String key rate:user: userId; Long count redisTemplate.opsForValue().increment(key); if (count ! null count 1) { redisTemplate.expire(key, Duration.ofSeconds(windowSeconds)); } return count ! null count limit; }逻辑说明计数值在 Redis 里自增超过 limit 直接拒绝窗口期用 expire 精确控制。注意这个方法不是严格的滑动窗口同一窗口后段请求可能全被放行但对防脚本刷接口完全够用。参数说明limit 按接口类型分开配置登录接口一分钟 5 次聊天发送接口一秒 3 次推荐列表一秒 5 次不要所有接口共用一个计数器否则会被正常用户的下拉刷新误伤。很多初次上线的人就是没给聊天接口加限流被脚本刷爆后才意识到社交类接口比普通接口更需要防护。我后来养成的习惯是每接一个接口先问自己三个问题不登录能不能调、狂点会不会崩、发出去的内容脏了怎么办。这三个问题想清楚系统基本就不会出大乱子。希望帮到你。本文还有配套的精品资源点击获取