Files
aiagent/backend/alembic/versions/025_billing.py
renjianbo 876789fac1 feat: multi-tenant workspace isolation, RBAC, sidebar nav, billing, and Android enhancements
- 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>
2026-07-04 01:00:22 +08:00

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')