mirror of https://github.com/Mai-with-u/MaiBot.git
404 lines
12 KiB
Python
404 lines
12 KiB
Python
"""人物信息管理 API 路由"""
|
||
|
||
from fastapi import APIRouter, HTTPException, Header, Query, Cookie
|
||
from pydantic import BaseModel
|
||
from typing import Optional, List, Dict
|
||
from src.common.logger import get_logger
|
||
from src.common.database.database_model import PersonInfo
|
||
from .auth import verify_auth_token_from_cookie_or_header
|
||
import json
|
||
import time
|
||
|
||
logger = get_logger("webui.person")
|
||
|
||
# 创建路由器
|
||
router = APIRouter(prefix="/person", tags=["Person"])
|
||
|
||
|
||
class PersonInfoResponse(BaseModel):
|
||
"""人物信息响应"""
|
||
|
||
id: int
|
||
is_known: bool
|
||
person_id: str
|
||
person_name: Optional[str]
|
||
name_reason: Optional[str]
|
||
platform: str
|
||
user_id: str
|
||
nickname: Optional[str]
|
||
group_nick_name: Optional[List[Dict[str, str]]] # 解析后的 JSON
|
||
memory_points: Optional[str]
|
||
know_times: Optional[float]
|
||
know_since: Optional[float]
|
||
last_know: Optional[float]
|
||
|
||
|
||
class PersonListResponse(BaseModel):
|
||
"""人物列表响应"""
|
||
|
||
success: bool
|
||
total: int
|
||
page: int
|
||
page_size: int
|
||
data: List[PersonInfoResponse]
|
||
|
||
|
||
class PersonDetailResponse(BaseModel):
|
||
"""人物详情响应"""
|
||
|
||
success: bool
|
||
data: PersonInfoResponse
|
||
|
||
|
||
class PersonUpdateRequest(BaseModel):
|
||
"""人物信息更新请求"""
|
||
|
||
person_name: Optional[str] = None
|
||
name_reason: Optional[str] = None
|
||
nickname: Optional[str] = None
|
||
memory_points: Optional[str] = None
|
||
is_known: Optional[bool] = None
|
||
|
||
|
||
class PersonUpdateResponse(BaseModel):
|
||
"""人物信息更新响应"""
|
||
|
||
success: bool
|
||
message: str
|
||
data: Optional[PersonInfoResponse] = None
|
||
|
||
|
||
class PersonDeleteResponse(BaseModel):
|
||
"""人物删除响应"""
|
||
|
||
success: bool
|
||
message: str
|
||
|
||
|
||
class BatchDeleteRequest(BaseModel):
|
||
"""批量删除请求"""
|
||
|
||
person_ids: List[str]
|
||
|
||
|
||
class BatchDeleteResponse(BaseModel):
|
||
"""批量删除响应"""
|
||
|
||
success: bool
|
||
message: str
|
||
deleted_count: int
|
||
failed_count: int
|
||
failed_ids: List[str] = []
|
||
|
||
|
||
def verify_auth_token(
|
||
maibot_session: Optional[str] = None,
|
||
authorization: Optional[str] = None,
|
||
) -> bool:
|
||
"""验证认证 Token,支持 Cookie 和 Header"""
|
||
return verify_auth_token_from_cookie_or_header(maibot_session, authorization)
|
||
|
||
|
||
def parse_group_nick_name(group_nick_name_str: Optional[str]) -> Optional[List[Dict[str, str]]]:
|
||
"""解析群昵称 JSON 字符串"""
|
||
if not group_nick_name_str:
|
||
return None
|
||
try:
|
||
return json.loads(group_nick_name_str)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return None
|
||
|
||
|
||
def person_to_response(person: PersonInfo) -> PersonInfoResponse:
|
||
"""将 PersonInfo 模型转换为响应对象"""
|
||
return PersonInfoResponse(
|
||
id=person.id,
|
||
is_known=person.is_known,
|
||
person_id=person.person_id,
|
||
person_name=person.person_name,
|
||
name_reason=person.name_reason,
|
||
platform=person.platform,
|
||
user_id=person.user_id,
|
||
nickname=person.nickname,
|
||
group_nick_name=parse_group_nick_name(person.group_nick_name),
|
||
memory_points=person.memory_points,
|
||
know_times=person.know_times,
|
||
know_since=person.know_since,
|
||
last_know=person.last_know,
|
||
)
|
||
|
||
|
||
@router.get("/list", response_model=PersonListResponse)
|
||
async def get_person_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_known: Optional[bool] = Query(None, description="是否已认识筛选"),
|
||
platform: Optional[str] = Query(None, description="平台筛选"),
|
||
maibot_session: Optional[str] = Cookie(None),
|
||
authorization: Optional[str] = Header(None),
|
||
):
|
||
"""
|
||
获取人物信息列表
|
||
|
||
Args:
|
||
page: 页码 (从 1 开始)
|
||
page_size: 每页数量 (1-100)
|
||
search: 搜索关键词 (匹配 person_name, nickname, user_id)
|
||
is_known: 是否已认识筛选
|
||
platform: 平台筛选
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
人物信息列表
|
||
"""
|
||
try:
|
||
verify_auth_token(maibot_session, authorization)
|
||
|
||
# 构建查询
|
||
query = PersonInfo.select()
|
||
|
||
# 搜索过滤
|
||
if search:
|
||
query = query.where(
|
||
(PersonInfo.person_name.contains(search))
|
||
| (PersonInfo.nickname.contains(search))
|
||
| (PersonInfo.user_id.contains(search))
|
||
)
|
||
|
||
# 已认识状态过滤
|
||
if is_known is not None:
|
||
query = query.where(PersonInfo.is_known == is_known)
|
||
|
||
# 平台过滤
|
||
if platform:
|
||
query = query.where(PersonInfo.platform == platform)
|
||
|
||
# 排序:最后更新时间倒序(NULL 值放在最后)
|
||
# Peewee 不支持 nulls_last,使用 CASE WHEN 来实现
|
||
from peewee import Case
|
||
|
||
query = query.order_by(Case(None, [(PersonInfo.last_know.is_null(), 1)], 0), PersonInfo.last_know.desc())
|
||
|
||
# 获取总数
|
||
total = query.count()
|
||
|
||
# 分页
|
||
offset = (page - 1) * page_size
|
||
persons = query.offset(offset).limit(page_size)
|
||
|
||
# 转换为响应对象
|
||
data = [person_to_response(person) for person in persons]
|
||
|
||
return PersonListResponse(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("/{person_id}", response_model=PersonDetailResponse)
|
||
async def get_person_detail(person_id: str, maibot_session: Optional[str] = Cookie(None), authorization: Optional[str] = Header(None)):
|
||
"""
|
||
获取人物详细信息
|
||
|
||
Args:
|
||
person_id: 人物唯一 ID
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
人物详细信息
|
||
"""
|
||
try:
|
||
verify_auth_token(maibot_session, authorization)
|
||
|
||
person = PersonInfo.get_or_none(PersonInfo.person_id == person_id)
|
||
|
||
if not person:
|
||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {person_id} 的人物信息")
|
||
|
||
return PersonDetailResponse(success=True, data=person_to_response(person))
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"获取人物详情失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"获取人物详情失败: {str(e)}") from e
|
||
|
||
|
||
@router.patch("/{person_id}", response_model=PersonUpdateResponse)
|
||
async def update_person(person_id: str, request: PersonUpdateRequest, maibot_session: Optional[str] = Cookie(None), authorization: Optional[str] = Header(None)):
|
||
"""
|
||
增量更新人物信息(只更新提供的字段)
|
||
|
||
Args:
|
||
person_id: 人物唯一 ID
|
||
request: 更新请求(只包含需要更新的字段)
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
更新结果
|
||
"""
|
||
try:
|
||
verify_auth_token(maibot_session, authorization)
|
||
|
||
person = PersonInfo.get_or_none(PersonInfo.person_id == person_id)
|
||
|
||
if not person:
|
||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {person_id} 的人物信息")
|
||
|
||
# 只更新提供的字段
|
||
update_data = request.model_dump(exclude_unset=True)
|
||
|
||
if not update_data:
|
||
raise HTTPException(status_code=400, detail="未提供任何需要更新的字段")
|
||
|
||
# 更新最后修改时间
|
||
update_data["last_know"] = time.time()
|
||
|
||
# 执行更新
|
||
for field, value in update_data.items():
|
||
setattr(person, field, value)
|
||
|
||
person.save()
|
||
|
||
logger.info(f"人物信息已更新: {person_id}, 字段: {list(update_data.keys())}")
|
||
|
||
return PersonUpdateResponse(
|
||
success=True, message=f"成功更新 {len(update_data)} 个字段", data=person_to_response(person)
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"更新人物信息失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"更新人物信息失败: {str(e)}") from e
|
||
|
||
|
||
@router.delete("/{person_id}", response_model=PersonDeleteResponse)
|
||
async def delete_person(person_id: str, maibot_session: Optional[str] = Cookie(None), authorization: Optional[str] = Header(None)):
|
||
"""
|
||
删除人物信息
|
||
|
||
Args:
|
||
person_id: 人物唯一 ID
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
删除结果
|
||
"""
|
||
try:
|
||
verify_auth_token(maibot_session, authorization)
|
||
|
||
person = PersonInfo.get_or_none(PersonInfo.person_id == person_id)
|
||
|
||
if not person:
|
||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {person_id} 的人物信息")
|
||
|
||
# 记录删除信息
|
||
person_name = person.person_name or person.nickname or person.user_id
|
||
|
||
# 执行删除
|
||
person.delete_instance()
|
||
|
||
logger.info(f"人物信息已删除: {person_id} ({person_name})")
|
||
|
||
return PersonDeleteResponse(success=True, message=f"成功删除人物信息: {person_name}")
|
||
|
||
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_person_stats(maibot_session: Optional[str] = Cookie(None), authorization: Optional[str] = Header(None)):
|
||
"""
|
||
获取人物信息统计数据
|
||
|
||
Args:
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
统计数据
|
||
"""
|
||
try:
|
||
verify_auth_token(maibot_session, authorization)
|
||
|
||
total = PersonInfo.select().count()
|
||
known = PersonInfo.select().where(PersonInfo.is_known).count()
|
||
unknown = total - known
|
||
|
||
# 按平台统计
|
||
platforms = {}
|
||
for person in PersonInfo.select(PersonInfo.platform):
|
||
platform = person.platform
|
||
platforms[platform] = platforms.get(platform, 0) + 1
|
||
|
||
return {"success": True, "data": {"total": total, "known": known, "unknown": unknown, "platforms": platforms}}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"获取统计数据失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"获取统计数据失败: {str(e)}") from e
|
||
|
||
|
||
@router.post("/batch/delete", response_model=BatchDeleteResponse)
|
||
async def batch_delete_persons(request: BatchDeleteRequest, maibot_session: Optional[str] = Cookie(None), authorization: Optional[str] = Header(None)):
|
||
"""
|
||
批量删除人物信息
|
||
|
||
Args:
|
||
request: 包含person_ids列表的请求
|
||
authorization: Authorization header
|
||
|
||
Returns:
|
||
批量删除结果
|
||
"""
|
||
try:
|
||
verify_auth_token(maibot_session, authorization)
|
||
|
||
if not request.person_ids:
|
||
raise HTTPException(status_code=400, detail="未提供要删除的人物ID")
|
||
|
||
deleted_count = 0
|
||
failed_count = 0
|
||
failed_ids = []
|
||
|
||
for person_id in request.person_ids:
|
||
try:
|
||
person = PersonInfo.get_or_none(PersonInfo.person_id == person_id)
|
||
if person:
|
||
person.delete_instance()
|
||
deleted_count += 1
|
||
logger.info(f"批量删除: {person_id}")
|
||
else:
|
||
failed_count += 1
|
||
failed_ids.append(person_id)
|
||
except Exception as e:
|
||
logger.error(f"删除 {person_id} 失败: {e}")
|
||
failed_count += 1
|
||
failed_ids.append(person_id)
|
||
|
||
message = f"成功删除 {deleted_count} 个人物"
|
||
if failed_count > 0:
|
||
message += f",{failed_count} 个失败"
|
||
|
||
return BatchDeleteResponse(
|
||
success=True,
|
||
message=message,
|
||
deleted_count=deleted_count,
|
||
failed_count=failed_count,
|
||
failed_ids=failed_ids,
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.exception(f"批量删除人物信息失败: {e}")
|
||
raise HTTPException(status_code=500, detail=f"批量删除失败: {str(e)}") from e
|