更新查询方式

This commit is contained in:
2026-01-08 19:33:42 +08:00
parent f6f50532a6
commit 61ddc96f17
13 changed files with 341 additions and 275 deletions

View File

@@ -1,9 +1,72 @@
from typing import Annotated
"""
FastAPI 依赖注入
from fastapi import Depends
包含 HTTP 端点的通用依赖:
- SessionDep: 数据库会话依赖
- TimeFilterRequestDep: 时间筛选查询依赖(用于 count 等统计接口)
- TableViewRequestDep: 分页排序查询依赖(包含时间筛选 + 分页排序)
"""
from datetime import datetime
from typing import Annotated, Literal, TypeAlias
from fastapi import Depends, Query
from sqlmodel.ext.asyncio.session import AsyncSession
from models.database import get_session
from models.mixin import TimeFilterRequest, TableViewRequest
SessionDep = Annotated[AsyncSession, Depends(get_session)]
# --- 数据库会话依赖 ---
SessionDep: TypeAlias = Annotated[AsyncSession, Depends(get_session)]
"""数据库会话依赖,用于路由函数中获取数据库会话"""
# --- 时间筛选依赖 ---
async def _get_time_filter_queries(
created_after_datetime: Annotated[datetime | None, Query()] = None,
created_before_datetime: Annotated[datetime | None, Query()] = None,
updated_after_datetime: Annotated[datetime | None, Query()] = None,
updated_before_datetime: Annotated[datetime | None, Query()] = None,
) -> TimeFilterRequest:
"""解析时间筛选查询参数"""
return TimeFilterRequest(
created_after_datetime=created_after_datetime,
created_before_datetime=created_before_datetime,
updated_after_datetime=updated_after_datetime,
updated_before_datetime=updated_before_datetime,
)
TimeFilterRequestDep: TypeAlias = Annotated[TimeFilterRequest, Depends(_get_time_filter_queries)]
"""获取时间筛选参数的依赖(用于 count 等统计接口)"""
# --- 分页排序依赖 ---
async def _get_table_view_queries(
offset: Annotated[int | None, Query(ge=0)] = 0,
limit: Annotated[int | None, Query(ge=1, le=100)] = 20,
desc: bool | None = True,
order: Literal["created_at", "updated_at"] | None = "created_at",
created_after_datetime: Annotated[datetime | None, Query()] = None,
created_before_datetime: Annotated[datetime | None, Query()] = None,
updated_after_datetime: Annotated[datetime | None, Query()] = None,
updated_before_datetime: Annotated[datetime | None, Query()] = None,
) -> TableViewRequest:
"""解析分页排序和时间筛选查询参数"""
return TableViewRequest(
offset=offset,
limit=limit,
desc=desc,
order=order,
created_after_datetime=created_after_datetime,
created_before_datetime=created_before_datetime,
updated_after_datetime=updated_after_datetime,
updated_before_datetime=updated_before_datetime,
)
TableViewRequestDep: TypeAlias = Annotated[TableViewRequest, Depends(_get_table_view_queries)]
"""获取分页排序和时间筛选参数的依赖"""

View File

