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

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

No files matched your search

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