feat: Enhance file management and user features

- Add file deduplication mechanism based on PhysicalFile reference counting.
- Implement chunked upload support for large files with resumable uploads.
- Update sharing page to automatically render README and preview content.
- Integrate Redis for caching and token storage (optional).
- Refactor project structure to include new models for download tasks, nodes, and tasks.
- Introduce user filtering parameters for admin user management.
- Add CORS middleware for handling cross-origin requests.
- Improve error messages for authentication failures.
- Update user model to include two-factor authentication key management.
- Enhance API documentation and response models for clarity.
- Implement admin checks for user management and permissions.
This commit is contained in:
2026-01-13 15:29:52 +08:00
parent 61ddc96f17
commit b12aad4e73
13 changed files with 467 additions and 4512 deletions

4407
.xml

File diff suppressed because it is too large Load Diff

View File

@@ -27,10 +27,12 @@
- **多存储策略**支持本地存储、S3 兼容 API、阿里云 OSS、OneDrive 等多种存储后端 - **多存储策略**支持本地存储、S3 兼容 API、阿里云 OSS、OneDrive 等多种存储后端
- **远程节点**:可对接从节点分担存储和下载任务 - **远程节点**:可对接从节点分担存储和下载任务
- **WebDAV 兼容**:提供标准 WebDAV 接口,支持第三方客户端访问 - **WebDAV 兼容**:提供标准 WebDAV 接口,支持第三方客户端访问
- **文件去重**:基于 PhysicalFile 引用计数的文件去重机制
### 文件管理 ### 文件管理
- **统一对象模型**:文件和目录采用统一的 Object 模型管理 - **统一对象模型**:文件和目录采用统一的 Object 模型管理
- **分片上传**:支持大文件分片上传,可断点续传
- **在线压缩/解压**:支持批量打包下载 - **在线压缩/解压**:支持批量打包下载
- **离线下载**:内置离线下载服务,也可对接 Aria2/qBittorrent - **离线下载**:内置离线下载服务,也可对接 Aria2/qBittorrent
@@ -50,7 +52,7 @@
### 分享功能 ### 分享功能
- **分享链接管理**:可设置密码、过期时间 - **分享链接管理**:可设置密码、过期时间
- **分享页展示** 自动渲染分享中的 README、在线预览内容 - **分享页展示**自动渲染分享中的 README、在线预览内容
### 增值服务 ### 增值服务
@@ -67,8 +69,10 @@
| [Python 3.13+](https://www.python.org/) | 编程语言 | | [Python 3.13+](https://www.python.org/) | 编程语言 |
| [FastAPI](https://fastapi.tiangolo.com/) | 高性能异步 Web 框架 | | [FastAPI](https://fastapi.tiangolo.com/) | 高性能异步 Web 框架 |
| [SQLModel](https://sqlmodel.tiangolo.com/) | 类型安全的 ORMSQLAlchemy + Pydantic | | [SQLModel](https://sqlmodel.tiangolo.com/) | 类型安全的 ORMSQLAlchemy + Pydantic |
| [Redis](https://redis.io/) | 缓存与令牌存储(可选) |
| [aiohttp](https://docs.aiohttp.org/) | 异步 HTTP 客户端 | | [aiohttp](https://docs.aiohttp.org/) | 异步 HTTP 客户端 |
| [aiosqlite](https://aiosqlite.omnilib.dev/) | 异步 SQLite 驱动 | | [aiosqlite](https://aiosqlite.omnilib.dev/) | 异步 SQLite 驱动 |
| [asyncpg](https://magicstack.github.io/asyncpg/) | 异步 PostgreSQL 驱动 |
| [Loguru](https://loguru.readthedocs.io/) | 现代化日志库 | | [Loguru](https://loguru.readthedocs.io/) | 现代化日志库 |
| [PyJWT](https://pyjwt.readthedocs.io/) | JWT 令牌处理 | | [PyJWT](https://pyjwt.readthedocs.io/) | JWT 令牌处理 |
| [WebAuthn](https://pypi.org/project/webauthn/) | Passkey 认证支持 | | [WebAuthn](https://pypi.org/project/webauthn/) | Passkey 认证支持 |
@@ -81,35 +85,59 @@
Server/ Server/
├── main.py # 应用入口 ├── main.py # 应用入口
├── models/ # 数据模型 ├── models/ # 数据模型
│ ├── base/ # 基类定义 (SQLModelBase, TableBase) │ ├── base/ # 基类定义 (SQLModelBase)
│ ├── mixin/ # Mixin 模块 (TableBaseMixin, UUIDTableBaseMixin)
│ ├── user.py # 用户模型 │ ├── user.py # 用户模型
│ ├── user_authn.py # WebAuthn 凭证
│ ├── group.py # 用户组模型 │ ├── group.py # 用户组模型
│ ├── object.py # 文件/目录统一模型 │ ├── object.py # 文件/目录统一模型 + 上传会话
│ ├── physical_file.py # 物理文件模型(文件去重)
│ ├── policy.py # 存储策略模型 │ ├── policy.py # 存储策略模型
│ ├── share.py # 分享模型 │ ├── share.py # 分享模型
│ ├── download.py # 离线下载任务
│ ├── node.py # 节点模型
│ ├── task.py # 任务模型
│ └── ... │ └── ...
├── routers/ # API 路由 ├── routers/ # API 路由
── api/v1/ # v1 版本 API ── api/v1/ # v1 版本 API
├── user/ # 用户相关接口 ├── user/ # 用户相关接口
├── directory/ # 目录相关接口 ├── directory/ # 目录相关接口
├── file/ # 文件相关接口 ├── file/ # 文件上传/下载接口
├── admin/ # 管理员接口 ├── object/ # 对象操作接口
── ... ── share/ # 分享接口
│ │ ├── admin/ # 管理员接口
│ │ │ ├── user/ # 用户管理
│ │ │ ├── group/ # 用户组管理
│ │ │ ├── policy/ # 存储策略管理
│ │ │ ├── file/ # 文件管理
│ │ │ ├── share/ # 分享管理
│ │ │ ├── task/ # 任务管理
│ │ │ └── vas/ # 增值服务管理
│ │ └── ...
│ └── dav/ # WebDAV 路由
├── service/ # 业务服务层 ├── service/ # 业务服务层
│ ├── user/ # 用户服务 │ ├── user/ # 用户服务(登录)
│ ├── captcha/ # 验证码服务 │ ├── storage/ # 存储服务(本地存储)
── oauth/ # OAuth 服务 ── captcha/ # 验证码服务reCAPTCHA、Turnstile
│ ├── oauth/ # OAuth 服务QQ、GitHub
│ └── redis/ # Redis 服务(连接管理、令牌存储)
├── middleware/ # 中间件 ├── middleware/ # 中间件
│ ├── auth.py # 认证中间件 │ ├── auth.py # 认证中间件
│ └── dependencies.py # 依赖注入 │ └── dependencies.py # 依赖注入
├── utils/ # 工具函数 ├── utils/ # 工具函数
│ ├── JWT/ # JWT 处理 │ ├── JWT/ # JWT 处理
│ ├── password/ # 密码处理 │ ├── password/ # 密码处理Argon2、TOTP
│ ├── conf/ # 配置管理 │ ├── conf/ # 配置管理
│ ├── http/ # HTTP 异常处理
│ └── lifespan/ # 生命周期管理 │ └── lifespan/ # 生命周期管理
└── tests/ # 测试用例 └── tests/ # 测试用例
├── unit/ # 单元测试 ├── unit/ # 单元测试
│ ├── models/ # 模型测试
│ ├── service/ # 服务测试
│ └── utils/ # 工具测试
├── integration/ # 集成测试 ├── integration/ # 集成测试
│ ├── api/ # API 测试
│ └── middleware/ # 中间件测试
└── fixtures/ # 测试夹具 └── fixtures/ # 测试夹具
``` ```
@@ -121,7 +149,7 @@ Server/
| 用户 | `/api/v1/user` | 用户注册、登录、设置 | | 用户 | `/api/v1/user` | 用户注册、登录、设置 |
| 目录 | `/api/v1/directory` | 目录浏览和管理 | | 目录 | `/api/v1/directory` | 目录浏览和管理 |
| 文件 | `/api/v1/file` | 文件上传、下载、管理 | | 文件 | `/api/v1/file` | 文件上传、下载、管理 |
| 对象 | `/api/v1/object` | 文件和目录的通用操作 | | 对象 | `/api/v1/object` | 文件和目录的通用操作(删除、移动、复制、重命名) |
| 分享 | `/api/v1/share` | 分享链接管理 | | 分享 | `/api/v1/share` | 分享链接管理 |
| 下载 | `/api/v1/download` | 离线下载管理 | | 下载 | `/api/v1/download` | 离线下载管理 |
| 标签 | `/api/v1/tag` | 用户标签管理 | | 标签 | `/api/v1/tag` | 用户标签管理 |
@@ -137,6 +165,7 @@ Server/
- Python 3.13 或更高版本 - Python 3.13 或更高版本
- uv (推荐) 或 pip - uv (推荐) 或 pip
- Redis (可选,用于令牌存储和缓存)
### 安装 ### 安装
@@ -157,8 +186,17 @@ uv sync
# 调试模式 # 调试模式
DEBUG=false DEBUG=false
# 运行模式: master主节点或 slave从节点
MODE=master
# 数据库连接(默认使用 SQLite # 数据库连接(默认使用 SQLite
DATABASE_URL=sqlite+aiosqlite:///disknext.db DATABASE_URL=sqlite+aiosqlite:///disknext.db
# Redis 配置(可选,不配置则使用内存缓存)
REDIS_URL=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
``` ```
### 启动 ### 启动
@@ -171,7 +209,9 @@ fastapi dev
fastapi run fastapi run
``` ```
访问 <http://localhost:8000/docs> 查看 API 文档。 访问 <http://localhost:8000/docs> 查看 API 文档(仅 DEBUG=true 时可用)
首次启动会自动初始化数据库并创建默认管理员账户,**请注意控制台输出的初始密码**。
## 测试 ## 测试

View File

@@ -24,17 +24,25 @@
- [x] FastAPI 应用框架搭建 - [x] FastAPI 应用框架搭建
- [x] SQLModel ORM 集成 - [x] SQLModel ORM 集成
- [x] 异步数据库支持 (aiosqlite) - [x] 异步数据库支持 (aiosqlite, asyncpg)
- [x] 项目结构规范化 - [x] 项目结构规范化
- [x] 开发规范文档 (CLAUDE.md) - [x] 开发规范文档 (AGENTS.md)
- [x] Redis 集成(可选,支持降级)
- [x] 生命周期管理(启动/关闭事件)
- [x] 数据库自动迁移/初始化
#### 数据模型 #### 数据模型
- [x] 基类定义 (SQLModelBase, TableBase, UUIDTableBase) - [x] 基类定义 (SQLModelBase, TableBaseMixin, UUIDTableBaseMixin)
- [x] 自定义元类支持联表继承和多态
- [x] 用户模型 (User) - [x] 用户模型 (User)
- [x] 用户 WebAuthn 凭证模型 (UserAuthn)
- [x] 用户组模型 (Group, GroupOptions) - [x] 用户组模型 (Group, GroupOptions)
- [x] 统一对象模型 (Object) - 合并文件和目录 - [x] 统一对象模型 (Object) - 合并文件和目录
- [x] 存储策略模型 (Policy) - [x] 物理文件模型 (PhysicalFile) - 文件去重
- [x] 上传会话模型 (UploadSession)
- [x] 文件元数据模型 (FileMetadata)
- [x] 存储策略模型 (Policy, PolicyOptions)
- [x] 分享模型 (Share) - [x] 分享模型 (Share)
- [x] 标签模型 (Tag) - [x] 标签模型 (Tag)
- [x] WebDAV 模型 (WebDAV) - [x] WebDAV 模型 (WebDAV)
@@ -48,21 +56,62 @@
- [x] JWT 令牌认证 - [x] JWT 令牌认证
- [x] 获取当前用户信息 - [x] 获取当前用户信息
- [x] 用户存储空间查询 - [x] 用户存储空间查询
- [x] 用户设置获取
#### 目录系统
- [x] 目录浏览接口(路径解析)
- [x] 目录创建接口
#### 文件系统
- [x] 创建上传会话
- [x] 分片上传接口
- [x] 上传会话管理(删除、清除)
- [x] 创建空白文件
- [x] 下载令牌生成
- [x] 文件下载(一次性令牌)
#### 对象操作
- [x] 对象删除(支持递归删除目录)
- [x] 对象移动
- [x] 对象复制(引用计数,不复制物理文件)
- [x] 对象重命名
- [x] 对象属性查询(基本属性、详细属性)
#### 认证安全 #### 认证安全
- [x] Argon2 密码哈希 - [x] Argon2 密码哈希
- [x] JWT 令牌生成与验证 - [x] JWT 令牌生成与验证
- [x] 认证中间件 - [x] 认证中间件
- [x] 管理员权限中间件
- [x] 两步验证 (2FA/TOTP) 初始化与启用 - [x] 两步验证 (2FA/TOTP) 初始化与启用
- [x] WebAuthn 注册初始化 - [x] WebAuthn 注册初始化
- [x] 下载令牌验证(一次性使用)
- [x] 令牌存储Redis/内存降级)
#### 存储策略
- [x] 本地存储策略实现
- [x] 存储路径生成(支持命名规则)
- [x] 文件写入/读取/删除
- [x] 回收站机制
#### 管理后台
- [x] 管理员权限验证
- [x] 站点概况统计(用户数、文件数、分享数趋势)
- [x] 设置管理(获取、批量更新)
- [x] Aria2 连接测试
#### 测试 #### 测试
- [x] pytest 测试框架配置 - [x] pytest 测试框架配置
- [x] 单元测试结构 - [x] 单元测试结构models, service, utils
- [x] 集成测试结构 - [x] 集成测试结构api, middleware
- [x] 测试夹具 (fixtures) - [x] 测试夹具 (fixtures)
- [x] 覆盖率报告配置
### 进行中 ### 进行中
@@ -73,15 +122,8 @@
- [ ] 用户设置管理 - [ ] 用户设置管理
- [ ] 头像上传/Gravatar - [ ] 头像上传/Gravatar
#### 目录系统
- [ ] 目录浏览接口
- [ ] 目录创建接口
- [ ] 路径解析优化
#### 存储策略 #### 存储策略
- [ ] 本地存储策略实现
- [ ] S3 存储策略实现 - [ ] S3 存储策略实现
--- ---
@@ -90,21 +132,16 @@
### 文件操作 ### 文件操作
- [ ] 文件上传(单文件)
- [ ] 文件上传(分块上传)
- [ ] 文件下载
- [ ] 文件预览 URL 生成 - [ ] 文件预览 URL 生成
- [ ] 缩略图生成 - [ ] 缩略图生成
- [ ] 文件移动/复制 - [ ] 文件内容获取(文本文件)
- [ ] 文件重命名 - [ ] Office 文档预览
- [ ] 文件删除(软删除/回收站) - [ ] 文件外链功能
### 目录操作 ### 目录操作
- [ ] 目录树查询 - [ ] 目录树查询
- [ ] 目录移动/复制 - [ ] 批量操作优化
- [ ] 目录删除(递归)
- [ ] 批量操作
### 存储策略完善 ### 存储策略完善
@@ -117,10 +154,17 @@
### 用户组权限 ### 用户组权限
- [ ] 权限验证中间件 - [ ] 权限验证中间件
- [ ] 存储空间限制 - [ ] 存储空间限制检查
- [ ] 文件类型限制 - [ ] 文件类型限制
- [ ] 单文件大小限制 - [ ] 单文件大小限制
### 管理后台完善
- [ ] 用户管理接口
- [ ] 用户组管理接口
- [ ] 存储策略管理接口
- [ ] 文件管理接口(封禁/解封)
### Webhook 事件系统 ### Webhook 事件系统
- [ ] 文件事件推送(创建、修改、删除、分享) - [ ] 文件事件推送(创建、修改、删除、分享)
@@ -161,15 +205,12 @@
- [ ] 存储容量包 - [ ] 存储容量包
- [ ] 订单管理 - [ ] 订单管理
### 管理后台 ### 文件处理
- [ ] 用户管理接口 - [ ] 文件压缩任务
- [ ] 用户组管理接口 - [ ] 文件解压任务
- [ ] 存储策略管理接口 - [ ] 文件转移任务
- [ ] 系统设置接口 - [ ] 文件搜索功能
- [ ] 任务管理接口
- [ ] 文件管理接口
- [ ] 数据统计接口
### 协作功能 ### 协作功能
@@ -416,4 +457,4 @@
--- ---
*最后更新2025年12* *最后更新2026年1月*

10
main.py
View File

@@ -1,6 +1,7 @@
from typing import NoReturn from typing import NoReturn
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from utils.conf import appmeta from utils.conf import appmeta
from utils.http.http_exceptions import raise_internal_error from utils.http.http_exceptions import raise_internal_error
@@ -33,6 +34,15 @@ app = FastAPI(
openapi_url="/openapi.json" if appmeta.debug else None, openapi_url="/openapi.json" if appmeta.debug else None,
) )
# 配置 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 开发环境允许所有来源,生产环境应该限制为具体域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.exception_handler(Exception) @app.exception_handler(Exception)
async def handle_unexpected_exceptions(request: Request, exc: Exception) -> NoReturn: async def handle_unexpected_exceptions(request: Request, exc: Exception) -> NoReturn:
""" """

View File

@@ -33,7 +33,7 @@ async def auth_required(
return user return user
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
http_exceptions.raise_unauthorized("账号或密码错误") http_exceptions.raise_unauthorized("凭据过期或无效")
async def admin_required( async def admin_required(
user: Annotated[User, Depends(auth_required)], user: Annotated[User, Depends(auth_required)],

View File

@@ -5,15 +5,18 @@ FastAPI 依赖注入
- SessionDep: 数据库会话依赖 - SessionDep: 数据库会话依赖
- TimeFilterRequestDep: 时间筛选查询依赖(用于 count 等统计接口) - TimeFilterRequestDep: 时间筛选查询依赖(用于 count 等统计接口)
- TableViewRequestDep: 分页排序查询依赖(包含时间筛选 + 分页排序) - TableViewRequestDep: 分页排序查询依赖(包含时间筛选 + 分页排序)
- UserFilterParamsDep: 用户筛选参数依赖(用于管理员用户列表)
""" """
from datetime import datetime from datetime import datetime
from typing import Annotated, Literal, TypeAlias from typing import Annotated, Literal, TypeAlias
from uuid import UUID
from fastapi import Depends, Query from fastapi import Depends, Query
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from models.database import get_session from models.database import get_session
from models.mixin import TimeFilterRequest, TableViewRequest from models.mixin import TimeFilterRequest, TableViewRequest
from models.user import UserFilterParams, UserStatus
# --- 数据库会话依赖 --- # --- 数据库会话依赖 ---
@@ -70,3 +73,24 @@ async def _get_table_view_queries(
TableViewRequestDep: TypeAlias = Annotated[TableViewRequest, Depends(_get_table_view_queries)] TableViewRequestDep: TypeAlias = Annotated[TableViewRequest, Depends(_get_table_view_queries)]
"""获取分页排序和时间筛选参数的依赖""" """获取分页排序和时间筛选参数的依赖"""
# --- 用户筛选依赖 ---
async def _get_user_filter_params(
group_id: Annotated[UUID | None, Query(description="按用户组UUID筛选")] = None,
username: Annotated[str | None, Query(max_length=50, description="按用户名模糊搜索")] = None,
nickname: Annotated[str | None, Query(max_length=50, description="按昵称模糊搜索")] = None,
status: Annotated[UserStatus | None, Query(description="按用户状态筛选")] = None,
) -> UserFilterParams:
"""解析用户过滤查询参数"""
return UserFilterParams(
group_id=group_id,
username_contains=username,
nickname_contains=nickname,
status=status,
)
UserFilterParamsDep: TypeAlias = Annotated[UserFilterParams, Depends(_get_user_filter_params)]
"""获取用户筛选参数的依赖(用于管理员用户列表)"""

View File

@@ -8,10 +8,10 @@
models/ models/
├── base/ # 基础模型类 ├── base/ # 基础模型类
│ ├── __init__.py # 导出 SQLModelBase │ ├── __init__.py # 导出 SQLModelBase
│ └── sqlmodel_base.py # SQLModelBase 基类(元类魔法 │ └── sqlmodel_base.py # SQLModelBase 基类(自定义元类,支持联表继承
├── mixin/ # Mixin 模块 ├── mixin/ # Mixin 模块
│ ├── __init__.py # 统一导出 │ ├── __init__.py # 统一导出
│ ├── table.py # TableBaseMixin, UUIDTableBaseMixinCRUD + 时间戳) │ ├── table.py # TableBaseMixin, UUIDTableBaseMixinCRUD + 时间戳 + 分页
│ ├── polymorphic.py # 联表继承工具create_subclass_id_mixin 等) │ ├── polymorphic.py # 联表继承工具create_subclass_id_mixin 等)
│ └── info_response.py # DTO 用的 id/时间戳 Mixin │ └── info_response.py # DTO 用的 id/时间戳 Mixin
├── user.py # 用户模型 ├── user.py # 用户模型
@@ -19,7 +19,7 @@ models/
├── group.py # 用户组模型 ├── group.py # 用户组模型
├── policy.py # 存储策略模型 ├── policy.py # 存储策略模型
├── physical_file.py # 物理文件模型(文件去重) ├── physical_file.py # 物理文件模型(文件去重)
├── object.py # 统一对象模型(文件/目录)+ 上传会话 ├── object.py # 统一对象模型(文件/目录)+ 上传会话 + 文件元数据
├── share.py # 分享模型 ├── share.py # 分享模型
├── tag.py # 标签模型 ├── tag.py # 标签模型
├── download.py # 离线下载任务 ├── download.py # 离线下载任务
@@ -33,7 +33,8 @@ models/
├── storage_pack.py # 容量包模型 ├── storage_pack.py # 容量包模型
├── webdav.py # WebDAV 账户模型 ├── webdav.py # WebDAV 账户模型
├── color.py # 主题颜色 DTO ├── color.py # 主题颜色 DTO
├── model_base.py # 响应基类 DTO ├── model_base.py # 响应基类 DTOResponseBase, MCP 等)
├── migration.py # 数据库初始化和迁移
└── database.py # 数据库连接配置 └── database.py # 数据库连接配置
``` ```
@@ -43,9 +44,13 @@ models/
### SQLModelBase ### SQLModelBase
所有模型的基类,位于 `models.base.sqlmodel_base`配置了 所有模型的基类,位于 `models.base.sqlmodel_base`使用自定义元类 `__DeclarativeMeta` 实现
- `use_attribute_docstrings=True`:使用属性后的 docstring 作为字段描述 - `use_attribute_docstrings=True`:使用属性后的 docstring 作为字段描述
- `validate_by_name=True`:允许按名称验证 - `validate_by_name=True`:允许按名称验证
- **自动设置 table=True**:继承 TableBaseMixin 的类自动成为数据库表
- **联表继承支持**:自动检测并处理 Joined Table Inheritance
- **多态支持**:支持 `polymorphic_on`, `polymorphic_identity` 等参数
- **Python 3.14 兼容**:包含针对 PEP 649 的兼容性修复
### TableBaseMixin ### TableBaseMixin
@@ -61,13 +66,18 @@ models/
提供的 CRUD 方法: 提供的 CRUD 方法:
- `add()` - 新增记录(类方法) - `add()` - 新增记录(类方法)
- `save()` - 保存实例 - `save()` - 保存实例**必须使用返回值**
- `update()` - 更新记录 - `update()` - 更新记录**必须使用返回值**
- `delete()` - 删除记录 - `delete()` - 删除记录
- `get()` - 查询记录(类方法) - `get()` - 查询记录(类方法,支持分页、排序、时间筛选、多态加载
- `get_with_count()` - 分页查询(类方法 - `get_with_count()` - 分页查询并返回总数(类方法,返回 `ListResponse[T]`
- `get_exist_one()` - 获取存在的记录(不存在则抛出 404 - `get_exist_one()` - 获取存在的记录(不存在则抛出 404
- `count()` - 统计记录数(类方法) - `count()` - 统计记录数(类方法,支持时间筛选
分页排序请求类:
- `TimeFilterRequest` - 时间筛选参数
- `PaginationRequest` - 分页排序参数
- `TableViewRequest` - 组合分页排序和时间筛选
**使用方式** **使用方式**
```python ```python
@@ -104,6 +114,23 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
password: str password: str
``` ```
### ListResponse[T]
泛型分页响应类,用于所有 LIST 端点的标准化响应格式:
```python
class ListResponse(BaseModel, Generic[ItemT]):
count: int # 符合条件的记录总数
items: list[T] # 当前页的记录列表
```
**使用示例**
```python
@router.get("", response_model=ListResponse[UserResponse])
async def list_users(session: SessionDep, table_view: TableViewRequestDep):
return await User.get_with_count(session, table_view=table_view)
```
--- ---
## 数据库表模型 ## 数据库表模型
@@ -118,10 +145,10 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
| `id` | `UUID` | 用户 UUID主键 | | `id` | `UUID` | 用户 UUID主键 |
| `username` | `str` | 用户名,唯一,不可更改 | | `username` | `str` | 用户名,唯一,不可更改 |
| `nickname` | `str?` | 用户昵称 | | `nickname` | `str?` | 用户昵称 |
| `password` | `str` | 密码(加密 | | `password` | `str` | 密码(Argon2 加密) |
| `status` | `UserStatus` | 用户状态active/admin_banned/system_banned | | `status` | `UserStatus` | 用户状态active/admin_banned/system_banned |
| `storage` | `int` | 已用存储空间(字节) | | `storage` | `int` | 已用存储空间(字节) |
| `two_factor` | `str?` | 两步验证密钥 | | `two_factor` | `str?` | 两步验证密钥TOTP |
| `avatar` | `str` | 头像类型/地址 | | `avatar` | `str` | 头像类型/地址 |
| `score` | `int` | 用户积分 | | `score` | `int` | 用户积分 |
| `group_expires` | `datetime?` | 当前用户组过期时间 | | `group_expires` | `datetime?` | 当前用户组过期时间 |
@@ -134,6 +161,8 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
**关系**: **关系**:
- `group`: 所属用户组 - `group`: 所属用户组
- `previous_group`: 之前的用户组(用于过期后恢复) - `previous_group`: 之前的用户组(用于过期后恢复)
- `tags`: 用户的标签列表
- `authns`: 用户的 WebAuthn 凭证列表
--- ---
@@ -327,12 +356,17 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
- `children`: 子对象列表(自引用) - `children`: 子对象列表(自引用)
- `source_links`: 源链接列表 - `source_links`: 源链接列表
- `shares`: 分享列表 - `shares`: 分享列表
- `policy`: 存储策略
**业务属性**: **业务属性**:
- `source_name`: 向后兼容属性,返回物理文件的存储路径 - `source_name`: 向后兼容属性,返回物理文件的存储路径
- `is_file`: 是否为文件 - `is_file`: 是否为文件
- `is_folder`: 是否为目录 - `is_folder`: 是否为目录
**类方法**:
- `get_by_path()`: 根据路径获取对象
- `get_children()`: 获取子对象列表
--- ---
### 10. FileMetadata文件元数据 ### 10. FileMetadata文件元数据
@@ -1026,8 +1060,8 @@ class OrderStatus(StrEnum):
|-----|------| |-----|------|
| `LoginRequest` | 登录请求 | | `LoginRequest` | 登录请求 |
| `RegisterRequest` | 注册请求 | | `RegisterRequest` | 注册请求 |
| `TokenResponse` | 访问令牌响应 | | `TokenResponse` | 访问令牌响应access_token, refresh_token, expires_in |
| `UserResponse` | 用户信息响应 | | `UserResponse` | 用户信息响应(包含 group |
| `UserPublic` | 用户公开信息 | | `UserPublic` | 用户公开信息 |
| `UserSettingResponse` | 用户设置响应 | | `UserSettingResponse` | 用户设置响应 |
| `WebAuthnInfo` | WebAuthn 信息 | | `WebAuthnInfo` | WebAuthn 信息 |
@@ -1042,37 +1076,44 @@ class OrderStatus(StrEnum):
|-----|------| |-----|------|
| `GroupBase` | 用户组基础字段 | | `GroupBase` | 用户组基础字段 |
| `GroupOptionsBase` | 用户组选项基础字段 | | `GroupOptionsBase` | 用户组选项基础字段 |
| `GroupResponse` | 用户组响应 | | `GroupAllOptionsBase` | 用户组所有选项基础字段 |
| `GroupResponse` | 用户组响应(包含 options |
| `GroupCreateRequest` | 管理员创建用户组请求 |
| `GroupUpdateRequest` | 管理员更新用户组请求 |
| `GroupDetailResponse` | 管理员用户组详情响应 |
| `GroupListResponse` | 用户组列表响应 |
### 存储策略相关 ### 存储策略相关
| DTO | 说明 | | DTO | 说明 |
|-----|------| |-----|------|
| `PolicyBase` | 存储策略基础字段 |
| `PolicyOptionsBase` | 存储策略选项基础字段 | | `PolicyOptionsBase` | 存储策略选项基础字段 |
| `PolicyResponse` | 存储策略响应id, name, type, max_size |
| `PolicySummary` | 存储策略摘要 |
### 对象相关 ### 对象相关
| DTO | 说明 | | DTO | 说明 |
|-----|------| |-----|------|
| `ObjectBase` | 对象基础字段 | | `ObjectBase` | 对象基础字段 |
| `ObjectResponse` | 对象响应 | | `ObjectResponse` | 对象响应(目录列表中的单个项) |
| `DirectoryCreateRequest` | 创建目录请求 | | `DirectoryCreateRequest` | 创建目录请求parent_id, name, policy_id? |
| `DirectoryResponse` | 目录响应 | | `DirectoryResponse` | 目录响应id, parent, objects, policy |
| `ObjectMoveRequest` | 移动对象请求 | | `ObjectMoveRequest` | 移动对象请求src_ids, dst_id |
| `ObjectDeleteRequest` | 删除对象请求 | | `ObjectDeleteRequest` | 删除对象请求ids |
| `ObjectCopyRequest` | 复制对象请求 | | `ObjectCopyRequest` | 复制对象请求src_ids, dst_id |
| `ObjectRenameRequest` | 重命名对象请求 | | `ObjectRenameRequest` | 重命名对象请求id, new_name |
| `ObjectPropertyResponse` | 对象基本属性响应 | | `ObjectPropertyResponse` | 对象基本属性响应 |
| `ObjectPropertyDetailResponse` | 对象详细属性响应 | | `ObjectPropertyDetailResponse` | 对象详细属性响应(含元数据、分享统计) |
| `PolicyResponse` | 存储策略响应 |
### 上传相关 ### 上传相关
| DTO | 说明 | | DTO | 说明 |
|-----|------| |-----|------|
| `CreateUploadSessionRequest` | 创建上传会话请求 | | `CreateUploadSessionRequest` | 创建上传会话请求file_name, file_size, parent_id |
| `UploadSessionResponse` | 上传会话响应 | | `UploadSessionResponse` | 上传会话响应id, chunk_size, total_chunks |
| `UploadChunkResponse` | 上传分片响应 | | `UploadChunkResponse` | 上传分片响应uploaded_chunks, is_complete |
| `CreateFileRequest` | 创建空白文件请求 | | `CreateFileRequest` | 创建空白文件请求 |
### 管理员文件管理 ### 管理员文件管理
@@ -1083,15 +1124,52 @@ class OrderStatus(StrEnum):
| `FileBanRequest` | 文件封禁请求 | | `FileBanRequest` | 文件封禁请求 |
| `AdminFileListResponse` | 管理员文件列表响应 | | `AdminFileListResponse` | 管理员文件列表响应 |
### 管理员概况
| DTO | 说明 |
|-----|------|
| `MetricsSummary` | 统计摘要(日期列表、每日增量、总计) |
| `LicenseInfo` | 许可证信息 |
| `VersionInfo` | 版本信息 |
| `AdminSummaryData` | 管理员概况数据 |
| `AdminSummaryResponse` | 管理员概况响应 |
### 系统设置 ### 系统设置
| DTO | 说明 | | DTO | 说明 |
|-----|------| |-----|------|
| `SiteConfigResponse` | 站点配置响应 | | `SiteConfigResponse` | 站点配置响应 |
| `ThemeResponse` | 主题颜色响应 | | `ThemeResponse` | 主题颜色响应 |
| `SettingItem` | 设置项 | | `SettingItem` | 设置项type, name, value |
| `SettingsUpdateRequest` | 更新设置请求 | | `SettingsListResponse` | 设置列表响应 |
| `SettingsGetResponse` | 获取设置响应 | | `SettingsUpdateRequest` | 更新设置请求settings[] |
| `SettingsUpdateResponse` | 更新设置响应updated, created |
### 分享相关
| DTO | 说明 |
|-----|------|
| `ShareBase` | 分享基础字段 |
| `ShareCreateRequest` | 创建分享请求 |
| `ShareResponse` | 分享响应 |
| `AdminShareListItem` | 管理员分享列表项 |
### 任务相关
| DTO | 说明 |
|-----|------|
| `TaskPropsBase` | 任务属性基础字段 |
| `TaskSummary` | 任务摘要 |
### 通用响应
| DTO | 说明 |
|-----|------|
| `ResponseBase` | 通用响应基类code, msg, data |
| `ListResponse[T]` | 泛型分页响应count, items |
| `MCPBase` | MCP 基类 |
| `MCPRequestBase` | MCP 请求基类 |
| `MCPResponseBase` | MCP 响应基类 |
--- ---
@@ -1115,6 +1193,13 @@ objects = await Object.get(
(Object.owner_id == user_id) & (Object.type == ObjectType.FILE), (Object.owner_id == user_id) & (Object.type == ObjectType.FILE),
fetch_mode="all" fetch_mode="all"
) )
# 分页查询并返回总数
from models.mixin import TableViewRequest, ListResponse
table_view = TableViewRequest(offset=0, limit=20, desc=True, order="created_at")
result: ListResponse[User] = await User.get_with_count(session, table_view=table_view)
print(f"总数: {result.count}, 当前页: {len(result.items)}")
``` ```
### 创建文件对象 ### 创建文件对象
@@ -1126,15 +1211,64 @@ file = Object(
size=1024, size=1024,
owner_id=user.id, owner_id=user.id,
parent_id=folder.id, parent_id=folder.id,
policy_id=policy.id policy_id=policy.id,
physical_file_id=physical_file.id,
) )
file = await file.save(session) file = await file.save(session) # 必须使用返回值
``` ```
### 多对多关系操作 ### 多对多关系操作
```python ```python
# 为用户组添加存储策略 # 为用户组添加存储策略
group.policies.append(policy) from models import GroupPolicyLink
await group.save(session)
link = GroupPolicyLink(group_id=group.id, policy_id=policy.id)
session.add(link)
await session.commit()
```
### 文件上传流程
```python
# 1. 创建上传会话
upload_session = UploadSession(
file_name="large_file.zip",
file_size=104857600, # 100MB
chunk_size=52428800, # 50MB
total_chunks=2,
owner_id=user.id,
parent_id=folder.id,
policy_id=policy.id,
)
upload_session = await upload_session.save(session)
# 2. 上传分片后更新进度
upload_session.uploaded_chunks += 1
upload_session.uploaded_size += chunk_size
upload_session = await upload_session.save(session)
# 3. 检查是否完成
if upload_session.is_complete:
# 创建 PhysicalFile 和 Object 记录
...
```
### 文件引用计数(去重)
```python
# 复制文件时,只增加引用计数,不复制物理文件
if src.is_file and src.physical_file_id:
physical_file = await PhysicalFile.get(session, PhysicalFile.id == src.physical_file_id)
physical_file.increment_reference()
await physical_file.save(session)
# 删除文件时,减少引用计数
physical_file.decrement_reference()
if physical_file.can_be_deleted:
# 引用计数为0可以删除物理文件
await storage_service.delete_file(physical_file.storage_path)
await PhysicalFile.delete(session, physical_file)
else:
await physical_file.save(session)
``` ```

View File

@@ -25,7 +25,7 @@ default_settings: list[Setting] = [
Setting(name="siteURL", value="http://localhost", type=SettingsType.BASIC), Setting(name="siteURL", value="http://localhost", type=SettingsType.BASIC),
Setting(name="siteName", value="DiskNext", type=SettingsType.BASIC), Setting(name="siteName", value="DiskNext", type=SettingsType.BASIC),
Setting(name="register_enabled", value="1", type=SettingsType.REGISTER), Setting(name="register_enabled", value="1", type=SettingsType.REGISTER),
Setting(name="default_group", value="", type=SettingsType.REGISTER), # UUID 在组创建后更新 Setting(name="default_group", value="", type=SettingsType.REGISTER),
Setting(name="siteKeywords", value="网盘,网盘", type=SettingsType.BASIC), Setting(name="siteKeywords", value="网盘,网盘", type=SettingsType.BASIC),
Setting(name="siteDes", value="DiskNext", type=SettingsType.BASIC), Setting(name="siteDes", value="DiskNext", type=SettingsType.BASIC),
Setting(name="siteTitle", value="云星启智", type=SettingsType.BASIC), Setting(name="siteTitle", value="云星启智", type=SettingsType.BASIC),

View File

@@ -1,14 +1,19 @@
from datetime import datetime from datetime import datetime
from enum import StrEnum from enum import StrEnum
from typing import Literal, TYPE_CHECKING from typing import Literal, TYPE_CHECKING, TypeVar
from uuid import UUID from uuid import UUID
from sqlmodel import Field, Relationship
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import BinaryExpression, ClauseElement, and_
from sqlmodel import Field, Relationship
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlmodel.main import RelationshipInfo
from .base import SQLModelBase from .base import SQLModelBase
from .model_base import ResponseBase from .model_base import ResponseBase
from .mixin import UUIDTableBaseMixin from .mixin import UUIDTableBaseMixin, TableViewRequest, ListResponse
T = TypeVar("T", bound="User")
if TYPE_CHECKING: if TYPE_CHECKING:
from .group import Group from .group import Group
@@ -38,12 +43,33 @@ class ThemeType(StrEnum):
class UserStatus(StrEnum): class UserStatus(StrEnum):
"""用户状态枚举""" """用户状态枚举"""
ACTIVE = "active" ACTIVE = "active"
ADMIN_BANNED = "admin_banned" ADMIN_BANNED = "admin_banned"
SYSTEM_BANNED = "system_banned" SYSTEM_BANNED = "system_banned"
# ==================== 筛选参数 ====================
class UserFilterParams(SQLModelBase):
"""
用户过滤参数
用于管理员搜索用户列表,支持用户组、用户名、昵称、状态等过滤。
"""
group_id: UUID | None = None
"""按用户组UUID筛选"""
username_contains: str | None = Field(default=None, max_length=50)
"""用户名包含(不区分大小写的模糊搜索)"""
nickname_contains: str | None = Field(default=None, max_length=50)
"""昵称包含(不区分大小写的模糊搜索)"""
status: UserStatus | None = None
"""按用户状态筛选"""
# ==================== Base 模型 ==================== # ==================== Base 模型 ====================
class UserBase(SQLModelBase): class UserBase(SQLModelBase):
@@ -165,7 +191,7 @@ class UserPublic(UserBase):
id: UUID | None = None id: UUID | None = None
"""用户UUID""" """用户UUID"""
nick: str | None = None nickname: str | None = None
"""昵称""" """昵称"""
storage: int = 0 storage: int = 0
@@ -180,6 +206,9 @@ class UserPublic(UserBase):
group_id: UUID | None = None group_id: UUID | None = None
"""所属用户组UUID""" """所属用户组UUID"""
two_factor: str | None = None
"""两步验证密钥32位字符串null 表示未启用)"""
created_at: datetime | None = None created_at: datetime | None = None
"""创建时间""" """创建时间"""
@@ -235,6 +264,9 @@ class UserAdminUpdateRequest(SQLModelBase):
group_expires: datetime | None = None group_expires: datetime | None = None
"""用户组过期时间""" """用户组过期时间"""
two_factor: str | None = None
"""两步验证密钥32位字符串传 null 可清除,不传则不修改)"""
class UserCalibrateResponse(SQLModelBase): class UserCalibrateResponse(SQLModelBase):
"""用户存储校准响应 DTO""" """用户存储校准响应 DTO"""
@@ -395,4 +427,59 @@ class User(UserBase, UUIDTableBaseMixin):
def to_public(self) -> "UserPublic": def to_public(self) -> "UserPublic":
"""转换为公开 DTO排除敏感字段""" """转换为公开 DTO排除敏感字段"""
return UserPublic.model_validate(self) return UserPublic.model_validate(self)
@classmethod
async def get_with_count(
cls: type[T],
session: AsyncSession,
condition: BinaryExpression | ClauseElement | None = None,
*,
filter_params: 'UserFilterParams | None' = None,
join: type[T] | tuple[type[T], ClauseElement] | None = None,
options: list | None = None,
load: RelationshipInfo | None = None,
order_by: list[ClauseElement] | None = None,
filter: BinaryExpression | ClauseElement | None = None,
table_view: TableViewRequest | None = None,
) -> 'ListResponse[T]':
"""
获取分页用户列表及总数,支持用户过滤参数
:param filter_params: UserFilterParams 过滤参数对象(用户组、用户名、昵称、状态等)
:param 其他参数: 继承自 UUIDTableBaseMixin.get_with_count()
"""
# 构建过滤条件
merged_condition = condition
if filter_params is not None:
filter_conditions: list[BinaryExpression] = []
if filter_params.group_id is not None:
filter_conditions.append(cls.group_id == filter_params.group_id)
if filter_params.username_contains is not None:
filter_conditions.append(cls.username.ilike(f"%{filter_params.username_contains}%"))
if filter_params.nickname_contains is not None:
filter_conditions.append(cls.nickname.ilike(f"%{filter_params.nickname_contains}%"))
if filter_params.status is not None:
filter_conditions.append(cls.status == filter_params.status)
if filter_conditions:
combined_filter = and_(*filter_conditions)
if merged_condition is not None:
merged_condition = and_(merged_condition, combined_filter)
else:
merged_condition = combined_filter
return await super().get_with_count(
session,
merged_condition,
join=join,
options=options,
load=load,
order_by=order_by,
filter=filter,
table_view=table_view,
)

View File

@@ -1,6 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, status
from loguru import logger as l from loguru import logger as l
from sqlalchemy import and_ from sqlalchemy import and_
@@ -56,6 +56,23 @@ admin_aria2_router = APIRouter(
tags=['admin', 'admin_aria2'] tags=['admin', 'admin_aria2']
) )
@admin_router.get(
path='/',
summary='自己是否为管理员',
dependencies=[Depends(admin_required)],
status_code=status.HTTP_204_NO_CONTENT
)
async def is_admin() -> None:
"""
检查当前用户是否具有管理员权限。
如果用户是管理员,则返回 204 No Content 响应;否则返回 403 Forbidden 错误。
Returns:
None: 无内容响应
"""
return None
@admin_router.get( @admin_router.get(
path='/summary', path='/summary',
summary='获取站点概况', summary='获取站点概况',

View File

@@ -5,14 +5,14 @@ from loguru import logger as l
from sqlalchemy import func, and_ from sqlalchemy import func, and_
from middleware.auth import admin_required from middleware.auth import admin_required
from middleware.dependencies import SessionDep, TableViewRequestDep from middleware.dependencies import SessionDep, TableViewRequestDep, UserFilterParamsDep
from models import ( from models import (
User, ResponseBase, UserPublic, ListResponse, User, ResponseBase, UserPublic, ListResponse,
Group, Object, ObjectType, ) Group, Object, ObjectType, )
from models.user import ( from models.user import (
UserAdminUpdateRequest, UserCalibrateResponse, UserAdminUpdateRequest, UserCalibrateResponse,
) )
from utils import Password from utils import Password, http_exceptions
admin_user_router = APIRouter( admin_user_router = APIRouter(
prefix="/user", prefix="/user",
@@ -50,15 +50,17 @@ async def router_admin_get_user(session: SessionDep, user_id: int) -> ResponseBa
async def router_admin_get_users( async def router_admin_get_users(
session: SessionDep, session: SessionDep,
table_view: TableViewRequestDep, table_view: TableViewRequestDep,
filter_params: UserFilterParamsDep,
) -> ListResponse[UserPublic]: ) -> ListResponse[UserPublic]:
""" """
获取用户列表,支持分页、排序时间筛选。 获取用户列表,支持分页、排序时间筛选和用户筛选
:param session: 数据库会话依赖项 :param session: 数据库会话依赖项
:param table_view: 分页排序参数依赖 :param table_view: 分页排序参数依赖
:param filter_params: 用户筛选参数(用户组、用户名、昵称、状态)
:return: 分页用户列表 :return: 分页用户列表
""" """
result = await User.get_with_count(session, table_view=table_view) result = await User.get_with_count(session, filter_params=filter_params, table_view=table_view)
return ListResponse( return ListResponse(
items=[user.to_public() for user in result.items], items=[user.to_public() for user in result.items],
count=result.count, count=result.count,
@@ -114,6 +116,10 @@ async def router_admin_update_user(
if not user: if not user:
raise HTTPException(status_code=404, detail="用户不存在") raise HTTPException(status_code=404, detail="用户不存在")
# 默认管理员(用户名为 admin不允许更改用户组
if request.group_id and user.username == "admin" and request.group_id != user.group_id:
http_exceptions.raise_forbidden("默认管理员不允许更改用户组")
# 如果更新用户组,验证新组存在 # 如果更新用户组,验证新组存在
if request.group_id: if request.group_id:
group = await Group.get(session, Group.id == request.group_id) group = await Group.get(session, Group.id == request.group_id)
@@ -127,6 +133,11 @@ async def router_admin_update_user(
elif 'password' in update_data: elif 'password' in update_data:
del update_data['password'] # 空密码不更新 del update_data['password'] # 空密码不更新
# 验证两步验证密钥格式(如果提供了值且不为 None长度必须为 32
if 'two_factor' in update_data and update_data['two_factor'] is not None:
if len(update_data['two_factor']) != 32:
raise HTTPException(status_code=400, detail="两步验证密钥必须为32位字符串")
# 更新字段 # 更新字段
for key, value in update_data.items(): for key, value in update_data.items():
setattr(user, key, value) setattr(user, key, value)

View File

@@ -260,7 +260,7 @@ def router_user_avatar(id: str, size: int = 128) -> models.ResponseBase:
summary='获取用户信息', summary='获取用户信息',
description='Get user information.', description='Get user information.',
dependencies=[Depends(dependency=auth_required)], dependencies=[Depends(dependency=auth_required)],
response_model=models.ResponseBase, response_model=models.UserResponse,
) )
async def router_user_me( async def router_user_me(
session: SessionDep, session: SessionDep,
@@ -285,7 +285,7 @@ async def router_user_me(
# 异步加载 tags 关系 # 异步加载 tags 关系
user_tags = await user.awaitable_attrs.tags user_tags = await user.awaitable_attrs.tags
user_response = models.UserResponse( return models.UserResponse(
id=user.id, id=user.id,
username=user.username, username=user.username,
status=user.status, status=user.status,
@@ -297,8 +297,6 @@ async def router_user_me(
tags=[tag.name for tag in user_tags] if user_tags else [], tags=[tag.name for tag in user_tags] if user_tags else [],
) )
return models.ResponseBase(data=user_response.model_dump())
@user_router.get( @user_router.get(
path='/storage', path='/storage',
summary='存储信息', summary='存储信息',

View File

@@ -16,21 +16,21 @@ def raise_bad_request(*args, **kwargs) -> NoReturn:
"""Raises an HTTP 400 Bad Request exception.""" """Raises an HTTP 400 Bad Request exception."""
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, *args, **kwargs) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, *args, **kwargs)
def raise_unauthorized(*args, **kwargs) -> NoReturn: def raise_unauthorized(detail: str | None = None, *args, **kwargs) -> NoReturn:
"""Raises an HTTP 401 Unauthorized exception.""" """Raises an HTTP 401 Unauthorized exception."""
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, *args, **kwargs) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=detail, *args, **kwargs)
def raise_insufficient_quota(detail: str = "积分不足,请充值", *args, **kwargs) -> NoReturn: def raise_insufficient_quota(detail: str | None = None, *args, **kwargs) -> NoReturn:
"""Raises an HTTP 402 Payment Required exception.""" """Raises an HTTP 402 Payment Required exception."""
raise HTTPException(status_code=status.HTTP_402_PAYMENT_REQUIRED, detail=detail, *args, **kwargs) raise HTTPException(status_code=status.HTTP_402_PAYMENT_REQUIRED, detail=detail, *args, **kwargs)
def raise_forbidden(*args, **kwargs) -> NoReturn: def raise_forbidden(detail: str | None = None, *args, **kwargs) -> NoReturn:
"""Raises an HTTP 403 Forbidden exception.""" """Raises an HTTP 403 Forbidden exception."""
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, *args, **kwargs) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=detail, *args, **kwargs)
def raise_not_found(*args, **kwargs) -> NoReturn: def raise_not_found(detail: str | None = None, *args, **kwargs) -> NoReturn:
"""Raises an HTTP 404 Not Found exception.""" """Raises an HTTP 404 Not Found exception."""
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, *args, **kwargs) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail, *args, **kwargs)
def raise_conflict(*args, **kwargs) -> NoReturn: def raise_conflict(*args, **kwargs) -> NoReturn:
"""Raises an HTTP 409 Conflict exception.""" """Raises an HTTP 409 Conflict exception."""