# 库存管理模块说明 > 本文档描述系统商品库存管理的完整架构,涵盖库存数据模型、服务层设计、库存变动流程、与订单/支付/退款模块的集成,以及幂等性保护机制。 > 帮助开发人员理解库存数据的生命周期和关键设计决策。 --- ## 一、模块概述 ### 1.1 库存管理范围 本系统的库存管理**完全在本地进行**,haha 平台不提供库存管理 API。两者的分工: | 数据 | 管理方 | 说明 | |------|--------|------| | 商品信息(名称、条码、价格、图片) | haha 平台 → 本地同步 | `DataSyncService.syncProducts()` 定时拉取 | | 设备列表 | haha 平台 → 本地同步 | `DataSyncService.syncDevices()` | | 订单支付状态 | 本地 → haha 平台推送 | `notifyHahaOrderStatusChanged()` | | **库存数量** | **纯本地管理** | haha 不存库存,不提供库存接口 | ### 1.2 库存维度 库存按 **设备 + 商品** 维度管理,即同一商品在不同设备上有独立的库存记录(`t_device_inventory` 表)。 ### 1.3 为什么库存不放在 haha 平台? haha 平台的职责是 AI 识别(开关门事件 + 商品识别结果)和设备管理。其 SDK 提供的 `GoodsApi` 和 `OrderApi` 均无库存查询/更新端点,商品 API 仅管理产品目录和上架状态。 ### 1.4 库存初始化机制 库存数据**没有自动初始化**,唯一的入口是人工上货操作: ``` 管理员/补货员手动上货 → DeviceInventoryService.increaseStock() → 首次上货该商品 → 新建 t_device_inventory 记录 → 再次上货 → 累加 stock ``` 以下流程**不创建库存**: | 流程 | 做了什么 | 为什么不创建库存 | |------|---------|-----------------| | 设备同步 | 拉取设备信息写入 `t_device` | haha 的 `DeviceInfo` 不含库存字段 | | 商品同步 | 拉取商品目录写入 `t_product` | 商品信息不绑定设备和数量 | | 商品上架 | 调用 haha API 设置可售卖商品 | 只是标记可售卖,不涉及数量 | > **未来扩展**:如果 haha 平台升级 API 在设备同步时回传库存数据,策略是 **首次同步用 haha 数据初始化,后续同步以本地为准**。因为首次时本地无数据,哈哈的数据可作为初始值;之后本地实时扣减/补货,本地数据才是最新真相源。 --- ## 二、数据库设计 ### 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 | **当前库存数量** | | `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` 表保证只扣一次: ```java 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,且本地管理可灵活控制预警阈值、统计维度 | | **无自动初始化** | 库存数据完全由人工上货创建,设备/商品同步不创建库存记录 | | **哈哈库存首次导入策略** | 如哈哈后续提供设备库存数据:首次同步做初始化,后续同步丢弃以本地为准 | --- ## 九、相关文件索引 ### 实体 - `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` 字段