初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
This commit is contained in:
commit
78ff261b5a
199 files changed
+69126
No files matched your search
@@ -0,0 +1,10 @@
|
||||
from . import auth
|
||||
from . import chat
|
||||
from . import channels
|
||||
from . import knowledge
|
||||
from . import market
|
||||
from . import token_usage
|
||||
from . import payment
|
||||
from . import profile
|
||||
from . import notifications
|
||||
from . import assets
|
||||
@@ -0,0 +1 @@
|
||||
# Admin API routes package
|
||||
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
广告位管理 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import AdBanner, AIModel
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import os
|
||||
import json
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 广告图片存储目录
|
||||
ADS_IMAGE_DIR = "/data/share/project/data/ads"
|
||||
os.makedirs(ADS_IMAGE_DIR, exist_ok=True)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_ads(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取广告轮播图列表"""
|
||||
ads = db.query(AdBanner).order_by(AdBanner.sort_order.asc()).all()
|
||||
return [
|
||||
{
|
||||
"id": a.id,
|
||||
"title": a.title,
|
||||
"image_url": a.image_url,
|
||||
"link_app_id": a.link_app_id,
|
||||
"link_app_name": a.link_app_name,
|
||||
"sort_order": a.sort_order,
|
||||
"click_count": a.click_count,
|
||||
"status": a.status,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
||||
}
|
||||
for a in ads
|
||||
]
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_ad(
|
||||
title: str,
|
||||
image_url: str = None,
|
||||
link_app_id: str = None,
|
||||
link_app_name: str = None,
|
||||
sort_order: int = 0,
|
||||
status: str = "active",
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建广告"""
|
||||
ad = AdBanner(
|
||||
id=str(uuid.uuid4()),
|
||||
title=title,
|
||||
image_url=image_url,
|
||||
link_app_id=link_app_id,
|
||||
link_app_name=link_app_name,
|
||||
sort_order=sort_order,
|
||||
status=status,
|
||||
)
|
||||
db.add(ad)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_ad",
|
||||
target_type="ad_banner",
|
||||
target_id=ad.id,
|
||||
detail=f"创建广告 {title}",
|
||||
)
|
||||
|
||||
return {"message": "广告已创建", "id": ad.id}
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_ad(
|
||||
id: str,
|
||||
title: str = None,
|
||||
image_url: str = None,
|
||||
link_app_id: str = None,
|
||||
link_app_name: str = None,
|
||||
sort_order: int = None,
|
||||
status: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新广告"""
|
||||
ad = db.query(AdBanner).filter(AdBanner.id == id).first()
|
||||
if not ad:
|
||||
raise HTTPException(status_code=404, detail="广告不存在")
|
||||
|
||||
if title is not None:
|
||||
ad.title = title
|
||||
if image_url is not None:
|
||||
ad.image_url = image_url
|
||||
if link_app_id is not None:
|
||||
ad.link_app_id = link_app_id
|
||||
if link_app_name is not None:
|
||||
ad.link_app_name = link_app_name
|
||||
if sort_order is not None:
|
||||
ad.sort_order = sort_order
|
||||
if status is not None:
|
||||
ad.status = status
|
||||
ad.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_ad",
|
||||
target_type="ad_banner",
|
||||
target_id=id,
|
||||
detail=f"更新广告 {ad.title}",
|
||||
)
|
||||
|
||||
return {"message": "广告已更新"}
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_ad(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除广告"""
|
||||
ad = db.query(AdBanner).filter(AdBanner.id == id).first()
|
||||
if not ad:
|
||||
raise HTTPException(status_code=404, detail="广告不存在")
|
||||
|
||||
db.delete(ad)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_ad",
|
||||
target_type="ad_banner",
|
||||
target_id=id,
|
||||
detail=f"删除广告 {ad.title}",
|
||||
)
|
||||
|
||||
return {"message": "广告已删除"}
|
||||
|
||||
|
||||
@router.put("/{id}/toggle")
|
||||
async def toggle_ad(
|
||||
id: str,
|
||||
status: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""切换广告状态"""
|
||||
ad = db.query(AdBanner).filter(AdBanner.id == id).first()
|
||||
if not ad:
|
||||
raise HTTPException(status_code=404, detail="广告不存在")
|
||||
|
||||
ad.status = status
|
||||
ad.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="toggle_ad",
|
||||
target_type="ad_banner",
|
||||
target_id=id,
|
||||
detail=f"切换广告 {ad.title} 状态为 {status}",
|
||||
)
|
||||
|
||||
return {"message": f"广告已{status}"}
|
||||
|
||||
|
||||
@router.post("/upload-image")
|
||||
async def upload_ad_image(
|
||||
file: UploadFile = File(...),
|
||||
admin_user=Depends(get_current_admin),
|
||||
):
|
||||
"""上传广告图片,保存到 /data/share/project/data/ads/ 目录"""
|
||||
# 校验文件类型
|
||||
allowed_types = {"image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"}
|
||||
if file.content_type not in allowed_types:
|
||||
raise HTTPException(status_code=400, detail="仅支持 PNG/JPG/GIF/WebP/SVG 格式的图片")
|
||||
|
||||
# 生成唯一文件名
|
||||
ext = os.path.splitext(file.filename or "image.png")[1] or ".png"
|
||||
if ext.lower() not in (".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"):
|
||||
ext = ".png"
|
||||
filename = f"{uuid.uuid4()}{ext}"
|
||||
filepath = os.path.join(ADS_IMAGE_DIR, filename)
|
||||
|
||||
# 保存文件
|
||||
content = await file.read()
|
||||
if len(content) > 10 * 1024 * 1024: # 10MB 限制
|
||||
raise HTTPException(status_code=400, detail="图片大小不能超过10MB")
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
# 返回可访问的URL
|
||||
image_url = f"/app/api/admin/ads/images/{filename}"
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="upload_ad_image",
|
||||
target_type="ad_banner",
|
||||
detail=f"上传广告图片 {filename}",
|
||||
)
|
||||
|
||||
return {"message": "图片上传成功", "image_url": image_url, "filename": filename}
|
||||
|
||||
|
||||
@router.get("/images/{filename}")
|
||||
async def get_ad_image(filename: str):
|
||||
"""获取广告图片"""
|
||||
filepath = os.path.join(ADS_IMAGE_DIR, filename)
|
||||
if not os.path.exists(filepath):
|
||||
raise HTTPException(status_code=404, detail="图片不存在")
|
||||
return FileResponse(filepath)
|
||||
|
||||
|
||||
@router.post("/generate-image")
|
||||
async def generate_ad_image(
|
||||
prompt: str = Query(default=""),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""AI生成广告图:调用LLM生成prompt,然后生成图片(这里生成渐变色占位图)"""
|
||||
# 如果没有传入prompt,使用默认prompt
|
||||
final_prompt = prompt.strip() if prompt.strip() else "A modern tech AI platform banner advertisement, sleek interface, gradient blue purple theme, high quality"
|
||||
|
||||
# 尝试使用默认LLM模型生成更优的prompt
|
||||
enhanced_prompt = final_prompt
|
||||
try:
|
||||
default_model = db.query(AIModel).filter(
|
||||
AIModel.is_default == True, AIModel.is_enabled == True
|
||||
).first()
|
||||
if default_model and default_model.endpoint:
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
headers = {}
|
||||
if default_model.api_key:
|
||||
headers["Authorization"] = f"Bearer {default_model.api_key}"
|
||||
resp = await client.post(
|
||||
default_model.endpoint,
|
||||
json={
|
||||
"model": default_model.name,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个广告创意专家。请根据用户输入的主题,生成一个简短的中文广告图像prompt描述(不超过100字),用于AI图像生成。只需要输出prompt文本本身,不要其他内容。"},
|
||||
{"role": "user", "content": f"主题: {final_prompt}"},
|
||||
],
|
||||
"max_tokens": 200,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
if content:
|
||||
enhanced_prompt = content.strip()[:200]
|
||||
except Exception as e:
|
||||
# LLM增强失败,使用原始prompt
|
||||
pass
|
||||
|
||||
# 生成一个唯一的图片文件名(使用prompt的hash作为标识)
|
||||
import hashlib
|
||||
prompt_hash = hashlib.md5(enhanced_prompt.encode()).hexdigest()[:12]
|
||||
filename = f"ai_gen_{prompt_hash}.svg"
|
||||
filepath = os.path.join(ADS_IMAGE_DIR, filename)
|
||||
|
||||
# 生成SVG广告图
|
||||
svg_content = f'''<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="400" viewBox="0 0 1200 400">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1"/>
|
||||
<stop offset="50%" style="stop-color:#764ba2;stop-opacity:1"/>
|
||||
<stop offset="100%" style="stop-color:#5B5FED;stop-opacity:1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="shape" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:rgba(255,255,255,0.15);stop-opacity:1"/>
|
||||
<stop offset="100%" style="stop-color:rgba(255,255,255,0.05);stop-opacity:1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1200" height="400" fill="url(#bg)"/>
|
||||
<circle cx="1000" cy="100" r="200" fill="url(#shape)" opacity="0.3"/>
|
||||
<circle cx="200" cy="300" r="150" fill="url(#shape)" opacity="0.2"/>
|
||||
<rect x="600" y="150" width="300" height="200" rx="20" fill="url(#shape)" opacity="0.15" transform="rotate(-15 750 250)"/>
|
||||
<text x="600" y="180" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="bold" fill="#ffffff" filter="drop-shadow(0 2px 4px rgba(0,0,0,0.2))">{enhanced_prompt[:40]}</text>
|
||||
<text x="600" y="240" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" fill="rgba(255,255,255,0.8)">Powered by AIFlow</text>
|
||||
</svg>'''
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(svg_content)
|
||||
|
||||
image_url = f"/app/api/admin/ads/images/{filename}"
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="generate_ad_image",
|
||||
target_type="ad_banner",
|
||||
detail=f"AI生成广告图 {filename}, prompt: {enhanced_prompt[:50]}",
|
||||
)
|
||||
|
||||
return {"message": "AI广告图已生成", "image_url": image_url, "prompt": enhanced_prompt}
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
管理员仪表盘 API
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import (
|
||||
User, AIModel, Order, TokenUsageLog, OperationLog, AuditLog
|
||||
)
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
from datetime import datetime, date
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_dashboard_stats(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取管理员仪表盘统计数据"""
|
||||
today = date.today()
|
||||
|
||||
active_users = db.query(func.count(User.id)).filter(
|
||||
User.status == "active"
|
||||
).scalar() or 0
|
||||
|
||||
enabled_models = db.query(func.count(AIModel.id)).filter(
|
||||
AIModel.is_enabled == True
|
||||
).scalar() or 0
|
||||
|
||||
today_orders = db.query(func.count(Order.id)).filter(
|
||||
func.date(Order.created_at) == today
|
||||
).scalar() or 0
|
||||
|
||||
today_tokens = db.query(func.sum(TokenUsageLog.total_tokens)).filter(
|
||||
func.date(TokenUsageLog.created_at) == today
|
||||
).scalar() or 0
|
||||
|
||||
total_tokens = db.query(func.sum(TokenUsageLog.total_tokens)).scalar() or 0
|
||||
|
||||
return {
|
||||
"active_users": active_users,
|
||||
"enabled_models": enabled_models,
|
||||
"today_orders": today_orders,
|
||||
"today_tokens": int(today_tokens),
|
||||
"total_tokens": int(total_tokens),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/recent-logs")
|
||||
async def get_recent_logs(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取最近10条操作日志"""
|
||||
logs = db.query(OperationLog).order_by(
|
||||
OperationLog.created_at.desc()
|
||||
).limit(10).all()
|
||||
return [
|
||||
{
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"username": log.username,
|
||||
"action": log.action,
|
||||
"target": log.target,
|
||||
"target_id": log.target_id,
|
||||
"detail": log.detail,
|
||||
"ip": log.ip,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
]
|
||||
|
||||
|
||||
@router.get("/recent-audit")
|
||||
async def get_recent_audit(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取最近10条审计日志"""
|
||||
logs = db.query(AuditLog).order_by(
|
||||
AuditLog.created_at.desc()
|
||||
).limit(10).all()
|
||||
return [
|
||||
{
|
||||
"id": log.id,
|
||||
"admin_id": log.admin_id,
|
||||
"admin_name": log.admin_name,
|
||||
"action": log.action,
|
||||
"target_type": log.target_type,
|
||||
"target_id": log.target_id,
|
||||
"detail": log.detail,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
]
|
||||
@@ -0,0 +1,810 @@
|
||||
"""
|
||||
全平台日志与风控 API(管理员)
|
||||
关键约束: 操作日志、Token日志、审计日志三个实体绝对不能写DELETE操作
|
||||
|
||||
增强功能:
|
||||
- 审计日志: 按时间范围、用户、操作类型多条件过滤
|
||||
- 操作日志: 关键词搜索和导出
|
||||
- API调用统计: 端点调用次数、平均响应时间、错误率
|
||||
- 慢请求记录: 响应时间超过阈值自动标记
|
||||
- 统计概览API: /app/api/admin/logs/stats
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, text
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import (
|
||||
OperationLog, TokenUsageLog, Order, AuditLog, RiskRule,
|
||||
ApiCallStat, SlowRequestLog, User
|
||||
)
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import csv
|
||||
import io
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 日志概览统计 API
|
||||
# ============================================================
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_log_stats(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取日志概览数据(今日调用量、错误数、活跃用户等)"""
|
||||
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
# 今日API调用总量
|
||||
today_calls = db.query(func.sum(ApiCallStat.call_count)).filter(
|
||||
ApiCallStat.updated_at >= today_start
|
||||
).scalar() or 0
|
||||
|
||||
# 今日错误数
|
||||
today_errors = db.query(func.sum(ApiCallStat.error_count)).filter(
|
||||
ApiCallStat.updated_at >= today_start
|
||||
).scalar() or 0
|
||||
|
||||
# 今日操作日志数
|
||||
today_ops = db.query(OperationLog).filter(
|
||||
OperationLog.created_at >= today_start
|
||||
).count()
|
||||
|
||||
# 今日审计日志数
|
||||
today_audits = db.query(AuditLog).filter(
|
||||
AuditLog.created_at >= today_start
|
||||
).count()
|
||||
|
||||
# 今日活跃用户数(有操作记录的用户)
|
||||
today_active_users = db.query(func.count(func.distinct(OperationLog.user_id))).filter(
|
||||
OperationLog.created_at >= today_start
|
||||
).scalar() or 0
|
||||
|
||||
# 今日Token消耗总量
|
||||
today_tokens = db.query(func.sum(TokenUsageLog.total_tokens)).filter(
|
||||
TokenUsageLog.created_at >= today_start
|
||||
).scalar() or 0
|
||||
|
||||
# 慢请求数(今日)
|
||||
today_slow = db.query(SlowRequestLog).filter(
|
||||
SlowRequestLog.created_at >= today_start
|
||||
).count()
|
||||
|
||||
# 总用户数
|
||||
total_users = db.query(User).count()
|
||||
|
||||
# 错误率
|
||||
error_rate = round(today_errors / max(today_calls, 1) * 100, 2)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"today": {
|
||||
"api_calls": int(today_calls),
|
||||
"errors": int(today_errors),
|
||||
"error_rate": error_rate,
|
||||
"operation_logs": today_ops,
|
||||
"audit_logs": today_audits,
|
||||
"active_users": today_active_users,
|
||||
"total_tokens": int(today_tokens),
|
||||
"slow_requests": today_slow,
|
||||
},
|
||||
"total": {
|
||||
"users": total_users,
|
||||
},
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# API调用统计
|
||||
# ============================================================
|
||||
|
||||
@router.get("/api-stats")
|
||||
async def get_api_call_stats(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
sort_by: str = Query("call_count", description="排序字段: call_count / avg_time / error_rate"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取API调用统计:各端点调用次数、平均响应时间、错误率"""
|
||||
query = db.query(ApiCallStat)
|
||||
|
||||
# 排序
|
||||
if sort_by == "avg_time":
|
||||
query = query.order_by(ApiCallStat.total_response_time / func.greatest(ApiCallStat.call_count, 1))
|
||||
elif sort_by == "error_rate":
|
||||
query = query.order_by(
|
||||
(ApiCallStat.error_count * 1.0 / func.greatest(ApiCallStat.call_count, 1)).desc()
|
||||
)
|
||||
else:
|
||||
query = query.order_by(ApiCallStat.call_count.desc())
|
||||
|
||||
total = query.count()
|
||||
stats = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": s.id,
|
||||
"endpoint": s.endpoint,
|
||||
"method": s.method,
|
||||
"call_count": s.call_count,
|
||||
"avg_response_time": round(s.total_response_time / max(s.call_count, 1), 4),
|
||||
"error_count": s.error_count,
|
||||
"error_rate": round(s.error_count / max(s.call_count, 1) * 100, 2),
|
||||
"last_called_at": s.last_called_at.isoformat() if s.last_called_at else None,
|
||||
}
|
||||
for s in stats
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 慢请求记录
|
||||
# ============================================================
|
||||
|
||||
@router.get("/slow-requests")
|
||||
async def get_slow_requests(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
min_response_time: float = Query(None, description="最小响应时间过滤"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取慢请求记录列表"""
|
||||
query = db.query(SlowRequestLog)
|
||||
|
||||
if min_response_time is not None:
|
||||
query = query.filter(SlowRequestLog.response_time >= min_response_time)
|
||||
|
||||
total = query.count()
|
||||
logs = query.order_by(SlowRequestLog.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": log.id,
|
||||
"request_id": log.request_id,
|
||||
"method": log.method,
|
||||
"endpoint": log.endpoint,
|
||||
"status_code": log.status_code,
|
||||
"response_time": log.response_time,
|
||||
"threshold": log.threshold,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 操作日志(增强: 关键词搜索 + 导出)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/operations")
|
||||
async def get_operation_logs(
|
||||
user_id: str = Query(None, description="按用户ID筛选"),
|
||||
username: str = Query(None, description="按用户名筛选"),
|
||||
action: str = Query(None, description="按操作类型筛选"),
|
||||
keyword: str = Query(None, description="关键词搜索(匹配目标、详情)"),
|
||||
start_date: str = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||
end_date: str = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取操作日志列表(增强:支持关键词搜索和日期范围过滤)"""
|
||||
query = db.query(OperationLog)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(OperationLog.user_id == user_id)
|
||||
if username:
|
||||
query = query.filter(OperationLog.username.like(f"%{username}%"))
|
||||
if action:
|
||||
query = query.filter(OperationLog.action == action)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
(OperationLog.target.like(f"%{keyword}%")) |
|
||||
(OperationLog.detail.like(f"%{keyword}%"))
|
||||
)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(OperationLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(OperationLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
logs = query.order_by(OperationLog.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"username": log.username,
|
||||
"action": log.action,
|
||||
"target": log.target,
|
||||
"target_id": log.target_id,
|
||||
"detail": log.detail,
|
||||
"ip": log.ip,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Token调用日志
|
||||
# ============================================================
|
||||
|
||||
@router.get("/token-logs")
|
||||
async def get_token_logs(
|
||||
user_id: str = Query(None, description="按用户ID筛选"),
|
||||
model_id: str = Query(None, description="按模型ID筛选"),
|
||||
keyword: str = Query(None, description="关键词搜索(匹配模型名称/应用名称)"),
|
||||
start_date: str = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||
end_date: str = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取Token调用日志列表"""
|
||||
query = db.query(TokenUsageLog)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(TokenUsageLog.user_id == user_id)
|
||||
if model_id:
|
||||
query = query.filter(TokenUsageLog.model_id == model_id)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
(TokenUsageLog.model_name.like(f"%{keyword}%")) |
|
||||
(TokenUsageLog.app_name.like(f"%{keyword}%"))
|
||||
)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(TokenUsageLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(TokenUsageLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
logs = query.order_by(TokenUsageLog.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"model_id": log.model_id,
|
||||
"model_name": log.model_name,
|
||||
"app_id": log.app_id,
|
||||
"app_name": log.app_name,
|
||||
"conversation_id": log.conversation_id,
|
||||
"prompt_tokens": log.prompt_tokens,
|
||||
"completion_tokens": log.completion_tokens,
|
||||
"total_tokens": log.total_tokens,
|
||||
"cost": float(log.cost) if log.cost else 0,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 付费订单日志
|
||||
# ============================================================
|
||||
|
||||
@router.get("/payment-logs")
|
||||
async def get_payment_logs(
|
||||
user_id: str = Query(None, description="按用户ID筛选"),
|
||||
status: str = Query(None, description="按状态筛选"),
|
||||
keyword: str = Query(None, description="关键词搜索(匹配项目名称)"),
|
||||
start_date: str = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||
end_date: str = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取付费订单日志列表"""
|
||||
query = db.query(Order)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(Order.user_id == user_id)
|
||||
if status:
|
||||
query = query.filter(Order.status == status)
|
||||
if keyword:
|
||||
query = query.filter(Order.item_name.like(f"%{keyword}%"))
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(Order.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(Order.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
orders = query.order_by(Order.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": o.id,
|
||||
"user_id": o.user_id,
|
||||
"order_type": o.order_type,
|
||||
"item_id": o.item_id,
|
||||
"item_name": o.item_name,
|
||||
"amount": float(o.amount) if o.amount else 0,
|
||||
"coupon_id": o.coupon_id,
|
||||
"discount_amount": float(o.discount_amount) if o.discount_amount else 0,
|
||||
"payment_method": o.payment_method,
|
||||
"payment_id": o.payment_id,
|
||||
"status": o.status,
|
||||
"created_at": o.created_at.isoformat() if o.created_at else None,
|
||||
"updated_at": o.updated_at.isoformat() if o.updated_at else None,
|
||||
}
|
||||
for o in orders
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 审计日志(增强:多条件过滤)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/audit-logs")
|
||||
async def get_audit_logs(
|
||||
admin_id: str = Query(None, description="按管理员ID筛选"),
|
||||
admin_name: str = Query(None, description="按管理员名称筛选"),
|
||||
action: str = Query(None, description="按操作类型筛选"),
|
||||
target_type: str = Query(None, description="按目标类型筛选"),
|
||||
start_date: str = Query(None, description="开始日期 (YYYY-MM-DD)"),
|
||||
end_date: str = Query(None, description="结束日期 (YYYY-MM-DD)"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取审计日志列表(增强:支持按管理员、操作类型、目标类型、日期范围过滤)"""
|
||||
query = db.query(AuditLog)
|
||||
|
||||
if admin_id:
|
||||
query = query.filter(AuditLog.admin_id == admin_id)
|
||||
if admin_name:
|
||||
query = query.filter(AuditLog.admin_name.like(f"%{admin_name}%"))
|
||||
if action:
|
||||
query = query.filter(AuditLog.action == action)
|
||||
if target_type:
|
||||
query = query.filter(AuditLog.target_type == target_type)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(AuditLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(AuditLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
logs = query.order_by(AuditLog.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": log.id,
|
||||
"admin_id": log.admin_id,
|
||||
"admin_name": log.admin_name,
|
||||
"action": log.action,
|
||||
"target_type": log.target_type,
|
||||
"target_id": log.target_id,
|
||||
"detail": log.detail,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 日志导出
|
||||
# ============================================================
|
||||
|
||||
@router.get("/export/operations")
|
||||
async def export_operations(
|
||||
user_id: str = Query(None),
|
||||
action: str = Query(None),
|
||||
keyword: str = Query(None),
|
||||
start_date: str = Query(None),
|
||||
end_date: str = Query(None),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出操作日志(CSV)- 增强:支持与列表相同的过滤条件"""
|
||||
query = db.query(OperationLog)
|
||||
if user_id:
|
||||
query = query.filter(OperationLog.user_id == user_id)
|
||||
if action:
|
||||
query = query.filter(OperationLog.action == action)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
(OperationLog.target.like(f"%{keyword}%")) |
|
||||
(OperationLog.detail.like(f"%{keyword}%"))
|
||||
)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(OperationLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(OperationLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
logs = query.order_by(OperationLog.created_at.desc()).all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["ID", "用户ID", "用户名", "操作", "目标", "目标ID", "详情", "IP", "时间"])
|
||||
|
||||
for log in logs:
|
||||
writer.writerow([
|
||||
log.id, log.user_id, log.username, log.action,
|
||||
log.target, log.target_id, log.detail, log.ip,
|
||||
log.created_at.isoformat() if log.created_at else "",
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=operations_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/export/token-logs")
|
||||
async def export_token_logs(
|
||||
user_id: str = Query(None),
|
||||
keyword: str = Query(None),
|
||||
start_date: str = Query(None),
|
||||
end_date: str = Query(None),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出Token日志(CSV)"""
|
||||
query = db.query(TokenUsageLog)
|
||||
if user_id:
|
||||
query = query.filter(TokenUsageLog.user_id == user_id)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
(TokenUsageLog.model_name.like(f"%{keyword}%")) |
|
||||
(TokenUsageLog.app_name.like(f"%{keyword}%"))
|
||||
)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(TokenUsageLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(TokenUsageLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
logs = query.order_by(TokenUsageLog.created_at.desc()).all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow([
|
||||
"ID", "用户ID", "模型ID", "模型名称", "应用ID", "应用名称",
|
||||
"会话ID", "Prompt Tokens", "Completion Tokens", "Total Tokens", "费用", "时间"
|
||||
])
|
||||
|
||||
for log in logs:
|
||||
writer.writerow([
|
||||
log.id, log.user_id, log.model_id, log.model_name,
|
||||
log.app_id, log.app_name, log.conversation_id,
|
||||
log.prompt_tokens, log.completion_tokens, log.total_tokens,
|
||||
float(log.cost) if log.cost else 0,
|
||||
log.created_at.isoformat() if log.created_at else "",
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=token_logs_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/export/audit-logs")
|
||||
async def export_audit_logs(
|
||||
admin_id: str = Query(None),
|
||||
action: str = Query(None),
|
||||
target_type: str = Query(None),
|
||||
start_date: str = Query(None),
|
||||
end_date: str = Query(None),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出审计日志(CSV)"""
|
||||
query = db.query(AuditLog)
|
||||
if admin_id:
|
||||
query = query.filter(AuditLog.admin_id == admin_id)
|
||||
if action:
|
||||
query = query.filter(AuditLog.action == action)
|
||||
if target_type:
|
||||
query = query.filter(AuditLog.target_type == target_type)
|
||||
if start_date:
|
||||
try:
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(AuditLog.created_at >= sd)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(AuditLog.created_at < ed)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
logs = query.order_by(AuditLog.created_at.desc()).all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["ID", "管理员ID", "管理员名称", "操作", "目标类型", "目标ID", "详情", "时间"])
|
||||
|
||||
for log in logs:
|
||||
writer.writerow([
|
||||
log.id, log.admin_id, log.admin_name, log.action,
|
||||
log.target_type, log.target_id, log.detail,
|
||||
log.created_at.isoformat() if log.created_at else "",
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=audit_logs_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 操作类型枚举
|
||||
# ============================================================
|
||||
|
||||
@router.get("/action-types")
|
||||
async def get_action_types(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取所有操作类型枚举值(用于筛选下拉)"""
|
||||
ops = db.query(OperationLog.action).distinct().all()
|
||||
audits = db.query(AuditLog.action).distinct().all()
|
||||
audit_targets = db.query(AuditLog.target_type).distinct().all()
|
||||
|
||||
return {
|
||||
"operation_actions": [r[0] for r in ops if r[0]],
|
||||
"audit_actions": [r[0] for r in audits if r[0]],
|
||||
"audit_target_types": [r[0] for r in audit_targets if r[0]],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 风控规则管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/risk-rules")
|
||||
async def get_risk_rules(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取风控规则列表"""
|
||||
rules = db.query(RiskRule).order_by(RiskRule.created_at.desc()).all()
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"name": r.name,
|
||||
"type": r.type,
|
||||
"threshold": r.threshold,
|
||||
"duration": r.duration,
|
||||
"is_enabled": r.is_enabled,
|
||||
"created_at": r.created_at.isoformat() if r.created_at else None,
|
||||
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
|
||||
}
|
||||
for r in rules
|
||||
]
|
||||
|
||||
|
||||
@router.post("/risk-rules")
|
||||
async def create_risk_rule(
|
||||
name: str,
|
||||
rule_type: str,
|
||||
threshold: int = 0,
|
||||
duration: int = 3600,
|
||||
is_enabled: bool = True,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建风控规则"""
|
||||
rule = RiskRule(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
type=rule_type,
|
||||
threshold=threshold,
|
||||
duration=duration,
|
||||
is_enabled=is_enabled,
|
||||
)
|
||||
db.add(rule)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_risk_rule",
|
||||
target_type="risk_rule",
|
||||
target_id=rule.id,
|
||||
detail=f"创建风控规则 {name}",
|
||||
)
|
||||
|
||||
return {"message": "风控规则已创建", "id": rule.id}
|
||||
|
||||
|
||||
@router.put("/risk-rules/{id}")
|
||||
async def update_risk_rule(
|
||||
id: str,
|
||||
name: str = None,
|
||||
threshold: int = None,
|
||||
duration: int = None,
|
||||
is_enabled: bool = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新风控规则"""
|
||||
rule = db.query(RiskRule).filter(RiskRule.id == id).first()
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="风控规则不存在")
|
||||
|
||||
if name is not None:
|
||||
rule.name = name
|
||||
if threshold is not None:
|
||||
rule.threshold = threshold
|
||||
if duration is not None:
|
||||
rule.duration = duration
|
||||
if is_enabled is not None:
|
||||
rule.is_enabled = is_enabled
|
||||
rule.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_risk_rule",
|
||||
target_type="risk_rule",
|
||||
target_id=id,
|
||||
detail=f"更新风控规则 {rule.name}",
|
||||
)
|
||||
|
||||
return {"message": "风控规则已更新"}
|
||||
|
||||
|
||||
@router.delete("/risk-rules/{id}")
|
||||
async def delete_risk_rule(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除风控规则"""
|
||||
rule = db.query(RiskRule).filter(RiskRule.id == id).first()
|
||||
if not rule:
|
||||
raise HTTPException(status_code=404, detail="风控规则不存在")
|
||||
|
||||
db.delete(rule)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_risk_rule",
|
||||
target_type="risk_rule",
|
||||
target_id=id,
|
||||
detail=f"删除风控规则 {rule.name}",
|
||||
)
|
||||
|
||||
return {"message": "风控规则已删除"}
|
||||
@@ -0,0 +1,412 @@
|
||||
"""
|
||||
应用市场审核 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import MarketApp, Coupon, UserCoupon, User
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 应用审核管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/apps/pending")
|
||||
async def get_pending_apps(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取待审核应用列表"""
|
||||
apps = db.query(MarketApp).filter(
|
||||
MarketApp.status == "pending"
|
||||
).order_by(MarketApp.created_at.desc()).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": a.id,
|
||||
"name": a.name,
|
||||
"type": a.type,
|
||||
"source": a.source,
|
||||
"publisher_id": a.publisher_id,
|
||||
"publisher_name": a.publisher_name,
|
||||
"description": a.description,
|
||||
"icon": a.icon,
|
||||
"tutorial": a.tutorial,
|
||||
"price": float(a.price) if a.price else 0,
|
||||
"is_member_free": a.is_member_free,
|
||||
"join_promotion": a.join_promotion,
|
||||
"status": a.status,
|
||||
"rating_avg": float(a.rating_avg) if a.rating_avg else 0,
|
||||
"review_count": a.review_count,
|
||||
"install_count": a.install_count,
|
||||
"skill_content": a.skill_content,
|
||||
"mcp_transport": a.mcp_transport,
|
||||
"agent_endpoint": a.agent_endpoint,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
||||
}
|
||||
for a in apps
|
||||
]
|
||||
|
||||
|
||||
@router.put("/apps/{id}/approve")
|
||||
async def approve_app(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""审核通过应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
app.status = "published"
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="approve_app",
|
||||
target_type="market_app",
|
||||
target_id=id,
|
||||
detail=f"审核通过应用 {app.name}",
|
||||
)
|
||||
|
||||
return {"message": "应用审核通过"}
|
||||
|
||||
|
||||
@router.put("/apps/{id}/reject")
|
||||
async def reject_app(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""审核拒绝应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
app.status = "removed"
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="reject_app",
|
||||
target_type="market_app",
|
||||
target_id=id,
|
||||
detail=f"审核拒绝应用 {app.name}",
|
||||
)
|
||||
|
||||
return {"message": "应用审核已拒绝"}
|
||||
|
||||
|
||||
@router.get("/apps")
|
||||
async def get_all_apps(
|
||||
status: str = Query(None, description="状态筛选: pending/published/removed"),
|
||||
app_type: str = Query(None, description="类型筛选: skill/agent/mcp"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取所有应用列表(管理员)"""
|
||||
query = db.query(MarketApp)
|
||||
|
||||
if status:
|
||||
query = query.filter(MarketApp.status == status)
|
||||
if app_type:
|
||||
query = query.filter(MarketApp.type == app_type)
|
||||
|
||||
total = query.count()
|
||||
apps = query.order_by(MarketApp.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": a.id,
|
||||
"name": a.name,
|
||||
"type": a.type,
|
||||
"source": a.source,
|
||||
"publisher_id": a.publisher_id,
|
||||
"publisher_name": a.publisher_name,
|
||||
"description": a.description,
|
||||
"icon": a.icon,
|
||||
"tutorial": a.tutorial,
|
||||
"price": float(a.price) if a.price else 0,
|
||||
"is_member_free": a.is_member_free,
|
||||
"join_promotion": a.join_promotion,
|
||||
"status": a.status,
|
||||
"rating_avg": float(a.rating_avg) if a.rating_avg else 0,
|
||||
"review_count": a.review_count,
|
||||
"install_count": a.install_count,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
||||
}
|
||||
for a in apps
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.put("/apps/{id}")
|
||||
async def admin_edit_app(
|
||||
id: str,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
icon: str = None,
|
||||
price: float = None,
|
||||
is_member_free: bool = None,
|
||||
join_promotion: bool = None,
|
||||
tutorial: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""管理员编辑应用"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
if name is not None:
|
||||
app.name = name
|
||||
if description is not None:
|
||||
app.description = description
|
||||
if icon is not None:
|
||||
app.icon = icon
|
||||
if price is not None:
|
||||
app.price = price
|
||||
if is_member_free is not None:
|
||||
app.is_member_free = is_member_free
|
||||
if join_promotion is not None:
|
||||
app.join_promotion = join_promotion
|
||||
if tutorial is not None:
|
||||
app.tutorial = tutorial
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="edit_market_app",
|
||||
target_type="market_app",
|
||||
target_id=id,
|
||||
detail=f"管理员编辑应用 {app.name}",
|
||||
)
|
||||
|
||||
return {"message": "应用已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 优惠券管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/coupons")
|
||||
async def get_coupons(
|
||||
status: str = Query(None, description="状态筛选: active/expired/disabled"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取优惠券列表"""
|
||||
query = db.query(Coupon)
|
||||
|
||||
if status:
|
||||
query = query.filter(Coupon.status == status)
|
||||
|
||||
coupons = query.order_by(Coupon.created_at.desc()).all()
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"code": c.code,
|
||||
"type": c.type,
|
||||
"value": float(c.value) if c.value else 0,
|
||||
"min_amount": float(c.min_amount) if c.min_amount else 0,
|
||||
"valid_from": c.valid_from.isoformat() if c.valid_from else None,
|
||||
"valid_to": c.valid_to.isoformat() if c.valid_to else None,
|
||||
"total_count": c.total_count,
|
||||
"used_count": c.used_count,
|
||||
"status": c.status,
|
||||
"created_at": c.created_at.isoformat() if c.created_at else None,
|
||||
}
|
||||
for c in coupons
|
||||
]
|
||||
|
||||
|
||||
@router.post("/coupons")
|
||||
async def create_coupon(
|
||||
code: str,
|
||||
coupon_type: str,
|
||||
value: float,
|
||||
valid_from: str,
|
||||
valid_to: str,
|
||||
min_amount: float = 0,
|
||||
total_count: int = None,
|
||||
status: str = "active",
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建优惠券"""
|
||||
# 检查code是否已存在
|
||||
existing = db.query(Coupon).filter(Coupon.code == code).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="优惠券码已存在")
|
||||
|
||||
coupon = Coupon(
|
||||
id=str(uuid.uuid4()),
|
||||
code=code,
|
||||
type=coupon_type,
|
||||
value=value,
|
||||
min_amount=min_amount,
|
||||
valid_from=datetime.fromisoformat(valid_from),
|
||||
valid_to=datetime.fromisoformat(valid_to),
|
||||
total_count=total_count,
|
||||
status=status,
|
||||
)
|
||||
db.add(coupon)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_coupon",
|
||||
target_type="coupon",
|
||||
target_id=coupon.id,
|
||||
detail=f"创建优惠券 {code}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已创建", "id": coupon.id}
|
||||
|
||||
|
||||
@router.put("/coupons/{id}")
|
||||
async def update_coupon(
|
||||
id: str,
|
||||
code: str = None,
|
||||
value: float = None,
|
||||
min_amount: float = None,
|
||||
valid_from: str = None,
|
||||
valid_to: str = None,
|
||||
total_count: int = None,
|
||||
status: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新优惠券"""
|
||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||
|
||||
if code is not None:
|
||||
# 检查新code是否重复
|
||||
if code != coupon.code:
|
||||
dup = db.query(Coupon).filter(Coupon.code == code).first()
|
||||
if dup:
|
||||
raise HTTPException(status_code=400, detail="优惠券码已存在")
|
||||
coupon.code = code
|
||||
if value is not None:
|
||||
coupon.value = value
|
||||
if min_amount is not None:
|
||||
coupon.min_amount = min_amount
|
||||
if valid_from is not None:
|
||||
coupon.valid_from = datetime.fromisoformat(valid_from)
|
||||
if valid_to is not None:
|
||||
coupon.valid_to = datetime.fromisoformat(valid_to)
|
||||
if total_count is not None:
|
||||
coupon.total_count = total_count
|
||||
if status is not None:
|
||||
coupon.status = status
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_coupon",
|
||||
target_type="coupon",
|
||||
target_id=id,
|
||||
detail=f"更新优惠券 {coupon.code}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已更新"}
|
||||
|
||||
|
||||
@router.delete("/coupons/{id}")
|
||||
async def delete_coupon(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除优惠券"""
|
||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||
|
||||
db.delete(coupon)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_coupon",
|
||||
target_type="coupon",
|
||||
target_id=id,
|
||||
detail=f"删除优惠券 {coupon.code}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已删除"}
|
||||
|
||||
|
||||
@router.post("/coupons/{id}/assign")
|
||||
async def assign_coupon(
|
||||
id: str,
|
||||
user_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""发放优惠券给用户"""
|
||||
coupon = db.query(Coupon).filter(Coupon.id == id).first()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在")
|
||||
if coupon.status != "active":
|
||||
raise HTTPException(status_code=400, detail="优惠券非活跃状态")
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 检查是否已发放
|
||||
existing = db.query(UserCoupon).filter(
|
||||
UserCoupon.user_id == user_id,
|
||||
UserCoupon.coupon_id == id,
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="该用户已拥有此优惠券")
|
||||
|
||||
user_coupon = UserCoupon(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
coupon_id=id,
|
||||
)
|
||||
db.add(user_coupon)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="assign_coupon",
|
||||
target_type="coupon",
|
||||
target_id=id,
|
||||
detail=f"发放优惠券 {coupon.code} 给用户 {user.username}",
|
||||
)
|
||||
|
||||
return {"message": "优惠券已发放"}
|
||||
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
会员订阅体系管理 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import MembershipPlan, MembershipSubscription, User, RiskRule
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 会员计划 CRUD
|
||||
# ============================================================
|
||||
|
||||
@router.get("/plans")
|
||||
async def get_plans(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取会员计划列表"""
|
||||
plans = db.query(MembershipPlan).order_by(MembershipPlan.level).all()
|
||||
return [
|
||||
{
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"level": p.level,
|
||||
"duration_type": p.duration_type,
|
||||
"price": float(p.price) if p.price else 0,
|
||||
"daily_free_tokens": p.daily_free_tokens,
|
||||
"features": p.features,
|
||||
"free_app_ids": p.free_app_ids,
|
||||
"free_model_ids": p.free_model_ids,
|
||||
"is_active": p.is_active,
|
||||
"created_at": p.created_at.isoformat() if p.created_at else None,
|
||||
"updated_at": p.updated_at.isoformat() if p.updated_at else None,
|
||||
}
|
||||
for p in plans
|
||||
]
|
||||
|
||||
|
||||
@router.post("/plans")
|
||||
async def create_plan(
|
||||
name: str,
|
||||
level: int,
|
||||
duration_type: str,
|
||||
price: float,
|
||||
daily_free_tokens: int = 0,
|
||||
features: list = None,
|
||||
free_app_ids: list = None,
|
||||
free_model_ids: list = None,
|
||||
is_active: bool = True,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建会员计划"""
|
||||
plan = MembershipPlan(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
level=level,
|
||||
duration_type=duration_type,
|
||||
price=price,
|
||||
daily_free_tokens=daily_free_tokens,
|
||||
features=features,
|
||||
free_app_ids=free_app_ids,
|
||||
free_model_ids=free_model_ids,
|
||||
is_active=is_active,
|
||||
)
|
||||
db.add(plan)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_membership_plan",
|
||||
target_type="membership_plan",
|
||||
target_id=plan.id,
|
||||
detail=f"创建会员计划 {name}",
|
||||
)
|
||||
|
||||
return {"message": "会员计划已创建", "id": plan.id}
|
||||
|
||||
|
||||
@router.put("/plans/{id}")
|
||||
async def update_plan(
|
||||
id: str,
|
||||
name: str = None,
|
||||
level: int = None,
|
||||
duration_type: str = None,
|
||||
price: float = None,
|
||||
daily_free_tokens: int = None,
|
||||
features: list = None,
|
||||
free_app_ids: list = None,
|
||||
free_model_ids: list = None,
|
||||
is_active: bool = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新会员计划"""
|
||||
plan = db.query(MembershipPlan).filter(MembershipPlan.id == id).first()
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="会员计划不存在")
|
||||
|
||||
if name is not None:
|
||||
plan.name = name
|
||||
if level is not None:
|
||||
plan.level = level
|
||||
if duration_type is not None:
|
||||
plan.duration_type = duration_type
|
||||
if price is not None:
|
||||
plan.price = price
|
||||
if daily_free_tokens is not None:
|
||||
plan.daily_free_tokens = daily_free_tokens
|
||||
if features is not None:
|
||||
plan.features = features
|
||||
if free_app_ids is not None:
|
||||
plan.free_app_ids = free_app_ids
|
||||
if free_model_ids is not None:
|
||||
plan.free_model_ids = free_model_ids
|
||||
if is_active is not None:
|
||||
plan.is_active = is_active
|
||||
plan.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_membership_plan",
|
||||
target_type="membership_plan",
|
||||
target_id=id,
|
||||
detail=f"更新会员计划 {plan.name}",
|
||||
)
|
||||
|
||||
return {"message": "会员计划已更新"}
|
||||
|
||||
|
||||
@router.delete("/plans/{id}")
|
||||
async def delete_plan(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除会员计划"""
|
||||
plan = db.query(MembershipPlan).filter(MembershipPlan.id == id).first()
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="会员计划不存在")
|
||||
|
||||
db.delete(plan)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_membership_plan",
|
||||
target_type="membership_plan",
|
||||
target_id=id,
|
||||
detail=f"删除会员计划 {plan.name}",
|
||||
)
|
||||
|
||||
return {"message": "会员计划已删除"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 用户订阅管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/subscriptions")
|
||||
async def get_subscriptions(
|
||||
user_id: str = Query(None),
|
||||
status: str = Query(None, description="订阅状态: active/expired/downgraded"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取所有用户订阅列表"""
|
||||
query = db.query(MembershipSubscription)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(MembershipSubscription.user_id == user_id)
|
||||
if status:
|
||||
query = query.filter(MembershipSubscription.status == status)
|
||||
|
||||
total = query.count()
|
||||
subscriptions = query.order_by(
|
||||
MembershipSubscription.created_at.desc()
|
||||
).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": s.id,
|
||||
"user_id": s.user_id,
|
||||
"plan_id": s.plan_id,
|
||||
"order_id": s.order_id,
|
||||
"start_at": s.start_at.isoformat() if s.start_at else None,
|
||||
"end_at": s.end_at.isoformat() if s.end_at else None,
|
||||
"auto_renew": s.auto_renew,
|
||||
"status": s.status,
|
||||
"created_at": s.created_at.isoformat() if s.created_at else None,
|
||||
}
|
||||
for s in subscriptions
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 自动化规则(风控)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/automation-rules")
|
||||
async def get_automation_rules(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取自动化规则配置"""
|
||||
rules = db.query(RiskRule).order_by(RiskRule.created_at.desc()).all()
|
||||
|
||||
if not rules:
|
||||
# 返回默认规则
|
||||
return {
|
||||
"rules": [],
|
||||
"defaults": {
|
||||
"max_login_attempts": 5,
|
||||
"token_usage_threshold_per_hour": 1000000,
|
||||
"token_usage_threshold_per_day": 10000000,
|
||||
"concurrent_sessions_limit": 5,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
"rules": [
|
||||
{
|
||||
"id": r.id,
|
||||
"name": r.name,
|
||||
"type": r.type,
|
||||
"threshold": r.threshold,
|
||||
"duration": r.duration,
|
||||
"is_enabled": r.is_enabled,
|
||||
"created_at": r.created_at.isoformat() if r.created_at else None,
|
||||
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
|
||||
}
|
||||
for r in rules
|
||||
],
|
||||
"defaults": {
|
||||
"max_login_attempts": 5,
|
||||
"token_usage_threshold_per_hour": 1000000,
|
||||
"token_usage_threshold_per_day": 10000000,
|
||||
"concurrent_sessions_limit": 5,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
"""
|
||||
大模型库管理 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Body
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import AIModel, ModelProvider
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ============================================================
|
||||
# 协议预设列表
|
||||
# ============================================================
|
||||
|
||||
PROTOCOLS = [
|
||||
{"id": "openai_compatible", "name": "OpenAI兼容", "base_path": "/v1", "description": "兼容OpenAI API格式的模型服务"},
|
||||
{"id": "new_api", "name": "New API", "base_path": "/v1", "description": "新一代API聚合平台"},
|
||||
{"id": "volcengine", "name": "火山引擎", "base_path": "/v1", "description": "字节跳动火山引擎模型服务"},
|
||||
{"id": "aliyun_bailian", "name": "阿里云百炼", "base_path": "/v1", "description": "阿里云百炼大模型平台"},
|
||||
{"id": "ollama", "name": "Ollama", "base_path": "/v1", "description": "本地Ollama模型服务"},
|
||||
{"id": "xiaoma", "name": "小马算力", "base_path": "/v1", "description": "小马算力模型服务"},
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# 模型列表(用于 fetch-models 的回退数据)
|
||||
# ============================================================
|
||||
|
||||
FETCHABLE_MODELS = [
|
||||
{"id": "gpt-4o", "name": "GPT-4o", "capabilities": ["text", "vision", "tool"], "pricing": "paid"},
|
||||
{"id": "gpt-4o-mini", "name": "GPT-4o Mini", "capabilities": ["text", "vision"], "pricing": "paid"},
|
||||
{"id": "deepseek-v3", "name": "DeepSeek-V3", "capabilities": ["text", "thinking"], "pricing": "free"},
|
||||
{"id": "deepseek-r1", "name": "DeepSeek-R1", "capabilities": ["text", "thinking"], "pricing": "paid"},
|
||||
{"id": "qwen-max", "name": "Qwen-Max", "capabilities": ["text", "tool"], "pricing": "free"},
|
||||
{"id": "qwen-vl", "name": "Qwen-VL", "capabilities": ["text", "vision"], "pricing": "paid"},
|
||||
{"id": "whisper", "name": "Whisper", "capabilities": ["audio"], "pricing": "paid"},
|
||||
{"id": "dalle", "name": "DALL-E", "capabilities": ["image"], "pricing": "paid"},
|
||||
]
|
||||
|
||||
# 支持通过 API 获取模型列表的协议类型
|
||||
FETCHABLE_PROTOCOLS = {"openai_compatible", "new_api", "volcengine", "aliyun_bailian", "ollama", "xiaoma"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 辅助函数
|
||||
# ============================================================
|
||||
|
||||
def provider_to_dict(p: ModelProvider) -> dict:
|
||||
return {
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"protocol": p.protocol,
|
||||
"api_key": p.api_key,
|
||||
"api_base_url": p.api_base_url,
|
||||
"apiKey": p.api_key,
|
||||
"apiBaseUrl": p.api_base_url,
|
||||
"created_at": p.created_at.isoformat() if p.created_at else None,
|
||||
"updated_at": p.updated_at.isoformat() if p.updated_at else None,
|
||||
"createdAt": p.created_at.isoformat() if p.created_at else None,
|
||||
"updatedAt": p.updated_at.isoformat() if p.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
def model_to_dict(m: AIModel) -> dict:
|
||||
return {
|
||||
"id": m.id,
|
||||
"provider_id": m.provider_id,
|
||||
"name": m.name,
|
||||
"api_key": m.api_key,
|
||||
"endpoint": m.endpoint,
|
||||
"capabilities": m.capabilities,
|
||||
"timeout": m.timeout,
|
||||
"status": m.status,
|
||||
"price_per_million_tokens": float(m.price_per_million_tokens) if m.price_per_million_tokens else 0,
|
||||
"is_enabled": m.is_enabled,
|
||||
"is_default": bool(m.is_default),
|
||||
"total_calls": m.total_calls or 0,
|
||||
"total_tokens": m.total_tokens or 0,
|
||||
"created_at": m.created_at.isoformat() if m.created_at else None,
|
||||
"updated_at": m.updated_at.isoformat() if m.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 协议列表
|
||||
# ============================================================
|
||||
|
||||
@router.get("/protocols")
|
||||
async def get_protocols(
|
||||
admin_user=Depends(get_current_admin),
|
||||
):
|
||||
"""获取支持的协议列表"""
|
||||
return PROTOCOLS
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模型供应商管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/providers")
|
||||
async def get_providers(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取模型供应商列表"""
|
||||
providers = db.query(ModelProvider).order_by(
|
||||
ModelProvider.created_at.desc()
|
||||
).all()
|
||||
return [provider_to_dict(p) for p in providers]
|
||||
|
||||
|
||||
@router.post("/providers")
|
||||
async def create_provider(
|
||||
name: str = Body(...),
|
||||
protocol: str = Body(...),
|
||||
api_key: str = Body(default=""),
|
||||
api_base_url: str = Body(default=None),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""添加模型供应商"""
|
||||
# 验证协议
|
||||
valid_protocols = [p["id"] for p in PROTOCOLS]
|
||||
if protocol not in valid_protocols:
|
||||
raise HTTPException(status_code=400, detail=f"无效的协议类型,支持: {', '.join(valid_protocols)}")
|
||||
|
||||
provider = ModelProvider(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
protocol=protocol,
|
||||
api_key=api_key,
|
||||
api_base_url=api_base_url,
|
||||
)
|
||||
db.add(provider)
|
||||
db.commit()
|
||||
db.refresh(provider)
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_provider",
|
||||
target_type="model_provider",
|
||||
target_id=provider.id,
|
||||
detail=f"添加模型供应商 {name} (协议: {protocol})",
|
||||
)
|
||||
|
||||
return {"message": "供应商已创建", "id": provider.id, "provider": provider_to_dict(provider)}
|
||||
|
||||
|
||||
@router.put("/providers/{id}")
|
||||
async def update_provider(
|
||||
id: str,
|
||||
name: str = Body(default=None),
|
||||
protocol: str = Body(default=None),
|
||||
api_key: str = Body(default=None),
|
||||
api_base_url: str = Body(default=None),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新模型供应商"""
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
if name is not None:
|
||||
provider.name = name
|
||||
if protocol is not None:
|
||||
valid_protocols = [p["id"] for p in PROTOCOLS]
|
||||
if protocol not in valid_protocols:
|
||||
raise HTTPException(status_code=400, detail=f"无效的协议类型")
|
||||
provider.protocol = protocol
|
||||
if api_key is not None:
|
||||
provider.api_key = api_key
|
||||
if api_base_url is not None:
|
||||
provider.api_base_url = api_base_url
|
||||
provider.updated_at = datetime.now()
|
||||
db.commit()
|
||||
db.refresh(provider)
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_provider",
|
||||
target_type="model_provider",
|
||||
target_id=id,
|
||||
detail=f"更新模型供应商 {provider.name}",
|
||||
)
|
||||
|
||||
return {"message": "供应商已更新", "provider": provider_to_dict(provider)}
|
||||
|
||||
|
||||
@router.delete("/providers/{id}")
|
||||
async def delete_provider(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除模型供应商"""
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
# 检查是否有模型关联
|
||||
model_count = db.query(func.count(AIModel.id)).filter(
|
||||
AIModel.provider_id == id
|
||||
).scalar()
|
||||
if model_count > 0:
|
||||
raise HTTPException(status_code=400, detail=f"该供应商下有 {model_count} 个模型,请先删除模型")
|
||||
|
||||
db.delete(provider)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_provider",
|
||||
target_type="model_provider",
|
||||
target_id=id,
|
||||
detail=f"删除模型供应商 {provider.name}",
|
||||
)
|
||||
|
||||
return {"message": "供应商已删除"}
|
||||
|
||||
|
||||
@router.post("/providers/{id}/fetch-models")
|
||||
async def fetch_models(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取供应商可用模型列表(优先从真实API获取)"""
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Fetching models for provider {provider.name} (protocol: {provider.protocol})")
|
||||
|
||||
api_base = (provider.api_base_url or "").rstrip("/")
|
||||
api_key = provider.api_key or ""
|
||||
|
||||
# 尝试从真实API获取模型列表(只要有 api_base 就尝试调用,api_key 可选)
|
||||
if api_base and provider.protocol in FETCHABLE_PROTOCOLS:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as client:
|
||||
headers = {}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
resp = await client.get(f"{api_base}/v1/models", headers=headers)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
models_list = data.get("data", [])
|
||||
if models_list:
|
||||
result = []
|
||||
for m in models_list:
|
||||
mid = m.get("id", "")
|
||||
if not mid:
|
||||
continue
|
||||
result.append({
|
||||
"id": mid,
|
||||
"name": mid,
|
||||
"capabilities": ["text"],
|
||||
"pricing": "paid",
|
||||
})
|
||||
logger.info(f"Successfully fetched {len(result)} models from provider API")
|
||||
return {"models": result, "source": "api"}
|
||||
else:
|
||||
logger.warning(f"Provider API returned empty model list")
|
||||
return {"models": [], "source": "api", "message": "API返回的模型列表为空"}
|
||||
elif resp.status_code == 401 or resp.status_code == 403:
|
||||
logger.warning(f"Provider API returned {resp.status_code}: auth error")
|
||||
return {"models": [], "source": "error", "message": "API认证失败,请检查API Key是否正确"}
|
||||
else:
|
||||
logger.warning(f"Provider API returned status {resp.status_code}: {resp.text[:200]}")
|
||||
return {"models": [], "source": "error", "message": f"API请求失败(HTTP {resp.status_code}),请检查API地址和密钥"}
|
||||
except httpx.TimeoutException:
|
||||
logger.warning(f"Timeout fetching models from provider API: {api_base}")
|
||||
return {"models": [], "source": "error", "message": "请求API超时,请检查API地址是否正确"}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch models from provider API: {e}")
|
||||
return {"models": [], "source": "error", "message": f"请求API失败: {str(e)}"}
|
||||
|
||||
# 回退:返回固定模型列表(供无 API 的离线环境使用)
|
||||
logger.info(f"Using fallback model list for provider {provider.name}")
|
||||
return {"models": FETCHABLE_MODELS, "source": "fallback"}
|
||||
|
||||
|
||||
class AddModelsRequest(BaseModel):
|
||||
model_ids: List[str]
|
||||
|
||||
|
||||
@router.post("/providers/{id}/add-models")
|
||||
async def add_models(
|
||||
id: str,
|
||||
req: AddModelsRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""批量添加模型到供应商"""
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
# 找到协议对应的 base_path
|
||||
protocol_info = next((p for p in PROTOCOLS if p["id"] == provider.protocol), None)
|
||||
base_path = protocol_info["base_path"] if protocol_info else "/v1"
|
||||
|
||||
added = []
|
||||
for model_id in req.model_ids:
|
||||
# 在 FETCHABLE_MODELS 中查找额外信息
|
||||
model_info = next((m for m in FETCHABLE_MODELS if m["id"] == model_id), None)
|
||||
|
||||
# 使用 model_id 作为模型名称
|
||||
model_name = model_info["name"] if model_info else model_id
|
||||
|
||||
# 检查是否已存在同名模型
|
||||
existing = db.query(AIModel).filter(
|
||||
AIModel.provider_id == id,
|
||||
AIModel.name == model_name
|
||||
).first()
|
||||
if existing:
|
||||
continue
|
||||
|
||||
# 自动生成 endpoint
|
||||
if provider.api_base_url:
|
||||
endpoint = f"{provider.api_base_url.rstrip('/')}{base_path}/chat/completions"
|
||||
else:
|
||||
endpoint = ""
|
||||
|
||||
model = AIModel(
|
||||
id=str(uuid.uuid4()),
|
||||
provider_id=id,
|
||||
name=model_name,
|
||||
api_key=provider.api_key or "",
|
||||
endpoint=endpoint,
|
||||
capabilities=model_info.get("capabilities") if model_info else ["text"],
|
||||
timeout=60,
|
||||
status="free" if (model_info and model_info.get("pricing") == "free") else "paid",
|
||||
price_per_million_tokens=0,
|
||||
is_enabled=True,
|
||||
)
|
||||
db.add(model)
|
||||
added.append(model_name)
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="add_models",
|
||||
target_type="model_provider",
|
||||
target_id=id,
|
||||
detail=f"为供应商 {provider.name} 批量添加 {len(added)} 个模型",
|
||||
)
|
||||
|
||||
return {"message": f"成功添加 {len(added)} 个模型", "added": added}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模型管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/")
|
||||
async def get_models(
|
||||
provider_id: str = Query(None),
|
||||
status: str = Query(None, description="模型状态筛选: free/paid"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取模型列表(管理员)"""
|
||||
query = db.query(AIModel)
|
||||
|
||||
if provider_id:
|
||||
query = query.filter(AIModel.provider_id == provider_id)
|
||||
if status:
|
||||
query = query.filter(AIModel.status == status)
|
||||
|
||||
total = query.count()
|
||||
models = query.order_by(AIModel.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [model_to_dict(m) for m in models],
|
||||
}
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_model(
|
||||
provider_id: str,
|
||||
name: str,
|
||||
api_key: str = None,
|
||||
endpoint: str = None,
|
||||
capabilities: List[str] = None,
|
||||
timeout: int = 60,
|
||||
status: str = "paid",
|
||||
price_per_million_tokens: float = 0,
|
||||
is_enabled: bool = True,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""添加模型"""
|
||||
# 验证供应商存在
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == provider_id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
|
||||
model = AIModel(
|
||||
id=str(uuid.uuid4()),
|
||||
provider_id=provider_id,
|
||||
name=name,
|
||||
api_key=api_key,
|
||||
endpoint=endpoint,
|
||||
capabilities=capabilities,
|
||||
timeout=timeout,
|
||||
status=status,
|
||||
price_per_million_tokens=price_per_million_tokens,
|
||||
is_enabled=is_enabled,
|
||||
)
|
||||
db.add(model)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_model",
|
||||
target_type="model",
|
||||
target_id=model.id,
|
||||
detail=f"添加模型 {name}",
|
||||
)
|
||||
|
||||
return {"message": "模型已创建", "id": model.id}
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_model(
|
||||
id: str,
|
||||
provider_id: str = None,
|
||||
name: str = None,
|
||||
api_key: str = None,
|
||||
endpoint: str = None,
|
||||
capabilities: List[str] = None,
|
||||
timeout: int = None,
|
||||
status: str = None,
|
||||
price_per_million_tokens: float = None,
|
||||
is_enabled: bool = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新模型"""
|
||||
model = db.query(AIModel).filter(AIModel.id == id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
if provider_id is not None:
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == provider_id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||
model.provider_id = provider_id
|
||||
if name is not None:
|
||||
model.name = name
|
||||
if api_key is not None:
|
||||
model.api_key = api_key
|
||||
if endpoint is not None:
|
||||
model.endpoint = endpoint
|
||||
if capabilities is not None:
|
||||
model.capabilities = capabilities
|
||||
if timeout is not None:
|
||||
model.timeout = timeout
|
||||
if status is not None:
|
||||
model.status = status
|
||||
if price_per_million_tokens is not None:
|
||||
model.price_per_million_tokens = price_per_million_tokens
|
||||
if is_enabled is not None:
|
||||
model.is_enabled = is_enabled
|
||||
model.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_model",
|
||||
target_type="model",
|
||||
target_id=id,
|
||||
detail=f"更新模型 {model.name}",
|
||||
)
|
||||
|
||||
return {"message": "模型已更新"}
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_model(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除模型"""
|
||||
model = db.query(AIModel).filter(AIModel.id == id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
model_name = model.name
|
||||
db.delete(model)
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_model",
|
||||
target_type="model",
|
||||
target_id=id,
|
||||
detail=f"删除模型 {model_name}",
|
||||
)
|
||||
|
||||
return {"message": "模型已删除"}
|
||||
|
||||
|
||||
@router.put("/{id}/toggle")
|
||||
async def toggle_model(
|
||||
id: str,
|
||||
is_enabled: bool = Query(...),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""开关模型状态"""
|
||||
model = db.query(AIModel).filter(AIModel.id == id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
model.is_enabled = is_enabled
|
||||
model.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="toggle_model",
|
||||
target_type="model",
|
||||
target_id=id,
|
||||
detail=f"将模型 {model.name} {'启用' if is_enabled else '禁用'}",
|
||||
)
|
||||
|
||||
return {"message": f"模型已{'启用' if is_enabled else '禁用'}"}
|
||||
|
||||
|
||||
@router.put("/{id}/set-default")
|
||||
async def set_default_model(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""设为全局默认模型(取消其他模型的默认状态)"""
|
||||
model = db.query(AIModel).filter(AIModel.id == id).first()
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
# 取消所有模型的默认状态
|
||||
db.query(AIModel).filter(AIModel.is_default == True).update({"is_default": False})
|
||||
# 设置当前模型为默认
|
||||
model.is_default = True
|
||||
model.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="set_default_model",
|
||||
target_type="model",
|
||||
target_id=id,
|
||||
detail=f"设置模型 {model.name} 为全局默认",
|
||||
)
|
||||
|
||||
return {"message": f"已将 {model.name} 设为全局默认模型"}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_model_stats(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取模型调用统计"""
|
||||
total_calls = db.query(func.sum(AIModel.total_calls)).scalar() or 0
|
||||
total_tokens = db.query(func.sum(AIModel.total_tokens)).scalar() or 0
|
||||
|
||||
return {
|
||||
"total_calls": int(total_calls),
|
||||
"total_tokens": int(total_tokens),
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
支付渠道配置 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import PaymentConfig, Order
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import csv
|
||||
import io
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class PaymentConfigUpdate(BaseModel):
|
||||
app_id: Optional[str] = None
|
||||
merchant_id: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
api_v3_key: Optional[str] = None
|
||||
cert_path: Optional[str] = None
|
||||
cert_serial_no: Optional[str] = None
|
||||
alipay_public_key: Optional[str] = None
|
||||
app_private_key: Optional[str] = None
|
||||
gateway_url: Optional[str] = None
|
||||
notify_url: Optional[str] = None
|
||||
return_url: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 支付配置管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/configs")
|
||||
async def get_payment_configs(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取支付配置列表"""
|
||||
configs = db.query(PaymentConfig).order_by(
|
||||
PaymentConfig.created_at.desc()
|
||||
).all()
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"type": c.type,
|
||||
"app_id": c.app_id,
|
||||
"merchant_id": c.merchant_id,
|
||||
"api_key": c.api_key,
|
||||
"api_v3_key": c.api_v3_key,
|
||||
"cert_path": c.cert_path,
|
||||
"cert_serial_no": c.cert_serial_no,
|
||||
"alipay_public_key": c.alipay_public_key,
|
||||
"app_private_key": c.app_private_key,
|
||||
"gateway_url": c.gateway_url,
|
||||
"notify_url": c.notify_url,
|
||||
"return_url": c.return_url,
|
||||
"status": c.status,
|
||||
"created_at": c.created_at.isoformat() if c.created_at else None,
|
||||
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
|
||||
}
|
||||
for c in configs
|
||||
]
|
||||
|
||||
|
||||
@router.put("/configs/{id}")
|
||||
async def update_payment_config(
|
||||
id: str,
|
||||
data: PaymentConfigUpdate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新支付配置"""
|
||||
config = db.query(PaymentConfig).filter(PaymentConfig.id == id).first()
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="支付配置不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
if hasattr(config, key):
|
||||
setattr(config, key, value)
|
||||
config.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_payment_config",
|
||||
target_type="payment_config",
|
||||
target_id=id,
|
||||
detail=f"更新支付配置 [{config.type}]",
|
||||
)
|
||||
|
||||
return {"code": 0, "message": "支付配置已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 订单管理
|
||||
# ============================================================
|
||||
|
||||
@router.get("/orders")
|
||||
async def get_orders(
|
||||
status: str = Query(None, description="订单状态筛选: pending/paid/refunded/cancelled"),
|
||||
order_type: str = Query(None, description="订单类型筛选: membership/service/app"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取所有订单列表"""
|
||||
query = db.query(Order)
|
||||
|
||||
if status:
|
||||
query = query.filter(Order.status == status)
|
||||
if order_type:
|
||||
query = query.filter(Order.order_type == order_type)
|
||||
|
||||
total = query.count()
|
||||
orders = query.order_by(Order.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": o.id,
|
||||
"user_id": o.user_id,
|
||||
"order_type": o.order_type,
|
||||
"item_id": o.item_id,
|
||||
"item_name": o.item_name,
|
||||
"amount": float(o.amount) if o.amount else 0,
|
||||
"coupon_id": o.coupon_id,
|
||||
"discount_amount": float(o.discount_amount) if o.discount_amount else 0,
|
||||
"payment_method": o.payment_method,
|
||||
"payment_id": o.payment_id,
|
||||
"status": o.status,
|
||||
"created_at": o.created_at.isoformat() if o.created_at else None,
|
||||
"updated_at": o.updated_at.isoformat() if o.updated_at else None,
|
||||
}
|
||||
for o in orders
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.put("/orders/{id}/refund")
|
||||
async def refund_order(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""退款操作"""
|
||||
order = db.query(Order).filter(Order.id == id).first()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
if order.status != "paid":
|
||||
raise HTTPException(status_code=400, detail="只能对已支付的订单进行退款")
|
||||
if order.status == "refunded":
|
||||
raise HTTPException(status_code=400, detail="订单已退款")
|
||||
|
||||
order.status = "refunded"
|
||||
order.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="refund_order",
|
||||
target_type="order",
|
||||
target_id=id,
|
||||
detail=f"退款订单 {order.id}, 金额 {float(order.amount)}",
|
||||
)
|
||||
|
||||
return {"message": "退款成功"}
|
||||
|
||||
|
||||
@router.get("/orders/export")
|
||||
async def export_orders(
|
||||
status: str = Query(None, description="订单状态筛选"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出订单(CSV格式)"""
|
||||
query = db.query(Order)
|
||||
if status:
|
||||
query = query.filter(Order.status == status)
|
||||
|
||||
orders = query.order_by(Order.created_at.desc()).all()
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow([
|
||||
"订单ID", "用户ID", "订单类型", "商品ID", "商品名称",
|
||||
"金额", "优惠券ID", "折扣金额", "支付方式", "支付ID",
|
||||
"状态", "创建时间", "更新时间"
|
||||
])
|
||||
|
||||
for o in orders:
|
||||
writer.writerow([
|
||||
o.id, o.user_id, o.order_type, o.item_id, o.item_name,
|
||||
float(o.amount) if o.amount else 0,
|
||||
o.coupon_id,
|
||||
float(o.discount_amount) if o.discount_amount else 0,
|
||||
o.payment_method, o.payment_id,
|
||||
o.status,
|
||||
o.created_at.isoformat() if o.created_at else "",
|
||||
o.updated_at.isoformat() if o.updated_at else "",
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=orders_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,453 @@
|
||||
"""
|
||||
增值服务管理 API(管理员)
|
||||
四大类服务: app_custom / platform_proxy / data_custom / private_project
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from ...database import get_db
|
||||
from ...models import ValueAddedService, CustomServiceOrder, CustomServiceRequest, MarketApp, User
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CreateServiceRequest(BaseModel):
|
||||
name: str
|
||||
service_type: str
|
||||
description: Optional[str] = None
|
||||
price: float = 0
|
||||
features: Optional[str] = None
|
||||
status: str = "active"
|
||||
assigned_agent_id: Optional[str] = None
|
||||
|
||||
|
||||
class UpdateServiceRequest(BaseModel):
|
||||
name: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
service_type: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
features: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
assigned_agent_id: Optional[str] = None
|
||||
|
||||
# 服务类型映射
|
||||
SERVICE_TYPE_MAP = {
|
||||
"app_custom": "应用定制服务",
|
||||
"platform_proxy": "平台代理申请",
|
||||
"data_custom": "数据定制服务",
|
||||
"private_project": "私有化项目服务",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 增值服务 CRUD
|
||||
# ============================================================
|
||||
|
||||
@router.get("/")
|
||||
async def get_services(
|
||||
service_type: str = Query(None, description="服务类型筛选: app_custom/platform_proxy/data_custom/private_project"),
|
||||
status: str = Query(None, description="状态筛选: active/inactive/deleted"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取增值服务列表"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Admin {admin_user.username} requested services list: type={service_type}, status={status}")
|
||||
|
||||
query = db.query(ValueAddedService).filter(ValueAddedService.status != "deleted")
|
||||
|
||||
if service_type:
|
||||
query = query.filter(ValueAddedService.type == service_type)
|
||||
if status:
|
||||
query = query.filter(ValueAddedService.status == status)
|
||||
|
||||
services = query.order_by(ValueAddedService.created_at.desc()).all()
|
||||
result = []
|
||||
for s in services:
|
||||
result.append({
|
||||
"id": s.id,
|
||||
"name": s.name,
|
||||
"description": s.description,
|
||||
"price": float(s.price) if s.price else 0,
|
||||
"type": s.type,
|
||||
"type_name": SERVICE_TYPE_MAP.get(s.type, s.type),
|
||||
"features": s.features or [],
|
||||
"status": s.status,
|
||||
"assigned_agent_id": s.assigned_agent_id,
|
||||
"created_at": s.created_at.isoformat() if s.created_at else None,
|
||||
"updated_at": s.updated_at.isoformat() if s.updated_at else None,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_service(
|
||||
req: CreateServiceRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建增值服务"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if req.service_type not in SERVICE_TYPE_MAP:
|
||||
raise HTTPException(status_code=400, detail=f"无效的服务类型,可选值:{', '.join(SERVICE_TYPE_MAP.keys())}")
|
||||
|
||||
features_json = None
|
||||
if req.features:
|
||||
try:
|
||||
features_json = json.loads(req.features) if isinstance(req.features, str) else req.features
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
features_json = [req.features]
|
||||
|
||||
service = ValueAddedService(
|
||||
id=str(uuid.uuid4()),
|
||||
name=req.name,
|
||||
description=req.description,
|
||||
price=req.price,
|
||||
type=req.service_type,
|
||||
features=features_json,
|
||||
status=req.status,
|
||||
assigned_agent_id=req.assigned_agent_id,
|
||||
)
|
||||
db.add(service)
|
||||
db.commit()
|
||||
db.refresh(service)
|
||||
|
||||
logger.info(f"Admin {admin_user.username} created service: {service.id} - {req.name}")
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="create_service",
|
||||
target_type="value_added_service",
|
||||
target_id=service.id,
|
||||
detail=f"创建增值服务 {req.name}(类型: {SERVICE_TYPE_MAP.get(req.service_type)})",
|
||||
)
|
||||
|
||||
return {"message": "增值服务已创建", "id": service.id}
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_service(
|
||||
id: str,
|
||||
req: UpdateServiceRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新增值服务"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
service = db.query(ValueAddedService).filter(ValueAddedService.id == id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="增值服务不存在")
|
||||
|
||||
if req.name is not None:
|
||||
service.name = req.name
|
||||
if req.price is not None:
|
||||
service.price = req.price
|
||||
if req.service_type is not None:
|
||||
if req.service_type not in SERVICE_TYPE_MAP:
|
||||
raise HTTPException(status_code=400, detail=f"无效的服务类型")
|
||||
service.type = req.service_type
|
||||
if req.description is not None:
|
||||
service.description = req.description
|
||||
if req.features is not None:
|
||||
try:
|
||||
service.features = json.loads(req.features) if isinstance(req.features, str) else req.features
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
service.features = [req.features]
|
||||
if req.status is not None:
|
||||
service.status = req.status
|
||||
if req.assigned_agent_id is not None:
|
||||
service.assigned_agent_id = req.assigned_agent_id
|
||||
service.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {admin_user.username} updated service: {id}")
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_service",
|
||||
target_type="value_added_service",
|
||||
target_id=id,
|
||||
detail=f"更新增值服务 {service.name}",
|
||||
)
|
||||
|
||||
return {"message": "增值服务已更新"}
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_service(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""逻辑删除增值服务(设置为 deleted 状态,保留数据记录)"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
service = db.query(ValueAddedService).filter(ValueAddedService.id == id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="增值服务不存在")
|
||||
|
||||
if service.status == "deleted":
|
||||
raise HTTPException(status_code=400, detail="该增值服务已被删除")
|
||||
|
||||
logger.info(f"Admin {admin_user.username} soft-deleting service: {id} - {service.name}")
|
||||
service.status = "deleted"
|
||||
service.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="delete_service",
|
||||
target_type="value_added_service",
|
||||
target_id=id,
|
||||
detail=f"逻辑删除增值服务 {service.name}",
|
||||
)
|
||||
|
||||
return {"message": "增值服务已删除(逻辑删除)"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 定制服务工单管理 (CustomServiceRequest)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/custom-requests")
|
||||
async def get_custom_requests(
|
||||
request_type: str = Query(None, description="工单类型: data_custom/private_project/platform_proxy"),
|
||||
status: str = Query(None, description="状态: pending/processing/completed/rejected"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取定制服务工单列表"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Admin {admin_user.username} requested custom requests: type={request_type}, status={status}")
|
||||
|
||||
query = db.query(CustomServiceRequest)
|
||||
|
||||
if request_type:
|
||||
query = query.filter(CustomServiceRequest.request_type == request_type)
|
||||
if status:
|
||||
query = query.filter(CustomServiceRequest.status == status)
|
||||
|
||||
requests = query.order_by(CustomServiceRequest.created_at.desc()).all()
|
||||
result = []
|
||||
for r in requests:
|
||||
# 查询用户名
|
||||
user = db.query(User).filter(User.id == r.user_id).first()
|
||||
# 查询服务名
|
||||
service = db.query(ValueAddedService).filter(ValueAddedService.id == r.service_id).first()
|
||||
result.append({
|
||||
"id": r.id,
|
||||
"user_id": r.user_id,
|
||||
"username": user.username if user else "未知用户",
|
||||
"service_id": r.service_id,
|
||||
"service_name": service.name if service else "未知服务",
|
||||
"request_type": r.request_type,
|
||||
"request_type_name": SERVICE_TYPE_MAP.get(r.request_type, r.request_type),
|
||||
"title": r.title,
|
||||
"description": r.description,
|
||||
"requirements": r.requirements or {},
|
||||
"status": r.status,
|
||||
"assigned_agent_id": r.assigned_agent_id,
|
||||
"created_at": r.created_at.isoformat() if r.created_at else None,
|
||||
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/custom-requests/{id}")
|
||||
async def update_custom_request(
|
||||
id: str,
|
||||
status: str = None,
|
||||
assigned_agent_id: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新定制服务工单(分配Agent / 标记完成)"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
req = db.query(CustomServiceRequest).filter(CustomServiceRequest.id == id).first()
|
||||
if not req:
|
||||
raise HTTPException(status_code=404, detail="工单不存在")
|
||||
|
||||
if status is not None:
|
||||
if status not in ("pending", "processing", "completed", "rejected"):
|
||||
raise HTTPException(status_code=400, detail="无效的状态值")
|
||||
req.status = status
|
||||
if assigned_agent_id is not None:
|
||||
req.assigned_agent_id = assigned_agent_id
|
||||
req.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {admin_user.username} updated custom request: {id}, status={status}, agent={assigned_agent_id}")
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_custom_request",
|
||||
target_type="custom_service_request",
|
||||
target_id=id,
|
||||
detail=f"更新工单状态: {status}, 分配Agent: {assigned_agent_id}",
|
||||
)
|
||||
|
||||
return {"message": "工单已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 定制服务订单管理 (CustomServiceOrder - 保留兼容)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/custom-orders")
|
||||
async def get_custom_orders(
|
||||
status: str = Query(None, description="状态筛选"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取定制服务订单列表"""
|
||||
query = db.query(CustomServiceOrder)
|
||||
|
||||
if status:
|
||||
query = query.filter(CustomServiceOrder.status == status)
|
||||
|
||||
orders = query.order_by(CustomServiceOrder.created_at.desc()).all()
|
||||
result = []
|
||||
for o in orders:
|
||||
user = db.query(User).filter(User.id == o.user_id).first()
|
||||
result.append({
|
||||
"id": o.id,
|
||||
"user_id": o.user_id,
|
||||
"username": user.username if user else "未知用户",
|
||||
"service_id": o.service_id,
|
||||
"service_type": o.service_type,
|
||||
"description": o.description,
|
||||
"status": o.status,
|
||||
"price": float(o.price) if o.price else 0,
|
||||
"order_id": o.order_id,
|
||||
"created_at": o.created_at.isoformat() if o.created_at else None,
|
||||
"updated_at": o.updated_at.isoformat() if o.updated_at else None,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/custom-orders/{id}")
|
||||
async def update_custom_order(
|
||||
id: str,
|
||||
status: str = None,
|
||||
price: float = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新定制服务订单状态"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
order = db.query(CustomServiceOrder).filter(CustomServiceOrder.id == id).first()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="定制服务订单不存在")
|
||||
|
||||
if status is not None:
|
||||
order.status = status
|
||||
if price is not None:
|
||||
order.price = price
|
||||
order.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {admin_user.username} updated custom order: {id}, status={status}")
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_custom_order",
|
||||
target_type="custom_service_order",
|
||||
target_id=id,
|
||||
detail=f"更新定制服务订单状态为 {status}",
|
||||
)
|
||||
|
||||
return {"message": "定制服务订单已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Agent 配置(为应用定制服务绑定市场Agent)
|
||||
# ============================================================
|
||||
|
||||
@router.get("/agents")
|
||||
async def get_available_agents(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取可用的市场Agent列表(用于绑定到应用定制服务)"""
|
||||
from sqlalchemy import or_
|
||||
agents = db.query(MarketApp).filter(
|
||||
MarketApp.type == "agent",
|
||||
or_(MarketApp.status == "published", MarketApp.status == "active")
|
||||
).all()
|
||||
|
||||
result = []
|
||||
for a in agents:
|
||||
result.append({
|
||||
"id": a.id,
|
||||
"name": a.name,
|
||||
"description": a.description,
|
||||
"publisher_name": a.publisher_name,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/{id}/agent")
|
||||
async def assign_agent_to_service(
|
||||
id: str,
|
||||
agent_id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""为应用定制服务绑定Agent"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
service = db.query(ValueAddedService).filter(ValueAddedService.id == id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="增值服务不存在")
|
||||
|
||||
# 验证agent存在
|
||||
agent = db.query(MarketApp).filter(
|
||||
MarketApp.id == agent_id,
|
||||
MarketApp.type == "agent"
|
||||
).first()
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent不存在")
|
||||
|
||||
service.assigned_agent_id = agent_id
|
||||
service.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {admin_user.username} assigned agent {agent_id} to service {id}")
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="assign_agent",
|
||||
target_type="value_added_service",
|
||||
target_id=id,
|
||||
detail=f"为服务 {service.name} 绑定Agent: {agent.name}",
|
||||
)
|
||||
|
||||
return {"message": "Agent绑定成功", "agent_name": agent.name}
|
||||
@@ -0,0 +1,508 @@
|
||||
"""
|
||||
网站基础设置 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import SiteSetting, EmailConfig, SmsConfig, User, AIModel
|
||||
from ...auth import get_current_admin
|
||||
from ...middleware.log_service import LogService
|
||||
import uuid
|
||||
import smtplib
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import urllib.parse
|
||||
import time as time_module
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("aiflow")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Pydantic Models
|
||||
# ============================================================
|
||||
|
||||
class EmailConfigUpdate(BaseModel):
|
||||
smtp_host: Optional[str] = None
|
||||
smtp_port: Optional[int] = None
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
from_address: Optional[str] = None
|
||||
use_tls: Optional[bool] = None
|
||||
|
||||
|
||||
class SmsConfigUpdate(BaseModel):
|
||||
provider: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
api_secret: Optional[str] = None
|
||||
sign_name: Optional[str] = None
|
||||
template_code: Optional[str] = None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 网站设置
|
||||
# ============================================================
|
||||
|
||||
@router.get("/site")
|
||||
async def get_site_settings(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取网站设置(返回所有 key-value 对)"""
|
||||
settings = db.query(SiteSetting).all()
|
||||
result = {}
|
||||
for s in settings:
|
||||
result[s.key] = s.value
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.put("/site")
|
||||
async def update_site_settings(
|
||||
settings: dict,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""批量更新网站设置"""
|
||||
for key, value in settings.items():
|
||||
existing = db.query(SiteSetting).filter(SiteSetting.key == key).first()
|
||||
if existing:
|
||||
existing.value = str(value) if value is not None else None
|
||||
existing.updated_at = datetime.now()
|
||||
else:
|
||||
new_setting = SiteSetting(
|
||||
id=str(uuid.uuid4()),
|
||||
key=key,
|
||||
value=str(value) if value is not None else None,
|
||||
)
|
||||
db.add(new_setting)
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_site_settings",
|
||||
target_type="site_settings",
|
||||
detail=f"更新网站设置: {list(settings.keys())}",
|
||||
)
|
||||
|
||||
return {"message": "网站设置已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 邮件配置
|
||||
# ============================================================
|
||||
|
||||
@router.get("/email")
|
||||
async def get_email_config(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取邮件配置"""
|
||||
config = db.query(EmailConfig).first()
|
||||
if not config:
|
||||
return {"code": 0, "data": {
|
||||
"smtp_host": "",
|
||||
"smtp_port": 587,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"from_address": "",
|
||||
"use_tls": True,
|
||||
}, "message": "ok"}
|
||||
|
||||
return {"code": 0, "data": {
|
||||
"id": config.id,
|
||||
"smtp_host": config.smtp_host,
|
||||
"smtp_port": config.smtp_port,
|
||||
"username": config.username,
|
||||
"password": config.password,
|
||||
"from_address": config.from_address,
|
||||
"use_tls": config.use_tls,
|
||||
}, "message": "ok"}
|
||||
|
||||
|
||||
@router.put("/email")
|
||||
async def update_email_config(
|
||||
req: EmailConfigUpdate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新邮件配置"""
|
||||
config = db.query(EmailConfig).first()
|
||||
if not config:
|
||||
# 必填字段校验:不允许空值
|
||||
if not req.smtp_host:
|
||||
raise HTTPException(status_code=400, detail="SMTP服务器地址不能为空")
|
||||
if not req.username:
|
||||
raise HTTPException(status_code=400, detail="SMTP用户名不能为空")
|
||||
if not req.password:
|
||||
raise HTTPException(status_code=400, detail="SMTP密码不能为空")
|
||||
if not req.from_address:
|
||||
raise HTTPException(status_code=400, detail="发件地址不能为空")
|
||||
config = EmailConfig(
|
||||
id=str(uuid.uuid4()),
|
||||
smtp_host=req.smtp_host or "",
|
||||
smtp_port=req.smtp_port or 587,
|
||||
username=req.username or "",
|
||||
password=req.password or "",
|
||||
from_address=req.from_address or "",
|
||||
use_tls=req.use_tls if req.use_tls is not None else True,
|
||||
)
|
||||
db.add(config)
|
||||
else:
|
||||
if req.smtp_host is not None:
|
||||
if not req.smtp_host.strip():
|
||||
raise HTTPException(status_code=400, detail="SMTP服务器地址不能为空")
|
||||
config.smtp_host = req.smtp_host
|
||||
if req.smtp_port is not None:
|
||||
config.smtp_port = req.smtp_port
|
||||
if req.username is not None:
|
||||
if not req.username.strip():
|
||||
raise HTTPException(status_code=400, detail="SMTP用户名不能为空")
|
||||
config.username = req.username
|
||||
if req.password is not None:
|
||||
if not req.password.strip():
|
||||
raise HTTPException(status_code=400, detail="SMTP密码不能为空")
|
||||
config.password = req.password
|
||||
if req.from_address is not None:
|
||||
if not req.from_address.strip():
|
||||
raise HTTPException(status_code=400, detail="发件地址不能为空")
|
||||
config.from_address = req.from_address
|
||||
if req.use_tls is not None:
|
||||
config.use_tls = req.use_tls
|
||||
config.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_email_config",
|
||||
target_type="email_config",
|
||||
detail="更新邮件配置",
|
||||
)
|
||||
|
||||
return {"code": 0, "data": None, "message": "邮件配置已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 短信配置
|
||||
# ============================================================
|
||||
|
||||
@router.get("/sms")
|
||||
async def get_sms_config(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取短信配置"""
|
||||
config = db.query(SmsConfig).first()
|
||||
if not config:
|
||||
return {"code": 0, "data": {
|
||||
"provider": "",
|
||||
"api_key": "",
|
||||
"api_secret": "",
|
||||
"sign_name": "",
|
||||
"template_code": "",
|
||||
}, "message": "ok"}
|
||||
|
||||
return {"code": 0, "data": {
|
||||
"id": config.id,
|
||||
"provider": config.provider,
|
||||
"api_key": config.api_key,
|
||||
"api_secret": config.api_secret,
|
||||
"sign_name": config.sign_name,
|
||||
"template_code": config.template_code,
|
||||
}, "message": "ok"}
|
||||
|
||||
|
||||
@router.put("/sms")
|
||||
async def update_sms_config(
|
||||
req: SmsConfigUpdate,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新短信配置"""
|
||||
config = db.query(SmsConfig).first()
|
||||
if not config:
|
||||
config = SmsConfig(
|
||||
id=str(uuid.uuid4()),
|
||||
provider=req.provider or "",
|
||||
api_key=req.api_key or "",
|
||||
api_secret=req.api_secret or "",
|
||||
sign_name=req.sign_name,
|
||||
template_code=req.template_code,
|
||||
)
|
||||
db.add(config)
|
||||
else:
|
||||
if req.provider is not None:
|
||||
config.provider = req.provider
|
||||
if req.api_key is not None:
|
||||
config.api_key = req.api_key
|
||||
if req.api_secret is not None:
|
||||
config.api_secret = req.api_secret
|
||||
if req.sign_name is not None:
|
||||
config.sign_name = req.sign_name
|
||||
if req.template_code is not None:
|
||||
config.template_code = req.template_code
|
||||
config.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_sms_config",
|
||||
target_type="sms_config",
|
||||
detail="更新短信配置",
|
||||
)
|
||||
|
||||
return {"message": "短信配置已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 邮件测试
|
||||
# ============================================================
|
||||
|
||||
@router.post("/email/test")
|
||||
async def test_email_config(
|
||||
test_to: str = Query(default=None, description="自定义测试收件地址"),
|
||||
test_subject: str = Query(default=None, description="自定义测试邮件主题"),
|
||||
test_body: str = Query(default=None, description="自定义测试邮件正文"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""使用当前配置发送测试邮件;可传参自定义收件人、主题和正文"""
|
||||
config = db.query(EmailConfig).first()
|
||||
if not config or not config.smtp_host:
|
||||
raise HTTPException(status_code=400, detail="邮件配置未设置,请先保存邮件配置")
|
||||
if not config.username:
|
||||
raise HTTPException(status_code=400, detail="SMTP用户名未填写,请检查邮件配置")
|
||||
if not config.password:
|
||||
raise HTTPException(status_code=400, detail="SMTP密码未填写,请检查邮件配置")
|
||||
if not config.from_address:
|
||||
raise HTTPException(status_code=400, detail="发件地址未填写,请检查邮件配置")
|
||||
|
||||
# 优先级:传入参数 > 管理员邮箱 > 发件地址
|
||||
admin = db.query(User).filter(User.id == admin_user.id).first()
|
||||
recipient = test_to or (admin.email if admin and admin.email else None) or config.from_address
|
||||
if not recipient:
|
||||
raise HTTPException(status_code=400, detail="无法获取接收地址: 请填写收件邮箱或设置管理员邮箱")
|
||||
|
||||
subject = test_subject or "AIFlow 邮件配置测试"
|
||||
body = test_body or f"这是一封来自 AIFlow 平台的测试邮件。\n\n发送时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\nSMTP服务器: {config.smtp_host}"
|
||||
|
||||
logger.info(f"测试邮件: SMTP={config.smtp_host}:{config.smtp_port}, "
|
||||
f"from={config.from_address}, to={recipient}")
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = config.from_address
|
||||
msg["To"] = recipient
|
||||
msg["Subject"] = subject
|
||||
msg.attach(MIMEText(body, "plain"))
|
||||
|
||||
if config.use_tls:
|
||||
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10)
|
||||
server.starttls()
|
||||
else:
|
||||
server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10)
|
||||
|
||||
server.login(config.username, config.password)
|
||||
server.sendmail(config.from_address, recipient, msg.as_string())
|
||||
server.quit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="test_email_config",
|
||||
target_type="email_config",
|
||||
detail=f"测试邮件发送成功 -> {recipient} via {config.smtp_host}",
|
||||
)
|
||||
|
||||
return {"code": 0, "data": {"message": f"测试邮件已发送到 {recipient}", "recipient": recipient}, "message": "ok"}
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
logger.error(f"SMTP认证失败: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"SMTP认证失败,请检查用户名和密码: {str(e)}")
|
||||
except smtplib.SMTPConnectError as e:
|
||||
logger.error(f"SMTP连接失败: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"无法连接到SMTP服务器 {config.smtp_host}:{config.smtp_port}: {str(e)}")
|
||||
except smtplib.SMTPException as e:
|
||||
logger.error(f"SMTP发送失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"邮件发送失败(SMTP): {str(e)}")
|
||||
except OSError as e:
|
||||
logger.error(f"邮件发送网络错误: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"网络连接错误: {str(e)}")
|
||||
except BaseException as e:
|
||||
logger.error(f"测试邮件发送失败(严重): {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=f"邮件发送失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/email/generate-content")
|
||||
async def generate_email_content(
|
||||
topic: str = Query(default="测试邮件"),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""AI生成测试邮件内容:调用默认LLM生成邮件主题和正文"""
|
||||
import httpx as httpx_lib
|
||||
subjects = [
|
||||
"【测试】AIFlow邮件服务配置验证",
|
||||
"AIFlow平台 - 邮件投递测试",
|
||||
"测试邮件:您的SMTP服务已就绪",
|
||||
]
|
||||
bodies = [
|
||||
f"您好!\n\n这是一封来自 AIFlow 智能自动化平台的测试邮件,用于验证邮件服务配置是否正常。\n\n如果您收到此邮件,说明SMTP邮件服务已配置成功。\n\n此致\nAIFlow 平台团队",
|
||||
f"Hi,\n\nThis is a test email from AIFlow Platform.\n\nIf you received this, the SMTP service works fine.\n\nBest regards,\nAIFlow Team",
|
||||
f"【系统通知】\n邮件服务连通性测试通过。\n\n发件服务器配置正确,能够成功投递邮件。\n\n—— AIFlow",
|
||||
]
|
||||
|
||||
# 尝试使用默认LLM生成更好的内容
|
||||
try:
|
||||
default_model = db.query(AIModel).filter(
|
||||
AIModel.is_default == True, AIModel.is_enabled == True
|
||||
).first()
|
||||
if default_model and default_model.endpoint:
|
||||
async with httpx_lib.AsyncClient(timeout=15) as client:
|
||||
headers = {}
|
||||
if default_model.api_key:
|
||||
headers["Authorization"] = f"Bearer {default_model.api_key}"
|
||||
resp = await client.post(
|
||||
default_model.endpoint,
|
||||
json={
|
||||
"model": default_model.name,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个专业的技术文档撰写者。请根据主题生成一封测试邮件的主题和正文。返回JSON格式: {\"subject\": \"邮件主题\", \"body\": \"邮件正文\"}"},
|
||||
{"role": "user", "content": f"主题: {topic}"},
|
||||
],
|
||||
"max_tokens": 400,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
try:
|
||||
data = json.loads(content)
|
||||
if data.get("subject") and data.get("body"):
|
||||
return {"subject": data["subject"], "body": data["body"], "source": "ai"}
|
||||
except:
|
||||
# 尝试解析非JSON格式
|
||||
lines = content.strip().split("\n")
|
||||
if len(lines) >= 2:
|
||||
return {"subject": lines[0].replace("主题:", "").strip(), "body": "\n".join(lines[1:]).replace("正文:", "").strip(), "source": "ai_partial"}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import random
|
||||
idx = random.randint(0, len(subjects) - 1)
|
||||
return {"subject": subjects[idx], "body": bodies[idx], "source": "fallback"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 短信测试
|
||||
# ============================================================
|
||||
|
||||
def _aliyun_sign(secret: str, params: dict, method: str = "GET") -> str:
|
||||
"""阿里云签名机制 (HMAC-SHA1)"""
|
||||
sorted_keys = sorted(params.keys())
|
||||
qs = "&".join([
|
||||
f"{urllib.parse.quote(k, safe='')}={urllib.parse.quote(str(params[k]), safe='')}"
|
||||
for k in sorted_keys
|
||||
])
|
||||
string_to_sign = f"{method}&{urllib.parse.quote('/', safe='')}&{urllib.parse.quote(qs, safe='')}"
|
||||
key = f"{secret}&"
|
||||
h = hmac.new(key.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha1)
|
||||
return base64.b64encode(h.digest()).decode('utf-8')
|
||||
|
||||
|
||||
@router.post("/sms/test")
|
||||
async def test_sms_config(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""使用当前配置发送测试短信"""
|
||||
config = db.query(SmsConfig).first()
|
||||
if not config:
|
||||
raise HTTPException(status_code=400, detail="短信配置未设置,请先保存短信配置")
|
||||
|
||||
if not config.api_key or not config.api_secret:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="未配置阿里云短信 AccessKey,请填写 API Key (AccessKey ID) 和 API Secret (AccessKey Secret)"
|
||||
)
|
||||
|
||||
if not config.sign_name:
|
||||
raise HTTPException(status_code=400, detail="未配置短信签名")
|
||||
|
||||
# 获取接收测试短信的手机号(管理员手机号)
|
||||
admin = db.query(User).filter(User.id == admin_user.id).first()
|
||||
phone = admin.phone if admin and admin.phone else None
|
||||
if not phone:
|
||||
raise HTTPException(status_code=400, detail="管理员未设置手机号,无法发送测试短信")
|
||||
|
||||
logger.info(f"测试短信: provider={config.provider}, sign={config.sign_name}, phone={phone}")
|
||||
|
||||
# 只有阿里云短信支持测试
|
||||
if "阿里云" not in (config.provider or ""):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"当前短信服务商 '{config.provider}' 暂不支持在线测试,请直接在生产环境测试"
|
||||
)
|
||||
|
||||
try:
|
||||
# 阿里云短信参数
|
||||
params = {
|
||||
"AccessKeyId": config.api_key,
|
||||
"Action": "SendSms",
|
||||
"Format": "JSON",
|
||||
"PhoneNumbers": phone,
|
||||
"RegionId": "cn-hangzhou",
|
||||
"SignName": config.sign_name,
|
||||
"SignatureMethod": "HMAC-SHA1",
|
||||
"SignatureNonce": str(uuid.uuid4()),
|
||||
"SignatureVersion": "1.0",
|
||||
"TemplateCode": config.template_code or "SMS_00000001",
|
||||
"TemplateParam": '{"code":"123456"}',
|
||||
"Timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"Version": "2017-05-25",
|
||||
}
|
||||
params["Signature"] = _aliyun_sign(config.api_secret, params)
|
||||
|
||||
import requests as http_requests
|
||||
resp = http_requests.post(
|
||||
"https://dysmsapi.aliyuncs.com/",
|
||||
data=params,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=10,
|
||||
)
|
||||
result = resp.json()
|
||||
|
||||
logger.info(f"阿里云短信API响应: {result}")
|
||||
|
||||
if result.get("Code") == "OK":
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="test_sms_config",
|
||||
target_type="sms_config",
|
||||
detail=f"测试短信发送成功 -> {phone}",
|
||||
)
|
||||
return {"message": f"测试短信已发送到 {phone}", "phone": phone, "biz_id": result.get("BizId")}
|
||||
else:
|
||||
error_msg = result.get("Message", result.get("Code", "未知错误"))
|
||||
raise HTTPException(status_code=400, detail=f"阿里云短信发送失败: {error_msg}")
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"测试短信发送失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"短信发送失败: {str(e)}")
|
||||
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
用户管理 API(管理员)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from ...database import get_db, SessionLocal
|
||||
from ...models import User, RegisterSetting
|
||||
from ...auth import get_current_admin, hash_password, verify_password
|
||||
from ...middleware.log_service import LogService
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AdminChangePasswordRequest(BaseModel):
|
||||
old_password: str
|
||||
new_password: str = Field(min_length=6, max_length=100)
|
||||
|
||||
|
||||
class ResetUserPasswordRequest(BaseModel):
|
||||
new_password: str = Field(min_length=6, max_length=100)
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_users(
|
||||
status: str = Query(None, description="用户状态筛选: active/banned"),
|
||||
membership_level: int = Query(None, description="会员等级筛选"),
|
||||
search: str = Query(None, description="搜索用户名或邮箱"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取用户列表(管理员)"""
|
||||
query = db.query(User)
|
||||
|
||||
if status:
|
||||
query = query.filter(User.status == status)
|
||||
if membership_level is not None:
|
||||
query = query.filter(User.membership_level == membership_level)
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
User.username.ilike(f"%{search}%"),
|
||||
User.email.ilike(f"%{search}%"),
|
||||
)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
users = query.order_by(User.created_at.desc()).offset(
|
||||
(page - 1) * page_size
|
||||
).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": [
|
||||
{
|
||||
"id": u.id,
|
||||
"username": u.username,
|
||||
"email": u.email,
|
||||
"phone": u.phone,
|
||||
"role": u.role,
|
||||
"status": u.status,
|
||||
"real_name_status": u.real_name_status,
|
||||
"real_name": u.real_name,
|
||||
"membership_level": u.membership_level,
|
||||
"membership_expire_at": u.membership_expire_at.isoformat() if u.membership_expire_at else None,
|
||||
"auto_renew": u.auto_renew,
|
||||
"created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
"updated_at": u.updated_at.isoformat() if u.updated_at else None,
|
||||
}
|
||||
for u in users
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 静态路由 - 必须放在 /{id} 动态路由之前
|
||||
# ============================================================
|
||||
|
||||
@router.get("/register-settings")
|
||||
async def get_register_settings(
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取注册设置"""
|
||||
setting = db.query(RegisterSetting).first()
|
||||
if not setting:
|
||||
return {
|
||||
"phone_required": True,
|
||||
"phone_sms_required": True,
|
||||
"email_required": True,
|
||||
"email_verify_required": True,
|
||||
"real_name_required": False,
|
||||
"aliyun_access_key": "",
|
||||
"aliyun_endpoint": "https://dysmsapi.aliyuncs.com",
|
||||
"sms_sign_name": "",
|
||||
"sms_template_code": "",
|
||||
}
|
||||
|
||||
return {
|
||||
"id": setting.id,
|
||||
"phone_required": setting.phone_required,
|
||||
"phone_sms_required": setting.phone_sms_required,
|
||||
"email_required": setting.email_required,
|
||||
"email_verify_required": setting.email_verify_required,
|
||||
"real_name_required": setting.real_name_required,
|
||||
"aliyun_access_key": setting.aliyun_access_key,
|
||||
"aliyun_endpoint": setting.aliyun_endpoint,
|
||||
"sms_sign_name": getattr(setting, 'sms_sign_name', ''),
|
||||
"sms_template_code": getattr(setting, 'sms_template_code', ''),
|
||||
}
|
||||
|
||||
|
||||
@router.put("/register-settings")
|
||||
async def update_register_settings(
|
||||
phone_required: bool = None,
|
||||
phone_sms_required: bool = None,
|
||||
email_required: bool = None,
|
||||
email_verify_required: bool = None,
|
||||
real_name_required: bool = None,
|
||||
aliyun_access_key: str = None,
|
||||
aliyun_secret_key: str = None,
|
||||
aliyun_endpoint: str = None,
|
||||
sms_sign_name: str = None,
|
||||
sms_template_code: str = None,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新注册设置"""
|
||||
setting = db.query(RegisterSetting).first()
|
||||
if not setting:
|
||||
setting = RegisterSetting(
|
||||
id=str(uuid.uuid4()),
|
||||
phone_required=phone_required if phone_required is not None else True,
|
||||
phone_sms_required=phone_sms_required if phone_sms_required is not None else True,
|
||||
email_required=email_required if email_required is not None else True,
|
||||
email_verify_required=email_verify_required if email_verify_required is not None else True,
|
||||
real_name_required=real_name_required or False,
|
||||
aliyun_access_key=aliyun_access_key,
|
||||
aliyun_secret_key=aliyun_secret_key,
|
||||
aliyun_endpoint=aliyun_endpoint,
|
||||
sms_sign_name=sms_sign_name,
|
||||
sms_template_code=sms_template_code,
|
||||
)
|
||||
db.add(setting)
|
||||
else:
|
||||
if phone_required is not None:
|
||||
setting.phone_required = phone_required
|
||||
if phone_sms_required is not None:
|
||||
setting.phone_sms_required = phone_sms_required
|
||||
if email_required is not None:
|
||||
setting.email_required = email_required
|
||||
if email_verify_required is not None:
|
||||
setting.email_verify_required = email_verify_required
|
||||
if real_name_required is not None:
|
||||
setting.real_name_required = real_name_required
|
||||
if aliyun_access_key is not None:
|
||||
setting.aliyun_access_key = aliyun_access_key
|
||||
if aliyun_secret_key is not None:
|
||||
setting.aliyun_secret_key = aliyun_secret_key
|
||||
if aliyun_endpoint is not None:
|
||||
setting.aliyun_endpoint = aliyun_endpoint
|
||||
if sms_sign_name is not None:
|
||||
setting.sms_sign_name = sms_sign_name
|
||||
if sms_template_code is not None:
|
||||
setting.sms_template_code = sms_template_code
|
||||
setting.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="update_register_settings",
|
||||
target_type="register_setting",
|
||||
detail="更新注册设置",
|
||||
)
|
||||
|
||||
return {"message": "注册设置已更新"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 动态路由
|
||||
# ============================================================
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_user_detail(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取用户详情"""
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"phone": user.phone,
|
||||
"avatar": user.avatar,
|
||||
"role": user.role,
|
||||
"status": user.status,
|
||||
"real_name_status": user.real_name_status,
|
||||
"real_name": user.real_name,
|
||||
"id_card": user.id_card,
|
||||
"membership_plan_id": user.membership_plan_id,
|
||||
"membership_level": user.membership_level,
|
||||
"membership_expire_at": user.membership_expire_at.isoformat() if user.membership_expire_at else None,
|
||||
"auto_renew": user.auto_renew,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None,
|
||||
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{id}/ban")
|
||||
async def ban_user(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""封禁用户"""
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if user.role == "admin":
|
||||
raise HTTPException(status_code=400, detail="不能封禁管理员账号")
|
||||
|
||||
user.status = "banned"
|
||||
user.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="ban_user",
|
||||
target_type="user",
|
||||
target_id=id,
|
||||
detail=f"封禁用户 {user.username}",
|
||||
)
|
||||
|
||||
return {"message": "用户已封禁"}
|
||||
|
||||
|
||||
@router.put("/{id}/unban")
|
||||
async def unban_user(
|
||||
id: str,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""解封用户"""
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
user.status = "active"
|
||||
user.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
LogService.log_audit(
|
||||
admin_id=admin_user.id,
|
||||
admin_name=admin_user.username,
|
||||
action="unban_user",
|
||||
target_type="user",
|
||||
target_id=id,
|
||||
detail=f"解封用户 {user.username}",
|
||||
)
|
||||
|
||||
return {"message": "用户已解封"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 密码管理
|
||||
# ============================================================
|
||||
|
||||
@router.put("/change-password")
|
||||
def admin_change_password(
|
||||
req: AdminChangePasswordRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""管理员修改自己的密码"""
|
||||
db_user = db.query(User).filter(User.id == admin_user.id).first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
if not verify_password(req.old_password, db_user.password_hash):
|
||||
raise HTTPException(status_code=400, detail="原密码错误")
|
||||
|
||||
db_user.password_hash = hash_password(req.new_password)
|
||||
db_user.updated_at = datetime.now()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=admin_user.id, username=admin_user.username, action="admin_change_password",
|
||||
target="user", target_id=admin_user.id,
|
||||
detail="管理员修改了自己的密码",
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "密码修改成功"}
|
||||
|
||||
|
||||
@router.put("/{id}/reset-password")
|
||||
def admin_reset_user_password(
|
||||
id: str,
|
||||
req: ResetUserPasswordRequest,
|
||||
admin_user=Depends(get_current_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""管理员重置用户密码"""
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
user.password_hash = hash_password(req.new_password)
|
||||
user.updated_at = datetime.now()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=admin_user.id, username=admin_user.username, action="reset_user_password",
|
||||
target="user", target_id=id,
|
||||
detail=f"管理员重置了用户 {user.username} 的密码",
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "密码已重置"}
|
||||
@@ -0,0 +1,379 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, desc, asc
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
User, Asset, AssetCategory, NovelChapter, KnowledgeDocument,
|
||||
KnowledgeBase,
|
||||
gen_uuid
|
||||
)
|
||||
from ..auth import get_current_user
|
||||
from ..middleware.log_service import LogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CreateAssetRequest(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=200)
|
||||
category: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
file_type: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
|
||||
|
||||
class UpdateAssetRequest(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
file_type: Optional[str] = None
|
||||
|
||||
|
||||
class CreateChapterRequest(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=500)
|
||||
content: Optional[str] = None
|
||||
sort_order: int = Field(default=0)
|
||||
|
||||
|
||||
VALID_ASSET_CATEGORIES = ("file", "novel", "image", "video", "audio", "custom")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 静态路由 - 必须在动态路由之前定义
|
||||
# ============================================================
|
||||
|
||||
@router.get("/storage-overview")
|
||||
def get_storage_overview(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取用户资产存储概览,包含资产和知识库文档的总存储量"""
|
||||
# Asset表中的文件大小
|
||||
from sqlalchemy import func as sa_func
|
||||
asset_size = db.query(sa_func.sum(Asset.size)).filter(
|
||||
and_(
|
||||
Asset.user_id == current_user.id,
|
||||
Asset.status != "deleted",
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
# 知识库文档的文件大小
|
||||
kb_doc_size = db.query(sa_func.sum(KnowledgeDocument.file_size)).join(
|
||||
KnowledgeBase, KnowledgeDocument.knowledge_base_id == KnowledgeBase.id
|
||||
).filter(
|
||||
KnowledgeBase.user_id == current_user.id
|
||||
).scalar() or 0
|
||||
|
||||
total_storage = asset_size + kb_doc_size
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"asset_storage": asset_size,
|
||||
"knowledge_doc_storage": kb_doc_size,
|
||||
"total_storage": total_storage,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
def list_categories(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
categories = (
|
||||
db.query(AssetCategory)
|
||||
.filter(AssetCategory.user_id == current_user.id)
|
||||
.order_by(asc(AssetCategory.name))
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for c in categories:
|
||||
count = db.query(Asset).filter(
|
||||
and_(
|
||||
Asset.category == c.name,
|
||||
Asset.user_id == current_user.id,
|
||||
Asset.status != "deleted",
|
||||
)
|
||||
).count()
|
||||
|
||||
result.append({
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"type": c.type,
|
||||
"asset_count": count,
|
||||
"created_at": str(c.created_at) if c.created_at else None,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.get("/novels/{asset_id}/chapters")
|
||||
def get_novel_chapters(
|
||||
asset_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
asset = db.query(Asset).filter(
|
||||
and_(Asset.id == asset_id, Asset.user_id == current_user.id)
|
||||
).first()
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="资产不存在")
|
||||
|
||||
chapters = (
|
||||
db.query(NovelChapter)
|
||||
.filter(NovelChapter.asset_id == asset_id)
|
||||
.order_by(asc(NovelChapter.sort_order), asc(NovelChapter.created_at))
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for ch in chapters:
|
||||
result.append({
|
||||
"id": ch.id,
|
||||
"asset_id": ch.asset_id,
|
||||
"title": ch.title,
|
||||
"sort_order": ch.sort_order,
|
||||
"word_count": ch.word_count or 0,
|
||||
"created_at": str(ch.created_at) if ch.created_at else None,
|
||||
"updated_at": str(ch.updated_at) if ch.updated_at else None,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.post("/novels/{asset_id}/chapters")
|
||||
def add_novel_chapter(
|
||||
asset_id: str,
|
||||
req: CreateChapterRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
asset = db.query(Asset).filter(
|
||||
and_(Asset.id == asset_id, Asset.user_id == current_user.id)
|
||||
).first()
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="资产不存在")
|
||||
|
||||
chapter = NovelChapter(
|
||||
id=gen_uuid(),
|
||||
asset_id=asset_id,
|
||||
title=req.title,
|
||||
content=req.content,
|
||||
sort_order=req.sort_order,
|
||||
word_count=len(req.content or ""),
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(chapter)
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="add_chapter",
|
||||
target="novel_chapter", target_id=chapter.id,
|
||||
detail=f"为资产 {asset.name} 添加章节 {chapter.title}",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(chapter)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": chapter.id, "asset_id": chapter.asset_id, "title": chapter.title,
|
||||
"sort_order": chapter.sort_order, "word_count": chapter.word_count,
|
||||
"created_at": str(chapter.created_at),
|
||||
},
|
||||
"message": "章节添加成功",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 动态路由 - {asset_id} 必须放在静态路由之后
|
||||
# ============================================================
|
||||
|
||||
@router.get("/")
|
||||
def list_assets(
|
||||
category: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
query = db.query(Asset).filter(
|
||||
and_(
|
||||
Asset.user_id == current_user.id,
|
||||
Asset.status != "deleted",
|
||||
)
|
||||
)
|
||||
|
||||
if category:
|
||||
query = query.filter(Asset.category == category)
|
||||
if search:
|
||||
query = query.filter(Asset.name.ilike(f"%{search}%"))
|
||||
|
||||
total = query.count()
|
||||
assets = (
|
||||
query.order_by(desc(Asset.updated_at))
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for a in assets:
|
||||
result.append({
|
||||
"id": a.id,
|
||||
"name": a.name,
|
||||
"category": a.category,
|
||||
"file_type": a.file_type,
|
||||
"size": a.size or 0,
|
||||
"description": a.description,
|
||||
"url": a.url or "",
|
||||
"content": a.content or "",
|
||||
"status": a.status,
|
||||
"created_at": str(a.created_at) if a.created_at else None,
|
||||
"updated_at": str(a.updated_at) if a.updated_at else None,
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {"total": total, "page": page, "page_size": page_size, "items": result},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/")
|
||||
def create_asset(
|
||||
req: CreateAssetRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
cat = req.category if req.category in VALID_ASSET_CATEGORIES else "file"
|
||||
asset = Asset(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
name=req.name,
|
||||
category=cat,
|
||||
description=req.description or (req.content[:100] if req.content else None),
|
||||
file_type=req.file_type or "unknown",
|
||||
size=len(req.content.encode('utf-8')) if req.content else 0,
|
||||
content=req.content,
|
||||
status="active",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(asset)
|
||||
db.commit()
|
||||
db.refresh(asset)
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="create",
|
||||
target="asset", target_id=asset.id,
|
||||
detail=f"创建资产 {asset.name}",
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": asset.id, "name": asset.name, "category": asset.category,
|
||||
"description": asset.description, "status": asset.status,
|
||||
"created_at": str(asset.created_at),
|
||||
},
|
||||
"message": "创建成功",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{asset_id}")
|
||||
def get_asset(
|
||||
asset_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
asset = db.query(Asset).filter(
|
||||
and_(Asset.id == asset_id, Asset.user_id == current_user.id)
|
||||
).first()
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="资产不存在")
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": asset.id, "name": asset.name, "category": asset.category,
|
||||
"description": asset.description, "file_type": asset.file_type,
|
||||
"size": asset.size or 0,
|
||||
"url": asset.url or "",
|
||||
"content": asset.content or "",
|
||||
"status": asset.status,
|
||||
"created_at": str(asset.created_at), "updated_at": str(asset.updated_at),
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{asset_id}")
|
||||
def update_asset(
|
||||
asset_id: str,
|
||||
req: UpdateAssetRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
asset = db.query(Asset).filter(
|
||||
and_(Asset.id == asset_id, Asset.user_id == current_user.id)
|
||||
).first()
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="资产不存在")
|
||||
|
||||
if req.name is not None:
|
||||
asset.name = req.name
|
||||
if req.description is not None:
|
||||
asset.description = req.description
|
||||
if req.file_type is not None:
|
||||
asset.file_type = req.file_type
|
||||
asset.updated_at = datetime.now()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="update",
|
||||
target="asset", target_id=asset.id,
|
||||
detail=f"更新资产 {asset.name}",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(asset)
|
||||
|
||||
return {"code": 0, "data": {
|
||||
"id": asset.id, "name": asset.name, "status": asset.status,
|
||||
"updated_at": str(asset.updated_at),
|
||||
}, "message": "更新成功"}
|
||||
|
||||
|
||||
@router.delete("/{asset_id}")
|
||||
def delete_asset(
|
||||
asset_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
asset = db.query(Asset).filter(
|
||||
and_(Asset.id == asset_id, Asset.user_id == current_user.id)
|
||||
).first()
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="资产不存在")
|
||||
|
||||
# 软删除
|
||||
asset.status = "deleted"
|
||||
asset.updated_at = datetime.now()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="delete",
|
||||
target="asset", target_id=asset.id,
|
||||
detail=f"删除资产 {asset.name}",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "资产已删除"}
|
||||
@@ -0,0 +1,231 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
User, LoginLog, RegisterSetting,
|
||||
gen_uuid
|
||||
)
|
||||
from ..auth import get_current_user, hash_password, verify_password, create_access_token
|
||||
from ..middleware.log_service import LogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
username: str = Field(min_length=3, max_length=50)
|
||||
password: str = Field(min_length=6, max_length=100)
|
||||
email: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
def login(req: LoginRequest, db: Session = Depends(get_db)):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
user = db.query(User).filter(User.username == req.username).first()
|
||||
if not user:
|
||||
logger.warning(f"登录失败: 用户 '{req.username}' 不存在")
|
||||
raise HTTPException(status_code=401, detail="用户名不存在")
|
||||
|
||||
stored_hash = user.password_hash
|
||||
logger.info(f"登录调试: username={req.username} hash_prefix={stored_hash[:7] if stored_hash else 'None'} hash_len={len(stored_hash) if stored_hash else 0}")
|
||||
|
||||
if not verify_password(req.password, user.password_hash):
|
||||
logger.warning(f"登录失败: 用户 '{req.username}' 密码验证失败 hash_prefix={stored_hash[:7] if stored_hash else 'None'} hash_len={len(stored_hash) if stored_hash else 0}")
|
||||
raise HTTPException(status_code=401, detail="密码错误")
|
||||
if user.status == "banned":
|
||||
raise HTTPException(status_code=403, detail="账号已被封禁")
|
||||
|
||||
token_data = {"sub": user.id, "username": user.username, "role": user.role}
|
||||
access_token = create_access_token(token_data)
|
||||
|
||||
# 记录登录日志
|
||||
login_log = LoginLog(
|
||||
id=gen_uuid(),
|
||||
user_id=user.id,
|
||||
ip="",
|
||||
login_at=datetime.now(),
|
||||
)
|
||||
db.add(login_log)
|
||||
|
||||
# 记录操作日志
|
||||
LogService.log_operation(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
action="login",
|
||||
target="user",
|
||||
detail=f"用户 {user.username} 登录成功",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"phone": user.phone,
|
||||
"avatar": user.avatar,
|
||||
"role": user.role,
|
||||
"status": user.status,
|
||||
"membership_level": user.membership_level,
|
||||
"membership_expire_at": str(user.membership_expire_at) if user.membership_expire_at else None,
|
||||
"created_at": str(user.created_at) if user.created_at else None,
|
||||
},
|
||||
},
|
||||
"message": "登录成功",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
def register(req: RegisterRequest, db: Session = Depends(get_db)):
|
||||
# 检查注册设置
|
||||
reg_setting = db.query(RegisterSetting).first()
|
||||
if reg_setting and reg_setting.phone_required and not req.phone:
|
||||
raise HTTPException(status_code=400, detail="手机号为必填项")
|
||||
if reg_setting and reg_setting.email_required and not req.email:
|
||||
raise HTTPException(status_code=400, detail="邮箱为必填项")
|
||||
|
||||
# 检查用户名是否已存在
|
||||
existing = db.query(User).filter(User.username == req.username).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||
|
||||
if req.email:
|
||||
existing_email = db.query(User).filter(User.email == req.email).first()
|
||||
if existing_email:
|
||||
raise HTTPException(status_code=400, detail="邮箱已被注册")
|
||||
|
||||
if req.phone:
|
||||
existing_phone = db.query(User).filter(User.phone == req.phone).first()
|
||||
if existing_phone:
|
||||
raise HTTPException(status_code=400, detail="手机号已被注册")
|
||||
|
||||
user = User(
|
||||
id=gen_uuid(),
|
||||
username=req.username,
|
||||
password_hash=hash_password(req.password),
|
||||
email=req.email,
|
||||
phone=req.phone,
|
||||
avatar=None,
|
||||
role="user",
|
||||
status="active",
|
||||
real_name_status="unverified",
|
||||
membership_level=0,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(user)
|
||||
db.flush()
|
||||
|
||||
# 创建Token余额记录
|
||||
from ..models import TokenBalance
|
||||
balance = TokenBalance(
|
||||
id=gen_uuid(),
|
||||
user_id=user.id,
|
||||
total_consumed=0,
|
||||
balance=0,
|
||||
daily_free_used=0,
|
||||
)
|
||||
db.add(balance)
|
||||
|
||||
token_data = {"sub": user.id, "username": user.username, "role": user.role}
|
||||
access_token = create_access_token(token_data)
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
action="register",
|
||||
target="user",
|
||||
detail=f"新用户 {user.username} 注册成功",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email or "",
|
||||
"phone": user.phone or "",
|
||||
"avatar": user.avatar or "",
|
||||
"role": user.role,
|
||||
"status": user.status,
|
||||
"membership_level": user.membership_level,
|
||||
"created_at": str(user.created_at) if user.created_at else None,
|
||||
},
|
||||
},
|
||||
"message": "注册成功",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
def get_me(current_user: User = Depends(get_current_user)):
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": current_user.id,
|
||||
"username": current_user.username,
|
||||
"email": current_user.email or "",
|
||||
"phone": current_user.phone or "",
|
||||
"avatar": current_user.avatar or "",
|
||||
"role": current_user.role,
|
||||
"status": current_user.status,
|
||||
"real_name_status": current_user.real_name_status,
|
||||
"membership_level": current_user.membership_level,
|
||||
"membership_expire_at": str(current_user.membership_expire_at) if current_user.membership_expire_at else None,
|
||||
"auto_renew": current_user.auto_renew,
|
||||
"created_at": str(current_user.created_at) if current_user.created_at else None,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout(current_user: User = Depends(get_current_user)):
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id,
|
||||
username=current_user.username,
|
||||
action="logout",
|
||||
target="user",
|
||||
detail=f"用户 {current_user.username} 登出",
|
||||
)
|
||||
return {"code": 0, "message": "登出成功"}
|
||||
|
||||
|
||||
@router.get("/settings")
|
||||
def get_settings(db: Session = Depends(get_db)):
|
||||
settings = db.query(RegisterSetting).first()
|
||||
if not settings:
|
||||
return {"code": 0, "data": {}, "message": "ok"}
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"phone_required": settings.phone_required,
|
||||
"phone_sms_required": settings.phone_sms_required,
|
||||
"email_required": settings.email_required,
|
||||
"email_verify_required": settings.email_verify_required,
|
||||
"real_name_required": settings.real_name_required,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,781 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, desc, asc
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
import re
|
||||
import random
|
||||
import json
|
||||
import httpx
|
||||
import os
|
||||
import logging
|
||||
|
||||
from ..database import get_db, SessionLocal
|
||||
from ..models import (
|
||||
User, Conversation, Message, AIModel, ModelProvider, TokenUsageLog,
|
||||
KnowledgeBase, KnowledgeDocument, EvolutionSkill,
|
||||
EvolutionTrajectory,
|
||||
gen_uuid
|
||||
)
|
||||
from ..auth import get_current_user
|
||||
from ..middleware.log_service import LogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreateConversationRequest(BaseModel):
|
||||
title: str = Field(default="新对话")
|
||||
model_id: Optional[str] = None
|
||||
|
||||
|
||||
class UpdateConversationRequest(BaseModel):
|
||||
title: Optional[str] = None
|
||||
status: Optional[str] = None # active, archived
|
||||
pinned: Optional[bool] = None
|
||||
|
||||
|
||||
class SendMessageRequest(BaseModel):
|
||||
content: str
|
||||
model_id: Optional[str] = None
|
||||
skill_ids: Optional[list] = None
|
||||
agent_ids: Optional[list] = None
|
||||
knowledge_base_ids: Optional[list] = None
|
||||
use_tools: Optional[bool] = False
|
||||
|
||||
|
||||
@router.get("/conversations")
|
||||
def list_conversations(
|
||||
status: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
query = db.query(Conversation).filter(Conversation.user_id == current_user.id)
|
||||
|
||||
if status:
|
||||
query = query.filter(Conversation.status == status)
|
||||
else:
|
||||
# 默认不返回已归档的
|
||||
query = query.filter(Conversation.status != "archived")
|
||||
|
||||
if search:
|
||||
query = query.filter(Conversation.title.ilike(f"%{search}%"))
|
||||
|
||||
# pinned优先,然后按更新时间倒序
|
||||
query = query.order_by(desc(Conversation.pinned), desc(Conversation.updated_at))
|
||||
|
||||
conversations = query.all()
|
||||
result = []
|
||||
for conv in conversations:
|
||||
result.append({
|
||||
"id": conv.id,
|
||||
"title": conv.title,
|
||||
"model_id": conv.model_id,
|
||||
"status": conv.status,
|
||||
"pinned": conv.pinned,
|
||||
"knowledge_graph_count": conv.knowledge_graph_count or 0,
|
||||
"created_at": str(conv.created_at) if conv.created_at else None,
|
||||
"updated_at": str(conv.updated_at) if conv.updated_at else None,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.post("/conversations")
|
||||
def create_conversation(
|
||||
req: CreateConversationRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
conv = Conversation(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
title=req.title,
|
||||
model_id=req.model_id,
|
||||
status="active",
|
||||
pinned=False,
|
||||
knowledge_graph_count=0,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(conv)
|
||||
db.commit()
|
||||
db.refresh(conv)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": conv.id,
|
||||
"title": conv.title,
|
||||
"model_id": conv.model_id,
|
||||
"status": conv.status,
|
||||
"pinned": conv.pinned,
|
||||
"knowledge_graph_count": conv.knowledge_graph_count,
|
||||
"created_at": str(conv.created_at) if conv.created_at else None,
|
||||
"updated_at": str(conv.updated_at) if conv.updated_at else None,
|
||||
},
|
||||
"message": "创建成功",
|
||||
}
|
||||
|
||||
|
||||
@router.put("/conversations/{conv_id}")
|
||||
def update_conversation(
|
||||
conv_id: str,
|
||||
req: UpdateConversationRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
conv = db.query(Conversation).filter(
|
||||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||||
).first()
|
||||
if not conv:
|
||||
raise HTTPException(status_code=404, detail="对话不存在")
|
||||
|
||||
if req.title is not None:
|
||||
conv.title = req.title
|
||||
if req.status is not None:
|
||||
conv.status = req.status
|
||||
if req.pinned is not None:
|
||||
conv.pinned = req.pinned
|
||||
conv.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
db.refresh(conv)
|
||||
|
||||
return {"code": 0, "data": {
|
||||
"id": conv.id, "title": conv.title, "status": conv.status, "pinned": conv.pinned,
|
||||
"updated_at": str(conv.updated_at),
|
||||
}, "message": "更新成功"}
|
||||
|
||||
|
||||
@router.delete("/conversations/{conv_id}")
|
||||
def delete_conversation(
|
||||
conv_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
conv = db.query(Conversation).filter(
|
||||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||||
).first()
|
||||
if not conv:
|
||||
raise HTTPException(status_code=404, detail="对话不存在")
|
||||
|
||||
# 软删除
|
||||
conv.status = "archived"
|
||||
conv.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "删除成功"}
|
||||
|
||||
|
||||
@router.get("/conversations/{conv_id}/messages")
|
||||
def get_messages(
|
||||
conv_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
conv = db.query(Conversation).filter(
|
||||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||||
).first()
|
||||
if not conv:
|
||||
raise HTTPException(status_code=404, detail="对话不存在")
|
||||
|
||||
messages = (
|
||||
db.query(Message)
|
||||
.filter(Message.conversation_id == conv_id)
|
||||
.order_by(asc(Message.created_at))
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for msg in messages:
|
||||
result.append({
|
||||
"id": msg.id,
|
||||
"conversation_id": msg.conversation_id,
|
||||
"role": msg.role,
|
||||
"content": msg.content,
|
||||
"token_used": msg.token_used or 0,
|
||||
"model_id": msg.model_id,
|
||||
"used_skills": msg.used_skills or [],
|
||||
"created_at": str(msg.created_at) if msg.created_at else None,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.post("/conversations/{conv_id}/messages")
|
||||
async def send_message(
|
||||
conv_id: str,
|
||||
req: SendMessageRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
conv = db.query(Conversation).filter(
|
||||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||||
).first()
|
||||
if not conv:
|
||||
raise HTTPException(status_code=404, detail="对话不存在")
|
||||
|
||||
# 保存用户消息
|
||||
user_msg = Message(
|
||||
id=gen_uuid(),
|
||||
conversation_id=conv_id,
|
||||
role="user",
|
||||
content=req.content,
|
||||
token_used=len(req.content) // 2, # 粗略估算
|
||||
model_id=req.model_id or conv.model_id,
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
db.add(user_msg)
|
||||
|
||||
# 调用 AI 模型 API
|
||||
user_id = current_user.id
|
||||
model_id = req.model_id or (conv.model_id if hasattr(conv, 'model_id') else None)
|
||||
prompt_tokens = 0
|
||||
completion_tokens = 0
|
||||
total_tokens = 0
|
||||
|
||||
ai_model = None
|
||||
if model_id:
|
||||
ai_model = db.query(AIModel).filter(AIModel.id == model_id).first()
|
||||
|
||||
if ai_model and ai_model.endpoint:
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == ai_model.provider_id).first()
|
||||
api_key = ai_model.api_key or (provider.api_key if provider else None)
|
||||
api_url = ai_model.endpoint
|
||||
|
||||
if not api_url and provider and provider.api_base_url:
|
||||
api_url = f"{provider.api_base_url.rstrip('/')}/v1/chat/completions"
|
||||
|
||||
if api_key and api_url:
|
||||
try:
|
||||
# 构建消息列表:先system prompt(含skills/agents上下文),再历史消息,再用户消息
|
||||
messages = []
|
||||
system_parts = ["你是AIFlow平台的AI助手。"]
|
||||
|
||||
# 加载skills信息用于tool-call
|
||||
skills_list = []
|
||||
function_definitions = []
|
||||
if req.skill_ids and len(req.skill_ids) > 0:
|
||||
skills_list = db.query(EvolutionSkill).filter(EvolutionSkill.id.in_(req.skill_ids)).all()
|
||||
if skills_list:
|
||||
skill_descs = ", ".join([f"{s.skill_name}({s.category or 'general'})" for s in skills_list])
|
||||
system_parts.append(f"可用技能: {skill_descs}")
|
||||
system_parts.append("重要: 当用户的请求可以使用上述技能处理时,你必须优先调用对应的工具函数(tool-call)来完成任务,而不是直接回复文字。")
|
||||
|
||||
# 构建function definitions用于tool-call (OpenAI function calling协议)
|
||||
if req.use_tools:
|
||||
for s in skills_list:
|
||||
func_name = s.skill_name.replace(" ", "_").replace("-", "_").lower()
|
||||
_desc = s.description or f"{s.skill_name} - {s.category or 'general'}"
|
||||
function_definitions.append({
|
||||
"name": func_name,
|
||||
"description": f"调用技能「{s.skill_name}」处理用户请求。使用场景: {_desc}。当用户请求涉及{s.category or '通用'}类任务时优先调用此工具。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "string",
|
||||
"description": f"需要传递给{s.skill_name}的完整用户输入内容",
|
||||
}
|
||||
},
|
||||
"required": ["input"],
|
||||
},
|
||||
})
|
||||
|
||||
if req.agent_ids and len(req.agent_ids) > 0:
|
||||
system_parts.append(f"已选择Agent辅助处理任务")
|
||||
if req.knowledge_base_ids and len(req.knowledge_base_ids) > 0:
|
||||
kbs = db.query(KnowledgeBase).filter(KnowledgeBase.id.in_(req.knowledge_base_ids)).all()
|
||||
if kbs:
|
||||
kb_names = ", ".join([k.name for k in kbs])
|
||||
system_parts.append(f"参考知识库: {kb_names}")
|
||||
|
||||
messages.append({"role": "system", "content": "; ".join(system_parts)})
|
||||
|
||||
# 加载最近的对话历史(最多10条)
|
||||
history = db.query(Message).filter(
|
||||
Message.conversation_id == conv_id
|
||||
).order_by(asc(Message.created_at)).limit(10).all()
|
||||
for h in history:
|
||||
messages.append({"role": h.role, "content": h.content})
|
||||
|
||||
messages.append({"role": "user", "content": req.content})
|
||||
|
||||
# Tool-call循环(最多3轮)
|
||||
max_tool_rounds = 3
|
||||
ai_content = ""
|
||||
for round_num in range(max_tool_rounds):
|
||||
request_body = {
|
||||
"model": ai_model.name,
|
||||
"messages": messages,
|
||||
"max_tokens": 2048,
|
||||
}
|
||||
if function_definitions and req.use_tools:
|
||||
request_body["tools"] = [
|
||||
{"type": "function", "function": fd}
|
||||
for fd in function_definitions
|
||||
]
|
||||
request_body["tool_choice"] = "auto"
|
||||
|
||||
async with httpx.AsyncClient(timeout=ai_model.timeout or 60) as client:
|
||||
resp = await client.post(
|
||||
api_url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json=request_body
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
if round_num == 0:
|
||||
ai_content = f"AI模型调用失败 (HTTP {resp.status_code}),请检查模型配置。"
|
||||
break
|
||||
|
||||
data = resp.json()
|
||||
choice = data.get("choices", [{}])[0]
|
||||
message = choice.get("message", {})
|
||||
|
||||
# 记录token使用量
|
||||
usage = data.get("usage", {})
|
||||
prompt_tokens += usage.get("prompt_tokens", 0)
|
||||
completion_tokens += usage.get("completion_tokens", 0)
|
||||
total_tokens += usage.get("total_tokens", 0)
|
||||
|
||||
# 检查是否有tool_calls
|
||||
tool_calls = message.get("tool_calls", [])
|
||||
if tool_calls and req.use_tools:
|
||||
# 处理tool call:将assistant消息(含tool_calls)加入上下文
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": message.get("content") or "",
|
||||
"tool_calls": tool_calls,
|
||||
})
|
||||
|
||||
# 执行每个tool call
|
||||
for tc in tool_calls:
|
||||
func_name = tc.get("function", {}).get("name", "")
|
||||
func_args_str = tc.get("function", {}).get("arguments", "{}")
|
||||
try:
|
||||
func_args = json.loads(func_args_str)
|
||||
except Exception:
|
||||
func_args = {"input": func_args_str}
|
||||
|
||||
# 查找匹配的skill并生成tool结果
|
||||
tool_result = _execute_skill_tool(db, func_name, func_args, skills_list)
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tc.get("id", f"call_{round_num}"),
|
||||
"content": tool_result,
|
||||
})
|
||||
|
||||
# 继续下一轮,让模型处理tool结果
|
||||
continue
|
||||
else:
|
||||
# 没有tool_calls,正常回复
|
||||
ai_content = message.get("content", "")
|
||||
if not ai_content and round_num > 0:
|
||||
ai_content = "AI模型已处理工具调用结果,但返回了空响应。"
|
||||
break
|
||||
|
||||
if not ai_content:
|
||||
ai_content = "AI模型返回了空响应,请稍后重试。"
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f"AI API调用异常: {e}")
|
||||
ai_content = "AI服务暂时不可用,请稍后重试或检查模型API配置。"
|
||||
else:
|
||||
ai_content = "该模型缺少API密钥或端点配置,请在后台管理-大模型库中配置API Key和Endpoint。"
|
||||
else:
|
||||
ai_content = "请选择一个已配置的AI模型进行对话。"
|
||||
else:
|
||||
ai_content = "请先在聊天设置中选择一个AI模型。"
|
||||
|
||||
ai_token_count = len(ai_content) // 2
|
||||
ai_msg = Message(
|
||||
id=gen_uuid(),
|
||||
conversation_id=conv_id,
|
||||
role="assistant",
|
||||
content=ai_content,
|
||||
token_used=ai_token_count,
|
||||
model_id=req.model_id or conv.model_id,
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
db.add(ai_msg)
|
||||
|
||||
# 基于请求中skill_ids关联用户真实技能作为used_skills
|
||||
used_skills = []
|
||||
if req.skill_ids and len(req.skill_ids) > 0:
|
||||
selected_skills = db.query(EvolutionSkill).filter(
|
||||
EvolutionSkill.id.in_(req.skill_ids),
|
||||
EvolutionSkill.user_id == current_user.id
|
||||
).all()
|
||||
if selected_skills:
|
||||
used_skills = [s.skill_name for s in selected_skills]
|
||||
ai_msg.used_skills = used_skills
|
||||
# 更新每个被使用技能的use_count和last_used_at
|
||||
now = datetime.now()
|
||||
for s in selected_skills:
|
||||
s.use_count = (s.use_count or 0) + 1
|
||||
s.last_used_at = now
|
||||
|
||||
# 更新对话统计
|
||||
conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1
|
||||
conv.updated_at = datetime.now()
|
||||
|
||||
# 记录Token使用
|
||||
token_usage = TokenUsageLog(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
model_id=req.model_id or conv.model_id,
|
||||
model_name=ai_model.name if model_id and ai_model else None,
|
||||
conversation_id=conv_id,
|
||||
prompt_tokens=prompt_tokens or len(req.content) // 2,
|
||||
completion_tokens=completion_tokens or ai_token_count,
|
||||
total_tokens=total_tokens or (len(req.content) // 2 + ai_token_count),
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
db.add(token_usage)
|
||||
|
||||
# 记录 AI 进化轨迹 (每次对话产生一条轨迹,供PipelineScheduler处理)
|
||||
# 去重:检查该对话是否已有trajectory记录,避免重复创建
|
||||
try:
|
||||
existing = db.query(EvolutionTrajectory).filter(
|
||||
EvolutionTrajectory.conversation_id == conv_id
|
||||
).first()
|
||||
if not existing:
|
||||
trajectory = EvolutionTrajectory(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
conversation_id=conv_id,
|
||||
status="discovered",
|
||||
token_used=ai_token_count,
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
db.add(trajectory)
|
||||
db.flush() # 确保trajectory先写入,再启动pipeline
|
||||
|
||||
# 自动启动用户进化Pipeline(如果未运行)
|
||||
try:
|
||||
from app.services.evolution_service import EvolutionWatcherManager
|
||||
watcher = EvolutionWatcherManager()
|
||||
status = watcher.get_user_status(current_user.id, lambda: SessionLocal())
|
||||
if not status.get("scheduler_running"):
|
||||
watcher.start_user_evolution(current_user.id, lambda: SessionLocal())
|
||||
logger.info(f"[Evolution] 自动启动用户 {current_user.id} 的进化Pipeline")
|
||||
except Exception as evolution_err:
|
||||
logger.warning(f"[Evolution] 自动启动Pipeline失败: {evolution_err}")
|
||||
except Exception:
|
||||
pass # 轨迹记录失败不影响主流程
|
||||
|
||||
db.commit()
|
||||
db.refresh(user_msg)
|
||||
db.refresh(ai_msg)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"user_message": {
|
||||
"id": user_msg.id, "role": user_msg.role, "content": user_msg.content,
|
||||
"token_used": user_msg.token_used, "created_at": str(user_msg.created_at),
|
||||
},
|
||||
"assistant_message": {
|
||||
"id": ai_msg.id, "role": ai_msg.role, "content": ai_msg.content,
|
||||
"token_used": ai_msg.token_used, "created_at": str(ai_msg.created_at),
|
||||
"used_skills": used_skills,
|
||||
},
|
||||
},
|
||||
"message": "发送成功",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/conversations/{conv_id}/clear")
|
||||
def clear_conversation(
|
||||
conv_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
conv = db.query(Conversation).filter(
|
||||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||||
).first()
|
||||
if not conv:
|
||||
raise HTTPException(status_code=404, detail="对话不存在")
|
||||
|
||||
# 清空消息(物理删除消息记录)
|
||||
db.query(Message).filter(
|
||||
Message.conversation_id == conv_id
|
||||
).delete()
|
||||
|
||||
conv.knowledge_graph_count = 0
|
||||
conv.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "对话上下文已清空"}
|
||||
|
||||
|
||||
@router.post("/conversations/{conv_id}/to-knowledge-graph")
|
||||
def convert_to_knowledge_graph(
|
||||
conv_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""将对话内容转换为知识图谱:提取消息内容、创建知识库、构建知识图谱"""
|
||||
conv = db.query(Conversation).filter(
|
||||
and_(Conversation.id == conv_id, Conversation.user_id == current_user.id)
|
||||
).first()
|
||||
if not conv:
|
||||
raise HTTPException(status_code=404, detail="对话不存在")
|
||||
|
||||
# 获取该对话的所有消息
|
||||
messages = (
|
||||
db.query(Message)
|
||||
.filter(Message.conversation_id == conv_id)
|
||||
.order_by(asc(Message.created_at))
|
||||
.all()
|
||||
)
|
||||
|
||||
if not messages:
|
||||
raise HTTPException(status_code=400, detail="对话中没有消息,无法生成知识图谱")
|
||||
|
||||
# 1. 自动创建一个知识库(名为"对话图谱-{对话标题}")
|
||||
kb_name = f"对话图谱-{conv.title}"
|
||||
kb = KnowledgeBase(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
name=kb_name,
|
||||
description=f"从对话「{conv.title}」自动生成的知识图谱",
|
||||
directory_path="/",
|
||||
document_count=0,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(kb)
|
||||
db.flush()
|
||||
|
||||
# 2. 将每条消息内容作为文档存入知识库
|
||||
doc_count = 0
|
||||
for idx, msg in enumerate(messages):
|
||||
# 提取消息中的简明摘要作为文档文件名
|
||||
role_label = "用户" if msg.role == "user" else "AI"
|
||||
preview = re.sub(r'[\\n\\r\\t]+', ' ', msg.content[:40]).strip()
|
||||
filename = f"msg_{idx+1:03d}_{role_label}_{preview}.txt"
|
||||
|
||||
doc = KnowledgeDocument(
|
||||
id=gen_uuid(),
|
||||
knowledge_base_id=kb.id,
|
||||
filename=filename,
|
||||
file_type="txt",
|
||||
file_size=len(msg.content.encode('utf-8')),
|
||||
status="completed",
|
||||
chunk_count=max(1, len(msg.content) // 500 + 1),
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(doc)
|
||||
doc_count += 1
|
||||
|
||||
kb.document_count = doc_count
|
||||
db.flush()
|
||||
|
||||
# 3. 调用知识图谱构建逻辑(类似 knowledge.py 中的 build-graph)
|
||||
documents = db.query(KnowledgeDocument).filter(
|
||||
and_(
|
||||
KnowledgeDocument.knowledge_base_id == kb.id,
|
||||
KnowledgeDocument.status == "completed",
|
||||
)
|
||||
).all()
|
||||
|
||||
graph_nodes = []
|
||||
graph_edges = []
|
||||
|
||||
doc_entity_types = [
|
||||
{"type": "concept", "label": "核心概念"},
|
||||
{"type": "entity", "label": "关键实体"},
|
||||
{"type": "attribute", "label": "属性特征"},
|
||||
{"type": "relation", "label": "关联关系"},
|
||||
]
|
||||
|
||||
for idx, doc in enumerate(documents):
|
||||
doc_node_id = f"doc-{doc.id[:8]}"
|
||||
graph_nodes.append({
|
||||
"id": doc_node_id,
|
||||
"label": doc.filename,
|
||||
"type": "document",
|
||||
"size": 18,
|
||||
"group": "document",
|
||||
})
|
||||
|
||||
filename_base = doc.filename.rsplit(".", 1)[0]
|
||||
words = filename_base.replace("_", " ").replace("-", " ").split()
|
||||
|
||||
for wi, word in enumerate(words):
|
||||
if len(word) >= 2:
|
||||
entity_type = doc_entity_types[wi % len(doc_entity_types)]
|
||||
node_id = f"ent-{doc.id[:8]}-{wi}"
|
||||
graph_nodes.append({
|
||||
"id": node_id,
|
||||
"label": word,
|
||||
"type": entity_type["type"],
|
||||
"size": 10 + (wi % 8),
|
||||
"group": entity_type["type"],
|
||||
})
|
||||
graph_edges.append({
|
||||
"source": doc_node_id,
|
||||
"target": node_id,
|
||||
"label": entity_type["label"],
|
||||
"weight": 0.5 + (0.5 / (wi + 1)),
|
||||
})
|
||||
|
||||
# 在文档之间建立关联
|
||||
for i in range(len(documents)):
|
||||
for j in range(i + 1, len(documents)):
|
||||
fi = documents[i].filename.lower().rsplit(".", 1)[0]
|
||||
fj = documents[j].filename.lower().rsplit(".", 1)[0]
|
||||
words_i = set(fi.replace("_", " ").replace("-", " ").split())
|
||||
words_j = set(fj.replace("_", " ").replace("-", " ").split())
|
||||
common = words_i & words_j
|
||||
if common and len(common) > 0:
|
||||
graph_edges.append({
|
||||
"source": f"doc-{documents[i].id[:8]}",
|
||||
"target": f"doc-{documents[j].id[:8]}",
|
||||
"label": f"共有关键词: {', '.join(list(common)[:3])}",
|
||||
"weight": min(0.9, 0.3 * len(common)),
|
||||
})
|
||||
|
||||
graph_data = {
|
||||
"nodes": graph_nodes,
|
||||
"edges": graph_edges,
|
||||
"stats": {
|
||||
"total_nodes": len(graph_nodes),
|
||||
"total_edges": len(graph_edges),
|
||||
"document_count": len(documents),
|
||||
"entity_types": {et["type"]: sum(1 for n in graph_nodes if n.get("group") == et["type"])
|
||||
for et in doc_entity_types},
|
||||
},
|
||||
}
|
||||
|
||||
# 更新知识库的知识图谱计数
|
||||
kb.knowledge_graph_count = len(graph_nodes)
|
||||
kb.updated_at = datetime.now()
|
||||
|
||||
# 更新对话的知识图谱计数
|
||||
conv.knowledge_graph_count = (conv.knowledge_graph_count or 0) + 1
|
||||
conv.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="convert_to_graph",
|
||||
target="conversation", target_id=conv_id,
|
||||
detail=f"将对话 {conv.title} 转换为知识图谱 {kb.name},共 {len(graph_nodes)} 节点、{len(graph_edges)} 边",
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"knowledge_base_id": kb.id,
|
||||
"knowledge_base_name": kb.name,
|
||||
"graph": graph_data,
|
||||
},
|
||||
"message": f"知识图谱构建完成,共 {len(graph_nodes)} 个节点、{len(graph_edges)} 条边",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/models")
|
||||
def get_models(db: Session = Depends(get_db)):
|
||||
models = db.query(AIModel).filter(AIModel.is_enabled == True).all()
|
||||
result = []
|
||||
for m in models:
|
||||
provider = db.query(ModelProvider).filter(ModelProvider.id == m.provider_id).first()
|
||||
result.append({
|
||||
"id": m.id,
|
||||
"name": m.name,
|
||||
"provider_id": m.provider_id,
|
||||
"provider_name": provider.name if provider else "",
|
||||
"status": m.status,
|
||||
"price_per_million_tokens": float(m.price_per_million_tokens or 0),
|
||||
"is_enabled": m.is_enabled,
|
||||
"is_default": bool(m.is_default) if hasattr(m, 'is_default') else False,
|
||||
"total_calls": m.total_calls or 0,
|
||||
"total_tokens": m.total_tokens or 0,
|
||||
"created_at": str(m.created_at) if m.created_at else None,
|
||||
})
|
||||
# 默认模型排在最前面
|
||||
result.sort(key=lambda x: (0 if x["is_default"] else 1, x["name"]))
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
def _execute_skill_tool(db: Session, func_name: str, args: dict, skills_list: list) -> str:
|
||||
"""执行tool-call中的skill工具调用,返回工具执行结果"""
|
||||
# 根据func_name匹配对应的skill(支持新旧两种命名方式)
|
||||
matched_skill = None
|
||||
for s in skills_list:
|
||||
expected_name = s.skill_name.replace(" ", "_").replace("-", "_").lower()
|
||||
if func_name == expected_name or func_name == f"skill_{s.id[:8]}":
|
||||
matched_skill = s
|
||||
break
|
||||
|
||||
skill_name = matched_skill.skill_name if matched_skill else func_name
|
||||
input_text = args.get("input", "")
|
||||
|
||||
# 根据skill类型生成模拟执行结果
|
||||
skill_type = matched_skill.skill_type if matched_skill else "unknown"
|
||||
|
||||
if skill_type == "writer":
|
||||
result = (
|
||||
f"技能 '{skill_name}' (写作类) 执行结果:\n"
|
||||
f"根据输入 '{input_text[:100]}' 生成以下内容:\n"
|
||||
f"---\n"
|
||||
f"# 生成文本\n\n这是一篇关于 {input_text[:50]} 的专业文章。"
|
||||
f"通过深入分析和研究,我们得出以下核心观点:首先,该主题在当前领域具有重要的实践价值;"
|
||||
f"其次,从技术发展趋势来看,该方向有望在未来3-5年内实现重大突破。"
|
||||
f"\n\n## 关键建议\n1. 加强基础研究投入\n2. 建立标准化流程\n3. 推动产业协同创新"
|
||||
f"\n\n*由技能 '{skill_name}' 自动生成*"
|
||||
)
|
||||
elif skill_type == "analyzer":
|
||||
result = (
|
||||
f"技能 '{skill_name}' (分析类) 执行结果:\n"
|
||||
f"对输入内容进行了以下分析:\n"
|
||||
f"---\n"
|
||||
f"分析主题: {input_text[:80]}\n"
|
||||
f"情感倾向: 中性偏积极\n"
|
||||
f"关键词提取: {', '.join([w for w in input_text[:80].replace(',', ',').split(',')[:5]])}\n"
|
||||
f"关键实体: 识别到 {max(1, len(input_text) // 50)} 个实体\n"
|
||||
f"语义密度: {'高' if len(input_text) > 50 else '中'}\n"
|
||||
f"建议行动: 进一步细化分析维度,增加对比数据"
|
||||
)
|
||||
elif skill_type == "translator":
|
||||
result = (
|
||||
f"技能 '{skill_name}' (翻译类) 执行结果:\n"
|
||||
f"输入: {input_text[:100]}\n"
|
||||
f"英文翻译: [This is the English translation of: {input_text[:80]}...]"
|
||||
f"\n日文翻译: [これは次の内容の日本語訳です: {input_text[:50]}...]"
|
||||
)
|
||||
elif skill_type == "summarizer":
|
||||
result = (
|
||||
f"技能 '{skill_name}' (摘要类) 执行结果:\n"
|
||||
f"原文长度: {len(input_text)} 字符\n"
|
||||
f"摘要:\n"
|
||||
f"本文围绕 {input_text[:50]} 展开讨论,核心观点包括:技术革新驱动效率提升、"
|
||||
f"数据标准化是行业发展的关键瓶颈、以及跨平台协作将成为未来趋势。"
|
||||
f"\n关键数字: 预计增长率为15-20%,实施周期为6-12个月。"
|
||||
)
|
||||
else:
|
||||
result = (
|
||||
f"技能 '{skill_name}' ({skill_type}) 执行结果:\n"
|
||||
f"已处理输入: {input_text[:200]}\n"
|
||||
f"处理状态: 成功\n"
|
||||
f"输出: 基于技能 '{skill_name}' 的处理逻辑,已完成对输入内容的分析处理。"
|
||||
f"建议结合具体业务场景进一步优化结果。"
|
||||
)
|
||||
|
||||
# 更新skill使用计数
|
||||
if matched_skill:
|
||||
matched_skill.use_count = (matched_skill.use_count or 0) + 1
|
||||
matched_skill.last_used_at = datetime.now()
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
AI自进化 API路由
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..auth import get_current_user
|
||||
from ..models import User
|
||||
from ..services.evolution_service import get_evolution_service, EvolutionWatcherManager
|
||||
from ..database import get_db, SessionLocal
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ============================================================
|
||||
# Request/Response Models
|
||||
# ============================================================
|
||||
|
||||
class SkillUpdateRequest(BaseModel):
|
||||
description: Optional[str] = None
|
||||
tags: Optional[List[str]] = None
|
||||
category: Optional[str] = None
|
||||
|
||||
class TrajectorySubmitRequest(BaseModel):
|
||||
conversation_id: str
|
||||
messages: List[dict] = []
|
||||
|
||||
class PublishRequest(BaseModel):
|
||||
price: float = 0.00
|
||||
market_description: str = ""
|
||||
|
||||
class ShareToTeamRequest(BaseModel):
|
||||
team_id: str
|
||||
|
||||
# ============================================================
|
||||
# 技能管理 API
|
||||
# ============================================================
|
||||
|
||||
@router.get("/skills")
|
||||
def list_skills(
|
||||
state: Optional[str] = Query(None, description="技能状态过滤"),
|
||||
keyword: Optional[str] = Query(None, description="关键词搜索"),
|
||||
sort_by: str = Query("updated_at", description="排序字段"),
|
||||
order: str = Query("desc", description="排序方向"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取用户技能列表"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
return {"code": 0, "data": svc.get_skills(state=state, keyword=keyword, sort_by=sort_by, order=order)}
|
||||
|
||||
|
||||
@router.get("/skills/{skill_name}")
|
||||
def get_skill_detail(
|
||||
skill_name: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取技能详情"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
result = svc.get_skill_detail(skill_name)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="技能不存在")
|
||||
return {"code": 0, "data": result}
|
||||
|
||||
|
||||
@router.put("/skills/{skill_name}")
|
||||
def update_skill(
|
||||
skill_name: str,
|
||||
data: SkillUpdateRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""编辑技能"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
result = svc.update_skill(skill_name, data.model_dump(exclude_none=True))
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="技能不存在")
|
||||
return {"code": 0, "data": result}
|
||||
|
||||
|
||||
@router.delete("/skills/{skill_name}")
|
||||
def delete_skill(
|
||||
skill_name: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除技能"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
if not svc.delete_skill(skill_name):
|
||||
raise HTTPException(status_code=404, detail="技能不存在")
|
||||
return {"code": 0, "message": "技能已删除"}
|
||||
|
||||
|
||||
@router.post("/skills/{skill_name}/freeze")
|
||||
def freeze_skill(
|
||||
skill_name: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""冻结技能"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
result = svc.freeze_skill(skill_name)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="技能不存在")
|
||||
return {"code": 0, "data": result}
|
||||
|
||||
|
||||
@router.post("/skills/{skill_name}/unfreeze")
|
||||
def unfreeze_skill(
|
||||
skill_name: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""解冻技能"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
result = svc.unfreeze_skill(skill_name)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="技能不存在")
|
||||
return {"code": 0, "data": result}
|
||||
|
||||
|
||||
@router.get("/skills/{skill_name}/history")
|
||||
def get_skill_history(
|
||||
skill_name: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取技能版本历史"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
return {"code": 0, "data": svc.get_skill_history(skill_name)}
|
||||
|
||||
|
||||
@router.get("/skills/{skill_name}/trajectories")
|
||||
def get_skill_trajectories(
|
||||
skill_name: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取技能来源轨迹列表"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
return {"code": 0, "data": svc.get_skill_trajectories(skill_name)}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 进化事件 API
|
||||
# ============================================================
|
||||
|
||||
@router.get("/events")
|
||||
def list_events(
|
||||
event_type: Optional[str] = Query(None),
|
||||
skill_name: Optional[str] = Query(None),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取进化事件列表"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
return {"code": 0, "data": svc.get_events(event_type=event_type, skill_name=skill_name, limit=limit)}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 驾驶舱数据 API
|
||||
# ============================================================
|
||||
|
||||
@router.get("/dashboard/skill-tree")
|
||||
def get_skill_tree(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取技能图谱数据(面板A)"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
return {"code": 0, "data": svc.get_skill_tree()}
|
||||
|
||||
|
||||
@router.get("/dashboard/timeline")
|
||||
def get_timeline(
|
||||
event_type: Optional[str] = Query(None),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取进化时间线数据(面板B)"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
return {"code": 0, "data": svc.get_timeline(event_type=event_type)}
|
||||
|
||||
|
||||
@router.get("/dashboard/statistics")
|
||||
def get_statistics(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取技能使用统计数据(面板C)"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
return {"code": 0, "data": svc.get_statistics()}
|
||||
|
||||
|
||||
@router.get("/dashboard/profile")
|
||||
def get_profile(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取AI能力画像数据(面板D)"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
return {"code": 0, "data": svc.get_profile()}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 轨迹提交 API
|
||||
# ============================================================
|
||||
|
||||
@router.post("/trajectory/submit")
|
||||
def submit_trajectory(
|
||||
data: TrajectorySubmitRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""提交对话轨迹到进化流水线"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
result = svc.submit_trajectory(data.conversation_id, data.messages)
|
||||
return {"code": 0, "data": result, "message": "轨迹已提交"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# v2 新增 API
|
||||
# ============================================================
|
||||
|
||||
|
||||
@router.post("/skills/{skill_name}/rollback/{version}")
|
||||
def rollback_skill(
|
||||
skill_name: str,
|
||||
version: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""技能回滚到指定版本"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
result = svc.rollback_skill(skill_name, version)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="技能或版本不存在")
|
||||
return {"code": 0, "data": result}
|
||||
|
||||
|
||||
@router.post("/skills/{skill_name}/publish")
|
||||
def publish_skill(
|
||||
skill_name: str,
|
||||
data: PublishRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""发布技能到技能市场"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
result = svc.publish_to_market(skill_name, data.price, data.market_description)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="技能不存在")
|
||||
return {"code": 0, "data": result}
|
||||
|
||||
|
||||
@router.post("/skills/{skill_name}/share-to-team")
|
||||
def share_skill_to_team(
|
||||
skill_name: str,
|
||||
data: ShareToTeamRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""将技能共享给团队"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
result = svc.share_to_team(skill_name, data.team_id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="技能或团队不存在")
|
||||
return {"code": 0, "data": result}
|
||||
|
||||
|
||||
@router.get("/pipeline/status")
|
||||
def get_pipeline_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取流水线状态"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
return {"code": 0, "data": svc.get_pipeline_status()}
|
||||
|
||||
|
||||
@router.get("/reports")
|
||||
def list_reports(
|
||||
report_type: Optional[str] = Query(None, description="报告类型过滤"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取报告列表"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
return {"code": 0, "data": svc.get_reports(report_type=report_type)}
|
||||
|
||||
|
||||
@router.post("/reports/generate")
|
||||
def generate_report(
|
||||
report_type: str = Query("weekly", description="报告类型: weekly/monthly"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""手动生成进化报告"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
result = svc.generate_report(report_type=report_type)
|
||||
return {"code": 0, "data": result, "message": f"{report_type}报告生成完成"}
|
||||
|
||||
|
||||
@router.get("/reports/{report_id}")
|
||||
def get_report_detail(
|
||||
report_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取报告详情"""
|
||||
svc = get_evolution_service(db, current_user.id)
|
||||
result = svc.get_report_detail(report_id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="报告不存在")
|
||||
return {"code": 0, "data": result}
|
||||
|
||||
|
||||
@router.get("/skills/{skill_name}/versions")
|
||||
def get_skill_versions(
|
||||
skill_name: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取技能所有版本记录(版本快照列表)"""
|
||||
from ..models import SkillVersion
|
||||
versions = db.query(SkillVersion).filter(
|
||||
SkillVersion.user_id == current_user.id,
|
||||
SkillVersion.skill_name == skill_name
|
||||
).order_by(SkillVersion.version.desc()).all()
|
||||
return {"code": 0, "data": [{
|
||||
"id": v.id,
|
||||
"skill_name": v.skill_name,
|
||||
"version": v.version,
|
||||
"git_commit_sha": v.git_commit_sha,
|
||||
"description": v.description,
|
||||
"created_at": v.created_at.isoformat() if v.created_at else None,
|
||||
} for v in versions]}
|
||||
|
||||
|
||||
@router.post("/pipeline/trigger")
|
||||
def trigger_pipeline(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""手动触发进化流水线(如果未运行则启动,如果已运行则重启)"""
|
||||
watcher = EvolutionWatcherManager()
|
||||
# 先尝试启动,如果已存在则重启
|
||||
if not watcher.start_user_evolution(current_user.id, lambda: SessionLocal()):
|
||||
watcher.restart_user_evolution(current_user.id, lambda: SessionLocal())
|
||||
return {"code": 0, "message": "流水线已触发"}
|
||||
|
||||
|
||||
@router.get("/watcher/status")
|
||||
def get_watcher_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取当前用户的Watcher状态"""
|
||||
watcher = EvolutionWatcherManager()
|
||||
status = watcher.get_user_status(current_user.id, lambda: SessionLocal())
|
||||
return {"code": 0, "data": status}
|
||||
|
||||
|
||||
@router.post("/watcher/restart")
|
||||
def restart_watcher(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""重启用户进化引擎"""
|
||||
watcher = EvolutionWatcherManager()
|
||||
ok = watcher.restart_user_evolution(current_user.id, lambda: SessionLocal())
|
||||
return {"code": 0, "message": "进化引擎已重启" if ok else "进化引擎重启失败"}
|
||||
|
||||
|
||||
@router.get("/admin/watchers")
|
||||
def get_all_watchers(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""管理员端点 - 获取所有用户的Watcher状态"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="无权限访问")
|
||||
watcher = EvolutionWatcherManager()
|
||||
all_status = {}
|
||||
for uid, scheduler in watcher._schedulers.items():
|
||||
all_status[str(uid)] = {
|
||||
"user_id": uid,
|
||||
"running": scheduler.is_running(),
|
||||
}
|
||||
return {"code": 0, "data": all_status}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,598 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, desc, asc, func
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
User, MarketApp, AppPurchase, AppReview, AppInstallation, Order,
|
||||
gen_uuid
|
||||
)
|
||||
from ..auth import get_current_user, get_current_user_id, get_current_user_id_optional
|
||||
from ..middleware.log_service import LogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class UploadAppRequest(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=200)
|
||||
description: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
type: Optional[str] = None # skill, agent, mcp
|
||||
source: Optional[str] = None
|
||||
price: float = Field(default=0, ge=0)
|
||||
download_url: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
is_member_free: Optional[bool] = False
|
||||
join_promotion: Optional[bool] = False
|
||||
agent_endpoint: Optional[str] = None
|
||||
|
||||
|
||||
class PurchaseAppRequest(BaseModel):
|
||||
amount: Optional[float] = None
|
||||
|
||||
|
||||
class ReviewAppRequest(BaseModel):
|
||||
rating: int = Field(ge=1, le=5)
|
||||
content: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/apps")
|
||||
def list_apps(
|
||||
type: Optional[str] = Query(None),
|
||||
source: Optional[str] = Query(None),
|
||||
category: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
sort: str = Query(default="latest"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
user_id: Optional[str] = Depends(get_current_user_id_optional),
|
||||
):
|
||||
query = db.query(MarketApp).filter(MarketApp.status == "published")
|
||||
|
||||
if type:
|
||||
query = query.filter(MarketApp.type == type)
|
||||
if source:
|
||||
query = query.filter(MarketApp.source == source)
|
||||
if category:
|
||||
query = query.filter(MarketApp.type == category)
|
||||
if search:
|
||||
query = query.filter(
|
||||
and_(
|
||||
MarketApp.name.ilike(f"%{search}%")
|
||||
| MarketApp.description.ilike(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
if sort == "popular":
|
||||
query = query.order_by(desc(MarketApp.install_count))
|
||||
elif sort == "rating":
|
||||
query = query.order_by(desc(MarketApp.rating_avg))
|
||||
else:
|
||||
query = query.order_by(desc(MarketApp.created_at))
|
||||
|
||||
total = query.count()
|
||||
apps = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
# 如果用户已登录,获取该用户已安装的应用ID集合(确保按用户隔离)
|
||||
installed_app_ids = set()
|
||||
if user_id:
|
||||
installations = db.query(AppInstallation.app_id).filter(
|
||||
AppInstallation.user_id == user_id
|
||||
).all()
|
||||
installed_app_ids = {inst[0] for inst in installations}
|
||||
|
||||
# 查询已安装状态时始终带 user_id 过滤
|
||||
|
||||
result = []
|
||||
for app in apps:
|
||||
item = {
|
||||
"id": app.id,
|
||||
"name": app.name,
|
||||
"description": app.description,
|
||||
"icon": app.icon,
|
||||
"type": app.type,
|
||||
"source": app.source,
|
||||
"publisher_id": app.publisher_id,
|
||||
"publisher_name": app.publisher_name,
|
||||
"is_member_free": app.is_member_free if app.is_member_free else False,
|
||||
"join_promotion": app.join_promotion if app.join_promotion else False,
|
||||
"status": app.status,
|
||||
"price": float(app.price) if app.price else 0,
|
||||
"install_count": app.install_count or 0,
|
||||
"rating_avg": float(app.rating_avg) if app.rating_avg else 0,
|
||||
"review_count": app.review_count or 0,
|
||||
"created_at": str(app.created_at) if app.created_at else None,
|
||||
"installed": app.id in installed_app_ids if user_id else False,
|
||||
}
|
||||
result.append(item)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": result,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/apps/{app_id}")
|
||||
def get_app(
|
||||
app_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(get_current_user),
|
||||
):
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
# 检查当前用户是否已购买(按用户隔离)
|
||||
purchased = False
|
||||
installed = False
|
||||
if current_user:
|
||||
purchase = db.query(AppPurchase).filter(
|
||||
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
|
||||
).first()
|
||||
if purchase:
|
||||
purchased = True
|
||||
installed = purchase.status == "installed"
|
||||
# 通过 AppInstallation 表检查是否已安装(按用户隔离)
|
||||
installation = db.query(AppInstallation).filter(
|
||||
and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
|
||||
).first()
|
||||
if installation:
|
||||
installed = True
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": app.id, "name": app.name, "description": app.description,
|
||||
"icon": app.icon, "type": app.type, "source": app.source,
|
||||
"type": app.type,
|
||||
"price": float(app.price) if app.price else 0,
|
||||
"install_count": app.install_count or 0,
|
||||
"rating_avg": float(app.rating_avg) if app.rating_avg else 0,
|
||||
"review_count": app.review_count or 0,
|
||||
"publisher_id": app.publisher_id,
|
||||
"download_url": app.download_url,
|
||||
"detail": app.detail,
|
||||
"created_at": str(app.created_at),
|
||||
"purchased": purchased,
|
||||
"installed": installed,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/apps")
|
||||
def upload_app(
|
||||
req: UploadAppRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
app = MarketApp(
|
||||
id=gen_uuid(),
|
||||
name=req.name,
|
||||
description=req.description,
|
||||
icon=req.icon,
|
||||
type=req.type if req.type in ("skill","agent","mcp") else "skill",
|
||||
source=req.source if req.source in ("official","user") else "user",
|
||||
price=req.price,
|
||||
install_count=0,
|
||||
rating_avg=0,
|
||||
review_count=0,
|
||||
publisher_id=current_user.id,
|
||||
publisher_name=current_user.username,
|
||||
is_member_free=req.is_member_free if req.is_member_free is not None else False,
|
||||
join_promotion=req.join_promotion if req.join_promotion is not None else False,
|
||||
agent_endpoint=req.agent_endpoint,
|
||||
status="published", # 用户创建的应用直接发布
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(app)
|
||||
db.commit()
|
||||
db.refresh(app)
|
||||
|
||||
# 用户创建/导入的应用自动标记为已安装
|
||||
existing_install = db.query(AppInstallation).filter(
|
||||
AppInstallation.user_id == current_user.id,
|
||||
AppInstallation.app_id == app.id,
|
||||
).first()
|
||||
if not existing_install:
|
||||
installation = AppInstallation(
|
||||
user_id=current_user.id,
|
||||
app_id=app.id,
|
||||
installed_at=datetime.now(),
|
||||
)
|
||||
db.add(installation)
|
||||
db.commit()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="upload_app",
|
||||
target="market_app", target_id=app.id,
|
||||
detail=f"上传应用 {app.name}",
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": app.id, "name": app.name, "status": app.status,
|
||||
"created_at": str(app.created_at),
|
||||
},
|
||||
"message": "提交成功,等待审核",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/purchase")
|
||||
def purchase_app(
|
||||
app_id: str,
|
||||
req: PurchaseAppRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
app = db.query(MarketApp).filter(
|
||||
and_(MarketApp.id == app_id, MarketApp.status == "published")
|
||||
).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在或未上架")
|
||||
|
||||
# 检查是否已购买
|
||||
existing = db.query(AppPurchase).filter(
|
||||
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
|
||||
).first()
|
||||
if existing and existing.status in ("purchased", "installed"):
|
||||
raise HTTPException(status_code=400, detail="您已购买该应用")
|
||||
|
||||
amount = req.amount or app.price
|
||||
|
||||
# 创建订单
|
||||
order = Order(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
order_type="app",
|
||||
item_id=app_id,
|
||||
item_name=app.name,
|
||||
amount=amount,
|
||||
status="pending",
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
db.add(order)
|
||||
db.flush()
|
||||
|
||||
# 创建购买记录
|
||||
purchase = AppPurchase(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
app_id=app_id,
|
||||
order_id=order.id,
|
||||
status="purchased",
|
||||
purchased_at=datetime.now(),
|
||||
)
|
||||
db.add(purchase)
|
||||
|
||||
# 更新应用下载量
|
||||
app.install_count = (app.install_count or 0) + 1
|
||||
app.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"order_id": order.id,
|
||||
"purchase_id": purchase.id,
|
||||
"amount": amount,
|
||||
"status": "purchased",
|
||||
},
|
||||
"message": "购买成功",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/install")
|
||||
def install_app(
|
||||
app_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
# 检查是否已安装(通过 AppInstallation 表)
|
||||
existing_install = db.query(AppInstallation).filter(
|
||||
and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
|
||||
).first()
|
||||
if existing_install:
|
||||
raise HTTPException(status_code=400, detail="应用已安装")
|
||||
|
||||
# 检查是否已购买(付费应用需要先购买)
|
||||
purchase = db.query(AppPurchase).filter(
|
||||
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
|
||||
).first()
|
||||
|
||||
# 对于付费应用,必须已购买才能安装
|
||||
if float(app.price or 0) > 0:
|
||||
if not purchase:
|
||||
raise HTTPException(status_code=403, detail="请先购买应用")
|
||||
if purchase.status == "installed":
|
||||
raise HTTPException(status_code=400, detail="应用已安装")
|
||||
|
||||
# 创建安装记录
|
||||
installation = AppInstallation(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
app_id=app_id,
|
||||
installed_at=datetime.now(),
|
||||
)
|
||||
db.add(installation)
|
||||
|
||||
# 如果有购买记录,更新状态为已安装
|
||||
if purchase:
|
||||
purchase.status = "installed"
|
||||
purchase.installed_at = datetime.now()
|
||||
|
||||
# 更新应用安装量
|
||||
app.install_count = (app.install_count or 0) + 1
|
||||
app.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {"installation_id": installation.id, "status": "installed"},
|
||||
"message": "安装成功",
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/apps/{app_id}/uninstall")
|
||||
def uninstall_app(
|
||||
app_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""卸载应用 - 删除当前用户的安装记录(按用户隔离)"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
# 查找当前用户的安装记录(按用户隔离)
|
||||
installation = db.query(AppInstallation).filter(
|
||||
and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
|
||||
).first()
|
||||
if not installation:
|
||||
raise HTTPException(status_code=400, detail="应用未安装")
|
||||
|
||||
# 删除安装记录
|
||||
db.delete(installation)
|
||||
|
||||
# 问题4修复: 将AppPurchase状态从installed重置为purchased
|
||||
purchase = db.query(AppPurchase).filter(
|
||||
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
|
||||
).first()
|
||||
if purchase and purchase.status == "installed":
|
||||
purchase.status = "purchased"
|
||||
|
||||
# 更新应用安装量(减1,最小为0)
|
||||
if app.install_count and app.install_count > 0:
|
||||
app.install_count = app.install_count - 1
|
||||
app.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="uninstall_app",
|
||||
target="market_app", target_id=app_id,
|
||||
detail=f"卸载应用 {app.name}",
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"message": "卸载成功",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/review")
|
||||
def review_app(
|
||||
app_id: str,
|
||||
req: ReviewAppRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
# 检查是否已评价
|
||||
existing = db.query(AppReview).filter(
|
||||
and_(AppReview.app_id == app_id, AppReview.user_id == current_user.id)
|
||||
).first()
|
||||
if existing:
|
||||
# 更新已有评价
|
||||
old_rating = existing.rating
|
||||
existing.rating = req.rating
|
||||
existing.content = req.content
|
||||
existing.updated_at = datetime.now()
|
||||
|
||||
# 重新计算平均分 (总数不变,减去旧评分加新评分)
|
||||
if app.review_count and app.review_count > 0:
|
||||
total_rating = float(app.rating_avg or 0) * app.review_count
|
||||
total_rating = total_rating - old_rating + req.rating
|
||||
app.rating_avg = total_rating / app.review_count
|
||||
else:
|
||||
app.rating_avg = req.rating
|
||||
app.review_count = 1
|
||||
else:
|
||||
# 创建新评价
|
||||
review = AppReview(
|
||||
id=gen_uuid(),
|
||||
app_id=app_id,
|
||||
user_id=current_user.id,
|
||||
rating=req.rating,
|
||||
content=req.content,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(review)
|
||||
|
||||
# 重新计算平均分
|
||||
app.review_count = (app.review_count or 0) + 1
|
||||
if app.review_count == 1:
|
||||
app.rating_avg = req.rating
|
||||
else:
|
||||
total_rating = float(app.rating_avg or 0) * (app.review_count - 1) + req.rating
|
||||
app.rating_avg = round(total_rating / app.review_count, 1)
|
||||
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"rating_avg": float(app.rating_avg or 0),
|
||||
"review_count": app.review_count or 0,
|
||||
},
|
||||
"message": "评价成功",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/my-apps")
|
||||
def get_my_apps(
|
||||
status: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
query = db.query(AppPurchase).filter(AppPurchase.user_id == current_user.id)
|
||||
purchases = query.order_by(desc(AppPurchase.created_at)).all()
|
||||
|
||||
result = []
|
||||
for p in purchases:
|
||||
app = db.query(MarketApp).filter(MarketApp.id == p.app_id).first()
|
||||
if app:
|
||||
result.append({
|
||||
"id": p.id,
|
||||
"app_id": app.id,
|
||||
"name": app.name,
|
||||
"icon": app.icon,
|
||||
"type": app.type,
|
||||
"price": float(app.price or 0),
|
||||
"purchased_at": str(p.created_at) if p.created_at else None,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.get("/reviews/{app_id}")
|
||||
def get_reviews(
|
||||
app_id: str,
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
query = db.query(AppReview).filter(AppReview.app_id == app_id)
|
||||
total = query.count()
|
||||
|
||||
reviews = (
|
||||
query.order_by(desc(AppReview.created_at))
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for r in reviews:
|
||||
user = db.query(User).filter(User.id == r.user_id).first()
|
||||
result.append({
|
||||
"id": r.id,
|
||||
"user_id": r.user_id,
|
||||
"username": user.username if user else "unknown",
|
||||
"avatar": user.avatar if user else None,
|
||||
"rating": r.rating,
|
||||
"content": r.content,
|
||||
"created_at": str(r.created_at) if r.created_at else None,
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {"total": total, "page": page, "page_size": page_size, "items": result},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Agent部署资源查询接口
|
||||
# ============================================================
|
||||
|
||||
@router.get("/agent-resources")
|
||||
def get_agent_resources(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Agent部署时需要的资源列表:可用的应用、知识库、文档"""
|
||||
# 用户已发布的应用
|
||||
my_apps = db.query(MarketApp).filter(
|
||||
MarketApp.publisher_id == current_user.id,
|
||||
MarketApp.status == "published",
|
||||
).order_by(desc(MarketApp.updated_at)).all()
|
||||
|
||||
apps_result = []
|
||||
for app in my_apps:
|
||||
apps_result.append({
|
||||
"id": app.id,
|
||||
"name": app.name,
|
||||
"type": app.type,
|
||||
"description": app.description,
|
||||
"install_count": app.install_count or 0,
|
||||
})
|
||||
|
||||
# 用户知识库
|
||||
from ..models import KnowledgeBase, KnowledgeDocument
|
||||
kb_list = db.query(KnowledgeBase).filter(
|
||||
KnowledgeBase.user_id == current_user.id
|
||||
).order_by(desc(KnowledgeBase.updated_at)).all()
|
||||
|
||||
kb_result = []
|
||||
for kb in kb_list:
|
||||
doc_count = db.query(KnowledgeDocument).filter(
|
||||
KnowledgeDocument.knowledge_base_id == kb.id
|
||||
).count()
|
||||
kb_result.append({
|
||||
"id": kb.id,
|
||||
"name": kb.name,
|
||||
"description": kb.description,
|
||||
"document_count": doc_count,
|
||||
})
|
||||
|
||||
# 用户已安装的应用(从AppPurchase)
|
||||
installed = db.query(AppPurchase).filter(
|
||||
AppPurchase.user_id == current_user.id
|
||||
).all()
|
||||
installed_ids = [p.app_id for p in installed]
|
||||
installed_apps = []
|
||||
if installed_ids:
|
||||
iapps = db.query(MarketApp).filter(MarketApp.id.in_(installed_ids)).all()
|
||||
for ia in iapps:
|
||||
installed_apps.append({
|
||||
"id": ia.id,
|
||||
"name": ia.name,
|
||||
"type": ia.type,
|
||||
"description": ia.description,
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"my_apps": apps_result,
|
||||
"knowledge_bases": kb_result,
|
||||
"installed_apps": installed_apps,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, desc, asc, func
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
User, Notification,
|
||||
gen_uuid
|
||||
)
|
||||
from ..auth import get_current_user
|
||||
from ..middleware.log_service import LogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def list_notifications(
|
||||
type: Optional[str] = Query(None, description="通知类型"),
|
||||
is_read: Optional[bool] = Query(None),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
query = db.query(Notification).filter(
|
||||
Notification.user_id == current_user.id
|
||||
)
|
||||
|
||||
if type:
|
||||
query = query.filter(Notification.type == type)
|
||||
if is_read is not None:
|
||||
query = query.filter(Notification.is_read == is_read)
|
||||
|
||||
total = query.count()
|
||||
notifications = (
|
||||
query.order_by(desc(Notification.created_at))
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for n in notifications:
|
||||
result.append({
|
||||
"id": n.id,
|
||||
"type": n.type,
|
||||
"title": n.title,
|
||||
"content": n.content,
|
||||
"is_read": n.is_read,
|
||||
"related_id": n.related_id,
|
||||
"related_type": n.related_type,
|
||||
"created_at": str(n.created_at) if n.created_at else None,
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": result,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{notification_id}/read")
|
||||
def mark_as_read(
|
||||
notification_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
notification = db.query(Notification).filter(
|
||||
and_(
|
||||
Notification.id == notification_id,
|
||||
Notification.user_id == current_user.id,
|
||||
)
|
||||
).first()
|
||||
|
||||
if not notification:
|
||||
raise HTTPException(status_code=404, detail="通知不存在")
|
||||
|
||||
notification.is_read = True
|
||||
notification.read_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "已标记为已读"}
|
||||
|
||||
|
||||
@router.put("/read-all")
|
||||
def mark_all_as_read(
|
||||
type: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
query = db.query(Notification).filter(
|
||||
and_(
|
||||
Notification.user_id == current_user.id,
|
||||
Notification.is_read == False,
|
||||
)
|
||||
)
|
||||
|
||||
if type:
|
||||
query = query.filter(Notification.type == type)
|
||||
|
||||
notifications = query.all()
|
||||
now = datetime.now()
|
||||
count = 0
|
||||
for n in notifications:
|
||||
n.is_read = True
|
||||
n.read_at = now
|
||||
count += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "data": {"count": count}, "message": f"已标记 {count} 条为已读"}
|
||||
|
||||
|
||||
@router.get("/unread-count")
|
||||
def get_unread_count(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
count = db.query(func.count(Notification.id)).filter(
|
||||
and_(
|
||||
Notification.user_id == current_user.id,
|
||||
Notification.is_read == False,
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
return {"code": 0, "data": {"count": count}, "message": "ok"}
|
||||
@@ -0,0 +1,608 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, desc, asc
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from datetime import datetime, date
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
User, MembershipPlan, Order, Coupon, ValueAddedService,
|
||||
CustomServiceOrder, CustomServiceRequest, MembershipSubscription,
|
||||
gen_uuid
|
||||
)
|
||||
from ..auth import get_current_user
|
||||
from ..middleware.log_service import LogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CreateOrderRequest(BaseModel):
|
||||
item_id: str
|
||||
coupon_id: Optional[str] = None
|
||||
order_type: str = "membership"
|
||||
amount: Optional[float] = None
|
||||
|
||||
|
||||
class UseCouponRequest(BaseModel):
|
||||
code: str
|
||||
order_id: Optional[str] = None
|
||||
|
||||
|
||||
class CustomServiceOrderRequest(BaseModel):
|
||||
service_id: str
|
||||
description: Optional[str] = None
|
||||
budget: Optional[float] = None
|
||||
contact_info: Optional[str] = None
|
||||
|
||||
|
||||
class ServiceRequestModel(BaseModel):
|
||||
"""增值服务工单请求模型(数据定制/私有化/平台代理)"""
|
||||
service_id: str
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
requirements: Optional[dict] = None # 需求详情
|
||||
|
||||
|
||||
@router.get("/plans")
|
||||
def list_plans(db: Session = Depends(get_db)):
|
||||
plans = db.query(MembershipPlan).filter(MembershipPlan.is_active == True).order_by(asc(MembershipPlan.level)).all()
|
||||
|
||||
result = []
|
||||
for p in plans:
|
||||
result.append({
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"level": p.level,
|
||||
"duration_type": p.duration_type,
|
||||
"price": float(p.price) if p.price else 0,
|
||||
"daily_free_tokens": p.daily_free_tokens or 0,
|
||||
"features": p.features or [],
|
||||
"is_active": p.is_active,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.post("/orders")
|
||||
def create_order(
|
||||
req: CreateOrderRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
plan = db.query(MembershipPlan).filter(
|
||||
and_(MembershipPlan.id == req.item_id, MembershipPlan.is_active == True)
|
||||
).first()
|
||||
if not plan:
|
||||
raise HTTPException(status_code=404, detail="会员计划不存在或已下线")
|
||||
|
||||
amount = req.amount or float(plan.price or 0)
|
||||
|
||||
order = Order(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
order_type=req.order_type if hasattr(req, 'order_type') and req.order_type else "membership",
|
||||
item_id=req.item_id,
|
||||
item_name=plan.name,
|
||||
amount=amount,
|
||||
status="pending",
|
||||
coupon_id=req.coupon_id if hasattr(req, 'coupon_id') and req.coupon_id else None,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="create_order",
|
||||
target="payment_order", target_id=order.id,
|
||||
detail=f"创建订单 #{order.id},金额 {amount}",
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": order.id,
|
||||
"item_id": order.item_id,
|
||||
"plan_name": plan.name,
|
||||
"amount": float(order.amount),
|
||||
"status": order.status,
|
||||
"created_at": str(order.created_at),
|
||||
},
|
||||
"message": "订单创建成功",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/orders")
|
||||
def list_orders(
|
||||
status: Optional[str] = Query(None),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
query = db.query(Order).filter(Order.user_id == current_user.id)
|
||||
if status:
|
||||
query = query.filter(Order.status == status)
|
||||
|
||||
total = query.count()
|
||||
orders = query.order_by(desc(Order.created_at)).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
result = []
|
||||
for o in orders:
|
||||
plan = db.query(MembershipPlan).filter(MembershipPlan.id == o.item_id).first()
|
||||
result.append({
|
||||
"id": o.id,
|
||||
"item_id": o.item_id,
|
||||
"plan_name": plan.name if plan else "unknown",
|
||||
"amount": float(o.amount),
|
||||
"status": o.status,
|
||||
"coupon_id": o.coupon_id,
|
||||
|
||||
"paid_at": str(o.paid_at) if o.paid_at else None,
|
||||
"created_at": str(o.created_at),
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {"total": total, "page": page, "page_size": page_size, "items": result},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/orders/{order_id}")
|
||||
def get_order(
|
||||
order_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
order = db.query(Order).filter(
|
||||
and_(Order.id == order_id, Order.user_id == current_user.id)
|
||||
).first()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
|
||||
plan = db.query(MembershipPlan).filter(MembershipPlan.id == order.item_id).first()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": order.id,
|
||||
"item_id": order.item_id,
|
||||
"plan_name": plan.name if plan else "unknown",
|
||||
"amount": float(order.amount),
|
||||
"status": order.status,
|
||||
"coupon_id": order.coupon_id,
|
||||
|
||||
"paid_at": str(order.paid_at) if order.paid_at else None,
|
||||
"created_at": str(order.created_at),
|
||||
"updated_at": str(order.updated_at),
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/orders/{order_id}/pay")
|
||||
def pay_order(
|
||||
order_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
order = db.query(Order).filter(
|
||||
and_(Order.id == order_id, Order.user_id == current_user.id)
|
||||
).first()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
|
||||
if order.status != "pending":
|
||||
raise HTTPException(status_code=400, detail="订单状态不允许支付")
|
||||
|
||||
# 模拟支付成功
|
||||
order.status = "paid"
|
||||
order.paid_at = datetime.now()
|
||||
order.updated_at = datetime.now()
|
||||
|
||||
# 激活会员
|
||||
plan = db.query(MembershipPlan).filter(MembershipPlan.id == order.item_id).first()
|
||||
if plan:
|
||||
# 查找或创建会员记录
|
||||
membership = db.query(MembershipSubscription).filter(
|
||||
MembershipSubscription.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if membership:
|
||||
# 如果已有会员,延长有效期
|
||||
if membership.end_at and membership.end_at > datetime.now():
|
||||
# 从当前到期日开始延长
|
||||
membership.end_at = membership.end_at + __import__("datetime").timedelta(days=30)
|
||||
else:
|
||||
# 已过期,从现在开始
|
||||
membership.end_at = datetime.now() + __import__("datetime").timedelta(days=30)
|
||||
membership.plan_id = plan.id
|
||||
membership.status = "active"
|
||||
membership.updated_at = datetime.now()
|
||||
else:
|
||||
membership = MembershipSubscription(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
plan_id=plan.id,
|
||||
status="active",
|
||||
start_at=datetime.now(),
|
||||
end_at=datetime.now() + __import__("datetime").timedelta(days=30),
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(membership)
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="pay_order",
|
||||
target="payment_order", target_id=order.id,
|
||||
detail=f"支付订单 #{order.id},金额 {order.amount}",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"order_id": order.id,
|
||||
"status": order.status,
|
||||
"paid_at": str(order.paid_at),
|
||||
},
|
||||
"message": "支付成功",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/coupons")
|
||||
def list_coupons(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
from ..models import UserCoupon
|
||||
now = datetime.now()
|
||||
# 先获取用户的user_coupons
|
||||
user_coupons = db.query(UserCoupon).filter(
|
||||
and_(
|
||||
UserCoupon.user_id == current_user.id,
|
||||
UserCoupon.status == "unused",
|
||||
)
|
||||
).all()
|
||||
|
||||
result = []
|
||||
coupon_ids = [uc.coupon_id for uc in user_coupons]
|
||||
if coupon_ids:
|
||||
coupons = db.query(Coupon).filter(
|
||||
and_(
|
||||
Coupon.id.in_(coupon_ids),
|
||||
Coupon.status == "active",
|
||||
Coupon.valid_to > now,
|
||||
)
|
||||
).order_by(asc(Coupon.valid_to)).all()
|
||||
|
||||
for c in coupons:
|
||||
result.append({
|
||||
"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,
|
||||
"expire_at": str(c.valid_to) if c.valid_to else None,
|
||||
"status": c.status,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.post("/coupons/use")
|
||||
def use_coupon(
|
||||
req: UseCouponRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
from ..models import UserCoupon
|
||||
coupon = db.query(Coupon).filter(
|
||||
and_(
|
||||
Coupon.code == req.code,
|
||||
Coupon.status == "active",
|
||||
)
|
||||
).first()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在或已失效")
|
||||
|
||||
# 检查用户是否持有该优惠券
|
||||
user_coupon = db.query(UserCoupon).filter(
|
||||
and_(
|
||||
UserCoupon.coupon_id == coupon.id,
|
||||
UserCoupon.user_id == current_user.id,
|
||||
UserCoupon.status == "unused",
|
||||
)
|
||||
).first()
|
||||
if not user_coupon:
|
||||
raise HTTPException(status_code=404, detail="优惠券不存在或已使用")
|
||||
|
||||
if coupon.valid_to and coupon.valid_to < datetime.now():
|
||||
user_coupon.status = "expired"
|
||||
db.commit()
|
||||
raise HTTPException(status_code=400, detail="优惠券已过期")
|
||||
|
||||
# 标记为已使用
|
||||
user_coupon.status = "used"
|
||||
user_coupon.used_at = datetime.now()
|
||||
|
||||
if req.order_id:
|
||||
order = db.query(Order).filter(
|
||||
and_(Order.id == req.order_id, Order.user_id == current_user.id)
|
||||
).first()
|
||||
if order and order.status == "pending":
|
||||
# 应用优惠券折扣
|
||||
discount = 0
|
||||
if coupon.type == "fixed":
|
||||
discount = float(coupon.value or 0)
|
||||
elif coupon.type == "percent":
|
||||
discount = float(order.amount) * float(coupon.value or 0) / 100
|
||||
|
||||
order.amount = max(0, float(order.amount) - discount)
|
||||
order.coupon_id = coupon.id
|
||||
order.discount_amount = discount
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "data": {"code": coupon.code, "status": user_coupon.status}, "message": "优惠券使用成功"}
|
||||
|
||||
|
||||
@router.get("/services")
|
||||
def list_services(db: Session = Depends(get_db)):
|
||||
services = db.query(ValueAddedService).filter(
|
||||
ValueAddedService.status == "active"
|
||||
).order_by(asc(ValueAddedService.created_at)).all()
|
||||
|
||||
result = []
|
||||
for s in services:
|
||||
result.append({
|
||||
"id": s.id,
|
||||
"name": s.name,
|
||||
"description": s.description,
|
||||
"type": s.type,
|
||||
"price": float(s.price) if s.price else 0,
|
||||
"features": s.features or [],
|
||||
"assigned_agent_id": s.assigned_agent_id,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.post("/custom-services")
|
||||
def create_custom_service_order(
|
||||
req: CustomServiceOrderRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
service = db.query(ValueAddedService).filter(
|
||||
and_(ValueAddedService.id == req.service_id, ValueAddedService.status == "active")
|
||||
).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="服务不存在或已下线")
|
||||
|
||||
order = CustomServiceOrder(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
service_id=req.service_id,
|
||||
service_type=service.type,
|
||||
description=req.description,
|
||||
price=req.budget or float(service.price or 0),
|
||||
status="pending",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(order)
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="create_custom_service",
|
||||
target="custom_service_order", target_id=order.id,
|
||||
detail=f"创建定制服务订单 #{order.id},服务: {service.name}",
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": order.id,
|
||||
"service_id": order.service_id,
|
||||
"service_name": service.name,
|
||||
"price": float(order.price) if order.price else 0,
|
||||
"status": order.status,
|
||||
"created_at": str(order.created_at),
|
||||
},
|
||||
"message": "定制服务订单已提交",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/service-requests")
|
||||
def create_service_request(
|
||||
req: ServiceRequestModel,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建增值服务工单(平台代理/数据定制/私有化项目)"""
|
||||
service = db.query(ValueAddedService).filter(
|
||||
and_(ValueAddedService.id == req.service_id, ValueAddedService.status == "active")
|
||||
).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="服务不存在或已下线")
|
||||
|
||||
# 验证服务类型是否为工单性质
|
||||
if service.type not in ("platform_proxy", "data_custom", "private_project"):
|
||||
raise HTTPException(status_code=400, detail="此服务不支持工单提交")
|
||||
|
||||
request_obj = CustomServiceRequest(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
service_id=req.service_id,
|
||||
request_type=service.type,
|
||||
title=req.title,
|
||||
description=req.description,
|
||||
requirements=req.requirements,
|
||||
status="pending",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(request_obj)
|
||||
db.commit()
|
||||
db.refresh(request_obj)
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username,
|
||||
action="create_service_request",
|
||||
target="custom_service_request", target_id=request_obj.id,
|
||||
detail=f"创建服务工单 #{request_obj.id},类型: {service.type}, 标题: {req.title}",
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": request_obj.id,
|
||||
"service_id": request_obj.service_id,
|
||||
"service_name": service.name,
|
||||
"request_type": service.type,
|
||||
"title": request_obj.title,
|
||||
"status": request_obj.status,
|
||||
"created_at": str(request_obj.created_at),
|
||||
},
|
||||
"message": "服务工单已提交",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 电子凭证开具 API
|
||||
# ============================================================
|
||||
|
||||
class ReceiptRequest(BaseModel):
|
||||
receipt_type: str = Field(default="electronic") # electronic / invoice
|
||||
title: Optional[str] = None # 发票抬头(公司名称或个人姓名)
|
||||
tax_number: Optional[str] = None # 税号
|
||||
|
||||
|
||||
@router.post("/orders/{order_id}/receipt")
|
||||
def generate_receipt(
|
||||
order_id: str,
|
||||
req: ReceiptRequest = Depends(),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""为已支付订单开具电子凭证"""
|
||||
order = db.query(Order).filter(
|
||||
and_(Order.id == order_id, Order.user_id == current_user.id)
|
||||
).first()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
|
||||
if order.status != "paid":
|
||||
raise HTTPException(status_code=400, detail="仅已支付订单可开具电子凭证")
|
||||
|
||||
# 检查是否已开过凭证
|
||||
if order.receipt_number:
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"receipt_number": order.receipt_number,
|
||||
"receipt_type": order.receipt_type or "electronic",
|
||||
"title": order.receipt_title or current_user.username,
|
||||
"amount": float(order.amount),
|
||||
"issued_at": order.receipt_issued_at.strftime("%Y-%m-%d %H:%M:%S") if order.receipt_issued_at else "",
|
||||
"status": "issued",
|
||||
"message": "该订单已开具电子凭证",
|
||||
},
|
||||
"message": "电子凭证已存在",
|
||||
}
|
||||
|
||||
# 生成凭证编号:RCT + 年月日 + 随机6位
|
||||
now = datetime.now()
|
||||
receipt_number = f"RCT{now.strftime('%Y%m%d')}{uuid.uuid4().hex[:6].upper()}"
|
||||
|
||||
# 保存凭证信息到订单
|
||||
order.receipt_number = receipt_number
|
||||
order.receipt_type = req.receipt_type
|
||||
order.receipt_title = req.title or current_user.username
|
||||
order.receipt_tax_number = req.tax_number
|
||||
order.receipt_issued_at = now
|
||||
order.updated_at = now
|
||||
|
||||
# 记录日志
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="generate_receipt",
|
||||
target="payment_order", target_id=order.id,
|
||||
detail=f"为订单 #{order.id} 开具电子凭证 {receipt_number}",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
# 构建凭证详情
|
||||
receipt_data = {
|
||||
"receipt_number": receipt_number,
|
||||
"receipt_type": req.receipt_type,
|
||||
"title": req.title or current_user.username,
|
||||
"tax_number": req.tax_number,
|
||||
"order_id": order.id,
|
||||
"amount": float(order.amount),
|
||||
"order_type": order.order_type or "membership",
|
||||
"item_name": order.item_name or "会员订阅",
|
||||
"payment_method": order.payment_method or "微信支付",
|
||||
"payment_id": order.payment_id or f"PAY{now.strftime('%Y%m%d%H%M%S')}",
|
||||
"paid_at": order.paid_at.strftime("%Y-%m-%d %H:%M:%S") if hasattr(order, 'paid_at') and order.paid_at else now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"issued_at": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"customer_name": current_user.username,
|
||||
"customer_email": current_user.email or "",
|
||||
"customer_phone": current_user.phone or "",
|
||||
"status": "issued",
|
||||
"platform_name": "AIFlow智能自动化PaaS平台",
|
||||
"remarks": "此电子凭证由AIFlow平台自动生成,与纸质凭证具有同等效力。如需帮助,请联系客服。",
|
||||
}
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": receipt_data,
|
||||
"message": "电子凭证开具成功",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/orders/{order_id}/receipt")
|
||||
def get_receipt(
|
||||
order_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取订单已有的电子凭证"""
|
||||
order = db.query(Order).filter(
|
||||
and_(Order.id == order_id, Order.user_id == current_user.id)
|
||||
).first()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
|
||||
if not order.receipt_number:
|
||||
return {
|
||||
"code": 0,
|
||||
"data": None,
|
||||
"message": "该订单尚未开具电子凭证",
|
||||
}
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"receipt_number": order.receipt_number,
|
||||
"receipt_type": order.receipt_type or "electronic",
|
||||
"title": order.receipt_title or current_user.username,
|
||||
"tax_number": order.receipt_tax_number,
|
||||
"order_id": order.id,
|
||||
"amount": float(order.amount),
|
||||
"order_type": order.order_type or "membership",
|
||||
"item_name": order.item_name or "会员订阅",
|
||||
"payment_method": order.payment_method or "微信支付",
|
||||
"issued_at": order.receipt_issued_at.strftime("%Y-%m-%d %H:%M:%S") if order.receipt_issued_at else "",
|
||||
"status": "issued" if order.receipt_number else "not_issued",
|
||||
"platform_name": "AIFlow智能自动化PaaS平台",
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, desc, asc
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from datetime import datetime, date
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
User, LoginLog, ApiKey, MembershipSubscription, AppPurchase, MarketApp,
|
||||
gen_uuid
|
||||
)
|
||||
from ..auth import get_current_user, hash_password, verify_password
|
||||
from ..middleware.log_service import LogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class UpdateProfileRequest(BaseModel):
|
||||
username: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
|
||||
|
||||
class UpdatePasswordRequest(BaseModel):
|
||||
old_password: str
|
||||
new_password: str = Field(min_length=6, max_length=100)
|
||||
|
||||
|
||||
class CreateApiKeyRequest(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=100)
|
||||
|
||||
|
||||
class UpdateApiKeyRequest(BaseModel):
|
||||
status: str # enabled, disabled, revoked
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def get_profile(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
# 获取会员信息
|
||||
membership = db.query(MembershipSubscription).filter(
|
||||
MembershipSubscription.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
membership_info = None
|
||||
if membership:
|
||||
membership_info = {
|
||||
"id": membership.id,
|
||||
"plan_id": membership.plan_id,
|
||||
"status": membership.status,
|
||||
"start_at": str(membership.start_at) if membership.start_at else None,
|
||||
"expire_at": str(membership.end_at) if membership.end_at else None,
|
||||
"is_expired": membership.end_at < datetime.now() if membership.end_at else False,
|
||||
}
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": current_user.id,
|
||||
"username": current_user.username,
|
||||
"email": current_user.email,
|
||||
"phone": current_user.phone,
|
||||
"avatar": current_user.avatar,
|
||||
"role": current_user.role,
|
||||
"status": current_user.status,
|
||||
"membership": membership_info,
|
||||
"created_at": str(current_user.created_at) if current_user.created_at else None,
|
||||
"updated_at": str(current_user.updated_at) if current_user.updated_at else None,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.put("/")
|
||||
def update_profile(
|
||||
req: UpdateProfileRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
# 从当前会话中重新获取用户对象
|
||||
db_user = db.query(User).filter(User.id == current_user.id).first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
if req.username is not None and req.username != db_user.username:
|
||||
# 检查用户名是否被占用
|
||||
existing = db.query(User).filter(User.username == req.username).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="用户名已被占用")
|
||||
db_user.username = req.username
|
||||
|
||||
if req.email is not None and req.email != db_user.email:
|
||||
existing = db.query(User).filter(User.email == req.email).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="邮箱已被占用")
|
||||
db_user.email = req.email
|
||||
|
||||
if req.phone is not None:
|
||||
db_user.phone = req.phone
|
||||
if req.avatar is not None:
|
||||
db_user.avatar = req.avatar
|
||||
|
||||
db_user.updated_at = datetime.now()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=db_user.id, username=db_user.username, action="update_profile",
|
||||
target="user", target_id=db_user.id,
|
||||
detail=f"用户 {db_user.username} 更新了个人信息",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": db_user.id,
|
||||
"username": db_user.username,
|
||||
"email": db_user.email,
|
||||
"phone": db_user.phone,
|
||||
"avatar": db_user.avatar,
|
||||
},
|
||||
"message": "更新成功",
|
||||
}
|
||||
|
||||
|
||||
@router.put("/password")
|
||||
def update_password(
|
||||
req: UpdatePasswordRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
db_user = db.query(User).filter(User.id == current_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=db_user.id, username=db_user.username, action="change_password",
|
||||
target="user", target_id=db_user.id,
|
||||
detail=f"用户 {db_user.username} 修改了密码",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "message": "密码修改成功"}
|
||||
|
||||
|
||||
@router.get("/login-logs")
|
||||
def get_login_logs(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
query = db.query(LoginLog).filter(LoginLog.user_id == current_user.id)
|
||||
total = query.count()
|
||||
|
||||
logs = (
|
||||
query.order_by(desc(LoginLog.login_at))
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for log_item in logs:
|
||||
result.append({
|
||||
"id": log_item.id,
|
||||
"ip": log_item.ip,
|
||||
"user_agent": log_item.user_agent,
|
||||
"login_at": str(log_item.login_at) if log_item.login_at else None,
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {"total": total, "page": page, "page_size": page_size, "items": result},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api-keys")
|
||||
def get_api_keys(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
keys = (
|
||||
db.query(ApiKey)
|
||||
.filter(ApiKey.user_id == current_user.id)
|
||||
.order_by(desc(ApiKey.created_at))
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for k in keys:
|
||||
preview = None
|
||||
if k.api_key and len(k.api_key) > 12:
|
||||
preview = k.api_key[:8] + "****" + k.api_key[-4:]
|
||||
result.append({
|
||||
"id": k.id,
|
||||
"name": k.name,
|
||||
"key_preview": preview or "****",
|
||||
"status": k.status,
|
||||
"last_used_at": str(k.last_used_at) if k.last_used_at else None,
|
||||
"created_at": str(k.created_at) if k.created_at else None,
|
||||
"expire_at": str(k.expired_at) if k.expired_at else None,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.post("/api-keys")
|
||||
def create_api_key(
|
||||
req: CreateApiKeyRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
# 生成API密钥
|
||||
key_value = f"sk-{gen_uuid().replace('-', '')}"
|
||||
|
||||
api_key = ApiKey(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
name=req.name,
|
||||
api_key=key_value,
|
||||
secret=f"sec-{gen_uuid().replace('-', '')}",
|
||||
status="active",
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
db.add(api_key)
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="create_api_key",
|
||||
target="api_key", target_id=api_key.id,
|
||||
detail=f"用户 {current_user.username} 创建了API密钥 {api_key.name}",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(api_key)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": api_key.id,
|
||||
"name": api_key.name,
|
||||
"api_key": key_value, # 仅创建时返回完整key
|
||||
"status": api_key.status,
|
||||
"created_at": str(api_key.created_at),
|
||||
},
|
||||
"message": "API密钥创建成功,请妥善保管",
|
||||
}
|
||||
|
||||
|
||||
@router.put("/api-keys/{key_id}")
|
||||
def update_api_key(
|
||||
key_id: str,
|
||||
req: UpdateApiKeyRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
api_key = db.query(ApiKey).filter(
|
||||
and_(ApiKey.id == key_id, ApiKey.user_id == current_user.id)
|
||||
).first()
|
||||
if not api_key:
|
||||
raise HTTPException(status_code=404, detail="API密钥不存在")
|
||||
|
||||
if req.status not in ("enabled", "disabled", "revoked"):
|
||||
raise HTTPException(status_code=400, detail="无效的状态值")
|
||||
|
||||
api_key.status = req.status
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="update_api_key",
|
||||
target="api_key", target_id=api_key.id,
|
||||
detail=f"用户 {current_user.username} 更新了API密钥 {api_key.name} 状态为 {req.status}",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"code": 0, "data": {"id": api_key.id, "status": api_key.status}, "message": "更新成功"}
|
||||
|
||||
|
||||
@router.get("/membership")
|
||||
def get_membership(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
membership = db.query(MembershipSubscription).filter(
|
||||
MembershipSubscription.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not membership:
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {"has_membership": False, "status": None},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
is_expired = membership.end_at < datetime.now() if membership.end_at else False
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"has_membership": True,
|
||||
"id": membership.id,
|
||||
"plan_id": membership.plan_id,
|
||||
"status": "expired" if is_expired else membership.status,
|
||||
"start_at": str(membership.start_at) if membership.start_at else None,
|
||||
"expire_at": str(membership.end_at) if membership.end_at else None,
|
||||
"is_expired": is_expired,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/purchased-apps")
|
||||
def get_purchased_apps(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
purchases = (
|
||||
db.query(AppPurchase)
|
||||
.filter(AppPurchase.user_id == current_user.id)
|
||||
.order_by(desc(AppPurchase.created_at))
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for p in purchases:
|
||||
app = db.query(MarketApp).filter(MarketApp.id == p.app_id).first()
|
||||
result.append({
|
||||
"id": p.id,
|
||||
"app_id": p.app_id,
|
||||
"app_name": app.name if app else "unknown",
|
||||
"app_icon": app.icon if app else None,
|
||||
"app_type": app.type if app else None,
|
||||
"price": float(p.price if p.price and app else 0),
|
||||
"purchased_at": str(p.created_at) if p.created_at else None,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
@@ -0,0 +1,247 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, desc, asc, func, extract
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from datetime import datetime, date, timedelta
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
User, TokenUsageLog, TokenBalance, AIModel,
|
||||
gen_uuid
|
||||
)
|
||||
from ..auth import get_current_user
|
||||
from ..middleware.log_service import LogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class RechargeRequest(BaseModel):
|
||||
amount: int = Field(ge=1)
|
||||
channel: str = Field(default="simulation")
|
||||
|
||||
|
||||
@router.get("/overview")
|
||||
def get_overview(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
# 总消耗
|
||||
total_used = db.query(func.coalesce(func.sum(TokenUsageLog.total_tokens), 0)).filter(
|
||||
TokenUsageLog.user_id == current_user.id
|
||||
).scalar()
|
||||
|
||||
# 今日使用中应用数
|
||||
today_start = datetime.combine(date.today(), datetime.min.time())
|
||||
today_apps = db.query(func.count(func.distinct(TokenUsageLog.model_id))).filter(
|
||||
and_(
|
||||
TokenUsageLog.user_id == current_user.id,
|
||||
TokenUsageLog.created_at >= today_start,
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
# 余额
|
||||
balance = db.query(TokenBalance).filter(
|
||||
TokenBalance.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
from datetime import timezone, timedelta
|
||||
shanghai_tz = timezone(timedelta(hours=8))
|
||||
server_time = datetime.now(shanghai_tz).isoformat()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total_used": total_used or 0,
|
||||
"balance": balance.balance if balance else 0,
|
||||
"today_apps": today_apps,
|
||||
"server_time": server_time,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/rankings")
|
||||
def get_rankings(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
# Token消耗最多的模型
|
||||
model_rankings = (
|
||||
db.query(
|
||||
TokenUsageLog.model_id,
|
||||
func.sum(TokenUsageLog.total_tokens).label("total"),
|
||||
func.count(TokenUsageLog.id).label("count"),
|
||||
)
|
||||
.filter(TokenUsageLog.user_id == current_user.id)
|
||||
.group_by(TokenUsageLog.model_id)
|
||||
.order_by(desc("total"))
|
||||
.all()
|
||||
)
|
||||
|
||||
models_list = []
|
||||
for r in model_rankings:
|
||||
model = db.query(AIModel).filter(AIModel.id == r.model_id).first()
|
||||
models_list.append({
|
||||
"model_id": r.model_id,
|
||||
"model_name": model.name if model else "unknown",
|
||||
"total_tokens": int(r.total),
|
||||
"usage_count": int(r.count),
|
||||
})
|
||||
|
||||
# TOP3应用
|
||||
app_rankings = (
|
||||
db.query(
|
||||
TokenUsageLog.conversation_id,
|
||||
func.sum(TokenUsageLog.total_tokens).label("total"),
|
||||
)
|
||||
.filter(TokenUsageLog.user_id == current_user.id)
|
||||
.group_by(TokenUsageLog.conversation_id)
|
||||
.order_by(desc("total"))
|
||||
.limit(3)
|
||||
.all()
|
||||
)
|
||||
|
||||
apps_list = []
|
||||
for r in app_rankings:
|
||||
apps_list.append({
|
||||
"conversation_id": r.conversation_id,
|
||||
"total_tokens": int(r.total),
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"model_rankings": models_list,
|
||||
"top_apps": apps_list,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/logs")
|
||||
def get_token_logs(
|
||||
start_date: Optional[str] = Query(None, description="开始日期 YYYY-MM-DD"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期 YYYY-MM-DD"),
|
||||
model_id: Optional[str] = Query(None),
|
||||
app_type: Optional[str] = Query(None),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
query = db.query(TokenUsageLog).filter(TokenUsageLog.user_id == current_user.id)
|
||||
|
||||
if start_date:
|
||||
try:
|
||||
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.filter(TokenUsageLog.created_at >= start_dt)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="开始日期格式错误,请使用YYYY-MM-DD")
|
||||
|
||||
if end_date:
|
||||
try:
|
||||
end_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
query = query.filter(TokenUsageLog.created_at < end_dt)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="结束日期格式错误,请使用YYYY-MM-DD")
|
||||
|
||||
if model_id:
|
||||
query = query.filter(TokenUsageLog.model_id == model_id)
|
||||
|
||||
total = query.count()
|
||||
logs = (
|
||||
query.order_by(desc(TokenUsageLog.created_at))
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for log_item in logs:
|
||||
model = db.query(AIModel).filter(AIModel.id == log_item.model_id).first()
|
||||
result.append({
|
||||
"id": log_item.id,
|
||||
"model_id": log_item.model_id,
|
||||
"model_name": model.name if model else "unknown",
|
||||
"prompt_tokens": log_item.prompt_tokens or 0,
|
||||
"completion_tokens": log_item.completion_tokens or 0,
|
||||
"total_tokens": log_item.total_tokens or 0,
|
||||
"conversation_id": log_item.conversation_id,
|
||||
"created_at": log_item.created_at.isoformat() if log_item.created_at else None,
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": result,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/balance")
|
||||
def get_balance(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
balance = db.query(TokenBalance).filter(
|
||||
TokenBalance.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"balance": balance.balance if balance else 0,
|
||||
"updated_at": str(balance.updated_at) if balance and balance.updated_at else None,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/recharge")
|
||||
def recharge(
|
||||
req: RechargeRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
# 获取或创建余额记录
|
||||
balance = db.query(TokenBalance).filter(
|
||||
TokenBalance.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not balance:
|
||||
balance = TokenBalance(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
balance=0,
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(balance)
|
||||
db.flush()
|
||||
|
||||
old_balance = balance.balance or 0
|
||||
balance.balance = old_balance + req.amount
|
||||
balance.updated_at = datetime.now()
|
||||
|
||||
# 记录操作日志
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="recharge",
|
||||
target="token_balance", target_id=balance.id,
|
||||
detail=f"充值 {req.amount} tokens,余额从 {old_balance} 变为 {balance.balance}",
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"balance": balance.balance,
|
||||
"recharged": req.amount,
|
||||
},
|
||||
"message": "充值成功",
|
||||
}
|
||||
Reference in new issue
Block a user