Преглед изворни кода

feat(wm-bpm+frontend): #35 跨系统流程编排+Webhook回调完整实现

- entity: OrchestrationExecution, OrchestrationLog, OrchestrationStep, WebhookRegistration
- mapper: OrchestrationExecutionMapper, OrchestrationLogMapper
- service: ProcessOrchestrationService (编排CRUD/启动执行/步骤流转/跨系统API模拟/Webhook回调/重试机制), OrchestrationLogService
- controller: ProcessOrchestrationController (/api/bpm/orchestration/*)
- config: WebhookConfig (Webhook注册/签名验证HMAC-SHA256/回调触发)
- SQL DDL: V_bpm_orchestration.sql (编排表+执行表+日志表+示例数据)
- frontend: OrchestrationView.vue (编排管理/执行记录/日志查看/Webhook配置), orchestrationApi.ts
- tests: ProcessOrchestrationServiceTest, OrchestrationLogServiceTest, WebhookConfigTest
bot_dev2 пре 5 дана
родитељ
комит
0860b4d1d4

+ 106
- 0
db/V_bpm_orchestration.sql Прегледај датотеку

@@ -0,0 +1,106 @@
1
+-- =====================================================
2
+-- 跨系统流程编排 DDL
3
+-- Issue #35: 跨系统流程编排 + Webhook 回调通知
4
+-- =====================================================
5
+
6
+-- 编排定义表 (已存在则跳过)
7
+CREATE TABLE IF NOT EXISTS bpm_orchestration (
8
+    id              BIGSERIAL PRIMARY KEY,
9
+    orchestration_name  VARCHAR(200) NOT NULL,
10
+    orchestration_code  VARCHAR(100) UNIQUE,
11
+    description         TEXT,
12
+    process_definition_ids  JSONB,
13
+    orchestration_rules     JSONB,
14
+    trigger_type        VARCHAR(50) DEFAULT 'manual',
15
+    cron_expression     VARCHAR(100),
16
+    event_name          VARCHAR(200),
17
+    status              INTEGER DEFAULT 0,
18
+    created_by          VARCHAR(100),
19
+    execution_count     INTEGER DEFAULT 0,
20
+    tenant_id           VARCHAR(100),
21
+    deleted             INTEGER DEFAULT 0,
22
+    created_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
23
+    updated_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP
24
+);
25
+
26
+-- 编排执行记录表
27
+CREATE TABLE IF NOT EXISTS bpm_orchestration_execution (
28
+    id                  BIGSERIAL PRIMARY KEY,
29
+    orchestration_id    BIGINT NOT NULL REFERENCES bpm_orchestration(id),
30
+    execution_no        VARCHAR(100) UNIQUE NOT NULL,
31
+    current_step_index  INTEGER DEFAULT 0,
32
+    current_step_name   VARCHAR(200),
33
+    status              VARCHAR(50) DEFAULT 'pending',
34
+    started_at          TIMESTAMP,
35
+    completed_at        TIMESTAMP,
36
+    triggered_by        VARCHAR(100),
37
+    trigger_type        VARCHAR(50) DEFAULT 'manual',
38
+    input_params        JSONB,
39
+    output_result       JSONB,
40
+    failure_reason      TEXT,
41
+    retry_count         INTEGER DEFAULT 0,
42
+    tenant_id           VARCHAR(100),
43
+    deleted             INTEGER DEFAULT 0,
44
+    created_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
45
+    updated_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP
46
+);
47
+
48
+CREATE INDEX IF NOT EXISTS idx_orch_exec_orch_id ON bpm_orchestration_execution(orchestration_id);
49
+CREATE INDEX IF NOT EXISTS idx_orch_exec_no ON bpm_orchestration_execution(execution_no);
50
+CREATE INDEX IF NOT EXISTS idx_orch_exec_status ON bpm_orchestration_execution(status);
51
+CREATE INDEX IF NOT EXISTS idx_orch_exec_started ON bpm_orchestration_execution(started_at);
52
+
53
+-- 编排日志表
54
+CREATE TABLE IF NOT EXISTS bpm_orchestration_log (
55
+    id                  BIGSERIAL PRIMARY KEY,
56
+    execution_id        BIGINT NOT NULL REFERENCES bpm_orchestration_execution(id),
57
+    execution_no        VARCHAR(100) NOT NULL,
58
+    step_index          INTEGER,
59
+    step_name           VARCHAR(200),
60
+    action              VARCHAR(50) NOT NULL,
61
+    target_system       VARCHAR(100),
62
+    request_body        JSONB,
63
+    response_body       JSONB,
64
+    http_status         INTEGER,
65
+    duration_ms         BIGINT,
66
+    success             BOOLEAN DEFAULT TRUE,
67
+    error_message       TEXT,
68
+    log_time            TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
69
+    operator            VARCHAR(100),
70
+    deleted             INTEGER DEFAULT 0,
71
+    created_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
72
+    updated_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP
73
+);
74
+
75
+CREATE INDEX IF NOT EXISTS idx_orch_log_exec_id ON bpm_orchestration_log(execution_id);
76
+CREATE INDEX IF NOT EXISTS idx_orch_log_exec_no ON bpm_orchestration_log(execution_no);
77
+CREATE INDEX IF NOT EXISTS idx_orch_log_time ON bpm_orchestration_log(log_time);
78
+CREATE INDEX IF NOT EXISTS idx_orch_log_action ON bpm_orchestration_log(action);
79
+
80
+-- 插入示例编排定义
81
+INSERT INTO bpm_orchestration (orchestration_name, orchestration_code, description, trigger_type, orchestration_rules, status, created_by)
82
+VALUES
83
+('日常巡检数据同步编排', 'DAILY_PATROL_SYNC', '每日自动同步IoT设备数据、巡检任务、营收数据并发送通知', 'scheduled',
84
+ '[
85
+   {"stepName":"查询IoT设备状态","targetSystem":"iot","apiPath":"/api/iot/devices/status","method":"GET","timeoutMs":5000,"skippable":false,"maxRetries":3},
86
+   {"stepName":"同步巡检任务","targetSystem":"patrol","apiPath":"/api/patrol/tasks/sync","method":"POST","timeoutMs":10000,"skippable":false,"maxRetries":3},
87
+   {"stepName":"汇总营收数据","targetSystem":"revenue","apiPath":"/api/revenue/daily/summary","method":"GET","timeoutMs":8000,"skippable":true,"maxRetries":2},
88
+   {"stepName":"发送汇总通知","targetSystem":"notify","apiPath":"/api/notify/send","method":"POST","timeoutMs":5000,"skippable":true,"maxRetries":1}
89
+ ]',
90
+ 1, 'system'),
91
+('应急事件处理编排', 'EMERGENCY_RESPONSE', '突发事件触发时:获取设备数据→派发巡检任务→通知相关人员', 'event',
92
+ '[
93
+   {"stepName":"获取异常设备数据","targetSystem":"iot","apiPath":"/api/iot/devices/alerts","method":"GET","timeoutMs":3000,"skippable":false,"maxRetries":3},
94
+   {"stepName":"派发应急巡检任务","targetSystem":"patrol","apiPath":"/api/patrol/tasks/emergency","method":"POST","timeoutMs":5000,"skippable":false,"maxRetries":3},
95
+   {"stepName":"发送紧急通知","targetSystem":"notify","apiPath":"/api/notify/emergency","method":"POST","timeoutMs":3000,"skippable":false,"maxRetries":2}
96
+ ]',
97
+ 1, 'system')
98
+ON CONFLICT DO NOTHING;
99
+
100
+-- 注释
101
+COMMENT ON TABLE bpm_orchestration IS '跨系统流程编排定义表';
102
+COMMENT ON TABLE bpm_orchestration_execution IS '编排执行记录表';
103
+COMMENT ON TABLE bpm_orchestration_log IS '编排执行日志表';
104
+COMMENT ON COLUMN bpm_orchestration.orchestration_rules IS '编排步骤JSON数组';
105
+COMMENT ON COLUMN bpm_orchestration_execution.status IS '执行状态: pending/running/completed/failed/cancelled';
106
+COMMENT ON COLUMN bpm_orchestration_log.action IS '动作类型: start/complete/fail/retry/callback/skip/cancel';

+ 181
- 0
frontend/src/api/orchestrationApi.ts Прегледај датотеку

@@ -0,0 +1,181 @@
1
+import request from './request'
2
+
3
+// ============== 类型定义 ==============
4
+
5
+export interface OrchestrationStep {
6
+  stepName: string
7
+  targetSystem: 'iot' | 'revenue' | 'patrol' | 'notify'
8
+  apiPath: string
9
+  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
10
+  timeoutMs: number
11
+  skippable: boolean
12
+  maxRetries: number
13
+}
14
+
15
+export interface BpmOrchestration {
16
+  id?: number
17
+  orchestrationName: string
18
+  orchestrationCode: string
19
+  description?: string
20
+  processDefinitionIds?: string
21
+  orchestrationRules?: string
22
+  triggerType: 'manual' | 'scheduled' | 'event'
23
+  cronExpression?: string
24
+  eventName?: string
25
+  status?: number
26
+  createdBy?: string
27
+  executionCount?: number
28
+  createdAt?: string
29
+  updatedAt?: string
30
+}
31
+
32
+export interface OrchestrationExecution {
33
+  id: number
34
+  orchestrationId: number
35
+  executionNo: string
36
+  currentStepIndex: number
37
+  currentStepName: string
38
+  status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
39
+  startedAt: string
40
+  completedAt?: string
41
+  triggeredBy: string
42
+  triggerType: string
43
+  inputParams?: string
44
+  outputResult?: string
45
+  failureReason?: string
46
+  retryCount: number
47
+}
48
+
49
+export interface OrchestrationLog {
50
+  id: number
51
+  executionId: number
52
+  executionNo: string
53
+  stepIndex: number
54
+  stepName: string
55
+  action: 'start' | 'complete' | 'fail' | 'retry' | 'callback' | 'skip' | 'cancel'
56
+  targetSystem: string
57
+  requestBody?: string
58
+  responseBody?: string
59
+  httpStatus?: number
60
+  durationMs?: number
61
+  success: boolean
62
+  errorMessage?: string
63
+  logTime: string
64
+  operator: string
65
+}
66
+
67
+export interface WebhookRegistration {
68
+  callbackUrl: string
69
+  secretKey?: string
70
+  signatureEnabled: boolean
71
+  eventType: string
72
+  description?: string
73
+}
74
+
75
+export interface ExecutionStats {
76
+  total: number
77
+  completed: number
78
+  failed: number
79
+  running: number
80
+  successRate: string
81
+}
82
+
83
+// ============== 编排定义 API ==============
84
+
85
+export function createOrchestration(data: BpmOrchestration) {
86
+  return request.post('/bpm/orchestration/definitions', data)
87
+}
88
+
89
+export function updateOrchestration(id: number, data: BpmOrchestration) {
90
+  return request.put(`/bpm/orchestration/definitions/${id}`, data)
91
+}
92
+
93
+export function deleteOrchestration(id: number) {
94
+  return request.delete(`/bpm/orchestration/definitions/${id}`)
95
+}
96
+
97
+export function getOrchestration(id: number) {
98
+  return request.get(`/bpm/orchestration/definitions/${id}`)
99
+}
100
+
101
+export function listOrchestrations(params?: { keyword?: string; status?: number; page?: number; size?: number }) {
102
+  return request.get('/bpm/orchestration/definitions', { params })
103
+}
104
+
105
+export function toggleOrchestrationStatus(id: number, status: number) {
106
+  return request.put(`/bpm/orchestration/definitions/${id}/status`, null, { params: { status } })
107
+}
108
+
109
+// ============== 编排执行 API ==============
110
+
111
+export function startExecution(data: { orchestrationId: number; triggeredBy?: string; triggerType?: string; inputParams?: Record<string, any> }) {
112
+  return request.post('/bpm/orchestration/executions', data)
113
+}
114
+
115
+export function getExecution(id: number) {
116
+  return request.get(`/bpm/orchestration/executions/${id}`)
117
+}
118
+
119
+export function getExecutionByNo(executionNo: string) {
120
+  return request.get(`/bpm/orchestration/executions/no/${executionNo}`)
121
+}
122
+
123
+export function listExecutions(params?: { orchestrationId?: number; status?: string; page?: number; size?: number }) {
124
+  return request.get('/bpm/orchestration/executions', { params })
125
+}
126
+
127
+export function cancelExecution(id: number) {
128
+  return request.put(`/bpm/orchestration/executions/${id}/cancel`)
129
+}
130
+
131
+export function getExecutionStats(orchestrationId?: number) {
132
+  return request.get('/bpm/orchestration/executions/stats', { params: { orchestrationId } })
133
+}
134
+
135
+// ============== 编排日志 API ==============
136
+
137
+export function getLogs(params?: { executionId?: number; action?: string; success?: boolean; startTime?: string; endTime?: string; page?: number; size?: number }) {
138
+  return request.get('/bpm/orchestration/logs', { params })
139
+}
140
+
141
+export function getLogsByExecutionId(executionId: number) {
142
+  return request.get(`/bpm/orchestration/logs/execution/${executionId}`)
143
+}
144
+
145
+export function getLogsByExecutionNo(executionNo: string) {
146
+  return request.get(`/bpm/orchestration/logs/execution-no/${executionNo}`)
147
+}
148
+
149
+export function getStepLogs(executionId: number, stepIndex: number) {
150
+  return request.get(`/bpm/orchestration/logs/execution/${executionId}/step/${stepIndex}`)
151
+}
152
+
153
+export function countLogs(executionId?: number) {
154
+  return request.get('/bpm/orchestration/logs/count', { params: { executionId } })
155
+}
156
+
157
+// ============== Webhook API ==============
158
+
159
+export function registerWebhook(id: string, data: WebhookRegistration) {
160
+  return request.post(`/bpm/orchestration/webhooks`, data, { params: { id } })
161
+}
162
+
163
+export function unregisterWebhook(id: string) {
164
+  return request.delete(`/bpm/orchestration/webhooks/${id}`)
165
+}
166
+
167
+export function listWebhooks() {
168
+  return request.get('/bpm/orchestration/webhooks')
169
+}
170
+
171
+export function getWebhook(id: string) {
172
+  return request.get(`/bpm/orchestration/webhooks/${id}`)
173
+}
174
+
175
+export function triggerWebhook(eventType: string, payload: Record<string, any>) {
176
+  return request.post('/bpm/orchestration/webhooks/trigger', { eventType, payload })
177
+}
178
+
179
+export function verifySignature(payload: string, secretKey: string, signature: string) {
180
+  return request.post('/bpm/orchestration/webhooks/verify-signature', { payload, secretKey, signature })
181
+}

+ 1
- 0
frontend/src/router/index.ts Прегледај датотеку

@@ -16,6 +16,7 @@ const routes = [
16 16
       { path: 'cs/knowledge', name: 'csKnowledge', component: () => import('@/views/cs/KnowledgeBaseView.vue') },
17 17
       { path: 'cs/announcement', name: 'csAnnouncement', component: () => import('@/views/cs/AnnouncementView.vue') },
18 18
       { path: 'cs/kpi', name: 'csKpi', component: () => import('@/views/cs/KpiDashboardView.vue') },
19
+      { path: 'bpm/orchestration', name: 'bpmOrchestration', component: () => import('@/views/bpm/OrchestrationView.vue') },
19 20
     ]
20 21
   },
21 22
   { path: '/:pathMatch(.*)*', redirect: '/dashboard' }

+ 412
- 0
frontend/src/views/bpm/OrchestrationView.vue Прегледај датотеку

@@ -0,0 +1,412 @@
1
+<template>
2
+  <div class="orchestration-container">
3
+    <el-tabs v-model="activeTab" type="border-card">
4
+      <!-- 编排管理 -->
5
+      <el-tab-pane label="编排管理" name="definitions">
6
+        <div class="toolbar">
7
+          <el-input v-model="searchKeyword" placeholder="搜索编排名称/编码" clearable style="width:240px" @input="loadOrchestrations" />
8
+          <el-select v-model="filterStatus" placeholder="状态" clearable style="width:120px;margin-left:12px" @change="loadOrchestrations">
9
+            <el-option label="草稿" :value="0" />
10
+            <el-option label="启用" :value="1" />
11
+            <el-option label="停用" :value="2" />
12
+          </el-select>
13
+          <el-button type="primary" style="margin-left:12px" @click="showCreateDialog">新建编排</el-button>
14
+        </div>
15
+
16
+        <el-table :data="orchestrations" v-loading="loadingOrch" stripe style="width:100%;margin-top:16px">
17
+          <el-table-column prop="id" label="ID" width="60" />
18
+          <el-table-column prop="orchestrationName" label="编排名称" />
19
+          <el-table-column prop="orchestrationCode" label="编码" width="160" />
20
+          <el-table-column prop="triggerType" label="触发方式" width="100">
21
+            <template #default="{ row }">
22
+              <el-tag :type="triggerTypeTag(row.triggerType)">{{ triggerTypeLabel(row.triggerType) }}</el-tag>
23
+            </template>
24
+          </el-table-column>
25
+          <el-table-column prop="status" label="状态" width="80">
26
+            <template #default="{ row }">
27
+              <el-tag :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
28
+            </template>
29
+          </el-table-column>
30
+          <el-table-column prop="executionCount" label="执行次数" width="90" />
31
+          <el-table-column label="操作" width="280">
32
+            <template #default="{ row }">
33
+              <el-button size="small" @click="viewSteps(row)">步骤</el-button>
34
+              <el-button size="small" type="success" @click="executeOrchestration(row)" :disabled="row.status !== 1">执行</el-button>
35
+              <el-button size="small" :type="row.status === 1 ? 'warning' : 'success'" @click="toggleStatus(row)">{{ row.status === 1 ? '停用' : '启用' }}</el-button>
36
+              <el-button size="small" type="danger" @click="deleteOrch(row)">删除</el-button>
37
+            </template>
38
+          </el-table-column>
39
+        </el-table>
40
+      </el-tab-pane>
41
+
42
+      <!-- 执行记录 -->
43
+      <el-tab-pane label="执行记录" name="executions">
44
+        <div class="toolbar">
45
+          <el-select v-model="filterExecStatus" placeholder="执行状态" clearable style="width:140px" @change="loadExecutions">
46
+            <el-option label="运行中" value="running" />
47
+            <el-option label="已完成" value="completed" />
48
+            <el-option label="已失败" value="failed" />
49
+            <el-option label="已取消" value="cancelled" />
50
+          </el-select>
51
+          <el-button style="margin-left:12px" @click="loadExecutions">刷新</el-button>
52
+          <el-button type="info" style="margin-left:12px" @click="loadStats">统计</el-button>
53
+        </div>
54
+
55
+        <el-table :data="executions" v-loading="loadingExec" stripe style="width:100%;margin-top:16px">
56
+          <el-table-column prop="executionNo" label="执行编号" width="280" />
57
+          <el-table-column prop="currentStepName" label="当前步骤" />
58
+          <el-table-column prop="status" label="状态" width="100">
59
+            <template #default="{ row }">
60
+              <el-tag :type="execStatusTag(row.status)">{{ row.status }}</el-tag>
61
+            </template>
62
+          </el-table-column>
63
+          <el-table-column prop="triggeredBy" label="触发人" width="100" />
64
+          <el-table-column prop="startedAt" label="开始时间" width="180" />
65
+          <el-table-column prop="completedAt" label="完成时间" width="180" />
66
+          <el-table-column prop="retryCount" label="重试" width="60" />
67
+          <el-table-column label="操作" width="180">
68
+            <template #default="{ row }">
69
+              <el-button size="small" @click="viewLogs(row)">日志</el-button>
70
+              <el-button size="small" type="warning" @click="cancelExec(row)" :disabled="row.status !== 'running'">取消</el-button>
71
+            </template>
72
+          </el-table-column>
73
+        </el-table>
74
+      </el-tab-pane>
75
+
76
+      <!-- 执行日志 -->
77
+      <el-tab-pane label="执行日志" name="logs">
78
+        <div class="toolbar">
79
+          <el-input v-model="logExecutionNo" placeholder="执行编号" clearable style="width:280px" />
80
+          <el-button type="primary" style="margin-left:12px" @click="loadLogsByNo">查询</el-button>
81
+        </div>
82
+
83
+        <el-timeline style="margin-top:20px">
84
+          <el-timeline-item v-for="log in logs" :key="log.id"
85
+            :timestamp="log.logTime"
86
+            :type="log.success ? 'success' : 'danger'"
87
+            :hollow="log.action === 'retry'">
88
+            <el-card shadow="hover">
89
+              <p><strong>步骤:</strong> {{ log.stepName }} ({{ log.stepIndex }})</p>
90
+              <p><strong>动作:</strong> <el-tag size="small">{{ log.action }}</el-tag> → {{ log.targetSystem }}</p>
91
+              <p v-if="log.durationMs"><strong>耗时:</strong> {{ log.durationMs }}ms</p>
92
+              <p v-if="log.errorMessage"><strong style="color:#f56c6c">错误:</strong> {{ log.errorMessage }}</p>
93
+            </el-card>
94
+          </el-timeline-item>
95
+        </el-timeline>
96
+      </el-tab-pane>
97
+
98
+      <!-- Webhook 配置 -->
99
+      <el-tab-pane label="Webhook 配置" name="webhooks">
100
+        <div class="toolbar">
101
+          <el-button type="primary" @click="showWebhookDialog">注册 Webhook</el-button>
102
+        </div>
103
+
104
+        <el-table :data="webhooks" stripe style="width:100%;margin-top:16px">
105
+          <el-table-column prop="callbackUrl" label="回调 URL" />
106
+          <el-table-column prop="eventType" label="事件类型" width="220" />
107
+          <el-table-column prop="signatureEnabled" label="签名" width="80">
108
+            <template #default="{ row }">
109
+              <el-tag :type="row.signatureEnabled ? 'success' : 'info'" size="small">{{ row.signatureEnabled ? '是' : '否' }}</el-tag>
110
+            </template>
111
+          </el-table-column>
112
+          <el-table-column prop="description" label="描述" />
113
+        </el-table>
114
+      </el-tab-pane>
115
+    </el-tabs>
116
+
117
+    <!-- 创建编排对话框 -->
118
+    <el-dialog v-model="createDialogVisible" title="新建编排" width="700px">
119
+      <el-form :model="newOrchestration" label-width="100px">
120
+        <el-form-item label="编排名称" required>
121
+          <el-input v-model="newOrchestration.orchestrationName" />
122
+        </el-form-item>
123
+        <el-form-item label="编码" required>
124
+          <el-input v-model="newOrchestration.orchestrationCode" />
125
+        </el-form-item>
126
+        <el-form-item label="描述">
127
+          <el-input v-model="newOrchestration.description" type="textarea" :rows="2" />
128
+        </el-form-item>
129
+        <el-form-item label="触发方式">
130
+          <el-select v-model="newOrchestration.triggerType" style="width:100%">
131
+            <el-option label="手动" value="manual" />
132
+            <el-option label="定时" value="scheduled" />
133
+            <el-option label="事件" value="event" />
134
+          </el-select>
135
+        </el-form-item>
136
+        <el-form-item label="编排步骤">
137
+          <div v-for="(step, index) in newSteps" :key="index" class="step-row">
138
+            <el-input v-model="step.stepName" placeholder="步骤名称" style="width:140px" />
139
+            <el-select v-model="step.targetSystem" placeholder="目标系统" style="width:120px;margin:0 8px">
140
+              <el-option label="IoT" value="iot" />
141
+              <el-option label="营收" value="revenue" />
142
+              <el-option label="巡检" value="patrol" />
143
+              <el-option label="通知" value="notify" />
144
+            </el-select>
145
+            <el-input v-model="step.apiPath" placeholder="API路径" style="width:180px" />
146
+            <el-button type="danger" size="small" @click="removeStep(index)">删除</el-button>
147
+          </div>
148
+          <el-button type="primary" size="small" @click="addStep" style="margin-top:8px">+ 添加步骤</el-button>
149
+        </el-form-item>
150
+      </el-form>
151
+      <template #footer>
152
+        <el-button @click="createDialogVisible = false">取消</el-button>
153
+        <el-button type="primary" @click="submitCreate">创建</el-button>
154
+      </template>
155
+    </el-dialog>
156
+
157
+    <!-- Webhook 注册对话框 -->
158
+    <el-dialog v-model="webhookDialogVisible" title="注册 Webhook" width="500px">
159
+      <el-form :model="newWebhook" label-width="100px">
160
+        <el-form-item label="Webhook ID">
161
+          <el-input v-model="webhookId" />
162
+        </el-form-item>
163
+        <el-form-item label="回调 URL" required>
164
+          <el-input v-model="newWebhook.callbackUrl" />
165
+        </el-form-item>
166
+        <el-form-item label="事件类型">
167
+          <el-select v-model="newWebhook.eventType" style="width:100%">
168
+            <el-option label="编排完成" value="orchestration.completed" />
169
+            <el-option label="编排失败" value="orchestration.failed" />
170
+            <el-option label="步骤完成" value="orchestration.step.completed" />
171
+          </el-select>
172
+        </el-form-item>
173
+        <el-form-item label="签名密钥">
174
+          <el-input v-model="newWebhook.secretKey" type="password" />
175
+        </el-form-item>
176
+        <el-form-item label="启用签名">
177
+          <el-switch v-model="newWebhook.signatureEnabled" />
178
+        </el-form-item>
179
+        <el-form-item label="描述">
180
+          <el-input v-model="newWebhook.description" />
181
+        </el-form-item>
182
+      </el-form>
183
+      <template #footer>
184
+        <el-button @click="webhookDialogVisible = false">取消</el-button>
185
+        <el-button type="primary" @click="submitWebhook">注册</el-button>
186
+      </template>
187
+    </el-dialog>
188
+
189
+    <!-- 统计对话框 -->
190
+    <el-dialog v-model="statsDialogVisible" title="执行统计" width="400px">
191
+      <el-descriptions :column="1" border>
192
+        <el-descriptions-item label="总执行次数">{{ stats.total }}</el-descriptions-item>
193
+        <el-descriptions-item label="已完成">{{ stats.completed }}</el-descriptions-item>
194
+        <el-descriptions-item label="已失败">{{ stats.failed }}</el-descriptions-item>
195
+        <el-descriptions-item label="运行中">{{ stats.running }}</el-descriptions-item>
196
+        <el-descriptions-item label="成功率">{{ stats.successRate }}</el-descriptions-item>
197
+      </el-descriptions>
198
+    </el-dialog>
199
+
200
+    <!-- 步骤查看对话框 -->
201
+    <el-dialog v-model="stepsDialogVisible" title="编排步骤" width="600px">
202
+      <el-steps direction="vertical" :active="viewingSteps.length" finish-status="success">
203
+        <el-step v-for="(step, idx) in viewingSteps" :key="idx"
204
+          :title="step.stepName"
205
+          :description="`${step.targetSystem} → ${step.apiPath} (${step.method}) | 超时: ${step.timeoutMs}ms | 重试: ${step.maxRetries}次${step.skippable ? ' | 可跳过' : ''}`" />
206
+      </el-steps>
207
+    </el-dialog>
208
+  </div>
209
+</template>
210
+
211
+<script setup lang="ts">
212
+import { ref, reactive, onMounted } from 'vue'
213
+import { ElMessage, ElMessageBox } from 'element-plus'
214
+import {
215
+  type BpmOrchestration, type OrchestrationExecution, type OrchestrationLog,
216
+  type OrchestrationStep, type WebhookRegistration, type ExecutionStats,
217
+  createOrchestration, deleteOrchestration, listOrchestrations, toggleOrchestrationStatus,
218
+  startExecution, listExecutions, cancelExecution, getExecutionStats,
219
+  getLogsByExecutionNo, registerWebhook, listWebhooks
220
+} from '@/api/orchestrationApi'
221
+
222
+const activeTab = ref('definitions')
223
+
224
+// ============== 编排管理 ==============
225
+const orchestrations = ref<BpmOrchestration[]>([])
226
+const loadingOrch = ref(false)
227
+const searchKeyword = ref('')
228
+const filterStatus = ref<number | undefined>(undefined)
229
+const createDialogVisible = ref(false)
230
+const stepsDialogVisible = ref(false)
231
+const viewingSteps = ref<OrchestrationStep[]>([])
232
+
233
+const newOrchestration = reactive<BpmOrchestration>({
234
+  orchestrationName: '', orchestrationCode: '', description: '',
235
+  triggerType: 'manual', orchestrationRules: ''
236
+})
237
+const newSteps = ref<OrchestrationStep[]>([])
238
+
239
+async function loadOrchestrations() {
240
+  loadingOrch.value = true
241
+  try {
242
+    const res: any = await listOrchestrations({ keyword: searchKeyword.value || undefined, status: filterStatus.value, page: 1, size: 50 })
243
+    orchestrations.value = res.data || []
244
+  } catch (e) { /* handled by interceptor */ }
245
+  loadingOrch.value = false
246
+}
247
+
248
+function showCreateDialog() {
249
+  Object.assign(newOrchestration, { orchestrationName: '', orchestrationCode: '', description: '', triggerType: 'manual' })
250
+  newSteps.value = [{ stepName: '', targetSystem: 'iot', apiPath: '', method: 'GET', timeoutMs: 5000, skippable: false, maxRetries: 3 }]
251
+  createDialogVisible.value = true
252
+}
253
+
254
+function addStep() {
255
+  newSteps.value.push({ stepName: '', targetSystem: 'iot', apiPath: '', method: 'GET', timeoutMs: 5000, skippable: false, maxRetries: 3 })
256
+}
257
+
258
+function removeStep(idx: number) {
259
+  newSteps.value.splice(idx, 1)
260
+}
261
+
262
+async function submitCreate() {
263
+  if (!newOrchestration.orchestrationName || !newOrchestration.orchestrationCode) {
264
+    ElMessage.warning('请填写编排名称和编码')
265
+    return
266
+  }
267
+  newOrchestration.orchestrationRules = JSON.stringify(newSteps.value)
268
+  try {
269
+    await createOrchestration(newOrchestration)
270
+    ElMessage.success('创建成功')
271
+    createDialogVisible.value = false
272
+    loadOrchestrations()
273
+  } catch (e) { /* handled */ }
274
+}
275
+
276
+function viewSteps(row: BpmOrchestration) {
277
+  try {
278
+    viewingSteps.value = row.orchestrationRules ? JSON.parse(row.orchestrationRules) : []
279
+  } catch { viewingSteps.value = [] }
280
+  stepsDialogVisible.value = true
281
+}
282
+
283
+async function toggleStatus(row: BpmOrchestration) {
284
+  const newStatus = row.status === 1 ? 2 : 1
285
+  try {
286
+    await toggleOrchestrationStatus(row.id!, newStatus)
287
+    ElMessage.success(newStatus === 1 ? '已启用' : '已停用')
288
+    loadOrchestrations()
289
+  } catch (e) { /* handled */ }
290
+}
291
+
292
+async function deleteOrch(row: BpmOrchestration) {
293
+  try {
294
+    await ElMessageBox.confirm('确认删除该编排?', '提示', { type: 'warning' })
295
+    await deleteOrchestration(row.id!)
296
+    ElMessage.success('已删除')
297
+    loadOrchestrations()
298
+  } catch (e) { /* cancelled */ }
299
+}
300
+
301
+async function executeOrchestration(row: BpmOrchestration) {
302
+  try {
303
+    await startExecution({ orchestrationId: row.id!, triggeredBy: '当前用户', triggerType: 'manual' })
304
+    ElMessage.success('执行已启动')
305
+    loadExecutions()
306
+  } catch (e) { /* handled */ }
307
+}
308
+
309
+// ============== 执行记录 ==============
310
+const executions = ref<OrchestrationExecution[]>([])
311
+const loadingExec = ref(false)
312
+const filterExecStatus = ref<string | undefined>(undefined)
313
+const statsDialogVisible = ref(false)
314
+const stats = reactive<ExecutionStats>({ total: 0, completed: 0, failed: 0, running: 0, successRate: 'N/A' })
315
+
316
+async function loadExecutions() {
317
+  loadingExec.value = true
318
+  try {
319
+    const res: any = await listExecutions({ status: filterExecStatus.value, page: 1, size: 50 })
320
+    executions.value = res.data || []
321
+  } catch (e) { /* handled */ }
322
+  loadingExec.value = false
323
+}
324
+
325
+async function cancelExec(row: OrchestrationExecution) {
326
+  try {
327
+    await ElMessageBox.confirm('确认取消该执行?', '提示', { type: 'warning' })
328
+    await cancelExecution(row.id)
329
+    ElMessage.success('已取消')
330
+    loadExecutions()
331
+  } catch (e) { /* cancelled */ }
332
+}
333
+
334
+async function loadStats() {
335
+  try {
336
+    const res: any = await getExecutionStats()
337
+    Object.assign(stats, res.data)
338
+    statsDialogVisible.value = true
339
+  } catch (e) { /* handled */ }
340
+}
341
+
342
+// ============== 日志 ==============
343
+const logs = ref<OrchestrationLog[]>([])
344
+const logExecutionNo = ref('')
345
+
346
+async function viewLogs(row: OrchestrationExecution) {
347
+  logExecutionNo.value = row.executionNo
348
+  activeTab.value = 'logs'
349
+  await loadLogsByNo()
350
+}
351
+
352
+async function loadLogsByNo() {
353
+  if (!logExecutionNo.value) return
354
+  try {
355
+    const res: any = await getLogsByExecutionNo(logExecutionNo.value)
356
+    logs.value = res.data || []
357
+  } catch (e) { logs.value = [] }
358
+}
359
+
360
+// ============== Webhook ==============
361
+const webhooks = ref<WebhookRegistration[]>([])
362
+const webhookDialogVisible = ref(false)
363
+const webhookId = ref('')
364
+const newWebhook = reactive<WebhookRegistration>({
365
+  callbackUrl: '', secretKey: '', signatureEnabled: true, eventType: 'orchestration.completed', description: ''
366
+})
367
+
368
+async function loadWebhooks() {
369
+  try {
370
+    const res: any = await listWebhooks()
371
+    webhooks.value = res.data || []
372
+  } catch (e) { /* handled */ }
373
+}
374
+
375
+function showWebhookDialog() {
376
+  webhookId.value = ''
377
+  Object.assign(newWebhook, { callbackUrl: '', secretKey: '', signatureEnabled: true, eventType: 'orchestration.completed', description: '' })
378
+  webhookDialogVisible.value = true
379
+}
380
+
381
+async function submitWebhook() {
382
+  if (!webhookId.value || !newWebhook.callbackUrl) {
383
+    ElMessage.warning('请填写 Webhook ID 和回调 URL')
384
+    return
385
+  }
386
+  try {
387
+    await registerWebhook(webhookId.value, newWebhook)
388
+    ElMessage.success('注册成功')
389
+    webhookDialogVisible.value = false
390
+    loadWebhooks()
391
+  } catch (e) { /* handled */ }
392
+}
393
+
394
+// ============== 辅助 ==============
395
+function statusLabel(s: number) { return s === 0 ? '草稿' : s === 1 ? '启用' : '停用' }
396
+function statusTag(s: number) { return s === 0 ? 'info' : s === 1 ? 'success' : 'danger' }
397
+function triggerTypeLabel(t: string) { return t === 'manual' ? '手动' : t === 'scheduled' ? '定时' : '事件' }
398
+function triggerTypeTag(t: string) { return t === 'manual' ? 'info' : t === 'scheduled' ? 'warning' : 'primary' }
399
+function execStatusTag(s: string) { return s === 'completed' ? 'success' : s === 'failed' ? 'danger' : s === 'running' ? 'primary' : 'info' }
400
+
401
+onMounted(() => {
402
+  loadOrchestrations()
403
+  loadExecutions()
404
+  loadWebhooks()
405
+})
406
+</script>
407
+
408
+<style scoped>
409
+.orchestration-container { padding: 20px; }
410
+.toolbar { display: flex; align-items: center; }
411
+.step-row { display: flex; align-items: center; margin-bottom: 8px; }
412
+</style>

