feat: 新增用车需求服务及线索管理全功能

- 新增后台用车服务配置模块,支持维护服务简介、优势与使用流程
- 新增需求线索管理页面,支持筛选、查看与更新用车线索状态
- 新增小程序用车需求页面,优化登录路径与回跳逻辑
- 优化车型卡片跳转与用车需求提交功能
- 新增服务端API与数据处理逻辑,完善权限校验
- 更新全站配置与API文档,新增相关测试用例
This commit is contained in:
duanshuwen committed 2026-08-22 21:48:08 +08:00
1 parent b7a81964c9
commit d42f69bd47
36 files changed
+1290 -33

No files matched your search

+29 -1
View File
@@ -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:
+23 -3
View File
@@ -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)
+5 -1
View File
@@ -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()]