昨天我們讓訂單完成後會自動通知顧客,但實際測試時卻發現 Worker 沒有成功推播。
Log 顯示:
[ERROR] 推播失敗 (undefined): Request failed with status code 400
這代表 LINE API 拒絕了我們的請求。今天,我們要釐清這個錯誤的根源,並實作一個更聰明的 Worker,讓它能自動查詢使用者的 LINE ID,真正讓通知「動起來」。
同時,我們也會升級通知邏輯:根據不同取貨方式(自取、外送、宅配)發送不同推播訊息。
在前一篇中,我們的 queue payload 是:
{
"type": "order_completed",
"orderId": "68d6ad52638e4327f4ad9553"
}
缺少了 userId
,導致 Push API 不知道要通知誰。
這就是 400 Bad Request
的真正原因。
既然 Order
文件中本來就有 userId
欄位,我們只要讓 Worker:
1️⃣ 讀取 queue 的 orderId
2️⃣ 查詢該訂單,並 populate('userId')
3️⃣ 從關聯的 user.lineUserId
拿到真正的 LINE 使用者 ID
4️⃣ 呼叫 Push API 發送訊息
就能讓通知成功送出。
// 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 錯誤。
如果你遇到以下錯誤:
Operation `orders.findOne()` buffering timed out after 10000ms
代表 Worker 沒有成功連上 MongoDB。由於 Worker 是獨立進程,不會自動共用主伺服器的資料庫連線,必須手動連接。
// 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
判斷訊息內容。
// src/models/order.model.js
pickup: { type: String, enum: ["自取", "外送", "宅配"], default: "自取" }
<label>取貨方式</label>
<select id="pickup">
<option value="自取">自取</option>
<option value="外送">外送</option>
<option value="宅配">宅配</option>
</select>
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 訂單完成
LINE 顯示:
🎉 您的餐點已備妥,請至櫃台取貨 🍱
訂單編號:68d6ad52638e4327f4ad9553
400 Bad Request
通常代表 payload 缺少關鍵欄位(如 userId)。
Worker 啟動時要記得先 connectMongo()
,否則會出現 buffering timed out
。
讓 Worker 自動查 orderId → userId → lineUserId
才是正確架構。
queue payload 只需最小資訊,維護更簡單。
加入多取貨方式後,通知內容更貼近真實場景。
系統從「能通知」正式升級為「智慧通知」。