修复数据库调用问题

This commit is contained in:
2025-10-05 12:41:33 +08:00
parent c1c36c606f
commit ee684d67cf
11 changed files with 422 additions and 282 deletions

8
app.py
View File

@@ -2,6 +2,9 @@ from fastapi import FastAPI
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi import Request, HTTPException from fastapi import Request, HTTPException
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from routes import (session, admin, object) from routes import (session, admin, object)
import model.database import model.database
import os, asyncio import os, asyncio
@@ -30,6 +33,11 @@ app.include_router(admin.Router)
app.include_router(session.Router) app.include_router(session.Router)
app.include_router(object.Router) app.include_router(object.Router)
# 挂载Slowapi限流中间件
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.get("/") @app.get("/")
def read_root(): def read_root():
if not os.path.exists("dist/index.html"): if not os.path.exists("dist/index.html"):

34
middleware/admin.py Normal file
View File

@@ -0,0 +1,34 @@
from typing import Annotated, Literal
from fastapi import Depends
from fastapi import HTTPException
import JWT
import jwt
from jwt import InvalidTokenError
from model import database
from sqlmodel.ext.asyncio.session import AsyncSession
from model import User
from .user import get_current_user
# 验证是否为管理员
async def is_admin(
token: Annotated[str, Depends(get_current_user)],
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
) -> Literal[True]:
'''
验证是否为管理员。
使用方法:
>>> APIRouter(dependencies=[Depends(is_admin)])
'''
not_admin_exception = HTTPException(
status_code=403,
detail="Admin access required",
headers={"WWW-Authenticate": "Bearer"},
)
user = await get_current_user(token, session)
if not user.is_admin:
raise not_admin_exception
else:
return True

33
middleware/user.py Normal file
View File

@@ -0,0 +1,33 @@
from typing import Annotated, Literal
from fastapi import Depends
from fastapi import HTTPException
import JWT
import jwt
from jwt import InvalidTokenError
from model import database
from sqlmodel.ext.asyncio.session import AsyncSession
from model import User
# 验证是否为管理员
async def get_current_user(
token: Annotated[str, Depends(JWT.oauth2_scheme)],
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
) -> User:
'''
验证用户身份并返回当前用户信息。
'''
not_login_exception = HTTPException(
status_code=401,
detail="Login required",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, await JWT.get_secret_key(), algorithms=[JWT.ALGORITHM])
username = payload.get("sub")
stored_account = await User.get(session, User.email == username)
if username is None or not stored_account.email == username:
raise not_login_exception
return stored_account
except InvalidTokenError:
raise not_login_exception

View File

@@ -1,3 +1,4 @@
from . import token from . import token
from .setting import Setting from .setting import Setting
from .object import Object from .object import Object
from .user import User

View File

@@ -1,12 +1,13 @@
from loguru import logger from loguru import logger
from sqlmodel import select from sqlmodel import select
from .setting import Setting from .setting import Setting
from .user import User
from pkg import Password from pkg import Password
default_settings: list[Setting] = [ default_settings: list[Setting] = [
Setting(type='string', name='version', value='1.0.0'), Setting(type='string', name='version', value='1.0.0'),
Setting(type='int', name='ver', value='1'), Setting(type='int', name='jwt_token_exp', value='30'),
Setting(type='string', name='account', value='admin@yuxiaoqiu.cn'), Setting(type='string', name='server_chan_key', value=''),
] ]
async def migration(session): async def migration(session):
@@ -17,20 +18,45 @@ async def migration(session):
# 已有数据,说明不是第一次运行,直接返回 # 已有数据,说明不是第一次运行,直接返回
return return
# 生成初始密码与密钥
admin_password = Password.generate()
logger.warning(f"密码(请牢记,后续不再显示): {admin_password}")
settings.append(Setting(type='string', name='password', value=Password.hash(admin_password)))
settings.append(Setting(type='string', name='SECRET_KEY', value=Password.generate(64))) settings.append(Setting(type='string', name='SECRET_KEY', value=Password.generate(64)))
# 读取库里已存在的 name避免主键冲突 # 读取库里已存在的 name避免主键冲突
names = [s.name for s in settings] names = [s.name for s in settings]
exist_stmt = select(Setting.name).where(Setting.name.in_(names)) existed_settings = await Setting.get(
exist_rs = await session.exec(exist_stmt) session,
existed: set[str] = set(exist_rs.all()) Setting.name.in_(names),
fetch_mode="all"
)
existed: set[str] = {s.name for s in (existed_settings or [])}
to_insert = [s for s in settings if s.name not in existed] to_insert = [s for s in settings if s.name not in existed]
if to_insert: if to_insert:
# 使用你写好的通用新增方法(是类方法),并传入会话
await Setting.add(session, to_insert, refresh=False) await Setting.add(session, to_insert, refresh=False)
if await User.get(session, User.id == 1):
# 已有超级管理员用户,说明不是第一次运行
# 修复数据库id为1的用户不是管理员的问题
admin_user = await User.get(session, User.id == 1)
if admin_user and not admin_user.is_admin:
admin_user.is_admin = True
await User.update(session, admin_user, refresh=False)
# 已有用户,直接返回
return
# 生成初始密码与密钥
admin_password = Password.generate()
logger.warning("当前无管理员用户,已自动创建初始管理员用户:")
logger.warning("邮箱: admin@yxqi.cn")
logger.warning(f"密码: {admin_password}")
admin_user = User(
id=1,
email='admin@yxqi.cn',
username='Admin',
password=Password.hash(admin_password),
is_admin=True
)
await User.add(session, admin_user, refresh=False)

