Refactor models and routes for item management

Reorganized model structure by replacing 'object' and 'items' with a unified 'item' model using UUIDs, and moved base model logic into separate files. Updated routes to use the new item model and improved request/response handling. Enhanced user and setting models, added utility functions, and improved error handling throughout the codebase. Also added initial .idea project files and minor admin API improvements.

Co-Authored-By: 砂糖橘 <54745033+Foxerine@users.noreply.github.com>
This commit is contained in:
2025-10-05 18:58:46 +08:00
parent ee684d67cf
commit cd35c6fbed
34 changed files with 782 additions and 491 deletions

View File

@@ -1,16 +1,17 @@
import random
from typing import Annotated, Literal
from uuid import UUID
from fastapi import APIRouter, Request, Query, HTTPException
from fastapi.responses import JSONResponse
from loguru import logger
from slowapi import Limiter
from slowapi.util import get_remote_address
from model import database, Object, Setting
from model import User
from model.items import Item
from middleware.user import get_current_user
from loguru import logger
from model.response import DefaultResponse, ObjectData
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated, Literal
from middleware.user import get_current_user
from model import DefaultResponse, ItemDataResponse, User, database, Setting, Item
from model.item import ItemDataUpdateRequest
from pkg.utils import raise_not_found, raise_bad_request, raise_internal_error, raise_service_unavailable
limiter = Limiter(key_func=get_remote_address)
@@ -32,21 +33,21 @@ async def get_items(
token: Annotated[User, Depends(get_current_user)],
id: int | None = Query(default=None, ge=1, description='物品ID'),
key: str | None = Query(default=None, description='物品序列号')):
'''
"""
获得物品信息。
不传参数返回所有信息,否则可传入 `id` 或 `key` 进行筛选。
'''
"""
# 根据条件查询物品,只获取当前用户的物品
if id is not None:
results = await Object.get(session, (Object.id == id) & (Object.user_id == token.id))
results = await Item.get(session, (Item.id == id) & (Item.user_id == token.id))
results = [results] if results else []
elif key is not None:
results = await Object.get(session, (Object.key == key) & (Object.user_id == token.id))
results = await Item.get(session, (Item.key == key) & (Item.user_id == token.id))
results = [results] if results else []
else:
results = await Object.get(session, Object.user_id == token.id, fetch_mode="all")
results = await Item.get(session, Item.user_id == token.id, fetch_mode="all")
if results:
items = []
@@ -54,7 +55,7 @@ async def get_items(
items.append(Item(
id=obj.id,
type=obj.type,
key=obj.key,
key=obj.id,
name=obj.name,
icon=obj.icon or "",
status=obj.status or "",
@@ -77,35 +78,22 @@ async def get_items(
)
async def add_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
token: Annotated[User, Depends(get_current_user)],
key: str,
type: Literal['normal', 'car'],
name: str,
icon: str,
phone: str
user: Annotated[User, Depends(get_current_user)],
request: ItemDataUpdateRequest
) -> DefaultResponse:
'''
"""
添加物品信息。
- **key**: 物品的关键字
- **type**: 物品的类型
- **name**: 物品的名称
- **icon**: 物品的图标
- **phone**: 联系电话
'''
"""
try:
# 创建新物品对象,关联当前用户
new_object = Object(
key=key,
type=type,
name=name,
icon=icon,
phone=phone,
user_id=token.id
)
# 使用 base.py 中的 add 方法
await Object.add(session, new_object)
await Item.add(session, Item.model_validate(request))
except Exception as e:
logger.error(f"Failed to add item: {e}")
raise HTTPException(status_code=500, detail=str(e))
@@ -113,7 +101,7 @@ async def add_items(
return DefaultResponse(data=True)
@Router.patch(
path='/items',
path='/items/{item_id}',
summary='更新物品信息',
description='更新现有物品的信息',
response_model=DefaultResponse,
@@ -121,22 +109,15 @@ async def add_items(
)
async def update_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
token: Annotated[User, Depends(get_current_user)],
id: int = Query(ge=1),
key: str | None = None,
name: str | None = None,
icon: str | None = None,
status: str | None = None,
phone: int | None = None,
lost_description: str | None = None,
find_ip: str | None = None,
lost_time: str | None = None
) -> DefaultResponse:
'''
user: Annotated[User, Depends(get_current_user)],
item_id: UUID,
request: ItemDataUpdateRequest
) -> DefaultResponse:
"""
更新物品信息。
只有 `id` 是必填参数,其余参数都是可选的,在不传入任何值的时候将不做任何更改。
- **id**: 物品的ID
- **key**: 物品的序列号 **不建议修改此项,这样会导致生成的物品二维码直接失效**
- **name**: 物品的名称
@@ -146,41 +127,16 @@ async def update_items(
- **lost_description**: 物品丢失描述
- **find_ip**: 找到物品的IP
- **lost_time**: 物品丢失时间
'''
try:
# 获取现有物品,验证归属权
obj = await Object.get(session, (Object.id == id) & (Object.user_id == token.id))
if not obj:
raise HTTPException(status_code=404, detail="Item not found or access denied")
# 更新字段
if key is not None:
obj.key = key
if name is not None:
obj.name = name
if icon is not None:
obj.icon = icon
if status is not None:
obj.status = status
if phone is not None:
obj.phone = str(phone)
if lost_description is not None:
obj.context = lost_description
if find_ip is not None:
obj.find_ip = find_ip
if lost_time is not None:
from datetime import datetime
obj.lost_at = datetime.fromisoformat(lost_time)
# 保存更新
await obj.save(session)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse(data=True)
"""
# 获取现有物品,验证归属权
obj = await Item.get(session, (Item.id == item_id) & (Item.user_id == user.id))
if not obj:
raise_not_found("Item not found or access denied")
await obj.update(session, request)
return DefaultResponse(data=True)
@Router.delete(
path='/items',
@@ -191,27 +147,21 @@ async def update_items(
)
async def delete_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
token: Annotated[User, Depends(get_current_user)],
user: Annotated[User, Depends(get_current_user)],
id: int
) -> DefaultResponse:
'''
"""
删除物品信息。
- **id**: 物品的ID
'''
try:
# 获取现有物品,验证归属权
obj = await Object.get(session, (Object.id == id) & (Object.user_id == token.id))
if not obj:
raise HTTPException(status_code=404, detail="Item not found or access denied")
# 使用 base.py 中的 delete 方法
await Object.delete(session, obj)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse(data=True)
"""
# 获取现有物品,验证归属权
obj = await Item.get(session, (Item.id == id) & (Item.user_id == user.id))
if not obj:
raise_not_found("Item not found or access denied")
await Item.delete(session, obj)
return DefaultResponse(data=True)
@Router.get(
path='/{item_key}',
@@ -224,47 +174,21 @@ async def get_object(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
item_key: str,
request: Request
):
) -> DefaultResponse:
"""
获取物品信息 / Get object information
"""
object_data = await Object.get(session, Object.key == item_key)
object_data = await Item.get(session, Item.key == item_key)
if object_data:
if object_data.status == 'lost':
# 物品已标记为丢失更新IP地址
await Object.update(
session,
id=object_data.id,
find_ip=str(request.client.host)
)
object_data.find_ip = str(request.client.host)
object_data = await object_data.save(session)
# 添加一些随机延迟类似JWT身份验证时根据延迟爆破引发的问题
await asyncio.sleep(random.uniform(0.10, 0.30))
print(object_data)
return DefaultResponse(
data=ObjectData(
id=object_data.id,
type=object_data.type,
key=object_data.key,
name=object_data.name,
icon=object_data.icon,
status=object_data.status,
phone=object_data.phone,
lost_description=object_data.lost_description,
create_time=object_data.create_time,
lost_time=object_data.lost_time
).model_dump()
)
else: return JSONResponse(
status_code=404,
content=DefaultResponse(
code=404,
msg='物品不存在或出现异常'
).model_dump()
)
return DefaultResponse(data=ItemDataResponse.model_validate(object_data))
else:
raise_not_found('物品不存在或出现异常')
@Router.put(
path='/{item_id}',
@@ -274,7 +198,7 @@ async def get_object(
response_description="挪车通知结果"
)
@limiter.limit(
limit_value="1/30minute", # 每30分钟允许1次请求
limit_value="1/5minute", # 每5分钟允许1次请求
error_message="小主已经通知过车主了,请稍安勿躁~"
)
async def notify_move_car(
@@ -283,7 +207,8 @@ async def notify_move_car(
item_id: int,
phone: str = None,
):
"""通知车主进行挪车 / Notify car owner to move the car
"""
通知车主进行挪车 / Notify car owner to move the car
Args:
item_id (int): 物品ID / Item ID
@@ -291,36 +216,18 @@ async def notify_move_car(
"""
# 检查是否存在该物品
object_data = await Object.get(session, Object.id == item_id)
object_data = await Item.get(session, Item.id == item_id)
if not object_data:
return JSONResponse(
status_code=404,
content=DefaultResponse(
code=404,
msg='物品不存在或出现异常'
).model_dump()
)
raise_not_found()
# 检查物品类型是否为车辆
if object_data.type != 'car':
return JSONResponse(
status_code=400,
content=DefaultResponse(
code=400,
msg='该物品不是车辆,无法发送挪车通知'
).model_dump()
)
raise_bad_request("Item is not car")
# 发起挪车通知目前仅适配Server酱
server_chan_key = await Setting.get(session, Setting.name == 'server_chan_key')
if not server_chan_key:
return JSONResponse(
status_code=500,
content=DefaultResponse(
code=500,
msg='未配置Server酱无法发送挪车通知'
).model_dump()
)
raise_internal_error('未配置Server酱无法发送挪车通知')
title = "挪车通知 - Findreve"
description = f"您的车辆“{object_data.name}”被请求挪车。\n\n"
@@ -342,21 +249,15 @@ async def notify_move_car(
return DefaultResponse(msg='挪车通知发送成功')
else:
error_msg = resp_json.get('message')
logger.error(f"Failed to send notification via Server Chan: error_code={resp_json.get('code')}, error_message={error_msg}, item_id={item_id}, response={resp_json}")
return JSONResponse(
status_code=500,
content=DefaultResponse(
code=500,
msg=f"挪车通知发送失败,Server酱返回错误:{error_msg}"
).model_dump()
logger.error(
f"Failed to send notification via Server Chan: error_code={resp_json.get('code')}, "
f"error_message={error_msg}, item_id={item_id}, response={resp_json}"
)
raise_service_unavailable('Server酱出现问题发送失败')
else:
response_text = await resp.text()
logger.error(f"Failed to send notification via Server Chan: http_status={resp.status}, item_id={item_id}, response_body={response_text}, url={resp.url}")
return JSONResponse(
status_code=500,
content=DefaultResponse(
code=500,
msg=f"挪车通知发送失败,HTTP状态码:{resp.status}"
).model_dump()
)
logger.error(
f"Failed to send notification via Server Chan: http_status={resp.status}, item_id={item_id}, "
f"response_body={response_text}, url={resp.url}"
)
raise_internal_error('挪车通知发送失败')