Update main.py
Browse files💣 地雷一:空購物車或異常資料會讓「庫存 API」直接崩潰 (Server Error 500)
💣 地雷二:「補付款 API」金額鎖死 1000 元,外帶掉單會虧錢!
main.py
CHANGED
|
@@ -63,7 +63,7 @@ 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秒)
|
|
@@ -135,7 +135,6 @@ async def auto_rescue_dropped_order(order_id: str, amount: int):
|
|
| 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:
|
|
@@ -238,10 +237,7 @@ async def confirm_payment(payload: ConfirmPayload):
|
|
| 238 |
res_data = res.json()
|
| 239 |
|
| 240 |
if res_data.get("returnCode") == "0000":
|
| 241 |
-
# 1. 更新資料庫狀態為已付款
|
| 242 |
update_res = supabase.table("bookings").update({"status": "待處理 (已付訂金)"}).ilike("remarks", f"%{payload.order_id}%").execute()
|
| 243 |
-
|
| 244 |
-
# 2. 發送通知給老闆
|
| 245 |
if update_res.data:
|
| 246 |
b = update_res.data[0]
|
| 247 |
notify_boss(b['name'], b['tel'], b['date'], b['time'], b['pax'], payload.amount)
|
|
@@ -252,7 +248,7 @@ 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="資料庫未連線")
|
|
@@ -265,7 +261,22 @@ async def repay_payment(payload: RepayPayload):
|
|
| 265 |
if "已付" in booking.get("status", "") or "確認" in booking.get("status", ""):
|
| 266 |
raise HTTPException(status_code=400, detail="此訂單已完成付款或確認,無需重新結帳")
|
| 267 |
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
new_order_id = f"{payload.order_id}-R{int(time.time())}"
|
| 270 |
|
| 271 |
request_body = {
|
|
@@ -273,8 +284,8 @@ async def repay_payment(payload: RepayPayload):
|
|
| 273 |
"currency": "TWD",
|
| 274 |
"orderId": new_order_id,
|
| 275 |
"packages": [{
|
| 276 |
-
"id": "pkg_repay", "amount": amount, "name": "Cié Cié Taipei 補繳
|
| 277 |
-
"products": [{"name": "餐飲訂金", "quantity": 1, "price": amount}]
|
| 278 |
}],
|
| 279 |
"redirectUrls": {
|
| 280 |
"confirmUrl": f"{RETURN_URL}?action=payment_confirm&amount={amount}&orderId={payload.order_id}",
|
|
@@ -315,7 +326,7 @@ def notify_boss(name, tel, date, time, pax, amount):
|
|
| 315 |
except: pass
|
| 316 |
|
| 317 |
# ==========================================
|
| 318 |
-
# 🌟
|
| 319 |
# ==========================================
|
| 320 |
@app.get("/api/inventory/{query_date}")
|
| 321 |
async def get_inventory(query_date: str):
|
|
@@ -330,15 +341,21 @@ async def get_inventory(query_date: str):
|
|
| 330 |
if "取消" in b.get("status", "") or "No-Show" in b.get("status", ""):
|
| 331 |
continue
|
| 332 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
# 計算這筆訂單買了什麼
|
| 334 |
-
cart = b.get("cart", {})
|
| 335 |
for item_id, qty in cart.items():
|
| 336 |
-
# 防呆:確保 qty 是整數
|
| 337 |
try: qty = int(qty)
|
| 338 |
except: qty = 0
|
| 339 |
sold_counts[item_id] = sold_counts.get(item_id, 0) + qty
|
| 340 |
|
| 341 |
-
return sold_counts
|
| 342 |
except Exception as e:
|
| 343 |
print(f"Inventory Error: {e}")
|
| 344 |
-
return {}
|
|
|
|
| 63 |
order_id: str
|
| 64 |
|
| 65 |
# ==========================================
|
| 66 |
+
# 🌟 背景自動救援掉單機器人 🌟
|
| 67 |
# ==========================================
|
| 68 |
async def auto_rescue_dropped_order(order_id: str, amount: int):
|
| 69 |
# 讓程式在背景默默等待 3 分鐘 (180秒)
|
|
|
|
| 135 |
def read_root():
|
| 136 |
return {"status": "online", "message": "Cié Cié FastAPI is running."}
|
| 137 |
|
|
|
|
| 138 |
@app.post("/api/submit_booking")
|
| 139 |
async def submit_booking(payload: OrderPayload, background_tasks: BackgroundTasks):
|
| 140 |
if not supabase:
|
|
|
|
| 237 |
res_data = res.json()
|
| 238 |
|
| 239 |
if res_data.get("returnCode") == "0000":
|
|
|
|
| 240 |
update_res = supabase.table("bookings").update({"status": "待處理 (已付訂金)"}).ilike("remarks", f"%{payload.order_id}%").execute()
|
|
|
|
|
|
|
| 241 |
if update_res.data:
|
| 242 |
b = update_res.data[0]
|
| 243 |
notify_boss(b['name'], b['tel'], b['date'], b['time'], b['pax'], payload.amount)
|
|
|
|
| 248 |
except Exception as e:
|
| 249 |
raise HTTPException(status_code=500, detail=str(e))
|
| 250 |
|
| 251 |
+
# 處理重新產生付款連結的 API (防呆升級版)
|
| 252 |
@app.post("/api/linepay/repay")
|
| 253 |
async def repay_payment(payload: RepayPayload):
|
| 254 |
if not supabase: raise HTTPException(status_code=500, detail="資料庫未連線")
|
|
|
|
| 261 |
if "已付" in booking.get("status", "") or "確認" in booking.get("status", ""):
|
| 262 |
raise HTTPException(status_code=400, detail="此訂單已完成付款或確認,無需重新結帳")
|
| 263 |
|
| 264 |
+
# 🌟 修改:向 LINE Pay 查詢最初應付的正確金額,避免外帶單只收 1000 元 🌟
|
| 265 |
+
amount = 1000 # 預設防護底線
|
| 266 |
+
try:
|
| 267 |
+
chk_uri = "/v3/payments"
|
| 268 |
+
chk_query = urllib.parse.urlencode({"orderId": payload.order_id})
|
| 269 |
+
chk_nonce = str(uuid.uuid4())
|
| 270 |
+
chk_msg = LINE_PAY_CHANNEL_SECRET + chk_uri + chk_query + chk_nonce
|
| 271 |
+
chk_sig = base64.b64encode(hmac.new(LINE_PAY_CHANNEL_SECRET.encode(), chk_msg.encode(), hashlib.sha256).digest()).decode()
|
| 272 |
+
chk_headers = { "Content-Type": "application/json", "X-LINE-ChannelId": LINE_PAY_CHANNEL_ID, "X-LINE-Authorization-Nonce": chk_nonce, "X-LINE-Authorization": chk_sig }
|
| 273 |
+
chk_res = requests.get(f"{LINE_PAY_BASE_URL}{chk_uri}?{chk_query}", headers=chk_headers).json()
|
| 274 |
+
if chk_res.get("returnCode") == "0000" and chk_res.get("info"):
|
| 275 |
+
# 成功抓出這筆訂單原本設定的金額!
|
| 276 |
+
amount = chk_res["info"][0].get("payInfo", [{}])[0].get("amount", 1000)
|
| 277 |
+
except Exception as e:
|
| 278 |
+
print(f"無法取得原始金額,使用預設值: {e}")
|
| 279 |
+
|
| 280 |
new_order_id = f"{payload.order_id}-R{int(time.time())}"
|
| 281 |
|
| 282 |
request_body = {
|
|
|
|
| 284 |
"currency": "TWD",
|
| 285 |
"orderId": new_order_id,
|
| 286 |
"packages": [{
|
| 287 |
+
"id": "pkg_repay", "amount": amount, "name": "Cié Cié Taipei 補繳結帳",
|
| 288 |
+
"products": [{"name": "餐飲訂金或外帶全額", "quantity": 1, "price": amount}]
|
| 289 |
}],
|
| 290 |
"redirectUrls": {
|
| 291 |
"confirmUrl": f"{RETURN_URL}?action=payment_confirm&amount={amount}&orderId={payload.order_id}",
|
|
|
|
| 326 |
except: pass
|
| 327 |
|
| 328 |
# ==========================================
|
| 329 |
+
# 🌟 計算某日庫存與銷量的 API (防護升級版) 🌟
|
| 330 |
# ==========================================
|
| 331 |
@app.get("/api/inventory/{query_date}")
|
| 332 |
async def get_inventory(query_date: str):
|
|
|
|
| 341 |
if "取消" in b.get("status", "") or "No-Show" in b.get("status", ""):
|
| 342 |
continue
|
| 343 |
|
| 344 |
+
# 🌟 修改:加入型別檢查防護,避免 null 或 字串 引發當機
|
| 345 |
+
cart = b.get("cart")
|
| 346 |
+
if not cart:
|
| 347 |
+
cart = {}
|
| 348 |
+
elif isinstance(cart, str):
|
| 349 |
+
try: cart = json.loads(cart)
|
| 350 |
+
except: cart = {}
|
| 351 |
+
|
| 352 |
# 計算這筆訂單買了什麼
|
|
|
|
| 353 |
for item_id, qty in cart.items():
|
|
|
|
| 354 |
try: qty = int(qty)
|
| 355 |
except: qty = 0
|
| 356 |
sold_counts[item_id] = sold_counts.get(item_id, 0) + qty
|
| 357 |
|
| 358 |
+
return sold_counts
|
| 359 |
except Exception as e:
|
| 360 |
print(f"Inventory Error: {e}")
|
| 361 |
+
return {}
|