基于SpringBoot+Vue的入校申报审批系统设计与实现
简介本资源是一套面向计算机专业本科生的高质量毕业设计实战项目聚焦高校场景下的入校申报与线上审批业务流程采用SpringBoot后端Vue前端的主流前后端分离架构适用于毕业设计选题、课程设计及期末综合实训。压缩包共367个文件涵盖83个Java核心业务与控制层代码、32个Vue组件与页面逻辑、161个SVG图标资源、10个XML配置及SQL数据库脚本等完整支撑系统开发、部署与演示全流程整体大小28.54MB结构清晰含bat一键部署脚本、样式与静态资源、多格式图片及3个MP4演示视频。目前已有82人学习下载资源已通过导师指导并获97分高分答辩评价在Windows 10/11环境实测可直接运行配套使用文档详尽覆盖环境搭建、数据库导入、前后端启动及功能操作说明是兼具工程规范性与教学实用性的优质参考范例。1. 为什么选“入校申报审批”做毕业设计选题逻辑与业务价值每年毕业设计选题季Java方向最不缺的就是“XX管理系统”图书馆管理系统、学生管理系统、超市收银系统……这类题目不能说错但太容易撞车答辩老师一眼就能看出来你是从某个课程设计模板改的。我当时选“入校申报审批系统”核心原因只有一个它有一个真实存在的业务痛点而且这个痛点足够复杂能同时撑起业务设计、权限模型、流程状态管理这些毕业设计必须展示的能力点。你可以把“入校申报审批”理解成一个轻量级的访客管理场景。学校里每天都有外来人员要进校学生家长、快递员、维修工、合作企业人员、参加学术会议的嘉宾。以前靠门卫登记效率低、信息不透明、审批无痕迹现在普遍的做法是线上申报、审批、门卫核验放行。这套逻辑和很多企业里的“访客预约系统”“工单审批系统”本质上是同一个内核你把这个项目做熟了以后面试聊到工作流、权限管理、前后端分离都有话说。从毕业设计的评分角度看这个题目有几个天然优势角色清晰访客申报人、审批人辅导员/部门负责人、保卫处/门卫核验、系统管理员天然适合做多角色权限管理。流程完整从提交申请到审批通过/驳回再到入校核验、离校登记甚至黑名单管理状态流转非常清晰。技术栈覆盖面全SpringBoot负责接口和业务逻辑Vue负责前端交互MySQL存数据Redis可以做缓存和token管理整个链路完整。应用场景有说服力答辩时老师问“你这个系统有什么用”你不需要编真实场景就摆在那里。这篇文章我会把整个项目的设计思路、核心模块实现、数据库设计、部署步骤、答辩要点全部复盘一遍。无论你是打算照这个题目做还是想借鉴它的思路做其他审批类系统都能直接抄作业。源码和数据库我都会给出关键部分完整版文档和演示视频配合着看会更清楚。2. 需求边界是如何敲定的用户角色、功能清单与状态流转动手写代码之前最关键的步骤是定需求边界。很多同学一上来就建表、写接口做到一半发现功能对不上返工成本极高。我建议你先把自己当成产品经理把所有角色和操作在纸上画一遍再开始编码。2.1 三种核心角色与职责切分这个系统我最终设计了四类角色但核心业务角色是三个申报人、审批人、门卫/核验人。角色核心职责典型用户申报人提交入校申请、查看审批进度、在有效期内入校校外访客、学生家长、外来人员审批人审核申报信息、通过/驳回、填写审批意见辅导员、院系负责人核验人扫码或输入审批编号核验访客身份、登记离校保卫处、门卫系统管理员用户管理、角色配置、部门配置、数据统计信息中心老师注意申报人这个角色比较特殊。如果是校外人员自己申报他得先注册账号如果是校内老师代校外人员申报那申报人就是校内老师本人。我在系统里同时支持了这两种模式注册时通过邀请码区分校内人员和校外人员这样设计出来功能更完整答辩时有东西可讲。2.2 功能清单哪些功能是必须做的哪些是加分项说实话毕业设计不需要做得多花哨但核心功能必须闭环。我最终敲定的功能清单分了三层第一层基础必备登录注册含验证码入校申报填写访客姓名、手机号、身份证号、事由、来访部门、预计入校时间、预计离校时间、车辆信息选填审批管理审批人查看待办列表、通过/驳回、填写意见入校核验核验人输入审批编号或扫描二维码查看申请详情确认放行我的申请申报人查看自己的申请记录与状态第二层提升完整度通知提醒审批结果通过站内信通知申报人黑名单管理恶意申报人员可被加入黑名单数据统计按日/周/月统计入校人数、审批通过率用户管理管理员可重置密码、禁用账号、分配角色第三层加分项二维码核验审批通过后生成二维码门卫扫码核验审批意见多级流转可选如有需要可扩展为多级审批Excel导入导出管理员可导出当日入校名单操作日志记录关键操作方便审计我的建议是第一层做稳第二层选做2-3个第三层如果时间充裕可以挑一个实现。我当时做了Web端完整功能二维码是通过前端生成、门卫手动输入编号核验也支持这样即使没有打印机也不影响演示。2.3 审批状态机的设计从“待审批”到“已失效”这个系统最核心的难点不是CRUD而是审批状态的管理。我在设计阶段专门画了一张状态流转表每个状态的转换条件、触发动作都写清楚这样后面写业务逻辑的时候不会漏判。状态含义可流转到触发条件PENDING待审批APPROVED / REJECTED / CANCELLED提交申报后进入审批人通过/驳回申报人主动取消APPROVED审批通过CHECKED_IN / EXPIRED / REVOKED访客入校核验超过有效期未入校管理员撤销REJECTED审批驳回无可重新申报审批人驳回需重新提交CHECKED_IN已入校CHECKED_OUT门卫核验放行后CHECKED_OUT已离校终态门卫登记离校CANCELLED已取消终态申报人主动取消EXPIRED已失效终态超过最后入校时间未核验REVOKED已撤销终态管理员/审批人撤销已通过的申请实际开发时这个状态字段我建议用字符串常量维护不要用魔法值到处写。在Java里建一个枚举类比如ApprovalStatusEnum每个状态定义code和desc这样前端拿到的是英文code展示的时候再映射成中文逻辑清晰也不容易出现脏数据。提示状态机设计是答辩时的高频提问点。老师会问你“如果审批通过后访客没来怎么办”“如果核验时发现信息不符怎么办”。提前想好这些边界场景答辩会从容很多。3. 技术选型与项目骨架SpringBoot加Vue这套组合为什么顺手毕业设计的技术选型核心原则是“主流、稳定、能讲清原理”。SpringBoot加Vue是当前JavaWeb方向最主流的组合网上资料多遇到问题查得到老师也认可。下面说说我的选型理由和版本选择。3.1 后端技术栈SpringBoot为主体MyBatis-Plus省去大量样板代码后端我用的核心组件如下组件版本/方案用途JDK1.8或11运行环境SpringBoot2.7.x项目基础框架MyBatis-Plus3.5.xORM框架简化单表CRUDMySQL5.7 / 8.0主数据库Redis5.x / 6.x选配token缓存、验证码存储JWTjjwt 0.9.x / 0.11.x无状态登录认证Hutool5.8.x工具类库比如验证码生成Lombok最新稳定版简化实体类代码SpringBoot选2.7.x而不是3.x原因很简单3.x要求JDK17起步虽然也成熟了但很多教学资料和毕业设计模板还在用2.x而且对新手来说SpringBoot 2.7的配置信息更丰富踩坑少。如果你对SpringBoot 3很熟那用3.x也没问题但没必要在毕业设计里增加额外变量。MyBatis-Plus是国产ORM框架它的亮点是单表CRUD不用写SQL通过BaseMapper直接继承方法就能完成增删改查。这对毕业设计来说非常省时间可以把精力放在核心业务逻辑上。多表关联查询还是老老实实写XML里的SQL可控性更强。3.2 前端技术栈Vue2还是Vue3前端我用了Vue2。不是Vue3不好而是考虑到毕业设计的实际情况Vue3的Composition API对新手来说上手门槛略高Vue2的Options API更直观而且Element UIVue2配套组件库的中文文档和案例非常多遇到样式问题一搜就有答案。前端组件版本/方案用途Vue2.6.x前端框架Vue Router3.x前端路由Vuex3.x状态管理Element UI2.15.xUI组件库Axios1.xHTTP请求库ECharts5.x数据统计图表qrcode前端库生成二维码如果你现在才开始动手且对前端有一定基础直接用Vue3加Element Plus也可以。两者的核心思路差别没有想象中大关键是路由、状态管理、组件通信这套逻辑要打通。3.3 项目结构前后端分离如何组织代码前后端分离的项目我建议在Git仓库里分两个子目录而不是混在一起school-entry-system/ ├── backend/ # SpringBoot后端 │ ├── src/main/java/com/example/entry/ │ │ ├── controller/ # 控制层处理HTTP请求 │ │ ├── service/ # 业务逻辑层核心代码 │ │ ├── mapper/ # 数据访问层MyBatis-Plus接口 │ │ ├── entity/ # 实体类对应数据库表 │ │ ├── dto/ # 前端传参对象接收和校验参数 │ │ ├── vo/ # 返回给前端的视图对象 │ │ ├── config/ # 配置类跨域、拦截器、Redis等 │ │ ├── common/ # 统一返回结果、异常处理、常量定义 │ │ └── utils/ # 工具类JWT、日期等 │ └── src/main/resources/ │ ├── mapper/ # MyBatis-Plus XML文件位置 │ ├── application.yml # 配置文件 │ └── sql/ # 建表脚本最好放在这 └── frontend/ # Vue前端 ├── src/ │ ├── api/ # 按模块封装的接口请求 │ ├── router/ # 路由配置 │ ├── store/ # Vuex状态管理 │ ├── views/ # 页面组件 │ ├── components/ # 公共组件 │ └── utils/ # 请求封装、工具函数 └── package.json后端分层我坚持用最经典的Controller-Service-Mapper三层架构。Controller只管接收参数和返回结果业务逻辑全部放Service这样代码可读性好答辩时讲起来也清楚。DTO和VO分开是我比较推荐的做法有人觉得麻烦但项目稍微大一点直接用Entity接收前端参数容易出现字段暴露问题而且返回给前端时也会把密码等敏感字段带出去。4. 后台核心模块的实现要点SpringBoot侧最需要下功夫的几处后端代码里真正值得花时间打磨的有这么几块登录认证与权限控制、入校申报的核心业务流程、审批的逻辑闭环、以及定时任务处理过期数据。下面逐个讲。4.1 登录认证与权限控制JWT配拦截器状态由Redis控制登录方案我用了JWT加拦截器的方式流程是这样用户输入用户名、密码、验证码后端校验通过后生成一个tokentoken里包含用户ID、角色、过期时间。后端把token返回给前端前端存在localStorage里每次请求在header里带上Authorization: Bearer {token}。后端写一个拦截器拦截所有需要登录的接口从header里取出token解析校验通过后放行。权限控制上在自定义注解里标注需要的角色然后在拦截器中校验当前用户是否包含该角色。JWT的好处是无状态服务器不需要存session适合前后端分离。但要注意一个问题JWT一旦签发在过期之前是无法主动失效的。所以我把token的时间设置短一点比如2小时同时用Redis存了一个login:token:userId的键值对。用户修改密码或管理员禁用账号时把这个键删掉下次请求就会在拦截器里发现token和Redis对不上强制重新登录。核心拦截器大致长这样Component public class JwtInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate stringRedisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果是预检请求OPTIONS直接放行 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseJWT(token); String userId claims.getSubject(); // 校验Redis中是否存在有效登录态 String redisKey login:token: userId; String redisToken stringRedisTemplate.opsForValue().get(redisKey); if (token.equals(redisToken)) { // 把用户信息放入ThreadLocal方便后续使用 UserContext.set(userId, claims.get(role, String.class)); return true; } } catch (Exception e) { log.error(token校验失败, e); } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write(JSONUtil.toJsonStr(Result.error(401, 未登录或登录已过期))); return false; } }这里我把解析出来的User对象放到了ThreadLocal里这样后续任何Service里都能通过UserContext.getUserId()拿到当前登录用户不用每个接口都从参数传一遍用户ID代码干净很多。实战心得很多教程里的拦截器只校验JWT是否合法但没有校验用户是否还在有效状态。毕业设计里把“登录态主动失效”这个细节做出来属于很容易被答辩老师认可的设计点。4.2 入校申报的核心业务逻辑一次申请的完整生命周期入校申报接口是系统的核心它做的事情比较多但每一步逻辑都不复杂public void submitApplication(ApplicationCreateDTO dto, Long userId) { // 1. 参数校验手机号格式、时间是否合法、必填字段 // 2. 检查是否存在黑名单记录 // 3. 组装实体状态设为PENDING // 4. 保存申请记录 // 5. 分配审批人根据所选来访部门找到该部门下审批角色用户 // 6. 生成一条审批任务记录approval_record // 7. 给审批人发一条站内信通知 }第5步“找到审批人”我最初设计得比较简单就是查部门表里roleAPPROVER的用户。后来发现现实场景里一个部门可能有多个审批人那就有“一人审还是所有人审”的问题。我最终用了“其中一个审批人审批即可”也就是查到部门下所有审批人后选第一个状态正常的。如果你有时间也可以做成“抢单式”的所有审批人都能看到这条待办谁先处理就算谁的。这里我给一个建议申请信息里的来访事由一定要用下拉框加文本补充的模式比如“公务会议”“探亲访友”“维修施工“三个选项选“其他”时必须填写备注。这种设计让数据在后续统计时能够结构化也是体现你做过真实需求分析的一个细节。审批通过后系统会生成一个六位数的审批编号这个编号就是门卫核验时输入的那个码。我的生成逻辑是“日期加随机数”比如20240520123456实际上用的是时间戳后六位加随机数保证同一天内不重复即可。同时把审批通过的时间、最后允许入校的时间都算好方便后续状态判断。4.3 审批环节的设计通过、驳回、撤销和黑名单审批人端的核心接口有三个approve(approvalId, applicationId, comment)通过申请。reject(approvalId, applicationId, comment)驳回申请。revoke(applicationId, comment)撤销已通过的申请比如发现信息虚假。驳回的时候有个小坑要处理同一张申请单可能被多数人审批过。如果某个申请被第一个审批人通过后第二个审批人还能不能操作我最终约定一个申请只要有一个审批人通过就算通过后面的审批人只能查看不能操作如果第一个审批人驳回了整个流程直接结束申报人可以修改后重新提交。这个逻辑听着简单但代码里容易漏掉“状态判断”。比如申报人A撤销申请的同时审批人B正在点通过按钮数据库更新时就要用乐观锁或者状态作为更新条件来防止脏操作。MyBatis-Plus的Version注解能做乐观锁我实际用的时候是在更新语句里加AND status PENDING来判断这样即使并发也能保证状态正确的记录才会被更新。黑名单的逻辑我放在了申报提交时校验也在审批时做了二次校验。如果某个手机号或身份证号被加入黑名单系统直接拦截不允许提交如果已经提交的申请审核过程中发现该访客有不良记录审批人可以一键拉黑并驳回申请。黑名单记录里我存了拉黑原因和操作人方便审计。4.4 通知消息与过期数据清理通知消息我用了最简单可靠的站内信方案没有接短信平台。每次关键动作发生后都会写入一条notification记录包括申报提交成功通知申报人“您的入校申请已提交等待审批”。审批结果出来通知申报人“审批已通过审批编号为xxx”或“审批被驳回原因xxx”。撤销申请通知相关审批人“申请已由申报人撤销”。前端在顶部导航栏上显示未读消息数量通过轮询接口获取。这个功能不复杂但能让系统的完整度拉高一个档次。过期数据清理我用SpringBoot自带的任务调度搞定Component public class ApplicationSchedule { Autowired private ApplicationService applicationService; Scheduled(cron 0 0/30 * * * ?) // 每30分钟执行一次 public void expireApplications() { // 找到所有状态为APPROVED且最后入校时间小于当前时间的申请 // 批量更新状态为EXPIRED applicationService.expireApprovedApplications(); } }一开始我没加定时任务演示的时候发现数据库里有很多过了时间还是“审批通过”状态的申请数据很乱。加了定时任务后整体状态就干净了。而且每当系统有状态更新时都尽量生成一条日志记录哪怕只是简单的文本日志审计的时候非常有用。5. 前端交互的关键实现Vue侧怎么做到好用又完整前端这块最容易出问题的不是页面写不出来而是接口对接时的数据结构一致性、路由权限控制、页面跳转后状态同步。下面讲几个让我花过不少时间的点。5.1 路由与权限控制不同角色进入不同菜单前端路由我分了两种公共路由登录页、注册页。业务路由根据角色懒加载。每次登录成功后后端返回当前用户的角色和菜单权限列表前端根据这些数据动态拼装路由然后用router.addRoutes()注入。比如申报人角色看到的菜单是“我的申报、新建申请、通知消息”审批人看到的是“审批任务、申请记录本部门的、数据统计”管理员的菜单则包含用户管理、部门管理、黑名单管理、系统日志。具体实现上路由表我维护了一个常量路由数组constantRoutes然后动态生成的角色路由数组dynamicRoutes存在Vuex里。页面刷新时Vuex数据会丢失所以我做了持久化把路由数据存到sessionStorage里刷新时重新取。一个小细节按钮级权限。同样一个“撤销申请”按钮只有申请人和审批人可见如果角色不对就算前端显示出来了后端接口也会校验权限。前端权限控制只是提升体验真正的安全防线还是后端接口的角色拦截。这个原则一定要在脑子里刻清楚。5.2 axios封装统一错误处理与token携带axios我封装了一个请求实例所有页面都通过统一的request.js发请求。封装做的事很简单请求拦截器从localStorage取出token放到header里。响应拦截器判断响应码如果401表示未登录或登录过期清除本地登录态跳转登录页如果500或业务错误码统一弹出错误提示。service.interceptors.response.use( (response) { const res response.data if (res.code 200) { return res } if (res.code 401) { // 清除登录态并跳转登录页 localStorage.removeItem(token) localStorage.removeItem(userInfo) window.location.href /login return Promise.reject(new Error(未登录或登录已过期)) } Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message || 请求失败)) }, (error) { Message.error(error.message || 网络异常) return Promise.reject(error) } )我踩过一个坑后端接口返回体有多层嵌套时前端经常忘了判断最外层的code直接取data导致空指针。所以统一封装好拦截器所有接口只要处理data即可省了很多无意义的重复代码。5.3 新建申报表单的时间处理与校验新建申报页是最容易出bug的页面。表单字段多尤其是时间字段。我的做法是预计入校时间、预计离校时间用Element UI的日期时间选择器限制只能选未来时间。入校时间必须早于离校时间提交前一起校验。如果填了车牌号自动加上车牌号格式校验比如[A-Z][0-9A-Z]{5,6}。手机号用运营商号段正则校验但也要做后端校验不能只依赖前端。前端校验是体验后端校验是安全。这个原则我在代码注释里都写了答辩讲到这块时很有话料。提交成功后页面跳转到“我的申请”列表页列表数据从接口拉取最新状态。列表页我用了Element UI的table组件加了一个“状态”列用tag标签展示不同颜色绿色为通过红色为驳回灰色为失效让状态一目了然。5.4 审批任务页与数据看板审批人的任务页用了两个tab待办和已办。待办列表展示所有PENDING状态的申请点“审批详情”进入弹窗查看申请完整信息然后点“通过”或“驳回”必须填写审批意见才允许提交。已办列表展示该审批人处理过的历史记录支持按日期筛选。数据看板用了ECharts展示三类图每日入校人数折线图、审批通过率环形图、访问事由分布柱状图。数据接口由后端聚合查询返回前端直接渲染。ECharts组件化的时候我遇到过一个坑数据还没加载完成就开始渲染图表导致图表空白。解法是在this.$nextTick之后初始化或者用v-if保证容器处于显示状态。另一个小坑下拉框选项值类型不一致。后端返回的status字段是字符串APPROVED前端比较时不小心用了 approved查了半天才发现大小写不一致。这种问题我建议前后端约定所有状态值统一用大写字符串避免混乱。6. 数据库怎么设计数据表、字段与索引的一次复盘数据库设计直接决定系统上线的数据质量。我从第一版表结构到最终版改了三轮第三轮才算比较稳定。下面分享最终的表设计和一些设计时的思考。6.1 核心数据表清单表名说明核心字段sys_user用户表id, username, password, real_name, phone, role, dept_id, statussys_dept部门表id, name, parent_idvisit_application入校申报表id, applicant_id, visitor_name, visitor_phone, visitor_id_card, visit_reason, visit_dept_id, expected_in_time, expected_out_time, plate_number, status, approval_code, created_timeapproval_record审批记录表id, application_id, approver_id, opinion, status(PENDING/APPROVED/REJECTED), handled_timenotification通知消息表id, user_id, title, content, is_read, created_timeblacklist黑名单表id, name, phone, id_card, reason, operator_id, created_timesys_log操作日志表id, user_id, action, detail, created_time6.2 关键表字段设计的思考visit_application表的 status 字段我用了varchar存储字符串状态没有用int。原因是可以直接在数据库里看清数据排查问题方便。当然int加注释也可以但字符串状态配合枚举类代码和数据的可读性都更好。visitor_id_card身份证号字段加密存储吗真实生产环境肯定要加密毕业设计阶段我建议至少做脱敏展示。可以在查询时用SQL里的CONCAT(LEFT(id_card, 3), ********, RIGHT(id_card, 4))把中间部分遮挡掉避免敏感信息直接暴露在页面上。这是个细节分。approval_code审批编号唯一索引必须加。门上扫码核验时是根据这个编号精确查找的不加唯一索引可能出现重复编号导致查询返回多条。索引设计访问最频繁的查询是“按用户查申请列表”和“按状态查待办列表”。所以我在visit_application表上建了三个索引ALTER TABLE visit_application ADD INDEX idx_applicant (applicant_id); ALTER TABLE visit_application ADD INDEX idx_status (status); ALTER TABLE visit_application ADD INDEX idx_expected_in_time (expected_in_time);申请记录表按application_id建索引通知表按user_id和is_read建联合索引。索引不用建太多够用就行。6.3 建表SQL示例CREATE TABLE visit_application ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, applicant_id bigint(20) NOT NULL COMMENT 申报人用户ID, visitor_name varchar(50) NOT NULL COMMENT 访客姓名, visitor_phone varchar(20) NOT NULL COMMENT 访客手机号, visitor_id_card varchar(32) DEFAULT NULL COMMENT 访客身份证号, visit_reason varchar(100) NOT NULL COMMENT 来访事由, visit_dept_id bigint(20) NOT NULL COMMENT 来访部门ID, expected_in_time datetime NOT NULL COMMENT 预计入校时间, expected_out_time datetime NOT NULL COMMENT 预计离校时间, plate_number varchar(20) DEFAULT NULL COMMENT 车牌号选填, status varchar(20) NOT NULL DEFAULT PENDING COMMENT 状态, approval_code varchar(32) DEFAULT NULL COMMENT 审批编号, remark varchar(500) DEFAULT NULL COMMENT 备注, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_approval_code (approval_code), KEY idx_applicant (applicant_id), KEY idx_status (status), KEY idx_expected_in_time (expected_in_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入校申报表;updated_time字段配合MyBatis-Plus的自动填充功能更新记录时不用手动维护非常省心。MyBatis-Plus里配一个MetaObjectHandler就行Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createdTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updatedTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updatedTime, LocalDateTime.class, LocalDateTime.now()); } }提示数据库表用utf8mb4字符集不要用utf8。utf8mb4是utf8的超集能存emoji和生僻字现在的新项目默认都用utf8mb4。如果建表时用了utf8后面入库带表情符号时很容易报错。7. 本地跑通这个项目的操作清单及常见坑这部分写给拿到源码后想自己跑起来的同学。我假设你用的是Windows系统MySQL和Redis已经安装完成Node环境也配好了。7.1 基础环境准备后端需要的工具JDK 1.8或11到官网下载安装后配置JAVA_HOME环境变量。Maven 3.6下载解压后配置MAVEN_HOME并把bin目录加到path里。国内使用建议配置阿里云镜像否则依赖下载速度慢到怀疑人生。MySQL 5.7或8.0安装后设置root密码注意MySQL 8.0的密码加密方式默认是caching_sha2_password如果连不上可能要改回mysql_native_password。Redis可选如果不装Redis可以把登录token的Redis校验关闭或者改用内存存储但最好还是装上。Windows版Redis可以通过微软的Redis镜像或Memurai获取也可以使用WSL方式跑Linux版选一个自己顺手的。前端需要的工具Node.js 12/14/16皆可npm自带。Vue CLI如果使用Vue2脚手架。7.2 后端的启动步骤用IDEA打开backend目录等待Maven下载依赖完成。修改application.yml里的数据库连接信息、Redis连接信息。在MySQL中创建数据库school_entry执行项目里的sql/init.sql建表脚本并初始化管理员账号和部门数据。启动项目控制台出现“Started Application in xxxx seconds”说明启动成功。用Postman或浏览器访问/api/user/login使用管理员账号测试登录接口。常见问题排查MySQL连接报错Public Key Retrieval is not allowed在JDBC URL里加allowPublicKeyRetrievaltrueuseSSLfalse。端口被占用SpringBoot默认8080被其他程序占用了可以在application.yml里改成8090。Redis连接失败确认Redis服务是否启动检查密码配置是否一致。如果不用Redis把登录校验逻辑改为不过期即可先跑通。7.3 前端的启动步骤用VSCode或WebStorm打开frontend目录。执行npm install安装依赖。如果网速慢可以设置淘宝镜像npm config set registry https://registry.npmmirror.com。修改src/utils/request.js里的baseURL改成后端实际地址比如http://localhost:8080/api。执行npm run dev看到编译成功提示后浏览器访问http://localhost:8081或终端提示的地址。使用初始账号登录走一遍“新建申请-审批-核验”的完整流程。前端常见问题跨域报错后端已经配置了CORS过滤器但如果配置不对前端请求还是会被拦截。推荐在后端写一个CorsConfig配置类允许所有来源和常用方法前端不用额外配代理。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }npm install时node-sass报错node-sass是个老坑如果项目里用了node-sass建议改成了sassdart-sass安装稳定很多。新版Vue CLI创建的项目默认用的就是sass。7.4 演示视频里的一个亮点操作一条完整业务流的演示路径如果你要录演示视频我强烈建议按下面的路径走逻辑最顺也最能展示系统亮点用管理员账号登录创建两个用户一个审批人、一个访客。退出登录用访客账号注册或登录提交一条入校申请。退出登录用审批人账号登录看到待办提醒查看申请详情后通过。退出登录用访客账号登录看到审批通过的通知查看审批编号。用门卫/核验人账号登录输入审批编号核实访客信息后点击“确认入校”。到数据统计页展示今日入校人数和审批通过率演示就非常完整了。8. 项目答辩时可能被追问的几个问题答辩时间有限老师不会让你把项目从头演示一遍更多是盯着你“有没有真正理解自己做的系统”。下面这几个问题我遇到过也帮很多学弟学妹模拟过答辩提前准备好答案现场会从容很多。8.1 为什么用JWT不用session标准答案思路前后端分离架构下前端和后端可能部署在不同域名/端口session依赖cookie传递跨域场景下cookie处理比较麻烦JWT是无状态的服务端不需要保存会话信息天然适合分布式部署同时JWT携带用户身份信息方便做权限校验。缺点是需要自己处理过期和主动失效问题我在项目里通过Redis辅助解决。这个回答既说了优点也主动说了缺点和解决方案老师会认为你真的在实践里摸过。8.2 如果同一时间有大量访客申请入校系统如何优化这个问题考查的是你对系统性能的思考深度。可以从几个层面回答数据库层面列表查询页做了分页并对常用查询条件加了索引。缓存层面审批人待办数量、部门列表等热点数据可以缓存到Redis减少数据库压力。接口层面大流量时把申报提交接口做成异步处理先返回“提交成功”后台再处理审批任务分配。部署层面如果条件允许前端用Nginx部署静态资源后端多实例部署用Nginx负载均衡。不一定真做了但能把思路说出来老师就知道你有扩展意识。8.3 审批流程能不能扩展成多级审批我设计时没有做多级审批但状态机里预留了扩展空间。如果把approval_record表增加level字段每个申请关联多个审批节点按照顺序依次审批就能做成一串审批链。这里只需要把“通过”逻辑从“直接变成APPROVED”改成“跳到下一级审批节点”。我的建议是不要先动手做先把思路讲清楚老师如果追问再展开。8.4 你如何理解前后端分离不要背概念用自己的经历说。比如开发时我一个人同时写前后端所以深有体会前后端分离本质上是“职责分离”后端专注提供数据接口前端专注展示和交互两者通过JSON交换数据约定好接口文档或在线文档工具比如Swagger就行好处是并行开发效率高一台后端服务可以同时支撑多个客户端Web端、小程序端。我自己在项目里用Swagger生成接口文档省去了很多口头沟通成本。8.5 为什么选MyBatis-Plus而不用原生MyBatisMyBatis-Plus在单表CRUD场景下可以少写大量重复SQL它帮你把selectById、selectList、insert这些通用方法都做好了你只需要在Mapper接口里继承BaseMapperT就行。项目里复杂查询还是自己写XML里的SQL保留了对SQL的完全控制。这样既高效又能保证复杂场景的灵活性。这样的回答既展示了你了解工具的便利性也说明你没被框架限制住。9. 写在最后做毕业设计的一点心得这套系统从需求调研到开发完成前后花了我大概三周多的时间。现在回头复盘最深的体会是毕业设计能不能拿高分靠的不是炫技而是“闭环感”和“细节度”。一个申请从提交、审批、核验、离校到统计整个流程能跑通每个关键节点都有数据留痕这种完整度远比某个页面做得花哨更重要。给你几个实操层面的建议自己写代码之前先手绘状态流转图和数据表关系图哪怕画得难看也能帮你理清思路。所有接口统一返回结果对象前后端联调时省心很多。每个状态字段都写清楚注释一个月后你再回来看代码会感谢当时的自己。答辩前完整走一遍业务流最好把每个关键页面的截图存下来方便做PPT。如果时间紧张优先保证核心流程不出bug再做Excel导入导出等加分功能。我在实际开发中还发现一个容易翻车的问题赶工时容易忽略异常处理。比如审批人并发处理同一张申请单如果没有做状态校验就会出现两个人同时批同一单的情况。这种问题平时不容易暴露但一旦演示时出现就很尴尬。建议所有更新操作都带上“当前状态”作为更新条件宁可多写两行判断也要保证状态流转的唯一性。最后分享一个小技巧做这种角色较多的系统一开始把测试账号密码固定好比如管理员账号admin / 123456审批人账号approver / 123456申报人账号applicant / 123456门卫账号guard / 123456演示时直接登录省去前面输入账号的时间。演示视频里重点展示的流程分清楚哪些是录制前准备好的数据哪些是现场操作的自己心里有数就行。如果你准备做这个题目别怕一开始思路乱按“角色—功能—状态—表结构—接口—页面”这个顺序一步步来稳扎稳打做完就是一套很能打的毕业设计。本文还有配套的精品资源点击获取