From 2aa0ae83c24f00a6dc4ac622753280e274834f38 Mon Sep 17 00:00:00 2001 From: wangyu <823267011@qq.com> Date: Thu, 23 Apr 2026 10:22:08 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E7=8A=B6=E6=80=81=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/sql/mysql/item/gear_stock_io_order.sql | 38 +- .../mysql/item/gear_stock_io_order_design.md | 367 ++++++++++++------ 2 files changed, 272 insertions(+), 133 deletions(-) diff --git a/script/sql/mysql/item/gear_stock_io_order.sql b/script/sql/mysql/item/gear_stock_io_order.sql index f232be4..57b80c8 100644 --- a/script/sql/mysql/item/gear_stock_io_order.sql +++ b/script/sql/mysql/item/gear_stock_io_order.sql @@ -1,9 +1,10 @@ -- ================================ --- 出入库单据控制层(新增) +-- 出入库单据控制层 +-- 目标:通过“单据 -> 审核 -> 执行”控制库存变更,避免直接操作出入库流水 -- 说明: --- 1) 业务层先创建“出入库单”,再由单据生成实际的 gear_stock_io / gear_stock_io_detail --- 2) 单据状态控制提交、审核、作废与执行结果 --- 3) 通过 source_io_id 关联已生成的实际出入库记录,确保单据可追溯 +-- 1) 业务先创建出入库单据,再由单据驱动实际 gear_stock_io / gear_stock_io_detail +-- 2) 单据支持草稿、提交、审核、执行、作废、冲销 +-- 3) 通过关联字段保留来源单、回退单、执行单之间的链路 -- ================================ 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 '单据编号(唯一)', io_type char(1) NOT NULL COMMENT '出入库类型(I入库 O出库 T调拨)', 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_order_id bigint(20) DEFAULT NULL COMMENT '来源单据ID(业务单ID)', 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', - 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已执行)', + 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_time datetime COMMENT '审核时间', execute_by varchar(64) DEFAULT '' COMMENT '执行人', @@ -33,12 +41,15 @@ CREATE TABLE gear_stock_io_order ( update_time datetime COMMENT '更新时间', PRIMARY KEY (order_id), UNIQUE KEY uk_order_code (order_code), + UNIQUE KEY uk_reversal_order_id (reversal_order_id), KEY idx_status (status), KEY idx_exec_flag (exec_flag), KEY idx_biz_type (biz_type), KEY idx_source_no (source_no), + KEY idx_source_order_id (source_order_id), 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='出入库单据表'; 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 '单价快照', amount decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '金额快照', source_detail_no varchar(64) DEFAULT '' COMMENT '来源明细编号', + reversal_detail_id bigint(20) DEFAULT NULL COMMENT '对应冲销明细ID', remark varchar(500) DEFAULT NULL COMMENT '备注', del_flag char(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0存在 2删除)', 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_item_id (item_id), 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='出入库单据明细表'; --- 建议补充的业务索引说明: --- 1) 先保存 gear_stock_io_order/gear_stock_io_order_detail 为草稿 --- 2) 提交后进入审核 --- 3) 审核通过后生成实际 gear_stock_io / gear_stock_io_detail,并回写 source_io_id --- 4) 如果需要严格防重复执行,可在业务层通过 order_code + status / source_io_id 控制幂等 +-- 建议: +-- 1) 业务层只允许通过单据驱动库存变更,禁止直接改 gear_stock / gear_stock_log +-- 2) 审核通过后,统一调用执行入口生成实际 gear_stock_io / gear_stock_io_detail +-- 3) 已执行错误时,通过反向单据进行冲销,不直接修改历史流水 diff --git a/script/sql/mysql/item/gear_stock_io_order_design.md b/script/sql/mysql/item/gear_stock_io_order_design.md index e1a771a..57e7a2d 100644 --- a/script/sql/mysql/item/gear_stock_io_order_design.md +++ b/script/sql/mysql/item/gear_stock_io_order_design.md @@ -2,172 +2,299 @@ ## 1. 背景 -当前系统中的出入库逻辑是直接操作 `gear_stock_io` 与 `gear_stock_io_detail`,缺少“单据层”的控制,因此会出现以下问题: +当前系统中的出入库是直接操作 `gear_stock_io` 与 `gear_stock_io_detail`,这会带来两个核心问题: -- 业务流程不完整,无法体现草稿、提交、审核、执行等状态 -- 直接操作库存,缺少审批与追溯能力 -- 无法与采购、销售、退货、调拨等来源单据建立统一关联 -- 事务失败、重复执行、人工误操作时难以定位责任与恢复 +- 库存变更没有“单据审批链”,无法体现业务流程 +- 出入库做错后,只能人工修库存,缺少标准回退方案 -因此建议在现有出入库表之上新增一层“出入库单据表”,用于统一控制流程,再由单据驱动实际库存操作。 +因此建议增加一层“出入库单据”,由单据统一控制库存执行。 + +--- ## 2. 设计目标 -1. 先单据,后执行:所有库存变化必须先落单据 -2. 状态可追踪:支持草稿、已提交、已审核、已执行、已作废 -3. 数据可追溯:单据可关联来源业务单号与实际出入库记录 -4. 支持扩展:兼容入库、出库、调拨等场景 -5. 幂等可控:避免重复执行导致库存被重复增减 +1. 所有库存变动都先落单据 +2. 单据必须支持草稿、提交、审核、执行、作废、冲销 +3. 单据执行后自动生成实际出入库流水 +4. 做错时通过反向单据回退,不直接改历史流水 +5. 保留完整审计链路,方便追责和排查 -## 3. 现有表与新增表的关系 +--- -### 3.1 现有表 +## 3. 表结构设计 -- `gear_stock_io`:实际出入库主表 -- `gear_stock_io_detail`:实际出入库明细表 -- `gear_stock`:库存汇总表 -- `gear_stock_log`:库存变动日志表 +### 3.1 单据主表 `gear_stock_io_order` -### 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`:单据编号,唯一 -- `io_type`:出入库类型,建议使用 `I` 入库、`O` 出库、`T` 调拨 +- `io_type`:I 入库、O 出库、T 调拨 - `biz_type`:业务类型,如 `purchase`、`sale`、`return`、`transfer`、`other` -- `source_type`:来源单据类型 -- `source_no`:来源单据编号 -- `warehouse_id`:主仓库 -- `from_warehouse_id`:调出仓库 -- `to_warehouse_id`:调入仓库 +- `source_type` / `source_no` / `source_order_id`:关联来源业务单 +- `warehouse_id` / `from_warehouse_id` / `to_warehouse_id`:仓库维度信息 - `status`:单据状态 -- `exec_flag`:执行标志 -- `source_io_id`:关联的实际出入库单据ID -- `total_qty`:总数量 +- `exec_flag`:是否已执行 +- `reversal_flag` / `reversal_order_id`:冲销标记与原单关联 +- `source_io_id`:对应实际执行的出入库单 -### 状态建议 +状态建议: - `0` 草稿 - `1` 已提交 - `2` 已审核 - `3` 已执行 - `4` 已作废 +- `5` 已冲销 -### 说明 +### 3.2 单据明细表 `gear_stock_io_order_detail` -- 草稿阶段允许编辑 -- 已提交后进入审核流程 -- 已审核后仅允许执行,不建议再修改 -- 已执行后视为完成,若需变更应走冲销或红字单据 -- 已作废单据不可再执行 +用于保存每一行出入库明细。 -## 4.2 出入库单据明细表 `gear_stock_io_order_detail` +核心字段说明: -### 核心字段 - -- `detail_id`:明细主键 -- `order_id`:所属单据ID - `line_no`:行号 -- `item_type`:物料类型 -- `item_id`:物料ID -- `item_name`:物料名称快照 -- `spec_name`:规格型号快照 -- `warehouse_id`:入库仓库 -- `from_warehouse_id`:出库仓库 +- `item_type` / `item_id`:物料信息 +- `item_name` / `spec_name`:快照字段 +- `warehouse_id` / `from_warehouse_id`:入库/出库仓库 - `quantity`:数量 - `unit`:单位 - `batch_no`:批次号 -- `unit_price`:单价快照 -- `amount`:金额快照 -- `source_detail_no`:来源明细编号 +- `unit_price` / `amount`:金额快照 +- `source_detail_no`:来源明细号 +- `reversal_detail_id`:冲销明细关联 -### 说明 +--- -- 明细表保留快照字段,避免后续基础资料变更影响历史单据 -- `line_no` 作为单据内行号,保证同单据明细顺序和唯一性 +## 4. 错单、超量、回退怎么处理 -## 5. 业务流程建议 +### 4.1 未执行前发现错误 -### 5.1 入库流程 +如果单据还没执行,处理方式最简单: -1. 创建入库单据草稿 -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` 与执行状态 +1. 撤回到草稿,修改明细 +2. 或直接作废单据,重新建正确单据 -### 5.2 出库流程 +这种场景不涉及库存回退,因为库存还没变。 -1. 创建出库单据草稿 -2. 录入出库明细 -3. 提交单据 -4. 审核单据 -5. 审核通过后生成实际出库单 -6. 扣减库存并写库存日志 -7. 标记单据已执行 +### 4.2 已执行后发现多入/多出/录错 -### 5.3 调拨流程 +如果单据已经执行,**不要直接改历史明细或库存汇总表**,而是使用冲销单处理。 -1. 创建调拨单据 -2. 录入调出仓库与调入仓库 -3. 审核通过后生成实际调拨出入库记录 -4. 先扣减调出仓库库存,再增加调入仓库库存 -5. 回写单据执行结果 +举例: -## 6. 幂等与异常控制建议 +- 入库多了 10 件:创建一张出库冲销单,把这 10 件扣回去 +- 出库多了 10 件:创建一张入库冲销单,把这 10 件补回来 +- 仓库选错:先冲销原单,再按正确仓库重建新单 +- 批次录错:按原批次反向扣回,再按正确批次重新入账 -1. 单据编号 `order_code` 必须唯一 -2. 执行前检查 `exec_flag`,避免重复执行 -3. 单据执行时采用事务,保证主表、明细、库存、日志一致性 -4. 建议在业务层增加“审核通过后仅允许一次执行”的判断 -5. 如果执行失败,单据状态不应直接置为已执行 -6. 对已执行单据的修改建议通过红字冲销或新单据处理 +### 4.3 回退的原则 -## 7. 建议的后续开发接口 +1. 原单据不删除 +2. 原执行流水不删除 +3. 通过反向单据把库存恢复到正确状态 +4. 回退单与原单必须建立关联 +5. 每次回退都写审计日志 -- 单据新增 -- 单据编辑 -- 单据提交 -- 单据审核 -- 单据执行 -- 单据作废 -- 单据详情查询 -- 单据列表查询 -- 按来源单号查询 +### 4.4 推荐的冲销链路 -## 8. 与现有表的兼容方式 +- 原单执行成功 +- 发现错误 +- 创建冲销单 `reversal_flag=1` +- 冲销单引用原单 `reversal_order_id` +- 冲销单审核并执行 +- 冲销单执行后,库存回到正确状态 +- 原单状态标记为“已冲销”或“已回退完成” -本次设计不删除现有出入库表,而是在其上增加单据控制层,便于平滑迁移: +--- -- 老接口仍可保留直接写 `gear_stock_io` -- 新接口优先写 `gear_stock_io_order` -- 后端逐步切换为“单据驱动库存”模式 -- 未来如需更严格的流程控制,可将 `gear_stock_io` 仅作为执行结果表使用 +## 5. 后端实现步骤 -## 9. 结论 +下面按后端开发落地来拆解。 -建议采用“单据表 + 执行表”的双层设计: +### 5.1 新增数据库实体 -- 单据表负责流程控制、审批、追溯 -- 执行表负责真实库存变更 +先新增两个实体: -这样可以兼顾当前系统兼容性和后续业务扩展能力。 \ No newline at end of file +- `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 方法清单和状态流转图说明。 \ No newline at end of file