From b364b740ca2a006362197048d81d44a321bc24f8 Mon Sep 17 00:00:00 2001 From: Yuerchu Date: Thu, 27 Nov 2025 21:22:40 +0800 Subject: [PATCH] feat: Enhance models and README with additional features and improvements - Updated README to include KodBox in project vision. - Added model descriptions for clarity in Download, File, Folder, Group, Node, Order, Policy, Redeem, Report, Request, Response, Setting, Share, SourceLink, StoragePack, Tag, Task, User, and WebDAV. - Changed optional fields from Optional[...] to the new union type syntax (e.g., str | None). - Improved foreign key references in models for consistency. - Refactored relationships in models to use singular forms where appropriate. - Updated login service to reflect changes in request model types. --- README.md | 8 ++--- models/download.py | 17 ++++++----- models/file.py | 22 +++++++------- models/folder.py | 7 ++--- models/group.py | 66 +++++++++++++++++++++--------------------- models/node.py | 13 ++++----- models/order.py | 13 ++++----- models/policy.py | 21 +++++++------- models/redeem.py | 9 ++---- models/report.py | 11 ++++--- models/request.py | 4 +-- models/response.py | 12 ++++---- models/setting.py | 13 +++++---- models/share.py | 17 ++++++----- models/source_link.py | 5 ++-- models/storage_pack.py | 8 ++--- models/tag.py | 11 +++---- models/task.py | 9 +++--- models/user.py | 26 ++++++++++++----- models/webdav.py | 7 +++-- service/user/login.py | 4 +-- 21 files changed, 156 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 7e1ea5f..c56e934 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ 本来此项目并没有这么快开始,但由于我曾经使用过的某个类似产品的作者吃相实在难看,故决定自己实现一个。 -此项目的愿景是集百家之长(Cloudreve + Alist + FnOS)。你也可以考虑付费支持我们的发展 -> `DiskNext Pro`. +此项目的愿景是集百家之长(Cloudreve + Alist + FnOS + KodBox)。你也可以考虑付费支持我们的发展 -> `DiskNext Pro`. 目前正处于 `OMEGA` 实验阶段,比 `Alpha` 版还更早期,仅供测试。 @@ -25,12 +25,12 @@ - 在线预览/编辑多种文件,包括但不限于视频、图片、音频、PDF、ePub、Office、Markdown、图表等 - 自定义主题色、深浅色主题、PWA、i18n -## :alembic: 技术栈 +## ⚗️ 技术栈 * [Python](https://www.python.org/) + [FastAPI](https://fastapi.tiangolo.com/) -## :scroll: 许可证 +## 📜 许可证 -GPL V3 \ No newline at end of file +GPL V3 diff --git a/models/download.py b/models/download.py index c5be7c5..d07600a 100644 --- a/models/download.py +++ b/models/download.py @@ -11,7 +11,8 @@ class DownloadBase(SQLModelBase): pass class Download(DownloadBase, UUIDTableBase, table=True): - __tablename__ = 'downloads' + """离线下载任务模型""" + __table_args__ = ( UniqueConstraint("node_id", "g_id", name="uq_download_node_gid"), ) @@ -21,18 +22,18 @@ class Download(DownloadBase, UUIDTableBase, table=True): source: str = Field(description="来源URL或标识") total_size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="总大小(字节)") downloaded_size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="已下载大小(字节)") - g_id: Optional[str] = Field(default=None, index=True, description="Aria2 GID") + g_id: str | None = Field(default=None, index=True, description="Aria2 GID") speed: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="下载速度 (bytes/s)") - parent: Optional[str] = Field(default=None, description="父任务标识") - attrs: Optional[str] = Field(default=None, description="额外属性 (JSON格式)") + parent: str | None = Field(default=None, description="父任务标识") + attrs: str | None = Field(default=None, description="额外属性 (JSON格式)") # attrs 示例: {"gid":"65c5faf38374cc63","status":"removed","totalLength":"0","completedLength":"0","uploadLength":"0","bitfield":"","downloadSpeed":"0","uploadSpeed":"0","infoHash":"ca159db2b1e78f6e95fd972be72251f967f639d4","numSeeders":"0","seeder":"","pieceLength":"16384","numPieces":"0","connections":"0","errorCode":"31","errorMessage":"","followedBy":null,"belongsTo":"","dir":"/data/ccaaDown/aria2/7a208304-9126-46d2-ba47-a6959f236a07","files":[{"index":"1","path":"[METADATA]zh-cn_windows_11_consumer_editions_version_21h2_updated_aug_2022_x64_dvd_a29983d5.iso","length":"0","completedLength":"0","selected":"true","uris":[]}],"bittorrent":{"announceList":[["udp://tracker.opentrackr.org:1337/announce"],["udp://9.rarbg.com:2810/announce"],["udp://tracker.openbittorrent.com:6969/announce"],["https://opentracker.i2p.rocks:443/announce"],["http://tracker.openbittorrent.com:80/announce"],["udp://open.stealth.si:80/announce"],["udp://tracker.torrent.eu.org:451/announce"],["udp://exodus.desync.com:6969/announce"],["udp://tracker.tiny-vps.com:6969/announce"],["udp://tracker.pomf.se:80/announce"],["udp://tracker.moeking.me:6969/announce"],["udp://tracker.dler.org:6969/announce"],["udp://open.demonii.com:1337/announce"],["udp://explodie.org:6969/announce"],["udp://chouchou.top:8080/announce"],["udp://bt.oiyo.tk:6969/announce"],["https://tracker.nanoha.org:443/announce"],["https://tracker.lilithraws.org:443/announce"],["http://tracker3.ctix.cn:8080/announce"],["http://tracker.nucozer-tracker.ml:2710/announce"]],"comment":"","creationDate":0,"mode":"","info":{"name":""}}} - error: Optional[str] = Field(default=None, description="错误信息") + error: str | None = Field(default=None, description="错误信息") dst: str = Field(description="目标存储路径") # 外键 - user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") - task_id: Optional[int] = Field(default=None, foreign_key="tasks.id", index=True, description="关联的任务ID") - node_id: int = Field(foreign_key="nodes.id", index=True, description="执行下载的节点ID") + user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID") + task_id: int | None = Field(default=None, foreign_key="task.id", index=True, description="关联的任务ID") + node_id: int = Field(foreign_key="node.id", index=True, description="执行下载的节点ID") # 关系 user: "User" = Relationship(back_populates="downloads") diff --git a/models/file.py b/models/file.py index b3a1d97..0c815a8 100644 --- a/models/file.py +++ b/models/file.py @@ -1,8 +1,7 @@ -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint, Index from .base import TableBase -from datetime import datetime if TYPE_CHECKING: from .user import User @@ -11,7 +10,6 @@ if TYPE_CHECKING: from .source_link import SourceLink class File(TableBase, table=True): - __tablename__ = 'files' __table_args__ = ( UniqueConstraint("folder_id", "name", name="uq_file_folder_name_active"), CheckConstraint("name NOT LIKE '%/%' AND name NOT LIKE '%\\%'", name="ck_file_name_no_slash"), @@ -21,18 +19,18 @@ class File(TableBase, table=True): ) name: str = Field(max_length=255, description="文件名") - source_name: Optional[str] = Field(default=None, description="源文件名") + source_name: str | None = Field(default=None, description="源文件名") size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="文件大小(字节)") - upload_session_id: Optional[str] = Field(default=None, max_length=255, unique=True, index=True, description="分块上传会话ID") - file_metadata: Optional[str] = Field(default=None, description="文件元数据 (JSON格式)") # 后续可以考虑模型继承? + upload_session_id: str | None = Field(default=None, max_length=255, unique=True, index=True, description="分块上传会话ID") + file_metadata: str | None = Field(default=None, description="文件元数据 (JSON格式)") # 后续可以考虑模型继承? # 外键 - user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") - folder_id: int = Field(foreign_key="folders.id", index=True, description="所在目录ID") - policy_id: int = Field(foreign_key="policies.id", index=True, description="所属存储策略ID") + user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID") + folder_id: int = Field(foreign_key="folder.id", index=True, description="所在目录ID") + policy_id: int = Field(foreign_key="policy.id", index=True, description="所属存储策略ID") # 关系 - user: list["User"] = Relationship(back_populates="files") - folder: list["Folder"] = Relationship(back_populates="files") - policy: list["Policy"] = Relationship(back_populates="files") + user: "User" = Relationship(back_populates="files") + folder: "Folder" = Relationship(back_populates="files") + policy: "Policy" = Relationship(back_populates="files") source_links: list["SourceLink"] = Relationship(back_populates="file") \ No newline at end of file diff --git a/models/folder.py b/models/folder.py index c86171d..09f1cbd 100644 --- a/models/folder.py +++ b/models/folder.py @@ -10,7 +10,6 @@ if TYPE_CHECKING: from .file import File class Folder(TableBase, table=True): - __tablename__ = 'folders' __table_args__ = ( UniqueConstraint( "owner_id", @@ -27,9 +26,9 @@ class Folder(TableBase, table=True): name: str = Field(max_length=255, nullable=False, description="目录名") # 外键 - parent_id: Optional[int] = Field(default=None, foreign_key="folders.id", index=True, description="父目录ID") - owner_id: int = Field(foreign_key="users.id", index=True, description="所有者用户ID") - policy_id: int = Field(foreign_key="policies.id", index=True, description="所属存储策略ID") + parent_id: int | None = Field(default=None, foreign_key="folder.id", index=True, description="父目录ID") + owner_id: int = Field(foreign_key="user.id", index=True, description="所有者用户ID") + policy_id: int = Field(foreign_key="policy.id", index=True, description="所属存储策略ID") # 关系 owner: "User" = Relationship(back_populates="folders") diff --git a/models/group.py b/models/group.py index e99682b..9ef4966 100644 --- a/models/group.py +++ b/models/group.py @@ -8,24 +8,24 @@ if TYPE_CHECKING: from .user import User class GroupOptions(SQLModel): - archive_download: Optional[bool] = False - archive_task: Optional[bool] = False - share_download: Optional[bool] = False - share_free: Optional[bool] = False - webdav_proxy: Optional[bool] = False - aria2: Optional[bool] = False - relocate: Optional[bool] = False - source_batch: Optional[int] = 10 - redirected_source: Optional[bool] = False - available_nodes: Optional[List[int]] = [] - select_node: Optional[bool] = False - advance_delete: Optional[bool] = False + archive_download: bool | None = False + archive_task: bool | None = False + share_download: bool | None = False + share_free: bool | None = False + webdav_proxy: bool | None = False + aria2: bool | None = False + relocate: bool | None = False + source_batch: int | None = 10 + redirected_source: bool | None = False + available_nodes: List[int] | None = [] + select_node: bool | None = False + advance_delete: bool | None = False class Group(TableBase, table=True): - __tablename__ = 'groups' + """用户组模型""" name: str = Field(max_length=255, unique=True, description="用户组名") - policies: Optional[str] = Field(default=None, max_length=255, description="允许的策略ID列表,逗号分隔") + policies: str | None = Field(default=None, max_length=255, description="允许的策略ID列表,逗号分隔") max_storage: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="最大存储空间(字节)") share_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否允许创建分享") web_dav_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否允许使用WebDAV") @@ -34,11 +34,11 @@ class Group(TableBase, table=True): options: GroupOptions = Field(default=GroupOptions, sa_column=Column(JSON), description="其他选项") # 关系:一个组可以有多个用户 - users: List["User"] = Relationship( + user: List["User"] = Relationship( back_populates="group", sa_relationship_kwargs={"foreign_keys": "User.group_id"} ) - previous_users: List["User"] = Relationship( + previous_user: List["User"] = Relationship( back_populates="previous_group", sa_relationship_kwargs={"foreign_keys": "User.previous_group_id"} ) @@ -46,13 +46,13 @@ class Group(TableBase, table=True): @staticmethod async def create( group: Optional["Group"] = None, - name: Optional[str] = None, - policies: Optional[str] = None, + name: str | None = None, + policies: str | None = None, max_storage: int = 0, share_enabled: bool = False, web_dav_enabled: bool = False, speed_limit: int = 0, - options: Optional[dict] = None, + options: dict | None = None, ) -> "Group": """ 向数据库内添加用户组。如果提供了 `group` 参数,则使用该对象,否则创建一个新的用户组对象。 @@ -128,13 +128,13 @@ class Group(TableBase, table=True): @staticmethod async def set( id: int, - name: Optional[str] = None, - policies: Optional[str] = None, - max_storage: Optional[int] = None, - share_enabled: Optional[bool] = None, - web_dav_enabled: Optional[bool] = None, - speed_limit: Optional[int] = None, - options: Optional[str] = None + name: str | None = None, + policies: str | None = None, + max_storage: int | None = None, + share_enabled: bool | None = None, + web_dav_enabled: bool | None = None, + speed_limit: int | None = None, + options: str | None = None ) -> Optional["Group"]: """ 更新用户组信息。 @@ -142,19 +142,19 @@ class Group(TableBase, table=True): :param id: 用户组ID :type id: int :param name: 用户组名 - :type name: Optional[str] + :type name: str | None :param policies: 允许的策略ID列表,逗号分隔 - :type policies: Optional[str] + :type policies: str | None :param max_storage: 最大存储空间(字节) - :type max_storage: Optional[int] + :type max_storage: int | None :param share_enabled: 是否允许创建分享 - :type share_enabled: Optional[bool] + :type share_enabled: bool | None :param web_dav_enabled: 是否允许使用WebDAV - :type web_dav_enabled: Optional[bool] + :type web_dav_enabled: bool | None :param speed_limit: 速度限制 (KB/s), 0为不限制 - :type speed_limit: Optional[int] + :type speed_limit: int | None :param options: 其他选项 (JSON格式) - :type options: Optional[str] + :type options: str | None :return: 更新后的用户组对象或 None :rtype: Optional[Group] """ diff --git a/models/node.py b/models/node.py index 037f056..b644e82 100644 --- a/models/node.py +++ b/models/node.py @@ -1,23 +1,22 @@ -from typing import Optional, TYPE_CHECKING -from sqlmodel import Field, Relationship, text, Column, func, DateTime +from typing import TYPE_CHECKING +from sqlmodel import Field, Relationship, text from .base import TableBase -from datetime import datetime if TYPE_CHECKING: from .download import Download class Node(TableBase, table=True): - __tablename__ = 'nodes' + """节点模型""" status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="节点状态: 0=正常, 1=离线") name: str = Field(max_length=255, unique=True, description="节点名称") type: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="节点类型") server: str = Field(max_length=255, description="节点地址(IP或域名)") - slave_key: Optional[str] = Field(default=None, description="从机通讯密钥") - master_key: Optional[str] = Field(default=None, description="主机通讯密钥") + slave_key: str | None = Field(default=None, description="从机通讯密钥") + master_key: str | None = Field(default=None, description="主机通讯密钥") aria2_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否启用Aria2") - aria2_options: Optional[str] = Field(default=None, description="Aria2配置 (JSON格式)") + aria2_options: str | None = Field(default=None, description="Aria2配置 (JSON格式)") rank: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="节点排序权重") # 关系 diff --git a/models/order.py b/models/order.py index ce2922a..08f01c2 100644 --- a/models/order.py +++ b/models/order.py @@ -1,26 +1,25 @@ -from typing import Optional, TYPE_CHECKING -from sqlmodel import Field, Relationship, Column, func, DateTime +from typing import TYPE_CHECKING +from sqlmodel import Field, Relationship from .base import TableBase -from datetime import datetime if TYPE_CHECKING: from .user import User class Order(TableBase, table=True): - __tablename__ = 'orders' + """订单模型""" order_no: str = Field(max_length=255, unique=True, index=True, description="订单号,唯一") type: int = Field(description="订单类型") - method: Optional[str] = Field(default=None, max_length=255, description="支付方式") - product_id: Optional[int] = Field(default=None, description="商品ID") + method: str | None = Field(default=None, max_length=255, description="支付方式") + product_id: int | None = Field(default=None, description="商品ID") num: int = Field(default=1, sa_column_kwargs={"server_default": "1"}, description="购买数量") name: str = Field(max_length=255, description="商品名称") price: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="订单价格(分)") status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="订单状态: 0=待支付, 1=已完成, 2=已取消") # 外键 - user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") + user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID") # 关系 user: "User" = Relationship(back_populates="orders") \ No newline at end of file diff --git a/models/policy.py b/models/policy.py index fccc11f..54e0250 100644 --- a/models/policy.py +++ b/models/policy.py @@ -1,30 +1,29 @@ from typing import Optional, List, TYPE_CHECKING -from sqlmodel import Field, Relationship, text, Column, func, DateTime +from sqlmodel import Field, Relationship, text from .base import TableBase -from datetime import datetime if TYPE_CHECKING: from .file import File from .folder import Folder class Policy(TableBase, table=True): - __tablename__ = 'policies' + """存储策略模型""" name: str = Field(max_length=255, unique=True, description="策略名称") type: str = Field(max_length=255, description="存储类型 (e.g. 'local', 's3')") - server: Optional[str] = Field(default=None, max_length=255, description="服务器地址(本地策略为路径)") - bucket_name: Optional[str] = Field(default=None, max_length=255, description="存储桶名称") + server: str | None = Field(default=None, max_length=255, description="服务器地址(本地策略为路径)") + bucket_name: str | None = Field(default=None, max_length=255, description="存储桶名称") is_private: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")}, description="是否为私有空间") - base_url: Optional[str] = Field(default=None, max_length=255, description="访问文件的基础URL") - access_key: Optional[str] = Field(default=None, description="Access Key") - secret_key: Optional[str] = Field(default=None, description="Secret Key") + base_url: str | None = Field(default=None, max_length=255, description="访问文件的基础URL") + access_key: str | None = Field(default=None, description="Access Key") + secret_key: str | None = Field(default=None, description="Secret Key") max_size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="允许上传的最大文件尺寸(字节)") auto_rename: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否自动重命名") - dir_name_rule: Optional[str] = Field(default=None, max_length=255, description="目录命名规则") - file_name_rule: Optional[str] = Field(default=None, max_length=255, description="文件命名规则") + dir_name_rule: str | None = Field(default=None, max_length=255, description="目录命名规则") + file_name_rule: str | None = Field(default=None, max_length=255, description="文件命名规则") is_origin_link_enable: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否开启源链接访问") - options: Optional[str] = Field(default=None, description="其他选项 (JSON格式)") + options: str | None = Field(default=None, description="其他选项 (JSON格式)") # options 示例: {"token":"","file_type":null,"mimetype":"","od_redirect":"http://127.0.0.1:8000/...","chunk_size":52428800,"s3_path_style":false} # 关系 diff --git a/models/redeem.py b/models/redeem.py index 873b509..d00bfab 100644 --- a/models/redeem.py +++ b/models/redeem.py @@ -1,14 +1,11 @@ - -from typing import Optional -from sqlmodel import Field, text, Column, func, DateTime +from sqlmodel import Field, text from .base import TableBase -from datetime import datetime class Redeem(TableBase, table=True): - __tablename__ = 'redeems' + """兑换码模型""" type: int = Field(description="兑换码类型") - product_id: Optional[int] = Field(default=None, description="关联的商品/权益ID") + product_id: int | None = Field(default=None, description="关联的商品/权益ID") num: int = Field(default=1, sa_column_kwargs={"server_default": "1"}, description="可兑换数量/时长等") code: str = Field(unique=True, index=True, description="兑换码,唯一") used: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否已使用") \ No newline at end of file diff --git a/models/report.py b/models/report.py index 9380bbe..161ca4c 100644 --- a/models/report.py +++ b/models/report.py @@ -1,20 +1,19 @@ -from typing import Optional, TYPE_CHECKING -from sqlmodel import Field, Relationship, Column, func, DateTime +from typing import TYPE_CHECKING +from sqlmodel import Field, Relationship from .base import TableBase -from datetime import datetime if TYPE_CHECKING: from .share import Share class Report(TableBase, table=True): - __tablename__ = 'reports' + """举报模型""" reason: int = Field(description="举报原因代码") - description: Optional[str] = Field(default=None, max_length=255, description="补充描述") + description: str | None = Field(default=None, max_length=255, description="补充描述") # 外键 - share_id: int = Field(foreign_key="shares.id", index=True, description="被举报的分享ID") + share_id: int = Field(foreign_key="share.id", index=True, description="被举报的分享ID") # 关系 share: "Share" = Relationship(back_populates="reports") \ No newline at end of file diff --git a/models/request.py b/models/request.py index afaf2db..0f43dd0 100644 --- a/models/request.py +++ b/models/request.py @@ -13,5 +13,5 @@ class LoginRequest(BaseModel): """ username: str = Field(..., description="用户名或邮箱") password: str = Field(..., description="用户密码") - captcha: Optional[str] = Field(None, description="验证码") - twoFaCode: Optional[str] = Field(None, description="两步验证代码") \ No newline at end of file + captcha: str | None = Field(None, description="验证码") + twoFaCode: str | None = Field(None, description="两步验证代码") \ No newline at end of file diff --git a/models/response.py b/models/response.py index 9bdc2db..288d965 100644 --- a/models/response.py +++ b/models/response.py @@ -13,7 +13,7 @@ class ResponseModel(BaseModel): ''' code: int = Field(default=0, description="系统内部状态码, 0表示成功,其他表示失败", lt=60000, gt=0) data: Union[dict, list, str, int, float, None] = Field(None, description="响应数据") - msg: Optional[str] = Field(default=None, description="响应消息,可以是错误消息或信息提示") + msg: str | None = Field(default=None, description="响应消息,可以是错误消息或信息提示") instance_id: str = Field(default_factory=lambda: str(uuid4()), description="实例ID,用于标识请求的唯一性") class ThemeModel(BaseModel): @@ -81,12 +81,12 @@ class SiteConfigModel(ResponseModel): title: str = Field(default="DiskNext", description="网站标题") themes: dict = Field(default_factory=dict, description="网站主题配置") default_theme: str = Field(default="default", description="默认主题RGB色号") - site_notice: Optional[str] = Field(default=None, description="网站公告") + site_notice: str | None = Field(default=None, description="网站公告") user: dict = Field(default_factory=dict, description="用户信息") - logo_light: Optional[str] = Field(default=None, description="网站Logo URL") - logo_dark: Optional[str] = Field(default=None, description="网站Logo URL(深色模式)") + logo_light: str | None = Field(default=None, description="网站Logo URL") + logo_dark: str | None = Field(default=None, description="网站Logo URL(深色模式)") captcha_type: Literal['none', 'default', 'gcaptcha', 'cloudflare turnstile'] = Field(default='none', description="验证码类型") - captcha_key: Optional[str] = Field(default=None, description="验证码密钥") + captcha_key: str | None = Field(default=None, description="验证码密钥") class AuthnModel(BaseModel): ''' @@ -100,7 +100,7 @@ class UserSettingModel(BaseModel): 用户设置模型 ''' authn: Optional[AuthnModel] = Field(default=None, description="认证信息") - group_expires: Optional[datetime] = Field(default=None, description="用户组过期时间") + group_expires: datetime | None = Field(default=None, description="用户组过期时间") prefer_theme: str = Field(default="#5898d4", description="用户首选主题") qq: str | bool = Field(default=False, description="QQ号") themes: dict = Field(default_factory=dict, description="用户主题配置") diff --git a/models/setting.py b/models/setting.py index 919c659..4f211df 100644 --- a/models/setting.py +++ b/models/setting.py @@ -34,18 +34,19 @@ SETTINGS_TYPE = Literal[ # 数据库模型 class Setting(TableBase, table=True): - __tablename__ = 'settings' + """设置模型""" + __table_args__ = (UniqueConstraint("type", "name", name="uq_setting_type_name"),) type: str = Field(max_length=255, description="设置类型/分组") name: str = Field(max_length=255, description="设置项名称") - value: Optional[str] = Field(default=None, description="设置值") + value: str | None = Field(default=None, description="设置值") @staticmethod async def add( type: SETTINGS_TYPE = None, name: str = None, - value: Optional[str] = None + value: str | None = None ) -> None: """ 向数据库内添加设置项目。 @@ -55,7 +56,7 @@ class Setting(TableBase, table=True): :param name: 设置项名称 :type name: str :param value: 设置值,默认为 None - :type value: Optional[str] + :type value: str | None """ from .database import get_session @@ -114,7 +115,7 @@ class Setting(TableBase, table=True): async def set( type: SETTINGS_TYPE, name: str, - value: Optional[str] = None + value: str | None = None ) -> None: """ 更新指定类型和名称的设置项的值。 @@ -124,7 +125,7 @@ class Setting(TableBase, table=True): :param name: 设置项名称 :type name: str :param value: 新的设置值,默认为 None - :type value: Optional[str] + :type value: str | None :raises ValueError: 如果设置项不存在,则抛出异常 """ diff --git a/models/share.py b/models/share.py index 4c59cb5..1e5a6d5 100644 --- a/models/share.py +++ b/models/share.py @@ -10,7 +10,8 @@ if TYPE_CHECKING: from .report import Report class Share(TableBase, table=True): - __tablename__ = 'shares' + """分享模型""" + __table_args__ = ( UniqueConstraint("code", name="uq_share_code"), CheckConstraint("(file_id IS NOT NULL) <> (folder_id IS NOT NULL)", name="ck_share_xor"), @@ -19,22 +20,22 @@ class Share(TableBase, table=True): ) code: str = Field(max_length=64, nullable=False, index=True, description="分享码") - password: Optional[str] = Field(default=None, max_length=255, description="分享密码(加密后)") + password: str | None = Field(default=None, max_length=255, description="分享密码(加密后)") is_dir: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否为目录分享") - file_id: Optional[int] = Field(default=None, foreign_key="files.id", index=True, description="文件ID(二选一)") - folder_id: Optional[int] = Field(default=None, foreign_key="folders.id", index=True, description="目录ID(二选一)") + file_id: int | None = Field(default=None, foreign_key="file.id", index=True, description="文件ID(二选一)") + folder_id: int | None = Field(default=None, foreign_key="folder.id", index=True, description="目录ID(二选一)") views: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="浏览次数") downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="下载次数") - remain_downloads: Optional[int] = Field(default=None, description="剩余下载次数 (NULL为不限制)") - expires: Optional[datetime] = Field(default=None, description="过期时间 (NULL为永不过期)") + remain_downloads: int | None = Field(default=None, description="剩余下载次数 (NULL为不限制)") + expires: datetime | None = Field(default=None, description="过期时间 (NULL为永不过期)") preview_enabled: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")}, description="是否允许预览") - source_name: Optional[str] = Field(default=None, max_length=255, index=True, description="源名称(冗余字段,便于展示)") + source_name: str | None = Field(default=None, max_length=255, index=True, description="源名称(冗余字段,便于展示)") score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="兑换此分享所需的积分") # 外键 - user_id: int = Field(foreign_key="users.id", index=True, description="创建分享的用户ID") + user_id: int = Field(foreign_key="user.id", index=True, description="创建分享的用户ID") # 关系 user: "User" = Relationship(back_populates="shares") diff --git a/models/source_link.py b/models/source_link.py index faadc9b..989ccc4 100644 --- a/models/source_link.py +++ b/models/source_link.py @@ -8,7 +8,8 @@ if TYPE_CHECKING: from .file import File class SourceLink(TableBase, table=True): - __tablename__ = 'source_links' + """链接模型""" + __table_args__ = ( Index("ix_sourcelink_file_name", "file_id", "name"), ) @@ -17,7 +18,7 @@ class SourceLink(TableBase, table=True): downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="通过此链接的下载次数") # 外键 - file_id: int = Field(foreign_key="files.id", index=True, description="关联的文件ID") + file_id: int = Field(foreign_key="file.id", index=True, description="关联的文件ID") # 关系 file: "File" = Relationship(back_populates="source_links") \ No newline at end of file diff --git a/models/storage_pack.py b/models/storage_pack.py index 593eff9..f2b9537 100644 --- a/models/storage_pack.py +++ b/models/storage_pack.py @@ -9,15 +9,15 @@ if TYPE_CHECKING: from .user import User class StoragePack(TableBase, table=True): - __tablename__ = 'storage_packs' + """容量包模型""" name: str = Field(max_length=255, description="容量包名称") - active_time: Optional[datetime] = Field(default=None, description="激活时间") - expired_time: Optional[datetime] = Field(default=None, index=True, description="过期时间") + active_time: datetime | None = Field(default=None, description="激活时间") + expired_time: datetime | None = Field(default=None, index=True, description="过期时间") size: int = Field(description="容量包大小(字节)") # 外键 - user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") + user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID") # 关系 user: "User" = Relationship(back_populates="storage_packs") \ No newline at end of file diff --git a/models/tag.py b/models/tag.py index 9cca515..2be91a8 100644 --- a/models/tag.py +++ b/models/tag.py @@ -8,17 +8,18 @@ if TYPE_CHECKING: from .user import User class Tag(TableBase, table=True): - __tablename__ = 'tags' + """标签模型""" + __table_args__ = (UniqueConstraint("name", "user_id", name="uq_tag_name_user"),) name: str = Field(max_length=255, description="标签名称") - icon: Optional[str] = Field(default=None, max_length=255, description="标签图标") - color: Optional[str] = Field(default=None, max_length=255, description="标签颜色") + icon: str | None = Field(default=None, max_length=255, description="标签图标") + color: str | None = Field(default=None, max_length=255, description="标签颜色") type: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="标签类型: 0=手动, 1=自动") - expression: Optional[str] = Field(default=None, description="自动标签的匹配表达式") + expression: str | None = Field(default=None, description="自动标签的匹配表达式") # 外键 - user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") + user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID") # 关系 user: "User" = Relationship(back_populates="tags") \ No newline at end of file diff --git a/models/task.py b/models/task.py index 7da4e42..60279ab 100644 --- a/models/task.py +++ b/models/task.py @@ -9,7 +9,8 @@ if TYPE_CHECKING: from .download import Download class Task(TableBase, table=True): - __tablename__ = 'tasks' + """任务模型""" + __table_args__ = ( CheckConstraint("progress BETWEEN 0 AND 100", name="ck_task_progress_range"), ) @@ -17,11 +18,11 @@ class Task(TableBase, table=True): status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="任务状态: 0=排队中, 1=处理中, 2=完成, 3=错误") type: int = Field(description="任务类型") progress: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="任务进度 (0-100)") - error: Optional[str] = Field(default=None, description="错误信息") - props: Optional[str] = Field(default=None, description="任务属性 (JSON格式)") + error: str | None = Field(default=None, description="错误信息") + props: str | None = Field(default=None, description="任务属性 (JSON格式)") # 外键 - user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") + user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID") # 关系 user: "User" = Relationship(back_populates="tasks") diff --git a/models/user.py b/models/user.py index bae550b..e879495 100644 --- a/models/user.py +++ b/models/user.py @@ -15,7 +15,7 @@ from .task import Task from .webdav import WebDAV class User(TableBase, table=True): - __tablename__ = 'users' + """用户模型""" email: str = Field(max_length=100, unique=True, index=True) """用户邮箱,唯一""" @@ -25,44 +25,56 @@ class User(TableBase, table=True): nick: str | None = Field(default=None, max_length=50) """用户昵称""" + password: str = Field(max_length=255) """用户密码(加密后)""" - status: bool | None = Field(default=None, sa_column_kwargs={"server_default": "0"}) - """用户状态: True=正常, None=未激活, False=封禁""" + + status: bool = Field(default=True, sa_column_kwargs={"server_default": "true"}) + """用户状态: True=正常, False=封禁""" + storage: int = Field(default=0, sa_column_kwargs={"server_default": "0"}) """已用存储空间(字节)""" + two_factor: str | None = Field(default=None, max_length=255) """两步验证密钥""" + avatar: str | None = Field(default=None, max_length=255) """头像地址""" + options: str | None = Field(default=None) """用户个人设置 (JSON格式)""" + authn: str | None = Field(default=None) """WebAuthn 凭证""" + open_id: str | None = Field(default=None, max_length=255, unique=True, index=True) """第三方登录OpenID""" + score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}) """用户积分""" + group_expires: datetime | None = Field(default=None) """当前用户组过期时间""" + phone: str | None = Field(default=None, max_length=255, unique=True, index=True) """手机号""" # 外键 - group_id: int = Field(foreign_key="groups.id", index=True) + group_id: int = Field(foreign_key="group.id", index=True) """所属用户组ID""" - previous_group_id: int | None = Field(default=None, foreign_key="groups.id") + + previous_group_id: int | None = Field(default=None, foreign_key="group.id") """之前的用户组ID(用于过期后恢复)""" # 关系 group: "Group" = Relationship( - back_populates="users", + back_populates="user", sa_relationship_kwargs={ "foreign_keys": "User.group_id" } ) previous_group: Optional["Group"] = Relationship( - back_populates="previous_users", + back_populates="previous_user", sa_relationship_kwargs={ "foreign_keys": "User.previous_group_id" } diff --git a/models/webdav.py b/models/webdav.py index 51e4b1c..dc0cf63 100644 --- a/models/webdav.py +++ b/models/webdav.py @@ -7,17 +7,18 @@ if TYPE_CHECKING: from .user import User class WebDAV(TableBase, table=True): - __tablename__ = 'webdavs' + """WebDAV账户模型""" + __table_args__ = (UniqueConstraint("name", "user_id", name="uq_webdav_name_user"),) name: str = Field(max_length=255, description="WebDAV账户名") - password: str = Field(max_length=255, description="WebDAV密码(加密后)") + password: str = Field(max_length=255, description="WebDAV密码") root: str = Field(default="/", sa_column_kwargs={"server_default": "'/'"}, description="根目录路径") readonly: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否只读") use_proxy: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否使用代理下载") # 外键 - user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") + user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID") # 关系 user: "User" = Relationship(back_populates="webdavs") \ No newline at end of file diff --git a/service/user/login.py b/service/user/login.py index 0b3d7cd..7f8495d 100644 --- a/service/user/login.py +++ b/service/user/login.py @@ -18,9 +18,9 @@ async def Login(LoginRequest: LoginRequest) -> TokenModel | bool | None: :param password: 用户密码 :type password: str :param captcha: 验证码 - :type captcha: Optional[str] + :type captcha: str | None :param twoFaCode: 两步验证代码 - :type twoFaCode: Optional[str] + :type twoFaCode: str | None :return: TokenModel 对象或状态码或 None :rtype: TokenModel | int | None