USB 扫码枪的默认工作模式是 HID 键盘(keyboard-wedge):解码成功后,它把结果文本逐字符「打字」进操作系统当前聚焦的输入控件,并按出厂配置在末尾追加一个回车(Enter/CR)。对应用而言,扫一次码 ≈ 用户瞬间在输入框里粘了一串文本又按了回车。
我们标签上二维码编码的正是一条 URL(后端 product.go:290 用 go-qrcode 生成):
因此扫一次,捕获框里会瞬间出现整条 URL + 回车。前端只需:一个常聚焦的输入框 + 监听回车 + 解析 URL 取 public_id。
区分扫码 vs 人手打字:扫码是爆发式输入(字符间隔通常 <30ms,几十毫秒打完整条 URL),人手打字间隔 >100ms。可用「回车结束 + 输入速度」双判据;但出库这种专用扫码模式下,只靠「专用捕获框 + 回车提交」就够,不必依赖计时。
⚠ 必须是 2D(二维码)扫码枪。便宜的一维条码枪读不了 QR。若采购的是 1D 枪 → 见附录 B(改印一维条码)。
扫码枪插 PC(Windows / macOS)USB 口用,所以扫码出库主要落在桌面/宽屏端的出库建单页。手机端不接扫码枪——手机若要扫码得走摄像头(mobile_scanner)那是另一条技术路线,本方案不含。
前端只在 stock_out_form_screen.dart 的宽屏布局里加「扫码模式」开关,窄屏不出现。
关键 gap:出库明细行以 product_id(数字主键)为锚(StockOutItem.ProductID),但扫到的是 public_id / code。需要一步映射,把扫到的码解析成当前店的 product + 该仓库可用库存。分两段:
public_id = 路径中 product/ 之后、? 之前的段。容错两种前缀(/app/product/ 与 SSR 的 /product/),也容错「直接扫纯 public_id / 纯 code」的情况。code = query 参数 code(作兜底匹配用)。命中不到 / 不属本店 / 该仓无库存 / 已售罄 → 返回明确错误码,前端据此红字提示。返回结构复用出库表单已有的 _PickerItem,前端拿到即可直接装配明细行,无需二次转换。
为何要新接口、不复用现有的:
| 现有能力 | 为什么不能直接用 |
|---|---|
鉴权 GET /inventory?code= | code 是 LIKE 模糊匹配(inventory.go:113),不精确、也不吃 public_id |
公开 GET /public/products/:public_id | 按 public_id 查,但无鉴权、无 shop 隔离、无库存维度,不能驱动出库 |
鉴权 GET /inventory?product_id= | 精确,但入参已经是 product_id——正是我们缺的那一步转换 |
整个过程手不离枪:焦点始终锁在捕获框,一次扫码 = 一次「解析 → 查 → 加行 → 提示」闭环,无需鼠标。
数据铁律里 product = 一个特有产品 / 序列号,很多时候一个 product 对应一件 / 一批具体货。这决定「扫一次加几件」:
| 口径 | 行为 | 适用 |
|---|---|---|
| A · 每扫 +1建议默认 | 每扫一次数量 +1,可内联改;超可用量即时红字告警 | 一个 product = 多件同款,标签贴整批,按扫的次数点件数 |
| B · 一码一件 | 扫一次即锁定该 product(固定 1,或整行可用量),重复扫视为重复→忽略/提示 | 严格一物一码一序列号,一个码就是一件 |
建议先按 A(每扫 +1,数量可改) 落地,最贴合现在「批量勾选后改数量」的习惯。请你确认口径再进实现。
GET /api/v1/inventory/scan:handler + service 查询,shop 隔离、排除 sold、返回 _PickerItem 同构体。(inventory.go / router)/app/product/ → 与 SSR 路由 /product/ 统一(product.go:290),消除解析歧义。不改也行,前端做前缀容错即可。client/lib/screens/stock_out/stock_out_form_screen.dartRawKeyboardListener 或自动重夺焦点的隐藏 TextField)。_PickerItem 装配 → 加行 / 累加数量。mobile_scanner / camera——扫码枪走键盘,零摄像头依赖,pubspec.yaml 不动。| 坑 | 处理 |
|---|---|
| 1D 枪读不了 QR | 需 2D 枪;或加印 Code128(code)(附录 B) |
| 中文输入法拦截扫码字符 | 捕获用 raw key 监听绕过 IME,或强制英文态 |
| 扫码枪没配回车后缀 | 多数出厂默认带;没有则扫设置码开「Add Enter/CR Suffix」 |
| 焦点丢失(用户点了别处) | 失焦自动重夺 + 明显的「扫码模式」态提示 |
| 枪抖动同码连发 | 去重窗口:300ms 内同 public_id 忽略 |
| 跨店 / 售罄 / 未上架 | 后端硬拦(shop_id 隔离 + 排除 sold),前端红字 |
| 权限 | 出库受 ReadOnly 中间件保护;scan 接口只读,只读用户可查不可提交,天然安全 |
| 连扫速度 | 每扫打一次后端;若要极致速度可预载该仓全量 inventory 做本地 public_id→product_id 映射(附录 C,可选) |
| 扫码枪(本方案) | 手机摄像头 | |
|---|---|---|
| 技术路线 | HID 键盘输入,无图像 | mobile_scanner + 摄像头 + 图像解码 |
| 端 | 桌面 PC(Win/Mac) | 手机 App |
| 前端依赖 | 零新增 | 新增扫码库 + 摄像头权限 |
| 连扫效率 | 高(扣扳机即出) | 中(对焦、找码) |
| 硬件成本 | 一把 2D 枪 | 已有手机 |
| 本方案 | 采用 | 不含(未来可作手机端补充) |
若采购的是便宜的一维条码枪(读不了 QR),改在标签上加印一段 Code128 一维条码,内容 = 商品 code(同店唯一)。扫到的就是纯 code,走 GET /inventory/scan?code={code} 精确匹配即可(同样带 shop 隔离)。QR 与一维条码可并存于同一标签(QR 供顾客溯源,条码供出库)。改动落在 label_data.dart / 打印工具。
选好仓库后一次性把该仓 inventory 全量拉到前端,建 public_id → {product_id, 售价, 可用量} 内存映射。扫码时本地命中、零网络往返,连扫如飞;提交前再由后端 CheckInventoryAvailability 兜底校验。适合货多、连扫密集的场景,第一版可不做。
本改动 = 新接口 + 跨后端/前端 2 模块,属项目规则里的「大改」:按 CLAUDE.md,实现前须先进 plan 模式经你批准;且前端改动须遵守 design-first(先改设计系统原型再改真实页面)。本文件仅方案评审,你确认「数量口径(A/B)」+「是否修正二维码路径前缀」后,我再出实现 plan。