whistle.mockbubu 1.0.0-dev.4 → 2.0.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.
@@ -0,0 +1,520 @@
1
+ # Mock 数据分组功能 - 技术方案
2
+
3
+ **版本**: v2.0.0
4
+ **设计日期**: 2025-10-16
5
+ **设计者**: Claude Code
6
+
7
+ ---
8
+
9
+ ## 一、需求分析与功能定义
10
+
11
+ ### 核心需求
12
+ 实现 Mock 数据的分组管理,让用户可以在不同场景(如:开发环境、测试环境、演示环境)下快速切换不同的 Mock 数据配置组合。
13
+
14
+ ### 功能特性
15
+ 1. **默认组**: 当前使用的面板即为默认组,所有现有功能保持不变
16
+ 2. **组管理**: CRUD 操作(创建、读取、更新、删除)
17
+ 3. **组切换**: 快速切换当前使用的组
18
+ 4. **组复制**: 从现有组复制创建新组
19
+ 5. **原始数据源**: 插件抓取的请求作为原始数据,不属于任何组
20
+ 6. **独立配置**: 每个组拥有独立的 mock 状态、锁定状态、版本等配置
21
+
22
+ ### 使用场景示例
23
+ - **场景A**: 用户正常流程,需要 mock 登录成功、订单列表有数据
24
+ - **场景B**: 异常测试流程,需要 mock 登录失败、订单列表为空
25
+ - 切换组后,所有 mock 状态、数据内容都自动切换,无需手动逐个修改
26
+
27
+ ---
28
+
29
+ ## 二、架构设计
30
+
31
+ ### 2.1 数据模型
32
+
33
+ #### 原始数据层(Source Layer)
34
+ ```javascript
35
+ // Whistle Storage 结构保持不变
36
+ files/
37
+ ├── {url-1} // 原始抓包 session 数据(req + res)
38
+ ├── {url-2}
39
+ └── ...
40
+
41
+ properties
42
+ ├── {url-1} // 基础元数据(url, method, status, date 等)
43
+ ├── {url-2}
44
+ └── ...
45
+ ```
46
+
47
+ **说明**:
48
+ - 原始数据不属于任何组
49
+ - 插件抓取的请求都存储在这里
50
+ - 作为所有组的数据源,只读不改
51
+
52
+ #### 分组数据层(Group Layer)
53
+ ```javascript
54
+ // 新增 Storage 结构
55
+ properties/
56
+ ├── __groups__ // 组列表配置
57
+ │ └── {
58
+ │ groups: [
59
+ │ {id, name, description, createTime, updateTime, isDefault},
60
+ │ ...
61
+ │ ],
62
+ │ currentGroupId: 'group-xxx' // 当前激活的组
63
+ │ }
64
+
65
+ ├── group.{groupId}.{filename} // 组级别的配置
66
+ │ └── {
67
+ │ mock: true/false,
68
+ │ locked: true/false,
69
+ │ mockVersion: 'v1',
70
+ │ mockTime: timestamp,
71
+ │ version.{name}: content, // 该组的版本数据
72
+ │ versionMeta.{name}: {...},
73
+ │ customData: {...} // 组内自定义编辑的数据
74
+ │ }
75
+ └── ...
76
+ ```
77
+
78
+ **核心概念**:
79
+ 1. **原始数据(Source)**: files/ 中的 session 数据,所有组共享
80
+ 2. **组配置(Group Config)**: 每个组对每个文件的个性化配置(mock状态、版本等)
81
+ 3. **组数据(Group Data)**: 每个组内编辑的自定义数据(覆盖原始数据)
82
+
83
+ ### 2.2 数据读取优先级
84
+
85
+ ```
86
+ 用户访问 API →
87
+ 检查当前组ID →
88
+ 读取 group.{groupId}.{filename} 配置 →
89
+ 如果 mock=true:
90
+ 如果有 customData: 返回 customData
91
+ 否则如果有 mockVersion: 返回 version.{name}
92
+ 否则: 返回原始 session.res.body
93
+ 否则: 透传请求
94
+ ```
95
+
96
+ ### 2.3 组切换流程
97
+
98
+ ```
99
+ 用户选择组 →
100
+ 更新 __groups__.currentGroupId →
101
+ 前端刷新列表 (带 groupId 参数) →
102
+ 后端根据 groupId 读取该组配置 →
103
+ 返回该组视图的数据列表
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 三、实现方案
109
+
110
+ ### 3.1 后端实现
111
+
112
+ #### 新增文件: `lib/group-manager.js`
113
+ ```javascript
114
+ // 组管理核心逻辑
115
+ class GroupManager {
116
+ constructor(storage) {
117
+ this.storage = storage
118
+ this.GROUPS_KEY = '__groups__'
119
+ }
120
+
121
+ // 获取所有组
122
+ getGroups() { ... }
123
+
124
+ // 获取当前组ID
125
+ getCurrentGroupId() { ... }
126
+
127
+ // 设置当前组
128
+ setCurrentGroup(groupId) { ... }
129
+
130
+ // 创建组
131
+ createGroup({name, description, copyFromGroupId}) { ... }
132
+
133
+ // 更新组
134
+ updateGroup(groupId, {name, description}) { ... }
135
+
136
+ // 删除组
137
+ deleteGroup(groupId) { ... }
138
+
139
+ // 复制组
140
+ copyGroup(sourceGroupId, {name, description}) { ... }
141
+
142
+ // 获取组的文件配置
143
+ getGroupFileConfig(groupId, filename) { ... }
144
+
145
+ // 设置组的文件配置
146
+ setGroupFileConfig(groupId, filename, config) { ... }
147
+ }
148
+ ```
149
+
150
+ #### 新增文件: `lib/uiServer/router/group-router.js`
151
+ ```javascript
152
+ // 组管理 API 路由
153
+ router.post('/cgi-bin/mockbubu/groups/list', ...) // 获取组列表
154
+ router.post('/cgi-bin/mockbubu/groups/create', ...) // 创建组
155
+ router.post('/cgi-bin/mockbubu/groups/update', ...) // 更新组
156
+ router.post('/cgi-bin/mockbubu/groups/delete', ...) // 删除组
157
+ router.post('/cgi-bin/mockbubu/groups/copy', ...) // 复制组
158
+ router.post('/cgi-bin/mockbubu/groups/switch', ...) // 切换当前组
159
+ router.post('/cgi-bin/mockbubu/groups/current', ...) // 获取当前组
160
+ ```
161
+
162
+ #### 修改文件: `lib/uiServer/router/index.js`
163
+ ```javascript
164
+ // 所有 API 增加 groupId 参数支持
165
+ // 修改点:
166
+ 1. /api-list: 增加 groupId 参数,返回该组视图的数据
167
+ 2. /update-api-mock: 更新指定组的 mock 状态
168
+ 3. /update-api-data: 更新指定组的数据
169
+ 4. /create-api-data: 在指定组创建数据
170
+ 5. /delete-api: 删除指定组的配置
171
+ ... 所有现有 API 都需要支持 groupId
172
+ ```
173
+
174
+ #### 修改文件: `lib/server.js`
175
+ ```javascript
176
+ // 请求拦截时读取当前组配置
177
+ module.exports = (server, { storage }) => {
178
+ const groupManager = new GroupManager(storage)
179
+
180
+ server.on('request', (req, res) => {
181
+ const currentGroupId = groupManager.getCurrentGroupId()
182
+ const groupConfig = groupManager.getGroupFileConfig(currentGroupId, filename)
183
+
184
+ // 根据组配置决定 mock 行为
185
+ if (groupConfig.mock) {
186
+ // 优先使用组的自定义数据
187
+ if (groupConfig.customData) {
188
+ res.end(JSON.stringify(groupConfig.customData))
189
+ } else if (groupConfig.mockVersion) {
190
+ // 使用组的版本数据
191
+ res.end(JSON.stringify(groupConfig.version[versionName]))
192
+ } else {
193
+ // 使用原始数据
194
+ res.end(sessionCache.res.body)
195
+ }
196
+ } else {
197
+ req.passThrough()
198
+ }
199
+ })
200
+ }
201
+ ```
202
+
203
+ ### 3.2 前端实现
204
+
205
+ #### 新增组件: `app/src/components/group/GroupManager.vue`
206
+ ```vue
207
+ <template>
208
+ <el-dialog title="分组管理" :visible.sync="visible" width="800px">
209
+ <!-- 组列表 -->
210
+ <el-table :data="groups">
211
+ <el-table-column label="组名" prop="name" />
212
+ <el-table-column label="描述" prop="description" />
213
+ <el-table-column label="创建时间" prop="createTime" />
214
+ <el-table-column label="操作" width="300">
215
+ <template slot-scope="scope">
216
+ <el-button @click="handleSwitch(scope.row)">切换</el-button>
217
+ <el-button @click="handleEdit(scope.row)">编辑</el-button>
218
+ <el-button @click="handleCopy(scope.row)">复制</el-button>
219
+ <el-button @click="handleDelete(scope.row)">删除</el-button>
220
+ </template>
221
+ </el-table-column>
222
+ </el-table>
223
+
224
+ <!-- 创建/编辑对话框 -->
225
+ <el-button @click="showCreateDialog">新建分组</el-button>
226
+ </el-dialog>
227
+ </template>
228
+ ```
229
+
230
+ #### 新增组件: `app/src/components/group/GroupSelector.vue`
231
+ ```vue
232
+ <template>
233
+ <div class="group-selector">
234
+ <el-select v-model="currentGroupId" @change="handleSwitch">
235
+ <el-option
236
+ v-for="group in groups"
237
+ :key="group.id"
238
+ :label="group.name"
239
+ :value="group.id"
240
+ />
241
+ </el-select>
242
+ <el-button @click="openManager">管理分组</el-button>
243
+ </div>
244
+ </template>
245
+ ```
246
+
247
+ #### 修改组件: `app/src/components/header/ActionPanel.vue`
248
+ ```vue
249
+ <template>
250
+ <div class="action-panel">
251
+ <!-- 新增:组选择器 -->
252
+ <GroupSelector />
253
+
254
+ <!-- 原有搜索区域 -->
255
+ <div class="action-panel__search">...</div>
256
+
257
+ <!-- 原有操作区域 -->
258
+ <div class="action-panel__actions">...</div>
259
+ </div>
260
+ </template>
261
+ ```
262
+
263
+ #### 修改文件: `app/src/service/index.js`
264
+ ```javascript
265
+ // 新增组管理相关 API
266
+ export function getGroups() { ... }
267
+ export function createGroup(params) { ... }
268
+ export function updateGroup(groupId, params) { ... }
269
+ export function deleteGroup(groupId) { ... }
270
+ export function copyGroup(sourceGroupId, params) { ... }
271
+ export function switchGroup(groupId) { ... }
272
+ export function getCurrentGroup() { ... }
273
+
274
+ // 修改现有 API,增加 groupId 参数
275
+ export function search(params) {
276
+ const groupId = getCurrentGroupIdFromStore()
277
+ return fetch('/cgi-bin/mockbubu/api-list', {
278
+ method: 'post',
279
+ body: JSON.stringify({ ...params, groupId }),
280
+ ...
281
+ })
282
+ }
283
+ // ... 其他 API 同样修改
284
+ ```
285
+
286
+ #### 新增文件: `app/src/store/group.js`
287
+ ```javascript
288
+ // Vuex/Pinia store 或 localStorage 管理当前组
289
+ export const groupStore = {
290
+ currentGroupId: null,
291
+ groups: [],
292
+
293
+ async init() {
294
+ const res = await getCurrentGroup()
295
+ this.currentGroupId = res.data.currentGroupId
296
+ this.groups = res.data.groups
297
+ },
298
+
299
+ async switchGroup(groupId) {
300
+ await switchGroup(groupId)
301
+ this.currentGroupId = groupId
302
+ // 刷新页面数据
303
+ window.location.reload()
304
+ }
305
+ }
306
+ ```
307
+
308
+ ---
309
+
310
+ ## 四、实施步骤
311
+
312
+ ### Phase 1: 基础架构 ✅ 完成
313
+ 1. ✅ 实现 GroupManager 类
314
+ 2. ✅ 创建默认组(迁移现有数据)
315
+ 3. ✅ 修改 server.js 支持组配置读取
316
+ 4. ✅ 测试数据读取优先级
317
+ 5. ✅ 添加组级别版本管理方法
318
+
319
+ ### Phase 2: 后端 API ✅ 完成
320
+ 1. ✅ 实现 group-router.js 所有 API
321
+ 2. ✅ 修改现有 API 支持 groupId 参数
322
+ 3. ✅ API 测试
323
+ 4. ✅ 修复 version-router.js 使用组级别版本操作
324
+
325
+ ### Phase 3: 前端 UI ✅ 完成
326
+ 1. ✅ 实现 GroupSelector 组件
327
+ 2. ✅ 实现 GroupManager 对话框
328
+ 3. ✅ 集成到 ActionPanel
329
+ 4. ✅ 修改所有 API 调用增加 groupId
330
+ 5. ✅ 添加表单验证和用户体验优化
331
+ 6. ✅ 添加列表刷新过渡动画
332
+
333
+ ### Phase 4: 数据迁移 ✅ 完成
334
+ 1. ✅ 编写迁移脚本,将现有数据移到默认组
335
+ 2. ✅ 兼容性测试
336
+ 3. ✅ 向后兼容旧数据
337
+
338
+ ### Phase 5: Bug 修复 ✅ 完成
339
+ 1. ✅ 修复 Element UI Form 组件注册问题
340
+ 2. ✅ 修复表单验证回调模式
341
+ 3. ✅ 修复 Mock 不生效问题(缓存缺失处理)
342
+ 4. ✅ 修复版本数据独立性问题(核心修复)
343
+ 5. ✅ 优化组切换用户体验
344
+ 6. ✅ 完整功能测试
345
+
346
+ **总计**: 已完成所有开发和测试
347
+
348
+ ---
349
+
350
+ ## 五、关键技术点
351
+
352
+ ### 5.1 数据隔离
353
+ - 每个组的配置独立存储
354
+ - 组之间互不影响
355
+ - 删除组只删除配置,不删除原始数据
356
+
357
+ ### 5.2 向后兼容
358
+ - 首次启动自动创建默认组
359
+ - 将现有数据迁移到默认组
360
+ - 旧版本插件数据可无缝升级
361
+
362
+ ### 5.3 性能优化
363
+ - 组列表缓存
364
+ - 当前组配置缓存
365
+ - 批量读取组配置
366
+
367
+ ### 5.4 错误处理
368
+ - 组不存在时降级到默认组
369
+ - 删除当前组时自动切换到默认组
370
+ - 组配置损坏时使用原始数据
371
+
372
+ ---
373
+
374
+ ## 六、用户体验设计
375
+
376
+ ### 组选择器位置
377
+ 放在 ActionPanel 左上角,搜索框之前:
378
+ ```
379
+ [分组: 默认组 ▼] [管理分组] | [搜索框] [筛选器] ...
380
+ ```
381
+
382
+ ### 管理分组对话框
383
+ - 表格展示所有组
384
+ - 当前组高亮显示
385
+ - 提供快捷操作(切换、编辑、复制、删除)
386
+ - 创建组时可选择从哪个组复制
387
+
388
+ ### 组切换提示
389
+ - 切换组后显示提示:"已切换到【测试组】"
390
+ - 页面自动刷新数据
391
+ - 保持当前选中的文件(如果存在)
392
+
393
+ ---
394
+
395
+ ## 七、风险与应对
396
+
397
+ ### 风险1: 数据迁移失败
398
+ **应对**:
399
+ - 迁移前自动备份
400
+ - 提供回滚脚本
401
+ - 灰度发布,小范围测试
402
+
403
+ ### 风险2: 性能下降
404
+ **应对**:
405
+ - 组配置缓存机制
406
+ - 懒加载组数据
407
+ - 限制组数量上限(如10个)
408
+
409
+ ### 风险3: 用户困惑
410
+ **应对**:
411
+ - 提供详细文档和视频教程
412
+ - 新手引导提示
413
+ - 默认组自动创建,用户无感知
414
+
415
+ ---
416
+
417
+ ## 八、后续扩展
418
+
419
+ ### 可能的增强功能
420
+ 1. **组导入导出**: 导出组配置为 JSON,分享给团队
421
+ 2. **组权限**: 只读组、编辑组、管理员组
422
+ 3. **组模板**: 预设常用场景组(成功场景、失败场景等)
423
+ 4. **组对比**: 对比两个组的配置差异
424
+ 5. **组历史**: 记录组的修改历史,支持回滚
425
+
426
+ ---
427
+
428
+ ## 九、API 接口定义
429
+
430
+ ### 组管理 API
431
+
432
+ #### 1. 获取组列表
433
+ ```http
434
+ POST /cgi-bin/mockbubu/groups/list
435
+ Response: {
436
+ code: 200,
437
+ data: {
438
+ groups: [
439
+ {id, name, description, createTime, updateTime, isDefault}
440
+ ],
441
+ currentGroupId: 'xxx'
442
+ }
443
+ }
444
+ ```
445
+
446
+ #### 2. 创建组
447
+ ```http
448
+ POST /cgi-bin/mockbubu/groups/create
449
+ Body: {name, description, copyFromGroupId?}
450
+ Response: {code: 200, data: {newGroup}}
451
+ ```
452
+
453
+ #### 3. 更新组
454
+ ```http
455
+ POST /cgi-bin/mockbubu/groups/update
456
+ Body: {groupId, name?, description?}
457
+ Response: {code: 200, data: {updatedGroup}}
458
+ ```
459
+
460
+ #### 4. 删除组
461
+ ```http
462
+ POST /cgi-bin/mockbubu/groups/delete
463
+ Body: {groupId}
464
+ Response: {code: 200, msg: '删除成功'}
465
+ ```
466
+
467
+ #### 5. 切换当前组
468
+ ```http
469
+ POST /cgi-bin/mockbubu/groups/switch
470
+ Body: {groupId}
471
+ Response: {code: 200, data: {currentGroup}}
472
+ ```
473
+
474
+ #### 6. 获取当前组
475
+ ```http
476
+ POST /cgi-bin/mockbubu/groups/current
477
+ Response: {code: 200, data: {currentGroup}}
478
+ ```
479
+
480
+ ---
481
+
482
+ ## 十、数据库 Schema
483
+
484
+ ### properties.__groups__
485
+ ```json
486
+ {
487
+ "groups": [
488
+ {
489
+ "id": "default",
490
+ "name": "默认组",
491
+ "description": "系统默认分组",
492
+ "createTime": 1697472000000,
493
+ "updateTime": 1697472000000,
494
+ "isDefault": true
495
+ }
496
+ ],
497
+ "currentGroupId": "default"
498
+ }
499
+ ```
500
+
501
+ ### properties.group.{groupId}.{filename}
502
+ ```json
503
+ {
504
+ "mock": true,
505
+ "locked": false,
506
+ "mockVersion": "v1",
507
+ "mockTime": 1697472000000,
508
+ "version.v1": "{...json...}",
509
+ "versionMeta.v1": {
510
+ "description": "版本1",
511
+ "createTime": 1697472000000,
512
+ "updateTime": 1697472000000
513
+ }
514
+ }
515
+ ```
516
+
517
+ ---
518
+
519
+ **文档版本**: v1.0
520
+ **最后更新**: 2025-10-16
package/lib/const.js CHANGED
@@ -26,3 +26,22 @@ exports.LockedFilterMap = {
26
26
  'locked': { key: 'locked', value: true, method: 'equal' },
27
27
  'unlocked': { key: 'locked', value: false, method: 'equal' },
28
28
  }
29
+
30
+ // HTTP Method 筛选映射
31
+ exports.MethodFilterMap = {
32
+ 'GET': { key: 'method', value: 'GET', method: 'equal' },
33
+ 'POST': { key: 'method', value: 'POST', method: 'equal' },
34
+ 'PUT': { key: 'method', value: 'PUT', method: 'equal' },
35
+ 'DELETE': { key: 'method', value: 'DELETE', method: 'equal' },
36
+ 'PATCH': { key: 'method', value: 'PATCH', method: 'equal' },
37
+ 'HEAD': { key: 'method', value: 'HEAD', method: 'equal' },
38
+ 'OPTIONS': { key: 'method', value: 'OPTIONS', method: 'equal' },
39
+ }
40
+
41
+ // 状态码筛选映射(使用自定义 method: 'statusRange')
42
+ exports.StatusFilterMap = {
43
+ '2xx': { key: 'status', value: [200, 299], method: 'range' },
44
+ '3xx': { key: 'status', value: [300, 399], method: 'range' },
45
+ '4xx': { key: 'status', value: [400, 499], method: 'range' },
46
+ '5xx': { key: 'status', value: [500, 599], method: 'range' },
47
+ }