這篇文章將引導你使用 n8n 建立一個每週自動從你過去解過的題目中,抽選 1 題來複習,並將它發送到你的 Discord 頻道
讓我們開始吧!
我們的目標是讓這個流程每週自動執行 1 次
來到儀表板,新增一個流程「Create Workflow」

初始節點選擇排程「On a schedule」

選擇禮拜日早上 08:00

接下來,我們要從 LeetCode 的 API 獲取你所有已解答(Accepted)的題目列表
下個節點選擇「HTTP Request」去傳送網路請求

方法選擇「POST」
網址是
https://leetcode.com/graphql
「Send Body」打開,並選擇格式為「Using JSON」

請求的 JSON 填入以下資訊,「USERNAME」改為自己的
{
  "query": "query recentAcSubmissions($username: String!, $limit: Int!) { recentAcSubmissionList(username: $username, limit: $limit) { id title titleSlug timestamp } }",
  "variables": {
    "username": "USERNAME",
    "limit": 2000
  }
}
我們已經拿到了題目列表,但裡面可能包含重複的題目,而且我們只想複習那些有點久以前解過的題目
下個節點選擇「Code」,來撰寫抽選機制

程式碼如下
// 1. 取得從上個節點 (HTTP Request) 傳來的題目列表
const submissions = $input.item.json.data.recentAcSubmissionList;
// 2. 使用 Map 整理並去除重複的題目
const uniqueProblemsMap = new Map();
submissions.forEach((sub) => {
  uniqueProblemsMap.set(sub.titleSlug, {
    // 使用 .set() 方法
    title: sub.title,
    titleSlug: sub.titleSlug,
    timestamp: sub.timestamp,
  });
});
// 從 Map 的 values() 取出所有物件,並轉換成陣列
const uniqueAccepted = Array.from(uniqueProblemsMap.values());
// 3. 計算 30 天前的 Unix timestamp (單位:秒)
const now = new Date(); // 取得當前時間
const thirtyDaysInMs = 30 * 24 * 60 * 60 * 1000; // 30 天的毫秒數
const thirtyDaysAgoTimestampInSeconds = Math.floor(
  (now.getTime() - thirtyDaysInMs) / 1000
);
// 4. 進行篩選:只挑出在 30 天前解過的題目
const oldProblems = uniqueAccepted.filter(
  (p) => parseInt(p.timestamp) < thirtyDaysAgoTimestampInSeconds
);
// 5. 決定抽籤池 (Pool) - 這是防呆機制
// 如果有超過30天的舊題目,就從舊題目中抽籤;
// 如果沒有(例如你是新帳號,所有題目都在30天內解的),就從全部題目中抽,避免 workflow 出錯。
const pool = oldProblems.length > 0 ? oldProblems : uniqueAccepted;
// 6. 從最終的抽籤池中隨機挑選 1 題
const randomIndex = Math.floor(Math.random() * pool.length);
const randomProblem = pool[randomIndex];
// 7. 回傳選中的題目物件,包含所有資訊以便後續使用
return randomProblem;
最後一步,就是將我們精心挑選出來的題目發送到 Discord,提醒自己該複習了
接下來選擇「Discord」把抽選結果傳出去


「Connection Type」選擇「Webhook」,憑證的串接在之前的文章有撰寫過,這邊就不重複惹

