ARTICLE DETAIL

资讯详情

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

游泳馆管理系统开发实战:从扫码核销到闸机开闸的完整链路

游泳馆管理系统开发实战:从扫码核销到闸机开闸的完整链路 简介这是一份面向高校计算机与软件工程专业学生的Java数据库课程设计资源以游泳馆日常运营为业务背景帮助学习者把数据库原理与Java开发落到一个完整可运行的管理系统上。系统覆盖会员管理、预约管理、场地管理与收费管理四大模块涉及会员等级与消费记录分析、预约冲突处理与状态实时更新、泳道及更衣室等设施调度、多种支付方式记账与财务报表统计并贯穿ER模型设计、实体属性关系建模等数据库核心知识点。资源包共1286个文件以1132个htm页面、63个gif素材、29个class与28个java源码为主另含js脚本、dll与jar依赖、sql建库脚本及mdf、ldf数据库文件压缩包约3.81MB目录结构完整便于按模块查阅源码与页面。目前已有1533人学习下载适合作为课程设计参考、Java与数据库综合练习或二次开发的基础工程。1. 游泳馆管理系统从手写登记到扫码核销一套系统到底要管什么夏天高峰期前台两个工作人员一个接电话一个收现金手写登记本上密密麻麻的名字和手牌号会员来了翻半天找不到记录散客排队排到门口更衣室柜子钥匙对不上号月底老板问“这个月卖了多少次卡、哪个时段人最多”没人答得上来。这不是段子是大多数中小游泳馆的真实状态。游泳馆管理系统要解决的核心问题就三件事把入场核销从纸面搬到线上、把会员卡和次卡的钱算清楚、把泳池的水质和救生员排班纳入日常记录。它适合单店 200 到 2000 平米、日均客流 100 到 800 人的场馆也适合连锁 3 到 5 家店需要统一后台的运营方。这套系统不是简单的收银软件它同时踩在票务、会员、硬件闸机、水质监测四条线上选型和落地都有讲究。下面按“先想清楚管什么、再动手搭最小可用版本、最后避坑”的顺序讲透。2. 需求拆解与选型别一上来就买成品 SaaS2.1 先画业务流再决定买还是自己搭很多馆主第一反应是买个现成的收银系统结果发现闸机对接不了、次卡核销逻辑对不上、水质记录模块根本没有。我一般建议先用一张纸把三条主线画出来入场线散客买票→扫码→闸机开→入场、会员线办卡→充值→扣次/扣时长→到期提醒、运营线水质检测→投药记录→救生员排班→交接班。画完之后你会发现入场线和会员线是强关联的运营线相对独立但涉及合规检查。选型上分三种情况。第一种纯散客、无会员、无闸机日均 100 人以下直接用微信/支付宝收款码加一个 Excel 台账就够了上系统是浪费。第二种有会员次卡、有闸机、单店经营建议用轻量级自研或开源方案二次开发因为成品 SaaS 的闸机对接往往要额外收费且不灵活。第三种连锁多店、有线上购票小程序、需要总部看板这时候才值得考虑成熟的商业 SaaS但一定要确认它开放 API 或者至少支持标准闸机协议。提示选型前先确认你的闸机品牌和通信协议。常见的是韦根 26/34 协议和 RS485 串口如果闸机只支持韦根那软件端必须能输出韦根信号或者通过控制器中转这一点直接决定你能不能自己搭。2.2 技术栈怎么选一张表看清三种路线路线适用场景技术栈建议开发周期月成本估算Excel 收款码日均100人无会员无00轻量自研单店有会员和闸机Python FastAPI SQLite/PostgreSQL Vue2-4周服务器 50-100元商业 SaaS连锁多店统一管理按厂商方案1-2周部署300-2000元自研路线的核心模块不多会员表、卡类型表、消费记录表、闸机控制接口、水质记录表。用 FastAPI 是因为它写接口快、自带文档、和硬件串口库配合也方便。前端用 Vue 或者直接服务端渲染都行前台操作界面越简单越好最好三个按钮搞定入场。2.3 数据库表设计四张核心表撑起整个系统-- 会员表一个会员可以有多张卡 CREATE TABLE member ( id SERIAL PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20) UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); -- 卡类型表次卡、时长卡、储值卡 CREATE TABLE card_type ( id SERIAL PRIMARY KEY, type_name VARCHAR(30) NOT NULL, -- 次卡,月卡,储值卡 total_times INT, -- 次卡总次数其他类型为NULL price DECIMAL(10,2) NOT NULL, valid_days INT -- 有效天数 ); -- 会员卡表会员持有的具体卡实例 CREATE TABLE member_card ( id SERIAL PRIMARY KEY, member_id INT REFERENCES member(id), card_type_id INT REFERENCES card_type(id), remaining_times INT, -- 剩余次数 balance DECIMAL(10,2), -- 储值余额 start_date DATE NOT NULL, end_date DATE, status VARCHAR(10) DEFAULT active -- active/expired/frozen ); -- 入场记录表每次核销写一条 CREATE TABLE entry_log ( id SERIAL PRIMARY KEY, member_card_id INT REFERENCES member_card(id), entry_time TIMESTAMP DEFAULT NOW(), gate_id VARCHAR(20), -- 哪个闸机 operator VARCHAR(30) -- 操作员 );这四张表的关系是会员办卡时在 member_card 里插一条记录每次入场先查 member_card 的剩余次数和有效期扣减后写 entry_log。储值卡扣的是 balance次卡扣的是 remaining_times。注意 end_date 为空表示长期有效但次卡一般也要设有效期防止一张卡用五年。参数说明remaining_times 用 INT 而不是 BOOLEAN因为次卡可能一次买 50 次balance 用 DECIMAL 不用 FLOAT金额计算不能有浮点误差entry_log 的 gate_id 用于排查“明明刷卡了闸机没开”的问题。3. 最小可用版本搭建从扫码到开闸的完整链路3.1 环境准备与项目骨架# 创建项目目录 mkdir swimming-pool-system cd swimming-pool-system python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖 pip install fastapi uvicorn sqlalchemy psycopg2-binary pyserial qrcode pillowpyserial 是用来控制闸机的如果你的闸机走网络 TCP 就换成 socket 库。qrcode 用来生成会员码前台扫码枪扫的就是这个码。数据库用 PostgreSQLSQLite 也能跑但并发写入容易锁表高峰期 200 人同时入场会出问题。项目结构建议main.py 放 FastAPI 入口models.py 放 SQLAlchemy 模型gate.py 封装闸机控制templates/ 放前台页面。不要一上来就搞微服务单店系统一个进程足够。3.2 会员核销接口扣次、扣费、开闸三步走from fastapi import FastAPI, HTTPException from sqlalchemy.orm import Session from datetime import date import serial app FastAPI() # 闸机串口初始化根据实际设备改端口和波特率 gate_serial serial.Serial(/dev/ttyUSB0, 9600, timeout1) app.post(/api/entry/{card_id}) def entry(card_id: int, gate_id: str gate_01): db Session() card db.query(MemberCard).filter(MemberCard.id card_id).first() if not card: raise HTTPException(404, 卡不存在) if card.status ! active: raise HTTPException(400, 卡已冻结或过期) if card.end_date and card.end_date date.today(): raise HTTPException(400, 卡已过期) # 次卡扣次储值卡扣费时长卡不扣 if card.remaining_times is not None: if card.remaining_times 0: raise HTTPException(400, 次数已用完) card.remaining_times - 1 elif card.balance is not None: if card.balance 30: # 假设单次入场30元 raise HTTPException(400, 余额不足) card.balance - 30 # 写入场记录 log EntryLog(member_card_idcard_id, gate_idgate_id) db.add(log) db.commit() # 发送开闸信号韦根协议一般是发一串十六进制 gate_serial.write(b\x01\x02\x03) # 具体指令看闸机手册 return {status: ok, remaining: card.remaining_times, balance: float(card.balance or 0)}逻辑说明先校验卡状态和有效期再根据卡类型决定扣次还是扣费最后写记录并开闸。注意扣减和写记录必须在同一个事务里否则扣了次但记录没写对账时就是一笔糊涂账。开闸信号放在 commit 之后万一开闸失败至少钱扣了有记录可查。参数说明gate_serial 的端口和波特率必须和实际设备一致韦根协议通常需要额外的控制器把串口信号转成韦根直接用串口发指令只适用于支持串口控制的闸机。单次入场价格 30 元是示例实际按场馆定价改。3.3 前台扫码页面三个按钮搞定入场!DOCTYPE html html headmeta charsetutf-8title入场核销/title/head body h2游泳馆入场核销/h2 input idcardInput placeholder扫码或输入卡号 autofocus button onclickdoEntry()确认入场/button div idresult/div script async function doEntry() { const cardId document.getElementById(cardInput).value.trim(); if (!cardId) return; const res await fetch(/api/entry/${cardId}, {method: POST}); const data await res.json(); const el document.getElementById(result); if (res.ok) { el.innerHTML p stylecolor:green入场成功剩余次数${data.remaining ?? 不限}余额${data.balance}/p; } else { el.innerHTML p stylecolor:red失败${data.detail}/p; } document.getElementById(cardInput).value ; document.getElementById(cardInput).focus(); } /script /body /html这个页面故意做得极简因为前台高峰期没时间点来点去。扫码枪本质上是一个键盘输入设备扫完自动回车触发 doEntry。autofocus 保证光标始终在输入框扫完一张卡立刻能扫下一张。结果区域用颜色区分成功失败前台一眼就能判断。注意扫码枪的输入速度很快如果页面有防抖逻辑要关掉否则会漏字符。另外卡号建议用纯数字避免扫码枪把字母识别错。3.4 水质记录模块合规检查要能导出app.post(/api/water-quality) def add_water_record(ph: float, chlorine: float, temp: float, operator: str): # pH 标准 7.2-7.8余氯 0.3-1.0mg/L水温 26-28度 warnings [] if not (7.2 ph 7.8): warnings.append(fpH异常{ph}) if not (0.3 chlorine 1.0): warnings.append(f余氯异常{chlorine}) record WaterQuality(phph, chlorinechlorine, temptemp, operatoroperator) db.add(record) db.commit() return {status: ok, warnings: warnings}水质记录看起来简单但卫生监督所检查时要看连续记录缺一天就要写说明。所以这个接口要加一个定时任务每天固定时间提醒值班人员录入。warnings 字段直接返回给前台超标了立刻处理别等检查时才发现。4. 避坑与排查上线后最容易翻车的五个地方4.1 闸机开了但系统没扣次现象会员刷卡后闸机开了但后台查剩余次数没变。原因开闸信号和数据库 commit 的顺序反了先开闸后 commitcommit 失败时闸已经开了。解决严格按“校验→扣减→commit→开闸”的顺序commit 失败就不发开闸信号。如果闸机支持双向通信最好等闸机返回“已开”再写记录。4.2 高峰期扫码枪漏字符现象排队人多时扫码枪扫了但输入框只显示一半卡号。原因页面有输入防抖或者扫码枪回车触发太快上一次请求还没返回就扫了下一张。解决去掉所有防抖逻辑请求改成同步或者加一个“处理中”状态锁锁住期间不接受新输入。另外扫码枪本身可以设置字符间隔调慢一点。4.3 次卡扣次和时长卡混淆现象月卡会员入场一次扣了一次次数月底发现次数扣完了。原因卡类型判断逻辑写成了 if remaining_times is not None但月卡的 remaining_times 字段没设成 NULL 而是 0。解决建卡时严格按类型赋值次卡设次数月卡和储值卡的 remaining_times 必须为 NULL判断时用 is not None 而不是 0。4.4 数据库连接池耗尽现象下午高峰期系统卡死报错“too many connections”。原因每个请求都新建 Session 没关闭连接池默认 5 个连接很快用完。解决用 FastAPI 的依赖注入管理 Session或者用 context manager 确保每个请求结束后 close。PostgreSQL 默认最大连接 100但应用层连接池设 10-20 就够了。4.5 水质记录补录导致时间戳混乱现象检查前补录前几天的水质数据导出报表时时间顺序乱了。原因created_at 用了默认 NOW()补录时插入的是当前时间。解决水质表加一个 record_date 字段补录时手动指定日期报表按 record_date 排序而不是 created_at。5. 进阶技巧用入场数据反推运营策略系统跑起来之后entry_log 表就是一座金矿。我一般会加一个简单的统计接口按小时聚合入场人数跑一周就能看出高峰时段。比如数据显示 18:00-20:00 入场人数占全天的 45%那救生员排班就要重点覆盖这个时段而不是平均分配。再进一步把会员卡的到期时间和剩余次数结合筛选出“30 天内到期且剩余次数大于 10 次”的会员前台可以定向提醒续卡转化率比群发短信高得多。app.get(/api/stats/hourly) def hourly_stats(date_str: str): # 按小时统计入场人数 rows db.execute( SELECT EXTRACT(HOUR FROM entry_time) AS hour, COUNT(*) AS cnt FROM entry_log WHERE entry_time::date :d GROUP BY hour ORDER BY hour , {d: date_str}).fetchall() return [{hour: int(r[0]), count: r[1]} for r in rows]这个查询在 entry_log 数据量到十万级时开始变慢加一个 entry_time 的索引就能解决。如果连锁多店把 gate_id 也加进 GROUP BY就能对比不同门店的客流曲线。另一个实用技巧是给闸机加一个“反向核销”按钮。会员出场时再刷一次卡记录离场时间这样能算出平均停留时长。停留时长超过 3 小时的要留意可能是教练在私下带课也可能是设备故障导致重复入场。这些细节不一定要马上做但表结构里预留 exit_time 字段以后想加随时能加。我自己踩过最深的坑是早期没做操作日志前台误操作把会员卡删了查不到是谁干的。后来加了 operator 字段和软删除所有删除都是标记 statusdeleted数据还在只是不显示。这个习惯救过我好几次希望帮到你。本文还有配套的精品资源点击获取
返回列表