feat(cost-center): 成本中心与商品映射开发草案 (含文档与订单页调整 WIP)

This commit is contained in:
yml2213
2026-10-20 10:01:52 +08:00
parent 7542d547bf
commit 111bc76357
32 changed files with 7088 additions and 156 deletions
@@ -2,10 +2,18 @@
-- 人工支出登记 + 轻量人工处理记录(建表草案)
-- ----------------------------------------------------------------------------
-- 状态:草案(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 §九)。
-- 依赖:本文引用 product_catalog / cost_items / cost_snapshots / manual_expenses,
-- 必须**在** `2026-10-17-成本中心与销售事实-DDL草案.sql` **之后**应用;
-- `manual_expenses` 已由迁移 100 创建(客服备用金支出)。
-- 当前已使用迁移 100/101;成本中心草案拆为 102–104,本文建议顺延为
-- `105_manual_cases.sql`、`106_manual_expenses.sql`,接入时按实际最大号调整并先跑 `npm run check:sql`。
-- 基线:main @ 7542d547;依据:2026-10-17 业务口径确认 + 2026-10-18 备用金落地复核。
--
-- 重要命名边界:
-- * `manual_expenses`(迁移 100)= 客服备用金支出明细,已有索引/约束名 `*_manual_expenses_*`;
-- * `manual_expense_records`(本草案)= 公司人工支出/垫付登记;
-- * 本文件所有索引和约束必须使用 `*_manual_expense_records_*`,禁止复用 `manual_expenses` 的名字。
-- * 两者通过 `manual_expense_records.fund_expense_id` 关联,不能合并成一张表。
--
-- 业务原话(定优先级):
-- 「人工块现在意义不大,主要是把流程搞清楚:哪个人处理了哪些东西,付了多少钱。
@@ -98,6 +106,8 @@ CREATE TABLE IF NOT EXISTS manual_expense_records (
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,
-- 可选:这笔公司支出来源于客服备用金支出时填写;公司直付留空
fund_expense_id BIGINT REFERENCES manual_expenses(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,
@@ -124,55 +134,59 @@ CREATE TABLE IF NOT EXISTS manual_expense_records (
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 UNIQUE INDEX IF NOT EXISTS uq_manual_expense_records_expense_no ON manual_expense_records (expense_no);
CREATE INDEX IF NOT EXISTS idx_manual_expense_records_order ON manual_expense_records (order_id);
CREATE INDEX IF NOT EXISTS idx_manual_expense_records_item ON manual_expense_records (order_item_id);
CREATE INDEX IF NOT EXISTS idx_manual_expense_records_case ON manual_expense_records (case_id);
CREATE INDEX IF NOT EXISTS idx_manual_expense_records_paid_at ON manual_expense_records (paid_at DESC);
CREATE INDEX IF NOT EXISTS idx_manual_expense_records_status ON manual_expense_records (status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_manual_expense_records_cost_item ON manual_expense_records (cost_item_id);
-- 同一笔客服备用金支出最多生成一条公司人工支出,避免重复入账
CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_expense_records_fund_expense
ON manual_expense_records (fund_expense_id) WHERE fund_expense_id IS NOT NULL;
-- 付款人日结对账用
CREATE INDEX IF NOT EXISTS idx_manual_expenses_payer ON manual_expense_records (paid_by_admin_user_id, paid_at DESC);
CREATE INDEX IF NOT EXISTS idx_manual_expense_records_payer ON manual_expense_records (paid_by_admin_user_id, paid_at DESC);
-- 批量归组查询用
CREATE INDEX IF NOT EXISTS idx_manual_expenses_batch
CREATE INDEX IF NOT EXISTS idx_manual_expense_records_batch
ON manual_expense_records (expense_batch_no) WHERE expense_batch_no <> '';
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_amount') THEN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expense_records_amount') THEN
ALTER TABLE manual_expense_records
ADD CONSTRAINT chk_manual_expenses_amount CHECK (amount_fen <> 0);
ADD CONSTRAINT chk_manual_expense_records_amount CHECK (amount_fen <> 0);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_status') THEN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expense_records_status') THEN
ALTER TABLE manual_expense_records
ADD CONSTRAINT chk_manual_expenses_status
ADD CONSTRAINT chk_manual_expense_records_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
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expense_records_payee_type') THEN
ALTER TABLE manual_expense_records
ADD CONSTRAINT chk_manual_expenses_payee_type
ADD CONSTRAINT chk_manual_expense_records_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
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expense_records_method') THEN
ALTER TABLE manual_expense_records
ADD CONSTRAINT chk_manual_expenses_method
ADD CONSTRAINT chk_manual_expense_records_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
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expense_records_category') THEN
ALTER TABLE manual_expense_records
ADD CONSTRAINT chk_manual_expenses_category
ADD CONSTRAINT chk_manual_expense_records_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
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expense_records_approved') THEN
ALTER TABLE manual_expense_records
ADD CONSTRAINT chk_manual_expenses_approved
ADD CONSTRAINT chk_manual_expense_records_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 TABLE manual_expense_records IS '公司人工支出登记:付款项目/金额/订单号/付款人/收款人/凭证;与客服备用金 manual_expenses 通过 fund_expense_id 关联';
COMMENT ON COLUMN manual_expense_records.fund_expense_id IS '来源客服备用金支出(manual_expenses.id);公司直付为空';
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 '实际垫付/付款的后台用户;用于“谁付了多少钱”对账';
@@ -249,7 +263,7 @@ COMMENT ON TABLE manual_expense_events IS '人工支出状态变更审计:只
-- manual_expense:{expense_no}:reversed 冲正(退款/仲裁接入后启用,本轮仅预留)
--
-- 建议分录(approved 时,金额 A):
-- 借:5101 履约成本-人工(或按 cost_items 映射的科目) A
-- 借:5102 履约成本-人工客服(或按 cost_items 映射的科目) A
-- 贷:2005 其他应付-垫付人 A
-- reimbursed 时:
-- 借:2005 其他应付-垫付人 A
@@ -1,12 +1,13 @@
-- ============================================================================
-- 通用成本中心 + 标准商品 + 销售事实(建表草案)
-- ----------------------------------------------------------------------------
-- 状态:草案(DRAFT)。**未接入** apps/backend/src/db/migrations。
-- 接入时应拆成 `101_product_catalog.sql`、`102_cost_center.sql`、`103_sales_daily_facts.sql`,
-- 并先跑 `npm run check:sql`(SQL 防退化护栏)。
-- 基线:main @ ccd5ed2a;数据依据:本地只读克隆(生产快照,orders 316,597,数据截至 2026-10-14)。
-- 状态:部分落地。商品主数据 + 渠道映射 + 成本中心已接入 `102_product_catalog.sql`、
-- `103_cost_center.sql`、`104_cost_center_bootstrap_fix.sql`;本文件仅保留设计追溯。
-- 尚未实施的 `sales_daily_facts` 等后续内容,接入时从新迁移号继续,并先跑 `npm run check:sql`。
-- 基线:main @ 7542d547;数据依据:本地只读克隆(生产快照,orders 316,597,数据截至 2026-10-14)。
-- 金额单位:分(BIGINT)。本文件所有 *_fen 列均为分。
-- 设计依据:docs/成本中心与三块履约模型.md(三块载体、成本版本与快照)、docs/财务系统集成方案.md。
-- 当前状态:商品主数据、渠道映射、成本中心已实施 P0;销售事实未实施。客服备用金支出已实施,但不是本文件的成本中心。
--
-- 目标(业务原话):以后改成本只在一个地方。
-- -> 所有成本金额只写 cost_versions;业务表只保存 cost_snapshot_id 与派生金额,永不回写历史。
@@ -2,10 +2,11 @@
-- 载体判定与人工块量化(只读量化 + 回填草案)
-- ----------------------------------------------------------------------------
-- 状态:草案(DRAFT)。**未接入** apps/backend/src/db/migrations。
-- 接入时应拆成 `100_order_item_carrier.sql`(见 §3),并先跑 `npm run check:sql`。
-- 基线:main @ ccd5ed2a;数据来源 = 本地只读克隆(生产快照,orders 316,597,数据截至 2026-10-14 14:52)
-- 当前已使用迁移 100/101;接入时按当前最大迁移号顺延,不占用 100/101,并先跑 `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 条口径。
-- ============================================================================