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)