-- 9. audit:审计事件(架构 §3.10,FR-M6-07) -- 来源:docs/architecture/schema-draft.sql(2026-11-03 验证过的草案) CREATE TABLE audit_event ( id uuid PRIMARY KEY, tenant_id uuid NOT NULL REFERENCES tenant (id), actor_kind text NOT NULL CHECK (actor_kind IN ('user', 'ops', 'system')), actor_user_id uuid REFERENCES user_account (id), actor_ops_id uuid, actor_name text, -- 姓名快照:人离职后留痕上仍显示当时的人 action text NOT NULL, -- 如 checklist_item.update、finding.resolve object_type text NOT NULL, object_id uuid, project_id uuid, -- 不加外键:删除项目后审计保留 before jsonb, after jsonb, content_digest bytea, -- before / after 的哈希;抹掉内容后哈希链仍可校验 content_scrubbed_at timestamptz, -- 删除项目时抹掉业务内容(G10) request_id text, ip inet, prev_hash bytea, -- 可选的哈希链 row_hash bytea, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX ON audit_event (tenant_id, project_id, created_at); CREATE INDEX ON audit_event (tenant_id, object_type, object_id); -- 审计只追加;唯一允许的修改是「抹掉业务内容」 CREATE FUNCTION audit_scrub_only() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'audit_event is append-only'; END IF; IF NEW.before IS NOT NULL OR NEW.after IS NOT NULL OR NEW.content_scrubbed_at IS NULL OR (to_jsonb(NEW) - ARRAY['before', 'after', 'content_scrubbed_at', 'actor_name']) IS DISTINCT FROM (to_jsonb(OLD) - ARRAY['before', 'after', 'content_scrubbed_at', 'actor_name']) THEN RAISE EXCEPTION 'audit_event only allows scrubbing content'; END IF; RETURN NEW; END $$; CREATE TRIGGER audit_event_guard BEFORE UPDATE OR DELETE ON audit_event FOR EACH ROW EXECUTE FUNCTION audit_scrub_only();