简介:本资源是哈尔滨工程大学《应用软件架构设计》课程的大作业成果——防疫信息管理系统完整文档,面向高校计算机/软件工程专业学生及数据库与Web开发初学者,聚焦后疫情时代流动人口核酸检测信息的规范化、自动化管理需求。文档详述了系统设计目标、B/S架构选型(SpringBoot+MyBatis+MySQL)、三层分层实现、三类用户角色(普通用户、普通管理员、超级管理员)用例模型,以及索引、触发器、视图等数据库高级特性应用,兼具理论深度与工程实践价值。压缩包为单个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 重复 setAttribute;template-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 写法简洁,但 N+1 查询问题需通过
@EntityGraph或@Query显式解决。
2.2.1 MyBatis 多表关联查询的典型实现
以“获取用户核酸记录及所属社区名称”为例,在NucleicAcidRecordMapper.xml中:
<select id="selectWithCommunity" resultType="com.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} </select>resultType指向 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/add | POST | USER, ADMIN, SUPER_ADMIN | UserController.addUser() |
/admin/community | GET | ADMIN, SUPER_ADMIN | CommunityController.list() |
/sys/backup | POST | SUPER_ADMIN | DatabaseBakController.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; } // 从数据库加载该路径所需角色 List<String> 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表:
| id | name | url | parent_id | sort_order | role_required |
|---|---|---|---|---|---|
| 1 | 人员管理 | /admin/user | 0 | 1 | ADMIN,SUPER_ADMIN |
| 2 | 流动人口 | /admin/user/flow | 1 | 1 | ADMIN,SUPER_ADMIN |
| 3 | 常住人口 | /admin/user/resident | 1 | 2 | ADMIN,SUPER_ADMIN |
| 4 | 系统设置 | /sys/config | 0 | 5 | SUPER_ADMIN |
3.2.1 菜单树构建与前端渲染
MenuService.java中:
public List<Menu> buildMenuTree(String userRole) { List<Menu> allMenus = menuMapper.selectAll(); // 查询全部菜单 // 过滤当前用户角色可见的菜单 List<Menu> filtered = allMenus.stream() .filter(menu -> Arrays.asList(menu.getRoleRequired().split(",")).contains(userRole)) .collect(Collectors.toList()); // 构建树形结构(parent_id=0 为根节点) Map<Long, Menu> menuMap = filtered.stream() .collect(Collectors.toMap(Menu::getId, menu -> menu)); List<Menu> 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_ADMIN);buildMenuTree()返回嵌套结构,Controller 直接model.addAttribute("menus", menuService.buildMenuTree(role));- Freemarker 模板中递归渲染:
<#list menus as menu><@renderMenu menu/></#list>,renderMenu宏处理子菜单。
3.3 角色切换与 Session 隔离的边界处理
当超级管理员需临时以普通管理员身份操作时(如测试权限),系统提供“角色切换”功能,但绝不共享 Session:
@PostMapping("/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(pattern="yyyy-MM-dd") Date startDate, @RequestParam @DateTimeFormat(pattern="yyyy-MM-dd") Date endDate) { List<NucleicStat> 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 List<String> categories; // X轴日期 private List<Series> series; // Y轴数据序列 // getter/setter... } public class Series { private String name; private List<Integer> data; // getter/setter... }@ResponseBody确保返回 JSON,而非跳转视图;@DateTimeFormat自动解析2023-06-01字符串为Date对象,避免手动SimpleDateFormat;Series类封装图表数据结构,前端 ECharts 直接option.xAxis.data = data.categories。
4.1.1 前端 ECharts 初始化代码
nucleic-chart.ftl中:
<div id="chartContainer" style="width: 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()绑定 DOM,setOption()渲染,符合 ECharts 5.x 标准用法。
4.2 数据库备份与还原:不只是mysqldump,而是可审计的脚本化流程
DatabaseBakController.java提供 Web 界面操作,但底层调用的是封装好的BackupService:
@Service 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 + Logback,logback-spring.xml中配置:
<logger name="com.heu.controller" level="DEBUG"/> <logger name="com.heu.service" level="DEBUG"/> <logger name="com.heu.dao" level="DEBUG"/> <logger name="org.springframework.web.servlet.DispatcherServlet" level="WARN"/>com.heu.controllerDEBUG 级别可查看请求参数、返回值;com.heu.daoDEBUG 级别输出 MyBatis 执行的 SQL 及参数(需开启mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl);DispatcherServlet设为 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.ftl); - FreeMarker 配置是否生效:在
SpringBoot启动类上加@EnableWebMvc会覆盖默认配置,需手动注册FreeMarkerViewResolver; - Model 数据是否为空:在 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.33);application.yml中 URL 是否含serverTimezone=GMT%2B8(中文环境必需):
spring: datasource: url: jdbc:mysql://localhost:3306/heu_nucleic?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%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是两个独立字段,前者控制菜单显示,后者控制接口访问,二者需协同配置。
本文还有配套的精品资源,点击获取