feat: 新增客服备用金台账与后台用户中文显示名
- 新增备用金账户、流水、支出、对账与权限 - 后台用户支持中文显示名并同步备用金姓名 - 审计、登录记录、任务及工单操作人优先展示中文名 - 补充备用金与人工支出方案文档
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
-- 100_fund_accounts.sql —— 客服备用金台账(主管池 / 客服余额 / 支出 / 对账)。
|
||||
--
|
||||
-- 业务口径(2026-10-18 确认):
|
||||
-- 公司把钱交给主管,主管发给客服;客服用于充值/采购/赔偿;充值后立即消耗,直接记支出。
|
||||
-- 系统要回答:钱在谁手里、每笔花到哪、每个客服还剩多少、账实是否对平。
|
||||
--
|
||||
-- 设计约束:
|
||||
-- 1. 余额只能由 fund_entries 产生,不允许手工改;
|
||||
-- 2. 发放/缴回必须同时写主管与客服两条流水,金额相反;
|
||||
-- 3. 支出必须同时写客服负数流水和 manual_expenses 明细;
|
||||
-- 4. 已记账流水不修改、不删除,差异走 adjustment。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fund_accounts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
account_no TEXT NOT NULL,
|
||||
account_type TEXT NOT NULL,
|
||||
owner_admin_user_id BIGINT NOT NULL REFERENCES admin_users(id) ON DELETE RESTRICT,
|
||||
owner_name TEXT NOT NULL DEFAULT '',
|
||||
responsible_supervisor_id BIGINT REFERENCES fund_accounts(id) ON DELETE SET NULL,
|
||||
balance_fen BIGINT NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_fund_accounts_account_no
|
||||
ON fund_accounts (account_no);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_fund_accounts_owner_type
|
||||
ON fund_accounts (owner_admin_user_id, account_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_fund_accounts_type_status
|
||||
ON fund_accounts (account_type, status, owner_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_fund_accounts_supervisor
|
||||
ON fund_accounts (responsible_supervisor_id, status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fund_entries (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
entry_no TEXT NOT NULL,
|
||||
account_id BIGINT NOT NULL REFERENCES fund_accounts(id) ON DELETE RESTRICT,
|
||||
entry_type TEXT NOT NULL,
|
||||
amount_fen BIGINT NOT NULL,
|
||||
balance_after_fen BIGINT NOT NULL,
|
||||
transfer_no TEXT NOT NULL DEFAULT '',
|
||||
counterparty_account_id BIGINT REFERENCES fund_accounts(id) ON DELETE SET NULL,
|
||||
reconciliation_id BIGINT,
|
||||
occurred_at TIMESTAMPTZ NOT NULL,
|
||||
created_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
evidence_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_fund_entries_entry_no
|
||||
ON fund_entries (entry_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_fund_entries_account_time
|
||||
ON fund_entries (account_id, occurred_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_fund_entries_type_time
|
||||
ON fund_entries (entry_type, occurred_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_fund_entries_transfer
|
||||
ON fund_entries (transfer_no) WHERE transfer_no <> '';
|
||||
CREATE INDEX IF NOT EXISTS idx_fund_entries_counterparty
|
||||
ON fund_entries (counterparty_account_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS manual_expenses (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
expense_no TEXT NOT NULL,
|
||||
account_id BIGINT NOT NULL REFERENCES fund_accounts(id) ON DELETE RESTRICT,
|
||||
amount_fen BIGINT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
payee_type TEXT NOT NULL,
|
||||
payee_name TEXT NOT NULL,
|
||||
payee_account TEXT NOT NULL DEFAULT '',
|
||||
platform_name TEXT NOT NULL DEFAULT '',
|
||||
platform_account TEXT NOT NULL DEFAULT '',
|
||||
order_id BIGINT REFERENCES orders(id) ON DELETE SET NULL,
|
||||
order_item_id BIGINT REFERENCES order_items(id) ON DELETE SET NULL,
|
||||
paid_at TIMESTAMPTZ NOT NULL,
|
||||
evidence_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
fund_entry_id BIGINT NOT NULL REFERENCES fund_entries(id) ON DELETE RESTRICT,
|
||||
created_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_expenses_expense_no
|
||||
ON manual_expenses (expense_no);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_expenses_fund_entry
|
||||
ON manual_expenses (fund_entry_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_expenses_account_time
|
||||
ON manual_expenses (account_id, paid_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_expenses_category_time
|
||||
ON manual_expenses (category, paid_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_expenses_order
|
||||
ON manual_expenses (order_id) WHERE order_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_manual_expenses_payee
|
||||
ON manual_expenses (payee_name, paid_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fund_reconciliations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
reconciliation_no TEXT NOT NULL,
|
||||
account_id BIGINT NOT NULL REFERENCES fund_accounts(id) ON DELETE RESTRICT,
|
||||
period_start TIMESTAMPTZ NOT NULL,
|
||||
period_end TIMESTAMPTZ NOT NULL,
|
||||
system_balance_fen BIGINT NOT NULL,
|
||||
actual_balance_fen BIGINT NOT NULL,
|
||||
difference_fen BIGINT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
evidence_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
confirmed_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
confirmed_at TIMESTAMPTZ,
|
||||
created_by_admin_user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_fund_reconciliations_no
|
||||
ON fund_reconciliations (reconciliation_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_fund_reconciliations_account_time
|
||||
ON fund_reconciliations (account_id, period_end DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_fund_reconciliations_status_time
|
||||
ON fund_reconciliations (status, period_end DESC);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_fund_accounts_type') THEN
|
||||
ALTER TABLE fund_accounts
|
||||
ADD CONSTRAINT chk_fund_accounts_type
|
||||
CHECK (account_type IN ('supervisor', 'agent'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_fund_accounts_status') THEN
|
||||
ALTER TABLE fund_accounts
|
||||
ADD CONSTRAINT chk_fund_accounts_status
|
||||
CHECK (status IN ('active', 'frozen', 'closed'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_fund_accounts_balance') THEN
|
||||
ALTER TABLE fund_accounts
|
||||
ADD CONSTRAINT chk_fund_accounts_balance
|
||||
CHECK (balance_fen >= 0);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_fund_entries_type') THEN
|
||||
ALTER TABLE fund_entries
|
||||
ADD CONSTRAINT chk_fund_entries_type
|
||||
CHECK (entry_type IN ('opening', 'funding', 'issue', 'expense', 'return', 'adjustment'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_fund_entries_amount') THEN
|
||||
ALTER TABLE fund_entries
|
||||
ADD CONSTRAINT chk_fund_entries_amount
|
||||
CHECK (amount_fen <> 0);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_fund_entries_balance') THEN
|
||||
ALTER TABLE fund_entries
|
||||
ADD CONSTRAINT chk_fund_entries_balance
|
||||
CHECK (balance_after_fen >= 0);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_amount') THEN
|
||||
ALTER TABLE manual_expenses
|
||||
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_category') THEN
|
||||
ALTER TABLE manual_expenses
|
||||
ADD CONSTRAINT chk_manual_expenses_category
|
||||
CHECK (category IN ('platform_recharge', 'purchase', 'compensation', 'freight', 'other'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_payee_type') THEN
|
||||
ALTER TABLE manual_expenses
|
||||
ADD CONSTRAINT chk_manual_expenses_payee_type
|
||||
CHECK (payee_type IN ('platform', 'supplier', 'customer', 'other'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_fund_reconciliations_status') THEN
|
||||
ALTER TABLE fund_reconciliations
|
||||
ADD CONSTRAINT chk_fund_reconciliations_status
|
||||
CHECK (status IN ('pending', 'confirmed', 'discrepancy'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_fund_reconciliations_difference') THEN
|
||||
ALTER TABLE fund_reconciliations
|
||||
ADD CONSTRAINT chk_fund_reconciliations_difference
|
||||
CHECK (difference_fen = actual_balance_fen - system_balance_fen);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE fund_accounts IS '备用金资金账户:主管池与客服备用金账户';
|
||||
COMMENT ON COLUMN fund_accounts.responsible_supervisor_id IS '客服的责任主管账户;对账由该主管确认';
|
||||
COMMENT ON COLUMN fund_accounts.balance_fen IS '当前余额,只能由 fund_entries 变动';
|
||||
COMMENT ON TABLE fund_entries IS '备用金流水:期初/拨付/发放/支出/缴回/调整,只追加';
|
||||
COMMENT ON COLUMN fund_entries.amount_fen IS '有符号金额:正数流入,负数流出';
|
||||
COMMENT ON COLUMN fund_entries.balance_after_fen IS '记账后的账户余额快照';
|
||||
COMMENT ON COLUMN fund_entries.transfer_no IS '发放/缴回双边流水批次号,主管与客服两条共用';
|
||||
COMMENT ON TABLE manual_expenses IS '客服备用金支出明细;充值后立即消耗,直接记支出';
|
||||
COMMENT ON TABLE fund_reconciliations IS '备用金对账:系统余额与实际余额差异,差异通过 adjustment 流水修正';
|
||||
|
||||
INSERT INTO admin_permissions (permission_key, permission_name, permission_group, description)
|
||||
VALUES
|
||||
('fund.view', '查看备用金', '备用金管理', '查看备用金账户、流水和支出明细'),
|
||||
('fund.issue', '发放/缴回备用金', '备用金管理', '主管向客服发放或收回备用金'),
|
||||
('fund.expense', '登记备用金支出', '备用金管理', '客服登记备用金支出'),
|
||||
('fund.reconcile', '备用金对账', '备用金管理', '发起和确认备用金对账'),
|
||||
('fund.adjust', '调整备用金余额', '备用金管理', '对账差异确认后调整账户余额'),
|
||||
('fund.export', '导出备用金数据', '备用金管理', '导出备用金账户、流水和支出')
|
||||
ON CONFLICT (permission_key) DO UPDATE SET
|
||||
permission_name = EXCLUDED.permission_name,
|
||||
permission_group = EXCLUDED.permission_group,
|
||||
description = EXCLUDED.description,
|
||||
updated_at = NOW();
|
||||
|
||||
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||
SELECT 'admin', permission_key
|
||||
FROM admin_permissions
|
||||
WHERE permission_key LIKE 'fund.%'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||
VALUES
|
||||
('finance', 'fund.view'),
|
||||
('finance', 'fund.reconcile'),
|
||||
('finance', 'fund.adjust'),
|
||||
('finance', 'fund.export'),
|
||||
('operator', 'fund.view'),
|
||||
('operator', 'fund.issue'),
|
||||
('operator', 'fund.reconcile'),
|
||||
('support', 'fund.view'),
|
||||
('support', 'fund.expense')
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,45 @@
|
||||
-- 101_admin_display_name.sql —— 后台用户增加中文姓名/显示名。
|
||||
--
|
||||
-- 账号 username 仍然是登录凭证,只能使用字母、数字和 ._-;
|
||||
-- display_name 只用于展示,可输入中文姓名。为空时回退显示 username。
|
||||
|
||||
ALTER TABLE admin_users
|
||||
ADD COLUMN IF NOT EXISTS display_name TEXT NOT NULL DEFAULT '';
|
||||
|
||||
UPDATE admin_users
|
||||
SET display_name = username
|
||||
WHERE display_name = '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_users_display_name
|
||||
ON admin_users (display_name);
|
||||
|
||||
COMMENT ON COLUMN admin_users.display_name IS '后台用户显示名(中文姓名等),为空时回退 username';
|
||||
|
||||
-- 备用金账户里的 owner_name 是姓名快照;改名后同步,避免还要逐表手工改。
|
||||
CREATE OR REPLACE FUNCTION sync_admin_display_name_to_fund_accounts()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
UPDATE fund_accounts
|
||||
SET
|
||||
owner_name = COALESCE(NULLIF(NEW.display_name, ''), NEW.username),
|
||||
updated_at = NOW()
|
||||
WHERE owner_admin_user_id = NEW.id
|
||||
AND owner_name IS DISTINCT FROM COALESCE(NULLIF(NEW.display_name, ''), NEW.username);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_admin_users_display_name_fund_accounts ON admin_users;
|
||||
CREATE TRIGGER trg_admin_users_display_name_fund_accounts
|
||||
AFTER UPDATE OF display_name, username ON admin_users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION sync_admin_display_name_to_fund_accounts();
|
||||
|
||||
UPDATE fund_accounts fa
|
||||
SET
|
||||
owner_name = COALESCE(NULLIF(au.display_name, ''), au.username),
|
||||
updated_at = NOW()
|
||||
FROM admin_users au
|
||||
WHERE fa.owner_admin_user_id = au.id
|
||||
AND fa.owner_name IS DISTINCT FROM COALESCE(NULLIF(au.display_name, ''), au.username);
|
||||
Reference in New Issue
Block a user