feat(saas): add billing infrastructure — tables, types, service, handlers
B1.1 Billing database: - 5 tables: billing_plans, billing_subscriptions, billing_invoices, billing_payments, billing_usage_quotas - Seed data: Free(¥0)/Pro(¥49)/Team(¥199) plans - JSONB limits for flexible plan configuration Billing module (crates/zclaw-saas/src/billing/): - types.rs: BillingPlan, Subscription, Invoice, Payment, UsageQuota - service.rs: plan CRUD, subscription lookup, usage tracking, quota check - handlers.rs: REST API (plans list/detail, subscription, usage) - mod.rs: routes registered at /api/v1/billing/* Cargo.toml: added chrono feature to sqlx for DateTime<Utc> support
This commit is contained in:
133
crates/zclaw-saas/migrations/20260402000001_billing_tables.sql
Normal file
133
crates/zclaw-saas/migrations/20260402000001_billing_tables.sql
Normal file
@@ -0,0 +1,133 @@
|
||||
-- Migration: Billing tables for subscription management
|
||||
-- Supports: Free/Pro/Team plans, Alipay + WeChat Pay, usage quotas
|
||||
|
||||
-- Plan definitions (Free/Pro/Team)
|
||||
CREATE TABLE IF NOT EXISTS billing_plans (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
price_cents INTEGER NOT NULL DEFAULT 0,
|
||||
currency TEXT NOT NULL DEFAULT 'CNY',
|
||||
interval TEXT NOT NULL DEFAULT 'month',
|
||||
features JSONB NOT NULL DEFAULT '{}',
|
||||
limits JSONB NOT NULL DEFAULT '{}',
|
||||
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_plans_status ON billing_plans(status);
|
||||
|
||||
-- Account subscriptions
|
||||
CREATE TABLE IF NOT EXISTS billing_subscriptions (
|
||||
id TEXT PRIMARY KEY,
|
||||
account_id TEXT NOT NULL,
|
||||
plan_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
current_period_start TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
current_period_end TIMESTAMPTZ NOT NULL,
|
||||
trial_end TIMESTAMPTZ,
|
||||
canceled_at TIMESTAMPTZ,
|
||||
cancel_at_period_end BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (plan_id) REFERENCES billing_plans(id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_sub_account ON billing_subscriptions(account_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_billing_sub_active
|
||||
ON billing_subscriptions(account_id)
|
||||
WHERE status IN ('trial', 'active', 'past_due');
|
||||
|
||||
-- Invoices
|
||||
CREATE TABLE IF NOT EXISTS billing_invoices (
|
||||
id TEXT PRIMARY KEY,
|
||||
account_id TEXT NOT NULL,
|
||||
subscription_id TEXT,
|
||||
plan_id TEXT,
|
||||
amount_cents INTEGER NOT NULL,
|
||||
currency TEXT NOT NULL DEFAULT 'CNY',
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
due_at TIMESTAMPTZ,
|
||||
paid_at TIMESTAMPTZ,
|
||||
voided_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (subscription_id) REFERENCES billing_subscriptions(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (plan_id) REFERENCES billing_plans(id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_inv_account ON billing_invoices(account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_inv_status ON billing_invoices(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_inv_time ON billing_invoices(created_at);
|
||||
|
||||
-- Payment records (Alipay / WeChat Pay)
|
||||
CREATE TABLE IF NOT EXISTS billing_payments (
|
||||
id TEXT PRIMARY KEY,
|
||||
invoice_id TEXT NOT NULL,
|
||||
account_id TEXT NOT NULL,
|
||||
amount_cents INTEGER NOT NULL,
|
||||
currency TEXT NOT NULL DEFAULT 'CNY',
|
||||
method TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
external_trade_no TEXT,
|
||||
paid_at TIMESTAMPTZ,
|
||||
refunded_at TIMESTAMPTZ,
|
||||
failure_reason TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
FOREIGN KEY (invoice_id) REFERENCES billing_invoices(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_pay_invoice ON billing_payments(invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_pay_account ON billing_payments(account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_pay_trade_no ON billing_payments(external_trade_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_pay_status ON billing_payments(status);
|
||||
|
||||
-- Monthly usage quotas (per account per billing period)
|
||||
CREATE TABLE IF NOT EXISTS billing_usage_quotas (
|
||||
id TEXT PRIMARY KEY,
|
||||
account_id TEXT NOT NULL,
|
||||
period_start TIMESTAMPTZ NOT NULL,
|
||||
period_end TIMESTAMPTZ NOT NULL,
|
||||
input_tokens BIGINT NOT NULL DEFAULT 0,
|
||||
output_tokens BIGINT NOT NULL DEFAULT 0,
|
||||
relay_requests INTEGER NOT NULL DEFAULT 0,
|
||||
hand_executions INTEGER NOT NULL DEFAULT 0,
|
||||
pipeline_runs INTEGER NOT NULL DEFAULT 0,
|
||||
max_input_tokens BIGINT,
|
||||
max_output_tokens BIGINT,
|
||||
max_relay_requests INTEGER,
|
||||
max_hand_executions INTEGER,
|
||||
max_pipeline_runs INTEGER,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
UNIQUE(account_id, period_start)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_usage_account ON billing_usage_quotas(account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_billing_usage_period ON billing_usage_quotas(period_start, period_end);
|
||||
|
||||
-- Seed: default plans
|
||||
INSERT INTO billing_plans (id, name, display_name, description, price_cents, interval, features, limits, is_default, sort_order)
|
||||
VALUES
|
||||
('plan-free', 'free', '免费版', '基础功能,适合个人体验', 0, 'month',
|
||||
'{"hands": ["browser", "collector", "researcher"], "chat_modes": ["flash", "thinking"], "pipelines": 3, "support": "community"}'::jsonb,
|
||||
'{"max_input_tokens_monthly": 500000, "max_output_tokens_monthly": 500000, "max_relay_requests_monthly": 100, "max_hand_executions_monthly": 20, "max_pipeline_runs_monthly": 5}'::jsonb,
|
||||
TRUE, 0),
|
||||
('plan-pro', 'pro', '专业版', '全功能解锁,适合知识工作者', 4900, 'month',
|
||||
'{"hands": "all", "chat_modes": "all", "pipelines": -1, "support": "priority", "memory": true, "export": true}'::jsonb,
|
||||
'{"max_input_tokens_monthly": 5000000, "max_output_tokens_monthly": 5000000, "max_relay_requests_monthly": 2000, "max_hand_executions_monthly": 200, "max_pipeline_runs_monthly": 100}'::jsonb,
|
||||
FALSE, 1),
|
||||
('plan-team', 'team', '团队版', '多席位协作,适合企业团队', 19900, 'month',
|
||||
'{"hands": "all", "chat_modes": "all", "pipelines": -1, "support": "dedicated", "memory": true, "export": true, "sharing": true, "admin": true}'::jsonb,
|
||||
'{"max_input_tokens_monthly": 50000000, "max_output_tokens_monthly": 50000000, "max_relay_requests_monthly": 20000, "max_hand_executions_monthly": 1000, "max_pipeline_runs_monthly": 500}'::jsonb,
|
||||
FALSE, 2)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
Reference in New Issue
Block a user