扫码出库 · 设计方案

用扫码枪扫商品标签已印的二维码,自动加进出库单 · 2026-08-24 · 方案评审(未实现)
把出库建单从「手动翻库存勾选」变成「对着货连扫」:操作员拿扫码枪扫商品标签上已经在印的二维码, 系统自动把对应商品加进当前出库单、数量累加。核心认知——扫码枪不是摄像头,是一把「键盘」:它把扫到的 URL 当作键盘输入敲进聚焦的输入框、末尾补一个回车。所以本方案零摄像头、零图像识别依赖,重点全在 「聚焦捕获 → 回车触发 → 按扫到的码查本店库存 → 装配明细行」这条链路上。

1. 原理:扫码枪 = HID 键盘

USB 扫码枪的默认工作模式是 HID 键盘(keyboard-wedge):解码成功后,它把结果文本逐字符「打字」进操作系统当前聚焦的输入控件,并按出厂配置在末尾追加一个回车(Enter/CR)。对应用而言,扫一次码 ≈ 用户瞬间在输入框里粘了一串文本又按了回车。

我们标签上二维码编码的正是一条 URL(后端 product.go:290go-qrcode 生成):

# 二维码内容 {PublicURL}/app/product/{public_id}?code={code}

因此扫一次,捕获框里会瞬间出现整条 URL + 回车。前端只需:一个常聚焦的输入框 + 监听回车 + 解析 URL 取 public_id

区分扫码 vs 人手打字:扫码是爆发式输入(字符间隔通常 <30ms,几十毫秒打完整条 URL),人手打字间隔 >100ms。可用「回车结束 + 输入速度」双判据;但出库这种专用扫码模式下,只靠「专用捕获框 + 回车提交」就够,不必依赖计时。

⚠ 必须是 2D(二维码)扫码枪。便宜的一维条码枪读不了 QR。若采购的是 1D 枪 → 见附录 B(改印一维条码)。

2. 端上定位:桌面优先

扫码枪插 PC(Windows / macOS)USB 口用,所以扫码出库主要落在桌面/宽屏端的出库建单页。手机端不接扫码枪——手机若要扫码得走摄像头(mobile_scanner)那是另一条技术路线,本方案不含

前端只在 stock_out_form_screen.dart宽屏布局里加「扫码模式」开关,窄屏不出现。

3. 数据链路:扫到的 URL → 本店 product_id

关键 gap:出库明细行以 product_id(数字主键)为锚(StockOutItem.ProductID),但扫到的是 public_id / code。需要一步映射,把扫到的码解析成当前店的 product + 该仓库可用库存。分两段:

3.1 前端解析 URL

3.2 后端新增解析接口(鉴权 + shop 隔离)

GET /api/v1/inventory/scan?public_id={id}&warehouse_id={wid} (登录鉴权,shop_id 取自 JWT) 逻辑: 1. 本店精确查 product WHERE shop_id=? AND public_id=? (跨店查不到 → 404) 2. 查该仓该 product 的 inventory 行 status IN (stock,on_sale) (排除 sold) 3. 返回 { product_id, product_code, name, series, spec, available_qty, sale_price } (结构同前端 _PickerItem)

命中不到 / 不属本店 / 该仓无库存 / 已售罄 → 返回明确错误码,前端据此红字提示。返回结构复用出库表单已有的 _PickerItem,前端拿到即可直接装配明细行,无需二次转换。

为何要新接口、不复用现有的

现有能力为什么不能直接用
鉴权 GET /inventory?code=codeLIKE 模糊匹配(inventory.go:113),不精确、也不吃 public_id
公开 GET /public/products/:public_id按 public_id 查,但无鉴权、无 shop 隔离、无库存维度,不能驱动出库
鉴权 GET /inventory?product_id=精确,但入参已经是 product_id——正是我们缺的那一步转换

4. 交互流程:扫一个,加一个

① 打开出库建单 → 选仓库 → 开「扫码模式」 ② 捕获框自动聚焦(失焦自动重夺) ③ 扣扳机 ─────────────▶ URL + 回车 进框 ④ 前端解析 public_id ─▶ GET /inventory/scan?public_id=..&warehouse_id=.. ⑤ 命中: 该 product 已在明细 ─▶ 数量 +1(可内联改) 不在明细 ─▶ 新增一行 qty=1,带 product_id / 售价 / 可用量 ✓ 成功提示音 + toast「茅台飞天 已加,当前 3 件」,焦点留在捕获框,接着扫 ⑥ 未命中 / 售罄 / 跨店: ✗ 失败提示音 + 红 toast,不加行 ⑦ 扫完 → 照常提交出库单(提交仍走 CheckInventoryAvailability 库存校验)

整个过程手不离枪:焦点始终锁在捕获框,一次扫码 = 一次「解析 → 查 → 加行 → 提示」闭环,无需鼠标。

⚑ 需要你拍板:序列号语义下,一次扫码加多少数量?

数据铁律里 product = 一个特有产品 / 序列号,很多时候一个 product 对应一件 / 一批具体货。这决定「扫一次加几件」:

口径行为适用
A · 每扫 +1建议默认每扫一次数量 +1,可内联改;超可用量即时红字告警一个 product = 多件同款,标签贴整批,按扫的次数点件数
B · 一码一件扫一次即锁定该 product(固定 1,或整行可用量),重复扫视为重复→忽略/提示严格一物一码一序列号,一个码就是一件

建议先按 A(每扫 +1,数量可改) 落地,最贴合现在「批量勾选后改数量」的习惯。请你确认口径再进实现。

5. 改动清单

后端

前端 · client/lib/screens/stock_out/stock_out_form_screen.dart

6. 边界与坑

处理
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,可选)

附录 A · 扫码枪 vs 手机摄像头

扫码枪(本方案)手机摄像头
技术路线HID 键盘输入,无图像mobile_scanner + 摄像头 + 图像解码
桌面 PC(Win/Mac)手机 App
前端依赖零新增新增扫码库 + 摄像头权限
连扫效率高(扣扳机即出)中(对焦、找码)
硬件成本一把 2D 枪已有手机
本方案采用不含(未来可作手机端补充)

附录 B · 若用 1D 枪 → 加印一维条码

若采购的是便宜的一维条码枪(读不了 QR),改在标签上加印一段 Code128 一维条码,内容 = 商品 code(同店唯一)。扫到的就是纯 code,走 GET /inventory/scan?code={code} 精确匹配即可(同样带 shop 隔离)。QR 与一维条码可并存于同一标签(QR 供顾客溯源,条码供出库)。改动落在 label_data.dart / 打印工具。

附录 C · 本地映射加速(可选优化)

选好仓库后一次性把该仓 inventory 全量拉到前端,建 public_id → {product_id, 售价, 可用量} 内存映射。扫码时本地命中、零网络往返,连扫如飞;提交前再由后端 CheckInventoryAvailability 兜底校验。适合货多、连扫密集的场景,第一版可不做。

落地评级

本改动 = 新接口 + 跨后端/前端 2 模块,属项目规则里的「大改」:按 CLAUDE.md,实现前须先进 plan 模式经你批准;且前端改动须遵守 design-first(先改设计系统原型再改真实页面)。本文件仅方案评审,你确认「数量口径(A/B)」+「是否修正二维码路径前缀」后,我再出实现 plan。