餐饮包(restaurant)设计文档 · 波波 2026-09-24

目标:把餐饮包做成与住宿包同构、且可被住宿"含餐产品"复用的独立域。
基本能力对齐住宿(管理闭环),额外设计一层"含餐组合"让住宿侧能调用餐饮模块。
商户身份一律不写死:merchant_id 参数化 + is_test 隔离。

1. 范围与边界

1.1 餐饮包 MVP(B端工作台闭环)

镜像住宿包结构,覆盖三类核心管理面:

住宿包餐饮包对应说明
房型管理 room_types菜品管理 menu_items可售单元(含分类/价格/上下架)
房态 session-manage桌台状态 restaurant_tables堂食桌台占用/空闲/预订
预订接单台 bookings餐饮订单 dining_orders + dining_order_items开单/加菜/改状态/结账
客史 guests复用 guests商户级,跨业态通用,不新建
账务 folios复用 folios商户级,餐饮结账挂账直接复用
交接班 shift-handover复用 shift-handover商户级
操作日志 operation_logs复用 operation_logs商户级
MVP 克制:不单独建"菜品分类表"——menu_items.category 用文本字段 + sort_order 排序即可;不建独立的"套餐组成表"——套餐用 is_combo + dining_order_items 表达(MVP 先不做组合菜品明细拆解)。

1.2 与住宿兼容(含餐组合层)★重点

需求:"住宿含餐的产品,需要调用一部分餐饮模块"。

设计原则:餐饮不反向依赖住宿;住宿只通过"引用菜品 id"实现含餐,单向引用。

新增兼容实体 room_meal_plans(挂在住宿侧,但内容指向餐饮菜品):

跨域只读接口(给住宿 C端下单/详情调用):

→ 返回该房型含餐的菜品清单(名称/价格/餐型),由 C端住宿流程渲染"含餐内容",本质是调餐饮模块的数据。

这样住宿"含餐产品"无需自建菜品库,直接复用餐饮 menu_items;餐饮模块独立可售(堂食/外卖)时也完全自洽。

1.3 不在本期范围


2. 复用决策

项决策理由
埋点/事件不涉及(B端管理台)—
guests 客史直接复用已是商户级表,餐饮顾客即商户顾客
folios 账务直接复用结账金额按现有 folio 结构挂账
shift-handover 交接班直接复用商户级
operation_logs 操作日志直接复用商户级
价格约定沿用 *_fen 内部分、对外 _yuan() 元、入参元→*100与住宿 hotel.py 完全一致
SLA/实时计算不涉及—
域名路由范式console_router 前缀 /merchant-console/restaurant,靠 merchant_staff 解析 merchant_id镜像 hotel.py

3. 数据模型(新表 DDL 草稿 → 落地为 backend/sql/090_restaurant_domain.sql)

3.1 menu_items 菜品

