# Temporal Workflow Runtime

本目录包含 Workflow 执行引擎的所有组件。

## 架构

```
┌─────────────────────────────────────────────────────────────┐
│                    Temporal Server                            │
│               (调度中心，不执行业务代码)                        │
│                                                              │
│  ┌──────────┐  ┌──────────────┐  ┌────────────────────┐    │
│  │ 状态存储  │  │  任务队列管理  │  │  历史/可见性引擎    │    │
│  │ (PG)     │  │  (分发任务)    │  │  (查询/搜索)       │    │
│  └──────────┘  └──────┬───────┘  └────────────────────┘    │
│                        │                                     │
└────────────────────────┼─────────────────────────────────────┘
                         │  gRPC (任务分发 + 状态回报)
                         ▼
┌─────────────────────────────────────────────────────────────┐
│                    Temporal Worker                            │
│                (你的代码，执行业务逻辑)                         │
│                                                              │
│  ┌─────────────────┐    ┌──────────────────────────┐        │
│  │  Workflow 代码    │    │  Activity 代码            │        │
│  │  (DSL 解释器)    │    │  (HTTP调用/DB/通知/...)   │        │
│  └─────────────────┘    └──────────────────────────┘        │
└─────────────────────────────────────────────────────────────┘
```

### Server vs Worker

| | Server | Worker |
|--|--------|--------|
| 谁提供的 | Temporal 官方 Docker 镜像 | **你写的代码** |
| 执行业务逻辑？ | ❌ 完全不碰 | ✅ 全部在这里 |
| 有状态？ | ✅ 持久化到 PG | ❌ 无状态，可随时重启/扩容 |
| 挂了怎么办 | 所有 workflow 暂停 | Server 保留进度，Worker 恢复后继续 |
| 能横向扩展？ | 可以但复杂 | **随便加**，同一 task queue 多个 Worker |

**一句话：Server 是"大脑"（调度+记忆），Worker 是"手脚"（干活）。**

## 目录结构

```
temporal/
├── package.json              # 便捷脚本
├── docker-compose.yml        # Temporal Server + Web UI
├── config/
│   └── development-sql.yaml  # Server 动态配置
└── worker/                   # DSL 解释器 Worker
    ├── package.json
    ├── tsconfig.json
    ├── .env                  # Worker 环境变量
    └── src/
        ├── index.ts          # Worker 启动入口
        ├── config.ts         # 配置读取
        ├── outbox-poller.ts  # 事件轮询器（扫描 outbox 表触发 workflow）
        ├── workflows/
        │   └── dsl-workflow.ts   # 核心：DSL 动态解释器
        ├── activities/
        │   ├── index.ts
        │   ├── http-request.ts   # HTTP 调用
        │   ├── db-query.ts       # 数据库查询
        │   ├── db-execute.ts     # 数据库写入
        │   ├── send-notification.ts  # 多渠道通知
        │   ├── code-execute.ts   # 沙箱 JS 执行
        │   └── reporting.ts      # 状态回写到业务 DB
        └── dsl/
            ├── types.ts      # DSL 类型定义
            ├── graph.ts      # DAG 解析 + 拓扑排序
            ├── context.ts    # 变量/上下文管理
            └── condition.ts  # 条件表达式求值
```

## 快速启动

```bash
cd temporal

# 1. 启动 Temporal Server（复用项目现有 PostgreSQL）
npm run up

# 2. 安装 Worker 依赖
npm run worker:install

# 3. 启动 Worker（开发模式）
npm run worker:dev
```

## 工作流程

```
1. 用户在前端点击 "Run" 按钮
       │
       ▼
2. NestJS 调用 temporalService.startWorkflow()
       │
       ▼
3. Temporal Server 收到请求，创建记录，放入 task queue
       │
       ▼
4. Worker 长轮询 Server，收到新任务
       │
       ▼
5. Worker 执行 dslWorkflow()：
   - 解析 DSL JSON → DAG
   - 拓扑排序确定执行顺序
   - 逐节点执行对应 Activity
   - 条件节点做分支选择
   - 并行节点用 Promise.all
   - 审批节点等待 Signal
       │
       ▼
6. 每个节点执行结果回写到 t_workflow_node_execution
       │
       ▼
7. Workflow 完成/失败 → 更新 t_workflow_instance 状态
```

