如果你正在为毕业设计或课程设计发愁,或者想用一个完整项目来练手前后端分离开发,那“SpringBoot + Vue 实习生管理系统”绝对是经典中的经典。这个项目我在指导过的学生项目里见得太多了,自己也完整搭过好几遍,它几乎是“技术栈全覆盖+业务逻辑清晰+演示效果好”的完美结合体:后端用 Java 生态里的 SpringBoot,前端用当下最主流的 Vue 全家桶,数据库用 MySQL,整个系统从前到后全是硬通货技能。这篇文章我就从项目拆解、数据库设计、前后端实现到部署踩坑,把我实际做这个项目的经验完整分享出来,适合拿来当毕设基础框架,也适合刚开始学前后端分离的开发者照着敲一遍。
先说清楚这个系统到底能做什么。实习生管理系统,核心就是围绕“实习生”这一角色,把从入职到实习结束的整个流程管起来。对毕设来说,这类系统的优势非常明显:业务场景贴近现实、功能边界清晰、可扩展性强,而且演示时能讲的东西特别多——权限控制、增删改查、统计报表、文件上传,每一项都是答辩时的加分点。更关键的是,SpringBoot + Vue 这个组合本身就是企业里最常用的开发模式之一,做完这个项目,你简历上写“熟悉前后端分离开发”绝对不会心虚。
1. 整体设计与技术选型:为什么是 SpringBoot + Vue + MySQL
1.1 技术栈选择的底层逻辑
很多同学在选技术栈的时候纠结要不要上微服务、中间件、容器编排这些“高级货”,我的建议是:毕设和课设阶段,SpringBoot + Vue + MySQL 是最稳妥的“标准答案”。为什么这么说?
先看后端。SpringBoot 最大的价值在于“约定优于配置”,你不需要像早期 SSM 框架那样写大量的 XML 配置文件,一个启动类加几个注解就能跑起来。对一个实习生管理系统来说,业务复杂度还没到需要分布式架构的程度,单体应用加清晰的分层结构反而是最容易被理解和维护的。而且 SpringBoot 自带的 Spring MVC、数据校验、异常处理机制,足够覆盖这类管理系统的全部需求。
再看前端。Vue 这个框架对新手特别友好,核心思想是“数据驱动视图”,你只需要维护好数据状态,页面会自动更新。配合 Element UI 或 Element Plus 组件库,表格、表单、弹窗、菜单这些后台管理系统的高频组件全是现成的,不用自己从零写 CSS。我自己做这类项目时,前端页面完成速度往往比后端还快,原因就是组件库太强大了。
数据库选 MySQL 没什么好说的,开源免费、资料多、面试常问,而且 MySQL 8.0 之后的功能已经非常完善,窗口函数、公共表表达式这些高级特性都用得上。对于实习生管理系统这种多表关联查询频繁的业务场景,MySQL 的关系建模能力是天然匹配的。
1.2 系统核心角色与功能边界
在设计一个管理系统之前,第一步一定是“理清角色”。实习生管理系统通常包含三种角色,分别是:管理员、实习生、导师(或部门负责人),有些扩展版本还会加“HR专员”这类角色。
管理员的权限最大,管理实习生的信息档案、分配导师、审核请假申请、发布公告通知、查看统计数据,说白了就是整个系统的总控制台。实习生登录后能维护自己的基本信息、查看实习任务和通知、提交请假申请、填写实习周报或日报、查看自己的考勤记录和成绩评价。导师负责确认实习生的工作安排、审批周报、评价实习表现。
这三个角色的权限边界必须在数据库设计阶段就规划清楚,我会在下一节详细说表结构。这里只强调一个原则:** 权限控制是这类系统的灵魂**,答辩时老师一定会问“不同角色登录后看到的内容怎么隔离”,你回答“前端控制路由,后端控制接口权限”基本就能拿到高分。
1.3 项目目录结构规划
项目采用前后端完全分离的方式,前端一个目录,后端一个目录,部署时分开启动。实际开发中我会把项目的分包结构设计成这样:
code复制intern-management-system/
├── frontend/ # Vue 前端工程
│ ├── src/
│ │ ├── api/ # 接口请求封装
│ │ ├── assets/ # 静态资源
│ │ ├── components/ # 公共组件
│ │ ├── router/ # 路由配置
│ │ ├── store/ # Vuex 状态管理
│ │ ├── views/ # 页面组件
│ │ ├── utils/ # 工具函数
│ │ ├── App.vue
│ │ └── main.js
│ ├── package.json
│ └── vite.config.js # 或 vue.config.js
├── backend/ # SpringBoot 后端工程
│ └── src/main/java/com/example/intern/
│ ├── config/ # 配置类(跨域、安全、MyBatis等)
│ ├── controller/ # 控制层
│ ├── service/ # 业务层(接口+实现)
│ ├── mapper/ # 数据访问层
│ ├── entity/ # 实体类
│ ├── common/ # 公共类(统一返回结果、异常处理等)
│ ├── utils/ # 工具类(JWT、密码加密等)
│ └── InternApplication.java
└── sql/ # 数据库脚本
这种结构看起来规整,但更重要的是让后续的编码工作变得极其顺畅——每一层只负责自己的事,出了问题也能快速定位。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:一张表一张表地拆给你看
2.1 核心表结构设计
数据库设计是整个系统最见功底的部分。我的习惯是先用 PowerDesigner 或 Navicat 画 ER 图,明确表之间的关联关系,然后再建表。实习生管理系统至少需要这几张表:用户表(含角色)、实习生信息表、部门表、实习任务表、考勤记录表、请假申请表、实习评价表、公告表。
用户表是系统的认证基础,包含用户ID、账号、密码、角色、状态等字段。密码一定要加密存储,我之前见过很多毕设直接把明文密码放数据库里的,这种安全问题在答辩时被问到会非常尴尬。推荐用 BCryptPasswordEncoder 进行加盐哈希加密,Spring Security 里自带这个工具。
实习生信息表是业务核心,跟用户表通过用户ID关联,包含实习生姓名、性别、学号/工号、学校、专业、联系方式、入职日期、实习结束日期、所属部门、导师ID等。导师本身也是员工,所以也有自己的用户账号。
部门表比较简单,就是部门ID、名称、负责人。部门与实习生是一对多的关系,一个部门可以有多名实习生。实习任务表记录每个实习生的任务安排,包含任务标题、内容描述、开始时间、结束时间、状态、实习生ID。考勤与请假表用于记录日常考勤和请假信息,请假表还要包含审批状态和审批人ID。
表的创建脚本可以直接用 SQL 文件导出,配好外键约束和合适的索引。由于前后端分离后端接口的开发是基于这些表的,所以设计时就要想好“哪些字段需要经常用条件查询”——比如按部门查实习生、按状态查请假、按时间范围查考勤,这些字段最好都加上普通索引。
2.2 ER 关系与关键字段说明
这里的核心关联关系是这样的:用户表(user) 1对1 关联 实习生表(intern),一个用户只对应一份实习档案(如果扩展为导师/管理员,可以另外扩展 staff 表);部门表(dept) 1对多 关联 实习生表(intern);实习生表 1对多 关联 考勤表(attendance)、请假表(leave)、任务表(task)、评价表(evaluation)。
我在做表结构时遇到的一个问题是“导师信息怎么存”。最直观的方案是在 intern 表加一个 mentor_id 字段,指向员工表里对应的导师用户ID,这完全没问题。更规范的做法是单独建一张 mentor_intern_relation 关系表,但毕设阶段没必要过度设计,加外键字段就足够了,答辩时你说得清楚“一名导师带多名实习生”,老师就能理解。
为了减少复杂关联查询,我习惯在后端实体类上直接用 @TableField(exist = false) 标注非数据库字段,比如 Intern 实体里加一个 deptName 字段,通过关联查询填充数据。这样前端拿到的数据直接就是“实习生+部门名称”的组合,不用二次拼接。
2.3 MySQL 建表脚本与初始化数据
建表脚本我放在 sql/init.sql 里,方便直接导入数据库。这里贴几个关键的建表语句,你可以直接抄:
sql复制CREATE DATABASE IF NOT EXISTS intern_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE intern_db;
-- 用户表
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '加密后的密码',
`real_name` varchar(50) NOT NULL COMMENT '真实姓名',
`role` tinyint(4) NOT NULL DEFAULT 2 COMMENT '角色:0管理员 1导师 2实习生',
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:1正常 0禁用',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 实习生信息表
CREATE TABLE `intern_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '关联用户ID',
`dept_id` bigint(20) DEFAULT NULL COMMENT '所属部门',
`mentor_id` bigint(20) DEFAULT NULL COMMENT '导师用户ID',
`student_no` varchar(30) DEFAULT NULL COMMENT '学号/工号',
`school` varchar(100) DEFAULT NULL COMMENT '毕业院校',
`major` varchar(50) DEFAULT NULL COMMENT '专业',
`phone` varchar(20) DEFAULT NULL,
`start_date` date DEFAULT NULL COMMENT '实习开始日期',
`end_date` date DEFAULT NULL COMMENT '实习结束日期',
`evaluation` text COMMENT '实习评语',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_dept_id` (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='实习生信息表';
初始化数据方面,一定要造好测试账号,比如:
- 管理员账号:admin / 明文密码 admin123(入库前 BCrypt 加密)
- 实习生账号:intern01 / intern01
- 导师账号:mentor01 / mentor01
这样前后端联调和演示时能快速切换角色,展示不同权限下的界面差异。
3. 后端 SpringBoot 实现:分层架构 + 核心接口开发
3.1 项目初始化与依赖配置
创建 SpringBoot 项目可以直接用 IDEA 的 Spring Initializr,或者去官网 start.spring.io 生成。我常用的依赖组合是这样的:
spring-boot-starter-web:Web 基础框架,包含内嵌 Tomcatmybatis-plus-boot-starter:MyBatis-Plus,极大简化单表 CRUDmysql-connector-j:MySQL 驱动lombok:减少实体类的 getter/setter 代码spring-boot-starter-validation:参数校验jjwt:JWT 登录认证spring-boot-starter-data-redis:可选,用于 Token 黑名单或验证码存储(毕设可省略)
application.yml 里的关键配置我习惯这样写:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/intern_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 你的数据库密码
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 数据库下划线字段自动转驼峰
global-config:
db-config:
id-type: auto # 主键自增
这里特别提醒两点。第一,serverTimezone=Asia/Shanghai 一定不能省,否则连接数据库时会报时区错误。第二,MyBatis-Plus 开启驼峰映射后,start_date 这样的列就能自动对应实体类的 startDate 字段,省去一堆 @TableField 注解,这是我用它的最大原因。
3.2 统一返回结果与异常处理
一个规范的后端接口必须有一致的“返回格式”。我的习惯是定义 Result<T> 类,包含 code、message、data 三个字段:
java复制@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
再配一个全局异常处理器,用 @RestControllerAdvice 捕获所有未处理异常。这样就算代码里漏了 try-catch,接口返回的 JSON 也是统一格式,不会直接把异常堆栈丢给前端。对于登录认证失败、参数校验失败这类常见情况,我在 GlobalExceptionHandler 里专门处理,返回对应的错误码。前端拿到 code != 200 时统一弹出 message,开发调试时特别省心。
3.3 登录认证与权限控制
登录认证我强烈推荐 JWT(JSON Web Token)。流程是:用户提交账号密码,后端校验通过后生成一个带有效期的 Token 返回给前端,前端每次请求都在请求头里带上 Authorization: Bearer <token>,后端通过拦截器验证 Token 是否合法,并取出用户信息。
这里附一个核心的 JWT 工具类:
java复制@Component
public class JwtUtils {
private static final String SECRET = "your-secret-key"; // 生产环境用更复杂的密钥
// 生成Token
public String generateToken(Integer userId, String username, Integer role) {
return Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.claim("role", role)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 1天过期
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
// 解析Token
public Claims parseToken(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}
}
拦截器实现上,我自定义一个 JwtInterceptor 实现 HandlerInterceptor,在 preHandle 里检查请求头里是否有合法 Token。注意要放行登录接口本身,不然就陷入“先有鸡还是先有蛋”的死循环了。角色控制方面,可以配合自定义注解 @RequireRole("admin"),在拦截器里判断当前用户角色是否匹配,这样在 Controller 方法上加一行注解就能实现权限隔离,代码非常干净。
3.4 核心业务接口开发
说几个必须重点实现的核心接口。
实习生管理接口是系统的重头戏。列表查询要支持多条件组合筛选和分页,MyBatis-Plus 的 LambdaQueryWrapper 配合 Page 可以一行代码搞定:
java复制@GetMapping("/list")
public Result<Page<InternVO>> list(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long deptId) {
LambdaQueryWrapper<Intern> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.hasText(keyword), Intern::getStudentNo, keyword)
.or().like(StringUtils.hasText(keyword), Intern::getSchool, keyword)
.eq(deptId != null, Intern::getDeptId, deptId)
.orderByDesc(Intern::getCreateTime);
Page<Intern> page = internService.page(new Page<>(pageNum, pageSize), wrapper);
// 通过Service批量填充deptName等关联字段,组装成InternVO
return Result.success(internVOIPage);
}
这里有一个我踩过坑的点:like 关键词同时查 studentNo 和 school 时,用 .or() 会让条件变成 (A OR B) AND 其他条件,所以 OR 条件的括号问题一定要搞清楚。MyBatis-Plus 里 .and(wrapper -> wrapper.like(...).or().like(...)) 才是正确的嵌套写法,不然 SQL 逻辑会出错。
请假审批接口体现业务流转。实习生提交请假申请,状态为“待审批”,导师或管理员查看到待审批列表后,同意或驳回。这个流程在后端就是一个状态字段的更新,但要注意记录审批人和审批时间。前端在待办列表里可以直接展示当前待处理的数量,这个数值通过“状态=待审批 + 当前登录人为审批人”的查询条件就能实时得到。
考勤统计与报表接口是答辩时的亮点。可以用 SELECT DATE_FORMAT(attendance_date, '%Y-%m') AS month, COUNT(*) AS total, SUM(status='正常') AS normal FROM ... GROUP BY month 按月份聚合,再配一个图表插件如 ECharts 展示,视觉效果特别好。
4. 前端 Vue 实现:页面布局 + 组件封装 + 权限路由
4.1 工程初始化与目录组织
前端我用 Vue 3 + Vite + Element Plus + Vuex + Vue Router + Axios 这套组合。相比 Vue 2,Vue 3 的 Composition API 写业务逻辑更灵活,而且配套的 Element Plus 组件库在风格和性能上都提升不少。
初始化直接跑 npm create vite@latest frontend -- --template vue,然后安装核心依赖:
bash复制npm install element-plus axios vue-router@4 vuex@4
为了让 Element Plus 全局可用,在 main.js 里引入:
javascript复制import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import store from './store'
const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.use(store)
app.mount('#app')
页面的整体布局用 Element Plus 的 el-container 组件,左侧是侧边栏菜单,右侧是内容区。菜单项根据角色动态生成,这一步放在路由守卫里做,后面会详细说。顶部放用户头像、退出登录按钮,这是后台管理系统最常见的布局形态,也最容易复用到其他项目。
4.2 Axios 封装与请求拦截
如果不封装 Axios,你会发现每个页面都要重复写同样的请求代码,而且错误处理极其混乱。我的封装思路是创建一个 utils/request.js,统一处理三件事:请求头注入 Token、统一错误提示、响应拦截。
javascript复制import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const request = axios.create({
baseURL: '/api', // 开发环境 vite 代理到 http://localhost:8080
timeout: 10000
})
// 请求拦截器:带上 Token
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) {
ElMessage.error('登录已过期,请重新登录')
localStorage.removeItem('token')
router.push('/login')
} else {
ElMessage.error(error.message || '网络异常')
}
return Promise.reject(error)
}
)
export default request
这里有个细节要提:响应拦截器里已经把 res 解包了,所以在页面里调用 request.get('/intern/list') 拿到的直接就是 Result 对象里的 data 字段,不用再嵌套一层 res.data.data。这个解包习惯一旦养成,写业务代码会非常舒服。
4.3 动态路由与权限控制
很多同学不知道“不同角色看到不同菜单”具体怎么实现。我的做法是:登录成功后,后端在 Login 接口返回用户信息和角色,前端根据角色去匹配一份“菜单配置表”。
简单版可以直接在前端定义两份菜单数组:
javascript复制const adminMenus = [
{ path: '/dashboard', name: '工作台' },
{ path: '/intern', name: '实习生管理' },
{ path: '/dept', name: '部门管理' },
{ path: '/leave', name: '请假审批' },
{ path: '/attendance', name: '考勤统计' },
{ path: '/notice', name: '公告管理' }
]
const internMenus = [
{ path: '/dashboard', name: '工作台' },
{ path: '/myProfile', name: '个人信息' },
{ path: '/myTask', name: '我的任务' },
{ path: '/myLeave', name: '我的请假' },
{ path: '/myAttendance', name: '我的考勤' }
]
然后配合路由守卫,每次进入页面前动态筛选菜单:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (!token && to.path !== '/login') {
next('/login')
} else {
next()
}
})
更严谨的做法是在后端提供“查询当前用户的菜单权限”接口,前端根据接口返回渲染,这样以后扩展角色种类不需要改前端代码。但毕设阶段定义静态菜单也就够了,核心是演示“管理员能看到实习生管理入口,实习生看不到”,这个效果动态筛选就能实现。
4.4 核心页面实现细节
登录页的完整逻辑是:表单校验 -> 调登录接口 -> 保存 Token 和用户信息到 localStorage -> 跳转首页。记得在 store 里维护一份用户状态,这样多个页面都能通过 store.getters 拿到当前用户信息,不用反复读 localStorage。
实习生列表页面是大表单页面的集大成者:顶部是筛选区,包含关键词输入和部门下拉,点查询重新加载列表;中间是新增和批量删除按钮;下面是表格,每行有编辑、删除、详情按钮。这里我用 el-table 加分页组件,数据绑定逻辑可以总结为“请求参数 = 查询条件 + 分页参数”,每次查询或分页变化就调一次列表接口。删除操作建议用 ElMessageBox.confirm 做二次确认,这个细节演示时很有仪式感。
请假申请页我用了 el-steps 组件展示审批流程,待审批 -> 已通过 / 已驳回,每条审批记录都有时间线和批注,这个视觉设计在答辩讲解时非常加分。考勤统计页用 ECharts 做柱状图展示月度出勤率,数据来自后端统计接口,直接把后端返回的 JSON 绑定到 ECharts 的 option 上就行。
5. 联调、部署与常见问题排查
5.1 前后端联调配置
本地开发时前端和后端是两个独立端口,前端在 vite.config.js 里配置代理:
javascript复制export default defineConfig({
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
这样前端页面访问 /api/intern/list 时,请求会被转发到 http://localhost:8080/intern/list,完美规避跨域问题。有些同学喜欢在后端写 @CrossOrigin 注解,也能解决,但代理是更接近生产环境的方式,推荐优先使用。
5.2 打包部署流程
后端打包执行为 mvn clean package -DskipTests,会生成一个 jar 文件,然后直接运行:
bash复制java -jar intern-system-0.0.1.jar
如果嫌控制台日志乱,可以用 nohup java -jar xxx.jar > app.log 2>&1 & 放到后台运行。前端执行 npm run build,产物在 dist/ 目录下,既可以用 Nginx 托管,也可以直接把 dist 文件夹里的静态资源扔进 SpringBoot 的 src/main/resources/static 目录里,这样前后端就合并成一个服务了。但对于演示项目,我更喜欢保持前后端独立部署,这样能同时展示“前端打包部署”和“后端服务部署”两个环节,答辩时内容更充实。
5.3 常见问题速查表
以下是这个项目在开发过程中最容易踩的坑,我直接整理成表格方便对照排查:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 数据库连接失败,报时区错误 | serverTimezone 参数缺失 |
连接 URL加上 serverTimezone=Asia/Shanghai |
| 前端请求接口返回 404 | Vite 代理没配上或环路径不匹配 | 检查 proxy 配置,确认后端接口前缀 |
| 跨域报错(CORS) | 前端和后端端口不一致且没有代理 | 用 Vite 代理或后端配 @CrossOrigin |
| 登录成功后刷新页面就退出 | Token 只存在内存中 | 用 localStorage 持久化 Token |
| 密码加密后长度不够 | 密码字段设置成了 varchar(32) | 至少用 varchar(100) 存 BCrypt 结果 |
| 分页数据查不出来 | Page 对象没传递给 Mapper | selectPage(page, wrapper) 别写反参数 |
| 日期格式显示为时间戳 | 没配 Jackson 全局日期格式 | yml 里加 spring.jackson.date-format |
| 前端页面白屏 | 路由鉴权逻辑拦截了当前路径 | 检查 router.beforeEach 是否死循环 |
5.4 照片上传与文件存储方案
实习生管理系统里经常要上传照片或简历附件。我建议用本地存储方案:后端接收 MultipartFile,保存到本地的 uploads 目录,再把文件访问路径存到数据库表字段里。提供一个静态资源映射配置:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/**")
.addResourceHandler("file:" + System.getProperty("user.dir") + "/uploads/");
}
}
前端用 el-upload 组件上传,上传成功之后把返回的 URL 拼到表单里提交,图片预览用 el-image 组件的 src 直接指向这个 URL。如果你不想费心处理文件,也可以直接用 Base64 把图片存进数据库字段,但这种方法会让数据库急剧膨胀,只能算偷懒方案。
6. 从“能跑”到“好看”:打磨细节提升毕设档次
6.1 演示数据与业务闭环
很多学生的系统跑起来之后空空荡荡,演示效果大打折扣。我建议在数据库里提前造一批看起来很真实的数据,比如:
- 20个实习生分布在5个不同部门,姓名、学校、专业、实习起止日期各不相同
- 每个实习生有近三个月的考勤记录,部分月份有缺勤或迟到记录,这样统计图表才有波动
- 请假记录覆盖“待审批”“已通过”“已驳回”三种状态,方便演示审批流程
- 公告通知发布三五条,带上不同发布时间
这些数据配合系统操作一条龙走下来,从“管理员发布公告” -> “实习生登录看到待办” -> “提交请假” -> “导师审批” -> “管理员看统计图表”,业务是闭环的,演示时故事线非常清晰。建议你把这条演示路径写在答辩 PPT 里,比干巴巴讲架构更能打动老师。
6.2 前端体验优化细节
绝对不要忽略“视觉和交互”层面的优化,因为答辩时老师首先看到的就是界面。我在前端做过的几个实用优化可供参考:
首先,所有列表页要有加载状态提示,v-loading 指令用上,请求接口时按钮显示 loading,避免让人以为点击没反应。其次,表单提交前后要做校验和防重复提交,比如提交请假按钮在请求期间要禁用,防止连点两次生成两条重复数据。最后,表格的每一行操作按钮加上文字提示,空数据时要展示 el-empty 空状态组件,这些细节看起来不起眼,但整体观感提升非常明显。
值得一提的是,仪表盘首页用统计卡片展示核心数据:实习生总数、待审批请假、本月出勤率、任务完成数,再加上 ECharts 图表展示趋势。这个页面在演示时几乎不需要解释,一眼就能看明白系统的价值,属于“视觉开口说话”的设计。
6.3 代码层面的加分项
如果你的目标不是“通过毕设”而是“拿优秀毕设”,可以在代码层面加几个动作:
第一,后端接口写单元测试和接口调试截图,把测试结果截图放进答辩 PPT,这是企业级的专业表现。第二,用 Git 管理整个项目,提交记录保持整洁,远程仓库的提交图也是加分项。第三,写一份高质量的 README 文档,包含项目介绍、技术栈、快速启动步骤、演示账号和页面截图,这份文档既能证明你的工程化素养,也能帮评审老师快速了解项目。
如果还有余力,可以做两个低成本但高亮点的功能:数据导出 Excel,用 EasyExcel 或 Apache POI 实现;登录验证码,用 Hutool 的 CaptchaUtil 一行代码生成图片验证码。这两个功能技术含量不算高,但视觉演示效果很好,答辩时经常能引来老师的好评。
6.4 系统扩展方向
实习生管理系统天然是一个可以纵向深挖的领域。如果你有时间,我建议尝试以下扩展,这种“轻量扩展”通常不会破坏原有架构:
把实习生档案分成“基本信息、教育经历、工作经历、技能证书、奖惩记录”多个子表,做成 Tab 页展示。把考勤从手动录入改成二维码签到,实习生在手机上扫码即可打卡,这会让项目从“管理后端”升格为“全场景应用”。把实习生考核细化为月度 KPI,支持导师打分和自动汇总,配合折线图展示整个实习期的成长曲线。接入消息通知能力,比如请假审批通过后给实习生发短信提醒,或者邮件模板。这些扩展哪个拎出来都能在答辩时多讲五分钟,而且都是“基于现有框架的合理演进”。
我个人的体会是,这类管理系统的技术天花板不在于用了什么新框架,而在于业务逻辑是否清晰、数据流转是否严谨、体验是否好用。把“实习生从入职到结束”这条完整链路踏踏实实做透,你的 SpringBoot 和 Vue 开发能力就已经能支撑大部分企业级应用的日常开发了。最后再分享一个小技巧:开发过程中每完成一个功能模块就手动完整测试一遍,顺手在西城新维的文件管理表里把接口文档和变更记录同步更新,养成这个习惯,你会在答辩前夜轻松很多。
