@mldong/jeeflow 1.0.0 → 1.1.0
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/README.md +58 -55
- package/dist/engine.d.ts +3 -0
- package/dist/engine.js +66 -7
- package/dist/facade.d.ts +22 -0
- package/dist/facade.js +329 -0
- package/dist/jdbc/ext.d.ts +29 -0
- package/dist/jdbc/ext.js +258 -0
- package/dist/jdbc/shared.d.ts +7 -0
- package/dist/jdbc/shared.js +80 -3
- package/dist/memory-ext.d.ts +21 -0
- package/dist/memory-ext.js +83 -0
- package/dist/memory.d.ts +5 -0
- package/dist/memory.js +36 -0
- package/dist/model.d.ts +33 -0
- package/dist/spi.d.ts +21 -1
- package/package.json +45 -45
package/README.md
CHANGED
|
@@ -1,55 +1,58 @@
|
|
|
1
|
-
# jeeflow · Node.js / TypeScript
|
|
2
|
-
|
|
3
|
-
[jeeflow](https://jeeflow-doc.mldong.com) 引擎规范的 **TypeScript 实现**。仅依赖 `node:*` 标准库,零外部依赖(引擎核心)。
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
44
|
-
| `src/
|
|
45
|
-
| `src/
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
# jeeflow · Node.js / TypeScript
|
|
2
|
+
|
|
3
|
+
[jeeflow](https://jeeflow-doc.mldong.com) 引擎规范的 **TypeScript 实现**。仅依赖 `node:*` 标准库,零外部依赖(引擎核心)。
|
|
4
|
+
|
|
5
|
+
> **v1.1.0**:新增管理扩展(流程设计/历史/委托 + `ProcessExtRepository`)与统一门面
|
|
6
|
+
> `JeeflowFacade.flow(action, args)`;assignee 变量解析与 `flow.auto`/`flow.admin` 系统代执行对齐 boot2/boot3。
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @mldong/jeeflow # 等发布到 npm 后
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 快速开始
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { EngineImpl, MemoryRepository } from '@mldong/jeeflow'
|
|
16
|
+
|
|
17
|
+
const repo = new MemoryRepository()
|
|
18
|
+
const engine = new EngineImpl(repo)
|
|
19
|
+
|
|
20
|
+
const def = {
|
|
21
|
+
id: 1, name: 'leave', displayName: '请假审批',
|
|
22
|
+
content: JSON.stringify({ nodes: [...], edges: [...] })
|
|
23
|
+
}
|
|
24
|
+
repo.addDefine(def)
|
|
25
|
+
|
|
26
|
+
const inst = await engine.startProcessInstanceById(def.id, '张三')
|
|
27
|
+
const tasks = await repo.findDoingTasks(inst.id)
|
|
28
|
+
await engine.executeProcessTask(tasks[0].id, 'leader')
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 运行演示
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone https://github.com/mldong/jeeflow-node
|
|
35
|
+
cd jeeflow-node
|
|
36
|
+
npm install
|
|
37
|
+
npm run demo # http://localhost:8082
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 目录
|
|
41
|
+
|
|
42
|
+
| 路径 | 说明 |
|
|
43
|
+
|------|------|
|
|
44
|
+
| `src/engine.ts` | 引擎核心 |
|
|
45
|
+
| `src/model.ts` | 域类型 |
|
|
46
|
+
| `src/spi.ts` | SPI 接口 |
|
|
47
|
+
| `src/memory.ts` | 内存仓储 |
|
|
48
|
+
| `src/jdbc/` | JDBC 多库实现(shared 核心 + mysql/postgres 适配器) |
|
|
49
|
+
| `demo/main.ts` | Express 演示 |
|
|
50
|
+
| `__tests__/` | 9 项合规测试 |
|
|
51
|
+
|
|
52
|
+
## 节点支持
|
|
53
|
+
|
|
54
|
+
对齐 [SPEC.md](https://jeeflow-doc.mldong.com) v1.0——与 Java/Go 版一致。
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
Apache-2.0
|
package/dist/engine.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export declare const KeyDeptID = "u_deptId";
|
|
|
10
10
|
export declare const KeyDeptName = "u_deptName";
|
|
11
11
|
export declare const KeyPostID = "u_postId";
|
|
12
12
|
export declare const KeyPostName = "u_postName";
|
|
13
|
+
export declare const KeyNextNodeOperator = "tf_nextNodeOperator";
|
|
14
|
+
export declare const KeyAutoExecute = "flow.auto";
|
|
15
|
+
export declare const KeyAdminID = "flow.admin";
|
|
13
16
|
export interface Engine {
|
|
14
17
|
startProcessInstanceById(defineId: number, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
|
|
15
18
|
executeProcessTask(taskId: number, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
|
package/dist/engine.js
CHANGED
|
@@ -8,6 +8,11 @@ export const KeyDeptID = 'u_deptId';
|
|
|
8
8
|
export const KeyDeptName = 'u_deptName';
|
|
9
9
|
export const KeyPostID = 'u_postId';
|
|
10
10
|
export const KeyPostName = 'u_postName';
|
|
11
|
+
// v1.0.1:下一节点处理人(对齐 boot3 tf_nextNodeOperator)
|
|
12
|
+
export const KeyNextNodeOperator = 'tf_nextNodeOperator';
|
|
13
|
+
// v1.0.1:系统代执行 / 超级管理员(对齐 boot3 FlowConst)
|
|
14
|
+
export const KeyAutoExecute = 'flow.auto';
|
|
15
|
+
export const KeyAdminID = 'flow.admin';
|
|
11
16
|
export class EngineImpl {
|
|
12
17
|
repo;
|
|
13
18
|
userProv;
|
|
@@ -74,6 +79,9 @@ export class EngineImpl {
|
|
|
74
79
|
// 聚合根:完成任务(子实体状态转换 + 实例变量合并)
|
|
75
80
|
inst.completeTask(task, operator, vars, now);
|
|
76
81
|
await this.repo.updateTask(task);
|
|
82
|
+
// v1.0.1:updateInstance 级联持久化依赖聚合内任务副本为最新状态,
|
|
83
|
+
// completeTask 改的是外部任务对象,需同步回聚合根
|
|
84
|
+
syncTaskToAggregate(inst, task);
|
|
77
85
|
await this.fireEvent({ type: EventType.TaskComplete, instanceId: inst.id, taskId: task.id, nodeId: task.taskName, operator });
|
|
78
86
|
const def = await this.repo.findDefineById(inst.defineId);
|
|
79
87
|
const flow = JSON.parse(typeof def.content === 'string' ? def.content : new TextDecoder().decode(def.content));
|
|
@@ -132,6 +140,8 @@ export class EngineImpl {
|
|
|
132
140
|
// 子实体:完成任务
|
|
133
141
|
task.finish(operator, task.variables, now);
|
|
134
142
|
await this.repo.updateTask(task);
|
|
143
|
+
// v1.0.1:同步回聚合根,避免 updateInstance 级联把任务写回旧状态
|
|
144
|
+
syncTaskToAggregate(inst, task);
|
|
135
145
|
// 聚合根:驳回
|
|
136
146
|
inst.reject(now);
|
|
137
147
|
await this.repo.updateInstance(inst);
|
|
@@ -286,8 +296,8 @@ export class EngineImpl {
|
|
|
286
296
|
}
|
|
287
297
|
}
|
|
288
298
|
}
|
|
289
|
-
async createTask(node, inst, operator,
|
|
290
|
-
const actors = await this.resolveActors(node, inst);
|
|
299
|
+
async createTask(node, inst, operator, vars) {
|
|
300
|
+
const actors = await this.resolveActors(node, inst, vars);
|
|
291
301
|
if (!actors.length)
|
|
292
302
|
return;
|
|
293
303
|
const performType = parseInt(String(node.properties?.performType ?? '0'));
|
|
@@ -316,9 +326,13 @@ export class EngineImpl {
|
|
|
316
326
|
return;
|
|
317
327
|
}
|
|
318
328
|
}
|
|
319
|
-
|
|
329
|
+
// 普通任务:一个任务承载全部参与者(对齐 boot3 createTask + addTaskActor,多参与者任一可办)
|
|
330
|
+
const nt = inst.createTask(this.nextId(), node.id, node.text.value, actors[0], operator, form, now);
|
|
331
|
+
if (actors.length > 1)
|
|
332
|
+
nt.actorIds = actors;
|
|
333
|
+
await this.repo.saveTask(nt);
|
|
320
334
|
}
|
|
321
|
-
async resolveActors(node, inst) {
|
|
335
|
+
async resolveActors(node, inst, vars) {
|
|
322
336
|
// 1a. Registry 按名称解析(推荐)
|
|
323
337
|
if (this.registry) {
|
|
324
338
|
const handlerName = node.properties?.assignmentHandler ?? '';
|
|
@@ -335,21 +349,56 @@ export class EngineImpl {
|
|
|
335
349
|
if (Array.isArray(result) && result.length > 0)
|
|
336
350
|
return result;
|
|
337
351
|
}
|
|
338
|
-
// 2.
|
|
352
|
+
// 2. 动态指定下一节点处理人优先(v1.0.1:对齐 boot3 tf_nextNodeOperator)
|
|
353
|
+
const nextOp = vars[KeyNextNodeOperator];
|
|
354
|
+
if (nextOp != null) {
|
|
355
|
+
if (typeof nextOp === 'string')
|
|
356
|
+
return nextOp.split(',').map(s => s.trim()).filter(Boolean);
|
|
357
|
+
if (Array.isArray(nextOp))
|
|
358
|
+
return nextOp.map(String);
|
|
359
|
+
return [String(nextOp)];
|
|
360
|
+
}
|
|
361
|
+
// 3. 固定指派 assignee——token 即变量 key,能替换就换,换不了就是字面量(v1.0.1 对齐 boot3 args.get(token, token))
|
|
339
362
|
const assignee = node.properties?.assignee;
|
|
340
363
|
if (assignee) {
|
|
341
|
-
const actors =
|
|
342
|
-
|
|
364
|
+
const actors = [];
|
|
365
|
+
for (const raw of assignee.split(',')) {
|
|
366
|
+
let token = raw.trim();
|
|
367
|
+
if (!token)
|
|
368
|
+
continue;
|
|
369
|
+
// mldong 契约特殊值:applicant → 流程发起人
|
|
370
|
+
if (token.includes('applicant'))
|
|
371
|
+
token = token.replace('applicant', inst.operator);
|
|
372
|
+
if (token in vars) {
|
|
373
|
+
const val = vars[token];
|
|
374
|
+
if (Array.isArray(val))
|
|
375
|
+
actors.push(...val.map(String));
|
|
376
|
+
else
|
|
377
|
+
actors.push(String(val));
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
actors.push(token);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return actors;
|
|
343
384
|
}
|
|
344
385
|
return [];
|
|
345
386
|
}
|
|
346
387
|
isAllowed(task, operator) {
|
|
388
|
+
// v1.0.1:系统代执行(flow.auto)/超级管理员(flow.admin)放行(对齐 boot3 isAllowed)
|
|
389
|
+
if (operator && (operator.toLowerCase() === KeyAutoExecute || operator.toLowerCase() === KeyAdminID)) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
347
392
|
// 子实体:actorIds 权限判断
|
|
348
393
|
return task.isAllowed(operator);
|
|
349
394
|
}
|
|
350
395
|
async addUserInfo(operator, vars) {
|
|
351
396
|
if (!this.userProv)
|
|
352
397
|
return;
|
|
398
|
+
// v1.0.1:系统代执行(flow.auto)/超级管理员(flow.admin)非真实用户,跳过注入(对齐 boot3)
|
|
399
|
+
if (operator && (operator.toLowerCase() === KeyAutoExecute || operator.toLowerCase() === KeyAdminID)) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
353
402
|
const u = await this.userProv.getUser(operator);
|
|
354
403
|
if (!u)
|
|
355
404
|
return;
|
|
@@ -389,6 +438,16 @@ function getCsState(vars, nodeId) {
|
|
|
389
438
|
const lc = parseInt(String(vars[`loopCounter_${nodeId}`] ?? '0'));
|
|
390
439
|
return [actors, lc];
|
|
391
440
|
}
|
|
441
|
+
// syncTaskToAggregate 把外部任务对象的最新状态同步回聚合根任务副本
|
|
442
|
+
// (v1.0.1:updateInstance 级联持久化依赖聚合内任务副本为最新状态)
|
|
443
|
+
function syncTaskToAggregate(inst, task) {
|
|
444
|
+
for (let i = 0; i < inst.tasks.length; i++) {
|
|
445
|
+
if (inst.tasks[i].id === task.id) {
|
|
446
|
+
inst.tasks[i] = task;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
392
451
|
function isTruthy(v) {
|
|
393
452
|
if (typeof v === 'boolean')
|
|
394
453
|
return v;
|
package/dist/facade.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ProcessExtRepository, ProcessRepository } from './spi.js';
|
|
2
|
+
import type { EngineImpl } from './engine.js';
|
|
3
|
+
export declare class JeeflowFacade {
|
|
4
|
+
private readonly engine;
|
|
5
|
+
private readonly repo;
|
|
6
|
+
private readonly extRepo?;
|
|
7
|
+
constructor(engine: EngineImpl, repo: ProcessRepository, extRepo?: ProcessExtRepository | undefined);
|
|
8
|
+
flow(action: string, args?: Record<string, any>): Promise<Record<string, any>>;
|
|
9
|
+
private dispatch;
|
|
10
|
+
private startAndExecute;
|
|
11
|
+
private deploy;
|
|
12
|
+
private saveDeployedDefine;
|
|
13
|
+
private redeploy;
|
|
14
|
+
private withdraw;
|
|
15
|
+
private execute;
|
|
16
|
+
private designPage;
|
|
17
|
+
private designDetail;
|
|
18
|
+
private designSave;
|
|
19
|
+
private surrogatePage;
|
|
20
|
+
private surrogateSave;
|
|
21
|
+
private ext;
|
|
22
|
+
}
|
package/dist/facade.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// 统一门面(v1.1.0)——"接口即 POST + JSON body"风格的单入口
|
|
2
|
+
//
|
|
3
|
+
// 集成方只实现一个转发端点:把 body JSON 转成对象传入 flow(),所有流程能力按
|
|
4
|
+
// action(boot2/boot3 端点短名)路由。返回统一结构 {code, msg, data}
|
|
5
|
+
// (code=0 成功 / 99999999 失败)。操作人约定:args.operator 显式传入。
|
|
6
|
+
// submitType 枚举(对齐 boot3)
|
|
7
|
+
const SUBMIT_APPLY = 0;
|
|
8
|
+
const SUBMIT_AGREE = 1;
|
|
9
|
+
const SUBMIT_REJECT = 2;
|
|
10
|
+
const SUBMIT_ROLLBACK = 3;
|
|
11
|
+
const SUBMIT_JUMP = 4;
|
|
12
|
+
const SUBMIT_ROLLBACK_TO_OPERATOR = 6;
|
|
13
|
+
const SUBMIT_COUNTERSIGN_DISAGREE = 20;
|
|
14
|
+
export class JeeflowFacade {
|
|
15
|
+
engine;
|
|
16
|
+
repo;
|
|
17
|
+
extRepo;
|
|
18
|
+
constructor(engine, repo, extRepo) {
|
|
19
|
+
this.engine = engine;
|
|
20
|
+
this.repo = repo;
|
|
21
|
+
this.extRepo = extRepo;
|
|
22
|
+
}
|
|
23
|
+
async flow(action, args = {}) {
|
|
24
|
+
try {
|
|
25
|
+
const data = await this.dispatch(action, args);
|
|
26
|
+
return { code: 0, msg: '成功', data: data ?? null };
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return { code: 99999999, msg: e?.message ?? String(e) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async dispatch(action, args) {
|
|
33
|
+
switch (action) {
|
|
34
|
+
case 'processDefine/startAndExecute':
|
|
35
|
+
case 'processInstance/startAndExecute':
|
|
36
|
+
return this.startAndExecute(args);
|
|
37
|
+
case 'processDefine/deploy':
|
|
38
|
+
case 'processDesign/deploy':
|
|
39
|
+
return this.deploy(args, action === 'processDesign/deploy');
|
|
40
|
+
case 'processDefine/redeploy':
|
|
41
|
+
return this.redeploy(args);
|
|
42
|
+
case 'processDefine/remove':
|
|
43
|
+
return this.repo.removeDefine(toId(args.id));
|
|
44
|
+
case 'processDefine/upAndDown':
|
|
45
|
+
return this.repo.updateDefineState(toId(args.id), toInt(args.state));
|
|
46
|
+
case 'processInstance/withdraw':
|
|
47
|
+
return this.withdraw(args);
|
|
48
|
+
case 'processTask/execute':
|
|
49
|
+
return this.execute(args);
|
|
50
|
+
case 'processDesign/page':
|
|
51
|
+
return this.designPage(args);
|
|
52
|
+
case 'processDesign/detail':
|
|
53
|
+
return this.designDetail(args);
|
|
54
|
+
case 'processDesign/save':
|
|
55
|
+
return this.designSave(args);
|
|
56
|
+
case 'processDesign/remove':
|
|
57
|
+
return this.ext().removeDesign(toId(args.id));
|
|
58
|
+
case 'processSurrogate/page':
|
|
59
|
+
return this.surrogatePage(args);
|
|
60
|
+
case 'processSurrogate/save':
|
|
61
|
+
return this.surrogateSave(args);
|
|
62
|
+
case 'processSurrogate/remove':
|
|
63
|
+
return this.ext().removeSurrogate(toId(args.id));
|
|
64
|
+
default:
|
|
65
|
+
throw new Error(`未知 action: ${action}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ── 流程定义 / 实例 ──────────────────────────────────────────────────────
|
|
69
|
+
async startAndExecute(args) {
|
|
70
|
+
const defineId = toId(args.processDefineId);
|
|
71
|
+
const operator = String(args.operator ?? 'user1');
|
|
72
|
+
const flowArgs = {};
|
|
73
|
+
for (const [k, v] of Object.entries(args)) {
|
|
74
|
+
if (k === 'processDefineId' || k === 'operator')
|
|
75
|
+
continue;
|
|
76
|
+
flowArgs[k] = v;
|
|
77
|
+
}
|
|
78
|
+
const inst = await this.engine.startProcessInstanceById(defineId, operator, flowArgs);
|
|
79
|
+
// startAndExecute:自动完成申请节点(assignee="applicant" → 发起人)
|
|
80
|
+
const doing = await this.repo.findDoingTasks(inst.id);
|
|
81
|
+
for (const task of doing) {
|
|
82
|
+
await this.repo.addTaskActor(task.id, [operator]);
|
|
83
|
+
flowArgs.submitType = SUBMIT_APPLY;
|
|
84
|
+
await this.engine.executeProcessTask(task.id, operator, flowArgs);
|
|
85
|
+
}
|
|
86
|
+
return { processInstanceId: inst.id };
|
|
87
|
+
}
|
|
88
|
+
// deploy 版本管理(对齐 boot3):按 name 查最新定义,存在 version+1 插新记录,否则从 0 起
|
|
89
|
+
async deploy(args, fromDesign) {
|
|
90
|
+
let content;
|
|
91
|
+
if (fromDesign) {
|
|
92
|
+
const designId = toId(args.id);
|
|
93
|
+
const ext = this.ext();
|
|
94
|
+
const design = await ext.findDesignById(designId);
|
|
95
|
+
if (!design)
|
|
96
|
+
throw new Error('流程设计不存在');
|
|
97
|
+
const hisList = await ext.listDesignHis(designId);
|
|
98
|
+
if (hisList.length === 0)
|
|
99
|
+
throw new Error('流程设计没有内容,无法发布');
|
|
100
|
+
content = toStr(hisList[0].content);
|
|
101
|
+
const defineId = await this.saveDeployedDefine(content, args);
|
|
102
|
+
design.isDeployed = 1;
|
|
103
|
+
design.updateUser = String(args.operator ?? 'system');
|
|
104
|
+
await ext.updateDesign(design);
|
|
105
|
+
return { processDefineId: defineId };
|
|
106
|
+
}
|
|
107
|
+
content = toStr(args.content);
|
|
108
|
+
const defineId = await this.saveDeployedDefine(content, args);
|
|
109
|
+
return { processDefineId: defineId };
|
|
110
|
+
}
|
|
111
|
+
async saveDeployedDefine(content, args) {
|
|
112
|
+
let flow;
|
|
113
|
+
try {
|
|
114
|
+
flow = JSON.parse(content);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new Error('流程定义 JSON 解析失败');
|
|
118
|
+
}
|
|
119
|
+
const name = flow?.name;
|
|
120
|
+
if (!name)
|
|
121
|
+
throw new Error('流程定义缺少 name');
|
|
122
|
+
let version = 0;
|
|
123
|
+
const latest = await this.repo.findDefineByName(name);
|
|
124
|
+
if (latest)
|
|
125
|
+
version = (latest.version ?? 0) + 1;
|
|
126
|
+
const operator = String(args.operator ?? 'system');
|
|
127
|
+
const def = {
|
|
128
|
+
id: 0, name, displayName: flow.displayName ?? '', type: flow.type ?? 'approval',
|
|
129
|
+
state: 1, content, version, createTime: new Date(), createUser: operator,
|
|
130
|
+
updateTime: new Date(), updateUser: operator,
|
|
131
|
+
};
|
|
132
|
+
await this.repo.saveDefine(def);
|
|
133
|
+
return def.id;
|
|
134
|
+
}
|
|
135
|
+
async redeploy(args) {
|
|
136
|
+
const defineId = toId(args.processDefineId);
|
|
137
|
+
const content = toStr(args.content);
|
|
138
|
+
let flow;
|
|
139
|
+
try {
|
|
140
|
+
flow = JSON.parse(content);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new Error('流程定义 JSON 解析失败');
|
|
144
|
+
}
|
|
145
|
+
await this.repo.updateDefine({
|
|
146
|
+
id: defineId, name: flow?.name ?? '', displayName: flow?.displayName ?? '',
|
|
147
|
+
type: flow?.type ?? 'approval', state: 1, content, version: 0,
|
|
148
|
+
createTime: new Date(), createUser: '', updateTime: new Date(),
|
|
149
|
+
updateUser: String(args.operator ?? 'system'),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async withdraw(args) {
|
|
153
|
+
const instanceId = toId(args.id);
|
|
154
|
+
const inst = await this.repo.findInstanceById(instanceId);
|
|
155
|
+
if (!inst)
|
|
156
|
+
throw new Error('流程实例不存在');
|
|
157
|
+
// 撤回:废弃全部 doing 任务 + 实例状态(v1.0.1:updateInstance 级联落库)
|
|
158
|
+
const operator = String(args.operator ?? 'user1');
|
|
159
|
+
const now = new Date();
|
|
160
|
+
const abandoned = inst.abandonAllDoing(now);
|
|
161
|
+
inst.reject(now);
|
|
162
|
+
inst.updateUser = operator;
|
|
163
|
+
for (const t of abandoned)
|
|
164
|
+
await this.repo.updateTask(t);
|
|
165
|
+
await this.repo.updateInstance(inst);
|
|
166
|
+
}
|
|
167
|
+
// ── 流程任务 ─────────────────────────────────────────────────────────────
|
|
168
|
+
async execute(args) {
|
|
169
|
+
const taskId = toId(args.processTaskId);
|
|
170
|
+
const operator = String(args.operator ?? 'user1');
|
|
171
|
+
const submitType = toInt(args.submitType ?? SUBMIT_AGREE);
|
|
172
|
+
const flowArgs = {};
|
|
173
|
+
for (const [k, v] of Object.entries(args)) {
|
|
174
|
+
if (k === 'processTaskId' || k === 'operator')
|
|
175
|
+
continue;
|
|
176
|
+
flowArgs[k] = v;
|
|
177
|
+
}
|
|
178
|
+
flowArgs.submitType = submitType;
|
|
179
|
+
// boot3 execute 分发(spec §11.2)
|
|
180
|
+
switch (submitType) {
|
|
181
|
+
case SUBMIT_REJECT:
|
|
182
|
+
await this.engine.executeAndJumpToEnd(taskId, operator, flowArgs);
|
|
183
|
+
break;
|
|
184
|
+
case SUBMIT_ROLLBACK:
|
|
185
|
+
await this.engine.executeAndJumpTask(taskId, operator, flowArgs, '');
|
|
186
|
+
break;
|
|
187
|
+
case SUBMIT_JUMP:
|
|
188
|
+
await this.engine.executeAndJumpTask(taskId, operator, flowArgs, String(args.taskName ?? ''));
|
|
189
|
+
break;
|
|
190
|
+
case SUBMIT_ROLLBACK_TO_OPERATOR:
|
|
191
|
+
await this.engine.executeAndJumpToFirstTaskNode(taskId, operator, flowArgs);
|
|
192
|
+
break;
|
|
193
|
+
case SUBMIT_COUNTERSIGN_DISAGREE:
|
|
194
|
+
flowArgs.countersignDisagreeFlag = 1;
|
|
195
|
+
await this.engine.executeProcessTask(taskId, operator, flowArgs);
|
|
196
|
+
break;
|
|
197
|
+
default: // 0 APPLY / 1 AGREE / 5 重新提交
|
|
198
|
+
await this.engine.executeProcessTask(taskId, operator, flowArgs);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ── 流程设计(需扩展仓储) ───────────────────────────────────────────────
|
|
202
|
+
async designPage(args) {
|
|
203
|
+
const [rows, total] = await this.ext().pageDesigns(toInt(args.pageNum ?? 1), toInt(args.pageSize ?? 10));
|
|
204
|
+
return { rows, recordCount: total };
|
|
205
|
+
}
|
|
206
|
+
async designDetail(args) {
|
|
207
|
+
const ext = this.ext();
|
|
208
|
+
const design = await ext.findDesignById(toId(args.id));
|
|
209
|
+
if (!design)
|
|
210
|
+
throw new Error('流程设计不存在');
|
|
211
|
+
const data = {
|
|
212
|
+
id: design.id, name: design.name, displayName: design.displayName,
|
|
213
|
+
type: design.type, icon: design.icon, isDeployed: design.isDeployed, remark: design.remark,
|
|
214
|
+
};
|
|
215
|
+
const hisList = await ext.listDesignHis(design.id);
|
|
216
|
+
if (hisList.length > 0) {
|
|
217
|
+
try {
|
|
218
|
+
data.jsonObject = JSON.parse(toStr(hisList[0].content));
|
|
219
|
+
}
|
|
220
|
+
catch { /* ignore */ }
|
|
221
|
+
}
|
|
222
|
+
data.his = hisList;
|
|
223
|
+
return data;
|
|
224
|
+
}
|
|
225
|
+
async designSave(args) {
|
|
226
|
+
const ext = this.ext();
|
|
227
|
+
const operator = String(args.operator ?? 'user1');
|
|
228
|
+
const designId = args.id != null ? toId(args.id) : 0;
|
|
229
|
+
let design;
|
|
230
|
+
if (!designId) {
|
|
231
|
+
design = {
|
|
232
|
+
id: 0, name: String(args.name ?? ''), displayName: String(args.displayName ?? ''),
|
|
233
|
+
type: String(args.type ?? 'approval'), icon: String(args.icon ?? ''),
|
|
234
|
+
isDeployed: 0, remark: String(args.remark ?? ''),
|
|
235
|
+
createTime: new Date(), createUser: operator,
|
|
236
|
+
updateTime: new Date(), updateUser: operator,
|
|
237
|
+
};
|
|
238
|
+
await ext.saveDesign(design);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const found = await ext.findDesignById(designId);
|
|
242
|
+
if (!found)
|
|
243
|
+
throw new Error('流程设计不存在');
|
|
244
|
+
if (args.displayName != null)
|
|
245
|
+
found.displayName = String(args.displayName);
|
|
246
|
+
if (args.type != null)
|
|
247
|
+
found.type = String(args.type);
|
|
248
|
+
if (args.icon != null)
|
|
249
|
+
found.icon = String(args.icon);
|
|
250
|
+
if (args.remark != null)
|
|
251
|
+
found.remark = String(args.remark);
|
|
252
|
+
found.updateUser = operator;
|
|
253
|
+
await ext.updateDesign(found);
|
|
254
|
+
design = found;
|
|
255
|
+
}
|
|
256
|
+
// 内容快照(设计稿内容存历史表)
|
|
257
|
+
if (args.content != null) {
|
|
258
|
+
await ext.saveDesignHis({
|
|
259
|
+
id: 0, processDesignId: design.id, content: String(args.content),
|
|
260
|
+
createTime: new Date(), createUser: operator,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return { id: design.id };
|
|
264
|
+
}
|
|
265
|
+
// ── 委托代理(需扩展仓储) ───────────────────────────────────────────────
|
|
266
|
+
async surrogatePage(args) {
|
|
267
|
+
const filters = args.operator != null ? { operator: String(args.operator) } : undefined;
|
|
268
|
+
const [rows, total] = await this.ext().pageSurrogates(toInt(args.pageNum ?? 1), toInt(args.pageSize ?? 10), filters);
|
|
269
|
+
return { rows, recordCount: total };
|
|
270
|
+
}
|
|
271
|
+
async surrogateSave(args) {
|
|
272
|
+
const ext = this.ext();
|
|
273
|
+
const operator = String(args.operator ?? 'user1');
|
|
274
|
+
const surrogateId = args.id != null ? toId(args.id) : 0;
|
|
275
|
+
let surrogate;
|
|
276
|
+
if (!surrogateId) {
|
|
277
|
+
surrogate = {
|
|
278
|
+
id: 0, operator, // 授权人 = 操作人
|
|
279
|
+
surrogate: String(args.surrogate ?? ''), processName: String(args.processName ?? ''),
|
|
280
|
+
enabled: toInt(args.enabled ?? 1),
|
|
281
|
+
createTime: new Date(), createUser: operator,
|
|
282
|
+
updateTime: new Date(), updateUser: operator,
|
|
283
|
+
};
|
|
284
|
+
await ext.saveSurrogate(surrogate);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
const found = await ext.findSurrogateById(surrogateId);
|
|
288
|
+
if (!found)
|
|
289
|
+
throw new Error('委托记录不存在');
|
|
290
|
+
if (args.surrogate != null)
|
|
291
|
+
found.surrogate = String(args.surrogate);
|
|
292
|
+
if (args.processName != null)
|
|
293
|
+
found.processName = String(args.processName);
|
|
294
|
+
if (args.enabled != null)
|
|
295
|
+
found.enabled = toInt(args.enabled);
|
|
296
|
+
found.updateUser = operator;
|
|
297
|
+
await ext.updateSurrogate(found);
|
|
298
|
+
surrogate = found;
|
|
299
|
+
}
|
|
300
|
+
return { id: surrogate.id };
|
|
301
|
+
}
|
|
302
|
+
ext() {
|
|
303
|
+
if (!this.extRepo)
|
|
304
|
+
throw new Error('未配置 ProcessExtRepository(扩展仓储)');
|
|
305
|
+
return this.extRepo;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// ── 工具 ──────────────────────────────────────────────────────────────────────
|
|
309
|
+
function toStr(v) {
|
|
310
|
+
if (v == null)
|
|
311
|
+
return '';
|
|
312
|
+
if (typeof v === 'string')
|
|
313
|
+
return v;
|
|
314
|
+
if (v instanceof Uint8Array || Buffer.isBuffer(v))
|
|
315
|
+
return Buffer.from(v).toString('utf8');
|
|
316
|
+
return String(v);
|
|
317
|
+
}
|
|
318
|
+
function toId(v) {
|
|
319
|
+
const n = Number(v);
|
|
320
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
321
|
+
throw new Error('id 缺失或非法');
|
|
322
|
+
return n;
|
|
323
|
+
}
|
|
324
|
+
function toInt(v) {
|
|
325
|
+
const n = Number(v);
|
|
326
|
+
if (!Number.isFinite(n))
|
|
327
|
+
throw new Error('数值缺失或非法');
|
|
328
|
+
return n;
|
|
329
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ProcessDesign, ProcessDesignHis, ProcessSurrogate } from '../model.js';
|
|
2
|
+
import type { IDGenerator, ProcessExtRepository } from '../spi.js';
|
|
3
|
+
import { type SqlAdapter } from './shared.js';
|
|
4
|
+
export declare class JdbcProcessExtRepository implements ProcessExtRepository {
|
|
5
|
+
private readonly adapter;
|
|
6
|
+
private readonly idGen;
|
|
7
|
+
constructor(adapter: SqlAdapter, idGen?: IDGenerator);
|
|
8
|
+
private sql;
|
|
9
|
+
private c;
|
|
10
|
+
private done;
|
|
11
|
+
private static DESIGN_COLS;
|
|
12
|
+
findDesignById(id: number): Promise<ProcessDesign | null>;
|
|
13
|
+
saveDesign(d: ProcessDesign): Promise<void>;
|
|
14
|
+
updateDesign(d: ProcessDesign): Promise<void>;
|
|
15
|
+
removeDesign(id: number): Promise<void>;
|
|
16
|
+
pageDesigns(pageNum?: number, pageSize?: number, filters?: Record<string, any>): Promise<[ProcessDesign[], number]>;
|
|
17
|
+
saveDesignHis(his: ProcessDesignHis): Promise<void>;
|
|
18
|
+
listDesignHis(designId: number): Promise<ProcessDesignHis[]>;
|
|
19
|
+
private static SURROGATE_COLS;
|
|
20
|
+
findSurrogateById(id: number): Promise<ProcessSurrogate | null>;
|
|
21
|
+
saveSurrogate(s: ProcessSurrogate): Promise<void>;
|
|
22
|
+
updateSurrogate(s: ProcessSurrogate): Promise<void>;
|
|
23
|
+
removeSurrogate(id: number): Promise<void>;
|
|
24
|
+
pageSurrogates(pageNum?: number, pageSize?: number, filters?: Record<string, any>): Promise<[ProcessSurrogate[], number]>;
|
|
25
|
+
getSurrogate(operator: string, processName: string, at?: Date): Promise<ProcessSurrogate | null>;
|
|
26
|
+
private querySurrogate;
|
|
27
|
+
private mapDesign;
|
|
28
|
+
private mapSurrogate;
|
|
29
|
+
}
|