DeepLearning101 commited on
Commit
a541218
·
verified ·
1 Parent(s): b4479dc

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +70 -90
main.py CHANGED
@@ -10,11 +10,10 @@ import json
10
  import base64
11
  import hashlib
12
  import hmac
13
- import time
14
 
15
  app = FastAPI(title="Cié Cié Backend API")
16
 
17
- # 🌟 解決 CORS (跨域) 問題:允許您的 GitHub Pages 前端呼叫這台主機
18
  app.add_middleware(
19
  CORSMiddleware,
20
  allow_origins=["*"],
@@ -23,7 +22,7 @@ app.add_middleware(
23
  allow_headers=["*"],
24
  )
25
 
26
- # 讀取環境變數 (請在 Hugging Face Settings 中設定)
27
  SUPABASE_URL = os.getenv("SUPABASE_URL", "")
28
  SUPABASE_KEY = os.getenv("SUPABASE_KEY", "") # 必須使用 service_role key
29
  LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN", "")
@@ -32,15 +31,13 @@ BOSS_LINE_ID = os.getenv("BOSS_LINE_ID", "")
32
  # 🌟 LINE Pay 金鑰設定 🌟
33
  LINE_PAY_CHANNEL_ID = os.getenv("LINE_PAY_CHANNEL_ID", "")
34
  LINE_PAY_CHANNEL_SECRET = os.getenv("LINE_PAY_CHANNEL_SECRET", "")
35
- LINE_PAY_BASE_URL = "https://sandbox-api-pay.line.me" # 這是沙盒(測試)環境的專屬網址
36
 
37
- # 結帳完成後要跳回哪個網頁?(設定回您的官網)
38
- RETURN_URL = "https://ciecietaipei.github.io/index.html"
39
 
40
- # 初始化 Supabase
41
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) if SUPABASE_URL else None
42
 
43
- # 定義前端傳來的資料結構 (Payload)
44
  class OrderPayload(BaseModel):
45
  service_type: str
46
  name: str
@@ -53,6 +50,11 @@ class OrderPayload(BaseModel):
53
  deposit_required: int = 0
54
  total_amount: int = 0
55
 
 
 
 
 
 
56
  @app.get("/")
57
  def read_root():
58
  return {"status": "online", "message": "Cié Cié FastAPI is running."}
@@ -62,71 +64,40 @@ async def submit_booking(payload: OrderPayload):
62
  if not supabase:
63
  raise HTTPException(status_code=500, detail="資料庫未設定")
64
 
65
- # ==========================================
66
- # 🕵️‍♂️ 階段 1:查核 No-Show 黑名單
67
- # ==========================================
68
  is_noshow = False
69
  try:
70
- # 用電話號碼去資料庫找有沒有 No-Show 紀錄
71
  res = supabase.table("bookings").select("id").eq("tel", payload.tel).eq("status", "No-Show").execute()
72
  is_noshow = len(res.data) > 0
73
- except Exception as e:
74
- print("查詢黑名單失敗:", e)
75
 
76
- # 決定最終訂金:如果是黑名單,原本不用付訂金的也強制收 $1000
77
  final_deposit = payload.deposit_required
78
  if is_noshow and final_deposit == 0:
79
  final_deposit = 1000
80
 
81
- # ==========================================
82
- # 💳 階段 2:金流分流處理 (需要收訂金 vs 不用收訂金)
83
- # ==========================================
84
-
85
- # 狀況 A:需要付款 (產生真實 LINE Pay 連結)
86
  if final_deposit > 0:
87
  order_id = f"ORDER-{uuid.uuid4().hex[:8].upper()}"
88
 
89
- # 1. 準備發送給 LINE Pay 的訂單資料 (JSON)
90
  request_body = {
91
  "amount": final_deposit,
92
  "currency": "TWD",
93
  "orderId": order_id,
94
- "packages": [
95
- {
96
- "id": "pkg_1",
97
- "amount": final_deposit,
98
- "name": "Cié Cié Taipei 預約/點餐",
99
- "products": [
100
- {
101
- "name": "餐飲訂金與預付金",
102
- "quantity": 1,
103
- "price": final_deposit
104
- }
105
- ]
106
- }
107
- ],
108
  "redirectUrls": {
109
- "confirmUrl": RETURN_URL, # 客人付款完,LINE Pay 會把他導回您的首頁
110
- "cancelUrl": RETURN_URL
 
111
  }
112
  }
