这套“基于Java+SSM+Flask的学生就业管理系统”,是我前阵子帮某高校信息中心落地的项目。整个系统核心围绕学生就业信息管理平台展开,学生端可以完善简历、浏览岗位、在线投递、查看就业进度,企业端能发布职位、筛选简历、反馈面试结果,管理员还能做学生就业跟踪、数据统计和报表导出。技术栈选的是Java生态的SSM(Spring + SpringMVC + MyBatis)作为主业务后端,再用Python Flask搭了一个辅助服务,专门处理职位采集、简历解析这类偏工具型的任务。如果你正准备做毕业设计、课程设计,或者想理解“Java + Python混合开发”在真实项目里怎么配合,这篇文章可以当作一份比较完整的参考。
项目建成后,最直观的价值是把原来靠Excel表格登记就业信息的方式彻底换掉了。辅导员不用再拿着几十个文件来回合并,学生也能实时看到自己的投递状态,学校的就业率统计从“月底手动算”变成“系统自动出”。这套东西不是那种花哨的演示Demo,而是能真正给就业办日常管理用的工具。
1. 项目整体设计与技术选型思路
1.1 需求定位与功能全景
在动手写代码之前,我把需求拆成三个角色的使用场景:
- 学生用户:注册登录、完善个人信息和简历、浏览招聘职位、投递简历、查看面试邀请、收到录用/淘汰通知、查看自己的就业跟踪时间线。
- 企业用户:注册登录、维护公司资料、发布职位、查看投递该职位的学生列表、下载简历、反馈面试结果(待筛选、待面试、已录用、不合适)。
- 系统管理员:学生信息审核、企业信息审核、职位审核、跟踪所有学生的就业状态、按学院/专业/年份统计就业率、导出报表。
从功能上看,这就是一个典型的“B端管理 + C端服务”混合系统。学生和企业端更看重流程顺畅,管理员端更看重数据完整和可统计。所以我在做数据库设计时,把用户信息、企业信息、职位信息、投递记录、跟踪流水全部分开,方便后续按不同维度查数。
1.2 为什么用Java+SSM+Flask混合架构
很多人会问:一个学生就业管理系统,老老实实用SSM写完整套不好吗?为什么要额外引入Flask?
这里说一下我的考量。
SSM这套组合在Java业务系统里已经非常成熟,Spring管理组件、SpringMVC处理请求、MyBatis操作数据库,分工明确,稳定可靠。但项目里有几个功能是Java实现起来比较繁琐的,比如:
- 解析学生上传的PDF、Word格式简历,提取文本内容;
- 定时抓取公开的招聘信息源,清洗后写入本地库;
- 给投递学生发送站内信和邮件通知。
这些操作如果用Java写,不是不行,但代码量会明显增加,而且Python生态里现成的库更多。比如解析简历可以用PyPDF2、python-docx,抓取信息用requests+BeautifulSoup,写起来比Java要简洁得多。
于是我把系统拆成了两个服务:
- Java+SSM服务:承担所有核心业务,包括用户认证、职位管理、投递流程、就业跟踪、统计报表。
- Flask辅助服务:封装简历解析、职位采集、邮件通知、关键词匹配推荐这些“工具型能力”,对Java服务提供HTTP接口。
Java和Flask之间通过RESTful API通信,两边都只依赖JSON格式数据。这样一来,Java端保持业务纯粹,Flask端也能独立开发和测试,后期哪个服务压力大了还可以单独扩展。
1.3 系统架构与数据流
系统整体是前后端分离的经典结构,不过这里的前端页面不是独立Vue工程,而是用JSP/HTML放在SpringMVC里托管。考虑到项目体量,没有强行拆成“前端工程 + 后端工程 + 网关”的三层架构,而是走“浏览器 -> Java后端 -> MySQL/Redis”的主链路,Flask服务挂在旁边按需被调用。
一次完整的数据流大致是:
- 学生登录系统,Java后端校验账号密码,签发JWT令牌。
- 学生上传简历文件,Java服务接收文件,转发到Flask服务的
/api/resume/parse接口。 - Flask解析简历,返回结构化JSON(姓名、电话、教育经历、技能标签等),Java再把结构化数据写入数据库。
- 学生浏览职位时,Java服务查询职位列表,同时可以调用Flask的推荐接口,基于学生技能标签做匹配排序。
- 学生点击投递,Java服务生成投递记录,同时把一条“投递时间”写入就业跟踪流水表。
- 企业反馈面试结果后,Java更新投递状态,并触发Flask发送通知邮件。
这个数据流不复杂,但每个环节都边界清晰。Java不会直接去抓网页、解析PDF,Flask也不会直接操作核心业务表,尽量减少跨语言协作时的耦合。
2. 核心模块拆解与关键细节
2.1 登录与权限设计
登录是SSM项目里最容易被做糊掉的部分。我这个系统没有引入Spring Security,而是自己写了一套基于JWT的认证逻辑,理由很简单:项目角色只有三种,权限规则并不复杂,用拦截器 + 自定义注解就能控制得明明白白,引入Spring Security反而会增加配置成本。
用户表设计参考如下:
CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL, `role` tinyint(4) NOT NULL COMMENT '1学生 2企业 3管理员', `status` tinyint(4) DEFAULT 1 COMMENT '1正常 0禁用', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;登录成功后,后端返回一个JWT字符串,里面带上用户ID、角色和过期时间。前端把令牌存在本地,每次请求放到Authorization请求头里。后端写了一个LoginInterceptor,在SpringMVC的配置中拦截需要登录的路径。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } UserContext.setUser(JwtUtil.getUser(token)); return true; }这里有个细节值得注意:JWT是无状态的,服务端没法主动吊销,如果学生账号被管理员禁用,已经发出的Token在过期之前仍然有效。为了解决这个问题,我加了一个简单的Redis黑名单机制——用户被禁用时,把Token的jti写入Redis,过期时间跟Token一致,拦截器解析时查一下黑名单即可。
权限控制则用@RequireRole(value = {2,3})这样的角色注解加在Controller方法上,在拦截器里根据用户角色判断是否放行。这种方式对业务入侵小,后来扩展角色也很方便。
2.2 就业信息管理模块
这个模块可以说是系统的核心地盘。职位信息、简历信息、学生信息都在这里汇聚。我在设计时把“职位”和“学生”两个对象做成独立主表,中间用“投递记录表”关联起来,而不是在职位表里冗余学生数据。
职位表核心字段包括:
| 字段 | 说明 |
|---|---|
| id | 主键 |
| company_id | 所属企业ID |
| title | 职位名称 |
| category | 职位类别(技术/市场/运营等) |
| salary_min / salary_max | 薪资范围 |
| city | 工作城市 |
| degree | 学历要求 |
| status | 0待审核 1已上架 2已下架 |
| create_time | 发布时间 |
学生简历表单独拆开,因为一份简历可能包含教育经历、实习经历、项目经历、技能标签等多段信息。如果全塞在一个字段里,后续做搜索和推荐会很痛苦。我用了一个相对朴素的方案:主表存基本信息和技能标签(逗号分隔),子表存教育/实习/项目的多行记录。
这个模块最容易被忽略的是职位审核流程。企业发布职位后,不能直接上架,必须管理员审核通过后才对学生可见。很多初版项目跳过这一步,结果系统里涌入一堆垃圾职位,学生体验直线下降。我在这里加了状态机:待审核 -> 已上架 -> 已下架,审核操作在管理端完成,并写入审核日志。
2.3 就业跟踪流程的设计
就业跟踪是整个系统的灵魂,它比单纯做“投递 + 状态”要复杂一点。跟踪的本质是记录每一个时间点发生了什么:什么时候投递、什么时候收到面试、什么时候录用、什么时候签约。这些时间点组合起来就形成了一条“就业时间线”。
我设计了一张跟踪流水表:
CREATE TABLE `track_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `student_id` bigint(20) NOT NULL, `application_id` bigint(20) DEFAULT NULL, `event_type` varchar(20) NOT NULL COMMENT 'APPLY INTERVIEW OFFER CONTRACT', `event_content` varchar(255) DEFAULT NULL, `event_time` datetime NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;学生每投递一个职位,就自动生成一条APPLY记录;企业把状态改成“邀约面试”时,自动生成一条INTERVIEW记录;企业反馈“录用”时,生成OFFER记录。学生端按时间倒序展示这些记录,形成清晰的求职时间表。
这样做带来两个好处。第一,就业状态的统计不是只看当前结果,而是可以分析过程数据,比如“这个专业的学生平均投递多少份简历能获得第一次面试”。第二,辅导员可以针对长期没有新记录的学生做精准提醒,而不是盲目群发消息。
2.4 Flask辅助服务的设计
Flask服务在项目里承担三个主要功能,下面逐个说明。
简历解析
Java端上传文件后,Flask接收文件,根据扩展名调用对应解析库,提取文本后通过正则和关键词库捞出姓名、电话、邮箱、教育经历、技能关键词。返回的JSON大概长这样:
{ "name": "张三", "phone": "138xxxx1234", "email": "zhangsan@example.com", "skills": ["Java", "Spring", "MySQL"], "education": ["某大学-计算机科学与技术-本科-2024"] }解析逻辑并不复杂,真正的坑在于PDF里的表格、图片、艺术字。后面我会专门讲这个问题。
职位采集
我写了一个定时抓取服务,从几个公开的招聘聚合页抓取IT类职位信息,清洗后写入“职位采集中间表”。企业用户发布职位后,管理员可以在后台参考采集到的职位信息作为审核依据,也可以在系统中优先展示这些公共职位。
通知推送
学生投递简历、企业反馈结果时,Java服务调用Flask的/api/notify/send接口,Flask负责发送站内信和邮件。把邮件服务单独拆到Flask里,Java端不用管SMTP配置,测试时也可以用Flask的日志代替邮件发送,非常方便。
3. 实操过程与核心环节实现
3.1 环境准备与项目初始化
先列一下我当时的环境:
- JDK 1.8
- Maven 3.6.3
- Tomcat 8.5
- MySQL 5.7
- Redis 3.2
- Python 3.8
- Flask 2.x
- Node.js(前后端分离构建时用到,JSP模式可省)
项目采用多模块Maven结构,不过严格说不是多模块,就是一个普通Maven工程加上一个Python服务目录。目录结构如下:
student-employment/ ├── ssm-main/ │ ├── src/main/java │ ├── src/main/resources │ ├── src/main/webapp │ └── pom.xml ├── flask-service/ │ ├── app.py │ ├── requirements.txt │ ├── resume_parser.py │ ├── job_collector.py │ └── notify_sender.py └── sql/ └── init.sql初始化数据库时,我直接用init.sql一次建好所有表,并插入管理员初始账号。这里建议字符集统一使用utf8mb4,避免MySQL 5.7下表情符号或生僻字导致乱码。SSM连接串也要加上characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai。
3.2 数据库建模与核心表关系
除了上面已经展示的用户表和跟踪流水表,还有几张核心表的关系需要理清。
resume_info:学生简历主表,和user表一对一。resume_education、resume_experience:教育经历/项目经历子表,和简历主表一对多。job_info:职位主表,和company_info多对一。job_application:投递记录,关联学生ID和职位ID。notification:站内信表,记录通知接收人、内容和已读状态。
设计投递记录表时,我加了唯一约束(student_id, job_id),防止学生重复投递同一职位。如果业务上允许重新投递,可以改成联合唯一并加状态过滤,或者干脆让重复投递时自动更新原记录,而不是新增。
统计就业率时,是通过“学生当前状态”来判断的。我建议在学生主表上加一个current_status字段,例如0求职中 1已面试 2已offer 3已签约 4未就业,定时或实时根据跟踪流水更新。这样管理员统计数据时,一条SELECT COUNT(*) GROUP BY current_status就能出来,不需要每次全表扫跟踪记录。
3.3 SSM后端核心接口实现
SSM后端最核心的接口可以分三类:
登录接口
@PostMapping("/api/login") @ResponseBody public Result login(@RequestBody LoginRequest req) { SysUser user = userService.findByUsername(req.getUsername()); if (user == null || !MD5Util.md5(req.getPassword()).equals(user.getPassword())) { return Result.error("用户名或密码错误"); } if (user.getStatus() == 0) { return Result.error("账号已被禁用"); } String token = JwtUtil.createToken(user.getId(), user.getRole()); return Result.success(token); }密码存储我用的是MD5加盐,虽然现在更推荐BCrypt,但考虑到项目演示和学习成本,MD5加盐也够用。生产环境请一定换成BCrypt。
职位发布接口
企业用户登录后,将职位信息封装成JSON发送到后端。后端先保存职位,状态置为0,等管理员审核。这个逻辑不复杂,但要注意金额和数字的范围校验,防止出现负数工资或空城市。
投递接口
投递时,开启一个事务:
- 校验学生简历是否完整;
- 插入
job_application记录; - 插入
track_record一条APPLY事件; - 调用Flask的防重复投递检查接口(或者本地校验唯一键)。
事务的好处是,如果简历校验失败,前面的插入全部回滚,不会留下脏数据。
3.4 Java与Flask服务对接方式
Java和Flask通信非常简单,我用Spring的RestTemplate,配置好连接超时和读取超时。
@Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(5000); restTemplate.setRequestFactory(factory); return restTemplate; }调用Flask解析简历时:
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); body.add("file", new FileSystemResource(tempFile)); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); ResponseEntity<String> resp = restTemplate.postForEntity("http://localhost:5000/api/resume/parse", new HttpEntity<>(body, headers), String.class);这里有个经验:RestTemplate上传文件时,MultiValueMap里必须用FileSystemResource,如果用字节数组,Flask那边接收文件时可能会出问题。另外,Java和Flask服务如果部署在不同的服务器上,要把地址配置到application.properties中,别写成常量埋死在代码里。
Flask端对应接口:
@app.route('/api/resume/parse', methods=['POST']) def parse_resume(): file = request.files.get('file') if not file: return jsonify({'code': 400, 'msg': 'no file'}) ext = file.filename.rsplit('.', 1)[-1].lower() text = extract_text(file.read(), ext) data = parse_info(text) return jsonify({'code': 200, 'data': data})3.5 前端页面与调试技巧
前端我是用原生HTML + Bootstrap + jQuery(不要笑,这套组合在学生管理类项目中依然很实用)。页面不多,主要有:
- 登录页
- 学生首页/职位列表页
- 学生简历编辑页
- 企业职位管理页
- 管理员数据统计页
调试时最常用的是浏览器开发者工具。前后端联调时经常遇到“明明登录了,但请求还是401”的问题,十有八九是请求头里没带Authorization,或者本地存的Token过期。我习惯写一个公共的ajaxSetup,自动加上认证头:
$.ajaxSetup({ beforeSend: function(xhr) { var token = localStorage.getItem("token"); if (token) { xhr.setRequestHeader("Authorization", token); } } });这样至少能免掉一半的401困扰。
4. 常见问题与排查技巧实录
4.1 登录与权限踩坑
登录功能看起来简单,但有几个坑我在调试时反复碰到。
第一个是密码加密方式不一致。数据库初始化脚本里插入的用户密码是某种加密结果,但登录代码里用的可能是另一种算法,或者忘记加盐,结果明明密码正确却登录失败。我建议把密码加密逻辑统一放到一个工具类里,初始化和校验都用同一个方法。
第二个是JWT密钥硬编码。很多初版代码把密钥写在Java类里,一旦打包上线,想更换就非常麻烦。最好放在配置文件中,并通过环境变量覆盖。
第三个是拦截器放行路径遗漏。比如登录页面、静态资源、Flask回调接口如果没放行,会被拦截器挡在外面,导致页面白屏或者回调失败。SpringMVC配置放行时,要注意把/css/**、/js/**、/images/**、/api/register等路径全部添加进去。
4.2 中文乱码与数据库时区问题
中文乱码基本围绕三个环节:
- 数据库连接串没加
characterEncoding=utf-8; - 页面响应头编码不是UTF-8;
- Tomcat的URIEncoding没有配置。
我的排查顺序是:先看数据库是否有乱码,如果没有,再看HTTP响应头;如果数据库就是乱码,优先检查SQL文件创建表的字符集和连接串。MySQL 5.7默认字符集往往是latin1,所以建库时一定要显式指定。
数据库时区问题也很典型。如果连接串不设置serverTimezone,在高版本MySQL驱动下会直接报错,或者时间数据差8小时。我在配置里统一加了serverTimezone=Asia/Shanghai,同时在Java时间写入时使用LocalDateTime,避免用Date在不同时区下产生偏移。
4.3 Flask服务与Java联调典型问题
Flask服务端口占用:Flask默认跑在5000端口,如果机器上已经有一个Flask服务或其它程序占用了,Java调用时就会连接失败。我用python app.py --port=5010指定端口,然后在Java配置里保持一致。
文件上传大小限制:Flask默认限制上传文件大小?不一定有,但Nginx或Java端可能有限制。如果学生上传的简历超过几MB,Java侧RestTemplate会报错。我是把上传限制调到10MB,同时在前端做一次大小预检,超了就提示学生压缩后上传。
CORS跨域:我的前端页面由Java服务托管,所以调用Java接口是同域,问题不大。但Java调用Flask属于服务端到服务端,不涉及浏览器跨域。如果以后把前端独立部署,就要在Java端加CORS配置。Flask端可以使用flask-cors扩展,允许来自Java服务地址的跨域请求。
4.4 简历解析的坑
简历解析是Flask服务里最容易出幺蛾子的地方。主要问题集中在PDF提取:
- 扫描版PDF没有文字层,用
PyPDF2提取出来是空文本; - 表格型简历,文本顺序混乱;
- docx文件里的图片型内容,python-docx无法提取。
我的临时方案是:解析不到内容时,后端提示“简历内容解析失败,请上传可编辑版本”,同时把原始文件保留,供企业用户在线下载查看。对大多数学生简历来说,纯文本提取已经能满足关键词匹配需求。
如果要做得更细,可以用OCR工具识别扫描版PDF,但会增加部署复杂度,我建议在演示项目中做一个降级策略就好。
5. 部署与运维经验
5.1 项目打包与部署步骤
Java端使用Maven打包成war包,扔到Tomcat的webapps目录。Flask端用pip freeze > requirements.txt锁定依赖,服务器上执行pip install -r requirements.txt安装环境。
我习惯把Java和Flask分开部署,Java部署在内网服务器,Flask部署在可以访问外网的机器(因为要做职位采集和邮件发送),两边通过内网API网关互通。如果只有一台服务器,就用Nginx做反向代理,把/api开头的请求转发到Java端口,把/flask开头的请求转发到Flask端口。
Nginx的配置片段:
server { listen 80; server_name example.com; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; } location /flask/ { proxy_pass http://127.0.0.1:5010/; proxy_set_header Host $host; } }5.2 日志、备份与日常维护
日志是运维的核心。Java端用Log4j2输出到独立的日志文件,Flask端用Python的logging模块写到logs/app.log。我还在Java里加了一个异步日志切面,把关键操作(投递、审核、登录失败)记录到数据库日志表,方便管理员在后台查看。
数据库每天凌晨用mysqldump备份一次,保留最近30天。简历文件单独备份到文件服务器或云存储,防止本地磁盘故障丢数据。
5.3 扩展想法
如果后续继续做这个系统,我可能会把Flask服务中的推荐逻辑换成更正式的推荐算法,比如基于标签的协同过滤;Java端也可以把SpringMVC升级到Spring Boot,省掉Tomcat配置的麻烦。但就目前来看,Java+SSM+Flask这套组合已经足够撑起一个稳定、可维护、能演示的学生就业管理系统了。
最后分享一个实际运维中的小体会:这种混合技术栈的项目,最怕的是Flask服务挂了自己还不知道。我建议给Flask加一个/api/health健康检查接口,Java端每30秒调用一次,连续失败3次就发告警邮件,同时把投递、通知等非核心操作降级为本地处理,避免整个系统被一个辅助服务拖垮。这个设计在演示时虽然用不上,但上线后真的能救命。