
1. 项目概述基于SSM的人才招聘管理系统作为一名有多年Java开发经验的程序员我最近完成了一个基于SSM框架的人才招聘管理系统。这个系统采用SpringSpringMVCMyBatis的技术栈实现了企业招聘和求职者应聘的全流程管理。从技术选型到功能实现再到最后的测试部署整个过程让我积累了不少实战经验今天就来详细分享一下这个项目的开发过程。人才招聘管理系统是现代企业HR部门不可或缺的工具它能够有效提升招聘效率、优化人才管理流程。传统的招聘方式往往存在信息不对称、流程繁琐等问题而这个系统通过信息化手段解决了这些痛点。系统主要面向两类用户企业HR和求职者为他们提供简历投递、职位发布、面试安排等核心功能。2. 系统架构设计2.1 技术栈选型在项目初期技术选型是至关重要的决策。经过仔细考量我最终确定了以下技术组合后端技术Spring框架5.3.18版本提供IoC和AOP支持SpringMVC处理Web请求和响应MyBatis3.5.9版本作为ORM框架Shiro1.8.0版本负责权限控制前端技术Bootstrap5.1.3版本响应式布局jQuery3.6.0版本简化DOM操作Thymeleaf3.0.15版本模板引擎数据库MySQL8.0.28版本Redis6.2.6版本用于缓存开发工具IDEA2022.1版本Maven3.8.4版本依赖管理Git版本控制选择这些技术主要基于以下考虑Spring框架成熟稳定社区支持好MyBatis相比Hibernate更灵活适合复杂SQL场景Shiro比Spring Security更轻量学习曲线平缓Bootstrap能快速构建美观的响应式界面2.2 系统架构设计系统采用经典的三层架构表现层(Web层) - 业务逻辑层(Service层) - 数据访问层(DAO层)表现层使用SpringMVC处理HTTP请求Thymeleaf渲染视图。这一层主要负责接收用户请求参数校验调用Service层返回响应业务逻辑层包含核心业务逻辑如用户认证授权简历处理流程职位匹配算法面试安排逻辑数据访问层通过MyBatis与数据库交互实现数据持久化复杂查询优化事务管理2.3 数据库设计数据库设计遵循第三范式主要表结构包括用户表(users)CREATE TABLE users ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, salt varchar(50) NOT NULL, email varchar(100) NOT NULL, phone varchar(20) DEFAULT NULL, user_type tinyint NOT NULL COMMENT 1-求职者 2-企业HR, status tinyint NOT NULL DEFAULT 1, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY idx_username (username), UNIQUE KEY idx_email (email) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;简历表(resumes)CREATE TABLE resumes ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, title varchar(100) NOT NULL, content text NOT NULL, education varchar(50) NOT NULL, work_experience int NOT NULL, skills varchar(255) NOT NULL, status tinyint NOT NULL DEFAULT 1, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;职位表(jobs)CREATE TABLE jobs ( id bigint NOT NULL AUTO_INCREMENT, company_id bigint NOT NULL, title varchar(100) NOT NULL, description text NOT NULL, requirements text NOT NULL, salary_range varchar(50) NOT NULL, location varchar(100) NOT NULL, status tinyint NOT NULL DEFAULT 1, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_company_id (company_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;数据库设计时特别注意了以下几点所有表都有create_time和update_time字段便于追踪数据变更为常用查询字段添加了索引使用utf8mb4字符集支持完整的Unicode设置了合理的字段长度和类型3. 核心功能实现3.1 用户认证与授权系统采用Shiro框架实现认证和授权功能。核心配置如下ShiroConfig.javaConfiguration public class ShiroConfig { Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); MapString, String filterMap new LinkedHashMap(); filterMap.put(/static/**, anon); filterMap.put(/login, anon); filterMap.put(/register, anon); filterMap.put(/**, authc); shiroFilter.setFilterChainDefinitionMap(filterMap); shiroFilter.setLoginUrl(/login); shiroFilter.setSuccessUrl(/dashboard); return shiroFilter; } Bean public Realm realm() { return new CustomRealm(); } Bean public SecurityManager securityManager(Realm realm) { DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(realm); return securityManager; } }CustomRealm.javapublic class CustomRealm extends AuthorizingRealm { Autowired private UserService userService; Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username (String) principals.getPrimaryPrincipal(); User user userService.findByUsername(username); SimpleAuthorizationInfo authorizationInfo new SimpleAuthorizationInfo(); if (user.getUserType() 1) { authorizationInfo.addRole(JOB_SEEKER); } else { authorizationInfo.addRole(HR); } return authorizationInfo; } Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username (String) token.getPrincipal(); User user userService.findByUsername(username); if (user null) { throw new UnknownAccountException(用户不存在); } return new SimpleAuthenticationInfo( user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName() ); } }认证流程说明用户提交用户名和密码Shiro调用CustomRealm的doGetAuthenticationInfo方法根据用户名查询用户信息比对密码密码使用盐值加密认证成功后将用户信息存入Session授权流程说明用户访问受保护资源Shiro调用CustomRealm的doGetAuthorizationInfo方法根据用户类型分配角色检查用户是否有访问该资源的权限3.2 简历管理模块简历管理是求职者的核心功能主要包括创建/编辑简历预览简历投递简历ResumeController.javaController RequestMapping(/resume) public class ResumeController { Autowired private ResumeService resumeService; GetMapping(/create) public String createForm(Model model) { model.addAttribute(resume, new Resume()); return resume/create; } PostMapping(/save) public String save(Valid Resume resume, BindingResult result, AuthenticationPrincipal User user) { if (result.hasErrors()) { return resume/create; } resume.setUserId(user.getId()); resumeService.save(resume); return redirect:/resume/list; } GetMapping(/list) public String list(Model model, AuthenticationPrincipal User user) { ListResume resumes resumeService.findByUserId(user.getId()); model.addAttribute(resumes, resumes); return resume/list; } GetMapping(/preview/{id}) public String preview(PathVariable Long id, Model model) { Resume resume resumeService.findById(id); model.addAttribute(resume, resume); return resume/preview; } }简历投递功能实现Service public class ApplicationServiceImpl implements ApplicationService { Autowired private ApplicationMapper applicationMapper; Autowired private JobMapper jobMapper; Autowired private ResumeMapper resumeMapper; Transactional Override public void apply(Long jobId, Long resumeId, Long userId) { // 检查职位是否存在 Job job jobMapper.selectById(jobId); if (job null || job.getStatus() ! 1) { throw new BusinessException(职位不存在或已关闭); } // 检查简历是否属于当前用户 Resume resume resumeMapper.selectById(resumeId); if (resume null || !resume.getUserId().equals(userId)) { throw new BusinessException(简历不存在或不属于当前用户); } // 检查是否已经投递过 LambdaQueryWrapperApplication queryWrapper new LambdaQueryWrapper(); queryWrapper.eq(Application::getJobId, jobId) .eq(Application::getResumeId, resumeId); if (applicationMapper.selectCount(queryWrapper) 0) { throw new BusinessException(已经投递过该职位); } // 创建投递记录 Application application new Application(); application.setJobId(jobId); application.setResumeId(resumeId); application.setUserId(userId); application.setStatus(0); // 0-待处理 application.setCreateTime(new Date()); application.setUpdateTime(new Date()); applicationMapper.insert(application); } }3.3 职位搜索功能职位搜索采用了Elasticsearch来提高查询效率主要实现以下功能多条件组合搜索分页显示搜索结果高亮JobSearchServiceImpl.javaService public class JobSearchServiceImpl implements JobSearchService { Autowired private RestHighLevelClient elasticsearchClient; Override public PageJob search(String keyword, String location, String salaryRange, int page, int size) { try { // 构建搜索请求 SearchRequest searchRequest new SearchRequest(jobs); SearchSourceBuilder sourceBuilder new SearchSourceBuilder(); // 构建布尔查询 BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); if (StringUtils.isNotBlank(keyword)) { boolQuery.must(QueryBuilders.multiMatchQuery(keyword, title, description, requirements)); } if (StringUtils.isNotBlank(location)) { boolQuery.must(QueryBuilders.termQuery(location, location)); } if (StringUtils.isNotBlank(salaryRange)) { String[] ranges salaryRange.split(-); if (ranges.length 2) { boolQuery.must(QueryBuilders .rangeQuery(salary_min) .gte(Integer.parseInt(ranges[0])) .lte(Integer.parseInt(ranges[1]))); } } sourceBuilder.query(boolQuery); sourceBuilder.from((page - 1) * size); sourceBuilder.size(size); // 设置高亮 HighlightBuilder highlightBuilder new HighlightBuilder(); highlightBuilder.field(title); highlightBuilder.field(description); highlightBuilder.preTags(em classhighlight); highlightBuilder.postTags(/em); sourceBuilder.highlighter(highlightBuilder); searchRequest.source(sourceBuilder); // 执行搜索 SearchResponse response elasticsearchClient.search( searchRequest, RequestOptions.DEFAULT); // 处理结果 ListJob jobs new ArrayList(); for (SearchHit hit : response.getHits().getHits()) { Job job JSON.parseObject(hit.getSourceAsString(), Job.class); // 处理高亮 MapString, HighlightField highlightFields hit.getHighlightFields(); if (highlightFields.containsKey(title)) { job.setTitle(highlightFields.get(title).fragments()[0].string()); } jobs.add(job); } long total response.getHits().getTotalHits().value; return new PageImpl(jobs, PageRequest.of(page - 1, size), total); } catch (IOException e) { throw new RuntimeException(搜索职位失败, e); } } }4. 系统优化与部署4.1 性能优化在实际开发中我对系统进行了以下几方面的性能优化数据库优化为常用查询字段添加索引使用explain分析慢查询优化复杂SQL避免全表扫描合理使用连接查询和子查询缓存策略使用Redis缓存热点数据实现二级缓存MyBatisRedis设置合理的缓存过期时间Redis配置示例Configuration EnableCaching public class RedisConfig extends CachingConfigurerSupport { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // 使用Jackson2JsonRedisSerializer序列化value Jackson2JsonRedisSerializerObject serializer new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.activateDefaultTyping( om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL ); serializer.setObjectMapper(om); // 设置key和value的序列化规则 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(1)) // 设置缓存有效期1小时 .disableCachingNullValues() .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory) .cacheDefaults(config) .transactionAware() .build(); } }前端优化使用CDN加载静态资源实现懒加载压缩JS/CSS文件合理使用浏览器缓存4.2 安全防护系统安全是重中之重我采取了以下安全措施XSS防护使用Thymeleaf默认的HTML转义对用户输入进行过滤设置HttpOnly的CookieCSRF防护启用Spring Security的CSRF保护重要操作使用POST请求添加CSRF TokenSQL注入防护使用MyBatis预编译语句避免拼接SQL使用MyBatis的#{}语法密码安全使用BCrypt加密添加盐值强制复杂密码策略密码加密实现Service public class PasswordService { public String encryptPassword(String password, String salt) { return new SimpleHash(SHA-256, password, salt, 1024).toString(); } public boolean matches(String rawPassword, String salt, String encodedPassword) { String newEncoded encryptPassword(rawPassword, salt); return newEncoded.equals(encodedPassword); } public String generateSalt() { return UUID.randomUUID().toString().replaceAll(-, ).substring(0, 16); } }4.3 系统部署系统采用Docker容器化部署主要包含以下服务Web应用服务Spring BootMySQL数据库Redis缓存Elasticsearch搜索Nginx反向代理docker-compose.ymlversion: 3.8 services: web: image: recruitment-system:1.0.0 container_name: recruitment-web ports: - 8080:8080 environment: - SPRING_PROFILES_ACTIVEprod - DB_URLjdbc:mysql://mysql:3306/recruitment - DB_USERNAMEroot - DB_PASSWORD123456 - REDIS_HOSTredis depends_on: - mysql - redis - elasticsearch mysql: image: mysql:8.0.28 container_name: recruitment-mysql ports: - 3306:3306 environment: - MYSQL_ROOT_PASSWORD123456 - MYSQL_DATABASErecruitment volumes: - mysql_data:/var/lib/mysql redis: image: redis:6.2.6 container_name: recruitment-redis ports: - 6379:6379 volumes: - redis_data:/data elasticsearch: image: elasticsearch:7.16.3 container_name: recruitment-elasticsearch environment: - discovery.typesingle-node - bootstrap.memory_locktrue - ES_JAVA_OPTS-Xms512m -Xmx512m ulimits: memlock: soft: -1 hard: -1 ports: - 9200:9200 volumes: - es_data:/usr/share/elasticsearch/data nginx: image: nginx:1.21.6 container_name: recruitment-nginx ports: - 80:80 volumes: - ./nginx.conf:/etc/nginx/nginx.conf depends_on: - web volumes: mysql_data: redis_data: es_data:Nginx配置worker_processes 1; events { worker_connections 1024; } http { upstream backend { server web:8080; } server { listen 80; server_name localhost; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /static/ { alias /static/; expires 30d; } } }部署步骤构建Docker镜像docker build -t recruitment-system:1.0.0 .启动服务docker-compose up -d初始化数据库执行SQL脚本导入Elasticsearch数据使用Logstash或API导入5. 开发经验与问题解决5.1 开发中的典型问题在开发过程中我遇到了几个典型问题以下是解决方案问题1MyBatis关联查询性能低下当查询职位列表需要显示公司信息时最初使用了一对一关联查询resultMap idjobWithCompany typeJob id propertyid columnid/ !-- 其他字段 -- association propertycompany javaTypeCompany id propertyid columncompany_id/ !-- 公司其他字段 -- /association /resultMap select idselectJobsWithCompany resultMapjobWithCompany SELECT j.*, c.* FROM jobs j LEFT JOIN companies c ON j.company_id c.id LIMIT #{offset}, #{size} /select当数据量增大时这种查询方式性能明显下降。解决方案改为两次查询先查询职位列表再批量查询公司信息public ListJob listJobs(int page, int size) { // 查询职位列表 ListJob jobs jobMapper.selectPage(page, size); // 收集公司ID SetLong companyIds jobs.stream() .map(Job::getCompanyId) .collect(Collectors.toSet()); // 批量查询公司信息 MapLong, Company companyMap companyMapper.selectByIds(companyIds) .stream() .collect(Collectors.toMap(Company::getId, Function.identity())); // 设置公司信息 jobs.forEach(job - job.setCompany(companyMap.get(job.getCompanyId()))); return jobs; }问题2事务失效问题在简历投递方法上添加了Transactional注解但事务没有生效。原因分析方法被同类中的其他方法调用绕过了代理异常类型不是RuntimeException数据库引擎不支持事务如MyISAM解决方案确保方法是通过代理调用的捕获所有异常并转换为RuntimeException使用InnoDB引擎Service public class ApplicationServiceImpl implements ApplicationService { Transactional(rollbackFor Exception.class) Override public void apply(Long jobId, Long resumeId, Long userId) { try { // 业务逻辑 } catch (Exception e) { throw new RuntimeException(投递失败, e); } } }5.2 开发经验总结代码规范遵循阿里巴巴Java开发手册使用Checkstyle进行代码检查重要方法添加详细注释保持方法短小精悍不超过50行测试策略单元测试覆盖核心业务逻辑集成测试验证模块交互使用Mockito模拟依赖持续集成GitHub Actions测试示例SpringBootTest public class ResumeServiceTest { Autowired private ResumeService resumeService; MockBean private ResumeMapper resumeMapper; Test public void testCreateResume() { Resume resume new Resume(); resume.setTitle(Java开发工程师); resume.setContent(5年Java开发经验...); when(resumeMapper.insert(any())).thenReturn(1); resumeService.save(resume); verify(resumeMapper, times(1)).insert(any()); } }版本控制使用Git进行版本管理遵循Git Flow工作流提交信息规范化定期合并主干分支文档编写使用Swagger生成API文档维护CHANGELOG.md编写部署手册记录常见问题解决方案5.3 项目扩展方向这个系统还有很大的扩展空间未来可以考虑微服务化改造拆分为用户服务、简历服务、职位服务等使用Spring Cloud Alibaba引入服务注册发现Nacos添加API网关Spring Cloud Gateway大数据分析收集用户行为数据使用Flink进行实时分析构建人才画像智能推荐职位移动端支持开发微信小程序实现APP推送通知优化移动端用户体验国际化支持多语言切换时区处理本地化内容这个人才招聘管理系统的开发过程让我对SSM框架有了更深入的理解特别是在性能优化和安全防护方面积累了不少实战经验。希望我的分享能对正在开发类似系统的同学有所帮助。如果在实现过程中遇到任何问题欢迎交流讨论。