feat(borrow): 转交流水表补 borrow_no 与 status(双向握手数据层)

背景
----
1) **单内撕裂**:原 /transfer 收的是**明细行 ID**,只更新那一行。一张单有
   2 条明细时,转交后一条归接收人、另一条仍是原借用人 —— 前端按 borrow_no
   聚合便同时显示两个名字(实测 BOR-20260917-0001:108→测试 / 109→杜邢宸)。
2) **单向强塞**:原实现发起即生效,接收人在毫不知情的情况下背上资产责任。

本次改动
----
· 新增 borrow_no —— 单据身份。一次转交要覆盖该单**多行**,单行 ID 表达不了
  覆盖范围;accept 时据此批量更新。borrow_id 保留为「发起时的代表明细」供追溯。
· 新增 status —— PENDING / ACCEPTED / REJECTED 状态机。
· 存量 1 行按旧语义(发起即生效)标记为 ACCEPTED 并回填 borrow_no:
  它事实上已经生效,若标 PENDING,接收人会收到一条早已生效的待办。
· 建 borrow_no / status / (to_user_id,status) 索引,后者支撑「待我接收」查询。

同单号同一时刻只允许一条 PENDING(应用层强制),避免两个接收人争抢同一批实物。
This commit is contained in:
yueli
2026-09-17 10:04:05 +08:00
parent 0eca1805b6
commit b271ca3a49
2 changed files with 113 additions and 1 deletions

View File

@ -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;

View File

@ -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,