From 839be8f97fe14724abd57666d795e1c61eb51d96 Mon Sep 17 00:00:00 2001 From: qyyfw Date: Tue, 7 Jul 2026 15:39:01 +0800 Subject: [PATCH] feat: membership benefits refinement - permissions and quotas linked to plans --- .../python/app/api/admin/membership_admin.py | 611 ++++++++++- backend/python/app/models.py | 17 + frontend/src/api/index.ts | 23 +- frontend/src/pages/admin/Membership.tsx | 963 +++++++++++------- 4 files changed, 1188 insertions(+), 426 deletions(-) diff --git a/backend/python/app/api/admin/membership_admin.py b/backend/python/app/api/admin/membership_admin.py index 09de9bb..9a51569 100644 --- a/backend/python/app/api/admin/membership_admin.py +++ b/backend/python/app/api/admin/membership_admin.py @@ -1,18 +1,55 @@ """ -会员订阅体系管理 API(管理员) +会员订阅体系管理 API(管理员)— 增强版:权益细化与权限挂钩 """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session +from sqlalchemy import text from ...database import get_db, SessionLocal from ...models import MembershipPlan, MembershipSubscription, User, RiskRule from ...auth import get_current_admin from ...middleware.log_service import LogService import uuid -from datetime import datetime +import json +from datetime import datetime, date router = APIRouter() +def _format_plan(p): + """格式化会员计划数据""" + return { + "id": p.id, + "name": p.name, + "level": p.level, + "duration_type": p.duration_type, + "price": float(p.price) if p.price else 0, + "daily_free_tokens": p.daily_free_tokens or 0, + "features": p.features or [], + "free_app_ids": p.free_app_ids or [], + "free_model_ids": p.free_model_ids or [], + "is_active": p.is_active, + # 配额字段 + "max_agents": getattr(p, 'max_agents', 5) or 5, + "max_workflows": getattr(p, 'max_workflows', 3) or 3, + "max_knowledge_bases": getattr(p, 'max_knowledge_bases', 2) or 2, + "max_storage_mb": getattr(p, 'max_storage_mb', 1024) or 1024, + "api_calls_per_day": getattr(p, 'api_calls_per_day', 100) or 100, + "api_calls_per_min": getattr(p, 'api_calls_per_min', 10) or 10, + # 权限字段 + "can_publish_apps": bool(getattr(p, 'can_publish_apps', 0)), + "can_create_api_keys": bool(getattr(p, 'can_create_api_keys', 0)), + "can_use_workflow": bool(getattr(p, 'can_use_workflow', 0)), + "can_use_mcp": bool(getattr(p, 'can_use_mcp', 0)), + "can_use_code_exec": bool(getattr(p, 'can_use_code_exec', 0)), + "can_use_privacy_mode": bool(getattr(p, 'can_use_privacy_mode', 0)), + "can_use_dag": bool(getattr(p, 'can_use_dag', 0)), + "allowed_model_tiers": getattr(p, 'allowed_model_tiers', None) or ["free"], + "permissions": getattr(p, 'permissions', None) or {}, + "created_at": p.created_at.isoformat() if p.created_at else None, + "updated_at": p.updated_at.isoformat() if p.updated_at else None, + } + + # ============================================================ # 会员计划 CRUD # ============================================================ @@ -22,25 +59,9 @@ async def get_plans( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): - """获取会员计划列表""" + """获取会员计划列表(含权益配置)""" plans = db.query(MembershipPlan).order_by(MembershipPlan.level).all() - return [ - { - "id": p.id, - "name": p.name, - "level": p.level, - "duration_type": p.duration_type, - "price": float(p.price) if p.price else 0, - "daily_free_tokens": p.daily_free_tokens, - "features": p.features, - "free_app_ids": p.free_app_ids, - "free_model_ids": p.free_model_ids, - "is_active": p.is_active, - "created_at": p.created_at.isoformat() if p.created_at else None, - "updated_at": p.updated_at.isoformat() if p.updated_at else None, - } - for p in plans - ] + return [_format_plan(p) for p in plans] @router.post("/plans") @@ -53,11 +74,28 @@ async def create_plan( features: list = None, free_app_ids: list = None, free_model_ids: list = None, + # 配额参数 + max_agents: int = 5, + max_workflows: int = 3, + max_knowledge_bases: int = 2, + max_storage_mb: int = 1024, + api_calls_per_day: int = 100, + api_calls_per_min: int = 10, + # 权限参数 + can_publish_apps: bool = False, + can_create_api_keys: bool = False, + can_use_workflow: bool = False, + can_use_mcp: bool = False, + can_use_code_exec: bool = False, + can_use_privacy_mode: bool = False, + can_use_dag: bool = False, + allowed_model_tiers: list = None, + permissions: dict = None, is_active: bool = True, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): - """创建会员计划""" + """创建会员计划(含权益配置)""" plan = MembershipPlan( id=str(uuid.uuid4()), name=name, @@ -70,6 +108,24 @@ async def create_plan( free_model_ids=free_model_ids, is_active=is_active, ) + # 设置配额字段 + plan.max_agents = max_agents + plan.max_workflows = max_workflows + plan.max_knowledge_bases = max_knowledge_bases + plan.max_storage_mb = max_storage_mb + plan.api_calls_per_day = api_calls_per_day + plan.api_calls_per_min = api_calls_per_min + # 设置权限字段 + plan.can_publish_apps = can_publish_apps + plan.can_create_api_keys = can_create_api_keys + plan.can_use_workflow = can_use_workflow + plan.can_use_mcp = can_use_mcp + plan.can_use_code_exec = can_use_code_exec + plan.can_use_privacy_mode = can_use_privacy_mode + plan.can_use_dag = can_use_dag + plan.allowed_model_tiers = allowed_model_tiers or ["free"] + plan.permissions = permissions or {} + db.add(plan) db.commit() @@ -79,7 +135,7 @@ async def create_plan( action="create_membership_plan", target_type="membership_plan", target_id=plan.id, - detail=f"创建会员计划 {name}", + detail=f"创建会员计划 {name} (level={level})", ) return {"message": "会员计划已创建", "id": plan.id} @@ -96,15 +152,33 @@ async def update_plan( features: list = None, free_app_ids: list = None, free_model_ids: list = None, + # 配额参数 + max_agents: int = None, + max_workflows: int = None, + max_knowledge_bases: int = None, + max_storage_mb: int = None, + api_calls_per_day: int = None, + api_calls_per_min: int = None, + # 权限参数 + can_publish_apps: bool = None, + can_create_api_keys: bool = None, + can_use_workflow: bool = None, + can_use_mcp: bool = None, + can_use_code_exec: bool = None, + can_use_privacy_mode: bool = None, + can_use_dag: bool = None, + allowed_model_tiers: list = None, + permissions: dict = None, is_active: bool = None, admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): - """更新会员计划""" + """更新会员计划(含权益配置)""" plan = db.query(MembershipPlan).filter(MembershipPlan.id == id).first() if not plan: raise HTTPException(status_code=404, detail="会员计划不存在") + # 基础字段 if name is not None: plan.name = name if level is not None: @@ -123,6 +197,41 @@ async def update_plan( plan.free_model_ids = free_model_ids if is_active is not None: plan.is_active = is_active + + # 配额字段 + if max_agents is not None: + plan.max_agents = max_agents + if max_workflows is not None: + plan.max_workflows = max_workflows + if max_knowledge_bases is not None: + plan.max_knowledge_bases = max_knowledge_bases + if max_storage_mb is not None: + plan.max_storage_mb = max_storage_mb + if api_calls_per_day is not None: + plan.api_calls_per_day = api_calls_per_day + if api_calls_per_min is not None: + plan.api_calls_per_min = api_calls_per_min + + # 权限字段 + if can_publish_apps is not None: + plan.can_publish_apps = can_publish_apps + if can_create_api_keys is not None: + plan.can_create_api_keys = can_create_api_keys + if can_use_workflow is not None: + plan.can_use_workflow = can_use_workflow + if can_use_mcp is not None: + plan.can_use_mcp = can_use_mcp + if can_use_code_exec is not None: + plan.can_use_code_exec = can_use_code_exec + if can_use_privacy_mode is not None: + plan.can_use_privacy_mode = can_use_privacy_mode + if can_use_dag is not None: + plan.can_use_dag = can_use_dag + if allowed_model_tiers is not None: + plan.allowed_model_tiers = allowed_model_tiers + if permissions is not None: + plan.permissions = permissions + plan.updated_at = datetime.now() db.commit() @@ -132,7 +241,7 @@ async def update_plan( action="update_membership_plan", target_type="membership_plan", target_id=id, - detail=f"更新会员计划 {plan.name}", + detail=f"更新会员计划 {plan.name} (含权益配置)", ) return {"message": "会员计划已更新"} @@ -149,6 +258,11 @@ async def delete_plan( if not plan: raise HTTPException(status_code=404, detail="会员计划不存在") + # 检查是否有用户正在使用此计划 + user_count = db.query(User).filter(User.membership_plan_id == id).count() + if user_count > 0: + raise HTTPException(status_code=400, detail=f"有 {user_count} 个用户正在使用此计划,无法删除") + db.delete(plan) db.commit() @@ -164,6 +278,352 @@ async def delete_plan( return {"message": "会员计划已删除"} +# ============================================================ +# 会员权益对比(前端对比表用) +# ============================================================ + +@router.get("/benefits-comparison") +async def get_benefits_comparison( + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取会员权益对比表数据""" + plans = db.query(MembershipPlan).filter( + MembershipPlan.is_active == True + ).order_by(MembershipPlan.level).all() + + # 定义对比维度 + dimensions = [ + {"key": "daily_free_tokens", "label": "每日免费Token", "type": "number"}, + {"key": "max_agents", "label": "最大Agent数量", "type": "number"}, + {"key": "max_workflows", "label": "最大工作流数量", "type": "number"}, + {"key": "max_knowledge_bases", "label": "最大知识库数量", "type": "number"}, + {"key": "max_storage_mb", "label": "存储配额(MB)", "type": "number"}, + {"key": "api_calls_per_day", "label": "每日API调用限额", "type": "number"}, + {"key": "api_calls_per_min", "label": "每分钟API调用限额", "type": "number"}, + {"key": "can_publish_apps", "label": "发布应用到市场", "type": "boolean"}, + {"key": "can_create_api_keys", "label": "创建开发者API密钥", "type": "boolean"}, + {"key": "can_use_workflow", "label": "使用工作流编排", "type": "boolean"}, + {"key": "can_use_mcp", "label": "使用MCP工具", "type": "boolean"}, + {"key": "can_use_code_exec", "label": "使用代码执行", "type": "boolean"}, + {"key": "can_use_privacy_mode", "label": "使用隐私模式", "type": "boolean"}, + {"key": "can_use_dag", "label": "使用DAG多Agent", "type": "boolean"}, + {"key": "allowed_model_tiers", "label": "可用模型层级", "type": "list"}, + ] + + return { + "dimensions": dimensions, + "plans": [_format_plan(p) for p in plans], + } + + +# ============================================================ +# 全局会员配置管理 +# ============================================================ + +@router.get("/global-configs") +async def get_global_configs( + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取全局会员配置""" + result = db.execute(text("SELECT id, config_key, config_value, config_type, description FROM membership_global_configs ORDER BY config_key")) + configs = [] + for row in result: + val = row[2] + if row[3] == 'boolean': + val = val == 'true' + elif row[3] == 'number': + try: + val = int(val) + except: + val = 0 + elif row[3] == 'json': + try: + val = json.loads(val) + except: + pass + configs.append({ + "id": row[0], + "key": row[1], + "value": val, + "type": row[3], + "description": row[4] or "", + }) + return configs + + +@router.put("/global-configs/{key}") +async def update_global_config( + key: str, + value: str = None, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """更新全局会员配置""" + result = db.execute(text("SELECT id, config_type FROM membership_global_configs WHERE config_key = :key"), {"key": key}) + row = result.fetchone() + if not row: + raise HTTPException(status_code=404, detail="配置项不存在") + + config_id, config_type = row[0], row[1] + store_value = value + if config_type == 'boolean': + store_value = 'true' if value in ('true', '1', 'True') else 'false' + + db.execute(text("UPDATE membership_global_configs SET config_value = :val WHERE id = :id"), {"val": store_value, "id": config_id}) + db.commit() + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="update_membership_config", + target_type="membership_config", + target_id=config_id, + detail=f"更新会员配置 {key}={store_value}", + ) + + return {"message": "配置已更新"} + + +# ============================================================ +# 用户会员管理 +# ============================================================ + +@router.get("/user-memberships") +async def get_user_memberships( + user_id: str = Query(None), + plan_id: str = Query(None), + status: str = Query(None, description="会员状态: active/expired"), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取用户会员信息列表""" + query = db.query(User).filter(User.role == 'user') + + if user_id: + query = query.filter(User.id == user_id) + + users_q = query + if plan_id: + users_q = users_q.filter(User.membership_plan_id == plan_id) + + total = users_q.count() + users = users_q.order_by(User.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() + + items = [] + for u in users: + plan = db.query(MembershipPlan).filter(MembershipPlan.id == u.membership_plan_id).first() if u.membership_plan_id else None + # 判断状态 + now = datetime.now() + if u.membership_expire_at and u.membership_expire_at < now: + member_status = "expired" + elif u.membership_level > 0: + member_status = "active" + else: + member_status = "free" + + if status and member_status != status: + continue + + items.append({ + "user_id": u.id, + "username": u.username, + "email": u.email, + "membership_plan_id": u.membership_plan_id, + "membership_plan_name": plan.name if plan else "免费版", + "membership_level": u.membership_level or 0, + "membership_expire_at": u.membership_expire_at.isoformat() if u.membership_expire_at else None, + "auto_renew": bool(u.auto_renew), + "member_status": member_status, + "created_at": u.created_at.isoformat() if u.created_at else None, + }) + + return {"total": total, "page": page, "page_size": page_size, "items": items} + + +@router.put("/users/{user_id}/assign-plan") +async def assign_plan_to_user( + user_id: str, + plan_id: str, + duration_days: int = Query(30, description="订阅时长(天)"), + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """为用户分配会员计划(管理员手动分配)""" + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + plan = db.query(MembershipPlan).filter(MembershipPlan.id == plan_id).first() + if not plan: + raise HTTPException(status_code=404, detail="会员计划不存在") + + from datetime import timedelta + now = datetime.now() + # 如果用户当前会员未到期,从到期时间续期 + start_at = now + if user.membership_expire_at and user.membership_expire_at > now: + start_at = user.membership_expire_at + end_at = start_at + timedelta(days=duration_days) + + user.membership_plan_id = plan_id + user.membership_level = plan.level + user.membership_expire_at = end_at + + # 创建订阅记录 + sub = MembershipSubscription( + id=str(uuid.uuid4()), + user_id=user_id, + plan_id=plan_id, + start_at=start_at, + end_at=end_at, + status="active", + ) + db.add(sub) + db.commit() + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="assign_membership_plan", + target_type="user", + target_id=user_id, + detail=f"为用户 {user.username} 分配计划 {plan.name} ({duration_days}天)", + ) + + return {"message": f"已为用户 {user.username} 分配 {plan.name} 计划", "expire_at": end_at.isoformat()} + + +@router.put("/users/{user_id}/revoke") +async def revoke_membership( + user_id: str, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """撤销用户会员(降级为免费版)""" + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + old_plan = user.membership_plan_id + user.membership_plan_id = None + user.membership_level = 0 + user.membership_expire_at = None + user.auto_renew = False + db.commit() + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="revoke_membership", + target_type="user", + target_id=user_id, + detail=f"撤销用户 {user.username} 的会员 (原计划ID: {old_plan})", + ) + + return {"message": f"已撤销用户 {user.username} 的会员"} + + +# ============================================================ +# 用户配额使用情况 +# ============================================================ + +@router.get("/users/{user_id}/usage") +async def get_user_usage( + user_id: str, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取用户资源使用情况""" + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 获取用户计划 + plan = None + if user.membership_plan_id: + plan = db.query(MembershipPlan).filter(MembershipPlan.id == user.membership_plan_id).first() + + # 统计用户实际资源使用 + from ...models import Agent, KnowledgeBase + agents_count = db.query(Agent).filter(Agent.owner_id == user_id).count() if hasattr(Agent, 'owner_id') else 0 + + # 使用原生SQL统计 + try: + kb_result = db.execute(text("SELECT COUNT(*) FROM knowledge_bases WHERE owner_id = :uid"), {"uid": user_id}) + kb_count = kb_result.fetchone()[0] + except: + kb_count = 0 + + try: + agent_result = db.execute(text("SELECT COUNT(*) FROM agents WHERE owner_id = :uid"), {"uid": user_id}) + agent_count = agent_result.fetchone()[0] + except: + agent_count = 0 + + try: + wf_result = db.execute(text("SELECT COUNT(*) FROM workflows WHERE owner_id = :uid"), {"uid": user_id}) + wf_count = wf_result.fetchone()[0] + except: + wf_count = 0 + + # 获取今日API使用量 + today = date.today() + try: + usage_result = db.execute(text("SELECT api_calls_total, tokens_used FROM user_api_usage WHERE user_id = :uid AND usage_date = :d"), {"uid": user_id, "d": today}) + usage_row = usage_result.fetchone() + api_calls_today = usage_row[0] if usage_row else 0 + tokens_today = usage_row[1] if usage_row else 0 + except: + api_calls_today = 0 + tokens_today = 0 + + # 获取存储使用量 + try: + storage_result = db.execute(text("SELECT storage_used_mb FROM user_resource_usage WHERE user_id = :uid"), {"uid": user_id}) + storage_row = storage_result.fetchone() + storage_used = float(storage_row[0]) if storage_row else 0.0 + except: + storage_used = 0.0 + + plan_limits = {} + if plan: + plan_limits = { + "max_agents": getattr(plan, 'max_agents', 5) or 5, + "max_workflows": getattr(plan, 'max_workflows', 3) or 3, + "max_knowledge_bases": getattr(plan, 'max_knowledge_bases', 2) or 2, + "max_storage_mb": getattr(plan, 'max_storage_mb', 1024) or 1024, + "api_calls_per_day": getattr(plan, 'api_calls_per_day', 100) or 100, + "daily_free_tokens": plan.daily_free_tokens or 0, + } + + return { + "user_id": user_id, + "username": user.username, + "plan_name": plan.name if plan else "免费版", + "plan_level": user.membership_level or 0, + "usage": { + "agents_count": agent_count, + "workflows_count": wf_count, + "knowledge_bases_count": kb_count, + "storage_used_mb": storage_used, + "api_calls_today": api_calls_today, + "tokens_today": tokens_today, + }, + "limits": plan_limits, + "over_limit": { + "agents": agent_count > plan_limits.get("max_agents", 999) if plan_limits else False, + "workflows": wf_count > plan_limits.get("max_workflows", 999) if plan_limits else False, + "knowledge_bases": kb_count > plan_limits.get("max_knowledge_bases", 999) if plan_limits else False, + "storage": storage_used > plan_limits.get("max_storage_mb", 999999) if plan_limits else False, + "api_calls": api_calls_today > plan_limits.get("api_calls_per_day", 999999) if plan_limits else False, + }, + } + + # ============================================================ # 用户订阅管理 # ============================================================ @@ -190,24 +650,98 @@ async def get_subscriptions( MembershipSubscription.created_at.desc() ).offset((page - 1) * page_size).limit(page_size).all() + # 关联用户名和计划名 + items = [] + for s in subscriptions: + u = db.query(User).filter(User.id == s.user_id).first() + p = db.query(MembershipPlan).filter(MembershipPlan.id == s.plan_id).first() + items.append({ + "id": s.id, + "user_id": s.user_id, + "username": u.username if u else "", + "plan_id": s.plan_id, + "plan_name": p.name if p else "", + "order_id": s.order_id, + "start_at": s.start_at.isoformat() if s.start_at else None, + "end_at": s.end_at.isoformat() if s.end_at else None, + "auto_renew": s.auto_renew, + "status": s.status, + "created_at": s.created_at.isoformat() if s.created_at else None, + }) + return { "total": total, "page": page, "page_size": page_size, - "items": [ - { - "id": s.id, - "user_id": s.user_id, - "plan_id": s.plan_id, - "order_id": s.order_id, - "start_at": s.start_at.isoformat() if s.start_at else None, - "end_at": s.end_at.isoformat() if s.end_at else None, - "auto_renew": s.auto_renew, - "status": s.status, - "created_at": s.created_at.isoformat() if s.created_at else None, - } - for s in subscriptions - ], + "items": items, + } + + +# ============================================================ +# 会员统计概览 +# ============================================================ + +@router.get("/stats") +async def get_membership_stats( + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取会员统计概览""" + total_users = db.query(User).filter(User.role == 'user').count() + + # 各等级用户数 + level_stats = {} + for level in range(4): + count = db.query(User).filter(User.membership_level == level).count() + level_stats[f"level_{level}"] = count + + # 活跃会员(未到期) + now = datetime.now() + active_members = db.query(User).filter( + User.membership_level > 0, + User.membership_expire_at > now + ).count() + + # 已过期会员 + expired_members = db.query(User).filter( + User.membership_level > 0, + User.membership_expire_at <= now + ).count() + + # 自动续费用户数 + auto_renew_count = db.query(User).filter( + User.auto_renew == True, + User.membership_level > 0 + ).count() + + # 各计划订阅数 + plan_stats = [] + plans = db.query(MembershipPlan).order_by(MembershipPlan.level).all() + for plan in plans: + count = db.query(User).filter(User.membership_plan_id == plan.id).count() + plan_stats.append({ + "plan_id": plan.id, + "plan_name": plan.name, + "level": plan.level, + "user_count": count, + "is_active": plan.is_active, + }) + + # 今日新增订阅 + today_start = datetime(now.year, now.month, now.day) + today_new_subs = db.query(MembershipSubscription).filter( + MembershipSubscription.created_at >= today_start + ).count() + + return { + "total_users": total_users, + "active_members": active_members, + "expired_members": expired_members, + "free_users": level_stats.get("level_0", 0), + "auto_renew_count": auto_renew_count, + "level_stats": level_stats, + "plan_stats": plan_stats, + "today_new_subs": today_new_subs, } @@ -224,7 +758,6 @@ async def get_automation_rules( rules = db.query(RiskRule).order_by(RiskRule.created_at.desc()).all() if not rules: - # 返回默认规则 return { "rules": [], "defaults": { diff --git a/backend/python/app/models.py b/backend/python/app/models.py index 5977785..1398984 100644 --- a/backend/python/app/models.py +++ b/backend/python/app/models.py @@ -347,6 +347,23 @@ class MembershipPlan(Base): features = Column(JSON, default=None) free_app_ids = Column(JSON, default=None) free_model_ids = Column(JSON, default=None) + # 配额字段 + max_agents = Column(Integer, default=5) + max_workflows = Column(Integer, default=3) + max_knowledge_bases = Column(Integer, default=2) + max_storage_mb = Column(Integer, default=1024) + api_calls_per_day = Column(Integer, default=100) + api_calls_per_min = Column(Integer, default=10) + # 权限字段 + can_publish_apps = Column(Boolean, default=False) + can_create_api_keys = Column(Boolean, default=False) + can_use_workflow = Column(Boolean, default=False) + can_use_mcp = Column(Boolean, default=False) + can_use_code_exec = Column(Boolean, default=False) + can_use_privacy_mode = Column(Boolean, default=False) + can_use_dag = Column(Boolean, default=False) + allowed_model_tiers = Column(JSON, default=None) + permissions = Column(JSON, default=None) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 6aafa52..f96e825 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -360,13 +360,28 @@ export const adminAPI = { updateCustomRequest: (id: string, data: any) => put(`/admin/services/custom-requests/${id}`, data), getAvailableAgents: () => get('/admin/services/agents'), assignAgentToService: (id: string, agentId: string) => put(`/admin/services/${id}/agent?agent_id=${agentId}`), - // 会员管理 + // 会员管理(增强版) getPlans: () => get('/admin/membership/plans'), - addPlan: (data: any) => post('/admin/membership/plans', data), - updatePlan: (id: string, data: any) => put(`/admin/membership/plans/${id}`, data), + addPlan: (params: string) => post(`/admin/membership/plans${params}`), + updatePlan: (id: string, params: string) => put(`/admin/membership/plans/${id}${params}`), deletePlan: (id: string) => del(`/admin/membership/plans/${id}`), - getSubscriptions: () => get('/admin/membership/subscriptions'), + getSubscriptions: (page?: number) => get(`/admin/membership/subscriptions${page ? `?page=${page}` : ''}`), getAutomationRules: () => get('/admin/membership/automation-rules'), + getBenefitsComparison: () => get('/admin/membership/benefits-comparison'), + getMembershipConfigs: () => get('/admin/membership/global-configs'), + updateMembershipConfig: (key: string, params: string) => put(`/admin/membership/global-configs/${key}${params}`), + getUserMemberships: (page?: number, planId?: string, status?: string) => { + const p = new URLSearchParams(); + if (page) p.append('page', String(page)); + if (planId) p.append('plan_id', planId); + if (status) p.append('status', status); + const qs = p.toString(); + return get(`/admin/membership/user-memberships${qs ? `?${qs}` : ''}`); + }, + assignPlan: (userId: string, planId: string, days: number) => put(`/admin/membership/users/${userId}/assign-plan?plan_id=${planId}&duration_days=${days}`), + revokeMembership: (userId: string) => put(`/admin/membership/users/${userId}/revoke`), + getUserUsage: (userId: string) => get(`/admin/membership/users/${userId}/usage`), + getMembershipStats: () => get('/admin/membership/stats'), // 广告管理 getAds: () => get('/admin/ads/'), addAd: (data: any) => post('/admin/ads/', data), diff --git a/frontend/src/pages/admin/Membership.tsx b/frontend/src/pages/admin/Membership.tsx index 9189096..9966e9f 100644 --- a/frontend/src/pages/admin/Membership.tsx +++ b/frontend/src/pages/admin/Membership.tsx @@ -1,6 +1,5 @@ -import React, { useState } from 'react'; -import { mockMembershipPlans, mockApps, mockModels } from '@/mock/data'; -import type { MembershipPlan } from '@/types'; +import React, { useState, useEffect, useCallback } from 'react'; +import { adminAPI } from '@/api'; const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => ( @@ -8,50 +7,168 @@ const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ child ); -const levelColors = [ - { bg: 'var(--color-bg-tertiary)', accent: 'var(--color-text-secondary)', label: '免费版' }, - { bg: 'var(--color-accent-light)', accent: 'var(--color-accent)', label: '专业版' }, - { bg: '#FFF7E6', accent: '#E8922C', label: '企业版' }, -]; +const levelColors: Record = { + 0: { bg: 'var(--color-bg-tertiary)', accent: 'var(--color-text-secondary)', label: '免费版' }, + 1: { bg: 'rgba(99,102,241,0.1)', accent: '#818cf8', label: '基础版' }, + 2: { bg: 'rgba(34,197,94,0.1)', accent: '#22c55e', label: '专业版' }, + 3: { bg: 'rgba(245,158,11,0.1)', accent: '#f59e0b', label: '企业版' }, +}; -const typeLabels: Record = { skill: 'Skill', agent: 'Agent', mcp: 'MCP' }; -const typeBadgeColors: Record = { skill: 'badge-success', agent: 'badge-info', mcp: 'badge-warning' }; +const Toggle: React.FC<{ checked: boolean; onChange: (v: boolean) => void }> = ({ checked, onChange }) => ( + +); -// 所有收费应用 (price > 0, published) -const allPaidApps = mockApps.filter(a => a.price > 0 && a.status === 'published'); -// 所有收费模型 -const allPaidModels = mockModels.filter(m => m.status === 'paid'); +interface Plan { + id: string; name: string; level: number; duration_type: string; price: number; + daily_free_tokens: number; features: string[]; free_app_ids: string[]; free_model_ids: string[]; + is_active: boolean; + max_agents: number; max_workflows: number; max_knowledge_bases: number; max_storage_mb: number; + api_calls_per_day: number; api_calls_per_min: number; + can_publish_apps: boolean; can_create_api_keys: boolean; can_use_workflow: boolean; + can_use_mcp: boolean; can_use_code_exec: boolean; can_use_privacy_mode: boolean; can_use_dag: boolean; + allowed_model_tiers: string[]; permissions: Record; +} + +interface UserMembership { + user_id: string; username: string; email: string; + membership_plan_id: string; membership_plan_name: string; membership_level: number; + membership_expire_at: string; auto_renew: boolean; member_status: string; +} + +interface GlobalConfig { + id: string; key: string; value: any; type: string; description: string; +} const Membership: React.FC = () => { - const [plans, setPlans] = useState(mockMembershipPlans); - const [editingPlan, setEditingPlan] = useState(null); + const [activeTab, setActiveTab] = useState<'plans' | 'benefits' | 'users' | 'configs' | 'stats'>('plans'); + const [plans, setPlans] = useState([]); + const [loading, setLoading] = useState(false); + const [editingPlan, setEditingPlan] = useState(null); const [showModal, setShowModal] = useState(false); - const [autoDowngrade, setAutoDowngrade] = useState(true); - const [autoRenew, setAutoRenew] = useState(true); + const [showAssignModal, setShowAssignModal] = useState(false); + const [assignUserId, setAssignUserId] = useState(''); + const [assignPlanId, setAssignPlanId] = useState(''); + const [assignDays, setAssignDays] = useState(30); - const [form, setForm] = useState({ - name: '', level: 0, durationType: 'month' as 'month' | 'quarter' | 'year', - price: 0, dailyFreeTokens: 0, features: [] as string[], - freeAppIds: [] as string[], freeModelIds: [] as string[], - }); + // User memberships + const [userMemberships, setUserMemberships] = useState([]); + const [userTotal, setUserTotal] = useState(0); + const [userPage, setUserPage] = useState(1); + + // Configs + const [configs, setConfigs] = useState([]); + + // Stats + const [stats, setStats] = useState(null); + + const fetchPlans = useCallback(async () => { + setLoading(true); + try { + const res = await adminAPI.getPlans(); + setPlans(Array.isArray(res.data) ? res.data : (Array.isArray(res) ? res : [])); + } catch (e) { console.error('fetchPlans:', e); } + setLoading(false); + }, []); + + const fetchUserMemberships = useCallback(async () => { + try { + const res = await adminAPI.getUserMemberships(userPage); + const d = res.data || res; + setUserMemberships(d?.items || []); + setUserTotal(d?.total || 0); + } catch (e) { console.error('fetchUserMemberships:', e); } + }, [userPage]); + + const fetchConfigs = useCallback(async () => { + try { + const res = await adminAPI.getMembershipConfigs(); + setConfigs(Array.isArray(res.data) ? res.data : (Array.isArray(res) ? res : [])); + } catch (e) { console.error('fetchConfigs:', e); } + }, []); + + const fetchStats = useCallback(async () => { + try { + const res = await adminAPI.getMembershipStats(); + setStats(res.data || res); + } catch (e) { console.error('fetchStats:', e); } + }, []); + + useEffect(() => { + fetchPlans(); + }, [fetchPlans]); + + useEffect(() => { + if (activeTab === 'users') fetchUserMemberships(); + if (activeTab === 'configs') fetchConfigs(); + if (activeTab === 'stats') fetchStats(); + }, [activeTab, fetchUserMemberships, fetchConfigs, fetchStats]); + + // ===================== Plan Edit Form ===================== + const emptyForm: Plan = { + id: '', name: '', level: 0, duration_type: 'month', price: 0, daily_free_tokens: 0, + features: [], free_app_ids: [], free_model_ids: [], is_active: true, + max_agents: 5, max_workflows: 3, max_knowledge_bases: 2, max_storage_mb: 1024, + api_calls_per_day: 100, api_calls_per_min: 10, + can_publish_apps: false, can_create_api_keys: false, can_use_workflow: false, + can_use_mcp: false, can_use_code_exec: false, can_use_privacy_mode: false, can_use_dag: false, + allowed_model_tiers: ['free'], permissions: {}, + }; + const [form, setForm] = useState(emptyForm); const [newFeature, setNewFeature] = useState(''); - const handleEdit = (plan: MembershipPlan) => { + const handleEdit = (plan: Plan) => { setEditingPlan(plan); - setForm({ - name: plan.name, level: plan.level, durationType: plan.durationType, - price: plan.price, dailyFreeTokens: plan.dailyFreeTokens, features: [...plan.features], - freeAppIds: [...(plan.freeAppIds || [])], freeModelIds: [...(plan.freeModelIds || [])], - }); + setForm({ ...plan }); setShowModal(true); }; - const handleSave = () => { - if (editingPlan) { - setPlans(prev => prev.map(p => p.id === editingPlan.id ? { ...p, ...form } : p)); - } - setShowModal(false); + const handleCreate = () => { setEditingPlan(null); + setForm({ ...emptyForm }); + setShowModal(true); + }; + + const handleSave = async () => { + try { + const params = new URLSearchParams(); + Object.keys(form).forEach(k => { + if (k === 'id' || k === 'created_at' || k === 'updated_at') return; + const val = (form as any)[k]; + if (val === null || val === undefined) return; + if (typeof val === 'boolean') params.append(k, String(val)); + else if (Array.isArray(val)) params.append(k, JSON.stringify(val)); + else if (typeof val === 'object') params.append(k, JSON.stringify(val)); + else params.append(k, String(val)); + }); + if (editingPlan) { + await adminAPI.updatePlan(editingPlan.id, `?${params.toString()}`); + } else { + await adminAPI.addPlan(`?${params.toString()}`); + } + setShowModal(false); + fetchPlans(); + } catch (e) { console.error('handleSave:', e); alert('保存失败: ' + (e as any)?.message); } + }; + + const handleDelete = async (id: string) => { + if (!confirm('确定删除此会员计划?')) return; + try { + await adminAPI.deletePlan(id); + fetchPlans(); + } catch (e) { alert('删除失败: ' + (e as any)?.message); } }; const addFeature = () => { @@ -61,417 +178,497 @@ const Membership: React.FC = () => { } }; - const removeFeature = (idx: number) => { - setForm({ ...form, features: form.features.filter((_, i) => i !== idx) }); - }; - - const toggleAppId = (appId: string) => { + const toggleModelTier = (tier: string) => { setForm(prev => ({ ...prev, - freeAppIds: prev.freeAppIds.includes(appId) - ? prev.freeAppIds.filter(id => id !== appId) - : [...prev.freeAppIds, appId], + allowed_model_tiers: prev.allowed_model_tiers.includes(tier) + ? prev.allowed_model_tiers.filter(t => t !== tier) + : [...prev.allowed_model_tiers, tier], })); }; - const toggleModelId = (modelId: string) => { - setForm(prev => ({ - ...prev, - freeModelIds: prev.freeModelIds.includes(modelId) - ? prev.freeModelIds.filter(id => id !== modelId) - : [...prev.freeModelIds, modelId], - })); + // ===================== Assign Plan ===================== + const handleAssignPlan = async () => { + if (!assignUserId || !assignPlanId) { alert('请填写用户ID和选择计划'); return; } + try { + await adminAPI.assignPlan(assignUserId, assignPlanId, assignDays); + setShowAssignModal(false); + setAssignUserId(''); setAssignPlanId(''); setAssignDays(30); + fetchUserMemberships(); + } catch (e) { alert('分配失败: ' + (e as any)?.message); } }; - // 根据 freeAppIds 获取应用名称列表(用于卡片展示) - const getFreeAppNames = (plan: MembershipPlan): string[] => { - return (plan.freeAppIds || []).map(id => allPaidApps.find(a => a.id === id)?.name).filter(Boolean) as string[]; + const handleRevoke = async (userId: string, username: string) => { + if (!confirm(`确定撤销 ${username} 的会员?`)) return; + try { + await adminAPI.revokeMembership(userId); + fetchUserMemberships(); + } catch (e) { alert('撤销失败: ' + (e as any)?.message); } }; - const getFreeModelNames = (plan: MembershipPlan): string[] => { - return (plan.freeModelIds || []).map(id => allPaidModels.find(m => m.id === id)?.name).filter(Boolean) as string[]; + const handleUpdateConfig = async (key: string, value: any) => { + try { + const params = new URLSearchParams(); + params.append('value', String(value)); + await adminAPI.updateMembershipConfig(key, `?${params.toString()}`); + fetchConfigs(); + } catch (e) { alert('更新失败: ' + (e as any)?.message); } + }; + + // ===================== Render ===================== + const tabs = [ + { key: 'plans', label: '会员计划', icon: <> }, + { key: 'benefits', label: '权益对比', icon: <> }, + { key: 'users', label: '用户会员', icon: <> }, + { key: 'configs', label: '全局配置', icon: <> }, + { key: 'stats', label: '统计概览', icon: <> }, + ]; + + const inputStyle: React.CSSProperties = { + background: 'var(--color-bg-secondary)', border: '1px solid var(--color-border)', + borderRadius: 'var(--radius-md)', padding: '8px 12px', color: 'var(--color-text)', + fontSize: 'var(--text-sm)', width: '100%', outline: 'none', + }; + const labelStyle: React.CSSProperties = { + fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)', fontWeight: 600, marginBottom: 4, display: 'block', + }; + const sectionStyle: React.CSSProperties = { + padding: 'var(--space-lg)', borderBottom: '1px solid var(--color-border)', }; return (
-

会员订阅体系管理

+
+

会员订阅体系管理

+ {activeTab === 'plans' && ( + + )} +
- {/* Plan Cards */} -
- {plans.map((plan, idx) => { - const colors = levelColors[idx] || levelColors[0]; - const freeAppNames = getFreeAppNames(plan); - const freeModelNames = getFreeModelNames(plan); - return ( -
-
-
- - {colors.label} - -

{plan.name}

-
- -
+ {/* Tabs */} +
+ {tabs.map(tab => ( + + ))} +
- {/* Pricing */} -
-
-
月付
-
{plan.price} 元
+ {/* ===================== Plans Tab ===================== */} + {activeTab === 'plans' && ( +
+ {plans.map((plan) => { + const colors = levelColors[plan.level] || levelColors[0]; + return ( +
+
+
+ + {colors.label} · Level {plan.level} + +

{plan.name}

+
+
+ + +
-
-
季付
-
{Math.round(plan.price * 3 * 0.85)} 元
-
-
-
年付
-
{Math.round(plan.price * 12 * 0.7)} 元
-
-
- {/* Features */} -
-
会员权益
-
    - {plan.features.map((f, i) => ( -
  • - - {f} -
  • + {/* Price */} +
    +
    +
    价格
    +
    ¥{plan.price}
    +
    +
    +
    每日Token
    +
    {plan.daily_free_tokens?.toLocaleString()}
    +
    +
    + + {/* Quotas */} +
    + {[ + { label: 'Agent', value: plan.max_agents }, + { label: '工作流', value: plan.max_workflows }, + { label: '知识库', value: plan.max_knowledge_bases }, + { label: '存储(MB)', value: plan.max_storage_mb }, + { label: 'API/日', value: plan.api_calls_per_day }, + { label: 'API/分', value: plan.api_calls_per_min }, + ].map((item, i) => ( +
    +
    {item.label}
    +
    {item.value >= 999 ? '∞' : item.value?.toLocaleString()}
    +
    ))} -
-
- - {/* Free tokens */} -
- 每日免费额度: {plan.dailyFreeTokens.toLocaleString()} Token -
- - {/* Free apps & models summary */} - {plan.level > 0 && ( -
- {freeAppNames.length > 0 && ( -
- - 免费应用({freeAppNames.length}): {freeAppNames.join(', ')} -
- )} - {freeModelNames.length > 0 && ( -
- - 免费模型({freeModelNames.length}): {freeModelNames.join(', ')} -
- )}
- )} -
- ); - })} -
- {/* Automation Rules */} -
-
-

自动化规则配置

-
-
-
-
-
到期自动降级
-
会员到期后自动降级为免费版
-
- -
+ {/* Permissions */} +
+ {[ + { key: 'can_publish_apps', label: '发布应用' }, + { key: 'can_create_api_keys', label: 'API密钥' }, + { key: 'can_use_workflow', label: '工作流' }, + { key: 'can_use_mcp', label: 'MCP' }, + { key: 'can_use_code_exec', label: '代码执行' }, + { key: 'can_use_privacy_mode', label: '隐私模式' }, + { key: 'can_use_dag', label: 'DAG' }, + ].map(perm => { + const enabled = (plan as any)[perm.key]; + return enabled ? ( + + ✓ {perm.label} + + ) : ( + + ✗ {perm.label} + + ); + })} +
-
-
-
自动续费默认开启
-
新会员订阅默认开启自动续费
-
- -
+ {/* Features */} + {plan.features && plan.features.length > 0 && ( +
+
会员权益
+
    + {plan.features.map((f, i) => ( +
  • + + {f} +
  • + ))} +
+
+ )} +
+ ); + })}
-
+ )} - {/* Global Free Application List */} -
-
-

当前各等级会员免费应用总览

-
-
+ {/* ===================== Benefits Comparison Tab ===================== */} + {activeTab === 'benefits' && ( +
- - - - - + + {plans.map(p => { + const c = levelColors[p.level] || levelColors[0]; + return ; + })} - {allPaidApps.map(app => { - const freePlans = plans.filter(p => (p.freeAppIds || []).includes(app.id)); - return ( - - + {[ + { key: 'price', label: '月价格(元)', type: 'number' }, + { key: 'daily_free_tokens', label: '每日免费Token', type: 'number' }, + { key: 'max_agents', label: '最大Agent数', type: 'number' }, + { key: 'max_workflows', label: '最大工作流数', type: 'number' }, + { key: 'max_knowledge_bases', label: '最大知识库数', type: 'number' }, + { key: 'max_storage_mb', label: '存储配额(MB)', type: 'number' }, + { key: 'api_calls_per_day', label: '每日API限额', type: 'number' }, + { key: 'api_calls_per_min', label: '每分钟API限额', type: 'number' }, + { key: 'can_publish_apps', label: '发布应用到市场', type: 'boolean' }, + { key: 'can_create_api_keys', label: '创建开发者API密钥', type: 'boolean' }, + { key: 'can_use_workflow', label: '工作流编排', type: 'boolean' }, + { key: 'can_use_mcp', label: 'MCP工具', type: 'boolean' }, + { key: 'can_use_code_exec', label: '代码执行', type: 'boolean' }, + { key: 'can_use_privacy_mode', label: '隐私模式', type: 'boolean' }, + { key: 'can_use_dag', label: 'DAG多Agent', type: 'boolean' }, + { key: 'allowed_model_tiers', label: '可用模型层级', type: 'list' }, + ].map(row => ( + + + {plans.map(p => { + const val = (p as any)[row.key]; + let display; + if (row.type === 'boolean') { + display = val ? ✓ : ✗; + } else if (row.type === 'number') { + display = val >= 999 ? '不限' : (val?.toLocaleString() || '0'); + } else if (row.type === 'list') { + display = Array.isArray(val) ? val.join(', ') : '-'; + } else { + display = String(val ?? '-'); + } + return ; + })} + + ))} + +
应用名称类型来源价格免费等级权益项 + {p.name} +
{app.name}
{row.label}{display}
+
+ )} + + {/* ===================== Users Tab ===================== */} + {activeTab === 'users' && ( +
+
+ +
+
+ + + + + + + + + {userMemberships.length === 0 ? ( + + ) : userMemberships.map(u => ( + + + + + + + - - - ); - })} - -
用户名邮箱会员计划等级到期时间自动续费状态操作
暂无数据
{u.username}{u.email || '-'}{u.membership_plan_name}Level {u.membership_level}{u.membership_expire_at ? new Date(u.membership_expire_at).toLocaleString('zh-CN') : '-'}{u.auto_renew ? '✓' : '✗'} - - {typeLabels[app.type] || app.type.toUpperCase()} + + {u.member_status === 'active' ? '活跃' : u.member_status === 'expired' ? '已过期' : '免费'} {app.source === 'official' ? '官方' : '用户'}{app.price} 元 - {freePlans.length > 0 - ? freePlans.map(p => ( - - {p.name} - - )) - : 无 - } + {u.membership_level > 0 && ( + + )}
+ ))} + + +
+ {userTotal > 20 && ( +
+ + 第 {userPage} 页 + +
+ )}
-
+ )} - {/* Global Free Model List */} -
-
-

当前各等级会员免费大模型总览

+ {/* ===================== Configs Tab ===================== */} + {activeTab === 'configs' && ( +
+

全局会员配置

+
+ {configs.map(cfg => ( +
+
+
{cfg.description}
+
{cfg.key}
+
+ {cfg.type === 'boolean' ? ( + handleUpdateConfig(cfg.key, v)} /> + ) : cfg.type === 'number' ? ( + handleUpdateConfig(cfg.key, e.target.value)} style={{ ...inputStyle, width: 80 }} /> + ) : ( + handleUpdateConfig(cfg.key, e.target.value)} style={{ ...inputStyle, width: 200 }} /> + )} +
+ ))} +
-
- - - - - - - - - - - {allPaidModels.map(model => { - const freePlans = plans.filter(p => (p.freeModelIds || []).includes(model.id)); - return ( - - - - - + )} + + {/* ===================== Stats Tab ===================== */} + {activeTab === 'stats' && stats && ( +
+
+ {[ + { label: '总用户数', value: stats.total_users, color: '#818cf8' }, + { label: '活跃会员', value: stats.active_members, color: '#22c55e' }, + { label: '已过期', value: stats.expired_members, color: '#ef4444' }, + { label: '免费用户', value: stats.free_users, color: 'var(--color-text-secondary)' }, + { label: '自动续费', value: stats.auto_renew_count, color: '#f59e0b' }, + { label: '今日新增', value: stats.today_new_subs, color: '#06b6d4' }, + ].map((s, i) => ( +
+
{s.value?.toLocaleString()}
+
{s.label}
+
+ ))} +
+
+

各计划订阅统计

+
模型名称提供商每百万Token价格免费等级
{model.name}{model.providerName}{model.pricePerMillionTokens} 元 - {freePlans.length > 0 - ? freePlans.map(p => ( - - {p.name} - - )) - : 无 - } -
+ + + {stats.plan_stats?.map((ps: any) => ( + + + + + - ); - })} - -
计划名称等级用户数状态
{ps.plan_name}Level {ps.level}{ps.user_count}{ps.is_active ? 启用 : 停用}
+ ))} + + +
-
+ )} - {/* Edit Modal */} + {/* ===================== Edit/Create Plan Modal ===================== */} {showModal && (
{ setShowModal(false); setEditingPlan(null); }}> + paddingTop: '4vh', animation: 'fadeIn 0.2s', + }} onClick={() => setShowModal(false)}>
e.stopPropagation()}> -

