- Backend: workspace_id isolation for 14 model tables + safe migration/backfill - Backend: RBAC system with 4 roles and 23 permissions, seeded on startup - Backend: workspace admin endpoints (list/manage all workspaces) - Backend: admin user management API (CRUD, reset password) - Backend: billing API with subscription plans, usage tracking, rate limiting - Backend: fix system_logs.py UNION query and wrong column references - Backend: WebSocket JWT auth and workspace enforcement - Frontend: sidebar navigation replacing top dropdown menu - Frontend: user management page (Users.vue) for admins - Frontend: enhanced Workspaces.vue with admin table view - Frontend: workspace RBAC computed properties in user store - Android: agent marketplace, billing/subscription UI, onboarding wizard - Android: phone login, analytics tracker, crash handler, network diagnostics - Android: splash screen, encrypted token storage, app update enhancements - Docs: multi-tenant RBAC guide with 8 sections and role-permission matrix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
99 lines
5.9 KiB
Python
99 lines
5.9 KiB
Python
"""Add billing tables and user subscription fields
|
|
|
|
Revision ID: 025_billing
|
|
Revises: 104e05fc9cf2
|
|
Create Date: 2026-07-03
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects.mysql import CHAR
|
|
|
|
revision = '025_billing'
|
|
down_revision = '104e05fc9cf2'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# billing_plans
|
|
op.create_table(
|
|
'billing_plans',
|
|
sa.Column('id', CHAR(36), primary_key=True, comment='套餐ID'),
|
|
sa.Column('name', sa.String(100), nullable=False, comment='套餐名称'),
|
|
sa.Column('tier', sa.String(20), nullable=False, comment='套餐等级: free/pro/enterprise'),
|
|
sa.Column('price_monthly', sa.Float, nullable=False, server_default='0', comment='月付价格(CNY)'),
|
|
sa.Column('price_yearly', sa.Float, nullable=False, server_default='0', comment='年付价格(CNY)'),
|
|
sa.Column('daily_quota', sa.Integer, nullable=False, server_default='0', comment='每日对话次数限制'),
|
|
sa.Column('agent_limit', sa.Integer, nullable=False, server_default='0', comment='智能体数量限制'),
|
|
sa.Column('knowledge_limit', sa.Integer, nullable=False, server_default='0', comment='知识条目限制'),
|
|
sa.Column('file_upload_mb', sa.Integer, nullable=False, server_default='5', comment='文件上传大小限制(MB)'),
|
|
sa.Column('models', sa.JSON, nullable=True, comment='可用模型列表'),
|
|
sa.Column('features', sa.JSON, nullable=True, comment='权益特性列表'),
|
|
sa.Column('is_active', sa.Boolean, server_default='1', comment='是否启用'),
|
|
sa.Column('sort_order', sa.Integer, server_default='0', comment='排序'),
|
|
sa.Column('created_at', sa.DateTime, server_default=sa.func.now(), comment='创建时间'),
|
|
sa.Column('updated_at', sa.DateTime, server_default=sa.func.now(), comment='更新时间'),
|
|
)
|
|
|
|
# billing_orders
|
|
op.create_table(
|
|
'billing_orders',
|
|
sa.Column('id', CHAR(36), primary_key=True, comment='订单ID'),
|
|
sa.Column('user_id', CHAR(36), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, comment='用户ID'),
|
|
sa.Column('plan_id', CHAR(36), sa.ForeignKey('billing_plans.id', ondelete='SET NULL'), nullable=True, comment='套餐ID'),
|
|
sa.Column('amount', sa.Float, nullable=False, comment='支付金额(CNY)'),
|
|
sa.Column('period', sa.String(10), nullable=False, server_default="'monthly'", comment='周期: monthly/yearly'),
|
|
sa.Column('status', sa.String(20), nullable=False, server_default="'pending'", comment='状态: pending/paid/cancelled/expired'),
|
|
sa.Column('payment_method', sa.String(20), nullable=True, comment='支付方式: mock/wechat/alipay'),
|
|
sa.Column('payment_ref', sa.String(255), nullable=True, comment='第三方支付流水号'),
|
|
sa.Column('paid_at', sa.DateTime, nullable=True, comment='支付时间'),
|
|
sa.Column('created_at', sa.DateTime, server_default=sa.func.now(), comment='创建时间'),
|
|
sa.Column('updated_at', sa.DateTime, server_default=sa.func.now(), comment='更新时间'),
|
|
)
|
|
op.create_index('ix_billing_orders_user_id', 'billing_orders', ['user_id'])
|
|
|
|
# user_subscriptions
|
|
op.create_table(
|
|
'user_subscriptions',
|
|
sa.Column('id', CHAR(36), primary_key=True, comment='订阅ID'),
|
|
sa.Column('user_id', CHAR(36), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, comment='用户ID'),
|
|
sa.Column('plan_id', CHAR(36), sa.ForeignKey('billing_plans.id', ondelete='SET NULL'), nullable=True, comment='当前套餐ID'),
|
|
sa.Column('tier', sa.String(20), nullable=False, server_default="'free'", comment='当前等级'),
|
|
sa.Column('status', sa.String(20), nullable=False, server_default="'active'", comment='状态: active/cancelled/expired'),
|
|
sa.Column('started_at', sa.DateTime, nullable=True, comment='订阅开始时间'),
|
|
sa.Column('expires_at', sa.DateTime, nullable=True, comment='订阅到期时间'),
|
|
sa.Column('auto_renew', sa.Boolean, server_default='0', comment='是否自动续费'),
|
|
sa.Column('created_at', sa.DateTime, server_default=sa.func.now(), comment='创建时间'),
|
|
sa.Column('updated_at', sa.DateTime, server_default=sa.func.now(), comment='更新时间'),
|
|
)
|
|
op.create_index('ix_user_subscriptions_user_id', 'user_subscriptions', ['user_id'], unique=True)
|
|
|
|
# usage_records
|
|
op.create_table(
|
|
'usage_records',
|
|
sa.Column('id', CHAR(36), primary_key=True, comment='记录ID'),
|
|
sa.Column('user_id', CHAR(36), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, comment='用户ID'),
|
|
sa.Column('date', sa.Date, nullable=False, comment='日期'),
|
|
sa.Column('count', sa.Integer, nullable=False, server_default='0', comment='当日对话次数'),
|
|
sa.Column('created_at', sa.DateTime, server_default=sa.func.now(), comment='创建时间'),
|
|
)
|
|
op.create_index('ix_usage_user_date', 'usage_records', ['user_id', 'date'], unique=True)
|
|
|
|
# Add columns to users
|
|
op.add_column('users', sa.Column('subscription_tier', sa.String(20), server_default="'free'", comment='订阅等级'))
|
|
op.add_column('users', sa.Column('subscription_expires_at', sa.DateTime, nullable=True, comment='订阅到期时间'))
|
|
op.add_column('users', sa.Column('daily_usage_count', sa.Integer, server_default='0', comment='今日对话次数'))
|
|
op.add_column('users', sa.Column('daily_usage_date', sa.Date, nullable=True, comment='用量统计日期'))
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_column('users', 'daily_usage_date')
|
|
op.drop_column('users', 'daily_usage_count')
|
|
op.drop_column('users', 'subscription_expires_at')
|
|
op.drop_column('users', 'subscription_tier')
|
|
op.drop_table('usage_records')
|
|
op.drop_table('user_subscriptions')
|
|
op.drop_index('ix_billing_orders_user_id', table_name='billing_orders')
|
|
op.drop_table('billing_orders')
|
|
op.drop_table('billing_plans')
|