132 lines
9.1 KiB
PL/PgSQL
132 lines
9.1 KiB
PL/PgSQL
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;
|