+ 199
- 0
wm-bpm/src/main/java/com/water/bpm/config/WebhookConfig.java Прегледај датотеку

@@ -0,0 +1,199 @@
1
+package com.water.bpm.config;
2
+
3
+import com.fasterxml.jackson.databind.ObjectMapper;
4
+import com.water.bpm.entity.dto.WebhookRegistration;
5
+import lombok.Data;
6
+import lombok.extern.slf4j.Slf4j;
7
+import org.springframework.http.*;
8
+import org.springframework.stereotype.Component;
9
+import org.springframework.web.client.RestTemplate;
10
+
11
+import javax.crypto.Mac;
12
+import javax.crypto.spec.SecretKeySpec;
13
+import java.nio.charset.StandardCharsets;
14
+import java.security.InvalidKeyException;
15
+import java.security.NoSuchAlgorithmException;
16
+import java.util.*;
17
+import java.util.concurrent.ConcurrentHashMap;
18
+import java.util.stream.Collectors;
19
+
20
+/**
21
+ * Webhook 回调配置管理
22
+ * 负责 Webhook 注册、签名验证和回调触发
23
+ */
24
+@Slf4j
25
+@Component
26
+public class WebhookConfig {
27
+
28
+    private final Map<String, WebhookRegistration> registrations = new ConcurrentHashMap<>();
29
+    private final RestTemplate restTemplate;
30
+    private final ObjectMapper objectMapper;
31
+
32
+    public WebhookConfig() {
33
+        this.restTemplate = new RestTemplate();
34
+        this.objectMapper = new ObjectMapper();
35
+        // 注册默认 Webhook (示例)
36
+        registerDefaultWebhooks();
37
+    }
38
+
39
+    /**
40
+     * 注册 Webhook
41
+     */
42
+    public WebhookRegistration register(String id, WebhookRegistration registration) {
43
+        registrations.put(id, registration);
44
+        log.info("Webhook registered: id={}, url={}, eventType={}", id, registration.getCallbackUrl(), registration.getEventType());
45
+        return registration;
46
+    }
47
+
48
+    /**
49
+     * 注销 Webhook
50
+     */
51
+    public void unregister(String id) {
52
+        registrations.remove(id);
53
+        log.info("Webhook unregistered: id={}", id);
54
+    }
55
+
56
+    /**
57
+     * 获取所有注册的 Webhook
58
+     */
59
+    public List<WebhookRegistration> listAll() {
60
+        return new ArrayList<>(registrations.values());
61
+    }
62
+
63
+    /**
64
+     * 根据 ID 获取 Webhook
65
+     */
66
+    public WebhookRegistration getById(String id) {
67
+        return registrations.get(id);
68
+    }
69
+
70
+    /**
71
+     * 根据事件类型获取 Webhook 列表
72
+     */
73
+    public List<WebhookRegistration> getByEventType(String eventType) {
74
+        return registrations.values().stream()
75
+                .filter(r -> eventType.equals(r.getEventType()))
76
+                .collect(Collectors.toList());
77
+    }
78
+
79
+    /**
80
+     * 触发 Webhook 回调
81
+     *
82
+     * @param eventType 事件类型
83
+     * @param payload   回调数据
84
+     * @return 回调结果列表
85
+     */
86
+    public List<WebhookCallbackResult> triggerCallback(String eventType, Map<String, Object> payload) {
87
+        List<WebhookRegistration> hooks = getByEventType(eventType);
88
+        List<WebhookCallbackResult> results = new ArrayList<>();
89
+
90
+        for (WebhookRegistration hook : hooks) {
91
+            WebhookCallbackResult result = sendCallback(hook, payload);
92
+            results.add(result);
93
+        }
94
+
95
+        return results;
96
+    }
97
+
98
+    /**
99
+     * 发送单个回调
100
+     */
101
+    private WebhookCallbackResult sendCallback(WebhookRegistration hook, Map<String, Object> payload) {
102
+        WebhookCallbackResult result = new WebhookCallbackResult();
103
+        result.setCallbackUrl(hook.getCallbackUrl());
104
+        result.setEventType(hook.getEventType());
105
+
106
+        try {
107
+            String jsonPayload = objectMapper.writeValueAsString(payload);
108
+
109
+            HttpHeaders headers = new HttpHeaders();
110
+            headers.setContentType(MediaType.APPLICATION_JSON);
111
+            headers.set("X-Webhook-Event", hook.getEventType());
112
+            headers.set("X-Webhook-Timestamp", String.valueOf(System.currentTimeMillis()));
113
+
114
+            // HMAC-SHA256 签名
115
+            if (Boolean.TRUE.equals(hook.getSignatureEnabled()) && hook.getSecretKey() != null) {
116
+                String signature = generateSignature(jsonPayload, hook.getSecretKey());
117
+                headers.set("X-Webhook-Signature", signature);
118
+                log.debug("Webhook signature generated for {}", hook.getCallbackUrl());
119
+            }
120
+
121
+            HttpEntity<String> entity = new HttpEntity<>(jsonPayload, headers);
122
+            long startTime = System.currentTimeMillis();
123
+
124
+            ResponseEntity<String> response = restTemplate.exchange(
125
+                    hook.getCallbackUrl(), HttpMethod.POST, entity, String.class);
126
+
127
+            long duration = System.currentTimeMillis() - startTime;
128
+
129
+            result.setSuccess(response.getStatusCode().is2xxSuccessful());
130
+            result.setHttpStatus(response.getStatusCode().value());
131
+            result.setResponseBody(response.getBody());
132
+            result.setDurationMs(duration);
133
+
134
+            log.info("Webhook callback sent: url={}, status={}, duration={}ms",
135
+                    hook.getCallbackUrl(), response.getStatusCode(), duration);
136
+
137
+        } catch (Exception e) {
138
+            result.setSuccess(false);
139
+            result.setErrorMessage(e.getMessage());
140
+            log.warn("Webhook callback failed: url={}, error={}", hook.getCallbackUrl(), e.getMessage());
141
+        }
142
+
143
+        return result;
144
+    }
145
+
146
+    /**
147
+     * 生成 HMAC-SHA256 签名
148
+     */
149
+    public String generateSignature(String payload, String secretKey) {
150
+        try {
151
+            Mac mac = Mac.getInstance("HmacSHA256");
152
+            SecretKeySpec keySpec = new SecretKeySpec(
153
+                    secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
154
+            mac.init(keySpec);
155
+            byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
156
+            return bytesToHex(hash);
157
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
158
+            throw new RuntimeException("Failed to generate HMAC-SHA256 signature", e);
159
+        }
160
+    }
161
+
162
+    /**
163
+     * 验证 Webhook 签名
164
+     */
165
+    public boolean verifySignature(String payload, String secretKey, String signature) {
166
+        String expected = generateSignature(payload, secretKey);
167
+        return expected.equals(signature);
168
+    }
169
+
170
+    private String bytesToHex(byte[] bytes) {
171
+        StringBuilder sb = new StringBuilder();
172
+        for (byte b : bytes) {
173
+            sb.append(String.format("%02x", b));
174
+        }
175
+        return sb.toString();
176
+    }
177
+
178
+    /**
179
+     * 注册默认 Webhook (开发测试用)
180
+     */
181
+    private void registerDefaultWebhooks() {
182
+        // 开发环境默认不注册外部 webhook,仅记录日志
183
+        log.info("WebhookConfig initialized, no default webhooks registered");
184
+    }
185
+
186
+    /**
187
+     * Webhook 回调结果
188
+     */
189
+    @Data
190
+    public static class WebhookCallbackResult {
191
+        private String callbackUrl;
192
+        private String eventType;
193
+        private boolean success;
194
+        private int httpStatus;
195
+        private String responseBody;
196
+        private String errorMessage;
197
+        private long durationMs;
198
+    }
199
+}

+ 212
- 0
wm-bpm/src/main/java/com/water/bpm/controller/ProcessOrchestrationController.java Прегледај датотеку

@@ -0,0 +1,212 @@
1
+package com.water.bpm.controller;
2
+
3
+import com.water.bpm.config.WebhookConfig;
4
+import com.water.bpm.entity.BpmOrchestration;
5
+import com.water.bpm.entity.OrchestrationExecution;
6
+import com.water.bpm.entity.OrchestrationLog;
7
+import com.water.bpm.entity.dto.WebhookRegistration;
8
+import com.water.bpm.service.OrchestrationLogService;
9
+import com.water.bpm.service.ProcessOrchestrationService;
10
+import com.water.common.core.result.R;
11
+import io.swagger.v3.oas.annotations.Operation;
12
+import io.swagger.v3.oas.annotations.tags.Tag;
13
+import lombok.RequiredArgsConstructor;
14
+import org.springframework.web.bind.annotation.*;
15
+
16
+import java.time.LocalDateTime;
17
+import java.util.*;
18
+
19
+/**
20
+ * 跨系统流程编排控制器
21
+ */
22
+@Tag(name = "流程编排")
23
+@RestController
24
+@RequestMapping("/bpm/orchestration")
25
+@RequiredArgsConstructor
26
+public class ProcessOrchestrationController {
27
+
28
+    private final ProcessOrchestrationService orchestrationService;
29
+    private final OrchestrationLogService logService;
30
+    private final WebhookConfig webhookConfig;
31
+
32
+    // ============== 编排定义 ==============
33
+
34
+    @Operation(summary = "创建编排定义")
35
+    @PostMapping("/definitions")
36
+    public R<BpmOrchestration> createDefinition(@RequestBody BpmOrchestration orchestration) {
37
+        return R.ok(orchestrationService.createOrchestration(orchestration));
38
+    }
39
+
40
+    @Operation(summary = "更新编排定义")
41
+    @PutMapping("/definitions/{id}")
42
+    public R<BpmOrchestration> updateDefinition(@PathVariable Long id, @RequestBody BpmOrchestration orchestration) {
43
+        return R.ok(orchestrationService.updateOrchestration(id, orchestration));
44
+    }
45
+
46
+    @Operation(summary = "删除编排定义")
47
+    @DeleteMapping("/definitions/{id}")
48
+    public R<Void> deleteDefinition(@PathVariable Long id) {
49
+        orchestrationService.deleteOrchestration(id);
50
+        return R.ok();
51
+    }
52
+
53
+    @Operation(summary = "获取编排定义")
54
+    @GetMapping("/definitions/{id}")
55
+    public R<BpmOrchestration> getDefinition(@PathVariable Long id) {
56
+        return R.ok(orchestrationService.getOrchestration(id));
57
+    }
58
+
59
+    @Operation(summary = "查询编排列表")
60
+    @GetMapping("/definitions")
61
+    public R<List<BpmOrchestration>> listDefinitions(
62
+            @RequestParam(required = false) String keyword,
63
+            @RequestParam(required = false) Integer status,
64
+            @RequestParam(defaultValue = "1") int page,
65
+            @RequestParam(defaultValue = "20") int size) {
66
+        return R.ok(orchestrationService.listOrchestrations(keyword, status, page, size));
67
+    }
68
+
69
+    @Operation(summary = "启用/停用编排")
70
+    @PutMapping("/definitions/{id}/status")
71
+    public R<BpmOrchestration> toggleStatus(@PathVariable Long id, @RequestParam Integer status) {
72
+        return R.ok(orchestrationService.toggleStatus(id, status));
73
+    }
74
+
75
+    // ============== 编排执行 ==============
76
+
77
+    @Operation(summary = "启动编排执行")
78
+    @PostMapping("/executions")
79
+    public R<OrchestrationExecution> startExecution(@RequestBody Map<String, Object> request) {
80
+        Long orchestrationId = Long.parseLong(String.valueOf(request.get("orchestrationId")));
81
+        String triggeredBy = (String) request.getOrDefault("triggeredBy", "system");
82
+        String triggerType = (String) request.getOrDefault("triggerType", "manual");
83
+        @SuppressWarnings("unchecked")
84
+        Map<String, Object> inputParams = (Map<String, Object>) request.getOrDefault("inputParams", new HashMap<>());
85
+        return R.ok(orchestrationService.startExecution(orchestrationId, triggeredBy, triggerType, inputParams));
86
+    }
87
+
88
+    @Operation(summary = "获取执行记录")
89
+    @GetMapping("/executions/{id}")
90
+    public R<OrchestrationExecution> getExecution(@PathVariable Long id) {
91
+        return R.ok(orchestrationService.getExecution(id));
92
+    }
93
+
94
+    @Operation(summary = "按编号获取执行记录")
95
+    @GetMapping("/executions/no/{executionNo}")
96
+    public R<OrchestrationExecution> getExecutionByNo(@PathVariable String executionNo) {
97
+        return R.ok(orchestrationService.getExecutionByNo(executionNo));
98
+    }
99
+
100
+    @Operation(summary = "查询执行记录列表")
101
+    @GetMapping("/executions")
102
+    public R<List<OrchestrationExecution>> listExecutions(
103
+            @RequestParam(required = false) Long orchestrationId,
104
+            @RequestParam(required = false) String status,
105
+            @RequestParam(defaultValue = "1") int page,
106
+            @RequestParam(defaultValue = "20") int size) {
107
+        return R.ok(orchestrationService.listExecutions(orchestrationId, status, page, size));
108
+    }
109
+
110
+    @Operation(summary = "取消执行")
111
+    @PutMapping("/executions/{id}/cancel")
112
+    public R<OrchestrationExecution> cancelExecution(@PathVariable Long id) {
113
+        return R.ok(orchestrationService.cancelExecution(id));
114
+    }
115
+
116
+    @Operation(summary = "获取执行统计")
117
+    @GetMapping("/executions/stats")
118
+    public R<Map<String, Object>> getExecutionStats(@RequestParam(required = false) Long orchestrationId) {
119
+        return R.ok(orchestrationService.getExecutionStats(orchestrationId));
120
+    }
121
+
122
+    // ============== 编排日志 ==============
123
+
124
+    @Operation(summary = "查询执行日志")
125
+    @GetMapping("/logs")
126
+    public R<List<OrchestrationLog>> getLogs(
127
+            @RequestParam(required = false) Long executionId,
128
+            @RequestParam(required = false) String action,
129
+            @RequestParam(required = false) Boolean success,
130
+            @RequestParam(required = false) String startTime,
131
+            @RequestParam(required = false) String endTime,
132
+            @RequestParam(defaultValue = "1") int page,
133
+            @RequestParam(defaultValue = "50") int size) {
134
+        LocalDateTime start = startTime != null ? LocalDateTime.parse(startTime) : null;
135
+        LocalDateTime end = endTime != null ? LocalDateTime.parse(endTime) : null;
136
+        return R.ok(logService.queryLogs(executionId, action, success, start, end, page, size));
137
+    }
138
+
139
+    @Operation(summary = "按执行ID查询日志")
140
+    @GetMapping("/logs/execution/{executionId}")
141
+    public R<List<OrchestrationLog>> getLogsByExecutionId(@PathVariable Long executionId) {
142
+        return R.ok(logService.getLogsByExecutionId(executionId));
143
+    }
144
+
145
+    @Operation(summary = "按执行编号查询日志")
146
+    @GetMapping("/logs/execution-no/{executionNo}")
147
+    public R<List<OrchestrationLog>> getLogsByExecutionNo(@PathVariable String executionNo) {
148
+        return R.ok(logService.getLogsByExecutionNo(executionNo));
149
+    }
150
+
151
+    @Operation(summary = "查询步骤日志")
152
+    @GetMapping("/logs/execution/{executionId}/step/{stepIndex}")
153
+    public R<List<OrchestrationLog>> getStepLogs(@PathVariable Long executionId, @PathVariable Integer stepIndex) {
154
+        return R.ok(logService.getLogsByStep(executionId, stepIndex));
155
+    }
156
+
157
+    @Operation(summary = "日志统计")
158
+    @GetMapping("/logs/count")
159
+    public R<Map<String, Object>> countLogs(@RequestParam(required = false) Long executionId) {
160
+        Map<String, Object> result = new HashMap<>();
161
+        result.put("count", logService.countLogs(executionId));
162
+        return R.ok(result);
163
+    }
164
+
165
+    // ============== Webhook 管理 ==============
166
+
167
+    @Operation(summary = "注册 Webhook")
168
+    @PostMapping("/webhooks")
169
+    public R<WebhookRegistration> registerWebhook(@RequestParam String id, @RequestBody WebhookRegistration registration) {
170
+        return R.ok(webhookConfig.register(id, registration));
171
+    }
172
+
173
+    @Operation(summary = "注销 Webhook")
174
+    @DeleteMapping("/webhooks/{id}")
175
+    public R<Void> unregisterWebhook(@PathVariable String id) {
176
+        webhookConfig.unregister(id);
177
+        return R.ok();
178
+    }
179
+
180
+    @Operation(summary = "获取所有 Webhook")
181
+    @GetMapping("/webhooks")
182
+    public R<List<WebhookRegistration>> listWebhooks() {
183
+        return R.ok(webhookConfig.listAll());
184
+    }
185
+
186
+    @Operation(summary = "获取单个 Webhook")
187
+    @GetMapping("/webhooks/{id}")
188
+    public R<WebhookRegistration> getWebhook(@PathVariable String id) {
189
+        return R.ok(webhookConfig.getById(id));
190
+    }
191
+
192
+    @Operation(summary = "手动触发 Webhook 回调")
193
+    @PostMapping("/webhooks/trigger")
194
+    public R<List<WebhookConfig.WebhookCallbackResult>> triggerWebhook(@RequestBody Map<String, Object> request) {
195
+        String eventType = (String) request.get("eventType");
196
+        @SuppressWarnings("unchecked")
197
+        Map<String, Object> payload = (Map<String, Object>) request.getOrDefault("payload", new HashMap<>());
198
+        return R.ok(webhookConfig.triggerCallback(eventType, payload));
199
+    }
200
+
201
+    @Operation(summary = "验证 Webhook 签名")
202
+    @PostMapping("/webhooks/verify-signature")
203
+    public R<Map<String, Object>> verifySignature(@RequestBody Map<String, String> request) {
204
+        String payload = request.get("payload");
205
+        String secretKey = request.get("secretKey");
206
+        String signature = request.get("signature");
207
+        boolean valid = webhookConfig.verifySignature(payload, secretKey, signature);
208
+        Map<String, Object> result = new HashMap<>();
209
+        result.put("valid", valid);
210
+        return R.ok(result);
211
+    }
212
+}

+ 60
- 0
wm-bpm/src/main/java/com/water/bpm/entity/OrchestrationExecution.java Прегледај датотеку

@@ -0,0 +1,60 @@
1
+package com.water.bpm.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.time.LocalDateTime;
9
+
10
+/**
11
+ * 编排执行记录实体
12
+ * 记录每次编排执行的完整生命周期
13
+ */
14
+@Data
15
+@EqualsAndHashCode(callSuper = true)
16
+@TableName("bpm_orchestration_execution")
17
+public class OrchestrationExecution extends BaseEntity {
18
+
19
+    /** 编排ID */
20
+    private Long orchestrationId;
21
+
22
+    /** 执行编号 (UUID) */
23
+    private String executionNo;
24
+
25
+    /** 当前步骤索引 (从0开始) */
26
+    private Integer currentStepIndex;
27
+
28
+    /** 当前步骤名称 */
29
+    private String currentStepName;
30
+
31
+    /** 执行状态: pending/running/completed/failed/cancelled */
32
+    private String status;
33
+
34
+    /** 开始时间 */
35
+    private LocalDateTime startedAt;
36
+
37
+    /** 完成时间 */
38
+    private LocalDateTime completedAt;
39
+
40
+    /** 触发人 */
41
+    private String triggeredBy;
42
+
43
+    /** 触发方式: manual/scheduled/event */
44
+    private String triggerType;
45
+
46
+    /** 输入参数 JSON */
47
+    private String inputParams;
48
+
49
+    /** 输出结果 JSON */
50
+    private String outputResult;
51
+
52
+    /** 失败原因 */
53
+    private String failureReason;
54
+
55
+    /** 重试次数 */
56
+    private Integer retryCount;
57
+
58
+    /** 租户ID */
59
+    private String tenantId;
60
+}

+ 60
- 0
wm-bpm/src/main/java/com/water/bpm/entity/OrchestrationLog.java Прегледај датотеку

@@ -0,0 +1,60 @@
1
+package com.water.bpm.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import com.water.common.core.entity.BaseEntity;
5
+import lombok.Data;
6
+import lombok.EqualsAndHashCode;
7
+
8
+import java.time.LocalDateTime;
9
+
10
+/**
11
+ * 编排日志实体
12
+ * 记录编排执行过程中每个步骤的详细日志
13
+ */
14
+@Data
15
+@EqualsAndHashCode(callSuper = true)
16
+@TableName("bpm_orchestration_log")
17
+public class OrchestrationLog extends BaseEntity {
18
+
19
+    /** 执行ID */
20
+    private Long executionId;
21
+
22
+    /** 执行编号 */
23
+    private String executionNo;
24
+
25
+    /** 步骤索引 */
26
+    private Integer stepIndex;
27
+
28
+    /** 步骤名称 */
29
+    private String stepName;
30
+
31
+    /** 动作类型: start/complete/fail/retry/callback/skip */
32
+    private String action;
33
+
34
+    /** 目标系统: iot/revenue/patrol/notify/internal */
35
+    private String targetSystem;
36
+
37
+    /** 请求内容 JSON */
38
+    private String requestBody;
39
+
40
+    /** 响应内容 JSON */
41
+    private String responseBody;
42
+
43
+    /** HTTP状态码 */
44
+    private Integer httpStatus;
45
+
46
+    /** 耗时(毫秒) */
47
+    private Long durationMs;
48
+
49
+    /** 是否成功 */
50
+    private Boolean success;
51
+
52
+    /** 错误信息 */
53
+    private String errorMessage;
54
+
55
+    /** 日志时间 */
56
+    private LocalDateTime logTime;
57
+
58
+    /** 操作人 */
59
+    private String operator;
60
+}

+ 31
- 0
wm-bpm/src/main/java/com/water/bpm/entity/dto/OrchestrationStep.java Прегледај датотеку

@@ -0,0 +1,31 @@
1
+package com.water.bpm.entity.dto;
2
+
3
+import lombok.Data;
4
+
5
+/**
6
+ * 编排步骤定义
7
+ */
8
+@Data
9
+public class OrchestrationStep {
10
+
11
+    /** 步骤名称 */
12
+    private String stepName;
13
+
14
+    /** 目标系统: iot/revenue/patrol/notify */
15
+    private String targetSystem;
16
+
17
+    /** API路径 (模拟) */
18
+    private String apiPath;
19
+
20
+    /** 请求方法: GET/POST/PUT/DELETE */
21
+    private String method;
22
+
23
+    /** 超时时间(毫秒) */
24
+    private Integer timeoutMs;
25
+
26
+    /** 是否允许跳过 */
27
+    private Boolean skippable;
28
+
29
+    /** 重试次数上限 */
30
+    private Integer maxRetries;
31
+}

+ 25
- 0
wm-bpm/src/main/java/com/water/bpm/entity/dto/WebhookRegistration.java Прегледај датотеку

@@ -0,0 +1,25 @@
1
+package com.water.bpm.entity.dto;
2
+
3
+import lombok.Data;
4
+
5
+/**
6
+ * Webhook 回调注册
7
+ */
8
+@Data
9
+public class WebhookRegistration {
10
+
11
+    /** 回调URL */
12
+    private String callbackUrl;
13
+
14
+    /** 签名密钥 */
15
+    private String secretKey;
16
+
17
+    /** 是否启用签名验证 */
18
+    private Boolean signatureEnabled;
19
+
20
+    /** 事件类型: orchestration.completed/orchestration.failed/orchestration.step.completed */
21
+    private String eventType;
22
+
23
+    /** 描述 */
24
+    private String description;
25
+}

+ 22
- 0
wm-bpm/src/main/java/com/water/bpm/mapper/OrchestrationExecutionMapper.java Прегледај датотеку

@@ -0,0 +1,22 @@
1
+package com.water.bpm.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.bpm.entity.OrchestrationExecution;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.util.List;
10
+
11
+@Mapper
12
+public interface OrchestrationExecutionMapper extends BaseMapper<OrchestrationExecution> {
13
+
14
+    @Select("SELECT * FROM bpm_orchestration_execution WHERE orchestration_id = #{orchestrationId} AND deleted = 0 ORDER BY created_at DESC")
15
+    List<OrchestrationExecution> selectByOrchestrationId(@Param("orchestrationId") Long orchestrationId);
16
+
17
+    @Select("SELECT * FROM bpm_orchestration_execution WHERE execution_no = #{executionNo} AND deleted = 0 LIMIT 1")
18
+    OrchestrationExecution selectByExecutionNo(@Param("executionNo") String executionNo);
19
+
20
+    @Select("SELECT * FROM bpm_orchestration_execution WHERE status = #{status} AND deleted = 0 ORDER BY created_at ASC")
21
+    List<OrchestrationExecution> selectByStatus(@Param("status") String status);
22
+}

+ 22
- 0
wm-bpm/src/main/java/com/water/bpm/mapper/OrchestrationLogMapper.java Прегледај датотеку

@@ -0,0 +1,22 @@
1
+package com.water.bpm.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.water.bpm.entity.OrchestrationLog;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.util.List;
10
+
11
+@Mapper
12
+public interface OrchestrationLogMapper extends BaseMapper<OrchestrationLog> {
13
+
14
+    @Select("SELECT * FROM bpm_orchestration_log WHERE execution_id = #{executionId} AND deleted = 0 ORDER BY log_time ASC")
15
+    List<OrchestrationLog> selectByExecutionId(@Param("executionId") Long executionId);
16
+
17
+    @Select("SELECT * FROM bpm_orchestration_log WHERE execution_no = #{executionNo} AND deleted = 0 ORDER BY log_time ASC")
18
+    List<OrchestrationLog> selectByExecutionNo(@Param("executionNo") String executionNo);
19
+
20
+    @Select("SELECT * FROM bpm_orchestration_log WHERE execution_id = #{executionId} AND step_index = #{stepIndex} AND deleted = 0 ORDER BY log_time ASC")
21
+    List<OrchestrationLog> selectByStep(@Param("executionId") Long executionId, @Param("stepIndex") Integer stepIndex);
22
+}

+ 115
- 0
wm-bpm/src/main/java/com/water/bpm/service/OrchestrationLogService.java Прегледај датотеку

@@ -0,0 +1,115 @@
1
+package com.water.bpm.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.water.bpm.entity.OrchestrationLog;
5
+import com.water.bpm.mapper.OrchestrationLogMapper;
6
+import lombok.RequiredArgsConstructor;
7
+import lombok.extern.slf4j.Slf4j;
8
+import org.springframework.stereotype.Service;
9
+
10
+import java.time.LocalDateTime;
11
+import java.util.List;
12
+
13
+/**
14
+ * 编排日志服务
15
+ * 负责记录、查询编排执行过程中的详细日志
16
+ */
17
+@Slf4j
18
+@Service
19
+@RequiredArgsConstructor
20
+public class OrchestrationLogService {
21
+
22
+    private final OrchestrationLogMapper orchestrationLogMapper;
23
+
24
+    /**
25
+     * 记录日志
26
+     */
27
+    public OrchestrationLog recordLog(Long executionId, String executionNo, Integer stepIndex,
28
+                                       String stepName, String action, String targetSystem,
29
+                                       String requestBody, String responseBody,
30
+                                       Integer httpStatus, Long durationMs,
31
+                                       Boolean success, String errorMessage, String operator) {
32
+        OrchestrationLog logEntry = new OrchestrationLog();
33
+        logEntry.setExecutionId(executionId);
34
+        logEntry.setExecutionNo(executionNo);
35
+        logEntry.setStepIndex(stepIndex);
36
+        logEntry.setStepName(stepName);
37
+        logEntry.setAction(action);
38
+        logEntry.setTargetSystem(targetSystem);
39
+        logEntry.setRequestBody(requestBody);
40
+        logEntry.setResponseBody(responseBody);
41
+        logEntry.setHttpStatus(httpStatus);
42
+        logEntry.setDurationMs(durationMs);
43
+        logEntry.setSuccess(success);
44
+        logEntry.setErrorMessage(errorMessage);
45
+        logEntry.setLogTime(LocalDateTime.now());
46
+        logEntry.setOperator(operator);
47
+        logEntry.setCreatedAt(LocalDateTime.now());
48
+
49
+        orchestrationLogMapper.insert(logEntry);
50
+        log.debug("Recorded orchestration log: executionNo={}, step={}, action={}, success={}",
51
+                executionNo, stepName, action, success);
52
+
53
+        return logEntry;
54
+    }
55
+
56
+    /**
57
+     * 查询执行日志
58
+     */
59
+    public List<OrchestrationLog> getLogsByExecutionId(Long executionId) {
60
+        return orchestrationLogMapper.selectByExecutionId(executionId);
61
+    }
62
+
63
+    /**
64
+     * 按执行编号查询日志
65
+     */
66
+    public List<OrchestrationLog> getLogsByExecutionNo(String executionNo) {
67
+        return orchestrationLogMapper.selectByExecutionNo(executionNo);
68
+    }
69
+
70
+    /**
71
+     * 查询指定步骤的日志
72
+     */
73
+    public List<OrchestrationLog> getLogsByStep(Long executionId, Integer stepIndex) {
74
+        return orchestrationLogMapper.selectByStep(executionId, stepIndex);
75
+    }
76
+
77
+    /**
78
+     * 分页查询日志
79
+     */
80
+    public List<OrchestrationLog> queryLogs(Long executionId, String action, Boolean success,
81
+                                             LocalDateTime startTime, LocalDateTime endTime,
82
+                                             int page, int size) {
83
+        LambdaQueryWrapper<OrchestrationLog> wrapper = new LambdaQueryWrapper<>();
84
+        if (executionId != null) {
85
+            wrapper.eq(OrchestrationLog::getExecutionId, executionId);
86
+        }
87
+        if (action != null && !action.isEmpty()) {
88
+            wrapper.eq(OrchestrationLog::getAction, action);
89
+        }
90
+        if (success != null) {
91
+            wrapper.eq(OrchestrationLog::getSuccess, success);
92
+        }
93
+        if (startTime != null) {
94
+            wrapper.ge(OrchestrationLog::getLogTime, startTime);
95
+        }
96
+        if (endTime != null) {
97
+            wrapper.le(OrchestrationLog::getLogTime, endTime);
98
+        }
99
+        wrapper.orderByAsc(OrchestrationLog::getLogTime);
100
+        wrapper.last("LIMIT " + size + " OFFSET " + (page - 1) * size);
101
+
102
+        return orchestrationLogMapper.selectList(wrapper);
103
+    }
104
+
105
+    /**
106
+     * 统计执行日志数量
107
+     */
108
+    public long countLogs(Long executionId) {
109
+        LambdaQueryWrapper<OrchestrationLog> wrapper = new LambdaQueryWrapper<>();
110
+        if (executionId != null) {
111
+            wrapper.eq(OrchestrationLog::getExecutionId, executionId);
112
+        }
113
+        return orchestrationLogMapper.selectCount(wrapper);
114
+    }
115
+}

+ 481
- 0
wm-bpm/src/main/java/com/water/bpm/service/ProcessOrchestrationService.java Прегледај датотеку

@@ -0,0 +1,481 @@
1
+package com.water.bpm.service;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.fasterxml.jackson.core.JsonProcessingException;
5
+import com.fasterxml.jackson.core.type.TypeReference;
6
+import com.fasterxml.jackson.databind.ObjectMapper;
7
+import com.water.bpm.config.WebhookConfig;
8
+import com.water.bpm.entity.BpmOrchestration;
9
+import com.water.bpm.entity.OrchestrationExecution;
10
+import com.water.bpm.entity.dto.OrchestrationStep;
11
+import com.water.bpm.mapper.BpmOrchestrationMapper;
12
+import com.water.bpm.mapper.OrchestrationExecutionMapper;
13
+import lombok.RequiredArgsConstructor;
14
+import lombok.extern.slf4j.Slf4j;
15
+import org.springframework.stereotype.Service;
16
+import org.springframework.transaction.annotation.Transactional;
17
+
18
+import java.time.LocalDateTime;
19
+import java.util.*;
20
+import java.util.concurrent.ConcurrentHashMap;
21
+import java.util.stream.Collectors;
22
+
23
+/**
24
+ * 跨系统流程编排服务
25
+ * 负责编排定义管理、执行调度、步骤流转、跨系统API调用、Webhook回调、重试机制
26
+ */
27
+@Slf4j
28
+@Service
29
+@RequiredArgsConstructor
30
+public class ProcessOrchestrationService {
31
+
32
+    private final BpmOrchestrationMapper orchestrationMapper;
33
+    private final OrchestrationExecutionMapper executionMapper;
34
+    private final OrchestrationLogService logService;
35
+    private final WebhookConfig webhookConfig;
36
+    private final ObjectMapper objectMapper = new ObjectMapper();
37
+
38
+    /** 模拟外部系统响应 */
39
+    private final Map<String, Map<String, Object>> simulatedResponses = new ConcurrentHashMap<>();
40
+
41
+    // ============== 编排定义 CRUD ==============
42
+
43
+    /**
44
+     * 创建编排定义
45
+     */
46
+    @Transactional
47
+    public BpmOrchestration createOrchestration(BpmOrchestration orchestration) {
48
+        orchestration.setStatus(0); // 草稿
49
+        orchestration.setExecutionCount(0);
50
+        orchestration.setCreatedAt(LocalDateTime.now());
51
+        orchestrationMapper.insert(orchestration);
52
+        log.info("Created orchestration: id={}, name={}", orchestration.getId(), orchestration.getOrchestrationName());
53
+        return orchestration;
54
+    }
55
+
56
+    /**
57
+     * 更新编排定义
58
+     */
59
+    @Transactional
60
+    public BpmOrchestration updateOrchestration(Long id, BpmOrchestration orchestration) {
61
+        orchestration.setId(id);
62
+        orchestration.setUpdatedAt(LocalDateTime.now());
63
+        orchestrationMapper.updateById(orchestration);
64
+        log.info("Updated orchestration: id={}", id);
65
+        return orchestrationMapper.selectById(id);
66
+    }
67
+
68
+    /**
69
+     * 删除编排定义 (逻辑删除)
70
+     */
71
+    @Transactional
72
+    public void deleteOrchestration(Long id) {
73
+        orchestrationMapper.deleteById(id);
74
+        log.info("Deleted orchestration: id={}", id);
75
+    }
76
+
77
+    /**
78
+     * 获取编排定义
79
+     */
80
+    public BpmOrchestration getOrchestration(Long id) {
81
+        return orchestrationMapper.selectById(id);
82
+    }
83
+
84
+    /**
85
+     * 查询编排列表
86
+     */
87
+    public List<BpmOrchestration> listOrchestrations(String keyword, Integer status, int page, int size) {
88
+        LambdaQueryWrapper<BpmOrchestration> wrapper = new LambdaQueryWrapper<>();
89
+        if (keyword != null && !keyword.isEmpty()) {
90
+            wrapper.and(w -> w.like(BpmOrchestration::getOrchestrationName, keyword)
91
+                    .or().like(BpmOrchestration::getOrchestrationCode, keyword));
92
+        }
93
+        if (status != null) {
94
+            wrapper.eq(BpmOrchestration::getStatus, status);
95
+        }
96
+        wrapper.orderByDesc(BpmOrchestration::getCreatedAt);
97
+        wrapper.last("LIMIT " + size + " OFFSET " + (page - 1) * size);
98
+        return orchestrationMapper.selectList(wrapper);
99
+    }
100
+
101
+    /**
102
+     * 启用/停用编排
103
+     */
104
+    @Transactional
105
+    public BpmOrchestration toggleStatus(Long id, Integer status) {
106
+        BpmOrchestration orchestration = orchestrationMapper.selectById(id);
107
+        if (orchestration == null) {
108
+            throw new RuntimeException("编排定义不存在: " + id);
109
+        }
110
+        orchestration.setStatus(status);
111
+        orchestration.setUpdatedAt(LocalDateTime.now());
112
+        orchestrationMapper.updateById(orchestration);
113
+        log.info("Orchestration status changed: id={}, status={}", id, status);
114
+        return orchestration;
115
+    }
116
+
117
+    // ============== 编排执行 ==============
118
+
119
+    /**
120
+     * 启动编排执行
121
+     */
122
+    @Transactional
123
+    public OrchestrationExecution startExecution(Long orchestrationId, String triggeredBy,
124
+                                                   String triggerType, Map<String, Object> inputParams) {
125
+        BpmOrchestration orchestration = orchestrationMapper.selectById(orchestrationId);
126
+        if (orchestration == null) {
127
+            throw new RuntimeException("编排定义不存在: " + orchestrationId);
128
+        }
129
+        if (orchestration.getStatus() != 1) {
130
+            throw new RuntimeException("编排未启用,无法执行: " + orchestration.getOrchestrationName());
131
+        }
132
+
133
+        // 解析编排步骤
134
+        List<OrchestrationStep> steps = parseSteps(orchestration.getOrchestrationRules());
135
+        if (steps.isEmpty()) {
136
+            throw new RuntimeException("编排步骤为空: " + orchestration.getOrchestrationName());
137
+        }
138
+
139
+        // 创建执行记录
140
+        OrchestrationExecution execution = new OrchestrationExecution();
141
+        execution.setOrchestrationId(orchestrationId);
142
+        execution.setExecutionNo(UUID.randomUUID().toString());
143
+        execution.setCurrentStepIndex(0);
144
+        execution.setCurrentStepName(steps.get(0).getStepName());
145
+        execution.setStatus("running");
146
+        execution.setStartedAt(LocalDateTime.now());
147
+        execution.setTriggeredBy(triggeredBy);
148
+        execution.setTriggerType(triggerType);
149
+        execution.setRetryCount(0);
150
+        execution.setCreatedAt(LocalDateTime.now());
151
+
152
+        try {
153
+            execution.setInputParams(objectMapper.writeValueAsString(inputParams));
154
+        } catch (JsonProcessingException e) {
155
+            execution.setInputParams("{}");
156
+        }
157
+
158
+        executionMapper.insert(execution);
159
+
160
+        // 更新编排执行次数
161
+        orchestration.setExecutionCount(orchestration.getExecutionCount() + 1);
162
+        orchestrationMapper.updateById(orchestration);
163
+
164
+        // 记录开始日志
165
+        logService.recordLog(execution.getId(), execution.getExecutionNo(), 0,
166
+                steps.get(0).getStepName(), "start", "internal",
167
+                execution.getInputParams(), null, null, 0L, true, null, triggeredBy);
168
+
169
+        log.info("Started orchestration execution: orchestrationId={}, executionNo={}, steps={}",
170
+                orchestrationId, execution.getExecutionNo(), steps.size());
171
+
172
+        // 异步执行步骤 (这里同步模拟)
173
+        executeSteps(execution, steps, inputParams);
174
+
175
+        return executionMapper.selectById(execution.getId());
176
+    }
177
+
178
+    /**
179
+     * 执行编排步骤 (同步模拟,生产环境应异步)
180
+     */
181
+    private void executeSteps(OrchestrationExecution execution, List<OrchestrationStep> steps,
182
+                               Map<String, Object> inputParams) {
183
+        int startIndex = execution.getCurrentStepIndex();
184
+        Map<String, Object> context = new HashMap<>(inputParams != null ? inputParams : Collections.emptyMap());
185
+
186
+        for (int i = startIndex; i < steps.size(); i++) {
187
+            OrchestrationStep step = steps.get(i);
188
+            long stepStart = System.currentTimeMillis();
189
+
190
+            log.info("Executing step {}/{}: {} -> {}", i + 1, steps.size(),
191
+                    step.getStepName(), step.getTargetSystem());
192
+
193
+            // 更新当前步骤
194
+            execution.setCurrentStepIndex(i);
195
+            execution.setCurrentStepName(step.getStepName());
196
+            executionMapper.updateById(execution);
197
+
198
+            // 执行步骤 (含重试)
199
+            boolean success = executeStepWithRetry(execution, step, i, context);
200
+
201
+            long duration = System.currentTimeMillis() - stepStart;
202
+
203
+            if (success) {
204
+                // 步骤完成日志
205
+                logService.recordLog(execution.getId(), execution.getExecutionNo(), i,
206
+                        step.getStepName(), "complete", step.getTargetSystem(),
207
+                        null, "{\"status\":\"success\"}", 200, duration, true, null, execution.getTriggeredBy());
208
+
209
+                // 触发步骤完成 Webhook
210
+                Map<String, Object> stepPayload = new HashMap<>();
211
+                stepPayload.put("executionNo", execution.getExecutionNo());
212
+                stepPayload.put("stepIndex", i);
213
+                stepPayload.put("stepName", step.getStepName());
214
+                stepPayload.put("targetSystem", step.getTargetSystem());
215
+                stepPayload.put("success", true);
216
+                webhookConfig.triggerCallback("orchestration.step.completed", stepPayload);
217
+
218
+                log.info("Step completed: {} ({}ms)", step.getStepName(), duration);
219
+            } else {
220
+                // 步骤失败
221
+                if (Boolean.TRUE.equals(step.getSkippable())) {
222
+                    logService.recordLog(execution.getId(), execution.getExecutionNo(), i,
223
+                            step.getStepName(), "skip", step.getTargetSystem(),
224
+                            null, null, null, duration, false, "步骤失败但可跳过", execution.getTriggeredBy());
225
+                    log.warn("Step failed but skippable: {}", step.getStepName());
226
+                } else {
227
+                    // 不可跳过,执行失败
228
+                    execution.setStatus("failed");
229
+                    execution.setCompletedAt(LocalDateTime.now());
230
+                    execution.setFailureReason("步骤失败: " + step.getStepName());
231
+                    executionMapper.updateById(execution);
232
+
233
+                    logService.recordLog(execution.getId(), execution.getExecutionNo(), i,
234
+                            step.getStepName(), "fail", step.getTargetSystem(),
235
+                            null, null, 500, duration, false, "步骤执行失败,已达最大重试次数", execution.getTriggeredBy());
236
+
237
+                    // 触发失败 Webhook
238
+                    Map<String, Object> failPayload = new HashMap<>();
239
+                    failPayload.put("executionNo", execution.getExecutionNo());
240
+                    failPayload.put("orchestrationId", execution.getOrchestrationId());
241
+                    failPayload.put("failedStep", step.getStepName());
242
+                    failPayload.put("reason", "步骤执行失败");
243
+                    webhookConfig.triggerCallback("orchestration.failed", failPayload);
244
+
245
+                    return;
246
+                }
247
+            }
248
+        }
249
+
250
+        // 所有步骤完成
251
+        execution.setStatus("completed");
252
+        execution.setCompletedAt(LocalDateTime.now());
253
+        execution.setCurrentStepIndex(steps.size() - 1);
254
+        try {
255
+            execution.setOutputResult(objectMapper.writeValueAsString(context));
256
+        } catch (JsonProcessingException e) {
257
+            execution.setOutputResult("{\"status\":\"completed\"}");
258
+        }
259
+        executionMapper.updateById(execution);
260
+
261
+        // 触发完成 Webhook
262
+        Map<String, Object> completePayload = new HashMap<>();
263
+        completePayload.put("executionNo", execution.getExecutionNo());
264
+        completePayload.put("orchestrationId", execution.getOrchestrationId());
265
+        completePayload.put("totalSteps", steps.size());
266
+        completePayload.put("status", "completed");
267
+        webhookConfig.triggerCallback("orchestration.completed", completePayload);
268
+
269
+        log.info("Orchestration execution completed: executionNo={}, steps={}", execution.getExecutionNo(), steps.size());
270
+    }
271
+
272
+    /**
273
+     * 执行单个步骤 (含重试机制)
274
+     */
275
+    private boolean executeStepWithRetry(OrchestrationExecution execution, OrchestrationStep step,
276
+                                          int stepIndex, Map<String, Object> context) {
277
+        int maxRetries = step.getMaxRetries() != null ? step.getMaxRetries() : 3;
278
+        int retryCount = 0;
279
+
280
+        while (retryCount <= maxRetries) {
281
+            try {
282
+                boolean success = callExternalSystem(step, context);
283
+                if (success) {
284
+                    return true;
285
+                }
286
+                retryCount++;
287
+                if (retryCount <= maxRetries) {
288
+                    logService.recordLog(execution.getId(), execution.getExecutionNo(), stepIndex,
289
+                            step.getStepName(), "retry", step.getTargetSystem(),
290
+                            null, null, null, 0L, false, "重试第" + retryCount + "次", execution.getTriggeredBy());
291
+                    log.warn("Step {} failed, retrying ({}/{})", step.getStepName(), retryCount, maxRetries);
292
+                    Thread.sleep(500L * retryCount); // 指数退避
293
+                }
294
+            } catch (Exception e) {
295
+                retryCount++;
296
+                log.error("Step {} error: {}", step.getStepName(), e.getMessage());
297
+                if (retryCount > maxRetries) break;
298
+                try {
299
+                    Thread.sleep(500L * retryCount);
300
+                } catch (InterruptedException ie) {
301
+                    Thread.currentThread().interrupt();
302
+                    break;
303
+                }
304
+            }
305
+        }
306
+
307
+        execution.setRetryCount(execution.getRetryCount() + retryCount);
308
+        return false;
309
+    }
310
+
311
+    /**
312
+     * 调用外部系统 API (模拟)
313
+     * 实际生产中应使用 RestTemplate/WebClient 调用真实 API
314
+     */
315
+    private boolean callExternalSystem(OrchestrationStep step, Map<String, Object> context) {
316
+        String targetSystem = step.getTargetSystem();
317
+        String apiPath = step.getApiPath();
318
+
319
+        log.info("Calling external system: {} -> {}", targetSystem, apiPath);
320
+
321
+        // 模拟不同系统的响应
322
+        switch (targetSystem.toLowerCase()) {
323
+            case "iot":
324
+                return simulateIoTCall(step, context);
325
+            case "revenue":
326
+                return simulateRevenueCall(step, context);
327
+            case "patrol":
328
+                return simulatePatrolCall(step, context);
329
+            case "notify":
330
+                return simulateNotifyCall(step, context);
331
+            default:
332
+                log.warn("Unknown target system: {}, treating as success", targetSystem);
333
+                return true;
334
+        }
335
+    }
336
+
337
+    private boolean simulateIoTCall(OrchestrationStep step, Map<String, Object> context) {
338
+        // 模拟 IoT 设备数据查询
339
+        context.put("iot_deviceCount", 156);
340
+        context.put("iot_onlineDevices", 142);
341
+        context.put("iot_alertCount", 3);
342
+        context.put("iot_lastQuery", LocalDateTime.now().toString());
343
+        log.info("IoT API simulated: deviceCount=156, onlineDevices=142, alerts=3");
344
+        return true;
345
+    }
346
+
347
+    private boolean simulateRevenueCall(OrchestrationStep step, Map<String, Object> context) {
348
+        // 模拟营收系统数据同步
349
+        context.put("revenue_totalAmount", 125680.50);
350
+        context.put("revenue_transactionCount", 342);
351
+        context.put("revenue_syncStatus", "completed");
352
+        log.info("Revenue API simulated: total=125680.50, transactions=342");
353
+        return true;
354
+    }
355
+
356
+    private boolean simulatePatrolCall(OrchestrationStep step, Map<String, Object> context) {
357
+        // 模拟巡检系统任务派发
358
+        context.put("patrol_tasksCreated", 12);
359
+        context.put("patrol_assignedTeams", 4);
360
+        context.put("patrol_coverageRate", "95%");
361
+        log.info("Patrol API simulated: tasks=12, teams=4, coverage=95%");
362
+        return true;
363
+    }
364
+
365
+    private boolean simulateNotifyCall(OrchestrationStep step, Map<String, Object> context) {
366
+        // 模拟通知系统消息发送
367
+        context.put("notify_messagesSent", 28);
368
+        context.put("notify_channels", "sms,email,wechat");
369
+        log.info("Notify API simulated: messages=28, channels=sms,email,wechat");
370
+        return true;
371
+    }
372
+
373
+    // ============== 执行记录查询 ==============
374
+
375
+    /**
376
+     * 获取执行记录
377
+     */
378
+    public OrchestrationExecution getExecution(Long id) {
379
+        return executionMapper.selectById(id);
380
+    }
381
+
382
+    /**
383
+     * 按执行编号获取
384
+     */
385
+    public OrchestrationExecution getExecutionByNo(String executionNo) {
386
+        return executionMapper.selectByExecutionNo(executionNo);
387
+    }
388
+
389
+    /**
390
+     * 查询编排的执行记录
391
+     */
392
+    public List<OrchestrationExecution> listExecutions(Long orchestrationId, String status, int page, int size) {
393
+        LambdaQueryWrapper<OrchestrationExecution> wrapper = new LambdaQueryWrapper<>();
394
+        if (orchestrationId != null) {
395
+            wrapper.eq(OrchestrationExecution::getOrchestrationId, orchestrationId);
396
+        }
397
+        if (status != null && !status.isEmpty()) {
398
+            wrapper.eq(OrchestrationExecution::getStatus, status);
399
+        }
400
+        wrapper.orderByDesc(OrchestrationExecution::getCreatedAt);
401
+        wrapper.last("LIMIT " + size + " OFFSET " + (page - 1) * size);
402
+        return executionMapper.selectList(wrapper);
403
+    }
404
+
405
+    /**
406
+     * 取消执行
407
+     */
408
+    @Transactional
409
+    public OrchestrationExecution cancelExecution(Long executionId) {
410
+        OrchestrationExecution execution = executionMapper.selectById(executionId);
411
+        if (execution == null) {
412
+            throw new RuntimeException("执行记录不存在: " + executionId);
413
+        }
414
+        if ("completed".equals(execution.getStatus()) || "failed".equals(execution.getStatus())) {
415
+            throw new RuntimeException("已完成或失败的执行无法取消");
416
+        }
417
+        execution.setStatus("cancelled");
418
+        execution.setCompletedAt(LocalDateTime.now());
419
+        executionMapper.updateById(execution);
420
+
421
+        logService.recordLog(executionId, execution.getExecutionNo(), execution.getCurrentStepIndex(),
422
+                execution.getCurrentStepName(), "cancel", "internal",
423
+                null, null, null, 0L, true, "用户取消执行", "system");
424
+
425
+        log.info("Cancelled execution: id={}, executionNo={}", executionId, execution.getExecutionNo());
426
+        return execution;
427
+    }
428
+
429
+    // ============== 辅助方法 ==============
430
+
431
+    /**
432
+     * 解析编排步骤
433
+     */
434
+    public List<OrchestrationStep> parseSteps(String orchestrationRules) {
435
+        if (orchestrationRules == null || orchestrationRules.isEmpty()) {
436
+            return Collections.emptyList();
437
+        }
438
+        try {
439
+            return objectMapper.readValue(orchestrationRules, new TypeReference<List<OrchestrationStep>>() {});
440
+        } catch (JsonProcessingException e) {
441
+            log.error("Failed to parse orchestration rules: {}", e.getMessage());
442
+            return Collections.emptyList();
443
+        }
444
+    }
445
+
446
+    /**
447
+     * 获取执行统计
448
+     */
449
+    public Map<String, Object> getExecutionStats(Long orchestrationId) {
450
+        Map<String, Object> stats = new HashMap<>();
451
+        LambdaQueryWrapper<OrchestrationExecution> wrapper = new LambdaQueryWrapper<>();
452
+        if (orchestrationId != null) {
453
+            wrapper.eq(OrchestrationExecution::getOrchestrationId, orchestrationId);
454
+        }
455
+
456
+        long total = executionMapper.selectCount(wrapper);
457
+
458
+        wrapper = new LambdaQueryWrapper<>();
459
+        if (orchestrationId != null) wrapper.eq(OrchestrationExecution::getOrchestrationId, orchestrationId);
460
+        wrapper.eq(OrchestrationExecution::getStatus, "completed");
461
+        long completed = executionMapper.selectCount(wrapper);
462
+
463
+        wrapper = new LambdaQueryWrapper<>();
464
+        if (orchestrationId != null) wrapper.eq(OrchestrationExecution::getOrchestrationId, orchestrationId);
465
+        wrapper.eq(OrchestrationExecution::getStatus, "failed");
466
+        long failed = executionMapper.selectCount(wrapper);
467
+
468
+        wrapper = new LambdaQueryWrapper<>();
469
+        if (orchestrationId != null) wrapper.eq(OrchestrationExecution::getOrchestrationId, orchestrationId);
470
+        wrapper.eq(OrchestrationExecution::getStatus, "running");
471
+        long running = executionMapper.selectCount(wrapper);
472
+
473
+        stats.put("total", total);
474
+        stats.put("completed", completed);
475
+        stats.put("failed", failed);
476
+        stats.put("running", running);
477
+        stats.put("successRate", total > 0 ? String.format("%.1f%%", (completed * 100.0 / total)) : "N/A");
478
+
479
+        return stats;
480
+    }
481
+}

+ 106
- 0
wm-bpm/src/main/resources/db/V_bpm_orchestration.sql Прегледај датотеку

@@ -0,0 +1,106 @@
1
+-- =====================================================
2
+-- 跨系统流程编排 DDL
3
+-- Issue #35: 跨系统流程编排 + Webhook 回调通知
4
+-- =====================================================
5
+
6
+-- 编排定义表 (已存在则跳过)
7
+CREATE TABLE IF NOT EXISTS bpm_orchestration (
8
+    id              BIGSERIAL PRIMARY KEY,
9
+    orchestration_name  VARCHAR(200) NOT NULL,
10
+    orchestration_code  VARCHAR(100) UNIQUE,
11
+    description         TEXT,
12
+    process_definition_ids  JSONB,
13
+    orchestration_rules     JSONB,
14
+    trigger_type        VARCHAR(50) DEFAULT 'manual',
15
+    cron_expression     VARCHAR(100),
16
+    event_name          VARCHAR(200),
17
+    status              INTEGER DEFAULT 0,
18
+    created_by          VARCHAR(100),
19
+    execution_count     INTEGER DEFAULT 0,
20
+    tenant_id           VARCHAR(100),
21
+    deleted             INTEGER DEFAULT 0,
22
+    created_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
23
+    updated_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP
24
+);
25
+
26
+-- 编排执行记录表
27
+CREATE TABLE IF NOT EXISTS bpm_orchestration_execution (
28
+    id                  BIGSERIAL PRIMARY KEY,
29
+    orchestration_id    BIGINT NOT NULL REFERENCES bpm_orchestration(id),
30
+    execution_no        VARCHAR(100) UNIQUE NOT NULL,
31
+    current_step_index  INTEGER DEFAULT 0,
32
+    current_step_name   VARCHAR(200),
33
+    status              VARCHAR(50) DEFAULT 'pending',
34
+    started_at          TIMESTAMP,
35
+    completed_at        TIMESTAMP,
36
+    triggered_by        VARCHAR(100),
37
+    trigger_type        VARCHAR(50) DEFAULT 'manual',
38
+    input_params        JSONB,
39
+    output_result       JSONB,
40
+    failure_reason      TEXT,
41
+    retry_count         INTEGER DEFAULT 0,
42
+    tenant_id           VARCHAR(100),
43
+    deleted             INTEGER DEFAULT 0,
44
+    created_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
45
+    updated_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP
46
+);
47
+
48
+CREATE INDEX IF NOT EXISTS idx_orch_exec_orch_id ON bpm_orchestration_execution(orchestration_id);
49
+CREATE INDEX IF NOT EXISTS idx_orch_exec_no ON bpm_orchestration_execution(execution_no);
50
+CREATE INDEX IF NOT EXISTS idx_orch_exec_status ON bpm_orchestration_execution(status);
51
+CREATE INDEX IF NOT EXISTS idx_orch_exec_started ON bpm_orchestration_execution(started_at);
52
+
53
+-- 编排日志表
54
+CREATE TABLE IF NOT EXISTS bpm_orchestration_log (
55
+    id                  BIGSERIAL PRIMARY KEY,
56
+    execution_id        BIGINT NOT NULL REFERENCES bpm_orchestration_execution(id),
57
+    execution_no        VARCHAR(100) NOT NULL,
58
+    step_index          INTEGER,
59
+    step_name           VARCHAR(200),
60
+    action              VARCHAR(50) NOT NULL,
61
+    target_system       VARCHAR(100),
62
+    request_body        JSONB,
63
+    response_body       JSONB,
64
+    http_status         INTEGER,
65
+    duration_ms         BIGINT,
66
+    success             BOOLEAN DEFAULT TRUE,
67
+    error_message       TEXT,
68
+    log_time            TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
69
+    operator            VARCHAR(100),
70
+    deleted             INTEGER DEFAULT 0,
71
+    created_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
72
+    updated_at          TIMESTAMP DEFAULT CURRENT_TIMESTAMP
73
+);
74
+
75
+CREATE INDEX IF NOT EXISTS idx_orch_log_exec_id ON bpm_orchestration_log(execution_id);
76
+CREATE INDEX IF NOT EXISTS idx_orch_log_exec_no ON bpm_orchestration_log(execution_no);
77
+CREATE INDEX IF NOT EXISTS idx_orch_log_time ON bpm_orchestration_log(log_time);
78
+CREATE INDEX IF NOT EXISTS idx_orch_log_action ON bpm_orchestration_log(action);
79
+
80
+-- 插入示例编排定义
81
+INSERT INTO bpm_orchestration (orchestration_name, orchestration_code, description, trigger_type, orchestration_rules, status, created_by)
82
+VALUES
83
+('日常巡检数据同步编排', 'DAILY_PATROL_SYNC', '每日自动同步IoT设备数据、巡检任务、营收数据并发送通知', 'scheduled',
84
+ '[
85
+   {"stepName":"查询IoT设备状态","targetSystem":"iot","apiPath":"/api/iot/devices/status","method":"GET","timeoutMs":5000,"skippable":false,"maxRetries":3},
86
+   {"stepName":"同步巡检任务","targetSystem":"patrol","apiPath":"/api/patrol/tasks/sync","method":"POST","timeoutMs":10000,"skippable":false,"maxRetries":3},
87
+   {"stepName":"汇总营收数据","targetSystem":"revenue","apiPath":"/api/revenue/daily/summary","method":"GET","timeoutMs":8000,"skippable":true,"maxRetries":2},
88
+   {"stepName":"发送汇总通知","targetSystem":"notify","apiPath":"/api/notify/send","method":"POST","timeoutMs":5000,"skippable":true,"maxRetries":1}
89
+ ]',
90
+ 1, 'system'),
91
+('应急事件处理编排', 'EMERGENCY_RESPONSE', '突发事件触发时:获取设备数据→派发巡检任务→通知相关人员', 'event',
92
+ '[
93
+   {"stepName":"获取异常设备数据","targetSystem":"iot","apiPath":"/api/iot/devices/alerts","method":"GET","timeoutMs":3000,"skippable":false,"maxRetries":3},
94
+   {"stepName":"派发应急巡检任务","targetSystem":"patrol","apiPath":"/api/patrol/tasks/emergency","method":"POST","timeoutMs":5000,"skippable":false,"maxRetries":3},
95
+   {"stepName":"发送紧急通知","targetSystem":"notify","apiPath":"/api/notify/emergency","method":"POST","timeoutMs":3000,"skippable":false,"maxRetries":2}
96
+ ]',
97
+ 1, 'system')
98
+ON CONFLICT DO NOTHING;
99
+
100
+-- 注释
101
+COMMENT ON TABLE bpm_orchestration IS '跨系统流程编排定义表';
102
+COMMENT ON TABLE bpm_orchestration_execution IS '编排执行记录表';
103
+COMMENT ON TABLE bpm_orchestration_log IS '编排执行日志表';
104
+COMMENT ON COLUMN bpm_orchestration.orchestration_rules IS '编排步骤JSON数组';
105
+COMMENT ON COLUMN bpm_orchestration_execution.status IS '执行状态: pending/running/completed/failed/cancelled';
106
+COMMENT ON COLUMN bpm_orchestration_log.action IS '动作类型: start/complete/fail/retry/callback/skip/cancel';

