优化数据库模型与关系
This commit is contained in:
@@ -11,17 +11,18 @@ class TableBase(SQLModel, AsyncAttrs):
|
|||||||
|
|
||||||
id: Optional[int] = Field(default=None, primary_key=True, description="主键ID")
|
id: Optional[int] = Field(default=None, primary_key=True, description="主键ID")
|
||||||
created_at: datetime = Field(
|
created_at: datetime = Field(
|
||||||
|
sa_type=DateTime,
|
||||||
default_factory=utcnow,
|
default_factory=utcnow,
|
||||||
description="创建时间",
|
description="创建时间",
|
||||||
)
|
)
|
||||||
updated_at: datetime = Field(
|
updated_at: datetime = Field(
|
||||||
sa_type=DateTime,
|
sa_type=DateTime,
|
||||||
description="更新时间",
|
|
||||||
sa_column_kwargs={"default": utcnow, "onupdate": utcnow},
|
sa_column_kwargs={"default": utcnow, "onupdate": utcnow},
|
||||||
default_factory=utcnow
|
default_factory=utcnow,
|
||||||
|
description="更新时间",
|
||||||
)
|
)
|
||||||
deleted_at: Optional[datetime] = Field(
|
deleted_at: Optional[datetime] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
nullable=True,
|
||||||
description="删除时间",
|
description="删除时间",
|
||||||
sa_column={"nullable": True}
|
|
||||||
)
|
)
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
# my_project/models/download.py
|
# my_project/models/download.py
|
||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
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 .base import TableBase
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
from .user import User
|
||||||
@@ -12,6 +11,9 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class Download(TableBase, table=True):
|
class Download(TableBase, table=True):
|
||||||
__tablename__ = 'downloads'
|
__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=错误")
|
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="任务类型")
|
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)")
|
speed: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="下载速度 (bytes/s)")
|
||||||
parent: Optional[str] = Field(default=None, description="父任务标识")
|
parent: Optional[str] = Field(default=None, description="父任务标识")
|
||||||
attrs: Optional[str] = Field(default=None, description="额外属性 (JSON格式)")
|
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="错误信息")
|
error: Optional[str] = Field(default=None, description="错误信息")
|
||||||
dst: str = Field(description="目标存储路径")
|
dst: str = Field(description="目标存储路径")
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# my_project/models/file.py
|
# my_project/models/file.py
|
||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
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 .base import TableBase
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -13,13 +13,19 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class File(TableBase, table=True):
|
class File(TableBase, table=True):
|
||||||
__tablename__ = 'files'
|
__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="文件名")
|
name: str = Field(max_length=255, description="文件名")
|
||||||
source_name: Optional[str] = Field(default=None, description="源文件名")
|
source_name: Optional[str] = Field(default=None, description="源文件名")
|
||||||
size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, 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")
|
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")
|
user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# my_project/models/folder.py
|
# my_project/models/folder.py
|
||||||
|
|
||||||
from typing import Optional, List, TYPE_CHECKING
|
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 .base import TableBase
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -12,9 +12,20 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class Folder(TableBase, table=True):
|
class Folder(TableBase, table=True):
|
||||||
__tablename__ = 'folders'
|
__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")
|
parent_id: Optional[int] = Field(default=None, foreign_key="folders.id", index=True, description="父目录ID")
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
# my_project/models/group.py
|
# my_project/models/group.py
|
||||||
|
|
||||||
from tokenize import group
|
|
||||||
from typing import Optional, List, TYPE_CHECKING
|
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 .base import TableBase
|
||||||
from datetime import datetime
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
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):
|
class Group(TableBase, table=True):
|
||||||
__tablename__ = 'groups'
|
__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")
|
web_dav_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否允许使用WebDAV")
|
||||||
admin: bool = Field(default=False, description="是否为管理员组")
|
admin: bool = Field(default=False, description="是否为管理员组")
|
||||||
speed_limit: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="速度限制 (KB/s), 0为不限制")
|
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(
|
users: List["User"] = Relationship(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class Policy(TableBase, table=True):
|
|||||||
file_name_rule: Optional[str] = Field(default=None, max_length=255, description="文件命名规则")
|
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="是否开启源链接访问")
|
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: 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")
|
files: List["File"] = Relationship(back_populates="policy")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
from datetime import datetime
|
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 .base import TableBase
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -12,10 +12,20 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class Share(TableBase, table=True):
|
class Share(TableBase, table=True):
|
||||||
__tablename__ = 'shares'
|
__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="分享密码(加密后)")
|
password: Optional[str] = Field(default=None, max_length=255, description="分享密码(加密后)")
|
||||||
|
|
||||||
is_dir: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, 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="浏览次数")
|
views: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="浏览次数")
|
||||||
downloads: 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为不限制)")
|
remain_downloads: Optional[int] = Field(default=None, description="剩余下载次数 (NULL为不限制)")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# my_project/models/source_link.py
|
# my_project/models/source_link.py
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
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 .base import TableBase
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -10,6 +10,9 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class SourceLink(TableBase, table=True):
|
class SourceLink(TableBase, table=True):
|
||||||
__tablename__ = 'source_links'
|
__tablename__ = 'source_links'
|
||||||
|
__table_args__ = (
|
||||||
|
Index("ix_sourcelink_file_name", "file_id", "name"),
|
||||||
|
)
|
||||||
|
|
||||||
name: str = Field(max_length=255, description="链接名称")
|
name: str = Field(max_length=255, description="链接名称")
|
||||||
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="通过此链接的下载次数")
|
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="通过此链接的下载次数")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# my_project/models/task.py
|
# my_project/models/task.py
|
||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
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 .base import TableBase
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -11,6 +11,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class Task(TableBase, table=True):
|
class Task(TableBase, table=True):
|
||||||
__tablename__ = 'tasks'
|
__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=错误")
|
status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="任务状态: 0=排队中, 1=处理中, 2=完成, 3=错误")
|
||||||
type: int = Field(description="任务类型")
|
type: int = Field(description="任务类型")
|
||||||
@@ -19,7 +23,7 @@ class Task(TableBase, table=True):
|
|||||||
props: Optional[str] = Field(default=None, description="任务属性 (JSON格式)")
|
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")
|
user: "User" = Relationship(back_populates="tasks")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlmodel import Field, Relationship, Column, func, DateTime
|
from sqlmodel import Field, Relationship, UniqueConstraint
|
||||||
from .base import TableBase
|
from .base import TableBase
|
||||||
from .database import get_session
|
from .database import get_session
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
@@ -24,6 +24,8 @@ class User(TableBase, table=True):
|
|||||||
__tablename__ = 'users'
|
__tablename__ = 'users'
|
||||||
|
|
||||||
email: str = Field(max_length=100, unique=True, index=True, description="用户邮箱,唯一")
|
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="用户昵称")
|
nick: Optional[str] = Field(default=None, max_length=50, description="用户昵称")
|
||||||
password: str = Field(max_length=255, description="用户密码(加密后)")
|
password: str = Field(max_length=255, description="用户密码(加密后)")
|
||||||
status: Optional[bool] = Field(default=None, sa_column_kwargs={"server_default": "0"}, description="用户状态: True=正常, None=未激活, False=封禁")
|
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(
|
group: "Group" = Relationship(
|
||||||
back_populates="users",
|
back_populates="users",
|
||||||
sa_relationship_kwargs={"foreign_keys": "User.group_id"}
|
sa_relationship_kwargs={
|
||||||
|
"foreign_keys": "User.group_id"
|
||||||
|
}
|
||||||
)
|
)
|
||||||
previous_group: Optional["Group"] = Relationship(
|
previous_group: Optional["Group"] = Relationship(
|
||||||
back_populates="previous_users",
|
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")
|
downloads: list["Download"] = Relationship(back_populates="user")
|
||||||
|
|||||||
Reference in New Issue
Block a user