Files
WonderQ-Project/WonderQ-Admin/app/routers/public.py

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)