diff --git a/db_migrations/phase4c_borrow_transfer_handshake.sql b/db_migrations/phase4c_borrow_transfer_handshake.sql new file mode 100644 index 0000000..8d22bd7 --- /dev/null +++ b/db_migrations/phase4c_borrow_transfer_handshake.sql @@ -0,0 +1,93 @@ +-- ============================================================================= +-- 借库转交 · 双向握手 + 整单覆盖 +-- +-- 本次解决两个已发生的问题: +-- 1) **单内撕裂**:原 /transfer 收的是**明细行 ID**,只更新那一行。 +-- 一张单有 2 条明细时,转交后 108 归属接收人、109 仍是原借用人 —— +-- 前端按 borrow_no 聚合,两个名字同时出现(实测 BOR-20260917-0001)。 +-- 修复方向:转交必须按 **borrow_no** 覆盖该单全部未还明细。 +-- 2) **单向强塞**:原实现发起即生效,接收人「被通知式」地背上资产责任。 +-- 改为双向握手:发起只落 PENDING,接收人 accept 后才真正转移责任。 +-- +-- --------------------------------------------------------------------------- +-- 1) 为什么新增 borrow_no 而不是复用 borrow_id +-- borrow_id 是**明细行**主键,一条转交要覆盖该单多行,用单行 ID 无法表达 +-- 「本次转交覆盖了哪些行」。borrow_no 是单据身份,accept 时据此批量更新。 +-- borrow_id 保留为「发起时的代表明细」,供追溯与旧前端兼容。 +-- +-- 2) 为什么存量行标记为 ACCEPTED +-- 旧语义是「发起即生效」—— 写流水的同时就改了主表 current_holder。 +-- 本表现有 1 行(BOR-20260917-0001)已是生效状态,标为 ACCEPTED 才符合 +-- 事实;若标 PENDING,接收人会收到一条早已生效的待办。 +-- +-- 3) 状态机 +-- PENDING ──accept──> ACCEPTED (主表 current_holder 正式转移) +-- └────reject──> REJECTED (主表不动,责任仍在原持有人) +-- 同一单号同一时刻只允许一条 PENDING(应用层强制),避免两个接收人 +-- 争抢同一批实物。 +-- +-- 幂等:带 IF NOT EXISTS,可重复执行。 +-- 执行:docker exec -i inventory_db psql -U test -d inventory_system < 本文件 +-- ============================================================================= + +BEGIN; + +ALTER TABLE trans_borrow_transfer + ADD COLUMN IF NOT EXISTS borrow_no varchar(100), + ADD COLUMN IF NOT EXISTS status varchar(20) NOT NULL DEFAULT 'PENDING'; + +COMMENT ON COLUMN trans_borrow_transfer.borrow_no IS + '借用单号(单据身份)。一次转交覆盖该单号下全部未还明细,accept 时据此批量更新'; +COMMENT ON COLUMN trans_borrow_transfer.status IS + 'PENDING 待接收 / ACCEPTED 已接收(主表已转移)/ REJECTED 已拒绝(主表未动)'; +COMMENT ON COLUMN trans_borrow_transfer.borrow_id IS + '发起转交时的代表明细ID(trans_borrow.id),仅供追溯;覆盖范围以 borrow_no 为准'; + +-- 存量行:按旧语义(发起即生效)回填单号并标记为已接收 +UPDATE trans_borrow_transfer t + SET borrow_no = tb.borrow_no, + status = 'ACCEPTED' + FROM trans_borrow tb + WHERE t.borrow_id = tb.id + AND t.status = 'PENDING'; + +CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_no + ON trans_borrow_transfer (borrow_no); +CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_status + ON trans_borrow_transfer (status); +-- 支撑「待我接收」的可见性与列表查询 +CREATE INDEX IF NOT EXISTS ix_trans_borrow_transfer_to_user + ON trans_borrow_transfer (to_user_id, status); + +COMMIT; + + +-- ============================================================================= +-- 执行后核对 +-- ============================================================================= +\echo '--- 1) 新列已就位 ---' +SELECT column_name, data_type, column_default + FROM information_schema.columns + WHERE table_name = 'trans_borrow_transfer' + AND column_name IN ('borrow_no', 'status') + ORDER BY column_name; + +\echo '--- 2) 存量行状态(应为 ACCEPTED 且 borrow_no 已回填)---' +SELECT id, borrow_id, borrow_no, status, from_user_name, to_user_name + FROM trans_borrow_transfer ORDER BY id; + +\echo '--- 3) 不应存在无单号的流水(应为 0)---' +SELECT count(*) AS 缺单号 FROM trans_borrow_transfer WHERE borrow_no IS NULL; + + +-- ============================================================================= +-- 回滚段 +-- ============================================================================= +-- BEGIN; +-- DROP INDEX IF EXISTS ix_trans_borrow_transfer_no; +-- DROP INDEX IF EXISTS ix_trans_borrow_transfer_status; +-- DROP INDEX IF EXISTS ix_trans_borrow_transfer_to_user; +-- ALTER TABLE trans_borrow_transfer +-- DROP COLUMN IF EXISTS borrow_no, +-- DROP COLUMN IF EXISTS status; +-- COMMIT; diff --git a/inventory-backend/app/models/transaction.py b/inventory-backend/app/models/transaction.py index 666c537..fcf3028 100644 --- a/inventory-backend/app/models/transaction.py +++ b/inventory-backend/app/models/transaction.py @@ -143,15 +143,27 @@ class TransBorrow(db.Model): # ============================================================================= +# 转交状态机:发起只落 PENDING,接收人 accept 后才真正转移责任 +TRANSFER_STATUS_PENDING = 'PENDING' # 待接收(主表 current_holder 未动) +TRANSFER_STATUS_ACCEPTED = 'ACCEPTED' # 已接收(主表已转移) +TRANSFER_STATUS_REJECTED = 'REJECTED' # 已拒绝(主表不动,责任仍在原持有人) + + class TransBorrowTransfer(db.Model): - """借库转交流水:一行 = 一次转交,不做覆盖式更新。""" + """借库转交流水:一行 = 一次转交动作,不做覆盖式更新。""" __tablename__ = 'trans_borrow_transfer' id = db.Column(db.Integer, primary_key=True) # 关联主表(不建 FK:与 trans_return 一致,台账必须能独立存活) + # ★ 代表明细:一次转交要覆盖该单号下**多行**,单行 ID 表达不了覆盖范围, + # 故覆盖范围以 borrow_no 为准,此列仅供追溯。 borrow_id = db.Column(db.Integer, nullable=False, index=True) + # ★ 单据身份:accept 时据此批量更新该单全部未还明细的持有人 + borrow_no = db.Column(db.String(100), index=True) + # ★ 状态机:见文件顶部常量 + status = db.Column(db.String(20), nullable=False, default=TRANSFER_STATUS_PENDING, index=True) # 转出方(= 转交前的 current_holder) from_user_id = db.Column(db.Integer) @@ -172,6 +184,13 @@ class TransBorrowTransfer(db.Model): return { 'id': self.id, 'borrow_id': self.borrow_id, + 'borrow_no': self.borrow_no, + 'status': self.status, + 'status_text': { + TRANSFER_STATUS_PENDING: '待接收', + TRANSFER_STATUS_ACCEPTED: '已接收', + TRANSFER_STATUS_REJECTED: '已拒绝', + }.get(self.status, self.status), 'from_user_id': self.from_user_id, 'from_user_name': self.from_user_name, 'to_user_id': self.to_user_id,