☰
基于SpringBoot+Vue的高校实习管理系统设计与实现
2026/10/13 8:31:45 网站建设 项目流程

最近这两年,被问到最多的一句话就是“你们学校实习管理系统用什么做的”。高校实习管理这事儿,细想起来真的很碎:学生要找岗位、要提交申请,企业要发布职位、要确认接收,校内导师要审核、要批周报,最后还有成绩评定和归档。几方角色搅在一起,Excel来回传、群里接龙这些土办法早就撑不住了。我去年帮某高校完整落地了一套基于SpringBoot+Vue的高校实习管理系统,后端SpringBoot做接口,MyBatis做持久层,数据库用MySQL,前端Vue全家桶,从需求梳理、数据库建模到前后端联调、部署上线,算是把整个流程走了一遍。这篇文章就把这套系统的设计思路、核心实现和踩坑记录都写出来,想拿来做毕业设计、接手类似项目或纯粹想了解这类系统怎么搭的,都可以直接参考。

1. 项目从0到1:需求拆解与功能规划

1.1 高校实习管理到底在管什么

很多人一听“实习管理系统”,第一反应就是做个岗位发布和申请功能。但实际和高校教务、院系导师聊过之后你会发现,真正的痛点远不止这些。管理员要维护每一个学生的实习状态,从申请、审批、报到、打卡、提交周报到最终评定,整个生命周期都得有记录;校内导师要能看见自己名下每个学生的实习进度;企业端需要录入岗位和接收学生;学生自己则需要一个简洁的入口,能投递岗位、交材料、看审批结果。

所以这套系统本质上不是一个简单CRUD,而是一个围绕“实习全过程”的多角色协同平台。我把用户角色拆成四类:学生、校内导师、企业端、系统管理员。不同的角色看到的功能入口完全不同,这直接决定了后端接口的权限设计不能搞一刀切,每个接口都要做角色校验。

在功能规划上,我按业务优先级把模块分成几个层次:第一层是基础数据管理,包括学生信息、教师信息、企业信息、专业和班级的维护;第二层是流程核心,包括岗位发布、实习申请、审批流转、周报提交与批阅;第三层是扩展能力,包括实习成绩评定、数据统计报表、通知公告推送。我的建议是,如果是教学项目或者时间紧的毕业设计,先把第二层做扎实,再把第一层做好,第三层可以选做,别一上来就铺开所有边缘功能。

1.2 功能模块清单与优先级划分

拿我当时做的这套系统举例,最终落地的功能清单是这样的:

  • 系统管理:用户登录、个人中心、角色权限分配、账号管理。
  • 基础数据:学生档案、导师档案、企业信息、专业班级、实习批次配置。
  • 岗位管理:企业发布实习岗位、岗位上下架、岗位搜索与筛选。
  • 实习申请:学生提交实习申请、上传实习协议、企业同意接收、校内导师审核。
  • 实习过程:周报/月报提交、指导老师批阅、实习打卡记录、实习单位变更申请。
  • 成绩管理:实习成绩评定、成绩导出、综合统计。
  • 消息通知:站内信、审批结果推送。

这里面最容易忽略的是“实习批次配置”。高校实习通常分小学期、寒暑假、毕业实习等不同批次,每批次的时间范围、要求、参与学生都不同。第一版我没做批次概念,结果老师一问“这批学生是哪个批次的”,系统根本答不上来,后来才在实习申请里加了batch_id外键。

2. 技术选型:为什么是SpringBoot+Vue+MyBatis+MySQL

2.1 后端技术栈的选择逻辑

SpringBoot在Java后端开发里已经是事实标准,它最大的价值是“约定大于配置”,把传统SSH那套繁琐的XML配置全简化掉了,内嵌Tomcat也让部署省了很多事。我做这套系统时用的是SpringBoot 2.7.x,直接抛弃了SpringMVC那一堆手工配置,写接口基本就是Controller加几个注解的事。

ORM层用了MyBatis而不是MyBatis-Plus或者Spring Data JPA,这个选择我是有考量的。MyBatis的SQL完全掌控在自己手里,像“统计各专业学生实习落实率”这种多表联查、条件动态拼装的复杂统计,手写SQL反而比框架自动生成的更直观可控。MyBatis还有一个优势就是SQL和Java代码分离,维护起来逻辑清晰,尤其是SQL优化的空间完全开放。当然如果是特别赶进度的项目,用MyBatis-Plus会更快,但前提是你得接受它的代码生成和常见封装有时候并不贴合你的表结构。

