Korat
功能

餐厅收银、后厨显示屏和桌边扫码点单

餐厅点餐系统从桌台开始:食客扫一下二维码就进了菜单。店家拿到厨房显示屏、一份和收银机对得上的逐项账单,以及一个外送队列——而没有任何一个价格,是由发出请求的那部手机说了算的。整套免费,没有月费。

餐厅点餐软件通常败在两处:陌生人能把菜点到不属于他的桌上的那一刻,以及顾客屏幕上的总额与收银机上的总额对不上的那一刻。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_orderplace_shop_order 从数据库读取菜品价格、选项价差和配送费,忽略手机传来的任何值。一个被信任的客户端字段就是一张自助折扣券。同样的原则覆盖评价——只有数据库能证明确实堂食过或点过外送的顾客才能写评价,而星级由触发器重算,从来不是谁手填的。

不想让客人坐下等、只想发号的,看排队叫号;厨房以外的商品也要卖出去(外带、周边、预制包)的,在零售里和店内库存共用同一份数据。

厨房和外场今天谁上班、谁请假,来自我的职场那张排班表;第一次把一家真能干活的店开出来,从面向商家那一页开始。

常见问题

顾客能不扫桌上的二维码就点单吗?

堂食不能。这里没有手动输入桌号,因为印出来的号码是一份全城迟早都有的凭据。店员从控制台开台,开台会铸出二维码所携带的令牌。

外送和自取则相反——那些从店铺主页下单,不涉及任何桌台。

有人拍下我们的桌台二维码,事后能点单吗?

不能。二维码里的令牌是单次有效的,且绑定在店员开出的那次会话上。会话一结束,令牌就匹配不上,扫码解析不出任何东西。这项检查发生在 SECURITY DEFINER 数据库函数里而不是应用里,所以直接调用 API 也绕不过去。

订单会直接进厨房吗?

不会。每一单先落进厨房显示屏上的待确认队列,由店员确认或拒绝。这一步确认,正是错误和滥用在造成食材损失之前被拦住的地方。

同桌的每个人能各自刷卡付自己那份吗?

不能。拆账视图只计算每个人该付多少——自己点的东西,加上任何自助餐套餐的均摊份额——付款仍然和店家结清。Korat 不处理按人分开的刷卡付款。

顾客卡在我们打烊那一刻下单,会怎样?

店家配置营业时间和一段最后点单缓冲。在缓冲期内,应用会提示厨房即将打烊;过了这段时间,点单被直接阻止,而不是先接下来事后再取消。

开一桌,把回路走一遍

食客那一侧和控制台那一侧是同一个应用。装上它,建一家店,扫你自己的二维码。