## 数据库表分工

Temporal 体系下有 **两类 PG 表**，分布在不同 schema，互不干扰：

| 类别 | 所在 schema | 谁来写 | 典型表 | 用途 |
|------|------------|--------|--------|------|
| **Temporal 系统表** | `temporal` (Server 启动时自动建表) | 仅 Temporal Server | `executions`、`history_node`、`history_tree`、`current_executions`、`task_queues`、`tasks`、`timer_tasks`、`shard`、`namespaces` | Workflow 状态机、事件历史、任务队列、定时器、分片协调 |
| **业务表** | `public` (Prisma 管理) | NestJS / Worker Activity | `t_workflow`、`t_workflow_version`、`t_workflow_instance`、`t_workflow_node_execution`、`t_workflow_event_outbox` | 业务可见的工作流定义、实例、节点执行记录、事件触发 outbox |

> Temporal Server **从不读写**业务表；业务代码（NestJS / Worker Activity）**从不读写** Temporal 系统表。两者通过 `temporal_run_id` 字段（保存在 `t_workflow_instance` 上）建立关联。

## 两种触发场景的时序图

### 场景 A：NestJS 主动调用 `temporalService.startWorkflow()`

入口：用户在前端点 "Run"，或 Agenda 定时任务 / 其他业务代码主动调用 `WorkflowService.executeWorkflow()`。

```
┌────────┐   ┌──────────────────┐   ┌─────────────────┐   ┌────────────────┐   ┌────────────┐
│Frontend│   │ NestJS           │   │ PostgreSQL      │   │ Temporal Server│   │ Worker     │
│        │   │ WorkflowService  │   │ (业务 schema)    │   │ (gRPC :7233)   │   │ (long-poll)│
└───┬────┘   └────────┬─────────┘   └────────┬────────┘   └───────┬────────┘   └─────┬──────┘
    │ POST /run       │                      │                    │                   │
    ├────────────────►│                      │                    │                   │
    │                 │ SELECT t_workflow + t_workflow_version    │                   │
    │                 ├─────────────────────►│                    │                   │
    │                 │◄─────────────────────┤ 校验 PUBLISHED/enabled                  │
    │                 │                      │                    │                   │
    │                 │ INSERT t_workflow_instance (status=PENDING)│                   │
    │                 ├─────────────────────►│                    │                   │
    │                 │◄──── instanceId ─────┤                    │                   │
    │                 │                      │                    │                   │
    │                 │ temporalService.startWorkflow('dslWorkflow', …)               │
    │                 ├──────────────────────────────────────────►│                   │
    │                 │                      │  写 temporal.executions / history_node │
    │                 │                      │  + 投递 WorkflowTask 到 task_queues    │
    │                 │                      │◄───────────────────┤                   │
    │                 │◄──── temporalRunId ──────────────────────┤                   │
    │                 │                      │                    │                   │
    │                 │ UPDATE t_workflow_instance SET temporal_run_id│                │
    │                 ├─────────────────────►│                    │                   │
    │                 │                      │                    │ PollWorkflowTask  │
    │                 │                      │                    │◄──────────────────┤
    │                 │                      │                    ├──── task ────────►│
    │                 │                      │                    │                   │ 执行 dslWorkflow()
    │                 │                      │                    │                   │ ──┐
    │                 │                      │                    │ ScheduleActivity  │ <─┘
    │                 │                      │                    │◄──────────────────┤
    │                 │                      │                    ├── activity task ─►│
    │                 │                      │                    │                   │ Activity 内调用
    │                 │                      │                    │                   │ reporting.ts:
    │                 │                      │◄────── INSERT/UPDATE t_workflow_node_execution ─┤
    │                 │                      │◄────── UPDATE t_workflow_instance (RUNNING/DONE)┤
    │                 │                      │                    │ CompleteActivity  │
    │                 │                      │                    │◄──────────────────┤
    │                 │                      │                    │                   │
    │                 │                      │                    │ … 直到 Workflow 完成 …
    │◄────── 200 OK { instanceId, temporalRunId } ────────────────┤                   │
```

**涉及的 PG 表：**