数据库选MySQL没什么悬念,高校信息化的体量远没到要用Oracle或者PostgreSQL才能撑住的程度,MySQLInnoDB引擎、utf8mb4字符集、日常事务都够用。我建议在MySQL配置里重点检查max_allowed_packet和连接池参数,前期没注意这两个,文件上传一多就报错,排查半天。

2.2 前端选型:Vue3还是Vue2

前端我选了Vue3,搭配Vite构建工具。如果是2024年之后新建项目,直接Vue3就行,组件生态已经非常成熟。配合Element Plus做后台管理界面,开发效率确实高,表格、表单、弹窗、分页这些组件开箱即用,尤其table的列配置和分页逻辑帮省了大量时间。

前端项目结构我按views、components、api、router、store来划分。views按角色建目录,比如views/student、views/teacher、views/company、views/admin,每个目录里放对应角色的页面;api目录统一封装axios请求,每个模块一个文件;router里配置动态路由,根据登录用户的角色动态注册对应路由表,配合路由守卫实现页面级别的访问控制。

这里有个容易踩坑的地方:Vue3如果用Element Plus,表格分页组件在二次封装时要注意pagination的事件名称从v2的current-change变成了update:current-page,不少从Vue2转过来的人在这里卡了半天,控制台还报没有报错,就是页码点了没反应。

2.3 前后端分离与接口约定

前后端分离意味着接口协议必须提前定清楚,否则联调就是灾难。我通常会在项目启动前先定一份接口规范文档,包含统一响应结构、状态码定义、时间格式、分页参数。统一响应结构我用的是这种格式:code表示业务状态码,200是成功,401是未登录或token失效,403是权限不足,500是系统异常;message带提示信息;data放具体数据。所有接口都严格遵守,前端axios响应拦截器里统一处理这些状态码,不用每个页面重复写错误提示。

接口路径设计也尽量符合REST风格,但不用教条,比如岗位列表用GET /api/company/positions,提交实习申请用POST /api/internship/apply。每个接口都要求带认证头Authorization: Bearer ,后端用拦截器统一解析token并设置当前用户上下文,这样Controller里就能直接用当前登录人信息,不用每个方法都去解析一遍。

3. 从零搭建:核心代码与数据库设计实战

3.1 数据库表设计思路

数据库设计是这套系统最核心的部分,我把它视为整个项目的龙骨。核心表建了十几张,这里挑几条关键的说清楚设计逻辑。

学生表student、教师表teacher、企业表company这三张是基础档案表,字段比较常规,注意一点:企业表一定要有统一社会信用代码字段并加唯一索引,不然同一家企业被不同院系录入两次,后面岗位归属会乱套。

实习批次表batch包含批次名称、开始时间、结束时间、当前是否启用。实习申请流程就依赖这个批次概念:学生发起的申请必须关联一个批次的id,审批通过后才计算有效实习时长。数据库里用逻辑外键,不加物理外键约束,避免后续改数据时被外键锁死。

岗位表position需要设计的内容很多,除了岗位名称、岗位描述、人数上限,还要有发布状态字段,比如0草稿、1已发布、2已下架。企业按这个字段控制学生端能看到哪些岗位,下架不是删数据,否则学生历史记录就断掉了。

实习申请表internship_apply是整个业务的核心,我给它设计了这些关键字段:student_id关联学生,company_id关联企业,position_id关联岗位,batch_id关联批次,status字段表示申请状态,取值0待审核、1企业已接收、2校内导师已通过、3已实习中、4已完成、-1已驳回。多个审核环节用不同字段记录审核意见和审核时间,比如company_audit_opinion、company_audit_time、teacher_audit_opinion、teacher_audit_time。

周报表internship_report包含student_id、internship_id、report_content、report_date、teacher_comment、status。成绩表internship_score包含internship_id、score、grade_level、evaluate_content。这里强调一点:分数不要直接存在申请表里,独立一张成绩表的好处是同一段实习可以有多次评分记录,后面调整计算规则也不用动主流程表。

