iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
Modern Web

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

打造完整訂單流程:商品 → 數量 → 確認 → 送出

  • 分享至 

  • xImage
  •  

昨天我們用 Flex Message 製作了商品選單,今天要把選單和訂單邏輯串起來,讓顧客可以完成一次完整的下單流程。這包含:選商品 → 選數量 → 確認訂單 → 送出。本文將帶你設計互動邏輯,並實作對應的訊息回覆。


流程設計思路

  1. 使用者從 Flex Message 點擊商品。

  2. Bot 問使用者要幾份。

  3. 使用者輸入或選擇數量。

  4. Bot 回覆訂單確認訊息(商品、數量、金額)。

  5. 使用者按下「確認」後,系統進行訂單寫入(Day 11 再串接 DB)。

這裡會遇到一個挑戰:我們必須記得使用者目前進行到哪一步。舉例來說:

  • A 使用者正在輸入數量。

  • B 使用者已經到確認階段。

  • C 使用者才剛點商品。

為了不搞混不同使用者的流程,我們需要「狀態管理」。

流程示意圖

https://ithelp.ithome.com.tw/upload/images/20250924/20178868x991805itW.png


為什麼需要狀態管理?

  • 每個使用者可能在不同流程階段。

  • 必須記錄「此使用者目前在第幾步」。

  • 可以先用簡單的 in-memory 狀態(物件暫存)實作。

  • 之後會用 Redis 優化(Day 19)。

in-memory 狀態的意思?

  • in-memory 就是「存在程式記憶體裡」的資料。

  • 當我們在 Node.js 裡宣告一個物件,例如:

    const userState = {};

    這個物件只會活在伺服器程式執行的記憶體裡。

  • 所以「in-memory 狀態」就是:

    • 用變數、物件或陣列,直接在記憶體裡暫存使用者的流程狀態。

    • 程式還在跑 → 狀態就存在。

    • 程式重啟、伺服器掛掉 → 狀態就消失。


實作步驟

Step 1:商品選擇

Flex Message 按鈕 action 建議用 postback,避免聊天室充滿文字:

{
  "type": "button",
  "action": {
    "type": "postback",
    "label": "下單",
    "data": "action=order&item=紅茶拿鐵&price=60"
  }
}

什麼是 postback?

當使用者點擊按鈕時,不會在聊天室顯示文字,而是直接把一段「隱藏的資料」送到你的伺服器 Webhook。

換句話說:

  • message action:使用者點按鈕後,聊天室會顯示一段文字(例如「我要紅茶拿鐵」)。

  • postback action:使用者點按鈕後,聊天室 不會顯示文字,但伺服器會收到一筆事件(event),裡面包含你設定的 data

Step 2:詢問數量

Bot 收到 postback event:

你選擇了 紅茶拿鐵 ($60),請輸入數量

Step 3:確認訂單

使用者輸入數字後,Bot 回覆:

你要訂購:紅茶拿鐵 x 2,共 $120
請輸入「確認」送出訂單,或輸入「取消」

Step 4:送出訂單

  • 使用者輸入「確認」 → Bot 回覆「訂單已送出!」(Day 11 再寫 DB)。

  • 使用者輸入「取消」 → Bot 回覆「訂單已取消」。

https://ithelp.ithome.com.tw/upload/images/20250924/20178868OxE0W774cG.png


程式碼範例

const userState = {}; // 簡單的記憶體暫存

/**
 * 處理 LINE Webhook 傳入的 event。
 * - 支援兩種情境:
 *   1) postback:使用者在 Flex Button 點「下單」後,帶 action/item/price 進來
 *   2) message(text):依照使用者當前狀態(輸入數量/確認或取消)往下走
 *
 * 需求環境假設:
 * - client:已初始化的 LINE Messaging API 客戶端
 * - userState:簡單的 in-memory 狀態儲存 { [userId]: { item, price, step, quantity? } }
 *   注意:未來將用 Redis/DB 持久化並做逾時機制
 */