| 表 | 操作 | 写入方 |
|----|------|--------|
| `t_workflow`、`t_workflow_version` | SELECT | NestJS（取 DSL + 校验状态） |
| `t_workflow_instance` | INSERT (PENDING) → UPDATE (temporal_run_id) → UPDATE (status) | NestJS 创建；Worker `reporting.ts` 在执行中改 |
| `t_workflow_node_execution` | INSERT/UPDATE 每个节点 | Worker `reporting.ts`（Activity 内调用） |
| `temporal.executions` / `current_executions` | INSERT (新 WorkflowExecution) → UPDATE 状态 | Temporal Server（Frontend / History Service） |
| `temporal.history_node` / `history_tree` | INSERT 事件（Started / ActivityScheduled / Completed …） | Temporal Server |
| `temporal.task_queues` / `tasks` | INSERT/DELETE Workflow & Activity Task | Temporal Server（Matching Service） |
| `temporal.timer_tasks` | 节点超时、Workflow 总超时、Sleep 节点 | Temporal Server |

---

### 场景 B：Worker `outbox-poller` 因业务表变更触发

入口：业务表（如 `t_order`）发生 INSERT/UPDATE/DELETE → AFTER 触发器写一行到 `t_workflow_event_outbox` → Worker 进程内 outbox-poller 扫到。

```
┌────────────┐   ┌────────────────┐   ┌─────────────────────┐   ┌────────────────┐   ┌──────────────┐
│ 业务来源    │   │ PostgreSQL     │   │ Worker              │   │ Temporal Server│   │ Worker       │
│ (任意 INSERT│   │ (业务 schema)  │   │ outbox-poller.ts    │   │ (gRPC :7233)   │   │ dslWorkflow  │
│  /UPDATE)  │   │                │   │ (Worker 进程内)     │   │                │   │ (同一进程)    │
└─────┬──────┘   └───────┬────────┘   └──────────┬──────────┘   └───────┬────────┘   └──────┬───────┘
      │ INSERT/UPDATE t_order                    │                      │                   │
      ├─────────────────►│                       │                      │                   │
      │                  │ AFTER TRIGGER notify_workflow_event()        │                   │
      │                  │ INSERT t_workflow_event_outbox (同事务)       │                   │
      │                  │                       │                      │                   │
      │                  │ ◄── 每 10s ─────────  │ UPDATE outbox        │                   │
      │                  │                       │ SET processed=true   │                   │
      │                  │                       │ ... FOR UPDATE SKIP LOCKED                │
      │                  ├──── 未处理事件批 ────►│                      │                   │
      │                  │                       │                      │                   │
      │                  │ SELECT t_workflow JOIN t_workflow_version    │                   │
      │                  │ WHERE status='PUBLISHED' AND is_enabled      │                   │
      │                  ├──────────────────────►│ matchEventName + filterExpression         │
      │                  │                       │                      │                   │
      │                  │ INSERT t_workflow_instance (creator='event_trigger')             │
      │                  │◄──────────────────────┤                      │                   │
      │                  ├──── instanceId ──────►│                      │                   │
      │                  │                       │ temporalClient.workflow.start('dslWorkflow', …)
      │                  │                       ├─────────────────────►│                   │
      │                  │                       │                      │ 写 temporal.executions
      │                  │                       │                      │ + 投递 WorkflowTask│
      │                  │                       │◄────── runId ────────┤                   │
      │                  │ UPDATE t_workflow_instance SET temporal_run_id│                   │
      │                  │◄──────────────────────┤                      │                   │
      │                  │                       │                      │ PollWorkflowTask  │
      │                  │                       │                      │◄──────────────────┤
      │                  │                       │                      ├──── task ────────►│
      │                  │                       │                      │                   │ 执行节点 + Activity
      │                  │◄──────── INSERT/UPDATE t_workflow_node_execution ─────────────────┤
      │                  │◄──────── UPDATE t_workflow_instance (status, ended_at) ──────────┤
      │                  │                       │                      │                   │
      │                  │ (失败时) UPDATE t_workflow_event_outbox       │                   │
      │                  │ SET processed=false, retry_count=n            │                   │
      │                  │◄──────────────────────┤                      │                   │
```

**涉及的 PG 表：**

