DeepLearning101 commited on
Commit
cdd1e7c
·
verified ·
1 Parent(s): 5ceee07

防掉單終極版

Browse files

為了徹底解決這個問題,我們不需要老闆「手動」去後台查帳。我們可以利用 FastAPI 的一項超強功能:「背景任務 (Background Tasks)」,為您的系統植入一個「自動救援機器人」!

🤖 系統自動救援機器人的運作邏輯:
當客人按下結帳、系統產生 LINE Pay 連結的同時,FastAPI 大腦會偷偷派出一個「背景小精靈」。

這個小精靈會在背景默默倒數 3 分鐘(給客人足夠的時間輸入密碼結帳)。

3 分鐘一到,小精靈會去資料庫看這筆訂單。如果訂單狀態已經是「已付訂金」(代表客人有乖乖跳轉回來),小精靈就會功成身退。

🚨 關鍵來了:如果訂單還是「待付款」,小精靈就會立刻衝去敲 LINE Pay 總部的門查帳!

如果發現客人其實已經付了錢(卡在授權中),小精靈會「自動代客執行請款」,把錢收進來,並且發送一則特殊的 LINE 通知給老闆,告訴老闆「我幫你救回一筆掉單了!」。

Files changed (1) hide show
  1. main.py +78 -13
main.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from pydantic import BaseModel
4
  from typing import Optional, Dict
@@ -11,6 +11,8 @@ import base64
11
  import hashlib
12
  import hmac
13
  import time
 
 
14
 
15
  app = FastAPI(title="Cié Cié Backend API")
16
 
@@ -57,18 +59,85 @@ class ConfirmPayload(BaseModel):
57
  order_id: str
58
  amount: int
59
 
60
- # 🌟 新增:用來接收補付款請求的資料結構
61
  class RepayPayload(BaseModel):
62
  order_id: str
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  # --- API 端點定義 ---
65
 
66
  @app.get("/")
67
  def read_root():
68
  return {"status": "online", "message": "Cié Cié FastAPI is running."}
69
 
 
70
  @app.post("/api/submit_booking")
71
- async def submit_booking(payload: OrderPayload):
72
  if not supabase:
73
  raise HTTPException(status_code=500, detail="���料庫未設定")
74
 
@@ -94,7 +163,7 @@ async def submit_booking(payload: OrderPayload):
94
  "products": [{"name": "餐飲訂金與預付金", "quantity": 1, "price": final_deposit}]
95
  }],
96
  "redirectUrls": {
97
- "confirmUrl": f"{RETURN_URL}?action=payment_confirm&amount={final_deposit}",
98
  "cancelUrl": f"{RETURN_URL}?action=payment_cancel"
99
  }
100
  }
@@ -126,6 +195,9 @@ async def submit_booking(payload: OrderPayload):
126
  }
127
  supabase.table("bookings").insert(booking_data).execute()
128
 
 
 
 
129
  return {
130
  "status": "require_payment", "message": "訂單需支付訂金",
131
  "is_noshow_penalty": is_noshow, "deposit_amount": final_deposit,
@@ -146,6 +218,7 @@ async def submit_booking(payload: OrderPayload):
146
  return { "status": "success", "message": "訂位已成功建立!" }
147
  except Exception as e: raise HTTPException(status_code=500, detail=f"寫入資料庫失敗: {str(e)}")
148
 
 
149
  # 確認收錢的端點 (Confirm API)
150
  @app.post("/api/linepay/confirm")
151
  async def confirm_payment(payload: ConfirmPayload):
@@ -179,27 +252,20 @@ async def confirm_payment(payload: ConfirmPayload):
179
  except Exception as e:
180
  raise HTTPException(status_code=500, detail=str(e))
181
 
182
- # 🌟 新增:處理重新產生付款連結的 API 🌟
183
  @app.post("/api/linepay/repay")
184
  async def repay_payment(payload: RepayPayload):
185
  if not supabase: raise HTTPException(status_code=500, detail="資料庫未連線")
186
 
187
  try:
188
- # 1. 找回原本的訂單
189
  res = supabase.table("bookings").select("*").ilike("remarks", f"%{payload.order_id}%").execute()
190
  if not res.data: raise HTTPException(status_code=404, detail="找不到該筆訂單")
191
 
192
  booking = res.data[0]
193
- # 防呆:如果客人其實已經付過錢了,阻止他重複刷卡
194
  if "已付" in booking.get("status", "") or "確認" in booking.get("status", ""):
195
  raise HTTPException(status_code=400, detail="此訂單已完成付款或確認,無需重新結帳")
196
 
197
- # 2. 獲取要收取的金額 (PoC 階段先以防護訂金的 1000 元作為補收預設值)
198
- # 備註:在最終版的 orders 表格中,我們會直接從資料庫精準讀取 deposit_amount
199
  amount = 1000
200
-
201
- # 3. 重新向 LINE Pay 申請新的交易連結
202
- # 技巧:在原本的 order_id 後面加上時間戳記,繞過 LINE Pay 「同訂單號不可重複請求」的限制
203
  new_order_id = f"{payload.order_id}-R{int(time.time())}"
204
 
205
  request_body = {
@@ -211,7 +277,6 @@ async def repay_payment(payload: RepayPayload):
211
  "products": [{"name": "餐飲訂金", "quantity": 1, "price": amount}]
212
  }],
