设计文档完善状态机

This commit is contained in:
2026-04-23 10:22:08 +08:00
parent 774fe86941
commit 2aa0ae83c2
2 changed files with 272 additions and 133 deletions

View File

@@ -1,9 +1,10 @@
-- ================================ -- ================================
-- 出入库单据控制层(新增) -- 出入库单据控制层
-- 目标:通过“单据 -> 审核 -> 执行”控制库存变更,避免直接操作出入库流水
-- 说明: -- 说明:
-- 1) 业务先创建出入库单,再由单据生成实际 gear_stock_io / gear_stock_io_detail -- 1) 业务先创建出入库单,再由单据驱动实际 gear_stock_io / gear_stock_io_detail
-- 2) 单据状态控制提交、审核、作废与执行结果 -- 2) 单据支持草稿、提交、审核、执行、作废、冲销
-- 3) 通过 source_io_id 关联已生成的实际出入库记录,确保单据可追溯 -- 3) 通过关联字段保留来源单、回退单、执行单之间的链路
-- ================================ -- ================================
DROP TABLE IF EXISTS gear_stock_io_order; DROP TABLE IF EXISTS gear_stock_io_order;
@@ -12,13 +13,20 @@ CREATE TABLE gear_stock_io_order (
order_code varchar(64) NOT NULL COMMENT '单据编号(唯一)', order_code varchar(64) NOT NULL COMMENT '单据编号(唯一)',
io_type char(1) NOT NULL COMMENT '出入库类型I入库 O出库 T调拨', io_type char(1) NOT NULL COMMENT '出入库类型I入库 O出库 T调拨',
biz_type varchar(32) NOT NULL COMMENT '业务类型purchase/sale/return/transfer/other', biz_type varchar(32) NOT NULL COMMENT '业务类型purchase/sale/return/transfer/other',
source_type varchar(32) DEFAULT '' COMMENT '来源单据类型(采购单/销售单/报工单等)', source_type varchar(32) DEFAULT '' COMMENT '来源单据类型(采购单/销售单/退货单/调拨单等)',
source_no varchar(64) DEFAULT '' COMMENT '来源单据编号', source_no varchar(64) DEFAULT '' COMMENT '来源单据编号',
source_order_id bigint(20) DEFAULT NULL COMMENT '来源单据ID业务单ID',
warehouse_id bigint(20) DEFAULT NULL COMMENT '主仓库ID', warehouse_id bigint(20) DEFAULT NULL COMMENT '主仓库ID',
from_warehouse_id bigint(20) DEFAULT NULL COMMENT '调出仓库ID', from_warehouse_id bigint(20) DEFAULT NULL COMMENT '调出仓库ID',
to_warehouse_id bigint(20) DEFAULT NULL COMMENT '调入仓库ID', to_warehouse_id bigint(20) DEFAULT NULL COMMENT '调入仓库ID',
status char(1) NOT NULL DEFAULT '0' COMMENT '单据状态0草稿 1已提交 2已审核 3已执行 4已作废', status char(1) NOT NULL DEFAULT '0' COMMENT '单据状态0草稿 1已提交 2已审核 3已执行 4已作废 5已冲销',
exec_flag char(1) NOT NULL DEFAULT '0' COMMENT '执行标志0未执行 1已执行', exec_flag char(1) NOT NULL DEFAULT '0' COMMENT '执行标志0未执行 1已执行',
reversal_flag char(1) NOT NULL DEFAULT '0' COMMENT '是否冲销单0否 1是',
reversal_order_id bigint(20) DEFAULT NULL COMMENT '对应原单据ID',
reversal_reason varchar(500) DEFAULT '' COMMENT '冲销原因',
reversal_time datetime COMMENT '冲销时间',
cancel_reason varchar(500) DEFAULT '' COMMENT '作废原因',
cancel_time datetime COMMENT '作废时间',
audit_by varchar(64) DEFAULT '' COMMENT '审核人', audit_by varchar(64) DEFAULT '' COMMENT '审核人',
audit_time datetime COMMENT '审核时间', audit_time datetime COMMENT '审核时间',
execute_by varchar(64) DEFAULT '' COMMENT '执行人', execute_by varchar(64) DEFAULT '' COMMENT '执行人',
@@ -33,12 +41,15 @@ CREATE TABLE gear_stock_io_order (
update_time datetime COMMENT '更新时间', update_time datetime COMMENT '更新时间',
PRIMARY KEY (order_id), PRIMARY KEY (order_id),
UNIQUE KEY uk_order_code (order_code), UNIQUE KEY uk_order_code (order_code),
UNIQUE KEY uk_reversal_order_id (reversal_order_id),
KEY idx_status (status), KEY idx_status (status),
KEY idx_exec_flag (exec_flag), KEY idx_exec_flag (exec_flag),
KEY idx_biz_type (biz_type), KEY idx_biz_type (biz_type),
KEY idx_source_no (source_no), KEY idx_source_no (source_no),
KEY idx_source_order_id (source_order_id),
KEY idx_create_time (create_time), KEY idx_create_time (create_time),
KEY idx_source_io_id (source_io_id) KEY idx_source_io_id (source_io_id),
KEY idx_reversal_flag (reversal_flag)
) ENGINE=InnoDB COMMENT='出入库单据表'; ) ENGINE=InnoDB COMMENT='出入库单据表';
DROP TABLE IF EXISTS gear_stock_io_order_detail; DROP TABLE IF EXISTS gear_stock_io_order_detail;
@@ -58,6 +69,7 @@ CREATE TABLE gear_stock_io_order_detail (
unit_price decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '单价快照', unit_price decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '单价快照',
amount decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '金额快照', amount decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '金额快照',
source_detail_no varchar(64) DEFAULT '' COMMENT '来源明细编号', source_detail_no varchar(64) DEFAULT '' COMMENT '来源明细编号',
reversal_detail_id bigint(20) DEFAULT NULL COMMENT '对应冲销明细ID',
remark varchar(500) DEFAULT NULL COMMENT '备注', remark varchar(500) DEFAULT NULL COMMENT '备注',
del_flag char(1) NOT NULL DEFAULT '0' COMMENT '删除标志0存在 2删除', del_flag char(1) NOT NULL DEFAULT '0' COMMENT '删除标志0存在 2删除',
create_by varchar(64) DEFAULT '' COMMENT '创建者', create_by varchar(64) DEFAULT '' COMMENT '创建者',
@@ -69,11 +81,11 @@ CREATE TABLE gear_stock_io_order_detail (
KEY idx_order_id (order_id), KEY idx_order_id (order_id),
KEY idx_item_id (item_id), KEY idx_item_id (item_id),
KEY idx_batch_no (batch_no), KEY idx_batch_no (batch_no),
KEY idx_source_detail_no (source_detail_no) KEY idx_source_detail_no (source_detail_no),
KEY idx_reversal_detail_id (reversal_detail_id)
) ENGINE=InnoDB COMMENT='出入库单据明细表'; ) ENGINE=InnoDB COMMENT='出入库单据明细表';
-- 建议补充的业务索引说明 -- 建议:
-- 1) 先保存 gear_stock_io_order/gear_stock_io_order_detail 为草稿 -- 1) 业务层只允许通过单据驱动库存变更,禁止直接改 gear_stock / gear_stock_log
-- 2) 提交后进入审核 -- 2) 审核通过后,统一调用执行入口生成实际 gear_stock_io / gear_stock_io_detail
-- 3) 审核通过后生成实际 gear_stock_io / gear_stock_io_detail并回写 source_io_id -- 3) 已执行错误时,通过反向单据进行冲销,不直接修改历史流水
-- 4) 如果需要严格防重复执行,可在业务层通过 order_code + status / source_io_id 控制幂等

