-- ============================================================================ -- 载体判定与人工块量化(只读量化 + 回填草案) -- ---------------------------------------------------------------------------- -- 状态:草案(DRAFT)。**未接入** apps/backend/src/db/migrations。 -- 当前已使用迁移至 106;接入时按当前最大迁移号顺延,并先跑 `npm run check:sql`。 -- 基线:main @ 7542d547;数据来源 = 本地只读克隆(生产快照,orders 316,597,数据截至 2026-10-14 14:52) -- 金额单位:分(BIGINT)。orders.total_amount / work_orders.reward_amount / source_order_amount_fen 均为分。 -- 口径基准:§1 所有查询以「已支付订单项」为单位;人工块 = 既无 fulfillment_tasks 也无 work_orders。 -- 用途:§1 是可重复执行的只读量化;§2–§4 是载体判定/回填草案,不作为现行迁移。 -- 设计依据:docs/成本中心与三块履约模型.md、docs/README.md 第 2 条口径。 -- ============================================================================ -- ============================================================================ -- §0 只读安全(对生产库执行 §1 时先设置;只跑 SELECT) -- ============================================================================ -- SET default_transaction_read_only = on; -- SET statement_timeout = '60s'; -- 所有查询里的“截止时间”统一由 CTE w 取 max(paid_at),避免写死日期导致窗口漂移。 -- ============================================================================ -- §1 量化查询(只读,可直接对生产/克隆执行) -- ============================================================================ -- Q1 载体分布:按来源统计「自动发货 / 工单 / 人工 / 双载体冲突」 -- 实测(快照,近 30 天):91 人工 38,451(34.2%,¥485.3 万);PDD 人工 2,066(55.6%,¥36.1 万); -- 抖音 人工 2/4;双载体冲突 = 0(三载体互斥在数据上成立)。 WITH w AS ( SELECT max(paid_at) AS hi FROM orders WHERE pay_status = 'paid' AND paid_at IS NOT NULL ), base AS ( SELECT oi.id AS item_id, o.provider, o.shop_id, o.total_amount, EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash') AS has_auto, EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id) AS has_task, EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) AS has_wo FROM order_items oi JOIN orders o ON o.id = oi.order_id, w WHERE o.pay_status = 'paid' AND o.paid_at >= w.hi - interval '30 days' ) SELECT provider, count(*) AS paid_items, count(*) FILTER (WHERE has_auto) AS auto_items, count(*) FILTER (WHERE has_wo) AS work_order_items, count(*) FILTER (WHERE NOT has_task AND NOT has_wo) AS manual_items, count(*) FILTER (WHERE has_task AND has_wo) AS conflict_items, round(sum(total_amount) FILTER (WHERE NOT has_task AND NOT has_wo) / 100.0) AS manual_yuan, round(100.0 * count(*) FILTER (WHERE NOT has_task AND NOT has_wo) / count(*), 1) AS manual_pct FROM base GROUP BY provider ORDER BY manual_items DESC; -- Q1b 非 affiliate_dash 的任务执行器(识别历史执行器,避免把“历史自动”误判成人工) -- 实测:affiliate_dash 30,902 项;kuaishou_ct_assisted 262 项(= KUAISHOU_CLOUD,已下线,见 executors/registry.ts) WITH w AS ( SELECT max(paid_at) AS hi FROM orders WHERE pay_status = 'paid' AND paid_at IS NOT NULL ), base AS ( SELECT oi.id AS item_id FROM order_items oi JOIN orders o ON o.id = oi.order_id, w WHERE o.pay_status = 'paid' AND o.paid_at >= w.hi - interval '30 days' ) SELECT ft.executor_key, count(DISTINCT b.item_id) AS items, count(*) AS tasks FROM base b JOIN fulfillment_tasks ft ON ft.order_item_id = b.item_id GROUP BY 1 ORDER BY 2 DESC; -- Q2 人工块:店铺集中度 + 金额分位(用于判断监管收益与人工成本量级) -- 实测:91 只有 8 个店铺,avg ¥126.22 / p50 ¥53 / p90 ¥288;≥¥100 有 13,229 项,≥¥500 有 2,708 项。 WITH w AS ( SELECT max(paid_at) AS hi FROM orders WHERE pay_status = 'paid' AND paid_at IS NOT NULL ), base AS ( SELECT o.provider, o.shop_id, o.shop_name, o.total_amount FROM order_items oi JOIN orders o ON o.id = oi.order_id, w WHERE o.pay_status = 'paid' AND o.paid_at >= w.hi - interval '30 days' AND NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id) AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) ) SELECT provider, count(DISTINCT shop_id) AS shops, count(*) AS items, round(avg(total_amount) / 100.0, 2) AS avg_yuan, round(percentile_disc(0.5) WITHIN GROUP (ORDER BY total_amount) / 100.0, 2) AS p50_yuan, round(percentile_disc(0.9) WITHIN GROUP (ORDER BY total_amount) / 100.0, 2) AS p90_yuan, count(*) FILTER (WHERE total_amount >= 10000) AS ge_100y, count(*) FILTER (WHERE total_amount >= 50000) AS ge_500y FROM base GROUP BY provider ORDER BY items DESC; -- Q3 人工块是否存在任何“完成信号” -- 实测:40,519 项全部 order_status='paid' AND pay_status='paid' AND claim_status='active' —— 零完成信号。 WITH w AS ( SELECT max(paid_at) AS hi FROM orders WHERE pay_status = 'paid' AND paid_at IS NOT NULL ) SELECT o.order_status, o.pay_status, o.claim_status, count(*) AS items, round(sum(o.total_amount) / 100.0) AS yuan FROM order_items oi JOIN orders o ON o.id = oi.order_id, w WHERE o.pay_status = 'paid' AND o.paid_at >= w.hi - interval '30 days' AND NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id) AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) GROUP BY 1, 2, 3 ORDER BY items DESC; -- Q4 月度趋势:确认人工块不是历史遗留 -- 实测:2026-08 人工 46,337 / 已支付 142,969;2026-09 39,148 / 103,010;2026-10(部分月)17,469 / 55,647。 SELECT to_char(o.paid_at, 'YYYY-MM') AS mon, count(*) AS paid_items, count(*) FILTER ( WHERE NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id) AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) ) AS manual_items FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE o.pay_status = 'paid' AND o.paid_at >= '2026-08-01' GROUP BY 1 ORDER BY 1; -- Q5 数量口径:quantity 不可用于“卖了多少件” -- 实测(全量已支付 316,585 项):quantity=1 的有 316,578 项;quantity>1 仅 7 项; -- 其中 104,710 项的名称里带「N个」;sum(quantity)=316,593,而按名称解析 sum=2,316,222 —— 相差 7.3 倍。 SELECT count(*) AS items, count(*) FILTER (WHERE quantity = 1) AS qty_eq_1, count(*) FILTER (WHERE quantity = 1 AND sku_name ~ '[0-9]+[ ]*个') AS qty1_name_has_num, count(*) FILTER (WHERE quantity > 1) AS qty_gt_1, sum(quantity) AS sum_quantity, sum(coalesce((regexp_match(sku_name, '([0-9]+)[ ]*个'))[1]::int, quantity)) AS sum_name_quantity FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE o.pay_status = 'paid'; -- Q6 SKU 多码一名:同一商品名对应多个 sku_code(必须先做映射再统计) -- 实测(全量已支付):苏尔南钻石100颗 47 个码 / 3,319 项;指挥官密钥1个 42 个码 / 26,748 项; -- 指挥官密钥5个 41 个码;指挥官密钥10个 37 个码;苏尔南钻石50颗 35 个码。 SELECT sku_name, count(DISTINCT sku_code) AS distinct_sku, count(*) AS items FROM order_items oi JOIN orders o ON o.id = oi.order_id WHERE o.pay_status = 'paid' GROUP BY 1 HAVING count(DISTINCT sku_code) > 5 ORDER BY 2 DESC LIMIT 20; -- ============================================================================ -- §2 判定规则(唯一口径,代码与回填必须一致) -- ---------------------------------------------------------------------------- -- 执行载体 carrier_type ∈ { affiliate_dash, work_order, legacy, manual } -- ※ 与文档三分类的差异:数据中存在已下线执行器(kuaishou_ct_assisted / kuaishou_feifei / -- kuaishou-industry)的存量任务,单列 legacy,不并入 affiliate_dash,也不并入 manual。 -- 近 30 天 legacy = 262 项。需业务确认其归属(建议只读展示、不计入自动发货口径)。 -- -- 判定优先级(严格顺序,命中即停): -- 1. 存在 fulfillment_tasks 且 executor_key IN ('affiliate_dash') -> affiliate_dash -- 2. 存在 fulfillment_tasks 且 executor_key IN ('kuaishou_ct_assisted', -- 'kuaishou_feifei', 'kuaishou-industry') -> legacy -- 3. 存在 work_orders(任意状态,含已取消) -> work_order -- 4. 以上都无 -> manual -- 命中 1/2 且 3 同时成立 = 冲突,写入异常队列,不静默取其一(当前实测 0 条)。 -- -- carrier_status ∈ { pending, active, completed, failed, cancelled } -- 终态判定:affiliate_dash 取 task_status IN ('redeemed','closed');work_order 取 status='accepted'; -- manual 只能由人工 case 完成时写入(当前无表,见 docs/成本中心与三块履约模型.md §6)。 -- 宽限期:paid_at 之后 N 小时(建议 24h)内允许 carrier_type 为空(判定未跑完); -- 超过宽限期仍无载体 -> 强制置 manual 并建 case,禁止长期为空。 -- -- 重要:本判定是**派生列**,不是事实来源。事实来源仍是 fulfillment_tasks / work_orders 存在性; -- 派生列只用于报表、监管与成本挂载,必须提供重算作业(见 §4)。 -- ============================================================================ -- ============================================================================ -- §3 回填草案(接入时拆成 100_order_item_carrier.sql;分三步,可回滚) -- ============================================================================ -- 3.1 加列(ADD COLUMN + 常量 DEFAULT 在 PG11+ 为 metadata-only,不重写 31 万行) ALTER TABLE order_items ADD COLUMN IF NOT EXISTS carrier_type TEXT, ADD COLUMN IF NOT EXISTS carrier_status TEXT, ADD COLUMN IF NOT EXISTS carrier_decided_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS carrier_reason TEXT NOT NULL DEFAULT ''; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_carrier_type') THEN ALTER TABLE order_items ADD CONSTRAINT chk_order_items_carrier_type CHECK (carrier_type IS NULL OR carrier_type IN ('affiliate_dash', 'work_order', 'legacy', 'manual')); END IF; IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_carrier_status') THEN ALTER TABLE order_items ADD CONSTRAINT chk_order_items_carrier_status CHECK (carrier_status IS NULL OR carrier_status IN ('pending', 'active', 'completed', 'failed', 'cancelled')); END IF; END $$; COMMENT ON COLUMN order_items.carrier_type IS '执行载体(派生): affiliate_dash(自动发货)/work_order(接单工单)/legacy(已下线执行器存量)/manual(人工客服)'; COMMENT ON COLUMN order_items.carrier_status IS '载体状态: pending/active/completed/failed/cancelled'; COMMENT ON COLUMN order_items.carrier_decided_at IS '载体判定时间(重算作业会更新)'; COMMENT ON COLUMN order_items.carrier_reason IS '载体判定原因或人工改派说明'; -- 3.2 回填(只填 NULL,可反复执行;建议按 10 万行分批,避免长事务) -- 注意:不要在 UPDATE 里直接 JOIN 多张一对多表,统一用 EXISTS,避免行放大。 UPDATE order_items oi SET carrier_type = CASE WHEN EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash') THEN 'affiliate_dash' WHEN EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id AND ft.executor_key IN ('kuaishou_ct_assisted', 'kuaishou_feifei', 'kuaishou-industry')) THEN 'legacy' WHEN EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) THEN 'work_order' ELSE 'manual' END, carrier_decided_at = now(), carrier_reason = 'backfill:2026-10-17' WHERE oi.carrier_type IS NULL AND oi.id IN ( SELECT oi2.id FROM order_items oi2 JOIN orders o ON o.id = oi2.order_id WHERE o.pay_status = 'paid' AND o.paid_at < now() - interval '24 hours' LIMIT 100000 ); -- 3.3 索引(CONCURRENTLY 不能放在事务里,需单独执行;生产执行前确认无长事务) -- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_order_items_carrier_type -- ON order_items (carrier_type); -- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_fulfillment_tasks_order_item_executor -- ON fulfillment_tasks (order_item_id, executor_key); -- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_work_orders_order_item -- ON work_orders (order_item_id); -- 3.4 回滚 -- ALTER TABLE order_items DROP CONSTRAINT IF EXISTS chk_order_items_carrier_type; -- ALTER TABLE order_items DROP CONSTRAINT IF EXISTS chk_order_items_carrier_status; -- ALTER TABLE order_items -- DROP COLUMN IF EXISTS carrier_type, -- DROP COLUMN IF EXISTS carrier_status, -- DROP COLUMN IF EXISTS carrier_decided_at, -- DROP COLUMN IF EXISTS carrier_reason; -- ============================================================================ -- §4 校验(每次重算后必须跑;差异不为 0 说明派生列与事实不一致) -- ============================================================================ -- Q7 守恒校验:三类 + legacy 必须等于已支付订单项总数 WITH w AS ( SELECT max(paid_at) AS hi FROM orders WHERE pay_status = 'paid' AND paid_at IS NOT NULL ), paid AS ( SELECT oi.id AS item_id FROM order_items oi JOIN orders o ON o.id = oi.order_id, w WHERE o.pay_status = 'paid' AND o.paid_at >= w.hi - interval '30 days' ) SELECT count(*) AS paid_items, count(*) FILTER (WHERE oi.carrier_type = 'affiliate_dash') AS auto_items, count(*) FILTER (WHERE oi.carrier_type = 'work_order') AS work_order_items, count(*) FILTER (WHERE oi.carrier_type = 'legacy') AS legacy_items, count(*) FILTER (WHERE oi.carrier_type = 'manual') AS manual_items, count(*) FILTER (WHERE oi.carrier_type IS NULL) AS still_null, count(*) FILTER (WHERE oi.carrier_type = 'manual' AND EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id) OR oi.carrier_type <> 'manual' AND NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id) AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) ) AS mismatched FROM paid p JOIN order_items oi ON oi.id = p.item_id; -- Q8 冲突与长期无载体(应进异常队列,不进报表) SELECT oi.id, oi.order_id, oi.carrier_type, EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id) AS has_task, EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) AS has_wo FROM order_items oi WHERE oi.carrier_type IN ('affiliate_dash', 'legacy') AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) LIMIT 100; -- ============================================================================ -- §5 人工成本区间(按单成本 × 人工块件数) -- ---------------------------------------------------------------------------- -- 人工块实测:40,519 项 / 30 天(91 38,451 + PDD 2,066 + 抖音 2),涉及金额 ¥5,214,708。 -- 单均人工成本 X 元时的月度人工成本: -- X=3 -> ¥121,557 X=5 -> ¥202,595 X=8 -> ¥324,152 X=15 -> ¥607,785 -- 说明:这是“人工履约投入”的量级估算,不含商品/货源成本;商品成本走 cost_versions。 -- 建议先按单固定成本落地,同时记录 started_at/completed_at 以备后续按工时核算。 -- ============================================================================