Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from typing import Optional, Dict | |
| from supabase import create_client, Client | |
| import os | |
| import requests | |
| import uuid | |
| app = FastAPI(title="Cié Cié Backend API") | |
| # 🌟 解決 CORS (跨域) 問題:允許您的 GitHub Pages 前端呼叫這台主機 | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # 正式上線可改為 ["https://ciecietaipei.github.io"] | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # 讀取環境變數 (請在 Hugging Face Settings 中設定) | |
| SUPABASE_URL = os.getenv("SUPABASE_URL", "") | |
| # ⚠️ 注意:這裡必須使用 service_role key,才能無視 RLS 直接查核黑名單與寫入! | |
| SUPABASE_KEY = os.getenv("SUPABASE_KEY", "") | |
| LINE_ACCESS_TOKEN = os.getenv("LINE_ACCESS_TOKEN", "") | |
| BOSS_LINE_ID = os.getenv("BOSS_LINE_ID", "") # 老闆的 LINE User ID | |
| # 初始化 Supabase | |
| supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) if SUPABASE_URL else None | |
| # 定義前端傳來的資料結構 (Payload) | |
| class OrderPayload(BaseModel): | |
| service_type: str | |
| name: str | |
| tel: str | |
| date: str | |
| time: str | |
| line_id: Optional[str] = "" | |
| pax: int = 2 | |
| cart: Dict[str, int] = {} | |
| deposit_required: int = 0 | |
| total_amount: int = 0 | |
| def read_root(): | |
| return {"status": "online", "message": "Cié Cié FastAPI is running."} | |
| async def submit_booking(payload: OrderPayload): | |
| if not supabase: | |
| raise HTTPException(status_code=500, detail="資料庫未設定") | |
| # ========================================== | |
| # 🕵️♂️ 階段 1:查核 No-Show 黑名單 | |
| # ========================================== | |
| is_noshow = False | |
| try: | |
| # 用電話號碼去資料庫找有沒有 No-Show 紀錄 | |
| res = supabase.table("bookings").select("id").eq("tel", payload.tel).eq("status", "No-Show").execute() | |
| is_noshow = len(res.data) > 0 | |
| except Exception as e: | |
| print("查詢黑名單失敗:", e) | |
| # 決定最終訂金:如果是黑名單,原本不用付訂金的也強制收 $1000 | |
| final_deposit = payload.deposit_required | |
| if is_noshow and final_deposit == 0: | |
| final_deposit = 1000 | |
| # ========================================== | |
| # 💳 階段 2:金流分流處理 (需要收訂金 vs 不用收訂金) | |
| # ========================================== | |
| # 狀況 A:需要付款 (產生 LINE Pay 連結) | |
| if final_deposit > 0: | |
| order_id = f"ORDER-{uuid.uuid4().hex[:8].upper()}" | |
| # ⚠️ 這裡未來會串接真實的 LINE Pay API,目前先回傳模擬的結帳網址 | |
| mock_payment_url = f"https://sandbox-web-pay.line.me/web/payment/wait?transactionReserveId=mock&orderId={order_id}" | |
| # 我們不先把資料寫入資料庫,而是等他「付款成功」的 Webhook 再寫入 | |
| return { | |
| "status": "require_payment", | |
| "message": "訂單需支付訂金", | |
| "is_noshow_penalty": is_noshow, # 讓前端知道是不是因為被懲罰才要付錢 | |
| "deposit_amount": final_deposit, | |
| "payment_url": mock_payment_url, | |
| "order_id": order_id | |
| } | |
| # 狀況 B:不需付款 (直接寫入資料庫並完成訂位) | |
| booking_data = { | |
| "name": payload.name, | |
| "tel": payload.tel, | |
| "date": payload.date, | |
| "time": payload.time, | |
| "pax": payload.pax, | |
| "email": "", # 可由前端擴充 | |
| "user_id": payload.line_id, | |
| "status": "待處理", | |
| "remarks": f"類型: {'外帶' if payload.service_type == 'takeout' else '內用'}\n餐點內容: {payload.cart}" | |
| } | |
| try: | |
| supabase.table("bookings").insert(booking_data).execute() | |
| # 🔔 通知老闆有新訂位 | |
| notify_boss(payload.name, payload.tel, payload.date, payload.time, payload.pax) | |
| return { | |
| "status": "success", | |
| "message": "訂位已成功建立!" | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"寫入資料庫失敗: {str(e)}") | |
| def notify_boss(name, tel, date, time, pax): | |
| """發送 LINE 通知給老闆 (需設定環境變數)""" | |
| if not LINE_ACCESS_TOKEN or not BOSS_LINE_ID: | |
| return | |
| msg = f"🔔 【新訂位通知】\n姓名:{name}\n電話:{tel}\n時間:{date} {time}\n人數:{pax}位" | |
| headers = {"Authorization": f"Bearer {LINE_ACCESS_TOKEN}"} | |
| payload = {"to": BOSS_LINE_ID, "messages": [{"type": "text", "text": msg}]} | |
| try: | |
| requests.post("https://api.line.me/v2/bot/message/push", headers=headers, json=payload) | |
| except: | |
| pass |