本次提交包含多类代码优化: 1. 修复多处单行代码换行格式,统一代码排版 2. 为外部系统模块新增快捷菜单配置 3. 优化前端API请求参数命名一致性 4. 补充媒体下载超限错误码与领域异常类 5. 完善仓储层更新逻辑,支持显式清空字段 6. 优化部分测试用例与工具函数代码结构 7. 为微信插件白名单缓存增加过期时间
557 lines
21 KiB
Python
557 lines
21 KiB
Python
"""Asset 子域 Router。
|
||
|
||
外部系统限界上下文的适配器资产管理 API,覆盖资产 CRUD / 校验 / 预览 /
|
||
引用 / 生成预览。所有端点通过 ``create_use_cases_from_db`` 装配 use_cases,
|
||
经 ``asset_service`` 端口调用用例。
|
||
|
||
Request Schema 与 Input DTO 不共享类,Router 内显式构造 DTO,操作人字段由
|
||
current_user.uid 填充。``content`` 字段在 Request Schema 中为 base64 字符串,
|
||
Router 内统一解码为 bytes(含 probe 端点);``size`` 与 ``checksum`` 由 service
|
||
根据 ``content`` 统一计算,不在 Router 侧透传,避免与 service 计算结果不一致。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
import binascii
|
||
import logging
|
||
from typing import Any
|
||
|
||
from fastapi import (
|
||
APIRouter,
|
||
Depends,
|
||
File,
|
||
Form,
|
||
Path,
|
||
Query,
|
||
UploadFile,
|
||
)
|
||
from pydantic import (
|
||
BaseModel,
|
||
ConfigDict,
|
||
Field,
|
||
field_validator,
|
||
model_validator,
|
||
)
|
||
from yuxi.external_systems.exceptions import ExternalSystemError, FrameworkError
|
||
from yuxi.external_systems.infrastructure.container import UseCases
|
||
from yuxi.external_systems.use_cases.dto.asset import (
|
||
BatchDeleteFailureItem,
|
||
BatchDeleteOutput,
|
||
BatchUploadFailureItem,
|
||
BatchUploadOutput,
|
||
BatchValidateFailureItem,
|
||
BatchValidateOutput,
|
||
CreateAssetInput,
|
||
DeleteAssetInput,
|
||
GeneratePreviewInput,
|
||
GetAssetByChecksumInput,
|
||
GetAssetInput,
|
||
GetAssetReferencesInput,
|
||
ListAssetsInput,
|
||
PreviewAssetInput,
|
||
ProbeAssetInput,
|
||
UpdateAssetInput,
|
||
ValidateAssetInput,
|
||
)
|
||
from yuxi.external_systems.use_cases.services.asset_service import MAX_CONTENT_BYTES
|
||
from yuxi.storage.postgres.models_business import User
|
||
from yuxi.utils.trace_context import get_trace_id
|
||
|
||
from server.routers.external_systems import get_use_cases, parse_and_naive, validate_time_range
|
||
from server.utils.auth_middleware import get_admin_user, get_required_user
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
asset_router = APIRouter(prefix="/assets", tags=["external-systems-asset"])
|
||
|
||
# base64 字符串最大长度(≈10MB 二进制,含 33% 编码开销 + padding 余量)。
|
||
# 二进制上限引用 ``asset_service.MAX_CONTENT_BYTES``,保证两处一致。
|
||
_MAX_BASE64_LENGTH = int(MAX_CONTENT_BYTES * 1.4)
|
||
|
||
|
||
# ---------------- Request Schemas ----------------
|
||
|
||
|
||
def _validate_base64_content(value: str) -> str:
|
||
"""校验 base64 编码字符串,非法时抛 ValueError(由 Pydantic 映射为 422)。"""
|
||
try:
|
||
base64.b64decode(value, validate=True)
|
||
except (binascii.Error, ValueError) as exc:
|
||
raise ValueError("content 必须是合法的 base64 编码") from exc
|
||
return value
|
||
|
||
|
||
class CreateAssetRequest(BaseModel):
|
||
"""创建资产请求体。字段对齐 ``CreateAssetInput``(不含 size、checksum 与 created_by)。
|
||
|
||
``content`` 为 base64 编码的字符串,Router 内解码为 bytes;
|
||
``size`` 与 ``checksum`` 由 service 根据 ``content`` 统一计算。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
adapter_type: str = Field(..., max_length=32)
|
||
asset_type: str = Field(..., max_length=32)
|
||
name: str = Field(..., min_length=1, max_length=128)
|
||
content: str = Field(..., min_length=1, max_length=_MAX_BASE64_LENGTH)
|
||
|
||
@field_validator("content")
|
||
@classmethod
|
||
def validate_content(cls, v: str) -> str:
|
||
return _validate_base64_content(v)
|
||
|
||
|
||
class UpdateAssetRequest(BaseModel):
|
||
"""更新资产请求体。字段对齐 ``UpdateAssetInput``(不含 id、size、checksum 与 updated_by)。
|
||
|
||
``content`` 为 base64 编码的字符串,Router 内解码为 bytes;
|
||
``size`` 与 ``checksum`` 由 service 根据 ``content`` 统一计算,
|
||
不接受客户端单独传入 ``checksum``,避免与实际内容不一致。
|
||
仅透传客户端显式设置的字段。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
name: str | None = Field(default=None, min_length=1, max_length=128)
|
||
status: str | None = Field(default=None, max_length=32)
|
||
status_message: str | None = None
|
||
content: str | None = Field(default=None, min_length=1, max_length=_MAX_BASE64_LENGTH)
|
||
|
||
@field_validator("content")
|
||
@classmethod
|
||
def validate_content(cls, v: str | None) -> str | None:
|
||
if v is None:
|
||
return v
|
||
return _validate_base64_content(v)
|
||
|
||
|
||
class BatchDeleteAssetsRequest(BaseModel):
|
||
"""批量删除资产请求体。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
asset_ids: list[int] = Field(..., min_length=1, max_length=100)
|
||
|
||
|
||
class BatchValidateAssetsRequest(BaseModel):
|
||
"""批量校验资产请求体。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
asset_ids: list[int] = Field(..., min_length=1, max_length=100)
|
||
|
||
|
||
class ProbeAssetRequest(BaseModel):
|
||
"""资产探测请求体。``content`` 为 base64 编码字符串,与 ``url`` 二选一。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
adapter_type: str = Field(..., max_length=32)
|
||
source_type: str = Field(..., max_length=32)
|
||
content: str | None = Field(default=None, min_length=1, max_length=_MAX_BASE64_LENGTH)
|
||
url: str | None = Field(default=None, max_length=2048)
|
||
|
||
@field_validator("content")
|
||
@classmethod
|
||
def validate_content(cls, v: str | None) -> str | None:
|
||
if v is None:
|
||
return v
|
||
return _validate_base64_content(v)
|
||
|
||
@model_validator(mode="after")
|
||
def validate_source(self) -> ProbeAssetRequest:
|
||
if self.content is None and self.url is None:
|
||
raise ValueError("必须提供 url 或 content")
|
||
if self.content is not None and self.url is not None:
|
||
raise ValueError("url 与 content 不能同时提供")
|
||
return self
|
||
|
||
|
||
# ---------------- Endpoints ----------------
|
||
|
||
|
||
@asset_router.get("", response_model=dict)
|
||
async def list_assets(
|
||
limit: int = Query(20, ge=1, le=500),
|
||
offset: int = Query(0, ge=0),
|
||
adapter_type: str | None = Query(None, max_length=32),
|
||
asset_type: str | None = Query(None, max_length=32),
|
||
status: str | None = Query(None, max_length=32),
|
||
keyword: str | None = Query(None, max_length=128),
|
||
start_time: str | None = Query(None, description="创建时间起始(ISO 8601)"),
|
||
end_time: str | None = Query(None, description="创建时间结束(ISO 8601)"),
|
||
created_by: str | None = Query(None, max_length=64, description="按创建人过滤"),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""分页列出适配器资产。"""
|
||
logger.info(
|
||
"list_assets: user=%s limit=%s offset=%s adapter_type=%s asset_type=%s status=%s",
|
||
current_user.uid,
|
||
limit,
|
||
offset,
|
||
adapter_type,
|
||
asset_type,
|
||
status,
|
||
)
|
||
start_dt = parse_and_naive("start_time", start_time)
|
||
end_dt = parse_and_naive("end_time", end_time)
|
||
validate_time_range(start_dt, end_dt)
|
||
input_dto = ListAssetsInput(
|
||
limit=limit,
|
||
offset=offset,
|
||
adapter_type=adapter_type,
|
||
asset_type=asset_type,
|
||
status=status,
|
||
keyword=keyword,
|
||
start_time=start_dt,
|
||
end_time=end_dt,
|
||
created_by=created_by,
|
||
)
|
||
output = await use_cases.asset_service.list_assets(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@asset_router.post("", response_model=dict)
|
||
async def create_asset(
|
||
payload: CreateAssetRequest,
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""创建适配器资产。``content`` 为 base64 字符串,解码后交由 service 计算 size 与 checksum。"""
|
||
content = base64.b64decode(payload.content)
|
||
input_dto = CreateAssetInput(
|
||
adapter_type=payload.adapter_type,
|
||
asset_type=payload.asset_type,
|
||
name=payload.name,
|
||
content=content,
|
||
created_by=current_user.uid,
|
||
)
|
||
output = await use_cases.asset_service.create_asset(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@asset_router.get("/stats", response_model=dict)
|
||
async def get_asset_stats(
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询资产统计(按适配器类型、资产类型、状态分组)。"""
|
||
logger.info("get_asset_stats: user=%s", current_user.uid)
|
||
output = await use_cases.asset_service.get_asset_stats()
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@asset_router.get("/by-checksum/{checksum}", response_model=dict)
|
||
async def get_asset_by_checksum(
|
||
checksum: str = Path(
|
||
...,
|
||
min_length=64,
|
||
max_length=64,
|
||
pattern=r"^[0-9a-f]{64}$",
|
||
description="SHA-256 checksum(64 位十六进制字符串)",
|
||
),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""按 SHA-256 checksum 查询资产(上传去重检查)。资产不存在时 data 为 null。"""
|
||
input_dto = GetAssetByChecksumInput(checksum=checksum)
|
||
output = await use_cases.asset_service.get_asset_by_checksum(input_dto)
|
||
return {"success": True, "data": output.model_dump() if output else None}
|
||
|
||
|
||
@asset_router.post("/batch-upload", response_model=BatchUploadOutput)
|
||
async def batch_upload_assets(
|
||
adapter_type: str = Form(..., max_length=32),
|
||
asset_type: str = Form(..., max_length=32),
|
||
files: list[UploadFile] = File(..., min_length=1, max_length=100),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> BatchUploadOutput:
|
||
"""批量上传资产文件(multipart/form-data),允许部分成功。
|
||
|
||
所有文件共享一个 ``adapter_type`` 与 ``asset_type``,适用于批量上传同类型资产场景。
|
||
单次最多 100 个文件,每个文件独立创建,失败项记录 filename 与 reason。
|
||
业务异常(``ExternalSystemError``)与技术异常(``FrameworkError``)均不中断批量流程,
|
||
技术异常额外记录堆栈日志便于排查。
|
||
"""
|
||
uploaded = []
|
||
failed = []
|
||
for file in files:
|
||
filename = file.filename or "unnamed"
|
||
try:
|
||
content = await file.read()
|
||
input_dto = CreateAssetInput(
|
||
adapter_type=adapter_type,
|
||
asset_type=asset_type,
|
||
name=filename,
|
||
content=content,
|
||
created_by=current_user.uid,
|
||
)
|
||
output = await use_cases.asset_service.create_asset(input_dto)
|
||
uploaded.append(output)
|
||
except FrameworkError as exc:
|
||
logger.exception("批量上传文件 %s 时发生框架异常", filename)
|
||
failed.append(
|
||
BatchUploadFailureItem(
|
||
filename=filename,
|
||
reason=str(exc),
|
||
error_code=exc.error_code,
|
||
trace_id=exc.trace_id or get_trace_id(),
|
||
)
|
||
)
|
||
except ExternalSystemError as exc:
|
||
failed.append(
|
||
BatchUploadFailureItem(
|
||
filename=filename,
|
||
reason=str(exc),
|
||
error_code=exc.error_code,
|
||
trace_id=exc.trace_id or get_trace_id(),
|
||
)
|
||
)
|
||
except Exception as exc:
|
||
logger.exception("批量上传文件 %s 时发生非预期异常", filename)
|
||
wrapped = FrameworkError(f"批量上传文件 {filename} 失败: {exc}")
|
||
failed.append(
|
||
BatchUploadFailureItem(
|
||
filename=filename,
|
||
reason=str(wrapped),
|
||
error_code=wrapped.error_code,
|
||
trace_id=get_trace_id(),
|
||
)
|
||
)
|
||
return BatchUploadOutput(uploaded=uploaded, failed=failed)
|
||
|
||
|
||
@asset_router.post("/batch-delete", response_model=BatchDeleteOutput)
|
||
async def batch_delete_assets(
|
||
payload: BatchDeleteAssetsRequest,
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> BatchDeleteOutput:
|
||
"""批量删除资产(软删除),允许部分成功。
|
||
|
||
循环调用 ``delete_asset`` 保证每个资产的引用检查完整执行。
|
||
业务异常(``ExternalSystemError``)与技术异常(``FrameworkError``)均不中断批量流程,
|
||
技术异常额外记录堆栈日志便于排查。
|
||
"""
|
||
deleted_count = 0
|
||
failed = []
|
||
for asset_id in payload.asset_ids:
|
||
try:
|
||
input_dto = DeleteAssetInput(id=asset_id, user=current_user.uid)
|
||
await use_cases.asset_service.delete_asset(input_dto)
|
||
deleted_count += 1
|
||
except FrameworkError as exc:
|
||
logger.exception("批量删除资产 %s 时发生框架异常", asset_id)
|
||
failed.append(
|
||
BatchDeleteFailureItem(
|
||
asset_id=asset_id,
|
||
reason=str(exc),
|
||
error_code=exc.error_code,
|
||
trace_id=exc.trace_id or get_trace_id(),
|
||
)
|
||
)
|
||
except ExternalSystemError as exc:
|
||
failed.append(
|
||
BatchDeleteFailureItem(
|
||
asset_id=asset_id,
|
||
reason=str(exc),
|
||
error_code=exc.error_code,
|
||
trace_id=exc.trace_id or get_trace_id(),
|
||
)
|
||
)
|
||
except Exception as exc:
|
||
logger.exception("批量删除资产 %s 时发生非预期异常", asset_id)
|
||
wrapped = FrameworkError(f"批量删除资产 {asset_id} 失败: {exc}")
|
||
failed.append(
|
||
BatchDeleteFailureItem(
|
||
asset_id=asset_id,
|
||
reason=str(wrapped),
|
||
error_code=wrapped.error_code,
|
||
trace_id=get_trace_id(),
|
||
)
|
||
)
|
||
return BatchDeleteOutput(deleted_count=deleted_count, failed=failed)
|
||
|
||
|
||
@asset_router.post("/batch-validate", response_model=BatchValidateOutput)
|
||
async def batch_validate_assets(
|
||
payload: BatchValidateAssetsRequest,
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> BatchValidateOutput:
|
||
"""批量校验资产并更新状态,允许部分成功。
|
||
|
||
业务异常(``ExternalSystemError``)与技术异常(``FrameworkError``)均不中断批量流程,
|
||
技术异常额外记录堆栈日志便于排查。
|
||
"""
|
||
validated = []
|
||
failed = []
|
||
for asset_id in payload.asset_ids:
|
||
try:
|
||
input_dto = ValidateAssetInput(id=asset_id, user=current_user.uid)
|
||
output = await use_cases.asset_service.validate_asset(input_dto)
|
||
validated.append(output)
|
||
except FrameworkError as exc:
|
||
logger.exception("批量校验资产 %s 时发生框架异常", asset_id)
|
||
failed.append(
|
||
BatchValidateFailureItem(
|
||
asset_id=asset_id,
|
||
reason=str(exc),
|
||
error_code=exc.error_code,
|
||
trace_id=exc.trace_id or get_trace_id(),
|
||
)
|
||
)
|
||
except ExternalSystemError as exc:
|
||
failed.append(
|
||
BatchValidateFailureItem(
|
||
asset_id=asset_id,
|
||
reason=str(exc),
|
||
error_code=exc.error_code,
|
||
trace_id=exc.trace_id or get_trace_id(),
|
||
)
|
||
)
|
||
except Exception as exc:
|
||
logger.exception("批量校验资产 %s 时发生非预期异常", asset_id)
|
||
wrapped = FrameworkError(f"批量校验资产 {asset_id} 失败: {exc}")
|
||
failed.append(
|
||
BatchValidateFailureItem(
|
||
asset_id=asset_id,
|
||
reason=str(wrapped),
|
||
error_code=wrapped.error_code,
|
||
trace_id=get_trace_id(),
|
||
)
|
||
)
|
||
return BatchValidateOutput(validated=validated, failed=failed)
|
||
|
||
|
||
@asset_router.post("/probe", response_model=dict)
|
||
async def probe_asset(
|
||
payload: ProbeAssetRequest,
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""探测资产内容(URL 或 base64 内容),返回适配器解析的摘要,不保存资产。
|
||
|
||
``content`` 为 base64 字符串,Router 内解码为 bytes 后传给 service,
|
||
与 create/update 端点保持一致。
|
||
"""
|
||
content_bytes: bytes | None = None
|
||
if payload.content is not None:
|
||
content_bytes = base64.b64decode(payload.content)
|
||
input_dto = ProbeAssetInput(
|
||
adapter_type=payload.adapter_type,
|
||
source_type=payload.source_type,
|
||
content=content_bytes,
|
||
url=payload.url,
|
||
)
|
||
output = await use_cases.asset_service.probe_asset(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@asset_router.get("/{asset_id}", response_model=dict)
|
||
async def get_asset(
|
||
asset_id: int = Path(ge=1),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取资产详情。"""
|
||
input_dto = GetAssetInput(id=asset_id)
|
||
output = await use_cases.asset_service.get_asset(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@asset_router.put("/{asset_id}", response_model=dict)
|
||
async def update_asset(
|
||
payload: UpdateAssetRequest,
|
||
asset_id: int = Path(ge=1),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""更新资产。``content`` 为 base64 字符串,解码后交由 service 计算 size 与 checksum。仅透传显式设置的字段。"""
|
||
data = payload.model_dump(exclude_unset=True)
|
||
content_b64 = data.pop("content", None)
|
||
if content_b64 is not None:
|
||
data["content"] = base64.b64decode(content_b64)
|
||
input_dto = UpdateAssetInput(
|
||
id=asset_id,
|
||
updated_by=current_user.uid,
|
||
**data,
|
||
)
|
||
output = await use_cases.asset_service.update_asset(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@asset_router.delete("/{asset_id}", response_model=dict)
|
||
async def delete_asset(
|
||
asset_id: int = Path(ge=1),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""删除资产。"""
|
||
input_dto = DeleteAssetInput(id=asset_id, user=current_user.uid)
|
||
output = await use_cases.asset_service.delete_asset(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@asset_router.post("/{asset_id}/validate", response_model=dict)
|
||
async def validate_asset(
|
||
asset_id: int = Path(ge=1),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""校验资产。"""
|
||
input_dto = ValidateAssetInput(id=asset_id, user=current_user.uid)
|
||
output = await use_cases.asset_service.validate_asset(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@asset_router.get("/{asset_id}/preview", response_model=dict)
|
||
async def preview_asset(
|
||
asset_id: int = Path(ge=1),
|
||
max_lines: int = Query(200, ge=1, le=10000),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""预览资产内容(适配器视角的结构化摘要)。
|
||
|
||
调用适配器的 ``preview_asset`` 能力,返回适配器解析的摘要(如 OpenAPI 的 endpoint 列表)。
|
||
与 ``generate_preview`` 的区别:本端点调用外部适配器获取结构化摘要,
|
||
``generate_preview`` 仅本地解码内容并截取前 N 行原始文本。
|
||
"""
|
||
input_dto = PreviewAssetInput(id=asset_id, max_lines=max_lines)
|
||
output = await use_cases.asset_service.preview_asset(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@asset_router.get("/{asset_id}/generate-preview", response_model=dict)
|
||
async def generate_preview(
|
||
asset_id: int = Path(ge=1),
|
||
max_lines: int = Query(200, ge=1, le=10000),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""生成资产文本预览(本地解码内容并截取前 N 行)。
|
||
|
||
不调用外部适配器,仅本地解码内容并截取前 N 行,适用于查看资产原始文本内容。
|
||
与 ``preview_asset`` 的区别:本端点不调用外部适配器,仅返回原始文本前 N 行;
|
||
``preview_asset`` 调用适配器获取结构化摘要。
|
||
"""
|
||
input_dto = GeneratePreviewInput(id=asset_id, max_lines=max_lines)
|
||
output = await use_cases.asset_service.generate_preview(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@asset_router.get("/{asset_id}/references", response_model=dict)
|
||
async def get_asset_references(
|
||
asset_id: int = Path(ge=1),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取资产引用(关联的系统与工具)。"""
|
||
input_dto = GetAssetReferencesInput(id=asset_id)
|
||
output = await use_cases.asset_service.get_asset_references(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|