# **【LINE Bot + Google Sheets, 部署到 Reader】**
:::info
- 設置 Google Cloud Platform (GCP)
- 設置 Line Bot
- Project
- 本地端部署 ngrok
- 雲端部署 Render
:::
朋友是二手相機店老闆,當業務想知道客戶近期交易資訊時,步驟會是:開電腦 -> 開報表 -> 篩選客戶姓名 -> 查看,有點麻煩
如果做一個 Line Bot 呢?
只需要輸入客戶姓名+單一編號(防止同名),回傳近期幾筆交易資料,就可以解決了吧?
我創建了一個假的數據表

<br/>
### 設置 Google Cloud Platform (GCP)
要去把 google sheet 的 API 授權打開
[Google Cloud Platform](https://console.cloud.google.com/welcome?inv=1&invt=AbzqrA&project=cameralinebot)
上方新增專案,這裡設置為 cameraLINEBot

左邊導航欄 -> API 與服務 -> 啟用 API 和服務

在上面搜尋,把 google sheet api、google cloud api 打開


左邊導航欄 -> IAM 與管理員 -> 服務帳戶 -> (上方) +建立服務帳戶


選取角色選單
檢視者:可以查看專案中的所有資源,但不允許修改
編輯者:可以從 Bot 寫入 Sheet

創建成功後點進去 -> 新增鍵 -> 建立私密金鑰
:+1: PS 這個電子郵件複製下來,等等會用到



會下載到一個 json檔,放到跟後面 app.py 同一資料夾下

:+1: PS 剛才複製的電子郵件設置為 google sheet 的共用編輯者

<br/>
### 設置 Line Bot
登入 [Line Developers](https://developers.line.biz/zh-hant/)
創一個 provider -> create new channel 選 Messaging API -> 填寫 Channel 資訊


```=
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 記下來後面會用到


Basic settings 滑到最下面 Channel secret,記下來後面會用到

<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) 註冊

```=
# 確認有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
```

這時候開另一個終端機
```=
# 確認有cd到app.py在的資料夾
ngrok http 5000
```
會看到類似下面這樣

紅色的字,加上"/callback",貼到 Line Bot Webhook URL 設置頁面,並把下面的 use webhook 打開

因為是免費用戶,只要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 上測試了

<br/>
### 雲端部署 Render
現在改部署到雲端平台 [Render](https://dashboard.render.com/)
先把檔案傳到github
LINE_CHANNEL_ACCESS_TOKEN, LINE_CHANNEL_SECRET, GOOGLE_SERVICE_ACCOUNT_JSON 要設成環境變數,json檔不要上傳

註冊好後登入,選 +New -> Web Service

選 github Repo

```
Build Command: pip install -r requirements.txt
Start Command: python app.py
```
下面記得改成free

導覽列 Enviroment 把環境變數都填入,json檔案{}整段都放入


如果沒有自動部署,導覽列 Event 選手動部署

導覽列 Log 查看是否成功

最後一步把網址更新進webhook
```
https://<github repo name>.onrender.com/callback
```
部署好後,就可以在 Line 上使用了
