feat: Add WonderQ-MiniAPP Public API documentation
- 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.
This commit is contained in:
@@ -3,33 +3,37 @@ from typing import Literal
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator
|
||||
|
||||
|
||||
ProductStatus = Literal["draft", "published", "archived"]
|
||||
LeadStatus = Literal["new", "assigned", "contacted", "planning", "won", "invalid"]
|
||||
|
||||
|
||||
class LoginIn(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=6)
|
||||
|
||||
|
||||
class ProductImageIn(BaseModel):
|
||||
url: str = Field(min_length=1)
|
||||
url: str = ""
|
||||
alt: str | None = None
|
||||
sortOrder: int = 0
|
||||
|
||||
|
||||
class TextBlock(BaseModel):
|
||||
type: Literal["text"]
|
||||
text: str = Field(min_length=1)
|
||||
text: str = ""
|
||||
|
||||
|
||||
class ImageBlock(BaseModel):
|
||||
type: Literal["image"]
|
||||
url: str = Field(min_length=1)
|
||||
url: str = ""
|
||||
alt: str | None = None
|
||||
|
||||
|
||||
class ProductDetailSectionIn(BaseModel):
|
||||
key: str = Field(min_length=1)
|
||||
label: str = Field(min_length=1)
|
||||
key: str = ""
|
||||
label: str = ""
|
||||
title: str | None = None
|
||||
blocks: list[TextBlock | ImageBlock] = []
|
||||
blocks: list[TextBlock | ImageBlock] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ProductCreateIn(BaseModel):
|
||||
@@ -38,12 +42,12 @@ class ProductCreateIn(BaseModel):
|
||||
destinationId: str | None = None
|
||||
priceAmount: int | None = Field(default=None, ge=0)
|
||||
priceUnit: str | None = None
|
||||
tags: list[str] = []
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
coverImage: str | None = None
|
||||
summary: str | None = None
|
||||
images: list[ProductImageIn] | None = None
|
||||
detailSections: list[ProductDetailSectionIn] | None = None
|
||||
status: Literal["draft", "published", "archived"] = "draft"
|
||||
status: ProductStatus = "draft"
|
||||
sortWeight: int = 0
|
||||
|
||||
|
||||
@@ -58,7 +62,7 @@ class ProductUpdateIn(BaseModel):
|
||||
summary: str | None = None
|
||||
images: list[ProductImageIn] | None = None
|
||||
detailSections: list[ProductDetailSectionIn] | None = None
|
||||
status: Literal["draft", "published", "archived"] | None = None
|
||||
status: ProductStatus | None = None
|
||||
sortWeight: int | None = None
|
||||
|
||||
|
||||
@@ -76,28 +80,41 @@ class LeadCreateIn(BaseModel):
|
||||
@field_validator("phone")
|
||||
@classmethod
|
||||
def normalize_phone(cls, value: str) -> str:
|
||||
return " ".join(value.strip().split())
|
||||
normalized = " ".join(value.strip().split())
|
||||
if not normalized:
|
||||
raise ValueError("联系方式不能为空")
|
||||
return normalized
|
||||
|
||||
@field_validator("travelDate", mode="before")
|
||||
@classmethod
|
||||
def parse_date_only_travel_date(cls, value):
|
||||
if isinstance(value, str) and len(value) == 10:
|
||||
try:
|
||||
return datetime.strptime(value, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
class LeadStatusIn(BaseModel):
|
||||
status: Literal["new", "assigned", "contacted", "planning", "won", "invalid"]
|
||||
status: LeadStatus
|
||||
|
||||
|
||||
class ProductQuery(BaseModel):
|
||||
keyword: str | None = None
|
||||
destinationId: str | None = None
|
||||
status: str = "published"
|
||||
status: ProductStatus = "published"
|
||||
take: int = Field(default=48, ge=1, le=100)
|
||||
|
||||
|
||||
class AdminProductQuery(BaseModel):
|
||||
keyword: str | None = None
|
||||
status: str | None = None
|
||||
status: ProductStatus | None = None
|
||||
take: int = Field(default=100, ge=1, le=200)
|
||||
|
||||
|
||||
class LeadQuery(BaseModel):
|
||||
status: str | None = None
|
||||
status: LeadStatus | None = None
|
||||
take: int = Field(default=100, ge=1, le=200)
|
||||
|
||||
|
||||
@@ -105,9 +122,17 @@ class SiteConfigPatchIn(BaseModel):
|
||||
title: str | None = None
|
||||
kicker: str | None = None
|
||||
name: str | None = None
|
||||
slug: str | None = None
|
||||
region: str | None = None
|
||||
label: str | None = None
|
||||
alt: str | None = None
|
||||
image: str | None = None
|
||||
targetType: str | None = None
|
||||
targetValue: str | None = None
|
||||
isHot: bool | None = None
|
||||
isActive: bool | None = None
|
||||
sortOrder: int | None = None
|
||||
|
||||
|
||||
class SiteConfigReorderIn(BaseModel):
|
||||
itemIds: list[str] = Field(default_factory=list)
|
||||
|
||||
Reference in New Issue
Block a user