Files
WonderQ-Project/WonderQ-Admin/app/schemas.py
duanshuwen feec6ab80f feat(wanfa): 新增路线详情参考价格配置及展示功能
- 新增数据库迁移脚本,为详情表添加四个价格相关字段
- 完善后端schema校验、序列化逻辑,增加价格相关的数据清理与验证规则
- 新增价格配置校验逻辑,设置起售价时必须选择对应的计价单位
- 在Admin UI详情编辑器中新增价格配置模块,支持配置起售价、计价单位、适用人数范围和价格说明
- 更新小程序端详情页面,支持展示配置的参考价格信息
- 补充相关测试用例,更新API文档与类型定义
2026-08-27 22:02:51 +08:00

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)