+ 161
- 0
wm-bpm/src/test/java/com/water/bpm/config/WebhookConfigTest.java Прегледај датотеку

@@ -0,0 +1,161 @@
1
+package com.water.bpm.config;
2
+
3
+import com.water.bpm.entity.dto.WebhookRegistration;
4
+import org.junit.jupiter.api.BeforeEach;
5
+import org.junit.jupiter.api.DisplayName;
6
+import org.junit.jupiter.api.Test;
7
+
8
+import java.util.HashMap;
9
+import java.util.List;
10
+import java.util.Map;
11
+
12
+import static org.junit.jupiter.api.Assertions.*;
13
+
14
+@DisplayName("Webhook 配置测试")
15
+class WebhookConfigTest {
16
+
17
+    private WebhookConfig webhookConfig;
18
+
19
+    @BeforeEach
20
+    void setUp() {
21
+        webhookConfig = new WebhookConfig();
22
+    }
23
+
24
+    @Test
25
+    @DisplayName("注册 Webhook")
26
+    void testRegister() {
27
+        WebhookRegistration reg = createRegistration("http://localhost:8080/callback", "orchestration.completed");
28
+        WebhookRegistration result = webhookConfig.register("test-1", reg);
29
+
30
+        assertNotNull(result);
31
+        assertEquals("http://localhost:8080/callback", result.getCallbackUrl());
32
+        assertEquals("orchestration.completed", result.getEventType());
33
+    }
34
+
35
+    @Test
36
+    @DisplayName("获取所有 Webhook")
37
+    void testListAll() {
38
+        webhookConfig.register("test-1", createRegistration("http://localhost:8080/cb1", "orchestration.completed"));
39
+        webhookConfig.register("test-2", createRegistration("http://localhost:8080/cb2", "orchestration.failed"));
40
+
41
+        List<WebhookRegistration> list = webhookConfig.listAll();
42
+        assertEquals(2, list.size());
43
+    }
44
+
45
+    @Test
46
+    @DisplayName("注销 Webhook")
47
+    void testUnregister() {
48
+        webhookConfig.register("test-1", createRegistration("http://localhost:8080/cb1", "orchestration.completed"));
49
+        webhookConfig.unregister("test-1");
50
+
51
+        assertNull(webhookConfig.getById("test-1"));
52
+    }
53
+
54
+    @Test
55
+    @DisplayName("按事件类型获取 Webhook")
56
+    void testGetByEventType() {
57
+        webhookConfig.register("test-1", createRegistration("http://localhost:8080/cb1", "orchestration.completed"));
58
+        webhookConfig.register("test-2", createRegistration("http://localhost:8080/cb2", "orchestration.failed"));
59
+        webhookConfig.register("test-3", createRegistration("http://localhost:8080/cb3", "orchestration.completed"));
60
+
61
+        List<WebhookRegistration> list = webhookConfig.getByEventType("orchestration.completed");
62
+        assertEquals(2, list.size());
63
+    }
64
+
65
+    @Test
66
+    @DisplayName("生成 HMAC-SHA256 签名")
67
+    void testGenerateSignature() {
68
+        String payload = "{\"test\":\"data\"}";
69
+        String secretKey = "my-secret-key";
70
+
71
+        String signature = webhookConfig.generateSignature(payload, secretKey);
72
+
73
+        assertNotNull(signature);
74
+        assertFalse(signature.isEmpty());
75
+        assertEquals(64, signature.length()); // HMAC-SHA256 produces 32 bytes = 64 hex chars
76
+    }
77
+
78
+    @Test
79
+    @DisplayName("验证签名 - 正确")
80
+    void testVerifySignatureValid() {
81
+        String payload = "{\"test\":\"data\"}";
82
+        String secretKey = "my-secret-key";
83
+
84
+        String signature = webhookConfig.generateSignature(payload, secretKey);
85
+        boolean valid = webhookConfig.verifySignature(payload, secretKey, signature);
86
+
87
+        assertTrue(valid);
88
+    }
89
+
90
+    @Test
91
+    @DisplayName("验证签名 - 错误")
92
+    void testVerifySignatureInvalid() {
93
+        String payload = "{\"test\":\"data\"}";
94
+        String secretKey = "my-secret-key";
95
+
96
+        boolean valid = webhookConfig.verifySignature(payload, secretKey, "invalid-signature");
97
+
98
+        assertFalse(valid);
99
+    }
100
+
101
+    @Test
102
+    @DisplayName("触发回调 - 无注册 Webhook")
103
+    void testTriggerCallbackNoHooks() {
104
+        Map<String, Object> payload = new HashMap<>();
105
+        payload.put("test", "data");
106
+
107
+        List<WebhookConfig.WebhookCallbackResult> results = webhookConfig.triggerCallback("nonexistent.event", payload);
108
+
109
+        assertNotNull(results);
110
+        assertTrue(results.isEmpty());
111
+    }
112
+
113
+    @Test
114
+    @DisplayName("触发回调 - Webhook URL 不可达")
115
+    void testTriggerCallbackUnreachable() {
116
+        WebhookRegistration reg = createRegistration("http://localhost:99999/unreachable", "orchestration.completed");
117
+        webhookConfig.register("test-1", reg);
118
+
119
+        Map<String, Object> payload = new HashMap<>();
120
+        payload.put("executionNo", "test-123");
121
+
122
+        List<WebhookConfig.WebhookCallbackResult> results = webhookConfig.triggerCallback("orchestration.completed", payload);
123
+
124
+        assertNotNull(results);
125
+        assertEquals(1, results.size());
126
+        assertFalse(results.get(0).isSuccess());
127
+    }
128
+
129
+    @Test
130
+    @DisplayName("签名一致性 - 相同输入产生相同签名")
131
+    void testSignatureConsistency() {
132
+        String payload = "{\"executionNo\":\"abc-123\",\"status\":\"completed\"}";
133
+        String secretKey = "water-system-secret-2024";
134
+
135
+        String sig1 = webhookConfig.generateSignature(payload, secretKey);
136
+        String sig2 = webhookConfig.generateSignature(payload, secretKey);
137
+
138
+        assertEquals(sig1, sig2);
139
+    }
140
+
141
+    @Test
142
+    @DisplayName("签名差异 - 不同输入产生不同签名")
143
+    void testSignatureDifference() {
144
+        String secretKey = "my-secret-key";
145
+
146
+        String sig1 = webhookConfig.generateSignature("payload1", secretKey);
147
+        String sig2 = webhookConfig.generateSignature("payload2", secretKey);
148
+
149
+        assertNotEquals(sig1, sig2);
150
+    }
151
+
152
+    private WebhookRegistration createRegistration(String url, String eventType) {
153
+        WebhookRegistration reg = new WebhookRegistration();
154
+        reg.setCallbackUrl(url);
155
+        reg.setSecretKey("test-secret");
156
+        reg.setSignatureEnabled(true);
157
+        reg.setEventType(eventType);
158
+        reg.setDescription("Test webhook");
159
+        return reg;
160
+    }
161
+}

