资讯动态

Spring Boot + uni-app 社区实战:对象存储、短信验证码与私信聊天全链路解析

发布时间:2026/9/12 1:38:37 来源:尧图企业网站定制
简介这份源码项目基于Spring Boot和Vue构建前端使用uniapp完成移动端页面设计整体风格参考小红书与堆糖集成了短信发送、对象存储、私信聊天等常用功能是一个功能较完整的前后端分离实战项目。面向计算机相关专业在校学生、教师及企业开发者尤其适合用作毕业设计、课程设计或项目初期立项演示也可作为学习Java后端与Vue前端协作开发的进阶案例。压缩包共2006个文件其中JS脚本1565个、Vue组件125个、Java源码113个另外还包含Markdown说明文档、CSS样式、XML配置、JSON数据以及SQL数据库脚本整体大小127.29MB目录结构清晰便于按模块阅读和复用。已有66人学习下载项目代码经测试运行成功答辩评审分达95分可直接部署查看实际效果也支持在此基础上继续扩展业务逻辑。对于希望快速上手全栈社交类应用开发的人来说这份代码覆盖了注册登录、内容展示、消息互动等主要环节值得参考借鉴。1. 一个 Spring Boot uni-app 的组合把页面、短信、存储和聊天装进同一套工程一个能发短信验证码、上传图片不经过业务服务器、还能像小红书一样刷双列瀑布流的 uni-app 项目真正的设计重点不在 UI 多炫而在 Spring Boot 这一层的接口边界是否划得清楚。短信发送、对象存储、私信聊天在业务上彼此独立却必须共用同一套用户体系、同一份 token 鉴权、同一个异常模型。社区类产品里 App 端用 uni-app、管理端用 Vue、后端用 Spring Boot是最常见也最省人的技术组合——新人能照着页面写接口熟手能把存储和消息两条链路完全解耦到独立服务。这篇按这个组合最常见的落地方式把四条链路的实现口径、参数配置和容易踩空的接口细节梳理一遍给正在搭同类项目的后端和全栈工程师做参照。2. Spring Boot 后端先定接口契约再接入对象存储和短信后端不能一上来就写业务代码。常见做法是先把前端页面路由列出来每个页面对应一组 REST 接口统一响应结构再把对象存储预签名、短信密钥这类与业务无关的横切能力单独抽出配置这样后续前端从 Vue 管理端换到 uni-app 用户端后端几乎不用动。2.1 按页面路由拆分 REST 接口用统一响应体约束前后端uni-app 页面拆成首页瀑布流、笔记详情、个人页、私信列表、聊天窗口五组后端就对应五组 controller。每组接口的返回结构固定为统一响应体前端 request 封装里只认这个壳。// common/ApiResponse.java public class ApiResponseT { private int code; private String message; private T data; public static T ApiResponseT ok(T data) { ApiResponseT r new ApiResponse(); r.code 200; r.message success; r.data data; return r; } public static T ApiResponseT fail(int code, String message) { ApiResponseT r new ApiResponse(); r.code code; r.message message; return r; } }统一响应体的价值在于uni-app 端可以只写一个拦截器code 非 200 时统一弹 toastcode 为 401 时统一跳登录页业务代码里不需要到处写错误分支。接口契约的核心是要把分页参数固定下来页面接口路径方法关键参数返回内容首页瀑布流/api/feed/listGETpage、size、lastId卡片列表笔记详情/api/note/{id}GET无图片列表、标题、标签私信列表/api/conversation/listGET无会话列表及未读数聊天窗口/api/message/listGETconversationId、lastId历史消息发送验证码/api/auth/sms-codePOSTphone、scene是否触发频控提示列表分页统一用 lastId 游标而不是 page 偏移量。瀑布流是无限滚动的深翻页时 MySQL 的 offset 性能衰减非常明显游标分页在 where 条件里直接走主键索引响应时间几乎恒定。2.2 对象存储用 S3 兼容协议对接 MinIO 或 Ceph对象存储在这个项目里管两类数据笔记图片和聊天图片。最常见的做法不是后端接收文件再转存而是后端下发签名 URL前端直接 PUT 到对象存储这样业务服务器不碰文件字节流带宽和磁盘压力都小。// config/OssConfig.java Configuration public class OssConfig { Value(${oss.endpoint}) private String endpoint; Value(${oss.access-key}) private String accessKey; Value(${oss.secret-key}) private String secretKey; Bean public AmazonS3 amazonS3() { return AmazonS3ClientBuilder.standard() .withEndpointConfiguration(new AwsClientBuilder .EndpointConfiguration(endpoint, auto)) .withCredentials(new AWSStaticCredentialsProvider( new BasicAWSCredentials(accessKey, secretKey))) .build(); } }S3 SDK 的 region 参数填auto或者填对象存储服务商提供的 region 标识避免 SDK 默认强制 us-east-1 导致请求被路由到错误机房。endpoint 写 MinIO 或 Ceph RGW 的地址时注意区分 http 和 https内网部署往往没有证书SDK 里还需要关闭校验链。上传策略接口如下RestController RequestMapping(/api/upload) public class UploadController { Resource private AmazonS3 amazonS3; Value(${oss.bucket}) private String bucket; GetMapping(/policy) public ApiResponseMapString, String policy(RequestParam String fileName) { String objectKey note/ UUID.randomUUID() - fileName; GeneratePresignedUrlRequest req new GeneratePresignedUrlRequest( bucket, objectKey, HttpMethod.PUT); req.setExpiration(new Date(System.currentTimeMillis() 5 * 60 * 1000)); URL url amazonS3.generatePresignedUrl(req); MapString, String result new HashMap(); result.put(url, url.toString()); result.put(objectKey, objectKey); return ApiResponse.ok(result); } }预签名 URL 默认只允许 PUT 一次5 分钟有效期足够前端完成上传。objectKey 要带上业务前缀比如笔记图片用note/聊天图片用chat/后续生命周期规则可以按前缀分别设置冷存储或过期清理运维成本低很多。2.3 用 Jasypt 处理短信与存储的密文配置Spring Boot 的 yml 里直接写短信平台 accessKey、对象存储 secretKey一旦代码仓库泄露所有线上凭据等于裸奔。我一般用 jasypt-spring-boot-starter 做配置加密yml 里只保留密文。# application.yml spring: datasource: url: jdbc:mysql://localhost:3306/community username: root password: ENC(X8kf3dLq9vKJmN2lRAbCdE) sms: access-key: ENC(4fTpQz1xWcVbNm8YuIopLk) secret-key: ENC(9sDfGhJkLzXcVbNmQwErTy) oss: endpoint: http://192.168.1.10:9000 bucket: community-images加密的入口不在代码里而在启动命令java -jar app.jar --jasypt.encryptor.password你的主密钥。主密钥不落盘、不进配置中心由运维在发布平台注入环境变量这样即使拿到源码也解不开密文。Jasypt 不适合加密所有配置只建议加密凭据类字段。像 endpoint、bucket 这类非敏感信息保持明文避免排查问题时还要找密钥才能看到配置。3. uni-app 页面设计瀑布流布局、图片懒加载与 Vue3 适配uni-app 的跨端能力决定页面不能直接用 CSS column 属性堆瀑布流——小程序端的渲染引擎对 column 布局支持不一致。这个项目的小红书风格首页和堆糖风格图片流需要按 uni-app 的渲染机制重新设计。3.1 双列瀑布流手动分列比外部组件更可控社区里常见的做法是引入第三方瀑布流组件但遇到下拉刷新和滚动加载同时开启时第三方组件的刷新高度计算经常出问题。我更建议手动分列逻辑简单且完全可控。template view classwaterfall scroll-view scroll-y classscroll-wrap scrolltolowerloadMore view classwaterfall-inner view classwaterfall-col v-for(col, idx) in cols :keyidx view classcard v-foritem in col :keyitem.id image :srcitem.coverUrl modewidthFix lazy-load :style{ height: item.coverHeight rpx } / view classcard-title{{ item.title }}/view /view /view /view /scroll-view /view /template script setup import { computed } from vue; import { useFeedStore } from /stores/feed; const feedStore useFeedStore(); const cols computed(() { const colCount 2; const result Array.from({ length: colCount }, () []); const heights new Array(colCount).fill(0); feedStore.feedList.forEach((item) { // 找到当前高度最短的一列新卡片放进这一列 const targetIdx heights.indexOf(Math.min(...heights)); result[targetIdx].push(item); heights[targetIdx] item.coverHeight; }); return result; }); function loadMore() { feedStore.fetchNextPage(); } /script分列逻辑的核心是 heights 数组。每条 feed 数据由后端返回 coverHeight 字段这是图片宽高比换算出的 rpx 高度。短列优先分配的策略能让两列底部分叉不至于太大视觉接近小红书原生效果。滚动容器不能省略。scroll-view配合scrolltolower触发加载下一页而不是用页面级onReachBottom。原因在于页面级触底在 App 端和微信小程序端的触发时机不一致在自定义导航栏场景下经常提前或延后触发scroll-view 的行为则完全可控。3.2 图片懒加载与对象存储的尺寸裁剪参数lazy-load属性在微信小程序和 H5 端有效App 端目前不生效App 端用户量大的话需要自己做 IntersectionObserver 判断。这里有个更简单的思路让对象存储在 URL 层面出图列表页永远加载缩略图。https://your-oss-endpoint/bucket/note/xxx.jpg?x-oss-processimage/resize,w_600,m_lfit图片处理参数拼在 URL 查询串上w_600控制宽度m_lfit表示等比缩放。列表页传 600 宽度详情页去掉处理参数直接拉原图图片流量可以下降一半以上。这个做法对 MinIO 不适用MinIO 默认没有图片处理能力生产环境可以在 MinIO 前面挂一层轻量图片处理服务或者直接选择带处理能力的云对象存储。3.3 Vue2 老项目升级 Vue3 的五个改动点热词里经常搜到「uniapp vue2转vue3」真实项目迁移时最常踩的坑集中在这五处filter被移除原来模板里{{ price | format }}要改成方法调用v-model的$event传参顺序变了自定义组件上绑定值需要显式写:modelValue和update:modelValue全局 API 从Vue.prototype改为app.config.globalProperties$children被移除需要改用ref生命周期钩子destroyed改名unmounted。项目里如果用了 uView 这类依赖 Vue2 的 UI 库升级前先确认组件库版本是否支持 Vue3否则页面哪怕只报一个警告整个模块都可能渲染异常。4. 短信发送与私信聊天的完整链路实现这两个功能单独做都不难放在社区项目里真正麻烦的是交叉问题发送验证码要防刷、私信要保证离线消息不丢、聊天图片要复用对象存储的签名逻辑。4.1 短信验证码的发送与 Redis 频控设计短信验证码的接口设计直接决定安全性。常见的错误是只做「同一手机号 60 秒一条」的限制攻击者换号码批量轰炸照样打爆短信费用。正确的做法是 IP 维度频控加手机号维度频控双写。// service/SmsService.java Service public class SmsService { Resource private RedisTemplateString, String redisTemplate; Value(${sms.sign-name}) private String signName; Value(${sms.template-code}) private String templateCode; public void sendVerificationCode(String phone, String ip) { // 1. IP 维度同一 IP 1 小时最多发 10 条 String ipKey sms:ip: ip; Long ipCount redisTemplate.opsForValue().increment(ipKey); if (ipCount ! null ipCount 1) { redisTemplate.expire(ipKey, 1, TimeUnit.HOURS); } if (ipCount ! null ipCount 10) { throw new BizException(429, 发送过于频繁请稍后再试); } // 2. 手机号维度60 秒一条且验证码 5 分钟有效 String codeKey sms:code: phone; Long ttl redisTemplate.getExpire(codeKey, TimeUnit.SECONDS); if (ttl ! null ttl 240) { throw new BizException(429, 请求过于频繁); } String code String.valueOf(ThreadLocalRandom.current().nextInt(100000, 999999)); redisTemplate.opsForValue().set(codeKey, code, 5, TimeUnit.MINUTES); // 3. 调短信服务商 SDK 发送此处用日志代替真实调用 log.info(send sms to {}: code{}, phone, code); } public boolean verifyCode(String phone, String code) { String key sms:code: phone; String cached redisTemplate.opsForValue().get(key); if (cached ! null cached.equals(code)) { redisTemplate.delete(key); return true; } return false; } }手机号维度频控用验证码 key 的剩余过期时间判断逻辑上要求验证码有效期 5 分钟那么 ttl 大于 240 秒说明这条验证码是 1 分钟内刚发的直接拦截。这样省掉一个独立的频控 keyRedis 里少存一份数据TTL 天然由验证码的过期时间托管不会出现「频控 key 忘了过期导致用户永远发不了」的事故。verifyCode 校验成功立即删除 key防止验证码被重放。登录场景下验证码一旦校验通过就必须作废这是短信登录最常见的安全漏洞点。4.2 私信聊天消息落库、在线推送与离线补拉私信聊天的技术选型要看团队维护能力这里先给方案对比方案实时性维护成本适用规模离线消息HTTP 短轮询3~5 秒延迟最低万级用户以下天然支持WebSocket 自研秒级中十万级需要消息表兜底第三方 IM SDK毫秒级低任意内置自研场景下最常见的配置是 Spring Boot 内嵌容器 ServerEndpoint。这个方案对中小项目足够单机撑几千个连接没问题但要注意两点ServerEndpoint创建的实例不是 Spring 管理的想注入 Service 需要从 ApplicationContext 手动获取另外多节点部署时需要引入 Redis 做消息转发否则两个用户连到不同节点就收不到彼此的实时消息。// websocket/ChatWebSocket.java Component ServerEndpoint(/ws/chat/{token}) public class ChatWebSocket { private static final MapLong, Session ONLINE new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(token) String token) { Long userId JwtUtil.parse(token).getUserId(); ONLINE.put(userId, session); session.setMaxIdleTimeout(0); } OnMessage public void onMessage(String raw, Session session) { // 前端传 {toUserId, content, type} JSONObject msg JSON.parseObject(raw); // 1. 消息落库MySQL 或 MongoDB messageService.save(msg); // 2. 接收方在线则实时推送不在线则等对方上线后拉取 Session target ONLINE.get(msg.getLong(toUserId)); if (target ! null target.isOpen()) { target.getBasicRemote().sendText(JSON.toJSONString(msg)); } } OnClose public void onClose(Session session, PathParam(token) String token) { Long userId JwtUtil.parse(token).getUserId(); ONLINE.remove(userId); } }在线状态用ConcurrentHashMap在内存里维护简单的场景够了。session.setMaxIdleTimeout(0)必须写否则容器默认空闲超时时间可能把长连接踢掉前端只会在几小时后收到一个莫名的断开通知。离线消息的兜底策略是消息先落库再推送接收方上线后调/api/message/list按 lastId 增量拉取。注意聊天列表页和聊天窗口页拉消息的游标要用同一条消息 ID 递增序列否则会出现「窗口里能看到新消息列表页未读数却不变」的脏数据。4.3 聊天里的图片消息复用对象存储聊天的图片消息不要走 base64 塞进 WebSocket 文本帧正确做法是前端先调/api/upload/policy拿到签名 URL上传完成后把 objectKey 作为消息内容发给对方。接收方解析消息内容时根据 type 字段决定渲染image组件还是text组件。这里就看出 2.2 节按业务前缀隔离 objectKey 的价值了聊天图片统一走chat/前缀可以单独配置生命周期规则比如 180 天后自动转为低频存储避免聊天图片长期占用标准存储成本。5. 打包联调前必查的三个验证点功能开发完到真正能跑通的路上拦路的往往不是业务逻辑而是配置和打包细节。这里给三个最值得花时间验证的点。5.1 用 curl 直接验证签名 URL 和短信回调对象存储的签名链路是否通不用等前端写上传逻辑直接命令行验证# 先取预签名地址 curl -X GET http://localhost:8080/api/upload/policy?fileNametest.jpg # 拿返回的 url 字段本地放一张测试图直接 PUT curl -X PUT -T ./test.jpg http://192.168.1.10:9000/community-images/note/xxx.jpg?X-Amz-Signature... # 验证读权限签名 GET 可用未签名 GET 应 403 curl -o /dev/null -w %{http_code} http://192\.168\.1\.10:9000/community-images/note/xxx.jpg签名 URL 里包含特殊字符bash 里要整体加引号防止被解释成后台执行符号。返回 403 不代表配置错误只代表 bucket 权限是私有的私有读正是预期行为。5.2 双账号验证消息的在线与离线两条路径聊天模块的验证必须用两个账号。A 在线、B 在线的实时推送路径验一次A 下线、B 发消息、A 重新上线后通过拉取接口补齐历史消息这条离线路径也必须跑通。常见问题是 A 重连时 WebSocket 的 token 已过期ONLINE里旧连接没清干净导致新连接建立失败。排查时先看 Spring Boot 日志里有没有onClose触发再看ONLINE的 size 是否和在线人数一致。5.3 检查 manifest.json 的模块权限声明uni-app 打包微信小程序或安卓 App 前manifest.json 里必须核对模块权限。私信聊天如果做了本地推送通知需要勾选 Push 模块H5 端嵌入微信公众号要获取定位需要在 manifest 里声明定位权限并在微信公众平台配置 JS 接口安全域名。漏掉任何一项进入页面时调用对应 API 会静默失败控制台不报错但功能就是没反应。注意manifest 里的权限声明调整后要重新生成各端配置直接改源码目录下的 manifest.json 但没重新编译打包产物里仍然是旧配置这是改了半天还是一样的最常见原因。验证全部通过后把后端接口的响应时间打点加上重点盯/api/feed/list和/api/message/list两个接口的 P99这两个接口承载了页面 80% 以上的流量慢查询的根因一般都在连表查图片 URL 上考虑直接把图片处理参数拼进数据库字段省掉每次请求的字符串拼接开销。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价