@gadmin2n/schematics 0.0.145 → 0.0.147

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.
@@ -0,0 +1,8 @@
1
+ ## 与 Gadmin2 模板的关系
2
+
3
+ 本项目(gadmin-test)是 gadmin2 模板的**实例项目**,用于日常迭代新功能。迭代完成后需将新特性合并回 gadmin2 模板。
4
+
5
+ | | 路径 |
6
+ |------|------|
7
+ | 实例(本项目) | `/Users/kavenma/Documents/work/code/oit/test/gadmin-test` |
8
+ | 模板(上游) | `/Users/kavenma/Documents/work/code/common/gadmin2/schematics/src/lib/application/files/gadmin2-game-angle-demo` |
@@ -0,0 +1,70 @@
1
+ ## 项目简介
2
+ - 本项目由 lowcode 生成工具 Gadmin2 生成:Admin page(CRUD) + Dashboard,包括前端和后端。
3
+ - 前端的 Admin page(CRUD) 代码由配置(`config/ui/{ModelName}.ts`)驱动生成,后端的数据模型和 CRUD 代码由 prisma schema(`config/prisma/{module}.prisma`)驱动生成。
4
+
5
+ ## 常用命令
6
+
7
+ ```bash
8
+ # 改了 config/prisma/*.prisma —— 迁移数据库 + 生成前后端代码
9
+ gadmin2 g prisma:dev
10
+
11
+ # 只改了 config/ui/*.ts —— 只生成代码,不碰数据库
12
+ gadmin2 g prisma
13
+
14
+ cd server && yarn start:dev # 后端,端口 8000(优先读 server/.env.local)
15
+ cd web && yarn dev # 前端,端口 3000(注意web/ 用 yarn,不用 npm)
16
+ cd server && yarn seed # 执行 seed(页面/资源/权限声明)
17
+ ```
18
+
19
+ > gadmin2 命令需要在项目根目录执行
20
+
21
+ ## Architecture Overview
22
+
23
+ ### Frontend Tech Stack
24
+ React 18, TypeScript, Vite, Ant Design v5, Refinedev, ECharts, i18next
25
+
26
+ ```
27
+ web/src/
28
+ ├── routes/ #页面模块:user/、role/ 等
29
+ ├── components/ # 共享 UI 组件(PageHeader、RowActions、SearchBar 等)
30
+ ├── hooks/ # 自定义 hooks(useECharts、useDynamicResources、useListPageState 等)
31
+ ├── providers/ # auth / data / access control provider
32
+ ├── config/ # routeRegistry.tsx(路由注册)、http.ts
33
+ ├── constants/# layout.ts(页面布局共享常量)
34
+ ├── enums/ # 共享枚举
35
+ └── helpers/ # 工具函数(http.ts的 customRequest 等)
36
+ ```
37
+
38
+ ### Backend Tech Stack
39
+ NestJS v10, Prisma v6, TypeScript, nest-access-control, Winston logger
40
+
41
+ ```
42
+ server/src/
43
+ ├── modules/ # 业务模块,每个含 Module → Controller → Service
44
+ ├── generated/ # Prisma 自动生成(禁止直接修改)
45
+ ├── lib/ # 核心:auth.guard.ts、roles.guard.ts、taihu.ts、woaAuth.ts、logger.ts
46
+ ├── enums/ # 共享枚举
47
+ └── utils/ # 工具函数
48
+ ```
49
+
50
+ ### Permission Model (RBAC)
51
+ 参考 `.agent/references/permission-model.md`
52
+
53
+ ### 数据库迁移、dev/prod 模式差异、认证绕过等本地开发
54
+ 细节见 `.agent/references/local-dev.md`。
55
+
56
+ ### playwright 无头浏览器访问项目页面
57
+ 用 `server/scripts/gen-jwt.ts` 生成临时 token。
58
+
59
+ ## 开发原则(必须遵守)
60
+ - 使用 TypeScript 严格模式
61
+ - 严格遵循前、后端既有技术栈,如无必要不要引入新库
62
+ - 不确定时询问,不要猜;存在歧义时不要默默选择;有更简单的方案要说出来
63
+ - 修改 config/ui/ 或 config/prisma/ 配置后,必须在**项目根目录**执行生成命令 `gadmin2 g prisma` 或者 `gadmin2 g prisma:dev`。
64
+ - 修改 config/prisma/ 后必须同步检查 server/seed/:raw SQL 引用的列/表、seed 数据的必填字段与字段名要与新 schema 一致,改完需要提醒开发者 yarn seed 验证。
65
+ - **优先使用 skill 解决问题** skill 的决策逻辑(配置优先、DB 先于页面等)见 `.agent/references/decision-map.md`
66
+ - **默认业务代码范围:** 前端修改限定在 `web/src/routes/`,后端修改限定在 `server/src/modules/`、`server/seed/`。
67
+ - **配置驱动源码:** `config/prisma/`、`config/ui/` 可按生成流程修改;标准 CRUD 优先改配置,不要绕过配置直接改生成物。
68
+ - **跨切面窄白名单:** `web/src/config/routeRegistry.tsx` 仅可用于注册自定义路由。
69
+ - skill 中列出的目标文件不会自动扩大修改权限。除上述范围外,修改 `web/src/components/`、`web/src/hooks/`、`web/src/constants/`、`web/src/providers/`、全局认证文件,以及 `server/src/lib/`、`server/src/app.module.ts`、`server/src/main.ts` 等跨切面文件,必须由用户明确要求或确认具体实施计划,并限定为最小必要改动。
70
+ - **生成目录禁止直接修改:** `web/src/generated/`、`server/src/generated/` 只能由生成命令更新。
@@ -59,3 +59,4 @@ docs/
59
59
  agent-eval/runs/