+ 128
- 0
wm-bpm/src/test/java/com/water/bpm/service/OrchestrationLogServiceTest.java Прегледај датотеку

@@ -0,0 +1,128 @@
1
+package com.water.bpm.service;
2
+
3
+import com.water.bpm.entity.OrchestrationLog;
4
+import com.water.bpm.mapper.OrchestrationLogMapper;
5
+import org.junit.jupiter.api.BeforeEach;
6
+import org.junit.jupiter.api.DisplayName;
7
+import org.junit.jupiter.api.Test;
8
+import org.junit.jupiter.api.extension.ExtendWith;
9
+import org.mockito.InjectMocks;
10
+import org.mockito.Mock;
11
+import org.mockito.junit.jupiter.MockitoExtension;
12
+
13
+import java.util.Arrays;
14
+import java.util.List;
15
+
16
+import static org.junit.jupiter.api.Assertions.*;
17
+import static org.mockito.ArgumentMatchers.any;
18
+import static org.mockito.Mockito.*;
19
+
20
+@ExtendWith(MockitoExtension.class)
21
+@DisplayName("编排日志服务测试")
22
+class OrchestrationLogServiceTest {
23
+
24
+    @Mock
25
+    private OrchestrationLogMapper orchestrationLogMapper;
26
+
27
+    @InjectMocks
28
+    private OrchestrationLogService logService;
29
+
30
+    @BeforeEach
31
+    void setUp() {
32
+    }
33
+
34
+    @Test
35
+    @DisplayName("记录日志")
36
+    void testRecordLog() {
37
+        when(orchestrationLogMapper.insert(any(OrchestrationLog.class))).thenReturn(1);
38
+
39
+        OrchestrationLog result = logService.recordLog(
40
+                1L, "exec-123", 0, "查询IoT设备",
41
+                "start", "iot", "{\"query\":\"all\"}",
42
+                "{\"count\":156}", 200, 150L,
43
+                true, null, "tester");
44
+
45
+        assertNotNull(result);
46
+        assertEquals(1L, result.getExecutionId());
47
+        assertEquals("exec-123", result.getExecutionNo());
48
+        assertEquals("start", result.getAction());
49
+        assertTrue(result.getSuccess());
50
+        verify(orchestrationLogMapper, times(1)).insert(any(OrchestrationLog.class));
51
+    }
52
+
53
+    @Test
54
+    @DisplayName("记录失败日志")
55
+    void testRecordLogFailure() {
56
+        when(orchestrationLogMapper.insert(any(OrchestrationLog.class))).thenReturn(1);
57
+
58
+        OrchestrationLog result = logService.recordLog(
59
+                1L, "exec-123", 1, "同步巡检任务",
60
+                "fail", "patrol", null,
61
+                null, 500, 3000L,
62
+                false, "连接超时", "system");
63
+
64
+        assertNotNull(result);
65
+        assertFalse(result.getSuccess());
66
+        assertEquals("连接超时", result.getErrorMessage());
67
+    }
68
+
69
+    @Test
70
+    @DisplayName("按执行ID查询日志")
71
+    void testGetLogsByExecutionId() {
72
+        OrchestrationLog log1 = createLog(1L, 0, "start");
73
+        OrchestrationLog log2 = createLog(1L, 1, "complete");
74
+        when(orchestrationLogMapper.selectByExecutionId(1L)).thenReturn(Arrays.asList(log1, log2));
75
+
76
+        List<OrchestrationLog> logs = logService.getLogsByExecutionId(1L);
77
+
78
+        assertNotNull(logs);
79
+        assertEquals(2, logs.size());
80
+    }
81
+
82
+    @Test
83
+    @DisplayName("按执行编号查询日志")
84
+    void testGetLogsByExecutionNo() {
85
+        OrchestrationLog log1 = createLog(1L, 0, "start");
86
+        when(orchestrationLogMapper.selectByExecutionNo("exec-123")).thenReturn(List.of(log1));
87
+
88
+        List<OrchestrationLog> logs = logService.getLogsByExecutionNo("exec-123");
89
+
90
+        assertNotNull(logs);
91
+        assertEquals(1, logs.size());
92
+    }
93
+
94
+    @Test
95
+    @DisplayName("按步骤查询日志")
96
+    void testGetLogsByStep() {
97
+        OrchestrationLog log1 = createLog(1L, 0, "start");
98
+        OrchestrationLog log2 = createLog(1L, 0, "complete");
99
+        when(orchestrationLogMapper.selectByStep(1L, 0)).thenReturn(Arrays.asList(log1, log2));
100
+
101
+        List<OrchestrationLog> logs = logService.getLogsByStep(1L, 0);
102
+
103
+        assertNotNull(logs);
104
+        assertEquals(2, logs.size());
105
+    }
106
+
107
+    @Test
108
+    @DisplayName("统计日志数量")
109
+    void testCountLogs() {
110
+        when(orchestrationLogMapper.selectCount(any())).thenReturn(15L);
111
+
112
+        long count = logService.countLogs(1L);
113
+
114
+        assertEquals(15L, count);
115
+    }
116
+
117
+    private OrchestrationLog createLog(Long executionId, Integer stepIndex, String action) {
118
+        OrchestrationLog log = new OrchestrationLog();
119
+        log.setId(System.currentTimeMillis());
120
+        log.setExecutionId(executionId);
121
+        log.setExecutionNo("exec-123");
122
+        log.setStepIndex(stepIndex);
123
+        log.setStepName("Step " + stepIndex);
124
+        log.setAction(action);
125
+        log.setSuccess(true);
126
+        return log;
127
+    }
128
+}

+ 239
- 0
wm-bpm/src/test/java/com/water/bpm/service/ProcessOrchestrationServiceTest.java Прегледај датотеку

@@ -0,0 +1,239 @@
1
+package com.water.bpm.service;
2
+
3
+import com.fasterxml.jackson.core.JsonProcessingException;
4
+import com.fasterxml.jackson.core.type.TypeReference;
5
+import com.fasterxml.jackson.databind.ObjectMapper;
6
+import com.water.bpm.config.WebhookConfig;
7
+import com.water.bpm.entity.BpmOrchestration;
8
+import com.water.bpm.entity.OrchestrationExecution;
9
+import com.water.bpm.entity.dto.OrchestrationStep;
10
+import com.water.bpm.mapper.BpmOrchestrationMapper;
11
+import com.water.bpm.mapper.OrchestrationExecutionMapper;
12
+import org.junit.jupiter.api.BeforeEach;
13
+import org.junit.jupiter.api.DisplayName;
14
+import org.junit.jupiter.api.Test;
15
+import org.junit.jupiter.api.extension.ExtendWith;
16
+import org.mockito.InjectMocks;
17
+import org.mockito.Mock;
18
+import org.mockito.junit.jupiter.MockitoExtension;
19
+
20
+import java.util.*;
21
+
22
+import static org.junit.jupiter.api.Assertions.*;
23
+import static org.mockito.ArgumentMatchers.any;
24
+import static org.mockito.Mockito.*;
25
+
26
+@ExtendWith(MockitoExtension.class)
27
+@DisplayName("流程编排服务测试")
28
+class ProcessOrchestrationServiceTest {
29
+
30
+    @Mock
31
+    private BpmOrchestrationMapper orchestrationMapper;
32
+
33
+    @Mock
34
+    private OrchestrationExecutionMapper executionMapper;
35
+
36
+    @Mock
37
+    private OrchestrationLogService logService;
38
+
39
+    @Mock
40
+    private WebhookConfig webhookConfig;
41
+
42
+    @InjectMocks
43
+    private ProcessOrchestrationService orchestrationService;
44
+
45
+    private BpmOrchestration testOrchestration;
46
+    private ObjectMapper objectMapper;
47
+
48
+    @BeforeEach
49
+    void setUp() {
50
+        objectMapper = new ObjectMapper();
51
+        testOrchestration = new BpmOrchestration();
52
+        testOrchestration.setId(1L);
53
+        testOrchestration.setOrchestrationName("测试编排");
54
+        testOrchestration.setOrchestrationCode("TEST_ORCH");
55
+        testOrchestration.setStatus(1); // 启用
56
+        testOrchestration.setExecutionCount(0);
57
+
58
+        List<OrchestrationStep> steps = new ArrayList<>();
59
+        OrchestrationStep step1 = new OrchestrationStep();
60
+        step1.setStepName("查询IoT设备");
61
+        step1.setTargetSystem("iot");
62
+        step1.setApiPath("/api/iot/devices");
63
+        step1.setMethod("GET");
64
+        step1.setTimeoutMs(5000);
65
+        step1.setSkippable(false);
66
+        step1.setMaxRetries(3);
67
+        steps.add(step1);
68
+
69
+        OrchestrationStep step2 = new OrchestrationStep();
70
+        step2.setStepName("同步巡检任务");
71
+        step2.setTargetSystem("patrol");
72
+        step2.setApiPath("/api/patrol/tasks");
73
+        step2.setMethod("POST");
74
+        step2.setTimeoutMs(10000);
75
+        step2.setSkippable(false);
76
+        step2.setMaxRetries(3);
77
+        steps.add(step2);
78
+
79
+        try {
80
+            testOrchestration.setOrchestrationRules(objectMapper.writeValueAsString(steps));
81
+        } catch (JsonProcessingException e) {
82
+            fail("Failed to serialize steps");
83
+        }
84
+    }
85
+
86
+    @Test
87
+    @DisplayName("创建编排定义")
88
+    void testCreateOrchestration() {
89
+        when(orchestrationMapper.insert(any(BpmOrchestration.class))).thenReturn(1);
90
+
91
+        BpmOrchestration result = orchestrationService.createOrchestration(testOrchestration);
92
+
93
+        assertNotNull(result);
94
+        assertEquals(0, result.getStatus()); // 应为草稿状态
95
+        assertEquals(0, result.getExecutionCount());
96
+        verify(orchestrationMapper, times(1)).insert(any(BpmOrchestration.class));
97
+    }
98
+
99
+    @Test
100
+    @DisplayName("解析编排步骤 - 正常JSON")
101
+    void testParseStepsValid() {
102
+        List<OrchestrationStep> steps = orchestrationService.parseSteps(testOrchestration.getOrchestrationRules());
103
+
104
+        assertNotNull(steps);
105
+        assertEquals(2, steps.size());
106
+        assertEquals("查询IoT设备", steps.get(0).getStepName());
107
+        assertEquals("iot", steps.get(0).getTargetSystem());
108
+        assertEquals("同步巡检任务", steps.get(1).getStepName());
109
+    }
110
+
111
+    @Test
112
+    @DisplayName("解析编排步骤 - 空字符串")
113
+    void testParseStepsEmpty() {
114
+        List<OrchestrationStep> steps = orchestrationService.parseSteps("");
115
+        assertNotNull(steps);
116
+        assertTrue(steps.isEmpty());
117
+    }
118
+
119
+    @Test
120
+    @DisplayName("解析编排步骤 - null")
121
+    void testParseStepsNull() {
122
+        List<OrchestrationStep> steps = orchestrationService.parseSteps(null);
123
+        assertNotNull(steps);
124
+        assertTrue(steps.isEmpty());
125
+    }
126
+
127
+    @Test
128
+    @DisplayName("解析编排步骤 - 无效JSON")
129
+    void testParseStepsInvalidJson() {
130
+        List<OrchestrationStep> steps = orchestrationService.parseSteps("invalid json");
131
+        assertNotNull(steps);
132
+        assertTrue(steps.isEmpty());
133
+    }
134
+
135
+    @Test
136
+    @DisplayName("启动编排执行 - 编排不存在")
137
+    void testStartExecutionOrchestrationNotFound() {
138
+        when(orchestrationMapper.selectById(999L)).thenReturn(null);
139
+
140
+        RuntimeException ex = assertThrows(RuntimeException.class, () ->
141
+                orchestrationService.startExecution(999L, "tester", "manual", new HashMap<>()));
142
+        assertTrue(ex.getMessage().contains("编排定义不存在"));
143
+    }
144
+
145
+    @Test
146
+    @DisplayName("启动编排执行 - 编排未启用")
147
+    void testStartExecutionOrchestrationDisabled() {
148
+        testOrchestration.setStatus(0); // 草稿
149
+        when(orchestrationMapper.selectById(1L)).thenReturn(testOrchestration);
150
+
151
+        RuntimeException ex = assertThrows(RuntimeException.class, () ->
152
+                orchestrationService.startExecution(1L, "tester", "manual", new HashMap<>()));
153
+        assertTrue(ex.getMessage().contains("编排未启用"));
154
+    }
155
+
156
+    @Test
157
+    @DisplayName("启动编排执行 - 正常执行")
158
+    void testStartExecutionSuccess() {
159
+        when(orchestrationMapper.selectById(1L)).thenReturn(testOrchestration);
160
+        when(executionMapper.insert(any(OrchestrationExecution.class))).thenAnswer(invocation -> {
161
+            OrchestrationExecution exec = invocation.getArgument(0);
162
+            exec.setId(100L);
163
+            return 1;
164
+        });
165
+        when(executionMapper.updateById(any(OrchestrationExecution.class))).thenReturn(1);
166
+        when(executionMapper.selectById(anyLong())).thenReturn(createMockExecution());
167
+        when(orchestrationMapper.updateById(any(BpmOrchestration.class))).thenReturn(1);
168
+        when(logService.recordLog(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()))
169
+                .thenReturn(null);
170
+        when(webhookConfig.triggerCallback(any(), any())).thenReturn(Collections.emptyList());
171
+
172
+        Map<String, Object> params = new HashMap<>();
173
+        params.put("testKey", "testValue");
174
+
175
+        OrchestrationExecution result = orchestrationService.startExecution(1L, "tester", "manual", params);
176
+
177
+        assertNotNull(result);
178
+        verify(executionMapper, atLeastOnce()).insert(any(OrchestrationExecution.class));
179
+        verify(orchestrationMapper, atLeastOnce()).updateById(any(BpmOrchestration.class));
180
+        verify(logService, atLeastOnce()).recordLog(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any());
181
+    }
182
+
183
+    @Test
184
+    @DisplayName("取消执行 - 成功")
185
+    void testCancelExecutionSuccess() {
186
+        OrchestrationExecution execution = createMockExecution();
187
+        execution.setStatus("running");
188
+        when(executionMapper.selectById(100L)).thenReturn(execution);
189
+        when(executionMapper.updateById(any(OrchestrationExecution.class))).thenReturn(1);
190
+        when(logService.recordLog(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()))
191
+                .thenReturn(null);
192
+
193
+        OrchestrationExecution result = orchestrationService.cancelExecution(100L);
194
+
195
+        assertNotNull(result);
196
+        assertEquals("cancelled", result.getStatus());
197
+        verify(executionMapper, times(1)).updateById(any(OrchestrationExecution.class));
198
+    }
199
+
200
+    @Test
201
+    @DisplayName("取消执行 - 已完成")
202
+    void testCancelExecutionAlreadyCompleted() {
203
+        OrchestrationExecution execution = createMockExecution();
204
+        execution.setStatus("completed");
205
+        when(executionMapper.selectById(100L)).thenReturn(execution);
206
+
207
+        RuntimeException ex = assertThrows(RuntimeException.class, () ->
208
+                orchestrationService.cancelExecution(100L));
209
+        assertTrue(ex.getMessage().contains("已完成或失败"));
210
+    }
211
+
212
+    @Test
213
+    @DisplayName("获取执行统计")
214
+    void testGetExecutionStats() {
215
+        when(executionMapper.selectCount(any())).thenReturn(10L, 7L, 2L, 1L);
216
+
217
+        Map<String, Object> stats = orchestrationService.getExecutionStats(1L);
218
+
219
+        assertNotNull(stats);
220
+        assertEquals(10L, stats.get("total"));
221
+        assertEquals(7L, stats.get("completed"));
222
+        assertEquals(2L, stats.get("failed"));
223
+        assertEquals(1L, stats.get("running"));
224
+        assertEquals("70.0%", stats.get("successRate"));
225
+    }
226
+
227
+    private OrchestrationExecution createMockExecution() {
228
+        OrchestrationExecution execution = new OrchestrationExecution();
229
+        execution.setId(100L);
230
+        execution.setOrchestrationId(1L);
231
+        execution.setExecutionNo(UUID.randomUUID().toString());
232
+        execution.setCurrentStepIndex(0);
233
+        execution.setCurrentStepName("查询IoT设备");
234
+        execution.setStatus("running");
235
+        execution.setTriggeredBy("tester");
236
+        execution.setRetryCount(0);
237
+        return execution;
238
+    }
239
+}