---
name: mcd-deal-gen
description: 麦当劳穷鬼套餐生成器。给定预算、餐食时段、营养约束（控热量/控钠/控蛋白）或「用券点单」等需求，自动完成门店定位 → 门店可用券核验 → 餐品营养匹配 → 带券价格试算，输出实付金额最低的组合方案。当用户说「我要吃麦当劳」「帮我配个便宜的」「30块以内怎么点」「用券点单」「麦当劳有什么优惠」「低钠套餐」时使用。
agent_created: true
---

# 穷鬼套餐生成器

## 适用场景

用户想点麦当劳，诉求通常是这几种之一（可组合）：

| 诉求 | 典型说法 | 需要的数据 |
|---|---|---|
| 省钱 | 「30块以内怎么点」「最便宜的」 | 券 + 实付试算 |
| 用券 | 「我有哪些券能用」「把券用掉」 | 卡包券 ∩ 门店可用券 |
| 限时 | 「早餐吃什么」「现在还能点啥」 | 当前时段可用性 |
| 健康 | 「低钠」「控热量」「高蛋白」 | 营养成分表 |
| 特定组合 | 「不想吃汉堡」「来个甜品」 | 餐品清单 |

**通用原则：不写死门店、不写死场景、不写死预算。** 每次运行时从用户输入和探测结果推导。

---

## 核心事实（已实测验证，务必遵守）

### 1. 顺序不能反：先问门店此刻能用啥，再看我有什么券

`query-my-coupons`（卡包资产）**不做门店、渠道、场景校验**，返回的是「我拥有的」，
不代表此刻能用。实测：卡包14 张券，同一时刻在成都南站门店只有 1 张被`query-store-coupons` 认出来，
其余 10 张早餐券因已过05:00–10:29 时段而不可用。

**因此禁止「拿卡包券直接去套门店」。** 必须以 `query-store-coupons` 的返回为准。

### 2. `reservationDate` 会导致返回空数组（重要坑）

同门店、同场景实测：

- 传 `reservationDate=2026-10-10 08:30` → `data:[]`（空）
- 不传 `reservationDate` → 正常返回真实券

**处理策略**：先不传 `reservationDate` 查询。若用户需要预约场景，再从
`query-nearby-stores` 返回的 `reservationTimeOptions` 中取真实时段字符串重试；
仍为空则如实告知「该时段暂无可用券」，**不要臆造**。

### 3. 价格单位是「分」

`calculate-price` 所有金额字段（`price`、`originalPrice`、`discount`、`productPrice`…）
单位均为分，**展示时必须除以 100**。这是 MCP 文档特别强调的点，漏掉会差100 倍。

### 4. 券与门店绑定，`productCode` 是试算必需

`query-store-coupons` 返回的每个 `couponId` 可能对应多个 `couponCode`，
且必须搭配该券适用的 `productCode` 才能在 `calculate-price` 里生效。

### 5. 实测计价样例（可作为输出格式参考）

成都南站门店 `3450312` ·薯薯任选 ×1：
原价 ¥16.00 → 用券后 ¥9.90（优惠 ¥6.10），同时返回取餐方式：堂食（eat-in）/ 外带（locker-in）。

### 6. `calculate-price` 可能不可用，算价应回退到本地

实测（2026-10-13）该接口持续返回 `/items: must be array`，
连续 4 次不同参数组合均失败。且即使可用，**为每个候选组合调一次远程接口**
在枚举上千方案时也不现实。

**推荐做法**：MCP 只取数（菜单标价、券面信息），算价用仓库内的本地引擎：

```python
from mcd_planner.pricing import load_menu, load_coupons, price_basket

menu = load_menu()
coupons = load_coupons()
quote = price_basket([("9900012387", 1)], menu, coupons)
print(quote.summary())   # 实付 ¥13.50（原价 ¥27.00，省 ¥13.50）
```

本地引擎已覆盖：券的时段/星期/场景判定、券叠加冲突、划线价对照。
**叠加规则说明**：MCP 未公开 stacking 字段，本地默认「一单一张券」（保守），
输出中须标注该假设，不可宣称比实际更优惠。

若 `calculate-price` 恢复可用，可作为最终校验，但不作为主路径。

---

## 执行流程

### 第 0 步：补齐必需信息

调用任何工具前，先确认这几项（能从输入推断的就别问）：

| 要素 | 说明 | 缺失时 |
|---|---|---|
| 取餐场景 | 到店自取(beType=1) / 得来速(beType=5) / 麦乐送(beType=2) / 团餐(beType=6) | **必须问**，场景决定 orderType 和 beCode 传参 |
| 位置 | 城市 + 关键词，或已收藏门店 | 必须问 |
| 预算 | 总价上限 | 可选，默认给「最省方案」 |
| 健康约束 | 控钠/热量/蛋白等 | 可选，无则跳过营养筛选 |

**多要素缺失时一次性问完，不要挤牙膏式追问。**

### 第 1 步：定位门店

```
query-nearby-stores
  searchType=1 → 收藏门店（先试这个）
  searchType=2 → 城市 + 关键词
  beType：到店场景传 1 或 5
```

失败态处理：`searchType=1` 返回 `code=600050`（收藏餐厅列表为空）是正常情况，
直接回退到 `searchType=2` 让用户给城市和关键词。

**必须完整展示 `storeCode`**（后续所有调用都依赖它），得来速场景还要展示 `beCode`。

### 第 2 步：核验该门店真实可用券（关键步骤）

