feat: 新增客片案例与团队共创模块,重构玩法与配置
- 新增客片案例全流程功能,包含前台页面、管理端配置、后端API、数据库迁移与文档 - 新增团队共创详情页面与对应接口 - 重构玩法模块:将静态playData替换为动态API获取,拆分类型定义到playTypes.ts - 优化vite构建配置与环境变量处理,调整详情页操作栏文案 - 更新全量相关文档与配图,新增客片案例、团队共创API文档 - 新增测试用例,完善数据归一化逻辑 - 调整路由配置与环境变量示例文件
This commit is contained in:
1 parent
cda8069630
commit
7ba92f3c5d
76 files changed
+7970
-137
No files matched your search
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Literal
|
||||
from urllib.parse import urlsplit
|
||||
from pydantic import BaseModel, EmailStr, Field, ValidationInfo, field_validator
|
||||
|
||||
|
||||
@@ -91,3 +92,409 @@ class SiteConfigPatchIn(BaseModel):
|
||||
|
||||
class SiteConfigReorderIn(BaseModel):
|
||||
itemIds: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
def normalize_required_text(value: str) -> str:
|
||||
normalized = " ".join(value.strip().split())
|
||||
if not normalized:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalized
|
||||
|
||||
|
||||
def normalize_optional_text(value: str) -> str:
|
||||
return " ".join(value.strip().split())
|
||||
|
||||
|
||||
def normalize_required_url(value: str) -> str:
|
||||
normalized = normalize_required_text(value)
|
||||
parsed = urlsplit(normalized)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
||||
raise ValueError("图片地址必须是有效的 HTTP(S) URL")
|
||||
return normalized
|
||||
|
||||
|
||||
class WanfaCategoryCreate(BaseModel):
|
||||
label: str = Field(min_length=1, max_length=40)
|
||||
|
||||
@field_validator("label")
|
||||
@classmethod
|
||||
def normalize_label(cls, value: str) -> str:
|
||||
return normalize_required_text(value)
|
||||
|
||||
|
||||
class WanfaCategoryPatch(BaseModel):
|
||||
label: str | None = Field(default=None, min_length=1, max_length=40)
|
||||
|
||||
@field_validator("label", mode="before")
|
||||
@classmethod
|
||||
def normalize_label(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_required_text(value)
|
||||
|
||||
|
||||
class WanfaRouteCreate(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=80)
|
||||
subtitle: str = Field(min_length=1, max_length=160)
|
||||
image: str = Field(min_length=1, max_length=2048)
|
||||
routeCount: int = Field(ge=0)
|
||||
demandKeyword: str = Field(min_length=1, max_length=60)
|
||||
|
||||
@field_validator("title", "subtitle", "image", "demandKeyword")
|
||||
@classmethod
|
||||
def normalize_route_text(cls, value: str) -> str:
|
||||
return normalize_required_text(value)
|
||||
|
||||
|
||||
class WanfaRoutePatch(BaseModel):
|
||||
title: str | None = Field(default=None, min_length=1, max_length=80)
|
||||
subtitle: str | None = Field(default=None, min_length=1, max_length=160)
|
||||
image: str | None = Field(default=None, min_length=1, max_length=2048)
|
||||
routeCount: int | None = Field(default=None, ge=0)
|
||||
demandKeyword: str | None = Field(default=None, min_length=1, max_length=60)
|
||||
|
||||
@field_validator("title", "subtitle", "image", "demandKeyword", mode="before")
|
||||
@classmethod
|
||||
def normalize_route_text(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_required_text(value)
|
||||
|
||||
|
||||
class WanfaReorderIn(BaseModel):
|
||||
itemIds: list[str] = Field(default_factory=list)
|
||||
|
||||
@field_validator("itemIds")
|
||||
@classmethod
|
||||
def validate_item_ids(cls, value: list[str]) -> list[str]:
|
||||
if any(not item.strip() for item in value):
|
||||
raise ValueError("排序 ID 不能为空")
|
||||
return [item.strip() for item in value]
|
||||
|
||||
|
||||
class HomeExperienceCreate(BaseModel):
|
||||
badge: str = Field(min_length=1, max_length=40)
|
||||
category: str = Field(min_length=1, max_length=80)
|
||||
title: str = Field(min_length=1, max_length=120)
|
||||
englishTitle: str = Field(min_length=1, max_length=120)
|
||||
image: str = Field(min_length=1, max_length=2048)
|
||||
demandKeyword: str = Field(min_length=1, max_length=120)
|
||||
isActive: bool = True
|
||||
sortOrder: int | None = Field(default=None, ge=0)
|
||||
|
||||
@field_validator("badge", "category", "title", "englishTitle", "demandKeyword")
|
||||
@classmethod
|
||||
def normalize_experience_text(cls, value: str) -> str:
|
||||
return normalize_required_text(value)
|
||||
|
||||
@field_validator("image")
|
||||
@classmethod
|
||||
def normalize_experience_image(cls, value: str) -> str:
|
||||
return normalize_required_url(value)
|
||||
|
||||
|
||||
class HomeExperiencePatch(BaseModel):
|
||||
badge: str | None = Field(default=None, min_length=1, max_length=40)
|
||||
category: str | None = Field(default=None, min_length=1, max_length=80)
|
||||
title: str | None = Field(default=None, min_length=1, max_length=120)
|
||||
englishTitle: str | None = Field(default=None, min_length=1, max_length=120)
|
||||
image: str | None = Field(default=None, min_length=1, max_length=2048)
|
||||
demandKeyword: str | None = Field(default=None, min_length=1, max_length=120)
|
||||
isActive: bool | None = None
|
||||
sortOrder: int | None = Field(default=None, ge=0)
|
||||
|
||||
@field_validator("badge", "category", "title", "englishTitle", "demandKeyword", mode="before")
|
||||
@classmethod
|
||||
def normalize_experience_patch_text(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_required_text(value)
|
||||
|
||||
@field_validator("image", mode="before")
|
||||
@classmethod
|
||||
def normalize_experience_patch_image(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_required_url(value)
|
||||
|
||||
@field_validator("isActive", "sortOrder", mode="before")
|
||||
@classmethod
|
||||
def reject_experience_patch_null(cls, value):
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return value
|
||||
|
||||
|
||||
class HomeTeamBuildingCreate(BaseModel):
|
||||
tag: str = Field(min_length=1, max_length=40)
|
||||
title: str = Field(min_length=1, max_length=120)
|
||||
description: str = Field(min_length=1, max_length=240)
|
||||
image: str = Field(min_length=1, max_length=2048)
|
||||
demandKeyword: str = Field(min_length=1, max_length=120)
|
||||
detailSubtitle: str | None = Field(default=None, max_length=160)
|
||||
detailParagraphs: list[str] | None = Field(default=None, max_length=30)
|
||||
isActive: bool = True
|
||||
sortOrder: int | None = Field(default=None, ge=0)
|
||||
|
||||
@field_validator("tag", "title", "description", "demandKeyword")
|
||||
@classmethod
|
||||
def normalize_team_text(cls, value: str) -> str:
|
||||
return normalize_required_text(value)
|
||||
|
||||
@field_validator("image")
|
||||
@classmethod
|
||||
def normalize_team_image(cls, value: str) -> str:
|
||||
return normalize_required_url(value)
|
||||
|
||||
@field_validator("detailSubtitle", mode="before")
|
||||
@classmethod
|
||||
def normalize_team_detail_subtitle(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return normalize_optional_text(value)
|
||||
|
||||
@field_validator("detailParagraphs")
|
||||
@classmethod
|
||||
def normalize_team_detail_paragraphs(cls, value: list[str] | None) -> list[str] | None:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = [normalize_required_text(item) for item in value if item.strip()]
|
||||
return normalized or None
|
||||
|
||||
|
||||
class HomeTeamBuildingPatch(BaseModel):
|
||||
tag: str | None = Field(default=None, min_length=1, max_length=40)
|
||||
title: str | None = Field(default=None, min_length=1, max_length=120)
|
||||
description: str | None = Field(default=None, min_length=1, max_length=240)
|
||||
image: str | None = Field(default=None, min_length=1, max_length=2048)
|
||||
demandKeyword: str | None = Field(default=None, min_length=1, max_length=120)
|
||||
detailSubtitle: str | None = Field(default=None, max_length=160)
|
||||
detailParagraphs: list[str] | None = Field(default=None, max_length=30)
|
||||
isActive: bool | None = None
|
||||
sortOrder: int | None = Field(default=None, ge=0)
|
||||
|
||||
@field_validator("tag", "title", "description", "demandKeyword", mode="before")
|
||||
@classmethod
|
||||
def normalize_team_patch_text(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_required_text(value)
|
||||
|
||||
@field_validator("image", mode="before")
|
||||
@classmethod
|
||||
def normalize_team_patch_image(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_required_url(value)
|
||||
|
||||
@field_validator("detailSubtitle", mode="before")
|
||||
@classmethod
|
||||
def normalize_team_detail_patch_subtitle(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_optional_text(value)
|
||||
|
||||
@field_validator("detailParagraphs")
|
||||
@classmethod
|
||||
def normalize_team_detail_patch_paragraphs(cls, value: list[str] | None) -> list[str] | None:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
normalized = [normalize_required_text(item) for item in value if item.strip()]
|
||||
if not normalized:
|
||||
raise ValueError("详情正文至少需要一段")
|
||||
return normalized
|
||||
|
||||
@field_validator("isActive", "sortOrder", mode="before")
|
||||
@classmethod
|
||||
def reject_team_patch_null(cls, value):
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return value
|
||||
|
||||
|
||||
class HomeWildArchiveCreate(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=120)
|
||||
image: str = Field(min_length=1, max_length=2048)
|
||||
images: list[str] | None = Field(default=None, max_length=30)
|
||||
demandKeyword: str = Field(min_length=1, max_length=120)
|
||||
isActive: bool = True
|
||||
sortOrder: int | None = Field(default=None, ge=0)
|
||||
|
||||
@field_validator("title", "demandKeyword")
|
||||
@classmethod
|
||||
def normalize_archive_text(cls, value: str) -> str:
|
||||
return normalize_required_text(value)
|
||||
|
||||
@field_validator("image")
|
||||
@classmethod
|
||||
def normalize_archive_image(cls, value: str) -> str:
|
||||
return normalize_required_url(value)
|
||||
|
||||
@field_validator("images")
|
||||
@classmethod
|
||||
def normalize_archive_images(cls, value: list[str] | None) -> list[str] | None:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = [normalize_required_url(item) for item in value]
|
||||
if not normalized:
|
||||
raise ValueError("案例详情至少需要一张图片")
|
||||
return normalized
|
||||
|
||||
|
||||
class HomeWildArchivePatch(BaseModel):
|
||||
title: str | None = Field(default=None, min_length=1, max_length=120)
|
||||
image: str | None = Field(default=None, min_length=1, max_length=2048)
|
||||
images: list[str] | None = Field(default=None, max_length=30)
|
||||
demandKeyword: str | None = Field(default=None, min_length=1, max_length=120)
|
||||
isActive: bool | None = None
|
||||
sortOrder: int | None = Field(default=None, ge=0)
|
||||
|
||||
@field_validator("title", "demandKeyword", mode="before")
|
||||
@classmethod
|
||||
def normalize_archive_patch_text(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_required_text(value)
|
||||
|
||||
@field_validator("image", mode="before")
|
||||
@classmethod
|
||||
def normalize_archive_patch_image(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_required_url(value)
|
||||
|
||||
@field_validator("images")
|
||||
@classmethod
|
||||
def normalize_archive_patch_images(cls, value: list[str] | None) -> list[str] | None:
|
||||
if value is None:
|
||||
raise ValueError("案例详情至少需要一张图片")
|
||||
normalized = [normalize_required_url(item) for item in value]
|
||||
if not normalized:
|
||||
raise ValueError("案例详情至少需要一张图片")
|
||||
return normalized
|
||||
|
||||
@field_validator("isActive", "sortOrder", mode="before")
|
||||
@classmethod
|
||||
def reject_archive_patch_null(cls, value):
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return value
|
||||
|
||||
|
||||
class HomeWanfaRecommendationCreate(BaseModel):
|
||||
categoryId: str = Field(min_length=1, max_length=120)
|
||||
isActive: bool = True
|
||||
sortOrder: int | None = Field(default=None, ge=0)
|
||||
|
||||
@field_validator("categoryId")
|
||||
@classmethod
|
||||
def normalize_recommendation_category_id(cls, value: str) -> str:
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise ValueError("玩法分类不能为空")
|
||||
return value
|
||||
|
||||
|
||||
class HomeWanfaRecommendationPatch(BaseModel):
|
||||
categoryId: str | None = Field(default=None, min_length=1, max_length=120)
|
||||
isActive: bool | None = None
|
||||
sortOrder: int | None = Field(default=None, ge=0)
|
||||
|
||||
@field_validator("categoryId", mode="before")
|
||||
@classmethod
|
||||
def normalize_recommendation_category_patch_id(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("玩法分类不能为空")
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise ValueError("玩法分类不能为空")
|
||||
return value
|
||||
|
||||
@field_validator("isActive", "sortOrder", mode="before")
|
||||
@classmethod
|
||||
def reject_recommendation_patch_null(cls, value):
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return value
|
||||
|
||||
|
||||
class HomeReorderIn(BaseModel):
|
||||
itemIds: list[str] = Field(default_factory=list)
|
||||
|
||||
@field_validator("itemIds")
|
||||
@classmethod
|
||||
def validate_home_item_ids(cls, value: list[str]) -> list[str]:
|
||||
if any(not item.strip() for item in value):
|
||||
raise ValueError("排序 ID 不能为空")
|
||||
return [item.strip() for item in value]
|
||||
|
||||
|
||||
class ConciergeDetailIn(BaseModel):
|
||||
icon: str = Field(min_length=1, max_length=40)
|
||||
label: str = Field(min_length=1, max_length=120)
|
||||
|
||||
@field_validator("icon", "label")
|
||||
@classmethod
|
||||
def normalize_detail_text(cls, value: str) -> str:
|
||||
return normalize_required_text(value)
|
||||
|
||||
|
||||
class ConciergeAdvisorCreate(BaseModel):
|
||||
avatar: str = Field(min_length=1, max_length=2048)
|
||||
name: str = Field(min_length=1, max_length=80)
|
||||
role: str = Field(min_length=1, max_length=120)
|
||||
details: list[dict[str, str]] = Field(default_factory=list, max_length=20)
|
||||
qrImage: str = Field(min_length=1, max_length=2048)
|
||||
isActive: bool = True
|
||||
sortOrder: int | None = Field(default=None, ge=0)
|
||||
|
||||
@field_validator("name", "role")
|
||||
@classmethod
|
||||
def normalize_advisor_text(cls, value: str) -> str:
|
||||
return normalize_required_text(value)
|
||||
|
||||
@field_validator("avatar", "qrImage")
|
||||
@classmethod
|
||||
def normalize_advisor_image_url(cls, value: str) -> str:
|
||||
return normalize_required_url(value)
|
||||
|
||||
@field_validator("details")
|
||||
@classmethod
|
||||
def normalize_details(cls, value: list[dict[str, str]]) -> list[dict[str, str]]:
|
||||
return [ConciergeDetailIn.model_validate(detail).model_dump() for detail in value]
|
||||
|
||||
|
||||
class ConciergeAdvisorPatch(BaseModel):
|
||||
avatar: str | None = Field(default=None, min_length=1, max_length=2048)
|
||||
name: str | None = Field(default=None, min_length=1, max_length=80)
|
||||
role: str | None = Field(default=None, min_length=1, max_length=120)
|
||||
details: list[dict[str, str]] | None = Field(default=None, max_length=20)
|
||||
qrImage: str | None = Field(default=None, min_length=1, max_length=2048)
|
||||
isActive: bool | None = None
|
||||
sortOrder: int | None = Field(default=None, ge=0)
|
||||
|
||||
@field_validator("name", "role", mode="before")
|
||||
@classmethod
|
||||
def normalize_patch_text(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_required_text(value)
|
||||
|
||||
@field_validator("avatar", "qrImage", mode="before")
|
||||
@classmethod
|
||||
def normalize_patch_image_url(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return normalize_required_url(value)
|
||||
|
||||
@field_validator("details", "sortOrder", "isActive", mode="before")
|
||||
@classmethod
|
||||
def reject_null_patch_values(cls, value):
|
||||
if value is None:
|
||||
raise ValueError("字段不能为空")
|
||||
return value
|
||||
|
||||
@field_validator("details")
|
||||
@classmethod
|
||||
def normalize_patch_details(cls, value: list[dict[str, str]]) -> list[dict[str, str]]:
|
||||
return [ConciergeDetailIn.model_validate(detail).model_dump() for detail in value]
|
||||
Reference in new issue
Block a user