View File

@@ -2,172 +2,299 @@
## 1. 背景 ## 1. 背景
当前系统中的出入库逻辑是直接操作 `gear_stock_io``gear_stock_io_detail`缺少“单据层”的控制,因此会出现以下问题: 当前系统中的出入库是直接操作 `gear_stock_io``gear_stock_io_detail`这会带来两个核心问题:
- 业务流程不完整,无法体现草稿、提交、审核、执行等状态 - 库存变更没有“单据审批链”,无法体现业务流程
- 直接操作库存,缺少审批与追溯能力 - 出入库做错后,只能人工修库存,缺少标准回退方案
- 无法与采购、销售、退货、调拨等来源单据建立统一关联
- 事务失败、重复执行、人工误操作时难以定位责任与恢复
因此建议在现有出入库表之上新增一层“出入库单据”,用于统一控制流程,再由单据驱动实际库存操作 因此建议增一层“出入库单据”,由单据统一控制库存执行
---
## 2. 设计目标 ## 2. 设计目标
1. 先单据,后执行:所有库存变化必须先落单据 1. 所有库存变动都先落单据
2. 状态可追踪:支持草稿、提交、审核、执行、作废 2. 单据必须支持草稿、提交、审核、执行、作废、冲销
3. 数据可追溯:单据可关联来源业务单号与实际出入库记录 3. 单据执行后自动生成实际出入库流水
4. 支持扩展:兼容入库、出库、调拨等场景 4. 做错时通过反向单据回退,不直接改历史流水
5. 幂等可控:避免重复执行导致库存被重复增减 5. 保留完整审计链路,方便追责和排查
## 3. 现有表与新增表的关系 ---
### 3.1 现有表 ## 3. 表结构设计
- `gear_stock_io`:实际出入库主表 ### 3.1 单据主表 `gear_stock_io_order`
- `gear_stock_io_detail`:实际出入库明细表
- `gear_stock`:库存汇总表
- `gear_stock_log`:库存变动日志表
### 3.2 新增表 用于保存出入库单据头信息与状态控制。
- `gear_stock_io_order`:出入库单据主表 核心字段说明:
- `gear_stock_io_order_detail`:出入库单据明细表
### 3.3 关系说明
- `gear_stock_io_order` 1 - N `gear_stock_io_order_detail`
- `gear_stock_io_order` 可关联一个实际 `gear_stock_io`,字段为 `source_io_id`
- 业务层在单据审核通过后,生成实际 `gear_stock_io``gear_stock_io_detail`
- `gear_stock_io_order` 作为流程控制层,`gear_stock_io` 作为库存执行层
## 4. 表结构设计
## 4.1 出入库单据主表 `gear_stock_io_order`
### 核心字段
- `order_id`:单据主键
- `order_code`:单据编号,唯一 - `order_code`:单据编号,唯一
- `io_type`出入库类型,建议使用 `I` 入库、`O` 出库、`T` 调拨 - `io_type`I 入库、O 出库、T 调拨
- `biz_type`:业务类型,如 `purchase``sale``return``transfer``other` - `biz_type`:业务类型,如 `purchase``sale``return``transfer``other`
- `source_type`:来源单据类型 - `source_type` / `source_no` / `source_order_id`:关联来源业务单
- `source_no`:来源单据编号 - `warehouse_id` / `from_warehouse_id` / `to_warehouse_id`:仓库维度信息
- `warehouse_id`:主仓库
- `from_warehouse_id`:调出仓库
- `to_warehouse_id`:调入仓库
- `status`:单据状态 - `status`:单据状态
- `exec_flag`:执行标志 - `exec_flag`是否已执行
- `source_io_id`关联的实际出入库单据ID - `reversal_flag` / `reversal_order_id`:冲销标记与原单关联
- `total_qty`:总数量 - `source_io_id`:对应实际执行的出入库单
### 状态建议 状态建议
- `0` 草稿 - `0` 草稿
- `1` 已提交 - `1` 已提交
- `2` 已审核 - `2` 已审核
- `3` 已执行 - `3` 已执行
- `4` 已作废 - `4` 已作废
- `5` 已冲销
### 说明 ### 3.2 单据明细表 `gear_stock_io_order_detail`
- 草稿阶段允许编辑 用于保存每一行出入库明细。
- 已提交后进入审核流程
- 已审核后仅允许执行,不建议再修改
- 已执行后视为完成,若需变更应走冲销或红字单据
- 已作废单据不可再执行
## 4.2 出入库单据明细表 `gear_stock_io_order_detail` 核心字段说明:
### 核心字段
- `detail_id`:明细主键
- `order_id`所属单据ID
- `line_no`:行号 - `line_no`:行号
- `item_type`:物料类型 - `item_type` / `item_id`:物料信息
- `item_id`物料ID - `item_name` / `spec_name`:快照字段
- `item_name`:物料名称快照 - `warehouse_id` / `from_warehouse_id`:入库/出库仓库
- `spec_name`:规格型号快照
- `warehouse_id`:入库仓库
- `from_warehouse_id`:出库仓库
- `quantity`:数量 - `quantity`:数量
- `unit`:单位 - `unit`:单位
- `batch_no`:批次号 - `batch_no`:批次号
- `unit_price`:单价快照 - `unit_price` / `amount`:金额快照
- `amount`:金额快照 - `source_detail_no`:来源明细号
- `source_detail_no`来源明细编号 - `reversal_detail_id`冲销明细关联
### 说明 ---
- 明细表保留快照字段,避免后续基础资料变更影响历史单据 ## 4. 错单、超量、回退怎么处理
- `line_no` 作为单据内行号,保证同单据明细顺序和唯一性
## 5. 业务流程建议 ### 4.1 未执行前发现错误
### 5.1 入库流程 如果单据还没执行,处理方式最简单:
1. 创建入库单据草稿 1. 撤回到草稿,修改明细
2. 录入明细并保存 2. 或直接作废单据,重新建正确单据
3. 提交单据
4. 审核单据
5. 审核通过后生成实际入库单 `gear_stock_io`
6. 生成实际入库明细 `gear_stock_io_detail`
7. 更新库存 `gear_stock`
8. 写入库存日志 `gear_stock_log`
9. 回写 `gear_stock_io_order.source_io_id` 与执行状态
### 5.2 出库流程 这种场景不涉及库存回退,因为库存还没变。
1. 创建出库单据草稿 ### 4.2 已执行后发现多入/多出/录错
2. 录入出库明细
3. 提交单据
4. 审核单据
5. 审核通过后生成实际出库单
6. 扣减库存并写库存日志
7. 标记单据已执行
### 5.3 调拨流程 如果单据已经执行,**不要直接改历史明细或库存汇总表**,而是使用冲销单处理。
1. 创建调拨单据 举例:
2. 录入调出仓库与调入仓库
3. 审核通过后生成实际调拨出入库记录
4. 先扣减调出仓库库存,再增加调入仓库库存
5. 回写单据执行结果
## 6. 幂等与异常控制建议 - 入库多了 10 件:创建一张出库冲销单,把这 10 件扣回去
- 出库多了 10 件:创建一张入库冲销单,把这 10 件补回来
- 仓库选错:先冲销原单,再按正确仓库重建新单
- 批次录错:按原批次反向扣回,再按正确批次重新入账
1. 单据编号 `order_code` 必须唯一 ### 4.3 回退的原则
2. 执行前检查 `exec_flag`,避免重复执行
3. 单据执行时采用事务,保证主表、明细、库存、日志一致性
4. 建议在业务层增加“审核通过后仅允许一次执行”的判断
5. 如果执行失败,单据状态不应直接置为已执行
6. 对已执行单据的修改建议通过红字冲销或新单据处理
## 7. 建议的后续开发接口 1. 原单据不删除
2. 原执行流水不删除
3. 通过反向单据把库存恢复到正确状态
4. 回退单与原单必须建立关联
5. 每次回退都写审计日志
- 单据新增 ### 4.4 推荐的冲销链路
- 单据编辑
- 单据提交
- 单据审核
- 单据执行
- 单据作废
- 单据详情查询
- 单据列表查询
- 按来源单号查询
## 8. 与现有表的兼容方式 - 原单执行成功
- 发现错误
- 创建冲销单 `reversal_flag=1`
- 冲销单引用原单 `reversal_order_id`
- 冲销单审核并执行
- 冲销单执行后,库存回到正确状态
- 原单状态标记为“已冲销”或“已回退完成”
本次设计不删除现有出入库表,而是在其上增加单据控制层,便于平滑迁移: ---
- 老接口仍可保留直接写 `gear_stock_io` ## 5. 后端实现步骤
- 新接口优先写 `gear_stock_io_order`
- 后端逐步切换为“单据驱动库存”模式
- 未来如需更严格的流程控制,可将 `gear_stock_io` 仅作为执行结果表使用
## 9. 结论 下面按后端开发落地来拆解。
建议采用“单据表 + 执行表”的双层设计: ### 5.1 新增数据库实体
- 单据表负责流程控制、审批、追溯 先新增两个实体:
- 执行表负责真实库存变更
这样可以兼顾当前系统兼容性和后续业务扩展能力。 - `StockIoOrder`
- `StockIoOrderDetail`
实体字段与 SQL 保持一致,尤其注意:
- 单据状态字段
- 执行标志字段
- 冲销字段
- 来源单据字段
- 审核/执行人字段
### 5.2 新增单据服务层
建议新增服务:
- `StockIoOrderService`
- `StockIoOrderDetailService`
其中主服务负责流程,明细服务负责增删改查。
推荐核心方法:
- `createOrder(...)`
- `updateOrder(...)`
- `submitOrder(orderId)`
- `auditOrder(orderId)`
- `executeOrder(orderId)`
- `cancelOrder(orderId, reason)`
- `reverseOrder(orderId, reason)`
- `queryOrderDetail(orderId)`
### 5.3 统一执行入口
把原来直接操作库存的逻辑改成一个统一入口,例如:
- `applyStockChangeByOrder(orderId)`
执行流程建议:
1. 查询单据主表和明细
2. 校验状态必须为“已审核”
3. 校验 `exec_flag=0`
4. 校验库存是否足够(出库类)
5. 组装实际 `gear_stock_io` 主表
6. 组装实际 `gear_stock_io_detail` 明细
7. 更新 `gear_stock`
8. 写入 `gear_stock_log`
9. 回写单据 `source_io_id``exec_flag``status`
### 5.4 冲销单生成逻辑
新增反向单据生成方法:
- `buildReversalOrder(originalOrderId, reason)`
处理规则:
1. 读取原单据和明细
2. 主单的 `io_type` 取反
- 原来是入库 -> 冲销单做出库
- 原来是出库 -> 冲销单做入库
- 原来是调拨 -> 冲销单按反方向调拨
3. 明细数量保留原数量,但执行时按反向逻辑处理
4. 记录 `reversal_order_id`
5. 记录 `reversal_reason`
6. 冲销单提交、审核、执行
### 5.5 状态机控制
建议在代码里明确状态流转校验:
- 草稿 -> 提交
- 已提交 -> 审核
- 已审核 -> 执行
- 已执行 -> 冲销完成 / 已冲销
- 草稿/已提交/已审核 -> 作废(未执行时)
禁止的操作:
- 已执行后直接修改明细
- 已执行后再次执行
- 作废单据再执行
- 冲销单再次冲销自己
### 5.6 事务控制
执行与冲销都必须包事务,至少包括:
- 单据状态更新
- 实际出入库单写入
- 库存更新
- 库存日志写入
要求:
- 任一步失败,整体回滚
- 不允许库存更新成功但单据状态失败
- 不允许单据状态成功但库存未更新
### 5.7 幂等控制
建议增加以下幂等控制:
- `order_code` 唯一
- `exec_flag` 防止重复执行
- `reversal_order_id` 唯一,防止同一原单重复冲销
- 关键执行方法中加锁或乐观校验,防并发重复提交
### 5.8 库存回退代码建议
不要写“直接减库存/加库存”的散落代码,而要统一封装:
- `increaseStock(...)`
- `decreaseStock(...)`
- `applyStockChangeByOrder(...)`
- `applyReversalByOrder(...)`
其中冲销逻辑只是在“执行入口”里改变方向,而不是单独写一套完全不同的库存更新代码。
### 5.9 日志与审计
每次执行或冲销必须记录:
- 操作人
- 操作时间
- 原单号
- 冲销单号
- 原因
- 影响仓库
- 影响物料
- 影响批次
---
## 6. 代码改造建议
### 6.1 原先直接改库存的代码
如果项目里有类似:
- 保存单据后立即改 `gear_stock`
- 前端点一次按钮就直接扣库存
- 业务代码里到处 `update stock set qty = qty +/- x`
都建议改成:
- 先保存单据
- 审核通过后统一执行
- 出错后走冲销单
### 6.2 建议改造顺序
1. 先加单据表和明细表
2. 再加实体和 Mapper
3. 再加单据 Service
4. 再把库存执行逻辑抽到统一方法
5. 再把原直接操作入口改成单据入口
6. 最后补冲销接口
### 6.3 推荐接口拆分
- `POST /stock-io-order`
- `PUT /stock-io-order`
- `POST /stock-io-order/submit/{id}`
- `POST /stock-io-order/audit/{id}`
- `POST /stock-io-order/execute/{id}`
- `POST /stock-io-order/cancel/{id}`
- `POST /stock-io-order/reverse/{id}`
---
## 7. 总结
这套方案的关键点是:
- 业务上:先单据后执行
- 技术上:统一库存执行入口
- 出错后:通过反向单据冲销,不直接修历史数据
- 落地上:先建表,再加服务,再改原逻辑,再补回退
如果你愿意我下一步可以继续直接帮你把“后端实现步骤”再细化成接口清单、Java Service 方法清单和状态流转图说明。