Files
bianchengshequ/backend/routers/admin.py

459 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""后台管理路由"""
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]