whistle.mock-plugins 1.0.4 → 1.0.6-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -176,6 +176,7 @@ module.exports = function(req, res) {
176
176
  proxyType: interfaceData.proxyType,
177
177
  responseContent: interfaceData.responseContent,
178
178
  targetUrl: interfaceData.targetUrl,
179
+ customHeaders: interfaceData.customHeaders || {},
179
180
  filePath: interfaceData.filePath,
180
181
  httpStatus: interfaceData.httpStatus || 200,
181
182
  responseDelay: interfaceData.responseDelay || 0,
@@ -200,6 +201,7 @@ module.exports = function(req, res) {
200
201
  proxyType: interfaceData.proxyType || 'response',
201
202
  responseContent: interfaceData.responseContent || '',
202
203
  targetUrl: interfaceData.targetUrl || '',
204
+ customHeaders: interfaceData.customHeaders || {},
203
205
  filePath: interfaceData.filePath || '',
204
206
  httpStatus: interfaceData.httpStatus || 200,
205
207
  responseDelay: interfaceData.responseDelay || 0,
@@ -316,6 +318,7 @@ module.exports = function(req, res) {
316
318
  proxyType: interfaceData.proxyType || interfacesData.interfaces[index].proxyType,
317
319
  responseContent: interfaceData.responseContent !== undefined ? interfaceData.responseContent : interfacesData.interfaces[index].responseContent,
318
320
  targetUrl: interfaceData.targetUrl !== undefined ? interfaceData.targetUrl : interfacesData.interfaces[index].targetUrl,
321
+ customHeaders: interfaceData.customHeaders !== undefined ? interfaceData.customHeaders : interfacesData.interfaces[index].customHeaders || {},
319
322
  filePath: interfaceData.filePath !== undefined ? interfaceData.filePath : interfacesData.interfaces[index].filePath,
320
323
  httpStatus: interfaceData.httpStatus || interfacesData.interfaces[index].httpStatus,
321
324
  responseDelay: interfaceData.responseDelay !== undefined ? interfaceData.responseDelay : interfacesData.interfaces[index].responseDelay,
@@ -1,6 +1,189 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const Mock = require('mockjs');
4
+ const url = require('url');
5
+
6
+ // 缓存控制
7
+ const CONFIG = {
8
+ // 缓存有效期(毫秒),默认 60 秒
9
+ CACHE_INTERVAL: process.env.MOCK_PLUGIN_CACHE_INTERVAL || 60000
10
+ };
11
+
12
+ // 缓存控制变量
13
+ let enabledInterfacesCache = null; // 仅缓存启用状态的接口
14
+ let cacheTime = 0;
15
+
16
+ // 获取文件修改时间
17
+ const getFileModTime = (filePath) => {
18
+ try {
19
+ const stats = fs.statSync(filePath);
20
+ return stats.mtimeMs;
21
+ } catch (err) {
22
+ return 0;
23
+ }
24
+ };
25
+
26
+ // 生成随机数工具函数(与rules-server.js中保持一致)
27
+ const generateRandomValue = (pattern) => {
28
+ // 如果不是以@开头的模式,直接返回原值
29
+ if (!pattern || !pattern.startsWith('@')) {
30
+ return pattern;
31
+ }
32
+
33
+ const formatPattern = pattern.substring(1); // 去掉@前缀
34
+ // 使用单次正则替换优化性能
35
+ return formatPattern.replace(/x/g, () => {
36
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
37
+ return chars.charAt(Math.floor(Math.random() * chars.length));
38
+ });
39
+ };
40
+
41
+ // 正则表达式缓存
42
+ const regexCache = new Map();
43
+
44
+ // URL匹配函数,提取为纯函数减少重复代码
45
+ const isUrlMatchPattern = (url, pattern, proxyType) => {
46
+ if (!url || !pattern) return false;
47
+
48
+ try {
49
+ // 对于url_redirect类型,需要完全匹配
50
+ if (proxyType === 'url_redirect') {
51
+ return url === pattern;
52
+ }
53
+
54
+ // 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
55
+ if (proxyType === 'redirect') {
56
+ return url.startsWith(pattern);
57
+ }
58
+
59
+ // 默认的匹配逻辑(用于response类型等)
60
+ // 精确匹配
61
+ if (pattern === url) {
62
+ return true;
63
+ }
64
+
65
+ // 正则表达式
66
+ if (pattern.startsWith('/') && pattern.length > 2 && pattern.endsWith('/')) {
67
+ // 使用缓存的正则表达式
68
+ const cacheKey = `regex:${pattern}`;
69
+ let regex = regexCache.get(cacheKey);
70
+
71
+ if (!regex) {
72
+ regex = new RegExp(pattern.slice(1, -1));
73
+ regexCache.set(cacheKey, regex);
74
+ }
75
+
76
+ return regex.test(url);
77
+ }
78
+
79
+ // 通配符模式
80
+ if (pattern.includes('*')) {
81
+ // 使用缓存的正则表达式
82
+ const cacheKey = `wildcard:${pattern}`;
83
+ let regex = regexCache.get(cacheKey);
84
+
85
+ if (!regex) {
86
+ const regexPattern = pattern
87
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
88
+ .replace(/\*/g, '.*');
89
+ regex = new RegExp(`^${regexPattern}$`);
90
+ regexCache.set(cacheKey, regex);
91
+ }
92
+
93
+ return regex.test(url);
94
+ }
95
+
96
+ return false;
97
+ } catch (e) {
98
+ console.error('URL匹配检查失败:', e);
99
+ return false;
100
+ }
101
+ };
102
+
103
+ // 加载启用的接口数据,带缓存
104
+ const loadEnabledInterfaces = (dataDir, interfacesFile, featuresFile) => {
105
+ try {
106
+ const currentTime = Date.now();
107
+ const interfacesFileModTime = getFileModTime(interfacesFile);
108
+ const featuresFileModTime = getFileModTime(featuresFile);
109
+
110
+ // 取两个文件的最后修改时间的最大值
111
+ const lastModTime = Math.max(interfacesFileModTime, featuresFileModTime);
112
+
113
+ // 使用缓存
114
+ if (enabledInterfacesCache &&
115
+ currentTime - cacheTime < CONFIG.CACHE_INTERVAL &&
116
+ lastModTime <= cacheTime) {
117
+ return enabledInterfacesCache;
118
+ }
119
+
120
+ // 1. 读取并解析所有功能模块
121
+ let enabledFeatureIds = [];
122
+ if (fs.existsSync(featuresFile)) {
123
+ try {
124
+ const featuresData = fs.readJsonSync(featuresFile);
125
+ let features = [];
126
+
127
+ if (featuresData && Array.isArray(featuresData)) {
128
+ features = featuresData;
129
+ } else if (featuresData && featuresData.features && Array.isArray(featuresData.features)) {
130
+ features = featuresData.features;
131
+ }
132
+
133
+ // 过滤出启用状态的功能ID
134
+ enabledFeatureIds = features
135
+ .filter(feature => feature.active === true)
136
+ .map(feature => feature.id);
137
+ } catch (err) {
138
+ console.error('读取功能模块失败:', err);
139
+ }
140
+ }
141
+
142
+ // 2. 读取并解析所有接口
143
+ let enabledInterfaces = [];
144
+ if (fs.existsSync(interfacesFile)) {
145
+ try {
146
+ const interfacesData = fs.readJsonSync(interfacesFile);
147
+ let interfaces = [];
148
+
149
+ if (interfacesData && Array.isArray(interfacesData)) {
150
+ interfaces = interfacesData;
151
+ } else if (interfacesData && interfacesData.interfaces && Array.isArray(interfacesData.interfaces)) {
152
+ interfaces = interfacesData.interfaces;
153
+ }
154
+
155
+ // 过滤出启用状态的接口
156
+ enabledInterfaces = interfaces.filter(intf => {
157
+ // 首先检查接口本身是否启用
158
+ const isInterfaceEnabled = intf.active !== false; // 默认为true
159
+
160
+ if (!isInterfaceEnabled) {
161
+ return false;
162
+ }
163
+
164
+ // 如果接口有关联功能,检查功能是否启用
165
+ if (intf.featureId) {
166
+ return enabledFeatureIds.includes(intf.featureId);
167
+ }
168
+
169
+ // 如果接口没有关联功能,只要接口本身启用就可以
170
+ return true;
171
+ });
172
+ } catch (err) {
173
+ console.error('读取接口配置失败:', err);
174
+ }
175
+ }
176
+
177
+ // 更新缓存
178
+ enabledInterfacesCache = enabledInterfaces;
179
+ cacheTime = currentTime;
180
+
181
+ return enabledInterfaces;
182
+ } catch (err) {
183
+ console.error('加载启用接口失败:', err);
184
+ return enabledInterfacesCache || [];
185
+ }
186
+ };
4
187
 
5
188
  module.exports = async function(req, res) {
6
189
  const dataDir = this.dataDir;
@@ -29,27 +212,19 @@ module.exports = async function(req, res) {
29
212
  });
30
213
  }
31
214
 
32
- // 读取接口数据
33
- let interfacesData;
34
- try {
35
- interfacesData = fs.readJsonSync(interfacesFile);
36
- if (!interfacesData || !interfacesData.interfaces) {
37
- return res.status(404).json({
38
- code: 404,
39
- message: '未找到接口数据',
40
- data: null
41
- });
42
- }
43
- } catch (err) {
44
- return res.status(500).json({
45
- code: 500,
46
- message: '读取接口数据失败',
215
+ // 读取接口数据(使用缓存)
216
+ const interfaces = loadEnabledInterfaces(dataDir, interfacesFile, featuresFile);
217
+
218
+ if (!interfaces || interfaces.length === 0) {
219
+ return res.status(404).json({
220
+ code: 404,
221
+ message: '未找到接口数据',
47
222
  data: null
48
223
  });
49
224
  }
50
225
 
51
226
  // 查找指定接口
52
- const targetInterface = interfacesData.interfaces.find(item => item.id === interfaceId);
227
+ const targetInterface = interfaces.find(item => item.id === interfaceId);
53
228
 
54
229
  if (!targetInterface) {
55
230
  return res.status(404).json({
@@ -59,240 +234,114 @@ module.exports = async function(req, res) {
59
234
  });
60
235
  }
61
236
 
62
- // 检查接口是否已禁用
63
- if (targetInterface.active === false) {
64
- return res.status(400).json({
65
- code: 400,
66
- message: '接口已禁用,无法测试',
67
- data: null
68
- });
69
- }
237
+ // 检查接口URL是否与测试URL匹配
238
+ const pattern = targetInterface.pattern;
239
+ const proxyType = targetInterface.proxyType || 'response';
70
240
 
71
- // 检查功能模块是否已禁用
72
- try {
73
- const featuresData = fs.readJsonSync(featuresFile);
74
-
75
- if (featuresData && featuresData.features) {
76
- const featureItem = featuresData.features.find(item => item.id === targetInterface.featureId);
77
-
78
- if (featureItem && featureItem.active === false) {
79
- return res.status(400).json({
80
- code: 400,
81
- message: '所属功能模块已禁用,接口无法使用',
82
- data: null
83
- });
84
- }
85
- }
86
- } catch (err) {
87
- // 忽略读取错误,继续处理
88
- }
241
+ // 使用URL匹配检查
242
+ const isMatch = isUrlMatchPattern(url, pattern, proxyType);
89
243
 
90
- const startTime = Date.now();
244
+ // 如果存在method配置,检查HTTP方法是否匹配
245
+ const methodMatch = !targetInterface.method ||
246
+ targetInterface.method === 'all' ||
247
+ targetInterface.method.toLowerCase() === 'all' ||
248
+ req.method.toLowerCase() === targetInterface.method.toLowerCase();
91
249
 
92
- // 根据代理类型处理不同的响应
93
- let responseData;
250
+ // 确定匹配结果
251
+ const matchResult = isMatch && methodMatch;
94
252
 
95
- // 检查 URL 是否匹配模式
96
- const isUrlMatchPattern = (url, pattern, proxyType) => {
97
- if (!url || !pattern) return false;
98
-
99
- try {
100
- // 对于url_redirect类型,需要完全匹配
101
- if (proxyType === 'url_redirect') {
102
- return url === pattern;
103
- }
104
-
105
- // 对于redirect类型,只要url以pattern开头即可命中(前缀匹配)
106
- if (proxyType === 'redirect') {
107
- return url.indexOf(pattern) === 0;
108
- }
109
-
110
- // 默认的匹配逻辑(用于response类型等)
111
- // 如果是正则表达式
112
- if (pattern.startsWith('/') && pattern.endsWith('/')) {
113
- const regex = new RegExp(pattern.slice(1, -1));
114
- return regex.test(url);
115
- }
116
-
117
- // 如果是通配符模式
118
- if (pattern.includes('*')) {
119
- const regexPattern = pattern
120
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
121
- .replace(/\*/g, '.*');
122
- const regex = new RegExp(`^${regexPattern}$`);
123
- return regex.test(url);
124
- }
125
-
126
- // 精确匹配
127
- return url === pattern;
128
- } catch (e) {
129
- console.error('URL匹配检查失败:', e);
130
- return false;
253
+ // 构建测试结果
254
+ const result = {
255
+ interfaceId: targetInterface.id,
256
+ interfaceName: targetInterface.name,
257
+ proxyType: targetInterface.proxyType,
258
+ pattern: targetInterface.pattern,
259
+ url: url,
260
+ isMatch: matchResult,
261
+ matchDetail: {
262
+ urlMatch: isMatch,
263
+ methodMatch: methodMatch
131
264
  }
132
265
  };
133
266
 
134
- // 处理不同的代理类型
135
- switch (targetInterface.proxyType) {
136
- case 'response':
137
- // 处理内容,支持 Mock.js 模板语法
138
- let responseBody = targetInterface.responseContent;
139
-
140
- try {
141
- // 如果响应内容是JSON,则尝试应用Mock.js模板
142
- if (targetInterface.contentType && targetInterface.contentType.includes('json')) {
143
- // 尝试解析JSON
144
- const jsonTemplate = JSON.parse(responseBody);
145
- // 使用Mock.js生成随机数据
146
- const mockData = Mock.mock(jsonTemplate);
147
- // 将结果转换回字符串
148
- responseBody = JSON.stringify(mockData);
149
- }
150
- } catch (err) {
151
- // 如果Mock失败,使用原始内容
152
- }
153
-
154
- // 模拟接口处理延迟
155
- const delay = targetInterface.responseDelay || 0;
156
- if (delay > 0) {
157
- await new Promise(resolve => setTimeout(resolve, delay));
158
- }
159
-
160
- const endTime = Date.now();
161
- const duration = endTime - startTime;
162
-
163
- // 模拟接口处理
164
- responseData = {
165
- statusCode: targetInterface.httpStatus || 200,
166
- contentType: targetInterface.contentType || 'application/json; charset=utf-8',
167
- responseBody: responseBody,
168
- mockInfo: {
169
- matchedPattern: targetInterface.urlPattern,
170
- requestMethod: targetInterface.httpMethod,
171
- delay: delay,
172
- duration: duration,
173
- timestamp: new Date().toISOString()
174
- }
175
- };
176
- break;
177
-
178
- case 'redirect':
179
- // 处理重定向
180
- const redirectTarget = targetInterface.targetUrl || '';
181
-
182
- // 验证目标URL是否有效
183
- if (!redirectTarget) {
184
- return res.status(400).json({
185
- code: 400,
186
- message: '重定向目标URL不能为空',
187
- data: null
188
- });
189
- }
190
-
191
- try {
192
- // 验证URL格式
193
- new URL(redirectTarget);
194
- } catch (err) {
195
- return res.status(400).json({
196
- code: 400,
197
- message: '重定向目标URL格式无效,必须包含http://或https://',
198
- data: null
199
- });
200
- }
201
-
202
- // 验证URL匹配(前缀匹配)
203
- if (url.indexOf(targetInterface.urlPattern) !== 0) {
204
- return res.status(400).json({
205
- code: 400,
206
- message: `测试URL不匹配重定向规则,URL必须以 ${targetInterface.urlPattern} 开头`,
207
- data: null
208
- });
267
+ // 根据代理类型处理额外信息
268
+ if (matchResult) {
269
+ if (proxyType === 'response') {
270
+ // 添加响应数据(如果有)
271
+ if (targetInterface.response) {
272
+ result.responseData = targetInterface.response;
209
273
  }
210
-
211
- responseData = {
212
- statusCode: 302,
213
- contentType: 'application/json; charset=utf-8',
214
- proxyType: 'redirect',
215
- targetUrl: redirectTarget,
216
- mockInfo: {
217
- matchedPattern: targetInterface.urlPattern,
218
- requestMethod: targetInterface.httpMethod,
219
- timestamp: new Date().toISOString()
274
+ } else if (proxyType === 'redirect' || proxyType === 'url_redirect') {
275
+ // 添加重定向信息(如果有)
276
+ if (targetInterface.targetUrl) {
277
+ // 处理动态生成的目标URL
278
+ const targetUrl = generateRandomValue(targetInterface.targetUrl);
279
+ result.redirectInfo = {
280
+ targetUrl: targetUrl,
281
+ targetFormatted: targetUrl
282
+ };
283
+
284
+ // 添加自定义请求头(如果有)
285
+ if (targetInterface.customHeaders) {
286
+ try {
287
+ result.redirectInfo.customHeaders = targetInterface.customHeaders;
288
+ } catch (e) {
289
+ console.error('解析自定义请求头失败:', e);
290
+ }
220
291
  }
221
- };
222
- break;
223
-
224
- case 'url_redirect':
225
- // 处理URL重定向
226
- const baseUrl = targetInterface.targetUrl || '';
227
-
228
- // 验证基础URL是否有效
229
- if (!baseUrl) {
230
- return res.status(400).json({
231
- code: 400,
232
- message: 'URL重定向目标URL不能为空',
233
- data: null
234
- });
235
- }
236
-
237
- try {
238
- // 验证URL格式
239
- new URL(baseUrl);
240
- } catch (err) {
241
- return res.status(400).json({
242
- code: 400,
243
- message: 'URL重定向目标URL格式无效,必须包含http://或https://',
244
- data: null
245
- });
246
- }
247
-
248
- // 验证URL完全匹配
249
- if (url !== targetInterface.urlPattern) {
250
- return res.status(400).json({
251
- code: 400,
252
- message: `测试URL不匹配URL重定向规则,URL必须完全匹配 ${targetInterface.urlPattern}`,
253
- data: null
254
- });
255
292
  }
256
-
257
- responseData = {
258
- statusCode: 302,
259
- contentType: 'application/json; charset=utf-8',
260
- proxyType: 'url_redirect',
261
- targetUrl: baseUrl,
262
- mockInfo: {
263
- matchedPattern: targetInterface.urlPattern,
264
- requestMethod: targetInterface.httpMethod,
265
- timestamp: new Date().toISOString()
266
- }
267
- };
268
- break;
269
-
270
- default:
271
- // 不支持的代理类型
272
- return res.status(400).json({
273
- code: 400,
274
- message: `不支持的代理类型: ${targetInterface.proxyType || 'undefined'}`,
275
- data: null
276
- });
293
+ }
277
294
  }
278
295
 
296
+ // 返回测试结果
279
297
  return res.json({
280
- code: 0,
281
- message: '成功',
282
- data: responseData
298
+ code: 200,
299
+ message: matchResult ? '接口匹配成功' : '接口匹配失败',
300
+ data: result
283
301
  });
284
- } else {
285
- // 不支持的请求方法
286
- return res.status(405).json({
287
- code: 405,
288
- message: '不支持的请求方法',
302
+ }
303
+
304
+ // 处理缓存刷新请求
305
+ if (req.path === '/_flush_cache') {
306
+ // 清空缓存
307
+ enabledInterfacesCache = null;
308
+ cacheTime = 0;
309
+
310
+ return res.json({
311
+ code: 200,
312
+ message: '缓存已刷新',
289
313
  data: null
290
314
  });
291
315
  }
316
+
317
+ // 处理缓存状态请求
318
+ if (req.path === '/_cache_status') {
319
+ return res.json({
320
+ code: 200,
321
+ message: '缓存状态',
322
+ data: {
323
+ enabledInterfaces: {
324
+ cached: !!enabledInterfacesCache,
325
+ lastUpdate: cacheTime,
326
+ count: enabledInterfacesCache ? enabledInterfacesCache.length : 0
327
+ },
328
+ cacheInterval: CONFIG.CACHE_INTERVAL
329
+ }
330
+ });
331
+ }
332
+
333
+ // 其他请求返回405
334
+ return res.status(405).json({
335
+ code: 405,
336
+ message: '方法不允许',
337
+ data: null
338
+ });
292
339
  } catch (err) {
340
+ console.error('测试接口处理错误:', err);
341
+
293
342
  return res.status(500).json({
294
343
  code: 500,
295
- message: '服务器内部错误: ' + err.message,
344
+ message: '服务器内部错误: ' + (err.message || err),
296
345
  data: null
297
346
  });
298
347
  }