| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- from __future__ import annotations
- """测试TravelSearchService的航班/酒店搜索与规范化功能。"""
- import asyncio
- from collections import Counter
- from datetime import date
- from rich.console import Console
- from rich.table import Table
- from app.clients.serpapi_client import (
- SerpApiClient,
- SerpApiError,
- )
- from app.config import get_settings
- from app.schemas.flight import FlightSearchQuery
- from app.schemas.hotel import HotelSearchQuery
- from app.services.travel_search_service import (
- TravelSearchService,
- )
- console = Console()
- def format_duration(minutes: int | None) -> str:
- if minutes is None:
- return "未知"
- hours, remaining_minutes = divmod(minutes, 60)
- return f"{hours}小时{remaining_minutes}分"
- def format_flight_numbers(flight) -> str:
- flight_numbers = [
- segment.flight_number
- for segment in flight.segments
- if segment.flight_number
- ]
- return " / ".join(flight_numbers) or "未知"
- async def test_flight_service(
- service: TravelSearchService,
- ) -> None:
- console.rule("[bold]TravelSearchService 航班测试")
- query = FlightSearchQuery(
- departure_airports=["SHA", "PVG"],
- arrival_airports=["CTU", "TFU"],
- outbound_date=date(2026, 8, 10),
- flight_type="one_way",
- adults=2,
- # 当前不限制时间、机场、中转次数和价格。
- show_hidden=False,
- deep_search=False,
- )
- result = await service.search_flights(query)
- airport_counts = Counter(
- flight.final_arrival_airport_code
- for flight in result.flights
- )
- console.print(
- {
- "标准化航班总数": result.total_count,
- "抵达机场分布": dict(airport_counts),
- "数据源": result.source.provider,
- "查询ID": result.source.search_id,
- }
- )
- table = Table(
- title=(
- f"航班结果预览:展示10条,"
- f"实际保留{result.total_count}条"
- )
- )
- table.add_column("来源")
- table.add_column("航司/航班号")
- table.add_column("出发")
- table.add_column("到达")
- table.add_column("耗时")
- table.add_column("中转")
- table.add_column("单张票价")
- # 这里只限制终端预览,不修改 result.flights。
- for flight in result.flights[:10]:
- first_segment = flight.segments[0]
- last_segment = flight.segments[-1]
- airline_text = (
- "/".join(flight.airlines)
- if flight.airlines
- else "未知"
- )
- table.add_row(
- flight.source_group,
- (
- f"{airline_text}\n"
- f"{format_flight_numbers(flight)}"
- ),
- (
- f"{first_segment.departure_airport_code}\n"
- f"{flight.departure_time:%m-%d %H:%M}"
- ),
- (
- f"{last_segment.arrival_airport_code}\n"
- f"{flight.arrival_time:%m-%d %H:%M}"
- ),
- format_duration(
- flight.total_duration_minutes
- ),
- (
- "直飞"
- if flight.stop_count == 0
- else f"{flight.stop_count}次"
- ),
- (
- f"¥{flight.price}"
- if flight.price is not None
- else "未知"
- ),
- )
- console.print(table)
- for warning in result.warnings:
- console.print(f"[yellow]提示:{warning}[/yellow]")
- async def test_hotel_service(
- service: TravelSearchService,
- ) -> None:
- console.rule("[bold]TravelSearchService 酒店测试")
- query = HotelSearchQuery(
- query="成都酒店",
- check_in_date=date(2026, 8, 10),
- check_out_date=date(2026, 8, 13),
- adults=2,
- )
- result = await service.search_hotels(query)
- console.print(
- {
- "标准化酒店总数": result.total_count,
- "数据源": result.source.provider,
- "查询ID": result.source.search_id,
- }
- )
- table = Table(
- title=(
- f"酒店结果预览:展示10家,"
- f"实际保留{result.total_count}家"
- )
- )
- table.add_column("酒店")
- table.add_column("星级")
- table.add_column("评分")
- table.add_column("每晚")
- table.add_column("整个入住期")
- table.add_column("客观信息")
- # 这里是客观信息摘要,不是个性化推荐理由。
- for hotel in result.hotels[:10]:
- highlights: list[str] = []
- if hotel.review_count is not None:
- highlights.append(
- f"{hotel.review_count}条评价"
- )
- if hotel.location_rating is not None:
- highlights.append(
- f"位置评分{hotel.location_rating}"
- )
- highlights.extend(hotel.amenities[:2])
- if hotel.deal_description:
- highlights.append(
- hotel.deal_description
- )
- table.add_row(
- hotel.name,
- (
- f"{hotel.hotel_class}星"
- if hotel.hotel_class
- else "未知"
- ),
- (
- str(hotel.overall_rating)
- if hotel.overall_rating is not None
- else "未知"
- ),
- (
- f"¥{hotel.price_per_night:.0f}"
- if hotel.price_per_night is not None
- else "未知"
- ),
- (
- f"¥{hotel.total_price:.0f}"
- if hotel.total_price is not None
- else "未知"
- ),
- ";".join(highlights) or "暂无",
- )
- console.print(table)
- async def main() -> None:
- settings = get_settings()
- async with SerpApiClient(
- api_key=settings.require(
- "serpapi_api_key"
- ),
- timeout_seconds=(
- settings.request_timeout_seconds
- ),
- ) as client:
- service = TravelSearchService(client)
- await test_flight_service(service)
- await test_hotel_service(service)
- if __name__ == "__main__":
- try:
- asyncio.run(main())
- except SerpApiError as exc:
- console.print(
- f"[red]真实接口测试失败:{exc}[/red]"
- )
- raise SystemExit(1) from exc
|