iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Modern Web

用 LINE OA 打造中小企業訂單系統:從零開始的 30 天實作紀錄系列 第 24

讓通知真正動起來!修復 userId 問題 + 多取貨方式邏輯升級

  • 分享至 

  • xImage
  •  

昨天我們讓訂單完成後會自動通知顧客,但實際測試時卻發現 Worker 沒有成功推播。
Log 顯示:

[ERROR] 推播失敗 (undefined): Request failed with status code 400

這代表 LINE API 拒絕了我們的請求。今天,我們要釐清這個錯誤的根源,並實作一個更聰明的 Worker,讓它能自動查詢使用者的 LINE ID,真正讓通知「動起來」。
同時,我們也會升級通知邏輯:根據不同取貨方式(自取、外送、宅配)發送不同推播訊息。


一、錯誤排查:推播失敗 (400 Bad Request)

在前一篇中,我們的 queue payload 是:

{
  "type": "order_completed",
  "orderId": "68d6ad52638e4327f4ad9553"
}

缺少了 userId,導致 Push API 不知道要通知誰。
這就是 400 Bad Request 的真正原因。


二、修正策略:讓 Worker 自動查詢 userId

既然 Order 文件中本來就有 userId 欄位,我們只要讓 Worker:
1️⃣ 讀取 queue 的 orderId
2️⃣ 查詢該訂單,並 populate('userId')
3️⃣ 從關聯的 user.lineUserId 拿到真正的 LINE 使用者 ID
4️⃣ 呼叫 Push API 發送訊息
就能讓通知成功送出。


三、改良後的 Worker 程式

// worker/notify-worker.js
require("dotenv").config();
const { ensureConnected } = require("../src/lib/redis");
const { QUEUE_KEY } = require("../src/queues/notify");
const { notifyOrderCompleted } = require("../src/utils/notifyUser");
const logger = require("../src/lib/logger");
const Order = require("../src/models/order.model");
const User = require("../src/models/user.model");
const { connectMongo } = require("../src/lib/db"); // ✅ 新增 MongoDB 連線

(async () => {
  await connectMongo(); // 先連線 MongoDB 再啟動 Worker
  const redis = await ensureConnected();
  console.log("Notify worker started.");

  while (true) {
    try {
      const res = await redis.blPop(QUEUE_KEY, 5);
      if (!res) continue;

      const jobStr = res.element;
      const payload = JSON.parse(jobStr);

      if (payload.type === "order_completed") {
        const order = await Order.findById(payload.orderId).populate("userId").lean();
        if (!order) {
          logger.error(`找不到訂單:${payload.orderId}`);
          continue;
        }

        const lineUserId = order.userId?.lineUserId;
        if (!lineUserId) {
          logger.error(`訂單 ${payload.orderId} 的使用者沒有 lineUserId`);
          continue;
        }

        logger.info(`推播給使用者 ${lineUserId} (訂單 ${payload.orderId})`);
        await notifyOrderCompleted(lineUserId, payload.orderId);
      }
    } catch (err) {
      const detail = err?.response?.data || err.message;
      logger.error(`通知任務錯誤:${JSON.stringify(detail)}`);
    }
  }
})();

✅ 改良重點:

  • queue 中只放 orderId,Worker 自動查 userId

  • 減少資料重複、讓流程更乾淨。

  • 再也不會因缺少 userId 導致 400 錯誤。


四、常見錯誤:Worker 無法連上 MongoDB

如果你遇到以下錯誤:

Operation `orders.findOne()` buffering timed out after 10000ms

代表 Worker 沒有成功連上 MongoDB。由於 Worker 是獨立進程,不會自動共用主伺服器的資料庫連線,必須手動連接。

✅ 解法:建立共用的 MongoDB 連線模組

// src/lib/db.js
const mongoose = require('mongoose');
let connected = false;

async function connectMongo() {
  if (connected) return;
  const uri = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/line-order';

  mongoose.set('strictQuery', true);
  await mongoose.connect(uri, { serverSelectionTimeoutMS: 10000 });
  connected = true;
  console.log('[Mongo] connected:', uri);
}

module.exports = { connectMongo };

然後在 Worker 開始執行時呼叫:

const { connectMongo } = require('../src/lib/db');
await connectMongo(); // ✅ 先連 Mongo,再跑 Worker

這樣 Worker 才能正確查詢 Order 資料,不會再出現 Timeout。


五、升級邏輯:多取貨方式通知

在真實場景中,不同取貨方式應該有不同通知內容,例如:

取貨方式 通知內容
自取 餐點已備妥,請至櫃台取貨 🍱
外送 餐點已出發,請保持電話暢通 🚴‍♂️
宅配 商品已出貨,預計 2–3 天內送達 📦

我們可以直接在 Worker 裡根據 order.pickup 判斷訊息內容。

Order Schema 新增取貨方式欄位

// src/models/order.model.js
pickup: { type: String, enum: ["自取", "外送", "宅配"], default: "自取" }

LIFF 表單新增選項

<label>取貨方式</label>
<select id="pickup">
  <option value="自取">自取</option>
  <option value="外送">外送</option>
  <option value="宅配">宅配</option>
</select>

Worker 中加入動態推播邏輯

const pickupMsg = {
  "自取": "您的餐點已備妥,請至櫃台取貨 🍱",
  "外送": "餐點已出發,請保持電話暢通 🚴‍♂️",
  "宅配": "商品已出貨,預計 2–3 天內送達 📦"
}[order.pickup] || "您的訂單已完成 🎉";

await notifyOrderCompleted(lineUserId, `${pickupMsg}\n訂單編號:${order._id}`);

六、測試結果

當訂單完成時,Worker 會自動:
1️⃣ 從 Redis 拿出任務。
2️⃣ 用 orderId 查詢訂單與使用者。
3️⃣ 根據取貨方式決定推播訊息。
4️⃣ 呼叫 Messaging API 推播成功。

終端輸出:

推播給使用者 U1234567890 (訂單 68d6ad52638e4327f4ad9553)
[INFO] 推播成功:通知用戶 U1234567890 訂單完成

https://ithelp.ithome.com.tw/upload/images/20251008/20178868GNzF0jZzs8.png

LINE 顯示:

🎉 您的餐點已備妥,請至櫃台取貨 🍱
訂單編號:68d6ad52638e4327f4ad9553

https://ithelp.ithome.com.tw/upload/images/20251008/20178868UXvqahAKZ1.png


七、重點回顧

  • 400 Bad Request 通常代表 payload 缺少關鍵欄位(如 userId)。

  • Worker 啟動時要記得先 connectMongo(),否則會出現 buffering timed out

  • 讓 Worker 自動查 orderId → userId → lineUserId 才是正確架構。

  • queue payload 只需最小資訊,維護更簡單。

  • 加入多取貨方式後,通知內容更貼近真實場景。

  • 系統從「能通知」正式升級為「智慧通知」。


上一篇
讓 Bot 自己說話:使用 Messaging API 自動通知顧客
系列文
用 LINE OA 打造中小企業訂單系統:從零開始的 30 天實作紀錄24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言