餐饮 SaaS 优惠券系统架构演进(九)营销策略 DSL 与规则生命周期:从运营配置、静态校验到编译执行、影响面分析与安全发布

在 PriceQuote 版本化、Golden Case、Shadow Diff 和灰度 Gate 之上,继续解决规则从哪里来、怎样表达、如何证明安全、以什么制品上线,以及发生异常时如何精确回退。本篇用一个"空门店范围被解释为全量"的脱敏案例,完整拆解 DSL、编译器、依赖图、Impact Report 与生命周期控制。

Promotion DSLStatic AnalysisImpact AnalysisRCA / Postmortem

结论先行: 营销 DSL 的价值不是让运营写表达式,而是把规则能力收敛到一个类型化、可静态分析、可编译、可回放、可审批、可灰度的受限语言中。在线 Runtime 不直接解释草稿 DSL,只执行带版本、Hash 和签名的不可变制品;每次发布必须同时携带依赖快照、影响面报告和回退目标。

案例边界与脱敏: 本文中的租户、门店、商品、活动、券、规则键和版本号均为示例值,例如 tenant_demo、store_demo_1001 和 rule_weekday_2026_09_v3。案例保留真实技术机制,不暴露任何生产标识或业务数据。

1. 背景与问题:配置中心不是规则平台

第八篇已经让 PriceQuote 可以版本化、回放和审计,但规则本身仍可能以数据库 JSON、配置中心键值或大量 if/else 存在。只要规则创作、校验、发布和执行没有统一语义,PriceQuote 只能证明"某个版本算出了这个结果",却无法保证这个版本在进入生产前已经被充分理解。

一次策略发布演练中,运营清空了门店列表,准备改选门店分组。前端保存了 storeRefs: [],JSON Schema 认为它是合法数组;序列化层随后裁剪空数组,运行时又把缺失范围解释为 ALL。原本只面向 20 家门店的规则因此覆盖全部门店。整个链路没有语法错误,真正的问题是同一个状态在不同层拥有三套语义。

图 1:空门店范围被解释为全量范围的事故链

2. 目标与约束:让规则变更成为可证明的工程事件

目标 可验证标准 关键约束
表达力受控 覆盖活动、会员价、优惠券、买赠与预算场景 不支持任意代码、网络、反射和无界循环
语义唯一 缺失、空集合、全量、零金额都有明确含义 禁止依赖前端或运行时默认值猜测
静态可判定 发布前发现类型、单位、引用、依赖和冲突问题 校验必须返回源码位置和稳定诊断码
执行确定 同一 Fact + 规则制品得到相同 Candidate 与 Trace 时间、随机数和外部状态必须显式注入
安全发布 Impact Gate、审批、灰度和回退形成闭环 ACTIVE 版本不可原地修改

核心不变量: same(dsl, compilerVersion, dependencySnapshot) ⇒ same(canonicalIrHash, artifactHash);same(artifact, factSnapshot) ⇒ same(candidate, executionTrace)。

3. 系统架构:创作平面、证明平面、发布平面与运行平面

规则平台必须把"编辑"和"执行"隔离。Operations Console 只创建草稿;Validator / Compiler 把草稿转成不可变制品;Simulation / Impact Analysis 证明候选版本的影响;Release Control 在审批和 Gate 通过后生成发布批次;在线 Runtime 只消费被签名的 Artifact。

图 2:营销策略 DSL 治理架构

  • **创作平面:**结构化表单与 DSL 双向映射,运营不需要直接编写底层表达式。
  • **证明平面:**静态分析、Golden Case、历史回放和影响面分析生成机器可读证据。
  • **发布平面:**审批、生效窗口、灰度范围、Gate 和回退版本被锁定在 Release Batch。
  • **运行平面:**按稳定业务键路由版本,只加载已编译制品,并输出 Trace / reasonCode。

4. DSL 核心设计:JSON/YAML 是载体,类型与语义才是语言

如果只是把原有配置换成 YAML,系统并没有获得 DSL 能力。真正的 DSL 至少要定义:有哪些事实可读、有哪些操作符、每种类型如何比较、范围如何表达、Action 输出什么、冲突和预算如何声明、错误怎样定位,以及不同 Schema 版本如何兼容。

图 3:PromotionRule 的六层 DSL 结构

