From 0de0fd7b5831f57517b1a0c1037e0bc65de4b80b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 27 Oct 2026 12:37:21 +0800 Subject: [PATCH] =?UTF-8?q?chore(db):=20=E5=B0=86=20108=E2=80=93115=20?= =?UTF-8?q?=E8=A1=A5=E4=B8=81=E8=BF=81=E7=A7=BB=E6=94=B6=E6=95=9B=E4=B8=BA?= =?UTF-8?q?=20108=E2=80=93111?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原 108/111/112/113/114 合并为 108_finance_cost_integrity: 成本异常归档、人工成本完整性、影子总账、商品页/模板查询索引 - 原 115 注释迁移改为 111_work_order_share_cancel_reason_comment - 新增 reconcile-migration-history.sql,用于已跑旧 112–115 的库 重建 schema_migrations 顺序,不重复执行 DDL - 已验证:001→111、101→111 两条升级路径; 旧 115 与新 111 的 pg_dump schema 完全一致 - 测试/文档迁移编号同步到 103–111 验证:backend check 853(849 pass / 4 skip);frontend check 172 + build。 --- .../scripts/reconcile-migration-history.sql | 54 ++++ .../migrations/108_cost_exception_archive.sql | 49 --- .../migrations/108_finance_cost_integrity.sql | 301 ++++++++++++++++++ .../migrations/111_manual_cost_integrity.sql | 85 ----- ...ork_order_share_cancel_reason_comment.sql} | 0 .../src/db/migrations/112_finance_ledger.sql | 152 --------- .../113_cost_center_query_indexes.sql | 25 -- .../114_template_evidence_indexes.sql | 17 - .../manual-fulfillment-integrity.test.ts | 9 +- docs/README.md | 10 +- docs/履约配置/多发货平台与电子凭证整体链路.md | 2 +- docs/影子总账说明.md | 2 +- docs/成本中心API契约.md | 2 +- docs/成本中心与履约模型.md | 26 +- docs/接单模板匹配引擎重构设计.md | 2 +- docs/财务上线初始化与验收-2026-10-26.md | 6 +- docs/财务与履约口径总纲-2026-10-23.md | 4 +- docs/财务系统集成方案.md | 6 +- docs/部署启动与数据库重建.md | 2 +- 19 files changed, 394 insertions(+), 360 deletions(-) create mode 100644 apps/backend/scripts/reconcile-migration-history.sql delete mode 100644 apps/backend/src/db/migrations/108_cost_exception_archive.sql create mode 100644 apps/backend/src/db/migrations/108_finance_cost_integrity.sql delete mode 100644 apps/backend/src/db/migrations/111_manual_cost_integrity.sql rename apps/backend/src/db/migrations/{115_work_order_share_cancel_reason_comment.sql => 111_work_order_share_cancel_reason_comment.sql} (100%) delete mode 100644 apps/backend/src/db/migrations/112_finance_ledger.sql delete mode 100644 apps/backend/src/db/migrations/113_cost_center_query_indexes.sql delete mode 100644 apps/backend/src/db/migrations/114_template_evidence_indexes.sql diff --git a/apps/backend/scripts/reconcile-migration-history.sql b/apps/backend/scripts/reconcile-migration-history.sql new file mode 100644 index 00000000..717b9a7f --- /dev/null +++ b/apps/backend/scripts/reconcile-migration-history.sql @@ -0,0 +1,54 @@ +-- 旧 108–115 → 新 108–111 的迁移元数据校准(仅用于已经跑过旧 112–115 的本地/测试库)。 +-- +-- 背景:2026-10-27 把原 108/111/112/113/114 合并成 108_finance_cost_integrity, +-- 原 115 注释迁移改名为 111。新库从 001/101 直接跑新文件即可; +-- 已跑过旧 112–115 的库 schema 已等价,只需把 schema_migrations 的旧名字替换掉, +-- 否则 node-pg-migrate 会因 108 顺序冲突拒绝启动。 +-- +-- 用法(先备份): +-- docker exec -i order_site-postgres-1 psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ +-- -v ON_ERROR_STOP=1 < apps/backend/scripts/reconcile-migration-history.sql +-- +-- 幂等:只在检测到旧 112_finance_ledger 时执行。 + +BEGIN; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM schema_migrations WHERE name = '112_finance_ledger') THEN + RAISE NOTICE '未检测到旧 112_finance_ledger,跳过迁移历史校准'; + RETURN; + END IF; + + -- 108–111 整段重建,保证 node-pg-migrate 的 runNames 顺序与文件顺序一致。 + -- 109/110 的 schema 已经存在,重新登记不会重复执行。 + DELETE FROM schema_migrations + WHERE name IN ( + '108_cost_exception_archive', + '108_finance_cost_integrity', + '109_work_order_share_participation', + '110_work_order_hall_and_shop_indexes', + '111_manual_cost_integrity', + '111_work_order_share_cancel_reason_comment', + '112_finance_ledger', + '113_cost_center_query_indexes', + '114_template_evidence_indexes', + '115_work_order_share_cancel_reason_comment' + ); + + INSERT INTO schema_migrations (name, filename, run_on) + VALUES + ('108_finance_cost_integrity', '108_finance_cost_integrity.sql', NOW()), + ('109_work_order_share_participation', '109_work_order_share_participation.sql', NOW()), + ('110_work_order_hall_and_shop_indexes', '110_work_order_hall_and_shop_indexes.sql', NOW()), + ('111_work_order_share_cancel_reason_comment', '111_work_order_share_cancel_reason_comment.sql', NOW()) + ON CONFLICT (name) DO NOTHING; + + -- 旧 115 的注释效果已经在 schema 里;这里保证新基线也带上同一注释。 + COMMENT ON COLUMN work_order_shares.cancelled_reason IS + '取消来源:timeout/worker_request/admin/admin_partial/problem/settlement'; + + RAISE NOTICE '迁移历史已校准为 108–111'; +END $$; + +COMMIT; diff --git a/apps/backend/src/db/migrations/108_cost_exception_archive.sql b/apps/backend/src/db/migrations/108_cost_exception_archive.sql deleted file mode 100644 index 08659668..00000000 --- a/apps/backend/src/db/migrations/108_cost_exception_archive.sql +++ /dev/null @@ -1,49 +0,0 @@ --- 108_cost_exception_archive.sql —— 成本异常归档表。 --- --- 背景(2026-10-25): --- cost_exceptions 只增不减。本地快照上 260,253 行(resolved 151,939 / open 108,314), --- 其中 open 的 product_mapping_missing 与 cost_missing 仍是可操作队列, --- 但已处理(resolved / ignored)的历史行只是审计痕迹,继续留在热表里拖慢: --- - 按状态 + 时间分页要扫过大量已完成行; --- - 唯一索引 uq_cost_exceptions_subject_reason 随行数膨胀。 --- --- 策略: --- 1. 只归档**已处理**(status <> 'open')且超过保留期(默认 60 天)的行; --- 2. open 行永远留在热表,避免把未解决的问题藏起来; --- 3. 归档按批(每次 1000 行)在 data-retention 定时任务里执行,不锁全表; --- 4. 归档表不建外键:历史异常可能指向已删除的订单项/工单,保留原值即可。 --- --- 保留期由 COST_EXCEPTION_RETENTION_DAYS 控制(默认 60)。 - -CREATE TABLE IF NOT EXISTS cost_exceptions_archive ( - id BIGINT PRIMARY KEY, - subject_type TEXT NOT NULL, - subject_id BIGINT NOT NULL, - order_item_id BIGINT, - work_order_id BIGINT, - reason TEXT NOT NULL, - status TEXT NOT NULL, - detail_json JSONB NOT NULL DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ NOT NULL, - resolved_at TIMESTAMPTZ, - resolved_by_admin_user_id BIGINT, - resolution_note TEXT NOT NULL DEFAULT '', - archived_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_cost_exceptions_archive_archived_at - ON cost_exceptions_archive (archived_at DESC); -CREATE INDEX IF NOT EXISTS idx_cost_exceptions_archive_subject - ON cost_exceptions_archive (subject_type, subject_id); -CREATE INDEX IF NOT EXISTS idx_cost_exceptions_archive_reason - ON cost_exceptions_archive (reason, archived_at DESC); - -COMMENT ON TABLE cost_exceptions_archive IS - '已处理成本异常的归档:data-retention 任务把超过保留期(默认 60 天)的 resolved/ignored 行搬到这里,open 行不归档'; -COMMENT ON COLUMN cost_exceptions_archive.archived_at IS - '归档时间;原表 created_at / resolved_at 原样保留,便于按原时间追溯'; - --- 热表上补一个「待归档」用的部分索引,避免每日任务扫全表。 -CREATE INDEX IF NOT EXISTS idx_cost_exceptions_archivable - ON cost_exceptions (COALESCE(resolved_at, created_at)) - WHERE status <> 'open'; diff --git a/apps/backend/src/db/migrations/108_finance_cost_integrity.sql b/apps/backend/src/db/migrations/108_finance_cost_integrity.sql new file mode 100644 index 00000000..3686e857 --- /dev/null +++ b/apps/backend/src/db/migrations/108_finance_cost_integrity.sql @@ -0,0 +1,301 @@ +-- 108_finance_cost_integrity.sql +-- 财务/成本完整性与影子总账(合并原 108/111/112/113/114)。 +-- +-- 合并原因:2026-10-26 之前这几块是分批补丁,生产基线 101 升级时会产生 +-- 5 个只差索引/约束/注释的迁移,顺序和回滚都难维护。合并后: +-- 1. 成本异常归档表 +-- 2. 人工成本订单项级唯一与 subject 语义 +-- 3. 影子总账/期间/对账 +-- 4. 商品页/模板预览查询索引 +-- 全部语句保持幂等;新库从 101 升级与空库初始化同一路径。 + + +-- --------------------------------------------------------------------------- +-- 成本异常归档(原 108_cost_exception_archive.sql) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS cost_exceptions_archive ( + id BIGINT PRIMARY KEY, + subject_type TEXT NOT NULL, + subject_id BIGINT NOT NULL, + order_item_id BIGINT, + work_order_id BIGINT, + reason TEXT NOT NULL, + status TEXT NOT NULL, + detail_json JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL, + resolved_at TIMESTAMPTZ, + resolved_by_admin_user_id BIGINT, + resolution_note TEXT NOT NULL DEFAULT '', + archived_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_cost_exceptions_archive_archived_at + ON cost_exceptions_archive (archived_at DESC); +CREATE INDEX IF NOT EXISTS idx_cost_exceptions_archive_subject + ON cost_exceptions_archive (subject_type, subject_id); +CREATE INDEX IF NOT EXISTS idx_cost_exceptions_archive_reason + ON cost_exceptions_archive (reason, archived_at DESC); + +COMMENT ON TABLE cost_exceptions_archive IS + '已处理成本异常的归档:data-retention 任务把超过保留期(默认 60 天)的 resolved/ignored 行搬到这里,open 行不归档'; +COMMENT ON COLUMN cost_exceptions_archive.archived_at IS + '归档时间;原表 created_at / resolved_at 原样保留,便于按原时间追溯'; + +-- 热表上补一个「待归档」用的部分索引,避免每日任务扫全表。 +CREATE INDEX IF NOT EXISTS idx_cost_exceptions_archivable + ON cost_exceptions (COALESCE(resolved_at, created_at)) + WHERE status <> 'open'; + + +-- --------------------------------------------------------------------------- +-- 人工成本完整性(原 111_manual_cost_integrity.sql) +-- --------------------------------------------------------------------------- +UPDATE cost_snapshots +SET subject_id = order_item_id +WHERE subject_type = 'manual_case' + AND order_item_id IS NOT NULL + AND subject_id > 0 + AND subject_id <> order_item_id; + +-- 2. 同一订单项存在重复有效人工快照时,只保留最新一条(历史脏数据兜底)。 +UPDATE cost_snapshots c +SET superseded_at = NOW() +WHERE c.subject_type = 'manual_case' + AND c.superseded_at IS NULL + AND c.subject_id > 0 + AND EXISTS ( + SELECT 1 + FROM cost_snapshots newer + WHERE newer.subject_type = 'manual_case' + AND newer.order_item_id = c.order_item_id + AND newer.superseded_at IS NULL + AND newer.id > c.id + ); + +-- 3. 数据库级唯一:一个订单项只有一条有效人工成本快照。 +CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_snapshots_manual_order_item + ON cost_snapshots (order_item_id) + WHERE subject_type = 'manual_case' AND superseded_at IS NULL AND subject_id > 0; + +-- 4. 语义约束:新写入的 manual_case 必须 subject_id = order_item_id。 +-- NOT VALID 只约束新写入,负数历史孤儿快照(迁移 104 约定)保持原样。 +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_manual_subject' + ) THEN + ALTER TABLE cost_snapshots + ADD CONSTRAINT chk_cost_snapshots_manual_subject + CHECK (subject_type <> 'manual_case' OR subject_id = order_item_id) + NOT VALID; + END IF; +END $$; + +-- 5. 人工成本监控 / 对账按订单项反查备用金支出。 +CREATE INDEX IF NOT EXISTS idx_manual_expenses_order_item + ON manual_expenses (order_item_id) + WHERE order_item_id IS NOT NULL; + +-- 6. 人工 SKU 匹配键索引:spec_json.skuNick -> outSkuSn -> sku_code -> sku_name。 +-- 旧索引只覆盖前两级,缺 SKU 键的人工单会走顺序扫描;这里替换为同一套兜底表达式。 +DROP INDEX IF EXISTS idx_order_items_manual_internal_sku; +CREATE INDEX IF NOT EXISTS idx_order_items_manual_sku_key + ON order_items ( + COALESCE( + NULLIF(spec_json->>'skuNick', ''), + NULLIF(spec_json->>'outSkuSn', ''), + NULLIF(sku_code, ''), + NULLIF(sku_name, '') + ) + ) + WHERE fulfillment_mode = 'manual'; + +COMMENT ON INDEX uq_cost_snapshots_manual_order_item IS + '人工成本完整性:一个订单项最多一条有效 manual_case 快照(并发重复确认的数据库兜底)'; + + +-- --------------------------------------------------------------------------- +-- 影子总账(原 112_finance_ledger.sql) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS finance_accounts ( + account_code TEXT PRIMARY KEY, + account_name TEXT NOT NULL, + account_type TEXT NOT NULL + CHECK (account_type IN ('asset', 'liability', 'equity', 'revenue', 'expense')), + normal_side TEXT NOT NULL CHECK (normal_side IN ('debit', 'credit')), + parent_code TEXT REFERENCES finance_accounts(account_code) ON DELETE RESTRICT, + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')), + description TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE IF NOT EXISTS finance_journal_entries ( + id BIGSERIAL PRIMARY KEY, + entry_no TEXT NOT NULL UNIQUE, + event_type TEXT NOT NULL, + ref_type TEXT NOT NULL, + ref_id TEXT NOT NULL, + occurred_at TIMESTAMPTZ NOT NULL, + period_key TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'posted' CHECK (status IN ('posted', 'reversed')), + memo TEXT NOT NULL DEFAULT '', + metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + UNIQUE (event_type, ref_type, ref_id) +); + +CREATE INDEX IF NOT EXISTS idx_finance_entries_period + ON finance_journal_entries (period_key, occurred_at DESC); +CREATE INDEX IF NOT EXISTS idx_finance_entries_ref + ON finance_journal_entries (ref_type, ref_id); + +CREATE TABLE IF NOT EXISTS finance_journal_lines ( + id BIGSERIAL PRIMARY KEY, + entry_id BIGINT NOT NULL REFERENCES finance_journal_entries(id) ON DELETE CASCADE, + line_no INTEGER NOT NULL, + account_code TEXT NOT NULL REFERENCES finance_accounts(account_code) ON DELETE RESTRICT, + debit_fen BIGINT NOT NULL DEFAULT 0, + credit_fen BIGINT NOT NULL DEFAULT 0, + memo TEXT NOT NULL DEFAULT '', + dimensions_json JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL, + CHECK (debit_fen >= 0 AND credit_fen >= 0), + CHECK ((debit_fen > 0) <> (credit_fen > 0)), + UNIQUE (entry_id, line_no) +); + +CREATE INDEX IF NOT EXISTS idx_finance_lines_account + ON finance_journal_lines (account_code); + +CREATE TABLE IF NOT EXISTS finance_periods ( + period_key TEXT PRIMARY KEY, + starts_at TIMESTAMPTZ NOT NULL, + ends_at TIMESTAMPTZ NOT NULL, + status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'closed')), + summary_json JSONB NOT NULL DEFAULT '{}'::jsonb, + closed_at TIMESTAMPTZ, + closed_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE IF NOT EXISTS finance_reconciliations ( + id BIGSERIAL PRIMARY KEY, + provider TEXT NOT NULL, + period_from DATE NOT NULL, + period_to DATE NOT NULL, + source TEXT NOT NULL DEFAULT 'manual_import', + status TEXT NOT NULL DEFAULT 'imported' + CHECK (status IN ('imported', 'matched', 'differences', 'resolved')), + expected_fen BIGINT NOT NULL DEFAULT 0, + actual_fen BIGINT NOT NULL DEFAULT 0, + diff_fen BIGINT NOT NULL DEFAULT 0, + summary_json JSONB NOT NULL DEFAULT '{}'::jsonb, + imported_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL, + resolved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_finance_reconciliations_period + ON finance_reconciliations (provider, period_from DESC, id DESC); + +CREATE TABLE IF NOT EXISTS finance_reconciliation_items ( + id BIGSERIAL PRIMARY KEY, + reconciliation_id BIGINT NOT NULL + REFERENCES finance_reconciliations(id) ON DELETE CASCADE, + line_no INTEGER NOT NULL, + provider TEXT NOT NULL, + platform_order_id TEXT NOT NULL DEFAULT '', + external_ref TEXT NOT NULL DEFAULT '', + entry_type TEXT NOT NULL CHECK (entry_type IN ('sale', 'refund')), + amount_fen BIGINT NOT NULL, + occurred_at TIMESTAMPTZ, + matched_order_id BIGINT, + matched_refund_id BIGINT, + expected_fen BIGINT, + diff_fen BIGINT, + match_status TEXT NOT NULL + CHECK (match_status IN ( + 'matched', 'amount_mismatch', 'missing_order', 'missing_refund', 'duplicate' + )), + detail_json JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL, + UNIQUE (reconciliation_id, line_no) +); + +CREATE INDEX IF NOT EXISTS idx_finance_reconciliation_items_status + ON finance_reconciliation_items (reconciliation_id, match_status); + +-- 科目表(可按业务裁剪;借贷方向先定死) +INSERT INTO finance_accounts ( + account_code, account_name, account_type, normal_side, description, created_at, updated_at +) +VALUES + ('1001', '银行存款', 'asset', 'debit', '公司银行/支付通道资金', NOW(), NOW()), + ('1002', '客服备用金', 'asset', 'debit', '已拨付给客服的备用金', NOW(), NOW()), + ('1101', '应收账款-平台', 'asset', 'debit', '平台待结算/待收款', NOW(), NOW()), + ('1201', '预付账款-客服积分', 'asset', 'debit', '已充值到平台的客服积分', NOW(), NOW()), + ('2001', '应付履约款', 'liability', 'credit', '应付打手/人工履约成本', NOW(), NOW()), + ('2005', '其他应付-垫付人', 'liability', 'credit', '客服垫付待核销', NOW(), NOW()), + ('3001', '投入资金/权益', 'equity', 'credit', '股东投入与期初权益', NOW(), NOW()), + ('4001', '商品销售收入', 'revenue', 'credit', '按用户实付确认的收入', NOW(), NOW()), + ('4101', '退款冲减', 'revenue', 'debit', '退款冲减收入(借方)', NOW(), NOW()), + ('5001', '履约成本', 'expense', 'debit', '打手报酬/人工/自动发货成本', NOW(), NOW()), + ('5101', '人工履约成本', 'expense', 'debit', '人工客服实际垫付成本', NOW(), NOW()) +ON CONFLICT (account_code) DO UPDATE +SET account_name = EXCLUDED.account_name, + account_type = EXCLUDED.account_type, + normal_side = EXCLUDED.normal_side, + description = EXCLUDED.description, + updated_at = NOW(); + +COMMENT ON TABLE finance_accounts IS '会计科目表;影子总账使用,不接管业务钱包余额'; +COMMENT ON TABLE finance_journal_entries IS '凭证头;event_type + ref_type + ref_id 保证业务幂等'; +COMMENT ON TABLE finance_journal_lines IS '凭证行;每行只能借或贷,单张凭证借贷必须平衡'; +COMMENT ON TABLE finance_periods IS '会计期间;closed 期间禁止新增凭证'; +COMMENT ON TABLE finance_reconciliations IS '平台对账批次:导入平台账单后与订单/退款匹配'; +COMMENT ON TABLE finance_reconciliation_items IS '对账明细:matched / amount_mismatch / missing_* / duplicate'; + + +-- --------------------------------------------------------------------------- +-- 成本中心查询索引(原 113_cost_center_query_indexes.sql) +-- --------------------------------------------------------------------------- +CREATE INDEX IF NOT EXISTS idx_order_items_unmapped_mode + ON order_items (fulfillment_mode) + WHERE canonical_product_id IS NULL; + +CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_affiliate_recent + ON fulfillment_tasks (created_at) + WHERE executor_key = 'affiliate_dash'; + +COMMENT ON INDEX idx_order_items_unmapped_mode IS + '成本中心总览:按履约方式统计未归一订单项,避免全表扫 order_items'; +COMMENT ON INDEX idx_fulfillment_tasks_affiliate_recent IS + '自动发货任务 30 天聚合:只索引 affiliate_dash,减少商品列表聚合扫描'; + +-- 新索引建好后立刻更新统计信息,避免参数化查询在旧统计下继续选全表扫描。 +ANALYZE order_items; +ANALYZE product_catalog; +ANALYZE cost_exceptions; +ANALYZE fulfillment_tasks; + + +-- --------------------------------------------------------------------------- +-- 模板证据索引(原 114_template_evidence_indexes.sql) +-- --------------------------------------------------------------------------- +CREATE INDEX IF NOT EXISTS idx_orders_basis_date + ON orders (((COALESCE(paid_at, created_at) AT TIME ZONE 'Asia/Shanghai')::date)); + +CREATE INDEX IF NOT EXISTS idx_order_items_order_sku_cover + ON order_items (order_id) INCLUDE (id, sku_code, sku_name); + +COMMENT ON INDEX idx_orders_basis_date IS + '模板初始化/时间筛选:按 Asia/Shanghai 订单日期过滤,避免全表扫 orders'; +COMMENT ON INDEX idx_order_items_order_sku_cover IS + '模板初始化的 SKU 证据覆盖索引,避免回表取 sku_code/sku_name'; + +ANALYZE orders; +ANALYZE order_items; diff --git a/apps/backend/src/db/migrations/111_manual_cost_integrity.sql b/apps/backend/src/db/migrations/111_manual_cost_integrity.sql deleted file mode 100644 index 726f9556..00000000 --- a/apps/backend/src/db/migrations/111_manual_cost_integrity.sql +++ /dev/null @@ -1,85 +0,0 @@ --- 111_manual_cost_integrity.sql --- 人工成本完整性:一个订单项只允许一条有效的人工成本快照。 --- --- 背景: --- 1. 旧实现把 cost_snapshots.subject_id 写成 manual_expenses.id 或 --- platform_credit_entries.id(两张表各自 BIGSERIAL),与唯一索引 --- (subject_type, subject_id, cost_item_id) 叠加后,两条链路会互相撞 id: --- 第二次合法确认被当成「已有人工成本快照」拒绝,而钱已经扣了。 --- 2. 人工确认原先是「先扣款、后写快照」两个事务,快照失败会留下已扣款未记成本。 --- 现在改为同一事务,并由本迁移提供数据库级兜底: --- 一个 order_item 只允许一条有效 (superseded_at IS NULL) 的 manual_case 快照。 --- --- 语义调整: --- * subject_type = 'manual_case' 的 subject_id 统一为 order_item_id; --- 资金来源(manual_expenses.id / platform_credit_entries.id)只写 metadata_json, --- 不再作为 subject_id。 --- * 正数 subject_id 的历史行(旧实现写入的支出单/积分流水 id)修正为 order_item_id; --- 负数 subject_id 的历史孤儿快照按迁移 104 的约定保留,不参与新链路。 --- * 重复正数行保留最新一条,其余标记 superseded,避免唯一索引创建失败。 --- --- 全部语句幂等,可安全重跑。 - --- 1. 历史人工快照的 subject_id 归一为 order_item_id。 -UPDATE cost_snapshots -SET subject_id = order_item_id -WHERE subject_type = 'manual_case' - AND order_item_id IS NOT NULL - AND subject_id > 0 - AND subject_id <> order_item_id; - --- 2. 同一订单项存在重复有效人工快照时,只保留最新一条(历史脏数据兜底)。 -UPDATE cost_snapshots c -SET superseded_at = NOW() -WHERE c.subject_type = 'manual_case' - AND c.superseded_at IS NULL - AND c.subject_id > 0 - AND EXISTS ( - SELECT 1 - FROM cost_snapshots newer - WHERE newer.subject_type = 'manual_case' - AND newer.order_item_id = c.order_item_id - AND newer.superseded_at IS NULL - AND newer.id > c.id - ); - --- 3. 数据库级唯一:一个订单项只有一条有效人工成本快照。 -CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_snapshots_manual_order_item - ON cost_snapshots (order_item_id) - WHERE subject_type = 'manual_case' AND superseded_at IS NULL AND subject_id > 0; - --- 4. 语义约束:新写入的 manual_case 必须 subject_id = order_item_id。 --- NOT VALID 只约束新写入,负数历史孤儿快照(迁移 104 约定)保持原样。 -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_manual_subject' - ) THEN - ALTER TABLE cost_snapshots - ADD CONSTRAINT chk_cost_snapshots_manual_subject - CHECK (subject_type <> 'manual_case' OR subject_id = order_item_id) - NOT VALID; - END IF; -END $$; - --- 5. 人工成本监控 / 对账按订单项反查备用金支出。 -CREATE INDEX IF NOT EXISTS idx_manual_expenses_order_item - ON manual_expenses (order_item_id) - WHERE order_item_id IS NOT NULL; - --- 6. 人工 SKU 匹配键索引:spec_json.skuNick -> outSkuSn -> sku_code -> sku_name。 --- 旧索引只覆盖前两级,缺 SKU 键的人工单会走顺序扫描;这里替换为同一套兜底表达式。 -DROP INDEX IF EXISTS idx_order_items_manual_internal_sku; -CREATE INDEX IF NOT EXISTS idx_order_items_manual_sku_key - ON order_items ( - COALESCE( - NULLIF(spec_json->>'skuNick', ''), - NULLIF(spec_json->>'outSkuSn', ''), - NULLIF(sku_code, ''), - NULLIF(sku_name, '') - ) - ) - WHERE fulfillment_mode = 'manual'; - -COMMENT ON INDEX uq_cost_snapshots_manual_order_item IS - '人工成本完整性:一个订单项最多一条有效 manual_case 快照(并发重复确认的数据库兜底)'; diff --git a/apps/backend/src/db/migrations/115_work_order_share_cancel_reason_comment.sql b/apps/backend/src/db/migrations/111_work_order_share_cancel_reason_comment.sql similarity index 100% rename from apps/backend/src/db/migrations/115_work_order_share_cancel_reason_comment.sql rename to apps/backend/src/db/migrations/111_work_order_share_cancel_reason_comment.sql diff --git a/apps/backend/src/db/migrations/112_finance_ledger.sql b/apps/backend/src/db/migrations/112_finance_ledger.sql deleted file mode 100644 index 4e063384..00000000 --- a/apps/backend/src/db/migrations/112_finance_ledger.sql +++ /dev/null @@ -1,152 +0,0 @@ --- 112_finance_ledger.sql --- 财务总账(影子账):科目、凭证、期间关账、平台对账。 --- --- 设计边界: --- * 影子账不接管现有钱包余额;现有 worker_wallets / fund_accounts 仍是运行时权威。 --- * 凭证按业务/日期幂等(event_type + ref_type + ref_id 唯一),可重复同步。 --- * 销售事实同步按日生成收入/退款/成本凭证;人工确认履约时补记「付款冲应付」凭证。 --- * 期间关账只阻断该期间的新凭证,不修改历史凭证。 --- --- 全部建表/种子幂等。 - -CREATE TABLE IF NOT EXISTS finance_accounts ( - account_code TEXT PRIMARY KEY, - account_name TEXT NOT NULL, - account_type TEXT NOT NULL - CHECK (account_type IN ('asset', 'liability', 'equity', 'revenue', 'expense')), - normal_side TEXT NOT NULL CHECK (normal_side IN ('debit', 'credit')), - parent_code TEXT REFERENCES finance_accounts(account_code) ON DELETE RESTRICT, - status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')), - description TEXT NOT NULL DEFAULT '', - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL -); - -CREATE TABLE IF NOT EXISTS finance_journal_entries ( - id BIGSERIAL PRIMARY KEY, - entry_no TEXT NOT NULL UNIQUE, - event_type TEXT NOT NULL, - ref_type TEXT NOT NULL, - ref_id TEXT NOT NULL, - occurred_at TIMESTAMPTZ NOT NULL, - period_key TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'posted' CHECK (status IN ('posted', 'reversed')), - memo TEXT NOT NULL DEFAULT '', - metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - UNIQUE (event_type, ref_type, ref_id) -); - -CREATE INDEX IF NOT EXISTS idx_finance_entries_period - ON finance_journal_entries (period_key, occurred_at DESC); -CREATE INDEX IF NOT EXISTS idx_finance_entries_ref - ON finance_journal_entries (ref_type, ref_id); - -CREATE TABLE IF NOT EXISTS finance_journal_lines ( - id BIGSERIAL PRIMARY KEY, - entry_id BIGINT NOT NULL REFERENCES finance_journal_entries(id) ON DELETE CASCADE, - line_no INTEGER NOT NULL, - account_code TEXT NOT NULL REFERENCES finance_accounts(account_code) ON DELETE RESTRICT, - debit_fen BIGINT NOT NULL DEFAULT 0, - credit_fen BIGINT NOT NULL DEFAULT 0, - memo TEXT NOT NULL DEFAULT '', - dimensions_json JSONB NOT NULL DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ NOT NULL, - CHECK (debit_fen >= 0 AND credit_fen >= 0), - CHECK ((debit_fen > 0) <> (credit_fen > 0)), - UNIQUE (entry_id, line_no) -); - -CREATE INDEX IF NOT EXISTS idx_finance_lines_account - ON finance_journal_lines (account_code); - -CREATE TABLE IF NOT EXISTS finance_periods ( - period_key TEXT PRIMARY KEY, - starts_at TIMESTAMPTZ NOT NULL, - ends_at TIMESTAMPTZ NOT NULL, - status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'closed')), - summary_json JSONB NOT NULL DEFAULT '{}'::jsonb, - closed_at TIMESTAMPTZ, - closed_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL -); - -CREATE TABLE IF NOT EXISTS finance_reconciliations ( - id BIGSERIAL PRIMARY KEY, - provider TEXT NOT NULL, - period_from DATE NOT NULL, - period_to DATE NOT NULL, - source TEXT NOT NULL DEFAULT 'manual_import', - status TEXT NOT NULL DEFAULT 'imported' - CHECK (status IN ('imported', 'matched', 'differences', 'resolved')), - expected_fen BIGINT NOT NULL DEFAULT 0, - actual_fen BIGINT NOT NULL DEFAULT 0, - diff_fen BIGINT NOT NULL DEFAULT 0, - summary_json JSONB NOT NULL DEFAULT '{}'::jsonb, - imported_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL, - resolved_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_finance_reconciliations_period - ON finance_reconciliations (provider, period_from DESC, id DESC); - -CREATE TABLE IF NOT EXISTS finance_reconciliation_items ( - id BIGSERIAL PRIMARY KEY, - reconciliation_id BIGINT NOT NULL - REFERENCES finance_reconciliations(id) ON DELETE CASCADE, - line_no INTEGER NOT NULL, - provider TEXT NOT NULL, - platform_order_id TEXT NOT NULL DEFAULT '', - external_ref TEXT NOT NULL DEFAULT '', - entry_type TEXT NOT NULL CHECK (entry_type IN ('sale', 'refund')), - amount_fen BIGINT NOT NULL, - occurred_at TIMESTAMPTZ, - matched_order_id BIGINT, - matched_refund_id BIGINT, - expected_fen BIGINT, - diff_fen BIGINT, - match_status TEXT NOT NULL - CHECK (match_status IN ( - 'matched', 'amount_mismatch', 'missing_order', 'missing_refund', 'duplicate' - )), - detail_json JSONB NOT NULL DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ NOT NULL, - UNIQUE (reconciliation_id, line_no) -); - -CREATE INDEX IF NOT EXISTS idx_finance_reconciliation_items_status - ON finance_reconciliation_items (reconciliation_id, match_status); - --- 科目表(可按业务裁剪;借贷方向先定死) -INSERT INTO finance_accounts ( - account_code, account_name, account_type, normal_side, description, created_at, updated_at -) -VALUES - ('1001', '银行存款', 'asset', 'debit', '公司银行/支付通道资金', NOW(), NOW()), - ('1002', '客服备用金', 'asset', 'debit', '已拨付给客服的备用金', NOW(), NOW()), - ('1101', '应收账款-平台', 'asset', 'debit', '平台待结算/待收款', NOW(), NOW()), - ('1201', '预付账款-客服积分', 'asset', 'debit', '已充值到平台的客服积分', NOW(), NOW()), - ('2001', '应付履约款', 'liability', 'credit', '应付打手/人工履约成本', NOW(), NOW()), - ('2005', '其他应付-垫付人', 'liability', 'credit', '客服垫付待核销', NOW(), NOW()), - ('3001', '投入资金/权益', 'equity', 'credit', '股东投入与期初权益', NOW(), NOW()), - ('4001', '商品销售收入', 'revenue', 'credit', '按用户实付确认的收入', NOW(), NOW()), - ('4101', '退款冲减', 'revenue', 'debit', '退款冲减收入(借方)', NOW(), NOW()), - ('5001', '履约成本', 'expense', 'debit', '打手报酬/人工/自动发货成本', NOW(), NOW()), - ('5101', '人工履约成本', 'expense', 'debit', '人工客服实际垫付成本', NOW(), NOW()) -ON CONFLICT (account_code) DO UPDATE -SET account_name = EXCLUDED.account_name, - account_type = EXCLUDED.account_type, - normal_side = EXCLUDED.normal_side, - description = EXCLUDED.description, - updated_at = NOW(); - -COMMENT ON TABLE finance_accounts IS '会计科目表;影子总账使用,不接管业务钱包余额'; -COMMENT ON TABLE finance_journal_entries IS '凭证头;event_type + ref_type + ref_id 保证业务幂等'; -COMMENT ON TABLE finance_journal_lines IS '凭证行;每行只能借或贷,单张凭证借贷必须平衡'; -COMMENT ON TABLE finance_periods IS '会计期间;closed 期间禁止新增凭证'; -COMMENT ON TABLE finance_reconciliations IS '平台对账批次:导入平台账单后与订单/退款匹配'; -COMMENT ON TABLE finance_reconciliation_items IS '对账明细:matched / amount_mismatch / missing_* / duplicate'; diff --git a/apps/backend/src/db/migrations/113_cost_center_query_indexes.sql b/apps/backend/src/db/migrations/113_cost_center_query_indexes.sql deleted file mode 100644 index ac125931..00000000 --- a/apps/backend/src/db/migrations/113_cost_center_query_indexes.sql +++ /dev/null @@ -1,25 +0,0 @@ --- 113_cost_center_query_indexes.sql --- 成本中心商品页性能:总览的「未映射订单项」与商品列表的 affiliate 任务聚合。 --- --- 背景:测试库 40 万 order_items 时,总览对 canonical_product_id IS NULL 做全表扫描约 3.8s; --- 商品列表在人工 Tab 仍会聚合 30 天 affiliate 任务,测试机 IO 差时超过 10s。 --- 两个索引都是部分索引,体积远小于全表索引。 - -CREATE INDEX IF NOT EXISTS idx_order_items_unmapped_mode - ON order_items (fulfillment_mode) - WHERE canonical_product_id IS NULL; - -CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_affiliate_recent - ON fulfillment_tasks (created_at) - WHERE executor_key = 'affiliate_dash'; - -COMMENT ON INDEX idx_order_items_unmapped_mode IS - '成本中心总览:按履约方式统计未归一订单项,避免全表扫 order_items'; -COMMENT ON INDEX idx_fulfillment_tasks_affiliate_recent IS - '自动发货任务 30 天聚合:只索引 affiliate_dash,减少商品列表聚合扫描'; - --- 新索引建好后立刻更新统计信息,避免参数化查询在旧统计下继续选全表扫描。 -ANALYZE order_items; -ANALYZE product_catalog; -ANALYZE cost_exceptions; -ANALYZE fulfillment_tasks; diff --git a/apps/backend/src/db/migrations/114_template_evidence_indexes.sql b/apps/backend/src/db/migrations/114_template_evidence_indexes.sql deleted file mode 100644 index 1fd81d02..00000000 --- a/apps/backend/src/db/migrations/114_template_evidence_indexes.sql +++ /dev/null @@ -1,17 +0,0 @@ --- 114_template_evidence_indexes.sql --- 接单模板初始化预览:按订单日期筛 30 天,再按订单项取 SKU 证据。 --- 测试库 40 万订单项时缺索引会到 40 秒;两个索引把预览降到秒级。 - -CREATE INDEX IF NOT EXISTS idx_orders_basis_date - ON orders (((COALESCE(paid_at, created_at) AT TIME ZONE 'Asia/Shanghai')::date)); - -CREATE INDEX IF NOT EXISTS idx_order_items_order_sku_cover - ON order_items (order_id) INCLUDE (id, sku_code, sku_name); - -COMMENT ON INDEX idx_orders_basis_date IS - '模板初始化/时间筛选:按 Asia/Shanghai 订单日期过滤,避免全表扫 orders'; -COMMENT ON INDEX idx_order_items_order_sku_cover IS - '模板初始化的 SKU 证据覆盖索引,避免回表取 sku_code/sku_name'; - -ANALYZE orders; -ANALYZE order_items; diff --git a/apps/backend/src/services/finance/manual-fulfillment-integrity.test.ts b/apps/backend/src/services/finance/manual-fulfillment-integrity.test.ts index 253a2225..76658531 100644 --- a/apps/backend/src/services/finance/manual-fulfillment-integrity.test.ts +++ b/apps/backend/src/services/finance/manual-fulfillment-integrity.test.ts @@ -127,9 +127,9 @@ test('人工成本缺失必须进入异常队列,且只监控首次配置之 ) }) -test('迁移 111 提供订单项级人工成本唯一约束与 subject 语义约束', async () => { +test('迁移 108 提供订单项级人工成本唯一约束与 subject 语义约束', async () => { const source = await readFile( - new URL('../../db/migrations/111_manual_cost_integrity.sql', import.meta.url), + new URL('../../db/migrations/108_finance_cost_integrity.sql', import.meta.url), 'utf8', ) assert.ok(source.includes('uq_cost_snapshots_manual_order_item'), '缺少订单项级人工快照唯一索引') @@ -146,7 +146,10 @@ test('迁移 111 提供订单项级人工成本唯一约束与 subject 语义约 test('总账/对账/关账/平台退款能力已接入', async () => { const [migration, ledger, reconciliation, refund, manual] = await Promise.all([ - readFile(new URL('../../db/migrations/112_finance_ledger.sql', import.meta.url), 'utf8'), + readFile( + new URL('../../db/migrations/108_finance_cost_integrity.sql', import.meta.url), + 'utf8', + ), readFile(new URL('./finance-ledger-service.ts', import.meta.url), 'utf8'), readFile(new URL('./finance-reconciliation-service.ts', import.meta.url), 'utf8'), readFile(new URL('./order-refund-service.ts', import.meta.url), 'utf8'), diff --git a/docs/README.md b/docs/README.md index 1b9d4ab6..c0712bda 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,7 @@ # order_site 文档索引 -> 最后校准:2026-10-26,代码基线 `dev`(合并 `dev1023` + 人工成本完整性修复,迁移到 `112`;**未推送、未部署生产**)。 -> 数据基线:生产 dump `backups/postgres/order_site-20261022-092601.dump`(数据 2026-07-10 ~ 2026-10-22);本地生产副本已升级到 112,只跑了近 30 天增量。历史全量数字仅作基线,**上线验收以 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 为准**。 +> 最后校准:2026-10-26,代码基线 `dev`(合并 `dev1023` + 人工成本完整性修复,迁移基线收敛为 `103`–`111`;**未部署生产**)。 +> 数据基线:生产 dump `backups/postgres/order_site-20261022-092601.dump`(数据 2026-07-10 ~ 2026-10-22);测试/本地旧编号已到 115,生产按收敛后的 101 → 111 路径执行。历史全量数字仅作基线,**上线验收以 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 为准**。 > 本文是 `docs/` 的唯一入口。**财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;数据模型看 [成本中心与履约模型.md](成本中心与履约模型.md),接口看 [成本中心API契约.md](成本中心API契约.md)。 > 新增文档必须在下表登记状态;过期文档不再直接改写结论,统一移入 `docs/archive/`。 @@ -91,7 +91,7 @@ | 平台对账 / 期间关账 | **已实施 P0** | 平台账单 JSON 导入匹配、差异明细、标记解决;关账/反关账;见 `/admin/finance/ledger` | | 平台退款 API | **部分实施** | 快手行业(91)同意退款已接入;抖音/拼多多暂未接 API,仍手工登记 | -> 分支状态(2026-10-26):`dev` 已合并 `dev1023` 与人工成本完整性修复,迁移到 `112`;本地生产副本已验证 101 → 112(109/110 索引在生产规模数据上实测 < 0.3s),临时库全量执行与重跑幂等通过。**未推送、未部署生产**;生产口径仍以 `main` 为准。上线前按 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 初始化资金与人工 SKU。 +> 分支状态(2026-10-26):`dev` 已合并 `dev1023` 与人工成本完整性修复,迁移基线收敛为 `103`–`111`(原 108–115 的财务/性能补丁合并进 108);临时库从 001 全量执行与从 101 升级均待验收。**未推送、未部署生产**;生产口径仍以 `main` 为准。上线前按 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 初始化资金与人工 SKU。 > 财务与履约口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准。 ## 三、统一实施顺序 @@ -103,7 +103,7 @@ 2.5 人工成本链路收敛(确认履约 → 扣积分/备用金 → 写快照) 已完成 P0(迁移 104/107/111) 3. 人工商品归一与上线后人工成本全覆盖 已完成代码;生产数据需上线后配置 SKU(历史不补录) 4. 退款冲减与异常队列 已完成手工退款 + 快手平台退款;人工缺成本已入队 -5. 影子总账(销售事实 + 手工凭证) 已完成 P0(迁移 112) +5. 影子总账(销售事实 + 手工凭证) 已完成 P0(迁移 108) 6. 平台对账、期间关账 已完成 P0(手工账单导入);平台自动拉单未实施 ``` @@ -129,7 +129,7 @@ | 文档 | 状态 | 用途 | | --- | --- | --- | | [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) | **现行唯一口径** | 履约方式、成本模型、人工全覆盖、历史策略、实施顺序、运营日检与验收 SQL | -| [成本中心与履约模型.md](成本中心与履约模型.md) | 现行 | 商品、成本版本、快照、异常、销售事实的数据模型;迁移 `103`–`112`;履约回填;迁移基线与本地库同步;索引与性能 | +| [成本中心与履约模型.md](成本中心与履约模型.md) | 现行 | 商品、成本版本、快照、异常、销售事实的数据模型;迁移 `103`–`111`;履约回填;迁移基线与本地库同步;索引与性能 | | [成本中心API契约.md](成本中心API契约.md) | 现行 | 成本中心接口字段替换总表、枚举、模块边界(后端 5 模块 + 前端 4 个固定 Tab) | | [人工支出与备用金.md](人工支出与备用金.md) | 现行(P0 已实施) | 确认履约流程/权限、客服备用金台账、本人账户与编号规则 | | [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) | **现行上线手册** | 迁移后资金/人工 SKU 初始化、验收 SQL、日常监控 | diff --git a/docs/履约配置/多发货平台与电子凭证整体链路.md b/docs/履约配置/多发货平台与电子凭证整体链路.md index 1c8e1016..7351ffce 100644 --- a/docs/履约配置/多发货平台与电子凭证整体链路.md +++ b/docs/履约配置/多发货平台与电子凭证整体链路.md @@ -1,6 +1,6 @@ # 多发货平台与电子凭证整体链路 -> 状态:现行。最后校准:2026-10-23(代码基线 `dev`(合并 dev1023 后)+ 迁移 `103`–`112`;履约方式已统一落库)。 +> 状态:现行。最后校准:2026-10-23(代码基线 `dev`(合并 dev1023 后)+ 迁移 `103`–`111`;履约方式已统一落库)。 > 财务与履约总口径见 [财务与履约口径总纲-2026-10-23.md](../财务与履约口径总纲-2026-10-23.md)。 > 来源口径:本系统只有三个订单来源——`kuaishou-91`、`pdd`、`douyin-mashida`。**91 回调与快手 send-code 是同一个 `kuaishou-91` 来源的两条数据通道**(91=骨架权威源,快手 send-code=补全/兜底源),不是一个来源加另一个来源。 diff --git a/docs/影子总账说明.md b/docs/影子总账说明.md index d28dc4ee..e8dbb449 100644 --- a/docs/影子总账说明.md +++ b/docs/影子总账说明.md @@ -2,7 +2,7 @@ > 状态:**现行说明(P0 已实施,2026-10-26)** > 最后校准:2026-10-26 -> 代码基线:`dev`(合并 dev1023 后)+ 迁移 `103`–`112`(已合并到 `dev`,**未推送、未部署生产**) +> 代码基线:`dev`(合并 dev1023 后)+ 迁移 `103`–`111`(已合并到 `dev`,**未推送、未部署生产**) > 配套方案:[财务系统集成方案.md](财务系统集成方案.md) > **总口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准。** > 前置:统一履约事实、商品主数据、渠道映射、成本中心与销售事实;实施顺序以总纲第六节为准。 diff --git a/docs/成本中心API契约.md b/docs/成本中心API契约.md index 4475d47a..d2840c50 100644 --- a/docs/成本中心API契约.md +++ b/docs/成本中心API契约.md @@ -2,7 +2,7 @@ > 状态:**现行**。后端服务与前端页面都必须严格按此实现,不得各自发挥。 > 最后校准:2026-10-25 -> 代码基线:`dev`(合并 dev1023 后)+ 迁移 `103`–`112`(已合并到 `dev`,**未推送、未部署生产**) +> 代码基线:`dev`(合并 dev1023 后)+ 迁移 `103`–`111`(已合并到 `dev`,**未推送、未部署生产**) > 详细业务口径见 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md);数据模型见 [成本中心与履约模型.md](成本中心与履约模型.md)。 ## 一、字段替换总表 diff --git a/docs/成本中心与履约模型.md b/docs/成本中心与履约模型.md index da524550..c41d9ef3 100644 --- a/docs/成本中心与履约模型.md +++ b/docs/成本中心与履约模型.md @@ -2,7 +2,7 @@ > 状态:**现行**。商品主数据、成本中心、成本快照、销售事实、履约事实均已落地。 > 最后校准:2026-10-26 -> 代码基线:`dev`(合并 dev1023 + 人工成本完整性修复,迁移到 `112`) +> 代码基线:`dev`(合并 dev1023 + 人工成本完整性修复,迁移基线收敛为 `103`–`111`) > 数据基线:生产 dump `order_site-20261022-092601.dump`(2026-07-10 ~ 2026-10-22);本地只跑了近 30 天增量,历史数字仅作基线。 > 上线初始化/验收见 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md)。 > **业务口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;本文只负责数据模型、迁移与运维。 @@ -35,7 +35,7 @@ orders ──┬── order_items ──── fulfillment_mode (唯一履约 ## 一、迁移基线:10 个收敛为 5 个 -原 `dev` 分支上,商品、成本中心、人工履约用 10 个**历史编号**迁移(`103`–`112`,1,275 行)分批落地,字段反复摇摆。其中: +原 `dev` 分支上,商品、成本中心、人工履约用 10 个**历史编号**迁移(`103`–`111`,1,275 行)分批落地,字段反复摇摆。其中: - 原 `108`(下线人工支出登记)与原 `109`(隔离历史快照主体)在「生产 102 → 当前」的唯一升级路径上是**空操作**; - 原 `111` 是给 `104` 自身种子打的补丁(draft 占位改 active 占位); @@ -58,8 +58,8 @@ orders ──┬── order_items ──── fulfillment_mode (唯一履约 | `108_cost_exception_archive.sql` | 成本异常 60 天归档 | | `109_work_order_share_participation.sql` | 拼单多次参与份额模型(与本模块无关,同批上线) | | `110_work_order_hall_and_shop_indexes.sql` | 大厅置顶/店铺筛选索引(与本模块无关) | -| `111_manual_cost_integrity.sql` | 人工成本单事务兜底:订单项级唯一、`subject_id=order_item_id`、人工 SKU 匹配索引、`manual_cost_missing` 监控所需索引 | -| `112_finance_ledger.sql` | 财务总账(科目/凭证/期间/对账),与本模块无直接依赖,同批上线 | +| `111_work_order_share_cancel_reason_comment.sql` | 拼单取消原因注释(与本模块无关) | +| `108_finance_cost_integrity.sql` | 成本异常归档 + 人工成本单事务兜底(订单项级唯一、`subject_id=order_item_id`、SKU 索引)+ 财务总账/期间/对账 + 商品页/模板查询索引(合并原 108/111/112/113/114) | **合并原则**:同一批上线的结构性 DDL 才合并;数据回填与业务口径注释跟着所属表走。`103` 保持独立(商品主数据是成本与销售事实的共同上游),`106` 保持独立(退款是独立业务线)。 @@ -428,18 +428,22 @@ WHERE oi.id = d.id --- -## 八、迁移基线与本地库同步(2026-10-25 更新) +## 八、迁移基线与本地库同步(2026-10-27 收敛) -生产尚未执行过 `103` 及之后的迁移。本地库与代码对齐有两条路径: +生产尚未执行过 `103` 及之后的迁移。2026-10-27 已把原来的 `108/111/112/113/114` 合并为 `108_finance_cost_integrity`,原 `115` 注释迁移改为 `111`;生产升级统一走 **101 → 111**,不再出现 108–115 的补丁链。 -1. **全新升级**:从生产 dump 恢复到 `101` / `102`,直接 `npm run db:migrate` 升到 `111`; -2. **已有本地库**:清掉 `schema_migrations` 里 `103`–`112` 的记录后重跑。所有迁移都是 `IF NOT EXISTS`,数据步骤幂等: +1. **全新升级**:从生产 dump 恢复到 `101`,直接 `npm run db:migrate` 升到 `111`; +2. **已跑过旧 112–115 的本地/测试库**:schema 已等价,不要删 103–111 重跑;执行一次迁移历史校准: -```sql -DELETE FROM schema_migrations WHERE name ~ '^(10[3-9]|11[0-1])_'; +```bash +docker exec -i order_site-postgres-1 psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ + -v ON_ERROR_STOP=1 < apps/backend/scripts/reconcile-migration-history.sql +npm run db:migrate ``` -2026-10-25 实测:按第 2 条路径升级后的库,与全新构建的库 **schema 完全一致**(`pg_dump --schema-only` 仅随机 token 不同),种子数据(3 个系统成本项、2 个全局占位版本、4 个人工履约档案)也一致。 +脚本只把 `schema_migrations` 的旧 `108–115` 行重建为新的 `108–111`,不重复执行 DDL;若库仍停留在旧 103–107,直接正常迁移即可。 + +2026-10-27 实测:旧 115 库与新 111 库 `pg_dump --schema-only` **完全一致**(仅 pg_dump 随机 token/版本注释不同);新库 001→111 与 101→111 两条路径均通过。 旧的逐列改造脚本 `scripts/rewrite-local-db-2026-10-23.sql` 已随迁移合并作废,移入 [archive/2026-10-23/](archive/2026-10-23/);只在追溯当时 `carrier_type → cost_model` / `fulfillment_mode` 的改造矩阵时查阅,**不要在新环境执行**。 diff --git a/docs/接单模板匹配引擎重构设计.md b/docs/接单模板匹配引擎重构设计.md index e6ab3d9a..cf58b649 100644 --- a/docs/接单模板匹配引擎重构设计.md +++ b/docs/接单模板匹配引擎重构设计.md @@ -1,6 +1,6 @@ # 接单模板匹配引擎重构设计 -> 目标读者:后端 / 后台运营。状态:**P0 + P1 已实施;P2 / P3 待办**。最后校准:2026-10-23(代码基线 `dev`(合并 dev1023 后)+ 迁移 `103`–`112`;匹配业务逻辑未变)。 +> 目标读者:后端 / 后台运营。状态:**P0 + P1 已实施;P2 / P3 待办**。最后校准:2026-10-23(代码基线 `dev`(合并 dev1023 后)+ 迁移 `103`–`111`;匹配业务逻辑未变)。 > > 一句话:把「订单信息 → 接单模板(价格/拼单/要求)」的匹配,从散落的多个消费点、平台判断和"店铺必填"的绑定表里,收敛为**一个平台无关的引擎**:匹配条件完全由**订单信息**表达,**店铺只是可选条件之一**,新增来源只加一个适配器 + 一组配置。 > diff --git a/docs/财务上线初始化与验收-2026-10-26.md b/docs/财务上线初始化与验收-2026-10-26.md index f6cd2cfb..85f8cab0 100644 --- a/docs/财务上线初始化与验收-2026-10-26.md +++ b/docs/财务上线初始化与验收-2026-10-26.md @@ -1,7 +1,7 @@ # 财务/成本上线初始化与验收(2026-10-26) > 状态:现行操作手册。适用于 `dev` 合并后的财务/成本模块上线;**历史人工成本不补录**。 -> 代码基线:迁移 `103`–`112`(本手册写于人工成本完整性与配置自动匹配修复之后)。 +> 代码基线:迁移 `103`–`111`(本手册写于人工成本完整性与配置自动匹配修复之后)。 > 数据基线:生产 dump `2026-10-22`(迁移 `101`)。历史人工支出找不回来,只保证**首次配置人工履约之后**的新单可记录、可监控。 --- @@ -42,7 +42,7 @@ bash deploy/ubuntu-deploy.sh --backup # 迁移前 pg_dump + OSS, ```sql SELECT name FROM schema_migrations ORDER BY id DESC LIMIT 5; --- 期望包含:111_manual_cost_integrity / 110_work_order_hall_and_shop_indexes / 109_work_order_share_participation +-- 期望包含:111_work_order_share_cancel_reason_comment / 110_work_order_hall_and_shop_indexes / 109_work_order_share_participation / 108_finance_cost_integrity ``` > 迁移 `109`/`110`/`111` 都是增量、幂等;`111` 会给 `orders(375k)` 建表达式索引,实测写阻塞约 200–300ms。 @@ -266,7 +266,7 @@ GROUP BY 1; --- -## 四、总账、期间关账与平台对账(迁移 112) +## 四、总账、期间关账与平台对账(迁移 108) 入口:后台「财务 → 总账与对账」(`/admin/finance/ledger`)。 diff --git a/docs/财务与履约口径总纲-2026-10-23.md b/docs/财务与履约口径总纲-2026-10-23.md index 623d9b45..b92ae249 100644 --- a/docs/财务与履约口径总纲-2026-10-23.md +++ b/docs/财务与履约口径总纲-2026-10-23.md @@ -2,7 +2,7 @@ > 状态:**现行唯一口径**。本文是财务、成本、履约分类的总入口;其他文档与本文件冲突时,以本文为准。 > 最后校准:2026-10-26 -> 代码基线:`dev`(合并 dev1023 + 人工成本完整性修复,迁移到 `112`) +> 代码基线:`dev`(合并 dev1023 + 人工成本完整性修复,迁移到 `111`) > 数据基线:§二/§七 的数字来自 2026-10-22 生产 dump 的**历史快照**,只用于理解历史缺口,**不再作为上线验收依据**; > 上线初始化与验收以 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 为准。 > 适用范围:订单接入、履约判定、商品归一、成本快照、销售事实、人工录单、备用金、后续总账与对账。 @@ -181,7 +181,7 @@ P0 的具体迁移、回填、代码清单与验收 SQL 见 [成本中心与履 | 影子总账、对账、关账 | **已实施 P0** | 迁移 `112`;销售事实按日凭证 + 手工凭证 + 试算平衡 + 账单导入对账 + 关账/反关账 | > 历史平台仍有 53,901 个订单项(¥389.12 万)未归一,不属于 P1 范围;人工历史成本按既定口径不补录,因此人工毛利当前是“收入已归商品、成本仅新单有”的状态。 -> 分支事实:以上实现已合并到 `dev`;迁移 `103`–`112` **未推送远端、未部署生产**。生产口径仍以 `main` 为准;上线前按 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 初始化资金与人工 SKU。 +> 分支事实:以上实现已合并到 `dev`;迁移 `103`–`111` **未推送远端、未部署生产**。生产口径仍以 `main` 为准;上线前按 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 初始化资金与人工 SKU。 --- diff --git a/docs/财务系统集成方案.md b/docs/财务系统集成方案.md index 482a2f62..dd7e8d1e 100644 --- a/docs/财务系统集成方案.md +++ b/docs/财务系统集成方案.md @@ -2,7 +2,7 @@ > 状态:**现行方案(影子总账 P0 已实施,2026-10-26)**。`finance_accounts / journal_entries / journal_lines / periods / reconciliations` 已建;总账不接管钱包余额;对账当前为手工账单导入,平台自动拉单待接。 > 最后校准:2026-10-26 -> 代码基线:`dev`(迁移到 `112`,**未推送、未部署生产**) +> 代码基线:`dev`(迁移到 `111`,**未推送、未部署生产**) > 说明:本文是总账/对账/关账的目标架构与分期;当前已实施 P0(销售事实凭证、手工凭证、试算平衡、期间关账、手工账单对账)与快手平台退款。初始化步骤见 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md)。 > **履约方式、成本模型、人工全覆盖口径以 [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) 为准**;本文不重复分类设计。 > 目标:在**不破坏现有抢单、押金、结算、寄卖、提现链路**的前提下,增加可对账、可关账、可追溯的复式记账底座。 @@ -21,8 +21,8 @@ | `sales_daily_facts` / 订单级毛利 | **已实施 P0(`dev` 分支)**,迁移 `105`;94 天收入 ¥2,650.87 万、成本 ¥950.27 万 | | 人工成本录入(确认履约 → 扣积分/备用金 → 写快照) | **已实施 P0(`dev` 分支)**,迁移 `104/107`;2026-10-24 人工履约页下线、入口收敛到订单页;**无登记单、无审批、无报销** | | 履约方式统一 `fulfillment_mode` | **已实施 P0(`dev` 分支)**;迁移 `105`,316,585 项已回填;订单页、成本快照、销售事实统一读该字段 | -| `finance_accounts` / `finance_journal_entries` / `finance_journal_lines` | **已实施 P0(迁移 112)** | -| `finance_periods` / `finance_reconciliations` / `finance_reconciliation_items` | **已实施 P0(迁移 112)** | +| `finance_accounts` / `finance_journal_entries` / `finance_journal_lines` | **已实施 P0(迁移 108)** | +| `finance_periods` / `finance_reconciliations` / `finance_reconciliation_items` | **已实施 P0(迁移 108)** | | `finance_business_events` / `finance_settlements` | 未实施(当前用条目唯一键幂等) | | 现有 `admin-finance-service.ts` | 仍是业务事件 `UNION ALL`,不是总账 | diff --git a/docs/部署启动与数据库重建.md b/docs/部署启动与数据库重建.md index a86370d1..b92f27c9 100644 --- a/docs/部署启动与数据库重建.md +++ b/docs/部署启动与数据库重建.md @@ -219,7 +219,7 @@ docker compose ps docker compose logs -f backend ``` -## 财务/成本上线后的初始化(迁移 102–112) +## 财务/成本上线后的初始化(迁移 102–111) 代码与迁移只负责建表;**备用金账户、积分余额、人工 SKU 配置不会自动创建**。 首次上线后按 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 执行: