-- Supabase SQL Editor で実行-- 1) ポイント台帳(1行 = 1トランザクション)。idempotency_key で二重加点を防ぐcreate table public.point_ledger ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, action text not null, points integer not null, idempotency_key text not null, created_at timestamptz not null default now(), constraint uq_idem unique (user_id, idempotency_key));-- 2) 集計済みスコア(リーダーボード用)create table public.user_scores ( user_id uuid primary key references auth.users(id) on delete cascade, display_name text not null, total_points integer not null default 0, updated_at timestamptz not null default now());-- 3) サーバー権威の加点関数。点数表はサーバーが持つ(クライアントは送らない)create or replace function public.award_points( p_action text, p_idempotency_key text) returns integerlanguage plpgsqlsecurity defineras $$declare v_user uuid := auth.uid(); v_points integer; v_total integer;begin if v_user is null then raise exception 'not authenticated'; end if; -- 点数はサーバー側のソース・オブ・トゥルース v_points := case p_action when 'daily_login' then 10 when 'complete_task' then 25 when 'share_content' then 15 when 'invite_friend' then 50 else 0 end; if v_points = 0 then raise exception 'unknown action: %', p_action; end if; -- 冪等性: 同じキーが既にあれば加点せず現在値を返す begin insert into public.point_ledger(user_id, action, points, idempotency_key) values (v_user, p_action, v_points, p_idempotency_key); exception when unique_violation then select total_points into v_total from public.user_scores where user_id = v_user; return coalesce(v_total, 0); end; -- 集計を1文でアトミックに更新(読み取り→書き込みの競合を避ける) insert into public.user_scores(user_id, display_name, total_points) values (v_user, coalesce((auth.jwt() ->> 'name'), 'Player'), v_points) on conflict (user_id) do update set total_points = public.user_scores.total_points + excluded.total_points, updated_at = now() returning total_points into v_total; return v_total;end;$$;
-- 自分の順位の前後5人だけを返す(ウィンドウ表示)create or replace function public.leaderboard_around_me()returns table(rnk bigint, display_name text, total_points integer, is_me boolean)language sql security definer as $$ with ranked as ( select user_id, display_name, total_points, rank() over (order by total_points desc) as rnk from public.user_scores ), me as (select rnk from ranked where user_id = auth.uid()) select r.rnk, r.display_name, r.total_points, (r.user_id = auth.uid()) as is_me from ranked r, me where r.rnk between me.rnk - 5 and me.rnk + 5 order by r.rnk;$$;