- 新增数据库迁移脚本,为详情表添加四个价格相关字段 - 完善后端schema校验、序列化逻辑,增加价格相关的数据清理与验证规则 - 新增价格配置校验逻辑,设置起售价时必须选择对应的计价单位 - 在Admin UI详情编辑器中新增价格配置模块,支持配置起售价、计价单位、适用人数范围和价格说明 - 更新小程序端详情页面,支持展示配置的参考价格信息 - 补充相关测试用例,更新API文档与类型定义
764 lines
28 KiB
Python
764 lines
28 KiB
Python
from datetime import date, datetime, timedelta
|
|
from typing import Literal
|
|
from urllib.parse import urlsplit
|
|
from pydantic import BaseModel, EmailStr, Field, ValidationInfo, field_validator, model_validator
|
|
|
|
from .menu_rules import normalize_menu_values
|
|
|
|
|
|
LeadStatus = Literal["new", "assigned", "contacted", "planning", "won", "invalid"]
|
|
LeadType = Literal["general", "vehicle"]
|
|
VehicleServiceType = Literal["charter", "transfer"]
|
|
CharterDuration = Literal["halfDay", "fullDay"]
|
|
BrowseHistoryType = Literal["wanfa-route", "team-building", "wild-archive"]
|
|
PriceUnit = Literal["person", "day", "group"]
|
|
|
|
|
|
class LoginIn(BaseModel):
|
|
email: EmailStr
|
|
password: str = Field(min_length=6)
|
|
captchaId: str = Field(min_length=1, max_length=64)
|
|
captchaCode: str = Field(min_length=4, max_length=8)
|
|
rememberMe: bool = False
|
|
|
|
@field_validator("captchaId")
|
|
@classmethod
|
|
def normalize_captcha_id(cls, value: str) -> str:
|
|
normalized = value.strip()
|
|
if not normalized:
|
|
raise ValueError("验证码不能为空")
|
|
return normalized
|
|
|
|
@field_validator("captchaCode")
|
|
@classmethod
|
|
def normalize_captcha_code(cls, value: str) -> str:
|
|
normalized = value.strip().upper()
|
|
if not normalized:
|
|
raise ValueError("验证码不能为空")
|
|
return normalized
|
|
|
|
|
|
class PhoneLoginIn(BaseModel):
|
|
code: str = Field(min_length=1, max_length=256)
|
|
|
|
|
|
class VehicleDemandIn(BaseModel):
|
|
serviceType: VehicleServiceType
|
|
charterDuration: CharterDuration | None = None
|
|
travelDate: date
|
|
pickupTime: str | None = Field(default=None, max_length=32)
|
|
pickupLocation: str = Field(min_length=1, max_length=200)
|
|
dropoffLocation: str = Field(min_length=1, max_length=200)
|
|
peopleCount: int = Field(gt=0, le=1000)
|
|
luggageCount: int | None = Field(default=None, ge=0, le=1000)
|
|
vehicleOptionId: str | None = Field(default=None, max_length=128)
|
|
vehicleOptionTitle: str | None = Field(default=None, max_length=128)
|
|
specialRequirements: str | None = Field(default=None, max_length=1000)
|
|
|
|
@field_validator("pickupTime", "vehicleOptionId", "vehicleOptionTitle", "specialRequirements", mode="before")
|
|
@classmethod
|
|
def normalize_optional_text(cls, value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
normalized = " ".join(value.strip().split())
|
|
return normalized or None
|
|
|
|
@field_validator("pickupLocation", "dropoffLocation")
|
|
@classmethod
|
|
def normalize_location(cls, value: str) -> str:
|
|
normalized = " ".join(value.strip().split())
|
|
if not normalized:
|
|
raise ValueError("地点不能为空")
|
|
return normalized
|
|
|
|
@model_validator(mode="after")
|
|
def validate_charter_duration(self):
|
|
if self.serviceType == "charter" and not self.charterDuration:
|
|
raise ValueError("包车服务必须选择半天或全天")
|
|
if self.serviceType == "transfer":
|
|
self.charterDuration = None
|
|
return self
|
|
|
|
|
|
class LeadCreateIn(BaseModel):
|
|
leadType: LeadType = "general"
|
|
contactName: str | None = Field(default=None, max_length=80)
|
|
destination: str | None = None
|
|
phone: str = Field(min_length=2, max_length=64)
|
|
travelDate: datetime | None = None
|
|
peopleCount: int | None = Field(default=None, gt=0)
|
|
budgetMin: int | None = Field(default=None, ge=0)
|
|
budgetMax: int | None = Field(default=None, ge=0)
|
|
note: str | None = Field(default=None, max_length=1000)
|
|
sourcePage: str | None = None
|
|
vehicleDemand: VehicleDemandIn | None = None
|
|
|
|
@field_validator("contactName", "destination", "sourcePage", mode="before")
|
|
@classmethod
|
|
def normalize_optional_text(cls, value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
normalized = " ".join(value.strip().split())
|
|
return normalized or None
|
|
|
|
@field_validator("phone")
|
|
@classmethod
|
|
def normalize_phone(cls, value: str) -> str:
|
|
normalized = " ".join(value.strip().split())
|
|
if not normalized:
|
|
raise ValueError("联系方式不能为空")
|
|
return normalized
|
|
|
|
@field_validator("travelDate", mode="before")
|
|
@classmethod
|
|
def parse_date_only_travel_date(cls, value):
|
|
if isinstance(value, str) and len(value) == 10:
|
|
try:
|
|
return datetime.strptime(value, "%Y-%m-%d")
|
|
except ValueError:
|
|
return value
|
|
return value
|
|
|
|
@model_validator(mode="after")
|
|
def validate_vehicle_demand(self):
|
|
if self.leadType == "vehicle" and self.vehicleDemand is None:
|
|
raise ValueError("用车需求不能为空")
|
|
if self.leadType == "general":
|
|
self.vehicleDemand = None
|
|
return self
|
|
|
|
|
|
class BrowseHistoryCreateIn(BaseModel):
|
|
itemType: BrowseHistoryType
|
|
itemId: str = Field(min_length=1, max_length=128)
|
|
|
|
@field_validator("itemId")
|
|
@classmethod
|
|
def normalize_item_id(cls, value: str) -> str:
|
|
normalized = value.strip()
|
|
if not normalized:
|
|
raise ValueError("内容 ID 不能为空")
|
|
return normalized
|
|
|
|
|
|
class LeadStatusIn(BaseModel):
|
|
status: LeadStatus
|
|
|
|
|
|
class LeadQuery(BaseModel):
|
|
leadType: LeadType | None = None
|
|
status: LeadStatus | None = None
|
|
sourcePage: str | None = None
|
|
keyword: str | None = None
|
|
createdFrom: datetime | None = None
|
|
createdTo: datetime | None = None
|
|
take: int = Field(default=100, ge=1, le=200)
|
|
|
|
@field_validator("createdFrom", "createdTo", mode="before")
|
|
@classmethod
|
|
def parse_date_only_range(cls, value, info: ValidationInfo):
|
|
if isinstance(value, str) and len(value) == 10:
|
|
try:
|
|
parsed = datetime.strptime(value, "%Y-%m-%d")
|
|
return parsed + timedelta(days=1) if info.field_name == "createdTo" else parsed
|
|
except ValueError:
|
|
return value
|
|
return value
|
|
|
|
class SiteConfigPatchIn(BaseModel):
|
|
title: str | None = None
|
|
kicker: str | None = None
|
|
subtitle: str | None = None
|
|
image: str | None = None
|
|
description: str | None = None
|
|
steps: list[str] | None = None
|
|
destinationLabel: str | None = None
|
|
destinationPlaceholder: str | None = None
|
|
phoneLabel: str | None = None
|
|
phonePlaceholder: str | None = None
|
|
noteLabel: str | None = None
|
|
notePlaceholder: str | None = None
|
|
submitLabel: str | None = None
|
|
chips: list[str] | None = None
|
|
introTitle: str | None = None
|
|
intro: str | None = None
|
|
serviceSections: list[dict[str, str]] | None = None
|
|
advantages: list[str] | None = None
|
|
processSteps: list[dict[str, str]] | None = None
|
|
targetType: str | None = None
|
|
targetValue: str | None = None
|
|
isActive: bool | None = None
|
|
sortOrder: int | None = None
|
|
|
|
|
|
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]
|
|
|
|
|
|
def normalize_detail_list(value: list[str]) -> list[str]:
|
|
return [normalize_required_text(item) for item in value if item.strip()]
|
|
|
|
|
|
def normalize_detail_gallery(value: list[str]) -> list[str]:
|
|
normalized = [normalize_required_url(item) for item in value if item.strip()]
|
|
return normalized
|
|
|
|
|
|
def normalize_optional_price_text(value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
normalized = normalize_optional_text(value)
|
|
return normalized or None
|
|
|
|
|
|
class DetailCreate(BaseModel):
|
|
key: str = Field(min_length=1, max_length=120)
|
|
eyebrow: str = Field(min_length=1, max_length=80)
|
|
duration: str = Field(min_length=1, max_length=80)
|
|
title: str = Field(min_length=1, max_length=160)
|
|
subtitle: str = Field(min_length=1, max_length=240)
|
|
intro: str = Field(min_length=1, max_length=4000)
|
|
highlights: list[str] = Field(default_factory=list)
|
|
included: list[str] = Field(default_factory=list)
|
|
excluded: list[str] = Field(default_factory=list)
|
|
notes: list[str] = Field(default_factory=list)
|
|
gallery: list[str] = Field(default_factory=list)
|
|
priceStartingValue: float | None = Field(default=None, ge=0)
|
|
priceUnit: PriceUnit | None = None
|
|
pricePeopleRange: str | None = Field(default=None, max_length=80)
|
|
priceDescription: str | None = Field(default=None, max_length=500)
|
|
conciergeAdvisorId: str | None = Field(default=None, max_length=120)
|
|
isActive: bool = True
|
|
sortOrder: int | None = Field(default=None, ge=0)
|
|
|
|
@field_validator("key", "eyebrow", "duration", "title", "subtitle", "intro")
|
|
@classmethod
|
|
def normalize_detail_text(cls, value: str) -> str:
|
|
return normalize_required_text(value)
|
|
|
|
@field_validator("highlights", "included", "excluded", "notes")
|
|
@classmethod
|
|
def normalize_detail_lists(cls, value: list[str]) -> list[str]:
|
|
return normalize_detail_list(value)
|
|
|
|
@field_validator("gallery")
|
|
@classmethod
|
|
def validate_detail_gallery(cls, value: list[str]) -> list[str]:
|
|
return normalize_detail_gallery(value)
|
|
|
|
@field_validator("pricePeopleRange", "priceDescription", mode="before")
|
|
@classmethod
|
|
def normalize_price_text(cls, value: str | None) -> str | None:
|
|
return normalize_optional_price_text(value)
|
|
|
|
@field_validator("conciergeAdvisorId", mode="before")
|
|
@classmethod
|
|
def normalize_concierge_advisor_id(cls, value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
normalized = str(value).strip()
|
|
return normalized or None
|
|
|
|
|
|
class DetailPatch(BaseModel):
|
|
key: str | None = Field(default=None, min_length=1, max_length=120)
|
|
eyebrow: str | None = Field(default=None, min_length=1, max_length=80)
|
|
duration: str | None = Field(default=None, min_length=1, max_length=80)
|
|
title: str | None = Field(default=None, min_length=1, max_length=160)
|
|
subtitle: str | None = Field(default=None, min_length=1, max_length=240)
|
|
intro: str | None = Field(default=None, min_length=1, max_length=4000)
|
|
highlights: list[str] | None = None
|
|
included: list[str] | None = None
|
|
excluded: list[str] | None = None
|
|
notes: list[str] | None = None
|
|
gallery: list[str] | None = None
|
|
priceStartingValue: float | None = Field(default=None, ge=0)
|
|
priceUnit: PriceUnit | None = None
|
|
pricePeopleRange: str | None = Field(default=None, max_length=80)
|
|
priceDescription: str | None = Field(default=None, max_length=500)
|
|
conciergeAdvisorId: str | None = Field(default=None, max_length=120)
|
|
isActive: bool | None = None
|
|
sortOrder: int | None = Field(default=None, ge=0)
|
|
|
|
@field_validator("key", "eyebrow", "duration", "title", "subtitle", "intro", mode="before")
|
|
@classmethod
|
|
def normalize_optional_detail_text(cls, value: str | None) -> str | None:
|
|
if value is None:
|
|
raise ValueError("字段不能为空")
|
|
return normalize_required_text(value)
|
|
|
|
@field_validator("highlights", "included", "excluded", "notes", mode="before")
|
|
@classmethod
|
|
def normalize_optional_detail_lists(cls, value: list[str] | None) -> list[str] | None:
|
|
if value is None:
|
|
raise ValueError("字段不能为空")
|
|
return normalize_detail_list(value)
|
|
|
|
@field_validator("gallery", mode="before")
|
|
@classmethod
|
|
def validate_optional_detail_gallery(cls, value: list[str] | None) -> list[str] | None:
|
|
if value is None:
|
|
raise ValueError("字段不能为空")
|
|
return normalize_detail_gallery(value)
|
|
|
|
@field_validator("pricePeopleRange", "priceDescription", mode="before")
|
|
@classmethod
|
|
def normalize_optional_price_text_fields(cls, value: str | None) -> str | None:
|
|
return normalize_optional_price_text(value)
|
|
|
|
@field_validator("conciergeAdvisorId", mode="before")
|
|
@classmethod
|
|
def normalize_optional_concierge_advisor_id(cls, value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
normalized = str(value).strip()
|
|
return normalized or None
|
|
|
|
|
|
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]
|
|
|
|
|
|
DataScope = Literal["all", "dept", "dept_and_children", "custom_dept", "self"]
|
|
MenuType = Literal["directory", "page", "button"]
|
|
|
|
|
|
class AdminRoleCreate(BaseModel):
|
|
name: str = Field(min_length=1, max_length=80)
|
|
code: str = Field(min_length=1, max_length=80, pattern=r"^[a-z][a-z0-9_:-]*$")
|
|
dataScope: DataScope = "self"
|
|
description: str | None = Field(default=None, max_length=500)
|
|
isActive: bool = True
|
|
menuIds: list[str] = Field(default_factory=list, max_length=500)
|
|
deptIds: list[str] = Field(default_factory=list, max_length=500)
|
|
|
|
|
|
class AdminRolePatch(BaseModel):
|
|
name: str | None = Field(default=None, min_length=1, max_length=80)
|
|
code: str | None = Field(default=None, min_length=1, max_length=80, pattern=r"^[a-z][a-z0-9_:-]*$")
|
|
dataScope: DataScope | None = None
|
|
description: str | None = Field(default=None, max_length=500)
|
|
isActive: bool | None = None
|
|
menuIds: list[str] | None = Field(default=None, max_length=500)
|
|
deptIds: list[str] | None = Field(default=None, max_length=500)
|
|
|
|
|
|
class AdminMenuCreate(BaseModel):
|
|
parentId: str | None = None
|
|
name: str = Field(min_length=1, max_length=80)
|
|
type: MenuType = "page"
|
|
path: str | None = Field(default=None, max_length=200)
|
|
componentKey: str | None = Field(default=None, max_length=120)
|
|
permissionCode: str | None = Field(default=None, max_length=120)
|
|
icon: str | None = Field(default=None, max_length=80)
|
|
sortOrder: int = Field(default=0, ge=0)
|
|
isVisible: bool = True
|
|
isActive: bool = True
|
|
|
|
@model_validator(mode="after")
|
|
def validate_menu_fields(self):
|
|
normalized = normalize_menu_values(self.model_dump())
|
|
for field, value in normalized.items():
|
|
setattr(self, field, value)
|
|
return self
|
|
|
|
|
|
class AdminMenuPatch(BaseModel):
|
|
parentId: str | None = None
|
|
name: str | None = Field(default=None, min_length=1, max_length=80)
|
|
type: MenuType | None = None
|
|
path: str | None = Field(default=None, max_length=200)
|
|
componentKey: str | None = Field(default=None, max_length=120)
|
|
permissionCode: str | None = Field(default=None, max_length=120)
|
|
icon: str | None = Field(default=None, max_length=80)
|
|
sortOrder: int | None = Field(default=None, ge=0)
|
|
isVisible: bool | None = None
|
|
isActive: bool | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def normalize_menu_fields(self):
|
|
normalized = normalize_menu_values(self.model_dump(exclude_unset=True), partial=True)
|
|
for field, value in normalized.items():
|
|
setattr(self, field, value)
|
|
return self
|
|
|
|
|
|
class AdminDepartmentCreate(BaseModel):
|
|
parentId: str | None = None
|
|
name: str = Field(min_length=1, max_length=120)
|
|
sortOrder: int = Field(default=0, ge=0)
|
|
isActive: bool = True
|
|
|
|
|
|
class AdminDepartmentPatch(BaseModel):
|
|
parentId: str | None = None
|
|
name: str | None = Field(default=None, min_length=1, max_length=120)
|
|
sortOrder: int | None = Field(default=None, ge=0)
|
|
isActive: bool | None = None
|
|
|
|
|
|
class AdminUserCreate(BaseModel):
|
|
email: EmailStr
|
|
name: str = Field(min_length=1, max_length=80)
|
|
password: str = Field(min_length=8, max_length=128)
|
|
isActive: bool = True
|
|
roleIds: list[str] = Field(default_factory=list, max_length=100)
|
|
deptIds: list[str] = Field(default_factory=list, max_length=100)
|
|
|
|
|
|
class AdminUserPatch(BaseModel):
|
|
name: str | None = Field(default=None, min_length=1, max_length=80)
|
|
password: str | None = Field(default=None, min_length=8, max_length=128)
|
|
isActive: bool | None = None
|
|
roleIds: list[str] | None = Field(default=None, max_length=100)
|
|
deptIds: list[str] | None = Field(default=None, max_length=100)
|