ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

JAVA无人洗车系统源码设计:从设备控制到支付闭环的完整实践

JAVA无人洗车系统源码设计:从设备控制到支付闭环的完整实践 无人洗车这行这几年铺得特别快从北方到南方加油站、小区停车场、园区地库到处都能看到那种带卷帘门的“自动洗车房”。但说真的很多设备装上了运营却跟不上——要么设备状态不清楚要么支付链路经常出错要么对账靠人工Excel硬扛。我自己前前后后帮几个场地方做过类似的系统改造说实话一套稳定好用的JAVA无人洗车系统源码能省掉的运维成本不是一点点。这篇文章不聊空泛的概念直接从源码层面拆一拆一整套24小时扫码自助洗车系统到底该怎么设计、怎么搭、怎么避坑目标是把核心逻辑讲透给正要入局的朋友一个可以直接参考的蓝本。1. 无人洗车的技术脉络与方案全貌1.1 一套无人洗车系统“背后到底有什么”很多人以为无人洗车就是“装个扫码枪、接个小程序”真正落地之后才发现事情远没那么简单。一套完整的24小时自助洗车系统至少要含这几块东西设备控制层洗车机、龙门架、水泵、风机、泡沫喷头、水枪这些硬件不是“通电就能用”需要有一个可靠的控制信号通道一般通过PLC、继电器板卡或者工控机来响应云端指令。业务服务层这就是JAVA代码真正发力的地方负责用户登录、余额管理、下单计费、优惠券、会员套餐、扫码拉起设备、任务超时处理、设备状态监控。支付通道层微信、支付宝、聚合支付等扫码之后拉起支付支付结果要异步回调到服务端再由服务端下发设备指令。运营管理端老板、维护人员、财务要看的东西包括实时流水、设备在线情况、故障预警、分账结算、区域代理管理。如果只做了支付那一头设备靠人工开关那不叫无人洗车如果只做了设备控制支付靠人工收款那和传统洗车房没区别。真正的核心是业务、支付、设备三者闭环联动而这个闭环里最难做稳定的地方恰恰是那些看起来不起眼的细节比如支付回调丢失怎么办、设备指令没送达怎么办、用户扫码后迟迟不支付要不要释放资源。1.2 为什么用Java做核心服务选型这件事情我见不少团队绕了弯路。有些用Python写脚本快速验证有些用Node.js做小程序接口但跑到生产环境之后遇到并发、稳定性、运维监控的问题最后又回过头来重构。为什么JAVA在这个场景里特别合适首先是生态成熟。Spring Boot、MyBatis Plus、Redis、XXL-Job、RabbitMQ这些组件随便一组合就能搭出一套抗得住千万级订单的服务端骨架。对于设备控制、订单流水这种重状态、高一致性的场景Java的事务管理、多线程体系、异常处理机制都相对可靠。然后是团队人才储备。国内做后端开发的Java工程师基数巨大哪怕是三线城市也能很轻松找到会Spring Boot的人。这套源码如果选了小众语言后续交接维护会非常痛苦。我自己招人的时候简历里写“精通Java、熟悉主流框架”的人一抓一大把但敢说懂Modbus协议、懂RS485串口通信的少之又少。所以JAVA负责业务服务硬件通信放到设备端网关去处理这是一种非常务实的分工。再就是物联网生态的配合。设备端一般用C/C或者Python去跑嵌入式逻辑而云端服务用JAVA对外提供HTTP接口两边通过JSON MQTT或者HTTP TCP长连接交互这个模式在很多工业场景里已经跑了十多年稳定性和兼容性都经过了验证。1.3 这套源码到底解决了哪些痛点市场上现成的无人洗车系统其实不少但大多数是SaaS化的“黑盒子”。你花几万块买设备平台方给你开个账号设备数据、用户数据、交易数据全在别人手里想二次开发、想私有化部署、想对接自己的会员体系门都没有。用这套JAVA源码核心价值在于“完全拥有”。你可以把整套系统部署在自己的服务器上设备通讯协议自己定义数据库表结构完全开放运营数据随时可以导出分析。比如我想在某个加油站试点“洗车加油”的联营模式需要把洗车的用户订单同步到加油站的会员系统这种定制化需求用SaaS方案根本没法做但基于源码就能轻松实现。另外源码方案在成本上也有明显优势。按一套系统服务20台设备计算SaaS平台每年要按设备数收服务费三五年下来是一笔不小的开销。而源码IP一旦买断后续扩容只需要增加服务器配置硬件设备采购也只和厂商有关系整体运维成本可控得多。2. 系统架构与关键技术选型2.1 整体架构云端服务 设备端 小程序端这套系统在设计的时候我采用了非常经典的“端 - 云”分离结构。整体分成三层每一层各自独立演进第一层是用户触点层主要包含微信小程序、公众号H5、扫码页面。用户通过扫描设备上的二维码进入小程序选择洗车套餐、支付费用、发起洗车。小程序端最核心的两个体验指标是“扫码后3秒内能点开始”和“支付成功后10秒内设备必须启动”。用户不会关心你后端怎么转发指令他只知道“我付了钱车就得动”。第二层是业务服务层也就是JAVA后端主服务。主要模块有用户认证与会员体系JWT Redis Session、订单服务下单、支付回调、订单状态流转、设备控制服务向设备网关下发指令并记录指令回执、定时任务调度超时未支付订单、设备状态心跳检测、异常订单自动退款、开放接口小程序调用、后台管理系统调用。第三层是设备接入层。这一层不直接用JAVA和PLC通信而是在场地侧部署一个“边缘网关”通常是一块树莓派或者工业级迷你主机通过RS485或者Modbus TCP和PLC交互。边缘网关把PLC的开关量、传感器模拟量转换成标准JSON再通过MQTT或者HTTP接口上报给JAVA服务端。这么设计的好处是云端不关心具体是哪个品牌的洗车机只要设备网关能上报标准协议就能顺利接入。组装上还有一个小程序管理后台我用的是Vue Elment Plus那套后端就是Spring Boot提供RESTful API不做服务端渲染完全前后端分离。部署的时候Nginx托管前端静态文件后端跑在Docker容器里数据库用MySQL 8.0缓存用Redis。2.2 设备控制模块的设计要点设备控制是无人洗车系统里最考验细节的地方。很多新手写代码的时候总以为“下发指令”就是给设备发一个HTTP请求等设备返回成功就完事了。真实场景完全不是这样。设备执行一次洗车动作中间会有多个状态切换正常流程分这些状态IDLE空闲→POWER_ON开机→RINSE冲洗→FOAM打泡沫→BRUSH刷洗→RINSE_2二次冲洗→WAX打蜡→DRY风干→FINISH完成→IDLE空闲。流程中任意一个环节分到异常都要有对应的恢复机制。设备端的执行是异步的服务端下发“开始洗车”之后设备会按照PLC预置的顺序逐步执行每个步骤执行完会返回一个步骤状态回调。所以JAVA端不能简单用“一次性同步调用”而是要设计成状态机驱动。我用的是状态模式 事件驱动的方式订单对象内部维护一个状态机收到设备上报的步骤回调后触发状态流转。这里有一段核心的状态机定义代码直接给大家看一下public enum DeviceState { IDLE(0, 空闲), POWER_ON(1, 开机自检), RINSE(2, 冲洗), FOAM(3, 泡沫), BRUSH(4, 刷洗), RINSE_2(5, 二次冲洗), WAX(6, 打蜡), DRY(7, 风干), FINISH(8, 完成), ERROR(99, 异常); private final int code; private final String desc; DeviceState(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } /** * 判断当前状态是否允许跳转到目标状态 */ public boolean canTransitionTo(DeviceState target) { // 允许异常状态随时介入 if (target ERROR) { return true; } // IDLE和FINISH都可以直接回到空闲 if (target IDLE (this FINISH || this ERROR)) { return true; } // 其他情况按照枚举顺序流转 return target.ordinal() this.ordinal() 1; } }这套状态机设计得比较轻巧当设备网关上报“当前状态FOAM”时服务端会校验订单状态是否处于允许流转的前置状态如果校验通过就更新数据库的订单状态并记录一条操作日志。如果校验不通过说明存在乱序上报或者指令丢失就会自动触发一次指令重发操作。2.3 支付与订单核心链路支付模块千万不能想得太简单。微信支付、支付宝支付的回调是异步的而且为了保证送达会多次推送。我刚开始做的时候就在回调这里踩过坑没有做幂等处理用户支付成功后微信回调来了3次结果订单被处理了3次生成了3条券码钱也退了3次。那一次对账差点把人搞疯。正确的做法是在回调处理服务里增加一个“是否已处理”的判断。我习惯的做法是给订单表加一个唯一的业务流水号字段并且设置唯一约束然后回调处理逻辑改成两步走第一步捕获微信回调的报文提取我们自己的订单号。第二步以订单号为条件查询redis中是否已经存在“已处理”标识如果不存在则执行后续业务逻辑并在redis中写入处理标识。为了更保险数据库里的支付流水表也会对订单号做唯一索引双保险兜底。具体的回调处理逻辑大致是这样RestController public class WxPayCallbackController { Autowired private OrderService orderService; Autowired private StringRedisTemplate stringRedisTemplate; PostMapping(/api/pay/wechat/callback) public String wechatCallback(HttpServletRequest request, HttpServletResponse response) { String body HttpHelper.getBodyString(request); MapString, String params WxPayUtil.xmlToMap(body); // 1. 签名校验 if (!WxPayUtil.verifySign(params, wxPayConfig.getApiV3Key())) { return WxPayUtil.fail(签名错误); } // 2. 判断订单状态是否已处理 String outTradeNo params.get(out_trade_no); String processedKey pay:processed: outTradeNo; Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(processedKey, 1, 24, TimeUnit.HOURS); if (flag ! null flag) { // 3. 第一次收到回调正常处理 orderService.handlePaidOrder(outTradeNo, params); return WxPayUtil.success(); } if (orderService.isOrderProcessed(outTradeNo)) { // 4. 已经处理过直接返回成功 return WxPayUtil.success(); } return WxPayUtil.fail(重复通知处理失败); } }这套逻辑跑下来虽然代码多了几行但大大减少了资金风险。有一点要特别强调无论微信还是支付宝返回给支付平台的结果一定不能乱写。处理成功就返回SUCCESS没有处理成功就返回FAIL并附带原因让支付平台继续重试。如果你在业务没处理完时就返回成功那这笔订单的钱就永远悬空了对账时特别麻烦。2.4 管理端与多门店扩展管理端这一块主要解决的是“网点多、老板忙不过来”的问题。我做的后台具备几个核心能力设备地图监控、订单流水明细、会员管理、套餐配置、分账设置、故障告警。多门店扩展看起来复杂但只要数据库结构设计好了就简单很多。我建议把“适用门店”作为一张独立关联表而不是直接给设备表加一个storeId字段。这样同一台设备可以灵活调整到不同门店还方便做门店搬迁记录。分账逻辑上每个门店、每个代理都可以设计独立的分佣比例订单结算时自动计算平台收入、门店收入、代理奖励避免月底手工算账。比如一个门店的洗车套餐定价15元平台抽成3元门店收入12元如果有区域代理再按比例从平台收入里分一部分。这套分账逻辑跑一遍很顺畅关键是数据透明每一笔钱都能追溯能省去很多沟通成本。3. 核心源码实现与细节解析3.1 数据库设计订单、设备、用户数据库设计是整套系统的地基。表设计不合理后面写业务代码简直处处受制。我挑几张最核心的表拆一下。第一张是设备表device。字段包括主键、设备编号、设备类型龙门式/往复式/隧道式、品牌型号、所属门店ID、设备状态在线/离线/维护、当前状态关联状态机、最后心跳时间、创建时间。针对在线状态我额外加了一个“离线告警阈值”字段比如设定180秒没收到心跳就判定离线这样运维能及时介入。第二张是订单主表wash_order。这张表非常关键字段包括主键、业务流水号wx_order_no、用户ID、设备ID、套餐ID、支付金额、优惠金额、实付金额、订单状态、支付状态、创建时间、支付时间、开始洗车时间、结束时、异常原因。支付状态和订单状态一定要分开不要搞成一个状态字段否则查询和排障时很容易混乱。比如订单已经创建但用户没有支付此时订单状态是“待支付”支付状态是“未支付”。用户支付成功后订单状态变成“支付成功”支付状态变成“已支付”。第三张是用户表user。包含基础信息、手机号、微信OpenID、UnionID、会员等级、余额、累计消费金额。这里提醒一下虽然小程序端可以用手机号快速登录但服务端一定不要只依赖OpenID做关联因为同一用户在不同小程序或者公众号下的OpenID不同建议用UnionID统一标识用户身份。还要有一个“设备运行日志表”用来记录设备每次上报的状态变化、指令下发内容、操作结果。这张表平时不怎么查但一旦出现设备故障、用户投诉它的价值就体现出来了。通过日志可以完整还原当时设备收到了什么指令、执行了什么动作、用户操作了什么。排查问题的时候我就靠它来“时间回溯”。3.2 扫码启动洗车的核心流程代码扫码启动是用户接触最频繁的操作也是最容易出“体验事故”的地方。完整逻辑分这几步第一步用户通过小程序扫码拿到设备编号。第二步小程序请求后端/api/wash/start接口传入设备编号和所需套餐ID。第三步后端先校验设备是否空闲校验用户余额或会员资格是否满足然后预下单并返回支付参数。第四步用户在小程序端调起微信支付。第五步支付回调到达后端更新订单状态并调用设备控制服务下发射击指令。第六步设备开始工作小程序页面轮询设备状态展示“洗车中”、“风干中”等实时信息。这个里面最影响体验的是“支付成功后设备启动的延迟”。用户是付完钱马上盯着的如果超过10秒设备还没有反应大部分人会以为系统坏了甚至会投诉。所以支付回调的处理一定要快。我的实现里回调处理线程只做核心动作更新数据库订单状态、向设备网关发送启动指令然后把结果显示给用户。至于洗车流程里的日志记录、数据上报、分账计算全部丢到MQ异步队列里去执行不阻塞主链路。public WashStartResult startWash(WashStartRequest request) { // 1. 查询设备状态 DeviceDevice device deviceMapper.selectByDeviceNo(request.getDeviceNo()); if (device null) { return WashStartResult.fail(设备不存在); } if (device.getDeviceState() ! DeviceState.IDLE) { return WashStartResult.fail(设备繁忙请等待当前车辆洗车完成); } // 2. 校验用户身份和会员状态 User user userMapper.selectById(request.getUserId()); if (user.getBalance().compareTo(request.getAmount()) 0 !user.isVip()) { return WashStartResult.fail(余额不足请充值); } // 3. 创建订单 WashOrder order new WashOrder(); order.setUserNo(request.getUserId()); order.setDeviceNo(device.getDeviceNo()); order.setOrderAmount(request.getAmount()); order.setStatus(OrderStatus.CREATED); order.setPayStatus(PayStatus.UN_PAY); orderMapper.insert(order); // 4. 唤起预支付 PrepayResult prepayResult payService.createWxPrepay(order.getOrderNo(), order.getOrderAmount()); return WashStartResult.ok(prepayResult); }这里有一个很多人忽略的点就是“设备繁忙”的判断。无人洗车不像人工洗车一个师傅可以协调多辆车。设备只有一个入口同一时间只能有一辆车在洗。如果设备处于洗车中状态用户扫码就应该直接提示“设备繁忙”而不是让他白下单白支付。有些系统做得粗糙用户都支付成功了才提示设备忙最后还得走退款流程体验极差。3.3 设备状态机从空闲到洗车完成的运转逻辑状态机这块我刚才已经有点影了这里把它延伸完整。设备状态机的核心是想清楚“谁在推动状态变化”。驱动源有两类一类是服务端主动下发的指令比如“开始洗车”、“切换冲洗模式”另一类是设备端上报的状态回调比如“当前正在打泡沫”、“当前已完成吹风”。这两类信号交织在一起状态机很容易出现“竞争条件”。举例来说服务端刚下发“开始洗车”指令设备端网络延迟同时上报了“空闲”状态这时候如果状态机不加以校验系统就可能错误地把刚启动的设备置成空闲导致后续步骤全部错乱。所以我在状态机流转前加了一道“乐观锁”校验。具体实现是每次状态更新都带上当前状态的版本号更新时使用SQL的UPDATE ... WHERE state ?如果影响行数为0说明状态已经被其他线程修改本次操作视为无效。Update(UPDATE device SET device_state #{newState}, version version 1 WHERE id #{deviceId} AND device_state #{oldState}) int compareAndSetState(Param(deviceId) Long deviceId, Param(oldState) Integer oldState, Param(newState) Integer newState);这样就能保证同一时刻只有一个线程能成功修改设备状态。比如服务端想从“空闲”改成“洗车中”如果另一条线程已经把状态改成“维护中”那这次更新就会失败服务端会拿到0然后返回“设备状态异常请稍后再试”。状态机还有一个特别重要的分支就是异常情况。洗车过程中如果设备断电、传感器故障、水压过低PLC会通过边缘网关上报“故障”状态。服务端收到后要立刻把订单置为“异常待处理”同时通知管理人员。等故障恢复设备回到“空闲”状态如果订单只是执行到一半可以给用户保留一部分余额或者引导用户联系客服退款。3.4 定时任务超时订单与设备巡检定时任务在无人洗车系统里简直是“隐形劳模”。系统长了一定会遇到这几种情况第一种用户下单后不支付订单卡在“待支付”。虽然没有资金风险但会占着资源让后续下单的用户疑惑为什么自己的订单一直是“待处理”。我写了一个定时任务每小时扫描一次超过30分钟未支付的订单自动置为“已取消”。第二种用户支付成功但设备始终没收到启动指令。这类情况通常是因为设备网关掉线或者指令丢失。定时任务每分钟扫描一次“已支付但还没进入洗车中”的订单如果超过1分钟未响应就触发一次指令重发。如果连续3次重发都失败则自动为用户发起退款并通知运维人员。第三种设备心跳超时。设备会每隔30秒向服务端上报一次心跳心跳内容包括设备编号、当前状态、故障代码、累计洗车次数。定时任务每2分钟查一次所有设备的最新心跳时间如果超过2分钟没收到心跳就将设备状态置为“离线”并在管理后台和微信服务号上推送给运维人员。用XXL-Job来做这些定时任务好处是支持动态调整执行周期还自带日志面板任务执行失败会有告警。我跑了一段时间发现这种轮询式的定时任务虽然简单但是要想好执行频率别设得太密集不然数据库压力大也别设得太久否则异常订单积压会影响用户体验。我目前的标准是未支付订单检查每小时跑一次支付超时未启动检查每30秒跑一次设备心跳巡检每2分钟跑一次退款状态查询每5分钟跑一次这个频次跑下来数据库压力可控用户体验也基本能做到“异常及时反馈”。4. 部署、联调与硬件对接指南4.1 服务器环境与项目启动拿到这套源码之后部署其实不算复杂但有几处细节需要注意。建议服务器配置至少是4核8G起步硬盘用SSD系统盘和数据盘分开。因为是无人值守服务中途不能频繁重启稳妥起见我建议数据库和Redis单独部署服务端主程序用Docker容器化部署。部署流程大概是这样安装JDK 17配置JAVA_HOME环境变量。安装MySQL 8.0创建数据库名car_wash导入项目里提供的init.sql脚本。安装Redis 6.0默认端口设置密码。修改application.yml把数据库连接池、Redis连接、微信支付相关参数替换成你自己的。打包mvn clean package -DskipTests生成jar包。用nohup java -jar car-wash-server.jar --spring.profiles.activeprod 方式启动。配置Nginx反向代理把/api/路径转发到Java服务端口前端静态文件放到/usr/share/nginx/html下。启动过程中最容易出的问题是数据库连接不上。很多人刚买完服务器密码中含有特殊字符比如、#没有在application.yml里做URL编码结果一直报连接超时。这个建议仔细检查一下。4.2 硬件接线与PLC控制对接硬件这块很多做纯软件的人一听到就头大。其实不用怕大部分逻辑在边缘网关里已经封装好了。标准的对接流程是先在PLC里设置好继电器地址比如“水枪继电器对应线圈Q0.0”“泡沫继电器对应线圈Q0.1”“风干机对应线圈Q0.2”。然后在边缘网关上配置Modbus地址映射把Q0.0映射为relay_gunQ0.1映射为relay_foam。这样云端服务下发的JSON指令经过网关翻译成Modbus协议报文最后控制PLC的线圈通断实现物理设备的启停。云端和边缘网关的通信我建议直接用MQTT协议。设置一个主题比如device/{deviceNo}/command服务端发布指令网关订阅指令。网关执行成功后再发布一个device/{deviceNo}/ack的应答消息。这套机制天然支持断线重连和消息去重比HTTP轮询要省心很多。有一个很大的坑要提醒你不要把云服务器直接暴露到公网上的设备控制端口。边缘网关通过主动长连接的方式连接云端而云端不要去监听设备侧的任何入站端口。这样即使洗车场地遭遇断网、IP变动、被恶意扫描攻击者也很难直接操作设备。很多设备被黑都是因为厂家为了图方便把PLC的端口直接映射到了公网上。4.3 联调全流程联调是整个项目中承上启下的关键阶段顺序不对后面调试会非常痛苦。我建议按以下顺序推进第一步先做接口联调。不开设备用Postman模拟设备端上报各种状态看服务端的状态机流转是否正确。比如模拟下发“开始洗车”指令然后依次上报“冲洗”、“泡沫”、“刷洗”等步骤检查数据库订单状态是否跟着同步。第二步做小程序联调。把小程序跑起来用一个测试号支付1分钱测试订单检查支付回调、订单生成、余额扣减是否正常。这个阶段最容易发现的问题是支付回调签名错误、回调地址配置错误。第三步接真实设备。把边缘网关和PLC连上先手动控制继电器通断确认线缆和地址没问题再让云端下发指令控制设备。这一步最好找个空旷场地测试避免设备刚启动就撞到障碍物。第四步做压力测试。模拟100个用户同时扫码下单、同时查询订单状态看看数据库连接池够不够、Redis缓存是否命中率正常。我的经验是数据库连接池初始连接数设10、最大连接数设20单机支撑100台设备的并发查询基本没问题。4.4 安全加固与异常兜底无人洗车系统直接面向真实资金安全这块怎么强调都不过分。我从几个层面对源码做了加固第一层接口鉴权。小程序端所有接口都要走微信登录态认证服务端用JWT做Token校验过期时间设为2小时。同时做接口幂等性处理防止用户连续点击导致重复下单。第二层数据权限。管理后台按角色区分平台管理员能看到所有设备、所有订单店长只能看到自己门店的数据维修工只能看到设备运行状态不能看财务数据。角色权限用Spring Security 自定义注解实现非常灵活。第三层异常兜底。所有向设备下发的指令都要有超时重试机制。比如下发“开机”指令后如果设备网关在3秒内没有返回ACK服务端先重试一次再失败就发告警通知运维。另外退款操作做成“异步提交支付平台”并且定时扫描退款结果防止退款状态卡死。5. 实战踩坑记录与排查方案5.1 支付回调丢了怎么办刚做这套系统的时候有用户反馈“我付了钱但洗车机不动”。查来查去发现微信支付回调因为服务器防火墙配置错误回调请求没到Java应用。有一种比较尴尬的情况是微信服务器支付结果通知如果连续多次没有收到成功响应就会停止通知这时候服务端单方面等回调是永远等不到的。解决思路就是“主动查单”。我在定时任务里增加了一个“对账补单”机制扫描订单表中所有“已支付但未进入设备启动”状态的订单从支付平台主动查询订单状态。如果支付平台返回“已支付”就把本地订单置为“已支付”开始向设备下发指令。这样即使回调丢失也不会出现用户付了钱却没反应的情况。5.2 设备卡死与传感器误触无人设备在室外环境下传感器很容易被水雾、沙尘、光线影响导致误触发。比如光电传感器检测到“有车进入”但如果水雾太大或者反光异常会在无车状态下误报导致设备在空闲状态突然启动。这个坑我在现场调试时踩过最后靠调整传感器灵敏度 增加“双重检测”逻辑解决光靠一个传感器判断不够要同时检测地磁传感器和光电开关的信号两者同时触发才判定有车进入。另外设备执行中突然断电恢复供电后设备状态是未知的。我在PLC侧写了一个“开机自检”逻辑上电后先把所有继电器复位到关闭状态然后向云端上报“开机自检完成”由云端决定是否继续执行未完成的订单。这样有效避免了设备重启后突然吸入或转动的安全隐患。5.3 并发下的订单重复问题无人洗车高峰时段的并发其实不低尤其是小区门口的洗车机周末早高峰可能20分钟内有8到10辆车排队。这时候如果有两个用户同时扫码同一台设备会发生什么如果没有并发控制两个人可能都下单支付成功但设备只能服务一个人。处理方案我在前面提到过就是在“创建订单”之前先给设备加分布式锁。我用Redis的SETNX命令实现了简单的分布式锁private boolean tryLockDevice(String deviceNo) { Boolean success stringRedisTemplate.opsForValue() .setIfAbsent(lock:device: deviceNo, 1, 10, TimeUnit.SECONDS); return success ! null success; }如果拿锁失败直接返回“设备正在工作中”。这里锁的过期时间一定要设得短一点防止服务异常时锁一直占着。我设的是10秒洗车开始后设备状态已变为“洗车中”锁就可以删了。如果设备状态长时间不变再抢救一次锁整体逻辑还算严谨。5.4 数据库连接与内存问题系统稳定运行一个月后我发现数据库连接池偶尔会出现“连接获取超时”的报错。排查发现是因为定时任务里的SQL查询没有正确的参数替换脏数据导致慢查询连接长时间被占满。后来给每条SQL都做了Explain分析对慢查询加了联合索引同时调整了MyBatis的批量操作整体才稳定下来。内存方面JVM启动参数里最好加上-Xms2048m -Xmx2048m防止大促时频繁Full GC。垃圾回收器就用G1默认参数即可Java 17的G1调优压力很小。前期如果不调优高峰期可能出现频繁GC导致接口超时。这块不要省事。6. 运营视角从源码到真正盈利6.1 计费策略与价格设计源码搭起来了最终要落地赚钱。无人洗车的计费模式主流就三种单次扫码计费、会员套餐包月/包年、充值余额送优惠。我建议上线初期用“单次低价 充值赠送”的组合比如单次洗车15元充值100送20。这么设计的原因是用户的决策成本低愿意尝试一旦充值就锁定了后续消费粘度。套餐包月是给高频用户的比如每天通勤的上班族一个月洗8次车办月卡简直不要更划算。价格设计上一定要算清楚成本。设备水电耗材、场地租金、设备折旧、维护人工都要摊进客单价里。我算过一笔账一套设备加场地成本约8万按日均20辆车、客单价15元计算月流水大约9000元扣除水电耗材成本约3000元月净利6000元一年能回本。如果叠加会员套餐和增值服务回本周期可以进一步缩短到8个月。这组数字虽然没有考虑设备维修和意外支出但整体方向可以做参考。6.2 用户端体验的细节打磨用户端体验是最容易拉开差距的地方。在系统试运营阶段我每天自己扫一遍码洗一次车专门找卡顿和不舒服的地方。有几处体验细节我认为做了之后用户复购率明显提升第一洗车进度可视。用户支付后小程序页面能实时看到“正在冲洗、正在打泡沫、正在风干”等进度。这样用户不会干等看进度条心里踏实。第二洗车完成后推送服务通知。用微信订阅消息洗车完成后推一条“本次洗车已完成耗时8分32秒”顺便带一张下次洗车的优惠券。即使用户已经离开了也被再次拉回。第三余额提醒和过期提醒。余额不足或者套餐快到期时自动发模板消息提醒用户充值或续费。这块虽然简单但能大幅提升充值率。6.3 多站点复制的架构友好性做无人洗车单店试水容易连锁扩张难。很多系统在单店跑得动扩到5家店就崩了。原因在于单机版做得再完善如果数据库、缓存、设备管理逻辑没有按多门店设计复制扩容就会很痛苦。这套JAVA源码从一开始就做了多门店扩展门店和设备是一对多关系订单表里冗余了门店ID和门店名称分账逻辑按门店维度汇总。运营层面配置区域代理也简单每个代理管理一组门店代理商可以在后台看到自己名下门店的实时流水。实际跑下来从1家店扩到10家店只需要增加服务器内存、做好数据库备份和监控告警不需要改一行业务代码。这就是架构合理带来的红利。我个人在实际操作中最深的体会是无人洗车系统拼的不是多炫酷的界面、多花哨的功能而是稳定、可靠、可维护。把支付、设备控制、状态机、异常处理这几个核心链路吃透其他都是锦上添花。最后再分享一个小建议无论你是打算自己开店还是做服务商上线前一定要找几台不同品牌、不同型号的设备做兼容测试把设备协议适配层做厚后续接设备时会省很多事。这套源码的设计已经充分考虑了这一层你只需要根据自己的硬件情况补充协议适配就能平滑跑通。
返回列表