
1. 为什么这个技术栈成了毕设和中小型后台的“标配”先说个现象最近几年我帮人看的Java Web项目里大学生考勤系统、学生管理系统、仓库管理系统这类后台项目十套里有七八套都是SpringBoot Vue3 MyBatis-Plus MySQL这个组合。不是大家约好了而是这套组合确实解决了实际开发里最痛的那几个问题——后端配置繁琐、前端组件割裂、数据库操作重复、环境搭建踩坑。拿这个“Java Web大学生考勤系统”来说顶上挂的SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0四个词每一个都不是随便选的背后都有明确的对应价值点SpringBoot2负责把后端业务逻辑跑起来它最核心的价值是“约定大于配置”。不用像SSH时代那样写一堆XML配置文件一个启动类加上几个注解就能把Web服务拉起来这对课设、毕设这类时间紧、重点在业务逻辑的项目来说太重要了。Vue3负责前端交互相比Vue2它的组合式APIComposition API能把同一个功能的逻辑集中在一块儿写配合script setup语法代码量砍掉三分之一是很正常的。再加上Element Plus这套组件库表格、表单、弹窗、分页这些后台管理页面的标配组件开箱即用。MyBatis-Plus解决的是数据访问层的效率问题。它最大的卖点是“单表CRUD不用写SQL”内置的BaseMapper直接帮你把增删改查、分页查询、条件构造器都封装好了你只需要写业务里那些真正复杂一点的多表联查SQL。MySQL8.0是存储层它相比5.7在窗口函数、JSON支持、字符集排序规则上都有明显改进而且现在新装的数据库基本都是8.0起步不存在老版本迁移的包袱。这个系统的目标场景很明确高校里老师要记录学生考勤传统方式是纸质点名或者Excel登记统计缺勤率、迟到次数、请假记录都靠人工效率低还容易出错。做成Web系统之后学生打卡、教师登记、辅导员按班级查看统计报表整个流程就线上化了。适合谁来参考准备做毕业设计的在校生想快速搭一个后台管理系统的初级开发以及想了解这套技术栈如何组合在一起干活的人。接下来我按自己的实操经过把几个关键环节的“为什么”和“怎么做”拆开讲包括项目骨架怎么搭、数据表怎么设计、MyBatis-Plus怎么用得顺手、权限认证怎么处理、以及我踩过的那些环境坑。2. 项目骨架搭建Vue3初始化、后端分层与目录规划这一节讲的是拿到项目之后整个工程应该怎么组织。很多新手上来就写代码结果写到一半发现前端页面和后端接口互相找不到或者Controller里堆了太多业务逻辑后面改需求时痛不欲生。我这里给出一套项目的标准组织方式。2.1 前端Vue3初始化选Vite还是Vue CLI这是一个选择题但2024年之后基本没什么好纠结的——直接选Vite。Vite基于ES Module启动速度是秒级的开发时改代码热更新几毫秒就生效Vue CLI基于Webpack冷启动可能要等好几秒甚至十几秒改一次代码全量刷新一下在项目大一点之后体验差距非常明显。初始化命令很简单npm create vitelatest student-attendance-ui -- --template vue这一步会生成一个带src目录的最小工程。我建议进去之后先把目录按模块拆好不要所有组件都堆在src/components下面。完整的规划大概是这样的src/ |-- api/ // 接口请求封装按模块拆文件 | |-- auth.js | |-- student.js | |-- attendance.js |-- assets/ // 静态资源 |-- components/ // 通用组件 |-- router/ // 路由配置 |-- store/ // Pinia状态管理 |-- views/ // 页面级组件 | |-- dashboard/ | |-- attendance/ | |-- student/ | |-- user/ |-- utils/ // 工具函数如request.js |-- App.vue |-- main.jsapi目录按业务模块拆分文件每个文件里按功能导出一个个函数比如student.js里就是getStudentList(page, params)、addStudent(data)、updateStudent(id, data)。这样做的意义在于前端所有接口调用点可以统一管理、统一复用后端接口路径变了只需要改这一处。另外要装Element Plus、Pinia、Vue Router和Axiosnpm install element-plus pinia vue-router4 axiosElement Plus在main.js里全局注册一下就行新手阶段不用考虑按需引入的优化问题项目做完之后再回来做打包优化不迟。2.2 后端SpringBoot的分层结构后端我用的是标准的三层架构Controller控制层、Service服务层、Mapper数据访问层。用Maven或者Gradle建工程包结构规划如下com.example.attendance |-- controller/ // 接口入口只做参数接收和结果返回 |-- service/ // 业务逻辑层写具体规则 |-- mapper/ // 数据访问层继承BaseMapper |-- entity/ // 数据库实体类与表字段一一对应 |-- dto/ // 数据传输对象用于接收前端参数 |-- vo/ // 视图对象用于向前端返回数据 |-- config/ // 配置类如MyBatis-Plus插件配置、跨域配置 |-- common/ // 通用类如统一返回结果Result、全局异常处理器 |-- utils/ // 工具类如JWT工具、日期工具实体类entity和VO分开这个点值得展开说说。很多初学者喜欢把数据库实体直接返回给前端图省事但这样做有两个问题第一数据库表的字段不应全部暴露给前端比如密码、手机号、内部备注这类字段第二前端需要的字段往往是组合出来的比如“应到人数”“实到人数”“出勤率”这种统计值不是表里直接有的字段。用VO接收组装好的数据接口的返回结构才稳定可控。我习惯用一个统一的返回类形状是{ code: 200, message: success, data: {} }所有接口都返回这个结构配合一个全局异常处理器Controller里就专心做参数接收和调用Service业务代码里抛异常、返回错误信息的逻辑都统一收口前端处理响应时也只用判断code。2.3 版本选型的一个实际坑SpringBoot 2.7.x与3.xSpringBoot现在有2.x和3.x两条线标题里明确写了SpringBoot2。我给的建议是除非你想挑战新特性否则毕设老老实实用2.7.x。原因是3.x基于Jakarta EE规范、JDK最低要求17很多传统教程、现成代码片段都是基于javax命名空间的SpringBoot2写法你真去查资料、找解决方案的时候2.7的社区资料量完全碾压3.x。SpringBoot2.7.x配JDK8或JDK11都能跑配合MyBatis-Plus的mybatis-plus-boot-starter版本用3.5.x系列就很稳定。这个组合我实测过无数遍属于怎么折腾都不会翻车的搭配。3. 数据库设计与考勤核心业务表结构拆解数据库设计是这类系统最容易暴露问题的地方因为表没设计好后面写业务代码时处处拧巴。我拿考勤系统里最核心的几张表来拆解设计思路。3.1 基础五张表用户、学生、班级、课程、教师学生考勤系统的数据模型绕不开这几个主体谁学生、属于哪个集体班级、上什么课课程、记录谁教的教师。这五张表是基础骨架表结构大概是这样用户表(sys_user)字段名类型说明idbigint主键usernamevarchar(50)登录账号passwordvarchar(100)加密后的密码real_namevarchar(50)真实姓名rolevarchar(20)角色admin/teacher/studentcreate_timedatetime创建时间学生表(student)字段名类型说明idbigint主键user_idbigint关联sys_user表student_novarchar(20)学号namevarchar(50)姓名class_idbigint班级IDphonevarchar(20)手机号这里有个设计决策要讲清楚为什么学生表要单独存一份user_id关联登录账号而不是直接拿学号当登录账号因为一个学生可能同时是班干部未来可能还兼职助教角色和身份是会发生变化的把“登录身份”和“个人基础信息”拆开后面扩展权限体系会灵活很多。班级表(class)班级名称、所属专业、年级、导员ID关联教师用户。课程表(course)课程名称、课程编号、授课教师ID、上课时间、上课地点。教师表(teacher)教师工号、姓名、所属院系。3.2 考勤记录表与请假单表核心流程落点考勤系统的业务核心就落在这两张表上。考勤记录表(attendance_record)字段名类型说明idbigint主键student_idbigint学生IDcourse_idbigint课程IDclass_datedate上课日期statustinyint状态1正常 2迟到 3早退 4缺勤sign_timedatetime签到时间remarkvarchar(255)备注create_timedatetime记录创建时间状态我用数字枚举维护具体含义写死在代码的常量类或枚举里。不建议直接在数据库里存中文因为后续要统计、筛选排序时数字类型性能更好、代码可读性也更强。请假单表(leave_request)字段名类型说明idbigint主键student_idbigint学生IDcourse_idbigint课程IDreasonvarchar(255)请假原因start_timedatetime开始时间end_timedatetime结束时间statustinyint状态0待审批 1通过 2驳回create_timedatetime提交时间这两张表之间的业务联动是请假通过之后对应时间段内的考勤记录应该自动标记为“请假”或直接不生成“缺勤”记录。这个逻辑在Service层实现不要在数据库层面做物理外键约束——在实际项目里逻辑关联比物理外键好用得多物理外键会导致删除、更新时各种约束冲突尤其在课设里改数据改到怀疑人生。3.3 一个容易被忽视的字段设计细节统一使用逻辑删除我给所有业务表都预留了deleted字段类型tinyint0未删除、1已删除。配合MyBatis-Plus的TableLogic注解所有查询自动带deleted0条件删除操作也被拦截转换为更新操作。这样做的好处是数据不会真正消失追查历史记录、恢复误删数据都方便属于正规项目的标配习惯。对课设而言这个点写进报告里也是个很实用的加分项。3.4 考勤判定规则迟到早退边界怎么算判定规则是考勤系统的核心算法逻辑这里展示一下最简单的实现思路课程表里配了上课开始时间start_time和下课时间end_time。学生打卡时拿当前时间和课程开始时间比较打卡时间在开始时间之前状态为正常开始时间到开始时间后15分钟之间状态为迟到之后则为缺勤或者不允许打卡。学生签退时拿当前时间和下课时间比较小于下课时间则为早退。这个规则看着简单但实际做的时候要小心一点判断状态不能只靠一次打卡时间。比如学生既迟到又早退那到底算哪个状态我的处理方式是设优先级缺勤 迟到 早退 正常双异常取最严重状态。这种边界决策写清楚后后端接口逻辑就固定了不会出现前端展示与后端统计结果对不上的情况。4. MyBatis-Plus实战从BaseMapper到通用CRUD的取舍边界这一节是这篇博文的重点之一。既然标题里专门点名了MyBatis-Plus我把它能提速的点和我建议“不要偷懒”的点都讲一遍避免被它宠坏了遇到复杂场景反而不会写SQL了。4.1 BaseMapper与ServiceImpl单表操作为什么不需要写SQLMyBatis-Plus的核心爽点在于你的Mapper接口只要继承BaseMapperT就自动拥有了insert、deleteById、selectById、updateById、selectList、selectPage这批方法。Service层再继承ServiceImplMapper, Entity连接口和基础实现类都帮你造好了。以学生管理为例SA层的代码只需要public interface StudentService extends IServiceStudent { PageResultStudentVO pageQuery(StudentQueryDTO dto); } Service public class StudentServiceImpl extends ServiceImplStudentMapper, Student implements StudentService { // 复用ServiceImpl自带的 save、removeById、getById、page 等方法 }Controller里直接调用studentService.save(student)、studentService.page(new Page(pageNum, pageSize), wrapper)就能完成八成的基础功能。单表CRUD完全不碰SQL这对课设项目的开发速度提升是肉眼可见的。4.2 条件构造器LambdaQueryWrapper连筛选带排序一把梭多条件筛选是后台管理页面最常见的形式比如学生列表按班级筛选、按姓名模糊查询、按学号精确查询。MyBatis-Plus给的条件构造器可以把这些条件链式地拼起来LambdaQueryWrapperStudent wrapper Wrappers.lambdaQuery(); wrapper.eq(StringUtils.isNotBlank(dto.getClassName()), Student::getClassId, dto.getClassId()) .like(StringUtils.isNotBlank(dto.getName()), Student::getName, dto.getName()) .orderByDesc(Student::getCreateTime); IPageStudent page studentMapper.selectPage(new Page(pageNum, pageSize), wrapper);这里有个细节值得讲eq的第一个参数是布尔条件false时整条条件不拼接。用这种方式作筛选就不用自己手动拼SQL字符串也避免了SQL注入风险只有dto.getClassName()有值时才真正追加班级ID的过滤条件干净利落。另外注意条件构造器里所有字段都是用Student::getName这种Lambda表达式的写法这比写字符串name要安全因为如果实体类字段名改了而代码没改编译期就会报错不会拖到运行时才发现SQL拼错这一点对新手特别友好。4.3 分页插件与代码生成器省时间的两个利器分页插件。MyBatis-Plus做分页需要先配置一个分页插件不配的话selectPage传了页码也是全查出来自己切页效率差很多。配置代码是Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }配好之后selectPage(page, wrapper)会自动执行带LIMIT ? OFFSET ?的SQL返回的IPage对象带total总记录数一次搞定分页数据。代码生成器。MyBatis-Plus官方有个代码生成器插件mybatis-plus-generator配置好数据库连接和包名之后可以一键生成实体类、Mapper接口、Service、Controller整套模板代码。我建议你可以用它先生成一版地基代码但Controller和Service里生成的模板逻辑要改因为生成器生成的Controller是直接调用IService的业务校验和组合逻辑基本没有你要在后面自己补业务。生成器适合当“骨架生成工具”不适合当“业务生成器”。4.4 通用CRUD的边界多表联查必须手写SQLMyBatis-Plus好用归好用但一遇到多表联查它就不灵了。比如“查询最近一周每个班级的出勤率”这种需求需要班级表、学生表、考勤记录表三表关联还要按班级分组算状态占比。这个场景用MyBatis-Plus自带的Wrapper套不出来老老实实写XMLselect idselectAttendanceRate resultTypecom.example.attendance.vo.ClassAttendanceRateVO SELECT c.name AS className, COUNT(DISTINCT s.id) AS totalStudents, SUM(CASE WHEN ar.status 1 THEN 1 ELSE 0 END) AS normalCount, ROUND(SUM(CASE WHEN ar.status 1 THEN 1 ELSE 0 END) / COUNT(DISTINCT s.id) * 100, 2) AS rate FROM class c LEFT JOIN student s ON s.class_id c.id AND s.deleted 0 LEFT JOIN attendance_record ar ON ar.student_id s.id AND ar.class_date BETWEEN #{startDate} AND #{endDate} AND ar.deleted 0 WHERE c.deleted 0 GROUP BY c.id /selectMapper接口里对应写ListClassAttendanceRateVO selectAttendanceRate(Param(startDate) String startDate, Param(endDate) String endDate);这个查询里有一个点要专门跟新手说LEFT JOIN attendance_record后面的AND ar.class_date BETWEEN和AND ar.deleted 0为什么要写在JOIN的ON条件里而不是写在WHERE里因为写在WHERE里时如果某个班级在这段时间内一条考勤记录都没有LEFT JOIN的结果会出现右表字段全是NULL但WHERE ar.class_date BETWEEN ...会把NULL的行过滤掉导致那个班级整行消失最后统计结果少了一个班。放在ON里就不会它能保留左表的所有班级右表没有匹配就显示NULL之后再配合SUM和COUNT(DISTINCT)做统计结果才正确。这是我帮人排查报表数据时见过最多的一类错误。4.5 用MyBatis-Plus实现“无状态增删改查”的工具类设计标题相关的热搜词里有一条叫“通用CRUD服务基于MyBatis-Plus工具类实现无状态增删改查”这个点我顺便展开说下。所谓无状态增删改查本质上是抛开HttpSession那一套会话状态依赖每次请求通过参数或者Token识别操作对象Service层方法不依赖上下文状态同一个方法在任意时刻调用结果都是可预期的。实现方式就是定义一个泛型工具类public class CrudUtils { public static T boolean save(IServiceT service, T entity) { return service.save(entity); } public static T boolean update(IServiceT service, T entity) { return service.updateById(entity); } public static T boolean delete(IServiceT service, Serializable id) { return service.removeById(id); } public static T T getById(IServiceT service, Serializable id) { return service.getById(id); } public static T IPageT page(IServiceT service, PageT page, WrapperT wrapper) { return service.page(page, wrapper); } }这个封装类的好处是Controller层拿到对应Service后所有标准操作都是一行调用通用性极强。但在实际项目中我不建议所有实体都走这一个入口因为不同表的字段校验、状态流转、关联记录清理规则各不相同通用工具类的定位应该是处理那些真正无差别的操作比如根据主键查详情、根据主键删记录剩下的复杂业务还是得老老实实写对应Service方法。5. 权限认证与多角色访问控制JWT在考勤系统里的落地姿势考勤系统的用户有三种角色管理员、教师、学生。不同角色看到的菜单和能执行的操作完全不同这块涉及登录认证和权限控制是一个后台系统的安全红线也是答辩时老师最爱追问的部分。5.1 为什么不选Session而选JWT传统Java Web用HttpSession用户登录后SessionId存在Cookie里每次请求带上后端内存里维护Session。这个方案的问题在于后端多实例部署时Session不共享需要引入Redis做集中式Session管理而且Cookie受同源策略限制跨域请求时处理麻烦前后端分离模式下限制更多。JWT的方案是登录成功后后端签发一个Token里面带用户ID、用户名、角色这些声明信息用密钥签名防篡改。前端把Token存起来每次请求放进Header里后端只校验签名是否合法、是否过期完全无状态不需要在服务端额外存储会话数据。我用的依赖是jjwt的0.9.1版本工具类里两个核心方法public static String generateToken(UserLoginVO user) { return Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(role, user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); }Token有效期我设7天课设场景够了。密钥SECRET_KEY不要写在代码里写死最好放在配置文件或环境变量中答辩说说这点会很加分。5.2 前端路由守卫 后端拦截器双保险推荐的方式是前端和后端各做一道拦截不要只依赖一边。前端用Vue Router的路由守卫控制页面跳转未登录的跳登录页已登录但角色不对的跳无权限页看到的是导航菜单的动态渲染。核心逻辑是路由meta里配置roles比如{ path: /attendance/records, name: AttendanceRecords, component: () import(/views/attendance/AttendanceRecords.vue), meta: { roles: [admin, teacher] } }路由守卫里判断当前用户的角色是否在meta.roles里不在就next({ path: /401 })。后端用拦截器统一拦截除登录接口以外的所有请求取出Header里的Token解析校验再把用户信息放进ThreadLocal后续业务代码直接取当前用户不用每次传用户ID。这里有个容易忽略的坑白名单列表要维护好比如Swagger的API文档路径、静态资源路径、登录接口本身都得放行否则前端调登录接口会先被拦截器拦住导致死循环。5.3 教师端点名的推演一次完整的多角色交互拿教师录入学生考勤记录这个场景来串一下整个流程教师登录后选择今天上的课程前端调后端接口/api/course/mine后端根据JWT里的userId查出授课课程列表。教师选择某节课后调/api/attendance/course/{courseId}后端查出该课程有哪些学生返回一个学生名单列表前端渲染出点名表格。教师在表格里给每个学生选状态正常/迟到/缺勤/请假一次性提交POST /api/attendance/save-batch。后端批量入库注意要处理“同一学生同一课程同一天只能有一条考勤记录”的冲突检测防止重复提交。学生端登录后调/api/attendance/my按当前登录用户查全部考勤记录和统计信息展示个人出勤日历或列表。权限在这个流程里的体现是第3步的“点名学生名单”接口必须校验当前教师确实教授这个课程而不是随便传一个courseId就能拉到别人的名单。这种越权漏洞是管理员最不愿意看到的写接口时每一处ID传参都要自问一句当前用户有没有权利操作这个ID对应的数据。5.4 答辩必问的JWT三个问题Token被窃取了怎么办答设置较短的过期时间、使用HTTPS传输、后端维护一个退出登录时的Token黑名单或直接采用双Token刷新机制。毕设做双Token刷新有点重至少把退出登录时前端清掉本地Token、后端如果做了Token黑名单更好这条讲清楚。无状态服务如何踢用户下线答单点登录场景需要后端配合把每次登录时签发的Token记录下来踢下线时把这个Token拉入黑名单。注意讲这些话之前别把自己绕进去要是项目里没做就说这是生产级扩展方案课设里靠短过期时间控制即可。JWT的payload信息能加密吗答能JWT本身支持加密JWE但通常业务场景只需要签名防篡改payload别放敏感信息密码等绝不放进Token里。6. 环境与部署MySQL8.0、Docker、连接驱动以及Vue3的一堆小问题这一节专门讲环境配置的坑。标题里点名了MySQL8.0热搜词里一大堆都是“mysql8.0安装教程”“docker安装mysql8.0”“mysql8.0下载”这种可见卡在这里的人比写业务代码的人还多。另外Vue3相关的几个热搜问题也是实际高频出现的。6.1 MySQL8.0的三种安装姿势与选择建议第一种本机直接安装。去官网下载MySQL InstallerWindows环境下图形界面点下一步就能装。要注意选择Server only别顺手把那些MySQL Workbench、Sample Databases都装了那不是必需的。安装过程中会让你设置root密码设一个记得住的。这个方案的好处是本地开发调试最顺手坏处是如果电脑配置一般同时跑后端、前端、数据库内存压力会比较大。第二种Docker安装。如果你电脑上已经有Docker Desktop一句话就能拉起一个MySQL8.0docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEattendance_db \ -v mysql_data:/var/lib/mysql \ mysql:8.0-v mysql_data:/var/lib/mysql这行很重要它把容器的数据目录挂载到宿主机的一个命名卷里容器删了重建数据还在否则哪天Docker容器一删数据库数据全没了哭都来不及。这个方案我推荐给所有有一定动手能力的人省去了卸载、升级、环境变量的各种麻烦。第三种用集成环境。比如phpStudy、XAMPP这类带MySQL的集成环境好处是零配置坏处是自带的可能不是MySQL8.0或版本锁定不能灵活切换。做课设图省事用这个也可以但建议还是装个原生8.0后续项目升级时能省掉一次迁移成本。6.2 JDBC连接串与驱动的坑时区、SSL和Public Key RetrievalSpringBoot项目接MySQL8.0时application.yml里数据库连接串如果还抄着MySQL5.7的写法大概率会报错。我习惯这样写spring: datasource: url: jdbc:mysql://localhost:3306/attendance_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver几个关键参数逐个解释serverTimezoneAsia/Shanghai8.0要求显式指定时区不然报“The server time zone value”错误用中国本地时区存进去的时间才是北京时间。useSSLfalse本地开发没必要走加密连接也避免证书不匹配问题。allowPublicKeyRetrievaltrueMySQL8.0默认用caching_sha2_password认证插件非SSL连接时客户端要取服务器公钥不加这个参数或加了false连接时会直接报Public Key Retrieval is not allowed。这个问题非常高频网上报错一大半是它。driver-class-name记得是com.mysql.cj.jdbc.Driver比5.7时代的com.mysql.jdbc.Driver多了个cj写错直接启动报类找不到。6.3 Vue3在Edge浏览器、开发启动及相关“疑难杂症”复盘热搜词里那句“vue3项目在edge浏览器中有时候无法关闭浏览器右上角的最小化按钮”看着离谱但我们自己做项目时确实遇到过类似的精神污染级小问题。这类十有八九不是Vue3本身的问题而是页面被某个自定义样式或遮罩层干扰了浏览器原生控件的行为。排查思路是打开开发者工具看页面根节点有没有异常的全屏定位样式、z-index特别高的遮罩层。如果只是偶尔复现通常和浏览器多标签页事件状态、系统性能有关。作为开发者记住一条经验就行遇到和业务无关的渲染小毛病优先怀疑样式覆盖和全局监听事件别急着怀疑框架。再说一个真正的常见坑npm run dev启动后浏览器访问Vite默认的5173端口页面白屏。白屏十次有九次是路由模式或者初始路由配置的问题。Vite的Vue项目用createWebHistory模式时如果后端没有做history fallback刷新子路由页面就会404createWebHashHistory则不会有这个问题。本地开发用createWebHashHistory省心url里多个#字符无伤大雅。等部署到Nginx时再换createWebHistory并配try_files。另一个Edge下特有的现象是localStorage偶尔读写异常。有些前端项目喜欢把用户信息存在localStorage里Edge开启严格隐私模式或清理缓存之后业务代码读取localStorage抛异常会导致整个应用启动失败。稳妥的写法是封装一个存储工具读写时用try...catch包一层出异常就降级为内存存储。这个细节虽然小但真实项目里能救你一命。6.4 部署层面的最小可行方案课设项目一般只需要部署到一台机器或者干脆就让导师在你电脑上跑。两种常见方案方案一本地双起。后端mvn spring-boot:run前端npm run dev浏览器访问Vite开发服务器通过Vite的proxy代理把/api开头的请求转发到后端8080端口。前端配置代理的方式是在vite.config.js里export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端代码里的请求地址都写/api/xxx不需要写完整后端地址避免了跨域问题也方便以后改代理目标。方案二后端打包部署到云服务器。后端mvn clean package打成jar包服务器上java -jar xxx.jar跑起来前端npm run build把生成的dist目录扔到Nginx的html目录下Nginx再配一个反向代理把/api转发到后端8080端口。教科书上那种用Tomcat打war包的方式现在真的没人这么干SpringBoot项目了jar包内嵌Tomcat一句命令启动简单可靠。7. 实测总结与避坑清单含答辩前建议做一个这类系统功能本身不难难的是把细节收尾。我最后列一份自己在实操过程中踩过、也看别人反反复复踩的问题清单。7.1 我踩过的高频坑Lombok与实体类的坑。实体类用了Data注解但登录取用户信息时发现getter没生成。排查办法很简单确认IDEA里装了Lombok插件且开启了注解处理Settings - Build - Annotation Processing - Enable。另外多个模块时Lombok依赖要每个需要的模块都引入只在父POM里声明不够。前端传日期格式后端解析报400。前端用Element Plus的日期选择器组件提交的格式是2024-05-12T10:00:00.000Z后端实体类是LocalDateTime默认解析不了带T和毫秒的ISO格式。解决办法是全局加一个日期转换配置或者前端提交前先格式化好再传。我推荐后端统一用一个JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解双方约定好这个格式字符串。批量插入MyBatis-Plus的saveBatch性能问题。数据量小感知不到但如果一次性导入上千条考勤记录默认的saveBatch是一批1000条循环插入速度比较慢。可以通过配置rewriteBatchedStatementstrue让MySQL驱动真正把批量插入合并成一条多VALUES语句执行效率翻几倍这个参数写在JDBC连接串里就行。统一的异常处理里没有处理MethodArgumentNotValidException。前端传参不符合校验注解比如学号为空时默认返回SpringBoot的默认错误结构前端拿不到自己约定的code和message。全局异常处理器里把这个异常捕获一下转成统一返回结构这个细节能让你联调时少很多莫名其妙的对账。7.2 答辩前的三个准备要点如果这是你的毕设项目除了把功能和代码跑通建议再准备三个东西第一数据库设计文档。把E-R图、表结构、字段说明整理成文档答辩时老师问“为什么这么设计”时你能有理有据地讲出来。标题里“含文档”那三个字不是白写的项目文档本身就是评分点。第二核心模块的流程串讲。选一个完整流程比如“教师点名到学生查看考勤统计”从登录到数据落库再到页面展示走一遍讲清楚每一步的数据流向和涉及的表、接口、组件。老师最喜欢问这种贯穿性的问题你能当场走通说明项目真的是自己做的。第三性能和安全的一个扩展点。比如分页查询如果数据量大怎么做缓存、接口做不做限流、密码明文还是密文存储。不一定要真做出来但要有思路表明你理解了的边界在哪。7.3 对这套技术栈选型的最终体会SpringBoot2 Vue3 MyBatis-Plus MySQL8.0这四个东西单拎出来每一个都有学习曲线但组合在一起之后各自把对方最麻烦的环节扛掉了SpringBoot简化了服务端配置Vue3配合组合式API简化了前端逻辑组织MyBatis-Plus把单表操作的时间压缩到几乎为零MySQL8.0提供了可靠稳定的存储底座。整个项目写下来你会发现精力真正花在了考勤规则、权限控制、统计查询这些业务本质上而不是浪费在“怎么配数据源”“怎么封装分页”这类体力活上。对于一个大学生考勤系统来说这个选型可能是当下性价比最高的一个组合。