必须用嵌套文档+预定义字段结构+$expr查询,禁用自由JSON;促销规则需固定schema、数值类型、小写下划线枚举;validator约束结构;$expr实现跨字段比较;高频查询按ESR建复合索引;状态机管理启用/停用;schema变更须DBA审批+自动化测试。

直接用嵌套文档 + 预定义字段结构 + $expr 查询,别搞自由 JSON 字段或运行时拼字符串条件。
促销规则必须用固定 schema,不能存 raw JSON 字符串
很多团队一开始把促销逻辑写成一段 JSON 字符串塞进 rule_config 字段,比如 {"type":"cart_total","min":200,"discount":"15%","exclude_skus":["sku_abc"]}。这看着灵活,实际查不了、索不了、改不了——find({ "rule_config.type": "cart_total" }) 会命中所有文档,因为 MongoDB 不会对字符串内容建索引。
- 所有规则类型(如
cart_total、sku_count、user_level)必须作为顶层字段显式声明 - 数值型条件(
min_amount、max_items)用数字类型,不是字符串 - 枚举值(如
status、trigger_type)强制限定为小写英文 + 下划线,不接受自由输入 - 用
validator在 collection 级别约束结构,例如:{ $jsonSchema: { properties: { trigger_type: { enum: ["cart_total", "sku_count", "user_level"] }, min_amount: { bsonType: "double", minimum: 0 } } } }
动态条件查询必须靠 $expr,不是 $where 或应用层 if-else
促销匹配需要跨字段比较(比如“订单总金额 ≥ 规则 min_amount”、“用户等级 ≥ 规则 required_level”),这种逻辑不能靠 Java/PHP 里先查出所有规则再逐条判断——延迟高、内存爆、无法分页。
-
$expr允许在$match阶段直接引用文档内多个字段,例如:{ $expr: { $and: [ { $gte: [ "$order.total", "$rule.min_amount" ] }, { $gte: [ "$user.level", "$rule.required_level" ] } ] } } - 注意:只有
$eq、$lt、$lte、$gt、$gte在$expr中能走索引,且仅限字段与常量比较;字段间比较(如"$a"vs"$b")不走索引 - 避免在
$expr里调用$function或自定义 JS——MongoDB 已弃用$where,且 JS 执行无索引、不可并行、不安全
高频筛选字段必须单独建复合索引,按 ESR 顺序
促销引擎上线后,最卡的永远是“查当前生效、指定类型、满足用户等级的规则”,这类查询如果没索引,explain() 一眼看到 COLLSCAN。
- 索引字段顺序严格按 ESR:等值字段(
status: "active"、trigger_type: "cart_total")放最左,排序字段(如priority: -1)居中,范围字段(如start_time: { $lte: ISODate(...) }、end_time: { $gte: ISODate(...) })放最右 - 错误示例:
{ status: 1, start_time: 1, priority: -1 }——start_time是范围条件,放中间会让priority排序失效 - 正确示例:
{ status: 1, trigger_type: 1, priority: -1, start_time: 1, end_time: 1 },配合查询{ status: "active", trigger_type: "cart_total", start_time: { $lte: now }, end_time: { $gte: now } } - 时间范围字段(
start_time/end_time)必须加 TTL 索引或定期归档,否则集合膨胀后扫描成本指数上升
规则启用/停用不要用软删除字段,用状态机 + 时间窗口
见过太多团队加个 is_deleted: false 字段来控制规则是否生效,结果查询时总漏掉这个条件,或者前端传参错写成 is_deleted: "false"(字符串 vs 布尔)。
- 状态字段只允许三个值:
"draft"、"active"、"expired",由后台定时任务根据start_time/end_time自动更新,不依赖人工开关 - 查询一律基于时间窗口 + 状态双校验:
{ status: "active", start_time: { $lte: now }, end_time: { $gte: now } } - 避免用
$or合并多个时间条件(如{ $or: [{ start_time: null }, { start_time: { $lte: now } }] }),这会让索引失效;宁可补默认值(start_time: ISODate("1970-01-01"))保持查询结构稳定
真正难的不是字段怎么设,而是每次加一个新规则类型(比如“老用户复购返券”),都要同步改 validator、加索引、更新查询语句里的 $expr 表达式——漏掉任意一环,线上就出错。schema 变更必须走 DBA 审批 + 自动化测试验证,不能靠开发凭感觉改。


















