news 2026/9/26 8:17:05

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("success"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String msg) { Result<T> 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 Result<Void> 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 Result<Void> 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 Result<Void> 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 class="course-list"> <view v-for="item in courseList" :key="item.id" class="course-card" @click="goDetail(item)" > <view class="course-name">{{ item.courseName }}</view> <view class="course-info"> 教师:{{ item.teacher }} &nbsp; 学分:{{ item.credit }} </view> <view class="course-info"> 已选 {{ item.selectedCount }} / 容量 {{ item.capacity }} </view> <button size="mini" :disabled="item.selectedCount >= item.capacity" @click.stop="selectCourse(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; } } </script>

submitting这个标记是防重复提交的关键。选课接口的响应速度取决于后端事务耗时,用户手速快的话 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 上限 1MB,App 端一般没这限制。选课系统里如果要把课程列表缓存到本地,不要往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.sql

mysqldump导出的 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-set=utf8mb4参数导出的文件,恢复时也要加上这个参数执行:

mysql --default-character-set=utf8mb4 -u root -p your_db_name < backup.sql

定时备份可以用 cron 实现,选课高峰结束后做一次手动备份,平时一天一次就够了。Linux 环境下 cron 配置示例:

0 2 * * * mysqldump -u root -p'yourpass' 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.yml:

spring: datasource: url: jdbc:mysql://localhost:3306/select_course?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 connection-test-query: SELECT 1

maximum-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 = 2

innodb_flush_log_at_trx_commit默认是 1,意味着每次事务提交都要刷盘,数据最安全但性能最慢。选课高峰时如果有冲库压力,可以临时改成 2,性能提升明显,代价是断电时最多丢失 1 秒的事务日志。生产环境如果不是银行级业务,2 是常见的折中选择。

热词里搜"mysql 安装教程""linux 安装 mysql"的人很多,这里提一个常见问题:ERROR 2002 (HY000): Can't 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 多端登录后偶发 401,App 端正常但小程序端频繁掉线

现象:同样的账号在 App 端能稳定保持登录态,微信小程序端用一会儿就跳回登录页。

原因:小程序端 token 存储失效或请求头拼写错误。uni.getStorageSync在小程序里是同步且可靠的,但如果请求封装里header的 key 写成了小写authorization,Spring 的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-set=utf8mb4。打开 sql 文件确认第一行有没有SET NAMES utf8mb4,没有的话补上。用 Navicat 等图形化工具导入时同样在"高级-编码"里选 utf8mb4。

6.5 移动端真机预览连不上后端接口,电脑上却正常

现象:H5 端在浏览器里跑能通,用手机扫码预览 uniapp 的 H5 版,接口全部超时。

原因:BASE_URL配的是localhost:8080,手机访问localhost指向的是手机自己,不是电脑。局域网真机调试必须用电脑的局域网 IP。

解决:先查电脑局域网 IP(ipconfig或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.address=0.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,真正值钱的部分全在并发和数据一致性上。把乐观锁、事务边界、连接池参数和部署反代这几个点想清楚,哪怕业务页面简单一点,整个系统的完成度也会上一个大台阶,希望这些经验帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 8:16:12

从“工具人”到“能自主思考的代理”:LLM Agent核心概念与实操详解

1. 第四章到底在讲什么&#xff1a;Agents从“工具人”到“能自主思考的代理” 我先说说自己拿到这一章时的第一感受&#xff1a;之前几章还在教你如何搭prompt、调参数、把大模型当一个聪明的“问答机器”用&#xff0c;到了第四章&#xff0c;视角彻底变了——从“你告诉模型…

作者头像 李华
网站建设 2026/9/26 8:16:12

浏览器本地运行40MB离线模型:纯网页端自动抠图实战指南

前阵子一个朋友想做自动抠图的小工具&#xff0c;问我要不要把模型部署到服务器上&#xff0c;顺便接个付费API。我直接摇头&#xff0c;告诉他现在有个更省事的做法&#xff1a;一个网页页面&#xff0c;塞一个40MB的离线模型&#xff0c;打开就能自动抠图&#xff0c;不用Pyt…

作者头像 李华
网站建设 2026/9/26 8:14:56

Sketch素材包实战:从解压到组件化,打造可复用的美食APP设计资产

简介&#xff1a;这是一套面向UI/UX设计师、产品经理及移动端设计学习者的美食类APP界面Sketch素材包&#xff0c;可快速用于订餐、菜谱分享或美食推荐类应用的视觉原型与方案演示。压缩包共4个文件&#xff0c;以Sketch源文件为核心&#xff0c;配合HTML格式的设计说明、作者展…

作者头像 李华
网站建设 2026/9/26 8:14:56

后台挂原神真能优化游戏帧率?显卡调度机制实测拆解

在游戏社区里泡久了&#xff0c;你一定听过类似的说法&#xff1a;“后台挂着原神&#xff0c;玩别的游戏反而更流畅了。”第一次听到我是嗤之以鼻的&#xff0c;觉得这又是某种玩家玄学&#xff0c;跟“睡前不关机第二天手机会更快”一样属于心理暗示。直到有一次我开着原神去…

作者头像 李华
网站建设 2026/9/26 8:14:35

轴承寿命预测的时域变换四步法:从振动信号到可建模特征

简介&#xff1a;本资源是一套面向工业物联网与设备健康管理领域的轴承寿命预测MATLAB实践代码包&#xff0c;适用于机械故障诊断初学者、自动化专业学生及从事预测性维护的工程师。资源聚焦轴承振动信号的时域特征提取与寿命建模&#xff0c;涵盖均方根&#xff08;RMS&#x…

作者头像 李华
网站建设 2026/9/26 8:12:30

BT种子与磁力链接解析:从bencode到infohash的字节级还原

1. 这不是“下载教程”&#xff0c;而是一次底层协议解剖手术你点开一个磁力链接&#xff0c;浏览器或下载器几秒内就识别出文件名、大小、做种人数——这个过程快得像魔法。但魔法背后没有咒语&#xff0c;只有清晰、可验证、被全球数千万客户端严格执行的二进制规则。BT种子与…

作者头像 李华