应分集合建模:店铺、餐品、规格项、规格组合,用引用代替嵌套;规格定义(spec_options)与具体组合(food_spec_combos)分离,支持原子库存扣减、高效查询与灵活扩展。

外卖点餐系统里,菜单和规格不能用“一个集合存所有餐品+嵌套数组”硬扛——查规格组合会慢,改价格容易冲突,库存更新难原子化。
为什么不用单集合嵌套规格(如 items 数组里放 specifications)
表面省事,实际踩坑密集:
-
updateOne修改某个规格的库存时,得先$elemMatch定位,再用$set路径更新,路径易写错(比如"items.0.specifications.2.stock"),且无法保证多规格同时扣减的原子性 - 前端要展示“小份+无辣+加蛋”这种组合时,得在应用层遍历匹配,响应延迟明显,尤其菜单超 50 个餐品后
- 规格变更(比如“加蛋”从 2 元涨到 3 元)需批量更新所有含该规格的餐品,
updateMany+$pull/$push易漏或误操作 - 聚合统计(如“本月销量 Top10 规格组合”)几乎没法直接用
$unwind展开多层嵌套,性能崩得快
推荐分集合建模:店铺、餐品、规格项、规格组合
核心是把“可复用的规格定义”和“具体餐品绑定的规格实例”拆开,用引用代替嵌套:
-
stores集合存店铺基础信息(_id,name,status) -
food_items存餐品主干(_id,store_id,name,base_price,image_url),不存任何规格字段 -
spec_options存独立规格项(如“辣度”、“份量”、“配料”),每条记录是单一维度的可选项:{ _id: ObjectId, name: "辣度", options: ["微辣", "中辣", "免辣"] } -
food_spec_combos存具体组合(即用户最终下单的 SKU):{ _id: ObjectId, food_id: ObjectId, spec_option_ids: [ObjectId, ObjectId], price_delta: 1.5, stock: 99, sku_code: "F123-LD-ZL-001" }
这样设计后,查“宫保鸡丁的所有可选规格组合”只需两步:db.food_spec_combos.find({ food_id: ObjectId("...") }),再根据 spec_option_ids 去 spec_options 查维度名和选项值,前端拼装自由度高,且 food_spec_combos 可加复合索引 { food_id: 1, spec_option_ids: 1 } 加速查询。
库存扣减必须用 findAndModify 或事务,不能靠应用层判断
用户提交订单时,并发扣减同一规格组合库存极易超卖。MongoDB 4.0+ 支持多文档事务,但要注意:
- 事务内所有操作必须在同一个分片上,所以
food_spec_combos集合建议以food_id分片,避免跨分片事务失败 - 简单场景用
findOneAndUpdate更轻量:db.food_spec_combos.findOneAndUpdate({ _id: comboId, stock: { $gt: 0 } }, { $inc: { stock: -1 } }, { returnDocument: "after" })—— 条件匹配失败就返回 null,应用层直接报“库存不足” - 如果订单含多个规格组合(如主食+饮料+小食各一个),必须用事务包裹全部
findOneAndUpdate操作,任一失败则整体回滚
搜索与筛选时,$lookup 要谨慎用,优先预计算字段
用户按“有辣度选项”“支持加蛋”筛选店铺菜单时,若每次走 $lookup 关联 spec_options,聚合管道会变慢。更稳的做法是:
- 在
food_items集合里冗余一个spec_tags字段,存字符串数组:["spicy", "extra_egg", "large_size"],由后台服务在规格变更时异步更新 - 给
spec_tags建多键索引:db.food_items.createIndex({ spec_tags: 1 }),find({ spec_tags: "spicy" })秒出 - 仅当需要展示规格详情(如“中辣 ¥0,免辣 ¥0”)时,才用
$lookup查spec_options,且限制只查当前页餐品 ID 列表
规格组合的 sku_code 字段别省,它既是唯一标识,也是日志追踪、对账、客服查单的关键线索——少这一步,线上问题定位成本翻倍。