复制代码
apiVersion: promotion/v2
kind: CouponRule
metadata:
  ruleKey: rule_weekday_demo
  owner: marketing-platform
scope:
  mode: INCLUDE
  storeRefs: [store_demo_1001, store_demo_1002]
when:
  all:
    - gte: [{fact: order.payableAmount}, {money: "80.00", currency: CNY}]
    - between: [{fact: clock.localTime}, "10:00", "14:00"]
then:
  discount: {type: AMOUNT_OFF, amount: "10.00", currency: CNY}
policy:
  priority: 120
  stackGroup: lunch_coupon
  budgetRef: budget_demo_monthly
explain:
  reasonCode: WEEKDAY_LUNCH_COUPON

范围语义必须显式: scope.mode = ALL 才表示全量;INCLUDE 和 EXCLUDE 均要求非空引用集合。缺失 mode、空列表和未知门店必须 fail closed。

5. 编译流程:AST、静态语义与 Canonical IR

在线解释 DSL 会把解析、默认值、类型转换和错误处理带到核心定价链路,既慢又难以证明。更稳妥的方式是发布前编译:Parser 保留源码位置,Schema Check 处理结构,Type Checker 处理 Money / Time / Quantity,Semantic Analyzer 解析依赖和约束,最后生成稳定 Canonical IR 与签名制品。

图 4:DSL 到不可变执行制品的编译流水线

阶段 输入与输出 典型诊断
Parse YAML/JSON → Source AST E001 非法语法、E002 重复字段
Schema AST → Typed Node Skeleton S101 未知字段、S102 不支持的 apiVersion
Type / Unit 类型推断与单位归一 T201 Money 与 Decimal 混算、T202 时区缺失
Semantic 范围、引用、依赖图、可达性 M301 空 INCLUDE、M302 循环依赖、M303 无效预算
Canonical IR 显式默认值、稳定排序、内容 Hash C401 非确定字段、C402 不稳定集合
Artifact 索引 + Predicate + Action + 签名 A501 制品过大、A502 成本超限
复制代码
CompileResult compile(SourceDsl source) {
  Ast ast = parser.parseWithLocation(source);
  TypedAst typed = schemaAndTypeChecker.check(ast);
  DependencyGraph graph = semanticAnalyzer.resolve(typed);
  requireAcyclic(graph);
  CanonicalIr ir = canonicalizer.lower(typed, graph.snapshot());
  require(costEstimator.estimate(ir).withinLimit());
  return artifactSigner.sign(artifactBuilder.build(ir));
}

6. 数据模型与 ERD:定义、版本、制品和发布批次不能混在一张表

rule_definition 表示稳定业务身份,rule_version 表示一次不可变内容,rule_artifact 表示某编译器生成的可执行制品。Simulation、Approval 和 Release Batch 都引用 ruleVersionId,但不回写 DSL 内容;Execution Trace 关联 Quote 与实际执行的规则版本。

图 5:规则生命周期数据模型与表关联关系

表 主键 / 业务键 职责与关系
rule_definition rule_id;rule_key 唯一 稳定规则身份;与 rule_version 为 1:N
rule_version rule_version_id;rule_id + version_no 不可变 DSL、Schema 版本、内容 Hash 与生效窗口
rule_artifact artifact_id;rule_version_id + compiler_version Canonical IR、索引、签名与 artifactHash
rule_dependency dependency_id 引用预算、券模板、商品组、门店组及依赖 Hash
simulation_run run_id;dataset_version 回放输入、Impact Summary、Diff 与结论
approval_record / release_batch approval_id / batch_id 审批证据、版本集合、灰度计划和回退目标
execution_trace trace_id;quote_id + rule_version_id 命中、淘汰、耗时、reasonCode 与实际制品

6.1 ID 映射与写入顺序

  • ruleKey 是稳定业务键,ruleVersionId 是不可变版本主键,artifactId 是编译产物主键。
  • 保存 Draft 后先生成 ruleVersion;静态校验和编译通过后写 Artifact / Dependency Snapshot。
  • Simulation Run 完成并冻结 Impact Report 后才能创建 Approval;审批通过后创建 Release Batch。
  • Release Batch、版本集合、灰度键和 rollbackBatchId 应在同一事务中锁定,并通过 Outbox 发布。

7. 静态校验:从字段合法走向业务可执行

JSON Schema 只能证明结构像一条规则,不能证明它在业务上可执行。语义校验必须把多字段关系、不变量、引用对象和依赖图一起检查,并给出稳定诊断码、JSON Pointer、源码行列和修复建议。

类别 校验示例 失败策略
范围 INCLUDE / EXCLUDE 必须非空;ALL 不允许携带 refs 阻断编译
类型与单位 Money 比较必须币种一致;Duration 不能与 Time 直接比较 阻断编译
引用完整性 门店组、券模板、预算和商品组必须存在且可发布 阻断发布并返回 dependencyRef
依赖图 禁止循环依赖、跨租户引用和未固定版本引用 阻断编译
可达性 永真、永假、互相矛盾或被更高优先级完全遮蔽 警告或阻断,按风险分级
复杂度 AST 深度、集合大小、候选数和预计执行成本 超过配额则拒绝制品

8. 影响面分析:发布前必须回答'会影响谁、多少钱、为什么'

Impact Analysis 不能只给出一个 Diff Rate。报告必须同时覆盖人群、规则重叠、金额与预算、PriceQuote 差异、运行成本和上下游兼容性;每个百分比都应带分母、采样窗口、数据集版本和可复现 caseId。

图 7:规则影响面分析的六个维度

Gate 示例阈值 阻断原因
Scope Gate 新增覆盖门店 ≤ 审批声明;命中订单增幅 < 5% 防止范围意外放大
Money Gate P0 = 0;P1 < 0.01%;单笔优惠不超过上限 防止价格与资金风险
Budget Gate 日预算 P95 消耗预测不超过 85% 保留峰值缓冲
Conflict Gate 新增高优先级遮蔽规则必须人工确认 避免旧规则静默失效
Performance Gate P95 增幅 < 10%;候选数与 AST 成本不超配额 保护在线链路
Compatibility Gate Fact / Quote Schema 与 reasonCode 均向前兼容 保护下游订单、退款与客服

9. 发布流程:把校验、模拟、审批和灰度串成不可跳过的状态机

发布不是把 status 改成 ACTIVE。候选版本先编译,再模拟,再审批,最后由 Release Control 锁定 ruleVersionSet、artifactHash、dependencyHash、灰度计划和 rollbackBatchId。任一证据过期,例如门店组或预算版本变化,都必须重新模拟。

图 6:规则发布 Sequence Diagram

图 9:规则版本生命周期状态机

**版本不可变:**ACTIVE 规则禁止原地修改。紧急停用只改变路由状态,不删除版本或制品;修复必须创建新 ruleVersionId,并重新经过静态校验、影响分析和审批。

10. 运行时执行:先索引缩小候选,再执行受限 Predicate

在线 Runtime 不解析 YAML,也不访问配置中心。Version Router 先锁定版本集合;索引根据租户、门店、渠道和时段预筛;类型化 Predicate 只读 Fact Snapshot;Action 仅产生 Candidate,由统一 Conflict / Allocation 阶段决定最终 PriceQuote。

图 8:编译制品的在线执行与证据输出路径

  • 禁止网络、文件、反射、动态类加载、任意脚本和无界循环。
  • 只允许白名单函数,例如金额比较、集合包含、时间窗口和确定性字符串匹配。
  • 限制 AST 深度、集合长度、候选数、执行步数、内存与单规则耗时。
  • 超限、未知 Fact、缺失依赖和签名失败全部 fail closed,并记录稳定 reasonCode。
  • Trace 至少包含 ruleVersionId、artifactHash、输入 factHash、结果、淘汰原因和耗时。

11. 依赖图与一致性:规则引用必须固定到可验证快照

DSL 常会引用门店组、商品组、券模板、预算和会员分群。如果运行时只保存一个逻辑名称,引用目标被修改后,同一规则版本会产生不同结果。编译时必须解析依赖并保存 dependencyHash;发布前再次核验,运行时按快照或不可变版本读取。

复制代码
RuleArtifactIdentity {
  ruleVersionId: "rv_demo_0009",
  dslSchemaVersion: "promotion/v2",
  compilerVersion: "dsl-compiler-3.4.1",
  canonicalIrHash: "sha256:...",
  dependencies: [
    {type: "STORE_GROUP", ref: "sg_demo_01", version: "7", hash: "sha256:..."},
    {type: "BUDGET", ref: "budget_demo_monthly", version: "12", hash: "sha256:..."}
  ],
  artifactHash: "sha256:...",
  signatureKeyVersion: "sig-key-05"
}

