初始提交:极码 GeekCode 全栈项目(FastAPI + Vue3)
This commit is contained in:
458
backend/routers/admin.py
Normal file
458
backend/routers/admin.py
Normal file
@@ -0,0 +1,458 @@
|
||||
"""后台管理路由"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func as sa_func, distinct
|
||||
from datetime import datetime, timedelta
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
from database import get_db
|
||||
from models.user import User
|
||||
from models.post import Post
|
||||
from models.comment import Comment
|
||||
from models.like import Like, Collect
|
||||
from models.system_config import SystemConfig
|
||||
from models.category import Category
|
||||
from routers.auth import get_admin_user, get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ---------- 对象存储配置管理(腾讯云COS) ----------
|
||||
|
||||
COS_CONFIG_KEYS = [
|
||||
{"key": "cos_secret_id", "description": "SecretId"},
|
||||
{"key": "cos_secret_key", "description": "SecretKey"},
|
||||
{"key": "cos_bucket", "description": "Bucket(如 bianchengshequ-1250000000)"},
|
||||
{"key": "cos_region", "description": "Region(如 ap-beijing)"},
|
||||
{"key": "cos_custom_domain", "description": "自定义域名(可选,CDN加速域名)"},
|
||||
]
|
||||
|
||||
|
||||
def get_cos_config_from_db(db: Session) -> dict:
|
||||
"""从数据库读取COS配置"""
|
||||
config = {}
|
||||
for item in COS_CONFIG_KEYS:
|
||||
row = db.query(SystemConfig).filter(SystemConfig.key == item["key"]).first()
|
||||
config[item["key"]] = row.value if row else ""
|
||||
return config
|
||||
|
||||
|
||||
class CosConfigUpdate(BaseModel):
|
||||
cos_secret_id: str = ""
|
||||
cos_secret_key: Optional[str] = None
|
||||
cos_bucket: str = ""
|
||||
cos_region: str = ""
|
||||
cos_custom_domain: str = ""
|
||||
|
||||
|
||||
@router.get("/storage/config")
|
||||
async def get_storage_config(
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""获取对象存储配置"""
|
||||
config = get_cos_config_from_db(db)
|
||||
# 脱敏 SecretKey
|
||||
secret = config.get("cos_secret_key", "")
|
||||
if secret and len(secret) > 6:
|
||||
config["cos_secret_key_masked"] = secret[:3] + "*" * (len(secret) - 6) + secret[-3:]
|
||||
else:
|
||||
config["cos_secret_key_masked"] = "*" * len(secret) if secret else ""
|
||||
config.pop("cos_secret_key", None)
|
||||
return {"config": config, "fields": COS_CONFIG_KEYS}
|
||||
|
||||
|
||||
@router.put("/storage/config")
|
||||
async def update_storage_config(
|
||||
data: CosConfigUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""更新对象存储配置"""
|
||||
updates = data.dict(exclude_none=True)
|
||||
for key, value in updates.items():
|
||||
row = db.query(SystemConfig).filter(SystemConfig.key == key).first()
|
||||
if row:
|
||||
row.value = value
|
||||
else:
|
||||
desc = next((i["description"] for i in COS_CONFIG_KEYS if i["key"] == key), "")
|
||||
db.add(SystemConfig(key=key, value=value, description=desc))
|
||||
db.commit()
|
||||
return {"message": "配置已保存"}
|
||||
|
||||
|
||||
@router.post("/storage/test")
|
||||
async def test_storage_connection(
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""测试COS连接"""
|
||||
config = get_cos_config_from_db(db)
|
||||
secret_id = config.get("cos_secret_id", "")
|
||||
secret_key = config.get("cos_secret_key", "")
|
||||
bucket = config.get("cos_bucket", "")
|
||||
region = config.get("cos_region", "")
|
||||
|
||||
if not all([secret_id, secret_key, bucket, region]):
|
||||
raise HTTPException(status_code=400, detail="COS配置不完整,请先填写所有必填项")
|
||||
|
||||
try:
|
||||
from qcloud_cos import CosConfig, CosS3Client
|
||||
cos_config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)
|
||||
client = CosS3Client(cos_config)
|
||||
# 尝试获取bucket信息来验证连接
|
||||
client.head_bucket(Bucket=bucket)
|
||||
return {"success": True, "message": "连接成功"}
|
||||
except ImportError:
|
||||
raise HTTPException(status_code=500, detail="服务器未安装 cos-python-sdk-v5 库,请执行 pip install cos-python-sdk-v5")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"连接失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_stats(
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""获取管理后台统计数据"""
|
||||
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
# 基础统计
|
||||
total_users = db.query(sa_func.count(User.id)).scalar() or 0
|
||||
total_posts = db.query(sa_func.count(Post.id)).scalar() or 0
|
||||
total_comments = db.query(sa_func.count(Comment.id)).scalar() or 0
|
||||
total_likes = db.query(sa_func.count(Like.id)).scalar() or 0
|
||||
|
||||
# 今日新增
|
||||
today_users = db.query(sa_func.count(User.id)).filter(User.created_at >= today).scalar() or 0
|
||||
today_posts = db.query(sa_func.count(Post.id)).filter(Post.created_at >= today).scalar() or 0
|
||||
|
||||
# 今日活跃(今日有发帖/评论/点赞行为的用户)
|
||||
active_post = db.query(distinct(Post.user_id)).filter(Post.created_at >= today)
|
||||
active_comment = db.query(distinct(Comment.user_id)).filter(Comment.created_at >= today)
|
||||
active_like = db.query(distinct(Like.user_id)).filter(Like.created_at >= today)
|
||||
active_ids = set()
|
||||
for row in active_post.all():
|
||||
active_ids.add(row[0])
|
||||
for row in active_comment.all():
|
||||
active_ids.add(row[0])
|
||||
for row in active_like.all():
|
||||
active_ids.add(row[0])
|
||||
today_active = len(active_ids)
|
||||
|
||||
# 7日趋势
|
||||
user_trend = []
|
||||
post_trend = []
|
||||
for i in range(6, -1, -1):
|
||||
day_start = today - timedelta(days=i)
|
||||
day_end = day_start + timedelta(days=1)
|
||||
date_str = day_start.strftime("%m-%d")
|
||||
|
||||
u_count = db.query(sa_func.count(User.id)).filter(
|
||||
User.created_at >= day_start, User.created_at < day_end
|
||||
).scalar() or 0
|
||||
p_count = db.query(sa_func.count(Post.id)).filter(
|
||||
Post.created_at >= day_start, Post.created_at < day_end
|
||||
).scalar() or 0
|
||||
|
||||
user_trend.append({"date": date_str, "count": u_count})
|
||||
post_trend.append({"date": date_str, "count": p_count})
|
||||
|
||||
# 最近注册用户
|
||||
recent_users = db.query(User).order_by(User.created_at.desc()).limit(5).all()
|
||||
recent_users_data = [
|
||||
{"id": u.id, "username": u.username, "email": u.email, "created_at": str(u.created_at)}
|
||||
for u in recent_users
|
||||
]
|
||||
|
||||
# 最近发布帖子
|
||||
recent_posts = db.query(Post).order_by(Post.created_at.desc()).limit(5).all()
|
||||
recent_posts_data = []
|
||||
for p in recent_posts:
|
||||
author = db.query(User).filter(User.id == p.user_id).first()
|
||||
recent_posts_data.append({
|
||||
"id": p.id, "title": p.title,
|
||||
"author": author.username if author else "未知",
|
||||
"created_at": str(p.created_at),
|
||||
})
|
||||
|
||||
return {
|
||||
"total_users": total_users,
|
||||
"total_posts": total_posts,
|
||||
"total_comments": total_comments,
|
||||
"total_likes": total_likes,
|
||||
"today_users": today_users,
|
||||
"today_posts": today_posts,
|
||||
"today_active": today_active,
|
||||
"user_trend": user_trend,
|
||||
"post_trend": post_trend,
|
||||
"recent_users": recent_users_data,
|
||||
"recent_posts": recent_posts_data,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
async def list_users(
|
||||
search: str = "",
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""用户管理列表"""
|
||||
query = db.query(User)
|
||||
if search:
|
||||
query = query.filter(User.username.contains(search))
|
||||
|
||||
total = query.count()
|
||||
users = query.order_by(User.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
items = []
|
||||
for u in users:
|
||||
post_count = db.query(sa_func.count(Post.id)).filter(Post.user_id == u.id).scalar() or 0
|
||||
comment_count = db.query(sa_func.count(Comment.id)).filter(Comment.user_id == u.id).scalar() or 0
|
||||
items.append({
|
||||
"id": u.id,
|
||||
"username": u.username,
|
||||
"email": u.email,
|
||||
"avatar": u.avatar or "",
|
||||
"is_admin": u.is_admin,
|
||||
"is_banned": getattr(u, 'is_banned', False),
|
||||
"is_approved": getattr(u, 'is_approved', True),
|
||||
"post_count": post_count,
|
||||
"comment_count": comment_count,
|
||||
"created_at": str(u.created_at),
|
||||
})
|
||||
|
||||
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/toggle-admin")
|
||||
async def toggle_admin(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""切换用户管理员身份"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if user.id == admin.id:
|
||||
raise HTTPException(status_code=400, detail="不能修改自己的管理员状态")
|
||||
user.is_admin = not user.is_admin
|
||||
db.commit()
|
||||
return {"message": f"已{'设为' if user.is_admin else '取消'}管理员", "is_admin": user.is_admin}
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/toggle-ban")
|
||||
async def toggle_ban(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""封禁/解封用户"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if user.id == admin.id:
|
||||
raise HTTPException(status_code=400, detail="不能封禁自己")
|
||||
if user.is_admin:
|
||||
raise HTTPException(status_code=400, detail="不能封禁管理员")
|
||||
user.is_banned = not user.is_banned
|
||||
db.commit()
|
||||
return {"message": f"已{'封禁' if user.is_banned else '解封'}该用户", "is_banned": user.is_banned}
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/approve")
|
||||
async def approve_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""审核通过用户"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if getattr(user, 'is_approved', False):
|
||||
raise HTTPException(status_code=400, detail="该用户已通过审核")
|
||||
user.is_approved = True
|
||||
db.commit()
|
||||
return {"message": f"已审核通过用户:{user.username}", "is_approved": True}
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/reject")
|
||||
async def reject_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""拒绝/撤回用户审核"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if user.is_admin:
|
||||
raise HTTPException(status_code=400, detail="不能拒绝管理员")
|
||||
user.is_approved = False
|
||||
db.commit()
|
||||
return {"message": f"已拒绝用户:{user.username}", "is_approved": False}
|
||||
|
||||
|
||||
@router.get("/posts")
|
||||
async def list_posts(
|
||||
search: str = "",
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""帖子管理列表"""
|
||||
query = db.query(Post)
|
||||
if search:
|
||||
query = query.filter(Post.title.contains(search))
|
||||
|
||||
total = query.count()
|
||||
posts = query.order_by(Post.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
items = []
|
||||
for p in posts:
|
||||
author = db.query(User).filter(User.id == p.user_id).first()
|
||||
like_count = db.query(sa_func.count(Like.id)).filter(Like.post_id == p.id).scalar() or 0
|
||||
comment_count = db.query(sa_func.count(Comment.id)).filter(Comment.post_id == p.id).scalar() or 0
|
||||
items.append({
|
||||
"id": p.id,
|
||||
"title": p.title,
|
||||
"author": author.username if author else "未知",
|
||||
"author_id": p.user_id,
|
||||
"category": p.category or "",
|
||||
"is_public": p.is_public,
|
||||
"like_count": like_count,
|
||||
"comment_count": comment_count,
|
||||
"view_count": p.view_count,
|
||||
"created_at": str(p.created_at),
|
||||
})
|
||||
|
||||
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||||
|
||||
|
||||
@router.delete("/posts/{post_id}")
|
||||
async def delete_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""管理员删除帖子"""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="帖子不存在")
|
||||
# 删除关联数据
|
||||
db.query(Comment).filter(Comment.post_id == post_id).delete()
|
||||
db.query(Like).filter(Like.post_id == post_id).delete()
|
||||
db.query(Collect).filter(Collect.post_id == post_id).delete()
|
||||
db.delete(post)
|
||||
db.commit()
|
||||
return {"message": "删除成功"}
|
||||
|
||||
|
||||
# ---------- 分类管理 ----------
|
||||
|
||||
@router.get("/categories")
|
||||
async def list_categories(
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""获取所有分类(含禁用的)"""
|
||||
cats = db.query(Category).order_by(Category.sort_order, Category.id).all()
|
||||
return [{"id": c.id, "name": c.name, "sort_order": c.sort_order, "is_active": c.is_active} for c in cats]
|
||||
|
||||
|
||||
class CategoryCreate(BaseModel):
|
||||
name: str
|
||||
|
||||
class CategoryUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
sort_order: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
@router.post("/categories")
|
||||
async def create_category(
|
||||
data: CategoryCreate,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""新增分类"""
|
||||
existing = db.query(Category).filter(Category.name == data.name).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="分类名称已存在")
|
||||
max_order = db.query(sa_func.max(Category.sort_order)).scalar() or 0
|
||||
cat = Category(name=data.name, sort_order=max_order + 1)
|
||||
db.add(cat)
|
||||
db.commit()
|
||||
db.refresh(cat)
|
||||
return {"id": cat.id, "name": cat.name, "sort_order": cat.sort_order, "is_active": cat.is_active}
|
||||
|
||||
|
||||
@router.put("/categories/{cat_id}")
|
||||
async def update_category(
|
||||
cat_id: int,
|
||||
data: CategoryUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""修改分类"""
|
||||
cat = db.query(Category).filter(Category.id == cat_id).first()
|
||||
if not cat:
|
||||
raise HTTPException(status_code=404, detail="分类不存在")
|
||||
if data.name is not None:
|
||||
dup = db.query(Category).filter(Category.name == data.name, Category.id != cat_id).first()
|
||||
if dup:
|
||||
raise HTTPException(status_code=400, detail="分类名称已存在")
|
||||
cat.name = data.name
|
||||
if data.sort_order is not None:
|
||||
cat.sort_order = data.sort_order
|
||||
if data.is_active is not None:
|
||||
cat.is_active = data.is_active
|
||||
db.commit()
|
||||
return {"id": cat.id, "name": cat.name, "sort_order": cat.sort_order, "is_active": cat.is_active}
|
||||
|
||||
|
||||
@router.delete("/categories/{cat_id}")
|
||||
async def delete_category(
|
||||
cat_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""删除分类"""
|
||||
cat = db.query(Category).filter(Category.id == cat_id).first()
|
||||
if not cat:
|
||||
raise HTTPException(status_code=404, detail="分类不存在")
|
||||
db.delete(cat)
|
||||
db.commit()
|
||||
return {"message": "删除成功"}
|
||||
|
||||
|
||||
@router.put("/categories/reorder")
|
||||
async def reorder_categories(
|
||||
items: list,
|
||||
db: Session = Depends(get_db),
|
||||
admin: User = Depends(get_admin_user),
|
||||
):
|
||||
"""批量更新分类排序"""
|
||||
for item in items:
|
||||
cat = db.query(Category).filter(Category.id == item["id"]).first()
|
||||
if cat:
|
||||
cat.sort_order = item["sort_order"]
|
||||
db.commit()
|
||||
return {"message": "排序已更新"}
|
||||
|
||||
|
||||
# ---------- 公开分类API(无需管理员权限) ----------
|
||||
|
||||
@router.get("/public/categories")
|
||||
async def get_public_categories(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取启用的分类列表(前台使用)"""
|
||||
cats = db.query(Category).filter(Category.is_active == True).order_by(Category.sort_order, Category.id).all()
|
||||
return [c.name for c in cats]
|
||||
Reference in New Issue
Block a user