feat(finance): 完成成本快照运行层、销售事实与人工支出登记
迁移 108-111:快照业务时间与预估转实际、销售事实表、系统成本项、公司人工支出登记。接单平台按工单实际 reward 入账;人工支出支持登记/审批/驳回/报销并写 actual 快照。新增销售毛利页、人工支出页与订单入口。
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
-- 108_cost_snapshot_runtime.sql —— 成本快照进入运行层。
|
||||
--
|
||||
-- 目标:
|
||||
-- 1. 成本版本按业务发生时间(订单支付/工单验收)解析,而不是迁移或回填时的 now();
|
||||
-- 2. 预估值可以在结算时被实际值替代,替代后旧快照保留但不再参与汇总;
|
||||
-- 3. 订单项固化收入金额,销售事实可以按订单项归集,不再直接拿整单金额。
|
||||
--
|
||||
-- 口径(2026-10-22 确认):
|
||||
-- * 收入按 paid_at 归属销售日;成本版本按 paid_at / accepted_at 解析;
|
||||
-- * 一单一品时订单收入全部归该订单项;多订单项按等额拆分,余数给最后一项,
|
||||
-- 并把 revenue_basis 标成 equal_split,后续有平台明细时再替换;
|
||||
-- * 历史快照不删除,只把被替代行标记 superseded_at。
|
||||
|
||||
ALTER TABLE cost_snapshots
|
||||
ADD COLUMN IF NOT EXISTS cost_basis_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS superseded_at TIMESTAMPTZ;
|
||||
|
||||
-- 已存在的快照发生在旧逻辑的 now(),没有更好的业务时间,先用 resolved_at 兜底。
|
||||
UPDATE cost_snapshots
|
||||
SET cost_basis_at = resolved_at
|
||||
WHERE cost_basis_at IS NULL;
|
||||
|
||||
ALTER TABLE cost_snapshots
|
||||
ALTER COLUMN cost_basis_at SET DEFAULT NOW();
|
||||
|
||||
-- 同一订单项/工单/人工单的同一成本项,只允许一条“当前有效”快照。
|
||||
DROP INDEX IF EXISTS uq_cost_snapshots_subject_item;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_snapshots_active_subject_item
|
||||
ON cost_snapshots (subject_type, subject_id, cost_item_id)
|
||||
WHERE superseded_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cost_snapshots_active_subject
|
||||
ON cost_snapshots (subject_type, subject_id)
|
||||
WHERE superseded_at IS NULL;
|
||||
|
||||
COMMENT ON COLUMN cost_snapshots.cost_basis_at IS
|
||||
'解析成本版本时使用的业务时间:订单支付时间/工单验收时间/人工完成时间';
|
||||
COMMENT ON COLUMN cost_snapshots.superseded_at IS
|
||||
'被更新的实际成本替代的时间;非空行只保留历史,不再参与成本汇总';
|
||||
|
||||
ALTER TABLE order_items
|
||||
ADD COLUMN IF NOT EXISTS revenue_amount_fen BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS revenue_basis TEXT NOT NULL DEFAULT 'unknown';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
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 >= 0);
|
||||
END IF;
|
||||
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 ('order_total', 'equal_split', 'platform_item', 'manual', 'unknown'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 历史订单回填:当前生产快照全部是一单一品,仍按通用规则处理多订单项。
|
||||
WITH item_counts AS (
|
||||
SELECT order_id, COUNT(*)::int AS item_count
|
||||
FROM order_items
|
||||
GROUP BY order_id
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
oi.id,
|
||||
GREATEST(COALESCE(o.total_amount, 0), 0)::bigint AS order_revenue_fen,
|
||||
ic.item_count,
|
||||
ROW_NUMBER() OVER (PARTITION BY oi.order_id ORDER BY oi.id ASC) AS item_rank
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
JOIN item_counts ic ON ic.order_id = oi.order_id
|
||||
)
|
||||
UPDATE order_items oi
|
||||
SET
|
||||
revenue_amount_fen = CASE
|
||||
WHEN r.item_count = 1 THEN r.order_revenue_fen
|
||||
ELSE (r.order_revenue_fen / r.item_count)
|
||||
+ CASE WHEN r.item_rank = r.item_count THEN (r.order_revenue_fen % r.item_count) ELSE 0 END
|
||||
END,
|
||||
revenue_basis = CASE WHEN r.item_count = 1 THEN 'order_total' ELSE 'equal_split' END,
|
||||
updated_at = NOW()
|
||||
FROM ranked r
|
||||
WHERE oi.id = r.id
|
||||
AND oi.revenue_basis = 'unknown';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_revenue_basis
|
||||
ON order_items (revenue_basis);
|
||||
|
||||
COMMENT ON COLUMN order_items.revenue_amount_fen IS
|
||||
'订单项收入(分):一单一品=订单实付;多品=当前按等额拆分,basis 标明口径';
|
||||
COMMENT ON COLUMN order_items.revenue_basis IS
|
||||
'order_total=整单归属于唯一订单项;equal_split=多订单项等额拆分;platform_item=平台明细;manual/unknown=未定';
|
||||
@@ -0,0 +1,96 @@
|
||||
-- 109_sales_daily_facts.sql —— 店铺 × 日期 × 标准商品 × 履约载体的销售事实。
|
||||
--
|
||||
-- 口径(2026-10-22 确认):
|
||||
-- * 销售日 = orders.paid_at 的 Asia/Shanghai 日期;没有 paid_at 时退回 created_at;
|
||||
-- * 收入 = order_items.revenue_amount_fen(迁移 108 固化),不再直接拿整单金额;
|
||||
-- * 未归一商品允许 canonical_product_id 为空,单独成“未归一”行,保证总收入能对平;
|
||||
-- * 成本来自 cost_snapshots 当前有效行;没有快照的订单项计入 cost_missing_count;
|
||||
-- * 退款暂未接入,refund_amount_fen 先按 pay_status=refunded 预留。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sales_daily_facts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
stat_date DATE NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
shop_id TEXT NOT NULL DEFAULT '',
|
||||
canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
|
||||
carrier_type TEXT NOT NULL,
|
||||
order_count INTEGER NOT NULL DEFAULT 0,
|
||||
item_count INTEGER NOT NULL DEFAULT 0,
|
||||
quantity NUMERIC(14, 3) NOT NULL DEFAULT 0,
|
||||
gross_revenue_fen BIGINT NOT NULL DEFAULT 0,
|
||||
refund_amount_fen BIGINT NOT NULL DEFAULT 0,
|
||||
net_revenue_fen BIGINT NOT NULL DEFAULT 0,
|
||||
cost_amount_fen BIGINT NOT NULL DEFAULT 0,
|
||||
cost_missing_count INTEGER NOT NULL DEFAULT 0,
|
||||
cost_stage TEXT NOT NULL DEFAULT 'missing',
|
||||
gross_profit_fen BIGINT NOT NULL DEFAULT 0,
|
||||
computed_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
-- canonical_product_id 为空时唯一索引仍要生效,因此用 COALESCE(..., 0) 作为“未归一”桶。
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_sales_daily_facts_key
|
||||
ON sales_daily_facts (
|
||||
stat_date,
|
||||
provider,
|
||||
shop_id,
|
||||
COALESCE(canonical_product_id, 0),
|
||||
carrier_type
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_date
|
||||
ON sales_daily_facts (stat_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_shop
|
||||
ON sales_daily_facts (provider, shop_id, stat_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_product
|
||||
ON sales_daily_facts (canonical_product_id, stat_date DESC);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_carrier'
|
||||
) THEN
|
||||
ALTER TABLE sales_daily_facts
|
||||
ADD CONSTRAINT chk_sales_daily_facts_carrier
|
||||
CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'manual', 'shared', 'unknown'));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_cost_stage'
|
||||
) THEN
|
||||
ALTER TABLE sales_daily_facts
|
||||
ADD CONSTRAINT chk_sales_daily_facts_cost_stage
|
||||
CHECK (cost_stage IN ('missing', 'partial', 'estimated', 'actual'));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_amounts'
|
||||
) THEN
|
||||
ALTER TABLE sales_daily_facts
|
||||
ADD CONSTRAINT chk_sales_daily_facts_amounts
|
||||
CHECK (
|
||||
order_count >= 0
|
||||
AND item_count >= 0
|
||||
AND quantity >= 0
|
||||
AND gross_revenue_fen >= 0
|
||||
AND refund_amount_fen >= 0
|
||||
AND net_revenue_fen >= 0
|
||||
AND cost_amount_fen >= 0
|
||||
AND cost_missing_count >= 0
|
||||
);
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_profit'
|
||||
) THEN
|
||||
ALTER TABLE sales_daily_facts
|
||||
ADD CONSTRAINT chk_sales_daily_facts_profit
|
||||
CHECK (gross_profit_fen = net_revenue_fen - cost_amount_fen);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE sales_daily_facts IS
|
||||
'店铺×日期(Asia/Shanghai)×标准商品×履约载体的销售与成本事实;报表直接读这张表';
|
||||
COMMENT ON COLUMN sales_daily_facts.canonical_product_id IS
|
||||
'标准商品;为空表示渠道映射未命中,报表单独展示“未归一”占比';
|
||||
COMMENT ON COLUMN sales_daily_facts.cost_stage IS
|
||||
'missing=全部无成本快照;partial=部分无快照;estimated=全部预估;actual=全部实际';
|
||||
COMMENT ON COLUMN sales_daily_facts.cost_missing_count IS
|
||||
'没有当前有效成本快照的订单项数;>0 时该行毛利不可直接用于决策';
|
||||
@@ -0,0 +1,95 @@
|
||||
-- 110_system_cost_items.sql —— 内置接单平台实际报酬与人工客服成本项。
|
||||
--
|
||||
-- 背景:接单平台的成本金额在验收时已经由 work_orders.reward_amount 确定,
|
||||
-- 不需要财务再按模板猜一个预估价;人工客服成本则必须由人工支出登记给出。
|
||||
--
|
||||
-- 口径:
|
||||
-- 1. sys_work_order_reward 由系统维护,预估阶段不参与;工单验收后按实际
|
||||
-- reward_amount 直接写 actual 快照。版本金额 0 只是占位,不参与汇总。
|
||||
-- 2. sys_manual_service 由人工支出登记使用,先建草稿版本占位;
|
||||
-- 财务填写真实按单金额并启用后,才参与成本解析。
|
||||
|
||||
INSERT INTO cost_items (
|
||||
code, name, cost_type, carrier_type, pricing_mode, unit_name, currency,
|
||||
status, note, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'sys_work_order_reward',
|
||||
'接单平台打手报酬(实际结算)',
|
||||
'work_order_reward',
|
||||
'work_order',
|
||||
'per_order',
|
||||
'单',
|
||||
'CNY',
|
||||
'active',
|
||||
'系统内置:实际金额取工单 reward_amount;版本金额仅占位,不参与预估成本',
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (code) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
cost_type = EXCLUDED.cost_type,
|
||||
carrier_type = EXCLUDED.carrier_type,
|
||||
pricing_mode = EXCLUDED.pricing_mode,
|
||||
note = EXCLUDED.note,
|
||||
updated_at = NOW();
|
||||
|
||||
INSERT INTO cost_items (
|
||||
code, name, cost_type, carrier_type, pricing_mode, unit_name, currency,
|
||||
status, note, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
'sys_manual_service',
|
||||
'人工客服履约成本',
|
||||
'manual_service',
|
||||
'manual',
|
||||
'per_order',
|
||||
'单',
|
||||
'CNY',
|
||||
'active',
|
||||
'系统内置:由公司人工支出登记写入 actual 快照;草稿版本待财务填写实际单价',
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (code) DO UPDATE
|
||||
SET
|
||||
name = EXCLUDED.name,
|
||||
cost_type = EXCLUDED.cost_type,
|
||||
carrier_type = EXCLUDED.carrier_type,
|
||||
pricing_mode = EXCLUDED.pricing_mode,
|
||||
note = EXCLUDED.note,
|
||||
updated_at = NOW();
|
||||
|
||||
-- 接单平台:active 占位版本,供 cost_snapshots.cost_version_id 引用。
|
||||
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,
|
||||
status, change_reason, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
ci.id, 'global', '', '', NULL, NULL, 0, 0,
|
||||
'1970-01-01T00:00:00+08:00', 'manual', 'active',
|
||||
'系统占位:接单平台实际金额取工单 reward_amount',
|
||||
NOW(), NOW()
|
||||
FROM cost_items ci
|
||||
WHERE ci.code = 'sys_work_order_reward'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 人工客服:draft 版本,避免在财务未填写金额时静默按 0 计成本。
|
||||
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,
|
||||
status, change_reason, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
ci.id, 'global', '', '', NULL, NULL, 0, 0,
|
||||
'1970-01-01T00:00:00+08:00', 'manual', 'draft',
|
||||
'草稿占位:待财务填写人工客服实际按单成本后启用',
|
||||
NOW(), NOW()
|
||||
FROM cost_items ci
|
||||
WHERE ci.code = 'sys_manual_service'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
COMMENT ON COLUMN cost_items.code IS
|
||||
'成本项编码;sys_ 前缀为系统内置项,业务侧不要删除或改编码';
|
||||
@@ -0,0 +1,161 @@
|
||||
-- 111_manual_expense_records.sql —— 公司人工支出/垫付登记。
|
||||
--
|
||||
-- 与迁移 100 的 `manual_expenses` 严格区分:
|
||||
-- manual_expenses = 客服备用金支出明细(减少客服余额)
|
||||
-- manual_expense_records = 公司口径人工支出/垫付登记(谁为哪个订单付了多少钱)
|
||||
-- 两者通过 fund_expense_id 关联,不合并、不改名、不共用索引名。
|
||||
--
|
||||
-- 本轮只做登记、审批、报销与冲正预留;影子总账分录在 finance_* 阶段接入。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS manual_expense_records (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
expense_no TEXT NOT 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,
|
||||
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,
|
||||
currency TEXT NOT NULL DEFAULT 'CNY',
|
||||
paid_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
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,
|
||||
rejected_by TEXT NOT NULL DEFAULT '',
|
||||
rejected_at TIMESTAMPTZ,
|
||||
reject_reason TEXT NOT NULL DEFAULT '',
|
||||
reimbursed_by TEXT NOT NULL DEFAULT '',
|
||||
reimbursed_at TIMESTAMPTZ,
|
||||
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,
|
||||
expense_batch_no TEXT NOT NULL DEFAULT '',
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
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_work_order
|
||||
ON manual_expense_records (work_order_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 INDEX IF NOT EXISTS idx_manual_expense_records_payer
|
||||
ON manual_expense_records (paid_by_admin_user_id, paid_at DESC);
|
||||
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_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_expense_records_amount'
|
||||
) THEN
|
||||
ALTER TABLE manual_expense_records
|
||||
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_expense_records_status'
|
||||
) THEN
|
||||
ALTER TABLE manual_expense_records
|
||||
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_expense_records_payee_type'
|
||||
) THEN
|
||||
ALTER TABLE manual_expense_records
|
||||
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_expense_records_method'
|
||||
) THEN
|
||||
ALTER TABLE manual_expense_records
|
||||
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_expense_records_category'
|
||||
) THEN
|
||||
ALTER TABLE manual_expense_records
|
||||
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_expense_records_approved'
|
||||
) THEN
|
||||
ALTER TABLE manual_expense_records
|
||||
ADD CONSTRAINT chk_manual_expense_records_approved
|
||||
CHECK (
|
||||
status NOT IN ('approved', 'reimbursed')
|
||||
OR (approved_at IS NOT NULL AND approved_by <> '')
|
||||
);
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expense_records_rejected'
|
||||
) THEN
|
||||
ALTER TABLE manual_expense_records
|
||||
ADD CONSTRAINT chk_manual_expense_records_rejected
|
||||
CHECK (
|
||||
status <> 'rejected'
|
||||
OR (rejected_at IS NOT NULL AND rejected_by <> '' AND reject_reason <> '')
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
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.evidence_json IS
|
||||
'凭证:支付截图对象 key、流水号、聊天记录 key 等';
|
||||
COMMENT ON COLUMN manual_expense_records.reversal_of_id IS
|
||||
'预留:退款/仲裁接入后按反向登记冲正,禁止 UPDATE 已通过记录';
|
||||
|
||||
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 '',
|
||||
actor_name 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);
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_expense_events_type
|
||||
ON manual_expense_events (event_type, created_at DESC);
|
||||
|
||||
COMMENT ON TABLE manual_expense_events IS
|
||||
'人工支出状态变更审计:只追加,不更新、不删除';
|
||||
Reference in New Issue
Block a user