# Bug 修复报告:图标搜索 API 调用问题 **日期**: 2026-02-16 **严重程度**: 高(阻止功能使用) **状态**: ✅ 已修复 ## 问题描述 用户在前端调用图标搜索 API 时遇到 400 错误: ```json { "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "request": [ "The request field is required." ], "$.keywords": [ "The JSON value could not be converted to System.Collections.Generic.List`1[System.String]..." ] } } ``` ## 根本原因 在 `Web/src/views/ClassificationEdit.vue` 中,`searchIcons` API 调用传递了错误的参数类型。 ### 错误代码(第 377-387 行) ```javascript const { success: keywordsSuccess, data: keywords } = await generateSearchKeywords(category.name) if (!keywordsSuccess || !keywords || keywords.length === 0) { showToast('生成搜索关键字失败') return } // ❌ 错误:keywords 是 SearchKeywordsResponse 对象,不是数组 const { success: iconsSuccess, data: icons } = await searchIcons(keywords) ``` ### 问题分析 1. `generateSearchKeywords()` 返回的 `data` 是 `SearchKeywordsResponse` 对象: ```javascript { keywords: ["food", "restaurant", "dining"] } ``` 2. 代码错误地将整个对象传递给 `searchIcons()`: ```javascript // 实际发送的请求体 { keywords: { keywords: ["food", "restaurant"] } } ``` 3. 后端期望的格式: ```javascript { keywords: ["food", "restaurant"] // 数组,不是对象 } ``` ## 修复方案 ### 修复后的代码 ```javascript const { success: keywordsSuccess, data: keywordsResponse } = await generateSearchKeywords(category.name) if (!keywordsSuccess || !keywordsResponse || !keywordsResponse.keywords || keywordsResponse.keywords.length === 0) { showToast('生成搜索关键字失败') return } // ✅ 正确:提取 keywords 数组 const { success: iconsSuccess, data: icons } = await searchIcons(keywordsResponse.keywords) ``` ### 关键变更 1. 重命名变量:`data: keywords` → `data: keywordsResponse`(更清晰) 2. 访问嵌套属性:`keywordsResponse.keywords` 3. 更新验证逻辑:检查 `keywordsResponse.keywords` 是否存在 ## 影响范围 - **受影响文件**: `Web/src/views/ClassificationEdit.vue` - **受影响功能**: 分类图标选择功能 - **用户影响**: 无法为分类选择 Iconify 图标 ## 测试验证 ### 1. 单元测试 已有的 130 个测试用例验证后端 API 正确性: - ✅ IconController 集成测试通过 - ✅ Service 层单元测试通过 ### 2. 手动测试步骤 ```bash # 1. 启动后端 cd WebApi dotnet run # 2. 启动前端 cd Web pnpm dev # 3. 测试流程 # - 访问分类管理页面 # - 点击"选择图标"按钮 # - 验证图标选择器正常打开 # - 搜索并选择图标 # - 确认图标正确保存 ``` ### 3. API 测试脚本 参见 `.doc/test-icon-api.sh` 脚本: ```bash # 测试搜索图标 API curl -X POST http://localhost:5071/api/icons/search \ -H "Content-Type: application/json" \ -d '{"keywords": ["food", "restaurant"]}' # 预期响应 { "success": true, "data": [ { "collectionName": "mdi", "iconName": "food", "iconIdentifier": "mdi:food" }, ... ] } ``` ## 预防措施 ### 1. 类型安全改进 考虑将前端 API 客户端迁移到 TypeScript: ```typescript interface SearchKeywordsResponse { keywords: string[] } export const generateSearchKeywords = async (categoryName: string): Promise> => { // TypeScript 会在编译时捕获类型错误 } ``` ### 2. API 客户端注释改进 更新 `Web/src/api/icons.js` 的 JSDoc: ```javascript /** * 生成搜索关键字 * @param {string} categoryName - 分类名称 * @returns {Promise<{success: boolean, data: {keywords: string[]}}>} * 注意: data 是对象,包含 keywords 数组字段 */ ``` ### 3. 单元测试补充 为前端组件添加单元测试,验证 API 调用参数: ```javascript // ClassificationEdit.spec.js describe('ClassificationEdit - Icon Selection', () => { it('should pass keywords array to searchIcons', async () => { const mockKeywords = { keywords: ['food', 'restaurant'] } generateSearchKeywords.mockResolvedValue({ success: true, data: mockKeywords }) await openIconSelector(category) expect(searchIcons).toHaveBeenCalledWith(['food', 'restaurant']) }) }) ``` ## 相关文档 - **API 文档**: `.doc/ICONIFY_INTEGRATION.md` - **任务列表**: `openspec/changes/icon-search-integration/tasks.md` - **测试脚本**: `.doc/test-icon-api.sh` ## 经验教训 1. **响应结构验证**: 在使用 API 响应数据前,应验证数据结构 2. **变量命名清晰**: 使用清晰的变量名(如 `keywordsResponse` 而非 `keywords`) 3. **类型安全**: TypeScript 可以在编译时捕获此类错误 4. **测试覆盖**: 需要为前端组件添加集成测试 --- **修复者**: AI Assistant **审核者**: 待审核 **最后更新**: 2026-02-16