「Message」填入以下內容
**今週 LeetCode 複習!** 🧠
**隨機抽選:** [{{ $json.title }}](https://leetcode.com/problems/{{ $json.titleSlug }}/)
**上次解題時間:** {{ DateTime.fromSeconds(parseInt($json.timestamp)).setZone('Asia/Taipei').toFormat('yyyy-MM-dd') }}
接著點選上方的「Execute step」來測試一下

Discord 看到訊息代表成功啦

記得要到設定的地方設定時區

設定為台灣時間

完成的流程會長這樣,記得到上方切換為「Active」哦

恭喜!現在你有了一個自動化的 LeetCode 複習小幫手,每週都會貼心地提醒你該溫故知新惹
最後也附上完整流程的 JSON
{
  "name": "Weekdays LeetCode",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtHour": 8
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, 0],
      "id": "e247a697-9ea9-4c94-a7c2-1f3922b5b858",
      "name": "Schedule Trigger"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://leetcode.com/graphql",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n    \"query\": \"query recentAcSubmissions($username: String!, $limit: Int!) { recentAcSubmissionList(username: $username, limit: $limit) { id title titleSlug timestamp } }\",\n    \"variables\": {\n        \"username\": \"YOUR_LEETCODE_USERNAME\",\n        \"limit\": 2000\n    }\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [220, 0],
      "id": "0b45f22a-6ec1-404d-af5c-9b6e1b5aedb8",
      "name": "HTTP Request"
    },
    {
      "parameters": {
        "jsCode": "// 1. 取得從上個節點 (HTTP Request) 傳來的題目列表\nconst submissions = $input.item.json.data.recentAcSubmissionList;\n\n// 2. 使用 Map 整理並去除重複的題目\nconst uniqueProblemsMap = new Map();\nsubmissions.forEach(sub => {\n  uniqueProblemsMap.set(sub.titleSlug, { // 使用 .set() 方法\n    title: sub.title,\n    titleSlug: sub.titleSlug,\n    timestamp: sub.timestamp\n  });\n});\n// 從 Map 的 values() 取出所有物件,並轉換成陣列\nconst uniqueAccepted = Array.from(uniqueProblemsMap.values()); \n\n// 3. 計算 30 天前的 Unix timestamp (單位:秒)\nconst now = new Date(); // 取得當前時間\nconst thirtyDaysInMs = 30 * 24 * 60 * 60 * 1000; // 30 天的毫秒數\nconst thirtyDaysAgoTimestampInSeconds = Math.floor((now.getTime() - thirtyDaysInMs) / 1000);\n\n// 4. 進行篩選:只挑出在 30 天前解過的題目\nconst oldProblems = uniqueAccepted.filter(p => parseInt(p.timestamp) < thirtyDaysAgoTimestampInSeconds);\n\n// 5. 決定抽籤池 (Pool) - 這是防呆機制\n// 如果有超過30天的舊題目,就從舊題目中抽籤;\n// 如果沒有(例如你是新帳號,所有題目都在30天內解的),就從全部題目中抽,避免 workflow 出錯。\nconst pool = oldProblems.length > 0 ? oldProblems : uniqueAccepted;\n\n// 6. 從最終的抽籤池中隨機挑選 1 題\nconst randomIndex = Math.floor(Math.random() * pool.length);\nconst randomProblem = pool[randomIndex];\n\n// 7. 回傳選中的題目物件,包含所有資訊以便後續使用\nreturn randomProblem;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [440, 0],
      "id": "84f4439a-e18e-49fb-868b-8c2c555632b9",
      "name": "Code"
    },
    {
      "parameters": {
        "authentication": "webhook",
        "content": "=**今週 LeetCode 複習!** 🧠\n\n**隨機抽選:** [{{ $json.title }}](https://leetcode.com/problems/{{ $json.titleSlug }}/)\n**上次解題時間:** {{ DateTime.fromSeconds(parseInt($json.timestamp)).setZone('Asia/Taipei').toFormat('yyyy-MM-dd') }}\n",
        "options": {}
      },
      "type": "n8n-nodes-base.discord",
      "typeVersion": 2,
      "position": [660, 0],
      "id": "fb300201-1a50-4dcf-805f-d12f225f2ea5",
      "name": "Discord"
    }
  ],
  "pinData": {},
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code": {
      "main": [
        [
          {
            "node": "Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Taipei",
    "callerPolicy": "workflowsFromSameOwner",
    "executionTimeout": -1
  },
  "versionId": "16b54aac-760b-40c3-8598-b525bb88d7d7",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "GHWECbpBcPSCRF1A",
  "tags": []
}