ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于SSM框架的网上人才招聘系统开发实战与避坑指南

基于SSM框架的网上人才招聘系统开发实战与避坑指南

简介:基于Java的SSM框架的网上人才招聘系统设计与实现毕业论文(Word文档,docx格式),面向计算机相关专业学生以及需要完成同类毕业设计的开发者,提供了从选题、系统设计到论文撰写的完整参考。内容围绕管理员与用户两类功能单元,覆盖企业信息审核、招聘信息发布、简历投递与职位申请等核心流程,采用JSP技术、MySQL数据库和浏览器/服务器(B/S)模式进行实现,并阐述了数据安全保护与系统测试等关键环节。压缩包仅含1个Word文档,大小约4.79MB,文档包含摘要、绪论、相关技术概述、系统功能设计、数据库设计及测试结果等章节,目录结构清晰,便于直接借鉴章节布局和论证思路。已有39人浏览学习,特别适合需要快速搭建论文框架的本科生,也可为开发者理解网上人才招聘系统的需求分析、模块划分和技术选型提供参考。

1. 网上人才招聘系统:SSM 组合凭什么还是毕业设计与小团队首选

如果你正在用 java 做毕设或接一个中小型招聘平台,大概率会搜到“java基于SSM的网上人才招聘系统设计与实现毕业论文.docx”这类标题。它本质上是一个典型的企业级 CRUD 项目:前端页面 + SpringMVC 控制器 + Service 业务层 + MyBatis 持久层,外加 MySQL 存储简历、职位、投递记录。SSM 不是最新潮的技术栈,但它把“面向接口编程”“声明式事务”“SQL 与 Java 分离”这些 java 基础功放在一个能跑通的系统里,对理解 java 后端开发流程非常友好。这套系统最值得做的不是炫技,而是让你把 jsp、Spring IoC、MyBatis 映射、拦截器鉴权这些零散知识点串成一条线。适合准备 java 面试八股文之前先动手做一遍的人,也适合需要快速交付、后续好维护的团队。

正文会从一个最小可运行的工程结构讲起,再拆数据库设计、SSM 常用注解的配置方式、简历与职位模块的编码思路,最后单独列一坑:那些在电脑上能跑、一部署就翻车的问题。你照着做,不用追求复杂业务,先把“投递—审核—通知”这条主链路跑通,比堆一堆没用的页面更有说服力。

2. 先搭一个能启动的 SSM 骨架:从 Maven 结构到 Spring 容器配置

2.1 为什么选 SSM 而不是 Spring Boot 来写招聘系统

可能你会问,现在新项目都用 Spring Boot,为什么这个题目还在用 SSM。常见原因是学校模板、旧系统维护或者课程设计指定了 SSM。另一个现实原因是,SSM 把“配置”这件事暴露得明明白白:你亲手写 web.xml、spring-mvc.xml、mybatis-config.xml,才会理解 Spring Boot 里那些“约定大于配置”到底省了什么。对 java 学习者来说,SSM 是理解 java 容器、bean 生命周期、AOP 事务的好教材。生产环境里,SSM 也常用于维护 2015 到 2020 年间留下的大量老系统,所以“会配 SSM”仍是 java 开发工程师面试题中经常出现的隐性要求。

从招聘系统本身看,业务不算复杂:用户注册登录、简历管理、职位发布、投递管理、管理员审核。这种强度用 SSM 完全够,而且 MyBatis 写动态 SQL(比如多条件筛选职位)比 JPA 更直观。如果你有 Spring Boot 经验,学 SSM 的额外成本就是记住各种 XML 标签,其他思想是通用的。

2.2 用 Maven 创建工程并锁定依赖版本

我一般先创建 Maven war 包项目,因为要部署到 Tomcat。关键依赖如下,版本建议锁死,不要随手写 latest:

<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.version>5.2.22.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.15</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>

这里有几个容易踩坑的地方:Spring 5.2 配 JDK 8 很稳,别用 Spring 6 配 JDK 8 会报 CGLIB 版本冲突;MySQL 驱动用 8.x 时,dataSource 的 driverClassName 要写com.mysql.cj.jdbc.Driver,同时 URL 里必须加serverTimezone=Asia/Shanghai,否则连接报时区错误。mybatis-spring 2.0.7 对应 MyBatis 3.5.x,这组搭配是用来跑招聘系统这种 CRUD 最省心的组合。

2.3 配置 applicationContext.xml 与 spring-mvc.xml 的分工

