初始提交: Qihu-AIFlow v2.0.1 完整项目

- 前端 React+TypeScript (源码+构建产物)
- 后端 Python FastAPI
- 数据库迁移 001-015
- Docker部署配置
- 微信ClawBot API频道绑定
- 项目文档 README.md
This commit is contained in:
qyyfw committed 2026-07-02 14:40:46 +08:00
commit 78ff261b5a
199 files changed
+69126

No files matched your search

+1
View File
@@ -0,0 +1 @@
# Admin API routes package
+311
View File
@@ -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}
+99
View File
@@ -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
]
+810
View File
@@ -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,
},
}
+602
View File
@@ -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"
},
)
+453
View File
@@ -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}
+508
View File
@@ -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)}")
+334
View File
@@ -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": "密码已重置"}