feat: 新增用车需求服务及线索管理全功能
- 新增后台用车服务配置模块,支持维护服务简介、优势与使用流程 - 新增需求线索管理页面,支持筛选、查看与更新用车线索状态 - 新增小程序用车需求页面,优化登录路径与回跳逻辑 - 优化车型卡片跳转与用车需求提交功能 - 新增服务端API与数据处理逻辑,完善权限校验 - 更新全站配置与API文档,新增相关测试用例
This commit is contained in:
1 parent
b7a81964c9
commit
d42f69bd47
36 files changed
+1290
-33
No files matched your search
@@ -32,6 +32,7 @@ from ..models import (
|
||||
MediaAsset,
|
||||
SiteVersion,
|
||||
VehicleOption,
|
||||
VehicleServiceConfig,
|
||||
WanfaCategory,
|
||||
WanfaRoute,
|
||||
utc_now,
|
||||
@@ -153,6 +154,17 @@ SITE_CONFIG_MODULES = {
|
||||
"empty_to_none": {"description", "image"},
|
||||
"create_defaults": {"description": None, "image": None},
|
||||
},
|
||||
"vehicleService": {
|
||||
"model": VehicleServiceConfig,
|
||||
"entity": "vehicle_service_config",
|
||||
"primary": "introTitle",
|
||||
"fields": {"introTitle", "intro", "serviceSections", "advantages", "processSteps", "isActive"},
|
||||
"none_to_empty": set(),
|
||||
"empty_to_none": set(),
|
||||
"create_defaults": {"intro": "", "serviceSections": [], "advantages": [], "processSteps": []},
|
||||
"ordered": False,
|
||||
"singleton": True,
|
||||
},
|
||||
}
|
||||
|
||||
def media_error(status_code: int, message: str, code: str, details: dict | None = None) -> None:
|
||||
@@ -318,8 +330,16 @@ def normalize_string_list(value) -> list[str]:
|
||||
|
||||
|
||||
def site_field_value(config: dict, field: str, value):
|
||||
if field in {"steps", "chips"}:
|
||||
if field in {"steps", "chips", "advantages"}:
|
||||
return normalize_string_list(value)
|
||||
if field in {"serviceSections", "processSteps"}:
|
||||
return [
|
||||
{key: item_value.strip() for key, item_value in item.model_dump().items() if isinstance(item_value, str)}
|
||||
if hasattr(item, "model_dump")
|
||||
else {key: item_value.strip() for key, item_value in item.items() if isinstance(item_value, str)}
|
||||
for item in (value or [])
|
||||
if isinstance(item, dict) or hasattr(item, "model_dump")
|
||||
]
|
||||
value = clean_site_value(value)
|
||||
if value == "" and field in config.get("empty_to_none", set()):
|
||||
return None
|
||||
@@ -1254,6 +1274,10 @@ def admin_site_config(_user: AdminUser = Depends(require_admin), db: Session = D
|
||||
model_dict(item)
|
||||
for item in db.scalars(select(VehicleOption).order_by(VehicleOption.sortOrder.asc())).all()
|
||||
],
|
||||
"vehicleService": [
|
||||
model_dict(item)
|
||||
for item in db.scalars(select(VehicleServiceConfig).order_by(VehicleServiceConfig.createdAt.asc())).all()
|
||||
],
|
||||
"demandHero": [
|
||||
model_dict(item)
|
||||
for item in db.scalars(select(DemandHero).order_by(DemandHero.sortOrder.asc())).all()
|
||||
@@ -1372,6 +1396,7 @@ def delete_site_config(
|
||||
|
||||
@router.get("/leads")
|
||||
def list_leads(
|
||||
leadType: str | None = None,
|
||||
status_value: LeadStatus | None = Query(default=None, alias="status"),
|
||||
sourcePage: str | None = None,
|
||||
keyword: str | None = None,
|
||||
@@ -1382,6 +1407,7 @@ def list_leads(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
query = LeadQuery(
|
||||
leadType=leadType,
|
||||
status=status_value,
|
||||
sourcePage=sourcePage,
|
||||
keyword=keyword,
|
||||
@@ -1397,6 +1423,8 @@ def list_leads(
|
||||
)
|
||||
if query.status:
|
||||
stmt = stmt.where(Lead.status == query.status)
|
||||
if query.leadType:
|
||||
stmt = stmt.where(Lead.leadType == query.leadType)
|
||||
if query.sourcePage:
|
||||
stmt = stmt.where(Lead.sourcePage == query.sourcePage.strip())
|
||||
if query.createdFrom:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from datetime import datetime, time
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
from .shared import site_config
|
||||
from ..auth import create_customer_token, require_customer
|
||||
from ..auth import create_customer_token, optional_customer, require_customer
|
||||
from ..api_response import success_response
|
||||
from ..database import get_db
|
||||
from ..models import ConciergeAdvisor, Customer, DetailRecord, HomeTeamBuilding, HomeWanfaRecommendation, HomeWildArchive, Lead, WanfaCategory
|
||||
@@ -170,8 +171,27 @@ def current_customer(customer: Customer = Depends(require_customer)):
|
||||
|
||||
|
||||
@router.post("/api/public/leads", status_code=status.HTTP_201_CREATED)
|
||||
def create_lead(body: LeadCreateIn, db: Session = Depends(get_db)):
|
||||
lead = Lead(**body.model_dump(exclude_none=True), status="new")
|
||||
def create_lead(
|
||||
body: LeadCreateIn,
|
||||
customer: Customer | None = Depends(optional_customer),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
if body.leadType == "vehicle" and customer is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用车需求提交前请先登录")
|
||||
|
||||
payload = body.model_dump(exclude_none=True, exclude={"vehicleDemand"})
|
||||
if body.leadType == "vehicle" and body.vehicleDemand:
|
||||
demand = body.vehicleDemand
|
||||
payload.update(
|
||||
{
|
||||
"customerId": customer.id if customer else None,
|
||||
"destination": demand.dropoffLocation,
|
||||
"travelDate": datetime.combine(demand.travelDate, time.min),
|
||||
"peopleCount": demand.peopleCount,
|
||||
"vehicleDemand": demand.model_dump(mode="json"),
|
||||
}
|
||||
)
|
||||
lead = Lead(**payload, status="new")
|
||||
db.add(lead)
|
||||
db.commit()
|
||||
db.refresh(lead)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models import DemandFeatureCard, DemandForm, DemandHero, DestinationHero, HeroSlide, VehicleOption
|
||||
from ..models import DemandFeatureCard, DemandForm, DemandHero, DestinationHero, HeroSlide, VehicleOption, VehicleServiceConfig
|
||||
from ..serializers import model_dict
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ def site_config(db: Session, active_only: bool, include_public_extras: bool = Tr
|
||||
demand_hero_stmt = select(DemandHero).order_by(DemandHero.sortOrder.asc())
|
||||
demand_feature_card_stmt = select(DemandFeatureCard).order_by(DemandFeatureCard.sortOrder.asc())
|
||||
demand_form_stmt = select(DemandForm).order_by(DemandForm.createdAt.asc())
|
||||
vehicle_service_stmt = select(VehicleServiceConfig).order_by(VehicleServiceConfig.createdAt.asc())
|
||||
if active_only:
|
||||
hero_stmt = hero_stmt.where(HeroSlide.isActive.is_(True))
|
||||
destination_hero_stmt = destination_hero_stmt.where(DestinationHero.isActive.is_(True))
|
||||
@@ -18,6 +19,7 @@ def site_config(db: Session, active_only: bool, include_public_extras: bool = Tr
|
||||
demand_hero_stmt = demand_hero_stmt.where(DemandHero.isActive.is_(True))
|
||||
demand_feature_card_stmt = demand_feature_card_stmt.where(DemandFeatureCard.isActive.is_(True))
|
||||
demand_form_stmt = demand_form_stmt.where(DemandForm.isActive.is_(True))
|
||||
vehicle_service_stmt = vehicle_service_stmt.where(VehicleServiceConfig.isActive.is_(True))
|
||||
|
||||
hero_slides = db.scalars(hero_stmt).all()
|
||||
destination_hero = db.scalars(destination_hero_stmt).all()
|
||||
@@ -27,6 +29,8 @@ def site_config(db: Session, active_only: bool, include_public_extras: bool = Tr
|
||||
}
|
||||
if include_public_extras:
|
||||
result["vehicleOptions"] = [model_dict(item) for item in db.scalars(vehicle_stmt).all()]
|
||||
vehicle_service = db.scalars(vehicle_service_stmt).first()
|
||||
result["vehicleService"] = model_dict(vehicle_service) if vehicle_service else None
|
||||
result["demandHero"] = [model_dict(item) for item in db.scalars(demand_hero_stmt).all()]
|
||||
result["demandFeatureCards"] = [model_dict(item) for item in db.scalars(demand_feature_card_stmt).all()]
|
||||
result["demandForm"] = [model_dict(item) for item in db.scalars(demand_form_stmt).all()]
|
||||
|
||||
Reference in new issue
Block a user