每年到了这个时间点,私信里问得最多的就是“有没有完整的、能跑的、适合当毕业设计的系统”。这次我把之前做的篮球馆管理系统整套源码翻出来重新整理了一遍,标题里的 44431 是我打包时的归档编号,压缩包里包括后端工程、前端页面、数据库初始化脚本还有一份写好的 README。这个项目能帮你解决的不只是“交一份代码上去”,而是让你在答辩现场真的有事可讲。
先把项目的定位说清楚:它不是什么高并发分布式架构,而是一个典型的、业务规则完整的场地管理系统。用户注册登录、篮球场地浏览、按日期选择场次、在线预约、钱包充值、余额支付、订单核销、后台营收统计,业务链条是闭合的。之所以选篮球馆而不是图书管理、学生管理这类更常见的题目,是因为篮球馆的场地属于典型的稀缺资源,系统里天然存在“同一时间段不能被重复预约”这类约束,这就能引出事务、锁、状态机、接口幂等这些真正有含金量的话题。源码是免费放出来的,但比起直接拿去交作业,我更建议你花半小时把本文的设计思路过一遍,遇到问题时能自己改代码,这才是这份源码最大的价值。
1. 毕设题目不是越新越好:篮球馆管理系统为什么会“经典”
很多人看到管理系统四个字就头疼,觉得这是被做烂了的题目。但我的看法刚好相反:题目本省并不能直接决定你分数的高低,真正拉开差距的是你有没有在系统里做出“业务感”。
图书管理系统、学生信息管理系统这类题目的核心问题在于:它们基本都是静态数据的增删改查,用户表、图书表、借阅表,最多再加一个统计报表。做的时候你很难体现出对业务规则的理解,答辩老师问几句就到底了。而篮球馆管理天然带着一个所有场馆类业务都逃不掉的核心矛盾:空间有限、时间连续、同一个场次只能卖给一个人。
这意味着系统不只是维护场地资料,它必须管理“时间片”这种资源。用户看到的不是一条场地记录,而是“室内全场 18:00-19:00 是否可约”;后台处理的不只是订单记录,还有“支付之后场地状态要跟着变”、“订单取消后场次要释放”、“退款之后账目要能对上”。当这些规则串起来,系统就不再是一堆孤立表格,而是一个有真实逻辑的业务系统。
站在毕业设计的评价维度看,这套系统覆盖的考察点也比较完整,我给你们列一个对照:
| 考察层面 | 系统里的对应内容 |
|---|---|
| 后端基础能力 | Spring Boot 分层架构、MyBatis-Plus 操作数据库 |
| 前端基础能力 | Vue 页面组件、Axios 请求交互、路由权限控制 |
| 数据库设计能力 | 角色表、场地表、场次表、订单表、流水表之间的关系 |
| 业务规则设计 | 场次状态机、订单状态机、预约冲突处理 |
| 工程化意识 | 统一返回结构、全局异常处理、JWT 身份认证 |
| 演示与包装能力 | 模拟收银台、数据可视化统计、演示数据准备 |
这里我要多说一句:每年选管理系统类题目的同学非常多,但大部分人都把精力花在了页面数量上,忽略了业务约束,最后整个系统就是一张表的增删改查。篮球馆管理系统能够成为流传很广的毕设题目,恰恰是因为它在一个看起来很普通的业务场景里藏了足够多的设计点,做好了,这题是有得聊的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先定角色和业务边界,比先写代码重要得多
我每次开始一个这种管理系统项目,第一步不是建 Spring Boot 工程,而是先在纸上把“谁在用这个系统”和“核心业务怎么流转”画清楚。系统做大了不可怕,角色边界模糊才可怕。
2.1 只保留三个角色,为什么是三个
这个系统我没有设计复杂的 RBAC 权限模型,只保留了三个角色:普通用户、前台/场馆管理员、超级管理员。
普通用户做的事情是:注册登录、查看场地和场次、预约下单、充值、支付、查看自己的订单、取消订单。前台/场馆管理员偏向门店视角:维护场地信息、生成每天的可约场次、对用户的订单做核销、处理退款申请。超级管理员关心的是整体运营:管理用户状态、查看所有订单、看营收统计、发布公告。
为什么不是两个角色?因为用户和管理员的视角差别非常大,如果系统只有管理员一个后台,你很难展示“用户能预约”和“管理员能核销”之间的联动。为什么不是四个五个角色?因为权限粒度越细,需要维护的关系表越多,对一个小型毕设系统来说是负担而不是亮点。两个端三种角色,既能说明白权限控制,又不会把自己绕进去。
2.2 核心业务闭环是一条直线
这一步做完之后,系统的业务主流程就很清楚了,一句话就能讲完:管理员维护篮球场地并生成某天的可约场次,用户选择场次下单并完成支付,管理员在订单到达后核销,系统同时记录钱包流水和营收统计。围绕这条主线再去扩展次要点,比如余额充值、公告、场地类型筛选。
我建议你也把业务主流程这样归纳成一句话,因为答辩老师让你介绍系统时,第一句话一定是“你这个系统是做什么的”。你说得越简洁,对方越容易建立画面感。很多人在这一句上就讲砸了,上来就背功能列表,听完什么也没记住。
2.3 技术选型背后的原因
这套系统最终用的是 Spring Boot 2.7 + MyBatis-Plus + MySQL 8 + Vue 3 + Element Plus,权限用的是 JWT,没有引入 Spring Security 的重量级权限框架。
选择 Spring Boot 没什么悬念,它是目前最容易写、资料最多、毕业设计团队里通用性最强的后端框架。MyBatis-Plus 解决的是单表 CRUD 的效率问题,让你把精力放在业务逻辑上,而不是每天写各种重复的 XML SQL。Vue 3 + Element Plus 是目前比较主流的后台前端组合,组件美观度够用,文档也清晰。
对于某些人可能会问的“为什么不用 Spring Security”,我的想法是:这个系统的权限只有登录状态和角色区分两层,用拦截器加注解就能做得很清楚。Spring Security 的过滤器链和配置复杂度会给一个业务并不复杂的系统增加太多噪音,反而不利于你在答辩时把权限模型讲透。等老师问“如果要接入更复杂的权限体系,你会怎么扩展”,你再回答“替换为 Spring Security 并引入角色-权限关联表”,这个思路会显得你对边界有判断力。
3. 场地、场次、订单三张表的设计,决定后面写代码顺不顺利
数据库是管理系统的地基。这个项目里最核心的表其实就三张:场地表 venue、场次表 schedule、订单表 booking_order。把这三张表的关系想清楚,后面的代码能少写一半。
3.1 为什么中间要加一个场次表
刚开始设计时容易犯的错误是把“用户预约”直接存成一条带日期和开始时间的订单记录。比如订单表里直接写 venue_id、book_date、start_time、end_time。这个方案看起来简单,但会带来几个隐患:第一,判断某个场地某天某时段是否可约,你得去订单表里查有没有状态冲突的订单,查询逻辑复杂且容易漏状态;第二,每个场地每天有哪些时段可以开放,没有一个统一维护的地方;第三,管理员想临时关闭某个场次或调整某天某个时段的价格,做起来非常别扭。
正确的做法是在场地和订单之间插入一个场次表。管理员提前为某个场地生成可约场次,比如室内全场 5 月 20 日 18:00-19:00 生成一条 schedule 记录,状态是可约。用户预约时,实际上预约的是这条场次记录,而不是直接在订单里写一个时间范围。
这样设计的好处非常明显:查询可约状态只需要查场次表,订单表只负责记录“谁买了哪条场次”;要关闭某个场次,只要把那条 schedule 的状态置为不可约;要搞优惠价或者节假日调价,也只需要在生成场次时维护价格字段。场次表有点类似电影票里的“场次”概念,或者酒店里的“房态”概念,核心思想是:把资源实例化,让每一个可卖的时间片都变成一条独立的数据。
3.2 订单表里的冗余字段不是浪费
订单表我除了记录 user_id 和 schedule_id 之外,还冗余了 venue_name、venue_type、start_time、price 等字段。有些教科书会说这是“冗余设计”,要去范式化,但在这个场景里我强烈建议保留。
原因很简单:订单是业务快照。如果用户两个月之后来查历史订单,而期间场馆管理员已经把场地名称从“室内全场”改成了“VIP 全场”,你不希望用户看到的旧订单也变成新名字。更关键的是价格,如果今天你以 80 元订了场次,下周场地涨价到 100 元,你的历史订单里的金额也不能跟着变。所以凡是会随着时间变化的业务属性,在订单生成那一刻都应该在订单表里存一份副本,查询时优先展示快照数据。
金额字段一定要用 decimal,不能用 float 或 double,这是支付类相关数据的常识。用浮点数做金额加减会出现 0.1 + 0.2 不等于 0.3 这种问题,在涉及余额和退款的时候会有精度隐患。
下面是我建表时的核心结构:
sql复制CREATE TABLE venue (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
venue_name VARCHAR(64) NOT NULL COMMENT '场地名称',
venue_type TINYINT NOT NULL COMMENT '1-半场 2-全场',
price_hour DECIMAL(10,2) NOT NULL COMMENT '基准单价/小时',
status TINYINT NOT NULL DEFAULT 1 COMMENT '1-正常 0-停用',
create_time DATETIME NOT NULL
);
CREATE TABLE schedule (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
venue_id BIGINT NOT NULL,
open_date DATE NOT NULL COMMENT '可约日期',
start_time VARCHAR(10) NOT NULL COMMENT '18:00',
end_time VARCHAR(10) NOT NULL COMMENT '19:00',
price DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0-可约 1-已约 2-关闭',
UNIQUE KEY uk_venue_date_time (venue_id, open_date, start_time)
);
CREATE TABLE booking_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL COMMENT '订单编号',
user_id BIGINT NOT NULL,
schedule_id BIGINT NOT NULL,
venue_name VARCHAR(64) NOT NULL COMMENT '订单快照',
start_time VARCHAR(10) NOT NULL COMMENT '订单快照',
amount DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已核销 3-已取消',
create_time DATETIME,
pay_time DATETIME,
UNIQUE KEY uk_order_no (order_no)
);
3.3 唯一索引要放在真正需要的位置
场地表里的场地名和 schedule 表里的日期场次组合,都是需要保证唯一的数据。我在 schedule 表上加了联合唯一索引 uk_venue_date_time,用来防止管理员重复生成同一个场次。但要注意,唯一索引和逻辑删除字段放在一起会踩坑,这个后面专门讲。
4. 并发预约的冲突处理:最值得拿出去讲的模块
管理系统最容易被老师追问的地方就是并发。篮球馆的预约场景非常典型:一个场次晚上 18:00-19:00,好几个人同时盯着,谁先提交谁就能约到。如果你用“先查一下该场次是否可约,再插入订单”这种写法,高并发下一定会出问题。
4.1 先查后插的问题到底出在哪
假设用户 A 和用户 B 同时请求预约同一个场次。用户 A 的请求先执行了 select,发现场次状态是“可约”;用户 B 的请求也执行了 select,同样看到“可约”;然后 A 插入订单成功,B 接着也插入订单成功,这个场次就被卖出了两次。
问题出在“检查”和“写入”不是原子操作。它们之间存在一个时间窗口,两个并发请求彼此看不到对方。这种问题只靠加一个 if 判断是解决不了的。
4.2 我的做法:把“预约”变成一个原子更新
我用的是利用数据库行锁实现原子更新的方案。核心逻辑很简单:不先查询再插入订单,而是先尝试把 schedule 表里那条场次记录的状态从未预约改成已预约,这是一个带条件的 update 操作,数据库会通过行锁串行化两个并发请求。
sql复制UPDATE schedule
SET status = 1
WHERE id = #{scheduleId} AND status = 0
这条 SQL 执行的时候,如果影响行数是 1,说明当前场次被你成功抢到;如果影响行数是 0,说明在你之前已经有别的请求把它置为已约了。第二个请求不会看到脏数据,因为它在更新时需要等待第一个事务提交,这是数据库的原子性保证。
后端服务层代码如下:
java复制@Transactional(rollbackFor = Exception.class)
public Long createOrder(CreateOrderReq req) {
Schedule schedule = scheduleMapper.selectById(req.getScheduleId());
if (schedule == null || schedule.getStatus() == 0) {
throw new BizException("场次不存在或已经被预约");
}
// 关键:条件更新,同一时间只有一个请求能成功
int rows = scheduleMapper.occupySchedule(req.getScheduleId());
if (rows == 0) {
throw new BizException("手慢了,该场次刚刚被预约");
}
// 到这里说明场次已经锁定,下面创建订单
BookingOrder order = new BookingOrder();
// 组装订单数据
orderMapper.insert(order);
return order.getId();
}
这里还有一个容易被忽略的点:创建订单和更新场次状态必须在同一个事务里。如果订单创建成功,但场次状态更新提交失败,事务会整体回滚,不会出现“场次被锁但没有订单”的情况。我在 createOrder 方法上加了 @Transactional,保证这两步操作的原子性。
4.3 演示的时候怎么“表演”并发
答辩时想要展示这个问题,你不需要真的搭高并发环境。我当时的做法是:打开两个不同的浏览器(比如 Chrome 和 Edge),各自登录一个测试账号,同时点击同一个场次的“立即预约”按钮。因为两个浏览器是独立的会话,请求会被并发发到后端,这时候大概率只有一个请求能成功,另一个会弹出一个“手慢了”的提示。
如果你想让效果更明显,可以在代码里给 occupySchedule 的 update 操作之前加一个线程睡眠 100 毫秒的调试代码,人为放大两个请求的冲突窗口。当然这只在演示环境可以用,改完记得删掉。
4.4 关于分布式锁的补充想法
有人可能会问,为什么不用 Redis 分布式锁?我的回答是:为了一个预约为主要场景的系统引入 Redis,需要额外部署和维护,而数据库本身的原子更新已经能解决这个并发问题。Redis 分布式锁在真正的高并发、多实例部署场景下才有优势,一个单体应用加 MySQL 的场景,先做数据库层面的正确性才是关键。这个回答如果能在答辩时说出来,会很加分。
5. 订单状态机与余额流水:让系统告别“纯增删改查”
订单状态是这个系统的业务核心。管理系统的很多 bug 不是代码写错,而是状态流转没有定义好,允许用户从一个状态跳到了不该到的状态。
5.1 一条订单的生命周期
我定义了五个订单状态:待支付、已支付、已核销、已取消、已退款。
用户提交预约后先创建待支付订单,支付成功进入已支付;用户到场馆开始打球,前台在后台核销,订单进入已核销;用户在支付前主动取消,订单进入已取消;已支付的订单因为特殊原因需要退,则走已退款。状态必须按这个方向流转,代码里要禁止随意跳转。
表格如下:
| 状态 | 含义 | 可跳转到的状态 |
|---|---|---|
| 0-待支付 | 订单已锁定场次,还没付款 | 1-已支付、3-已取消 |
| 1-已支付 | 付款完成,等待用户到馆使用 | 2-已核销、4-已退款 |
| 2-已核销 | 用户已经使用场地 | 无 |
| 3-已取消 | 支付前取消,场次释放 | 无 |
| 4-已退款 | 支付后退款,场次释放 | 无 |
订单进入已取消或已退款时,必须把对应的场次状态从“已约”改回“可约”,否则这个时间片就永远被锁住了,这也是业务上我最开始容易漏掉的一步。
5.2 超时未支付订单的自动取消
如果用户创建了订单但一直不支付,场次就会被占用,其他人也约不了。我加了定时任务,每两分钟扫描一次超过 15 分钟仍未支付的待支付订单,自动把它置为已取消,同时释放场次。
java复制@Scheduled(fixedDelay = 120000)
public void cancelExpiredOrders() {
List<BookingOrder> expiredOrders = orderMapper.selectList(new LambdaQueryWrapper<BookingOrder>()
.eq(BookingOrder::getStatus, 0)
.lt(BookingOrder::getCreateTime, LocalDateTime.now().minusMinutes(15))
.ge(BookingOrder::getCreateTime, LocalDateTime.now().minusDays(1))
);
for (BookingOrder order : expiredOrders) {
cancelOrder(order.getId());
}
}
这里有一个细节需要注意:定时任务处理的是已经超时的订单,但在它执行的同时,用户可能正好点击了“支付”按钮。为了避免把刚刚支付的订单取消掉,cancelOrder 方法里更新订单状态时一定要带上当前状态条件,比如 update booking_order set status = 3 where id = ? and status = 0,这样即使定时任务和支付请求并发,也只有一个能成功。
5.3 钱包余额与充值流水
用户充值后,余额存在用户表的 balance 字段。这个字段更新我必须强调一件事:任何余额变动都要写流水。用户充值 100 元,余额从 0 变成 100,这个变化要记录到 charge_record;用户下单支付了 80 元,余额变成 20,这个变化要记录到 pay_record 或者同一张 account_flow 表。
为什么要这样设计?因为余额业务最怕的就是对不上账。你给用户加了余额但没写流水,用户说我没充过;用户支付扣了余额但没写流水,后台说这笔钱去哪了。流水表是财务数据的审计日志,哪怕代码里暂时没用到它,也一定要设计进去。这些细节才是一个管理系统区别于学生作业的地方。
我这里把充值和写流水放在同一个事务里:
java复制@Transactional(rollbackFor = Exception.class)
public void recharge(Long userId, BigDecimal amount, String channel) {
// 加余额
userMapper.increaseBalance(userId, amount);
// 写流水
AccountFlow flow = new AccountFlow();
flow.setUserId(userId);
flow.setChangeAmount(amount);
flow.setType(1); // 充值
flow.setCreateTime(LocalDateTime.now());
accountFlowMapper.insert(flow);
}
支付时则要额外校验余额是否充足:先查余额,余额小于支付金额直接抛异常;余额足够扣款并写流水。如果以后要接入真实微信支付宝,再把“模拟支付成功后的回调接口”改成“真实的异步通知解析”,整体改动量很小。
6. 开发过程中踩过的三个坑和完整排查过程
这部分内容是我最想跟你分享的。如果只看业务设计,你会觉得所有东西都应该顺理成章地跑通,但实际上代码落地的时候会遇到一堆和业务无关但能让你卡一整晚的问题。我把开发中印象最深、也最有代表性的三个坑完整记录下来,希望你拿到源码后别在同样的地方浪费时间。
6.1 坑一:日期时间字段 400,报错信息看得懂但不会改
一个很常见的报错现象:前端页面提交新增场次表单,返回 400 Bad Request,浏览器 Network 面板里的响应是:
json复制{
"status": 400,
"error": "Bad Request",
"message": "JSON parse error: Cannot deserialize value of type java.time.LocalDateTime from String \"2024-05-20 08:00:00\""
}
我第一次遇到时觉得莫名其妙,前端传的时间格式明明没有错,为什么后端不认。后来想明白了:Spring Boot 处理 JSON 默认用的是 Jackson,对 LocalDateTime 这种 Java 8 时间类型,默认期望的格式是 ISO 8601 标准格式,也就是类似 2024-05-20T08:00:00 这种带字母 T 的写法,而不是我们平时习惯的带空格的写法。
解决方案比较容易,在需要接收时间的字段上加上格式注解:
java复制@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime startTime;
如果不想每个字段都加,可以在配置类里定义一个全局的 Jackson 时间格式。但为了简单直观,我建议直接在 DTO 字段上加 @JsonFormat。排查这类问题的思路也很固定:先用浏览器发起的最小请求判断是不是后端字段类型不匹配,再去看具体报错信息,最后再决定加注解还是改前端格式。
6.2 坑二:逻辑删除字段和唯一索引打架
这个坑和数据库相关。场地表 venue 用了逻辑删除字段 deleted,MyBatis-Plus 的 @TableLogic 注解在做删除操作时会把 deleted 从 0 改成 1,而不是物理删除行。这个设计本身没问题,问题出在给场地名称加了唯一索引 uk_name 之后。
到项目后期,我发现一个诡异的现象:后台明明已经删除了一个叫“室内全场”的场地,再次新增名字叫“室内全场”的场地时,数据库报 Duplicate entry "室内全场" for key "uk_name"。我代码里明明查过表里没有这个场地了,怎么还会冲突?
查了挺久才意识到:MyBatis-Plus 的查询默认会加 deleted = 0 条件,所以逻辑上你看不到那条数据,但数据库里那条被删除的记录还在,唯一索引也没有失效,于是“室内全场”这个值在索引层面依然存在。新插入的“室内全场”自然就冲突了。
解决方案有两个方向。如果你必须保证某个字段绝对唯一,最简单粗暴的就把数据物理删除,或者逻辑删除时把名字改成带 id 后缀的形式,比如室内全场#18,保证索引值不冲突。如果唯一性不是那么强的数据,比如停车场名称这种,也可以去掉唯一索引,在应用层插入前先按 deleted = 0 条件查一下是否重名。我当时选择了去掉唯一索引加应用层校验,因为系统里并不存在真正的并发新增场地的场景。
这个坑做毕设的时候很常见,因为教程里往往会同时告诉你两个看似都正确的实践:要用逻辑删除、要加唯一索引,但没告诉你会同时用完这两个实践后会产生冲突。学到这个知识比填完这个坑本身更值钱。
6.3 坑三:登录后跨域请求带不上 token,接口一直 401
前端用 Vue 开发服务器是 localhost:5173,后端是 localhost:8080,这属于跨域。用 Axios 发登录请求能成功,但登录之后调用预约接口就一直 401 未授权。打开 F12 的 Network 面板,点开预约请求查看 Headers,发现请求头里根本没有 Authorization 字段,而后端需要通过这个字段去解析用户身份。
这个问题的排查链路其实很清晰:先看前端请求有没有把 token 带上去,再看后端取不取得到。最后发现是 Axios 默认没有把本地保存的 token 放到请求头里,需要在请求拦截器里统一添加:
javascript复制axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
return config;
});
加完这个之后,大部分 401 问题就消失了。但如果你把 axios 的 withCredentials 设为 true,或者在 Spring 后端的跨域配置里设置了 allowCredentials(true),那么跨域配置就不能简单写成 allowedOrigins("*"),因为浏览器会直接拦截这种通配符加 credentials 的跨域响应。需要改成:
java复制registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*");
这一类跨域问题排查的通用顺序是:先看 Network,确认浏览器到底有没有发出预检 OPTIONS 请求、预检会不会失败、真正的业务请求里有没有携带需要的信息,逐层定位。
7. 答辩演示顺序和拿到源码后的使用建议
最后这部分专门写给准备毕业答辩的同学。你以为源码跑起来就完事了,但实际上很多人在答辩现场讲得一团糟,不是因为没有做,而是因为演示没有设计。
7.1 我个人推荐的演示顺序
演示的时候不要在启动页上浪费太多时间。我的顺序是先打开门户首页,快速展示场地列表和用户评论;然后切到某个场地的详情页,点“选择日期”,这个交互能展示日历组件和可约场次的实时状态;此时用一个演示账号登录,选择一个晚上 18:00 的场次,提交预约,弹出模拟收银台界面,确认支付;支付成功之后,立刻切到管理后台,用另一个管理员账号找到这张订单并执行核销;最后回到统计页面,展示今日营收和订单趋势。
这个顺序其实就是用户真实使用路径,评审老师跟着走一遍很容易理解系统价值。需要注意演示数据要提前造好:先由管理员批量生成未来几天的场次,给账号预充一笔余额,否则现场注册、充值、生成场次会显得又慢又乱。
7.2 源码目录结构你得先读懂
很多同学下载了源码却不看目录,被老师问“你的 controller 在哪”就懵了。我用的目录结构是常见的 Maven 分层结构。后端在 controller 层放接口入口,service 层放业务逻辑,mapper 层操作数据库,entity 层放实体。前端则是 views 目录下按模块分页面,api 目录统一封装请求,router 目录配置路由。建议你花一个下午时间,按“登录发起请求到后端控制层再到数据库”这条链路跟踪一遍代码,你就能在答辩时把每个目录说得有理有据。
7.3 老师最常追问的三个问题怎么接
第一个问题肯定是“同一个场次被两个人同时预约了怎么办”。这就是第 4 章讲的并发控制,你回答的时候直接点出 update schedule set status = 1 where id = ? and status = 0 这个条件更新的关键步骤,说明是通过数据库原子更新加事务保证的,基本上就过关了。
第二个问题是“你的支付是真支付吗”。诚实回答是模拟收银台,没有接真实的微信支付宝,可以补充一句由于个人没有商户资质,真实支付申请不了,但代码已经预留了回调接口,等接入真实支付时只需要替换支付方法。不要支支吾吾,大方承认并说明自己的思考就好。
第三个问题是“如果用户没支付就不来了,场地会不会被一直占着”。这就是第 5 章的定时任务扫描待支付订单,你可以把调度周期、超时时间、状态变更条件简单讲出来,再加一句“取消订单的同时会释放场次”,逻辑完整度一下就体现出来了。
一点实在的建议
源码打包时我编的目录号是 44431,这份篮球馆管理系统的完整代码、SQL 文件和说明文档都在里面。但我个人认为,从网上下载项目之后,最值得做的不是急着改论文,而是主动往项目里加一个小功能。比如给半场和全场设置不同的夜间灯光附加费,或者根据用户的累计消费金额自动升级会员等级并享受折扣。加一个小需求的过程,会迫使我重新走一遍建表、写接口、写页面、配置权限的完整链路,这是把所有代码逻辑真正变成自己知识的最快方式。希望这份源码能帮你少熬几个夜,也祝你在答辩那天能底气十足地把系统背后的设计讲清楚。
