📧 Email Template 架构重设计

从隐式变量传递到显式合约驱动 — 2026-03-01

1. 问题诊断:当前系统的三大隐患

核心问题:变量合约(Variable Contract)是隐式的。模板编辑器、变量解析器、触发系统三方各自定义变量,没有 Single Source of Truth。

1.1 变量命名不一致

前端 template-variables.ts

appointment.date
appointment.time
guests[].name
services[].price
technician.name

后端 Seed migration 1807000000012

{{appointment_date}}
{{appointment_time}}
{{customer_name}}
{{service_name}}
{{employee_name}}

前端定义的是嵌套结构appointment.date),后端 seed 模板用的是扁平命名appointment_date)。两者不兼容。

1.2 数据源 → 变量的映射是隐式的

要知道 "appointment confirmation 模板能用哪些变量",必须:

  1. execution-processor.js_buildFullVariableContext()
  2. 再读 service-context.jsbuildContext()
  3. 再读 template-variables.ts 的前端定义

三个文件分散定义,没有编译时或运行时校验它们是否一致。

1.3 编辑器易用性问题

编辑器现状问题
HTML Editor原始 HTML + 组件库非技术用户门槛过高
Canvas Editor (Fabric.js)拖拽式可视化主要用于 Email Designs,变量插入靠剪贴板
GrapesJS Visual EditorSpec 070,Draft尚未完成,变量支持为 MVP 级别

2. 目标架构:四层分离模型

graph TB subgraph "Layer 1: Event Type 事件类型" E1["appointment_created"] E2["appointment_updated"] E3["service_completed"] E4["birthday"] E5["gift_card_expiry"] end subgraph "Layer 2: Automation Rule 自动化规则" AR["调度策略 + 通道 + 受众"] AR1["When: 立即 / +2h / -24h / cron"] AR2["Channel: email / sms"] AR3["Audience: all / vip / custom"] AR4["Template: appointment_confirm"] end subgraph "Layer 3: Variable Schema 变量合约 SSOT" VS["声明式变量定义"] VS1["appointment.date, time, status"] VS2["guest.name, email, phone"] VS3["services - name, price, duration"] VS4["store.name, address, phone"] VS5["system.cancel_link, calendar_url"] end subgraph "Layer 4: Content Template 内容模板" CT1["HTML Handlebars"] CT2["Plain Text"] CT3["Visual Block Editor"] end E1 --> AR E2 --> AR E3 --> AR E4 --> AR E5 --> AR AR --> VS VS --> CT1 VS --> CT2 VS --> CT3 style VS fill:#2563eb,stroke:#1d4ed8,color:#fff style AR fill:#7c3aed,stroke:#6d28d9,color:#fff

2.1 各层职责

职责关键输出
Layer 1 Event Type 定义"发生了什么"(信号),携带 seed data { appointmentId, guestId, eventTime }
Layer 2 Automation Rule 定义"何时发、发给谁、用哪个模板"(调度 + 路由) 调度时间、通道、受众、模板引用
Layer 3 Variable Schema SSOT:声明式定义每种模板类型可用的变量集 变量名、类型、是否数组、示例值、描述
Layer 4 Content Template 使用 Variable Schema 内的变量渲染内容 渲染后的 HTML / 纯文本

3. Variable Schema:核心设计

核心理念:Variable Schema 是连接触发系统和模板系统的显式合约。它不是代码里的隐式行为,而是一个可声明、可校验、可展示的数据结构。

3.1 Schema 的四个消费者

graph LR VS["Variable Schema
SSOT"] ED["Editor
只展示 schema 内的变量"] RE["Resolver
负责组装 schema 要求的变量"] VA["Validator
渲染前校验变量完整性"] US["User
在编辑器中看到可用变量列表"] VS --> ED VS --> RE VS --> VA VS --> US style VS fill:#2563eb,stroke:#1d4ed8,color:#fff

3.2 Schema 数据结构示例

{
  "schema_type": "service_email",
  "description": "用于预约相关通知(确认、提醒、取消等)",
  "variables": {
    "appointment": {
      "type": "object",
      "fields": {
        "id":     { "type": "string", "example": "APT-20260301-001" },
        "date":   { "type": "string", "example": "2026-03-15", "format": "date" },
        "time":   { "type": "string", "example": "14:00" },
        "status": { "type": "string", "example": "confirmed", "enum": ["confirmed","cancelled","completed"] },
        "total":  { "type": "number", "example": 85.00, "format": "currency" }
      }
    },
    "guest": {
      "type": "object",
      "fields": {
        "name":  { "type": "string", "example": "Jane Smith" },
        "email": { "type": "string", "example": "jane@example.com" },
        "phone": { "type": "string", "example": "+1234567890" }
      }
    },
    "services": {
      "type": "array",
      "item_fields": {
        "name":     { "type": "string", "example": "Gel Manicure" },
        "price":    { "type": "number", "example": 45.00, "format": "currency" },
        "duration": { "type": "number", "example": 60, "unit": "minutes" }
      }
    },
    "technician": {
      "type": "object",
      "fields": {
        "name":  { "type": "string", "example": "Emily" },
        "phone": { "type": "string", "example": "+1234567890" }
      }
    },
    "store": {
      "type": "object",
      "fields": {
        "name":    { "type": "string", "example": "QQ Nails Flushing" },
        "address": { "type": "string", "example": "123 Main St" },
        "phone":   { "type": "string", "example": "+1234567890" }
      }
    },
    "system": {
      "type": "object",
      "fields": {
        "cancel_link":     { "type": "string", "example": "https://..." },
        "reschedule_link": { "type": "string", "example": "https://..." },
        "calendar_url":    { "type": "string", "example": "https://..." }
      }
    }
  },
  "resolver": "service-context",
  "seed_fields": ["appointmentId", "guestId"]
}

3.3 Schema Types 对照表

Schema Type适用场景核心变量Resolver
service_email 预约确认、提醒、取消、改期 appointment, guest, services[], technician, store, system service-context.js
promotion_email 营销活动、折扣推广 guest (含 loyalty), store, campaign, system promotion-context.js
lifecycle_email 生日祝福、流失挽回 guest (含 visit history), store, system lifecycle-context.js (待建)
transactional_email 支付收据、退款通知、礼品卡 guest, transaction, store, system transactional-context.js (待建)

4. 端到端数据流(重设计后)

sequenceDiagram participant Event as Event Source participant TE as Trigger Engine participant AR as Automation Rule participant Resolver as Variable Resolver participant Schema as Variable Schema participant Editor as Template Engine participant Sender as Email Sender Event->>TE: appointment_created
{appointmentId, guestId} TE->>AR: 匹配 active rules AR->>AR: 计算调度时间
确定 channel + template AR->>Resolver: seed data + schema_type Resolver->>Schema: 获取 schema 定义 Schema-->>Resolver: 返回变量清单 Resolver->>Resolver: 查 DB 组装变量
guest, appointment, services, store Resolver->>Resolver: 校验: 所有 required 变量已填充 Resolver->>Editor: 完整变量上下文 + template body Editor->>Editor: Handlebars 渲染 Editor->>Sender: 渲染后 HTML + subject Sender->>Sender: 通过 Resend/SES 发送

4.1 与现状对比

环节现状(隐式)目标(显式)
变量定义 前端 ts + 后端 context builder + seed 模板
三处各自定义
Variable Schema JSON 为 SSOT
前后端从中派生
变量命名 嵌套 vs 扁平不一致 统一嵌套命名 appointment.date
编辑器变量发现 前端硬编码 template-variables.ts 从 Schema 动态加载
按 schema_type 过滤
渲染前校验 无(missing 变量静默变空字符串) Resolver 输出对照 Schema 校验
缺失变量 warning log
调度时间 绑定在 trigger_templates 表 属于 Automation Rule(同一事件可多种调度)

5. 编辑器体验改进方向

5.1 内容模板的三种渲染形式

无论哪种形式,都共享同一个 Variable Schema

graph LR subgraph "Variable Schema SSOT" S["service_email schema"] end subgraph "Content Template 形式" HTML["HTML Editor
Handlebars 语法
适合开发者"] Text["Plain Text
纯文本 + 变量
适合 SMS"] Visual["Visual Block Editor
拖拽式 WYSIWYG
适合非技术用户"] end S --> HTML S --> Text S --> Visual style S fill:#2563eb,stroke:#1d4ed8,color:#fff

5.2 变量插入体验

形式当前体验改进方向
HTML Editor 手动输入 {{var}} VariableCascader + 点击插入(已有,需对齐 Schema)
Visual Editor 剪贴板复制粘贴 inline variable chip(选中文本块 → 插入变量 → 显示为可视化标签)
Plain Text 不存在 简单文本区 + 变量列表侧边栏

6. 关键设计决策(待讨论)

Decision 1: Variable Schema 存储位置

方案优点缺点
A. 代码中 JSON 文件
backend/schemas/variable-schemas/*.json
版本控制、代码审查、简单 新增 schema 需部署
B. 数据库表
variable_schemas
运行时可修改、租户可自定义 增加复杂度、需要管理界面
C. 混合
系统 schema 在代码,自定义在 DB
平衡灵活性和可控性 两个数据源需要合并逻辑

Decision 2: Automation Rule 与 Trigger Template 的关系

当前 trigger_templates (public schema) 定义了事件类型 + 调度时间。重设计后调度时间属于 Automation Rule。

方案说明
A. 保留 trigger_templates 作为"事件类型注册表",去掉调度字段,移到 automation_rules
B. 合并到 automation_rules trigger_templates 功能全部吸收进 automation_rules

Decision 3: 后端 seed 模板的变量迁移

现有 seed 模板使用扁平命名({{store_name}}),需要迁移到嵌套命名({{store.name}})。

方案说明
A. 一次性迁移 更新所有 seed 模板 + context builder 输出
B. 兼容层 context builder 同时输出嵌套和扁平两种格式,逐步迁移

7. 相关文件索引

组件文件
前端变量定义frontend/web_app/src/lib/template-variables.ts
前端变量选择器frontend/web_app/src/components/TemplateEditor/VariableCascader.tsx
后端变量引擎backend/services/templating/variable-engine.js
Service 上下文构建器backend/services/templating/context-builders/service-context.js
Promotion 上下文构建器backend/services/templating/context-builders/promotion-context.js
自动化执行处理器backend/services/automation/execution-processor.js
触发引擎backend/services/automation/trigger-engine.js
邮件发送backend/services/email/email-sender.js
HTML 模板编辑器 APIbackend/api/email-template-builder.js
Email Designs APIbackend/api/email-designs.js
Canvas 可视化编辑器frontend/web_app/src/components/EmailBuilder/CanvasEditor/
Seed 模板迁移backend/database/migrations/1807000000012_seed-appointment-email-templates.js

8. 相关 Specs

Spec状态关系
018-marketing-automationDraft定义了触发引擎、自动化规则、调度系统
070-visual-email-editorDraftGrapesJS 可视化编辑器,变量插入为 MVP
001-email-sms-integrationDone邮件/SMS 发送基础设施

更新日期: 2026-03-01