Files
order_site/docs/drafts/2026-10-17-载体判定与人工块量化.sql
T

289 lines
17 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================================
-- 载体判定与人工块量化(只读量化 + 回填草案)
-- ----------------------------------------------------------------------------
-- 状态:草案(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 以备后续按工时核算。
-- ============================================================================