开箱即用的一套实习生管理系统SpringBoot后端、Vue前端、MySQL做数据存储三件套组合已经被无数个中小型项目验证过了。这套源码有完整的用户登录、角色权限、实习过程记录、日报周报、考勤统计、成绩评定能直接跑起来也适合拿来二次开发。这篇内容我不打算贴一堆官方文档而是把整个系统的设计逻辑、核心模块的实现思路、数据库表怎么建、前后端怎么联调、部署时最容易翻车的几个点全部拆开讲一遍。无论你是打算拿这套系统做课程设计、企业内部工具还是想学一下前后端分离项目的完整结构后面的内容都值得认真看一遍。1. 项目全景一个实习管理系统到底解决了什么问题1.1 核心需求拆解实习生管理在很多企业里是一个“看似简单实际人人头疼”的事情。实习生来了一波又一波信息登记靠Excel日报周报靠邮箱考勤靠微信群里手动签到最后实习结束要给个评价又要去翻聊天记录。这套系统的出现就是把这些散落的信息收拢到一个平台上让实习生、指导老师、企业管理员三方都在一个页面里完成自己的工作。从角色上看系统至少要拆成三类用户实习生本人、指导老师或者部门主管、系统管理员。实习生要能维护个人资料、查看实习单位信息、打卡签到、提交日报周报、查看自己的得分指导老师要能看到名下带的学生、审核日报周报、登记考勤异常、录入评语和分数管理员则是综合管理包括创建用户、分配角色、维护单位信息、查看统计报表。从业务流程上看实习生进入企业之后管理员先开通账号分配实习单位和指导老师。实习生每天登录系统签到定期提交日报或者周报指导老师对这些内容进行审核。实习期结束指导老师和单位负责人打分系统汇总成最终成绩。这个过程如果全靠线下任何一个环节断掉都会出问题而信息化的核心就是把流程中的每一步变成可跟踪的数据记录。1.2 技术选型的逻辑为什么是SpringBoot Vue MySQL很多人一上来会问为什么用SpringBoot而不是SSH或者Servlet JSP为什么用Vue而不是JQuery Bootstrap。这个问题其实得结合项目交付和后续维护来看。SpringBoot最大的优势是“配置简化 生态成熟”。实习生管理系统属于典型的管理信息系统涉及用户登录、权限控制、CRUD、文件上传、报表统计这些在Spring生态里都有非常成熟的解决方案。用SpringBoot可以迅速把项目骨架搭起来内嵌Tomcat打成jar包就能跑部署成本极低。相比传统的SSH那一套省掉了大量XML配置对新手更友好对老手来说写代码也更清爽。Vue在前端的价值体现在组件化和数据驱动的页面更新。管理后台这种界面左边菜单、顶部栏、中间内容区加上一堆表格和表单用Vue写起来比JQuery直接操作DOM要舒服太多。尤其是表格数据要和后端接口联动Vue的响应式机制能自动刷新视图不需要手动拼接HTML字符串。再加上Element UI这类组件库表格、弹窗、表单验证开箱即用界面能在一两天内搭出专业感。MySQL在这个场景下完全是性价比最高的选择。实习生管理系统的数据量不大事务要求也不高MySQL的单机性能足够支撑几千用户的规模。而且MySQL的生态工具链非常完整Navicat、DBeaver、mysqldump备份、迁移、调试都方便。用PostgreSQL当然也可以但项目要“可直接运行”MySQL的普适性明显更强。2. 系统设计从数据库到接口把业务理清楚2.1 数据库设计核心表结构解析一个信息管理系统的地基在数据库。如果表建得稀烂后面写代码全是补丁。这套系统的库名建议直接用intern_manage字符集用utf8mb4排序规则用utf8mb4_general_ci因为要存中文也可能会存表情符号utf8mb4是最稳的。核心表离不开这么几张用户表、实习生信息表、实习单位表、日报/周报表、考勤表、成绩评定表。下面逐个说用户表sys_user是所有登录账号的根基字段包括用户ID、用户名、密码、真实姓名、角色类型、手机号、邮箱、状态、创建时间。密码必须存加密后的密文千万不要明文存。角色类型可以用一个字符串字段区分STUDENT、TEACHER、ADMIN也可以用角色关联表但小项目里直接一个字段最省事。实习生信息表intern_info和用户表是一对一的关系额外记录学号、学校、专业、实习单位ID、指导老师ID、实习开始日期、实习结束日期、实习状态。学校和专业用普通字符串就行不需要单独建字典表除非你的字典项特别多。实习单位表company_info记录单位名称、统一社会信用代码、单位地址、联系人、联系电话、单位性质。这个表可以被实习生表和成绩评定表引用。日报/周报表intern_report是业务流水表字段包括ID、实习生用户ID、报告类型日/周、报告日期、所属周、工作内容、问题与收获、指导老师评语、审核状态、提交时间、审核时间。审核状态用数字0/1/2表示待审核、已通过、已驳回比用字符串更有效率。考勤表attendance_record字段包括ID、实习生用户ID、签到日期、签到时间、签到状态、备注。每天一人一条记录当天重复签到就做更新而不是插入新记录。成绩评定表score_assessment字段包括ID、实习生用户ID、单位评分、导师评分、自评分、综合得分、评价内容、评定时间。综合得分可以前端算也可以后端算建议后端算保持逻辑统一。这里有一个容易忽略的点所有业务表都要带上create_time和update_time哪怕你觉得没必要。后面排查数据问题、写统计SQL的时候时间字段会让你省掉一大半麻烦。2.2 后端工程结构与接口分层后端包结构建议按模块分不要一个包下面堆一百个类。我建议这样拆com.example.intern ├── common │ ├── Result.java │ ├── ResultCode.java │ └── GlobalExceptionHandler.java ├── config │ ├── CorsConfig.java │ └── WebMvcConfig.java ├── controller │ ├── AuthController.java │ ├── InternController.java │ ├── ReportController.java │ ├── AttendanceController.java │ └── ScoreController.java ├── service │ ├── AuthService.java │ ├── InternService.java │ ├── ReportService.java │ └── ScoreService.java ├── mapper │ ├── UserMapper.java │ ├── InternMapper.java │ ├── ReportMapper.java │ └── ... ├── entity │ ├── SysUser.java │ ├── InternInfo.java │ └── ... └── utils └── JwtUtils.javaController层只做参数接收和结果返回Service层写业务逻辑Mapper层通过MyBatis操作数据库。这样分层的好处是如果前端字段变了你只需改Controller如果业务规则变了你只需改Service如果数据库表改了你只需改Mapper。统一返回结果类是前后端联调的基础。每个接口返回的JSON结构要一致比如public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message 操作成功; result.data data; return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }前端只要判断code 200就取data其余情况弹message。这个约定看上去简单但能省掉大量因返回结构不一致导致的联调时间。全局异常处理也很重要。写代码的时候难免出现空指针、参数不合法如果不做兜底系统会直接返回Tomcat默认错误页前端根本没法解析。用RestControllerAdvice拦截所有异常统一返回Result.error(500, e.getMessage())这样后端崩了前端也能看到一条明确提示而不是白白刷新页面。2.3 前端页面与交互逻辑Vue前端工程目录按页面模块划分整个后台采用经典布局左侧菜单、顶部栏、中间内容区。路由表用动态渲染根据用户角色过滤可见菜单。src ├── api │ ├── auth.js │ ├── intern.js │ ├── report.js │ └── score.js ├── router │ └── index.js ├── store │ └── user.js ├── views │ ├── login.vue │ ├── layout.vue │ ├── intern/info.vue │ ├── report/list.vue │ ├── attendance/calender.vue │ └── score/table.vue └── utils └── request.jsaxios请求封装建议统一放在request.js设置基础URL、超时时间、拦截器。请求拦截器里带上token响应拦截器里统一处理401跳转登录和错误提示。import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization 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.data }, error { if (error.response error.response.status 401) { localStorage.clear() window.location.href /login } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default request这个封装几乎是所有Vue后台管理项目的通用模板拿来直接用就行。注意baseURL要配合开发环境的代理否则会出现跨域。3. 核心功能实现几个关键模块的实战拆解3.1 权限控制如何做到不同角色各看各的权限控制是信息系统绕不开的环节。这套系统最简单可靠的方案是“登录认证 角色拦截”。后端使用JWT做登录认证。用户登录成功后根据用户名查询用户信息把用户ID和角色塞进token设置有效期返回给前端。前端把token存在localStorage里每次请求在请求头带上。后端通过一个拦截器验证token解析出用户信息后存入ThreadLocal后面的Controller和Service直接从ThreadLocal取当前用户不用每个接口都传用户ID。JWT工具类的核心代码大致是这样public class JwtUtils { private static final String SECRET your-secret-key-change-me; private static final long EXPIRE 7 * 24 * 60 * 60 * 1000L; public static String createToken(String userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }拦截器里对不需要认证的接口比如登录接口、验证码接口放行其余接口全部校验。校验通过后可以根据角色再做接口级控制比如管理员可以调用GET /api/intern/list查看所有实习生而实习生只能拿到自己的数据。最简单的做法是在Service层加一个判断if (STUDENT.equals(userRole)) { query.setInternUserId(currentUserId); }前端页面同样要做角色控制。路由表里给每个路由配置meta.roles比如{ path: /score, component: Layout, meta: { roles: [ADMIN, TEACHER] }, children: [ { path: list, component: ScoreList, meta: { roles: [ADMIN, TEACHER] } } ] }在路由守卫里读取当前用户的角色如果不在可访问列表里就重定向到首页。不要只靠前端隐藏菜单后端必须做校验否则别人直接调接口就能越权。3.2 实习过程管理从签到到日报的流转实习过程管理是整个系统的业务核心。实习生每天登录系统后的第一件事就是签到。签到接口的逻辑是先判断当前用户是否是实习生角色再查今天是否已经签到。如果已签到直接返回“今日已签到”如果没签到插入一条记录。为了避免重复插入数据库层面可以给attendance_record表加一个唯一索引uk_intern_date(intern_user_id, sign_date)这样即使并发请求数据库也会拒绝重复记录而不是让代码去判断。这个做法看起来简单但很多新手会忽略导致并发情况下数据有两条。日报提交的逻辑类似。实习生填写工作内容、遇到问题、明日计划提交后状态是“待审核”。指导老师打开审核列表看到自己名下的实习生提交的报告可以点击通过或者驳回驳回时必须填写原因实习生端会看到驳回原因并重新编辑提交。这里的状态流转可以用数字字段控制0待审核 1已通过 2已驳回。写一个提交日报的接口示例PostMapping(/report/submit) public ResultVoid submitReport(RequestBody ReportSubmitDTO dto) { SysUser currentUser UserContext.getCurrentUser(); if (!STUDENT.equals(currentUser.getRole())) { return Result.error(403, 只有实习生可以提交报告); } Report report new Report(); report.setInternUserId(currentUser.getId()); report.setReportType(dto.getReportType()); report.setReportDate(dto.getReportDate()); report.setContent(dto.getContent()); report.setStatus(0); reportService.save(report); return Result.success(null); }实际代码里还要校验报告日期是否在实习期内、同一日期的报告是否重复提交等细节。这些校验逻辑一旦漏掉就会在真实使用中冒出一堆脏数据。3.3 成绩评定与统计报表成绩评定涉及三个维度实习生自评、指导老师评分、实习单位评分。每个维度满分100分最后综合得分按权重计算自评20%导师评价50%单位评价30%。权重可以在系统参数表里配置也可以写死在代码里为了演示方便先写死但设计时最好留一个配置项。后端在计算综合得分时先查出三个分数然后加权double finalScore selfScore * 0.2 teacherScore * 0.5 companyScore * 0.3;成绩表和实习考核表可以通过实习记录ID关联不过更简单的方式是直接挂在实习生ID下一个实习生只有一个最终成绩。统计报表部分需要写SQL做分组聚合。比如查询每个实习单位的平均分SELECT c.company_name, ROUND(AVG(s.final_score), 2) AS avg_score FROM score_assessment s LEFT JOIN intern_info i ON s.intern_user_id i.user_id LEFT JOIN company_info c ON i.company_id c.id GROUP BY c.company_name ORDER BY avg_score DESC;后端用MyBatis写一个对应的Mapper方法返回一个Map列表前端用ECharts展示柱状图或者饼图。这里要注意SQL中的LEFT JOIN一定不能在连接条件上放多余过滤条件比如想过滤实习生状态要写在WHERE中否则会丢数据。4. 环境准备与直接运行让人少踩坑的部署全过程标题里写了“可直接运行”这背后其实藏着不少环境坑。下面按我实测的流程走一遍。4.1 基础环境要求后端部分需要JDK 1.8及以上推荐1.8因为SpringBoot 2.x在1.8下最稳定不会出现新版本特性带来的兼容问题。Maven 3.6用来拉取依赖。前端部分需要Node.js 14npm 6如果你装的是Node 18也没关系但是很多旧依赖的版本会冲突所以建议用Node 14.21.3这种稳定版本。数据库用MySQL 5.7或8.05.7在老设备上兼容性更好8.0功能更新但需要对应驱动版本如果项目里mysql-connector-java是5.x连8.0会报错所以最好按源码的配置走。4.2 初始化数据库拿到源码后在源码目录下找到一个类似sql/intern_manage.sql的文件。用Navicat或者命令行创建数据库并导入mysql -u root -p -e CREATE DATABASE intern_manage DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -u root -p intern_manage sql/intern_manage.sql导入成功后打开后端的application.yml修改数据库连接信息spring: datasource: url: jdbc:mysql://localhost:3306/intern_manage?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver这里最需要注意的是URL里的serverTimezoneAsia/Shanghai。没有这个参数Java连接MySQL 8.0会报时区异常页面上的时间也会差8小时。4.3 启动后端服务在项目根目录下打开终端先执行mvn clean install -DskipTests然后启动mvn spring-boot:run如果没有报错日志里会看到Tomcat started on port(s): 8080。如果你想打包部署执行mvn clean package -DskipTests然后java -jar target/intern-management-1.0.0.jar提醒一句检测端口是否被占用时注意有些机器的8080端口被其他服务占着。可以用netstat -ano | findstr 8080查看如果占用就改配置。4.4 启动前端工程前端目录一般是frontend或vue-web。进入目录后npm install npm run serve第一次npm install会很慢因为要拉几十个依赖包。如果遇到网络问题可以使用国内镜像源npm config set registry https://registry.npmmirror.com安装成功后运行npm run serve默认端口是8080但后端已经占用了8080Vue的脚手架会检测到端口冲突自动改用8081。所以前端访问地址通常是http://localhost:8081。开发环境下跨域需要在根目录创建vue.config.jsmodule.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }这个配置的含义是前端把请求发给/api开头的地址开发服务器转发到后端的8080并且把路径中的/api前缀去掉。如果你的后端接口本身就带/api前缀就不用pathRewrite。这个细节很容易搞错一旦配置错接口会404。4.5 验证系统可访问打开浏览器输入http://localhost:8081正常情况会跳到登录页。源码里一般预置了三个账号admin/admin123、teacher/123456、student/123456。先用admin登录左侧菜单应该能看到全部功能模块。点击“实习生管理”可以新增一条实习生数据然后给该实习生关联一个账号切换到实习生账号登录能看到自己的信息、签到功能和日报提交流程。如果这几个流程走通说明系统运行正常。5. 实际运行中常见问题与排查经验5.1 数据库连接失败的 5 种可能后端启动或者登录时报数据库连接错误基本逃不出下面几种情况现象原因解决方式Access denied for user用户名或密码不对检查application.yml中的账号密码和数据库实际密码是否一致Unknown database intern_manage数据库没有创建或名字写错执行CREATE DATABASE后重新导入SQLThe server time zone value ...缺少时区参数URL加上serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL 8.0的认证插件导致URL加上allowPublicKeyRetrievaltrueCommunications link failure数据库服务没启动或端口不对检查MySQL服务是否运行、端口是3306还是其他每一条都是真实踩出来的。尤其是allowPublicKeyRetrieval这个参数MySQL 8.0默认情况下连接会直接被拒绝很多人查半天查不到原因。5.2 跨域问题的定位与处理前端的请求发出去了但浏览器控制台报No Access-Control-Allow-Origin header is present这就是跨域问题。解决跨域有两个办法。开发环境优先使用Vue的proxy代理前面已经写了配置。生产环境如果前端和后端不在一起需要后端开启CORSConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }还有一个容易忽略的点使用了JWT后前端请求头带了Authorization那allowedHeaders(*)必须写否则浏览器会以预检请求的形式拦掉带自定义头的请求。5.3 前端页面空白或接口404页面空白大概率是路由问题。如果你用的是history模式并且部署到Nginx没有配置try_files刷新页面就会404。开发环境很少遇到但打包部署后很常见。Nginx配置需要加location / { try_files $uri $uri/ /index.html; }如果是接口404先看请求路径。开发环境下前端proxy代理的target写错或者pathRewrite把不该去掉的前缀去掉了都会导致后端找不到接口。最简单的排查方式打开前端控制台看Network里的请求URL复制出来在浏览器直接访问看返回什么。5.4 端口冲突与配置修改后端端口、前端端口、数据库端口都可能撞车。最常见的场景启动了两个无关的Java服务8080被占用导致SpringBoot启动失败。这时候别急着改端口先查占用。如果确实需要修改后端端口在application.yml里写server: port: 8082同时前端proxy的target也要改成http://localhost:8082。前端端口则通过vue.config.js的devServer.port修改。记住一个原则端口改动一定要同时检查后端配置、前端代理、以及页面里的接口地址漏一个就废。6. 项目扩展方向从可用到好用的升级建议6.1 增加消息通知与定时任务现在的系统都是用户主动刷新才能看到新数据。比如指导老师审核了日报实习生要自己点进详情页才能看到结果。更好的做法是加入站内消息通知审核状态一变实习生登录后首页就有未读消息。后端可以用Spring自带的Scheduled定时任务也可以引入消息队列但小系统没必要上重量级框架。用一个消息表记录通知前端每隔30秒拉一次未读数量这个方案足够简单可靠。定时任务还有一个典型用途每天晚上自动把当天未签到的实习生生成提醒列表第二天早上推送给指导老师。6.2 引入Excel导入导出管理员经常需要批量导入实习生名单而不是一个个在网页上添加。用Apache POI或者阿里开源的EasyExcel都能实现。EasyExcel性能更好API也清爽得多。导入时先解析Excel逐行校验必填项、学号重复、用户是否已存在最后批量插入。导出功能则可以把实习名单、成绩表一键生成Excel文件下载这个功能在真实企业场景里几乎天天用到。6.3 部署方式升级现在直接在服务器上java -jar和npm run build拷贝静态文件已经能应付大部分需求。但如果换机器或者多环境部署环境差异会带来麻烦。建议用Docker Compose把MySQL、后端、前端全部编排起来version: 3 services: mysql: image: mysql:5.7 container_name: intern-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: intern_manage ports: - 3306:3306 volumes: - ./sql:/docker-entrypoint-initdb.d backend: build: ./backend container_name: intern-backend ports: - 8080:8080 depends_on: - mysql frontend: build: ./frontend container_name: intern-frontend ports: - 80:80 depends_on: - backend这样在一台新服务器上只需一条docker-compose up -d就能全部跑起来初始化SQL也会在MySQL容器首次启动时自动执行。6.4 安全增强与日志监控这套系统已经有JWT认证但距离生产标准还差几步。密码如果存的是明文赶紧改成BCrypt加密接口层要加上参数校验注解比如NotNull、Size对于频繁调用的登录接口、接口日志输出建议引入Logback的按天滚动日志并且把登录失败记录、异常堆栈单独输出到文件。线上环境还要加一个健康检查接口配合运维监控系统定时探测系统挂了第一时间告警。我在实际维护这类系统时最大的体会是功能开发只占一半精力另一半全在环境配置、边界条件校验和数据一致性上。比如日报同日重复提交、成绩权重变更、用户被删除后历史数据保留这些细节看起来不起眼却决定了系统能不能从演示Demo走向真正长期使用。把这套源码跑起来只是第一步后续的每一次需求变更都会逼着你把设计想得更完整。如果你准备在这个基础上做二次开发建议先在数据库层把所有外键关系梳理清楚再动手改代码会少走很多弯路。