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

-357
View File
@@ -1,357 +0,0 @@
"""
资产中心管理 API(管理员)
Phase 3.8: Asset Center Enhancement - Admin asset management, analytics, activity log
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_, func, desc, text
from typing import Optional
import json
from datetime import datetime, timedelta
from ...database import get_db, SessionLocal
from ...models import Asset, AssetCategory, UserStorageQuota, User
from ...auth import get_current_admin
from ...middleware.log_service import LogService
router = APIRouter()
CATEGORY_LABELS = {
"file": "文件", "novel": "小说", "image": "图片",
"video": "视频", "audio": "音频", "custom": "自定义", "doc": "文档",
}
def _execute_raw(db, sql, params=None):
result = db.execute(text(sql), params or {})
try:
rows = result.fetchall()
return [dict(row._mapping) if hasattr(row, '_mapping') else dict(row) for row in rows]
except:
return []
def _asset_to_dict(a):
return {
"id": a.id,
"user_id": a.user_id,
"name": a.name,
"category": a.category,
"sub_category": a.sub_category,
"file_type": a.file_type,
"size": a.size or 0,
"description": a.description,
"cover_url": a.cover_url,
"url": a.url,
"status": a.status,
"created_at": a.created_at.strftime('%Y-%m-%d %H:%M:%S') if a.created_at else None,
"updated_at": a.updated_at.strftime('%Y-%m-%d %H:%M:%S') if a.updated_at else None,
}
# ============================================================
# Stats / Overview
# ============================================================
@router.get("/stats", summary="资产中心总览")
async def get_asset_stats(
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取资产中心总览数据"""
# Total assets
total_assets = _execute_raw(db, "SELECT COUNT(*) as cnt FROM assets WHERE status = 'active'")
total = total_assets[0]["cnt"] if total_assets else 0
# Total size
total_size = _execute_raw(db, "SELECT COALESCE(SUM(size), 0) as sz FROM assets WHERE status = 'active'")
size = total_size[0]["sz"] if total_size else 0
# By category
cat_stats = _execute_raw(db, """
SELECT category, COUNT(*) as cnt, COALESCE(SUM(size), 0) as sz
FROM assets WHERE status = 'active'
GROUP BY category
""")
# By file type
type_stats = _execute_raw(db, """
SELECT COALESCE(file_type, 'unknown') as ft, COUNT(*) as cnt, COALESCE(SUM(size), 0) as sz
FROM assets WHERE status = 'active'
GROUP BY file_type
ORDER BY cnt DESC
LIMIT 10
""")
# Top users by storage
top_users = _execute_raw(db, """
SELECT a.user_id, u.username, u.avatar,
COUNT(*) as asset_count, COALESCE(SUM(a.size), 0) as total_size
FROM assets a
LEFT JOIN users u ON a.user_id = u.id
WHERE a.status = 'active'
GROUP BY a.user_id, u.username, u.avatar
ORDER BY total_size DESC
LIMIT 10
""")
# Storage quota summary
quota_stats = _execute_raw(db, """
SELECT
COUNT(*) as total_users,
COALESCE(SUM(quota_bytes), 0) as total_quota,
COALESCE(SUM(used_bytes), 0) as total_used
FROM user_storage_quotas
""")
# Recent 7 days trend
trend = []
for i in range(6, -1, -1):
date = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d')
new_uploads = _execute_raw(db, """
SELECT COUNT(*) as cnt FROM assets
WHERE DATE(created_at) = :d AND status = 'active'
""", {"d": date})
trend.append({
"date": date,
"new_uploads": new_uploads[0]["cnt"] if new_uploads else 0,
})
# Categories list
categories = _execute_raw(db, """
SELECT type, COUNT(*) as cnt FROM asset_categories GROUP BY type
""")
return {
"code": 0,
"data": {
"total_assets": total,
"total_size": size,
"total_size_formatted": _format_size(size),
"by_category": [{
"category": r["category"],
"label": CATEGORY_LABELS.get(r["category"], r["category"]),
"count": r["cnt"],
"size": r["sz"],
"size_formatted": _format_size(r["sz"]),
} for r in cat_stats],
"by_file_type": [{
"file_type": r["ft"],
"count": r["cnt"],
"size": r["sz"],
"size_formatted": _format_size(r["sz"]),
} for r in type_stats],
"top_users": [{
"user_id": r["user_id"],
"username": r["username"] or "Unknown",
"avatar": r["avatar"],
"asset_count": r["asset_count"],
"total_size": r["total_size"],
"total_size_formatted": _format_size(r["total_size"]),
} for r in top_users],
"quota_summary": {
"total_users": quota_stats[0]["total_users"] if quota_stats else 0,
"total_quota": quota_stats[0]["total_quota"] if quota_stats else 0,
"total_used": quota_stats[0]["total_used"] if quota_stats else 0,
"total_quota_formatted": _format_size(quota_stats[0]["total_quota"]) if quota_stats else "0",
"total_used_formatted": _format_size(quota_stats[0]["total_used"]) if quota_stats else "0",
"usage_percent": round((quota_stats[0]["total_used"] / quota_stats[0]["total_quota"] * 100), 2) if quota_stats and quota_stats[0]["total_quota"] > 0 else 0,
},
"trend_7d": trend,
"categories": [{"type": r["type"], "count": r["cnt"]} for r in categories],
}
}
def _format_size(bytes_val):
if not bytes_val or bytes_val == 0:
return "0 B"
if bytes_val < 1024:
return f"{bytes_val} B"
if bytes_val < 1024 * 1024:
return f"{bytes_val / 1024:.1f} KB"
if bytes_val < 1024 * 1024 * 1024:
return f"{bytes_val / (1024 * 1024):.1f} MB"
return f"{bytes_val / (1024 * 1024 * 1024):.2f} GB"
# ============================================================
# Asset list & management
# ============================================================
@router.get("/", summary="List all assets")
async def list_all_assets(
user_id: str = Query(None),
category: str = Query(None),
file_type: str = Query(None),
keyword: str = Query(None),
status: str = Query("active"),
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),
):
"""List all user assets"""
where_clauses = []
params = {}
if status:
where_clauses.append("a.status = :status")
params["status"] = status
if user_id:
where_clauses.append("a.user_id = :uid")
params["uid"] = user_id
if category:
where_clauses.append("a.category = :cat")
params["cat"] = category
if file_type:
where_clauses.append("a.file_type = :ft")
params["ft"] = file_type
if keyword:
where_clauses.append("(a.name LIKE :kw OR a.description LIKE :kw)")
params["kw"] = "%" + keyword + "%"
where_sql = " AND ".join(where_clauses)
where_clause = "WHERE " + where_sql if where_sql else ""
count_sql = "SELECT COUNT(*) as cnt FROM assets a " + where_clause
count_rows = _execute_raw(db, count_sql, params)
total = count_rows[0]["cnt"] if count_rows else 0
offset = (page - 1) * page_size
list_sql = "SELECT a.id, a.user_id, a.name, a.category, a.sub_category, a.file_type, a.size, a.description, a.cover_url, a.url, a.status, a.created_at, a.updated_at, u.username, u.avatar FROM assets a LEFT JOIN users u ON a.user_id = u.id " + where_clause + " ORDER BY a.created_at DESC LIMIT :limit OFFSET :offset"
list_params = dict(params)
list_params["limit"] = page_size
list_params["offset"] = offset
rows = _execute_raw(db, list_sql, list_params)
items = []
for r in rows:
items.append({
"id": r["id"], "user_id": r["user_id"], "name": r["name"],
"category": r["category"], "sub_category": r["sub_category"],
"file_type": r["file_type"], "size": r["size"] or 0,
"size_formatted": _format_size(r["size"] or 0),
"description": r["description"], "cover_url": r["cover_url"],
"url": r["url"], "status": r["status"],
"username": r["username"] or "Unknown", "avatar": r["avatar"],
"created_at": r["created_at"].strftime("%Y-%m-%d %H:%M:%S") if r["created_at"] else None,
"updated_at": r["updated_at"].strftime("%Y-%m-%d %H:%M:%S") if r["updated_at"] else None,
})
return {"code": 0, "data": {"total": total, "page": page, "page_size": page_size, "items": items}}
@router.delete("/{asset_id}", summary="管理员删除资产")
async def admin_delete_asset(
asset_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""管理员删除用户资产"""
asset = db.query(Asset).filter(Asset.id == asset_id).first()
if not asset:
raise HTTPException(status_code=404, detail="资产不存在")
asset_name = asset.name
asset.status = "deleted"
asset.updated_at = datetime.now()
db.commit()
LogService.log_action(db, "admin", admin_user["id"], "delete_asset", f"Deleted asset: {asset_name}")
return {"code": 0, "message": "资产已删除"}
@router.get("/activity", summary="最近资产活动")
async def get_asset_activity(
limit: int = Query(30, le=100),
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取最近的资产活动"""
activities = _execute_raw(db, """
SELECT a.id, a.name, a.category, a.file_type, a.size, a.status,
a.user_id, u.username, u.avatar,
a.created_at, a.updated_at
FROM assets a
LEFT JOIN users u ON a.user_id = u.id
ORDER BY a.updated_at DESC
LIMIT :lim
""", {"lim": limit})
return {
"code": 0,
"data": [{
"id": r["id"],
"name": r["name"],
"category": r["category"],
"category_label": CATEGORY_LABELS.get(r["category"], r["category"]),
"file_type": r["file_type"],
"size": r["size"] or 0,
"size_formatted": _format_size(r["size"] or 0),
"status": r["status"],
"user_id": r["user_id"],
"username": r["username"] or "Unknown",
"avatar": r["avatar"],
"created_at": r["created_at"].strftime('%Y-%m-%d %H:%M:%S') if r["created_at"] else None,
"updated_at": r["updated_at"].strftime('%Y-%m-%d %H:%M:%S') if r["updated_at"] else None,
} for r in activities]
}
@router.get("/users/{user_id}/detail", summary="用户资产详情")
async def get_user_asset_detail(
user_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取指定用户的资产详情"""
# User info
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
# Asset stats
stats = _execute_raw(db, """
SELECT category, COUNT(*) as cnt, COALESCE(SUM(size), 0) as sz
FROM assets WHERE user_id = :uid AND status = 'active'
GROUP BY category
""", {"uid": user_id})
total = _execute_raw(db, """
SELECT COUNT(*) as cnt, COALESCE(SUM(size), 0) as sz
FROM assets WHERE user_id = :uid AND status = 'active'
""", {"uid": user_id})
# Quota
quota = db.query(UserStorageQuota).filter(UserStorageQuota.user_id == user_id).first()
return {
"code": 0,
"data": {
"user": {
"id": user.id,
"username": user.username,
"avatar": user.avatar,
"membership_level": user.membership_level,
},
"total_assets": total[0]["cnt"] if total else 0,
"total_size": total[0]["sz"] if total else 0,
"total_size_formatted": _format_size(total[0]["sz"]) if total else "0 B",
"by_category": [{
"category": r["category"],
"label": CATEGORY_LABELS.get(r["category"], r["category"]),
"count": r["cnt"],
"size": r["sz"],
"size_formatted": _format_size(r["sz"]),
} for r in stats],
"quota": {
"quota_bytes": quota.quota_bytes if quota else 0,
"used_bytes": quota.used_bytes if quota else 0,
"quota_formatted": _format_size(quota.quota_bytes) if quota else "0 B",
"used_formatted": _format_size(quota.used_bytes) if quota else "0 B",
"usage_percent": round((quota.used_bytes / quota.quota_bytes * 100), 2) if quota and quota.quota_bytes > 0 else 0,
} if quota else None,
}
}