迁移服务层

This commit is contained in:
2025-10-10 18:11:36 +08:00
parent a71cde7b82
commit 93830c3d03
13 changed files with 437 additions and 188 deletions

View File

@@ -1,12 +1,12 @@
from typing import Annotated
from fastapi import APIRouter, HTTPException
from fastapi import Depends
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from middleware.admin import is_admin
from model import database, Setting, SettingResponse
from model import database
from model.response import DefaultResponse
from services import admin as admin_service
Router = APIRouter(
prefix='/api/admin',
@@ -41,19 +41,7 @@ async def get_settings(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
name: str | None = None
) -> DefaultResponse:
data = []
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 = await Setting.get(session, fetch_mode="all")
if settings:
data = [SettingResponse.model_validate(s) for s in settings]
data = await admin_service.fetch_settings(session=session, name=name)
return DefaultResponse(data=data)
@@ -69,11 +57,5 @@ async def update_settings(
name: str,
value: str
) -> DefaultResponse:
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 DefaultResponse(data=True)
result = await admin_service.update_setting_value(session=session, name=name, value=value)
return DefaultResponse(data=result)

View File

@@ -1,25 +1,20 @@
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Request, Query, HTTPException
from loguru import logger
from fastapi import APIRouter, Depends, Query, Request
from slowapi import Limiter
from slowapi.util import get_remote_address
from sqlalchemy.ext.asyncio import AsyncSession
from dependencies import SessionDep
from middleware.user import get_current_user
from model import DefaultResponse, ItemDataResponse, User, database, Setting, Item
from model.item import ItemDataUpdateRequest, ItemTypeEnum
from pkg.sender import ServerChatBot, WeChatBot
from pkg.utils import raise_not_found, raise_bad_request, raise_internal_error
from model import DefaultResponse, User, database
from model.item import ItemDataUpdateRequest
from services import object as object_service
from starlette.status import HTTP_204_NO_CONTENT
limiter = Limiter(key_func=get_remote_address)
from fastapi import Depends
Router = APIRouter(prefix='/api/object', tags=['物品 Object'])
@Router.get(
@@ -39,36 +34,13 @@ async def get_items(
不传参数返回所有信息,否则可传入 `id` 或 `key` 进行筛选。
"""
# 根据条件查询物品,只获取当前用户的物品
if id is not None:
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 Item.get(session, (Item.key == key) & (Item.user_id == token.id))
results = [results] if results else []
else:
results = await Item.get(session, Item.user_id == token.id, fetch_mode="all")
if results:
items = []
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 DefaultResponse(data=items)
else:
return DefaultResponse(data=[])
items = await object_service.list_items(
session=session,
user=token,
item_id=id,
key=key,
)
return DefaultResponse(data=items)
@Router.post(
path='/items',
@@ -85,16 +57,11 @@ async def add_items(
"""
添加物品信息。
"""
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 e:
logger.error(f"Failed to add item: {e}")
raise HTTPException(status_code=500, detail=str(e))
await object_service.create_item(
session=session,
user=user,
request=request,
)
@Router.patch(
path='/items/{item_id}',
@@ -123,14 +90,14 @@ async def update_items(
- **lost_description**: 物品丢失描述
- **find_ip**: 找到物品的IP
- **lost_time**: 物品丢失时间
"""
# 获取现有物品,验证归属权
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)
await object_service.update_item(
session=session,
user=user,
item_id=item_id,
request=request,
)
@Router.delete(
path='/items/{item_id}',
@@ -146,14 +113,13 @@ async def delete_items(
):
"""
删除物品信息。
- **id**: 物品的ID
"""
# 获取现有物品,验证归属权
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)
await object_service.delete_item(
session=session,
user=user,
item_id=item_id,
)
@Router.get(
path='/{item_id}',
@@ -170,19 +136,12 @@ async def get_object(
"""
获取物品信息 / Get object information
"""
object_data = await Item.get(session, Item.id == item_id)
if object_data:
if object_data.status == 'lost':
# 物品已标记为丢失更新IP地址
object_data.find_ip = str(request.client.host)
object_data = await object_data.save(session)
data = ItemDataResponse.model_validate(object_data)
return DefaultResponse(data=data.model_dump())
else:
raise_not_found('物品不存在或出现异常')
data = await object_service.retrieve_object(
session=session,
item_id=item_id,
client_host=str(request.client.host),
)
return DefaultResponse(data=data.model_dump())
@Router.post(
path='/{item_id}/notify_move_car',
@@ -192,7 +151,7 @@ async def get_object(
response_description="挪车通知结果"
)
async def notify_move_car(
request: Request,
_request: Request,
session: SessionDep,
item_id: UUID,
phone: str = None,
@@ -201,41 +160,13 @@ async def notify_move_car(
通知车主进行挪车 / Notify car owner to move the car
Args:
request (Request): ...
_request (Request): ...
session (AsyncSession): 数据库会话 / Database session
item_id (int): 物品ID / Item ID
phone (str): 挪车发起者电话 / Phone number of the person initiating the move. Defaults to None.
"""
# 检查是否存在该物品
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}”被请求挪车。
{f"请求挪车者电话:[{phone}](tel:{phone})" if phone else ""}
请尽快联系请求者并挪车。"""
# 获取通知的方式
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'
)
await object_service.notify_move_car(
session=session,
item_id=item_id,
phone=phone,
)

View File

@@ -1,50 +1,16 @@
# 导入库
from typing import Annotated
from datetime import datetime, timedelta, timezone
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from fastapi import APIRouter
import jwt, JWT
from sqlmodel.ext.asyncio.session import AsyncSession
from pkg import Password
from loguru import logger
from model import Setting, User, database
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel.ext.asyncio.session import AsyncSession
from model import database
from model.response import TokenResponse
from services import session as session_service
Router = APIRouter(tags=["令牌 session"])
# 创建访问令牌
async def create_access_token(
session: AsyncSession,
data: dict,
expires_delta: timedelta | None = None
):
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):
# 验证账号和密码
account = await User.get(session, User.email == username)
if not account:
logger.error("Account or password not set in settings.")
return False
if account.email != username or not Password.verify(account.password, password):
logger.error("Invalid username or password.")
return False
return account
# FastAPI 登录路由 / FastAPI login route
@Router.post(
path="/api/token",
@@ -57,22 +23,16 @@ async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
) -> TokenResponse:
user = await authenticate_user(
session=session,
username=form_data.username,
password=form_data.password
token_response = await session_service.login_for_access_token(
session=session,
username=form_data.username,
password=form_data.password,
)
if not user:
if not token_response:
raise HTTPException(
status_code=401,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = await create_access_token(
session=session,
data={"sub": user.email},
)
return TokenResponse(
access_token=access_token,
)
return token_response

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter
from model.response import DefaultResponse
from pkg import conf
from services import site as site_service
Router = APIRouter(prefix='/api/site', tags=['站点 Site'])
@@ -17,4 +17,5 @@ async def ping():
:return: Findreve 版本号
"""
return DefaultResponse(data=conf.VERSION)
version = await site_service.get_version()
return DefaultResponse(data=version)