@mldong/jeeflow 1.7.0 → 1.8.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/dist/engine.js +20 -6
- package/dist/meta.d.ts +7 -0
- package/dist/meta.js +58 -8
- package/dist/persist.d.ts +40 -12
- package/dist/persist.js +161 -43
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TypeStart, TypeEnd, TypeTask, TypeDecision, TypeFork, TypeJoin, TypeCustom, ProcessInstance, TaskState, } from './model.js';
|
|
1
|
+
import { TypeStart, TypeEnd, TypeTask, TypeDecision, TypeFork, TypeJoin, TypeCustom, ProcessInstance, TaskState, SubmitType, } from './model.js';
|
|
2
2
|
import { EventType } from './extensions.js';
|
|
3
3
|
export const KeySubmitType = 'submitType';
|
|
4
4
|
export const KeyBusinessNo = 'BUSINESS_NO';
|
|
@@ -97,6 +97,9 @@ export class EngineImpl {
|
|
|
97
97
|
await this.repo.updateInstance(inst);
|
|
98
98
|
const curNode = findNode(flow, task.taskName);
|
|
99
99
|
if (curNode) {
|
|
100
|
+
// 1.8.0:任务完成节点自身的后置拦截器(SYNC 同步演进——任务节点推进更新状态/字段)。
|
|
101
|
+
// createTask 不再触发(引擎语义修正),此处为完成任务节点的唯一触发点
|
|
102
|
+
await this.firePost(curNode, inst);
|
|
100
103
|
const ct = curNode.properties?.countersignType;
|
|
101
104
|
if (ct === 'SEQUENTIAL') {
|
|
102
105
|
const doing = await this.repo.findDoingTasks(inst.id);
|
|
@@ -209,13 +212,16 @@ export class EngineImpl {
|
|
|
209
212
|
return { task, inst };
|
|
210
213
|
}
|
|
211
214
|
async executeNode(flow, inst, node, operator, vars) {
|
|
215
|
+
// 任务创建(对齐 Java CreateTaskHandler:不触发节点拦截器——创建任务 ≠ 节点执行完成;
|
|
216
|
+
// 任务完成的拦截器由 executeProcessTask 显式触发,1.8.0 SYNC 同步演进)
|
|
217
|
+
if (node.type === TypeTask || node.type === TypeCustom) {
|
|
218
|
+
await this.createTask(node, inst, operator, vars);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
212
221
|
if (!(await this.firePre(node, inst)))
|
|
213
222
|
return;
|
|
214
223
|
try {
|
|
215
224
|
switch (node.type) {
|
|
216
|
-
case TypeTask:
|
|
217
|
-
case TypeCustom:
|
|
218
|
-
return this.createTask(node, inst, operator, vars);
|
|
219
225
|
case TypeDecision:
|
|
220
226
|
return this.evaluateDecision(flow, inst, node, operator, vars);
|
|
221
227
|
case TypeFork:
|
|
@@ -229,12 +235,20 @@ export class EngineImpl {
|
|
|
229
235
|
await this.executeNode(flow, inst, n, operator, vars);
|
|
230
236
|
return;
|
|
231
237
|
}
|
|
232
|
-
case TypeEnd:
|
|
233
|
-
|
|
238
|
+
case TypeEnd: {
|
|
239
|
+
// 对齐 Java EndProcessHandler:submitType=REJECT → reject,否则 finish
|
|
240
|
+
const submitType = inst.variables[KeySubmitType];
|
|
241
|
+
if (submitType != null && Number(submitType) === SubmitType.Reject) {
|
|
242
|
+
inst.reject(new Date());
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
inst.finish(new Date());
|
|
246
|
+
}
|
|
234
247
|
inst.variables = vars;
|
|
235
248
|
await this.repo.updateInstance(inst);
|
|
236
249
|
await this.fireEvent({ type: EventType.ProcessFinish, instanceId: inst.id, operator });
|
|
237
250
|
return;
|
|
251
|
+
}
|
|
238
252
|
}
|
|
239
253
|
}
|
|
240
254
|
finally {
|
package/dist/meta.d.ts
CHANGED
|
@@ -50,10 +50,17 @@ export declare class MetaTableWriter implements DynamicTableWriter {
|
|
|
50
50
|
constructor(base: DynamicTableWriter, provider: IDynamicMetaProvider);
|
|
51
51
|
filterColumns(tableName: string, columns: string[]): string[] | Promise<string[]>;
|
|
52
52
|
insert(tableName: string, data: Record<string, unknown>): unknown;
|
|
53
|
+
/** 按元数据 storageType 组装 SET 列(SYNC 同步演进,issues/24)——
|
|
54
|
+
* NORMAL/JSON/EXPAND 参与更新;ONE2ONE/ONE2MANY 子表不参与中途更新
|
|
55
|
+
* (任务推进只更新主表行状态,子表数据变动走重新提交);未消费字段直通。 */
|
|
56
|
+
update(tableName: string, data: Record<string, unknown>, whereColumn: string, whereValue: unknown): number | Promise<number>;
|
|
53
57
|
exists(tableName: string, bizKey: string, bizKeyValue: unknown): boolean | Promise<boolean>;
|
|
54
58
|
fillSystemFields(data: Record<string, unknown>, isInsert: boolean): void;
|
|
55
59
|
private expandInto;
|
|
56
60
|
private insertSubTable;
|
|
61
|
+
/** 子表单行插入(issues/24):继承主表 apply_user_id(拦截器场景=流程 operator),
|
|
62
|
+
* 子表单显式同名字段优先(??=)——fillSystemFields 的用户列默认值可解析到 operator,
|
|
63
|
+
* 避免 BIGINT create_user/update_user 列回落 "system" 严格模式报错 */
|
|
57
64
|
private insertSubRow;
|
|
58
65
|
}
|
|
59
66
|
/** 业务表查询器——按列等值查询原始行(与写入器职责分离) */
|
package/dist/meta.js
CHANGED
|
@@ -117,12 +117,45 @@ export class MetaTableWriter {
|
|
|
117
117
|
let pk = this.base.insert(tableName, row); // 主表插入(自增/生成器返回主键)
|
|
118
118
|
if (pk === undefined || pk === null)
|
|
119
119
|
pk = findRowValue(row, pkOf(meta));
|
|
120
|
-
//
|
|
120
|
+
// 子表递归插入(外键=主表主键;继承主表 apply_user_id,issues/24)
|
|
121
121
|
for (const [name, v] of subData) {
|
|
122
|
-
this.insertSubTable(meta, findField(meta, name), v, pk);
|
|
122
|
+
this.insertSubTable(meta, findField(meta, name), v, pk, data);
|
|
123
123
|
}
|
|
124
124
|
return pk;
|
|
125
125
|
}
|
|
126
|
+
/** 按元数据 storageType 组装 SET 列(SYNC 同步演进,issues/24)——
|
|
127
|
+
* NORMAL/JSON/EXPAND 参与更新;ONE2ONE/ONE2MANY 子表不参与中途更新
|
|
128
|
+
* (任务推进只更新主表行状态,子表数据变动走重新提交);未消费字段直通。 */
|
|
129
|
+
update(tableName, data, whereColumn, whereValue) {
|
|
130
|
+
const meta = this.provider.loadTableMeta(tableName);
|
|
131
|
+
if (!meta)
|
|
132
|
+
return this.base.update(tableName, data, whereColumn, whereValue); // 无元数据:回落基础 writer
|
|
133
|
+
const row = {};
|
|
134
|
+
for (const f of meta.fields) {
|
|
135
|
+
const v = data[f.name];
|
|
136
|
+
if (v === undefined || v === null)
|
|
137
|
+
continue;
|
|
138
|
+
const st = f.storageType ?? StorageType.Normal;
|
|
139
|
+
if (st === StorageType.Json) {
|
|
140
|
+
row[columnOf(f)] = JSON.stringify(v);
|
|
141
|
+
}
|
|
142
|
+
else if (st === StorageType.Expand) {
|
|
143
|
+
this.expandInto(f, v, row);
|
|
144
|
+
}
|
|
145
|
+
else if (st === StorageType.One2One || st === StorageType.One2Many) {
|
|
146
|
+
continue; // 子表不参与中途更新
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
row[columnOf(f)] = v;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 未消费字段(流程上下文/状态字段等)直通基础 writer
|
|
153
|
+
for (const [k, v] of Object.entries(data)) {
|
|
154
|
+
if (!findField(meta, k))
|
|
155
|
+
row[k] ??= v;
|
|
156
|
+
}
|
|
157
|
+
return this.base.update(tableName, row, whereColumn, whereValue);
|
|
158
|
+
}
|
|
126
159
|
exists(tableName, bizKey, bizKeyValue) {
|
|
127
160
|
return this.base.exists(tableName, bizKey, bizKeyValue);
|
|
128
161
|
}
|
|
@@ -138,25 +171,31 @@ export class MetaTableWriter {
|
|
|
138
171
|
row[col] = obj[sub];
|
|
139
172
|
}
|
|
140
173
|
}
|
|
141
|
-
insertSubTable(parentMeta, f, v, parentPk) {
|
|
174
|
+
insertSubTable(parentMeta, f, v, parentPk, parentData) {
|
|
142
175
|
if (parentPk === undefined || parentPk === null) {
|
|
143
176
|
throw new Error(`persist: parent primary key missing, cannot insert sub table ${f.name}`);
|
|
144
177
|
}
|
|
145
178
|
const fk = f.foreignKey || pkOf(parentMeta);
|
|
146
179
|
const st = f.storageType ?? StorageType.Normal;
|
|
147
180
|
if (st === StorageType.One2One && typeof v === 'object' && v !== null) {
|
|
148
|
-
this.insertSubRow(f, v, fk, parentPk);
|
|
181
|
+
this.insertSubRow(f, v, fk, parentPk, parentData);
|
|
149
182
|
}
|
|
150
183
|
else if (st === StorageType.One2Many && Array.isArray(v)) {
|
|
151
184
|
for (const item of v) {
|
|
152
185
|
if (typeof item === 'object' && item !== null) {
|
|
153
|
-
this.insertSubRow(f, item, fk, parentPk);
|
|
186
|
+
this.insertSubRow(f, item, fk, parentPk, parentData);
|
|
154
187
|
}
|
|
155
188
|
}
|
|
156
189
|
}
|
|
157
190
|
}
|
|
158
|
-
|
|
191
|
+
/** 子表单行插入(issues/24):继承主表 apply_user_id(拦截器场景=流程 operator),
|
|
192
|
+
* 子表单显式同名字段优先(??=)——fillSystemFields 的用户列默认值可解析到 operator,
|
|
193
|
+
* 避免 BIGINT create_user/update_user 列回落 "system" 严格模式报错 */
|
|
194
|
+
insertSubRow(f, subData, fk, parentPk, parentData) {
|
|
159
195
|
const row = { ...subData, [fk]: parentPk };
|
|
196
|
+
if (parentData['apply_user_id'] !== undefined) {
|
|
197
|
+
row['apply_user_id'] ??= parentData['apply_user_id'];
|
|
198
|
+
}
|
|
160
199
|
this.insert(f.targetTable, row); // 递归走子表自身元数据
|
|
161
200
|
}
|
|
162
201
|
}
|
|
@@ -224,9 +263,10 @@ export class MetaTableReader {
|
|
|
224
263
|
result[f.name] = v;
|
|
225
264
|
}
|
|
226
265
|
}
|
|
227
|
-
// 未在元数据中的列带出(key
|
|
266
|
+
// 未在元数据中的列带出(key 统一小写);
|
|
267
|
+
// EXPAND 展开列(挂在某字段 expandFields 映射里,对象形式已带出)不重复平铺(issues/24)
|
|
228
268
|
for (const [k, v] of Object.entries(row)) {
|
|
229
|
-
if (!findFieldByColumn(meta, k))
|
|
269
|
+
if (!findFieldByColumn(meta, k) && !isExpandColumn(meta, k))
|
|
230
270
|
result[k.toLowerCase()] ??= v;
|
|
231
271
|
}
|
|
232
272
|
return result;
|
|
@@ -266,3 +306,13 @@ export function findRowValue(row, columnName) {
|
|
|
266
306
|
}
|
|
267
307
|
return undefined;
|
|
268
308
|
}
|
|
309
|
+
/** 列是否为某字段的 EXPAND 展开列(issues/24:已消费,不重复平铺带出) */
|
|
310
|
+
function isExpandColumn(meta, column) {
|
|
311
|
+
for (const f of meta.fields) {
|
|
312
|
+
for (const col of Object.values(f.expandFields ?? {})) {
|
|
313
|
+
if (col.toLowerCase() === (column ?? '').toLowerCase())
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
package/dist/persist.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export interface DynamicTableWriter {
|
|
|
12
12
|
filterColumns(tableName: string, columns: string[]): string[] | Promise<string[]>;
|
|
13
13
|
/** 参数化 INSERT(按列过滤结果落库),返回生成主键 */
|
|
14
14
|
insert(tableName: string, data: Record<string, unknown>): unknown | Promise<unknown>;
|
|
15
|
+
/** 参数化 UPDATE(按列过滤结果组装 SET;条件列排除,防注入),返回受影响行数
|
|
16
|
+
* (SYNC 同步演进,issues/24) */
|
|
17
|
+
update(tableName: string, data: Record<string, unknown>, whereColumn: string, whereValue: unknown): number | Promise<number>;
|
|
15
18
|
/** 幂等检查:指定业务键(如 process_instance_id)是否已存在 */
|
|
16
19
|
exists(tableName: string, bizKey: string, bizKeyValue: unknown): boolean | Promise<boolean>;
|
|
17
20
|
/** 按配置列名填充系统字段(未配置的列跳过) */
|
|
@@ -49,33 +52,58 @@ export declare class SqliteDynamicTableWriter implements DynamicTableWriter {
|
|
|
49
52
|
/** 在 data 中找匹配指定表列的 key(宽松模式驼峰 key 匹配下划线列) */
|
|
50
53
|
private findDataKey;
|
|
51
54
|
exists(tableName: string, bizKey: string, bizKeyValue: unknown): boolean;
|
|
55
|
+
/** 参数化 UPDATE(SYNC 同步演进,issues/24):列过滤(宽松匹配)+ 条件列排除 +
|
|
56
|
+
* 参数化 SET,返回受影响行数。对齐 Java JdbcDynamicTableWriter.update。 */
|
|
57
|
+
update(tableName: string, data: Record<string, unknown>, whereColumn: string, whereValue: unknown): number;
|
|
52
58
|
fillSystemFields(data: Record<string, unknown>, isInsert: boolean): void;
|
|
53
59
|
/** 默认用户值(issues/19):优先取 data 中已注入的 apply_user_id
|
|
54
60
|
* (拦截器场景 = 流程 operator,BIGINT 用户列表开箱即用),否则回落配置默认值 */
|
|
55
61
|
private resolveDefaultUser;
|
|
56
62
|
}
|
|
57
|
-
/** 流程定义加载器(用于解析 relTableName / 流程 name),通常透传仓库 findDefineById */
|
|
63
|
+
/** 流程定义加载器(用于解析 relTableName / persistMode / 流程 name),通常透传仓库 findDefineById */
|
|
58
64
|
export type DefineLoader = (defineId: number) => Promise<ProcessDefine | null>;
|
|
65
|
+
/** 持久化模式(流程定义顶层 persistMode,缺省 ARCHIVE) */
|
|
66
|
+
export declare const PersistModeArchive = "ARCHIVE";
|
|
67
|
+
export declare const PersistModeSync = "SYNC";
|
|
68
|
+
/** 字段权限值(任务节点 properties.field 的 PERMISSION_{字段名},vben5-wf 机制) */
|
|
69
|
+
export declare const PermReadOnly = 1;
|
|
70
|
+
export declare const PermEdit = 2;
|
|
71
|
+
export declare const PermHidden = 3;
|
|
59
72
|
/**
|
|
60
|
-
*
|
|
73
|
+
* 工作流业务数据入库适配拦截器——按流程定义顶层 persistMode 分派:
|
|
61
74
|
*
|
|
62
|
-
*
|
|
63
|
-
* -
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* - 幂等:bizKey = process_instance_id(先查后插,跨请求有效)
|
|
69
|
-
* - 静默跳过:非结束节点 / 非同意 / 未配置表名 / writer 未注入
|
|
70
|
-
* - 表不存在 = 配置错误 → 显性抛错(快速失败)
|
|
75
|
+
* - ARCHIVE(缺省):流程结束同意后,f_ 表单数据写入业务表(一次落库)
|
|
76
|
+
* - SYNC(1.8.0,issues/24 同步演进):提交申请即入库(start 节点 INSERT 全量),
|
|
77
|
+
* 任务节点推进 UPDATE(f_ 按节点字段权限过滤 + tf_ 冗余 + 状态字段=DOING),
|
|
78
|
+
* 结束节点定稿 UPDATE(最终状态 FINISHED/REJECT)——不管成功失败都入库
|
|
79
|
+
*
|
|
80
|
+
* 对标 Java PersistPostInterceptor(1.8.0)。
|
|
71
81
|
*/
|
|
72
82
|
export declare class PersistPostInterceptor implements FlowInterceptor {
|
|
73
83
|
private writer;
|
|
74
84
|
private loader;
|
|
75
85
|
order: number;
|
|
76
86
|
fieldPrefix: string;
|
|
87
|
+
taskFieldPrefix: string;
|
|
77
88
|
constructor(writer: DynamicTableWriter | null, loader: DefineLoader | null);
|
|
78
89
|
preHandle(_node: FlowNode, _inst: ProcessInstance): Promise<boolean>;
|
|
79
90
|
postHandle(node: FlowNode, inst: ProcessInstance): Promise<void>;
|
|
80
|
-
private
|
|
91
|
+
private handleArchive;
|
|
92
|
+
private handleSync;
|
|
93
|
+
private resolveDefine;
|
|
94
|
+
/** 同链重复触发防护(issues/19,1.8.0 节点级):同一执行链中**每个节点**触发一次
|
|
95
|
+
* (任务推进更新 + 结束定稿是不同节点,都要生效),同节点不重复;exists 兜底跨请求。 */
|
|
96
|
+
private markChain;
|
|
97
|
+
/** 提取字段:f_ 去前缀(SYNC 下按字段权限过滤——只读/隐藏不更新;
|
|
98
|
+
* includeFormFields=false 时不带出,用于非任务节点定稿避免覆盖只读限制);
|
|
99
|
+
* tf_ 去前缀冗余(有列则写,列过滤由 writer 做) */
|
|
100
|
+
private extractFields;
|
|
101
|
+
/** 任务节点字段权限(node.properties.field 的 PERMISSION_x;缺省 null=全部可编辑) */
|
|
102
|
+
private resolveFieldPermission;
|
|
103
|
+
/** 字段可编辑判定:无声明或 EDIT(2) 可更新;READ_ONLY(1)/HIDDEN(3) 不更新 */
|
|
104
|
+
private isEditable;
|
|
105
|
+
/** 状态字段写入:优先 {节点ID}_{状态码} 列,无则 {节点ID} 列(列探测过滤) */
|
|
106
|
+
private putStateField;
|
|
107
|
+
/** 流程上下文字段(蛇形列名约定,与 writer 系统字段一致) */
|
|
108
|
+
private fillContext;
|
|
81
109
|
}
|
package/dist/persist.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KeySubmitType, KeyDeptID } from './engine.js';
|
|
2
|
-
import { TypeEnd, InstanceState, SubmitType } from './model.js';
|
|
2
|
+
import { TypeEnd, TypeTask, TypeCustom, InstanceState, SubmitType } from './model.js';
|
|
3
3
|
// ─── 默认实现:node:sqlite(内置零依赖) ──────────────────────────────────────
|
|
4
4
|
const TABLE_NAME_RE = /^[A-Za-z0-9_]+$/;
|
|
5
5
|
/** 列名归一(issues/20):转小写 + 去下划线(companyName / company_name / COMPANY_NAME 等价) */
|
|
@@ -128,6 +128,30 @@ export class SqliteDynamicTableWriter {
|
|
|
128
128
|
const row = this.db.prepare(`SELECT COUNT(1) AS c FROM ${tableName} WHERE ${bizKey} = ?`).get(bizKeyValue);
|
|
129
129
|
return Number(row?.c ?? 0) > 0;
|
|
130
130
|
}
|
|
131
|
+
/** 参数化 UPDATE(SYNC 同步演进,issues/24):列过滤(宽松匹配)+ 条件列排除 +
|
|
132
|
+
* 参数化 SET,返回受影响行数。对齐 Java JdbcDynamicTableWriter.update。 */
|
|
133
|
+
update(tableName, data, whereColumn, whereValue) {
|
|
134
|
+
checkTableName(tableName);
|
|
135
|
+
if (!whereColumn)
|
|
136
|
+
throw new Error(`persist: update ${tableName} requires where column`);
|
|
137
|
+
const cols = this.tableColumns(tableName);
|
|
138
|
+
const sets = [];
|
|
139
|
+
const values = [];
|
|
140
|
+
for (const m of cols) {
|
|
141
|
+
if (normalizeColumn(m.name) === normalizeColumn(whereColumn))
|
|
142
|
+
continue; // 条件列不参与 SET
|
|
143
|
+
const key = this.findDataKey(data, m.name);
|
|
144
|
+
if (key) {
|
|
145
|
+
sets.push(`${m.name} = ?`);
|
|
146
|
+
values.push(data[key]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (sets.length === 0)
|
|
150
|
+
return 0; // 无更新列(如结束节点仅状态探测未命中)
|
|
151
|
+
values.push(whereValue);
|
|
152
|
+
const res = this.db.prepare(`UPDATE ${tableName} SET ${sets.join(',')} WHERE ${whereColumn} = ?`).run(...values);
|
|
153
|
+
return Number(res.changes ?? 0);
|
|
154
|
+
}
|
|
131
155
|
fillSystemFields(data, isInsert) {
|
|
132
156
|
const now = nowText();
|
|
133
157
|
if (isInsert) {
|
|
@@ -155,24 +179,29 @@ export class SqliteDynamicTableWriter {
|
|
|
155
179
|
return data['apply_user_id'] ?? this.defaultUserValue;
|
|
156
180
|
}
|
|
157
181
|
}
|
|
182
|
+
/** 持久化模式(流程定义顶层 persistMode,缺省 ARCHIVE) */
|
|
183
|
+
export const PersistModeArchive = 'ARCHIVE'; // 结束归档(现状):流程结束同意后落库
|
|
184
|
+
export const PersistModeSync = 'SYNC'; // 同步演进:发起 INSERT → 任务节点 UPDATE → 结束定稿
|
|
185
|
+
/** 字段权限值(任务节点 properties.field 的 PERMISSION_{字段名},vben5-wf 机制) */
|
|
186
|
+
export const PermReadOnly = 1; // 只读:不更新
|
|
187
|
+
export const PermEdit = 2; // 可编辑:更新
|
|
188
|
+
export const PermHidden = 3; // 隐藏:不更新
|
|
158
189
|
/**
|
|
159
|
-
*
|
|
190
|
+
* 工作流业务数据入库适配拦截器——按流程定义顶层 persistMode 分派:
|
|
191
|
+
*
|
|
192
|
+
* - ARCHIVE(缺省):流程结束同意后,f_ 表单数据写入业务表(一次落库)
|
|
193
|
+
* - SYNC(1.8.0,issues/24 同步演进):提交申请即入库(start 节点 INSERT 全量),
|
|
194
|
+
* 任务节点推进 UPDATE(f_ 按节点字段权限过滤 + tf_ 冗余 + 状态字段=DOING),
|
|
195
|
+
* 结束节点定稿 UPDATE(最终状态 FINISHED/REJECT)——不管成功失败都入库
|
|
160
196
|
*
|
|
161
|
-
*
|
|
162
|
-
* - 时机:结束节点执行后 + 实例 Done + submitType=AGREE(不同意/退回不入库)
|
|
163
|
-
* - 字段:实例 Variables 中 f_ 前缀字段,去前缀
|
|
164
|
-
* - 表名:流程定义 content 顶层 relTableName,缺省回落流程 name
|
|
165
|
-
* - 系统字段:writer 通用字段 + 流程上下文(process_instance_id / apply_user_id /
|
|
166
|
-
* apply_dept_id,蛇形列名约定)
|
|
167
|
-
* - 幂等:bizKey = process_instance_id(先查后插,跨请求有效)
|
|
168
|
-
* - 静默跳过:非结束节点 / 非同意 / 未配置表名 / writer 未注入
|
|
169
|
-
* - 表不存在 = 配置错误 → 显性抛错(快速失败)
|
|
197
|
+
* 对标 Java PersistPostInterceptor(1.8.0)。
|
|
170
198
|
*/
|
|
171
199
|
export class PersistPostInterceptor {
|
|
172
200
|
writer;
|
|
173
201
|
loader;
|
|
174
202
|
order = 0;
|
|
175
203
|
fieldPrefix = 'f_';
|
|
204
|
+
taskFieldPrefix = 'tf_';
|
|
176
205
|
constructor(writer, loader) {
|
|
177
206
|
this.writer = writer;
|
|
178
207
|
this.loader = loader;
|
|
@@ -183,57 +212,146 @@ export class PersistPostInterceptor {
|
|
|
183
212
|
async postHandle(node, inst) {
|
|
184
213
|
if (!this.writer || !this.loader)
|
|
185
214
|
return; // 未注入:静默跳过
|
|
215
|
+
if (!node || !inst)
|
|
216
|
+
return;
|
|
217
|
+
const { tableName, persistMode } = await this.resolveDefine(inst);
|
|
218
|
+
if (!tableName)
|
|
219
|
+
return; // 未配置:静默跳过
|
|
220
|
+
if (persistMode.toUpperCase() === PersistModeSync) {
|
|
221
|
+
await this.handleSync(node, inst, tableName);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
await this.handleArchive(node, inst, tableName);
|
|
225
|
+
}
|
|
226
|
+
// ─── ARCHIVE(现状:结束同意归档) ──────────────────────────────────────────
|
|
227
|
+
async handleArchive(node, inst, tableName) {
|
|
228
|
+
const writer = this.writer;
|
|
229
|
+
if (!writer)
|
|
230
|
+
return;
|
|
186
231
|
// 时机:仅结束节点 + 流程正常完成(Done)+ 同意
|
|
187
|
-
if (
|
|
232
|
+
if (node.type !== TypeEnd)
|
|
188
233
|
return;
|
|
189
|
-
if (
|
|
234
|
+
if (inst.state !== InstanceState.Done)
|
|
190
235
|
return;
|
|
191
236
|
const submitType = Number(inst.variables[KeySubmitType]);
|
|
192
237
|
if (submitType !== SubmitType.Agree)
|
|
193
238
|
return;
|
|
194
|
-
|
|
195
|
-
// 同一执行链(共享 inst.variables)只插一次。标记写入时实例已完成持久化
|
|
196
|
-
// (引擎 executeNode 先 updateInstance 后触发拦截器,repo 存副本)不会落库;
|
|
197
|
-
// exists 保留作为跨请求/重启的幂等兜底(先查后插语义不变)。
|
|
198
|
-
const chainKey = `__persist_executed_${inst.id}`;
|
|
199
|
-
if (inst.variables[chainKey] === true)
|
|
239
|
+
if (!this.markChain(node, inst))
|
|
200
240
|
return;
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
241
|
+
// 幂等:以 process_instance_id 为键,先查后插。
|
|
242
|
+
// 表不存在等探测失败是配置错误,必须显性暴露(与 Java/Python 抛异常一致)
|
|
243
|
+
if (await writer.exists(tableName, 'process_instance_id', inst.id))
|
|
244
|
+
return;
|
|
245
|
+
const data = this.extractFields(inst, null, false, true); // 只 f_ 全量
|
|
246
|
+
this.fillContext(data, inst);
|
|
247
|
+
writer.fillSystemFields(data, true);
|
|
248
|
+
await writer.insert(tableName, data);
|
|
249
|
+
}
|
|
250
|
+
// ─── SYNC(1.8.0 同步演进:发起入库 → 节点推进 → 结束定稿) ──────────────────
|
|
251
|
+
async handleSync(node, inst, tableName) {
|
|
252
|
+
const writer = this.writer;
|
|
253
|
+
if (!writer)
|
|
254
|
+
return;
|
|
255
|
+
if (!this.markChain(node, inst))
|
|
256
|
+
return; // 同链同节点不重复(节点级,issues/19 演进)
|
|
257
|
+
const exists = await writer.exists(tableName, 'process_instance_id', inst.id);
|
|
258
|
+
// 任务节点(TypeTask/TypeCustom)才更新业务字段:f_ 按节点字段权限过滤;
|
|
259
|
+
// 结束/网关等非任务节点只定稿状态,避免全量覆盖任务节点的只读/隐藏限制
|
|
260
|
+
const isTask = node.type === TypeTask || node.type === TypeCustom;
|
|
261
|
+
const fieldPerm = isTask ? this.resolveFieldPermission(node) : null;
|
|
262
|
+
const data = this.extractFields(inst, fieldPerm, !exists || isTask, !exists || isTask);
|
|
263
|
+
// 状态字段:优先 {节点ID}_{状态码} 列,无则 {节点ID} 列。
|
|
264
|
+
// 任务节点写 DOING(10)——任务推进状态;结束节点写实例最终状态(FINISHED/REJECT)
|
|
265
|
+
let stateCode = Number(inst.state);
|
|
266
|
+
if (isTask)
|
|
267
|
+
stateCode = InstanceState.Doing;
|
|
268
|
+
this.putStateField(writer, tableName, data, node.id, stateCode);
|
|
269
|
+
this.fillContext(data, inst);
|
|
270
|
+
if (!exists) {
|
|
271
|
+
writer.fillSystemFields(data, true);
|
|
272
|
+
await writer.insert(tableName, data);
|
|
208
273
|
return;
|
|
209
|
-
// 提取 f_ 前缀字段(去前缀)
|
|
210
|
-
const prefix = this.fieldPrefix || 'f_';
|
|
211
|
-
const data = {};
|
|
212
|
-
for (const [k, v] of Object.entries(inst.variables)) {
|
|
213
|
-
if (k.startsWith(prefix) && k.length > prefix.length)
|
|
214
|
-
data[k.slice(prefix.length)] = v;
|
|
215
274
|
}
|
|
216
|
-
//
|
|
217
|
-
data
|
|
218
|
-
data['apply_user_id'] ??= inst.operator;
|
|
219
|
-
data['apply_dept_id'] ??= inst.variables[KeyDeptID] ?? null;
|
|
220
|
-
// 通用系统字段(writer 按配置列填充)
|
|
221
|
-
this.writer.fillSystemFields(data, true);
|
|
222
|
-
await this.writer.insert(tableName, data);
|
|
275
|
+
writer.fillSystemFields(data, false); // 只填 update 组
|
|
276
|
+
await writer.update(tableName, data, 'process_instance_id', inst.id);
|
|
223
277
|
}
|
|
224
|
-
|
|
278
|
+
// ─── 公共 ───────────────────────────────────────────────────────────────────
|
|
279
|
+
async resolveDefine(inst) {
|
|
225
280
|
const define = await this.loader(inst.defineId);
|
|
226
281
|
if (!define)
|
|
227
|
-
return '';
|
|
282
|
+
return { tableName: '', persistMode: '' };
|
|
228
283
|
const content = typeof define.content === 'string' ? define.content : new TextDecoder().decode(define.content);
|
|
229
284
|
let meta;
|
|
230
285
|
try {
|
|
231
286
|
meta = JSON.parse(content);
|
|
232
287
|
}
|
|
233
288
|
catch {
|
|
234
|
-
return '';
|
|
289
|
+
return { tableName: '', persistMode: '' };
|
|
235
290
|
}
|
|
236
291
|
const tableName = (meta.relTableName ?? '').trim();
|
|
237
|
-
return
|
|
292
|
+
return {
|
|
293
|
+
tableName: tableName || (meta.name ?? '').trim(), // 缺省回落流程 name
|
|
294
|
+
persistMode: (meta.persistMode ?? '').trim(),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/** 同链重复触发防护(issues/19,1.8.0 节点级):同一执行链中**每个节点**触发一次
|
|
298
|
+
* (任务推进更新 + 结束定稿是不同节点,都要生效),同节点不重复;exists 兜底跨请求。 */
|
|
299
|
+
markChain(node, inst) {
|
|
300
|
+
const chainKey = `__persist_executed_${inst.id}_${node.id}`;
|
|
301
|
+
if (inst.variables[chainKey] === true)
|
|
302
|
+
return false;
|
|
303
|
+
inst.variables[chainKey] = true;
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
/** 提取字段:f_ 去前缀(SYNC 下按字段权限过滤——只读/隐藏不更新;
|
|
307
|
+
* includeFormFields=false 时不带出,用于非任务节点定稿避免覆盖只读限制);
|
|
308
|
+
* tf_ 去前缀冗余(有列则写,列过滤由 writer 做) */
|
|
309
|
+
extractFields(inst, fieldPerm, includeTaskFields, includeFormFields) {
|
|
310
|
+
const prefix = this.fieldPrefix || 'f_';
|
|
311
|
+
const taskPrefix = this.taskFieldPrefix || 'tf_';
|
|
312
|
+
const data = {};
|
|
313
|
+
for (const [k, v] of Object.entries(inst.variables)) {
|
|
314
|
+
if (includeFormFields && k.startsWith(prefix) && k.length > prefix.length) {
|
|
315
|
+
const name = k.slice(prefix.length);
|
|
316
|
+
if (!this.isEditable(fieldPerm, name))
|
|
317
|
+
continue;
|
|
318
|
+
data[name] = v;
|
|
319
|
+
}
|
|
320
|
+
else if (includeTaskFields && k.startsWith(taskPrefix) && k.length > taskPrefix.length) {
|
|
321
|
+
data[k.slice(taskPrefix.length)] = v;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return data;
|
|
325
|
+
}
|
|
326
|
+
/** 任务节点字段权限(node.properties.field 的 PERMISSION_x;缺省 null=全部可编辑) */
|
|
327
|
+
resolveFieldPermission(node) {
|
|
328
|
+
const field = node.properties?.field;
|
|
329
|
+
if (field && typeof field === 'object' && Object.keys(field).length > 0) {
|
|
330
|
+
return field;
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
/** 字段可编辑判定:无声明或 EDIT(2) 可更新;READ_ONLY(1)/HIDDEN(3) 不更新 */
|
|
335
|
+
isEditable(fieldPerm, fieldName) {
|
|
336
|
+
if (!fieldPerm)
|
|
337
|
+
return true;
|
|
338
|
+
const perm = fieldPerm[`PERMISSION_${fieldName}`];
|
|
339
|
+
if (perm == null)
|
|
340
|
+
return true;
|
|
341
|
+
return Number(perm) === PermEdit;
|
|
342
|
+
}
|
|
343
|
+
/** 状态字段写入:优先 {节点ID}_{状态码} 列,无则 {节点ID} 列(列探测过滤) */
|
|
344
|
+
putStateField(writer, tableName, data, nodeId, stateCode) {
|
|
345
|
+
if (!nodeId)
|
|
346
|
+
return;
|
|
347
|
+
const kept = writer.filterColumns(tableName, [`${nodeId}_${stateCode}`, nodeId]);
|
|
348
|
+
if (Array.isArray(kept) && kept.length > 0)
|
|
349
|
+
data[kept[0]] = stateCode;
|
|
350
|
+
}
|
|
351
|
+
/** 流程上下文字段(蛇形列名约定,与 writer 系统字段一致) */
|
|
352
|
+
fillContext(data, inst) {
|
|
353
|
+
data['process_instance_id'] ??= inst.id;
|
|
354
|
+
data['apply_user_id'] ??= inst.operator;
|
|
355
|
+
data['apply_dept_id'] ??= inst.variables[KeyDeptID] ?? null;
|
|
238
356
|
}
|
|
239
357
|
}
|