306 lines
11 KiB
Python
306 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from typing import Iterable
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import and_, desc, func, or_, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.enums.upload_resource import (
|
|
UPLOAD_RESOURCE_MODULE_LABELS,
|
|
UPLOAD_RESOURCE_TYPE_LABELS,
|
|
UploadResourceBindStatusEnum,
|
|
UploadResourceDeletePolicyEnum,
|
|
UploadResourceEventEnum,
|
|
UploadResourceTypeEnum,
|
|
)
|
|
from app.models.upload_resource import UploadResource
|
|
from app.schemas.upload_resource import (
|
|
UPLOAD_RESOURCE_HISTORY_ALLOWED_RESOURCE_TYPES,
|
|
UploadResourceHistoryDayGroupOut,
|
|
UploadResourceHistoryDayItemsOut,
|
|
UploadResourceHistoryGroupedOut,
|
|
UploadResourceHistoryItemOut,
|
|
UploadResourceMediaReferenceOut,
|
|
)
|
|
from app.services.upload_resource.log_service import log_upload_resource_event
|
|
|
|
HISTORY_SOURCE = "upload_resource"
|
|
HISTORY_SOURCE_LABEL = "历史上传素材"
|
|
DEFAULT_GROUP_ITEMS_LIMIT = 10
|
|
MAX_GROUP_PAGE_SIZE = 10
|
|
MAX_DAY_PAGE_SIZE = 100
|
|
|
|
|
|
def _normalize_resource_type(resource_type: str | None) -> str | None:
|
|
if resource_type is None or str(resource_type).strip() == "":
|
|
return None
|
|
value = str(resource_type).strip()
|
|
if value not in UPLOAD_RESOURCE_HISTORY_ALLOWED_RESOURCE_TYPES:
|
|
raise HTTPException(status_code=400, detail="resource_type 仅支持 image、video、audio")
|
|
return value
|
|
|
|
|
|
def _page(value: int, *, default: int = 1) -> int:
|
|
return max(int(value or default), 1)
|
|
|
|
|
|
def _page_size(value: int, *, default: int, max_value: int) -> int:
|
|
return min(max(int(value or default), 1), max_value)
|
|
|
|
|
|
def _date_text(value: object) -> str:
|
|
if isinstance(value, date):
|
|
return value.isoformat()
|
|
return str(value)[:10]
|
|
|
|
|
|
def _display_url(resource: UploadResource) -> str:
|
|
return str(resource.resource_url or "")
|
|
|
|
|
|
def _resource_type(resource: UploadResource) -> str:
|
|
return str(resource.resource_type or "")
|
|
|
|
|
|
def _item_to_out(resource: UploadResource) -> UploadResourceHistoryItemOut:
|
|
resource_type = _resource_type(resource)
|
|
display_url = _display_url(resource)
|
|
file_name = resource.file_name or display_url.rsplit("/", 1)[-1] or resource.id
|
|
duration = float(resource.duration_seconds) if resource.duration_seconds is not None else None
|
|
|
|
media_reference = UploadResourceMediaReferenceOut(
|
|
name=file_name,
|
|
type=resource_type, # type: ignore[arg-type]
|
|
url=display_url,
|
|
label="",
|
|
duration=duration if resource_type in {UploadResourceTypeEnum.VIDEO.value, UploadResourceTypeEnum.AUDIO.value} else None,
|
|
source=HISTORY_SOURCE,
|
|
upload_resource_id=resource.id,
|
|
)
|
|
|
|
return UploadResourceHistoryItemOut(
|
|
id=resource.id,
|
|
source_type=HISTORY_SOURCE,
|
|
history_source=HISTORY_SOURCE,
|
|
history_source_label=HISTORY_SOURCE_LABEL,
|
|
module=resource.module,
|
|
module_label=UPLOAD_RESOURCE_MODULE_LABELS.get(resource.module, resource.module),
|
|
resource_type=resource_type, # type: ignore[arg-type]
|
|
resource_type_label=UPLOAD_RESOURCE_TYPE_LABELS.get(resource_type, resource_type),
|
|
resource_url=display_url,
|
|
display_url=display_url,
|
|
preview_url=display_url,
|
|
image_url=display_url if resource_type == UploadResourceTypeEnum.IMAGE.value else None,
|
|
video_url=display_url if resource_type == UploadResourceTypeEnum.VIDEO.value else None,
|
|
audio_url=display_url if resource_type == UploadResourceTypeEnum.AUDIO.value else None,
|
|
file_name=resource.file_name,
|
|
file_ext=resource.file_ext,
|
|
mime_type=resource.mime_type,
|
|
file_size_bytes=int(resource.file_size_bytes or 0),
|
|
duration_seconds=duration,
|
|
width=int(resource.width) if resource.width is not None else None,
|
|
height=int(resource.height) if resource.height is not None else None,
|
|
bind_status=resource.bind_status,
|
|
delete_policy=resource.delete_policy,
|
|
deletable=True,
|
|
media_reference=media_reference,
|
|
created_at=resource.created_at,
|
|
updated_at=resource.updated_at,
|
|
)
|
|
|
|
|
|
def _base_filters(*, user_id: str, resource_type: str | None = None, keyword: str | None = None) -> list:
|
|
filters = [
|
|
UploadResource.user_id == user_id,
|
|
UploadResource.deleted_at.is_(None),
|
|
UploadResource.source_model.is_(None),
|
|
UploadResource.source_id.is_(None),
|
|
UploadResource.bind_status == UploadResourceBindStatusEnum.PENDING.value,
|
|
UploadResource.delete_policy == UploadResourceDeletePolicyEnum.USER_DELETABLE.value,
|
|
UploadResource.resource_type.in_(list(UPLOAD_RESOURCE_HISTORY_ALLOWED_RESOURCE_TYPES)),
|
|
]
|
|
if resource_type:
|
|
filters.append(UploadResource.resource_type == resource_type)
|
|
if keyword and keyword.strip():
|
|
pattern = f"%{keyword.strip()}%"
|
|
filters.append(
|
|
or_(
|
|
UploadResource.file_name.ilike(pattern),
|
|
UploadResource.resource_url.ilike(pattern),
|
|
)
|
|
)
|
|
return filters
|
|
|
|
|
|
async def list_upload_resource_history_grouped_days(
|
|
db: AsyncSession,
|
|
*,
|
|
user_id: str,
|
|
resource_type: str | None = None,
|
|
page: int = 1,
|
|
page_size: int = 10,
|
|
keyword: str | None = None,
|
|
) -> UploadResourceHistoryGroupedOut:
|
|
"""按上传日期分组查询可展示/可复用/可删除的 UploadResource。"""
|
|
|
|
resource_type = _normalize_resource_type(resource_type)
|
|
page = _page(page)
|
|
page_size = _page_size(page_size, default=10, max_value=MAX_GROUP_PAGE_SIZE)
|
|
filters = _base_filters(user_id=user_id, resource_type=resource_type, keyword=keyword)
|
|
date_expr = func.date(UploadResource.created_at).label("generated_date")
|
|
|
|
log_upload_resource_event(
|
|
event_type=UploadResourceEventEnum.HISTORY_LIST_START.value,
|
|
user_id=user_id,
|
|
detail={"resource_type": resource_type, "page": page, "page_size": page_size, "keyword": keyword},
|
|
)
|
|
|
|
total_days_stmt = select(func.count()).select_from(
|
|
select(date_expr).where(and_(*filters)).group_by(date_expr).subquery()
|
|
)
|
|
total_days = int((await db.execute(total_days_stmt)).scalar_one() or 0)
|
|
|
|
group_stmt = (
|
|
select(date_expr, func.count(UploadResource.id).label("total"))
|
|
.where(and_(*filters))
|
|
.group_by(date_expr)
|
|
.order_by(desc(date_expr))
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
)
|
|
group_rows = (await db.execute(group_stmt)).mappings().all()
|
|
date_values = [row["generated_date"] for row in group_rows]
|
|
date_texts = [_date_text(value) for value in date_values]
|
|
|
|
items_by_date: dict[str, list[UploadResourceHistoryItemOut]] = {date_text: [] for date_text in date_texts}
|
|
if date_texts:
|
|
rn = func.row_number().over(
|
|
partition_by=func.date(UploadResource.created_at),
|
|
order_by=(UploadResource.created_at.desc(), UploadResource.id.desc()),
|
|
).label("rn")
|
|
id_subq = (
|
|
select(
|
|
UploadResource.id.label("id"),
|
|
func.date(UploadResource.created_at).label("generated_date"),
|
|
rn,
|
|
)
|
|
.where(and_(*filters), func.date(UploadResource.created_at).in_(date_values))
|
|
.subquery()
|
|
)
|
|
item_stmt = (
|
|
select(UploadResource)
|
|
.join(id_subq, UploadResource.id == id_subq.c.id)
|
|
.where(id_subq.c.rn <= DEFAULT_GROUP_ITEMS_LIMIT)
|
|
.order_by(id_subq.c.generated_date.desc(), UploadResource.created_at.desc(), UploadResource.id.desc())
|
|
)
|
|
resources = list((await db.execute(item_stmt)).scalars().all())
|
|
for resource in resources:
|
|
items_by_date.setdefault(_date_text(resource.created_at.date()), []).append(_item_to_out(resource))
|
|
|
|
groups = [
|
|
UploadResourceHistoryDayGroupOut(
|
|
generated_date=_date_text(row["generated_date"]),
|
|
total=int(row["total"] or 0),
|
|
page=1,
|
|
items=items_by_date.get(_date_text(row["generated_date"]), []),
|
|
)
|
|
for row in group_rows
|
|
]
|
|
|
|
log_upload_resource_event(
|
|
event_type=UploadResourceEventEnum.HISTORY_LIST_SUCCESS.value,
|
|
user_id=user_id,
|
|
detail={"resource_type": resource_type, "page": page, "page_size": page_size, "groups": len(groups)},
|
|
)
|
|
return UploadResourceHistoryGroupedOut(total_days=total_days, page=page, page_size=page_size, groups=groups)
|
|
|
|
|
|
async def list_upload_resource_history_day_items(
|
|
db: AsyncSession,
|
|
*,
|
|
user_id: str,
|
|
generated_date: str,
|
|
resource_type: str | None = None,
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
keyword: str | None = None,
|
|
) -> UploadResourceHistoryDayItemsOut:
|
|
"""查询指定上传日期下的 UploadResource 历史素材。"""
|
|
|
|
try:
|
|
target_date = date.fromisoformat(str(generated_date))
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="generated_date 必须是 YYYY-MM-DD 格式")
|
|
|
|
resource_type = _normalize_resource_type(resource_type)
|
|
page = _page(page)
|
|
page_size = _page_size(page_size, default=20, max_value=MAX_DAY_PAGE_SIZE)
|
|
filters = _base_filters(user_id=user_id, resource_type=resource_type, keyword=keyword)
|
|
filters.append(func.date(UploadResource.created_at) == target_date)
|
|
|
|
log_upload_resource_event(
|
|
event_type=UploadResourceEventEnum.HISTORY_DAY_LIST_START.value,
|
|
user_id=user_id,
|
|
detail={"generated_date": generated_date, "resource_type": resource_type, "page": page, "page_size": page_size, "keyword": keyword},
|
|
)
|
|
|
|
total_stmt = select(func.count(UploadResource.id)).where(and_(*filters))
|
|
total = int((await db.execute(total_stmt)).scalar_one() or 0)
|
|
|
|
item_stmt = (
|
|
select(UploadResource)
|
|
.where(and_(*filters))
|
|
.order_by(UploadResource.created_at.desc(), UploadResource.id.desc())
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
)
|
|
resources = list((await db.execute(item_stmt)).scalars().all())
|
|
items = [_item_to_out(resource) for resource in resources]
|
|
|
|
log_upload_resource_event(
|
|
event_type=UploadResourceEventEnum.HISTORY_DAY_LIST_SUCCESS.value,
|
|
user_id=user_id,
|
|
detail={"generated_date": generated_date, "resource_type": resource_type, "page": page, "page_size": page_size, "total": total},
|
|
)
|
|
return UploadResourceHistoryDayItemsOut(
|
|
generated_date=target_date.isoformat(),
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
items=items,
|
|
)
|
|
|
|
|
|
async def assert_upload_resource_history_ids_visible(
|
|
db: AsyncSession,
|
|
*,
|
|
user_id: str,
|
|
resource_ids: Iterable[str],
|
|
) -> list[UploadResource]:
|
|
ids = [str(value).strip() for value in resource_ids if str(value or "").strip()]
|
|
if not ids:
|
|
raise HTTPException(status_code=400, detail="resource_ids 不能为空")
|
|
stmt = (
|
|
select(UploadResource)
|
|
.where(
|
|
UploadResource.id.in_(ids),
|
|
*_base_filters(user_id=user_id),
|
|
)
|
|
.with_for_update()
|
|
)
|
|
resources = list((await db.execute(stmt)).scalars().all())
|
|
found = {resource.id for resource in resources}
|
|
missing = [resource_id for resource_id in ids if resource_id not in found]
|
|
if missing:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={
|
|
"message": "上传素材不存在、已删除、已绑定模块业务记录或无权操作",
|
|
"missing_ids": missing,
|
|
"missing_count": len(missing),
|
|
},
|
|
)
|
|
return resources
|