View File

@@ -1,28 +1,45 @@
from typing import Literal from typing import Literal, TYPE_CHECKING
from sqlmodel import Field, Column, String, DateTime from sqlmodel import Field, Column, String, DateTime, Relationship
from .base import TableBase, IdMixin from .base import TableBase, IdMixin
from datetime import datetime from datetime import datetime
if TYPE_CHECKING:
from .user import User
class Object(IdMixin, TableBase, table=True): class Object(IdMixin, TableBase, table=True):
key: str = Field(index=True, nullable=False, description="物品外部ID") user_id: int = Field(foreign_key="user.id", index=True, nullable=False, description="所属用户ID")
key: str = Field(index=True, nullable=False, unique=True, description="物品外部ID")
type: Literal['normal', 'car'] = Field( type: Literal['normal', 'car'] = Field(
default='normal', default='normal',
description="物品类型", description="物品类型",
sa_column=Column(String, default='normal', nullable=False) sa_column=Column(
String,
default='normal',
nullable=False
)
) )
name: str = Field(nullable=False, description="物品名称") name: str = Field(nullable=False, description="物品名称")
icon: str | None = Field(default=None, description="物品图标") icon: str | None = Field(default=None, description="物品图标")
status: Literal['ok', 'lost'] = Field( status: Literal['ok', 'lost'] = Field(
default='ok', default='ok',
description="物品状态", description="物品状态",
sa_column=Column(String, default='ok', nullable=False) sa_column=Column(
String,
default='ok',
nullable=False
)
) )
phone: str | None = Field(default=None, description="联系电话") phone: str | None = Field(default=None, description="联系电话")
context: str | None = Field(default=None, description="物品描述") description: str | None = Field(default=None, description="物品描述")
find_ip: str | None = Field(default=None, description="最后一次发现的IP地址") find_ip: str | None = Field(default=None, description="最后一次发现的IP地址")
lost_at: datetime | None = Field( lost_at: datetime | None = Field(
default=None, default=None,
description="物品标记为丢失的时间", description="物品标记为丢失的时间",
sa_column=Column(DateTime, nullable=True) sa_column=Column(
DateTime,
nullable=True
) )
)
user: "User" = Relationship(back_populates="objects")

16
model/user.py Normal file
View File

@@ -0,0 +1,16 @@
from typing import TYPE_CHECKING
from sqlmodel import Field, Column, String, Boolean, Relationship
from .base import TableBase, IdMixin
if TYPE_CHECKING:
from .object import Object
class User(IdMixin, TableBase, table=True):
email: str = Field(sa_column=Column(String(100), index=True, unique=True))
username: str = Field(sa_column=Column(String(50), index=True, unique=True))
password: str = Field(sa_column=Column(String(100)))
is_admin: bool = Field(default=False, sa_column=Column(Boolean, default=False))
objects: list["Object"] = Relationship(back_populates="user")

Binary file not shown.

View File

