792 lines
27 KiB
Python
792 lines
27 KiB
Python
"""
|
|
会员订阅体系管理 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,
|
|
},
|
|
}
|