尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SpringBoot+Vue实习管理系统:状态机、权限与防重复提交
简介这是一套基于 Spring Boot 与 Vue.js 开发的前后端分离实习过程管理系统完整源码面向高校院系、实习指导教师及 Java 全栈学习者用于打通实习申请、审批、过程监督与评价反馈等环节。压缩包共 486 个文件约 3.81MB以 Java 后端代码、Vue 组件、JavaScript 脚本和 SCSS 样式为主辅以 SQL 初始化脚本、XML 与 YAML 配置以及前后端启动批处理文件涵盖后台管理、学生端、教师端三类角色模块。已有 65 人学习。读者可获得可直接部署运行的三端分离项目结构、数据库建表与初始数据脚本、基于 RESTful API 的前后端通信示例以及权限控制、实习报告提交、教师评价等业务实现适合课程设计、毕业设计参考或二次开发练手。1. 从一张表格管不住实习说起这套系统到底解决什么问题每年实习季负责这块的老师桌上都会同时摊着几个版本的表格实习单位登记、周报汇总、成绩评定谁改过、改到哪一版只能靠文件名里的日期猜。麻烦不在于活多而在于过程没有留痕——学生说周报交了老师说没看到最后只能翻聊天记录对时间。基于SpringBootVue的实习过程管理系统要解决的就是这件事把实习申请、周报提交、教师批阅、成绩评定串成一条带状态的主线每次状态变更都落库谁在什么时间做了什么查得到。这套方案适合正在做完整毕设或课程设计的同学也适合要给院系交一套能跑起来的系统的人——它不复杂但每个环节都能讲出设计理由答辩时经得起追问。2. 需求拆解与角色建模三类用户、五个状态和一条主线动手写代码之前最该花时间的是把人想清楚。实习管理这类系统功能列表看着不多真正难的是“谁能看什么、谁能在什么阶段做什么”。这三类角色一旦边界含糊后面接口就会写成大杂烩改一个需求牵动五六个文件。2.1 三类角色的权限边界与数据可见性学生是数据的生产者。他要能提交实习申请选企业、填岗位、起止时间、企业导师、上传接收函或三方协议在实习期间按周提交周报上传工作截图或日志附件查看教师的批阅意见最后看自己的成绩。学生的所有查询都必须限定在“本人”范围内这是整条权限链里最容易松掉的一环。指导教师是数据的审核者。他要能看到名下学生的申请列表并做通过或驳回要能按周批阅周报并打分写评语实习结束后录入企业评价分和总结报告分生成总评。教师的数据范围是“我指导的学生”不是“全部学生”这两者在中型院系里差别很大。管理员不参与业务判断只管基础数据和兜底。批次管理、企业库维护、账号批量导入、跨学院的统计导出、异常数据修正都归他。管理员有最大的数据范围但操作应该有日志不然出了问题查不到人。落到实现上权限要分两层做。前端按角色过滤菜单和按钮这只是体验真正的拦截在接口层每个列表查询都要从 token 里取当前用户 ID反查数据范围而不是信任前端传来的studentId。我见过太多反例列表接口写成select * from intern_weekly where apply_id #{applyId}学生只要把 URL 里的 applyId 改一位就能翻到别人的周报。正确做法是先按applyId查申请单校验apply.studentId currentUserId再返回数据。多一次查询换的是整条数据链的安全。提示角色判断不要散落在各个 Controller 里写if (ADMIN.equals(role))抽一个PermissionChecker或拦截器统一处理后面加角色时只改一处。2.2 实习主线的状态机设计这条主线是整个系统的骨架。我一般会先把状态图画在纸上确认每个状态有明确的进入条件和退出条件再动数据库。最后收敛成五个主状态加一个驳回回退当前状态触发动作操作角色下一状态附加校验草稿 DRAFT提交申请学生待审核 PENDING企业、岗位、起止时间必填附件至少一个待审核 PENDING审核通过指导教师实习中 ONGOING与本人在途申请的时间段不重叠待审核 PENDING驳回指导教师草稿 DRAFT必须填写驳回理由实习中 ONGOING提交周报学生不变周次唯一同一周不可重复提交实习中 ONGOING结束实习学生或教师已结束 FINISHED周报数量达到批次设定的最低要求已结束 FINISHED录入成绩指导教师已评价 EVALUATED三项分数齐全总评自动计算两个设计取舍值得说。第一状态字段存字符串常量而不是数字。数字省空间但排查问题时你看到status 3得回去翻注释日志里全是猜谜字符串ONGOING虽然占几个字节可读性换来的调试效率远大于那点存储。第二驳回不是单独的状态而是回退到草稿学生改完可以重新提交历史审核记录另存一张流水表。这样状态机的迁移路径是收敛的不会出现“驳回中又提交”这种谁也说不清的中间态。成绩评定挂在最后一步。三项分数周报平均分占 40%企业评价占 30%总结报告占 30%总评保留一位小数后按区间定等级90 分及以上优秀80 到 89 良好70 到 79 中等60 到 69 及格60 以下不及格。权重和分档最好做成批次维度的可配置项不然换个院系就得改代码。2.3 容易漏掉的非功能需求功能列表写完之后真正决定这套系统能不能交出去的是这几条。防重复提交。网络慢的时候学生连点两次提交按钮前端加了防抖也挡不住唯一的办法是数据库加唯一索引uk_apply_week(apply_id, week_no)服务层捕获重复键异常转成一句“本周周报已提交请勿重复操作”。文件存储。限制单文件 10MB白名单只放 pdf、jpg、png、zip按年月分目录落盘数据库只存相对路径。存绝对路径的系统换一台机器部署就全线 404这是血泪经验。导出性能。统计页一次性导出几千条周报记录如果用全表查询再循环写单元格内存会顶上去。做法是分批查、流式写单批 500 条。安全底线。密码存 BCrypt 哈希不存明文也不用自己写的 MD5JWT 设过期时间前端 401 统一跳登录管理端接口做二次角色校验。集中访问。期末那一周教师会集中批阅intern_weekly上必须建(teacher_id, score)这类组合索引别等页面转圈了才想起来加。3. 总体架构与数据库设计前后端分离的三层结构和八张核心表技术选型上没什么好纠结的后端 SpringBoot 配 MyBatis-Plus 和 MySQL前端 Vue 3 配 Vite、Element Plus 和 Axios。这套组合的资料密度足够高出问题搜得到答案对毕设和中小型交付来说是最稳的选择不需要为了炫技上微服务。3.1 分层结构与一次请求的完整链路后端按 Controller、Service、Mapper 三层切DTO 接收入参VO 返回出参实体只跟数据库打交道。这层纪律听着老套但不做的话Controller 里会慢慢长出业务判断最后状态流转逻辑散在五六个方法里改一个规则要全局搜。一次典型请求是这样走的浏览器发请求到 Nginx静态资源直接返回/api开头的反向代理到 SpringBoot请求先过 JWT 拦截器解析 token 拿到 userId 和 role 放进 ThreadLocal再到 Controller 做参数校验Service 里做业务判断和事务控制Mapper 落库返回统一响应体前端拦截器拆包。整条链路上只有 Service 层允许写业务规则其他层只做搬运和校验。前端按页面模块划分目录views/apply、views/weekly、views/evaluation、views/admin公共组件放components接口调用集中在api目录按模块拆文件。别在页面里直接写 axios接口地址一改要满项目找。注意开发期用 Vite 的 proxy 转发/api到本地 8080生产环境用 Nginx 同源代理这样前后端都不需要处理跨域省掉 CORS 配置的一堆坑。3.2 核心表设计与索引取舍八张表撑起整个系统字段不多但约束要写全表名作用关键字段与约束sys_user账号与角色username 唯一password 存 BCryptrole 区分 STUDENT/TEACHER/ADMINintern_batch实习批次学年、学期、起止日期限定周报可提交的时间窗口intern_company企业库企业名称、信用代码、联系人、行业管理员维护学生只选不改intern_apply实习申请主表student_id、company_id、status、audit_user、audit_remarkintern_weekly周报apply_id week_no 唯一score、comment、teacher_idintern_evaluation成绩评定apply_id 唯一三项分数与总评、等级sys_file附件原始文件名、相对路径、大小、上传人、业务类型sys_login_log登录日志登录时间、IP、结果排查账号异常用申请主表和周报表的建表语句我一般写成这样CREATE TABLE intern_apply ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 学生用户ID, company_id BIGINT NOT NULL COMMENT 实习企业ID, post_name VARCHAR(64) NOT NULL COMMENT 实习岗位, start_date DATE NOT NULL, end_date DATE NOT NULL, corp_mentor VARCHAR(32) COMMENT 企业导师姓名, status VARCHAR(20) NOT NULL DEFAULT DRAFT COMMENT DRAFT/PENDING/ONGOING/FINISHED/EVALUATED, audit_user BIGINT COMMENT 审核教师ID, audit_time DATETIME COMMENT 审核时间, audit_remark VARCHAR(255) COMMENT 驳回理由或审核备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_student (student_id, status), KEY idx_audit (audit_user, status) ) ENGINEInnoDB COMMENT实习申请; CREATE TABLE intern_weekly ( id BIGINT PRIMARY KEY AUTO_INCREMENT, apply_id BIGINT NOT NULL COMMENT 所属申请, week_no TINYINT NOT NULL COMMENT 第几周从1开始, content TEXT NOT NULL COMMENT 周报正文, file_id BIGINT COMMENT 附件ID指向 sys_file, submit_time DATETIME COMMENT 提交时间, score DECIMAL(4,1) COMMENT 百分制评分, comment VARCHAR(500) COMMENT 教师评语, teacher_id BIGINT COMMENT 批阅教师, UNIQUE KEY uk_apply_week (apply_id, week_no), KEY idx_teacher_score (teacher_id, score) ) ENGINEInnoDB COMMENT实习周报;两个索引用途不同。uk_apply_week是业务约束靠数据库兜住并发下的重复提交比在 Service 里先查后插可靠得多idx_student和idx_audit服务于列表页学生端按“我的申请 状态”筛教师端按“我审的单子 状态”筛这两个组合条件几乎覆盖了全部列表查询。日期字段用DATE而不是DATETIME实习起止是天级粒度用 DATETIME 会带来时区换算和比较时的边界问题。外键我建议不建。不是因为性能而是因为删除和迁移时的约束会让测试数据准备变得很别扭引用完整性放在 Service 层校验配合定期跑一遍孤儿数据检查脚本够用。3.3 接口契约统一响应体与错误码前后端一旦约定好响应结构联调时间能砍掉一半。统一返回三个字段{ code: 0, msg: ok, data: { list: [], total: 0 } }code为 0 表示成功其他值按业务分类401 未登录或 token 过期403 越权1001 当前状态不允许该操作1002 重复提交1004 数据不存在。分页接口统一接收page、size两个参数返回list和total前端表格组件直接吃这个结构。错误码集中定义在一个枚举类里前端也维护一份同名字符常量别用魔法数字对暗号。4. 双端落地SpringBoot 接口层与 Vue 页面的对接细节到了写代码这一层重点是两件事状态迁移只能有一个入口前端守卫只管体验不管安全。4.1 后端状态流转、事务与行级权限状态迁移我做成一张白名单表集中在一个 Service 方法里判断Controller 不直接改status字段Service public class ApplyServiceImpl implements ApplyService { // 允许的状态迁移集中维护新增状态只改这一处 private static final MapString, ListString ALLOWED Map.of( DRAFT, List.of(PENDING), PENDING, List.of(ONGOING, DRAFT), ONGOING, List.of(FINISHED), FINISHED, List.of(EVALUATED) ); Resource private InternApplyMapper applyMapper; Override Transactional(rollbackFor Exception.class) public void changeStatus(Long applyId, String target, Long operatorId, String role, String remark) { InternApply apply applyMapper.selectById(applyId); if (apply null) { throw new BizException(1004, 申请不存在); } // 行级权限学生只能操作自己的申请单 if (STUDENT.equals(role) !apply.getStudentId().equals(operatorId)) { throw new BizException(403, 无权操作该申请); } // 教师只能审核分配给自己的单子 if (TEACHER.equals(role) !operatorId.equals(apply.getAuditUser())) { throw new BizException(403, 该申请不在你的指导范围内); } ListString next ALLOWED.getOrDefault(apply.getStatus(), List.of()); if (!next.contains(target)) { throw new BizException(1001, 当前状态不允许该操作); } if (DRAFT.equals(target) (remark null || remark.isBlank())) { throw new BizException(1005, 驳回必须填写理由); } // 带上原状态做条件更新避免并发下双请求同时通过 int rows applyMapper.updateStatus(applyId, apply.getStatus(), target, operatorId, remark); if (rows 0) { throw new BizException(1006, 状态已变更请刷新后重试); } } }配套的 Mapper 用条件更新实现乐观锁Update(UPDATE intern_apply SET status #{target}, audit_user #{operatorId}, audit_time NOW(), audit_remark #{remark} WHERE id #{id} AND status #{expect}) int updateStatus(Param(id) Long id, Param(expect) String expect, Param(target) String target, Param(operatorId) Long operatorId, Param(remark) String remark);这里没有用select ... for update加悲观锁。这个系统的写冲突概率极低条件更新一条 SQL 就解决了“两个审核请求同时到达”的问题返回影响行数为 0 时给出明确提示比锁表轻得多。注意Transactional的rollbackFor一定要显式写Exception.class否则抛出受检异常时事务不回滚这是很多人踩过的坑。4.2 前端路由守卫、动态菜单与表单校验前端第一道闸是路由守卫作用是防止用户误入无权页面以及在 token 失效时踢回登录页// router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: () import(/views/Login.vue), meta: { public: true } }, { path: /apply, component: () import(/views/apply/ApplyList.vue), meta: { roles: [STUDENT, ADMIN] } }, { path: /weekly, component: () import(/views/weekly/WeeklyList.vue), meta: { roles: [STUDENT] } }, { path: /review, component: () import(/views/weekly/WeeklyReview.vue), meta: { roles: [TEACHER] } }, { path: /evaluation, component: () import(/views/evaluation/ScorePage.vue), meta: { roles: [TEACHER, ADMIN] } } ] const router createRouter({ history: createWebHistory(), routes }) router.beforeEach((to) { const token localStorage.getItem(token) if (!to.meta.public !token) { return { path: /login, query: { redirect: to.fullPath } } } const roles JSON.parse(localStorage.getItem(roles) || []) if (to.meta.roles !to.meta.roles.some(r roles.includes(r))) { return { path: /403 } } return true })菜单由同一份路由表生成过滤掉当前角色没有的路由项即可不需要再维护一份菜单配置。这里要强调一句守卫只解决“看不到”解决不了“拿不到”接口层的校验一个都不能省。第二道是请求拦截器负责带 token 和统一拆包// utils/request.js import 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 { const body res.data if (body.code 0) return body.data // 业务成功直接返回 data if (body.code 401) { // 登录态失效清空并跳转 localStorage.clear() location.href /login } return Promise.reject(new Error(body.msg)) }, err Promise.reject(err) ) export default service表单校验用 Element Plus 的 rules 就够了申请页里岗位、起止日期、附件是必填结束日期必须晚于开始日期周报页的正文加最小字数限制防止学生交一句“本周正常”糊弄过去。校验规则写在前端只是体验后端 DTO 上同样要加NotBlank、NotNull两边都写一遍不冤枉。4.3 联调细节分页、文件上传与跨域分页参数从第 0 页还是第 1 页开始这个必须提前说死。后端统一接收page从 1 开始MyBatis-Plus 的分页插件里做一次减一前端表格组件的current-page直接绑定。否则联调第一天就会卡在“第一页数据对不上”。文件上传用multipart/form-data后端配置spring.servlet.multipart.max-file-size10MB和max-request-size20MB超限时全局异常处理返回友好提示不要让用户看到一屏堆栈。上传成功后返回fileId和路径业务表只存fileId下载走统一接口按 ID 查路径这样以后换存储位置不影响业务数据。跨域在开发期交给 Vite proxy生产交给 Nginx两端代码里都不出现 CORS 配置。这样做的副作用是本地调试时后端如果单独启动需要用 curl 或接口工具测但这比在生产环境里留着allowCredentials(*)强得多。5. 避坑与常见问题排查五个最容易翻车的点这一章的内容基本都是我在别人的项目里真实见过的每条按现象、原因、解决三段说赶时间的话可以直接照着排查。5.1 数据与权限相关的三个坑第一个周报重复提交。现象是同一个学生同一周出现两条周报记录教师批阅时看到两条一样的。原因有两层前端按钮只做了禁用没做防抖网络慢时用户连点两次后端是先查询再插入两个请求同时通过查询判断。解决办法是在intern_weekly上加唯一索引uk_apply_weekService 层用 try-catch 捕获DuplicateKeyException转成业务异常提示“本周周报已提交”。数据库约束是最后一道防线任何时候都不要只靠代码判断。第二个水平越权。现象是学生 A 修改请求参数里的 ID能看到学生 B 的申请详情。原因是详情接口只按主键查没有校验归属。解决方式有两个层次Service 层统一抽一个assertOwner(applyId, currentUserId)方法查询前先校验或者干脆在 SQL 的 where 条件里带上student_id #{currentUserId}查不到就返回空。后者更简单也更彻底我一般优先选它。第三个状态被跳步修改。现象是学生直接调用更新接口把草稿状态的申请改成了已评价。原因是 Controller 里有一个通用的 update 接口直接接收前端传的整个实体对象。解决办法是明确禁止任何接口直接设置status字段DTO 里根本不提供这个字段状态只能通过changeStatus这一个入口走白名单校验。这类问题的根子在于接口给得太宽松而不是使用者不守规矩。5.2 上传与部署相关的两个坑第四个文件上传后访问 404。现象是上传成功列表里显示有附件点下载却打不开。原因通常是数据库存了服务器绝对路径换环境后路径失效或者 Nginx 没有配置静态资源目录请求直接被前端路由接管。解决办法是数据库只存相对路径配合配置项拼出访问前缀Nginx 单独加一个location /files/指向存储目录并且在 SpringBoot 里也配一份ResourceHandler方便本地调试。第五个日期差一天。现象是学生填的 2024-03-01存进库变成 2024-02-29 或者页面上少一天。原因是java.util.Date和LocalDate混用JSON 序列化时按 UTC 处理。解决办法是全链路统一用LocalDate实体字段加JsonFormat(pattern yyyy-MM-dd)数据库列用DATE类型JDBC 连接串显式指定时区。这条看着小但一旦出现在答辩演示里解释成本很高。注意以上五个问题里前三个属于设计缺陷后两个属于环境配置。前者必须改代码后者查配置文件就能定位排查顺序应该反过来——先确认环境没问题再怀疑逻辑。6. 验收与答辩三张清单和四个必被追问的问题验收这件事我建议从演示脚本倒推。准备一个学生账号、一个教师账号、一个管理员账号用三分钟走完“提交申请、审核通过、提交两周周报、批阅打分、结束实习、录入总评”这条主线中间不切页面乱点。这条线走通说明系统是活的走不通功能列表写得再长也没意义。具体验收按这张表逐项过验收项操作动作通过标准状态机完整性分别用合法与非法状态调用接口非法迁移返回 1001数据不变权限隔离用学生 token 请求他人申请详情返回 403 或空数据不泄露内容重复提交同一周连续提交两次周报第二次提示已提交库中仅一条附件链路上传、列表展示、下载三步都能打开换机器部署后仍可访问成绩计算录入三项分数总评与等级按权重和分档正确统计导出导出某批次全部周报行数与库中一致无乱码答辩时的追问基本跑不出四个方向。为什么用前后端分离答前端负责交互和展示后端只出数据接口可复用也方便以后接小程序端。状态为什么存字符串答可读性和排查效率优先数据量小不构成存储压力。并发怎么处理答状态变更用条件更新做乐观锁重复提交靠数据库唯一索引兜底。学生中途换实习单位怎么办答不覆盖原记录新建一条申请走完整流程旧的置为已结束并保留历史统计时按学生去重取最新一条。最后一个问题最能看出有没有真做过提前想清楚。还有个小技巧演示前把数据库清空重跑一遍初始化脚本只留演示需要的数据。我吃过一次亏测试阶段留下的脏数据把统计页的总数撑到了几百条评委当场问“你们院有这么多学生吗”。从那以后我每次交付前都强制清库重跑一遍主流程宁可多花二十分钟也不要在台上解释数据。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

