@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 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
- inst.finish(new Date());
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
- insertSubRow(f, subData, fk, parentPk) {
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
- * 工作流业务数据入库适配拦截器——流程结束同意后,f_ 表单数据写入业务表。
73
+ * 工作流业务数据入库适配拦截器——按流程定义顶层 persistMode 分派:
61
74
  *
62
- * 语义(spec 契约,四语言一致):
63
- * - 时机:结束节点执行后 + 实例 Done + submitType=AGREE(不同意/退回不入库)
64
- * - 字段:实例 Variables f_ 前缀字段,去前缀
65
- * - 表名:流程定义 content 顶层 relTableName,缺省回落流程 name
66
- * - 系统字段:writer 通用字段 + 流程上下文(process_instance_id / apply_user_id /
67
- * apply_dept_id,蛇形列名约定)
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 resolveTableName;
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
- * 工作流业务数据入库适配拦截器——流程结束同意后,f_ 表单数据写入业务表。
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
- * 语义(spec 契约,四语言一致):
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 (!node || node.type !== TypeEnd)
232
+ if (node.type !== TypeEnd)
188
233
  return;
189
- if (!inst || inst.state !== InstanceState.Done)
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
- // 同链重复触发防护(issues/19):最后任务节点与结束节点都会触发后置拦截器,
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
- inst.variables[chainKey] = true;
202
- // 表名:流程定义顶层 relTableName,缺省回落流程 name
203
- const tableName = await this.resolveTableName(inst);
204
- if (!tableName)
205
- return; // 未配置:静默跳过
206
- // 幂等:以 process_instance_id 为键,先查后插
207
- if (await this.writer.exists(tableName, 'process_instance_id', inst.id))
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
- // 流程上下文字段(蛇形列名约定,与 writer 系统字段一致)
217
- data['process_instance_id'] ??= inst.id;
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
- async resolveTableName(inst) {
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 tableName || (meta.name ?? '').trim(); // 缺省回落流程 name
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mldong/jeeflow",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "jeeflow workflow engine — Node.js / TypeScript implementation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",