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
134 lines
6.3 KiB
SQL
134 lines
6.3 KiB
SQL
-- 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;
|