从需求文档到方案-无人售货机纯视觉与免密支付

02-从需求文档到方案-无人售货机纯视觉与免密支付

作者:黒漂技术佬|系列:URM Ultra 方案理念与架构篇

上篇把 URM Ultra 的全景摊开了。这一篇咱们换个视角------从一份原始需求文档出发,看一个"卖货的柜子"是怎么一步步变成软件方案的。

很多新手写代码的通病是:需求扔过来,上手就建表、写接口,结果写到一半发现"支付怎么做""识别结果怎么来"全没想清楚。URM Ultra 的售货机部分,恰恰是一个"需求 → 方案 → Demo 简化"的标准教科书案例。今天我就把它拆给你看。


一、原始需求长什么样

项目里的需求文档,关于"无人售货机"这一段其实非常短,我给你提炼成白话:

原始需求 大白话
纯视觉、商品识别用三方算法 不装重力传感器、不贴 RFID,靠摄像头"看"用户拿了啥
设备端是安卓工控,双门、2 路锁、4 摄、4G 一台柜子俩门、两把电控锁、四个摄像头、走 4G 网
支付打算用微信支付分免密 用户先授权,拿完货后台自动扣款,不用现场扫码
后台是 Spring Boot,含商品/订单/设备商品等,还要承接无人车、补货配送 一个 Java 后台统管所有业务
消费端是小程序 用户用微信小程序下单
本项目只做简单实现,作为 Demo 别堆生产级基建,把闭环跑通就行

你看,需求里没有一句话提数据库表怎么设计、接口怎么命名。它描述的是"业务事实"和"硬件事实"。从事实到代码,中间隔着一层"翻译"。


二、翻译第一层:纯视觉识别意味着什么

"纯视觉"三个字,对软件架构的影响极其深远。我们先对比一下三种识别方案:

方案 原理 软件侧成本 缺点
重力感应 每层放称重托盘,重量变化算拿了啥 低,读传感器即可 只能感知"重量变了",同重商品区分不了
RFID 商品贴电子标签,柜内读卡 中,要绑标签 每个商品贴标签,成本高、易损
纯视觉 摄像头拍照,算法识别商品 高,依赖识别服务 需要算法供应商、需要画面质量

URM Ultra 选纯视觉,意味着后台必须有一个"识别服务"的概念,而且这个服务大概率是外部三方提供的(需求明确写了"三方识别")。这直接催生了一个关键设计:

识别能力必须做成接口,不能写死。

项目源码里是这样落地的(节选自 vision 包):

java 复制代码
// 项目源码:视觉识别抽象接口
public interface VisionService {
    // deviceNo:哪台柜子;frameRef:这一帧画面的引用(如图片地址/帧ID)
    List<RecognitionItem> recognize(String deviceNo, String frameRef);
}

RecognitionItem 返回的字段,就是"视觉识别结果"的标准结构:

java 复制代码
// 项目源码:识别结果项
public class RecognitionItem {
    private String slotNo;       // 货道号(哪个格子)
    private Long productId;      // 商品ID
    private String productName;  // 商品名
    private String barcode;      // 条码
    private double confidence;   // 置信度 0~1,算法有多确定
    private int quantity;        // 拿了几个
    private BigDecimal price;    // 货道售价
}

为什么要有 confidence?因为算法不是神仙,它会"不太确定"。0.91.0 是高置信,低于某个阈值可能要人工复核。Demo 里 Mock 实现固定返回 0.91.0,真实三方算法接进来后,这个值才是真家伙。

划重点:VisionService 是接口,MockVisionService 是它的假实现。Demo 阶段 Mock 随机挑一个启用货道返回"拿了 1 件",目的只有一个------把"识别→下单→扣款→扣库存"这条链路先跑通。等真算法接进来,只要新写一个实现类,业务代码一行不动。这就是"解耦"的威力。


三、翻译第二层:安卓工控 + 双门两路锁四摄 4G

需求里这句硬件描述,新手最容易一笔带过。但它是"设备建模"的依据,直接影响后台那张 Device 表长啥样。

硬件事实 软件里怎么体现
安卓工控 设备端跑安卓,负责采摄像头、控锁、心跳
双门 doorCount = 2
2 路锁 lockCount = 2(每门一路电控锁)
4 摄 cameraCount = 4(柜内 4 个摄像头)
4G network = "4G"(网络类型字段)

这些字段不是我拍脑袋的,它们直接来自需求里的硬件清单 。后台把这些"物理属性"原样存进 Device 实体,运营在后台就能看到"这台柜子几扇门、几把锁、几个摄像头、走什么网"。第 03 篇我会专讲这张表,这里先记住:硬件事实 → 软件字段是 URM Ultra 一以贯之的建模哲学。

还有个隐藏点:4G 网络意味着设备可能离线 。所以 Device 必须有 status(在线/离线/故障)和 lastHeartbeat(最后一次心跳时间)------车、臂同理。这也是后面状态机的由来。


四、翻译第三层:微信支付分免密

"免密支付"是这套方案体验的灵魂。传统柜子:开门 → 拿货 → 关门 → 掏手机扫码付钱 → 走人。URM Ultra:开门 → 拿货 → 关门 → 走人(后台已自动扣款)。

但这里有个合规前提:不能真"偷偷扣"。它用的是微信支付分免密模式,流程是:

arduino 复制代码
1) 用户首次使用:微信里授权"支付分免密"(先签约)
2) 用户拿货关门
3) 后台生成订单,调用支付分:先"预授权"冻结额度
4) 视觉识别出金额,执行扣款(payScoreOrderNo 落地)
5) 出问题可退款(refund)

源码里同样做成接口,避免把"微信"写死:

