初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
This commit is contained in:
commit
78ff261b5a
199 files changed
+69126
No files matched your search
@@ -0,0 +1 @@
|
||||
# Admin API routes package
|
||||
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
广告位管理 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import AdBanner, AIModel
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import os
|
||||
import json
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 广告图片存储目录
|
||||
ADS_IMAGE_DIR = "/data/share/project/data/ads"
|
||||
os.makedirs(ADS_IMAGE_DIR, exist_ok=True)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_ads(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取广告轮播图列表"""
|
||||
ads = db.query(AdBanner).order_by(AdBanner.sort_order.asc()).all()
|
||||
return [
|
||||
{
|
||||
"id": a.id,
|
||||
"title": a.title,
|
||||
"image_url": a.image_url,
|
||||
"link_app_id": a.link_app_id,
|
||||
"link_app_name": a.link_app_name,
|
||||
"sort_order": a.sort_order,
|
||||
"click_count": a.click_count,
|
||||
"status": a.status,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
||||
}
|
||||
for a in ads
|
||||
]
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_ad(
|
||||
title: str,
|
||||
image_url: str = None,
|
||||
link_app_id: str = None,
|
||||
link_app_name: str = None,
|
||||
sort_order: int = 0,
|
||||
status: str = "active",
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建广告"""
|
||||
ad = AdBanner(
|
||||
id=str(uuid.uuid4()),
|
||||
title=title,
|
||||
image_url=image_url,
|
||||
link_app_id=link_app_id,
|
||||
link_app_name=link_app_name,
|
||||
sort_order=sort_order,
|
||||
status=status,
|
||||
)
|
||||
db.add(ad)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_ad",
|
||||
target_type="ad_banner",
|
||||
target_id=ad.id,
|
||||
detail=f"创建广告 {title}",
|
||||
)
|
||||
|
||||
return {"message": "广告已创建", "id": ad.id}
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_ad(
|
||||
id: str,
|
||||
title: str = None,
|
||||
image_url: str = None,
|
||||
link_app_id: str = None,
|
||||
link_app_name: str = None,
|
||||
sort_order: int = None,
|
||||
status: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新广告"""
|
||||
ad = db.query(AdBanner).filter(AdBanner.id == id).first()
|
||||
if not ad:
|
||||
raise HTTPException(status_code=404, detail="广告不存在")
|
||||
|
||||
if title is not None:
|
||||
ad.title = title
|
||||
if image_url is not None:
|
||||
ad.image_url = image_url
|
||||
if link_app_id is not None:
|
||||
ad.link_app_id = link_app_id
|
||||
if link_app_name is not None:
|
||||
ad.link_app_name = link_app_name
|
||||
if sort_order is not None:
|
||||
ad.sort_order = sort_order
|
||||
if status is not None:
|
||||
ad.status = status
|
||||
ad.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_ad",
|
||||
target_type="ad_banner",
|
||||
target_id=id,
|
||||
detail=f"更新广告 {ad.title}",
|
||||
)
|
||||
|
||||
return {"message": "广告已更新"}
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_ad(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除广告"""
|
||||
ad = db.query(AdBanner).filter(AdBanner.id == id).first()
|
||||
if not ad:
|
||||
raise HTTPException(status_code=404, detail="广告不存在")
|
||||
|
||||
db.delete(ad)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_ad",
|
||||
target_type="ad_banner",
|
||||
target_id=id,
|
||||
detail=f"删除广告 {ad.title}",
|
||||
)
|
||||
|
||||
return {"message": "广告已删除"}
|
||||
|
||||
|
||||
@router.put("/{id}/toggle")
|
||||
async def toggle_ad(
|
||||
id: str,
|
||||
status: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""切换广告状态"""
|
||||
ad = db.query(AdBanner).filter(AdBanner.id == id).first()
|
||||
if not ad:
|
||||
raise HTTPException(status_code=404, detail="广告不存在")
|
||||
|
||||
ad.status = status
|
||||
ad.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="toggle_ad",
|
||||
target_type="ad_banner",
|
||||
target_id=id,
|
||||
detail=f"切换广告 {ad.title} 状态为 {status}",
|
||||
)
|
||||
|
||||
return {"message": f"广告已{status}"}
|
||||
|
||||
|
||||
@router.post("/upload-image")
|
||||
async def upload_ad_image(
|
||||
file: UploadFile = File(...),
|
||||
admin_user=Depends(get_current_admin),
|
||||
):
|
||||
"""上传广告图片,保存到 /data/share/project/data/ads/ 目录"""
|
||||
# 校验文件类型
|
||||
allowed_types = {"image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"}
|
||||
if file.content_type not in allowed_types:
|
||||
raise HTTPException(status_code=400, detail="仅支持 PNG/JPG/GIF/WebP/SVG 格式的图片")
|
||||
|
||||
# 生成唯一文件名
|
||||
ext = os.path.splitext(file.filename or "image.png")[1] or ".png"
|
||||
if ext.lower() not in (".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"):
|
||||
ext = ".png"
|
||||
filename = f"{uuid.uuid4()}{ext}"
|
||||
filepath = os.path.join(ADS_IMAGE_DIR, filename)
|
||||
|
||||
# 保存文件
|
||||
content = await file.read()
|
||||
if len(content) > 10 * 1024 * 1024: # 10MB 限制
|
||||
raise HTTPException(status_code=400, detail="图片大小不能超过10MB")
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
# 返回可访问的URL
|
||||
image_url = f"/app/api/admin/ads/images/{filename}"
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="upload_ad_image",
|
||||
target_type="ad_banner",
|
||||
detail=f"上传广告图片 {filename}",
|
||||
)
|
||||
|
||||
return {"message": "图片上传成功", "image_url": image_url, "filename": filename}
|
||||
|
||||
|
||||
@router.get("/images/{filename}")
|
||||
async def get_ad_image(filename: str):
|
||||
"""获取广告图片"""
|
||||
filepath = os.path.join(ADS_IMAGE_DIR, filename)
|
||||
if not os.path.exists(filepath):
|
||||
raise HTTPException(status_code=404, detail="图片不存在")
|
||||
return FileResponse(filepath)
|
||||
|
||||
|
||||
@router.post("/generate-image")
|
||||
async def generate_ad_image(
|
||||
prompt: str = Query(default=""),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""AI生成广告图:调用LLM生成prompt,然后生成图片(这里生成渐变色占位图)"""
|
||||
# 如果没有传入prompt,使用默认prompt
|
||||
final_prompt = prompt.strip() if prompt.strip() else "A modern tech AI platform banner advertisement, sleek interface, gradient blue purple theme, high quality"
|
||||
|
||||
# 尝试使用默认LLM模型生成更优的prompt
|
||||
enhanced_prompt = final_prompt
|
||||
try:
|
||||
default_model = db.query(AIModel).filter(
|
||||
AIModel.is_default == True, AIModel.is_enabled == True
|
||||
).first()
|
||||
if default_model and default_model.endpoint:
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
headers = {}
|
||||
if default_model.api_key:
|
||||
headers["Authorization"] = f"Bearer {default_model.api_key}"
|
||||
resp = await client.post(
|
||||
default_model.endpoint,
|
||||
json={
|
||||
"model": default_model.name,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个广告创意专家。请根据用户输入的主题,生成一个简短的中文广告图像prompt描述(不超过100字),用于AI图像生成。只需要输出prompt文本本身,不要其他内容。"},
|
||||
{"role": "user", "content": f"主题: {final_prompt}"},
|
||||
],
|
||||
"max_tokens": 200,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
if content:
|
||||
enhanced_prompt = content.strip()[:200]
|
||||
except Exception as e:
|
||||
# LLM增强失败,使用原始prompt
|
||||
pass
|
||||
|
||||
# 生成一个唯一的图片文件名(使用prompt的hash作为标识)
|
||||
import hashlib
|
||||
prompt_hash = hashlib.md5(enhanced_prompt.encode()).hexdigest()[:12]
|
||||
filename = f"ai_gen_{prompt_hash}.svg"
|
||||
filepath = os.path.join(ADS_IMAGE_DIR, filename)
|
||||
|
||||
# 生成SVG广告图
|
||||
svg_content = f'''<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="400" viewBox="0 0 1200 400">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1"/>
|
||||
<stop offset="50%" style="stop-color:#764ba2;stop-opacity:1"/>
|
||||
<stop offset="100%" style="stop-color:#5B5FED;stop-opacity:1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="shape" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:rgba(255,255,255,0.15);stop-opacity:1"/>
|
||||
<stop offset="100%" style="stop-color:rgba(255,255,255,0.05);stop-opacity:1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1200" height="400" fill="url(#bg)"/>
|
||||
<circle cx="1000" cy="100" r="200" fill="url(#shape)" opacity="0.3"/>
|
||||
<circle cx="200" cy="300" r="150" fill="url(#shape)" opacity="0.2"/>
|
||||
<rect x="600" y="150" width="300" height="200" rx="20" fill="url(#shape)" opacity="0.15" transform="rotate(-15 750 250)"/>
|
||||
<text x="600" y="180" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="bold" fill="#ffffff" filter="drop-shadow(0 2px 4px rgba(0,0,0,0.2))">{enhanced_prompt[:40]}</text>
|
||||
<text x="600" y="240" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" fill="rgba(255,255,255,0.8)">Powered by AIFlow</text>
|
||||
</svg>'''
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(svg_content)
|
||||
|
||||
image_url = f"/app/api/admin/ads/images/{filename}"
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="generate_ad_image",
|
||||
target_type="ad_banner",
|
||||
detail=f"AI生成广告图 {filename}, prompt: {enhanced_prompt[:50]}",
|
||||
)
|
||||
|
||||
return {"message": "AI广告图已生成", "image_url": image_url, "prompt": enhanced_prompt}
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
管理员仪表盘 API
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import (
|
||||
User, AIModel, Order, TokenUsageLog, OperationLog, AuditLog
|
||||
)
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
from datetime import datetime, date
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_dashboard_stats(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取管理员仪表盘统计数据"""
|
||||
today = date.today()
|
||||
|
||||
active_users = db.query(func.count(User.id)).filter(
|
||||
User.status == "active"
|
||||
).scalar() or 0
|
||||
|
||||
enabled_models = db.query(func.count(AIModel.id)).filter(
|
||||
AIModel.is_enabled == True
|
||||
).scalar() or 0
|
||||
|
||||
today_orders = db.query(func.count(Order.id)).filter(
|
||||
func.date(Order.created_at) == today
|
||||
).scalar() or 0
|
||||
|
||||
today_tokens = db.query(func.sum(TokenUsageLog.total_tokens)).filter(
|
||||
func.date(TokenUsageLog.created_at) == today
|
||||
).scalar() or 0
|
||||
|
||||
total_tokens = db.query(func.sum(TokenUsageLog.total_tokens)).scalar() or 0
|
||||
|
||||
return {
|
||||
"active_users": active_users,
|
||||
"enabled_models": enabled_models,
|
||||
"today_orders": today_orders,
|
||||
"today_tokens": int(today_tokens),
|
||||
"total_tokens": int(total_tokens),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/recent-logs")
|
||||
async def get_recent_logs(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取最近10条操作日志"""
|
||||
logs = db.query(OperationLog).order_by(
|
||||
OperationLog.created_at.desc()
|
||||
).limit(10).all()
|
||||
return [
|
||||
{
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"username": log.username,
|
||||
"action": log.action,
|
||||
"target": log.target,
|
||||
"target_id": log.target_id,
|
||||
"detail": log.detail,
|
||||
"ip": log.ip,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
]
|
||||
|
||||
|
||||
@router.get("/recent-audit")
|
||||
async def get_recent_audit(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取最近10条审计日志"""
|
||||
logs = db.query(AuditLog).order_by(
|
||||
AuditLog.created_at.desc()
|
||||
).limit(10).all()
|
||||
return [
|
||||
{
|
||||
"id": log.id,
|
||||
"admin_id": log.admin_id,
|
||||
"admin_name": log.admin_name,
|
||||
"action": log.action,
|
||||
"target_type": log.target_type,
|
||||
"target_id": log.target_id,
|
||||
"detail": log.detail,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
]
|
||||
@@ -0,0 +1,810 @@
|
||||
"""
|
||||
全平台日志与风控 API(管理员)
|
||||
关键约束: 操作日志、Token日志、审计日志三个实体绝对不能写DELETE操作
|
||||
|
||||
增强功能:
|
||||
- 审计日志: 按时间范围、用户、操作类型多条件过滤
|
||||
- 操作日志: 关键词搜索和导出
|
||||
- API调用统计: 端点调用次数、平均响应时间、错误率
|
||||
- 慢请求记录: 响应时间超过阈值自动标记
|
||||
- 统计概览API: /app/api/admin/logs/stats
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, text
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import (
|
||||
OperationLog, TokenUsageLog, Order, AuditLog, RiskRule,
|
||||
ApiCallStat, SlowRequestLog, User
|
||||
)
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import csv
|
||||
import io
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 日志概览统计 API
|
||||
# ============================================================
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_log_stats(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取日志概览数据(今日调用量、错误数、活跃用户等)"""
|
||||
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
# 今日API调用总量
|
||||
today_calls = db.query(func.sum(ApiCallStat.call_count)).filter(
|
||||
ApiCallStat.updated_at >= today_start
|
||||
).scalar() or 0
|
||||
|
||||
# 今日错误数
|
||||
today_errors = db.query(func.sum(ApiCallStat.error_count)).filter(
|
||||
ApiCallStat.updated_at >= today_start
|
||||
).scalar() or 0
|
||||
|
||||
# 今日操作日志数
|
||||
today_ops = db.query(OperationLog).filter(
|
||||
OperationLog.created_at >= today_start
|
||||
).count()
|
||||
|
||||
# 今日审计日志数
|
||||
today_audits = db.query(AuditLog).filter(
|
||||
AuditLog.created_at >= today_start
|
||||
).count()
|
||||
|
||||
# 今日活跃用户数(有操作记录的用户)
|
||||
today_active_users = db.query(func.count(func.distinct(OperationLog.user_id))).filter(
|
||||
OperationLog.created_at >= today_start
|
||||
).scalar() or 0
|
||||
|
||||
# 今日Token消耗总量
|
||||
today_tokens = db.query(func.sum(TokenUsageLog.total_tokens)).filter(
|
||||
TokenUsageLog.created_at >= today_start
|
||||
).scalar() or 0
|
||||
|
||||
# 慢请求数(今日)
|
||||
today_slow = db.query(SlowRequestLog).filter(
|
||||
SlowRequestLog.created_at >= today_start
|
||||
).count()
|
||||
|
||||
# 总用户数
|
||||
total_users = db.query(User).count()
|
||||
|
||||
# 错误率
|
||||
error_rate = round(today_errors / max(today_calls, 1) * 100, 2)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"today": {
|
||||
"api_calls": int(today_calls),
|
||||
"errors": int(today_errors),
|
||||
"error_rate": error_rate,
|
||||
"operation_logs": today_ops,
|
||||
"audit_logs": today_audits,
|
||||
"active_users": today_active_users,
|
||||
"total_tokens": int(today_tokens),
|
||||
"slow_requests": today_slow,
|
||||
},
|
||||
"total": {
|
||||
"users": total_users,
|
||||
},
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# API调用统计
|
||||
# ============================================================
|
||||
|
||||
@router.get("/api-stats")
|
||||
async def get_api_call_stats(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
sort_by: str = Query("call_count", description="排序字段: call_count / avg_time / error_rate"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取API调用统计:各端点调用次数、平均响应时间、错误率"""
|
||||
query = db.query(ApiCallStat)
|
||||
|
||||
# 排序
|
||||
if sort_by == "avg_time":
|
||||
query = query.order_by(ApiCallStat.total_response_time / func.greatest(ApiCallStat.call_count, 1))
|
||||
elif sort_by == "error_rate":
|
||||
query = query.order_by(
|
||||
(ApiCallStat.error_count * 1.0 / func.greatest(ApiCallStat.call_count, 1)).desc()
|
||||
)
|
||||
else:
|
||||
query = query.order_by(ApiCallStat.call_count.desc())
|
||||
|
||||
total = query.count()
|
||||
stats = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": s.id,
|
||||
"endpoint": s.endpoint,
|
||||
"method": s.method,
|
||||
"call_count": s.call_count,
|
||||
"avg_response_time": round(s.total_response_time / max(s.call_count, 1), 4),
|
||||
"error_count": s.error_count,
|
||||
"error_rate": round(s.error_count / max(s.call_count, 1) * 100, 2),
|
||||
"last_called_at": s.last_called_at.isoformat() if s.last_called_at else None,
|
||||
}
|
||||
for s in stats
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 慢请求记录
|
||||
# ============================================================
|
||||
|
||||
@router.get("/slow-requests")
|
||||
async def get_slow_requests(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
min_response_time: float = Query(None, description="最小响应时间过滤"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取慢请求记录列表"""
|
||||
query = db.query(SlowRequestLog)
|
||||
|
||||
if min_response_time is not None:
|
||||
query = query.filter(SlowRequestLog.response_time >= min_response_time)
|
||||
|
||||
total = query.count()
|
||||
logs = query.order_by(SlowRequestLog.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": log.id,
|
||||
"request_id": log.request_id,
|
||||
"method": log.method,
|
||||
"endpoint": log.endpoint,
|
||||
"status_code": log.status_code,
|
||||
"response_time": log.response_time,
|
||||
"threshold": log.threshold,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 操作日志(增强: 关键词搜索 + 导出)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/operations")
|
||||
async def get_operation_logs(
|
||||
user_id: str = Query(None, description="按用户ID筛选"),
|
||||
username: str = Query(None, description="按用户名筛选"),
|
||||
action: str = Query(None, description="按操作类型筛选"),
|
||||
keyword: str = Query(None, description="关键词搜索(匹配目标、详情)"),
|
||||
start_date: str = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||
end_date: str = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||
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(OperationLog)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(OperationLog.user_id == user_id)
|
||||
if username:
|
||||
query = query.filter(OperationLog.username.like(f"%{username}%"))
|
||||
if action:
|
||||
query = query.filter(OperationLog.action == action)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
(OperationLog.target.like(f"%{keyword}%")) |
|
||||
(OperationLog.detail.like(f"%{keyword}%"))
|
||||
)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(OperationLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(OperationLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
logs = query.order_by(OperationLog.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"username": log.username,
|
||||
"action": log.action,
|
||||
"target": log.target,
|
||||
"target_id": log.target_id,
|
||||
"detail": log.detail,
|
||||
"ip": log.ip,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Token调用日志
|
||||
# ============================================================
|
||||
|
||||
@router.get("/token-logs")
|
||||
async def get_token_logs(
|
||||
user_id: str = Query(None, description="按用户ID筛选"),
|
||||
model_id: str = Query(None, description="按模型ID筛选"),
|
||||
keyword: str = Query(None, description="关键词搜索(匹配模型名称/应用名称)"),
|
||||
start_date: str = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||
end_date: str = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||
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),
|
||||
):
|
||||
"""获取Token调用日志列表"""
|
||||
query = db.query(TokenUsageLog)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(TokenUsageLog.user_id == user_id)
|
||||
if model_id:
|
||||
query = query.filter(TokenUsageLog.model_id == model_id)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
(TokenUsageLog.model_name.like(f"%{keyword}%")) |
|
||||
(TokenUsageLog.app_name.like(f"%{keyword}%"))
|
||||
)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(TokenUsageLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(TokenUsageLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
logs = query.order_by(TokenUsageLog.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"model_id": log.model_id,
|
||||
"model_name": log.model_name,
|
||||
"app_id": log.app_id,
|
||||
"app_name": log.app_name,
|
||||
"conversation_id": log.conversation_id,
|
||||
"prompt_tokens": log.prompt_tokens,
|
||||
"completion_tokens": log.completion_tokens,
|
||||
"total_tokens": log.total_tokens,
|
||||
"cost": float(log.cost) if log.cost else 0,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 付费订单日志
|
||||
# ============================================================
|
||||
|
||||
@router.get("/payment-logs")
|
||||
async def get_payment_logs(
|
||||
user_id: str = Query(None, description="按用户ID筛选"),
|
||||
status: str = Query(None, description="按状态筛选"),
|
||||
keyword: str = Query(None, description="关键词搜索(匹配项目名称)"),
|
||||
start_date: str = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||
end_date: str = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||
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(Order)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(Order.user_id == user_id)
|
||||
if status:
|
||||
query = query.filter(Order.status == status)
|
||||
if keyword:
|
||||
query = query.filter(Order.item_name.like(f"%{keyword}%"))
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(Order.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(Order.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
orders = query.order_by(Order.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": o.id,
|
||||
"user_id": o.user_id,
|
||||
"order_type": o.order_type,
|
||||
"item_id": o.item_id,
|
||||
"item_name": o.item_name,
|
||||
"amount": float(o.amount) if o.amount else 0,
|
||||
"coupon_id": o.coupon_id,
|
||||
"discount_amount": float(o.discount_amount) if o.discount_amount else 0,
|
||||
"payment_method": o.payment_method,
|
||||
"payment_id": o.payment_id,
|
||||
"status": o.status,
|
||||
"created_at": o.created_at.isoformat() if o.created_at else None,
|
||||
"updated_at": o.updated_at.isoformat() if o.updated_at else None,
|
||||
}
|
||||
for o in orders
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 审计日志(增强:多条件过滤)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/audit-logs")
|
||||
async def get_audit_logs(
|
||||
admin_id: str = Query(None, description="按管理员ID筛选"),
|
||||
admin_name: str = Query(None, description="按管理员名称筛选"),
|
||||
action: str = Query(None, description="按操作类型筛选"),
|
||||
target_type: str = Query(None, description="按目标类型筛选"),
|
||||
start_date: str = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||
end_date: str = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||
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(AuditLog)
|
||||
|
||||
if admin_id:
|
||||
query = query.filter(AuditLog.admin_id == admin_id)
|
||||
if admin_name:
|
||||
query = query.filter(AuditLog.admin_name.like(f"%{admin_name}%"))
|
||||
if action:
|
||||
query = query.filter(AuditLog.action == action)
|
||||
if target_type:
|
||||
query = query.filter(AuditLog.target_type == target_type)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(AuditLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(AuditLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
logs = query.order_by(AuditLog.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": log.id,
|
||||
"admin_id": log.admin_id,
|
||||
"admin_name": log.admin_name,
|
||||
"action": log.action,
|
||||
"target_type": log.target_type,
|
||||
"target_id": log.target_id,
|
||||
"detail": log.detail,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 日志导出
|
||||
# ============================================================
|
||||
|
||||
@router.get("/export/operations")
|
||||
async def export_operations(
|
||||
user_id: str = Query(None),
|
||||
action: str = Query(None),
|
||||
keyword: str = Query(None),
|
||||
start_date: str = Query(None),
|
||||
end_date: str = Query(None),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出操作日志(CSV)- 增强:支持与列表相同的过滤条件"""
|
||||
query = db.query(OperationLog)
|
||||
if user_id:
|
||||
query = query.filter(OperationLog.user_id == user_id)
|
||||
if action:
|
||||
query = query.filter(OperationLog.action == action)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
(OperationLog.target.like(f"%{keyword}%")) |
|
||||
(OperationLog.detail.like(f"%{keyword}%"))
|
||||
)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(OperationLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(OperationLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
logs = query.order_by(OperationLog.created_at.desc()).all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["ID", "用户ID", "用户名", "操作", "目标", "目标ID", "详情", "IP", "时间"])
|
||||
|
||||
for log in logs:
|
||||
writer.writerow([
|
||||
log.id, log.user_id, log.username, log.action,
|
||||
log.target, log.target_id, log.detail, log.ip,
|
||||
log.created_at.isoformat() if log.created_at else "",
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=operations_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/export/token-logs")
|
||||
async def export_token_logs(
|
||||
user_id: str = Query(None),
|
||||
keyword: str = Query(None),
|
||||
start_date: str = Query(None),
|
||||
end_date: str = Query(None),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出Token日志(CSV)"""
|
||||
query = db.query(TokenUsageLog)
|
||||
if user_id:
|
||||
query = query.filter(TokenUsageLog.user_id == user_id)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
(TokenUsageLog.model_name.like(f"%{keyword}%")) |
|
||||
(TokenUsageLog.app_name.like(f"%{keyword}%"))
|
||||
)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(TokenUsageLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(TokenUsageLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
logs = query.order_by(TokenUsageLog.created_at.desc()).all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow([
|
||||
"ID", "用户ID", "模型ID", "模型名称", "应用ID", "应用名称",
|
||||
"会话ID", "Prompt Tokens", "Completion Tokens", "Total Tokens", "费用", "时间"
|
||||
])
|
||||
|
||||
for log in logs:
|
||||
writer.writerow([
|
||||
log.id, log.user_id, log.model_id, log.model_name,
|
||||
log.app_id, log.app_name, log.conversation_id,
|
||||
log.prompt_tokens, log.completion_tokens, log.total_tokens,
|
||||
float(log.cost) if log.cost else 0,
|
||||
log.created_at.isoformat() if log.created_at else "",
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=token_logs_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/export/audit-logs")
|
||||
async def export_audit_logs(
|
||||
admin_id: str = Query(None),
|
||||
action: str = Query(None),
|
||||
target_type: str = Query(None),
|
||||
start_date: str = Query(None),
|
||||
end_date: str = Query(None),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出审计日志(CSV)"""
|
||||
query = db.query(AuditLog)
|
||||
if admin_id:
|
||||
query = query.filter(AuditLog.admin_id == admin_id)
|
||||
if action:
|
||||
query = query.filter(AuditLog.action == action)
|
||||
if target_type:
|
||||
query = query.filter(AuditLog.target_type == target_type)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(AuditLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(AuditLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
logs = query.order_by(AuditLog.created_at.desc()).all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["ID", "管理员ID", "管理员名称", "操作", "目标类型", "目标ID", "详情", "时间"])
|
||||
|
||||
for log in logs:
|
||||
writer.writerow([
|
||||
log.id, log.admin_id, log.admin_name, log.action,
|
||||
log.target_type, log.target_id, log.detail,
|
||||
log.created_at.isoformat() if log.created_at else "",
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=audit_logs_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 操作类型枚举
|
||||
# ============================================================
|
||||
|
||||
@router.get("/action-types")
|
||||
async def get_action_types(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取所有操作类型枚举值(用于筛选下拉)"""
|
||||
ops = db.query(OperationLog.action).distinct().all()
|
||||
audits = db.query(AuditLog.action).distinct().all()
|
||||
audit_targets = db.query(AuditLog.target_type).distinct().all()
|
||||
|
||||
return {
|
||||
"operation_actions": [r[0] for r in ops if r[0]],
|
||||
"audit_actions": [r[0] for r in audits if r[0]],
|
||||
"audit_target_types": [r[0] for r in audit_targets if r[0]],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 风控规则管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/risk-rules")
|
||||
async def get_risk_rules(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取风控规则列表"""
|
||||
rules = db.query(RiskRule).order_by(RiskRule.created_at.desc()).all()
|
||||
return [
|
||||
{
|
||||
"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
|
||||
]
|
||||
|
||||
|
||||
@router.post("/risk-rules")
|
||||
async def create_risk_rule(
|
||||
name: str,
|
||||
rule_type: str,
|
||||
threshold: int = 0,
|
||||
duration: int = 3600,
|
||||
is_enabled: bool = True,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建风控规则"""
|
||||
rule = RiskRule(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
type=rule_type,
|
||||
threshold=threshold,
|
||||
duration=duration,
|
||||
is_enabled=is_enabled,
|
||||
)
|
||||
db.add(rule)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_risk_rule",
|
||||
target_type="risk_rule",
|
||||
target_id=rule.id,
|
||||
detail=f"创建风控规则 {name}",
|
||||
)
|
||||
|
||||
return {"message": "风控规则已创建", "id": rule.id}
|
||||
|
||||
|
||||
@router.put("/risk-rules/{id}")
|
||||
async def update_risk_rule(
|
||||
id: str,
|
||||
name: str = None,
|
||||
threshold: int = None,
|
||||
duration: int = None,
|
||||
is_enabled: bool = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新风控规则"""
|
||||
rule = db.query(RiskRule).filter(RiskRule.id == id).first()
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="风控规则不存在")
|
||||
|
||||
if name is not None:
|
||||
rule.name = name
|
||||
if threshold is not None:
|
||||
rule.threshold = threshold
|
||||
if duration is not None:
|
||||
rule.duration = duration
|
||||
if is_enabled is not None:
|
||||
rule.is_enabled = is_enabled
|
||||
rule.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_risk_rule",
|
||||
target_type="risk_rule",
|
||||
target_id=id,
|
||||
detail=f"更新风控规则 {rule.name}",
|
||||
)
|
||||
|
||||
return {"message": "风控规则已更新"}
|
||||
|
||||
|
||||
@router.delete("/risk-rules/{id}")
|
||||
async def delete_risk_rule(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除风控规则"""
|
||||
rule = db.query(RiskRule).filter(RiskRule.id == id).first()
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="风控规则不存在")
|
||||
|
||||
db.delete(rule)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_risk_rule",
|
||||
target_type="risk_rule",
|
||||
target_id=id,
|
||||
detail=f"删除风控规则 {rule.name}",
|
||||
)
|
||||
|
||||
return {"message": "风控规则已删除"}
|
||||
@@ -0,0 +1,412 @@
|
||||
"""
|
||||
应用市场审核 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import MarketApp, Coupon, UserCoupon, User
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 应用审核管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/apps/pending")
|
||||
async def get_pending_apps(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取待审核应用列表"""
|
||||
apps = db.query(MarketApp).filter(
|
||||
MarketApp.status == "pending"
|
||||
).order_by(MarketApp.created_at.desc()).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": a.id,
|
||||
"name": a.name,
|
||||
"type": a.type,
|
||||
"source": a.source,
|
||||
"publisher_id": a.publisher_id,
|
||||
"publisher_name": a.publisher_name,
|
||||
"description": a.description,
|
||||
"icon": a.icon,
|
||||
"tutorial": a.tutorial,
|
||||
"price": float(a.price) if a.price else 0,
|
||||
"is_member_free": a.is_member_free,
|
||||
"join_promotion": a.join_promotion,
|
||||
"status": a.status,
|
||||
"rating_avg": float(a.rating_avg) if a.rating_avg else 0,
|
||||
"review_count": a.review_count,
|
||||
"install_count": a.install_count,
|
||||
"skill_content": a.skill_content,
|
||||
"mcp_transport": a.mcp_transport,
|
||||
"agent_endpoint": a.agent_endpoint,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
||||
}
|
||||
for a in apps
|
||||
]
|
||||
|
||||
|
||||
@router.put("/apps/{id}/approve")
|
||||
async def approve_app(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""审核通过应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
app.status = "published"
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="approve_app",
|
||||
target_type="market_app",
|
||||
target_id=id,
|
||||
detail=f"审核通过应用 {app.name}",
|
||||
)
|
||||
|
||||
return {"message": "应用审核通过"}
|
||||
|
||||
|
||||
@router.put("/apps/{id}/reject")
|
||||
async def reject_app(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""审核拒绝应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
app.status = "removed"
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="reject_app",
|
||||
target_type="market_app",
|
||||
target_id=id,
|
||||
detail=f"审核拒绝应用 {app.name}",
|
||||
)
|
||||
|
||||
return {"message": "应用审核已拒绝"}
|
||||
|
||||
|
||||
@router.get("/apps")
|
||||
async def get_all_apps(
|
||||
status: str = Query(None, description="状态筛选: pending/published/removed"),
|
||||
app_type: str = Query(None, description="类型筛选: skill/agent/mcp"),
|
||||
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(MarketApp)
|
||||
|
||||
if status:
|
||||
query = query.filter(MarketApp.status == status)
|
||||
if app_type:
|
||||
query = query.filter(MarketApp.type == app_type)
|
||||
|
||||
total = query.count()
|
||||
apps = query.order_by(MarketApp.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": a.id,
|
||||
"name": a.name,
|
||||
"type": a.type,
|
||||
"source": a.source,
|
||||
"publisher_id": a.publisher_id,
|
||||
"publisher_name": a.publisher_name,
|
||||
"description": a.description,
|
||||
"icon": a.icon,
|
||||
"tutorial": a.tutorial,
|
||||
"price": float(a.price) if a.price else 0,
|
||||
"is_member_free": a.is_member_free,
|
||||
"join_promotion": a.join_promotion,
|
||||
"status": a.status,
|
||||
"rating_avg": float(a.rating_avg) if a.rating_avg else 0,
|
||||
"review_count": a.review_count,
|
||||
"install_count": a.install_count,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
||||
}
|
||||
for a in apps
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.put("/apps/{id}")
|
||||
async def admin_edit_app(
|
||||
id: str,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
icon: str = None,
|
||||
price: float = None,
|
||||
is_member_free: bool = None,
|
||||
join_promotion: bool = None,
|
||||
tutorial: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""管理员编辑应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
if name is not None:
|
||||
app.name = name
|
||||
if description is not None:
|
||||
app.description = description
|
||||
if icon is not None:
|
||||
app.icon = icon
|
||||
if price is not None:
|
||||
app.price = price
|
||||
if is_member_free is not None:
|
||||
app.is_member_free = is_member_free
|
||||
if join_promotion is not None:
|
||||
app.join_promotion = join_promotion
|
||||
if tutorial is not None:
|
||||
app.tutorial = tutorial
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="edit_market_app",
|
||||
target_type="market_app",
|
||||
target_id=id,
|
||||
detail=f"管理员编辑应用 {app.name}",
|
||||
)
|
||||
|
||||
return {"message": "应用已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 优惠券管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/coupons")
|
||||
async def get_coupons(
|
||||
status: str = Query(None, description="状态筛选: active/expired/disabled"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取优惠券列表"""
|
||||
query = db.query(Coupon)
|
||||
|
||||
if status:
|
||||
query = query.filter(Coupon.status == status)
|
||||
|
||||
coupons = query.order_by(Coupon.created_at.desc()).all()
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"code": c.code,
|
||||
"type": c.type,
|
||||
"value": float(c.value) if c.value else 0,
|
||||
"min_amount": float(c.min_amount) if c.min_amount else 0,
|
||||
"valid_from": c.valid_from.isoformat() if c.valid_from else None,
|
||||
"valid_to": c.valid_to.isoformat() if c.valid_to else None,
|
||||
"total_count": c.total_count,
|
||||
"used_count": c.used_count,
|
||||
"status": c.status,
|
||||
"created_at": c.created_at.isoformat() if c.created_at else None,
|
||||
}
|
||||
for c in coupons
|
||||
]
|
||||
|
||||
|
||||
@router.post("/coupons")
|
||||
async def create_coupon(
|
||||
code: str,
|
||||
coupon_type: str,
|
||||
value: float,
|
||||
valid_from: str,
|
||||
valid_to: str,
|
||||
min_amount: float = 0,
|
||||
total_count: int = None,
|
||||
status: str = "active",
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建优惠券"""
|
||||
# 检查code是否已存在
|
||||
existing = db.query(Coupon).filter(Coupon.code == code).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="优惠券码已存在")
|
||||
|
||||
coupon = Coupon(
|
||||
id=str(uuid.uuid4()),
|
||||
code=code,
|
||||
type=coupon_type,
|
||||
value=value,
|
||||
min_amount=min_amount,
|
||||
valid_from=datetime.fromisoformat(valid_from),
|
||||
valid_to=datetime.fromisoformat(valid_to),
|
||||
total_count=total_count,
|
||||
status=status,
|
||||
)
|
||||
db.add(coupon)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_coupon",
|
||||
target_type="coupon",
|
||||
target_id=coupon.id,
|
||||
detail=f"创建优惠券 {code}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已创建", "id": coupon.id}
|
||||
|
||||
|
||||
@router.put("/coupons/{id}")
|
||||
async def update_coupon(
|
||||
id: str,
|
||||
code: str = None,
|
||||
value: float = None,
|
||||
min_amount: float = None,
|
||||
valid_from: str = None,
|
||||
valid_to: str = None,
|
||||
total_count: int = None,
|
||||
status: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新优惠券"""
|
||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||
|
||||
if code is not None:
|
||||
# 检查新code是否重复
|
||||
if code != coupon.code:
|
||||
dup = db.query(Coupon).filter(Coupon.code == code).first()
|
||||
if dup:
|
||||
raise HTTPException(status_code=400, detail="优惠券码已存在")
|
||||
coupon.code = code
|
||||
if value is not None:
|
||||
coupon.value = value
|
||||
if min_amount is not None:
|
||||
coupon.min_amount = min_amount
|
||||
if valid_from is not None:
|
||||
coupon.valid_from = datetime.fromisoformat(valid_from)
|
||||
if valid_to is not None:
|
||||
coupon.valid_to = datetime.fromisoformat(valid_to)
|
||||
if total_count is not None:
|
||||
coupon.total_count = total_count
|
||||
if status is not None:
|
||||
coupon.status = status
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_coupon",
|
||||
target_type="coupon",
|
||||
target_id=id,
|
||||
detail=f"更新优惠券 {coupon.code}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已更新"}
|
||||
|
||||
|
||||
@router.delete("/coupons/{id}")
|
||||
async def delete_coupon(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除优惠券"""
|
||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||
|
||||
db.delete(coupon)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_coupon",
|
||||
target_type="coupon",
|
||||
target_id=id,
|
||||
detail=f"删除优惠券 {coupon.code}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已删除"}
|
||||
|
||||
|
||||
@router.post("/coupons/{id}/assign")
|
||||
async def assign_coupon(
|
||||
id: str,
|
||||
user_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""发放优惠券给用户"""
|
||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||
if coupon.status != "active":
|
||||
raise HTTPException(status_code=400, detail="优惠券非活跃状态")
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 检查是否已发放
|
||||
existing = db.query(UserCoupon).filter(
|
||||
UserCoupon.user_id == user_id,
|
||||
UserCoupon.coupon_id == id,
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="该用户已拥有此优惠券")
|
||||
|
||||
user_coupon = UserCoupon(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
coupon_id=id,
|
||||
)
|
||||
db.add(user_coupon)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="assign_coupon",
|
||||
target_type="coupon",
|
||||
target_id=id,
|
||||
detail=f"发放优惠券 {coupon.code} 给用户 {user.username}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已发放"}
|
||||
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
会员订阅体系管理 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import MembershipPlan, MembershipSubscription, User, RiskRule
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 会员计划 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 [
|
||||
{
|
||||
"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")
|
||||
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,
|
||||
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,
|
||||
)
|
||||
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}",
|
||||
)
|
||||
|
||||
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,
|
||||
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
|
||||
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="会员计划不存在")
|
||||
|
||||
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("/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()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": s.id,
|
||||
"user_id": s.user_id,
|
||||
"plan_id": s.plan_id,
|
||||
"order_id": s.order_id,
|
||||
"start_at": s.start_at.isoformat() if s.start_at else None,
|
||||
"end_at": s.end_at.isoformat() if s.end_at else None,
|
||||
"auto_renew": s.auto_renew,
|
||||
"status": s.status,
|
||||
"created_at": s.created_at.isoformat() if s.created_at else None,
|
||||
}
|
||||
for s in subscriptions
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 自动化规则(风控)
|
||||
# ============================================================
|
||||
|
||||
@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,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
"""
|
||||
大模型库管理 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Body
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import AIModel, ModelProvider
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ============================================================
|
||||
# 协议预设列表
|
||||
# ============================================================
|
||||
|
||||
PROTOCOLS = [
|
||||
{"id": "openai_compatible", "name": "OpenAI兼容", "base_path": "/v1", "description": "兼容OpenAI API格式的模型服务"},
|
||||
{"id": "new_api", "name": "New API", "base_path": "/v1", "description": "新一代API聚合平台"},
|
||||
{"id": "volcengine", "name": "火山引擎", "base_path": "/v1", "description": "字节跳动火山引擎模型服务"},
|
||||
{"id": "aliyun_bailian", "name": "阿里云百炼", "base_path": "/v1", "description": "阿里云百炼大模型平台"},
|
||||
{"id": "ollama", "name": "Ollama", "base_path": "/v1", "description": "本地Ollama模型服务"},
|
||||
{"id": "xiaoma", "name": "小马算力", "base_path": "/v1", "description": "小马算力模型服务"},
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# 模型列表(用于 fetch-models 的回退数据)
|
||||
# ============================================================
|
||||
|
||||
FETCHABLE_MODELS = [
|
||||
{"id": "gpt-4o", "name": "GPT-4o", "capabilities": ["text", "vision", "tool"], "pricing": "paid"},
|
||||
{"id": "gpt-4o-mini", "name": "GPT-4o Mini", "capabilities": ["text", "vision"], "pricing": "paid"},
|
||||
{"id": "deepseek-v3", "name": "DeepSeek-V3", "capabilities": ["text", "thinking"], "pricing": "free"},
|
||||
{"id": "deepseek-r1", "name": "DeepSeek-R1", "capabilities": ["text", "thinking"], "pricing": "paid"},
|
||||
{"id": "qwen-max", "name": "Qwen-Max", "capabilities": ["text", "tool"], "pricing": "free"},
|
||||
{"id": "qwen-vl", "name": "Qwen-VL", "capabilities": ["text", "vision"], "pricing": "paid"},
|
||||
{"id": "whisper", "name": "Whisper", "capabilities": ["audio"], "pricing": "paid"},
|
||||
{"id": "dalle", "name": "DALL-E", "capabilities": ["image"], "pricing": "paid"},
|
||||
]
|
||||
|
||||
# 支持通过 API 获取模型列表的协议类型
|
||||
FETCHABLE_PROTOCOLS = {"openai_compatible", "new_api", "volcengine", "aliyun_bailian", "ollama", "xiaoma"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 辅助函数
|
||||
# ============================================================
|
||||
|
||||
def provider_to_dict(p: ModelProvider) -> dict:
|
||||
return {
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"protocol": p.protocol,
|
||||
"api_key": p.api_key,
|
||||
"api_base_url": p.api_base_url,
|
||||
"apiKey": p.api_key,
|
||||
"apiBaseUrl": p.api_base_url,
|
||||
"created_at": p.created_at.isoformat() if p.created_at else None,
|
||||
"updated_at": p.updated_at.isoformat() if p.updated_at else None,
|
||||
"createdAt": p.created_at.isoformat() if p.created_at else None,
|
||||
"updatedAt": p.updated_at.isoformat() if p.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
def model_to_dict(m: AIModel) -> dict:
|
||||
return {
|
||||
"id": m.id,
|
||||
"provider_id": m.provider_id,
|
||||
"name": m.name,
|
||||
"api_key": m.api_key,
|
||||
"endpoint": m.endpoint,
|
||||
"capabilities": m.capabilities,
|
||||
"timeout": m.timeout,
|
||||
"status": m.status,
|
||||
"price_per_million_tokens": float(m.price_per_million_tokens) if m.price_per_million_tokens else 0,
|
||||
"is_enabled": m.is_enabled,
|
||||
"is_default": bool(m.is_default),
|
||||
"total_calls": m.total_calls or 0,
|
||||
"total_tokens": m.total_tokens or 0,
|
||||
"created_at": m.created_at.isoformat() if m.created_at else None,
|
||||
"updated_at": m.updated_at.isoformat() if m.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 协议列表
|
||||
# ============================================================
|
||||
|
||||
@router.get("/protocols")
|
||||
async def get_protocols(
|
||||
admin_user=Depends(get_current_admin),
|
||||
):
|
||||
"""获取支持的协议列表"""
|
||||
return PROTOCOLS
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模型供应商管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/providers")
|
||||
async def get_providers(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取模型供应商列表"""
|
||||
providers = db.query(ModelProvider).order_by(
|
||||
ModelProvider.created_at.desc()
|
||||
).all()
|
||||
return [provider_to_dict(p) for p in providers]
|
||||
|
||||
|
||||
@router.post("/providers")
|
||||
async def create_provider(
|
||||
name: str = Body(...),
|
||||
protocol: str = Body(...),
|
||||
api_key: str = Body(default=""),
|
||||
api_base_url: str = Body(default=None),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""添加模型供应商"""
|
||||
# 验证协议
|
||||
valid_protocols = [p["id"] for p in PROTOCOLS]
|
||||
if protocol not in valid_protocols:
|
||||
raise HTTPException(status_code=400, detail=f"无效的协议类型,支持: {', '.join(valid_protocols)}")
|
||||
|
||||
provider = ModelProvider(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
protocol=protocol,
|
||||
api_key=api_key,
|
||||
api_base_url=api_base_url,
|
||||
)
|
||||
db.add(provider)
|
||||
db.commit()
|
||||
db.refresh(provider)
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_provider",
|
||||
target_type="model_provider",
|
||||
target_id=provider.id,
|
||||
detail=f"添加模型供应商 {name} (协议: {protocol})",
|
||||
)
|
||||
|
||||
return {"message": "供应商已创建", "id": provider.id, "provider": provider_to_dict(provider)}
|
||||
|
||||
|
||||
@router.put("/providers/{id}")
|
||||
async def update_provider(
|
||||
id: str,
|
||||
name: str = Body(default=None),
|
||||
protocol: str = Body(default=None),
|
||||
api_key: str = Body(default=None),
|
||||
api_base_url: str = Body(default=None),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新模型供应商"""
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
if name is not None:
|
||||
provider.name = name
|
||||
if protocol is not None:
|
||||
valid_protocols = [p["id"] for p in PROTOCOLS]
|
||||
if protocol not in valid_protocols:
|
||||
raise HTTPException(status_code=400, detail=f"无效的协议类型")
|
||||
provider.protocol = protocol
|
||||
if api_key is not None:
|
||||
provider.api_key = api_key
|
||||
if api_base_url is not None:
|
||||
provider.api_base_url = api_base_url
|
||||
provider.updated_at = datetime.now()
|
||||
db.commit()
|
||||
db.refresh(provider)
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_provider",
|
||||
target_type="model_provider",
|
||||
target_id=id,
|
||||
detail=f"更新模型供应商 {provider.name}",
|
||||
)
|
||||
|
||||
return {"message": "供应商已更新", "provider": provider_to_dict(provider)}
|
||||
|
||||
|
||||
@router.delete("/providers/{id}")
|
||||
async def delete_provider(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除模型供应商"""
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
# 检查是否有模型关联
|
||||
model_count = db.query(func.count(AIModel.id)).filter(
|
||||
AIModel.provider_id == id
|
||||
).scalar()
|
||||
if model_count > 0:
|
||||
raise HTTPException(status_code=400, detail=f"该供应商下有 {model_count} 个模型,请先删除模型")
|
||||
|
||||
db.delete(provider)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_provider",
|
||||
target_type="model_provider",
|
||||
target_id=id,
|
||||
detail=f"删除模型供应商 {provider.name}",
|
||||
)
|
||||
|
||||
return {"message": "供应商已删除"}
|
||||
|
||||
|
||||
@router.post("/providers/{id}/fetch-models")
|
||||
async def fetch_models(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取供应商可用模型列表(优先从真实API获取)"""
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Fetching models for provider {provider.name} (protocol: {provider.protocol})")
|
||||
|
||||
api_base = (provider.api_base_url or "").rstrip("/")
|
||||
api_key = provider.api_key or ""
|
||||
|
||||
# 尝试从真实API获取模型列表(只要有 api_base 就尝试调用,api_key 可选)
|
||||
if api_base and provider.protocol in FETCHABLE_PROTOCOLS:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
headers = {}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
resp = await client.get(f"{api_base}/v1/models", headers=headers)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
models_list = data.get("data", [])
|
||||
if models_list:
|
||||
result = []
|
||||
for m in models_list:
|
||||
mid = m.get("id", "")
|
||||
if not mid:
|
||||
continue
|
||||
result.append({
|
||||
"id": mid,
|
||||
"name": mid,
|
||||
"capabilities": ["text"],
|
||||
"pricing": "paid",
|
||||
})
|
||||
logger.info(f"Successfully fetched {len(result)} models from provider API")
|
||||
return {"models": result, "source": "api"}
|
||||
else:
|
||||
logger.warning(f"Provider API returned empty model list")
|
||||
return {"models": [], "source": "api", "message": "API返回的模型列表为空"}
|
||||
elif resp.status_code == 401 or resp.status_code == 403:
|
||||
logger.warning(f"Provider API returned {resp.status_code}: auth error")
|
||||
return {"models": [], "source": "error", "message": "API认证失败,请检查API Key是否正确"}
|
||||
else:
|
||||
logger.warning(f"Provider API returned status {resp.status_code}: {resp.text[:200]}")
|
||||
return {"models": [], "source": "error", "message": f"API请求失败(HTTP {resp.status_code}),请检查API地址和密钥"}
|
||||
except httpx.TimeoutException:
|
||||
logger.warning(f"Timeout fetching models from provider API: {api_base}")
|
||||
return {"models": [], "source": "error", "message": "请求API超时,请检查API地址是否正确"}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch models from provider API: {e}")
|
||||
return {"models": [], "source": "error", "message": f"请求API失败: {str(e)}"}
|
||||
|
||||
# 回退:返回固定模型列表(供无 API 的离线环境使用)
|
||||
logger.info(f"Using fallback model list for provider {provider.name}")
|
||||
return {"models": FETCHABLE_MODELS, "source": "fallback"}
|
||||
|
||||
|
||||
class AddModelsRequest(BaseModel):
|
||||
model_ids: List[str]
|
||||
|
||||
|
||||
@router.post("/providers/{id}/add-models")
|
||||
async def add_models(
|
||||
id: str,
|
||||
req: AddModelsRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""批量添加模型到供应商"""
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
# 找到协议对应的 base_path
|
||||
protocol_info = next((p for p in PROTOCOLS if p["id"] == provider.protocol), None)
|
||||
base_path = protocol_info["base_path"] if protocol_info else "/v1"
|
||||
|
||||
added = []
|
||||
for model_id in req.model_ids:
|
||||
# 在 FETCHABLE_MODELS 中查找额外信息
|
||||
model_info = next((m for m in FETCHABLE_MODELS if m["id"] == model_id), None)
|
||||
|
||||
# 使用 model_id 作为模型名称
|
||||
model_name = model_info["name"] if model_info else model_id
|
||||
|
||||
# 检查是否已存在同名模型
|
||||
existing = db.query(AIModel).filter(
|
||||
AIModel.provider_id == id,
|
||||
AIModel.name == model_name
|
||||
).first()
|
||||
if existing:
|
||||
continue
|
||||
|
||||
# 自动生成 endpoint
|
||||
if provider.api_base_url:
|
||||
endpoint = f"{provider.api_base_url.rstrip('/')}{base_path}/chat/completions"
|
||||
else:
|
||||
endpoint = ""
|
||||
|
||||
model = AIModel(
|
||||
id=str(uuid.uuid4()),
|
||||
provider_id=id,
|
||||
name=model_name,
|
||||
api_key=provider.api_key or "",
|
||||
endpoint=endpoint,
|
||||
capabilities=model_info.get("capabilities") if model_info else ["text"],
|
||||
timeout=60,
|
||||
status="free" if (model_info and model_info.get("pricing") == "free") else "paid",
|
||||
price_per_million_tokens=0,
|
||||
is_enabled=True,
|
||||
)
|
||||
db.add(model)
|
||||
added.append(model_name)
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="add_models",
|
||||
target_type="model_provider",
|
||||
target_id=id,
|
||||
detail=f"为供应商 {provider.name} 批量添加 {len(added)} 个模型",
|
||||
)
|
||||
|
||||
return {"message": f"成功添加 {len(added)} 个模型", "added": added}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模型管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/")
|
||||
async def get_models(
|
||||
provider_id: str = Query(None),
|
||||
status: str = Query(None, description="模型状态筛选: free/paid"),
|
||||
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(AIModel)
|
||||
|
||||
if provider_id:
|
||||
query = query.filter(AIModel.provider_id == provider_id)
|
||||
if status:
|
||||
query = query.filter(AIModel.status == status)
|
||||
|
||||
total = query.count()
|
||||
models = query.order_by(AIModel.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [model_to_dict(m) for m in models],
|
||||
}
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_model(
|
||||
provider_id: str,
|
||||
name: str,
|
||||
api_key: str = None,
|
||||
endpoint: str = None,
|
||||
capabilities: List[str] = None,
|
||||
timeout: int = 60,
|
||||
status: str = "paid",
|
||||
price_per_million_tokens: float = 0,
|
||||
is_enabled: bool = True,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""添加模型"""
|
||||
# 验证供应商存在
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == provider_id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
model = AIModel(
|
||||
id=str(uuid.uuid4()),
|
||||
provider_id=provider_id,
|
||||
name=name,
|
||||
api_key=api_key,
|
||||
endpoint=endpoint,
|
||||
capabilities=capabilities,
|
||||
timeout=timeout,
|
||||
status=status,
|
||||
price_per_million_tokens=price_per_million_tokens,
|
||||
is_enabled=is_enabled,
|
||||
)
|
||||
db.add(model)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_model",
|
||||
target_type="model",
|
||||
target_id=model.id,
|
||||
detail=f"添加模型 {name}",
|
||||
)
|
||||
|
||||
return {"message": "模型已创建", "id": model.id}
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_model(
|
||||
id: str,
|
||||
provider_id: str = None,
|
||||
name: str = None,
|
||||
api_key: str = None,
|
||||
endpoint: str = None,
|
||||
capabilities: List[str] = None,
|
||||
timeout: int = None,
|
||||
status: str = None,
|
||||
price_per_million_tokens: float = None,
|
||||
is_enabled: bool = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新模型"""
|
||||
model = db.query(AIModel).filter(AIModel.id == id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
if provider_id is not None:
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == provider_id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
model.provider_id = provider_id
|
||||
if name is not None:
|
||||
model.name = name
|
||||
if api_key is not None:
|
||||
model.api_key = api_key
|
||||
if endpoint is not None:
|
||||
model.endpoint = endpoint
|
||||
if capabilities is not None:
|
||||
model.capabilities = capabilities
|
||||
if timeout is not None:
|
||||
model.timeout = timeout
|
||||
if status is not None:
|
||||
model.status = status
|
||||
if price_per_million_tokens is not None:
|
||||
model.price_per_million_tokens = price_per_million_tokens
|
||||
if is_enabled is not None:
|
||||
model.is_enabled = is_enabled
|
||||
model.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_model",
|
||||
target_type="model",
|
||||
target_id=id,
|
||||
detail=f"更新模型 {model.name}",
|
||||
)
|
||||
|
||||
return {"message": "模型已更新"}
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_model(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除模型"""
|
||||
model = db.query(AIModel).filter(AIModel.id == id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
model_name = model.name
|
||||
db.delete(model)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_model",
|
||||
target_type="model",
|
||||
target_id=id,
|
||||
detail=f"删除模型 {model_name}",
|
||||
)
|
||||
|
||||
return {"message": "模型已删除"}
|
||||
|
||||
|
||||
@router.put("/{id}/toggle")
|
||||
async def toggle_model(
|
||||
id: str,
|
||||
is_enabled: bool = Query(...),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""开关模型状态"""
|
||||
model = db.query(AIModel).filter(AIModel.id == id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
model.is_enabled = is_enabled
|
||||
model.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="toggle_model",
|
||||
target_type="model",
|
||||
target_id=id,
|
||||
detail=f"将模型 {model.name} {'启用' if is_enabled else '禁用'}",
|
||||
)
|
||||
|
||||
return {"message": f"模型已{'启用' if is_enabled else '禁用'}"}
|
||||
|
||||
|
||||
@router.put("/{id}/set-default")
|
||||
async def set_default_model(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""设为全局默认模型(取消其他模型的默认状态)"""
|
||||
model = db.query(AIModel).filter(AIModel.id == id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
# 取消所有模型的默认状态
|
||||
db.query(AIModel).filter(AIModel.is_default == True).update({"is_default": False})
|
||||
# 设置当前模型为默认
|
||||
model.is_default = True
|
||||
model.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="set_default_model",
|
||||
target_type="model",
|
||||
target_id=id,
|
||||
detail=f"设置模型 {model.name} 为全局默认",
|
||||
)
|
||||
|
||||
return {"message": f"已将 {model.name} 设为全局默认模型"}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_model_stats(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取模型调用统计"""
|
||||
total_calls = db.query(func.sum(AIModel.total_calls)).scalar() or 0
|
||||
total_tokens = db.query(func.sum(AIModel.total_tokens)).scalar() or 0
|
||||
|
||||
return {
|
||||
"total_calls": int(total_calls),
|
||||
"total_tokens": int(total_tokens),
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
支付渠道配置 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import PaymentConfig, Order
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import csv
|
||||
import io
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class PaymentConfigUpdate(BaseModel):
|
||||
app_id: Optional[str] = None
|
||||
merchant_id: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
api_v3_key: Optional[str] = None
|
||||
cert_path: Optional[str] = None
|
||||
cert_serial_no: Optional[str] = None
|
||||
alipay_public_key: Optional[str] = None
|
||||
app_private_key: Optional[str] = None
|
||||
gateway_url: Optional[str] = None
|
||||
notify_url: Optional[str] = None
|
||||
return_url: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 支付配置管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/configs")
|
||||
async def get_payment_configs(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取支付配置列表"""
|
||||
configs = db.query(PaymentConfig).order_by(
|
||||
PaymentConfig.created_at.desc()
|
||||
).all()
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"type": c.type,
|
||||
"app_id": c.app_id,
|
||||
"merchant_id": c.merchant_id,
|
||||
"api_key": c.api_key,
|
||||
"api_v3_key": c.api_v3_key,
|
||||
"cert_path": c.cert_path,
|
||||
"cert_serial_no": c.cert_serial_no,
|
||||
"alipay_public_key": c.alipay_public_key,
|
||||
"app_private_key": c.app_private_key,
|
||||
"gateway_url": c.gateway_url,
|
||||
"notify_url": c.notify_url,
|
||||
"return_url": c.return_url,
|
||||
"status": c.status,
|
||||
"created_at": c.created_at.isoformat() if c.created_at else None,
|
||||
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
|
||||
}
|
||||
for c in configs
|
||||
]
|
||||
|
||||
|
||||
@router.put("/configs/{id}")
|
||||
async def update_payment_config(
|
||||
id: str,
|
||||
data: PaymentConfigUpdate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新支付配置"""
|
||||
config = db.query(PaymentConfig).filter(PaymentConfig.id == id).first()
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="支付配置不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
if hasattr(config, key):
|
||||
setattr(config, key, value)
|
||||
config.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_payment_config",
|
||||
target_type="payment_config",
|
||||
target_id=id,
|
||||
detail=f"更新支付配置 [{config.type}]",
|
||||
)
|
||||
|
||||
return {"code": 0, "message": "支付配置已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 订单管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/orders")
|
||||
async def get_orders(
|
||||
status: str = Query(None, description="订单状态筛选: pending/paid/refunded/cancelled"),
|
||||
order_type: str = Query(None, description="订单类型筛选: membership/service/app"),
|
||||
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(Order)
|
||||
|
||||
if status:
|
||||
query = query.filter(Order.status == status)
|
||||
if order_type:
|
||||
query = query.filter(Order.order_type == order_type)
|
||||
|
||||
total = query.count()
|
||||
orders = query.order_by(Order.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": o.id,
|
||||
"user_id": o.user_id,
|
||||
"order_type": o.order_type,
|
||||
"item_id": o.item_id,
|
||||
"item_name": o.item_name,
|
||||
"amount": float(o.amount) if o.amount else 0,
|
||||
"coupon_id": o.coupon_id,
|
||||
"discount_amount": float(o.discount_amount) if o.discount_amount else 0,
|
||||
"payment_method": o.payment_method,
|
||||
"payment_id": o.payment_id,
|
||||
"status": o.status,
|
||||
"created_at": o.created_at.isoformat() if o.created_at else None,
|
||||
"updated_at": o.updated_at.isoformat() if o.updated_at else None,
|
||||
}
|
||||
for o in orders
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.put("/orders/{id}/refund")
|
||||
async def refund_order(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""退款操作"""
|
||||
order = db.query(Order).filter(Order.id == id).first()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
if order.status != "paid":
|
||||
raise HTTPException(status_code=400, detail="只能对已支付的订单进行退款")
|
||||
if order.status == "refunded":
|
||||
raise HTTPException(status_code=400, detail="订单已退款")
|
||||
|
||||
order.status = "refunded"
|
||||
order.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="refund_order",
|
||||
target_type="order",
|
||||
target_id=id,
|
||||
detail=f"退款订单 {order.id}, 金额 {float(order.amount)}",
|
||||
)
|
||||
|
||||
return {"message": "退款成功"}
|
||||
|
||||
|
||||
@router.get("/orders/export")
|
||||
async def export_orders(
|
||||
status: str = Query(None, description="订单状态筛选"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出订单(CSV格式)"""
|
||||
query = db.query(Order)
|
||||
if status:
|
||||
query = query.filter(Order.status == status)
|
||||
|
||||
orders = query.order_by(Order.created_at.desc()).all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow([
|
||||
"订单ID", "用户ID", "订单类型", "商品ID", "商品名称",
|
||||
"金额", "优惠券ID", "折扣金额", "支付方式", "支付ID",
|
||||
"状态", "创建时间", "更新时间"
|
||||
])
|
||||
|
||||
for o in orders:
|
||||
writer.writerow([
|
||||
o.id, o.user_id, o.order_type, o.item_id, o.item_name,
|
||||
float(o.amount) if o.amount else 0,
|
||||
o.coupon_id,
|
||||
float(o.discount_amount) if o.discount_amount else 0,
|
||||
o.payment_method, o.payment_id,
|
||||
o.status,
|
||||
o.created_at.isoformat() if o.created_at else "",
|
||||
o.updated_at.isoformat() if o.updated_at else "",
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=orders_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,453 @@
|
||||
"""
|
||||
增值服务管理 API(管理员)
|
||||
四大类服务: app_custom / platform_proxy / data_custom / private_project
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from ...database import get_db
|
||||
from ...models import ValueAddedService, CustomServiceOrder, CustomServiceRequest, MarketApp, User
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CreateServiceRequest(BaseModel):
|
||||
name: str
|
||||
service_type: str
|
||||
description: Optional[str] = None
|
||||
price: float = 0
|
||||
features: Optional[str] = None
|
||||
status: str = "active"
|
||||
assigned_agent_id: Optional[str] = None
|
||||
|
||||
|
||||
class UpdateServiceRequest(BaseModel):
|
||||
name: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
service_type: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
features: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
assigned_agent_id: Optional[str] = None
|
||||
|
||||
# 服务类型映射
|
||||
SERVICE_TYPE_MAP = {
|
||||
"app_custom": "应用定制服务",
|
||||
"platform_proxy": "平台代理申请",
|
||||
"data_custom": "数据定制服务",
|
||||
"private_project": "私有化项目服务",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 增值服务 CRUD
|
||||
# ============================================================
|
||||
|
||||
@router.get("/")
|
||||
async def get_services(
|
||||
service_type: str = Query(None, description="服务类型筛选: app_custom/platform_proxy/data_custom/private_project"),
|
||||
status: str = Query(None, description="状态筛选: active/inactive/deleted"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取增值服务列表"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Admin {admin_user.username} requested services list: type={service_type}, status={status}")
|
||||
|
||||
query = db.query(ValueAddedService).filter(ValueAddedService.status != "deleted")
|
||||
|
||||
if service_type:
|
||||
query = query.filter(ValueAddedService.type == service_type)
|
||||
if status:
|
||||
query = query.filter(ValueAddedService.status == status)
|
||||
|
||||
services = query.order_by(ValueAddedService.created_at.desc()).all()
|
||||
result = []
|
||||
for s in services:
|
||||
result.append({
|
||||
"id": s.id,
|
||||
"name": s.name,
|
||||
"description": s.description,
|
||||
"price": float(s.price) if s.price else 0,
|
||||
"type": s.type,
|
||||
"type_name": SERVICE_TYPE_MAP.get(s.type, s.type),
|
||||
"features": s.features or [],
|
||||
"status": s.status,
|
||||
"assigned_agent_id": s.assigned_agent_id,
|
||||
"created_at": s.created_at.isoformat() if s.created_at else None,
|
||||
"updated_at": s.updated_at.isoformat() if s.updated_at else None,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_service(
|
||||
req: CreateServiceRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建增值服务"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if req.service_type not in SERVICE_TYPE_MAP:
|
||||
raise HTTPException(status_code=400, detail=f"无效的服务类型,可选值:{', '.join(SERVICE_TYPE_MAP.keys())}")
|
||||
|
||||
features_json = None
|
||||
if req.features:
|
||||
try:
|
||||
features_json = json.loads(req.features) if isinstance(req.features, str) else req.features
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
features_json = [req.features]
|
||||
|
||||
service = ValueAddedService(
|
||||
id=str(uuid.uuid4()),
|
||||
name=req.name,
|
||||
description=req.description,
|
||||
price=req.price,
|
||||
type=req.service_type,
|
||||
features=features_json,
|
||||
status=req.status,
|
||||
assigned_agent_id=req.assigned_agent_id,
|
||||
)
|
||||
db.add(service)
|
||||
db.commit()
|
||||
db.refresh(service)
|
||||
|
||||
logger.info(f"Admin {admin_user.username} created service: {service.id} - {req.name}")
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_service",
|
||||
target_type="value_added_service",
|
||||
target_id=service.id,
|
||||
detail=f"创建增值服务 {req.name}(类型: {SERVICE_TYPE_MAP.get(req.service_type)})",
|
||||
)
|
||||
|
||||
return {"message": "增值服务已创建", "id": service.id}
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_service(
|
||||
id: str,
|
||||
req: UpdateServiceRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新增值服务"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
service = db.query(ValueAddedService).filter(ValueAddedService.id == id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="增值服务不存在")
|
||||
|
||||
if req.name is not None:
|
||||
service.name = req.name
|
||||
if req.price is not None:
|
||||
service.price = req.price
|
||||
if req.service_type is not None:
|
||||
if req.service_type not in SERVICE_TYPE_MAP:
|
||||
raise HTTPException(status_code=400, detail=f"无效的服务类型")
|
||||
service.type = req.service_type
|
||||
if req.description is not None:
|
||||
service.description = req.description
|
||||
if req.features is not None:
|
||||
try:
|
||||
service.features = json.loads(req.features) if isinstance(req.features, str) else req.features
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
service.features = [req.features]
|
||||
if req.status is not None:
|
||||
service.status = req.status
|
||||
if req.assigned_agent_id is not None:
|
||||
service.assigned_agent_id = req.assigned_agent_id
|
||||
service.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {admin_user.username} updated service: {id}")
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_service",
|
||||
target_type="value_added_service",
|
||||
target_id=id,
|
||||
detail=f"更新增值服务 {service.name}",
|
||||
)
|
||||
|
||||
return {"message": "增值服务已更新"}
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_service(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""逻辑删除增值服务(设置为 deleted 状态,保留数据记录)"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
service = db.query(ValueAddedService).filter(ValueAddedService.id == id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="增值服务不存在")
|
||||
|
||||
if service.status == "deleted":
|
||||
raise HTTPException(status_code=400, detail="该增值服务已被删除")
|
||||
|
||||
logger.info(f"Admin {admin_user.username} soft-deleting service: {id} - {service.name}")
|
||||
service.status = "deleted"
|
||||
service.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_service",
|
||||
target_type="value_added_service",
|
||||
target_id=id,
|
||||
detail=f"逻辑删除增值服务 {service.name}",
|
||||
)
|
||||
|
||||
return {"message": "增值服务已删除(逻辑删除)"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 定制服务工单管理 (CustomServiceRequest)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/custom-requests")
|
||||
async def get_custom_requests(
|
||||
request_type: str = Query(None, description="工单类型: data_custom/private_project/platform_proxy"),
|
||||
status: str = Query(None, description="状态: pending/processing/completed/rejected"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取定制服务工单列表"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Admin {admin_user.username} requested custom requests: type={request_type}, status={status}")
|
||||
|
||||
query = db.query(CustomServiceRequest)
|
||||
|
||||
if request_type:
|
||||
query = query.filter(CustomServiceRequest.request_type == request_type)
|
||||
if status:
|
||||
query = query.filter(CustomServiceRequest.status == status)
|
||||
|
||||
requests = query.order_by(CustomServiceRequest.created_at.desc()).all()
|
||||
result = []
|
||||
for r in requests:
|
||||
# 查询用户名
|
||||
user = db.query(User).filter(User.id == r.user_id).first()
|
||||
# 查询服务名
|
||||
service = db.query(ValueAddedService).filter(ValueAddedService.id == r.service_id).first()
|
||||
result.append({
|
||||
"id": r.id,
|
||||
"user_id": r.user_id,
|
||||
"username": user.username if user else "未知用户",
|
||||
"service_id": r.service_id,
|
||||
"service_name": service.name if service else "未知服务",
|
||||
"request_type": r.request_type,
|
||||
"request_type_name": SERVICE_TYPE_MAP.get(r.request_type, r.request_type),
|
||||
"title": r.title,
|
||||
"description": r.description,
|
||||
"requirements": r.requirements or {},
|
||||
"status": r.status,
|
||||
"assigned_agent_id": r.assigned_agent_id,
|
||||
"created_at": r.created_at.isoformat() if r.created_at else None,
|
||||
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/custom-requests/{id}")
|
||||
async def update_custom_request(
|
||||
id: str,
|
||||
status: str = None,
|
||||
assigned_agent_id: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新定制服务工单(分配Agent / 标记完成)"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
req = db.query(CustomServiceRequest).filter(CustomServiceRequest.id == id).first()
|
||||
if not req:
|
||||
raise HTTPException(status_code=404, detail="工单不存在")
|
||||
|
||||
if status is not None:
|
||||
if status not in ("pending", "processing", "completed", "rejected"):
|
||||
raise HTTPException(status_code=400, detail="无效的状态值")
|
||||
req.status = status
|
||||
if assigned_agent_id is not None:
|
||||
req.assigned_agent_id = assigned_agent_id
|
||||
req.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {admin_user.username} updated custom request: {id}, status={status}, agent={assigned_agent_id}")
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_custom_request",
|
||||
target_type="custom_service_request",
|
||||
target_id=id,
|
||||
detail=f"更新工单状态: {status}, 分配Agent: {assigned_agent_id}",
|
||||
)
|
||||
|
||||
return {"message": "工单已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 定制服务订单管理 (CustomServiceOrder - 保留兼容)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/custom-orders")
|
||||
async def get_custom_orders(
|
||||
status: str = Query(None, description="状态筛选"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取定制服务订单列表"""
|
||||
query = db.query(CustomServiceOrder)
|
||||
|
||||
if status:
|
||||
query = query.filter(CustomServiceOrder.status == status)
|
||||
|
||||
orders = query.order_by(CustomServiceOrder.created_at.desc()).all()
|
||||
result = []
|
||||
for o in orders:
|
||||
user = db.query(User).filter(User.id == o.user_id).first()
|
||||
result.append({
|
||||
"id": o.id,
|
||||
"user_id": o.user_id,
|
||||
"username": user.username if user else "未知用户",
|
||||
"service_id": o.service_id,
|
||||
"service_type": o.service_type,
|
||||
"description": o.description,
|
||||
"status": o.status,
|
||||
"price": float(o.price) if o.price else 0,
|
||||
"order_id": o.order_id,
|
||||
"created_at": o.created_at.isoformat() if o.created_at else None,
|
||||
"updated_at": o.updated_at.isoformat() if o.updated_at else None,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/custom-orders/{id}")
|
||||
async def update_custom_order(
|
||||
id: str,
|
||||
status: str = None,
|
||||
price: float = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新定制服务订单状态"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
order = db.query(CustomServiceOrder).filter(CustomServiceOrder.id == id).first()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="定制服务订单不存在")
|
||||
|
||||
if status is not None:
|
||||
order.status = status
|
||||
if price is not None:
|
||||
order.price = price
|
||||
order.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {admin_user.username} updated custom order: {id}, status={status}")
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_custom_order",
|
||||
target_type="custom_service_order",
|
||||
target_id=id,
|
||||
detail=f"更新定制服务订单状态为 {status}",
|
||||
)
|
||||
|
||||
return {"message": "定制服务订单已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Agent 配置(为应用定制服务绑定市场Agent)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/agents")
|
||||
async def get_available_agents(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取可用的市场Agent列表(用于绑定到应用定制服务)"""
|
||||
from sqlalchemy import or_
|
||||
agents = db.query(MarketApp).filter(
|
||||
MarketApp.type == "agent",
|
||||
or_(MarketApp.status == "published", MarketApp.status == "active")
|
||||
).all()
|
||||
|
||||
result = []
|
||||
for a in agents:
|
||||
result.append({
|
||||
"id": a.id,
|
||||
"name": a.name,
|
||||
"description": a.description,
|
||||
"publisher_name": a.publisher_name,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/{id}/agent")
|
||||
async def assign_agent_to_service(
|
||||
id: str,
|
||||
agent_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""为应用定制服务绑定Agent"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
service = db.query(ValueAddedService).filter(ValueAddedService.id == id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="增值服务不存在")
|
||||
|
||||
# 验证agent存在
|
||||
agent = db.query(MarketApp).filter(
|
||||
MarketApp.id == agent_id,
|
||||
MarketApp.type == "agent"
|
||||
).first()
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent不存在")
|
||||
|
||||
service.assigned_agent_id = agent_id
|
||||
service.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {admin_user.username} assigned agent {agent_id} to service {id}")
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="assign_agent",
|
||||
target_type="value_added_service",
|
||||
target_id=id,
|
||||
detail=f"为服务 {service.name} 绑定Agent: {agent.name}",
|
||||
)
|
||||
|
||||
return {"message": "Agent绑定成功", "agent_name": agent.name}
|
||||
@@ -0,0 +1,508 @@
|
||||
"""
|
||||
网站基础设置 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import SiteSetting, EmailConfig, SmsConfig, User, AIModel
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import smtplib
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import urllib.parse
|
||||
import time as time_module
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("aiflow")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Pydantic Models
|
||||
# ============================================================
|
||||
|
||||
class EmailConfigUpdate(BaseModel):
|
||||
smtp_host: Optional[str] = None
|
||||
smtp_port: Optional[int] = None
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
from_address: Optional[str] = None
|
||||
use_tls: Optional[bool] = None
|
||||
|
||||
|
||||
class SmsConfigUpdate(BaseModel):
|
||||
provider: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
api_secret: Optional[str] = None
|
||||
sign_name: Optional[str] = None
|
||||
template_code: Optional[str] = None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 网站设置
|
||||
# ============================================================
|
||||
|
||||
@router.get("/site")
|
||||
async def get_site_settings(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取网站设置(返回所有 key-value 对)"""
|
||||
settings = db.query(SiteSetting).all()
|
||||
result = {}
|
||||
for s in settings:
|
||||
result[s.key] = s.value
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.put("/site")
|
||||
async def update_site_settings(
|
||||
settings: dict,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""批量更新网站设置"""
|
||||
for key, value in settings.items():
|
||||
existing = db.query(SiteSetting).filter(SiteSetting.key == key).first()
|
||||
if existing:
|
||||
existing.value = str(value) if value is not None else None
|
||||
existing.updated_at = datetime.now()
|
||||
else:
|
||||
new_setting = SiteSetting(
|
||||
id=str(uuid.uuid4()),
|
||||
key=key,
|
||||
value=str(value) if value is not None else None,
|
||||
)
|
||||
db.add(new_setting)
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_site_settings",
|
||||
target_type="site_settings",
|
||||
detail=f"更新网站设置: {list(settings.keys())}",
|
||||
)
|
||||
|
||||
return {"message": "网站设置已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 邮件配置
|
||||
# ============================================================
|
||||
|
||||
@router.get("/email")
|
||||
async def get_email_config(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取邮件配置"""
|
||||
config = db.query(EmailConfig).first()
|
||||
if not config:
|
||||
return {"code": 0, "data": {
|
||||
"smtp_host": "",
|
||||
"smtp_port": 587,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"from_address": "",
|
||||
"use_tls": True,
|
||||
}, "message": "ok"}
|
||||
|
||||
return {"code": 0, "data": {
|
||||
"id": config.id,
|
||||
"smtp_host": config.smtp_host,
|
||||
"smtp_port": config.smtp_port,
|
||||
"username": config.username,
|
||||
"password": config.password,
|
||||
"from_address": config.from_address,
|
||||
"use_tls": config.use_tls,
|
||||
}, "message": "ok"}
|
||||
|
||||
|
||||
@router.put("/email")
|
||||
async def update_email_config(
|
||||
req: EmailConfigUpdate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新邮件配置"""
|
||||
config = db.query(EmailConfig).first()
|
||||
if not config:
|
||||
# 必填字段校验:不允许空值
|
||||
if not req.smtp_host:
|
||||
raise HTTPException(status_code=400, detail="SMTP服务器地址不能为空")
|
||||
if not req.username:
|
||||
raise HTTPException(status_code=400, detail="SMTP用户名不能为空")
|
||||
if not req.password:
|
||||
raise HTTPException(status_code=400, detail="SMTP密码不能为空")
|
||||
if not req.from_address:
|
||||
raise HTTPException(status_code=400, detail="发件地址不能为空")
|
||||
config = EmailConfig(
|
||||
id=str(uuid.uuid4()),
|
||||
smtp_host=req.smtp_host or "",
|
||||
smtp_port=req.smtp_port or 587,
|
||||
username=req.username or "",
|
||||
password=req.password or "",
|
||||
from_address=req.from_address or "",
|
||||
use_tls=req.use_tls if req.use_tls is not None else True,
|
||||
)
|
||||
db.add(config)
|
||||
else:
|
||||
if req.smtp_host is not None:
|
||||
if not req.smtp_host.strip():
|
||||
raise HTTPException(status_code=400, detail="SMTP服务器地址不能为空")
|
||||
config.smtp_host = req.smtp_host
|
||||
if req.smtp_port is not None:
|
||||
config.smtp_port = req.smtp_port
|
||||
if req.username is not None:
|
||||
if not req.username.strip():
|
||||
raise HTTPException(status_code=400, detail="SMTP用户名不能为空")
|
||||
config.username = req.username
|
||||
if req.password is not None:
|
||||
if not req.password.strip():
|
||||
raise HTTPException(status_code=400, detail="SMTP密码不能为空")
|
||||
config.password = req.password
|
||||
if req.from_address is not None:
|
||||
if not req.from_address.strip():
|
||||
raise HTTPException(status_code=400, detail="发件地址不能为空")
|
||||
config.from_address = req.from_address
|
||||
if req.use_tls is not None:
|
||||
config.use_tls = req.use_tls
|
||||
config.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_email_config",
|
||||
target_type="email_config",
|
||||
detail="更新邮件配置",
|
||||
)
|
||||
|
||||
return {"code": 0, "data": None, "message": "邮件配置已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 短信配置
|
||||
# ============================================================
|
||||
|
||||
@router.get("/sms")
|
||||
async def get_sms_config(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取短信配置"""
|
||||
config = db.query(SmsConfig).first()
|
||||
if not config:
|
||||
return {"code": 0, "data": {
|
||||
"provider": "",
|
||||
"api_key": "",
|
||||
"api_secret": "",
|
||||
"sign_name": "",
|
||||
"template_code": "",
|
||||
}, "message": "ok"}
|
||||
|
||||
return {"code": 0, "data": {
|
||||
"id": config.id,
|
||||
"provider": config.provider,
|
||||
"api_key": config.api_key,
|
||||
"api_secret": config.api_secret,
|
||||
"sign_name": config.sign_name,
|
||||
"template_code": config.template_code,
|
||||
}, "message": "ok"}
|
||||
|
||||
|
||||
@router.put("/sms")
|
||||
async def update_sms_config(
|
||||
req: SmsConfigUpdate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新短信配置"""
|
||||
config = db.query(SmsConfig).first()
|
||||
if not config:
|
||||
config = SmsConfig(
|
||||
id=str(uuid.uuid4()),
|
||||
provider=req.provider or "",
|
||||
api_key=req.api_key or "",
|
||||
api_secret=req.api_secret or "",
|
||||
sign_name=req.sign_name,
|
||||
template_code=req.template_code,
|
||||
)
|
||||
db.add(config)
|
||||
else:
|
||||
if req.provider is not None:
|
||||
config.provider = req.provider
|
||||
if req.api_key is not None:
|
||||
config.api_key = req.api_key
|
||||
if req.api_secret is not None:
|
||||
config.api_secret = req.api_secret
|
||||
if req.sign_name is not None:
|
||||
config.sign_name = req.sign_name
|
||||
if req.template_code is not None:
|
||||
config.template_code = req.template_code
|
||||
config.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_sms_config",
|
||||
target_type="sms_config",
|
||||
detail="更新短信配置",
|
||||
)
|
||||
|
||||
return {"message": "短信配置已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 邮件测试
|
||||
# ============================================================
|
||||
|
||||
@router.post("/email/test")
|
||||
async def test_email_config(
|
||||
test_to: str = Query(default=None, description="自定义测试收件地址"),
|
||||
test_subject: str = Query(default=None, description="自定义测试邮件主题"),
|
||||
test_body: str = Query(default=None, description="自定义测试邮件正文"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""使用当前配置发送测试邮件;可传参自定义收件人、主题和正文"""
|
||||
config = db.query(EmailConfig).first()
|
||||
if not config or not config.smtp_host:
|
||||
raise HTTPException(status_code=400, detail="邮件配置未设置,请先保存邮件配置")
|
||||
if not config.username:
|
||||
raise HTTPException(status_code=400, detail="SMTP用户名未填写,请检查邮件配置")
|
||||
if not config.password:
|
||||
raise HTTPException(status_code=400, detail="SMTP密码未填写,请检查邮件配置")
|
||||
if not config.from_address:
|
||||
raise HTTPException(status_code=400, detail="发件地址未填写,请检查邮件配置")
|
||||
|
||||
# 优先级:传入参数 > 管理员邮箱 > 发件地址
|
||||
admin = db.query(User).filter(User.id == admin_user.id).first()
|
||||
recipient = test_to or (admin.email if admin and admin.email else None) or config.from_address
|
||||
if not recipient:
|
||||
raise HTTPException(status_code=400, detail="无法获取接收地址: 请填写收件邮箱或设置管理员邮箱")
|
||||
|
||||
subject = test_subject or "AIFlow 邮件配置测试"
|
||||
body = test_body or f"这是一封来自 AIFlow 平台的测试邮件。\n\n发送时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\nSMTP服务器: {config.smtp_host}"
|
||||
|
||||
logger.info(f"测试邮件: SMTP={config.smtp_host}:{config.smtp_port}, "
|
||||
f"from={config.from_address}, to={recipient}")
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = config.from_address
|
||||
msg["To"] = recipient
|
||||
msg["Subject"] = subject
|
||||
msg.attach(MIMEText(body, "plain"))
|
||||
|
||||
if config.use_tls:
|
||||
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10)
|
||||
server.starttls()
|
||||
else:
|
||||
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10)
|
||||
|
||||
server.login(config.username, config.password)
|
||||
server.sendmail(config.from_address, recipient, msg.as_string())
|
||||
server.quit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="test_email_config",
|
||||
target_type="email_config",
|
||||
detail=f"测试邮件发送成功 -> {recipient} via {config.smtp_host}",
|
||||
)
|
||||
|
||||
return {"code": 0, "data": {"message": f"测试邮件已发送到 {recipient}", "recipient": recipient}, "message": "ok"}
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
logger.error(f"SMTP认证失败: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"SMTP认证失败,请检查用户名和密码: {str(e)}")
|
||||
except smtplib.SMTPConnectError as e:
|
||||
logger.error(f"SMTP连接失败: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"无法连接到SMTP服务器 {config.smtp_host}:{config.smtp_port}: {str(e)}")
|
||||
except smtplib.SMTPException as e:
|
||||
logger.error(f"SMTP发送失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"邮件发送失败(SMTP): {str(e)}")
|
||||
except OSError as e:
|
||||
logger.error(f"邮件发送网络错误: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"网络连接错误: {str(e)}")
|
||||
except BaseException as e:
|
||||
logger.error(f"测试邮件发送失败(严重): {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=f"邮件发送失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/email/generate-content")
|
||||
async def generate_email_content(
|
||||
topic: str = Query(default="测试邮件"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""AI生成测试邮件内容:调用默认LLM生成邮件主题和正文"""
|
||||
import httpx as httpx_lib
|
||||
subjects = [
|
||||
"【测试】AIFlow邮件服务配置验证",
|
||||
"AIFlow平台 - 邮件投递测试",
|
||||
"测试邮件:您的SMTP服务已就绪",
|
||||
]
|
||||
bodies = [
|
||||
f"您好!\n\n这是一封来自 AIFlow 智能自动化平台的测试邮件,用于验证邮件服务配置是否正常。\n\n如果您收到此邮件,说明SMTP邮件服务已配置成功。\n\n此致\nAIFlow 平台团队",
|
||||
f"Hi,\n\nThis is a test email from AIFlow Platform.\n\nIf you received this, the SMTP service works fine.\n\nBest regards,\nAIFlow Team",
|
||||
f"【系统通知】\n邮件服务连通性测试通过。\n\n发件服务器配置正确,能够成功投递邮件。\n\n—— AIFlow",
|
||||
]
|
||||
|
||||
# 尝试使用默认LLM生成更好的内容
|
||||
try:
|
||||
default_model = db.query(AIModel).filter(
|
||||
AIModel.is_default == True, AIModel.is_enabled == True
|
||||
).first()
|
||||
if default_model and default_model.endpoint:
|
||||
async with httpx_lib.AsyncClient(timeout=15) as client:
|
||||
headers = {}
|
||||
if default_model.api_key:
|
||||
headers["Authorization"] = f"Bearer {default_model.api_key}"
|
||||
resp = await client.post(
|
||||
default_model.endpoint,
|
||||
json={
|
||||
"model": default_model.name,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个专业的技术文档撰写者。请根据主题生成一封测试邮件的主题和正文。返回JSON格式: {\"subject\": \"邮件主题\", \"body\": \"邮件正文\"}"},
|
||||
{"role": "user", "content": f"主题: {topic}"},
|
||||
],
|
||||
"max_tokens": 400,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
try:
|
||||
data = json.loads(content)
|
||||
if data.get("subject") and data.get("body"):
|
||||
return {"subject": data["subject"], "body": data["body"], "source": "ai"}
|
||||
except:
|
||||
# 尝试解析非JSON格式
|
||||
lines = content.strip().split("\n")
|
||||
if len(lines) >= 2:
|
||||
return {"subject": lines[0].replace("主题:", "").strip(), "body": "\n".join(lines[1:]).replace("正文:", "").strip(), "source": "ai_partial"}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import random
|
||||
idx = random.randint(0, len(subjects) - 1)
|
||||
return {"subject": subjects[idx], "body": bodies[idx], "source": "fallback"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 短信测试
|
||||
# ============================================================
|
||||
|
||||
def _aliyun_sign(secret: str, params: dict, method: str = "GET") -> str:
|
||||
"""阿里云签名机制 (HMAC-SHA1)"""
|
||||
sorted_keys = sorted(params.keys())
|
||||
qs = "&".join([
|
||||
f"{urllib.parse.quote(k, safe='')}={urllib.parse.quote(str(params[k]), safe='')}"
|
||||
for k in sorted_keys
|
||||
])
|
||||
string_to_sign = f"{method}&{urllib.parse.quote('/', safe='')}&{urllib.parse.quote(qs, safe='')}"
|
||||
key = f"{secret}&"
|
||||
h = hmac.new(key.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha1)
|
||||
return base64.b64encode(h.digest()).decode('utf-8')
|
||||
|
||||
|
||||
@router.post("/sms/test")
|
||||
async def test_sms_config(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""使用当前配置发送测试短信"""
|
||||
config = db.query(SmsConfig).first()
|
||||
if not config:
|
||||
raise HTTPException(status_code=400, detail="短信配置未设置,请先保存短信配置")
|
||||
|
||||
if not config.api_key or not config.api_secret:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="未配置阿里云短信 AccessKey,请填写 API Key (AccessKey ID) 和 API Secret (AccessKey Secret)"
|
||||
)
|
||||
|
||||
if not config.sign_name:
|
||||
raise HTTPException(status_code=400, detail="未配置短信签名")
|
||||
|
||||
# 获取接收测试短信的手机号(管理员手机号)
|
||||
admin = db.query(User).filter(User.id == admin_user.id).first()
|
||||
phone = admin.phone if admin and admin.phone else None
|
||||
if not phone:
|
||||
raise HTTPException(status_code=400, detail="管理员未设置手机号,无法发送测试短信")
|
||||
|
||||
logger.info(f"测试短信: provider={config.provider}, sign={config.sign_name}, phone={phone}")
|
||||
|
||||
# 只有阿里云短信支持测试
|
||||
if "阿里云" not in (config.provider or ""):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"当前短信服务商 '{config.provider}' 暂不支持在线测试,请直接在生产环境测试"
|
||||
)
|
||||
|
||||
try:
|
||||
# 阿里云短信参数
|
||||
params = {
|
||||
"AccessKeyId": config.api_key,
|
||||
"Action": "SendSms",
|
||||
"Format": "JSON",
|
||||
"PhoneNumbers": phone,
|
||||
"RegionId": "cn-hangzhou",
|
||||
"SignName": config.sign_name,
|
||||
"SignatureMethod": "HMAC-SHA1",
|
||||
"SignatureNonce": str(uuid.uuid4()),
|
||||
"SignatureVersion": "1.0",
|
||||
"TemplateCode": config.template_code or "SMS_00000001",
|
||||
"TemplateParam": '{"code":"123456"}',
|
||||
"Timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"Version": "2017-05-25",
|
||||
}
|
||||
params["Signature"] = _aliyun_sign(config.api_secret, params)
|
||||
|
||||
import requests as http_requests
|
||||
resp = http_requests.post(
|
||||
"https://dysmsapi.aliyuncs.com/",
|
||||
data=params,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=10,
|
||||
)
|
||||
result = resp.json()
|
||||
|
||||
logger.info(f"阿里云短信API响应: {result}")
|
||||
|
||||
if result.get("Code") == "OK":
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="test_sms_config",
|
||||
target_type="sms_config",
|
||||
detail=f"测试短信发送成功 -> {phone}",
|
||||
)
|
||||
return {"message": f"测试短信已发送到 {phone}", "phone": phone, "biz_id": result.get("BizId")}
|
||||
else:
|
||||
error_msg = result.get("Message", result.get("Code", "未知错误"))
|
||||
raise HTTPException(status_code=400, detail=f"阿里云短信发送失败: {error_msg}")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"测试短信发送失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"短信发送失败: {str(e)}")
|
||||
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
用户管理 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import User, RegisterSetting
|
||||
from ...auth import get_current_admin, hash_password, verify_password
|
||||
from ...middleware.log_service import LogService
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AdminChangePasswordRequest(BaseModel):
|
||||
old_password: str
|
||||
new_password: str = Field(min_length=6, max_length=100)
|
||||
|
||||
|
||||
class ResetUserPasswordRequest(BaseModel):
|
||||
new_password: str = Field(min_length=6, max_length=100)
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_users(
|
||||
status: str = Query(None, description="用户状态筛选: active/banned"),
|
||||
membership_level: int = Query(None, description="会员等级筛选"),
|
||||
search: str = Query(None, description="搜索用户名或邮箱"),
|
||||
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)
|
||||
|
||||
if status:
|
||||
query = query.filter(User.status == status)
|
||||
if membership_level is not None:
|
||||
query = query.filter(User.membership_level == membership_level)
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
User.username.ilike(f"%{search}%"),
|
||||
User.email.ilike(f"%{search}%"),
|
||||
)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
users = query.order_by(User.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": u.id,
|
||||
"username": u.username,
|
||||
"email": u.email,
|
||||
"phone": u.phone,
|
||||
"role": u.role,
|
||||
"status": u.status,
|
||||
"real_name_status": u.real_name_status,
|
||||
"real_name": u.real_name,
|
||||
"membership_level": u.membership_level,
|
||||
"membership_expire_at": u.membership_expire_at.isoformat() if u.membership_expire_at else None,
|
||||
"auto_renew": u.auto_renew,
|
||||
"created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
"updated_at": u.updated_at.isoformat() if u.updated_at else None,
|
||||
}
|
||||
for u in users
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 静态路由 - 必须放在 /{id} 动态路由之前
|
||||
# ============================================================
|
||||
|
||||
@router.get("/register-settings")
|
||||
async def get_register_settings(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取注册设置"""
|
||||
setting = db.query(RegisterSetting).first()
|
||||
if not setting:
|
||||
return {
|
||||
"phone_required": True,
|
||||
"phone_sms_required": True,
|
||||
"email_required": True,
|
||||
"email_verify_required": True,
|
||||
"real_name_required": False,
|
||||
"aliyun_access_key": "",
|
||||
"aliyun_endpoint": "https://dysmsapi.aliyuncs.com",
|
||||
"sms_sign_name": "",
|
||||
"sms_template_code": "",
|
||||
}
|
||||
|
||||
return {
|
||||
"id": setting.id,
|
||||
"phone_required": setting.phone_required,
|
||||
"phone_sms_required": setting.phone_sms_required,
|
||||
"email_required": setting.email_required,
|
||||
"email_verify_required": setting.email_verify_required,
|
||||
"real_name_required": setting.real_name_required,
|
||||
"aliyun_access_key": setting.aliyun_access_key,
|
||||
"aliyun_endpoint": setting.aliyun_endpoint,
|
||||
"sms_sign_name": getattr(setting, 'sms_sign_name', ''),
|
||||
"sms_template_code": getattr(setting, 'sms_template_code', ''),
|
||||
}
|
||||
|
||||
|
||||
@router.put("/register-settings")
|
||||
async def update_register_settings(
|
||||
phone_required: bool = None,
|
||||
phone_sms_required: bool = None,
|
||||
email_required: bool = None,
|
||||
email_verify_required: bool = None,
|
||||
real_name_required: bool = None,
|
||||
aliyun_access_key: str = None,
|
||||
aliyun_secret_key: str = None,
|
||||
aliyun_endpoint: str = None,
|
||||
sms_sign_name: str = None,
|
||||
sms_template_code: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新注册设置"""
|
||||
setting = db.query(RegisterSetting).first()
|
||||
if not setting:
|
||||
setting = RegisterSetting(
|
||||
id=str(uuid.uuid4()),
|
||||
phone_required=phone_required if phone_required is not None else True,
|
||||
phone_sms_required=phone_sms_required if phone_sms_required is not None else True,
|
||||
email_required=email_required if email_required is not None else True,
|
||||
email_verify_required=email_verify_required if email_verify_required is not None else True,
|
||||
real_name_required=real_name_required or False,
|
||||
aliyun_access_key=aliyun_access_key,
|
||||
aliyun_secret_key=aliyun_secret_key,
|
||||
aliyun_endpoint=aliyun_endpoint,
|
||||
sms_sign_name=sms_sign_name,
|
||||
sms_template_code=sms_template_code,
|
||||
)
|
||||
db.add(setting)
|
||||
else:
|
||||
if phone_required is not None:
|
||||
setting.phone_required = phone_required
|
||||
if phone_sms_required is not None:
|
||||
setting.phone_sms_required = phone_sms_required
|
||||
if email_required is not None:
|
||||
setting.email_required = email_required
|
||||
if email_verify_required is not None:
|
||||
setting.email_verify_required = email_verify_required
|
||||
if real_name_required is not None:
|
||||
setting.real_name_required = real_name_required
|
||||
if aliyun_access_key is not None:
|
||||
setting.aliyun_access_key = aliyun_access_key
|
||||
if aliyun_secret_key is not None:
|
||||
setting.aliyun_secret_key = aliyun_secret_key
|
||||
if aliyun_endpoint is not None:
|
||||
setting.aliyun_endpoint = aliyun_endpoint
|
||||
if sms_sign_name is not None:
|
||||
setting.sms_sign_name = sms_sign_name
|
||||
if sms_template_code is not None:
|
||||
setting.sms_template_code = sms_template_code
|
||||
setting.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_register_settings",
|
||||
target_type="register_setting",
|
||||
detail="更新注册设置",
|
||||
)
|
||||
|
||||
return {"message": "注册设置已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 动态路由
|
||||
# ============================================================
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_user_detail(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取用户详情"""
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"phone": user.phone,
|
||||
"avatar": user.avatar,
|
||||
"role": user.role,
|
||||
"status": user.status,
|
||||
"real_name_status": user.real_name_status,
|
||||
"real_name": user.real_name,
|
||||
"id_card": user.id_card,
|
||||
"membership_plan_id": user.membership_plan_id,
|
||||
"membership_level": user.membership_level,
|
||||
"membership_expire_at": user.membership_expire_at.isoformat() if user.membership_expire_at else None,
|
||||
"auto_renew": user.auto_renew,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None,
|
||||
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{id}/ban")
|
||||
async def ban_user(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""封禁用户"""
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if user.role == "admin":
|
||||
raise HTTPException(status_code=400, detail="不能封禁管理员账号")
|
||||
|
||||
user.status = "banned"
|
||||
user.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="ban_user",
|
||||
target_type="user",
|
||||
target_id=id,
|
||||
detail=f"封禁用户 {user.username}",
|
||||
)
|
||||
|
||||
return {"message": "用户已封禁"}
|
||||
|
||||
|
||||
@router.put("/{id}/unban")
|
||||
async def unban_user(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""解封用户"""
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
user.status = "active"
|
||||
user.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="unban_user",
|
||||
target_type="user",
|
||||
target_id=id,
|
||||
detail=f"解封用户 {user.username}",
|
||||
)
|
||||
|
||||
return {"message": "用户已解封"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 密码管理
|
||||
# ============================================================
|
||||
|
||||
@router.put("/change-password")
|
||||
def admin_change_password(
|
||||
req: AdminChangePasswordRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""管理员修改自己的密码"""
|
||||
db_user = db.query(User).filter(User.id == admin_user.id).first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
if not verify_password(req.old_password, db_user.password_hash):
|
||||
raise HTTPException(status_code=400, detail="原密码错误")
|
||||
|
||||
db_user.password_hash = hash_password(req.new_password)
|
||||
db_user.updated_at = datetime.now()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=admin_user.id, username=admin_user.username, action="admin_change_password",
|
||||
target="user", target_id=admin_user.id,
|
||||
detail="管理员修改了自己的密码",
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "密码修改成功"}
|
||||
|
||||
|
||||
@router.put("/{id}/reset-password")
|
||||
def admin_reset_user_password(
|
||||
id: str,
|
||||
req: ResetUserPasswordRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""管理员重置用户密码"""
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
user.password_hash = hash_password(req.new_password)
|
||||
user.updated_at = datetime.now()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=admin_user.id, username=admin_user.username, action="reset_user_password",
|
||||
target="user", target_id=id,
|
||||
detail=f"管理员重置了用户 {user.username} 的密码",
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "密码已重置"}
|
||||
Reference in new issue
Block a user