113
 
114
- # 2. 核心大魔王:生成 HMAC-SHA256 簽章
115
  uri = "/v3/payments/request"
116
  nonce = str(uuid.uuid4())
117
  body_str = json.dumps(request_body)
118
  message = LINE_PAY_CHANNEL_SECRET + uri + body_str + nonce
119
-
120
- # 執行加密
121
- signature = base64.b64encode(
122
- hmac.new(
123
- LINE_PAY_CHANNEL_SECRET.encode(),
124
- message.encode(),
125
- hashlib.sha256
126
- ).digest()
127
- ).decode()
128
-
129
- # 3. 發送請求給 LINE Pay 總部
130
  headers = {
131
  "Content-Type": "application/json",
132
  "X-LINE-ChannelId": LINE_PAY_CHANNEL_ID,
@@ -135,69 +106,78 @@ async def submit_booking(payload: OrderPayload):
135
  }
136
 
137
  try:
138
- line_pay_res = requests.post(
139
- f"{LINE_PAY_BASE_URL}{uri}",
140
- headers=headers,
141
- data=body_str
142
- )
143
  res_data = line_pay_res.json()
144
 
145
- # 如果 LINE Pay 成功受理,它會回傳一組專屬網址給我們
146
  if res_data.get("returnCode") == "0000":
147
  payment_url = res_data["info"]["paymentUrl"]["web"]
148
-
149
- # 這裡我們先把訂單狀態記為 "待付款",寫入資料庫
150
  booking_data = {
151
  "name": payload.name, "tel": payload.tel, "date": payload.date,
152
  "time": payload.time, "pax": payload.pax, "user_id": payload.line_id,
153
- "status": "待付款", # 特別標記為待付款
154
  "remarks": f"類型: {'外帶' if payload.service_type == 'takeout' else '內用'}\n餐點: {payload.cart}\n訂單號: {order_id}"
155
  }
156
  supabase.table("bookings").insert(booking_data).execute()
157
 
158
  return {
159
- "status": "require_payment",
160
- "message": "訂單需支付訂金",
161
- "is_noshow_penalty": is_noshow,
162
- "deposit_amount": final_deposit,
163
- "payment_url": payment_url, # 這是真實會跳轉去刷卡的網址!
164
- "order_id": order_id
165
  }
166
- else:
167
- raise HTTPException(status_code=500, detail=f"LINE Pay 錯誤: {res_data.get('returnMessage')}")
168
-
169
- except Exception as e:
170
- raise HTTPException(status_code=500, detail=f"金流連線失敗: {str(e)}")
171
 
172
- # 狀況 B:不需付款 (直接寫入資料庫並完成訂位)
173
  booking_data = {
174
- "name": payload.name,
175
- "tel": payload.tel,
176
- "date": payload.date,
177
- "time": payload.time,
178
- "pax": payload.pax,
179
- "email": "",
180
- "user_id": payload.line_id,
181
- "status": "待處理",
182
  "remarks": f"類型: {'外帶' if payload.service_type == 'takeout' else '內用'}\n餐點: {payload.cart}"
183
  }
184
 
185
  try:
186
  supabase.table("bookings").insert(booking_data).execute()
