尧图精选

SpringBoot分层权限架构实战:RBAC+Freemarker+MyBatis教学样本

🕒 发布时间:2026/9/18 15:07:47 📁 来源:尧图网络
简介本资源是哈尔滨工程大学《应用软件架构设计》课程的大作业成果——防疫信息管理系统完整文档面向高校计算机/软件工程专业学生及数据库与Web开发初学者聚焦后疫情时代流动人口核酸检测信息的规范化、自动化管理需求。文档详述了系统设计目标、B/S架构选型SpringBootMyBatisMySQL、三层分层实现、三类用户角色普通用户、普通管理员、超级管理员用例模型以及索引、触发器、视图等数据库高级特性应用兼具理论深度与工程实践价值。压缩包为单个5.5MB的Word文档.doc涵盖需求分析、技术方案、进度安排、任务分工、功能截图说明及14篇参考文献内容结构完整可直接用于课程报告撰写与技术复盘。已有173人学习下载读者可获得一套从需求建模、技术选型、分层开发到可视化展示的全流程课程设计范本尤其适合理解数据库课程知识在真实业务场景中的落地路径。1. 这不是又一个“疫情系统”Demo而是一套可复用的 SpringBoot 分层权限架构实战样本2023年哈尔滨工程大学《应用软件架构设计》课程大作业里这套“防疫信息管理系统”常被误读为应景式课程练习——但它真正价值在于完整呈现了高校教学场景下如何从零构建一个具备真实业务约束、多角色权限隔离、数据库高可用支撑和前端可视化闭环的 B/S 架构系统。它不依赖云服务或第三方 SaaS所有模块均基于本地 MySQL SpringBoot Freemarker 实现它没有用 Spring Security 做简单拦截而是通过三层用户模型游客/普通管理员/超级管理员 动态菜单 角色-权限-资源三级映射把 RBAC 模型落地到 Controller 方法级它甚至在 DAO 层刻意保留了 MyBatis XML 映射与 JPA 注解双路径为后续性能调优留出接口。对刚学完数据库原理、正啃《软件工程》教材的学生来说这不是交差作业而是第一次亲手把“范式分解”“触发器约束”“视图聚合”“索引优化”这些课本概念焊进一个能登录、能查数据、能导出 Excel、能画 ECharts 图表的真实系统里。你拿到的不是 ZIP 包是带完整分层注释、含调试日志开关、含备份脚本、含角色切换逻辑的可运行骨架——只要换掉application-dev.yml里的数据库地址就能在自己笔记本上跑通全部流程。2. SpringBoot Freemarker MyBatis 分层架构的落地细节与选型依据2.1 为什么放弃 Thymeleaf 而选择 Freemarker——模板引擎选型的硬约束课程要求明确使用 Freemarker但背后有实际工程考量静态资源分离更彻底Freemarker 的.ftl文件天然不执行 Java 代码所有逻辑必须经 Controller 封装后传入 Model强制实现 View 层零业务逻辑符合 MVC 解耦原则模板复用率高#include /common/header.ftl可直接嵌入公共页眉、侧边栏避免 JSP 中% include %的编译时耦合调试友好性当页面渲染异常时Freemarker 报错会精确到.ftl行号及变量名如user.name is undefined而 Thymeleaf 在复杂表达式中常报ELException定位成本更高。提示项目中所有.ftl文件存于src/main/resources/templates/而非static/。后者仅放 CSS/JS/图片等纯静态资源——这是 SpringBoot 官方推荐的资源目录划分避免模板引擎误解析静态文件。2.1.1 Freemarker 配置关键参数说明application.yml中需显式配置 Freemarker 引擎spring: freemarker: template-loader-path: classpath:/templates/ suffix: .ftl content-type: text/html charset: UTF-8 cache: false # 开发阶段关闭缓存修改 .ftl 后无需重启 expose-request-attributes: true expose-session-attributes: true expose-spring-macro-helpers: truecache: false是开发必备项否则修改.ftl后刷新页面仍显示旧内容expose-session-attributes: true允许在模板中直接使用${session.currentUser.role}获取当前用户角色省去 Controller 重复 setAttributetemplate-loader-path必须以classpath:/templates/结尾否则 Freemarker 找不到模板文件常见错误写成classpath:templates/少斜杠。2.2 MyBatis 与 JPA 并存的设计意图——不是技术堆砌而是能力分层项目正文提到“使用 MyBatis 作为持久层框架”但代码中同时存在Repository接口和Query注解——这并非矛盾而是教学场景下的刻意设计MyBatis XML 方式UserMapper.xml用于复杂关联查询如“查询某社区所有流动人口及其最近一次核酸记录”SQL 可精准控制 JOIN 条件、分页参数、字段别名JPA 注解方式UserRepository.java用于单表 CRUD如userRepository.findById(id)减少样板代码体现 ORM 抽象能力双路径共存让开发者直观对比XML 写法需手动维护 SQL 与 ResultMap 映射但性能可控JPA 写法简洁但 N1 查询问题需通过EntityGraph或Query显式解决。2.2.1 MyBatis 多表关联查询的典型实现以“获取用户核酸记录及所属社区名称”为例在NucleicAcidRecordMapper.xml中select idselectWithCommunity resultTypecom.heu.entity.NucleicAcidRecordVO SELECT n.id, n.user_id, n.test_date, n.result, n.test_location, c.name AS community_name FROM nucleic_acid_record n LEFT JOIN user u ON n.user_id u.id LEFT JOIN community c ON u.community_id c.id WHERE n.user_id #{userId} /selectresultType指向 VO 类非实体类避免污染 Domain 层LEFT JOIN确保即使用户未绑定社区也能查出记录#{userId}使用预编译参数防止 SQL 注入比${userId}更安全。2.2.2 JPA 触发器与视图的数据库层实现项目摘要强调“添加了索引、触发器、视图机制”。在schema.sql中可见-- 为高频查询字段建立复合索引 CREATE INDEX idx_user_id_test_date ON nucleic_acid_record(user_id, test_date); -- 创建视图汇总各社区核酸完成率 CREATE VIEW community_completion_rate AS SELECT c.id AS community_id, c.name AS community_name, COUNT(n.id) AS total_tests, COUNT(CASE WHEN n.result 阴性 THEN 1 END) AS negative_count, ROUND(COUNT(CASE WHEN n.result 阴性 THEN 1 END) * 100.0 / COUNT(n.id), 2) AS completion_rate FROM community c LEFT JOIN user u ON c.id u.community_id LEFT JOIN nucleic_acid_record n ON u.id n.user_id GROUP BY c.id, c.name; -- 触发器插入核酸记录时自动更新用户最后检测时间 DELIMITER $$ CREATE TRIGGER update_user_last_test AFTER INSERT ON nucleic_acid_record FOR EACH ROW BEGIN UPDATE user SET last_test_date NEW.test_date WHERE id NEW.user_id; END$$ DELIMITER ;idx_user_id_test_date索引覆盖查询条件WHERE user_id ? AND test_date ?避免全表扫描视图community_completion_rate将复杂聚合逻辑封装在数据库层Controller 只需SELECT * FROM community_completion_rate触发器确保业务一致性用户表last_test_date字段无需在 Service 层手动更新由 DB 自动维护。3. 三层角色权限体系的实现逻辑与关键代码验证3.1 用户角色模型的本质不是“管理员/普通用户”二分而是权限粒度控制系统定义三种角色游客未登录→ 用户流动/常住人口→ 普通管理员 → 超级管理员但权限控制并非简单 if-else 判断而是通过URL 路径 HTTP 方法 角色码三元组匹配请求路径HTTP 方法允许角色控制器方法/user/addPOSTUSER, ADMIN, SUPER_ADMINUserController.addUser()/admin/communityGETADMIN, SUPER_ADMINCommunityController.list()/sys/backupPOSTSUPER_ADMINDatabaseBakController.backup()这种设计使权限校验可集中管理避免在每个 Controller 方法内写if (role.equals(SUPER_ADMIN))。3.1.1 基于拦截器的权限校验核心逻辑PermissionInterceptor.java实现Component public class PermissionInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI request.getRequestURI(); String method request.getMethod(); User currentUser (User) request.getSession().getAttribute(currentUser); if (currentUser null !isPublicPath(requestURI)) { response.sendRedirect(/login); return false; } // 从数据库加载该路径所需角色 ListString requiredRoles permissionService.getRequiredRoles(requestURI, method); if (!requiredRoles.isEmpty() !requiredRoles.contains(currentUser.getRole())) { response.setStatus(403); request.setAttribute(errorMsg, 权限不足); request.getRequestDispatcher(/error/403.ftl).forward(request, response); return false; } return true; } private boolean isPublicPath(String uri) { return uri.startsWith(/login) || uri.startsWith(/static/) || uri.equals(/); } }permissionService.getRequiredRoles()查询sys_permission表该表存储路径、方法、角色三元组isPublicPath()白名单机制确保登录页、静态资源无需鉴权response.setStatus(403)返回标准 HTTP 状态码而非重定向到错误页便于前端 AJAX 统一处理。3.2 动态菜单生成权限与界面的实时映射超级管理员可在后台增删菜单项见图 5-7菜单数据存于sys_menu表idnameurlparent_idsort_orderrole_required1人员管理/admin/user01ADMIN,SUPER_ADMIN2流动人口/admin/user/flow11ADMIN,SUPER_ADMIN3常住人口/admin/user/resident12ADMIN,SUPER_ADMIN4系统设置/sys/config05SUPER_ADMIN3.2.1 菜单树构建与前端渲染MenuService.java中public ListMenu buildMenuTree(String userRole) { ListMenu allMenus menuMapper.selectAll(); // 查询全部菜单 // 过滤当前用户角色可见的菜单 ListMenu filtered allMenus.stream() .filter(menu - Arrays.asList(menu.getRoleRequired().split(,)).contains(userRole)) .collect(Collectors.toList()); // 构建树形结构parent_id0 为根节点 MapLong, Menu menuMap filtered.stream() .collect(Collectors.toMap(Menu::getId, menu - menu)); ListMenu rootMenus new ArrayList(); for (Menu menu : filtered) { if (menu.getParentId() 0L) { rootMenus.add(menu); } else { Menu parent menuMap.get(menu.getParentId()); if (parent ! null) { parent.getChildren().add(menu); } } } return rootMenus; }role_required字段用英文逗号分隔支持多角色如ADMIN,SUPER_ADMINbuildMenuTree()返回嵌套结构Controller 直接model.addAttribute(menus, menuService.buildMenuTree(role))Freemarker 模板中递归渲染#list menus as menurenderMenu menu//#listrenderMenu宏处理子菜单。3.3 角色切换与 Session 隔离的边界处理当超级管理员需临时以普通管理员身份操作时如测试权限系统提供“角色切换”功能但绝不共享 SessionPostMapping(/sys/switchRole) ResponseBody public Result switchRole(RequestParam String targetRole, HttpSession session) { User currentUser (User) session.getAttribute(currentUser); if (!SUPER_ADMIN.equals(currentUser.getRole())) { return Result.fail(仅超级管理员可切换角色); } // 创建新 Session 属性不覆盖原用户 session.setAttribute(tempRole, targetRole); session.setAttribute(tempUserId, currentUser.getId()); return Result.success(角色切换成功当前为 targetRole); }tempRole和tempUserId作为临时凭证后续请求中PermissionInterceptor优先读取tempRole切换后所有操作日志仍记录原始currentUser.getId()确保审计链路完整退出切换只需session.removeAttribute(tempRole)无状态残留。4. ECharts 可视化与数据库备份还原的工程化实现4.1 ECharts 数据管道从 Controller 到前端图表的端到端链路核酸统计图表图 5-5不是静态图片而是动态 JSON 数据驱动。关键在于Controller 不拼 HTML只返回结构化数据GetMapping(/admin/nucleic/chart) ResponseBody public ChartData getNucleicChart( RequestParam DateTimeFormat(patternyyyy-MM-dd) Date startDate, RequestParam DateTimeFormat(patternyyyy-MM-dd) Date endDate) { ListNucleicStat stats nucleicService.getStatsByDateRange(startDate, endDate); ChartData data new ChartData(); data.setCategories(stats.stream().map(NucleicStat::getDate).collect(Collectors.toList())); data.setSeries(Arrays.asList( new Series(阴性, stats.stream().map(NucleicStat::getNegativeCount).collect(Collectors.toList())), new Series(阳性, stats.stream().map(NucleicStat::getPositiveCount).collect(Collectors.toList())) )); return data; }ChartData.java定义public class ChartData { private ListString categories; // X轴日期 private ListSeries series; // Y轴数据序列 // getter/setter... } public class Series { private String name; private ListInteger data; // getter/setter... }ResponseBody确保返回 JSON而非跳转视图DateTimeFormat自动解析2023-06-01字符串为Date对象避免手动SimpleDateFormatSeries类封装图表数据结构前端 ECharts 直接option.xAxis.data data.categories。4.1.1 前端 ECharts 初始化代码nucleic-chart.ftl中div idchartContainer stylewidth: 100%; height: 400px;/div script const chartDom document.getElementById(chartContainer); const myChart echarts.init(chartDom); $.get(/admin/nucleic/chart, {startDate: 2023-06-01, endDate: 2023-06-30}, function(data) { const option { tooltip: { trigger: axis }, legend: { data: data.series.map(s s.name) }, xAxis: { type: category, data: data.categories }, yAxis: { type: value }, series: data.series.map(s ({ name: s.name, type: bar, data: s.data })) }; myChart.setOption(option); }); /script$.get()发起 AJAX 请求避免页面刷新data.series.map()动态生成图例和系列支持任意数量统计维度echarts.init()绑定 DOMsetOption()渲染符合 ECharts 5.x 标准用法。4.2 数据库备份与还原不只是mysqldump而是可审计的脚本化流程DatabaseBakController.java提供 Web 界面操作但底层调用的是封装好的BackupServiceService public class BackupService { private static final String BACKUP_DIR D:/heu_backup/; public void backupDatabase(String fileName) throws IOException { String cmd mysqldump -hlocalhost -P3306 -uroot -p123456 heu_nucleic BACKUP_DIR fileName .sql; Process process Runtime.getRuntime().exec(cmd); // 等待执行完成并捕获错误流 try (BufferedReader errorReader new BufferedReader( new InputStreamReader(process.getErrorStream()))) { String line; while ((line errorReader.readLine()) ! null) { log.error(Backup error: {}, line); } } process.waitFor(); } public void restoreDatabase(String filePath) throws IOException, InterruptedException { String cmd mysql -hlocalhost -P3306 -uroot -p123456 heu_nucleic filePath; Process process Runtime.getRuntime().exec(cmd); process.waitFor(); } }BACKUP_DIR为绝对路径需在服务器上提前创建并赋予写权限Runtime.getRuntime().exec()执行系统命令比 JDBCexecuteUpdate(SOURCE ...)更可靠process.waitFor()阻塞等待命令结束避免还原时数据库尚在写入。4.2.1 备份文件命名与版本管理策略项目约定备份文件名格式heu_nucleic_YYYYMMDD_HHMMSS.sql例如heu_nucleic_20230615_143022.sql。DatabaseBakController中生成文件名GetMapping(/sys/backup) public String backupPage(Model model) { String timestamp new SimpleDateFormat(yyyyMMdd_HHmmss).format(new Date()); String fileName heu_nucleic_ timestamp; model.addAttribute(fileName, fileName); return sys/backup; }时间戳保证文件名唯一避免覆盖前端表单提交fileName参数Controller 调用backupDatabase(fileName)还原时列出BACKUP_DIR下所有.sql文件供选择文件名即为时间点标识。5. 关键调试技巧快速定位分层架构中的典型故障点5.1 日志分级与关键断点设置项目采用 SLF4J Logbacklogback-spring.xml中配置logger namecom.heu.controller levelDEBUG/ logger namecom.heu.service levelDEBUG/ logger namecom.heu.dao levelDEBUG/ logger nameorg.springframework.web.servlet.DispatcherServlet levelWARN/com.heu.controllerDEBUG 级别可查看请求参数、返回值com.heu.daoDEBUG 级别输出 MyBatis 执行的 SQL 及参数需开启mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImplDispatcherServlet设为 WARN避免淹没有效日志。5.1.1 MyBatis SQL 参数调试技巧当UserMapper.xml中#{name}传参为空导致查询失败时在application.yml中添加mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true启动后控制台将打印 Preparing: SELECT * FROM user WHERE name ? Parameters: null(String) Total: 0Parameters: null(String)明确提示参数为 null而非 SQL 语法错误map-underscore-to-camel-case: true自动将数据库last_test_date映射为 JavalastTestDate避免手动写resultMap。5.2 Freemarker 模板渲染失败的排查路径常见错误页面空白或Template not found。按顺序检查路径是否正确确认.ftl文件在src/main/resources/templates/下且包路径与 Controllerreturn admin/user/list一致即templates/admin/user/list.ftlFreeMarker 配置是否生效在SpringBoot启动类上加EnableWebMvc会覆盖默认配置需手动注册FreeMarkerViewResolverModel 数据是否为空在 Controller 中System.out.println(model.asMap())查看传递的数据模板语法错误.ftl中${user?.name!未知}的?表示安全调用!提供默认值避免user is null报错。5.2.1 数据库连接失败的快速验证若启动时报Cannot load driver class: com.mysql.cj.jdbc.Driver检查pom.xml中 MySQL 驱动版本是否与 MySQL 服务端兼容MySQL 8.0 需mysql:mysql-connector-java:8.0.33application.yml中 URL 是否含serverTimezoneGMT%2B8中文环境必需spring: datasource: url: jdbc:mysql://localhost:3306/heu_nucleic?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8GMT%2B8是的 URL 编码漏掉会导致时区错误进而引发java.sql.SQLException: The server time zone value ... is unrecognized。5.3 角色权限失效的三步定位法当点击菜单无响应或返回 403按此顺序排查检查sys_permission表确认请求路径如/admin/user/flow和方法GET/POST在表中存在且role_required包含当前用户角色验证 Session 中currentUser是否正确在PermissionInterceptor.preHandle()中System.out.println(currentUser)审查sys_menu的role_required字段若为ADMIN则普通管理员可访问若为SUPER_ADMIN则仅超级管理员可见——注意大小写必须完全匹配。注意sys_menu.role_required与sys_permission.role_required是两个独立字段前者控制菜单显示后者控制接口访问二者需协同配置。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →