Create main.py
Browse files
main.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from typing import Optional, Dict
|
| 5 |
+
from supabase import create_client, Client
|
| 6 |
+
import os
|
| 7 |
+
import requests
|
| 8 |
+
import uuid
|
| 9 |
+
|
| 10 |
+
app = FastAPI(title="Cié Cié Backend API")
|
| 11 |
+
|
| 12 |
+
# 🌟 解決 CORS (跨域) 問題:允許您的 GitHub Pages 前端呼叫這台主機
|
| 13 |
+
app.add_middleware(
|
| 14 |
+
CORSMiddleware,
|
| 15 |
+
allow_origins=["*"], # 正式上線可改為 ["https://ciecietaipei.github.io"]
|
| 16 |
+
allow_credentials=True,
|
| 17 |
+
allow_methods=["*"],
|
| 18 |
+
allow_headers=["*"],
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# 讀取環境變數 (請在 Hugging Face Settings 中設定)
|
| 22 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
|
| 23 |
+
# ⚠️ 注意:這裡必須使用 service_role key,才能無視 RLS 直接查核黑名單與寫入!
|
| 24 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
|
| 25 |
+
|
| 26 |
+
LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN", "")
|
| 27 |
+
BOSS_LINE_ID = os.getenv("BOSS_LINE_ID", "") # 老闆的 LINE User ID
|
| 28 |
+
|
| 29 |
+
# 初始化 Supabase
|
| 30 |
+
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) if SUPABASE_URL else None
|
| 31 |
+
|
| 32 |
+
# 定義前端傳來的資料結構 (Payload)
|
| 33 |
+
class OrderPayload(BaseModel):
|
| 34 |
+
service_type: str
|
| 35 |
+
name: str
|
| 36 |
+
tel: str
|
| 37 |
+
date: str
|
| 38 |
+
time: str
|
| 39 |
+
line_id: Optional[str] = ""
|
| 40 |
+
pax: int = 2
|
| 41 |
+
cart: Dict[str, int] = {}
|
| 42 |
+
deposit_required: int = 0
|
| 43 |
+
total_amount: int = 0
|
| 44 |
+
|
| 45 |
+
@app.get("/")
|
| 46 |
+
def read_root():
|
| 47 |
+
return {"status": "online", "message": "Cié Cié FastAPI is running."}
|
| 48 |
+
|
| 49 |
+
@app.post("/api/submit_booking")
|
| 50 |
+
async def submit_booking(payload: OrderPayload):
|
| 51 |
+
if not supabase:
|
| 52 |
+
raise HTTPException(status_code=500, detail="資料庫未設定")
|
| 53 |
+
|
| 54 |
+
# ==========================================
|
| 55 |
+
# 🕵️♂️ 階段 1:查核 No-Show 黑名單
|
| 56 |
+
# ==========================================
|
| 57 |
+
is_noshow = False
|
| 58 |
+
try:
|
| 59 |
+
# 用電話號碼去資料庫找有沒有 No-Show 紀錄
|
| 60 |
+
res = supabase.table("bookings").select("id").eq("tel", payload.tel).eq("status", "No-Show").execute()
|
| 61 |
+
is_noshow = len(res.data) > 0
|
| 62 |
+
except Exception as e:
|
| 63 |
+
print("查詢黑名單失敗:", e)
|
| 64 |
+
|
| 65 |
+
# 決定最終訂金:如果是黑名單,原本不用付訂金的也強制收 $1000
|
| 66 |
+
final_deposit = payload.deposit_required
|
| 67 |
+
if is_noshow and final_deposit == 0:
|
| 68 |
+
final_deposit = 1000
|
| 69 |
+
|
| 70 |
+
# ==========================================
|
| 71 |
+
# 💳 階段 2:金流分流處理 (需要收訂金 vs 不用收訂金)
|
| 72 |
+
# ==========================================
|
| 73 |
+
|
| 74 |
+
# 狀況 A:需要付款 (產生 LINE Pay 連結)
|
| 75 |
+
if final_deposit > 0:
|
| 76 |
+
order_id = f"ORDER-{uuid.uuid4().hex[:8].upper()}"
|
| 77 |
+
|
| 78 |
+
# ⚠️ 這裡未來會串接真實的 LINE Pay API,目前先回傳模擬的結帳網址
|
| 79 |
+
mock_payment_url = f"https://sandbox-web-pay.line.me/web/payment/wait?transactionReserveId=mock&orderId={order_id}"
|
| 80 |
+
|
| 81 |
+
# 我們不先把資料寫入資料庫,而是等他「付款成功」的 Webhook 再寫入
|
| 82 |
+
return {
|
| 83 |
+
"status": "require_payment",
|
| 84 |
+
"message": "訂單需支付訂金",
|
| 85 |
+
"is_noshow_penalty": is_noshow, # 讓前端知道是不是因為被懲罰才要付錢
|
| 86 |
+
"deposit_amount": final_deposit,
|
| 87 |
+
"payment_url": mock_payment_url,
|
| 88 |
+
"order_id": order_id
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
# 狀況 B:不需付款 (直接寫入資料庫並完成訂位)
|
| 92 |
+
booking_data = {
|
| 93 |
+
"name": payload.name,
|
| 94 |
+
"tel": payload.tel,
|
| 95 |
+
"date": payload.date,
|
| 96 |
+
"time": payload.time,
|
| 97 |
+
"pax": payload.pax,
|
| 98 |
+
"email": "", # 可由前端擴充
|
| 99 |
+
"user_id": payload.line_id,
|
| 100 |
+
"status": "待處理",
|
| 101 |
+
"remarks": f"類型: {'外帶' if payload.service_type == 'takeout' else '內用'}\n餐點內容: {payload.cart}"
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
try:
|
| 105 |
+
supabase.table("bookings").insert(booking_data).execute()
|
| 106 |
+
|
| 107 |
+
# 🔔 通知老闆有新訂位
|
| 108 |
+
notify_boss(payload.name, payload.tel, payload.date, payload.time, payload.pax)
|
| 109 |
+
|
| 110 |
+
return {
|
| 111 |
+
"status": "success",
|
| 112 |
+
"message": "訂位已成功建立!"
|
| 113 |
+
}
|
| 114 |
+
except Exception as e:
|
| 115 |
+
raise HTTPException(status_code=500, detail=f"寫入資料庫失敗: {str(e)}")
|
| 116 |
+
|
| 117 |
+
def notify_boss(name, tel, date, time, pax):
|
| 118 |
+
"""發送 LINE 通知給老闆 (需設定環境變數)"""
|
| 119 |
+
if not LINE_ACCESS_TOKEN or not BOSS_LINE_ID:
|
| 120 |
+
return
|
| 121 |
+
msg = f"🔔 【新訂位通知】\n姓名:{name}\n電話:{tel}\n時間:{date} {time}\n人數:{pax}位"
|
| 122 |
+
headers = {"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"}
|
| 123 |
+
payload = {"to": BOSS_LINE_ID, "messages": [{"type": "text", "text": msg}]}
|
| 124 |
+
try:
|
| 125 |
+
requests.post("https://api.line.me/v2/bot/message/push", headers=headers, json=payload)
|
| 126 |
+
except:
|
| 127 |
+
pass
|