管理后台上传集成API | 首页素材装修追加提词/附件
This commit is contained in:
@@ -16,6 +16,7 @@ from app.enums.home_material import (
|
||||
from app.models.home_material import HomeMaterialAsset, HomeMaterialCategory, HomeMaterialWatermark
|
||||
from app.schemas.home_material import (
|
||||
HomeMaterialAssetOut,
|
||||
HomeMaterialMediaReference,
|
||||
HomeMaterialCategoryOut,
|
||||
HomeMaterialPublicAssetOut,
|
||||
HomeMaterialPublicCategoryGroupOut,
|
||||
@@ -39,6 +40,24 @@ def _load_json(value: str | None) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
|
||||
def _load_media_references(value: str | None) -> list[HomeMaterialMediaReference]:
|
||||
if not value:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(value)
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
refs: list[HomeMaterialMediaReference] = []
|
||||
for item in data:
|
||||
try:
|
||||
refs.append(HomeMaterialMediaReference(**item))
|
||||
except Exception:
|
||||
continue
|
||||
return refs
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
class HomeMaterialQueryService:
|
||||
"""首页素材高性能查询组装层:列表查询 → ID 去重 → 批量查询 → map 组装。"""
|
||||
|
||||
@@ -152,6 +171,8 @@ class HomeMaterialQueryService:
|
||||
watermark_id=asset.watermark_id,
|
||||
watermark_name=watermark.name if watermark else None,
|
||||
watermark_config=_load_json(asset.watermark_config_json),
|
||||
generation_prompt=asset.generation_prompt,
|
||||
media_references=_load_media_references(asset.media_references_json),
|
||||
width=asset.width,
|
||||
height=asset.height,
|
||||
duration_seconds=asset.duration_seconds,
|
||||
@@ -340,6 +361,8 @@ class HomeMaterialQueryService:
|
||||
width=a.width,
|
||||
height=a.height,
|
||||
duration_seconds=a.duration_seconds,
|
||||
generation_prompt=a.generation_prompt,
|
||||
media_references=_load_media_references(a.media_references_json),
|
||||
sort_order=a.sort_order,
|
||||
)
|
||||
for a in assets
|
||||
@@ -403,6 +426,8 @@ class HomeMaterialQueryService:
|
||||
width=asset.width,
|
||||
height=asset.height,
|
||||
duration_seconds=asset.duration_seconds,
|
||||
generation_prompt=asset.generation_prompt,
|
||||
media_references=_load_media_references(asset.media_references_json),
|
||||
sort_order=asset.sort_order,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -12,14 +12,20 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.config import settings
|
||||
from app.enums.home_material import (
|
||||
HOME_MATERIAL_DEFAULT_CONFIG,
|
||||
HOME_MATERIAL_GENERATION_PROMPT_MAX_LEN,
|
||||
HOME_MATERIAL_MEDIA_REFERENCES_JSON_MAX_LEN,
|
||||
HOME_MATERIAL_MEDIA_REFERENCES_MAX_COUNT,
|
||||
HomeMaterialAssetStatus,
|
||||
HomeMaterialConfigKeyEnum,
|
||||
HomeMaterialLogEventEnum,
|
||||
HomeMaterialMediaType,
|
||||
HomeMaterialPublicResponseMode,
|
||||
HomeMaterialWatermarkPosition,
|
||||
HomeMaterialWatermarkSizeMode,
|
||||
HomeMaterialWatermarkType,
|
||||
)
|
||||
from app.enums.common import LogEventStatusEnum, LogSourceEnum
|
||||
from app.enums.upload_resource import UploadResourceModuleEnum, UploadResourceSourceModelEnum
|
||||
from app.models.base import async_session
|
||||
from app.models.home_material import HomeMaterialAsset, HomeMaterialCategory, HomeMaterialWatermark
|
||||
from app.models.system_config import SystemConfig
|
||||
@@ -36,6 +42,7 @@ from app.schemas.home_material import (
|
||||
HomeMaterialConfigUpdate,
|
||||
HomeMaterialPublicCategoryListOut,
|
||||
HomeMaterialPublicFlatOut,
|
||||
HomeMaterialMediaReference,
|
||||
HomeMaterialPublicGroupedOut,
|
||||
HomeMaterialRegenerateWatermarkRequest,
|
||||
HomeMaterialTextWatermarkPreviewRequest,
|
||||
@@ -49,6 +56,8 @@ from app.schemas.home_material import (
|
||||
from app.services.home_material.query import query_service
|
||||
from app.services.home_material.storage import storage_service
|
||||
from app.services.home_material.watermark_processor import watermark_processor
|
||||
from app.services.operation_log_service import build_exception_detail, log_operation_event
|
||||
from app.services.upload_resource.bind_service import bind_upload_resources
|
||||
from app.utils.id_gen import generate_id
|
||||
|
||||
|
||||
@@ -66,11 +75,99 @@ def _json_loads(value: str | None) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
|
||||
def _json_loads_list(value: str | None) -> list[Any]:
|
||||
if not value:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(value)
|
||||
return data if isinstance(data, list) else []
|
||||
except Exception as exc:
|
||||
log_operation_event(
|
||||
domain="home_material",
|
||||
module="home_material_asset",
|
||||
event_type=HomeMaterialLogEventEnum.MEDIA_REFERENCES_PARSE_FAILED.value,
|
||||
event_status=LogEventStatusEnum.WARNING.value,
|
||||
source=LogSourceEnum.SERVICE.value,
|
||||
detail={"raw_prefix": value[:500]},
|
||||
error=str(exc),
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
def _clean_title(value: str | None) -> str | None:
|
||||
value = (value or "").strip()
|
||||
return value or None
|
||||
|
||||
|
||||
def _clean_generation_prompt(value: str | None) -> str | None:
|
||||
text = (value or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
if len(text) > HOME_MATERIAL_GENERATION_PROMPT_MAX_LEN:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"生成提词不能超过 {HOME_MATERIAL_GENERATION_PROMPT_MAX_LEN} 个字符")
|
||||
return text
|
||||
|
||||
|
||||
def _media_references_from_json(value: str | None) -> list[HomeMaterialMediaReference]:
|
||||
if not value or not value.strip():
|
||||
return []
|
||||
if len(value) > HOME_MATERIAL_MEDIA_REFERENCES_JSON_MAX_LEN:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"附件JSON不能超过 {HOME_MATERIAL_MEDIA_REFERENCES_JSON_MAX_LEN} 个字符")
|
||||
try:
|
||||
raw = json.loads(value)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="附件JSON格式错误") from exc
|
||||
if raw is None:
|
||||
return []
|
||||
if not isinstance(raw, list):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="附件必须是数组格式")
|
||||
return _normalize_media_references(raw)
|
||||
|
||||
|
||||
def _normalize_media_references(value: list[Any] | None) -> list[HomeMaterialMediaReference]:
|
||||
if not value:
|
||||
return []
|
||||
if len(value) > HOME_MATERIAL_MEDIA_REFERENCES_MAX_COUNT:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"附件最多 {HOME_MATERIAL_MEDIA_REFERENCES_MAX_COUNT} 个")
|
||||
refs: list[HomeMaterialMediaReference] = []
|
||||
for item in value:
|
||||
try:
|
||||
ref = item if isinstance(item, HomeMaterialMediaReference) else HomeMaterialMediaReference(**item)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"附件格式错误:{exc}") from exc
|
||||
refs.append(ref)
|
||||
return refs
|
||||
|
||||
|
||||
def _dump_media_references_json(refs: list[HomeMaterialMediaReference] | None) -> str | None:
|
||||
items = [r.model_dump(mode="json", exclude_none=True) for r in (refs or [])]
|
||||
if not items:
|
||||
return None
|
||||
text = _json_dumps(items)
|
||||
if len(text) > HOME_MATERIAL_MEDIA_REFERENCES_JSON_MAX_LEN:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"附件JSON不能超过 {HOME_MATERIAL_MEDIA_REFERENCES_JSON_MAX_LEN} 个字符")
|
||||
return text
|
||||
|
||||
|
||||
def _media_references_out(value: str | None) -> list[HomeMaterialMediaReference]:
|
||||
return _normalize_media_references(_json_loads_list(value))
|
||||
|
||||
|
||||
def _media_reference_resource_ids(refs: list[HomeMaterialMediaReference]) -> list[str]:
|
||||
return [str(r.upload_resource_id).strip() for r in refs if r.upload_resource_id and str(r.upload_resource_id).strip()]
|
||||
|
||||
|
||||
def _media_reference_urls(refs: list[HomeMaterialMediaReference]) -> list[str]:
|
||||
return [str(r.url).strip() for r in refs if r.url and str(r.url).strip()]
|
||||
|
||||
|
||||
def _media_reference_type_counts(refs: list[HomeMaterialMediaReference]) -> dict[str, int]:
|
||||
counts: dict[str, int] = {}
|
||||
for ref in refs:
|
||||
counts[ref.type] = counts.get(ref.type, 0) + 1
|
||||
return counts
|
||||
|
||||
|
||||
def _normalize_watermark_config_dict(value: dict[str, Any] | None, fallback_watermark_id: str | None = None) -> dict[str, Any]:
|
||||
"""兼容旧水印配置。旧数据没有 watermark_type 时按 image 处理。"""
|
||||
data = dict(value or {})
|
||||
@@ -108,6 +205,8 @@ def _asset_snapshot(asset: HomeMaterialAsset | None) -> dict[str, Any] | None:
|
||||
"cover_url": asset.cover_url,
|
||||
"watermark_id": asset.watermark_id,
|
||||
"watermark_config": _json_loads(asset.watermark_config_json),
|
||||
"generation_prompt": asset.generation_prompt,
|
||||
"media_references_count": len(_media_references_out(asset.media_references_json)),
|
||||
"is_active": asset.is_active,
|
||||
"sort_order": asset.sort_order,
|
||||
"deleted_at": asset.deleted_at,
|
||||
@@ -142,6 +241,31 @@ def _watermark_snapshot(watermark: HomeMaterialWatermark | None) -> dict[str, An
|
||||
}
|
||||
|
||||
|
||||
|
||||
def _log_generation_config_event(
|
||||
event_type: str,
|
||||
*,
|
||||
admin_id: str | None,
|
||||
asset_id: str | None = None,
|
||||
event_status: str = LogEventStatusEnum.SUCCESS.value,
|
||||
message: str | None = None,
|
||||
detail: dict[str, Any] | None = None,
|
||||
error: str | None = None,
|
||||
) -> None:
|
||||
log_operation_event(
|
||||
domain="home_material",
|
||||
module="home_material_asset",
|
||||
event_type=event_type,
|
||||
event_status=event_status,
|
||||
source=LogSourceEnum.API.value,
|
||||
user_id=admin_id,
|
||||
asset_id=asset_id,
|
||||
message=message,
|
||||
detail=detail or {},
|
||||
error=error,
|
||||
)
|
||||
|
||||
|
||||
async def _flush_refresh(db: AsyncSession, obj: Any) -> None:
|
||||
"""
|
||||
写入后立即返回 ORM 对象前必须显式刷新。
|
||||
@@ -372,10 +496,24 @@ class HomeMaterialService:
|
||||
watermark_config: HomeMaterialWatermarkConfig,
|
||||
is_active: bool,
|
||||
sort_order: int,
|
||||
generation_prompt: str | None = None,
|
||||
media_references_json: str | None = None,
|
||||
admin_id: str | None,
|
||||
) -> tuple[HomeMaterialUploadResultOut, dict[str, Any]]:
|
||||
await self._get_category(db, category_id, active_only=False)
|
||||
clean_title = _clean_title(title)
|
||||
clean_prompt = _clean_generation_prompt(generation_prompt)
|
||||
refs = _media_references_from_json(media_references_json)
|
||||
refs_json = _dump_media_references_json(refs)
|
||||
_log_generation_config_event(
|
||||
HomeMaterialLogEventEnum.GENERATION_CONFIG_VALIDATE_SUCCESS.value,
|
||||
admin_id=admin_id,
|
||||
detail={
|
||||
"generation_prompt_length": len(clean_prompt or ""),
|
||||
"media_references_count": len(refs),
|
||||
"media_reference_types": _media_reference_type_counts(refs),
|
||||
},
|
||||
)
|
||||
watermark_type = watermark_config.watermark_type
|
||||
final_watermark_id: str | None = None
|
||||
if watermark_type == HomeMaterialWatermarkType.IMAGE:
|
||||
@@ -415,6 +553,8 @@ class HomeMaterialService:
|
||||
original_storage_path=stored.storage_path,
|
||||
watermark_id=final_watermark_id,
|
||||
watermark_config_json=_json_dumps(cfg.model_dump(mode="json")),
|
||||
generation_prompt=clean_prompt,
|
||||
media_references_json=refs_json,
|
||||
status=HomeMaterialAssetStatus.PROCESSING.value,
|
||||
width=probe.width,
|
||||
height=probe.height,
|
||||
@@ -427,7 +567,10 @@ class HomeMaterialService:
|
||||
)
|
||||
db.add(asset)
|
||||
await _flush_refresh(db, asset)
|
||||
return self._upload_result(asset, message="素材已上传,水印处理中"), _asset_snapshot(asset) or {}
|
||||
bind_stats = await self._bind_media_reference_resources(db, asset_id=asset.id, admin_id=admin_id, refs=refs)
|
||||
after = _asset_snapshot(asset) or {}
|
||||
after["media_reference_bind_stats"] = bind_stats
|
||||
return self._upload_result(asset, message="素材已上传,水印处理中"), after
|
||||
|
||||
async def list_assets(
|
||||
self,
|
||||
@@ -466,15 +609,67 @@ class HomeMaterialService:
|
||||
asset = await self._get_asset(db, asset_id)
|
||||
await self._get_category(db, req.category_id)
|
||||
before = _asset_snapshot(asset) or {}
|
||||
asset.category_id = req.category_id
|
||||
asset.title = req.title
|
||||
asset.is_active = req.is_active
|
||||
asset.sort_order = req.sort_order
|
||||
asset.updated_by = admin_id
|
||||
db.add(asset)
|
||||
await _flush_refresh(db, asset)
|
||||
after = _asset_snapshot(asset) or {}
|
||||
return await self.get_asset_detail(db, asset_id), before, after
|
||||
try:
|
||||
_log_generation_config_event(
|
||||
HomeMaterialLogEventEnum.GENERATION_CONFIG_VALIDATE_STARTED.value,
|
||||
admin_id=admin_id,
|
||||
asset_id=asset_id,
|
||||
event_status=LogEventStatusEnum.STARTED.value,
|
||||
)
|
||||
clean_prompt = _clean_generation_prompt(req.generation_prompt)
|
||||
refs = _normalize_media_references(req.media_references)
|
||||
refs_json = _dump_media_references_json(refs)
|
||||
_log_generation_config_event(
|
||||
HomeMaterialLogEventEnum.GENERATION_CONFIG_VALIDATE_SUCCESS.value,
|
||||
admin_id=admin_id,
|
||||
asset_id=asset_id,
|
||||
detail={
|
||||
"generation_prompt_length": len(clean_prompt or ""),
|
||||
"media_references_count": len(refs),
|
||||
"media_reference_types": _media_reference_type_counts(refs),
|
||||
},
|
||||
)
|
||||
_log_generation_config_event(
|
||||
HomeMaterialLogEventEnum.GENERATION_CONFIG_UPDATE_STARTED.value,
|
||||
admin_id=admin_id,
|
||||
asset_id=asset_id,
|
||||
event_status=LogEventStatusEnum.STARTED.value,
|
||||
)
|
||||
asset.category_id = req.category_id
|
||||
asset.title = req.title
|
||||
asset.is_active = req.is_active
|
||||
asset.sort_order = req.sort_order
|
||||
asset.generation_prompt = clean_prompt
|
||||
asset.media_references_json = refs_json
|
||||
asset.updated_by = admin_id
|
||||
db.add(asset)
|
||||
await _flush_refresh(db, asset)
|
||||
bind_stats = await self._bind_media_reference_resources(db, asset_id=asset.id, admin_id=admin_id, refs=refs)
|
||||
after = _asset_snapshot(asset) or {}
|
||||
after["media_reference_bind_stats"] = bind_stats
|
||||
_log_generation_config_event(
|
||||
HomeMaterialLogEventEnum.GENERATION_CONFIG_UPDATE_SUCCESS.value,
|
||||
admin_id=admin_id,
|
||||
asset_id=asset_id,
|
||||
detail={
|
||||
"changed_fields": [k for k in ("generation_prompt", "media_references_count") if before.get(k) != after.get(k)],
|
||||
"generation_prompt_length": len(clean_prompt or ""),
|
||||
"media_references_count": len(refs),
|
||||
"media_reference_types": _media_reference_type_counts(refs),
|
||||
"bind_stats": bind_stats,
|
||||
},
|
||||
)
|
||||
return await self.get_asset_detail(db, asset_id), before, after
|
||||
except Exception as exc:
|
||||
_log_generation_config_event(
|
||||
HomeMaterialLogEventEnum.GENERATION_CONFIG_UPDATE_FAILED.value,
|
||||
admin_id=admin_id,
|
||||
asset_id=asset_id,
|
||||
event_status=LogEventStatusEnum.FAILED.value,
|
||||
detail=build_exception_detail(exc, {"before": before}),
|
||||
error=str(exc),
|
||||
)
|
||||
raise
|
||||
|
||||
async def prepare_regenerate(
|
||||
self,
|
||||
@@ -509,6 +704,28 @@ class HomeMaterialService:
|
||||
db.add(asset)
|
||||
return asset, before
|
||||
|
||||
|
||||
async def _bind_media_reference_resources(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
asset_id: str,
|
||||
admin_id: str | None,
|
||||
refs: list[HomeMaterialMediaReference],
|
||||
) -> dict[str, int]:
|
||||
if not admin_id or not refs:
|
||||
return {"matched": 0, "bound": 0, "skipped": 0, "conflict": 0}
|
||||
return await bind_upload_resources(
|
||||
db,
|
||||
user_id=admin_id,
|
||||
module=UploadResourceModuleEnum.HOME_MATERIAL.value,
|
||||
source_model=UploadResourceSourceModelEnum.HOME_MATERIAL_ASSET.value,
|
||||
source_id=asset_id,
|
||||
resource_ids=_media_reference_resource_ids(refs),
|
||||
urls=_media_reference_urls(refs),
|
||||
allow_common_migrate=False,
|
||||
)
|
||||
|
||||
async def get_asset_status(self, db: AsyncSession, asset_id: str) -> HomeMaterialAssetStatusOut:
|
||||
asset = await self._get_asset(db, asset_id)
|
||||
return HomeMaterialAssetStatusOut(
|
||||
@@ -541,6 +758,8 @@ class HomeMaterialService:
|
||||
watermarked_url=asset.watermarked_url,
|
||||
cover_url=asset.cover_url,
|
||||
watermark_config=_json_loads(asset.watermark_config_json),
|
||||
generation_prompt=asset.generation_prompt,
|
||||
media_references=_media_references_out(asset.media_references_json),
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user