From 7c3b67bf53a31197c28f1e8b4ac5f0e9d2a8e138 Mon Sep 17 00:00:00 2001 From: yueli Date: Wed, 23 Sep 2026 09:10:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(outbound):=20=E5=87=BA=E5=BA=93=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E8=A1=A5=E8=AE=B0=E6=9D=A5=E6=BA=90=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E5=8D=95=20request=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit trans_outbound 原先只落了 applicant_id(申请人,phase8 加的),没有任何指回 outbound_approval 的外键。出库时 request_id 是强制必填、approval 对象也一直 在手上,但只把 applicant_id 复制过来就丢弃了 —— 于是从一条出库明细无法回答 「这是哪张申请单出的库」:单号 request_no、申请说明、明细快照 items_json 都在 申请单上,同一张单分几次扫码出库的明细也串不起来。 与 applicant_id 语义正交:applicant_id 是「人」(退回补发要挂回真正该拿东西 的人),request_id 是「那张单」(单据追溯用)。两者都由同一个 approval 带出。 存量行留 NULL 不回填 —— 与 phase8 同一个理由:历史出库与其来源审批单之间没有 任何可用关联,按单号/时间猜会重蹈「重名错绑」的覆辙。NULL 表示「产生于本列 上线之前」。 DDL 见 db_migrations/phase11_trans_outbound_request_link.sql —— 幂等 (ADD COLUMN IF NOT EXISTS),可重复执行,文件尾部自带核对 SELECT 与回滚段。 执行:docker exec -i inventory_db psql -U test -d inventory_system < 该文件 --- .../phase11_trans_outbound_request_link.sql | 72 +++++++++++++++++++ inventory-backend/app/models/outbound.py | 10 +++ 2 files changed, 82 insertions(+) create mode 100644 db_migrations/phase11_trans_outbound_request_link.sql diff --git a/db_migrations/phase11_trans_outbound_request_link.sql b/db_migrations/phase11_trans_outbound_request_link.sql new file mode 100644 index 0000000..e8170ef --- /dev/null +++ b/db_migrations/phase11_trans_outbound_request_link.sql @@ -0,0 +1,72 @@ +-- ============================================================================= +-- 出库明细补记「来源申请单」,让出库流水能反查回它属于哪张单 +-- +-- 问题 +-- trans_outbound 只有 applicant_id(申请人,见 phase8),没有任何指回 +-- outbound_approval 的外键。出库时 request_id 是**强制必填**的,approval +-- 对象在服务层也一直在手上,但只把 applicant_id 复制过来就丢弃了 —— 于是 +-- 从一条出库明细无法回答「这是哪张申请单出的库」: +-- · 申请单号 request_no 查不到 +-- · 申请说明 / 明细快照 items_json 查不到 +-- · 同一张申请单分几次扫码出库时,这几条明细也串不起来 +-- +-- 本次改动 +-- trans_outbound 新增 request_id,创建出库时从关联审批单带出。 +-- 创建出库时 approval 恒非 None(request_id 已强制必填),故新单据必然有值。 +-- +-- --------------------------------------------------------------------------- +-- ★ 为什么是 request_id(主键)而不是 request_no(单号) +-- 与 phase8 同一个理由:主键没有歧义,单号是业务字段、将来格式可能变。 +-- 要展示单号时 join 一次 outbound_approval 即可。 +-- +-- ★ 为什么不回填存量行 +-- 与 phase8 完全一样:存量出库明细与其来源审批单之间**没有任何可用的关联** +-- (创建时只把审批单状态置为 3,没落任何外键),无从回填。刻意留 NULL 而不是 +-- 按单号/时间猜 —— NULL 表示「这张单产生于本列上线之前」。 +-- +-- ★ 与 applicant_id 的关系 +-- 两者都由 approval 带出,但语义正交:applicant_id 是「人」,request_id 是 +-- 「那张单」。退回补发用前者(挂回真正该拿东西的人),单据追溯用后者。 +-- +-- 幂等:带 IF NOT EXISTS,可重复执行。不含 psql 元命令,DataGrip 可直接执行。 +-- ============================================================================= + +BEGIN; + +ALTER TABLE trans_outbound + ADD COLUMN IF NOT EXISTS request_id integer; + +COMMENT ON COLUMN trans_outbound.request_id IS + '来源出库申请单ID(outbound_approval.id),创建出库时从关联审批单带出。该列上线前的历史行为 NULL(无从回填)'; + +-- 支撑「某张申请单出过哪些明细」的反查,以及按单聚合对账 +CREATE INDEX IF NOT EXISTS ix_trans_outbound_request + ON trans_outbound (request_id); + +COMMIT; + + +-- ============================================================================= +-- 执行后核对 +-- ============================================================================= +SELECT '=== 1) 新列已就位 ===' AS "核对项"; +SELECT column_name, data_type FROM information_schema.columns + WHERE table_name = 'trans_outbound' AND column_name = 'request_id'; + +SELECT '=== 2) 索引已就位 ===' AS "核对项"; +SELECT indexname FROM pg_indexes + WHERE tablename = 'trans_outbound' AND indexname = 'ix_trans_outbound_request'; + +SELECT '=== 3) 存量行应全部为 NULL(历史无从回填)===' AS "核对项"; +SELECT count(*) AS 出库明细总数, + count(request_id) AS 已记录来源单 + FROM trans_outbound; + + +-- ============================================================================= +-- 回滚段 +-- ============================================================================= +-- BEGIN; +-- DROP INDEX IF EXISTS ix_trans_outbound_request; +-- ALTER TABLE trans_outbound DROP COLUMN IF EXISTS request_id; +-- COMMIT; diff --git a/inventory-backend/app/models/outbound.py b/inventory-backend/app/models/outbound.py index f06e591..e9f67bb 100644 --- a/inventory-backend/app/models/outbound.py +++ b/inventory-backend/app/models/outbound.py @@ -148,6 +148,15 @@ class TransOutbound(db.Model): # ⚠ 该列上线前的历史行为 NULL —— 存量出库与其来源审批单之间没有任何可用 # 关联,无从回填;退回时由库管在选择器里明确指定。 applicant_id = db.Column(db.Integer, index=True) + # ★ 来源申请单ID:创建出库时从**关联审批单**带出(出库时 request_id 已强制 + # 必填,approval 恒非 None)。与上面的 applicant_id 语义正交 —— + # applicant_id 是「人」,本列是「那张单」。 + # 为什么要它:原先只落了 applicant_id,从一条出库明细反查不到它属于哪张 + # 申请单(单号 request_no、申请说明、明细快照 items_json 都在申请单上)。 + # ⚠ 该列上线前的历史行为 NULL —— 存量出库与来源审批单之间没有任何可用关联, + # 无从回填。 + # DDL 见 db_migrations/phase11_trans_outbound_request_link.sql + request_id = db.Column(db.Integer, index=True) signature_path = db.Column(db.Text) # 电子签名图片路径 outbound_time = db.Column(db.DateTime, default=beijing_time) operator_name = db.Column(db.String(100)) # 操作员 @@ -179,6 +188,7 @@ class TransOutbound(db.Model): 'returnable_quantity': qty - returned, 'unit_price': float(self.unit_price) if self.unit_price else 0, 'consumer_name': self.consumer_name, + 'request_id': self.request_id, 'signature_path': self.signature_path, 'outbound_time': self.outbound_time.strftime('%Y-%m-%d %H:%M:%S') if self.outbound_time else None, 'operator_name': self.operator_name,