feat: membership benefits refinement - permissions and quotas linked to plans
This commit is contained in:
1 parent
0886c26157
commit
839be8f97f
4 files changed
+1158
-396
No files matched your search
@@ -1,18 +1,55 @@
|
|||||||
"""
|
"""
|
||||||
会员订阅体系管理 API(管理员)
|
会员订阅体系管理 API(管理员)— 增强版:权益细化与权限挂钩
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import text
|
||||||
from ...database import get_db, SessionLocal
|
from ...database import get_db, SessionLocal
|
||||||
from ...models import MembershipPlan, MembershipSubscription, User, RiskRule
|
from ...models import MembershipPlan, MembershipSubscription, User, RiskRule
|
||||||
from ...auth import get_current_admin
|
from ...auth import get_current_admin
|
||||||
from ...middleware.log_service import LogService
|
from ...middleware.log_service import LogService
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
import json
|
||||||
|
from datetime import datetime, date
|
||||||
|
|
||||||
router = APIRouter()
|
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
|
# 会员计划 CRUD
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -22,25 +59,9 @@ async def get_plans(
|
|||||||
admin_user=Depends(get_current_admin),
|
admin_user=Depends(get_current_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""获取会员计划列表"""
|
"""获取会员计划列表(含权益配置)"""
|
||||||
plans = db.query(MembershipPlan).order_by(MembershipPlan.level).all()
|
plans = db.query(MembershipPlan).order_by(MembershipPlan.level).all()
|
||||||
return [
|
return [_format_plan(p) for p in plans]
|
||||||
{
|
|
||||||
"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
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/plans")
|
@router.post("/plans")
|
||||||
@@ -53,11 +74,28 @@ async def create_plan(
|
|||||||
features: list = None,
|
features: list = None,
|
||||||
free_app_ids: list = None,
|
free_app_ids: list = None,
|
||||||
free_model_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,
|
is_active: bool = True,
|
||||||
admin_user=Depends(get_current_admin),
|
admin_user=Depends(get_current_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""创建会员计划"""
|
"""创建会员计划(含权益配置)"""
|
||||||
plan = MembershipPlan(
|
plan = MembershipPlan(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
name=name,
|
name=name,
|
||||||
@@ -70,6 +108,24 @@ async def create_plan(
|
|||||||
free_model_ids=free_model_ids,
|
free_model_ids=free_model_ids,
|
||||||
is_active=is_active,
|
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.add(plan)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@@ -79,7 +135,7 @@ async def create_plan(
|
|||||||
action="create_membership_plan",
|
action="create_membership_plan",
|
||||||
target_type="membership_plan",
|
target_type="membership_plan",
|
||||||
target_id=plan.id,
|
target_id=plan.id,
|
||||||
detail=f"创建会员计划 {name}",
|
detail=f"创建会员计划 {name} (level={level})",
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"message": "会员计划已创建", "id": plan.id}
|
return {"message": "会员计划已创建", "id": plan.id}
|
||||||
@@ -96,15 +152,33 @@ async def update_plan(
|
|||||||
features: list = None,
|
features: list = None,
|
||||||
free_app_ids: list = None,
|
free_app_ids: list = None,
|
||||||
free_model_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,
|
is_active: bool = None,
|
||||||
admin_user=Depends(get_current_admin),
|
admin_user=Depends(get_current_admin),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""更新会员计划"""
|
"""更新会员计划(含权益配置)"""
|
||||||
plan = db.query(MembershipPlan).filter(MembershipPlan.id == id).first()
|
plan = db.query(MembershipPlan).filter(MembershipPlan.id == id).first()
|
||||||
if not plan:
|
if not plan:
|
||||||
raise HTTPException(status_code=404, detail="会员计划不存在")
|
raise HTTPException(status_code=404, detail="会员计划不存在")
|
||||||
|
|
||||||
|
# 基础字段
|
||||||
if name is not None:
|
if name is not None:
|
||||||
plan.name = name
|
plan.name = name
|
||||||
if level is not None:
|
if level is not None:
|
||||||
@@ -123,6 +197,41 @@ async def update_plan(
|
|||||||
plan.free_model_ids = free_model_ids
|
plan.free_model_ids = free_model_ids
|
||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
plan.is_active = is_active
|
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()
|
plan.updated_at = datetime.now()
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@@ -132,7 +241,7 @@ async def update_plan(
|
|||||||
action="update_membership_plan",
|
action="update_membership_plan",
|
||||||
target_type="membership_plan",
|
target_type="membership_plan",
|
||||||
target_id=id,
|
target_id=id,
|
||||||
detail=f"更新会员计划 {plan.name}",
|
detail=f"更新会员计划 {plan.name} (含权益配置)",
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"message": "会员计划已更新"}
|
return {"message": "会员计划已更新"}
|
||||||
@@ -149,6 +258,11 @@ async def delete_plan(
|
|||||||
if not plan:
|
if not plan:
|
||||||
raise HTTPException(status_code=404, detail="会员计划不存在")
|
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.delete(plan)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@@ -164,6 +278,352 @@ async def delete_plan(
|
|||||||
return {"message": "会员计划已删除"}
|
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()
|
MembershipSubscription.created_at.desc()
|
||||||
).offset((page - 1) * page_size).limit(page_size).all()
|
).offset((page - 1) * page_size).limit(page_size).all()
|
||||||
|
|
||||||
return {
|
# 关联用户名和计划名
|
||||||
"total": total,
|
items = []
|
||||||
"page": page,
|
for s in subscriptions:
|
||||||
"page_size": page_size,
|
u = db.query(User).filter(User.id == s.user_id).first()
|
||||||
"items": [
|
p = db.query(MembershipPlan).filter(MembershipPlan.id == s.plan_id).first()
|
||||||
{
|
items.append({
|
||||||
"id": s.id,
|
"id": s.id,
|
||||||
"user_id": s.user_id,
|
"user_id": s.user_id,
|
||||||
|
"username": u.username if u else "",
|
||||||
"plan_id": s.plan_id,
|
"plan_id": s.plan_id,
|
||||||
|
"plan_name": p.name if p else "",
|
||||||
"order_id": s.order_id,
|
"order_id": s.order_id,
|
||||||
"start_at": s.start_at.isoformat() if s.start_at else None,
|
"start_at": s.start_at.isoformat() if s.start_at else None,
|
||||||
"end_at": s.end_at.isoformat() if s.end_at else None,
|
"end_at": s.end_at.isoformat() if s.end_at else None,
|
||||||
"auto_renew": s.auto_renew,
|
"auto_renew": s.auto_renew,
|
||||||
"status": s.status,
|
"status": s.status,
|
||||||
"created_at": s.created_at.isoformat() if s.created_at else None,
|
"created_at": s.created_at.isoformat() if s.created_at else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"items": items,
|
||||||
}
|
}
|
||||||
for s in subscriptions
|
|
||||||
],
|
|
||||||
|
# ============================================================
|
||||||
|
# 会员统计概览
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@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()
|
rules = db.query(RiskRule).order_by(RiskRule.created_at.desc()).all()
|
||||||
|
|
||||||
if not rules:
|
if not rules:
|
||||||
# 返回默认规则
|
|
||||||
return {
|
return {
|
||||||
"rules": [],
|
"rules": [],
|
||||||
"defaults": {
|
"defaults": {
|
||||||
|
|||||||
@@ -347,6 +347,23 @@ class MembershipPlan(Base):
|
|||||||
features = Column(JSON, default=None)
|
features = Column(JSON, default=None)
|
||||||
free_app_ids = Column(JSON, default=None)
|
free_app_ids = Column(JSON, default=None)
|
||||||
free_model_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)
|
is_active = Column(Boolean, default=True)
|
||||||
created_at = Column(DateTime, default=datetime.now)
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|||||||
@@ -360,13 +360,28 @@ export const adminAPI = {
|
|||||||
updateCustomRequest: (id: string, data: any) => put<any>(`/admin/services/custom-requests/${id}`, data),
|
updateCustomRequest: (id: string, data: any) => put<any>(`/admin/services/custom-requests/${id}`, data),
|
||||||
getAvailableAgents: () => get<any[]>('/admin/services/agents'),
|
getAvailableAgents: () => get<any[]>('/admin/services/agents'),
|
||||||
assignAgentToService: (id: string, agentId: string) => put<any>(`/admin/services/${id}/agent?agent_id=${agentId}`),
|
assignAgentToService: (id: string, agentId: string) => put<any>(`/admin/services/${id}/agent?agent_id=${agentId}`),
|
||||||
// 会员管理
|
// 会员管理(增强版)
|
||||||
getPlans: () => get<any[]>('/admin/membership/plans'),
|
getPlans: () => get<any[]>('/admin/membership/plans'),
|
||||||
addPlan: (data: any) => post<any>('/admin/membership/plans', data),
|
addPlan: (params: string) => post<any>(`/admin/membership/plans${params}`),
|
||||||
updatePlan: (id: string, data: any) => put<any>(`/admin/membership/plans/${id}`, data),
|
updatePlan: (id: string, params: string) => put<any>(`/admin/membership/plans/${id}${params}`),
|
||||||
deletePlan: (id: string) => del<any>(`/admin/membership/plans/${id}`),
|
deletePlan: (id: string) => del<any>(`/admin/membership/plans/${id}`),
|
||||||
getSubscriptions: () => get<any[]>('/admin/membership/subscriptions'),
|
getSubscriptions: (page?: number) => get<any>(`/admin/membership/subscriptions${page ? `?page=${page}` : ''}`),
|
||||||
getAutomationRules: () => get<any>('/admin/membership/automation-rules'),
|
getAutomationRules: () => get<any>('/admin/membership/automation-rules'),
|
||||||
|
getBenefitsComparison: () => get<any>('/admin/membership/benefits-comparison'),
|
||||||
|
getMembershipConfigs: () => get<any[]>('/admin/membership/global-configs'),
|
||||||
|
updateMembershipConfig: (key: string, params: string) => put<any>(`/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<any>(`/admin/membership/user-memberships${qs ? `?${qs}` : ''}`);
|
||||||
|
},
|
||||||
|
assignPlan: (userId: string, planId: string, days: number) => put<any>(`/admin/membership/users/${userId}/assign-plan?plan_id=${planId}&duration_days=${days}`),
|
||||||
|
revokeMembership: (userId: string) => put<any>(`/admin/membership/users/${userId}/revoke`),
|
||||||
|
getUserUsage: (userId: string) => get<any>(`/admin/membership/users/${userId}/usage`),
|
||||||
|
getMembershipStats: () => get<any>('/admin/membership/stats'),
|
||||||
// 广告管理
|
// 广告管理
|
||||||
getAds: () => get<any[]>('/admin/ads/'),
|
getAds: () => get<any[]>('/admin/ads/'),
|
||||||
addAd: (data: any) => post<any>('/admin/ads/', data),
|
addAd: (data: any) => post<any>('/admin/ads/', data),
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user