| 表 | 操作 | 写入方 |
|----|------|--------|
| 业务表（如 `t_order`） | 任意 DML | 业务来源 |
| `t_workflow_event_outbox` | INSERT（触发器，业务事务内）→ UPDATE processed/retry_count（poller） | DB Trigger + outbox-poller |
| `t_workflow`、`t_workflow_version` | SELECT 已发布且启用的最新版本 | outbox-poller |
| `t_workflow_instance` | INSERT (PENDING, creator='event_trigger') → UPDATE temporal_run_id → UPDATE status | outbox-poller 创建；Worker `reporting.ts` 在执行中改 |
| `t_workflow_node_execution` | INSERT/UPDATE 每个节点 | Worker `reporting.ts` |
| `temporal.executions` / `history_node` / `task_queues` / `tasks` / `timer_tasks` | 同场景 A | Temporal Server |

---

---

### 场景 C：Temporal Schedule API（定时任务触发，`cron_trigger`）

> ✅ **当前状态**：已实现。`WorkflowService.publish()` 解析 DSL 中的 `cron_trigger` 节点，调 `TemporalService.upsertCronSchedule()` 注册 Temporal Schedule；`toggleEnabled` / `remove` 联动 pause/unpause/delete schedule。Worker 新增 `cronTriggerWorkflow`，由 Schedule 触发后创建 `t_workflow_instance(creator='cron')` 并以子工作流方式执行 `dslWorkflow`。

入口：Workflow 中包含 `cron_trigger` 节点（带 `cronExpression` 配置），点击 Publish 时由 NestJS 把 cron 表达式注册到 Temporal Server 的 Schedule 子系统；之后由 **Server 自己**按时间触发，无需 NestJS / Worker 长跑组件。

```
┌────────┐   ┌──────────────────┐   ┌─────────────────┐   ┌──────────────────────────┐   ┌────────────┐
│Frontend│   │ NestJS           │   │ PostgreSQL      │   │ Temporal Server          │   │ Worker     │
│        │   │ WorkflowService  │   │ (业务 schema)    │   │ (Schedule + Workflow)    │   │ (long-poll)│
└───┬────┘   └────────┬─────────┘   └────────┬────────┘   └───────┬──────────────────┘   └─────┬──────┘
    │ POST /:id/publish│                     │                    │                             │
    ├────────────────►│                      │                    │                             │
    │                 │ 解析 DSL：找 cron_trigger 节点              │                             │
    │                 │ UPDATE t_workflow SET status='PUBLISHED'   │                             │
    │                 ├─────────────────────►│                    │                             │
    │                 │                      │                    │                             │
    │                 │ client.schedule.create({                  │                             │
    │                 │   scheduleId: `wf-${workflowId}-cron`,    │                             │
    │                 │   spec:{ cronExpressions:['0 2 * * *'] }, │                             │
    │                 │   action:{ type:'startWorkflow',          │                             │
    │                 │     workflowType:'cronTriggerWorkflow',   │                             │
    │                 │     args:[{ workflowId, versionId, dsl }] │                             │
    │                 │   }                                       │                             │
    │                 │ })                                        │                             │
    │                 ├──────────────────────────────────────────►│                             │
    │                 │                      │                    │ Server 写 temporal.schedules + │
    │                 │                      │                    │ 内部 timer 任务              │
    │                 │◄──── ok ──────────────────────────────────┤                             │
    │◄── 200 OK ──────┤                      │                    │                             │
    │                 │                      │                    │                             │
    ⋮                 ⋮                      ⋮         (等到 cron 时间到，Server 自动起 Workflow)  ⋮
    │                 │                      │                    │                             │
    │                 │                      │                    │ Server: 写 executions /     │
    │                 │                      │                    │ history_node + 投 WorkflowTask│
    │                 │                      │                    │                             │
    │                 │                      │                    │             PollWorkflowTask│
    │                 │                      │                    │◄────────────────────────────┤
    │                 │                      │                    ├──── task ──────────────────►│
    │                 │                      │                    │                             │ 1) cronTriggerWorkflow:
    │                 │                      │                    │                             │   Activity:createInstance
    │                 │                      │ INSERT t_workflow_instance (creator='cron')      │
    │                 │                      │◄──────────────────────────────────────────────── ┤
    │                 │                      │                    │                             │ 2) executeChildWorkflow
    │                 │                      │                    │                             │   ('dslWorkflow', { instanceId, dsl, … })
    │                 │                      │                    │                             │ ─→ 与场景 A 第 5 步起完全一致
    │                 │                      │ INSERT/UPDATE t_workflow_node_execution          │
    │                 │                      │◄──────────────────────────────────────────────── ┤
    │                 │                      │ UPDATE t_workflow_instance (status, ended_at)    │
    │                 │                      │◄──────────────────────────────────────────────── ┤
```

