迁移服务层
This commit is contained in:
13
services/__init__.py
Normal file
13
services/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
服务层模块聚合。
|
||||
"""
|
||||
|
||||
from . import admin, object, session, site # noqa: F401
|
||||
|
||||
|
||||
__all__ = [
|
||||
"admin",
|
||||
"object",
|
||||
"session",
|
||||
"site",
|
||||
]
|
||||
52
services/admin.py
Normal file
52
services/admin.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
管理员相关业务逻辑。
|
||||
"""
|
||||
|
||||
from typing import Iterable, List
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from model import Setting
|
||||
from model import SettingResponse
|
||||
|
||||
|
||||
async def fetch_settings(
|
||||
session: AsyncSession,
|
||||
name: str | None = None,
|
||||
) -> List[SettingResponse]:
|
||||
"""
|
||||
按名称获取设置项,默认返回全部。
|
||||
"""
|
||||
data: list[SettingResponse] = []
|
||||
|
||||
if name:
|
||||
setting = await Setting.get(session, Setting.name == name)
|
||||
if setting:
|
||||
data.append(SettingResponse.model_validate(setting))
|
||||
else:
|
||||
raise HTTPException(404, detail="Setting not found")
|
||||
else:
|
||||
settings: Iterable[Setting] | None = await Setting.get(session, fetch_mode="all")
|
||||
if settings:
|
||||
data = [SettingResponse.model_validate(s) for s in settings]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def update_setting_value(
|
||||
session: AsyncSession,
|
||||
name: str,
|
||||
value: str,
|
||||
) -> bool:
|
||||
"""
|
||||
更新设置项的值。
|
||||
"""
|
||||
setting = await Setting.get(session, Setting.name == name)
|
||||
if not setting:
|
||||
raise HTTPException(404, detail="Setting not found")
|
||||
|
||||
setting.value = value
|
||||
await Setting.save(session)
|
||||
|
||||
return True
|
||||
164
services/object.py
Normal file
164
services/object.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
物品相关业务逻辑。
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from model import Item, ItemDataResponse, Setting, User
|
||||
from model.item import ItemDataUpdateRequest, ItemTypeEnum
|
||||
from pkg.sender import ServerChatBot, WeChatBot
|
||||
from pkg.utils import raise_bad_request, raise_internal_error, raise_not_found
|
||||
from starlette.status import HTTP_204_NO_CONTENT
|
||||
|
||||
|
||||
async def list_items(
|
||||
session: AsyncSession,
|
||||
user: User,
|
||||
item_id: int | None = None,
|
||||
key: str | None = None,
|
||||
) -> List[Item]:
|
||||
"""
|
||||
根据条件获取当前用户的物品列表。
|
||||
"""
|
||||
if item_id is not None:
|
||||
results = await Item.get(session, (Item.id == item_id) & (Item.user_id == user.id))
|
||||
results = [results] if results else []
|
||||
elif key is not None:
|
||||
results = await Item.get(session, (Item.key == key) & (Item.user_id == user.id))
|
||||
results = [results] if results else []
|
||||
else:
|
||||
results = await Item.get(session, Item.user_id == user.id, fetch_mode="all")
|
||||
|
||||
if not results:
|
||||
return []
|
||||
|
||||
items: list[Item] = []
|
||||
for obj in results:
|
||||
items.append(
|
||||
Item(
|
||||
id=obj.id,
|
||||
type=obj.type,
|
||||
key=obj.id,
|
||||
name=obj.name,
|
||||
icon=obj.icon or "",
|
||||
status=obj.status or "",
|
||||
phone=obj.phone if obj.phone and obj.phone.isdigit() else None,
|
||||
lost_description=obj.description,
|
||||
find_ip=obj.find_ip,
|
||||
create_time=obj.created_at.isoformat(),
|
||||
lost_time=obj.lost_at.isoformat() if obj.lost_at else None,
|
||||
)
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
async def create_item(
|
||||
session: AsyncSession,
|
||||
user: User,
|
||||
request: ItemDataUpdateRequest,
|
||||
) -> None:
|
||||
"""
|
||||
创建新的物品信息。
|
||||
"""
|
||||
try:
|
||||
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 exc: # noqa: BLE001
|
||||
logger.error(f"Failed to add item: {exc}")
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
async def update_item(
|
||||
session: AsyncSession,
|
||||
user: User,
|
||||
item_id: UUID,
|
||||
request: ItemDataUpdateRequest,
|
||||
) -> None:
|
||||
"""
|
||||
更新物品信息。
|
||||
"""
|
||||
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, exclude_unset=True)
|
||||
|
||||
|
||||
async def delete_item(
|
||||
session: AsyncSession,
|
||||
user: User,
|
||||
item_id: UUID,
|
||||
) -> None:
|
||||
"""
|
||||
删除指定物品。
|
||||
"""
|
||||
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 Item.delete(session, obj)
|
||||
|
||||
|
||||
async def retrieve_object(
|
||||
session: AsyncSession,
|
||||
item_id: UUID,
|
||||
client_host: str,
|
||||
) -> ItemDataResponse:
|
||||
"""
|
||||
根据物品 ID 获取物品信息并视情况更新寻找者 IP。
|
||||
"""
|
||||
object_data = await Item.get(session, Item.id == item_id)
|
||||
|
||||
if not object_data:
|
||||
raise_not_found("物品不存在或出现异常")
|
||||
|
||||
if object_data.status == "lost":
|
||||
object_data.find_ip = client_host
|
||||
object_data = await object_data.save(session)
|
||||
|
||||
return ItemDataResponse.model_validate(object_data)
|
||||
|
||||
|
||||
async def notify_move_car(
|
||||
session: AsyncSession,
|
||||
item_id: UUID,
|
||||
phone: str | None = None,
|
||||
) -> int:
|
||||
"""
|
||||
向车主发送挪车通知。
|
||||
"""
|
||||
item_data = await Item.get_exist_one(session=session, id=item_id)
|
||||
|
||||
if item_data.type != ItemTypeEnum.car:
|
||||
raise_bad_request("Item is not car")
|
||||
|
||||
server_chan_key = await Setting.get(session, Setting.name == "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酱,无法发送挪车通知")
|
||||
|
||||
title = "挪车通知 - Findreve"
|
||||
description = (
|
||||
f"您的车辆“{item_data.name}”被请求挪车。\n"
|
||||
f"{f'请求挪车者电话:[{phone}](tel:{phone})' if phone else ''}\n"
|
||||
"请尽快联系请求者并挪车。"
|
||||
)
|
||||
|
||||
mentioned_channel = (await Setting.get(session, Setting.name == "mentioned_channel")).value
|
||||
|
||||
if mentioned_channel == "server_chan":
|
||||
await ServerChatBot.send_text(session=session, title=title, description=description)
|
||||
elif mentioned_channel == "wechat_bot":
|
||||
await WeChatBot.send_markdown(
|
||||
session=session,
|
||||
markdown=f"# {title}\n\n{description}",
|
||||
version="v1",
|
||||
)
|
||||
|
||||
return HTTP_204_NO_CONTENT
|
||||
74
services/session.py
Normal file
74
services/session.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
会话服务,负责处理登录与令牌生成逻辑。
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
import JWT
|
||||
import jwt
|
||||
from loguru import logger
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from model import Setting, User
|
||||
from model.response import TokenResponse
|
||||
from pkg import Password
|
||||
|
||||
|
||||
async def create_access_token(
|
||||
session: AsyncSession,
|
||||
data: dict[str, Any],
|
||||
expires_delta: timedelta | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
创建访问令牌。
|
||||
"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
jwt_exp_setting = await Setting.get(session, Setting.name == "jwt_token_exp")
|
||||
expire = datetime.now(timezone.utc) + timedelta(int(jwt_exp_setting.value))
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, key=await JWT.get_secret_key(), algorithm="HS256")
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
async def authenticate_user(
|
||||
session: AsyncSession,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> User | None:
|
||||
"""
|
||||
验证用户名和密码,返回认证后的用户。
|
||||
"""
|
||||
account = await User.get(session, User.email == username)
|
||||
|
||||
if not account:
|
||||
logger.error("Account or password not set in settings.")
|
||||
return None
|
||||
|
||||
if account.email != username or not Password.verify(account.password, password):
|
||||
logger.error("Invalid username or password.")
|
||||
return None
|
||||
|
||||
return account
|
||||
|
||||
|
||||
async def login_for_access_token(
|
||||
session: AsyncSession,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> TokenResponse | None:
|
||||
"""
|
||||
登录并生成访问令牌。
|
||||
"""
|
||||
user = await authenticate_user(session=session, username=username, password=password)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
access_token = await create_access_token(
|
||||
session=session,
|
||||
data={"sub": user.email},
|
||||
)
|
||||
return TokenResponse(access_token=access_token)
|
||||
12
services/site.py
Normal file
12
services/site.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
站点信息服务。
|
||||
"""
|
||||
|
||||
from pkg import conf
|
||||
|
||||
|
||||
async def get_version() -> str:
|
||||
"""
|
||||
返回站点版本信息。
|
||||
"""
|
||||
return conf.VERSION
|
||||
Reference in New Issue
Block a user