
简介这是一份面向高校计算机专业学生与Java初学者的数据库课程设计完整源码包以游泳馆日常运营为业务背景帮助读者理解如何将数据库原理与Java编程落地为可运行的管理系统。系统围绕会员管理、预约管理、场地管理与收费管理四大模块展开涉及会员等级与消费记录分析、预约冲突处理与状态实时更新、泳道及更衣室等设施调度、多方式支付与财务报表统计等典型场景并配套ER模型设计与实体关系规划适合作为课程设计、毕业设计或自学练手项目。资源包共1286个文件以1132个htm页面、63个gif图片、29个class与28个java源码为主另含js脚本、dll与jar依赖、sql建库脚本及mdf、ldf数据库文件压缩包约3.81MB目录结构完整便于直接导入运行与二次修改。目前已有1533人学习下载可帮助读者快速掌握Java Swing或JavaFX界面开发、关系型数据库建表与增删改查、事务与数据一致性处理等实用技能。1. 游泳馆管理系统从手写登记到扫码核销一套系统到底管什么夏天高峰期前台三个人同时开工一个负责收现金、一个翻纸质会员卡找余额、一个接电话回答“今天人多不多”。这种场景在很多中小型游泳馆里年年上演。游泳馆管理系统要解决的核心问题不是“把纸质表格搬到电脑上”而是把会员身份、入场核销、泳池水质、储物柜分配、培训课消这几条线拧成一条数据链让前台一个人就能完成入场、扣次、发柜、记录的全流程。它适合谁适合单店 200 到 2000 平米、日均客流 100 到 800 人、有会员卡和培训业务但还没上系统的场馆经营者也适合想接这类私活的后端或全栈开发者。一套能跑的系统重点不在界面多漂亮而在核销不能重复扣次、水质记录能追溯到时段、离线断网时前台还能放人进场。下面按落地顺序拆开讲。2. 需求拆解与数据模型先想清楚一张会员卡的生命周期2.1 游泳馆管理系统到底要管哪几件事很多人一上来就画界面结果做到一半发现“次卡”和“时段卡”的扣减逻辑完全不一样返工重来。我一般先把业务对象列全再决定表结构。游泳馆的核心对象有六个会员、卡种、卡实例、入场记录、储物柜、水质记录。培训业务再挂课程和课消记录。会员和卡的关系是一对多一个人可以买次卡、年卡、培训课包。卡种定义规则卡实例记录余额和有效期。入场记录是核销的凭证必须带时间戳和操作人。储物柜要记录手牌号和占用状态。水质记录按泳池和时段存 PH、余氯、水温。这里有个容易忽略的点入场记录不能只存“扣了一次”要存扣减前后的余额快照。否则会员来扯皮说“我明明还剩 5 次”你翻遍日志也说不清。常见做法是在入场记录表里加balance_before和balance_after两个字段。2.2 用 SQL 建出能追溯的核销表结构下面这套表结构是我在几个单店项目里反复用过的精简版MySQL 8.0 可直接跑。重点看entry_record表的快照字段和card_instance的乐观锁版本号。-- 会员表 CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, phone VARCHAR(20) NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 卡种表type1次卡 2时段卡 3年卡 CREATE TABLE card_type ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, type TINYINT NOT NULL, total_times INT DEFAULT 0, -- 次卡总次数其他类型为0 valid_days INT DEFAULT 0 -- 有效天数0表示永久 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 卡实例表version 用于乐观锁防并发重复扣次 CREATE TABLE card_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, card_type_id INT NOT NULL, balance_times INT DEFAULT 0, -- 剩余次数 expire_at DATETIME NULL, status TINYINT DEFAULT 1, -- 1正常 2冻结 3过期 version INT DEFAULT 0, INDEX idx_member (member_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 入场核销记录带余额快照 CREATE TABLE entry_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, card_instance_id BIGINT NOT NULL, balance_before INT NOT NULL, balance_after INT NOT NULL, operator VARCHAR(32) NOT NULL, entry_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_member_time (member_id, entry_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明card_instance.version是并发控制的关键。前台两个窗口同时给同一个人核销时第一个请求把 version 从 0 改成 1第二个请求带着 version0 来更新就会失败系统提示“请重试”避免扣成负数。参数上balance_times对时段卡和年卡可以存剩余有效天数或直接存 0核销时只校验expire_at是否过期不扣次数。entry_record的balance_before/after是扯皮时的后悔药一定要写。2.3 卡种规则怎么映射成代码里的判断分支卡种不同核销逻辑分三条路。次卡看balance_times 0且未过期时段卡看当前时间是否在有效期内年卡只看expire_at。我一般写一个canEntry方法统一判断返回枚举值前台根据返回值决定是放行、提示充值还是转人工。from datetime import datetime from enum import Enum class EntryResult(Enum): OK ok NO_BALANCE no_balance EXPIRED expired FROZEN frozen def can_entry(card, nowNone): now now or datetime.now() if card.status 2: return EntryResult.FROZEN if card.expire_at and card.expire_at now: return EntryResult.EXPIRED # 次卡才校验剩余次数时段卡和年卡 balance_times 为 0 也放行 if card.card_type.type 1 and card.balance_times 0: return EntryResult.NO_BALANCE return EntryResult.OK参数说明card.card_type.type从卡种表带出来1 代表次卡。expire_at为 NULL 表示永久有效年卡和部分次卡会设过期时间。这个函数不碰数据库纯逻辑判断方便单元测试覆盖边界。实际核销时再配合乐观锁更新两步分开排查问题时能快速定位是规则拦截还是并发冲突。3. 核销与并发前台两个人同时点“入场”会怎样3.1 为什么不能用“先查再扣”的写法新手最容易写出的代码是先SELECT balance_times判断大于 0再UPDATE balance_times balance_times - 1。这个写法在单窗口没事两个窗口同时操作同一张卡就会翻车。两个请求都查到余额是 1都判断通过都执行减一结果余额变成 -1会员白游一次月底对账对不上。血泪经验是判断和扣减必须在一条 SQL 里完成或者用乐观锁版本号兜底。我一般用带条件的 UPDATE把余额判断塞进 WHERE 子句。UPDATE card_instance SET balance_times balance_times - 1, version version 1 WHERE id ? AND version ? AND balance_times 0 AND status 1;执行后看affected_rows等于 1 说明扣减成功等于 0 说明要么版本对不上、要么余额不足、要么卡被冻结。再查一次当前状态给前台提示。这个写法把并发问题交给数据库的行锁简单可靠。3.2 用事务包住核销和写记录扣减和写入场记录必须在一个事务里。否则扣了次数但记录没写进去会员查不到入场历史前台也说不清。下面这段是核销的完整流程用 Python 的 DB 连接池演示。def do_entry(conn, member_id, card_id, operator, client_version): with conn.cursor() as cur: conn.begin() try: # 1. 乐观锁扣减 cur.execute( UPDATE card_instance SET balance_times balance_times - 1, version version 1 WHERE id %s AND version %s AND balance_times 0 AND status 1 , (card_id, client_version)) if cur.rowcount 0: conn.rollback() return {ok: False, msg: 扣减失败请刷新后重试} # 2. 查扣减后余额写快照 cur.execute(SELECT balance_times FROM card_instance WHERE id %s, (card_id,)) after cur.fetchone()[0] before after 1 cur.execute( INSERT INTO entry_record (member_id, card_instance_id, balance_before, balance_after, operator) VALUES (%s, %s, %s, %s, %s) , (member_id, card_id, before, after, operator)) conn.commit() return {ok: True, balance: after} except Exception as e: conn.rollback() raise e逻辑说明client_version是前台页面加载时读到的版本号提交时带回来。如果期间有人改过这张卡版本号对不上rowcount为 0直接回滚并提示重试。参数上operator存前台工号或姓名方便追责。before用after 1反推因为次卡每次只扣 1如果以后支持一次扣多次这里要改成扣减前先查一次。3.3 断网了前台还能不能放人进场中小场馆的网络说断就断前台不能因为系统连不上数据库就停止营业。常见做法是给前台页面加一个本地缓存队列断网时把核销请求存到浏览器 IndexedDB网络恢复后按顺序补传。补传时服务端要能识别重复请求避免同一张卡扣两次。我一般给每次核销生成一个客户端 UUID服务端entry_record表加client_uuid唯一索引。补传时如果 UUID 已存在直接返回成功不重复扣减。这个机制不复杂但能救命。注意本地缓存只存待补传的核销请求不存完整会员数据避免数据泄露风险。4. 水质与储物柜两个容易被当成“附属功能”的模块4.1 水质记录为什么必须按泳池分时段存卫生监督部门来检查要看的不是“今天水质合格”而是“今天上午 10 点这个泳池的余氯是多少”。水质记录如果只存一条日汇总检查时拿不出分时数据是要吃罚单的。我一般按泳池编号加时段存每两小时一条前台或救生员用手机扫码录入。CREATE TABLE water_quality ( id BIGINT PRIMARY KEY AUTO_INCREMENT, pool_no VARCHAR(16) NOT NULL, -- 泳池编号如 A池、儿童池 ph DECIMAL(4,2) NOT NULL, chlorine DECIMAL(4,2) NOT NULL, -- 余氯 mg/L temperature DECIMAL(4,1) NOT NULL, recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP, recorder VARCHAR(32) NOT NULL, INDEX idx_pool_time (pool_no, recorded_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明ph保留两位小数正常范围 7.0 到 7.8chlorine余氯正常 0.3 到 1.0 mg/Ltemperature水温常见 26 到 28 度。录入时前端做范围校验超出范围弹提醒但不阻止提交因为实际可能正在投药调整。recorder存录入人方便追溯。4.2 储物柜手牌分配怎么避免“一柜两人”储物柜管理的坑在于手牌还回来了但系统没点“释放”下一个客人拿到同一手牌开柜发现里面有东西。我一般用状态机管手牌空闲、占用、维修。分配时用带条件的 UPDATE 抢占释放时校验当前占用人才允许释放。-- 分配手牌只有空闲状态才能被抢占 UPDATE locker SET status 2, member_id ?, occupy_at NOW() WHERE locker_no ? AND status 1; -- 释放手牌只有本人或管理员才能释放 UPDATE locker SET status 1, member_id NULL, occupy_at NULL WHERE locker_no ? AND status 2 AND (member_id ? OR ? admin);逻辑说明第一条 SQL 的status 1是抢占条件两个前台同时给不同客人分配同一手牌只有一个能成功。第二条的member_id ? OR ? admin允许本人释放或管理员强制释放。参数上status用 1/2/3 表示空闲/占用/维修维修状态不参与分配。实际运营中还要加一个“超时自动释放”比如占用超过 6 小时且场馆已闭馆定时任务批量释放避免手牌被长期占用。5. 避坑与排查上线后最容易翻车的五个地方5.1 会员手机号重复导致一人多卡现象同一个手机号在系统里查出三个会员余额分散在三张卡上前台不知道该扣哪张。原因早期注册没做手机号唯一约束或者前台图省事用“张三”“张三2”建了多条。解决member.phone加唯一索引注册时先查后插已存在的直接引导到已有会员。历史数据用手机号分组保留最早一条其余合并卡实例到主会员下。5.2 次卡过期后余额还在但核销被拒现象会员卡里明明还有 10 次前台核销提示“已过期”。原因can_entry先判断expire_at再判断余额过期直接拦截。解决这是预期行为但前台需要看到明确提示“卡已过期剩余 10 次是否续期”。续期逻辑是更新expire_at并保留balance_times不要新建卡实例否则历史记录断裂。5.3 水质录入时间用了客户端时间导致顺序错乱现象检查时发现水质记录时间跳来跳去10 点的记录排在 9 点前面。原因前端用new Date()取客户端时间不同手机时间不准。解决recorded_at由服务端NOW()生成客户端只传泳池编号和数值。如果必须用客户端时间加一个server_time字段做校准。5.4 储物柜释放接口没校验权限被恶意调用现象有人抓包发现释放手牌的接口只要传手牌号就能释放别人的柜子。原因接口只校验了登录态没校验手牌占用人。解决释放 SQL 加member_id 当前登录人 OR 角色 admin条件affected_rows为 0 时返回无权限。这个坑在早期版本里很常见上线前一定用不同账号交叉测一遍。5.5 核销记录快照字段没写导致对账对不上现象月底会员说少了一次查entry_record只有扣减后余额没有扣减前余额无法证明扣了几次。原因开发时觉得快照冗余只存了balance_after。解决balance_before和balance_after都写before在扣减前查一次或用after 扣减数反推。已经上线的表用ALTER TABLE加字段历史数据用相邻记录推算补上。6. 进阶技巧用核销数据反推场馆真实客流系统跑起来三个月后entry_record表里就攒下了真实客流数据。我一般会写一个按小时聚合的查询帮老板看清“到底几点人最多”用来排班和调水温。SELECT HOUR(entry_at) AS hour_slot, COUNT(*) AS entry_count, COUNT(DISTINCT member_id) AS unique_members FROM entry_record WHERE entry_at DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY HOUR(entry_at) ORDER BY hour_slot;这个查询输出 30 天内每个小时的入场人次和独立会员数。entry_count远大于unique_members说明有大量次卡会员高频到场可以考虑推年卡两者接近说明散客多适合推次卡。参数上INTERVAL 30 DAY可以改成 7 天看短期波动改成 90 天看季节趋势。注意entry_at要建索引否则数据量上到十万级查询会变慢。再进一步把水质记录和客流按小时对齐能看出“人多的时候余氯掉得快”的规律。我一般用下面这个关联查询帮场馆决定投药时间。SELECT e.hour_slot, e.entry_count, AVG(w.chlorine) AS avg_chlorine FROM ( SELECT HOUR(entry_at) AS hour_slot, COUNT(*) AS entry_count FROM entry_record WHERE entry_at DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY HOUR(entry_at) ) e LEFT JOIN water_quality w ON HOUR(w.recorded_at) e.hour_slot GROUP BY e.hour_slot, e.entry_count ORDER BY e.hour_slot;逻辑说明子查询先算每小时的入场人次再按小时关联水质记录求平均余氯。LEFT JOIN保证没有水质记录的小时也能显示客流。参数上INTERVAL 7 DAY适合看一周规律避开单日异常。如果某个小时entry_count高但avg_chlorine低于 0.3就该在那个时段前加投药。这套系统我前后在几个场馆落地过最大的教训是别在核销逻辑上省事快照字段和乐观锁一个都不能少。前台可以忍受界面丑但忍受不了扣错次数被会员堵门。另一个习惯是每次上线新功能前用两个浏览器窗口同时点同一个操作看会不会出并发问题。这个土办法帮我拦下过至少三次重复扣次的 bug。希望帮到你。本文还有配套的精品资源点击获取