@@ -67,7 +67,7 @@ from .object import (
)
from .physical_file import PhysicalFile, PhysicalFileBase
from .order import Order, OrderStatus, OrderType
from .policy import Policy, PolicyBase, PolicyOptions, PolicyOptionsBase, PolicyType
from .policy import Policy, PolicyBase, PolicyOptions, PolicyOptionsBase, PolicyType, PolicySummary
from .redeem import Redeem, RedeemType
from .report import Report, ReportReason
from .setting import (
@@ -75,11 +75,11 @@ from .setting import (
# 管理员DTO
SettingItem, SettingsListResponse, SettingsUpdateRequest, SettingsUpdateResponse,
)
from .share import Share, ShareBase, ShareCreateRequest, ShareResponse
from .share import Share, ShareBase, ShareCreateRequest, ShareResponse, AdminShareListItem
from .source_link import SourceLink
from .storage_pack import StoragePack
from .tag import Tag, TagType
from .task import Task, TaskProps, TaskPropsBase, TaskStatus, TaskType
from .task import Task, TaskProps, TaskPropsBase, TaskStatus, TaskType, TaskSummary
from .webdav import WebDAV
from .database import engine, get_session
@@ -97,3 +97,6 @@ from .model_base import (
AdminSummaryData,
AdminSummaryResponse,
)
# mixin 中的通用分页模型
from .mixin import ListResponse

View File

@@ -124,8 +124,8 @@ class GroupUpdateRequest(SQLModelBase):
"""关联的存储策略UUID列表"""
class GroupDetailResponse(GroupAllOptionsBase):
"""用户组详情响应 DTO"""
class GroupCoreBase(SQLModelBase):
"""用户组核心字段(从 Group 模型提取)"""
id: UUID
"""用户组UUID"""
@@ -148,12 +148,35 @@ class GroupDetailResponse(GroupAllOptionsBase):
speed_limit: int = 0
"""速度限制 (KB/s)"""
class GroupDetailResponse(GroupCoreBase, GroupAllOptionsBase):
"""用户组详情响应 DTO"""
user_count: int = 0
"""用户数量"""
policy_ids: list[UUID] = []
"""关联的存储策略UUID列表"""
@classmethod
def from_group(
cls,
group: "Group",
user_count: int,
policies: list["Policy"],
) -> "GroupDetailResponse":
"""从 Group ORM 对象构建"""
opts = group.options
return cls(
# GroupCoreBase 字段(从 Group 模型提取)
**GroupCoreBase.model_validate(group, from_attributes=True).model_dump(),
# GroupAllOptionsBase 字段(从 GroupOptions 提取)
**(GroupAllOptionsBase.model_validate(opts, from_attributes=True).model_dump() if opts else {}),
# 计算字段
user_count=user_count,
policy_ids=[p.id for p in policies],
)
class GroupListResponse(SQLModelBase):
"""用户组列表响应 DTO"""

View File

@@ -695,6 +695,32 @@ class AdminFileResponse(ObjectResponse):
ban_reason: str | None = None
"""封禁原因"""
@classmethod
def from_object(
cls,
obj: "Object",
owner: "User | None",
policy: "Policy | None",
) -> "AdminFileResponse":
"""从 Object ORM 对象构建"""
return cls(
# ObjectBase 字段
**ObjectBase.model_validate(obj, from_attributes=True).model_dump(),
# ObjectResponse 字段
id=obj.id,
thumb=False,
date=obj.updated_at,
create_date=obj.created_at,
source_enabled=False,
# AdminFileResponse 字段
owner_id=obj.owner_id,
owner_username=owner.username if owner else "unknown",
policy_name=policy.name if policy else "unknown",
is_banned=obj.is_banned,
banned_at=obj.banned_at,
ban_reason=obj.ban_reason,
)
class FileBanRequest(SQLModelBase):
"""文件封禁请求 DTO"""

View File

@@ -78,6 +78,34 @@ class PolicyBase(SQLModelBase):
"""是否开启源链接访问"""
# ==================== DTO 模型 ====================
class PolicySummary(SQLModelBase):
"""策略摘要,用于列表展示"""
id: UUID
"""策略UUID"""
name: str
"""策略名称"""
type: PolicyType
"""策略类型"""
server: str | None
"""服务器地址"""
max_size: int
"""最大文件尺寸"""
is_private: bool
"""是否私有"""
# ==================== 数据库模型 ====================
class PolicyOptionsBase(SQLModelBase):
"""存储策略选项的基础模型"""

View File

@@ -160,3 +160,61 @@ class ShareResponse(SQLModelBase):
has_password: bool
"""是否有密码"""
class ShareListItemBase(SQLModelBase):
"""分享列表项基础字段"""
id: int
"""分享ID"""
code: str
"""分享码"""
views: int
"""浏览次数"""
downloads: int
"""下载次数"""
remain_downloads: int | None
"""剩余下载次数"""
expires: datetime | None
"""过期时间"""
preview_enabled: bool
"""是否允许预览"""
score: int
"""积分"""
user_id: UUID
"""用户UUID"""
created_at: datetime
"""创建时间"""
class AdminShareListItem(ShareListItemBase):
"""管理员分享列表项 DTO添加关联字段"""
username: str | None
"""用户名"""
object_name: str | None
"""对象名称"""
@classmethod
def from_share(
cls,
share: "Share",
user: "User | None",
obj: "Object | None",
) -> "AdminShareListItem":
"""从 Share ORM 对象构建"""
return cls(
**ShareListItemBase.model_validate(share, from_attributes=True).model_dump(),
username=user.username if user else None,
object_name=obj.name if obj else None,
)

View File

@@ -9,8 +9,8 @@ from .base import SQLModelBase
from .mixin import TableBaseMixin
if TYPE_CHECKING:
from .user import User
from .download import Download
from .user import User
class TaskStatus(StrEnum):
@@ -31,6 +31,55 @@ class TaskType(StrEnum):
pass
# ==================== DTO 模型 ====================
class TaskSummaryBase(SQLModelBase):
"""任务摘要基础字段"""
id: int
"""任务ID"""
type: int
"""任务类型"""
status: TaskStatus
"""任务状态"""
progress: int
"""进度0-100"""
error: str | None
"""错误信息"""
user_id: UUID
"""用户UUID"""
created_at: datetime
"""创建时间"""
updated_at: datetime
"""更新时间"""
class TaskSummary(TaskSummaryBase):
"""任务摘要,用于管理员列表展示"""
username: str | None
"""用户名"""
@classmethod
def from_task(cls, task: "Task", user: "User | None") -> "TaskSummary":
"""从 Task ORM 对象构建"""
return cls(
**TaskSummaryBase.model_validate(task, from_attributes=True).model_dump(),
username=user.username if user else None,
)
# ==================== 数据库模型 ====================
class TaskPropsBase(SQLModelBase):
"""任务属性基础模型"""

View File

@@ -8,11 +8,10 @@ from loguru import logger as l
from sqlalchemy import and_
from middleware.auth import admin_required
from middleware.dependencies import SessionDep
from middleware.dependencies import SessionDep, TableViewRequestDep
from models import (
Policy, PolicyType, User, ResponseBase,
Object, ObjectType, )
from models.object import AdminFileResponse, FileBanRequest
Policy, PolicyType, User, ResponseBase, ListResponse,
Object, ObjectType, AdminFileResponse, FileBanRequest, )
from service.storage import LocalStorageService
admin_file_router = APIRouter(
@@ -28,25 +27,21 @@ admin_file_router = APIRouter(
)
async def router_admin_get_file_list(
session: SessionDep,
table_view: TableViewRequestDep,
user_id: UUID | None = None,
is_banned: bool | None = None,
keyword: str | None = None,
page: int = 1,
page_size: int = 20,
) -> ResponseBase:
) -> ListResponse[AdminFileResponse]:
"""
获取系统中的文件列表,支持筛选。
:param session: 数据库会话
:param table_view: 分页排序参数依赖
:param user_id: 按用户筛选
:param is_banned: 按封禁状态筛选
:param keyword: 按文件名搜索
:param page: 页码
:param page_size: 每页数量
:return: 文件列表
:return: 分页文件列表
"""
offset = (page - 1) * page_size
# 构建查询条件
conditions = [Object.type == ObjectType.FILE]
if user_id:
@@ -56,42 +51,17 @@ async def router_admin_get_file_list(
if keyword:
conditions.append(Object.name.ilike(f"%{keyword}%"))
combined_condition = and_(*conditions) if len(conditions) > 1 else conditions[0]
files = await Object.get(
session,
combined_condition,
fetch_mode="all",
offset=offset,
limit=page_size,
load=Object.owner,
)
total = await Object.count(session, combined_condition)
condition = and_(*conditions) if len(conditions) > 1 else conditions[0]
result = await Object.get_with_count(session, condition, table_view=table_view, load=Object.owner)
# 构建响应
file_list = []
for f in files:
items: list[AdminFileResponse] = []
for f in result.items:
owner = await f.awaitable_attrs.owner
policy = await f.awaitable_attrs.policy
file_list.append(AdminFileResponse(
id=f.id,
name=f.name,
type=f.type,
size=f.size,
thumb=False,
date=f.updated_at,
create_date=f.created_at,
source_enabled=False,
owner_id=f.owner_id,
owner_username=owner.username if owner else "unknown",
policy_name=policy.name if policy else "unknown",
is_banned=f.is_banned,
banned_at=f.banned_at,
ban_reason=f.ban_reason,
).model_dump())
items.append(AdminFileResponse.from_object(f, owner, policy))
return ResponseBase(data={"files": file_list, "total": total})
return ListResponse(items=items, count=result.count)
@admin_file_router.get(

View File

@@ -4,9 +4,9 @@ from fastapi import APIRouter, Depends, HTTPException
from loguru import logger as l
from middleware.auth import admin_required
from middleware.dependencies import SessionDep
from middleware.dependencies import SessionDep, TableViewRequestDep
from models import (
User, ResponseBase,
User, ResponseBase, UserPublic, ListResponse,
Group, GroupOptions, )
from models.group import (
GroupCreateRequest, GroupUpdateRequest, GroupDetailResponse, )
@@ -25,61 +25,25 @@ admin_group_router = APIRouter(
)
async def router_admin_get_groups(
session: SessionDep,
page: int = 1,
page_size: int = 20,
) -> ResponseBase:
table_view: TableViewRequestDep,
) -> ListResponse[GroupDetailResponse]:
"""
获取用户组列表,支持分页。
获取用户组列表,支持分页、排序和时间筛选
:param session: 数据库会话
:param page: 页码
:param page_size: 每页数量
:return: 用户组列表
:param table_view: 分页排序参数依赖
:return: 分页用户组列表
"""
offset = (page - 1) * page_size
groups = await Group.get(
session,
None,
fetch_mode="all",
offset=offset,
limit=page_size,
load=Group.options,
)
total = await Group.count(session, None)
result = await Group.get_with_count(session, table_view=table_view, load=Group.options)
# 构建响应
group_list = []
for g in groups:
opts = g.options
items: list[GroupDetailResponse] = []
for g in result.items:
policies = await g.awaitable_attrs.policies
user_count = await User.count(session, User.group_id == g.id)
items.append(GroupDetailResponse.from_group(g, user_count, policies))
group_list.append(GroupDetailResponse(
id=g.id,
name=g.name,
max_storage=g.max_storage,
share_enabled=g.share_enabled,
web_dav_enabled=g.web_dav_enabled,
admin=g.admin,
speed_limit=g.speed_limit,
user_count=user_count,
policy_ids=[p.id for p in policies],
share_download=opts.share_download if opts else False,
share_free=opts.share_free if opts else False,
relocate=opts.relocate if opts else False,
source_batch=opts.source_batch if opts else 0,
select_node=opts.select_node if opts else False,
advance_delete=opts.advance_delete if opts else False,
archive_download=opts.archive_download if opts else False,
archive_task=opts.archive_task if opts else False,
webdav_proxy=opts.webdav_proxy if opts else False,
aria2=opts.aria2 if opts else False,
redirected_source=opts.redirected_source if opts else False,
).model_dump())
return ResponseBase(data={"groups": group_list, "total": total})
return ListResponse(items=items, count=result.count)
@admin_group_router.get(
@@ -104,32 +68,9 @@ async def router_admin_get_group(
if not group:
raise HTTPException(status_code=404, detail="用户组不存在")
opts = group.options
policies = await group.awaitable_attrs.policies
user_count = await User.count(session, User.group_id == group_id)
response = GroupDetailResponse(
id=group.id,
name=group.name,
max_storage=group.max_storage,
share_enabled=group.share_enabled,
web_dav_enabled=group.web_dav_enabled,
admin=group.admin,
speed_limit=group.speed_limit,
user_count=user_count,
policy_ids=[p.id for p in policies],
share_download=opts.share_download if opts else False,
share_free=opts.share_free if opts else False,
relocate=opts.relocate if opts else False,
source_batch=opts.source_batch if opts else 0,
select_node=opts.select_node if opts else False,
advance_delete=opts.advance_delete if opts else False,
archive_download=opts.archive_download if opts else False,
archive_task=opts.archive_task if opts else False,
webdav_proxy=opts.webdav_proxy if opts else False,
aria2=opts.aria2 if opts else False,
redirected_source=opts.redirected_source if opts else False,
)
response = GroupDetailResponse.from_group(group, user_count, policies)
return ResponseBase(data=response.model_dump())
@@ -143,40 +84,28 @@ async def router_admin_get_group(
async def router_admin_get_group_members(
session: SessionDep,
group_id: UUID,
page: int = 1,
page_size: int = 20,
) -> ResponseBase:
table_view: TableViewRequestDep,
) -> ListResponse[UserPublic]:
"""
根据用户组ID获取用户组成员列表。
:param session: 数据库会话
:param group_id: 用户组UUID
:param page: 页码
:param page_size: 每页数量
:return: 成员列表
:param table_view: 分页排序参数依赖
:return: 分页成员列表
"""
# 验证组存在
group = await Group.get(session, Group.id == group_id)
if not group:
raise HTTPException(status_code=404, detail="用户组不存在")
offset = (page - 1) * page_size
result = await User.get_with_count(session, User.group_id == group_id, table_view=table_view)
users = await User.get(
session,
User.group_id == group_id,
fetch_mode="all",
offset=offset,
limit=page_size,
return ListResponse(
items=[u.to_public() for u in result.items],
count=result.count,
)
total = await User.count(session, User.group_id == group_id)
return ResponseBase(data={
"members": [u.to_public().model_dump() for u in users],
"total": total,
})
@admin_group_router.post(
path='/',

View File

@@ -5,10 +5,10 @@ from loguru import logger as l
from sqlmodel import Field
from middleware.auth import admin_required
from middleware.dependencies import SessionDep
from middleware.dependencies import SessionDep, TableViewRequestDep
from models import (
Policy, PolicyBase, PolicyType, ResponseBase,
Object, )
Policy, PolicyBase, PolicyType, PolicySummary, ResponseBase,
ListResponse, Object, )
from models.base import SQLModelBase
from service.storage import DirectoryCreationError, LocalStorageService
@@ -45,44 +45,22 @@ class PolicyCreateRequest(PolicyBase):
)
async def router_policy_list(
session: SessionDep,
page: int = 1,
page_size: int = 20,
) -> ResponseBase:
table_view: TableViewRequestDep,
) -> ListResponse[PolicySummary]:
"""
获取所有存储策略列表。
:param session: 数据库会话
:param page: 页码
:param page_size: 每页数量
:return: 策略列表
:param table_view: 分页排序参数依赖
:return: 分页策略列表
"""
offset = (page - 1) * page_size
result = await Policy.get_with_count(session, table_view=table_view)
policies = await Policy.get(
session,
None,
fetch_mode="all",
offset=offset,
limit=page_size,
return ListResponse(
items=[PolicySummary.model_validate(p, from_attributes=True) for p in result.items],
count=result.count,
)
total = await Policy.count(session, None)
return ResponseBase(data={
"policies": [
{
"id": str(p.id),
"name": p.name,
"type": p.type.value,
"server": p.server,
"max_size": p.max_size,
"is_private": p.is_private,
}
for p in policies
],
"total": total,
})
@admin_policy_router.post(
path='/test/path',

View File

@@ -4,10 +4,10 @@ from fastapi import APIRouter, Depends, HTTPException
from loguru import logger as l
from middleware.auth import admin_required
from middleware.dependencies import SessionDep
from middleware.dependencies import SessionDep, TableViewRequestDep
from models import (
ResponseBase,
Share, )
ResponseBase, ListResponse,
Share, AdminShareListItem, )
admin_share_router = APIRouter(
prefix='/share',
@@ -22,53 +22,27 @@ admin_share_router = APIRouter(
)
async def router_admin_get_share_list(
session: SessionDep,
table_view: TableViewRequestDep,
user_id: UUID | None = None,
page: int = 1,
page_size: int = 20,
) -> ResponseBase:
) -> ListResponse[AdminShareListItem]:
"""
获取分享列表。
:param session: 数据库会话
:param table_view: 分页排序参数依赖
:param user_id: 按用户筛选
:param page: 页码
:param page_size: 每页数量
:return: 分享列表
:return: 分页分享列表
"""
offset = (page - 1) * page_size
condition = Share.user_id == user_id if user_id else None
result = await Share.get_with_count(session, condition, table_view=table_view, load=Share.user)
shares = await Share.get(
session,
condition,
fetch_mode="all",
offset=offset,
limit=page_size,
load=Share.user,
)
total = await Share.count(session, condition)
share_list = []
for s in shares:
items: list[AdminShareListItem] = []
for s in result.items:
user = await s.awaitable_attrs.user
obj = await s.awaitable_attrs.object
share_list.append({
"id": s.id,
"code": s.code,
"views": s.views,
"downloads": s.downloads,
"remain_downloads": s.remain_downloads,
"expires": s.expires.isoformat() if s.expires else None,
"preview_enabled": s.preview_enabled,
"score": s.score,
"user_id": str(s.user_id),
"username": user.username if user else None,
"object_name": obj.name if obj else None,
"created_at": s.created_at.isoformat(),
})
items.append(AdminShareListItem.from_share(s, user, obj))
return ResponseBase(data={"shares": share_list, "total": total})
return ListResponse(items=items, count=result.count)
@admin_share_router.get(

View File

@@ -5,10 +5,10 @@ from loguru import logger as l
from sqlalchemy import and_
from middleware.auth import admin_required
from middleware.dependencies import SessionDep
from middleware.dependencies import SessionDep, TableViewRequestDep
from models import (
ResponseBase,
Task,
ResponseBase, ListResponse,
Task, TaskSummary,
)
admin_task_router = APIRouter(
@@ -24,23 +24,19 @@ admin_task_router = APIRouter(
)
async def router_admin_get_task_list(
session: SessionDep,
table_view: TableViewRequestDep,
user_id: UUID | None = None,
status: str | None = None,
page: int = 1,
page_size: int = 20,
) -> ResponseBase:
) -> ListResponse[TaskSummary]:
"""
获取任务列表。
:param session: 数据库会话
:param table_view: 分页排序参数依赖
:param user_id: 按用户筛选
:param status: 按状态筛选
:param page: 页码
:param page_size: 每页数量
:return: 任务列表
:return: 分页任务列表
"""
offset = (page - 1) * page_size
conditions = []
if user_id:
conditions.append(Task.user_id == user_id)
@@ -48,34 +44,14 @@ async def router_admin_get_task_list(
conditions.append(Task.status == status)
condition = and_(*conditions) if conditions else None
result = await Task.get_with_count(session, condition, table_view=table_view, load=Task.user)
tasks = await Task.get(
session,
condition,
fetch_mode="all",
offset=offset,
limit=page_size,
load=Task.user,
)
total = await Task.count(session, condition)
task_list = []
for t in tasks:
items: list[TaskSummary] = []
for t in result.items:
user = await t.awaitable_attrs.user
task_list.append({
"id": t.id,
"status": t.status,
"type": t.type,
"progress": t.progress,
"error": t.error,
"user_id": str(t.user_id),
"username": user.username if user else None,
"created_at": t.created_at.isoformat(),
"updated_at": t.updated_at.isoformat(),
})
items.append(TaskSummary.from_task(t, user))
return ResponseBase(data={"tasks": task_list, "total": total})
return ListResponse(items=items, count=result.count)
@admin_task_router.get(

View File

@@ -5,9 +5,9 @@ from loguru import logger as l
from sqlalchemy import func, and_
from middleware.auth import admin_required
from middleware.dependencies import SessionDep
from middleware.dependencies import SessionDep, TableViewRequestDep
from models import (
User, ResponseBase,
User, ResponseBase, UserPublic, ListResponse,
Group, Object, ObjectType, )
from models.user import (
UserAdminUpdateRequest, UserCalibrateResponse,
@@ -49,30 +49,19 @@ async def router_admin_get_user(session: SessionDep, user_id: int) -> ResponseBa
)
async def router_admin_get_users(
session: SessionDep,
page: int = 1,
page_size: int = 20
) -> ResponseBase:
table_view: TableViewRequestDep,
) -> ListResponse[UserPublic]:
"""
获取用户列表,支持分页。
获取用户列表,支持分页、排序和时间筛选
Args:
session: 数据库会话依赖项。
page (int): 页码默认为1。
page_size (int): 每页显示的用户数量默认为20。
Returns:
ResponseBase: 包含用户列表的响应模型。
:param session: 数据库会话依赖项
:param table_view: 分页排序参数依赖
:return: 分页用户列表
"""
offset = (page - 1) * page_size
users: list[User] = await User.get(
session,
None,
fetch_mode="all",
offset=offset,
limit=page_size
)
return ResponseBase(
data=[user.to_public().model_dump() for user in users]
result = await User.get_with_count(session, table_view=table_view)
return ListResponse(
items=[user.to_public() for user in result.items],
count=result.count,
)