alter table public.content_assets add column if not exists security_scan_status text not null default 'not_required', add column if not exists security_scanned_at timestamptz, add column if not exists security_scan_provider text, add column if not exists security_scan_summary jsonb not null default '{}'::jsonb; update public.content_assets set security_scan_status = case when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos') and object_key is not null and upload_status = 'verified' then 'passed' when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos') and object_key is not null and upload_status = 'failed' then 'skipped' when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos') and object_key is not null then 'pending' else 'not_required' end, security_scanned_at = case when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos') and object_key is not null and upload_status = 'verified' then coalesce(security_scanned_at, verified_at, updated_at, created_at, now()) else security_scanned_at end, security_scan_provider = case when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos') and object_key is not null and upload_status = 'verified' then coalesce(security_scan_provider, 'legacy_backfill') else security_scan_provider end, security_scan_summary = case when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos') and object_key is not null and upload_status = 'verified' then coalesce(security_scan_summary, '{}'::jsonb) || jsonb_build_object( 'riskLevel', 'none', 'issueCodes', '[]'::jsonb, 'backfilled', true, 'backfilledAt', now() ) else coalesce(security_scan_summary, '{}'::jsonb) end where security_scan_status = 'not_required' or security_scan_summary = '{}'::jsonb; do $$ begin if not exists (select 1 from pg_constraint where conname = 'content_assets_security_scan_status_check') then alter table public.content_assets add constraint content_assets_security_scan_status_check check (security_scan_status in ('not_required', 'pending', 'scanning', 'passed', 'failed', 'skipped')); end if; end $$; create table if not exists public.content_asset_security_scan_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, provider text not null, scan_status text not null check (scan_status in ('pending', 'scanning', 'passed', 'failed', 'skipped')), risk_level text not null default 'none' check (risk_level in ('none', 'low', 'medium', 'high', 'critical')), issue_codes text[] not null default '{}', details jsonb not null default '{}'::jsonb, created_at timestamptz not null default now() ); comment on table public.content_asset_security_scan_events is '内容资源安全扫描事件。用于记录托管对象的内置规则扫描、杀毒/内容安全 provider 扫描结果、失败原因和审计轨迹。'; create index if not exists idx_asset_security_scan_events_asset on public.content_asset_security_scan_events(tenant_id, asset_id, created_at desc); create index if not exists idx_asset_security_scan_events_status on public.content_asset_security_scan_events(tenant_id, scan_status, risk_level, created_at desc); create index if not exists idx_content_assets_security_scan_status on public.content_assets(tenant_id, security_scan_status, upload_status, status, updated_at desc); alter table public.content_asset_security_scan_events enable row level security; drop policy if exists tenant_isolation on public.content_asset_security_scan_events; create policy tenant_isolation on public.content_asset_security_scan_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());