@gadmin2n/schematics 0.0.135 → 0.0.137
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/canvas.prisma +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +14 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/User.ts +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +13 -92
- package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +105 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/ensure-database.js +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +7 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/gen-jwt.ts +85 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js +9 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +43 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.dto.ts +27 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.spec.ts +430 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.ts +203 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +36 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.module.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +54 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +245 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.spec.ts +366 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.ts +110 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +19 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +15 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +41 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +37 -573
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/ecosystem.dev.config.cjs +12 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +3 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +39 -26
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +3 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +23 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +66 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +16 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +21 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +30 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ExportModal.tsx +84 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ImportModal.tsx +166 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/hooks/useCanvasHistory.ts +52 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +6 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +18 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +225 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/utilities/lazy-with-reload.ts +31 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +0 -220
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +0 -132
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +0 -152
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/config/development-sql.yaml +0 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/docker-compose.yml +0 -25
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/package.json +0 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +0 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.env +0 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +0 -79
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +0 -270
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/SANDBOX.md +0 -122
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package-lock.json +0 -4285
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package.json +0 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/code-execute.ts +0 -51
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-execute.ts +0 -85
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-query.ts +0 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/http-request.ts +0 -54
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +0 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +0 -85
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/send-notification.ts +0 -47
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/config.ts +0 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/condition.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/context.ts +0 -58
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/graph.ts +0 -184
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/helpers.ts +0 -133
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +0 -96
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/types.ts +0 -77
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +0 -109
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +0 -382
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/code-execute.test.ts +0 -44
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/http-request.test.ts +0 -87
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +0 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/helpers.test.ts +0 -225
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/node-type-consistency.test.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +0 -205
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +0 -499
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/tsconfig.json +0 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/vitest.config.ts +0 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/yarn.lock +0 -1905
|
@@ -1,587 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# temporal/
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Phase 3 起本目录已废弃** — gadmin-test 不再内嵌 Temporal 服务。所有 workflow 执行
|
|
4
|
+
走共享部署 `common/gadmin2/temporal-shared`。
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
设计文档:`docs/superpowers/specs/2026-07-06-shared-temporal-worker-design.md`
|
|
7
|
+
Phase 3 说明:`docs/superpowers/specs/2026-07-14-shared-temporal-phase3-design.md`
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
│ Temporal Server │
|
|
10
|
-
│ (调度中心,不执行业务代码) │
|
|
11
|
-
│ │
|
|
12
|
-
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────┐ │
|
|
13
|
-
│ │ 状态存储 │ │ 任务队列管理 │ │ 历史/可见性引擎 │ │
|
|
14
|
-
│ │ (PG) │ │ (分发任务) │ │ (查询/搜索) │ │
|
|
15
|
-
│ └──────────┘ └──────┬───────┘ └────────────────────┘ │
|
|
16
|
-
│ │ │
|
|
17
|
-
└────────────────────────┼─────────────────────────────────────┘
|
|
18
|
-
│ gRPC (任务分发 + 状态回报)
|
|
19
|
-
▼
|
|
20
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
21
|
-
│ Temporal Worker │
|
|
22
|
-
│ (你的代码,执行业务逻辑) │
|
|
23
|
-
│ │
|
|
24
|
-
│ ┌─────────────────┐ ┌──────────────────────────┐ │
|
|
25
|
-
│ │ Workflow 代码 │ │ Activity 代码 │ │
|
|
26
|
-
│ │ (DSL 解释器) │ │ (HTTP调用/DB/通知/...) │ │
|
|
27
|
-
│ └─────────────────┘ └──────────────────────────┘ │
|
|
28
|
-
└─────────────────────────────────────────────────────────────┘
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Server vs Worker
|
|
32
|
-
|
|
33
|
-
| | Server | Worker |
|
|
34
|
-
|--|--------|--------|
|
|
35
|
-
| 谁提供的 | Temporal 官方 Docker 镜像 | **你写的代码** |
|
|
36
|
-
| 执行业务逻辑? | ❌ 完全不碰 | ✅ 全部在这里 |
|
|
37
|
-
| 有状态? | ✅ 持久化到 PG | ❌ 无状态,可随时重启/扩容 |
|
|
38
|
-
| 挂了怎么办 | 所有 workflow 暂停 | Server 保留进度,Worker 恢复后继续 |
|
|
39
|
-
| 能横向扩展? | 可以但复杂 | **随便加**,同一 task queue 多个 Worker |
|
|
40
|
-
|
|
41
|
-
**一句话:Server 是"大脑"(调度+记忆),Worker 是"手脚"(干活)。**
|
|
42
|
-
|
|
43
|
-
## 目录结构
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
temporal/
|
|
47
|
-
├── package.json # 便捷脚本
|
|
48
|
-
├── docker-compose.yml # Temporal Server + Web UI
|
|
49
|
-
├── config/
|
|
50
|
-
│ └── development-sql.yaml # Server 动态配置
|
|
51
|
-
└── worker/ # DSL 解释器 Worker
|
|
52
|
-
├── package.json
|
|
53
|
-
├── tsconfig.json
|
|
54
|
-
├── .env # Worker 环境变量
|
|
55
|
-
└── src/
|
|
56
|
-
├── index.ts # Worker 启动入口
|
|
57
|
-
├── config.ts # 配置读取
|
|
58
|
-
├── outbox-poller.ts # 事件轮询器(扫描 outbox 表触发 workflow)
|
|
59
|
-
├── workflows/
|
|
60
|
-
│ └── dsl-workflow.ts # 核心:DSL 动态解释器
|
|
61
|
-
├── activities/
|
|
62
|
-
│ ├── index.ts
|
|
63
|
-
│ ├── http-request.ts # HTTP 调用
|
|
64
|
-
│ ├── db-query.ts # 数据库查询
|
|
65
|
-
│ ├── db-execute.ts # 数据库写入
|
|
66
|
-
│ ├── send-notification.ts # 多渠道通知
|
|
67
|
-
│ ├── code-execute.ts # 沙箱 JS 执行
|
|
68
|
-
│ └── reporting.ts # 状态回写到业务 DB
|
|
69
|
-
└── dsl/
|
|
70
|
-
├── types.ts # DSL 类型定义
|
|
71
|
-
├── graph.ts # DAG 解析 + 拓扑排序
|
|
72
|
-
├── context.ts # 变量/上下文管理
|
|
73
|
-
└── condition.ts # 条件表达式求值
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## 快速启动
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
cd temporal
|
|
80
|
-
|
|
81
|
-
# 1. 启动 Temporal Server(复用项目现有 PostgreSQL)
|
|
82
|
-
npm run up
|
|
83
|
-
|
|
84
|
-
# 2. 安装 Worker 依赖
|
|
85
|
-
npm run worker:install
|
|
86
|
-
|
|
87
|
-
# 3. 启动 Worker(开发模式)
|
|
88
|
-
npm run worker:dev
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## 工作流程
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
1. 用户在前端点击 "Run" 按钮
|
|
95
|
-
│
|
|
96
|
-
▼
|
|
97
|
-
2. NestJS 调用 temporalService.startWorkflow()
|
|
98
|
-
│
|
|
99
|
-
▼
|
|
100
|
-
3. Temporal Server 收到请求,创建记录,放入 task queue
|
|
101
|
-
│
|
|
102
|
-
▼
|
|
103
|
-
4. Worker 长轮询 Server,收到新任务
|
|
104
|
-
│
|
|
105
|
-
▼
|
|
106
|
-
5. Worker 执行 dslWorkflow():
|
|
107
|
-
- 解析 DSL JSON → DAG
|
|
108
|
-
- 拓扑排序确定执行顺序
|
|
109
|
-
- 逐节点执行对应 Activity
|
|
110
|
-
- 条件节点做分支选择
|
|
111
|
-
- 并行节点用 Promise.all
|
|
112
|
-
- 审批节点等待 Signal
|
|
113
|
-
│
|
|
114
|
-
▼
|
|
115
|
-
6. 每个节点执行结果回写到 t_workflow_node_execution
|
|
116
|
-
│
|
|
117
|
-
▼
|
|
118
|
-
7. Workflow 完成/失败 → 更新 t_workflow_instance 状态
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## 数据库表分工
|
|
122
|
-
|
|
123
|
-
Temporal 体系下有 **两类 PG 表**,分布在不同 schema,互不干扰:
|
|
124
|
-
|
|
125
|
-
| 类别 | 所在 schema | 谁来写 | 典型表 | 用途 |
|
|
126
|
-
|------|------------|--------|--------|------|
|
|
127
|
-
| **Temporal 系统表** | `temporal` (Server 启动时自动建表) | 仅 Temporal Server | `executions`、`history_node`、`history_tree`、`current_executions`、`task_queues`、`tasks`、`timer_tasks`、`shard`、`namespaces` | Workflow 状态机、事件历史、任务队列、定时器、分片协调 |
|
|
128
|
-
| **业务表** | `public` (Prisma 管理) | NestJS / Worker Activity | `t_workflow`、`t_workflow_version`、`t_workflow_instance`、`t_workflow_node_execution`、`t_workflow_event_outbox` | 业务可见的工作流定义、实例、节点执行记录、事件触发 outbox |
|
|
129
|
-
|
|
130
|
-
> Temporal Server **从不读写**业务表;业务代码(NestJS / Worker Activity)**从不读写** Temporal 系统表。两者通过 `temporal_run_id` 字段(保存在 `t_workflow_instance` 上)建立关联。
|
|
131
|
-
|
|
132
|
-
## 两种触发场景的时序图
|
|
133
|
-
|
|
134
|
-
### 场景 A:NestJS 主动调用 `temporalService.startWorkflow()`
|
|
135
|
-
|
|
136
|
-
入口:用户在前端点 "Run",或 Agenda 定时任务 / 其他业务代码主动调用 `WorkflowService.executeWorkflow()`。
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
┌────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌────────────────┐ ┌────────────┐
|
|
140
|
-
│Frontend│ │ NestJS │ │ PostgreSQL │ │ Temporal Server│ │ Worker │
|
|
141
|
-
│ │ │ WorkflowService │ │ (业务 schema) │ │ (gRPC :7233) │ │ (long-poll)│
|
|
142
|
-
└───┬────┘ └────────┬─────────┘ └────────┬────────┘ └───────┬────────┘ └─────┬──────┘
|
|
143
|
-
│ POST /run │ │ │ │
|
|
144
|
-
├────────────────►│ │ │ │
|
|
145
|
-
│ │ SELECT t_workflow + t_workflow_version │ │
|
|
146
|
-
│ ├─────────────────────►│ │ │
|
|
147
|
-
│ │◄─────────────────────┤ 校验 PUBLISHED/enabled │
|
|
148
|
-
│ │ │ │ │
|
|
149
|
-
│ │ INSERT t_workflow_instance (status=PENDING)│ │
|
|
150
|
-
│ ├─────────────────────►│ │ │
|
|
151
|
-
│ │◄──── instanceId ─────┤ │ │
|
|
152
|
-
│ │ │ │ │
|
|
153
|
-
│ │ temporalService.startWorkflow('dslWorkflow', …) │
|
|
154
|
-
│ ├──────────────────────────────────────────►│ │
|
|
155
|
-
│ │ │ 写 temporal.executions / history_node │
|
|
156
|
-
│ │ │ + 投递 WorkflowTask 到 task_queues │
|
|
157
|
-
│ │ │◄───────────────────┤ │
|
|
158
|
-
│ │◄──── temporalRunId ──────────────────────┤ │
|
|
159
|
-
│ │ │ │ │
|
|
160
|
-
│ │ UPDATE t_workflow_instance SET temporal_run_id│ │
|
|
161
|
-
│ ├─────────────────────►│ │ │
|
|
162
|
-
│ │ │ │ PollWorkflowTask │
|
|
163
|
-
│ │ │ │◄──────────────────┤
|
|
164
|
-
│ │ │ ├──── task ────────►│
|
|
165
|
-
│ │ │ │ │ 执行 dslWorkflow()
|
|
166
|
-
│ │ │ │ │ ──┐
|
|
167
|
-
│ │ │ │ ScheduleActivity │ <─┘
|
|
168
|
-
│ │ │ │◄──────────────────┤
|
|
169
|
-
│ │ │ ├── activity task ─►│
|
|
170
|
-
│ │ │ │ │ Activity 内调用
|
|
171
|
-
│ │ │ │ │ reporting.ts:
|
|
172
|
-
│ │ │◄────── INSERT/UPDATE t_workflow_node_execution ─┤
|
|
173
|
-
│ │ │◄────── UPDATE t_workflow_instance (RUNNING/DONE)┤
|
|
174
|
-
│ │ │ │ CompleteActivity │
|
|
175
|
-
│ │ │ │◄──────────────────┤
|
|
176
|
-
│ │ │ │ │
|
|
177
|
-
│ │ │ │ … 直到 Workflow 完成 …
|
|
178
|
-
│◄────── 200 OK { instanceId, temporalRunId } ────────────────┤ │
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**涉及的 PG 表:**
|
|
182
|
-
|
|
183
|
-
| 表 | 操作 | 写入方 |
|
|
184
|
-
|----|------|--------|
|
|
185
|
-
| `t_workflow`、`t_workflow_version` | SELECT | NestJS(取 DSL + 校验状态) |
|
|
186
|
-
| `t_workflow_instance` | INSERT (PENDING) → UPDATE (temporal_run_id) → UPDATE (status) | NestJS 创建;Worker `reporting.ts` 在执行中改 |
|
|
187
|
-
| `t_workflow_node_execution` | INSERT/UPDATE 每个节点 | Worker `reporting.ts`(Activity 内调用) |
|
|
188
|
-
| `temporal.executions` / `current_executions` | INSERT (新 WorkflowExecution) → UPDATE 状态 | Temporal Server(Frontend / History Service) |
|
|
189
|
-
| `temporal.history_node` / `history_tree` | INSERT 事件(Started / ActivityScheduled / Completed …) | Temporal Server |
|
|
190
|
-
| `temporal.task_queues` / `tasks` | INSERT/DELETE Workflow & Activity Task | Temporal Server(Matching Service) |
|
|
191
|
-
| `temporal.timer_tasks` | 节点超时、Workflow 总超时、Sleep 节点 | Temporal Server |
|
|
9
|
+
保留本目录(而非删除整个 `temporal/`)是为了让代码库树一眼看得到"这里以前有内嵌
|
|
10
|
+
temporal,现在已迁走",避免新同事/AI 找不到线索反复重建。
|
|
192
11
|
|
|
193
12
|
---
|
|
194
13
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
入口:业务表(如 `t_order`)发生 INSERT/UPDATE/DELETE → AFTER 触发器写一行到 `t_workflow_event_outbox` → Worker 进程内 outbox-poller 扫到。
|
|
198
|
-
|
|
199
|
-
```
|
|
200
|
-
┌────────────┐ ┌────────────────┐ ┌─────────────────────┐ ┌────────────────┐ ┌──────────────┐
|
|
201
|
-
│ 业务来源 │ │ PostgreSQL │ │ Worker │ │ Temporal Server│ │ Worker │
|
|
202
|
-
│ (任意 INSERT│ │ (业务 schema) │ │ outbox-poller.ts │ │ (gRPC :7233) │ │ dslWorkflow │
|
|
203
|
-
│ /UPDATE) │ │ │ │ (Worker 进程内) │ │ │ │ (同一进程) │
|
|
204
|
-
└─────┬──────┘ └───────┬────────┘ └──────────┬──────────┘ └───────┬────────┘ └──────┬───────┘
|
|
205
|
-
│ INSERT/UPDATE t_order │ │ │
|
|
206
|
-
├─────────────────►│ │ │ │
|
|
207
|
-
│ │ AFTER TRIGGER notify_workflow_event() │ │
|
|
208
|
-
│ │ INSERT t_workflow_event_outbox (同事务) │ │
|
|
209
|
-
│ │ │ │ │
|
|
210
|
-
│ │ ◄── 每 10s ───────── │ UPDATE outbox │ │
|
|
211
|
-
│ │ │ SET processed=true │ │
|
|
212
|
-
│ │ │ ... FOR UPDATE SKIP LOCKED │
|
|
213
|
-
│ ├──── 未处理事件批 ────►│ │ │
|
|
214
|
-
│ │ │ │ │
|
|
215
|
-
│ │ SELECT t_workflow JOIN t_workflow_version │ │
|
|
216
|
-
│ │ WHERE status='PUBLISHED' AND is_enabled │ │
|
|
217
|
-
│ ├──────────────────────►│ matchEventName + filterExpression │
|
|
218
|
-
│ │ │ │ │
|
|
219
|
-
│ │ INSERT t_workflow_instance (creator='event_trigger') │
|
|
220
|
-
│ │◄──────────────────────┤ │ │
|
|
221
|
-
│ ├──── instanceId ──────►│ │ │
|
|
222
|
-
│ │ │ temporalClient.workflow.start('dslWorkflow', …)
|
|
223
|
-
│ │ ├─────────────────────►│ │
|
|
224
|
-
│ │ │ │ 写 temporal.executions
|
|
225
|
-
│ │ │ │ + 投递 WorkflowTask│
|
|
226
|
-
│ │ │◄────── runId ────────┤ │
|
|
227
|
-
│ │ UPDATE t_workflow_instance SET temporal_run_id│ │
|
|
228
|
-
│ │◄──────────────────────┤ │ │
|
|
229
|
-
│ │ │ │ PollWorkflowTask │
|
|
230
|
-
│ │ │ │◄──────────────────┤
|
|
231
|
-
│ │ │ ├──── task ────────►│
|
|
232
|
-
│ │ │ │ │ 执行节点 + Activity
|
|
233
|
-
│ │◄──────── INSERT/UPDATE t_workflow_node_execution ─────────────────┤
|
|
234
|
-
│ │◄──────── UPDATE t_workflow_instance (status, ended_at) ──────────┤
|
|
235
|
-
│ │ │ │ │
|
|
236
|
-
│ │ (失败时) UPDATE t_workflow_event_outbox │ │
|
|
237
|
-
│ │ SET processed=false, retry_count=n │ │
|
|
238
|
-
│ │◄──────────────────────┤ │ │
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
**涉及的 PG 表:**
|
|
242
|
-
|
|
243
|
-
| 表 | 操作 | 写入方 |
|
|
244
|
-
|----|------|--------|
|
|
245
|
-
| 业务表(如 `t_order`) | 任意 DML | 业务来源 |
|
|
246
|
-
| `t_workflow_event_outbox` | INSERT(触发器,业务事务内)→ UPDATE processed/retry_count(poller) | DB Trigger + outbox-poller |
|
|
247
|
-
| `t_workflow`、`t_workflow_version` | SELECT 已发布且启用的最新版本 | outbox-poller |
|
|
248
|
-
| `t_workflow_instance` | INSERT (PENDING, creator='event_trigger') → UPDATE temporal_run_id → UPDATE status | outbox-poller 创建;Worker `reporting.ts` 在执行中改 |
|
|
249
|
-
| `t_workflow_node_execution` | INSERT/UPDATE 每个节点 | Worker `reporting.ts` |
|
|
250
|
-
| `temporal.executions` / `history_node` / `task_queues` / `tasks` / `timer_tasks` | 同场景 A | Temporal Server |
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
### 场景 C:Temporal Schedule API(定时任务触发,`cron_trigger`)
|
|
257
|
-
|
|
258
|
-
> ✅ **当前状态**:已实现。`WorkflowService.publish()` 解析 DSL 中的 `cron_trigger` 节点,调 `TemporalService.upsertCronSchedule()` 注册 Temporal Schedule;`toggleEnabled` / `remove` 联动 pause/unpause/delete schedule。Worker 新增 `cronTriggerWorkflow`,由 Schedule 触发后创建 `t_workflow_instance(creator='cron')` 并以子工作流方式执行 `dslWorkflow`。
|
|
259
|
-
|
|
260
|
-
入口:Workflow 中包含 `cron_trigger` 节点(带 `cronExpression` 配置),点击 Publish 时由 NestJS 把 cron 表达式注册到 Temporal Server 的 Schedule 子系统;之后由 **Server 自己**按时间触发,无需 NestJS / Worker 长跑组件。
|
|
261
|
-
|
|
262
|
-
```
|
|
263
|
-
┌────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌──────────────────────────┐ ┌────────────┐
|
|
264
|
-
│Frontend│ │ NestJS │ │ PostgreSQL │ │ Temporal Server │ │ Worker │
|
|
265
|
-
│ │ │ WorkflowService │ │ (业务 schema) │ │ (Schedule + Workflow) │ │ (long-poll)│
|
|
266
|
-
└───┬────┘ └────────┬─────────┘ └────────┬────────┘ └───────┬──────────────────┘ └─────┬──────┘
|
|
267
|
-
│ POST /:id/publish│ │ │ │
|
|
268
|
-
├────────────────►│ │ │ │
|
|
269
|
-
│ │ 解析 DSL:找 cron_trigger 节点 │ │
|
|
270
|
-
│ │ UPDATE t_workflow SET status='PUBLISHED' │ │
|
|
271
|
-
│ ├─────────────────────►│ │ │
|
|
272
|
-
│ │ │ │ │
|
|
273
|
-
│ │ client.schedule.create({ │ │
|
|
274
|
-
│ │ scheduleId: `wf-${workflowId}-cron`, │ │
|
|
275
|
-
│ │ spec:{ cronExpressions:['0 2 * * *'] }, │ │
|
|
276
|
-
│ │ action:{ type:'startWorkflow', │ │
|
|
277
|
-
│ │ workflowType:'cronTriggerWorkflow', │ │
|
|
278
|
-
│ │ args:[{ workflowId, versionId, dsl }] │ │
|
|
279
|
-
│ │ } │ │
|
|
280
|
-
│ │ }) │ │
|
|
281
|
-
│ ├──────────────────────────────────────────►│ │
|
|
282
|
-
│ │ │ │ Server 写 temporal.schedules + │
|
|
283
|
-
│ │ │ │ 内部 timer 任务 │
|
|
284
|
-
│ │◄──── ok ──────────────────────────────────┤ │
|
|
285
|
-
│◄── 200 OK ──────┤ │ │ │
|
|
286
|
-
│ │ │ │ │
|
|
287
|
-
⋮ ⋮ ⋮ (等到 cron 时间到,Server 自动起 Workflow) ⋮
|
|
288
|
-
│ │ │ │ │
|
|
289
|
-
│ │ │ │ Server: 写 executions / │
|
|
290
|
-
│ │ │ │ history_node + 投 WorkflowTask│
|
|
291
|
-
│ │ │ │ │
|
|
292
|
-
│ │ │ │ PollWorkflowTask│
|
|
293
|
-
│ │ │ │◄────────────────────────────┤
|
|
294
|
-
│ │ │ ├──── task ──────────────────►│
|
|
295
|
-
│ │ │ │ │ 1) cronTriggerWorkflow:
|
|
296
|
-
│ │ │ │ │ Activity:createInstance
|
|
297
|
-
│ │ │ INSERT t_workflow_instance (creator='cron') │
|
|
298
|
-
│ │ │◄──────────────────────────────────────────────── ┤
|
|
299
|
-
│ │ │ │ │ 2) executeChildWorkflow
|
|
300
|
-
│ │ │ │ │ ('dslWorkflow', { instanceId, dsl, … })
|
|
301
|
-
│ │ │ │ │ ─→ 与场景 A 第 5 步起完全一致
|
|
302
|
-
│ │ │ INSERT/UPDATE t_workflow_node_execution │
|
|
303
|
-
│ │ │◄──────────────────────────────────────────────── ┤
|
|
304
|
-
│ │ │ UPDATE t_workflow_instance (status, ended_at) │
|
|
305
|
-
│ │ │◄──────────────────────────────────────────────── ┤
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
**编辑/取消调度时:**
|
|
309
|
-
|
|
310
|
-
| 用户操作 | NestJS 调用 | 说明 |
|
|
311
|
-
|----------|-------------|------|
|
|
312
|
-
| 修改 cron 表达式(发布新版本) | `client.schedule.getHandle(id).update(...)` | 原 schedule 改 spec,不会丢历史 |
|
|
313
|
-
| 暂停 / 启用 Workflow | `handle.pause()` / `handle.unpause()` | Server 跳过触发但保留 schedule |
|
|
314
|
-
| 删除 Workflow / 取消发布 | `handle.delete()` | 同时清掉 schedule,避免遗留 |
|
|
315
|
-
|
|
316
|
-
**涉及的 PG 表:**
|
|
317
|
-
|
|
318
|
-
| 表 | 操作 | 写入方 |
|
|
319
|
-
|----|------|--------|
|
|
320
|
-
| `t_workflow` | UPDATE status | NestJS(发布动作) |
|
|
321
|
-
| `t_workflow_instance` | INSERT (creator='cron') → UPDATE status | Worker `cronTriggerWorkflow` 内的 createInstance Activity |
|
|
322
|
-
| `t_workflow_node_execution` | INSERT/UPDATE | Worker `reporting.ts` |
|
|
323
|
-
| `temporal.schedules` / `schedules_by_namespace` | INSERT/UPDATE/DELETE schedule 记录 | Temporal Server(Frontend Service) |
|
|
324
|
-
| `temporal.executions` / `history_node` / `task_queues` | 同场景 A | Temporal Server(Schedule 触发后) |
|
|
325
|
-
|
|
326
|
-
**为什么不用 outbox-poller 模式 / AgendaService?**
|
|
327
|
-
|
|
328
|
-
| 方案 | 缺点 |
|
|
329
|
-
|------|------|
|
|
330
|
-
| Worker 内 cron-poller(仿 outbox-poller) | 多 Worker 实例需自己加分布式锁,且会跟 Temporal 状态机割裂 |
|
|
331
|
-
| 复用 AgendaService | Agenda 跑在 NestJS 进程,Workflow 调度跨两个时序源,故障定位复杂 |
|
|
332
|
-
| **Temporal Schedule API** ✅ | Server 原生支持,自动持久化到 `temporal.schedules`,UI 可见、支持 pause/backfill/skip |
|
|
333
|
-
|
|
334
|
-
---
|
|
335
|
-
|
|
336
|
-
### 场景 D:NestJS Public 端点(外部 webhook 触发,`webhook_trigger`)
|
|
337
|
-
|
|
338
|
-
> ✅ **当前状态**:已实现(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。
|
|
339
|
-
|
|
340
|
-
入口:外部系统(如 GitHub / Stripe / 内部其他服务)按 Workflow 编辑器配置的 `webhookPath` 发 HTTP 请求 → NestJS 公开路由(无需登录态)匹配 published workflow 并复用 `executeWorkflow()` 流程。
|
|
341
|
-
|
|
342
|
-
```
|
|
343
|
-
┌──────────┐ ┌────────────────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌────────────────┐ ┌────────────┐
|
|
344
|
-
│ 外部系统 │ │ NestJS │ │ NestJS │ │ PostgreSQL │ │ Temporal Server│ │ Worker │
|
|
345
|
-
│ (任何 HTTP│ │ WebhookController │ │ WorkflowService │ │ (业务 schema) │ │ (gRPC :7233) │ │ (long-poll)│
|
|
346
|
-
│ 客户端) │ │ @Public │ │ │ │ │ │ │ │ │
|
|
347
|
-
└────┬─────┘ └──────────┬─────────┘ └────────┬─────────┘ └────────┬────────┘ └───────┬────────┘ └─────┬──────┘
|
|
348
|
-
│ POST /api/workflow/webhook/:path │ │ │ │
|
|
349
|
-
│ Headers: X-Signature, body=payload │ │ │ │
|
|
350
|
-
├──────────────────►│ │ │ │ │
|
|
351
|
-
│ │ (可选) 校验签名 / IP 白名单 │ │ │
|
|
352
|
-
│ │ │ │ │ │
|
|
353
|
-
│ │ SELECT workflow JOIN version 找含 webhook_trigger 节点 │ │
|
|
354
|
-
│ │ 且 config.webhookPath = :path 的最新发布版本 │ │
|
|
355
|
-
│ ├──────────────────────────────────────────────►│ │ │
|
|
356
|
-
│ │◄──────── 0 / 1 / N 个匹配 workflow ───────────┤ │ │
|
|
357
|
-
│ │ │ │ │ │
|
|
358
|
-
│ │ 对每个匹配的 workflow(通常仅 1 个): │ │ │
|
|
359
|
-
│ │ executeWorkflow(workflowId, { context: { headers, query, body } }, creator='webhook') │
|
|
360
|
-
│ ├──────────────────────►│ │ │ │
|
|
361
|
-
│ │ │ INSERT t_workflow_instance (creator='webhook', context=请求体) │
|
|
362
|
-
│ │ ├─────────────────────►│ │ │
|
|
363
|
-
│ │ │◄────── instanceId ───┤ │ │
|
|
364
|
-
│ │ │ │ │ │
|
|
365
|
-
│ │ │ temporalService.startWorkflow('dslWorkflow', …) │
|
|
366
|
-
│ │ ├──────────────────────────────────────────►│ │
|
|
367
|
-
│ │ │ │ 写 executions / history_node │
|
|
368
|
-
│ │ │ │ + 投 WorkflowTask │ │
|
|
369
|
-
│ │ │◄────── runId ────────────────────────────┤ │
|
|
370
|
-
│ │ │ UPDATE t_workflow_instance SET temporal_run_id│ │
|
|
371
|
-
│ │ ├─────────────────────►│ │ │
|
|
372
|
-
│ │◄── { instanceId } ────┤ │ │ PollWorkflowTask │
|
|
373
|
-
│ │ │ │ │◄──────────────────┤
|
|
374
|
-
│◄── 202 Accepted { instanceIds } ──────────┤ │ ├── task ──────────►│
|
|
375
|
-
│ (异步:webhook 立刻返回,工作流后台跑) │ │ │ │ 执行 dslWorkflow
|
|
376
|
-
│ │ │ │ INSERT/UPDATE t_workflow_node_execution│
|
|
377
|
-
│ │ │ │◄──────────────────────────────────────┤
|
|
378
|
-
│ │ │ │ UPDATE t_workflow_instance (status) │
|
|
379
|
-
│ │ │ │◄──────────────────────────────────────┤
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
**关键设计点:**
|
|
383
|
-
|
|
384
|
-
| 关注点 | 推荐做法 |
|
|
385
|
-
|--------|----------|
|
|
386
|
-
| 鉴权 | NestJS Controller 用 `@Public()` 跳过登录态,但每个 workflow 自带 `webhookSecret`,请求需带 `X-Signature: HMAC_SHA256(secret, body)` |
|
|
387
|
-
| 路径冲突 | `webhookPath` 必须在 published workflow 间唯一(Publish 时校验冲突,或用 `wf_${workflowId}` 做后缀) |
|
|
388
|
-
| 同步 vs 异步 | 默认异步:返回 `202 Accepted { instanceId }`,由前端轮询 `/instance/:id` 看结果。同步模式可选:等到 workflow 完成再返回(注意网关超时) |
|
|
389
|
-
| 多 workflow 订阅同一路径 | 与 outbox 一致:并行启动多个 instance,各自独立 |
|
|
390
|
-
| 重放保护 | 校验 `X-Timestamp` 在 ±5min 内 + payload 哈希做去重(可选,存 Redis 5min TTL) |
|
|
391
|
-
|
|
392
|
-
**涉及的 PG 表:**
|
|
393
|
-
|
|
394
|
-
| 表 | 操作 | 写入方 |
|
|
395
|
-
|----|------|--------|
|
|
396
|
-
| `t_workflow`、`t_workflow_version` | SELECT 含 `webhook_trigger` 节点的最新发布版本 | NestJS `WebhookController` |
|
|
397
|
-
| `t_workflow_instance` | INSERT (creator='webhook') → UPDATE temporal_run_id → UPDATE status | NestJS 创建;Worker `reporting.ts` 改状态 |
|
|
398
|
-
| `t_workflow_node_execution` | INSERT/UPDATE | Worker `reporting.ts` |
|
|
399
|
-
| `temporal.executions` / `history_node` / `task_queues` | 同场景 A | Temporal Server |
|
|
400
|
-
|
|
401
|
-
**为什么不让 Worker 自己开 HTTP 端口接收 webhook?**
|
|
402
|
-
|
|
403
|
-
- Worker 是无状态计算节点,不应面向公网;外部网络/网关/WAF 都暴露在 NestJS 那一层
|
|
404
|
-
- NestJS 已有完善的 Guard、Logger、Swagger、错误处理体系,复用更合适
|
|
405
|
-
- 校验签名、限流、IP 白名单等都属于"边界关注点",归属 Web 层
|
|
406
|
-
|
|
407
|
-
---
|
|
408
|
-
|
|
409
|
-
### 一句话对比(四种触发场景)
|
|
410
|
-
|
|
411
|
-
| | 场景 A:NestJS startWorkflow | 场景 B:Outbox Poller | 场景 C:Temporal Schedule | 场景 D:NestJS Webhook |
|
|
412
|
-
|--|------------------------------|------------------------|----------------------------|--------------------------|
|
|
413
|
-
| **当前状态** | ✅ 已实现 | ✅ 已实现 | ✅ 已实现 | ✅ 已实现(HMAC 严格) |
|
|
414
|
-
| 触发点 | 用户/定时业务调用 NestJS | 业务表 DML → 触发器 → outbox | Server 内置 cron 调度 | 外部 HTTP POST |
|
|
415
|
-
| 入口节点 | (任何,无 trigger 节点) | `event_trigger` | `cron_trigger` | `webhook_trigger` |
|
|
416
|
-
| 谁创建 `t_workflow_instance` | NestJS `WorkflowService` | Worker `outbox-poller` | Worker(cronTriggerWorkflow 内 Activity) | NestJS `WorkflowService` |
|
|
417
|
-
| 谁调用 `workflow.start()` | NestJS `TemporalService` | Worker `outbox-poller` | **Temporal Server 自己**(Schedule 系统) | NestJS `TemporalService` |
|
|
418
|
-
| 是否走 Temporal Server | ✅ | ✅ | ✅(且 schedule 本身也由 Server 持久化) | ✅ |
|
|
419
|
-
| `creator` 字段 | 登录用户 | `'event_trigger'` | `'cron'` | `'webhook'` |
|
|
420
|
-
| 触发延迟 | <100ms | ≤ 轮询间隔(默认 10s) | 由 Server 内部 timer 控制(秒级精度) | <100ms |
|
|
421
|
-
| 失败重试 | Workflow/Activity 自带重试 | outbox 行重试 ≤ `OUTBOX_MAX_RETRIES` | Schedule 不重试触发;Workflow 内部正常重试 | NestJS 直接返回 4xx/5xx;webhook 客户端自己决定是否重投 |
|
|
422
|
-
| 多实例并发安全 | NestJS 多 Pod 各自 OK | `FOR UPDATE SKIP LOCKED` 防重复消费 | Server 单一 schedule 实例,天然不重复 | NestJS 多 Pod 各自 OK |
|
|
423
|
-
|
|
424
|
-
**关键点:** 不论从哪个入口触发,只要调用了 `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 完成。
|
|
425
|
-
|
|
426
|
-
## 环境变量
|
|
427
|
-
|
|
428
|
-
| 变量 | 默认值 | 说明 |
|
|
429
|
-
|------|--------|------|
|
|
430
|
-
| `TEMPORAL_ADDRESS` | `localhost:7233` | Temporal Server gRPC 地址 |
|
|
431
|
-
| `TEMPORAL_NAMESPACE` | `default` | Temporal 命名空间 |
|
|
432
|
-
| `TEMPORAL_TASK_QUEUE` | `workflow-execution` | 任务队列名称 |
|
|
433
|
-
| `DATABASE_URL` | (见 .env) | Worker 回写状态用的数据库连接 |
|
|
434
|
-
|
|
435
|
-
## 常用命令
|
|
436
|
-
|
|
437
|
-
```bash
|
|
438
|
-
npm run up # 启动 Server + UI
|
|
439
|
-
npm run down # 停止 Server + UI
|
|
440
|
-
npm run logs # 查看 Server 日志
|
|
441
|
-
npm run worker:dev # 开发模式启动 Worker
|
|
442
|
-
npm run worker:build # 编译 Worker
|
|
443
|
-
npm run worker:start # 生产模式启动 Worker
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
## Web UI
|
|
447
|
-
|
|
448
|
-
启动后访问 http://localhost:8080 可查看:
|
|
449
|
-
- 运行中的 Workflow 列表
|
|
450
|
-
- Workflow 执行历史
|
|
451
|
-
- 单个 Workflow 的事件时间线
|
|
452
|
-
- Worker 连接状态
|
|
453
|
-
|
|
454
|
-
---
|
|
455
|
-
|
|
456
|
-
## Event Trigger(数据库事件触发 Workflow)
|
|
457
|
-
|
|
458
|
-
通过 Transactional Outbox 模式,当业务表发生变更时自动触发对应的 Workflow。
|
|
459
|
-
|
|
460
|
-
### 原理
|
|
461
|
-
|
|
462
|
-
```
|
|
463
|
-
┌─────────────┐ INSERT/UPDATE/DELETE ┌──────────────────────────┐
|
|
464
|
-
│ 任何来源 │ ────────────────────────→ │ PostgreSQL │
|
|
465
|
-
│ (NestJS / │ │ │
|
|
466
|
-
│ 外部系统) │ │ AFTER TRIGGER │
|
|
467
|
-
└─────────────┘ │ → INSERT INTO outbox │
|
|
468
|
-
│ (同一事务,保证不丢) │
|
|
469
|
-
└─────────────┬─────────────┘
|
|
470
|
-
│
|
|
471
|
-
outbox 表中有新行
|
|
472
|
-
│
|
|
473
|
-
▼
|
|
474
|
-
┌──────────────────────────┐
|
|
475
|
-
│ Worker │
|
|
476
|
-
│ outbox-poller.ts │
|
|
477
|
-
│ │
|
|
478
|
-
│ 每 10s 扫描一次: │
|
|
479
|
-
│ 1. SELECT 未处理行 │
|
|
480
|
-
│ 2. 匹配 event_trigger │
|
|
481
|
-
│ 3. 启动 Temporal workflow │
|
|
482
|
-
│ 4. 标记行为已处理 │
|
|
483
|
-
└──────────────────────────┘
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
### 为什么选轮询而不是 PG LISTEN/NOTIFY
|
|
487
|
-
|
|
488
|
-
| | PG NOTIFY | Outbox 轮询 |
|
|
489
|
-
|--|--|--|
|
|
490
|
-
| Worker 离线期间 | ❌ 事件丢失 | ✅ 事件留在表中,恢复后处理 |
|
|
491
|
-
| 可靠性 | best-effort | **at-least-once(事务保证)** |
|
|
492
|
-
| 复杂度 | 需要长连接 + 去重 | 只需定时 SELECT |
|
|
493
|
-
| 延迟 | <100ms | ≤ 轮询间隔(默认 10s) |
|
|
494
|
-
|
|
495
|
-
### 支持的操作类型
|
|
496
|
-
|
|
497
|
-
| 数据库操作 | eventName 示例 |
|
|
498
|
-
|-----------|---------------|
|
|
499
|
-
| 新增记录 | `t_order.insert` |
|
|
500
|
-
| 修改记录 | `t_order.update` |
|
|
501
|
-
| 删除记录 | `t_order.delete` |
|
|
502
|
-
|
|
503
|
-
### eventName 通配符匹配
|
|
504
|
-
|
|
505
|
-
在 Workflow DSL 的 `event_trigger` 节点中配置 `eventName`:
|
|
506
|
-
|
|
507
|
-
| eventName 配置 | 触发条件 |
|
|
508
|
-
|---|---|
|
|
509
|
-
| `t_order.insert` | 仅该表新增时触发 |
|
|
510
|
-
| `t_order.update` | 仅该表修改时触发 |
|
|
511
|
-
| `t_order.delete` | 仅该表删除时触发 |
|
|
512
|
-
| `t_order.*` | 该表任何变更都触发 |
|
|
513
|
-
| `*.insert` | 所有表的新增都触发 |
|
|
514
|
-
|
|
515
|
-
### 使用步骤
|
|
516
|
-
|
|
517
|
-
**Step 1:创建 outbox 表 + 通用触发器函数(只需执行一次)**
|
|
518
|
-
|
|
519
|
-
Outbox 表 `t_workflow_event_outbox` 由 Prisma 模型 `WorkflowEventOutbox` 维护,跟随 `prisma migrate` 流程自动建表;通用触发器函数 `notify_workflow_event()` 和未处理事件的部分索引 `idx_outbox_unprocessed` 由 seed 脚本创建:
|
|
520
|
-
|
|
521
|
-
```bash
|
|
522
|
-
# server 目录
|
|
523
|
-
cd server
|
|
524
|
-
yarn seed
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
幂等可重复执行。详见 `server/seed/workflow-event-trigger.ts`。
|
|
528
|
-
|
|
529
|
-
**Step 2:给需要监听的表挂触发器**
|
|
530
|
-
|
|
531
|
-
```sql
|
|
532
|
-
-- 只监听 INSERT
|
|
533
|
-
CREATE TRIGGER trg_order_workflow
|
|
534
|
-
AFTER INSERT ON t_order
|
|
535
|
-
FOR EACH ROW EXECUTE FUNCTION notify_workflow_event();
|
|
536
|
-
|
|
537
|
-
-- 监听所有变更(INSERT + UPDATE + DELETE)
|
|
538
|
-
CREATE TRIGGER trg_order_workflow
|
|
539
|
-
AFTER INSERT OR UPDATE OR DELETE ON t_order
|
|
540
|
-
FOR EACH ROW EXECUTE FUNCTION notify_workflow_event();
|
|
541
|
-
|
|
542
|
-
-- 只在特定条件下触发(例如状态变为 APPROVED)
|
|
543
|
-
CREATE OR REPLACE FUNCTION notify_order_approved() RETURNS trigger AS $$
|
|
544
|
-
BEGIN
|
|
545
|
-
IF NEW.status = 'APPROVED' AND (OLD.status IS NULL OR OLD.status != 'APPROVED') THEN
|
|
546
|
-
INSERT INTO t_workflow_event_outbox (event_name, payload)
|
|
547
|
-
VALUES ('t_order.status_approved', row_to_json(NEW)::jsonb);
|
|
548
|
-
END IF;
|
|
549
|
-
RETURN NEW;
|
|
550
|
-
END;
|
|
551
|
-
$$ LANGUAGE plpgsql;
|
|
552
|
-
|
|
553
|
-
CREATE TRIGGER trg_order_approved
|
|
554
|
-
AFTER UPDATE ON t_order
|
|
555
|
-
FOR EACH ROW EXECUTE FUNCTION notify_order_approved();
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
**Step 3:在 Workflow 编辑器中配置 event_trigger 节点**
|
|
14
|
+
## 快速上手(Phase 3 后新开发者视角)
|
|
559
15
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
```
|
|
16
|
+
1. 确认共享 temporal 已在本机或内网跑起来(本机方案见
|
|
17
|
+
`common/gadmin2/temporal-shared/README.md`)
|
|
18
|
+
2. `cp server/.env server/.env.local` 并填入 6 个变量(下方清单)
|
|
19
|
+
3. 首次注册本项目为共享 temporal 的租户:`bash dev/register-tenant.sh`
|
|
20
|
+
4. `docker compose up -d` — 4 个 service 起来(postgres / server-init / server / web)
|
|
21
|
+
5. Web UI 触发 workflow → 到共享 temporal UI(默认 http://localhost:8080)查看
|
|
569
22
|
|
|
570
|
-
|
|
571
|
-
- `filterExpression`(可选):JS 表达式,对 payload 的过滤条件
|
|
23
|
+
## `.env.local` 需要填的 6 个变量
|
|
572
24
|
|
|
573
|
-
|
|
25
|
+
| 变量 | 用途 | 示例 |
|
|
26
|
+
|------|------|------|
|
|
27
|
+
| `TEMPORAL_ADDRESS` | 共享 temporal 的 gRPC 地址 | `localhost:7233` |
|
|
28
|
+
| `TEMPORAL_NAMESPACE` | 本项目在共享 temporal 里的 namespace | `gadmin-test` |
|
|
29
|
+
| `SHARED_TEMPORAL_ADMIN_URL` | 共享 temporal admin API | `http://localhost:8081` |
|
|
30
|
+
| `SHARED_TEMPORAL_ADMIN_TOKEN` | admin API token | 问运维;32 hex |
|
|
31
|
+
| `GADMIN_TENANT_NAMESPACE` | admin API 里 tenant 的 namespace(= `TEMPORAL_NAMESPACE`) | `gadmin-test` |
|
|
32
|
+
| `SHARED_TENANT_DB_URL` | 共享 worker 视角看到的本项目 db url | `postgresql://kavenma:kavenma@host.containers.internal:5432/gadmin_demo` |
|
|
574
33
|
|
|
575
|
-
|
|
34
|
+
**为什么 `SHARED_TENANT_DB_URL` 与 `server/.env` 里的 `DATABASE_URL` 不同**:本项目内部
|
|
35
|
+
服务(server / server-init)走 docker network 用 `postgresql://.../postgres:5432/gadmin_demo`;
|
|
36
|
+
共享 worker 在独立 podman/docker 网络里,必须走 host 网络才能到达本项目的 PG,因此
|
|
37
|
+
用 `host.containers.internal:5432` 或 `host.docker.internal:5432`。见设计文档 §2.2。
|
|
576
38
|
|
|
577
|
-
|
|
39
|
+
## 健康检查
|
|
578
40
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
41
|
+
- 共享 temporal server 健康:`curl -s $TEMPORAL_ADDRESS/health` (7233 是 gRPC,不能直接
|
|
42
|
+
curl;用 `docker compose ps` 看 `shared-temporal-server` 状态 healthy 更方便)
|
|
43
|
+
- 共享 admin API:`curl -s $SHARED_TEMPORAL_ADMIN_URL/health/live` → `{"status":"ok"}`
|
|
44
|
+
- 本项目租户已注册:`curl -sH "X-Admin-Token: $SHARED_TEMPORAL_ADMIN_TOKEN" \
|
|
45
|
+
$SHARED_TEMPORAL_ADMIN_URL/admin/tenants | grep gadmin-test`
|
|
582
46
|
|
|
583
|
-
|
|
47
|
+
## 回滚
|
|
584
48
|
|
|
585
|
-
-
|
|
586
|
-
-
|
|
587
|
-
|
|
49
|
+
`git checkout phase-2-complete` 一条命令回到 Phase 2 收尾(temporal/ 目录 + 3 个
|
|
50
|
+
docker-compose service 全部复活)。之后 `docker compose up -d` 走老流程。无需 DB
|
|
51
|
+
迁移,新老 worker 都写同一张 `t_workflow_node_execution` 表结构。
|