简介:这份数据库应用课程设计资源面向高校计算机相关专业学生与Web开发入门者,围绕教室管理系统这一典型课程大作业场景,提供从数据库建模到前后端实现的完整实践素材。压缩包共39个文件、约1.94MB,以8个jsp页面文件、4个css样式与4个js脚本为主体,配套Java源文件、classpath与project工程配置,以及mdf、ldf数据库数据与日志文件,另有docx设计文档和字体、图标等静态资源,构成可直接导入IDE运行的Web项目骨架。资源已有2113人学习下载,说明其在同类课程设计中具备较高参考价值。读者可据此对照E-R概念设计、关系模式与表结构定义,理解MVC分层下视图与控制逻辑的组织方式,并结合SQL查询、索引优化与登录权限校验等环节,完成课程报告撰写与系统二次开发,也可作为数据库应用综合训练的自学范本。
1. 一份数据库课程设计,为什么值得单独拆开看
上学期帮 A 同学救一个数据库课程设计,压缩包解压出来是个教室管理系统的 Web 大作业,他卡在「预约时段重叠」的 SQL 上三天没有推进。这大概是数据库课程设计最典型的形态:需求本身不复杂,但表结构、约束、事务、索引这些关键内容,全藏在几行 SQL 里。这份资源就是一个完整可跑的教室管理系统,前面是浏览器里的排课、预约、审批页面,后面是 MySQL 里六张互相关联的表,覆盖了 Web 大作业从建库到联表查询的主要考点。适合正在赶数据库课程设计的本科生,也适合想拿一个「整体规模不大但各模块齐全」的 CRUD 项目练手 Spring Boot + MyBatis 的开发者。跑通大概半小时,读懂那几段 SQL 需要两小时,改造成自己能交的版本,按下面的路子走。
2. 技术栈与表结构:教室管理系统的骨架是怎么搭的
2.1 需求边界:这个系统到底管几件事
教室管理系统听起来简单,真排需求的时候会发现边界很容易含糊。什么算「占用」、临时借用算不算排课、教室坏了之后还能不能预约,这些问题不先定清楚,写到一半就会返工。这份课程设计把范围收得很克制,只做四件事:教室台账,记录哪个楼哪个房间、能坐多少人、有没有投影和空调;排课占用,记录哪个班的哪门课、在周几第几节、用哪间教室;临时预约,社团活动、补课、考试借用都走这条路;报修登记,教室设备坏了谁报的、处理到哪一步。
四件事对应四类使用者。学生只做两件:查空教室、提交预约。教师看自己的课表和名下学生的预约。教务管理员维护教室和课程,审批预约。系统管理员管账号。这种按角色切分权限的设计在课程设计里几乎是标配,好处是权限表可以做得简单:用户表加一个role字段就够,不需要复杂的 RBAC 五张表,答辩时也讲得清。
我一般建议学生在动手写代码之前,先把这份边界写成一张纸的用例表,标清楚每个用例对应哪张表、读还是写。这一步花二十分钟,能省掉后面两天改表结构的痛苦。教室管理这类系统的通病是「删教室」——直接DELETE会撞外键,所以t_classroom里必须留一个status字段做软删除,0 正常、1 停用,删的时候只改状态。这个坑后面第 5 章还会再说一次。
2.2 技术选型:Spring Boot + MyBatis + MySQL 三件套
后端是 Spring Boot 2.7 + MyBatis + MySQL 8,JDK 8,Maven 构建。持久层选 MyBatis 而不是 JPA,理由很实际:答辩的时候老师会问「你这个查询怎么写的」,用 MyBatis 你可以直接把 XML 里的 SQL 摊开讲,用 JPA 生成的 SQL 反而解释不清。课程设计的评分点本来就偏向数据库设计,选一个能把 SQL 摆在明面上的框架更合适。
前端没有做前后端分离,用的是 Thymeleaf 模板加 Layui 静态资源,登录后的列表页、表单页都是服务端渲染。这个选择对课程设计极其友好:一个java -jar起来,浏览器直接访问,不用再单独起一个 Node 服务,演示的时候也不会因为前端环境没装好而失败。代价是页面交互比较传统,但对一个要交作业的项目来说,能在任何一台装了 JDK 和 MySQL 的机器上十分钟内跑起来,比交互炫酷重要得多。
目录结构大致是这样,认准几个关键位置就不会迷路:
classroom-management/ ├── src/main/java/com/demo/room/ │ ├── ClassroomApplication.java # 启动类 │ ├── config/ # 拦截器、跨域、MyBatis 配置 │ ├── controller/ # 按角色分包:admin / teacher / student │ ├── service/ + service/impl/ # 业务逻辑,事务加在这一层 │ ├── mapper/ # 接口,方法名和 XML 的 id 一一对应 │ ├── entity/ # 与表一一对应的实体 │ └── common/ # 统一返回体、异常、常量 ├── src/main/resources/ │ ├── application.yml │ ├── mapper/ # 所有 XML 放这里,别和接口混在 java 目录 │ ├── templates/ # Thymeleaf 页面 │ └── static/ # layui、css、js └── sql/ ├── schema.sql # 建表语句 └── data.sql # 初始数据,含默认账号有一点值得单独提醒:mapper的 XML 放在resources/mapper下,application.yml里配mybatis.mapper-locations: classpath:mapper/*.xml。新手最常见的错误是把 XML 和 Java 接口放同一个包里,本地 IDE 能跑,打成 jar 之后路径就变了,直接在演示机器上出现问题。
2.3 六张表:外键、状态位和那几个容易漏的字段
整个系统的数据模型是六张表:t_user、t_classroom、t_course、t_schedule、t_reservation、t_repair。前四张是基础数据,后两张是业务流程,流程表通过外键挂在基础表上。
| 表名 | 作用 | 关键字段 | 关联 |
|---|---|---|---|
| t_user | 账号与角色 | username、password、role、class_id | 被预约/报修引用 |
| t_classroom | 教室台账 | room_no、building、capacity、type、status | 被排课/预约引用 |
| t_course | 课程字典 | course_name、teacher_id、semester | 被排课引用 |
| t_schedule | 固定排课 | course_id、classroom_id、week_day、start_section、end_section、week_range | 外键指向课程和教室 |
| t_reservation | 临时预约 | classroom_id、user_id、use_date、start_time、end_time、status、auditor_id | 外键指向教室和用户 |
| t_repair | 报修记录 | classroom_id、reporter_id、description、status | 外键指向教室和用户 |
两个最容易漏的字段,我拎出来说。第一个是t_reservation.status,它是整个审批流程的状态机载体,0 待审批、1 已通过、2 已驳回、3 已取消。没有它就做不了「已取消的预约应该释放时段」这个逻辑,冲突校验会误判。第二个是t_schedule.week_range,存的是「1-16」这种周次范围字符串,表示这门课只在第 1 到 16 周上。很多同学图省事只存一个week_day,结果单双周课程完全没法表达。
关键的建表语句大概长这样,注意时间字段用TIME而不是VARCHAR:
CREATE TABLE t_reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, classroom_id BIGINT NOT NULL COMMENT '教室ID', user_id BIGINT NOT NULL COMMENT '申请人', use_date DATE NOT NULL COMMENT '使用日期', start_time TIME NOT NULL COMMENT '开始时刻', end_time TIME NOT NULL COMMENT '结束时刻', purpose VARCHAR(200) NOT NULL COMMENT '用途', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审批 1通过 2驳回 3取消', auditor_id BIGINT NULL COMMENT '审批人', audit_time DATETIME NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_room_date (classroom_id, use_date, status), -- 冲突校验的命门 CONSTRAINT fk_res_room FOREIGN KEY (classroom_id) REFERENCES t_classroom(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教室预约';idx_room_date这个联合索引不是可选项。冲突校验的 SQL 永远是按「教室 + 日期 + 状态」三段过滤,没有这个索引,几千条数据就开始明显变慢;有了它,查询基本走索引,答辩演示时翻页也是瞬间出结果。索引列的顺序也有讲究,区分度高的classroom_id放最左,status放最右,因为它的取值只有四种,选择性最低。
3. 环境搭建与跑通:从建库到浏览器打开首页
3.1 建库导数据:字符集和时区先定死
第一步永远是建库。字符集用utf8mb4而不是utf8,因为后者在 MySQL 里其实是utf8mb3,存不了 emoji 和部分生僻字,教室里带个「㙟」字的名字就能把插入搞挂,这种问题在答辩现场出现会很难看。
CREATE DATABASE classroom_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE classroom_db; SOURCE /path/to/sql/schema.sql; -- 建表 SOURCE /path/to/sql/data.sql; -- 初始数据:三个角色各一个账号data.sql里会预置三个账号,学生、教师、管理员各一个,密码是加密后的串,登录页面上有提示。这里有个细节:密码字段不要存明文,课程设计里用 MD5 加固定盐也算及格,用 BCrypt 更好,BCryptPasswordEncoder.matches()一行就能验证。存明文的项目在答辩时被问一句「你这个密码怎么存的」就很被动了。
如果不想用命令行,用 Navicat 或者 DataGrip 右键「运行 SQL 文件」也行,注意执行顺序必须是先 schema 后 data,因为 data 里有外键引用,反过来会报Cannot add or update a child row。
3.2 配置连接:连接串里三个参数决定能不能连上
application.yml是第二个容易出问题的地方。很多同学照着网上的模板抄,抄完发现不是连不上就是乱码,问题基本都在连接串的参数上。
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/classroom_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你的密码 thymeleaf: cache: false # 开发阶段关掉模板缓存,改页面不用重启 prefix: classpath:/templates/ suffix: .html mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.demo.room.entity configuration: map-underscore-to-camel-case: true # room_no 自动映射到 roomNo四个参数逐个解释一下。useUnicode=true&characterEncoding=utf8保证连接层不乱码;serverTimezone=Asia/Shanghai是 MySQL 8 的必需项,少了它经常报The server time zone value is unrecognized,或者查出来的时间莫名差 8 小时;useSSL=false在本地关掉省事;allowPublicKeyRetrieval=true是 MySQL 8 默认认证方式caching_sha2_password需要的一个开关,不开会报公钥检索失败。
map-underscore-to-camel-case: true这一行看着不起眼,但省掉大量手写resultMap的工作。t_classroom表的room_no会自动映射到Classroom.roomNo,不用再一个个写<result column="room_no" property="roomNo"/>。
3.3 启动自检:从登录页到第一张列表
配置改完,mvn clean package -DskipTests打包,java -jar target/xxx.jar启动。控制台出现Started ClassroomApplication in 3.2 seconds并且 Tomcat 起了 8080 端口,说明服务本身没问题。这时候别急着用浏览器,先做三步自检,能少走很多弯路。
第一步,浏览器访问/login,页面能出来说明静态资源和 Thymeleaf 模板路径没问题。第二步,用管理员账号登录,能跳转到教室列表说明数据库连通、MyBatis 的 XML 被扫描到了。第三步,打开教室列表后随便按一个条件筛选,能出结果说明动态 SQL 条件拼装正确。这三步分别对应静态资源、持久层、业务层三条链路,卡在哪一步基本就能定位到哪一层。
启动时如果报Invalid bound statement (not found),九成是 XML 没被打进 jar,或者mapper-locations路径写错,去target/classes/mapper/底下看一眼文件在不在就清楚了。
4. 核心模块拆解:一个预约请求从进入到落库的完整链路
4.1 登录与角色拦截:别只靠前端藏按钮
课程设计里权限做不严是最常见的扣分点。页面上的按钮按角色隐藏了,但地址栏直接敲/admin/room/list一样能进去,这种「前端权限」在老师眼里等于没做。正确做法是加一个HandlerInterceptor,在服务端做一次拦截。
// config/LoginInterceptor.java public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); // 未登录统一踢回登录页,注意带上 contextPath if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } // 管理员路径做二次角色校验,防止学生直接敲 URL 进后台 String uri = request.getRequestURI(); if (uri.startsWith("/admin") && !"ADMIN".equals(user.getRole())) { response.sendError(403, "无权访问"); return false; } return true; } }拦截器写完后必须在WebMvcConfigurer里注册,并且用excludePathPatterns放行/login、/logout、/static/**,否则登录页自己也过不去,会陷入死循环跳转。这个坑非常经典:注册完拦截器登录页白屏或者报重定向次数过多,回来检查一下放行名单就行。
登录校验本身不复杂,用 MyBatis 按username查一条记录,然后把提交的明文密码和库里的哈希比对。比对成功后把整个User对象塞进 session,后续所有页面从 session 里取当前用户,不需要每次请求都查库。
4.2 预约冲突校验:一行 SQL 决定整个模块成不成立
整个系统最核心的一段逻辑就是时段重叠判定。它的原理不难,但写法上差异很大,写错了会出现「明明没人用却提示冲突」或者更糟的「两个人都约上了同一间教室」。
判断两个时间区间是否重叠,标准写法是A.start < B.end AND A.end > B.start,而不是BETWEEN。原因很简单:BETWEEN只能处理「被包含」的情况,无法处理「包含」和「部分交叉」。比如已有预约是 8:00-12:00,新预约 9:00-10:00 用BETWEEN能判出来,但新预约 7:00-13:00 就漏了。
<!-- resources/mapper/ReservationMapper.xml --> <select id="countConflict" resultType="int"> SELECT COUNT(1) FROM t_reservation WHERE classroom_id = #{classroomId} AND use_date = #{useDate} AND status IN (0, 1) <!-- 只有待审批和已通过占时段 --> AND start_time < #{endTime} <!-- 注意 XML 里要转义 < > --> AND end_time > #{startTime} </select>这段 SQL 有三个地方值得逐条说明。第一,status IN (0, 1)只把「待审批」和「已通过」算作占用,已驳回和已取消的必须排除,否则用户取消预约后时段永远释放不出来。第二,<和>是 XML 转义,直接写<会被 XML 解析器当成标签开头,报SAXParseException。第三,参数全部用#{},不要用${},后者是字符串拼接,会被 SQL 注入。
光校验预约表还不够,固定排课的时段也得算进去,否则用户可以约到正在上课的教室里。这一段用week_day加节次的区间判断:
<select id="countScheduleConflict" resultType="int"> SELECT COUNT(1) FROM t_schedule WHERE classroom_id = #{classroomId} AND week_day = #{weekDay} AND start_section <= #{endSection} AND end_section >= #{startSection} AND semester = #{semester} </select>两次校验都通过才允许写库,整个方法包在事务里。这里有一个容易被忽略的点:两次查询和一次插入之间理论上存在并发窗口,两个用户同时提交可能都通过校验。课程设计层面用@Transactional加数据库默认的REPEATABLE READ基本够用,真要做到严格互斥,得靠唯一索引或者SELECT ... FOR UPDATE锁住教室行,这一点在第 6 章展开。
4.3 动态 SQL 与空教室查询:条件拼装怎么写才不出错
学生最常用的页面是「查空教室」,条件有楼栋、容量、教室类型、日期、节次,用户可能只填其中一两个。这种场景必须用 MyBatis 的动态 SQL,不然就得在 Java 里拼字符串,可读性和安全性都差。
<select id="selectFreeRooms" resultType="com.demo.room.entity.Classroom"> SELECT c.* FROM t_classroom c WHERE c.status = 0 <if test="building != null and building != ''"> AND c.building = #{building} </if> <if test="minCapacity != null"> AND c.capacity >= #{minCapacity} </if> <if test="roomType != null and roomType != ''"> AND c.type = #{roomType} </if> <!-- 排除已被预约的 --> AND c.id NOT IN ( SELECT classroom_id FROM t_reservation WHERE use_date = #{useDate} AND status IN (0,1) AND start_time < #{endTime} AND end_time > #{startTime} UNION SELECT classroom_id FROM t_schedule WHERE semester = #{semester} AND week_day = #{weekDay} AND start_section <= #{endSection} AND end_section >= #{startSection} ) ORDER BY c.building, c.room_no </select><if>的判断条件必须同时判null和空串。只判null的话,前端传过来一个building=""会拼出AND building = '',结果永远为空,页面看着像「没有空教室」,实际是 SQL 拼错了。UNION在这里比OR更清晰,两个子查询各自走各自的索引,执行计划也好读。
分页用 PageHelper 最省事,PageHelper.startPage(pageNum, pageSize)放在查询前一行,返回的PageInfo里直接带total和list。手写分页的话要记住 count 查询和 list 查询的WHERE条件必须完全一致,否则会出现「列表 5 条、总数显示 200」这种数据对不上的情况,第 5 章会细说。
4.4 审批流转:状态机加乐观锁
预约提交后进入待审批状态,管理员在后台处理。这个环节看着只是改一个字段,实际是整个系统里并发问题最集中的地方——两个管理员同时打开同一条待审批记录,都点了「通过」,如果没有防护,审批人会互相覆盖。
@Service public class ReservationServiceImpl implements ReservationService { @Override @Transactional(rollbackFor = Exception.class) public void audit(Long reservationId, Long auditorId, int result, String remark) { Reservation r = reservationMapper.selectById(reservationId); if (r == null) { throw new BizException("预约记录不存在"); } // 只有待审批状态才允许处理,已通过/已驳回的直接拒绝 if (r.getStatus() != 0) { throw new BizException("该预约已处理,不能重复审批"); } // 乐观锁:WHERE 带上旧状态,影响行数为 0 说明被别人抢先改了 int rows = reservationMapper.updateStatus(reservationId, 0, result, auditorId, remark); if (rows == 0) { throw new BizException("审批冲突,请刷新后重试"); } } }<update id="updateStatus"> UPDATE t_reservation SET status = #{result}, auditor_id = #{auditorId}, audit_time = NOW() WHERE id = #{reservationId} AND status = #{expectStatus} <!-- 乐观锁的关键:旧状态作为条件 --> </update>@Transactional(rollbackFor = Exception.class)里的rollbackFor不能省。Spring 默认只在遇到RuntimeException时回滚,自定义的BizException如果继承了Exception而不是RuntimeException,抛出去之后事务照样提交,数据就脏了。这个细节我在别人的项目里见过不止一次。
状态流转的合法性也要在服务层卡死:0 → 1、0 → 2允许,1 → 3(已通过后取消)也允许但要记录操作人,2 → 1直接拒绝。把这几个转换写成一个Map<Integer, Set<Integer>>常量表,比散落在各个if里好维护得多。
5. 避坑与排查:跑不起来先看这五处
5.1 启动阶段:连不上库和页面 404
现象一:启动报Access denied for user 'root'@'localhost'。原因通常不是密码错了,而是 MySQL 8 的root默认只允许localhost走 socket 认证,而 JDBC 走的是 TCP。解决办法是用 root 登录后执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';然后FLUSH PRIVILEGES;,或者干脆新建一个业务账号,只授权classroom_db库,权限最小化,也更符合课程设计文档里写的安全设计。
现象二:登录页能打开,一登录就 500,日志里是Invalid bound statement (not found)。这是 XML 没被加载。先确认target/classes/mapper/目录下有没有对应的 XML;再确认 XML 里的namespace写的是 Mapper 接口的全限定名,少一个包名就绑不上;最后确认接口方法名和 XML 的id完全一致,包括大小写。
现象三:本地 IDE 里跑得好好的,打成 jar 之后页面 404。Thymeleaf 模板放在src/main/resources/templates下才能被打进BOOT-INF/classes/templates,如果放到了webapp或者项目根目录,IDE 因为工作目录的关系能找到,jar 里就找不到了。判断方法很简单:jar tf target/xxx.jar | grep templates,看看你的页面在不在里面。
5.2 数据与 SQL 层面:冲突误判和分页错乱
现象四:明明没人用的时段,提交预约却提示「教室已被占用」。原因有两个方向。一是冲突 SQL 里status过滤条件写错了,把3(已取消)也算进占用,导致取消过的时段永远释放不出来;二是时间字段建成了VARCHAR,'9:00' < '10:00'这种字符串比较在位数不齐的时候会出错,比如'9:00' > '10:00'是true,因为字符'9'大于'1'。检查方法是在 MySQL 里直接跑那条countConflict,把参数换成具体值看返回几,如果是 1 就去查是哪条记录占的,一下就能定位。
现象五:列表页只显示 5 条,但分页控件显示总共 200 条,翻到第 3 页就空了。这是手写分页时 count 查询和 list 查询的WHERE条件不一致导致的。典型场景是 list 查询里有NOT IN (SELECT ...)排除已占用的教室,而 count 查询里忘了加这段,总数就被算多了。用 PageHelper 的话这个问题会自动规避,因为它基于原 SQL 生成 count 语句;手写的话就把条件抽成一个<sql id="baseWhere">片段,两处都<include refid="baseWhere"/>,从根上避免重复。
现象六:想删一间停用的教室,报Cannot delete or update a parent row。这是外键约束在起作用,t_reservation或t_schedule里还有引用。正确做法根本不该物理删除,教室是历史数据,删了之后往期预约查出来就是空的。改成软删除,把status置为 1,列表查询默认加WHERE status = 0,导出报表时再放开。这一点我在第 2 章提过一次,实际踩坑率很高。
提示:排查这类问题最快的办法不是看 Java 日志,而是打开 MyBatis 的 SQL 打印。在
application.yml里把com.demo.room.mapper包的日志级别调成debug,控制台会输出实际执行的 SQL 和参数值,复制到数据库客户端里跑一遍,问题立刻现形。
6. 二次开发:把课程设计改成能拿得出手的东西
这份代码跑通只是起点,真要交上去或者写进简历,还得动几刀。按投入产出比排序,我建议从下面几个方向改:
| 改造点 | 具体做法 | 难度 | 收益 |
|---|---|---|---|
| 冲突校验加固 | 用唯一索引或时间片表替代应用层判断 | 中 | 高,能讲并发 |
| 空教室查询加缓存 | 按「日期+节次」做 Redis 缓存,预约成功即失效 | 中 | 中,能讲缓存一致性 |
| 报表统计 | 按楼栋/时段统计教室使用率,写一条 GROUP BY | 低 | 高,答辩加分项 |
| 操作日志 | AOP 切面记录谁在什么时候改了哪条记录 | 中 | 高,体现工程意识 |
| 接口化 | 抽出一层 REST API,前端换 Vue | 高 | 视目标而定 |
我最推荐先做冲突校验的加固,因为它是这个项目里唯一有真正技术含量的地方,也是面试官最可能追问的点。前面说的应用层「先查再插」在并发下是有窗口的,真正的解法是把时间切成固定粒度的片,比如以 30 分钟为单位,一门课 8:00-9:40 就占 8:00、8:30、9:00、9:30 四个片,建一张t_room_slot表,(classroom_id, use_date, slot_index)建唯一索引。插入冲突时数据库直接抛DuplicateKeyException,捕获后返回「时段已被占用」,并发问题从根上消失了。
// 时间片占用:靠数据库唯一索引兜底,比应用层判断可靠 @Transactional(rollbackFor = Exception.class) public void occupy(Long roomId, LocalDate date, LocalTime start, LocalTime end, Long reservationId) { List<Integer> slots = SlotUtils.split(start, end); // 30 分钟一片,向上取整 for (Integer slot : slots) { try { slotMapper.insert(roomId, date, slot, reservationId); } catch (DuplicateKeyException e) { // 任意一片插入失败,整个事务回滚,之前的片自动释放 throw new BizException("时段 " + slot + " 已被占用,请重新选择"); } } }这段代码的关键在于异常被捕获后必须继续往外抛,让@Transactional感知到失败并回滚。如果吞掉异常继续循环,前面的片就留在库里了,反而制造了幽灵占用。改完之后记得在论文或者设计文档里补一张时间片表的 ER 图,这才是真正的「数据库设计」,比堆一堆 CRUD 页面有说服力。
验证方法也简单:开两个浏览器窗口,用两个学生账号同时提交同一间教室同一时段,一个成功一个必须失败。这一步走通了,说明你的方案在并发下站得住。
从那以后我每次拿到这种课程设计类的项目,都强制先做一件事:把最核心的那条业务 SQL 单独拎出来,在数据库客户端里用极端参数跑一遍——时间边界相等、状态全是取消、跨天、容量刚好卡线。这几组参数能过,代码基本就稳了。希望帮到你。
本文还有配套的精品资源,点击获取