@@ -1,44 +1,8 @@
from fastapi import APIRouter from fastapi import APIRouter
from typing import Annotated, Literal from fastapi import Depends
from fastapi import Depends, Query
from fastapi import HTTPException
import JWT
import jwt
from jwt import InvalidTokenError
from model import database
from model.response import DefaultResponse from model.response import DefaultResponse
from model.items import Item
from sqlmodel.ext.asyncio.session import AsyncSession
from model import Setting
from model.object import Object
# 验证是否为管理员 from middleware.admin import is_admin
async def is_admin(
token: Annotated[str, Depends(JWT.oauth2_scheme)],
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
) -> Literal[True]:
'''
验证是否为管理员。
使用方法:
>>> APIRouter(dependencies=[Depends(is_admin)])
'''
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, await JWT.get_secret_key(), algorithms=[JWT.ALGORITHM])
username = payload.get("sub")
stored_account = await Setting.get(session, Setting.name == 'account')
if username is None or not stored_account.value == username:
raise credentials_exception
else:
return True
except InvalidTokenError:
raise credentials_exception
Router = APIRouter( Router = APIRouter(
@@ -62,184 +26,3 @@ async def verity_admin() -> DefaultResponse:
- 若不是管理员,抛出 `401` 错误 - 若不是管理员,抛出 `401` 错误
''' '''
return DefaultResponse(data=True) return DefaultResponse(data=True)
@Router.get(
path='/items',
summary='获取物品信息',
description='返回物品信息列表',
response_model=DefaultResponse,
response_description='物品信息列表'
)
async def get_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
id: int | None = Query(default=None, ge=1, description='物品ID'),
key: str | None = Query(default=None, description='物品序列号')):
'''
获得物品信息。
不传参数返回所有信息,否则可传入 `id` 或 `key` 进行筛选。
'''
# 根据条件查询物品
if id is not None:
results = await Object.get(session, Object.id == id)
results = [results] if results else []
elif key is not None:
results = await Object.get(session, Object.key == key)
results = [results] if results else []
else:
results = await Object.get(session, None, fetch_mode="all")
if results:
items = []
for obj in results:
items.append(Item(
id=obj.id,
type=obj.type,
key=obj.key,
name=obj.name,
icon=obj.icon or "",
status=obj.status or "",
phone=int(obj.phone) if obj.phone and obj.phone.isdigit() else 0,
lost_description=obj.context,
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=[])
@Router.post(
path='/items',
summary='添加物品信息',
description='添加新的物品信息',
response_model=DefaultResponse,
response_description='添加物品成功'
)
async def add_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
key: str,
type: Literal['normal', 'car'],
name: str,
icon: str,
phone: str
) -> DefaultResponse:
'''
添加物品信息。
- **key**: 物品的关键字
- **type**: 物品的类型
- **name**: 物品的名称
- **icon**: 物品的图标
- **phone**: 联系电话
'''
try:
# 创建新物品对象
new_object = Object(
key=key,
type=type,
name=name,
icon=icon,
phone=phone
)
# 使用 base.py 中的 add 方法
await Object.add(session, new_object)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse(data=True)
@Router.patch(
path='/items',
summary='更新物品信息',
description='更新现有物品的信息',
response_model=DefaultResponse,
response_description='更新物品成功'
)
async def update_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
id: int = Query(ge=1),
key: str | None = None,
name: str | None = None,
icon: str | None = None,
status: str | None = None,
phone: int | None = None,
lost_description: str | None = None,
find_ip: str | None = None,
lost_time: str | None = None
) -> DefaultResponse:
'''
更新物品信息。
只有 `id` 是必填参数,其余参数都是可选的,在不传入任何值的时候将不做任何更改。
- **id**: 物品的ID
- **key**: 物品的序列号 **不建议修改此项,这样会导致生成的物品二维码直接失效**
- **name**: 物品的名称
- **icon**: 物品的图标
- **status**: 物品的状态
- **phone**: 联系电话
- **lost_description**: 物品丢失描述
- **find_ip**: 找到物品的IP
- **lost_time**: 物品丢失时间
'''
try:
# 获取现有物品
obj = await Object.get_exist_one(session, id)
# 更新字段
if key is not None:
obj.key = key
if name is not None:
obj.name = name
if icon is not None:
obj.icon = icon
if status is not None:
obj.status = status
if phone is not None:
obj.phone = str(phone)
if lost_description is not None:
obj.context = lost_description
if find_ip is not None:
obj.find_ip = find_ip
if lost_time is not None:
from datetime import datetime
obj.lost_at = datetime.fromisoformat(lost_time)
# 保存更新
await obj.save(session)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse(data=True)
@Router.delete(
path='/items',
summary='删除物品信息',
description='删除指定的物品信息',
response_model=DefaultResponse,
response_description='删除物品成功'
)
async def delete_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
id: int) -> DefaultResponse:
'''
删除物品信息。
- **id**: 物品的ID
'''
try:
# 获取现有物品
obj = await Object.get_exist_one(session, id)
# 使用 base.py 中的 delete 方法
await Object.delete(session, obj)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse(data=True)