function handleEvent(event) {
  // 情境一:使用者從 Flex Message 的「下單」按鈕觸發 postback
  if (event.type === "postback") {
    // 解析 postback data(如 "action=order&item=紅茶拿鐵&price=60")
    const data = new URLSearchParams(event.postback.data);

    // 確認是下單動作
    if (data.get("action") === "order") {
      // 取出商品名稱與價格(⚠️實務上 price 不應信任,應以伺服端商品表/DB為準)
      const item = data.get("item");
      const price = data.get("price");

      // 以 userId 當 key,為該使用者建立/更新訂單狀態
      userState[event.source.userId] = {
        item,
        price: Number(price), // 確保是數字
        step: "askQuantity",  // 下一步:請使用者輸入數量
      };

      // 回覆訊息:提示輸入數量
      return client.replyMessage(event.replyToken, {
        type: "text",
        text: `你選擇了 ${item} ($${price}),請輸入數量`,
      });
    }

  // 情境二:一般文字訊息事件(使用者回傳數量、或輸入「確認/取消」)
  } else if (event.type === "message" && event.message.type === "text") {
    // 讀取該使用者的狀態(可能 undefined)
    const state = userState[event.source.userId];

    // 方便除錯:印出目前狀態步驟
    console.log("目前該筆訂單狀態:", state?.step);

    // 步驟:輸入數量
    // 條件:state 存在、step 是 askQuantity,且訊息是數字
    if (state?.step === "askQuantity" && !isNaN(event.message.text)) {
      // 轉成整數數量
      state.quantity = parseInt(event.message.text, 10);

      // 切到下一步:確認
      state.step = "confirm";

      // 回覆:顯示品項 x 數量 與總價,並提示輸入「確認」或「取消」
      return client.replyMessage(event.replyToken, {
        type: "text",
        text: `你要訂購:${state.item} x ${state.quantity},共 $${state.price * state.quantity}\n請輸入「確認」或「取消」`,
      });

    // 步驟:確認或取消
    } else if (state?.step === "confirm") {
      // 使用者輸入「確認」→ 送出訂單
      if (event.message.text === "確認") {
        // 清除此使用者的暫存狀態(避免殘留)
        delete userState[event.source.userId];

        // 回覆:訂單已送出
        return client.replyMessage(event.replyToken, {
          type: "text",
          text: "訂單已送出!",
        });

      // 使用者輸入「取消」→ 取消訂單
      } else if (event.message.text === "取消") {
        // 清除此使用者的暫存狀態
        delete userState[event.source.userId];

        // 回覆:訂單已取消
        return client.replyMessage(event.replyToken, {
          type: "text",
          text: "訂單已取消",
        });
      }
    }
  }

  // 其他情況(沒有狀態或流程剛開始):回傳商品選單(Flex Carousel)
  // 讓使用者可以直接點「下單」按鈕,觸發 postback 帶入 action/item/price
  return client.replyMessage(event.replyToken, {
    type: "flex",
    altText: "商品選單", // 無法顯示 Flex 的裝置會看到這段文字
    contents: {
      type: "carousel",
      contents: [
        // 商品卡片 1:紅茶拿鐵
        {
          type: "bubble",
          body: {
            type: "box",
            layout: "vertical",
            contents: [
              { type: "text", text: "紅茶拿鐵", weight: "bold", size: "xl" },
              { type: "text", text: "$60", color: "#AAAAAA", size: "sm" },
            ],
          },
          footer: {
            type: "box",
            layout: "horizontal",
            contents: [
              {
                type: "button",
                action: {
                  type: "postback",
                  label: "下單",
                  // 這裡把訂購資訊放在 data;點下會以 postback 事件回到本函式最上方
                  data: "action=order&item=紅茶拿鐵&price=60",
                },
              },
            ],
          },
        },
        // 商品卡片 2:起司蛋餅
        {
          type: "bubble",
          body: {
            type: "box",
            layout: "vertical",
            contents: [
              { type: "text", text: "起司蛋餅", weight: "bold", size: "xl" },
              { type: "text", text: "$45", color: "#AAAAAA", size: "sm" },
            ],
          },
          footer: {
            type: "box",
            layout: "horizontal",
            contents: [
              {
                type: "button",
                action: {
                  type: "postback",
                  label: "下單",
                  data: "action=order&item=起司蛋餅&price=45",
                },
              },
            ],
          },
        },
      ],
    },
  });
}


開發小提醒

  • 建議使用 postback,比 message 更適合流程控制。

  • 這裡先用 in-memory 狀態,後續會改用 Redis。

  • 還沒寫入 MongoDB,Day 11 會把訂單記錄到資料庫。


總結與重點回顧

今天我們完成了:

  • 串接商品 → 數量 → 確認 → 送出的完整流程。

  • 建立最簡單的使用者狀態管理。

  • 學會處理 postback 與文字輸入的結合。

重點回顧

  • 訂單流程需要狀態管理。

  • 用 postback 可以避免聊天室充滿雜訊。

  • 「確認 / 取消」是最小可行的訂單控制。

明天(Day 11),我們要把訂單流程真正「寫進 MongoDB」,完成資料落地!


上一篇
讓商品直接出現在 LINE!用 Flex Message 打造互動選單 🛒
系列文
用 LINE OA 打造中小企業訂單系統:從零開始的 30 天實作紀錄10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言