SSM 有两个核心 Spring 配置文件,很多人放在一起也跑得通,但分开更规范。applicationContext.xml管 service、dao、数据源、事务;spring-mvc.xml只管 controller、视图解析器、静态资源、拦截器。因为 controller 依赖 service,容器是父子关系,子容器(mvc)能拿到父容器(root)的 bean。

<!-- applicationContext.xml 核心片段 --> <context:component-scan base-package="com.hr.service"/> <context:property-placeholder location="classpath:db.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.hr.entity"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.hr.dao"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

这里依赖db.properties保存数据库连接信息。我习惯把 mapper 文件单独放resources/mapper,与 dao 接口所在包不在同一路径,所以必须通过mapperLocations告诉 MyBatis 去哪里找 XML。否则最常见的报错就是Invalid bound statement (not found),看着是接口方法没实现,其实是没扫描到 XML。

2.4 配置 web.xml 与 spring-mvc.xml:中文乱码和 404 从这里开始

web.xml 里注册 Spring 监听器和 DispatcherServlet。乱码问题通常不是页面编码,而是 CharacterEncodingFilter 没有放在过滤器链最前面。如果你 filter 顺序写错,POST 请求里的中文姓名、自我介绍会变成“???”,简历内容直接没法看。

<web-app version="3.1"> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <filter> <filter-name>encoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>

spring-mvc.xml 里除了开启@Controller和@RequestMapping扫描,还要配视图解析器。招聘系统页面比较传统,我常用 JSP 视图:

<context:component-scan base-package="com.hr.controller"/> <mvc:annotation-driven/> <mvc:default-servlet-handler/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>

mvc:default-servlet-handler这一行很重要:它告诉 Spring 静态资源(css、js、图片)由容器默认 Servlet 处理,否则你访问/css/style.css会被 DispatcherServlet 拦截,然后报 404。很多人部署后页面长得像毛坯房,就是少了这行。

3. 数据库设计与 MyBatis 映射:把招聘系统的五张核心表串起来

3.1 表结构设计:用户、简历、职位、投递、收藏

网上人才招聘系统如果只做“用户+职位+投递”三张表也能演示,但毕业论文或真实交付时,评委/甲方常会问:简历和用户是一对一吗?职位过期怎么处理?投递状态如何流转?所以至少需要五张表:t_user、t_resume、t_position、t_delivery、t_favorite。其中t_user用role字段区分求职者(0)、招聘者(1)、管理员(2),这样可以省去三套账号体系。

