尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot2+Vue3新生报到系统实战:从数据库设计到并发处理

SpringBoot2+Vue3新生报到系统实战:从数据库设计到并发处理 又到了一年一度各大高校做信息化系统的旺季新生报到模块几乎是每所高校数字化建设里都绕不开的一个坎。我最近正好把一个基于SpringBoot2 Vue3 MyBatis-Plus MySQL8.0的大学新生报到系统完整整理成了可运行的源码附带全套开发文档和数据库脚本。这套东西从设计到落地前后踩了不少坑网上虽然有很多类似的“XX管理系统”源码但能真正跑到迎新现场、顶得住几百人同时办理的其实不多。这次把项目从技术选型到核心业务实现完整拆一遍大家拿到源码后也能快速跑起来不用再从零摸索。这套系统解决的不是“写几个增删改查页面交作业”这种问题而是实打实的报到业务闭环新生名单批量导入、线上信息登记、线下报到确认、宿舍分配、缴费状态核验、各学院报到进度实时统计。技术栈选的是目前中小型项目最稳的组合SpringBoot2负责后端接口Vue3 Vite负责前端交互MyBatis-Plus扛起数据库操作的大梁配合MySQL8.0存储业务数据。无论是课程设计、毕业设计还是学校信息中心要做一个可落地的Demo这套项目的参考价值都在——它把真实业务场景里那些棘手的地方都处理好了比如并发报到时的数据一致性、大文件名单导入的超时问题、前后端跨域联调的坑。1. 项目整体设计思路与技术选型1.1 为什么是 SpringBoot2 Vue3 这套组合先解释一下技术选型的逻辑因为很多人在做同类系统的时候容易陷入“什么新用什么”的误区。这套系统不是追求最新版本号而是追求“稳定能跑 资料好查 团队上手快”的组合所以最终敲定了SpringBoot 2.7.x Vue3.2.x MyBatis-Plus 3.5.x MySQL 8.0.x这套配置。SpringBoot2选型很关键。虽然SpringBoot3已经发布很久了但现实是很多高校、中小企业的生产环境还是以JDK8为主而SpringBoot3强制要求JDK17起步光这一点就能劝退大部分团队。SpringBoot2.7是2.x系列的最终维护版本既保留了javax命名空间下各种老依赖的兼容性又能支持完善的后端生态包括Spring Security、Redis、MinIO对象存储等全部无缝衔接。说白了选SpringBoot2不是在用旧技术而是在用“稳定到极致”的技术。Vue3这边是组合式API TypeScript的路线。相比Vue2的选项式API组合式API在复杂的表单页面和流程性业务里优势非常明显。比如新生报到信息登记页需要联动学院、专业、班级三个级联选择器还需要根据是否缴纳学费动态展示宿舍分配区域。这种多状态耦合的页面如果用Vue2的data methods watch去写代码迟早乱成一锅粥。Vue3的setup语法配合computed和watchEffect逻辑复用干净利落调试的时候也不用在data和methods之间反复横跳。后端持久层我选了MyBatis-Plus而不是Spring Data JPA原因有两点。第一是多表关联查询的可控性。新生报到系统里有一堆复杂统计SQL比如按学院分组统计报到人数、查未报到学生的高中来源分布、关联宿舍表和缴费表生成学生的综合状态卡。这种场景下JPA的自动映射反而碍手碍脚MyBatis-Plus可以直接写XML里的自定义SQL怎么查、怎么关联、怎么聚合全由开发者掌控。第二是单表操作开发效率。MyBatis-Plus内置了通用Mapper和通用Service单表的增删改查、分页查询、条件构造器全部内置省去手写大量样板代码这在业务模块多的时候优势特别明显。1.2 业务角色与报到流程梳理一个完整的报到系统角色划分和状态流转是灵魂。这套系统里一共设计了三种角色系统管理员、学院管理员辅导员、新生。三者权限严格分离前端通过Vue Router路由守卫控制页面可见性后端通过Spring Security JWT做接口权限验证。新生报到业务流程不是一锤子买卖而是分阶段推进的线上预报到阶段新生拿到录取通知书后扫码进入系统登录后完善个人信息身高体重、血型、紧急联系人、上传证件照、登记到校时间与交通方式。现场报到阶段学生到校后辅导员或志愿者登录学院端通过扫码枪或手动输入学号调出学生信息依次办理身份核验、学费缴纳确认、宿舍分配、军训服装领取、校园卡发放。统计汇总阶段系统大屏实时展示全校报到率、各学院报到人数、今日到校人数趋势。这部分是学校领导最关心的也是系统价值最直观的体现。业务状态机围绕着“报到进度”这条主线展开我设计了四个核心状态未报到INIT → 已到校ARRIVED → 已缴费PAID → 已完成COMPLETED。每一个步骤都有对应的时间戳和操作人记录方便后续审计追踪。2. 开发环境搭建与工程初始化2.1 环境版本踩坑记录这套系统在开发环境上花了不少时间关键是版本兼容问题。先把稳定经过验证的版本组合整理如下照着配就能少走很多弯路组件版本号备注JDK1.88u202不要用太老的u版本否则部分Maven插件会报错Maven3.6.33.8对镜像配置有调整记得用阿里云镜像Node.js16.14.xVue3 Vite4在这个版本下最稳Node20也试过但会有openssl报错MySQL8.0.338.0以上就行驱动注意配置时区Redis5.0用于验证码缓存可选但建议加上IDE后端IntelliJ IDEA 2022Lombok插件必须装IDE前端VS Code开箱即用MySQL8.0的安装有个老生常谈但又特别容易忽略的坑认证插件。MySQL8默认使用caching_sha2_password而如果你用了一些老旧JDBC驱动mysql-connector-java 5.x连接的时候会直接报Unable to load authentication plugin caching_sha2_password。解决办法有两个一是升级驱动到8.x推荐直接换依赖就行二是在创建用户时手动指定认证插件为mysql_native_password。我项目里用的是mysql-connector-java 8.0.33版本完全兼容不用额外操作。Node.js版本这边我吃了亏。最开始用了Node 20版本Vite和esbuild各种不兼容最后锁定16.14.x才彻底稳定下来。如果你用的也是Vite4老老实实切回16或者18的LTS版本别追新追新结果就是浪费时间。2.2 后端工程目录搭建与Maven依赖后端项目的包结构我按照模块化分层的思想设计跟网上大多数“Controller里堆业务代码”的Demo不同目录结构可以直接照抄到企业级项目里com.example.enrollment ├── common // 通用模块统一返回结果、异常处理、工具类 │ ├── Result.java │ ├── PageResult.java │ ├── GlobalExceptionHandler.java │ └── JwtUtil.java ├── config // 配置类MyBatis-Plus分页、跨域、静态资源映射 │ ├── MybatisPlusConfig.java │ ├── CorsConfig.java │ └── WebMvcConfig.java ├── controller // API入口只有参数接收和结果返回不做业务 │ ├── StudentController.java │ ├── CheckInController.java │ ├── DormitoryController.java │ ├── StatsController.java │ └── FileController.java ├── service // 业务接口 实现核心逻辑都在这层 │ ├── StudentService.java │ └── impl/StudentServiceImpl.java ├── mapper // MyBatis-Plus的Mapper接口 XML文件 │ ├── StudentMapper.java │ └── xml/StudentMapper.xml ├── entity // 数据库实体类与表字段一一对应 ├── dto // 前端传参对象VO和DTO分离避免实体暴露给前端 └── utils // Excel导入导出、图片压缩等工具这种分层的好处是当出现一个“报到统计”需求时你只需要在controller里加一个接口、在service里加一个方法、在mapper里加一个SQL各层职责清清楚楚不会出现一个5000行的Controller文件让人崩溃的情况。Maven核心依赖如下注意MyBatis-Plus和SpringBoot2版本之间的兼容性我用的是3.5.3.1版本完全兼容SpringBoot2.7.xdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-generator/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency2.3 Vue3前端工程初始化与路由配置前端用Vite作为构建工具创建方式非常简单npm create vite4 enrollment-web -- --template vue cd enrollment-web npm install npm install vue-router4 pinia element-plus axios装完后我还要加一个合理的前端目录组织不能把所有组件都塞到views里时间久了就乱套。我的实践是src ├── api // 所有后端接口的请求函数都集中在这里按模块分文件 │ ├── student.js │ ├── checkin.js │ └── stats.js ├── components // 公共组件上传组件、分页组件、级联选择器 ├── layout // 主布局侧边栏 头部 内容区 ├── router // 路由配置 守卫 ├── store // Pinia状态管理 ├── utils // axios封装、token管理、格式化 └── views // 页面级组件 ├── login ├── dashboard ├── student ├── checkin └── stats路由守卫是前端权限控制的关键核心思路是每次跳转前检查localStorage里的token没有token就强制回到登录页有token但当前页面需要特定角色权限时从Pinia仓库里拉取用户角色信息做比对。这个逻辑不复杂但却是整个前端安全的第一道防线一定要写对router.beforeEach((to, from, next) { const token localStorage.getItem(token) const userRole localStorage.getItem(role) if (!token to.path ! /login) { next(/login) return } if (token to.meta.roles !to.meta.roles.includes(userRole)) { next(/403) return } next() })3. 数据库设计要点与 MyBatis-Plus 实战3.1 核心表结构设计思路数据库设计是整套系统的地基。新生报到系统的核心数据表并不算多但每一张都得斟酌好字段和索引因为报到当天会有大量并发查询和写入。我设计了以下几张核心表表名用途关键字段说明sys_user系统用户管理员、辅导员id, username, password, role密码用BCrypt加密存储student新生基本信息id, student_no, name, id_card, gender, college_id, major_id, phone, address, photo_url, check_status学号和身份证号加唯一索引check_in_record报到办理记录id, student_id, operator_id, step, status, remark, create_time每完成一个环节记录一条dormitory宿舍信息id, building, floor, room_no, bed_no, max_beds, used_beds床位数量实时更新dorm_assignment学生宿舍分配关系id, student_id, dorm_id, assign_time一个学生只能有一条有效分配记录fee_record缴费状态记录id, student_id, total_amount, paid_amount, status对接线上支付后更新announcement通知公告id, title, content, publish_time新生可在移动端看到关于表字段设计有几个容易忽略的细节。逻辑删除字段必须预留我用的是deleted字段0为正常1为删除配合MyBatis-Plus的TableLogic注解使用。创建时间和更新时间两个字段是刚需配合MyBatis-Plus的自动填充功能插入和更新数据时不用手动set时间。版本号字段version是可选但强烈的建议尤其是在宿舍分配场景下两个人同时抢最后一个床位的时候如果没有乐观锁保护数据库里的used_beds可能被覆盖成错误值。宿舍床位分配是典型的并发竞争场景。我用update ... where used_beds max_beds配合MyBatis-Plus乐观锁实现了一个绝对安全的分配方案。这里先简单说思路每次分配床位时先查宿舍当前使用量然后执行更新语句在UPDATE语句里加上WHERE used_beds max_beds条件如果更新影响行数为0说明床位已满立即给前端返回“该宿舍已满”的提示。这种方式在数据库层面保证了原子性比在Java代码里用synchronized锁或Redis分布式锁要简单可靠得多。3.2 MyBatis-Plus 核心配置与分页插件MyBatis-Plus的配置有三个地方必须写对分页插件、自动填充、乐观锁插件。这三个配置错了项目基本上跑不起来或出现诡异Bug。分页插件的注册是有顺序讲究的。如果你同时用多个拦截器分页插件要放在最后面Configuration MapperScan(com.example.enrollment.mapper) public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } Bean public MetaObjectHandler metaObjectHandler() { return new MetaObjectHandler() { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }; } }分页查询的使用很顺手传入页码和每页条数配合条件构造器LambdaQueryWrapper再也不用拼接SQL字符串了public PageResultStudentVO queryStudentPage(StudentQueryDTO dto) { PageStudent page new Page(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapperStudent wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(dto.getKeyword()), Student::getName, dto.getKeyword()) .eq(dto.getCollegeId() ! null, Student::getCollegeId, dto.getCollegeId()) .eq(dto.getCheckStatus() ! null, Student::getCheckStatus, dto.getCheckStatus()) .orderByDesc(Student::getCreateTime); PageStudent result studentMapper.selectPage(page, wrapper); return PageResult.of(result); }LambdaQueryWrapper最爽的地方就是类型安全字段名用方法引用Student::getName一旦字段改名或者写错编译期直接报错不会线上运行到一半才发现SQL写错。3.3 XML自定义SQL与动态统计查询虽然MyBatis-Plus的单表操作很爽但复杂的多表统计SQL还是得老老实实写在XML里。这里有个配置细节经常有人搞错XML文件放在哪里。如果你的XML文件跟Mapper接口放在同一个包下例如com.example.enrollment.mapper.xml目录必须要在application.yml里声明mapper-locations路径如果你放在resources/mapper/目录下同样也要声明。忘了配置的话启动后调用带XML的Mapper方法会报Invalid bound statement (not added to mapper)。我的XML文件放在resources/mapper/StudentMapper.xml配置如下mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl报表统计是整个系统里最能体现价值的功能。举个例子大屏上要显示“各学院报到率排行榜”这个SQL要关联student表和system中的学院字典表统计每个学院的学生总数和已报到人数select idselectCollegeCheckInStats resultTypecom.example.enrollment.dto.CollegeStatsVO SELECT c.id AS collegeId, c.name AS collegeName, COUNT(s.id) AS totalCount, SUM(CASE WHEN s.check_status COMPLETED THEN 1 ELSE 0 END) AS checkedCount, ROUND( SUM(CASE WHEN s.check_status COMPLETED THEN 1 ELSE 0 END) / COUNT(s.id) * 100, 2 ) AS checkRate FROM sys_college c LEFT JOIN student s ON s.college_id c.id WHERE s.deleted 0 GROUP BY c.id, c.name ORDER BY checkRate DESC /select这里有个关键细节c.id在xml里直接写承接来自组织架构。注意查询统计时LEFT JOIN要保留所有学院即使用户还没为学院导入新生。这样即使某个学院报到人数为0排行榜上依然能看到数据而不是直接少了一行。另一个细节是ROUND(..., 2)保留两位小数前端展示百分比时不用再做额外处理。4. 核心业务模块实现与关键代码解析4.1 新生报到流程控制与状态机设计报到流程是整个系统的核心前端页面只负责把学生的基本信息展示给操作员而后端的核心任务是状态机的正确流转。我在设计时没有用复杂的流程引擎而是自己用代码维护了一套简单的状态流转逻辑配合数据库事务保证数据一致性。学生的报到状态流转如下被定义在一个枚举类里public enum CheckStatus { INIT(0, 未报到), ARRIVED(1, 已到校), PAID(2, 已缴费), COMPLETED(3, 已完成); private final int code; private final String desc; }每个状态之间的转换不是无条件允许的比如学生必须先从INIT流转到ARRIVED才能进入PAID状态。我在Service层写了一个通用的状态更新方法结合乐观锁防止并发情况下状态错乱Transactional(rollbackFor Exception.class) public void updateCheckStatus(Long studentId, CheckStatus targetStatus, Long operatorId) { Student student studentMapper.selectById(studentId); if (student null) { throw new BizException(学生不存在); } // 核心校验当前状态必须小于目标状态不允许跳跃式流转 CheckStatus currentStatus CheckStatus.fromCode(student.getCheckStatus()); if (currentStatus.getCode() targetStatus.getCode()) { throw new BizException(状态流转不合法当前状态为 currentStatus.getDesc()); } // 乐观锁更新version字段防并发 Student update new Student(); update.setId(studentId); update.setCheckStatus(targetStatus.getCode()); update.setVersion(student.getVersion()); int rows studentMapper.updateById(update); if (rows 0) { throw new BizException(系统繁忙请重试); } // 记录办理日志 CheckInRecord record new CheckInRecord(); record.setStudentId(studentId); record.setOperatorId(operatorId); record.setStep(targetStatus.getDesc()); record.setStatus(targetStatus.getCode()); checkInRecordMapper.insert(record); }这段代码里有几个实战层面的细节值得说明。第一Transactional加上rollbackFor Exception.class确保如果后面的操作日志写入失败前面的状态更新也会回滚不会出现状态变了但日志没记录的情况。第二乐观锁字段version在更新之前先查出来再作为更新条件传入如果更新行数为0说明其他线程已经抢先修改过这条数据这时候宁可报个“系统繁忙”的提示也不能让它把脏数据写进去。第三状态机校验逻辑放在Service层而不是Controller层避免有多个入口时校验逻辑被遗漏。4.2 Excel批量导入新生名单功能每年招生办给的都是Excel文件一份文件几百到几千人不等所以新生名单的批量导入是刚需中的刚需。这个模块我踩了不少坑核心难点不在读取Excel而在数据清洗和异常定位。当5000行数据里有20行格式不对时你得告诉用户哪些行有问题、哪一列有问题而不是笼统报一句“导入失败”。Excel读取用的是阿里巴巴的EasyExcel相比Apache POI它最大的优势是内存消耗极小读大文件不会OOM。具体实现思路是前端用Element Plus的上传组件把文件传到后端后端异步解析Excel通过监听器逐行读取、逐行校验、逐行入库。这种逐行处理的模式对内存非常友好。数据校验规则包括学号不能重复需要先查库如果已存在则跳过、身份证号必须为18位且校验位正确、手机号格式合法、学院和专业的名称必须在数据库中存在。每一条校验失败都会记录到错误列表里最后生成一个带错误原因的结果对象返回给前端展示。这个过程中有个很重要的优化校验数据时如果逐行查询数据库性能会非常差。我的做法是先把数据库里已有的学号一次性查出来放到一个HashSet里然后逐行用contains()判断以减少对数据库的查询频次SetString existingStudentNos studentMapper.selectAllStudentNos(); for (StudentExcelVO row : excelRows) { if (existingStudentNos.contains(row.getStudentNo())) { errors.add(第 row.getRowNum() 行学号已存在); continue; } // 其他字段校验... }EasyExcel的监听器模式和分页插入模式结合起来整套导入流程非常顺畅实测5000行数据差不多10秒内完成解析和入库完全满足实际使用。4.3 宿舍分配并发场景的数据库级锁方案宿舍分配是整个系统里并发冲突最明显的场景。想象一下报到当天上午同一个宿舍楼的最后一个床位可能有三个新生几乎同时办理分配。如果代码写成“先查剩余床位→判断是否0→执行update”那么这三个请求可能同时查到剩余床位为1然后同时执行update最终used_beds被写成了2甚至3超出了max_beds数据就错了。我的解决方案是上面提到的数据库条件更新核心SQL如下int rows dormitoryMapper.updateUsedBedsWithCheck(dormId); update idupdateUsedBedsWithCheck UPDATE dormitory SET used_beds used_beds 1 WHERE id #{dormId} AND used_beds max_beds /update当update返回的行数为1说明这个宿舍还有余量这次床位扣减成功了返回0则说明床位已满立刻给前端返回“该宿舍已满”的提示。这个方案不需要Java层加锁也不需要Redis分布式锁数据库的原子性帮我们挡住了所有并发请求这是最高效可靠的方式。分配成功后还要往dorm_assignment表里插入一条分配记录。这里涉及一个多表一致性的问题宿舍床位数更新成功但分配记录插入失败怎么办某个学生分配到了505宿舍但分配记录里找不到他那宿舍的工作人员会陷入“这个学生到底住哪”的尴尬局面。这种情况的处理方式是把它俩放到同一个事务里宿舍扣减成功但分配记录插入失败时宿舍扣减自动回滚Transactional(rollbackFor Exception.class) public void assignDormitory(Long studentId, Long dormId) { int rows dormitoryMapper.updateUsedBedsWithCheck(dormId); if (rows 0) { throw new BizException(该宿舍已满请选择其他宿舍); } // 将之前的分配记录作废再插入新记录 dormAssignmentMapper.updateInvalid(studentId); DormAssignment assignment new DormAssignment(); assignment.setStudentId(studentId); assignment.setDormId(dormId); assignment.setAssignTime(LocalDateTime.now()); dormAssignmentMapper.insert(assignment); }观察这里先扣减床位再插入分配记录的执行顺序是有讲究的床位资源是稀缺资源必须第一时间把它占住否则先插入记录再扣减可能最后记录插了但床位没扣到逻辑上还不够严谨。事务保证了这个组合操作的原子性实际压测下来这台接口在100并发下没有出现超卖的情况。5. 前后端联调与常见问题排查实录5.1 MySQL8.0 时区与连接配置那些坑MySQL8.0和旧版本最明显的区别是默认时区从系统时区改为了UTC。当时区配置不对时Java后端插入时间到数据库后再查出来就会差8个小时。这种Bug特别隐蔽因为开发环境偶尔没问题一部署到服务器上就开始乱。根本原因是应用服务器时区比如Asia/Shanghai跟数据库服务器时区UTC不一致。解决方案是在JDBC连接串里明确指定时区不要依赖服务器默认设置spring: datasource: url: jdbc:mysql://localhost:3306/enrollment?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue注意allowPublicKeyRetrievaltrue这个参数是MySQL8.0特有的。因为8.0默认使用caching_sha2_password认证插件当连接是SSL加密时客户端需要获取服务器的公钥来加密密码传输。如果没有这个参数某些情况下会报错Public Key Retrieval is not allowed加上这个参数就稳妥了。另外一个坑是关于utf8mb4字符集。MySQL8.0默认字符集已经是utf8mb4但如果你是从MySQL5.7导过来的数据建库时还是老配置偶尔会出现中文乱码或者emoji无法存储的问题。建议建库时显式声明CREATE DATABASE enrollment DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.2 跨域与前端代理配置前后端分离项目一定会遇到跨域问题尤其是用Vue开发调试的过程中。跨域问题说白了就是浏览器的同源策略限制前端跑在http://localhost:5173后端跑在http://localhost:8080端口不同浏览器会拦截后端返回的响应。解决方案有两种我在项目里都做了。方案一后端配置全局CORS策略。适合生产环境部署在同一个域名下但不同端口或者前端域名固定已知的情况。我在后端Config里注册一个CORS过滤器允许指定来源访问Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意addAllowedOriginPattern(*)不能跟AllowCredentials(true)一起用老版本的addAllowedOrigin(*)否则浏览器会报错。这是SpringBoot2.4以后改动的一个细节。方案二前端开发时用Vite代理。dev环境下配置Vite的proxy代理让前端请求转发到后端浏览器的角度看起来请求就是发到同源的5173端口就不会触发跨域拦截。这个方案在开发阶段更方便不用反复改后端CORS配置// vite.config.js export default { server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, } } } }5.3 常见报错速查表把这几个月开发调试过程中遇到的高频报错统一整理成表格对号入座排查效率高不少报错信息原因分析解决方案Invalid bound statement (not added to mapper)MyBatis-Plus扫描不到XML文件检查mapper-locations路径配置确认XML的namespace是否与Mapper接口完全一致Public Key Retrieval is not allowedMySQL8.0认证插件公钥获取问题JDBC连接串加allowPublicKeyRetrievaltrueCommunications link failure数据库连接中断排查MySQL服务是否启动、连接串的IP和端口是否正确Access denied for user rootlocalhost用户名密码错误或权限不足核对密码用root账号执行GRANT ALL ON *.* TO rootlocalhostTable xxx doesnt exist表名大小写问题Linux下MySQL默认区分大小写确保建表SQL和实体类TableName的表名完全一致Failed to convert property value of type java.lang.String to required type java.lang.LongURL传参类型转换错误Controller里接收参数的字段加PathVariable或RequestParam后类型改成Long或StringDuplicate entry xxx for key yyy唯一索引冲突检查业务逻辑是否有重复提交场景页面提交按钮做防重复点击处理排错有个很重要的习惯——不要只盯着错误信息的最后一行。最常见的场景是报了一个Parameter index out of range你以为自己SQL写错了结果往上一翻日志发现是#{}里写成了${}。所以每次遇到问题先看完整堆栈再看SQL日志最后才改代码。MyBatis-Plus里我在dev环境把log-impl设置成了StdOutImpl能自动打印执行SQL和参数这排错效率直接翻倍。6. 文档编写与项目部署经验补充6.1 含文档项目如何快速跑起来这套源码里附带了一整套文档包括需求文档、数据库设计文档、接口文档、部署文档。很多人觉得写文档是浪费时间但我个人的经验是一个项目如果过了三个月你再回来看它没有文档的话你大概率连自己当时某个表为什么这么设计都想不起来。尤其是毕业设计或者课程设计交差时文档的分量不比代码轻。接口文档部分我推荐直接用Apifox或者ShowDoc来管理。Apifox 配合Spring的注解可以自动同步接口信息表单字段、数据类型、返回结构一目了然。部署文档也别图省事把从环境搭建到启动后端、再到打包前端的每一条命令都整理清楚下次换一台机器部署的时候就知道这玩意儿有多救命。6.2 生产部署的几点切身经验最后聊一下部署阶段踩过的坑。前端打包后是一堆静态资源后端是SpringBoot的Jar包我用Nginx作为Web服务器同时用来托管前端资源和反向代理后端接口。Nginx配置里最关键的是location /api/的反向代理规则把前端的API请求转发到后端的Java进程端口server { listen 80; server_name your-domain.com; root /var/www/enrollment; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }后端运行用nohup或者systemd的方式直接用java -jar命令启动。记得JVM参数中加上-Xms和-Xmx我一般给2G堆内存对这套系统绰绰有余。数据库的备份必须有报到数据太重要了我每天凌晨用cron定时执行mysqldump带上--single-transaction参数避免锁表影响在线业务。mysqldump -u root -p enrollment --single-transaction --routines --triggers backup_$(date %Y%m%d).sql我自己在实际操作中最深的一个体会是技术选型时稳定大于新颖业务实现时数据一致性永远优先于花哨功能。这套系统里每一个方案都选择的是最能保证“人在现场、事能办成”的方式复杂的并发控制交给数据库高频的模板代码交给MyBatis-Plus繁琐的页面交互交给Vue3的组合式函数各司其职、各得其所。如果你准备拿这套源码做二次开发或者当作业交差我建议别急着改功能先跟着部署文档把系统完整跑一遍把报到流程走通一次再动手改代码。跑通一遍之后你对整个前后端如何协作、数据如何流转会比看十遍代码文档都清楚得多。最后再提醒一句开发前多用Git做版本管理每完成一个模块就提交一次不仅能防止代码弄丢更重要的是你能随时回看每一步的实现思路这对成长的价值远超那几行命令。
返回列表