refactor: sync running-container source back to repo for reproducible docker compose build; remove dead code (orphan pipeline.py + 104 stale files from iterative dev) and drop unused app/ mirror dir

This commit is contained in:
qyyfw committed 2026-07-11 06:29:32 +08:00
1 parent 1346fbbd4b
commit 5d7f32db0a
138 files changed
+12846 -84248

No files matched your search

@@ -1,904 +0,0 @@
"""
OSS存储配置管理 API
Phase 3.1: 支持 MinIO / 阿里云OSS / AWS S3 等对象存储配置
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Body
from sqlalchemy import or_, func, desc, asc, text
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from datetime import datetime
import uuid
import json
import logging
import httpx
from ...database import get_db
from ...models import (
gen_uuid,
)
from ...auth import get_current_admin
from ...middleware.log_service import LogService
router = APIRouter()
logger = logging.getLogger("aiflow")
# ============================================================
# Pydantic Models
# ============================================================
class OSSConfigCreate(BaseModel):
name: str = Field(min_length=1, max_length=200)
provider: str = Field(min_length=1, max_length=20)
endpoint: str = Field(min_length=1, max_length=500)
access_key: str = Field(min_length=1, max_length=255)
secret_key: str = Field(min_length=1, max_length=255)
bucket: str = Field(min_length=1, max_length=200)
region: Optional[str] = ""
base_path: Optional[str] = ""
is_default: bool = False
is_enabled: bool = True
config: Optional[Dict[str, Any]] = None
class OSSConfigUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=200)
provider: Optional[str] = Field(None, min_length=1, max_length=20)
endpoint: Optional[str] = Field(None, min_length=1, max_length=500)
access_key: Optional[str] = Field(None, min_length=1, max_length=255)
secret_key: Optional[str] = Field(None, min_length=1, max_length=255)
bucket: Optional[str] = Field(None, min_length=1, max_length=200)
region: Optional[str] = None
base_path: Optional[str] = None
is_default: Optional[bool] = None
is_enabled: Optional[bool] = None
config: Optional[Dict[str, Any]] = None
class OSSConnectionTest(BaseModel):
"""测试连接请求 - 可以测试已保存的配置或临时配置"""
config_id: Optional[str] = None # 已保存的配置ID
# 以下为临时测试参数
provider: Optional[str] = None
endpoint: Optional[str] = None
access_key: Optional[str] = None
secret_key: Optional[str] = None
bucket: Optional[str] = None
region: Optional[str] = None
# ============================================================
# Helper Functions
# ============================================================
def config_to_dict(config) -> dict:
"""将OSSStorageConfig模型转换为字典"""
result = {
"id": config.id,
"name": config.name,
"provider": config.provider,
"endpoint": config.endpoint,
"access_key": config.access_key[:8] + "****" if config.access_key and len(config.access_key) > 8 else "****",
"secret_key": "****", # 永远不返回完整密钥
"bucket": config.bucket,
"region": config.region or "",
"base_path": config.base_path or "",
"is_default": getattr(config, "is_default", False),
"is_enabled": config.is_enabled,
"status": config.status or "disconnected",
"created_at": config.created_at.isoformat() if config.created_at else None,
"updated_at": config.updated_at.isoformat() if config.updated_at else None,
}
# 安全处理 JSON 字段
config_raw = getattr(config, "config", None)
if config_raw:
try:
result["config"] = json.loads(config_raw) if isinstance(config_raw, str) else config_raw
except Exception:
result["config"] = {}
else:
result["config"] = {}
return result
# ============================================================
# Storage Quota Management (Phase C5)
# ============================================================
DEFAULT_QUOTAS = {
0: 1073741824, # Free: 1GB
2: 10737418240, # Pro: 10GB
3: 107374182400, # Enterprise: 100GB
}
TIER_LABELS = {0: "\u514d\u8d39\u7248", 2: "\u4e13\u4e1a\u7248", 3: "\u4f01\u4e1a\u7248"}
class QuotaAdjustRequest(BaseModel):
quota_bytes: Optional[int] = Field(None, ge=0)
quota_type: Optional[str] = Field(None, max_length=20)
def ensure_user_quota(user_id: str, membership_level: int, plan_id: str = None, db: Session = None) -> dict:
from ...models import UserStorageQuota
quota = db.query(UserStorageQuota).filter(UserStorageQuota.user_id == user_id).first()
default_bytes = DEFAULT_QUOTAS.get(membership_level, DEFAULT_QUOTAS[0])
if not quota:
quota = UserStorageQuota(
id=gen_uuid(), user_id=user_id, quota_bytes=default_bytes,
used_bytes=0, quota_type="membership",
membership_level=membership_level, plan_id=plan_id,
)
db.add(quota)
db.commit()
db.refresh(quota)
elif quota.quota_type == "membership" and quota.membership_level != membership_level:
quota.quota_bytes = default_bytes
quota.membership_level = membership_level
quota.plan_id = plan_id
db.commit()
db.refresh(quota)
return {
"id": quota.id, "user_id": quota.user_id,
"quota_bytes": quota.quota_bytes, "used_bytes": quota.used_bytes,
"quota_type": quota.quota_type, "membership_level": quota.membership_level,
"plan_id": quota.plan_id,
"created_at": quota.created_at.isoformat() if quota.created_at else None,
"updated_at": quota.updated_at.isoformat() if quota.updated_at else None,
}
def recalculate_storage_usage(user_id: str, db: Session) -> int:
from ...models import KnowledgeDocument, KnowledgeBase
total = db.query(func.coalesce(func.sum(KnowledgeDocument.file_size), 0)).join(
KnowledgeBase, KnowledgeDocument.knowledge_base_id == KnowledgeBase.id
).filter(
KnowledgeBase.user_id == user_id,
).scalar() or 0
return int(total)
@router.get("/quota/overview", summary="Storage Quota Overview")
async def get_quota_overview(
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
from ...models import UserStorageQuota, User
total_quota_users = db.query(func.count(UserStorageQuota.id)).scalar() or 0
total_quota_bytes = db.query(func.coalesce(func.sum(UserStorageQuota.quota_bytes), 0)).scalar() or 0
total_used_bytes = db.query(func.coalesce(func.sum(UserStorageQuota.used_bytes), 0)).scalar() or 0
total_users = db.query(func.count(User.id)).filter(User.status == "active").scalar() or 0
users_without_quota = total_users - total_quota_users
by_level = {}
level_stats = db.query(
UserStorageQuota.membership_level,
func.count(UserStorageQuota.id),
func.coalesce(func.sum(UserStorageQuota.quota_bytes), 0),
func.coalesce(func.sum(UserStorageQuota.used_bytes), 0),
).group_by(UserStorageQuota.membership_level).all()
for level, count, total_q, total_u in level_stats:
by_level[str(level)] = {
"label": TIER_LABELS.get(level, f"Level {level}"),
"user_count": count,
"total_quota_bytes": int(total_q),
"total_used_bytes": int(total_u),
}
over_quota_count = db.query(func.count(UserStorageQuota.id)).filter(
UserStorageQuota.quota_bytes > 0,
UserStorageQuota.used_bytes > UserStorageQuota.quota_bytes
).scalar() or 0
return {
"code": 0,
"data": {
"total_users": total_users,
"quota_users": total_quota_users,
"users_without_quota": users_without_quota,
"total_quota_bytes": int(total_quota_bytes),
"total_used_bytes": int(total_used_bytes),
"over_quota_count": over_quota_count,
"by_level": by_level,
"default_quotas": {str(k): v for k, v in DEFAULT_QUOTAS.items()},
"tier_labels": TIER_LABELS,
},
"message": "ok"
}
@router.get("/quota/users", summary="User Storage Quota List")
async def list_user_quotas(
keyword: str = Query(None),
membership_level: int = Query(None),
over_quota: bool = Query(None),
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),
):
from ...models import UserStorageQuota, User
results = db.query(
User.id, User.username, User.email, User.membership_level, User.real_name,
UserStorageQuota.id.label("quota_id"),
UserStorageQuota.quota_bytes, UserStorageQuota.used_bytes,
UserStorageQuota.quota_type, UserStorageQuota.updated_at,
).join(UserStorageQuota, User.id == UserStorageQuota.user_id, isouter=True
).filter(User.status == "active")
if keyword:
results = results.filter(
or_(User.username.ilike(f"%{keyword}%"), User.email.ilike(f"%{keyword}%"))
)
if membership_level is not None:
results = results.filter(User.membership_level == membership_level)
total = results.count()
items = results.order_by(desc(UserStorageQuota.used_bytes)).offset(
(page - 1) * page_size).limit(page_size).all()
user_list = []
for row in items:
uid, username, email, level, real_name, qid, qbytes, ubytes, qtype, updated = row
quota_b = int(qbytes or DEFAULT_QUOTAS.get(level or 0, DEFAULT_QUOTAS[0]))
used_b = int(ubytes or 0)
usage_pct = round(used_b / quota_b * 100, 1) if quota_b > 0 else 0
user_list.append({
"user_id": uid, "username": username, "email": email or "",
"real_name": real_name or "", "membership_level": level or 0,
"tier_label": TIER_LABELS.get(level or 0, "\u514d\u8d39\u7248"),
"quota_id": qid, "quota_bytes": quota_b, "used_bytes": used_b,
"usage_percent": usage_pct, "is_over_quota": usage_pct >= 100,
"quota_type": qtype or "membership",
"updated_at": updated.isoformat() if updated else None,
})
if over_quota:
user_list = [u for u in user_list if u["is_over_quota"]]
return {
"code": 0,
"data": {
"items": user_list,
"total": len(user_list) if over_quota else total,
"page": page, "page_size": page_size,
},
"message": "ok"
}
@router.get("/quota/users/{user_id}", summary="Get User Quota Detail")
async def get_user_quota(
user_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
from ...models import UserStorageQuota, User, KnowledgeDocument, KnowledgeBase
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
quota = db.query(UserStorageQuota).filter(UserStorageQuota.user_id == user_id).first()
actual_used = recalculate_storage_usage(user_id, db)
file_count = db.query(func.count(KnowledgeDocument.id)).join(
KnowledgeBase, KnowledgeDocument.knowledge_base_id == KnowledgeBase.id
).filter(KnowledgeBase.user_id == user_id).scalar() or 0
file_type_stats = {}
type_counts = db.query(
KnowledgeDocument.file_type, func.count(KnowledgeDocument.id),
func.coalesce(func.sum(KnowledgeDocument.file_size), 0),
).join(
KnowledgeBase, KnowledgeDocument.knowledge_base_id == KnowledgeBase.id
).filter(KnowledgeBase.user_id == user_id
).group_by(KnowledgeDocument.file_type).all()
for ftype, cnt, fsize in type_counts:
file_type_stats[ftype] = {"count": cnt, "total_bytes": int(fsize)}
quota_data = None
if quota:
quota_data = {
"id": quota.id, "quota_bytes": int(quota.quota_bytes),
"used_bytes": actual_used, "original_used": int(quota.used_bytes),
"quota_type": quota.quota_type, "membership_level": quota.membership_level,
"plan_id": quota.plan_id,
"created_at": quota.created_at.isoformat() if quota.created_at else None,
"updated_at": quota.updated_at.isoformat() if quota.updated_at else None,
}
if actual_used != int(quota.used_bytes):
quota.used_bytes = actual_used
db.commit()
return {
"code": 0,
"data": {
"user": {
"id": user.id, "username": user.username,
"email": user.email or "", "real_name": user.real_name or "",
"membership_level": user.membership_level or 0,
"tier_label": TIER_LABELS.get(user.membership_level or 0, "\u514d\u8d39\u7248"),
},
"quota": quota_data, "file_count": file_count,
"file_type_stats": file_type_stats,
},
"message": "ok"
}
@router.put("/quota/users/{user_id}", summary="Adjust User Quota")
async def adjust_user_quota(
user_id: str,
req: QuotaAdjustRequest,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
from ...models import UserStorageQuota, User
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
quota = db.query(UserStorageQuota).filter(UserStorageQuota.user_id == user_id).first()
if not quota:
default_bytes = DEFAULT_QUOTAS.get(user.membership_level or 0, DEFAULT_QUOTAS[0])
quota = UserStorageQuota(
id=gen_uuid(), user_id=user_id, quota_bytes=default_bytes,
used_bytes=0, quota_type="membership",
membership_level=user.membership_level or 0,
)
db.add(quota)
db.flush()
if req.quota_bytes is not None:
quota.quota_bytes = req.quota_bytes
if req.quota_type is not None:
quota.quota_type = req.quota_type
quota.used_bytes = recalculate_storage_usage(user_id, db)
db.commit()
db.refresh(quota)
LogService.log_audit(
admin_id=admin_user.id, admin_name=admin_user.username,
action="adjust_storage_quota", target_type="user_storage_quota",
target_id=quota.id,
detail=f"Adjusted quota for {user.username} to {quota.quota_bytes} bytes"
)
usage_pct = round(quota.used_bytes / quota.quota_bytes * 100, 1) if quota.quota_bytes > 0 else 0
return {
"code": 0,
"data": {
"id": quota.id, "user_id": quota.user_id,
"quota_bytes": int(quota.quota_bytes),
"used_bytes": int(quota.used_bytes),
"quota_type": quota.quota_type, "usage_percent": usage_pct,
},
"message": "Quota adjusted successfully"
}
@router.post("/quota/recalculate/{user_id}", summary="Recalculate User Storage")
async def recalculate_user_quota(
user_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
from ...models import UserStorageQuota
quota = db.query(UserStorageQuota).filter(UserStorageQuota.user_id == user_id).first()
actual_used = recalculate_storage_usage(user_id, db)
if quota:
quota.used_bytes = actual_used
db.commit()
return {
"code": 0,
"data": {
"user_id": user_id, "used_bytes": actual_used,
"quota_bytes": int(quota.quota_bytes),
},
"message": f"Recalculated: {actual_used} bytes"
}
else:
return {
"code": 0,
"data": {"user_id": user_id, "used_bytes": actual_used},
"message": "No quota record for this user"
}
@router.post("/quota/init-all", summary="Init All User Quotas")
async def init_all_user_quotas(
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
from ...models import UserStorageQuota, User
users = db.query(User).filter(User.status == "active").all()
created, skipped = 0, 0
for user in users:
existing = db.query(UserStorageQuota).filter(
UserStorageQuota.user_id == user.id).first()
if existing:
skipped += 1
continue
default_bytes = DEFAULT_QUOTAS.get(user.membership_level or 0, DEFAULT_QUOTAS[0])
used = recalculate_storage_usage(user.id, db)
quota = UserStorageQuota(
id=gen_uuid(), user_id=user.id, quota_bytes=default_bytes,
used_bytes=used, quota_type="membership",
membership_level=user.membership_level or 0,
plan_id=user.membership_plan_id,
)
db.add(quota)
created += 1
db.commit()
return {
"code": 0,
"data": {"created": created, "skipped": skipped, "total": len(users)},
"message": f"Init complete: {created} new, {skipped} existing, {len(users)} total"
}
async def test_oss_connection(provider: str, endpoint: str, access_key: str, secret_key: str,
bucket: str, region: str = "") -> Dict[str, Any]:
"""测试OSS连接 - 使用httpx发送HTTP请求到对象存储端点"""
import time
start_time = time.time()
try:
if provider in ("minio", "aws_s3"):
# 使用 AWS S3 兼容 API 测试
# ListBuckets 请求
import hmac
import hashlib
from urllib.parse import urlparse
parsed = urlparse(endpoint)
host = parsed.netloc or parsed.path
if not host:
return {"success": False, "error": "无法解析端点地址", "latency_ms": 0}
# 先尝试简单的 bucket HEAD 请求
bucket_url = f"{endpoint.rstrip('/')}/{bucket}"
async with httpx.AsyncClient(timeout=10.0, verify=False) as client:
resp = await client.head(bucket_url)
latency = int((time.time() - start_time) * 1000)
if resp.status_code in (200, 403):
# 200: bucket存在且有权限; 403: bucket存在但无权限(也是连通的表现)
return {
"success": True,
"message": f"连接成功!Bucket '{bucket}' 可访问",
"latency_ms": latency,
"status_code": resp.status_code
}
elif resp.status_code == 404:
return {
"success": False,
"error": f"Bucket '{bucket}' 不存在",
"latency_ms": latency
}
else:
return {
"success": False,
"error": f"连接失败: HTTP {resp.status_code}",
"latency_ms": latency
}
elif provider == "aliyun_oss":
# 阿里云OSS: 简单HEAD请求
bucket_url = f"{endpoint.rstrip('/')}/{bucket}" if not endpoint.endswith(bucket) else endpoint
async with httpx.AsyncClient(timeout=10.0, verify=False) as client:
resp = await client.head(bucket_url)
latency = int((time.time() - start_time) * 1000)
if resp.status_code in (200, 403):
return {
"success": True,
"message": f"连接成功!Bucket '{bucket}' 可访问",
"latency_ms": latency,
"status_code": resp.status_code
}
elif resp.status_code == 404:
return {
"success": False,
"error": f"Bucket '{bucket}' 不存在",
"latency_ms": latency
}
else:
return {
"success": False,
"error": f"连接失败: HTTP {resp.status_code}",
"latency_ms": latency
}
else:
return {"success": False, "error": f"不支持的存储类型: {provider}", "latency_ms": 0}
except httpx.ConnectError:
latency = int((time.time() - start_time) * 1000)
return {"success": False, "error": f"无法连接到 {endpoint}", "latency_ms": latency}
except httpx.TimeoutException:
latency = int((time.time() - start_time) * 1000)
return {"success": False, "error": "连接超时(10秒)", "latency_ms": latency}
except Exception as e:
latency = int((time.time() - start_time) * 1000)
return {"success": False, "error": f"测试失败: {str(e)}", "latency_ms": latency}
# ============================================================
# API Endpoints
# ============================================================
@router.get("/stats/overview", summary="存储配置统计概览")
async def get_storage_stats(
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取存储配置的统计信息"""
from ...models import OSSStorageConfig
total = db.query(func.count(OSSStorageConfig.id)).scalar() or 0
enabled_count = db.query(func.count(OSSStorageConfig.id)).filter(
OSSStorageConfig.is_enabled == True
).scalar() or 0
default_count = db.query(func.count(OSSStorageConfig.id)).filter(
OSSStorageConfig.is_default == True
).scalar() or 0
# 按类型统计
by_provider = {}
providers = db.query(OSSStorageConfig.provider, func.count(OSSStorageConfig.id)).group_by(
OSSStorageConfig.provider
).all()
for provider, count in providers:
by_provider[provider] = count
# 按状态统计
by_status = {}
statuses = db.query(OSSStorageConfig.status, func.count(OSSStorageConfig.id)).group_by(
OSSStorageConfig.status
).all()
for status, count in statuses:
by_status[status] = count
return {
"code": 0,
"data": {
"total": total,
"enabled_count": enabled_count,
"default_count": default_count,
"by_provider": by_provider,
"by_status": by_status,
},
"message": "ok"
}
@router.get("/", summary="获取OSS存储配置列表")
async def list_storage_configs(
provider: str = Query(None, description="按提供商筛选"),
is_enabled: bool = Query(None, description="按启用状态筛选"),
keyword: 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),
):
"""获取OSS存储配置列表,支持分页和筛选"""
from ...models import OSSStorageConfig
query = db.query(OSSStorageConfig)
if provider:
query = query.filter(OSSStorageConfig.provider == provider)
if is_enabled is not None:
query = query.filter(OSSStorageConfig.is_enabled == is_enabled)
if keyword:
keyword_filter = or_(
OSSStorageConfig.name.ilike(f"%{keyword}%"),
OSSStorageConfig.endpoint.ilike(f"%{keyword}%"),
OSSStorageConfig.bucket.ilike(f"%{keyword}%"),
)
query = query.filter(keyword_filter)
total = query.count()
items = query.order_by(desc(OSSStorageConfig.is_default), desc(OSSStorageConfig.updated_at)) \
.offset((page - 1) * page_size).limit(page_size).all()
return {
"code": 0,
"data": {
"items": [config_to_dict(item) for item in items],
"total": total,
"page": page,
"page_size": page_size,
},
"message": "ok"
}
@router.get("/{config_id}", summary="获取OSS存储配置详情")
async def get_storage_config(
config_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取单个OSS存储配置的详细信息"""
from ...models import OSSStorageConfig
config = db.query(OSSStorageConfig).filter(OSSStorageConfig.id == config_id).first()
if not config:
raise HTTPException(status_code=404, detail="存储配置不存在")
return {
"code": 0,
"data": config_to_dict(config),
"message": "ok"
}
@router.post("/", summary="创建OSS存储配置")
async def create_storage_config(
req: OSSConfigCreate,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""创建新的OSS存储配置"""
from ...models import OSSStorageConfig
# 检查名称重复
existing = db.query(OSSStorageConfig).filter(OSSStorageConfig.name == req.name).first()
if existing:
raise HTTPException(status_code=400, detail="配置名称已存在")
# 如果设为默认,先取消其他默认
if req.is_default:
db.query(OSSStorageConfig).filter(OSSStorageConfig.is_default == True).update(
{"is_default": False}
)
config = OSSStorageConfig(
id=gen_uuid(),
name=req.name,
provider=req.provider,
endpoint=req.endpoint,
access_key=req.access_key,
secret_key=req.secret_key,
bucket=req.bucket,
region=req.region or "",
base_path=req.base_path or "",
is_default=req.is_default,
is_enabled=req.is_enabled,
status="disconnected",
config=req.config,
)
db.add(config)
db.commit()
db.refresh(config)
# 审计日志
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="create_oss_config",
target_type="oss_storage_config",
target_id=config.id,
detail=f"创建了OSS存储配置: {config.name} ({config.provider})"
)
return {
"code": 0,
"data": config_to_dict(config),
"message": "创建成功"
}
@router.put("/{config_id}", summary="更新OSS存储配置")
async def update_storage_config(
config_id: str,
req: OSSConfigUpdate,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""更新OSS存储配置"""
from ...models import OSSStorageConfig
config = db.query(OSSStorageConfig).filter(OSSStorageConfig.id == config_id).first()
if not config:
raise HTTPException(status_code=404, detail="存储配置不存在")
# 检查名称重复
if req.name is not None and req.name != config.name:
existing = db.query(OSSStorageConfig).filter(
OSSStorageConfig.name == req.name,
OSSStorageConfig.id != config_id
).first()
if existing:
raise HTTPException(status_code=400, detail="配置名称已存在")
# 如果设为默认,先取消其他默认
if req.is_default and not config.is_default:
db.query(OSSStorageConfig).filter(OSSStorageConfig.is_default == True).update(
{"is_default": False}
)
# 更新字段
update_fields = ["name", "provider", "endpoint", "access_key", "secret_key",
"bucket", "region", "base_path", "is_default", "is_enabled"]
for field in update_fields:
value = getattr(req, field, None)
if value is not None:
setattr(config, field, value)
# 如果修改了连接参数,重置状态
if field in ("endpoint", "access_key", "secret_key", "bucket", "provider"):
config.status = "disconnected"
if req.config is not None:
config.config = req.config
config.updated_at = datetime.now()
db.commit()
db.refresh(config)
# 审计日志
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="update_oss_config",
target_type="oss_storage_config",
target_id=config.id,
detail=f"更新了OSS存储配置: {config.name}"
)
return {
"code": 0,
"data": config_to_dict(config),
"message": "更新成功"
}
@router.delete("/{config_id}", summary="删除OSS存储配置")
async def delete_storage_config(
config_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""删除OSS存储配置"""
from ...models import OSSStorageConfig
config = db.query(OSSStorageConfig).filter(OSSStorageConfig.id == config_id).first()
if not config:
raise HTTPException(status_code=404, detail="存储配置不存在")
name = config.name
db.delete(config)
db.commit()
# 审计日志
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="delete_oss_config",
target_type="oss_storage_config",
target_id=config_id,
detail=f"删除了OSS存储配置: {name}"
)
return {
"code": 0,
"data": None,
"message": f"已删除配置: {name}"
}
@router.put("/{config_id}/toggle", summary="启用/禁用存储配置")
async def toggle_storage_config(
config_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""切换存储配置的启用/禁用状态"""
from ...models import OSSStorageConfig
config = db.query(OSSStorageConfig).filter(OSSStorageConfig.id == config_id).first()
if not config:
raise HTTPException(status_code=404, detail="存储配置不存在")
config.is_enabled = not config.is_enabled
config.updated_at = datetime.now()
db.commit()
db.refresh(config)
action = "启用" if config.is_enabled else "禁用"
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="toggle_oss_config",
target_type="oss_storage_config",
target_id=config.id,
detail=f"{action}了OSS存储配置: {config.name}"
)
return {
"code": 0,
"data": config_to_dict(config),
"message": f"已{action}"
}
@router.put("/{config_id}/set-default", summary="设为默认存储配置")
async def set_default_storage_config(
config_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""将指定配置设为默认存储"""
from ...models import OSSStorageConfig
config = db.query(OSSStorageConfig).filter(OSSStorageConfig.id == config_id).first()
if not config:
raise HTTPException(status_code=404, detail="存储配置不存在")
# 取消所有默认
db.query(OSSStorageConfig).filter(OSSStorageConfig.is_default == True).update(
{"is_default": False}
)
# 设置新默认
config.is_default = True
config.updated_at = datetime.now()
db.commit()
db.refresh(config)
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="set_default_oss_config",
target_type="oss_storage_config",
target_id=config.id,
detail=f"将 {config.name} 设为默认存储配置"
)
return {
"code": 0,
"data": config_to_dict(config),
"message": f"已将 {config.name} 设为默认存储"
}
@router.post("/test", summary="测试OSS连接")
async def test_storage_connection(
req: OSSConnectionTest,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""测试OSS存储连接"""
from ...models import OSSStorageConfig
provider = req.provider
endpoint = req.endpoint
access_key = req.access_key
secret_key = req.secret_key
bucket = req.bucket
region = req.region or ""
# 如果提供了config_id,从数据库获取配置
if req.config_id:
config = db.query(OSSStorageConfig).filter(OSSStorageConfig.id == req.config_id).first()
if not config:
raise HTTPException(status_code=404, detail="存储配置不存在")
provider = config.provider
endpoint = config.endpoint
access_key = config.access_key
secret_key = config.secret_key
bucket = config.bucket
region = config.region or ""
if not all([provider, endpoint, access_key, secret_key, bucket]):
raise HTTPException(status_code=400, detail="缺少必要的连接参数")
result = await test_oss_connection(provider, endpoint, access_key, secret_key, bucket, region)
# 如果是已保存的配置,更新连接状态
if req.config_id:
config = db.query(OSSStorageConfig).filter(OSSStorageConfig.id == req.config_id).first()
if config:
config.status = "connected" if result["success"] else "disconnected"
config.updated_at = datetime.now()
db.commit()
return {
"code": 0,
"data": result,
"message": "连接测试完成"
}