302 lines
18 KiB
Python
302 lines
18 KiB
Python
from __future__ import annotations
|
||
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||
|
||
from app.enums.home_material import (
|
||
HomeMaterialAssetStatus,
|
||
HomeMaterialMediaType,
|
||
HomeMaterialPublicResponseMode,
|
||
HomeMaterialWatermarkPosition,
|
||
HomeMaterialWatermarkSizeMode,
|
||
HomeMaterialWatermarkType,
|
||
)
|
||
from app.schemas.common import NaiveDatetimeOptional
|
||
|
||
|
||
class HomeMaterialConfigOut(BaseModel):
|
||
enabled: bool = Field(False, description="首页是否展示素材装修模块。false 时前台行业和素材接口返回 enabled=false 且列表为空。")
|
||
title: str = Field("行业素材案例", max_length=64, description="首页素材模块标题。")
|
||
subtitle: str = Field("精选图片与视频素材展示", max_length=128, description="首页素材模块副标题。")
|
||
show_original_in_admin: bool = Field(True, description="后台是否展示原始素材链接。前台接口永远不返回原始素材链接。")
|
||
|
||
|
||
class HomeMaterialConfigUpdate(BaseModel):
|
||
enabled: bool = Field(False, description="首页是否展示素材装修模块。")
|
||
title: str = Field("行业素材案例", min_length=1, max_length=64, description="首页素材模块标题。")
|
||
subtitle: str = Field("精选图片与视频素材展示", max_length=128, description="首页素材模块副标题。")
|
||
show_original_in_admin: bool = Field(True, description="后台是否展示原始素材链接。")
|
||
|
||
|
||
class HomeMaterialCategoryCreate(BaseModel):
|
||
name: str = Field(..., min_length=1, max_length=64, description="行业名称,例如:美妆护肤、食品生鲜。")
|
||
key: str = Field(..., min_length=1, max_length=64, pattern=r"^[A-Za-z0-9_-]+$", description="行业唯一标识,只允许字母、数字、下划线、中划线;前台可通过 category_key 查询。")
|
||
description: str | None = Field(None, max_length=255, description="行业描述。")
|
||
icon: str | None = Field(None, max_length=64, description="前端图标名称,例如 PictureOutlined、VideoCameraOutlined。")
|
||
is_active: bool = Field(True, description="是否启用。禁用后前台不展示。")
|
||
sort_order: int = Field(0, ge=0, le=999999, description="排序值,越小越靠前。")
|
||
|
||
|
||
class HomeMaterialCategoryUpdate(HomeMaterialCategoryCreate):
|
||
pass
|
||
|
||
|
||
class HomeMaterialCategoryOut(BaseModel):
|
||
id: str = Field(..., description="行业ID。")
|
||
name: str = Field(..., description="行业名称。")
|
||
key: str = Field(..., description="行业唯一标识。")
|
||
description: str | None = Field(None, description="行业描述。")
|
||
icon: str | None = Field(None, description="前端图标名称。")
|
||
is_active: bool = Field(True, description="是否启用。")
|
||
sort_order: int = Field(0, description="排序值。")
|
||
asset_count: int = Field(0, description="可展示素材总数。后台列表按未删除素材统计,前台按 success + is_active 统计。")
|
||
image_count: int = Field(0, description="图片素材数量。")
|
||
video_count: int = Field(0, description="视频素材数量。")
|
||
created_at: NaiveDatetimeOptional = Field(None, description="创建时间。")
|
||
updated_at: NaiveDatetimeOptional = Field(None, description="更新时间。")
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
|
||
class HomeMaterialCategoryListOut(BaseModel):
|
||
items: list[HomeMaterialCategoryOut] = Field(default_factory=list, description="行业列表。")
|
||
total: int = Field(0, description="符合条件的行业总数。")
|
||
|
||
|
||
class HomeMaterialWatermarkOut(BaseModel):
|
||
id: str = Field(..., description="水印ID。")
|
||
name: str = Field(..., description="水印名称。")
|
||
file_url: str = Field(..., description="水印图片URL。")
|
||
file_name: str | None = Field(None, description="上传文件名。")
|
||
file_size_bytes: int = Field(0, description="文件大小,单位字节。")
|
||
width: int | None = Field(None, description="水印图片宽度。")
|
||
height: int | None = Field(None, description="水印图片高度。")
|
||
is_default: bool = Field(False, description="是否默认水印。")
|
||
is_active: bool = Field(True, description="是否启用。")
|
||
created_at: NaiveDatetimeOptional = None
|
||
updated_at: NaiveDatetimeOptional = None
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
|
||
class HomeMaterialWatermarkUpdate(BaseModel):
|
||
name: str = Field(..., min_length=1, max_length=128, description="水印名称。")
|
||
is_default: bool = Field(False, description="是否设为默认水印。设为 true 时会自动取消其他默认水印。")
|
||
is_active: bool = Field(True, description="是否启用。")
|
||
|
||
|
||
class HomeMaterialWatermarkListOut(BaseModel):
|
||
items: list[HomeMaterialWatermarkOut] = Field(default_factory=list, description="水印列表。")
|
||
total: int = Field(0, description="符合条件的水印总数。")
|
||
|
||
|
||
class HomeMaterialTextWatermarkConfig(BaseModel):
|
||
"""重复文字水印配置。字体固定由后端 HOME_MATERIAL_TEXT_WATERMARK_FONT 指定,不允许前端传字体,避免版权和一致性问题。"""
|
||
|
||
text: str = Field(..., min_length=1, max_length=64, description="重复水印文字。")
|
||
opacity_level: int = Field(2, ge=1, le=10, description="文字透明度档位,1-10;1=0.1,10=1.0。")
|
||
opacity: float = Field(0.2, ge=0.1, le=1.0, description="实际透明度,后端根据 opacity_level 计算。")
|
||
font_size_px: int = Field(28, ge=8, le=160, description="字号,单位 px。")
|
||
color: str = Field("#ffffff", pattern=r"^#[0-9A-Fa-f]{6}$", description="文字颜色,#RRGGBB。")
|
||
rotate_deg: int = Field(-30, ge=-90, le=90, description="文字旋转角度,单位度。")
|
||
gap_x: int = Field(220, ge=20, le=2000, description="横向平铺间距。")
|
||
gap_y: int = Field(140, ge=20, le=2000, description="纵向平铺间距。")
|
||
staggered: bool = Field(True, description="是否交错排列。")
|
||
|
||
@field_validator("text")
|
||
@classmethod
|
||
def validate_text(cls, value: str) -> str:
|
||
value = (value or "").strip()
|
||
if not value:
|
||
raise ValueError("重复文字水印内容不能为空")
|
||
return value
|
||
|
||
@model_validator(mode="after")
|
||
def fill_opacity(self) -> "HomeMaterialTextWatermarkConfig":
|
||
self.opacity = round(self.opacity_level / 10, 2)
|
||
return self
|
||
|
||
|
||
class HomeMaterialWatermarkConfig(BaseModel):
|
||
watermark_type: HomeMaterialWatermarkType = Field(HomeMaterialWatermarkType.IMAGE, description="水印类型:image=图片水印;repeated_text=重复文字水印。旧数据无该字段时默认 image。")
|
||
watermark_id: str | None = Field(None, description="图片水印ID。watermark_type=image 时使用。上传素材时可由 watermark_id 指定,也可通过临时 watermark_file 生成。")
|
||
opacity_level: int = Field(6, ge=1, le=10, description="图片水印透明度档位,1-10;1=0.1,10=1.0。")
|
||
opacity: float = Field(0.6, ge=0.1, le=1.0, description="图片水印实际透明度,后端根据 opacity_level 计算。")
|
||
position: HomeMaterialWatermarkPosition = Field(HomeMaterialWatermarkPosition.BOTTOM_RIGHT, description="图片水印位置枚举。")
|
||
custom_x_ratio: float | None = Field(None, ge=0, le=1, description="图片水印自定义拖拽位置 X 比例,position=custom 时使用,0-1。")
|
||
custom_y_ratio: float | None = Field(None, ge=0, le=1, description="图片水印自定义拖拽位置 Y 比例,position=custom 时使用,0-1。")
|
||
size_mode: HomeMaterialWatermarkSizeMode = Field(HomeMaterialWatermarkSizeMode.RATIO, description="图片水印尺寸模式:ratio=按素材宽度比例;px=固定像素宽度。")
|
||
width_ratio: float | None = Field(0.18, ge=0.01, le=1, description="图片水印 size_mode=ratio 时使用,水印宽度占素材宽度比例,例如 0.18。")
|
||
width_px: int | None = Field(None, ge=1, le=10000, description="图片水印 size_mode=px 时使用,固定水印宽度。")
|
||
margin_x: int = Field(24, ge=0, le=2000, description="图片水印横向边距,九宫格位置使用。")
|
||
margin_y: int = Field(24, ge=0, le=2000, description="图片水印纵向边距,九宫格位置使用。")
|
||
text_watermark: HomeMaterialTextWatermarkConfig | None = Field(None, description="重复文字水印配置。watermark_type=repeated_text 时必填。")
|
||
|
||
@model_validator(mode="after")
|
||
def validate_by_type(self) -> "HomeMaterialWatermarkConfig":
|
||
self.opacity = round(self.opacity_level / 10, 2)
|
||
if self.watermark_type == HomeMaterialWatermarkType.REPEATED_TEXT:
|
||
if self.text_watermark is None:
|
||
raise ValueError("watermark_type=repeated_text 时 text_watermark 必填")
|
||
self.watermark_id = None
|
||
return self
|
||
|
||
if self.position == HomeMaterialWatermarkPosition.CUSTOM:
|
||
if self.custom_x_ratio is None or self.custom_y_ratio is None:
|
||
raise ValueError("position=custom 时 custom_x_ratio 和 custom_y_ratio 必填")
|
||
if self.size_mode == HomeMaterialWatermarkSizeMode.RATIO and self.width_ratio is None:
|
||
raise ValueError("size_mode=ratio 时 width_ratio 必填")
|
||
if self.size_mode == HomeMaterialWatermarkSizeMode.PX and self.width_px is None:
|
||
raise ValueError("size_mode=px 时 width_px 必填")
|
||
self.text_watermark = None
|
||
return self
|
||
|
||
|
||
class HomeMaterialTextWatermarkPreviewRequest(BaseModel):
|
||
width: int = Field(..., ge=1, le=8192, description="预览层宽度,建议传素材真实宽度。")
|
||
height: int = Field(..., ge=1, le=8192, description="预览层高度,建议传素材真实高度。")
|
||
text_watermark: HomeMaterialTextWatermarkConfig = Field(..., description="重复文字水印配置。")
|
||
|
||
|
||
class HomeMaterialTextWatermarkPreviewResponse(BaseModel):
|
||
width: int = Field(..., description="预览层宽度。")
|
||
height: int = Field(..., description="预览层高度。")
|
||
preview_layer_data_url: str = Field(..., description="透明 PNG 预览层 data URL。")
|
||
|
||
|
||
class HomeMaterialAssetOut(BaseModel):
|
||
id: str = Field(..., description="素材ID。")
|
||
category_id: str = Field(..., description="行业ID。")
|
||
category_name: str | None = Field(None, description="行业名称,列表接口批量查询回填。")
|
||
category_key: str | None = Field(None, description="行业key,列表接口批量查询回填。")
|
||
title: str | None = Field(None, description="素材标题。")
|
||
media_type: HomeMaterialMediaType = Field(..., description="素材类型:image=图片,video=视频。")
|
||
status: HomeMaterialAssetStatus = Field(..., description="处理状态:draft草稿,processing处理中,success成功,failed失败。")
|
||
original_url: str | None = Field(None, description="原始素材URL,仅后台返回;前台不返回。")
|
||
watermarked_url: str | None = Field(None, description="水印素材URL。")
|
||
cover_url: str | None = Field(None, description="视频封面URL。")
|
||
watermark_id: str | None = Field(None, description="图片水印ID。重复文字水印素材为空。")
|
||
watermark_name: str | None = Field(None, description="图片水印名称,列表接口批量查询回填。重复文字水印显示为空。")
|
||
watermark_config: HomeMaterialWatermarkConfig | dict[str, Any] | None = Field(None, description="水印配置快照。")
|
||
width: int | None = Field(None, description="素材宽度。")
|
||
height: int | None = Field(None, description="素材高度。")
|
||
duration_seconds: Decimal | float | None = Field(None, description="视频时长,图片为空。")
|
||
file_size_bytes: int = Field(0, description="原始文件大小。")
|
||
watermarked_file_size_bytes: int | None = Field(None, description="水印后文件大小。")
|
||
is_active: bool = Field(True, description="是否前台展示。")
|
||
sort_order: int = Field(0, description="排序值。")
|
||
error_message: str | None = Field(None, description="失败原因。")
|
||
processed_at: NaiveDatetimeOptional = Field(None, description="处理完成时间。")
|
||
created_at: NaiveDatetimeOptional = None
|
||
updated_at: NaiveDatetimeOptional = None
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
|
||
class HomeMaterialAssetListOut(BaseModel):
|
||
items: list[HomeMaterialAssetOut] = Field(default_factory=list, description="素材列表。")
|
||
total: int = Field(0, description="符合条件的素材总数。")
|
||
|
||
|
||
class HomeMaterialAssetUpdate(BaseModel):
|
||
category_id: str = Field(..., description="行业ID。")
|
||
title: str | None = Field(None, max_length=128, description="素材标题。")
|
||
is_active: bool = Field(True, description="是否前台展示。")
|
||
sort_order: int = Field(0, ge=0, le=999999, description="排序值。")
|
||
|
||
@field_validator("title")
|
||
@classmethod
|
||
def clean_title(cls, value: str | None) -> str | None:
|
||
value = (value or "").strip()
|
||
return value or None
|
||
|
||
|
||
class HomeMaterialRegenerateWatermarkRequest(HomeMaterialWatermarkConfig):
|
||
wait: bool = Field(False, description="是否等待水印处理完成。默认 false,前端轮询状态。")
|
||
wait_timeout_seconds: int = Field(30, ge=1, le=60, description="wait=true 时最长等待秒数,最大 60 秒。")
|
||
|
||
|
||
class HomeMaterialAssetStatusOut(BaseModel):
|
||
id: str = Field(..., description="素材ID。")
|
||
status: HomeMaterialAssetStatus = Field(..., description="处理状态。")
|
||
error_message: str | None = Field(None, description="失败原因。")
|
||
original_url: str | None = Field(None, description="原始素材URL,仅后台返回。")
|
||
watermarked_url: str | None = Field(None, description="水印素材URL。")
|
||
cover_url: str | None = Field(None, description="视频封面URL。")
|
||
processed_at: NaiveDatetimeOptional = Field(None, description="处理完成时间。")
|
||
|
||
|
||
class HomeMaterialPublicCategoryOut(BaseModel):
|
||
id: str = Field(..., description="行业ID。")
|
||
name: str = Field(..., description="行业名称。")
|
||
key: str = Field(..., description="行业key。")
|
||
icon: str | None = Field(None, description="前端图标名。")
|
||
description: str | None = Field(None, description="行业描述。")
|
||
sort_order: int = Field(0, description="排序。")
|
||
asset_count: int = Field(0, description="可展示素材总数。")
|
||
image_count: int = Field(0, description="图片素材数量。")
|
||
video_count: int = Field(0, description="视频素材数量。")
|
||
|
||
|
||
class HomeMaterialPublicCategoryListOut(BaseModel):
|
||
enabled: bool = Field(False, description="首页素材展示开关。false 时 items 为空。")
|
||
title: str = Field("行业素材案例", description="首页素材模块标题。")
|
||
subtitle: str = Field("精选图片与视频素材展示", description="首页素材模块副标题。")
|
||
items: list[HomeMaterialPublicCategoryOut] = Field(default_factory=list, description="行业列表。")
|
||
|
||
|
||
class HomeMaterialPublicAssetOut(BaseModel):
|
||
id: str = Field(..., description="素材ID。")
|
||
title: str | None = Field(None, description="素材标题。")
|
||
media_type: HomeMaterialMediaType = Field(..., description="素材类型。")
|
||
url: str = Field(..., description="前台展示URL,永远返回 watermarked_url。")
|
||
cover_url: str | None = Field(None, description="视频封面URL。")
|
||
width: int | None = Field(None, description="宽度。")
|
||
height: int | None = Field(None, description="高度。")
|
||
duration_seconds: Decimal | float | None = Field(None, description="视频时长。")
|
||
sort_order: int = Field(0, description="排序。")
|
||
|
||
|
||
class HomeMaterialPublicCategoryGroupOut(BaseModel):
|
||
category: HomeMaterialPublicCategoryOut
|
||
assets: list[HomeMaterialPublicAssetOut] = Field(default_factory=list)
|
||
|
||
|
||
class HomeMaterialPublicGroupedOut(BaseModel):
|
||
enabled: bool = Field(True, description="首页素材展示开关。")
|
||
title: str = Field("行业素材案例", description="首页素材模块标题。")
|
||
subtitle: str = Field("精选图片与视频素材展示", description="首页素材模块副标题。")
|
||
response_mode: HomeMaterialPublicResponseMode = Field(HomeMaterialPublicResponseMode.GROUPED, description="返回模式。")
|
||
categories: list[HomeMaterialPublicCategoryGroupOut] = Field(default_factory=list, description="行业分组素材。")
|
||
|
||
|
||
class HomeMaterialPublicFlatItemOut(HomeMaterialPublicAssetOut):
|
||
category_id: str = Field(..., description="行业ID。")
|
||
category_name: str = Field(..., description="行业名称。")
|
||
category_key: str = Field(..., description="行业key。")
|
||
category_icon: str | None = Field(None, description="行业图标。")
|
||
|
||
|
||
class HomeMaterialPublicFlatOut(BaseModel):
|
||
enabled: bool = Field(True, description="首页素材展示开关。")
|
||
title: str = Field("行业素材案例", description="首页素材模块标题。")
|
||
subtitle: str = Field("精选图片与视频素材展示", description="首页素材模块副标题。")
|
||
response_mode: HomeMaterialPublicResponseMode = Field(HomeMaterialPublicResponseMode.FLAT, description="返回模式。")
|
||
items: list[HomeMaterialPublicFlatItemOut] = Field(default_factory=list, description="平铺素材列表。")
|
||
total: int = Field(0, description="总数。")
|
||
|
||
|
||
class HomeMaterialUploadResultOut(BaseModel):
|
||
id: str = Field(..., description="素材ID。")
|
||
category_id: str = Field(..., description="行业ID。")
|
||
title: str | None = Field(None, description="素材标题。")
|
||
media_type: HomeMaterialMediaType = Field(..., description="素材类型。")
|
||
status: HomeMaterialAssetStatus = Field(..., description="处理状态。")
|
||
original_url: str | None = Field(None, description="原始素材URL。")
|
||
watermarked_url: str | None = Field(None, description="水印素材URL。")
|
||
cover_url: str | None = Field(None, description="视频封面URL。")
|
||
watermark_config: HomeMaterialWatermarkConfig | dict[str, Any] | None = Field(None, description="水印配置快照。")
|
||
message: str = Field("", description="提示消息。")
|