1. 从行业痛点说起:无人共享棋牌茶室台球室到底在解决什么问题
这两年,"自助棋牌室""无人茶室"在二三线城市几乎是爆发式增长。我接触过好几个从传统茶楼转型的老板,他们说得最多的一句是:"以前开茶楼要养服务员、收银员、保洁,三班倒还怕员工私吞订金,现在搞无人共享,一个人管三家店都行。"
这句话基本概括了无人共享棋牌茶室台球室的核心价值:用一套系统替代日常人力管理,同时保证包间使用率的最大化和账目的透明。
在我的理解里,这类项目本质上是一个"线下空间的自动化运营系统",核心业务链路并不复杂:
- 用户在小程序或APP上选择门店,查看空闲包间;
- 预约时段、支付押金或全额费用;
- 到店后通过扫码或输入订单号开门;
- 进门后自动通电(灯光、麻将机、空调、热水壶);
- 使用过程中可续费、呼叫客服;
- 结束后系统自动结算、锁门断电,推送账单给用户。
听起来简单,真正落地时会遇到一堆问题:门禁设备怎么接入?断电后麻将机被卡住怎么办?用户超时不走如何处理?退款争议怎么仲裁?这些细节决定了系统是"能用"还是"好用"。
我这次选择用Java作为主语言实现整个后端服务,原因很直接:团队对Spring Boot最熟,生态成熟,支付、WebSocket、任务调度、物联网设备对接都有相对稳的解决方案。如果你问我为什么不用PHP或Node.js,我的回答是:这个场景里Java不是最强的,但它是"最不让人操心"的。后面我会详细说。
这篇文章就是我这套系统的完整复盘,从架构设计到每个核心模块的实现思路,再到实际运营中踩过的坑,希望对准备入局这个赛道的朋友有帮助。
2. 系统总体架构:一台服务器管三家门店是怎么做到的
先说结论:整套系统做成了一台服务器多门店的模式,也就是SaaS架构。每个门店虽然名字、地址、包间数量都不同,但共用同一个后端服务、同一个数据库实例,只是用storeId区分数据归属。
这个选择是经过对比的。最开始的方案是每家门店独立部署一套系统,好处是故障隔离,坏处也明显——硬件成本高、升级麻烦、数据没法汇总看板。后来改成多租户单应用模式,一台4核8G的服务器就能扛住三个门店的日常并发,月成本比独立部署低了将近三分之二。
提示:多租户模式下最关键的是数据隔离。我在所有核心表都加了
store_id字段,查询时强制带上该条件,否则一旦出现跨店数据泄露,运营事故就跑不了。
2.1 整体技术选型与依赖清单
这套系统的技术栈如下:
| 模块 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 稳定,社区资料多,招人容易 |
| 持久层 | MyBatis-Plus | 单表CRUD几乎不用写SQL |
| 缓存 | Redis | 门禁鉴权、会话状态、热点数据 |
| 数据库 | MySQL 8.0 | 业务数据落地 |
| 任务调度 | Quartz + Redisson 分布式锁 | 超时检测、自动结算、日结报表 |
| 实时通信 | WebSocket + Netty | 设备状态上报、开门指令下发 |
| 支付 | 微信支付V3 + 支付宝 | 押金、支付、退款 |
| IoT对接 | 自研协议适配层 | 对接不同品牌门禁、电控设备 |
| 管理端 | Vue3 + Element Plus | 独立运营后台 |
这里我想特别强调一下选型时的逻辑。很多人一看到物联网,立刻想到要用Netty甚至自研协议栈,其实不必要。市面上成熟的门禁控制器、智能电表、麻将机感应板,大多提供的是HTTP回调或者TCP长连接,真正需要自己从零写协议的极少。**核心难点不在协议本身,而在设备异常状态的兜底处理。**比如你给门禁控制器下发了一条开门指令,TCP超时了,你并不知道门到底开了没有——这就要靠状态回查机制。
2.2 硬件设备的协作关系与数据流
硬件层面,每个包间部署的设备包括:人脸识别门禁一体机、智能断路器(控制灯光和麻将机电)、烟雾传感器、红外人体感应器、水电能耗采集模块。这些设备通过店内局域网接入一台边缘网关,网关再与云端服务器通信。
数据流大致是这样的:
- 用户小程序端提交预约订单,MySQL写入订单表,状态为
待支付; - 支付成功后,订单状态变为
已支付,同时生成一个有效期内的入场凭证(二维码); - 用户到店扫码,小程序把二维码发给后端,后端校验订单有效性后,调用门禁设备接口下发开门指令;
- 门禁打开的同时,智能断路器合闸,包间通电,系统开始记录使用时长;
- 麻将机通过感应板检测到有人开始摸牌,状态同步到门店大屏;
- 用户离店关门后,人体感应器未再检测到人体,系统进入"预结算"流程,等待5分钟确认后自动结算断电。
这套流程在架构上就是三个核心子系统:订单履约子系统、设备控制子系统、财务结算子系统,外加一个运营管理后台。接下来我一个个拆解。
3. 订单与预订模块:时间分片和包间状态机设计
无人共享场景下最核心的领域模型是包间(Room),而包间的状态流转是整个系统的心脏。我把它设计成了五态:
| 状态 | 含义 | 触发动作 |
|---|---|---|
| IDLE | 空闲 | 无订单、设备全部复位 |
| LOCKED | 已预订未使用 | 用户支付了订金或整单费用,等待入场 |
| IN_USE | 使用中 | 用户已开门且包间通电 |
| PAUSED | 暂停计费 | 用户临时离开,系统暂停计时 |
| MAINTENANCE | 维护中 | 保洁或维修人员进入包间 |
这个状态机看起来简单,但实现时容易出问题的点在于并发预约。比如同一个包间,19:00-20:00的时间片被人订了,另一个用户想订19:30-20:30,如果只判断"当前包间是否空闲",那第二个人也能订成功,但到了现场就会撞车。
所以订单模块我引入了一个"时间片冲突校验"的概念,核心逻辑是:同一包间在同一时段只能有一个有效占用的订单。不用数据库锁,而是用Redis的分布式锁 + 数据库唯一索引兜底。
// 伪代码:时间片冲突校验 public boolean isTimeSlotAvailable(Long roomId, LocalDateTime startTime, LocalDateTime endTime) { String lockKey = "lock:room_slot:" + roomId; Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS); if (!Boolean.TRUE.equals(locked)) { throw new BizException("操作太频繁,请稍后重试"); } try { // 判断是否存在交叉订单 LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Order::getRoomId, roomId) .in(Order::getStatus, Arrays.asList("PAID", "IN_USE", "PAUSED")) .and(w -> w .lt(Order::getStartTime, endTime) .gt(Order::getEndTime, startTime)); Long count = orderMapper.selectCount(wrapper); return count == 0; } finally { redisTemplate.delete(lockKey); } }为什么用Redis锁而不是完全依赖数据库?因为under高并发时,MySQL的事务锁会导致连接池被占满,进而拖垮其他接口。加一层Redis锁可以快速挡住大部分冲突请求,只有拿到了锁的请求才去查数据库,性能会好很多。
另外,订单的状态不能只靠用户主动操作去更新。我在Quartz里配置了每30秒跑一次的"订单状态扫描任务",把PAID状态且超过预约开始时间15分钟仍未入场的订单自动取消并全额退款,这个补偿机制极大减少了"订了不来"导致的包间空转。
4. 设备控制与门禁联动:从开门到断电的每一步实现
设备控制模块是我花时间最多的地方。先声明一个背景:市面上的门禁控制器品牌五花八门,主流的有TCP/IP网络控制器、485总线控制器、人脸识别一体机,它们的接口协议各不相同。
我的做法是抽象出统一的设备接口,用策略模式屏蔽差异。
public interface DeviceGateway { DeviceResult openDoor(String roomId, AuthToken token); DeviceResult closeDoor(String roomId); DeviceResult powerOn(String roomId); DeviceResult powerOff(String roomId); DeviceStatus queryStatus(String roomId); }每个品牌的门禁设备实现这个接口,内部再去拼各自的HTTP请求或TCP报文。这样业务层完全不需要感知具体设备是什么牌子,后期新增设备厂商只需要新增一个实现类,不改任何业务代码。这就是设计模式在实战中最朴素也最有价值的应用。
4.1 开门指令的可靠性设计
开门这个动作,看着只是"用户扫码→调用设备接口→锁打开",实际执行时会遇到各种意外:
- 门禁设备断网了;
- 设备接口返回超时,但门其实已经打开了;
- 用户下单后还没走到门口,手机信号差,二维码刷新不出来;
- 门禁控制器和云端服务器不在同一个局域网,透传通道断了。
针对这些情况,我给开门指令设计了一套"三级降级"逻辑:
- 优先走云端直连:后端直接调用设备网关接口开门;
- 云端直连失败,走店内局域网边缘网关:边缘网关收到开门指令后,通过本地接口开门,不依赖云端在线;
- 边缘网关也失败,启用应急密码:门禁设备支持一次性动态密码,下单后同步推送到用户小程序,用户可在密码键盘上输入开门。
这套三级降级在实测中非常管用,把"用户到店开不了门"的客诉率从一开始的每周四五次降到了几乎为零。判断逻辑上要注意的是超时不能直接判定失败,必须做一次状态回查,否则可能重复下发指令导致门禁被锁死。
关于回查,我写过一个小工具方法,专门用来处理"指令下发后确认最终状态"的逻辑:
public DeviceStatus sendWithRetry(String roomId, BiFunction<String, AuthToken, DeviceResult> action, int maxRetry) { DeviceResult result = action.apply(roomId, currentToken); for (int i = 0; i < maxRetry; i++) { // 立即回查一次设备状态 DeviceStatus status = deviceGateway.queryStatus(roomId); if (status.isOperated()) { return status; } // 等待2秒再查一次 try { Thread.sleep(2000); } catch (InterruptedException ignored) {} } throw new DeviceTimeoutException("设备指令执行超时,请人工介入"); }4.2 离店自动断电与安全兜底
自动断电这块要格外谨慎。我最初的设计是"人体感应器检测到无人后,系统直接断电"。但运营一个月后,有老客户投诉说他的手机落在了包里,人在走廊打电话,感应器检测不到人,包间就断电了,打完牌回来手机落店里。
后来我把流程改成了"三段式离店确认":
- 人体感应器持续5分钟未检测到人体活动;
- 系统向用户微信推送"是否离店"确认消息,附带10分钟倒计时;
- 用户不回复,则系统再等10分钟,之后切断麻将机和空调电源,保留灯光5分钟后关闭。
这样一来,误断电率大幅下降,客户投诉量也减少了。安全兜底则是烟雾传感器一旦触发告警,系统立即打开门禁(防止门锁死影响逃生),同时推送告警给门店管理员。这是所有逻辑里优先级最高的一条,我单独抽了一个EmergencyHandler来处理,绝不和普通业务逻辑混在一起。
5. 计费规则与自动结算:如何避免超时、续费和退款纠纷
计费是无人场景下最容易引发纠纷的环节。传统有人前台可以上门催费、当面沟通,无人模式下全靠系统规则。所以规则设计要尽量透明,给用户充分的提前告知。
5.1 计费规则设计
目前采用了"基础时长费 + 超时费"的组合模式,没有搞太复杂的阶梯价格,核心是让用户在小程序里一目了然:
| 项目 | 规则 | 备注 |
|---|---|---|
| 基础费用 | 按预订时长整段计费 | 如订2小时按2小时计费 |
| 提前离店 | 按实际使用时长结算,多退少补 | 不足1小时按1小时计费 |
| 超时 | 每超时15分钟按半小时费率的50%收取 | 超时上限30分钟,超后自动断电锁门 |
| 押金 | 房间押金=基础费用的50%,封顶100元 | 结算后自动原路退回 |
这个计费逻辑里有个比较隐蔽但实用的点:不足1小时按1小时计费。很多同行会按"分钟粒度"精确计费,听起来更公平,但实际会产生非常多的零头账单,用户体验其实变差了。棋牌茶室这类场景,用户普遍能接受"按小时取整",反而账单更清爽,退款也更少出争议。
5.2 自动结算的时序逻辑
结算模块不是简单算个钱就完事,它是一套完整的事务性流程。我把它拆成以下几步:
- 触发时机:订单结束时间到了,或用户在小程序主动点击"结束使用";
- 状态确认:校验门禁已锁、设备状态正常,如果没有,则发起一次强制关门和断电指令;
- 费用计算:读取Redis里记录的开机时间、加时记录,计算总费用;
- 扣费与退款:从押金中扣减费用,剩余部分原路退回;如果费用超过押金,则发起一笔新的支付请求给用户;
- 订单归档:写流水账单到财务表,同时生成经营日报数据。
这里最让我头疼过的是部分退款的幂等性。微信支付退款接口要求同一退款单不能重复提交,如果系统在结算时网络抖动,第一次退款成功了但回调没收到,Quartz重试时又发起了一次退款,用户就会收到双倍退款。
我的解决方案是引入refund_no(退款单号)和数据库唯一索引,加上一个refund_status状态机(INIT → PROCESSING → SUCCESS/FAILED),每次退款前都要检查当前状态,只有INIT才允许发起退款请求。这个思路和订单状态的幂等控制是一脉相承的——凡是涉及钱的表,必须设计状态机,不允许从代码逻辑里随意跳变。
5.3 超时占用处理
无人场景下最经典的冲突就是:用户A定的时间到了,但房间里的人还没走,用户B已经到了门口。系统必须能处理这种现实情况。
我把超时处理分成了"三步走":
- 软提醒:结束前10分钟、5分钟分别推送提醒给当前用户;
- 硬拦截:超时后,系统先自动发起"结束订单"流程,如果用户不结束订单也不离开,则启动"超时占用附加费"计费模式,每分钟记录一张超时计费单;
- 强制清场:超时满30分钟,系统强制断电锁门,同时通知用户取回遗留物品。强制清场需要在管理端留一个人工复核入口,防止出现"用户还在包间内、被锁在里面"的情况——所以锁门动作和人体感应器是有联锁的,如果检测到包间内有人,强制锁门指令会被拦截,仅做断电处理并触发人工介入通知。
这套逻辑上线后,"房客冲突"的客诉处理时间从平均40分钟降到了10分钟以内,大部分情况用户在收到附加费扣款提醒后就会主动离开。
6. 高并发场景下的缓存设计与数据一致性保障
无人共享棋牌茶室有一个明显的流量特征:晚上7点到11点是高峰,其他时段几乎没流量。高峰期时三个门店共三百多个包间同时在线,每5秒所有门禁设备都会上报一次状态,加上用户端频繁刷新包间列表,后端接口的QPS会瞬间冲到几千。
这种突发流量用传统关系型数据库硬扛肯定不行,必须上缓存。我的缓存策略分为三层:
- 第一层:包间状态缓存,使用Redis的Hash结构存储每个包间的最新状态,key为
room:status:{roomId},field为power、lock、person、updateTime等。设备上报时只更新Redis,不直接写MySQL; - 第二层:门店聚合信息缓存,使用Redis的String结构存储"门店总览数据",包括空闲包间数、使用中数、今日营业额等,每30秒刷新一次;
- 第三层:订单热点缓存,近期有效订单存放于Redis,key为
order:active:{orderNo},设置6小时过期,减少数据库查询。
设备状态上报接口的性能优化有个关键点:高频字段(设备状态)和低频字段(账单数据)分开存储,不要塞在同一个表里。我设计了两张表:device_status_realtime只保留最新一条状态,device_status_log记录每次上报的流水。查询大屏数据时只查实时表,排查问题时查流水表,这样既保证了查询速度,也不会因为日志爆炸影响核心链路。
说到数据一致性,有一个经典问题:Redis中的包间状态和数据库里的订单状态不一致怎么办?比如设备上报"门已打开",但订单模块还没更新状态,此时用户端看到的是"使用中"而订单实际还停在"已支付",这就会导致用户无法正常结束订单。
我处理这种方式很简单粗暴但有效:以数据库为准,Redis是加速层,最终一致性通过定时任务校准。每10分钟跑一次"对账任务",扫描所有IN_USE且Redis状态异常的订单,重新拉取设备状态修正缓存,并把状态变更写入操作日志。这个对账任务上线后,状态不一致导致的客服工单从每周30多个直接降到了3个以下。
7. 运营管理后台与BI看板:老板关心的不是技术而是数据
给老板做的管理后台,功能其实比用户端还多。我自己接触的这些棋牌茶室老板,他们懂经营但不一定懂技术,后台界面的逻辑必须完全贴合"门店管理"的真实流程。
我这边管理后台拆成了六个模块:
| 模块 | 核心功能 | 设计要点 |
|---|---|---|
| 门店管理 | 门店信息、营业时间、门店图册 | 支持多门店切换 |
| 包间管理 | 包间类型、设备绑定、计费模板 | 设备绑定采用"选择设备型号"的引导式配置 |
| 订单管理 | 订单列表、详情、退款操作 | 支持异常订单人工介入 |
| 财务管理 | 收入流水、提现记录、对账报表 | 所有金额变动须有操作日志 |
| 设备监控 | 在线状态、异常告警、远程控制 | 地图视图展示门店设备状态 |
| 数据分析 | 日/周/月经营看板、时段热度、用户复购 | 图表化展示,支持导出Excel |
数据看板里最有价值的一个指标是**"包间利用率"**。最初老板需求是看"营业额",但单纯看营业额并不能反映经营健康度——同样的营业额,可能是低价时段大量卖出的结果,也可能伴随着包间紧张、客户流失。后来我把利用率拆解成三个维度:时段利用率、包间周转率、峰谷配比。老板看完后立刻调整了定价策略,低谷时段降价约20%,整体营业额反而提升了约15%。这就是数据对经营决策的直接价值。
技术实现上,看板数据走的是异步多线程聚合。用户点击"今日经营报表"时,后端同时查订单表、退款表、充值表、设备日志表,用CompletableFuture并行执行,最后合并返回,响应时间基本控制在200毫秒内。
还有一个老板高频需求是**"实时视频监控"**。但这个功能我不太建议自己从零搭,直接接入现有云视频服务平台更省事。摄像头通过RTMP推流到云平台,管理后台嵌入视频播放组件即可。合规方面注意要在门店入口和包间内张贴"监控区域"提示。
8. 上线之后的踩坑实录:三个最典型的线上事故
8.1 场景一:顾客在包间里被"锁死",门禁指令冲突
上线第三周,凌晨一点多我接到值班电话,说3号门店有个包间门打不开,顾客在屋里出不来。我赶紧查日志,发现门禁控制器同时收到了"开门"和"关门"两条指令,由于指令执行没有做串行化,控制器执行到关门指令时直接按"非使用中"处理,把门给锁住了。
这个问题的根因是:WebSocket回调线程和Quartz定时任务线程同时处理同一个包间,两个线程各自发送了指令,设备控制器无法处理指令冲突。
修复方案很直接:所有针对同一包间的设备指令必须走同一个分布式的指令队列,按顺序消费。我在Redis里实现了一个简单的FIFO队列,包间ID为key,指令执行服务作为消费者从队列里取指令,确保同一时间只有一条指令在被执行。
注意:设备指令串行化这个设计,一定要在架构初期就加上。我当时图省事没做,结果出了大事故。分布式锁 + FIFO队列能规避绝大部分指令争抢问题。
8.2 场景二:押金退款卡死,用户投诉电话打爆
上线一个多月后,某天下午突然大量用户反映退款迟迟不到账。排查发现是我在服务重启时丢了部分未完成退款单。原因是我把"待退款"状态存在了内存Map里,服务重启后这些状态全都没了,数据库里订单还停在INIT状态,但内存里的消费任务已经没了,导致永远不会触发退款。
这就是典型的"本地内存做持久化"的反面教材。修复方案是把待退款队列迁移到Redis的List结构中,服务启动时自动扫描Redis中未完成的退款单,重新投递到退款消费线程。从此服务重启再也不会丢退款单。
这类问题在分布式系统里属于"消息丢失"的经典场景,通用的解决思路是:可靠的消息投递必须依赖外部存储(Redis、MQ或DB),绝不能依赖进程内存。用Redis List做轻量级MQ,对这类小规模系统完全够用。
8.3 场景三:商家端看板数据对不上账
第三个事故不出现在线上,但出现在财务对账。老板发现昨天的营业额和微信支付后台实际金额差了80多块。排查下来,原因是订单退款时,我直接在MySQL里更新了订单表的金额字段,但没有同步更新统计报表的汇总表。第二天定时任务重新汇总时,又把退款前的金额算了一遍,成了"虚增营业额"。
这种问题在开发里很常见:业务主流水和数据汇总表的数据一致性,如果靠定时任务"全量重算"解决,一旦中间有状态漏更新,数据就对不上。
我最终改造的方案是:引入"事件溯源"思路,所有资金变动(支付、退款、消费扣款)都写入一张finance_transaction_log表,报表模块只基于这张流水表做聚合计算,不再直接读订单表。这样业务表和报表表解耦,任何金额追溯都有据可查。这个改造之后,财务对账一次通过,再也没有出现过差账。
9. 项目成本参考与运营建议
最后说点实际的——这套系统从零开发到稳定上线大概花多少成本,以及如果不想完全自研,可以怎么分阶段落地。
| 成本项 | 参考预算 | 说明 |
|---|---|---|
| 服务器(云主机) | 每月200-500元 | 4核8G起步,带宽5M |
| 门禁设备(每包间) | 800-1500元 | 人脸识别一体机 + 门磁 |
| 智能断路器(每包间) | 300-600元 | 支持远程控制 |
| 传感器(每包间) | 200-400元 | 人体感应 + 烟雾报警 |
| 系统开发(自研人力) | 约2-3个月工作量 | 1个后端 + 1个前端 |
| 第三方接口费 | 按量计费 | 支付、短信、云视频等 |
如果不想一步到位,我的建议是先砍掉硬件联动,用"人工核销 + 密码锁"的轻量模式验证商业模型。也就是:用户在小程序下单,系统生成一个动态密码,到店后按密码进门,灯光和麻将机手动开关。这种模式只需要一个后端 + 一个密码锁硬件,开发周期能压缩到3-4周,但商业模式的核心闭环已经能跑通了——订单管理、计费、结算、客户管理全部在线化。
等验证单店模型确实盈利后,再逐步接入门禁控制器、智能断路器和传感器,把"无人值守"从"半自动"升级到"全自动"。这个路径对个人开发者或小团队来说,是我认为性价比最高的方式,不会一上来就被硬件适配的泥潭拖住。
另外说一句运营层面的题外话:系统做出来只是第一步,真正决定盈利的是选址和时段定价。我见过不少同行在技术选型上精益求精,但忽略了门店周边3公里内是否真的有足够的棋牌消费人群,结果系统再稳定也回不了本。技术和运营一定要两条腿走路。
10. 个人经验总结与下一步演进方向
这套系统从想法到稳定运行,我自己最大的体会是:无人共享场景的软件难点不在高并发,而在业务状态的一致性和异常兜底。你面对的不是服务器宕机这种极端故障,而是大量"用户行为不符合预期"的日常情况——订单订了不来、人走了没关门、设备报了故障但实际是正常的、用户超时了还想继续打牌。这些场景如果每一件都要人工介入,系统就谈不上"无人"了。
因此在设计时,我给自己定了一条准则:系统必须预设每个环节失败时的兜底路径,并且兜底路径要自动化。开门失败有密码兜底,结算失败有重试队列,设备上报异常有回查任务,退款失败有对账补偿。这些兜底并不会让代码更复杂,反而让系统更扛造——真正复杂的代码是基于这些兜底之上不断堆叠的"特殊情况处理"才算复杂。
如果接下来还要迭代,我重点关注三个方向:一是引入设备预测性维护,通过能耗数据和上报频率预测设备故障,提前安排检修;二是打通会员体系,让用户在跨店消费时共享卡券和积分;三是把经营数据能力变成SaaS服务向同行输出,这可能是比单店运营更值得探索的商业模式。
这套东西还有很大优化空间,尤其是智能定价和实时库存预测方面。如果你也在做类似的无人共享空间系统,欢迎在评论区和我说说你们的计费规则是怎么设计的,尤其是"超时占用"这块,我已经踩过不少坑,想看看有没有更好的处理思路。