feat(finance): 支持代录指定客服账户,并上线影子总账/对账/关账/快手平台退款
- 人工确认履约:管理员/财务/运营可传 fundAccountId / creditOwnerAdminUserId 指定客服备用金/积分账户;客服仍强制本人账户 - 新增迁移 112:finance_accounts / journal_entries / journal_lines / periods / reconciliations / reconciliation_items,种子 11 个科目 - 影子总账:销售事实按日生成收入/退款/成本凭证,支持手工凭证、试算平衡; 不接管钱包余额;总账同步接入定时任务与退款重算 - 期间关账/反关账:关账先补凭证并校验试算平衡,closed 期间禁止新增凭证 - 平台对账:JSON 账单导入,按 provider + platform_order_id 匹配订单/退款, 标记 matched/amount_mismatch/missing_order/missing_refund/duplicate - 平台退款:快手行业(91)同意退款 API 接入,成功后落 source=platform; 抖音/拼多多返回 501,保留手工退款 - 新增后台「财务 → 总账与对账」页面(总账/期间/对账三个 Tab) - 集成测试扩展到代录、总账平衡、关账阻断、手工凭证、对账匹配 验证:backend check 844(840 pass / 4 skip);frontend check 172 + build; 迁移 112 在 scratch 库全量执行并重跑幂等;本地生产副本已升级到 112, 销售事实同步 56 条凭证,借贷合计 1,885,351,864 = 1,885,351,864。
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
-- 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';
|
||||
Reference in New Issue
Block a user