459 lines
16 KiB
Python
459 lines
16 KiB
Python
"""后台管理路由"""
|
||
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]
|