清理履约假草稿商品残留关联

This commit is contained in:
yml
2026-11-15 00:06:09 +08:00
parent 36f52a7f68
commit e77f96ee3c
3 changed files with 102 additions and 7 deletions
@@ -0,0 +1,68 @@
-- 167_fulfillment_invalid_draft_product_cleanup.sql
--
-- 迁移 166 已停止生成并归档接单平台的 tpl:<内部SKU> 假草稿商品。
-- 待判定履约记录仍可能保留这些商品的派生关联,需要在重试前清空,
-- 避免页面继续展示旧商品或后续成本归集到错误商品。
WITH invalid_products AS (
SELECT product.id
FROM product_catalog product
WHERE product.source_kind = 'work_order'
AND product.canonical_sku LIKE 'tpl:%'
AND COALESCE(product.attributes_json ->> 'draft', 'false') = 'true'
AND NOT EXISTS (
SELECT 1
FROM work_product_rules rule
WHERE rule.canonical_product_id = product.id
)
)
UPDATE fulfillments fulfillment
SET canonical_product_id = NULL,
updated_at = NOW()
FROM invalid_products invalid
WHERE fulfillment.canonical_product_id = invalid.id;
CREATE OR REPLACE FUNCTION assert_fulfillment_product_not_invalid_draft()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.canonical_product_id IS NULL THEN
RETURN NEW;
END IF;
IF EXISTS (
SELECT 1
FROM product_catalog product
WHERE product.id = NEW.canonical_product_id
AND product.source_kind = 'work_order'
AND product.canonical_sku LIKE 'tpl:%'
AND COALESCE(product.attributes_json ->> 'draft', 'false') = 'true'
AND NOT EXISTS (
SELECT 1
FROM work_product_rules rule
WHERE rule.canonical_product_id = product.id
)
) THEN
RAISE EXCEPTION
'fulfillment % cannot reference invalid work-order draft product %',
NEW.order_item_id,
NEW.canonical_product_id
USING ERRCODE = '23503',
HINT = 'Attach the canonical product from the matched work_product_rule instead.';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_fulfillments_invalid_draft_product
ON fulfillments;
CREATE TRIGGER trg_fulfillments_invalid_draft_product
BEFORE INSERT OR UPDATE OF canonical_product_id
ON fulfillments
FOR EACH ROW
EXECUTE FUNCTION assert_fulfillment_product_not_invalid_draft();
COMMENT ON FUNCTION assert_fulfillment_product_not_invalid_draft() IS
'履约商品不能指向接单平台历史伪造的 tpl 草稿商品;必须使用真实接单模板商品';
@@ -281,6 +281,30 @@ test(
finalFulfillments.rows.every((row) => row.issue_code === '' && row.pending_reason === null),
)
const invalidDraftProduct = await query<{ id: number }>(
`INSERT INTO product_catalog (
canonical_sku, canonical_name, category, unit_name, source_kind, status,
attributes_json, created_at, updated_at
) VALUES (
$1, '历史假草稿商品', '接单测试', '件', 'work_order', 'archived',
'{"draft":true}'::jsonb, NOW(), NOW()
) RETURNING id`,
[`tpl:invalid-${suffix}`],
)
const invalidDraftProductId = Number(invalidDraftProduct.rows[0]!.id)
productIds.push(invalidDraftProductId)
await assert.rejects(
() =>
query(
`UPDATE fulfillments
SET canonical_product_id = $2,
updated_at = NOW()
WHERE order_item_id = $1`,
[orderItemIds[0], invalidDraftProductId],
),
/invalid work-order draft product/,
)
await assert.rejects(
() =>
query(
@@ -3,16 +3,16 @@ type: 权威架构与实施计划
description: 订单、内部 SKU、履约路由、履约执行、商品、成本与财务的唯一权威口径及重构计划。
tags: [订单, 内部SKU, 履约, 商品, 成本, 财务, 迁移]
status: authoritative
version: 2.22
updated_at: 2026-11-14
version: 2.23
updated_at: 2026-11-15
---
# 统一订单、履约与财务模型
> 状态:**唯一权威文档**。
> 最后校准:2026-11-14。
> 生产基线:迁移 `165`。
> 开发基线:迁移 `166`(仅本地,尚未部署生产)。
> 最后校准:2026-11-15。
> 生产基线:迁移 `166`。
> 开发基线:迁移 `167`(仅本地,尚未部署生产)。
>
> 从本文件生效起,订单、内部 SKU、履约渠道、履约载体、标准商品、成本和财务相关实现,
> 只能以本文件为业务口径。其他旧分析只作为历史资料,不再具备设计或验收效力。
@@ -409,6 +409,8 @@ manual fulfillment
- 统一渠道配置不得为接单平台创建 `tpl:<内部 SKU>` 假草稿商品。真实成本商品来自命中的模板,
工单创建后写入 `fulfillments.canonical_product_id`。
- 错误 key、历史草稿商品和错误履约商品关联由迁移 `166` 清理;历史订单、模板和成本事实不删除。
- 迁移 `167` 清空仍指向历史假草稿商品的 `fulfillments.canonical_product_id`,并拒绝后续写入
未被真实接单模板引用的 `tpl:` 草稿商品;订单、工单、成本和资金事实不变。
### 3.5 商品与财务域
@@ -1055,7 +1057,7 @@ docs/archive/
## 十三、当前下一步
截至 2026-11-14,生产基线为 `165`,集成分支开发基线为 `166`。迁移 `152` 保护订单来源快照,
截至 2026-11-15,生产基线为 `166`,集成分支开发基线为 `167`。迁移 `152` 保护订单来源快照,
迁移 `158–160` 收口寄卖登记人同步、寄卖平台与自动匹配设置;
迁移 `161` 一次建立人工渠道半自动任务、内置 Connector 默认值、账号级 Cookie JSON 凭证、
任务凭证引用和最终约束;迁移 `162` 建立履约待办四桶只读投影、补载体专用索引和新单优先扫描口径。
@@ -1067,12 +1069,13 @@ docs/archive/
工单在发布时物化最新价格,避免每次改价扫描数万行。
迁移 `166` 让接单平台路由支持动态模板匹配,清理历史伪造 key 和草稿商品,并拒绝对不存在模板
的非空 `workRuleKey` 写入。
迁移 `167` 清理待判定履约记录对历史假草稿商品的残留引用,并增加履约商品关联闸门。
迁移中保留的 `semi_auto.credential.export` 权限仅作兼容记录,
不再作为 CK 导出门禁。
1. 生产升级直接按 `deploy/` 脚本、实际迁移和停写流程执行,不再维护独立升级手册。
2. 升级前备份必须完成隔离恢复验证;恢复验证未通过时禁止迁移。
3. `165 → 166` 迁移后刷新统计并完成商品、履约、
3. `166 → 167` 迁移后刷新统计并完成商品、履约、
财务和大厅价格审计;无法确定的数据进入未路由或待分类,不做猜测。
4. 执行 `finance:rebuild-derived` 的 dry-run,核对 A 级数据守恒后,
再按窗口执行 `--execute --confirm=REBUILD_DERIVED`,并验证第二次重建幂等。