内容
# 麦麦购物车最优解 · mcd-cart-optimizer
> 一个基于麦当劳 MCP 的决策型 Skill。不推荐套餐,只做一件事:**把手上的券算清楚。**
---
## 一句话说明它解决什么问题
你手上有几张麦当劳优惠券,想买的东西也定了。问题是:
> **这单到底怎么买最便宜?**
绝大多数省钱工具会告诉你「有张满 50 减 20 的券,很划算」。但它们不会告诉你:
**那张券可能让你花更多钱。**
### 一个真实会发生的例子
| | |
|---|---|
| 你想买 | 巨无霸套餐 ¥26 |
| 手上的券 | 满 30 减 8、满 50 减 20、汉堡类满 25 减 5、无门槛减 3 |
券面金额最大的是「满 50 减 20」。那么它最省吗?
| 方案 | 实付 |
|---|---|
| 汉堡类满 25 减 5(直接可用) | **¥21.00** ← 最优 |
| 满 30 减 8(凑 ¥4 圆筒冰淇淋) | ¥22.00 |
| 无门槛减 3 | ¥23.00 |
| 不用券 | ¥26.00 |
| 满 50 减 20(凑 ¥24 麦乐鸡+薯条) | **¥30.00** ← 最贵 |
**券面 ¥20 的那张,实付比券面 ¥5 的那张贵 ¥9。**
因为它的门槛要求你多花 ¥24 去够 ¥50,而折扣只有 ¥20 —— 净亏 ¥4。
这就是本项目的全部价值:**把「券面金额」和「这单实际省多少」拆开算。**
---
## 核心公式
```
凑单净收益 = 券面折扣 − 凑单额外支出
```
| 结果 | 含义 |
|---|---|
| `> 0` | 凑单划算,去做 |
| `= 0` | 无意义的折腾(多买了不需要的东西,钱上打平) |
| `< 0` | **最常见陷阱**:为了「省 8 元」多花 10 元 |
还有一条被普遍忽略的规则:
> **门槛要用「券适用范围内的小计」去比,不是购物车总额。**
「汉堡类满 25 减 5」这张券,如果你购物车里是 ¥15 咖啡 + ¥12 薯条(合计 ¥27),
总额过了 ¥25,但这张券**根本用不了** —— 适用商品小计是 ¥0。
拿总额去比门槛的省钱工具,在这里会直接算错。
---
## 目标用户
- **每周吃麦当劳、券包常年堆着十几张的人** —— 下单前花 10 秒跑一遍,知道该用哪张
- **对「凑单」这件事本能想凑、但没算过账的人** —— 这个工具会明确告诉你哪次凑单是亏的
- **在意券过期时间的人** —— 会提示哪张券即将浪费掉
- **不想被「满减」话术牵着走的人** —— 只看实付,不看券面
不适用于:完全没有明确想买什么、想让 AI 从零推荐套餐的场景。本 Skill 优化的是**已知意图**,不是替代点餐。
---
## 安装
### 0. 前置:申请麦当劳 MCP Token
前往 <https://open.mcd.cn/mcp>,手机号登录 → 右上角「控制台」 → 「激活」 → 复制 Token。
### 1. 配置连接器(以 WorkBuddy 为例)
打开 WorkBuddy → 左侧栏【专家·技能·连接器】→【连接器】→ 右上角【自定义连接器】→【配置 MCP】,
填入 `mcp-config.example.json` 的内容,并把 `YOUR_MCP_TOKEN` 替换为真实 Token:
```json
{
"mcpServers": {
"mcd-mcp": {
"type": "streamablehttp",
"url": "https://mcp.mcd.cn",
"headers": {
"Authorization": "Bearer YOUR_MCP_TOKEN"
}
}
}
}
```
> ⚠️ `Bearer ` 前缀不能省。只填 token 会被服务端判为 401。
>
> ⚠️ **保存后必须回到【自定义连接器】把 `mcd-mcp` 启用/信任一次**,写入配置文件不会自动生效。
### 2. 安装本 Skill
把 `skills/mcd-cart-optimizer/` 整个目录拷贝到你的 Skill 目录:
- 用户级(跨项目可用):`~/.workbuddy/skills/mcd-cart-optimizer/`
- 项目级(仅当前项目):`<项目根>/.workbuddy/skills/mcd-cart-optimizer/`
无需安装任何第三方依赖 —— 本项目只用 Python 标准库。
### 3. 验证
```bash
python tests/test_optimizer.py # 算法层 22 项
python tests/test_adapter.py # 适配层 19 项
python tests/run_all.py # 一次跑完
```
---
## 使用示例
### 对话方式(推荐)
配置好之后,直接在对话里说:
> 我想买个巨无霸套餐,手上有什么券能便宜点?
Skill 会自动调用 `query-my-coupons` 取券、`query-meals` 取菜单,然后给出:
```
本单最优:汉堡类满25减5(直接可用) · 实付 ¥21.00(省 ¥5.00)
为什么不是那张大额券:
「满50减20」需要多花 ¥24 才够 ¥50 门槛,
凑完实付 ¥30.00,反而比上面这个方案贵 ¥9.00。
备选:
· 满30减8(凑 ¥4 圆筒冰淇淋) —— 实付 ¥22.00
· 无门槛减3 —— 实付 ¥23.00
```
### 离线演示(不需要 MCP,不需要联网)
```bash
python skills/mcd-cart-optimizer/scripts/demo.py
```
会完整打印方案排序表、凑单临界点分析、以及反直觉提示。
样例数据在 `examples/sample-session.json`(全部为虚构内容)。
### 作为库调用
```python
import sys; sys.path.insert(0, "skills/mcd-cart-optimizer/scripts")
from optimizer import Item, Coupon, solve, threshold_analysis, fmt
baseline = [Item("combo", "巨无霸套餐", 2600, "combo")]
coupons = [Coupon("c1", "满30减8", threshold=3000, discount=800)]
catalog = [Item("cone", "圆筒冰淇淋", 400, "dessert")]
plans = solve(baseline, coupons, catalog, max_topup_overpay=1500)
print(plans[0].label, fmt(plans[0].payable)) # 满30减8(需凑单) ¥22.00
for p in threshold_analysis(baseline, coupons, catalog):
print(p.coupon.name, p.verdict) # 满30减8 凑单划算
```
---
## 项目结构
```
.
├── README.md # 本文件
├── CONTEST_DECLARATION.md # 参赛声明(官方版本,未做任何改动)
├── MCP_INTEGRATION.md # MCP 集成说明:用了哪些 Tool、调用流程、业务价值
├── mcp-config.example.json # 脱敏配置示例,仅含环境变量占位符
├── workbuddy.md # WorkBuddy 开发对话上下文
├── skills/mcd-cart-optimizer/
│ ├── SKILL.md # Skill 定义与工作流
│ └── scripts/
│ ├── optimizer.py # ★ 核心优化引擎(纯函数,无 IO)
│ ├── mcd_adapter.py # MCP 响应 → 领域模型的适配层
│ ├── probe.py # 联调探针:诊断真实响应结构
│ └── demo.py # 离线演示
├── examples/
│ └── sample-session.json # 虚构样例数据
└── tests/
├── test_optimizer.py # 算法层测试(22 项)
├── test_adapter.py # 适配层测试(19 项)
└── run_all.py # 测试入口
```
### 为什么算法层要独立
`optimizer.py` 不依赖网络、不依赖 MCP、不做任何 IO。它只吃 `Item` / `Coupon` 对象。
这样带来三个好处:
1. **可测试** —— 41 项测试全部离线运行,不消耗 MCP 配额
2. **可演进** —— MCP 字段变了只改 `mcd_adapter.py`,算法层零改动
3. **可复用** —— 想接别的餐饮 API,换个适配层就能用
---
## 技术要点
### 凑单求解是一个背包问题
不是「买最便宜的凑上」,也不是「买最贵的顶上」。举例:缺 ¥7,候选有 ¥4 / ¥5 / ¥6 三件,
- 买 ¥6 → 不够
- 买 ¥4+¥6 = ¥10
- **最优是 ¥4+¥5 = ¥9**
本项目用 DP 求「最小超集和」,并做了上界剪枝:最优解的超出量必定小于候选中的最高单价。
(证明:若最优解 `S >= gap + max_price`,去掉单价最高的那件后仍有 `S' >= gap`,与最小性矛盾。)
因为剪枝,搜索空间被压到 `[gap, gap + max_price)`,100 件候选商品也是毫秒级。
### 金额一律用分
**绝不使用浮点数。** 凑单场景对边界极其敏感 —— `29.999999` 和 `30.0` 差的是
「这张券能不能用」。全流程使用 `int` 分,只在最终展示时才格式化成元。
### 元 / 分的自动推断
MCP 的金额字段是「元」还是「分」,单看一个数无法判断 —— ¥4 是 `400` 分,
¥10 是 `1000` 分,两种口径的取值区间完全重叠。任何「小于某个数就是元」的阈值规则
都必然在某个价位上失手。
因此改为**在整批数据上找证据**:
1. 只要出现小数(`26.5`)→ 一定是「元」(分永远是整数)
2. 全是整数且最大值 `< 200` → 视为「元」(一份菜单最贵单品按分算不到 ¥2,不合理)
3. 其余 → 视为「分」
接入真实数据后建议打印一次 `min / max / 是否有小数` 确认分支,或用
`python scripts/probe.py <响应文件>.json` 直接看判定依据。
---
## 用到的麦当劳 MCP 能力
| Tool | 用途 |
|---|---|
| `query-my-coupons` | 用户手上的券 → 优化器的核心输入 |
| `available-coupons` | 可领取的券 → 提示「先领再买更划算」 |
| `query-store-coupons` | 门店维度可用券 |
| `query-meals` / `query-meal-detail` | 菜单与价格 → 基线商品与凑单候选池 |
| `query-my-account` | 积分余额(辅助决策) |
| `campaign-calendar` | 活动日历(提示「等活动日再下单」) |
详细调用流程与业务价值见 [MCP_INTEGRATION.md](./MCP_INTEGRATION.md)。
---
## 测试
```
$ python tests/run_all.py
tests/test_optimizer.py
PASS test_applicable_subtotal_by_category
...
22/22 passed
tests/test_adapter.py
PASS test_end_to_end_from_raw_payloads
...
19/19 passed
合计 41/41 passed
```
---
## 免责声明
本项目为**麦当劳程序员创意开发大赛参赛作品**,由参赛者独立开发,**非麦当劳官方产品**。
- 项目输出仅供参考,不构成任何专业建议
- 餐品信息、价格及供应状态以麦当劳官方渠道的实时结果为准
- 本 Skill 只做决策建议,**不代用户下单**;涉及下单的操作必须先由用户确认商品与金额
- 项目代码中不含任何真实 Token、密钥或账号凭证
## 许可
MIT