优化数据库模型与关系

This commit is contained in:
2025-09-27 22:40:10 +08:00
parent cf5539f3c2
commit 0d45a07ba7
10 changed files with 81 additions and 23 deletions

View File

@@ -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}
) )

View File

@@ -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="目标存储路径")

View File

@@ -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")

View File

@@ -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")

View File

@@ -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(

View File

@@ -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")

View File

@@ -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为不限制)")

View File

@@ -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="通过此链接的下载次数")

View File

@@ -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")

View File

@@ -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")