**编辑/取消调度时：**

| 用户操作 | NestJS 调用 | 说明 |
|----------|-------------|------|
| 修改 cron 表达式（发布新版本） | `client.schedule.getHandle(id).update(...)` | 原 schedule 改 spec，不会丢历史 |
| 暂停 / 启用 Workflow | `handle.pause()` / `handle.unpause()` | Server 跳过触发但保留 schedule |
| 删除 Workflow / 取消发布 | `handle.delete()` | 同时清掉 schedule，避免遗留 |

**涉及的 PG 表：**

| 表 | 操作 | 写入方 |
|----|------|--------|
| `t_workflow` | UPDATE status | NestJS（发布动作） |
| `t_workflow_instance` | INSERT (creator='cron') → UPDATE status | Worker `cronTriggerWorkflow` 内的 createInstance Activity |
| `t_workflow_node_execution` | INSERT/UPDATE | Worker `reporting.ts` |
| `temporal.schedules` / `schedules_by_namespace` | INSERT/UPDATE/DELETE schedule 记录 | Temporal Server（Frontend Service） |
| `temporal.executions` / `history_node` / `task_queues` | 同场景 A | Temporal Server（Schedule 触发后） |

**为什么不用 outbox-poller 模式 / AgendaService？**

| 方案 | 缺点 |
|------|------|
| Worker 内 cron-poller（仿 outbox-poller） | 多 Worker 实例需自己加分布式锁，且会跟 Temporal 状态机割裂 |
| 复用 AgendaService | Agenda 跑在 NestJS 进程，Workflow 调度跨两个时序源，故障定位复杂 |
| **Temporal Schedule API** ✅ | Server 原生支持，自动持久化到 `temporal.schedules`，UI 可见、支持 pause/backfill/skip |

---

### 场景 D：NestJS Public 端点（外部 webhook 触发，`webhook_trigger`）

> ✅ **当前状态**：已实现（HMAC 严格模式）。`POST /api/workflow/webhook/:path` 由 `WorkflowController.receiveWebhook` 接收，路由用 `@AllowUnauthorizedRequest()` 跳过登录态。请求必须带 `X-Workflow-Timestamp`（±5min）+ `X-Workflow-Signature: sha256=HEX(HMAC_SHA256(secret, "{ts}.{rawBody}"))`。验签失败 / 路径未匹配 / 时间戳过期分别返回 401 / 404 / 401。

入口：外部系统（如 GitHub / Stripe / 内部其他服务）按 Workflow 编辑器配置的 `webhookPath` 发 HTTP 请求 → NestJS 公开路由（无需登录态）匹配 published workflow 并复用 `executeWorkflow()` 流程。

