docs: 完成API文档编写(任务26)
创建完整的API文档: - 模板管理API(5个接口) * 获取模板列表 * 获取模板详情(含使用统计) * 创建模板(含验证规则) * 更新模板(含版本控制) * 删除模板(含使用检查) - 模板验证API * 内置模板验证逻辑 * 自定义模板验证逻辑 * 内置模板列表 - 字段池管理API(2个接口) * 获取字段池列表 * 同步字段池 - 分类字段管理API * 获取分类管理字段 - 错误代码规范 * 统一错误响应格式 * 常见错误代码说明 - 性能指标 * 响应时间目标 * 数据库优化说明(20+个索引) - 最佳实践 * 版本控制使用方法 * 错误处理示例 * 字段验证示例 * 性能优化建议 文档位置:docs/api/template-system-api.md
This commit is contained in:
parent
b7776d468a
commit
473176997a
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"name": "功能完成单元测试",
|
"name": "功能完成单元测试",
|
||||||
"description": "当完成一个功能或者修改一个功能完成时,自动执行单元测试并生成测试汇报",
|
"description": "当完成一个功能或者修改一个功能完成时,自动执行单元测试并生成测试汇报,用中文回复",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"when": {
|
"when": {
|
||||||
"type": "fileEdited",
|
"type": "fileEdited",
|
||||||
@ -17,5 +17,7 @@
|
|||||||
"then": {
|
"then": {
|
||||||
"type": "askAgent",
|
"type": "askAgent",
|
||||||
"prompt": "检测到代码文件已修改。请执行以下操作:\n\n1. 分析修改的文件和相关功能\n2. 识别需要测试的功能模块\n3. 运行相关的单元测试(如果存在)\n4. 如果没有单元测试,建议创建测试用例\n5. 生成测试汇报,包括:\n - 测试覆盖的功能点\n - 测试结果(通过/失败)\n - 代码覆盖率\n - 发现的问题和建议\n - 测试执行时间\n\n请使用 npm test 或 jest 命令执行测试,并提供详细的测试报告。"
|
"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"
|
||||||
}
|
}
|
||||||
@ -208,7 +208,9 @@
|
|||||||
- 测试字段池管理器(34个测试用例)
|
- 测试字段池管理器(34个测试用例)
|
||||||
- 测试选项清洗逻辑(已包含在字段池测试中)
|
- 测试选项清洗逻辑(已包含在字段池测试中)
|
||||||
- 测试模板CRUD API(13个测试用例)
|
- 测试模板CRUD API(13个测试用例)
|
||||||
- 总计72个测试用例全部通过
|
- 测试防抖Hooks(17个测试用例:9个useDebounce + 8个useDebouncedCallback)
|
||||||
|
- 总计89个测试用例全部通过
|
||||||
|
- 代码覆盖率: useDebounce.ts 100% statements, 66.66% branches, 100% functions, 100% lines ✅
|
||||||
- 目标覆盖率: >80% ✅
|
- 目标覆盖率: >80% ✅
|
||||||
|
|
||||||
- [ ] 25. 编写集成测试
|
- [ ] 25. 编写集成测试
|
||||||
@ -219,10 +221,13 @@
|
|||||||
|
|
||||||
## 阶段 8: 文档和部署
|
## 阶段 8: 文档和部署
|
||||||
|
|
||||||
- [ ] 26. 更新API文档
|
- [x] 26. 更新API文档
|
||||||
- 记录所有新增和修改的API
|
- 记录所有新增和修改的API(模板管理、字段池管理、分类字段管理)
|
||||||
- 添加请求/响应示例
|
- 添加请求/响应示例(包含完整的JSON示例)
|
||||||
- 记录错误代码和处理方式
|
- 记录错误代码和处理方式(统一错误格式)
|
||||||
|
- 添加性能指标和数据库优化说明
|
||||||
|
- 添加最佳实践指南
|
||||||
|
- 文档位置:`docs/api/template-system-api.md`
|
||||||
|
|
||||||
- [ ] 27. 编写用户手册
|
- [ ] 27. 编写用户手册
|
||||||
- 如何创建自定义模板
|
- 如何创建自定义模板
|
||||||
|
|||||||
574
docs/api/template-system-api.md
Normal file
574
docs/api/template-system-api.md
Normal file
@ -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 <div>{/* 渲染模板列表 */}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### 2024-12-13
|
||||||
|
- 添加数据库索引优化查询性能
|
||||||
|
- 添加模板使用统计API
|
||||||
|
- 添加字段填写率统计
|
||||||
|
- 优化错误响应格式
|
||||||
|
- 添加版本控制支持
|
||||||
|
|
||||||
|
### 2024-12-12
|
||||||
|
- 初始版本
|
||||||
|
- 实现模板CRUD API
|
||||||
|
- 实现字段池管理API
|
||||||
|
- 实现模板验证功能
|
||||||
@ -2,7 +2,7 @@
|
|||||||
* @jest-environment jsdom
|
* @jest-environment jsdom
|
||||||
*/
|
*/
|
||||||
import { renderHook, act } from '@testing-library/react'
|
import { renderHook, act } from '@testing-library/react'
|
||||||
import { useDebounce } from '../useDebounce'
|
import { useDebounce, useDebouncedCallback } from '../useDebounce'
|
||||||
|
|
||||||
describe('useDebounce', () => {
|
describe('useDebounce', () => {
|
||||||
beforeEach(() => {
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user