02-从需求文档到方案-无人售货机纯视觉与免密支付作者黒漂技术佬系列URM Ultra 方案理念与架构篇上篇把 URM Ultra 的全景摊开了。这一篇咱们换个视角——从一份原始需求文档出发看一个卖货的柜子是怎么一步步变成软件方案的。很多新手写代码的通病是需求扔过来上手就建表、写接口结果写到一半发现支付怎么做识别结果怎么来全没想清楚。URM Ultra 的售货机部分恰恰是一个需求 → 方案 → Demo 简化的标准教科书案例。今天我就把它拆给你看。一、原始需求长什么样项目里的需求文档关于无人售货机这一段其实非常短我给你提炼成白话原始需求大白话纯视觉、商品识别用三方算法不装重力传感器、不贴 RFID靠摄像头看用户拿了啥设备端是安卓工控双门、2 路锁、4 摄、4G一台柜子俩门、两把电控锁、四个摄像头、走 4G 网支付打算用微信支付分免密用户先授权拿完货后台自动扣款不用现场扫码后台是 Spring Boot含商品/订单/设备商品等还要承接无人车、补货配送一个 Java 后台统管所有业务消费端是小程序用户用微信小程序下单本项目只做简单实现作为 Demo别堆生产级基建把闭环跑通就行你看需求里没有一句话提数据库表怎么设计、接口怎么命名。它描述的是业务事实和硬件事实。从事实到代码中间隔着一层翻译。二、翻译第一层纯视觉识别意味着什么纯视觉三个字对软件架构的影响极其深远。我们先对比一下三种识别方案方案原理软件侧成本缺点重力感应每层放称重托盘重量变化算拿了啥低读传感器即可只能感知重量变了同重商品区分不了RFID商品贴电子标签柜内读卡中要绑标签每个商品贴标签成本高、易损纯视觉摄像头拍照算法识别商品高依赖识别服务需要算法供应商、需要画面质量URM Ultra 选纯视觉意味着后台必须有一个识别服务的概念而且这个服务大概率是外部三方提供的需求明确写了三方识别。这直接催生了一个关键设计识别能力必须做成接口不能写死。项目源码里是这样落地的节选自vision包// 项目源码视觉识别抽象接口publicinterfaceVisionService{// deviceNo哪台柜子frameRef这一帧画面的引用如图片地址/帧IDListRecognitionItemrecognize(StringdeviceNo,StringframeRef);}RecognitionItem返回的字段就是视觉识别结果的标准结构// 项目源码识别结果项publicclassRecognitionItem{privateStringslotNo;// 货道号哪个格子privateLongproductId;// 商品IDprivateStringproductName;// 商品名privateStringbarcode;// 条码privatedoubleconfidence;// 置信度 0~1算法有多确定privateintquantity;// 拿了几个privateBigDecimalprice;// 货道售价}为什么要有confidence因为算法不是神仙它会不太确定。0.9~1.0 是高置信低于某个阈值可能要人工复核。Demo 里 Mock 实现固定返回 0.9~1.0真实三方算法接进来后这个值才是真家伙。划重点VisionService是接口MockVisionService是它的假实现。Demo 阶段 Mock 随机挑一个启用货道返回拿了 1 件目的只有一个——把识别→下单→扣款→扣库存这条链路先跑通。等真算法接进来只要新写一个实现类业务代码一行不动。这就是解耦的威力。三、翻译第二层安卓工控 双门两路锁四摄 4G需求里这句硬件描述新手最容易一笔带过。但它是设备建模的依据直接影响后台那张Device表长啥样。硬件事实软件里怎么体现安卓工控设备端跑安卓负责采摄像头、控锁、心跳双门doorCount 22 路锁lockCount 2每门一路电控锁4 摄cameraCount 4柜内 4 个摄像头4Gnetwork 4G网络类型字段这些字段不是我拍脑袋的它们直接来自需求里的硬件清单。后台把这些物理属性原样存进Device实体运营在后台就能看到这台柜子几扇门、几把锁、几个摄像头、走什么网。第 03 篇我会专讲这张表这里先记住硬件事实 → 软件字段是 URM Ultra 一以贯之的建模哲学。还有个隐藏点4G 网络意味着设备可能离线。所以Device必须有status在线/离线/故障和lastHeartbeat最后一次心跳时间——车、臂同理。这也是后面状态机的由来。四、翻译第三层微信支付分免密免密支付是这套方案体验的灵魂。传统柜子开门 → 拿货 → 关门 → 掏手机扫码付钱 → 走人。URM Ultra开门 → 拿货 → 关门 →走人后台已自动扣款。但这里有个合规前提不能真偷偷扣。它用的是微信支付分免密模式流程是1) 用户首次使用微信里授权支付分免密先签约 2) 用户拿货关门 3) 后台生成订单调用支付分先预授权冻结额度 4) 视觉识别出金额执行扣款payScoreOrderNo 落地 5) 出问题可退款refund源码里同样做成接口避免把微信写死// 项目源码微信支付分免密抽象publicinterfaceWechatPayScoreService{booleanpreAuthorize(StringopenId);// 预授权PayScoreResultpay(StringorderNo,StringopenId,BigDecimalamount,StringgoodsDesc);// 扣款PayScoreResultrefund(StringorderNo,BigDecimalamount,Stringreason);// 退款PayScoreResultquery(StringorderNo);// 查询}Mock 实现的假扣款很诚实——它不联网用内存 Map 记状态pay永远返回成功// 项目源码Mock 支付演示用非真实微信ServiceConditionalOnProperty(nameurm.wechat-pay.enabled,havingValuefalse,matchIfMissingtrue)publicclassMockWechatPayScoreServiceimplementsWechatPayScoreService{privatefinalConcurrentMapString,StringstatesnewConcurrentHashMap();publicPayScoreResultpay(StringorderNo,StringopenId,BigDecimalamount,StringgoodsDesc){states.put(orderNo,SUCCESS);// 假成功// 返回伪 payScoreOrderNo / transactionIdreturnPayScoreResult.success(...);}}注意ConditionalOnProperty这个注解——urm.wechat-pay.enabledfalse时加载 Mock设成true时加载真实微信实现。配置一改实现自动切换业务 Service 无感。这是 Spring Boot 条件化装配的标准玩法值得学。五、把三块拼成下单链路需求最终要落到一个消费者能用的动作在小程序点一下购买货道识别、扣款、扣库存一气呵成。后台OrderService.createAndPay就是这条链路的真实实现项目源码简化注释1) 校验设备是否存在deviceNo 查 Device 2) 视觉识别visionService.recognize(deviceNo) → 拿到商品列表 3) 生成订单 OrderstatusPENDING_PAYMENTpayTypeWECHAT_PAY_SCORE 4) 逐条组装 OrderItem单价×数量累加总金额 5) 免密扣款wechatPayScoreService.pay(...) 失败 → 订单 CANCELED抛支付失败 6) 扣库存deviceService.deductStock(deviceId, slotNo, qty) 7) 订单置 PAID写支付记录 PaymentRecord(SUCCESS)一张图看全貌小程序点购买 │ ▼ 后台 createAndPay ├─▶ 视觉识别VisionService ── 识别拿了啥 ├─▶ 生成订单Order ── 待支付 ├─▶ 免密扣款PayScore ── 自动扣钱 └─▶ 扣减库存deductStock ── 货道 -N │ ▼ 订单完成用户已走这条链路逻辑是 100% 真实的事务、扣库存、写支付记录都真刀真枪只有视觉识别和支付扣款这两步是 Mock。这正是 Demo 的精髓把不确定交给 Mock把确定性留给真实代码。六、Demo 做了哪些简化以及为什么可以简化需求说只做简单实现。我列一下简化点并说明为什么这些简化不影响你学架构简化项真实世界Demo 做法影响视觉识别真实摄像头 YOLO 类算法Mock 随机返回链路照跑接算法只换实现微信支付真实商户号 证书 回调Mock 内存成功扣款逻辑照跑接微信只换实现安卓工控真实安卓 App 采摄像头控锁设备端 heartbeat/recognize 接口模拟后台契约已定义App 照着调鉴权OAuth2 / 租户隔离不接纯演示生产必补设备事件持久化 驱动门锁/event仅回显received桩代码标注了真实要做啥看到没所有简化都集中在外部依赖和生产基建上而业务状态机、任务编排、库存扣减这些核心一个没省。你学这套代码学到的是真东西。七、给新手的两条心得需求里的每一个硬件参数都是一张表的一个字段。双门→doorCount4 摄→cameraCount别嫌烦照实记。凡是外部不确定的能力先抽象成接口 Mock。识别、支付、甚至未来的路径规划都该如此。这样你的主干代码永远可跑、可测、可读。下一篇我们钻进那张Device表看看双门两路锁四摄是怎么变成一行数据库记录的。