目标:把餐饮包做成与住宿包同构、且可被住宿"含餐产品"复用的独立域。
基本能力对齐住宿(管理闭环),额外设计一层"含餐组合"让住宿侧能调用餐饮模块。
商户身份一律不写死:merchant_id参数化 +is_test隔离。
镜像住宿包结构,覆盖三类核心管理面:
| 住宿包 | 餐饮包对应 | 说明 |
|---|---|---|
房型管理 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 先不做组合菜品明细拆解)。
需求:"住宿含餐的产品,需要调用一部分餐饮模块"。
设计原则:餐饮不反向依赖住宿;住宿只通过"引用菜品 id"实现含餐,单向引用。
新增兼容实体 room_meal_plans(挂在住宿侧,但内容指向餐饮菜品):
room_meal_plans(merchant_id, room_type_id, meal_type, menu_item_ids int[], is_test)meal_type ∈ breakfast / lunch / dinner / supper(早/午/晚/夜宵)menu_item_ids 指向 menu_items.id(餐饮模块维护的菜品)跨域只读接口(给住宿 C端下单/详情调用):
GET /api/miniapp/room-meal-plans?merchant_id=&room_type_id=→ 返回该房型含餐的菜品清单(名称/价格/餐型),由 C端住宿流程渲染"含餐内容",本质是调餐饮模块的数据。
这样住宿"含餐产品"无需自建菜品库,直接复用餐饮 menu_items;餐饮模块独立可售(堂食/外卖)时也完全自洽。
| 项 | 决策 | 理由 |
|---|---|---|
| 埋点/事件 | 不涉及(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 |
backend/sql/090_restaurant_domain.sql)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 售罄临时不可点
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);
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 取消。
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);
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);
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 | 餐饮概览(今日订单数/营业额/桌台占用率) |
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/miniapp/room-meal-plans?merchant_id=&room_type_id= | 返回该房型含餐菜品清单(含餐型) |
backend/app/main.py 新增 include_router(restaurant.router)b-workbench/src/utils/business-pack.ts:PACK_EXTRA_ROUTES.restaurant 从 [] 填入餐饮页路由b-workbench/src/App.tsx:注册餐饮页路由镜像住宿页范式(HotelRoomTypes / HotelBookings / HotelCheckIn 房态):
| 页面 | 路由 | 对应住宿页 |
|---|---|---|
菜品管理 RestaurantMenu.tsx | /restaurant-menu | HotelRoomTypes |
桌台状态 RestaurantTables.tsx | /restaurant-tables | 房态 session-manage |
餐饮订单接单台 RestaurantOrders.tsx | /restaurant-orders | HotelBookings |
餐饮概览 RestaurantDashboard.tsx | /restaurant-dashboard | (新增汇总) |
C端扫码点餐页不在本期(小齐/小冈域)。qr_token 字段本期预留。
room_meal_plans.menu_item_ids 单向引用 menu_items.id。GET /api/miniapp/room-meal-plans,拿到菜品清单渲染——这一步即"调用餐饮模块"。POST /merchant-console/restaurant/orders 带 type=room_meal, source=room_meal 写入餐饮单,实现双向可追溯。*_fen,两端金额单位一致,无换算歧义。backend/sql/090_restaurant_domain.sql(5 表,幂等 IF NOT EXISTS)backend/app/routers/restaurant.py(console_router + 跨域只读)main.py 注册;business-pack.ts 填 PACK_EXTRA_ROUTES.restaurant;App.tsx 注册路由[no-deploy])→ staging 自动迁移+部署;prod 走 DEPLOY_WITH_MIGRATIONS=1 ./scripts/deploy.sh prod090_restaurant_domain.sqlPOST /merchant-console/restaurant/menu-items 新增菜品(元入参→分存储)— 实测创建返回 idGET /merchant-console/restaurant/menu-items 返回 _yuan 元 — 实测POST /merchant-console/restaurant/orders 开单 + items 写入 + total_fen 正确 — 实测 18.5×2=37.0 / 12×3=36.0PUT /orders/{id}/status 改状态 — 实测 created→servedPOST /orders/{id}/settle 结账 paid_fen — 实测 paid=total,status=paidGET /api/miniapp/room-meal-plans?merchant_id=&room_type_id= 返回含餐菜品(含餐兼容层)— 实测无 token 调通,返回 meal_type+菜品明细restaurant-menu/restaurant-orders/餐饮包 路由与标签$2::interval 传字符串 '7 days',asyncpg 编码失败。改为 make_interval(days => $2) 传整数。提交 4d3ebd6b。settle_order 直接置 status='paid' 但没释放 dinein 桌台(桌台释放逻辑只在 set_order_status)。已在 settle 内补堂食桌台置 free。提交 43a894fc。schema_migrations 仅记录到 085,而 room_types 为空表、folios/guests/rate_plans/operation_logs(087–089)表在 staging 不存在。导致 deploy.sh staging 的迁移步骤在 086 种子数据 FK 处失败、阻断后续迁移。migrate.sh staging --mark-until 089(若仅需登记)或补齐 085 种子+086–089(若需真实表)修复。本餐饮任务不擅自改动住宿存量。