零售库存管理的全栈复盘:扫码、批量操作与账本一致性
本文目录
本文根据个人项目中一套 React 管理端和 Node.js API 的默认分支做只读源码复核。两个应用分别负责库存运营界面与业务接口;文章只讨论可复用的工程经验,不包含私有仓库地址、服务器域名、账号、环境文件内容或认证材料。没有构建这些远端项目、连接数据库、登录系统或访问生产服务。
一条完整的库存操作链
管理端用 React 与 Ant Design 实现库存流水列表。页面支持配件名称检索、入库/出库筛选、日期范围、分页、扫码枪录入、手动选择配件、批量入库/出库、修改流水和库存预警提示。操作记录保留变动前后值、原因、条码、操作人和时间。API 侧采用 Express、Sequelize 和关系型数据库,库存路由通过身份验证与管理员角色中间件保护。
这条链路的设计方向是对的:库存不仅是商品表上的一个数字,操作人、原因和变动前后值都对日常追查有用;扫码和批量入口也能减少重复录入。不过,前端展示、接口响应和数据库写入必须共享同一份成功/失败语义。只要其中一层把“请求返回 2xx”理解成“所有条目都已完成”,运营人员看到的状态就可能和账本不一致。
复核发现一:统计数字只是当前页统计
页面请求日志时固定带分页参数,单页大小为 10;页面上的入库次数、入库总量、出库次数和出库总量随后直接从当前 logs 数组计算。这个数组受当前关键词、类型、日期和分页共同影响,因此这些卡片表示“当前筛选条件下的当前页”,并不是全部库存流水的总计。翻页后数字也会跟着变。
这不一定是计算错误,问题在于页面没有把统计范围表达出来。可以选择两种明确设计:
- 若卡片只用于当前页快速核对,标题注明“本页”,并确保分页、筛选后的说明始终可见。
- 若它们要表达全量或筛选范围总计,由 API 根据同一组筛选条件计算聚合值,与日志分页结果一起返回;不要为了统计而在浏览器里拉取全部流水。
后端聚合需要说明时间边界和时区、是否包括软删除记录、入库/出库数量是按操作次数还是数量求和,并为大数据量查询设计索引。否则,一个看似简单的卡片可能同时混淆统计口径与分页口径。
复核发现二:批量出库的部分失败会被界面报成成功
前端会先过滤出有配件 ID 且数量大于零的行,再调用批量出库接口。成功回调不检查实际结果,就提示“批量出库成功,共 N 件配件”;这里的 N 是有效行数,不是数量总和。后端响应则分别包含已处理的 results、失败的 errors 和低库存 warnings:不存在的配件或库存不足可以只让某些行失败,整个 HTTP 请求仍正常返回。
当前界面只读取 warnings,没有向操作者展示 errors,也没有用 results 计算真实成功数量。于是一次部分成功可能显示为整体成功,而某一行甚至可能没有被处理。修复时应把接口结果变成逐行回执:显示配件、请求数量、实际结果、失败原因和是否可重试;汇总区明确成功行数、失败行数与数量合计。若选择“全部成功或全部回滚”的整批原子语义,则接口必须用事务实现该契约,不能只改提示文案。
复核发现三:库存更新和流水写入不是一个原子操作
单条入库/出库、扫码操作和批量操作都先读取配件库存、再更新配件、最后创建流水;代码没有把这些步骤放进同一数据库事务,也没有对库存行加锁。更新库存成功而创建流水失败时,当前库存会改变但没有对应流水;相邻请求同时读取旧库存时,也可能各自基于同一个旧值计算并覆盖彼此的结果。
库存数量和流水前后值属于一个业务事实,应在同一事务内一起提交或一起回滚。并发控制可按数据库能力选用行锁,或使用带库存下限条件的原子更新,并检查受影响行数。批量操作还要明确定义是整批原子,还是每条独立提交并返回部分结果;重试可能重复扣减时,应增加幂等键或业务操作号。
编辑或删除旧流水也会调整当前库存。当前这些操作同样分多次写入;更重要的是,直接删除旧流水会损害审计解释能力。正式账本通常追加一条冲正或盘点调整记录并引用原流水,而不是抹掉已经发生的历史。
复核发现四:扫码接口信任了未经校验的运行时输入
TypeScript 请求体类型和前端输入控件不能替代服务端校验。扫码接口只检查 type 和 quantity 是否为空,再把 quantity 转成数字;它没有确认 type 严格等于 in/out,也没有拒绝非有限数、分数或负数。非 out 的未知 type 会走出库分支,负数量可能反向增加库存;NaN 等异常值也可能一路进入数据库调用后才失败。单条和批量入库还使用 JavaScript 加法处理请求数量,未先将字符串输入规范化为经过验证的数字。
所有写入口都应在控制器边界将未知请求解析为明确的命令结构:类型必须是允许值,数量必须是正的安全整数,商品 ID 必须合法,数组大小和重复项目也应受限。非法请求应在任何数据库写入前返回稳定的 4xx 错误。单条、扫码、批量与后台修正最终应调用同一领域服务,避免入口之间各有一套校验。
复核发现五:列表与扫码历史使用了可能过期的异步状态
库存列表会在页码、关键词、类型或日期范围变化时发起新请求,但请求没有 AbortController 或递增序号校验。用户快速切换筛选时,较早发出的请求可能较晚返回并覆盖新筛选结果;旧请求的 finally 也可能提前关闭仍在执行的新请求加载状态。请求层应取消过期请求,或在写入列表和结束 loading 前确认响应仍属于当前查询。
扫码操作等待接口响应后,会用闭包捕获的 scanHistory 数组追加成功或失败记录。若连续扫码产生的请求重叠,多个回调可能基于同一份旧数组写回,导致部分记录消失。应使用函数式状态更新,并给每次扫描分配稳定 ID;如果界面按扫描顺序展示,还要显式按发起序号排序,避免网络返回顺序改变历史顺序。
这两项是从当前前端源码推导出的竞态路径,本次没有运行原项目或人为制造慢响应,因此不能说已在用户设备上复现。回归测试应让旧筛选请求晚于新请求返回,并让两个扫码请求以相反顺序完成,确认当前查询结果不回退、两条扫码回执都保留。
复核发现六:手动选择只请求固定数量的配件
库存页的配件加载函数只调用一次列表接口,并传入 pageSize: 200;批量入库、批量出库和“条码无法识别时手动入库”都从这份内存列表构造选择项。前端没有继续翻页或按搜索词重新查询的逻辑。若接口按常规分页返回最多 200 条,超过这一页的配件就不会出现在这些手动选择器中;扫码枪直接按条码查询的路径不受这项 UI 限制影响。
应让选择器把搜索词交给服务端,并支持分页或按需加载;页面还要显示加载失败、无匹配项和已加载数量,不能把“当前页没有”误说成“配件不存在”。验收时用至少 201 条配件的测试数据,确认位于后续页的配件也能通过搜索加入批量操作和手动入库。此次只核对了管理端调用,没有验证该 API 的分页上限或真实大目录响应,因此这是有明确源码依据的条件性完整性风险。
建议的验收标准
- 统计 API 的筛选条件与日志列表完全一致;分页改变不改变筛选范围总计,页面清楚标出时间区间和统计口径。
- 批量接口逐条返回稳定的结果码,前端完整显示成功、失败、跳过和预警,不把有效行数称作处理数量。
- 流水写入失败时库存更新回滚;并发出库不能让库存低于零,也不能丢失一次有效变更。
- 扫码、单条和批量接口都拒绝未知类型、负数、零、分数、NaN、Infinity、数字字符串和超大数量,且在写数据库前失败。
- 同一幂等键重放不会重复入库或出库;一批中重复出现相同配件时,响应和库存语义有明文约定。
- 编辑/撤销操作保留审计链,能从调整记录追溯到原流水、操作者和原因。
- 管理端测试覆盖部分失败回执和当前页/全量统计口径;服务端测试覆盖事务回滚、并发更新和权限拒绝。
相关专题与复核边界
上述结论来自对管理端库存页面、库存 API 控制器和路由中间件的静态检查;没有运行项目测试、构建、数据库操作或生产验收。尤其是并发竞态和写入失败的影响属于源码暴露出的风险路径,并非已观察到的线上事故。正式改造前应先补回归测试,再在隔离环境验证事务、并发及部分成功契约。
参考资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。