Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import os
|
|
|
|
| 2 |
import google.generativeai as genai
|
| 3 |
from flask import Flask, request, abort
|
| 4 |
from linebot import LineBotApi, WebhookHandler
|
|
@@ -8,6 +9,8 @@ import json
|
|
| 8 |
from PIL import Image
|
| 9 |
from linebot.models import MessageEvent, TextMessage, TextSendMessage, QuickReply, QuickReplyButton, MessageAction, ImageMessage, FlexSendMessage, LocationMessage, LocationAction
|
| 10 |
import urllib.parse
|
|
|
|
|
|
|
| 11 |
|
| 12 |
app = Flask(__name__)
|
| 13 |
|
|
@@ -232,31 +235,164 @@ def handle_image(event):
|
|
| 232 |
)
|
| 233 |
|
| 234 |
# ================= 【新增】專門處理使用者傳送位置的邏輯 =================
|
|
|
|
|
|
|
| 235 |
@handler.add(MessageEvent, message=LocationMessage)
|
| 236 |
def handle_location(event):
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
lon = event.message.longitude
|
| 241 |
|
| 242 |
-
#
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
3️⃣ 寶雅 台北站前店
|
| 252 |
-
📍距離 800 公尺"""
|
| 253 |
-
|
| 254 |
-
# 直接回傳給使用者
|
| 255 |
-
line_bot_api.reply_message(
|
| 256 |
-
event.reply_token,
|
| 257 |
-
TextSendMessage(text=reply_text)
|
| 258 |
-
)
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
# ================= 3. 多輪對話核心邏輯 =================
|
| 262 |
@handler.add(MessageEvent, message=TextMessage)
|
|
@@ -264,21 +400,48 @@ def handle_message(event):
|
|
| 264 |
user_id = event.source.user_id
|
| 265 |
user_msg = event.message.text.strip()
|
| 266 |
|
|
|
|
| 267 |
if user_msg == "附近店家":
|
| 268 |
-
#
|
| 269 |
-
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
])
|
| 272 |
|
| 273 |
line_bot_api.reply_message(
|
| 274 |
event.reply_token,
|
| 275 |
TextSendMessage(
|
| 276 |
-
text="
|
| 277 |
-
quick_reply=
|
| 278 |
)
|
| 279 |
)
|
| 280 |
return
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
# 任何時候輸入「精靈顧問」或「重新開始」,皆重置該用戶的狀態
|
| 283 |
if user_msg == "精靈顧問" or user_msg == "重新開始":
|
| 284 |
user_states[user_id] = {"step": "ASK_SKIN"}
|
|
|
|
| 1 |
import os
|
| 2 |
+
import requests
|
| 3 |
import google.generativeai as genai
|
| 4 |
from flask import Flask, request, abort
|
| 5 |
from linebot import LineBotApi, WebhookHandler
|
|
|
|
| 9 |
from PIL import Image
|
| 10 |
from linebot.models import MessageEvent, TextMessage, TextSendMessage, QuickReply, QuickReplyButton, MessageAction, ImageMessage, FlexSendMessage, LocationMessage, LocationAction
|
| 11 |
import urllib.parse
|
| 12 |
+
import math
|
| 13 |
+
import urllib.parse
|
| 14 |
|
| 15 |
app = Flask(__name__)
|
| 16 |
|
|
|
|
| 235 |
)
|
| 236 |
|
| 237 |
# ================= 【新增】專門處理使用者傳送位置的邏輯 =================
|
| 238 |
+
# ================= 【升級版】全台通用的真實附近店家搜尋 =================
|
| 239 |
+
# ================= 【終極版】客製化品牌 + 營業時間 + Flex 卡片 =================
|
| 240 |
@handler.add(MessageEvent, message=LocationMessage)
|
| 241 |
def handle_location(event):
|
| 242 |
+
user_id = event.source.user_id
|
| 243 |
+
user_lat = event.message.latitude
|
| 244 |
+
user_lon = event.message.longitude
|
|
|
|
| 245 |
|
| 246 |
+
# 取出剛剛使用者選擇的品牌 (如果沒有紀錄,預設用全都要)
|
| 247 |
+
state = user_states.get(user_id, {})
|
| 248 |
+
brand_preference = state.get("search_brand", "全都要")
|
| 249 |
+
|
| 250 |
+
google_api_key = os.environ.get('GOOGLE_MAPS_API_KEY')
|
| 251 |
+
if not google_api_key:
|
| 252 |
+
line_bot_api.reply_message(event.reply_token, TextSendMessage(text="❌ 系統尚未設定 Google Maps API 金鑰。"))
|
| 253 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
+
# 計算距離的數學魔法
|
| 256 |
+
def calculate_distance(lat1, lon1, lat2, lon2):
|
| 257 |
+
R = 6371.0
|
| 258 |
+
lat1_rad, lon1_rad = math.radians(lat1), math.radians(lon1)
|
| 259 |
+
lat2_rad, lon2_rad = math.radians(lat2), math.radians(lon2)
|
| 260 |
+
dlon = lon2_rad - lon1_rad
|
| 261 |
+
dlat = lat2_rad - lat1_rad
|
| 262 |
+
a = math.sin(dlat / 2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2)**2
|
| 263 |
+
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
| 264 |
+
return int(R * c * 1000)
|
| 265 |
+
|
| 266 |
+
# 決定呼叫 Google 的關鍵字
|
| 267 |
+
search_keyword = "藥妝店" if brand_preference == "全都要" else brand_preference
|
| 268 |
+
search_radius = 2000 # 稍微擴大到 2 公里,確保「全都要」時能找到足夠的店
|
| 269 |
+
|
| 270 |
+
url = f"https://maps.googleapis.com/maps/api/place/textsearch/json?query={search_keyword}&location={user_lat},{user_lon}&radius={search_radius}&language=zh-TW&key={google_api_key}"
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
line_bot_api.reply_message(event.reply_token, TextSendMessage(text="🧚♀️ 精靈正在為您繪製專屬地圖卡片,請稍候..."))
|
| 274 |
+
|
| 275 |
+
response = requests.get(url)
|
| 276 |
+
data = response.json()
|
| 277 |
+
|
| 278 |
+
if data['status'] != 'OK' or not data.get('results'):
|
| 279 |
+
line_bot_api.push_message(user_id, TextSendMessage(text="🥺 哎呀,精靈在您附近找不到符合的藥妝店呢..."))
|
| 280 |
+
if user_id in user_states: del user_states[user_id]
|
| 281 |
+
return
|
| 282 |
+
|
| 283 |
+
# 整理資料並加上「營業時間」判斷
|
| 284 |
+
stores = []
|
| 285 |
+
for place in data['results']:
|
| 286 |
+
store_lat = place['geometry']['location']['lat']
|
| 287 |
+
store_lon = place['geometry']['location']['lng']
|
| 288 |
+
distance = calculate_distance(user_lat, user_lon, store_lat, store_lon)
|
| 289 |
+
|
| 290 |
+
# 判斷營業狀態 (open_now)
|
| 291 |
+
open_now = place.get('opening_hours', {}).get('open_now')
|
| 292 |
+
if open_now is True:
|
| 293 |
+
hours_text = "🟢 營業中"
|
| 294 |
+
elif open_now is False:
|
| 295 |
+
hours_text = "🔴 休息中"
|
| 296 |
+
else:
|
| 297 |
+
hours_text = "🕒 營業時間需確認"
|
| 298 |
+
|
| 299 |
+
stores.append({
|
| 300 |
+
"name": place.get('name', '未知名稱'),
|
| 301 |
+
"rating": place.get('rating', '無評分'),
|
| 302 |
+
"lat": store_lat,
|
| 303 |
+
"lon": store_lon,
|
| 304 |
+
"distance": distance,
|
| 305 |
+
"address": place.get('formatted_address', ''),
|
| 306 |
+
"hours": hours_text # 將營業狀態存入
|
| 307 |
+
})
|
| 308 |
+
|
| 309 |
+
# 依照距離由近到遠排序
|
| 310 |
+
sorted_stores = sorted(stores, key=lambda x: x['distance'])
|
| 311 |
+
|
| 312 |
+
# 🌟 核心邏輯:依照使用者選擇來過濾店家!
|
| 313 |
+
nearest_stores = []
|
| 314 |
+
|
| 315 |
+
if brand_preference == "全都要":
|
| 316 |
+
# 如果全都要,精靈會逐一尋找各品牌的第一名
|
| 317 |
+
found_w = found_c = found_p = False
|
| 318 |
+
for s in sorted_stores:
|
| 319 |
+
if "屈臣氏" in s['name'] and not found_w:
|
| 320 |
+
nearest_stores.append(s)
|
| 321 |
+
found_w = True
|
| 322 |
+
elif "康是美" in s['name'] and not found_c:
|
| 323 |
+
nearest_stores.append(s)
|
| 324 |
+
found_c = True
|
| 325 |
+
elif "寶雅" in s['name'] and not found_p:
|
| 326 |
+
nearest_stores.append(s)
|
| 327 |
+
found_p = True
|
| 328 |
+
# 如果三家都找到了就提早結束尋找
|
| 329 |
+
if found_w and found_c and found_p:
|
| 330 |
+
break
|
| 331 |
+
else:
|
| 332 |
+
# 如果有指定品牌,就直接拿該品牌距離最近的前 3 名
|
| 333 |
+
nearest_stores = sorted_stores[:3]
|
| 334 |
+
|
| 335 |
+
# 準備製作 Flex Message 的泡泡卡片
|
| 336 |
+
bubbles = []
|
| 337 |
+
for store in nearest_stores:
|
| 338 |
+
map_query = urllib.parse.quote(f"{store['name']} {store['address']}")
|
| 339 |
+
map_url = f"https://www.google.com/maps/search/?api=1&query={map_query}"
|
| 340 |
+
|
| 341 |
+
# 組裝單張卡片 (加入營業時間欄位)
|
| 342 |
+
card = {
|
| 343 |
+
"type": "bubble",
|
| 344 |
+
"size": "kilo",
|
| 345 |
+
"body": {
|
| 346 |
+
"type": "box",
|
| 347 |
+
"layout": "vertical",
|
| 348 |
+
"spacing": "sm",
|
| 349 |
+
"contents": [
|
| 350 |
+
{"type": "text", "text": f"🏪 {store['name']}", "weight": "bold", "size": "md", "wrap": True, "color": "#111111"},
|
| 351 |
+
{"type": "text", "text": f"📍 距離 {store['distance']} 公尺", "size": "sm", "color": "#8c8c8c"},
|
| 352 |
+
{"type": "text", "text": store['hours'], "size": "sm", "color": "#8c8c8c"}, # 🌟 這裡新增了營業時間!
|
| 353 |
+
{"type": "text", "text": f"⭐ 評分: {store['rating']}", "size": "sm", "color": "#fbbc04", "weight": "bold"}
|
| 354 |
+
]
|
| 355 |
+
},
|
| 356 |
+
"footer": {
|
| 357 |
+
"type": "box",
|
| 358 |
+
"layout": "vertical",
|
| 359 |
+
"spacing": "sm",
|
| 360 |
+
"contents": [
|
| 361 |
+
{
|
| 362 |
+
"type": "button",
|
| 363 |
+
"style": "primary",
|
| 364 |
+
"color": "#00B900",
|
| 365 |
+
"height": "sm",
|
| 366 |
+
"action": {
|
| 367 |
+
"type": "uri",
|
| 368 |
+
"label": "地圖導航",
|
| 369 |
+
"uri": map_url
|
| 370 |
+
}
|
| 371 |
+
}
|
| 372 |
+
]
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
bubbles.append(card)
|
| 376 |
+
|
| 377 |
+
flex_message = FlexSendMessage.new_from_json_dict({
|
| 378 |
+
"type": "flex",
|
| 379 |
+
"alt_text": "📍 附近的藥妝店來囉!",
|
| 380 |
+
"contents": {
|
| 381 |
+
"type": "carousel",
|
| 382 |
+
"contents": bubbles
|
| 383 |
+
}
|
| 384 |
+
})
|
| 385 |
+
|
| 386 |
+
line_bot_api.push_message(user_id, flex_message)
|
| 387 |
+
|
| 388 |
+
# 任務完成,清除狀態,避免卡住
|
| 389 |
+
if user_id in user_states:
|
| 390 |
+
del user_states[user_id]
|
| 391 |
+
|
| 392 |
+
except Exception as e:
|
| 393 |
+
line_bot_api.push_message(user_id, TextSendMessage(text=f"❌ 搜尋過程發生錯誤:{e}"))
|
| 394 |
+
if user_id in user_states:
|
| 395 |
+
del user_states[user_id]
|
| 396 |
|
| 397 |
# ================= 3. 多輪對話核心邏輯 =================
|
| 398 |
@handler.add(MessageEvent, message=TextMessage)
|
|
|
|
| 400 |
user_id = event.source.user_id
|
| 401 |
user_msg = event.message.text.strip()
|
| 402 |
|
| 403 |
+
# ================= 【升級】附近店家 - 步驟 1:選擇品牌 =================
|
| 404 |
if user_msg == "附近店家":
|
| 405 |
+
user_states[user_id] = {"step": "ASK_BRAND"} # 記錄用戶進入找店流程
|
| 406 |
+
|
| 407 |
+
brand_buttons = QuickReply(items=[
|
| 408 |
+
QuickReplyButton(action=MessageAction(label="屈臣氏", text="屈臣氏")),
|
| 409 |
+
QuickReplyButton(action=MessageAction(label="康是美", text="康是美")),
|
| 410 |
+
QuickReplyButton(action=MessageAction(label="寶雅", text="寶雅")),
|
| 411 |
+
QuickReplyButton(action=MessageAction(label="全都要", text="全都要"))
|
| 412 |
])
|
| 413 |
|
| 414 |
line_bot_api.reply_message(
|
| 415 |
event.reply_token,
|
| 416 |
TextSendMessage(
|
| 417 |
+
text="請問你想尋找哪一間藥妝店呢?",
|
| 418 |
+
quick_reply=brand_buttons
|
| 419 |
)
|
| 420 |
)
|
| 421 |
return
|
| 422 |
|
| 423 |
+
# ================= 【升級】附近店家 - 步驟 2:要求定位 =================
|
| 424 |
+
state = user_states.get(user_id)
|
| 425 |
+
if state and state.get("step") == "ASK_BRAND":
|
| 426 |
+
if user_msg in ["屈臣氏", "康是美", "寶雅", "全都要"]:
|
| 427 |
+
state["search_brand"] = user_msg # 把使用者選的品牌存起來
|
| 428 |
+
state["step"] = "ASK_LOCATION" # 推進到下一步
|
| 429 |
+
|
| 430 |
+
location_button = QuickReply(items=[
|
| 431 |
+
QuickReplyButton(action=LocationAction(label="📍 點我分享位置"))
|
| 432 |
+
])
|
| 433 |
+
|
| 434 |
+
line_bot_api.reply_message(
|
| 435 |
+
event.reply_token,
|
| 436 |
+
TextSendMessage(
|
| 437 |
+
text=f"沒問題!請分享您的位置,精靈來幫您找最近的【{user_msg}】!",
|
| 438 |
+
quick_reply=location_button
|
| 439 |
+
)
|
| 440 |
+
)
|
| 441 |
+
else:
|
| 442 |
+
line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請點擊下方的按鈕選擇品牌喔!"))
|
| 443 |
+
return
|
| 444 |
+
|
| 445 |
# 任何時候輸入「精靈顧問」或「重新開始」,皆重置該用戶的狀態
|
| 446 |
if user_msg == "精靈顧問" or user_msg == "重新開始":
|
| 447 |
user_states[user_id] = {"step": "ASK_SKIN"}
|