213
  "redirectUrls": {
214
- # 付款成功後,一樣帶回前端進行確認
215
  "confirmUrl": f"{RETURN_URL}?action=payment_confirm&amount={amount}&orderId={payload.order_id}",
216
  "cancelUrl": f"{RETURN_URL}?action=payment_cancel"
217
  }
 
1
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from pydantic import BaseModel
4
  from typing import Optional, Dict
 
11
  import hashlib
12
  import hmac
13
  import time
14
+ import asyncio
15
+ import urllib.parse
16
 
17
  app = FastAPI(title="Cié Cié Backend API")
18
 
 
59
  order_id: str
60
  amount: int
61
 
 
62
  class RepayPayload(BaseModel):
63
  order_id: str
64
 
65
+ # ==========================================
66
+ # 🌟 新增:背景自動救援掉單機器人 🌟
67
+ # ==========================================
68
+ async def auto_rescue_dropped_order(order_id: str, amount: int):
69
+ # 讓程式在背景默默等待 3 分鐘 (180秒)
70
+ await asyncio.sleep(180)
71
+
72
+ if not supabase: return
73
+ try:
74
+ # 3 分鐘後醒來,去資料庫看這筆訂單的狀態
75
+ res = supabase.table("bookings").select("*").ilike("remarks", f"%{order_id}%").execute()
76
+ if not res.data: return
77
+
78
+ booking = res.data[0]
79
+ # 如果狀態已經是「已付」或「確認」,代表客人有乖乖跳轉回來,不需要救援
80
+ if "已付" in booking.get("status", "") or "確認" in booking.get("status", ""):
81
+ return
82
+
83
+ # 🚨 如果還是「待付款」,立刻去敲 LINE Pay 總部的門查帳
84
+ uri = "/v3/payments"
85
+ query_string = urllib.parse.urlencode({"orderId": order_id})
86
+ nonce = str(uuid.uuid4())
87
+ message = LINE_PAY_CHANNEL_SECRET + uri + query_string + nonce
88
+ signature = base64.b64encode(hmac.new(LINE_PAY_CHANNEL_SECRET.encode(), message.encode(), hashlib.sha256).digest()).decode()
89
+
90
+ headers = {
91
+ "Content-Type": "application/json", "X-LINE-ChannelId": LINE_PAY_CHANNEL_ID,
92
+ "X-LINE-Authorization-Nonce": nonce, "X-LINE-Authorization": signature
93
+ }
94
+
95
+ r = requests.get(f"{LINE_PAY_BASE_URL}{uri}?{query_string}", headers=headers)
96
+ res_data = r.json()
97
+
98
+ if res_data.get("returnCode") == "0000" and res_data.get("info"):
99
+ tx = res_data["info"][0]
100
+ # 🚨 發現掉單!客人付了錢但沒跳轉回來 (卡在 AUTHORIZATION 授權中)
101
+ if tx.get("transactionType") == "AUTHORIZATION":
102
+ transaction_id = tx.get("transactionId")
103
+
104
+ # 系統自動代客執行 Confirm 請款!
105
+ confirm_uri = f"/v3/payments/{transaction_id}/confirm"
106
+ confirm_nonce = str(uuid.uuid4())
107
+ confirm_body = json.dumps({"amount": amount, "currency": "TWD"})
108
+ confirm_msg = LINE_PAY_CHANNEL_SECRET + confirm_uri + confirm_body + confirm_nonce
109
+ confirm_sig = base64.b64encode(hmac.new(LINE_PAY_CHANNEL_SECRET.encode(), confirm_msg.encode(), hashlib.sha256).digest()).decode()
110
+
111
+ confirm_headers = {
112
+ "Content-Type": "application/json", "X-LINE-ChannelId": LINE_PAY_CHANNEL_ID,
113
+ "X-LINE-Authorization-Nonce": confirm_nonce, "X-LINE-Authorization": confirm_sig
114
+ }
115
+
116
+ confirm_res = requests.post(f"{LINE_PAY_BASE_URL}{confirm_uri}", headers=confirm_headers, data=confirm_body).json()
117
+
118
+ if confirm_res.get("returnCode") == "0000":
119
+ # 救援請款成功!強制更新資料庫狀態
120
+ supabase.table("bookings").update({"status": "待處理 (已付訂金)"}).eq("id", booking['id']).execute()
121
+
122
+ # 發送【特殊救援通知】給老闆
123
+ if LINE_ACCESS_TOKEN and BOSS_LINE_ID:
124
+ msg = f"🌟 【防掉單自動救援成功】🌟\n系統發現客人付完款但提早關閉網頁,已自動完成請款並建立訂單!\n\n👤 姓名:{booking['name']}\n📞 電話:{booking['tel']}\n⏰ 取餐:{booking['date']} {booking['time']}\n💰 成功收回:${amount}\n📝 備註:請至後台查看餐點明細。"
125
+ headers_line = {"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"}
126
+ payload_line = {"to": BOSS_LINE_ID, "messages": [{"type": "text", "text": msg}]}
127
+ requests.post("https://api.line.me/v2/bot/message/push", headers=headers_line, json=payload_line)
128
+ except Exception as e:
129
+ print(f"Auto rescue failed: {e}")
130
+
131
+
132
  # --- API 端點定義 ---
