feat: add local auth billing and usage management

This commit is contained in:
inman committed 2026-08-12 12:13:06 +08:00
1 parent f642b5e71f
commit 196fdde83f
119 files changed
+15695 -2650

No files matched your search

+237 -2
View File
@@ -42,6 +42,7 @@ create table if not exists generation_jobs (
webhook_url text,
webhook_attempts integer not null default 0,
webhook_last_status jsonb,
usage_context jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
@@ -60,17 +61,63 @@ alter table generation_jobs add column if not exists completed_at timestamptz;
alter table generation_jobs add column if not exists webhook_url text;
alter table generation_jobs add column if not exists webhook_attempts integer not null default 0;
alter table generation_jobs add column if not exists webhook_last_status jsonb;
alter table generation_jobs add column if not exists usage_context jsonb;
create table if not exists usage_events (
id text primary key,
owner_id text not null,
job_id text not null references generation_jobs(id) on delete cascade,
job_id text not null,
source text not null default 'platform',
capability text not null,
provider text,
req_key text,
account_username text,
account_display_name text,
tenant_id text,
organization_id text,
organization_name text,
quantity integer not null default 1,
estimated_unit text not null default 'image',
estimated_unit text not null default 'job',
created_at timestamptz not null default now()
);
alter table usage_events add column if not exists source text;
alter table usage_events add column if not exists provider text;
alter table usage_events add column if not exists req_key text;
alter table usage_events add column if not exists account_username text;
alter table usage_events add column if not exists account_display_name text;
alter table usage_events add column if not exists tenant_id text;
alter table usage_events add column if not exists organization_id text;
alter table usage_events add column if not exists organization_name text;
update usage_events as usage
set source = case
when jobs.external_client_id is not null or usage.owner_id like 'api:%' then 'api'
else 'platform'
end,
provider = coalesce(usage.provider, jobs.provider),
req_key = coalesce(usage.req_key, jobs.req_key),
quantity = 1,
estimated_unit = 'job'
from generation_jobs as jobs
where jobs.id = usage.job_id;
update usage_events
set source = case when owner_id like 'api:%' then 'api' else 'platform' end,
quantity = 1,
estimated_unit = 'job'
where source is null;
alter table usage_events alter column source set default 'platform';
alter table usage_events alter column source set not null;
alter table usage_events alter column estimated_unit set default 'job';
alter table usage_events drop constraint if exists usage_events_job_id_fkey;
delete from usage_events as later
using usage_events as earlier
where later.job_id = earlier.job_id
and (later.created_at, later.id) > (earlier.created_at, earlier.id);
create table if not exists projects (
id text primary key,
owner_id text not null,
@@ -104,6 +151,9 @@ create unique index if not exists generation_jobs_idempotency_idx
on generation_jobs(owner_id, external_client_id, idempotency_key)
where external_client_id is not null and idempotency_key is not null;
create index if not exists usage_events_owner_created_idx on usage_events(owner_id, created_at desc);
create unique index if not exists usage_events_job_id_idx on usage_events(job_id);
create index if not exists usage_events_source_created_idx on usage_events(source, created_at desc);
create index if not exists usage_events_organization_created_idx on usage_events(organization_id, created_at desc);
create index if not exists image_templates_owner_sort_idx on image_templates(owner_id, sort_order asc, updated_at desc);
create or replace function claim_generation_jobs(
@@ -143,3 +193,188 @@ begin
select * from updated;
end;
$$;
create table if not exists platform_organizations (
id text primary key,
name text not null unique,
status text not null default 'active' check (status in ('active', 'disabled')),
archive_owner_id text not null unique,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists platform_users (
id text primary key,
phone text not null unique,
display_name text not null,
role text not null default 'user' check (role in ('super_admin', 'organization_admin', 'user')),
organization_id text references platform_organizations(id) on delete set null,
status text not null default 'active' check (status in ('active', 'disabled')),
password_hash text not null,
password_salt text not null,
failed_login_count integer not null default 0,
locked_until timestamptz,
session_version integer not null default 1,
last_login_at timestamptz,
legacy_subject text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists platform_account_migrations (
id text primary key,
legacy_owner_id text not null unique,
legacy_phone text,
platform_user_id text not null,
created_at timestamptz not null default now()
);
create index if not exists platform_users_organization_idx on platform_users(organization_id, created_at desc);
create index if not exists platform_users_status_idx on platform_users(status, role);
create index if not exists platform_account_migrations_user_idx on platform_account_migrations(platform_user_id);
alter table generation_jobs add column if not exists billing jsonb;
alter table usage_events add column if not exists charged_amount_fen bigint;
alter table usage_events add column if not exists currency text;
create table if not exists billing_price_rules (
id text primary key,
provider text not null check (provider in ('volcengine-visual', 'evolink', 'seedance', 'bailian', 'mock')),
capability text not null check (capability in ('image.generate', 'video.generate')),
req_key text,
variant_key text,
unit text not null check (unit in ('request', 'image', 'video_second')),
standard_unit_price_fen bigint not null check (standard_unit_price_fen >= 0),
markup_multiplier numeric(12, 4) not null default 1.0000 check (markup_multiplier >= 1),
enabled boolean not null default true,
conditions jsonb not null default '{}'::jsonb,
quantity_source text check (quantity_source in ('request', 'image_count', 'duration')),
priority integer not null default 0,
note text,
source jsonb,
parameter_dimensions jsonb not null default '[]'::jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table billing_price_rules add column if not exists variant_key text;
alter table billing_price_rules add column if not exists source jsonb;
alter table billing_price_rules add column if not exists conditions jsonb not null default '{}'::jsonb;
alter table billing_price_rules add column if not exists quantity_source text;
alter table billing_price_rules add column if not exists priority integer not null default 0;
alter table billing_price_rules add column if not exists parameter_dimensions jsonb not null default '[]'::jsonb;
alter table billing_price_rules drop constraint if exists billing_price_rules_quantity_source_check;
alter table billing_price_rules add constraint billing_price_rules_quantity_source_check check (quantity_source is null or quantity_source in ('request', 'image_count', 'duration'));
drop index if exists billing_price_rules_match_idx;
create unique index if not exists billing_price_rules_match_idx
on billing_price_rules(provider, capability, coalesce(req_key, ''), coalesce(variant_key, ''), coalesce(conditions, '{}'::jsonb));
create table if not exists billing_wallets (
organization_id text primary key references platform_organizations(id) on delete restrict,
balance_fen bigint not null default 0 check (balance_fen >= 0),
total_recharged_fen bigint not null default 0 check (total_recharged_fen >= 0),
total_charged_fen bigint not null default 0 check (total_charged_fen >= 0),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists billing_ledger (
id text primary key,
organization_id text not null references platform_organizations(id) on delete restrict,
account_id text,
job_id text,
kind text not null check (kind in ('recharge', 'charge', 'refund', 'adjustment')),
delta_fen bigint not null check (delta_fen <> 0),
balance_after_fen bigint not null check (balance_after_fen >= 0),
currency text not null default 'CNY' check (currency = 'CNY'),
idempotency_key text not null unique,
description text not null,
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now()
);
create index if not exists billing_ledger_organization_created_idx on billing_ledger(organization_id, created_at desc);
create index if not exists billing_ledger_account_created_idx on billing_ledger(account_id, created_at desc);
create index if not exists billing_ledger_job_idx on billing_ledger(job_id);
create or replace function billing_post_wallet_entry(
p_ledger_id text,
p_organization_id text,
p_account_id text,
p_job_id text,
p_kind text,
p_delta_fen bigint,
p_currency text,
p_idempotency_key text,
p_description text,
p_metadata jsonb
)
returns table (
ledger_id text,
balance_after_fen bigint,
balance_fen bigint,
total_recharged_fen bigint,
total_charged_fen bigint,
created_at timestamptz,
updated_at timestamptz,
delta_fen bigint
)
language plpgsql
as $$
declare
v_existing billing_ledger%rowtype;
v_wallet billing_wallets%rowtype;
v_entry billing_ledger%rowtype;
begin
if p_currency <> 'CNY' then
raise exception 'BILLING_UNSUPPORTED_CURRENCY';
end if;
if p_delta_fen = 0 then
raise exception 'BILLING_ZERO_DELTA';
end if;
select * into v_existing from billing_ledger where idempotency_key = p_idempotency_key;
if found then
select * into v_wallet from billing_wallets where organization_id = v_existing.organization_id;
return query select v_existing.id, v_existing.balance_after_fen, v_wallet.balance_fen,
v_wallet.total_recharged_fen, v_wallet.total_charged_fen, v_existing.created_at,
v_wallet.updated_at, v_existing.delta_fen;
return;
end if;
insert into billing_wallets(organization_id)
values (p_organization_id)
on conflict (organization_id) do nothing;
select * into v_wallet from billing_wallets
where organization_id = p_organization_id
for update;
if p_delta_fen < 0 and v_wallet.balance_fen < abs(p_delta_fen) then
raise exception 'BILLING_INSUFFICIENT_BALANCE';
end if;
update billing_wallets
set balance_fen = v_wallet.balance_fen + p_delta_fen,
total_recharged_fen = v_wallet.total_recharged_fen + case when p_kind = 'recharge' and p_delta_fen > 0 then p_delta_fen else 0 end,
total_charged_fen = v_wallet.total_charged_fen + case when p_kind = 'charge' and p_delta_fen < 0 then abs(p_delta_fen) else 0 end,
updated_at = now()
where organization_id = p_organization_id
returning * into v_wallet;
insert into billing_ledger(
id, organization_id, account_id, job_id, kind, delta_fen,
balance_after_fen, currency, idempotency_key, description, metadata
) values (
p_ledger_id, p_organization_id,
case when p_kind in ('recharge', 'adjustment') then null else p_account_id end,
p_job_id, p_kind, p_delta_fen,
v_wallet.balance_fen, p_currency, p_idempotency_key, p_description, coalesce(p_metadata, '{}'::jsonb)
) returning * into v_entry;
return query select v_entry.id, v_entry.balance_after_fen, v_wallet.balance_fen,
v_wallet.total_recharged_fen, v_wallet.total_charged_fen, v_entry.created_at,
v_wallet.updated_at, v_entry.delta_fen;
end;
$$;