445 lines
15 KiB
PL/PgSQL
445 lines
15 KiB
PL/PgSQL
-- ============================================================================
|
||
-- 数据完整性强约束 - 确保财务与核心数据100%正确
|
||
-- ============================================================================
|
||
-- 目标:系统错误率 ≤ 0.01% (万分之一)
|
||
-- 范围:production_plans, inventory_records, payments, accounts_payable
|
||
-- ============================================================================
|
||
|
||
-- ============================================================================
|
||
-- 1. 创建数据校验函数
|
||
-- ============================================================================
|
||
|
||
-- 1.1 验证计划数量一致性(completed_quantity = SUM(inventory_records.quantity))
|
||
CREATE OR REPLACE FUNCTION validate_plan_quantity_consistency()
|
||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||
DECLARE
|
||
v_inventory_total NUMERIC;
|
||
v_tolerance NUMERIC := 0.01; -- 允许0.01米的浮点误差
|
||
BEGIN
|
||
-- 仅当 completed_quantity 被更新时触发
|
||
IF OLD.completed_quantity IS DISTINCT FROM NEW.completed_quantity THEN
|
||
SELECT COALESCE(SUM(quantity), 0) INTO v_inventory_total
|
||
FROM inventory_records
|
||
WHERE plan_id = NEW.id;
|
||
|
||
IF ABS(NEW.completed_quantity - v_inventory_total) > v_tolerance THEN
|
||
RAISE EXCEPTION
|
||
'数据完整性错误:计划 % 的完成数量(%)与入库记录总和(%)不一致,差异: %',
|
||
NEW.plan_code, NEW.completed_quantity, v_inventory_total,
|
||
NEW.completed_quantity - v_inventory_total;
|
||
END IF;
|
||
END IF;
|
||
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
-- 1.2 验证入库记录不能为负数
|
||
CREATE OR REPLACE FUNCTION validate_inventory_record_positive()
|
||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||
BEGIN
|
||
IF NEW.quantity < 0 THEN
|
||
RAISE EXCEPTION '入库数量不能为负数: %', NEW.quantity;
|
||
END IF;
|
||
|
||
IF NEW.rolls < 0 THEN
|
||
RAISE EXCEPTION '入库匹数不能为负数: %', NEW.rolls;
|
||
END IF;
|
||
|
||
IF NEW.price_per_meter < 0 THEN
|
||
RAISE EXCEPTION '单价不能为负数: %', NEW.price_per_meter;
|
||
END IF;
|
||
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
-- 1.3 验证付款金额一致性
|
||
CREATE OR REPLACE FUNCTION validate_payment_amount()
|
||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||
DECLARE
|
||
v_expected_amount NUMERIC;
|
||
v_tolerance NUMERIC := 0.01;
|
||
BEGIN
|
||
-- 计算预期金额 = 数量 × 单价
|
||
v_expected_amount := NEW.quantity * NEW.price_per_meter;
|
||
|
||
IF ABS(NEW.amount - v_expected_amount) > v_tolerance THEN
|
||
RAISE EXCEPTION
|
||
'付款金额不一致:期望 %(数量 % × 单价 %),实际 %',
|
||
v_expected_amount, NEW.quantity, NEW.price_per_meter, NEW.amount;
|
||
END IF;
|
||
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
-- 1.4 验证应付账款汇总一致性
|
||
CREATE OR REPLACE FUNCTION validate_accounts_payable_totals()
|
||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||
DECLARE
|
||
v_items_total NUMERIC;
|
||
v_items_paid NUMERIC;
|
||
v_tolerance NUMERIC := 0.01;
|
||
BEGIN
|
||
-- 从明细表重新计算
|
||
SELECT
|
||
COALESCE(SUM(amount), 0),
|
||
COALESCE(SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END), 0)
|
||
INTO v_items_total, v_items_paid
|
||
FROM accounts_payable_items
|
||
WHERE accounts_payable_id = NEW.id;
|
||
|
||
-- 验证总金额
|
||
IF ABS(NEW.total_amount - v_items_total) > v_tolerance THEN
|
||
RAISE EXCEPTION
|
||
'应付账款总金额不一致:表头 %, 明细合计 %',
|
||
NEW.total_amount, v_items_total;
|
||
END IF;
|
||
|
||
-- 验证已付金额
|
||
IF ABS(NEW.paid_amount - v_items_paid) > v_tolerance THEN
|
||
RAISE EXCEPTION
|
||
'应付账款已付金额不一致:表头 %, 明细合计 %',
|
||
NEW.paid_amount, v_items_paid;
|
||
END IF;
|
||
|
||
-- 验证未付金额
|
||
IF ABS(NEW.unpaid_amount - (v_items_total - v_items_paid)) > v_tolerance THEN
|
||
RAISE EXCEPTION
|
||
'应付账款未付金额不一致:表头 %, 计算值 %',
|
||
NEW.unpaid_amount, (v_items_total - v_items_paid);
|
||
END IF;
|
||
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
-- ============================================================================
|
||
-- 2. 创建触发器
|
||
-- ============================================================================
|
||
|
||
-- 2.1 计划数量一致性触发器
|
||
DROP TRIGGER IF EXISTS trg_validate_plan_quantity ON production_plans;
|
||
CREATE TRIGGER trg_validate_plan_quantity
|
||
BEFORE UPDATE ON production_plans
|
||
FOR EACH ROW
|
||
WHEN (OLD.completed_quantity IS DISTINCT FROM NEW.completed_quantity)
|
||
EXECUTE FUNCTION validate_plan_quantity_consistency();
|
||
|
||
-- 2.2 入库记录正数验证触发器
|
||
DROP TRIGGER IF EXISTS trg_validate_inventory_positive ON inventory_records;
|
||
CREATE TRIGGER trg_validate_inventory_positive
|
||
BEFORE INSERT OR UPDATE ON inventory_records
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION validate_inventory_record_positive();
|
||
|
||
-- 2.3 付款金额验证触发器
|
||
DROP TRIGGER IF EXISTS trg_validate_payment ON payments;
|
||
CREATE TRIGGER trg_validate_payment
|
||
BEFORE INSERT OR UPDATE ON payments
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION validate_payment_amount();
|
||
|
||
-- 2.4 应付账款汇总验证触发器
|
||
DROP TRIGGER IF EXISTS trg_validate_accounts_payable ON accounts_payable;
|
||
CREATE TRIGGER trg_validate_accounts_payable
|
||
BEFORE INSERT OR UPDATE ON accounts_payable
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION validate_accounts_payable_totals();
|
||
|
||
-- ============================================================================
|
||
-- 3. 创建审计日志表(记录所有关键数据变更)
|
||
-- ============================================================================
|
||
|
||
CREATE TABLE IF NOT EXISTS data_audit_logs (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
table_name TEXT NOT NULL,
|
||
record_id UUID NOT NULL,
|
||
operation TEXT NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE')),
|
||
old_values JSONB,
|
||
new_values JSONB,
|
||
changed_by UUID REFERENCES auth.users(id),
|
||
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
validation_passed BOOLEAN NOT NULL DEFAULT TRUE,
|
||
error_message TEXT
|
||
);
|
||
|
||
-- 创建索引加速查询
|
||
CREATE INDEX IF NOT EXISTS idx_audit_logs_table_record ON data_audit_logs(table_name, record_id);
|
||
CREATE INDEX IF NOT EXISTS idx_audit_logs_changed_at ON data_audit_logs(changed_at DESC);
|
||
CREATE INDEX IF NOT EXISTS idx_audit_logs_validation ON data_audit_logs(validation_passed) WHERE validation_passed = FALSE;
|
||
|
||
-- 启用 RLS
|
||
ALTER TABLE data_audit_logs ENABLE ROW LEVEL SECURITY;
|
||
|
||
-- 审计日志只读策略(仅管理员可查看)
|
||
CREATE POLICY admin_select_audit_logs ON data_audit_logs
|
||
FOR SELECT USING (true); -- 暂时开放,后续可改为角色检查
|
||
|
||
CREATE POLICY system_insert_audit_logs ON data_audit_logs
|
||
FOR INSERT WITH CHECK (true); -- 系统自动写入
|
||
|
||
-- ============================================================================
|
||
-- 4. 创建审计触发器函数
|
||
-- ============================================================================
|
||
|
||
CREATE OR REPLACE FUNCTION log_data_audit()
|
||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||
DECLARE
|
||
v_old_values JSONB;
|
||
v_new_values JSONB;
|
||
v_operation TEXT;
|
||
BEGIN
|
||
v_operation := TG_OP;
|
||
|
||
IF TG_OP = 'DELETE' THEN
|
||
v_old_values := to_jsonb(OLD);
|
||
v_new_values := NULL;
|
||
ELSIF TG_OP = 'UPDATE' THEN
|
||
v_old_values := to_jsonb(OLD);
|
||
v_new_values := to_jsonb(NEW);
|
||
ELSE -- INSERT
|
||
v_old_values := NULL;
|
||
v_new_values := to_jsonb(NEW);
|
||
END IF;
|
||
|
||
INSERT INTO data_audit_logs (
|
||
table_name, record_id, operation, old_values, new_values, changed_by
|
||
) VALUES (
|
||
TG_TABLE_NAME,
|
||
COALESCE(NEW.id, OLD.id),
|
||
v_operation,
|
||
v_old_values,
|
||
v_new_values,
|
||
auth.uid()
|
||
);
|
||
|
||
IF TG_OP = 'DELETE' THEN
|
||
RETURN OLD;
|
||
ELSE
|
||
RETURN NEW;
|
||
END IF;
|
||
END;
|
||
$$;
|
||
|
||
-- ============================================================================
|
||
-- 5. 为核心表添加审计触发器
|
||
-- ============================================================================
|
||
|
||
-- 5.1 production_plans 审计
|
||
DROP TRIGGER IF EXISTS trg_audit_production_plans ON production_plans;
|
||
CREATE TRIGGER trg_audit_production_plans
|
||
AFTER INSERT OR UPDATE OR DELETE ON production_plans
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION log_data_audit();
|
||
|
||
-- 5.2 inventory_records 审计
|
||
DROP TRIGGER IF EXISTS trg_audit_inventory_records ON inventory_records;
|
||
CREATE TRIGGER trg_audit_inventory_records
|
||
AFTER INSERT OR UPDATE OR DELETE ON inventory_records
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION log_data_audit();
|
||
|
||
-- 5.3 payments 审计
|
||
DROP TRIGGER IF EXISTS trg_audit_payments ON payments;
|
||
CREATE TRIGGER trg_audit_payments
|
||
AFTER INSERT OR UPDATE OR DELETE ON payments
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION log_data_audit();
|
||
|
||
-- 5.4 accounts_payable 审计
|
||
DROP TRIGGER IF EXISTS trg_audit_accounts_payable ON accounts_payable;
|
||
CREATE TRIGGER trg_audit_accounts_payable
|
||
AFTER INSERT OR UPDATE OR DELETE ON accounts_payable
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION log_data_audit();
|
||
|
||
-- 5.5 accounts_payable_items 审计
|
||
DROP TRIGGER IF EXISTS trg_audit_accounts_payable_items ON accounts_payable_items;
|
||
CREATE TRIGGER trg_audit_accounts_payable_items
|
||
AFTER INSERT OR UPDATE OR DELETE ON accounts_payable_items
|
||
FOR EACH ROW
|
||
EXECUTE FUNCTION log_data_audit();
|
||
|
||
-- ============================================================================
|
||
-- 6. 创建数据一致性检查视图
|
||
-- ============================================================================
|
||
|
||
-- 6.1 计划数量一致性检查视图
|
||
CREATE OR REPLACE VIEW v_plan_quantity_check AS
|
||
SELECT
|
||
p.id as plan_id,
|
||
p.plan_code,
|
||
p.status,
|
||
p.completed_quantity,
|
||
COALESCE(SUM(ir.quantity), 0) as inventory_total,
|
||
p.completed_quantity - COALESCE(SUM(ir.quantity), 0) as difference,
|
||
CASE
|
||
WHEN ABS(p.completed_quantity - COALESCE(SUM(ir.quantity), 0)) <= 0.01 THEN '✓ 一致'
|
||
ELSE '✗ 不一致'
|
||
END as check_result
|
||
FROM production_plans p
|
||
LEFT JOIN inventory_records ir ON ir.plan_id = p.id
|
||
GROUP BY p.id, p.plan_code, p.status, p.completed_quantity;
|
||
|
||
-- 6.2 付款金额一致性检查视图
|
||
CREATE OR REPLACE VIEW v_payment_check AS
|
||
SELECT
|
||
id as payment_id,
|
||
plan_id,
|
||
quantity,
|
||
price_per_meter,
|
||
amount,
|
||
quantity * price_per_meter as expected_amount,
|
||
amount - (quantity * price_per_meter) as difference,
|
||
CASE
|
||
WHEN ABS(amount - (quantity * price_per_meter)) <= 0.01 THEN '✓ 一致'
|
||
ELSE '✗ 不一致'
|
||
END as check_result
|
||
FROM payments;
|
||
|
||
-- 6.3 应付账款汇总一致性检查视图
|
||
CREATE OR REPLACE VIEW v_accounts_payable_check AS
|
||
SELECT
|
||
ap.id as ap_id,
|
||
ap.plan_id,
|
||
ap.total_amount,
|
||
ap.paid_amount,
|
||
ap.unpaid_amount,
|
||
COALESCE(SUM(api.amount), 0) as items_total,
|
||
COALESCE(SUM(CASE WHEN api.status = 'paid' THEN api.amount ELSE 0 END), 0) as items_paid,
|
||
ap.total_amount - COALESCE(SUM(api.amount), 0) as total_diff,
|
||
ap.paid_amount - COALESCE(SUM(CASE WHEN api.status = 'paid' THEN api.amount ELSE 0 END), 0) as paid_diff,
|
||
CASE
|
||
WHEN ABS(ap.total_amount - COALESCE(SUM(api.amount), 0)) <= 0.01
|
||
AND ABS(ap.paid_amount - COALESCE(SUM(CASE WHEN api.status = 'paid' THEN api.amount ELSE 0 END), 0)) <= 0.01
|
||
THEN '✓ 一致'
|
||
ELSE '✗ 不一致'
|
||
END as check_result
|
||
FROM accounts_payable ap
|
||
LEFT JOIN accounts_payable_items api ON api.accounts_payable_id = ap.id
|
||
GROUP BY ap.id, ap.plan_id, ap.total_amount, ap.paid_amount, ap.unpaid_amount;
|
||
|
||
-- ============================================================================
|
||
-- 7. 创建定期一致性检查函数(可由 Edge Function 调用)
|
||
-- ============================================================================
|
||
|
||
CREATE OR REPLACE FUNCTION run_data_integrity_check()
|
||
RETURNS TABLE (
|
||
check_type TEXT,
|
||
total_records BIGINT,
|
||
inconsistent_records BIGINT,
|
||
error_rate NUMERIC,
|
||
status TEXT
|
||
) LANGUAGE plpgsql AS $$
|
||
BEGIN
|
||
-- 检查计划数量一致性
|
||
RETURN QUERY
|
||
SELECT
|
||
'plan_quantity'::TEXT,
|
||
COUNT(*)::BIGINT,
|
||
SUM(CASE WHEN check_result = '✗ 不一致' THEN 1 ELSE 0 END)::BIGINT,
|
||
ROUND(
|
||
SUM(CASE WHEN check_result = '✗ 不一致' THEN 1 ELSE 0 END)::NUMERIC /
|
||
NULLIF(COUNT(*), 0) * 100, 4
|
||
),
|
||
CASE
|
||
WHEN SUM(CASE WHEN check_result = '✗ 不一致' THEN 1 ELSE 0 END) = 0 THEN 'PASS'
|
||
ELSE 'FAIL'
|
||
END
|
||
FROM v_plan_quantity_check;
|
||
|
||
-- 检查付款金额一致性
|
||
RETURN QUERY
|
||
SELECT
|
||
'payment_amount'::TEXT,
|
||
COUNT(*)::BIGINT,
|
||
SUM(CASE WHEN check_result = '✗ 不一致' THEN 1 ELSE 0 END)::BIGINT,
|
||
ROUND(
|
||
SUM(CASE WHEN check_result = '✗ 不一致' THEN 1 ELSE 0 END)::NUMERIC /
|
||
NULLIF(COUNT(*), 0) * 100, 4
|
||
),
|
||
CASE
|
||
WHEN SUM(CASE WHEN check_result = '✗ 不一致' THEN 1 ELSE 0 END) = 0 THEN 'PASS'
|
||
ELSE 'FAIL'
|
||
END
|
||
FROM v_payment_check;
|
||
|
||
-- 检查应付账款汇总一致性
|
||
RETURN QUERY
|
||
SELECT
|
||
'accounts_payable'::TEXT,
|
||
COUNT(*)::BIGINT,
|
||
SUM(CASE WHEN check_result = '✗ 不一致' THEN 1 ELSE 0 END)::BIGINT,
|
||
ROUND(
|
||
SUM(CASE WHEN check_result = '✗ 不一致' THEN 1 ELSE 0 END)::NUMERIC /
|
||
NULLIF(COUNT(*), 0) * 100, 4
|
||
),
|
||
CASE
|
||
WHEN SUM(CASE WHEN check_result = '✗ 不一致' THEN 1 ELSE 0 END) = 0 THEN 'PASS'
|
||
ELSE 'FAIL'
|
||
END
|
||
FROM v_accounts_payable_check;
|
||
END;
|
||
$$;
|
||
|
||
-- ============================================================================
|
||
-- 8. 添加 CHECK 约束(作为最后一道防线)
|
||
-- ============================================================================
|
||
|
||
-- 8.1 入库记录非负约束
|
||
ALTER TABLE inventory_records
|
||
DROP CONSTRAINT IF EXISTS chk_inventory_positive;
|
||
ALTER TABLE inventory_records
|
||
ADD CONSTRAINT chk_inventory_positive
|
||
CHECK (quantity >= 0 AND rolls >= 0 AND (price_per_meter IS NULL OR price_per_meter >= 0));
|
||
|
||
-- 8.2 付款金额非负约束
|
||
ALTER TABLE payments
|
||
DROP CONSTRAINT IF EXISTS chk_payment_positive;
|
||
ALTER TABLE payments
|
||
ADD CONSTRAINT chk_payment_positive
|
||
CHECK (amount >= 0 AND quantity >= 0 AND price_per_meter >= 0);
|
||
|
||
-- 8.3 应付账款金额逻辑约束
|
||
ALTER TABLE accounts_payable
|
||
DROP CONSTRAINT IF EXISTS chk_ap_amounts;
|
||
ALTER TABLE accounts_payable
|
||
ADD CONSTRAINT chk_ap_amounts
|
||
CHECK (
|
||
total_amount >= 0 AND
|
||
paid_amount >= 0 AND
|
||
unpaid_amount >= 0 AND
|
||
paid_amount <= total_amount AND
|
||
unpaid_amount <= total_amount
|
||
);
|
||
|
||
-- ============================================================================
|
||
-- 9. 创建错误率监控视图
|
||
-- ============================================================================
|
||
|
||
CREATE OR REPLACE VIEW v_error_rate_monitor AS
|
||
SELECT
|
||
DATE_TRUNC('day', changed_at) as check_date,
|
||
table_name,
|
||
COUNT(*) as total_operations,
|
||
SUM(CASE WHEN validation_passed = FALSE THEN 1 ELSE 0 END) as failed_operations,
|
||
ROUND(
|
||
SUM(CASE WHEN validation_passed = FALSE THEN 1 ELSE 0 END)::NUMERIC /
|
||
NULLIF(COUNT(*), 0) * 10000, 2
|
||
) as error_rate_per_10k,
|
||
CASE
|
||
WHEN SUM(CASE WHEN validation_passed = FALSE THEN 1 ELSE 0 END)::NUMERIC /
|
||
NULLIF(COUNT(*), 0) * 10000 <= 1 THEN '✓ 达标 (≤1/万)'
|
||
ELSE '✗ 超标 (>1/万)'
|
||
END as target_status
|
||
FROM data_audit_logs
|
||
WHERE changed_at >= NOW() - INTERVAL '30 days'
|
||
GROUP BY DATE_TRUNC('day', changed_at), table_name
|
||
ORDER BY check_date DESC, table_name;
|
||
|
||
-- ============================================================================
|
||
-- 结束
|
||
-- ============================================================================
|