""" 会员订阅体系管理 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 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 # ============================================================ @router.get("/plans") 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 [_format_plan(p) for p in plans] @router.post("/plans") async def create_plan( name: str, level: int, duration_type: str, price: float, daily_free_tokens: int = 0, 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, level=level, duration_type=duration_type, price=price, daily_free_tokens=daily_free_tokens, features=features, free_app_ids=free_app_ids, 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() LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="create_membership_plan", target_type="membership_plan", target_id=plan.id, detail=f"创建会员计划 {name} (level={level})", ) return {"message": "会员计划已创建", "id": plan.id} @router.put("/plans/{id}") async def update_plan( id: str, name: str = None, level: int = None, duration_type: str = None, price: float = None, daily_free_tokens: int = None, 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: plan.level = level if duration_type is not None: plan.duration_type = duration_type if price is not None: plan.price = price if daily_free_tokens is not None: plan.daily_free_tokens = daily_free_tokens if features is not None: plan.features = features if free_app_ids is not None: plan.free_app_ids = free_app_ids if free_model_ids is not None: 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() LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="update_membership_plan", target_type="membership_plan", target_id=id, detail=f"更新会员计划 {plan.name} (含权益配置)", ) return {"message": "会员计划已更新"} @router.delete("/plans/{id}") async def delete_plan( id: str, 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="会员计划不存在") # 检查是否有用户正在使用此计划 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() LogService.log_audit( admin_id=admin_user.id, admin_name=admin_user.username, action="delete_membership_plan", target_type="membership_plan", target_id=id, detail=f"删除会员计划 {plan.name}", ) 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, }, } # ============================================================ # 用户订阅管理 # ============================================================ @router.get("/subscriptions") async def get_subscriptions( user_id: str = Query(None), status: str = Query(None, description="订阅状态: active/expired/downgraded"), 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(MembershipSubscription) if user_id: query = query.filter(MembershipSubscription.user_id == user_id) if status: query = query.filter(MembershipSubscription.status == status) total = query.count() subscriptions = query.order_by( 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": 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, } # ============================================================ # 自动化规则(风控) # ============================================================ @router.get("/automation-rules") async def get_automation_rules( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取自动化规则配置""" rules = db.query(RiskRule).order_by(RiskRule.created_at.desc()).all() if not rules: return { "rules": [], "defaults": { "max_login_attempts": 5, "token_usage_threshold_per_hour": 1000000, "token_usage_threshold_per_day": 10000000, "concurrent_sessions_limit": 5, }, } return { "rules": [ { "id": r.id, "name": r.name, "type": r.type, "threshold": r.threshold, "duration": r.duration, "is_enabled": r.is_enabled, "created_at": r.created_at.isoformat() if r.created_at else None, "updated_at": r.updated_at.isoformat() if r.updated_at else None, } for r in rules ], "defaults": { "max_login_attempts": 5, "token_usage_threshold_per_hour": 1000000, "token_usage_threshold_per_day": 10000000, "concurrent_sessions_limit": 5, }, }