简介:本资源是一套面向计算机专业本科生的高分毕业设计实战项目,聚焦高校入校申报审批业务场景,采用SpringBoot后端+Vue前端的主流全栈技术架构,完整实现用户管理、申报提交、多级审批、数据统计与系统配置等核心功能,适用于毕业设计选题参考、课程设计开发及期末综合实训。压缩包共367个文件,包含83个Java后端逻辑类、32个Vue组件页面、15个JS交互脚本、10个XML配置文件、7个MP4演示视频及配套SQL数据库脚本、BAT一键部署脚本和详细使用文档,整体28.54MB,结构清晰、开箱即用。已有83人学习下载,项目已通过导师指导与答辩评审,获97分高分,所有代码在Windows 10/11环境实测可运行,并提供完整部署教程与调试说明,助力开发者快速掌握前后端分离开发流程与审批类系统设计要点。
1. 这不是又一个“学生管理系统”:SpringBoot+Vue入校申报审批系统,为什么能拿高分?
你手头可能正压着三四个Java毕设选题——图书借阅、宿舍管理、在线考试……但真正让答辩老师眼睛一亮、让指导教师主动推荐进优秀案例库的,从来不是功能堆砌,而是业务闭环的真实感 + 技术落地的克制感。这个“基于SpringBoot+Vue的入校申报审批系统”,表面看是流程审批,内核却是高校数字化迎新场景下对多角色协同、状态机驱动、异步通知与数据一致性的一次紧凑实践。它不炫技:没上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.18(JDK 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表是否有id=1的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.yml:
spring: datasource: url: jdbc:mysql://localhost:3306/school_approval?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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.active=dev启动成功后,立刻验证两个核心接口:
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.active=dev激活开发配置,避免误用生产环境的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.expiration=7200在application.yml中) JwtUtil.validateToken()方法中secret是否与application.yml中jwt.secret一致(大小写敏感!)JwtAuthenticationFilter是否被Spring Security正确加载(看启动日志是否有Registering filter JwtAuthenticationFilter)
2.5 前后端联调第一个业务接口:获取当前用户待办审批
这是检验“角色权限+数据隔离”的关键接口。新生登录后应看到自己提交的申请;院系老师登录后应看到本院系待审核的申请。
// Vue组件中调用(例如在Dashboard.vue) this.$axios.get('/api/approval/todo-list').then(res => { this.todoList = res.data; });后端对应Controller:
@GetMapping("/todo-list") public Result<List<ApprovalRecord>> 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 QueryWrapper:
- STUDENT → 查
apply_form.user_id = ? AND status IN ('reviewing','approved') - DEAN → 查
apply_form.department_id = ? AND status = 'submitted' - ADMIN → 查所有
status = 'reviewing'
逻辑说明:这里没用Shiro或Spring Security ACL做细粒度授权,而是用业务层条件过滤,既降低复杂度,又确保数据不出界。这也是高分项目“务实”的体现——不为用框架而用框架。
3. 审批流状态机设计:为什么不用Activiti?手写状态流转的3个核心表与2个校验点
很多同学看到“审批系统”第一反应是上工作流引擎。但Activiti在毕设场景有三大硬伤:学习曲线陡峭、流程图XML难维护、与SpringBoot整合易冲突。本项目采用轻量级状态机模式,用3张表+2个校验点,实现可读、可测、可调试的审批逻辑。
3.1 三张表构成状态机骨架
| 表名 | 核心字段 | 作用 | 关键约束 |
|---|---|---|---|
apply_form | id,user_id,department_id,status,create_time,update_time | 申报主实体 | status为ENUM('draft','submitted','reviewing','approved','rejected'),NOT NULL |
approval_record | id,form_id,approver_id,status,comment,approve_time | 每次审批动作留痕 | form_id外键关联apply_form.id,status同主表枚举 |
approval_config | id,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→ADMIN,rejected→DEAN等映射预加载。这样避免每次查库,也防止SQL注入式状态篡改(如前端传status=approved但当前是学生角色)。
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`) -- ← 唯一索引防重复 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;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-upload=true,会立即发请求。但申报表单有多个字段(文本、日期、文件),必须等所有字段校验通过后再上传。错误写法:
<!-- ❌ 错误:auto-upload导致文件提前上传 --> <el-upload action="/api/file/upload" auto-upload> <el-button size="small" type="primary">点击上传</el-button> </el-upload>正确写法(手动触发):
<!-- ✅ 正确:绑定ref,submit时调用upload() --> <el-upload ref="uploadRef" :action="'/api/file/upload?id=' + formData.id" <!-- 动态传表单ID --> :http-request="handleUpload" :on-success="handleUploadSuccess" :on-error="handleUploadError" :show-file-list="false" > <el-button size="small" type="primary">上传附件</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-list=false避免Element默认UI干扰。
4.2 SpringBoot文件存储:本地路径安全与磁盘空间预警
后端接收文件后,不存数据库Blob,而存本地磁盘(/uploads/),数据库只存相对路径。但直接file.transferTo(new File(path))有风险:
@PostMapping("/upload") public Result<String> 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> <groupId>com.itextpdf</groupId> <artifactId>itext7-core</artifactId> <version>7.1.15</version> <type>pom</type> <scope>compile</scope> </dependency> <!-- 中文字体支持 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>font-asian</artifactId> <version>7.1.15</version> </dependency>Java导出核心代码:
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_server=utf8mb4 - 数据库:
SHOW CREATE DATABASE school_approval;确保DEFAULT CHARSET=utf8mb4 - 表:
SHOW CREATE TABLE apply_form;确保DEFAULT CHARSET=utf8mb4 application.yml:url必须含?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
提示:
serverTimezone=Asia/Shanghai解决时区问题,避免create_time存成0000-00-00。
5.3 现象:Vue页面刷新后登录状态丢失,提示“请先登录”
原因:JWT Token过期时间(jwt.expiration=7200)短于浏览器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-size=1MB,超出即拒绝。
根治方案:在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返回1(admin用户)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" \ # macOS,Windows用gdigrab -f x11grab -video_size 1920x1080 -i :0.0+0,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_362") - MySQL版本(
mysql Ver 8.0.33) - Node.js版本(
v16.20.2) - 5条必须执行的命令(含数据库导入、后端启动、前端启动、初始账号、验证URL) 没有解释,没有“建议”,只有可复制粘贴的命令。因为答辩老师可能只给你3分钟看部署过程。
习惯2:数据库SQL脚本里加一行-- VERSION: 20240520
在init.sql第一行写明版本时间戳。这样当多个同学共享代码时,我能一眼看出谁用了旧版SQL(比如漏了file_attachment表)。Git提交时,SQL脚本的修改必须同步更新这个时间戳——它是我判断“数据结构是否一致”的唯一依据。
最后说一句:这个系统能拿高分,不是因为它用了多少新技术,而是因为每个技术点都选择了最稳的解法,并把边界条件写进了代码注释里。比如JwtUtil.java里写着// 注意:secret长度必须>=32字符,否则HS256算法会抛异常;比如ApplyService.java里写着// TODO:未来可接入短信网关,当前用print模拟。这些细节,才是评委在你离开后,还会翻看你代码的原因。
希望帮到你。
本文还有配套的精品资源,点击获取