Files
WonderQ-Project/WonderQ-Admin/app/routers/public.py
duanshuwen d42f69bd47 feat: 新增用车需求服务及线索管理全功能
- 新增后台用车服务配置模块,支持维护服务简介、优势与使用流程
- 新增需求线索管理页面,支持筛选、查看与更新用车线索状态
- 新增小程序用车需求页面,优化登录路径与回跳逻辑
- 优化车型卡片跳转与用车需求提交功能
- 新增服务端API与数据处理逻辑,完善权限校验
- 更新全站配置与API文档,新增相关测试用例
2026-08-22 21:48:08 +08:00

199 lines
7.4 KiB
Python

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, 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
from ..schemas import LeadCreateIn, PhoneLoginIn
from ..serializers import (
public_concierge_advisor_dict,
public_home_team_building_dict,
public_home_team_building_detail_dict,
public_home_wanfa_recommendation_dict,
public_home_wild_archive_detail_dict,
public_home_wild_archive_dict,
public_detail_dict,
public_wanfa_category_dict,
)
from ..wechat import WechatApiError, WechatConfigError, exchange_phone_code
router = APIRouter()
def mask_phone(phone: str) -> str:
normalized = phone.strip()
if len(normalized) <= 7:
return "***"
return f"{normalized[:3]}****{normalized[-4:]}"
def public_customer_dict(customer: Customer) -> dict:
return {"id": customer.id, "phoneMasked": mask_phone(customer.phone)}
@router.get("/health")
def health():
return success_response({"ok": True, "service": "miniapp-api"})
@router.get("/api/public/site-config")
def get_site_config(db: Session = Depends(get_db)):
return success_response(site_config(db, active_only=True))
@router.get("/api/public/wanfa/categories")
def get_public_wanfa_categories(db: Session = Depends(get_db)):
categories = db.scalars(
select(WanfaCategory)
.options(selectinload(WanfaCategory.routes))
.order_by(WanfaCategory.sortOrder.asc())
).all()
return success_response({"categories": [public_wanfa_category_dict(category) for category in categories]})
@router.get("/api/public/details/{detail_key}")
def get_public_detail(detail_key: str, db: Session = Depends(get_db)):
detail = db.scalar(
select(DetailRecord).where(
DetailRecord.key == detail_key,
DetailRecord.isActive.is_(True),
)
)
if not detail:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="详情不存在")
advisor = None
if detail.conciergeAdvisorId:
candidate = db.get(ConciergeAdvisor, detail.conciergeAdvisorId)
if candidate and candidate.isActive:
advisor = candidate
return success_response(public_detail_dict(detail, advisor))
@router.get("/api/public/concierge/advisors")
def get_public_concierge_advisors(db: Session = Depends(get_db)):
advisors = db.scalars(
select(ConciergeAdvisor)
.where(ConciergeAdvisor.isActive.is_(True))
.order_by(ConciergeAdvisor.sortOrder.asc())
).all()
return success_response({"advisors": [public_concierge_advisor_dict(advisor) for advisor in advisors]})
@router.get("/api/public/home")
def get_public_home(db: Session = Depends(get_db)):
team_buildings = db.scalars(
select(HomeTeamBuilding)
.where(HomeTeamBuilding.isActive.is_(True))
.order_by(HomeTeamBuilding.sortOrder.asc())
).all()
wild_archives = db.scalars(
select(HomeWildArchive)
.where(HomeWildArchive.isActive.is_(True))
.order_by(HomeWildArchive.sortOrder.asc())
).all()
play_recommendations = db.scalars(
select(HomeWanfaRecommendation)
.options(selectinload(HomeWanfaRecommendation.category).selectinload(WanfaCategory.routes))
.where(HomeWanfaRecommendation.isActive.is_(True))
.order_by(HomeWanfaRecommendation.sortOrder.asc())
).all()
return success_response(
{
"experiences": [public_home_wanfa_recommendation_dict(item) for item in play_recommendations],
"teamBuildings": [public_home_team_building_dict(item) for item in team_buildings],
"wildArchives": [public_home_wild_archive_dict(item) for item in wild_archives],
}
)
@router.get("/api/public/home/team-buildings/{team_building_id}")
def get_public_team_building(team_building_id: str, db: Session = Depends(get_db)):
team_building = db.scalar(
select(HomeTeamBuilding).where(
HomeTeamBuilding.id == team_building_id,
HomeTeamBuilding.isActive.is_(True),
)
)
if not team_building:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="团队共创不存在")
return success_response(public_home_team_building_detail_dict(team_building))
@router.get("/api/public/home/wild-archives")
def list_public_wild_archives(db: Session = Depends(get_db)):
archives = db.scalars(
select(HomeWildArchive)
.where(HomeWildArchive.isActive.is_(True))
.order_by(HomeWildArchive.sortOrder.asc())
).all()
return success_response({"items": [public_home_wild_archive_dict(item) for item in archives]})
@router.get("/api/public/home/wild-archives/{archive_id}")
def get_public_wild_archive(archive_id: str, db: Session = Depends(get_db)):
archive = db.scalar(
select(HomeWildArchive).where(
HomeWildArchive.id == archive_id,
HomeWildArchive.isActive.is_(True),
)
)
if not archive:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="客片案例不存在")
return success_response(public_home_wild_archive_detail_dict(archive))
@router.post("/api/public/auth/phone-login")
def phone_login(body: PhoneLoginIn, db: Session = Depends(get_db)):
try:
phone = exchange_phone_code(body.code)
except WechatConfigError as exc:
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(exc)) from exc
except WechatApiError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
customer = db.scalar(select(Customer).where(Customer.phone == phone))
if not customer:
customer = Customer(phone=phone)
db.add(customer)
db.flush()
db.commit()
db.refresh(customer)
return success_response({"token": create_customer_token(customer), "customer": public_customer_dict(customer)})
@router.get("/api/public/auth/me")
def current_customer(customer: Customer = Depends(require_customer)):
return success_response(public_customer_dict(customer))
@router.post("/api/public/leads", status_code=status.HTTP_201_CREATED)
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)
return success_response({"id": lead.id, "status": lead.status}, status_code=status.HTTP_201_CREATED)