```
┌──────────┐   ┌────────────────────┐   ┌──────────────────┐   ┌─────────────────┐   ┌────────────────┐   ┌────────────┐
│ 外部系统  │   │ NestJS             │   │ NestJS           │   │ PostgreSQL      │   │ Temporal Server│   │ Worker     │
│ (任何 HTTP│   │ WebhookController  │   │ WorkflowService  │   │ (业务 schema)    │   │ (gRPC :7233)   │   │ (long-poll)│
│  客户端)  │   │ @Public            │   │                  │   │                 │   │                │   │            │
└────┬─────┘   └──────────┬─────────┘   └────────┬─────────┘   └────────┬────────┘   └───────┬────────┘   └─────┬──────┘
     │ POST /api/workflow/webhook/:path          │                      │                    │                   │
     │ Headers: X-Signature, body=payload        │                      │                    │                   │
     ├──────────────────►│                       │                      │                    │                   │
     │                   │ (可选) 校验签名 / IP 白名单                    │                      │                    │
     │                   │                       │                      │                    │                   │
     │                   │ SELECT workflow JOIN version 找含 webhook_trigger 节点          │                   │
     │                   │ 且 config.webhookPath = :path 的最新发布版本                       │                   │
     │                   ├──────────────────────────────────────────────►│                    │                   │
     │                   │◄──────── 0 / 1 / N 个匹配 workflow ───────────┤                    │                   │
     │                   │                       │                      │                    │                   │
     │                   │ 对每个匹配的 workflow（通常仅 1 个）：           │                      │                    │
     │                   │ executeWorkflow(workflowId, { context: { headers, query, body } }, creator='webhook')   │
     │                   ├──────────────────────►│                      │                    │                   │
     │                   │                       │ INSERT t_workflow_instance (creator='webhook', context=请求体)   │
     │                   │                       ├─────────────────────►│                    │                   │
     │                   │                       │◄────── instanceId ───┤                    │                   │
     │                   │                       │                      │                    │                   │
     │                   │                       │ temporalService.startWorkflow('dslWorkflow', …)               │
     │                   │                       ├──────────────────────────────────────────►│                   │
     │                   │                       │                      │ 写 executions / history_node          │
     │                   │                       │                      │ + 投 WorkflowTask  │                   │
     │                   │                       │◄────── runId ────────────────────────────┤                   │
     │                   │                       │ UPDATE t_workflow_instance SET temporal_run_id│                │
     │                   │                       ├─────────────────────►│                    │                   │
     │                   │◄── { instanceId } ────┤                      │                    │ PollWorkflowTask  │
     │                   │                       │                      │                    │◄──────────────────┤
     │◄── 202 Accepted { instanceIds } ──────────┤                      │                    ├── task ──────────►│
     │ (异步：webhook 立刻返回，工作流后台跑)        │                      │                    │                   │ 执行 dslWorkflow
     │                   │                       │                      │ INSERT/UPDATE t_workflow_node_execution│
     │                   │                       │                      │◄──────────────────────────────────────┤
     │                   │                       │                      │ UPDATE t_workflow_instance (status)   │
     │                   │                       │                      │◄──────────────────────────────────────┤
```

**关键设计点：**

| 关注点 | 推荐做法 |
|--------|----------|
| 鉴权 | NestJS Controller 用 `@Public()` 跳过登录态，但每个 workflow 自带 `webhookSecret`，请求需带 `X-Signature: HMAC_SHA256(secret, body)` |
| 路径冲突 | `webhookPath` 必须在 published workflow 间唯一（Publish 时校验冲突，或用 `wf_${workflowId}` 做后缀） |
| 同步 vs 异步 | 默认异步：返回 `202 Accepted { instanceId }`，由前端轮询 `/instance/:id` 看结果。同步模式可选：等到 workflow 完成再返回（注意网关超时） |
| 多 workflow 订阅同一路径 | 与 outbox 一致：并行启动多个 instance，各自独立 |
| 重放保护 | 校验 `X-Timestamp` 在 ±5min 内 + payload 哈希做去重（可选，存 Redis 5min TTL） |

**涉及的 PG 表：**

| 表 | 操作 | 写入方 |
|----|------|--------|
| `t_workflow`、`t_workflow_version` | SELECT 含 `webhook_trigger` 节点的最新发布版本 | NestJS `WebhookController` |
| `t_workflow_instance` | INSERT (creator='webhook') → UPDATE temporal_run_id → UPDATE status | NestJS 创建；Worker `reporting.ts` 改状态 |
| `t_workflow_node_execution` | INSERT/UPDATE | Worker `reporting.ts` |
| `temporal.executions` / `history_node` / `task_queues` | 同场景 A | Temporal Server |

**为什么不让 Worker 自己开 HTTP 端口接收 webhook？**

- Worker 是无状态计算节点，不应面向公网；外部网络/网关/WAF 都暴露在 NestJS 那一层
- NestJS 已有完善的 Guard、Logger、Swagger、错误处理体系，复用更合适
- 校验签名、限流、IP 白名单等都属于"边界关注点"，归属 Web 层

---

### 一句话对比（四种触发场景）

