ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot+Vue的学校热点新闻推送系统:RBAC权限设计与实践

基于SpringBoot+Vue的学校热点新闻推送系统:RBAC权限设计与实践 简介一份基于 Vue、Spring Boot 与 MySQL 的学校热点新闻推送系统毕业设计资源面向需要完成课程设计或毕业设计的计算机专业学生也适合想学习前后端分离项目结构、RBAC 权限模型及 Vue 与 Spring Boot 整合的中级开发者。系统以热点新闻管理为核心覆盖热点新闻、留言、评论、收藏业务模块同时内置用户、部门、角色、菜单、日志、数据字典、文件管理、图表展示等基础功能权限方面采用基于角色的访问控制支持自定义角色并分配权限按钮级细粒度控制可满足学校管理员、学生等不同角色的差异化使用需求。压缩包约 7.09MB携带便捷适合下载后对照学习或作为毕业设计、课程设计及权限控制相关二次开发的参考蓝本。已有 354 人学习下载可用于毕设选题、项目实训阶段深入研读与功能扩展。1. 学校热点新闻推送系统的技术定位与选题边界学校热点新闻系统和普通资讯站最大的区别不在新闻的增删改查而在「谁能发、谁能删、谁能审」。用 Java Vue SpringBoot MySQL 这套技术栈做热点新闻推送比单纯写 CRUD 有意义的地方也在这里管理端负责 RBAC 权限控制学生端负责热点新闻的评论、收藏、留言互动。系统自带用户、部门、角色、菜单、日志、数据字典、文件、图表等基础模块权限可精确到按钮级别也支持自定义角色分配权限。它的边界很清晰适合三类人正在选毕业设计题目的 Java 方向学生要给院系或社团搭校园信息发布平台的人以及想在简历里突出全栈能力和权限设计、准备 java 与 springboot 面试题的求职者。2. SpringBootMyBatis 的 RBAC 权限模型与按钮级校验实现2.1 五张权限表的建模与关系这套系统的权限核心不是把角色字段塞进用户表而是用标准 RBAC 的五张表用户表 sys_user、角色表 sys_role、菜单表 sys_menu以及两张关联表 sys_user_role、sys_role_menu。菜单表里同时存目录、页面和按钮用 menu_type 字段区分1 是目录2 是菜单页3 是按钮。按钮的权限标识放在 perms 字段例如 news:add 表示新增热点新闻news:delete 表示删除热点新闻。建表时几个容易踩的细节菜单表里 parent_id 要默认 0 而不是 NULL否则前端组装树形组件时要多写一层判空perms 字段要加唯一索引因为后端做按钮校验时是直接拿字符串匹配的重复标识会让权限判断失去意义。标准建表语句如下CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0 NOT NULL, menu_name VARCHAR(64) NOT NULL, menu_type TINYINT NOT NULL COMMENT 1 目录, 2 菜单, 3 按钮, perms VARCHAR(100) DEFAULT NULL COMMENT 权限标识, 如 news:add, path VARCHAR(200) DEFAULT NULL, component VARCHAR(200) DEFAULT NULL, sort_no INT DEFAULT 0, UNIQUE KEY uk_perms (perms) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;parent_id 用 0 做根节点目录和菜单都挂在根下perms 只对按钮有实际意义目录和菜单可以留空。这个表设计同时服务两端管理端用 menu_type 为 1 和 2 的记录渲染左侧菜单树后端用 menu_type 为 3 的记录做接口权限校验同一张表喂两个场景避免建两张结构几乎一样的表。2.2 登录后权限集合的组装与缓存策略用户权限集合如果每次请求都现算会连着 join 四张表接口一多压力就上来了。常见做法是登录成功后一次性查出当前用户全部权限标识放到内存或 Redis 里。查询 SQL 是标准的 RBAC 五表关联SELECT DISTINCT m.perms FROM sys_user u JOIN sys_user_role ur ON u.id ur.user_id JOIN sys_role_menu rm ON ur.role_id rm.role_id JOIN sys_menu m ON rm.menu_id m.id WHERE u.id #{userId} AND m.perms IS NOT NULL这条 SQL 返回的结果会存到一个线程安全的用户上下文里后续拦截器只从上下文取值不再回查数据库。单体部署时可以直接用 ConcurrentHashMap 做缓存key 用 login:perms:userId如果以后拆成多实例再换成 Redis。我一般会顺手把用户基本信息也存进上下文这样日志模块记录操作人时不用再按 userId 查一次用户表。刷新权限的时机也很明确管理员修改角色菜单后把该角色下所有用户的缓存 key 删掉下次请求自动重建。2.3 基于注解的按钮级权限校验后端实现按钮级权限最清晰的方案是自定义注解加拦截器。定义一个RequiresPermission注解参数就是权限标识字符串Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface RequiresPermission { String value(); }Controller 上使用PostMapping(/news) RequiresPermission(news:add) public ResultVoid addNews(RequestBody News news) { newsService.add(news); return Result.success(); }拦截器里做校验public class PermissionInterceptor extends HandlerInterceptorAdapter { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; // 静态资源和预检请求直接放行 } HandlerMethod handlerMethod (HandlerMethod) handler; RequiresPermission annotation handlerMethod.getMethodAnnotation(RequiresPermission.class); if (annotation null) { return true; // 未标注注解的接口按公开接口处理 } ListString perms UserContext.getCurrentPerms(); if (!perms.contains(annotation.value())) { throw new PermissionDeniedException(无访问权限: annotation.value()); } return true; } }这里有个容易被忽略的细节判断 handler 是否为 HandlerMethod 这一步不能省否则 Swagger 接口、静态资源请求进来时 handler 不是 HandlerMethod 类型强转会直接抛 ClassCastException。另外注意注解的默认策略是「没有注解就放行」开发阶段方便调试但上线前要逐个接口检查敏感操作是否都补了权限标注漏一个就是越权风险点。2.4 前端菜单树与权限标识的映射策略权限标识的设计直接决定前后端联动的成本。拿热点新闻管理页举例在 sys_menu 中会插入一条 menu_type 为 2 的菜单记录path 为 /newscomponent 为 news/index下面再挂若干 menu_type 为 3 的按钮记录。按钮标识建议统一用「模块:动作」命名权限标识所在模块动作说明news:list热点新闻查询列表与详情news:add热点新闻发布热点新闻news:edit热点新闻修改已发布内容news:delete热点新闻下架或删除新闻news:comment热点评论发表评论news:favorite热点收藏收藏与取消收藏这套命名在后端按前缀判断归属模块在前端按冒号切分后绑定按钮显隐两侧只要共用同一份权限字典就不会出现「按钮能看见但接口 403」或「接口能调通但按钮不显示」的问题。常见误用是让前端根据角色名判断按钮显隐角色一旦自定义就不起作用而这套系统支持自定义角色分配权限所以一切判断都必须以 perms 字符串为准。3. VueElement UI 动态路由与热点列表页数据流设计3.1 动态路由的组装与菜单过滤前端不能把菜单写死否则管理员改了角色权限学生端刷新后依然能看到被回收的菜单。常见做法是登录后从后端拉取当前用户的菜单树转成 Vue Router 需要的路由对象再通过 addRoute 动态注册。路由守卫里做前置判断router.beforeEach(async (to, from, next) { const token getToken() if (!token) { next(/login) return } if (store.state.user.permissions.length 0) { const user await store.dispatch(user/getInfo) const routes buildRoutes(user.menus) routes.forEach(route router.addRoute(route)) next({ ...to, replace: true }) return } next() })buildRoutes 的核心工作是把后端菜单记录里的 component 字符串映射到真实组件对象。直接用 import 的 glob 形式一次性注册而不是手写 require 路径const modules import.meta.glob(../views/**/*.vue) function buildRoutes(menus) { return menus.filter(m m.menuType 2).map(m ({ path: m.path, name: m.path.replaceAll(/, ), component: modules[../views/${m.component}.vue] })) }menuType 过滤很关键目录和按钮不需要生成路由只把类型为 2 的菜单页注册进去。用 import.meta.glob 的好处是 Vite 构建时能静态分析出所有组件打包后不会因为动态字符串导致路由对应组件找不到、页面白屏。这里也顺便处理了 vue 路由参数的问题动态路由里的查询参数通过route.query读取列表页搜索关键字就是走这条链路。3.2 axios 封装与热点列表的分页参数热点列表页涉及三个核心请求参数pageNum、pageSize、keyword。axios 实例统一设置 baseURL 和 token 请求头响应拦截器对 401 做跳登录处理const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { config.headers[Authorization] getToken() return config }) service.interceptors.response.use( response response.data.data, error { if (error.response error.response.status 401) { store.commit(user/logout) router.push(/login) } return Promise.reject(error) } )列表接口函数把分页参数显式透传export function getHotNewsList(params) { return service.get(/news/list, { params: { pageNum: params.pageNum, pageSize: params.pageSize, keyword: params.keyword || } }) }接口返回的数据结构是{ rows: [], total: 0 }el-pagination 组件直接绑定 total 即可。一个容易被忽略的点响应拦截器把 response.data.data 直接剥出来了分页组件拿到的就是业务数据而不是 axios 的 response 对象组件里 data 赋值少一层嵌套。代价是类型提示会丢但做毕设和内部系统这个取舍值得调试时少踩 undefined 的坑。3.3 v-permission 指令控制按钮显隐前端按钮级权限不能只靠 v-if 手写判断正确的姿势是自定义指令。全局注册一个 permission 指令在元素插入 DOM 时检查当前用户权限集合Vue.directive(permission, { inserted(el, binding) { const required binding.value const permissions store.state.user.permissions if (required !permissions.includes(required)) { el.parentNode el.parentNode.removeChild(el) } } })页面上直接使用el-button v-permissionnews:add typeprimary clickopenAddDialog 发布热点/el-button这个方案有两个注意点第一Vue 3 里指令生命周期从 inserted 改成了 mounted写法要跟着框架版本走第二按钮如果本身还有 v-if 条件指令插入和元素插入的时序要保证权限集合已经加载完成所以路由守卫里必须先 await 完 getInfo 再放行进页面否则 permissions 还是空数组所有按钮都会被整个删掉。与后端权限字典保持一致的对照关系如下权限标识后端用法前端用法news:list查询列表接口放行校验列表页不控显隐news:addController 注解校验发布按钮 v-permissionnews:edit修改接口校验编辑按钮 v-permissionnews:delete删除接口校验删除按钮 v-permissionnews:comment评论接口校验评论提交按钮 v-permission从前端路由守卫到按钮指令这一层本质是在复刻后端权限模型。真正容易出问题的不是单点而是两侧不同步后端改了 perms 标识前端指令没跟着改按钮显示了但接口 403前端改了后端没改接口能调但按钮不显示。两端始终用同一份权限字典是这套系统稳定运行的前提。4. 热点评论、留言与收藏模块的表设计和事务边界4.1 评论模块的索引设计与防重复提交热点新闻下的评论表不能只设计成 id、news_id、content 三列的极简结构实际场景里要区分「根评论」和「子回复」还要支持后台审核状态。适合直接落地的结构如下CREATE TABLE news_comment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, news_id BIGINT NOT NULL, user_id BIGINT NOT NULL, parent_id BIGINT DEFAULT 0 COMMENT 0 表示根评论, content VARCHAR(500) NOT NULL, status TINYINT DEFAULT 1 COMMENT 1 显示, 0 隐藏, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_news_time (news_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE news_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, content TEXT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0 未读, 1 已读, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;评论列表页是典型的「先查列表再补详情」场景先按新闻 ID 分页查根评论再按根评论 ID 集合一次性 IN 查询子评论在内存里按 parentId 分组避免逐条查子评论造成 N1。idx_news_time 这个联合索引让「查某条新闻的最新评论」能走索引回表而不是全表扫描。分页深了之后limit 10000, 20 这种深分页会越来越慢后续优化方向是改成 create_time 游标翻页。评论和留言的 status 字段是给审核流程用的管理员在后台通过后状态置 1学生端才可见。这个状态流转要记到日志模块方便回溯是哪位管理员、在什么时间点的审核对应到 sys_log 里就是一条 module 为 news_comment、action 为 audit 的操作记录。4.2 收藏模块的幂等设计与事务边界收藏是典型的幂等敏感操作用户快速双击「收藏」按钮会产生两个并发请求如果只在前端做节流后端依然可能插入两条重复记录。最底层的兜底是表结构上直接加唯一索引CREATE TABLE news_favorite ( id BIGINT PRIMARY KEY AUTO_INCREMENT, news_id BIGINT NOT NULL, user_id BIGINT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_news_user (news_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;后端方法用事务控制收藏关系与收藏计数的同步Transactional(rollbackFor Exception.class) public void toggleFavorite(Long newsId, Long userId) { Favorite favorite favoriteMapper.selectByNewsAndUser(newsId, userId); if (favorite ! null) { favoriteMapper.deleteById(favorite.getId()); newsMapper.decreaseFavoriteCount(newsId); } else { favoriteMapper.insert(newsId, userId); newsMapper.increaseFavoriteCount(newsId); } }这里的事务边界非常明确收藏关系记录和新闻表里的 favorite_count 必须同时成功或同时回滚。否则会出现用户已收藏但计数没加或者计数加了但收藏关系没落库的数据不一致。唯一索引在并发下会抛 DuplicateKeyException捕获后返回「已收藏请勿重复操作」比引入分布式锁简单得多。对学生端个人中心来说收藏列表就是按创建时间倒序查 news_favorite 再关联新闻表数据量不大单表查询足够。4.3 留言模块的内容过滤与状态流转热点留言模块的处理链路是学生提交留言、内容过滤、入库、管理员审核、展示。内容过滤不能只靠前端 hidden 校验后端必须过滤一次。常见做法是启动时加载敏感词文件到内存用多模式匹配算法过滤命中后替换为星号。项目里不引入额外重量级依赖的话可以用 hutool 的 SensitiveUtil内置敏感词库也支持自定义词库。Component public class SensitiveWordFilter { public String filter(String text) { if (text null || text.length() 0) { return text; } // 命中敏感词后统一替换为 *避免前端展示时出现跳过判断的越权内容 return SensitiveUtil.getSensitiveWordReplace(text, *); } }这段逻辑的作用是在留言和评论入库前统一做一道拦截替换后再走正常的业务校验。注意 SensitiveUtil 匹配的是自定义词库文件里的词条部署新词库后需要重启或提供热加载刷新接口否则词库更新不生效。4.4 业务状态字段与数据字典的对应关系系统带的数据字典模块主要就是管理这类状态字段的可视化映射。页面展示时如果直接 if-else 写死状态文字后面加一种状态就要改前端代码而数据字典把「代码值」和「展示文案」解耦了字段取值含义数据字典类型news_comment.status0隐藏comment_statusnews_comment.status1显示comment_statusnews_message.status0未读message_statusnews_message.status1已读message_status后端返回给前端时可以通过字典类型加代码值查询文案前端只渲染从 dict 接口拿到的 label不再硬编码状态文字。新增状态只需要在字典表里加一条记录管理端的数据字典管理页面就能实时生效。5. 用 sys_log 和聚合查询验证热点推送效果5.1 从日志表统计推送点击率日志模块记录了每个用户的关键操作包括热点新闻的查看、评论、收藏。要验证「推送到底有没有效果」直接对 sys_log 做聚合查询SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(DISTINCT user_id) AS uv, COUNT(*) AS pv FROM sys_log WHERE module news AND action view GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY day DESC;COUNT(DISTINCT user_id) 和 COUNT(*) 的差值能看出单日访问是否被个别用户刷起来uv 低但 pv 高说明来了几个深度阅读的用户uv 高但 pv 低说明内容覆盖到了人但没人点进去需要优化标题和封面。5.2 图表展示模块的日期聚合口径后台首页的图表模块通常渲染近 7 天的新增新闻量和收藏量两条 SQL 分别按天聚合在服务端合成 List 返回给 ECharts。折线图只需要 x 轴日期数组和 y 轴数量数组后端返回[{ day: 2025-03-01, count: 12 }]这种结构前端用 map 拆开即可。按新闻维度排行时用一条分组查询SELECT news_id, COUNT(*) AS favorite_count FROM news_favorite GROUP BY news_id ORDER BY favorite_count DESC LIMIT 10;5.3 用 EXPLAIN 验证热点列表的索引命中热点列表如果出现慢查询先用 EXPLAIN 看执行计划确认索引是否生效EXPLAIN SELECT * FROM news_comment WHERE news_id 10086 ORDER BY create_time DESC LIMIT 20;type 是 ref 说明命中了 idx_news_time 联合索引type 是 ALL 说明索引没建上或查询条件没走到索引。容易被忽视的一点是ORDER BY create_time 要和 WHERE 的 news_id 组成联合索引否则 MySQL 回表后还要做 filesort数据量过了十万之后排序会变成明显瓶颈。验证完索引还不够最好把这条慢查询的完整语句记录到日志表里因为这个动作本身就是 sys_log 模块的典型用法日志不只是给管理员看的操作记录更是排查性能问题和统计业务指标的数据源。本文还有配套的精品资源点击获取
返回列表