ARTICLE DETAIL

资讯详情

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

Spring Boot+Vue大学生兼职系统:前后端分离与权限实战

Spring Boot+Vue大学生兼职系统:前后端分离与权限实战 简介这是一套基于SpringbootVue开发的大学生兼职服务系统完整项目面向具备Java Web基础的学习者以及需要快速搭建兼职平台的毕业设计、课程设计场景。系统采用前后端分离架构后端以Springboot整合MyBatis与MySQL前端使用Vue.js完成组件化页面与交互涵盖用户注册登录、职位发布浏览、在线申请、信息管理等核心模块并包含密码加密存储与内容审核等安全设计。压缩包共220个文件包括97个Java源码、41个Vue组件、20个XML配置、11个JS脚本、SQL数据库脚本及系统操作文档包体约9.69MB目录结构清晰便于按模块对照学习。已有97人学习下载。资源提供可直接运行的工程代码与部署说明可帮助理解Springboot自动配置、MyBatis持久层映射、Vue响应式数据交互等关键实现同时附带的简历模板和开发环境配置也方便二次开发与项目上线。1. 基于SpringbootVue大学生兼职服务系统先理清三方角色再做前后端分离用 Spring Boot Vue 做大学生兼职服务系统不少人第一反应是「先写登录、再写岗位列表」。这个顺序很容易翻车。兼职系统的实际难点不在 CRUD而在学生、企业、管理员三方角色的权限边界和数据流转学生要搜岗、投递、看进度企业要发岗、审核简历、标记结果管理员要审岗、管账号。基于 Spring Boot Vue 的前后端分离方案正好把接口能力和页面交互拆开各司其职。我见过不少项目在这个选题上栽跟头最典型的不是不会写代码而是 JWT 认证、跨域和角色权限的坑没趟平。下面按我的落地路径把选型、表设计、关键代码、权限和部署一次讲清楚新手能照着跑熟手也能看到边界。2. 选型与架构Spring Boot 自动装配和 Vue 组件化凭什么撑起兼职系统2.1 后端选型Spring Boot自动装配原理与版本选择很多同学在选型时会问这个系统用 SSM 不行吗行但没必要。Spring Boot 的核心价值是自动装配它把 Spring 容器、数据源、Web MVC、事务管理的配置全部收敛到 starter 里。引入 spring-boot-starter-web 后一个 Controller 加一个 Application 类就能跑起来。这一特性对基于 Spring Boot 的项目来说意味着新成员可以快速接手不会被大量 XML 配置淹没。如果面试被问到 Spring Boot 自动装配原理标准回答路径是SpringBootApplication 是组合注解其中 EnableAutoConfiguration 通过 AutoConfigurationImportSelector 读取自动配置文件拿到候选配置类再结合 ConditionalOnClass、ConditionalOnMissingBean 等条件注解决定哪些生效。能讲清楚这条链路比背 Spring Boot 面试题答案有用得多。版本选择是第一个实际决策。Spring Boot 2.x 和 3.x 差异很大3.x 强制 JDK 17并把 javax 换成 jakarta 命名空间导致部分旧依赖不兼容。我做这类系统一般选 2.7.x 加 JDK 8 或 11因为大多数实验室机器、云服务器镜像都是 JDK 8部署成本最低。如果团队已经统一 JDK 17选 3.x 也没问题但 MyBatis-Plus、knife4j 这些工具要特意找适配版本。另一个容易被忽略的点是多环境配置。我习惯在 application.yml 里把 dev 和 prod 的数据源分开用 spring.profiles.active 切换spring: profiles: active: dev --- spring: config: activate: on-profile: dev datasource: url: jdbc:mysql://localhost:3306/parttime?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root --- spring: config: activate: on-profile: prod datasource: url: jdbc:mysql://your-server:3306/parttime username: prod_user password: prod_password注意Spring Boot 2.4 之后配置块改用 spring.config.activate.on-profile老写法 spring.profiles 在新版本上会静默失效。很多从老教程复制的 yml 在本地跑不起来启动日志里看不到 profile 被激活就是这个原因。启动 jar 时通过 --spring.profiles.activeprod 切换到生产配置。这套多环境做法属于 Spring Boot 项目结构的基本功没有它开发机连生产库是早晚的事。兼职系统还有一个典型需求是定时任务岗位过期后要自动下架。在启动类加 EnableScheduling在 Service 里用 Scheduled(cron 0 0 2 * * ?) 每天凌晨两点扫描 work_end 早于当前时间的岗位批量改状态。别小看这个功能系统真实运行后如果没有它过期岗位一直挂着学生投了简历没有回应体验会立刻打折扣。2.2 前端选型Vue路由、插槽与组件化的使用边界前端选 Vue 的核心原因是组件化和渐进式。这个项目里岗位卡片、分页器、筛选表单在多个页面复用组件化能省大量重复代码。Vue Router 管理路由/student/jobs 和 /company/post 各对应一个组件配合路由守卫做权限跳转。Vue 插槽slot又是组件化里最常见的扩展机制同一张岗位卡片学生端在底部放「投递」企业端放「下架」管理员端放「审核」。与其复制三个组件不如把按钮区域留成插槽template div classjob-card h3{{ job.title }}/h3 p{{ job.salaryHour }} 元/小时/p slot nameaction/slot /div /template调用方决定插槽内容JobCard v-forjob in jobs :keyjob.id :jobjob template #action el-button v-ifrole student clickapply(job.id)投递/el-button el-button v-else-ifrole company clickclose(job.id)下架/el-button /template /JobCard这个模式的价值是组件的「骨架」和「行为」解耦职责边界清楚。Vue 面试题里高频的「插槽是什么」到这一层才算落地。版本选择上新项目我推荐 Vue 3 加 Vite 加 Element Plus。Vue 3 的组合式 API 更适合逻辑复用Vite 开发服务器启动快、热更新及时。Vue 2 的生态案例多但新项目再选它等于给自己留技术债。环境配置方面Vite 需要 Node 16 及以上LTS 版本兼容性最好装依赖前先用 node -v 检查版本否则 vue 安装及环境配置时会冒出各种不明错误。2.3 基于Springboot的项目结构前后端目录怎么摆整体架构是标准的前后端分离前端 Vue 项目占用 5173 端口后端 Spring Boot 占用 8080 端口数据库 MySQL。前端通过 JSON 调用后端接口JWT 做身份认证。后端目录结构我习惯这样分src/main/java/com/example/parttime ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo ├── config └── commoncontroller 只做参数收口和结果返回service 写业务逻辑mapper 对接数据库。为什么不能把业务逻辑写在 controller核心原因是可测试性和复用性业务规则写在 controller 里单元测试没法写事务控制难加多个调用方无法复用。这些理由在 Spring Boot 面试题里常被问到能结合实际说出来才有说服力。前端目录一般由 Vite 生成src 下分 views、components、router、store、api、utils。views 放页面级组件components 放可复用组件api 里统一封装请求。很多人问「vue 项目源码怎么发给别人」其实是目录不清晰——别人拿到源码不知道入口在哪。我的做法是README 里写清 Node 版本、启动命令、目录说明src 下按模块分文件夹比如 views/student、views/company、views/admin。这样无论是接手还是答辩都省心。接口规范也要在项目第一天定下来。后端统一返回 Result 结构public class ResultT { private int code; private String message; private T data; }所有接口返回这个结构前端 axios 响应拦截器统一判断 code错误提示也统一处理。资源路径用名词复数例如 GET /api/jobsPOST /api/applies而不是 /getJobList。分页接口用 GET 加参数写操作用 POST 加 JSON。这些细节直接决定了前后端联调时是「顺利对接」还是「一边改一边骂」。3. 后端落地表设计、分页查询与简历投递的核心实现3.1 核心表设计用户、岗位、简历、投递记录兼职系统的数据模型核心是四张表用户表、兼职岗位表、简历表、投递记录表。用户表要同时装学生和企业两种角色用 role 字段区分。常见设计是CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, role TINYINT NOT NULL COMMENT 1-学生 2-企业 3-管理员, real_name VARCHAR(50), phone VARCHAR(20), email VARCHAR(100), company_id BIGINT COMMENT 企业用户关联企业信息, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这里 password 存的是 BCrypt 加密后的密文不是明文。很多新手在写注册接口时会直接用明文这在毕业设计答辩时会被直接问倒。BCrypt 的好处是自带盐值同样的密码每次加密结果都不一样攻击者无法用彩虹表直接反推。在 Spring Boot 里直接用 spring-security-crypto 包里的 BCryptPasswordEncoder 就行不需要引入完整 Spring Security。密码存密文这个决定建议在一开始就定下来别等用户表都写完了再回头改。岗位表是这个系统的核心业务表设计时要把兼职的特点考虑进去。兼职不同于全职它有时长、时薪、结算方式、紧急程度这些字段。常见设计如下CREATE TABLE t_job ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT, category VARCHAR(30), salary_hour DECIMAL(10,2), work_start DATETIME, work_end DATETIME, location VARCHAR(200), status TINYINT DEFAULT 0 COMMENT 0-待审核 1-已上架 2-已下架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这里最容易被忽略的是 status 字段。如果没有状态机企业的发布操作没有审核流程管理员就失去了监管入口这类系统在真实场景里会很快被垃圾信息淹没。在设计阶段就要想清楚企业发布的岗位先落到待审核状态管理员审核通过后才变成已上架学生端只能看到已上架的岗位。这就是一个最基础的状态机后面所有接口都要带上 status 的判断省得在 SQL 里到处都是 where status 1 却忘带条件。投递记录表建议单独建。不要把投递状态塞到岗位表里因为一次投递本身是一个独立业务动作它有自己的生命周期已投递、企业已查看、已通知面试、已录用、已结束。设计如下CREATE TABLE t_apply ( id BIGINT PRIMARY KEY AUTO_INCREMENT, job_id BIGINT NOT NULL, student_id BIGINT NOT NULL, resume_id BIGINT, status TINYINT DEFAULT 0 COMMENT 0-已投递 1-已查看 2-已通知面试 3-已录用 4-已拒绝, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );简历表则要考虑附件和内容两种形式。最简单的实现是存一个 pdf 或 word 附件 URL再存一段富文本自我介绍。字段不需要太多student_id、content、attachment_url、updated_at。另外给 t_job 的 status 和 created_at 建联合索引给 t_apply 的 job_id 和 student_id 建普通索引数据量上来之后分页和查重都会快很多。3.2 岗位分页查询MyBatis-Plus的分页插件与条件构造器后端接口里使用频率最高的是岗位分页查询。用 MyBatis-Plus 写起来非常快但不代表可以乱写。先看代码Override public IPageJobVO queryJobList(JobQueryDTO dto) { LambdaQueryWrapperJob wrapper new LambdaQueryWrapper(); wrapper.eq(Job::getStatus, 1) .like(StringUtils.hasText(dto.getKeyword()), Job::getTitle, dto.getKeyword()) .eq(StringUtils.hasText(dto.getCategory()), Job::getCategory, dto.getCategory()) .ge(dto.getMinSalary() ! null, Job::getSalaryHour, dto.getMinSalary()) .orderByDesc(Job::getCreatedAt); PageJob page new Page(dto.getPageNum(), dto.getPageSize()); IPageJob result jobMapper.selectPage(page, wrapper); IPageJobVO voPage result.convert(job - { JobVO vo new JobVO(); BeanUtils.copyProperties(job, vo); return vo; }); return voPage; }这里有几个点要说明。LambdaQueryWrapper 是 MyBatis-Plus 的条件构造器eq 是精确条件like 是模糊查询前面的 boolean 参数控制是否拼接条件。注意 ge 这种范围条件只有在参数不为空时才拼接否则会让整表都满足条件。分页用的是 Page 对象MyBatis-Plus 会自动拼 limit 语句前提是你配置了分页插件否则 selectPage 会查全表。分页插件配置是一个容易忘的点。单独列一下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }没有这个配置你会看到分页接口返回的都是全量数据自己在 SQL 里手动拼接 limit 的话又容易在 SQL 注入的边缘试探。这个配置我在不同的项目里至少帮别人排查过三次每一次都是「查全部数据」但接口明明传了 pageSize。顺手说一句分页查询的入参最好做一个上限保护比如 pageSize 最大允许 100防止有人把 pageSize 调到 10000 直接把数据库拖垮。这就是后端工程师该有的边界感。3.3 用 Transactional 实现简历投递与状态流转投递接口的逻辑比较直观但有两个隐藏点不能重复投递同一岗位以及投递时要校验岗位是否已上架。直接看代码Transactional public ApplyResultDTO applyJob(ApplyDTO dto, Long studentId) { Job job jobMapper.selectById(dto.getJobId()); if (job null || job.getStatus() ! 1) { throw new BusinessException(岗位不存在或已下架); } Long count applyMapper.selectCount( new LambdaQueryWrapperApply() .eq(Apply::getJobId, dto.getJobId()) .eq(Apply::getStudentId, studentId) ); if (count 0) { throw new BusinessException(请勿重复投递); } Apply apply new Apply(); apply.setJobId(dto.getJobId()); apply.setStudentId(studentId); apply.setResumeId(dto.getResumeId()); apply.setStatus(0); applyMapper.insert(apply); applyLogService.record(apply.getId(), 学生投递了该岗位, studentId); return new ApplyResultDTO(apply.getId()); }这段代码的 Transactional 必须说明。投递操作涉及两次写操作一次插入投递记录一次写入日志如果第二步失败而第一步没回滚会出现脏数据。Spring 的声明式事务默认只在抛出 RuntimeException 时回滚所以你的业务异常最好继承 RuntimeException。投递成功之后企业端的待处理列表就是一个典型的按状态查询接口。这个接口我建议直接返回 VO不要把数据库实体直接返回给前端。实体里暴露了 company_id、create_time 这些字段前端用不到就算了万一被别有用心的人拿到企业 ID 去猜接口你的安全边界就漏了一截。VO 转实体用 BeanUtils.copyProperties 是省事但注意字段名不一致时必须手动映射别指望它能帮你自动转换。到这里后端主流程基本打通。接下来脑子要切换到前端。很多人后端写得很顺一到 Vue 就翻车原因大多是路由和跨域的概念没打通以为「页面能打开」就是万事大吉。4. 前端落地Vue 项目搭建、Axios 封装与岗位列表页4.1 创建 Vue 项目与安装依赖版本和源的坑前端第一步是用脚手架创建项目。常见做法是使用 Vite 创建一个 Vue 3 项目npm create vitelatest parttime-web -- --template vue cd parttime-web npm install第一条命令创建项目骨架第二条进入目录第三条安装基础依赖。这里有一个新手高频翻车点npm install 装到一半失败。原因大概率是网络源不稳定或者 node_modules 里有残留。解决方法很简单切换镜像源后重新安装npm config set registry https://registry.npmmirror.com rm -rf node_modules package-lock.json npm installnpm 的镜像源切换是血泪经验尤其是国内网络环境下不换源装依赖等于拼运气。另外项目里需要路由和状态管理记得除了 vue 本身还要安装配套依赖npm install vue-router4 pinia element-plus axiosvue-router 管路由pinia 管登录状态这种全局数据element-plus 是组件库axios 是请求库。装完依赖把 main.js 改成熟知的挂载写法这个项目就可以起步了。很多人卡在「vue 安装及环境配置」这个搜索词里实际就是依赖没装全或者 Node 版本不对。如果你安装时报错说 engine 不匹配先看自己的 Node 版本是不是超过 20 了最新的 LTS 通常最省心太高的版本在某些旧项目里反而有问题。4.2 用 Axios 拦截器统一处理 token 与跨域前后端分离项目里Vue 跑在 5173 端口Spring Boot 跑在 8080 端口浏览器直接调接口会触发跨域。常见解决方式有三种后端加 CORS 配置开发阶段让前端开发服务器转发请求生产环境用 Nginx 转发。开发阶段我推荐用前端转发在 Vite 的 server 节点里配置 target 指向后端的 http://localhost:8080changeOrigin 设为 truerewrite 负责把路径里的 /api 前缀去掉。参数作用target后端真实地址通常写成 http://localhost:8080changeOrigin是否改写请求头中的 Host 信息开发阶段必须为 truerewrite是否把请求路径里的 /api 前缀去掉避免后端 controller 多一层路径这样前端代码里所有请求都写成 /api/job/list后端接口还是 /job/list两边互不干扰。如果配置之后请求还是被浏览器拦截先打开开发者工具看网络面板确认请求 URL 是否被正确转发而不是在代码里乱试。axios 封装也要单独说。很多人直接在组件里 this.$http.post结果每个组件都要处理拦截器、token 注入、错误提示代码重复到爆炸。通常的封装思路是这样import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push({ path: /login, query: { redirect: router.currentRoute.value.fullPath } }) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request这里有两个关键点。一是请求拦截器里把 token 放到 Authorization 头里统一注入不在每个接口单独写。二是响应拦截器里处理 401token 过期时自动清掉登录态并跳转登录页这是所有带权限系统必备的兜底逻辑。很多人只在页面里判断 response 里的 code却没有在 401 时做全局跳转导致用户 token 失效后停留在假登录状态点哪个接口都失败体验极差。timeout 我习惯设 10 秒宁可提示超时也别让页面长时间转圈。4.3 岗位列表页实现组合式 API 与插槽的落地写法岗位列表页是整系统里最典型的页面。它包含分页表格、筛选表单、投递操作三个区域。以 Vue 3 组合式 API 为例核心逻辑大概长这样script setup import { ref, onMounted } from vue import request from ../api/request import { ElMessage } from element-plus const jobList ref([]) const total ref(0) const query ref({ pageNum: 1, pageSize: 10, keyword: , category: }) const fetchJobs async () { const res await request.get(/job/list, { params: query.value }) jobList.value res.data.records total.value res.data.total } const apply async (jobId) { const res await request.post(/apply/create, { jobId }) ElMessage.success(res.message || 投递成功) fetchJobs() } onMounted(fetchJobs) /script这段代码的精髓在于 query 对象。它承担了分页和筛选的全部状态每次翻页或修改筛选条件只需要改 query 再重新调用 fetchJobs。分页组件需要监听 current-page 变化调 fetchJobs 时把页码参数带进去。在 Vue 里响应式数据 ref 和 reactive 的边界要想清楚基础类型用 ref复杂对象用 reactive别混着用会绕晕自己。岗位卡片组件的设计可以用到 Vue 插槽。父组件决定岗位卡片底部显示什么按钮——学生端显示「投递」按钮企业端显示「下架」按钮管理员端显示「审核」按钮。这样一个 JobCard 组件就被三个角色复用这就是插槽的实际价值。在面试问答里你会被问到这个点能说出真实应用场景会比背定义加分。分页组件的绑定方式也值得写一下el-pagination v-model:current-pagequery.pageNum v-model:page-sizequery.pageSize :totaltotal layouttotal, prev, pager, next, jumper current-changefetchJobs /v-model:current-page 和 v-model:page-size 是双向绑定切换页码后 query 里已经更新再触发 fetchJobs 重新请求数据流是闭环的。这里有个小坑直接改 query.pageNum 不会自动触发请求必须手动调用 fetchJobs。很多人以为绑定就够了结果点了页码没反应就是忘了关联事件。5. 权限与避坑排查JWT、路由守卫与五个高频翻车点5.1 JWT 认证流程与 token 存储方式JWT 是这个系统的认证基石。流程不复杂用户登录时后端校验用户名密码签发一个 JWT 返回给前端前端后续请求在 Authorization 头里带上后端写一个拦截器或过滤器解析 JWT通过后把用户信息塞进请求上下文供业务接口使用。建议后端代码里有一个拦截器做 token 校验。常见做法是注册一个 HandlerInterceptorpublic class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(未登录); } Long userId JwtUtils.parseToken(token.replace(Bearer , )); UserContext.set(userId); return true; } }这里要注意 UserContext 的设计简单用 ThreadLocal 即可但业务结束后要记得 clear否则线程池复用会造成用户信息串号。这是线上系统最容易悄悄翻车的地方调试期间不容易复现只有高并发下才暴露。我会在拦截器的 afterCompletion 里调用 UserContext.clear()这是一个已经养成习惯的动作。前端存 token 用什么很多人用 localStorage也有用 sessionStorage 的。两者区别在于失效期localStorage 不主动清就会一直在sessionStorage 关标签页就没了。Token 的过期时间由后端签发时确定一般设 2 小时。实际场景里我一般把 token 放 localStorage把用户信息放内存里的状态管理Pinia。这样既能在刷新页面时从 localStorage 恢复登录态又避免把用户信息这种个人数据塞进浏览器持久层。5.2 路由守卫与动态路由权限第一道门Vue Router 的前置守卫是权限控制的第一道门。代码很简单但容易写错router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })这个守卫的逻辑目标路由标了 requiresAuth 且当前没有 token就弹回登录页并带上 redirect 参数方便登录成功后跳回原页面。这里有两个易错点。第一登录页本身不能设 requiresAuth否则死循环。第二query 里的 redirect 要记得编码不然目标路径带参数时会出问题。动态路由是另一个概念。如果系统的角色权限差异很大比如管理员页面和学生页面完全不同可以在登录成功后根据角色动态注册路由。常见做法是前端先只有 login 和基础布局登录成功后从后端拿当前用户的角色再 router.addRoute 注册对应权限的路由表。这个方案比把所有页面都放在路由表里、然后靠守卫拦截更干净。但动态路由的坑在于刷新页面时路由会丢失需要在每次刷新时重新拉用户信息并重新注册路由。5.3 常见问题避坑跨域、token过期与角色权限的五条记录这里把我在类似项目里高频遇到、也经常在社区问答里看到的问题集中列出来。每条都是「现象 → 原因 → 解决」的格式。坑一前端能打开页面但接口全部 404 或返回 HTML。现象axios 请求报 404或者返回的不是 JSON 而是 index.html 的内容。原因开发环境大概率是转发配置没生效或者请求路径没走 /api 前缀生产环境是 Vue 打包后放在 Spring Boot 的 static 目录里路由刷新时后端没做兜底。解决开发环境检查 vite 配置里的 target 和 rewrite 是否命中生产环境在 Spring Boot 里把非 /api 的路径统一转发到 index.html。这里要注意不能让 /api 的请求也落到 index.html否则接口全废。坑二登录成功后刷新页面就跳回登录页。现象用户登录后一切正常一刷新就又回到登录页。原因Vue 是单页应用刷新时内存里的用户信息丢失而路由守卫判断的依据常常是「用户信息是否存在」而不是「token 是否存在」。解决路由守卫里判断 token而不是用户信息对象。用户信息通过 /user/info 接口重新拉取但这个拉取要在守卫里用 await 阻塞路由跳转否则没等数据回来页面已经跳到登录页了。这个异步守卫的写法很多人容易忽略。坑三企业用户能访问学生端的接口。现象学生端页面隐藏了但接口还能调通。比如学生 A 拿自己的 token 调企业端的上架接口居然成功了。原因前端路由守卫只是隐藏了页面入口后端没有做角色权限校验。接口只校验了 token没有校验角色。解决后端拦截器解析 token 后再把用户角色塞进上下文在需要权限的接口上用注解或手动判断角色。比如管理员的审核接口在 Controller 里加一行校验if (!UserContext.isAdmin()) throw new BusinessException(无权限)。千万别信任前端传的 role 字段前端传什么都能伪装。坑四MyBatis-Plus 分页查出来是全量数据。现象明明传了 pageNum1 和 pageSize10后端返回 total 却等于表中全部行数。原因MyBatis-Plus 的 selectPage 依赖分页插件没有配置 MybatisPlusInterceptor 时物理分页不会生效。解决检查是否有 MybatisPlusConfig确认 DbType 是 MYSQL。没有就按 3.2 节的配置补上。这个坑在换新电脑、新克隆代码库时特别容易复现因为配置可能没提交到 Git克隆下来就缺。坑五文件上传成功但图片不显示。现象学生简历上传成功返回了 URL但前端 img 标签显示 404。原因上传的文件存在本机磁盘但 Spring Boot 没有做静态资源映射前端拿到的 URL /upload/xxx.png 在后端找不到对应目录。解决在 WebMvcConfigurer 里加资源映射把 /upload/** 映射到本地磁盘目录。或者直接把文件放在项目的 static/upload 下但要注意重启服务器文件会丢。实际生产环境一般用对象存储本地存储只用于开发和毕设。这个坑我每次都要说一遍文件落盘后没映射等于白存。6. 部署与验证Vue 打包放进 Spring Boot 的两种做法6.1 前后端分离部署的两种常见做法第一种是前后端分开部署。Vue 打包产物 dist 交给 Nginx 托管Nginx 负责托管静态页面并把 /api 开头的请求转发给后端服务。这种模式生产环境最多见静态资源和后端互相不影响后端升级时前端不用动。第二种是把 dist 内容复制进 Spring Boot 的 src/main/resources/static打成单个 jar适合内部小系统、学生项目和毕设演示。用第二种时Vue 打包前把 base 配置成 ./否则静态资源会按绝对路径去找出现白屏。选第二种时Spring Boot 要处理路由兜底否则在非 /api 路径下刷新页面会 404。常见做法是写一个转发规则把所有不带 /api 的路径转发到 index.html。我这里不贴完整代码但记住一个原则/api 开头走控制器其他路径走前端静态页。6.2 部署后验证清单与我的检查习惯上线前我按清单过一遍用学生、企业、管理员三个账号分别登录验证角色边界企业发的岗位在学生端不可见直到管理员审核通过学生重复投递被拦截token 过期后前端自动跳登录页刷新页面登录态保持。这五项过了再谈体验。文件上传的目录要检查映射是否生效、清理策略有没有。我见过太多项目上线一个月后服务器磁盘被简历附件塞满原因是没人做过期清理。定时任务虽然写了但没验证定时任务是否真的跑起来。习惯上每次发版我都会看一眼日志里有没有定时任务执行记录顺手查一下慢 SQL。这是我一直保留的习惯。希望帮到你。本文还有配套的精品资源点击获取
返回列表