View File

@@ -1,13 +1,218 @@
import random import random
from fastapi import APIRouter, Request from fastapi import APIRouter, Request, Query, HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from model.database import Database from slowapi import Limiter
from slowapi.util import get_remote_address
from model import database, Object, Setting
from model import User
from model.items import Item
from middleware.user import get_current_user
from loguru import logger
from model.response import DefaultResponse, ObjectData from model.response import DefaultResponse, ObjectData
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Annotated, Literal
limiter = Limiter(key_func=get_remote_address)
from fastapi import Depends
import asyncio import asyncio
import aiohttp import aiohttp
Router = APIRouter(prefix='/api/object', tags=['物品 Object']) Router = APIRouter(prefix='/api/object', tags=['物品 Object'])
@Router.get(
path='/items',
summary='获取物品信息',
description='返回物品信息列表',
response_model=DefaultResponse,
response_description='物品信息列表'
)
async def get_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
token: Annotated[User, Depends(get_current_user)],
id: int | None = Query(default=None, ge=1, description='物品ID'),
key: str | None = Query(default=None, description='物品序列号')):
'''
获得物品信息。
不传参数返回所有信息,否则可传入 `id` 或 `key` 进行筛选。
'''
# 根据条件查询物品,只获取当前用户的物品
if id is not None:
results = await Object.get(session, (Object.id == id) & (Object.user_id == token.id))
results = [results] if results else []
elif key is not None:
results = await Object.get(session, (Object.key == key) & (Object.user_id == token.id))
results = [results] if results else []
else:
results = await Object.get(session, Object.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.key,
name=obj.name,
icon=obj.icon or "",
status=obj.status or "",
phone=int(obj.phone) if obj.phone and obj.phone.isdigit() else 0,
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=[])
@Router.post(
path='/items',
summary='添加物品信息',
description='添加新的物品信息',
response_model=DefaultResponse,
response_description='添加物品成功'
)
async def add_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
token: Annotated[User, Depends(get_current_user)],
key: str,
type: Literal['normal', 'car'],
name: str,
icon: str,
phone: str
) -> DefaultResponse:
'''
添加物品信息。
- **key**: 物品的关键字
- **type**: 物品的类型
- **name**: 物品的名称
- **icon**: 物品的图标
- **phone**: 联系电话
'''
try:
# 创建新物品对象,关联当前用户
new_object = Object(
key=key,
type=type,
name=name,
icon=icon,
phone=phone,
user_id=token.id
)
# 使用 base.py 中的 add 方法
await Object.add(session, new_object)
except Exception as e:
logger.error(f"Failed to add item: {e}")
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse(data=True)
@Router.patch(
path='/items',
summary='更新物品信息',
description='更新现有物品的信息',
response_model=DefaultResponse,
response_description='更新物品成功'
)
async def update_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
token: Annotated[User, Depends(get_current_user)],
id: int = Query(ge=1),
key: str | None = None,
name: str | None = None,
icon: str | None = None,
status: str | None = None,
phone: int | None = None,
lost_description: str | None = None,
find_ip: str | None = None,
lost_time: str | None = None
) -> DefaultResponse:
'''
更新物品信息。
只有 `id` 是必填参数,其余参数都是可选的,在不传入任何值的时候将不做任何更改。
- **id**: 物品的ID
- **key**: 物品的序列号 **不建议修改此项,这样会导致生成的物品二维码直接失效**
- **name**: 物品的名称
- **icon**: 物品的图标
- **status**: 物品的状态
- **phone**: 联系电话
- **lost_description**: 物品丢失描述
- **find_ip**: 找到物品的IP
- **lost_time**: 物品丢失时间
'''
try:
# 获取现有物品,验证归属权
obj = await Object.get(session, (Object.id == id) & (Object.user_id == token.id))
if not obj:
raise HTTPException(status_code=404, detail="Item not found or access denied")
# 更新字段
if key is not None:
obj.key = key
if name is not None:
obj.name = name
if icon is not None:
obj.icon = icon
if status is not None:
obj.status = status
if phone is not None:
obj.phone = str(phone)
if lost_description is not None:
obj.context = lost_description
if find_ip is not None:
obj.find_ip = find_ip
if lost_time is not None:
from datetime import datetime
obj.lost_at = datetime.fromisoformat(lost_time)
# 保存更新
await obj.save(session)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse(data=True)
@Router.delete(
path='/items',
summary='删除物品信息',
description='删除指定的物品信息',
response_model=DefaultResponse,
response_description='删除物品成功'
)
async def delete_items(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
token: Annotated[User, Depends(get_current_user)],
id: int
) -> DefaultResponse:
'''
删除物品信息。
- **id**: 物品的ID
'''
try:
# 获取现有物品,验证归属权
obj = await Object.get(session, (Object.id == id) & (Object.user_id == token.id))
if not obj:
raise HTTPException(status_code=404, detail="Item not found or access denied")
# 使用 base.py 中的 delete 方法
await Object.delete(session, obj)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
else:
return DefaultResponse(data=True)
@Router.get( @Router.get(
path='/{item_key}', path='/{item_key}',
summary="获取物品信息", summary="获取物品信息",
@@ -15,36 +220,44 @@ Router = APIRouter(prefix='/api/object', tags=['物品 Object'])
response_model=DefaultResponse, response_model=DefaultResponse,
response_description="物品信息" response_description="物品信息"
) )
async def get_object(item_key: str, request: Request): async def get_object(
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
item_key: str,
request: Request
):
""" """
获取物品信息 / Get object information 获取物品信息 / Get object information
""" """
db = Database() object_data = await Object.get(session, Object.key == item_key)
await db.init_db()
object_data = await db.get_object(key=item_key)
if object_data: if object_data:
if object_data[5] == 'lost': if object_data.status == 'lost':
# 物品已标记为丢失更新IP地址 # 物品已标记为丢失更新IP地址
await db.update_object(id=object_data[0], find_ip=str(request.client.host)) await Object.update(
session,
id=object_data.id,
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))
print(object_data) print(object_data)
return DefaultResponse(data=ObjectData( return DefaultResponse(
id=object_data[0], data=ObjectData(
type=object_data[1], id=object_data.id,
key=object_data[2], type=object_data.type,
name=object_data[3], key=object_data.key,
icon=object_data[4], name=object_data.name,
status=object_data[5], icon=object_data.icon,
phone=object_data[6], status=object_data.status,
lost_description=object_data[7], phone=object_data.phone,
create_time=object_data[9], lost_description=object_data.lost_description,
lost_time=object_data[10] create_time=object_data.create_time,
).model_dump()) lost_time=object_data.lost_time
).model_dump()
)
else: return JSONResponse( else: return JSONResponse(
status_code=404, status_code=404,
content=DefaultResponse( content=DefaultResponse(
@@ -60,7 +273,13 @@ async def get_object(item_key: str, request: Request):
response_model=DefaultResponse, response_model=DefaultResponse,
response_description="挪车通知结果" response_description="挪车通知结果"
) )
@limiter.limit(
limit_value="1/30minute", # 每30分钟允许1次请求
error_message="小主已经通知过车主了,请稍安勿躁~"
)
async def notify_move_car( async def notify_move_car(
request: Request,
session: Annotated[AsyncSession, Depends(database.Database.get_session)],
item_id: int, item_id: int,
phone: str = None, phone: str = None,
): ):
@@ -70,11 +289,9 @@ async def notify_move_car(
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.
""" """
db = Database()
await db.init_db()
# 检查是否存在该物品 # 检查是否存在该物品
object_data = await db.get_object(id=item_id) object_data = await Object.get(session, Object.id == item_id)
if not object_data: if not object_data:
return JSONResponse( return JSONResponse(
status_code=404, status_code=404,
@@ -85,7 +302,7 @@ async def notify_move_car(
) )
# 检查物品类型是否为车辆 # 检查物品类型是否为车辆
if object_data[1] != 'car': if object_data.type != 'car':
return JSONResponse( return JSONResponse(
status_code=400, status_code=400,
content=DefaultResponse( content=DefaultResponse(
@@ -95,7 +312,7 @@ async def notify_move_car(
) )
# 发起挪车通知目前仅适配Server酱 # 发起挪车通知目前仅适配Server酱
server_chan_key = await db.get_setting('server_chan_key') server_chan_key = await Setting.get(session, Setting.name == 'server_chan_key')
if not server_chan_key: if not server_chan_key:
return JSONResponse( return JSONResponse(
status_code=500, status_code=500,
@@ -106,14 +323,14 @@ async def notify_move_car(
) )
title = "挪车通知 - Findreve" title = "挪车通知 - Findreve"
description = f"您的车辆“{object_data[3]}”被请求挪车。\n\n" description = f"您的车辆“{object_data.name}”被请求挪车。\n\n"
if phone: if phone:
description += f"请求挪车者电话:[{phone}](tel:{phone})\n\n" description += f"请求挪车者电话:[{phone}](tel:{phone})\n\n"
description += "请尽快联系请求者并挪车。" description += "请尽快联系请求者并挪车。"
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post( async with session.post(
url=f"https://sctapi.ftqq.com/{server_chan_key}.send", url=f"https://sctapi.ftqq.com/{server_chan_key.value}.send",
data={ data={
"title": title, "title": title,
"desp": description "desp": description
@@ -124,18 +341,22 @@ async def notify_move_car(
if resp_json.get('code') == 0: if resp_json.get('code') == 0:
return DefaultResponse(msg='挪车通知发送成功') return DefaultResponse(msg='挪车通知发送成功')
else: else:
error_msg = resp_json.get('message')
logger.error(f"Failed to send notification via Server Chan: error_code={resp_json.get('code')}, error_message={error_msg}, item_id={item_id}, response={resp_json}")
return JSONResponse( return JSONResponse(
status_code=500, status_code=500,
content=DefaultResponse( content=DefaultResponse(
code=500, code=500,
msg=f"挪车通知发送失败Server酱返回错误{resp_json.get('message')}" msg=f"挪车通知发送失败,Server酱返回错误:{error_msg}"
).model_dump() ).model_dump()
) )
else: else:
response_text = await resp.text()
logger.error(f"Failed to send notification via Server Chan: http_status={resp.status}, item_id={item_id}, response_body={response_text}, url={resp.url}")
return JSONResponse( return JSONResponse(
status_code=500, status_code=500,
content=DefaultResponse( content=DefaultResponse(
code=500, code=500,
msg=f"挪车通知发送失败HTTP状态码{resp.status}" msg=f"挪车通知发送失败,HTTP状态码:{resp.status}"
).model_dump() ).model_dump()
) )

View File

@@ -10,17 +10,17 @@ from pkg import Password
from loguru import logger from loguru import logger
from model.token import Token from model.token import Token
from model import Setting, database from model import Setting, User, database
Router = APIRouter(tags=["令牌 session"]) Router = APIRouter(tags=["令牌 session"])
# 创建令牌 # 创建令牌
async def create_access_token(data: dict, expires_delta: timedelta | None = None): async def create_access_token(session: AsyncSession, data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy() to_encode = data.copy()
if expires_delta: if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta expire = datetime.now(timezone.utc) + expires_delta
else: else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15) expire = datetime.now(timezone.utc) + timedelta(minutes=await Setting.get(session, 'jwt_token_exp'))
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, key=await JWT.get_secret_key(), algorithm='HS256') encoded_jwt = jwt.encode(to_encode, key=await JWT.get_secret_key(), algorithm='HS256')
return encoded_jwt return encoded_jwt
@@ -28,18 +28,17 @@ async def create_access_token(data: dict, expires_delta: timedelta | None = None
# 验证账号密码 # 验证账号密码
async def authenticate_user(session: AsyncSession, username: str, password: str): async def authenticate_user(session: AsyncSession, username: str, password: str):
# 验证账号和密码 # 验证账号和密码
account = await Setting.get(session, Setting.name == 'account') account = await User.get(session, User.email == username)
stored_password = await Setting.get(session, Setting.name == 'password')
if not account or not stored_password: if not account:
logger.error("Account or password not set in settings.") logger.error("Account or password not set in settings.")
return False return False
if account.value != username or not Password.verify(stored_password.value, password): if account.email != username or not Password.verify(account.password, password):
logger.error("Invalid username or password.") logger.error("Invalid username or password.")
return False return False
return {'is_authenticated': True} return account
# FastAPI 登录路由 / FastAPI login route # FastAPI 登录路由 / FastAPI login route
@Router.post( @Router.post(
@@ -66,6 +65,8 @@ async def login_for_access_token(
) )
access_token_expires = timedelta(hours=1) access_token_expires = timedelta(hours=1)
access_token = await create_access_token( access_token = await create_access_token(
data={"sub": form_data.username}, expires_delta=access_token_expires session=session,
data={"sub": form_data.username},
expires_delta=access_token_expires
) )
return Token(access_token=access_token, token_type="bearer") return Token(access_token=access_token, token_type="bearer")