Files
gongxue-base/supabase/migrations/202606210007_content_import_assets.sql
2026-06-21 21:54:43 +08:00

165 lines
8.1 KiB
SQL

alter table public.content_assets
add column if not exists asset_type text not null default 'document',
add column if not exists storage_provider text not null default 'external_url',
add column if not exists bucket text,
add column if not exists object_key text,
add column if not exists mime_type text,
add column if not exists file_size_bytes bigint,
add column if not exists checksum_sha256 text,
add column if not exists visibility text not null default 'tenant',
add column if not exists region_id uuid references public.regions(id) on delete set null,
add column if not exists subject_id uuid references public.subjects(id) on delete set null,
add column if not exists category_id uuid references public.categories(id) on delete set null,
add column if not exists node_id uuid references public.module_nodes(id) on delete set null,
add column if not exists preview_url text,
add column if not exists status text not null default 'active',
add column if not exists sort_order integer not null default 0,
add column if not exists access_rules jsonb not null default '{}'::jsonb,
add column if not exists created_by uuid references public.platform_users(id) on delete set null,
add column if not exists updated_by uuid references public.platform_users(id) on delete set null,
add column if not exists source text not null default 'manual',
add column if not exists download_count integer not null default 0;
update public.content_assets
set visibility = case when is_public then 'public' else visibility end,
storage_provider = case when cdn_url is not null and cdn_url <> '' then 'external_url' else storage_provider end
where visibility = 'tenant' or storage_provider = 'external_url';
do $$
begin
if not exists (select 1 from pg_constraint where conname = 'content_assets_asset_type_check') then
alter table public.content_assets
add constraint content_assets_asset_type_check
check (asset_type in ('pdf', 'video', 'image', 'audio', 'document', 'package', 'link', 'other'));
end if;
if not exists (select 1 from pg_constraint where conname = 'content_assets_storage_provider_check') then
alter table public.content_assets
add constraint content_assets_storage_provider_check
check (storage_provider in ('external_url', 'supabase_storage', 'aliyun_oss', 'tencent_cos', 'qiniu_kodo', 'local_dev'));
end if;
if not exists (select 1 from pg_constraint where conname = 'content_assets_visibility_check') then
alter table public.content_assets
add constraint content_assets_visibility_check
check (visibility in ('public', 'tenant', 'members', 'svip', 'private'));
end if;
if not exists (select 1 from pg_constraint where conname = 'content_assets_status_check') then
alter table public.content_assets
add constraint content_assets_status_check
check (status in ('draft', 'active', 'archived'));
end if;
if not exists (select 1 from pg_constraint where conname = 'content_assets_file_size_check') then
alter table public.content_assets
add constraint content_assets_file_size_check
check (file_size_bytes is null or file_size_bytes >= 0);
end if;
if not exists (select 1 from pg_constraint where conname = 'content_assets_download_count_check') then
alter table public.content_assets
add constraint content_assets_download_count_check
check (download_count >= 0);
end if;
end $$;
create table if not exists public.content_import_jobs (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
created_by uuid references public.platform_users(id) on delete set null,
import_type text not null check (import_type in ('questions', 'vocabulary', 'handbook', 'scoreline', 'assets', 'videos')),
source_format text not null default 'json' check (source_format in ('json', 'excel', 'csv', 'pocketbase', 'api')),
status text not null default 'preview' check (status in ('preview', 'pending', 'importing', 'completed', 'completed_with_errors', 'failed', 'rejected')),
source_name text,
source_hash text,
target_region_id uuid references public.regions(id) on delete set null,
target_subject_id uuid references public.subjects(id) on delete set null,
target_category_id uuid references public.categories(id) on delete set null,
target_node_id uuid references public.module_nodes(id) on delete set null,
target_question_bank_id uuid references public.question_banks(id) on delete set null,
dry_run boolean not null default true,
total_count integer not null default 0,
valid_count integer not null default 0,
error_count integer not null default 0,
warning_count integer not null default 0,
inserted_count integer not null default 0,
updated_count integer not null default 0,
skipped_count integer not null default 0,
summary jsonb not null default '{}'::jsonb,
raw_payload jsonb not null default '[]'::jsonb,
normalized_payload jsonb not null default '[]'::jsonb,
error_message text,
started_at timestamptz,
finished_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists public.content_import_items (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
job_id uuid not null references public.content_import_jobs(id) on delete cascade,
row_no integer not null,
external_id text,
status text not null default 'valid' check (status in ('valid', 'invalid', 'inserted', 'updated', 'skipped', 'failed')),
target_type text,
target_id uuid,
source_payload jsonb not null default '{}'::jsonb,
normalized_payload jsonb not null default '{}'::jsonb,
content_hash text,
issues_count integer not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (job_id, row_no)
);
create table if not exists public.content_import_issues (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
job_id uuid not null references public.content_import_jobs(id) on delete cascade,
item_id uuid references public.content_import_items(id) on delete cascade,
row_no integer,
severity text not null default 'error' check (severity in ('error', 'warning')),
code text not null,
field_path text,
message text not null,
details jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists idx_content_assets_filters
on public.content_assets(tenant_id, status, visibility, asset_type, region_id, subject_id, category_id, sort_order);
create index if not exists idx_content_assets_object
on public.content_assets(tenant_id, storage_provider, bucket, object_key);
create index if not exists idx_content_assets_checksum
on public.content_assets(tenant_id, checksum_sha256)
where checksum_sha256 is not null;
create index if not exists idx_content_import_jobs_tenant_status
on public.content_import_jobs(tenant_id, import_type, status, created_at desc);
create index if not exists idx_content_import_items_job_status
on public.content_import_items(tenant_id, job_id, status, row_no);
create index if not exists idx_content_import_issues_job
on public.content_import_issues(tenant_id, job_id, severity, row_no);
do $$
declare
table_name text;
begin
foreach table_name in array array[
'content_import_jobs', 'content_import_items', 'content_import_issues'
]
loop
execute format('alter table public.%I enable row level security', table_name);
execute format('drop policy if exists tenant_isolation on public.%I', table_name);
execute format(
'create policy tenant_isolation on public.%I 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())',
table_name
);
execute format('drop trigger if exists set_updated_at on public.%I', table_name);
execute format('create trigger set_updated_at before update on public.%I for each row execute function app.touch_updated_at()', table_name);
end loop;
end $$;