# **【LINE Bot + Google Sheets, 部署到 Reader】** :::info - 設置 Google Cloud Platform (GCP) - 設置 Line Bot - Project - 本地端部署 ngrok - 雲端部署 Render ::: 朋友是二手相機店老闆,當業務想知道客戶近期交易資訊時,步驟會是:開電腦 -> 開報表 -> 篩選客戶姓名 -> 查看,有點麻煩 如果做一個 Line Bot 呢? 只需要輸入客戶姓名+單一編號(防止同名),回傳近期幾筆交易資料,就可以解決了吧? 我創建了一個假的數據表 ![截圖 2025-06-09 23.37.35](https://hackmd.io/_uploads/r1eCktVXxx.png) <br/> ### 設置 Google Cloud Platform (GCP) 要去把 google sheet 的 API 授權打開 [Google Cloud Platform](https://console.cloud.google.com/welcome?inv=1&invt=AbzqrA&project=cameralinebot) 上方新增專案,這裡設置為 cameraLINEBot ![截圖 2025-06-09 23.40.32](https://hackmd.io/_uploads/r11qgYNXxe.png) 左邊導航欄 -> API 與服務 -> 啟用 API 和服務 ![截圖 2025-06-09 23.42.46](https://hackmd.io/_uploads/Sy-UbKE7xl.png) 在上面搜尋,把 google sheet api、google cloud api 打開 ![截圖 2025-06-09 23.43.00](https://hackmd.io/_uploads/HJc8ZtNXee.png) ![截圖 2025-06-09 23.43.17](https://hackmd.io/_uploads/H11vZYV7gx.png) 左邊導航欄 -> IAM 與管理員 -> 服務帳戶 -> (上方) +建立服務帳戶 ![截圖 2025-06-09 23.45.02](https://hackmd.io/_uploads/S1oyftNQlx.png) ![截圖 2025-06-09 23.47.15](https://hackmd.io/_uploads/ByXfGtEQel.png) 選取角色選單 檢視者:可以查看專案中的所有資源,但不允許修改 編輯者:可以從 Bot 寫入 Sheet ![截圖 2025-06-09 23.47.55](https://hackmd.io/_uploads/SkjIGtVQll.png) 創建成功後點進去 -> 新增鍵 -> 建立私密金鑰 :+1: PS 這個電子郵件複製下來,等等會用到 ![截圖 2025-06-10 00.02.23](https://hackmd.io/_uploads/H1_pHtVQeg.png) ![截圖 2025-06-10 00.02.37](https://hackmd.io/_uploads/ByCprY47lg.png) ![截圖 2025-06-10 00.05.00](https://hackmd.io/_uploads/HJ5LUFN7ge.png) 會下載到一個 json檔,放到跟後面 app.py 同一資料夾下 ![截圖 2025-06-10 00.03.45](https://hackmd.io/_uploads/HkPf8Y4Xgx.png) :+1: PS 剛才複製的電子郵件設置為 google sheet 的共用編輯者 ![截圖 2025-06-10 00.09.12](https://hackmd.io/_uploads/ByfIDt4mlg.png) <br/> ### 設置 Line Bot 登入 [Line Developers](https://developers.line.biz/zh-hant/) 創一個 provider -> create new channel 選 Messaging API -> 填寫 Channel 資訊 ![截圖 2025-06-10 00.47.48](https://hackmd.io/_uploads/S1wUe5Vmxl.png) ![截圖 2025-06-10 00.48.45](https://hackmd.io/_uploads/H1zqe5V7lx.png) ```= Channel type: Messaging API Provider: (選剛才創建的 Provider) Channel icon: Bot 的頭像 Channel name: Bot 的名稱 Channel description: Bot 的功能 Category / Subcategory: 選擇適合的分類 Email address: 信箱 ``` 創建成功後 Messaging API 滑到最下面的 Channel access token,點issue,把 token 記下來後面會用到 ![截圖 2025-06-10 00.51.48](https://hackmd.io/_uploads/HJErbqV7ll.png) ![1749488022406](https://hackmd.io/_uploads/H1P3-5Emgx.jpg) Basic settings 滑到最下面 Channel secret,記下來後面會用到 ![1749488158136](https://hackmd.io/_uploads/HkavzcE7ge.jpg) <br/> ### Project ```= linebot_camera_shop/ ├── app.py ├── 金鑰.json └── requirements.txt ``` 套件記得裝 ```= pip install Flask line-bot-sdk gspread oauth2client ``` requirements.txt ```= Flask==3.0.3 line-bot-sdk==3.9.0 gspread==6.0.0 oauth2client==4.1.2 ``` app.py 把剛才記下來的 Channel access token、Channel secret 貼到文件 ```= import os from flask import Flask, request, abort import gspread import json # --- Line Bot SDK v2 --- from linebot import LineBotApi, WebhookHandler from linebot.exceptions import InvalidSignatureError from linebot.models import MessageEvent, TextMessage, TextSendMessage app = Flask(__name__) LINE_CHANNEL_ACCESS_TOKEN = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', '@@@') LINE_CHANNEL_SECRET = os.getenv('LINE_CHANNEL_SECRET', '@@@') line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN) handler = WebhookHandler(LINE_CHANNEL_SECRET) # --- Google Sheet --- GOOGLE_SHEET_CREDENTIALS = 'cameralinebot-5943c988fba3.json' GOOGLE_SHEET_NAME = 'camera_客戶交易紀錄' GOOGLE_WORKSHEET_NAME = '2024_上半年' # --- Google Sheet 認證與開啟 --- try: gc = gspread.service_account(filename=GOOGLE_SHEET_CREDENTIALS) spreadsheet = gc.open(GOOGLE_SHEET_NAME) worksheet = spreadsheet.worksheet(GOOGLE_WORKSHEET_NAME) print("成功連接到 Google Sheet!") except Exception as e: print(f"連接 Google Sheet 失敗: {e}") worksheet = None # --- Line Bot Webhook --- @app.route("/callback", methods=['POST']) def callback(): signature = request.headers['X-Line-Signature'] body = request.get_data(as_text=True) app.logger.info("Request body: " + body) try: handler.handle(body, signature) except InvalidSignatureError: abort(400) return 'OK' # @handler.add(MessageEvent, message=TextMessage) def handle_message(event): text = event.message.text.strip() reply_message = "請輸入 '查詢 客戶姓名 客戶ID' 來查詢交易紀錄。" if text.startswith('查詢'): parts = text.split(' ', 2) if len(parts) == 3: query_name = parts[1] query_uuid = parts[2] reply_message = search_customer_transactions(query_name, query_uuid) else: reply_message = "指令格式不正確。\n請輸入 '查詢 客戶姓名 客戶ID'。" line_bot_api.reply_message( event.reply_token, TextSendMessage(text=reply_message) ) def search_customer_transactions(customer_name, customer_uuid): if worksheet is None: return "很抱歉,無法連接到交易資料庫,請稍後再試或聯繫管理員。" try: # get_all_records() 會將第一行作為鍵 (Header) all_records = worksheet.get_all_records() found_transactions = [] for r in all_records: sheet_name = r.get('客戶姓名') sheet_uuid = r.get('客戶uuid') if sheet_name and sheet_uuid and \ str(sheet_name).strip() == customer_name.strip() and \ str(sheet_uuid).strip() == customer_uuid.strip(): found_transactions.append(r) if not found_transactions: return f"找不到客戶 {customer_name} ({customer_uuid}) 的交易紀錄。" try: found_transactions.sort(key=lambda x: x.get('日期', ''), reverse=True) except TypeError: pass # --- 找出所有未結清的交易 --- unsettled_transactions = [] for r in found_transactions: is_settled_value = str(r.get('是否結清', '')).strip().lower() if is_settled_value == 'false' or is_settled_value == '否': # 您可以根據實際情況添加更多判斷條件 unsettled_transactions.append(r) # 取最近三筆 recent_three_transactions = found_transactions[:3] response_messages = [] # 添加最近三筆交易的標題和內容 if recent_three_transactions: response_messages.append("--- 最近三筆交易紀錄 ---") for r in recent_three_transactions: msg = ( f"📅 日期:{r.get('日期', 'N/A')}\n" f"N 交易編號:{r.get('交易編號', 'N/A')}\n" # <-- 這裡已經有交易編號 f"👤 客戶:{r.get('客戶姓名', 'N/A')}({r.get('客戶uuid', 'N/A')})\n" f"📌 細項:{r.get('細項', 'N/A')}\n" f"🎞 產品:{r.get('產品', 'N/A')}\n" f"💰 標價:{r.get('標價', 'N/A')}\n" f"💸 客戶收支:{r.get('客戶收支', 'N/A')}\n" f"🏪 店家實收:{r.get('店家實收', 'N/A')}\n" f"$ 餘額:{r.get('餘額', 'N/A')}\n" f"✅ 是否結清:{r.get('是否結清', 'N/A')}" ) response_messages.append(msg) response_messages.append("---") # # 如果有未結清的交易且與最近三筆沒有完全重疊 recent_three_ids = {json.dumps(t, sort_keys=True) for t in recent_three_transactions} actual_unsettled_to_show = [] for t in unsettled_transactions: if json.dumps(t, sort_keys=True) not in recent_three_ids: actual_unsettled_to_show.append(t) if actual_unsettled_to_show: response_messages.append("\n--- 未結清交易紀錄 ---") for r in actual_unsettled_to_show: msg = ( f"📅 日期:{r.get('日期', 'N/A')}\n" f"N 交易編號:{r.get('交易編號', 'N/A')}\n" f"👤 客戶:{r.get('客戶姓名', 'N/A')}({r.get('客戶uuid', 'N/A')})\n" f"📌 細項:{r.get('細項', 'N/A')}\n" f"🎞 產品:{r.get('產品', 'N/A')}\n" f"💰 標價:{r.get('標價', 'N/A')}\n" f"💸 客戶收支:{r.get('客戶收支', 'N/A')}\n" f"🏪 店家實收:{r.get('店家實收', 'N/A')}\n" f"$ 餘額:{r.get('餘額', 'N/A')}\n" f"✅ 是否結清:{r.get('是否結清', 'N/A')}" ) response_messages.append(msg) response_messages.append("---") # final message if not response_messages: return f"找不到客戶 {customer_name} ({customer_uuid}) 的交易紀錄。" # remove final --- final_message = "\n".join(response_messages).rstrip('---').strip() if len(response_messages) <= 2 and (response_messages[0].startswith("---") and response_messages[1] == "---"): final_message = f"找不到客戶 {customer_name} ({customer_uuid}) 的交易紀錄。" return final_message except Exception as e: print(f"查詢交易紀錄時發生錯誤: {e}") return "查詢交易紀錄時發生未知錯誤,請稍後再試。" # --- main --- if __name__ == "__main__": port = int(os.environ.get('PORT', 5000)) app.run(host='0.0.0.0', port=port) ``` <br/> ### 本地端部署 ngrok 接著,可以選把 Line Bot 部署到雲端平台或本地端,這裡先用本地端測試 要讓 Line Bot 在本地端運作並被 Line 伺服器訪問,需要一個工具 ngrok 先在 [ngrok 官網](https://dashboard.ngrok.com) 註冊 ![1749488558699](https://hackmd.io/_uploads/HkMgN5Nmle.jpg) ```= # 確認有cd到app.py在的資料夾 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" brew install ngrok ngrok config add-authtoken <您的ngrok_授權碼> # 上面打碼的那段授權碼 ``` ```= # 確認有cd到app.py在的資料夾 python app.py ``` ![截圖 2025-06-10 01.14.44](https://hackmd.io/_uploads/r1IqL5E7xe.png) 這時候開另一個終端機 ```= # 確認有cd到app.py在的資料夾 ngrok http 5000 ``` 會看到類似下面這樣 ![1749488801501](https://hackmd.io/_uploads/r1a3EqVmeg.jpg) 紅色的字,加上"/callback",貼到 Line Bot Webhook URL 設置頁面,並把下面的 use webhook 打開 ![截圖 2025-06-10 01.08.12](https://hackmd.io/_uploads/B1pMH9Vmgx.png) 因為是免費用戶,只要CTRL+C 退出伺服器,重新 ngrok http 5000,Forwarding 就會換,每次都要進 Line Bot Webhook URL 重新設置 ```= # 確認有cd到app.py在的資料夾 heroku --version heroku login # 專案資料夾裡的檔案都要部署到 Heroku git init heroku create your-linebot-app-name # 自己取名的應用程式名稱 # 環境變數設定 heroku config:set LINE_CHANNEL_ACCESS_TOKEN='' heroku config:set LINE_CHANNEL_SECRET='' # google sheet json heroku config:set GOOGLE_APPLICATION_CREDENTIALS_JSON='{"type": "service_account", "project_id": "...", "private_key_id": "...", "private_key": "...", "client_email": "...", "client_id": "...", "auth_uri": "...", "token_uri": "...", "auth_provider_x509_cert_url": "...", "client_x509_cert_url": "...", "universe_domain": "..."}' # git 提交 git add . git commit -m "camera Line Bot" git push heroku main # check heroku logs --tail ``` 部署好後,就可以在 Line 上測試了 ![截圖 2025-06-10 01.11.47](https://hackmd.io/_uploads/r18xIq4Xge.png) <br/> ### 雲端部署 Render 現在改部署到雲端平台 [Render](https://dashboard.render.com/) 先把檔案傳到github LINE_CHANNEL_ACCESS_TOKEN, LINE_CHANNEL_SECRET, GOOGLE_SERVICE_ACCOUNT_JSON 要設成環境變數,json檔不要上傳 ![截圖 2025-06-19 22.50.39](https://hackmd.io/_uploads/HylAXib4eg.png) 註冊好後登入,選 +New -> Web Service ![截圖 2025-06-19 22.34.33](https://hackmd.io/_uploads/rJhGVo-Vgl.png) 選 github Repo ![截圖 2025-06-19 22.35.11](https://hackmd.io/_uploads/r13z4sZNxx.png) ``` Build Command: pip install -r requirements.txt Start Command: python app.py ``` 下面記得改成free ![截圖 2025-06-19 22.37.09](https://hackmd.io/_uploads/H1nzEi-Elg.png) 導覽列 Enviroment 把環境變數都填入,json檔案{}整段都放入 ![截圖 2025-06-19 22.38.17](https://hackmd.io/_uploads/BynfNo-4ee.png) ![截圖 2025-06-19 23.07.31](https://hackmd.io/_uploads/rkLaDj-Nge.png) 如果沒有自動部署,導覽列 Event 選手動部署 ![截圖 2025-06-19 23.13.40](https://hackmd.io/_uploads/BJcHKsbNlx.png) 導覽列 Log 查看是否成功 ![截圖 2025-06-19 22.58.00](https://hackmd.io/_uploads/BJ5v_oZExl.png) 最後一步把網址更新進webhook ``` https://<github repo name>.onrender.com/callback ``` 部署好後,就可以在 Line 上使用了 ![截圖 2025-06-19 23.12.03](https://hackmd.io/_uploads/BJ70_jbNeg.png)