| | 场景 A：NestJS startWorkflow | 场景 B：Outbox Poller | 场景 C：Temporal Schedule | 场景 D：NestJS Webhook |
|--|------------------------------|------------------------|----------------------------|--------------------------|
| **当前状态** | ✅ 已实现 | ✅ 已实现 | ✅ 已实现 | ✅ 已实现（HMAC 严格） |
| 触发点 | 用户/定时业务调用 NestJS | 业务表 DML → 触发器 → outbox | Server 内置 cron 调度 | 外部 HTTP POST |
| 入口节点 | （任何，无 trigger 节点） | `event_trigger` | `cron_trigger` | `webhook_trigger` |
| 谁创建 `t_workflow_instance` | NestJS `WorkflowService` | Worker `outbox-poller` | Worker（cronTriggerWorkflow 内 Activity） | NestJS `WorkflowService` |
| 谁调用 `workflow.start()` | NestJS `TemporalService` | Worker `outbox-poller` | **Temporal Server 自己**（Schedule 系统） | NestJS `TemporalService` |
| 是否走 Temporal Server | ✅ | ✅ | ✅（且 schedule 本身也由 Server 持久化） | ✅ |
| `creator` 字段 | 登录用户 | `'event_trigger'` | `'cron'` | `'webhook'` |
| 触发延迟 | <100ms | ≤ 轮询间隔（默认 10s） | 由 Server 内部 timer 控制（秒级精度） | <100ms |
| 失败重试 | Workflow/Activity 自带重试 | outbox 行重试 ≤ `OUTBOX_MAX_RETRIES` | Schedule 不重试触发；Workflow 内部正常重试 | NestJS 直接返回 4xx/5xx；webhook 客户端自己决定是否重投 |
| 多实例并发安全 | NestJS 多 Pod 各自 OK | `FOR UPDATE SKIP LOCKED` 防重复消费 | Server 单一 schedule 实例，天然不重复 | NestJS 多 Pod 各自 OK |

**关键点：** 不论从哪个入口触发，只要调用了 `temporalClient.workflow.start()`（或 Schedule 间接调用），就一定要经过 Temporal Server 的调度（写 history → 入 task queue → Worker long-poll 拉取），Server 是**唯一的状态权威**。Worker 内部直接读写业务表只发生在 Activity 执行阶段（`reporting.ts` 写 `t_workflow_node_execution` / `t_workflow_instance`，outbox-poller 写 outbox 与 instance），这是绕不过 Server 的——但写 Workflow 历史和调度本身仍由 Server 完成。

## 环境变量

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `TEMPORAL_ADDRESS` | `localhost:7233` | Temporal Server gRPC 地址 |
| `TEMPORAL_NAMESPACE` | `default` | Temporal 命名空间 |
| `TEMPORAL_TASK_QUEUE` | `workflow-execution` | 任务队列名称 |
| `DATABASE_URL` | (见 .env) | Worker 回写状态用的数据库连接 |

## 常用命令

```bash
npm run up           # 启动 Server + UI
npm run down         # 停止 Server + UI
npm run logs         # 查看 Server 日志
npm run worker:dev   # 开发模式启动 Worker
npm run worker:build # 编译 Worker
npm run worker:start # 生产模式启动 Worker
```

## Web UI

启动后访问 http://localhost:8080 可查看：
- 运行中的 Workflow 列表
- Workflow 执行历史
- 单个 Workflow 的事件时间线
- Worker 连接状态

---

## Event Trigger（数据库事件触发 Workflow）

通过 Transactional Outbox 模式，当业务表发生变更时自动触发对应的 Workflow。

### 原理

```
┌─────────────┐    INSERT/UPDATE/DELETE    ┌──────────────────────────┐
│  任何来源    │ ────────────────────────→  │  PostgreSQL               │
│  (NestJS /  │                            │                           │
│   外部系统)  │                            │  AFTER TRIGGER            │
└─────────────┘                            │  → INSERT INTO outbox     │
                                           │   (同一事务，保证不丢)     │
                                           └─────────────┬─────────────┘
                                                         │
                                                   outbox 表中有新行
                                                         │
                                                         ▼
                                           ┌──────────────────────────┐
                                           │  Worker                   │
                                           │  outbox-poller.ts         │
                                           │                           │
                                           │  每 10s 扫描一次：        │
                                           │  1. SELECT 未处理行        │
                                           │  2. 匹配 event_trigger    │
                                           │  3. 启动 Temporal workflow │
                                           │  4. 标记行为已处理         │
                                           └──────────────────────────┘
```

