8
n8n 中文网amn8n.com

自动化多平台游戏优惠追踪

中级

这是一个Personal Productivity领域的自动化工作流,包含 11 个节点。主要使用 If, Cron, Gmail, Sqlite, Function 等节点。 使用 Deku Deals 和 Gmail 提醒自动追踪多平台游戏优惠

前置要求
  • Google 账号和 Gmail API 凭证
  • 可能需要目标 API 的认证凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移

无法加载工作流预览

导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "nodes": [
    {
      "name": "每日检查(上午 8 点)",
      "type": "n8n-nodes-base.cron",
      "notes": {
        "text": "### 1. Daily Check (8 AM)\n\nThis `Cron` node triggers the workflow automatically every **day at 8:00 AM** (based on your n8n server's local time zone). This allows for frequent checks for new deals.\n\n**To change the schedule:** Adjust the 'Hour' and 'Minute' fields to your preferred time and frequency.",
        "position": "right"
      },
      "position": [
        240,
        300
      ],
      "parameters": {
        "mode": "everyDay",
        "value": {
          "hour": [
            8
          ],
          "minute": [
            0
          ]
        },
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "name": "获取 Deku Deals 页面",
      "type": "n8n-nodes-base.httpRequest",
      "notes": {
        "text": "### 2. Fetch Deku Deals Page\n\nThis `HTTP Request` node downloads the HTML content of Deku Deals' 'Most Popular' page.\n\n**Setup:**\n1.  **URL:** The URL is pre-filled for Deku Deals' 'Most Popular' deals. You can change this to `/deals` or another specific section if you prefer.\n2.  **Response Format:** Ensure this is set to `string` (for HTML content).\n\n**Considerations:**\n* Deku Deals is generally well-structured for scraping. If their layout changes, the `HTML Extract` node will need updates.",
        "position": "right"
      },
      "position": [
        480,
        300
      ],
      "parameters": {
        "url": "https://www.dekudeals.com/most-popular",
        "options": {},
        "responseFormat": "string"
      },
      "typeVersion": 3
    },
    {
      "name": "提取每个交易卡片",
      "type": "n8n-nodes-base.htmlExtract",
      "notes": {
        "text": "### 3. Extract Each Deal Card\n\nThis `HTML Extract` node first extracts each individual 'game card' HTML block. This makes it easier to process each deal independently in the next step.\n\n**Setup:**\n1.  **HTML:** This field is already set to `{{ $node[\"Fetch Deku Deals Page\"].json.data }}`.\n2.  **Selector:** `div.game-card` (This targets the main container for each deal).\n3.  **Attribute:** `html` (to get the full HTML content of each card).\n\n**If the layout changes:** You might need to update `div.game-card` to the new wrapper for individual game deals.",
        "position": "right"
      },
      "position": [
        720,
        300
      ],
      "parameters": {
        "html": "={{ $node[\"Fetch Deku Deals Page\"].json.data }}",
        "extractOperations": [
          {
            "options": {},
            "selector": "div.game-card",
            "attribute": "html",
            "operation": "extract",
            "propertyName": "dealHtml"
          }
        ]
      },
      "typeVersion": 1
    },
    {
      "name": "解析交易详情(函数代码)",
      "type": "n8n-nodes-base.function",
      "notes": {
        "text": "### 4. Parse Deal Details (Function Code)\n\nThis `Function` node dives into the HTML of each deal card to extract the specific details (title, price, link, platform, etc.). It also creates a `dealUniqueId` for tracking.\n\n**Setup:**\n* This node uses JavaScript with `jsdom` (an n8n built-in library for parsing HTML) to select individual elements within each `dealHtml` item.\n* **Crucially, the selectors here (`div.name > a`, `div.price.current`, etc.) are also subject to website changes.** You might need to update them if the layout of `div.game-card` itself changes.\n\n**Output:** Each item will now have fields like `dealUniqueId`, `gameTitle`, `gameLink`, `platforms`, `currentPrice`, `originalPrice`, `discount`.",
        "position": "right"
      },
      "position": [
        960,
        300
      ],
      "parameters": {
        "options": {},
        "function": "const { JSDOM } = require('jsdom');\n\nconst deals = [];\n\nfor (const item of items) {\n  const dom = new JSDOM(item.json.dealHtml);\n  const doc = dom.window.document;\n\n  const titleElement = doc.querySelector('div.name > a');\n  const linkElement = doc.querySelector('div.name > a');\n  const currentPriceElement = doc.querySelector('div.price-wrapper > div.price.current');\n  const originalPriceElement = doc.querySelector('div.price-wrapper > div.price.original');\n  const discountElement = doc.querySelector('div.price-wrapper > div.price.discount');\n  const platformElements = doc.querySelectorAll('div.platforms > span.platform');\n\n  const gameTitle = titleElement ? titleElement.textContent.trim() : 'N/A';\n  const gameLink = linkElement ? `https://www.dekudeals.com${linkElement.getAttribute('href')}` : 'N/A';\n  const currentPrice = currentPriceElement ? currentPriceElement.textContent.trim() : 'N/A';\n  const originalPrice = originalPriceElement ? originalPriceElement.textContent.trim() : 'N/A';\n  const discount = discountElement ? discountElement.textContent.trim() : 'N/A';\n  const platforms = Array.from(platformElements).map(p => p.textContent.trim()).join(', ');\n\n  // Create a unique ID for the deal for tracking in SQLite\n  // Combination of title, platform, and current price should be fairly unique\n  const dealUniqueId = `${gameTitle}|${platforms}|${currentPrice}`;\n\n  deals.push({\n    json: {\n      dealUniqueId: dealUniqueId,\n      gameTitle: gameTitle,\n      gameLink: gameLink,\n      platforms: platforms,\n      currentPrice: currentPrice,\n      originalPrice: originalPrice,\n      discount: discount\n    }\n  });\n}\n\nreturn deals;"
      },
      "typeVersion": 1
    },
    {
      "name": "SQLite:确保表存在",
      "type": "n8n-nodes-base.sqlite",
      "notes": {
        "text": "### 5. SQLite: Ensure Table Exists\n\nThis `SQLite` node ensures that a local database table named `notified_deals` exists. This table will store the unique IDs of deals you've already been notified about.\n\n**Setup:**\n* **Database:** `dekudeals` (this creates a file `dekudeals.db` in your n8n data directory).\n* **Query:** `CREATE TABLE IF NOT EXISTS notified_deals (...)` as shown.\n\n**No further action needed**; this node will run automatically. On subsequent runs, it will simply confirm the table exists.",
        "position": "right"
      },
      "position": [
        1200,
        220
      ],
      "parameters": {
        "query": "CREATE TABLE IF NOT EXISTS notified_deals (deal_id TEXT PRIMARY KEY, game_title TEXT, platforms TEXT, current_price TEXT, original_price TEXT, discount TEXT, deal_link TEXT, notified_date TEXT)",
        "database": "dekudeals"
      },
      "typeVersion": 1
    },
    {
      "name": "SQLite:检查是否已通知",
      "type": "n8n-nodes-base.sqlite",
      "notes": {
        "text": "### 6. SQLite: Check if Deal Already Notified\n\nThis `SQLite` node checks if each extracted deal (using its `dealUniqueId`) is already present in your `notified_deals` database.\n\n**Setup:**\n* **Database:** `dekudeals`\n* **Query:** `SELECT deal_id FROM notified_deals WHERE deal_id = '{{ $json.dealUniqueId }}'` (It looks for a match for the current deal's unique ID).\n\n**Output:** If the deal is found, this node will output an item. If not, it will output no item, which is crucial for the 'Item Lists' node below.",
        "position": "right"
      },
      "position": [
        1440,
        300
      ],
      "parameters": {
        "query": "SELECT deal_id FROM notified_deals WHERE deal_id = '{{ $json.dealUniqueId }}'",
        "database": "dekudeals"
      },
      "typeVersion": 1
    },
    {
      "name": "拆分为已通知/新交易",
      "type": "n8n-nodes-base.itemLists",
      "notes": {
        "text": "### 7. Split into Notified/New\n\nThis `Item Lists` node takes the results from the 'SQLite: Check if Notified' node and splits them into two paths:\n* **Original items without a matching ID in the database:** These are **NEW** deals (`Output 1`).\n* **Original items with a matching ID in the database:** These are **ALREADY NOTIFIED** deals (`Output 2`).\n\n**No configuration needed**; it automatically separates the items based on whether the `SQLite` query found a match or not.",
        "position": "right"
      },
      "position": [
        1680,
        300
      ],
      "parameters": {
        "mode": "splitInBatches",
        "property": "dealUniqueId"
      },
      "typeVersion": 1
    },
    {
      "name": "如果(发现新交易)",
      "type": "n8n-nodes-base.if",
      "notes": {
        "text": "### 8. If (New Deals Found)\n\nThis `If` node checks if there are any *new* deals (i.e., items coming from the 'New' path of the 'Split into Notified/New' node).\n\n* **'True' branch:** If new deals are found, the workflow proceeds to insert them into the database and send a notification.\n* **'False' branch:** If no new deals are found, the workflow ends here (no notification needed).\n\n**No configuration needed**; it automatically checks if there are any items.",
        "position": "right"
      },
      "position": [
        1920,
        220
      ],
      "parameters": {
        "conditions": [
          {
            "value1": "={{ $json.length }}",
            "value2": "0",
            "operation": "notEqual"
          }
        ]
      },
      "typeVersion": 1
    },
    {
      "name": "SQLite:插入新交易",
      "type": "n8n-nodes-base.sqlite",
      "notes": {
        "text": "### 9. SQLite: Insert New Deals\n\nThis `SQLite` node inserts the details of the newly found deals into your `notified_deals` database. This ensures you won't be notified about them again on subsequent runs.\n\n**Setup:**\n* **Database:** `dekudeals`\n* **Query:** The `INSERT INTO` query is pre-filled, saving all the extracted deal details.\n\n**No further action needed**; it automatically stores the new deal information.",
        "position": "right"
      },
      "position": [
        2160,
        140
      ],
      "parameters": {
        "query": "INSERT INTO notified_deals (deal_id, game_title, platforms, current_price, original_price, discount, deal_link, notified_date) VALUES ('{{ $json.dealUniqueId }}', '{{ $json.gameTitle }}', '{{ $json.platforms }}', '{{ $json.currentPrice }}', '{{ $json.originalPrice }}', '{{ $json.discount }}', '{{ $json.gameLink }}', '{{ new Date().toISOString() }}')",
        "database": "dekudeals"
      },
      "typeVersion": 1
    },
    {
      "name": "格式化通知消息",
      "type": "n8n-nodes-base.function",
      "notes": {
        "text": "### 10. Format Notification Message\n\nThis `Function` node takes the new deal details and formats them into a clear, readable message for your notification (e.g., email or Telegram).\n\n**Customization:**\n* You can change the introductory text, add more emojis, or adjust the display format of each deal. It dynamically adds 'Was' and 'Discount' info only if available.\n\n**No configuration needed if your property names match the previous node's output.**",
        "position": "right"
      },
      "position": [
        2400,
        140
      ],
      "parameters": {
        "options": {},
        "function": "let message = \"🎮 **New Game Deals on Deku Deals!** 🎮\\n\\n\";\n\nfor (const item of items) {\n  message += `**${item.json.gameTitle}** (${item.json.platforms})\\n` +\n             `  Current Price: **${item.json.currentPrice}**` +\n             (item.json.originalPrice !== 'N/A' ? ` (Was: ${item.json.originalPrice})` : '') +\n             (item.json.discount !== 'N/A' ? ` | Discount: ${item.json.discount}` : '') +\n             `\\n  Claim/View Deal: ${item.json.gameLink}\\n\\n`;\n}\n\nmessage += \"Check out all deals: https://www.dekudeals.com/most-popular\";\n\nreturn [{ json: { notificationMessage: message } }];"
      },
      "typeVersion": 1
    },
    {
      "name": "发送邮件通知",
      "type": "n8n-nodes-base.gmail",
      "notes": {
        "text": "### 11. Send Email Notification\n\nThis `Gmail` node sends the formatted notification message about the new game deals.\n\n**Setup:**\n1.  **Gmail Credential:** Select your Gmail API credential.\n2.  **From Email:** Enter your Gmail address (must match the authenticated account).\n3.  **To Email:** **IMPORTANT: Change `YOUR_RECIPIENT_EMAIL@example.com` to your actual email address!**\n4.  **Subject & Text:** These fields pull the formatted message from the previous node.\n\n**To switch to Telegram/Slack/Discord:** Simply replace this node with your preferred notification service's node, and map the `notificationMessage` to its text field. You'll need to set up credentials for that service.",
        "position": "right"
      },
      "position": [
        2640,
        140
      ],
      "parameters": {
        "text": "={{ $json.notificationMessage }}",
        "options": {},
        "subject": "🎮 New Game Deals Alert! (Deku Deals)",
        "toEmail": "YOUR_RECIPIENT_EMAIL@example.com",
        "fromEmail": "YOUR_GMAIL_EMAIL@gmail.com"
      },
      "credentials": {
        "gmailApi": {
          "id": "YOUR_GMAIL_CREDENTIAL_ID",
          "resolve": false
        }
      },
      "typeVersion": 2
    }
  ],
  "pinData": {},
  "version": 1,
  "connections": {
    "Daily Check (8 AM)": {
      "main": [
        [
          {
            "node": "Fetch Deku Deals Page",
            "type": "main"
          }
        ]
      ]
    },
    "If (New Deals Found)": {
      "main": [
        [
          {
            "node": "SQLite: Insert New Deals",
            "type": "main"
          }
        ],
        []
      ]
    },
    "Fetch Deku Deals Page": {
      "main": [
        [
          {
            "node": "Extract Each Deal Card",
            "type": "main"
          }
        ]
      ]
    },
    "Extract Each Deal Card": {
      "main": [
        [
          {
            "node": "Parse Deal Details (Function Code)",
            "type": "main"
          }
        ]
      ]
    },
    "Split into Notified/New": {
      "main": [
        [
          {
            "node": "If (New Deals Found)",
            "type": "main"
          }
        ],
        []
      ]
    },
    "SQLite: Insert New Deals": {
      "main": [
        [
          {
            "node": "Format Notification Message",
            "type": "main"
          }
        ]
      ]
    },
    "SQLite: Check if Notified": {
      "main": [
        [
          {
            "node": "Split into Notified/New",
            "type": "main"
          }
        ]
      ],
      "output": [
        {
          "type": "item",
          "toIndex": 0,
          "fromIndex": 0,
          "destination": [
            {
              "node": "Split into Notified/New",
              "input": "input2"
            }
          ]
        }
      ]
    },
    "Format Notification Message": {
      "main": [
        [
          {
            "node": "Send Email Notification",
            "type": "main"
          }
        ]
      ]
    },
    "SQLite: Ensure Table Exists": {
      "main": [
        [
          {
            "node": "SQLite: Check if Notified",
            "type": "main"
          }
        ]
      ]
    },
    "Parse Deal Details (Function Code)": {
      "main": [
        [
          {
            "node": "SQLite: Check if Notified",
            "type": "main"
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

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

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

中级 - 个人效率

需要付费吗?

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

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

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

作者
Piotr Sobolewski

Piotr Sobolewski

@piotrsobolewski

AI PhD with 7 years experience as a game dev CEO, currently teaching, helping others and building something new.

外部链接
在 n8n.io 查看

分享此工作流