flight.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. from __future__ import annotations
  2. """航班查询与结果模型:FlightSearchQuery查询参数与FlightOption搜索结果。"""
  3. from datetime import datetime,date
  4. from typing import Literal
  5. from pydantic import field_validator,Field,model_validator
  6. from app.schemas.common import SourceMetadata,AppModel
  7. FlightType = Literal["round_trip","one_way"]
  8. TravelClass = Literal["economy","business","first","premium_economy"]
  9. FlightSort = Literal["top","price","departure_time","arrival_time","duration","emissions"]
  10. StopsFilter = Literal["any","nonstop","one_or_fewer","two_or_fewer"]
  11. class FlightSearchQuery(AppModel):
  12. """
  13. 航班查询参数
  14. 当前所有筛选条件默认不启用。
  15. 后续由需求解析agent根据用户真实需求赋值
  16. """
  17. departure_airports:list[str] = Field(min_length=1)
  18. arrival_airports:list[str] = Field(min_length=1)
  19. outbound_date:date
  20. return_date:date | None = None
  21. flight_type:FlightType="one_way"
  22. adults:int = Field(default=1, ge=1, le=9)
  23. children:int = Field(default=0, ge=0, le=8)
  24. travel_class:TravelClass="economy"
  25. currency:str = "CNY"
  26. # 以下全部是可选条件,不设置就不限制
  27. sort_by:FlightSort | None=None
  28. stops:StopsFilter | None=None
  29. max_price:int | None = Field(default=None,gt=0)
  30. # SerpApi 时间范围格式,例如:
  31. # "9,20" 或 "9,20,10,22"
  32. outbound_times:str | None=None
  33. language:str = "zh-cn"
  34. country:str = "cn"
  35. no_cache:bool = False
  36. # 是否加载 Google Flights 的“查看更多航班”
  37. show_hidden: bool = False
  38. # 是否启用更完整但响应更慢的深度查询
  39. deep_search: bool = False
  40. @field_validator("departure_airports","arrival_airports")
  41. @classmethod
  42. def normalize_airports(cls,value:list[str])->list[str]:
  43. """机场代码统一转为大写并去重"""
  44. normalized:list[str] = []
  45. for item in value:
  46. code = item.strip().upper()
  47. if not code:
  48. continue
  49. if code not in normalized:
  50. normalized.append(code)
  51. if not normalized:
  52. raise ValueError("机场代码不能为空")
  53. return normalized
  54. @field_validator("currency")
  55. @classmethod
  56. def normalize_currency(cls,value:str)->str:
  57. return value.upper()
  58. @model_validator(mode="after")
  59. def validate_dates(self)->"FlightSearchQuery":
  60. if self.flight_type == "round_trip":
  61. if self.return_date is None:
  62. raise ValueError("往返航班必须提供return_date。")
  63. if self.return_date <= self.outbound_date:
  64. raise ValueError("返程日期必须晚于出发日期。")
  65. return self
  66. class FlightSegment(AppModel):
  67. """一个具体航段,中转航班会包含FlightSegment。"""
  68. flight_number:str | None=None
  69. airline:str | None=None
  70. departure_airport_code:str
  71. departure_airport_name:str | None=None
  72. departure_time:datetime
  73. arrival_airport_code:str
  74. arrival_airport_name:str | None=None
  75. arrival_time:datetime
  76. duration_minutes:int | None=Field(default=None,ge=0)
  77. airplane:str | None=None
  78. travel_class:str | None=None
  79. legroom:str | None=None
  80. overnight:bool = False
  81. often_delayed_by_over_30_min:bool = False
  82. planed_and_crew_by:str | None=None
  83. extensions:list[str] = Field(default_factory=list)
  84. ticket_also_sold_by:list[str] = Field(default_factory=list)
  85. class Layover(AppModel):
  86. """经停或中转信息。"""
  87. airport_code:str
  88. airport_name:str | None=None
  89. duration_minutes:int | None=Field(default=None,ge=0)
  90. overnight:bool = False
  91. class CarbonEmissions(AppModel):
  92. """航班碳排放数据。"""
  93. this_flight_grams:int | None=None
  94. typical_for_route_grams:int | None=None
  95. difference_percent:int | None=None
  96. # SerpApi 返回的航班分组:best_flights / other_flights 等
  97. FlightSourceGroup = Literal["best","other","best_flights","other_flights"]
  98. class FlightOption(AppModel):
  99. """一个完整的去程或者返程候选方案。"""
  100. option_id:str
  101. source_group:FlightSourceGroup
  102. provider_rank:int = Field(ge=0)
  103. segments:list[FlightSegment] = Field(min_length=1)
  104. layovers:list[Layover] = Field(default_factory=list)
  105. departure_airport_code:str
  106. final_arrival_airport_code:str
  107. departure_time:datetime
  108. arrival_time:datetime
  109. total_duration_minutes:int | None=Field(default=None,ge=0)
  110. stop_count:int = Field(default=0,ge=0)
  111. price:int | None=Field(default=None,gt=0)
  112. currency:str = "CNY"
  113. flight_type:str | None=None
  114. airlines:list[str] = Field(default_factory=list)
  115. is_overnight:bool = False
  116. carbon_emissions:CarbonEmissions | None=None
  117. # 往返查询选择去程后,用于继续查询返程
  118. departure_token:str | None=None
  119. # 获取预定选项是可能使用
  120. booking_token:str | None=None
  121. class FlightSearchResult(AppModel):
  122. """标准化后的航班查询结果。"""
  123. query:FlightSearchQuery
  124. flights:list[FlightOption] = Field(default_factory=list)
  125. total_count:int = Field(default=0,ge=0)
  126. source:SourceMetadata
  127. price_insights:dict | None=None
  128. warnings:list[str] = Field(default_factory=list)