diff --git a/apps/backend/src/db/migrations/104_cost_center.sql b/apps/backend/src/db/migrations/104_cost_center.sql index 70d27f2f..a686ffed 100644 --- a/apps/backend/src/db/migrations/104_cost_center.sql +++ b/apps/backend/src/db/migrations/104_cost_center.sql @@ -246,9 +246,6 @@ CREATE INDEX IF NOT EXISTS idx_cost_exceptions_status_created ON cost_exceptions (status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_cost_exceptions_item ON cost_exceptions (order_item_id, status); --- 按履约方式筛异常时走半连接,这条索引让 order_items 侧只扫目标履约方式的行。 -CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_id - ON order_items (fulfillment_mode, id); DO $$ BEGIN diff --git a/apps/backend/src/db/migrations/107_order_item_fulfillment_mode.sql b/apps/backend/src/db/migrations/107_order_item_fulfillment_mode.sql index 2b630a18..2db73d2d 100644 --- a/apps/backend/src/db/migrations/107_order_item_fulfillment_mode.sql +++ b/apps/backend/src/db/migrations/107_order_item_fulfillment_mode.sql @@ -40,6 +40,10 @@ CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_pending ON order_items (order_id) WHERE fulfillment_mode IS NULL; +-- 按履约方式筛异常时走半连接,这条索引让 order_items 侧只扫目标履约方式的行。 +CREATE INDEX IF NOT EXISTS idx_order_items_fulfillment_id + ON order_items (fulfillment_mode, id); + COMMENT ON COLUMN order_items.fulfillment_mode IS '履约方式:work_order 接单平台 / affiliate_dash 自动发货 / legacy_platform 历史平台(已下线)/ manual 人工;唯一真源,报表与后台只读此字段'; COMMENT ON COLUMN order_items.fulfillment_decided_at IS diff --git a/apps/backend/src/db/migrations/108_drop_manual_expense_records.sql b/apps/backend/src/db/migrations/108_drop_manual_expense_records.sql index 94d04a82..8feaa16e 100644 --- a/apps/backend/src/db/migrations/108_drop_manual_expense_records.sql +++ b/apps/backend/src/db/migrations/108_drop_manual_expense_records.sql @@ -22,13 +22,18 @@ -- 关联备用金支出单(fund_expense_id 为空)的,保留原主体 id 不动,避免误删成本; -- 有 fund_expense_id 的改挂到备用金支出单。 -UPDATE cost_snapshots cs -SET subject_id = mer.fund_expense_id -FROM manual_expense_records mer -WHERE cs.subject_type = 'manual_case' - AND mer.fund_expense_id IS NOT NULL - AND cs.subject_id = mer.id - AND cs.subject_id <> mer.fund_expense_id; +DO $$ +BEGIN + IF to_regclass('public.manual_expense_records') IS NOT NULL THEN + UPDATE cost_snapshots cs + SET subject_id = mer.fund_expense_id + FROM manual_expense_records mer + WHERE cs.subject_type = 'manual_case' + AND mer.fund_expense_id IS NOT NULL + AND cs.subject_id = mer.id + AND cs.subject_id <> mer.fund_expense_id; + END IF; +END $$; -- 若同一备用金支出单已存在有效快照,保留先写的,把重复的作废(superseded_at)。 UPDATE cost_snapshots dup diff --git a/apps/backend/src/db/migrations/111_manual_service_zero_placeholder.sql b/apps/backend/src/db/migrations/111_manual_service_zero_placeholder.sql index b8720ab8..758053af 100644 --- a/apps/backend/src/db/migrations/111_manual_service_zero_placeholder.sql +++ b/apps/backend/src/db/migrations/111_manual_service_zero_placeholder.sql @@ -14,6 +14,8 @@ WHERE ci.id = v.cost_item_id AND v.status = 'active'; -- 若人工成本项还没有生效版本,补一条 0 元全局占位,保证快照能挂到版本上。 +-- 104 已写入一条 1970 年的 draft 占位;这里必须用新的 effective_from, +-- 否则会与 draft 行撞 uq_cost_versions_scope(唯一键不区分状态)。 INSERT INTO cost_versions ( cost_item_id, scope_type, provider, shop_id, canonical_product_id, work_product_rule_id, amount_fen, rate_bps, effective_from, source, @@ -21,7 +23,7 @@ INSERT INTO cost_versions ( ) SELECT ci.id, 'global', '', '', NULL, NULL, 0, 0, - '1970-01-01T00:00:00+08:00', 'manual', 'active', + NOW(), 'manual', 'active', '系统占位:人工成本按人工 SKU 配置的消耗积分结算', NOW(), NOW() FROM cost_items ci WHERE ci.code = 'sys_manual_service' diff --git a/apps/backend/src/services/cost-center/cost-snapshot-sync-service.ts b/apps/backend/src/services/cost-center/cost-snapshot-sync-service.ts index 6fc3452e..52a8aff5 100644 --- a/apps/backend/src/services/cost-center/cost-snapshot-sync-service.ts +++ b/apps/backend/src/services/cost-center/cost-snapshot-sync-service.ts @@ -536,13 +536,13 @@ async function syncCommittedWorkOrderRewardSnapshots( ORDER BY wo.order_item_id, wo.id DESC ) INSERT INTO cost_snapshots ( - subject_type, subject_id, order_item_id, work_order_id, + subject_type, subject_id, order_id, order_item_id, work_order_id, cost_item_id, cost_version_id, canonical_product_id, quantity, unit_amount_fen, total_amount_fen, snapshot_stage, source, metadata_json, cost_basis_at, resolved_at, created_at ) SELECT - 'order_item', l.order_item_id, oi.order_id, l.work_order_id, + 'order_item', l.order_item_id, oi.order_id, l.order_item_id, l.work_order_id, si.cost_item_id, COALESCE(rv.id, si.cost_version_id), oi.canonical_product_id, 1, l.reward_amount, l.reward_amount, 'estimated', 'resolved', diff --git a/apps/backend/src/services/cost-center/template-bootstrap-service.ts b/apps/backend/src/services/cost-center/template-bootstrap-service.ts index 309375c8..44cb9b68 100644 --- a/apps/backend/src/services/cost-center/template-bootstrap-service.ts +++ b/apps/backend/src/services/cost-center/template-bootstrap-service.ts @@ -419,12 +419,12 @@ export async function backfillAdminAffiliateDashSkus() { type WorkOrderSkuBackfillRow = { work_order_item_count: number - matched_count: number - updated_count: number from_date: string | null to_date: string | null } +const WORK_ORDER_BACKFILL_BATCH_SIZE = 20000 + /** * 回填接单平台订单项的标准商品。 * @@ -434,60 +434,78 @@ type WorkOrderSkuBackfillRow = { */ export async function backfillAdminWorkOrderProductSkus() { const now = nowIso() + // order_item_id 有唯一索引,这里可以直接 count,避免 DISTINCT ON 被 + // 三个子查询反复物化后撞 15 秒语句超时。 const result = await query( ` - WITH work_order_item AS ( - SELECT DISTINCT ON (wo.order_item_id) - wo.order_item_id AS id, - wo.product_rule_id - FROM work_orders wo - WHERE wo.order_item_id IS NOT NULL - ORDER BY wo.order_item_id, wo.id DESC - ), - target AS ( + SELECT + COUNT(*)::int AS work_order_item_count, + to_char( + MIN((COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date), + 'YYYY-MM-DD' + ) AS from_date, + to_char( + MAX((COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date), + 'YYYY-MM-DD' + ) AS to_date + FROM work_orders wo + JOIN order_items oi ON oi.id = wo.order_item_id + JOIN orders o ON o.id = oi.order_id + WHERE wo.order_item_id IS NOT NULL + `, + ) + + // 单条 UPDATE 覆盖十几万行会撞 15 秒语句超时;按订单项 id 分批, + // 每批只选 2 万条工单证据,再按 id 数组更新。 + let lastRuleItemId = 0 + let ruleMatchedCount = 0 + let ruleUpdatedCount = 0 + for (let batch = 0; batch < 40; batch += 1) { + const targetRows = await query<{ id: number; canonical_product_id: number }>( + ` + WITH work_order_item AS ( + SELECT DISTINCT ON (wo.order_item_id) + wo.order_item_id AS id, + wo.product_rule_id + FROM work_orders wo + WHERE wo.order_item_id IS NOT NULL + AND wo.order_item_id > $1 + ORDER BY wo.order_item_id, wo.id DESC + LIMIT $2 + ) SELECT woi.id, p.id AS canonical_product_id FROM work_order_item woi JOIN work_product_rules wpr ON wpr.id = woi.product_rule_id JOIN product_catalog p ON p.canonical_sku = 'tpl:' || wpr.rule_key - ), - updated AS ( + ORDER BY woi.id + `, + [lastRuleItemId, WORK_ORDER_BACKFILL_BATCH_SIZE], + ) + if (targetRows.rows.length === 0) break + const lastSelectedId = Number(targetRows.rows[targetRows.rows.length - 1]?.id || 0) + if (lastSelectedId <= lastRuleItemId) break + lastRuleItemId = lastSelectedId + ruleMatchedCount += targetRows.rows.length + + const updated = await query( + ` UPDATE order_items oi - SET canonical_product_id = target.canonical_product_id, + SET canonical_product_id = input.canonical_product_id, updated_at = $1 - FROM target - WHERE oi.id = target.id - AND oi.canonical_product_id IS DISTINCT FROM target.canonical_product_id - RETURNING oi.id - ) - SELECT - ( - SELECT COUNT(DISTINCT wo.order_item_id)::int - FROM work_orders wo - WHERE wo.order_item_id IS NOT NULL - ) AS work_order_item_count, - (SELECT COUNT(*)::int FROM target) AS matched_count, - (SELECT COUNT(*)::int FROM updated) AS updated_count, - ( - SELECT to_char( - MIN((COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date), - 'YYYY-MM-DD' - ) - FROM work_order_item - JOIN order_items oi ON oi.id = work_order_item.id - JOIN orders o ON o.id = oi.order_id - ) AS from_date, - ( - SELECT to_char( - MAX((COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date), - 'YYYY-MM-DD' - ) - FROM work_order_item - JOIN order_items oi ON oi.id = work_order_item.id - JOIN orders o ON o.id = oi.order_id - ) AS to_date - `, - [now], - ) + FROM unnest($2::bigint[], $3::bigint[]) + AS input(id, canonical_product_id) + WHERE oi.id = input.id + AND oi.canonical_product_id IS DISTINCT FROM input.canonical_product_id + `, + [ + now, + targetRows.rows.map((row) => Number(row.id)), + targetRows.rows.map((row) => Number(row.canonical_product_id)), + ], + ) + ruleUpdatedCount += updated.rowCount || 0 + if (targetRows.rows.length < WORK_ORDER_BACKFILL_BATCH_SIZE) break + } const tailProducts = await query<{ inserted: boolean }>( ` @@ -579,16 +597,21 @@ export async function backfillAdminWorkOrderProductSkus() { [now], ) - const tailUpdated = await query<{ id: number }>( - ` - WITH tail AS ( - SELECT oi.id, oi.normalized_sku_name - FROM order_items oi - WHERE oi.canonical_product_id IS NULL - AND btrim(COALESCE(oi.normalized_sku_name, '')) <> '' - AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) - ), - resolved AS ( + let lastTailItemId = 0 + let tailUpdatedCount = 0 + for (let batch = 0; batch < 40; batch += 1) { + const tailRows = await query<{ id: number; canonical_product_id: number }>( + ` + WITH tail AS ( + SELECT oi.id, oi.normalized_sku_name + FROM order_items oi + WHERE oi.canonical_product_id IS NULL + AND btrim(COALESCE(oi.normalized_sku_name, '')) <> '' + AND oi.id > $1 + AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) + ORDER BY oi.id + LIMIT $2 + ) SELECT t.id, p.id AS canonical_product_id FROM tail t JOIN LATERAL ( @@ -598,25 +621,39 @@ export async function backfillAdminWorkOrderProductSkus() { ORDER BY (canonical_sku LIKE 'tpl:%') DESC, id LIMIT 1 ) p ON TRUE - ) - UPDATE order_items oi - SET canonical_product_id = r.canonical_product_id, - updated_at = $1 - FROM resolved r - WHERE oi.id = r.id - AND oi.canonical_product_id IS NULL - RETURNING oi.id - `, - [now], - ) + ORDER BY t.id + `, + [lastTailItemId, WORK_ORDER_BACKFILL_BATCH_SIZE], + ) + if (tailRows.rows.length === 0) break + const lastSelectedId = Number(tailRows.rows[tailRows.rows.length - 1]?.id || 0) + if (lastSelectedId <= lastTailItemId) break + lastTailItemId = lastSelectedId + + const updated = await query( + ` + UPDATE order_items oi + SET canonical_product_id = input.canonical_product_id, + updated_at = $1 + FROM unnest($2::bigint[], $3::bigint[]) + AS input(id, canonical_product_id) + WHERE oi.id = input.id + AND oi.canonical_product_id IS NULL + `, + [ + now, + tailRows.rows.map((row) => Number(row.id)), + tailRows.rows.map((row) => Number(row.canonical_product_id)), + ], + ) + tailUpdatedCount += updated.rowCount || 0 + if (tailRows.rows.length < WORK_ORDER_BACKFILL_BATCH_SIZE) break + } const row = result.rows[0] const workOrderItemCount = Number(row?.work_order_item_count || 0) - const ruleMatchedCount = Number(row?.matched_count || 0) - const ruleUpdatedCount = Number(row?.updated_count || 0) const tailProductCount = tailProducts.rows.filter((item) => item.inserted).length const tailMappingCount = tailMappings.rows.length - const tailUpdatedCount = tailUpdated.rows.length const updatedCount = ruleUpdatedCount + tailUpdatedCount const remainingResult = await query<{ total: number }>( ` diff --git a/docs/README.md b/docs/README.md index f55b8815..7165c71e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,7 @@ # order_site 文档索引 -> 最后校准:2026-10-23,代码基线 `dev @ 43f402cf + 迁移 103–109`(已合并到 `dev`,**未推送、未部署生产**)。 +> 最后校准:2026-10-24,代码基线 `dev @ 790de676 + 本地未提交修复`(**未推送、未部署生产**)。 +> 数据基线:生产 dump `backups/postgres/order_site-20261022-092601.dump`(数据 2026-07-10 ~ 2026-10-22);本地已按真实升级路径从生产迁移 101 升到 112,并完成商品、成本快照、销售事实初始化。 > 本文是 `docs/` 的唯一入口。**财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;数据模型看 [成本中心与履约模型.md](成本中心与履约模型.md),接口看 [成本中心API契约.md](成本中心API契约.md)。 > 新增文档必须在下表登记状态;过期文档不再直接改写结论,统一移入 `docs/archive/`。 @@ -77,16 +78,16 @@ | --- | --- | --- | | 客服备用金台账 | **已实施 P0** | 迁移 `100/101/106`;账户、流水、支出、对账;编号 `FA-<用户名>`;客服只能使用本人账户 | | 后台用户中文显示名 | **已实施** | `admin_users.display_name`,备用金姓名触发器同步;审计、登录记录、任务处理人优先显示中文名 | -| 商品主数据 + 渠道 SKU 映射 | **已实施 P0** | 745 个标准商品、4,426 条映射;只覆盖自动发货 38 个 + 接单平台 707 个。人工 11,349 项虽已归一但全部挂 `tpl:*` 模板商品,需在 P1 复核;人工专用商品尚未建立 | -| 成本中心 | **已实施 P0** | 迁移 `104`;`cost_items.cost_model` 取代 `cost_type + carrier_type`;38 个商品级版本 + 662 个规则级版本;人工走 `sys_manual_service` 实际快照 | -| 销售事实 / 订单毛利 | **已实施 P0** | 迁移 `105`;94 天事实收入 ¥2,650.87 万、成本 ¥950.27 万;`sales_daily_facts.fulfillment_mode` | +| 商品主数据 + 渠道 SKU 映射 | **已实施 P0/P1** | 2,017 个标准商品、4,897 条映射:`tpl:` 710、`man:` 1,195、`aff:` 49、`wo:` 63;订单项已按履约证据/人工内部 SKU 归一 | +| 成本中心 | **已实施 P0** | 迁移 `104`;710 个规则级版本 + 2 个全局占位版本;工单成本快照 151,939 条,其中 actual 129,430、estimated 22,509 | +| 销售事实 / 订单毛利 | **已实施 P0** | 迁移 `105`;375,447 项、收入 ¥3,143.61 万、成本 ¥572.95 万、毛利 ¥2,570.66 万;`sales_daily_facts.fulfillment_mode` | | 人工成本录入(确认履约) | **已实施 P0(2026-10-24 入口收敛到订单页)** | 人工履约页已下线;确认即扣积分/备用金并写 `actual` 快照;无登记单/审批/报销 | -| 履约方式统一 | **已实施 P0** | 迁移 `107`;316,585 项已回填,四项计数与总纲 §二 一致;订单页、成本快照、销售事实只读 `fulfillment_mode` | -| 人工商品归一 | **未实施(P1)** | 人工单按 `spec.skuId` 建 `man:`;历史 11,349 项 `tpl:*` 商品待复核 | +| 履约方式统一 | **已实施 P0** | 迁移 `107`;375,447 项全部回填:接单平台 153,148、人工 115,194、历史平台 53,901、自动发货 53,204 | +| 人工商品归一 | **已实施 P1(本次快照恢复执行)** | 人工 115,194 项已归一为 1,195 个 `man:<内部SKU>` 商品;历史人工支出仍不补录 | | 影子总账 | 未实施 | 现有财务页仍是 `UNION ALL` 业务事件拼装,不是复式总账 | | 退款冲减 / 平台对账 / 期间关账 | 未实施 | 仅方案 | -> 分支状态(2026-10-23):原 `103–114` 十二个迁移已合并为 `103–107` 五个,另加 `108` 清理迁移(删人工支出登记两表);已快进合并到 `dev`(tip `43f402cf`),本地库已按新 schema 改造并重跑回填,**未推送远端、未部署生产**;生产口径仍以 `main` 为准。 +> 分支状态(2026-10-24):工作区基于 `dev @ 790de676`,本地有迁移兼容性、批量回填和成本快照列映射修复,**未提交、未推送、未部署生产**。本地库已用 `order_site-20261022-092601.dump` 重建:生产 101 → dev 112、履约方式回填、商品/映射/成本/销售事实初始化全部完成;生产口径仍以 `main` 为准。 > 财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准。 ## 三、统一实施顺序 diff --git a/docs/成本中心与履约模型.md b/docs/成本中心与履约模型.md index 039efcc7..5451d056 100644 --- a/docs/成本中心与履约模型.md +++ b/docs/成本中心与履约模型.md @@ -1,9 +1,9 @@ # 成本中心与履约模型 > 状态:**现行**。商品主数据、成本中心、成本快照、销售事实、履约事实均已落地。 -> 最后校准:2026-10-23 -> 代码基线:`dev @ 43f402cf + 迁移 103–109`(已合并到 `dev`,**未推送、未部署生产**) -> 数据基线:本地生产快照克隆(数据截至 2026-10-14),已按新 schema 原地改造 +> 最后校准:2026-10-24 +> 代码基线:`dev @ 790de676 + 本地未提交修复`(**未推送、未部署生产**) +> 数据基线:生产 dump `order_site-20261022-092601.dump`(2026-07-10 ~ 2026-10-22);本地已升级到迁移 112,并完成商品、映射、成本快照与销售事实初始化 > **业务口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;本文只负责数据模型、迁移与运维。 > 接口字段见 [成本中心API契约.md](成本中心API契约.md)。 diff --git a/docs/财务与履约口径总纲-2026-10-23.md b/docs/财务与履约口径总纲-2026-10-23.md index e2afe582..58051045 100644 --- a/docs/财务与履约口径总纲-2026-10-23.md +++ b/docs/财务与履约口径总纲-2026-10-23.md @@ -1,9 +1,9 @@ # 财务与履约口径总纲(2026-10-23 重写版) > 状态:**现行唯一口径**。本文是财务、成本、履约分类的总入口;其他文档与本文件冲突时,以本文为准。 -> 最后校准:2026-10-23 -> 代码基线:`dev @ 43f402cf + 迁移 103–109`(已合并到 `dev`,**未推送、未部署生产**) -> 数据基线:本地生产快照克隆(数据截至 2026-10-14),已按新 schema 原地改造 +> 最后校准:2026-10-24 +> 代码基线:`dev @ 790de676 + 本地未提交修复`(**未推送、未部署生产**) +> 数据基线:生产 dump `order_site-20261022-092601.dump`(2026-07-10 ~ 2026-10-22);本地已升级到迁移 112,并完成履约方式、商品、成本快照与销售事实初始化 > 适用范围:订单接入、履约判定、商品归一、成本快照、销售事实、人工录单、备用金、后续总账与对账。 --- @@ -41,31 +41,23 @@ --- -## 二、当前真实履约构成(本地库,2026-07-10 ~ 2026-10-14) +## 二、当前真实履约构成(本地库,2026-07-10 ~ 2026-10-22) | 履约方式 | 订单项 | 收入 | 当前成本情况 | | --- | ---: | ---: | --- | -| 接单平台 `work_order` | 118,053 | ¥533.57 万 | 基本完整,缺 966 项 | -| 自动发货 `affiliate_dash` | 41,676 | ¥508.97 万 | 完整 | -| 历史平台 `legacy_platform` | 53,901 | ¥389.12 万 | 历史成本未采集,单列 | -| **人工 `manual`** | **102,955** | **¥1,219.20 万** | **仅 1 条 ¥320,历史基本未采集** | -| 合计 | 316,585 | ¥2,650.87 万 | — | +| 接单平台 `work_order` | 153,148 | ¥680.27 万 | 151,939 条快照,成本 ¥572.95 万;1,209 项缺成本(取消/0 报酬等) | +| 自动发货 `affiliate_dash` | 53,204 | ¥627.20 万 | 商品已归一;上游采购成本版本未配置,全部进入 `cost_missing` | +| 历史平台 `legacy_platform` | 53,901 | ¥389.12 万 | 已下线通道,历史成本未采集,商品与成本均保持缺口 | +| **人工 `manual`** | **115,194** | **¥1,447.01 万** | 商品已归一为 1,195 个 `man:*`;历史实际支出未采集,全部进入 `cost_missing` | +| 合计 | 375,447 | ¥3,143.61 万 | 成本 ¥572.95 万,毛利 ¥2,570.66 万 | -> **数字口径必须先分清"全量"和"未归一"。** 人工全量是 102,955 项;其中 11,349 项已有 `canonical_product_id`,但这 11,349 项全部指向 `tpl:*` 接单模板商品,只说明 SKU 命中过模板,**不代表它们有工单或走过接单平台**。真正未归一的是 91,606 项。历史平台 53,901 项全部未归一。两者相加才是报表里的"未归一 145,507 项"。 +平台分布:91kaquan 368,343 项 / ¥3,023.996 万;pdd 5,563 项 / ¥105.051 万;douyin_mashida 1,541 项 / ¥14.560 万(2026-10-12 ~ 2026-10-22)。 -人工 102,955 项的来源与核销码事实: +> **商品前缀只表示归一来源,不反推履约方式。** `tpl:` 710 个接单模板商品、`man:` 1,195 个人工内部 SKU、`aff:` 49 个自动发货 SKU、`wo:` 63 个工单长尾商品;渠道映射共 4,897 条(接单模板 4,748 + 工单长尾 149),自动发货与人工不写渠道映射。 -| 来源 | 订单项 | 收入 | 核销码事实 | -| --- | ---: | ---: | --- | -| 91/快手 | 100,618 | ¥1,177.45 万 | 全部有快手凭证记录:`CONSUMED` 87,310 / `DESTROYED` 13,247 / `UNUSED` 61 | -| 拼多多 | 2,335 | ¥41.74 万 | 无快手凭证 | -| 抖音 | 2 | ¥62.90 | 无快手凭证 | +成本缺口口径:`cost_missing` 共 223,508 项,其中人工 115,194、自动发货 53,204、历史平台 53,901、接单平台 1,209。人工与历史平台属于明确不补录的历史缺口;自动发货需配置上游采购成本版本;工单 1,209 项主要是取消/0 报酬,不属于商品未归一。 -这张表直接说明:**91 人工单全部有核销码,但仍然是人工。** 把"有核销码"当成 `voucher_only` 或自动发货,是订单页漏掉人工块的直接原因。 - -归一潜力:人工 102,955 项中 **100,529 项有 `spec_json.skuId`(97.6%)**,共 3,561 个 SKU;剩余 2,426 项主要来自拼多多,可用 `sku_code/sku_name` 建立映射。人工商品归一后,商品编码统一用 `man:`;不得因为复用了 `tpl:*` 商品就把履约方式改成 `work_order`。 - -成本现状:人工 102,955 项里只有 1 条实际成本快照(¥320),其余 102,954 项没有成本;历史平台 53,901 项全部没有成本。这两块是当前毛利失真的主要原因,但历史人工不补录,只做样本校准。 +异常队列:`product_mapping_missing` 仅剩历史平台 53,901 条,属于已下线通道的预期缺口;工单、自动发货、人工的商品归一均为 0 缺失。 --- @@ -73,8 +65,8 @@ | 成本模型 | 适用履约方式 | 成本来源 | 现状 | | --- | --- | --- | --- | -| `affiliate_purchase` | 自动发货 | 上游 SKU 固定成本版本 | 38 个商品,全部有 active 版本 | -| `work_order_reward` | 接单平台 | 模板规则级版本 / 工单实际报酬 | 662 个规则版本,966 项异常 | +| `affiliate_purchase` | 自动发货 | 上游 SKU 固定成本版本 | 49 个商品,本次快照恢复未配置采购成本版本 | +| `work_order_reward` | 接单平台 | 模板规则级版本 / 工单实际报酬 | 710 个规则版本;1,209 项取消/0 报酬缺口 | | `manual_actual` | 人工 | 订单页人工录单的实际支出(直接扣备用金) | 已实现,历史未采集 | | `platform_fee` / `payment_fee` / `after_sales_loss` / `other` | 全部履约方式 | 通用成本项 | 未配置 |