java 复制代码
// 项目源码:微信支付分免密抽象
public interface WechatPayScoreService {
    boolean preAuthorize(String openId);                    // 预授权
    PayScoreResult pay(String orderNo, String openId,
                       BigDecimal amount, String goodsDesc); // 扣款
    PayScoreResult refund(String orderNo, BigDecimal amount, String reason); // 退款
    PayScoreResult query(String orderNo);                   // 查询
}

Mock 实现的"假扣款"很诚实------它不联网,用内存 Map 记状态,pay 永远返回成功:

java 复制代码
// 项目源码:Mock 支付(演示用,非真实微信)
@Service
@ConditionalOnProperty(name="urm.wechat-pay.enabled",
                       havingValue="false", matchIfMissing=true)
public class MockWechatPayScoreService implements WechatPayScoreService {
    private final ConcurrentMap<String,String> states = new ConcurrentHashMap<>();
    public PayScoreResult pay(String orderNo, String openId,
                              BigDecimal amount, String goodsDesc) {
        states.put(orderNo, "SUCCESS");   // 假成功
        // 返回伪 payScoreOrderNo / transactionId
        return PayScoreResult.success(...);
    }
}

注意 @ConditionalOnProperty 这个注解------urm.wechat-pay.enabled=false 时加载 Mock,设成 true 时加载真实微信实现。配置一改,实现自动切换,业务 Service 无感。这是 Spring Boot 条件化装配的标准玩法,值得学。


五、把三块拼成"下单链路"

需求最终要落到一个消费者能用的动作:在小程序点一下"购买",货道识别、扣款、扣库存一气呵成 。后台 OrderService.createAndPay 就是这条链路的真实实现(项目源码,简化注释):

scss 复制代码
1) 校验设备是否存在(deviceNo 查 Device)
2) 视觉识别:visionService.recognize(deviceNo) → 拿到商品列表
3) 生成订单 Order,status=PENDING_PAYMENT,payType=WECHAT_PAY_SCORE
4) 逐条组装 OrderItem(单价×数量),累加总金额
5) 免密扣款:wechatPayScoreService.pay(...)
      失败 → 订单 CANCELED,抛"支付失败"
6) 扣库存:deviceService.deductStock(deviceId, slotNo, qty)
7) 订单置 PAID,写支付记录 PaymentRecord(SUCCESS)

一张图看全貌:

sql 复制代码
小程序点"购买"
      │
      ▼
后台 createAndPay
      ├─▶ 视觉识别(VisionService)   ── 识别拿了啥
      ├─▶ 生成订单(Order)           ── 待支付
      ├─▶ 免密扣款(PayScore)        ── 自动扣钱
      └─▶ 扣减库存(deductStock)     ── 货道 -N
      │
      ▼
  订单完成,用户已走

这条链路逻辑是 100% 真实的 (事务、扣库存、写支付记录都真刀真枪),只有"视觉识别"和"支付扣款"这两步是 Mock。这正是 Demo 的精髓:把不确定交给 Mock,把确定性留给真实代码。


六、Demo 做了哪些"简化"(以及为什么可以简化)

需求说"只做简单实现"。我列一下简化点,并说明为什么这些简化不影响你学架构:

简化项 真实世界 Demo 做法 影响
视觉识别 真实摄像头 + YOLO 类算法 Mock 随机返回 链路照跑,接算法只换实现
微信支付 真实商户号 + 证书 + 回调 Mock 内存成功 扣款逻辑照跑,接微信只换实现
安卓工控 真实安卓 App 采摄像头控锁 设备端 heartbeat/recognize 接口模拟 后台契约已定义,App 照着调
鉴权 OAuth2 / 租户隔离 不接 纯演示,生产必补
设备事件 持久化 + 驱动门锁 /event 仅回显"received" 桩代码,标注了真实要做啥

看到没?所有简化都集中在"外部依赖"和"生产基建"上,而业务状态机、任务编排、库存扣减这些核心,一个没省。你学这套代码,学到的是真东西。


七、给新手的两条心得

  1. 需求里的每一个硬件参数,都是一张表的一个字段。 双门→doorCount,4 摄→cameraCount,别嫌烦,照实记。
  2. 凡是"外部不确定"的能力,先抽象成接口 + Mock。 识别、支付、甚至未来的路径规划,都该如此。这样你的主干代码永远可跑、可测、可读。

下一篇,我们钻进那张 Device 表,看看"双门两路锁四摄"是怎么变成一行数据库记录的。

相关推荐
阿拉斯攀登1 小时前
双门两路锁四摄-售货机硬件形态的软件建模
架构
abigalexy2 小时前
图解AI应用架构设计
人工智能·ai·架构·系统架构·aigc
龙亘川2 小时前
智慧交通运输监管平台业务建模与架构解析
人工智能·架构·智慧城市·数据可视化·政务
AOI小白新手上路2 小时前
韦东山 3-5 I.MX6ULL 的 LED 编程 · 学习笔记
arm开发·笔记·单片机·学习·架构·硬件架构
Marst Code2 小时前
IBM 开放了 PC 架构,然后把自己“开放“出局了
架构
范桂飓3 小时前
AWS Agent Infra 架构分析
java·架构·aws
Dawson Zhu4 小时前
几何深度学习:原理解析与工程实践
人工智能·语言模型·架构·aigc·agi
AI你一生一世4 小时前
当广告采集器成为大模型的“眼睛“:跨站上下文注入的架构与代价
人工智能·架构·大模型·rag·隐私安全·上下文注入·跨站追踪
狗凯之家源码网4 小时前
网址导航系统源码评测:个性 UI 与轻量化架构实战
ui·架构·网址导航系统