ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot高校人员管理系统开发实战:从数据建模到Docker部署

SpringBoot高校人员管理系统开发实战:从数据建模到Docker部署 1. 拿到“疫情期间高校人员管理”这个题目后我先拆了什么前段时间有位读者私信我说毕设选题抽到了“SpringBoot疫情期间高校人员管理”编号11798问我该怎么下手。这题目乍一看很大但拆开之后其实就是一个非常典型的Spring Boot管理信息系统人员档案、状态上报、出入登记、审批流转、数据统计。几乎每个模块都能用成熟的技术栈去实现而且非常适合拿来练手。如果你刚学完Spring Boot基础想找一个能写进简历、能在答辩时把“为什么这么设计”讲清楚的项目这类人员管理系统比商城、博客更适合。它没有复杂的订单拆单逻辑却把权限控制、事务、定时任务、报表统计这些后端常用能力都覆盖了一遍。我个人建议的技术栈是Spring Boot 2.7 MyBatis Plus MySQL 8 Redis 做后端前端用 Vue3 Element Plus登录用 JWT部署用 Docker。为什么这么选下面详细说。1.1 这个系统到底要管什么人、什么状态项目管理类系统最忌讳一上来就写代码。第一步应该是把“人”和“状态”理清楚。高校人员管理里人大概分四类学生、教职工、院系管理人员、校级管理员。如果再算上门卫、外包人员、临时访客就是第五类。不同角色关心的数据完全不一样学生和教职工每天要上报自己的健康状态辅导员要盯住本学院异常人员校级管理员要看全校的汇总数据。再想“状态”一个人在某一天有没有上报上报结果是否正常当前是在校还是已离校有没有请假请假有没有被审批通过这些状态是动态变化的。系统最核心的功能其实就是记录状态、驱动状态变化、留下每一条变化的痕迹。把这条主线想明白后面建表、写接口、做页面都会顺很多。1.2 技术选型为什么是Spring Boot MyBatis Plus很多新手会问这个项目到底用不用微服务我的建议是不要。高校人员管理系统本质上还是单体的CRUD 审批 统计用Spring Boot单体应用完全够。Spring Boot默认集成了Tomcat、Jackson、Spring MVC自动配置做得非常省心你只需要关注业务代码。这也是为什么大量高校毕设和中小型管理系统都选它。持久层我推荐MyBatis Plus而不是原生MyBatis。原因很简单单表CRUD不用写SQL用条件构造器就能解决分页有内置分页插件逻辑删除、自动填充、代码生成器都有现成方案。但这不代表SQL不用学后面的动态多条件查询、聚合统计还是得自己写XML否则会被不规范的查询拖死。至于Redis主要用来做验证码缓存、防止重复提交和可选的Token黑名单属于加分项。MySQL 8则负责常规数据落地字段用utf8mb4避免emoji和生僻字写入失败。2. 功能设计与数据建模先把表结构想清楚2.1 功能模块怎么切按题目背景和实际管理场景我习惯把系统切成六个模块。基础人员管理负责学生、教职工、院系、角色档案的增删改查校级管理员可以导入Excel批量创建账号。每日健康上报是系统的数据入口用户每天填写体温、身体状况、是否接触风险人群等信息同一人同一天只能上报一次。出入校管理覆盖进出校园登记和申请校内人员扫码或由门卫代登记访客则需要先提交申请审核通过后才能进入。请假审批用于在校生和教职工离校、返校的审批流程辅导员审批本学院人员校级管理员可以看到全部单子。异常信息处置是给辅导员和管理员用的一旦有人上报异常或连续未上报系统要能筛选出来。统计看板则从院系、年级、日期、状态几个维度做聚合决定每天要看的数据是否完整。这六个模块不是平级堆料而是围绕“状态”串起来的基础档案定义人上报和出入登记产生状态审批驱动状态变更统计把结果呈现出来。2.2 核心表设计我建表的原则是宁可多拆一张表也不要在一张表里堆大字段。下面这几张是核心表字段我按最简版本列出来了实际扩展时再补。表名关键字段作用userid, username, password, role, dept_id, status登录账号与角色student_infoid, user_id, student_no, college_id, grade, class_name学生扩展信息staff_infoid, user_id, staff_no, college_id, position教职工扩展信息deptid, parent_id, dept_name, dept_type学院/部门树health_reportid, user_id, report_date, temperature, health_status, exposure_flag, remark每日状态上报access_recordid, user_id, access_type, access_time, target_place, operator_id出入校登记流水leave_requestid, user_id, leave_type, start_time, end_time, reason, audit_status, audit_remark请假/返校审批notificationid, title, content, publish_time, target_role通知公告这里必须强调两个设计细节。一是health_report要加唯一索引(user_id, report_date)否则并发请求下会出现同一人同一天两条上报记录。二是access_record不要设计成只保留“当前状态”每次进入或离校都生成一条流水同时再更新user表里的current_status字段。流水用来追溯历史状态字段用来快速判断权限。2.3 权限与状态字段的约定权限不复杂不建议上Spring Security那套重量级过滤器链直接用一个JWT拦截器配合角色判断就够了。接口层先通过拦截器解析token拿到userId和role再用自定义注解或手动判断控制访问。角色我建议用字符串存比如 admin、dept_manager、student、teacher而不是用数字0、1、2因为数字可读性太差答辩时还要解释半天。状态字段也要定一套统一约定。比如所有“审核状态”统一用0待审核、1通过、2驳回用户状态统一用0禁用、1正常。这样前端做标签渲染和后端做枚举转换都很方便。还有一个容易忽略的点数据库里的时间统一存datetimeJava实体里用LocalDateTime前后端传输时统一转成yyyy-MM-dd HH:mm:ss字符串。时区问题在管理类系统里非常常见后面我会单独讲。3. 第1关到第3关从项目整合走到登录注册很多教程喜欢直接扔一个完整项目新手看的时候一脸懵。我更推荐按闯关的方式来做先让项目跑起来再做一个最简单的注册然后加登录鉴权。这个路径也叫“第1关、第2关”能让你每一步都能看到结果。下面就是我实际搭建时的过程。3.1 第1关Spring Boot整合MyBatis创建项目时直接用Spring Initializr选择Java 8或11Spring Boot版本我选2.7.x。为什么不选Spring Boot 3.x因为3.x基于Jakarta EE包名从javax变成了jakarta很多老教程和老依赖不兼容如果版本太高你搜到的资料大半会踩坑。等把这个项目跑通再考虑升级不迟。核心依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency然后是application.yml配置数据库连接串里一定要指定serverTimezone否则你会在本地正常、部署到服务器就报时间差8小时的问题。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/university_manager?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl启动类上别忘了加MapperScan否则Mapper接口不会被扫描到。很多人报了“Invalid bound statement”错误排查半天发现是这里漏了。SpringBootApplication MapperScan(com.university.manager.mapper) public class UniversityManagerApplication { public static void main(String[] args) { SpringApplication.run(UniversityManagerApplication.class, args); } }这一关跑通的标准是能正常启动日志里没有红色报错访问一个测试接口能返回数据。3.2 第2关最简单的注册功能注册功能是所有账号体系的基础也能帮你把MyBatis Plus的用法过一遍。先定义RegisterDTO用javax.validation注解做参数校验避免Controller里写一堆if判断。Data public class RegisterDTO { NotBlank(message 用户名不能为空) private String username; NotBlank(message 密码不能为空) Size(min 6, max 20, message 密码长度必须为6到20位) private String password; private String role; private Long deptId; }Controller层只负责接收参数和调用Service不要写业务逻辑。Service里先判断用户名是否存在再做密码加密最后插入。Service public class UserServiceImpl implements UserService { Resource private UserMapper userMapper; Override public void register(RegisterDTO dto) { Long count userMapper.selectCount( Wrappers.UserlambdaQuery() .eq(User::getUsername, dto.getUsername())); if (count 0) { throw new BizException(用户名已存在); } User user new User(); user.setUsername(dto.getUsername()); user.setRole(dto.getRole()); user.setDeptId(dto.getDeptId()); user.setPassword(DigestUtils.md5DigestAsHex( (dto.getPassword() SALT).getBytes(StandardCharsets.UTF_8))); user.setStatus(1); userMapper.insert(user); } }关于密码加密我建议答辩时不要只说“用MD5加密”。MD5本身是摘要算法不是加密算法而且裸MD5容易被撞库。为了简单演示用MD5加盐可以但如果想让答辩更有说服力建议用Spring Security里的BCryptPasswordEncoder它每次生成的哈希值不同安全性更好。这个点是我觉得最值得优化的一处。3.3 第3关登录、JWT与拦截器登录流程不复杂先查用户是否存在密码匹配后用JWT生成一个token返回前端。前端后续请求在Header里带上Authorization: Bearer token后端用拦截器统一校验。生成token时要把userId和role放进去这样接口里不需要再查一次用户表拿角色。JWT我用的是jjwt 0.9.1如果你用的JDK版本较高记得额外加jaxb依赖否则运行时会报ClassNotFoundException。拦截器核心逻辑Component public class JwtInterceptor implements HandlerInterceptor { Value(${jwt.secret}) private String secret; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); } try { Claims claims Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或token已失效\}); return false; } } }注册拦截器时记得排除登录、注册、验证码这些不需要鉴权的路径。如果不排除前端第一次登录拿不到token整个系统都进不去这个问题非常经典。4. 核心业务模块健康上报与出入校管理的实现细节登录注册跑通之后系统才算有了骨架接下来要把重头戏业务模块做扎实。这里重点讲健康上报和出入校管理因为它们是项目里最有区分度、也最容易在答辩时被追问的部分。4.1 健康上报动态SQL和唯一索引健康上报的核心是“一人一天一条”。后端接口先按user_id和report_date查询是否存在记录如果存在就返回“今日已上报”。但只有业务层判断还不够并发情况下两个请求同时查到不存在就可能插入两条。所以数据库层面必须加唯一索引。ALTER TABLE health_report ADD UNIQUE KEY uk_user_date (user_id, report_date);上报接口就变得很简单先检查再插入。如果插入时抛出DuplicateKeyException就捕获异常并返回“请勿重复提交”。不要在这个接口里放复杂逻辑越简单越不容易出错。查询端才是重点。辅导员要看本学院所有人员某一天的上报情况管理员要看全校异常人员列表这两个页面都需要动态多条件查询。我在Mapper XML里用 和 标签拼SQL最典型的一段是这样select idselectReportPage resultTypecom.university.manager.model.vo.HealthReportVO SELECT r.*, u.username, d.dept_name FROM health_report r LEFT JOIN user u ON r.user_id u.id LEFT JOIN dept d ON u.dept_id d.id where if testdeptId ! null AND u.dept_id #{deptId} /if if testhealthStatus ! null AND r.health_status #{healthStatus} /if if testreportDate ! null AND r.report_date #{reportDate} /if /where ORDER BY r.report_date DESC /select这里要注意XML转义。小于号、大于号在XML里有特殊含义不能直接写、要写和。很多新手报语法错误十有八九是栽在这里。统计看板还要用聚合SQL按院系统计某一天的上报率和异常人数SELECT d.dept_name, COUNT(DISTINCT u.id) AS total_user, COUNT(DISTINCT r.id) AS reported_count, SUM(CASE WHEN r.health_status 1 THEN 1 ELSE 0 END) AS abnormal_count FROM user u LEFT JOIN dept d ON u.dept_id d.id LEFT JOIN health_report r ON r.user_id u.id AND r.report_date #{reportDate} GROUP BY d.dept_name4.2 出入校登记状态机与幂等出入校管理和普通CRUD最大的区别是它存在状态流转。我把用户当前的在校状态保存在user表里的current_status字段0表示在校1表示已离校。门卫或管理员做登记时先判断当前状态再决定是否允许登记。当前状态操作允许新状态在校登记出校允许已离校已离校登记返校允许在校在校登记返校不允许提示重复不变已离校登记出校不允许提示重复不变状态判断做出来后还要处理并发。假如门卫同时在两个窗口给同一个人做出校登记两个请求都读到当前状态是“在校”就会都通过。解决办法是在user表加version字段更新时带上版本号UPDATE user SET current_status 1, version version 1 WHERE id #{userId} AND current_status 0 AND version #{oldVersion};如果更新影响行数为0说明这次操作已经无效直接返回“操作失败请刷新后重试”。这是典型的乐观锁方案管理类系统里用来防并发非常实用。同时access_record表作为流水必须保留操作前状态、操作后状态、操作人、操作时间。就算以后用户数据被删了审计流水也还在这种“只追加不修改”的设计在答辩时是很大的加分项。4.3 请假与返校审批一个轻量级工作流请假审批的逻辑比单纯CRUD多一层“状态推进”。请假单表里audit_status字段默认0表示待审核辅导员审核通过改成1驳回改成2。创建请假单时把用户状态改成“请假中”。注意这里不要直接改user表的current_status而是在请假单里维护一个leave_status避免和出入校登记的状态互相覆盖。审批接口要注意权限。我在Service里先解析当前登录身份取到角色和院系信息再判断这个请假单是否属于当前院系。如果辅导员看到的是全校的请求就说明这里漏了数据权限控制。数据权限比接口权限更容易被忽略却非常关键。审批动作建议放到一个事务方法里同时更新请假单状态和用户状态。一旦中间出错事务回滚不会出现“单子显示已通过但用户状态还是请假中”的不一致情况。这也是一个可以主动写进项目文档里的细节。5. 前后端联调、部署与高频问题排查系统开发到这一步后端的核心接口基本齐了。接下来要解决的是前后端怎么对接、项目怎么部署以及上线后最常见的那些坑。这部分我按实际操作顺序讲直接照着做就行。5.1 接口规范与跨域我做前后端分离项目时会先定一个统一的返回结构。所有接口返回Result对象包含code、message、data三个字段。成功时code为200业务异常时code为400未登录时code为401。前端只需要统一拦截非200的响应弹提示就可以了。这个规范能省掉很多无意义的沟通。Data public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(int code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }前后端分离必然遇到跨域问题。常见做法是写一个全局CORS配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }如果你已经加了JWT拦截器尤其要注意OPTIONS预检请求。预检请求不会带token如果拦截器把所有OPTIONS请求都拦截了前端就会出现“请求失败”但后端日志没有报错的情况。我习惯在拦截器里遇到OPTIONS请求直接放行。5.2 Docker部署Spring Boot项目部署最常见的姿势是Maven打成jar包然后扔到服务器上java -jar运行。如果你想更进一步用Docker部署会更规范也方便迁移。先执行mvn clean package生成jar包后写Dockerfile。FROM openjdk:8-jre-alpine WORKDIR /app COPY target/university-manager-1.0.0.jar app.jar EXPOSE 8080 ENV TZAsia/Shanghai ENTRYPOINT [java, -jar, app.jar]MySQL和Redis如果不想手动装可以用docker-compose一起编排。需要注意容器间通信用服务名而不是localhost。version: 3 services: mysql: image: mysql:8.0 container_name: manager-mysql environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: university_manager ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql command: --default-time-zone08:00 app: build: . container_name: manager-app ports: - 8080:8080 depends_on: - mysql这里有个很常见的坑容器里应用连数据库时jdbc url如果写localhost连的是容器自己的网络永远连不上MySQL。必须改成url: jdbc:mysql://mysql:3306/university_managermysql是docker-compose里定义的服务名docker内部dns会自动解析到MySQL容器的IP。5.3 高频踩坑排查清单列一个我在实际开发和帮读者排查问题时最常遇到的清单每条都是真实踩过的。现象原因解决办法Invalid bound statementMapper接口没被扫描或XML路径不对启动类加MapperScan检查yml里的mapper-locations时间差8小时MySQL连接串没指定时区url加serverTimezoneAsia/Shanghai接口返回LocalDateTime是数组缺少Jackson JavaTimeModule配置yml设置spring.jackson.date-format并引入jackson-datatype-jsr310前端报跨域缺少CORS配置或OPTIONS被拦截加CorsConfig拦截器放行OPTIONS明明密码正确却登录失败密码加密方式不一致统一加密算法避免服务端“忘了加盐”重复提交产生两条数据只有业务判断没有唯一索引加唯一索引并捕获DuplicateKeyException打包后找不到主类没加spring-boot-maven-pluginpom里加插件执行repackageRedis连接失败导致启动慢Redis没启动或地址写错先本地启动redis或把Redis相关代码先注释掉访问404Controller路径写错或没在同一个Application下检查注解和包扫描路径这些问题的共同点是表面看起来千奇百怪深挖全是配置或设计层面的基础问题。建议你每遇到一个就记录下来。答辩被问到“项目遇到什么难点”时从这张表里挑两三个讲比空谈“系统稳定性”要实在得多。6. 关于项目答辩与扩展我最后说几句6.1 项目亮点怎么提炼项目做完只是第一步把亮点讲清楚才是答辩环节的关键。不要一上来就说“我做的系统功能完善”这种话没有任何信息量。比较实用的表达方式是围绕具体设计展开健康上报用唯一索引加乐观锁解决了重复提交问题出入校流水采用只追加设计保证每次状态变化都可追溯请假审批用事务保证请假单状态和用户状态的一致性统计查询用动态SQL处理多条件组合筛选。这些点每一个都对应到一个真实的问题和解决方案答辩老师很容易听懂。6.2 从11798还能扩展成什么如果做完这个系统还有时间我建议按这几个方向扩展一是接入人脸识别或扫码核验替代手动登记二是增加定时任务每天自动生成未上报名单并通知辅导员三是导出Excel报表按院系、日期生成上报统计四是用MinIO做附件存储支持上传请假证明、健康码截图等材料。扩展不求多把一个点做深比堆五六个半成品模块更能体现你的工程能力。至于这个项目编号11798其实只是题目来源的一个编号但很适合作为你完整走通“需求分析、数据建模、编码实现、部署上线”全流程的起点。我个人在实际操作中的体会是管理类系统的难点从来不在增删改查而在状态如何流转、数据如何保证一致性、权限如何控制到数据级别。把这几个点想透你的Spring Boot能力会明显上一个台阶。
返回列表