新增挪车功能

This commit is contained in:
2025-10-02 21:52:52 +08:00
parent c648ad35ec
commit 43f22de8fd
7 changed files with 158 additions and 34 deletions

7
app.py
View File

@@ -11,7 +11,7 @@ asyncio.run(model.database.Database().init_db())
# 定义程序参数 # 定义程序参数
APP_NAME: str = 'Findreve' APP_NAME: str = 'Findreve'
VERSION: str = '2.0.0' VERSION: str = '2.0.0-ootc'
summary='标记、追踪与找回 —— 就这么简单。' summary='标记、追踪与找回 —— 就这么简单。'
description='Findreve 是一款强大且直观的解决方案,旨在帮助您管理个人物品,'\ description='Findreve 是一款强大且直观的解决方案,旨在帮助您管理个人物品,'\
'并确保丢失后能够安全找回。每个物品都会被分配一个 唯一 ID '\ '并确保丢失后能够安全找回。每个物品都会被分配一个 唯一 ID '\
@@ -42,11 +42,16 @@ app.include_router(object.Router)
@app.get("/") @app.get("/")
def read_root(): 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") return FileResponse("dist/index.html")
# 回退路由 # 回退路由
@app.get("/{path:path}") @app.get("/{path:path}")
async def serve_spa(request: Request, path: str): 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路由 # 排除API路由
if path.startswith("api/"): if path.startswith("api/"):
raise HTTPException(status_code=404, detail="Not Found") raise HTTPException(status_code=404, detail="Not Found")

View File

@@ -21,5 +21,5 @@ if __name__ == '__main__':
uvicorn.run( uvicorn.run(
'app:app', 'app:app',
host='0.0.0.0', host='0.0.0.0',
port=8080, port=8080
reload=True) )

View File

