智能旅行套餐查找器 - 使用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)可能需要您自行付费。
相关工作流推荐
Qwen3-VL-8B-Thinking旅行规划器
基于Skyscanner、Booking.com和Gmail的AI优化旅行行程生成器
Set
Code
Gmail
+7
18 节点Cheng Siong Chin
Personal Productivity
来自多个招聘网站的求职自动化
使用 5 个招聘平台和 AI 简历生成器自动化求职与申请
If
Set
Code
+14
34 节点Gerald Denor
Personal Productivity
AI-Deepseek-R1t 会议差旅审批与费用授权申请
通过Deepseek AI、Gmail和Google Sheets自动化会议差旅审批
If
Set
Code
+11
24 节点Cheng Siong Chin
Document Extraction
基于标题和摘要的AI驱动Qwen-Max期刊论文生成器
Qwen-Max:从标题/摘要生成期刊论文
Set
Code
Merge
+5
19 节点Cheng Siong Chin
Content Creation
通过 OpenAI GPT-4o 进行 AI 内容生成,包含人工审核与一键批准工作流
使用GPT-4o简化内容创建,集成一键人工审核批准
If
Set
Code
+8
23 节点Cheng Siong Chin
Content Creation
竞争对手内容差距分析器:自动化网站主题映射
使用Gemini AI、Apify和Google Sheets分析竞争对手内容差距
If
Set
Code
+10
30 节点Mychel Garzon
Miscellaneous
工作流信息
难度等级
中级
节点数量12
分类2
节点类型8
作者
Cheng Siong Chin
@cschinProf. 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 上查看 →
分享此工作流