refactor: 统一 sqlmodel_ext 用法至官方推荐模式
Some checks failed
Test / test (push) Failing after 3m47s

- 替换 Field(max_length=X) 为 StrX/TextX 类型别名(21 个 sqlmodels 文件)
- 替换 get + 404 检查为 get_exist_one()(17 个路由文件,约 50 处)
- 替换 save + session.refresh 为 save(load=...)
- 替换 session.add + commit 为 save()(dav/provider.py)
- 更新所有依赖至最新版本

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 11:13:16 +08:00
parent 9185f26b83
commit 6c96c43bea
57 changed files with 1091 additions and 761 deletions

View File

@@ -82,6 +82,7 @@ from .object import (
ObjectResponse,
ObjectSwitchPolicyRequest,
ObjectType,
FileCategory,
PolicyResponse,
UploadChunkResponse,
UploadSession,
@@ -116,12 +117,22 @@ from .custom_property import (
)
from .physical_file import PhysicalFile, PhysicalFileBase
from .uri import DiskNextURI, FileSystemNamespace
from .order import Order, OrderStatus, OrderType
from .order import (
Order, OrderStatus, OrderType,
CreateOrderRequest, OrderResponse,
)
from .policy import (
Policy, PolicyBase, PolicyCreateRequest, PolicyOptions, PolicyOptionsBase,
PolicyType, PolicySummary, PolicyUpdateRequest,
)
from .redeem import Redeem, RedeemType
from .product import (
Product, ProductBase, ProductType, PaymentMethod,
ProductCreateRequest, ProductUpdateRequest, ProductResponse,
)
from .redeem import (
Redeem, RedeemType,
RedeemCreateRequest, RedeemUseRequest, RedeemInfoResponse, RedeemAdminResponse,
)
from .report import Report, ReportReason
from .setting import (
Setting, SettingsType, SiteConfigResponse, AuthMethodConfig,
@@ -134,7 +145,7 @@ from .share import (
AdminShareListItem,
)
from .source_link import SourceLink
from .storage_pack import StoragePack
from .storage_pack import StoragePack, StoragePackResponse
from .tag import Tag, TagType
from .task import Task, TaskProps, TaskPropsBase, TaskStatus, TaskType, TaskSummary, TaskSummaryBase
from .webdav import (

View File

@@ -10,7 +10,7 @@ from uuid import UUID
from sqlmodel import Field, Relationship, UniqueConstraint
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str100, Str128, Str255, Text1024
if TYPE_CHECKING:
from .user import User
@@ -87,7 +87,7 @@ class ChangePasswordRequest(SQLModelBase):
old_password: str = Field(min_length=1)
"""当前密码"""
new_password: str = Field(min_length=8, max_length=128)
new_password: Str128 = Field(min_length=8)
"""新密码(至少 8 位)"""
@@ -103,13 +103,13 @@ class AuthIdentity(SQLModelBase, UUIDTableBaseMixin):
provider: AuthProviderType = Field(index=True)
"""提供者类型"""
identifier: str = Field(max_length=255, index=True)
identifier: Str255 = Field(index=True)
"""标识符(邮箱/手机号/OAuth openid"""
credential: str | None = Field(default=None, max_length=1024)
credential: Text1024 | None = None
"""凭证Argon2 哈希密码 / null"""
display_name: str | None = Field(default=None, max_length=100)
display_name: Str100 | None = None
"""OAuth 昵称"""
avatar_url: str | None = Field(default=None, max_length=512)

View File

@@ -13,7 +13,7 @@ from uuid import UUID
from sqlalchemy import JSON
from sqlmodel import Field, Relationship
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str100
if TYPE_CHECKING:
from .user import User
@@ -44,13 +44,13 @@ class CustomPropertyType(StrEnum):
class CustomPropertyDefinitionBase(SQLModelBase):
"""自定义属性定义基础模型"""
name: str = Field(max_length=100)
name: Str100
"""属性显示名称"""
type: CustomPropertyType
"""属性值类型"""
icon: str | None = Field(default=None, max_length=100)
icon: Str100 | None = None
"""图标标识iconify 名称)"""
options: list[str] | None = Field(default=None, sa_type=JSON)
@@ -90,7 +90,7 @@ class CustomPropertyDefinition(CustomPropertyDefinitionBase, UUIDTableBaseMixin)
class CustomPropertyCreateRequest(SQLModelBase):
"""创建自定义属性请求 DTO"""
name: str = Field(max_length=100)
name: Str100
"""属性显示名称"""
type: CustomPropertyType

View File

@@ -4,7 +4,7 @@ from uuid import UUID
from sqlmodel import Field, Relationship, UniqueConstraint, Index
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, TableBaseMixin
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, TableBaseMixin, Str255
if TYPE_CHECKING:
from .user import User
@@ -141,7 +141,7 @@ class Download(DownloadBase, UUIDTableBaseMixin):
speed: int = Field(default=0)
"""下载速度bytes/s"""
parent: str | None = Field(default=None, max_length=255)
parent: Str255 | None = None
"""父任务标识"""
error: str | None = Field(default=None)

View File

@@ -20,7 +20,7 @@ from uuid import UUID
from sqlmodel import Field, Relationship, UniqueConstraint
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, Str100, Str255, Text1024
if TYPE_CHECKING:
from .group import Group
@@ -119,7 +119,7 @@ class UserFileAppDefaultResponse(SQLModelBase):
class FileAppCreateRequest(SQLModelBase):
"""管理员创建应用请求 DTO"""
name: str = Field(max_length=100)
name: Str100
"""应用名称"""
app_key: str = Field(max_length=50)
@@ -128,7 +128,7 @@ class FileAppCreateRequest(SQLModelBase):
type: FileAppType
"""应用类型"""
icon: str | None = Field(default=None, max_length=255)
icon: Str255 | None = None
"""图标名称/URL"""
description: str | None = Field(default=None, max_length=500)
@@ -140,13 +140,13 @@ class FileAppCreateRequest(SQLModelBase):
is_restricted: bool = False
"""是否限制用户组访问"""
iframe_url_template: str | None = Field(default=None, max_length=1024)
iframe_url_template: Text1024 | None = None
"""iframe URL 模板"""
wopi_discovery_url: str | None = Field(default=None, max_length=512)
"""WOPI 发现端点 URL"""
wopi_editor_url_template: str | None = Field(default=None, max_length=1024)
wopi_editor_url_template: Text1024 | None = None
"""WOPI 编辑器 URL 模板"""
extensions: list[str] = []
@@ -159,7 +159,7 @@ class FileAppCreateRequest(SQLModelBase):
class FileAppUpdateRequest(SQLModelBase):
"""管理员更新应用请求 DTO所有字段可选"""
name: str | None = Field(default=None, max_length=100)
name: Str100 | None = None
"""应用名称"""
app_key: str | None = Field(default=None, max_length=50)
@@ -168,7 +168,7 @@ class FileAppUpdateRequest(SQLModelBase):
type: FileAppType | None = None
"""应用类型"""
icon: str | None = Field(default=None, max_length=255)
icon: Str255 | None = None
"""图标名称/URL"""
description: str | None = Field(default=None, max_length=500)
@@ -180,13 +180,13 @@ class FileAppUpdateRequest(SQLModelBase):
is_restricted: bool | None = None
"""是否限制用户组访问"""
iframe_url_template: str | None = Field(default=None, max_length=1024)
iframe_url_template: Text1024 | None = None
"""iframe URL 模板"""
wopi_discovery_url: str | None = Field(default=None, max_length=512)
"""WOPI 发现端点 URL"""
wopi_editor_url_template: str | None = Field(default=None, max_length=1024)
wopi_editor_url_template: Text1024 | None = None
"""WOPI 编辑器 URL 模板"""
@@ -325,7 +325,7 @@ class WopiDiscoveryResponse(SQLModelBase):
class FileApp(SQLModelBase, UUIDTableBaseMixin):
"""文件查看器应用注册表"""
name: str = Field(max_length=100)
name: Str100
"""应用名称"""
app_key: str = Field(max_length=50, unique=True, index=True)
@@ -334,7 +334,7 @@ class FileApp(SQLModelBase, UUIDTableBaseMixin):
type: FileAppType
"""应用类型"""
icon: str | None = Field(default=None, max_length=255)
icon: Str255 | None = None
"""图标名称/URL"""
description: str | None = Field(default=None, max_length=500)
@@ -346,13 +346,13 @@ class FileApp(SQLModelBase, UUIDTableBaseMixin):
is_restricted: bool = False
"""是否限制用户组访问"""
iframe_url_template: str | None = Field(default=None, max_length=1024)
iframe_url_template: Text1024 | None = None
"""iframe URL 模板,支持 {file_url} 占位符"""
wopi_discovery_url: str | None = Field(default=None, max_length=512)
"""WOPI 客户端发现端点 URL"""
wopi_editor_url_template: str | None = Field(default=None, max_length=1024)
wopi_editor_url_template: Text1024 | None = None
"""WOPI 编辑器 URL 模板,支持 {wopi_src} {access_token} {access_token_ttl}"""
# 关系

View File

@@ -5,7 +5,7 @@ from uuid import UUID
from sqlalchemy import BigInteger
from sqlmodel import Field, Relationship, text
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, Str255
if TYPE_CHECKING:
from .user import User
@@ -67,7 +67,7 @@ class GroupAllOptionsBase(GroupOptionsBase):
class GroupCreateRequest(GroupAllOptionsBase):
"""创建用户组请求 DTO"""
name: str = Field(max_length=255)
name: Str255
"""用户组名称"""
max_storage: int = Field(default=0, ge=0)
@@ -92,7 +92,7 @@ class GroupCreateRequest(GroupAllOptionsBase):
class GroupUpdateRequest(SQLModelBase):
"""更新用户组请求 DTO所有字段可选"""
name: str | None = Field(default=None, max_length=255)
name: Str255 | None = None
"""用户组名称"""
max_storage: int | None = Field(default=None, ge=0)
@@ -258,7 +258,7 @@ class GroupOptions(GroupAllOptionsBase, TableBaseMixin):
class Group(GroupBase, UUIDTableBaseMixin):
"""用户组模型"""
name: str = Field(max_length=255, unique=True)
name: Str255 = Field(unique=True)
"""用户组名"""
max_storage: int = Field(default=0, sa_type=BigInteger, sa_column_kwargs={"server_default": "0"})

View File

@@ -130,6 +130,11 @@ default_settings: list[Setting] = [
Setting(name="sms_provider", value="", type=SettingsType.MOBILE),
Setting(name="sms_access_key", value="", type=SettingsType.MOBILE),
Setting(name="sms_secret_key", value="", type=SettingsType.MOBILE),
# ==================== 文件分类扩展名配置 ====================
Setting(name="image", value="jpg,jpeg,png,gif,bmp,webp,svg,ico,tiff,tif,avif,heic,heif,psd,raw", type=SettingsType.FILE_CATEGORY),
Setting(name="video", value="mp4,mkv,avi,mov,wmv,flv,webm,m4v,ts,3gp,mpg,mpeg", type=SettingsType.FILE_CATEGORY),
Setting(name="audio", value="mp3,wav,flac,aac,ogg,wma,m4a,opus,ape,aiff,mid,midi", type=SettingsType.FILE_CATEGORY),
Setting(name="document", value="pdf,doc,docx,odt,rtf,txt,tex,epub,pages,ppt,pptx,odp,key,xls,xlsx,csv,ods,numbers,tsv,md,markdown,mdx", type=SettingsType.FILE_CATEGORY),
]
async def init_default_settings() -> None:
@@ -173,7 +178,7 @@ async def init_default_group() -> None:
admin=True,
)
admin_group_id = admin_group.id # 在 save 前保存 UUID
await admin_group.save(session)
admin_group = await admin_group.save(session)
await GroupOptions(
group_id=admin_group_id,
@@ -203,7 +208,7 @@ async def init_default_group() -> None:
web_dav_enabled=True,
)
member_group_id = member_group.id # 在 save 前保存 UUID
await member_group.save(session)
member_group = await member_group.save(session)
await GroupOptions(
group_id=member_group_id,
@@ -222,7 +227,7 @@ async def init_default_group() -> None:
default_group_setting = await Setting.get(session, Setting.name == "default_group")
if default_group_setting:
default_group_setting.value = str(member_group_id)
await default_group_setting.save(session)
default_group_setting = await default_group_setting.save(session)
# 未找到初始游客组时,则创建
if not await Group.get(session, Group.name == "游客"):
@@ -232,7 +237,7 @@ async def init_default_group() -> None:
web_dav_enabled=False,
)
guest_group_id = guest_group.id # 在 save 前保存 UUID
await guest_group.save(session)
guest_group = await guest_group.save(session)
await GroupOptions(
group_id=guest_group_id,
@@ -284,7 +289,7 @@ async def init_default_user() -> None:
group_id=admin_group.id,
)
admin_user_id = admin_user.id # 在 save 前保存 UUID
await admin_user.save(session)
admin_user = await admin_user.save(session)
# 创建 AuthIdentity邮箱密码身份
await AuthIdentity(
@@ -373,7 +378,7 @@ async def init_default_theme_presets() -> None:
error=ChromaticColor.RED,
neutral=NeutralColor.ZINC,
)
await default_preset.save(session)
default_preset = await default_preset.save(session)
log.info('已创建默认主题预设')
@@ -522,6 +527,6 @@ async def init_default_file_apps() -> None:
extension=ext.lower(),
priority=i,
)
await ext_record.save(session)
ext_record = await ext_record.save(session)
log.info(f'已创建 {len(_DEFAULT_FILE_APPS)} 个默认文件查看器应用')

View File

@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
from sqlmodel import Field, Relationship, text, Index
from sqlmodel_ext import SQLModelBase, TableBaseMixin
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
if TYPE_CHECKING:
from .download import Download
@@ -28,13 +28,13 @@ class NodeType(StrEnum):
class Aria2ConfigurationBase(SQLModelBase):
"""Aria2配置基础模型"""
rpc_url: str | None = Field(default=None, max_length=255)
rpc_url: Str255 | None = None
"""RPC地址"""
rpc_secret: str | None = None
"""RPC密钥"""
temp_path: str | None = Field(default=None, max_length=255)
temp_path: Str255 | None = None
"""临时下载路径"""
max_concurrent: int = Field(default=5, ge=1, le=50)
@@ -70,19 +70,19 @@ class Node(SQLModelBase, TableBaseMixin):
status: NodeStatus = Field(default=NodeStatus.ONLINE)
"""节点状态"""
name: str = Field(max_length=255, unique=True)
name: Str255 = Field(unique=True)
"""节点名称"""
type: NodeType
"""节点类型"""
server: str = Field(max_length=255)
server: Str255
"""节点地址IP或域名"""
slave_key: str | None = Field(default=None, max_length=255)
slave_key: Str255 | None = None
"""从机通讯密钥"""
master_key: str | None = Field(default=None, max_length=255)
master_key: Str255 | None = None
"""主机通讯密钥"""
aria2_enabled: bool = False

View File

@@ -7,7 +7,7 @@ from enum import StrEnum
from sqlalchemy import BigInteger
from sqlmodel import Field, Relationship, CheckConstraint, Index, text
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str255, Str256
from .policy import PolicyType
@@ -25,7 +25,15 @@ class ObjectType(StrEnum):
"""对象类型枚举"""
FILE = "file"
FOLDER = "folder"
class FileCategory(StrEnum):
"""文件类型分类枚举,用于按类别筛选文件"""
IMAGE = "image"
VIDEO = "video"
AUDIO = "audio"
DOCUMENT = "document"
# ==================== Base 模型 ====================
@@ -190,13 +198,13 @@ class Object(ObjectBase, UUIDTableBaseMixin):
# ==================== 基础字段 ====================
name: str = Field(max_length=255)
name: Str255
"""对象名称(文件名或目录名)"""
type: ObjectType
"""对象类型file 或 folder"""
password: str | None = Field(default=None, max_length=255)
password: Str255 | None = None
"""对象独立密码(仅当用户为对象单独设置密码时有效)"""
# ==================== 文件专属字段 ====================
@@ -204,7 +212,7 @@ class Object(ObjectBase, UUIDTableBaseMixin):
size: int = Field(default=0, sa_type=BigInteger, sa_column_kwargs={"server_default": "0"})
"""文件大小(字节),目录为 0"""
upload_session_id: str | None = Field(default=None, max_length=255, unique=True, index=True)
upload_session_id: Str255 | None = Field(default=None, unique=True, index=True)
"""分块上传会话ID仅文件有效"""
physical_file_id: UUID | None = Field(
@@ -469,6 +477,37 @@ class Object(ObjectBase, UUIDTableBaseMixin):
fetch_mode="all"
)
@classmethod
async def get_by_category(
cls,
session: 'AsyncSession',
user_id: UUID,
extensions: list[str],
table_view: 'TableViewRequest | None' = None,
) -> 'ListResponse[Object]':
"""
按扩展名列表查询用户的所有文件(跨目录)
只查询未删除、未封禁的文件对象,使用 ILIKE 匹配文件名后缀。
:param session: 数据库会话
:param user_id: 用户UUID
:param extensions: 扩展名列表(不含点号)
:param table_view: 分页排序参数
:return: 分页文件列表
"""
from sqlalchemy import or_
ext_conditions = [cls.name.ilike(f"%.{ext}") for ext in extensions]
condition = (
(cls.owner_id == user_id) &
(cls.type == ObjectType.FILE) &
(cls.deleted_at == None) &
(cls.is_banned == False) &
or_(*ext_conditions)
)
return await cls.get_with_count(session, condition, table_view=table_view)
@classmethod
async def resolve_uri(
cls,
@@ -546,7 +585,7 @@ class Object(ObjectBase, UUIDTableBaseMixin):
class UploadSessionBase(SQLModelBase):
"""上传会话基础字段"""
file_name: str = Field(max_length=255)
file_name: Str255
"""原始文件名"""
file_size: int = Field(ge=0, sa_type=BigInteger)
@@ -577,7 +616,7 @@ class UploadSession(UploadSessionBase, UUIDTableBaseMixin):
storage_path: str | None = Field(default=None, max_length=512)
"""文件存储路径"""
s3_upload_id: str | None = Field(default=None, max_length=256)
s3_upload_id: Str256 | None = None
"""S3 Multipart Upload ID仅 S3 策略使用)"""
s3_part_etags: str | None = None
@@ -624,7 +663,7 @@ class UploadSession(UploadSessionBase, UUIDTableBaseMixin):
class CreateUploadSessionRequest(SQLModelBase):
"""创建上传会话请求 DTO"""
file_name: str = Field(max_length=255)
file_name: Str255
"""文件名"""
file_size: int = Field(ge=0)
@@ -681,7 +720,7 @@ class UploadChunkResponse(SQLModelBase):
class CreateFileRequest(SQLModelBase):
"""创建空白文件请求 DTO"""
name: str = Field(max_length=255)
name: Str255
"""文件名"""
parent_id: UUID
@@ -719,7 +758,7 @@ class ObjectRenameRequest(SQLModelBase):
id: UUID
"""对象UUID"""
new_name: str = Field(max_length=255)
new_name: Str255
"""新名称"""

View File

@@ -25,7 +25,7 @@ from uuid import UUID
from sqlmodel import Field, UniqueConstraint, Index, Relationship
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str255
if TYPE_CHECKING:
from .object import Object
@@ -65,7 +65,7 @@ USER_WRITABLE_NAMESPACES: set[str] = {MetadataNamespace.CUSTOM}
class ObjectMetadataBase(SQLModelBase):
"""对象元数据 KV 基础模型"""
name: str = Field(max_length=255)
name: Str255
"""元数据键名格式namespace:key如 exif:width, stream:duration"""
value: str
@@ -113,7 +113,7 @@ class MetadataResponse(SQLModelBase):
class MetadataPatchItem(SQLModelBase):
"""单条元数据补丁 DTO"""
key: str = Field(max_length=255)
key: Str255
"""元数据键名"""
value: str | None = None

View File

@@ -1,58 +1,122 @@
from decimal import Decimal
from enum import StrEnum
from typing import TYPE_CHECKING
from uuid import UUID
from sqlalchemy import Numeric
from sqlmodel import Field, Relationship
from sqlmodel_ext import SQLModelBase, TableBaseMixin
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
if TYPE_CHECKING:
from .product import Product
from .user import User
class OrderStatus(StrEnum):
"""订单状态枚举"""
PENDING = "pending"
"""待支付"""
COMPLETED = "completed"
"""已完成"""
CANCELLED = "cancelled"
"""已取消"""
class OrderType(StrEnum):
"""订单类型枚举"""
# [TODO] 补充具体订单类型
pass
STORAGE_PACK = "storage_pack"
"""容量包"""
GROUP_TIME = "group_time"
"""用户组时长"""
SCORE = "score"
"""积分充值"""
# ==================== DTO 模型 ====================
class CreateOrderRequest(SQLModelBase):
"""创建订单请求 DTO"""
product_id: UUID
"""商品UUID"""
num: int = Field(default=1, ge=1)
"""购买数量"""
method: str
"""支付方式"""
class OrderResponse(SQLModelBase):
"""订单响应 DTO"""
id: int
"""订单ID"""
order_no: str
"""订单号"""
type: OrderType
"""订单类型"""
method: str | None = None
"""支付方式"""
product_id: UUID | None = None
"""商品UUID"""
num: int
"""购买数量"""
name: str
"""商品名称"""
price: float
"""订单价格(元)"""
status: OrderStatus
"""订单状态"""
user_id: UUID
"""所属用户UUID"""
# ==================== 数据库模型 ====================
class Order(SQLModelBase, TableBaseMixin):
"""订单模型"""
order_no: str = Field(max_length=255, unique=True, index=True)
order_no: Str255 = Field(unique=True, index=True)
"""订单号,唯一"""
type: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
"""订单类型 [TODO] 待定义枚举"""
type: OrderType
"""订单类型"""
method: str | None = Field(default=None, max_length=255)
method: Str255 | None = None
"""支付方式"""
product_id: int | None = Field(default=None)
"""商品ID"""
product_id: UUID | None = Field(default=None, foreign_key="product.id", ondelete="SET NULL")
"""关联商品UUID"""
num: int = Field(default=1, sa_column_kwargs={"server_default": "1"})
"""购买数量"""
name: str = Field(max_length=255)
name: Str255
"""商品名称"""
price: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
"""订单价格("""
price: Decimal = Field(sa_type=Numeric(12, 2), default=Decimal("0.00"))
"""订单价格("""
status: OrderStatus = Field(default=OrderStatus.PENDING)
"""订单状态"""
# 外键
user_id: UUID = Field(
foreign_key="user.id",
@@ -60,6 +124,22 @@ class Order(SQLModelBase, TableBaseMixin):
ondelete="CASCADE"
)
"""所属用户UUID"""
# 关系
user: "User" = Relationship(back_populates="orders")
user: "User" = Relationship(back_populates="orders")
product: "Product" = Relationship(back_populates="orders")
def to_response(self) -> OrderResponse:
"""转换为响应 DTO"""
return OrderResponse(
id=self.id,
order_no=self.order_no,
type=self.type,
method=self.method,
product_id=self.product_id,
num=self.num,
name=self.name,
price=float(self.price),
status=self.status,
user_id=self.user_id,
)

View File

@@ -15,7 +15,7 @@ from uuid import UUID
from sqlalchemy import BigInteger
from sqlmodel import Field, Relationship, Index
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str32, Str64
if TYPE_CHECKING:
from .object import Object
@@ -31,10 +31,10 @@ class PhysicalFileBase(SQLModelBase):
size: int = Field(default=0, sa_type=BigInteger)
"""文件大小(字节)"""
checksum_md5: str | None = Field(default=None, max_length=32)
checksum_md5: Str32 | None = None
"""MD5校验和用于文件去重和完整性校验"""
checksum_sha256: str | None = Field(default=None, max_length=64)
checksum_sha256: Str64 | None = None
"""SHA256校验和"""

View File

@@ -4,7 +4,7 @@ from uuid import UUID
from enum import StrEnum
from sqlmodel import Field, Relationship, text
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str64, Str255
if TYPE_CHECKING:
from .object import Object
@@ -37,22 +37,22 @@ class PolicyType(StrEnum):
class PolicyBase(SQLModelBase):
"""存储策略基础字段,供 DTO 和数据库模型共享"""
name: str = Field(max_length=255)
name: Str255
"""策略名称"""
type: PolicyType
"""存储策略类型"""
server: str | None = Field(default=None, max_length=255)
server: Str255 | None = None
"""服务器地址(本地策略为绝对路径)"""
bucket_name: str | None = Field(default=None, max_length=255)
bucket_name: Str255 | None = None
"""存储桶名称"""
is_private: bool = True
"""是否为私有空间"""
base_url: str | None = Field(default=None, max_length=255)
base_url: Str255 | None = None
"""访问文件的基础URL"""
access_key: str | None = None
@@ -67,10 +67,10 @@ class PolicyBase(SQLModelBase):
auto_rename: bool = False
"""是否自动重命名"""
dir_name_rule: str | None = Field(default=None, max_length=255)
dir_name_rule: Str255 | None = None
"""目录命名规则"""
file_name_rule: str | None = Field(default=None, max_length=255)
file_name_rule: Str255 | None = None
"""文件命名规则"""
is_origin_link_enable: bool = False
@@ -115,7 +115,7 @@ class PolicyCreateRequest(PolicyBase):
mimetype: str | None = Field(default=None, max_length=127)
"""MIME类型"""
od_redirect: str | None = Field(default=None, max_length=255)
od_redirect: Str255 | None = None
"""OneDrive重定向地址"""
chunk_size: int = Field(default=52428800, ge=1)
@@ -124,26 +124,26 @@ class PolicyCreateRequest(PolicyBase):
s3_path_style: bool = False
"""是否使用S3路径风格"""
s3_region: str = Field(default='us-east-1', max_length=64)
s3_region: Str64 = 'us-east-1'
"""S3 区域(如 us-east-1、ap-southeast-1仅 S3 策略使用"""
class PolicyUpdateRequest(SQLModelBase):
"""更新存储策略请求 DTO所有字段可选"""
name: str | None = Field(default=None, max_length=255)
name: Str255 | None = None
"""策略名称"""
server: str | None = Field(default=None, max_length=255)
server: Str255 | None = None
"""服务器地址"""
bucket_name: str | None = Field(default=None, max_length=255)
bucket_name: Str255 | None = None
"""存储桶名称"""
is_private: bool | None = None
"""是否为私有空间"""
base_url: str | None = Field(default=None, max_length=255)
base_url: Str255 | None = None
"""访问文件的基础URL"""
access_key: str | None = None
@@ -158,10 +158,10 @@ class PolicyUpdateRequest(SQLModelBase):
auto_rename: bool | None = None
"""是否自动重命名"""
dir_name_rule: str | None = Field(default=None, max_length=255)
dir_name_rule: Str255 | None = None
"""目录命名规则"""
file_name_rule: str | None = Field(default=None, max_length=255)
file_name_rule: Str255 | None = None
"""文件命名规则"""
is_origin_link_enable: bool | None = None
@@ -177,7 +177,7 @@ class PolicyUpdateRequest(SQLModelBase):
mimetype: str | None = Field(default=None, max_length=127)
"""MIME类型"""
od_redirect: str | None = Field(default=None, max_length=255)
od_redirect: Str255 | None = None
"""OneDrive重定向地址"""
chunk_size: int | None = Field(default=None, ge=1)
@@ -186,7 +186,7 @@ class PolicyUpdateRequest(SQLModelBase):
s3_path_style: bool | None = None
"""是否使用S3路径风格"""
s3_region: str | None = Field(default=None, max_length=64)
s3_region: Str64 | None = None
"""S3 区域"""
@@ -205,7 +205,7 @@ class PolicyOptionsBase(SQLModelBase):
mimetype: str | None = Field(default=None, max_length=127)
"""MIME类型"""
od_redirect: str | None = Field(default=None, max_length=255)
od_redirect: Str255 | None = None
"""OneDrive重定向地址"""
chunk_size: int = Field(default=52428800, sa_column_kwargs={"server_default": "52428800"})
@@ -214,7 +214,7 @@ class PolicyOptionsBase(SQLModelBase):
s3_path_style: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")})
"""是否使用S3路径风格"""
s3_region: str = Field(default='us-east-1', max_length=64, sa_column_kwargs={"server_default": "'us-east-1'"})
s3_region: Str64 = Field(default='us-east-1', sa_column_kwargs={"server_default": "'us-east-1'"})
"""S3 区域(如 us-east-1、ap-southeast-1仅 S3 策略使用"""
@@ -237,7 +237,7 @@ class Policy(PolicyBase, UUIDTableBaseMixin):
"""存储策略模型"""
# 覆盖基类字段以添加数据库专有配置
name: str = Field(max_length=255, unique=True)
name: Str255 = Field(unique=True)
"""策略名称"""
is_private: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")})

206
sqlmodels/product.py Normal file
View File

@@ -0,0 +1,206 @@
from decimal import Decimal
from enum import StrEnum
from typing import TYPE_CHECKING
from uuid import UUID
from sqlalchemy import Numeric, BigInteger
from sqlmodel import Field, Relationship, text
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str255
if TYPE_CHECKING:
from .order import Order
from .redeem import Redeem
class ProductType(StrEnum):
"""商品类型枚举"""
STORAGE_PACK = "storage_pack"
"""容量包"""
GROUP_TIME = "group_time"
"""用户组时长"""
SCORE = "score"
"""积分充值"""
class PaymentMethod(StrEnum):
"""支付方式枚举"""
ALIPAY = "alipay"
"""支付宝"""
WECHAT = "wechat"
"""微信支付"""
STRIPE = "stripe"
"""Stripe"""
EASYPAY = "easypay"
"""易支付"""
CUSTOM = "custom"
"""自定义支付"""
# ==================== DTO 模型 ====================
class ProductBase(SQLModelBase):
"""商品基础字段"""
name: str
"""商品名称"""
type: ProductType
"""商品类型"""
description: str | None = None
"""商品描述"""
class ProductCreateRequest(ProductBase):
"""创建商品请求 DTO"""
name: Str255
"""商品名称"""
price: Decimal = Field(ge=0, decimal_places=2)
"""商品价格(元)"""
is_active: bool = True
"""是否上架"""
sort_order: int = Field(default=0, ge=0)
"""排序权重(越大越靠前)"""
# storage_pack 专用
size: int | None = Field(default=None, ge=0)
"""容量大小字节type=storage_pack 时必填"""
duration_days: int | None = Field(default=None, ge=1)
"""有效天数type=storage_pack/group_time 时必填"""
# group_time 专用
group_id: UUID | None = None
"""目标用户组UUIDtype=group_time 时必填"""
# score 专用
score_amount: int | None = Field(default=None, ge=1)
"""积分数量type=score 时必填"""
class ProductUpdateRequest(SQLModelBase):
"""更新商品请求 DTO所有字段可选"""
name: Str255 | None = None
"""商品名称"""
description: str | None = None
"""商品描述"""
price: Decimal | None = Field(default=None, ge=0, decimal_places=2)
"""商品价格(元)"""
is_active: bool | None = None
"""是否上架"""
sort_order: int | None = Field(default=None, ge=0)
"""排序权重"""
size: int | None = Field(default=None, ge=0)
"""容量大小(字节)"""
duration_days: int | None = Field(default=None, ge=1)
"""有效天数"""
group_id: UUID | None = None
"""目标用户组UUID"""
score_amount: int | None = Field(default=None, ge=1)
"""积分数量"""
class ProductResponse(ProductBase):
"""商品响应 DTO"""
id: UUID
"""商品UUID"""
price: float
"""商品价格(元)"""
is_active: bool
"""是否上架"""
sort_order: int
"""排序权重"""
size: int | None = None
"""容量大小(字节)"""
duration_days: int | None = None
"""有效天数"""
group_id: UUID | None = None
"""目标用户组UUID"""
score_amount: int | None = None
"""积分数量"""
# ==================== 数据库模型 ====================
class Product(ProductBase, UUIDTableBaseMixin):
"""商品模型"""
name: Str255
"""商品名称"""
price: Decimal = Field(sa_type=Numeric(12, 2), default=Decimal("0.00"))
"""商品价格(元)"""
is_active: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")})
"""是否上架"""
sort_order: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
"""排序权重(越大越靠前)"""
# storage_pack 专用
size: int | None = Field(default=None, sa_type=BigInteger)
"""容量大小字节type=storage_pack 时必填"""
duration_days: int | None = None
"""有效天数type=storage_pack/group_time 时必填"""
# group_time 专用
group_id: UUID | None = Field(default=None, foreign_key="group.id", ondelete="SET NULL")
"""目标用户组UUIDtype=group_time 时必填"""
# score 专用
score_amount: int | None = None
"""积分数量type=score 时必填"""
# 关系
orders: list["Order"] = Relationship(back_populates="product")
"""关联的订单列表"""
redeems: list["Redeem"] = Relationship(back_populates="product")
"""关联的兑换码列表"""
def to_response(self) -> ProductResponse:
"""转换为响应 DTO"""
return ProductResponse(
id=self.id,
name=self.name,
type=self.type,
description=self.description,
price=float(self.price),
is_active=self.is_active,
sort_order=self.sort_order,
size=self.size,
duration_days=self.duration_days,
group_id=self.group_id,
score_amount=self.score_amount,
)

View File

@@ -1,22 +1,141 @@
from datetime import datetime
from enum import StrEnum
from typing import TYPE_CHECKING
from uuid import UUID
from sqlmodel import Field, text
from sqlmodel import Field, Relationship, text
from sqlmodel_ext import SQLModelBase, TableBaseMixin
if TYPE_CHECKING:
from .product import Product
from .user import User
class RedeemType(StrEnum):
"""兑换码类型枚举"""
# [TODO] 补充具体兑换码类型
pass
STORAGE_PACK = "storage_pack"
"""容量包"""
GROUP_TIME = "group_time"
"""用户组时长"""
SCORE = "score"
"""积分充值"""
# ==================== DTO 模型 ====================
class RedeemCreateRequest(SQLModelBase):
"""批量生成兑换码请求 DTO"""
product_id: UUID
"""关联商品UUID"""
count: int = Field(default=1, ge=1, le=100)
"""生成数量"""
class RedeemUseRequest(SQLModelBase):
"""使用兑换码请求 DTO"""
code: str
"""兑换码"""
class RedeemInfoResponse(SQLModelBase):
"""兑换码信息响应 DTO用户侧"""
type: RedeemType
"""兑换码类型"""
product_name: str | None = None
"""关联商品名称"""
num: int
"""可兑换数量"""
is_used: bool
"""是否已使用"""
class RedeemAdminResponse(SQLModelBase):
"""兑换码管理响应 DTO管理侧"""
id: int
"""兑换码ID"""
type: RedeemType
"""兑换码类型"""
product_id: UUID | None = None
"""关联商品UUID"""
num: int
"""可兑换数量"""
code: str
"""兑换码"""
is_used: bool
"""是否已使用"""
used_at: datetime | None = None
"""使用时间"""
used_by: UUID | None = None
"""使用者UUID"""
# ==================== 数据库模型 ====================
class Redeem(SQLModelBase, TableBaseMixin):
"""兑换码模型"""
type: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
"""兑换码类型 [TODO] 待定义枚举"""
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="是否已使用")
type: RedeemType
"""兑换码类型"""
product_id: UUID | None = Field(default=None, foreign_key="product.id", ondelete="SET NULL")
"""关联商品UUID"""
num: int = Field(default=1, sa_column_kwargs={"server_default": "1"})
"""可兑换数量/时长等"""
code: str = Field(unique=True, index=True)
"""兑换码,唯一"""
is_used: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")})
"""是否已使用"""
used_at: datetime | None = None
"""使用时间"""
used_by: UUID | None = Field(default=None, foreign_key="user.id", ondelete="SET NULL")
"""使用者UUID"""
# 关系
product: "Product" = Relationship(back_populates="redeems")
user: "User" = Relationship(back_populates="redeems")
def to_admin_response(self) -> RedeemAdminResponse:
"""转换为管理侧响应 DTO"""
return RedeemAdminResponse(
id=self.id,
type=self.type,
product_id=self.product_id,
num=self.num,
code=self.code,
is_used=self.is_used,
used_at=self.used_at,
used_by=self.used_by,
)
def to_info_response(self, product_name: str | None = None) -> RedeemInfoResponse:
"""转换为用户侧响应 DTO"""
return RedeemInfoResponse(
type=self.type,
product_name=product_name,
num=self.num,
is_used=self.is_used,
)

View File

@@ -4,7 +4,7 @@ from uuid import UUID
from sqlmodel import Field, Relationship
from sqlmodel_ext import SQLModelBase, TableBaseMixin
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
if TYPE_CHECKING:
from .share import Share
@@ -21,7 +21,7 @@ class Report(SQLModelBase, TableBaseMixin):
reason: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
"""举报原因 [TODO] 待定义枚举"""
description: str | None = Field(default=None, max_length=255, description="补充描述")
description: Str255 | None = Field(default=None, description="补充描述")
# 外键
share_id: UUID = Field(

View File

@@ -163,6 +163,7 @@ class SettingsType(StrEnum):
VERSION = "version"
VIEW = "view"
WOPI = "wopi"
FILE_CATEGORY = "file_category"
# 数据库模型
class Setting(SettingItem, TableBaseMixin):

View File

@@ -5,7 +5,7 @@ from uuid import UUID
from sqlmodel import Field, Relationship, text, UniqueConstraint, Index
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str64, Str255
from .model_base import ResponseBase
from .object import ObjectType
@@ -52,10 +52,10 @@ class Share(SQLModelBase, UUIDTableBaseMixin):
Index("ix_share_object", "object_id"),
)
code: str = Field(max_length=64, nullable=False, index=True)
code: Str64 = Field(nullable=False, index=True)
"""分享码"""
password: str | None = Field(default=None, max_length=255)
password: Str255 | None = None
"""分享密码(加密后)"""
object_id: UUID = Field(
@@ -80,7 +80,7 @@ class Share(SQLModelBase, UUIDTableBaseMixin):
preview_enabled: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")})
"""是否允许预览"""
source_name: str | None = Field(default=None, max_length=255)
source_name: Str255 | None = None
"""源名称(冗余字段,便于展示)"""
score: int = Field(default=0, ge=0)

View File

@@ -4,7 +4,7 @@ from uuid import UUID
from sqlmodel import Field, Relationship, Index
from sqlmodel_ext import SQLModelBase, TableBaseMixin
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
if TYPE_CHECKING:
from .object import Object
@@ -17,7 +17,7 @@ class SourceLink(SQLModelBase, TableBaseMixin):
Index("ix_sourcelink_object_name", "object_id", "name"),
)
name: str = Field(max_length=255)
name: Str255
"""链接名称"""
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"})

View File

@@ -1,23 +1,60 @@
from typing import TYPE_CHECKING
from datetime import datetime
from typing import TYPE_CHECKING
from uuid import UUID
from sqlmodel import Field, Relationship, Column, func, DateTime
from sqlalchemy import BigInteger
from sqlmodel import Field, Relationship
from sqlmodel_ext import SQLModelBase, TableBaseMixin
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
if TYPE_CHECKING:
from .user import User
# ==================== DTO 模型 ====================
class StoragePackResponse(SQLModelBase):
"""容量包响应 DTO"""
id: int
"""容量包ID"""
name: str
"""容量包名称"""
size: int
"""容量大小(字节)"""
active_time: datetime | None = None
"""激活时间"""
expired_time: datetime | None = None
"""过期时间"""
product_id: UUID | None = None
"""来源商品UUID"""
# ==================== 数据库模型 ====================
class StoragePack(SQLModelBase, TableBaseMixin):
"""容量包模型"""
name: str = Field(max_length=255, description="容量包名称")
active_time: datetime | None = Field(default=None, description="激活时间")
expired_time: datetime | None = Field(default=None, index=True, description="过期时间")
size: int = Field(description="容量包大小(字节)")
name: Str255
"""容量包名称"""
active_time: datetime | None = None
"""激活时间"""
expired_time: datetime | None = Field(default=None, index=True)
"""过期时间"""
size: int = Field(sa_type=BigInteger)
"""容量包大小(字节)"""
product_id: UUID | None = Field(default=None, foreign_key="product.id", ondelete="SET NULL")
"""来源商品UUID"""
# 外键
user_id: UUID = Field(
foreign_key="user.id",
@@ -25,6 +62,17 @@ class StoragePack(SQLModelBase, TableBaseMixin):
ondelete="CASCADE"
)
"""所属用户UUID"""
# 关系
user: "User" = Relationship(back_populates="storage_packs")
user: "User" = Relationship(back_populates="storage_packs")
def to_response(self) -> StoragePackResponse:
"""转换为响应 DTO"""
return StoragePackResponse(
id=self.id,
name=self.name,
size=self.size,
active_time=self.active_time,
expired_time=self.expired_time,
product_id=self.product_id,
)

View File

@@ -5,7 +5,7 @@ from datetime import datetime
from sqlmodel import Field, Relationship, UniqueConstraint, Column, func, DateTime
from sqlmodel_ext import SQLModelBase, TableBaseMixin
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
if TYPE_CHECKING:
from .user import User
@@ -24,13 +24,13 @@ class Tag(SQLModelBase, TableBaseMixin):
__table_args__ = (UniqueConstraint("name", "user_id", name="uq_tag_name_user"),)
name: str = Field(max_length=255)
name: Str255
"""标签名称"""
icon: str | None = Field(default=None, max_length=255)
icon: Str255 | None = None
"""标签图标"""
color: str | None = Field(default=None, max_length=255)
color: Str255 | None = None
"""标签颜色"""
type: TagType = Field(default=TagType.MANUAL)

View File

@@ -3,7 +3,7 @@ from uuid import UUID
from sqlmodel import Field
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str100
from .color import ChromaticColor, NeutralColor, ThemeColorsBase
@@ -11,7 +11,7 @@ from .color import ChromaticColor, NeutralColor, ThemeColorsBase
class ThemePresetBase(SQLModelBase):
"""主题预设基础字段"""
name: str = Field(max_length=100)
name: Str100
"""预设名称"""
is_default: bool = False
@@ -42,7 +42,7 @@ class ThemePresetBase(SQLModelBase):
class ThemePreset(ThemePresetBase, UUIDTableBaseMixin):
"""主题预设表"""
name: str = Field(max_length=100, unique=True)
name: Str100 = Field(unique=True)
"""预设名称(唯一约束)"""
@@ -51,7 +51,7 @@ class ThemePreset(ThemePresetBase, UUIDTableBaseMixin):
class ThemePresetCreateRequest(SQLModelBase):
"""创建主题预设请求 DTO"""
name: str = Field(max_length=100)
name: Str100
"""预设名称"""
colors: ThemeColorsBase
@@ -61,7 +61,7 @@ class ThemePresetCreateRequest(SQLModelBase):
class ThemePresetUpdateRequest(SQLModelBase):
"""更新主题预设请求 DTO"""
name: str | None = Field(default=None, max_length=100)
name: Str100 | None = None
"""预设名称(可选)"""
colors: ThemeColorsBase | None = None

View File

@@ -9,7 +9,7 @@ from sqlmodel import Field, Relationship
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlmodel.main import RelationshipInfo
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, TableViewRequest, ListResponse
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, TableViewRequest, ListResponse, Str255
from .auth_identity import AuthProviderType
from .color import ChromaticColor, NeutralColor, ThemeColorsBase
@@ -23,6 +23,7 @@ if TYPE_CHECKING:
from .download import Download
from .object import Object
from .order import Order
from .redeem import Redeem
from .share import Share
from .storage_pack import StoragePack
from .tag import Tag
@@ -476,7 +477,7 @@ class User(UserBase, UUIDTableBaseMixin):
storage: int = Field(default=0, sa_type=BigInteger, sa_column_kwargs={"server_default": "0"}, ge=0)
"""已用存储空间(字节)"""
avatar: str = Field(default="default", max_length=255)
avatar: Str255 = Field(default="default")
"""头像地址"""
score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, ge=0)
@@ -570,6 +571,14 @@ class User(UserBase, UUIDTableBaseMixin):
back_populates="user",
sa_relationship_kwargs={"cascade": "all, delete-orphan"}
)
redeems: list["Redeem"] = Relationship(
back_populates="user",
sa_relationship_kwargs={
"cascade": "all, delete-orphan",
"foreign_keys": "[Redeem.used_by]"
}
)
"""用户使用过的兑换码列表"""
shares: list["Share"] = Relationship(
back_populates="user",
sa_relationship_kwargs={"cascade": "all, delete-orphan"}

View File

@@ -5,7 +5,7 @@ from uuid import UUID
from sqlalchemy import Column, Text
from sqlmodel import Field, Relationship
from sqlmodel_ext import SQLModelBase, TableBaseMixin
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str32, Str100, Str255
if TYPE_CHECKING:
from .user import User
@@ -51,7 +51,7 @@ class AuthnDetailResponse(SQLModelBase):
class AuthnRenameRequest(SQLModelBase):
"""WebAuthn 凭证重命名请求 DTO"""
name: str = Field(max_length=100)
name: Str100
"""新的凭证名称"""
@@ -60,7 +60,7 @@ class AuthnRenameRequest(SQLModelBase):
class UserAuthn(SQLModelBase, TableBaseMixin):
"""用户 WebAuthn 凭证模型,与 User 为多对一关系"""
credential_id: str = Field(max_length=255, unique=True, index=True)
credential_id: Str255 = Field(unique=True, index=True)
"""凭证 IDBase64URL 编码"""
credential_public_key: str = Field(sa_column=Column(Text))
@@ -69,16 +69,16 @@ class UserAuthn(SQLModelBase, TableBaseMixin):
sign_count: int = Field(default=0, ge=0)
"""签名计数器,用于防重放攻击"""
credential_device_type: str = Field(max_length=32)
credential_device_type: Str32
"""凭证设备类型:'single_device''multi_device'"""
credential_backed_up: bool = Field(default=False)
"""凭证是否已备份"""
transports: str | None = Field(default=None, max_length=255)
transports: Str255 | None = None
"""支持的传输方式,逗号分隔,如 'usb,nfc,ble,internal'"""
name: str | None = Field(default=None, max_length=100)
name: Str100 | None = None
"""用户自定义的凭证名称,便于识别"""
# 外键

View File

@@ -9,7 +9,7 @@ from uuid import UUID
from sqlmodel import Field, Relationship, UniqueConstraint
from sqlmodel_ext import SQLModelBase, TableBaseMixin
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
if TYPE_CHECKING:
from .user import User
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
class WebDAVBase(SQLModelBase):
"""WebDAV 账户基础字段"""
name: str = Field(max_length=255)
name: Str255
"""账户名称(同一用户下唯一)"""
root: str = Field(default="/", sa_column_kwargs={"server_default": "'/'"})
@@ -40,7 +40,7 @@ class WebDAV(WebDAVBase, TableBaseMixin):
__table_args__ = (UniqueConstraint("name", "user_id", name="uq_webdav_name_user"),)
password: str = Field(max_length=255)
password: Str255
"""密码Argon2 哈希)"""
# 外键
@@ -60,10 +60,10 @@ class WebDAV(WebDAVBase, TableBaseMixin):
class WebDAVCreateRequest(SQLModelBase):
"""创建 WebDAV 账户请求"""
name: str = Field(max_length=255)
name: Str255
"""账户名称"""
password: str = Field(min_length=1, max_length=255)
password: Str255 = Field(min_length=1)
"""账户密码(明文,服务端哈希后存储)"""
root: str = "/"
@@ -79,7 +79,7 @@ class WebDAVCreateRequest(SQLModelBase):
class WebDAVUpdateRequest(SQLModelBase):
"""更新 WebDAV 账户请求"""
password: str | None = Field(default=None, min_length=1, max_length=255)
password: Str255 | None = Field(default=None, min_length=1)
"""新密码(为 None 时不修改)"""
root: str | None = None