修复数据库调用问题
This commit is contained in:
8
app.py
8
app.py
@@ -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
34
middleware/admin.py
Normal 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
33
middleware/user.py
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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):
|
||||||
@@ -16,21 +17,46 @@ async def migration(session):
|
|||||||
if await Setting.get(session, Setting.name == 'version'):
|
if await Setting.get(session, Setting.name == 'version'):
|
||||||
# 已有数据,说明不是第一次运行,直接返回
|
# 已有数据,说明不是第一次运行,直接返回
|
||||||
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)
|
||||||
@@ -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
16
model/user.py
Normal 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")
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
223
routes/admin.py
223
routes/admin.py
@@ -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(
|
||||||
@@ -61,185 +25,4 @@ async def verity_admin() -> DefaultResponse:
|
|||||||
- 若为管理员,返回 `True`
|
- 若为管理员,返回 `True`
|
||||||
- 若不是管理员,抛出 `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)
|
|
||||||
283
routes/object.py
283
routes/object.py
@@ -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()
|
||||||
)
|
)
|
||||||
@@ -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")
|
||||||
Reference in New Issue
Block a user