- 编辑 {editingPlan?.name} -

+
+

+ {editingPlan ? `编辑 ${editingPlan.name}` : '新建会员计划'} +

+
-
-
- - setForm({ ...form, price: Number(e.target.value) })} /> -
-
- - setForm({ ...form, dailyFreeTokens: Number(e.target.value) })} /> + {/* Basic Info */} +
+
基础信息
+
+
setForm({ ...form, name: e.target.value })} />
+
setForm({ ...form, level: Number(e.target.value) })} />
+
+
setForm({ ...form, price: Number(e.target.value) })} />
+
setForm({ ...form, daily_free_tokens: Number(e.target.value) })} />
+
+ +
-
- -
+ {/* Quotas */} +
+
配额限制
+
+
setForm({ ...form, max_agents: Number(e.target.value) })} />
+
setForm({ ...form, max_workflows: Number(e.target.value) })} />
+
setForm({ ...form, max_knowledge_bases: Number(e.target.value) })} />
+
setForm({ ...form, max_storage_mb: Number(e.target.value) })} />
+
setForm({ ...form, api_calls_per_day: Number(e.target.value) })} />
+
setForm({ ...form, api_calls_per_min: Number(e.target.value) })} />
+
+
+ + {/* Permissions */} +
+
功能权限
+
+ {[ + { key: 'can_publish_apps', label: '发布应用到市场' }, + { key: 'can_create_api_keys', label: '创建开发者API密钥' }, + { key: 'can_use_workflow', label: '使用工作流编排' }, + { key: 'can_use_mcp', label: '使用MCP工具' }, + { key: 'can_use_code_exec', label: '使用代码执行' }, + { key: 'can_use_privacy_mode', label: '使用隐私模式' }, + { key: 'can_use_dag', label: '使用DAG多Agent' }, + ].map(perm => ( + + ))} +
+
+ +
+ {['free', 'standard', 'premium'].map(tier => ( + + ))} +
+
+
+ + {/* Features */} +
+
会员权益标签
+
{form.features.map((f, i) => ( - + {f} - + ))}
- setNewFeature(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addFeature(); } }} - placeholder="输入权益名称,回车添加" - style={{ flex: 1 }} - /> + setNewFeature(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addFeature(); } }} placeholder="输入权益名称,回车添加" />
- {/* Free Applications Selection */} -
- -
- {allPaidApps.length === 0 && ( -
暂无收费应用
- )} - {allPaidApps.map(app => { - const checked = form.freeAppIds.includes(app.id); - return ( - - ); - })} -
-
- 已选 {form.freeAppIds.length}/{allPaidApps.length} 个应用 -
-
- - {/* Free Models Selection */} -
- -
- {allPaidModels.length === 0 && ( -
暂无收费模型
- )} - {allPaidModels.map(model => { - const checked = form.freeModelIds.includes(model.id); - return ( - - ); - })} -
-
- 已选 {form.freeModelIds.length}/{allPaidModels.length} 个模型 -
-
- -
+ {/* Actions */} +
+ - +
+
+
+ )} + + {/* ===================== Assign Plan Modal ===================== */} + {showAssignModal && ( +
setShowAssignModal(false)}> +
e.stopPropagation()}> +

分配会员计划

+
+
setAssignUserId(e.target.value)} placeholder="输入用户ID" />
+
+ +
+
setAssignDays(Number(e.target.value))} />
+
+
+ +