hotel.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. from __future__ import annotations
  2. """酒店查询与结果模型:HotelSearchQuery查询参数与HotelOption搜索结果。"""
  3. from datetime import date
  4. from typing import Literal
  5. from pydantic import field_validator,Field,model_validator
  6. from app.schemas.common import SourceMetadata,AppModel
  7. HotelSort = Literal["lowest_price","highest_price","most_reviewed"]
  8. HotelRatingFilter = Literal["3.5+","4.0+","4.5+"]
  9. HotelClass = Literal[2,3,4,5]
  10. class HotelSearchQuery(AppModel):
  11. """酒店查询参数"""
  12. query:str
  13. check_in_date:date
  14. check_out_date:date
  15. adults:int = Field(default=1, ge=1, le=9)
  16. # SerpApi要求提供儿童年龄,而不只是儿童人数。
  17. children_ages:list[int] = Field(default_factory=list)
  18. currency:str = "CNY"
  19. language:str = "zh-cn"
  20. country:str = "cn"
  21. # 以下全部为可选条件
  22. min_price:int | None = Field(default=None,gt=0)
  23. max_price:int | None = Field(default=None,gt=0)
  24. rating_filter:HotelRatingFilter | None=None
  25. hotel_class:list[HotelClass] = Field(default_factory=list)
  26. free_cancellation:bool | None=None
  27. sort:HotelSort | None=None
  28. no_cache:bool = False
  29. @field_validator("query")
  30. @classmethod
  31. def validate_query(cls,value:str)->str:
  32. if not value.strip():
  33. raise ValueError("酒店搜索关键词不能为空")
  34. return value.strip()
  35. @field_validator("currency")
  36. @classmethod
  37. def normalize_currency(cls,value:str)->str:
  38. return value.upper()
  39. @field_validator("children_ages")
  40. @classmethod
  41. def validate_children_ages(cls,values:list[int])->list[int]:
  42. for age in values:
  43. if age < 1 or age > 17:
  44. raise ValueError("儿童年龄必须在1-17岁")
  45. return values
  46. @model_validator(mode="after")
  47. def validate_dates_and_prices(self)->"HotelSearchQuery":
  48. if self.check_out_date <= self.check_in_date:
  49. raise ValueError("退房日期必须晚于入住日期")
  50. if (
  51. self.min_price is not None
  52. and self.max_price is not None
  53. and self.min_price > self.max_price
  54. ):
  55. raise ValueError("最低价格不能高于最高价格")
  56. return self
  57. class GeoPoint(AppModel):
  58. """地理坐标"""
  59. latitude:float
  60. longitude:float
  61. class HotelPriceOffer(AppModel):
  62. """某个酒店供应商给出的价格。"""
  63. source:str | None=None
  64. price_per_night:float | None=Field(default=None,gt=0)
  65. total_price:float | None=Field(default=None,gt=0)
  66. free_cancellation:bool | None=None
  67. class HotelOption(AppModel):
  68. """一个标准化酒店候选。"""
  69. hotel_id:str
  70. provider_rank:int = Field(ge=0)
  71. name:str
  72. property_type:str | None=None
  73. description:str | None=None
  74. coordinates:GeoPoint | None=None
  75. check_in_time:str | None=None
  76. check_out_time:str | None=None
  77. hotel_class:int | None=Field(default=None,ge=1,le=5)
  78. overall_rating:float | None=Field(default=None,ge=0,le=5)
  79. review_count:int | None=Field(default=None,ge=0)
  80. location_rating:float | None=Field(default=None,ge=0)
  81. price_per_night:float | None=Field(default=None,gt=0)
  82. total_price:float | None=Field(default=None,gt=0)
  83. currency:str = "CNY"
  84. amenities:list[str] = Field(default_factory=list)
  85. excluded_amenities:list[str] = Field(default_factory=list)
  86. free_cancellation:bool | None=None
  87. sponsored:bool = False
  88. eco_certified:bool | None=None
  89. property_token:str | None=None
  90. thumbnail_url:str | None=None
  91. price_offers:list[HotelPriceOffer] = Field(default_factory=list)
  92. # 新增这两个字段,用于保存接口返回的优惠信息
  93. deal: str | None = None
  94. deal_description: str | None = None
  95. class HotelSearchResult(AppModel):
  96. """酒店搜索结果"""
  97. query:HotelSearchQuery
  98. hotels:list[HotelOption] = Field(default_factory=list)
  99. total_count:int = Field(default=0,ge=0)
  100. source:SourceMetadata
  101. next_page_token:str | None=None
  102. warnings:list[str] = Field(default_factory=list)