from __future__ import annotations """酒店查询与结果模型:HotelSearchQuery查询参数与HotelOption搜索结果。""" from datetime import date from typing import Literal from pydantic import field_validator,Field,model_validator from app.schemas.common import SourceMetadata,AppModel HotelSort = Literal["lowest_price","highest_price","most_reviewed"] HotelRatingFilter = Literal["3.5+","4.0+","4.5+"] HotelClass = Literal[2,3,4,5] class HotelSearchQuery(AppModel): """酒店查询参数""" query:str check_in_date:date check_out_date:date adults:int = Field(default=1, ge=1, le=9) # SerpApi要求提供儿童年龄,而不只是儿童人数。 children_ages:list[int] = Field(default_factory=list) currency:str = "CNY" language:str = "zh-cn" country:str = "cn" # 以下全部为可选条件 min_price:int | None = Field(default=None,gt=0) max_price:int | None = Field(default=None,gt=0) rating_filter:HotelRatingFilter | None=None hotel_class:list[HotelClass] = Field(default_factory=list) free_cancellation:bool | None=None sort:HotelSort | None=None no_cache:bool = False @field_validator("query") @classmethod def validate_query(cls,value:str)->str: if not value.strip(): raise ValueError("酒店搜索关键词不能为空") return value.strip() @field_validator("currency") @classmethod def normalize_currency(cls,value:str)->str: return value.upper() @field_validator("children_ages") @classmethod def validate_children_ages(cls,values:list[int])->list[int]: for age in values: if age < 1 or age > 17: raise ValueError("儿童年龄必须在1-17岁") return values @model_validator(mode="after") def validate_dates_and_prices(self)->"HotelSearchQuery": if self.check_out_date <= self.check_in_date: raise ValueError("退房日期必须晚于入住日期") if ( self.min_price is not None and self.max_price is not None and self.min_price > self.max_price ): raise ValueError("最低价格不能高于最高价格") return self class GeoPoint(AppModel): """地理坐标""" latitude:float longitude:float class HotelPriceOffer(AppModel): """某个酒店供应商给出的价格。""" source:str | None=None price_per_night:float | None=Field(default=None,gt=0) total_price:float | None=Field(default=None,gt=0) free_cancellation:bool | None=None class HotelOption(AppModel): """一个标准化酒店候选。""" hotel_id:str provider_rank:int = Field(ge=0) name:str property_type:str | None=None description:str | None=None coordinates:GeoPoint | None=None check_in_time:str | None=None check_out_time:str | None=None hotel_class:int | None=Field(default=None,ge=1,le=5) overall_rating:float | None=Field(default=None,ge=0,le=5) review_count:int | None=Field(default=None,ge=0) location_rating:float | None=Field(default=None,ge=0) price_per_night:float | None=Field(default=None,gt=0) total_price:float | None=Field(default=None,gt=0) currency:str = "CNY" amenities:list[str] = Field(default_factory=list) excluded_amenities:list[str] = Field(default_factory=list) free_cancellation:bool | None=None sponsored:bool = False eco_certified:bool | None=None property_token:str | None=None thumbnail_url:str | None=None price_offers:list[HotelPriceOffer] = Field(default_factory=list) # 新增这两个字段,用于保存接口返回的优惠信息 deal: str | None = None deal_description: str | None = None class HotelSearchResult(AppModel): """酒店搜索结果""" query:HotelSearchQuery hotels:list[HotelOption] = Field(default_factory=list) total_count:int = Field(default=0,ge=0) source:SourceMetadata next_page_token:str | None=None warnings:list[str] = Field(default_factory=list)