- Introduced a comprehensive API contract for the WonderQ-MiniAPP, detailing endpoints for site configuration, product listings, and lead submissions. - Defined data types for various entities including HeroSlide, Destination, Theme, CtaBanner, PublicProduct, and more. - Specified request and response formats, including error handling guidelines. chore: Update requirements to include python-multipart - Added python-multipart dependency to requirements.txt for handling file uploads. test: Implement API contract tests - Created test suite for API contracts, validating serializers and endpoints for public products and leads. - Included tests for destination and product serializers, ensuring correct data handling and validation. test: Add configuration tests for OSS settings - Implemented tests to verify that OSS settings are correctly loaded from environment variables.
93 lines
3.3 KiB
Python
93 lines
3.3 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy import or_, select
|
|
from sqlalchemy.orm import Session, selectinload
|
|
from .shared import site_config
|
|
from ..database import get_db
|
|
from ..models import Destination, DestinationAlias, Lead, Product
|
|
from ..schemas import LeadCreateIn, ProductQuery, ProductStatus
|
|
from ..serializers import destination_dict, public_product_dict
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/health")
|
|
def health():
|
|
return {"ok": True, "service": "miniapp-api"}
|
|
|
|
|
|
@router.get("/api/public/site-config")
|
|
def get_site_config(db: Session = Depends(get_db)):
|
|
return site_config(db, active_only=True)
|
|
|
|
|
|
@router.get("/api/public/products")
|
|
def list_products(
|
|
keyword: str | None = None,
|
|
destinationId: str | None = None,
|
|
status_value: ProductStatus = Query(default="published", alias="status"),
|
|
take: int = Query(default=48, ge=1, le=100),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
query = ProductQuery(keyword=keyword, destinationId=destinationId, status=status_value, take=take)
|
|
stmt = (
|
|
select(Product)
|
|
.options(selectinload(Product.destination).selectinload(Destination.aliases), selectinload(Product.images))
|
|
.where(Product.status == query.status)
|
|
.order_by(Product.sortWeight.asc(), Product.createdAt.asc())
|
|
.limit(query.take)
|
|
)
|
|
if query.destinationId:
|
|
stmt = stmt.where(Product.destinationId == query.destinationId)
|
|
if query.keyword:
|
|
pattern = f"%{query.keyword}%"
|
|
stmt = (
|
|
stmt.outerjoin(Product.destination)
|
|
.outerjoin(Destination.aliases)
|
|
.where(
|
|
or_(
|
|
Product.title.ilike(pattern),
|
|
Product.subtitle.ilike(pattern),
|
|
Product.tags.any(query.keyword),
|
|
Destination.name.ilike(pattern),
|
|
DestinationAlias.alias.ilike(pattern),
|
|
)
|
|
)
|
|
.distinct()
|
|
)
|
|
products = db.scalars(stmt).unique().all()
|
|
return {"items": [public_product_dict(product) for product in products]}
|
|
|
|
|
|
@router.get("/api/public/products/{product_id}")
|
|
def get_product(product_id: str, db: Session = Depends(get_db)):
|
|
stmt = select(Product).options(selectinload(Product.destination), selectinload(Product.images))
|
|
if product_id.isdigit():
|
|
stmt = stmt.where(Product.sourceId == int(product_id))
|
|
else:
|
|
stmt = stmt.where(Product.id == product_id)
|
|
product = db.scalars(stmt).first()
|
|
if not product:
|
|
raise HTTPException(status_code=404, detail="线路不存在")
|
|
return public_product_dict(product)
|
|
|
|
|
|
@router.get("/api/public/destinations")
|
|
def list_destinations(db: Session = Depends(get_db)):
|
|
destinations = db.scalars(
|
|
select(Destination)
|
|
.options(selectinload(Destination.aliases))
|
|
.where(Destination.isActive.is_(True))
|
|
.order_by(Destination.sortOrder.asc())
|
|
).all()
|
|
return {"items": [destination_dict(destination) for destination in destinations]}
|
|
|
|
|
|
@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")
|
|
db.add(lead)
|
|
db.commit()
|
|
db.refresh(lead)
|
|
return {"id": lead.id, "status": lead.status}
|