From 43f22de8fd6f3655bde761500bdbb8dfae1ba6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=B0=8F=E4=B8=98?= Date: Thu, 2 Oct 2025 21:52:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8C=AA=E8=BD=A6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 7 ++- main.py | 4 +- model/database.py | 33 ++++++++++---- model/items.py | 1 + model/response.py | 1 + routes/admin.py | 32 ++++++++----- routes/object.py | 114 +++++++++++++++++++++++++++++++++++++++++----- 7 files changed, 158 insertions(+), 34 deletions(-) diff --git a/app.py b/app.py index 0ad36a0..e89af34 100644 --- a/app.py +++ b/app.py @@ -11,7 +11,7 @@ asyncio.run(model.database.Database().init_db()) # 定义程序参数 APP_NAME: str = 'Findreve' -VERSION: str = '2.0.0' +VERSION: str = '2.0.0-ootc' summary='标记、追踪与找回 —— 就这么简单。' description='Findreve 是一款强大且直观的解决方案,旨在帮助您管理个人物品,'\ '并确保丢失后能够安全找回。每个物品都会被分配一个 唯一 ID ,'\ @@ -42,11 +42,16 @@ app.include_router(object.Router) @app.get("/") def read_root(): + if not os.path.exists("dist/index.html"): + raise HTTPException(status_code=404, detail="Frontend not built. Please build the frontend first.") return FileResponse("dist/index.html") # 回退路由 @app.get("/{path:path}") async def serve_spa(request: Request, path: str): + if not os.path.exists("dist/index.html"): + raise HTTPException(status_code=404, detail="Frontend not built. Please build the frontend first.") + # 排除API路由 if path.startswith("api/"): raise HTTPException(status_code=404, detail="Not Found") diff --git a/main.py b/main.py index a45c395..240541f 100644 --- a/main.py +++ b/main.py @@ -21,5 +21,5 @@ if __name__ == '__main__': uvicorn.run( 'app:app', host='0.0.0.0', - port=8080, - reload=True) \ No newline at end of file + port=8080 + ) \ No newline at end of file diff --git a/model/database.py b/model/database.py index ddc7e62..0e8ab0c 100644 --- a/model/database.py +++ b/model/database.py @@ -13,7 +13,7 @@ import aiosqlite from datetime import datetime import tool import logging -from typing import Optional +from typing import Literal, Optional # 数据库类 class Database: @@ -32,6 +32,7 @@ class Database: create_objects_table = """ CREATE TABLE IF NOT EXISTS fr_objects ( id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, key TEXT NOT NULL, name TEXT NOT NULL, icon TEXT, @@ -109,10 +110,18 @@ class Database: await db.commit() logging.info("数据库初始化完成并提交更改") - async def add_object(self, key: str, name: str, icon: str = None, phone: str = None): + async def add_object( + self, + key: str, + type: Literal['normal', 'car'], + name: str, + icon: str = None, + phone: str = None, + ): """ 添加新对象 + :param type: 对象类型 :param key: 序列号 :param name: 名称 :param icon: 图标 @@ -126,14 +135,15 @@ class Database: now = datetime.now() now = now.strftime("%Y-%m-%d %H:%M:%S") await db.execute( - "INSERT INTO fr_objects (key, name, icon, phone, create_at, status) VALUES (?, ?, ?, ?, ?, 'ok')", - (key, name, icon, phone, now) + "INSERT INTO fr_objects (key, name, icon, phone, create_at, status, type) VALUES (?, ?, ?, ?, ?, 'ok', ?)", + (key, name, icon, phone, now, type) ) await db.commit() async def update_object( self, id: int, + type: Literal['normal', 'car'] = None, key: str = None, name: str = None, icon: str = None, @@ -141,11 +151,13 @@ class Database: phone: int = None, lost_description: Optional[str] = None, find_ip: Optional[str] = None, - lost_time: Optional[str] = None): + lost_time: Optional[str] = None + ): """ 更新对象信息 :param id: 对象ID + :param type: 对象类型 :param key: 序列号 :param name: 名称 :param icon: 图标 @@ -173,13 +185,18 @@ class Database: f"phone = COALESCE(?, phone), " f"context = COALESCE(?, context), " f"find_ip = COALESCE(?, find_ip), " - f"lost_at = COALESCE(?, lost_at) " + f"lost_at = COALESCE(?, lost_at), " + f"type = COALESCE(?, type) " f"WHERE id = ?", - (key, name, icon, status, phone, lost_description, find_ip, lost_time, id) + (key, name, icon, status, phone, lost_description, find_ip, lost_time, type, id) ) await db.commit() - async def get_object(self, id: int = None, key: str = None): + async def get_object( + self, + id: int = None, + key: str = None + ): """ 获取对象 diff --git a/model/items.py b/model/items.py index 5479206..b39ed3a 100644 --- a/model/items.py +++ b/model/items.py @@ -3,6 +3,7 @@ from typing import Optional class Item(BaseModel): id: int + type: str key: str name: str icon: str diff --git a/model/response.py b/model/response.py index 367ad96..a87dd32 100644 --- a/model/response.py +++ b/model/response.py @@ -8,6 +8,7 @@ class DefaultResponse(BaseModel): class ObjectData(BaseModel): id: int + type: Literal['normal', 'car'] key: str name: str icon: str diff --git a/routes/admin.py b/routes/admin.py index 9b2b795..f4b0437 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -80,17 +80,19 @@ async def get_items( items = results item = [] for i in items: + print(i) item.append(Item( id=i[0], - key=i[1], - name=i[2], - icon=i[3], - status=i[4], - phone=i[5], - lost_description=i[6], - find_ip=i[7], - create_time=i[8], - lost_time=i[9] + type=i[1], + key=i[2], + name=i[3], + icon=i[4], + status=i[5], + phone=i[6], + lost_description=i[7], + find_ip=i[8], + create_time=i[9], + lost_time=i[10] )) return DefaultResponse(data=item) else: @@ -105,13 +107,16 @@ async def get_items( ) async def add_items( key: str, + type: Literal['normal', 'car'], name: str, icon: str, - phone: str) -> DefaultResponse: + phone: str +) -> DefaultResponse: ''' 添加物品信息。 - **key**: 物品的关键字 + - **type**: 物品的类型 - **name**: 物品的名称 - **icon**: 物品的图标 - **phone**: 联系电话 @@ -119,7 +124,12 @@ async def add_items( try: await database.Database().add_object( - key=key, name=name, icon=icon, phone=phone) + key=key, + type=type, + name=name, + icon=icon, + phone=phone + ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) else: diff --git a/routes/object.py b/routes/object.py index cf209f8..e81ffc3 100644 --- a/routes/object.py +++ b/routes/object.py @@ -4,6 +4,7 @@ from fastapi.responses import JSONResponse from model.database import Database from model.response import DefaultResponse, ObjectData import asyncio +import aiohttp Router = APIRouter(prefix='/api/object', tags=['物品 Object']) @@ -24,23 +25,25 @@ async def get_object(item_key: str, request: Request): object_data = await db.get_object(key=item_key) if object_data: - if object_data[4] == 'lost': + if object_data[5] == 'lost': # 物品已标记为丢失,更新IP地址 await db.update_object(id=object_data[0], find_ip=str(request.client.host)) - # 添加一些随机延迟,类似JWT身份验证时根据延迟爆破引发的问题 - await asyncio.sleep(random.uniform(0.10, 0.30)) - else: - await asyncio.sleep(random.uniform(0.10, 0.30)) + # 添加一些随机延迟,类似JWT身份验证时根据延迟爆破引发的问题 + await asyncio.sleep(random.uniform(0.10, 0.30)) + print(object_data) return DefaultResponse(data=ObjectData( id=object_data[0], - key=object_data[1], - name=object_data[2], - icon=object_data[3], - status=object_data[4], - phone=object_data[5], - context=object_data[6] + type=object_data[1], + key=object_data[2], + name=object_data[3], + icon=object_data[4], + status=object_data[5], + phone=object_data[6], + lost_description=object_data[7], + create_time=object_data[9], + lost_time=object_data[10] ).model_dump()) else: return JSONResponse( status_code=404, @@ -48,4 +51,91 @@ async def get_object(item_key: str, request: Request): code=404, msg='物品不存在或出现异常' ).model_dump() - ) \ No newline at end of file + ) + +@Router.put( + path='/{item_id}', + summary="通知车主进行挪车", + description="向车主发送挪车通知", + response_model=DefaultResponse, + response_description="挪车通知结果" +) +async def notify_move_car( + item_id: int, + phone: str = None, +): + """通知车主进行挪车 / Notify car owner to move the car + + Args: + item_id (int): 物品ID / Item ID + phone (str, optional): 挪车发起者电话 / Phone number of the person initiating the move. Defaults to None. + """ + db = Database() + await db.init_db() + + # 检查是否存在该物品 + object_data = await db.get_object(id=item_id) + if not object_data: + return JSONResponse( + status_code=404, + content=DefaultResponse( + code=404, + msg='物品不存在或出现异常' + ).model_dump() + ) + + # 检查物品类型是否为车辆 + if object_data[1] != 'car': + return JSONResponse( + status_code=400, + content=DefaultResponse( + code=400, + msg='该物品不是车辆,无法发送挪车通知' + ).model_dump() + ) + + # 发起挪车通知(目前仅适配Server酱) + server_chan_key = await db.get_setting('server_chan_key') + if not server_chan_key: + return JSONResponse( + status_code=500, + content=DefaultResponse( + code=500, + msg='未配置Server酱,无法发送挪车通知' + ).model_dump() + ) + + title = "挪车通知 - Findreve" + description = f"您的车辆“{object_data[3]}”被请求挪车。\n\n" + if phone: + description += f"请求挪车者电话:[{phone}](tel:{phone})\n\n" + description += "请尽快联系请求者并挪车。" + + async with aiohttp.ClientSession() as session: + async with session.post( + url=f"https://sctapi.ftqq.com/{server_chan_key}.send", + data={ + "title": title, + "desp": description + } + ) as resp: + if resp.status == 200: + resp_json = await resp.json() + if resp_json.get('code') == 0: + return DefaultResponse(msg='挪车通知发送成功') + else: + return JSONResponse( + status_code=500, + content=DefaultResponse( + code=500, + msg=f"挪车通知发送失败,Server酱返回错误:{resp_json.get('message')}" + ).model_dump() + ) + else: + return JSONResponse( + status_code=500, + content=DefaultResponse( + code=500, + msg=f"挪车通知发送失败,HTTP状态码:{resp.status}" + ).model_dump() + ) \ No newline at end of file