ARTICLE DETAIL

资讯详情

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

Java羽毛球馆管理系统:时段模型与并发控制实战

Java羽毛球馆管理系统:时段模型与并发控制实战 简介这份资源是面向高校计算机专业学生与Java Web开发初学者的羽毛球馆管理系统毕业设计文档围绕场地预约、资源调度与运营效率等实际问题展开适合用作课程设计、毕设参考或Web全栈练手项目。压缩包内共1个docx文件约942KB内容涵盖系统摘要、需求分析、功能模块设计与实现思路涉及用户管理、在线预约、支付订单、提醒通知及统计分析等核心模块并说明了HTML、CSS、JavaScript、Vue.js前端与ThinkPHP、PHP后端及MySQL数据库的技术选型与配合方式。目前已有446人学习下载读者可借此了解一个完整管理系统的架构组织、数据库设计要点与权限控制、性能测试等收尾环节为撰写论文或搭建同类场馆预约平台提供可借鉴的目录结构与实现参考。1. 羽毛球馆管理系统从排场冲突到财务对账Java 能解决哪些真问题周末晚上七点到九点是羽毛球馆的黄金时段也是最容易出乱子的时段。前台用纸质本子记场地电话预约和现场散客的信息对不上同一个场地被卖了两次会员卡余额和实际收款对不上月底盘账差了三百多块教练课时统计靠微信群接龙月底结算时谁也不认账。这些不是管理态度问题是工具问题。一套基于 Java 的羽毛球馆管理系统核心要解决的就是场地时段的状态一致性、会员储值与消费的流水可追溯、以及教练课时的自动归集。它适合中小型球馆的经营者、需要做课程设计的学生以及想拿一个完整业务系统练手的 Java 开发者。热搜里常出现的「java 管理系统」「学生选课管理系统」「房屋租赁管理系统」本质上都是同一类东西——把资源、用户、订单三者的关系用代码管清楚羽毛球馆只是把资源换成了带时间属性的场地。2. 场地时段模型为什么不能只用一张 orders 表2.1 场地、时段、订单三张表的拆分逻辑很多人第一版设计会把所有信息塞进一张订单表场地号、开始时间、结束时间、用户、金额。跑起来才发现问题——查询某天某场地哪些时段空闲需要遍历所有订单做时间区间比对数据量一上来就慢而且「场地维护中」这种状态没地方放。正确的做法是拆成三张核心表场地表存物理信息时段模板表存可售的时间切片订单表存交易记录。时段模板是关键设计。球馆通常按小时售卖但黄金时段和闲时价格不同。我一般会预生成未来 14 天的时段记录每个时段一行状态字段标记「可售 / 已锁定 / 已售出 / 维护中」。这样查询空闲时段就是一条带索引的WHERE status AVAILABLE不需要做时间区间运算。-- 场地表只存物理属性 CREATE TABLE court ( id BIGINT PRIMARY KEY AUTO_INCREMENT, court_no VARCHAR(16) NOT NULL COMMENT 场地编号如 A01, court_type TINYINT DEFAULT 1 COMMENT 1木地板 2塑胶, status TINYINT DEFAULT 1 COMMENT 1启用 0停用, UNIQUE KEY uk_court_no (court_no) ); -- 时段表预生成带唯一约束防重复售卖 CREATE TABLE time_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, court_id BIGINT NOT NULL, slot_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, price DECIMAL(10,2) NOT NULL, status VARCHAR(16) DEFAULT AVAILABLE COMMENT AVAILABLE/LOCKED/SOLD/MAINTENANCE, order_id BIGINT DEFAULT NULL, UNIQUE KEY uk_court_slot (court_id, slot_date, start_time), KEY idx_date_status (slot_date, status) );uk_court_slot这个唯一约束是整个防超卖方案的地基。哪怕应用层并发控制出了漏洞数据库层面也会直接拒绝重复插入同一场地同一时段。idx_date_status覆盖了最高频的查询——按日期查可售时段。2.2 用状态机管住时段流转而不是靠 if-else 堆时段状态不能随便改。可售的才能被锁定锁定的才能被支付确认已售出的只能走退款流程回到可售。如果代码里到处写if (status 1) { ... } else if (status 2) { ... }加一个「教练占用」状态就要改十几个地方。常见做法是用枚举加状态转移表。public enum SlotStatus { AVAILABLE, LOCKED, SOLD, MAINTENANCE; private static final MapSlotStatus, SetSlotStatus TRANSFER Map.of( AVAILABLE, Set.of(LOCKED, MAINTENANCE), LOCKED, Set.of(SOLD, AVAILABLE), SOLD, Set.of(AVAILABLE), MAINTENANCE, Set.of(AVAILABLE) ); public boolean canTransferTo(SlotStatus target) { return TRANSFER.getOrDefault(this, Set.of()).contains(target); } }调用处只需要if (!current.canTransferTo(target)) throw new BizException(时段状态不允许此操作);。加新状态时只改枚举不改业务代码。这就是热搜里「java 设计模式」在真实项目里的落点——不是背概念是让状态变更只有一个入口。2.3 预生成时段的任务怎么写才不出错预生成任务用 Spring 的Scheduled每天凌晨跑一次补齐第 14 天的数据。注意两个坑一是任务重复执行会撞唯一约束要用INSERT IGNORE或先查后插二是价格要按日期类型工作日 / 周末 / 节假日区分不能写死。Scheduled(cron 0 30 1 * * ?) public void generateSlots() { LocalDate target LocalDate.now().plusDays(14); ListCourt courts courtMapper.selectEnabled(); for (Court court : courts) { for (int hour 8; hour 22; hour) { TimeSlot slot new TimeSlot(); slot.setCourtId(court.getId()); slot.setSlotDate(target); slot.setStartTime(LocalTime.of(hour, 0)); slot.setEndTime(LocalTime.of(hour 1, 0)); slot.setPrice(priceRule.calc(target, hour)); slot.setStatus(SlotStatus.AVAILABLE.name()); // INSERT IGNORE 保证幂等 timeSlotMapper.insertIgnore(slot); } } }priceRule.calc里判断target.getDayOfWeek()和节假日表返回对应单价。insertIgnore对应 SQL 的INSERT IGNORE INTO重复执行不会报错也不会产生脏数据。这个任务跑通一次之后日常运营就不需要人工干预了。3. 预约并发控制两个人同时点同一个时段怎么办3.1 乐观锁与悲观锁在时段锁定上的取舍用户 A 和用户 B 同时看到晚上 7 点 A01 场地可售同时点下单。如果代码是「先查状态再更新」两个请求都查到 AVAILABLE都执行更新就超卖了。解决方案有两类悲观锁在查询时就加FOR UPDATE把行锁住另一个请求排队乐观锁在更新时带版本号或状态条件更新影响行数为 0 就说明被别人抢先了。球馆场景下并发量不大但黄金时段开抢瞬间会有几十个请求打进来。我一般用乐观锁因为实现简单且不会长时间持锁。// 乐观锁更新只有状态还是 AVAILABLE 时才更新成功 Update(UPDATE time_slot SET status LOCKED, order_id #{orderId} WHERE id #{slotId} AND status AVAILABLE) int lockSlot(Param(slotId) Long slotId, Param(orderId) Long orderId);调用处判断返回值if (lockSlot(...) 0) throw new BizException(该时段已被预订);。这条 SQL 的AND status AVAILABLE就是乐观锁的核心——数据库保证同一行只有一个事务能更新成功。3.2 锁定后超时释放别让用户占着不放用户锁定时段后去支付如果中途关掉页面时段就永远锁死了。必须加超时释放。常见做法是用 Redis 的过期键或者定时任务扫描。我用定时任务因为不引入额外中间件小项目够用。Scheduled(fixedDelay 60_000) public void releaseExpiredLocks() { // 锁定超过 15 分钟未支付的释放回 AVAILABLE LocalDateTime deadline LocalDateTime.now().minusMinutes(15); ListTimeSlot expired timeSlotMapper.selectExpiredLocks(deadline); for (TimeSlot slot : expired) { timeSlotMapper.releaseLock(slot.getId()); orderMapper.cancelOrder(slot.getOrderId()); } }selectExpiredLocks查的是status LOCKED AND update_time deadline。releaseLock把状态改回 AVAILABLE 并清空 order_id。注意释放和取消订单要在同一个事务里否则可能出现时段释放了但订单还挂着。3.3 支付回调的幂等处理支付平台回调可能重复发送。如果回调接口不做幂等同一笔订单会被确认两次会员卡被扣两次钱。做法是在订单表加唯一约束回调时先查订单状态已支付就直接返回成功。Transactional public void handlePayCallback(String orderNo, String tradeNo) { Order order orderMapper.selectByOrderNo(orderNo); if (order null) throw new BizException(订单不存在); if (PAID.equals(order.getStatus())) { return; // 已处理过直接返回保证幂等 } orderMapper.markPaid(orderNo, tradeNo); timeSlotMapper.confirmSold(order.getId()); memberService.deductBalance(order.getMemberId(), order.getAmount()); }markPaid的 SQL 同样带AND status UNPAID条件双重保险。会员扣款用UPDATE member SET balance balance - #{amount} WHERE id #{id} AND balance #{amount}余额不足时影响行数为 0抛异常回滚。4. 会员储值与对账钱的事不能有玄学4.1 流水表设计每一分钱都要有迹可循会员卡余额不能只存一个数字。必须有一张流水表记录每次充值、消费、退款。余额字段是流水汇总的结果对账时用流水重算一遍和余额比对不一致就报警。CREATE TABLE member_balance_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, change_type VARCHAR(16) NOT NULL COMMENT RECHARGE/CONSUME/REFUND, amount DECIMAL(10,2) NOT NULL COMMENT 正数入账负数出账, balance_after DECIMAL(10,2) NOT NULL COMMENT 变动后余额, ref_order VARCHAR(64) COMMENT 关联订单号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_member (member_id, create_time) );balance_after字段是血泪经验——出问题时能直接定位到哪一笔开始对不上不用从头累加。写入流水和更新余额必须在同一事务且更新余额用UPDATE ... SET balance balance #{amount}的原子写法不要先查再算再写。4.2 对账任务每天凌晨自动核对对账逻辑很简单对每个会员用流水表SUM(amount)算出应有余额和会员表balance比对。不一致的记录写进异常表人工介入。Scheduled(cron 0 0 3 * * ?) public void reconcile() { ListLong memberIds memberMapper.selectAllIds(); for (Long id : memberIds) { BigDecimal sum balanceLogMapper.sumByMember(id); BigDecimal actual memberMapper.selectBalance(id); if (sum.compareTo(actual) ! 0) { reconcileExceptionMapper.insert(id, sum, actual); } } }这个任务跑起来后财务再也不用月底加班对账。注意sumByMember要处理 null——没有流水的会员返回 0 而不是 null。4.3 退款流程怎么不走丢退款比收款复杂要退余额、退时段、可能还涉及优惠券回退。顺序很重要——先退时段把状态改回 AVAILABLE再退余额写流水最后标记订单已退款。如果先退余额再退时段中间失败会导致钱退了但场地还占着。整个流程包在一个事务里任何一步失败全部回滚。5. 避坑与排查上线后最容易翻车的五个地方5.1 时段预生成任务重复执行导致数据翻倍现象某天发现未来第 14 天的时段每条出现了两次。原因定时任务在集群环境被多个节点同时触发或者手动执行了一次又赶上定时触发。解决INSERT IGNORE配合唯一约束只能防重复插入不能防重复执行逻辑。更稳妥的做法是加分布式锁或者用数据库的INSERT ... ON DUPLICATE KEY UPDATE只更新价格不新增。排查时直接SELECT court_id, slot_date, start_time, COUNT(*) FROM time_slot GROUP BY ... HAVING COUNT(*) 1。5.2 支付回调验签失败但订单已确认现象用户说付了钱但订单还是未支付。原因回调接口先确认订单再验签验签失败时订单已经改了。解决验签必须放在最前面验签通过才处理业务。排查时看回调日志里验签结果和订单状态变更的时间顺序。5.3 会员余额扣成负数现象会员卡余额出现负数。原因扣款 SQL 没加AND balance #{amount}条件或者加了但没判断影响行数。解决扣款 SQL 必须带余额充足条件且调用处判断返回值为 0 时抛异常回滚。排查时查流水表里balance_after 0的记录。5.4 时段释放任务把已支付的订单也释放了现象用户支付成功但场地被释放别人又能订。原因释放条件只判断了status LOCKED和时间没排除已支付但状态还没同步的情况。解决释放前先查关联订单状态已支付的不释放。排查时对比订单表的支付时间和时段表的释放时间。5.5 对账任务把正常数据报成异常现象对账异常表里一堆记录人工查都是对的。原因流水表SUM返回 null 时没处理和余额 0 比对不相等。解决SUM用COALESCE(SUM(amount), 0)。排查时先看异常记录的流水汇总值是不是 null。6. 用 Java 写管理系统的进阶习惯从能跑到好维护6.1 把业务规则收进领域对象别散在 Service 里第一版代码通常是一个OrderService里塞几百行预约、支付、退款全在一起。改一个规则要通读整个类。我的习惯是把时段状态流转、会员扣款规则、退款顺序这些收进对应的领域对象或领域服务。比如TimeSlot实体自己带lock()、confirmSold()、release()方法内部校验状态转移。Service 只负责编排和事务边界。这样单测好写——直接 new 一个 TimeSlot 调方法断言状态不需要起 Spring 容器。6.2 用枚举替代魔法数字status 1这种代码过两个月自己都不记得 1 代表什么。场地状态、订单状态、流水类型全部用枚举MyBatis 配 TypeHandler 自动转换。数据库里存字符串还是数字看团队习惯但 Java 侧必须是枚举。热搜里「java 基础」常考枚举真实项目里枚举的价值就是让代码自解释。6.3 日志打对地方排查省一半时间关键操作必须打日志时段锁定、支付回调、余额变动、退款。日志里带业务标识订单号、会员 ID、场地号不要只打「操作成功」。我一般用 MDC 把 traceId 放进每条日志出问题时一个 traceId 串起整个请求链路。另外异常日志要打全栈不要只打e.getMessage()——很多 bug 的线索在堆栈里。6.4 一个具体技巧用数据库唯一约束兜底所有并发问题应用层的锁可能因为代码改动失效但数据库的唯一约束永远生效。时段表的uk_court_slot、订单表的uk_order_no、流水表的uk_ref_order_type这些约束是最后一道防线。我踩过的坑是某次重构把乐观锁的AND status条件去掉了测试环境没并发没发现上线后黄金时段直接超卖。后来加了唯一约束即使代码写错数据库也会拒绝。代价是插入失败要处理异常但比超卖后给用户赔钱划算得多。这套系统我前后改了三版第一版能跑但不敢改第二版拆了表但并发有问题第三版才把状态机和唯一约束补齐。如果你也在做类似的管理系统先把时段模型和并发控制想清楚后面的会员和财务都是顺水推舟。希望帮到你。本文还有配套的精品资源点击获取
返回列表