初步迁移数据库
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
14
.idea/Findreve.iml
generated
Normal file
14
.idea/Findreve.iml
generated
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.12 (Findreve)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
13
.idea/dataSources.xml
generated
Normal file
13
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="data.db" uuid="c73289bb-3157-4ec3-b3f4-744da38bc64a">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<remarks>Findreve Data</remarks>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/data.db</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
29
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
29
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PyInterpreterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PyMissingTypeHintsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="E402" />
|
||||||
|
<option value="E713" />
|
||||||
|
<option value="E271" />
|
||||||
|
<option value="E302" />
|
||||||
|
<option value="E265" />
|
||||||
|
<option value="W292" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8NamingInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="N802" />
|
||||||
|
<option value="N806" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
17
.idea/material_theme_project_new.xml
generated
Normal file
17
.idea/material_theme_project_new.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MaterialThemeProjectNewConfig">
|
||||||
|
<option name="metadata">
|
||||||
|
<MTProjectMetadataState>
|
||||||
|
<option name="migrated" value="true" />
|
||||||
|
<option name="pristineConfig" value="false" />
|
||||||
|
<option name="userId" value="4dc9a07a:18f958e7499:-7ffe" />
|
||||||
|
</MTProjectMetadataState>
|
||||||
|
</option>
|
||||||
|
<option name="titleBarState">
|
||||||
|
<MTProjectTitleBarConfigState>
|
||||||
|
<option name="overrideColor" value="false" />
|
||||||
|
</MTProjectTitleBarConfigState>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.12 (Findreve)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (Findreve)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Findreve.iml" filepath="$PROJECT_DIR$/.idea/Findreve.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
26
.idea/runConfigurations/Findreve.xml
generated
Normal file
26
.idea/runConfigurations/Findreve.xml
generated
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="启动Findreve" type="PythonConfigurationType" factoryName="Python">
|
||||||
|
<module name="Findreve" />
|
||||||
|
<option name="ENV_FILES" value="" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="SDK_NAME" value="Python 3.12 (Findreve)" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="C:\Users\admin\Documents\Code\Findreve" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" />
|
||||||
|
<option name="PARAMETERS" value="" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../Findreve-NiceGUI"
|
"path": "../Findreve-NiceGUI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../vein-based-iov-simulator-main"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {}
|
"settings": {}
|
||||||
|
|||||||
20
app.py
20
app.py
@@ -5,21 +5,11 @@ from contextlib import asynccontextmanager
|
|||||||
from routes import (session, admin, object)
|
from routes import (session, admin, object)
|
||||||
import model.database
|
import model.database
|
||||||
import os, asyncio
|
import os, asyncio
|
||||||
|
import pkg.conf
|
||||||
|
|
||||||
# 初始化数据库
|
# 初始化数据库
|
||||||
asyncio.run(model.database.Database().init_db())
|
asyncio.run(model.database.Database().init_db())
|
||||||
|
|
||||||
# 定义程序参数
|
|
||||||
APP_NAME: str = 'Findreve'
|
|
||||||
VERSION: str = '2.0.0'
|
|
||||||
summary='标记、追踪与找回 —— 就这么简单。'
|
|
||||||
description='Findreve 是一款强大且直观的解决方案,旨在帮助您管理个人物品,'\
|
|
||||||
'并确保丢失后能够安全找回。每个物品都会被分配一个 唯一 ID ,'\
|
|
||||||
'并生成一个 安全链接 ,可轻松嵌入到 二维码 或 NFC 标签 中。'\
|
|
||||||
'当扫描该代码时,会将拾得者引导至一个专门的网页,上面显示物品详情和您的联系信息,'\
|
|
||||||
'既保障隐私又便于沟通。无论您是在管理个人物品还是专业资产,'\
|
|
||||||
'Findreve 都能以高效、简便的方式弥合丢失与找回之间的距离。'
|
|
||||||
|
|
||||||
# Findreve 的生命周期
|
# Findreve 的生命周期
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
@@ -28,10 +18,10 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# 定义 Findreve 服务器
|
# 定义 Findreve 服务器
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=APP_NAME,
|
title=pkg.conf.APP_NAME,
|
||||||
version=VERSION,
|
version=pkg.conf.VERSION,
|
||||||
summary=summary,
|
summary=pkg.conf.summary,
|
||||||
description=description,
|
description=pkg.conf.description,
|
||||||
lifespan=lifespan
|
lifespan=lifespan
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
from . import token
|
from . import token
|
||||||
|
|
||||||
from .object import Object
|
|
||||||
from .setting import Setting
|
from .setting import Setting
|
||||||
|
from .object import Object
|
||||||
__all__ = [
|
|
||||||
"Object",
|
|
||||||
"Setting"
|
|
||||||
]
|
|
||||||
112
model/base.py
112
model/base.py
@@ -1,22 +1,22 @@
|
|||||||
|
# model/base.py
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional, Type, TypeVar, Union, Literal, List
|
from typing import Optional, Type, TypeVar, Union, Literal, List
|
||||||
|
|
||||||
from sqlalchemy import DateTime, BinaryExpression, ClauseElement
|
from sqlalchemy import DateTime, BinaryExpression, ClauseElement
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlmodel import SQLModel, Field, select, Relationship
|
|
||||||
from sqlalchemy.ext.asyncio.session import AsyncSession
|
from sqlalchemy.ext.asyncio.session import AsyncSession
|
||||||
from sqlalchemy.sql._typing import _OnClauseArgument
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||||
|
from sqlmodel import SQLModel, Field, select, Relationship
|
||||||
|
from sqlalchemy.sql._typing import _OnClauseArgument
|
||||||
|
|
||||||
B = TypeVar('B', bound='BaseModel')
|
B = TypeVar('B', bound='TableBase')
|
||||||
M = TypeVar('M', bound='SQLModel')
|
M = TypeVar('M', bound='SQLModel')
|
||||||
|
|
||||||
utcnow = lambda: datetime.now(tz=timezone.utc)
|
utcnow = lambda: datetime.now(tz=timezone.utc)
|
||||||
|
|
||||||
class BaseModel(AsyncAttrs):
|
class TableBase(AsyncAttrs, SQLModel):
|
||||||
__abstract__ = True
|
__abstract__ = True
|
||||||
|
|
||||||
id: Optional[int] = Field(default=None, primary_key=True, description="主键ID")
|
|
||||||
created_at: datetime = Field(
|
created_at: datetime = Field(
|
||||||
default_factory=utcnow,
|
default_factory=utcnow,
|
||||||
description="创建时间",
|
description="创建时间",
|
||||||
@@ -40,65 +40,30 @@ class BaseModel(AsyncAttrs):
|
|||||||
instances: B | List[B],
|
instances: B | List[B],
|
||||||
refresh: bool = True
|
refresh: bool = True
|
||||||
) -> B | List[B]:
|
) -> B | List[B]:
|
||||||
"""
|
is_list = isinstance(instances, list)
|
||||||
新增一条记录
|
if is_list:
|
||||||
|
|
||||||
:param session: 异步会话对象
|
|
||||||
:param instances: 实例或实例列表
|
|
||||||
:param refresh: 是否刷新实例
|
|
||||||
:return: 新增的实例或实例列表
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
>>> from model.base import BaseModel
|
|
||||||
> from model.object import Object
|
|
||||||
> from database import Database
|
|
||||||
> import asyncio
|
|
||||||
> async def main():
|
|
||||||
> async with Database.get_session() as session:
|
|
||||||
> obj = Object(key="12345", name="Test Object", icon="icon.png")
|
|
||||||
> added_obj = await BaseModel.add(session, obj)
|
|
||||||
> print(added_obj)
|
|
||||||
> asyncio.run(main())
|
|
||||||
|
|
||||||
"""
|
|
||||||
is_list = False
|
|
||||||
if isinstance(instances, list):
|
|
||||||
is_list = True
|
|
||||||
session.add_all(instances)
|
session.add_all(instances)
|
||||||
else:
|
else:
|
||||||
session.add(instances)
|
session.add(instances)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
if refresh:
|
if refresh:
|
||||||
if is_list:
|
if is_list:
|
||||||
for instance in instances:
|
for i in instances:
|
||||||
await session.refresh(instance)
|
await session.refresh(i)
|
||||||
else:
|
else:
|
||||||
await session.refresh(instances)
|
await session.refresh(instances)
|
||||||
|
|
||||||
return instances
|
return instances
|
||||||
|
|
||||||
async def save(
|
async def save(
|
||||||
self: B,
|
self: B,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
load: Union[Relationship, None]
|
load: Union[Relationship, None] = None, # 设默认值,避免必须传
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
保存当前实例到数据库
|
|
||||||
|
|
||||||
:param session: 异步会话对象
|
|
||||||
:param load: 需要加载的关系属性
|
|
||||||
:return: None
|
|
||||||
|
|
||||||
"""
|
|
||||||
session.add(self)
|
session.add(self)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
if load is not None:
|
if load is not None:
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
return await cls.get(session, self.id, load=load)
|
return await cls.get(session, cls.id == self.id, load=load) # 若该模型没有 id,请别用 load 模式
|
||||||
else:
|
else:
|
||||||
await session.refresh(self)
|
await session.refresh(self)
|
||||||
return self
|
return self
|
||||||
@@ -110,27 +75,13 @@ class BaseModel(AsyncAttrs):
|
|||||||
extra_data: dict = None,
|
extra_data: dict = None,
|
||||||
exclude_unset: bool = True,
|
exclude_unset: bool = True,
|
||||||
) -> B:
|
) -> B:
|
||||||
"""
|
|
||||||
更新当前实例
|
|
||||||
|
|
||||||
:param session: 异步会话对象
|
|
||||||
:param other: 用于更新的实例
|
|
||||||
:param extra_data: 额外的数据字典
|
|
||||||
:param exclude_unset: 是否排除未设置的字段
|
|
||||||
:return: 更新后的实例
|
|
||||||
"""
|
|
||||||
self.sqlmodel_update(
|
self.sqlmodel_update(
|
||||||
other.model_dump(
|
other.model_dump(exclude_unset=exclude_unset),
|
||||||
exclude_unset=exclude_unset
|
|
||||||
),
|
|
||||||
update=extra_data
|
update=extra_data
|
||||||
)
|
)
|
||||||
|
|
||||||
session.add(self)
|
session.add(self)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(self)
|
await session.refresh(self)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -139,20 +90,11 @@ class BaseModel(AsyncAttrs):
|
|||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
instance: B | list[B],
|
instance: B | list[B],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
删除实例
|
|
||||||
|
|
||||||
:param session: 异步会话对象
|
|
||||||
:param instance: 实例或实例列表
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
if isinstance(instance, list):
|
if isinstance(instance, list):
|
||||||
for inst in instance:
|
for inst in instance:
|
||||||
await session.delete(inst)
|
await session.delete(inst)
|
||||||
else:
|
else:
|
||||||
await session.delete(instance)
|
await session.delete(instance)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -169,46 +111,23 @@ class BaseModel(AsyncAttrs):
|
|||||||
load: Union[Relationship, None] = None,
|
load: Union[Relationship, None] = None,
|
||||||
order_by: list[ClauseElement] | None = None
|
order_by: list[ClauseElement] | None = None
|
||||||
) -> B | List[B] | None:
|
) -> B | List[B] | None:
|
||||||
"""
|
|
||||||
异步获取模型实例
|
|
||||||
|
|
||||||
参数:
|
|
||||||
session: 异步数据库会话
|
|
||||||
condition: SQLAlchemy查询条件,如Model.id == 1
|
|
||||||
offset: 结果偏移量
|
|
||||||
limit: 结果数量限制
|
|
||||||
options: 查询选项,如selectinload(Model.relation),异步访问关系属性必备,不然会报错
|
|
||||||
fetch_mode: 获取模式 - "one"/"all"/"first"
|
|
||||||
join: 要联接的模型类
|
|
||||||
|
|
||||||
返回:
|
|
||||||
根据fetch_mode返回相应的查询结果
|
|
||||||
"""
|
|
||||||
statement = select(cls)
|
statement = select(cls)
|
||||||
|
|
||||||
if condition is not None:
|
if condition is not None:
|
||||||
statement = statement.where(condition)
|
statement = statement.where(condition)
|
||||||
|
|
||||||
if join is not None:
|
if join is not None:
|
||||||
statement = statement.join(*join)
|
statement = statement.join(*join)
|
||||||
|
|
||||||
if options:
|
if options:
|
||||||
statement = statement.options(*options)
|
statement = statement.options(*options)
|
||||||
|
|
||||||
if load:
|
if load:
|
||||||
statement = statement.options(selectinload(load))
|
statement = statement.options(selectinload(load))
|
||||||
|
|
||||||
if order_by is not None:
|
if order_by is not None:
|
||||||
statement = statement.order_by(*order_by)
|
statement = statement.order_by(*order_by)
|
||||||
|
|
||||||
if offset:
|
if offset:
|
||||||
statement = statement.offset(offset)
|
statement = statement.offset(offset)
|
||||||
|
|
||||||
if limit:
|
if limit:
|
||||||
statement = statement.limit(limit)
|
statement = statement.limit(limit)
|
||||||
|
|
||||||
result = await session.exec(statement)
|
result = await session.exec(statement)
|
||||||
|
|
||||||
if fetch_mode == "one":
|
if fetch_mode == "one":
|
||||||
return result.one()
|
return result.one()
|
||||||
elif fetch_mode == "first":
|
elif fetch_mode == "first":
|
||||||
@@ -220,10 +139,13 @@ class BaseModel(AsyncAttrs):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_exist_one(cls: Type[B], session: AsyncSession, id: int, load: Union[Relationship, None] = None) -> B:
|
async def get_exist_one(cls: Type[B], session: AsyncSession, id: int, load: Union[Relationship, None] = None) -> B:
|
||||||
"""此方法和 await session.get(cls, 主键)的区别就是当不存在时不返回None,
|
|
||||||
而是会抛出fastapi 404 异常"""
|
|
||||||
instance = await cls.get(session, cls.id == id, load=load)
|
instance = await cls.get(session, cls.id == id, load=load)
|
||||||
if not instance:
|
if not instance:
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
raise HTTPException(status_code=404, detail="Not found")
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
# 需要“自增 id 主键”的模型才混入它;Setting 不混入
|
||||||
|
class IdMixin(SQLModel):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True, description="主键ID")
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -8,6 +9,9 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
|||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
from .migration import migration
|
||||||
|
|
||||||
ASYNC_DATABASE_URL = "sqlite+aiosqlite:///data.db"
|
ASYNC_DATABASE_URL = "sqlite+aiosqlite:///data.db"
|
||||||
|
|
||||||
engine: AsyncEngine = create_async_engine(
|
engine: AsyncEngine = create_async_engine(
|
||||||
@@ -21,7 +25,7 @@ engine: AsyncEngine = create_async_engine(
|
|||||||
# max_overflow=64,
|
# max_overflow=64,
|
||||||
)
|
)
|
||||||
|
|
||||||
_async_session_factory = sessionmaker(engine, class_=AsyncSession)
|
_async_session_factory = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
|
|
||||||
# 数据库类
|
# 数据库类
|
||||||
class Database:
|
class Database:
|
||||||
@@ -33,6 +37,8 @@ class Database:
|
|||||||
):
|
):
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@asynccontextmanager
|
||||||
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
async with _async_session_factory() as session:
|
async with _async_session_factory() as session:
|
||||||
yield session
|
yield session
|
||||||
@@ -45,6 +51,9 @@ class Database:
|
|||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
await conn.run_sync(SQLModel.metadata.create_all)
|
await conn.run_sync(SQLModel.metadata.create_all)
|
||||||
|
|
||||||
|
async with self.get_session() as session:
|
||||||
|
await migration(session) # 执行迁移脚本
|
||||||
|
|
||||||
async def add_object(self, key: str, name: str, icon: str = None, phone: str = None):
|
async def add_object(self, key: str, name: str, icon: str = None, phone: str = None):
|
||||||
"""
|
"""
|
||||||
添加新对象
|
添加新对象
|
||||||
@@ -54,6 +63,9 @@ class Database:
|
|||||||
:param icon: 图标
|
:param icon: 图标
|
||||||
:param phone: 电话
|
:param phone: 电话
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
warnings.warn("因需要迁移至ORM,此方法已被废弃", DeprecationWarning)
|
||||||
|
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
async with db.execute("SELECT 1 FROM fr_objects WHERE key = ?", (key,)) as cursor:
|
async with db.execute("SELECT 1 FROM fr_objects WHERE key = ?", (key,)) as cursor:
|
||||||
if await cursor.fetchone():
|
if await cursor.fetchone():
|
||||||
|
|||||||
@@ -1,3 +1,36 @@
|
|||||||
|
from typing import Sequence
|
||||||
|
from sqlmodel import select
|
||||||
|
from .setting import Setting
|
||||||
|
import tool
|
||||||
|
|
||||||
|
default_settings: list[Setting] = [
|
||||||
|
Setting(type='string', name='version', value='1.0.0'),
|
||||||
|
Setting(type='int', name='ver', value='1'),
|
||||||
|
Setting(type='string', name='account', value='admin@yuxiaoqiu.cn'),
|
||||||
|
]
|
||||||
|
|
||||||
|
async def migration(session):
|
||||||
|
# 先准备基础配置
|
||||||
|
settings: list[Setting] = default_settings.copy()
|
||||||
|
|
||||||
|
# 生成初始密码与密钥
|
||||||
|
admin_password = tool.generate_password()
|
||||||
|
print(f"密码(请牢记,后续不再显示): {admin_password}")
|
||||||
|
|
||||||
|
settings.append(Setting(type='string', name='password', value=tool.hash_password(admin_password)))
|
||||||
|
settings.append(Setting(type='string', name='SECRET_KEY', value=tool.generate_password(64)))
|
||||||
|
|
||||||
|
# 读取库里已存在的 name,避免主键冲突
|
||||||
|
names = [s.name for s in settings]
|
||||||
|
exist_stmt = select(Setting.name).where(Setting.name.in_(names))
|
||||||
|
exist_rs = await session.exec(exist_stmt)
|
||||||
|
existed: set[str] = set(exist_rs.all())
|
||||||
|
|
||||||
|
to_insert = [s for s in settings if s.name not in existed]
|
||||||
|
if to_insert:
|
||||||
|
# 使用你写好的通用新增方法(是类方法),并传入会话
|
||||||
|
await Setting.add(session, to_insert, refresh=False)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# 初始化设置表数据
|
# 初始化设置表数据
|
||||||
async with db.execute("SELECT name FROM fr_settings WHERE name = 'version'") as cursor:
|
async with db.execute("SELECT name FROM fr_settings WHERE name = 'version'") as cursor:
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
from typing import Literal, Optional, TYPE_CHECKING
|
from typing import Literal, Optional, TYPE_CHECKING
|
||||||
from sqlmodel import Field, Column, SQLModel, String, DateTime
|
from sqlmodel import Field, Column, SQLModel, String, DateTime
|
||||||
from .base import BaseModel
|
from .base import TableBase, IdMixin
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .base import BaseModel
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
原建表语句:
|
原建表语句:
|
||||||
|
|
||||||
@@ -26,7 +24,7 @@ CREATE TABLE IF NOT EXISTS fr_objects (
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Object(SQLModel, BaseModel, table=True):
|
class Object(IdMixin, TableBase, table=True):
|
||||||
__tablename__ = 'fr_objects'
|
__tablename__ = 'fr_objects'
|
||||||
|
|
||||||
key: str = Field(index=True, nullable=False, description="物品外部ID")
|
key: str = Field(index=True, nullable=False, description="物品外部ID")
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
|
# model/setting.py
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field
|
||||||
from .base import BaseModel
|
from .base import TableBase
|
||||||
|
|
||||||
"""
|
"""
|
||||||
原建表语句:
|
原表:
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fr_settings (
|
CREATE TABLE IF NOT EXISTS fr_settings (
|
||||||
type TEXT,
|
type TEXT,
|
||||||
name TEXT PRIMARY KEY,
|
name TEXT PRIMARY KEY,
|
||||||
value TEXT
|
value TEXT
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Setting(SQLModel, BaseModel, table=True):
|
class Setting(TableBase, table=True):
|
||||||
__tablename__ = 'fr_settings'
|
__tablename__ = 'fr_settings'
|
||||||
|
|
||||||
type: str = Field(index=True, nullable=False, description="设置类型")
|
type: str = Field(index=True, nullable=False, description="设置类型")
|
||||||
name: str = Field(index=True, primary_key=True, nullable=False, description="设置名称")
|
name: str = Field(primary_key=True, nullable=False, description="设置名称") # name 为唯一主键
|
||||||
value: Optional[str] = Field(description="设置值")
|
value: Optional[str] = Field(description="设置值")
|
||||||
9
pkg/conf.py
Normal file
9
pkg/conf.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
APP_NAME: str = 'Findreve'
|
||||||
|
VERSION: str = '2.0.0'
|
||||||
|
summary='标记、追踪与找回 —— 就这么简单。'
|
||||||
|
description='Findreve 是一款强大且直观的解决方案,旨在帮助您管理个人物品,'\
|
||||||
|
'并确保丢失后能够安全找回。每个物品都会被分配一个 唯一 ID ,'\
|
||||||
|
'并生成一个 安全链接 ,可轻松嵌入到 二维码 或 NFC 标签 中。'\
|
||||||
|
'当扫描该代码时,会将拾得者引导至一个专门的网页,上面显示物品详情和您的联系信息,'\
|
||||||
|
'既保障隐私又便于沟通。无论您是在管理个人物品还是专业资产,'\
|
||||||
|
'Findreve 都能以高效、简便的方式弥合丢失与找回之间的距离。'
|
||||||
@@ -7,7 +7,7 @@ from fastapi import APIRouter
|
|||||||
import jwt, JWT
|
import jwt, JWT
|
||||||
|
|
||||||
from model.token import Token
|
from model.token import Token
|
||||||
from model import database
|
from model import Setting, database
|
||||||
from tool import verify_password
|
from tool import verify_password
|
||||||
|
|
||||||
Router = APIRouter(tags=["令牌 session"])
|
Router = APIRouter(tags=["令牌 session"])
|
||||||
@@ -26,8 +26,8 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
|||||||
# 验证账号密码
|
# 验证账号密码
|
||||||
async def authenticate_user(username: str, password: str):
|
async def authenticate_user(username: str, password: str):
|
||||||
# 验证账号和密码
|
# 验证账号和密码
|
||||||
account = await database.Database().get_setting('account')
|
account = await Setting.get('setting', 'account')
|
||||||
stored_password = await database.Database().get_setting('password')
|
stored_password = await Setting.get('setting', 'password')
|
||||||
|
|
||||||
if account != username or not verify_password(stored_password, password):
|
if account != username or not verify_password(stored_password, password):
|
||||||
return False
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user