187
- # 🔔 通知老闆有新訂位
188
- notify_boss(payload.name, payload.tel, payload.date, payload.time, payload.pax)
189
  return { "status": "success", "message": "訂位已成功建立!" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  except Exception as e:
191
- raise HTTPException(status_code=500, detail=f"寫入資料庫失敗: {str(e)}")
192
 
193
- def notify_boss(name, tel, date, time, pax):
194
- """發送 LINE 通知給老闆"""
195
- if not LINE_ACCESS_TOKEN or not BOSS_LINE_ID:
196
- return
197
- msg = f"🔔 【新訂位通知】\n姓名:{name}\n電話:{tel}\n時間:{date} {time}\n人數:{pax}位"
198
  headers = {"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"}
199
  payload = {"to": BOSS_LINE_ID, "messages": [{"type": "text", "text": msg}]}
200
- try:
201
- requests.post("https://api.line.me/v2/bot/message/push", headers=headers, json=payload)
202
- except:
203
- pass
 
10
  import base64
11
  import hashlib
12
  import hmac
 
13
 
14
  app = FastAPI(title="Cié Cié Backend API")
15
 
16
+ # 🌟 解決 CORS (跨域) 問題
17
  app.add_middleware(
18
  CORSMiddleware,
19
  allow_origins=["*"],
 
22
  allow_headers=["*"],
23
  )
24
 
25
+ # 讀取環境變數
26
  SUPABASE_URL = os.getenv("SUPABASE_URL", "")
27
  SUPABASE_KEY = os.getenv("SUPABASE_KEY", "") # 必須使用 service_role key
28
  LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN", "")
 
31
  # 🌟 LINE Pay 金鑰設定 🌟
32
  LINE_PAY_CHANNEL_ID = os.getenv("LINE_PAY_CHANNEL_ID", "")
33
  LINE_PAY_CHANNEL_SECRET = os.getenv("LINE_PAY_CHANNEL_SECRET", "")
34
+ LINE_PAY_BASE_URL = "https://sandbox-api-pay.line.me"
35
 
36
+ # 設定結帳後要跳回前端 (指向您的 GitHub Pages booking.html)
37
+ RETURN_URL = "https://ciecietaipei.github.io/booking.html"
38
 
 
39
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) if SUPABASE_URL else None
40
 
 
41
  class OrderPayload(BaseModel):
42
  service_type: str
43
  name: str
 
50
  deposit_required: int = 0
51
  total_amount: int = 0
52
 
53
+ class ConfirmPayload(BaseModel):
54
+ transaction_id: str
55
+ order_id: str
56
+ amount: int
57
+
58
  @app.get("/")
59
  def read_root():
60
  return {"status": "online", "message": "Cié Cié FastAPI is running."}
 
64
  if not supabase:
65
  raise HTTPException(status_code=500, detail="資料庫未設定")
66
 
 
 
 
67
  is_noshow = False
68
  try:
 
69
  res = supabase.table("bookings").select("id").eq("tel", payload.tel).eq("status", "No-Show").execute()
70
  is_noshow = len(res.data) > 0
71
+ except: pass
 
72
 
 
73
  final_deposit = payload.deposit_required
74
  if is_noshow and final_deposit == 0:
75
  final_deposit = 1000
76
 
 
 
 
 
 
77
  if final_deposit > 0:
78
  order_id = f"ORDER-{uuid.uuid4().hex[:8].upper()}"
79
 
 
80
  request_body = {
81
  "amount": final_deposit,
82
  "currency": "TWD",
83
  "orderId": order_id,
84
+ "packages": [{
85
+ "id": "pkg_1", "amount": final_deposit, "name": "Cié Cié Taipei 預付金",
86
+ "products": [{"name": "餐飲訂金與預付金", "quantity": 1, "price": final_deposit}]
87
+ }],
 
 
 
 
 
 
 
 
 
 
88
  "redirectUrls": {
89
+ # 這裡最關鍵:帶上 action=payment_confirm 讓前端知道要處理結帳確認
90
+ "confirmUrl": f"{RETURN_URL}?action=payment_confirm&amount={final_deposit}",
91
+ "cancelUrl": f"{RETURN_URL}?action=payment_cancel"
92
  }
93
  }
94
 
 
95
  uri = "/v3/payments/request"
96
  nonce = str(uuid.uuid4())
97
  body_str = json.dumps(request_body)
98
  message = LINE_PAY_CHANNEL_SECRET + uri + body_str + nonce
99
+ signature = base64.b64encode(hmac.new(LINE_PAY_CHANNEL_SECRET.encode(), message.encode(), hashlib.sha256).digest()).decode()
100
+
 
 
 
 
 
 
 
 
 
101
  headers = {
102
  "Content-Type": "application/json",
103
  "X-LINE-ChannelId": LINE_PAY_CHANNEL_ID,
 
106
  }
107
 
108
  try:
109
+ line_pay_res = requests.post(f"{LINE_PAY_BASE_URL}{uri}", headers=headers, data=body_str)
 
 
 
 
110
  res_data = line_pay_res.json()
111
 
 
112
  if res_data.get("returnCode") == "0000":
113
  payment_url = res_data["info"]["paymentUrl"]["web"]
 
 