CREATE TABLE IF NOT EXISTS menu_items (
    id              BIGSERIAL PRIMARY KEY,
    merchant_id     BIGINT NOT NULL REFERENCES merchants(id) ON DELETE CASCADE,
    name            VARCHAR(64) NOT NULL,
    description     TEXT NOT NULL DEFAULT '',
    category        VARCHAR(32) NOT NULL DEFAULT '未分类',
    price_fen       INTEGER NOT NULL DEFAULT 0 CHECK (price_fen >= 0),
    image_url       VARCHAR(512) NOT NULL DEFAULT '',
    status          VARCHAR(16) NOT NULL DEFAULT 'onsale'
                        CHECK (status IN ('onsale','offself','unavailable')),
    is_combo        BOOLEAN NOT NULL DEFAULT FALSE,   -- 是否套餐
    sort_order      INTEGER NOT NULL DEFAULT 0,
    is_test         BOOLEAN NOT NULL DEFAULT FALSE,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS menu_items_merchant_idx ON menu_items(merchant_id, sort_order);
CREATE INDEX IF NOT EXISTS menu_items_merchant_status_idx ON menu_items(merchant_id, status);
status:onsale 在售 / offself 下架 / unavailable 售罄临时不可点

3.2 restaurant_tables 桌台(堂食)

CREATE TABLE IF NOT EXISTS restaurant_tables (
    id              BIGSERIAL PRIMARY KEY,
    merchant_id     BIGINT NOT NULL REFERENCES merchants(id) ON DELETE CASCADE,
    table_no        VARCHAR(16) NOT NULL,             -- 桌号 A01/包厢1
    seats           INTEGER NOT NULL DEFAULT 2 CHECK (seats >= 1),
    status          VARCHAR(16) NOT NULL DEFAULT 'free'
                        CHECK (status IN ('free','occupied','reserved')),
    qr_token        VARCHAR(64) NOT NULL DEFAULT '',  -- 扫码点餐码(C端用,本期预留)
    is_test         BOOLEAN NOT NULL DEFAULT FALSE,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (merchant_id, table_no)
);
CREATE INDEX IF NOT EXISTS restaurant_tables_merchant_idx ON restaurant_tables(merchant_id, status);

3.3 dining_orders 餐饮订单

CREATE TABLE IF NOT EXISTS dining_orders (
    id              BIGSERIAL PRIMARY KEY,
    merchant_id     BIGINT NOT NULL REFERENCES merchants(id) ON DELETE CASCADE,
    order_no        VARCHAR(32) NOT NULL,
    table_id        BIGINT,                           -- 堂食桌台;外卖/打包为空
    guest_id        BIGINT,                           -- 复用 guests.id,可空
    type            VARCHAR(16) NOT NULL DEFAULT 'dinein'
                        CHECK (type IN ('dinein','takeout','delivery','room_meal')),
    status          VARCHAR(16) NOT NULL DEFAULT 'created'
                        CHECK (status IN ('created','preparing','served','paid','cancelled')),
    total_fen       INTEGER NOT NULL DEFAULT 0 CHECK (total_fen >= 0),
    paid_fen        INTEGER NOT NULL DEFAULT 0 CHECK (paid_fen >= 0),
    operator_id     BIGINT,                           -- 开单员工 merchant_staff.id
    source          VARCHAR(16) NOT NULL DEFAULT 'onsite'
                        CHECK (source IN ('onsite','miniapp','room_meal')),
    is_test         BOOLEAN NOT NULL DEFAULT FALSE,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS dining_orders_merchant_idx ON dining_orders(merchant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS dining_orders_merchant_status_idx ON dining_orders(merchant_id, status);
CREATE UNIQUE INDEX IF NOT EXISTS dining_orders_no_idx ON dining_orders(merchant_id, order_no);
type=room_meal / source=room_meal:标识"住宿含餐"产生的餐饮单(兼容层落地时由住宿流程写入)。
status 镜像住宿语义:created 开单 → preparing 备餐 → served 已上菜 → paid 已结账 → cancelled 取消。

3.4 dining_order_items 订单明细

CREATE TABLE IF NOT EXISTS dining_order_items (
    id              BIGSERIAL PRIMARY KEY,
    order_id        BIGINT NOT NULL REFERENCES dining_orders(id) ON DELETE CASCADE,
    item_id         BIGINT,                           -- 指向 menu_items.id(快照名称/价格)
    name            VARCHAR(64) NOT NULL DEFAULT '',
    price_fen       INTEGER NOT NULL DEFAULT 0 CHECK (price_fen >= 0),
    qty             INTEGER NOT NULL DEFAULT 1 CHECK (qty >= 1),
    is_combo        BOOLEAN NOT NULL DEFAULT FALSE,
    is_test         BOOLEAN NOT NULL DEFAULT FALSE,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS dining_order_items_order_idx ON dining_order_items(order_id);

3.5 room_meal_plans 含餐组合(跨域兼容层)

CREATE TABLE IF NOT EXISTS room_meal_plans (
    id              BIGSERIAL PRIMARY KEY,
    merchant_id     BIGINT NOT NULL REFERENCES merchants(id) ON DELETE CASCADE,
    room_type_id    BIGINT NOT NULL,                  -- 指向住宿 room_types.id
    meal_type       VARCHAR(16) NOT NULL DEFAULT 'breakfast'
                        CHECK (meal_type IN ('breakfast','lunch','dinner','supper')),
    menu_item_ids   INTEGER[] NOT NULL DEFAULT '{}',  -- 指向 menu_items.id
    is_test         BOOLEAN NOT NULL DEFAULT FALSE,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (merchant_id, room_type_id, meal_type)
);
CREATE INDEX IF NOT EXISTS room_meal_plans_merchant_rt_idx ON room_meal_plans(merchant_id, room_type_id);

4. 接口清单

4.1 B端 console_router 前缀 /merchant-console/restaurant(镜像 hotel)

鉴权:merchant_staff 解析 merchant_id(同 hotel.py 范式)。

方法路径说明
GET/menu-items菜品列表(过滤 status/category,分页)
POST/menu-items新增菜品(入参元→*100)
PUT/menu-items/{id}改菜品
DELETE/menu-items/{id}下架/删除
GET/tables桌台列表(含状态)
POST/tables新增桌台
PUT/tables/{id}改桌台/状态
DELETE/tables/{id}删桌台
GET/orders订单列表(status/type/日期过滤)
POST/orders开单(堂食/外卖/打包,写 dining_orders + items)
PUT/orders/{id}/items加菜/减菜(重算 total_fen)
PUT/orders/{id}/status改状态(preparing/served/paid/cancelled)
POST/orders/{id}/settle结账(paid_fen 写入 + 可选挂 folios)
GET/dashboard餐饮概览(今日订单数/营业额/桌台占用率)

4.2 跨域只读(C端用,住宿含餐调用餐饮)

方法路径说明
GET/api/miniapp/room-meal-plans?merchant_id=&room_type_id=返回该房型含餐菜品清单(含餐型)

4.3 路由注册


5. 前端页面规划(B端 H5,波波域)

镜像住宿页范式(HotelRoomTypes / HotelBookings / HotelCheckIn 房态):

页面路由对应住宿页
菜品管理 RestaurantMenu.tsx/restaurant-menuHotelRoomTypes
桌台状态 RestaurantTables.tsx/restaurant-tables房态 session-manage
餐饮订单接单台 RestaurantOrders.tsx/restaurant-ordersHotelBookings
餐饮概览 RestaurantDashboard.tsx/restaurant-dashboard(新增汇总)
C端扫码点餐页不在本期(小齐/小冈域)。qr_token 字段本期预留。

6. 与住宿兼容的设计要点(落地检查)

  1. 餐饮不 import 住宿任何模块;住宿通过 room_meal_plans.menu_item_ids 单向引用 menu_items.id。
  2. 住宿"含餐产品"下单时:C端调 GET /api/miniapp/room-meal-plans,拿到菜品清单渲染——这一步即"调用餐饮模块"。
  3. 若住宿订单生成了实际餐饮消费(如到店核销餐券),由住宿流程 POST /merchant-console/restaurant/orders 带 type=room_meal, source=room_meal 写入餐饮单,实现双向可追溯。
  4. 价格统一 *_fen,两端金额单位一致,无换算歧义。

7. 部署步骤

  1. 写 backend/sql/090_restaurant_domain.sql(5 表,幂等 IF NOT EXISTS)
  2. 写 backend/app/routers/restaurant.py(console_router + 跨域只读)
  3. main.py 注册;business-pack.ts 填 PACK_EXTRA_ROUTES.restaurant;App.tsx 注册路由
  4. 写 4 个 B端页面
  5. commit(无 [no-deploy])→ staging 自动迁移+部署;prod 走 DEPLOY_WITH_MIGRATIONS=1 ./scripts/deploy.sh prod
  6. 前端部署到 nginx(b-workbench 路径)

8. 验证清单(部署后回填 · 波波 2026-09-24)

8.1 数据库(双库已迁移)

8.2 接口(prod + staging 双环境冒烟全绿)

8.3 过程中发现并修复的 bug

  1. list_orders 500:原 $2::interval 传字符串 '7 days',asyncpg 编码失败。改为 make_interval(days => $2) 传整数。提交 4d3ebd6b。
  2. settle 不释放桌台:settle_order 直接置 status='paid' 但没释放 dinein 桌台(桌台释放逻辑只在 set_order_status)。已在 settle 内补堂食桌台置 free。提交 43a894fc。

8.4 已知缺口(非本任务引入,需单独决策)