@mldong/jeeflow 1.8.23 → 1.8.26
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.d.ts +3 -2
- package/dist/engine.js +15 -2
- package/dist/extensions.d.ts +4 -1
- package/dist/extensions.js +3 -0
- package/dist/facade.d.ts +7 -0
- package/dist/facade.js +288 -1
- package/dist/jdbc/shared.d.ts +10 -1
- package/dist/jdbc/shared.js +209 -1
- package/dist/memory.d.ts +33 -0
- package/dist/memory.js +198 -4
- package/dist/model.js +1 -1
- package/dist/spi.d.ts +23 -0
- package/package.json +1 -1
package/dist/engine.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ProcessInstance } from './model.js';
|
|
2
2
|
import type { ProcessRepository, UserProvider, IDGenerator, ExpressionEvaluator } from './spi.js';
|
|
3
|
-
import { type EngineExtensions } from './extensions.js';
|
|
3
|
+
import { type EngineExtensions, type ProcessEvent } from './extensions.js';
|
|
4
4
|
import { HandlerRegistry } from './registry.js';
|
|
5
5
|
export declare const KeySubmitType = "submitType";
|
|
6
6
|
export declare const KeyBusinessNo = "BUSINESS_NO";
|
|
@@ -23,6 +23,7 @@ export interface Engine {
|
|
|
23
23
|
executeAndJumpToFirstTaskNode(taskId: string, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
|
|
24
24
|
}
|
|
25
25
|
export declare class EngineImpl implements Engine {
|
|
26
|
+
#private;
|
|
26
27
|
private repo;
|
|
27
28
|
private userProv?;
|
|
28
29
|
private idGen?;
|
|
@@ -43,7 +44,7 @@ export declare class EngineImpl implements Engine {
|
|
|
43
44
|
/** 表达式求值(v1.5.0,门面 highLight 决策分支过滤用) */
|
|
44
45
|
evalExpr(expr: string, vars: Record<string, any>): Promise<any>;
|
|
45
46
|
private firePost;
|
|
46
|
-
|
|
47
|
+
fireEvent(evt: ProcessEvent): Promise<void>;
|
|
47
48
|
startProcessInstanceById(defineId: string, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
|
|
48
49
|
executeProcessTask(taskId: string, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
|
|
49
50
|
executeAndJumpToEnd(taskId: string, operator: string, args?: Record<string, any>): Promise<ProcessInstance>;
|
package/dist/engine.js
CHANGED
|
@@ -97,10 +97,23 @@ export class EngineImpl {
|
|
|
97
97
|
await ic.postHandle(node, inst);
|
|
98
98
|
}
|
|
99
99
|
async fireEvent(evt) {
|
|
100
|
+
// 公开事件发布入口(issues/102 CC_CREATE):facade 层 CC 实例创建后逐抄送人 fire;
|
|
101
|
+
// 无监听器(ext/listeners 为空)时零副作用,与上一版逐字节一致
|
|
102
|
+
await this.#fireEvent(evt);
|
|
103
|
+
}
|
|
104
|
+
async #fireEvent(evt) {
|
|
100
105
|
if (!this.ext?.listeners)
|
|
101
106
|
return;
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
// 兜底语义(issues/104 P2 统一口径):单监听器异常只记录不传播——
|
|
108
|
+
// 不得影响引擎主流程,也不得中断后续监听器(对齐 PHP per-listener catch)
|
|
109
|
+
for (const l of this.ext.listeners) {
|
|
110
|
+
try {
|
|
111
|
+
await l(evt);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
console.error(`[jeeflow] process event listener error: type=${evt.type} instanceId=${evt.instanceId}`, e);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
104
117
|
}
|
|
105
118
|
// ─── Start ─────────────────────────────────────────────────────────────────
|
|
106
119
|
async startProcessInstanceById(defineId, operator, args = {}) {
|
package/dist/extensions.d.ts
CHANGED
|
@@ -22,7 +22,8 @@ export declare enum EventType {
|
|
|
22
22
|
ProcessFinish = 1,
|
|
23
23
|
ProcessReject = 2,
|
|
24
24
|
TaskCreate = 3,
|
|
25
|
-
TaskComplete = 4
|
|
25
|
+
TaskComplete = 4,
|
|
26
|
+
CcCreate = 5
|
|
26
27
|
}
|
|
27
28
|
export interface ProcessEvent {
|
|
28
29
|
type: EventType;
|
|
@@ -30,6 +31,8 @@ export interface ProcessEvent {
|
|
|
30
31
|
taskId?: string;
|
|
31
32
|
nodeId?: string;
|
|
32
33
|
operator: string;
|
|
34
|
+
/** 抄送人 id 直传事件体,监听器免反查 cc 表(issues/102;对齐 Java ccActorId / Go CcActorID) */
|
|
35
|
+
ccActorId?: string;
|
|
33
36
|
}
|
|
34
37
|
export type ProcessEventListener = (event: ProcessEvent) => void | Promise<void>;
|
|
35
38
|
export interface EngineExtensions {
|
package/dist/extensions.js
CHANGED
|
@@ -5,4 +5,7 @@ export var EventType;
|
|
|
5
5
|
EventType[EventType["ProcessReject"] = 2] = "ProcessReject";
|
|
6
6
|
EventType[EventType["TaskCreate"] = 3] = "TaskCreate";
|
|
7
7
|
EventType[EventType["TaskComplete"] = 4] = "TaskComplete";
|
|
8
|
+
// issues/102:抄送知会(对齐 Java CC_CREATE / Go EventCCCreate / Python CC_CREATE / PHP CC_CREATE;
|
|
9
|
+
// 4 号位是活码 TaskComplete,0/1/2/3/4 不重排——码值契约 Go/Node=5)
|
|
10
|
+
EventType[EventType["CcCreate"] = 5] = "CcCreate";
|
|
8
11
|
})(EventType || (EventType = {}));
|
package/dist/facade.d.ts
CHANGED
|
@@ -87,4 +87,11 @@ export declare class JeeflowFacade {
|
|
|
87
87
|
private doneList;
|
|
88
88
|
/** 定义 content 解析为 LogicFlow JSON(issues/05 jsonObject) */
|
|
89
89
|
private parseGraph;
|
|
90
|
+
private static readonly DEFAULT_STATE_IN;
|
|
91
|
+
private static readonly DEFAULT_STATS_LIMIT;
|
|
92
|
+
private static readonly VALID_GRANULARITY;
|
|
93
|
+
private static readonly VALID_DIMENSION;
|
|
94
|
+
private statsOverview;
|
|
95
|
+
private statsTrend;
|
|
96
|
+
private statsGroup;
|
|
90
97
|
}
|
package/dist/facade.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// (code=0 成功 / 99999999 失败)。操作人约定:args.operator 显式传入。
|
|
6
6
|
import { TaskState, } from './model.js';
|
|
7
7
|
import { KeyNextNodeOperator, KeyProcessStartNextNodeOperator, isCountersign } from './engine.js';
|
|
8
|
+
import { EventType } from './extensions.js';
|
|
8
9
|
// submitType 枚举(对齐 boot3)
|
|
9
10
|
const SUBMIT_APPLY = 0;
|
|
10
11
|
const SUBMIT_AGREE = 1;
|
|
@@ -132,6 +133,12 @@ export class JeeflowFacade {
|
|
|
132
133
|
return this.taskAddActor(args);
|
|
133
134
|
case 'processTask/latest':
|
|
134
135
|
return this.taskLatest(args);
|
|
136
|
+
case 'processInstance/stats/overview':
|
|
137
|
+
return this.statsOverview(args);
|
|
138
|
+
case 'processInstance/stats/trend':
|
|
139
|
+
return this.statsTrend(args);
|
|
140
|
+
case 'processInstance/stats/group':
|
|
141
|
+
return this.statsGroup(args);
|
|
135
142
|
default:
|
|
136
143
|
throw new Error(`未知 action: ${action}`);
|
|
137
144
|
}
|
|
@@ -153,6 +160,11 @@ export class JeeflowFacade {
|
|
|
153
160
|
? flowArgs.f_ccActors.split(',').map((x) => x.trim()).filter(Boolean) : [];
|
|
154
161
|
if (ccList.length > 0) {
|
|
155
162
|
await this.repo.createCcInstance(inst.id, operator, ...ccList);
|
|
163
|
+
// issues/102:CC 实例落库后逐抄送人 fire CcCreate(ccActorId 直传事件体,
|
|
164
|
+
// 对齐 Go startAndExecute / Python _startAndExecute;监听器据此落抄送知会 NOTICE)
|
|
165
|
+
for (const actor of ccList) {
|
|
166
|
+
await this.engine.fireEvent({ type: EventType.CcCreate, instanceId: inst.id, operator, ccActorId: actor });
|
|
167
|
+
}
|
|
156
168
|
}
|
|
157
169
|
// startAndExecute:自动完成申请节点(assignee="applicant" → 发起人)
|
|
158
170
|
const doing = await this.repo.findDoingTasks(inst.id);
|
|
@@ -240,7 +252,8 @@ export class JeeflowFacade {
|
|
|
240
252
|
// 撤回:废弃全部 doing 任务 + 实例状态(v1.0.1:updateInstance 级联落库)
|
|
241
253
|
const operator = String(args.operator ?? 'user1');
|
|
242
254
|
const now = new Date();
|
|
243
|
-
// findInstanceById
|
|
255
|
+
// findInstanceById 现水合 tasks(issues/110),此处仍按实例单独查 doing 任务废弃,
|
|
256
|
+
// 且必须把聚合副本重置为仅被废弃项(见下方 inst.tasks = abandoned),防级联回写多余任务
|
|
244
257
|
const abandoned = [];
|
|
245
258
|
for (const t of await this.repo.findDoingTasks(instanceId)) {
|
|
246
259
|
t.abandon(now);
|
|
@@ -807,6 +820,10 @@ export class JeeflowFacade {
|
|
|
807
820
|
if (actors.length === 0)
|
|
808
821
|
throw new Error('actorIds 缺失');
|
|
809
822
|
await this.repo.createCcInstance(instanceId, operator, ...actors);
|
|
823
|
+
// issues/102:手动 CC 与发起路径同语义——逐抄送人 fire CcCreate
|
|
824
|
+
for (const actor of actors) {
|
|
825
|
+
await this.engine.fireEvent({ type: EventType.CcCreate, instanceId, operator, ccActorId: actor });
|
|
826
|
+
}
|
|
810
827
|
}
|
|
811
828
|
async updateCCStatus(args) {
|
|
812
829
|
const instanceId = toId(args.processInstanceId);
|
|
@@ -1091,6 +1108,197 @@ export class JeeflowFacade {
|
|
|
1091
1108
|
return undefined;
|
|
1092
1109
|
}
|
|
1093
1110
|
}
|
|
1111
|
+
// ── 统计(v1.8.25,issues/103) ──────────────────────────────────────────
|
|
1112
|
+
static DEFAULT_STATE_IN = [10, 20, 30, 40, 45, 50];
|
|
1113
|
+
static DEFAULT_STATS_LIMIT = 10;
|
|
1114
|
+
static VALID_GRANULARITY = new Set(['hour', 'day', 'week', 'month']);
|
|
1115
|
+
static VALID_DIMENSION = new Set([
|
|
1116
|
+
'state', 'define', 'category', 'approver', 'applicant',
|
|
1117
|
+
'node', 'stuckNode', 'stuckApprover', 'durationBucket',
|
|
1118
|
+
]);
|
|
1119
|
+
async statsOverview(args) {
|
|
1120
|
+
const start = parseSurrogateTime(args.start);
|
|
1121
|
+
const end = parseSurrogateTime(args.end);
|
|
1122
|
+
const stateIn = args.stateIn?.length ? args.stateIn.map(Number) : JeeflowFacade.DEFAULT_STATE_IN;
|
|
1123
|
+
const insts = await this.repo.queryInstancesForStats(stateIn, start, end);
|
|
1124
|
+
const total = insts.length;
|
|
1125
|
+
const inProgress = insts.filter(r => r.state === 10).length;
|
|
1126
|
+
const completed = insts.filter(r => r.state === 20).length;
|
|
1127
|
+
const withdrawn = insts.filter(r => r.state === 30).length;
|
|
1128
|
+
const rejected = insts.filter(r => r.state === 45).length;
|
|
1129
|
+
const suspended = insts.filter(r => r.state === 50).length;
|
|
1130
|
+
const now = new Date();
|
|
1131
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1132
|
+
const todayEnd = new Date(todayStart.getTime() + 86400000);
|
|
1133
|
+
// E:todayNew 恒按服务器当日、不过滤 state / 不受 stateIn 影响(对齐内置线 countTodayNew)
|
|
1134
|
+
const todayInsts = await this.repo.queryInstancesForStats(null, todayStart, todayEnd);
|
|
1135
|
+
const todayNew = todayInsts.length;
|
|
1136
|
+
const [pending, overdue] = await this.repo.statsPendingAndOverdueCount();
|
|
1137
|
+
const avgDur = await this.repo.statsAvgCompletedDurationSeconds(start, end);
|
|
1138
|
+
const [csTotal, csCount, onTime, onTimeDenom] = await this.repo.statsCompletedTaskAggregate();
|
|
1139
|
+
const countersignRate = csTotal > 0 ? statsRound4(csCount / csTotal) : 0;
|
|
1140
|
+
const onTimeRate = onTimeDenom > 0 ? statsRound4(onTime / onTimeDenom) : 0;
|
|
1141
|
+
const rejectRate = statsRound4(rejected / Math.max(1, completed + rejected));
|
|
1142
|
+
return {
|
|
1143
|
+
total, inProgress, completed, rejected, withdrawn, suspended,
|
|
1144
|
+
todayNew, avgDurationSeconds: avgDur,
|
|
1145
|
+
rejectRate, pendingTaskCount: pending,
|
|
1146
|
+
overdueTaskCount: overdue, countersignRate, onTimeRate,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
async statsTrend(args) {
|
|
1150
|
+
const granularity = String(args.granularity ?? '');
|
|
1151
|
+
if (!JeeflowFacade.VALID_GRANULARITY.has(granularity)) {
|
|
1152
|
+
throw new Error(`不支持的 granularity: ${granularity}`);
|
|
1153
|
+
}
|
|
1154
|
+
const start = parseSurrogateTime(args.start);
|
|
1155
|
+
const end = parseSurrogateTime(args.end);
|
|
1156
|
+
// C:start/end 必填(对齐内置线 20010012 缺参语义),不静默回退不限时间
|
|
1157
|
+
if (!start || !end) {
|
|
1158
|
+
throw new Error('trend 缺少必填参数:start/end/granularity');
|
|
1159
|
+
}
|
|
1160
|
+
// 实例侧无 state 过滤(对齐内置线 countInstanceStartedByBucket)
|
|
1161
|
+
const insts = await this.repo.queryInstancesForStats(null, start, end);
|
|
1162
|
+
const doneTasks = await this.repo.queryTasksForStats(20 /* TaskState.Done */, start, end);
|
|
1163
|
+
const buckets = statsEnumerateBuckets(start, end, granularity);
|
|
1164
|
+
const startedMap = new Map();
|
|
1165
|
+
for (const r of insts) {
|
|
1166
|
+
const ct = parseSurrogateTime(r.createTime);
|
|
1167
|
+
if (ct) {
|
|
1168
|
+
const bk = statsBucketKey(ct, granularity);
|
|
1169
|
+
startedMap.set(bk, (startedMap.get(bk) ?? 0) + 1);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const finishedMap = new Map();
|
|
1173
|
+
for (const r of doneTasks) {
|
|
1174
|
+
const ft = parseSurrogateTime(r.finishTime);
|
|
1175
|
+
if (ft) {
|
|
1176
|
+
const bk = statsBucketKey(ft, granularity);
|
|
1177
|
+
finishedMap.set(bk, (finishedMap.get(bk) ?? 0) + 1);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const series = buckets.map(b => ({
|
|
1181
|
+
bucket: b, started: startedMap.get(b) ?? 0, finished: finishedMap.get(b) ?? 0,
|
|
1182
|
+
}));
|
|
1183
|
+
// A:data 本体为裸数组(去掉 {granularity, series} 包装,对齐契约 spec 06 §4.2 / 内置线)
|
|
1184
|
+
return series;
|
|
1185
|
+
}
|
|
1186
|
+
async statsGroup(args) {
|
|
1187
|
+
const dimension = String(args.dimension ?? '');
|
|
1188
|
+
if (!JeeflowFacade.VALID_DIMENSION.has(dimension)) {
|
|
1189
|
+
throw new Error(`不支持的 dimension: ${dimension}`);
|
|
1190
|
+
}
|
|
1191
|
+
const start = parseSurrogateTime(args.start);
|
|
1192
|
+
const end = parseSurrogateTime(args.end);
|
|
1193
|
+
const limit = args.limit ? Number(args.limit) : JeeflowFacade.DEFAULT_STATS_LIMIT;
|
|
1194
|
+
let rows;
|
|
1195
|
+
if (dimension === 'define') {
|
|
1196
|
+
const raw = await this.repo.statsDefineGroup(start, end, limit);
|
|
1197
|
+
rows = raw.map(r => ({ key: r.key, label: r.label ?? null, count: r.count, avgDurationSeconds: r.avgDurationSeconds ?? null }));
|
|
1198
|
+
}
|
|
1199
|
+
else if (dimension === 'state') {
|
|
1200
|
+
// 无 state 过滤(对齐内置线 groupByDimension:仅按时间限定,契约 group 无 stateIn 入参)
|
|
1201
|
+
const insts = await this.repo.queryInstancesForStats(null, start, end);
|
|
1202
|
+
const grouped = new Map();
|
|
1203
|
+
for (const r of insts) {
|
|
1204
|
+
const k = String(r.state);
|
|
1205
|
+
grouped.set(k, (grouped.get(k) ?? 0) + 1);
|
|
1206
|
+
}
|
|
1207
|
+
const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
1208
|
+
rows = entries.map(([k, c]) => ({ key: k, label: null, count: c, avgDurationSeconds: null }));
|
|
1209
|
+
}
|
|
1210
|
+
else if (dimension === 'category') {
|
|
1211
|
+
const insts = await this.repo.queryInstancesForStats(null, start, end);
|
|
1212
|
+
const defineTypes = new Map();
|
|
1213
|
+
for (const r of insts) {
|
|
1214
|
+
if (!defineTypes.has(r.defineId)) {
|
|
1215
|
+
const def = await this.repo.findDefineById(r.defineId);
|
|
1216
|
+
defineTypes.set(r.defineId, def?.type ?? '');
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const grouped = new Map();
|
|
1220
|
+
for (const r of insts) {
|
|
1221
|
+
const tp = defineTypes.get(r.defineId) ?? '';
|
|
1222
|
+
grouped.set(tp, (grouped.get(tp) ?? 0) + 1);
|
|
1223
|
+
}
|
|
1224
|
+
const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
1225
|
+
rows = entries.map(([k, c]) => ({ key: k, label: null, count: c, avgDurationSeconds: null }));
|
|
1226
|
+
}
|
|
1227
|
+
else if (dimension === 'approver') {
|
|
1228
|
+
const tasks = await this.repo.queryTasksForStats(20, start, end);
|
|
1229
|
+
const grouped = new Map();
|
|
1230
|
+
for (const r of tasks) {
|
|
1231
|
+
if (!r.operator)
|
|
1232
|
+
continue;
|
|
1233
|
+
grouped.set(r.operator, (grouped.get(r.operator) ?? 0) + 1);
|
|
1234
|
+
}
|
|
1235
|
+
const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
1236
|
+
rows = entries.map(([k, c]) => ({ key: k, label: null, count: c, avgDurationSeconds: null }));
|
|
1237
|
+
}
|
|
1238
|
+
else if (dimension === 'applicant') {
|
|
1239
|
+
const insts = await this.repo.queryInstancesForStats(null, start, end);
|
|
1240
|
+
const grouped = new Map();
|
|
1241
|
+
for (const r of insts) {
|
|
1242
|
+
if (!r.operator)
|
|
1243
|
+
continue;
|
|
1244
|
+
grouped.set(r.operator, (grouped.get(r.operator) ?? 0) + 1);
|
|
1245
|
+
}
|
|
1246
|
+
const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
1247
|
+
rows = entries.map(([k, c]) => ({ key: k, label: null, count: c, avgDurationSeconds: null }));
|
|
1248
|
+
}
|
|
1249
|
+
else if (dimension === 'node') {
|
|
1250
|
+
const tasks = await this.repo.queryTasksForStats(20, start, end);
|
|
1251
|
+
const nodeAgg = new Map();
|
|
1252
|
+
for (const r of tasks) {
|
|
1253
|
+
if (!r.displayName)
|
|
1254
|
+
continue;
|
|
1255
|
+
let dur = 0;
|
|
1256
|
+
const ft = parseSurrogateTime(r.finishTime);
|
|
1257
|
+
const ct = parseSurrogateTime(r.createTime);
|
|
1258
|
+
if (ft && ct)
|
|
1259
|
+
dur = Math.floor((ft.getTime() - ct.getTime()) / 1000);
|
|
1260
|
+
const agg = nodeAgg.get(r.displayName) ?? { count: 0, totalDur: 0 };
|
|
1261
|
+
agg.count++;
|
|
1262
|
+
agg.totalDur += dur;
|
|
1263
|
+
nodeAgg.set(r.displayName, agg);
|
|
1264
|
+
}
|
|
1265
|
+
const entries = [...nodeAgg.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, limit);
|
|
1266
|
+
rows = entries.map(([name, agg]) => ({
|
|
1267
|
+
key: name, label: null, count: agg.count,
|
|
1268
|
+
avgDurationSeconds: agg.count > 0 ? Math.round(agg.totalDur / agg.count) : null,
|
|
1269
|
+
}));
|
|
1270
|
+
}
|
|
1271
|
+
else if (dimension === 'stuckNode') {
|
|
1272
|
+
const raw = await this.repo.statsStuckNodeGroup(limit);
|
|
1273
|
+
rows = raw.map(r => ({ key: r.key, label: r.label ?? null, count: r.count, avgDurationSeconds: r.avgDurationSeconds ?? null }));
|
|
1274
|
+
}
|
|
1275
|
+
else if (dimension === 'stuckApprover') {
|
|
1276
|
+
const raw = await this.repo.statsStuckApproverGroup(limit);
|
|
1277
|
+
rows = raw.map(r => ({ key: r.key, label: r.label ?? null, count: r.count, avgDurationSeconds: r.avgDurationSeconds ?? null }));
|
|
1278
|
+
}
|
|
1279
|
+
else if (dimension === 'durationBucket') {
|
|
1280
|
+
const durations = await this.repo.statsCompletedInstanceDurations(start, end);
|
|
1281
|
+
let sameDay = 0, d1to3 = 0, d3to7 = 0, over7d = 0;
|
|
1282
|
+
for (const dur of durations) {
|
|
1283
|
+
if (dur < 86400)
|
|
1284
|
+
sameDay++;
|
|
1285
|
+
else if (dur < 259200)
|
|
1286
|
+
d1to3++;
|
|
1287
|
+
else if (dur < 604800)
|
|
1288
|
+
d3to7++;
|
|
1289
|
+
else
|
|
1290
|
+
over7d++;
|
|
1291
|
+
}
|
|
1292
|
+
const keys = ['sameDay', '1to3d', '3to7d', 'over7d'];
|
|
1293
|
+
const counts = [sameDay, d1to3, d3to7, over7d];
|
|
1294
|
+
rows = keys.map((k, i) => ({ key: k, label: null, count: counts[i], avgDurationSeconds: null }));
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
rows = [];
|
|
1298
|
+
}
|
|
1299
|
+
// A:data 本体为裸数组(去掉 {dimension, rows} 包装,对齐契约 spec 06 §4.2 / 内置线)
|
|
1300
|
+
return rows;
|
|
1301
|
+
}
|
|
1094
1302
|
}
|
|
1095
1303
|
// ── 行转 Map(issues/05-2 列表字段契约 + 05-3 时间格式)─────────────────────
|
|
1096
1304
|
/** issues/15:取 vars 中 prefix 前缀字段,输出「带前缀 + 去前缀副本」(对齐 boot3 getFormData) */
|
|
@@ -1304,3 +1512,82 @@ function pageData(pageNum, pageSize, total, rows) {
|
|
|
1304
1512
|
totalPage = Math.ceil(total / ps);
|
|
1305
1513
|
return { pageNum: pn, pageSize: ps, recordCount: total, totalPage, rows };
|
|
1306
1514
|
}
|
|
1515
|
+
// ═══ 统计辅助(v1.8.25,issues/103) ═══
|
|
1516
|
+
function statsRound4(v) {
|
|
1517
|
+
return Math.round(v * 10000) / 10000;
|
|
1518
|
+
}
|
|
1519
|
+
function statsWeekKey(d) {
|
|
1520
|
+
const iso = isoWeek(d);
|
|
1521
|
+
return `${iso[0]}-W${String(iso[1]).padStart(2, '0')}`;
|
|
1522
|
+
}
|
|
1523
|
+
function isoWeek(d) {
|
|
1524
|
+
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
1525
|
+
const dayNum = date.getUTCDay() || 7;
|
|
1526
|
+
date.setUTCDate(date.getUTCDate() + 4 - dayNum);
|
|
1527
|
+
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
|
1528
|
+
const weekNo = Math.ceil((((date.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
|
1529
|
+
return [date.getUTCFullYear(), weekNo];
|
|
1530
|
+
}
|
|
1531
|
+
function statsEnumerateBuckets(start, end, granularity) {
|
|
1532
|
+
const now = new Date();
|
|
1533
|
+
const s = start ?? new Date(now.getTime() - 30 * 86400000);
|
|
1534
|
+
const e = end ?? now;
|
|
1535
|
+
const buckets = [];
|
|
1536
|
+
if (granularity === 'hour') {
|
|
1537
|
+
const cursor = new Date(s.getFullYear(), s.getMonth(), s.getDate(), s.getHours(), 0, 0);
|
|
1538
|
+
while (cursor <= e) {
|
|
1539
|
+
buckets.push(formatHour(cursor));
|
|
1540
|
+
cursor.setHours(cursor.getHours() + 1);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
else if (granularity === 'day') {
|
|
1544
|
+
const cursor = new Date(s.getFullYear(), s.getMonth(), s.getDate());
|
|
1545
|
+
const endDay = new Date(e.getFullYear(), e.getMonth(), e.getDate());
|
|
1546
|
+
while (cursor <= endDay) {
|
|
1547
|
+
buckets.push(formatDay(cursor));
|
|
1548
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
else if (granularity === 'week') {
|
|
1552
|
+
const cursor = new Date(s.getFullYear(), s.getMonth(), s.getDate());
|
|
1553
|
+
const weekday = cursor.getDay() || 7;
|
|
1554
|
+
cursor.setDate(cursor.getDate() - (weekday - 1));
|
|
1555
|
+
const endDay = new Date(e.getFullYear(), e.getMonth(), e.getDate());
|
|
1556
|
+
while (cursor <= endDay) {
|
|
1557
|
+
buckets.push(statsWeekKey(cursor));
|
|
1558
|
+
cursor.setDate(cursor.getDate() + 7);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
else if (granularity === 'month') {
|
|
1562
|
+
let year = s.getFullYear();
|
|
1563
|
+
let month = s.getMonth();
|
|
1564
|
+
const endYear = e.getFullYear();
|
|
1565
|
+
const endMonth = e.getMonth();
|
|
1566
|
+
while (year < endYear || (year === endYear && month <= endMonth)) {
|
|
1567
|
+
buckets.push(`${year}-${String(month + 1).padStart(2, '0')}`);
|
|
1568
|
+
month++;
|
|
1569
|
+
if (month > 11) {
|
|
1570
|
+
month = 0;
|
|
1571
|
+
year++;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return buckets;
|
|
1576
|
+
}
|
|
1577
|
+
function statsBucketKey(d, granularity) {
|
|
1578
|
+
if (granularity === 'hour')
|
|
1579
|
+
return formatHour(d);
|
|
1580
|
+
if (granularity === 'day')
|
|
1581
|
+
return formatDay(d);
|
|
1582
|
+
if (granularity === 'week')
|
|
1583
|
+
return statsWeekKey(d);
|
|
1584
|
+
if (granularity === 'month')
|
|
1585
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
|
1586
|
+
return '';
|
|
1587
|
+
}
|
|
1588
|
+
function formatHour(d) {
|
|
1589
|
+
return `${formatDay(d)} ${String(d.getHours()).padStart(2, '0')}:00`;
|
|
1590
|
+
}
|
|
1591
|
+
function formatDay(d) {
|
|
1592
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
1593
|
+
}
|
package/dist/jdbc/shared.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProcessInstance, ProcessTask, type ProcessDefine, type CcInstanceRow, type DefineRow, type InstanceRow, type TaskRow } from '../model.js';
|
|
2
|
-
import type { IDGenerator, ProcessRepository, QueryCondition } from '../spi.js';
|
|
2
|
+
import type { IDGenerator, ProcessRepository, QueryCondition, InstanceStatsRow, TaskStatsRow } from '../spi.js';
|
|
3
3
|
/** 默认 ID 生成器:时间戳毫秒 + 同毫秒递增序号(对齐 Java nextId 默认实现)——
|
|
4
4
|
* issue 38 E9:返回 string(数字值在 2^53 内精确,转字符串统一承载) */
|
|
5
5
|
export declare class TsIDGenerator implements IDGenerator {
|
|
@@ -95,4 +95,13 @@ export declare class JdbcRepository implements ProcessRepository {
|
|
|
95
95
|
total: number;
|
|
96
96
|
}>;
|
|
97
97
|
private mapCcRow;
|
|
98
|
+
queryInstancesForStats(stateIn?: number[] | null, start?: Date | null, end?: Date | null): Promise<InstanceStatsRow[]>;
|
|
99
|
+
queryTasksForStats(taskState?: number, start?: Date | null, end?: Date | null): Promise<TaskStatsRow[]>;
|
|
100
|
+
statsPendingAndOverdueCount(): Promise<[number, number]>;
|
|
101
|
+
statsCompletedTaskAggregate(): Promise<[number, number, number, number]>;
|
|
102
|
+
statsAvgCompletedDurationSeconds(start?: Date | null, end?: Date | null): Promise<number>;
|
|
103
|
+
statsDefineGroup(start?: Date | null, end?: Date | null, limit?: number): Promise<Record<string, any>[]>;
|
|
104
|
+
statsStuckNodeGroup(limit?: number): Promise<Record<string, any>[]>;
|
|
105
|
+
statsStuckApproverGroup(limit?: number): Promise<Record<string, any>[]>;
|
|
106
|
+
statsCompletedInstanceDurations(start?: Date | null, end?: Date | null): Promise<number[]>;
|
|
98
107
|
}
|
package/dist/jdbc/shared.js
CHANGED
|
@@ -213,7 +213,11 @@ export class JdbcRepository {
|
|
|
213
213
|
operator: row.operator, expireTime: row.expire_time,
|
|
214
214
|
createTime: row.create_time, createUser: rowId(row.create_user),
|
|
215
215
|
updateTime: row.update_time, updateUser: rowId(row.update_user),
|
|
216
|
-
|
|
216
|
+
// issues/110:聚合水合——二次查 wf_process_task 装任务副本(含 actorIds),
|
|
217
|
+
// 对齐 Java findTasksByInstanceId / PHP PdoProcessRepository / C# issues/89;
|
|
218
|
+
// 否则门面 detail 的 tasks/activeTaskList 恒空。
|
|
219
|
+
// 复用 findHistoryTasks(ORDER BY id ASC + 批查 actor,事务内经 c() 复用连接)
|
|
220
|
+
tasks: await this.findHistoryTasks(rowId(row.id)),
|
|
217
221
|
});
|
|
218
222
|
inst.variables = row.variable ? JSON.parse(row.variable) : {};
|
|
219
223
|
return inst;
|
|
@@ -648,4 +652,208 @@ export class JdbcRepository {
|
|
|
648
652
|
defineVersion: Number(r.version ?? 0),
|
|
649
653
|
};
|
|
650
654
|
}
|
|
655
|
+
// ── Stats(issues/103:统计接口契约) ─────────────────────────────────────
|
|
656
|
+
async queryInstancesForStats(stateIn, start, end) {
|
|
657
|
+
// stateIn 空 = 无 state 过滤(对齐内置线:仅 overview 六计数用 stateIn)
|
|
658
|
+
let sql = 'SELECT process_define_id, state, operator, create_time FROM wf_process_instance WHERE 1=1';
|
|
659
|
+
const args = [];
|
|
660
|
+
if (stateIn && stateIn.length) {
|
|
661
|
+
sql += ` AND state IN (${repeatPh(stateIn.length)})`;
|
|
662
|
+
args.push(...stateIn);
|
|
663
|
+
}
|
|
664
|
+
if (start) {
|
|
665
|
+
sql += ' AND create_time >= ?';
|
|
666
|
+
args.push(start);
|
|
667
|
+
}
|
|
668
|
+
if (end) {
|
|
669
|
+
sql += ' AND create_time < DATE_ADD(?, INTERVAL 1 SECOND)';
|
|
670
|
+
args.push(end);
|
|
671
|
+
}
|
|
672
|
+
sql += ' ORDER BY create_time';
|
|
673
|
+
const conn = await this.c();
|
|
674
|
+
try {
|
|
675
|
+
const rows = await conn.fetchAll(this.sql(sql), args);
|
|
676
|
+
return rows.map((r) => ({
|
|
677
|
+
defineId: rowId(r.process_define_id), state: r.state,
|
|
678
|
+
operator: r.operator ?? '', createTime: r.create_time,
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
finally {
|
|
682
|
+
await this.done(conn);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async queryTasksForStats(taskState, start, end) {
|
|
686
|
+
let sql = 'SELECT operator, display_name, perform_type, create_time, finish_time, expire_time FROM wf_process_task WHERE 1=1';
|
|
687
|
+
const args = [];
|
|
688
|
+
if (taskState != null) {
|
|
689
|
+
sql += ' AND task_state = ?';
|
|
690
|
+
args.push(taskState);
|
|
691
|
+
}
|
|
692
|
+
if (start) {
|
|
693
|
+
sql += ' AND finish_time >= ?';
|
|
694
|
+
args.push(start);
|
|
695
|
+
}
|
|
696
|
+
if (end) {
|
|
697
|
+
sql += ' AND finish_time < DATE_ADD(?, INTERVAL 1 SECOND)';
|
|
698
|
+
args.push(end);
|
|
699
|
+
}
|
|
700
|
+
const conn = await this.c();
|
|
701
|
+
try {
|
|
702
|
+
const rows = await conn.fetchAll(this.sql(sql), args);
|
|
703
|
+
return rows.map((r) => ({
|
|
704
|
+
operator: r.operator ?? '', displayName: r.display_name ?? '',
|
|
705
|
+
performType: r.perform_type ?? 0, createTime: r.create_time,
|
|
706
|
+
finishTime: r.finish_time, expireTime: r.expire_time,
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
finally {
|
|
710
|
+
await this.done(conn);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async statsPendingAndOverdueCount() {
|
|
714
|
+
const conn = await this.c();
|
|
715
|
+
try {
|
|
716
|
+
const r1 = await conn.fetchOne(this.sql('SELECT COUNT(*) FROM wf_process_task WHERE task_state = 10'), []);
|
|
717
|
+
const pending = r1 ? Number(Object.values(r1)[0] ?? 0) : 0;
|
|
718
|
+
const now = new Date();
|
|
719
|
+
const r2 = await conn.fetchOne(this.sql('SELECT COUNT(*) FROM wf_process_task WHERE task_state = 10 AND expire_time IS NOT NULL AND expire_time < ?'), [now]);
|
|
720
|
+
const overdue = r2 ? Number(Object.values(r2)[0] ?? 0) : 0;
|
|
721
|
+
return [pending, overdue];
|
|
722
|
+
}
|
|
723
|
+
finally {
|
|
724
|
+
await this.done(conn);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async statsCompletedTaskAggregate() {
|
|
728
|
+
const conn = await this.c();
|
|
729
|
+
try {
|
|
730
|
+
const r = await conn.fetchOne(this.sql('SELECT COUNT(*) AS total, ' +
|
|
731
|
+
'SUM(CASE WHEN perform_type = 1 THEN 1 ELSE 0 END) AS countersign, ' +
|
|
732
|
+
'SUM(CASE WHEN expire_time IS NOT NULL AND finish_time IS NOT NULL AND finish_time <= expire_time THEN 1 ELSE 0 END) AS on_time, ' +
|
|
733
|
+
'SUM(CASE WHEN expire_time IS NOT NULL THEN 1 ELSE 0 END) AS on_time_denom ' +
|
|
734
|
+
'FROM wf_process_task WHERE task_state = 20'), []);
|
|
735
|
+
if (!r)
|
|
736
|
+
return [0, 0, 0, 0];
|
|
737
|
+
return [
|
|
738
|
+
Number(r.total ?? 0),
|
|
739
|
+
Number(r.countersign ?? 0),
|
|
740
|
+
Number(r.on_time ?? 0),
|
|
741
|
+
Number(r.on_time_denom ?? 0),
|
|
742
|
+
];
|
|
743
|
+
}
|
|
744
|
+
finally {
|
|
745
|
+
await this.done(conn);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async statsAvgCompletedDurationSeconds(start, end) {
|
|
749
|
+
let sql = `SELECT COALESCE(ROUND(AVG(
|
|
750
|
+
TIMESTAMPDIFF(SECOND, i.create_time, (
|
|
751
|
+
SELECT MAX(t.finish_time) FROM wf_process_task t WHERE t.process_instance_id = i.id AND t.finish_time IS NOT NULL
|
|
752
|
+
))
|
|
753
|
+
)), 0) AS avg_sec FROM wf_process_instance i WHERE i.state = 20`;
|
|
754
|
+
const args = [];
|
|
755
|
+
if (start) {
|
|
756
|
+
sql += ' AND i.create_time >= ?';
|
|
757
|
+
args.push(start);
|
|
758
|
+
}
|
|
759
|
+
if (end) {
|
|
760
|
+
sql += ' AND i.create_time < DATE_ADD(?, INTERVAL 1 SECOND)';
|
|
761
|
+
args.push(end);
|
|
762
|
+
}
|
|
763
|
+
const conn = await this.c();
|
|
764
|
+
try {
|
|
765
|
+
const r = await conn.fetchOne(this.sql(sql), args);
|
|
766
|
+
if (!r)
|
|
767
|
+
return 0;
|
|
768
|
+
return Number(Object.values(r)[0] ?? 0);
|
|
769
|
+
}
|
|
770
|
+
finally {
|
|
771
|
+
await this.done(conn);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async statsDefineGroup(start, end, limit = 10) {
|
|
775
|
+
// 对齐内置线 mapper:count 全实例(无 state 过滤)、inner join define、
|
|
776
|
+
// avg 仅对 state=20 且有 finish 的实例聚合(MAX(task.finish_time) - create_time)
|
|
777
|
+
let sql = 'SELECT pd.name, pd.display_name, COUNT(*) AS cnt, ' +
|
|
778
|
+
'ROUND(AVG(CASE WHEN i.state = 20 AND sub.maxft IS NOT NULL ' +
|
|
779
|
+
'THEN TIMESTAMPDIFF(SECOND, i.create_time, sub.maxft) END)) AS avg_dur ' +
|
|
780
|
+
'FROM wf_process_instance i ' +
|
|
781
|
+
'JOIN wf_process_define pd ON i.process_define_id = pd.id ' +
|
|
782
|
+
'LEFT JOIN (SELECT process_instance_id, MAX(finish_time) AS maxft ' +
|
|
783
|
+
'FROM wf_process_task GROUP BY process_instance_id) sub ' +
|
|
784
|
+
'ON sub.process_instance_id = i.id ' +
|
|
785
|
+
'WHERE 1=1';
|
|
786
|
+
const args = [];
|
|
787
|
+
if (start) {
|
|
788
|
+
sql += ' AND i.create_time >= ?';
|
|
789
|
+
args.push(start);
|
|
790
|
+
}
|
|
791
|
+
if (end) {
|
|
792
|
+
sql += ' AND i.create_time < DATE_ADD(?, INTERVAL 1 SECOND)';
|
|
793
|
+
args.push(end);
|
|
794
|
+
}
|
|
795
|
+
sql += ' GROUP BY pd.id, pd.name, pd.display_name ORDER BY cnt DESC LIMIT ?';
|
|
796
|
+
args.push(limit);
|
|
797
|
+
const conn = await this.c();
|
|
798
|
+
try {
|
|
799
|
+
const rows = await conn.fetchAll(this.sql(sql), args);
|
|
800
|
+
return rows.map((r) => ({
|
|
801
|
+
key: r.name ?? null, label: r.display_name ?? null,
|
|
802
|
+
count: Number(r.cnt ?? 0),
|
|
803
|
+
avgDurationSeconds: r.avg_dur != null ? Math.round(Number(r.avg_dur)) : null,
|
|
804
|
+
}));
|
|
805
|
+
}
|
|
806
|
+
finally {
|
|
807
|
+
await this.done(conn);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
async statsStuckNodeGroup(limit = 10) {
|
|
811
|
+
const conn = await this.c();
|
|
812
|
+
try {
|
|
813
|
+
const rows = await conn.fetchAll(this.sql('SELECT display_name, COUNT(*) AS cnt FROM wf_process_task ' +
|
|
814
|
+
'WHERE task_state = 10 GROUP BY display_name ORDER BY cnt DESC LIMIT ?'), [limit]);
|
|
815
|
+
return rows.map((r) => ({
|
|
816
|
+
key: r.display_name ?? '', label: null, count: Number(r.cnt ?? 0), avgDurationSeconds: null,
|
|
817
|
+
}));
|
|
818
|
+
}
|
|
819
|
+
finally {
|
|
820
|
+
await this.done(conn);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async statsStuckApproverGroup(limit = 10) {
|
|
824
|
+
const conn = await this.c();
|
|
825
|
+
try {
|
|
826
|
+
const rows = await conn.fetchAll(this.sql('SELECT ta.actor_id, COUNT(DISTINCT t.id) AS cnt FROM wf_process_task_actor ta ' +
|
|
827
|
+
'INNER JOIN wf_process_task t ON ta.process_task_id = t.id ' +
|
|
828
|
+
'WHERE t.task_state = 10 GROUP BY ta.actor_id ORDER BY cnt DESC LIMIT ?'), [limit]);
|
|
829
|
+
return rows.map((r) => ({
|
|
830
|
+
key: rowId(r.actor_id), label: null, count: Number(r.cnt ?? 0), avgDurationSeconds: null,
|
|
831
|
+
}));
|
|
832
|
+
}
|
|
833
|
+
finally {
|
|
834
|
+
await this.done(conn);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async statsCompletedInstanceDurations(start, end) {
|
|
838
|
+
let sql = `SELECT TIMESTAMPDIFF(SECOND, i.create_time, (
|
|
839
|
+
SELECT MAX(t.finish_time) FROM wf_process_task t WHERE t.process_instance_id = i.id AND t.finish_time IS NOT NULL
|
|
840
|
+
)) AS dur FROM wf_process_instance i WHERE i.state = 20`;
|
|
841
|
+
const args = [];
|
|
842
|
+
if (start) {
|
|
843
|
+
sql += ' AND i.create_time >= ?';
|
|
844
|
+
args.push(start);
|
|
845
|
+
}
|
|
846
|
+
if (end) {
|
|
847
|
+
sql += ' AND i.create_time < DATE_ADD(?, INTERVAL 1 SECOND)';
|
|
848
|
+
args.push(end);
|
|
849
|
+
}
|
|
850
|
+
const conn = await this.c();
|
|
851
|
+
try {
|
|
852
|
+
const rows = await conn.fetchAll(this.sql(sql), args);
|
|
853
|
+
return rows.map((r) => Number(r.dur)).filter((v) => v != null && !isNaN(v));
|
|
854
|
+
}
|
|
855
|
+
finally {
|
|
856
|
+
await this.done(conn);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
651
859
|
}
|
package/dist/memory.d.ts
CHANGED
|
@@ -57,4 +57,37 @@ export declare class MemoryRepository implements ProcessRepository {
|
|
|
57
57
|
allDefines(): ProcessDefine[];
|
|
58
58
|
allInstances(): ProcessInstance[];
|
|
59
59
|
allTasks(): ProcessTask[];
|
|
60
|
+
private toDt;
|
|
61
|
+
queryInstancesForStats(stateIn?: number[] | null, start?: Date | null, end?: Date | null): Promise<{
|
|
62
|
+
defineId: string;
|
|
63
|
+
state: number;
|
|
64
|
+
operator: string;
|
|
65
|
+
createTime: Date | string | null;
|
|
66
|
+
}[]>;
|
|
67
|
+
queryTasksForStats(taskState?: number, start?: Date | null, end?: Date | null): Promise<{
|
|
68
|
+
operator: string;
|
|
69
|
+
displayName: string;
|
|
70
|
+
performType: number;
|
|
71
|
+
createTime: Date | string | null;
|
|
72
|
+
finishTime: Date | string | null;
|
|
73
|
+
expireTime: Date | string | null;
|
|
74
|
+
}[]>;
|
|
75
|
+
statsPendingAndOverdueCount(): Promise<[number, number]>;
|
|
76
|
+
statsCompletedTaskAggregate(): Promise<[number, number, number, number]>;
|
|
77
|
+
statsAvgCompletedDurationSeconds(start?: Date | null, end?: Date | null): Promise<number>;
|
|
78
|
+
statsDefineGroup(start?: Date | null, end?: Date | null, limit?: number): Promise<{
|
|
79
|
+
key: string;
|
|
80
|
+
label: string | null;
|
|
81
|
+
count: number;
|
|
82
|
+
avgDurationSeconds: number | null;
|
|
83
|
+
}[]>;
|
|
84
|
+
statsStuckNodeGroup(limit?: number): Promise<{
|
|
85
|
+
key: string;
|
|
86
|
+
count: number;
|
|
87
|
+
}[]>;
|
|
88
|
+
statsStuckApproverGroup(limit?: number): Promise<{
|
|
89
|
+
key: string;
|
|
90
|
+
count: number;
|
|
91
|
+
}[]>;
|
|
92
|
+
statsCompletedInstanceDurations(start?: Date | null, end?: Date | null): Promise<number[]>;
|
|
60
93
|
}
|
package/dist/memory.js
CHANGED
|
@@ -144,13 +144,13 @@ export class MemoryRepository {
|
|
|
144
144
|
cp.tasks = [];
|
|
145
145
|
this.instances.set(inst.id, cp);
|
|
146
146
|
// v1.0.1:级联保存聚合根内任务状态变更
|
|
147
|
-
for (const t of inst.tasks) {
|
|
147
|
+
for (const t of (inst.tasks ?? [])) {
|
|
148
148
|
if (!t.id)
|
|
149
149
|
continue;
|
|
150
150
|
const tc = cloneTask(t);
|
|
151
151
|
tc.actorIds = [];
|
|
152
152
|
this.tasks.set(t.id, tc);
|
|
153
|
-
if (t.actorIds
|
|
153
|
+
if (t.actorIds?.length)
|
|
154
154
|
this.actors.set(t.id, [...t.actorIds]);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
@@ -183,14 +183,14 @@ export class MemoryRepository {
|
|
|
183
183
|
const cp = cloneTask(task);
|
|
184
184
|
cp.actorIds = [];
|
|
185
185
|
this.tasks.set(task.id, cp);
|
|
186
|
-
if (task.actorIds
|
|
186
|
+
if (task.actorIds?.length)
|
|
187
187
|
this.actors.set(task.id, [...task.actorIds]);
|
|
188
188
|
}
|
|
189
189
|
async updateTask(task) {
|
|
190
190
|
const cp = cloneTask(task);
|
|
191
191
|
cp.actorIds = [];
|
|
192
192
|
this.tasks.set(task.id, cp);
|
|
193
|
-
if (task.actorIds
|
|
193
|
+
if (task.actorIds?.length)
|
|
194
194
|
this.actors.set(task.id, [...task.actorIds]);
|
|
195
195
|
}
|
|
196
196
|
async findDoingTasks(instanceId, taskNames) {
|
|
@@ -375,4 +375,198 @@ export class MemoryRepository {
|
|
|
375
375
|
return cp;
|
|
376
376
|
});
|
|
377
377
|
}
|
|
378
|
+
// ── 统计(v1.8.25,issues/103) ──────────────────────────────────────────
|
|
379
|
+
toDt(v) {
|
|
380
|
+
if (v == null)
|
|
381
|
+
return undefined;
|
|
382
|
+
if (v instanceof Date)
|
|
383
|
+
return isNaN(v.getTime()) ? undefined : v;
|
|
384
|
+
const d = new Date(String(v).replace(' ', 'T'));
|
|
385
|
+
return isNaN(d.getTime()) ? undefined : d;
|
|
386
|
+
}
|
|
387
|
+
async queryInstancesForStats(stateIn, start, end) {
|
|
388
|
+
const sd = this.toDt(start);
|
|
389
|
+
const ed = this.toDt(end);
|
|
390
|
+
const rows = [];
|
|
391
|
+
for (const inst of this.instances.values()) {
|
|
392
|
+
const sv = Number(inst.state);
|
|
393
|
+
// stateIn 空 = 无 state 过滤(对齐内置线:仅 overview 六计数用 stateIn)
|
|
394
|
+
if (stateIn && stateIn.length && !stateIn.includes(sv))
|
|
395
|
+
continue;
|
|
396
|
+
const ct = this.toDt(inst.createTime);
|
|
397
|
+
if (sd && ct && ct < sd)
|
|
398
|
+
continue;
|
|
399
|
+
if (ed && ct && ct > ed)
|
|
400
|
+
continue;
|
|
401
|
+
rows.push({ defineId: String(inst.defineId), state: sv, operator: inst.operator ?? '', createTime: inst.createTime ?? null });
|
|
402
|
+
}
|
|
403
|
+
return rows;
|
|
404
|
+
}
|
|
405
|
+
async queryTasksForStats(taskState, start, end) {
|
|
406
|
+
const sd = this.toDt(start);
|
|
407
|
+
const ed = this.toDt(end);
|
|
408
|
+
const rows = [];
|
|
409
|
+
for (const t of this.tasks.values()) {
|
|
410
|
+
if (taskState != null && Number(t.taskState) !== taskState)
|
|
411
|
+
continue;
|
|
412
|
+
const ft = this.toDt(t.finishTime);
|
|
413
|
+
if (sd && ft && ft < sd)
|
|
414
|
+
continue;
|
|
415
|
+
if (ed && ft && ft > ed)
|
|
416
|
+
continue;
|
|
417
|
+
rows.push({
|
|
418
|
+
operator: t.actorId ?? '', displayName: t.displayName ?? '',
|
|
419
|
+
performType: t.performType ?? 0,
|
|
420
|
+
createTime: t.createTime ?? null, finishTime: t.finishTime ?? null, expireTime: t.expireTime ?? null,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return rows;
|
|
424
|
+
}
|
|
425
|
+
async statsPendingAndOverdueCount() {
|
|
426
|
+
const now = new Date();
|
|
427
|
+
let pending = 0, overdue = 0;
|
|
428
|
+
for (const t of this.tasks.values()) {
|
|
429
|
+
if (Number(t.taskState) !== TaskState.Doing)
|
|
430
|
+
continue;
|
|
431
|
+
pending++;
|
|
432
|
+
const exp = this.toDt(t.expireTime);
|
|
433
|
+
if (exp && exp < now)
|
|
434
|
+
overdue++;
|
|
435
|
+
}
|
|
436
|
+
return [pending, overdue];
|
|
437
|
+
}
|
|
438
|
+
async statsCompletedTaskAggregate() {
|
|
439
|
+
let total = 0, countersign = 0, onTime = 0, onTimeDenom = 0;
|
|
440
|
+
for (const t of this.tasks.values()) {
|
|
441
|
+
if (Number(t.taskState) !== TaskState.Done)
|
|
442
|
+
continue;
|
|
443
|
+
total++;
|
|
444
|
+
if (t.performType === 1)
|
|
445
|
+
countersign++;
|
|
446
|
+
const ft = this.toDt(t.finishTime);
|
|
447
|
+
const exp = this.toDt(t.expireTime);
|
|
448
|
+
if (exp != null) {
|
|
449
|
+
onTimeDenom++;
|
|
450
|
+
if (ft && ft <= exp)
|
|
451
|
+
onTime++;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return [total, countersign, onTime, onTimeDenom];
|
|
455
|
+
}
|
|
456
|
+
async statsAvgCompletedDurationSeconds(start, end) {
|
|
457
|
+
const sd = this.toDt(start);
|
|
458
|
+
const ed = this.toDt(end);
|
|
459
|
+
let totalSec = 0, count = 0;
|
|
460
|
+
for (const inst of this.instances.values()) {
|
|
461
|
+
if (Number(inst.state) !== 20)
|
|
462
|
+
continue;
|
|
463
|
+
const ct = this.toDt(inst.createTime);
|
|
464
|
+
if (sd && ct && ct < sd)
|
|
465
|
+
continue;
|
|
466
|
+
if (ed && ct && ct > ed)
|
|
467
|
+
continue;
|
|
468
|
+
let maxFt;
|
|
469
|
+
for (const t of this.tasks.values()) {
|
|
470
|
+
if (t.processInstanceId !== inst.id)
|
|
471
|
+
continue;
|
|
472
|
+
const ft = this.toDt(t.finishTime);
|
|
473
|
+
if (ft && (!maxFt || ft > maxFt))
|
|
474
|
+
maxFt = ft;
|
|
475
|
+
}
|
|
476
|
+
if (maxFt && ct) {
|
|
477
|
+
totalSec += Math.floor((maxFt.getTime() - ct.getTime()) / 1000);
|
|
478
|
+
count++;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return count > 0 ? Math.floor(totalSec / count) : 0;
|
|
482
|
+
}
|
|
483
|
+
async statsDefineGroup(start, end, limit = 10) {
|
|
484
|
+
const sd = this.toDt(start);
|
|
485
|
+
const ed = this.toDt(end);
|
|
486
|
+
const grouped = new Map();
|
|
487
|
+
for (const inst of this.instances.values()) {
|
|
488
|
+
const ct = this.toDt(inst.createTime);
|
|
489
|
+
if (sd && ct && ct < sd)
|
|
490
|
+
continue;
|
|
491
|
+
if (ed && ct && ct > ed)
|
|
492
|
+
continue;
|
|
493
|
+
const did = String(inst.defineId);
|
|
494
|
+
if (!grouped.has(did)) {
|
|
495
|
+
const defn = this.defines.get(did);
|
|
496
|
+
grouped.set(did, { key: defn?.name ?? '', label: defn?.displayName ?? null, count: 0, totalDur: 0, durCount: 0 });
|
|
497
|
+
}
|
|
498
|
+
const g = grouped.get(did);
|
|
499
|
+
g.count++;
|
|
500
|
+
if (Number(inst.state) === 20) {
|
|
501
|
+
let maxFt;
|
|
502
|
+
for (const t of this.tasks.values()) {
|
|
503
|
+
if (t.processInstanceId !== inst.id)
|
|
504
|
+
continue;
|
|
505
|
+
const ft = this.toDt(t.finishTime);
|
|
506
|
+
if (ft && (!maxFt || ft > maxFt))
|
|
507
|
+
maxFt = ft;
|
|
508
|
+
}
|
|
509
|
+
if (maxFt && ct) {
|
|
510
|
+
g.totalDur += Math.floor((maxFt.getTime() - ct.getTime()) / 1000);
|
|
511
|
+
g.durCount++;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const entries = [...grouped.values()].sort((a, b) => b.count - a.count).slice(0, limit);
|
|
516
|
+
return entries.map(e => ({ key: e.key, label: e.label, count: e.count, avgDurationSeconds: e.durCount > 0 ? Math.floor(e.totalDur / e.durCount) : null }));
|
|
517
|
+
}
|
|
518
|
+
async statsStuckNodeGroup(limit = 10) {
|
|
519
|
+
const grouped = new Map();
|
|
520
|
+
for (const t of this.tasks.values()) {
|
|
521
|
+
if (Number(t.taskState) !== TaskState.Doing)
|
|
522
|
+
continue;
|
|
523
|
+
const dn = t.displayName;
|
|
524
|
+
if (!dn)
|
|
525
|
+
continue;
|
|
526
|
+
grouped.set(dn, (grouped.get(dn) ?? 0) + 1);
|
|
527
|
+
}
|
|
528
|
+
const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
529
|
+
return entries.map(([k, c]) => ({ key: k, count: c }));
|
|
530
|
+
}
|
|
531
|
+
async statsStuckApproverGroup(limit = 10) {
|
|
532
|
+
const grouped = new Map();
|
|
533
|
+
for (const t of this.tasks.values()) {
|
|
534
|
+
if (Number(t.taskState) !== TaskState.Doing)
|
|
535
|
+
continue;
|
|
536
|
+
const actors = this.actors.get(t.id) ?? [];
|
|
537
|
+
for (const aid of actors) {
|
|
538
|
+
if (!aid)
|
|
539
|
+
continue;
|
|
540
|
+
grouped.set(aid, (grouped.get(aid) ?? 0) + 1);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const entries = [...grouped.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
544
|
+
return entries.map(([k, c]) => ({ key: k, count: c }));
|
|
545
|
+
}
|
|
546
|
+
async statsCompletedInstanceDurations(start, end) {
|
|
547
|
+
const sd = this.toDt(start);
|
|
548
|
+
const ed = this.toDt(end);
|
|
549
|
+
const durations = [];
|
|
550
|
+
for (const inst of this.instances.values()) {
|
|
551
|
+
if (Number(inst.state) !== 20)
|
|
552
|
+
continue;
|
|
553
|
+
const ct = this.toDt(inst.createTime);
|
|
554
|
+
if (sd && ct && ct < sd)
|
|
555
|
+
continue;
|
|
556
|
+
if (ed && ct && ct > ed)
|
|
557
|
+
continue;
|
|
558
|
+
let maxFt;
|
|
559
|
+
for (const t of this.tasks.values()) {
|
|
560
|
+
if (t.processInstanceId !== inst.id)
|
|
561
|
+
continue;
|
|
562
|
+
const ft = this.toDt(t.finishTime);
|
|
563
|
+
if (ft && (!maxFt || ft > maxFt))
|
|
564
|
+
maxFt = ft;
|
|
565
|
+
}
|
|
566
|
+
if (maxFt && ct) {
|
|
567
|
+
durations.push(Math.floor((maxFt.getTime() - ct.getTime()) / 1000));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return durations;
|
|
571
|
+
}
|
|
378
572
|
}
|
package/dist/model.js
CHANGED
|
@@ -212,7 +212,7 @@ export class ProcessTask {
|
|
|
212
212
|
// ─── Clone Helpers(保留 class 原型)────────────────────────────────────────────
|
|
213
213
|
export function cloneInstance(inst) {
|
|
214
214
|
return Object.assign(Object.create(ProcessInstance.prototype), inst, {
|
|
215
|
-
tasks: inst.tasks.map(cloneTask),
|
|
215
|
+
tasks: (inst.tasks ?? []).map(cloneTask),
|
|
216
216
|
});
|
|
217
217
|
}
|
|
218
218
|
export function cloneTask(task) {
|
package/dist/spi.d.ts
CHANGED
|
@@ -4,6 +4,20 @@ export interface QueryCondition {
|
|
|
4
4
|
operator: string;
|
|
5
5
|
value: any;
|
|
6
6
|
}
|
|
7
|
+
export interface InstanceStatsRow {
|
|
8
|
+
defineId: string;
|
|
9
|
+
state: number;
|
|
10
|
+
operator: string;
|
|
11
|
+
createTime: Date | string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface TaskStatsRow {
|
|
14
|
+
operator: string;
|
|
15
|
+
displayName: string;
|
|
16
|
+
performType: number;
|
|
17
|
+
createTime: Date | string | null;
|
|
18
|
+
finishTime: Date | string | null;
|
|
19
|
+
expireTime: Date | string | null;
|
|
20
|
+
}
|
|
7
21
|
export interface ProcessRepository {
|
|
8
22
|
findDefineById(id: string): Promise<ProcessDefine | null>;
|
|
9
23
|
findDefineByName(name: string): Promise<ProcessDefine | null>;
|
|
@@ -45,6 +59,15 @@ export interface ProcessRepository {
|
|
|
45
59
|
rows: TaskRow[];
|
|
46
60
|
total: number;
|
|
47
61
|
}>;
|
|
62
|
+
queryInstancesForStats(stateIn?: number[] | null, start?: Date | null, end?: Date | null): Promise<InstanceStatsRow[]>;
|
|
63
|
+
queryTasksForStats(taskState?: number, start?: Date | null, end?: Date | null): Promise<TaskStatsRow[]>;
|
|
64
|
+
statsPendingAndOverdueCount(): Promise<[number, number]>;
|
|
65
|
+
statsCompletedTaskAggregate(): Promise<[number, number, number, number]>;
|
|
66
|
+
statsAvgCompletedDurationSeconds(start?: Date | null, end?: Date | null): Promise<number>;
|
|
67
|
+
statsDefineGroup(start?: Date | null, end?: Date | null, limit?: number): Promise<Record<string, any>[]>;
|
|
68
|
+
statsStuckNodeGroup(limit?: number): Promise<Record<string, any>[]>;
|
|
69
|
+
statsStuckApproverGroup(limit?: number): Promise<Record<string, any>[]>;
|
|
70
|
+
statsCompletedInstanceDurations(start?: Date | null, end?: Date | null): Promise<number[]>;
|
|
48
71
|
}
|
|
49
72
|
export interface UserProvider {
|
|
50
73
|
getUser(userId: string): Promise<UserInfo | null>;
|