feat: scaffold supabase multi-tenant backend

This commit is contained in:
Codex
2026-06-21 21:54:43 +08:00
commit c9c767c7bd
99 changed files with 36660 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
create table if not exists public.platform_saas_plans (
id uuid primary key default gen_random_uuid(),
code text not null unique,
name text not null,
description text,
billing_cycle text not null default 'yearly' check (billing_cycle in ('monthly', 'quarterly', 'yearly', 'one_time')),
base_amount_cents integer not null default 0,
currency text not null default 'CNY',
included_quotas jsonb not null default '{}'::jsonb,
overage_prices jsonb not null default '{}'::jsonb,
feature_flags jsonb not null default '{}'::jsonb,
status text not null default 'active' check (status in ('active', 'archived')),
sort_order integer not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists public.tenant_billing_profiles (
tenant_id uuid primary key references public.tenants(id) on delete cascade,
billing_name text,
tax_id text,
contact_name text,
contact_phone text,
contact_email citext,
billing_address text,
invoice_title text,
invoice_type text check (invoice_type in ('none', 'normal_vat', 'special_vat')),
bank_name text,
bank_account_masked text,
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists public.tenant_invoices (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
invoice_no text not null unique,
invoice_type text not null default 'subscription' check (invoice_type in ('subscription', 'service_fee', 'usage_overage', 'manual_adjustment')),
status text not null default 'draft' check (status in ('draft', 'issued', 'paid', 'void', 'overdue')),
currency text not null default 'CNY',
subtotal_cents integer not null default 0,
discount_cents integer not null default 0,
tax_cents integer not null default 0,
total_cents integer not null default 0,
paid_cents integer not null default 0,
balance_cents integer not null default 0,
billing_period_start date,
billing_period_end date,
due_date date,
issued_at timestamptz,
paid_at timestamptz,
note text,
metadata jsonb not null default '{}'::jsonb,
created_by uuid references public.platform_users(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists public.tenant_invoice_items (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
invoice_id uuid not null references public.tenant_invoices(id) on delete cascade,
item_type text not null default 'subscription',
item_ref_id uuid,
description text not null,
quantity numeric(12,2) not null default 1,
unit_amount_cents integer not null default 0,
amount_cents integer not null default 0,
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now()
);
create table if not exists public.tenant_invoice_payments (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
invoice_id uuid not null references public.tenant_invoices(id) on delete cascade,
payment_no text not null unique,
provider text not null default 'manual',
method text,
status text not null default 'paid' check (status in ('pending', 'paid', 'failed', 'refunded')),
amount_cents integer not null default 0,
paid_at timestamptz,
provider_trade_no text,
received_by uuid references public.platform_users(id) on delete set null,
raw_payload 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_saas_plans_status on public.platform_saas_plans(status, sort_order);
create index if not exists idx_tenant_subscriptions_tenant_status on public.tenant_subscriptions(tenant_id, status, expires_at desc);
create index if not exists idx_tenant_usage_records_lookup on public.tenant_usage_records(tenant_id, metric_key, period_start, period_end);
create index if not exists idx_tenant_invoices_tenant_status on public.tenant_invoices(tenant_id, status, due_date desc);
create index if not exists idx_tenant_invoice_items_invoice on public.tenant_invoice_items(invoice_id);
create index if not exists idx_tenant_invoice_payments_invoice on public.tenant_invoice_payments(invoice_id, status);
alter table public.platform_saas_plans enable row level security;
alter table public.tenant_billing_profiles enable row level security;
alter table public.tenant_invoices enable row level security;
alter table public.tenant_invoice_items enable row level security;
alter table public.tenant_invoice_payments enable row level security;
drop policy if exists platform_admin_saas_plans on public.platform_saas_plans;
create policy platform_admin_saas_plans on public.platform_saas_plans
for all
using (app.is_platform_admin())
with check (app.is_platform_admin());
do $$
declare
table_name text;
begin
foreach table_name in array array[
'tenant_billing_profiles',
'tenant_invoices',
'tenant_invoice_items',
'tenant_invoice_payments'
]
loop
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
);
end loop;
end $$;
do $$
declare
table_name text;
begin
foreach table_name in array array[
'platform_saas_plans',
'tenant_billing_profiles',
'tenant_invoices',
'tenant_invoice_payments'
]
loop
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 $$;
insert into public.platform_saas_plans (
code, name, description, billing_cycle, base_amount_cents,
included_quotas, overage_prices, feature_flags, sort_order
)
values
(
'starter_yearly',
'合作商基础版',
'适合单地区题库合作商,含基础品牌与域名能力。',
'yearly',
980000,
'{"students":1000,"questions":5000,"storageGb":20}'::jsonb,
'{"studentsExtraPerYearCents":500,"storageGbPerYearCents":12000}'::jsonb,
'{"customDomain":true,"tenantBranding":true,"paymentTenantCollect":false}'::jsonb,
10
),
(
'pro_yearly',
'合作商专业版',
'适合多地区、多课程运营,支持更多运营工具。',
'yearly',
1980000,
'{"students":5000,"questions":30000,"storageGb":100}'::jsonb,
'{"studentsExtraPerYearCents":300,"storageGbPerYearCents":9000}'::jsonb,
'{"customDomain":true,"tenantBranding":true,"paymentTenantCollect":true,"crmWebhook":true}'::jsonb,
20
)
on conflict (code)
do update set name = excluded.name,
description = excluded.description,
billing_cycle = excluded.billing_cycle,
base_amount_cents = excluded.base_amount_cents,
included_quotas = excluded.included_quotas,
overage_prices = excluded.overage_prices,
feature_flags = excluded.feature_flags,
sort_order = excluded.sort_order,
updated_at = now();