begin; create table if not exists public.feature_flags ( id uuid primary key default gen_random_uuid(), flag_key text not null check (flag_key ~ '^[a-z][a-z0-9_.-]{2,79}$'), version integer not null check (version > 0), enabled boolean not null default false, rollout_percentage integer not null default 100 check (rollout_percentage between 0 and 100), config jsonb not null default '{}'::jsonb check (jsonb_typeof(config)='object'), status text not null default 'draft' check (status in ('draft','published','retired')), created_by uuid references auth.users(id) on delete set null, created_at timestamptz not null default now(), published_at timestamptz, unique(flag_key,version) ); create unique index if not exists feature_flags_one_draft_idx on public.feature_flags(flag_key) where status='draft'; create unique index if not exists feature_flags_one_published_idx on public.feature_flags(flag_key) where status='published'; create table if not exists public.notification_templates ( id uuid primary key default gen_random_uuid(), template_key text not null check (template_key in ('subscription.expiring','payment.grant_failed','fair_use.blocked')), version integer not null check (version>0), channel text not null default 'in_app' check (channel in ('in_app','email')), subject text not null default '' check (char_length(subject)<=160), body text not null check (char_length(body) between 1 and 4000), status text not null default 'draft' check (status in ('draft','published','retired')), created_by uuid references auth.users(id) on delete set null, created_at timestamptz not null default now(), published_at timestamptz, unique(template_key,channel,version) ); create unique index if not exists notification_templates_published_idx on public.notification_templates(template_key,channel) where status='published'; create table if not exists public.pricing_experiment_events ( id uuid primary key default gen_random_uuid(), user_id uuid references auth.users(id) on delete set null, experiment_key text not null, variant text not null, event_name text not null check (event_name in ('view','checkout','paid','cancelled')), product_code text, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default clock_timestamp() ); create index if not exists pricing_experiment_events_created_idx on public.pricing_experiment_events(experiment_key,created_at desc); insert into public.feature_flags(flag_key,version,enabled,rollout_percentage,config,status,created_at,published_at) values ('billing.subscriptions',1,true,100,'{}','published',now(),now()), ('models.database_catalog',1,true,100,'{}','published',now(),now()), ('models.circuit_breaker',1,false,0,'{"failureThreshold":5,"cooldownSeconds":300}','published',now(),now()), ('billing.pro_products',1,false,0,'{}','published',now(),now()) on conflict(flag_key,version) do nothing; insert into public.notification_templates(template_key,version,channel,subject,body,status,published_at) values ('subscription.expiring',1,'in_app','会员即将到期','你的会员权益即将到期,可在账户页查看结束时间。','published',now()), ('payment.grant_failed',1,'in_app','支付权益待处理','支付已确认,但权益发放仍在重试。请勿重复付款。','published',now()), ('fair_use.blocked',1,'in_app','已达到合理使用上限','当前使用频率已达到会员合理使用上限,请稍后再试。','published',now()) on conflict(template_key,channel,version) do nothing; create or replace function public.admin_save_feature_flag( p_actor_user_id uuid,p_flag_id uuid,p_flag_key text,p_enabled boolean,p_rollout_percentage integer, p_config jsonb,p_expected_version integer,p_reason text,p_request_id text ) returns uuid language plpgsql security definer set search_path='' as $$ declare v_id uuid; v_version integer; begin if not public.admin_has_permission(p_actor_user_id,'ops.flags.write') then raise exception 'admin_permission_denied' using errcode='42501'; end if; if char_length(btrim(coalesce(p_reason,''))) not between 1 and 500 then raise exception 'admin_reason_required' using errcode='22023'; end if; if p_flag_id is null then select coalesce(max(version),0)+1 into v_version from public.feature_flags where flag_key=p_flag_key; insert into public.feature_flags(flag_key,version,enabled,rollout_percentage,config,created_by) values(p_flag_key,v_version,p_enabled,p_rollout_percentage,coalesce(p_config,'{}'::jsonb),p_actor_user_id) returning id into v_id; else update public.feature_flags set enabled=p_enabled,rollout_percentage=p_rollout_percentage,config=coalesce(p_config,'{}'::jsonb) where id=p_flag_id and status='draft' and version=p_expected_version returning id into v_id; if v_id is null then raise exception 'feature_flag_version_conflict' using errcode='40001'; end if; end if; insert into audit.admin_audit_logs(actor_user_id,actor_email,actor_role,action,target_type,target_id,after_value, request_id,permission_used,reason) select p_actor_user_id,lower(btrim(u.email)),'admin','ops.flag.draft.save','feature_flag',v_id, jsonb_build_object('flagKey',p_flag_key,'enabled',p_enabled,'rolloutPercentage',p_rollout_percentage), p_request_id,'ops.flags.write',btrim(p_reason) from identity.users u where u.id=p_actor_user_id on conflict do nothing; return v_id; end $$; create or replace function public.admin_publish_feature_flag( p_actor_user_id uuid,p_flag_id uuid,p_expected_version integer,p_reason text,p_request_id text ) returns uuid language plpgsql security definer set search_path='' as $$ declare v_flag public.feature_flags%rowtype; begin if not public.admin_has_permission(p_actor_user_id,'ops.flags.write') then raise exception 'admin_permission_denied' using errcode='42501'; end if; if char_length(btrim(coalesce(p_reason,''))) not between 1 and 500 then raise exception 'admin_reason_required' using errcode='22023'; end if; select * into v_flag from public.feature_flags where id=p_flag_id and status='draft' and version=p_expected_version for update; if not found then raise exception 'feature_flag_version_conflict' using errcode='40001'; end if; update public.feature_flags set status='retired' where flag_key=v_flag.flag_key and status='published'; update public.feature_flags set status='published',published_at=clock_timestamp() where id=p_flag_id; insert into audit.admin_audit_logs(actor_user_id,actor_email,actor_role,action,target_type,target_id,after_value, request_id,permission_used,reason) select p_actor_user_id,lower(btrim(u.email)),'admin','ops.flag.publish','feature_flag',p_flag_id, jsonb_build_object('flagKey',v_flag.flag_key,'version',v_flag.version,'enabled',v_flag.enabled), p_request_id,'ops.flags.write',btrim(p_reason) from identity.users u where u.id=p_actor_user_id on conflict do nothing; return p_flag_id; end $$; create or replace view public.admin_billing_overview as select (select count(*) from public.user_subscriptions where status='active' and starts_at<=now() and ends_at>now())::bigint as active_subscriptions, (select count(*) from public.payment_orders where status='paid' and paid_at>=date_trunc('day',now()))::bigint as paid_orders_today, (select coalesce(sum(money_cents),0) from public.payment_orders where status='paid' and paid_at>=date_trunc('month',now()))::bigint as revenue_cents_month, (select count(*) from public.payment_orders where status='grant_pending' or grant_status='failed')::bigint as grant_failures, (select count(*) from public.usage_reservations where status='completed' and reserved_at>=date_trunc('day',now()))::bigint as completed_usage_today, (select coalesce(sum(cost_microusd),0) from public.usage_ledger where created_at>=date_trunc('month',now()))::bigint as model_cost_microusd_month; alter table public.feature_flags enable row level security; alter table public.notification_templates enable row level security; alter table public.pricing_experiment_events enable row level security; revoke all on table public.feature_flags,public.notification_templates,public.pricing_experiment_events from public,anon,authenticated; grant select on table public.feature_flags,public.notification_templates,public.pricing_experiment_events to service_role; revoke all on function public.admin_save_feature_flag(uuid,uuid,text,boolean,integer,jsonb,integer,text,text), public.admin_publish_feature_flag(uuid,uuid,integer,text,text) from public,anon,authenticated; grant execute on function public.admin_save_feature_flag(uuid,uuid,text,boolean,integer,jsonb,integer,text,text), public.admin_publish_feature_flag(uuid,uuid,integer,text,text) to service_role; do $$ begin if exists(select 1 from pg_roles where rolname='admin_runtime') then grant select on table public.feature_flags,public.notification_templates,public.pricing_experiment_events to admin_runtime; grant select on public.admin_billing_overview to admin_runtime; grant execute on function public.admin_save_feature_flag(uuid,uuid,text,boolean,integer,jsonb,integer,text,text), public.admin_publish_feature_flag(uuid,uuid,integer,text,text) to admin_runtime; end if; end $$; commit;