353 lines
12 KiB
Python
353 lines
12 KiB
Python
from datetime import datetime, time
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy import func, 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, CustomerBrowseHistory, DetailRecord, HomeTeamBuilding, HomeWanfaRecommendation, HomeWildArchive, Lead, WanfaCategory, WanfaRoute, utc_now
|
|
from ..schemas import BrowseHistoryCreateIn, LeadCreateIn, PhoneLoginIn
|
|
from ..media_urls import resolve_media_url
|
|
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)}
|
|
|
|
|
|
def public_vehicle_demand_dict(lead: Lead) -> dict:
|
|
return {
|
|
"id": lead.id,
|
|
"status": lead.status,
|
|
"contactName": lead.contactName,
|
|
"phoneMasked": mask_phone(lead.phone),
|
|
"destination": lead.destination,
|
|
"travelDate": lead.travelDate.isoformat() if lead.travelDate else None,
|
|
"peopleCount": lead.peopleCount,
|
|
"note": lead.note,
|
|
"vehicleDemand": lead.vehicleDemand or {},
|
|
"createdAt": lead.createdAt.isoformat(),
|
|
"updatedAt": lead.updatedAt.isoformat(),
|
|
}
|
|
|
|
|
|
def public_browse_history_dict(item: CustomerBrowseHistory) -> dict:
|
|
return {
|
|
"id": item.id,
|
|
"itemType": item.itemType,
|
|
"itemId": item.itemId,
|
|
"title": item.title,
|
|
"image": resolve_media_url(item.image) or "",
|
|
"visitedAt": item.visitedAt.isoformat(),
|
|
}
|
|
|
|
|
|
def browse_source(item_type: str, item_id: str, db: Session):
|
|
models = {
|
|
"wanfa-route": WanfaRoute,
|
|
"team-building": HomeTeamBuilding,
|
|
"wild-archive": HomeWildArchive,
|
|
}
|
|
model = models.get(item_type)
|
|
if model is None:
|
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="浏览内容类型不支持")
|
|
|
|
conditions = [model.id == item_id]
|
|
if hasattr(model, "isActive"):
|
|
conditions.append(model.isActive.is_(True))
|
|
source = db.scalar(select(model).where(*conditions))
|
|
if not source:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="浏览内容不存在")
|
|
return source
|
|
|
|
|
|
@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.get("/api/public/customer/vehicle-demands")
|
|
def list_customer_vehicle_demands(
|
|
pageNum: int = Query(default=1, ge=1),
|
|
pageSize: int = Query(default=20, ge=1, le=50),
|
|
customer: Customer = Depends(require_customer),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
filters = [Lead.customerId == customer.id, Lead.leadType == "vehicle"]
|
|
leads = db.scalars(
|
|
select(Lead)
|
|
.where(*filters)
|
|
.order_by(Lead.createdAt.desc())
|
|
.offset((pageNum - 1) * pageSize)
|
|
.limit(pageSize)
|
|
).all()
|
|
total = db.scalar(select(func.count()).select_from(Lead).where(*filters)) or 0
|
|
return success_response(
|
|
{
|
|
"items": [public_vehicle_demand_dict(lead) for lead in leads],
|
|
"total": total,
|
|
"pageNum": pageNum,
|
|
"pageSize": pageSize,
|
|
}
|
|
)
|
|
|
|
|
|
@router.get("/api/public/customer/vehicle-demands/{lead_id}")
|
|
def get_customer_vehicle_demand(
|
|
lead_id: str,
|
|
customer: Customer = Depends(require_customer),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
lead = db.scalar(
|
|
select(Lead).where(
|
|
Lead.id == lead_id,
|
|
Lead.customerId == customer.id,
|
|
Lead.leadType == "vehicle",
|
|
)
|
|
)
|
|
if not lead:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用车记录不存在")
|
|
return success_response(public_vehicle_demand_dict(lead))
|
|
|
|
|
|
@router.get("/api/public/customer/browse-history")
|
|
def list_customer_browse_history(
|
|
pageNum: int = Query(default=1, ge=1),
|
|
pageSize: int = Query(default=20, ge=1, le=50),
|
|
customer: Customer = Depends(require_customer),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
filters = [CustomerBrowseHistory.customerId == customer.id]
|
|
items = db.scalars(
|
|
select(CustomerBrowseHistory)
|
|
.where(*filters)
|
|
.order_by(CustomerBrowseHistory.visitedAt.desc())
|
|
.offset((pageNum - 1) * pageSize)
|
|
.limit(pageSize)
|
|
).all()
|
|
total = db.scalar(select(func.count()).select_from(CustomerBrowseHistory).where(*filters)) or 0
|
|
return success_response(
|
|
{
|
|
"items": [public_browse_history_dict(item) for item in items],
|
|
"total": total,
|
|
"pageNum": pageNum,
|
|
"pageSize": pageSize,
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/api/public/customer/browse-history", status_code=status.HTTP_201_CREATED)
|
|
def save_customer_browse_history(
|
|
body: BrowseHistoryCreateIn,
|
|
customer: Customer = Depends(require_customer),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
source = browse_source(body.itemType, body.itemId, db)
|
|
item = db.scalar(
|
|
select(CustomerBrowseHistory).where(
|
|
CustomerBrowseHistory.customerId == customer.id,
|
|
CustomerBrowseHistory.itemType == body.itemType,
|
|
CustomerBrowseHistory.itemId == body.itemId,
|
|
)
|
|
)
|
|
now = utc_now()
|
|
if item:
|
|
item.title = source.title
|
|
item.image = source.image or ""
|
|
item.visitedAt = now
|
|
item.updatedAt = now
|
|
else:
|
|
item = CustomerBrowseHistory(
|
|
customerId=customer.id,
|
|
itemType=body.itemType,
|
|
itemId=body.itemId,
|
|
title=source.title,
|
|
image=source.image or "",
|
|
visitedAt=now,
|
|
createdAt=now,
|
|
updatedAt=now,
|
|
)
|
|
db.add(item)
|
|
db.commit()
|
|
db.refresh(item)
|
|
return success_response(public_browse_history_dict(item), status_code=status.HTTP_201_CREATED)
|
|
|
|
|
|
@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)
|