feat(wanfa): 新增路线详情参考价格配置及展示功能
- 新增数据库迁移脚本,为详情表添加四个价格相关字段 - 完善后端schema校验、序列化逻辑,增加价格相关的数据清理与验证规则 - 新增价格配置校验逻辑,设置起售价时必须选择对应的计价单位 - 在Admin UI详情编辑器中新增价格配置模块,支持配置起售价、计价单位、适用人数范围和价格说明 - 更新小程序端详情页面,支持展示配置的参考价格信息 - 补充相关测试用例,更新API文档与类型定义
This commit is contained in:
1 parent
f18971a430
commit
feec6ab80f
20 files changed
+351
-17
No files matched your search
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Index, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from .database import Base
|
||||
@@ -273,6 +273,10 @@ class DetailRecord(AdminOwnedContent, Base):
|
||||
excluded: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False)
|
||||
notes: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False)
|
||||
gallery: Mapped[list[str]] = mapped_column(JSONB, default=list, nullable=False)
|
||||
priceStartingValue: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
priceUnit: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
pricePeopleRange: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
priceDescription: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
conciergeAdvisorId: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
isActive: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True)
|
||||
sortOrder: Mapped[int] = mapped_column(Integer, default=0, nullable=False, index=True)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -80,6 +80,10 @@ def public_detail_dict(detail, concierge_advisor=None) -> dict:
|
||||
"excluded": detail.excluded or [],
|
||||
"notes": detail.notes or [],
|
||||
"gallery": [resolve_media_url(image) for image in (detail.gallery or [])],
|
||||
"priceStartingValue": detail.priceStartingValue,
|
||||
"priceUnit": detail.priceUnit,
|
||||
"pricePeopleRange": detail.pricePeopleRange,
|
||||
"priceDescription": detail.priceDescription,
|
||||
"conciergeAdvisor": public_concierge_advisor_dict(concierge_advisor) if concierge_advisor else None,
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user