Add notification sender and update item routes
This commit is contained in:
6
dependencies.py
Normal file
6
dependencies.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
from fastapi import Depends
|
||||||
|
from model import database
|
||||||
|
|
||||||
|
SessionDep = Annotated[AsyncSession, Depends(database.Database.get_session)]
|
||||||
@@ -196,5 +196,6 @@ class UUIDTableBase(TableBase):
|
|||||||
"""override"""
|
"""override"""
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@classmethod
|
||||||
async def get_exist_one(cls: Type[T], session: AsyncSession, id: uuid.UUID, load: Union[Relationship, None] = None) -> T:
|
async def get_exist_one(cls: Type[T], session: AsyncSession, id: uuid.UUID, load: Union[Relationship, None] = None) -> T:
|
||||||
return super().get_exist_one(session, id, load) # type: ignore
|
return await super().get_exist_one(session, id, load)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from typing import TYPE_CHECKING, Self, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import Field, Relationship
|
from sqlmodel import Field, Relationship
|
||||||
@@ -52,12 +52,19 @@ class Item(ItemBase, UUIDTableBase, table=True):
|
|||||||
|
|
||||||
user: 'User' = Relationship(back_populates='items')
|
user: 'User' = Relationship(back_populates='items')
|
||||||
|
|
||||||
parent_item_id: UUID | None = Field(foreign_key='item.id', ondelete='RESTRICT')
|
parent_item_id: UUID | None = Field(foreign_key='item.id', ondelete='RESTRICT', default=None)
|
||||||
parent_item: Optional['Item'] = Relationship(back_populates='sub_items', sa_relationship_kwargs={'remote_side': 'Item.id'})
|
parent_item: Optional['Item'] = Relationship(back_populates='sub_items', sa_relationship_kwargs={'remote_side': 'Item.id'})
|
||||||
sub_items: list['Item'] = Relationship(back_populates='parent_item', passive_deletes='all')
|
sub_items: list['Item'] = Relationship(back_populates='parent_item', passive_deletes='all')
|
||||||
|
|
||||||
class ItemDataUpdateRequest(ItemBase):
|
class ItemDataUpdateRequest(ItemBase):
|
||||||
pass
|
type: ItemTypeEnum | None = None
|
||||||
|
"""物品的类型"""
|
||||||
|
|
||||||
|
name: str | None = None
|
||||||
|
"""物品名称"""
|
||||||
|
|
||||||
|
status: ItemStatusEnum | None = None
|
||||||
|
"""物品状态"""
|
||||||
|
|
||||||
class ItemDataResponse(ItemBase):
|
class ItemDataResponse(ItemBase):
|
||||||
expires_at: datetime | None = None
|
expires_at: datetime | None = None
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ from pkg import Password
|
|||||||
default_settings: list[Setting] = [
|
default_settings: list[Setting] = [
|
||||||
Setting(type='string', name='version', value='2.0.0'), # 版本号,用于考虑是否需要数据迁移
|
Setting(type='string', name='version', value='2.0.0'), # 版本号,用于考虑是否需要数据迁移
|
||||||
Setting(type='int', name='jwt_token_exp', value='30'), # JWT Token 访问令牌
|
Setting(type='int', name='jwt_token_exp', value='30'), # JWT Token 访问令牌
|
||||||
|
Setting(type='int', name='mentioned_channel', value='wechat_bot'), # 通知推送通道
|
||||||
Setting(type='string', name='server_chan_key', value=''), # Server 酱推送密钥
|
Setting(type='string', name='server_chan_key', value=''), # Server 酱推送密钥
|
||||||
|
Setting(type='string', name='wechat_bot_key', value=''), # 企业微信机器人推送密钥
|
||||||
]
|
]
|
||||||
|
|
||||||
async def migration(session):
|
async def migration(session):
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from model.base import SQLModelBase
|
||||||
|
|
||||||
|
|
||||||
class DefaultResponse(BaseModel):
|
class DefaultResponse(BaseModel):
|
||||||
code: int = 0
|
code: int = 0
|
||||||
data: dict | list | bool | None
|
data: dict | list | bool | SQLModelBase | None = None
|
||||||
msg: str = ""
|
msg: str = ""
|
||||||
|
|
||||||
# FastAPI 鉴权返回模型
|
# FastAPI 鉴权返回模型
|
||||||
|
|||||||
2
pkg/sender/__init__.py
Normal file
2
pkg/sender/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .wechat_bot import WeChatBot
|
||||||
|
from .server_chan import ServerChatBot
|
||||||
42
pkg/sender/server_chan.py
Normal file
42
pkg/sender/server_chan.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from typing import Literal
|
||||||
|
from loguru import logger
|
||||||
|
from model import Setting
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
from pkg.utils import raise_internal_error, raise_service_unavailable
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
class ServerChatBot:
|
||||||
|
async def get_url(session: AsyncSession):
|
||||||
|
server_chan_key = await Setting.get(session, Setting.name == "server_chan_key")
|
||||||
|
|
||||||
|
if not server_chan_key.value:
|
||||||
|
raise_internal_error("Server酱未配置,请联系管理员")
|
||||||
|
|
||||||
|
url = f"https://sctapi.ftqq.com/{server_chan_key.value}.send"
|
||||||
|
return url
|
||||||
|
|
||||||
|
async def send_text(
|
||||||
|
session: AsyncSession,
|
||||||
|
title: str,
|
||||||
|
description: str,
|
||||||
|
) -> None:
|
||||||
|
"""发送的 Markdown 消息。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session (AsyncSession): 数据库会话
|
||||||
|
title (str): 需要发送的标题
|
||||||
|
description (str): 需要发送的文本消息
|
||||||
|
"""
|
||||||
|
async with aiohttp.ClientSession() as http_session:
|
||||||
|
async with http_session.post(
|
||||||
|
url=await ServerChatBot.get_url(session),
|
||||||
|
data={
|
||||||
|
"title": title,
|
||||||
|
"desp": description
|
||||||
|
}
|
||||||
|
) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
logger.error(f"Failed to send to Server Chan: {response.status}")
|
||||||
|
raise_internal_error("Server酱服务不可用,请稍后再试")
|
||||||
|
else:
|
||||||
|
logger.info("Server Chan message sent successfully")
|
||||||
102
pkg/sender/wechat_bot.py
Normal file
102
pkg/sender/wechat_bot.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
from typing import Literal
|
||||||
|
from loguru import logger
|
||||||
|
from model import Setting
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
from pkg.utils import raise_internal_error, raise_service_unavailable
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
webhook_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send"
|
||||||
|
|
||||||
|
class WeChatBot:
|
||||||
|
async def get_key(session: AsyncSession):
|
||||||
|
key = await Setting.get(session, Setting.name == "wechat_bot_key")
|
||||||
|
|
||||||
|
if not key.value:
|
||||||
|
raise_internal_error("企业微信机器人未配置,请联系管理员")
|
||||||
|
return key.value
|
||||||
|
|
||||||
|
async def send_text(
|
||||||
|
session: AsyncSession,
|
||||||
|
text: str,
|
||||||
|
mentioned_all: bool = False,
|
||||||
|
mentioned_list: list[str] = [],
|
||||||
|
mentioned_mobile_list: list[str] = []
|
||||||
|
) -> None:
|
||||||
|
"""发送文本类型的消息。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session (AsyncSession): 数据库会话
|
||||||
|
text (str): 需要发送的文本消息
|
||||||
|
mentioned_all (bool, optional): 是否提及所有人 Defaults to False.
|
||||||
|
mentioned_list (list[str], optional): 提及的用户列表 Defaults to [].
|
||||||
|
mentioned_mobile_list (list[str], optional): 提及的手机号码列表 Defaults to [].
|
||||||
|
"""
|
||||||
|
key = await WeChatBot.get_key(session)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as http_session:
|
||||||
|
async with http_session.post(
|
||||||
|
url=f"{webhook_url}?key={key}",
|
||||||
|
json={
|
||||||
|
"msgtype": "text",
|
||||||
|
"text": {
|
||||||
|
"content": text
|
||||||
|
},
|
||||||
|
"mentioned_list": ["@all"] if mentioned_all else mentioned_list,
|
||||||
|
"mentioned_mobile_list": ["@all"] if mentioned_all else mentioned_mobile_list
|
||||||
|
}
|
||||||
|
) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
logger.error(f"Failed to send WeChat message: {response.status}")
|
||||||
|
raise_internal_error("企业微信机器人服务不可用,请稍后再试")
|
||||||
|
else:
|
||||||
|
resp_json = await response.json()
|
||||||
|
if resp_json.get("errcode") != 0:
|
||||||
|
logger.error(f"WeChat API error: {resp_json.get('errmsg')}")
|
||||||
|
raise_service_unavailable("发送企业微信消息失败,请稍后再试或联系管理员")
|
||||||
|
else:
|
||||||
|
logger.info("WeChat message sent successfully")
|
||||||
|
|
||||||
|
async def send_markdown(
|
||||||
|
session: AsyncSession,
|
||||||
|
markdown: str,
|
||||||
|
version: Literal['v1', 'v2'],
|
||||||
|
mentioned_all: bool = False,
|
||||||
|
mentioned_list: list[str] = [],
|
||||||
|
mentioned_mobile_list: list[str] = []
|
||||||
|
) -> None:
|
||||||
|
key = await WeChatBot.get_key(session)
|
||||||
|
|
||||||
|
if version == 'v1':
|
||||||
|
payload = {
|
||||||
|
"msgtype": "markdown",
|
||||||
|
"markdown": {
|
||||||
|
"content": markdown,
|
||||||
|
"mentioned_list": ["@all"] if mentioned_all else mentioned_list,
|
||||||
|
"mentioned_mobile_list": ["@all"] if mentioned_all else mentioned_mobile_list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elif version == 'v2':
|
||||||
|
payload = {
|
||||||
|
"msgtype": "markdown_v2",
|
||||||
|
"markdown_v2": {
|
||||||
|
"content": markdown,
|
||||||
|
"mentioned_list": ["@all"] if mentioned_all else mentioned_list,
|
||||||
|
"mentioned_mobile_list": ["@all"] if mentioned_all else mentioned_mobile_list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as http_session:
|
||||||
|
async with http_session.post(
|
||||||
|
url=f"{webhook_url}?key={key}",
|
||||||
|
json=payload
|
||||||
|
) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
logger.error(f"Failed to send WeChat message: {response.status}")
|
||||||
|
raise_internal_error("企业微信机器人服务不可用,请稍后再试")
|
||||||
|
else:
|
||||||
|
resp_json = await response.json()
|
||||||
|
if resp_json.get("errcode") != 0:
|
||||||
|
logger.error(f"WeChat API error: {resp_json.get('errmsg')}")
|
||||||
|
raise_service_unavailable("发送企业微信消息失败,请稍后再试或联系管理员")
|
||||||
|
else:
|
||||||
|
logger.info("WeChat message sent successfully")
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
class SmsBao():
|
|
||||||
async def get
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
from typing import Any, NoReturn, TYPE_CHECKING
|
from typing import Any, NoReturn
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from starlette.status import (
|
from starlette.status import (
|
||||||
HTTP_400_BAD_REQUEST,
|
HTTP_400_BAD_REQUEST,
|
||||||
HTTP_401_UNAUTHORIZED,
|
HTTP_401_UNAUTHORIZED,
|
||||||
|
HTTP_402_PAYMENT_REQUIRED,
|
||||||
HTTP_403_FORBIDDEN,
|
HTTP_403_FORBIDDEN,
|
||||||
HTTP_404_NOT_FOUND,
|
HTTP_404_NOT_FOUND,
|
||||||
HTTP_409_CONFLICT,
|
HTTP_409_CONFLICT,
|
||||||
@@ -12,13 +13,9 @@ from starlette.status import (
|
|||||||
HTTP_500_INTERNAL_SERVER_ERROR,
|
HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
HTTP_501_NOT_IMPLEMENTED,
|
HTTP_501_NOT_IMPLEMENTED,
|
||||||
HTTP_503_SERVICE_UNAVAILABLE,
|
HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
HTTP_504_GATEWAY_TIMEOUT, HTTP_402_PAYMENT_REQUIRED,
|
HTTP_504_GATEWAY_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
||||||
|
|
||||||
|
|
||||||
# --- Request and Response Helpers ---
|
# --- Request and Response Helpers ---
|
||||||
|
|
||||||
def ensure_request_param(to_check: Any, detail: str) -> None:
|
def ensure_request_param(to_check: Any, detail: str) -> None:
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
140
routes/object.py
140
routes/object.py
@@ -1,23 +1,24 @@
|
|||||||
from typing import Annotated, Literal
|
from typing import Annotated
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, Query, HTTPException
|
from fastapi import APIRouter, Request, Query, HTTPException
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from slowapi import Limiter
|
from slowapi import Limiter
|
||||||
from slowapi.util import get_remote_address
|
from slowapi.util import get_remote_address
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from dependencies import SessionDep
|
||||||
from middleware.user import get_current_user
|
from middleware.user import get_current_user
|
||||||
from model import DefaultResponse, ItemDataResponse, User, database, Setting, Item
|
from model import DefaultResponse, ItemDataResponse, User, database, Setting, Item
|
||||||
from model.item import ItemDataUpdateRequest
|
from model.item import ItemDataUpdateRequest, ItemTypeEnum
|
||||||
from pkg.utils import raise_not_found, raise_bad_request, raise_internal_error, raise_service_unavailable
|
from pkg.sender import ServerChatBot, WeChatBot
|
||||||
|
from pkg.utils import raise_not_found, raise_bad_request, raise_internal_error
|
||||||
|
|
||||||
|
from starlette.status import HTTP_204_NO_CONTENT
|
||||||
|
|
||||||
limiter = Limiter(key_func=get_remote_address)
|
limiter = Limiter(key_func=get_remote_address)
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
Router = APIRouter(prefix='/api/object', tags=['物品 Object'])
|
Router = APIRouter(prefix='/api/object', tags=['物品 Object'])
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ async def get_items(
|
|||||||
name=obj.name,
|
name=obj.name,
|
||||||
icon=obj.icon or "",
|
icon=obj.icon or "",
|
||||||
status=obj.status or "",
|
status=obj.status or "",
|
||||||
phone=int(obj.phone) if obj.phone and obj.phone.isdigit() else 0,
|
phone=obj.phone if obj.phone and obj.phone.isdigit() else None,
|
||||||
lost_description=obj.description,
|
lost_description=obj.description,
|
||||||
find_ip=obj.find_ip,
|
find_ip=obj.find_ip,
|
||||||
create_time=obj.created_at.isoformat(),
|
create_time=obj.created_at.isoformat(),
|
||||||
@@ -73,53 +74,48 @@ async def get_items(
|
|||||||
path='/items',
|
path='/items',
|
||||||
summary='添加物品信息',
|
summary='添加物品信息',
|
||||||
description='添加新的物品信息',
|
description='添加新的物品信息',
|
||||||
response_model=DefaultResponse,
|
status_code=HTTP_204_NO_CONTENT,
|
||||||
response_description='添加物品成功'
|
response_description='添加物品成功'
|
||||||
)
|
)
|
||||||
async def add_items(
|
async def add_items(
|
||||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||||
user: Annotated[User, Depends(get_current_user)],
|
user: Annotated[User, Depends(get_current_user)],
|
||||||
request: ItemDataUpdateRequest
|
request: ItemDataUpdateRequest
|
||||||
) -> DefaultResponse:
|
):
|
||||||
"""
|
"""
|
||||||
添加物品信息。
|
添加物品信息。
|
||||||
|
|
||||||
- **key**: 物品的关键字
|
|
||||||
- **type**: 物品的类型
|
|
||||||
- **name**: 物品的名称
|
|
||||||
- **icon**: 物品的图标
|
|
||||||
- **phone**: 联系电话
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建新物品对象,关联当前用户
|
# 创建新物品对象,关联当前用户
|
||||||
await Item.add(session, Item.model_validate(request))
|
request_dict = request.model_dump()
|
||||||
|
request_dict['user'] = user
|
||||||
|
request_dict['user_id'] = user.id
|
||||||
|
|
||||||
|
await Item.add(session, Item.model_validate(request_dict))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to add item: {e}")
|
logger.error(f"Failed to add item: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
else:
|
|
||||||
return DefaultResponse(data=True)
|
|
||||||
|
|
||||||
@Router.patch(
|
@Router.patch(
|
||||||
path='/items/{item_id}',
|
path='/items/{item_id}',
|
||||||
summary='更新物品信息',
|
summary='更新物品信息',
|
||||||
description='更新现有物品的信息',
|
description='更新现有物品的信息',
|
||||||
response_model=DefaultResponse,
|
status_code=HTTP_204_NO_CONTENT,
|
||||||
response_description='更新物品成功'
|
response_description='更新物品成功'
|
||||||
)
|
)
|
||||||
async def update_items(
|
async def update_items(
|
||||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||||
user: Annotated[User, Depends(get_current_user)],
|
user: Annotated[User, Depends(get_current_user)],
|
||||||
item_id: UUID,
|
item_id: UUID,
|
||||||
request: ItemDataUpdateRequest
|
request: ItemDataUpdateRequest,
|
||||||
) -> DefaultResponse:
|
):
|
||||||
"""
|
"""
|
||||||
更新物品信息。
|
更新物品信息。
|
||||||
|
|
||||||
只有 `id` 是必填参数,其余参数都是可选的,在不传入任何值的时候将不做任何更改。
|
只有 `id` 是必填参数,其余参数都是可选的,在不传入任何值的时候将不做任何更改。
|
||||||
|
|
||||||
- **id**: 物品的ID
|
- **id**: 物品的ID
|
||||||
- **key**: 物品的序列号 **不建议修改此项,这样会导致生成的物品二维码直接失效**
|
- **key**: 物品的序列号
|
||||||
- **name**: 物品的名称
|
- **name**: 物品的名称
|
||||||
- **icon**: 物品的图标
|
- **icon**: 物品的图标
|
||||||
- **status**: 物品的状态
|
- **status**: 物品的状态
|
||||||
@@ -134,37 +130,33 @@ async def update_items(
|
|||||||
if not obj:
|
if not obj:
|
||||||
raise_not_found("Item not found or access denied")
|
raise_not_found("Item not found or access denied")
|
||||||
|
|
||||||
await obj.update(session, request)
|
await obj.update(session, request, exclude_unset=True)
|
||||||
|
|
||||||
return DefaultResponse(data=True)
|
|
||||||
|
|
||||||
@Router.delete(
|
@Router.delete(
|
||||||
path='/items',
|
path='/items/{item_id}',
|
||||||
summary='删除物品信息',
|
summary='删除物品信息',
|
||||||
description='删除指定的物品信息',
|
description='删除指定的物品信息',
|
||||||
response_model=DefaultResponse,
|
status_code=HTTP_204_NO_CONTENT,
|
||||||
response_description='删除物品成功'
|
response_description='删除物品成功'
|
||||||
)
|
)
|
||||||
async def delete_items(
|
async def delete_items(
|
||||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||||
user: Annotated[User, Depends(get_current_user)],
|
user: Annotated[User, Depends(get_current_user)],
|
||||||
id: int
|
item_id: UUID
|
||||||
) -> DefaultResponse:
|
):
|
||||||
"""
|
"""
|
||||||
删除物品信息。
|
删除物品信息。
|
||||||
|
|
||||||
- **id**: 物品的ID
|
- **id**: 物品的ID
|
||||||
"""
|
"""
|
||||||
# 获取现有物品,验证归属权
|
# 获取现有物品,验证归属权
|
||||||
obj = await Item.get(session, (Item.id == id) & (Item.user_id == user.id))
|
obj = await Item.get(session, (Item.id == item_id) & (Item.user_id == user.id))
|
||||||
if not obj:
|
if not obj:
|
||||||
raise_not_found("Item not found or access denied")
|
raise_not_found("Item not found or access denied")
|
||||||
await Item.delete(session, obj)
|
await Item.delete(session, obj)
|
||||||
|
|
||||||
return DefaultResponse(data=True)
|
|
||||||
|
|
||||||
@Router.get(
|
@Router.get(
|
||||||
path='/{item_key}',
|
path='/{item_id}',
|
||||||
summary="获取物品信息",
|
summary="获取物品信息",
|
||||||
description="根据物品键获取物品信息",
|
description="根据物品键获取物品信息",
|
||||||
response_model=DefaultResponse,
|
response_model=DefaultResponse,
|
||||||
@@ -172,13 +164,13 @@ async def delete_items(
|
|||||||
)
|
)
|
||||||
async def get_object(
|
async def get_object(
|
||||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
||||||
item_key: str,
|
item_id: UUID,
|
||||||
request: Request
|
request: Request
|
||||||
) -> DefaultResponse:
|
) -> DefaultResponse:
|
||||||
"""
|
"""
|
||||||
获取物品信息 / Get object information
|
获取物品信息 / Get object information
|
||||||
"""
|
"""
|
||||||
object_data = await Item.get(session, Item.key == item_key)
|
object_data = await Item.get(session, Item.id == item_id)
|
||||||
|
|
||||||
if object_data:
|
if object_data:
|
||||||
if object_data.status == 'lost':
|
if object_data.status == 'lost':
|
||||||
@@ -186,78 +178,64 @@ async def get_object(
|
|||||||
object_data.find_ip = str(request.client.host)
|
object_data.find_ip = str(request.client.host)
|
||||||
object_data = await object_data.save(session)
|
object_data = await object_data.save(session)
|
||||||
|
|
||||||
return DefaultResponse(data=ItemDataResponse.model_validate(object_data))
|
data = ItemDataResponse.model_validate(object_data)
|
||||||
|
|
||||||
|
return DefaultResponse(data=data.model_dump())
|
||||||
else:
|
else:
|
||||||
raise_not_found('物品不存在或出现异常')
|
raise_not_found('物品不存在或出现异常')
|
||||||
|
|
||||||
@Router.put(
|
@Router.post(
|
||||||
path='/{item_id}',
|
path='/{item_id}/notify_move_car',
|
||||||
summary="通知车主进行挪车",
|
summary="通知车主进行挪车",
|
||||||
description="向车主发送挪车通知",
|
description="向车主发送挪车通知",
|
||||||
response_model=DefaultResponse,
|
status_code=HTTP_204_NO_CONTENT,
|
||||||
response_description="挪车通知结果"
|
response_description="挪车通知结果"
|
||||||
)
|
)
|
||||||
@limiter.limit(
|
|
||||||
limit_value="1/5minute", # 每5分钟允许1次请求
|
|
||||||
error_message="小主已经通知过车主了,请稍安勿躁~"
|
|
||||||
)
|
|
||||||
async def notify_move_car(
|
async def notify_move_car(
|
||||||
request: Request,
|
request: Request,
|
||||||
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
|
session: SessionDep,
|
||||||
item_id: int,
|
item_id: UUID,
|
||||||
phone: str = None,
|
phone: str = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
通知车主进行挪车 / Notify car owner to move the car
|
通知车主进行挪车 / Notify car owner to move the car
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
request (Request): ...
|
||||||
|
session (AsyncSession): 数据库会话 / Database session
|
||||||
item_id (int): 物品ID / Item ID
|
item_id (int): 物品ID / Item ID
|
||||||
phone (str): 挪车发起者电话 / Phone number of the person initiating the move. Defaults to None.
|
phone (str): 挪车发起者电话 / Phone number of the person initiating the move. Defaults to None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 检查是否存在该物品
|
# 检查是否存在该物品
|
||||||
object_data = await Item.get(session, Item.id == item_id)
|
item_data = await Item.get_exist_one(session=session, id=item_id)
|
||||||
if not object_data:
|
|
||||||
raise_not_found()
|
|
||||||
|
|
||||||
# 检查物品类型是否为车辆
|
# 检查物品类型是否为车辆
|
||||||
if object_data.type != 'car':
|
if item_data.type != ItemTypeEnum.car:
|
||||||
raise_bad_request("Item is not car")
|
raise_bad_request("Item is not car")
|
||||||
|
|
||||||
# 发起挪车通知(目前仅适配Server酱)
|
# 发起挪车通知
|
||||||
server_chan_key = await Setting.get(session, Setting.name == 'server_chan_key')
|
server_chan_key = await Setting.get(session, Setting.name == 'server_chan_key')
|
||||||
if not server_chan_key:
|
wechat_bot_key = await Setting.get(session, Setting.name == 'wechat_bot_key')
|
||||||
|
if not (server_chan_key.value or wechat_bot_key.value):
|
||||||
raise_internal_error('未配置Server酱,无法发送挪车通知')
|
raise_internal_error('未配置Server酱,无法发送挪车通知')
|
||||||
|
|
||||||
title = "挪车通知 - Findreve"
|
title = "挪车通知 - Findreve"
|
||||||
description = f"您的车辆“{object_data.name}”被请求挪车。\n\n"
|
description = f"""您的车辆“{item_data.name}”被请求挪车。
|
||||||
if phone:
|
{f"请求挪车者电话:[{phone}](tel:{phone})" if phone else ""}
|
||||||
description += f"请求挪车者电话:[{phone}](tel:{phone})\n\n"
|
请尽快联系请求者并挪车。"""
|
||||||
description += "请尽快联系请求者并挪车。"
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
# 获取通知的方式
|
||||||
async with session.post(
|
mentioned_channel = (await Setting.get(session, Setting.name == 'mentioned_channel')).value
|
||||||
url=f"https://sctapi.ftqq.com/{server_chan_key.value}.send",
|
|
||||||
data={
|
if mentioned_channel == 'server_chan':
|
||||||
"title": title,
|
await ServerChatBot.send_text(
|
||||||
"desp": description
|
session=session,
|
||||||
}
|
title=title,
|
||||||
) as resp:
|
description=description
|
||||||
if resp.status == 200:
|
|
||||||
resp_json = await resp.json()
|
|
||||||
if resp_json.get('code') == 0:
|
|
||||||
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')}, "
|
|
||||||
f"error_message={error_msg}, item_id={item_id}, response={resp_json}"
|
|
||||||
)
|
)
|
||||||
raise_service_unavailable('Server酱出现问题,发送失败')
|
elif mentioned_channel == 'wechat_bot':
|
||||||
else:
|
await WeChatBot.send_markdown(
|
||||||
response_text = await resp.text()
|
session=session,
|
||||||
logger.error(
|
markdown=f"# {title}\n\n{description}",
|
||||||
f"Failed to send notification via Server Chan: http_status={resp.status}, item_id={item_id}, "
|
version='v1'
|
||||||
f"response_body={response_text}, url={resp.url}"
|
|
||||||
)
|
)
|
||||||
raise_internal_error('挪车通知发送失败')
|
|
||||||
|
|||||||
Reference in New Issue
Block a user