工程伦理是工程师的职业导航仪:从价值排序到风险决策

工程伦理是工程师的职业导航仪:从价值排序到风险决策

简介:本资源是面向研究生阶段工程伦理课程的雨课堂期末题库,专为高校工科类研究生备考、教师组卷及教学复盘设计,聚焦信息技术、水利工程、核能、环境工程等典型工程场景中的伦理判断与责任辨析。题库覆盖29道高质量试题,含判断、…

📅 2026/11/5 6:56:47
布匹瑕疵检测数据集实战:从标注转换到YOLO训练避坑指南

布匹瑕疵检测数据集实战:从标注转换到YOLO训练避坑指南

简介:这份资源是2019年天池布匹瑕疵检测赛题相关的图像数据集,面向从事工业质检、缺陷检测算法实践的开发者与学习者,可用于分类、目标检测等任务的训练与验证。压缩包共935个文件,以689张jpg瑕疵图像和245个xml标注文件为主&…

📅 2026/11/5 6:56:47
封装的比较通用的写入SLG1日志的函数

封装的比较通用的写入SLG1日志的函数

1.全局变量​​​​​​​CONSTANTS: const_callback_form TYPE baluet VALUE ,bal_param_lognumber TYPE spar-param VALUE %LOGNUMBER. DATA gv_lognumber TYPE balhdr-lognumber.2.函数代码FUNCTION zfm_create_log. *"--------------------------------------…

