每年毕业季,办公室最热闹的业务系统就是就业管理。岗位信息要汇总、投递记录要跟踪、企业数据要审核、简历要反复筛选,靠着Excel和微信群来回倒腾,信息一乱就全乱了。所以当我决定自己动手写一套Web就业管理系统时,心里很清楚:前端用Vue,后端用SpringBoot,数据落在MySQL,持久层交给MyBatis,一套全栈代码完全自己把控。这套系统做下来,前后花了三个完整周末,从数据库设计到接口联调,再到前端页面打磨,目前已经能稳定跑起来。这篇文章就把整个项目从需求拆解、表结构设计、后端接口实现、前端页面搭建到部署避坑完整记录下来,适合正在做Java全栈入门项目的人、准备大作业/答辩的在校学生,以及在公司内部想搭建内部招聘工具但又不想买商业系统的开发同学参考。
1. 项目定位与整体设计思路
1.1 就业管理这个场景到底需要什么
先别急着写代码,得把业务理顺。一个就业管理系统,表面上看就是“发布岗位、投简历、看状态”,真放到实际场景里会发现角色完全是三套脾气。
学生要的是“找得到、投得出去、查得到结果”,所以系统必须能按岗位名称、企业名称、工作城市去检索,投递之后能看到记录和状态,简历要能上传和编辑。企业端HR要的是“发岗位、筛简历、约面试”,所以要有企业资质信息、岗位管理、收到简历列表、对投递记录做状态流转(已投递、已查看、邀约面试、不合适)。管理员端则是统筹全局,审核企业注册信息、上/下架违规岗位、查看整体投递数据、管理用户状态。
我把系统角色定成三种:学生(student)、企业(company)、管理员(admin)。对应的功能模块就清楚分成了前台招聘大厅(面向学生)、企业后台(面向HR)、管理后台(面向运营人员)。这个设计思路基本沿用了几乎所有主流B端招聘产品的套路,不用发明创造,照着成熟的模式划分即可。
1.2 技术栈选型:为什么是这几个而不是别的
技术选型是这类项目最先被问到的点,也是答辩时老师最爱追问的地方。我最终定下来的是SpringBoot 2.7 + MyBatis + MySQL 8 + Vue 2.x(配合Element UI),下面这张表是我当时对比的思考过程:
| 选型 | 方案A | 方案B | 我的选择与理由 |
|---|---|---|---|
| 后端框架 | SpringBoot | SSM手写整合 | 用SpringBoot。自动配置解决了一堆XML配置问题,内嵌Tomcat,java -jar直接跑,减少环境摩擦。SSM整合虽然经典但入门成本高,纯手写配置文件很浪费时间 |
| 持久层 | MyBatis | Spring Data JPA | 用MyBatis。SQL可控性高,复杂查询(职位筛选、多条件动态SQL)写XML一眼看明白,排查慢SQL也方便。JPA省代码但遇到多表聚合时就有点“魔法”,调试时反而不透明 |
| 数据库 | MySQL 8 | PostgreSQL | 用MySQL。对普通开发者最熟悉、文档多、Navicat等工具成熟,部署到服务器上也省心。MySQL 8的JSON类型、窗口函数留着以后做数据统计也够用 |
| 前端 | Vue 2 + Element UI | Vue 3 + Element Plus | 用的Vue 2。因为Element UI组件生态对中后台系统支持极其成熟,网上资料多,遇到问题搜索成本低。如果你习惯组合式API也可以写Vue 3,核心业务组件基本都能平移 |
这套组合最大的特点是“资料多、坑少、好答辩”。SpringBoot和Vue各自都是生态最活跃的框架,MyBatis又是国内企业使用的重头,面试常问的Java八股、MyBatis分页插件、MySQL事务这些点在这个项目里全都能对上,做一个项目顺带把面试知识点也串起来了。
2. 数据库设计与核心表结构
2.1 表规划和字段设计逻辑
数据库设计是整个项目的地基,我吃过“表没设计好,后面改代码改到吐”的亏,所以这次老老实实画了两天ER图。最终核心表一共七张:用户表、学生信息表、企业信息表、岗位表、简历表、投递记录表、系统日志表。
用户表是登录的入口,我把它做成统一认证表,用角色字段区分身份:
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 '登录密码(加密存储)', role VARCHAR(20) NOT NULL COMMENT '角色: student/company/admin', status TINYINT DEFAULT 1 COMMENT '状态: 1启用 0禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';这里有几个容易被忽略的设计点。第一,username必须唯一,这是登录时查询的索引依据,不加唯一约束后面业务一定会出现脏数据。第二,密码绝不能明文存,我用MD5加盐的方式存储,后面讲安全的时候细说。第三,角色用字符串而不是数字枚举,牺牲一点点存储换来代码可读性,查询的时候WHERE role = 'student'比WHERE role = 1直观得多。
学生信息表和企业信息表都以用户表ID作为主键,同时兼任外键角色,属于“一对一扩展表”的思路:
CREATE TABLE student_profile ( user_id BIGINT PRIMARY KEY COMMENT '关联用户ID', name VARCHAR(30) NOT NULL, gender TINYINT, school VARCHAR(100), major VARCHAR(100), education VARCHAR(20), phone VARCHAR(20), email VARCHAR(50), expectation_city VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );岗位表是招聘大厅的核心数据来源,也是查询条件最多的表。我设计了标题、所属企业ID、工作城市、薪资范围上下限、学历要求、岗位类型、状态七类常用检索字段。薪资用两个整数字段而不是一个字符串,这样后续做薪资区间筛选时可以直接用SQL比较,如果用“8k-15k”这种字符串,筛选就成了噩梦。
2.2 表关系和外键为什么不用“物理外键”
表之间的关系比较清晰:用户表与学生/企业是一对一,企业与岗位是一对多,岗位与投递记录是一对多,学生与简历是一对一,投递记录关联学生和岗位。
我在建表的时候没有写任何FOREIGN KEY约束,只用逻辑外键(就是存对方ID但约束交给代码)。很多人会质疑这一点,我解释一下为什么。就业管理系统本质是读多写少的业务,而物理外键在MySQL的InnoDB引擎里每次插入都要检查关联表,表数据大了以后会成为写入瓶颈。更重要的是,项目上线后一旦要做分表分库或数据归档,物理外键会严重阻碍迁移。实际开发中我把数据一致性交给Service层的事务保证,性能更好,灵活性也更高。
投递记录表是整个系统状态流转的核心:
CREATE TABLE delivery_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, job_id BIGINT NOT NULL COMMENT '岗位ID', student_user_id BIGINT NOT NULL COMMENT '学生用户ID', company_user_id BIGINT NOT NULL COMMENT '企业用户ID', resume_id BIGINT COMMENT '使用的简历ID', status TINYINT DEFAULT 0 COMMENT '状态: 0已投递 1已查看 2邀约面试 3不合适', interview_time DATETIME COMMENT '面试时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_student (student_user_id), KEY idx_company (company_user_id) ) COMMENT='投递记录表';在投递记录表里同时冗余了学生和企业两端的用户ID,这样企业查“收到的简历”时单表直接WHERE company_user_id = ?,学生查“我的投递”时单表直接WHERE student_user_id = ?,完全不用多表关联,查询性能是最好的。面试时间字段也放在投递记录上,而不是单独开一张面试安排表,因为一个投递记录最多对应一次面试,没必要过度设计。这里要记住的是:在写业务代码之前,把所有状态字段的值先定义清楚,前后端共用同一套数字枚举,后面才不会出现“前端显示待处理,后端查出来状态码对不上”的情况。
3. 后端核心实现:SpringBoot与MyBatis落地细节
3.1 工程结构和代码分层
后端工程遵循标准的SpringBoot单体分层结构:
com.example.job ├── JobApplication.java // 启动类 ├── controller/ // 控制层,只做参数接收和结果返回 ├── service/ // 业务层,事务和核心逻辑 ├── mapper/ // MyBatis的Mapper接口 ├── entity/ // 数据库实体类 ├── dto/ // 前端交互的数据传输对象 ├── common/ // 统一结果封装、异常处理、工具类 └── config/ // WebMvc配置、拦截器、跨域配置分层这个事我多说两句。很多新手写SpringBoot项目时,Controller里直接塞一大堆业务代码,一个接口上百行,后面改一个需求能把人绕疯。我坚持Controller薄、Service厚的原则:Controller只负责接收请求、调用Service、把结果包成统一返回结构;业务判断、事务管理、数据组装都在Service层。实体类(entity)严格按照数据库字段来,但返回给前端的数据结构需要用DTO(Data Transfer Object)来定义,比如岗位列表页需要额外附带企业名称,这个字段主要从企业表查出来再塞进DTo返回。
统一返回结构的代码是每个接口都要用的基座:
@Data public class Result<T> { private Integer code; // 200表示成功 500表示失败 private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.setCode(500); result.setMsg(msg); return result; } }所有接口返回这个结构,前端Axios拦截器就能统一判断状态码,不需要每个接口单独写错误处理逻辑。这是我强烈建议所有全栈项目都遵守的统一约定。
3.2 MyBatis动态SQL与分页插件实战
对于像岗位筛选这种多条件组合查询,MyBatis的XML动态SQL是真正的核心利器。招聘大厅的检索条件是标题关键词、城市、学历、岗位类型、薪资范围,每个条件都可选,如果硬拼SQL字符串不仅容易出错,还容易产生SQL注入风险。我用<where>加<if>标签来实现。
<select id="selectJobList" resultType="com.example.job.entity.JobPosition"> SELECT j.*, c.company_name, c.company_logo FROM job_position j LEFT JOIN company_info c ON j.company_user_id = c.user_id <where> <if test="keyword != null and keyword != ''"> AND j.job_title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="city != null and city != ''"> AND j.city = #{city} </if> <if test="education != null and education != ''"> AND j.education = #{education} </if> <if test="jobType != null and jobType != ''"> AND j.job_type = #{jobType} </if> <if test="minSalary != null"> AND j.salary_min >= #{minSalary} </if> <if test="maxSalary != null"> AND j.salary_max <= #{maxSalary} </if> AND j.status = 1 </where> ORDER BY j.create_time DESC </select>这里注意两个细节。第一,<where>标签会自动去掉第一个AND,这样不需要在<if>里纠结有没有前置条件,MyBatis的where标签天生处理好了。第二,XML里>和<不能直接用,要用>和<转义,新手第一次写XML动态SQL多半是在这里报错的。
分页我用的是PageHelper插件,用法非常简单:
@Override public PageInfo<JobPositionVO> getJobList(JobQueryDTO query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<JobPositionVO> list = jobMapper.selectJobList(query); return new PageInfo<>(list); }注意一个核心规则:PageHelper.startPage()后面必须跟第一条SQL查询语句,中间不要穿插其他查询、Log输出或者赋值操作,否则分页插件会拦截到错误的SQL导致分页失效。这个坑我踩过,当时在startPage和select之间加了一行打印当前用户信息的日志,结果分页出来的总条数就错了。排查了半天才发现PageHelper是基于ThreadLocal实现的,它把分页参数绑定到了当前线程,遇到下一次SQL查询就会自动应用,所以“紧随其后”是铁律。
3.3 登录认证:JWT与拦截器
就业管理系统的三个角色都不希望别人能直接操作自己的数据,所以认证是必须的。我采用的方案是JWT(JSON Web Token)加拦截器。用户登录成功后,后端签发一个有效期为24小时的token,前端把它存在localStorage里,每次请求在请求头Authorization字段带上,拦截器统一校验。
签发token的核心逻辑:
public String generateToken(Integer userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }签到时候把用户ID放在subject里,同时把角色塞进claim,这样后面做接口级权限控制时,直接在拦截器里解析claim就能判断当前用户是不是对应角色。
拦截器的配置注意要放行登录接口和静态资源,其他接口一律拦截:
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/api/login", "/api/register", "/api/job/list", "/error" ); }密码加密这里我多说一句,虽然MD5已经被暴力破解得很厉害,但在这个体量的项目里配合加盐依然够用。用户注册的时候把密码和盐拼起来做MD5,数据库里同时存盐值,登录时取出盐重新计算比对。如果以后想升级,改成BCrypt也就一个工具类的事。
3.4 安全防护:XSS过滤与统一异常处理
标题里既然提到了XSS,千万别觉得那是“黑客才要关心的事”。就业管理系统的岗位名称、简历自我介绍都是文本输入框,如果有人在前端脚本里嵌了<script>alert(document.cookie)</script>,存储型XSS就能偷走管理员的登录凭证。我的处理方案是写一个全局的XSS过滤过滤器,基于Jsoup库把请求参数中可疑的HTML标签清理掉。
@Configuration public class XssConfig { @Bean public FilterRegistrationBean<XssFilter> xssFilterRegistration() { FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new XssFilter()); registration.addUrlPatterns("/*"); registration.setOrder(1); return registration; } }另外后端一定要配全局异常处理,不然数据库异常、空指针异常直接裸露给前端,既不好看也不安全。我用@RestControllerAdvice加@ExceptionHandler做了三层兜底:业务异常返回友好提示,BindException返回字段校验错误,兜底异常返回“系统繁忙,请稍后重试”并打印完整堆栈到日志。
4. 前端核心实现:Vue工程搭建与页面设计
4.1 工程初始化和必装依赖库
前端部分我用了Vue CLI来搭建骨架。执行vue create job-frontend,选上Router和Vuex,接下来安装UI组件库和网络请求库:
npm install element-ui axios npm install less less-loader --save-dev这里有个环境配置的小建议。Element UI按需引入能显著减小打包体积,但配置babel-plugin-component的步骤多一点。我的建议是先用全量引入,等系统功能定下来、开始优化性能的时候再切换按需引入。全量引入的代价是初始包体积大一点,但开发期省心,所有组件直接可用。
4.2 路由设计与会话保持
路由是前端页面的骨架,我设计了两套布局。不需要登录就能看的只有登录页;登录后根据角色加载主布局(侧边菜单加顶栏),内部包含招聘大厅、我的投递、个人中心(学生),岗位管理、投递管理、企业信息维护(企业),用户管理、岗位审核、数据概览(管理员)。
路由核心片段:
{ path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), meta: { public: true } }, { path: '/', component: Layout, redirect: '/home', children: [ { path: 'home', name: 'Home', component: () => import('@/views/Home.vue'), meta: { title: '招聘大厅', roles: ['student', 'company', 'admin'] } }, // 其他业务页面 ] }路由守卫用来拦截未登录访问:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.public) { next(); } else if (!token) { next('/login'); } else { next(); } });Vuex用来保存登录用户的基本信息和角色,页面刷新之后从localStorage重新取出来初始化,这样刷新页面不会掉登录状态。
4.3 核心页面拆解:招聘大厅、岗位发布、投递追踪
招聘大厅是最重要的前端页面,结构上选择了“筛选区 + 卡片列表 + 分页”的组合。筛选区是表单组件,绑定查询条件;卡片列表用el-card展示岗位标题、企业名称、城市、薪资、学历要求,每张卡片底部放“查看详情”“立即投递”两个按钮;分页组件绑定页码和每页大小。
“立即投递”之前要先检查用户是否已经填写过简历,没有简历的弹窗引导去完善。
岗位发布页面在企业端,用el-form做表单验证,薪资范围我用两个输入框填最小值和最大值,提交给后端以后端字段为准。发布成功之后岗位列表页会直接刷新出现新岗位。
投递追踪页对学生来说是一张状态表,对每个求职记录展示当前状态。我用el-tag显示状态不同的颜色:已投递是灰色,已查看是蓝色,邀约面试是绿色(附带面试时间展示),不合适是红色。企业端看收到的简历就投递记录反着来看,列表按投递时间排序,状态操作按钮放在行尾。
4.4 Axios封装与接口对接
前后端对接最忌讳每个页面都写一遍axios调用、重复处理错误。我在src/utils/request.js里统一封装了Axios实例:
import axios from 'axios'; const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API || '/api', timeout: 15000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = token; } return config; }); service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { if (res.code === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(new Error(res.msg || '请求失败')); } return res; }, error => { return Promise.reject(error); } ); export default service;请求拦截器负责自动附加token,响应拦截器负责统一处理业务状态码。这样在页面里调用接口就显得非常干净:
getJobList(query).then(res => { this.jobList = res.data.list; this.total = res.data.total; });对接阶段还注意一个点:前后端接口文档要提前定好。我用的方式是先在记事本上列出所有接口的URL、请求参数、返回结构,前后端都对照这同一份文档开发,比边写边对效率高得多,也少扯皮。
5. 联调部署与完整源码使用指南
5.1 本地环境启动全流程
拿到源码之后要能跑起来,这里把步骤和坑一次捋清楚。
第一步,准备环境:JDK 1.8以上、Maven 3.6+、MySQL 8、Node.js 14+。安装MySQL 8的时候记得密码认证插件选项选Legacy,否则老版本JDBC驱动连不上。数据库初始化用SQL脚本执行即可,脚本里包含建库、建表、初始化管理员账号的SQL。
第二步,修改后端配置。在application.yml里改成你的数据库账号密码:
spring: datasource: url: jdbc:mysql://localhost:3306/job_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case: true这个配置必须开,否则数据库字段create_time映射到Java的createTime就失败,查询出来全是null。我记得第一次弄这个项目时没开这个配置,前端列表所有时间字段都是空白,排查了半小时才发现是驼峰映射的问题。
第三步,启动后端。用IDEA打开工程,等Maven依赖解析完,运行JobApplication.java。在浏览器里访问http://localhost:8080/api/job/list如果能拿到JSON说明启动成功。
第四步,启动前端。在项目目录执行:
npm install npm run serve如果npm install报错,多半是依赖版本冲突,可以删掉package-lock.json和node_modules重新安装,或者用npm install --legacy-peer-deps跳过依赖冲突检查。启动后浏览器访问http://localhost:8081,遇到白屏先去地址栏确认端口号对不对。
第五步,前后端联调。开发环境下我用的是Vue CLI的代理配置解决跨域,在vue.config.js里这样设置:
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }所有请求都带/api前缀,代理到后端8080端口,开发环境完全规避了跨域问题。如果不用代理,就要在后端配置CorsConfig放行指定来源,两种方案我都试过,代理方案在开发期更干净,不需要动后端代码。
5.2 打包部署完整方案
部署到服务器上,我用的是前后端分开部署的方案。后端执行mvn clean package -DskipTests打出jar包,然后扔到服务器上:
nohup java -jar job-system-1.0.0.jar > app.log 2>&1 &前端在本地执行npm run build,产物在dist目录下,把它用Nginx部署。
这样后端占8080端口,Nginx占80端口,二者通过/api路径进行反向代理:
server { listen 80; server_name your_domain.com; root /usr/share/nginx/html; # dist目录位置 index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }注意try_files $uri $uri/ /index.html;这一行是Vue路由History模式部署的关键。如果没有这一行,刷新页面到/home这类路由时会返回404,因为Nginx找不到对应的物理文件。如果你不想折腾Nginx,最简单的办法是让后端直接把Vue构建产物放到src/main/resources/static目录下一起打包,这样java -jar一个进程就跑完了整个系统,只有8080一个端口,不用配Nginx,对初学者最友好。
6. 常见问题与排查技巧实录
6.1 典型报错与解决方案速查表
我从开发和上线过程中整理了一些高频报错,做成了速查表:
| 报错现象 | 根本原因 | 解决方案 |
|---|---|---|
访问后端接口报Access denied for user 'root'@'localhost' | 数据库密码不对,或MySQL 8认证插件不兼容 | 检查application.yml密码,连接串加useSSL=false&serverTimezone=Asia/Shanghai,必要时重设root密码 |
| 前端列表数据全部为null | MyBatis没有开启下划线转驼峰 | application.yml配置mybatis.configuration.map-underscore-to-camel-case: true |
| 分页查询返回条数错误 | PageHelper.startPage()和查询之间插入了其他SQL语句 | 确保startPage后紧贴第一条查询SQL,中间不执行任何其他Mapper方法 |
| 登录接口通,但其他接口全部401 | JWT拦截器没有放行登录接口,或token附加位置不对 | 检查拦截器excludePathPatterns配置,确认前端请求头传的是Authorization字段 |
多个页面接口报Failed to fetch | 前端代理没有生效或跨域配置缺失 | 确认访问地址走的是/api前缀、后端端口正确;生产环境检查Nginx的location匹配规则 |
| 上传简历后刷新丢失 | 简历文件保存到了临时目录或没有持久化 | 把上传目录配置到服务器固定路径如/data/upload/,并给Nginx加静态资源映射 |
| 岗位发布成功但列表不显示 | 岗位状态字段没有标记为启用 | 发布接口里status设置为1,列表查询条件默认过滤status=1 |
npm install报ERESOLVE错误 | Node版本较新与旧依赖冲突 | 加--legacy-peer-deps参数,或降级Node版本到16.x |
6.2 踩过几次坑之后的独家经验
第一个坑:前端多角色菜单权限。最开始我把菜单栏写死在侧边栏组件里,三个角色进来看到的内容一模一样,学生居然能点进“企业岗位管理”。后来改成动态菜单方案:登录成功后后端把当前用户的角色返回给前端,前端用一个router.addRoutes()方法按角色动态添加路由,菜单也根据router里的meta字段动态生成。改完之后不仅权限清晰了,代码也更好维护了。
第二个坑:面试时间展示。邀约面试时我本来只在后端存了一个面试时间字段,但企业HR存在多个时区的可能性(其实主要是不同城市作息),后来扩展成了面试时间加面试地点的组合字段,前端面试邀约弹窗里同时让HR填时间、地点、联系人,这样学生收到的通知才完整可用。
第三个坑:数据库时区导致时间错乱。有一次线上系统所有投递时间都比本地时间快了8个小时,查下来发现是MySQL连接串没加serverTimezone=Asia/Shanghai。MySQL 8默认时区是UTC,存储和读取的时候都会按UTC处理,如果你在中国,最终展示结果就差了8小时。连接串里加这一项之后问题彻底解决。
第四个坑:导出投递记录。企业HR要求把收到的简历列表导成Excel,当时图省事用前端把表格数据转CSV,结果Excel打开乱码。后来老老实实后端用EasyExcel生成Excel文件,前端拿blob下载,一步到位。这里顺带说一个下载文件容易踩的坑:axios请求响应类型要设置成responseType: 'blob',否则文件下载下来是损坏的JSON。
7. 写在最后的个人体会
整套系统从零到一做下来,我最大的感受是:这类偏管理的Web系统,难点从来不在某个单一技术,而在“串起来”的能力。数据库表之间怎么关联、后端接口怎么对齐前端页面的数据需求、分页参数和状态码怎么统一约定、部署的时候跨域和静态资源怎么处理,每一个环节单独拆开都不难,但组合起来就能淘汰掉一大批只会“跟着教程跑demo”的人。我在实际开发中坚持的顺序是:先画清楚ER图、再定义好统一返回结构、最后才开始写接口和页面,前两步做好了,后面编码时间至少省一半。这套就业管理系统后续还可以继续扩展统计报表、消息通知、在线笔试这些模块,骨架已经在了,加功能只是往上面长肌肉的事。如果你正在做类似的项目,希望这篇记录能帮你少踩几个坑,把时间花在真正有意思的需求上。