库存管理模块.md 20 KB

库存管理模块说明

本文档描述系统商品库存管理的完整架构,涵盖库存数据模型、服务层设计、库存变动流程、与订单/支付/退款模块的集成,以及幂等性保护机制。 帮助开发人员理解库存数据的生命周期和关键设计决策。


一、模块概述

1.1 库存管理范围

本系统的库存管理完全在本地进行,haha 平台不提供库存管理 API。两者的分工:

数据 管理方 说明
商品信息(名称、条码、价格、图片) haha 平台 → 本地同步 DataSyncService.syncProducts() 定时拉取
设备列表 haha 平台 → 本地同步 DataSyncService.syncDevices()
订单支付状态 本地 → haha 平台推送 notifyHahaOrderStatusChanged()
库存数量 纯本地管理 haha 不存库存,不提供库存接口

1.2 库存维度

库存按 设备 + 商品 维度管理,即同一商品在不同设备上有独立的库存记录(t_device_inventory 表)。

1.3 为什么库存不放在 haha 平台?

haha 平台的职责是 AI 识别(开关门事件 + 商品识别结果)和设备管理。其 SDK 提供的 GoodsApiOrderApi 均无库存查询/更新端点,商品 API 仅管理产品目录和上架状态。

1.4 库存初始化机制

库存数据有两个入口进入系统:

入口一:人工上货(主要,创建实际库存)

管理员/补货员手动上货 → DeviceInventoryService.increaseStock()
    → 首次上货该商品 → 新建 t_device_inventory 记录(stock > 0)
    → 再次上货 → 累加 stock

人工上货创建的记录带有 实际库存数量stock > 0),是唯一的物理库存来源。

入口二:层模版同步(辅助,创建占位记录)

设备层模版同步 → syncFromHaha(deviceId) → initInventoryFromTemplate(deviceId, apiData)
    → 解析层模版中的商品编码
    → 查本地 Product 表确认商品存在
    → 查 t_device_inventory 确认记录不存在
    → 新建 t_device_inventory 记录(stock = 0)

层模版同步创建的记录 stock 初始化为 0,目的是:

  • 让设备在库存列表中提前可见所有理论货道商品
  • recalculateStandardStock 提供记录载体(标准库存值写入已有记录)
  • 已存在的人工上货记录不受影响

创建条件

条件 行为
商品编码在本地 Product 表中不存在 跳过
该设备+商品已有库存记录 跳过(不覆盖已有 stock)
该设备+商品无库存记录 新建,stock=0, warningThreshold=5

不会创建库存的流程

流程 做了什么 为什么不创建库存
设备同步 拉取设备信息写入 t_device haha 的 DeviceInfo 不含库存字段
商品同步 拉取商品目录写入 t_product 商品信息不绑定设备和数量
商品上架 调用 haha API 设置可售卖商品 只是标记可售卖,不涉及数量

初始化策略总结:人工上货是唯一写入实际库存(stock>0)的途径,层模版同步提供占位能力让运营平台提前看到设备货道商品列表。


1.5 标准库存重算(recalculateStandardStock)

层模版同步完成后,会根据层模版中的货道商品配置重新计算每台设备每个商品的 standard_stock(理论满柜库存量)。

触发时机:层模版创建 / 更新 / 删除 / 同步 操作完成后自动调用。

计算逻辑

查询该设备所有层模版 (t_layer_template by deviceId)
  │
  ├─ 无层模版 → 所有库存记录的 standard_stock 置 0
  │
  └─ 有层模版 → 遍历每个 LayerTemplate:
        │
        ├─ 解析 leftFloors JSON → 遍历每层每个货道 → goodsItem.stock
        └─ 解析 rightFloors JSON → 遍历每层每个货道 → goodsItem.stock
              │
              └─ 按 productCode 累加 → standardMap.merge(code, stock, Integer::sum)
                 (同一商品跨层/跨门配置数量叠加)
        │
        └─ 遍历设备所有 t_device_inventory 记录:
              standard_stock = standardMap 中的累计值(无则为 0)
              → updateById()

关键规则

  • 同一商品在多个楼层或左右门都有配置 → 累加各货道的摆放数量
  • 层模版有但库存表没有的商品 → 不处理(由 initInventoryFromTemplate 预先建好记录)
  • 库存表有但层模版没有的商品 → standard_stock = 0
  • 每次全量覆盖,非增量更新

用途:standard_stock 作为理论满柜值,用于库存管理页面展示库存占比(stock / standard_stock),帮助运营判断缺货程度。

为什么不在同步时直接写入 stock? 物理库存必须由实际补货操作确认,层模版是"应该放什么商品"的配置,不代表当前柜内真实存量。standard_stock 记录的是"满柜时应该有多少",与实际 stock 分离。


