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

资讯详情

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

uniapp+MySQL选课系统开发:从表设计到并发控制实战

uniapp+MySQL选课系统开发:从表设计到并发控制实战 简介基于移动端选课系统的设计与实现完整源码包适合毕业设计、课程设计或初学uniapp前后端混合开发的开发者参考。功能覆盖个人中心、学生与教师管理、课程信息、学生选课及退选、系统管理等模块面向教与学场景后端采用Java/PHP数据库为MySQL 5.7移动端基于uniapp框架可借助HBuilder X与eclipse/idea完成混合开发整体结构便于前后端联调。资源共978个文件约32.87MB以231个png截图、196个vue页面、123个js脚本、95个java源码为主同时包含SQL脚本、说明文档、部署PPT、需求与演示文件、项目配置及部署批处理脚本目录结构清晰可按功能模块定位代码。已有63人学习下载。从中可获取完整前后端实现、数据库设计、系统演示与部署步骤能够快速搭建可运行的选课系统也可作为二次开发或课程实践的参考模板用于教学演示与功能扩展。1. 为什么一个选课系统要把 uniapp 和 mysql 绑在一起做选课系统可能是高校信息化里最短命又最高频的应用——每学期只用两三周但这两三周里全校学生同时挤进来抢课并发量瞬间拉满。如果只做一个后台管理端负责排课的老师倒是方便了但学生还得去电脑前操作体验很差。所以你看现在很多毕业设计和实际项目都选了移动端选课系统这个方向前端用 uniapp 一套代码同时出 App 和微信小程序后端配 mysql 存选课数据再做一套管理后台这就是标题里完整前后端mysql说明文档LW的典型构成。把一个选课系统拆开看核心业务其实不复杂学生登录、浏览课程、选课、退课、查看已选列表管理员维护课程信息、设置选课时间、处理特殊调整。复杂的是并发控制、数据一致性、移动端多端适配这些边界问题。我之前帮人评审过一个这类项目发现大部分翻车点不在业务逻辑本身而在于前后端怎么交互、选课请求怎么避免超卖、uniapp 打包后在微信小程序和 App 上的行为差异。这套东西如果只停留在能跑层面其实一周就能做出来但如果要做到能上线、能扛住选课高峰就得把细节抠透。这篇笔记会顺着标题里的技术栈往下走先梳理项目结构再给关键表设计和接口定义然后分别讲移动端 uniapp 调用逻辑和管理后台的落地最后把并发选课、部署环境、多端适配的几个血泪经验交代清楚。新手能照着把项目跑起来熟手也能在这里看到参数边界和容易翻车的地方。2. 项目骨架与数据库设计先把表结构定好后面才能少改2.1 标准三层结构uniapp 移动端、管理后台、后端服务标题里写了完整前后端我见过的大多数实现是这样一个布局前端分成两个入口一个是 uniapp 写的学生端跑在微信小程序和 Android/iOS App 上另一个是管理后台常见做法是用 Vue 或若依这类脚手架做也可以直接用 uniapp 的 H5 模式充当管理端但那样表格、复杂筛选会很别扭。后端一般用 Spring Boot 或 Node.js提供 RESTful API数据库就是 mysql。这样的拆分有个好处学生端和管理端互不干扰后端接口可以同时被 App、小程序和 Web 管理端调用。如果以后要加一个教师端查看选课统计只需要对着同一套接口再做个页面就行。目录结构上我建议按模块分包来组织参考下面这个布局uniapp-app/ # 学生端uniapp ├─ pages/ # 页面 │ ├─ login/ # 登录 │ ├─ course/ # 课程列表、课程详情 │ ├─ select/ # 选课操作、已选列表 │ └─ profile/ # 个人中心 ├─ utils/ # 请求封装、工具函数 ├─ api/ # 接口定义 ├─ store/ # 状态管理 ├─ static/ # 静态资源 └─ manifest.json # 多端配置 admin-web/ # 管理后台Vue 等 ├─ src/views/ # 课程管理、学生管理、选课记录 └─ src/api/ # 接口层 server/ # 后端服务Spring Boot 等 ├─ src/main/java/ ├─ src/main/resources/ └─ sql/ # 初始化脚本每个模块的职责要单一pages只放页面视图api统一管理接口路径utils放请求封装和登录态处理。如果写到后面发现某个页面里堆了三百行业务逻辑就该把它拆成组件或工具函数。2.2 mysql 表设计选课系统的核心表与字段语义数据库是这个项目的地基表设计不好后面接口改起来牵一发动全身。一个完整的选课系统至少需要这五张表用户表、课程表、选课记录表、课程时间安排表、公告表。下面是一个核心表结构直接用 SQL 脚本说明-- 用户表 CREATE TABLE t_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(32) NOT NULL COMMENT 学号/工号, password varchar(64) NOT NULL COMMENT md5加密后的密码, role tinyint(1) NOT NULL DEFAULT 2 COMMENT 1管理员, 2学生, real_name varchar(32) DEFAULT NULL, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1正常, 0禁用, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 课程表 CREATE TABLE t_course ( id int(11) NOT NULL AUTO_INCREMENT, course_no varchar(20) NOT NULL COMMENT 课程编号, course_name varchar(64) NOT NULL COMMENT 课程名称, teacher varchar(32) DEFAULT NULL, credit decimal(3,1) DEFAULT 0.0 COMMENT 学分, capacity int(11) NOT NULL DEFAULT 0 COMMENT 容量, selected_count int(11) NOT NULL DEFAULT 0 COMMENT 已选人数, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1可选, 0不可选, schedule varchar(128) DEFAULT NULL COMMENT 上课时间描述, location varchar(64) DEFAULT NULL, description text, PRIMARY KEY (id), UNIQUE KEY uk_course_no (course_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; -- 选课记录表 CREATE TABLE t_select_record ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, course_id int(11) NOT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1已选, 0已退, PRIMARY KEY (id), KEY idx_user_course (user_id, course_id), KEY idx_course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课记录表; -- 课程时间安排表可选进阶 CREATE TABLE t_course_time ( id int(11) NOT NULL AUTO_INCREMENT, course_id int(11) NOT NULL, weekday tinyint(1) NOT NULL COMMENT 1-7, start_section tinyint(1) NOT NULL COMMENT 开始节次, end_section tinyint(1) NOT NULL COMMENT 结束节次, week_start int(11) DEFAULT 1, week_end int(11) DEFAULT 18, PRIMARY KEY (id), KEY idx_course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程时间安排;t_course里有个字段要特别留意——selected_count已选人数。很多项目会把当前已选多少人实时查t_select_record聚合出来课程多、记录多以后这个聚合查询会越来越慢更关键的是在并发选课时先查再改会造成超卖。正确的做法是直接在课程表里维护一个计数器字段选课时用事务或乐观锁去更新它。这个字段就是后面做并发控制的基础。选课记录表要建联合索引(user_id, course_id)因为某个学生选了哪些课某个学生是否选了某门课是最常见的查询。如果漏了索引数据量到几千条的时候关联查询还好到几万条就开始卡了。2.3 初始化数据与导入脚本让项目第一次启动就能看到效果表结构建好以后还得有数据才能演示。我一般会在sql目录下放三个文件schema.sql建表语句、init_data.sql基础数据、test_data.sql测试数据。基础数据里至少包含一个管理员账号、几个学生账号、几十门课程测试数据可以多一些方便压测并发。-- init_data.sql 片段 INSERT INTO t_user (id, username, password, role, real_name) VALUES (1, admin, e10adc3949ba59abbe56e057f20f883e, 1, 系统管理员), -- 密码是 123456 的 md5 (2, 2024001, e10adc3949ba59abbe56e057f20f883e, 2, 张同学), (3, 2024002, e10adc3949ba59abbe56e057f20f883e, 2, 李同学); INSERT INTO t_course (id, course_no, course_name, teacher, credit, capacity, selected_count, schedule, location) VALUES (1, C001, 高等数学, 王老师, 4.0, 100, 0, 周一 1-2节, A101), (2, C002, 大学英语, 李老师, 3.0, 60, 0, 周二 3-4节, B202);密码用 md5 存储虽然是老项目常见做法但实际生产环境建议改用 bcrypt 或加盐哈希。这里为了保持说明文档和代码一致先用 md5 演示代码里记得加注释标注仅用于演示。3. 后端接口与选课核心逻辑事务、乐观锁和超卖问题的解法3.1 接口清单与统一返回格式前后端约定好联调才不吵架后端接口设计直接影响移动端的开发效率。选课系统通常需要下面这些接口登录、获取课程列表、获取课程详情、选课、退课、查看已选列表、查看公告以及管理端的课程增删改查、学生管理、选课记录查询。如果项目里用的是 Spring Boot返回格式统一成一个Result对象Data public class ResultT { private Integer code; private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMsg(success); r.setData(data); return r; } public static T ResultT error(Integer code, String msg) { ResultT r new Result(); r.setCode(code); r.setMsg(msg); r.setData(null); return r; } }统一返回格式的意义在联调时才能体会到如果有的接口返回{success: true}有的返回{status: 1}uniapp 前端的请求拦截器就得写两套判断逻辑任何一个接口漏了处理就会变成数据明明有页面就是渲染不出来的玄学问题。接口路径建议按资源名命名/api/login、/api/course/list、/api/course/select、/api/course/cancel。用POST JSON传参避免在 URL 里拼接长参数文件上传这种场景才用multipart。接口文档如果在项目里带了 word/LW 说明文档就把每个接口的入参、出参、状态码列表写清楚这部分最容易被评审老师追问。3.2 选课接口的事务与乐观锁三行 SQL 守住容量边界先看一段最容易翻车的写法也是网上很多示例代码的写法Transactional public ResultVoid selectCourse(Long userId, Long courseId) { // 先查课容量 Course course courseMapper.selectById(courseId); if (course.getSelectedCount() course.getCapacity()) { return Result.error(400, 课程已满); } // 再查是否已选过 Integer exists recordMapper.checkExists(userId, courseId); if (exists 0) { return Result.error(400, 不可重复选课); } // 插入选课记录 recordMapper.insert(userId, courseId); // 更新已选人数 courseMapper.increaseCount(courseId); return Result.ok(null); }这段代码单机单用户跑没毛病但并发一上来就出问题。两个请求同时查到selected_count 59容量是 60都判断没满然后都执行插入和更新最后选课记录多了一条容量爆了而且selected_count只加了 1。这就是典型的超卖。解法是用乐观锁。给t_course表加一个version字段更新的时候带上版本号条件ALTER TABLE t_course ADD COLUMN version int(11) NOT NULL DEFAULT 0; -- 更新已选人数时版本号必须匹配 UPDATE t_course SET selected_count selected_count 1, version version 1 WHERE id #{courseId} AND version #{version};在 Java 代码里先查出version执行更新后判断影响行数如果为 0说明其他请求已经更新过这时候回滚事务并返回课程已满或操作冲突Transactional public ResultVoid selectCourse(Long userId, Long courseId) { // 查课程信息含 version Course course courseMapper.selectById(courseId); // 业务校验已满、重复选课 if (course.getSelectedCount() course.getCapacity()) { return Result.error(400, 课程已满); } if (recordMapper.checkExists(userId, courseId) 0) { return Result.error(400, 不可重复选课); } // 乐观锁更新selected_count 1 且 version 匹配 int rows courseMapper.increaseCountWithVersion(courseId, course.getVersion()); if (rows 0) { // 这里会抛异常触发回滚 throw new BizException(课程容量异常请刷新后重试); } // 插入选课记录 recordMapper.insert(userId, courseId); return Result.ok(null); }这里注意两点。第一increaseCountWithVersion影响行数为 0 时要主动抛异常让Transactional触发回滚否则选课记录插进去了但计数没更新数据就不一致。第二recordMapper.insert里user_id和course_id如果建了联合唯一索引重复插入会在数据库层直接报错这相当于最后一道防线防止同一个学生同一门课选两次以任何方式漏过去。3.3 退课与容量回退简单操作也要注意状态机退课接口比选课简单但容易犯一个错直接删除选课记录。如果以后需要统计学生曾经选过哪些课退过几次课数据就丢了。我一般用软删除把t_select_record.status从 1 改成 0这样保留历史记录查询已选列表时用status 1过滤。Transactional public ResultVoid cancelCourse(Long userId, Long courseId) { // 把选课记录置为已退 int rows recordMapper.cancelRecord(userId, courseId); if (rows 0) { return Result.error(400, 未找到选课记录); } // 课程已选人数回退 courseMapper.decreaseCount(courseId); return Result.ok(null); }退课的时候要不要做并发控制大多数场景不需要因为退课只会让容量释放不会造成超卖。但要注意一个边界情况课程表里selected_count已经是 0此时有人在退课会不会把计数扣成负数加一个大于 0 才更新的条件就能避免UPDATE t_course SET selected_count selected_count - 1 WHERE id #{courseId} AND selected_count 0;这种细节很多项目不会写但评审答辩时被问到如果数据异常怎么兜底这就是一个亮点。4. uniapp 移动端实现从登录到选课的全流程调用4.1 请求封装与登录态uni.request 的统一拦截与 token 处理uniapp 端第一件事是封装uni.request。选课系统里几乎所有接口都需要登录态如果每个页面都自己调uni.request再判断返回值代码会膨胀到没法维护。我习惯在utils/request.js里做一层封装// utils/request.js const BASE_URL http://172.16.10.5:8080/api; // 开发环境地址manifest中也可配置 export function request({ url, method GET, data {} }) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || }, success: (res) { // 业务状态码处理 if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { // token失效跳转登录页 uni.navigateTo({ url: /pages/login/login }); reject(res.data); } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络异常请检查连接, icon: none }); reject(err); } }); }); }BASE_URL这里直接写死了一个局域网地址实际开发时建议处理成环境变量微信开发者工具里面调试用局域网 IP真机预览用电脑局域网 IP发布到正式环境再改成 https 域名。热词里面提到的uniapp 封装 h5 如何指向 2 个域名就是一个典型场景——开发和上线环境不一样可以在manifest.json里配h5.devServer或者把BASE_URL按process.env.NODE_ENV分别加载。token 存储用的是uni.getStorageSync这一步在微信小程序里天然可用在 App 端也兼容。不要在每次请求前去uni.getStorageSync再手动拼到 url 上那样微信小程序的请求签名和缓存管理都会出问题。4.2 课程列表与选课页下拉刷新、加载更多、防重复提交课程列表页是移动端最核心的页面需要考虑三个交互细节下拉刷新、无限加载、选课按钮防重复点击。下面是一个精简版逻辑template view classcourse-list view v-foritem in courseList :keyitem.id classcourse-card clickgoDetail(item) view classcourse-name{{ item.courseName }}/view view classcourse-info 教师{{ item.teacher }} nbsp; 学分{{ item.credit }} /view view classcourse-info 已选 {{ item.selectedCount }} / 容量 {{ item.capacity }} /view button sizemini :disableditem.selectedCount item.capacity click.stopselectCourse(item) {{ item.selectedCount item.capacity ? 已满 : 选课 }} /button /view /view /template script setup import { ref } from vue; import { request } from /utils/request; const courseList ref([]); const page ref(1); const pageSize 10; const submitting ref(false); async function loadCourses() { const data await request({ url: /course/list, method: GET, data: { page: page.value, pageSize } }); courseList.value data.rows; } // 选课带防重复提交 async function selectCourse(item) { if (submitting.value) return; submitting.value true; try { await request({ url: /course/select, method: POST, data: { courseId: item.id } }); uni.showToast({ title: 选课成功 }); loadCourses(); } finally { submitting.value false; } } /scriptsubmitting这个标记是防重复提交的关键。选课接口的响应速度取决于后端事务耗时用户手速快的话 500ms 内点两次后端还没返回item.selectedCount也没刷新第二枪就出去了。虽然后端有乐观锁兜底但前端能拦住最好不要给服务器添压力。移动端性能优化这里有个容易被忽略的点课程列表如果几百条一次性渲染所有卡片页面会卡顿。pageSize10 条起步配合滚动到底部加载更多或者用scroll-view的scrolltolower事件触发page。微信小程序里v-for渲染量控制在 20~30 个以内体验比较流畅超过以后肉眼能感觉到掉帧。4.3 manifest 配置与多端打包差异微信小程序、App、H5 的行为对比uniapp 的跨端能力是拿这个项目的核心卖点但一套代码多端运行在实践中要打折。热词里专门有人问uniapp 开发微信小程序 vs android/ios/鸿蒙的差异选课系统最容易在这几个地方翻车第一uni.request在微信小程序里要求域名必须配置到request合法域名列表而且必须是 https。开发时在微信开发者工具里勾选不校验合法域名可以临时跑但要上线就必须有备案域名和 https 证书。App 端则没有这个限制http 也能请求只要在 manifest 的 App 权限配置里允许明文传输即可。第二uni.showToast的图标在小程序端只有success、error、loading几种传其他值会被忽略建议用icon: none最稳妥。第三存储大小限制不同。微信小程序单个 key 上限 1MBApp 端一般没这限制。选课系统里如果要把课程列表缓存到本地不要往setStorageSync里塞大数组超过 1MB 在小程序端会直接写入失败且不报错表现为缓存了但又好像没缓存。manifest.json 里还有一个mp-weixin的usingComponents配置如果用到了 uni-ui 组件库需要确保easycom规则能匹配到。这属于新手最容易忽略的配置项编译报错组件未找到的时候先去查它。5. 管理后台与 mysql 运维课程管理、选课统计与备份恢复5.1 课程管理界面与前后端交互表格、筛选、状态切换管理后台一般用 Vue Element UI 之类的组件库实现如果项目里用的是若依框架那增删改查的模板几乎开箱即用。选课系统的管理后台核心功能有两个课程管理和选课记录查询。课程管理页面的核心表格列建议课程编号、课程名称、教师、容量、已选人数、状态、操作。操作列的内容要做成动态的——状态为可选时显示停用状态为不可选时显示启用。对应后端两个接口PUT /api/admin/course/{id}/status body: { status: 0/1 } DELETE /api/admin/course/{id} POST /api/admin/course已选人数这一列不要设计成可编辑的它只能由选课和退课操作驱动。如果做成可编辑就会出现后台改了数字、和选课记录对不上的问题最后还得写脚本修复。管理端和后端交互时有一个和 uniapp 端不同的地方管理端通常在浏览器里跑登录态用 cookie 或 localStorage 都行但跨域问题不可忽视。本地开发管理端跑在localhost:5173后端跑在localhost:8080必须配置 CORS 允许跨域否则接口全部被浏览器拦截。后端加一个全局 CORS 配置类就能解决别在每个 Controller 上写CrossOrigin。5.2 mysql 备份与恢复选课数据不能丢要有后悔药选课系统的数据重要程度被严重低估。学生选课结果一旦丢失补选流程会非常麻烦所以我强烈建议从第一天就做备份。mysql 的备份工具是mysqldump基本用法# 备份整个数据库到指定文件 mysqldump -u root -p your_db_name /backup/select_course_$(date %Y%m%d_%H%M%S).sql # 恢复 mysql -u root -p your_db_name /backup/select_course_20240601_120000.sqlmysqldump导出的 SQL 文件里同时包含建表语句和INSERT数据恢复时先创建数据库再导入即可CREATE DATABASE IF NOT EXISTS your_db_name DEFAULT CHARSET utf8mb4; USE your_db_name; SOURCE /path/to/backup.sql;这里有个常见坑如果原表用了utf8mb4字符集导入前必须把数据库和表的字符集保持一致否则中文课程名会变成乱码。mysqldump默认带--default-character-setutf8mb4参数导出的文件恢复时也要加上这个参数执行mysql --default-character-setutf8mb4 -u root -p your_db_name backup.sql定时备份可以用 cron 实现选课高峰结束后做一次手动备份平时一天一次就够了。Linux 环境下 cron 配置示例0 2 * * * mysqldump -u root -pyourpass your_db_name /backup/select_$(date \%Y\%m\%d).sql 2 /backup/backup.log注意 cron 命令里的%要转义否则会报错。Windows 上可以用任务计划程序调用 mysqldump原理一样。5.3 mysql 连接池与 Linux 部署生产环境必调的三个参数标题里带了 mysql说明数据库不是只用来存点数据还要能支撑真实选课场景。移动端选课最怕后端服务上手即崩连接池参数是第一个坑。Spring Boot 默认的连接池是 HikariCP配置在application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/select_course?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 connection-test-query: SELECT 1maximum-pool-size不是越大越好。选课系统并发高但如果这个值设成 200后端一启动就可能把 mysql 的连接数打满反而拖慢所有查询。一般建议 20~50 之间配合 mysql 的max_connections一起看。连接池满了以后请求会排队等待connection-timeout控制的是等待超时设 30 秒比较合理如果设太短高峰时会出现大量连接超时报错。mysql 服务端有几个参数和选课场景强相关# my.cnf 关键配置 max_connections 500 innodb_buffer_pool_size 1G innodb_flush_log_at_trx_commit 2innodb_flush_log_at_trx_commit默认是 1意味着每次事务提交都要刷盘数据最安全但性能最慢。选课高峰时如果有冲库压力可以临时改成 2性能提升明显代价是断电时最多丢失 1 秒的事务日志。生产环境如果不是银行级业务2 是常见的折中选择。热词里搜mysql 安装教程linux 安装 mysql的人很多这里提一个常见问题ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock本质是 mysql 服务没启动或 socket 路径不对。先systemctl status mysql看服务状态再用mysql -h 127.0.0.1 -P 3306 -u root -p走 TCP 连接绕开 socket这两个操作能解决大部分连不上的场景。6. 常见问题排查移动端、后端、数据库的三类翻车现场6.1 微信开发者工具里打开提示不在以下 request 合法域名列表中现象uniapp 编译到微信小程序后在开发者工具中请求后端接口控制台报错提示不在以下 request 合法域名列表中。原因微信小程序平台强制要求uni.request的请求地址必须是在小程序后台配置过的合法域名开发阶段如果没有做任何配置直接用局域网 IP 或 localhost就会被拦截。解决开发阶段在微信开发者工具的详情-本地设置里勾选不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书。调试完成后正式发布前必须在小程序管理后台配置request合法域名且必须 https。一个更彻底的做法是部署时用 nginx 做反向代理把/api路径转发到后端服务这样小程序端只需要配置一个域名后端 IP 变化时不需要重新发版。6.2 多端登录后偶发 401App 端正常但小程序端频繁掉线现象同样的账号在 App 端能稳定保持登录态微信小程序端用一会儿就跳回登录页。原因小程序端 token 存储失效或请求头拼写错误。uni.getStorageSync在小程序里是同步且可靠的但如果请求封装里header的 key 写成了小写authorizationSpring 的getHeader(Authorization)对大小写有要求就会导致后端拿不到 token直接按未登录处理。解决统一在request.js里用标准的Authorization写法后端如果用的 Spring Security 或 JWT 拦截器确认大小写匹配。另外检查小程序端的 token 是否有过期时间逻辑前后端时钟不一致时用数据库的expire_time做判断不要依赖客户端本地时间。6.3 选课高峰时大量请求报课程已满但明明还有名额现象并发压测时容量还有剩余但接口大量返回课程已满。原因乐观锁设计生效了。多个请求同时读到同一个version第一个成功更新后面的更新影响行数为 0业务代码抛出异常返回课程已满。这是业务提示不够准确的问题——用户看到已满以为没名额了实际上可能是并发冲突。解决把提示改成操作冲突请重试或者该课程选课人数较多请刷新后再次尝试。选课系统里这个文案细节很重要直接影响用户体验。如果要减少这种冲突可以在前端把课程的selectedCount展示为接近容量时就置灰按钮后端也可以在容量剩余较少时提前锁定选课入口。6.4 mysql 导入 sql 文件后中文乱码现象source导入一个 sql 文件后课程表里中文课程名变成???。原因sql 文件的字符集和执行终端字符集不一致。如果文件是utf8编码的但 mysql 客户端连接默认用latin1导入时就把中文按错误字符集解析了。解决执行导入前先设置客户端字符集SET NAMES utf8mb4; SOURCE /path/to/schema.sql;或者在命令行导入时用--default-character-setutf8mb4。打开 sql 文件确认第一行有没有SET NAMES utf8mb4没有的话补上。用 Navicat 等图形化工具导入时同样在高级-编码里选 utf8mb4。6.5 移动端真机预览连不上后端接口电脑上却正常现象H5 端在浏览器里跑能通用手机扫码预览 uniapp 的 H5 版接口全部超时。原因BASE_URL配的是localhost:8080手机访问localhost指向的是手机自己不是电脑。局域网真机调试必须用电脑的局域网 IP。解决先查电脑局域网 IPipconfig或ifconfig把请求封装里的BASE_URL改成http://192.168.x.x:8080/api。同时确保手机和电脑在同一 Wi-Fi 下Windows 防火墙开放 8080 端口。另外后端服务监听地址如果写的是127.0.0.1局域网也访问不了Spring Boot 需要监听0.0.0.0——在application.yml里写server.address: 0.0.0.0或用运行参数--server.address0.0.0.0。7. 进阶技巧把选课系统从能跑变成抗压7.1 用 Redis 做选课计数缓存mysql 数据最终落库如果选课高峰的并发量预测比较大一个务实做法是在 redis 里维护课程容量和已选人数选课接口先扣减 redis 计数成功后再异步写 mysql。这样 mysql 的压力被削峰选课体验也更快。实现思路并不复杂课程列表接口把capacity和selectedCount同步到 redis选课时用DECR扣减DECR返回值小于 0 说明超额需要INCR补回去并拒绝。注意 redis 的数据结构要用String而不是Hash因为DECR只支持String自减。异步落库可以用 Spring 的Async或者 MQ这里不展开但原理是先扣缓存后写库最终一致。7.2 用 curl 写一个并发压测脚本验证超卖是否真的被拦住写完乐观锁之后很多人不敢确定到底有没有用毕竟人不可能在选课瞬间开一百个手机。用curl可以模拟并发请求写一个简单的 bash 脚本#!/bin/bash for i in $(seq 1 30); do curl -s -X POST http://127.0.0.1:8080/api/course/select \ -H Content-Type: application/json \ -H Authorization: Bearer test_token_$i \ -d {\courseId\: 1, \userId\: $i} done wait echo done脚本用把 30 个请求同时发出去模拟 30 个学生抢同一门课。后端如果严格校验 token就用压测工具比如 JMeter 或 ab 来做。验证标准很简单压测后查t_select_record里的记录数必须等于t_course.selected_count的增量而且不允许超过capacity。7.3 部署形态nginx 托管前端静态资源与 api 反向代理最终的部署形态我建议这样mysql 一台或直接复用后端所在机器Spring Boot 后端一个进程管理后台和 uniapp 的 H5 构建产物让 nginx 托管。nginx 配置里把/api路径代理到后端端口server { listen 80; server_name your-domain.com; # uniapp H5 打包产物 location / { root /var/www/select-course-h5; try_files $uri $uri/ /index.html; } # 管理后台 location /admin/ { alias /var/www/admin-web/; try_files $uri $uri/ /admin/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; } }这里的try_files是前端路由 history 模式的关键不加它刷新页面时 404。学生端如果只打包成微信小程序nginx 只需要托管管理后台后端接口同样走/api反代。这样部署还有个好处小程序端配置域名时只需要填一个域名不用暴露后端端口安全性也更好。移动端真机调试时如果用 H5 打包产物记得把BASE_URL换成正式域名而不是局域网 IP否则发版后手机一换网络就挂。uniapp 的manifest.json里可以分别配置 H5 和小程序的请求地址我一般会写一个小工具函数按process.env.NODE_ENV判断避免手动改代码。这套项目做下来我最深的体会是选课系统表面是 CRUD真正值钱的部分全在并发和数据一致性上。把乐观锁、事务边界、连接池参数和部署反代这几个点想清楚哪怕业务页面简单一点整个系统的完成度也会上一个大台阶希望这些经验帮到你。本文还有配套的精品资源点击获取
返回列表