diff --git a/clean.py b/clean.py index 56fbb1a..a8d9ee5 100644 --- a/clean.py +++ b/clean.py @@ -69,7 +69,7 @@ def get_excluded_dirs(exclude_arg: str) -> Set[str]: def clean_pycache(root_dir: str, exclude_dirs: Set[str], dry_run: bool = False) -> List[str]: """清理 __pycache__ 目录""" - log.info("开始清理 __pycache__ 目录...") + log.info("开始清理 __pycache__ 目录pass") cleaned_paths = [] for dirpath, dirnames, _ in os.walk(root_dir): @@ -90,7 +90,7 @@ def clean_pycache(root_dir: str, exclude_dirs: Set[str], dry_run: bool = False) def clean_pyc_files(root_dir: str, exclude_dirs: Set[str], dry_run: bool = False) -> List[str]: """清理 .pyc 文件""" - log.info("开始清理 .pyc 文件...") + log.info("开始清理 .pyc 文件pass") cleaned_files = [] for dirpath, dirnames, filenames in os.walk(root_dir): @@ -112,7 +112,7 @@ def clean_pyc_files(root_dir: str, exclude_dirs: Set[str], dry_run: bool = False def clean_pytest_cache(root_dir: str, exclude_dirs: Set[str], dry_run: bool = False) -> List[str]: """清理 .pytest_cache 目录""" - log.info("开始清理 .pytest_cache 目录...") + log.info("开始清理 .pytest_cache 目录pass") cleaned_paths = [] for dirpath, dirnames, _ in os.walk(root_dir): @@ -133,7 +133,7 @@ def clean_pytest_cache(root_dir: str, exclude_dirs: Set[str], dry_run: bool = Fa def clean_nicegui(root_dir: str, dry_run: bool = False) -> Tuple[bool, str]: """清理 .nicegui 目录""" - log.info("开始清理 .nicegui 目录...") + log.info("开始清理 .nicegui 目录pass") nicegui_dir = os.path.join(root_dir, ".nicegui") if os.path.exists(nicegui_dir) and os.path.isdir(nicegui_dir): success, error = safe_remove(nicegui_dir, dry_run) @@ -146,7 +146,7 @@ def clean_nicegui(root_dir: str, dry_run: bool = False) -> Tuple[bool, str]: def clean_testdb(root_dir: str, dry_run: bool = False) -> Tuple[bool, str, str]: """清理测试数据库文件""" - log.info("开始清理 test.db 文件...") + log.info("开始清理 test.db 文件pass") test_db = os.path.join(root_dir, "test.db") if os.path.exists(test_db) and os.path.isfile(test_db): success, error = safe_remove(test_db, dry_run) diff --git a/main.py b/main.py index 2a4d1c4..6c110ee 100644 --- a/main.py +++ b/main.py @@ -4,10 +4,14 @@ from pkg.conf import appmeta from models.database import init_db from models.migration import init_default_settings from pkg.lifespan import lifespan +from pkg.JWT import jwt +# 添加初始化数据库启动项 lifespan.add_startup(init_db) lifespan.add_startup(init_default_settings) +lifespan.add_startup(jwt.load_secret_key) +# 创建应用实例并设置元数据 app = FastAPI( title=appmeta.APP_NAME, summary=appmeta.summary, @@ -18,6 +22,7 @@ app = FastAPI( lifespan=lifespan.lifespan, ) +# 挂载路由 for router in routers.Router: app.include_router( router, @@ -30,7 +35,8 @@ for router in routers.Router: 500: {"description": "内部服务器错误 Internal server error"} },) +# 启动时打印欢迎信息 if __name__ == "__main__": import uvicorn - uvicorn.run(app=app, host="0.0.0.0", port=5213) - # uvicorn.run(app='main:app', host="0.0.0.0", port=5213, reload=True) \ No newline at end of file + # uvicorn.run(app=app, host="0.0.0.0", port=5213) # 生产环境 + uvicorn.run(app='main:app', host="0.0.0.0", port=5213, reload=True) # 开发环境 \ No newline at end of file diff --git a/middleware/auth.py b/middleware/auth.py index 69ac55c..60717df 100644 --- a/middleware/auth.py +++ b/middleware/auth.py @@ -8,7 +8,7 @@ async def AuthRequired( ''' AuthRequired 需要登录 ''' - return True + from models.user import User async def SignRequired( token: Annotated[str, Depends(jwt.oauth2_scheme)] @@ -27,4 +27,4 @@ async def AdminRequired( 使用方法: >>> APIRouter(dependencies=[Depends(is_admin)]) ''' - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/models/database.py b/models/database.py index e91dffd..e24f438 100644 --- a/models/database.py +++ b/models/database.py @@ -6,7 +6,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession from sqlalchemy.orm import sessionmaker from typing import AsyncGenerator -ASYNC_DATABASE_URL = "sqlite+aiosqlite:///database.db" +ASYNC_DATABASE_URL = "sqlite+aiosqlite:///:memory:" engine = create_async_engine( ASYNC_DATABASE_URL, diff --git a/models/group.py b/models/group.py index 35fd11d..b6ae66e 100644 --- a/models/group.py +++ b/models/group.py @@ -49,43 +49,58 @@ class Group(BaseModel, table=True): sa_relationship_kwargs={"foreign_keys": "User.previous_group_id"} ) - async def add_group(self, name: str, policies: Optional[str] = None, max_storage: int = 0, - share_enabled: bool = False, web_dav_enabled: bool = False, - speed_limit: int = 0, options: Optional[str] = None) -> "Group": + @staticmethod + async def create( + group: "Group" + ) -> "Group": """ 向数据库内添加用户组。 - :param name: 用户组名 - :type name: str - :param policies: 允许的策略ID列表,逗号分隔,默认为 None - :type policies: Optional[str] - :param max_storage: 最大存储空间(字节),默认为 0 - :type max_storage: int - :param share_enabled: 是否允许创建分享,默认为 False - :type share_enabled: bool - :param web_dav_enabled: 是否允许使用WebDAV,默认为 False - :type web_dav_enabled: bool - :param speed_limit: 速度限制 (KB/s), 0为不限制,默认为 0 - :type speed_limit: int - :param options: 其他选项 (JSON格式),默认为 None - :type options: Optional[str] + :param group: 用户组对象 + :type group: Group :return: 新创建的用户组对象 :rtype: Group """ from .database import get_session + + async for session in get_session(): + try: + session.add(group) + await session.commit() + await session.refresh(group) + except Exception as e: + await session.rollback() + raise e + return group + + @staticmethod + async def get( + id: int = None + ) -> Optional["Group"]: + """ + 获取用户组信息。 + + :param id: 用户组ID,默认为 None + :type id: int + + :return: 用户组对象或 None + :rtype: Optional[Group] + """ + from .database import get_session + from sqlmodel import select + session = get_session() - new_group = Group( - name=name, - policies=policies, - max_storage=max_storage, - share_enabled=share_enabled, - web_dav_enabled=web_dav_enabled, - speed_limit=speed_limit, - options=options - ) - session.add(new_group) + if id is None: + return None - session.commit() - session.refresh(new_group) \ No newline at end of file + async for session in get_session(): + statement = select(Group).where(Group.id == id) + result = await session.exec(statement) + group = result.one_or_none() + + if group: + return group + else: + return None \ No newline at end of file diff --git a/models/migration.py b/models/migration.py index d8fc6e0..5d4a6d8 100644 --- a/models/migration.py +++ b/models/migration.py @@ -113,4 +113,55 @@ async def init_default_settings() -> None: type=setting.type, name=setting.name, value=setting.value - ) \ No newline at end of file + ) + +async def init_default_group() -> None: + from .group import Group + + try: + # 未找到初始管理组时,则创建 + if not Group.get(id=1): + Group.add( + name="管理员", + max_storage=1 * 1024 * 1024 * 1024, # 1GB + share_enabled=True, + web_dav_enabled=True, + options={ + "ArchiveDownload": True, + "ArchiveTask": True, + "ShareDownload": True, + "Aria2": True, + } + ) + except Exception as e: + raise RuntimeError(f"无法创建管理员用户组: {e}") from e + + try: + # 未找到初始注册会员时,则创建 + if not Group.get(id=2): + Group.add( + name="注册会员", + max_storage=1 * 1024 * 1024 * 1024, # 1GB + share_enabled=True, + web_dav_enabled=True, + options={ + "ShareDownload": True, + } + ) + except Exception as e: + raise RuntimeError(f"无法创建初始注册会员用户组: {e}") from e + + try: + # 未找到初始游客组时,则创建 + if not Group.get(id=3): + Group.add( + name="游客", + policies="[]", + share_enabled=False, + web_dav_enabled=False, + options={ + "ShareDownload": True, + } + ) + except Exception as e: + raise RuntimeError(f"无法创建初始游客用户组: {e}") from e diff --git a/models/setting.py b/models/setting.py index 12dfe0f..1a399a4 100644 --- a/models/setting.py +++ b/models/setting.py @@ -71,6 +71,7 @@ class Setting(BaseModel, table=True): ), ) + @staticmethod async def add( type: SETTINGS_TYPE = None, name: str = None, @@ -97,6 +98,7 @@ class Setting(BaseModel, table=True): await session.commit() + @staticmethod async def get( type: SETTINGS_TYPE, name: str, @@ -138,6 +140,7 @@ class Setting(BaseModel, table=True): else: raise ValueError(f"Unsupported format: {format}") + @staticmethod async def set( type: SETTINGS_TYPE, name: str, @@ -177,6 +180,7 @@ class Setting(BaseModel, table=True): setting.value = value await session.commit() + @staticmethod async def delete( type: SETTINGS_TYPE, name: str diff --git a/models/user.py b/models/user.py index e898029..ec928f9 100644 --- a/models/user.py +++ b/models/user.py @@ -86,4 +86,25 @@ class User(BaseModel, table=True): storage_packs: list["StoragePack"] = Relationship(back_populates="user") tags: list["Tag"] = Relationship(back_populates="user") tasks: list["Task"] = Relationship(back_populates="user") - webdavs: list["WebDAV"] = Relationship(back_populates="user") \ No newline at end of file + webdavs: list["WebDAV"] = Relationship(back_populates="user") + + async def create( + user: "User" + ): + """ + 向数据库内添加用户。 + + :param user: User 实例 + :type user: User + """ + from .database import get_session + + async for session in get_session(): + try: + session.add(user) + await session.commit() + await session.refresh(user) + except Exception as e: + await session.rollback() + raise e + return user \ No newline at end of file diff --git a/pkg/JWT/jwt.py b/pkg/JWT/jwt.py index 35dde07..4fcca1c 100644 --- a/pkg/JWT/jwt.py +++ b/pkg/JWT/jwt.py @@ -1,4 +1,5 @@ from fastapi.security import OAuth2PasswordBearer +from models.setting import Setting oauth2_scheme = OAuth2PasswordBearer( scheme_name='获取 JWT Bearer 令牌', @@ -6,4 +7,14 @@ oauth2_scheme = OAuth2PasswordBearer( tokenUrl="/api/user/session", ) -SECRET_KEY = '' \ No newline at end of file +SECRET_KEY = '' + +async def load_secret_key() -> None: + """ + 从数据库读取 JWT 的密钥。 + + :param key: 用于加密和解密 JWT 的密钥 + :type key: str + """ + global SECRET_KEY + SECRET_KEY = await Setting.get(type='auth', name='secret_key') \ No newline at end of file diff --git a/routers/controllers/admin.py b/routers/controllers/admin.py index cd91549..2c17cff 100644 --- a/routers/controllers/admin.py +++ b/routers/controllers/admin.py @@ -70,7 +70,7 @@ def router_admin_get_summary() -> ResponseModel: Returns: ResponseModel: 包含站点概况信息的响应模型。 """ - ... + pass @admin_router.get( path='/news', @@ -85,7 +85,7 @@ def router_admin_get_news() -> ResponseModel: Returns: ResponseModel: 包含社区新闻信息的响应模型。 """ - ... + pass @admin_router.patch( path='/settings', @@ -100,7 +100,7 @@ def router_admin_update_settings() -> ResponseModel: Returns: ResponseModel: 包含更新结果的响应模型。 """ - ... + pass @admin_router.get( path='/settings', @@ -115,7 +115,7 @@ def router_admin_get_settings() -> ResponseModel: Returns: ResponseModel: 包含站点设置的响应模型。 """ - ... + pass @admin_group_router.get( path='/', @@ -130,7 +130,7 @@ def router_admin_get_groups() -> ResponseModel: Returns: ResponseModel: 包含用户组列表的响应模型。 """ - ... + pass @admin_group_router.get( path='/{group_id}', @@ -148,7 +148,7 @@ def router_admin_get_group(group_id: int) -> ResponseModel: Returns: ResponseModel: 包含用户组信息的响应模型。 """ - ... + pass @admin_group_router.get( path='/list/{group_id}', @@ -172,7 +172,7 @@ def router_admin_get_group_members( Returns: ResponseModel: 包含用户组成员列表的响应模型。 """ - ... + pass @admin_group_router.post( path='/', @@ -187,7 +187,7 @@ def router_admin_create_group() -> ResponseModel: Returns: ResponseModel: 包含创建结果的响应模型。 """ - ... + pass @admin_group_router.patch( path='/{group_id}', @@ -205,7 +205,7 @@ def router_admin_update_group(group_id: int) -> ResponseModel: Returns: ResponseModel: 包含更新结果的响应模型。 """ - ... + pass @admin_group_router.delete( path='/{group_id}', @@ -223,7 +223,7 @@ def router_admin_delete_group(group_id: int) -> ResponseModel: Returns: ResponseModel: 包含删除结果的响应模型。 """ - ... + pass @admin_user_router.get( path='/info/{user_id}', @@ -241,7 +241,7 @@ def router_admin_get_user(user_id: int) -> ResponseModel: Returns: ResponseModel: 包含用户信息的响应模型。 """ - ... + pass @admin_user_router.get( path='/list', @@ -263,7 +263,7 @@ def router_admin_get_users( Returns: ResponseModel: 包含用户列表的响应模型。 """ - ... + pass @admin_user_router.post( path='/create', @@ -278,7 +278,7 @@ def router_admin_create_user() -> ResponseModel: Returns: ResponseModel: 包含创建结果的响应模型。 """ - ... + pass @admin_user_router.patch( path='/{user_id}', @@ -296,7 +296,7 @@ def router_admin_update_user(user_id: int) -> ResponseModel: Returns: ResponseModel: 包含更新结果的响应模型。 """ - ... + pass @admin_user_router.delete( path='/{user_id}', @@ -314,7 +314,7 @@ def router_admin_delete_user(user_id: int) -> ResponseModel: Returns: ResponseModel: 包含删除结果的响应模型。 """ - ... + pass @admin_user_router.post( path='/calibrate/{user_id}', @@ -323,7 +323,7 @@ def router_admin_delete_user(user_id: int) -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_admin_calibrate_storage(): - ... + pass @admin_file_router.get( path='/list', @@ -338,7 +338,7 @@ def router_admin_get_file_list() -> ResponseModel: Returns: ResponseModel: 包含文件列表的响应模型。 """ - ... + pass @admin_file_router.get( path='/preview/{file_id}', @@ -356,7 +356,7 @@ def router_admin_preview_file(file_id: int) -> ResponseModel: Returns: ResponseModel: 包含文件预览内容的响应模型。 """ - ... + pass @admin_file_router.patch( path='/ban/{file_id}', @@ -376,7 +376,7 @@ def router_admin_ban_file(file_id: int) -> ResponseModel: Returns: ResponseModel: 包含删除结果的响应模型。 """ - ... + pass @admin_file_router.delete( path='/{file_id}', @@ -394,7 +394,7 @@ def router_admin_delete_file(file_id: int) -> ResponseModel: Returns: ResponseModel: 包含删除结果的响应模型。 """ - ... + pass @admin_aria2_router.post( path='/test', @@ -403,7 +403,7 @@ def router_admin_delete_file(file_id: int) -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_admin_aira2_test() -> ResponseModel: - ... + pass @admin_policy_router.get( path='/list', @@ -412,7 +412,7 @@ def router_admin_aira2_test() -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_policy_list() -> ResponseModel: - ... + pass @admin_policy_router.post( path='/test/path', @@ -421,7 +421,7 @@ def router_policy_list() -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_policy_test_path() -> ResponseModel: - ... + pass @admin_policy_router.post( path='/test/slave', @@ -430,7 +430,7 @@ def router_policy_test_path() -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_policy_test_slave() -> ResponseModel: - ... + pass @admin_policy_router.post( path='/', @@ -439,7 +439,7 @@ def router_policy_test_slave() -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_policy_add_policy() -> ResponseModel: - ... + pass @admin_policy_router.post( path='/cors', @@ -448,7 +448,7 @@ def router_policy_add_policy() -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_policy_add_cors() -> ResponseModel: - ... + pass @admin_policy_router.post( path='/scf', @@ -457,7 +457,7 @@ def router_policy_add_cors() -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_policy_add_scf() -> ResponseModel: - ... + pass @admin_policy_router.get( path='/{id}/oauth', @@ -466,7 +466,7 @@ def router_policy_add_scf() -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_policy_onddrive_oauth() -> ResponseModel: - ... + pass @admin_policy_router.get( path='/{id}', @@ -475,7 +475,7 @@ def router_policy_onddrive_oauth() -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_policy_get_policy() -> ResponseModel: - ... + pass @admin_policy_router.delete( path='/{id}', @@ -484,4 +484,4 @@ def router_policy_get_policy() -> ResponseModel: dependencies=[Depends(AdminRequired)] ) def router_policy_delete_policy() -> ResponseModel: - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/aria2.py b/routers/controllers/aria2.py index d9799c5..91ed33d 100644 --- a/routers/controllers/aria2.py +++ b/routers/controllers/aria2.py @@ -20,7 +20,7 @@ def router_aria2_url() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the URL download task. """ - ... + pass @aria2_router.post( path='/torrent/{id}', @@ -38,7 +38,7 @@ def router_aria2_torrent(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the torrent download task. """ - ... + pass @aria2_router.put( path='/select/{gid}', @@ -56,7 +56,7 @@ def router_aria2_select(gid: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the re-selection of files. """ - ... + pass @aria2_router.delete( path='/task/{gid}', @@ -74,7 +74,7 @@ def router_aria2_delete(gid: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the deletion of the download task. """ - ... + pass @aria2_router.get( '/downloading', @@ -89,7 +89,7 @@ def router_aria2_downloading() -> ResponseModel: Returns: ResponseModel: A model containing the response data for currently downloading tasks. """ - ... + pass @aria2_router.get( path='/finished', @@ -104,4 +104,4 @@ def router_aria2_finished() -> ResponseModel: Returns: ResponseModel: A model containing the response data for finished tasks. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/callback.py b/routers/controllers/callback.py index 019f4c2..9a9c88a 100644 --- a/routers/controllers/callback.py +++ b/routers/controllers/callback.py @@ -39,7 +39,7 @@ def router_callback_qq() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the QQ OAuth callback. """ - ... + pass @oauth_router.post( path='/github', @@ -53,7 +53,7 @@ def router_callback_github() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the GitHub OAuth callback. """ - ... + pass @pay_router.post( path='/alipay', @@ -67,7 +67,7 @@ def router_callback_alipay() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the Alipay payment callback. """ - ... + pass @pay_router.post( path='/wechat', @@ -81,7 +81,7 @@ def router_callback_wechat() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the WeChat Pay payment callback. """ - ... + pass @pay_router.post( path='/stripe', @@ -95,7 +95,7 @@ def router_callback_stripe() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the Stripe payment callback. """ - ... + pass @pay_router.get( path='/easypay', @@ -109,7 +109,7 @@ def router_callback_easypay() -> PlainTextResponse: Returns: PlainTextResponse: A response containing the payment status for the EasyPay payment callback. """ - ... + pass # return PlainTextResponse("success", status_code=200) @pay_router.get( @@ -128,7 +128,7 @@ def router_callback_custom(order_no: str, id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the custom payment callback. """ - ... + pass @upload_router.post( path='/remote/{session_id}/{key}', @@ -146,7 +146,7 @@ def router_callback_remote(session_id: str, key: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the remote upload callback. """ - ... + pass @upload_router.post( path='/qiniu/{session_id}', @@ -163,7 +163,7 @@ def router_callback_qiniu(session_id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the Qiniu Cloud upload callback. """ - ... + pass @upload_router.post( path='/tencent/{session_id}', @@ -180,7 +180,7 @@ def router_callback_tencent(session_id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the Tencent Cloud upload callback. """ - ... + pass @upload_router.post( path='/aliyun/{session_id}', @@ -197,7 +197,7 @@ def router_callback_aliyun(session_id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the Aliyun upload callback. """ - ... + pass @upload_router.post( path='/upyun/{session_id}', @@ -214,7 +214,7 @@ def router_callback_upyun(session_id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the Upyun upload callback. """ - ... + pass @upload_router.post( path='/aws/{session_id}', @@ -231,7 +231,7 @@ def router_callback_aws(session_id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the AWS S3 upload callback. """ - ... + pass @upload_router.post( path='/onedrive/finish/{session_id}', @@ -248,7 +248,7 @@ def router_callback_onedrive_finish(session_id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the OneDrive upload completion callback. """ - ... + pass @upload_router.get( path='/ondrive/auth', @@ -262,7 +262,7 @@ def router_callback_onedrive_auth() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the OneDrive authorization callback. """ - ... + pass @upload_router.get( path='/google/auth', @@ -276,4 +276,4 @@ def router_callback_google_auth() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the Google OAuth completion callback. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/directory.py b/routers/controllers/directory.py index 77e43f6..c640364 100644 --- a/routers/controllers/directory.py +++ b/routers/controllers/directory.py @@ -20,7 +20,7 @@ def router_directory_create() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the directory creation. """ - ... + pass @directory_router.get( path='/{path:path}', @@ -38,4 +38,4 @@ def router_directory_get(path: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the directory contents. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/file.py b/routers/controllers/file.py index 48c7178..f36219c 100644 --- a/routers/controllers/file.py +++ b/routers/controllers/file.py @@ -1,4 +1,5 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, UploadFile +from fastapi.responses import FileResponse from middleware.auth import SignRequired from models.response import ResponseModel @@ -17,7 +18,7 @@ file_upload_router = APIRouter( summary='文件外链(直接输出文件数据)', description='Get file external link endpoint.', ) -def router_file_get(id: str, name: str) -> ResponseModel: +def router_file_get(id: str, name: str) -> FileResponse: """ Get file external link endpoint. @@ -26,9 +27,9 @@ def router_file_get(id: str, name: str) -> ResponseModel: name (str): The name of the file. Returns: - ResponseModel: A model containing the response data for the file. + FileResponse: A response containing the file data. """ - ... + pass @file_router.get( path='/source/{id}/{name}', @@ -46,7 +47,7 @@ def router_file_source(id: str, name: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file with a redirect. """ - ... + pass @file_upload_router.get( path='/download/{id}', @@ -63,7 +64,7 @@ def router_file_download(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file download. """ - ... + pass @file_upload_router.get( path='/archive/{sessionID}/archive.zip', @@ -80,14 +81,14 @@ def router_file_archive_download(sessionID: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the archived files download. """ - ... + pass @file_upload_router.post( path='/{sessionID}/{index}', summary='文件上传', description='File upload endpoint.', ) -def router_file_upload(sessionID: str, index: int) -> ResponseModel: +def router_file_upload(sessionID: str, index: int, file: UploadFile) -> ResponseModel: """ File upload endpoint. @@ -98,7 +99,7 @@ def router_file_upload(sessionID: str, index: int) -> ResponseModel: Returns: ResponseModel: A model containing the response data. """ - ... + pass @file_upload_router.put( path='/', @@ -113,7 +114,7 @@ def router_file_upload_session() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the upload session. """ - ... + pass @file_upload_router.delete( path='/{sessionID}', @@ -131,7 +132,7 @@ def router_file_upload_session_delete(sessionID: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the deletion. """ - ... + pass @file_upload_router.delete( path='/', @@ -146,7 +147,7 @@ def router_file_upload_session_clear() -> ResponseModel: Returns: ResponseModel: A model containing the response data for clearing all sessions. """ - ... + pass @file_router.put( path='/update/{id}', @@ -164,7 +165,7 @@ def router_file_update(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file update. """ - ... + pass @file_router.post( path='/create', @@ -179,7 +180,7 @@ def router_file_create() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file creation. """ - ... + pass @file_router.put( path='/download/{id}', @@ -197,7 +198,7 @@ def router_file_download(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file download session. """ - ... + pass @file_router.get( path='/preview/{id}', @@ -215,7 +216,7 @@ def router_file_preview(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file preview. """ - ... + pass @file_router.get( path='/content/{id}', @@ -233,7 +234,7 @@ def router_file_content(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the text file content. """ - ... + pass @file_router.get( path='/doc/{id}', @@ -251,7 +252,7 @@ def router_file_doc(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the Office document preview URL. """ - ... + pass @file_router.get( path='/thumb/{id}', @@ -269,7 +270,7 @@ def router_file_thumb(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file thumbnail. """ - ... + pass @file_router.post( path='/source/{id}', @@ -287,7 +288,7 @@ def router_file_source(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file external link. """ - ... + pass @file_router.post( path='/archive', @@ -305,7 +306,7 @@ def router_file_archive(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the archived files. """ - ... + pass @file_router.post( path='/compress', @@ -323,7 +324,7 @@ def router_file_compress(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file compression task. """ - ... + pass @file_router.post( path='/decompress', @@ -341,7 +342,7 @@ def router_file_decompress(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file extraction task. """ - ... + pass @file_router.post( path='/relocate', @@ -359,7 +360,7 @@ def router_file_relocate(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file relocation task. """ - ... + pass @file_router.get( path='/search/{type}/{keyword}', @@ -378,4 +379,4 @@ def router_file_search(type: str, keyword: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the file search. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/object.py b/routers/controllers/object.py index 0724c02..245aa72 100644 --- a/routers/controllers/object.py +++ b/routers/controllers/object.py @@ -20,7 +20,7 @@ def router_object_delete() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the object deletion. """ - ... + pass @object_router.patch( path='/', @@ -35,7 +35,7 @@ def router_object_move() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the object move. """ - ... + pass @object_router.post( path='/copy', @@ -50,7 +50,7 @@ def router_object_copy() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the object copy. """ - ... + pass @object_router.post( path='/rename', @@ -65,7 +65,7 @@ def router_object_rename() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the object rename. """ - ... + pass @object_router.get( path='/property/{id}', @@ -83,4 +83,4 @@ def router_object_property(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the object properties. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/share.py b/routers/controllers/share.py index 98b7ab1..5a20094 100644 --- a/routers/controllers/share.py +++ b/routers/controllers/share.py @@ -23,7 +23,7 @@ def router_share_get(info: str, id: str) -> ResponseModel: Returns: dict: A dictionary containing shared content information. """ - ... + pass @share_router.put( path='/download/{id}', @@ -40,7 +40,7 @@ def router_share_download(id: str) -> ResponseModel: Returns: dict: A dictionary containing download session information. """ - ... + pass @share_router.get( path='preview/{id}', @@ -57,7 +57,7 @@ def router_share_preview(id: str) -> ResponseModel: Returns: dict: A dictionary containing preview information. """ - ... + pass @share_router.get( path='/doc/{id}', @@ -74,7 +74,7 @@ def router_share_doc(id: str) -> ResponseModel: Returns: dict: A dictionary containing the document preview URL. """ - ... + pass @share_router.get( path='/content/{id}', @@ -91,7 +91,7 @@ def router_share_content(id: str) -> ResponseModel: Returns: str: The content of the text file. """ - ... + pass @share_router.get( path='/list/{id}/{path:path}', @@ -109,7 +109,7 @@ def router_share_list(id: str, path: str = '') -> ResponseModel: Returns: dict: A dictionary containing directory listing information. """ - ... + pass @share_router.get( path='/search/{id}/{type}/{keywords}', @@ -128,7 +128,7 @@ def router_share_search(id: str, type: str, keywords: str) -> ResponseModel: Returns: dict: A dictionary containing search results. """ - ... + pass @share_router.post( path='/archive/{id}', @@ -145,7 +145,7 @@ def router_share_archive(id: str) -> ResponseModel: Returns: dict: A dictionary containing archive download information. """ - ... + pass @share_router.get( path='/readme/{id}', @@ -162,7 +162,7 @@ def router_share_readme(id: str) -> ResponseModel: Returns: str: The content of the README file. """ - ... + pass @share_router.get( path='/thumb/{id}/{file}', @@ -180,7 +180,7 @@ def router_share_thumb(id: str, file: str) -> ResponseModel: Returns: str: A Base64 encoded string of the thumbnail image. """ - ... + pass @share_router.post( path='/report/{id}', @@ -197,7 +197,7 @@ def router_share_report(id: str) -> ResponseModel: Returns: dict: A dictionary containing report submission information. """ - ... + pass @share_router.get( path='/search', @@ -215,7 +215,7 @@ def router_share_search_public(keywords: str, type: str = 'all') -> ResponseMode Returns: dict: A dictionary containing search results for public shares. """ - ... + pass ##################### # 需要登录的接口 @@ -234,7 +234,7 @@ def router_share_create() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the new share creation. """ - ... + pass @share_router.get( path='/', @@ -249,7 +249,7 @@ def router_share_list() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the list of shares. """ - ... + pass @share_router.post( path='/save/{id}', @@ -267,7 +267,7 @@ def router_share_save(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the saved share. """ - ... + pass @share_router.patch( path='/{id}', @@ -285,7 +285,7 @@ def router_share_update(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the updated share. """ - ... + pass @share_router.delete( path='/{id}', @@ -303,4 +303,4 @@ def router_share_delete(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the deleted share. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/site.py b/routers/controllers/site.py index ae77c4d..9647abd 100644 --- a/routers/controllers/site.py +++ b/routers/controllers/site.py @@ -34,7 +34,7 @@ def router_site_captcha(): Returns: str: A Base64 encoded string of the captcha image. """ - ... + pass @site_router.get( path='/config', diff --git a/routers/controllers/slave.py b/routers/controllers/slave.py index 093d6f8..a278cb7 100644 --- a/routers/controllers/slave.py +++ b/routers/controllers/slave.py @@ -44,7 +44,7 @@ def router_slave_post(data: str) -> ResponseModel: Returns: ResponseModel: A response model indicating success. """ - ... + pass @slave_router.get( path='/get/{speed}/{path}/{name}', @@ -62,7 +62,7 @@ def router_slave_download(speed: int, path: str, name: str) -> ResponseModel: Returns: ResponseModel: A response model containing download information. """ - ... + pass @slave_router.get( path='/download/{sign}', @@ -80,7 +80,7 @@ def router_slave_download_by_sign(sign: str) -> FileResponse: Returns: FileResponse: A response containing the file to be downloaded. """ - ... + pass @slave_router.get( path='/source/{speed}/{path}/{name}', @@ -100,7 +100,7 @@ def router_slave_source(speed: int, path: str, name: str) -> ResponseModel: Returns: ResponseModel: A response model containing the external link for the file. """ - ... + pass @slave_router.get( path='/source/{sign}', @@ -118,7 +118,7 @@ def router_slave_source_by_sign(sign: str) -> FileResponse: Returns: FileResponse: A response containing the file to be retrieved. """ - ... + pass @slave_router.get( path='/thumb/{id}', @@ -136,7 +136,7 @@ def router_slave_thumb(id: str) -> ResponseModel: Returns: ResponseModel: A response model containing the Base64 encoded thumbnail image. """ - ... + pass @slave_router.delete( path='/delete', @@ -154,7 +154,7 @@ def router_slave_delete(path: str) -> ResponseModel: Returns: ResponseModel: A response model indicating success or failure of the deletion. """ - ... + pass @slave_aria2_router.post( path='/test', @@ -166,7 +166,7 @@ def router_slave_aria2_test() -> ResponseModel: """ Test the connection to the Aria2 service from the slave. """ - ... + pass @slave_aria2_router.get( path='/get/{gid}', @@ -184,7 +184,7 @@ def router_slave_aria2_get(gid: str = None) -> ResponseModel: Returns: ResponseModel: A response model containing the task information. """ - ... + pass @slave_aria2_router.post( path='/add', @@ -204,7 +204,7 @@ def router_slave_aria2_add(gid: str, url: str, options: dict = None) -> Response Returns: ResponseModel: A response model indicating success or failure of the task addition. """ - ... + pass @slave_aria2_router.delete( path='/remove/{gid}', @@ -222,4 +222,4 @@ def router_slave_aria2_remove(gid: str) -> ResponseModel: Returns: ResponseModel: A response model indicating success or failure of the task removal. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/tag.py b/routers/controllers/tag.py index 650d2d4..200dd73 100644 --- a/routers/controllers/tag.py +++ b/routers/controllers/tag.py @@ -20,7 +20,7 @@ def router_tag_create_filter() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the created tag. """ - ... + pass @tag_router.post( path='/link', @@ -35,7 +35,7 @@ def router_tag_create_link() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the created tag. """ - ... + pass @tag_router.delete( path='/{id}', @@ -53,4 +53,4 @@ def router_tag_delete(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the deletion operation. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/user.py b/routers/controllers/user.py index 81a5f06..55fc19d 100644 --- a/routers/controllers/user.py +++ b/routers/controllers/user.py @@ -25,7 +25,7 @@ def router_user_session() -> ResponseModel: Returns: dict: A dictionary containing user session information. """ - ... + pass @user_router.post( path='/', @@ -39,7 +39,7 @@ def router_user_register() -> ResponseModel: Returns: dict: A dictionary containing user registration information. """ - ... + pass @user_router.post( path='/2fa', @@ -53,7 +53,7 @@ def router_user_2fa() -> ResponseModel: Returns: dict: A dictionary containing two-factor authentication information. """ - ... + pass @user_router.post( path='/reset', @@ -67,7 +67,7 @@ def router_user_reset() -> ResponseModel: Returns: dict: A dictionary containing information about the password reset email. """ - ... + pass @user_router.patch( path='/reset', @@ -81,7 +81,7 @@ def router_user_reset_patch() -> ResponseModel: Returns: dict: A dictionary containing information about the password reset. """ - ... + pass @user_router.get( path='/qq', @@ -95,7 +95,7 @@ def router_user_qq() -> ResponseModel: Returns: dict: A dictionary containing QQ login initialization information. """ - ... + pass @user_router.get( path='/activate/{id}', @@ -112,7 +112,7 @@ def router_user_activate(id: str) -> ResponseModel: Returns: dict: A dictionary containing activation information. """ - ... + pass @user_router.get( path='authn/{username}', @@ -129,7 +129,7 @@ def router_user_authn(username: str) -> ResponseModel: Returns: dict: A dictionary containing WebAuthn initialization information. """ - ... + pass @user_router.post( path='authn/finish/{username}', @@ -146,7 +146,7 @@ def router_user_authn_finish(username: str) -> ResponseModel: Returns: dict: A dictionary containing WebAuthn login information. """ - ... + pass @user_router.get( path='/profile/{id}', @@ -163,7 +163,7 @@ def router_user_profile(id: str) -> ResponseModel: Returns: dict: A dictionary containing user profile information. """ - ... + pass @user_router.get( path='/avatar/{id}/{size}', @@ -181,7 +181,7 @@ def router_user_avatar(id: str, size: int = 128) -> ResponseModel: Returns: str: A Base64 encoded string of the user avatar image. """ - ... + pass ##################### # 需要登录的接口 @@ -200,7 +200,7 @@ def router_user_me() -> ResponseModel: Returns: dict: A dictionary containing user information. """ - ... + pass @user_router.get( path='/storage', @@ -215,7 +215,7 @@ def router_user_storage() -> ResponseModel: Returns: dict: A dictionary containing user storage information. """ - ... + pass @user_router.put( path='/authn/start', @@ -230,7 +230,7 @@ def router_user_authn_start() -> ResponseModel: Returns: dict: A dictionary containing WebAuthn initialization information. """ - ... + pass @user_router.put( path='/authn/finish', @@ -245,7 +245,7 @@ def router_user_authn_finish() -> ResponseModel: Returns: dict: A dictionary containing WebAuthn login information. """ - ... + pass @user_settings_router.get( path='/policies', @@ -259,7 +259,7 @@ def router_user_settings_policies() -> ResponseModel: Returns: dict: A dictionary containing available storage policies for the user. """ - ... + pass @user_settings_router.get( path='/nodes', @@ -274,7 +274,7 @@ def router_user_settings_nodes() -> ResponseModel: Returns: dict: A dictionary containing available nodes for the user. """ - ... + pass @user_settings_router.get( path='/tasks', @@ -289,7 +289,7 @@ def router_user_settings_tasks() -> ResponseModel: Returns: dict: A dictionary containing the user's task queue information. """ - ... + pass @user_settings_router.get( path='/', @@ -304,7 +304,7 @@ def router_user_settings() -> ResponseModel: Returns: dict: A dictionary containing the user's current settings. """ - ... + pass @user_settings_router.post( path='/avatar', @@ -319,7 +319,7 @@ def router_user_settings_avatar() -> ResponseModel: Returns: dict: A dictionary containing the result of the avatar upload. """ - ... + pass @user_settings_router.put( path='/avatar', @@ -334,7 +334,7 @@ def router_user_settings_avatar_gravatar() -> ResponseModel: Returns: dict: A dictionary containing the result of setting the Gravatar avatar. """ - ... + pass @user_settings_router.patch( path='/{option}', @@ -352,7 +352,7 @@ def router_user_settings_patch(option: str) -> ResponseModel: Returns: dict: A dictionary containing the result of the settings update. """ - ... + pass @user_settings_router.get( path='/2fa', @@ -367,4 +367,4 @@ def router_user_settings_2fa() -> ResponseModel: Returns: dict: A dictionary containing two-factor authentication setup information. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/vas.py b/routers/controllers/vas.py index 8e98569..0308c74 100644 --- a/routers/controllers/vas.py +++ b/routers/controllers/vas.py @@ -20,7 +20,7 @@ def router_vas_pack() -> ResponseModel: Returns: ResponseModel: A model containing the response data for storage packs and quotas. """ - ... + pass @vas_router.get( path='/product', @@ -35,7 +35,7 @@ def router_vas_product() -> ResponseModel: Returns: ResponseModel: A model containing the response data for products and payment information. """ - ... + pass @vas_router.post( path='/order', @@ -50,7 +50,7 @@ def router_vas_order() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the created order. """ - ... + pass @vas_router.get( path='/order/{id}', @@ -68,7 +68,7 @@ def router_vas_order_get(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the specified order. """ - ... + pass @vas_router.get( path='/redeem', @@ -86,7 +86,7 @@ def router_vas_redeem(code: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the specified redemption code. """ - ... + pass @vas_router.post( path='/redeem', @@ -101,4 +101,4 @@ def router_vas_redeem_post() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the redeemed code. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/routers/controllers/webdav.py b/routers/controllers/webdav.py index 489a6e7..5656c99 100644 --- a/routers/controllers/webdav.py +++ b/routers/controllers/webdav.py @@ -21,7 +21,7 @@ def router_webdav_accounts() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the account information. """ - ... + pass @webdav_router.post( path='/accounts', @@ -36,7 +36,7 @@ def router_webdav_create_account() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the created account. """ - ... + pass @webdav_router.delete( path='/accounts/{id}', @@ -54,7 +54,7 @@ def router_webdav_delete_account(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the deletion operation. """ - ... + pass @webdav_router.post( path='/mount', @@ -69,7 +69,7 @@ def router_webdav_create_mount() -> ResponseModel: Returns: ResponseModel: A model containing the response data for the created mount point. """ - ... + pass @webdav_router.delete( path='/mount/{id}', @@ -87,7 +87,7 @@ def router_webdav_delete_mount(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the deletion operation. """ - ... + pass @webdav_router.patch( path='accounts/{id}', @@ -105,4 +105,4 @@ def router_webdav_update_account(id: str) -> ResponseModel: Returns: ResponseModel: A model containing the response data for the updated account. """ - ... \ No newline at end of file + pass \ No newline at end of file diff --git a/service/user/login.py b/service/user/login.py new file mode 100644 index 0000000..8827549 --- /dev/null +++ b/service/user/login.py @@ -0,0 +1,15 @@ +from models.setting import Setting + +async def login( + username: str, + password: str +): + """ + """ + + isCaptchaRequired = await Setting.get(type='auth', name='login_captcha', type=bool) + captchaType = await Setting.get(type='auth', name='captcha_type', type=str) + + # [TODO] 验证码校验 + + \ No newline at end of file diff --git a/tests/test_database.py b/tests/test_database.py index 463abd7..9f852c4 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -25,52 +25,4 @@ async def test_initialize_db(): await database.init_db(url='sqlite:///:memory:') - await migration.init_default_settings() - -@pytest.mark.asyncio -async def test_add_settings(): - """测试数据库的增删改查""" - from models import database - from models.setting import Setting - - await database.init_db(url='sqlite:///:memory:') - - # 测试增 Create - await Setting.add( - type='example_type', - name='example_name', - value='example_value') - - # 测试查 Read - setting = await Setting.get( - type='example_type', - name='example_name') - - assert setting is not None, "设置项应该存在" - assert setting.value == 'example_value', "设置值不匹配" - - # 测试改 Update - await Setting.set( - type='example_type', - name='example_name', - value='updated_value') - - after_update_setting = await Setting.get( - type='example_type', - name='example_name' - ) - - assert after_update_setting is not None, "设置项应该存在" - assert after_update_setting.value == 'updated_value', "更新后的设置值不匹配" - - # 测试删 Delete - await Setting.delete( - type='example_type', - name='example_name') - - after_delete_setting = await Setting.get( - type='example_type', - name='example_name' - ) - - assert after_delete_setting is None, "设置项应该被删除" \ No newline at end of file + await migration.init_default_settings() \ No newline at end of file diff --git a/tests/test_db_settings.py b/tests/test_db_settings.py new file mode 100644 index 0000000..62c73c4 --- /dev/null +++ b/tests/test_db_settings.py @@ -0,0 +1,49 @@ +import pytest + +@pytest.mark.asyncio +async def test_settings_curd(): + """测试数据库的增删改查""" + from models import database + from models.setting import Setting + + await database.init_db(url='sqlite:///:memory:') + + # 测试增 Create + await Setting.add( + type='example_type', + name='example_name', + value='example_value') + + # 测试查 Read + setting = await Setting.get( + type='example_type', + name='example_name') + + assert setting is not None, "设置项应该存在" + assert setting == 'example_value', "设置值不匹配" + + # 测试改 Update + await Setting.set( + type='example_type', + name='example_name', + value='updated_value') + + after_update_setting = await Setting.get( + type='example_type', + name='example_name' + ) + + assert after_update_setting is not None, "设置项应该存在" + assert after_update_setting == 'updated_value', "更新后的设置值不匹配" + + # 测试删 Delete + await Setting.delete( + type='example_type', + name='example_name') + + after_delete_setting = await Setting.get( + type='example_type', + name='example_name' + ) + + assert after_delete_setting is None, "设置项应该被删除" \ No newline at end of file diff --git a/tests/test_db_user.py b/tests/test_db_user.py new file mode 100644 index 0000000..698d32d --- /dev/null +++ b/tests/test_db_user.py @@ -0,0 +1,29 @@ +import pytest + +@pytest.mark.asyncio +async def test_user_curd(): + """测试数据库的增删改查""" + from models import database + from models.group import Group + from models.user import User + + await database.init_db(url='sqlite:///:memory:') + + # 新建一个测试用户组 + test_group = Group(name='test_group') + created_group = await Group.create(test_group) + + test_user = User( + email='test_user', + password='test_password', + group_id=created_group.id + ) + + # 测试增 Create + created_user = await User.create(test_user) + + # 验证用户是否存在 + assert created_user.id is not None + assert created_user.email == 'test_user' + assert created_user.password == 'test_password' + assert created_user.group_id == created_group.id \ No newline at end of file