建表SQL里我建议把所有时间字段统一用datetime,不要混用timestamp和varchar存时间。varchar存时间的人真不少,排序和区间查询都要CAST,又慢又容易出问题。索引方面,status字段、student_id、company_id这些高频查询条件都要建普通索引,联查统计时explain看执行计划再调整。

3.2 后端核心接口实现

后端分层我沿用经典三层:Controller、Service、Mapper。Mapper上直接用MyBatis注解或者XML写SQL,只要涉及多表联查我一般选XML,单表CRUD用注解就够了,混着用反而乱了。

学生提交实习申请的接口核心代码如下:

@PostMapping("/api/internship/apply") public Result submitApply(@RequestBody ApplyForm form) { // 获取当前登录学生ID Integer studentId = SecurityUtils.getCurrentUserId(); // 参数校验 if (form.getPositionId() == null || form.getBatchId() == null) { return Result.error("岗位和批次不能为空"); } internshipApplyService.submitApply(studentId, form); return Result.success(); }

Service层处理关键业务逻辑:

@Transactional(rollbackFor = Exception.class) public void submitApply(Integer studentId, ApplyForm form) { // 检查是否已经申请过同批次同岗位 int count = applyMapper.countByStudentAndBatch(studentId, form.getBatchId()); if (count > 0) { throw new BusinessException("您已申请过该批次的实习岗位,请勿重复提交"); } // 检查该批次是否开放申请 Batch batch = batchMapper.selectById(form.getBatchId()); if (batch == null || batch.getStatus() == 0) { throw new BusinessException("该实习批次未开放申请"); } // 校验岗位是否还在发布状态 Position position = positionMapper.selectById(form.getPositionId()); if (position == null || position.getPublishStatus() != 1) { throw new BusinessException("该岗位已下架或不存在"); } // 写入申请记录 InternshipApply apply = new InternshipApply(); apply.setStudentId(studentId); apply.setPositionId(form.getPositionId()); apply.setCompanyId(position.getCompanyId()); apply.setBatchId(form.getBatchId()); apply.setStatus(0); apply.setCreateTime(new Date()); applyMapper.insert(apply); }

@Transactional这个注解必须加上,rollbackFor = Exception.class也一定要写,默认情况下它只对RuntimeException回滚,如果你在事务方法里抛的是自定义的BusinessException而它没继承RuntimeException,事务就不会回滚,数据就写脏了。这个坑我踩过一次,线上用户投诉重复申请记录,查到最后就是事务没生效。

审批环节的接口,企业确认接收和导师审核通过本质上都是状态流转,我封装了一个updateStatus方法,每次流转时校验当前状态是否合法。比如导师审核通过只允许在status=1(企业已接收)这条记录上发生,如果状态不是1,直接返回“当前状态不可操作”。状态机这个东西看起来简单,但真正防止了接口被乱调用导致的数据错乱。

查询学生实习记录时用了MyBatis的动态SQL来组合过滤条件:

<select id="selectApplyRecords" resultType="map"> SELECT a.id, a.status, a.create_time, p.position_name, c.company_name, b.batch_name FROM internship_apply a LEFT JOIN position p ON a.position_id = p.id LEFT JOIN company c ON a.company_id = c.id LEFT JOIN batch b ON a.batch_id = b.id <where> <if test="studentId != null"> AND a.student_id = #{studentId} </if> <if test="status != null"> AND a.status = #{status} </if> <if test="keyword != null and keyword != ''"> AND (p.position_name LIKE CONCAT('%', #{keyword}, '%') OR c.company_name LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY a.create_time DESC </select>

这种写法比在Java代码里拼SQL强多了,逻辑一目了然,以后加筛选条件直接加 就行。

3.3 前端核心页面实现

前端核心页面绕不开岗位列表、实习申请、审批管理、周报提交这几个。

岗位列表页,我用的Element Plus的el-table配合分页组件,数据从后端分页接口拿,查询条件区域放了岗位名称、企业名称、城市下拉筛选,点击查询按钮重新请求列表。这里我封装了一个usePagination组合式函数,把currentPage、pageSize、total、list数据统一管理,搜索条件变化时自动重置到第一页,这个细节对于体验很重要,不然你在第5页点搜索,结果跑到第1页去了,用户会以为搜索没生效。

申请流程前端的关键是页面状态展示,学生提交申请后要看当前流转到哪个环节。我在申请列表里用status字段映射到标签展示,0是待企业审核、1是待校内导师审核、2是审核通过、3是实习中、4是已完成。加上时间线组件el-steps展示完整流转过程,哪一步过了、哪一步卡住了,一眼就能看到,老师那边问起来也好解释。

导师审批页面用的是待办列表模式,只加载当前登录导师名下学生的待审批记录,每条记录后面有“通过”“驳回”两个按钮。点击通过会弹窗让填写审批意见,确认后调接口。列表数据用定时轮询刷新,每30秒拉一次,当时为了这个还引来了一个pr吐槽说“轮询不够优雅”,但实际情况是学校服务器的WebSocket服务不稳定,轮询反而是最稳的方案。

周报模块我觉得是整个系统里使用频率最高的功能。学生周五要交周报,导师周末要批周报,所以要写得特别顺手。学生端做了周报填写表单加历史记录列表,表单里字段就两个:实习内容描述和碰到的问题。导师端做了列表和详情预览,详情页右上角直接放通过和打回按钮,配一个textarea写评语。周报日期用周一到周日做自然周聚合,学生在这周内可以反复修改,周末自动锁死,这个功能当时老师特别满意,说再也不用催学生交周报了。

4. 部署上线与踩坑记录

4.1 本地开发环境搭建

开发环境这块我先说结论:JDK用1.8,Maven用3.6.3,Node用14以上的LTS版本,MySQL用5.7或者8.0都行,但注意MySQL8的连接驱动要匹配。SpringBoot项目用Maven打包,前端项目直接用Vite的build命令打包成静态文件,部署时可以放在同一个Web服务器下,也可以用Nginx做反向代理把/api开头的请求转发到后端端口,前端静态资源直接由Nginx托管。

我实际用的是Nginx部署方案,配置思路是这样:前端dist目录部署到Nginx的html目录,location /对应前端页面;location /api/把请求代理到后端的8080端口,同时配置后端允许跨域或者通过rewrite去掉/api前缀。这个方案的好处是上线后只有一个80端口对外开放,前后端都走同源策略,不存在跨域问题。

本地联调阶段,前端Vite开发服务器配置了proxy,把/api代理到localhost:8080。这里要提醒一句:Vite的proxy配置里changeOrigin默认是false,很多接口能访问到但登录接口一直拿不到session,别忘了设置changeOrigin: true。

4.2 数据库连接池与性能配置

SpringBoot默认自带HikariCP连接池,性能很强,但默认参数在高校这种低并发但连接数打得开的场景下有些要调。我的配置参考:

spring: datasource: url: jdbc:mysql://localhost:3306/internship_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: xxxx driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000

url里有两个参数最容易忘:characterEncoding=utf8确保中文正常,serverTimezone=Asia/Shanghai确保时间字段读写不偏移。之前接手过一个项目,就是serverTimezone没配,数据库里的时间全部变成UTC,凌晨写的数据早上看差了8个小时,排查了整整一下午。

4.3 常见问题与排查速查表

这一节我直接整理成表格,都是实际遇到过的问题和对应的排查思路,基本覆盖了这套系统里高频出现的坑。

问题现象可能原因处理办法
登录接口返回401但账号密码正确前端的密码传输没有做加密,或后端用MD5校验但前端存的是明文确认加密方式一致;建议登录传输时用RSA或BCrypt,别用MD5存密码
上传文件后浏览器崩溃或接口超时Nginx默认client_max_body_size太小,只有1M在Nginx的server块里配置client_max_body_size 20m,同时后端spring.servlet.multipart.max-file-size和max-request-size同步调整
前端页面表格数据不更新,刷新才有新增或删除后没有重新请求列表接口封装统一的刷新方法,在新增、删除、修改成功后调用refreshList()
MyBatis控制台打印的SQL实际执行异常Mapper XML里的#{}和${}混用,SQL注入或参数类型不对能用#{}就用#{},${}只用于动态表名、排序字段这类完全可信值的拼接
导师端看不到学生申请记录权限过滤条件写死teacherId,但实际申请表的导师字段为空审批列表关联查询时,先通过学生表和指导关系表查导师,再过滤记录,别在申请表里冗余存导师id导致不同步
时间字段显示为null或全是1970前端拿到的时间戳是Long,后端返回Date类型,JSON格式化不一致统一用时间字符串传输,后端配置Jackson的日期格式化,前端不做额外转换
两个浏览器同时登录同一账号,互相挤下线登录没有做会话互斥,或者JWT没有失效机制如果是JWT方案,简单的做法是登录时生成新的token,服务端维护每个用户的当前有效token;或者接受多端登录,按业务需求选
部署到Linux后文件下载不了Windows开发环境路径分隔符和Linux不同文件存储路径统一走配置项,不要硬编码/和\,存储文件名用UUID避免中文路径问题

这里的每一个问题都是真实排查。特别说一下“导师端看不到学生记录”这种,表面是前端问题,数据库一查发现是导师和学生的关联关系挂了,实际是初始化脚本里导师表的id和用户表的id没有对应起来。这类问题不要只盯着代码看,先去数据库里把关系链路查一遍,效率比反复加日志要高很多。

4.4 我实际踩过的三个隐蔽坑

第一个坑是Excel导出。实习管理的很多场景都要导出学生名单,开始时我直接用EasyExcel,本地一切正常,部署到服务器后导出中文文件名乱码。排查发现是服务器返回响应头时没有对文件名做URL编码,前端解析乱码。解决办法是用URLEncoder.encode处理filename,同时保留filename*字段支持RFC5987。

第二个坑是定时任务的时区。系统里有一个统计报表功能,每天凌晨跑一次统计数据,上线后一直正常。结果有一次放长假,老师反馈“那一天的统计数据是0”,查定时任务日志发现那天执行了,但统计的时间范围和数据库里的时间对不上。原因就是服务器时区比我本地的东八区晚了8小时,定时任务在服务器的凌晨0点执行时,数据库里的时间是前一天下午4点,统计SQL里的between自然就查不到数据。最后把服务器时区统一成Asia/Shanghai,定时任务逻辑里也明确指定时间参数,不再依赖服务器默认时区。

第三个坑是前端路由刷新后页面404。本地开发一切正常,部署到Nginx之后,点击浏览器刷新按钮直接404。这是Vue Router的history模式在Nginx下没有配置try_files导致的。需要在Nginx配置里加这样一个location:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

这个配置把前端路由的深层链接都回退到index.html,让Vue Router自己处理对应路由。忘了这一条,测试同学在系统里点刷新,会直接被Nginx的404页面砸到,体验非常糟糕。

5. 扩展方向的思考

这套系统跑通之后,我一直在想它还能往哪些方向延展。第一个方向是消息通知的推送渠道,站内信只能覆盖登录用户,学生实习期间不太可能天天登录系统,如果能接入移动端的推送或者邮件通知,导师审核通过、企业接收申请这些关键节点能第一时间触达学生,整体的体验会提升一大截。

第二个方向是统计报表的深度。当前版本的统计是固定报表模式:各院系实习落实率、岗位专业匹配度、平均实习时长、成绩分布这些,都是提前写好的SQL。如果要更灵活,可以考虑引入数据可视化大屏,把全院实习进度、企业参与数量、实习成绩分布实时展示给教务处,汇报的时候不用再拉Excel表格,直接投屏就行。这个方向技术上不复杂,前端用ECharts或者AntV就能实现,难度在于指标体系的梳理。

第三个方向是实习过程的质量把控。现在系统能做周报、签到和成绩评定,但还停留在学生个体层面。如果加一层“实习质量预警模型”,比如某个学生连续三周没有提交周报,或者某家企业的实习岗位长期没有学生申请,系统自动标记异常,推送给管理员处理,这就能从被动记录变成主动管理,价值会完全不一样。

最后给准备动手做类似项目的同学一个建议:不管技术选型是把SpringBoot换成SpringCloud,还是把Vue换成React,核心永远是业务逻辑的准确表达。我一开始也只是想写一个实习管理系统,后来发现真正花时间的不是写代码,而是把“实习这件事到底怎么流转”想清楚。数据库字段多设计一遍,状态流转多梳理一遍,后面写接口就顺畅得多。先跑通一条最小的完整流程,再逐步加功能,是最稳的路径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询