DEV Community

Cover image for 告別逾期罰單!用 n8n 免費打造全台路邊停車費自動查詢 Telegram 機器人通知
Let's Write
Let's Write

Posted on Originally published at letswrite.tw

告別逾期罰單!用 n8n 免費打造全台路邊停車費自動查詢 Telegram 機器人通知

本篇要解決的問題

最近裝的一個 App,因為變成必須要付費訂閱,才能有自動繳停車費的功能。為了避免停車費過期而多收錢,所以讓 ChatGPT 寫一個簡單的 n8n Workflow,匯入後修改一下設定資料,就可以每天自動通知還有沒有未繳的停車費。

August 希望做到以下幾件事:

  • 不用每天自己打開停車費網站查詢。
  • 可以同時查詢汽車、機車。
  • 可以設定多台車。
  • 一次查詢多個縣市。
  • 每天固定時間自動執行。
  • 有未繳停車費時,主動透過 Telegram 通知。
  • 某個縣市 API 發生錯誤時,也要告訴我,不能直接把它當成「沒有欠費」。

而且,以上全部的需求,都可以放在 同一個 n8n Workflow 裡,不需要汽車、機車各做一份。


Workflow JSON 下載、匯入

直接複製以下 n8n Workflow,接著進入 n8n,直接貼上:

