diff --git a/docs/README.md b/docs/README.md index fb46a632..6f22adf7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -66,6 +66,7 @@ | --- | --- | | [drafts/2026-10-17-载体判定与人工块量化.sql](drafts/2026-10-17-载体判定与人工块量化.sql) | 载体分布/人工块量化查询、`carrier_type` 判定与回填、守恒校验、人工成本区间 | | [drafts/2026-10-17-成本中心与销售事实-DDL草案.sql](drafts/2026-10-17-成本中心与销售事实-DDL草案.sql) | `product_catalog`、`channel_product_mappings`、`cost_items/versions/snapshots/exceptions`、`sales_daily_facts` 建表草案 | +| [drafts/2026-10-17-人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql) | `manual_fulfillment_cases`、`manual_expense_records`(付款项目/金额/订单号/付款人/凭证)、`manual_expense_events`;依赖成本中心草案,须在其后应用 | ## 三、厂商协议与外部资料 diff --git a/docs/drafts/2026-10-17-人工支出登记-DDL草案.sql b/docs/drafts/2026-10-17-人工支出登记-DDL草案.sql new file mode 100644 index 00000000..0a704058 --- /dev/null +++ b/docs/drafts/2026-10-17-人工支出登记-DDL草案.sql @@ -0,0 +1,255 @@ +-- ============================================================================ +-- 人工支出登记 + 轻量人工处理记录(建表草案) +-- ---------------------------------------------------------------------------- +-- 状态:草案(DRAFT)。**未接入** apps/backend/src/db/migrations。 +-- 依赖:本文引用 product_catalog / cost_items / cost_snapshots, +-- 必须**在** `2026-10-17-成本中心与销售事实-DDL草案.sql` **之后**应用。 +-- 接入时应拆成 `104_manual_cases.sql`、`105_manual_expenses.sql`,并先跑 `npm run check:sql`。 +-- 基线:main @ 801bfa0b;依据:2026-10-17 业务口径确认(见 docs/载体判定与成本中心落地-复核与草案-2026-10-17.md §九)。 +-- +-- 业务原话(定优先级): +-- 「人工块现在意义不大,主要是把流程搞清楚:哪个人处理了哪些东西,付了多少钱。 +-- 现在人工完全监管不到,很多地方需要人工付款,也不知道付款项目、金额、订单号啥的。」 +-- +-- 因此本草案的顺序是: +-- ① 人工支出可追溯(付款项目 / 金额 / 订单号 / 谁付的 / 凭证)——**核心诉求** +-- ② 处理人可见(谁在处理哪个订单项)——用轻量 case 承载 +-- ③ SLA / 超时升级 / 监管指标——**延后**,不进本轮最小实现 +-- ============================================================================ + + +-- ============================================================================ +-- §1 轻量人工处理记录(谁在处理哪些东西) +-- ---------------------------------------------------------------------------- +-- 与 docs/成本中心与三块履约模型.md §8.3 的差异:去掉 due_at/SLA 强约束与优先级体系, +-- 只保留“谁、什么时候、处理了哪个订单项、结果是什么”。SLA 字段等 ③ 阶段再加。 +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS manual_fulfillment_cases ( + id BIGSERIAL PRIMARY KEY, + case_no TEXT NOT NULL, + order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE, + order_item_id BIGINT REFERENCES order_items(id) ON DELETE CASCADE, + provider TEXT NOT NULL DEFAULT '', + shop_id TEXT NOT NULL DEFAULT '', + canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT, + quantity INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'pending_assign', + assignee_admin_user_id BIGINT, + assignee_name TEXT NOT NULL DEFAULT '', + assigned_at TIMESTAMPTZ, + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + outcome TEXT NOT NULL DEFAULT '', + delivery_reference TEXT NOT NULL DEFAULT '', + evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb, + note TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_cases_case_no ON manual_fulfillment_cases (case_no); +-- 一个订单项最多一个 case:与「一个订单项只有一个执行载体」保持一致 +CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_cases_order_item + ON manual_fulfillment_cases (order_item_id) WHERE order_item_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_manual_cases_status ON manual_fulfillment_cases (status, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_manual_cases_assignee ON manual_fulfillment_cases (assignee_admin_user_id, status); +CREATE INDEX IF NOT EXISTS idx_manual_cases_order ON manual_fulfillment_cases (order_id); + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_cases_status') THEN + ALTER TABLE manual_fulfillment_cases + ADD CONSTRAINT chk_manual_cases_status + CHECK (status IN ('pending_assign', 'assigned', 'processing', 'waiting_customer', + 'pending_review', 'completed', 'failed', 'cancelled')); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_cases_outcome') THEN + ALTER TABLE manual_fulfillment_cases + ADD CONSTRAINT chk_manual_cases_outcome + CHECK (outcome IN ('', 'delivered', 'refunded', 'failed', 'cancelled')); + END IF; + -- 完成必须有结果;避免“点完成但什么都没填” + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_cases_completed') THEN + ALTER TABLE manual_fulfillment_cases + ADD CONSTRAINT chk_manual_cases_completed + CHECK (status <> 'completed' OR (completed_at IS NOT NULL AND outcome <> '')); + END IF; +END $$; + +COMMENT ON TABLE manual_fulfillment_cases IS '轻量人工处理记录:谁在处理哪个订单项、结果如何(SLA 阶段延后)'; +COMMENT ON COLUMN manual_fulfillment_cases.evidence_json IS '交付凭证:截图对象 key、参考号等;完成后必填(由服务层校验)'; + + +-- ============================================================================ +-- §2 人工支出登记(核心诉求) +-- ---------------------------------------------------------------------------- +-- 必须能回答:这笔钱是什么项目、多少钱、关联哪个订单/订单项、谁付的、付给谁、什么时候、凭证在哪。 +-- 与成本中心的关系: +-- cost_item_id = “付款项目”,指向 cost_items(cost_type = 'manual_service' 或其它) +-- approved 后生成 cost_snapshots(subject_type = 'manual_case' 或 'order_item',source = 'manual_input') +-- 影子总账期:登记 approved 即产生一条业务事件,进复式分录(借:履约成本/费用,贷:其他应付/银行存款) +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS manual_expense_records ( + id BIGSERIAL PRIMARY KEY, + expense_no TEXT NOT NULL, + case_id BIGINT REFERENCES manual_fulfillment_cases(id) ON DELETE SET NULL, + order_id BIGINT REFERENCES orders(id) ON DELETE SET NULL, + order_item_id BIGINT REFERENCES order_items(id) ON DELETE SET NULL, + work_order_id BIGINT REFERENCES work_orders(id) ON DELETE SET NULL, + cost_item_id BIGINT REFERENCES cost_items(id) ON DELETE RESTRICT, + expense_category TEXT NOT NULL DEFAULT 'other', + amount_fen BIGINT NOT NULL, + currency TEXT NOT NULL DEFAULT 'CNY', + paid_by_admin_user_id BIGINT, + paid_by_name TEXT NOT NULL DEFAULT '', + payee_type TEXT NOT NULL DEFAULT 'other', + payee_name TEXT NOT NULL DEFAULT '', + payee_account TEXT NOT NULL DEFAULT '', + payment_method TEXT NOT NULL DEFAULT 'other', + paid_at TIMESTAMPTZ NOT NULL, + status TEXT NOT NULL DEFAULT 'submitted', + approved_by TEXT NOT NULL DEFAULT '', + approved_at TIMESTAMPTZ, + reject_reason TEXT NOT NULL DEFAULT '', + evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb, + cost_snapshot_id BIGINT REFERENCES cost_snapshots(id) ON DELETE RESTRICT, + -- 预留:退款/仲裁接入后用反向登记冲正,不修改已通过的原始记录 + reversal_of_id BIGINT REFERENCES manual_expense_records(id) ON DELETE RESTRICT, + note TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_expenses_expense_no ON manual_expense_records (expense_no); +CREATE INDEX IF NOT EXISTS idx_manual_expenses_order ON manual_expense_records (order_id); +CREATE INDEX IF NOT EXISTS idx_manual_expenses_item ON manual_expense_records (order_item_id); +CREATE INDEX IF NOT EXISTS idx_manual_expenses_case ON manual_expense_records (case_id); +CREATE INDEX IF NOT EXISTS idx_manual_expenses_paid_at ON manual_expense_records (paid_at DESC); +CREATE INDEX IF NOT EXISTS idx_manual_expenses_status ON manual_expense_records (status, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_manual_expenses_cost_item ON manual_expense_records (cost_item_id); +-- 付款人日结对账用 +CREATE INDEX IF NOT EXISTS idx_manual_expenses_payer ON manual_expense_records (paid_by_admin_user_id, paid_at DESC); + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_amount') THEN + ALTER TABLE manual_expense_records + ADD CONSTRAINT chk_manual_expenses_amount CHECK (amount_fen <> 0); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_status') THEN + ALTER TABLE manual_expense_records + ADD CONSTRAINT chk_manual_expenses_status + CHECK (status IN ('draft', 'submitted', 'approved', 'rejected', 'reimbursed', 'reversed')); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_payee_type') THEN + ALTER TABLE manual_expense_records + ADD CONSTRAINT chk_manual_expenses_payee_type + CHECK (payee_type IN ('worker', 'upstream', 'customer', 'platform', 'other')); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_method') THEN + ALTER TABLE manual_expense_records + ADD CONSTRAINT chk_manual_expenses_method + CHECK (payment_method IN ('wechat', 'alipay', 'bank', 'cash', 'platform_balance', 'other')); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_category') THEN + ALTER TABLE manual_expense_records + ADD CONSTRAINT chk_manual_expenses_category + CHECK (expense_category IN ('goods_purchase', 'compensation', 'refund_advance', 'service_fee', + 'freight', 'recharge', 'other')); + END IF; + -- 已通过/已报销必须有审批信息 + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_approved') THEN + ALTER TABLE manual_expense_records + ADD CONSTRAINT chk_manual_expenses_approved + CHECK (status NOT IN ('approved', 'reimbursed') OR (approved_at IS NOT NULL AND approved_by <> '')); + END IF; +END $$; + +COMMENT ON TABLE manual_expense_records IS '人工支出登记:付款项目/金额/订单号/付款人/收款人/凭证,解决人工付款不可追溯'; +COMMENT ON COLUMN manual_expense_records.cost_item_id IS '付款项目 -> cost_items;为空表示未归类,必须进异常队列而不是默认其他'; +COMMENT ON COLUMN manual_expense_records.expense_category IS '兜底分类:cost_item_id 未填时的粗分类,报表需同时暴露未归类占比'; +COMMENT ON COLUMN manual_expense_records.paid_by_admin_user_id IS '实际垫付/付款的后台用户;用于“谁付了多少钱”对账'; +COMMENT ON COLUMN manual_expense_records.evidence_json IS '凭证:支付截图对象 key、流水号、聊天记录 key 等;提交时必填(服务层校验)'; +COMMENT ON COLUMN manual_expense_records.reversal_of_id IS '预留:退款/仲裁接入后按反向登记冲正,禁止 UPDATE 已通过记录'; +COMMENT ON COLUMN manual_expense_records.status IS 'draft草稿/submitted待审/approved已通过/rejected已驳回/reimbursed已报销/reversed已冲正'; + +-- 状态变更审计(不可改) +CREATE TABLE IF NOT EXISTS manual_expense_events ( + id BIGSERIAL PRIMARY KEY, + expense_id BIGINT NOT NULL REFERENCES manual_expense_records(id) ON DELETE CASCADE, + event_type TEXT NOT NULL, + from_status TEXT NOT NULL DEFAULT '', + to_status TEXT NOT NULL DEFAULT '', + actor_type TEXT NOT NULL DEFAULT 'admin', + actor_id TEXT NOT NULL DEFAULT '', + payload_json JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_manual_expense_events_expense + ON manual_expense_events (expense_id, created_at); + +COMMENT ON TABLE manual_expense_events IS '人工支出状态变更审计:只追加,不更新、不删除'; + + +-- ============================================================================ +-- §3 校验与报表查询 +-- ============================================================================ + +-- Q1 「谁付了多少钱」:按付款人 × 项目 汇总(日结/周结用) +-- SELECT r.paid_by_name, coalesce(ci.name, r.expense_category) AS item, count(*) AS records, +-- round(sum(r.amount_fen) / 100.0, 2) AS yuan +-- FROM manual_expense_records r +-- LEFT JOIN cost_items ci ON ci.id = r.cost_item_id +-- WHERE r.status IN ('approved', 'reimbursed') AND r.paid_at >= now() - interval '7 days' +-- GROUP BY 1, 2 ORDER BY sum(r.amount_fen) DESC; + +-- Q2 未归类占比(必须持续为 0,否则报表口径不可信) +-- SELECT count(*) FILTER (WHERE cost_item_id IS NULL) AS unclassified, +-- count(*) AS total, +-- round(100.0 * count(*) FILTER (WHERE cost_item_id IS NULL) / nullif(count(*), 0), 2) AS unclassified_pct +-- FROM manual_expense_records WHERE status <> 'draft'; + +-- Q3 没有关联订单号的支出(你说的“也不知道订单号”,这里必须能点名) +-- SELECT count(*) FILTER (WHERE order_id IS NULL AND order_item_id IS NULL) AS no_order_link, +-- round(sum(amount_fen) FILTER (WHERE order_id IS NULL AND order_item_id IS NULL) / 100.0, 2) AS yuan +-- FROM manual_expense_records WHERE status IN ('approved', 'reimbursed'); + +-- Q4 订单损益:一个订单项的收入 - 各类成本(含人工支出) +-- SELECT oi.id AS order_item_id, o.provider, o.shop_id, +-- oi.revenue_amount_fen AS revenue_fen, +-- coalesce(sum(cs.total_amount_fen), 0) AS cost_fen, +-- coalesce(sum(r.amount_fen), 0) AS manual_expense_fen +-- FROM order_items oi +-- JOIN orders o ON o.id = oi.order_id +-- LEFT JOIN cost_snapshots cs ON cs.order_item_id = oi.id +-- LEFT JOIN manual_expense_records r ON r.order_item_id = oi.id AND r.status IN ('approved', 'reimbursed') +-- WHERE oi.id = $1 +-- GROUP BY 1, 2, 3, 4; + +-- Q5 与成本中心对账:approved 的支出必须都有 cost_snapshot(影子期允许延迟,转正期不允许缺) +-- SELECT count(*) AS approved_without_snapshot +-- FROM manual_expense_records +-- WHERE status IN ('approved', 'reimbursed') AND cost_snapshot_id IS NULL; + + +-- ============================================================================ +-- §4 与财务系统(影子总账)的对接约定 +-- ---------------------------------------------------------------------------- +-- 事件类型(幂等键): +-- manual_expense:{expense_no}:approved 人工支出登记通过 +-- manual_expense:{expense_no}:reimbursed 已报销(垫付 -> 公司支出) +-- manual_expense:{expense_no}:reversed 冲正(退款/仲裁接入后启用,本轮仅预留) +-- +-- 建议分录(approved 时,金额 A): +-- 借:5101 履约成本-人工(或按 cost_items 映射的科目) A +-- 贷:2005 其他应付-垫付人 A +-- reimbursed 时: +-- 借:2005 其他应付-垫付人 A +-- 贷:1001 银行存款 / 1002 支付通道待结算 A +-- +-- 退款/仲裁仍未接入:本轮只保留 reversal_of_id 与 status='reversed', +-- 不实现自动冲正逻辑;接入后按“反向登记 + 新分录”处理,禁止改动原记录。 +-- ============================================================================ diff --git a/docs/drafts/2026-10-17-成本中心与销售事实-DDL草案.sql b/docs/drafts/2026-10-17-成本中心与销售事实-DDL草案.sql index 830b11c7..784d3810 100644 --- a/docs/drafts/2026-10-17-成本中心与销售事实-DDL草案.sql +++ b/docs/drafts/2026-10-17-成本中心与销售事实-DDL草案.sql @@ -254,7 +254,15 @@ COMMENT ON TABLE cost_exceptions IS '成本缺失/冲突异常队列:报表必 -- 且 quantity 不可信(316,585 项里 316,578 项 = 1,名称带「N个」的有 104,710 项)。 -- 现状利好:目前每笔已支付订单恰好 1 个订单项(316,585 单 = 316,585 项), -- 所以 orders.total_amount 暂时可当订单项收入;但 schema 允许多项,属**潜在**缺口。 --- 收入口径实测差异:91 = maxAmount、PDD = order_amount、抖音 = pay_amount,不能直接相加。 +-- +-- 【口径已确认 2026-10-17】收入 = 用户付款金额,取 orders.total_amount: +-- · 91:total_amount 即用户付款(与快手发码配合)。数据核验:91 已支付 312,569 笔中 +-- 235,271 笔含快手 kuaishouSendCode.paymentFen,其中 235,268 笔与 total_amount **完全一致** +-- (100.00%),仅 3 笔差 ¥1.80~2.00(2026-09-05 / 09-18 / 10-04,可单独抽查)。 +-- · PDD:order_amount;抖音:pay_amount。三者同为“用户实付”,故统一 revenue_basis = 'user_payment'。 +-- · 打手报酬 = **支出/成本**(最终由打手提现、我方付款),因此按**总额法**: +-- 收入记用户实付全额,打手报酬作为履约成本,不做代收代付净额。 +-- · 退款/仲裁**暂未接入**:refunded_amount_fen / refunded_at 为**预留字段**,本轮不写逻辑。 -- ============================================================================ ALTER TABLE order_items @@ -270,7 +278,7 @@ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_revenue_basis') THEN ALTER TABLE order_items ADD CONSTRAINT chk_order_items_revenue_basis - CHECK (revenue_basis IN ('', 'ninety_one_max_amount', 'pdd_order_amount', 'douyin_pay_amount', 'manual')); + CHECK (revenue_basis IN ('', 'user_payment', 'manual_adjustment')); END IF; IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_revenue_amount') THEN ALTER TABLE order_items ADD CONSTRAINT chk_order_items_revenue_amount CHECK (revenue_amount_fen IS NULL OR revenue_amount_fen >= 0); @@ -280,8 +288,8 @@ END $$; CREATE INDEX IF NOT EXISTS idx_order_items_canonical_product ON order_items (canonical_product_id); COMMENT ON COLUMN order_items.normalized_quantity IS '归一化件数:从 SKU 名称解析并固化,禁止直接用 quantity 做销量'; -COMMENT ON COLUMN order_items.revenue_amount_fen IS '统一收入口径(分),必须同时写 revenue_basis 说明来源'; -COMMENT ON COLUMN order_items.refunded_at IS '退款时间:按日净销售额必需,当前 orders 只有 pay_status=refunded 与 updated_at'; +COMMENT ON COLUMN order_items.revenue_amount_fen IS '收入(分)= 用户实付,取 orders.total_amount;revenue_basis 通常为 user_payment'; +COMMENT ON COLUMN order_items.refunded_at IS '【预留】退款时间:退款/仲裁暂未接入,本轮不写逻辑;接入后按日净销售额 = 销售额 - 退款额'; -- 过渡:让工单规则逐步改用成本中心(有 cost_item_id 走成本中心,否则回退旧金额字段) ALTER TABLE work_product_rules @@ -292,6 +300,8 @@ CREATE INDEX IF NOT EXISTS idx_work_product_rules_cost_item ON work_product_rule COMMENT ON COLUMN work_product_rules.cost_item_id IS '迁移用过渡列:非空则金额取成本中心,空则回退 reward_amount/unit_price_fen'; COMMENT ON COLUMN work_product_rules.legacy_cost_source IS '回退来源标记:rule_amount=旧规则金额,便于统计未迁移规则数'; +-- 口径已确认(2026-10-17):打手报酬是**支出**(工单结算 -> 可提现 -> 提现打款), +-- 按总额法计入履约成本,与用户实付收入分开记,不做代收代付净额。 -- ============================================================================ @@ -375,9 +385,16 @@ COMMENT ON COLUMN sales_daily_facts.cost_missing_count IS '成本缺失件数: -- 4) §4 销售事实表 -> 店铺/日期/皮肤 的销量、收入、成本、毛利 -- 5) 之后才是影子总账(docs/财务系统集成方案.md) -- --- 待业务确认(阻塞项): --- a) 91 的 total_amount(maxAmount)到底是收入还是结算上限? --- b) 打手报酬是成本还是代收代付? --- c) 人工块按单成本取值,以及是否需要按工时核算? --- d) 退款/仲裁的冲减规则与归期口径。 +-- 【口径已确认 2026-10-17】 +-- a) 收入 = 用户付款金额(orders.total_amount),计收入;已用数据核验与快手 paymentFen 100% 对齐。 +-- b) 打手报酬 = 支出/成本(最终提现打款),按总额法。 +-- c) 人工块:第一优先级是**人工支出可追溯**(付款项目/金额/订单号/付款人/凭证), +-- 见 `2026-10-17-人工支出登记-DDL草案.sql`;按单还是按小时的取值口径仍待定,先按单登记。 +-- d) 退款/仲裁:**暂未接入,先预留**(字段 + reversal_of_id + 事件类型,不实现逻辑)。 +-- +-- 【仍待确认】 +-- 1) 收入确认时点(付款 / 发货 / 验收 / 售后期结束)——影响利润表与预收账款; +-- 2) 自动发货 cost_amount 是否稳定、是否含平台积分/汇率; +-- 3) 平台服务费/支付通道费由谁承担、结算周期与账单口径; +-- 4) 寄卖货款的采购成本确认时点。 -- ============================================================================ diff --git a/docs/成本中心与三块履约模型.md b/docs/成本中心与三块履约模型.md index e01068dc..078aeaba 100644 --- a/docs/成本中心与三块履约模型.md +++ b/docs/成本中心与三块履约模型.md @@ -332,16 +332,23 @@ orders + order_items + channel_product_mappings + cost_snapshots ## 八、人工客服块(当前最需要补的) +> **口径确认(2026-10-17)**:业务明确人工块当前**不以 SLA 监管为目标**,第一优先级是 +> **人工支出可追溯** ——「主要是把流程搞清楚:哪个人处理了哪些东西、付了多少钱。现在人工完全监管不到, +> 很多地方需要人工付款,也不知道付款项目、金额、订单号」。 +> 因此本章顺序调整为:**① 人工支出登记 → ② 处理人可见(轻量 case) → ③ SLA 与监管指标(延后)**。 +> DDL 见 [drafts/2026-10-17-人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql)。 + ### 8.1 问题本质 -不是“客服效率低”,而是**人工单没有进入系统状态机**: +不是“客服效率低”,而是**人工单没有进入系统状态机**,且**人工付款完全没有台账**: - 没有 case; - 没有处理人; - 没有开始/完成时间; - 没有交付证据; - 没有人工成本; -- 订单完成状态无法回写。 +- 订单完成状态无法回写; +- **付款项目、金额、关联订单号、付款人、凭证全部缺失** —— 这是当前最直接的损失点。 ### 8.2 载体判定 @@ -497,35 +504,36 @@ flowchart LR --- -## 十一、需要业务确认的问题 +## 十一、业务口径确认结果(2026-10-17) -1. **91 的 `orders.total_amount = maxAmount` 到底是“我方收入”还是“成本上限”?** -2. 收入确认时点:付款 / 发货 / 验收 / 售后期结束? -3. 打手报酬是成本,还是代收代付? -4. 自动发货的 `cost_amount` 是否稳定、是否含平台积分/汇率? -5. 人工成本按单还是按小时?客服是否在系统内记录工时? -6. 退款/仲裁按什么日期冲减收入? -7. 平台服务费/支付通道费由谁承担? -8. 高价值人工单的复核阈值是多少? +### 11.1 已确认 + +| # | 问题 | 结论 | 依据 | +| --- | --- | --- | --- | +| 1 | 91 的 `orders.total_amount` 是收入还是成本上限? | **收入**:`total_amount` 就是用户付款金额,与快手发码配合,计入收入 | 业务确认;数据核验:91 已支付 312,569 笔中 235,271 笔含快手 `paymentFen`,其中 235,268 笔与 `total_amount` **完全一致(100.00%)**,仅 3 笔差 ¥1.80~2.00 | +| 2 | 打手报酬是成本还是代收代付? | **支出/成本**:工单结算后打手会提现,我方实际付款给他们 → 按**总额法**(收入记用户实付全额,报酬记履约成本) | 业务确认 | +| 3 | 人工块的目标是什么? | **先做流程可见与支出可追溯**(谁处理了哪些、付了多少钱、什么项目、关联订单号、凭证),SLA/监管指标延后 | 业务确认 | +| 4 | 退款/仲裁怎么冲减? | **暂未接入,先预留**:字段、反向冲正关系与事件类型先留好,本轮不实现逻辑 | 业务确认 | + +### 11.2 仍待确认 + +1. 收入确认时点:付款 / 发货 / 验收 / 售后期结束?(金额口径已定 = 用户实付) +2. 自动发货的 `cost_amount` 是否稳定、是否含平台积分/汇率? +3. 人工支出按单还是按小时归集?(先按单登记,工时字段后续再加) +4. 平台服务费/支付通道费由谁承担、结算周期与账单口径? +5. 寄卖货款的采购成本确认时点? --- -## 十二、生产只读与克隆建议 +## 十二、分析基线与克隆现状(2026-10-17 更新) -本次已通过 SSH 连接生产库并执行只读聚合: +生产只读聚合(`SET default_transaction_read_only = on`)已完成,无写入、无改表、无改数据。 -```sql -SET default_transaction_read_only = on; -SET statement_timeout = '20s'; -``` +**现有基线**:本机已存在可反复使用的**生产快照克隆**(本地 docker `order_site-postgres-1`,`orders` 316,597 行,数据截至 2026-10-14), +因此**后续 SQL 迭代一律打本地克隆,不必再连生产**;需要更新时再取新快照。 -没有写入、没有改表、没有改数据。 - -建议暂时不做全量克隆: - -- 聚合数据已足够支撑第一轮设计; -- 全量库包含买家信息、原始报文、平台凭据,复制到本地有安全风险; -- 后续需要反复迭代 SQL 时,再做**选定表只读逻辑克隆**:`orders`、`order_items`、`work_orders`、`fulfillment_tasks`、`work_product_rules`、`worker_wallet_ledgers`、`worker_finance_requests`。 +本机备份目录已收敛:`backups/` 及子目录 `700`、两份 dump `600`,并删除了一个残留临时 dump(137 MB)。 +如需长期保留留档,建议只保留一份最新 dump,并确认磁盘加密。 --- diff --git a/docs/财务系统集成方案.md b/docs/财务系统集成方案.md index 17273dab..b254a7c9 100644 --- a/docs/财务系统集成方案.md +++ b/docs/财务系统集成方案.md @@ -274,7 +274,7 @@ CREATE TABLE finance_reconciliation_items ( | 事件 | 借方 | 贷方 | 备注 | | --- | --- | --- | --- | -| 订单支付 | 1002 支付通道待结算 / 1101 应收平台 | 2201 预收账款 | 收入确认时点需业务拍板 | +| 订单支付 | 1002 支付通道待结算 / 1101 应收平台 | 2201 预收账款 | 金额 = 用户实付(已确认);确认时点待拍板 | | 履约完成/验收 | 2201 预收账款 | 4001 商品销售收入 | 或按交付时点确认 | | 工单验收结算 | 5101 履约成本 | 2001 应付打手报酬 | 按份额逐笔 | | 打手报酬转可提现 | 2001 应付打手报酬 | 2004 打手可提现余额 | 与现有 `reward_settlement` 对齐 | @@ -288,8 +288,12 @@ CREATE TABLE finance_reconciliation_items ( | 寄卖验收货款 | 5001 商品采购成本 | 2002 应付卖家货款 | 货款金额取 `cost_price_fen || asking_price_fen` | | 支付卖家货款 | 2002 应付卖家货款 | 1001 银行存款 | 需结算单 | | 平台服务费 | 5401 平台服务费 | 2101 应付平台服务费 | 从平台账单导入 | +| 人工支出登记通过(垫付) | 5101 履约成本-人工(按 `cost_item_id` 映射科目) | 2005 其他应付-垫付人 | 付款项目/订单号/付款人必填;见 [人工支出登记草案](drafts/2026-10-17-人工支出登记-DDL草案.sql) | +| 人工垫付报销 | 2005 其他应付-垫付人 | 1001 银行存款 | 需凭证与审批留痕 | +| 退款冲正(预留) | 反向凭证 `reversal_of_id` 指向原凭证 | — | 退款/仲裁暂未接入,先预留不实现 | -> 具体收入确认时点(付款 / 发货 / 验收 / 售后期结束)必须由业务与财务共同拍板;这会直接影响利润表。 +> **已确认(2026-10-17)**:收入金额口径 = 用户实付;打手报酬 = 支出(总额法);退款/仲裁先预留;人工支出以登记表为入口。 +> **仍待拍板**:收入**确认时点**(付款 / 发货 / 验收 / 售后期结束),它直接影响利润表与预收账款。 --- @@ -448,16 +452,20 @@ POST /periods/:key/close ## 八、需要业务拍板的问题 -| # | 问题 | 影响 | -| --- | --- | --- | -| 1 | 收入确认时点:付款 / 发货 / 验收 / 售后期结束? | 决定利润表与预收账款 | -| 2 | 平台结算周期与账单口径(91、快手、PDD、抖音、affiliate-dash) | 决定对账与应收 | -| 3 | 打手报酬是“成本”还是“代收代付”? | 决定收入总额法/净额法 | -| 4 | 寄卖货款的采购成本确认时点 | 决定存货/成本 | -| 5 | 提现通道费、平台服务费由谁承担 | 决定费用科目与结算 | -| 6 | 退款、拒付、仲裁的冲减规则 | 决定售后与收入冲回 | -| 7 | 是否需要发票/税务 | 决定发票表与税额 | -| 8 | 是否多币种 | 当前 CNY,若否则需汇率与汇兑损益 | +> 2026-10-17 已确认 4 条口径,明细见 [成本中心与三块履约模型.md](成本中心与三块履约模型.md) §11。 + +| # | 问题 | 状态 | 影响 | +| --- | --- | --- | --- | +| 1 | 收入金额口径 | ✅ **已确认:收入 = 用户实付(`orders.total_amount`)**,与快手 `paymentFen` 数据核验 100% 一致 | 收入基数 | +| 2 | 收入确认时点:付款 / 发货 / 验收 / 售后期结束? | ⏳ 待定(金额口径已定) | 决定利润表与预收账款 | +| 3 | 平台结算周期与账单口径(91、快手、PDD、抖音、affiliate-dash) | ⏳ 待定 | 决定对账与应收 | +| 4 | 打手报酬是“成本”还是“代收代付”? | ✅ **已确认:支出/成本**(最终提现打款),按**总额法** | 收入总额法/净额法 | +| 5 | 寄卖货款的采购成本确认时点 | ⏳ 待定 | 决定存货/成本 | +| 6 | 提现通道费、平台服务费由谁承担 | ⏳ 待定 | 决定费用科目与结算 | +| 7 | 退款、拒付、仲裁的冲减规则 | ✅ **已确认:暂未接入,先预留**(字段 + `reversal_of_id` + 事件类型,不实现逻辑) | 决定售后与收入冲回 | +| 8 | 人工支出如何入账 | ✅ **已确认:以「人工支出登记」为入口**(付款项目/金额/订单号/付款人/凭证),SLA 监管延后 | 决定人工成本科目与总账事件 | +| 9 | 是否需要发票/税务 | ⏳ 待定 | 决定发票表与税额 | +| 10 | 是否多币种 | ⏳ 待定(当前 CNY) | 若否则需汇率与汇兑损益 | --- diff --git a/docs/载体判定与成本中心落地-复核与草案-2026-10-17.md b/docs/载体判定与成本中心落地-复核与草案-2026-10-17.md index 1ecd2462..22b604a2 100644 --- a/docs/载体判定与成本中心落地-复核与草案-2026-10-17.md +++ b/docs/载体判定与成本中心落地-复核与草案-2026-10-17.md @@ -129,11 +129,12 @@ Top 店铺(人工块):向日葵游戏服务店 14,903 项 / ¥183.9 万; | 文件 | 内容 | | --- | --- | | [载体判定与人工块量化.sql](drafts/2026-10-17-载体判定与人工块量化.sql) | §1 八组只读量化查询(含实测值注释);§2 判定规则与优先级(含 `legacy` 与冲突处理);§3 加列 + 回填 + 索引 + 回滚;§4 守恒校验与异常队列查询;§5 人工成本区间 | -| [成本中心与销售事实-DDL草案.sql](drafts/2026-10-17-成本中心与销售事实-DDL草案.sql) | `product_catalog`、`channel_product_mappings`、`cost_items`、`cost_versions`、`cost_snapshots`、`cost_exceptions`、`sales_daily_facts`;订单项口径补齐(`normalized_quantity`/`revenue_amount_fen`/`revenue_basis`/`refunded_at`);`work_product_rules.cost_item_id` 过渡列;§5 对平校验;§6 落地顺序与待业务确认 | +| [成本中心与销售事实-DDL草案.sql](drafts/2026-10-17-成本中心与销售事实-DDL草案.sql) | `product_catalog`、`channel_product_mappings`、`cost_items`、`cost_versions`、`cost_snapshots`、`cost_exceptions`、`sales_daily_facts`;订单项口径补齐(`normalized_quantity`/`revenue_amount_fen`/`revenue_basis`/`refunded_at`);`work_product_rules.cost_item_id` 过渡列;§5 对平校验;§6 已确认口径与剩余待确认 | +| [人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql) | `manual_fulfillment_cases`(轻量处理记录)+ `manual_expense_records`(付款项目/金额/订单号/付款人/收款人/凭证/审批)+ `manual_expense_events` 审计;§3 校验查询;§4 与影子总账的分录约定 | **验证方式**:在本地副本上 `pg_dump --schema-only` 建了一个一次性沙箱库(55 张表),按顺序 `psql -v ON_ERROR_STOP=1` 应用两份草案 —— **零错误**,生成 7 张新表(62 张)、`order_items` 由 9 列增至 19 列、新增 18 个 `CHECK` 约束;校验查询与回填语句均可执行。验证后沙箱库已 `DROP`。 -关键设计约束(两份草案共同遵循): +关键设计约束(三份草案共同遵循): 1. **改价只发生在 `cost_versions`**,历史成本靠 `cost_snapshots` 固化,永不回写; 2. **匹配不到成本不默认 0**,写入 `cost_exceptions`,报表必须暴露 `open` 数量; @@ -158,8 +159,31 @@ Top 店铺(人工块):向日葵游戏服务店 14,903 项 / ¥183.9 万; --- -## 八、下一步 +## 八、业务口径确认(2026-10-17) -1. **业务先拍板 4 个周长问题**(阻塞建表):91 的 `total_amount` 是收入还是结算上限;打手报酬是成本还是代收代付;人工块单均成本取值与是否按工时;退款/仲裁的冲减与归期口径。 -2. **工程先做两件不改业务代码的事**:跑 `channel_product_mappings` 的初始映射(把 47 个码归到 1 个标准商品);把 `normalized_quantity` 用现有 `resolveSkuNameQuantity` 解析后回填订单项。 -3. 之后再按「商品主数据 → 成本中心影子期 → 人工 case → 影子总账 → 对账转正」推进;`manual_fulfillment_cases` 的 DDL 尚未产出(本轮只做载体判定与成本/销售事实),可作为下一份草案。 +业务已就四个阻塞问题给出结论;其中两条已用克隆数据独立核验。 + +| # | 问题 | 结论 | 数据核验 | +| --- | --- | --- | --- | +| 1 | 91 的 `total_amount` 是收入还是成本上限? | **收入**:它就是**用户付款金额**,与快手发码配合 | ✅ 91 已支付 312,569 笔,其中 235,271 笔含快手 `kuaishouSendCode.paymentFen`;**235,268 笔与 `total_amount` 完全一致(100.00%)**,仅 3 笔差 ¥1.80 / ¥1.85 / ¥2.00(2026-09-05 / 09-18 / 10-04) | +| 2 | 打手报酬是成本还是代收代付? | **支出/成本**:工单结算 → 打手可提现 → 提现打款,属我方真实支出 → **总额法** | — | +| 3 | 人工块的目标? | **先把流程搞清楚:谁处理了哪些、付了多少钱**。现状「很多地方需要人工付款,但不知道付款项目、金额、订单号」→ 新增**人工支出登记**草案;SLA/监管指标延后 | — | +| 4 | 退款/仲裁? | **暂未接入,先预留**:字段 + `reversal_of_id` + 事件类型,本轮不实现逻辑 | — | + +影响: + +- 收入口径统一为 `revenue_amount_fen = orders.total_amount`(`revenue_basis = 'user_payment'`),草案中原本枚举的三家字段名已简化; +- 成本侧确认「打手报酬 = 支出」,`cost_items.cost_type = 'work_order_reward'` 计入履约成本,不做净额; +- 人工块方案重心从「case + SLA」改为「**人工支出登记**(付款项目 / 金额 / 订单号 / 付款人 / 收款人 / 凭证 / 审批)」; +- 退款相关列与冲正关系保留为预留,不写逻辑。 + +--- + +## 九、下一步 + +1. **业务仍待拍板 1 个关键口径**:收入**确认时点**(付款 / 发货 / 验收 / 售后期结束)—— 它决定利润表与预收账款;金额口径已定。 +2. **工程可先做两件不改业务代码的事**: + - 建 `channel_product_mappings` 初始映射,把 47 个 `sku_code` 归到 1 个标准商品; + - 用现有 `resolveSkuNameQuantity` 解析并回填 `normalized_quantity`。 +3. **紧接着做人工支出登记**(业务当前最痛):`manual_fulfillment_cases` + `manual_expense_records`,先解决「谁付的、什么项目、多少钱、哪个订单」。 +4. 之后按「商品主数据 → 成本中心影子期 → 人工支出 → 影子总账 → 对账转正」推进。 diff --git a/docs/项目全量分析报告-2026-10-17.md b/docs/项目全量分析报告-2026-10-17.md index a988feee..7ed6459a 100644 --- a/docs/项目全量分析报告-2026-10-17.md +++ b/docs/项目全量分析报告-2026-10-17.md @@ -156,7 +156,7 @@ pending_material → unassigned → open → in_progress - 91 无载体订单 42,391、PDD 2,258、抖音 8;全部 `claim_status=active`。 - `manual_dispatch` 任务全库只有 6 条,说明人工块基本没进任务系统。 - 已验收工单的 `orders.order_status` 仍为 `paid`,订单主状态不能代表履约完成。 -- 最近 30 天已验收工单:91 报酬/订单金额约 86.0%、PDD 80.0%、抖音 96.5%(91 的 `total_amount` 是 `maxAmount`,口径待确认)。 +- 最近 30 天已验收工单:91 报酬/订单金额约 86.0%、PDD 80.0%、抖音 96.5%。**口径已确认(2026-10-17):`total_amount` 即用户付款金额、计收入,打手报酬计支出(总额法)**;91 的 `total_amount` 与快手 `paymentFen` 数据核验 100% 一致。详见 [载体判定与成本中心落地-复核与草案-2026-10-17.md](载体判定与成本中心落地-复核与草案-2026-10-17.md) §九。 - 同一个商品名对应多个 `sku_code`:「指挥官密钥1个」27 个、「苏尔南钻石100颗」35 个,不能直接按 `sku_code` 做销量统计。 - 当前 30 天订单基本是一单一品,但模型必须按订单项设计。