尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot+Vue自媒体社区平台开发:架构、Redis缓存与Nginx部署实战

Spring Boot+Vue自媒体社区平台开发:架构、Redis缓存与Nginx部署实战 简介这是一份面向计算机专业毕业设计、课程设计和期末大作业的自媒体社区平台源码采用前端 Vue、后端 Spring Boot 的前后端分离架构适合正在完成毕设的学生以及希望进行项目实战练习的初级开发者。压缩包共 901 个文件大小仅 16.41MB主体包含 Java 后端业务代码、Vue 前端页面组件、JavaScript 交互脚本以及 HTML、CSS、SVG、JPG 等前端静态资源同时提供 YML、XML 等工程配置和安装、运行、构建批处理脚本关键页面还保留调试备份便于对照修改与本地启动。项目来自导师认可并通过的 98 分毕业设计代码经过严格调试描述中声明无已知 Bug整体目录覆盖前端组件、后端接口、系统配置与运行工具链可直接作为毕设方案参考也可作为继续扩展社区功能的基础工程。当前已有 90 人学习适合希望快速获得一套可运行源码并系统理解 Vue 与 Spring Boot 数据交互过程的读者。1. 为什么说 Spring Boot Vue 是自媒体社区平台最稳的组合自媒体社区平台这个选题每年毕业设计里能见到不下几十个版本但大量提交上来的代码只有一套后台管理加一个列表页。真正拉开差距的地方在于内容发布之后的那条链路文章审核状态怎么流转、关注关系怎么存、浏览量和点赞怎么扛住第一批用户、视频能不能直接播。用 Spring Boot 加 Vue 把这条链路完整打通既能覆盖 Java 后端的核心知识点又能把前端工程化、Nginx 部署和 Redis 缓存全部带出来。这篇内容面向负责主要开发的那个角色默认你已经会 Spring Boot 的基本 CRUD但没有完整从零做过一个社区类项目。2. 架构与数据模型先划清楚 Spring Boot 的服务边界2.1 技术选型Spring Boot 3.x 加 Vue 3 的取舍社区平台本质上是一个内容管理系统加社交关系链的组合后端选型主要卡在 Spring Boot 版本和 ORM 框架上。如果你所在机房或老师的验收环境允许 JDK 17直接用 Spring Boot 3.x它能少掉很多老版本依赖冲突而且内置的 observability 和虚拟线程在答辩时也是加分项如果环境锁死 JDK 8就退回 Spring Boot 2.7.x代码结构基本一致只有 javax 到 jakarta 的包名差异需要留意。对比项Spring Boot 2.7.xSpring Boot 3.xJDK 要求8 及以上17 及以上包名javax.*jakarta.*常见前端模板JSP、Thymeleaf前后端分离为主毕设工作量匹配度高高新依赖支持如新版 Redis Client部分兼容原生支持ORM 层我建议用 MyBatis-Plus 而不是 JPA。自媒体平台的核心查询大多是单表列表、联表查询很少MyBatis-Plus 的 LambdaQueryWrapper 写起来比 JPA 的 HQL 直观分页插件也成熟。前端 Vue 3 用 Composition API 加script setup状态管理选 Pinia 不选 Vuex理由很简单Pinia 对 TypeScript 支持更好定义 store 的模板代码少一半。2.2 六张核心表的字段设计与关系绑定自媒体社区平台的表不必一次建十几个围绕内容、用户、互动三条线各建一到两张就够了。用户表user、内容表article、评论表comment、关注关系表follow、点赞表like_record、通知表notification这六张表能覆盖社区平台九成以上的功能点。CREATE TABLE article ( id bigint NOT NULL AUTO_INCREMENT, author_id bigint NOT NULL COMMENT 作者ID, author_name varchar(32) NOT NULL COMMENT 作者昵称冗余, title varchar(120) NOT NULL COMMENT 标题, content_html mediumtext NOT NULL COMMENT 清洗后的HTML, status tinyint NOT NULL DEFAULT 0 COMMENT 0草稿 1审核中 2已发布 3下架, read_count int NOT NULL DEFAULT 0 COMMENT 展示用阅读数, like_count int NOT NULL DEFAULT 0, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_status_time (status, create_time DESC), KEY idx_author_time (author_id, create_time DESC) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT内容表;这段建表语句里有三个值得说明的点。content_html存的是 Markdown 转换并清洗后的 HTML不要在页面加载时实时渲染 Markdown否则用户量上来后每次请求都会白耗一次转换时间。status字段用数字枚举而不是字符串排序和索引都能吃到类型优势。author_name是冗余字段目的就是避免文章列表页每次都要 JOIN 用户表取昵称自媒体平台的文章列表是高频读场景宁可容忍昵称修改后老文章展示旧名字。2.3 关注关系与点赞表的唯一键设计CREATE TABLE follow ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 粉丝, follow_id bigint NOT NULL COMMENT 被关注者, create_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_user_follow (user_id, follow_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT关注关系;关注表用联合唯一键uk_user_follow从数据库层面挡住重复关注代码里的INSERT只需要捕获 DuplicateKeyException 然后吞掉比先查后插少一次查询也避免并发下插重复。点赞表结构类似但多了target_type字段用来区分点赞的是文章还是评论。这里注意点赞表和关注表都不要把user_id单独建索引联合索引最左前缀已经能覆盖。热点数据的处理放到 Redis 而不是 MySQL。点赞状态、点赞数、浏览数这三个值是写多读少的典型场景直接写入 MySQL 会导致文章表频繁更新行锁。常见做法是先把点赞和浏览量放到 Redis再由一个定时任务批量刷回 MySQL后面第 3 章会给出具体代码。3. 后端 Spring Boot 实现登录、发布、Feed 流的关键代码3.1 JWT 加 Redis 的双层登录态设计纯 JWT 登录的问题在于 token 签发后服务端无法主动让它失效用户改了密码或者被管理员封号时老 token 还能继续用。自媒体平台的答辩追问很容易问到这一点所以更稳的做法是 JWT 签发后同时把 token 写入 Redis后续请求在拦截器里先查 Redis 再验签名。// JwtUtil.java import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import javax.crypto.SecretKey; import java.util.Date; public class JwtUtil { // jjwt 0.11.5 写法注意密钥长度必须大于 32 字节 private static final SecretKey KEY Keys.hmacShaKeyFor( 自媒体社区平台毕业设计密钥.getBytes()); private static final long EXPIRE 7 * 24 * 60 * 60 * 1000L; public static String createToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder().setSigningKey(KEY).build() .parseClaimsJws(token).getBody(); } }生成 token 后用同一个工具类解析即可。上面的createToken把 userId 放进 subject把角色放进 claim后续接口判断权限时从 Claims 里取 role 字段不需要再查一次数据库。登录接口里签发 token 后要同步写 Redispublic String login(String username, String password) { User user userMapper.selectOne(new LambdaQueryWrapperUser() .eq(User::getUsername, username)); if (user null || !BCrypt.checkpw(password, user.getPassword())) { throw new BizException(用户名或密码错误); } String token JwtUtil.createToken(user.getId(), user.getRole()); // key 用 login:token:{userId}TTL 与 JWT 过期时间一致 redisTemplate.opsForValue().set( login:token: user.getId(), token, 7, TimeUnit.DAYS); return token; }Redis 里存 token 拷贝的意义在于登出时delete(login:token: userId)管理员封号时同样删掉这个 key被删的用户下一次请求拦截器查不到对应记录直接被判定未登录。注意 JWT 的过期时间和 Redis 的 TTL 必须一致否则会出现 token 明明过期但 Redis 里还有残留的情况。3.2 内容发布流程Markdown 转换与 XSS 清洗文章发布不能把用户输入的 HTML 直接存库。即使前端编辑器做了转义攻击者绕过前端直接调接口塞一段script进来成本极低。用 Jsoup 的 Safelist 做白名单清洗是最稳妥的做法// ArticleService.java import org.jsoup.Jsoup; import org.jsoup.safety.Safelist; public Long publishArticle(ArticleCreateDTO dto, Long authorId) { // 1. Markdown 转 HTML常见选择 flexmark 或 commonmark-java String rawHtml markdownRenderer.render(dto.getContentMd()); // 2. 白名单清洗只保留 p/strong/img/a/ul/ol/li/blockquote 等标签 String safeHtml Jsoup.clean(rawHtml, Safelist.relaxed()); Article article new Article(); article.setAuthorId(authorId); article.setAuthorName(userService.getNickname(authorId)); article.setTitle(dto.getTitle()); article.setContentHtml(safeHtml); article.setStatus(0); // 进入草稿或审核流程 articleMapper.insert(article); return article.getId(); }Safelist.relaxed()会放行图片和链接但会自动移除onerror、onclick这类事件属性对自媒体平台的文章正文够用了。发布流程建议带审核状态机先落库为草稿管理端审核通过后置为已发布这样答辩时可以说做了内容安全上的设计而不是只有增删改查。3.3 Redis 在浏览量计数与 Feed 流中的用法首页信息流和热门榜单是自媒体平台性能最容易出丑的两个地方。浏览量计数用INCR命令// 每次请求文章详情时调用 String key read:article: articleId; Long count redisTemplate.opsForValue().increment(key); // 如果 key 不存在increment 会从 0 开始所以这里 count 就是当前阅读数 if (count % 20 0) { // 每 20 次写一次 MySQL读到的 article.readCount 只做展示 articleMapper.updateReadCount(articleId, count); }每 20 次写一次 MySQL 这种做法叫“缓冲后批量刷盘”目的是减少 DB 写次数。留言区经常问为什么不直接每次都写原因是一个热门文章的详情页在秒级可能被刷几十次直接 UPDATE 会让行锁竞争非常激烈。时间线 Feed 流用 ZSET 按发布时间排序// 发布文章成功后把文章 id 推给所有粉丝的收件箱 ListLong followerIds followMapper.selectFollowerIds(authorId); String json { articleId: articleId }; for (Long followerId : followerIds) { redisTemplate.opsForZSet().add( feed:user: followerId, json, System.currentTimeMillis()); } // 拉取时 ZREVRANGE 按时间倒序 SetString feeds redisTemplate.opsForZSet().reverseRange( feed:user: currentUserId, 0, 9);这个方案空间换时间每个粉丝的收件箱里提前放好文章入口拉取首页信息流时一次 Redis 操作就能拿到结果。粉丝多时循环写 ZSET 有性能问题但毕业设计体量下完全够用而且这个设计思路本身可以写进论文的设计方案里。4. Vue 3 前端axios 封装、路由守卫与 m3u8 视频播放落地4.1 创建项目与安装依赖的常见问题Vue 3 项目用 Vite 创建npm create vuelatest生成的脚手架自带 Vue Router 和 Pinia。实际安装依赖时常遇到两个问题一是 npm 版本太旧导致ERESOLVE报错二是node-sass卸载不干净造成安装失败。前者用npm install -g npmlatest升级后重试后者统一改用sassDart Sass 版本替代。npm create vuelatest media-community cd media-community npm install npm install axios hls.js代码结构按api/、views/、components/、stores/四个目录组织。api/目录下每个模块一个文件例如api/article.js导出一个对象所有请求都走统一封装的request实例。4.2 axios 拦截器与路由守卫// utils/request.js import axios from axios import { ElMessage } from element-plus import router from ../router const service axios.create({ baseURL: /api, // 开发环境走 Vite proxy生产环境走 Nginx 反向代理 timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) service.interceptors.response.use( res res.data, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push({ name: login }) } ElMessage.error(error.response?.data?.message || 请求失败) return Promise.reject(error) } )拦截器里把 401 统一跳转到登录页不用在每个页面写错误处理。baseURL用/api前缀而不是完整域名开发环境在vite.config.js里写 proxy 指向后端 8080 端口生产环境让 Nginx 把/api/转发给后端这样前端代码里永远不会暴露服务端地址。路由守卫检查登录态时只做本地 token 判断会有一个边界问题token 过期后前端不知道用户会看到一个白屏或一堆报错。常规做法是加一层 Promise 调后端GET /user/info校验 token 有效性校验失败再跳登录页。4.3 Vue 播放 m3u8hls.js 是最小可行方案自媒体社区平台的视频模块如果只支持 mp4 播放功能上显得单薄。很多源码里给了 m3u8 的链接但原生video标签并不能直接播 HLS 格式这里需要用 hls.js。template video refvideoRef controls classw-full rounded-lg bg-black/video /template script setup import { ref, onMounted } from vue import Hls from hls.js const props defineProps({ src: { type: String, required: true } }) const videoRef ref(null) onMounted(() { // 桌面 Chrome/Firefox 需要 hls.js 转封装 if (Hls.isSupported()) { const hls new Hls({ maxBufferLength: 30, enableWorker: true }) hls.loadSource(props.src) hls.attachMedia(videoRef.value) } else if (videoRef.value.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 HLS直接给 src 就能播 videoRef.value.src props.src } else { console.error(当前浏览器不支持 HLS) } }) /scriptmaxBufferLength控制的是缓冲 30 秒视频数据对短视频社区足够避免默认 60 秒导致首屏加载变慢。enableWorker: true让转封装跑在 Web Worker 线程避免长视频播放时卡顿 UI。播放 m3u8 最容易踩的坑不是前端代码而是后端没有给.m3u8和.ts片段配置跨域头。视频文件如果放在 OSS 或独立静态服务器上必须在响应头里加Access-Control-Allow-Origin: *否则浏览器拉取 ts 分片会被 CORS 拦截表现为视频黑屏控制台报错。自建存储则在 Nginx 或 Spring Boot 静态资源配置里加同样的头。5. 部署与调优让自媒体社区平台从“能跑”到“答辩不慌”5.1 前后端分离打包与 Nginx 反代配置项目在本地能跑只是第一步答辩现场百分之百会问到“你怎么部署的”。前端用npm run build打包出dist/目录后端mvn package -DskipTests打出 jar 包。两者不要放在同一个 Tomcat 里正确做法是用 Nginx 托管前端静态文件同时反代后端 API。server { listen 80; server_name your.domain.com; root /home/www/media-community/dist; index index.html; # Vue Router 使用 history 模式时所有非文件路径回退到 index.html location / { try_files $uri $uri/ /index.html; } # API 反向代理注意 /api/ 后的斜杠会把前缀去掉 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html这行是 Vue Router history 模式的关键不加的话用户直接访问/article/123刷新页面会报 404。代理里proxy_pass http://127.0.0.1:8080/;末尾的斜杠会把前端的/api前缀剥掉例如前端请求/api/article/list后端收到的实际路径是/article/list。后端接口里不要写死/api前缀两边职责对不上会排查很久。5.2 一条慢查询分页深翻页的等价改写文章列表分页在数据量过千后会出现越翻越慢的现象根源是 MySQL 的LIMIT 100000, 10需要扫描并丢弃前面 10 万行数据。-- 慢写法 SELECT * FROM article WHERE status 2 ORDER BY id DESC LIMIT 100000, 10; -- 快写法上一页最后一条记录的 id 做游标 SELECT * FROM article WHERE status 2 AND id #{lastId} ORDER BY id DESC LIMIT 10;游标分页的代价是前端不能直接调页码翻页需要把上一页最后一条的 id 传给后端。对自媒体社区的信息流场景恰好合适因为用户大多数只会往下滑很少从第 800 页往回跳。这里建索引也需要注意idx_status_time (status, create_time DESC)覆盖了按发布时间排序的查询但只按 id 排序时用主键索引即可避免额外建冗余索引。5.3 答辩追问的两个高频问题“这个项目的权限控制是怎么做的”回答思路分两层登录态用 JWT 加 Redis接口权限在 Spring Boot 拦截器里检查 role 字段管理端接口在注解层面用自定义RequireRole(admin)拦截。前端路由守卫只控制页面入口好不好看真正的安全边界在后端。“点赞数和真实数据不一致怎么办”直接回答 Redis 是最终一致每 20 次写回 MySQL 一次若服务重启最多丢失 19 条计数对展示类数据无伤大雅。这个设计叫“写缓冲”属于高频面试考点答出来比“我用了一个 count 字段”有信息量得多。6. 加一个 WebSocket 实时通知拉开与普通 CRUD 毕业设计的差距大多数自媒体社区平台源码里评论和关注都停留在“我刷新列表才能看到新内容”的阶段。只需要加一个 WebSocket 推送用户体验立刻上一个档次而且实现成本很低。Spring Boot 里用原生ServerEndpoint注解即可。// WebSocket 配置类Spring Boot 3.x 必须手动注册 ServerEndpointExporter Configuration public class WebSocketConfig { Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }// 通知端点 ServerEndpoint(/notification/{userId}) Component public class NoticeSocket { private static final MapLong, Session SESSIONS new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(userId) Long userId) { SESSIONS.put(userId, session); } OnClose public void onClose(PathParam(userId) Long userId) { SESSIONS.remove(userId); } public static void push(Long userId, String message) { Session session SESSIONS.get(userId); if (session ! null session.isOpen()) { session.getAsyncRemote().sendText(message); } } }评论落库后调用NoticeSocket.push(authorId, { type: COMMENT, articleId: 1 })对应在线用户立即收到通知不在线的用户下次登录拉取未读消息表。前端在登录后建立连接const token localStorage.getItem(token) const userId JSON.parse(localStorage.getItem(userInfo)).id const ws new WebSocket(ws://${location.host}/notification/${userId}) ws.onmessage (e) { const data JSON.parse(e.data) if (data.type COMMENT) { ElNotification({ title: 收到新评论, message: 你的文章收到一条新回复, type: success }) } }验证 WebSocket 是否生效最快的办法是开两个浏览器标签页一个登录作者账号停在被评论的文章页另一个登录普通账号发一条评论观察作者标签页右上角是否弹出通知。F12 的网络面板里能看到ws://开头的连接状态码 101这是 WebSocket 握手成功的标志。如果连接一直 pending 或返回 403检查前端拼接的 userId 是否经过编码以及 Spring Boot 端ServerEndpoint路径与请求路径是否完全一致。本文还有配套的精品资源点击获取
返回列表