Files
2026-05-09 13:58:50 +08:00

255 lines
7.6 KiB
PL/PgSQL

create extension if not exists "pgcrypto";
create table if not exists public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
email text not null,
full_name text,
avatar_url text,
role text not null default 'user' check (role in ('user', 'super_admin')),
credit_balance integer not null default 0 check (credit_balance >= 0),
free_generations_used integer not null default 0 check (free_generations_used >= 0),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists public.credit_transactions (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
amount integer not null,
type text not null check (type in ('grant', 'purchase', 'generation', 'refund', 'adjustment')),
source text,
reference_id uuid,
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now()
);
create table if not exists public.generation_reservations (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
case_id integer not null,
prompt text not null,
status text not null default 'pending' check (status in ('pending', 'succeeded', 'failed')),
used_free_generation boolean not null default false,
credit_amount integer not null default 0 check (credit_amount >= 0),
error_code text,
created_at timestamptz not null default now(),
completed_at timestamptz
);
create index if not exists profiles_email_idx on public.profiles (lower(email));
create index if not exists profiles_role_idx on public.profiles (role);
create index if not exists credit_transactions_user_id_idx on public.credit_transactions (user_id, created_at desc);
create index if not exists generation_reservations_user_id_idx on public.generation_reservations (user_id, created_at desc);
create index if not exists generation_reservations_status_idx on public.generation_reservations (status);
alter table public.profiles enable row level security;
alter table public.credit_transactions enable row level security;
alter table public.generation_reservations enable row level security;
drop policy if exists "Users can read own profile" on public.profiles;
create policy "Users can read own profile"
on public.profiles for select
using ((select auth.uid()) = id);
drop policy if exists "Users can read own credit transactions" on public.credit_transactions;
create policy "Users can read own credit transactions"
on public.credit_transactions for select
using ((select auth.uid()) = user_id);
drop policy if exists "Users can read own generation reservations" on public.generation_reservations;
create policy "Users can read own generation reservations"
on public.generation_reservations for select
using ((select auth.uid()) = user_id);
create or replace function public.set_updated_at()
returns trigger
language plpgsql
set search_path = public
as $$
begin
new.updated_at = now();
return new;
end;
$$;
drop trigger if exists profiles_set_updated_at on public.profiles;
create trigger profiles_set_updated_at
before update on public.profiles
for each row
execute function public.set_updated_at();
create or replace function public.reserve_generation_usage(
p_user_id uuid,
p_case_id integer,
p_prompt text
)
returns table (
reservation_id uuid,
used_free_generation boolean,
credit_amount integer,
free_generations_used integer,
credit_balance integer
)
language plpgsql
security definer
set search_path = public
as $$
declare
v_profile public.profiles%rowtype;
v_reservation_id uuid;
begin
select *
into v_profile
from public.profiles
where id = p_user_id
for update;
if not found then
raise exception 'PROFILE_NOT_FOUND' using errcode = 'P0001';
end if;
if v_profile.free_generations_used < 1 then
update public.profiles
set free_generations_used = free_generations_used + 1
where id = p_user_id
returning * into v_profile;
insert into public.generation_reservations (
user_id,
case_id,
prompt,
used_free_generation,
credit_amount
)
values (p_user_id, p_case_id, p_prompt, true, 0)
returning id into v_reservation_id;
return query
select v_reservation_id, true, 0, v_profile.free_generations_used, v_profile.credit_balance;
return;
end if;
if v_profile.credit_balance >= 1 then
update public.profiles
set credit_balance = credit_balance - 1
where id = p_user_id
returning * into v_profile;
insert into public.generation_reservations (
user_id,
case_id,
prompt,
used_free_generation,
credit_amount
)
values (p_user_id, p_case_id, p_prompt, false, 1)
returning id into v_reservation_id;
insert into public.credit_transactions (
user_id,
amount,
type,
source,
reference_id,
metadata
)
values (
p_user_id,
-1,
'generation',
'case_generation_test',
v_reservation_id,
jsonb_build_object('caseId', p_case_id)
);
return query
select v_reservation_id, false, 1, v_profile.free_generations_used, v_profile.credit_balance;
return;
end if;
raise exception 'CREDITS_REQUIRED' using errcode = 'P0001';
end;
$$;
create or replace function public.complete_generation_reservation(
p_reservation_id uuid
)
returns void
language plpgsql
security definer
set search_path = public
as $$
begin
update public.generation_reservations
set status = 'succeeded',
completed_at = now()
where id = p_reservation_id
and status = 'pending';
end;
$$;
create or replace function public.release_generation_reservation(
p_reservation_id uuid,
p_error_code text default null
)
returns void
language plpgsql
security definer
set search_path = public
as $$
declare
v_reservation public.generation_reservations%rowtype;
begin
select *
into v_reservation
from public.generation_reservations
where id = p_reservation_id
for update;
if not found or v_reservation.status <> 'pending' then
return;
end if;
if v_reservation.used_free_generation then
update public.profiles
set free_generations_used = greatest(free_generations_used - 1, 0)
where id = v_reservation.user_id;
elsif v_reservation.credit_amount > 0 then
update public.profiles
set credit_balance = credit_balance + v_reservation.credit_amount
where id = v_reservation.user_id;
insert into public.credit_transactions (
user_id,
amount,
type,
source,
reference_id,
metadata
)
values (
v_reservation.user_id,
v_reservation.credit_amount,
'refund',
'generation_failed',
v_reservation.id,
jsonb_build_object('caseId', v_reservation.case_id, 'error', p_error_code)
);
end if;
update public.generation_reservations
set status = 'failed',
error_code = p_error_code,
completed_at = now()
where id = p_reservation_id;
end;
$$;
revoke execute on function public.reserve_generation_usage(uuid, integer, text) from public, anon, authenticated;
revoke execute on function public.complete_generation_reservation(uuid) from public, anon, authenticated;
revoke execute on function public.release_generation_reservation(uuid, text) from public, anon, authenticated;
grant execute on function public.reserve_generation_usage(uuid, integer, text) to service_role;
grant execute on function public.complete_generation_reservation(uuid) to service_role;
grant execute on function public.release_generation_reservation(uuid, text) to service_role;