更新超级管理员收入

This commit is contained in:
zhanggangyong
2026-01-12 07:47:50 +00:00
parent 0ad63198f5
commit b75f9671ff
12 changed files with 1120 additions and 116 deletions
@@ -0,0 +1,269 @@
"""
邮箱验证码功能
"""
import os
import random
import string
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import Optional
import structlog
from app.state import get_state
logger = structlog.get_logger(__name__)
# 邮箱配置 - 从环境变量读取
SMTP_SERVER = os.getenv("SMTP_SERVER", "smtp.office365.com")
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
SMTP_EMAIL = os.getenv("SMTP_EMAIL", "supportagnet@taijiaicloud.com")
# 注意:生产环境应该通过环境变量设置SMTP_PASSWORD,不要硬编码密码
# 这里使用默认值仅用于开发测试,生产环境必须通过环境变量配置
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "l5YYL7TOK2WvRKtf")
# 验证码配置
VERIFICATION_CODE_LENGTH = 6
VERIFICATION_CODE_EXPIRE_SECONDS = 600 # 10分钟
def _send_email_sync(msg: MIMEMultipart) -> None:
"""同步发送邮件(在 executor 中运行)"""
if not SMTP_PASSWORD:
raise ValueError("SMTP_PASSWORD环境变量未设置,无法发送邮件")
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
try:
server.starttls()
server.login(SMTP_EMAIL, SMTP_PASSWORD)
server.send_message(msg)
logger.info("邮件发送成功", to=msg['To'])
except smtplib.SMTPAuthenticationError as e:
logger.error("SMTP认证失败", error=str(e), smtp_server=SMTP_SERVER, smtp_email=SMTP_EMAIL)
raise
except smtplib.SMTPException as e:
logger.error("SMTP发送失败", error=str(e))
raise
finally:
server.quit()
def generate_verification_code() -> str:
"""生成6位数字验证码"""
return ''.join(random.choices(string.digits, k=VERIFICATION_CODE_LENGTH))
async def send_verification_code(email: str, code: str) -> bool:
"""
发送验证码邮件
Args:
email: 收件人邮箱
code: 验证码
Returns:
是否发送成功
"""
if not SMTP_PASSWORD:
logger.error("SMTP密码未配置,无法发送邮件", email=email)
return False
try:
# 创建邮件
msg = MIMEMultipart()
msg['From'] = SMTP_EMAIL
msg['To'] = email
msg['Subject'] = "Taiji AI-PAD 注册验证码"
# 邮件正文
body = f"""
尊敬的用户:
您的注册验证码是:{code}
验证码有效期为10分钟,请勿泄露给他人。
如果您没有进行注册操作,请忽略此邮件。
此邮件由系统自动发送,请勿回复。
---
Taiji AI-PAD 团队
"""
msg.attach(MIMEText(body, 'plain', 'utf-8'))
# 发送邮件(使用同步方式,因为 smtplib 不支持异步)
import asyncio
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, _send_email_sync, msg)
logger.info("验证码邮件发送成功", email=email)
return True
except smtplib.SMTPAuthenticationError as e:
logger.error(
"SMTP认证失败,请检查SMTP_PASSWORD是否正确",
email=email,
error=str(e),
smtp_server=SMTP_SERVER,
smtp_email=SMTP_EMAIL,
hint="Office365可能需要使用应用专用密码(App Password)而不是普通密码"
)
return False
except smtplib.SMTPException as e:
logger.error("SMTP发送失败", email=email, error=str(e), smtp_server=SMTP_SERVER)
return False
except Exception as e:
logger.error("验证码邮件发送失败", email=email, error=str(e), error_type=type(e).__name__)
return False
async def store_verification_code(email: str, code: str) -> bool:
"""
存储验证码到Redis
Args:
email: 邮箱地址
code: 验证码
Returns:
是否存储成功
"""
try:
state = get_state()
if not state.redis_client:
logger.warning("Redis未连接,无法存储验证码")
return False
key = f"verification_code:{email}"
await state.redis_client.setex(
key,
VERIFICATION_CODE_EXPIRE_SECONDS,
code
)
logger.info("验证码已存储", email=email)
return True
except Exception as e:
logger.error("验证码存储失败", email=email, error=str(e))
return False
async def verify_code(email: str, code: str) -> bool:
"""
验证验证码
Args:
email: 邮箱地址
code: 验证码
Returns:
是否验证成功
"""
try:
state = get_state()
if not state.redis_client:
logger.warning("Redis未连接,无法验证验证码")
return False
key = f"verification_code:{email}"
# 处理Redis集群的MOVED重定向
max_retries = 3
for attempt in range(max_retries):
try:
stored_code = await state.redis_client.get(key)
break
except Exception as e:
error_str = str(e)
if "MOVED" in error_str and attempt < max_retries - 1:
# Redis集群重定向,等待后重试
import asyncio
await asyncio.sleep(0.1)
logger.debug("Redis集群重定向,重试中", attempt=attempt+1, error=error_str)
continue
else:
raise
if not stored_code:
logger.warning("验证码不存在或已过期", email=email, provided_code=code)
return False
# 确保都是字符串类型进行比较
stored_code = str(stored_code).strip()
code = str(code).strip()
if stored_code != code:
logger.warning(
"验证码错误",
email=email,
provided_code=code,
stored_code=stored_code,
provided_type=type(code).__name__,
stored_type=type(stored_code).__name__
)
return False
# 验证成功后删除验证码(同样处理集群重定向)
for attempt in range(max_retries):
try:
await state.redis_client.delete(key)
break
except Exception as e:
error_str = str(e)
if "MOVED" in error_str and attempt < max_retries - 1:
import asyncio
await asyncio.sleep(0.1)
continue
else:
logger.warning("删除验证码失败,但验证已成功", email=email, error=error_str)
break
logger.info("验证码验证成功", email=email)
return True
except Exception as e:
logger.error("验证码验证失败", email=email, code=code, error=str(e), error_type=type(e).__name__)
return False
async def send_and_store_verification_code(email: str) -> Optional[str]:
"""
生成、发送并存储验证码
Args:
email: 邮箱地址
Returns:
验证码(如果成功),None(如果失败)
"""
import os
code = generate_verification_code()
# 先存储验证码(即使邮件发送失败,验证码也已存储,可以手动查看Redis)
store_success = await store_verification_code(email, code)
if not store_success:
logger.error("验证码存储失败,无法继续", email=email)
return None
# 发送邮件
send_success = await send_verification_code(email, code)
# 测试模式:即使邮件发送失败也返回验证码(仅用于开发/测试环境)
test_mode = os.getenv("ENABLE_TEST_MODE", "false").lower() == "true" or os.getenv("DEBUG", "false").lower() == "true"
if not send_success:
if test_mode:
# 测试模式:记录验证码到日志(仅测试环境)
logger.warning(
"测试模式:邮件发送失败,但验证码已存储到Redis",
email=email,
verification_code=code,
hint="验证码已存储到Redis,可通过Redis获取或查看日志(仅测试环境)"
)
return code
else:
# 生产模式:邮件发送失败则不返回验证码
logger.error("邮件发送失败,验证码已存储但未发送", email=email)
return None
logger.info("验证码已发送并存储", email=email)
return code
+31 -5
View File
@@ -39,11 +39,37 @@ def register_lifecycle_events(app: FastAPI) -> None:
# Redis是可选的,连接失败不影响服务启动
try:
if settings.redis_url:
state.redis_client = redis.from_url(
settings.redis_url,
encoding="utf-8",
decode_responses=True,
)
# 检查是否是集群模式(通过URL或错误信息判断)
# Azure Redis Cache集群模式需要使用集群客户端
try:
# 先尝试普通连接
test_client = redis.from_url(
settings.redis_url,
encoding="utf-8",
decode_responses=True,
)
await test_client.ping()
await test_client.aclose()
# 普通连接成功,使用普通客户端
state.redis_client = redis.from_url(
settings.redis_url,
encoding="utf-8",
decode_responses=True,
)
except Exception as cluster_test:
# 如果普通连接失败,可能是集群模式,尝试使用集群客户端
# 注意:redis-py的集群支持需要额外配置
# 这里先使用普通连接,但添加重定向处理
logger.warning("Redis普通连接失败,尝试集群模式", error=str(cluster_test))
# 对于Azure Redis Cache,通常使用普通连接但需要处理MOVED重定向
# 使用skip_full_coverage_check=True来允许部分节点连接
state.redis_client = redis.from_url(
settings.redis_url,
encoding="utf-8",
decode_responses=True,
skip_full_coverage_check=True,
)
await state.redis_client.ping()
redis_connections.set(1)
logger.info("Redis连接成功")
+19 -7
View File
@@ -18,7 +18,7 @@ from models import (
BillingRecord, Application, ModelProvider,
ChannelProviderAccess, ProviderApplication,
ChannelCustomAgentQuota, ResourceApplication, PlatformAgentQuota,
PlatformAgentTemplateConfig
PlatformAgentTemplateConfig, AgentBillingRecord, ModelBillingRecord
)
from app.auth import require_auth, get_password_hash
from app.schemas import (
@@ -483,13 +483,25 @@ async def get_admin_dashboard_stats(
# 总 Agent 数
total_agents = platform_agents_count + custom_agents_count
# 总调用次数
calls_count = await db.execute(select(func.count(BillingRecord.id)))
total_calls = calls_count.scalar() or 0
# 总调用次数 - 从 AgentBillingRecord 和 ModelBillingRecord 统计
agent_calls_count = await db.execute(select(func.count(AgentBillingRecord.id)))
agent_calls = agent_calls_count.scalar() or 0
# 总收入
revenue = await db.execute(select(func.sum(BillingRecord.cost)))
total_revenue = float(revenue.scalar() or 0)
model_calls_count = await db.execute(select(func.count(ModelBillingRecord.id)))
model_calls = model_calls_count.scalar() or 0
total_calls = agent_calls + model_calls
# 总收入 - 从 AgentBillingRecord 和 ModelBillingRecord 统计
# AgentBillingRecord.cost 是 Agent 使用费用
agent_revenue = await db.execute(select(func.sum(AgentBillingRecord.cost)))
agent_total = float(agent_revenue.scalar() or 0)
# ModelBillingRecord.total_cost 是模型调用费用
model_revenue = await db.execute(select(func.sum(ModelBillingRecord.total_cost)))
model_total = float(model_revenue.scalar() or 0)
total_revenue = agent_total + model_total
# 从 Agent Manager 获取 K8s 中实际运行的平台端 Agent 资源统计
k8s_agents_count = 0
+216 -5
View File
@@ -3,7 +3,7 @@
"""
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
import secrets
@@ -11,7 +11,10 @@ import hashlib
from typing import Optional
from database import get_db
from models import User, Channel, APIKey
from models import (
User, Channel, APIKey, Balance, TenantCustomAgentQuota,
PlatformAgentQuota, TenantModelKey, ResourceAllocation, ModelProvider
)
from app.auth import (
authenticate_user,
create_access_token,
@@ -28,7 +31,16 @@ from app.schemas import (
RegenerateAPIKeyResponse,
UserCreate,
)
from app.email_verification import verify_code, send_and_store_verification_code
from app.agent_manager_client import get_agent_manager_client, AgentManagerError
from app.litellm_client import get_litellm_client, LiteLLMClientError
from config import settings
import uuid
from datetime import datetime
from sqlalchemy import and_
import structlog
logger = structlog.get_logger(__name__)
router = APIRouter(prefix="/api/auth", tags=["认证"])
@@ -410,13 +422,60 @@ async def regenerate_api_key(
)
@router.post("/register/send-code", response_model=SuccessResponse)
async def send_verification_code_endpoint(
email: str = Query(..., description="邮箱地址"),
db: AsyncSession = Depends(get_db)
):
"""
发送邮箱验证码
在用户注册前,先调用此接口发送验证码到邮箱
"""
# 检查邮箱是否已存在
result = await db.execute(select(User).where(User.email == email))
existing_user = result.scalar_one_or_none()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该邮箱已被注册"
)
# 发送验证码
code = await send_and_store_verification_code(email)
if not code:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="验证码发送失败,请稍后重试"
)
return SuccessResponse(
message="验证码已发送到您的邮箱,请查收"
)
@router.post("/register", response_model=SuccessResponse)
async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
"""
用户注册接口
允许用户自由注册,创建普通用户账户
注册成功后自动分配默认资源:
- 分配到 taiji 渠道 (channelId: b415e70b-8d37-481c-b229-bc3b7871607b)
- 自定义 agent 配额:2 CPU, 2 GB 内存
- 平台 agent 各1个
- 供应商所有模型
- 余额:20元
"""
# 验证邮箱验证码
is_valid = await verify_code(req.email, req.verification_code)
if not is_valid:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="验证码错误或已过期"
)
# 检查邮箱是否已存在
result = await db.execute(select(User).where(User.email == req.email))
existing_user = result.scalar_one_or_none()
@@ -427,7 +486,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
detail="该邮箱已被注册"
)
# 检查用户名是否已存在(如果提供了username)
# 检查用户名是否已存在
if req.username:
result = await db.execute(select(User).where(User.username == req.username))
existing_username = result.scalar_one_or_none()
@@ -437,6 +496,20 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
detail="该用户名已被使用"
)
# taiji 渠道 ID
TAIJI_CHANNEL_ID = uuid.UUID("b415e70b-8d37-481c-b229-bc3b7871607b")
# 验证渠道存在
channel_result = await db.execute(
select(Channel).where(Channel.id == TAIJI_CHANNEL_ID)
)
channel = channel_result.scalar_one_or_none()
if not channel:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="渠道不存在,请联系管理员"
)
# 创建新用户
password_hash = get_password_hash(req.password)
username = req.username or req.email.split("@")[0]
@@ -450,6 +523,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
username=username,
full_name=req.full_name or name,
role="user", # 默认角色为普通用户
channel_id=TAIJI_CHANNEL_ID, # 分配到 taiji 渠道
status="active",
is_active=True,
is_admin=False,
@@ -460,8 +534,143 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
)
db.add(new_user)
await db.commit()
await db.refresh(new_user)
await db.flush() # 获取 user.id
user_id = new_user.id
try:
# 1. 创建余额记录,初始余额 20 元
balance = Balance(
user_id=user_id,
eu_balance=20.0
)
db.add(balance)
# 2. 分配自定义 Agent 配额:2 CPU, 2 GB 内存
custom_agent_quota = TenantCustomAgentQuota(
tenant_id=user_id,
cpu_quota=2.0,
memory_quota=2.0,
cpu_used=0.0,
memory_used=0.0,
agent_count=0
)
db.add(custom_agent_quota)
# 3. 获取所有平台 Agent 模板,为每个模板分配 1 个配额
try:
agent_manager_client = get_agent_manager_client()
platform_templates = await agent_manager_client.list_platform_templates()
for template in platform_templates:
template_name = template.template
# 创建平台 Agent 配额记录
platform_quota = PlatformAgentQuota(
target_id=user_id,
target_type="tenant",
template_name=template_name,
pod_quota=1, # 每个平台 agent 分配 1 个配额
pod_used=0,
allocated_by=None, # 系统自动分配
allocated_at=datetime.utcnow()
)
db.add(platform_quota)
# 同时创建 ResourceAllocation 记录(兼容旧逻辑)
allocation = ResourceAllocation(
target_id=user_id,
target_type="tenant",
resource_type="agent",
resource_id=template_name,
quantity=1
)
db.add(allocation)
except (AgentManagerError, Exception) as e:
logger.warning(f"获取平台 Agent 模板失败,跳过平台 Agent 分配: {e}")
# 4. 分配所有供应商模型
providers_result = await db.execute(
select(ModelProvider).where(ModelProvider.is_active == True)
)
providers = providers_result.scalars().all()
if channel.litellm_team_id:
litellm_client = get_litellm_client()
for provider in providers:
# 为每个供应商的每个模型创建 TenantModelKey
for model_name in provider.supported_models:
try:
# 在 LiteLLM 中创建 Key
key = await litellm_client.generate_key(
team_id=channel.litellm_team_id,
models=[model_name],
rpm_limit=provider.rpm or 60,
tpm_limit=provider.tpm or 10000,
max_budget=500.0, # 默认预算
budget_duration="monthly",
key_name=f"tenant-{user_id}-{model_name}",
metadata={
"tenant_id": str(user_id),
"tenant_name": name,
"channel_id": str(TAIJI_CHANNEL_ID),
"channel_name": channel.name,
"model": model_name,
}
)
# 加密存储 Key
encrypted_key = litellm_client.encrypt_key(key.key)
# 保存到数据库
tenant_key = TenantModelKey(
tenant_id=user_id,
channel_id=TAIJI_CHANNEL_ID,
model_name=model_name,
litellm_key_id=key.key,
litellm_key_hash=encrypted_key,
rpm_limit=provider.rpm or 60,
tpm_limit=provider.tpm or 10000,
max_budget=500.0,
budget_duration="monthly",
status="active",
)
db.add(tenant_key)
# 同时记录到 ResourceAllocation
model_allocation = ResourceAllocation(
target_id=user_id,
target_type="tenant",
resource_type="model",
resource_id=str(provider.id),
rpm=provider.rpm or 60,
tpm=provider.tpm or 10000,
)
db.add(model_allocation)
except (LiteLLMClientError, Exception) as e:
logger.warning(f"为模型 {model_name} 创建 LiteLLM Key 失败: {e}")
# 继续处理其他模型
else:
logger.warning(f"渠道 {TAIJI_CHANNEL_ID} 未配置 LiteLLM team,跳过模型分配")
# 提交所有更改
await db.commit()
await db.refresh(new_user)
logger.info(
f"用户注册成功并分配默认资源",
user_id=str(user_id),
email=req.email,
channel_id=str(TAIJI_CHANNEL_ID)
)
except Exception as e:
await db.rollback()
logger.error(f"用户注册失败: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"注册失败: {str(e)}"
)
# 创建JWT token,自动登录
token = create_access_token(
@@ -470,6 +679,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
"email": new_user.email,
"role": new_user.role,
"user_id": str(new_user.id),
"channelId": str(TAIJI_CHANNEL_ID),
}
)
@@ -483,6 +693,7 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
"email": new_user.email,
"username": new_user.username,
"role": new_user.role,
"channelId": str(TAIJI_CHANNEL_ID),
}
},
message="注册成功"
+10 -2
View File
@@ -295,11 +295,19 @@ async def allocate_tenant_resources(
detail="无法获取渠道ID"
)
# 验证租户存在且属于指定渠道
# 验证 tenant_id 格式并确认租户属于指定渠道
try:
tenant_uuid = uuid.UUID(tenant_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无效的租户ID格式"
)
result = await db.execute(
select(User).where(
and_(
User.id == tenant_id,
User.id == tenant_uuid,
User.channel_id == channel_id
)
)
@@ -897,102 +897,8 @@ async def admin_login(payload: Dict[str, str], db: AsyncSession = Depends(get_db
return {"token": token, "tokenType": "bearer", "email": email, "expiresIn": 60 * 60}
@router.get("/admin/dashboard/stats")
async def admin_dashboard_stats(db: AsyncSession = Depends(get_db)) -> Dict[str, Any]:
import logging
logger = logging.getLogger(__name__)
agent_count = (await db.execute(select(func.count(Agent.id)))).scalar() or 0
channels_count = (await db.execute(select(func.count(Channel.id)))).scalar() or 0
tenants_count = (await db.execute(select(func.count(User.id)).where(User.role == "user"))).scalar() or 0
balances = (await db.execute(select(func.coalesce(func.sum(Balance.eu_balance), 0)))).scalar() or 0
# 统计平台端 Agent (type='platform')
platform_agents = (await db.execute(
select(Agent).where(Agent.type == "platform")
)).scalars().all()
platform_agent_count = len(platform_agents)
platform_cpu = sum(float(agent.cpu or 0) for agent in platform_agents)
platform_memory = sum(float(agent.memory or 0) for agent in platform_agents)
# 统计自定义 Agent (type='custom')
custom_agents = (await db.execute(
select(Agent).where(Agent.type == "custom")
)).scalars().all()
custom_agent_count = len(custom_agents)
custom_cpu = sum(float(agent.cpu or 0) for agent in custom_agents)
custom_memory = sum(float(agent.memory or 0) for agent in custom_agents)
# 从 Agent Manager 获取 K8s 中实际运行的 Agent 资源统计
k8s_agents_count = 0
k8s_total_cpu = 0.0
k8s_total_memory = 0.0
try:
from app.agent_manager_client import get_agent_manager_client
client = get_agent_manager_client()
# 获取所有运行中的 Agent
k8s_agents_result = await client.list_agents()
k8s_agents = k8s_agents_result.agents # 从 AgentListResult 中获取 agents 列表
k8s_agents_count = len(k8s_agents)
# 获取每个 Agent 的资源配置
for agent in k8s_agents:
agent_name = agent.get("name")
if agent_name:
try:
metrics = await client.get_agent_metrics(agent_name)
# 解析 CPU (如 "500m" -> 0.5 核)
cpu_limit = metrics.limits.get("cpu", "0")
if cpu_limit.endswith("m"):
k8s_total_cpu += float(cpu_limit[:-1]) / 1000
else:
k8s_total_cpu += float(cpu_limit)
# 解析内存 (如 "512Mi" -> 0.5 GB)
memory_limit = metrics.limits.get("memory", "0")
if memory_limit.endswith("Mi"):
k8s_total_memory += float(memory_limit[:-2]) / 1024
elif memory_limit.endswith("Gi"):
k8s_total_memory += float(memory_limit[:-2])
elif memory_limit.endswith("Ki"):
k8s_total_memory += float(memory_limit[:-2]) / (1024 * 1024)
except Exception as e:
logger.warning(f"获取 Agent {agent_name} 资源指标失败: {e}")
except Exception as e:
logger.warning(f"连接 Agent Manager 失败,使用数据库统计: {e}")
# 如果 Agent Manager 不可用,使用数据库中的平台端 Agent 统计
k8s_total_cpu = platform_cpu
k8s_total_memory = platform_memory
# 总 Agent 数:平台端 + 自定义
total_agents = platform_agent_count + custom_agent_count
return {
"success": True,
"data": {
"totalChannels": channels_count,
"totalTenants": tenants_count,
"totalAgents": total_agents,
"totalCalls": 0,
"totalRevenue": round(float(balances), 2),
"totalAllocatedCpu": round(k8s_total_cpu + custom_cpu, 2),
"totalAllocatedMemory": round(k8s_total_memory + custom_memory, 2),
"platformAgents": {
"count": k8s_agents_count if k8s_agents_count > 0 else platform_agent_count,
"cpu": round(k8s_total_cpu if k8s_total_cpu > 0 else platform_cpu, 2),
"memory": round(k8s_total_memory if k8s_total_memory > 0 else platform_memory, 2),
},
"customAgents": {
"count": custom_agent_count,
"cpu": round(custom_cpu, 2),
"memory": round(custom_memory, 2),
},
},
"message": None,
}
# 注意: /admin/dashboard/stats 接口已移至 admin.py,避免重复定义
# 该接口从 AgentBillingRecord 和 ModelBillingRecord 统计收入和调用次数
@router.get("/admin/channels")
+9
View File
@@ -51,6 +51,15 @@ class PasswordChangeRequest(BaseModel):
new_password: str
class UserCreate(BaseModel):
"""创建用户请求(自由注册)"""
username: str = Field(..., min_length=3, max_length=50, description="用户名,3-50个字符")
email: EmailStr = Field(..., description="邮箱地址")
password: str = Field(..., min_length=8, description="密码,至少8个字符")
verification_code: str = Field(..., min_length=6, max_length=6, description="邮箱验证码,6位数字")
full_name: Optional[str] = Field(None, description="全名/显示名称")
# ============= 密钥管理 =============
class APIKeyInfo(BaseModel):
+1
View File
@@ -398,6 +398,7 @@ class UserCreate(BaseModel):
email: str = Field(..., pattern=r'^[^@]+@[^@]+\.[^@]+$')
password: str = Field(..., min_length=8)
full_name: Optional[str] = None
verification_code: str = Field(..., min_length=6, max_length=6, description="邮箱验证码")
class UserUpdate(BaseModel):