@@ -13,7 +13,7 @@ import aiosqlite
from datetime import datetime from datetime import datetime
import tool import tool
import logging import logging
from typing import Optional from typing import Literal, Optional
# 数据库类 # 数据库类
class Database: class Database:
@@ -32,6 +32,7 @@ class Database:
create_objects_table = """ create_objects_table = """
CREATE TABLE IF NOT EXISTS fr_objects ( CREATE TABLE IF NOT EXISTS fr_objects (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
key TEXT NOT NULL, key TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
icon TEXT, icon TEXT,
@@ -109,10 +110,18 @@ class Database:
await db.commit() await db.commit()
logging.info("数据库初始化完成并提交更改") 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 key: 序列号
:param name: 名称 :param name: 名称
:param icon: 图标 :param icon: 图标
@@ -126,14 +135,15 @@ class Database:
now = datetime.now() now = datetime.now()
now = now.strftime("%Y-%m-%d %H:%M:%S") now = now.strftime("%Y-%m-%d %H:%M:%S")
await db.execute( await db.execute(
"INSERT INTO fr_objects (key, name, icon, phone, create_at, status) VALUES (?, ?, ?, ?, ?, 'ok')", "INSERT INTO fr_objects (key, name, icon, phone, create_at, status, type) VALUES (?, ?, ?, ?, ?, 'ok', ?)",
(key, name, icon, phone, now) (key, name, icon, phone, now, type)
) )
await db.commit() await db.commit()
async def update_object( async def update_object(
self, self,
id: int, id: int,
type: Literal['normal', 'car'] = None,
key: str = None, key: str = None,
name: str = None, name: str = None,
icon: str = None, icon: str = None,
@@ -141,11 +151,13 @@ class Database:
phone: int = None, phone: int = None,
lost_description: Optional[str] = None, lost_description: Optional[str] = None,
find_ip: Optional[str] = None, find_ip: Optional[str] = None,
lost_time: Optional[str] = None): lost_time: Optional[str] = None
):
""" """
更新对象信息 更新对象信息
:param id: 对象ID :param id: 对象ID
:param type: 对象类型
:param key: 序列号 :param key: 序列号
:param name: 名称 :param name: 名称
:param icon: 图标 :param icon: 图标
@@ -173,13 +185,18 @@ class Database:
f"phone = COALESCE(?, phone), " f"phone = COALESCE(?, phone), "
f"context = COALESCE(?, context), " f"context = COALESCE(?, context), "
f"find_ip = COALESCE(?, find_ip), " 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 = ?", 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() 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
):
""" """
获取对象 获取对象

View File

@@ -3,6 +3,7 @@ from typing import Optional
class Item(BaseModel): class Item(BaseModel):
id: int id: int
type: str
key: str key: str
name: str name: str
icon: str icon: str

View File

@@ -8,6 +8,7 @@ class DefaultResponse(BaseModel):
class ObjectData(BaseModel): class ObjectData(BaseModel):
id: int id: int
type: Literal['normal', 'car']
key: str key: str
name: str name: str
icon: str icon: str

View File

@@ -80,17 +80,19 @@ async def get_items(
items = results items = results
item = [] item = []
for i in items: for i in items:
print(i)
item.append(Item( item.append(Item(
id=i[0], id=i[0],
key=i[1], type=i[1],
name=i[2], key=i[2],
icon=i[3], name=i[3],
status=i[4], icon=i[4],
phone=i[5], status=i[5],
lost_description=i[6], phone=i[6],
find_ip=i[7], lost_description=i[7],
create_time=i[8], find_ip=i[8],
lost_time=i[9] create_time=i[9],
lost_time=i[10]
)) ))
return DefaultResponse(data=item) return DefaultResponse(data=item)
else: else:
@@ -105,13 +107,16 @@ async def get_items(
) )
async def add_items( async def add_items(
key: str, key: str,
type: Literal['normal', 'car'],
name: str, name: str,
icon: str, icon: str,
phone: str) -> DefaultResponse: phone: str
) -> DefaultResponse:
''' '''
添加物品信息。 添加物品信息。
- **key**: 物品的关键字 - **key**: 物品的关键字
- **type**: 物品的类型
- **name**: 物品的名称 - **name**: 物品的名称
- **icon**: 物品的图标 - **icon**: 物品的图标
- **phone**: 联系电话 - **phone**: 联系电话
@@ -119,7 +124,12 @@ async def add_items(
try: try:
await database.Database().add_object( 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: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
else: else:

View File

@@ -4,6 +4,7 @@ from fastapi.responses import JSONResponse
from model.database import Database from model.database import Database
from model.response import DefaultResponse, ObjectData from model.response import DefaultResponse, ObjectData
import asyncio import asyncio
import aiohttp
Router = APIRouter(prefix='/api/object', tags=['物品 Object']) 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) object_data = await db.get_object(key=item_key)
if object_data: if object_data:
if object_data[4] == 'lost': if object_data[5] == 'lost':
# 物品已标记为丢失更新IP地址 # 物品已标记为丢失更新IP地址
await db.update_object(id=object_data[0], find_ip=str(request.client.host)) await db.update_object(id=object_data[0], find_ip=str(request.client.host))
# 添加一些随机延迟类似JWT身份验证时根据延迟爆破引发的问题 # 添加一些随机延迟类似JWT身份验证时根据延迟爆破引发的问题
await asyncio.sleep(random.uniform(0.10, 0.30)) await asyncio.sleep(random.uniform(0.10, 0.30))
else:
await asyncio.sleep(random.uniform(0.10, 0.30))
print(object_data)
return DefaultResponse(data=ObjectData( return DefaultResponse(data=ObjectData(
id=object_data[0], id=object_data[0],
key=object_data[1], type=object_data[1],
name=object_data[2], key=object_data[2],
icon=object_data[3], name=object_data[3],
status=object_data[4], icon=object_data[4],
phone=object_data[5], status=object_data[5],
context=object_data[6] phone=object_data[6],
lost_description=object_data[7],
create_time=object_data[9],
lost_time=object_data[10]
).model_dump()) ).model_dump())
else: return JSONResponse( else: return JSONResponse(
status_code=404, status_code=404,
@@ -48,4 +51,91 @@ async def get_object(item_key: str, request: Request):
code=404, code=404,
msg='物品不存在或出现异常' msg='物品不存在或出现异常'
).model_dump() ).model_dump()
) )
@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()
)