智能旅行套餐查找器 - 使用Skyscanner和Booking.com搜索航班和酒店

中级

这是一个Personal Productivity、Multimodal AI领域的自动化工作流,包含 12 个节点。主要使用 Set、Code、Gmail、Merge、Webhook 等节点。 智能旅行套餐查找器:通过 Skyscanner 和 Booking.com 搜索航班与酒店

前置要求
  • Google 账号和 Gmail API 凭证
  • HTTP Webhook 端点(n8n 会自动生成)
  • 可能需要目标 API 的认证凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "Q9TXNQXxpkuynljZ",
  "meta": {
    "instanceId": "b91e510ebae4127f953fd2f5f8d40d58ca1e71c746d4500c12ae86aad04c1502"
  },
  "name": "智能旅行套餐查找器 - 使用Skyscanner和Booking.com搜索航班和酒店",
  "tags": [],
  "nodes": [
    {
      "id": "e1964e90-e7d0-480d-8eed-36dd27d9d648",
      "name": "📥 旅行请求Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -640,
        -384
      ],
      "webhookId": "travel-itinerary-generator",
      "parameters": {
        "path": "travel-search",
        "options": {
          "rawBody": true
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "d8b0f0fc-3ab8-4f95-a9c8-6012ad56a9ab",
      "name": "📝 解析和验证输入",
      "type": "n8n-nodes-base.set",
      "position": [
        -480,
        -384
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "destination",
              "name": "destination",
              "type": "string",
              "value": "={{ $json.body.destination || 'Shanghai' }}"
            },
            {
              "id": "departure",
              "name": "departure",
              "type": "string",
              "value": "={{ $json.body.departure || 'New York' }}"
            },
            {
              "id": "checkInDate",
              "name": "checkInDate",
              "type": "string",
              "value": "={{ $json.body.checkInDate || '2025-12-01' }}"
            },
            {
              "id": "checkOutDate",
              "name": "checkOutDate",
              "type": "string",
              "value": "={{ $json.body.checkOutDate || '2025-12-08' }}"
            },
            {
              "id": "notificationEmail",
              "name": "notificationEmail",
              "type": "string",
              "value": "={{ $json.body.notificationEmail || $json.body.email }}"
            },
            {
              "id": "adults",
              "name": "adults",
              "type": "number",
              "value": "={{ $json.body.adults || 1 }}"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "89686042-6637-485a-ae2a-88eb5a2a7680",
      "name": "✈️ 搜索航班(Skyscanner)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -304,
        -496
      ],
      "parameters": {
        "url": "https://sky-scrapper.p.rapidapi.com/api/v1/flights/searchFlights",
        "options": {
          "timeout": 30000
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "originSkyId",
              "value": "={{ $json.departure }}"
            },
            {
              "name": "destinationSkyId",
              "value": "={{ $json.destination }}"
            },
            {
              "name": "originEntityId",
              "value": "27537542"
            },
            {
              "name": "destinationEntityId",
              "value": "27537579"
            },
            {
              "name": "date",
              "value": "={{ $json.checkInDate }}"
            },
            {
              "name": "adults",
              "value": "={{ $json.adults }}"
            },
            {
              "name": "currency",
              "value": "USD"
            },
            {
              "name": "market",
              "value": "en-US"
            },
            {
              "name": "countryCode",
              "value": "US"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "f752c284-3bc5-430e-8193-da6876f52f66",
      "name": "🏨 搜索酒店(Booking.com)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -304,
        -288
      ],
      "parameters": {
        "url": "https://booking-com.p.rapidapi.com/v1/hotels/search",
        "options": {
          "timeout": 30000
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "dest_type",
              "value": "city"
            },
            {
              "name": "dest_id",
              "value": "-1746443"
            },
            {
              "name": "checkin_date",
              "value": "={{ $json.checkInDate }}"
            },
            {
              "name": "checkout_date",
              "value": "={{ $json.checkOutDate }}"
            },
            {
              "name": "adults_number",
              "value": "={{ $json.adults }}"
            },
            {
              "name": "order_by",
              "value": "price"
            },
            {
              "name": "filter_by_currency",
              "value": "USD"
            },
            {
              "name": "units",
              "value": "metric"
            },
            {
              "name": "room_number",
              "value": "1"
            },
            {
              "name": "page_number",
              "value": "0"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "6f48d727-b21c-4b6f-b9b5-b8b4de4fee95",
      "name": "🔀 合并航班和酒店数据",
      "type": "n8n-nodes-base.merge",
      "position": [
        -96,
        -496
      ],
      "parameters": {
        "mode": "combine",
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "5267fce7-f040-4a6c-8159-23351371c325",
      "name": "🧮 生成行程组合",
      "type": "n8n-nodes-base.code",
      "position": [
        80,
        -496
      ],
      "parameters": {
        "jsCode": "// Travel Itinerary Combination Engine\n// Combines flights and hotels into ranked packages\n\nconst inputData = $input.all();\n\n// Extract flight and hotel data from merged inputs\nlet flightData = [];\nlet hotelData = [];\nlet searchParams = {};\n\n// Parse the merged data\nfor (const item of inputData) {\n  if (item.json.data && item.json.data.itineraries) {\n    // Flight data\n    const flights = item.json.data.itineraries.results || [];\n    flightData = flights.slice(0, 10); // Top 10 flights\n  } else if (item.json.result) {\n    // Hotel data\n    const hotels = item.json.result || [];\n    hotelData = hotels.slice(0, 10); // Top 10 hotels\n  }\n  \n  // Capture search parameters\n  if (item.json.destination) {\n    searchParams = {\n      destination: item.json.destination,\n      departure: item.json.departure,\n      checkInDate: item.json.checkInDate,\n      checkOutDate: item.json.checkOutDate,\n      notificationEmail: item.json.notificationEmail,\n      adults: item.json.adults || 1\n    };\n  }\n}\n\n// Fallback: Create mock data if APIs failed\nif (flightData.length === 0) {\n  console.log('No flight data - using mock data');\n  flightData = [\n    {\n      id: 'mock-flight-1',\n      price: { raw: 650, formatted: '$650' },\n      legs: [{\n        origin: { displayCode: searchParams.departure || 'NYC' },\n        destination: { displayCode: searchParams.destination || 'PVG' },\n        departure: searchParams.checkInDate + 'T08:00:00',\n        arrival: searchParams.checkInDate + 'T14:30:00',\n        durationInMinutes: 870,\n        carriers: { marketing: [{ name: 'United Airlines' }] }\n      }]\n    },\n    {\n      id: 'mock-flight-2',\n      price: { raw: 720, formatted: '$720' },\n      legs: [{\n        origin: { displayCode: searchParams.departure || 'NYC' },\n        destination: { displayCode: searchParams.destination || 'PVG' },\n        departure: searchParams.checkInDate + 'T10:30:00',\n        arrival: searchParams.checkInDate + 'T17:00:00',\n        durationInMinutes: 870,\n        carriers: { marketing: [{ name: 'Delta Airlines' }] }\n      }]\n    },\n    {\n      id: 'mock-flight-3',\n      price: { raw: 580, formatted: '$580' },\n      legs: [{\n        origin: { displayCode: searchParams.departure || 'NYC' },\n        destination: { displayCode: searchParams.destination || 'PVG' },\n        departure: searchParams.checkInDate + 'T14:00:00',\n        arrival: searchParams.checkInDate + 'T20:30:00',\n        durationInMinutes: 870,\n        carriers: { marketing: [{ name: 'American Airlines' }] }\n      }]\n    }\n  ];\n}\n\nif (hotelData.length === 0) {\n  console.log('No hotel data - using mock data');\n  const nights = calculateNights(searchParams.checkInDate, searchParams.checkOutDate);\n  hotelData = [\n    {\n      hotel_id: 'mock-hotel-1',\n      hotel_name: 'Shanghai Grand Hotel',\n      price_breakdown: { gross_price: 150 * nights },\n      review_score: 8.5,\n      address: 'Pudong District, Shanghai',\n      url: 'https://booking.com/hotel-1',\n      pricePerNight: 150,\n      totalNights: nights\n    },\n    {\n      hotel_id: 'mock-hotel-2',\n      hotel_name: 'Bund Riverside Inn',\n      price_breakdown: { gross_price: 120 * nights },\n      review_score: 8.2,\n      address: 'The Bund, Shanghai',\n      url: 'https://booking.com/hotel-2',\n      pricePerNight: 120,\n      totalNights: nights\n    },\n    {\n      hotel_id: 'mock-hotel-3',\n      hotel_name: 'Lujiazui Business Hotel',\n      price_breakdown: { gross_price: 180 * nights },\n      review_score: 8.8,\n      address: 'Lujiazui, Shanghai',\n      url: 'https://booking.com/hotel-3',\n      pricePerNight: 180,\n      totalNights: nights\n    }\n  ];\n}\n\n// Calculate nights\nfunction calculateNights(checkIn, checkOut) {\n  const start = new Date(checkIn);\n  const end = new Date(checkOut);\n  return Math.ceil((end - start) / (1000 * 60 * 60 * 24));\n}\n\nconst nights = calculateNights(searchParams.checkInDate, searchParams.checkOutDate);\n\n// Process and normalize flight data\nconst processedFlights = flightData.map(flight => {\n  const leg = flight.legs ? flight.legs[0] : {};\n  return {\n    id: flight.id,\n    airline: leg.carriers?.marketing?.[0]?.name || 'Unknown Airline',\n    origin: leg.origin?.displayCode || searchParams.departure,\n    destination: leg.destination?.displayCode || searchParams.destination,\n    departure: leg.departure || searchParams.checkInDate,\n    arrival: leg.arrival || searchParams.checkInDate,\n    duration: leg.durationInMinutes ? `${Math.floor(leg.durationInMinutes / 60)}h ${leg.durationInMinutes % 60}m` : 'N/A',\n    price: flight.price?.raw || 0,\n    priceFormatted: flight.price?.formatted || `$${flight.price?.raw || 0}`,\n    bookingLink: `https://www.skyscanner.com/transport/flights/${leg.origin?.displayCode}/${leg.destination?.displayCode}`\n  };\n});\n\n// Process and normalize hotel data\nconst processedHotels = hotelData.map(hotel => {\n  const totalPrice = hotel.price_breakdown?.gross_price || hotel.pricePerNight * nights || 0;\n  return {\n    id: hotel.hotel_id,\n    name: hotel.hotel_name || 'Unknown Hotel',\n    rating: hotel.review_score || 0,\n    address: hotel.address || searchParams.destination,\n    pricePerNight: hotel.pricePerNight || Math.round(totalPrice / nights),\n    totalPrice: totalPrice,\n    nights: nights,\n    bookingLink: hotel.url || `https://www.booking.com/hotel/${hotel.hotel_id}.html`\n  };\n});\n\n// Create all possible combinations\nconst itineraries = [];\nfor (const flight of processedFlights) {\n  for (const hotel of processedHotels) {\n    itineraries.push({\n      id: `${flight.id}-${hotel.id}`,\n      flight: flight,\n      hotel: hotel,\n      totalPrice: flight.price + hotel.totalPrice,\n      savings: 0 // Will calculate after sorting\n    });\n  }\n}\n\n// Sort by total price (cheapest first)\nitineraries.sort((a, b) => a.totalPrice - b.totalPrice);\n\n// Calculate savings compared to most expensive option\nconst mostExpensive = itineraries[itineraries.length - 1].totalPrice;\nitineraries.forEach(item => {\n  item.savings = mostExpensive - item.totalPrice;\n});\n\n// Get top 5 itineraries\nconst topItineraries = itineraries.slice(0, 5);\n\n// Return result with metadata\nreturn [{\n  json: {\n    searchParams: searchParams,\n    itineraries: topItineraries,\n    totalCombinations: itineraries.length,\n    flightsFound: processedFlights.length,\n    hotelsFound: processedHotels.length,\n    generatedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "80f6f70c-8df0-42db-ba0c-97def58e5a36",
      "name": "🎨 格式化HTML电子邮件",
      "type": "n8n-nodes-base.code",
      "position": [
        272,
        -496
      ],
      "parameters": {
        "jsCode": "// HTML Email Generator for Travel Itineraries\n\nconst data = $input.first().json;\nconst { searchParams, itineraries, totalCombinations, flightsFound, hotelsFound } = data;\n\n// Format currency\nfunction formatCurrency(amount) {\n  return new Intl.NumberFormat('en-US', {\n    style: 'currency',\n    currency: 'USD'\n  }).format(amount);\n}\n\n// Format date\nfunction formatDate(dateString) {\n  const date = new Date(dateString);\n  return date.toLocaleDateString('en-US', {\n    weekday: 'short',\n    year: 'numeric',\n    month: 'short',\n    day: 'numeric'\n  });\n}\n\n// Format time\nfunction formatTime(dateString) {\n  const date = new Date(dateString);\n  return date.toLocaleTimeString('en-US', {\n    hour: '2-digit',\n    minute: '2-digit'\n  });\n}\n\n// Generate itinerary cards HTML\nfunction generateItineraryCards() {\n  return itineraries.map((itinerary, index) => {\n    const ranking = index + 1;\n    const badge = ranking === 1 ? '<span style=\"background: #10b981; color: white; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-left: 10px;\">🏆 BEST VALUE</span>' : '';\n    \n    return `\n      <div style=\"background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border: ${ranking === 1 ? '3px solid #10b981' : '2px solid #e5e7eb'};\">\n        <div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;\">\n          <h2 style=\"margin: 0; color: #1f2937; font-size: 20px;\">Option ${ranking}${badge}</h2>\n          <div style=\"text-align: right;\">\n            <div style=\"font-size: 28px; font-weight: bold; color: #10b981;\">${formatCurrency(itinerary.totalPrice)}</div>\n            ${itinerary.savings > 0 ? `<div style=\"color: #6b7280; font-size: 14px;\">Save ${formatCurrency(itinerary.savings)}</div>` : ''}\n          </div>\n        </div>\n        \n        <!-- Flight Section -->\n        <div style=\"background: #f9fafb; border-radius: 8px; padding: 16px; margin-bottom: 16px;\">\n          <div style=\"display: flex; align-items: center; margin-bottom: 12px;\">\n            <span style=\"font-size: 20px; margin-right: 8px;\">✈️</span>\n            <h3 style=\"margin: 0; color: #374151; font-size: 16px;\">Flight Details</h3>\n          </div>\n          <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px;\">\n            <div>\n              <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">Airline</div>\n              <div style=\"color: #1f2937; font-weight: 600;\">${itinerary.flight.airline}</div>\n            </div>\n            <div>\n              <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">Duration</div>\n              <div style=\"color: #1f2937; font-weight: 600;\">${itinerary.flight.duration}</div>\n            </div>\n          </div>\n          <div style=\"display: flex; justify-content: space-between; align-items: center; padding: 12px; background: white; border-radius: 6px;\">\n            <div>\n              <div style=\"font-size: 18px; font-weight: bold; color: #1f2937;\">${itinerary.flight.origin}</div>\n              <div style=\"color: #6b7280; font-size: 13px;\">${formatDate(itinerary.flight.departure)}</div>\n              <div style=\"color: #374151; font-size: 14px; font-weight: 600;\">${formatTime(itinerary.flight.departure)}</div>\n            </div>\n            <div style=\"color: #9ca3af; font-size: 20px;\">→</div>\n            <div style=\"text-align: right;\">\n              <div style=\"font-size: 18px; font-weight: bold; color: #1f2937;\">${itinerary.flight.destination}</div>\n              <div style=\"color: #6b7280; font-size: 13px;\">${formatDate(itinerary.flight.arrival)}</div>\n              <div style=\"color: #374151; font-size: 14px; font-weight: 600;\">${formatTime(itinerary.flight.arrival)}</div>\n            </div>\n          </div>\n          <div style=\"margin-top: 12px; display: flex; justify-content: space-between; align-items: center;\">\n            <div style=\"color: #1f2937; font-weight: 600; font-size: 16px;\">${itinerary.flight.priceFormatted}</div>\n            <a href=\"${itinerary.flight.bookingLink}\" style=\"background: #3b82f6; color: white; padding: 8px 16px; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 600;\">Book Flight</a>\n          </div>\n        </div>\n        \n        <!-- Hotel Section -->\n        <div style=\"background: #f9fafb; border-radius: 8px; padding: 16px;\">\n          <div style=\"display: flex; align-items: center; margin-bottom: 12px;\">\n            <span style=\"font-size: 20px; margin-right: 8px;\">🏨</span>\n            <h3 style=\"margin: 0; color: #374151; font-size: 16px;\">Hotel Details</h3>\n          </div>\n          <div style=\"margin-bottom: 12px;\">\n            <div style=\"font-size: 16px; font-weight: bold; color: #1f2937; margin-bottom: 4px;\">${itinerary.hotel.name}</div>\n            <div style=\"color: #6b7280; font-size: 13px; margin-bottom: 4px;\">${itinerary.hotel.address}</div>\n            <div style=\"color: #f59e0b; font-size: 14px;\">⭐ ${itinerary.hotel.rating.toFixed(1)} / 10</div>\n          </div>\n          <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 12px; background: white; border-radius: 6px; margin-bottom: 12px;\">\n            <div>\n              <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">Nightly Rate</div>\n              <div style=\"color: #1f2937; font-weight: 600;\">${formatCurrency(itinerary.hotel.pricePerNight)}</div>\n            </div>\n            <div>\n              <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">Total (${itinerary.hotel.nights} nights)</div>\n              <div style=\"color: #1f2937; font-weight: 600;\">${formatCurrency(itinerary.hotel.totalPrice)}</div>\n            </div>\n          </div>\n          <div style=\"display: flex; justify-content: space-between; align-items: center;\">\n            <div style=\"color: #6b7280; font-size: 13px;\">${formatDate(searchParams.checkInDate)} - ${formatDate(searchParams.checkOutDate)}</div>\n            <a href=\"${itinerary.hotel.bookingLink}\" style=\"background: #10b981; color: white; padding: 8px 16px; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 600;\">Book Hotel</a>\n          </div>\n        </div>\n      </div>\n    `;\n  }).join('');\n}\n\n// Generate comparison table\nfunction generateComparisonTable() {\n  const rows = itineraries.map((itinerary, index) => `\n    <tr style=\"${index % 2 === 0 ? 'background: #f9fafb;' : 'background: white;'}\">\n      <td style=\"padding: 12px; text-align: center; font-weight: 600; color: #1f2937;\">${index + 1}</td>\n      <td style=\"padding: 12px; color: #374151;\">${itinerary.flight.airline}</td>\n      <td style=\"padding: 12px; color: #374151;\">${itinerary.hotel.name}</td>\n      <td style=\"padding: 12px; text-align: right; color: #6b7280;\">${itinerary.flight.priceFormatted}</td>\n      <td style=\"padding: 12px; text-align: right; color: #6b7280;\">${formatCurrency(itinerary.hotel.totalPrice)}</td>\n      <td style=\"padding: 12px; text-align: right; font-weight: bold; color: #10b981; font-size: 16px;\">${formatCurrency(itinerary.totalPrice)}</td>\n    </tr>\n  `).join('');\n  \n  return `\n    <table style=\"width: 100%; border-collapse: collapse; margin: 24px 0; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.05);\">\n      <thead>\n        <tr style=\"background: #1f2937; color: white;\">\n          <th style=\"padding: 14px; text-align: center; font-weight: 600;\">#</th>\n          <th style=\"padding: 14px; text-align: left; font-weight: 600;\">Flight</th>\n          <th style=\"padding: 14px; text-align: left; font-weight: 600;\">Hotel</th>\n          <th style=\"padding: 14px; text-align: right; font-weight: 600;\">Flight Price</th>\n          <th style=\"padding: 14px; text-align: right; font-weight: 600;\">Hotel Price</th>\n          <th style=\"padding: 14px; text-align: right; font-weight: 600;\">Total</th>\n        </tr>\n      </thead>\n      <tbody>\n        ${rows}\n      </tbody>\n    </table>\n  `;\n}\n\n// Generate complete HTML email\nconst htmlEmail = `\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Your Travel Itinerary Options</title>\n</head>\n<body style=\"margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f3f4f6;\">\n  <div style=\"max-width: 700px; margin: 0 auto; padding: 20px;\">\n    \n    <!-- Header -->\n    <div style=\"background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 32px; text-align: center; margin-bottom: 24px; color: white;\">\n      <h1 style=\"margin: 0 0 12px 0; font-size: 32px; font-weight: bold;\">✈️ Your Travel Options</h1>\n      <p style=\"margin: 0; font-size: 18px; opacity: 0.95;\">${searchParams.departure} → ${searchParams.destination}</p>\n      <p style=\"margin: 8px 0 0 0; font-size: 14px; opacity: 0.9;\">${formatDate(searchParams.checkInDate)} - ${formatDate(searchParams.checkOutDate)}</p>\n    </div>\n    \n    <!-- Summary Stats -->\n    <div style=\"background: white; border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);\">\n      <div style=\"display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; text-align: center;\">\n        <div>\n          <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">OPTIONS FOUND</div>\n          <div style=\"font-size: 24px; font-weight: bold; color: #3b82f6;\">${itineraries.length}</div>\n        </div>\n        <div>\n          <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">BEST PRICE</div>\n          <div style=\"font-size: 24px; font-weight: bold; color: #10b981;\">${formatCurrency(itineraries[0].totalPrice)}</div>\n        </div>\n        <div>\n          <div style=\"color: #6b7280; font-size: 12px; margin-bottom: 4px;\">MAX SAVINGS</div>\n          <div style=\"font-size: 24px; font-weight: bold; color: #f59e0b;\">${formatCurrency(itineraries[0].savings)}</div>\n        </div>\n      </div>\n    </div>\n    \n    <!-- Itinerary Cards -->\n    <div>\n      <h2 style=\"color: #1f2937; font-size: 22px; margin-bottom: 16px;\">📋 Recommended Packages</h2>\n      ${generateItineraryCards()}\n    </div>\n    \n    <!-- Comparison Table -->\n    <div style=\"margin-top: 32px;\">\n      <h2 style=\"color: #1f2937; font-size: 22px; margin-bottom: 16px;\">📊 Quick Comparison</h2>\n      ${generateComparisonTable()}\n    </div>\n    \n    <!-- Footer -->\n    <div style=\"background: #f9fafb; border-radius: 12px; padding: 20px; margin-top: 24px; text-align: center; color: #6b7280; font-size: 13px;\">\n      <p style=\"margin: 0 0 8px 0;\">✨ Generated by Smart Travel Itinerary System</p>\n      <p style=\"margin: 0;\">Analyzed ${totalCombinations} combinations from ${flightsFound} flights and ${hotelsFound} hotels</p>\n      <p style=\"margin: 8px 0 0 0; font-size: 11px; color: #9ca3af;\">Prices are subject to availability and may change</p>\n    </div>\n    \n  </div>\n</body>\n</html>\n`;\n\nreturn [{\n  json: {\n    subject: `🎉 ${itineraries.length} Travel Options: ${searchParams.departure} → ${searchParams.destination}`,\n    htmlBody: htmlEmail,\n    recipient: searchParams.notificationEmail,\n    itinerariesCount: itineraries.length,\n    bestPrice: formatCurrency(itineraries[0].totalPrice),\n    searchParams: searchParams\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7ec3fb5b-a73e-4e99-9019-0b241a2fe736",
      "name": "✉️ 通过Gmail发送",
      "type": "n8n-nodes-base.gmail",
      "position": [
        448,
        -496
      ],
      "webhookId": "d0670a84-770f-4fe9-9be4-040e3b63b1f9",
      "parameters": {
        "sendTo": "={{ $json.recipient }}",
        "message": "={{ $json.htmlBody }}",
        "options": {},
        "subject": "={{ $json.subject }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "ecdc9e1c-ae6d-4094-af8f-d9a9c4bc102c",
      "name": "📤 Webhook响应",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        608,
        -496
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ {\n  \"success\": true,\n  \"message\": \"Travel itinerary email sent successfully!\",\n  \"itinerariesGenerated\": $json.itinerariesCount,\n  \"bestPrice\": $json.bestPrice,\n  \"sentTo\": $json.recipient,\n  \"searchDetails\": {\n    \"from\": $json.searchParams.departure,\n    \"to\": $json.searchParams.destination,\n    \"checkIn\": $json.searchParams.checkInDate,\n    \"checkOut\": $json.searchParams.checkOutDate\n  },\n  \"timestamp\": new Date().toISOString()\n} }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "b43f6af3-042e-4258-8006-04cb17fade14",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1360,
        -496
      ],
      "parameters": {
        "width": 720,
        "height": 336,
        "content": "## 简介"
      },
      "typeVersion": 1
    },
    {
      "id": "b27a8a26-8e23-4b58-945c-01bf5d4e8fe9",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16,
        -352
      ],
      "parameters": {
        "color": 3,
        "width": 752,
        "height": 528,
        "content": "## 设置说明"
      },
      "typeVersion": 1
    },
    {
      "id": "2d4004f8-8e1c-477c-8669-6facc12b7d85",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1360,
        -128
      ],
      "parameters": {
        "color": 6,
        "width": 720,
        "height": 224,
        "content": "## 工作流步骤"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "5ed5ab9a-b808-494a-9bac-8919d45e814c",
  "connections": {
    "✉️ Send via Gmail": {
      "main": [
        [
          {
            "node": "📤 Webhook Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🎨 Format HTML Email": {
      "main": [
        [
          {
            "node": "✉️ Send via Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "📥 Travel Request Webhook": {
      "main": [
        [
          {
            "node": "📝 Parse & Validate Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "📝 Parse & Validate Inputs": {
      "main": [
        [
          {
            "node": "✈️ Search Flights (Skyscanner)",
            "type": "main",
            "index": 0
          },
          {
            "node": "🏨 Search Hotels (Booking.com)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🔀 Merge Flight & Hotel Data": {
      "main": [
        [
          {
            "node": "🧮 Generate Itinerary Combinations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🏨 Search Hotels (Booking.com)": {
      "main": [
        [
          {
            "node": "🔀 Merge Flight & Hotel Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "✈️ Search Flights (Skyscanner)": {
      "main": [
        [
          {
            "node": "🔀 Merge Flight & Hotel Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "🧮 Generate Itinerary Combinations": {
      "main": [
        [
          {
            "node": "🎨 Format HTML Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

这是一个中级难度的工作流,适用于Personal Productivity、Multimodal AI等场景。适合有一定经验的用户,包含 6-15 个节点的中等复杂度工作流

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
中级
节点数量12
分类2
节点类型8
难度说明

适合有一定经验的用户,包含 6-15 个节点的中等复杂度工作流

作者
Cheng Siong Chin

Cheng Siong Chin

@cschin

Prof. Cheng Siong CHIN serves as Chair Professor in Intelligent Systems Modelling and Simulation in Newcastle University, Singapore. His academic credentials include an M.Sc. in Advanced Control and Systems Engineering from The University of Manchester and a Ph.D. in Robotics from Nanyang Technological University.

外部链接
在 n8n.io 上查看 →

分享此工作流