清理履约假草稿商品残留关联
This commit is contained in:
@@ -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(
|
||||
|
||||
+10
-7
@@ -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`,并验证第二次重建幂等。
|
||||
|
||||
Reference in New Issue
Block a user