diff --git a/apps/backend/src/db/migrations/091_annotate_vestigial_work_order_task_id.sql b/apps/backend/src/db/migrations/091_annotate_vestigial_work_order_task_id.sql new file mode 100644 index 00000000..683ffe46 --- /dev/null +++ b/apps/backend/src/db/migrations/091_annotate_vestigial_work_order_task_id.sql @@ -0,0 +1,30 @@ +-- 091_annotate_vestigial_work_order_task_id.sql —— 标注残留字段 work_orders.task_id。 +-- +-- 该列自迁移 004 引入,设计意图是把接单工单关联到履约任务(fulfillment_tasks)。 +-- 但生产核查(2026-10-03,库 order_site @47.114.114.225)确认它从未被写入过, +-- 且结构上也无从关联: +-- +-- 1) 唯一写入点是 createWorkOrder 的 `input.taskId || null`,而两个调用方 +-- 一个是硬编码 `taskId: null`(sync-worker-orders-service),另一个根本不传; +-- updateWorkOrder 的 patch 白名单里也没有这一列; +-- git 全历史检索 `taskId:` 只出现过 null 一种取值。 +-- +-- 2) 即便强行写值也无从关联:工单关联 100,231 个订单、履约任务关联 87,603 个订单, +-- 交集为 0;按 order_item_id 比对 100,229 条同样 0 命中。而两者的时间范围 +-- (2026-07 ~ 10)与 orders.id 区间(1 ~ 280k)却都是重叠的 —— 说明两套系统 +-- 是按规则表分流的两条并行履约通道(命中 work_product_rules 建工单、 +-- 命中 fulfillment_profiles 建履约任务),一单只走一条,本就不存在对应关系。 +-- +-- 3) 依赖该列的三处 `LEFT JOIN fulfillment_tasks ft ON ft.id = wo.task_id` +-- (取消申请 / 售后案 / 工单反馈)永远匹配不上;其中用作店铺名兜底的那部分 +-- 已改为读 wo.material_json->'source'->>'shopName'(生产实测覆盖 2478 条 +-- 中 2476 条),死 JOIN 已删除。 +-- +-- 结论:保留列与索引(drop 不可逆,且类型定义与类型行多处引用),仅作标注, +-- 避免后续再被当成"数据没同步上来"重复排查。 + +COMMENT ON COLUMN work_orders.task_id IS + '【残留字段,勿用】设计上用于关联履约任务,但无任何代码写入,且工单与履约任务的订单集合零交集(order_id / order_item_id 比对均为 0)。需要店铺名等信息请读 material_json->''source'',不要 JOIN fulfillment_tasks。'; + +COMMENT ON INDEX idx_work_orders_task_unique IS + '【残留索引】服务于永不写入的 work_orders.task_id,当前 0 行;保留以免重建时结构漂移。'; diff --git a/apps/backend/src/repositories/worker-platform/after-sales-repo.ts b/apps/backend/src/repositories/worker-platform/after-sales-repo.ts index c103cbdc..cbb9b63c 100644 --- a/apps/backend/src/repositories/worker-platform/after-sales-repo.ts +++ b/apps/backend/src/repositories/worker-platform/after-sales-repo.ts @@ -23,8 +23,11 @@ const AFTER_SALES_CASE_SELECT = ` COALESCE( NULLIF(BTRIM(o.shop_name), ''), NULLIF(BTRIM(o.shop_id), ''), - NULLIF(BTRIM(ft.shop_name), ''), - NULLIF(BTRIM(ft.shop_id), ''), + -- 原先兜底用 fulfillment_tasks,但 wo.task_id 是残留字段(无代码写入、 + -- 两套系统订单集合零交集),那个 LEFT JOIN 永远匹配不上,兜底形同虚设。 + -- 改用工单自己的 material_json 快照(生产实测覆盖 2,478 条中的 2,476 条)。 + NULLIF(BTRIM(wo.material_json->'source'->>'shopName'), ''), + NULLIF(BTRIM(wo.material_json->'source'->>'shopId'), ''), '' ) AS shop_name, wu.username AS worker_username, @@ -33,7 +36,6 @@ const AFTER_SALES_CASE_SELECT = ` FROM worker_after_sales_cases ac INNER JOIN work_orders wo ON wo.id = ac.work_order_id LEFT JOIN orders o ON o.id = wo.order_id - LEFT JOIN fulfillment_tasks ft ON ft.id = wo.task_id INNER JOIN worker_users wu ON wu.id = ac.worker_id LEFT JOIN work_order_shares wos ON wos.id = ac.work_order_share_id ` diff --git a/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts index dd33e00d..6d29eaf8 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts @@ -36,6 +36,8 @@ export async function createWorkOrder(input: CreateWorkOrderInput): Promise'source'->>'shopName' 实测覆盖其中 2,476 条。 COALESCE( NULLIF(BTRIM(o.shop_name), ''), NULLIF(BTRIM(o.shop_id), ''), - NULLIF(BTRIM(ft.shop_name), ''), - NULLIF(BTRIM(ft.shop_id), ''), + NULLIF(BTRIM(wo.material_json->'source'->>'shopName'), ''), + NULLIF(BTRIM(wo.material_json->'source'->>'shopId'), ''), '' ) AS shop_name, wo.product_name, @@ -34,7 +39,6 @@ const WORKER_CANCEL_REQUEST_SELECT = ` FROM worker_order_cancel_requests cr INNER JOIN work_orders wo ON wo.id = cr.work_order_id LEFT JOIN orders o ON o.id = wo.order_id - LEFT JOIN fulfillment_tasks ft ON ft.id = wo.task_id INNER JOIN worker_users wu ON wu.id = cr.worker_id LEFT JOIN work_order_shares wos ON wos.id = cr.work_order_share_id ` diff --git a/apps/backend/src/repositories/worker-platform/worker-order-feedback-repo.ts b/apps/backend/src/repositories/worker-platform/worker-order-feedback-repo.ts index 46d6d1ff..f1686f67 100644 --- a/apps/backend/src/repositories/worker-platform/worker-order-feedback-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-order-feedback-repo.ts @@ -13,8 +13,11 @@ const WORKER_ORDER_FEEDBACK_SELECT = ` COALESCE( NULLIF(BTRIM(o.shop_name), ''), NULLIF(BTRIM(o.shop_id), ''), - NULLIF(BTRIM(ft.shop_name), ''), - NULLIF(BTRIM(ft.shop_id), ''), + -- 原先兜底用 fulfillment_tasks,但 wo.task_id 是残留字段(无代码写入、 + -- 两套系统订单集合零交集),那个 LEFT JOIN 永远匹配不上,兜底形同虚设。 + -- 改用工单自己的 material_json 快照(生产实测覆盖 2,478 条中的 2,476 条)。 + NULLIF(BTRIM(wo.material_json->'source'->>'shopName'), ''), + NULLIF(BTRIM(wo.material_json->'source'->>'shopId'), ''), '' ) AS shop_name, wu.username AS worker_username, @@ -23,7 +26,6 @@ const WORKER_ORDER_FEEDBACK_SELECT = ` FROM worker_order_feedbacks wf INNER JOIN work_orders wo ON wo.id = wf.work_order_id LEFT JOIN orders o ON o.id = wo.order_id - LEFT JOIN fulfillment_tasks ft ON ft.id = wo.task_id INNER JOIN worker_users wu ON wu.id = wf.worker_id LEFT JOIN work_order_shares wos ON wos.id = wf.work_order_share_id ` diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index c5ed1bf8..01015891 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -2620,6 +2620,52 @@ test('筛选统计必须有短缓存 + 并发合并,避免成本随工单量 } }) +test('残留字段 work_orders.task_id 不得再被当成关联键 JOIN(迁移 091)', async () => { + const { readFile } = await import('node:fs/promises') + + // 背景:task_id 无任何代码写入(git 全历史只出现过 null),且工单与履约任务的订单 + // 集合零交集(order_id / order_item_id 比对均为 0,2026-10-03 生产核查)。 + // 三处 LEFT JOIN fulfillment_tasks 永远匹配不上,店铺名兜底已改为读 material_json。 + for (const name of [ + 'worker-cancel-request-repo.ts', + 'after-sales-repo.ts', + 'worker-order-feedback-repo.ts', + ]) { + const source = await readFile( + new URL(`../../repositories/worker-platform/${name}`, import.meta.url), + 'utf8', + ) + // 只断言不存在 JOIN(注释里可以解释这段历史),并确认兜底已切到 material_json + assert.ok( + !/\bJOIN\s+fulfillment_tasks\b/i.test(source), + `${name} 不应再 JOIN fulfillment_tasks:wo.task_id 是永不写入的残留字段,该连接恒为空`, + ) + assert.ok( + source.includes("material_json->'source'->>'shopName'"), + `${name} 的店铺名兜底必须读工单自己的 material_json 快照`, + ) + } + + // 写入点必须保留说明,避免下一个人又把 task_id 当成"数据没同步上来" + const managementRepo = await readFile( + new URL('../../repositories/worker-platform/work-order-management-repo.ts', import.meta.url), + 'utf8', + ) + assert.ok( + managementRepo.includes('残留字段'), + 'createWorkOrder 的 task_id 绑定处必须保留残留字段说明', + ) + + const migration = await readFile( + new URL('../../db/migrations/091_annotate_vestigial_work_order_task_id.sql', import.meta.url), + 'utf8', + ) + assert.ok( + migration.includes('COMMENT ON COLUMN work_orders.task_id'), + '必须给 work_orders.task_id 留下数据库注释', + ) +}) + test('打手端「我的订单」概览必须有覆盖索引 + 短缓存(数据库累计耗时第一的热路径)', async () => { const { readFile } = await import('node:fs/promises') // 覆盖索引:不改 SQL,靠 index-only scan 让规划器放弃全表顺序扫