ARTICLE DETAIL

资讯详情

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

游泳馆管理系统实战:计费模型、并发控制与硬件联调全解析

游泳馆管理系统实战:计费模型、并发控制与硬件联调全解析 简介一套基于Java技术实现的游泳馆管理系统课程设计项目面向熟悉数据库原理与Java编程的在校生及初入行的开发者。系统覆盖会员管理、在线预约、场地调度与收费统计等核心模块演示了从ER模型设计到业务逻辑落地的完整过程。资源包共1286个文件约3.81MB包含28个java与29个class源码文件、sql数据库脚本以及mdf/ldf数据库备份另有大量htm页面和gif演示图便于直接部署和对照界面理解功能。已有1534人学习下载。通过该资源可掌握会员等级设置、预约冲突处理、场地状态管理与多方式计费报表等关键实现为课程设计、毕业设计或企业初版原型提供直接参考。1. 游泳馆管理系统不是订票软件是一场场馆数字化改造如果你以为游泳馆管理系统就是做个预约小程序、放几个二维码让顾客扫码进场那大概率会把项目做成一个“漂亮的玩具”。真正让游泳馆老板愿意掏钱的系统是能帮他解决三件事高峰期前台排队堵成血栓、手环押金和超时费对不上账、私教课约了不来还找不到人。也就是说这套系统的核心不是“在线订票”而是“场馆运营的数字化骨架”——从会员入馆、闸机通行、更衣柜分配、淋浴计时到离场结算、营收统计、教练排课一整条业务链路都要在系统里闭环。适合谁做适合那些已经接过类似“体育场馆管理系统”外包、手上有硬件资源闸机、储物柜锁控板或者有真实场馆运营方需求的团队。它比纯电商系统糙但比进销存系统要“带物理设备”调度和并发问题也更真实。2. 业务流程先理清再谈表结构散客、会员年卡、培训课三种计费模型2.1 游泳馆的业务流和订单流从入场到离场要过几个状态游泳馆和健身房最大的区别在于“计时计次”的复杂度。健身房一般按次卡或月卡进门即可但游泳馆有散客计时超时加收、年卡不限次、次卡扣次数、培训课固定时段、团体包场等多种计费方式而且这些计费方式可以叠加——比如一个学员既办了年卡又报了暑期培训班还买了私教课。系统如果按“一张卡一种规则”来做第二天就会被场馆方的真实需求打脸。我一般会先画一条“场内动线”顾客到达 → 前台购票/验卡 → 过闸机入场 → 领手环/分配更衣柜 → 游泳/上课 → 淋浴 → 归还手环 → 离场结算散客超时补费。这个动线里核心状态节点只有四个已入场IN、在池ACTIVE、已归还RETURNED、已离场OUT。所有计费逻辑都围绕这四个状态切换展开不要搞一堆花哨的“已预约”“已取消”“已过期”状态去干扰判断。订单表的设计上必须把“票券”和“订单”分离。票券是用户持有的权益一张年卡、一张10次卡、一个散客入场券订单是每次入场产生的消费记录。一个用户持年卡来游泳入场动作生成一张订单关联年卡券离场时订单关闭。这样统计营收时看订单表统计会员数时看票券表两边互不污染。很多新手把票券信息直接冗余到订单表里结果月底对账时怎么都对不上。2.2 核心表结构设计价格策略单独建表别写死在代码里价格策略是游泳馆管理系统最容易翻车的地方因为场馆的计费规则经常变暑期旺季散客 50 元/次淡季 30 元/次周一至周五上午 25 元/次早场票周六日全天 50 元/次超时每小时加收 15 元超过 30 分钟按一小时算年卡售价 2800 元但老会员续费打八折。这些规则如果写在代码里每一次调价都要发版运营人员会骂娘。我的做法是单独建一张price_policy表把计费规则抽象成“时段 适用人群 时长 费用”的四元组。-- 价格策略表散客计费、次卡扣次、超时费都在这里配置 CREATE TABLE price_policy ( id INT PRIMARY KEY AUTO_INCREMENT, policy_name VARCHAR(64) NOT NULL COMMENT 策略名如暑期散客票, scene_type TINYINT NOT NULL COMMENT 1-散客计时 2-次卡扣次 3-年卡不限次 4-超时补费, applicable_days VARCHAR(32) DEFAULT 1,2,3,4,5,6,7 COMMENT 适用星期1-7, time_start TIME DEFAULT 00:00:00 COMMENT 时段开始, time_end TIME DEFAULT 23:59:59 COMMENT 时段结束, base_fee DECIMAL(10,2) DEFAULT 0 COMMENT 基础费用, max_duration_minutes INT DEFAULT 120 COMMENT 含时分钟数超时另计, overtime_fee_per_hour DECIMAL(10,2) DEFAULT 0 COMMENT 超时每小时费用, overtime_min_charge INT DEFAULT 15 COMMENT 超时不足15分钟不收费, sort_order INT DEFAULT 0, enable_flag TINYINT DEFAULT 1, KEY idx_scene_days (scene_type, applicable_days) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT价格策略表;这张表的核心逻辑是“按时间段匹配”。用户入场时系统根据当前时刻的weekday()和TIME(NOW())去查这条策略表命中后取出base_fee和max_duration_minutes。离场结算时用“实际时长 - 含时分钟数”得到超时分钟数再按overtime_min_charge取整计算超时费。这里有个隐藏设计点超时费用必须走补费订单不能直接改原订单金额否则财务流水对不上。散客入场时收了 50 元超时补收 15 元这是两笔订单打印小票时也分开打。把补费合进原订单退款时就讲不清楚了。另一个容易被忽略的表是member_card_consume_log卡券消费流水。次卡用户每次入场扣 1 次但这个扣减必须发生在闸机放行之后而不是用户扫码付款时。因为存在“扫了码但因为手环不够没进去”的情况如果扫码就扣次用户会找前台吵。所以卡券扣次要跟随“入场状态确认”事件而不是“支付成功”事件。2.3 计费生命周期管理状态机随手环走别用定时任务扫表游泳馆有个很现实的场景顾客拿着手环进了场游了三个小时才出来。如果系统只在入场时记录enter_time离场时简单算“当前时间 - 进入时间”看起来没问题。但遇到跨天营业比如凌晨场的场馆、手环丢失、顾客中途出馆又返回时间计算就会乱。更关键的是超时计费的起点应该是“最后一次确认顾客还在场内”的时间而不是入场时间。我用的方案是“手环心跳 状态机更新”。每个手环绑定一个订单顾客经过闸机、领取储物柜、淋浴区出门这几个物理节点时读卡器都会上报一次手环 ID系统把订单的last_confirm_time更新为当前时间。离场结算时以last_confirm_time为计费终点其实也就是离场时间本身。这个设计看似多余但它解决了一个血淋淋的坑顾客入场时刷了卡手环没戴好出闸机时刷不到前台只能手工补录离场时间结果顾客硬说自己只游了一个小时。有了last_confirm_time至少能证明他最后一次经过泳池区的时间。订单状态机的迁移也建议用一张状态变更日志表来记录order_id、from_status、to_status、changed_by设备或操作员、changed_at。方便事后审计特别是顾客投诉“我明明没入场但你扣了钱”的时候直接查日志比查订单表高效得多。这张表还有一种隐藏用途统计每个顾客在泳池内的平均停留时长。游泳馆运营方非常看重这个指标——停留 60 分钟是合理游泳时长停留 3 小时的多半是来蹭淋浴的这类顾客占比过高说明淋浴区管理有漏洞。3. 预约与计费的并发处理一个 Redis 预扣模板解决超卖和重复支付3.1 高峰期并发场景为什么会让普通 CRUD 系统崩掉游泳馆管理系统高峰期集中在工作日晚 18:00-21:00 和周末全天。这时候会发生什么前台两台收银机在打票闸机口十几个人排队刷手环更衣柜管理员的平板在不停刷新分配记录。如果系统的事务全部落在 MySQL 上每秒可能只有几十个事务正常情况下没问题但问题是“散客超时补费”和“储物柜押金”这类操作会锁行。比如有 100 个人同时离场每个人都在更新自己的订单状态这没什么冲突但如果是 30 个人同时去抢同一个更衣柜的“空闲”状态那就是实实在在的行锁竞争。更麻烦的是预约场景。暑期游泳培训班的课程名额有限一个班 15 人家长在小程序上同时抢课如果控制不好一个名额可能被 20 个人“预约成功”最后到场 25 人教练崩溃。解决这类问题我不会把精力花在 MySQL 事务上而是用 Redis 做预扣。Redis 的DECR原子操作天然适合这种库存扣减场景配合 Lua 脚本还能做成“预扣 释放”的完整流程。3.2 课程名额与时段人数的双重预扣让 DB 只做落库不做实时判断// 课程预约预扣名额Redis Lua 脚本原子完成“检查余量 扣减” String luaScript local remaining tonumber(redis.call(GET, KEYS[1]) or 0) if remaining 0 then return -1 end redis.call(DECR, KEYS[1]) redis.call(SADD, KEYS[2], ARGV[1]) // 记录已预约用户ID return remaining - 1;这段脚本的含义是先取当前剩余名额小于等于 0 直接返回 -1否则执行DECR扣减并把用户 ID 写进一个 Set记录谁占了这个名额。返回值是扣减后的剩余数。这里有个细节DECR之后立即把用户写进SADD是为了防止同一个用户重复抢同一个课程。因为如果只扣名额不记用户用户换个手机号再抢一次名额就多扣了。KEYS[1]是课程库存键如course:stock:1024KEYS[2]是已预约用户集合键如course:booked:1024ARGV[1]是用户 ID。数据库侧只需要接收“预扣成功”的消息然后异步落库订单。预约表里加一个reserve_status字段0-待支付1-已确认2-已取消。预扣后写入“待支付”订单用户十分钟内付款则更新为“已确认”超时未付用延迟任务恢复 Redis 库存。这套流程下MySQL 不需要任何SELECT ... FOR UPDATE大幅降低锁竞争。池同时在池人数控制也是同理。游泳馆有安全规范比如标准池同时最多 200 人超了要限流。我在 Redis 里维护一个pool:occupancy计数器顾客入场时INCR离场时DECR。这不仅用于安全限流还用于前台展示“当前在池人数”方便运营决定要不要卖票。这里要注意闸机和人工通道都要走同一个计数器否则人工带进来的散客不算人头限流就成摆设了。3.3 离场结算的异步化为什么说同步算费是给自己挖坑离场结算如果做成同步事务——查询订单、计算超时费、生成补费订单、更新状态、释放更衣柜——在高峰期会非常慢。因为涉及多张表的读写还要和更衣柜控制板的硬件通信。我见过一个项目把更衣柜释放做成了同步调用结果某个柜子的控制板没响应等了 5 秒超时整个离场结算失败顾客在前台排了长队。正确做法是“状态先行、算费异步”。顾客刷手环离场时闸机系统只做一件事把订单状态更新为OUT记录leave_time然后立刻放行。这一步只更新一条记录耗时毫秒级。真正的费用计算、更衣柜释放、流水入账推给消息队列异步处理。算错了怎么办系统里设计一个“人工纠偏”入口前台可以调出订单重新计算费用生成差额退款或补费单。这个异步化思路也适用于手环遗失。顾客弄丢手环前台在系统里挂失系统自动生成“手环赔偿 超时费”两张账单同时释放原手环绑定的更衣柜。挂失操作不需要等柜锁控制板响应先记账、再物理开锁人工去确认柜子清空。如果同步等硬件响应又会在高峰期卡死。4. 闸机与更衣柜硬件联调TCP 长连接、心跳保活和 SQLite 补传4.1 硬件设备选型和通信边界别指望 USB 转串口能用一年游泳馆管理系统的硬件层主要有三类闸机摆闸/翼闸、更衣柜锁控板、手环读写器RFID。这三个设备看似独立实则共享同一个通信链路而且它们的稳定性决定了整个系统在顾客眼中的口碑。设备通信的常见做法是用 485 转 TCP 网关也就是把设备的 RS485 信号转换成网络信号统一接入局域网。不要直接用 USB 转串口线接电脑因为电脑重启后串口会掉、驱动会丢前台电脑一旦蓝屏整个场的闸机全瘫。每个网关会挂载多台设备通信协议通常是厂商自定义的命*令帧**格式。我一般会让硬件厂商提供一份通信协议文档然后自己做网关层适配。这里有个血泪经验不要同时开多个线程往同一个网关写指令。同一个网关下的所有设备共享一条 485 总线两个线程并发写总线数据会互相干扰设备直接无响应。必须在程序里给每个网关维护一个写队列所有指令串行发送。设备连接状态要展示在前台界面上用红绿灯标识——闸机离线、柜锁离线是运营事故级别的告警。我一般会专门做一张device_status表记录每台设备的last_heartbeat_time超过 20 秒未收到心跳就标为离线。4.2 一个稳定的设备通信服务串行写队列 心跳自动重连// 设备通信管理器每个网关一个独立连接写指令全部走串行队列 public class DeviceGatewayManager { private final MapString, DeviceConnection connectionMap new ConcurrentHashMap(); private final ScheduledExecutorService scheduler Executors.newScheduledThreadPool(4); public void sendCommand(String gatewayIp, byte[] command) { DeviceConnection conn connectionMap.get(gatewayIp); if (conn null || !conn.isAlive()) { conn reconnect(gatewayIp); // 断线自动重连重试3次 connectionMap.put(gatewayIp, conn); } conn.enqueue(command); // 串行发送避免485总线冲突 } public void startHeartbeat() { // 每10秒向所有网关发送心跳探测指令连续3次无响应则标记离线 scheduler.scheduleAtFixedRate(() - { connectionMap.forEach((ip, conn) - { if (!conn.isAlive()) { reconnect(ip); } }); }, 10, 10, TimeUnit.SECONDS); } }这段代码的背后逻辑是每个网关对应一个DeviceConnection对象内部维护一个写队列和一个 Socket 连接。所有要发给该网关的指令都进入队列由单线程消费者逐条发送确保 485 总线上不会出现并发写。reconnect方法负责断线重连但要注意重连不能无限循环否则网关坏掉时程序会空转。常规做法是连续重试 3 次、间隔 2 秒仍然失败就把事件写入告警表并在前端弹红点。硬件通信最容易被低估的是响应超时。设备执行一条“开锁”指令可能需要 1-2 秒机械结构动作慢程序如果等 500 毫秒没收到响应就报错那这个系统一天能报几百次错。我一般会把设备响应超时设为 5 秒同时把“指令已发送”和“设备已响应”两个事件分开记录。顾客看到的是“开锁中…”转圈 2 秒然后打开完全可以接受。4.3 闸机离线时的本地缓存SQLite 临时存储 恢复后补传场馆网络从来不是 100% 稳定的。路由器和交换机过热重启、物业拉闸检修、运营商光缆被挖断这些我都遇到过。闸机如果直接依赖服务器网络一断就罢工场馆门口排队的人能把前台玻璃挤碎。所以闸机端必须能离线独立运行本地缓存通行记录恢复联网后再补传。我这边的做法是闸机程序内置一个 SQLite 数据库入场记录先写本地同时异步同步到云端或本地服务器。离线时记录累积在 SQLite 里网络恢复后按时间顺序补传。补传最怕的是“重复”所以每条记录要有一个全局唯一 IDUUID 或雪花 ID云端接口做幂等校验——同一 ID 只入库一次。如果云端没做幂等断网 5 分钟、补传 200 条记录里面可能有一半是重复的会员的入场次数直接翻倍。4.4 更衣柜分配的三种模式固定柜、随机柜、临时柜更衣柜子系统的核心矛盾是“柜子数量少于在池人数上限”。游泳馆不可能给每个顾客配一个固定柜所以分配逻辑必须支持三种模式固定柜长租给会员年费 600 元、随机柜散客当天分配、临时柜手环直接挂门上防水袋里不额外收费。固定柜和随机柜的物理锁不同——固定柜用机械钥匙或独立密码锁随机柜用电控锁接系统。随机柜的分配接口要注意一个并发边界前台或顾客小程序同时发起开柜请求系统先查 Redis 里的“空闲柜队列”弹出一个柜号标记为预占再下发开锁指令到锁控板。预占状态必须有过期时间比如 3 分钟内顾客没走到柜子前刷手环开锁预占就释放回空闲队列。不然顾客开了柜又关掉走人这个柜子就成了僵尸资源永远被占用。这个过期机制用 Redis 的SETEX或SET key val EX 180就能实现。5. 游泳馆管理系统的 5 个高频踩坑点与排查思路5.1 闸机离线恢复后补传数据把计次卡扣成负数现象场馆网络恢复后闸机自动补传了离线期间的上百条通行记录结果一批次卡用户的剩余次数变成负数前台被投诉淹没。 原因补传记录和线上实时记录发生了重复。闸机本地 SQLite 里有一条记录云端也收到过一次但云端接口没有做幂等校验同一 UUID 入账两次。 解决所有设备上报数据入口统一做幂等处理。用记录的唯一 ID 做主键或唯一索引INSERT ... ON DUPLICATE KEY UPDATE保证重复 ID 不再扣次。另外给卡券扣次逻辑加“余额不足则拒绝”的保护宁可拒绝入场也不能把次数扣成负数。5.2 散客手环超时未归还账上显示“在池”人其实早走了现象顾客拿了手环进池游完直接把手环带走或扔在淋浴区前台没做归还登记系统一直显示该顾客“在场内”。 原因手环归还依赖前台人工操作漏刷了就漏了。没有离场超时保护机制。 解决增加“超时未归还自动挂起”逻辑。入场后超过 6 小时可配置没有离场记录订单自动置为“挂起”手环作废押金自动转为赔偿金。同时保留人工补录通道顾客拿手环回来也能正常退押金。5.3 更衣柜预占用过期释放但顾客还在池子里游现象顾客扫码开了柜门放完东西进池但锁控板响应慢预占 3 分钟过期柜子被分给下一个人。第二位顾客打开柜门发现里面有别人的衣服。 原因预占释放逻辑只看 Redis 过期时间没有和“手环入场状态”联动。顾客已经入场他的柜子就不该被释放。 解决预占释放前先查订单状态。如果手环已入场订单状态为 ACTIVE则预占自动续期只有订单未确认入场时才做释放。这个查询可以放在锁控板开锁之后触发不需要额外加定时任务。5.4 数据库连接池被打满一到高峰期系统卡成PPT现象高峰期所有操作都变得很慢数据库 CPU 不高但show processlist里有大量Sleep状态的连接。 原因代码里写了new Connection()忘记关闭每个请求泄漏一个连接。也可能是 Redis 操作超时后代码没有走降级逻辑一直在等。 解决全局统一用连接池HikariCP配置maximumPoolSize20、minimumIdle5、connectionTimeout3000。代码里禁止手动new Connection全部走try-with-resources。Redis 操作加超时set timeout 500超时后走 DB 降级先保证核心流程不中断。5.5 计费策略改了但订单还是按旧价格算现象运营在后台把暑期散客价从 50 改到 60但新订单仍然按 50 结算。 原因价格策略表用了缓存但缓存更新不及时。或者订单表的unit_price字段是在入场时冗余的后续策略修改不影响已生成的订单。 解决区分“订单生成时快照”和“策略实时查询”。已生成的订单必须保留入场时的价格快照这是正确的但策略修改后新的订单必须走新价格。排查时先看缓存 TTL 设置再看订单表的价格来源字段。如果订单表价格来自冗余字段确认入场服务有没有拿到最新策略——重点查分布式缓存有没有做EVENT失效通知。6. 报表与能耗监测让数据替场馆运营做决策系统稳定运行后游泳馆老板真正关心的是三张报表营收流水表每日/每周/每月对比、客流时段分布图几点人最多、星期几最空、会员转化漏斗散客多久转化为年卡会员。这些数据在订单表和票券表里都有难的是怎么展示。我不建议在管理后台里堆一堆 ECharts 折线图而是做一个“每日运营早报”推送给馆长——早上 9 点推送昨日营收、客流量、同比环比、今日预约数。馆长不需要打开系统只看一条消息就知道今天该怎么排班。能耗监测是很多人忽略的加分项。游泳馆的循环水泵和恒温加热是电费大头系统如果能接入电表数据把“营收/耗电”比值做成 KPI就能帮场馆发现“营收涨了但利润没涨”的问题。做法上不复杂——在总电表和主要设备电表上加带 Modbus RTU 协议的智能电表网关定时拉取数据写入时序表按小时汇总。这套东西做出来馆长会觉得你的系统不止是收银工具而是经营参谋。关于游泳馆管理系统的项目交付我个人的经验是先做能抗住高峰期的骨架再谈报表和智能硬件的花活。很多时候问题不是功能不够而是高峰期闸机一卡、全盘皆输。项目做完、系统稳定跑过三个月的暑期高峰之后你再回头优化算法、调整页面都来得及。希望这个方向的经验能帮你在接这类项目时少走点弯路。本文还有配套的精品资源点击获取
返回列表