{
  "name": "全台路邊停車費自動查詢|多車輛+21縣市+Telegram v4",
  "nodes": [
    {
      "parameters": {},
      "id": "9d4d1385-782c-4c2d-9c50-5b1a80cf6d83",
      "name": "手動測試",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [-1160, 160]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * *"
            }
          ]
        }
      },
      "id": "ec576177-01d7-468f-8777-196e1656a71d",
      "name": "每天 09:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [-1160, -40]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "4728964e-002f-4430-838c-6e44d2deda12",
              "name": "telegramChatId",
              "value": "請改成你的 Telegram Chat ID",
              "type": "string"
            },
            {
              "id": "990ecf7e-feaa-4a91-9118-f378adb5d4d9",
              "name": "notifyWhenClear",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "c69ea629-43b8-466a-bbb0-a5610d2d83e7",
              "name": "vehiclesJson",
              "value": "[\n  {\n    \"label\": \"我的汽車\",\n    \"carId\": \"ABC-1234\",\n    \"carType\": \"C\"\n  },\n  {\n    \"label\": \"我的機車\",\n    \"carId\": \"XYZ-5678\",\n    \"carType\": \"M\"\n  }\n]",
              "type": "string"
            },
            {
              "id": "fc5463ba-96f0-49a6-a280-3863bdd345bc",
              "name": "citiesJson",
              "value": "[\n  {\n    \"name\": \"臺北市\",\n    \"urlTemplate\": \"https://trafficapi.pma.gov.taipei/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"新北市\",\n    \"urlTemplate\": \"https://trafficapi.traffic.ntpc.gov.tw/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"桃園市\",\n    \"urlTemplate\": \"https://bill-epark.tycg.gov.tw/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"臺中市\",\n    \"urlTemplate\": \"http://tcparkingapi.taichung.gov.tw:8081/NationalParkingPayBillInquiry.Api/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true,\n    \"note\": \"來源未涵蓋:尚未確認此 :8081 API 是否支援 HTTPS,因此暫保留原 HTTP endpoint。\"\n  },\n  {\n    \"name\": \"臺南市\",\n    \"urlTemplate\": \"https://parkingbill.tainan.gov.tw/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"高雄市\",\n    \"urlTemplate\": \"https://kpp.tbkc.gov.tw/parking/V1/parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"基隆市\",\n    \"urlTemplate\": \"https://park.klcg.gov.tw/TrafficPayBill/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"新竹市\",\n    \"urlTemplate\": \"https://his.futek.com.tw:5443/TrafficPayBill/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"新竹縣\",\n    \"urlTemplate\": \"https://hcpark.hchg.gov.tw/NationalParkingPayBillInquiry/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"苗栗縣\",\n    \"urlTemplate\": \"https://miaoliparking.jotangi.com.tw/TrafficPayBill/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"彰化縣\",\n    \"urlTemplate\": \"https://chpark.chcg.gov.tw/TrafficPayBill/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"南投縣\",\n    \"urlTemplate\": \"https://parking.nantou.gov.tw/TrafficPayBill/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"雲林縣\",\n    \"urlTemplate\": \"https://parking.yunlin.gov.tw/TrafficPayBill/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"嘉義縣\",\n    \"urlTemplate\": \"https://8voc0wuf1g.execute-api.ap-southeast-1.amazonaws.com/default/chiayi/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"嘉義市\",\n    \"urlTemplate\": \"https://parking.chiayi.gov.tw/cypark/api/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"屏東縣\",\n    \"urlTemplate\": \"https://8voc0wuf1g.execute-api.ap-southeast-1.amazonaws.com/default/pingtung/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"宜蘭縣\",\n    \"urlTemplate\": \"https://billparking.e-land.gov.tw/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"花蓮縣\",\n    \"urlTemplate\": \"https://hl.parchere.com.tw/TrafficPayBill/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"臺東縣\",\n    \"urlTemplate\": \"https://tt.guoyun.com.tw/TrafficPayBill/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"澎湖縣\",\n    \"urlTemplate\": \"https://parking.penghu.gov.tw/TrafficPayBill/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"金門縣\",\n    \"urlTemplate\": \"https://km.guoyun.com.tw/TrafficPayBill/Parking/PayBill/CarID/{CarID}/CarType/{CarType}\",\n    \"enabled\": true\n  }\n]",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "cfc69392-3221-4479-b078-437991a0b749",
      "name": "設定(只改這裡)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-920, 60]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const cfg = $input.first().json;\n\nfunction parseJsonField(value, fieldName) {\n  if (Array.isArray(value)) return value;\n  try {\n    return JSON.parse(value);\n  } catch (e) {\n    throw new Error(`${fieldName} 不是合法 JSON:${e.message}`);\n  }\n}\n\nconst vehicles = parseJsonField(cfg.vehiclesJson, 'vehiclesJson');\nconst cities = parseJsonField(cfg.citiesJson, 'citiesJson');\n\nif (!Array.isArray(vehicles) || vehicles.length === 0) {\n  throw new Error('vehiclesJson 至少要有一台車。');\n}\n\nconst enabledCities = Array.isArray(cities)\n  ? cities.filter(c => c?.enabled !== false && c?.name && c?.urlTemplate)\n  : [];\n\nif (enabledCities.length === 0) {\n  throw new Error('citiesJson 至少要啟用一個縣市。');\n}\n\nconst allowedTypes = new Set(['C', 'M', 'O']);\nconst output = [];\n\nfor (const vehicle of vehicles) {\n  const carId = String(vehicle?.carId ?? '').trim();\n  const carType = String(vehicle?.carType ?? '').trim().toUpperCase();\n  const label = String(vehicle?.label ?? carId).trim();\n\n  if (!carId) throw new Error(`車輛缺少 carId:${JSON.stringify(vehicle)}`);\n  if (!allowedTypes.has(carType)) {\n    throw new Error(`${carId} 的 carType 必須是 C、M 或 O,目前為:${carType}`);\n  }\n\n  for (const city of enabledCities) {\n    const queryUrl = String(city.urlTemplate)\n      .replace('{CarID}', encodeURIComponent(carId))\n      .replace('{CarType}', encodeURIComponent(carType));\n\n    output.push({\n      json: {\n        label,\n        carId,\n        carType,\n        cityName: city.name,\n        queryUrl,\n        telegramChatId: cfg.telegramChatId ?? '',\n        notifyWhenClear: Boolean(cfg.notifyWhenClear)\n      }\n    });\n  }\n}\n\nreturn output;"
      },
      "id": "fe4cf409-253c-453c-bb87-34849dfcc386",
      "name": "建立 車輛 × 縣市 查詢清單",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-680, 60]
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "da0e1f61-1d54-4afb-82af-752252f5c50f",
      "name": "Loop Over Items(每次 1 筆)",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [-420, 60]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $json.queryUrl }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          },
          "timeout": 10000
        }
      },
      "id": "0707ce96-44ea-4630-a5fd-184d25e128bc",
      "name": "查詢停車費 API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [-160, 200],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const row = $json ?? {};\n\nconst base = {\n  cityName: row.cityName ?? '',\n  label: row.label ?? row.carId ?? '',\n  carId: row.carId ?? '',\n  carType: row.carType ?? '',\n  queryUrl: row.queryUrl ?? '',\n  telegramChatId: row.telegramChatId ?? '',\n  notifyWhenClear: Boolean(row.notifyWhenClear)\n};\n\n// Network/TLS/DNS errors continued by the HTTP Request node arrive as `error`.\nif (row.error) {\n  return {\n    json: {\n      ...base,\n      status: 'ERROR',\n      apiMessage: row.error.message || String(row.error),\n      httpStatus: null,\n      hasUnpaid: false,\n      totalCount: 0,\n      totalAmount: 0,\n      details: [],\n      isQueryFailed: true\n    }\n  };\n}\n\nconst httpStatus = Number(row.statusCode ?? 0);\nlet res = row.body ?? {};\n\nif (typeof res === 'string') {\n  try {\n    res = JSON.parse(res);\n  } catch {\n    return {\n      json: {\n        ...base,\n        status: 'ERROR',\n        apiMessage: `API 回傳非 JSON 內容${httpStatus ? `(HTTP ${httpStatus})` : ''}`,\n        httpStatus: httpStatus || null,\n        hasUnpaid: false,\n        totalCount: 0,\n        totalAmount: 0,\n        details: [],\n        isQueryFailed: true\n      }\n    };\n  }\n}\n\nif (httpStatus && (httpStatus < 200 || httpStatus >= 300)) {\n  return {\n    json: {\n      ...base,\n      status: 'ERROR',\n      apiMessage:\n        res?.Message ||\n        res?.message ||\n        `HTTP ${httpStatus}`,\n      httpStatus,\n      hasUnpaid: false,\n      totalCount: 0,\n      totalAmount: 0,\n      details: [],\n      isQueryFailed: true\n    }\n  };\n}\n\nconst status = String(res?.Status ?? '').toUpperCase();\nconst apiMessage = String(res?.Message ?? '');\n\nlet result = res?.Result ?? null;\nif (typeof result === 'string' && result.trim()) {\n  try {\n    result = JSON.parse(result);\n  } catch {\n    return {\n      json: {\n        ...base,\n        status: 'ERROR',\n        apiMessage: 'Result 欄位不是合法 JSON',\n        httpStatus: httpStatus || null,\n        hasUnpaid: false,\n        totalCount: 0,\n        totalAmount: 0,\n        details: [],\n        isQueryFailed: true\n      }\n    };\n  }\n}\n\nconst bills = Array.isArray(result?.Bills) ? result.Bills : [];\nconst reminders = Array.isArray(result?.Reminders) ? result.Reminders : [];\nconst details = [];\n\nfor (const bill of bills) {\n  details.push({\n    kind: '停車單',\n    amount: Number(bill?.PayAmount ?? bill?.Amount ?? 0),\n    parkingDate: bill?.ParkingDate ?? '',\n    dueDate: bill?.PayLimitDate ?? '',\n    billNo: bill?.BillNo ?? ''\n  });\n}\n\nfor (const reminder of reminders) {\n  details.push({\n    kind: '催繳單',\n    amount: Number(\n      reminder?.PayAmount ??\n      (Number(reminder?.Amount ?? 0) + Number(reminder?.ExtraCharge ?? 0))\n    ),\n    parkingDate: reminder?.ParkingDate ?? '',\n    dueDate: reminder?.ReminderLimitDate ?? reminder?.PayLimitDate ?? '',\n    billNo: reminder?.ReminderNo ?? reminder?.BillNo ?? ''\n  });\n}\n\nconst totalCount = Number(result?.TotalCount ?? details.length ?? 0);\nconst computedAmount = details.reduce((sum, d) => sum + Number(d.amount || 0), 0);\nconst totalAmount = Number(result?.TotalAmount ?? computedAmount);\n\nconst success = status === 'SUCCESS';\n\nreturn {\n  json: {\n    ...base,\n    status,\n    apiMessage,\n    httpStatus: httpStatus || null,\n    hasUnpaid: success && !!result && (totalCount > 0 || details.length > 0),\n    totalCount,\n    totalAmount,\n    details,\n    isQueryFailed: !success\n  }\n};"
      },
      "id": "0a7b65f9-6d37-49d4-9fd4-f4f749f73c46",
      "name": "整理單筆結果",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [320, 120]
    },
    {
      "parameters": {
        "amount": 1,
        "unit": "seconds"
      },
      "id": "aa5bc5ca-c35c-4f8a-97b4-1b29fef023a8",
      "name": "等待 1 秒(API 節流)",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [320, 200],
      "webhookId": "61c9dc0a-3b6d-49d5-b022-e6a9054e3dd3"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const rows = $input.all().map(i => i.json);\nconst first = rows[0] ?? {};\n\nconst unpaidRows = rows.filter(r => r.hasUnpaid);\nconst failedRows = rows.filter(r => r.isQueryFailed);\n\nconst totalCount = unpaidRows.reduce((s, r) => s + Number(r.totalCount || 0), 0);\nconst totalAmount = unpaidRows.reduce((s, r) => s + Number(r.totalAmount || 0), 0);\n\nconst lines = [];\n\nif (unpaidRows.length) {\n  lines.push('🚗 路邊停車費提醒', '');\n\n  for (const row of unpaidRows) {\n    const vehicleIcon = row.carType === 'M' ? '🛵' : '🚙';\n    lines.push(`${vehicleIcon}【${row.label}|${row.carId}】`);\n    lines.push(`${row.cityName}:${row.totalCount} 筆,合計 $${row.totalAmount}`);\n\n    for (const d of row.details.slice(0, 10)) {\n      const parts = [\n        d.kind,\n        `$${d.amount}`,\n        d.parkingDate ? `停車日 ${d.parkingDate}` : null,\n        d.dueDate ? `期限 ${d.dueDate}` : null\n      ].filter(Boolean);\n      lines.push(`- ${parts.join('|')}`);\n    }\n\n    if (row.details.length > 10) {\n      lines.push(`- 另有 ${row.details.length - 10} 筆未列出`);\n    }\n    lines.push('');\n  }\n\n  lines.push(`總計:${totalCount} 筆,$${totalAmount}`);\n} else {\n  lines.push('✅ 目前沒有查到待繳路邊停車費');\n}\n\nif (failedRows.length) {\n  lines.push('', `⚠️ 有 ${failedRows.length} 個查詢失敗,這些縣市「不能視為沒有欠費」:`);\n  for (const row of failedRows.slice(0, 20)) {\n    lines.push(`- ${row.label}|${row.cityName}|${row.apiMessage || row.status || '未知錯誤'}`);\n  }\n  if (failedRows.length > 20) {\n    lines.push(`- 另有 ${failedRows.length - 20} 個失敗未列出`);\n  }\n}\n\nconst shouldNotify =\n  unpaidRows.length > 0 ||\n  failedRows.length > 0 ||\n  Boolean(first.notifyWhenClear);\n\nreturn [{\n  json: {\n    telegramChatId: first.telegramChatId ?? '',\n    shouldNotify,\n    hasUnpaid: unpaidRows.length > 0,\n    totalCount,\n    totalAmount,\n    failedQueries: failedRows.length,\n    message: lines.join('\\n')\n  }\n}];"
      },
      "id": "4b6dabed-b0b9-4e24-bd69-249d9cac4bbf",
      "name": "彙總全部結果",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-160, -120]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "60315e8a-26c6-47fe-b311-0864f215d707",
              "leftValue": "={{ $json.shouldNotify }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "c44271f5-b4ec-4a02-bd5a-37e9b53cc1ab",
      "name": "需要通知?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [80, -120]
    },
    {
      "parameters": {
        "chatId": "={{ $json.telegramChatId }}",
        "text": "={{ $json.message }}",
        "additionalFields": {}
      },
      "id": "427558ca-fc5c-4d35-aa31-3e1d2286e556",
      "name": "Telegram 通知",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [320, -160]
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {}
      },
      "id": "e11fc337-e4bd-406f-9f9b-9cebc88e6e96",
      "name": "合併查詢參數與 HTTP 回應",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [80, 120]
    }
  ],
  "connections": {
    "手動測試": {
      "main": [
        [
          {
            "node": "設定(只改這裡)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "每天 09:00": {
      "main": [
        [
          {
            "node": "設定(只改這裡)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "設定(只改這裡)": {
      "main": [
        [
          {
            "node": "建立 車輛 × 縣市 查詢清單",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "建立 車輛 × 縣市 查詢清單": {
      "main": [
        [
          {
            "node": "Loop Over Items(每次 1 筆)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items(每次 1 筆)": {
      "main": [
        [
          {
            "node": "彙總全部結果",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "查詢停車費 API",
            "type": "main",
            "index": 0
          },
          {
            "node": "合併查詢參數與 HTTP 回應",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "查詢停車費 API": {
      "main": [
        [
          {
            "node": "合併查詢參數與 HTTP 回應",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "整理單筆結果": {
      "main": [
        [
          {
            "node": "等待 1 秒(API 節流)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "等待 1 秒(API 節流)": {
      "main": [
        [
          {
            "node": "Loop Over Items(每次 1 筆)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "彙總全部結果": {
      "main": [
        [
          {
            "node": "需要通知?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "需要通知?": {
      "main": [
        [
          {
            "node": "Telegram 通知",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "合併查詢參數與 HTTP 回應": {
      "main": [
        [
          {
            "node": "整理單筆結果",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Taipei"
  },
  "versionId": "1aa3b0b1-0acf-40f7-b71c-03a93d7beb1a",
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
Enter fullscreen mode Exit fullscreen mode

如果是想用匯入的方式,就把上面的程式碼複製後存成 JSON,進入 n8n:

Workflows
→ Import from File
→ 選擇剛才下載的 JSON
Enter fullscreen mode Exit fullscreen mode

不管是用複製貼上的,還是匯入,之後只需要修改 「設定(只改這裡)」 這個 Node 就可以,說明如下。

車輛設定

找到:

vehiclesJson
Enter fullscreen mode Exit fullscreen mode

例如:

[
  {
    "label": "我的汽車",
    "carId": "ABC-1234",
    "carType": "C"
  },
  {
    "label": "我的機車",
    "carId": "XYZ-5678",
    "carType": "M"
  }
]
Enter fullscreen mode Exit fullscreen mode

carType 的設定:

車種
C 汽車
M 機車
O 其他

如果有多台車,繼續往下增加即可。

例如:

[
  {
    "label": "汽車",
    "carId": "ABC-1234",
    "carType": "C"
  },
  {
    "label": "我的機車",
    "carId": "XYZ-5678",
    "carType": "M"
  },
  {
    "label": "家人的機車",
    "carId": "AAA-8888",
    "carType": "M"
  }
]
Enter fullscreen mode Exit fullscreen mode

Telegram 設定

接著修改:

telegramChatId
Enter fullscreen mode Exit fullscreen mode

填入自己的 Telegram Chat ID。

如果不知道自己的 Telegram Chat ID 是多少,只需要加入這個帳號,進行對話後就會看得到了:User Info

最後進入:

Telegram 通知
Enter fullscreen mode Exit fullscreen mode

設定自己的 Telegram Bot Credential。

如果不希望「沒有欠費」時也收到通知,可以保持:

notifyWhenClear = false
Enter fullscreen mode Exit fullscreen mode

這樣平常沒有欠費就完全不打擾。

如果沒有用 Telegram,或是想接收別的通知,那只需要把上面 Workflow 的 JSON,貼給 ChatGPT,請它改成你習慣的通知方式即可。


n8n Workflow 說明

這段是詳細說明上面那段 JSON 的各個部份,是在做什麼的。

整個 Workflow 最主要可以分成幾個部分。

每天自動執行

Workflow 預設:

每天 09:00
Enter fullscreen mode Exit fullscreen mode

自動執行一次。

另外也保留:

手動測試
Enter fullscreen mode Exit fullscreen mode

剛設定完成時,可以先按手動執行確認是否正常。


設定車輛與縣市

「設定(只改這裡)」Node 集中放:

telegramChatId
notifyWhenClear
vehiclesJson
citiesJson
Enter fullscreen mode Exit fullscreen mode

所以日後新增汽車、機車,基本上只需要改這一個地方。

citiesJson 則放各縣市停車費查詢端點。

目前這份 Workflow 已經預先放入 21 個縣市的設定;如果某個 API 日後有更換網址,也只需要修改這裡,不必重新改整個流程。

建立查詢清單

接下來的 Code Node 會把:

車輛
×
縣市
Enter fullscreen mode Exit fullscreen mode

自動組合成每一筆查詢。

例如:

我的汽車 → 臺北市
我的汽車 → 新北市
我的汽車 → 桃園市

我的機車 → 臺北市
我的機車 → 新北市
我的機車 → 桃園市
...
Enter fullscreen mode Exit fullscreen mode

所以新增車輛時,不需要增加新的 HTTP Request Node。

逐筆查詢停車費

接著使用:

Loop Over Items
Enter fullscreen mode Exit fullscreen mode

一次只處理一筆查詢。

流程是:

查詢 API
↓
整理結果
↓
等待 1 秒
↓
下一筆
Enter fullscreen mode Exit fullscreen mode

August 刻意加上 等待 1 秒,避免短時間內大量呼叫各縣市 API。

整理待繳停車費

每一個縣市回傳結果之後,Workflow 會統一整理成:

縣市
車牌
車種
待繳筆數
待繳金額
停車日期
繳費期限
停車單 / 催繳單
Enter fullscreen mode Exit fullscreen mode

所以即使不同縣市的資料來源不同,後面的通知流程仍然可以使用相同格式。

最後統一通知

全部縣市查詢完成後,再一次彙整。

例如:

🚗 路邊停車費提醒

🚙【我的汽車|ABC-1234】
臺北市:2 筆,合計 $80
- 停車單|$40|停車日 2026-09-10|期限 2026-09-25
- 停車單|$40|停車日 2026-09-11|期限 2026-09-26

🛵【我的機車|XYZ-5678】
新北市:1 筆,合計 $20
- 停車單|$20|停車日 2026-09-12|期限 2026-09-27

總計:3 筆,$100
Enter fullscreen mode Exit fullscreen mode

這樣不用收到幾十則 Telegram 訊息,而是每天最多收到一份整理好的結果。

另外,如果某個 API 查詢失敗,也會另外標示:

⚠️ 有 2 個查詢失敗,
這些縣市不能視為沒有欠費:

- 我的汽車|某縣市|查詢失敗
- 我的機車|某縣市|Timeout
Enter fullscreen mode Exit fullscreen mode

這點很重要,因為「API 查不到」跟「沒有停車費」是兩件完全不同的事情。

最後把 Workflow 啟用後,基本上就不用再管它了。

之後只要有未繳的路邊停車費,就會自動收到 Telegram 通知。

Top comments (0)