feat(finance): 人工成本单事务/幂等 + 配置自动匹配 + 缺成本监控
- 确认履约改为单事务:锁订单项 → 复查快照 → 扣积分/备用金 → 写成本快照; 快照失败整体回滚,不再出现「钱扣了、成本没记」 - manual_case 的 subject_id 统一为 order_item_id,新增订单项级唯一索引(迁移 111), 消除 manual_expenses.id / platform_credit_entries.id 跨表撞车 - 人工 SKU 匹配增加 sku_code / sku_name 兜底;保存配置立即回填最近 30 天, 定时任务按窗口 rematch,确认时再按 SKU 兜底匹配 - 新增 manual_cost_missing 异常队列:从首次人工配置起算,确认履约立即收口 - 补单元测试与真实 PostgreSQL 集成测试(并发、重复、强制快照失败回滚、双资金链路) - 校准财务文档并新增上线初始化/验收手册(历史人工不补录,只保证配置后的新单) 验证:backend typecheck/lint/format/check:sql/test 843(839 pass / 4 skip); frontend typecheck/lint/test 172;迁移 111 在 scratch 库全量执行 + 重跑幂等。
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
-- 111_manual_cost_integrity.sql
|
||||
-- 人工成本完整性:一个订单项只允许一条有效的人工成本快照。
|
||||
--
|
||||
-- 背景:
|
||||
-- 1. 旧实现把 cost_snapshots.subject_id 写成 manual_expenses.id 或
|
||||
-- platform_credit_entries.id(两张表各自 BIGSERIAL),与唯一索引
|
||||
-- (subject_type, subject_id, cost_item_id) 叠加后,两条链路会互相撞 id:
|
||||
-- 第二次合法确认被当成「已有人工成本快照」拒绝,而钱已经扣了。
|
||||
-- 2. 人工确认原先是「先扣款、后写快照」两个事务,快照失败会留下已扣款未记成本。
|
||||
-- 现在改为同一事务,并由本迁移提供数据库级兜底:
|
||||
-- 一个 order_item 只允许一条有效 (superseded_at IS NULL) 的 manual_case 快照。
|
||||
--
|
||||
-- 语义调整:
|
||||
-- * subject_type = 'manual_case' 的 subject_id 统一为 order_item_id;
|
||||
-- 资金来源(manual_expenses.id / platform_credit_entries.id)只写 metadata_json,
|
||||
-- 不再作为 subject_id。
|
||||
-- * 正数 subject_id 的历史行(旧实现写入的支出单/积分流水 id)修正为 order_item_id;
|
||||
-- 负数 subject_id 的历史孤儿快照按迁移 104 的约定保留,不参与新链路。
|
||||
-- * 重复正数行保留最新一条,其余标记 superseded,避免唯一索引创建失败。
|
||||
--
|
||||
-- 全部语句幂等,可安全重跑。
|
||||
|
||||
-- 1. 历史人工快照的 subject_id 归一为 order_item_id。
|
||||
UPDATE cost_snapshots
|
||||
SET subject_id = order_item_id
|
||||
WHERE subject_type = 'manual_case'
|
||||
AND order_item_id IS NOT NULL
|
||||
AND subject_id > 0
|
||||
AND subject_id <> order_item_id;
|
||||
|
||||
-- 2. 同一订单项存在重复有效人工快照时,只保留最新一条(历史脏数据兜底)。
|
||||
UPDATE cost_snapshots c
|
||||
SET superseded_at = NOW()
|
||||
WHERE c.subject_type = 'manual_case'
|
||||
AND c.superseded_at IS NULL
|
||||
AND c.subject_id > 0
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM cost_snapshots newer
|
||||
WHERE newer.subject_type = 'manual_case'
|
||||
AND newer.order_item_id = c.order_item_id
|
||||
AND newer.superseded_at IS NULL
|
||||
AND newer.id > c.id
|
||||
);
|
||||
|
||||
-- 3. 数据库级唯一:一个订单项只有一条有效人工成本快照。
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_snapshots_manual_order_item
|
||||
ON cost_snapshots (order_item_id)
|
||||
WHERE subject_type = 'manual_case' AND superseded_at IS NULL AND subject_id > 0;
|
||||
|
||||
-- 4. 语义约束:新写入的 manual_case 必须 subject_id = order_item_id。
|
||||
-- NOT VALID 只约束新写入,负数历史孤儿快照(迁移 104 约定)保持原样。
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_manual_subject'
|
||||
) THEN
|
||||
ALTER TABLE cost_snapshots
|
||||
ADD CONSTRAINT chk_cost_snapshots_manual_subject
|
||||
CHECK (subject_type <> 'manual_case' OR subject_id = order_item_id)
|
||||
NOT VALID;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 5. 人工成本监控 / 对账按订单项反查备用金支出。
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_expenses_order_item
|
||||
ON manual_expenses (order_item_id)
|
||||
WHERE order_item_id IS NOT NULL;
|
||||
|
||||
-- 6. 人工 SKU 匹配键索引:spec_json.skuNick -> outSkuSn -> sku_code -> sku_name。
|
||||
-- 旧索引只覆盖前两级,缺 SKU 键的人工单会走顺序扫描;这里替换为同一套兜底表达式。
|
||||
DROP INDEX IF EXISTS idx_order_items_manual_internal_sku;
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_manual_sku_key
|
||||
ON order_items (
|
||||
COALESCE(
|
||||
NULLIF(spec_json->>'skuNick', ''),
|
||||
NULLIF(spec_json->>'outSkuSn', ''),
|
||||
NULLIF(sku_code, ''),
|
||||
NULLIF(sku_name, '')
|
||||
)
|
||||
)
|
||||
WHERE fulfillment_mode = 'manual';
|
||||
|
||||
COMMENT ON INDEX uq_cost_snapshots_manual_order_item IS
|
||||
'人工成本完整性:一个订单项最多一条有效 manual_case 快照(并发重复确认的数据库兜底)';
|
||||
Reference in New Issue
Block a user