iloom-flatten/migrations/20260604_200000_data_integrity_constraints.sql

445 lines
15 KiB
MySQL
Raw Permalink Normal View History

-- ============================================================================
-- 数据完整性强约束 - 确保财务与核心数据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;
-- ============================================================================
-- 结束
-- ============================================================================