📅 2026/11/5 6:51:46
MORE NEWS

更多资讯

📰

基于multisim的双钮电子锁电路设计

11 双钮电子锁 设计任务: 1. 有两个按钮A和B,开锁密码可自设,如(3、5、7、9); 2. 若按B钮,则门铃响;(滴、嗒…); 3. 开锁过程:按3下A&…

📰

网页视频资源嗅探:不开抓包工具,5 分钟下走视频直链

网页视频资源嗅探:不开抓包工具,5 分钟下走视频直链 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓(cat-c…

📰

MemReduct 界面变回英文?3 条路径恢复语言 + 防复发清单

MemReduct 界面变回英文?3 条路径恢复语言 防复发清单 【免费下载链接】memreduct Lightweight real-time memory management application to monitor and clean system memory on your computer. 项目地址: https://gitcode.com/gh_mirrors/me/memreduct M…

📰

把团队的文档变成 AI 问答服务:PandaWiki 知识库搭建指南

把团队的文档变成 AI 问答服务:PandaWiki 知识库搭建指南 【免费下载链接】PandaWiki PandaWiki 是一款 AI 大模型驱动的开源知识库搭建系统,帮助你快速构建智能化的 产品文档、技术文档、FAQ、博客系统,借助大模型的力量为你提供 AI 创作、A…

📰

Buildroot 开机自启动:让你的 ARM64 程序随 Linux 启动运行

Buildroot 开机自启动:让你的 ARM64 程序随 Linux 启动运行 每次启动 QEMU,都要登录系统,再手动输入 /usr/bin/debug_demo。学习时这样做没问题,但如果这是一个采集数据、监测状态的设备,难道每次开机都要有人敲命令吗…

📰

Boring Notch 安装指南:3 步搞定未识别开发者弹窗

Boring Notch 安装指南:3 步搞定未识别开发者弹窗 【免费下载链接】boring.notch TheBoringNotch: Not so boring notch That Rocks 🎸🎶 项目地址: https://gitcode.com/gh_mirrors/bor/boring.notch 刚下载完 Boring Notch&#xff…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