本文档描述系统商品库存管理的完整架构,涵盖库存数据模型、服务层设计、库存变动流程、与订单/支付/退款模块的集成,以及幂等性保护机制。 帮助开发人员理解库存数据的生命周期和关键设计决策。
本系统的库存管理完全在本地进行,haha 平台不提供库存管理 API。两者的分工:
| 数据 | 管理方 | 说明 |
|---|---|---|
| 商品信息(名称、条码、价格、图片) | haha 平台 → 本地同步 | DataSyncService.syncProducts() 定时拉取 |
| 设备列表 | haha 平台 → 本地同步 | DataSyncService.syncDevices() |
| 订单支付状态 | 本地 → haha 平台推送 | notifyHahaOrderStatusChanged() |
| 库存数量 | 纯本地管理 | haha 不存库存,不提供库存接口 |
库存按 设备 + 商品 维度管理,即同一商品在不同设备上有独立的库存记录(t_device_inventory 表)。
haha 平台的职责是 AI 识别(开关门事件 + 商品识别结果)和设备管理。其 SDK 提供的 GoodsApi 和 OrderApi 均无库存查询/更新端点,商品 API 仅管理产品目录和上架状态。
库存数据没有自动初始化,唯一的入口是人工上货操作:
管理员/补货员手动上货 → DeviceInventoryService.increaseStock()
→ 首次上货该商品 → 新建 t_device_inventory 记录
→ 再次上货 → 累加 stock
以下流程不创建库存:
| 流程 | 做了什么 | 为什么不创建库存 |
|---|---|---|
| 设备同步 | 拉取设备信息写入 t_device |
haha 的 DeviceInfo 不含库存字段 |
| 商品同步 | 拉取商品目录写入 t_product |
商品信息不绑定设备和数量 |
| 商品上架 | 调用 haha API 设置可售卖商品 | 只是标记可售卖,不涉及数量 |
未来扩展:如果 haha 平台升级 API 在设备同步时回传库存数据,策略是 首次同步用 haha 数据初始化,后续同步以本地为准。因为首次时本地无数据,哈哈的数据可作为初始值;之后本地实时扣减/补货,本地数据才是最新真相源。
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 | 当前库存数量 |
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 | 上货后库存 |
| 服务 | 职责 |
|---|---|
DeviceInventoryService |
库存核心 CRUD:增、减、调整、查询、统计 |
InventoryLogService |
记录并查询库存变动日志,提供幂等性检查 |
StockRecordService |
管理上货任务(创建、完成、取消) |
OrderInventoryService |
桥梁服务:连接订单/支付/退款与库存操作 |
increaseStock() → 补货入库,首次自动创建库存记录
decreaseStock() → 销售出库,库存不足抛 BusinessException
adjustStock() → 直接设置库存值,用于盘点/退款恢复
getByDeviceAndProduct() → 查询单条库存记录
getStatistics() → 聚合统计(总记录数/总库存/低库存/零库存)
decreaseStockOnPaid(Order) → 扣减库存(幂等,识别时和支付时均可调用,先到达者生效)
restoreStockOnRefund(Order, List) → 退款成功后恢复库存(支持部分退款,受 restoreStock 标志控制)
checkStockAvailable(deviceId, items) → 下单前校验库存是否充足
管理员/补货员 → POST /inventory/increase 或 POST /replenisher/stock/replenish
→ DeviceInventoryService.increaseStock()
→ 查询现有库存记录
→ 不存在则新建,存在则累加
→ InventoryLogService.logChange(TYPE_RESTOCK)
特性:首次为设备添加商品时自动创建库存记录;后续补货直接累加数量。
核心原则:用户关门那一刻,商品已物理离开柜子——库存应立即扣减,不等支付。支付是独立的财务环节,可能延迟甚至失败。
主流程(识别时触发):
用户关门 → 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() |
核心原则:库存代表柜内物理商品数量。只有商品没被拿走时才恢复库存(如 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 明细逐商品恢复,只恢复退款的那部分数量。
decreaseStockOnPaid() 在识别时和支付时都会被调用,通过查询 t_inventory_log 表保证只扣一次:
long deducted = inventoryLogService.countByOrderIdAndType(orderNo, TYPE_SALE);
if (deducted > 0) {
log.info("订单库存已扣减,跳过重复扣减: orderNo={}", orderNo);
return;
}
保护场景:
注意:扣减失败不阻断主流程(外部 try-catch 包裹),确保库存问题不影响用户支付体验。
退款恢复依赖 restoreStock 标志控制是否执行。如果执行恢复,按 RefundItem 精确的商品明细操作,每次调用只恢复本次退款的商品数量。
当前策略:退款为管理员审核操作,重复调用概率极低。如需严格幂等,可在 InventoryLog.remark 中记录 refundId 实现精准去重。
| 端点 | 方法 | 用途 | 权限 |
|---|---|---|---|
/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 |
| 端点 | 方法 | 用途 |
|---|---|---|
/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,且本地管理可灵活控制预警阈值、统计维度 |
| 无自动初始化 | 库存数据完全由人工上货创建,设备/商品同步不创建库存记录 |
| 哈哈库存首次导入策略 | 如哈哈后续提供设备库存数据:首次同步做初始化,后续同步丢弃以本地为准 |
haha-entity/.../entity/DeviceInventory.javahaha-entity/.../entity/InventoryLog.javahaha-entity/.../entity/StockRecord.javahaha-entity/.../entity/StockRecordItem.javahaha-entity/.../entity/Refund.java — 含 restoreStock 标志haha-entity/.../entity/RefundItem.javahaha-service/.../service/DeviceInventoryService.javahaha-service/.../service/impl/DeviceInventoryServiceImpl.javahaha-service/.../service/InventoryLogService.javahaha-service/.../service/impl/InventoryLogServiceImpl.javahaha-service/.../service/OrderInventoryService.javahaha-service/.../service/impl/OrderInventoryServiceImpl.javahaha-service/.../service/StockRecordService.javahaha-service/.../service/impl/StockRecordServiceImpl.javahaha-service/.../service/RefundService.javahaha-service/.../service/impl/RefundServiceImpl.javahaha-admin/.../controller/InventoryController.javahaha-admin/.../controller/ReplenisherOperationController.javahaha-admin/.../sql/inventory.sqlhaha-admin/.../sql/inventory_test_data.sqlhaha-admin/.../db/migration/V5__create_refund_tables.sqlhaha-admin/.../db/migration/V6__add_refund_restore_stock.sqlhaha-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 标志控制)haha-entity/.../dto/RefundDTO.java — 管理员退款请求,含 restoreStock 字段haha-common/.../vo/RefundApplicationVO.java — 退款详情响应,含 restoreStock 字段