初始提交: 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

+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": "风控规则已删除"}