CREATE TABLE `t_user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL COMMENT 'MD5加密', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0求职者 1招聘者 2管理员', `phone` VARCHAR(20), `email` VARCHAR(100), `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `t_resume` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `real_name` VARCHAR(50), `gender` VARCHAR(2), `birthday` DATE, `education` VARCHAR(20), `work_years` INT, `phone` VARCHAR(20), `email` VARCHAR(100), `skill` TEXT, `work_experience` TEXT, `self_evaluation` TEXT, `create_time` DATETIME, `update_time` DATETIME, CONSTRAINT `fk_resume_user` FOREIGN KEY (`user_id`) REFERENCES `t_user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `t_position` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `company_name` VARCHAR(100) NOT NULL, `position_name` VARCHAR(100) NOT NULL, `salary_min` INT, `salary_max` INT, `city` VARCHAR(50), `education_require` VARCHAR(20), `experience_require` VARCHAR(20), `description` TEXT, `publisher_id` INT NOT NULL, `status` TINYINT DEFAULT 1 COMMENT '0下架 1上架', `create_time` DATETIME, `update_time` DATETIME, CONSTRAINT `fk_position_user` FOREIGN KEY (`publisher_id`) REFERENCES `t_user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `t_delivery` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `resume_id` INT NOT NULL, `position_id` INT NOT NULL, `user_id` INT NOT NULL, `status` TINYINT DEFAULT 0 COMMENT '0待查看 1已查看 2邀请面试 3不合适', `delivery_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT `fk_delivery_resume` FOREIGN KEY (`resume_id`) REFERENCES `t_resume`(`id`), CONSTRAINT `fk_delivery_position` FOREIGN KEY (`position_id`) REFERENCES `t_position`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里用utf8mb4而不是utf8,因为 MySQL 的utf8最多支持 3 字节,简历中如果有 emoji 表情(比如自我介绍里加个 👍)会直接插入失败。另一个坑:t_delivery里我冗余了user_id,虽然通过 resume 能反查,但投递列表页需要频繁按当前登录用户过滤,冗余一个字段能让 SQL 少一次 join,列表页响应更快。

3.2 设计 DAO 接口和 XML:多条件查询用动态 SQL

招聘系统最常见的需求是职位搜索:按关键字、城市、薪资范围、学历过滤。MyBatis 的<where>+<if>能很好解决这种“条件不确定”的 SQL。先写 DAO 接口:

public interface PositionDao { List<Position> search(PositionQuery query); int count(PositionQuery query); Position findById(int id); int insert(Position position); int updateStatus(@Param("id") int id, @Param("status") int status); }

对应PositionMapper.xml:

<select id="search" resultType="com.hr.entity.Position"> SELECT * FROM t_position <where> <if test="keyword != null and keyword != ''"> AND (position_name LIKE CONCAT('%', #{keyword}, '%') OR company_name LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="city != null and city != ''"> AND city = #{city} </if> <if test="minSalary != null"> AND salary_max &gt;= #{minSalary} </if> <if test="maxSalary != null"> AND salary_min &lt;= #{maxSalary} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>

这里的<where>标签会自动去掉第一个多余的AND,你不用担心 SQL 拼接出WHERE AND这种语法错误。注意&gt;=和&lt;=是 XML 转义,直接写>=会被 XML 解析器当成标签开始。另外分页用LIMIT #{offset}, #{pageSize},对应 Java 里(pageNum-1) * pageSize,这是 MyBatis 最简单的分页写法,不需要引入 PageHelper 插件。有次我把 offset 写成了LIMIT #{pageNum}, #{pageSize},第二页直接跳过了前两条数据,排错时一定要先看打印出来的 SQL。

3.3 Service 事务与业务校验:投递记录防重是关键

投递简历不是单纯的 insert。一个用户对同一个职位只能投递一次,否则招聘方会看到一堆重复简历,这在答辩时会被直接问“你怎么保证数据一致性”。我在 Service 层加校验,并用数据库唯一索引兜底:

@Service public class DeliveryServiceImpl implements DeliveryService { @Autowired private DeliveryDao deliveryDao; @Transactional(rollbackFor = Exception.class) @Override public void deliver(int userId, int resumeId, int positionId) { // 业务校验 Position position = positionDao.findById(positionId); if (position == null || position.getStatus() != 1) { throw new BusinessException("职位不存在或已下架"); } Resume resume = resumeDao.findById(resumeId); if (resume == null || resume.getUserId() != userId) { throw new BusinessException("简历不存在或不属于当前用户"); } int count = deliveryDao.countByUserAndPosition(userId, positionId); if (count > 0) { throw new BusinessException("你已经投递过该职位,请勿重复投递"); } Delivery delivery = new Delivery(); delivery.setUserId(userId); delivery.setResumeId(resumeId); delivery.setPositionId(positionId); delivery.setStatus(0); deliveryDao.insert(delivery); } }

@Transactional(rollbackFor = Exception.class)很关键:Spring 默认只对 RuntimeException 回滚,如果你抛的是BusinessException(继承自 Exception),不加 rollbackFor 的话事务不会回滚,简历表里可能插入了记录,投递表里却没插进去。这是 SSM 事务最经典的一个坑,面试时也常被问到。另外,我给t_delivery表加了联合唯一索引uk_user_position(user_id, position_id),这样即使代码并发漏判,数据库也会拦下重复投递。

4. Controller 层与 SSM 常用注解实战:从登录鉴权到文件上传

4.1 用 @Controller、@RequestMapping、@ResponseBody 搭 REST 风格接口

招聘系统的 Controller 有页面跳转和 AJAX 数据返回两种诉求。我习惯把返回 JSON 的接口统一用@ResponseBody,这样前端可以局部刷新投递状态,不用整页提交。下面是一个处理“投递简历”的典型写法:

@Controller @RequestMapping("/delivery") public class DeliveryController { @Autowired private DeliveryService deliveryService; @ResponseBody @RequestMapping(value = "/deliver", method = RequestMethod.POST) public Map<String, Object> deliver(HttpSession session, @RequestParam("positionId") int positionId) { Map<String, Object> result = new HashMap<>(); User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { result.put("code", 401); result.put("msg", "请先登录"); return result; } try { deliveryService.deliver(loginUser.getId(), loginUser.getResumeId(), positionId); result.put("code", 200); result.put("msg", "投递成功"); } catch (BusinessException e) { result.put("code", 500); result.put("msg", e.getMessage()); } return result; } }

注意@RequestParam和@PathVariable的区别:REST 风格中/delivery/deliver适合用 RequestParam 传 positionId;而/position/detail/{id}这种路径参数需要用@PathVariable。如果前端把参数名写错,Spring 会直接报 400,不会进到方法体,所以排错时可以先看浏览器 Network 面板的 Form Data 里字段名是否一致。

4.2 登录鉴权与拦截器:把“未登录就操作”堵在门外

招聘系统有很多操作需要登录后才能做,比如投递简历、修改简历、管理职位。每个方法里判 session 当然可以,但代码重复太多。我一般写一个LoginInterceptor统一拦截,并用注解区分哪些路径需要登录、哪些路径需要管理员:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); // 判断是否需要管理员权限 if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; RequireAdmin requireAdmin = hm.getMethodAnnotation(RequireAdmin.class); if (requireAdmin != null) { if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } if (user.getRole() != 2) { response.sendRedirect(request.getContextPath() + "/403.jsp"); return false; } } } if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } return true; } }

然后在 spring-mvc.xml 里注册拦截路径:

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/register"/> <mvc:exclude-mapping path="/position/list"/> <mvc:exclude-mapping path="/position/detail/**"/> <mvc:exclude-mapping path="/css/**"/> <mvc:exclude-mapping path="/js/**"/> <mvc:exclude-mapping path="/images/**"/> <bean class="com.hr.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>

这个配置经常出问题的地方是exclude-mapping没有包含静态资源。如果有 css/js 路径没放行,用户访问首页时样式加载会被拦截器重定向到登录页,控制台一片红。还有一点:我自定义了@RequireAdmin注解放在管理员接口上,这样不用在拦截器里写死 URL 列表,新增管理员接口时只要加注解就行,这是 java 注解驱动开发比较典型的用法。

4.3 处理简历上传:CommonsMultipartResolver 的配置与大小限制

招聘系统里常需要上传头像或附件简历(PDF/Word)。Spring MVC 的文件上传依赖CommonsMultipartResolver,配置如下:

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="UTF-8"/> <property name="maxUploadSize" value="10485760"/> <property name="maxInMemorySize" value="4096"/> </bean>

Controller 接收:

@ResponseBody @RequestMapping(value = "/upload", method = RequestMethod.POST) public String upload(@RequestParam("file") MultipartFile file, HttpServletRequest request) { if (file.isEmpty()) { return "请选择文件"; } String originalName = file.getOriginalFilename(); String ext = originalName.substring(originalName.lastIndexOf(".")); String newName = UUID.randomUUID().toString().replace("-", "") + ext; String realPath = request.getServletContext().getRealPath("/upload"); File dir = new File(realPath); if (!dir.exists()) { dir.mkdirs(); } try { file.transferTo(new File(dir, newName)); return "upload/" + newName; } catch (IOException e) { e.printStackTrace(); return "上传失败"; } }

上传后有三个常见坑:第一,表单必须加enctype="multipart/form-data",否则@RequestParam("file")拿不到对象;第二,保存路径不要硬编码成/data/upload,不同环境路径不同,用getRealPath("/upload")能跟随 webapps 目录部署,简单演示够用,但重启后可能丢失,生产环境建议存独立目录并配置虚拟映射;第三,file.transferTo在跨盘或目标文件已存在时会报 IOException,最好先判断dir.exists()再创建。

5. SSM 系统避坑实录:从本地 Tomcat 到答辩演示的四个翻车点

5.1 现象:Invalid bound statement (not found),Dao 接口明明有方法

原因通常是 Mapper XML 没被扫描到。常见有三种情况:mapperLocations路径写错(比如classpath:mapper/*.xml但 XML 在com/hr/mapper下);XML 的 namespace 写的不是接口全限定名;接口方法名与 XML 中 id 不一致。解决方法是先确认编译后的target/classes/mapper目录里有没有对应 XML,没有就检查 resources 目录标识,有就打开 XML 看<mapper namespace="com.hr.dao.PositionDao">是否正确。

5.2 现象:Post 请求中文全部乱码,数据库显示 “???”

原因不是 JSP 页面没设 UTF-8,而是CharacterEncodingFilter没有拦截请求,或者 filter 顺序不对。另一种可能是 MySQL 连接 URL 少了characterEncoding=UTF-8。解决:把CharacterEncodingFilter放在 web.xml 最前面;url 后追加?useUnicode=true&characterEncoding=UTF-8;同时确认表字符集是utf8mb4。这三层都要到位,漏一层都不行。

5.3 现象:启动后一个接口 404,但 controller 代码看着没问题

原因大概率是组件扫描包写窄了。比如spring-mvc.xml只扫描com.hr.controller,而 controller 里@Autowired的 service 在另一个包com.hr.service.impl,虽然 applicationContext 扫了 service,但可能漏了com.hr.service下一层 impl。或者@Controller注解没生效,因为 spring-mvc.xml 配的是<context:component-scan base-package="com.hr" use-default-filters="false">,把@Controller过滤掉了。解决:统一扫描com.hr.*,或者明确 include-filter 包含org.springframework.stereotype.Controller。我用后者更可控,因为不会误扫描到 service。

5.4 现象:投递简历时提示“服务器内部错误”,但控制台没有异常

这经常是@ResponseBody返回对象没有默认构造函数,或者对象里有无法序列化的字段。比如Position里有一个Date类型的createTime,Jackson 默认序列化没问题,但如果你把Date配成自定义类型,就没法转 JSON。更隐蔽的情况是实体类里有一个List<Delivery>,而 Delivery 又反向关联 Position,产生循环引用,Jackson 直接抛异常。解决:用@JsonIgnore标记反向关联字段,或者避免在查询时一次性把关联对象全部加载。答辩演示时遇到 500 页面,优先看 Tomcat 的 localhost.log,前后端分离看 Network 响应体,别只看浏览器控制台。

6. 进阶技巧:把简历导出与数据统计做成加分项

招聘系统做完基础 CRUD 后,最好加两个实用功能:导出简历 PDF/Word,以及职位投递量统计图表。这两块都能延伸出 java 基础与面试题里常考的 IO 流、模板引擎、日期格式化、行级权限等话题。

简历导出我常用开源的 freemarker 模板生成 HTML,再用IText或OpenPDF转 PDF。核心思路是:先把简历对象填充到 FTL 模板,渲染成 HTML 字符串,然后用 PDF 工具包写入输出流。下面是一个简化示例:

@GetMapping("/resume/export/{id}") public void export(@PathVariable int id, HttpServletResponse response) throws Exception { Resume resume = resumeService.getFullResume(id); // 检查权限:只允许本人或管理员导出 User current = (User) request.getSession().getAttribute("loginUser"); if (current.getRole() != 2 && resume.getUserId() != current.getId()) { response.sendError(403); return; } // 生成 HTML Template template = freemarkerConfig.getTemplate("resume.ftl"); StringWriter writer = new StringWriter(); template.process(resume, writer); String html = writer.toString(); // 转 PDF 输出 response.setContentType("application/pdf"); response.setHeader("Content-Disposition", "attachment;filename=resume_" + id + ".pdf"); ITextRenderer renderer = new ITextRenderer(); renderer.setDocumentFromString(html); renderer.layout(); renderer.createPDF(response.getOutputStream()); }

这段代码里的权限校验值得重点说明:导出接口不能只靠拦截器判断“已登录”,还得判断“资源归属”,也就是行级权限。学生简历只能自己导出,管理员能导出全部,招聘者不能导出求职者简历。不然系统在答辩演示时容易被评委一句“你能导出别人的简历吗”问翻车。

数据统计部分可以用一个简单的 SQL 按职位分组统计投递数量:

SELECT p.position_name, COUNT(d.id) AS delivery_count FROM t_position p LEFT JOIN t_delivery d ON p.id = d.position_id WHERE p.publisher_id = #{publisherId} GROUP BY p.id, p.position_name ORDER BY delivery_count DESC LIMIT 10;

这里用LEFT JOIN能保留投递数为 0 的职位,如果你用INNER JOIN,那些没人投的职位会被过滤掉,统计图就不完整了。前端可以把这个结果渲染成简单的柱状图,用 JSP 配合 ECharts 就行,不需要引入后端渲染框架。

回顾我做过的一个类似系统,最花时间的不是 CRUD,而是“边界”:重复投递、越权访问、上传文件大小、字符编码。这些点想清楚,代码写起来很快。另外,SSM 项目里不要把 SQL 拼接在 Java 代码中,哪怕只是小的更新语句,也放进 Mapper XML,这样以后换数据库、改字段时只改映射文件,不动 Java 层。希望这篇笔记能帮你把“网上人才招聘系统”从能跑通做到能答辩、能上线演示,少走几个常见的坑。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

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