mirror of https://github.com/Mai-with-u/MaiBot.git
484 lines
14 KiB
Python
484 lines
14 KiB
Python
"""表情包管理 API 路由"""
|
||
from fastapi import APIRouter, HTTPException, Header, Query
|
||
from pydantic import BaseModel
|
||
from typing import Optional, List
|
||
from src.common.logger import get_logger
|
||
from src.common.database.database_model import Emoji
|
||
from .token_manager import get_token_manager
|
||
import json
|
||
import time
|
||
|
||
logger = get_logger("webui.emoji")
|
||
|
||
# 创建路由器
|
||
router = APIRouter(prefix="/emoji", tags=["Emoji"])
|
||
|
||
|
||
class EmojiResponse(BaseModel):
|
||
"""表情包响应"""
|
||
id: int
|
||
full_path: str
|
||
format: str
|
||
emoji_hash: str
|
||
description: str
|
||
query_count: int
|
||
is_registered: bool
|
||
is_banned: bool
|
||
emotion: Optional[List[str]] # 解析后的 JSON
|
||
record_time: float
|
||
register_time: Optional[float]
|
||
usage_count: int
|
||
last_used_time: Optional[float]
|
||
|
||
|
||
class EmojiListResponse(BaseModel):
|
||
"""表情包列表响应"""
|
||
success: bool
|
||
total: int
|
||
page: int
|
||
page_size: int
|
||
data: List[EmojiResponse]
|
||
|
||
|
||
class EmojiDetailResponse(BaseModel):
|
||
"""表情包详情响应"""
|
||
success: bool
|
||
data: EmojiResponse
|
||
|
||
|
||
class EmojiUpdateRequest(BaseModel):
|
||
"""表情包更新请求"""
|
||
description: Optional[str] = None
|
||
is_registered: Optional[bool] = None
|
||
is_banned: Optional[bool] = None
|
||
emotion: Optional[List[str]] = None
|
||
|
||
|
||
class EmojiUpdateResponse(BaseModel):
|
||
"""表情包更新响应"""
|
||
success: bool
|
||
message: str
|
||
data: Optional[EmojiResponse] = None
|
||
|
||
|
||
class EmojiDeleteResponse(BaseModel):
|
||
"""表情包删除响应"""
|
||
success: bool
|
||
message: str
|
||
|
||
|
||
def verify_auth_token(authorization: Optional[str]) -> bool:
|
||
"""验证认证 Token"""
|
||
if not authorization or not authorization.startswith("Bearer "):
|
||
raise HTTPException(status_code=401, detail="未提供有效的认证信息")
|
||
|
||
token = authorization.replace("Bearer ", "")
|
||
token_manager = get_token_manager()
|
||
|
||
if not token_manager.verify_token(token):
|
||
raise HTTPException(status_code=401, detail="Token 无效或已过期")
|
||
|
||
return True
|
||
|
||
|
||
def parse_emotion(emotion_str: Optional[str]) -> Optional[List[str]]:
|
||
"""解析情感标签 JSON 字符串"""
|
||
if not emotion_str:
|
||
return None
|
||
try:
|
||
return json.loads(emotion_str)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return None
|
||
|
||
|
||
def emoji_to_response(emoji: Emoji) -> EmojiResponse:
|
||
"""将 Emoji 模型转换为响应对象"""
|
||
return EmojiResponse(
|
||
id=emoji.id,
|
||
full_path=emoji.full_path,
|
||
format=emoji.format,
|
||
emoji_hash=emoji.emoji_hash,
|
||
description=emoji.description,
|
||
query_count=emoji.query_count,
|
||
is_registered=emoji.is_registered,
|
||
is_banned=emoji.is_banned,
|
||
emotion=parse_emotion(emoji.emotion),
|
||
record_time=emoji.record_time,
|
||
register_time=emoji.register_time,
|
||
usage_count=emoji.usage_count,
|
||
last_used_time=emoji.last_used_time,
|
||
)
|
||
|
||
|
||
@router.get("/list", response_model=EmojiListResponse)
|
||
async def get_emoji_list(
|
||
page: int = Query(1, ge=1, description="页码"),
|
||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||
search: Optional[str] = Query(None, description="搜索关键词"),
|
||
is_registered: Optional[bool] = Query(None, description="是否已注册筛选"),
|
||
is_banned: Optional[bool] = Query(None, description="是否被禁用筛选"),
|
||
format: Optional[str] = Query(None, description="格式筛选"),
|
||
authorization: Optional[str] = Header(None)
|
||
):
|
||
"""
|
||
获取表情包列表
|
||
|
||
Args:
|
||
page: 页码 (从 1 开始)
|
||
page_size: 每页数量 (1-100)
|
||
search: 搜索关键词 (匹配 description, emoji_hash)
|
||
is_registered: 是否已注册筛选
|
||
is_banned: 是否被禁用筛选
|
||
format: 格式筛选
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
表情包列表
|
||
"""
|
||
try:
|
||
verify_auth_token(authorization)
|
||
|
||
# 构建查询
|
||
query = Emoji.select()
|
||
|
||
# 搜索过滤
|
||
if search:
|
||
query = query.where(
|
||
(Emoji.description.contains(search)) |
|
||
(Emoji.emoji_hash.contains(search))
|
||
)
|
||
|
||
# 注册状态过滤
|
||
if is_registered is not None:
|
||
query = query.where(Emoji.is_registered == is_registered)
|
||
|
||
# 禁用状态过滤
|
||
if is_banned is not None:
|
||
query = query.where(Emoji.is_banned == is_banned)
|
||
|
||
# 格式过滤
|
||
if format:
|
||
query = query.where(Emoji.format == format)
|
||
|
||
# 排序:使用次数倒序,然后按记录时间倒序
|
||
from peewee import Case
|
||
query = query.order_by(
|
||
Emoji.usage_count.desc(),
|
||
Case(None, [(Emoji.record_time.is_null(), 1)], 0),
|
||
Emoji.record_time.desc()
|
||
)
|
||
|
||
# 获取总数
|
||
total = query.count()
|
||
|
||
# 分页
|
||
offset = (page - 1) * page_size
|
||
emojis = query.offset(offset).limit(page_size)
|
||
|
||
# 转换为响应对象
|
||
data = [emoji_to_response(emoji) for emoji in emojis]
|
||
|
||
return EmojiListResponse(
|
||
success=True,
|
||
total=total,
|
||
page=page,
|
||
page_size=page_size,
|
||
data=data
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"获取表情包列表失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"获取表情包列表失败: {str(e)}") from e
|
||
|
||
|
||
@router.get("/{emoji_id}", response_model=EmojiDetailResponse)
|
||
async def get_emoji_detail(
|
||
emoji_id: int,
|
||
authorization: Optional[str] = Header(None)
|
||
):
|
||
"""
|
||
获取表情包详细信息
|
||
|
||
Args:
|
||
emoji_id: 表情包ID
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
表情包详细信息
|
||
"""
|
||
try:
|
||
verify_auth_token(authorization)
|
||
|
||
emoji = Emoji.get_or_none(Emoji.id == emoji_id)
|
||
|
||
if not emoji:
|
||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {emoji_id} 的表情包")
|
||
|
||
return EmojiDetailResponse(
|
||
success=True,
|
||
data=emoji_to_response(emoji)
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"获取表情包详情失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"获取表情包详情失败: {str(e)}") from e
|
||
|
||
|
||
@router.patch("/{emoji_id}", response_model=EmojiUpdateResponse)
|
||
async def update_emoji(
|
||
emoji_id: int,
|
||
request: EmojiUpdateRequest,
|
||
authorization: Optional[str] = Header(None)
|
||
):
|
||
"""
|
||
增量更新表情包(只更新提供的字段)
|
||
|
||
Args:
|
||
emoji_id: 表情包ID
|
||
request: 更新请求(只包含需要更新的字段)
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
更新结果
|
||
"""
|
||
try:
|
||
verify_auth_token(authorization)
|
||
|
||
emoji = Emoji.get_or_none(Emoji.id == emoji_id)
|
||
|
||
if not emoji:
|
||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {emoji_id} 的表情包")
|
||
|
||
# 只更新提供的字段
|
||
update_data = request.model_dump(exclude_unset=True)
|
||
|
||
if not update_data:
|
||
raise HTTPException(status_code=400, detail="未提供任何需要更新的字段")
|
||
|
||
# 处理情感标签(转换为 JSON)
|
||
if 'emotion' in update_data:
|
||
if update_data['emotion'] is None:
|
||
update_data['emotion'] = None
|
||
else:
|
||
update_data['emotion'] = json.dumps(update_data['emotion'], ensure_ascii=False)
|
||
|
||
# 如果注册状态从 False 变为 True,记录注册时间
|
||
if 'is_registered' in update_data and update_data['is_registered'] and not emoji.is_registered:
|
||
update_data['register_time'] = time.time()
|
||
|
||
# 执行更新
|
||
for field, value in update_data.items():
|
||
setattr(emoji, field, value)
|
||
|
||
emoji.save()
|
||
|
||
logger.info(f"表情包已更新: ID={emoji_id}, 字段: {list(update_data.keys())}")
|
||
|
||
return EmojiUpdateResponse(
|
||
success=True,
|
||
message=f"成功更新 {len(update_data)} 个字段",
|
||
data=emoji_to_response(emoji)
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"更新表情包失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"更新表情包失败: {str(e)}") from e
|
||
|
||
|
||
@router.delete("/{emoji_id}", response_model=EmojiDeleteResponse)
|
||
async def delete_emoji(
|
||
emoji_id: int,
|
||
authorization: Optional[str] = Header(None)
|
||
):
|
||
"""
|
||
删除表情包
|
||
|
||
Args:
|
||
emoji_id: 表情包ID
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
删除结果
|
||
"""
|
||
try:
|
||
verify_auth_token(authorization)
|
||
|
||
emoji = Emoji.get_or_none(Emoji.id == emoji_id)
|
||
|
||
if not emoji:
|
||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {emoji_id} 的表情包")
|
||
|
||
# 记录删除信息
|
||
emoji_hash = emoji.emoji_hash
|
||
|
||
# 执行删除
|
||
emoji.delete_instance()
|
||
|
||
logger.info(f"表情包已删除: ID={emoji_id}, hash={emoji_hash}")
|
||
|
||
return EmojiDeleteResponse(
|
||
success=True,
|
||
message=f"成功删除表情包: {emoji_hash}"
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"删除表情包失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"删除表情包失败: {str(e)}") from e
|
||
|
||
|
||
@router.get("/stats/summary")
|
||
async def get_emoji_stats(
|
||
authorization: Optional[str] = Header(None)
|
||
):
|
||
"""
|
||
获取表情包统计数据
|
||
|
||
Args:
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
统计数据
|
||
"""
|
||
try:
|
||
verify_auth_token(authorization)
|
||
|
||
total = Emoji.select().count()
|
||
registered = Emoji.select().where(Emoji.is_registered).count()
|
||
banned = Emoji.select().where(Emoji.is_banned).count()
|
||
|
||
# 按格式统计
|
||
formats = {}
|
||
for emoji in Emoji.select(Emoji.format):
|
||
fmt = emoji.format
|
||
formats[fmt] = formats.get(fmt, 0) + 1
|
||
|
||
# 获取最常用的表情包(前10)
|
||
top_used = Emoji.select().order_by(Emoji.usage_count.desc()).limit(10)
|
||
top_used_list = [
|
||
{
|
||
"id": emoji.id,
|
||
"emoji_hash": emoji.emoji_hash,
|
||
"description": emoji.description,
|
||
"usage_count": emoji.usage_count
|
||
}
|
||
for emoji in top_used
|
||
]
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"total": total,
|
||
"registered": registered,
|
||
"banned": banned,
|
||
"unregistered": total - registered,
|
||
"formats": formats,
|
||
"top_used": top_used_list
|
||
}
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"获取统计数据失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"获取统计数据失败: {str(e)}") from e
|
||
|
||
|
||
@router.post("/{emoji_id}/register", response_model=EmojiUpdateResponse)
|
||
async def register_emoji(
|
||
emoji_id: int,
|
||
authorization: Optional[str] = Header(None)
|
||
):
|
||
"""
|
||
注册表情包(快捷操作)
|
||
|
||
Args:
|
||
emoji_id: 表情包ID
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
更新结果
|
||
"""
|
||
try:
|
||
verify_auth_token(authorization)
|
||
|
||
emoji = Emoji.get_or_none(Emoji.id == emoji_id)
|
||
|
||
if not emoji:
|
||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {emoji_id} 的表情包")
|
||
|
||
if emoji.is_registered:
|
||
raise HTTPException(status_code=400, detail="该表情包已经注册")
|
||
|
||
if emoji.is_banned:
|
||
raise HTTPException(status_code=400, detail="该表情包已被禁用,无法注册")
|
||
|
||
# 注册表情包
|
||
emoji.is_registered = True
|
||
emoji.register_time = time.time()
|
||
emoji.save()
|
||
|
||
logger.info(f"表情包已注册: ID={emoji_id}")
|
||
|
||
return EmojiUpdateResponse(
|
||
success=True,
|
||
message="表情包注册成功",
|
||
data=emoji_to_response(emoji)
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"注册表情包失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"注册表情包失败: {str(e)}") from e
|
||
|
||
|
||
@router.post("/{emoji_id}/ban", response_model=EmojiUpdateResponse)
|
||
async def ban_emoji(
|
||
emoji_id: int,
|
||
authorization: Optional[str] = Header(None)
|
||
):
|
||
"""
|
||
禁用表情包(快捷操作)
|
||
|
||
Args:
|
||
emoji_id: 表情包ID
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
更新结果
|
||
"""
|
||
try:
|
||
verify_auth_token(authorization)
|
||
|
||
emoji = Emoji.get_or_none(Emoji.id == emoji_id)
|
||
|
||
if not emoji:
|
||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {emoji_id} 的表情包")
|
||
|
||
# 禁用表情包(同时取消注册)
|
||
emoji.is_banned = True
|
||
emoji.is_registered = False
|
||
emoji.save()
|
||
|
||
logger.info(f"表情包已禁用: ID={emoji_id}")
|
||
|
||
return EmojiUpdateResponse(
|
||
success=True,
|
||
message="表情包禁用成功",
|
||
data=emoji_to_response(emoji)
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"禁用表情包失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"禁用表情包失败: {str(e)}") from e
|