feat(wanfa): 新增路线详情参考价格配置及展示功能

- 新增数据库迁移脚本,为详情表添加四个价格相关字段
- 完善后端schema校验、序列化逻辑,增加价格相关的数据清理与验证规则
- 新增价格配置校验逻辑,设置起售价时必须选择对应的计价单位
- 在Admin UI详情编辑器中新增价格配置模块,支持配置起售价、计价单位、适用人数范围和价格说明
- 更新小程序端详情页面,支持展示配置的参考价格信息
- 补充相关测试用例,更新API文档与类型定义
This commit is contained in:
duanshuwen committed 2026-08-27 22:02:51 +08:00
1 parent f18971a430
commit feec6ab80f
20 files changed
+351 -17

No files matched your search

+26
View File
@@ -11,6 +11,7 @@ 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):
@@ -281,6 +282,13 @@ def normalize_detail_gallery(value: list[str]) -> list[str]:
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)
@@ -293,6 +301,10 @@ class DetailCreate(BaseModel):
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)
@@ -312,6 +324,11 @@ class DetailCreate(BaseModel):
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:
@@ -333,6 +350,10 @@ class DetailPatch(BaseModel):
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)
@@ -358,6 +379,11 @@ class DetailPatch(BaseModel):
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: