whistle.mock-plugins 1.1.0 → 1.1.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/cgi-bin/interfaces.js +3 -0
- package/app/public/js/index.js +1 -1
- package/lib/ruleManager.js +168 -58
- package/lib/server.js +120 -0
- package/package.json +1 -1
- package/ui/src/pages/InterfaceManagement.js +151 -3
- package/ui/src/pages/MockData.js +47 -5
- package/ui/src/styles/mock-data.css +37 -1
package/lib/ruleManager.js
CHANGED
|
@@ -3,6 +3,113 @@ const path = require('path');
|
|
|
3
3
|
const url = require('url');
|
|
4
4
|
const Mock = require('mockjs');
|
|
5
5
|
|
|
6
|
+
// 获取嵌套对象属性值的工具函数
|
|
7
|
+
const getNestedValue = (obj, path) => {
|
|
8
|
+
return path.split('.').reduce((current, key) => {
|
|
9
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
10
|
+
}, obj);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// 参数匹配功能
|
|
14
|
+
const isParamMatch = (req, paramMatchers, log) => {
|
|
15
|
+
if (!paramMatchers || !Array.isArray(paramMatchers) || paramMatchers.length === 0) {
|
|
16
|
+
return true; // 如果没有参数匹配规则,视为匹配成功
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// 合并所有可能的参数来源
|
|
21
|
+
let requestParams = {};
|
|
22
|
+
|
|
23
|
+
// URL查询参数
|
|
24
|
+
if (req.query) {
|
|
25
|
+
requestParams = { ...requestParams, ...req.query };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// POST请求体参数
|
|
29
|
+
if (req.body && typeof req.body === 'object') {
|
|
30
|
+
requestParams = { ...requestParams, ...req.body };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// URL路径参数(如果有的话)
|
|
34
|
+
if (req.params) {
|
|
35
|
+
requestParams = { ...requestParams, ...req.params };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (log) {
|
|
39
|
+
log(`参数匹配检查 - 请求参数: ${JSON.stringify(requestParams)}`);
|
|
40
|
+
log(`参数匹配检查 - 匹配规则: ${JSON.stringify(paramMatchers)}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 检查每个匹配规则
|
|
44
|
+
for (const matcher of paramMatchers) {
|
|
45
|
+
const { paramPath, paramValue, matchType = 'exact' } = matcher;
|
|
46
|
+
|
|
47
|
+
if (!paramPath) {
|
|
48
|
+
continue; // 跳过无效的匹配规则
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 获取实际参数值
|
|
52
|
+
const actualValue = getNestedValue(requestParams, paramPath);
|
|
53
|
+
|
|
54
|
+
if (log) {
|
|
55
|
+
log(`参数匹配检查 - 路径: ${paramPath}, 期望值: ${paramValue}, 实际值: ${actualValue}, 匹配类型: ${matchType}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 如果参数不存在,视为不匹配
|
|
59
|
+
if (actualValue === undefined || actualValue === null) {
|
|
60
|
+
if (log) {
|
|
61
|
+
log(`参数匹配失败 - 参数 ${paramPath} 不存在`);
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 根据匹配类型进行比较
|
|
67
|
+
let isMatch = false;
|
|
68
|
+
const actualStr = String(actualValue);
|
|
69
|
+
const expectedStr = String(paramValue);
|
|
70
|
+
|
|
71
|
+
switch (matchType) {
|
|
72
|
+
case 'exact':
|
|
73
|
+
isMatch = actualStr === expectedStr;
|
|
74
|
+
break;
|
|
75
|
+
case 'contains':
|
|
76
|
+
isMatch = actualStr.includes(expectedStr);
|
|
77
|
+
break;
|
|
78
|
+
case 'regex':
|
|
79
|
+
try {
|
|
80
|
+
const regex = new RegExp(expectedStr);
|
|
81
|
+
isMatch = regex.test(actualStr);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (log) {
|
|
84
|
+
log(`参数匹配失败 - 正则表达式无效: ${expectedStr}`);
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
isMatch = actualStr === expectedStr;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!isMatch) {
|
|
94
|
+
if (log) {
|
|
95
|
+
log(`参数匹配失败 - ${paramPath}: ${actualStr} 不匹配 ${expectedStr} (${matchType})`);
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (log) {
|
|
102
|
+
log('参数匹配成功 - 所有规则都匹配');
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (log) {
|
|
107
|
+
log(`参数匹配检查出错: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
6
113
|
/**
|
|
7
114
|
* 规则管理模块,负责处理 Whistle 规则和代理请求
|
|
8
115
|
*/
|
|
@@ -60,7 +167,7 @@ const ruleManager = {
|
|
|
60
167
|
const requestPath = parsedUrl.pathname;
|
|
61
168
|
|
|
62
169
|
// 查找匹配的接口
|
|
63
|
-
const matchedInterface = this.findMatchingInterface(enabledInterfaces, requestPath, req.method);
|
|
170
|
+
const matchedInterface = this.findMatchingInterface(enabledInterfaces, requestPath, req.method, req);
|
|
64
171
|
|
|
65
172
|
if (!matchedInterface) {
|
|
66
173
|
this.log(`[规则处理器] 未找到匹配的接口,请求将透传: ${req.method} ${requestPath}`);
|
|
@@ -93,9 +200,10 @@ const ruleManager = {
|
|
|
93
200
|
* @param {Array} interfaces 接口列表
|
|
94
201
|
* @param {string} requestPath 请求路径
|
|
95
202
|
* @param {string} method 请求方法
|
|
203
|
+
* @param {object} req 完整的请求对象,用于参数匹配
|
|
96
204
|
* @returns {object|null} 匹配的接口对象,如果没有匹配则返回 null
|
|
97
205
|
*/
|
|
98
|
-
findMatchingInterface(interfaces, requestPath, method) {
|
|
206
|
+
findMatchingInterface(interfaces, requestPath, method, req) {
|
|
99
207
|
// 简化日志,只记录正在进行匹配的操作
|
|
100
208
|
this.log(`[规则处理器] 尝试匹配接口,路径: ${requestPath}, 方法: ${method}`);
|
|
101
209
|
|
|
@@ -111,17 +219,16 @@ const ruleManager = {
|
|
|
111
219
|
return null;
|
|
112
220
|
}
|
|
113
221
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
222
|
+
// 收集所有URL匹配的接口
|
|
223
|
+
const urlMatchedInterfaces = [];
|
|
224
|
+
|
|
225
|
+
// 先收集所有URL匹配的接口(包括精确匹配和正则匹配)
|
|
226
|
+
for (const intf of interfaces) {
|
|
117
227
|
if (!intf || !intf.urlPattern) {
|
|
118
|
-
|
|
228
|
+
continue;
|
|
119
229
|
}
|
|
120
230
|
|
|
121
|
-
// 检查
|
|
122
|
-
const urlMatches = intf.urlPattern === requestPath;
|
|
123
|
-
|
|
124
|
-
// 修复: 兼容不同的方法名称和值
|
|
231
|
+
// 检查HTTP方法是否匹配
|
|
125
232
|
const methodField = intf.httpMethod || intf.method;
|
|
126
233
|
const methodValue = methodField && methodField.toUpperCase();
|
|
127
234
|
const methodMatches = !methodValue ||
|
|
@@ -129,32 +236,24 @@ const ruleManager = {
|
|
|
129
236
|
methodValue === 'ANY' ||
|
|
130
237
|
methodValue === method;
|
|
131
238
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// 使用正则表达式缓存优化性能
|
|
145
|
-
if (!this.regexCache) {
|
|
146
|
-
this.regexCache = new Map();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
matchedInterface = interfaces.find(intf => {
|
|
239
|
+
if (!methodMatches) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 检查URL是否匹配
|
|
244
|
+
let urlMatches = false;
|
|
245
|
+
|
|
246
|
+
// 精确匹配
|
|
247
|
+
if (intf.urlPattern === requestPath) {
|
|
248
|
+
urlMatches = true;
|
|
249
|
+
} else {
|
|
250
|
+
// 正则表达式匹配
|
|
151
251
|
try {
|
|
152
|
-
//
|
|
153
|
-
if (!
|
|
154
|
-
|
|
252
|
+
// 从缓存获取或创建正则表达式
|
|
253
|
+
if (!this.regexCache) {
|
|
254
|
+
this.regexCache = new Map();
|
|
155
255
|
}
|
|
156
256
|
|
|
157
|
-
// 从缓存获取或创建正则表达式
|
|
158
257
|
let regex;
|
|
159
258
|
if (this.regexCache.has(intf.urlPattern)) {
|
|
160
259
|
regex = this.regexCache.get(intf.urlPattern);
|
|
@@ -166,37 +265,48 @@ const ruleManager = {
|
|
|
166
265
|
this.regexCache.set(intf.urlPattern, regex);
|
|
167
266
|
}
|
|
168
267
|
|
|
169
|
-
|
|
170
|
-
const methodField = intf.httpMethod || intf.method;
|
|
171
|
-
const methodValue = methodField && methodField.toUpperCase();
|
|
172
|
-
|
|
173
|
-
// 检查URL和方法是否匹配
|
|
174
|
-
const urlMatches = regex.test(requestPath);
|
|
175
|
-
const methodMatches = !methodValue ||
|
|
176
|
-
methodValue === 'ALL' ||
|
|
177
|
-
methodValue === 'ANY' ||
|
|
178
|
-
methodValue === method;
|
|
179
|
-
|
|
180
|
-
return urlMatches && methodMatches;
|
|
268
|
+
urlMatches = regex.test(requestPath);
|
|
181
269
|
} catch (err) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return false;
|
|
270
|
+
this.log(`[规则处理器] 正则匹配接口 ${intf.name} 时出错: ${err.message}`);
|
|
271
|
+
continue;
|
|
185
272
|
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (urlMatches) {
|
|
276
|
+
urlMatchedInterfaces.push(intf);
|
|
277
|
+
}
|
|
191
278
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
279
|
+
|
|
280
|
+
this.log(`[规则处理器] 找到 ${urlMatchedInterfaces.length} 个URL匹配的接口`);
|
|
281
|
+
|
|
282
|
+
if (urlMatchedInterfaces.length === 0) {
|
|
196
283
|
this.log(`[规则处理器] 未找到匹配的接口`);
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 在URL匹配的接口中,查找参数也匹配的接口
|
|
288
|
+
for (const intf of urlMatchedInterfaces) {
|
|
289
|
+
// 检查参数匹配
|
|
290
|
+
if (isParamMatch(req, intf.paramMatchers, this.log)) {
|
|
291
|
+
this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL + 参数匹配)`);
|
|
292
|
+
return intf;
|
|
293
|
+
} else {
|
|
294
|
+
this.log(`[规则处理器] 接口 ${intf.name} URL匹配但参数不匹配,继续查找...`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 如果没有参数匹配的接口,返回第一个没有参数匹配规则的接口
|
|
299
|
+
const fallbackInterface = urlMatchedInterfaces.find(intf =>
|
|
300
|
+
!intf.paramMatchers || !Array.isArray(intf.paramMatchers) || intf.paramMatchers.length === 0
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (fallbackInterface) {
|
|
304
|
+
this.log(`[规则处理器] 使用回退接口: ${fallbackInterface.name} (无参数匹配规则)`);
|
|
305
|
+
return fallbackInterface;
|
|
197
306
|
}
|
|
198
307
|
|
|
199
|
-
|
|
308
|
+
this.log(`[规则处理器] 所有URL匹配的接口都有参数匹配规则且都不满足,未找到合适的接口`);
|
|
309
|
+
return null;
|
|
200
310
|
},
|
|
201
311
|
|
|
202
312
|
/**
|
package/lib/server.js
CHANGED
|
@@ -170,6 +170,99 @@ if (!fs.existsSync(INTERFACES_FILE)) {
|
|
|
170
170
|
const RULE_VALUE_HEADER = 'x-whistle-rule-value';
|
|
171
171
|
const MOCK_PREFIX = 'mock://';
|
|
172
172
|
|
|
173
|
+
// 获取嵌套对象属性值的工具函数
|
|
174
|
+
const getNestedValue = (obj, path) => {
|
|
175
|
+
return path.split('.').reduce((current, key) => {
|
|
176
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
177
|
+
}, obj);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// 参数匹配功能
|
|
181
|
+
const isParamMatch = (req, paramMatchers) => {
|
|
182
|
+
if (!paramMatchers || !Array.isArray(paramMatchers) || paramMatchers.length === 0) {
|
|
183
|
+
return true; // 如果没有参数匹配规则,视为匹配成功
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// 合并所有可能的参数来源
|
|
188
|
+
let requestParams = {};
|
|
189
|
+
|
|
190
|
+
// URL查询参数
|
|
191
|
+
if (req.query) {
|
|
192
|
+
requestParams = { ...requestParams, ...req.query };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// POST请求体参数
|
|
196
|
+
if (req.body && typeof req.body === 'object') {
|
|
197
|
+
requestParams = { ...requestParams, ...req.body };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// URL路径参数(如果有的话)
|
|
201
|
+
if (req.params) {
|
|
202
|
+
requestParams = { ...requestParams, ...req.params };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
logMessage(`参数匹配检查 - 请求参数: ${JSON.stringify(requestParams)}`);
|
|
206
|
+
logMessage(`参数匹配检查 - 匹配规则: ${JSON.stringify(paramMatchers)}`);
|
|
207
|
+
|
|
208
|
+
// 检查每个匹配规则
|
|
209
|
+
for (const matcher of paramMatchers) {
|
|
210
|
+
const { paramPath, paramValue, matchType = 'exact' } = matcher;
|
|
211
|
+
|
|
212
|
+
if (!paramPath) {
|
|
213
|
+
continue; // 跳过无效的匹配规则
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 获取实际参数值
|
|
217
|
+
const actualValue = getNestedValue(requestParams, paramPath);
|
|
218
|
+
|
|
219
|
+
logMessage(`参数匹配检查 - 路径: ${paramPath}, 期望值: ${paramValue}, 实际值: ${actualValue}, 匹配类型: ${matchType}`);
|
|
220
|
+
|
|
221
|
+
// 如果参数不存在,视为不匹配
|
|
222
|
+
if (actualValue === undefined || actualValue === null) {
|
|
223
|
+
logMessage(`参数匹配失败 - 参数 ${paramPath} 不存在`);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 根据匹配类型进行比较
|
|
228
|
+
let isMatch = false;
|
|
229
|
+
const actualStr = String(actualValue);
|
|
230
|
+
const expectedStr = String(paramValue);
|
|
231
|
+
|
|
232
|
+
switch (matchType) {
|
|
233
|
+
case 'exact':
|
|
234
|
+
isMatch = actualStr === expectedStr;
|
|
235
|
+
break;
|
|
236
|
+
case 'contains':
|
|
237
|
+
isMatch = actualStr.includes(expectedStr);
|
|
238
|
+
break;
|
|
239
|
+
case 'regex':
|
|
240
|
+
try {
|
|
241
|
+
const regex = new RegExp(expectedStr);
|
|
242
|
+
isMatch = regex.test(actualStr);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
logMessage(`参数匹配失败 - 正则表达式无效: ${expectedStr}`);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
default:
|
|
249
|
+
isMatch = actualStr === expectedStr;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!isMatch) {
|
|
253
|
+
logMessage(`参数匹配失败 - ${paramPath}: ${actualStr} 不匹配 ${expectedStr} (${matchType})`);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
logMessage('参数匹配成功 - 所有规则都匹配');
|
|
259
|
+
return true;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
logMessage(`参数匹配检查出错: ${error.message}`);
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
173
266
|
// 创建 Express 应用
|
|
174
267
|
const app = express();
|
|
175
268
|
|
|
@@ -577,6 +670,33 @@ const handleResponse = (interfaceItem, req, res) => {
|
|
|
577
670
|
logMessage(`匹配到接口: ${interfaceItem.name}, ID: ${interfaceItem.id}`);
|
|
578
671
|
logMessage(`URL匹配规则: ${interfaceItem.urlPattern}`);
|
|
579
672
|
|
|
673
|
+
// 检查参数匹配规则
|
|
674
|
+
if (!isParamMatch(req, interfaceItem.paramMatchers)) {
|
|
675
|
+
logMessage(`参数匹配失败,跳过接口: ${interfaceItem.name}`);
|
|
676
|
+
|
|
677
|
+
// 记录参数匹配失败的日志
|
|
678
|
+
addToJsonLog({
|
|
679
|
+
eventType: 'param_mismatch',
|
|
680
|
+
url: req.url,
|
|
681
|
+
method: req.method,
|
|
682
|
+
message: `参数匹配失败: ${interfaceItem.name}`,
|
|
683
|
+
pattern: interfaceItem.urlPattern,
|
|
684
|
+
status: 'skipped',
|
|
685
|
+
paramMatchers: interfaceItem.paramMatchers || []
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// 返回404,表示没有匹配的接口
|
|
689
|
+
return res.status(404).json({
|
|
690
|
+
code: 404,
|
|
691
|
+
message: '参数匹配失败,未找到符合条件的接口',
|
|
692
|
+
data: {
|
|
693
|
+
interfaceName: interfaceItem.name,
|
|
694
|
+
urlPattern: interfaceItem.urlPattern,
|
|
695
|
+
paramMatchers: interfaceItem.paramMatchers || []
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
580
700
|
// 记录请求详情
|
|
581
701
|
logMessage(`请求方法: ${req.method}`);
|
|
582
702
|
logMessage(`原始URL: ${req.url}`);
|
package/package.json
CHANGED
|
@@ -166,7 +166,8 @@ const InterfaceManagement = () => {
|
|
|
166
166
|
responses: defaultResponses,
|
|
167
167
|
activeResponseId: defaultResponseId,
|
|
168
168
|
httpMethod: 'ALL',
|
|
169
|
-
headerItems: [] // 初始化为空数组
|
|
169
|
+
headerItems: [], // 初始化为空数组
|
|
170
|
+
paramMatchers: [] // 初始化为空数组
|
|
170
171
|
});
|
|
171
172
|
|
|
172
173
|
setCurrentResponseId(defaultResponseId);
|
|
@@ -187,6 +188,16 @@ const InterfaceManagement = () => {
|
|
|
187
188
|
}));
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// 处理参数匹配规则,转换为数组格式
|
|
192
|
+
let paramMatchersArray = [];
|
|
193
|
+
if (record.paramMatchers && Array.isArray(record.paramMatchers)) {
|
|
194
|
+
paramMatchersArray = record.paramMatchers.map(matcher => ({
|
|
195
|
+
paramPath: matcher.paramPath || '',
|
|
196
|
+
paramValue: matcher.paramValue || '',
|
|
197
|
+
matchType: matcher.matchType || 'exact'
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
|
|
190
201
|
// 处理多响应数据
|
|
191
202
|
let initialResponses = [];
|
|
192
203
|
let initialResponseId = null;
|
|
@@ -233,7 +244,8 @@ const InterfaceManagement = () => {
|
|
|
233
244
|
activeResponseId: initialResponseId,
|
|
234
245
|
httpMethod: record.httpMethod || 'ALL',
|
|
235
246
|
targetUrl: record.targetUrl || '',
|
|
236
|
-
headerItems: headersArray // 使用数组存储表单项
|
|
247
|
+
headerItems: headersArray, // 使用数组存储表单项
|
|
248
|
+
paramMatchers: paramMatchersArray
|
|
237
249
|
});
|
|
238
250
|
setModalVisible(true);
|
|
239
251
|
};
|
|
@@ -373,6 +385,22 @@ const InterfaceManagement = () => {
|
|
|
373
385
|
}
|
|
374
386
|
}
|
|
375
387
|
|
|
388
|
+
// 处理参数匹配规则,清理无效的规则
|
|
389
|
+
let paramMatchers = [];
|
|
390
|
+
if (values.proxyType === 'response' && values.paramMatchers && values.paramMatchers.length > 0) {
|
|
391
|
+
paramMatchers = values.paramMatchers
|
|
392
|
+
.filter(item => item && item.paramPath && item.paramPath.trim() && item.paramValue !== undefined)
|
|
393
|
+
.map(item => ({
|
|
394
|
+
paramPath: item.paramPath.trim(),
|
|
395
|
+
paramValue: item.paramValue,
|
|
396
|
+
matchType: item.matchType || 'exact'
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
console.log('=== 参数匹配规则处理 ===');
|
|
401
|
+
console.log('原始参数匹配规则:', values.paramMatchers);
|
|
402
|
+
console.log('清理后的参数匹配规则:', paramMatchers);
|
|
403
|
+
|
|
376
404
|
const interfaceData = {
|
|
377
405
|
name: values.name,
|
|
378
406
|
featureId: selectedFeatureId,
|
|
@@ -385,6 +413,7 @@ const InterfaceManagement = () => {
|
|
|
385
413
|
responseContent: activeResponse ? activeResponse.content : '',
|
|
386
414
|
targetUrl: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? values.targetUrl : '',
|
|
387
415
|
customHeaders: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? customHeaders : {},
|
|
416
|
+
paramMatchers: paramMatchers, // 添加参数匹配规则
|
|
388
417
|
httpStatus: parseInt(values.statusCode, 10), // 转换为数字
|
|
389
418
|
contentType: values.contentType,
|
|
390
419
|
responseDelay: 0,
|
|
@@ -584,6 +613,43 @@ const InterfaceManagement = () => {
|
|
|
584
613
|
);
|
|
585
614
|
}
|
|
586
615
|
},
|
|
616
|
+
{
|
|
617
|
+
title: '参数匹配',
|
|
618
|
+
dataIndex: 'paramMatchers',
|
|
619
|
+
key: 'paramMatchers',
|
|
620
|
+
width: 100,
|
|
621
|
+
render: (_, record) => {
|
|
622
|
+
if (record.proxyType !== 'response') {
|
|
623
|
+
return '-';
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const matchers = record.paramMatchers || [];
|
|
627
|
+
const count = matchers.length;
|
|
628
|
+
|
|
629
|
+
if (count === 0) {
|
|
630
|
+
return '-';
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<Tooltip title={
|
|
635
|
+
<div>
|
|
636
|
+
{matchers.map((matcher, index) => (
|
|
637
|
+
<div key={index}>
|
|
638
|
+
{matcher.paramPath}: {matcher.paramValue}
|
|
639
|
+
<span style={{ color: '#52c41a', marginLeft: 4 }}>
|
|
640
|
+
({matcher.matchType === 'exact' ? '精确' : matcher.matchType === 'contains' ? '包含' : '正则'})
|
|
641
|
+
</span>
|
|
642
|
+
</div>
|
|
643
|
+
))}
|
|
644
|
+
</div>
|
|
645
|
+
}>
|
|
646
|
+
<span style={{ color: '#1890ff' }}>
|
|
647
|
+
{count}条规则
|
|
648
|
+
</span>
|
|
649
|
+
</Tooltip>
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
},
|
|
587
653
|
{
|
|
588
654
|
title: '请求方法',
|
|
589
655
|
dataIndex: 'httpMethod',
|
|
@@ -711,7 +777,8 @@ const InterfaceManagement = () => {
|
|
|
711
777
|
responses: [],
|
|
712
778
|
httpMethod: 'ALL',
|
|
713
779
|
targetUrl: '',
|
|
714
|
-
headerItems: []
|
|
780
|
+
headerItems: [],
|
|
781
|
+
paramMatchers: []
|
|
715
782
|
}}
|
|
716
783
|
>
|
|
717
784
|
{/* 基础信息表单项保持原样 */}
|
|
@@ -900,6 +967,87 @@ const InterfaceManagement = () => {
|
|
|
900
967
|
</div>
|
|
901
968
|
</div>
|
|
902
969
|
|
|
970
|
+
{/* 请求入参匹配设置 */}
|
|
971
|
+
<Form.Item
|
|
972
|
+
label="请求入参匹配"
|
|
973
|
+
tooltip="设置请求参数的匹配条件,只有当请求参数满足条件时才返回对应的响应。支持嵌套属性路径,如:a.b.c"
|
|
974
|
+
>
|
|
975
|
+
<Form.List name="paramMatchers">
|
|
976
|
+
{(fields, { add, remove }) => (
|
|
977
|
+
<>
|
|
978
|
+
{fields.map(({ key, name, ...restField }) => (
|
|
979
|
+
<div key={key} style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
|
980
|
+
<Form.Item
|
|
981
|
+
{...restField}
|
|
982
|
+
name={[name, 'paramPath']}
|
|
983
|
+
rules={[
|
|
984
|
+
{ required: true, message: '请输入参数路径' },
|
|
985
|
+
{
|
|
986
|
+
pattern: /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/,
|
|
987
|
+
message: '请输入有效的参数路径,如:userId 或 data.user.id'
|
|
988
|
+
}
|
|
989
|
+
]}
|
|
990
|
+
style={{ flex: 1, margin: 0 }}
|
|
991
|
+
>
|
|
992
|
+
<Input placeholder="参数路径(如:userId 或 data.user.id)" />
|
|
993
|
+
</Form.Item>
|
|
994
|
+
<Form.Item
|
|
995
|
+
{...restField}
|
|
996
|
+
name={[name, 'paramValue']}
|
|
997
|
+
rules={[{ required: true, message: '请输入期望值' }]}
|
|
998
|
+
style={{ flex: 1, margin: 0 }}
|
|
999
|
+
>
|
|
1000
|
+
<Input placeholder="期望值(如:123 或 admin)" />
|
|
1001
|
+
</Form.Item>
|
|
1002
|
+
<Form.Item
|
|
1003
|
+
{...restField}
|
|
1004
|
+
name={[name, 'matchType']}
|
|
1005
|
+
style={{ width: 100, margin: 0 }}
|
|
1006
|
+
initialValue="exact"
|
|
1007
|
+
>
|
|
1008
|
+
<Select placeholder="匹配类型">
|
|
1009
|
+
<Option value="exact">精确匹配</Option>
|
|
1010
|
+
<Option value="contains">包含</Option>
|
|
1011
|
+
<Option value="regex">正则</Option>
|
|
1012
|
+
</Select>
|
|
1013
|
+
</Form.Item>
|
|
1014
|
+
<Button
|
|
1015
|
+
type="text"
|
|
1016
|
+
danger
|
|
1017
|
+
icon={<DeleteOutlined />}
|
|
1018
|
+
onClick={() => remove(name)}
|
|
1019
|
+
/>
|
|
1020
|
+
</div>
|
|
1021
|
+
))}
|
|
1022
|
+
<Button
|
|
1023
|
+
type="dashed"
|
|
1024
|
+
onClick={() => add()}
|
|
1025
|
+
icon={<PlusOutlined />}
|
|
1026
|
+
style={{ width: '100%', marginTop: '8px' }}
|
|
1027
|
+
>
|
|
1028
|
+
添加参数匹配规则
|
|
1029
|
+
</Button>
|
|
1030
|
+
{fields.length > 0 && (
|
|
1031
|
+
<div style={{
|
|
1032
|
+
fontSize: '12px',
|
|
1033
|
+
color: '#666',
|
|
1034
|
+
marginTop: '8px',
|
|
1035
|
+
padding: '8px',
|
|
1036
|
+
backgroundColor: '#f5f5f5',
|
|
1037
|
+
borderRadius: '4px'
|
|
1038
|
+
}}>
|
|
1039
|
+
<div><strong>说明:</strong></div>
|
|
1040
|
+
<div>• <strong>精确匹配</strong>:参数值完全相等</div>
|
|
1041
|
+
<div>• <strong>包含</strong>:参数值包含指定内容</div>
|
|
1042
|
+
<div>• <strong>正则</strong>:参数值符合正则表达式</div>
|
|
1043
|
+
<div>• <strong>嵌套路径</strong>:使用点号分隔,如 data.user.id</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
)}
|
|
1046
|
+
</>
|
|
1047
|
+
)}
|
|
1048
|
+
</Form.List>
|
|
1049
|
+
</Form.Item>
|
|
1050
|
+
|
|
903
1051
|
{/* 隐藏的表单字段用于存储响应数据 */}
|
|
904
1052
|
<Form.Item
|
|
905
1053
|
name="responses"
|