- 替换 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:
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"""
|
||||
|
||||
# 关系
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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)} 个默认文件查看器应用')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
"""新名称"""
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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校验和"""
|
||||
|
||||
|
||||
|
||||
@@ -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
206
sqlmodels/product.py
Normal 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
|
||||
"""目标用户组UUID,type=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")
|
||||
"""目标用户组UUID,type=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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -163,6 +163,7 @@ class SettingsType(StrEnum):
|
||||
VERSION = "version"
|
||||
VIEW = "view"
|
||||
WOPI = "wopi"
|
||||
FILE_CATEGORY = "file_category"
|
||||
|
||||
# 数据库模型
|
||||
class Setting(SettingItem, TableBaseMixin):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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)
|
||||
"""凭证 ID,Base64URL 编码"""
|
||||
|
||||
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
|
||||
"""用户自定义的凭证名称,便于识别"""
|
||||
|
||||
# 外键
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user