资讯详情 无人台球厅管理系统实战:SSM+MySQL架构、事务与定时任务全解析
📅 2026/10/7 1:43:48
简介面向高校毕业设计的无人台球厅管理系统完整源码基于SSM框架与MySQL数据库实现覆盖前台用户预约、后台运营管理两大场景。前台包含首页轮播、用户注册登录、台球桌列表与状态展示、分时段预约下单、订单撤销与退款申请、充值会员获得折扣以及商品自助购买等完整业务链路后台按管理员与用户双视角划分支持桌台信息维护、预约与商品订单管理、支付监控、在线统计营业额等核心功能适合Java初学者或毕设开发者直接参考。压缩包共1279个文件以jsp视图页面、Java源码与class类、js脚本、css样式、xml配置文件及jar依赖库为主并附Sql脚本、说明文档、LW文档和PPT答辩材料整体约42.2MB。资源已上线CSDN目前已有42人浏览学习。部署环境明确为JDK1.8、Tomcat7.5、MySQL5.7拿到后可按说明运行既能用于毕业设计演示也可作为SSM项目分层、预约下单、支付对接与后台管理的实战案例。1. 无人台球厅管理系统为什么无人才是这个毕设的得分点很多人拿到无人台球厅管理系统这个题目第一反应是往硬件上想——门禁、摄像头、传感器。但真正做毕业设计你会发现无人值守的本质不是硬件联动而是一套把人从业务流程里拿掉的软件状态机谁开的台、开了多久、什么时候该结账、钱够不够扣这些判断全部交给代码按时序去执行。这套系统的核心价值不是能扫码开台而是没有店员盯着订单也不会出错。这套基于 SSM 和 MySQL 的源码方案适合两类人一类是 Java 方向、需要快速完成一个结构完整、能演示、能写进答辩 PPT 的毕设项目另一类是已经在学 SSM 框架想找一个能覆盖增删改查之外、还带一点业务复杂度的练手场景。它不像商城、博客那样只是 CRUD 的堆积台球厅场景天然包含计时、计费、并发开台、异常订单这些真实问题把这些讲清楚答辩时比我用了 SSM 做了个增删改查要有说服力得多。2. 从业务流程到数据表用 MySQL 把无人值守拆成四张核心表2.1 无人台球厅的业务闭环扫码上机、计时计费、余额不足自动结账在写任何代码之前得先把没有店员这件事落到具体的业务时序上。常见的无人台球厅流程是这样的顾客到店后用微信扫码选择一个空闲的台球桌系统创建订单并把桌子的状态从空闲改成使用中顾客打完球去前台结算或者更彻底一点——系统按台费标准自动计时顾客的预付余额不足时自动强制结账并释放桌子。如果做得再细一点还有中途加钟、会员折扣、过时未支付订单自动取消这些分支。对于毕业设计我建议把流程收敛成四个核心环节选桌开台 → 计时计费 → 结账扣费 → 释放资源。开台动作同时写两张表一是订单表插入一条新订单二是台球桌表的台桌状态改成使用中这两步必须放在同一个事务里否则会出现订单建了但桌子还是空闲的数据不一致。计时计费环节不要用前端定时器来算钱前端页面一刷新时间就丢了正确做法是由后端在查询订单时根据当前时间减去开台时间实时计算费用展示用的计时器只是好看计费的权威数据永远在后端。2.2 四张核心表的设计台桌、会员、订单、计费规则数据库设计直接决定后面 Service 层好不好写。我见过很多毕设把计费单价写死在订单表里结果改价格要改代码这种设计答辩时很容易被老师问住。正确的做法是把台费标准单独拆一张表按桌型和时段存单价订单表只存开台时间和结账时间费用要么实时算、要么在结账那一刻按单价表计算。下面这组建表脚本是一个足够应对毕设的版本MySQL 5.7 及以上都能直接跑-- 台球桌表 CREATE TABLE ball_table ( table_id int(11) NOT NULL AUTO_INCREMENT COMMENT 台桌ID, table_name varchar(50) NOT NULL COMMENT 台桌编号如 A01, table_type tinyint(4) NOT NULL DEFAULT 0 COMMENT 桌型0普通桌 1斯诺克桌, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0空闲 1使用中 2维护, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (table_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT台球桌信息表; -- 会员表 CREATE TABLE member ( member_id int(11) NOT NULL AUTO_INCREMENT COMMENT 会员ID, phone varchar(11) NOT NULL COMMENT 手机号登录账号, balance decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 账户余额, level tinyint(4) NOT NULL DEFAULT 1 COMMENT 会员等级1普通 2黄金 3钻石, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, PRIMARY KEY (member_id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员表; -- 订单表 CREATE TABLE order_info ( order_id int(11) NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no varchar(32) NOT NULL COMMENT 订单编号, member_id int(11) NOT NULL COMMENT 下单会员, table_id int(11) NOT NULL COMMENT 台桌ID, start_time datetime NOT NULL COMMENT 开台时间, end_time datetime DEFAULT NULL COMMENT 结账时间NULL表示未结账, total_amount decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 应收金额, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1进行中 2已结账 3已取消, PRIMARY KEY (order_id), UNIQUE KEY uk_order_no (order_no), KEY idx_table_status (table_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT台球订单表; -- 计费规则表 CREATE TABLE price_config ( price_id int(11) NOT NULL AUTO_INCREMENT, table_type tinyint(4) NOT NULL COMMENT 对应ball_table的table_type, weekday_price decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 工作日单价元/小时, weekend_price decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 周末单价元/小时, PRIMARY KEY (price_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT台费计费规则表;这套设计的核心意图是把状态和业务数据分离。order_info里的status字段是业务流转的唯一依据end_time为 NULL 就代表订单未结、台桌还在占用中price_config把计费规则独立出来是为了让改价格这件事不经过代码发布——答辩时你可以现场演示改数据库里的价格然后新订单立刻按新价格计费这是一个很好的加分点。字段类型上有一点提醒金额字段一定用DECIMAL而不是FLOAT或DOUBLE浮点数算钱会出现 0.1 0.2 不等于 0.3 的经典问题手机号用VARCHAR(11)而不是数值类型避免前端展示时出现科学计数法。索引方面order_info上加了idx_table_status因为查某张桌有没有未结订单是无人值守场景最高频的查询。3. 用 SSM 把无人逻辑写进 Service 层开台、计时、结账的代码实现3.1 SSM 三层架构在项目里到底怎么分工SSM 是 Spring Spring MVC MyBatis 的组合很多同学框架整合好了、CRUD 也能跑但一到写业务就不知道该把代码放在哪一层。这里有一个判断标准Controller 只做参数接收和结果返回Service 只做业务逻辑和事务控制Mapper 只做 SQL 数据访问。无人台球厅的无人逻辑本质上全部集中在 Service 层——因为判断台桌是否空闲、余额是否够扣、订单是否能结账这些都是业务规则不是数据访问。项目骨架用 Maven 建分包方式我一般这样组织controller放页面跳转和接口service放业务接口和实现类mapper放 MyBatis 的接口和 XMLentity放数据表对应的实体类common放统一返回结果和工具类。这样的结构在毕业设计说明文档里画架构图时非常清晰老师一眼就能看出你理解了分层。3.2 开台与结账两个必须用事务保护的 Service 方法开台是无人系统里并发压力最大的动作——两个顾客同时扫码都想订同一张桌子。如果代码是先查状态、再插入订单中间不加任何保护并发时两张订单会同时查到空闲然后一起下单。解决方式有两种一是把查询台桌状态和更新台桌状态放进一个事务并用SELECT ... FOR UPDATE锁行二是直接在更新台桌状态时加WHERE status 0靠更新影响的行数来判断是否抢台成功。第二种更轻量推荐优先使用。下面这个开台方法是去掉具体框架代码后的核心逻辑写法Transactional(rollbackFor Exception.class) public Result openTable(OpenTableDTO dto) { // 1. 校验台桌是否存在 BallTable table ballTableMapper.selectById(dto.getTableId()); if (table null) { return Result.error(台桌不存在); } // 2. 原子性抢占只有状态为0空闲时才能更新成1使用中 int rows ballTableMapper.compareAndSetStatus( dto.getTableId(), 0, 1); if (rows 0) { return Result.error(台桌已被占用请选择其他台桌); } // 3. 生成订单号时间戳 随机数避免并发重复 String orderNo TB System.currentTimeMillis() String.format(%04d, new Random().nextInt(10000)); OrderInfo order new OrderInfo(); order.setOrderNo(orderNo); order.setMemberId(dto.getMemberId()); order.setTableId(dto.getTableId()); order.setStartTime(new Date()); order.setStatus(1); orderInfoMapper.insert(order); return Result.success(开台成功, order.getOrderNo()); }这个方法的巧妙之处在第 2 步。compareAndSetStatus对应的 SQL 是UPDATE ball_table SET status 1 WHERE table_id ? AND status 0MySQL 的行锁保证同一时刻只有一个事务能更新成功另外一个事务的更新影响行数是 0直接返回已被占用。这就是无人场景下抢台的标准解法不需要显式加锁代码也简单。事务注解加上rollbackFor Exception.class很重要——默认情况下只有运行时异常才回滚如果业务里抛了受检异常而不回滚就会出现台桌状态改了但订单没插进去的脏数据。结账的逻辑比开台复杂一些需要按计费规则表计算时长费用然后扣减会员余额最后把订单和台桌状态一起更新。我直接贴出结账的 Service 核心代码Transactional(rollbackFor Exception.class) public Result settleOrder(Long orderId) { // 1. 查询进行中的订单加锁防止重复结账 OrderInfo order orderInfoMapper.selectActiveById(orderId); if (order null) { return Result.error(订单不存在或已结账); } // 2. 查询台桌类型和对应单价 BallTable table ballTableMapper.selectById(order.getTableId()); PriceConfig price priceConfigMapper.selectByTableType(table.getTableType()); // 3. 计算消费时长不满1小时按1小时计超过1小时按分钟计 long minutes (System.currentTimeMillis() - order.getStartTime().getTime()) / 60000; if (minutes 0) minutes 1; double hours Math.max(1.0, Math.ceil(minutes / 60.0)); // 4. 判断是否周末选择对应单价 Calendar cal Calendar.getInstance(); int dayOfWeek cal.get(Calendar.DAY_OF_WEEK); boolean isWeekend (dayOfWeek Calendar.SATURDAY || dayOfWeek Calendar.SUNDAY); BigDecimal unitPrice isWeekend ? price.getWeekendPrice() : price.getWeekdayPrice(); BigDecimal totalAmount unitPrice.multiply(BigDecimal.valueOf(hours)); // 5. 扣减会员余额 Member member memberMapper.selectById(order.getMemberId()); if (member.getBalance().compareTo(totalAmount) 0) { return Result.error(余额不足请先充值); } memberMapper.deductBalance(order.getMemberId(), totalAmount); // 6. 更新订单和台桌状态 orderInfoMapper.finishOrder(orderId, totalAmount); ballTableMapper.compareAndSetStatus(order.getTableId(), 1, 0); return Result.success(结账成功, 实付金额 totalAmount); }这段代码里有两个细节值得在答辩时展开讲。一个是计费规则不满一小时按一小时算的实现逻辑Math.ceil(minutes / 60.0)会把 61 分钟算成 2 小时、5 分钟算成 1 小时这比直接把minutes除以 60 更符合台球厅的实际经营规则。另一个是金额计算全程用BigDecimal而不是double这是 Java 面试题里高频考察的浮点数精度问题——用在毕设代码里体现的是你在意工程细节。结账方法里我特意在查询订单时用了selectActiveById这个查询对应 SQL 是SELECT * FROM order_info WHERE order_id ? AND status 1 FOR UPDATE。加上FOR UPDATE是防止两个请求同时到来时都读到未结账状态、两边都执行扣款——这是无人系统里重复支付的经典问题也是与普通 CRUD 拉开差距的关键点。4. 无人值守的边界情况掉线、断网、超时订单和余额不足的处理4.1 后台定时任务处理顾客打完球忘了结账的订单无人台球厅最大的风险不是技术故障而是顾客打完球直接走人、订单一直挂着。这时候必须有后台任务兜底。常见做法有两种一种是 Spring 自带的Scheduled定时注解在系统里配一个每 5 分钟跑一次的定时器扫描所有进行中且超过一定时长未结账的订单另一种是用数据库事件调度器但毕设阶段我建议用Scheduled代码可视化程度高写进说明文档和答辩 PPT 更容易解释。定时清理任务的逻辑是这样设定一个最大游玩时长比如 6 小时查询所有status 1且start_time在 6 小时之前的订单对这些订单执行强制结账——按当前时间计算费用、扣会员余额、释放台桌。如果会员余额不足就把订单标记为已结账、欠费状态同时把台桌释放。这个强制结账的逻辑和手动结账基本一样唯一的差别是它不需要人工触发。我把定时任务的写法贴出来注意Scheduled注解的参数含义Component public class OrderTimeoutTask { Autowired private OrderInfoMapper orderInfoMapper; Autowired private SettleService settleService; /** * 每隔5分钟执行一次扫描超时订单 * fixedDelay 表示上一次执行完成后间隔5分钟再执行下一次 */ Scheduled(fixedDelay 300000) public void autoSettleTimeoutOrders() { // 查询所有超过6小时仍未结账的进行中订单 Date deadline new Date(System.currentTimeMillis() - 6 * 60 * 60 * 1000); ListOrderInfo timeoutOrders orderInfoMapper.selectTimeoutOrders(deadline); for (OrderInfo order : timeoutOrders) { try { settleService.settleOrder(order.getOrderId()); } catch (Exception e) { // 单条订单失败不影响其他订单处理记录日志即可 log.error(自动结账失败订单号{}, order.getOrderNo(), e); } } } }这里有一个容易翻车的细节Scheduled注解默认是单线程执行的如果一个订单的处理时间比任务间隔还长下次任务会在上次没跑完时排队等待。不过对于毕设项目订单量不大这个问题基本不会发生。真正要注意的是fixedDelay和cron的区别——fixedDelay是上次执行完等 5 分钟再跑cron是固定时刻跑如果用的是 cron 表达式要特别注意时区问题否则可能出现定时任务在整点后 8 小时才执行的玄学现象。4.2 掉线重连和订单恢复一张表搞定幂等处理无人系统的另一个边界场景是网络波动。顾客开台后前端页面跟后端断开连接重新连上时订单还在不在答案是肯定在因为订单状态存在 MySQL 里而不是前端内存里。这个场景看起来简单但它牵扯出一个必须处理的工程问题——前端重连后如何恢复订单状态。常规做法是提供一个查询当前进行中订单的接口前端重连或刷新后调用它。这个接口根据会员 ID 查order_info里status 1的订单如果存在就恢复展示计时中的页面如果不存在就展示空闲状态。这个接口本身没什么技术含量但它保证了系统的无人特性——就算没有店员介入订单也能自洽地恢复。幂等处理上重点是防止前端在断线重连后重复提交结账请求。前端没收到响应会因为超时重发后端如果没有幂等保护同一个订单会被扣两次款。除了前面讲到的FOR UPDATE行锁还有一个兜底方案在订单表加一个version字段结账时执行UPDATE order_info SET status 2, version version 1 WHERE order_id ? AND version ?更新影响行数为 0 说明订单已经被结过账了直接忽略这次的请求。这种乐观锁方案适合用在状态一旦变更就不允许回退的业务上无人系统里订单状态恰好符合这个特征。4.3 余额不足时的半自动结账策略余额不足是无人台球厅必然出现的场景设计上要给出明确策略。最简单粗暴的做法是直接报错不允许结账但这会把顾客困在店里真实的球厅不会这么干。更合理的逻辑是如果余额不足以覆盖全部费用先扣光余额把差额记入欠款然后释放台桌下次该会员充值时优先抵扣欠款。这个策略对表结构提出了新要求需要在会员表加一个debt_amount字段。结账时代码变成两步先算总费用再判断余额是否够不够就部分扣款、记录欠款。这种设计在答辩时可以讲成业务闭环的健壮性设计——即使无人值守导致异常场景系统也能平滑降级而不是死锁。很多商业系统就是这么处理的你在一套毕设里体现出来评委老师会觉得你做的是工程而不是作业。5. 部署与答辩避坑自查SSM MySQL 项目最常见的 5 个翻车现场5.1 MySQL 8.0 驱动导致的 SSL 连接错误现象项目启动或首次连接数据库时控制台报Communications link failure后面跟着一串 SSL 相关的异常。原因MySQL Connector/J 8.0 以上版本默认开启 SSL 连接而本地 MySQL 5.7 的 SSL 配置不完整握手失败导致连接中断。解决在 JDBC 连接串上显式关闭 SSL 并指定时区jdbc:mysql://localhost:3306/billiard?useSSLfalseserverTimezoneAsia/Shanghai。如果你用的是 MySQL 5.7.44 安装包配环境第一次跑 SSM 项目遇到这种错误80% 是这个原因。5.2 MyBatis 的 mapper XML 没被扫描到运行时报Invalid bound statement现象Service 层调用 mapper 接口方法时抛BindingException: Invalid bound statement (not found)但代码编译和启动都没报错。原因pom.xml里没有把src/main/resources下的 mapper XML 文件打包进 target或者 Spring 配置里的mapper-locations路径没配好。默认情况下 Maven 只打包resources目录如果 XML 放在java包目录下构建时会被忽略。解决在pom.xml的build节点里加资源过滤把**/*.xml也打包进去同时确认 Spring 配置里写的是classpath:mapper/*.xml。这个坑能卡住一半的 SSM 新手排查顺序先看编译后 target 目录下有没有 XML。5.3 中文乱码数据库建库时没指定 utf8mb4现象插入的中文正常但查询出来是问号或者页面上显示乱码。原因MySQL 5.7 默认字符集是latin1如果建库时没指定DEFAULT CHARACTER SET utf8mb4即使表结构里写了 utf8mb4 也不起作用。另一个原因是 JDBC 连接串少了characterEncodingutf8参数。解决建库语句改成CREATE DATABASE billiard DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ciJDBC 连接串追加characterEncodingutf8。注意 MySQL 的utf8字符集实际只支持最多 3 字节不要在表情符号这类 4 字节字符上踩utf8的坑直接用utf8mb4最稳妥。5.4 Tomcat 启动慢或端口被占用现象启动 Tomcat 时报Port 8080 was already in use或者启动后页面一直转圈打不开。原因本地可能有其他进程占用了 8080 端口或者 Tomcat 配置的 JDK 版本不对。很多同学在本机装过多个 JDKJAVA_HOME指向了旧版本而项目是 JDK 8 编译的运行在 Tomcat 9 上是兼容的但如果你不小心用 JDK 11 跑 Tomcat 8Scannotation 或 JSP 解析会出各种奇怪问题。解决Windows 上用netstat -ano | findstr 8080找到占用进程的 PID确认不是系统关键进程后直接在任务管理器里结束JDK 环境变量统一检查一遍确保java -version和JAVA_HOME指向同一个版本。5.5Transactional不生效数据异常无法回滚现象Service 层方法里插入了订单但第二个更新语句报错事务没有回滚数据库里留下了一条孤儿订单。原因三个经典误用——一是方法被同一个类的另一个方法内部调用Spring 的 AOP 代理失效this.openTable()调不到代理对象二是方法不是public代理拦截不到三是数据库表用了 MyISAM 引擎不支持事务。解决事务方法从 Controller 层通过注入的 Service 对象调用不要在类的内部this调用检查方法修饰符是public建表语句统一用 InnoDB 引擎。这条排错经验在答辩前一定要自查一遍因为事务回滚是 SSM 项目的核心考点一旦演示时翻车这一题的分数基本就没了。6. 让无人演示出说服力造数据、模拟超时订单与一套验收清单毕设答辩时老师最想看到的不是你把 CRUD 跑通而是你验证过业务的特殊情况。这里我把自己做项目时的验证方法分享给你准备一套固定的演示脚本按顺序操作每一步都在 30 秒内能完成。第一个必演场景是并发抢台。开两个浏览器窗口登录两个不同的测试账号同时点击开台同一个台桌演示第二个请求被拒绝提示台桌被占用。这个场景直接验证了 Service 层的原子更新逻辑比讲十页 PPT 都有说服力。第二个场景是超时订单自动结账。手动把正在进行的订单的start_time改成 7 小时前等定时任务下一次扫描触发展示订单从进行中变成已结账、台桌从使用中变成空闲。这个操作全在数据库里完成不超过一分钟。第三个值得演示的是余额不足的降级路径。用一个余额只有 20 元的账号开台然后把结账时间调到一个多小时以后结账时展示系统扣光余额、把差额记为欠款、台桌状态正常释放。这三个演示串起来覆盖了正常开台结账、异常超时兜底、余额边界降级正好对应 Service 层代码里写的三套核心逻辑。做完整套项目后我自己的习惯是把所有踩过的坑整理进开发文档尤其是环境变量、JDK 版本、MySQL 字符集这三类。你在正式答辩前按新电脑部署一遍的标准重走一次流程如果全新环境下半小时能跑起来说明依赖配置是干净的如果走了三小时那说明很多依赖在你这台机器上才能跑通趁早补文档。这比多写两个没必要的功能更有价值。这个方向值不值得做我的判断是值得——它比商城项目更能说清事务、并发、定时任务这些 Java 核心概念又比纯管理系统多了一点真实业务的味道。希望这些拆解和踩坑记录能帮到你愿你的毕设一次通过、答辩顺利。本文还有配套的精品资源点击获取