做无人羽毛球馆系统,核心从来不是把代码写出来,而是把“人不在场时也能正常做生意”这件事做稳。我前前后后折腾过两版方案,第一版只做了个订单页面,结果扫码枪一断网整个场子就瘫了;第二版才把订单、门禁、计费、告警全部串起来,才敢说这是个能落地的“源码实现方案”。今天我把这套Java后端的设计思路、关键代码片段和踩过的硬件坑一并梳理出来,想自己做一套无人场馆管理系统、或者在课程设计里拿这个题目当主线的朋友,可以直接拿这份方案做底子。
这套系统的本质,就是用Java后端把微信小程序、智能门禁、灯光控制器、摄像头和计费规则全部串起来。用户在小程序里选场地下单,后端生成订单二维码,门禁扫码开门的同时推送开灯指令,计时结束自动扣款、自动关灯,全程不需要任何人工干预。适合的场景很明确:小区会所里的羽毛球馆、园区里的球馆分时租赁、学校体育馆的非工作时间段开放,只要场地具备网络覆盖和独立电控条件,这套方案都能套上去。
1. 整体架构设计与技术选型
无人场馆系统看起来只是“在线订场+自动开门”,但真正落地时你会发现,最难的不是某个单点功能,而是多个设备和服务之间的状态同步。我先梳理一下我最终采用的架构方案,再解释为什么这么选。
1.1 为什么用Java而不是Node.js或者Python
先说结论:用Java做这类系统的后端,并不是因为它写起来最快,而是因为它最适合“长时间无人值守”这种场景。
- 稳定性:Spring Boot应用可以长时间运行不重启,JVM的垃圾回收机制成熟,内存管理可控。我之前用Python写过一版闸机控制服务,连续跑了一周后内存涨了快两倍,排查了半天才发现是某个HTTP客户端连接泄漏。换成Java的OkHttp连接池之后,这个问题再没出现过。
- 生态成熟:微信支付、小程序登录、消息推送这一套,Java的SDK是最全的,文档案例也最多。遇到问题搜解决方案,Java相关的踩坑帖明显比Python多一个量级。
- 团队协作:如果这个系统以后要交给别人维护,Java后端的招人成本最低,几乎每个做后端开发的都能上手改Spring Boot项目。
当然,Java也有它的缺点:启动慢、部署包大。但对无人场馆这种后端常驻进程的场景来说,这些缺点完全不影响。
1.2 系统分层与模块划分
我第二版架构把系统分成了四层,每一层只干一件事:
设备接入层:负责和门禁控制器、灯光控制器、摄像头通信。门禁用HTTP接口控制,灯光用串口RS485指令控制,摄像头走ONVIF协议拉流。这一层统一封装成DeviceService接口,上层业务不关心具体硬件型号。
业务核心层:处理订单状态机、计费引擎、押金退款、超时告警。这是整个系统最复杂的部分,因为订单状态不仅仅是“待支付/已支付/已结束”,还有很多异常状态需要处理,比如“已开门但未开始计时”、“超时未开门自动取消”、“超时未离场自动续费”。
应用服务层:提供小程序端API和后台管理API。小程序端接口包括场地列表、创建订单、获取动态二维码、查询订单状态;后台接口包括场地管理、价格策略配置、设备状态监控、订单人工介入。
基础设施层:MySQL存订单和用户数据,Redis存二维码和场地状态,RabbitMQ做消息异步处理,XXL-Job做定时任务调度。
这套分层的好处是,每一层都可以独立替换。比如换一套不同品牌的门禁控制器,只需要改设备接入层的适配代码,业务层完全不用动。
1.3 核心业务时序流程
一个完整的使用流程是这样的:
- 用户在小程序里选择场地和时间段,提交订单并支付押金。
- 后端创建订单,生成一个有效期只有60秒的动态二维码,同时下发到小程序端。
- 用户到达场馆,在门禁扫码机上出示二维码。门禁控制器解析二维码后调用后端接口校验。
- 校验通过,门禁开锁,同时后端向灯光控制器发送“开灯”指令,并记录实际开场的开始时间。
- 用户预定时间结束前10分钟,后端推送微信订阅消息提醒用户续费或离场。
- 用户超时未离场,系统自动按超时时间追加计费,从押金中扣除。
- 用户离场后,系统默认设定为灯光自动关闭(以门禁再次打卡或摄像头检测空场为触发条件),计算总费用,从押金中扣除,剩余金额原路退回。
这套流程里最关键的设计,是**“实际开场时间”不等于“预定开始时间”**。用户可能提前到了,也可能迟到半小时,如果按预定时间计费,用户会投诉;如果按实际开门时间计费,后端就必须记录门禁验证通过并成功开锁的那个时间点。这个时间点以服务端收到门禁回调为准,而不是以门禁本地时间为准,防止设备时钟偏差导致计费争议。
2. 核心模块设计与关键代码实现
这一部分我把四个最核心的模块拆开讲:订单状态机、动态二维码门禁、计费引擎和灯光电源控制。每个模块我都会给出关键代码逻辑,但不贴完整工程,毕竟每个场馆的硬件型号和网络环境不同,你需要的是思路,不是可以无脑复制的死代码。
2.1 订单状态机设计
无人场馆的订单状态比普通电商订单多得多,我前前后后调整了四版状态枚举,最终稳定下来的是这六个状态:
| 状态 | 含义 | 触发条件 |
|---|---|---|
| PENDING_PAY | 待支付 | 用户提交订单但未支付押金 |
| PAID | 已支付待开场 | 押金支付成功,等待用户到达场馆 |
| PLAYING | 进行中 | 门禁扫码通过,场地开始计时 |
| TIMEOUT_EXTEND | 超时续费中 | 预定时间结束但用户未离场 |
| FINISHED | 已结束 | 用户确认离场或系统判定离场 |
| CANCELLED | 已取消 | 用户在开场前取消,或超时未扫码自动取消 |
这里容易踩坑的是PAID 到 PLAYING 的转换条件。第一版我用的是“门禁回调接口返回成功即为PLAYING”,但后来发现门禁控制器偶尔会重复回调同一个二维码,导致状态重复流转。解决办法是在门禁回调接口里做幂等校验:同一个二维码只能成功核销一次,如果门禁再次提交同一个二维码,返回“已被使用”错误,不再触发开灯和开始计时。
状态机流转代码我建议用策略模式实现,避免在Service里写一堆if-else:
public interface OrderStateHandler { void handle(OrderContext context); } public class PlayingStateHandler implements OrderStateHandler { @Override public void handle(OrderContext context) { Order order = context.getOrder(); if (order.getStatus() != OrderStatus.PAID) { throw new IllegalStateException("订单状态非法"); } order.setStatus(OrderStatus.PLAYING); order.setActualStartTime(LocalDateTime.now()); // 发送开灯指令 deviceService.controlLight(order.getCourtId(), true); // 推送开场通知给小程序 notifyService.pushPlayStart(order.getUserId(), order.getCourtId()); } }这样每新增一种状态流转,只需要新增一个Handler实现类,不影响已有逻辑。
2.2 动态二维码门禁方案
门禁二维码是整个系统里最容易出安全问题的环节。第一版我用的是“固定二维码+使用次数限制”,结果用户之间互相转发二维码,一个订单可以被四五个人轮着进出。后来改成动态二维码方案后,这个问题才彻底解决。
动态二维码的核心逻辑是:二维码内容本身不是一个固定的订单号,而是包含订单号、随机令牌和过期时间的加密字符串,并且每次扫码后都会刷新。
public String generateDynamicCode(Long orderId) { String token = UUID.randomUUID().toString().replace("-", ""); long expireAt = System.currentTimeMillis() + 60 * 1000; String raw = orderId + ":" + token + ":" + expireAt; // 用Base64编码,门禁端可以直接解码 return Base64.getUrlEncoder().encodeToString(raw.getBytes(StandardCharsets.UTF_8)); }门禁端扫码后,把解码出来的字符串提交回后端,后端验证三点:订单属于当前用户、当前时间小于expireAt、token没有被使用过。三点都通过,才执行开锁。
这里有一个问题需要注意:二维码刷新频率。如果每次点击都刷新,用户体验很差,因为用户在门禁前可能要点好几次才成功。我的方案是:入场前二维码60秒刷新一次,用户到达门禁前只需要保证最后一次刷新时间在60秒内就行。入场后,二维码立即失效,防止用户出场时拿同一个码再次开门。
2.3 计费引擎与押金自动退款
计费引擎是整个系统里最容易被忽略、但直接影响用户信任感的部分。羽毛球馆的计费规则看着简单,实际上有很多边界情况要考虑:
- 用户迟到20分钟,但预定时间没变,按实际开场时间到预定结束时间计费。
- 用户超时15分钟离场,超出的15分钟按分钟单价计算,不再是整段加收。
- 押金预授权,开场时不扣款,结束离场后统一从押金中扣除,剩余金额原路退回。
计费引擎我用的是策略模式,不同时段(高峰/非高峰)配置不同单价,超时部分再套一个超时单价。核心逻辑如下:
public BigDecimal calculateFee(Order order, TariffPolicy tariff) { LocalDateTime start = order.getActualStartTime(); LocalDateTime end = order.getActualEndTime(); long minutes = ChronoUnit.MINUTES.between(start, end); BigDecimal total = BigDecimal.ZERO; while (minutes > 0) { LocalDateTime sliceStart = end.minusMinutes(minutes); TariffRate rate = tariff.getRateForTime(sliceStart); long sliceMinutes = Math.min(minutes, rate.getStepMinutes()); total = total.add(rate.getPricePerMinute().multiply(BigDecimal.valueOf(sliceMinutes))); minutes -= sliceMinutes; } return total.setScale(2, RoundingMode.HALF_UP); }这里有一个必须处理的边界:如果扣款后押金不够覆盖超时费用怎么办。比如押金200元,用户打了2小时,费用120元,超时1小时又产生80元费用,加起来正好200元,没问题。但如果用户超时2小时,费用超过押金余额,那就不能只扣款,而是要先把订单标记为“欠费”,推送给用户补缴,补缴成功后才能释放场地,否则会一直被超时费用累积下去。这个逻辑不写清楚,财务对账时一定会出问题。
2.4 灯光电源控制与异常断电保护
羽毛球馆的灯光控制不是简单的“开/关”两个指令,因为灯管有启动电流,如果同一个配电箱下的所有灯同时开启,瞬时电流过大容易跳闸。我测试时遇到过一次,10片场地同时开场,灯光控制器把总闸给冲了,整个场馆一片黑。
后来改成分批启动策略:灯光控制器接收到开灯指令后,不一次性把所有灯全部打开,而是每隔300毫秒开一组。比如4盏灯一组,1.2秒内全部启动完毕,这个电流增量不会超过配电箱的额定负载。
控制指令我用的是RS485串口协议,Java端通过Modbus4J库发送Modbus RTU指令:
public void controlCourtLight(Long courtId, boolean on) { ModbusMaster master = ModbusMasterFactory.createModbusMaster("tcp://192.168.1.100:502"); master.setTimeout(2000); master.setRetries(2); int coilAddress = getCoilAddress(courtId); if (on) { // 分批开启,每次操作一个继电器 for (int i = 0; i < lampGroups.get(courtId).size(); i++) { master.writeSingleCoil(coilAddress + i, true); Thread.sleep(300); } } else { master.writeSingleCoil(coilAddress, false); } }还有一个容易忽略的细节:断电恢复后的灯状态。如果场馆断电重启,灯光控制器的输出端口默认是全部关闭的,但这时候如果订单还处于“进行中”状态,用户会发现自己被锁在黑暗里。这需要在启动时加一个恢复逻辑:扫描所有PLAYING状态的订单,对每个未结束订单重新发送开灯指令。
3. 硬件设备对接与物联网通信细节
这一部分可能是网上大多数“源码分享”不会细讲的内容,因为纯代码层面的方案一看就懂,但一旦和真实硬件打交道,你就会发现坑全在协议和物理连接上。我把我趟过的硬件对接经验写清楚,至少能帮你省一个月的排查时间。
3.1 设备选型与通信方式对比
无人羽毛球馆用到的硬件设备不算多:门禁控制器、电插锁、灯光控制器、摄像头、扫码枪。但不同设备之间的通信协议千差万别,选型时如果只看价格不看协议兼容性,后期开发成本会高出好几倍。
| 设备类型 | 通信方式 | 推荐品牌/型号 | 注意事项 |
|---|---|---|---|
| 门禁控制器 | TCP/IP HTTP接口 | 中控智慧、熵基 | 必须支持动态二维码,有些低端型号只支持卡/指纹 |
| 电插锁 | 12V脉冲信号 | 金盾、意林 | 选断电开锁型,防止断电后用户被锁在场馆内 |
| 灯光控制器 | RS485 Modbus RTU | 正泰、德力西 | 需要支持多路继电器独立控制 |
| 摄像头 | ONVIF/RTSP | 海康、大华 | 用于离场检测和安防监控,不需要额外买AI盒子 |
选型建议:有条件的话尽量选同一品牌的设备,或者至少选协议文档完整的设备。我之前贪便宜买过一款白牌门禁控制器,说明书上只写了“支持二维码”,结果SDK里根本没有二维码核销接口,只能通过HTTP POST方式自己拼报文,折腾了半个月才跑通。
3.2 门禁HTTP回调与异常兜底
门禁控制器扫码后的动作流程是:扫码枪解码二维码 → 将解码内容POST到后端接口 → 后端校验通过后返回“允许开门”指令 → 门禁控制器驱动电插锁开锁。
这个流程有几个异常情况必须处理:
门禁端网络中断:如果门禁和后端之间的网络断开,扫码后门禁一直等不到后端响应,用户会被挡在门外。我的方案是门禁本地加一个“离线白名单缓存”,把最近10分钟内已成功校验过的二维码缓存在门禁本地,网络恢复后自动同步状态。但这个方案只适合单门禁场景,如果场馆有多个出入口,离线缓存会导致一个二维码被重复使用。
后端接口超时:门禁调用后端接口时,如果后端响应超过3秒,门禁会直接开锁(安全模式),同时把这次开锁记录放在本地队列里,等网络恢复后再上报。这个逻辑是个“两害相权取其轻”的做法——宁可让没付费的人进来,也不能让付费的人进不去。但这样就带来一个问题:如果用户本身没下单,凭一个伪造二维码触发了门禁的安全模式开门,这笔账就算不到用户头上。所以,离线宽限时间不能设太长,我最终调成60秒,60秒内后端无响应才触发安全模式。
3.3 离场判定与灯光联动关断
无人场馆里最难的不是开门,而是判断用户什么时候离场。如果判断错了,要么灯一直开着浪费电,要么用户还没走就自动断电引发投诉。
我试过三种离场判定方案:
方案A:门禁手动按钮离场。在场馆内门禁旁装一个“离场”按钮,用户出场时按一下,后端收到信号后结束订单,停止计费,延时10秒关灯(给用户走回门口的时间)。这个方案最可靠,但依赖用户自觉,如果用户忘了按,订单就会一直计时到天荒地老。
方案B:摄像头人形检测。通过摄像头RTSP流接一个MotionEye或者其他开源人形检测服务,检测到场地内无人超过5分钟,自动结算订单。这个方案对硬件要求高,普通摄像头在暗光环境下误检率很高,经常把地上的羽毛球包认成人。
方案C:灯控联动。在灯光控制器上加装一个功率传感器,检测到该场地照明功率掉到正常值以下(比如用户自己关了灯),就认为用户离场。这个方案适合有工作人员在场馆内巡检的场景,单纯无人值守时不建议用。
我的最终方案是“主用A方案,辅以超时自动断电兜底”:订单超过预定结束时间20分钟后,如果用户还没有按离场按钮,系统自动结算订单并断电。用户如果还没走,通过小程序“申诉”功能重新开场。这个方案虽然偶尔会被用户吐槽,但至少不会出现费用无限上涨的问题。
4. 从开发到上线的完整实操记录
这一部分我按实际落地顺序来写,从环境搭建、核心流程配置,到双端部署、告警与对账,每一步都是可以直接抄作业的内容。
4.1 开发环境与核心依赖配置
我的开发环境是:JDK 17 + Spring Boot 3.1.5 + MyBatis-Plus 3.5.5 + MySQL 8.0 + Redis 7.0 + RabbitMQ 3.12。微服务能不用就不用,这个项目单体应用完全够,拆微服务只会增加部署和运维成本。
pom.xml里需要引入的核心依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>4.5.0</version> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>4.5.0</version> </dependency> <dependency> <groupId>com.digitalpetri.modbus</groupId> <artifactId>modbus4j</artifactId> <version>1.0.0</version> </dependency>数据库设计上,核心表有四张:用户表、场地表、订单表、流水表。其中订单表是最核心的,字段至少包含以下内容:
- order_id:订单号,雪花算法生成,避免用自增ID暴露订单量
- user_id:下单用户ID
- court_id:场地ID
- status:订单状态(见2.1的状态枚举)
- scheduled_start / scheduled_end:用户选择的预定时间段
- actual_start:门禁实际开门时间
- actual_end:离场或断电结算时间
- deposit_amount:押金金额
- total_amount:最终扣除费用
- refund_amount:退款金额
- extend_count:超时续费次数,防止用户无限次续费
4.2 双端部署方案
后端我部署在一台2核4G的云服务器上,操作系统是Ubuntu 22.04,用Docker Compose编排MySQL、Redis、RabbitMQ和应用服务。为什么不直接用云数据库?因为云数据库按量收费,对一个初期可能只有二三十个订单的场馆来说不划算,自建数据库完全够用,后期量大了再迁云也来得及。
Docker Compose文件的核心配置:
version: "3.8" services: mysql: image: mysql:8.0 container_name: badminton-mysql environment: MYSQL_ROOT_PASSWORD: yourpassword MYSQL_DATABASE: badminton volumes: - /data/mysql:/var/lib/mysql ports: - "3306:3306" redis: image: redis:7.0 container_name: badminton-redis ports: - "6379:6379" app: build: . container_name: badminton-app depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod ports: - "8080:8080"这里有一个经验:数据库连接池大小一定要设得保守。无人场馆的业务量不大,但门禁回调、微信支付回调、小程序端查询可能同时进来。连接池设太大会把MySQL的连接数打满,设太小又会在高峰时段排队。我最终设置为20个最大连接数,对这个规模的项目来说绰绰有余。
4.3 消息推送与自动告警
用户在场馆里使用系统,最烦的是“现场没人、出问题不知道找谁”。我的处理方式是分层告警:
- 用户侧告警:开场前推送微信订阅消息,超时前5分钟推送续费提醒。这个用小程序订阅消息模板即可,但订阅消息只能推送一次,所以我在Redis里存了一个推送标记,开场时先发送“已开场”通知,超时前再发“即将超时”通知,两个模板分别对应不同的推送动作。
- 运营侧告警:设备掉线、门禁接口连续报错、灯光控制器无响应,这些异常不会直接推给用户,而是通过邮件+短信发送给场馆管理员。短信用阿里云短信服务,邮件用JavaMailSender,配一个简单的告警规则类就可以实现。
这里最容易被忽视的一个告警场景是:微信支付回调失败。如果用户已经付款,但微信回调因为网络原因没有到达后端,订单会一直停留在“待支付”状态,用户进不了门。我的兜底方案是提供一个“支付结果主动查询”接口:小程序端在用户点击“我已支付”后调用后端,后端使用微信支付订单查询接口查询真实支付状态,如果已支付则立即更新订单状态并下发门禁二维码。这个接口必须在用户端和后台管端都能触发,是消除“支付了进不去”这类投诉的关键。
4.4 对账处理与账单核算
无人场馆没有前台,账目全部自动流水,对账逻辑必须严谨。每笔订单结束后,后端会生成一条账单记录,包含:订单收入、退款金额、实际扣款、平台手续费。每天凌晨,定时任务把所有账单汇总,和微信支付商户后台的账单做比对,差异超过1元的自动标记为异常订单。
对账的代码逻辑不复杂,但有一个时间字段很容易被忽略:微信支付账单里的时间戳是时区无关的UTC时间,而本地订单表用的是东八区时间。如果直接比对,每天会漏掉30多笔订单。我在比对前统一用Instant类型做转换,再按订单号关联,这个问题就解决了。
对账结果如果有异常,不自动处理,只推送告警给管理员,由人工在后台查看和处理。自动对账处理风险太大,万一写错退款规则,资金损失和客诉都很难收场。
5. 常见问题与排查技巧实录
我在实际开发和小规模试运营中遇到了不少问题,选几个有代表性的记录下来,希望能帮你少走弯路。这些问题网上大部分教程不会写,但它们真实存在于每一天的运行中。
5.1 微信回调与本地订单状态一致性
第一个大坑是微信支付回调。用户在小程序里支付成功后,微信服务器会异步回调后端接口,但这个回调不是100%可靠的。我在试运营中遇到过4次回调丢失,都是用户已经支付成功,但后端一直没有收到通知,导致订单卡在待支付状态,用户无法入场。
排查方式是实时查看微信号的支付记录和本地订单状态对比,一旦发现本地订单支付状态过期,主动调用微信支付订单查询接口刷新状态。代码可以这样处理:
public void refreshOrderPayStatus(String orderId) { WxPayOrderQueryResult result = wxPayService.queryOrder(null, orderId); if ("SUCCESS".equals(result.getTradeState())) { updateOrderToPaid(orderId); } }这类问题建议用一个定时任务兜底。比如每隔5分钟扫描一次创建时间超过5分钟且状态仍为待支付的订单,主动查询微信支付状态,查到了就更新本地订单,查不到就保持待支付状态。这个定时任务不复杂,但能避免90%的“付了钱进不去”问题。
5.2 门禁重复回调与锁冲突
另一个高频问题是门禁控制器的重复回调。有些型号的门禁控制器在扫码后会同时向两个端点请求,或者网络不好时会自动重试,导致同一张二维码被提交了多次。如果不做幂等处理,后端可能会重复扣款、重复开灯。
我的处理是:在Redis里以二维码token为key,设置一个“已使用”标记,并加一个较短的过期时间(比如10分钟),每次门禁回调先查询这个key是否存在:
public boolean checkAndMarkQrUsed(String qrToken) { // SET NX EX 10,只允许第一个请求设置成功 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent("qr_used:" + qrToken, "1", Duration.ofMinutes(10)); return Boolean.TRUE.equals(success); }这个方案看起来简单,但解决了一个大问题:即使门禁端不做任何防重逻辑,后端也能保证同一个二维码只能被成功核销一次。
5.3 灯光继电器粘连与安全检测
灯光控制器用久了,继电器可能会出现“粘连”问题:收到关灯指令后,触点没有真正断开,灯还是亮的。这个问题在无人值守场景下特别影响电力成本。
我最初的应对方案是:在关闭灯光后,通过功率传感器检测该场地实际功率是否降到阈值以下,如果5秒后功率仍高于阈值,判断为继电器故障,自动发送告警并切断该场地总闸电源。后来又加入了一个更彻底的方案:每天凌晨对所有场地进行一次继电器联动测试——发送开灯指令,等待3秒,再发送关灯指令,同时对比功率数据,发现异常就自动标记该继电器需要检修。
这个“自检”逻辑虽然简单,放在真实场景里很实用,因为大多数场馆管理员根本不会主动去检查每个继电器的状态,系统自动巡检能提前发现问题。
5.4 二维码放大与打印扫码识别问题
最后分享一个可能只有真正落地才会遇到的小问题:用户在小程序里打开的二维码,如果在手机上放大后打印成纸质版,很多普通的扫码枪会识别失败。原因是二维码的**静区(quiet zone)**太小,打印出来边缘被裁掉了。
解决办法是小程序端生成二维码时,在二维码周围保留至少4个模块宽度的白色边距;同时购买扫码枪时尽量选择支持“识读破损/污损二维码”的高端型号,普通百元级扫码枪在亮光下的识别率会差不少。如果场馆入口的照明条件不好,我建议加一个扫码灯箱,类似便利店那种扫码盒,能大幅提高识别成功率。
6. 这套方案的后续扩展方向
项目主体功能跑通之后,能扩展的方向不少,写几个我实测过思路的方向,仅供参考。
多场馆连锁管理:在场地表加一个场馆ID字段,把订单、设备、价格策略都按场馆维度隔离,就可以一个后台管理多个场馆。消息推送也可以按场馆维度的管理员配置告警,用户端则根据场馆显示不同的场地列表和价格表。
动态价格策略:把价格策略配置从写死改为数据库可配置,支持不同时段(工作日/周末/节假日)设置不同单价。高峰时段自动涨价,非高峰时段推出“早鸟价”“夜间特惠”,这些都可以在Spring Boot里用策略模式加一张价格配置表实现,不需要改代码。
AI离场辅助判定:摄像头人形检测准确率不够的问题,后来我换了基于YOLOv8的本地推理方案,把检测模型部署在球场入口的小型Linux盒子上,只在场地无人时触发离场判断。这个方案比云端的延迟低,隐私性也好,但部署成本会高一些,适合订单量起来之后再考虑。
会员储值卡:很多球馆的刚需功能是会员储值、次卡、包月。这个可以抽象成“卡券系统”,订单结算时先抵扣卡券余额,再用押金补足差额。卡券系统的核心是流水表要记录每一笔卡券变动,防止用户反复用同一张卡券套利。
我个人在实际操作中体会到,无人自助场馆这类项目,最怕的不是技术难题,而是线上线下状态的“错位”。你永远不知道用户会用什么姿势操作:有人进门扫码成功却进了隔壁场地,有人超时了故意不走等到断电才离场,还有人在门禁前站了两分钟结果发现二维码已经过期。这些边界情况的处理逻辑,才是这套Java后端方案里最有价值的部分。如果你正在做类似的项目,我建议先把订单状态机和设备异常处理流程打磨好,再去优化界面和用户体验。代码可以慢慢写,但状态不一致的问题,越早发现越容易修。