12. Troubleshooting:空门店范围意外覆盖全量的完整排查

Symptoms

规则 rule_weekday_demo 灰度后,非目标门店也出现优惠 Candidate;Shadow Diff 的命中订单量比 Impact Report 高 34 倍,但单笔折扣金额和金额不变量均正常,因此常规金额告警没有触发。

Investigation

  • 按 ruleVersionId 查询 Execution Trace,确认异常门店使用了同一 artifactHash,排除版本路由漂移。
  • 对比 Draft、Canonical IR 和 Artifact,发现 Draft 中存在 storeRefs: [],IR 中 scope 字段完全缺失。
  • 检查序列化日志,确认空数组被通用 NON_EMPTY 策略裁剪。
  • 检查 Runtime 默认逻辑,发现缺失 scope 为兼容旧配置被解释为 ALL。
  • 重放 Impact Dataset,发现模拟器把空数组解释为 NONE,与 Runtime 语义不一致。

Root Cause

直接根因是空数组被裁剪后触发 Runtime 的 ALL 默认值;系统性根因是规则范围没有显式代数模型,Draft、Simulator、Compiler 和 Runtime 各自推断缺失值语义。发布流程又只校验金额差异,没有把作用域扩张作为独立 Gate。

图 10:空门店范围变成全量范围的 5 Whys

Resolution

  • 引入 scope.mode = ALL | INCLUDE | EXCLUDE,INCLUDE / EXCLUDE 的 refs 必须非空。
  • Canonical IR 展开所有语义默认值,禁止序列化层裁剪有业务含义的空值。
  • Simulator 与 Runtime 共享同一编译制品,不再分别解释 Draft DSL。
  • 新增 Scope Gate:覆盖门店、命中订单和新增人群超过审批声明即阻断。
  • 把事故样本加入 Golden Case,并对历史规则执行静态迁移扫描。

Verification

修复后,对 3,216 个存量规则版本执行迁移检查,识别出 17 个范围语义不明确的版本并阻断自动迁移;对 28,640 个历史订单回放,Simulator 与 Runtime 的 artifactHash 和命中集合一致;连续 48 小时 Shadow 中 Scope P0/P1 为 0。

Lessons Learned

缺失、空集合和全量不是三个可互换的表示,而是三个业务状态。任何依赖默认值的规则语言都会在多实现、多版本或序列化边界上产生歧义;语义必须在 DSL 类型系统里显式封闭。

13. 为什么原方案会失败,最终方案为什么有效

原方案 失败机制 最终方案为何有效
JSON Schema 校验 只能检查结构,无法表达多字段语义和业务不变量 类型检查 + Semantic Analyzer 同时验证范围、引用、依赖与成本
在线解释配置 不同服务复制默认值与解析逻辑,结果容易漂移 发布前生成唯一 Canonical IR,Simulator 与 Runtime 共用制品
逻辑名称引用依赖 门店组、预算或模板修改后历史版本不可复现 Dependency Snapshot + Hash 固定引用语义
只看金额 Diff 作用域放大但单笔金额正常时无法发现 Scope、Budget、Conflict、Performance 多维 Gate
ACTIVE 原地修改 无法审计、回放和安全回退 版本只追加,路由切换表达发布与回退

14. 验证方案:编译器、规则库、回放与生产证据四层测试

  • **Compiler Unit:**Parser、类型、单位、默认值、Canonicalization 和诊断位置测试。
  • **Rule Property:**随机生成范围、金额、时间和集合,验证确定性、单调性与复杂度上限。
  • **Golden / Migration:**覆盖历史事故、Schema 升级、依赖版本和旧规则兼容。
  • **Shadow / Canary:**比较命中集合、Candidate、PriceQuote、预算消耗、耗时和 reasonCode。
复制代码
Invariant 1: INCLUDE / EXCLUDE => refs is non-empty
Invariant 2: missing scope.mode is always invalid
Invariant 3: canonicalize(parse(source)) is deterministic
Invariant 4: artifactHash binds compilerVersion and dependencyHash
Invariant 5: Simulator and Runtime execute the same artifact
Invariant 6: ACTIVE version content is immutable
Invariant 7: every execution result has ruleVersionId and reasonCode

