ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue校园招聘系统实战:从数据库设计到前后端部署

SpringBoot+Vue校园招聘系统实战:从数据库设计到前后端部署 简介一份基于SpringBoot和Vue.js的校园招聘系统完整项目包面向高校毕业生与企业两端用户覆盖职位发布、简历投递、面试通知、企业信息管理与个人中心等核心功能适合用作毕业设计、课程项目或前后端分离开发的学习范例。包内共904个文件压缩后约32.72MB以77个Vue组件、161个JavaScript脚本、137个Java源文件、58个HTML页面和52个CSS样式为主同时包含SVG图标、图片素材、启动部署脚本及doc/docx/xls说明文档可直接搭建运行环境并查看完整实现。目前已有51人学习浏览。压缩包内还包含一键构建与启动脚本文档部分补充了系统架构、功能说明和常见配置便于二次开发与排错。整体代码结构清晰前端与后端分层明确是理解招聘业务流程和SpringBootVue集成方法的直观案例。1. 校园招聘系统到底在解决什么职位发布、简历投递、面试通知组成的对接闭环一到招聘季高校就业办的邮箱就会被两种邮件塞满学生问企业为什么没有回音企业问学生简历为什么格式打不开。基于 SpringBoot 和 Vuejs 的校园招聘系统把职位发布、简历投递、面试通知、企业信息管理、个人中心装进同一个 Web 应用解决的就是这个“双方高效对接”的问题。它不只是增删改查真正的复杂度在状态流转一份简历投出去后要经历被查看、邀面试、不合适、入职每一步都得让学生端和企业端看到一致的结果。对 Java 全栈入行者和拿它做毕业设计的人来说这个方向也是把 SpringBoot 自动装配、Maven modules、Vue 路由和组件化整合起来的现成样本。接下来我从数据模型拆起把能照着改的表结构、接口和前端写法讲清楚。2. 先立数据模型再写 SpringBoot 模块六张核心表与目录结构的选型依据2.1 从学生、企业、职位、投递、面试拆出六张核心表业务不复杂但表结构容易拆碎。常见的错误是学生一张表、企业一张表、管理员一张表每个表都带 username 和 password后面登录逻辑要写三套。我一般把账号体系收成一张user表用role区分学生、企业和管理员学生和企业再各拿一张扩展表存业务资料。这样登录、改密码、封禁账号都只有一条路代码里也只要一个UserMapper。职位、投递、面试是三张核心业务表。投递表必须建唯一键否则同一学生对同一职位可以重复投递面试表要和投递表关联而不是直接对职位因为一次投递只会有一个面试流程这样状态更清晰。下面这套 SQL 是在这类招聘系统里反复验证过的最小结构CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL COMMENT BCrypt 密文, role tinyint NOT NULL DEFAULT 3 COMMENT 1-管理员 2-企业 3-学生, status tinyint NOT NULL DEFAULT 1 COMMENT 0-禁用 1-正常, phone varchar(20), create_time datetime, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ); CREATE TABLE student_profile ( id bigint PRIMARY KEY AUTO_INCREMENT, user_id bigint NOT NULL, real_name varchar(30), school varchar(60), major varchar(60), education varchar(10), graduation_year varchar(10), resume_url varchar(255), create_time datetime, KEY idx_school_major (school, major) ); CREATE TABLE company ( id bigint PRIMARY KEY AUTO_INCREMENT, user_id bigint NOT NULL, company_name varchar(100) NOT NULL, industry varchar(30), scale varchar(20), intro text, address varchar(200), verify_status tinyint DEFAULT 0 COMMENT 0-待核验 1-已认证, create_time datetime ); CREATE TABLE job ( id bigint PRIMARY KEY AUTO_INCREMENT, company_id bigint NOT NULL, title varchar(100) NOT NULL, category varchar(30), city varchar(30), salary_min int, salary_max int, requirement text, description text, status tinyint DEFAULT 0 COMMENT 0-草稿 1-待审核 2-招聘中 3-已下线 4-驳回, deadline date, create_time datetime ); CREATE TABLE delivery ( id bigint PRIMARY KEY AUTO_INCREMENT, student_id bigint NOT NULL, job_id bigint NOT NULL, resume_url varchar(255), status tinyint DEFAULT 0 COMMENT 0-已投递 1-已查看 2-邀面试 3-不合适 4-已入职, create_time datetime, update_time datetime, UNIQUE KEY uk_student_job (student_id, job_id) ); CREATE TABLE interview ( id bigint PRIMARY KEY AUTO_INCREMENT, delivery_id bigint NOT NULL, company_id bigint NOT NULL, student_id bigint NOT NULL, job_id bigint NOT NULL, interview_time datetime NOT NULL, location varchar(200), mode tinyint DEFAULT 0 COMMENT 0-线上 1-线下, content varchar(500), status tinyint DEFAULT 0 COMMENT 0-待确认 1-已接受 2-已拒绝 3-已完成, create_time datetime );唯一键uk_student_job是防重复投递的最后一道闸业务代码里先判断一次数据库这里再兜底一次。delivery.resume_url是投递时的简历快照而不是去读学生当前简历的实时地址——学生在个人中心改简历后企业看到的应该是投递那天的版本这个字段能避免很多“简历怎么变了”的纠纷。job.status里我预留了“待审核”和“驳回”是因为大多数校园招聘平台要有校方就业办审核不是企业一发就上线。如果项目只做演示可以直接用 0 草稿、1 招聘中、2 下线三档但字段空间先留够比后面加枚举值代价小得多。2.2 SpringBoot Modules 与 Vue 目录结构按业务域拆而不是按角色拆拿到标题里这种带 Web 前端的项目很多人会按“学生端、企业端、管理员端”拆结果三个模块互相依赖一个公共类改了三个端全编译失败。更稳的做法是前后端分目录后端按业务域拆 Maven modules前端按页面职责拆目录。后端目录一般我会这样摆recruit/backend ├── pom.xml ├── recruit-common # 统一返回体、异常、工具类 ├── recruit-system # 用户、认证、学生资料、企业信息 ├── recruit-recruit # 职位、投递、面试、定时任务 └── recruit-admin # SpringBoot 启动模块放 WebConfig 和 Applicationrecruit-common只放无业务依赖的通用代码recruit-system依赖 commonrecruit-recruit依赖 system。recruit-admin是打包入口其他模块作为依赖被引入。这个结构对应到 SpringBoot 的自动配置机制上有一个需要注意的点启动类所在的recruit-admin必须扫描到所有模块的Service、Mapper组件否则启动后接口能调通但 Service Bean 找不到。常见做法是在启动类上写SpringBootApplication(scanBasePackages com.recruit)把根包统一掉。前端用 Vite 加 Vue3目录按接口和页面聚拢recruit/frontend ├── src │ ├── api # 按后端 controller 分组比如 job.js、delivery.js │ ├── assets │ ├── components │ ├── router # 路由区分 student/company/admin 的父级路由 │ ├── store # 登录态、角色、菜单 │ ├── utils # axios 封装、日期格式化 │ └── views # 页面 vue 文件 ├── vite.config.js └── package.jsonapi目录和后端接口一一对应而不是在页面上到处写axios.get。这样做的好处是后端接口路径变了只改一个文件面试问起“前端怎么管理接口”也能说清楚封装思路。router里要用路由守卫控制角色跳转否则学生登录后直接改 URL 也能进企业后台权限就形同虚设。2.3 SpringBoot 自动装配与配置文件招聘系统的 yml 少了哪几个关键配置SpringBoot 项目启动时SpringBootApplication组合注解里的EnableAutoConfiguration会按 classpath 下的依赖自动生成 DataSource、SqlSessionFactory、DispatcherServlet 这些 Bean。很多新手以为“配置是玄学”其实它只是按约定批量装配你加了spring-boot-starter-web它就帮你配好内嵌 Tomcat你加了数据库驱动它就尝试创建数据源。招聘系统里最容易翻车的是同时引入 MySQL 和 H2 驱动自动装配不知道该用哪个启动报一堆奇怪的错。实际项目里我会在application.yml里把数据源、上传限制、JWT 参数全部显式写出来server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_recruit?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 30MB mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 custom: jwt: secret: campus-recruit-2024-secret expire-minutes: 120spring.datasource.url里三个参数一个都不能少useUnicodetrue防止中文乱码characterEncodingutf8指定编码serverTimezoneAsia/Shanghai解决 MySQL 8 的时区报错。multipart配置直接决定简历上传能不能用默认 1MB 连一张扫描版 PDF 都装不下。custom.jwt是我用来读取密钥和过期时间的自定义前缀用ConfigurationProperties绑定到属性类比把 secret 硬编码在代码里安全。文件上传目录、静态资源路径这些也建议在 yml 里预留后面部署时改配置就能动不用重新打包。3. SpringBoot 后端接口职位发布、简历投递、面试通知完整链路与参数校验3.1 JWT 拦截与登录学生和企业角色怎么区分权限这套系统有三类角色登录后请求必须带上 token。最省事的方案是引入 JWT登录成功时生成 token前端每次请求放在Authorization头里后端用一个拦截器统一校验。拦截逻辑里真正要做的只有三件事token 能不能解析、用户是否还存在、角色是否匹配当前接口。下面这个JwtUtil是核心它负责生成和解析 token。依赖引入 jjwt 后代码可以写成这样public class JwtUtil { private static SecretKey key; private static long expireMinutes; public static void init(String secret, long expireMinutes) { key Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public static String createToken(Long userId, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireMinutes * 60 * 1000)) .signWith(key) .compact(); } public static Claims parse(String token) { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); } }setSubject里放的是用户 IDclaim(role, role)放角色解析时根据role就能判断学生、企业还是管理员。init方法在启动类里读 yml 中的custom.jwt.secret和custom.jwt.expire-minutes调用。密钥长度不能太短否则 jjwt 的hmacShaKeyFor会直接抛异常这是个很常见的配置坑。拦截器层面用HandlerInterceptor的preHandle从请求头拿出来 token 解析把 userId 和 role 放到ThreadLocal或请求属性里Controller 直接取public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { Claims claims JwtUtil.parse(token.substring(7)); request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; } response.setStatus(401); return false; } }注意 token 在请求头里的格式统一写成Bearer xxxxxxx前端和后端约定好解析时先截掉前缀。需要区分权限的接口再写一个自定义注解RequireRole(2)放到 Controller 方法上在拦截器里读注解比对。给面试通知接口加上企业角色限制给学生投递接口加上学生角色限制只要在拦截器里写一次比每个 Controller 手写 if 判断干净太多。3.2 职位发布接口企业端发布职位状态字段怎么控制上下线企业发布职位的接口看起来简单但如果status设计不对后面审核、上下线、统计全部乱套。我建议把状态分成这几档0 草稿、1 待审核、2 招聘中、3 已下线、4 驳回。企业提交时直接置为 1管理员审核通过后变成 2手动下线是 3被驳回是 4企业修改后重新提交又回到 1。Controller 的写法也决定了这个接口好不好改RestController RequestMapping(/api/job) public class JobController { PostMapping public ResultLong create(RequestBody Valid JobCreateRequest req, HttpServletRequest request) { Long companyId (Long) request.getAttribute(companyId); Job job new Job(); BeanUtils.copyProperties(req, job); job.setCompanyId(companyId); job.setStatus(1); jobService.save(job); return Result.ok(job.getId()); } }companyId不应该由前端传而是从登录用户的资料里查出来。如果学生角色也调这个接口会被拦截器挡掉所以接口本身不需要再写角色判断。JobCreateRequest里要用NotBlank、NotNull这类校验注解约束 title、city、salaryMin、salaryMax避免前端传空值进来。薪资字段如果用字符串存“面议”后面做筛选排序会很痛苦所以我一般用两个 intsalaryMin0且salaryMax0表示面议。这个接口最应该注意的是审核逻辑。很多毕设项目企业一发就直接上线但“面向高校毕业生的智能就业平台”如果连审核都没有岗位质量没法保障。给系统预留管理员角色后职位列表查询就要区分“对外展示的职位”和“所有职位”两个版本学生端只查status 2企业端查自己名下所有状态管理员可以查全部。3.3 简历投递用数据库唯一索引挡住重复提交简历投递最怕的不是接口写不出来而是学生手快点了两次按钮数据库里出现两条一模一样的投递记录。如果只靠先查询再插入两个请求同时进来时都查不到记录还是会重复插入。所以我在表结构里加的唯一键uk_student_job才是真正的防线。Service 层写法可以简化成下面这样Transactional public void apply(StudentUser student, Long jobId, String resumeUrl) { Delivery delivery new Delivery(); delivery.setStudentId(student.getId()); delivery.setJobId(jobId); delivery.setResumeUrl(resumeUrl); delivery.setStatus(0); try { deliveryMapper.insert(delivery); } catch (DuplicateKeyException ex) { throw new BizException(400, 你已投递该职位请在个人中心查看进度); } }Transactional保证 insert 失败时事务回滚不会产生半截子数据。DuplicateKeyException是 Spring 对唯一键冲突的封装捕获后转成业务异常前端就能弹出友好提示。为什么不在 catch 里做“查询已有记录并直接返回”因为那一层查询可能是脏读直接把异常转成提示最稳妥。投递状态status的流转也要在 Service 里统一维护不要每次都在 Controller 里直接 update。比如企业查看简历后状态从 0 变成 1企业发送面试邀请前必须校验 delivery.status 是 1 或 2状态不对要拒绝操作。状态机看起来多写几行但它能避免面试通知发出去了投递记录还停在“已投递”的尴尬情况。3.4 面试通知加定时任务SpringBoot 的 Scheduled 处理面试提醒面试通知这个功能很多人只做了一个“企业添加面试记录”的接口但实际业务是有两个时间点的企业发出面试邀请时要立刻通知学生面试日期前一天提醒学生别迟到。第二个动作适合用 SpringBoot 的定时任务做。启动类或配置类上先加EnableScheduling然后写一个任务组件Component public class InterviewRemindTask { Scheduled(cron 0 0 8 * * ?) public void remindInterview() { LocalDateTime now LocalDateTime.now(); LocalDateTime tomorrow now.plusDays(1); ListInterview list interviewMapper.selectComing(now, tomorrow); for (Interview interview : list) { // 站内信写一条记录或调用短信网关 notifyService.sendRemind(interview.getStudentId(), interview); } } }cron 0 0 8 * * ?表示每天早上 8 点执行一次注意秒、分、时、日、月、周七个位置的顺序写错一个系统启动直接抱错还很难定位。selectComing查的是面试时间在现在到明天之间的记录SQL 里用![CDATA[处理符号或者把参数拼成now()调用。如果系统部署了多个实例这个定时任务会在每个节点各跑一遍通知会重复发。生产环境要加分布式锁或者把任务独立出来只在一个节点部署。面试通知的“通知”本身也要落表。站内信表可以简单设计成id, user_id, content, is_read, create_time学生在个人中心拉取未读消息。短信网关这类外部依赖不要放在主链路里发失败不能影响面试记录保存用异步或者消息队列解耦毕设阶段先用站内信邮件足够。4. Vue 前端如何与后端对接登录态、简历上传、面试通知和个人中心的落地写法4.1 初始化 Vite Vue3 并封装 axios代理和 401 跳转前端项目用 Vite 创建最优先做的是把 axios 封装统一起来。项目里所有页面都会调接口如果不封装每个页面都要写token拼接逻辑后续后端接口结构调整时改到怀疑人生。先看src/utils/request.jsimport axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( res res.data, err { if (err.response err.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(err) } ) export default servicebaseURL写成/api而不是完整域名是为了让开发环境用 Vite proxy 把请求转发到后端生产环境再让 Nginx 或 SpringBoot 处理同一个前缀这样前端代码不用区分环境。响应拦截器直接返回res.data是因为这个系统后端统一用Result包装code、message、data三件套页面里拿 data 就行一旦后端把结构改成别的只改这里。对应的vite.config.js里要配 proxyexport default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })配完 proxy 后浏览器里的请求还是localhost:5173/api/user/info跨域问题根本不会出现所以我不建议后端再到处写CrossOrigin。生产环境和这个不同前端打包后放进 SpringBoot 的静态资源目录就没有跨域问题了。4.2 职位列表页和简历投递分页查询、FormData 上传职位列表页是学生打开系统后的第一屏查询条件一般有城市、职位类别、薪资范围、关键词。接口参数用分页加筛选前端封装在src/api/job.jsimport request from /utils/request export function getJobs(params) { return request.get(/job/list, { params }) } export function applyJob(jobId, file) { const formData new FormData() formData.append(jobId, jobId) formData.append(file, file) return request.post(/delivery/apply, formData) }applyJob里使用FormData是因为简历是 PDF 或 Word 文件不能放到 JSON 请求体里。jobId以字符串形式 append后端用RequestParam(jobId) Long jobId接收文件用RequestPart(file) MultipartFile file接收。调用时不要手动设置Content-Type浏览器会自动加上multipart/form-data; boundary...一旦手动设置boundary 对不上就会报 multipart 解析失败。职位列表组件里只需要维护一个列表数组和分页对象比如currentPage、pageSize、total。切换筛选条件时重置页码到 1否则你在第 5 页筛选永远看不到第一页结果。每一行的“投递”按钮要加loading状态防止用户双击产生两次请求后端虽然用唯一键兜底了前端也要减少误操作。上传前还要检查文件类型和大小常见做法是file.type.indexOf(pdf) -1或者扩展名判断超过 10MB 直接提示别再往服务端传。4.3 面试通知和个人中心用 setTimeout 做轮询而不是 setInterval学生登录后个人中心最重要的信息是投递状态和面试通知。企业发出面试邀请后学生不可能手动刷新页面等结果所以要做一个小轮询。很多人一上来就写setInterval(fetch, 3000)但接口慢或者网络抖动时上一次请求还没返回下一次又发出去了响应顺序错乱页面数据会跳来跳去。推荐写法是递归 setTimeoutimport { onMounted, onUnmounted } from vue let timer null const loadNotifyCount async () { const res await getNotifyCount() state.unreadCount res.data timer setTimeout(loadNotifyCount, 30000) } onMounted(() { loadNotifyCount() }) onUnmounted(() { clearTimeout(timer) })每次请求返回后才设置下一次 30 秒后的定时器这样能保证请求不会叠加。30 秒是校园招聘这种低实时性场景的常规值如果改成 3 秒后端压力大且没有实际意义。如果学校网络环境好想做到秒级通知可以考虑 WebSocket 或者 SSE但轮询在毕设和中小系统里足够稳定。个人中心的投递记录要把后端返回的status数字翻译成文字和颜色。后端在接口里返回纯数字前端用映射表渲染const deliveryStatusMap { 0: 已投递, 1: 企业已查看, 2: 邀面试, 3: 不合适, 4: 已入职 }企业端看到的是反过来的投递列表同样一份delivery学生端和个人中心是企业端工作台状态映射文案可以不一样但数字不能各写各的否则前后端对不上。比如学生端显示“企业已查看”企业端显示“待处理”两边都要基于同一个 status 字段换算。4.4 把 Vue 打包放进 SpringBootstatic 映射和刷新 404开发完前端后最省事的部署方式是把 Vue 打包产物放进 SpringBoot 的静态资源目录一个 jar 包直接跑起来。操作上就是把dist里的文件复制到backend/src/main/resources/static这一步可以手动做也可以用 maven-resources-plugin 在构建时自动复制cd frontend npm run build rm -rf ../backend/src/main/resources/static/* cp -r dist/* ../backend/src/main/resources/static/如果平时用 Maven 打包后端可以在pom.xml里配置插件把前端打包产物作为资源复制过去。这里要注意的是复制之后的index.html是给前端路由用的后端如果没有对history模式的回退做处理直接访问/student/resume会得到 Whitelabel 404 页面。需要加一个转发 Controller把前端路由前缀转发到index.htmlController public class PageForwardController { GetMapping(value {/student/**, /company/**, /admin/**}) public String forward() { return forward:/index.html; } }三个前缀对应学生端、企业端、管理员端的路由路径。这样做能解决刷新 404但要注意/api/**不能走这个转发否则前端调用不存在的接口也会返回 HTML而不是 JSON 报错。更稳妥的方式是在后端配置了统一的/api上下文后再通过自定义 ErrorController 处理非/api的 404 跳转。比较省心的兜底方案是前端路由改用createWebHashHistory路径变成/#/student/resume刷新永远不会 404代价是 URL 不好看面试聊起来也不如 history 模式加分。5. 避坑基于 SpringBoot 和 Vue 的招聘系统最容易翻车的五个问题与排查5.1 跨域报错前端加 proxy后端就别再配一堆 CrossOrigin现象开发环境前端跑在 5173 端口后端跑在 8080 端口浏览器请求接口时报Access-Control-Allow-Origin错误。有人给每个 Controller 都加上CrossOrigin结果登录正常了带Authorization头的接口还是报CORS preflight did not succeed。原因跨域不是加一个注解那么简单。Authorization头属于自定义请求头浏览器会先发 OPTIONS 预检请求后端配置不允许对应 Header 时预检就过不去。另外前端同时引用了 proxy 和后端CrossOrigin部分请求走了代理部分请求直连后端行为不一致。解决开发环境统一用 Vite 的 proxy 配置浏览器始终请求同源地址跨域问题直接消失。生产环境如果前后端没有部署在同一端口后端用统一的WebMvcConfigurer写 CORS 规则配置allowedOriginPatterns(*)、allowedMethods(*)、allowedHeaders(*)不要在每个方法上注解。前后端约定请求头统一使用AuthorizationCORS 配置里必须显式放行这个头。5.2 简历上传失败multipart 请求被拒绝先查临时目录和大小限制现象学生投递简历时报Current request is not a multipart request或者上传超过 1MB 的文件直接失败。原因第一种情况通常是前端手动设置了Content-Type: application/json或者请求体里没有FormData第二种情况是 SpringBoot 默认的multipart.max-file-size只有 1MB简历 PDF 大部分都超过这个值。解决前端调用上传接口时一定不要手动设置 Content-Type交给浏览器自动生成。yaml 里把大小限制调大比如max-file-size: 10MBmax-request-size: 30MB。如果小文件能传、大文件失败去查服务器临时目录权限和磁盘空间Tomcat 默认会把上传文件写进系统 temp 目录权限不对或磁盘满都会导致解析失败。把spring.servlet.multipart.location指到一个专用目录能减少这类问题。5.3 LocalDateTime 返回格式前端看到一串带 T 的时间现象面试通知接口返回interviewTime: 2025-06-03T09:15:00前端页面直接显示看起来非常不专业。有时候列表接口因为某个字段格式问题直接序列化报错整个接口返回 500。原因Jackson 默认对LocalDateTime按 ISO-8601 格式输出中间会带一个T。spring.jackson.date-format配置只对老Date类型生效对LocalDateTime无效。解决在实体字段上加JsonFormatJsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone Asia/Shanghai) private LocalDateTime interviewTime;或者全局配置一个 Jackson 的Customizer统一所有LocalDateTime的格式。我建议全局配置不然每个时间字段都要加注解漏一个就出一次问题。配置时注意timezone要写否则时间可能比本地时间差 8 个小时这也是时区问题的另一种翻车形式。5.4 Vue 路由刷新 404部署在 SpringBoot 里F5 白屏现象前端打包放进 SpringBoot 后访问首页正常点击菜单跳转到/student/jobs后按 F5变成 Whitelabel Error Page。原因前端用createWebHistory时路由是浏览器端 history API 维护的后端只配置了静态资源映射没有对前端路由做回退。后端收到/student/jobs请求找不到对应的 Controller 和静态文件就返回 404。解决用 4.4 节里的PageForwardController把/student/**、/company/**、/admin/**这些前端前缀转发到index.html。如果前端部署在 Nginx 上就在 Nginx 配置里写try_files $uri $uri/ /index.html。注意只对页面路径做转发/api/**和静态资源路径不要进这个规则否则接口 404 会被伪装成 HTML 页面排查问题更痛苦。5.5 MySQL 连接参数“玄学”报错时区、SSL 和编码的坑现象本地数据库连接好端端的换到服务器启动时报The server time zone value й׼ʱ is unrecognized或者中文插入数据库变问号。原因MySQL 8 的 JDBC 驱动对时区处理更严格serverTimezone没设置就会报错。Windows 和 Linux 系统默认时区不同同一套配置在一个环境正常另一个环境就会炸。编码问题则是连接 URL 里缺少characterEncoding参数。解决JDBC URL 里统一写成jdbc:mysql://localhost:3306/campus_recruit?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai注意有两个细节一是 yml 里符号如果不加引号可能被当成 YAML 解析的锚点所以整段 URL 建议用单引号包起来二是 MySQL 8 的驱动类要写com.mysql.cj.jdbc.Driver老项目常见的com.mysql.jdbc.Driver是新驱动里已经被移除的类。数据库建表时排序规则也统一用utf8mb4_general_ci这样 emoji 和特殊字符都能存避免学生简历里的名字包含生僻字时变成问号。6. 发版前的验证习惯接口自测命令和页面路径的冒烟清单每次把系统从开发机部署到服务器我最怕的不是后端 500而是前端页面能打开但业务链路是断的。所以我养成一个习惯上线前跑一组 curl 命令做接口冒烟再人工点三个页面路径确认前后端连通。这组命令不长但能覆盖招聘系统的核心链路BASEhttp://localhost:8080/api # 1. 登录企业账号拿 token TOKEN$(curl -s -X POST $BASE/auth/login \ -H Content-Type: application/json \ -d {username:company_demo,password:123456} | jq -r .data.token) # 2. 发布一个职位 JOB_ID$(curl -s -X POST $BASE/job \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {title:Java开发实习生,city:上海,salaryMin:200,salaryMax:300} | jq -r .data.id) # 3. 学生端登录并投递 STU_TOKEN$(curl -s -X POST $BASE/auth/login \ -H Content-Type: application/json \ -d {username:student_demo,password:123456} | jq -r .data.token) curl -s -X POST $BASE/delivery/apply \ -H Authorization: Bearer $STU_TOKEN \ -F jobId$JOB_ID \ -F file/tmp/resume.pdf这四步走完登录、发布、投递三个核心接口就都通了一遍。后面再补一条企业端把投递状态改为“邀面试”的接口学生端再拉一次未读消息整个链路就算闭环了。页面路径我固定看/student/jobs、/company/deliveries、/student/resume这三个重点是刷新不 404、数据能渲染、上传 PDF 后能下载。我最早部署这类系统时只在本地把npm run dev和后端跑起来页面全打开就以为上线成功。结果服务器上 jar 包一跑学生端列表有数据企业端登录却报 401排查半天发现是 token 的密钥没在服务器环境变量里配。后来我把这组验证命令固定成脚本每次发版先跑脚本再人工点页面前后端状态对不上的情况就很少再出现了。多花这十分钟比上线后让用户当黑匣子测试体面得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表