初始提交:极码 GeekCode 全栈项目(FastAPI + Vue3)

This commit is contained in:
2026-04-12 10:12:18 +08:00
commit 6aecef16f6
104 changed files with 21009 additions and 0 deletions

458
backend/routers/admin.py Normal file
View 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]