create table if not exists public.content_asset_access_events ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, asset_id uuid references public.content_assets(id) on delete set null, user_id uuid references public.platform_users(id) on delete set null, actor_role text not null default 'anonymous' check (actor_role in ('anonymous', 'student', 'tenant_admin', 'tenant_content_editor', 'system')), access_type text not null check (access_type in ('download', 'preview', 'admin_download', 'admin_preview', 'upload_sign', 'upload_confirm')), visibility text, asset_type text, storage_provider text, disposition text check (disposition is null or disposition in ('attachment', 'inline')), expires_in_sec integer check (expires_in_sec is null or expires_in_sec > 0), signature_mode text, result text not null check (result in ('granted', 'denied')), deny_code text, ip_address text, user_agent text, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now() ); comment on table public.content_asset_access_events is '短期资源签名与资源访问授权事件。用于租户隔离审计、资料下载追踪、异常 CDN/私有资源排查。'; create index if not exists idx_asset_access_events_asset on public.content_asset_access_events(tenant_id, asset_id, created_at desc); create index if not exists idx_asset_access_events_user on public.content_asset_access_events(tenant_id, user_id, created_at desc) where user_id is not null; create index if not exists idx_asset_access_events_type on public.content_asset_access_events(tenant_id, access_type, result, created_at desc); alter table public.content_asset_access_events enable row level security; drop policy if exists tenant_isolation on public.content_asset_access_events; create policy tenant_isolation on public.content_asset_access_events for all using (tenant_id = app.current_tenant_id() or app.is_platform_admin()) with check (tenant_id = app.current_tenant_id() or app.is_platform_admin());