ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue宿舍管理系统实战:多角色流程与前后端分离架构设计

SpringBoot+Vue宿舍管理系统实战:多角色流程与前后端分离架构设计 你有没有遇到过这样的场景接手一个学校项目需求文档上写着“学生宿舍管理系统”听起来简单直接——不就是增删改查吗但当你真正开始动手才发现里面全是细节学生调宿申请怎么流转宿舍水电费如何自动计算和提醒维修报修流程怎么跟踪辅导员、宿管、学生、后勤不同角色看到的页面和权限天差地别。更头疼的是项目周期紧技术栈要选主流、易维护、前后端分离还得考虑后续可能的扩展。这不只是一个简单的管理系统它本质上是一个多角色、多状态、多流程的微型业务中台。很多人一上来就埋头写Student和Dormitory的 CRUD结果代码写到一半发现审批逻辑和业务状态耦合得一塌糊涂前端页面也跟着越改越乱。今天我们就以“基于 SpringBoot Vue 的高校学生宿舍管理系统”这个经典组合为例抛开那些泛泛而谈的“框架介绍”直接切入核心如何把一个看似普通的业务需求拆解成清晰、可维护、前后端协同的技术实现方案。我们不仅要“做出来”更要思考为什么这样设计以及在实际开发中哪些地方最容易“踩坑”。1. 为什么说宿舍管理系统是“业务逻辑的试金石”很多人低估了这类系统的复杂性认为它不过是数据库表的增删改查。但恰恰是这种涉及多角色、多状态流转和简单工作流的系统最能检验一个开发者是否具备基本的业务抽象和工程化思维。1.1 从“静态数据”到“动态流程”的思维转变一个最朴素的想法可能是有学生表、宿舍表、宿舍楼表然后做关联查询。这只能解决“信息登记”问题。真正的业务是从“学生申请调宿”开始的学生提交申请选择目标宿舍、填写理由。辅导员审批查看申请可以同意或驳回。宿管员确认辅导员同意后宿管需要核实目标宿舍是否有空床位并执行系统内的床位分配。状态同步与通知每一步状态变更如“待审批”、“已通过”、“已分配”、“已入住”都需要实时反馈给学生并可能触发消息通知。你看仅仅一个“调宿”就涉及至少三张表申请记录、审批流、床位状态和四个状态。如果你把这些逻辑全部用if-else堆在Service层的一个方法里代码很快就会变得难以阅读和维护。这里的核心挑战不是技术而是如何将业务流清晰地映射为代码里的状态机和职责分离。1.2 多角色权限视图与数据的隔离系统通常包含以下角色学生查看宿舍信息、申请调宿、报修、查看通知、缴纳电费。辅导员审批本班级学生的调宿申请、查看学生住宿情况。宿管员管理宿舍楼、分配/调整床位、处理维修工单、录入水电表数据。系统管理员管理所有基础数据、用户、角色、权限。不同角色看到的菜单、数据范围、操作按钮完全不同。例如学生不能看到整个楼的空床位只能看到开放申请的宿舍列表宿管员能看到本楼所有宿舍的详情和床位状态辅导员只能看到自己学生的信息。这意味着后端不能简单地返回完整的实体对象前端也不能做一套页面给所有人用。前后端都必须建立“基于角色的数据过滤和视图渲染”机制。后端在接口层或Service层就要根据当前登录用户的角色动态拼接查询条件WHERE子句。前端则需要一套灵活的权限指令或组件渲染逻辑来控制菜单、按钮和表格内容的显示。1.3 那些容易被忽略的“非功能需求”除了增删改查一些边缘但关键的需求往往决定系统的可用性数据统计与可视化各楼住宿率、空床位统计、维修高频问题分类。这需要后端提供聚合查询接口前端引入ECharts等图表库。日志与操作追踪谁在什么时候修改了哪个学生的宿舍分配这类审计需求要求对关键业务表进行变更日志记录。批量操作开学季宿管需要为大批新生批量分配宿舍。这需要设计友好的前端批量选择界面和高效的后端批量处理接口。文件导入导出学生名单导入、宿舍分配结果导出为 Excel。涉及文件上传、解析、模板设计、异步处理等问题。如果你一开始只规划了单条数据的操作后期补充这些功能会非常痛苦。因此在技术选型和架构设计初期就需要为这些扩展点留好余地。2. 后端核心设计SpringBoot 如何承载业务逻辑SpringBoot 提供了快速启动的能力但如何组织代码让业务逻辑清晰、易于扩展才是关键。2.1 分层架构与包结构规划避免所有代码都堆在同一个包里。一个清晰的分层有助于团队协作和理解。建议的包结构如下com.example.dorm ├── DormApplication.java // 启动类 ├── config/ // 配置类安全、跨域、Swagger等 ├── controller/ // 控制器层接收请求返回响应 │ ├── api/ // 前后端分离这里放API接口 │ │ ├── StudentController.java │ │ ├── DormApplyController.java │ │ └── ... ├── service/ // 业务逻辑层 │ ├── impl/ // 接口实现类 │ │ ├── StudentServiceImpl.java │ │ └── ... │ └── IStudentService.java // 服务接口 ├── mapper/ // 数据访问层MyBatis-Plus │ ├── StudentMapper.java │ └── ... ├── entity/ // 实体类与数据库表对应 │ ├── Student.java │ ├── Dormitory.java │ ├── DormApply.java // 调宿申请实体 │ └── ... ├── dto/ // 数据传输对象用于接口入参出参 │ ├── request/ // 请求DTO │ │ ├── DormApplyRequest.java │ │ └── ... │ └── response/ // 响应DTO │ ├── StudentDetailDTO.java │ └── ... ├── vo/ // 视图对象用于前端页面展示可合并到dto ├── enums/ // 枚举类定义状态、类型等 │ ├── ApplyStatusEnum.java // 申请状态待审批、已通过、已驳回... │ ├── RepairStatusEnum.java // 维修状态待处理、处理中、已完成... │ └── ... └── utils/ // 工具类关键点实体类Entity纯粹对应数据库表只包含属性和 JPA/MyBatis-Plus 注解。不要在这里加业务逻辑或复杂的 JSON 注解。DTOData Transfer Object这是前后端交互的“合同”。为什么不用Entity直接接收和返回因为接口需要的字段和Entity往往不同。例如学生列表接口可能只需要id, name, dormNumber而详情接口需要更多信息。使用 DTO 可以精确控制出入参避免暴露数据库敏感字段如密码哈希也方便做参数校验使用Valid。枚举类Enum将所有状态码、类型码用枚举管理。比如ApplyStatusEnum.PENDING.getCode()。这比在代码里写死status1要清晰、安全得多也便于维护。2.2 状态流转与审批逻辑的实现以调宿申请为例这是系统的核心业务流程。1. 设计申请实体与状态枚举// enums/ApplyStatusEnum.java public enum ApplyStatusEnum { PENDING(0, 待辅导员审批), APPROVED_BY_TUTOR(1, 辅导员已通过), REJECTED_BY_TUTOR(2, 辅导员已驳回), ASSIGNED_BY_MANAGER(3, 宿管已分配), COMPLETED(4, 已完成入住), CANCELLED(5, 已取消); // ... 构造方法、getter } // entity/DormApply.java Data TableName(dorm_apply) public class DormApply { private Long id; private Long studentId; private Long targetDormId; // 目标宿舍ID private String reason; private ApplyStatusEnum status; // 使用枚举类型 private Long tutorId; // 审批辅导员ID private Long managerId; // 处理宿管ID private String tutorComment; private String managerComment; private LocalDateTime applyTime; private LocalDateTime processTime; }2. 在Service层实现状态机逻辑审批不是简单的update set status 1。它需要校验当前状态只有“待审批”的申请才能被辅导员审批。校验操作人权限辅导员只能审批自己班级学生的申请。原子性操作更新状态、记录审批人、添加批注、可能触发下一步如通知宿管需要在同一个事务中。记录日志关键状态变更应记录操作日志。// service/impl/DormApplyServiceImpl.java Service Transactional(rollbackFor Exception.class) public class DormApplyServiceImpl implements IDormApplyService { Override public void tutorApprove(Long applyId, Long tutorId, String comment) { DormApply apply getById(applyId); // 1. 状态校验 if (apply.getStatus() ! ApplyStatusEnum.PENDING) { throw new BusinessException(该申请当前不可审批); } // 2. 权限校验伪代码判断tutorId是否有权审批此学生 if (!hasPermission(tutorId, apply.getStudentId())) { throw new BusinessException(无权审批此申请); } // 3. 更新状态和记录 apply.setStatus(ApplyStatusEnum.APPROVED_BY_TUTOR); apply.setTutorId(tutorId); apply.setTutorComment(comment); apply.setProcessTime(LocalDateTime.now()); updateById(apply); // 4. 记录操作日志 logService.saveApproveLog(applyId, tutorId, 辅导员审批通过); // 5. 可选发送消息通知宿管可通过消息队列异步处理 // messageService.notifyDormManager(apply); } }3. 使用工作流引擎对于这个规模的系统上述硬编码的状态机足够清晰。如果流程异常复杂超过10个状态多路分支会签等可以考虑集成轻量级工作流引擎如Flowable或Activiti。但对于宿舍管理过早引入工作流引擎会增加不必要的复杂度用枚举和Service逻辑控制是更务实的选择。2.3 权限控制Spring Security 与 JWT 实践前后端分离项目推荐使用JWT (JSON Web Token)进行无状态认证。1. 核心流程用户登录后端验证账号密码。验证通过后生成一个 JWT Token包含用户ID、角色等信息返回给前端。前端后续请求在 HTTP Header通常是Authorization: Bearer token中携带此 Token。后端通过一个JwtAuthenticationFilter拦截请求解析并验证 Token将用户信息存入SecurityContext。控制器或方法上使用PreAuthorize(“hasRole(‘ADMIN’)”)或PreAuthorize(“hasAuthority(‘dorm:apply:approve’)”)进行细粒度权限控制。2. 关键配置示例简化版// config/SecurityConfig.java Configuration EnableWebSecurity EnableGlobalMethodSecurity(prePostEnabled true) // 启用方法级安全注解 public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() // 前后端分离通常禁用CSRF .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 .and() .authorizeRequests() .antMatchers(“/api/auth/login”).permitAll() // 登录接口放行 .antMatchers(“/api/**”).authenticated() // 所有/api/开头的接口需要认证 .anyRequest().permitAll() .and() .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // 添加JWT过滤器 return http.build(); } Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } }3. 数据权限PreAuthorize解决了“能否访问某个接口”的问题。但“辅导员只能看自己班的学生”这类数据权限需要在Service或Mapper层实现。一个常见做法是在查询方法中自动根据当前登录用户的角色ID去关联查询其管辖范围如班级ID、宿舍楼ID并动态添加到WHERE条件中。可以使用 MyBatis-Plus 的租户插件思路或自定义数据权限拦截器来实现。3. 前端架构Vue 3 Element Plus 如何构建管理界面前端的目标是构建一个清晰、响应快、体验好的管理后台。Vue 3 的 Composition API 和响应式系统配合 Element Plus 的组件库能高效完成这个任务。3.1 项目初始化与基础配置使用Vite创建 Vue 3 项目是现在的标准做法速度更快。npm create vuelatest dorm-frontend # 按提示选择 TypeScript, Router, Pinia, ESLint 等 cd dorm-frontend npm install element-plus element-plus/icons-vue axios npm install sass -D # 如需使用Scss关键配置全局引入 Element Plus在main.ts中引入并注册。配置 Axios 实例创建src/utils/request.ts设置基础URL、请求/响应拦截器。在请求拦截器中为每个请求自动添加 JWT Token在响应拦截器中统一处理错误如 Token 过期跳转登录。配置路由守卫在src/router/index.ts中利用beforeEach钩子检查目标路由是否需要认证以及当前用户是否有权限访问。3.2 状态管理Pinia 管理用户与全局状态对于宿舍管理系统需要全局共享的状态不多主要是用户信息。使用 Pinia 比 Vuex 更简洁。// stores/user.ts import { defineStore } from pinia import { ref } from vue import type { UserInfo } from /types/api export const useUserStore defineStore(user, () { const token ref(localStorage.getItem(token) || ) const userInfo refUserInfo | null(null) const roles refstring[]([]) const setToken (newToken: string) { token.value newToken localStorage.setItem(token, newToken) } const setUserInfo (info: UserInfo) { userInfo.value info roles.value info.roles || [] } const logout () { token.value userInfo.value null roles.value [] localStorage.removeItem(token) router.push(/login) } return { token, userInfo, roles, setToken, setUserInfo, logout } })3.3 页面组件设计与业务逻辑抽取1. 典型页面结构一个管理页面通常包含查询表单el-form包含各种输入框、选择器用于过滤表格数据。功能按钮区新增、批量操作、导出等。数据表格el-table展示数据常配合分页组件el-pagination。对话框/抽屉el-dialog或el-drawer用于新增和编辑数据的表单。2. 逻辑抽取与复用不要把所有代码都堆在setup()里。将可复用的逻辑抽取成Composables(类似于 React Hooks)。// composables/useTableList.ts import { ref, onMounted } from vue import type { TableListParams, TableListResult } from /types/api export function useTableListT(fetchApi: (params: any) PromiseTableListResultT) { const loading ref(false) const tableData refT[]([]) const total ref(0) const queryParams refTableListParams({ page: 1, size: 10 }) const fetchData async () { loading.value true try { const res await fetchApi(queryParams.value) tableData.value res.records total.value res.total } catch (error) { console.error(获取列表失败, error) } finally { loading.value false } } onMounted(() { fetchData() }) const handleQuery () { queryParams.value.page 1 fetchData() } const handleReset () { // 重置查询参数 fetchData() } return { loading, tableData, total, queryParams, fetchData, handleQuery, handleReset } }在页面中这样使用script setup langts import { useTableList } from /composables/useTableList import { getStudentList } from /api/student const { loading, tableData, total, queryParams, handleQuery, handleReset } useTableList(getStudentList) /script3.4 权限控制在前端的实现前端权限控制主要是视图层控制核心是防止无权限的用户看到不该看的元素。1. 权限指令可以创建一个v-permission指令根据当前用户的角色或权限码来控制按钮或菜单的显示。// directives/permission.ts import { useUserStore } from /stores/user export const permissionDirective { mounted(el, binding) { const { value } binding const userStore useUserStore() if (value value instanceof Array value.length 0) { const requiredRoles value const hasRole userStore.roles.some(role requiredRoles.includes(role)) if (!hasRole) { el.parentNode el.parentNode.removeChild(el) } } else { throw new Error(使用方式 v-permission[admin]) } } }在组件中使用el-button v-permission[dorm_manager]分配床位/el-button2. 动态路由更彻底的做法是在用户登录后根据其角色从后端获取有权限访问的路由菜单数据然后通过router.addRoute()动态添加到路由实例中。这样用户根本看不到也无权访问不属于自己的菜单页面。这需要前后端约定好路由数据结构。4. 前后端协同与部署从联调到上线的关键点前后端分离项目联调和部署是容易出问题的环节。4.1 接口规范与联调1. 统一的响应体格式前后端必须约定好数据返回格式。一个常见的格式如下{ “code”: 200, “message”: “操作成功”, “data”: { ... }, // 成功时的数据 “timestamp”: 1678886400000 }后端可以创建一个Result工具类来统一包装响应。前端在axios的响应拦截器中根据code进行统一处理如code 401跳转登录页。2. 使用 Swagger/OpenAPI 生成接口文档在后端引入springdoc-openapi-starter-webmvc-ui通过注解自动生成在线 API 文档。这能极大减少前后端沟通成本也是测试接口的好工具。3. 解决跨域问题开发环境前端运行在localhost:5173后端在localhost:8080浏览器会因同源策略阻止请求。有两种解决方式后端配置 CORS在 SpringBoot 的Configuration类中通过Bean注入一个WebMvcConfigurer来允许前端源的请求。前端代理在vite.config.ts中配置proxy将/api开头的请求代理到后端服务器。这是开发环境更常用的方式因为它避免了后端代码为开发环境做特殊配置。4.2 部署方案1. 前端部署运行npm run build生成静态文件在dist目录。可以将dist目录的内容直接放到 Nginx 或 Apache 的静态资源目录下。需要配置 Nginx将所有非静态文件的请求通常是前端路由如/login,/dashboard重定向到index.html由 Vue Router 处理。location / { try_files $uri $uri/ /index.html; }2. 后端部署使用mvn clean package打包生成可执行的 JAR 文件内嵌 Tomcat。在服务器上安装 Java 运行环境JRE。通过nohup java -jar dorm-backend.jar app.log 21 命令在后台运行。更推荐使用Docker容器化部署能更好地解决环境一致性问题。编写Dockerfile基于openjdk:17-jdk-slim镜像构建应用镜像然后通过docker-compose管理容器。3. 前后端分离部署的注意事项API 地址前端打包后请求的后端 API 地址需要根据生产环境配置。通常通过注入环境变量或在构建时替换配置文件的方式解决。HTTPS生产环境务必启用 HTTPS。可以在 Nginx 层配置 SSL 证书同时代理前端静态资源和后端 API 请求。静态资源缓存为前端静态文件如 JS、CSS配置长期缓存并设置合理的更新策略如文件名带哈希。4.3 常见“坑点”与排查思路前端路由刷新 404这是部署时最常见的问题。根本原因是浏览器直接访问了一个前端路由如/student/listNginx 找不到这个文件。解决方案就是上面提到的 Nginxtry_files配置把所有非文件请求指向index.html。跨域问题在部署后出现开发环境用代理解决了但部署后前端域名和后端域名不同再次出现跨域。这时必须在后端生产环境配置中正确设置 CORS允许前端生产域名的请求。JWT Token 过期处理Token 过期后前端需要自动刷新或跳转登录。通常在axios响应拦截器中判断code 401然后清除用户信息跳转到登录页并携带当前路由作为重定向参数。文件上传下载问题上传大文件时注意调整 Nginx 的client_max_body_size和后端 SpringBoot 的spring.servlet.multipart.max-file-size配置。下载文件时后端接口应设置正确的Content-Type和Content-Disposition响应头。数据库连接池耗尽在压力稍大的情况下可能出现数据库连接不够用。检查并合理配置连接池参数如 HikariCP 的maximum-pool-size、connection-timeout。开发这样一个系统真正的价值不在于你用了多少时髦的技术栈而在于你是否通过这个项目建立起将复杂、琐碎的业务需求转化为清晰、稳定、可扩展的代码结构的能力。从明确角色权限到设计实体状态再到规划前后端交互每一步都需要你跳出“实现功能”的层面去思考“如何优雅地管理变化”。下次当你再看到“XX管理系统”的需求时希望你的第一反应不再是去搜现成的代码而是能快速在脑海里勾勒出它的业务边界、数据模型和核心流程框架。这才是从“码农”到“工程师”的关键一步。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进