"""后台管理路由""" 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]