扫码点餐 · 技术设计文档
v0.1 方案评审稿|2026-10-25|尚未开发或选定供应商
1. 架构选择
建议首期采用模块化单体 API + 独立后台任务与打印适配器,避免小规模阶段过早拆分微服务。顾客端移动 Web/PWA,商户移动 Web 后台;需后台接单和内置打印 SDK 的设备使用 Android 原生接单 APP。Web 浏览器不能直接稳定驱动任意 POS 内置打印机。
建议栈:TypeScript 前端(React/Vue 二选一,团队能力决定)、TypeScript API(NestJS 等)、PostgreSQL 事务库、Redis 短时缓存与限流、持久任务队列、对象存储与 CDN。打印代理建议 Kotlin 接各厂商 Android SDK。此处为候选设计,不锁定开发团队或部署云。
flowchart TD
C[顾客移动 Web · 四语] --> API[API · 身份/来源/限流]
M[商户移动后台] --> API
A[Android 接单 APP] --> API
API --> CORE[菜单 / 会话 / 订单 / 台账 / 支付 / 打印配置]
CORE --> DB[(PostgreSQL + Outbox)]
DB --> W[事件及后台任务 Worker]
W --> PAY[支付服务商适配器]
PAY --> CB[签名校验回调入口]
CB --> CORE
W --> N[长连接 / 系统推送 / 未确认升级提醒]
N --> A
W --> R[打印路由与任务账本]
R --> S[厂商云打印 API / CloudPRNT]
R --> L[店内 Android 或网关代理]
L --> P[内置 / USB / LAN 打印机]
S --> CP[云打印机]
推荐云区域按泰国访问性能、服务商支持、备份与数据政策评估;可候选新加坡区域,不能把候选地点等同于合规结论。首期按单区域高可用设计,跨区灾备后续按预算确定。
2. 领域模型与隔离
| 实体 | 关键字段与约束 |
|---|---|
| Merchant / Store | tenant_id、store_id、country、currency、timezone、status;收款主体绑定到店 |
| Location / QRLocator | store_id、type、label、opaque_token_hash、version、active;轮换撤销旧码 |
| DiningSession | location_id、session_id、policy_snapshot、status、expires_at;同一位置最多一笔活动后付台账 |
| GuestSession | 随机会话令牌、服务位置、可访问本人订单、语言;不放个人身份证明 |
| Product / Variant / Modifier | SKU、最小货币单位价格、必选限制、库存、版本 |
| ProductTranslation | product_id、locale、name、description、allergens;语言不复制 SKU |
| Category / Department | 销售分类与生产部门分别维护;组件可覆盖部门 |
| Order / OrderLine | tenant/store/session、order_version、policy、prices/tax/name/department 快照、batch |
| Bill / BillLine | 后付台账、冻结版本、未付额、结账锁;追踪每笔订单是否已计入 |
| PaymentAttempt | order_id 或 bill_id、provider、provider_ref、amount、currency、status、expires_at |
| Refund | payment_id、amount、reason、approval、provider_ref、status;退款累计不超过已付 |
| Device / DeviceStoreGrant | 租户归属、认证凭证、门店授权、能力、固件、在线状态;凭证可撤销 |
| PrintRoute / Template | 事件、门店/部门、设备、份数、版本、启停、主备策略 |
| PrintJob / Attempt | business_event_id、order_version、destination、copy_no、payload_hash、ack_level |
| Outbox / WebhookInbox | 事务事件与已处理回调;唯一事件标识,防重复 |
| AuditLog | 操作人、门店、对象、前后值、原因、时间;退款/改价/补打均记录 |
所有业务表带 tenant_id,查询先校验租户与门店授权;可叠加 PostgreSQL 行级安全。共享设备仅通过 DeviceStoreGrant 读取获授权门店的最小打印内容,不允许借设备凭证调用完整订单列表。跨商户共享首期关闭。
订单保存价格、税费、规格、商品名和生产部门快照。后续菜单、分类或规则变化不改变历史账单及重打内容。个别打印错误纠正应形成新修订并审计。
3. 金额、库存与并发
- THB 以 satang 整数存储(฿125.50 = 12550),后端计价;不能信任客户端提交的价格、部门、租户、策略或总额。
- 税费配置需区分含税/未税、服务费计税顺序、折扣分摊、行级或单级舍入,保存规则版本。首期 Demo 假设价格已含税、服务费为零,不据此推断泰国实际税制。
- 下单事务校验商品版本、可售时段、规格、库存与会话。先付短时库存预留,超时释放;后付接单时确认库存。实际保留时长由渠道支付有效期决定。
- 客户端生成 submission_id,唯一键 tenant + guest + submission_id;重试返回原单。金额变化后必须确认新版本并重新创建支付尝试。
- 同一台账最多一个活动结账版本;支付尝试锁定相同应收快照。并发加单、关台、退款用行锁/乐观版本校验防止重复结算。
4. 订单、支付与台账状态
订单履约与支付是两个状态机,不能用一个 status 混合表达。
| 维度 | 状态及主要转换 |
|---|---|
| 履约 | PENDING_PAYMENT → PENDING_ACCEPTANCE → ACCEPTED → PREPARING → READY → SERVED → COMPLETED;拒单进入 REJECTED;取消进入 CANCELLED |
| 支付尝试 | CREATED → PENDING → SUCCEEDED / FAILED / EXPIRED;SUCCEEDED 不因迟到失败回调回退 |
| 财务汇总 | UNPAID / PAID / PARTIALLY_REFUNDED / REFUNDED;由成功支付与退款账本计算 |
| 退款 | REQUESTED → APPROVED → PROCESSING → SUCCEEDED / FAILED;失败可人工处理并保留原记录 |
| 后付台账 | OPEN → CHECKOUT_LOCKED → SETTLED → CLOSED;未支付且渠道已确认取消/失效才可安全解锁 |
先付:创建待付款订单,支付成功并持久化后进入待接单;自动接单可同事务或可靠事件执行。人工接单成功才发布 ProductionReleased 事件。后付:下单直接待接单,接单后生产;整桌支付成功只结账,不能再触发生产打印。
客房服务的 READY → DELIVERING → SERVED 可作为场景分支。各部门单独维护任务状态;订单准备完成需所有必要部门完成,打印 ACK 不算制作完成。
关键异常:先付过期订单收到迟到到账,不自动丢弃;验证 provider_ref 与金额后进入 PAID_LATE_REVIEW,视库存与营业状态人工接单或退款。已成功尝试不允许再发起新付款;并发成功导致多付需进入差额异常账本及退款流程。
5. 支付集成边界
本期只列渠道,不写支付接入。未来建议每店通过合适的持牌服务商商户账户收款;平台不默认归集资金或自行分账。商户入驻、结算币种、退款能力、渠道在线/线下模式由服务商合同确定。
接口抽象:createPayment、getPayment、cancel/expire(如支持)、requestRefund、getRefund、verifyWebhook。记录 provider 的原始有限字段及不可逆脱敏日志,不存卡号/CVV;卡支付优先托管页/服务商 tokenization。
回调流程:验证服务商支持的签名/认证及防重放机制 → 插入 Inbox 去重 → 再查支付状态(按服务商规范)→ 校验商户收款账户、provider_ref、金额和币种 → 锁定支付尝试 → 写支付账本与订单变更及 Outbox → 提交事务 → 返回成功。不能假定每个服务商有相同的签名机制;不具备签名时使用其认证与主动查询方案。IP 白名单只能补充,不能替代可信到账核验。
回调超时/丢失由对账任务补偿。浏览器回跳只触发状态查询,不修改财务状态。付款二维码包含有效期及金额绑定;支付二维码与桌面来源二维码是两类不同对象。
退款原路能力、时效、部分退款、手续费退还及收款人补充资料按渠道逐一确认;不能承诺 PromptPay、钱包、卡具备同样退款体验。每日对账比较订单应收、成功扣款、退款和渠道结算,差异进入人工队列。
6. 打印路由与可靠性
6.1 任务生成
ProductionReleased 或 OrderAmended 事件提交后,Worker 按已冻结的路由版本生成计划:
- 找到订单租户及门店可用规则,验证所有目标设备授权。
- 收银生成 FULL_ORDER 任务;部门按订单行生产部门聚合生成 DEPARTMENT 任务。
- 套餐按组件展开生产行,保持父商品和数量关联;不能把套餐金额重复计入部门票。
- 每目标、模板、份数单独生成 PrintJob。唯一键为 event_id + destination + template_version + copy_no。
- 无部门、无路由、设备解绑进入 ROUTING_EXCEPTION,提醒店员;禁止“打印成功”掩盖未分配项目。
- 渲染成设备支持的指令或位图,存 payload_hash、订单版本、路由版本及源数据摘要。
同一物理打印机按队列串行调度,跨门店按公平顺序,防止票据字节交叉。前台和各厨房的逻辑任务可独立并行。配置错误时支持发布前预览路由图和试打。
6.2 投递与状态层级
QUEUED → LEASED → SENT → ACKNOWLEDGED / PRINT_CONFIRMED;可能进入 RETRY_WAIT / FAILED / UNKNOWN / CANCELLED。ACKNOWLEDGED 只代表服务商或设备收到了任务;PRINT_CONFIRMED 仅在 SDK 或设备确有对应回执能力时使用,并显示证据等级。
不承诺端到端物理 exactly-once。网络在“已打印但回执丢失”时中断,会造成状态不确定。可通过唯一任务号和代理持久账本尽量减少重复,但物理出纸无法由数据库事务回滚。UNKNOWN 不盲目自动补打或切备机,优先让店员核对;明确投递失败的任务才安全自动重试。
建议:代理租约与心跳、短断线持久队列、指数退避加抖动、达到阈值告警;具体重试次数和时间由样机验证。代理保存 task_id 与最后投递状态,重启后去重拉取;驱动调用后到落盘前崩溃仍可能产生未知状态。主备切换前检查旧设备是否可能已经出纸,不能承诺无重复。
人工补打创建新任务,引用原任务、操作者与原因,票头醒目标记“补打 REPRINT”;不覆盖原失败记录。打印成功不能消除业务接单超时告警。
6.3 四种连接方式
| 方式 | 接入设计 | 网络及限制 |
|---|---|---|
| Android POS 内置 | APP / 打印代理调用厂商 SDK | 需验证安装权限、SDK、后台运行与设备区域固件 |
| 店内 LAN/USB | Android 或专用网关代理驱动打印机 | 服务端不能直接访问门店私有 IP;代理主动连云 |
| 厂商云打印 | 服务端调用云 API,接收回执或轮询 | 厂商准入、区域、订阅、回执语义和数据路径须确认 |
| Star CloudPRNT | 打印机通过协议轮询/支持型号的消息机制取任务 | 需要实现协议服务;不是任意 LAN 打印机天然支持 |
打印机 LAN 端口不暴露公网。代理主动 HTTPS/WSS 连接,设备单独短期凭证;不把全平台 secret 安装到商户设备。
6.4 多语言票据
泰文涉及组合字形与上下标,不能仅依据“支持 ESC/POS”判断打印正确。优先使用经验证的厂商 Unicode 排版;不支持时在服务端/代理用含合法字体的排版引擎栅格化为单色位图,并测试速度与内存。58mm 与 80mm 使用实际点宽和可打印宽度,不能只按纸宽计算。
测试泰文附标、中文、英文长规格、马来文、混排、换行、取消标记和重复份数。打印字体缺失应报错或回退到已确认名称,不能静默乱码。
7. 候选 API 契约
下表仅为接口设计,无实现。所有金额由服务端计算,私有接口校验 RBAC、租户与门店。列表分页,变更接口校验 version。
| 接口 | 要点 |
|---|---|
| POST /v1/qr/resolve | 输入不透明 locator;返回门店、来源、有效策略及受限 GuestSession |
| GET /v1/menu?locale=th | 按会话确定门店;返回版本、语言回退标识、商品与规格 |
| POST /v1/orders/quote | 输入 SKU/规格/数量;返回 quote_id、明细、费用、有效期 |
| POST /v1/orders | quote_id、location_confirmed、submission_id;返回原订单或新订单 |
| POST /v1/orders/:id/payment-attempts | 只能支付本人可授权订单;绑定金额/币种/渠道 |
| GET /v1/orders/:id | 个人会话或店员授权,返回履约、支付与部门汇总 |
| POST /v1/bills/:id/checkout | 整桌账单授权;锁定 bill_version,返回结账凭证 |
| POST /v1/merchant/orders/:id/accept | 幂等接单,验证版本与营业规则 |
| POST /v1/merchant/orders/:id/reject | 原因、版本;已付款进入退款流程 |
| POST /v1/refunds | 权限、原支付、金额、原因、审批凭证与幂等键 |
| PUT /v1/print-routes/:id | 配置版本,目标门店及设备授权校验,草稿/发布 |
| POST /v1/print-routes/simulate | 输入示例订单,返回全单/部门行/异常预览,不出纸 |
| POST /v1/devices/pair | 一次性配对码,确认租户、门店授权、设备能力 |
| POST /v1/devices/:id/heartbeat | 凭证校验,报告能力、网络、可用状态与时间 |
| POST /v1/print-jobs/lease | 原子领取并续租,避免多代理同时处理 |
| POST /v1/print-jobs/:id/result | 回执等级、驱动结果、attempt_id;防伪造和重放 |
| POST /v1/print-jobs/:id/reprint | 角色、原因、引用原任务;生成新补打任务 |
| POST /v1/webhooks/payments/:provider | 按服务商规范认证、去重、快速响应与补偿 |
错误结构建议含 code、message_key、details、trace_id、retryable。典型码:LOCATION_DISABLED、SESSION_EXPIRED、PRICE_CHANGED、ITEM_SOLD_OUT、BILL_LOCKED、PAYMENT_PENDING、DEVICE_UNAUTHORIZED。日志不能包含支付密钥和房客敏感信息。
8. 非功能、安全与运维
- 顾客 TLS 全链路;短期 HttpOnly/Secure/SameSite 会话、CSRF 防护、内容安全策略、输入校验、限流和反刷;桌码 locator 不能当永久订单读取权限。
- 商户 RBAC + 多因素认证可选/管理员推荐;支付账户变更二次验证;审计覆盖退款、手动收款、设备授权与打印规则。
- 菜单缓存按 tenant/store/locale/version 隔离;缓存价格只能展示,下单再校验。静态图片 CDN,首页不强制加载四种完整字体。
- 租户隔离、房间隐私、收集最少个人数据、数据保留期限和跨境处理需在实施前由当地法律/合规顾问审定;本稿不是法律意见。
- 主业务数据库备份、恢复演练、任务死信队列;建议试点目标 RPO ≤15 分钟、RTO ≤4 小时,需预算及演练确认。
- 建议目标:菜单 API p95 <500ms、提交 p95 <1s(不含支付外部耗时);性能基线假设 100 店、峰值每店 10 单/分钟、每单 4 个打印任务,约 16.7 单/秒与 66.7 任务/秒。此为容量模型,不是已验证能力。
- 监控 Outbox 滞留、支付待确认、打印待发送/未知、设备最后心跳、接单超时、账单锁超时、退款失败;贯穿 trace_id/order_id/job_id。
- 断网允许浏览已缓存菜单与本地草稿,阻止提交;不支持离线可信支付或服务端不可达时自动接单。既已到店内代理的打印任务可按策略继续处理。
9. 实施前验证与阶段门槛
| 阶段 | 输出与退出条件 |
|---|---|
| 客户确认 | 范围、支付模式、店型、路由规则、语言责任、UI 签字 |
| 样机与服务商验证 | 泰文打印、后台接单、网络中断、云回执、渠道沙箱/准入;形成证据矩阵 |
| 最小闭环开发 | 一店一桌码 → 菜单 → 付款/后付 → 接单 → 全单及部门打印 |
| 试点 | 多店共享、断网/缺纸/重复回调/迟到到账演练,对账无遗漏 |
| 扩展 | 更多支付、PMS、KDS、营销和其他国家 |
在客户确认与样机验证前,不据此给出固定开发工期、报价或设备采购承诺。