二、数据库设计

2.1 核心表

t_device_inventory — 设备库存表

每台设备上每个商品一条记录,使用乐观锁保证并发安全。

字段 类型 说明
id BIGINT 主键,雪花算法
device_id VARCHAR(64) 设备 SN
product_id BIGINT 商品 ID(关联 t_product
product_code VARCHAR(100) haha 平台商品编码
product_name VARCHAR(200) 商品名称快照
stock INT 当前库存数量
standard_stock INT 标准库存(满柜理论值,由层模版同步自动重算)
shelf_num INT 货架层位置
position VARCHAR(50) 通道位置(left/right)
warning_threshold INT 低库存预警阈值(默认 5)
last_restock_time DATETIME 最近补货时间
version INT 乐观锁版本号(MyBatis-Plus @Version
create_time DATETIME 创建时间
update_time DATETIME 更新时间

唯一约束:(device_id, product_id)

t_inventory_log — 库存变动日志表

记录每一次库存变动的审计跟踪,用于溯源和统计。

字段 类型 说明
id BIGINT 主键
device_id VARCHAR(64) 设备 ID
product_id BIGINT 商品 ID
product_code VARCHAR(100) 商品编码
product_name VARCHAR(200) 商品名称
change_type INT 变动类型(见下表)
change_quantity INT 变动数量(正数增加,负数减少)
before_stock INT 变动前库存
after_stock INT 变动后库存
order_id VARCHAR(64) 关联订单号(销售时)
activity_id VARCHAR(64) 关联活动号
operator_id BIGINT 操作人 ID
operator_name VARCHAR(50) 操作人名称
remark VARCHAR(500) 备注
create_time DATETIME 创建时间

变动类型常量:

常量 说明
TYPE_RESTOCK 1 上货增加
TYPE_SALE 2 销售减少
TYPE_ADJUST_ADD 3 调整增加
TYPE_ADJUST_SUB 4 调整减少
TYPE_INVENTORY 5 盘点调整

t_stock_record — 上货记录表(补货任务)

字段 类型 说明
id BIGINT 主键
device_id VARCHAR(64) 设备 ID
type INT 类型:1=首次上货,2=补货
status INT 状态:1=进行中,2=已完成,3=已取消
operator_id BIGINT 操作人 ID
activity_id VARCHAR(64) 活动号
create_time DATETIME 创建时间

t_stock_record_item — 上货记录商品明细

字段 类型 说明
id BIGINT 主键
stock_record_id BIGINT 上货记录 ID
product_id BIGINT 商品 ID
product_code VARCHAR(100) 商品编码
product_name VARCHAR(200) 商品名称
quantity INT 上货数量
before_stock INT 上货前库存
after_stock INT 上货后库存

三、服务层设计

3.1 服务职责

服务 职责
DeviceInventoryService 库存核心 CRUD:增、减、调整、查询、统计
InventoryLogService 记录并查询库存变动日志,提供幂等性检查
StockRecordService 管理上货任务(创建、完成、取消)
OrderInventoryService 桥梁服务:连接订单/支付/退款与库存操作

3.2 核心方法

DeviceInventoryService

increaseStock()   → 补货入库,首次自动创建库存记录
decreaseStock()   → 销售出库,库存不足抛 BusinessException
adjustStock()     → 直接设置库存值,用于盘点/退款恢复
getByDeviceAndProduct() → 查询单条库存记录
getStatistics()   → 聚合统计(总记录数/总库存/低库存/零库存)

OrderInventoryService(桥梁服务)

decreaseStockOnPaid(Order)           → 扣减库存(幂等,识别时和支付时均可调用,先到达者生效)
restoreStockOnRefund(Order, List)    → 退款成功后恢复库存(支持部分退款,受 restoreStock 标志控制)
checkStockAvailable(deviceId, items) → 下单前校验库存是否充足

四、库存变动流程

4.1 补货入库流程

管理员/补货员 → POST /inventory/increase 或 POST /replenisher/stock/replenish
    → DeviceInventoryService.increaseStock()
        → 查询现有库存记录
        → 不存在则新建,存在则累加
        → InventoryLogService.logChange(TYPE_RESTOCK)

特性:首次为设备添加商品时自动创建库存记录;后续补货直接累加数量。

4.2 销售出库流程(AI 识别后立即扣减)

核心原则:用户关门那一刻,商品已物理离开柜子——库存应立即扣减,不等支付。支付是独立的财务环节,可能延迟甚至失败。

主流程(识别时触发):

用户关门 → AI 识别完成 → handleOrcResult
    → handleConsume (OUT 类型,正常取出)
        → updateOrderFromRecognition(设置 items)
        → decreaseStockOnPaid(order) ← 立即扣减
            → 幂等性检查:查询 t_inventory_log 是否已有 TYPE_SALE 记录
            → 解析订单 items JSON,遍历商品
            → DeviceInventoryService.decreaseStock()
                → 扣减并记录 InventoryLog(TYPE_SALE)
    → IN 类型(异物放入)跳过,不扣库存

兜底流程(支付时触发,幂等性保证不会重复扣减):

用户支付成功
    → 支付回调 / 支付分回调
        → decreaseStockOnPaid(order) ← 若识别时已扣,幂等检查跳过

触发点汇总

阶段 触发路径 服务 方法
识别时(主) AI 识别为 OUT 类型 HahaCallbackServiceImpl handleConsume()
支付时(兜底) 微信支付 V3 回调 PaymentServiceImpl handleCallback()
支付时(兜底) 支付分完结扣款成功 PayScoreServiceImpl completeServiceOrder()
支付时(兜底) 支付分 USER_PAID 回调 PayScoreServiceImpl handleCallback()
支付时(兜底) 支付分状态同步为 DONE PayScoreServiceImpl syncPayScoreStatus()
支付时(兜底) 支付分同步支付信息 PayScoreServiceImpl syncOrderPaid()
支付时(兜底) 订单回调中支付分扣费成功 HahaCallbackServiceImpl processPayScorePayment()
支付时(兜底) 直接完成订单(无支付分) OrderServiceImpl completeOrderWithPayScore()

4.3 退款恢复库存流程(区分场景)

核心原则:库存代表柜内物理商品数量。只有商品没被拿走时才恢复库存(如 AI 识别多扣);商品已被取走但退款(如质量问题)不应恢复。

通过 t_refund.restore_stock 字段控制(默认 1=恢复):

场景 举例 restoreStock 行为
识别多扣 拿了 1 瓶水,AI 识别成 2 瓶 1(默认) 退钱 + 恢复库存
订单错误 重复扣款、金额计算错误 1(默认) 退钱 + 恢复库存(用户实际没取走多余)
质量问题 拿了商品,发现有质量问题要退款 0 只退钱,不恢复库存(商品已取走)
用户不满 拿了商品,不满意要退款 0 只退钱,不恢复库存(商品已取走)
管理员审核通过 / 直接退款成功
    → RefundServiceImpl.approveRefund() 或 markRefunded()
        → restoreStockForRefund(refund)
            → 检查 refund.restoreStock:
                → 0(不恢复)→ 跳过,仅记录日志
                → 1 或 null(恢复)→ 继续
            → 获取订单 + 退款商品明细(RefundItem 列表)
            → OrderInventoryService.restoreStockOnRefund(order, refundItems)
                → 逐商品 adjustStock(current + quantity)
                → 记录 InventoryLog(TYPE_ADJUST_ADD)

支持部分退款:按 RefundItem 明细逐商品恢复,只恢复退款的那部分数量。


五、幂等性保护

5.1 库存扣减幂等性

decreaseStockOnPaid() 在识别时和支付时都会被调用,通过查询 t_inventory_log 表保证只扣一次:

long deducted = inventoryLogService.countByOrderIdAndType(orderNo, TYPE_SALE);
if (deducted > 0) {
    log.info("订单库存已扣减,跳过重复扣减: orderNo={}", orderNo);
    return;
}

保护场景

  • 识别时已扣减(主路径),支付回调到来时幂等跳过(兜底)
  • 微信支付回调可能因网络重试多次推送
  • 支付分回调与主动查询可能并发触发

注意:扣减失败不阻断主流程(外部 try-catch 包裹),确保库存问题不影响用户支付体验。

5.2 库存恢复幂等性

退款恢复依赖 restoreStock 标志控制是否执行。如果执行恢复,按 RefundItem 精确的商品明细操作,每次调用只恢复本次退款的商品数量。

当前策略:退款为管理员审核操作,重复调用概率极低。如需严格幂等,可在 InventoryLog.remark 中记录 refundId 实现精准去重。


六、API 端点

6.1 管理后台(InventoryController)

端点 方法 用途 权限
/inventory/device-stats GET 按设备分组的库存统计 inventory:view
/inventory/list GET 库存列表(分页,可筛选) inventory:view
/inventory/device/{deviceId} GET 单台设备库存详情 inventory:view
/inventory/low-stock GET 低库存商品列表 inventory:view
/inventory/statistics GET 汇总统计 inventory:view
/inventory/increase POST 增加库存(上货) inventory:update
/inventory/adjust POST 调整库存 inventory:update
/inventory/logs GET 库存变动日志 inventory:view
/inventory/logs/statistics GET 库存变动统计 inventory:view
/inventory/records GET 上货记录列表 inventory:view
/inventory/records/{id} GET 上货记录详情 inventory:view
/inventory/records POST 创建上货记录(V1) inventory:update
/inventory/records/v2 POST 创建上货记录(V2) inventory:update
/inventory/records/{id}/complete PUT 完成上货记录 inventory:update
/inventory/records/{id}/cancel PUT 取消上货记录 inventory:update
/inventory/records/stocker-statistics GET 补货员统计 inventory:view

6.2 补货员端(ReplenisherOperationController)

端点 方法 用途
/replenisher/my-info GET 当前补货员信息
/replenisher/device/list GET 补货员管辖的设备列表(含库存概览)
/replenisher/device/inventory/{deviceId} GET 单台设备库存详情
/replenisher/stock/replenish POST 执行补货操作

七、低库存预警

系统支持低库存预警机制:

  • 每个库存记录的 warning_threshold 字段(默认值 5)
  • stock <= warning_threshold 时,该商品出现在低库存列表中
  • 管理后台 /inventory/low-stock 和统计面板可查看低库存情况
  • 设备库存统计页按设备汇总:正常 / 低库存 / 缺货 三种状态

八、关键设计决策

决策 原因
识别时立即扣减 关门时商品已物理离开柜子,库存应即时反映;不等支付(可能延迟/失败)
支付时兜底扣减 若识别时未扣(如异常跳过),支付回调仍会触发;幂等性保证不重复
库存扣减不阻断主流程 库存问题不应影响用户支付/取货体验;异常仅记录日志
退款恢复区分场景 restore_stock 标志区分"识别多扣退库存"和"质量问题不退库存",避免恢复不该恢复的商品
乐观锁而非悲观锁 t_device_inventory@Version 字段,MyBatis-Plus 自动处理并发冲突
幂等性通过日志表实现 不在订单表增加 stock_deducted 字段,避免侵入;日志表本身就是审计需求
退款恢复用 adjustStock 相比 increaseStock,不需要补货所需的 shelf_num/position 等字段
库存纯本地管理 haha 平台不提供库存 API,且本地管理可灵活控制预警阈值、统计维度
层模版同步自动初始化 层模版同步时为设备中配置的商品自动创建 stock=0 的库存占位记录,已存在的人工上货记录不受影响
标准库存自动重算 层模版创建/更新/同步后自动调用 recalculateStandardStock,根据货道配置重算每个商品的理论满柜库存值
哈哈库存首次导入策略 如哈哈后续提供设备库存数据:首次同步做初始化,后续同步丢弃以本地为准

九、相关文件索引

实体

  • haha-entity/.../entity/DeviceInventory.java
  • haha-entity/.../entity/InventoryLog.java
  • haha-entity/.../entity/StockRecord.java
  • haha-entity/.../entity/StockRecordItem.java
  • haha-entity/.../entity/Refund.java — 含 restoreStock 标志
  • haha-entity/.../entity/RefundItem.java

服务

  • haha-service/.../service/DeviceInventoryService.java
  • haha-service/.../service/impl/DeviceInventoryServiceImpl.java
  • haha-service/.../service/InventoryLogService.java
  • haha-service/.../service/impl/InventoryLogServiceImpl.java
  • haha-service/.../service/OrderInventoryService.java
  • haha-service/.../service/impl/OrderInventoryServiceImpl.java
  • haha-service/.../service/StockRecordService.java
  • haha-service/.../service/impl/StockRecordServiceImpl.java
  • haha-service/.../service/RefundService.java
  • haha-service/.../service/impl/RefundServiceImpl.java

控制器

  • haha-admin/.../controller/InventoryController.java
  • haha-admin/.../controller/ReplenisherOperationController.java

SQL

  • haha-admin/.../sql/inventory.sql
  • haha-admin/.../sql/inventory_test_data.sql
  • haha-admin/.../db/migration/V5__create_refund_tables.sql
  • haha-admin/.../db/migration/V6__add_refund_restore_stock.sql

集成点

  • haha-service/.../service/impl/HahaCallbackServiceImpl.java主路径:AI 识别后立即扣减库存 + 支付分扣费兜底
  • haha-service/.../service/payment/impl/PaymentServiceImpl.java — 微信支付回调兜底扣减
  • haha-service/.../service/payment/payscore/impl/PayScoreServiceImpl.java — 支付分 4 个路径兜底扣减
  • haha-service/.../service/impl/OrderServiceImpl.java — 直接完成订单兜底扣减
  • haha-service/.../service/impl/RefundServiceImpl.java — 退款恢复库存(受 restoreStock 标志控制)

DTO / VO

  • haha-entity/.../dto/RefundDTO.java — 管理员退款请求,含 restoreStock 字段
  • haha-common/.../vo/RefundApplicationVO.java — 退款详情响应,含 restoreStock 字段