From 473176997aadc1d109f5b176115f064e76fa5d91 Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Sat, 13 Dec 2025 15:42:39 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AE=8C=E6=88=90API=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E7=BC=96=E5=86=99=EF=BC=88=E4=BB=BB=E5=8A=A126=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 创建完整的API文档: - 模板管理API(5个接口) * 获取模板列表 * 获取模板详情(含使用统计) * 创建模板(含验证规则) * 更新模板(含版本控制) * 删除模板(含使用检查) - 模板验证API * 内置模板验证逻辑 * 自定义模板验证逻辑 * 内置模板列表 - 字段池管理API(2个接口) * 获取字段池列表 * 同步字段池 - 分类字段管理API * 获取分类管理字段 - 错误代码规范 * 统一错误响应格式 * 常见错误代码说明 - 性能指标 * 响应时间目标 * 数据库优化说明(20+个索引) - 最佳实践 * 版本控制使用方法 * 错误处理示例 * 字段验证示例 * 性能优化建议 文档位置:docs/api/template-system-api.md --- .kiro/hooks/unit-test-on-completion.kiro.hook | 6 +- .../template-system-optimization/tasks.md | 15 +- docs/api/template-system-api.md | 574 ++++++++++++++++++ lib/hooks/__tests__/useDebounce.test.ts | 176 +++++- 4 files changed, 763 insertions(+), 8 deletions(-) create mode 100644 docs/api/template-system-api.md diff --git a/.kiro/hooks/unit-test-on-completion.kiro.hook b/.kiro/hooks/unit-test-on-completion.kiro.hook index d646afe..629dd47 100644 --- a/.kiro/hooks/unit-test-on-completion.kiro.hook +++ b/.kiro/hooks/unit-test-on-completion.kiro.hook @@ -1,7 +1,7 @@ { "enabled": true, "name": "功能完成单元测试", - "description": "当完成一个功能或者修改一个功能完成时,自动执行单元测试并生成测试汇报", + "description": "当完成一个功能或者修改一个功能完成时,自动执行单元测试并生成测试汇报,用中文回复", "version": "1", "when": { "type": "fileEdited", @@ -17,5 +17,7 @@ "then": { "type": "askAgent", "prompt": "检测到代码文件已修改。请执行以下操作:\n\n1. 分析修改的文件和相关功能\n2. 识别需要测试的功能模块\n3. 运行相关的单元测试(如果存在)\n4. 如果没有单元测试,建议创建测试用例\n5. 生成测试汇报,包括:\n - 测试覆盖的功能点\n - 测试结果(通过/失败)\n - 代码覆盖率\n - 发现的问题和建议\n - 测试执行时间\n\n请使用 npm test 或 jest 命令执行测试,并提供详细的测试报告。" - } + }, + "workspaceFolderName": "flnxi_sucai", + "shortName": "unit-test-on-completion" } \ No newline at end of file diff --git a/.kiro/specs/template-system-optimization/tasks.md b/.kiro/specs/template-system-optimization/tasks.md index 85efa3b..52c19ad 100644 --- a/.kiro/specs/template-system-optimization/tasks.md +++ b/.kiro/specs/template-system-optimization/tasks.md @@ -208,7 +208,9 @@ - 测试字段池管理器(34个测试用例) - 测试选项清洗逻辑(已包含在字段池测试中) - 测试模板CRUD API(13个测试用例) - - 总计72个测试用例全部通过 + - 测试防抖Hooks(17个测试用例:9个useDebounce + 8个useDebouncedCallback) + - 总计89个测试用例全部通过 + - 代码覆盖率: useDebounce.ts 100% statements, 66.66% branches, 100% functions, 100% lines ✅ - 目标覆盖率: >80% ✅ - [ ] 25. 编写集成测试 @@ -219,10 +221,13 @@ ## 阶段 8: 文档和部署 -- [ ] 26. 更新API文档 - - 记录所有新增和修改的API - - 添加请求/响应示例 - - 记录错误代码和处理方式 +- [x] 26. 更新API文档 + - 记录所有新增和修改的API(模板管理、字段池管理、分类字段管理) + - 添加请求/响应示例(包含完整的JSON示例) + - 记录错误代码和处理方式(统一错误格式) + - 添加性能指标和数据库优化说明 + - 添加最佳实践指南 + - 文档位置:`docs/api/template-system-api.md` - [ ] 27. 编写用户手册 - 如何创建自定义模板 diff --git a/docs/api/template-system-api.md b/docs/api/template-system-api.md new file mode 100644 index 0000000..b6c3606 --- /dev/null +++ b/docs/api/template-system-api.md @@ -0,0 +1,574 @@ +# 自定义上传模板系统 API 文档 + +## 概述 + +本文档描述了自定义上传模板系统的所有API接口,包括模板管理、字段管理、字段池管理等功能。 + +## 基础信息 + +- **Base URL**: `/api` +- **认证方式**: Session Cookie +- **内容类型**: `application/json` +- **字符编码**: UTF-8 + +## 错误代码 + +所有API遵循统一的错误响应格式: + +```json +{ + "error": "错误信息", + "errorCode": "ERROR_CODE", + "details": { + // 详细错误信息(可选) + } +} +``` + +### 常见错误代码 + +| 错误代码 | HTTP状态码 | 说明 | +|---------|-----------|------| +| `VALIDATION_ERROR` | 400 | 请求参数验证失败 | +| `UNAUTHORIZED` | 401 | 未授权访问 | +| `FORBIDDEN` | 403 | 权限不足 | +| `NOT_FOUND` | 404 | 资源不存在 | +| `CONFLICT` | 409 | 资源冲突(如版本冲突) | +| `INTERNAL_ERROR` | 500 | 服务器内部错误 | + +## 模板管理 API + +### 1. 获取模板列表 + +获取所有自定义上传模板列表。 + +**请求** + +```http +GET /api/admin/upload-templates +``` + +**响应** + +```json +[ + { + "id": "uuid", + "name": "3DMAX 素材模板", + "description": "适用于 3D 模型类素材", + "status": "active", + "materialType": "3DMAX", + "materialKind": "IMAGE", + "version": 1, + "fieldGroups": null, + "displayConfig": null, + "createdAt": "2024-01-01T00:00:00Z", + "updatedAt": "2024-01-01T00:00:00Z", + "fields": [ + { + "id": "uuid", + "key": "productModel", + "label": "SPU型号", + "type": "text", + "required": true, + "orderIndex": 0, + "layoutWidth": "full", + "validations": null, + "optionsStatic": null, + "allowedExtensions": null, + "visibleOnHomeFilter": false, + "isSearchable": true, + "isReadOnly": false, + "addToCategory": false, + "conditions": null + } + ] + } +] +``` + +**字段说明** + +- `status`: 模板状态 - `draft`(草稿)、`active`(启用)、`archived`(归档) +- `materialKind`: 素材大类 - `IMAGE`、`VIDEO`、`OTHER` +- `fields`: 模板字段数组,按 `orderIndex` 排序 + +### 2. 获取单个模板详情 + +获取指定模板的详细信息,包括使用统计。 + +**请求** + +```http +GET /api/admin/upload-templates/:id +``` + +**响应** + +```json +{ + "id": "uuid", + "name": "3DMAX 素材模板", + "description": "适用于 3D 模型类素材", + "status": "active", + "materialType": "3DMAX", + "materialKind": "IMAGE", + "version": 1, + "fieldGroups": null, + "displayConfig": null, + "createdAt": "2024-01-01T00:00:00Z", + "updatedAt": "2024-01-01T00:00:00Z", + "fields": [...], + "materialCount": 150, + "materials": [ + { + "id": "uuid", + "previewUrl": "https://...", + "productModel": "ABC-123" + } + ], + "fieldStats": [ + { + "key": "productModel", + "label": "SPU型号", + "filledCount": 150, + "fillRate": 100, + "isLowFillRate": false + } + ] +} +``` + +**字段说明** + +- `materialCount`: 使用该模板的素材数量 +- `materials`: 使用该模板的素材列表(最多10个预览) +- `fieldStats`: 字段填写率统计 + - `fillRate`: 填写率百分比(0-100) + - `isLowFillRate`: 是否为低填写率字段(<30%) + +### 3. 创建模板 + +创建新的自定义上传模板。 + +**请求** + +```http +POST /api/admin/upload-templates +Content-Type: application/json + +{ + "name": "新模板", + "description": "模板描述", + "status": "draft", + "materialType": "3DMAX", + "materialKind": "IMAGE", + "fieldGroups": null, + "displayConfig": null, + "fields": [ + { + "key": "productModel", + "label": "SPU型号", + "type": "text", + "required": true, + "orderIndex": 0, + "layoutWidth": "full", + "validations": null, + "optionsStatic": null, + "allowedExtensions": null, + "visibleOnHomeFilter": false, + "isSearchable": true, + "isReadOnly": false, + "addToCategory": false, + "conditions": null + } + ] +} +``` + +**验证规则** + +- `name`: 必填,不能为空 +- `materialType`: 必填,有效值:`3DMAX`、`PSD`、`AI 工作流`、`视频`、`模特`、`其他` +- `materialKind`: 必填,有效值:`IMAGE`、`VIDEO`、`OTHER` +- `fields`: 必填,至少包含一个字段 +- `fields[].key`: 必填,只能包含字母、数字、下划线,必须以字母开头,长度不超过50 +- `fields[].key`: 在同一模板内必须唯一 +- `fields[].type`: 必填,有效值:`text`、`textarea`、`number`、`select`、`checkbox`、`radio`、`date`、`uppercase` + +**响应** + +```json +{ + "id": "uuid", + "name": "新模板", + "version": 1, + ... +} +``` + +**错误响应示例** + +```json +{ + "error": "字段键名重复:字段 2 已使用键名 productModel", + "errorCode": "VALIDATION_ERROR" +} +``` + +### 4. 更新模板 + +更新现有模板,支持版本控制。 + +**请求** + +```http +PUT /api/admin/upload-templates/:id +Content-Type: application/json + +{ + "name": "更新后的模板名称", + "description": "更新后的描述", + "status": "active", + "materialType": "3DMAX", + "materialKind": "IMAGE", + "version": 1, + "fieldGroups": null, + "displayConfig": null, + "fields": [...] +} +``` + +**版本控制** + +- 请求必须包含 `version` 字段 +- 如果服务器上的版本号与请求中的版本号不匹配,返回 409 冲突错误 +- 更新成功后,版本号自动递增 + +**响应** + +```json +{ + "id": "uuid", + "name": "更新后的模板名称", + "version": 2, + ... +} +``` + +**版本冲突错误** + +```json +{ + "error": "模板版本冲突,请刷新后重试", + "errorCode": "VERSION_CONFLICT", + "details": { + "clientVersion": 1, + "serverVersion": 2, + "lastUpdated": "2024-01-01T00:00:00Z" + } +} +``` + +### 5. 删除模板 + +删除指定模板。如果模板正在被使用,会返回警告信息。 + +**请求** + +```http +DELETE /api/admin/upload-templates/:id +``` + +**响应** + +```json +{ + "success": true, + "message": "模板已删除" +} +``` + +**使用中的模板错误** + +```json +{ + "error": "该模板正在被 150 个素材使用,无法删除", + "errorCode": "TEMPLATE_IN_USE", + "details": { + "materialCount": 150 + } +} +``` + +## 模板验证 API + +### 验证模板ID + +验证模板ID是否有效(内置模板或自定义模板)。 + +**内部使用** + +此功能在素材上传API中自动调用,不需要单独调用。 + +**验证逻辑** + +1. 如果 `templateId` 以 `builtin-` 开头,验证是否为有效的内置模板 +2. 否则,从数据库查询验证模板是否存在 +3. 如果 `templateId` 为空或 null,允许通过(向后兼容) + +**内置模板列表** + +- `builtin-3dmax-material`: 3DMAX 素材模板 +- `builtin-psd-material`: PSD 素材模板 +- `builtin-ai-material`: AI 工作流模板 +- `builtin-video-material`: 视频素材模板 +- `builtin-model-material`: 模特素材模板 +- `builtin-other-material`: 其他素材模板 + +## 字段池管理 API + +### 1. 获取字段池列表 + +获取所有字段定义及其选项。 + +**请求** + +```http +GET /api/field-pool +``` + +**响应** + +```json +[ + { + "id": "uuid", + "key": "categoryName", + "label": "使用店铺", + "type": "select", + "description": "素材使用的店铺分类", + "source": "custom", + "sourceTemplateIds": ["uuid1", "uuid2"], + "isStandard": false, + "isFilterEnabled": true, + "filterDisplayName": "店铺", + "filterOrder": 1, + "createdAt": "2024-01-01T00:00:00Z", + "updatedAt": "2024-01-01T00:00:00Z", + "options": [ + { + "id": "uuid", + "value": "默认店铺", + "label": "默认店铺", + "orderIndex": 0 + } + ] + } +] +``` + +**字段说明** + +- `source`: 字段来源 - `standard`(标准字段)、`custom`(自定义字段) +- `isFilterEnabled`: 是否启用为筛选字段 +- `filterDisplayName`: 筛选时显示的名称(优先于 `label`) +- `filterOrder`: 筛选字段的排序顺序 + +### 2. 同步字段池 + +从模板同步字段到字段池(管理员操作)。 + +**请求** + +```http +POST /api/admin/field-pool/sync +``` + +**响应** + +```json +{ + "success": true, + "syncedFields": 5, + "syncedOptions": 25, + "message": "字段池同步成功" +} +``` + +## 分类字段管理 API + +### 获取分类管理字段 + +获取启用了分类管理的字段列表。 + +**请求** + +```http +GET /api/category-fields +``` + +**响应** + +```json +{ + "managedFields": [ + "categoryName", + "sceneName", + "renderer", + "brandName", + "seriesName", + "materialTexture" + ] +} +``` + +## 性能指标 + +### 响应时间目标 + +| API | 目标响应时间 | 说明 | +|-----|------------|------| +| 获取模板列表 | < 200ms | 已添加数据库索引优化 | +| 获取模板详情 | < 300ms | 包含统计信息 | +| 创建模板 | < 500ms | 包含字段验证和同步 | +| 更新模板 | < 500ms | 包含版本检查和同步 | +| 删除模板 | < 200ms | 包含使用检查 | +| 字段池同步 | < 2s | 批量操作,支持重试 | + +### 数据库优化 + +已添加以下索引以优化查询性能: + +**UploadTemplate 表** +- `status` - 按状态筛选 +- `materialType` - 按素材类型筛选 +- `materialKind` - 按素材大类筛选 +- `createdAt` / `updatedAt` - 按时间排序 + +**UploadField 表** +- `templateId` - 查询模板字段 +- `key` - 按字段键名查询 +- `type` - 按字段类型筛选 +- `visibleOnHomeFilter` - 查询首页筛选字段 +- `isSearchable` - 查询可搜索字段 +- `addToCategory` - 查询分类管理字段 +- `templateId, orderIndex` - 复合索引优化排序 + +**FieldDefinition 表** +- `source` - 按来源筛选 +- `isStandard` - 查询标准字段 +- `isFilterEnabled` - 查询启用的筛选字段 +- `isFilterEnabled, filterOrder` - 复合索引优化筛选查询 + +**FieldOption 表** +- `fieldId` - 查询字段选项 +- `fieldId, orderIndex` - 复合索引优化选项排序 + +## 最佳实践 + +### 1. 版本控制 + +更新模板时始终包含版本号,避免并发冲突: + +```javascript +// 获取模板 +const template = await fetch(`/api/admin/upload-templates/${id}`) +const data = await template.json() + +// 更新时包含版本号 +await fetch(`/api/admin/upload-templates/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...data, + name: '新名称', + version: data.version // 必须包含 + }) +}) +``` + +### 2. 错误处理 + +始终处理API错误并向用户显示友好的错误信息: + +```javascript +try { + const response = await fetch('/api/admin/upload-templates', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(templateData) + }) + + if (!response.ok) { + const error = await response.json() + // 显示错误信息 + showError(error.error || '操作失败') + return + } + + const result = await response.json() + // 处理成功结果 +} catch (error) { + // 处理网络错误 + showError('网络错误,请重试') +} +``` + +### 3. 字段验证 + +创建或更新模板前,在前端进行字段验证: + +```javascript +// 验证字段key格式 +const keyPattern = /^[a-zA-Z][a-zA-Z0-9_]*$/ +if (!keyPattern.test(field.key)) { + showError('字段键名必须以字母开头,只能包含字母、数字和下划线') + return +} + +// 验证字段key唯一性 +const keys = fields.map(f => f.key) +const duplicates = keys.filter((key, index) => keys.indexOf(key) !== index) +if (duplicates.length > 0) { + showError(`字段键名重复:${duplicates[0]}`) + return +} +``` + +### 4. 性能优化 + +使用SWR或类似的缓存库减少不必要的API请求: + +```javascript +import useSWR from 'swr' + +function TemplateList() { + const { data, error, mutate } = useSWR( + '/api/admin/upload-templates', + fetcher, + { + revalidateOnFocus: false, + dedupingInterval: 5000 + } + ) + + // 使用缓存的数据 + return
{/* 渲染模板列表 */}
+} +``` + +## 更新日志 + +### 2024-12-13 +- 添加数据库索引优化查询性能 +- 添加模板使用统计API +- 添加字段填写率统计 +- 优化错误响应格式 +- 添加版本控制支持 + +### 2024-12-12 +- 初始版本 +- 实现模板CRUD API +- 实现字段池管理API +- 实现模板验证功能 diff --git a/lib/hooks/__tests__/useDebounce.test.ts b/lib/hooks/__tests__/useDebounce.test.ts index 535af2c..26b9065 100644 --- a/lib/hooks/__tests__/useDebounce.test.ts +++ b/lib/hooks/__tests__/useDebounce.test.ts @@ -2,7 +2,7 @@ * @jest-environment jsdom */ import { renderHook, act } from '@testing-library/react' -import { useDebounce } from '../useDebounce' +import { useDebounce, useDebouncedCallback } from '../useDebounce' describe('useDebounce', () => { beforeEach(() => { @@ -160,3 +160,177 @@ describe('useDebounce', () => { }) }) }) + +describe('useDebouncedCallback', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.runOnlyPendingTimers() + jest.useRealTimers() + }) + + describe('基本功能', () => { + it('应该返回一个函数', () => { + const callback = jest.fn() + const { result } = renderHook(() => useDebouncedCallback(callback, 300)) + + expect(typeof result.current).toBe('function') + }) + + it('应该在延迟后调用回调函数', () => { + const callback = jest.fn() + const { result } = renderHook(() => useDebouncedCallback(callback, 300)) + + act(() => { + result.current('test') + }) + + expect(callback).not.toHaveBeenCalled() + + act(() => { + jest.advanceTimersByTime(300) + }) + + expect(callback).toHaveBeenCalledWith('test') + expect(callback).toHaveBeenCalledTimes(1) + }) + + it('应该在快速调用时只执行最后一次', () => { + const callback = jest.fn() + const { result } = renderHook(() => useDebouncedCallback(callback, 300)) + + // 分别调用,每次调用都会重置定时器 + act(() => { + result.current('call1') + }) + + act(() => { + result.current('call2') + }) + + act(() => { + result.current('call3') + }) + + expect(callback).not.toHaveBeenCalled() + + act(() => { + jest.advanceTimersByTime(300) + }) + + expect(callback).toHaveBeenCalledWith('call3') + expect(callback).toHaveBeenCalledTimes(1) + }) + }) + + describe('参数传递', () => { + it('应该正确传递单个参数', () => { + const callback = jest.fn() + const { result } = renderHook(() => useDebouncedCallback(callback, 300)) + + act(() => { + result.current('test') + }) + + act(() => { + jest.advanceTimersByTime(300) + }) + + expect(callback).toHaveBeenCalledWith('test') + }) + + it('应该正确传递多个参数', () => { + const callback = jest.fn() + const { result } = renderHook(() => useDebouncedCallback(callback, 300)) + + act(() => { + result.current('arg1', 'arg2', 'arg3') + }) + + act(() => { + jest.advanceTimersByTime(300) + }) + + expect(callback).toHaveBeenCalledWith('arg1', 'arg2', 'arg3') + }) + + it('应该正确传递对象参数', () => { + const callback = jest.fn() + const { result } = renderHook(() => useDebouncedCallback(callback, 300)) + + const obj = { name: 'test', value: 42 } + act(() => { + result.current(obj) + }) + + act(() => { + jest.advanceTimersByTime(300) + }) + + expect(callback).toHaveBeenCalledWith(obj) + }) + }) + + describe('延迟时间配置', () => { + it('应该支持自定义延迟时间', () => { + const callback = jest.fn() + const { result } = renderHook(() => useDebouncedCallback(callback, 500)) + + act(() => { + result.current('test') + }) + + // 300ms后还未调用 + act(() => { + jest.advanceTimersByTime(300) + }) + expect(callback).not.toHaveBeenCalled() + + // 再过200ms(总共500ms)后调用 + act(() => { + jest.advanceTimersByTime(200) + }) + expect(callback).toHaveBeenCalledWith('test') + }) + }) + + describe('定时器清理', () => { + it('应该在新调用时清除之前的定时器', () => { + const callback = jest.fn() + const { result } = renderHook(() => useDebouncedCallback(callback, 300)) + + act(() => { + result.current('call1') + }) + + // 快进150ms + act(() => { + jest.advanceTimersByTime(150) + }) + + // 再次调用,应该清除之前的定时器 + act(() => { + result.current('call2') + }) + + // 再快进150ms(距离第一次调用已经300ms,但距离第二次调用只有150ms) + act(() => { + jest.advanceTimersByTime(150) + }) + + // 此时callback还不应该被调用 + expect(callback).not.toHaveBeenCalled() + + // 再快进150ms(距离第二次调用已经300ms) + act(() => { + jest.advanceTimersByTime(150) + }) + + // 现在应该只调用一次,参数是call2 + expect(callback).toHaveBeenCalledWith('call2') + expect(callback).toHaveBeenCalledTimes(1) + }) + }) +})