资讯动态

Spring Boot人事管理系统实训:从数据建模到权限控制完整实践

发布时间:2026/10/20 9:00:36 来源:尧图企业网站定制
简介这是湖南科技大学计算机学院2023年综合创新实训3的软件工程课程资源主题为人事管理系统的设计与实现适合软件工程专业学生、课程设计者及需要撰写实训报告的学习者参考。压缩包内包含1个docx格式的完整实训报告文件总大小947KB内容覆盖需求分析、体系结构设计、接口与界面设计、数据结构与流程设计、数据建模、范式分析以及结合工厂模式与外观模式的详细设计、UML图绘制与工作日志等模块结构完整可直接用于理解人事管理系统的开发全流程。资源目前已有1188人学习说明其实践性与参考价值受到认可。通过这份文档读者可以快速掌握软件工程规范文档的写法了解从需求规约到详细设计、再到团队分工与进度记录的完整组织方式也能为自身课程设计或实训项目提供可直接借鉴的模板与思路。1. 人事管理系统这道实训题先想清楚它到底考什么人事管理系统在答辩现场十个人里有八个能跑通增删改查但能扛住“为什么这么设计”这句追问的人一只手数得过来。这道题表面是做个员工信息管理实际考的是你有没有把组织架构、员工档案、考勤请假、账号权限这条业务链路完整串起来的能力。多数翻车案例不是功能太少而是做成了“一张大表加四个按钮”的表格工具。综合创新实训3软件工程阶段选人事管理系统好处在于它覆盖了从建表到部署的完整流程既有标准的信息管理又有审批流、权限控制这类有逻辑深度的场景。这篇笔记写给正在做这道实训题、准备答辩或者想补一遍“数据建模→后端接口→权限控制→避坑→演示验证”全流程的开发者。后面的内容全部按我实际做这类项目时验证过的路数来写参数和坑都是真实的不是教科书推导。2. 选型与数据建模人事管理系统的地基不能歪2.1 技术栈选型三类常见组合到底怎么挑实训项目选技术栈第一原则不是“哪个最有面子”而是“答辩时老师问到的每一层你都能讲清楚”。前后端分离、微服务、消息队列这些词用在人事管理系统上反而容易被追问到答不上来。常见的可靠组合有三类我按推荐程度排一下。方案后端框架前端方案适合谁ASpring Boot 2.x MyBatis-PlusVue 3 Element Plus已经写过前后端联调想把项目放进简历BSpring Boot 2.x MyBatis-PlusThymeleaf 服务端渲染前端不熟想集中精力做后端逻辑C现成开源脚手架改脚手架自带页面只剩一周只求完整跑通方案A看起来最完整但前后端联调、跨域、token 存储这些坑会消耗大量时间方案B虽然页面朴素但数据流简单答辩时能把“请求怎么到数据库再返回来”这条链路讲得很清楚方案C的风险在于如果连脚手架里的代码都没看过老师随便指一个注解问你什么意思场面会很难看。我一般建议选B或者选A但只保留少量核心页面做前后端分离演示。确定方向后后端依赖不需要手写版本号直接继承 Spring Boot 父工程即可。实训阶段手写版本号反而容易引入冲突比如 MyBatis-Plus 和某个 Spring Boot 小版本不兼容排查起来非常耗时。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency不写版本号是我做实训项目时的习惯Spring Boot 2.x 的父工程已经把常用依赖版本统一管理好了自己写version反而会覆盖父工程的默认值。如果确实要指定也得先确认这个版本在 Maven 中央仓库真实存在并且和当前 Spring Boot 版本做过兼容验证。另外Lombok 加不加都行加了能少写 getter/setter但必须确认 IDE 装了对应插件否则答辩现场编译报错会非常尴尬。2.2 数据库表设计核心表和字段约束一起定下来人事管理系统最少需要六张表部门表、员工表、请假申请表以及第四章要讲的用户、角色、用户角色关联表。前两张表是地基设计时有一个底线主键统一用自增BIGINT每张表必须有create_time和update_time状态字段用数字不用字符串。这个习惯能让你后面写 MyBatis-Plus 条件构造器时少踩一半的坑。字段类型有几个容易翻车的点提前避掉。金额用DECIMAL(10,2)不用DOUBLE否则累计工资时会有精度误差手机号用VARCHAR(20)不用BIGINT否则以 0 开头的号码会丢前导零工号和部门编码要有唯一约束这是答辩时“数据库层面防止重复数据”的加分项。日期字段按用途区分入职日期用DATE操作记录用DATETIME不要混用。员工表的设计直接决定后续接口好不好写。注意区分两个容易混淆的字段status表示在职或离职deleted表示逻辑删除。前者是业务状态员工离职后档案还要保留后者是数据是否被软删配合 MyBatis-Plus 的TableLogic自动过滤。这两个概念在答辩时被问到的概率很高值得提前想清楚。CREATE TABLE dept ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 部门ID, dept_name VARCHAR(50) NOT NULL COMMENT 部门名称, dept_code VARCHAR(20) NOT NULL UNIQUE COMMENT 部门编码, manager_id BIGINT DEFAULT NULL COMMENT 负责人ID, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT 部门表; CREATE TABLE employee ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 员工ID, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT 工号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT DEFAULT NULL COMMENT 1男 2女, dept_id BIGINT NOT NULL COMMENT 所属部门, position VARCHAR(50) COMMENT 岗位, phone VARCHAR(20) COMMENT 手机号, entry_date DATE COMMENT 入职日期, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在职 0离职, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1已删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES dept(id) ) COMMENT 员工表;这段 SQL 里有几个值得跟答辩老师讲的细节。dept_code和emp_no上的唯一约束比在代码里用if判断重复更可靠因为并发请求同时插入时代码判断会漏掉数据库约束不会。update_time用ON UPDATE CURRENT_TIMESTAMP让数据库自动维护更新时间省掉 Java 代码里手动 set 的步骤。外键fk_emp_dept保证员工必须归属于一个真实存在的部门但注意后面做员工删除时需要处理关联约束第五章会专门讲这个坑。2.3 请假申请表的建表脚本和初始化数据请假申请表是这套系统里唯一一个带状态流转的表也是答辩时展示“业务逻辑深度”的关键。设计原则是一天请假记录对应一个审批状态不搞复杂的子表。状态字段用TINYINT0 待审批1 通过2 驳回。审批人和审批时间要在通过或驳回时写入这样人事才能在后台看到完整链路。CREATE TABLE leave_apply ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 申请ID, employee_id BIGINT NOT NULL COMMENT 申请人ID, leave_type TINYINT NOT NULL COMMENT 1事假 2病假 3年假, start_time DATETIME NOT NULL COMMENT 开始时间, end_time DATETIME NOT NULL COMMENT 结束时间, reason VARCHAR(255) COMMENT 请假原因, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审批 1通过 2驳回, approver_id BIGINT DEFAULT NULL COMMENT 审批人ID, approve_time DATETIME DEFAULT NULL COMMENT 审批时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_leave_emp FOREIGN KEY (employee_id) REFERENCES employee(id) ) COMMENT 请假申请表;初始化数据不要只插一条 admin 进去那样演示时列表页面空荡荡的。部门至少 5 个员工至少 20 个请假申请至少 3 条且状态各不相同这样列表筛选、状态展示都有东西可看。但初始化数据用手写 INSERT 太累我一般用给后面第六章的批量生成脚本留接口先把两个核心部门插进去当锚点。INSERT INTO dept (dept_name, dept_code) VALUES (人事部, D001), (技术部, D002); INSERT INTO employee (emp_no, name, gender, dept_id, position, entry_date) VALUES (E0001, 王某, 1, 1, 人事专员, 2022-03-01), (E0002, 李某, 2, 2, Java开发, 2022-07-15);这里有个命名约定的问题要提前说明表名、字段名全部用下划线风格Java 实体属性用驼峰风格MyBatis-Plus 默认开启map-underscore-to-camel-case会自动映射。如果不遵守这个约定查询结果会出现字段全是 null 的玄学问题而且排查起来很费劲。建议建表时就统一用dept_id而不是deptId实体类写deptId让映射交给框架。3. 后端接口实现员工、部门调动与审批的核心逻辑3.1 员工档案 CRUD参数校验放 Service 层而不是 Controller员工管理的接口是最普通的增删改查但普通接口也有值得讲究的地方。我的习惯是 Controller 层只做接收参数和返回结果业务校验全部放 Service 层。这样写单元测试时直接 new 一个 Service 就能测不用启动整个 Web 容器答辩现场调试也快。RestController RequestMapping(/api/employee) public class EmployeeController { private final EmployeeService employeeService; public EmployeeController(EmployeeService employeeService) { this.employeeService employeeService; } PostMapping public Result save(RequestBody Valid Employee employee) { employeeService.saveEmployee(employee); return Result.ok(); } DeleteMapping(/{id}) public Result delete(PathVariable Long id) { employeeService.deleteEmployee(id); return Result.ok(); } }Valid注解触发 JSR-303 参数校验RequestBody把前端 JSON 自动绑定到实体类。这里有个细节如果前端传的字段名和实体属性对不上绑定不会报错对应属性会是 null所以接口调试时先打印一遍接收到的对象别急着查数据库。Service 层的核心在于重复判断和业务校验。不能只依赖数据库唯一约束因为数据库报错是异常用户体验差代码里先查一次给出明确提示异常只留给并发这种极端情况。public void saveEmployee(Employee employee) { if (StringUtils.isBlank(employee.getEmpNo())) { throw new BizException(工号不能为空); } Long count employeeMapper.selectCount( new LambdaQueryWrapperEmployee() .eq(Employee::getEmpNo, employee.getEmpNo()) ); if (count 0) { throw new BizException(工号已存在); } employee.setCreateTime(LocalDateTime.now()); employeeMapper.insert(employee); }LambdaQueryWrapper的eq方法拼 SQL 的等值条件用了 Java 方法引用Employee::getEmpNo好处是字段名写错时编译期就报错不会等到运行时才暴露。selectCount返回记录数这里注意 MyBatis-Plus 3.x 的selectCount返回Long老版本返回Integer如果你的代码一直报类型不匹配先查一下版本。分页查询是另一个必写的接口。大多数实训项目用的是 PageHelper但既然用了 MyBatis-Plus直接用它的分页插件更省事。public PageEmployee pageQuery(EmployeeQuery query) { LambdaQueryWrapperEmployee wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getName()), Employee::getName, query.getName()) .eq(query.getDeptId() ! null, Employee::getDeptId, query.getDeptId()) .eq(Employee::getDeleted, 0) .orderByDesc(Employee::getCreateTime); return employeeMapper.selectPage( new Page(query.getPageNum(), query.getPageSize()), wrapper ); }like和eq的重载方法第一个参数是 boolean 条件条件为 false 时这个查询条件不会拼进 SQL。这样就不用写一堆if (xxx ! null)来判断要不要加条件代码干净很多。orderByDesc(Employee::getCreateTime)按创建时间倒序新员工排前面这是列表页默认应该有的排序。new Page(pageNum, pageSize)里pageNum从 1 开始前端如果传 0查询结果会是空的这一点前后端要约定好。3.2 部门调动一个事务里完成的双向更新部门调动是人事管理系统里最容易写错的地方。常见错误是只改了员工的dept_id忘了更新部门表的员工人数导致两边数据对不上。正确做法是把“改员工部门 旧部门人数减一 新部门人数加一”这三个操作放进同一个事务任何一个失败都整体回滚。Transactional(rollbackFor Exception.class) public void transferDept(Long employeeId, Long newDeptId) { Employee employee employeeMapper.selectById(employeeId); if (employee null) { throw new BizException(员工不存在); } Long oldDeptId employee.getDeptId(); if (oldDeptId.equals(newDeptId)) { return; } employee.setDeptId(newDeptId); employeeMapper.updateById(employee); deptMapper.reduceStaffCount(oldDeptId); deptMapper.increaseStaffCount(newDeptId); }Transactional(rollbackFor Exception.class)这一行是重点。Spring 事务默认只在抛出运行时异常时才回滚如果自定义的BizException继承的是Exception不加rollbackFor就会事务不生效数据只改了一半这是经典翻车点。为了保险所有自定义业务异常我建议都继承RuntimeException事务层面彻底避免这个问题。部门人数的更新用数据库原子操作而不是先查出来再加减Update(UPDATE dept SET staff_count staff_count - 1 WHERE id #{deptId} AND staff_count 0) int reduceStaffCount(Long deptId); Update(UPDATE dept SET staff_count staff_count 1 WHERE id #{deptId}) int increaseStaffCount(Long deptId);staff_count - 1放到 SQL 里执行而不是 Java 里先getStaffCount()再setStaffCount(count-1)原因有两个。第一避免并发时两个请求同时读到同一个旧值导致最终只减了一次第二AND staff_count 0防止人数被减成负数这在数据异常时能兜底。这里的staff_count可以在部门表里加上它不是冗余而是为了列表页直接展示不需要实时聚合查询。3.3 请假审批状态机让流程不乱走请假审批这个功能表面是 update 一条记录的状态但如果没有状态判断就会出现“已经驳回的单子还能再通过”这种业务错误。正确做法是给状态流转定义一个规则只有待审批状态可以变成通过或驳回通过和驳回是终态不能再操作。转账到代码里就是更新前先查当前状态不是待审批就拒绝操作。public void approveLeave(Long leaveId, Integer action, String operator) { LeaveApply leave leaveMapper.selectById(leaveId); if (leave null) { throw new BizException(请假申请不存在); } if (leave.getStatus() ! 0) { throw new BizException(该申请已处理请勿重复操作); } if (!action.equals(1) !action.equals(2)) { throw new BizException(非法的审批操作); } leave.setStatus(action); leave.setApprover(operator); leave.setApproveTime(LocalDateTime.now()); leaveMapper.updateById(leave); }这个接口的逻辑可以总结成一张状态流转表答辩时直接画给老师看当前状态操作结果状态待审批(0)通过已通过(1)待审批(0)驳回已驳回(2)已通过(1)任何操作拒绝抛异常已驳回(2)任何操作拒绝抛异常operator从当前登录用户拿而不是前端传否则任何人都能冒充审批人。这里的实现路径是拦截器把登录用户 ID 放进request的 attribute 里Service 接收参数时从上下文取。前端传过来的action必须是数字接口里做了白名单判断不在 1 或 2 范围内直接拒绝。这个判断不是多余的防止有人传 3、传 0 绕过状态机的约束。4. 登录与权限RBAC 权限模型在实训项目里的落地4.1 用户-角色-用户角色三表设计与数据初始化人事管理系统里不需要每个用户单独配权限那样管理成本太高。用 RBAC 模型用户关联角色角色关联权限员工调岗只需要换角色不用逐个改权限。实训版本做三张表就够了用户表、角色表、用户角色关联表。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密后存储, employee_id BIGINT DEFAULT NULL COMMENT 关联员工ID, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) COMMENT 用户表; CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 角色ID, role_code VARCHAR(30) NOT NULL UNIQUE COMMENT 角色编码, role_name VARCHAR(30) NOT NULL COMMENT 角色名称 ) COMMENT 角色表; CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ) COMMENT 用户角色关联表;初始化数据直接写死在 SQL 里答辩时不用再手动建账号。三个角色的划分要和业务对应管理员可以配置部门和账号人事专员可以处理员工档案和请假审批普通员工只能看自己的信息和提交请假。INSERT INTO sys_role (role_code, role_name) VALUES (admin, 管理员), (hr, 人事专员), (employee, 普通员工); INSERT INTO sys_user (username, password, employee_id, status) VALUES (admin, {bcrypt}$2a$10$7QvXlVvXlVvXlVvXlVvXlO, NULL, 1), (hr01, {bcrypt}$2a$10$7QvXlVvXlVvXlVvXlVvXlO, 1, 1), (emp01, {bcrypt}$2a$10$7QvXlVvXlVvXlVvXlVvXlO, 2, 1); INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1), (2, 2), (3, 3);初始化数据里我写了{bcrypt}前缀Spring Security 的DelegatingPasswordEncoder支持这种存储格式。但如果你没有引入 Spring Security只是为了调接口方便可以先在 Java 里用BCryptPasswordEncoder生成一段真实密文再放进 SQL不要直接用明文。这点在第五章会详细讲为什么明文密码是答辩减分项。4.2 登录接口与密码校验BCrypt 每次生成密文都不同登录接口是系统里第一个会被攻击者盯上的接口密码不能明文存储登录不能只查一次数据库就放行。我选择 BCrypt 做密码哈希而不是 MD5 或 SHA-256因为 BCrypt 内置盐值同一个密码每次生成的密文都不同可以直接防彩虹表。MD5 加不加盐都得自己实现盐的存储实训阶段很容易做错。public String login(String username, String rawPassword) { SysUser user userMapper.selectOne( new LambdaQueryWrapperSysUser() .eq(SysUser::getUsername, username) ); if (user null || !passwordEncoder.matches(rawPassword, user.getPassword())) { throw new BizException(用户名或密码错误); } if (user.getStatus() ! 1) { throw new BizException(账号已被禁用); } ListString roles roleMapper.selectRolesByUserId(user.getId()); String token JwtUtil.createToken(user.getId(), roles); return token; }密码校验用passwordEncoder.matches(rawPassword, user.getPassword())这个方法内部会从密文里提取盐再用盐去哈希明文比较。注意这里不能直接用passwordEncoder.encode(rawPassword).equals(user.getPassword())因为两次 encode 的结果不一样永远比不出来。登录成功返回 token后续请求在 header 里带Authorization: Bearer token。JWT 方案在实训里够用且好讲但如果答辩老师问“token 被别人偷走怎么办”你要能答出“JWT 无法主动失效有效期不能设太长敏感操作要二次校验”。如果说不清这一点说明 Session 方案可能更适合你至少服务端可以主动踢人。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BizException(401, 未登录); } Long userId JwtUtil.parseUserId(token); request.setAttribute(userId, userId); return true; } }登录拦截器的职责只有两个从 header 里取 token解析出 userId 放进 request 上下文。它不校验角色角色校验留给后面的路由和按钮权限。这样设计的好处是职责单一拦截器管“你是谁”权限判断管“你能干什么”。如果拦截器里既解析 token 又查数据库拿角色列表每次请求都要多一次数据库查询性能上不划算逻辑上也耦合。4.3 前端路由守卫和按钮级权限控制后端接口做了拦截还不够前端也要做两层控制路由级权限控制用户能访问哪些页面按钮级权限控制用户能看到哪些操作按钮。只做后端不做前端普通用户访问/admin路径时虽然接口会拒绝但页面会白屏体验很差。Vue 3 里用路由守卫实现页面级控制核心逻辑是判断角色是否在目标路由的meta.roles白名单里。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } const roles JSON.parse(localStorage.getItem(roles) || []) if (to.meta.roles !to.meta.roles.some(r roles.includes(r))) { next(/403) return } next() })角色列表在登录成功后由后端返回前端存到 localStorage刷新页面时从 localStorage 恢复。这里有个安全注意点localStorage 里的角色可以被用户手动修改所以前端隐藏按钮只是体验优化真正的权限校验必须在后端做。答辩时如果老师问“前端改掉角色是不是就能进管理页”你要回答“前端改了也进不去因为后端接口会校验真实角色”然后指向 LoginInterceptor 和后端角色查询代码。按钮级权限我用一个自定义指令或者工具函数实现模板里控制删除、审批这类敏感按钮。function hasPerm(perm) { const perms JSON.parse(localStorage.getItem(perms) || []) return perms.includes(perm) }el-button v-ifhasPerm(employee:delete) typedanger clickhandleDelete(scope.row) 删除/el-button这里我把权限细化为字符串如employee:delete、leave:approve后端登录接口返回的 roles 列表里额外带上 perms。按钮级权限的好处是答辩演示时可以直接展示管理员能看到删除按钮普通员工看不到。这个设计在真实项目里是标配而大多数实训作品没有这层你做出来就是差异化亮点。5. 避坑指南人事管理系统开发和答辩中最常见的五个坑5.1 中文乱码数据入库后全是问号现象前端页面上显示中文正常但往数据库插入中文后变成??或者页面上读出来就是乱码。原因最常见的两种一是 JDBC 连接串没设置字符编码二是数据库表或字段的字符集不是 utf8mb4。实训项目用 MySQL 5.7 时默认字符集可能是 latin1插入中文直接丢数据。解决JDBC 连接串加上useUnicodetruecharacterEncodingutf8建表时统一DEFAULT CHARSETutf8mb4字符串字段用VARCHAR而不是TEXT的也一并定好。如果已经是乱码先ALTER TABLE employee CONVERT TO CHARACTER SET utf8mb4;再重新插入测试数据。5.2 LocalDateTime 前后端格式对不上现象前端传2023-08-11 14:30:00后端报JSON parse error或者后端返回的时间格式是一长串数字。原因Spring Boot 默认使用 Jackson 序列化LocalDateTime 的默认格式不是yyyy-MM-dd HH:mm:ss。前端和后端各按各的格式解析互相不认。解决在实体时间字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)同时配置全局的 Jackson 格式化器。注意用了 MyBatis-Plus 的自动填充后LocalDateTime 的序列化格式要单独验证特别是插入后回显时间往往会出问题。5.3 删除员工时外键约束报错现象删除一个员工时控制台报Cannot delete or update a parent row: a foreign key constraint fails。原因员工在请假申请表里有记录外键约束阻止了直接删除。这是数据库层面的正确行为不能绕过也不应该绕过。解决员工删除从物理删除改成逻辑删除。employee 表加deleted字段MyBatis-Plus 实体上加TableLogic删除接口改成update employee set deleted 1。这样请假记录仍然能关联到员工历史数据列表查询也会自动过滤掉已删除员工。这个方案比先删子表记录再删员工更合理因为考勤和请假记录属于审计数据不应该物理删除。5.4 密码明文存储答辩老师的第一个问题现象演示时打开数据库表所有人的密码直接显示成明文比如123456。老师当场问“这个系统的密码安全吗”原因图省事注册或初始化时直接把用户输入的密码存进数据库。这是非常明显的安全隐患没有任何辩解空间。解决引入 Spring Security 的BCryptPasswordEncoder存密码用encode()校验密码用matches()。注意 BCrypt 生成的密文长度接近 60 个字符user 表的 password 字段至少定VARCHAR(100)否则插入报错。初始化数据的密码也要生成密文后再写入 SQL这一步别偷懒。5.5 事务和索引被追问时答不上来现象功能全部跑通答辩时老师问“你的系统哪里用了事务哪里用了索引”现场愣住。原因代码是照着网上的例子敲的只关注功能能否跑通没有主动关注性能和安全设计。这比 bug 更危险因为功能能跑是应该的设计意识才是实训考察的重点。解决答辩前把三处亮点整理出来部门调动接口的Transactional事务、工号和部门编码的唯一索引、BCrypt 密码加密。每处用一两句话说清楚“为什么这样设计”比罗列十几个功能有用得多。回答事务问题时直接指到 3.2 节那段代码说明三个更新操作要么全部成功要么全部回滚这是最直观的事务案例。6. 让演示更稳的收尾技巧造数据、验接口、打包部署实训答辩最怕的不是功能缺而是用例太少演示点都串不起来。我习惯在答辩前写一个批量造数脚本生成几十条部门、员工和请假数据用真实姓名风格而不是“测试1”“测试2”演示时观感完全不同。WITH RECURSIVE seq AS ( SELECT 1 AS n UNION ALL SELECT n 1 FROM seq WHERE n 20 ) INSERT INTO dept (dept_name, dept_code) SELECT CONCAT(部门, n), CONCAT(D, LPAD(n, 3, 0)) FROM seq;这个脚本用 MySQL 8.0 的递归 CTE 快速生成 20 个部门。如果你用的是 MySQL 5.7递归 CTE 不兼容可以用UNION ALL拼数字表的方式替代。演示数据的关键不是数量多而是覆盖不同状态、不同部门比如请假申请要有待审批和已通过的这样列表筛选才有东西可展示。接口验证我不用 Postman 导出 JSON直接命令行 curl 跑通两个核心接口答辩现场即使断网也能演示。TOKEN$(curl -s -X POST http://localhost:8080/api/login \ -H Content-Type: application/json \ -d {username:admin,password:admin123} | jq -r .data.token) curl -s http://localhost:8080/api/employee/page?pageNum1pageSize10 \ -H Authorization: Bearer $TOKEN第一条命令走登录接口拿 token第二条带 token 查询员工分页列表。jq不是每个环境都装了如果命令行提示找不到 jq就先手动发第一条请求把 token 复制出来再拼到第二条请求里。演示前先把这两条命令跑一遍确认登录、token 解析、分页查询三个关键链路没有断。打包部署部分用 Maven 标准命令注意打包前把测试切到不依赖本地环境的配置否则演示环境连不上数据库会直接启动失败。mvn clean package -DskipTests java -jar target/hr-system.jar --server.port8080-DskipTests用来跳过测试代码执行实训阶段没有完善的自动化测试跳过是合理的。如果数据库地址变了启动参数加上--spring.datasource.url和--spring.datasource.password比去改配置文件再重新打包快很多。启动日志里看到Started关键字后再打开浏览器不要心急。最后说一个我自己的习惯不管接到什么新需求第一件事不是写代码而是先想清楚数据怎么流动、状态有哪些、谁有权限碰它。这套人事管理系统就是把这三个问题完整锻炼一遍的题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