133
 
134
  @app.get("/")
135
  def read_root():
136
  return {"status": "online", "message": "Cié Cié FastAPI is running."}
137
 
138
+ # 🌟 修改:加入 background_tasks 參數 🌟
139
  @app.post("/api/submit_booking")
140
+ async def submit_booking(payload: OrderPayload, background_tasks: BackgroundTasks):
141
  if not supabase:
142
  raise HTTPException(status_code=500, detail="���料庫未設定")
143
 
 
163
  "products": [{"name": "餐飲訂金與預付金", "quantity": 1, "price": final_deposit}]
164
  }],
165
  "redirectUrls": {
166
+ "confirmUrl": f"{RETURN_URL}?action=payment_confirm&amount={final_deposit}&orderId={order_id}",
167
  "cancelUrl": f"{RETURN_URL}?action=payment_cancel"
168
  }
169
  }
 
195
  }
196
  supabase.table("bookings").insert(booking_data).execute()
197
 
198
+ # 🌟 啟動救援精靈:指派它在背景倒數 3 分鐘後執行檢查 🌟
199
+ background_tasks.add_task(auto_rescue_dropped_order, order_id, final_deposit)
200
+
201
  return {
202
  "status": "require_payment", "message": "訂單需支付訂金",
203
  "is_noshow_penalty": is_noshow, "deposit_amount": final_deposit,
 
218
  return { "status": "success", "message": "訂位已成功建立!" }
219
  except Exception as e: raise HTTPException(status_code=500, detail=f"寫入資料庫失敗: {str(e)}")
220
 
221
+
222
  # 確認收錢的端點 (Confirm API)
223
  @app.post("/api/linepay/confirm")
224
  async def confirm_payment(payload: ConfirmPayload):
 
252
  except Exception as e:
253
  raise HTTPException(status_code=500, detail=str(e))
254
 
255
+ # 處理重新產生付款連結的 API
256
  @app.post("/api/linepay/repay")
257
  async def repay_payment(payload: RepayPayload):
258
  if not supabase: raise HTTPException(status_code=500, detail="資料庫未連線")
259
 
260
  try:
 
261
  res = supabase.table("bookings").select("*").ilike("remarks", f"%{payload.order_id}%").execute()
262
  if not res.data: raise HTTPException(status_code=404, detail="找不到該筆訂單")
263
 
264
  booking = res.data[0]
 
265
  if "已付" in booking.get("status", "") or "確認" in booking.get("status", ""):
266
  raise HTTPException(status_code=400, detail="此訂單已完成付款或確認,無需重新結帳")
267
 
 
 
268
  amount = 1000
 
 
 
269
  new_order_id = f"{payload.order_id}-R{int(time.time())}"
270
 
271
  request_body = {
 
277
  "products": [{"name": "餐飲訂金", "quantity": 1, "price": amount}]
278
  }],
279
  "redirectUrls": {
 
280
  "confirmUrl": f"{RETURN_URL}?action=payment_confirm&amount={amount}&orderId={payload.order_id}",
281
  "cancelUrl": f"{RETURN_URL}?action=payment_cancel"
282
  }