【SpringBoot Web实战】从零构建企业级人事管理系统:部门与员工模块的CRUD实践
1. 项目背景与技术选型企业级人事管理系统是每个公司都离不开的核心应用它直接关系到组织架构管理和员工信息维护的效率。传统的人事管理系统往往采用单体架构存在扩展性差、维护成本高等问题。而基于SpringBoot的现代化解决方案能够快速构建高可用的RESTful服务。为什么选择SpringBoot我在实际项目中验证过它的三大优势内嵌Tomcat无需单独部署Web服务器一个jar包就能运行自动配置90%的常用配置已经预设好比如数据库连接池起步依赖引入spring-boot-starter-web就包含MVC、JSON等全套组件技术栈组合建议dependencies !-- Web核心 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 数据库访问 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.0/version /dependency !-- 分页插件 -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.2/version /dependency /dependencies2. 数据库设计与实体建模先来看部门表的DDL设计这里有个坑我踩过 - 一定要设置parent_id实现树形结构CREATE TABLE department ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT 部门名称, parent_id bigint DEFAULT NULL COMMENT 父部门ID, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_parent (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci;员工表设计时特别注意使用逻辑外键关联部门敏感字段如身份证号需要加密存储添加状态字段方便软删除对应的Java实体类要遵循JPA规范Data public class Employee { private Long id; private String name; private Integer gender; private String mobile; JsonFormat(pattern yyyy-MM-dd) private LocalDate entryDate; private Integer status; // 1在职 2离职 TableField(exist false) private Department department; }3. RESTful接口规范实践遵循Restful风格时我推荐这样的URL设计部门资源/api/departments员工资源/api/employees子资源/api/departments/{id}/employees统一响应体可以这样封装public class RT { private Integer code; private String msg; private T data; public static T RT ok(T data) { RT result new R(); result.setCode(200); result.setData(data); return result; } }在Controller层的典型应用RestController RequestMapping(/api/departments) public class DeptController { GetMapping public RListDepartment listDepartments() { return R.ok(deptService.listAll()); } DeleteMapping(/{id}) public RVoid delete(PathVariable Long id) { deptService.deleteById(id); return R.ok(null); } }4. 核心业务逻辑实现4.1 部门树形结构查询这里推荐使用MyBatis的嵌套查询resultMap idtreeResultMap typeDepartment collection propertychildren columnid selectfindByParentId/ /resultMap select idfindByParentId resultMaptreeResultMap SELECT * FROM department WHERE parent_id #{id} /selectService层处理树形结构public ListDepartment getDepartmentTree() { ListDepartment roots mapper.selectByParentId(null); roots.forEach(root - { root.setChildren(getChildren(root.getId())); }); return roots; }4.2 员工分页查询PageHelper的典型用法public PageInfoEmployee listEmployees(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); ListEmployee list mapper.selectAll(); return new PageInfo(list); }带条件分页查询的Mapper示例select idselectByCondition resultTypeEmployee SELECT * FROM employee where if testname ! null AND name LIKE CONCAT(%,#{name},%) /if if testdeptId ! null AND department_id #{deptId} /if /where /select4.3 事务管理在批量操作时务必添加事务注解Transactional(rollbackFor Exception.class) public void batchImport(ListEmployee employees) { employees.forEach(emp - { if(StringUtils.isEmpty(emp.getMobile())) { throw new RuntimeException(手机号不能为空); } mapper.insert(emp); }); }5. 前后端联调技巧5.1 跨域问题解决推荐全局配置方式Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(*) .allowedMethods(*) .maxAge(3600); } }5.2 接口文档生成Swagger的配置示例Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(com.example.hr)) .paths(PathSelectors.any()) .build(); }5.3 参数校验实践使用Validation注解Data public class EmployeeDTO { NotBlank(message 姓名不能为空) private String name; Pattern(regexp ^1[3-9]\\d{9}$, message 手机号格式错误) private String mobile; }Controller层校验PostMapping public R addEmployee(Valid RequestBody EmployeeDTO dto) { return R.ok(employeeService.add(dto)); }6. 性能优化方案6.1 二级缓存配置MyBatis开启二级缓存cache evictionLRU flushInterval60000 size512 readOnlytrue/6.2 接口响应优化使用Spring Cache注解Cacheable(value dept, key #id) public Department getById(Long id) { return mapper.selectById(id); }6.3 SQL性能监控建议配置Druid监控# 开启监控统计 spring.datasource.druid.filter.stat.enabledtrue # 开启慢SQL记录 spring.datasource.druid.filter.stat.log-slow-sqltrue spring.datasource.druid.filter.stat.slow-sql-millis20007. 异常处理机制全局异常处理器示例ControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) ResponseBody public R handleBusinessException(BusinessException e) { return R.fail(e.getCode(), e.getMessage()); } ExceptionHandler(Exception.class) ResponseBody public R handleException(Exception e) { log.error(系统异常, e); return R.fail(500, 系统繁忙); } }自定义业务异常类public class BusinessException extends RuntimeException { private Integer code; public BusinessException(Integer code, String message) { super(message); this.code code; } }8. 安全防护措施8.1 SQL注入防护永远不要拼接SQL// 错误示范 String sql SELECT * FROM user WHERE name name ; // 正确做法 mapper.selectByName(name);8.2 XSS过滤添加全局过滤器Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { chain.doFilter(new XssHttpServletRequestWrapper(request), response); }8.3 数据脱敏在DTO层处理敏感字段public String getMobile() { return StringUtils.overlay(mobile, ****, 3, 7); }9. 部署与监控9.1 打包部署推荐使用分层构建Docker镜像FROM adoptopenjdk:11-jre-hotspot COPY target/hr-system.jar /app.jar ENTRYPOINT [java,-jar,/app.jar]9.2 健康检查SpringBoot Actuator配置management.endpoints.web.exposure.includehealth,info management.endpoint.health.show-detailsalways9.3 日志收集Logback的ELK配置示例appender nameLOGSTASH classnet.logstash.logback.appender.LogstashTcpSocketAppender destinationlogstash:5044/destination encoder classnet.logstash.logback.encoder.LogstashEncoder/ /appender10. 扩展功能展望当基础功能稳定后可以考虑集成工作流引擎处理审批流程增加BI模块进行人力数据分析开发移动端应用支持随时随地办公接入AI能力实现智能排班我在实际项目中遇到过部门树形结构查询的性能瓶颈后来通过引入Redis缓存部门关系数据查询性能提升了8倍。关键代码是这样的public ListDepartment getCachedDepartmentTree() { String cacheKey dept:tree; String json redisTemplate.opsForValue().get(cacheKey); if(StringUtils.isNotEmpty(json)) { return JSON.parseArray(json, Department.class); } ListDepartment tree buildDepartmentTree(); redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(tree), 1, TimeUnit.HOURS); return tree; }