""" 资产中心管理 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, } }