114
  booking_data = {
115
  "name": payload.name, "tel": payload.tel, "date": payload.date,
116
  "time": payload.time, "pax": payload.pax, "user_id": payload.line_id,
117
+ "status": "待付款",
118
  "remarks": f"類型: {'外帶' if payload.service_type == 'takeout' else '內用'}\n餐點: {payload.cart}\n訂單號: {order_id}"
119
  }
120
  supabase.table("bookings").insert(booking_data).execute()
121
 
122
  return {
123
+ "status": "require_payment", "message": "訂單需支付訂金",
124
+ "is_noshow_penalty": is_noshow, "deposit_amount": final_deposit,
125
+ "payment_url": payment_url, "order_id": order_id
 
 
 
126
  }
127
+ else: raise HTTPException(status_code=500, detail=f"LINE Pay 錯誤: {res_data.get('returnMessage')}")
128
+ except Exception as e: raise HTTPException(status_code=500, detail=f"金流連線失敗: {str(e)}")
 
 
 
129
 
 
130
  booking_data = {
131
+ "name": payload.name, "tel": payload.tel, "date": payload.date, "time": payload.time,
132
+ "pax": payload.pax, "email": "", "user_id": payload.line_id, "status": "待處理",
 
 
 
 
 
 
133
  "remarks": f"類型: {'外帶' if payload.service_type == 'takeout' else '內用'}\n餐點: {payload.cart}"
134
  }
135
 
136
  try:
137
  supabase.table("bookings").insert(booking_data).execute()
138
+ notify_boss(payload.name, payload.tel, payload.date, payload.time, payload.pax, 0)
 
139
  return { "status": "success", "message": "訂位已成功建立!" }
140
+ except Exception as e: raise HTTPException(status_code=500, detail=f"寫入資料庫失敗: {str(e)}")
141
+
142
+ # 🌟 新增:確認收錢的端點 (Confirm API) 🌟
143
+ @app.post("/api/linepay/confirm")
144
+ async def confirm_payment(payload: ConfirmPayload):
145
+ uri = f"/v3/payments/{payload.transaction_id}/confirm"
146
+ nonce = str(uuid.uuid4())
147
+ body_str = json.dumps({"amount": payload.amount, "currency": "TWD"})
148
+ message = LINE_PAY_CHANNEL_SECRET + uri + body_str + nonce
149
+ signature = base64.b64encode(hmac.new(LINE_PAY_CHANNEL_SECRET.encode(), message.encode(), hashlib.sha256).digest()).decode()
150
+
151
+ headers = {
152
+ "Content-Type": "application/json", "X-LINE-ChannelId": LINE_PAY_CHANNEL_ID,
153
+ "X-LINE-Authorization-Nonce": nonce, "X-LINE-Authorization": signature
154
+ }
155
+
156
+ try:
157
+ res = requests.post(f"{LINE_PAY_BASE_URL}{uri}", headers=headers, data=body_str)
158
+ res_data = res.json()
159
+
160
+ if res_data.get("returnCode") == "0000":
161
+ # 1. 更新資料庫狀態為已付款
162
+ update_res = supabase.table("bookings").update({"status": "待處理 (已付訂金)"}).ilike("remarks", f"%{payload.order_id}%").execute()
163
+
164
+ # 2. 發送通知給老闆
165
+ if update_res.data:
166
+ b = update_res.data[0]
167
+ notify_boss(b['name'], b['tel'], b['date'], b['time'], b['pax'], payload.amount)
168
+
169
+ return {"status": "success", "message": "付款確認成功"}
170
+ else:
171
+ raise HTTPException(status_code=400, detail=res_data.get('returnMessage'))
172
  except Exception as e:
173
+ raise HTTPException(status_code=500, detail=str(e))
174
 
175
+ def notify_boss(name, tel, date, time, pax, amount):
176
+ if not LINE_ACCESS_TOKEN or not BOSS_LINE_ID: return
177
+ msg = f"🔔 【新訂單通知】\n姓名:{name}\n電話:{tel}\n時間:{date} {time}\n人數:{pax}位"
178
+ if amount > 0: msg += f"\n💰 已收到線上付款:${amount}"
179
+
180
  headers = {"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"}
181
  payload = {"to": BOSS_LINE_ID, "messages": [{"type": "text", "text": msg}]}
182
+ try: requests.post("https://api.line.me/v2/bot/message/push", headers=headers, json=payload)
183
+ except: pass