feat: 新增用车需求服务及线索管理全功能
- 新增后台用车服务配置模块,支持维护服务简介、优势与使用流程 - 新增需求线索管理页面,支持筛选、查看与更新用车线索状态 - 新增小程序用车需求页面,优化登录路径与回跳逻辑 - 优化车型卡片跳转与用车需求提交功能 - 新增服务端API与数据处理逻辑,完善权限校验 - 更新全站配置与API文档,新增相关测试用例
This commit is contained in:
1 parent
b7a81964c9
commit
d42f69bd47
36 files changed
+1290
-33
No files matched your search
@@ -1,10 +1,13 @@
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Literal
|
||||
from urllib.parse import urlsplit
|
||||
from pydantic import BaseModel, EmailStr, Field, ValidationInfo, field_validator
|
||||
from pydantic import BaseModel, EmailStr, Field, ValidationInfo, field_validator, model_validator
|
||||
|
||||
|
||||
LeadStatus = Literal["new", "assigned", "contacted", "planning", "won", "invalid"]
|
||||
LeadType = Literal["general", "vehicle"]
|
||||
VehicleServiceType = Literal["charter", "transfer"]
|
||||
CharterDuration = Literal["halfDay", "fullDay"]
|
||||
|
||||
|
||||
class LoginIn(BaseModel):
|
||||
@@ -16,7 +19,47 @@ 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
|
||||
@@ -25,6 +68,15 @@ class LeadCreateIn(BaseModel):
|
||||
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
|
||||
@@ -44,12 +96,21 @@ class LeadCreateIn(BaseModel):
|
||||
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 LeadStatusIn(BaseModel):
|
||||
status: LeadStatus
|
||||
|
||||
|
||||
class LeadQuery(BaseModel):
|
||||
leadType: LeadType | None = None
|
||||
status: LeadStatus | None = None
|
||||
sourcePage: str | None = None
|
||||
keyword: str | None = None
|
||||
@@ -68,7 +129,6 @@ class LeadQuery(BaseModel):
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
class SiteConfigPatchIn(BaseModel):
|
||||
title: str | None = None
|
||||
kicker: str | None = None
|
||||
@@ -84,6 +144,11 @@ class SiteConfigPatchIn(BaseModel):
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user