餐廳點餐軟體通常敗在兩處:陌生人能把菜點到不屬於他的桌上的那一刻,以及顧客螢幕上的總額與收銀機上的總額對不上的那一刻。Korat 的點餐模組是從這兩點倒推著設計的。桌臺會話由店員開出,攜帶單次有效的令牌;帳單就是收銀機讀的那幾行;place_dining_order 在伺服器上給每一行定價。選項組、自助餐倒計時和拆賬檢視,都建在這兩條保證之上。
桌臺二維碼是單次有效的令牌,不是桌號
這裡刻意沒有“手動輸入桌號”的入口。那是最容易想到的功能,也是那麼多堂食系統能從停車場被濫用的原因:如果唯一的憑據是印在桌上的號碼,那麼任何在這家店吃過一次的人,從此都能往任何一桌點菜。取而代之的是:店員從控制檯開臺,開臺會鑄出一枚新的、單次有效的令牌,嵌進那一桌顯示的二維碼裡。拍下來的二維碼、轉發給朋友的截圖、上週二列印的那張,攜帶的都是已經不再有效的令牌,掃不出結果。
走到桌邊的食客既不是這家店的成員,也不是一個尚不存在的會話的主人,所以在行級安全之下他根本讀不到 dining_sessions 表——不是讀不到別人的行,是一行也讀不到。RLS 能判斷誰在呼叫,卻無法判斷請求是否攜帶了有效令牌。所以掃碼走的是 join_session_by_token,一個 SECURITY DEFINER 函式:它在服務端匹配令牌,把第一個掃碼的人設為主人,匹配不上就返回空——而且不說為什麼。一個能區分“令牌錯誤”和“令牌過期”的報錯,就是一臺猜測預言機。
進來之後,主人可以用連結或聊天卡片把同桌其餘的人拉進來,晚到的朋友加入同一個會話,點的菜落在同一張帳單上。
桌臺二維碼是 https 網址:應用裡能開,網頁也能開
桌上的二維碼是一個 https 網址,不是應用私有的協議。裝了 Korat 的手機會透過已驗證域名的 App Link 在應用裡開啟;沒裝的手機——包括每一臺 iPhone——則會開啟網頁版點單頁。這修好的是整個產品裡曾經最死的一個瞬間:顧客坐下、舉起相機掃碼,然後什麼都沒發生。那個頁面在未登入狀態下就能讀到菜單、自助餐倒計時和酒類規則,只在真正提交訂單的那一刻才要求登入,而那是唯一躲不掉的時刻。早先印好、帶著 korat://dine 的二維碼,永遠都還能掃。
點單:選項組、備註,和一份知道自己快打烊的菜單
菜單按分類組織,每個菜品都可以帶選項組——多加一份濃縮、辣度、麵條種類——帶價格差、必選標記和最多可選數量。價格差掛在選項上,而不是打進備註裡,這也是廚房小票和帳單行對得上的原因。菜品還接受自由文字備註和數量,購物車在發出之前一直可編輯。
營業時間、最後點單緩衝與酒類年齡核驗
店家設定營業時間和一段最後點單緩衝。接近這段緩衝時,菜單會提示廚房即將打烊;過了這段時間,點單被直接阻止,而不是先接下來再悄悄取消。標為不可用或缺貨的菜品會在卡片上寫明,出現在點選之前。酒類有自己的標記:按食客生日做年齡核對,店員也可以一次性為整桌完成年齡核驗。
確認佇列:沒有人點頭,訂單就不進廚房
提交的訂單不會直接送到出餐口。它們落進廚房顯示屏上的一個待確認佇列,由店員逐單確認或拒絕,之後才變成活兒。允許一部手機把小票直接塞進廚房的系統,等於把“讓店家白白損失食材”的能力交給了陌生人,同時也拿掉了那個由人發現“六號桌不小心點了四份一樣的套餐”的時刻。
在廚房顯示屏上,店員推進訂單狀態;控制檯每隔幾秒重新整理的實時角標顯示待處理的量——進行中的訂單、正在呼叫服務員的桌、待確認的訂位、新的外送單——一旦有桌按下呼叫服務員,頂部立刻出現紅色橫幅。食客拿到這個迴路的另一半:呼叫服務員、要帳單、或者離席。
實時逐項帳單,與 PromptPay 二維碼拆賬
實時帳單顯示的正是收銀機讀的那幾行,隨著出餐確認而更新——沒有飯後揭曉,也不需要靠回憶重建。桌臺結束時,應用生成一張收尾頁,包含把同桌的人加為好友的建議,之後還有一次評價提醒;收據留在個人的帳單歷史裡。
拆賬:我該付多少
拆賬檢視回答人們真正會爭論的那個問題:我該付多少。每個人看到自己的單點小計,加上桌上任何自助餐套餐的均攤份額。可以按金額生成 PromptPay 二維碼,採用帶 CRC-16 校驗的泰國標準 QR 格式。
拆賬是一份明細,不是分別付款。它計算每個人該付多少;它不會為四份份額刷四張卡。錢怎麼分,仍然發生在同桌的人和店家之間。
而且刻意沒有“手動輸入桌號”的備用路徑。如果食客的二維碼掃不出來,店員從控制檯重新開臺。加一條手動入口,等於把當初鑄造令牌所要堵上的那個洞原樣還回去。
自助餐:人均套餐、倒計時與剩菜計費
自助餐有自己的模型,而不是用一個按人頭計價的菜品糊弄過去。一個套餐有人均價、可由店員延長的倒計時,以及分開的成人與兒童人數。菜品帶自助餐角色——包含、僅單點、或包含但加價——所以一份菜單同時服務兩類顧客。剩菜計費也支援,帶單位標籤:那條大多數泰國自助餐早就印在桌上、而大多數軟體假裝不存在的規則。
外送、自取與桌位預訂
同一份菜單也支撐外送和自取。訂單帶配送費、配送方、預約取餐時間和地址,在新單、製作中、配送中和已完成之間流轉,每次狀態變化兩側都收到通知。訂位收集日期、時間、人數和備註,等店家確認——待處理數量以實時角標停在儀表盤上。
每一個價格都在伺服器上計算
讓上述一切成立的那條規則是:客戶端從不設定價格。place_dining_order 和 place_shop_order 從資料庫讀取菜品價格、選項價差和配送費,忽略手機傳來的任何值。一個被信任的客戶端欄位就是一張自助折扣券。同樣的原則覆蓋評價——只有資料庫能證明確實堂食過或點過外送的顧客才能寫評價,而星級由觸發器重算,從來不是誰手填的。
不想讓客人坐下等、只想發號的,看排隊叫號;廚房以外的商品也要賣出去(外帶、周邊、預製包)的,在零售裡和店內庫存共用同一份資料。