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.
This commit is contained in:
2025-11-27 21:22:40 +08:00
parent 1533d9e89c
commit b364b740ca
21 changed files with 156 additions and 147 deletions

View File

@@ -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/)
<!-- * [React](https://github.com/facebook/react) + [Redux](https://github.com/reduxjs/redux) + [Material-UI](https://github.com/mui-org/material-ui) -->
## :scroll: 许可证
## 📜 许可证
GPL V3
GPL V3

View File

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

View File

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

View File

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

View File

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

View File

@@ -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="节点排序权重")
# 关系

View File

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

View File

@@ -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}
# 关系

View File

@@ -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="是否已使用")

View File

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

View File

@@ -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="两步验证代码")
captcha: str | None = Field(None, description="验证码")
twoFaCode: str | None = Field(None, description="两步验证代码")

View File

@@ -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="用户主题配置")

View File

@@ -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: 如果设置项不存在,则抛出异常
"""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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