SpringBoot+Vue高校入校审批系统实战指南

发布时间:2026/9/28 22:38:37
SpringBoot+Vue高校入校审批系统实战指南 简介本资源是一套面向计算机专业本科生的高分毕业设计实战项目聚焦高校入校申报审批业务场景采用SpringBoot后端Vue前端的主流全栈技术架构完整实现用户管理、申报提交、多级审批、数据统计与系统配置等核心功能适用于毕业设计选题参考、课程设计开发及期末综合实训。压缩包共367个文件包含83个Java后端逻辑类、32个Vue组件页面、15个JS交互脚本、10个XML配置文件、7个MP4演示视频及配套SQL数据库脚本、BAT一键部署脚本和详细使用文档整体28.54MB结构清晰、开箱即用。已有83人学习下载项目已通过导师指导与答辩评审获97分高分所有代码在Windows 10/11环境实测可运行并提供完整部署教程与调试说明助力开发者快速掌握前后端分离开发流程与审批类系统设计要点。1. 这不是又一个“学生管理系统”SpringBootVue入校申报审批系统为什么能拿高分你手头可能正压着三四个Java毕设选题——图书借阅、宿舍管理、在线考试……但真正让答辩老师眼睛一亮、让指导教师主动推荐进优秀案例库的从来不是功能堆砌而是业务闭环的真实感 技术落地的克制感。这个“基于SpringBootVue的入校申报审批系统”表面看是流程审批内核却是高校数字化迎新场景下对多角色协同、状态机驱动、异步通知与数据一致性的一次紧凑实践。它不炫技没上Redis集群做秒杀没硬套Nacos做服务发现但把SpringBoot的自动配置、MyBatis-Plus的CRUD效率、Vue Router的路由守卫、Element Plus表单校验、Axios拦截器统一错误处理这些“基础能力”拧成一股绳跑通了从新生填报→院系初审→教务复核→后勤备案→短信/邮件通知→PDF归档的全链路。适合想用真实业务倒逼技术理解的同学——不是写完增删改查就交差而是要回答“为什么这里用Async而不是MQ”“为什么审批流不用Activiti而手写状态机”“Vue里怎么防止重复提交导致双审核”本文不讲PPT美化技巧只拆解这套高分项目的可复现骨架、必调参数、血泪踩坑点和验收级验证方法。2. 搭建最小可行骨架SpringBoot后端启动Vue前端联调的5个关键命令一个毕设项目能否30分钟内跑起来决定了你后续80%的调试信心。这套系统采用前后端分离标准结构后端用SpringBoot 2.7.18JDK 8兼容性好避免Gradle版本冲突前端用Vue 2.6.14 Vue CLI 4.5.15避开Vue 3的Composition API学习成本聚焦业务逻辑。下面是你本地启动必须执行的5条命令每条都对应一个真实卡点。2.1 初始化MySQL数据库并导入初始数据提示别跳过这步很多同学卡在“登录页404”或“用户名密码错误”根源是数据库没建表或初始管理员没插入。# 1. 创建数据库字符集必须utf8mb4否则emoji和中文姓名存不全 mysql -u root -p -e CREATE DATABASE school_approval DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 2. 执行SQL脚本假设你已解压源码sql目录下有init.sql mysql -u root -p school_approval ./sql/init.sql # 3. 验证关键表是否存在尤其检查sys_user表是否有id1的admin用户 mysql -u root -p -e USE school_approval; SELECT id, username, password FROM sys_user WHERE id 1;init.sql脚本包含6张核心表sys_user用户、apply_form申报主表、approval_record审批记录、department院系、notice_template通知模板、file_attachment附件。其中apply_form.status字段是状态机核心值为draft草稿、submitted已提交、reviewing院系审核中、approved通过、rejected驳回——不是数字枚举而是字符串枚举方便后期扩展比如加waiting_payment支付待确认状态。2.2 启动SpringBoot后端并验证API连通性后端模块名为school-approval-server使用Maven构建。关键配置在application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/school_approval?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password # ← 这里必须填你本地MySQL密码 redis: host: localhost port: 6379 database: 0 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志调试时别关启动命令cd school-approval-server mvn spring-boot:run -Dspring.profiles.activedev启动成功后立刻验证两个核心接口GET http://localhost:8080/api/user/login→ 返回{code:401,msg:未登录}说明后端已启动JWT拦截器生效POST http://localhost:8080/api/user/login Body{username:admin,password:123456}→ 返回含token的JSON说明数据库、密码加密、JWT生成全链路通参数说明-Dspring.profiles.activedev激活开发配置避免误用生产环境的Redis地址或邮箱配置。mybatis-plus.configuration.log-impl开启SQL打印是排查“查不到数据”的第一线索——如果控制台没SQL输出先检查该配置是否拼错。2.3 启动Vue前端并解决跨域问题前端目录为school-approval-web依赖vue-cli-service。但直接npm run serve必然报错Network Error。因为Vue默认端口8080与后端冲突且未配代理。# 修改 vue.config.js关键 module.exports { devServer: { port: 8081, // ← 前端端口改为8081避开后端8080 proxy: { /api: { target: http://localhost:8080, // ← 代理到后端 changeOrigin: true, pathRewrite: { ^/api: // ← 把/api前缀去掉再转发 } } } } }然后启动cd school-approval-web npm install # 确保依赖完整尤其注意element-ui版本必须2.15.14新版有兼容问题 npm run serve访问http://localhost:8081若看到登录页打开浏览器开发者工具 → Network标签 → 点登录按钮观察POST /api/user/login请求是否返回200。如果仍是404检查vue.config.js是否保存、是否重启了npm run serve热更新不生效代理配置。2.4 验证JWT Token传递与权限拦截登录成功后前端会将Token存入localStorage并在后续请求Header中携带Authorization: Bearer token。后端用EnableWebSecurity 自定义JwtAuthenticationFilter拦截。验证方法# 手动构造一个带Token的请求用curl或Postman curl -X GET http://localhost:8080/api/apply/my-list \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx \ -H Content-Type: application/json预期返回200及申请列表。如果返回401检查Token是否过期默认2小时jwt.expiration7200在application.yml中JwtUtil.validateToken()方法中secret是否与application.yml中jwt.secret一致大小写敏感JwtAuthenticationFilter是否被Spring Security正确加载看启动日志是否有Registering filter JwtAuthenticationFilter2.5 前后端联调第一个业务接口获取当前用户待办审批这是检验“角色权限数据隔离”的关键接口。新生登录后应看到自己提交的申请院系老师登录后应看到本院系待审核的申请。// Vue组件中调用例如在Dashboard.vue this.$axios.get(/api/approval/todo-list).then(res { this.todoList res.data; });后端对应ControllerGetMapping(/todo-list) public ResultListApprovalRecord getTodoList() { Long userId SecurityUtils.getUserId(); // 从JWT解析出用户ID String userRole SecurityUtils.getUserRole(); // 获取角色STUDENT/DEAN/ADMIN return Result.success(approvalService.getTodoList(userId, userRole)); }getTodoList()方法根据userRole动态拼接MyBatis-Plus QueryWrapperSTUDENT → 查apply_form.user_id ? AND status IN (reviewing,approved)DEAN → 查apply_form.department_id ? AND status submittedADMIN → 查所有status reviewing逻辑说明这里没用Shiro或Spring Security ACL做细粒度授权而是用业务层条件过滤既降低复杂度又确保数据不出界。这也是高分项目“务实”的体现——不为用框架而用框架。3. 审批流状态机设计为什么不用Activiti手写状态流转的3个核心表与2个校验点很多同学看到“审批系统”第一反应是上工作流引擎。但Activiti在毕设场景有三大硬伤学习曲线陡峭、流程图XML难维护、与SpringBoot整合易冲突。本项目采用轻量级状态机模式用3张表2个校验点实现可读、可测、可调试的审批逻辑。3.1 三张表构成状态机骨架表名核心字段作用关键约束apply_formid,user_id,department_id,status,create_time,update_time申报主实体status为ENUM(draft,submitted,reviewing,approved,rejected)NOT NULLapproval_recordid,form_id,approver_id,status,comment,approve_time每次审批动作留痕form_id外键关联apply_form.idstatus同主表枚举approval_configid,role,next_role,required_fields审批环节配置如STUDENT提交后→DEAN审核role和next_role组合唯一避免环路注意approval_config表在初始化SQL中已预置两条记录(1, STUDENT, DEAN, [idCard,enrollmentYear])(2, DEAN, ADMIN, [reviewOpinion])这意味着学生提交时必须填身份证号和入学年份院系审核时必须填审核意见。字段校验由后端Valid注解自定义RequiredFields注解实现不是前端JS校验——防绕过。3.2 状态流转的两个强制校验点所有状态变更如“提交”、“同意”、“驳回”必须经过ApplyService.changeStatus()方法该方法内置两个不可绕过的校验校验点1前置状态合法性public boolean changeStatus(Long formId, String newStatus, Long approverId) { ApplyForm form applyFormMapper.selectById(formId); // 规则只有status为submitted的表单才能被DEAN审核 if (DEAN.equals(getCurrentUserRole()) !submitted.equals(form.getStatus())) { throw new BusinessException(当前表单状态不可审核请检查是否已提交); } // 规则只有status为reviewing的表单才能被ADMIN终审 if (ADMIN.equals(getCurrentUserRole()) !reviewing.equals(form.getStatus())) { throw new BusinessException(当前表单状态不可终审); } // ... 其他角色规则 }校验点2操作人角色匹配// 在changeStatus()开头加入 String currentRole SecurityUtils.getUserRole(); if (!APPROVAL_CONFIG_MAP.containsKey(currentRole)) { throw new BusinessException(您无权执行此操作); } // 检查当前角色是否在配置表中定义了下一步 String nextRole approvalConfigMapper.getNextRole(currentRole); if (nextRole null || !nextRole.equals(newStatusToRoleMap.get(newStatus))) { throw new BusinessException(非法状态跳转从 currentRole 不能直接变为 newStatus); }逻辑说明newStatusToRoleMap是内存缓存ConcurrentHashMap将approved→ADMINrejected→DEAN等映射预加载。这样避免每次查库也防止SQL注入式状态篡改如前端传statusapproved但当前是学生角色。3.3 审批记录的幂等性保障为什么用INSERT IGNORE而非UPDATE当院系老师点击“同意”后端需同时更新apply_form.status和插入approval_record。常见错误是先UPDATE再INSERT导致并发时重复记录。本项目采用原子化插入触发器兜底-- approval_record表建表语句关键 CREATE TABLE approval_record ( id bigint NOT NULL AUTO_INCREMENT, form_id bigint NOT NULL, approver_id bigint NOT NULL, status varchar(20) NOT NULL, comment text, approve_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_form_approver (form_id,approver_id) -- ← 唯一索引防重复 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;Java代码// 使用MyBatis-Plus的saveOrUpdate方法但传入record时指定id为null ApprovalRecord record new ApprovalRecord(); record.setFormId(formId); record.setApproverId(approverId); record.setStatus(newStatus); record.setComment(comment); // saveOrUpdate会根据uk_form_approver判断是否已存在存在则忽略 approvalRecordMapper.insert(record); // ← 直接insert靠唯一索引保证幂等参数说明UNIQUE KEY uk_form_approver (form_id,approver_id)是核心。同一表单同一审批人只能有一条记录避免老师手抖点两次“同意”产生两条记录。insert比replace into更安全——后者会删除旧记录再插入影响approve_time时间戳。4. 文件上传与PDF生成Vue上传组件SpringBoot文件存储IText PDF导出的避坑指南毕设答辩时评委最爱问“附件怎么存的PDF报告怎么生成的”——因为这是最易翻车的环节。本系统用Vue Element Upload组件 SpringBoot MultipartFile IText 7.1.15实现但中间有3个深坑踩一个就导致演示失败。4.1 Vue上传组件禁用自动上传手动控制时机Element UI的el-upload默认auto-uploadtrue会立即发请求。但申报表单有多个字段文本、日期、文件必须等所有字段校验通过后再上传。错误写法!-- ❌ 错误auto-upload导致文件提前上传 -- el-upload action/api/file/upload auto-upload el-button sizesmall typeprimary点击上传/el-button /el-upload正确写法手动触发!-- ✅ 正确绑定refsubmit时调用upload() -- el-upload refuploadRef :action/api/file/upload?id formData.id !-- 动态传表单ID -- :http-requesthandleUpload :on-successhandleUploadSuccess :on-errorhandleUploadError :show-file-listfalse el-button sizesmall typeprimary上传附件/el-button /el-upload script export default { methods: { // 表单提交时调用 async submitForm() { const valid await this.$refs.form.validate(); if (valid) { // 先保存表单主体获取id const res await this.$axios.post(/api/apply/save, this.formData); this.formData.id res.data.id; // 再触发上传 this.$refs.uploadRef.submit(); } }, handleUpload(fileObj) { // 自定义上传逻辑用axios发FormData const formData new FormData(); formData.append(file, fileObj.file); formData.append(formId, this.formData.id); return this.$axios.post(/api/file/upload, formData, { headers: { Content-Type: multipart/form-data } }); } } } /script逻辑说明http-request替代默认上传handleUpload中显式构造FormData并附带formId确保文件与申报表单强绑定。show-file-listfalse避免Element默认UI干扰。4.2 SpringBoot文件存储本地路径安全与磁盘空间预警后端接收文件后不存数据库Blob而存本地磁盘/uploads/数据库只存相对路径。但直接file.transferTo(new File(path))有风险PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file, RequestParam(formId) Long formId) { // ❌ 危险未校验文件名可能../webshell.jsp String originalFilename file.getOriginalFilename(); String filePath /uploads/ formId / originalFilename; // ✅ 正确白名单校验重命名 String ext FilenameUtils.getExtension(originalFilename).toLowerCase(); if (!Arrays.asList(pdf, jpg, jpeg, png, doc, docx).contains(ext)) { throw new BusinessException(不支持的文件类型 ext); } String safeName UUID.randomUUID().toString() . ext; Path uploadPath Paths.get(uploads, formId.toString(), safeName); Files.createDirectories(uploadPath.getParent()); // 自动创建多级目录 file.transferTo(uploadPath.toFile()); // 数据库存储相对路径便于Nginx静态服务 fileAttachmentMapper.insert(new FileAttachment() .setFormId(formId) .setFilePath(uploads/ formId / safeName) .setFileName(originalFilename) .setFileSize(file.getSize())); return Result.success(uploads/ formId / safeName); }参数说明FilenameUtils.getExtension()来自Apache Commons IO比originalFilename.substring(originalFilename.lastIndexOf(.))更安全防xxx.jpg.exe。Files.createDirectories()确保uploads/123/目录存在避免NoSuchFileException。4.3 IText PDF导出中文宋体支持与表格边框渲染导出《入校申报确认单》PDF是高分亮点。IText 7默认不支持中文需引入字体!-- pom.xml -- dependency groupIdcom.itextpdf/groupId artifactIditext7-core/artifactId version7.1.15/version typepom/type scopecompile/scope /dependency !-- 中文字体支持 -- dependency groupIdcom.itextpdf/groupId artifactIdfont-asian/artifactId version7.1.15/version /dependencyJava导出核心代码public void exportPdf(Long formId, HttpServletResponse response) throws IOException { ApplyForm form applyFormMapper.selectById(formId); // 加载思源黑体免费可商用 PdfFont font PdfFontFactory.createFont(src/main/resources/fonts/NotoSansCJKsc-Regular.otf, PdfEncodings.IDENTITY_H); // IDENTITY_H支持中文 PdfWriter writer new PdfWriter(response.getOutputStream()); PdfDocument pdf new PdfDocument(writer); Document document new Document(pdf); // 标题 document.add(new Paragraph(入校申报确认单) .setFont(font).setFontSize(18).setBold().setTextAlignment(TextAlignment.CENTER)); // 表格3列表格项目、内容、备注 Table table new Table(UnitValue.createPercentArray(new float[]{2, 3, 1})) .useAllAvailableWidth(); table.addHeaderCell(new Cell().add(new Paragraph(项目).setFont(font))); table.addHeaderCell(new Cell().add(new Paragraph(内容).setFont(font))); table.addHeaderCell(new Cell().add(new Paragraph(备注).setFont(font))); // 填充数据行注意中文内容必须setFont table.addCell(new Cell().add(new Paragraph(申请人).setFont(font))); table.addCell(new Cell().add(new Paragraph(form.getRealName()).setFont(font))); table.addCell(new Cell().add(new Paragraph().setFont(font))); // 设置表格边框关键默认无边框 table.setBorder(new SolidBorder(ColorConstants.BLACK, 0.5f)); // 整表边框 for (int i 0; i table.getNumberOfCells(); i) { table.getCell(i).setBorder(new SolidBorder(ColorConstants.LIGHT_GRAY, 0.3f)); } document.add(table); document.close(); }逻辑说明PdfFontFactory.createFont()加载OTF字体文件路径必须是绝对路径或ClassPath资源。setBorder()必须对每个Cell单独设置table.setBorder()只设外边框。SolidBorder的宽度0.5f比1f更细腻符合正式文档审美。5. 高频踩坑与排查5个让答辩老师皱眉的典型问题及根治方案毕设演示时最怕什么不是功能少而是明明代码写了却跑不通或者跑通了但数据不对。以下是我在3届毕设指导中统计的最高频5个问题每个都按“现象→原因→根治方案”给出可立即执行的修复。5.1 现象登录成功后跳转到空白页Network里显示GET /api/apply/my-list 401原因Vue前端将Token存入localStorage但axios.interceptors.request中未正确读取并添加到Header。根治方案检查utils/request.js// ✅ 正确写法必须用localStorage.getItem service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer token; // ← 注意Bearer后有空格 } return config; }); // ❌ 错误写法变量名写错或未加Bearer // config.headers.token token; // 服务端收不到 // config.headers.Authorization token; // 缺少Bearer 5.2 现象MySQL插入中文乱码数据库显示??????原因MySQL服务端、数据库、表、连接URL四层字符集不一致最常见是application.yml中URL缺serverTimezone参数。根治方案四步全检查MySQL服务端SHOW VARIABLES LIKE character_set%;确保character_set_serverutf8mb4数据库SHOW CREATE DATABASE school_approval;确保DEFAULT CHARSETutf8mb4表SHOW CREATE TABLE apply_form;确保DEFAULT CHARSETutf8mb4application.ymlurl必须含?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai提示serverTimezoneAsia/Shanghai解决时区问题避免create_time存成0000-00-00。5.3 现象Vue页面刷新后登录状态丢失提示“请先登录”原因JWT Token过期时间jwt.expiration7200短于浏览器Session或刷新时未重新校验Token有效性。根治方案前端增加Token续期逻辑// 在路由守卫 beforeEach 中 router.beforeEach(async (to, from, next) { const token localStorage.getItem(token); if (token) { try { // 发起一个轻量接口验证Token如GET /api/user/info await axios.get(/api/user/info); next(); } catch (e) { // Token失效清空并跳登录页 localStorage.removeItem(token); next(/login); } } else if (to.path ! /login) { next(/login); } else { next(); } });5.4 现象上传大文件10MB时后端报400 Bad Request原因SpringBoot默认spring.servlet.multipart.max-file-size1MB超出即拒绝。根治方案在application.yml中显式增大spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB注意Tomcat也有默认限制若用内嵌Tomcat还需在application.yml加server: tomcat: max-swallow-size: 52428800 # 50MB5.5 现象PDF导出中文显示方块或表格边框消失原因IText未正确加载中文字体或Cell.setBorder()未调用。根治方案字体确认NotoSansCJKsc-Regular.otf文件放在src/main/resources/fonts/且PdfFontFactory.createFont()路径正确用getClass().getResource(/fonts/...)更可靠边框必须对每个Cell调用setBorder()不能只设Table边框。可封装工具方法private static Cell withBorder(Cell cell) { return cell.setBorder(new SolidBorder(ColorConstants.BLACK, 0.5f)); } // 使用table.addCell(withBorder(new Cell().add(...)));6. 验证与交付用3个命令完成答辩级验收以及我坚持的2个交付习惯答辩前最后一小时你不需要重跑整个项目只需执行3条命令就能确认系统处于“可演示、可讲解、可截图”的交付状态。这比反复点击页面更高效也更能体现工程素养。6.1 一键验证数据库、API、前端三连检# 1. 检查数据库表结构与初始数据确保管理员存在 mysql -u root -p -e USE school_approval; SHOW TABLES; SELECT COUNT(*) FROM sys_user; # 2. 验证核心API连通性curl比浏览器更可控 curl -s -o /dev/null -w %{http_code} http://localhost:8080/api/user/login # 3. 检查前端静态资源是否构建成功生产环境验证 cd school-approval-web npm run build ls -lh dist/js/ | head -3预期输出SHOW TABLES显示6张表名SELECT COUNT(*) FROM sys_user返回1admin用户curl返回401说明后端启动且JWT拦截器工作ls -lh dist/js/显示至少3个JS文件大小均 100KB说明Vue打包成功提示curl -w %{http_code}只输出HTTP状态码-s静默-o /dev/null丢弃响应体——这是CI/CD常用技巧避免输出干扰。6.2 演示视频录制黄金参数FFmpeg命令与帧率选择答辩要求的“演示视频”不是录屏软件随便点点。我用FFmpeg命令行导出确保画质清晰、体积可控、无卡顿# 录制桌面区域1920x1080位置0,0音频来自麦克风 ffmpeg -f avfoundation -i 1:0 \ # macOSWindows用gdigrab -f x11grab -video_size 1920x1080 -i :0.00,0 \ -c:v libx264 -crf 23 -preset fast \ -c:a aac -b:a 128k \ -r 30 \ # 关键30fps比60fps更稳文件小50% -t 180 \ # 录制3分钟 demo.mp4参数说明-crf 23质量参数18~28之间23是视觉无损与体积的平衡点-preset fast编码速度fast比ultrafast压缩率高30%比medium快2倍-r 30帧率网页演示30fps足够60fps反而增大体积且无感知提升-t 180严格限时避免视频过长被评委快进6.3 我坚持的两个交付习惯习惯1源码包里永远放一份README_DEPLOY.md不叫README.md而叫README_DEPLOY.md内容只有一件事新人从零部署的完整命令清单。包括JDK版本openjdk version 1.8.0_362MySQL版本mysql Ver 8.0.33Node.js版本v16.20.25条必须执行的命令含数据库导入、后端启动、前端启动、初始账号、验证URL 没有解释没有“建议”只有可复制粘贴的命令。因为答辩老师可能只给你3分钟看部署过程。习惯2数据库SQL脚本里加一行-- VERSION: 20240520在init.sql第一行写明版本时间戳。这样当多个同学共享代码时我能一眼看出谁用了旧版SQL比如漏了file_attachment表。Git提交时SQL脚本的修改必须同步更新这个时间戳——它是我判断“数据结构是否一致”的唯一依据。最后说一句这个系统能拿高分不是因为它用了多少新技术而是因为每个技术点都选择了最稳的解法并把边界条件写进了代码注释里。比如JwtUtil.java里写着// 注意secret长度必须32字符否则HS256算法会抛异常比如ApplyService.java里写着// TODO未来可接入短信网关当前用print模拟。这些细节才是评委在你离开后还会翻看你代码的原因。希望帮到你。本文还有配套的精品资源点击获取