```
query-store-coupons
  storeCode=上一步的门店编码
  orderType：到店=1 / 外送=2
  beType：1 / 5 / 2 / 6
  beCode：得来速(5)、麦乐送(2)、团餐(6) 必传；到店自取(1) 不传
```

参数传递规则（严格遵守）：

| beType | orderType | beCode |
|---|---|---|
| 1 到店自取 | 1 | 不传 |
| 5 得来速 | 1 | **必传**（来自 `query-nearby-stores`） |
| 2 麦乐送 | 2 | **必传**（来自 `delivery-query-stores`） |
| 6 企业团餐 | 2 | **必传**（来自 `delivery-query-stores`） |

返回为空时：先尝试不带 `reservationDate`；仍为空则如实说明该门店此刻无可用券。

### 第 3 步：拉取餐品与营养数据

```
query-meals        → 门店在售餐品 + productCode
list-nutrition-foods → 160 项营养数据（能量/蛋白质/脂肪/碳水/钠/钙）
```

两张表按 `productName` 关联。营养数据里几个值得利用的点：

- **钠含量差异巨大**（巨无霸 961mg vs 麦辣鸡腿堡 1208mg vs 中杯可乐 0mg），
  控钠场景下这是主要筛选维度
- 「纯悦」标注能量约清水 1 杯、0 kcal，是零热量饮品替换的天然解
- 有 30+ 项饮品钠含量为 0，替换含糖饮料可同时降糖降钠

### 第 4 步：候选组合 + 算价

**首选本地引擎**（见核心事实 6）：

```python
price_basket([(code, qty), ...], menu, coupons, now=..., scope="dine_in")
# → Quote: lines / paid_total / list_total / saving / applied_coupons / notes
```

若本地引擎不可用或用户要求官方核价，再回退到远程接口：

```
calculate-price
  storeCode / orderType / beType / beCode
  items[]: { productCode, quantity, couponId?, couponCode? }
  needTableware: 是否需要餐具（可能产生餐具费）
```

组合策略（按优先级尝试）：

1. 能被单张券完全覆盖的组合（最省）
2. 无券但实付最低的单品组合
3. 若有健康约束，在以上候选中筛钠/热量达标者
4. 若用户已有目标单品，优先给「同类替代」而非整体重配

**试算时也要带上 `needTableware`**，否则报价会漏算餐具费。

### 第 4.5 步（可选）：价格 × 钠 帕累托前沿

当用户问「便宜和健康怎么权衡」「有没有两全的」时，用帕累托前沿回答：

```python
from mcd_planner.optimizer import optimize, format_frontier

r = optimize(budget=30, max_sodium_mg=1200, max_items=3)
print(format_frontier(r.frontier))
```

前沿上的每个点都不可支配：想更便宜就得多摄入钠，想更低钠就得多花钱。
**若前沿只有一两个点，如实说明「在当前菜单下没有更好的折中」**，
不要为了显得丰富而虚构方案。

### 第 5 步：输出方案

---

## 硬性安全边界

- **绝不自动下单。** 技能止步于 `calculate-price`。`create-order` 仅在用户明确说「就这个/下单/帮我点」时才调用。
- **绝不编造。** 券、门店、价格一律来自接口返回。接口没返回的券码、会员号、门店编码不得猜测或构造。
- **价格必须除以 100**（第 3 条事实）。
- **每张券必须标注真实到期时间**，不推荐已过期或当前时段不可用的券。
- **抽吸类操作不自动执行。** 涉及消耗积分的 `draw-lottery` 须先展示 `query-lottery-info` 的 `nextConsumption` 并获得用户明确确认（MCP 有频次限制，严禁试抽）。

---

## 输出格式

主方案 + 备选，标明每一项的优惠来源：

```
最省方案：薯薯任选 ×1
实付 ¥9.90　原价 ¥16.00　省 ¥6.10
券：麦麦省薯薯任选（今日 23:59 到期）
门店：成都南站站前广场（步行 273m）
取餐：堂食 / 外带

─── 其他可选 ───
· 火腿扒麦满分 单点 约 ¥11.00　无券可用，钠 608mg
· 巨无霸 ×1 + 中杯可乐 约 ¥24.00钠 961mg（若不控钠）

提示：你卡包中还有 10 张早餐券，但有效期为每日 05:00–10:29，当前时段不可用。
```

要点：**实付金额显眼、券的到期时间显眼、不适用的券主动说明为什么用不了**（避免用户以为漏算了）。

---

## 常见场景速查

| 用户说 | 走哪条路 |
|---|---|
| 「早餐吃什么」 | 场景=到店自取 → 门店 → 查券（此时段券才有效）→ 早餐餐品配券 |
| 「30 以内怎么点」 | 问预算 → 全流程 → 按实付排序，滤掉超预算 |
| 「低钠的推荐」 | 加控钠约束 → 营养表筛钠 → 试算 |
| 「我有什么券」 | 仅 `query-my-coupons` + `available-coupons`，说明哪些当前不可用 |
| 「附近有什么活动」 | `campaign-calendar` + `available-coupons` |
| 「公司订餐」 | beType=6 团餐链路 + `query-meal-assistance` 助餐服务 + `query-promotions` 满减满折规则解读 |

---

## 边界与免责

- 营养数据来自 MCP 静态表，与门店实际出品可能存在偏差，涉及健康目标时应提示用户以实际为准
- 门店库存、供应时段以 `query-meals` 与门店营业时间为准
- 团餐满减满折规则需自行解读（`query-promotions` 返回原始结构，不做最优推荐）
- 价格与优惠以 `create-order` 时的实际结算为准，本技能输出为**试算结果**