1. 为什么这个技术栈成了毕设和中小型后台的“标配”
先说个现象:最近几年我帮人看的Java Web项目里,大学生考勤系统、学生管理系统、仓库管理系统这类后台项目,十套里有七八套都是SpringBoot + Vue3 + MyBatis-Plus + MySQL这个组合。不是大家约好了,而是这套组合确实解决了实际开发里最痛的那几个问题——后端配置繁琐、前端组件割裂、数据库操作重复、环境搭建踩坑。
拿这个“Java Web大学生考勤系统”来说,顶上挂的SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0四个词,每一个都不是随便选的,背后都有明确的对应价值点:
- SpringBoot2负责把后端业务逻辑跑起来,它最核心的价值是“约定大于配置”。不用像SSH时代那样写一堆XML配置文件,一个启动类加上几个注解就能把Web服务拉起来,这对课设、毕设这类时间紧、重点在业务逻辑的项目来说太重要了。
- Vue3负责前端交互,相比Vue2,它的组合式API(Composition 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 vite@latest 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和Axios:
npm install element-plus pinia vue-router@4 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.x
SpringBoot现在有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):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar(50) | 登录账号 |
| password | varchar(100) | 加密后的密码 |
| real_name | varchar(50) | 真实姓名 |
| role | varchar(20) | 角色:admin/teacher/student |
| create_time | datetime | 创建时间 |
学生表(student):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 关联sys_user表 |
| student_no | varchar(20) | 学号 |
| name | varchar(50) | 姓名 |
| class_id | bigint | 班级ID |
| phone | varchar(20) | 手机号 |
这里有个设计决策要讲清楚:为什么学生表要单独存一份user_id关联登录账号,而不是直接拿学号当登录账号?因为一个学生可能同时是班干部,未来可能还兼职助教,角色和身份是会发生变化的,把“登录身份”和“个人基础信息”拆开,后面扩展权限体系会灵活很多。
班级表(class):班级名称、所属专业、年级、导员ID(关联教师用户)。课程表(course):课程名称、课程编号、授课教师ID、上课时间、上课地点。教师表(teacher):教师工号、姓名、所属院系。
3.2 考勤记录表与请假单表:核心流程落点
考勤系统的业务核心就落在这两张表上。
考勤记录表(attendance_record):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| student_id | bigint | 学生ID |
| course_id | bigint | 课程ID |
| class_date | date | 上课日期 |
| status | tinyint | 状态:1正常 2迟到 3早退 4缺勤 |
| sign_time | datetime | 签到时间 |
| remark | varchar(255) | 备注 |
| create_time | datetime | 记录创建时间 |
状态我用数字枚举维护,具体含义写死在代码的常量类或枚举里。不建议直接在数据库里存中文,因为后续要统计、筛选排序时,数字类型性能更好、代码可读性也更强。
请假单表(leave_request):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| student_id | bigint | 学生ID |
| course_id | bigint | 课程ID |
| reason | varchar(255) | 请假原因 |
| start_time | datetime | 开始时间 |
| end_time | datetime | 结束时间 |
| status | tinyint | 状态:0待审批 1通过 2驳回 |
| create_time | datetime | 提交时间 |
这两张表之间的业务联动是:请假通过之后,对应时间段内的考勤记录应该自动标记为“请假”或直接不生成“缺勤”记录。这个逻辑在Service层实现,不要在数据库层面做物理外键约束——在实际项目里,逻辑关联比物理外键好用得多,物理外键会导致删除、更新时各种约束冲突,尤其在课设里改数据改到怀疑人生。
3.3 一个容易被忽视的字段设计细节:统一使用逻辑删除
我给所有业务表都预留了deleted字段,类型tinyint,0未删除、1已删除。配合MyBatis-Plus的@TableLogic注解,所有查询自动带deleted=0条件,删除操作也被拦截转换为更新操作。这样做的好处是数据不会真正消失,追查历史记录、恢复误删数据都方便,属于正规项目的标配习惯。对课设而言,这个点写进报告里也是个很实用的加分项。
3.4 考勤判定规则:迟到早退边界怎么算
判定规则是考勤系统的核心算法逻辑,这里展示一下最简单的实现思路:
- 课程表里配了上课开始时间
start_time和下课时间end_time。 - 学生打卡时,拿当前时间和课程开始时间比较:打卡时间在开始时间之前,状态为
正常;开始时间到开始时间后15分钟之间,状态为迟到;之后则为缺勤(或者不允许打卡)。 - 学生签退时,拿当前时间和下课时间比较:小于下课时间则为
早退。
这个规则看着简单,但实际做的时候要小心一点:判断状态不能只靠一次打卡时间。比如学生既迟到又早退,那到底算哪个状态?我的处理方式是设优先级:缺勤 > 迟到 > 早退 > 正常,双异常取最严重状态。这种边界决策写清楚后,后端接口逻辑就固定了,不会出现前端展示与后端统计结果对不上的情况。
4. MyBatis-Plus实战:从BaseMapper到通用CRUD的取舍边界
这一节是这篇博文的重点之一。既然标题里专门点名了MyBatis-Plus,我把它能提速的点和我建议“不要偷懒”的点都讲一遍,避免被它宠坏了,遇到复杂场景反而不会写SQL了。
4.1 BaseMapper与ServiceImpl:单表操作为什么不需要写SQL
MyBatis-Plus的核心爽点在于,你的Mapper接口只要继承BaseMapper<T>,就自动拥有了insert、deleteById、selectById、updateById、selectList、selectPage这批方法。Service层再继承ServiceImpl<Mapper, Entity>,连接口和基础实现类都帮你造好了。
以学生管理为例,SA层的代码只需要:
public interface StudentService extends IService<Student> { PageResult<StudentVO> pageQuery(StudentQueryDTO dto); } @Service public class StudentServiceImpl extends ServiceImpl<StudentMapper, 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给的条件构造器可以把这些条件链式地拼起来:
LambdaQueryWrapper<Student> 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); IPage<Student> 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的边界:多表联查必须手写SQL
MyBatis-Plus好用归好用,但一遇到多表联查它就不灵了。比如“查询最近一周每个班级的出勤率”这种需求:需要班级表、学生表、考勤记录表三表关联,还要按班级分组算状态占比。这个场景用MyBatis-Plus自带的Wrapper套不出来,老老实实写XML:
<select id="selectAttendanceRate" resultType="com.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 </select>Mapper接口里对应写:
List<ClassAttendanceRateVO> 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(IService<T> service, T entity) { return service.save(entity); } public static <T> boolean update(IService<T> service, T entity) { return service.updateById(entity); } public static <T> boolean delete(IService<T> service, Serializable id) { return service.removeById(id); } public static <T> T getById(IService<T> service, Serializable id) { return service.getById(id); } public static <T> IPage<T> page(IService<T> service, Page<T> page, Wrapper<T> 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 Installer,Windows环境下图形界面点下一步就能装。要注意选择Server only,别顺手把那些MySQL Workbench、Sample Databases都装了,那不是必需的。安装过程中会让你设置root密码,设一个记得住的。这个方案的好处是本地开发调试最顺手,坏处是如果电脑配置一般,同时跑后端、前端、数据库,内存压力会比较大。
第二种,Docker安装。如果你电脑上已经有Docker Desktop,一句话就能拉起一个MySQL8.0:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_DATABASE=attendance_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 Retrieval
SpringBoot项目接MySQL8.0时,application.yml里数据库连接串如果还抄着MySQL5.7的写法,大概率会报错。我习惯这样写:
spring: datasource: url: jdbc:mysql://localhost:3306/attendance_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver几个关键参数逐个解释:
serverTimezone=Asia/Shanghai:8.0要求显式指定时区,不然报“The server time zone value”错误,用中国本地时区,存进去的时间才是北京时间。useSSL=false:本地开发没必要走加密连接,也避免证书不匹配问题。allowPublicKeyRetrieval=true:MySQL8.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,刷新子路由页面就会404;createWebHashHistory则不会有这个问题。本地开发用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条循环插入,速度比较慢。可以通过配置rewriteBatchedStatements=true让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提供了可靠稳定的存储底座。整个项目写下来,你会发现精力真正花在了考勤规则、权限控制、统计查询这些业务本质上,而不是浪费在“怎么配数据源”“怎么封装分页”这类体力活上。对于一个大学生考勤系统来说,这个选型可能是当下性价比最高的一个组合。