214 lines
5.1 KiB
Markdown
214 lines
5.1 KiB
Markdown
|
|
# 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<ApiResponse<SearchKeywordsResponse>> => {
|
|||
|
|
// 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
|