60
60
  agent-eval/baseline/
61
61
  tmp/
62
+ CODEBUDDY.local.md
@@ -45,13 +45,5 @@ export async function seedWorkflowOutbox(prisma: PrismaClient) {
45
45
  $$;
46
46
  `);
47
47
 
48
- console.log('Seeding workflow outbox partial index...');
49
-
50
- await prisma.$executeRawUnsafe(`
51
- CREATE INDEX IF NOT EXISTS idx_outbox_unprocessed
52
- ON t_workflow_event_outbox (created_at)
53
- WHERE processed = FALSE;
54
- `);
55
-
56
48
  console.log('Workflow outbox initialized.');
57
49
  }
@@ -15,9 +15,9 @@ import { seedWorkflowOutbox } from './workflow-outbox';
15
15
  * 1. 菜单结构:workflow 父分组及其管理、审批任务页面
16
16
  * 2. resource: workflow / workflowNodeInstance
17
17
  * 3. PageResource 绑定与 SYSTEM_ADMIN 授权
18
- * 4. 业务数据:node-types / workflows / Outbox index
18
+ * 4. 业务数据:node-types / Outbox 旧触发器清理
19
19
  *
20
- * 业务数据 seed 函数仍来自 ./workflow-{node-types,workflows,outbox}.ts
20
+ * 业务数据 seed 函数仍来自 ./workflow-{node-types,outbox}.ts
21
21
  * (文件较大,保持独立但只被本入口 import,不再由 seed/index.ts 直接调用)。
22
22
  *
23
23
  * 前置:seedBootstrap 已建好 SYSTEM_ADMIN。
@@ -494,44 +494,34 @@ export class AgendaService
494
494
  };
495
495
  }
496
496
 
497
- /** Requeue jobs — reschedule them to run immediately */
497
+ /**
498
+ * 重排任务 —— 将原实例的 nextRunAt 重置为现在,同一行重跑。
499
+ * 不用 cancel + now(会向队列插入新行,且销毁周期任务的 repeatInterval)。
500
+ */
498
501
  async requeueByIds(jobIds: string[]) {
499
502
  let affected = 0;
500
503
  for (const id of jobIds) {
501
504
  const result = await this.agenda.queryJobs({ id });
502
- if (result.jobs.length > 0) {
503
- const job = result.jobs[0];
504
- const liveJobs = await this.agenda.queryJobs({ id } as any);
505
- if (liveJobs.jobs.length > 0) {
506
- await this.agenda.cancel({ _id: id } as any);
507
- await this.agenda.now(job.name, job.data as Record<string, any>);
508
- affected++;
509
- }
510
- }
505
+ if (result.jobs.length === 0) continue;
506
+ // 用完整 attrs 重建 Job 实例,save() 走 UPDATE 而非 INSERT
507
+ const job = new Job(this.agenda, { ...result.jobs[0] } as any);
508
+ job.schedule(new Date());
509
+ await job.save();
510
+ affected++;
511
511
  }
512
512
  return { affected };
513
513
  }
514
514
 
515
- /** Retry failed jobs */
515
+ /** Retry failed jobs —— 同样重置原实例,不清除周期计划 */
516
516
  async retryByIds(jobIds: string[]) {
517
- let affected = 0;
518
- for (const id of jobIds) {
519
- const result = await this.agenda.queryJobs({ id });
520
- if (result.jobs.length > 0) {
521
- const job = result.jobs[0];
522
- await this.agenda.cancel({ _id: id } as any);
523
- await this.agenda.now(job.name, job.data as Record<string, any>);
524
- affected++;
525
- }
526
- }
527
- return { affected };
517
+ return this.requeueByIds(jobIds);
528
518
  }
529
519
 
530
- /** Delete jobs by IDs */
520
+ /** Delete jobs by IDs(postgres backend 的 cancel 只认 { id },{_id} 会静默匹配 0 条) */
531
521
  async deleteByIds(jobIds: string[]) {
532
522
  let affected = 0;
533
523
  for (const id of jobIds) {
534
- const count = await this.agenda.cancel({ _id: id } as any);
524
+ const count = await this.agenda.cancel({ id });
535
525
  affected += count ?? 0;
536
526
  }
537
527
  return { affected };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.145",
3
+ "version": "0.0.147",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -1,863 +0,0 @@
1
- import { Prisma, PrismaClient } from '@prisma/client';
2
-
3
- /**
4
- * Mock workflow data with DSL, versions, instances, and node executions.
5
- */
6
- const WORKFLOWS = [
7
- {
8
- code: 'daily_data_sync',
9
- name: '每日数据同步',
10
- description: '每天凌晨2点从外部API同步用户数据到本地数据库',
11
- status: 'PUBLISHED' as const,
12
- isEnabled: true,
13
- creator: 'admin',
14
- dsl: {
15
- nodes: [
16
- {
17
- id: 'trigger_1',
18
- type: 'cron_trigger',
19
- label: 'Cron 触发',
20
- position: { x: 100, y: 200 },
21
- },
22
- {
23
- id: 'action_1',
24
- type: 'http_request',
25
- label: '拉取外部数据',
26
- position: { x: 350, y: 200 },
27
- config: {},
28
- },
29
- {
30
- id: 'action_2',
31
- type: 'code',
32
- label: '数据转换',
33
- position: { x: 600, y: 200 },
34
- config: {},
35
- },
36
- {
37
- id: 'action_3',
38
- type: 'db_execute',
39
- label: '写入数据库',
40
- position: { x: 850, y: 200 },
41
- config: {},
42
- },
43
- ],
44
- edges: [
45
- { id: 'e-trigger_1-action_1', source: 'trigger_1', target: 'action_1' },
46
- { id: 'e-action_1-action_2', source: 'action_1', target: 'action_2' },
47
- { id: 'e-action_2-action_3', source: 'action_2', target: 'action_3' },
48
- ],
49
- },
50
- },
51
- {
52
- code: 'user_reg_approval',
53
- name: '新用户注册审批',
54
- description: '新用户注册后触发审批流程,管理员审批通过后激活用户',
55
- status: 'PUBLISHED' as const,
56
- isEnabled: true,
57
- creator: 'admin',
58
- dsl: {
59
- nodes: [
60
- {
61
- id: 'trigger_1',
62
- type: 'webhook_trigger',
63
- label: 'Webhook 接收注册',
64
- position: { x: 100, y: 200 },
65
- },
66
- {
67
- id: 'approval_1',
68
- type: 'approval',
69
- label: '管理员审批',
70
- position: { x: 600, y: 200 },
71
- config: {},
72
- },
73
- {
74
- id: 'condition_1',
75
- type: 'if_else',
76
- label: '审批结果判断',
77
- position: { x: 850, y: 200 },
78
- config: {
79
- conditions: [
80
- {
81
- id: 'approval-approved',
82
- left: { source: 'INPUT', path: 'response.approved' },
83
- operator: 'EQ',
84
- right: {
85
- source: 'FIXED',
86
- valueType: 'BOOLEAN',
87
- value: 'true',
88
- },
89
- },
90
- ],
91
- combineOperation: 'AND',
92
- },
93
- },
94
- {
95
- id: 'action_2',
96
- type: 'db_execute',
97
- label: '激活用户',
98
- position: { x: 1100, y: 100 },
99
- config: {},
100
- },
101
- ],
102
- edges: [
103
- {
104
- id: 'e-trigger_1-approval_1',
105
- source: 'trigger_1',
106
- target: 'approval_1',
107
- },
108
- {
109
- id: 'e-approval_1-condition_1',
110
- source: 'approval_1',
111
- target: 'condition_1',
112
- },
113
- {
114
- id: 'e-condition_1-action_2',
115
- source: 'condition_1',
116
- target: 'action_2',
117
- label: 'true',
118
- },
119
- ],
120
- },
121
- },
122
- {
123
- code: 'error_monitor_alert',
124
- name: '异常监控告警',
125
- description: '监听系统事件并记录异常监控结果',
126
- status: 'PUBLISHED' as const,
127
- isEnabled: true,
128
- creator: 'system',
129
- dsl: {
130
- nodes: [
131
- {
132
- id: 'trigger_1',
133
- type: 'event_trigger',
134
- label: '监听错误事件',
135
- position: { x: 100, y: 200 },
136
- config: {
137
- registrationCode: 'admin-audit-log-insert',
138
- processingMode: 'PARALLEL',
139
- },
140
- },
141
- {
142
- id: 'action_1',
143
- type: 'db_execute',
144
- label: '查询近5分钟错误数',
145
- position: { x: 350, y: 200 },
146
- config: {
147
- allowWrite: false,
148
- sql: "SELECT COUNT(*) AS error_count FROM t_admin_audit_log WHERE created_at >= NOW() - INTERVAL '5 minutes'",
149
- parameters: [],
150
- timeoutSeconds: 30,
151
- },
152
- },
153
- {
154
- id: 'condition_1',
155
- type: 'if_else',
156
- label: '错误率 > 5%?',
157
- position: { x: 600, y: 200 },
158
- config: {
159
- conditions: [
160
- {
161
- id: 'error-count-high',
162
- left: { source: 'INPUT', path: 'rows.0.error_count' },
163
- operator: 'GT',
164
- right: {
165
- source: 'FIXED',
166
- valueType: 'NUMBER',
167
- value: '5',
168
- },
169
- },
170
- ],
171
- combineOperation: 'AND',
172
- },
173
- },
174
- {
175
- id: 'action_3',
176
- type: 'code',
177
- label: '记录日志',
178
- position: { x: 850, y: 300 },
179
- config: {},
180
- },
181
- ],
182
- edges: [
183
- { id: 'e-trigger_1-action_1', source: 'trigger_1', target: 'action_1' },
184
- {
185
- id: 'e-action_1-condition_1',
186
- source: 'action_1',
187
- target: 'condition_1',
188
- },
189
- {
190
- id: 'e-condition_1-action_3',
191
- source: 'condition_1',
192
- target: 'action_3',
193
- label: 'false',
194
- },
195
- ],
196
- },
197
- },
198
- {
199
- code: 'batch_data_export',
200
- name: '批量数据导出',
201
- description: '手动触发导出指定表的数据为 CSV 文件',
202
- status: 'DRAFT' as const,
203
- isEnabled: false,
204
- creator: 'admin',
205
- dsl: {
206
- nodes: [
207
- {
208
- id: 'trigger_1',
209
- type: 'manual_trigger',
210
- label: '手动触发',
211
- position: { x: 100, y: 200 },
212
- },
213
- {
214
- id: 'action_1',
215
- type: 'db_execute',
216
- label: '查询数据',
217
- position: { x: 350, y: 200 },
218
- config: {
219
- allowWrite: false,
220
- sql: 'SELECT * FROM t_admin_audit_log',
221
- parameters: [],
222
- timeoutSeconds: 30,
223
- },
224
- },
225
- {
226
- id: 'loop_1',
227
- type: 'for_each',
228
- label: '遍历分页',
229
- position: { x: 600, y: 200 },
230
- config: {
231
- itemsBinding: { source: 'INPUT', path: 'rows' },
232
- execution: { mode: 'SEQUENTIAL' },
233
- errorPolicy: 'STOP',
234
- },
235
- },
236
- {
237
- id: 'action_2',
238
- type: 'code',
239
- label: '转换为CSV',
240
- position: { x: 850, y: 200 },
241
- config: {},
242
- },
243
- ],
244
- edges: [
245
- { id: 'e-trigger_1-action_1', source: 'trigger_1', target: 'action_1' },
246
- { id: 'e-action_1-loop_1', source: 'action_1', target: 'loop_1' },
247
- {
248
- id: 'e-loop_1-action_2',
249
- kind: 'CALL',
250
- pairId: 'pair-loop_1-action_2',
251
- source: 'loop_1',
252
- target: 'action_2',
253
- sourceHandle: 'start',
254
- targetHandle: 'data-in',
255
- },
256
- {
257
- id: 'e-action_2-loop_1',
258
- kind: 'RETURN',
259
- pairId: 'pair-loop_1-action_2',
260
- source: 'action_2',
261
- target: 'loop_1',
262
- sourceHandle: 'result-out',
263
- targetHandle: 'end',
264
- valueType: 'JSON',
265
- },
266
- ],
267
- },
268
- },
269
- {
270
- code: 'multi_env_deploy',
271
- name: '多环境部署流水线',
272
- description: '并行部署到staging和production环境,需人工审批production',
273
- status: 'PUBLISHED' as const,
274
- isEnabled: true,
275
- creator: 'devops',
276
- dsl: {
277
- nodes: [
278
- {
279
- id: 'trigger_1',
280
- type: 'webhook_trigger',
281
- label: 'CI 回调',
282
- position: { x: 100, y: 200 },
283
- },
284
- {
285
- id: 'parallel_1',
286
- type: 'parallel',
287
- label: '并行部署',
288
- position: { x: 350, y: 200 },
289
- config: {
290
- branches: [
291
- { id: 'staging', label: '部署 Staging' },
292
- { id: 'production', label: '部署 Production' },
293
- ],
294
- },
295
- },
296
- {
297
- id: 'action_1',
298
- type: 'http_request',
299
- label: '部署 Staging',
300
- position: { x: 600, y: 100 },
301
- config: {},
302
- },
303
- {
304
- id: 'action_2',
305
- type: 'http_request',
306
- label: '部署 Production',
307
- position: { x: 850, y: 300 },
308
- config: {},
309
- },
310
- ],
311
- edges: [
312
- {
313
- id: 'e-trigger_1-parallel_1',
314
- source: 'trigger_1',
315
- target: 'parallel_1',
316
- },
317
- {
318
- id: 'e-parallel_1-action_1',
319
- kind: 'CALL',
320
- pairId: 'pair-staging',
321
- source: 'parallel_1',
322
- target: 'action_1',
323
- sourceHandle: 'staging-start',
324
- targetHandle: 'request-in',
325
- },
326
- {
327
- id: 'e-action_1-parallel_1',
328
- kind: 'RETURN',
329
- pairId: 'pair-staging',
330
- source: 'action_1',
331
- target: 'parallel_1',
332
- sourceHandle: 'response-out',
333
- targetHandle: 'staging-end',
334
- valueType: 'JSON',
335
- },
336
- {
337
- id: 'e-parallel_1-action_2',
338
- kind: 'CALL',
339
- pairId: 'pair-production',
340
- source: 'parallel_1',
341
- target: 'action_2',
342
- sourceHandle: 'production-start',
343
- targetHandle: 'request-in',
344
- },
345
- {
346
- id: 'e-action_2-parallel_1',
347
- kind: 'RETURN',
348
- pairId: 'pair-production',
349
- source: 'action_2',
350
- target: 'parallel_1',
351
- sourceHandle: 'response-out',
352
- targetHandle: 'production-end',
353
- valueType: 'JSON',
354
- },
355
- ],
356
- },
357
- },
358
- {
359
- code: 'user_role_change',
360
- name: '用户角色变更审计',
361
- description: '当 t_admin_user_role 表中用户角色发生变更时,记录审计日志',
362
- status: 'PUBLISHED' as const,
363
- isEnabled: true,
364
- creator: 'admin',
365
- dsl: {
366
- nodes: [
367
- {
368
- id: 'trigger_1',
369
- type: 'event_trigger',
370
- label: '监听角色变更事件',
371
- position: { x: 100, y: 200 },
372
- config: {
373
- registrationCode: 'admin-user-role-update',
374
- processingMode: 'PARALLEL',
375
- },
376
- },
377
- {
378
- id: 'action_1',
379
- type: 'db_execute',
380
- label: '查询用户详情',
381
- position: { x: 350, y: 200 },
382
- config: {
383
- allowWrite: false,
384
- sql: 'SELECT userid, username, roles FROM t_admin_user_role WHERE userid = $1',
385
- parameters: [{ source: 'INPUT', path: 'userid' }],
386
- timeoutSeconds: 30,
387
- },
388
- },
389
- {
390
- id: 'action_2',
391
- type: 'code',
392
- label: '对比角色差异',
393
- position: { x: 600, y: 200 },
394
- config: {
395
- script:
396
- 'const prev = JSON.parse(input.previousRoles); const curr = JSON.parse(input.currentRoles); return { added: curr.filter(r => !prev.includes(r)), removed: prev.filter(r => !curr.includes(r)) };',
397
- },
398
- },
399
- {
400
- id: 'condition_1',
401
- type: 'if_else',
402
- label: '是否涉及ADMIN角色',
403
- position: { x: 850, y: 200 },
404
- config: {
405
- conditions: [
406
- {
407
- id: 'contains-system-admin',
408
- left: { source: 'INPUT', path: 'added' },
409
- operator: 'CONTAINS',
410
- right: {
411
- source: 'FIXED',
412
- valueType: 'STRING',
413
- value: 'SYSTEM_ADMIN',
414
- },
415
- },
416
- ],
417
- combineOperation: 'AND',
418
- },
419
- },
420
- {
421
- id: 'action_4',
422
- type: 'db_execute',
423
- label: '写入审计日志',
424
- position: { x: 1100, y: 300 },
425
- config: { operation: 'insert', table: 't_admin_audit_log' },
426
- },
427
- ],
428
- edges: [
429
- { id: 'e-trigger_1-action_1', source: 'trigger_1', target: 'action_1' },
430
- { id: 'e-action_1-action_2', source: 'action_1', target: 'action_2' },
431
- {
432
- id: 'e-action_2-condition_1',
433
- source: 'action_2',
434
- target: 'condition_1',
435
- },
436
- {
437
- id: 'e-condition_1-action_4-true',
438
- source: 'condition_1',
439
- target: 'action_4',
440
- label: 'true',
441
- },
442
- {
443
- id: 'e-condition_1-action_4',
444
- source: 'condition_1',
445
- target: 'action_4',
446
- label: 'false',
447
- },
448
- ],
449
- },
450
- },
451
- {
452
- code: 'event_remind_sync',
453
- name: 'Event活动提醒与同步',
454
- description: '当 t_event 表新增活动或活动即将开始时,自动同步到 ServiceNow',
455
- status: 'PUBLISHED' as const,
456
- isEnabled: true,
457
- creator: 'admin',
458
- dsl: {
459
- nodes: [
460
- {
461
- id: 'trigger_1',
462
- type: 'cron_trigger',
463
- label: '每日9点检查',
464
- position: { x: 100, y: 200 },
465
- config: { cronExpressions: ['0 9 * * *'], timezone: 'Asia/Shanghai' },
466
- },
467
- {
468
- id: 'action_1',
469
- type: 'db_execute',
470
- label: '查询3天内活动',
471
- position: { x: 350, y: 200 },
472
- config: {
473
- allowWrite: false,
474
- sql: "SELECT * FROM t_event WHERE start_date BETWEEN NOW() AND NOW() + INTERVAL '3 days'",
475
- parameters: [],
476
- timeoutSeconds: 30,
477
- },
478
- },
479
- {
480
- id: 'condition_1',
481
- type: 'if_else',
482
- label: '有即将开始的活动?',
483
- position: { x: 600, y: 200 },
484
- config: {
485
- conditions: [
486
- {
487
- id: 'has-upcoming-events',
488
- left: { source: 'INPUT', path: 'rowCount' },
489
- operator: 'GT',
490
- right: {
491
- source: 'FIXED',
492
- valueType: 'NUMBER',
493
- value: '0',
494
- },
495
- },
496
- ],
497
- combineOperation: 'AND',
498
- },
499
- },
500
- {
501
- id: 'loop_1',
502
- type: 'for_each',
503
- label: '遍历活动列表',
504
- position: { x: 850, y: 150 },
505
- config: {
506
- itemsBinding: { source: 'INPUT', path: 'rows' },
507
- execution: { mode: 'PARALLEL', concurrency: 3 },
508
- errorPolicy: 'STOP',
509
- },
510
- },
511
- {
512
- id: 'action_2',
513
- type: 'http_request',
514
- label: '同步到ServiceNow',
515
- position: { x: 1350, y: 80 },
516
- config: {
517
- url: 'https://servicenow.example.com/api/events',
518
- method: 'POST',
519
- },
520
- },
521
- {
522
- id: 'action_3',
523
- type: 'db_execute',
524
- label: '更新sn_sys_id',
525
- position: { x: 1600, y: 80 },
526
- config: { operation: 'update', table: 't_event', where: 'id = $1' },
527
- },
528
- {
529
- id: 'action_5',
530
- type: 'code',
531
- label: '记录检查完成',
532
- position: { x: 600, y: 350 },
533
- config: {
534
- script: 'return { checked: true, noUpcomingEvents: true };',
535
- },
536
- },
537
- ],
538
- edges: [
539
- { id: 'e-trigger_1-action_1', source: 'trigger_1', target: 'action_1' },
540
- {
541
- id: 'e-action_1-condition_1',
542
- source: 'action_1',
543
- target: 'condition_1',
544
- },
545
- {
546
- id: 'e-condition_1-loop_1',
547
- source: 'condition_1',
548
- target: 'loop_1',
549
- label: 'true',
550
- },
551
- {
552
- id: 'e-condition_1-action_5',
553
- source: 'condition_1',
554
- target: 'action_5',
555
- label: 'false',
556
- },
557
- {
558
- id: 'e-loop_1-action_2',
559
- kind: 'CALL',
560
- pairId: 'pair-loop_1-action_2',
561
- source: 'loop_1',
562
- target: 'action_2',
563
- sourceHandle: 'start',
564
- targetHandle: 'data-in',
565
- },
566
- {
567
- id: 'e-action_2-action_3',
568
- kind: 'FLOW',
569
- pairId: 'pair-loop_1-action_2',
570
- source: 'action_2',
571
- target: 'action_3',
572
- },
573
- {
574
- id: 'e-action_3-loop_1',
575
- kind: 'RETURN',
576
- pairId: 'pair-loop_1-action_2',
577
- source: 'action_3',
578
- target: 'loop_1',
579
- sourceHandle: 'result-out',
580
- targetHandle: 'end',
581
- valueType: 'JSON',
582
- },
583
- ],
584
- },
585
- },
586
- {
587
- code: 'new_user_onboard',
588
- name: '新增用户自动化流程',
589
- description:
590
- '当 t_admin_user_role 新增用户时,自动分配默认角色、同步到外部系统并记录审计日志',
591
- status: 'PUBLISHED' as const,
592
- isEnabled: true,
593
- creator: 'admin',
594
- dsl: {
595
- nodes: [
596
- {
597
- id: 'trigger_1',
598
- type: 'event_trigger',
599
- label: '监听用户创建事件',
600
- position: { x: 100, y: 200 },
601
- config: {
602
- registrationCode: 'admin-user-role-insert',
603
- processingMode: 'PARALLEL',
604
- },
605
- },
606
- {
607
- id: 'action_1',
608
- type: 'db_execute',
609
- label: '查询新用户信息',
610
- position: { x: 350, y: 200 },
611
- config: {
612
- allowWrite: false,
613
- sql: 'SELECT userid, username, roles, is_active FROM t_admin_user_role WHERE userid = $1',
614
- parameters: [{ source: 'INPUT', path: 'userid' }],
615
- timeoutSeconds: 30,
616
- },
617
- },
618
- {
619
- id: 'action_2',
620
- type: 'code',
621
- label: '初始化默认角色',
622
- position: { x: 600, y: 200 },
623
- config: {
624
- script:
625
- 'const user = input.rows[0]; if (!user.roles || user.roles === "[]") { return { ...user, roles: JSON.stringify(["GUEST"]), needsUpdate: true }; } return { ...user, needsUpdate: false };',
626
- },
627
- },
628
- {
629
- id: 'condition_1',
630
- type: 'if_else',
631
- label: '需要分配默认角色?',
632
- position: { x: 850, y: 200 },
633
- config: {
634
- conditions: [
635
- {
636
- id: 'needs-default-role',
637
- left: { source: 'INPUT', path: 'needsUpdate' },
638
- operator: 'EQ',
639
- right: {
640
- source: 'FIXED',
641
- valueType: 'BOOLEAN',
642
- value: 'true',
643
- },
644
- },
645
- ],
646
- combineOperation: 'AND',
647
- },
648
- },
649
- {
650
- id: 'action_3',
651
- type: 'db_execute',
652
- label: '写入默认角色',
653
- position: { x: 1100, y: 100 },
654
- config: {
655
- operation: 'update',
656
- table: 't_admin_user_role',
657
- values: [
658
- {
659
- column: 'roles',
660
- binding: {
661
- source: 'FIXED',
662
- valueType: 'STRING',
663
- value: '["GUEST"]',
664
- },
665
- },
666
- ],
667
- where: 'userid = $2',
668
- parameters: [{ source: 'INPUT', path: 'userid' }],
669
- timeoutSeconds: 30,
670
- },
671
- },
672
- {
673
- id: 'action_4',
674
- type: 'http_request',
675
- label: '同步到企业通讯录',
676
- position: { x: 1100, y: 300 },
677
- config: {
678
- url: 'https://api.wecom.example.com/user/sync',
679
- method: 'POST',
680
- },
681
- },
682
- {
683
- id: 'action_6',
684
- type: 'db_execute',
685
- label: '记录审计日志',
686
- position: { x: 1600, y: 200 },
687
- config: {
688
- operation: 'insert',
689
- table: 't_admin_audit_log',
690
- values: [
691
- {
692
- column: 'action',
693
- binding: {
694
- source: 'FIXED',
695
- valueType: 'STRING',
696
- value: 'user.onboarded',
697
- },
698
- },
699
- {
700
- column: 'resource',
701
- binding: {
702
- source: 'FIXED',
703
- valueType: 'STRING',
704
- value: 'user',
705
- },
706
- },
707
- ],
708
- parameters: [],
709
- timeoutSeconds: 30,
710
- },
711
- },
712
- ],
713
- edges: [
714
- { id: 'e-trigger_1-action_1', source: 'trigger_1', target: 'action_1' },
715
- { id: 'e-action_1-action_2', source: 'action_1', target: 'action_2' },
716
- {
717
- id: 'e-action_2-condition_1',
718
- source: 'action_2',
719
- target: 'condition_1',
720
- },
721
- {
722
- id: 'e-condition_1-action_3',
723
- source: 'condition_1',
724
- target: 'action_3',
725
- label: 'true',
726
- },
727
- {
728
- id: 'e-condition_1-action_4',
729
- source: 'condition_1',
730
- target: 'action_4',
731
- label: 'false',
732
- },
733
- { id: 'e-action_3-action_4', source: 'action_3', target: 'action_4' },
734
- { id: 'e-action_4-action_6', source: 'action_4', target: 'action_6' },
735
- ],
736
- },
737
- },
738
- ];
739
-
740
- export async function seedWorkflows(prisma: PrismaClient) {
741
- console.log('Seeding workflows...');
742
-
743
- for (const wf of WORKFLOWS) {
744
- // Upsert workflow by code
745
- const workflow = await prisma.workflow.upsert({
746
- where: { code: wf.code },
747
- update: {
748
- name: wf.name,
749
- description: wf.description,
750
- status: wf.status,
751
- isEnabled: wf.isEnabled,
752
- dsl: wf.dsl,
753
- },
754
- create: {
755
- code: wf.code,
756
- name: wf.name,
757
- description: wf.description,
758
- status: wf.status,
759
- isEnabled: wf.isEnabled,
760
- dsl: wf.dsl,
761
- creator: wf.creator,
762
- },
763
- });
764
-
765
- // Create version 1
766
- await prisma.workflowVersion.upsert({
767
- where: { workflowId_version: { workflowId: workflow.id, version: 1 } },
768
- update: {},
769
- create: {
770
- workflowId: workflow.id,
771
- version: 1,
772
- dsl: wf.dsl,
773
- prompt: `创建工作流: ${wf.name}`,
774
- changeSummary: '初始版本',
775
- creator: wf.creator,
776
- },
777
- });
778
-
779
- // For published workflows, create some mock instances
780
- if (wf.status === 'PUBLISHED') {
781
- const version = await prisma.workflowVersion.findFirst({
782
- where: { workflowId: workflow.id, version: 1 },
783
- });
784
-
785
- if (!version) continue;
786
-
787
- // Create 3 instances per published workflow with different statuses
788
- const instanceConfigs = [
789
- { status: 'COMPLETED' as const, daysAgo: 3 },
790
- { status: 'COMPLETED' as const, daysAgo: 1 },
791
- { status: 'RUNNING' as const, daysAgo: 0 },
792
- ];
793
-
794
- for (const cfg of instanceConfigs) {
795
- const startedAt = new Date(
796
- Date.now() - cfg.daysAgo * 24 * 60 * 60 * 1000,
797
- );
798
- const finishedAt =
799
- cfg.status === 'COMPLETED'
800
- ? new Date(startedAt.getTime() + 30000) // 30s later
801
- : null;
802
-
803
- const instance = await prisma.workflowInstance.create({
804
- data: {
805
- workflowId: workflow.id,
806
- versionId: version.id,
807
- status: cfg.status,
808
- context: { trigger: 'auto', env: 'production' },
809
- result:
810
- cfg.status === 'COMPLETED'
811
- ? { success: true, processedCount: 128 }
812
- : Prisma.JsonNull,
813
- startedAt,
814
- finishedAt,
815
- creator: wf.creator,
816
- },
817
- });
818
-
819
- // Create node executions for each instance
820
- const nodes = (wf.dsl as any).nodes || [];
821
- for (let i = 0; i < nodes.length; i++) {
822
- const node = nodes[i];
823
- const nodeStarted = new Date(startedAt.getTime() + i * 5000);
824
- const isLastNode = i === nodes.length - 1;
825
-
826
- let nodeStatus: 'COMPLETED' | 'RUNNING' | 'PENDING' = 'COMPLETED';
827
- if (cfg.status === 'RUNNING') {
828
- if (i === nodes.length - 1) nodeStatus = 'RUNNING';
829
- if (i > nodes.length - 1) nodeStatus = 'PENDING';
830
- }
831
-
832
- await prisma.workflowNodeExecution.upsert({
833
- where: {
834
- instanceId_nodeId: { instanceId: instance.id, nodeId: node.id },
835
- },
836
- update: {},
837
- create: {
838
- instanceId: instance.id,
839
- nodeId: node.id,
840
- nodeType: node.type,
841
- nodeLabel: node.label,
842
- status: nodeStatus,
843
- input: { data: `mock_input_${i}` },
844
- output:
845
- nodeStatus === 'COMPLETED'
846
- ? { data: `mock_output_${i}` }
847
- : Prisma.JsonNull,
848
- startedAt: nodeStarted,
849
- finishedAt:
850
- nodeStatus === 'COMPLETED'
851
- ? new Date(nodeStarted.getTime() + 3000)
852
- : null,
853
- },
854
- });
855
- }
856
- }
857
- }
858
- }
859
-
860
- console.log(
861
- `Seeded ${WORKFLOWS.length} workflows with versions, instances, and node executions.`,
862
- );
863
- }