### 为什么选轮询而不是 PG LISTEN/NOTIFY

| | PG NOTIFY | Outbox 轮询 |
|--|--|--|
| Worker 离线期间 | ❌ 事件丢失 | ✅ 事件留在表中，恢复后处理 |
| 可靠性 | best-effort | **at-least-once（事务保证）** |
| 复杂度 | 需要长连接 + 去重 | 只需定时 SELECT |
| 延迟 | <100ms | ≤ 轮询间隔（默认 10s） |

### 支持的操作类型

| 数据库操作 | eventName 示例 |
|-----------|---------------|
| 新增记录 | `t_order.insert` |
| 修改记录 | `t_order.update` |
| 删除记录 | `t_order.delete` |

### eventName 通配符匹配

在 Workflow DSL 的 `event_trigger` 节点中配置 `eventName`：

| eventName 配置 | 触发条件 |
|---|---|
| `t_order.insert` | 仅该表新增时触发 |
| `t_order.update` | 仅该表修改时触发 |
| `t_order.delete` | 仅该表删除时触发 |
| `t_order.*` | 该表任何变更都触发 |
| `*.insert` | 所有表的新增都触发 |

### 使用步骤

**Step 1：创建 outbox 表 + 通用触发器函数（只需执行一次）**

Outbox 表 `t_workflow_event_outbox` 由 Prisma 模型 `WorkflowEventOutbox` 维护，跟随 `prisma migrate` 流程自动建表；通用触发器函数 `notify_workflow_event()` 和未处理事件的部分索引 `idx_outbox_unprocessed` 由 seed 脚本创建：

```bash
# server 目录
cd server
yarn seed
```

幂等可重复执行。详见 `server/seed/workflow-event-trigger.ts`。

**Step 2：给需要监听的表挂触发器**

```sql
-- 只监听 INSERT
CREATE TRIGGER trg_order_workflow
  AFTER INSERT ON t_order
  FOR EACH ROW EXECUTE FUNCTION notify_workflow_event();

-- 监听所有变更（INSERT + UPDATE + DELETE）
CREATE TRIGGER trg_order_workflow
  AFTER INSERT OR UPDATE OR DELETE ON t_order
  FOR EACH ROW EXECUTE FUNCTION notify_workflow_event();

-- 只在特定条件下触发（例如状态变为 APPROVED）
CREATE OR REPLACE FUNCTION notify_order_approved() RETURNS trigger AS $$
BEGIN
  IF NEW.status = 'APPROVED' AND (OLD.status IS NULL OR OLD.status != 'APPROVED') THEN
    INSERT INTO t_workflow_event_outbox (event_name, payload)
    VALUES ('t_order.status_approved', row_to_json(NEW)::jsonb);
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_order_approved
  AFTER UPDATE ON t_order
  FOR EACH ROW EXECUTE FUNCTION notify_order_approved();
```

**Step 3：在 Workflow 编辑器中配置 event_trigger 节点**

```json
{
  "type": "event_trigger",
  "config": {
    "eventName": "t_order.insert",
    "filterExpression": "data.amount > 100"
  }
}
```

- `eventName`：匹配规则（支持通配符）
- `filterExpression`（可选）：JS 表达式，对 payload 的过滤条件

**Step 4：发布 Workflow**

发布后 Worker 的 outbox-poller 会自动匹配新事件到该 Workflow。

### 配置

| 环境变量 | 默认值 | 说明 |
|---------|--------|------|
| `EVENT_POLL_INTERVAL_MS` | `10000` | 轮询间隔（毫秒） |

### 注意事项

- Outbox 表会持续增长，建议定期清理已处理的旧行（如保留 7 天）
- 同一个 eventName 可以匹配多个 Workflow，它们会并行启动
- `filterExpression` 在 Worker 侧求值，不满足条件的事件仍会写入 outbox 但不触发 workflow
