From 0d45a07ba7a278e2108e1223c04109c44f770f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8E=E5=B0=8F=E4=B8=98?= Date: Sat, 27 Sep 2025 22:40:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E4=B8=8E=E5=85=B3=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/base.py | 7 ++++--- models/download.py | 7 +++++-- models/file.py | 12 +++++++++--- models/folder.py | 17 ++++++++++++++--- models/group.py | 21 +++++++++++++++++---- models/policy.py | 1 + models/share.py | 14 ++++++++++++-- models/source_link.py | 5 ++++- models/task.py | 8 ++++++-- models/user.py | 12 +++++++++--- 10 files changed, 81 insertions(+), 23 deletions(-) diff --git a/models/base.py b/models/base.py index 91d7c63..7b552e3 100644 --- a/models/base.py +++ b/models/base.py @@ -11,17 +11,18 @@ class TableBase(SQLModel, AsyncAttrs): id: Optional[int] = Field(default=None, primary_key=True, description="主键ID") created_at: datetime = Field( + sa_type=DateTime, default_factory=utcnow, description="创建时间", ) updated_at: datetime = Field( sa_type=DateTime, - description="更新时间", sa_column_kwargs={"default": utcnow, "onupdate": utcnow}, - default_factory=utcnow + default_factory=utcnow, + description="更新时间", ) deleted_at: Optional[datetime] = Field( default=None, + nullable=True, description="删除时间", - sa_column={"nullable": True} ) \ No newline at end of file diff --git a/models/download.py b/models/download.py index d0b0f3e..3722926 100644 --- a/models/download.py +++ b/models/download.py @@ -1,9 +1,8 @@ # my_project/models/download.py from typing import Optional, TYPE_CHECKING -from sqlmodel import Field, Relationship, Column, func, DateTime +from sqlmodel import Field, Relationship, UniqueConstraint from .base import TableBase -from datetime import datetime if TYPE_CHECKING: from .user import User @@ -12,6 +11,9 @@ if TYPE_CHECKING: class Download(TableBase, table=True): __tablename__ = 'downloads' + __table_args__ = ( + UniqueConstraint("node_id", "g_id", name="uq_download_node_gid"), + ) status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="下载状态: 0=进行中, 1=完成, 2=错误") type: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="任务类型") @@ -22,6 +24,7 @@ class Download(TableBase, table=True): 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格式)") + # 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="错误信息") dst: str = Field(description="目标存储路径") diff --git a/models/file.py b/models/file.py index 8c43ea8..4e60716 100644 --- a/models/file.py +++ b/models/file.py @@ -1,7 +1,7 @@ # my_project/models/file.py from typing import Optional, TYPE_CHECKING -from sqlmodel import Field, Relationship, Column, func, DateTime +from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint, Index from .base import TableBase from datetime import datetime @@ -13,13 +13,19 @@ if TYPE_CHECKING: 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"), + Index("ix_file_user_updated", "user_id", "updated_at"), + Index("ix_file_folder_updated", "folder_id", "updated_at"), + Index("ix_file_user_size", "user_id", "size"), + ) name: str = Field(max_length=255, description="文件名") source_name: Optional[str] = Field(default=None, description="源文件名") size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="文件大小(字节)") - pic_info: Optional[str] = Field(default=None, max_length=255, 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格式)") + file_metadata: Optional[str] = Field(default=None, description="文件元数据 (JSON格式)") # 后续可以考虑模型继承? # 外键 user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") diff --git a/models/folder.py b/models/folder.py index b2f66a4..778abdc 100644 --- a/models/folder.py +++ b/models/folder.py @@ -1,7 +1,7 @@ # my_project/models/folder.py from typing import Optional, List, TYPE_CHECKING -from sqlmodel import Field, Relationship, UniqueConstraint, Column, func, DateTime +from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint from .base import TableBase from datetime import datetime @@ -12,9 +12,20 @@ if TYPE_CHECKING: class Folder(TableBase, table=True): __tablename__ = 'folders' - __table_args__ = (UniqueConstraint("name", "parent_id", name="uq_folder_name_parent"),) + __table_args__ = ( + UniqueConstraint( + "owner_id", + "parent_id", + "name", + name="uq_folder_name_parent", + ), + CheckConstraint( + "name NOT LIKE '%/%' AND name NOT LIKE '%\\%'", + name="ck_folder_name_no_slash", + ), + ) - name: str = Field(max_length=255, description="目录名") + name: str = Field(max_length=255, nullable=False, description="目录名") # 外键 parent_id: Optional[int] = Field(default=None, foreign_key="folders.id", index=True, description="父目录ID") diff --git a/models/group.py b/models/group.py index 6e175c5..58f5526 100644 --- a/models/group.py +++ b/models/group.py @@ -1,14 +1,27 @@ # my_project/models/group.py -from tokenize import group from typing import Optional, List, TYPE_CHECKING -from sqlmodel import Field, Relationship, text, Column, func, DateTime +from sqlmodel import Field, Relationship, text, Column, JSON from .base import TableBase -from datetime import datetime +from sqlmodel import SQLModel 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 + class Group(TableBase, table=True): __tablename__ = 'groups' @@ -19,7 +32,7 @@ class Group(TableBase, table=True): web_dav_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否允许使用WebDAV") admin: bool = Field(default=False, description="是否为管理员组") speed_limit: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="速度限制 (KB/s), 0为不限制") - options: Optional[str] = Field(default=None, description="其他选项 (JSON格式)") + options: GroupOptions = Field(default=GroupOptions, sa_column=Column(JSON), description="其他选项") # 关系:一个组可以有多个用户 users: List["User"] = Relationship( diff --git a/models/policy.py b/models/policy.py index ac8e19f..7135972 100644 --- a/models/policy.py +++ b/models/policy.py @@ -26,6 +26,7 @@ class Policy(TableBase, table=True): file_name_rule: Optional[str] = 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 示例: {"token":"","file_type":null,"mimetype":"","od_redirect":"http://127.0.0.1:8000/...","chunk_size":52428800,"s3_path_style":false} # 关系 files: List["File"] = Relationship(back_populates="policy") diff --git a/models/share.py b/models/share.py index 13a249e..2c18a5c 100644 --- a/models/share.py +++ b/models/share.py @@ -2,7 +2,7 @@ from typing import Optional, TYPE_CHECKING from datetime import datetime -from sqlmodel import Field, Relationship, text, Column, func, DateTime +from sqlmodel import Field, Relationship, text, CheckConstraint, UniqueConstraint, Index from .base import TableBase from datetime import datetime @@ -12,10 +12,20 @@ if TYPE_CHECKING: 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"), + Index("ix_share_source_name", "source_name"), + Index("ix_share_user_created", "user_id", "created_at"), + ) + code: str = Field(max_length=64, nullable=False, index=True, description="分享码") password: Optional[str] = Field(default=None, max_length=255, description="分享密码(加密后)") + is_dir: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否为目录分享") - source_id: int = Field(description="源文件或目录的ID") + 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(二选一)") + 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为不限制)") diff --git a/models/source_link.py b/models/source_link.py index c34ee58..ecb033a 100644 --- a/models/source_link.py +++ b/models/source_link.py @@ -1,7 +1,7 @@ # my_project/models/source_link.py from typing import TYPE_CHECKING, Optional -from sqlmodel import Field, Relationship, Column, func, DateTime +from sqlmodel import Field, Relationship, Index from .base import TableBase from datetime import datetime @@ -10,6 +10,9 @@ if TYPE_CHECKING: class SourceLink(TableBase, table=True): __tablename__ = 'source_links' + __table_args__ = ( + Index("ix_sourcelink_file_name", "file_id", "name"), + ) name: str = Field(max_length=255, description="链接名称") downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="通过此链接的下载次数") diff --git a/models/task.py b/models/task.py index c6a81f4..7a84556 100644 --- a/models/task.py +++ b/models/task.py @@ -1,7 +1,7 @@ # my_project/models/task.py from typing import Optional, TYPE_CHECKING -from sqlmodel import Field, Relationship, Column, func, DateTime +from sqlmodel import Field, Relationship, CheckConstraint from .base import TableBase from datetime import datetime @@ -11,6 +11,10 @@ if TYPE_CHECKING: class Task(TableBase, table=True): __tablename__ = 'tasks' + __table_args__ = ( + CheckConstraint("progress BETWEEN 0 AND 100", name="ck_task_progress_range"), + ) + status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="任务状态: 0=排队中, 1=处理中, 2=完成, 3=错误") type: int = Field(description="任务类型") @@ -19,7 +23,7 @@ class Task(TableBase, table=True): props: Optional[str] = Field(default=None, description="任务属性 (JSON格式)") # 外键 - user_id: "int" = Field(foreign_key="users.id", index=True, description="所属用户ID") + user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") # 关系 user: "User" = Relationship(back_populates="tasks") diff --git a/models/user.py b/models/user.py index 9972bbd..2684967 100644 --- a/models/user.py +++ b/models/user.py @@ -2,7 +2,7 @@ from typing import Optional, TYPE_CHECKING from datetime import datetime -from sqlmodel import Field, Relationship, Column, func, DateTime +from sqlmodel import Field, Relationship, UniqueConstraint from .base import TableBase from .database import get_session from sqlmodel import select @@ -24,6 +24,8 @@ class User(TableBase, table=True): __tablename__ = 'users' email: str = Field(max_length=100, unique=True, index=True, description="用户邮箱,唯一") + phone: str = Field(default=None, nullable=True, index=True, description="用户手机号,唯一") + nick: Optional[str] = Field(default=None, max_length=50, description="用户昵称") password: str = Field(max_length=255, description="用户密码(加密后)") status: Optional[bool] = Field(default=None, sa_column_kwargs={"server_default": "0"}, description="用户状态: True=正常, None=未激活, False=封禁") @@ -44,11 +46,15 @@ class User(TableBase, table=True): # 关系 group: "Group" = Relationship( back_populates="users", - sa_relationship_kwargs={"foreign_keys": "User.group_id"} + sa_relationship_kwargs={ + "foreign_keys": "User.group_id" + } ) previous_group: Optional["Group"] = Relationship( back_populates="previous_users", - sa_relationship_kwargs={"foreign_keys": "User.previous_group_id"} + sa_relationship_kwargs={ + "foreign_keys": "User.previous_group_id" + } ) downloads: list["Download"] = Relationship(back_populates="user")