15. 方案取舍

决策 收益 代价与控制
受限 DSL 而非通用脚本 可静态分析、可估算成本、攻击面小 表达力受限;通过白名单算子和版本化扩展
发布前编译 在线低延迟,Simulator / Runtime 语义一致 编译链路更复杂;保存诊断、IR 与制品证据
依赖快照 历史可复现,发布影响明确 存储与治理成本增加;使用 Hash、引用计数和冷热分层
多维 Impact Gate 发现作用域、预算和兼容性风险 回放成本增加;分层采样与增量数据集
版本只追加 审计、灰度与回退简单可靠 版本数量增长;生命周期与保留期治理

16. Lessons Learned

  • **第一,**配置格式不等于 DSL;语言必须拥有类型、语义、诊断、版本和执行边界。
  • **第二,**缺失、空集合、零值和全量必须显式建模,不能交给序列化或默认值决定。
  • **第三,**Simulator 与 Runtime 必须执行同一个 Artifact,否则预览正确也无法证明生产正确。
  • **第四,**规则发布风险不只有金额,覆盖人群、规则重叠、预算、性能和兼容性同样需要 Gate。
  • **第五,**依赖也是版本的一部分;规则内容不变但引用对象变化,结果仍可能变化。
  • **第六,**版本不可变让审计和回退变简单,紧急操作也应切换路由而不是覆盖历史。

17. 后续优化

  • 建设可视化 AST / Dependency Graph,让运营和研发看到规则如何被编译。
  • 为诊断码建立知识库,提供可执行修复建议和 IDE 式即时反馈。
  • 引入规则等价性与可达性分析,识别永真、永假、遮蔽和冗余版本。
  • 建立存量规则迁移器:旧 Schema → 新 AST → Canonical IR,并保留迁移 Diff。
  • 把高风险 Impact 样本自动转成脱敏 Golden Case,经审批后进入长期回归集。
  • 对 Artifact 加入 SBOM 式元数据:编译器、函数库、Schema、依赖和签名密钥版本。

18. 总结

规则平台的成熟度,不取决于它能配置多少种优惠,而取决于每条规则能否被明确表达、静态理解、确定编译、量化影响、安全发布和完整回放。Promotion DSL 把业务意图变成受控语言,Compiler 把语言变成不可变制品,Impact Report 把变化变成证据,Lifecycle 与 Gate 则把证据变成发布约束。

**最终结论:**运营配置必须先成为可证明的 Rule Artifact,才能成为线上执行逻辑。没有类型系统、依赖快照、影响面分析和不可变版本的 DSL,只是另一种更难排查的配置文件。

19. 下一篇

第十篇建议继续进入权益履约与资金一致性:《营销权益执行与预算控制:预占、核销、释放、退款回补与分布式一致性》。重点讨论 Benefit Ledger、Budget Reservation、幂等键、状态机、Outbox / Inbox、补偿事务、超卖防护和财务对账。

相关推荐
Xudde.1 小时前
Billu: box 靶机渗透测试(Vulnhub Writeup)
笔记·学习·安全·web安全
小虎牙^O^1 小时前
棋盘格隐写:基于奇偶相位的双图隐藏与还原
图像处理·python·安全
jerryinwuhan1 小时前
从GPT到Agent:下一代人工智能系统架构演进
人工智能·gpt·系统架构
迪康Defender1 小时前
终端安全行为分析:审计日志如何转化为业务报表?落地实践与避坑指南
运维·经验分享·安全·信息可视化·自动化·安全威胁分析·运维开发
技术不好的崎鸣同学1 小时前
第七章:模块踩踏以覆写导出函数绕过CFG
c++·windows·安全·系统安全
黄一一9112431 小时前
吾爱大佬开发,离线可用!PDF文件处理工具,本地网页版文件-浏览器里就能搞定,所有操作系统可用
windows·安全·阿里云·pdf·.net·开源软件
诚心呈意共享外卖配送系统2 小时前
多平台订单聚合系统的技术架构:接 API 只是开始
java·开发语言·架构·系统架构·创业创新
mattleeee3 小时前
多策略组合系统的架构演进:从单策略到全天候
系统架构·架构演进·多策略
郝学胜-神的一滴3 小时前
Web安全入门:读懂Web世界里的攻防
前端·网络·后端·安全·web安全·hibernate