ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue选课系统实战:从高并发超选问题到数据库锁与事务设计

SpringBoot+Vue选课系统实战:从高并发超选问题到数据库锁与事务设计 每年选课季高校教务系统崩溃几乎是保留节目。我读本科时亲身经历过一次早上七点五十五宿舍四个人守着三台电脑一台手机八点整一刷新页面直接502。折腾到九点半才挤进去热门课早没了。后来我自己动手做高校学生选课系统毫不犹豫选了 SpringBoot Vue.js MyBatis MySQL 这套组合把整套源码从零写到完整可部署。这篇文章不是课程作业式的功能介绍而是把这套系统从需求梳理、表设计、后端选课逻辑、前端交互到部署上线的全过程拆给你看重点讲清楚选课系统最容易翻车的并发控制以及我实测踩过的那些坑。想拿这个题目做毕业设计、或者准备接手类似教务项目的同学可以直接照着抄。1. 选课场景的真实痛点先想清楚系统要扛住什么1.1 选课不是普通的CRUD很多人一听学生选课系统第一反应就是不就是课程表和选课记录两张表增删改查嘛。如果你真这么想写出来的东西大概率只能在演示PPT上跑通一遇到真实选课场景就会被打回原形。高校选课的业务特征非常特殊我归纳成三点时间窗口极度集中。学校一般会规定一个时间段比如中午12:00到13:00开放选课全校几千甚至上万名学生同时涌入。平时在线几十个人的系统瞬间要扛住几千并发。数据一致性要求极高。一门课程容量30人如果第29个和第30个学生同时提交选课请求系统必须保证只有一个成功不能出现两个人都选上、最后实际选了31人的事故。业务流程分阶段。选课通常不只一步有预选、正选、退选、补选等阶段。不同阶段学生的操作权限不一样比如预选可以随便选正选可能就要按优先级处理。这些状态切换如果不设计清楚后面改起来非常痛苦。所以我在动手写代码之前先花了一整天把业务规则全部列出来而不是急着建工程。这一步省了我后面至少一个星期的返工时间。1.2 三种角色三组截然不同的需求这个系统里一共有三种角色每种角色的痛点完全不同角色核心诉求典型操作学生快速选上想上的课、能看清时间冲突、随时退改浏览课程、选课、退课、查看个人课表教师开课审核、查看选课名单、了解课程热度开课申请、导出名单、查看选课统计管理员基础数据维护、选课规则配置、系统监控管理课程/学生/教师、设置选课时间、容量调整很多人做设计时容易把管理员功能做得很重学生功能做得很浅。实际上这个系统里最复杂、最需要打磨的是学生端的选课流程尤其是选课冲突检测和容量控制这两件事。管理员端反而相对标准化无非就是增删改查加一点统计报表。1.3 功能边界与状态控制我把选课的业务规则理成了一张状态表这是整个系统的地基课程有状态可选的、已满的、选课中的、已关闭的学生选课记录有状态已选、已退、待确认选课阶段有状态未开始、选课中、已结束后端每个接口在放行之前都要先判断当前处于哪个阶段、课程处于什么状态、学生是否满足选课条件。我把这套判断封装在一个SelectionValidator组件里凡是涉及选课状态变更的接口都要经过它避免每个Service里各写一套判断逻辑后面维护起来想死的心都有。提示状态判断一定要做在后端不能只靠前端按钮隐藏。真实环境下总有学生绕过前端直接调接口你在前端藏了选课按钮不等于后端就安全了。2. 技术选型这套组合拳是怎么打出来的2.1 后端为什么坚持SpringBoot说实话SpringBoot 现在已经不算什么新潮技术了但在高校选课这种业务场景里它依然是最稳妥的答案。选它有几个很实际的理由配置简化的收益非常直观。传统 SSM 框架要写一堆 XML 配置SpringBoot 用自动配置帮你把大部分工作干完了。项目里我只需要一个application.yml数据源、MyBatis、事务管理器都能通过配置搞定。内嵌Tomcat部署方便。一台服务器装好JDK就能跑 jar 包不用单独装Tomcat、配端口、配JNDI这对没有专职运维的高校服务器来说太友好了。生态成熟踩坑资料多。说实话做这个选题的同学百分之八九十都在用SpringBoot意味着你遇到任何问题搜索引擎能给你一堆解决方案。真到了赶工的时候这一点能救命。我用的版本是 SpringBoot 2.7.18。很多人问为什么不直接用 SpringBoot 3.x我的看法是如果你的项目不需要JDK17的新特性2.7是更稳的选择尤其是我本机环境还是JDK8生态兼容性更好。2025年做新项目我建议可以直接用3.x JDK17但如果你手上是教程资源比较老的2.7反而少踩坑。2.2 MyBatisSQL控得住才敢调优选 MyBatis 而不是 MyBatis-Plus 或者 JPA我是有明确理由的。选课系统的查询场景非常吃SQL能力课程要按学院、学分、上课时间、已选人数等多个条件动态过滤排序规则还经常变。MyBatis 最大的优势就是SQL 写在自己手里什么动态条件、连表查询、复杂聚合我都能精确控制。MyBatis-Plus 虽然开发效率高但遇到复杂查询时生成的SQL往往不是最优的。另一个重要原因是MyBatis 的缓存机制。MyBatis 自带一级缓存和二级缓存默认情况下同一个 SqlSession 内的查询会走缓存。但这里有个大坑选课操作涉及实时数据变更开二级缓存极有可能导致学生看到的选课人数不是最新的。我直接把二级缓存关了保证每次查询都走数据库。这个决策在后面压测中也证明了是对的——任何时候选课人数都比缓存快一步。如果你正在准备面试MyBatis 的一二级缓存、SQL执行流程、动态SQL的工作原理都是高频考点做这个项目顺手把这些搞懂了等于一鱼两吃。2.3 前端Vue.js省心且够用前端选 Vue.js 而不是 React核心原因很简单这个项目的交互复杂度还没到需要 React 那种灵活度的程度。Vue 的响应式数据绑定和指令系统非常适合选课页面这种列表 详情弹窗 已选列表联动的场景。比如学生选了一门课左侧课程列表的已选人数要实时 1右侧我的课表要立刻出现这节课这种双向联动在 Vue 里就是改一下数据的事。如果换 jQuery 时代的写法不知道要写多少 DOM 操作。Vue Router 做前端路由很方便配合导航守卫可以轻松实现未登录不能访问学生不能访问教师页面这类权限控制。我用的版本是 Vue 2.6配套 Element UI 组件库页面做出来比较规范适合课程设计和毕设展示。如果你是新项目用 Vue 3 Element Plus 也没问题核心逻辑大同小异。2.4 2025年的版本搭配参考我把自己实测跑通的版本组合列在这里照着装基本不会出兼容性问题组件版本说明JDK1.8 / 172.7配83.x配17SpringBoot2.7.18稳定、教程多MyBatis3.5.x与SpringBoot starter整合MySQL5.7.44兼容性好安装包好找Vue2.6.14配Element UINode.js16.x构建Vue前端用Maven3.8.x后端构建注意如果你用 MySQL 8.0记得驱动类名是com.mysql.cj.jdbc.DriverURL 里一定要加serverTimezoneAsia/Shanghai否则连接会报时区错误。这个坑几乎每个人都会踩一次。3. 数据库设计表结构定了系统就成了一半3.1 核心表设计选课系统的表结构我建议按这个思路拆用户体系一张表学生/教师扩展表各一张课程两张表选课记录一张表。我最终落地的核心表如下表名作用关键字段t_user登录账号id, username, password, rolet_student学生信息user_id, student_no, name, major_idt_teacher教师信息user_id, teacher_no, name, dept_idt_course课程基本信息id, course_name, credits, teacher_id, capacity, selected_countt_course_time课程上课时间id, course_id, day_of_week, start_section, end_sectiont_selection选课记录id, student_id, course_id, status, create_time这里有个细节你可能注意到了课程容量 capacity 和已选人数 selected_count 我直接放到了课程表里。为什么不通过COUNT(*)去统计选课记录来判断课程是否已满因为频繁的 COUNT 查询在大并发下会成为数据库的负担而且这种统计逻辑放到业务代码里容易出现竞态条件。在t_selection表上我建了一个唯一约束(student_id, course_id)这样从数据库层面就杜绝了同一个学生重复选同一门课。这种约束看起来简单实际上能挡掉很多并发场景下的逻辑漏洞。3.2 防超选的数据库层保险选课系统最怕的事就是超选课程容量50结果选进去51个人。想从根上防住数据库层面要做三层保险第一层课程表里的 selected_count 字段不能超过 capacity这个可以在应用层判断但不是可靠保证。第二层选课记录表的唯一约束保证一学生对一课程只有一条有效记录。第三层事务与锁这个我在第六章详细讲。这里先给你一个结论靠应用层的if (selectedCount capacity)判断在并发场景下是不够的必须配合数据库锁才行。我的实际做法是在t_course表里加了version字段乐观锁同时选课核心接口里用数据库悲观锁兜底。两种方案组合把超选的几率压到零。3.3 索引设计选课季的查询性能靠它选课季的查询流量非常大几个核心查询必须走索引否则数据库会直接被打满。我给课程表建了这三个索引idx_course_teacher按教师查课程教师端常用idx_course_name按课程名模糊搜索学生端常用idx_course_credit按学分筛选选课记录表建了唯一索引uk_student_course组合(student_id, course_id)idx_course_status组合(course_id, status) 用于快速查某门课的有效选课人数索引不是越多越好。每多一个索引写入的时候就要多维护一棵B树。选课系统的写入集中在选课阶段如果索引建得太多高并发写入时数据库反而会变慢。我上线前用慢查询日志验证了一遍把所有不常用索引都删掉了。3.4 时间字段与逻辑删除的取舍几个容易被忽略但很重要的字段设计create_time/update_time所有核心表都要有MyBatis 里通过插入时自动填充排查问题的时候时间线能救命deleted逻辑删除学生选课记录我用的是物理删除 状态字段。为什么不用逻辑删除因为选课记录的查询走唯一索引如果同一学生退课后再选课逻辑删除会产生两条未删除冲突记录处理起来很绕。直接把退课改成status CANCELLED并保留记录还能保留学生历史选课轨迹。提示设计表的时候就想清楚删除到底是真删还是标记删比写代码时候临时决定靠谱得多。选课记录建议保留历史但课程表里的垃圾数据定期清一清。4. 后端核心逻辑把选课事务写成铁桶4.1 分层结构与请求流转后端我用了经典的四层结构Controller → Service → Mapper → MySQL。选课系统的业务不算复杂这套结构完全够用而且面试官看着也舒服。请求流转的链路是前端发请求带 JWT Token拦截器校验Token解析出用户ID和角色Controller 接收参数调用 ServiceService 先做业务校验选课阶段、课程状态、时间冲突、容量判断Mapper 操作数据库统一返回ResultT结构这里要强调一点Controller 只做参数接收和返回所有业务逻辑必须放 Service。很多人为了图省事把判断写在 Controller 里一旦接口多了代码烂得飞快。我就是坚持这个规矩后面加接口的时候只需要复用Service省了太多事。4.2 JWT登录鉴权的实现细节Token 方案我选的是 JWT而不是传统的 Session。原因很简单后端被部署成前后端分离架构Session 跨域处理比较麻烦JWT 天然无状态后端只负责验签即可。JWT 的结构是 Header.Payload.Signature 三段式我用了一个简单封装// 生成Token String token Jwts.builder() .setSubject(userId.toString()) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(secretKey) .compact();然后在 SpringBoot 里写一个拦截器继承HandlerInterceptorAdapter对除了/api/login以外的所有接口做 Token 校验Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new AuthException(未登录); } // 解析Token把userId和role放进request attribute Claims claims Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token.substring(7)).getBody(); request.setAttribute(userId, Integer.parseInt(claims.getSubject())); request.setAttribute(role, claims.get(role)); return true; }这里有个我踩过的坑JWT 如果设置了过期时间一定要在拦截器里捕获 ExpiredJwtException 并返回401否则前端拿到的错误信息非常不友好排查问题时一脸懵。4.3 选课事务Transactional真的够用吗选课这个动作最少要操作两张表插入选课记录 更新课程表已选人数。这两步必须在一个事务里要么都成功要么都回滚。Spring 的Transactional注解用起来很简单但有几个坑必须注意事务方法不能被同类内部调用。比如SelectionServiceImpl里的selectCourse()方法加了Transactional如果同类里的另一个方法直接this.selectCourse()调用事务是不生效的因为 Spring 的声明式事务基于动态代理内部调用绕过了代理。我一开始就踩了这个坑排查半天发现事务没起作用。try-catch 吞掉异常会让事务失效。事务回滚依赖 RuntimeException 往外抛如果你在方法内部 catch 住了异常并吞掉事务就认为执行成功了不会回滚。正确做法是 catch 后要么往外抛要么TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记回滚。选课的事务我简单描述一下Transactional(rollbackFor Exception.class) public void selectCourse(Integer studentId, Integer courseId) { // 1. 校验阶段、课程状态、时间冲突 selectionValidator.validate(studentId, courseId); // 2. 悲观锁锁定课程行 Course course courseMapper.selectForUpdate(courseId); // 3. 容量判断 if (course.getSelectedCount() course.getCapacity()) { throw new SelectionException(课程已满); } // 4. 插入选课记录 selectionMapper.insert(...); // 5. 更新已选人数 courseMapper.increaseSelectedCount(courseId); }4.4 防超选的并发方案对比这是全系统最核心的技术点我花了很大精力。方案一乐观锁在课程表加version字段更新时带上版本号UPDATE t_course SET selected_count selected_count 1, version version 1 WHERE id #{courseId} AND version #{version}如果返回受影响行数为0说明 version 不匹配也就是有其他人抢先更新了这时候事务回滚提示学生课程已满或系统繁忙。方案二悲观锁SELECT ... FOR UPDATE锁住课程行让其他事务等这个事务提交后再操作SELECT * FROM t_course WHERE id #{courseId} FOR UPDATE我最终采用的是悲观锁为主、乐观锁兜底的组合方案。为什么因为选课的核心冲突全都集中在同一行课程数据上悲观锁能保证同一时刻只有一个请求能操作这一行的选课人数逻辑最简单直接。乐观锁虽然并发性能更强但会发生大量白忙一场的回滚和重试在选课这种激烈竞争场景下用户体验反而不如让请求排队等一下。提示悲观锁一定要配合事务使用并且在事务内尽早释放锁。FOR UPDATE 执行后到事务提交前其他线程会一直阻塞如果事务里还做了其他慢操作很容易造成数据库连接池耗尽。我实际压测时就把事务范围缩到最小只保留查 插 更新三步。4.5 MyBatis动态SQL多条件筛选一个方法搞定课程查询是学生使用频率最高的接口条件有课程名、学院、学分、上课时间等等而且可选条件不固定。这种场景用 MyBatis 动态SQL非常合适select idqueryCourses resultTypecom.example.dto.CourseVO SELECT c.*, t.name AS teacher_name FROM t_course c LEFT JOIN t_teacher t ON c.teacher_id t.id where if testcourseName ! null and courseName ! AND c.course_name LIKE CONCAT(%, #{courseName}, %) /if if testteacherName ! null and teacherName ! AND t.name LIKE CONCAT(%, #{teacherName}, %) /if if testcredits ! null AND c.credits #{credits} /if if testavailableOnly ! null and availableOnly true AND c.selected_count lt; c.capacity /if /where ORDER BY choose when testsortType hot c.selected_count DESC /when otherwise c.id ASC /otherwise /choose /select一个方法通吃列表页的全部筛选场景不用为每个条件组合写一个SQL。动态SQL执行前最好在本地先打印出最终SQL确认拼接逻辑正确避免出现 where 后面啥条件都没有的裸奔查询。5. Vue前端实现选课页面背后的交互逻辑5.1 路由与权限控制前端路由我按角色拆成了三个模块/student/*学生端含课程列表、我的课表、选课页面/teacher/*教师端含开课管理、选课名单/admin/*管理端含学生管理、课程管理、选课阶段设置每个模块的路由都挂在同一个Router实例上通过导航守卫做访问控制。核心代码router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next(/login); return; } const role localStorage.getItem(role); if (token !to.path.startsWith(/ role)) { next(/ role /dashboard); return; } next(); });这段守卫的逻辑是先判断有没有登录再判断角色能不能访问目标路由。回到第五章开头说的前端守卫只是提升用户体验的真正的权限校验永远在后端。前端可以随便改代码绕过守卫但改不了后端接口的权限判断。5.2 状态管理这个规模不用上Vuex很多教程一上来就给你装 Vuex/Pinia把用户信息、课程列表、已选列表全塞进去。但选课系统这种规模状态管理用多了反而复杂。我的做法是用户信息和 Token 放 localStorage页面刷新后自动恢复课程列表和已选列表放在各自的组件内通过 props 和事件通信只在学生选课页这种两个子组件课程列表和课表需要联动的地方用到 Vue 的响应式变量实测下来这套方案完全够用代码还更好理解。如果你做毕业设计想展示技术含量可以引入 Pinia 管理用户会话和选课列表状态但要有清晰的模块划分别把所有状态塞到一个 store 里。5.3 选课页的核心交互细节选课页是整个前端最复杂的部分我的布局是左侧课程列表支持筛选和搜索右侧我的课表按周一到周日分列。学生点课程卡片上的选课按钮前端要做三件事立即禁用该按钮防止重复点击造成重复请求弹出确认框显示课程信息包括时间、学分、余量请求成功后更新左侧余量和右侧课表这里最容易漏掉的是选课按钮的防抖。高并发场景下学生可能会双击按钮如果没有防抖前端会发出两个一模一样的请求。我在按钮点击事件里加了disabled状态切换同时在后端做了幂等校验——同一个学生在同一门课同时发起两次请求后会到的那次会因为重复选课校验直接失败。课程卡片的余量我用selected_count和capacity实时算出但前端显示的数字永远可能滞后于真实值所以我在页面顶部加了一条提示选课结果以下单结果为准。这既是实话也是在并发场景下降低学生困惑的常用策略。5.4 Vue打包后如何放进SpringBoot开发时前后端分离很舒服但部署时把两个项目分开跑前端Nginx 后端Tomcat在课程设计里显得不够一体化。其实完全可以把 Vue 打包后的静态文件直接放进 SpringBoot 里一个 jar 包搞定。步骤很简单# 1. 前端构建 npm run build # 2. 把dist目录下的所有文件复制到SpringBoot的static目录 cp -r dist/* src/main/resources/static/ # 3. 打包 mvn clean package -DskipTests需要注意一个关键配置如果前端用了 Vue Router 的 history 模式刷新页面会出现404因为后端没有对应的路由处理。解决办法是加一个路由转发把所有非API路径转发到 index.htmlController public class PageForwardController { RequestMapping(value {/, /student/**, /teacher/**, /admin/**}) public String forward() { return forward:/index.html; } }或者更简单直接用 hash 模式路由URL 里会带个#刷新不会404。我为了界面好看用了 history 模式然后配了这个转发。6. 一次真实翻车选课并发导致的超选事故复盘6.1 现象压测刚起就发现超选系统开发完第一次做模拟选课压测我用JMeter模拟了500个并发请求同时选一门容量300的课。压测跑完查数据库时头都大了选课记录表里这门课的有效选课记录一共312条超了12个人。当时第一反应是事务没生效。检查日志发现选课接口确实都走完了没有报错。这说明不是简单的逻辑漏洞而是并发条件下检查-插入-更新这三步之间的竞态问题。6.2 排查链路先查日志再查SQL最后定位到锁我的排查顺序是看后端日志确认300个成功之后还有请求正常返回说明程序根本没意识到课程已满看数据库日志发现好几个事务同时执行了SELECT selected_count FROM t_course WHERE id ?读到的都是299看事务配置发现Transactional生效但两个事务可以同时读到同一行数据在 MySQL 默认的 REPEATABLE READ 隔离级别下事务A读到选课人数299事务B也读到299两个人都认为自己可以选确认问题根因应用层先查后改的判断在并发下没有任何原子性保证问题其实出在我最初的代码写法上——先用普通 SELECT 查容量条件成立再插入。这种检查再执行模式在单线程下没问题多线程并发下就是个典型的竞态条件。6.3 修复一条FOR UPDATE锁解决定位到问题后修复方案很直接把查询改成悲观锁让查容量和更新人数之间不会插入第二个事务的操作。修改后的核心逻辑Transactional(rollbackFor Exception.class) public void selectCourse(Integer studentId, Integer courseId) { selectionValidator.validateSelectionStage(); // 关键使用悲观锁锁住课程行 Course course courseMapper.selectByIdForUpdate(courseId); if (course.getSelectedCount() course.getCapacity()) { throw new SelectionException(课程已满); } // 检查时间冲突 ListCourseTime studentCourseTimes courseTimeMapper.selectByStudentId(studentId); ListCourseTime thisCourseTimes courseTimeMapper.selectByCourseId(courseId); if (hasTimeConflict(studentCourseTimes, thisCourseTimes)) { throw new SelectionException(上课时间冲突); } selectionMapper.insert(Selection.builder() .studentId(studentId) .courseId(courseId) .status(SELECTED) .build()); courseMapper.increaseSelectedCount(courseId); }对应的 Mapperselect idselectByIdForUpdate resultTypeCourse SELECT * FROM t_course WHERE id #{id} FOR UPDATE /selectFOR UPDATE会锁定这行数据直到事务提交或回滚。第二个请求来了之后会发现锁被占着只能阻塞等待。等第一个事务提交后它读到的selected_count已经是更新后的值就能正确判断课程是否已满。同时我还加了一行乐观锁兜底UPDATE t_course SET selected_count selected_count 1 WHERE id #{id} AND selected_count capacity这行的意思是就算所有锁都没拦住最后这行 UPDATE 也必须满足已选人数小于容量才执行。如果影响行数为0说明超选了事务整体回滚。6.4 回归验证压测数据说话修复后重新跑同样的压测500并发抢300容量的课结果精确落在300条记录一条不多一条不少。我又测试了1000并发抢500容量的课最终也是500条整。有一点要诚实告诉你悲观锁方案在压测时接口的响应时间是有上升的500并发的平均响应时间从修复前的约0.8秒涨到了1.5秒左右。但这也是符合预期的因为并发请求本质上是串行排队处理了。对于选课这个场景排队但准确远比快速但出错重要。7. 部署上线与避坑清单7.1 环境准备一台普通服务器就够了选课系统部署实际用到的环境很简单我用一台4核8G的Linux服务器就能跑得很稳JDK 1.8MySQL 5.7Nginx 1.20如果前后端分离部署Maven 3.8如果你用的是 MySQL 8.0连接配置里要注意spring: datasource: url: jdbc:mysql://localhost:3306/course_selection?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.DriveruseSSLfalse一定要加否则 MySQL 8.0 会尝试建立 SSL 连接本地没有证书的话直接报错。我见过太多人卡在这一步。7.2 打包与配置分离后端打包我建议用 Maven 的 profile把开发环境和生产环境的配置分开# 开发环境打包 mvn clean package -DskipTests -Pdev # 生产环境打包 mvn clean package -DskipTests -Pprod生产环境的application-prod.yml单独放数据库连接、Redis地址等敏感信息这样换服务器部署时不用重新编译直接改外部配置文件就行。我实际部署时就把配置文件放在 jar 包同级的 config 目录下SpringBoot 启动时会优先读取外部配置非常方便。7.3 MySQL参数调优连接数与连接池选课系统的数据库压力集中在短时间内两个参数很关键max_connectionsMySQL默认最大连接数通常是151这个值在选课期间可能不够用。我调整到了500。如果不改连接数用完后端会报Too many connections。连接池大小SpringBoot 默认的 HikariCP 连接池配置按需调整。根据我压测的经验maximum-pool-size设置在30左右比较稳太小请求会排队太大反而增加数据库负担。还像这句话说的连接池不是一个越大越好的东西。7.4 我踩过的坑整理最后随便挑几个实际项目中印象最深的坑都是小事但每个都能卡住你好几个小时问题现象解决办法MySQL 时区错误连接报The server time zone value is unrecognizedURL加serverTimezoneAsia/Shanghai事务不生效选课接口异常了数据还是写进去了检查是不是同类内部调用把方法拆到不同Bean或用AOPMyBatis 驼峰映射失效查出来的selected_count字段是null配置map-underscore-to-camel-case: trueVue history 模式404刷新页面报404后端加页面转发或改用hash路由JWT 过期没处理前端一直收到500拦截器捕获ExpiredJwtException返回401这套系统从表设计到部署上线前后花了大概三周。代码量不算大最耗时间的反而是并发问题的排查和修复但这部分恰恰是这个项目最有含金量的地方。如果你也准备做选课系统建议把重心放在事务、锁、数据一致性这些看不见的地方而不是页面做得有多花哨——毕竟系统能扛住选课那天的流量才是真正检验实力的时刻。
返回列表