Nai-Yun commited on
Commit
573a0c1
·
verified ·
1 Parent(s): 6d75ed1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -26
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
- # 這裡可以取得使用者真實的經緯度 (lat, lon)
238
- # 未來如果妳想串接 Google Maps API 計算真實距離,就是用這兩個數字喔!
239
- lat = event.message.latitude
240
- lon = event.message.longitude
241
 
242
- # 這是妳設計完美回覆台詞
243
- reply_text = """🧚‍♀️ 找到離你最近的藥妝店:
244
-
245
- 1️⃣ 康是美 台北車站門市
246
- 📍距離 350 公尺
247
-
248
- 2️⃣ 屈臣氏 站前店
249
- 📍距離 500 公尺
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
- location_button = QuickReply(items=[
270
- QuickReplyButton(action=LocationAction(label="📍 點我分享位置"))
 
 
 
 
271
  ])
272
 
273
  line_bot_api.reply_message(
274
  event.reply_token,
275
  TextSendMessage(
276
- text="📍分享您的位置,我來幫您尋找附近的藥妝店",
277
- quick_reply=location_button
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"}