From 382e3aff21a3463810f03a95bfc6136d8ba7414b Mon Sep 17 00:00:00 2001 From: tobegold574 <2386340403@qq.com> Date: Sun, 30 Nov 2025 20:27:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(reset):=20=E4=BB=A5=E6=9E=84=E9=80=A0?= =?UTF-8?q?=E5=99=A8=E6=A8=A1=E5=BC=8F=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 加了大文件传输自定义分片协议 BREAKING CHANGES: 0.1.0(latest) --- FOLDER_STRUCTURE.md | 142 ------ README.md | 177 +++---- api/README.md | 57 --- api/chat-api.ts | 92 ++++ api/client.ts | 287 ----------- api/common.ts | 49 ++ api/errors.ts | 46 -- api/factory.ts | 41 -- api/index.ts | 59 ++- api/model-api.ts | 77 +++ api/modules/chat.ts | 48 -- api/modules/index.ts | 14 - api/modules/model.ts | 29 -- api/modules/post.ts | 79 --- api/modules/user.ts | 58 --- api/post-api.ts | 122 +++++ api/types.ts | 120 ----- api/upload-api.ts | 132 +++++ api/user-api.ts | 150 ++++++ auth/README.md | 217 --------- auth/auth-service.ts | 170 ------- auth/errors.ts | 68 --- auth/event-manager.ts | 66 --- auth/index.ts | 22 - auth/session-manager.ts | 93 ---- auth/storage-adapter.ts | 55 --- auth/types.ts | 85 ---- client/axios-client.ts | 62 +++ client/common.ts | 40 ++ client/index.ts | 2 + decorators/common.ts | 128 +++++ decorators/http-methods.ts | 41 ++ decorators/index.ts | 2 + decorators/meta.ts | 35 ++ index.ts | 3 - package.json | 10 +- test/README.md | 77 --- test/auth/session-manager.test.ts | 149 ------ test/mocks/data-factory.ts | 702 --------------------------- test/mocks/http-client.ts | 74 --- test/mocks/index.ts | 7 - test/mocks/storage.ts | 23 - test/setup.ts | 53 -- test/unit/api/client.test.ts | 464 ------------------ test/unit/api/modules/chat.test.ts | 345 ------------- test/unit/api/modules/model.test.ts | 239 --------- test/unit/api/modules/post.test.ts | 408 ---------------- test/unit/api/modules/user.test.ts | 334 ------------- test/unit/auth/auth-service.test.ts | 379 --------------- test/unit/auth/event-manager.test.ts | 174 ------- test/unit/utils/data.test.ts | 482 ------------------ test/unit/utils/date.test.ts | 157 ------ test/unit/utils/string.test.ts | 183 ------- test/unit/utils/validation.test.ts | 224 --------- test/utils/test-helpers.ts | 159 ------ types/chat/api.ts | 86 +--- types/chat/base.ts | 54 +-- types/chat/enum.ts | 19 - types/chat/index.ts | 2 +- types/chat/types.ts | 4 + types/common.ts | 26 + types/index.ts | 2 + types/model/api.ts | 136 +----- types/model/base.ts | 23 +- types/model/enum.ts | 7 - types/model/index.ts | 2 +- types/model/types.ts | 2 + types/post/api.ts | 101 ++-- types/post/base.ts | 50 +- types/post/enum.ts | 29 -- types/post/index.ts | 2 +- types/post/types.ts | 2 + types/upload/api.ts | 77 +++ types/upload/base.ts | 63 +++ types/upload/index.ts | 3 + types/user/api.ts | 105 +++- types/user/base.ts | 47 +- types/user/enum.ts | 8 - types/user/index.ts | 5 +- types/user/profile.ts | 38 -- types/user/search.ts | 13 - utils/data.ts | 14 +- 82 files changed, 1421 insertions(+), 7010 deletions(-) delete mode 100644 FOLDER_STRUCTURE.md delete mode 100644 api/README.md create mode 100644 api/chat-api.ts delete mode 100644 api/client.ts create mode 100644 api/common.ts delete mode 100644 api/errors.ts delete mode 100644 api/factory.ts create mode 100644 api/model-api.ts delete mode 100644 api/modules/chat.ts delete mode 100644 api/modules/index.ts delete mode 100644 api/modules/model.ts delete mode 100644 api/modules/post.ts delete mode 100644 api/modules/user.ts create mode 100644 api/post-api.ts delete mode 100644 api/types.ts create mode 100644 api/upload-api.ts create mode 100644 api/user-api.ts delete mode 100644 auth/README.md delete mode 100644 auth/auth-service.ts delete mode 100644 auth/errors.ts delete mode 100644 auth/event-manager.ts delete mode 100644 auth/index.ts delete mode 100644 auth/session-manager.ts delete mode 100644 auth/storage-adapter.ts delete mode 100644 auth/types.ts create mode 100644 client/axios-client.ts create mode 100644 client/common.ts create mode 100644 client/index.ts create mode 100644 decorators/common.ts create mode 100644 decorators/http-methods.ts create mode 100644 decorators/index.ts create mode 100644 decorators/meta.ts delete mode 100644 test/README.md delete mode 100644 test/auth/session-manager.test.ts delete mode 100644 test/mocks/data-factory.ts delete mode 100644 test/mocks/http-client.ts delete mode 100644 test/mocks/index.ts delete mode 100644 test/mocks/storage.ts delete mode 100644 test/setup.ts delete mode 100644 test/unit/api/client.test.ts delete mode 100644 test/unit/api/modules/chat.test.ts delete mode 100644 test/unit/api/modules/model.test.ts delete mode 100644 test/unit/api/modules/post.test.ts delete mode 100644 test/unit/api/modules/user.test.ts delete mode 100644 test/unit/auth/auth-service.test.ts delete mode 100644 test/unit/auth/event-manager.test.ts delete mode 100644 test/unit/utils/data.test.ts delete mode 100644 test/unit/utils/date.test.ts delete mode 100644 test/unit/utils/string.test.ts delete mode 100644 test/unit/utils/validation.test.ts delete mode 100644 test/utils/test-helpers.ts delete mode 100644 types/chat/enum.ts create mode 100644 types/chat/types.ts create mode 100644 types/common.ts delete mode 100644 types/model/enum.ts create mode 100644 types/model/types.ts delete mode 100644 types/post/enum.ts create mode 100644 types/post/types.ts create mode 100644 types/upload/api.ts create mode 100644 types/upload/base.ts create mode 100644 types/upload/index.ts delete mode 100644 types/user/enum.ts delete mode 100644 types/user/profile.ts delete mode 100644 types/user/search.ts diff --git a/FOLDER_STRUCTURE.md b/FOLDER_STRUCTURE.md deleted file mode 100644 index fb42726..0000000 --- a/FOLDER_STRUCTURE.md +++ /dev/null @@ -1,142 +0,0 @@ - . - ├─ Dockerfile - ├─ FOLDER_STRUCTURE.md - ├─ README.md - ├─ api - │   ├─ README.md - │   ├─ client.ts - │   ├─ errors.ts - │   ├─ factory.ts - │   ├─ index.ts - │   ├─ modules - │   │   ├─ chat.ts - │   │   ├─ index.ts - │   │   ├─ model.ts - │   │   ├─ post.ts - │   │   └─ user.ts - │   └─ types.d.ts - ├─ auth - │   ├─ README.md - │   ├─ auth-service.ts - │   ├─ errors.ts - │   ├─ event-manager.ts - │   ├─ index.ts - │   ├─ session-manager.ts - │   ├─ storage-adapter.ts - │   └─ types.d.ts - ├─ coverage - │   ├─ api - │   │   ├─ client.ts.html - │   │   ├─ errors.ts.html - │   │   ├─ index.html - │   │   └─ modules - │   │   ├─ chat.ts.html - │   │   ├─ index.html - │   │   ├─ model.ts.html - │   │   ├─ post.ts.html - │   │   └─ user.ts.html - │   ├─ auth - │   │   ├─ auth-service.ts.html - │   │   ├─ errors.ts.html - │   │   ├─ event-manager.ts.html - │   │   ├─ index.html - │   │   ├─ session-manager.ts.html - │   │   └─ storage-adapter.ts.html - │   ├─ base.css - │   ├─ block-navigation.js - │   ├─ coverage-final.json - │   ├─ favicon.png - │   ├─ index.html - │   ├─ prettify.css - │   ├─ prettify.js - │   ├─ sort-arrow-sprite.png - │   ├─ sorter.js - │   └─ utils - │   ├─ data.ts.html - │   ├─ date.ts.html - │   ├─ index.html - │   ├─ string.ts.html - │   └─ validation.ts.html - ├─ eslint.config.js - ├─ index.ts - ├─ node_modules - │   ├─ @eslint - │   │   └─ js - │   ├─ @types - │   │   ├─ lodash - │   │   └─ node - │   ├─ @typescript-eslint - │   │   ├─ eslint-plugin - │   │   └─ parser - │   ├─ @vitest - │   │   └─ coverage-v8 - │   ├─ axios - │   ├─ eslint - │   ├─ typescript - │   └─ vitest - ├─ package.json - ├─ pnpm-lock.yaml - ├─ test - │   ├─ README.md - │   ├─ auth - │   │   └─ session-manager.test.ts - │   ├─ integration - │   ├─ mocks - │   │   ├─ data-factory.ts - │   │   ├─ http-client.ts - │   │   ├─ index.ts - │   │   └─ storage.ts - │   ├─ setup.ts - │   ├─ unit - │   │   ├─ api - │   │   │   ├─ client.test.ts - │   │   │   └─ modules - │   │   │   ├─ chat.test.ts - │   │   │   ├─ model.test.ts - │   │   │   ├─ post.test.ts - │   │   │   └─ user.test.ts - │   │   ├─ auth - │   │   │   ├─ auth-service.test.ts - │   │   │   └─ event-manager.test.ts - │   │   └─ utils - │   │   ├─ data.test.ts - │   │   ├─ date.test.ts - │   │   ├─ string.test.ts - │   │   └─ validation.test.ts - │   └─ utils - │   └─ test-helpers.ts - ├─ tsconfig.json - ├─ types - │   ├─ README.md - │   ├─ chat - │   │   ├─ api.d.ts - │   │   ├─ base.d.ts - │   │   ├─ enum.d.ts - │   │   └─ index.d.ts - │   ├─ index.d.ts - │   ├─ model - │   │   ├─ api.d.ts - │   │   ├─ base.d.ts - │   │   ├─ enum.d.ts - │   │   └─ index.d.ts - │   ├─ post - │   │   ├─ api.d.ts - │   │   ├─ base.d.ts - │   │   ├─ enum.d.ts - │   │   └─ index.d.ts - │   └─ user - │   ├─ base.d.ts - │   ├─ enum.d.ts - │   ├─ index.d.ts - │   ├─ profile.d.ts - │   └─ search.d.ts - ├─ utils - │   ├─ README.md - │   ├─ data.ts - │   ├─ date.ts - │   ├─ index.ts - │   ├─ string.ts - │   └─ validation.ts - └─ vitest.config.ts - - 39 directories, 100 files diff --git a/README.md b/README.md index 1ae312f..390a265 100644 --- a/README.md +++ b/README.md @@ -1,139 +1,78 @@ # KnowAI Core -## 概述 +KnowAI Core 是前端核心库,提供完整的API服务、HTTP客户端、装饰器系统和类型定义,专注于后端通信和数据处理。 -KnowAI Core 是前端核心库,提供业务逻辑、数据处理和认证功能,严格遵循分层架构原则,专注于核心功能实现,不包含UI交互逻辑。 +## 核心功能 -## 架构设计 +### API 服务 +- **装饰器驱动**:基于TS5+装饰器实现的声明式API调用 +- **模块化设计**:按功能域划分API服务(用户、帖子、聊天、模型、上传) +- **统一错误处理**:标准化的错误响应和类型定义 +- **路径参数支持**:内置支持`:param`格式的动态路径参数 -### 分层架构 +### 客户端实现 +- **基于Axios**:高性能HTTP客户端封装 +- **统一请求配置**:标准化的请求/响应接口 +- **错误转换**:自动将Axios错误转换为应用错误类型 +- **类型安全**:完整的TypeScript泛型支持 -- **Core层**:提供核心业务逻辑、数据处理和状态管理 +### 装饰器系统 +- **HTTP方法装饰器**:支持GET、POST、PUT、DELETE、PATCH +- **请求执行装饰器**:自动处理API调用逻辑 +- **客户端注入装饰器**:自动注入HTTP客户端实例 +- **TS5+ Stage 3**:遵循最新的装饰器规范 -### 事件驱动设计 - -为了实现层间解耦,认证模块采用事件驱动架构: -- Core层定义事件类型和事件触发机制 -- 上层应用注册事件监听器,处理UI交互逻辑 -- 避免Core层直接依赖UI层或路由系统 - -## 核心模块 - -### API 模块 - -API模块负责处理所有与后端通信相关的逻辑,提供统一的HTTP请求接口和模块化的API服务: - -#### 核心功能 -- **请求工厂**:使用工厂模式创建API实例,统一配置请求参数 -- **拦截器系统**:支持请求/响应拦截器,实现统一的错误处理、日志记录和认证 -- **模块化API服务**:按功能域划分API服务,如用户API、帖子API等 -- **响应标准化**:统一处理API响应格式,提供一致的错误处理机制 -- **内容发现**:提供热门帖子、帖子榜单和热门作者功能,支持多种排序和统计周期 - -#### 新增功能 -##### 帖子相关 -- **热门帖子**:通过`getHotPosts()`获取指定时间内的热门帖子 -- **帖子榜单**:通过`getPostRanking()`获取不同周期(日/周/月)的帖子排行榜,支持按浏览量、点赞数、评论数排序 - -##### 用户相关 -- **热门作者**:通过`getHotAuthors()`获取指定时间内的热门作者 -- **作者榜单**:通过`getAuthorRanking()`获取不同周期(日/周/月)的作者排行榜,支持按发帖量、浏览量、点赞数排序 - -#### 架构特点 -- 基于axios构建,支持请求/响应转换 -- 自动处理认证令牌附加 -- 统一的错误处理和重试机制 -- 支持请求取消和超时控制 - -### Auth 模块 - -Auth模块提供完整的认证功能,管理用户身份验证和授权状态: - -#### 核心功能 -- **令牌管理**:管理访问令牌和刷新令牌,实现自动令牌刷新 -- **认证状态管理**:维护用户登录状态,提供状态查询接口 -- **事件驱动通知**:通过事件系统通知认证状态变化,如登录、登出、令牌过期等 -- **权限控制**:提供基于角色的权限检查功能 - -#### 架构特点 -- 事件驱动的状态通知机制,实现与UI层的解耦 -- 自动处理令牌过期和刷新流程 -- 支持多种认证方式(令牌、OAuth等) -- 安全的令牌存储机制 - -### Utils 模块 - -Utils模块提供常用的工具函数,支持数据处理和业务逻辑实现: - -#### 核心功能 -- **数据处理工具**:提供数据转换、过滤、排序等常用数据处理函数 -- **字符串处理工具**:提供字符串格式化、验证、转换等工具函数 -- **日期处理工具**:提供日期格式化、计算、比较等日期相关函数 -- **验证工具**:提供表单验证、数据格式验证等验证函数 - -#### 架构特点 -- 纯函数设计,无副作用,易于测试 -- 函数式编程风格,支持链式调用 -- 完整的TypeScript类型支持 -- 模块化设计,按需导入 - -### Types 模块 - -Types模块提供TypeScript类型定义,确保整个应用的类型安全: - -#### 核心功能 -- **API相关类型**:定义请求参数、响应数据等API相关类型 -- **业务实体类型**:定义业务领域中的实体类型,如用户、内容等 -- **通用类型定义**:提供通用的工具类型和类型别名 -- **事件类型定义**:定义事件系统中使用的事件类型 - -#### 架构特点 -- 严格的类型定义,提供编译时类型检查 -- 支持泛型和高级类型特性 -- 类型文档完善,提高开发体验 -- 与业务逻辑紧密关联,确保类型一致性 +### 类型系统 +- **完整类型定义**:覆盖所有API请求/响应类型 +- **业务实体类型**:用户、帖子、聊天、模型等核心实体类型 +- **模块化组织**:按功能域分类管理类型定义 ## 目录结构 ``` knowai-core/ -├── api/ # API模块 -├── auth/ # 认证模块 -├── utils/ # 工具函数模块 -├── types/ # 类型定义模块 -├── test/ # 测试文件 +├── api/ # API服务模块 +├── client/ # HTTP客户端实现 +├── decorators/ # 装饰器系统 +├── types/ # TypeScript类型定义 +├── utils/ # 工具函数 ├── index.ts # 主入口文件 ├── package.json # 项目配置 -├── tsconfig.json # TypeScript配置 -└── vitest.config.ts # 测试配置 +└── tsconfig.json # TypeScript配置 +``` + +## 使用方式 + +### 安装 + +```bash +# 使用 npm +npm install @knowai/core + +# 使用 pnpm +pnpm add @knowai/core +``` + +### 导入API服务 + +```typescript +import { api } from '@knowai/core'; + +// 调用API方法 +const posts = await api.post.getPosts({ page: 1, pageSize: 10 }); ``` ## 设计原则 -1. **关注点分离**:业务逻辑与UI逻辑分离,Core层专注于核心功能实现 -2. **事件驱动**:通过事件系统实现层间解耦,降低模块间耦合度 -3. **类型安全**:充分利用TypeScript类型系统,提供编译时类型检查 -4. **模块化**:按功能域组织,按需导入,提高代码复用性 -5. **可测试性**:纯函数和依赖注入提高可测试性,确保代码质量 +- **声明式API**:使用装饰器提供简洁的API定义方式 +- **类型安全**:全面利用TypeScript确保代码质量 +- **模块化**:高内聚低耦合的模块化设计 +- **可扩展性**:易于添加新的API服务和功能 +- **错误统一**:标准化的错误处理机制 -## 更新记录 +## 更新日志 -### 2025-11-10 -- 完成tsc eslint vitest测试,构建1.0.0镜像并推送 - -### 2025-11-11 -- 因服务器性能原因去除CI pipeline -- 重新整理所有逻辑模块架构,完成对应README撰写 - -### 2025-11-12 -- 实现帖子相关新功能:热门帖子、帖子榜单 -- 实现用户相关新功能:热门作者、作者榜单 -- 更新README文档,添加新功能说明 - -### 2025-11-18 -- 添加热门帖子、榜单、热门作者接口 -- 完成api-documentation.md文档,详细描述所有接口的功能、参数、响应格式等 -- 修复类型未导出问题 - -### 2025-11-19 -- 把所有.d.ts改成.ts,避免无js生成问题 \ No newline at end of file +### 2025-11-30 +- 用装饰器全部重构了一遍,迭代版本为0.1.0 +- 减少了百分之六十到八十的代码量 +- 先发布0.1.0版本 diff --git a/api/README.md b/api/README.md deleted file mode 100644 index ffea02b..0000000 --- a/api/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# API 模块 - -## 概述 - -API 模块提供统一的HTTP客户端和API请求处理功能,支持模块化的API端点管理。 - -## 核心组件 - -1. **API 客户端 (client.ts)** - - 基于Axios的HTTP客户端 - - 提供统一的请求和响应处理 - - 支持请求拦截器和响应拦截器 - -2. **API 工厂 (factory.ts)** - - 创建API客户端实例 - - 配置默认请求选项 - - 提供单例模式确保全局一致性 - -3. **API 模块 (modules/)** - - 按功能模块组织API端点 - - 提供类型安全的API方法 - - 包含用户、聊天等业务模块 - -4. **类型定义 (types.d.ts)** - - 定义API相关的接口和类型 - - 包含请求配置和响应格式 - -5. **错误处理 (errors.ts)** - - 定义API相关的错误类 - - 提供错误处理和转换功能 - -## 使用方法 - -```typescript -import { createApiClient } from './api'; - -// 创建API客户端 -const apiClient = createApiClient({ - baseURL: 'https://api.example.com', - timeout: 10000, -}); - -// 使用API模块 -import { userApi } from './api/modules/user'; - -// 获取用户信息 -const user = await userApi.getUser('userId'); - -// 更新用户信息 -await userApi.updateUser('userId', { name: 'New Name' }); -``` - -## 设计模式 - -- **工厂模式**:创建API客户端实例 -- **单例模式**:确保API客户端全局唯一 -- **模块化模式**:按功能组织API端点 \ No newline at end of file diff --git a/api/chat-api.ts b/api/chat-api.ts new file mode 100644 index 0000000..50d9b32 --- /dev/null +++ b/api/chat-api.ts @@ -0,0 +1,92 @@ +import { GET, POST, ApiClientBound, Request } from '../decorators'; +import { API_ENDPOINTS } from './common'; +import type { + CreateChatSessionRequest, + CreateChatSessionResponse, + SendMessageRequest, + SendMessageResponse, + GetChatSessionsRequest, + GetChatSessionsResponse, + GetChatMessagesRequest, + GetChatMessagesResponse, + SearchChatMessagesRequest, + SearchChatMessagesResponse +} from '../types'; +import type { AxiosHttpClient } from '../client'; + +/** + * 聊天相关API服务类 + */ +@ApiClientBound +export class ChatApiService { + // 语法要求 + declare client: AxiosHttpClient; + + /** + * 创建聊天会话 + * @param request 创建聊天会话请求参数 + * @returns 创建聊天会话响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.CHATS) + @Request + async createChatSession(_request: CreateChatSessionRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 获取聊天会话列表 + * @param request 获取聊天会话列表请求参数 + * @returns 获取聊天会话列表响应 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.CHATS) + @Request + async getChatSessions(_request: GetChatSessionsRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 获取聊天消息列表 + * @param request 获取聊天消息请求参数 + * @returns 获取聊天消息响应 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.CHAT_ONE_MESSAGES) + @Request + async getChatMessages(_request: GetChatMessagesRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 发送消息 + * @param request 发送消息请求参数 + * @returns 发送消息响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.CHAT_ONE_MESSAGES) + @Request + async sendMessage(_request: SendMessageRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 搜索聊天消息或会话 + * @param request 搜索聊天消息请求参数 + * @returns 搜索聊天消息响应 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.CHATS) + @Request + async searchChatMessages(_request: SearchChatMessagesRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } +} + +// 创建单例实例 +export const chatApi = new ChatApiService(); \ No newline at end of file diff --git a/api/client.ts b/api/client.ts deleted file mode 100644 index 36cbeef..0000000 --- a/api/client.ts +++ /dev/null @@ -1,287 +0,0 @@ -import axios from 'axios'; -import type { ApiClient } from './types'; -import type { AxiosError, AxiosResponse, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'; -import { ApiError } from './errors'; // 直接导入ApiError - -// 创建API客户端实例的工厂函数(直接用axios的requestConfig) -const createApiClientInstance = (config?: Partial): ApiClient => { - // 创建axios实例 - const instance = axios.create({ - baseURL: '/api', // Core层使用默认值,具体配置由Runtime层注入 - timeout: 10000, - headers: { - 'Content-Type': 'application/json' - }, - withCredentials: true, // 允许跨域携带cookie - ...config // 应用传入的配置 - }); - - // 请求拦截器数组 - const requestInterceptors: Array = []; - - // 响应拦截器数组 - const responseInterceptors: Array = []; - - // 添加请求拦截器(函数声明,非函数实体,接收两个钩子参数) - const addRequestInterceptor = ( - // InternalAxiosRequestConfig 是 axios 内部专门用来管理监听器的 - onFulfilled?: (_config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise, - onRejected?: (_error: unknown) => unknown - ): number => { - const handler = instance.interceptors.request.use(onFulfilled, onRejected); - requestInterceptors.push(handler); - return handler; - }; - - // 添加响应拦截器 - const addResponseInterceptor = ( - onFulfilled?: (_response: AxiosResponse) => AxiosResponse | Promise, - onRejected?: (_error: unknown) => unknown - ): number => { - const handler = instance.interceptors.response.use(onFulfilled, onRejected); - responseInterceptors.push(handler); - return handler; - }; - - // 移除请求拦截器 - const removeRequestInterceptor = (handler: number): void => { - const index = requestInterceptors.indexOf(handler); - if (index !== -1) { - // axios内部的移除逻辑 - instance.interceptors.request.eject(handler); - // 封装管理的数据结构的移除(有点像LRU) - requestInterceptors.splice(index, 1); - } - }; - - // 移除响应拦截器(同上) - const removeResponseInterceptor = (handler: number): void => { - const index = responseInterceptors.indexOf(handler); - if (index !== -1) { - instance.interceptors.response.eject(handler); - responseInterceptors.splice(index, 1); - } - }; - - // 设置默认配置(单例的) - // defaults是最底层请求配置 - const setDefaults = (_config: Partial): void => { - Object.assign(instance.defaults, _config); - }; - - // 更新基础URL,比上面那个更细 - const setBaseURL = (baseURL: string): void => { - instance.defaults.baseURL = baseURL; - }; - - // 创建新实例 - 用于跨域请求等特殊场景 - const createInstance = (config?: Partial): ApiClient => { - // 创建新的axios实例(工厂模式+原型模式) - const newInstance = axios.create({ - ...instance.defaults, - ...config - }); - - // 为新实例创建拦截器数组 - const newRequestInterceptors: Array = []; - const newResponseInterceptors: Array = []; - - // 复制拦截器 - // 其实逻辑有很大问题,包括闭包问题、handler不会对应、双重断言等等 - // 应该不复制,而是重新添加 - requestInterceptors.forEach(handler => { - // 由于AxiosInterceptorManager类型定义中没有handlers属性, - // 我们使用类型断言来访问内部属性 - type RequestHandlerType = { - id: number; - fulfilled: (_config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig; - rejected: (_error: unknown) => unknown; - }; - - type RequestManagerType = { - handlers?: Array; - }; - - const requestManager = instance.interceptors.request as unknown as RequestManagerType; - const originalInterceptor = requestManager.handlers?.find((h: RequestHandlerType) => h.id === handler); - if (originalInterceptor) { - const newHandler = newInstance.interceptors.request.use( - originalInterceptor.fulfilled, - originalInterceptor.rejected - ); - newRequestInterceptors.push(newHandler); - } - }); - - // 同上 - responseInterceptors.forEach(handler => { - // 由于AxiosInterceptorManager类型定义中没有handlers属性, - // 我们使用类型断言来访问内部属性 - type ResponseHandlerType = { - id: number; - fulfilled: (_response: AxiosResponse) => AxiosResponse; - rejected: (_error: AxiosError) => unknown; - }; - - type ResponseManagerType = { - handlers?: Array; - }; - - const responseManager = instance.interceptors.response as unknown as ResponseManagerType; - const originalInterceptor = responseManager.handlers?.find((h: ResponseHandlerType) => h.id === handler); - if (originalInterceptor) { - const newHandler = newInstance.interceptors.response.use( - originalInterceptor.fulfilled, - originalInterceptor.rejected - ); - newResponseInterceptors.push(newHandler); - } - }); - - // 返回一个符合ApiClient接口的对象 - return { - request: async (config: AxiosRequestConfig): Promise => { - try { - const response = await newInstance.request(config); - return response.data; - } catch (error) { - // 直接使用ApiError.fromAxiosError静态方法处理错误 - return Promise.reject(ApiError.fromAxiosError(error as AxiosError)); - } - }, - get: (url: string, config?: AxiosRequestConfig): Promise => { - return newInstance.request({ ...config, method: 'GET', url }).then((res: AxiosResponse) => res.data); - }, - post: (url: string, data?: unknown, config?: AxiosRequestConfig): Promise => { - return newInstance.request({ ...config, method: 'POST', url, data }).then((res: AxiosResponse) => res.data); - }, - put: (url: string, data?: unknown, config?: AxiosRequestConfig): Promise => { - return newInstance.request({ ...config, method: 'PUT', url, data }).then((res: AxiosResponse) => res.data); - }, - delete: (url: string, config?: AxiosRequestConfig): Promise => { - return newInstance.request({ ...config, method: 'DELETE', url }).then((res: AxiosResponse) => res.data); - }, - patch: (url: string, data?: unknown, config?: AxiosRequestConfig): Promise => { - return newInstance.request({ ...config, method: 'PATCH', url, data }).then((res: AxiosResponse) => res.data); - }, - setDefaults: (_config: Partial): void => { - Object.assign(newInstance.defaults, _config); - }, - setBaseURL: (baseURL: string): void => { - newInstance.defaults.baseURL = baseURL; - }, - createInstance: (newConfig?: Partial): ApiClient => { - // 手动处理headers字段的类型转换(这里newInstance实际上是原型) - const { headers, ...otherDefaults } = newInstance.defaults; - const configWithTypedHeaders: Partial = { - ...otherDefaults, - ...newConfig - }; - - // 只有当headers存在时才添加(因为defaults和axiosRequestConfig在这里类型不一样) - if (headers) { - // 手动转换headers字段,确保类型安全 - const convertedHeaders: Record = {}; - Object.entries(headers).forEach(([key, value]) => { - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - convertedHeaders[key] = value; - } - }); - configWithTypedHeaders.headers = convertedHeaders; - } - - return createInstance(configWithTypedHeaders); - }, - addRequestInterceptor: ( - onFulfilled?: (_config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise, - onRejected?: (_error: unknown) => unknown - ): number => { - const handler = newInstance.interceptors.request.use(onFulfilled, onRejected); - newRequestInterceptors.push(handler); - return handler; - }, - addResponseInterceptor: ( - onFulfilled?: (_response: AxiosResponse) => AxiosResponse | Promise, - onRejected?: (_error: unknown) => unknown - ): number => { - const handler = newInstance.interceptors.response.use(onFulfilled, onRejected); - newResponseInterceptors.push(handler); - return handler; - }, - removeRequestInterceptor: (handler: number): void => { - const index = newRequestInterceptors.indexOf(handler); - if (index !== -1) { - newInstance.interceptors.request.eject(handler); - newRequestInterceptors.splice(index, 1); - } - }, - removeResponseInterceptor: (handler: number): void => { - const index = newResponseInterceptors.indexOf(handler); - if (index !== -1) { - newInstance.interceptors.response.eject(handler); - newResponseInterceptors.splice(index, 1); - } - } - }; - }; - - // 基础请求方法 - const request = async (config: AxiosRequestConfig): Promise => { - try { - const response = await instance.request(config); - return response.data; - } catch (error) { - // 直接使用ApiError.fromAxiosError静态方法处理错误 - return Promise.reject(ApiError.fromAxiosError(error as AxiosError)); - } - }; - - // HTTP方法简写 - const get = (url: string, config?: AxiosRequestConfig): Promise => { - return request({ ...config, method: 'GET', url }); - }; - - const post = (url: string, data?: unknown, config?: AxiosRequestConfig): Promise => { - return request({ ...config, method: 'POST', url, data }); - }; - - const put = (url: string, data?: unknown, config?: AxiosRequestConfig): Promise => { - return request({ ...config, method: 'PUT', url, data }); - }; - - const del = (url: string, config?: AxiosRequestConfig): Promise => { - return request({ ...config, method: 'DELETE', url }); - }; - - const patch = (url: string, data?: unknown, config?: AxiosRequestConfig): Promise => { - return request({ ...config, method: 'PATCH', url, data }); - }; - - return { - request, - get, - post, - put, - delete: del, - patch, - setDefaults, - setBaseURL, - createInstance, - addRequestInterceptor, - addResponseInterceptor, - removeRequestInterceptor, - removeResponseInterceptor - }; -}; - -// 默认实例(单例) -const apiClient = createApiClientInstance(); - -// 工厂函数 -const createApiClient = (config?: Partial) => { - return createApiClientInstance(config); -}; - -// 导出API客户端单例和工厂函数 -// 其实单例没用,因为baseurl总归不一样的,但是怎么说呢,单例也只需要改一个baseurl就好了 -export { apiClient, createApiClient }; diff --git a/api/common.ts b/api/common.ts new file mode 100644 index 0000000..c79fa65 --- /dev/null +++ b/api/common.ts @@ -0,0 +1,49 @@ +// ApiError错误接口 +export interface ApiError { + status: number; // 状态码(来自response的status) + statusText: string; // 状态文本(来自response的msg) + code: string; // 错误码(字符串) + message: string; // 报错信息(字符串) + // 没有别的细节了,错误码直接对照常量映射 +} + +// 接口路径 +export const API_ENDPOINTS = { + // 用户相关(自己和别人分开) + // 视图类 + ME_PROFILE: '/me/profile', + USERS_PROFILES: '/users/profiles', + SOMEBODY_PROFILE: '/users/:userId/profile', + + // 会话相关(但是用的接口是在user文件夹定义的) + AUTH_LOGIN: '/auth/login', + AUTH_REGISTER: '/auth/register', + AUTH_CHANGE_PASSWORD: '/auth/changePassword', + USER_CURRENT: '/auth/me', // 只用于校验会话状态 + USER_PROFILE_UPDATE: '/me/profile/update', + USER_SEARCH: '/users/search', + USER_HOT_AUTHORS: '/users/hot', + USER_RANKING: '/users/ranking', + USER_NOTIFICATIONS: '/me/notifications', + + // 聊天相关(这个也肯定只有自己的,就不写/me了) + CHATS: '/chats', // GET 列表,搜索等等 + CHAT_ONE_MESSAGES: '/chats/:chatId/messages', // GET 单条消息 / POST 新建消息等 + + // 模型相关(公共接口) + MODELS: '/models', // GET 列表(列表里是模型的完整信息) + MODEL_ONE_LIKES: '/models/:modelId/likes', // 操作 + MODEL_ONE_COMMENTS: '/models/:modelId/comments', // 操作 + + // 帖子相关 + POSTS: '/posts', // GET 列表 + POST_ONE_LIKES: '/posts/:postId/likes', // 操作 + POST_ONE_STARS: '/posts/:postId/stars', // 收藏数 + POST_ONE_COMMENTS: '/posts/:postId/comments', // 操作 + + // 上传相关 + UPLOAD_CHUNK: '/upload/chunk', + MERGE_CHUNKS: '/upload/merge', + CHECK_UPLOAD_STATUS: '/upload/status' +} as const; + diff --git a/api/errors.ts b/api/errors.ts deleted file mode 100644 index 9ce4038..0000000 --- a/api/errors.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * API错误处理 - */ -import type { AxiosError } from 'axios'; - -/** - * API错误接口 - */ -export interface IApiError { - code: number; - message: string; - details?: unknown; -} - -/** - * API错误类(诶,就是要自定义) - */ -export class ApiError extends Error implements IApiError { - public readonly code: number; - public readonly details: unknown; - - constructor(code: number, message: string, details?: unknown) { - super(message); - this.name = 'ApiError'; - this.code = code; - this.details = details; - } - - /** - * 从Axios错误创建API错误(就是为了简化) - */ - static fromAxiosError(error: AxiosError): ApiError { - if (!error.response) { - return new ApiError(0, error.message || '网络错误'); - } - - const { status, data } = error.response; - const message = data && typeof data === 'object' && 'message' in data - ? data.message as string - : '请求失败'; - - return new ApiError(status, message, data); - } -} - - diff --git a/api/factory.ts b/api/factory.ts deleted file mode 100644 index a9ed835..0000000 --- a/api/factory.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { apiClient, createApiClient } from './client'; -import type { AxiosRequestConfig } from 'axios'; -import { postApi } from './modules/post'; -import { userApi } from './modules/user'; -import { chatApi } from './modules/chat'; -import { modelApi } from './modules/model'; - -/** - * API 工厂函数,用于创建和管理 API 实例(模块化封装) - * 提供统一的 API 访问入口和配置管理 - */ -export const createApi = (config?: Partial) => { - const client = config ? createApiClient(config) : apiClient; - - return { - // 核心客户端 - client, - - // API 模块 - 使用提供的客户端或默认客户端 - modules: { - post: postApi(client), - user: userApi(client), - chat: chatApi(client), - model: modelApi(client) - }, - - // 便捷访问 - post: postApi(client), - user: userApi(client), - chat: chatApi(client), - model: modelApi(client) - }; -}; - -// 导出默认 API 实例 -export const api = createApi(); - -// 向后兼容的导出 -// 单例还是输出了 -export { apiClient } from './client'; -export { postApi, userApi, chatApi, modelApi } from './modules'; diff --git a/api/index.ts b/api/index.ts index d4020e4..134d56b 100644 --- a/api/index.ts +++ b/api/index.ts @@ -1,19 +1,44 @@ -// 导出API工厂函数和默认实例 -export { createApi, api } from './factory'; +// 导出公共接口和常量 +export { API_ENDPOINTS } from './common'; +export type { ApiError } from './common'; -// 导出API类型和配置 -export { - API_ENDPOINTS, - ApiStatusCode, - ApiErrorType, - DEFAULT_REQUEST_CONFIG, - DEFAULT_PAGINATION_CONFIG, - type ApiClient, - type ApiResponse, - type PaginatedResponse, - type ErrorResponse -} from './types'; +// 导出帖子相关API +export { PostApiService } from './post-api'; -// 向后兼容的导出 -export { apiClient } from './client'; -export { postApi, userApi, chatApi, modelApi } from './modules'; +// 导出聊天相关API +export { ChatApiService } from './chat-api'; + +// 导出模型相关API +export { ModelApiService } from './model-api'; + +// 导出用户相关API +export { UserApiService } from './user-api'; + +// 导出上传相关API +export { UploadApiService } from './upload-api'; + +/** + * API模块入口 + * + * 本模块提供了所有业务相关的API服务,包括: + * - 帖子相关:创建、查询、点赞、收藏等操作 + * - 聊天相关:会话管理、消息发送与接收 + * - 模型相关:模型查询、评论、点赞等 + * - 用户相关:登录、注册、个人资料管理 + * + * 所有API服务都使用装饰器模式实现,通过ApiClientBound装饰器注入HTTP客户端实例, + * 使用HTTP方法装饰器(GET、POST等)定义请求方法和路径, + * 通过Request装饰器自动处理请求逻辑。 + */ +import { postApi } from './post-api'; +import { chatApi } from './chat-api'; +import { modelApi } from './model-api'; +import { userApi } from './user-api'; +import { uploadApi } from './upload-api'; +export const api = { + post: postApi, + chat: chatApi, + model: modelApi, + user: userApi, + upload: uploadApi, +}; \ No newline at end of file diff --git a/api/model-api.ts b/api/model-api.ts new file mode 100644 index 0000000..8423ea1 --- /dev/null +++ b/api/model-api.ts @@ -0,0 +1,77 @@ +import { GET, POST, ApiClientBound, Request } from '../decorators'; +import { API_ENDPOINTS } from './common'; +import type { + GetModelsRequest, + GetModelsResponse, + GetModelCommentsRequest, + GetModelCommentsResponse, + CreateModelCommentRequest, + CreateModelCommentResponse, + LikeModelRequest, + LikeModelResponse, +} from '../types/model/api'; +import type { AxiosHttpClient } from '../client'; + +/** + * 模型相关API服务类 + */ +@ApiClientBound +export class ModelApiService { + // 语法要求 + declare client: AxiosHttpClient; + + /** + * 获取模型列表 + * @param request 获取模型列表请求参数 + * @returns 获取模型列表响应 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.MODELS) + @Request + async getModels(_request: GetModelsRequest): Promise { + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 获取模型评论列表 + * @param request 获取模型评论列表请求参数 + * @returns 获取模型评论列表响应 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.MODEL_ONE_COMMENTS) + @Request + async getModelComments(_request: GetModelCommentsRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 发表模型评论 + * @param request 发表模型评论请求参数 + * @returns 发表模型评论响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.MODEL_ONE_COMMENTS) + @Request + async createModelComment(_request: CreateModelCommentRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 点赞模型 + * @param request 包含modelId的请求参数 + * @returns 点赞响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.MODEL_ONE_LIKES) + @Request + async likeModel(_request: LikeModelRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + +} + +// 创建单例实例 +export const modelApi = new ModelApiService(); \ No newline at end of file diff --git a/api/modules/chat.ts b/api/modules/chat.ts deleted file mode 100644 index dfaf658..0000000 --- a/api/modules/chat.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { AxiosRequestConfig } from 'axios'; -import type { ApiClient } from '../types'; -import type { - CreateChatSessionRequest, - UpdateChatSessionRequest, - SendMessageRequest, - GetChatSessionsRequest, - GetChatSessionsResponse, - GetChatMessagesRequest, - GetChatMessagesResponse, - MarkMessagesAsReadRequest, - MarkMessagesAsReadResponse -} from '@/types/chat/api'; -import type { ChatSession, ChatMessage } from '@/types/chat/base'; - -// 聊天API服务工厂函数 -export const chatApi = (client: ApiClient) => ({ - // 创建聊天会话 - createSession: (data: CreateChatSessionRequest): Promise<{ session: ChatSession }> => { - return client.post('/chat/sessions', data); - }, - - // 更新聊天会话(纯充数那个) - updateSession: ({ sessionId, ...data }: UpdateChatSessionRequest): Promise<{ session: ChatSession }> => { - return client.put(`/chat/sessions/${sessionId}`, data); - }, - - // 发送消息 - sendMessage: (data: SendMessageRequest): Promise<{ message: ChatMessage }> => { - return client.post(`/chat/sessions/${data.sessionId}/messages`, data); - }, - - // 获取聊天会话列表(不知道写了个什么玩意儿,转什么,应该不用参数的) - getSessions: (params?: GetChatSessionsRequest): Promise => { - const config: AxiosRequestConfig = params ? { params } : {}; - return client.get('/chat/sessions', config); - }, - - // 获取聊天消息 - getMessages: ({ sessionId, ...params }: GetChatMessagesRequest): Promise => { - return client.get(`/chat/sessions/${sessionId}/messages`, { params }); - }, - - // 标记消息已读 - markMessagesAsRead: ({ sessionId, messageIds }: MarkMessagesAsReadRequest): Promise => { - return client.post(`/chat/sessions/${sessionId}/read`, { messageIds }); - } -}); diff --git a/api/modules/index.ts b/api/modules/index.ts deleted file mode 100644 index 2968f7d..0000000 --- a/api/modules/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { apiClient } from '../client'; -import { postApi as createPostApi } from './post'; -import { userApi as createUserApi } from './user'; -import { chatApi as createChatApi } from './chat'; -import { modelApi as createModelApi } from './model'; - -// 导出工厂函数(和实例同名导出是什么鬼啊),幸好不是从这里导出的 -export { postApi as createPostApi, userApi as createUserApi, chatApi as createChatApi, modelApi as createModelApi }; - -// 向后兼容的默认实例 -export const postApi = createPostApi(apiClient); -export const userApi = createUserApi(apiClient); -export const chatApi = createChatApi(apiClient); -export const modelApi = createModelApi(apiClient); diff --git a/api/modules/model.ts b/api/modules/model.ts deleted file mode 100644 index 738df6d..0000000 --- a/api/modules/model.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { AxiosRequestConfig } from 'axios'; -import type { ApiClient } from '../types'; -import type { - GetAIPlazaRequest, - GetAIPlazaResponse, - GetModelDetailRequest, - GetModelDetailResponse, - GetModelCommentsRequest, - GetModelCommentsResponse -} from '@/types/model/api'; - -// AI模型API服务工厂函数 -export const modelApi = (client: ApiClient) => ({ - // 获取AI模型广场数据 - getAIPlaza: (params?: GetAIPlazaRequest): Promise => { - const config: AxiosRequestConfig = params ? { params } : {}; - return client.get('/models/plaza', config); - }, - - // 获取模型详情 - getModelDetail: ({ modelId, ...params }: GetModelDetailRequest): Promise => { - return client.get(`/models/${modelId}`, { params }); - }, - - // 获取模型评论 - getModelComments: ({ modelId, ...params }: GetModelCommentsRequest): Promise => { - return client.get(`/models/${modelId}/comments`, { params }); - } -}); diff --git a/api/modules/post.ts b/api/modules/post.ts deleted file mode 100644 index 6f85ded..0000000 --- a/api/modules/post.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { AxiosRequestConfig } from 'axios'; -import type { ApiClient } from '../types'; -import type { - CreatePostRequest, - CreatePostResponse, - GetPostsRequest, - GetPostsResponse, - GetPostRequest, - GetPostResponse, - LikePostRequest, - LikePostResponse, - BookmarkPostRequest, - BookmarkPostResponse, - CreateCommentRequest, - CreateCommentResponse, - GetCommentsRequest, - GetCommentsResponse, - LikeCommentRequest, - LikeCommentResponse, - GetHotPostsRequest, - GetHotPostsResponse, - GetPostRankingRequest, - GetPostRankingResponse -} from '@/types/post/api'; - -// 帖子API服务工厂函数 -export const postApi = (client: ApiClient) => ({ - // 创建帖子 - createPost: (data: CreatePostRequest): Promise => { - return client.post('/posts', data); - }, - - // 获取帖子列表 - getPosts: (params: GetPostsRequest): Promise => { - const config: AxiosRequestConfig = { params }; - return client.get('/posts', config); - }, - - // 获取帖子详情 - getPost: ({ postId }: GetPostRequest): Promise => { - return client.get(`/posts/${postId}`); - }, - - // 点赞帖子 - likePost: ({ postId }: LikePostRequest): Promise => { - return client.put(`/posts/${postId}/like`); - }, - - // 收藏帖子 - bookmarkPost: ({ postId }: BookmarkPostRequest): Promise => { - return client.put(`/posts/${postId}/bookmark`); - }, - - // 创建评论 - createComment: (data: CreateCommentRequest): Promise => { - return client.post(`/posts/${data.postId}/comments`, data); - }, - - // 获取评论列表 - getComments: (params: GetCommentsRequest): Promise => { - const { postId, ...queryParams } = params; - return client.get(`/posts/${postId}/comments`, { params: queryParams }); - }, - - // 点赞评论 - likeComment: ({ commentId }: LikeCommentRequest): Promise => { - return client.put(`/comments/${commentId}/like`); - }, - - // 获取热门帖子 - getHotPosts: (params: GetHotPostsRequest = {}): Promise => { - return client.get('/posts/hot', { params }); - }, - - // 获取帖子榜单 - getPostRanking: (params: GetPostRankingRequest = {}): Promise => { - return client.get('/posts/ranking', { params }); - } -}); diff --git a/api/modules/user.ts b/api/modules/user.ts deleted file mode 100644 index a399f1f..0000000 --- a/api/modules/user.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { ApiClient } from '../types'; -import type { - LoginRequest, - LoginResponse, - RegisterRequest, - RegisterResponse, - UserProfileUpdateRequest, - UserProfileUpdateResponse, - UserFollowRequest, - UserFollowResponse, - GetHotAuthorsRequest, - GetHotAuthorsResponse, - GetAuthorRankingRequest, - GetAuthorRankingResponse -} from '@/types/user'; - -// 用户API服务工厂函数 -export const userApi = (client: ApiClient) => ({ - // 用户登录 - login: (data: LoginRequest): Promise => { - return client.post('/auth/login', data); - }, - - // 用户注册 - register: (data: RegisterRequest): Promise => { - return client.post('/auth/register', data); - }, - - // 获取用户档案 - getProfile: (): Promise => { - return client.get('/user/profile'); - }, - - // 更新用户档案 - updateProfile: (data: UserProfileUpdateRequest): Promise => { - return client.put('/user/profile', data); - }, - - // 关注用户 - followUser: ({ userId }: UserFollowRequest): Promise => { - return client.put(`/user/follow/${userId}`); - }, - - // 取消关注用户 - unfollowUser: ({ userId }: UserFollowRequest): Promise => { - return client.delete(`/user/follow/${userId}`); - }, - - // 获取热门作者 - getHotAuthors: (params: GetHotAuthorsRequest = {}): Promise => { - return client.get('/user/hot', { params }); - }, - - // 获取作者榜单 - getAuthorRanking: (params: GetAuthorRankingRequest = {}): Promise => { - return client.get('/user/ranking', { params }); - } -}); diff --git a/api/post-api.ts b/api/post-api.ts new file mode 100644 index 0000000..35cca0d --- /dev/null +++ b/api/post-api.ts @@ -0,0 +1,122 @@ +import { GET, POST, ApiClientBound, Request } from '../decorators'; +import { API_ENDPOINTS } from './common'; +import type { + CreatePostRequest, + CreatePostResponse, + GetPostsRequest, + GetPostsResponse, + LikePostRequest, + LikePostResponse, + BookmarkPostRequest, + BookmarkPostResponse, + CreateCommentRequest, + CreateCommentResponse, + GetCommentsRequest, + GetCommentsResponse, + GetHotPostsRequest, + GetHotPostsResponse +} from '../types'; +import type { AxiosHttpClient } from '../client'; + +/** + * 帖子相关API服务类 + */ +@ApiClientBound +export class PostApiService { + // 语法要求 + declare client: AxiosHttpClient; + + /** + * 创建新帖子 + * @param request 创建帖子请求参数 + * @returns 创建帖子响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.POSTS) + @Request + async createPost(_request: CreatePostRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 获取帖子列表 + * @param request 获取帖子列表请求参数 + * @returns 获取帖子列表响应 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.POSTS) + @Request + async getPosts(_request: GetPostsRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 点赞帖子 + * @param request 点赞帖子请求参数 + * @returns 点赞帖子响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.POST_ONE_LIKES) + @Request + async likePost(_request: LikePostRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 收藏帖子 + * @param request 收藏帖子请求参数 + * @returns 收藏帖子响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.POST_ONE_STARS) + @Request + async bookmarkPost(_request: BookmarkPostRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 创建评论 + * @param request 创建评论请求参数 + * @returns 创建评论响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.POST_ONE_COMMENTS) + @Request + async createComment(_request: CreateCommentRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 获取评论列表 + * @param request 获取评论列表请求参数 + * @returns 获取评论列表响应 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.POST_ONE_COMMENTS) + @Request + async getComments(_request: GetCommentsRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 获取热门帖子 + * @param request 获取热门帖子请求参数 + * @returns 获取热门帖子响应 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.POSTS) + @Request + async getHotPosts(_request: GetHotPostsRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } +} + +// 创建单例实例 +export const postApi = new PostApiService(); \ No newline at end of file diff --git a/api/types.ts b/api/types.ts deleted file mode 100644 index 588b33e..0000000 --- a/api/types.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * API 类型定义文件 - */ - -import type { AxiosResponse, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'; - -// API客户端接口定义 -export interface ApiClient { - request(config: AxiosRequestConfig): Promise; - get(url: string, config?: AxiosRequestConfig): Promise; - post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise; - put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise; - delete(url: string, config?: AxiosRequestConfig): Promise; - patch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise; - setDefaults(config: Partial): void; - setBaseURL(baseURL: string): void; - createInstance(config?: Partial): ApiClient; - addRequestInterceptor( - onFulfilled?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise, - onRejected?: (error: unknown) => unknown - ): number; - addResponseInterceptor( - onFulfilled?: (response: AxiosResponse) => AxiosResponse | Promise, - onRejected?: (error: unknown) => unknown - ): number; - removeRequestInterceptor(handler: number): void; - removeResponseInterceptor(handler: number): void; -} - -// 后面的除了test里用,core里其他地方根本没用上过,都用泛型unknown了,这是不对的 -// API响应接口 -export interface ApiResponse { - success: boolean; - data: T; - message?: string; - code?: number; -} - -// 分页响应接口 -export interface PaginatedResponse { - items: T[]; - total: number; - page: number; - pageSize: number; - totalPages: number; -} - -// 错误响应接口 -export interface ErrorResponse { - success: false; - message: string; - code?: number; - details?: unknown; -} - -// API端点常量 -export const API_ENDPOINTS = { - // 用户相关 - USER_LOGIN: '/auth/login', - USER_REGISTER: '/auth/register', - USER_LOGOUT: '/auth/logout', - USER_PROFILE: '/user/profile', - USER_CURRENT: '/auth/me', - - // 聊天相关 - CHAT_LIST: '/chat/list', - CHAT_CREATE: '/chat/create', - CHAT_DETAIL: '/chat/detail', - CHAT_DELETE: '/chat/delete', - CHAT_MESSAGE: '/chat/message', - - // 模型相关 - MODEL_LIST: '/model/list', - MODEL_DETAIL: '/model/detail', - - // 帖子相关 - POST_LIST: '/post/list', - POST_DETAIL: '/post/detail', - POST_CREATE: '/post/create', - POST_UPDATE: '/post/update', - POST_DELETE: '/post/delete' -} as const; - -// API状态码 -export const ApiStatusCode = { - OK: 200, - CREATED: 201, - NO_CONTENT: 204, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - INTERNAL_SERVER_ERROR: 500 -} as const; - -// API错误类型 -export const ApiErrorType = { - NETWORK_ERROR: 'NETWORK_ERROR', - VALIDATION_ERROR: 'VALIDATION_ERROR', - AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR', - AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR', - NOT_FOUND_ERROR: 'NOT_FOUND_ERROR', - SERVER_ERROR: 'SERVER_ERROR', - UNKNOWN_ERROR: 'UNKNOWN_ERROR' -} as const; - -// 默认请求配置 -export const DEFAULT_REQUEST_CONFIG = { - timeout: 10000, - headers: { - 'Content-Type': 'application/json' - }, - withCredentials: true -} as const; - -// 默认分页配置 -export const DEFAULT_PAGINATION_CONFIG = { - page: 1, - pageSize: 20 -} as const; diff --git a/api/upload-api.ts b/api/upload-api.ts new file mode 100644 index 0000000..5d58e24 --- /dev/null +++ b/api/upload-api.ts @@ -0,0 +1,132 @@ +import { ApiClientBound } from '../decorators'; +import { API_ENDPOINTS } from './common'; +import type { + UploadChunkRequest, + UploadChunkResponse, + MergeChunksRequest, + MergeChunksResponse, + CheckUploadStatusRequest, + CheckUploadStatusResponse +} from '../types/upload/api'; +import type { AxiosHttpClient } from '../client'; + +/** + * 上传API服务类 + * 使用自定义实现而非@Request装饰器,以支持分片上传的复杂headers + */ +@ApiClientBound +export class UploadApiService { + /** + * 客户端实例,由ApiClientBound装饰器注入 + */ + declare client: AxiosHttpClient; + + /** + * 上传文件分片 + * @param chunkRequest 分片上传请求参数 + * @param chunkBlob 文件分片数据 + * @returns 分片上传响应 + */ + async uploadChunk( + chunkRequest: UploadChunkRequest, + chunkBlob: Blob + ): Promise { + // 创建FormData以支持文件上传 + const formData = new FormData(); + formData.append('chunk', chunkBlob); + formData.append('chunkIndex', chunkRequest.chunkIndex.toString()); + formData.append('totalChunks', chunkRequest.totalChunks.toString()); + formData.append('fileName', chunkRequest.fileName); + formData.append('fileType', chunkRequest.fileType); + formData.append('fileId', chunkRequest.fileId); + + // 自定义headers,不设置Content-Type,让浏览器自动设置multipart/form-data + const headers = { + 'X-Chunk-Index': chunkRequest.chunkIndex.toString(), + 'X-Total-Chunks': chunkRequest.totalChunks.toString(), + 'X-File-Id': chunkRequest.fileId, + 'X-Chunk-Size': chunkRequest.chunkSize.toString(), + 'X-Total-Size': chunkRequest.totalSize.toString(), + // 不设置Content-Type,让浏览器自动处理 + }; + + // 直接使用client实例发送请求,而不通过@Request装饰器 + return await this.client.request({ + method: 'POST', + url: API_ENDPOINTS.UPLOAD_CHUNK, + body: formData, + headers, + // 允许跨域携带凭证 + withCredentials: true, + // 禁用默认的Content-Type设置(禁止axios的jsonify处理) + transformRequest: [(data: any) => data] + }); + } + + /** + * 合并文件分片 + * @param mergeRequest 合并分片请求参数 + * @returns 合并分片响应 + */ + async mergeChunks(mergeRequest: MergeChunksRequest): Promise { + const headers = { + 'Content-Type': 'application/json', + 'X-File-Id': mergeRequest.fileId, + 'X-Total-Chunks': mergeRequest.totalChunks.toString() + }; + + return await this.client.request({ + method: 'POST', + url: API_ENDPOINTS.MERGE_CHUNKS, + body: mergeRequest, + headers, + withCredentials: true + }); + } + + /** + * 检查文件上传状态 + * @param checkRequest 检查上传状态请求参数 + * @returns 上传状态响应 + */ + async checkUploadStatus( + checkRequest: CheckUploadStatusRequest + ): Promise { + const headers = { + 'X-File-Id': checkRequest.fileId + }; + + return await this.client.request({ + method: 'POST', + url: API_ENDPOINTS.CHECK_UPLOAD_STATUS, + body: checkRequest, + headers, + withCredentials: true + }); + } + + /** + * 生成文件唯一ID + * @param _fileName 文件名(未使用,保留参数位置) + * @param fileSize 文件大小 + * @returns 文件唯一ID + */ + generateFileId(_fileName: string, fileSize: number): string { + const timestamp = Date.now().toString(36); + const random = Math.random().toString(36).substring(2, 9); + return `${timestamp}_${random}_${fileSize}`; + } + + /** + * 计算分片数量 + * @param fileSize 文件大小 + * @param chunkSize 分片大小 + * @returns 分片数量 + */ + calculateChunks(fileSize: number, chunkSize: number = 5 * 1024 * 1024): number { + return Math.ceil(fileSize / chunkSize); + } +} + +// 导出单例实例 +export const uploadApi = new UploadApiService(); diff --git a/api/user-api.ts b/api/user-api.ts new file mode 100644 index 0000000..a97ef40 --- /dev/null +++ b/api/user-api.ts @@ -0,0 +1,150 @@ +import { GET, POST, ApiClientBound, Request } from '../decorators'; +import { API_ENDPOINTS } from './common'; +import type { + GetMyProfileResponse, + GetUserProfileRequest, + GetUserProfileResponse, + UserProfileUpdateRequest, + UserProfileUpdateResponse, + LoginRequest, + LoginResponse, + RegisterRequest, + RegisterResponse, + ChangePasswordRequest, + ChangePasswordResponse, + UserSearchRequest, + UserSearchResponse, + GetHotAuthorsRequest, + GetHotAuthorsResponse, + GetAuthorRankingRequest, + GetAuthorRankingResponse +} from '../types'; +import type { AxiosHttpClient } from '../client'; + +/** + * 用户相关API服务类 + */ +@ApiClientBound +export class UserApiService { + // 语法要求 + declare client: AxiosHttpClient; + + /** + * 获取我的个人资料 + * @returns 个人资料信息 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.ME_PROFILE) + @Request + async getMyProfile(): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 获取他人个人资料 + * @param request 包含userId的请求参数 + * @returns 他人个人资料信息 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.SOMEBODY_PROFILE) + @Request + async getUserProfile(_request: GetUserProfileRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 用户登录 + * @param request 登录请求参数 + * @returns 登录响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.AUTH_LOGIN) + @Request + async login(_request: LoginRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 用户注册 + * @param request 注册请求参数 + * @returns 注册响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.AUTH_REGISTER) + @Request + async register(_request: RegisterRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 修改密码 + * @param request 修改密码请求参数 + * @returns 修改密码响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.AUTH_CHANGE_PASSWORD) + @Request + async changePassword(_request: ChangePasswordRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 更新个人资料 + * @param request 更新请求参数 + * @returns 更新响应 + * @throws ApiError 当请求失败时抛出 + */ + @POST(API_ENDPOINTS.USER_PROFILE_UPDATE) + @Request + async updateProfile(_request: UserProfileUpdateRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 搜索用户 + * @param request 搜索请求参数 + * @returns 搜索响应 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.USER_SEARCH) + @Request + async searchUsers(_request: UserSearchRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 获取热门作者 + * @param request 请求参数 + * @returns 热门作者列表 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.USER_HOT_AUTHORS) + @Request + async getHotAuthors(_request: GetHotAuthorsRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } + + /** + * 获取作者榜单 + * @param request 请求参数 + * @returns 作者榜单列表 + * @throws ApiError 当请求失败时抛出 + */ + @GET(API_ENDPOINTS.USER_RANKING) + @Request + async getAuthorRanking(_request: GetAuthorRankingRequest): Promise { + // 实际请求逻辑由Request装饰器处理 + throw new Error('Decorated by @Request and should never be executed.'); + } +} + +// 创建单例实例 +export const userApi = new UserApiService(); \ No newline at end of file diff --git a/auth/README.md b/auth/README.md deleted file mode 100644 index 4a91730..0000000 --- a/auth/README.md +++ /dev/null @@ -1,217 +0,0 @@ -# 认证模块 - -## 概述 - -认证模块提供基于Session的认证机制,支持用户登录、注册、登出和会话管理功能。该模块采用适配器模式,支持多种存储方式(Cookie、LocalStorage、内存),以适应不同的运行环境(浏览器、SSR等)。 - -## 核心组件 - -### 1. 认证服务 (AuthService) - -`AuthService` 接口定义了认证相关的核心功能: - -- `login(credentials)`: 用户登录 -- `register(userData)`: 用户注册 -- `logout()`: 用户登出 -- `isAuthenticated()`: 检查是否已认证 -- `getCurrentUser()`: 获取当前用户信息 -- `refreshSession()`: 刷新会话 -- `getSession()`: 获取会话信息 - -### 2. 会话管理器 (SessionManager) - -`SessionManager` 负责管理用户会话状态,包括: - -- 会话的创建、验证、刷新和清除 -- 用户信息的获取和缓存 -- 会话状态的持久化存储 - -### 3. 存储适配器 (StorageAdapter) - -`StorageAdapter` 提供统一的存储接口,支持多种存储方式: - -- `CookieStorageAdapter`: 使用Cookie存储,适用于SSR环境 -- `LocalStorageAdapter`: 使用浏览器LocalStorage存储 -- `MemoryStorageAdapter`: 使用内存存储,适用于测试或临时会话 - -### 4. 事件管理器 (EventManager) - -`EventManager` 负责管理认证相关事件: - -- 登录事件 (`login`) -- 注册事件 (`register`) -- 登出事件 (`logout`) -- 错误事件 (`error`) - -### 5. 类型定义 - -模块包含完整的TypeScript类型定义,确保类型安全。 - -## 使用方法 - -### 基本使用 - -```typescript -import { authService } from '@/auth'; - -// 用户登录 -const loginResult = await authService.login({ - username: 'user@example.com', - password: 'password123' -}); - -if (loginResult.success) { - console.log('登录成功', loginResult.data); -} else { - console.error('登录失败', loginResult.error); -} - -// 检查认证状态 -if (await authService.isAuthenticated()) { - // 获取当前用户信息 - const userResult = await authService.getCurrentUser(); - if (userResult.success) { - console.log('当前用户:', userResult.data); - } -} - -// 用户登出 -await authService.logout(); -``` - -### 测试环境使用 - -```typescript -import { getAuthService, resetAuthService } from '@/auth'; - -// 在测试前重置服务实例 -resetAuthService(); - -// 获取新的服务实例 -const authService = getAuthService(); -``` - -### 自定义存储方式 - -```typescript -import { createStorageAdapter } from '@/auth'; - -// 创建自定义存储适配器 -const customStorage = createStorageAdapter('localStorage'); - -// 注意:当前版本使用单例模式,自定义存储需要修改auth-service.ts中的实现 -``` - -### 使用自定义存储适配器 - -```typescript -import { createAuthService, createStorageAdapter } from '@/auth'; - -// 创建自定义Cookie存储适配器 -const cookieStorage = createStorageAdapter('cookie', { - domain: '.example.com', - secure: true, - httpOnly: false, - sameSite: 'strict' -}); - -// 使用自定义存储适配器 -const authService = createAuthService(undefined, cookieStorage); -``` - -### 事件监听 - -```typescript -import { createAuthService, createEventManager } from '@/auth'; - -const eventManager = createEventManager(); -const authService = createAuthService(); - -// 监听登录事件 -eventManager.on('login', (data) => { - console.log('用户登录:', data.user); -}); - -// 监听登出事件 -eventManager.on('logout', () => { - console.log('用户已登出'); -}); -``` - -## 会话管理 - -### 会话存储 - -会话信息通过StorageAdapter存储,默认使用Cookie存储,以支持SSR环境。会话信息包括: - -- `sessionId`: 会话ID -- `userId`: 用户ID -- `expiresAt`: 过期时间 -- `createdAt`: 创建时间 - -### 会话验证 - -SessionManager会自动验证会话的有效性: - -1. 检查本地是否存在会话ID -2. 验证会话是否过期 -3. 向服务器验证会话有效性 -4. 自动刷新过期会话 - -### 会话刷新 - -当会话即将过期时,SessionManager会自动尝试刷新会话: - -1. 使用当前会话ID请求刷新 -2. 更新本地存储的会话信息 -3. 清除缓存的用户信息,下次获取时重新请求 - -## API端点 - -认证模块需要以下API端点: - -- `POST /auth/login`: 用户登录 -- `POST /auth/register`: 用户注册 -- `POST /auth/logout`: 用户登出 -- `GET /auth/session/:sessionId`: 获取会话信息 -- `POST /auth/session/refresh`: 刷新会话 -- `GET /auth/user/:userId`: 获取用户信息 - -## 设计模式 - -认证模块采用以下设计模式: - -1. **适配器模式**: 通过StorageAdapter接口支持多种存储方式 -2. **工厂模式**: 提供createAuthService等工厂函数简化实例创建 -3. **观察者模式**: 通过EventManager实现事件发布订阅 -4. **策略模式**: 不同存储方式采用不同策略 - -## 安全考虑 - -1. **会话安全**: 使用HttpOnly Cookie存储会话ID,防止XSS攻击 -2. **CSRF防护**: 通过SameSite Cookie属性防止CSRF攻击 -3. **会话过期**: 设置合理的会话过期时间,并支持自动刷新 -4. **HTTPS**: 在生产环境中使用HTTPS传输敏感数据 - -## SSR支持 - -认证模块完全支持SSR环境: - -1. 默认使用Cookie存储,可在服务器端读取 -2. 提供统一的StorageAdapter接口,可针对不同环境实现 -3. 会话状态可在服务器和客户端之间同步 - -## 测试 - -认证模块包含完整的单元测试,覆盖: - -- 会话管理功能 -- 存储适配器功能 -- 认证服务功能 -- 错误处理 - -运行测试: - -```bash -npm test auth -``` \ No newline at end of file diff --git a/auth/auth-service.ts b/auth/auth-service.ts deleted file mode 100644 index 6e35cfc..0000000 --- a/auth/auth-service.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * 认证服务实现 - * 集成事件管理、会话管理和存储适配器功能 - * 完全实现AuthService接口定义 - */ -import type { ApiClient } from '@/api/types'; -import type { User } from '@/types'; -import type { - AuthService, - AuthEventType -} from './types'; -import type { - LoginRequest, - LoginResponse, - RegisterRequest, - RegisterResponse -} from '@/types/user'; -import { AuthError } from './errors'; -import { authEventManager } from './event-manager'; -import { createSessionManager } from './session-manager'; -import { createStorageAdapter } from './storage-adapter'; - -/** - * 默认认证服务实现 - */ -export class DefaultAuthService implements AuthService { - private readonly apiClient: ApiClient; - private readonly sessionManager: ReturnType; - private readonly storage: ReturnType; - - constructor(apiClient: ApiClient) { - this.apiClient = apiClient; - this.storage = createStorageAdapter('memory'); - this.sessionManager = createSessionManager(apiClient, this.storage); - } - - /** - * 用户登录 - * @param credentials 登录凭证 - * @returns 登录响应,包含用户信息和会话ID - */ - async login(credentials: LoginRequest): Promise { - try { - const response = await this.apiClient.post('/auth/login', credentials); - - // 触发登录成功事件 - authEventManager.emit('login', response.user); - - // 更新会话管理器中的用户信息 - await this.sessionManager.getUserInfo(); - - return response; - } catch (error) { - throw AuthError.loginFailed('Login failed', error); - } - } - - /** - * 用户注册 - * @param userData 注册数据 - * @returns 注册响应,包含用户信息和会话ID - */ - async register(userData: RegisterRequest): Promise { - try { - const response = await this.apiClient.post('/auth/register', userData); - - // 触发注册成功事件 - authEventManager.emit('register', response.user); - - // 更新会话管理器中的用户信息 - await this.sessionManager.getUserInfo(); - - return response; - } catch (error) { - throw AuthError.registerFailed('Registration failed', error); - } - } - - /** - * 用户登出 - * 调用服务器端登出API,清除会话 - */ - async logout(): Promise { - try { - // 调用服务器端登出API - await this.apiClient.post('/auth/logout'); - - // 清除本地缓存 - this.sessionManager.clearCache(); - - // 触发登出事件 - authEventManager.emit('logout'); - } catch (error) { - // 即使API调用失败,也要清除本地状态 - this.sessionManager.clearCache(); - throw AuthError.logoutFailed('Logout failed', error); - } - } - - /** - * 获取当前用户信息 - * @returns 当前用户信息 - */ - async getCurrentUser(): Promise { - try { - const user = await this.sessionManager.getUserInfo(); - if (!user) { - throw AuthError.notAuthenticated(); - } - return user; - } catch (error) { - // 如果已经是AuthError,直接抛出 - if (error instanceof AuthError) { - throw error; - } - throw AuthError.userNotFound('Failed to get current user', error); - } - } - - /** - * 检查用户是否已认证 - * @returns 是否已认证 - */ - async isAuthenticated(): Promise { - try { - return await this.sessionManager.isAuthenticated(); - } catch (error) { - // 如果已经是AuthError,直接抛出 - if (error instanceof AuthError) { - throw error; - } - throw AuthError.unknown('Authentication check failed', error); - } - } - - /** - * 添加认证事件监听器 - * @param event 事件类型 - * @param listener 事件监听器 - */ - on(event: AuthEventType, listener: (...args: unknown[]) => void): void { - authEventManager.on(event, listener); - } - - /** - * 移除认证事件监听器 - * @param event 事件类型 - * @param listener 事件监听器 - */ - off(event: AuthEventType, listener: (...args: unknown[]) => void): void { - authEventManager.off(event, listener); - } - - /** - * 清除本地缓存 - * 不影响服务器端session - */ - clearCache(): void { - this.sessionManager.clearCache(); - } -} - -/** - * 创建认证服务实例 - * @param apiClient API客户端 - * @returns 认证服务实例 - */ -export function createAuthService(apiClient: ApiClient): AuthService { - return new DefaultAuthService(apiClient); -} diff --git a/auth/errors.ts b/auth/errors.ts deleted file mode 100644 index 4f038a3..0000000 --- a/auth/errors.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 认证模块错误处理 - */ - -/** - * 认证错误类 - */ -export class AuthError extends Error { - public readonly code: string; - public readonly details: unknown; - - constructor(code: string, message: string, details?: unknown) { - super(message); - // 这个得删,还有test文件里的也要删 - this.name = 'AuthError'; - this.code = code; - this.details = details; - } - - /** - * 创建登录失败错误 - */ - static loginFailed(message: string, details?: unknown): AuthError { - return new AuthError('LOGIN_FAILED', message, details); - } - - /** - * 创建登出失败错误 - */ - static logoutFailed(message: string, details?: unknown): AuthError { - return new AuthError('LOGOUT_FAILED', message, details); - } - - /** - * 创建注册失败错误 - */ - static registerFailed(message: string, details?: unknown): AuthError { - return new AuthError('REGISTER_FAILED', message, details); - } - - /** - * 创建用户未找到错误 - */ - static userNotFound(message: string, details?: unknown): AuthError { - return new AuthError('USER_NOT_FOUND', message, details); - } - - /** - * Session过期 - */ - static sessionExpired(message: string, details?: unknown): AuthError { - return new AuthError('SESSION_EXPIRED', message, details); - } - - /** - * Session未认证 - */ - static notAuthenticated(message: string = 'User not authenticated', details?: unknown): AuthError { - return new AuthError('NOT_AUTHENTICATED', message, details); - } - - /** - * 通用未知报错 - */ - static unknown(message: string = 'Unknown authentication error', details?: unknown): AuthError { - return new AuthError('UNKNOWN', message, details); - } -} diff --git a/auth/event-manager.ts b/auth/event-manager.ts deleted file mode 100644 index 70ffd68..0000000 --- a/auth/event-manager.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * 认证事件管理器 - * 管理认证相关的事件监听和触发 - */ -import type { AuthEventType, AuthEventListener } from './types'; -import { AuthError } from './errors'; - -/** - * 认证事件管理器 - */ -export class AuthEventManager { - private listeners: Map = new Map(); - - /** - * 添加事件监听器 - */ - on(event: AuthEventType, listener: AuthEventListener): void { - if (!this.listeners.has(event)) { - this.listeners.set(event, []); - } - const eventListeners = this.listeners.get(event); - // 神人判断 - if (eventListeners) { - eventListeners.push(listener); - } - } - - /** - * 移除事件监听器 - */ - off(event: AuthEventType, listener: AuthEventListener): void { - const eventListeners = this.listeners.get(event); - if (eventListeners) { - const index = eventListeners.indexOf(listener); - if (index !== -1) { - eventListeners.splice(index, 1); - } - } - } - - /** - * 触发事件 - */ - emit(event: AuthEventType, ...args: unknown[]): void { - const eventListeners = this.listeners.get(event); - if (eventListeners) { - eventListeners.forEach(listener => { - try { - listener(...args); - } catch (error) { - throw AuthError.unknown(`Error in auth event listener for ${event}`, error); - } - }); - } - } - - /** - * 清除所有监听器 - */ - clear(): void { - this.listeners.clear(); - } -} - -// 创建全局事件管理器实例 -export const authEventManager = new AuthEventManager(); diff --git a/auth/index.ts b/auth/index.ts deleted file mode 100644 index c3c4485..0000000 --- a/auth/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * 认证模块入口文件 - */ - -// 导出类型定义 -export type { - AuthEventType, - AuthEventListener, - StorageAdapter, - AuthService, - SessionManager -} from './types'; - -// 导出实现类 -export { DefaultAuthService } from './auth-service'; -export { DefaultSessionManager } from './session-manager'; -export { AuthEventManager, authEventManager } from './event-manager'; -export { MemoryStorageAdapter, createStorageAdapter } from './storage-adapter'; - -// 导出工厂函数 -export { createAuthService } from './auth-service'; -export { createSessionManager } from './session-manager'; diff --git a/auth/session-manager.ts b/auth/session-manager.ts deleted file mode 100644 index a9fa52a..0000000 --- a/auth/session-manager.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * 会话管理器 - * 负责查询用户认证状态,不主动管理session - * session完全由服务器端控制,前端只通过API查询状态 - */ -import type { ApiClient } from '@/api/types'; -import type { User } from '@/types'; -import type { StorageAdapter } from './types'; -import { authEventManager } from './event-manager'; - -export class DefaultSessionManager { - private readonly apiClient: ApiClient; - private readonly storage: StorageAdapter; - private currentUser: User | null = null; - - constructor(apiClient: ApiClient, storage: StorageAdapter) { - this.apiClient = apiClient; - this.storage = storage; - } - - /** - * 检查是否已认证 - * 通过API调用验证用户认证状态,而不是直接访问cookie - * @returns 是否已认证 - */ - async isAuthenticated(): Promise { - try { - // 通过调用API验证认证状态 - await this.getUserInfo(); - return true; - } catch (error) { - // 如果认证失败,触发session_expired事件 - authEventManager.emit('session_expired', error); - return false; - } - } - - /** - * 获取当前用户信息 - * 幂等操作,每次都从服务器获取最新用户信息 - * @returns 用户信息 - * @throws 当获取用户信息失败时抛出错误 - */ - async getUserInfo(): Promise { - try { - // API客户端的get方法已经返回解构后的数据,不需要访问.data - const response = await this.apiClient.get<{ user: User }>('/auth/me'); - this.currentUser = response.user; - // 如果成功获取用户信息,触发session_authenticated事件 - authEventManager.emit('session_authenticated', this.currentUser); - return response.user; - } catch (error) { - this.currentUser = null; - // 如果获取用户信息失败,触发session_expired事件 - authEventManager.emit('session_expired', error); - // 抛出错误,让调用者处理 - throw error; - } - } - - /** - * 清除本地缓存 - * 不影响服务器端session,只清除前端缓存 - */ - clearCache(): void { - this.currentUser = null; - // 神人判断,没用接口 - // 清除存储适配器中的所有项(如果支持) - if ('clear' in this.storage && typeof this.storage.clear === 'function') { - this.storage.clear(); - } else { - // 如果不支持clear方法,逐个删除已知项 - const keys = ['user_preferences', 'ui_state']; // 示例键 - keys.forEach(key => this.storage.removeItem(key)); - } - // 触发session_logout事件 - authEventManager.emit('session_logout'); - } -} - -/** - * 创建会话管理器 - * @param apiClient API客户端 - * @param storage 存储适配器,仅用于非敏感数据缓存 - * @returns 会话管理器实例 - */ -export function createSessionManager( - apiClient: ApiClient, - storage: StorageAdapter -): DefaultSessionManager { - return new DefaultSessionManager(apiClient, storage); -} - diff --git a/auth/storage-adapter.ts b/auth/storage-adapter.ts deleted file mode 100644 index e9802dd..0000000 --- a/auth/storage-adapter.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 存储适配器接口 - * 提供统一的存储接口,用于非敏感数据的临时存储 - * 注意:此存储适配器不用于session管理,session完全由服务器端控制 - */ -import type { StorageAdapter } from './types'; - -/** - * 内存存储适配器 - * 适用于非敏感数据的临时存储,如UI状态、用户偏好设置等 - */ -export class MemoryStorageAdapter implements StorageAdapter { - private storage: Record = {}; - - getItem(key: string): string | null { - return this.storage[key] || null; - } - - setItem(key: string, value: string): void { - this.storage[key] = value; - } - - removeItem(key: string): void { - delete this.storage[key]; - } - - /** - * 清空所有存储项 - */ - clear(): void { - this.storage = {}; - } - - /** - * 获取所有存储键 - * @returns 存储键数组 - */ - keys(): string[] { - return Object.keys(this.storage); - } -} - -/** - * 创建存储适配器 - * @param type 存储类型,目前只支持memory - * @returns 存储适配器实例 - */ -export function createStorageAdapter(type: 'memory' = 'memory'): StorageAdapter { - switch (type) { - case 'memory': - return new MemoryStorageAdapter(); - default: - return new MemoryStorageAdapter(); - } -} diff --git a/auth/types.ts b/auth/types.ts deleted file mode 100644 index a8ae87b..0000000 --- a/auth/types.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * 认证模块类型定义 - */ - -import type { User } from '@/types'; -import type { - LoginRequest, - LoginResponse, - RegisterRequest, - RegisterResponse -} from '@/types/user'; - -// 事件类型定义 -export type AuthEventType = 'login' | 'logout' | 'register' | 'session_expired' | 'session_authenticated' | 'session_logout'; - -// 事件监听器类型(就是函数) -export type AuthEventListener = (...args: unknown[]) => void; - -// 存储适配器接口 - 仅用于非敏感数据存储,不涉及session管理 -export interface StorageAdapter { - /** - * 获取存储项 - * @param key 存储键 - * @returns 存储值或null - */ - getItem(key: string): string | null; - - /** - * 设置存储项 - * @param key 存储键 - * @param value 存储值 - */ - setItem(key: string, value: string): void; - - /** - * 删除存储项 - * @param key 存储键 - */ - removeItem(key: string): void; - - /** - * 清空所有存储项 - */ - clear(): void; - - /** - * 获取所有存储键 - * @returns 存储键数组 - */ - keys(): string[]; -} - -// 认证服务接口 -export interface AuthService { - login(credentials: LoginRequest): Promise; - register(userData: RegisterRequest): Promise; - logout(): Promise; - getCurrentUser(): Promise; - isAuthenticated(): Promise; // 改为异步,通过getUser验证 - - // 事件管理方法 - on(event: AuthEventType, listener: AuthEventListener): void; - off(event: AuthEventType, listener: AuthEventListener): void; - - // 会话管理方法 - clearCache(): void; -} - -// 会话管理器接口 - 仅作为状态查询器,不主动管理session -export interface SessionManager { - // 通过getUser验证用户认证状态,而不是直接访问cookie - isAuthenticated(): Promise; - // 获取当前用户信息,幂等操作 - getUserInfo(): Promise; - // 清除本地缓存,不影响服务器端session - clearCache(): void; -} - -// 扩展 Axios 请求配置以支持认证选项 -// 其实没必要,因为是服务器端处理过期 -declare module 'axios' { - interface AxiosRequestConfig { - skipAuth?: boolean; - } -} diff --git a/client/axios-client.ts b/client/axios-client.ts new file mode 100644 index 0000000..ee2c947 --- /dev/null +++ b/client/axios-client.ts @@ -0,0 +1,62 @@ +// axios-client.ts +import axios, { + type AxiosInstance, + type AxiosRequestConfig, + AxiosError, +} from 'axios'; + +import type { BaseRequestConfig, BaseResponseConfig } from './common'; +import { BaseError } from './common'; + +export interface HttpClient { + request(config: BaseRequestConfig): Promise; + + // 拦截器先别管 +} + +export class AxiosHttpClient implements HttpClient { + private instance: AxiosInstance; + + // 不需要工厂,defaults和request的配置是相通的,单例不会输出config参数 + constructor() { + this.instance = axios.create(); + } + + async request(config: BaseRequestConfig): Promise { + // ---- 映射层:BaseConfig → AxiosConfig ---- + const axiosConfig: AxiosRequestConfig = { + url: config.url, + method: config.method.toLowerCase(), + headers: config.headers || {}, + params: config.query || {}, + // 这里做了映射,上层可以用body + data: config.body || {}, + + // 其实可以直接放进defaults,但是为了和request的配置保持一致,就不这么做了 + timeout: config.timeout || 10000, + withCredentials: config.withCredentials || true, + }; + + // 类型化请求响应(直接赋值,避免额外的类型检查) + try { + const { data: res } = await this.instance.request>(axiosConfig); + return res.data; + } catch (error: unknown) { + // 类型断言,确保 error 是 AxiosError 类型 + if (axios.isAxiosError(error)) { + throw new BaseError({ + message: (error as AxiosError).message || '未知错误', + code: (error as AxiosError).code || '未知错误类型', + response: (error as AxiosError).response?.data as BaseResponseConfig, + }); + } + + throw new BaseError({ + message: (error as Error).message || '未知错误', + code: (error as Error).name || '未知错误类型', + }); + } + + } + +} diff --git a/client/common.ts b/client/common.ts new file mode 100644 index 0000000..0087482 --- /dev/null +++ b/client/common.ts @@ -0,0 +1,40 @@ +// common.ts —— 各协议共用的统一请求配置格式 + +export interface BaseRequestConfig { + url: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + headers?: Record; + query?: Record; // 暂时也就分页和搜索 + body?: any; + + timeout?: number; + withCredentials?: boolean; + // upload的时候处理表单要用,避免axios处理 + transformRequest?: ((data: any) => any) | ((data: any) => any)[]; +} + +// 设计Mock或者实际后端的时候也直接对应这个接口 +export interface BaseResponseConfig { + status: number; // 状态码(HTTP状态码) + msg?: string; // 报错信息(可以没有),和error不是一层语义 + data: T; +} + +/* + * 基础错误类(client层) + */ +export class BaseError extends Error { + code: string; // 错误码(字符串) + response?: BaseResponseConfig; + + constructor({ message, code, response }: { message: string; code: string; response?: BaseResponseConfig }) { + super(message); + this.name = 'BaseError'; // 补充缺失的 name 属性,会在toString()打印,ES5规范,还是遵守一下吧 + this.code = code; + if (response) { + this.response = response; + } + } +} + + diff --git a/client/index.ts b/client/index.ts new file mode 100644 index 0000000..911b06b --- /dev/null +++ b/client/index.ts @@ -0,0 +1,2 @@ +export * from './axios-client'; +export * from './common'; \ No newline at end of file diff --git a/decorators/common.ts b/decorators/common.ts new file mode 100644 index 0000000..ae74495 --- /dev/null +++ b/decorators/common.ts @@ -0,0 +1,128 @@ +import { setMeta, getMeta, type ApiMethodMeta } from './meta'; +import { AxiosHttpClient, type BaseError } from '../client'; + +// ApiError错误接口 +export interface ApiError { + status: number; // 状态码(来自response的status) + statusText: string; // 状态文本(来自response的msg) + code: string; // 错误码(字符串) + message: string; // 报错信息(字符串) + // 没有别的细节了,错误码直接对照常量映射 +} + +// 客户端请求参数接口 +export interface ClientRequestParams { + method: string; + url: string; + query?: Record; + body?: any; +} + +// 创建 AxiosHttpClient 单例实例 +const apiClientInstance = new AxiosHttpClient(); + +/** + * 创建 HTTP 方法装饰器的通用函数 + * @param method HTTP 方法类型 + * @returns 装饰器函数 + */ +export function createHttpMethodDecorator(method: ApiMethodMeta['method']) { + return function(path: string) { + return function(_target: Object, context: ClassMethodDecoratorContext) { + // 确保只应用于方法 + if (context.kind !== 'method') { + throw new Error(`HTTP method decorator (${method}) can only be applied to methods`); + } + + // 获取原始方法 + const originalMethod = _target[context.name as keyof Object] as Function; + + // 设置元数据 + setMeta(originalMethod, 'api', { + method, + path + } as ApiMethodMeta); + }; + }; +} + + +/** + * 为 API 客户端类绑定 HTTP 客户端实例的装饰器 + * @description 该装饰器对当前类注入 AxiosHttpClient 实例 + */ +export function ApiClientBound(target: T, _context: ClassDecoratorContext) { + // 定义一个新类,继承自目标类 + return class extends target { + // 在构造函数中初始化客户端实例 + constructor(...args: any[]) { + super(...args); + // 添加客户端实例 + this.client = apiClientInstance; + } + } +} + +/** + * 请求执行装饰器,自动处理 API 调用逻辑 + */ +export function Request(target: Object, context: ClassMethodDecoratorContext) { + const methodName = String(context.name); + + // 确保只应用于方法 + if (context.kind !== 'method') { + throw new Error('Request decorator can only be applied to methods'); + } + + // 保存原始方法 + const originalMethod = target[context.name as keyof Object] as Function; + + // 定义新的方法实现 + async function replacementMethod(this: { client: { request: (params: ClientRequestParams) => Promise } }, ...args: any[]) { + // 获取 API 元数据 + const meta = getMeta(originalMethod, 'api') as ApiMethodMeta | undefined; + if (!meta) { + throw new Error(`Missing API metadata on ${methodName}. Please use an HTTP method decorator (GET, POST, etc.)`); + } + + const { method, path } = meta; + + // 处理请求参数 + const params = args[0] ?? {}; + const body = args[1]; + + // 替换路径中的动态参数段 + let finalPath = path.replace(/:([\w]+)/g, (_, key) => { + const paramValue = params[key]; + if (paramValue === undefined) { + throw new Error(`Missing required path parameter '${key}' for API endpoint '${path}'`); + } + return String(paramValue); + }); + + // 执行请求并处理错误 + try { + return await this.client.request({ + method, + url: finalPath, + query: params, + body, + }); + } catch (error: BaseError | any) { + // 确保错误对象符合 ApiError 接口 + const apiError: ApiError = { + status: error.response?.status || 500, + statusText: error.response?.msg || 'Internal Server Error', + code: error?.code || 'UNKNOWN_ERROR', + message: error?.message || 'An unexpected error occurred', + }; + throw apiError; + } + } + + // TS5+直接返回新方法定义 + return replacementMethod; +} + + + diff --git a/decorators/http-methods.ts b/decorators/http-methods.ts new file mode 100644 index 0000000..1931229 --- /dev/null +++ b/decorators/http-methods.ts @@ -0,0 +1,41 @@ +import { createHttpMethodDecorator } from './common'; + +/** + * GET 请求装饰器 + * @param path API 路径,支持 :param 格式的动态参数 + */ +export function GET(path: string) { + return createHttpMethodDecorator('GET')(path); +} + +/** + * POST 请求装饰器 + * @param path API 路径,支持 :param 格式的动态参数 + */ +export function POST(path: string) { + return createHttpMethodDecorator('POST')(path); +} + +/** + * PUT 请求装饰器 + * @param path API 路径,支持 :param 格式的动态参数 + */ +export function PUT(path: string) { + return createHttpMethodDecorator('PUT')(path); +} + +/** + * DELETE 请求装饰器 + * @param path API 路径,支持 :param 格式的动态参数 + */ +export function DELETE(path: string) { + return createHttpMethodDecorator('DELETE')(path); +} + +/** + * PATCH 请求装饰器 + * @param path API 路径,支持 :param 格式的动态参数 + */ +export function PATCH(path: string) { + return createHttpMethodDecorator('PATCH')(path); +} diff --git a/decorators/index.ts b/decorators/index.ts new file mode 100644 index 0000000..d65b95a --- /dev/null +++ b/decorators/index.ts @@ -0,0 +1,2 @@ +export * from './http-methods'; +export * from './common'; diff --git a/decorators/meta.ts b/decorators/meta.ts new file mode 100644 index 0000000..f1fe3e8 --- /dev/null +++ b/decorators/meta.ts @@ -0,0 +1,35 @@ +/** + * API 方法元数据接口 + */ +export interface ApiMethodMeta { + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + path: string; +} + +// 使用 WeakMap 存储元数据,避免内存泄漏 +const metaStore = new WeakMap>(); + +/** + * 设置函数的元数据 + * @param target 目标函数 + * @param key 元数据键名 + * @param value 元数据值 + */ +export function setMeta(target: Function, _key: string, value: ApiMethodMeta): void { + let map = metaStore.get(target); + if (!map) { + map = new Map(); + metaStore.set(target, map); + } + map.set(_key, value); +} + +/** + * 获取函数的元数据 + * @param target 目标函数 + * @param key 元数据键名 + * @returns 元数据值或undefined + */ +export function getMeta(target: Function, key: string): ApiMethodMeta | undefined { + return metaStore.get(target)?.get(key); +} \ No newline at end of file diff --git a/index.ts b/index.ts index 97e352b..fe5ff8d 100644 --- a/index.ts +++ b/index.ts @@ -4,8 +4,5 @@ export * from './types'; // 导出API客户端和模块 export * from './api'; -// 导出鉴权相关功能 -export * from './auth'; - // 导出工具函数 export * from './utils'; diff --git a/package.json b/package.json index 480bc31..00f62e1 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,12 @@ { - "name": "knowai-core", - "version": "1.0.0", + "name": "@knowai/core", + "version": "0.1.0", "type": "module", - "description": "负责准备视觉层会用到的逻辑函数、常用工具函数封装以及鉴权函数封装。负责维护统一接口。", + "description": "Knowai核心库,提供API服务、客户端、装饰器、类型定义等。", + "files": [ + "dist" + ], + "publishConfig": { "access": "public" }, "exports": { ".": { "import": "./dist/index.js", diff --git a/test/README.md b/test/README.md deleted file mode 100644 index a48faa0..0000000 --- a/test/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# 认证架构测试 - -本目录包含了对新认证架构的全面测试套件,验证SessionManager、AccessTokenManager、SessionAuthService、ExtendedApiClient和兼容性处理的功能和安全性。 - -## 测试覆盖范围 - -### SessionManager测试 -- 认证状态检查 -- Session信息获取 -- Session刷新逻辑 -- Session清除 -- 用户角色检查 - -### AccessTokenManager测试(已废弃) -- Token生成 -- Token获取和缓存 -- Token刷新 -- Token清除 -- 过期Token处理 - -### SessionAuthService测试 -- 登录/注册/登出功能 -- 请求/响应拦截器 -- 事件处理 -- 错误处理 - -### ExtendedApiClient测试 -- 带Access Token的请求 -- 各种HTTP方法支持 -- Token注入 -- 错误处理 - -### 兼容性处理测试(已废弃) -- 认证模式切换 -- Token到Session的迁移 -- 混合认证模式 - -### 集成测试(并没有) -- 端到端认证流程 -- API请求流程 -- 事件处理 -- 错误处理 - -## 测试环境 - -测试使用Vitest框架,配置了以下环境: -- 测试环境:jsdom -- 全局设置:启用 -- 超时时间:10秒 -- 覆盖率提供者:v8 - -## 模拟对象 - -测试中使用了以下模拟对象: -- ApiClient:模拟HTTP客户端 -- SessionManager:模拟Session管理器 -- AccessTokenManager:模拟Access Token管理器(已废弃) -- AuthEventManager:模拟认证事件管理器 -- TokenManager:模拟Token管理器(已废弃) - -## 测试数据 - -测试使用以下模拟数据: -- 用户信息 -- Session信息 -- Access Token信息(已废弃) -- API响应数据 - -## 断言 - -测试使用以下断言: -- 功能正确性 -- 错误处理 -- 事件触发 -- 数据转换 -- 安全性检查 - diff --git a/test/auth/session-manager.test.ts b/test/auth/session-manager.test.ts deleted file mode 100644 index 825ced6..0000000 --- a/test/auth/session-manager.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * SessionManager测试用例 - * 验证SessionManager的功能和安全性 - */ - -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { DefaultSessionManager, createSessionManager } from '../../auth/session-manager'; -import { MemoryStorageAdapter } from '../../auth/storage-adapter'; -import type { User } from '@/types'; -import type { ApiClient } from '@/api/types'; - -// 模拟ApiClient -const mockApiClient = { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - patch: vi.fn(), - request: vi.fn(), - addRequestInterceptor: vi.fn(), - addResponseInterceptor: vi.fn(), - removeRequestInterceptor: vi.fn(), - removeResponseInterceptor: vi.fn(), - setDefaults: vi.fn(), - setBaseURL: vi.fn(), - createInstance: vi.fn() -} as unknown as ApiClient; - -describe('SessionManager', () => { - let sessionManager: DefaultSessionManager; - let mockStorage: MemoryStorageAdapter; - - beforeEach(() => { - vi.clearAllMocks(); - mockStorage = new MemoryStorageAdapter(); - sessionManager = new DefaultSessionManager(mockApiClient, mockStorage); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('isAuthenticated', () => { - it('当用户认证成功时返回true', async () => { - // 模拟API返回用户信息 - const mockUser: User = { - id: 'user-123', - username: 'testuser', - }; - - mockApiClient.get.mockResolvedValue({ user: mockUser }); - - // 验证认证状态 - const result = await sessionManager.isAuthenticated(); - expect(result).toBe(true); - expect(mockApiClient.get).toHaveBeenCalledWith('/auth/me'); - }); - - it('当用户认证失败时返回false', async () => { - // 模拟API返回错误 - mockApiClient.get.mockRejectedValue(new Error('Unauthorized')); - - // 验证认证状态 - const result = await sessionManager.isAuthenticated(); - expect(result).toBe(false); - expect(mockApiClient.get).toHaveBeenCalledWith('/auth/me'); - }); - }); - - describe('getUserInfo', () => { - it('当用户认证成功时返回用户信息', async () => { - // 模拟API返回用户信息 - const mockUser: User = { - id: 'user-123', - username: 'testuser', - email: 'test@example.com', - avatar: '', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; - - mockApiClient.get.mockResolvedValue({ user: mockUser }); - - // 获取用户信息 - const user = await sessionManager.getUserInfo(); - expect(user).toEqual(mockUser); - expect(mockApiClient.get).toHaveBeenCalledWith('/auth/me'); - }); - - it('当用户认证失败时抛出错误', async () => { - // 模拟API返回错误 - mockApiClient.get.mockRejectedValue(new Error('Unauthorized')); - - // 获取用户信息应该抛出错误 - await expect(sessionManager.getUserInfo()).rejects.toThrow('Unauthorized'); - expect(mockApiClient.get).toHaveBeenCalledWith('/auth/me'); - }); - - it('每次调用都从服务器获取最新用户信息', async () => { - // 模拟API返回用户信息 - const mockUser: User = { - id: 'user-123', - username: 'testuser', - email: 'test@example.com', - avatar: '', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; - - mockApiClient.get.mockResolvedValue({ user: mockUser }); - - // 第一次调用 - const user1 = await sessionManager.getUserInfo(); - expect(user1).toEqual(mockUser); - expect(mockApiClient.get).toHaveBeenCalledTimes(1); - - // 第二次调用应该再次调用API,因为getUserInfo是幂等操作 - const user2 = await sessionManager.getUserInfo(); - expect(user2).toEqual(mockUser); - expect(mockApiClient.get).toHaveBeenCalledTimes(2); // 有额外调用 - }); - }); - - describe('clearCache', () => { - it('应该清除缓存', () => { - // 设置一些存储项 - mockStorage.setItem('user_preferences', '{"theme":"dark"}'); - mockStorage.setItem('ui_state', '{"sidebarOpen":true}'); - - // 验证存储项存在 - expect(mockStorage.getItem('user_preferences')).toBe('{"theme":"dark"}'); - expect(mockStorage.getItem('ui_state')).toBe('{"sidebarOpen":true}'); - - // 清除缓存 - sessionManager.clearCache(); - - // 验证存储项已清除 - expect(mockStorage.getItem('user_preferences')).toBeNull(); - expect(mockStorage.getItem('ui_state')).toBeNull(); - }); - }); - - describe('createSessionManager', () => { - it('创建SessionManager实例', () => { - const newSessionManager = createSessionManager(mockApiClient, mockStorage); - expect(newSessionManager).toBeInstanceOf(DefaultSessionManager); - }); - }); -}); \ No newline at end of file diff --git a/test/mocks/data-factory.ts b/test/mocks/data-factory.ts deleted file mode 100644 index afc77c9..0000000 --- a/test/mocks/data-factory.ts +++ /dev/null @@ -1,702 +0,0 @@ -/** - * Mock数据工厂 - * 用于生成测试中使用的各种模拟数据 - */ - -import type { - User, - UserProfile, - LoginRequest, - LoginResponse, - RegisterRequest, - RegisterResponse, - RefreshTokenRequest, - RefreshTokenResponse, - UserProfileUpdateRequest, - UserProfileUpdateResponse, - UserFollowRequest, - UserFollowResponse -} from '@/types/user'; -import type { - Post, - PostComment, - CreatePostRequest, - CreatePostResponse, - GetPostsRequest, - GetPostsResponse, - GetPostRequest, - GetPostResponse, - LikePostRequest, - LikePostResponse, - BookmarkPostRequest, - BookmarkPostResponse, - CreateCommentRequest, - CreateCommentResponse, - GetCommentsRequest, - GetCommentsResponse, - LikeCommentRequest, - LikeCommentResponse -} from '@/types/post'; -import type { - AIModel, - ModelComment, - GetAIPlazaRequest, - GetAIPlazaResponse, - GetModelDetailRequest, - GetModelDetailResponse, - GetModelCommentsRequest, - GetModelCommentsResponse, - CreateModelCommentRequest, - CreateModelCommentResponse, - LikeModelCommentRequest, - LikeModelCommentResponse -} from '@/types/model'; -import type { - ChatSession, - ChatMessage, - CreateChatSessionRequest, - UpdateChatSessionRequest, - SendMessageRequest, - GetChatSessionsRequest, - GetChatSessionsResponse, - GetChatMessagesRequest, - GetChatMessagesResponse, - MarkMessagesAsReadRequest, - MarkMessagesAsReadResponse -} from '@/types/chat'; -import type { ApiResponse, ApiRequestConfig } from '@/types'; - -// 生成模拟用户数据 -export function createMockUser(overrides: Partial = {}): User { - return { - id: 'user-123', - username: 'testuser', - ...overrides - }; -} - -// 生成模拟用户档案数据 -export function createMockUserProfile(overrides: Partial = {}): UserProfile { - return { - user: createMockUser(), - avatar: 'https://example.com/avatar.jpg', - introduction: 'Test user introduction', - level: 'advanced', - ...overrides - }; -} - -// 生成模拟登录请求数据 -export function createMockLoginRequest(overrides: Partial = {}): LoginRequest { - return { - username: 'testuser', - password: 'password123', - ...overrides - }; -} - -// 生成模拟登录响应数据 -export function createMockLoginResponse(overrides: Partial = {}): LoginResponse { - return { - user: createMockUser(), - avatar: 'https://example.com/avatar.jpg', - token: 'mock-jwt-token', - expiresIn: 3600, - refreshToken: 'mock-refresh-token', - ...overrides - }; -} - -// 生成模拟注册请求数据 -export function createMockRegisterRequest(overrides: Partial = {}): RegisterRequest { - return { - username: 'newuser', - password: 'password123', - ...overrides - }; -} - -// 生成模拟注册响应数据 -export function createMockRegisterResponse(overrides: Partial = {}): RegisterResponse { - return { - user: createMockUser({ username: 'newuser' }), - avatar: 'https://example.com/avatar.jpg', - token: 'mock-jwt-token', - expiresIn: 3600, - refreshToken: 'mock-refresh-token', - ...overrides - }; -} - -// 生成模拟刷新令牌请求数据 -export function createMockRefreshTokenRequest(overrides: Partial = {}): RefreshTokenRequest { - return { - refreshToken: 'mock-refresh-token', - ...overrides - }; -} - -// 生成模拟刷新令牌响应数据 -export function createMockRefreshTokenResponse(overrides: Partial = {}): RefreshTokenResponse { - return { - token: 'new-mock-jwt-token', - expiresIn: 3600, - refreshToken: 'new-mock-refresh-token', - ...overrides - }; -} - -// 生成模拟用户档案更新请求数据 -export function createMockUserProfileUpdateRequest(overrides: Partial = {}): UserProfileUpdateRequest { - return { - avatar: 'https://example.com/new-avatar.jpg', - introduction: 'Updated introduction', - level: 'expert', - ...overrides - }; -} - -// 生成模拟用户档案更新响应数据 -export function createMockUserProfileUpdateResponse(overrides: Partial = {}): UserProfileUpdateResponse { - return { - profile: createMockUserProfile({ - avatar: 'https://example.com/new-avatar.jpg', - introduction: 'Updated introduction', - level: 'expert' - }), - ...overrides - }; -} - -// 生成模拟用户关注请求数据 -export function createMockUserFollowRequest(overrides: Partial = {}): UserFollowRequest { - return { - userId: 'user-456', - ...overrides - }; -} - -// 生成模拟用户关注响应数据 -export function createMockUserFollowResponse(overrides: Partial = {}): UserFollowResponse { - return { - success: true, - ...overrides - }; -} - -// 生成模拟帖子数据 -export function createMockPost(overrides: Partial = {}): Post { - const now = new Date(); - return { - id: 'post-123', - createdAt: now, - updatedAt: now, - title: 'Test Post Title', - excerpt: 'This is a test post excerpt', - tags: ['test', 'mock'], - stars: 5, - likes: 10, - comments: 2, - type: 'article', - authorId: 'user-123', - author: { user: createMockUser() }, - images: ['https://example.com/image.jpg'], - publishedAt: now, - ...overrides - }; -} - -// 生成模拟创建帖子请求数据 -export function createMockCreatePostRequest(overrides: Partial = {}): CreatePostRequest { - return { - title: 'New Test Post', - excerpt: 'This is a new test post excerpt', - tags: ['new', 'test'], - stars: 0, - likes: 0, - comments: 0, - type: 'article', - images: ['https://example.com/new-image.jpg'], - publishedAt: new Date(), - ...overrides - }; -} - -// 生成模拟创建帖子响应数据 -export function createMockCreatePostResponse(overrides: Partial = {}): CreatePostResponse { - return { - post: createMockPost(), - ...overrides - }; -} - -// 生成模拟获取帖子列表请求数据 -export function createMockGetPostsRequest(overrides: Partial = {}): GetPostsRequest { - return { - page: 1, - limit: 10, - sortBy: 'createdAt', - type: 'article', - sortOrder: 'desc', - authorId: 'user-123', - search: 'test query', - ...overrides - }; -} - -// 生成模拟获取帖子列表响应数据 -export function createMockGetPostsResponse(overrides: Partial = {}): GetPostsResponse { - return { - data: [createMockPost(), createMockPost({ id: 'post-456' })], - total: 2, - page: 1, - limit: 10, - hasMore: false, - sortBy: 'createdAt', - ...overrides - }; -} - -// 生成模拟获取帖子详情请求数据 -export function createMockGetPostRequest(overrides: Partial = {}): GetPostRequest { - return { - postId: 'post-123', - ...overrides - }; -} - -// 生成模拟获取帖子详情响应数据 -export function createMockGetPostResponse(overrides: Partial = {}): GetPostResponse { - return { - post: createMockPost(), - ...overrides - }; -} - -// 生成模拟点赞帖子请求数据 -export function createMockLikePostRequest(overrides: Partial = {}): LikePostRequest { - return { - postId: 'post-123', - ...overrides - }; -} - -// 生成模拟点赞帖子响应数据 -export function createMockLikePostResponse(overrides: Partial = {}): LikePostResponse { - return { - success: true, - ...overrides - }; -} - -// 生成模拟收藏帖子请求数据 -export function createMockBookmarkPostRequest(overrides: Partial = {}): BookmarkPostRequest { - return { - postId: 'post-123', - ...overrides - }; -} - -// 生成模拟收藏帖子响应数据 -export function createMockBookmarkPostResponse(overrides: Partial = {}): BookmarkPostResponse { - return { - success: true, - ...overrides - }; -} - -// 生成模拟帖子评论数据 -export function createMockPostComment(overrides: Partial = {}): PostComment { - const now = new Date(); - return { - id: 'comment-123', - createdAt: now, - updatedAt: now, - authorId: 'user-456', - author: { user: createMockUser({ id: 'user-456', username: 'commenter' }) }, - content: 'This is a test comment', - parentId: 'parent-comment-123', - ...overrides - }; -} - -// 生成模拟创建评论请求数据 -export function createMockCreateCommentRequest(overrides: Partial = {}): CreateCommentRequest { - return { - postId: 'post-123', - content: 'This is a new test comment', - parentId: 'parent-comment-123', - ...overrides - }; -} - -// 生成模拟创建评论响应数据 -export function createMockCreateCommentResponse(overrides: Partial = {}): CreateCommentResponse { - return { - comment: createMockPostComment(), - ...overrides - }; -} - -// 生成模拟获取评论列表请求数据 -export function createMockGetCommentsRequest(overrides: Partial = {}): GetCommentsRequest { - return { - page: 1, - limit: 10, - sortOrder: 'desc', - postId: 'post-123', - parentId: 'parent-comment-123', - sortBy: 'createdAt', - ...overrides - }; -} - -// 生成模拟获取评论列表响应数据 -export function createMockGetCommentsResponse(overrides: Partial = {}): GetCommentsResponse { - return { - data: [createMockPostComment(), createMockPostComment({ id: 'comment-456' })], - total: 2, - page: 1, - limit: 10, - hasMore: false, - ...overrides - }; -} - -// 生成模拟点赞评论请求数据 -export function createMockLikeCommentRequest(overrides: Partial = {}): LikeCommentRequest { - return { - commentId: 'comment-123', - ...overrides - }; -} - -// 生成模拟点赞评论响应数据 -export function createMockLikeCommentResponse(overrides: Partial = {}): LikeCommentResponse { - return { - success: true, - ...overrides - }; -} - -// 生成模拟AI模型数据 -export function createMockAIModel(overrides: Partial = {}): AIModel { - return { - id: 'model-123', - name: 'Test AI Model', - description: 'This is a test AI model description', - avatar: 'https://example.com/model-avatar.jpg', - tags: ['test', 'ai', 'model'], - website: 'https://example.com/model-website', - clickCount: 1000, - likeCount: 50, - ...overrides - }; -} - -// 生成模拟AI模型评论数据 -export function createMockModelComment(overrides: Partial = {}): ModelComment { - const now = new Date(); - return { - id: 'model-comment-123', - createdAt: now, - updatedAt: now, - modelId: 'model-123', - authorId: 'user-456', - author: { user: createMockUser({ id: 'user-456', username: 'reviewer' }) }, - content: 'This is a test model review', - parentId: 'parent-comment-123', - stats: { - stars: 5, - likes: 10, - comments: 2, - replies: 1 - }, - ...overrides - }; -} - -// 生成模拟获取AI模型广场请求数据 -export function createMockGetAIPlazaRequest(overrides: Partial = {}): GetAIPlazaRequest { - return { - ...overrides - }; -} - -// 生成模拟获取AI模型广场响应数据 -export function createMockGetAIPlazaResponse(overrides: Partial = {}): GetAIPlazaResponse { - return { - models: [createMockAIModel(), createMockAIModel({ id: 'model-456' })], - hotRankings: [ - { model: createMockAIModel(), rank: 1, change: 'up' }, - { model: createMockAIModel({ id: 'model-456' }), rank: 2, change: 'same' } - ], - clickRankings: [ - { model: createMockAIModel(), rank: 1, change: 'down' }, - { model: createMockAIModel({ id: 'model-456' }), rank: 2, change: 'up' } - ], - ...overrides - }; -} - -// 生成模拟获取模型详情请求数据 -export function createMockGetModelDetailRequest(overrides: Partial = {}): GetModelDetailRequest { - return { - modelId: 'model-123', - page: 1, - limit: 10, - sortBy: 'createdAt', - ...overrides - }; -} - -// 生成模拟获取模型详情响应数据 -export function createMockGetModelDetailResponse(overrides: Partial = {}): GetModelDetailResponse { - return { - model: createMockAIModel(), - comments: [createMockModelComment(), createMockModelComment({ id: 'model-comment-456' })], - totalComments: 2, - hasMoreComments: false, - ...overrides - }; -} - -// 生成模拟获取模型评论请求数据 -export function createMockGetModelCommentsRequest(overrides: Partial = {}): GetModelCommentsRequest { - return { - modelId: 'model-123', - page: 1, - limit: 10, - sortBy: 'createdAt', - parentId: 'parent-comment-123', - ...overrides - }; -} - -// 生成模拟获取模型评论响应数据 -export function createMockGetModelCommentsResponse(overrides: Partial = {}): GetModelCommentsResponse { - return { - comments: [createMockModelComment(), createMockModelComment({ id: 'model-comment-456' })], - total: 2, - page: 1, - limit: 10, - hasMore: false, - ...overrides - }; -} - -// 生成模拟创建模型评论请求数据 -export function createMockCreateModelCommentRequest(overrides: Partial = {}): CreateModelCommentRequest { - return { - modelId: 'model-123', - content: 'This is a new test model review', - parentId: 'parent-comment-123', - ...overrides - }; -} - -// 生成模拟创建模型评论响应数据 -export function createMockCreateModelCommentResponse(overrides: Partial = {}): CreateModelCommentResponse { - return { - comment: createMockModelComment(), - ...overrides - }; -} - -// 生成模拟点赞模型评论请求数据 -export function createMockLikeModelCommentRequest(overrides: Partial = {}): LikeModelCommentRequest { - return { - commentId: 'model-comment-123', - ...overrides - }; -} - -// 生成模拟点赞模型评论响应数据 -export function createMockLikeModelCommentResponse(overrides: Partial = {}): LikeModelCommentResponse { - return { - success: true, - ...overrides - }; -} - -// 生成模拟聊天会话数据 -export function createMockChatSession(overrides: Partial = {}): ChatSession { - const now = new Date(); - const user1 = createMockUser({ id: 'user-1', username: 'user1' }); - const user2 = createMockUser({ id: 'user-2', username: 'user2' }); - - return { - id: 'session-123', - participant1Id: 'user-1', - participant2Id: 'user-2', - participant1: user1, - participant2: user2, - lastMessage: { - id: 'message-123', - content: 'This is the last message', - senderId: 'user-1', - createdAt: now - }, - unreadCount1: 0, - unreadCount2: 1, - createdAt: now, - updatedAt: now, - metadata: { - backgroundColor: '#f0f0f0', - customProperty: 'test-value' - }, - ...overrides - }; -} - -// 生成模拟聊天消息数据 -export function createMockChatMessage(overrides: Partial = {}): ChatMessage { - const now = new Date(); - const sender = createMockUser({ id: 'user-1', username: 'sender' }); - const receiver = createMockUser({ id: 'user-2', username: 'receiver' }); - - return { - id: 'message-123', - sessionId: 'session-123', - sender, - receiver, - content: 'This is a test message', - type: 'text', - status: 'sent', - createdAt: now, - metadata: { - fileName: 'test-file.txt', - fileSize: 1024, - duration: 30, - thumbnail: 'https://example.com/thumbnail.jpg' - }, - ...overrides - }; -} - -// 生成模拟创建聊天会话请求数据 -export function createMockCreateChatSessionRequest(overrides: Partial = {}): CreateChatSessionRequest { - return { - participantId: 'user-456', - ...overrides - }; -} - -// 生成模拟更新聊天会话请求数据 -export function createMockUpdateChatSessionRequest(overrides: Partial = {}): UpdateChatSessionRequest { - return { - sessionId: 'session-123', - ...overrides - }; -} - -// 生成模拟发送消息请求数据 -export function createMockSendMessageRequest(overrides: Partial = {}): SendMessageRequest { - return { - sessionId: 'session-123', - content: 'This is a new test message', - type: 'text', - metadata: { - fileName: 'new-file.txt', - fileSize: 2048 - }, - ...overrides - }; -} - -// 生成模拟获取聊天会话列表请求数据 -export function createMockGetChatSessionsRequest(overrides: Partial = {}): GetChatSessionsRequest { - return { - page: 1, - limit: 10, - ...overrides - }; -} - -// 生成模拟获取聊天会话列表响应数据 -export function createMockGetChatSessionsResponse(overrides: Partial = {}): GetChatSessionsResponse { - return { - sessions: [createMockChatSession(), createMockChatSession({ id: 'session-456' })], - total: 2, - page: 1, - limit: 10, - ...overrides - }; -} - -// 生成模拟获取聊天消息请求数据 -export function createMockGetChatMessagesRequest(overrides: Partial = {}): GetChatMessagesRequest { - return { - sessionId: 'session-123', - page: 1, - limit: 10, - before: 'message-456', - after: 'message-123', - ...overrides - }; -} - -// 生成模拟获取聊天消息响应数据 -export function createMockGetChatMessagesResponse(overrides: Partial = {}): GetChatMessagesResponse { - return { - messages: [createMockChatMessage(), createMockChatMessage({ id: 'message-456' })], - total: 2, - page: 1, - limit: 10, - hasMore: false, - ...overrides - }; -} - -// 生成模拟标记消息已读请求数据 -export function createMockMarkMessagesAsReadRequest(overrides: Partial = {}): MarkMessagesAsReadRequest { - return { - sessionId: 'session-123', - messageIds: ['message-123', 'message-456'], - ...overrides - }; -} - -// 生成模拟标记消息已读响应数据 -export function createMockMarkMessagesAsReadResponse(overrides: Partial = {}): MarkMessagesAsReadResponse { - return { - success: true, - markedMessageIds: ['message-123', 'message-456'], - failedMessageIds: ['message-789'], - ...overrides - }; -} - -// 生成模拟API响应 -export function createMockApiResponse(data: T, overrides: Partial> = {}): ApiResponse { - return { - success: true, - data, - message: 'Success', - code: 200, - ...overrides - }; -} - -// 生成模拟API请求配置 -export function createMockApiRequestConfig(overrides: Partial = {}): ApiRequestConfig { - return { - url: '/api/test', - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - timeout: 10000, - ...overrides - }; -} - -// 生成模拟错误响应 -export function createMockErrorResponse(message: string, code: number = 400) { - return { - success: false, - data: null, - message, - code - }; -} diff --git a/test/mocks/http-client.ts b/test/mocks/http-client.ts deleted file mode 100644 index cd1fb41..0000000 --- a/test/mocks/http-client.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * HTTP客户端Mock - * 用于模拟axios行为 - */ - -import vi from 'vitest'; -import type { AxiosResponse, AxiosError } from 'axios'; -import { createMockApiResponse, createMockErrorResponse } from './data-factory'; - -// 模拟axios响应 -export function createMockAxiosResponse(data: T, status = 200): AxiosResponse { - return { - data, - status, - statusText: 'OK', - headers: {}, - config: {} as any - }; -} - -// 模拟axios错误 -export function createMockAxiosError(message: string, code = 'ERR_BAD_REQUEST', status = 400): AxiosError { - const error = new Error(message) as AxiosError; - error.code = code; - error.response = { - data: createMockErrorResponse(message, status), - status, - statusText: 'Bad Request', - headers: {}, - config: {} as any - }; - return error; -} - -// 创建模拟的axios实例 -export function createMockAxios() { - return { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - patch: vi.fn(), - request: vi.fn(), - interceptors: { - request: { - use: vi.fn(), - eject: vi.fn() - }, - response: { - use: vi.fn(), - eject: vi.fn() - } - }, - defaults: { - headers: { - common: {}, - get: {}, - post: {}, - put: {}, - delete: {} - } - } - }; -} - -// 模拟成功响应 -export function mockSuccessResponse(data: T) { - return Promise.resolve(createMockAxiosResponse(createMockApiResponse(data))); -} - -// 模拟失败响应 -export function mockErrorResponse(message: string, status = 400) { - return Promise.reject(createMockAxiosError(message, 'ERR_BAD_REQUEST', status)); -} diff --git a/test/mocks/index.ts b/test/mocks/index.ts deleted file mode 100644 index 0693f68..0000000 --- a/test/mocks/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 测试模拟工具导出 - */ - -export * from './data-factory'; -export * from './http-client'; -export * from './storage'; \ No newline at end of file diff --git a/test/mocks/storage.ts b/test/mocks/storage.ts deleted file mode 100644 index cfcea92..0000000 --- a/test/mocks/storage.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 测试用的模拟存储适配器 - */ - -import { vi } from 'vitest'; -import type { StorageAdapter } from '@/auth/storage-adapter'; - -/** - * 创建模拟的存储适配器 - * @returns 模拟的存储适配器实例 - */ -export const createMockStorage = (): StorageAdapter => { - const storage: Record = {}; - return { - getItem: vi.fn((key: string) => storage[key] || null), - setItem: vi.fn((key: string, value: string) => { - storage[key] = value; - }), - removeItem: vi.fn((key: string) => { - delete storage[key]; - }) - }; -}; \ No newline at end of file diff --git a/test/setup.ts b/test/setup.ts deleted file mode 100644 index eef0028..0000000 --- a/test/setup.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * 测试设置文件 - * 配置全局测试环境和共享设置 - */ - -import { vi } from 'vitest'; - -// 模拟浏览器API(其实用不到) -Object.defineProperty(window, 'localStorage', { - value: { - getItem: vi.fn(), - setItem: vi.fn(), - removeItem: vi.fn(), - clear: vi.fn() - }, - writable: true -}); - -Object.defineProperty(window, 'sessionStorage', { - value: { - getItem: vi.fn(), - setItem: vi.fn(), - removeItem: vi.fn(), - clear: vi.fn() - }, - writable: true -}); - -// 模拟IntersectionObserver(其实用不到) -globalThis.IntersectionObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn() -})); - -// 模拟ResizeObserver(其实用不到) -globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn() -})); - -// 模拟requestAnimationFrame(其实用不到) -globalThis.requestAnimationFrame = vi.fn((cb: FrameRequestCallback) => setTimeout(cb, 0)); - -// 模拟cancelAnimationFrame(其实用不到) -globalThis.cancelAnimationFrame = vi.fn((id: number) => clearTimeout(id)); - -// 模拟fetch -globalThis.fetch = vi.fn(); - -// 设置环境变量 -process.env.NODE_ENV = 'test'; diff --git a/test/unit/api/client.test.ts b/test/unit/api/client.test.ts deleted file mode 100644 index 8cf1778..0000000 --- a/test/unit/api/client.test.ts +++ /dev/null @@ -1,464 +0,0 @@ -/** - * API客户端测试 - * 测试API客户端的核心功能,包括请求、响应处理、拦截器管理等 - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { createMockAxiosResponse, createMockAxiosError } from '@/test/mocks/http-client'; -import { createMockApiRequestConfig } from '@/test/mocks/data-factory'; -import axios from 'axios'; - -// 使用vi.hoisted提升createMockAxios,避免初始化顺序问题 -const { createMockAxios } = vi.hoisted(() => { - let interceptorIdCounter = 0; - - const mockAxiosInstance = { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - patch: vi.fn(), - request: vi.fn(), - interceptors: { - request: { - use: vi.fn(() => ++interceptorIdCounter), - eject: vi.fn() - }, - response: { - use: vi.fn(() => ++interceptorIdCounter), - eject: vi.fn() - } - }, - defaults: { - headers: { - common: {}, - get: {}, - post: {}, - put: {}, - delete: {} - } - } - }; - - return { createMockAxios: () => mockAxiosInstance }; -}); - -// 模拟axios模块 -vi.mock('axios', () => ({ - default: { - create: vi.fn(() => createMockAxios()) - }, - AxiosError: class extends Error { - code?: string; - config?: any; - request?: any; - response?: any; - isAxiosError?: boolean; - - constructor(message: string, code?: string, config?: any, request?: any, response?: any) { - super(message); - this.name = 'AxiosError'; - this.code = code; - this.config = config; - this.request = request; - this.response = response; - this.isAxiosError = true; - } - } -})); - -// 在模拟设置后导入createApiClient -import { createApiClient } from '@/api/client'; - -// 获取模拟的axios类型 -type MockedAxios = ReturnType & { - get: ReturnType; - post: ReturnType; - put: ReturnType; - delete: ReturnType; - patch: ReturnType; - request: ReturnType; - interceptors: { - request: { - use: ReturnType; - eject: ReturnType; - }; - response: { - use: ReturnType; - eject: ReturnType; - }; - }; - defaults: { - timeout?: number; - baseURL?: string; - }; -}; - -describe('API客户端', () => { - let apiClient: ReturnType; - let mockAxios: MockedAxios; - - beforeEach(() => { - // 重置所有模拟 - vi.clearAllMocks(); - - // 创建新的API客户端实例 - apiClient = createApiClient(); - - // 获取模拟的axios实例 - mockAxios = vi.mocked(axios).create() as MockedAxios; - }); - - describe('基础请求方法', () => { - it('应该能够发送GET请求', async () => { - const mockData = { id: 1, name: 'Test' }; - mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData)); - - const result = await apiClient.get('/test'); - - expect(mockAxios.request).toHaveBeenCalledWith({ method: 'GET', url: '/test' }); - expect(result).toEqual(mockData); - }); - - it('应该能够发送POST请求', async () => { - const mockData = { id: 1, name: 'Test' }; - const requestData = { name: 'New Item' }; - mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData)); - - const result = await apiClient.post('/test', requestData); - - expect(mockAxios.request).toHaveBeenCalledWith({ method: 'POST', url: '/test', data: requestData }); - expect(result).toEqual(mockData); - }); - - it('应该能够发送PUT请求', async () => { - const mockData = { id: 1, name: 'Updated Test' }; - const requestData = { name: 'Updated Item' }; - mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData)); - - const result = await apiClient.put('/test/1', requestData); - - expect(mockAxios.request).toHaveBeenCalledWith({ method: 'PUT', url: '/test/1', data: requestData }); - expect(result).toEqual(mockData); - }); - - it('应该能够发送DELETE请求', async () => { - const mockData = { success: true }; - mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData)); - - const result = await apiClient.delete('/test/1'); - - expect(mockAxios.request).toHaveBeenCalledWith({ method: 'DELETE', url: '/test/1' }); - expect(result).toEqual(mockData); - }); - - it('应该能够发送PATCH请求', async () => { - const mockData = { id: 1, name: 'Patched Test' }; - const requestData = { name: 'Patched Item' }; - mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData)); - - const result = await apiClient.patch('/test/1', requestData); - - expect(mockAxios.request).toHaveBeenCalledWith({ method: 'PATCH', url: '/test/1', data: requestData }); - expect(result).toEqual(mockData); - }); - - it('应该能够发送通用请求', async () => { - const mockData = { id: 1, name: 'Test' }; - const config = createMockApiRequestConfig({ url: '/test', method: 'GET' }); - mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData)); - - const result = await apiClient.request(config); - - expect(mockAxios.request).toHaveBeenCalledWith(config); - expect(result).toEqual(mockData); - }); - }); - - describe('返回完整响应的请求方法', () => { - it('应该能够返回GET请求的完整响应', async () => { - const mockData = { id: 1, name: 'Test' }; - const mockResponse = createMockAxiosResponse(mockData); - mockAxios.request.mockResolvedValue(mockResponse); - - const result = await apiClient.request({ url: '/test', method: 'GET' }); - - expect(mockAxios.request).toHaveBeenCalled(); - expect(result).toEqual(mockData); - }); - - it('应该能够返回POST请求的完整响应', async () => { - const mockData = { id: 1, name: 'Test' }; - const requestData = { name: 'New Item' }; - const mockResponse = createMockAxiosResponse(mockData); - mockAxios.request.mockResolvedValue(mockResponse); - - const result = await apiClient.request({ url: '/test', method: 'POST', data: requestData }); - - expect(mockAxios.request).toHaveBeenCalled(); - expect(result).toEqual(mockData); - }); - }); - - describe('拦截器管理', () => { - it('应该能够添加请求拦截器', () => { - const onRequest = (config: any) => config; - const onRequestError = (error: any) => error; - - const interceptorId = apiClient.addRequestInterceptor(onRequest, onRequestError); - - expect(mockAxios.interceptors.request.use).toHaveBeenCalled(); - expect(typeof interceptorId).toBe('number'); - }); - - it('应该能够添加响应拦截器', () => { - const onResponse = (response: any) => response; - const onResponseError = (error: any) => error; - - const interceptorId = apiClient.addResponseInterceptor(onResponse, onResponseError); - - expect(mockAxios.interceptors.response.use).toHaveBeenCalled(); - expect(typeof interceptorId).toBe('number'); - }); - - it('应该能够移除请求拦截器', () => { - const onRequest = (config: any) => config; - - const interceptorId = apiClient.addRequestInterceptor(onRequest); - apiClient.removeRequestInterceptor(interceptorId); - - expect(mockAxios.interceptors.request.eject).toHaveBeenCalledWith(interceptorId); - }); - - it('应该能够处理移除不存在的请求拦截器', () => { - const nonExistentId = 999; - - // 不应该抛出错误 - expect(() => { - apiClient.removeRequestInterceptor(nonExistentId); - }).not.toThrow(); - - // 不应该调用eject方法,因为拦截器不存在 - expect(mockAxios.interceptors.request.eject).not.toHaveBeenCalledWith(nonExistentId); - }); - - it('应该能够移除响应拦截器', () => { - const onResponse = (response: any) => response; - - const interceptorId = apiClient.addResponseInterceptor(onResponse); - apiClient.removeResponseInterceptor(interceptorId); - - expect(mockAxios.interceptors.response.eject).toHaveBeenCalledWith(interceptorId); - }); - - it('应该能够处理移除不存在的响应拦截器', () => { - const nonExistentId = 999; - - // 不应该抛出错误 - expect(() => { - apiClient.removeResponseInterceptor(nonExistentId); - }).not.toThrow(); - - // 不应该调用eject方法,因为拦截器不存在 - expect(mockAxios.interceptors.response.eject).not.toHaveBeenCalledWith(nonExistentId); - }); - - it('应该能够添加和移除多个请求拦截器', () => { - const onRequest1 = (config: any) => config; - const onRequest2 = (config: any) => config; - const onRequest3 = (config: any) => config; - - // 添加三个拦截器 - const id1 = apiClient.addRequestInterceptor(onRequest1); - const id2 = apiClient.addRequestInterceptor(onRequest2); - const id3 = apiClient.addRequestInterceptor(onRequest3); - - // 移除中间的拦截器 - apiClient.removeRequestInterceptor(id2); - - // 验证eject被调用 - expect(mockAxios.interceptors.request.eject).toHaveBeenCalledWith(id2); - - // 移除剩余的拦截器 - apiClient.removeRequestInterceptor(id1); - apiClient.removeRequestInterceptor(id3); - - // 验证所有eject都被调用 - expect(mockAxios.interceptors.request.eject).toHaveBeenCalledWith(id1); - expect(mockAxios.interceptors.request.eject).toHaveBeenCalledWith(id3); - }); - - it('应该能够添加和移除多个响应拦截器', () => { - const onResponse1 = (response: any) => response; - const onResponse2 = (response: any) => response; - const onResponse3 = (response: any) => response; - - // 添加三个拦截器 - const id1 = apiClient.addResponseInterceptor(onResponse1); - const id2 = apiClient.addResponseInterceptor(onResponse2); - const id3 = apiClient.addResponseInterceptor(onResponse3); - - // 移除第一个和第三个拦截器 - apiClient.removeResponseInterceptor(id1); - apiClient.removeResponseInterceptor(id3); - - // 验证eject被调用 - expect(mockAxios.interceptors.response.eject).toHaveBeenCalledWith(id1); - expect(mockAxios.interceptors.response.eject).toHaveBeenCalledWith(id3); - - // 移除剩余的拦截器 - apiClient.removeResponseInterceptor(id2); - - // 验证所有eject都被调用 - expect(mockAxios.interceptors.response.eject).toHaveBeenCalledWith(id2); - }); - }); - - describe('配置管理', () => { - it('应该能够设置默认配置', () => { - const config = { timeout: 5000 }; - apiClient.setDefaults(config); - - expect(mockAxios.defaults.timeout).toBe(5000); - }); - - it('应该能够设置基础URL', () => { - const baseURL = 'https://api.example.com'; - apiClient.setBaseURL(baseURL); - - expect(mockAxios.defaults.baseURL).toBe(baseURL); - }); - }); - - describe('实例创建', () => { - it('应该能够创建新实例', () => { - const newConfig = { timeout: 8000 }; - const newInstance = apiClient.createInstance(newConfig); - - expect(newInstance).toBeDefined(); - expect(typeof newInstance.get).toBe('function'); - expect(typeof newInstance.post).toBe('function'); - }); - - it('应该能够复制拦截器到新实例', () => { - // 添加请求拦截器 - const onRequest = (config: any) => ({ ...config, intercepted: true }); - const onRequestError = (error: any) => error; - const requestInterceptorId = apiClient.addRequestInterceptor(onRequest, onRequestError); - - // 添加响应拦截器 - const onResponse = (response: any) => ({ ...response, intercepted: true }); - const onResponseError = (error: any) => error; - const responseInterceptorId = apiClient.addResponseInterceptor(onResponse, onResponseError); - - // 创建新实例 - const newInstance = apiClient.createInstance({ timeout: 5000 }); - - // 验证新实例有拦截器方法 - expect(typeof newInstance.addRequestInterceptor).toBe('function'); - expect(typeof newInstance.addResponseInterceptor).toBe('function'); - expect(typeof newInstance.removeRequestInterceptor).toBe('function'); - expect(typeof newInstance.removeResponseInterceptor).toBe('function'); - - // 清理 - apiClient.removeRequestInterceptor(requestInterceptorId); - apiClient.removeResponseInterceptor(responseInterceptorId); - }); - - it('应该能够处理headers类型转换', () => { - // 设置原始实例的headers - apiClient.setDefaults({ - headers: { - 'Content-Type': 'application/json', - 'X-Custom-Header': 'custom-value', - 'X-Number-Header': 123, - 'X-Boolean-Header': true, - 'X-Object-Header': { key: 'value' }, // 这个应该被过滤掉 - 'X-Function-Header': () => {} // 这个应该被过滤掉 - } - }); - - // 创建新实例 - const newInstance = apiClient.createInstance({ timeout: 5000 }); - - // 验证新实例创建成功 - expect(newInstance).toBeDefined(); - }); - - it('应该能够创建嵌套实例', () => { - // 创建第一层新实例 - const firstLevelInstance = apiClient.createInstance({ timeout: 3000 }); - - // 从第一层实例创建第二层实例 - const secondLevelInstance = firstLevelInstance.createInstance({ baseURL: 'https://api.example.com' }); - - // 验证第二层实例创建成功 - expect(secondLevelInstance).toBeDefined(); - expect(typeof secondLevelInstance.get).toBe('function'); - expect(typeof secondLevelInstance.post).toBe('function'); - }); - - it('应该能够处理空配置创建实例', () => { - const newInstance = apiClient.createInstance(); - - expect(newInstance).toBeDefined(); - expect(typeof newInstance.get).toBe('function'); - expect(typeof newInstance.post).toBe('function'); - }); - - it('应该能够处理没有headers的实例', () => { - // 创建一个没有headers的实例 - const noHeadersInstance = apiClient.createInstance({ timeout: 2000 }); - - // 从没有headers的实例创建新实例 - const newInstance = noHeadersInstance.createInstance({ baseURL: 'https://test.api.com' }); - - expect(newInstance).toBeDefined(); - }); - }); - - describe('错误处理', () => { - it('应该能够处理请求错误', async () => { - const errorMessage = 'Request failed'; - mockAxios.request.mockRejectedValue(createMockAxiosError(errorMessage)); - - await expect(apiClient.get('/test')).rejects.toThrow(errorMessage); - }); - - it('应该能够处理网络错误', async () => { - const networkError = new Error('Network Error'); - mockAxios.request.mockRejectedValue(networkError); - - await expect(apiClient.get('/test')).rejects.toThrow('Network Error'); - }); - }); - - describe('类型转换', () => { - it('应该正确处理请求配置转换', async () => { - const config = createMockApiRequestConfig({ - url: '/test', - method: 'POST', - data: { name: 'Test' }, - headers: { 'Custom-Header': 'value' } - }); - - const mockData = { success: true }; - mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData)); - - await apiClient.request(config); - - // 验证axios被调用时接收到了正确的配置 - expect(mockAxios.request).toHaveBeenCalled(); - }); - - it('应该正确处理响应数据转换', async () => { - const mockData = { id: 1, name: 'Test' }; - mockAxios.request.mockResolvedValue(createMockAxiosResponse(mockData)); - - const result = await apiClient.get('/test'); - - // 验证返回的数据是正确的 - expect(result).toEqual(mockData); - }); - }); -}); diff --git a/test/unit/api/modules/chat.test.ts b/test/unit/api/modules/chat.test.ts deleted file mode 100644 index 2e83727..0000000 --- a/test/unit/api/modules/chat.test.ts +++ /dev/null @@ -1,345 +0,0 @@ -/** - * 聊天API模块测试 - * 测试聊天相关的API调用,包括创建/更新聊天会话、发送消息、获取聊天会话列表、获取聊天消息、标记消息已读等功能 - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { chatApi } from '@/api/modules/chat'; -import { createMockAxios } from '@/test/mocks/http-client'; -import { - createMockChatSession, - createMockChatMessage, - createMockUser -} from '@/test/mocks/data-factory'; -import type { ApiClient } from '@/api/client'; -import type { - CreateChatSessionRequest, - UpdateChatSessionRequest, - SendMessageRequest, - GetChatSessionsRequest, - GetChatSessionsResponse, - GetChatMessagesRequest, - GetChatMessagesResponse, - MarkMessagesAsReadRequest, - MarkMessagesAsReadResponse -} from '@/types/chat/api'; - -describe('聊天API模块', () => { - let mockClient: ApiClient; - let chatApiInstance: ReturnType; - - beforeEach(() => { - // 创建模拟的API客户端 - mockClient = { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - patch: vi.fn(), - request: vi.fn(), - getWithResponse: vi.fn(), - postWithResponse: vi.fn(), - putWithResponse: vi.fn(), - deleteWithResponse: vi.fn(), - patchWithResponse: vi.fn(), - requestWithResponse: vi.fn(), - addRequestInterceptor: vi.fn(), - addResponseInterceptor: vi.fn(), - removeRequestInterceptor: vi.fn(), - removeResponseInterceptor: vi.fn(), - setDefaults: vi.fn(), - setBaseURL: vi.fn(), - createInstance: vi.fn(), - } as unknown as ApiClient; - - // 创建聊天API实例 - chatApiInstance = chatApi(mockClient); - }); - - describe('创建聊天会话功能', () => { - it('应该能够创建新的聊天会话', async () => { - const createRequest: CreateChatSessionRequest = { - title: '测试聊天会话', - modelId: 'model-123', - systemPrompt: '你是一个有用的助手' - }; - const mockSession = createMockChatSession({ - title: createRequest.title, - modelId: createRequest.modelId - }); - const mockResponse = { session: mockSession }; - - mockClient.post.mockResolvedValue(mockResponse); - - const result = await chatApiInstance.createSession(createRequest); - - expect(mockClient.post).toHaveBeenCalledWith('/chat/sessions', createRequest); - expect(result).toEqual(mockResponse); - }); - - it('应该处理创建聊天会话失败的情况', async () => { - const createRequest: CreateChatSessionRequest = { - title: '', - modelId: '', - systemPrompt: '' - }; - const errorResponse = { - success: false, - data: null, - message: '标题和模型ID不能为空', - code: 400 - }; - - mockClient.post.mockRejectedValue(errorResponse); - - await expect(chatApiInstance.createSession(createRequest)).rejects.toEqual(errorResponse); - expect(mockClient.post).toHaveBeenCalledWith('/chat/sessions', createRequest); - }); - }); - - describe('更新聊天会话功能', () => { - it('应该能够更新聊天会话', async () => { - const updateRequest: UpdateChatSessionRequest = { - sessionId: 'session-123', - title: '更新后的标题' - }; - const mockSession = createMockChatSession({ - id: updateRequest.sessionId, - title: updateRequest.title - }); - const mockResponse = { session: mockSession }; - - mockClient.put.mockResolvedValue(mockResponse); - - const result = await chatApiInstance.updateSession(updateRequest); - - expect(mockClient.put).toHaveBeenCalledWith('/chat/sessions/session-123', { title: '更新后的标题' }); - expect(result).toEqual(mockResponse); - }); - - it('应该处理更新不存在会话的情况', async () => { - const updateRequest: UpdateChatSessionRequest = { - sessionId: 'non-existent-session', - title: '更新后的标题' - }; - const errorResponse = { - success: false, - data: null, - message: '会话不存在', - code: 404 - }; - - mockClient.put.mockRejectedValue(errorResponse); - - await expect(chatApiInstance.updateSession(updateRequest)).rejects.toEqual(errorResponse); - expect(mockClient.put).toHaveBeenCalledWith('/chat/sessions/non-existent-session', { title: '更新后的标题' }); - }); - }); - - describe('发送消息功能', () => { - it('应该能够发送消息', async () => { - const sendMessageRequest: SendMessageRequest = { - sessionId: 'session-123', - content: '这是一条测试消息', - role: 'user' - }; - const mockMessage = createMockChatMessage({ - sessionId: sendMessageRequest.sessionId, - content: sendMessageRequest.content, - role: sendMessageRequest.role - }); - const mockResponse = { message: mockMessage }; - - mockClient.post.mockResolvedValue(mockResponse); - - const result = await chatApiInstance.sendMessage(sendMessageRequest); - - expect(mockClient.post).toHaveBeenCalledWith('/chat/sessions/session-123/messages', sendMessageRequest); - expect(result).toEqual(mockResponse); - }); - - it('应该处理发送消息失败的情况', async () => { - const sendMessageRequest: SendMessageRequest = { - sessionId: 'session-123', - content: '', - role: 'user' - }; - const errorResponse = { - success: false, - data: null, - message: '消息内容不能为空', - code: 400 - }; - - mockClient.post.mockRejectedValue(errorResponse); - - await expect(chatApiInstance.sendMessage(sendMessageRequest)).rejects.toEqual(errorResponse); - expect(mockClient.post).toHaveBeenCalledWith('/chat/sessions/session-123/messages', sendMessageRequest); - }); - }); - - describe('获取聊天会话列表功能', () => { - it('应该能够获取聊天会话列表', async () => { - const getRequest: GetChatSessionsRequest = { - page: 1, - limit: 10 - }; - const mockSessions = [ - createMockChatSession({ id: 'session-1' }), - createMockChatSession({ id: 'session-2' }) - ]; - const mockResponse: GetChatSessionsResponse = { - sessions: mockSessions, - pagination: { - page: 1, - limit: 10, - total: 20, - totalPages: 2 - } - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await chatApiInstance.getSessions(getRequest); - - expect(mockClient.get).toHaveBeenCalledWith('/chat/sessions', { params: getRequest }); - expect(result).toEqual(mockResponse); - }); - - it('应该能够在不带参数的情况下获取聊天会话列表', async () => { - const mockSessions = [ - createMockChatSession({ id: 'session-1' }), - createMockChatSession({ id: 'session-2' }) - ]; - const mockResponse: GetChatSessionsResponse = { - sessions: mockSessions, - pagination: { - page: 1, - limit: 20, - total: 2, - totalPages: 1 - } - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await chatApiInstance.getSessions(); - - expect(mockClient.get).toHaveBeenCalledWith('/chat/sessions', {}); - expect(result).toEqual(mockResponse); - }); - - it('应该处理获取聊天会话列表失败的情况', async () => { - const getRequest: GetChatSessionsRequest = { - page: -1, // 无效的页码 - limit: 10 - }; - const errorResponse = { - success: false, - data: null, - message: '页码必须大于0', - code: 400 - }; - - mockClient.get.mockRejectedValue(errorResponse); - - await expect(chatApiInstance.getSessions(getRequest)).rejects.toEqual(errorResponse); - expect(mockClient.get).toHaveBeenCalledWith('/chat/sessions', { params: getRequest }); - }); - }); - - describe('获取聊天消息功能', () => { - it('应该能够获取聊天消息', async () => { - const getRequest: GetChatMessagesRequest = { - sessionId: 'session-123', - page: 1, - limit: 10 - }; - const mockMessages = [ - createMockChatMessage({ id: 'message-1', sessionId: 'session-123' }), - createMockChatMessage({ id: 'message-2', sessionId: 'session-123' }) - ]; - const mockResponse: GetChatMessagesResponse = { - messages: mockMessages, - pagination: { - page: 1, - limit: 10, - total: 30, - totalPages: 3 - } - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await chatApiInstance.getMessages(getRequest); - - expect(mockClient.get).toHaveBeenCalledWith('/chat/sessions/session-123/messages', { - params: { page: 1, limit: 10 } - }); - expect(result).toEqual(mockResponse); - }); - - it('应该处理获取不存在会话的消息的情况', async () => { - const getRequest: GetChatMessagesRequest = { - sessionId: 'non-existent-session', - page: 1, - limit: 10 - }; - const errorResponse = { - success: false, - data: null, - message: '会话不存在', - code: 404 - }; - - mockClient.get.mockRejectedValue(errorResponse); - - await expect(chatApiInstance.getMessages(getRequest)).rejects.toEqual(errorResponse); - expect(mockClient.get).toHaveBeenCalledWith('/chat/sessions/non-existent-session/messages', { - params: { page: 1, limit: 10 } - }); - }); - }); - - describe('标记消息已读功能', () => { - it('应该能够标记消息为已读', async () => { - const markReadRequest: MarkMessagesAsReadRequest = { - sessionId: 'session-123', - messageIds: ['message-1', 'message-2', 'message-3'] - }; - const mockResponse: MarkMessagesAsReadResponse = { - success: true, - readCount: 3 - }; - - mockClient.post.mockResolvedValue(mockResponse); - - const result = await chatApiInstance.markMessagesAsRead(markReadRequest); - - expect(mockClient.post).toHaveBeenCalledWith('/chat/sessions/session-123/read', { - messageIds: ['message-1', 'message-2', 'message-3'] - }); - expect(result).toEqual(mockResponse); - }); - - it('应该处理标记不存在的消息为已读的情况', async () => { - const markReadRequest: MarkMessagesAsReadRequest = { - sessionId: 'session-123', - messageIds: ['non-existent-message'] - }; - const errorResponse = { - success: false, - data: null, - message: '消息不存在', - code: 404 - }; - - mockClient.post.mockRejectedValue(errorResponse); - - await expect(chatApiInstance.markMessagesAsRead(markReadRequest)).rejects.toEqual(errorResponse); - expect(mockClient.post).toHaveBeenCalledWith('/chat/sessions/session-123/read', { - messageIds: ['non-existent-message'] - }); - }); - }); -}); \ No newline at end of file diff --git a/test/unit/api/modules/model.test.ts b/test/unit/api/modules/model.test.ts deleted file mode 100644 index 82b96d3..0000000 --- a/test/unit/api/modules/model.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -/** - * AI模型API模块测试 - * 测试AI模型相关的API调用,包括获取AI模型广场、获取模型详情、获取模型评论等功能 - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { modelApi } from '@/api/modules/model'; -import { createMockAxios } from '@/test/mocks/http-client'; -import { - createMockAIModel, - createMockModelComment, - createMockUser -} from '@/test/mocks/data-factory'; -import type { ApiClient } from '@/api/client'; -import type { - GetAIPlazaRequest, - GetAIPlazaResponse, - GetModelDetailRequest, - GetModelDetailResponse, - GetModelCommentsRequest, - GetModelCommentsResponse -} from '@/types/model/api'; - -describe('AI模型API模块', () => { - let mockClient: ApiClient; - let modelApiInstance: ReturnType; - - beforeEach(() => { - // 创建模拟的API客户端 - mockClient = { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - patch: vi.fn(), - request: vi.fn(), - getWithResponse: vi.fn(), - postWithResponse: vi.fn(), - putWithResponse: vi.fn(), - deleteWithResponse: vi.fn(), - patchWithResponse: vi.fn(), - requestWithResponse: vi.fn(), - addRequestInterceptor: vi.fn(), - addResponseInterceptor: vi.fn(), - removeRequestInterceptor: vi.fn(), - removeResponseInterceptor: vi.fn(), - setDefaults: vi.fn(), - setBaseURL: vi.fn(), - createInstance: vi.fn(), - } as unknown as ApiClient; - - // 创建AI模型API实例 - modelApiInstance = modelApi(mockClient); - }); - - describe('获取AI模型广场功能', () => { - it('应该能够获取AI模型广场数据', async () => { - const getRequest: GetAIPlazaRequest = { - page: 1, - limit: 10, - category: 'text-generation' - }; - const mockModels = [ - createMockAIModel({ id: 'model-1', category: 'text-generation' }), - createMockAIModel({ id: 'model-2', category: 'text-generation' }) - ]; - const mockResponse: GetAIPlazaResponse = { - models: mockModels, - categories: [ - { id: 'text-generation', name: '文本生成', count: 50 }, - { id: 'image-generation', name: '图像生成', count: 30 } - ], - pagination: { - page: 1, - limit: 10, - total: 50, - totalPages: 5 - } - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await modelApiInstance.getAIPlaza(getRequest); - - expect(mockClient.get).toHaveBeenCalledWith('/models/plaza', { params: getRequest }); - expect(result).toEqual(mockResponse); - }); - - it('应该能够在不带参数的情况下获取AI模型广场数据', async () => { - const mockModels = [ - createMockAIModel({ id: 'model-1' }), - createMockAIModel({ id: 'model-2' }) - ]; - const mockResponse: GetAIPlazaResponse = { - models: mockModels, - categories: [ - { id: 'text-generation', name: '文本生成', count: 50 }, - { id: 'image-generation', name: '图像生成', count: 30 } - ], - pagination: { - page: 1, - limit: 20, - total: 2, - totalPages: 1 - } - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await modelApiInstance.getAIPlaza(); - - expect(mockClient.get).toHaveBeenCalledWith('/models/plaza', {}); - expect(result).toEqual(mockResponse); - }); - - it('应该处理获取AI模型广场失败的情况', async () => { - const getRequest: GetAIPlazaRequest = { - page: -1, // 无效的页码 - limit: 10 - }; - const errorResponse = { - success: false, - data: null, - message: '页码必须大于0', - code: 400 - }; - - mockClient.get.mockRejectedValue(errorResponse); - - await expect(modelApiInstance.getAIPlaza(getRequest)).rejects.toEqual(errorResponse); - expect(mockClient.get).toHaveBeenCalledWith('/models/plaza', { params: getRequest }); - }); - }); - - describe('获取模型详情功能', () => { - it('应该能够获取模型详情', async () => { - const getRequest: GetModelDetailRequest = { - modelId: 'model-123' - }; - const mockModel = createMockAIModel({ id: 'model-123' }); - const mockResponse: GetModelDetailResponse = { - model: mockModel, - relatedModels: [ - createMockAIModel({ id: 'related-1' }), - createMockAIModel({ id: 'related-2' }) - ] - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await modelApiInstance.getModelDetail(getRequest); - - expect(mockClient.get).toHaveBeenCalledWith('/models/model-123', { params: {} }); - expect(result).toEqual(mockResponse); - }); - - it('应该处理获取不存在模型详情的情况', async () => { - const getRequest: GetModelDetailRequest = { - modelId: 'non-existent-model' - }; - const errorResponse = { - success: false, - data: null, - message: '模型不存在', - code: 404 - }; - - mockClient.get.mockRejectedValue(errorResponse); - - await expect(modelApiInstance.getModelDetail(getRequest)).rejects.toEqual(errorResponse); - expect(mockClient.get).toHaveBeenCalledWith('/models/non-existent-model', { params: {} }); - }); - }); - - describe('获取模型评论功能', () => { - it('应该能够获取模型评论', async () => { - const getRequest: GetModelCommentsRequest = { - modelId: 'model-123', - page: 1, - limit: 10, - sortBy: 'newest' - }; - const mockComments = [ - createMockModelComment({ id: 'comment-1', modelId: 'model-123' }), - createMockModelComment({ id: 'comment-2', modelId: 'model-123' }) - ]; - const mockResponse: GetModelCommentsResponse = { - comments: mockComments, - pagination: { - page: 1, - limit: 10, - total: 25, - totalPages: 3 - }, - statistics: { - averageRating: 4.5, - totalComments: 25, - ratingDistribution: { - 5: 15, - 4: 7, - 3: 2, - 2: 1, - 1: 0 - } - } - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await modelApiInstance.getModelComments(getRequest); - - expect(mockClient.get).toHaveBeenCalledWith('/models/model-123/comments', { - params: { page: 1, limit: 10, sortBy: 'newest' } - }); - expect(result).toEqual(mockResponse); - }); - - it('应该处理获取不存在模型的评论的情况', async () => { - const getRequest: GetModelCommentsRequest = { - modelId: 'non-existent-model', - page: 1, - limit: 10 - }; - const errorResponse = { - success: false, - data: null, - message: '模型不存在', - code: 404 - }; - - mockClient.get.mockRejectedValue(errorResponse); - - await expect(modelApiInstance.getModelComments(getRequest)).rejects.toEqual(errorResponse); - expect(mockClient.get).toHaveBeenCalledWith('/models/non-existent-model/comments', { - params: { page: 1, limit: 10 } - }); - }); - }); -}); \ No newline at end of file diff --git a/test/unit/api/modules/post.test.ts b/test/unit/api/modules/post.test.ts deleted file mode 100644 index 17d0ae6..0000000 --- a/test/unit/api/modules/post.test.ts +++ /dev/null @@ -1,408 +0,0 @@ -/** - * 帖子API模块测试 - * 测试帖子相关的API调用,包括创建帖子、获取帖子列表、点赞、评论等功能 - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { postApi } from '@/api/modules/post'; -import { createMockAxios } from '@/test/mocks/http-client'; -import { - createMockPost, - createMockUser, - createMockPostComment -} from '@/test/mocks/data-factory'; -import type { ApiClient } from '@/api/client'; -import type { - CreatePostRequest, - CreatePostResponse, - GetPostsRequest, - GetPostsResponse, - GetPostRequest, - GetPostResponse, - LikePostRequest, - LikePostResponse, - BookmarkPostRequest, - BookmarkPostResponse, - CreateCommentRequest, - CreateCommentResponse, - GetCommentsRequest, - GetCommentsResponse, - LikeCommentRequest, - LikeCommentResponse -} from '@/types/post/api'; - -describe('帖子API模块', () => { - let mockClient: ApiClient; - let postApiInstance: ReturnType; - - beforeEach(() => { - // 创建模拟的API客户端 - mockClient = { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - patch: vi.fn(), - request: vi.fn(), - getWithResponse: vi.fn(), - postWithResponse: vi.fn(), - putWithResponse: vi.fn(), - deleteWithResponse: vi.fn(), - patchWithResponse: vi.fn(), - requestWithResponse: vi.fn(), - addRequestInterceptor: vi.fn(), - addResponseInterceptor: vi.fn(), - removeRequestInterceptor: vi.fn(), - removeResponseInterceptor: vi.fn(), - setDefaults: vi.fn(), - setBaseURL: vi.fn(), - createInstance: vi.fn(), - } as unknown as ApiClient; - - // 创建帖子API实例 - postApiInstance = postApi(mockClient); - }); - - describe('创建帖子功能', () => { - it('应该能够创建新帖子', async () => { - const createRequest: CreatePostRequest = { - title: '测试帖子标题', - content: '这是一个测试帖子的内容', - tags: ['测试', 'API'], - isPublic: true - }; - const mockPost = createMockPost({ - title: createRequest.title, - content: createRequest.content, - tags: createRequest.tags - }); - const mockResponse: CreatePostResponse = { - success: true, - data: mockPost, - message: '帖子创建成功', - code: 201 - }; - - mockClient.post.mockResolvedValue(mockResponse); - - const result = await postApiInstance.createPost(createRequest); - - expect(mockClient.post).toHaveBeenCalledWith('/posts', createRequest); - expect(result).toEqual(mockResponse); - }); - - it('应该处理创建帖子失败的情况', async () => { - const createRequest: CreatePostRequest = { - title: '', - content: '', - tags: [], - isPublic: true - }; - const errorResponse = { - success: false, - data: null, - message: '标题和内容不能为空', - code: 400 - }; - - mockClient.post.mockRejectedValue(errorResponse); - - await expect(postApiInstance.createPost(createRequest)).rejects.toEqual(errorResponse); - expect(mockClient.post).toHaveBeenCalledWith('/posts', createRequest); - }); - }); - - describe('获取帖子列表功能', () => { - it('应该能够获取帖子列表', async () => { - const getRequest: GetPostsRequest = { - page: 1, - limit: 10, - tag: '测试' - }; - const mockPosts = [ - createMockPost({ id: 'post-1' }), - createMockPost({ id: 'post-2' }) - ]; - const mockResponse: GetPostsResponse = { - success: true, - data: { - posts: mockPosts, - pagination: { - page: 1, - limit: 10, - total: 100, - totalPages: 10 - } - }, - message: '获取帖子列表成功', - code: 200 - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await postApiInstance.getPosts(getRequest); - - expect(mockClient.get).toHaveBeenCalledWith('/posts', { params: getRequest }); - expect(result).toEqual(mockResponse); - }); - - it('应该处理获取帖子列表失败的情况', async () => { - const getRequest: GetPostsRequest = { - page: -1, // 无效的页码 - limit: 10 - }; - const errorResponse = { - success: false, - data: null, - message: '页码必须大于0', - code: 400 - }; - - mockClient.get.mockRejectedValue(errorResponse); - - await expect(postApiInstance.getPosts(getRequest)).rejects.toEqual(errorResponse); - expect(mockClient.get).toHaveBeenCalledWith('/posts', { params: getRequest }); - }); - }); - - describe('获取帖子详情功能', () => { - it('应该能够获取帖子详情', async () => { - const getRequest: GetPostRequest = { postId: 'post-123' }; - const mockPost = createMockPost({ id: 'post-123' }); - const mockResponse: GetPostResponse = { - success: true, - data: mockPost, - message: '获取帖子详情成功', - code: 200 - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await postApiInstance.getPost(getRequest); - - expect(mockClient.get).toHaveBeenCalledWith('/posts/post-123'); - expect(result).toEqual(mockResponse); - }); - - it('应该处理获取不存在帖子的情况', async () => { - const getRequest: GetPostRequest = { postId: 'non-existent-post' }; - const errorResponse = { - success: false, - data: null, - message: '帖子不存在', - code: 404 - }; - - mockClient.get.mockRejectedValue(errorResponse); - - await expect(postApiInstance.getPost(getRequest)).rejects.toEqual(errorResponse); - expect(mockClient.get).toHaveBeenCalledWith('/posts/non-existent-post'); - }); - }); - - describe('点赞帖子功能', () => { - it('应该能够点赞帖子', async () => { - const likeRequest: LikePostRequest = { postId: 'post-123' }; - const mockResponse: LikePostResponse = { - success: true, - message: '点赞成功', - code: 200 - }; - - mockClient.put.mockResolvedValue(mockResponse); - - const result = await postApiInstance.likePost(likeRequest); - - expect(mockClient.put).toHaveBeenCalledWith('/posts/post-123/like'); - expect(result).toEqual(mockResponse); - }); - - it('应该处理点赞不存在帖子的情况', async () => { - const likeRequest: LikePostRequest = { postId: 'non-existent-post' }; - const errorResponse = { - success: false, - data: null, - message: '帖子不存在', - code: 404 - }; - - mockClient.put.mockRejectedValue(errorResponse); - - await expect(postApiInstance.likePost(likeRequest)).rejects.toEqual(errorResponse); - expect(mockClient.put).toHaveBeenCalledWith('/posts/non-existent-post/like'); - }); - }); - - describe('收藏帖子功能', () => { - it('应该能够收藏帖子', async () => { - const bookmarkRequest: BookmarkPostRequest = { postId: 'post-123' }; - const mockResponse: BookmarkPostResponse = { - success: true, - message: '收藏成功', - code: 200 - }; - - mockClient.put.mockResolvedValue(mockResponse); - - const result = await postApiInstance.bookmarkPost(bookmarkRequest); - - expect(mockClient.put).toHaveBeenCalledWith('/posts/post-123/bookmark'); - expect(result).toEqual(mockResponse); - }); - - it('应该处理收藏不存在帖子的情况', async () => { - const bookmarkRequest: BookmarkPostRequest = { postId: 'non-existent-post' }; - const errorResponse = { - success: false, - data: null, - message: '帖子不存在', - code: 404 - }; - - mockClient.put.mockRejectedValue(errorResponse); - - await expect(postApiInstance.bookmarkPost(bookmarkRequest)).rejects.toEqual(errorResponse); - expect(mockClient.put).toHaveBeenCalledWith('/posts/non-existent-post/bookmark'); - }); - }); - - describe('创建评论功能', () => { - it('应该能够创建评论', async () => { - const commentRequest: CreateCommentRequest = { - postId: 'post-123', - content: '这是一个测试评论', - parentId: 'parent-comment-123' - }; - const mockComment = createMockPostComment({ - postId: commentRequest.postId, - content: commentRequest.content, - parentId: commentRequest.parentId - }); - const mockResponse: CreateCommentResponse = { - success: true, - data: mockComment, - message: '评论创建成功', - code: 201 - }; - - mockClient.post.mockResolvedValue(mockResponse); - - const result = await postApiInstance.createComment(commentRequest); - - expect(mockClient.post).toHaveBeenCalledWith('/posts/post-123/comments', commentRequest); - expect(result).toEqual(mockResponse); - }); - - it('应该处理创建评论失败的情况', async () => { - const commentRequest: CreateCommentRequest = { - postId: 'post-123', - content: '', // 空评论内容 - parentId: 'parent-comment-123' - }; - const errorResponse = { - success: false, - data: null, - message: '评论内容不能为空', - code: 400 - }; - - mockClient.post.mockRejectedValue(errorResponse); - - await expect(postApiInstance.createComment(commentRequest)).rejects.toEqual(errorResponse); - expect(mockClient.post).toHaveBeenCalledWith('/posts/post-123/comments', commentRequest); - }); - }); - - describe('获取评论列表功能', () => { - it('应该能够获取评论列表', async () => { - const commentsRequest: GetCommentsRequest = { - postId: 'post-123', - page: 1, - limit: 10, - sortBy: 'createdAt', - sortOrder: 'desc' - }; - const mockComments = [ - createMockPostComment({ id: 'comment-1', postId: 'post-123' }), - createMockPostComment({ id: 'comment-2', postId: 'post-123' }) - ]; - const mockResponse: GetCommentsResponse = { - success: true, - data: { - comments: mockComments, - pagination: { - page: 1, - limit: 10, - total: 50, - totalPages: 5 - } - }, - message: '获取评论列表成功', - code: 200 - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await postApiInstance.getComments(commentsRequest); - - expect(mockClient.get).toHaveBeenCalledWith('/posts/post-123/comments', { - params: { page: 1, limit: 10, sortBy: 'createdAt', sortOrder: 'desc' } - }); - expect(result).toEqual(mockResponse); - }); - - it('应该处理获取评论列表失败的情况', async () => { - const commentsRequest: GetCommentsRequest = { - postId: 'post-123', - page: -1, // 无效的页码 - limit: 10 - }; - const errorResponse = { - success: false, - data: null, - message: '页码必须大于0', - code: 400 - }; - - mockClient.get.mockRejectedValue(errorResponse); - - await expect(postApiInstance.getComments(commentsRequest)).rejects.toEqual(errorResponse); - expect(mockClient.get).toHaveBeenCalledWith('/posts/post-123/comments', { - params: { page: -1, limit: 10 } - }); - }); - }); - - describe('点赞评论功能', () => { - it('应该能够点赞评论', async () => { - const likeCommentRequest: LikeCommentRequest = { commentId: 'comment-123' }; - const mockResponse: LikeCommentResponse = { - success: true, - message: '点赞评论成功', - code: 200 - }; - - mockClient.put.mockResolvedValue(mockResponse); - - const result = await postApiInstance.likeComment(likeCommentRequest); - - expect(mockClient.put).toHaveBeenCalledWith('/comments/comment-123/like'); - expect(result).toEqual(mockResponse); - }); - - it('应该处理点赞不存在评论的情况', async () => { - const likeCommentRequest: LikeCommentRequest = { commentId: 'non-existent-comment' }; - const errorResponse = { - success: false, - data: null, - message: '评论不存在', - code: 404 - }; - - mockClient.put.mockRejectedValue(errorResponse); - - await expect(postApiInstance.likeComment(likeCommentRequest)).rejects.toEqual(errorResponse); - expect(mockClient.put).toHaveBeenCalledWith('/comments/non-existent-comment/like'); - }); - }); -}) \ No newline at end of file diff --git a/test/unit/api/modules/user.test.ts b/test/unit/api/modules/user.test.ts deleted file mode 100644 index 11b0e89..0000000 --- a/test/unit/api/modules/user.test.ts +++ /dev/null @@ -1,334 +0,0 @@ -/** - * 用户API模块测试 - * 测试用户相关的API调用,包括登录、注册、刷新令牌、获取/更新用户档案、关注/取消关注用户等功能 - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { userApi } from '@/api/modules/user'; -import { createMockAxios } from '@/test/mocks/http-client'; -import { - createMockUser, - createMockUserProfile -} from '@/test/mocks/data-factory'; -import type { ApiClient } from '@/api/client'; -import type { - LoginRequest, - LoginResponse, - RegisterRequest, - RegisterResponse, - RefreshTokenRequest, - RefreshTokenResponse, - UserProfileUpdateRequest, - UserProfileUpdateResponse, - UserFollowRequest, - UserFollowResponse -} from '@/types/user'; - -describe('用户API模块', () => { - let mockClient: ApiClient; - let userApiInstance: ReturnType; - - beforeEach(() => { - // 创建模拟的API客户端 - mockClient = { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - patch: vi.fn(), - request: vi.fn(), - getWithResponse: vi.fn(), - postWithResponse: vi.fn(), - putWithResponse: vi.fn(), - deleteWithResponse: vi.fn(), - patchWithResponse: vi.fn(), - requestWithResponse: vi.fn(), - addRequestInterceptor: vi.fn(), - addResponseInterceptor: vi.fn(), - removeRequestInterceptor: vi.fn(), - removeResponseInterceptor: vi.fn(), - setDefaults: vi.fn(), - setBaseURL: vi.fn(), - createInstance: vi.fn(), - } as unknown as ApiClient; - - // 创建用户API实例 - userApiInstance = userApi(mockClient); - }); - - describe('用户登录功能', () => { - it('应该能够成功登录', async () => { - const loginRequest: LoginRequest = { - username: 'test@example.com', - password: 'password123' - }; - const mockUser = createMockUser({ username: loginRequest.username }); - const mockResponse: LoginResponse = { - user: mockUser, - sessionId: 'session-123' - }; - - mockClient.post.mockResolvedValue(mockResponse); - - const result = await userApiInstance.login(loginRequest); - - expect(mockClient.post).toHaveBeenCalledWith('/auth/login', loginRequest); - expect(result).toEqual(mockResponse); - }); - - it('应该处理登录失败的情况', async () => { - const loginRequest: LoginRequest = { - username: 'wrong@example.com', - password: 'wrongpassword' - }; - const errorResponse = { - success: false, - data: null, - message: '用户名或密码错误', - code: 401 - }; - - mockClient.post.mockRejectedValue(errorResponse); - - await expect(userApiInstance.login(loginRequest)).rejects.toEqual(errorResponse); - expect(mockClient.post).toHaveBeenCalledWith('/auth/login', loginRequest); - }); - }); - - describe('用户注册功能', () => { - it('应该能够成功注册', async () => { - const registerRequest: RegisterRequest = { - username: 'newuser', - email: 'newuser@example.com', - password: 'password123' - }; - const mockUser = createMockUser({ - username: registerRequest.username, - email: registerRequest.email - }); - const mockResponse: RegisterResponse = { - user: mockUser, - sessionId: 'session-456' - }; - - mockClient.post.mockResolvedValue(mockResponse); - - const result = await userApiInstance.register(registerRequest); - - expect(mockClient.post).toHaveBeenCalledWith('/auth/register', registerRequest); - expect(result).toEqual(mockResponse); - }); - - it('应该处理注册失败的情况', async () => { - const registerRequest: RegisterRequest = { - username: '', - email: 'invalid-email', - password: '123' // 密码太短 - }; - const errorResponse = { - success: false, - data: null, - message: '用户名不能为空,邮箱格式不正确,密码长度至少为6位', - code: 400 - }; - - mockClient.post.mockRejectedValue(errorResponse); - - await expect(userApiInstance.register(registerRequest)).rejects.toEqual(errorResponse); - expect(mockClient.post).toHaveBeenCalledWith('/auth/register', registerRequest); - }); - }); - - describe('刷新令牌功能', () => { - it('应该能够成功刷新令牌', async () => { - const refreshTokenRequest: RefreshTokenRequest = { - sessionId: 'session-456' - }; - const mockResponse: RefreshTokenResponse = { - sessionId: 'new-session-789' - }; - - mockClient.post.mockResolvedValue(mockResponse); - - const result = await userApiInstance.refreshToken(refreshTokenRequest); - - expect(mockClient.post).toHaveBeenCalledWith('/auth/refresh', refreshTokenRequest); - expect(result).toEqual(mockResponse); - }); - - it('应该处理刷新令牌失败的情况', async () => { - const refreshTokenRequest: RefreshTokenRequest = { - sessionId: 'invalid-session' - }; - const errorResponse = { - success: false, - data: null, - message: '会话无效或已过期', - code: 401 - }; - - mockClient.post.mockRejectedValue(errorResponse); - - await expect(userApiInstance.refreshToken(refreshTokenRequest)).rejects.toEqual(errorResponse); - expect(mockClient.post).toHaveBeenCalledWith('/auth/refresh', refreshTokenRequest); - }); - }); - - describe('获取用户档案功能', () => { - it('应该能够获取用户档案', async () => { - const mockUserProfile = createMockUserProfile(); - const mockResponse: UserProfileUpdateResponse = { - success: true, - data: mockUserProfile, - message: '获取用户档案成功', - code: 200 - }; - - mockClient.get.mockResolvedValue(mockResponse); - - const result = await userApiInstance.getProfile(); - - expect(mockClient.get).toHaveBeenCalledWith('/user/profile'); - expect(result).toEqual(mockResponse); - }); - - it('应该处理获取用户档案失败的情况', async () => { - const errorResponse = { - success: false, - data: null, - message: '用户未登录', - code: 401 - }; - - mockClient.get.mockRejectedValue(errorResponse); - - await expect(userApiInstance.getProfile()).rejects.toEqual(errorResponse); - expect(mockClient.get).toHaveBeenCalledWith('/user/profile'); - }); - }); - - describe('更新用户档案功能', () => { - it('应该能够更新用户档案', async () => { - const updateRequest: UserProfileUpdateRequest = { - username: 'updateduser', - bio: '这是我的个人简介', - avatar: 'https://example.com/avatar.jpg' - }; - const mockUserProfile = createMockUserProfile(updateRequest); - const mockResponse: UserProfileUpdateResponse = { - success: true, - data: mockUserProfile, - message: '用户档案更新成功', - code: 200 - }; - - mockClient.put.mockResolvedValue(mockResponse); - - const result = await userApiInstance.updateProfile(updateRequest); - - expect(mockClient.put).toHaveBeenCalledWith('/user/profile', updateRequest); - expect(result).toEqual(mockResponse); - }); - - it('应该处理更新用户档案失败的情况', async () => { - const updateRequest: UserProfileUpdateRequest = { - username: '', // 用户名不能为空 - bio: '', - avatar: '' - }; - const errorResponse = { - success: false, - data: null, - message: '用户名不能为空', - code: 400 - }; - - mockClient.put.mockRejectedValue(errorResponse); - - await expect(userApiInstance.updateProfile(updateRequest)).rejects.toEqual(errorResponse); - expect(mockClient.put).toHaveBeenCalledWith('/user/profile', updateRequest); - }); - }); - - describe('关注用户功能', () => { - it('应该能够关注用户', async () => { - const followRequest: UserFollowRequest = { - userId: 'user-123' - }; - const mockResponse: UserFollowResponse = { - success: true, - data: { - isFollowing: true, - followersCount: 101 - }, - message: '关注成功', - code: 200 - }; - - mockClient.put.mockResolvedValue(mockResponse); - - const result = await userApiInstance.followUser(followRequest); - - expect(mockClient.put).toHaveBeenCalledWith('/user/follow/user-123'); - expect(result).toEqual(mockResponse); - }); - - it('应该处理关注不存在的用户的情况', async () => { - const followRequest: UserFollowRequest = { - userId: 'non-existent-user' - }; - const errorResponse = { - success: false, - data: null, - message: '用户不存在', - code: 404 - }; - - mockClient.put.mockRejectedValue(errorResponse); - - await expect(userApiInstance.followUser(followRequest)).rejects.toEqual(errorResponse); - expect(mockClient.put).toHaveBeenCalledWith('/user/follow/non-existent-user'); - }); - }); - - describe('取消关注用户功能', () => { - it('应该能够取消关注用户', async () => { - const unfollowRequest: UserFollowRequest = { - userId: 'user-123' - }; - const mockResponse: UserFollowResponse = { - success: true, - data: { - isFollowing: false, - followersCount: 99 - }, - message: '取消关注成功', - code: 200 - }; - - mockClient.delete.mockResolvedValue(mockResponse); - - const result = await userApiInstance.unfollowUser(unfollowRequest); - - expect(mockClient.delete).toHaveBeenCalledWith('/user/follow/user-123'); - expect(result).toEqual(mockResponse); - }); - - it('应该处理取消关注不存在的用户的情况', async () => { - const unfollowRequest: UserFollowRequest = { - userId: 'non-existent-user' - }; - const errorResponse = { - success: false, - data: null, - message: '用户不存在', - code: 404 - }; - - mockClient.delete.mockRejectedValue(errorResponse); - - await expect(userApiInstance.unfollowUser(unfollowRequest)).rejects.toEqual(errorResponse); - expect(mockClient.delete).toHaveBeenCalledWith('/user/follow/non-existent-user'); - }); - }); -}); \ No newline at end of file diff --git a/test/unit/auth/auth-service.test.ts b/test/unit/auth/auth-service.test.ts deleted file mode 100644 index a8ec735..0000000 --- a/test/unit/auth/auth-service.test.ts +++ /dev/null @@ -1,379 +0,0 @@ -/** - * 认证服务集成测试 - */ - -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { createAuthService } from '@/auth'; -import { apiClient } from '@/api'; -import { createMockStorage } from '@/test/mocks'; -import type { StorageAdapter } from '@/auth/storage-adapter'; -import type { LoginRequest, LoginResponse, RegisterRequest, RegisterResponse } from '@/types/user/profile'; -import { ApiError } from '@/api/errors'; -import { AuthError } from '@/auth/errors'; - -// 模拟API客户端 -vi.mock('@/api', () => ({ - apiClient: { - addRequestInterceptor: vi.fn(), - addResponseInterceptor: vi.fn(), - removeRequestInterceptor: vi.fn(), - removeResponseInterceptor: vi.fn(), - setDefaults: vi.fn(), - setBaseURL: vi.fn(), - createInstance: vi.fn(), - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - patch: vi.fn(), - request: vi.fn() - } -})); - -// 模拟错误处理 -vi.mock('@/api/errors', () => ({ - isApiError: vi.fn((error: unknown): error is ApiError => - error && typeof error === 'object' && 'isApiError' in error && (error as any).isApiError - ), - ApiError: class extends Error { - public readonly code: number; - public readonly details: unknown; - - constructor(code: number, message: string, details?: unknown) { - super(message); - this.name = 'ApiError'; - this.code = code; - this.details = details; - } - } -})); - -describe('认证服务', () => { - let authService: ReturnType; - let mockStorage: StorageAdapter; - - beforeEach(() => { - mockStorage = createMockStorage(); - authService = createAuthService(apiClient); - vi.clearAllMocks(); - }); - - describe('login', () => { - it('应该成功登录并返回用户信息', async () => { - // 准备测试数据 - const loginData: LoginRequest = { - username: 'testuser', - password: 'password123' - }; - - const loginResponse: LoginResponse = { - user: { - id: '1', - username: 'testuser', - email: 'test@example.com', - avatar: '', - nickname: 'Test User', - bio: '', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }, - sessionId: 'test-session-id' - }; - - // 模拟API响应 - (apiClient.post as any).mockResolvedValue(loginResponse); - // 模拟getUserInfo调用 - (apiClient.get as any).mockResolvedValue({ user: loginResponse.user }); - - // 执行登录 - const result = await authService.login(loginData); - - // 验证结果 - expect(result).toEqual(loginResponse); - expect(apiClient.post).toHaveBeenCalledWith('/auth/login', loginData); - expect(apiClient.get).toHaveBeenCalledWith('/auth/me'); - }); - - it('应该处理登录失败', async () => { - // 准备测试数据 - const loginData: LoginRequest = { - username: 'testuser', - password: 'wrongpassword' - }; - - // 创建一个真正的ApiError对象 - const mockApiError = new ApiError(401, '用户名或密码错误', {}); - - // 模拟API错误响应 - (apiClient.post as any).mockRejectedValue(mockApiError); - - // 模拟sessionManager.getUserInfo,确保它不会被调用 - const mockSessionManager = { - getUserInfo: vi.fn() - }; - (authService as any).sessionManager = mockSessionManager; - - // 执行登录并期望失败 - await expect(authService.login(loginData)).rejects.toThrowError( - expect.objectContaining({ - name: 'AuthError', - code: 'LOGIN_FAILED' - }) - ); - expect(apiClient.post).toHaveBeenCalledWith('/auth/login', loginData); - expect(mockSessionManager.getUserInfo).not.toHaveBeenCalled(); - }); - }); - - describe('register', () => { - it('应该成功注册并返回用户信息', async () => { - // 准备测试数据 - const registerData: RegisterRequest = { - username: 'newuser', - email: 'newuser@example.com', - password: 'password123' - }; - - const registerResponse: RegisterResponse = { - user: { - id: '2', - username: 'newuser', - email: 'newuser@example.com', - avatar: '', - nickname: 'New User', - bio: '', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }, - sessionId: 'new-session-id' - }; - - // 模拟API响应 - (apiClient.post as any).mockResolvedValue(registerResponse); - // 模拟getUserInfo调用 - (apiClient.get as any).mockResolvedValue({ user: registerResponse.user }); - - // 执行注册 - const result = await authService.register(registerData); - - // 验证结果 - expect(result).toEqual(registerResponse); - expect(apiClient.post).toHaveBeenCalledWith('/auth/register', registerData); - expect(apiClient.get).toHaveBeenCalledWith('/auth/me'); - }); - - it('应该处理注册失败', async () => { - // 准备测试数据 - const registerData: RegisterRequest = { - username: 'existinguser', - email: 'existing@example.com', - password: 'password123' - }; - - // 创建一个真正的ApiError对象 - const mockApiError = new ApiError(409, '用户已存在', {}); - - // 模拟API错误响应 - (apiClient.post as any).mockRejectedValue(mockApiError); - - // 模拟sessionManager.getUserInfo,确保它不会被调用 - const mockSessionManager = { - getUserInfo: vi.fn() - }; - (authService as any).sessionManager = mockSessionManager; - - // 执行注册并期望失败 - await expect(authService.register(registerData)).rejects.toThrowError( - expect.objectContaining({ - name: 'AuthError', - code: 'REGISTER_FAILED' - }) - ); - expect(apiClient.post).toHaveBeenCalledWith('/auth/register', registerData); - expect(mockSessionManager.getUserInfo).not.toHaveBeenCalled(); - }); - }); - - describe('logout', () => { - it('应该调用登出API并清除缓存', async () => { - // 模拟API响应 - (apiClient.post as any).mockResolvedValue({}); - - // 模拟sessionManager.clearCache - const mockSessionManager = { - clearCache: vi.fn() - }; - (authService as any).sessionManager = mockSessionManager; - - // 执行登出 - await authService.logout(); - - // 验证API调用 - expect(apiClient.post).toHaveBeenCalledWith('/auth/logout'); - expect(mockSessionManager.clearCache).toHaveBeenCalledTimes(1); - }); - - it('应该处理登出API失败但仍清除缓存', async () => { - // 模拟API错误 - const apiError = new Error('Network error'); - (apiClient.post as any).mockRejectedValue(apiError); - - // 模拟sessionManager.clearCache - const mockSessionManager = { - clearCache: vi.fn() - }; - (authService as any).sessionManager = mockSessionManager; - - // 执行登出并期望失败 - await expect(authService.logout()).rejects.toThrowError( - expect.objectContaining({ - name: 'AuthError', - code: 'LOGOUT_FAILED' - }) - ); - - // 验证API调用和缓存清除 - expect(apiClient.post).toHaveBeenCalledWith('/auth/logout'); - expect(mockSessionManager.clearCache).toHaveBeenCalledTimes(1); - }); - }); - - describe('isAuthenticated', () => { - it('应该返回认证状态', async () => { - // 模拟sessionManager.isAuthenticated返回true - const mockSessionManager = { - isAuthenticated: vi.fn().mockResolvedValue(true) - }; - - // 通过访问私有属性来模拟sessionManager - (authService as any).sessionManager = mockSessionManager; - - // 验证认证状态 - const result = await authService.isAuthenticated(); - expect(result).toBe(true); - expect(mockSessionManager.isAuthenticated).toHaveBeenCalledTimes(1); - }); - }); - - describe('getCurrentUser', () => { - it('应该返回当前用户信息', async () => { - const mockUser = { - id: '1', - username: 'testuser', - email: 'test@example.com', - avatar: '', - nickname: 'Test User', - bio: '', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; - - // 模拟sessionManager.getUserInfo返回用户信息 - const mockSessionManager = { - getUserInfo: vi.fn().mockResolvedValue(mockUser) - }; - - // 通过访问私有属性来模拟sessionManager - (authService as any).sessionManager = mockSessionManager; - - // 获取当前用户 - const user = await authService.getCurrentUser(); - - // 验证用户信息 - expect(user).toEqual(mockUser); - expect(mockSessionManager.getUserInfo).toHaveBeenCalledTimes(1); - }); - - it('应该处理用户未认证的情况', async () => { - // 模拟sessionManager.getUserInfo抛出ApiError - const mockError = new ApiError(401, 'Session expired', {}); - const mockSessionManager = { - getUserInfo: vi.fn().mockRejectedValue(mockError) - }; - - // 通过访问私有属性来模拟sessionManager - (authService as any).sessionManager = mockSessionManager; - - // 获取当前用户并期望失败 - await expect(authService.getCurrentUser()).rejects.toThrowError( - expect.objectContaining({ - name: 'AuthError', - code: 'USER_NOT_FOUND' - }) - ); - - expect(mockSessionManager.getUserInfo).toHaveBeenCalledTimes(1); - }); - }); - - describe('clearCache', () => { - it('应该清除缓存', () => { - // 模拟sessionManager.clearCache - const mockSessionManager = { - clearCache: vi.fn() - }; - - // 通过访问私有属性来模拟sessionManager - (authService as any).sessionManager = mockSessionManager; - - // 清除缓存 - authService.clearCache(); - - // 验证clearCache被调用 - expect(mockSessionManager.clearCache).toHaveBeenCalledTimes(1); - }); - }); - - describe('事件监听', () => { - it('应该添加事件监听器', () => { - const mockListener = vi.fn(); - - // 添加事件监听器 - authService.on('login', mockListener); - - // 验证监听器已添加 - // 注意:由于我们使用的是单例authEventManager,这里我们只能验证方法被调用 - // 实际的事件触发测试在event-manager.test.ts中进行 - expect(typeof mockListener).toBe('function'); - }); - - it('应该移除事件监听器', () => { - const mockListener = vi.fn(); - - // 添加并移除事件监听器 - authService.on('login', mockListener); - authService.off('login', mockListener); - - // 验证监听器已移除 - // 注意:由于我们使用的是单例authEventManager,这里我们只能验证方法被调用 - // 实际的事件触发测试在event-manager.test.ts中进行 - expect(typeof mockListener).toBe('function'); - }); - }); - - describe('请求拦截器', () => { - it('应该添加请求拦截器', () => { - // 重置模拟函数的调用记录 - vi.clearAllMocks(); - - // 主动调用添加拦截器的方法 - apiClient.addRequestInterceptor(); - - // 验证拦截器是否被调用 - expect(apiClient.addRequestInterceptor).toHaveBeenCalled(); - }); - }); - - describe('响应拦截器', () => { - it('应该添加响应拦截器', () => { - // 重置模拟函数的调用记录 - vi.clearAllMocks(); - - // 主动调用添加拦截器的方法 - apiClient.addResponseInterceptor(); - - // 验证拦截器是否被调用 - expect(apiClient.addResponseInterceptor).toHaveBeenCalled(); - }); - }); -}); diff --git a/test/unit/auth/event-manager.test.ts b/test/unit/auth/event-manager.test.ts deleted file mode 100644 index 913611b..0000000 --- a/test/unit/auth/event-manager.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -/** - * AuthEventManager 单元测试 - */ - -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { AuthEventManager, authEventManager } from '@/auth/event-manager'; -import type { AuthEventListener } from '@/auth/event-manager'; - -describe('AuthEventManager', () => { - let eventManager: AuthEventManager; - let mockListener1: AuthEventListener; - let mockListener2: AuthEventListener; - - beforeEach(() => { - eventManager = new AuthEventManager(); - mockListener1 = vi.fn(); - mockListener2 = vi.fn(); - vi.clearAllMocks(); - }); - - describe('on', () => { - it('应该添加事件监听器', () => { - eventManager.on('login_success', mockListener1); - - // 触发事件 - eventManager.emit('login_success', { user: 'test' }); - - expect(mockListener1).toHaveBeenCalledWith({ user: 'test' }); - expect(mockListener1).toHaveBeenCalledTimes(1); - }); - - it('应该支持为同一事件添加多个监听器', () => { - eventManager.on('login_success', mockListener1); - eventManager.on('login_success', mockListener2); - - // 触发事件 - eventManager.emit('login_success', { user: 'test' }); - - expect(mockListener1).toHaveBeenCalledWith({ user: 'test' }); - expect(mockListener2).toHaveBeenCalledWith({ user: 'test' }); - }); - - it('应该支持为不同事件添加监听器', () => { - eventManager.on('login_success', mockListener1); - eventManager.on('logout', mockListener2); - - // 只触发登录成功事件 - eventManager.emit('login_success', { user: 'test' }); - - expect(mockListener1).toHaveBeenCalledWith({ user: 'test' }); - expect(mockListener1).toHaveBeenCalledTimes(1); - expect(mockListener2).not.toHaveBeenCalled(); - }); - }); - - describe('off', () => { - it('应该移除事件监听器', () => { - eventManager.on('login_success', mockListener1); - eventManager.on('login_success', mockListener2); - - // 移除第一个监听器 - eventManager.off('login_success', mockListener1); - - // 触发事件 - eventManager.emit('login_success', { user: 'test' }); - - expect(mockListener1).not.toHaveBeenCalled(); - expect(mockListener2).toHaveBeenCalledWith({ user: 'test' }); - expect(mockListener2).toHaveBeenCalledTimes(1); - }); - - it('应该只移除指定的监听器', () => { - eventManager.on('login_success', mockListener1); - eventManager.on('logout', mockListener1); - - // 只移除登录成功事件的监听器 - eventManager.off('login_success', mockListener1); - - // 触发两个事件 - eventManager.emit('login_success', { user: 'test' }); - eventManager.emit('logout', { user: 'test' }); - - expect(mockListener1).toHaveBeenCalledTimes(1); - expect(mockListener1).toHaveBeenCalledWith({ user: 'test' }); - }); - - it('应该处理移除不存在的监听器', () => { - // 尝试移除未添加的监听器 - eventManager.off('login_success', mockListener1); - - // 添加并触发事件 - eventManager.on('login_success', mockListener2); - eventManager.emit('login_success', { user: 'test' }); - - expect(mockListener1).not.toHaveBeenCalled(); - expect(mockListener2).toHaveBeenCalledWith({ user: 'test' }); - }); - }); - - describe('emit', () => { - it('应该触发事件并传递参数', () => { - eventManager.on('login_success', mockListener1); - - const args = [{ user: 'test' }, { token: 'abc123' }]; - eventManager.emit('login_success', ...args); - - expect(mockListener1).toHaveBeenCalledWith(...args); - }); - - it('应该触发所有监听器', () => { - eventManager.on('login_success', mockListener1); - eventManager.on('login_success', mockListener2); - - eventManager.emit('login_success', { user: 'test' }); - - expect(mockListener1).toHaveBeenCalledWith({ user: 'test' }); - expect(mockListener2).toHaveBeenCalledWith({ user: 'test' }); - }); - - it('应该处理没有监听器的事件', () => { - // 尝试触发没有监听器的事件 - expect(() => { - eventManager.emit('login_success', { user: 'test' }); - }).not.toThrow(); - }); - - it('应该处理监听器中的异常', () => { - const errorListener = vi.fn(() => { - throw new Error('Test error'); - }); - - eventManager.on('login_success', errorListener); - eventManager.on('login_success', mockListener2); - - // 即使一个监听器抛出异常,其他监听器也应该被调用 - expect(() => { - eventManager.emit('login_success', { user: 'test' }); - }).toThrow('Error in auth event listener for login_success'); - - expect(errorListener).toHaveBeenCalled(); - // 注意:由于第一个监听器抛出异常,第二个监听器可能不会被调用 - // 这取决于具体实现,这里我们只验证错误监听器被调用 - }); - }); - - describe('clear', () => { - it('应该清除所有监听器', () => { - // 添加多个事件监听器 - eventManager.on('login_success', mockListener1); - eventManager.on('logout', mockListener2); - - // 清除所有监听器 - eventManager.clear(); - - // 触发事件,验证监听器已被清除 - eventManager.emit('login_success', { user: 'test' }); - eventManager.emit('logout', { user: 'test' }); - - expect(mockListener1).not.toHaveBeenCalled(); - expect(mockListener2).not.toHaveBeenCalled(); - }); - }); -}); - -describe('authEventManager', () => { - it('应该是AuthEventManager的实例', () => { - expect(authEventManager).toBeInstanceOf(AuthEventManager); - }); - - it('应该是单例', () => { - const anotherInstance = authEventManager; - expect(anotherInstance).toBe(authEventManager); - }); -}); diff --git a/test/unit/utils/data.test.ts b/test/unit/utils/data.test.ts deleted file mode 100644 index 8f62d36..0000000 --- a/test/unit/utils/data.test.ts +++ /dev/null @@ -1,482 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { - throttle, - deepClone, - deepEqual, - pick, - omit, - merge, - toQueryString, - fromQueryString, - unique, - groupBy, - sortBy, - debounce -} from '@/utils/data'; - -describe('数据处理工具函数', () => { - describe('throttle', () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('应该节流函数调用', () => { - const mockFn = vi.fn(); - const throttledFn = throttle(mockFn, 100); - - // 多次调用 - throttledFn(); - throttledFn(); - throttledFn(); - - // 在等待时间内,函数应该只被调用一次 - expect(mockFn).toHaveBeenCalledTimes(1); - - // 快进时间 - vi.advanceTimersByTime(100); - - // 再次调用 - throttledFn(); - throttledFn(); - - // 函数应该被调用第二次 - expect(mockFn).toHaveBeenCalledTimes(2); - }); - - it('应该支持leading选项', () => { - const mockFn = vi.fn(); - const throttledFn = throttle(mockFn, 100, { leading: false }); - - // 多次调用 - throttledFn(); - throttledFn(); - - // 由于leading为false,函数不应该被调用 - expect(mockFn).toHaveBeenCalledTimes(0); - - // 快进时间 - vi.advanceTimersByTime(100); - - // 函数应该被调用 - expect(mockFn).toHaveBeenCalledTimes(1); - }); - - it('应该支持trailing选项', () => { - const mockFn = vi.fn(); - const throttledFn = throttle(mockFn, 100, { trailing: false }); - - // 多次调用 - throttledFn(); - throttledFn(); - - // 快进时间 - vi.advanceTimersByTime(100); - - // 由于trailing为false,函数应该只被调用一次(第一次调用) - expect(mockFn).toHaveBeenCalledTimes(1); - }); - }); - - describe('debounce', () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('应该防抖函数调用', () => { - const mockFn = vi.fn(); - const debouncedFn = debounce(mockFn, 100); - - // 多次调用 - debouncedFn(); - debouncedFn(); - debouncedFn(); - - // 在等待时间内,函数不应该被调用 - expect(mockFn).toHaveBeenCalledTimes(0); - - // 快进时间 - vi.advanceTimersByTime(100); - - // 函数应该被调用一次 - expect(mockFn).toHaveBeenCalledTimes(1); - }); - - it('应该支持immediate选项', () => { - const mockFn = vi.fn(); - const debouncedFn = debounce(mockFn, 100, true); - - // 第一次调用 - debouncedFn(); - - // 由于immediate为true,函数应该立即被调用 - expect(mockFn).toHaveBeenCalledTimes(1); - - // 再次调用(在等待时间内) - debouncedFn(); - - // 函数仍然应该只被调用一次(第二次调用被防抖了) - expect(mockFn).toHaveBeenCalledTimes(1); - - // 快进时间超过等待期 - vi.advanceTimersByTime(100); - - // 再次调用(等待期后) - debouncedFn(); - - // 函数应该被调用第二次(立即执行) - expect(mockFn).toHaveBeenCalledTimes(2); - }); - }); - - describe('deepClone', () => { - it('应该深拷贝基本类型', () => { - expect(deepClone(null)).toBe(null); - expect(deepClone(undefined)).toBe(undefined); - expect(deepClone(123)).toBe(123); - expect(deepClone('string')).toBe('string'); - expect(deepClone(true)).toBe(true); - }); - - it('应该深拷贝日期对象', () => { - const date = new Date('2023-05-15T10:30:45Z'); - const clonedDate = deepClone(date); - - expect(clonedDate).not.toBe(date); // 不是同一个对象 - expect(clonedDate.getTime()).toBe(date.getTime()); // 但时间相同 - }); - - it('应该深拷贝数组', () => { - const array = [1, 'string', { a: 1 }, [2, 3]]; - const clonedArray = deepClone(array); - - expect(clonedArray).not.toBe(array); // 不是同一个对象 - expect(clonedArray).toEqual(array); // 但内容相同 - expect(clonedArray[2]).not.toBe(array[2]); // 嵌套对象也被深拷贝 - expect(clonedArray[3]).not.toBe(array[3]); // 嵌套数组也被深拷贝 - }); - - it('应该深拷贝对象', () => { - const obj = { - a: 1, - b: 'string', - c: { - d: 2, - e: [3, 4] - }, - f: new Date('2023-05-15T10:30:45Z') - }; - const clonedObj = deepClone(obj); - - expect(clonedObj).not.toBe(obj); // 不是同一个对象 - expect(clonedObj).toEqual(obj); // 但内容相同 - expect(clonedObj.c).not.toBe(obj.c); // 嵌套对象也被深拷贝 - expect(clonedObj.c.e).not.toBe(obj.c.e); // 嵌套数组也被深拷贝 - expect(clonedObj.f).not.toBe(obj.f); // 日期对象也被深拷贝 - }); - }); - - describe('deepEqual', () => { - it('应该比较基本类型', () => { - expect(deepEqual(1, 1)).toBe(true); - expect(deepEqual('string', 'string')).toBe(true); - expect(deepEqual(true, true)).toBe(true); - expect(deepEqual(null, null)).toBe(true); - expect(deepEqual(undefined, undefined)).toBe(true); - - expect(deepEqual(1, 2)).toBe(false); - expect(deepEqual('string', 'other')).toBe(false); - expect(deepEqual(true, false)).toBe(false); - expect(deepEqual(null, undefined)).toBe(false); - }); - - it('应该比较数组', () => { - expect(deepEqual([1, 2, 3], [1, 2, 3])).toBe(true); - expect(deepEqual([1, 2, 3], [1, 2, 4])).toBe(false); - expect(deepEqual([1, 2, 3], [1, 2])).toBe(false); - expect(deepEqual([1, 2, 3], [3, 2, 1])).toBe(false); - }); - - it('应该比较对象', () => { - expect(deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true); - expect(deepEqual({ a: 1, b: 2 }, { a: 1, b: 3 })).toBe(false); - expect(deepEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false); - expect(deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true); // 顺序不重要 - }); - - it('应该深度比较嵌套对象', () => { - const obj1 = { a: 1, b: { c: 2, d: [3, 4] } }; - const obj2 = { a: 1, b: { c: 2, d: [3, 4] } }; - const obj3 = { a: 1, b: { c: 2, d: [3, 5] } }; - - expect(deepEqual(obj1, obj2)).toBe(true); - expect(deepEqual(obj1, obj3)).toBe(false); - }); - }); - - describe('pick', () => { - it('应该从对象中选取指定的属性', () => { - const obj = { a: 1, b: 2, c: 3, d: 4 }; - const result = pick(obj, ['a', 'c']); - - expect(result).toEqual({ a: 1, c: 3 }); - expect(result).not.toBe(obj); // 返回新对象 - }); - - it('应该处理不存在的属性', () => { - const obj = { a: 1, b: 2 }; - const result = pick(obj, ['a', 'c']); - - expect(result).toEqual({ a: 1 }); - }); - - it('应该处理空键数组', () => { - const obj = { a: 1, b: 2 }; - const result = pick(obj, []); - - expect(result).toEqual({}); - }); - }); - - describe('omit', () => { - it('应该从对象中排除指定的属性', () => { - const obj = { a: 1, b: 2, c: 3, d: 4 }; - const result = omit(obj, ['a', 'c']); - - expect(result).toEqual({ b: 2, d: 4 }); - expect(result).not.toBe(obj); // 返回新对象 - }); - - it('应该处理不存在的属性', () => { - const obj = { a: 1, b: 2 }; - const result = omit(obj, ['a', 'c']); - - expect(result).toEqual({ b: 2 }); - }); - - it('应该处理空键数组', () => { - const obj = { a: 1, b: 2 }; - const result = omit(obj, []); - - expect(result).toEqual(obj); - expect(result).not.toBe(obj); // 仍然返回新对象 - }); - }); - - describe('merge', () => { - it('应该合并多个对象', () => { - const obj1 = { a: 1, b: 2 }; - const obj2 = { b: 3, c: 4 }; - const obj3 = { d: 5 }; - - const result = merge(obj1, obj2, obj3); - - expect(result).toEqual({ a: 1, b: 3, c: 4, d: 5 }); - }); - - it('应该处理null或undefined对象', () => { - const obj1 = { a: 1 }; - const obj2 = null; - const obj3 = undefined; - const obj4 = { b: 2 }; - - const result = merge(obj1, obj2, obj3, obj4); - - expect(result).toEqual({ a: 1, b: 2 }); - }); - - it('应该处理非对象值', () => { - const obj1 = { a: 1 }; - const obj2 = 'string'; - const obj3 = 123; - - const result = merge(obj1, obj2, obj3); - - expect(result).toEqual({ a: 1 }); - }); - }); - - describe('toQueryString', () => { - it('应该将对象转换为查询字符串', () => { - const obj = { a: '1', b: 'test', c: 'value' }; - const result = toQueryString(obj); - - expect(result).toBe('a=1&b=test&c=value'); - }); - - it('应该处理null和undefined值', () => { - const obj = { a: '1', b: null, c: undefined, d: '4' }; - const result = toQueryString(obj); - - expect(result).toBe('a=1&d=4'); - }); - - it('应该处理数字和布尔值', () => { - const obj = { a: 1, b: true, c: false }; - const result = toQueryString(obj); - - expect(result).toBe('a=1&b=true&c=false'); - }); - - it('应该处理空对象', () => { - const obj = {}; - const result = toQueryString(obj); - - expect(result).toBe(''); - }); - }); - - describe('fromQueryString', () => { - it('应该将查询字符串转换为对象', () => { - const queryString = 'a=1&b=test&c=value'; - const result = fromQueryString(queryString); - - expect(result).toEqual({ a: '1', b: 'test', c: 'value' }); - }); - - it('应该处理空查询字符串', () => { - const queryString = ''; - const result = fromQueryString(queryString); - - expect(result).toEqual({}); - }); - - it('应该处理特殊字符', () => { - const queryString = 'a=hello%20world&b=test%20%26%20value'; - const result = fromQueryString(queryString); - - expect(result).toEqual({ a: 'hello world', b: 'test & value' }); - }); - }); - - describe('unique', () => { - it('应该对基本类型数组去重', () => { - const array = [1, 2, 3, 2, 4, 1, 5]; - const result = unique(array); - - expect(result).toEqual([1, 2, 3, 4, 5]); - }); - - it('应该使用键函数对复杂对象数组去重', () => { - const array = [ - { id: 1, name: 'Alice' }, - { id: 2, name: 'Bob' }, - { id: 1, name: 'Alice Smith' }, - { id: 3, name: 'Charlie' }, - { id: 2, name: 'Bob Johnson' } - ]; - - const result = unique(array, (item: { id: number }) => item.id); - - expect(result).toEqual([ - { id: 1, name: 'Alice' }, - { id: 2, name: 'Bob' }, - { id: 3, name: 'Charlie' } - ]); - }); - - it('应该处理空数组', () => { - const array: number[] = []; - const result = unique(array); - - expect(result).toEqual([]); - }); - }); - - describe('groupBy', () => { - it('应该按指定键对数组分组', () => { - const array = [ - { category: 'fruit', name: 'Apple' }, - { category: 'vegetable', name: 'Carrot' }, - { category: 'fruit', name: 'Banana' }, - { category: 'fruit', name: 'Orange' }, - { category: 'vegetable', name: 'Broccoli' } - ]; - - const result = groupBy(array, (item: { category: string }) => item.category); - - expect(result).toEqual({ - fruit: [ - { category: 'fruit', name: 'Apple' }, - { category: 'fruit', name: 'Banana' }, - { category: 'fruit', name: 'Orange' } - ], - vegetable: [ - { category: 'vegetable', name: 'Carrot' }, - { category: 'vegetable', name: 'Broccoli' } - ] - }); - }); - - it('应该处理空数组', () => { - const array: any[] = []; - const result = groupBy(array, (item: { category: string }) => item.category); - - expect(result).toEqual({}); - }); - - it('应该处理数字键', () => { - const array = [ - { score: 90, name: 'Alice' }, - { score: 80, name: 'Bob' }, - { score: 90, name: 'Charlie' }, - { score: 70, name: 'David' } - ]; - - const result = groupBy(array, (item: { score: number }) => item.score); - - expect(result).toEqual({ - 90: [ - { score: 90, name: 'Alice' }, - { score: 90, name: 'Charlie' } - ], - 80: [ - { score: 80, name: 'Bob' } - ], - 70: [ - { score: 70, name: 'David' } - ] - }); - }); - }); - - describe('sortBy', () => { - it('应该使用默认比较函数对数组排序', () => { - const array = [3, 1, 4, 1, 5, 9, 2, 6]; - const result = sortBy(array); - - expect(result).toEqual([1, 1, 2, 3, 4, 5, 6, 9]); - expect(result).not.toBe(array); // 返回新数组 - }); - - it('应该使用自定义比较函数对数组排序', () => { - const array = [ - { name: 'Alice', age: 30 }, - { name: 'Bob', age: 25 }, - { name: 'Charlie', age: 35 } - ]; - - const result = sortBy(array, (a: { age: number }, b: { age: number }) => a.age - b.age); - - expect(result).toEqual([ - { name: 'Bob', age: 25 }, - { name: 'Alice', age: 30 }, - { name: 'Charlie', age: 35 } - ]); - }); - - it('应该处理空数组', () => { - const array: any[] = []; - const result = sortBy(array); - - expect(result).toEqual([]); - }); - }); -}); diff --git a/test/unit/utils/date.test.ts b/test/unit/utils/date.test.ts deleted file mode 100644 index 7b17dd3..0000000 --- a/test/unit/utils/date.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { dateUtils } from '@/utils/date'; - -describe('dateUtils', () => { - // 模拟当前时间 - const mockCurrentDate = new Date('2023-05-15T12:00:00Z'); - - beforeEach(() => { - // 使用 vi.useFakeTimers 模拟当前时间 - vi.useFakeTimers(); - vi.setSystemTime(mockCurrentDate); - }); - - afterEach(() => { - // 恢复真实时间 - vi.useRealTimers(); - }); - - describe('formatDate', () => { - it('应该格式化日期对象', () => { - const date = new Date('2023-05-15T10:30:45Z'); - expect(dateUtils.formatDate(date, 'YYYY-MM-DD')).toBe('2023-05-15'); - expect(dateUtils.formatDate(date, 'YYYY-MM-DD HH:mm:ss')).toBe('2023-05-15 10:30:45'); - }); - - it('应该格式化日期字符串', () => { - const dateString = '2023-05-15T10:30:45Z'; - expect(dateUtils.formatDate(dateString, 'YYYY-MM-DD')).toBe('2023-05-15'); - expect(dateUtils.formatDate(dateString, 'YYYY-MM-DD HH:mm')).toBe('2023-05-15 10:30'); - }); - - it('应该使用默认格式', () => { - const date = new Date('2023-05-15T10:30:45Z'); - expect(dateUtils.formatDate(date)).toBe('2023-05-15'); - }); - - it('应该处理无效日期', () => { - const invalidDate = new Date('invalid'); - expect(dateUtils.formatDate(invalidDate)).toBe(''); - expect(dateUtils.formatDate('invalid date')).toBe(''); - }); - - it('应该正确格式化单个数字的月份和日期', () => { - const date = new Date('2023-01-02T03:04:05Z'); - expect(dateUtils.formatDate(date, 'YYYY-MM-DD HH:mm:ss')).toBe('2023-01-02 03:04:05'); - }); - }); - - describe('getRelativeTime', () => { - it('应该显示"刚刚"对于几秒前的时间', () => { - const date = new Date(mockCurrentDate.getTime() - 30 * 1000); // 30秒前 - expect(dateUtils.getRelativeTime(date)).toBe('刚刚'); - }); - - it('应该显示"X分钟前"对于几分钟前的时间', () => { - const date = new Date(mockCurrentDate.getTime() - 5 * 60 * 1000); // 5分钟前 - expect(dateUtils.getRelativeTime(date)).toBe('5分钟前'); - }); - - it('应该显示"X小时前"对于几小时前的时间', () => { - const date = new Date(mockCurrentDate.getTime() - 3 * 60 * 60 * 1000); // 3小时前 - expect(dateUtils.getRelativeTime(date)).toBe('3小时前'); - }); - - it('应该显示"X天前"对于几天前的时间', () => { - const date = new Date(mockCurrentDate.getTime() - 5 * 24 * 60 * 60 * 1000); // 5天前 - expect(dateUtils.getRelativeTime(date)).toBe('5天前'); - }); - - it('应该显示"X周前"对于几周前的时间', () => { - const date = new Date(mockCurrentDate.getTime() - 2 * 7 * 24 * 60 * 60 * 1000); // 2周前 - expect(dateUtils.getRelativeTime(date)).toBe('2周前'); - }); - - it('应该显示"X个月前"对于几个月前的时间', () => { - const date = new Date(mockCurrentDate.getTime() - 3 * 30 * 24 * 60 * 60 * 1000); // 3个月前 - expect(dateUtils.getRelativeTime(date)).toBe('3个月前'); - }); - - it('应该显示"X年前"对于几年前的时间', () => { - const date = new Date(mockCurrentDate.getTime() - 2 * 365 * 24 * 60 * 60 * 1000); // 2年前 - expect(dateUtils.getRelativeTime(date)).toBe('2年前'); - }); - - it('应该返回绝对时间对于未来的时间', () => { - const futureDate = new Date(mockCurrentDate.getTime() + 24 * 60 * 60 * 1000); // 1天后 - expect(dateUtils.getRelativeTime(futureDate)).toBe('2023-05-16 12:00'); - }); - - it('应该处理字符串日期', () => { - const dateString = '2023-05-14T12:00:00Z'; // 1天前 - expect(dateUtils.getRelativeTime(dateString)).toBe('1天前'); - }); - }); - - describe('isToday', () => { - it('应该识别今天的日期', () => { - const today = new Date(mockCurrentDate); - expect(dateUtils.isToday(today)).toBe(true); - }); - - it('应该识别不是今天的日期', () => { - const yesterday = new Date(mockCurrentDate.getTime() - 24 * 60 * 60 * 1000); - const tomorrow = new Date(mockCurrentDate.getTime() + 24 * 60 * 60 * 1000); - - expect(dateUtils.isToday(yesterday)).toBe(false); - expect(dateUtils.isToday(tomorrow)).toBe(false); - }); - - it('应该处理字符串日期', () => { - const todayString = mockCurrentDate.toISOString(); - expect(dateUtils.isToday(todayString)).toBe(true); - }); - - it('应该处理不同时间但同一天的日期', () => { - const morning = new Date(mockCurrentDate); - morning.setHours(0, 0, 0, 0); - - const evening = new Date(mockCurrentDate); - evening.setHours(23, 59, 59, 999); - - expect(dateUtils.isToday(morning)).toBe(true); - expect(dateUtils.isToday(evening)).toBe(true); - }); - }); - - describe('isYesterday', () => { - it('应该识别昨天的日期', () => { - const yesterday = new Date(mockCurrentDate.getTime() - 24 * 60 * 60 * 1000); - expect(dateUtils.isYesterday(yesterday)).toBe(true); - }); - - it('应该识别不是昨天的日期', () => { - const today = new Date(mockCurrentDate); - const twoDaysAgo = new Date(mockCurrentDate.getTime() - 2 * 24 * 60 * 60 * 1000); - - expect(dateUtils.isYesterday(today)).toBe(false); - expect(dateUtils.isYesterday(twoDaysAgo)).toBe(false); - }); - - it('应该处理字符串日期', () => { - const yesterdayString = new Date(mockCurrentDate.getTime() - 24 * 60 * 60 * 1000).toISOString(); - expect(dateUtils.isYesterday(yesterdayString)).toBe(true); - }); - - it('应该处理不同时间但昨天的日期', () => { - const yesterdayMorning = new Date(mockCurrentDate.getTime() - 24 * 60 * 60 * 1000); - yesterdayMorning.setHours(0, 0, 0, 0); - - const yesterdayEvening = new Date(mockCurrentDate.getTime() - 24 * 60 * 60 * 1000); - yesterdayEvening.setHours(23, 59, 59, 999); - - expect(dateUtils.isYesterday(yesterdayMorning)).toBe(true); - expect(dateUtils.isYesterday(yesterdayEvening)).toBe(true); - }); - }); -}); diff --git a/test/unit/utils/string.test.ts b/test/unit/utils/string.test.ts deleted file mode 100644 index 63f2e94..0000000 --- a/test/unit/utils/string.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { stringUtils } from '@/utils/string'; - -describe('stringUtils', () => { - describe('truncate', () => { - it('应该截断长字符串并添加省略号', () => { - const longString = 'This is a very long string that should be truncated'; - const result = stringUtils.truncate(longString, 20); - expect(result).toBe('This is a very lo...'); - }); - - it('应该使用自定义后缀', () => { - const longString = 'This is a very long string that should be truncated'; - const result = stringUtils.truncate(longString, 21, ' [more]'); - expect(result).toBe('This is a very [more]'); - }); - - it('不应该截断短字符串', () => { - const shortString = 'Short string'; - const result = stringUtils.truncate(shortString, 20); - expect(result).toBe(shortString); - }); - - it('应该处理长度等于最大长度的字符串', () => { - const exactString = 'Exact length'; - const result = stringUtils.truncate(exactString, 12); - expect(result).toBe(exactString); - }); - }); - - describe('capitalize', () => { - it('应该将字符串首字母大写', () => { - expect(stringUtils.capitalize('hello')).toBe('Hello'); - expect(stringUtils.capitalize('world')).toBe('World'); - }); - - it('应该处理空字符串', () => { - expect(stringUtils.capitalize('')).toBe(''); - }); - - it('应该保持已大写的首字母', () => { - expect(stringUtils.capitalize('Hello')).toBe('Hello'); - }); - - it('应该只改变首字母,保持其余部分不变', () => { - expect(stringUtils.capitalize('hELLO')).toBe('HELLO'); - }); - }); - - describe('toCamelCase', () => { - it('应该将短横线分隔的字符串转换为驼峰命名', () => { - expect(stringUtils.toCamelCase('hello-world')).toBe('helloWorld'); - expect(stringUtils.toCamelCase('my-variable-name')).toBe('myVariableName'); - }); - - it('应该将下划线分隔的字符串转换为驼峰命名', () => { - expect(stringUtils.toCamelCase('hello_world')).toBe('helloWorld'); - expect(stringUtils.toCamelCase('my_variable_name')).toBe('myVariableName'); - }); - - it('应该将空格分隔的字符串转换为驼峰命名', () => { - expect(stringUtils.toCamelCase('hello world')).toBe('helloWorld'); - expect(stringUtils.toCamelCase('my variable name')).toBe('myVariableName'); - }); - - it('应该处理混合分隔符', () => { - expect(stringUtils.toCamelCase('hello-world_test variable')).toBe('helloWorldTestVariable'); - }); - - it('应该处理空字符串', () => { - expect(stringUtils.toCamelCase('')).toBe(''); - }); - - it('应该处理已经驼峰命名的字符串', () => { - expect(stringUtils.toCamelCase('helloWorld')).toBe('helloWorld'); - }); - - it('应该将首字母小写', () => { - expect(stringUtils.toCamelCase('Hello-World')).toBe('helloWorld'); - expect(stringUtils.toCamelCase('HELLO_WORLD')).toBe('helloWorld'); - }); - }); - - describe('toKebabCase', () => { - it('应该将驼峰命名转换为短横线分隔', () => { - expect(stringUtils.toKebabCase('helloWorld')).toBe('hello-world'); - expect(stringUtils.toKebabCase('myVariableName')).toBe('my-variable-name'); - }); - - it('应该处理已短横线分隔的字符串', () => { - expect(stringUtils.toKebabCase('hello-world')).toBe('hello-world'); - }); - - it('应该处理下划线分隔的字符串', () => { - expect(stringUtils.toKebabCase('hello_world')).toBe('hello-world'); - }); - - it('应该处理空字符串', () => { - expect(stringUtils.toKebabCase('')).toBe(''); - }); - }); - - describe('toSnakeCase', () => { - it('应该将驼峰命名转换为下划线分隔', () => { - expect(stringUtils.toSnakeCase('helloWorld')).toBe('hello_world'); - expect(stringUtils.toSnakeCase('myVariableName')).toBe('my_variable_name'); - }); - - it('应该处理已下划线分隔的字符串', () => { - expect(stringUtils.toSnakeCase('hello_world')).toBe('hello_world'); - }); - - it('应该处理短横线分隔的字符串', () => { - expect(stringUtils.toSnakeCase('hello-world')).toBe('hello_world'); - }); - - it('应该处理空字符串', () => { - expect(stringUtils.toSnakeCase('')).toBe(''); - }); - }); - - describe('escapeHtml', () => { - it('应该转义HTML特殊字符', () => { - expect(stringUtils.escapeHtml('
hello & world
')).toBe('<div>hello & world</div>'); - expect(stringUtils.escapeHtml('a "quote" and \'apostrophe\'')).toBe('a "quote" and 'apostrophe''); - }); - - it('应该处理不包含特殊字符的字符串', () => { - expect(stringUtils.escapeHtml('plain text')).toBe('plain text'); - }); - - it('应该处理空字符串', () => { - expect(stringUtils.escapeHtml('')).toBe(''); - }); - - it('应该只转义已知的特殊字符', () => { - expect(stringUtils.escapeHtml('hello @ world #test')).toBe('hello @ world #test'); - }); - }); - - describe('randomString', () => { - it('应该生成指定长度的随机字符串', () => { - const length = 10; - const result = stringUtils.randomString(length); - expect(result).toHaveLength(length); - }); - - it('应该生成不同的随机字符串', () => { - const result1 = stringUtils.randomString(10); - const result2 = stringUtils.randomString(10); - expect(result1).not.toBe(result2); - }); - - it('应该使用默认长度', () => { - const result = stringUtils.randomString(); - expect(result).toHaveLength(10); - }); - - it('应该只包含字母和数字', () => { - const result = stringUtils.randomString(100); - const regex = /^[A-Za-z0-9]+$/; - expect(regex.test(result)).toBe(true); - }); - }); - - describe('isEmpty', () => { - it('应该识别空字符串', () => { - expect(stringUtils.isEmpty('')).toBe(true); - expect(stringUtils.isEmpty(' ')).toBe(true); - expect(stringUtils.isEmpty('\t\n')).toBe(true); - }); - - it('应该识别非空字符串', () => { - expect(stringUtils.isEmpty('hello')).toBe(false); - expect(stringUtils.isEmpty(' hello ')).toBe(false); - }); - - it('应该处理null和undefined', () => { - expect(stringUtils.isEmpty(null)).toBe(true); - expect(stringUtils.isEmpty(undefined)).toBe(true); - }); - }); -}); diff --git a/test/unit/utils/validation.test.ts b/test/unit/utils/validation.test.ts deleted file mode 100644 index 621c823..0000000 --- a/test/unit/utils/validation.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { validationUtils } from '@/utils/validation'; - -describe('validationUtils', () => { - describe('isEmail', () => { - it('应该验证有效的邮箱地址', () => { - expect(validationUtils.isEmail('test@example.com')).toBe(true); - expect(validationUtils.isEmail('user.name@domain.co.uk')).toBe(true); - expect(validationUtils.isEmail('user+tag@example.org')).toBe(true); - }); - - it('应该拒绝无效的邮箱地址', () => { - expect(validationUtils.isEmail('')).toBe(false); - expect(validationUtils.isEmail('test')).toBe(false); - expect(validationUtils.isEmail('test@')).toBe(false); - expect(validationUtils.isEmail('@example.com')).toBe(false); - expect(validationUtils.isEmail('test@.com')).toBe(false); - expect(validationUtils.isEmail('test@example')).toBe(false); - expect(validationUtils.isEmail('test example.com')).toBe(false); - }); - }); - - describe('isPhoneNumber', () => { - it('应该验证有效的手机号(中国大陆)', () => { - expect(validationUtils.isPhoneNumber('13812345678')).toBe(true); - expect(validationUtils.isPhoneNumber('15987654321')).toBe(true); - expect(validationUtils.isPhoneNumber('18612345678')).toBe(true); - }); - - it('应该拒绝无效的手机号', () => { - expect(validationUtils.isPhoneNumber('')).toBe(false); - expect(validationUtils.isPhoneNumber('12345678901')).toBe(false); - expect(validationUtils.isPhoneNumber('12812345678')).toBe(false); - expect(validationUtils.isPhoneNumber('1381234567')).toBe(false); - expect(validationUtils.isPhoneNumber('138123456789')).toBe(false); - expect(validationUtils.isPhoneNumber('abcdefghijk')).toBe(false); - }); - }); - - describe('isIdCard', () => { - it('应该验证有效的身份证号(中国大陆)', () => { - expect(validationUtils.isIdCard('123456789012345')).toBe(true); // 15位 - expect(validationUtils.isIdCard('123456789012345678')).toBe(true); // 18位数字 - expect(validationUtils.isIdCard('12345678901234567X')).toBe(true); // 18位以X结尾 - expect(validationUtils.isIdCard('12345678901234567x')).toBe(true); // 18位以x结尾 - }); - - it('应该拒绝无效的身份证号', () => { - expect(validationUtils.isIdCard('')).toBe(false); - expect(validationUtils.isIdCard('12345678901234')).toBe(false); // 14位 - expect(validationUtils.isIdCard('1234567890123456')).toBe(false); // 16位 - expect(validationUtils.isIdCard('1234567890123456789')).toBe(false); // 19位 - expect(validationUtils.isIdCard('12345678901234567Y')).toBe(false); // 18位以Y结尾 - }); - }); - - describe('isStrongPassword', () => { - it('应该验证强密码', () => { - expect(validationUtils.isStrongPassword('Password123!')).toBe(true); - expect(validationUtils.isStrongPassword('StrongPass@2023')).toBe(true); - expect(validationUtils.isStrongPassword('Pass123!')).toBe(true); - }); - - it('应该拒绝弱密码', () => { - expect(validationUtils.isStrongPassword('')).toBe(false); - expect(validationUtils.isStrongPassword('password')).toBe(false); // 没有大写字母、数字和特殊字符 - expect(validationUtils.isStrongPassword('PASSWORD')).toBe(false); // 没有小写字母、数字和特殊字符 - expect(validationUtils.isStrongPassword('12345678')).toBe(false); // 没有字母和特殊字符 - expect(validationUtils.isStrongPassword('Password')).toBe(false); // 没有数字和特殊字符 - expect(validationUtils.isStrongPassword('Password123')).toBe(false); // 没有特殊字符 - }); - }); - - describe('isEmpty', () => { - it('应该识别空值', () => { - expect(validationUtils.isEmpty(null)).toBe(true); - expect(validationUtils.isEmpty(undefined)).toBe(true); - expect(validationUtils.isEmpty('')).toBe(true); - expect(validationUtils.isEmpty(' ')).toBe(true); - expect(validationUtils.isEmpty([])).toBe(true); - expect(validationUtils.isEmpty({})).toBe(true); - }); - - it('应该识别非空值', () => { - expect(validationUtils.isEmpty('text')).toBe(false); - expect(validationUtils.isEmpty(' text ')).toBe(false); - expect(validationUtils.isEmpty([1, 2, 3])).toBe(false); - expect(validationUtils.isEmpty({ key: 'value' })).toBe(false); - expect(validationUtils.isEmpty(0)).toBe(false); - expect(validationUtils.isEmpty(false)).toBe(false); - }); - }); - - describe('isInRange', () => { - it('应该验证范围内的数字', () => { - expect(validationUtils.isInRange(5, 1, 10)).toBe(true); - expect(validationUtils.isInRange(1, 1, 10)).toBe(true); - expect(validationUtils.isInRange(10, 1, 10)).toBe(true); - }); - - it('应该拒绝超出范围的数字', () => { - expect(validationUtils.isInRange(0, 1, 10)).toBe(false); - expect(validationUtils.isInRange(11, 1, 10)).toBe(false); - }); - }); - - describe('isLengthValid', () => { - it('应该验证有效长度', () => { - expect(validationUtils.isLengthValid('hello', 3, 10)).toBe(true); - expect(validationUtils.isLengthValid('hello', 5)).toBe(true); - expect(validationUtils.isLengthValid('hello', 1, 5)).toBe(true); - }); - - it('应该拒绝无效长度', () => { - expect(validationUtils.isLengthValid('hello', 6, 10)).toBe(false); // 太短 - expect(validationUtils.isLengthValid('hello', 1, 4)).toBe(false); // 太长 - expect(validationUtils.isLengthValid('', 1, 10)).toBe(false); // 太短 - }); - - it('应该处理只有最小长度的情况', () => { - expect(validationUtils.isLengthValid('hello', 5)).toBe(true); - expect(validationUtils.isLengthValid('hello', 6)).toBe(false); - }); - }); - - describe('isNumber', () => { - it('应该验证数字', () => { - expect(validationUtils.isNumber(0)).toBe(true); - expect(validationUtils.isNumber(123)).toBe(true); - expect(validationUtils.isNumber(-456)).toBe(true); - expect(validationUtils.isNumber(3.14)).toBe(true); - }); - - it('应该拒绝非数字', () => { - expect(validationUtils.isNumber('123')).toBe(false); - expect(validationUtils.isNumber(NaN)).toBe(false); - expect(validationUtils.isNumber(Infinity)).toBe(true); // Infinity被认为是数字 - expect(validationUtils.isNumber(null)).toBe(false); - expect(validationUtils.isNumber(undefined)).toBe(false); - }); - }); - - describe('isInteger', () => { - it('应该验证整数', () => { - expect(validationUtils.isInteger(0)).toBe(true); - expect(validationUtils.isInteger(123)).toBe(true); - expect(validationUtils.isInteger(-456)).toBe(true); - }); - - it('应该拒绝非整数', () => { - expect(validationUtils.isInteger(3.14)).toBe(false); - expect(validationUtils.isInteger('123')).toBe(false); - expect(validationUtils.isInteger(NaN)).toBe(false); - expect(validationUtils.isInteger(null)).toBe(false); - }); - }); - - describe('isPositive', () => { - it('应该验证正数', () => { - expect(validationUtils.isPositive(1)).toBe(true); - expect(validationUtils.isPositive(123.456)).toBe(true); - }); - - it('应该拒绝非正数', () => { - expect(validationUtils.isPositive(0)).toBe(false); - expect(validationUtils.isPositive(-1)).toBe(false); - expect(validationUtils.isPositive(-123.456)).toBe(false); - }); - }); - - describe('isNegative', () => { - it('应该验证负数', () => { - expect(validationUtils.isNegative(-1)).toBe(true); - expect(validationUtils.isNegative(-123.456)).toBe(true); - }); - - it('应该拒绝非负数', () => { - expect(validationUtils.isNegative(0)).toBe(false); - expect(validationUtils.isNegative(1)).toBe(false); - expect(validationUtils.isNegative(123.456)).toBe(false); - }); - }); - - describe('isEven', () => { - it('应该验证偶数', () => { - expect(validationUtils.isEven(0)).toBe(true); - expect(validationUtils.isEven(2)).toBe(true); - expect(validationUtils.isEven(-4)).toBe(true); - }); - - it('应该拒绝奇数', () => { - expect(validationUtils.isEven(1)).toBe(false); - expect(validationUtils.isEven(3)).toBe(false); - expect(validationUtils.isEven(-5)).toBe(false); - }); - }); - - describe('isOdd', () => { - it('应该验证奇数', () => { - expect(validationUtils.isOdd(1)).toBe(true); - expect(validationUtils.isOdd(3)).toBe(true); - expect(validationUtils.isOdd(-5)).toBe(true); - }); - - it('应该拒绝偶数', () => { - expect(validationUtils.isOdd(0)).toBe(false); - expect(validationUtils.isOdd(2)).toBe(false); - expect(validationUtils.isOdd(-4)).toBe(false); - }); - }); - - describe('isLeapYear', () => { - it('应该验证闰年', () => { - expect(validationUtils.isLeapYear(2000)).toBe(true); // 能被400整除 - expect(validationUtils.isLeapYear(2020)).toBe(true); // 能被4整除但不能被100整除 - }); - - it('应该拒绝非闰年', () => { - expect(validationUtils.isLeapYear(1900)).toBe(false); // 能被100整除但不能被400整除 - expect(validationUtils.isLeapYear(2023)).toBe(false); // 不能被4整除 - }); - }); - -}); diff --git a/test/utils/test-helpers.ts b/test/utils/test-helpers.ts deleted file mode 100644 index 2aa0bc6..0000000 --- a/test/utils/test-helpers.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * 测试工具函数 - * 提供测试中常用的辅助函数 - */ - -import { vi } from 'vitest'; - -// 等待指定时间(和await一起用) -export function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -// 等待下一个事件循环(和await一起用) -export function nextTick(): Promise { - return new Promise(resolve => setTimeout(resolve, 0)); -} - -// 创建一个可控制的Promise(将promise的控制权提取到外部) -export function createControlledPromise() { - // 用闭包把内部函数暴露到外部 - let resolve: (value: T) => void; - let reject: (reason?: any) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - - return { - promise, - resolve: resolve!, - reject: reject! - }; -} - -// 模拟localStorage(闭包) -export function createMockLocalStorage() { - let store: Record = {}; - - return { - getItem: vi.fn((key: string) => store[key] || null), - setItem: vi.fn((key: string, value: string) => { - store[key] = value; - }), - removeItem: vi.fn((key: string) => { - delete store[key]; - }), - clear: vi.fn(() => { - store = {}; - }), - get length() { - return Object.keys(store).length; - }, - key: vi.fn((index: number) => { - const keys = Object.keys(store); - return keys[index] || null; - }) - }; -} - -// 模拟sessionStorage -export function createMockSessionStorage() { - return createMockLocalStorage(); -} - -// 创建一个可读写的流 -// 创建一个可读写的事件流 -export function createMockStream() { - const listeners: Record = {}; - - // 先创建对象引用 - const stream = { - on: vi.fn((event: string, callback: Function) => { - if (!listeners[event]) { - listeners[event] = []; - } - listeners[event].push(callback); - return stream; // 使用对象引用而不是 this - }), - - emit: vi.fn((event: string, ...args: any[]) => { - if (listeners[event]) { - listeners[event].forEach(callback => callback(...args)); - } - return stream; // 使用对象引用 - }), - - once: vi.fn((event: string, callback: Function) => { - const onceWrapper = (...args: any[]) => { - callback(...args); - // 移除监听器 - if (listeners[event]) { - listeners[event] = listeners[event].filter(cb => cb !== onceWrapper); - } - }; - return stream.on(event, onceWrapper); // 使用对象引用 - }), - - removeListener: vi.fn((event: string, callback: Function) => { - if (listeners[event]) { - listeners[event] = listeners[event].filter(cb => cb !== callback); - } - return stream; // 使用对象引用 - }), - - removeAllListeners: vi.fn((event?: string) => { - if (event) { - delete listeners[event]; - } else { - Object.keys(listeners).forEach(key => { - delete listeners[key]; - }); - } - return stream; // 使用对象引用 - }) - }; - - return stream; -} - -// 模拟IntersectionObserver -export function createMockIntersectionObserver() { - const callbacks: Function[] = []; - - return { - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), - root: null, - rootMargin: '', - thresholds: [], - constructor: vi.fn((callback: Function) => { - callbacks.push(callback); - return { - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn() - }; - }) - }; -} - -// 模拟ResizeObserver -export function createMockResizeObserver() { - const callbacks: Function[] = []; - - return { - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), - constructor: vi.fn((callback: Function) => { - callbacks.push(callback); - return { - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn() - }; - }) - }; -} diff --git a/types/chat/api.ts b/types/chat/api.ts index e3b1f0e..31b9844 100644 --- a/types/chat/api.ts +++ b/types/chat/api.ts @@ -1,5 +1,6 @@ +import type { PaginationRequest, PaginationResponse } from '../common'; import type { ChatSession, ChatMessage } from './base'; -import type { ChatMessageType } from './enum'; +import type { ChatMessageType } from './types'; // 创建聊天会话请求接口 export interface CreateChatSessionRequest { @@ -7,92 +8,55 @@ export interface CreateChatSessionRequest { participantId: string; } -// 更新聊天会话请求接口(占位) -export interface UpdateChatSessionRequest { - // 只能用于处理后台逻辑 - sessionId: string; +// 创建聊天会话响应接口 +export interface CreateChatSessionResponse { + success: boolean; + chatSessionId: string; // 成功时返回会话ID,应该是点击私信就跳转到私信 } + // 发送消息请求接口 export interface SendMessageRequest { - sessionId: string; + chatSessionId: string; // cookie存不了这个,所以得在请求体里 content: string; type: ChatMessageType; - metadata?: Record; +} + +export interface SendMessageResponse { + success: boolean; } // 获取聊天会话列表请求接口 -export interface GetChatSessionsRequest { - page?: number; - limit?: number; +export interface GetChatSessionsRequest extends PaginationRequest { + // 足矣 } // 获取聊天会话列表响应接口 -export interface GetChatSessionsResponse { - sessions: ChatSession[]; - total: number; - page: number; - limit: number; +export interface GetChatSessionsResponse extends PaginationResponse { + // 足矣 } + // 获取聊天消息请求接口 -export interface GetChatMessagesRequest { - sessionId: string; - page?: number; - limit?: number; +export interface GetChatMessagesRequest extends PaginationRequest { + chatSessionId: string; before?: string; // 消息ID,获取该消息之前的消息 after?: string; // 消息ID,获取该消息之后的消息 } // 获取聊天消息响应接口 -export interface GetChatMessagesResponse { - messages: ChatMessage[]; - total: number; - page: number; - limit: number; - hasMore: boolean; +export interface GetChatMessagesResponse extends PaginationResponse { + // 足矣 } -// 标记消息已读请求接口 -export interface MarkMessagesAsReadRequest { - sessionId: string; - messageIds: string[]; -} - -// 标记消息已读响应接口(已读状态只面向接收方) -export interface MarkMessagesAsReadResponse { - success: boolean; - markedMessageIds: string[]; // 成功标记的消息ID - failedMessageIds?: string[]; // 失败的消息ID -} - -// 搜索聊天消息请求接口 -export interface SearchChatMessagesRequest { - sessionId?: string; +// 搜索聊天消息/会话请求接口 +export interface SearchChatMessagesRequest extends PaginationRequest { query: string; - page?: number; - limit?: number; } // 搜索聊天消息响应接口 -export interface SearchChatMessagesResponse { - messages: ChatMessage[]; - total: number; - page: number; - limit: number; +export interface SearchChatMessagesResponse extends PaginationResponse { + // 足矣 } -// 搜索聊天会话请求接口 -export interface SearchChatSessionsRequest { - query: string; - page?: number; - limit?: number; -} -// 搜索聊天会话响应接口 -export interface SearchChatSessionsResponse { - sessions: ChatSession[]; - total: number; - page: number; - limit: number; -} diff --git a/types/chat/base.ts b/types/chat/base.ts index 5044751..6e939a3 100644 --- a/types/chat/base.ts +++ b/types/chat/base.ts @@ -1,47 +1,25 @@ -import type { User } from '../index'; -import type { ChatMessageType, ChatMessageStatus } from './enum'; +import type { UserProfile } from '../user'; +import type { ChatMessageType } from './types'; +import type { BaseEntity } from '../common'; // 聊天消息接口 -export interface ChatMessage { - id: string; +export interface ChatMessage extends BaseEntity { // 聊天会话id,下面那个接口的id - sessionId: string; - sender: User; - receiver: User; - content: string; - type: ChatMessageType; - status: ChatMessageStatus; + chatSessionId: string; + sender: UserProfile; + receiver: UserProfile; + content: string; // 只支持文本信息 + type: ChatMessageType; // 实际无用 createdAt: Date; - // 非文本消息类型的元数据 - metadata?: { - fileName?: string; - fileSize?: number; - duration?: number; - // 小图预览 - thumbnail?: string; - [key: string]: unknown; - }; + // 无元数据,因为只有文本消息 } // 聊天会话接口 -export interface ChatSession { +export interface ChatSession extends BaseEntity { id: string; - participant1Id: string; - participant2Id: string; - participant1: User; - participant2: User; - lastMessage?: { - id: string; - content: string; - senderId: string; - createdAt: Date; - }; - unreadCount1: number; // 参与者1的未读消息数 - unreadCount2: number; // 参与者2的未读消息数 - createdAt: Date; - updatedAt: Date; - // 会话元数据(特殊标记、背景设置、自定义属性等等) - metadata?: { - [key: string]: unknown; - }; + participant1: UserProfile; + participant2: UserProfile; + // 用于在聊天列表显示最后一句话 + lastMessage?: ChatMessage; + // 没有已读和未读 } diff --git a/types/chat/enum.ts b/types/chat/enum.ts deleted file mode 100644 index d925766..0000000 --- a/types/chat/enum.ts +++ /dev/null @@ -1,19 +0,0 @@ -// 聊天消息类型枚举(暂时用不到的) -export enum ChatMessageType { - TEXT = 'text', - IMAGE = 'image', - FILE = 'file', - VOICE = 'voice', - VIDEO = 'video', - SYSTEM = 'system' -} - -// 聊天消息状态枚举(暂时用不到的) -export enum ChatMessageStatus { - SENDING = 'sending', - SENT = 'sent', - DELIVERED = 'delivered', - READ = 'read', - FAILED = 'failed' -} - diff --git a/types/chat/index.ts b/types/chat/index.ts index cdcce8d..f2e8cec 100644 --- a/types/chat/index.ts +++ b/types/chat/index.ts @@ -1,3 +1,3 @@ -export * from './enum'; export * from './base'; export * from './api'; +export * from './types'; diff --git a/types/chat/types.ts b/types/chat/types.ts new file mode 100644 index 0000000..e18dbd3 --- /dev/null +++ b/types/chat/types.ts @@ -0,0 +1,4 @@ +// 聊天消息类型枚举(暂时只支持文本) +export type ChatMessageType = + | 'text'; + diff --git a/types/common.ts b/types/common.ts new file mode 100644 index 0000000..46ac9b7 --- /dev/null +++ b/types/common.ts @@ -0,0 +1,26 @@ +export interface PaginationRequest { + page?: number; + limit?: number; +} + +export interface PaginationResponse { + data: T[]; // 数据列表 + total: number; // 总数 + page: number; // 当前页码 + limit: number; // 每页数量 + hasMore?: boolean; // 是否有更多数据 +} + +// 基础实体接口 +export interface BaseEntity { + id: string; // 唯一标识符 + createdAt: Date; // 创建时间 + // 没有更新时间(不支持更新) +} + +// 基础数据统计接口 +export interface BaseStats { + stars?: number; // 收藏数 + likes?: number; // 点赞数 + comments?: number; // 评论数 +} \ No newline at end of file diff --git a/types/index.ts b/types/index.ts index 28536d1..a86b6ed 100644 --- a/types/index.ts +++ b/types/index.ts @@ -3,3 +3,5 @@ export * from './chat'; export * from './model'; export * from './post'; export * from './user'; +export * from './upload'; +export * from './common'; diff --git a/types/model/api.ts b/types/model/api.ts index 0140b30..9363bce 100644 --- a/types/model/api.ts +++ b/types/model/api.ts @@ -1,57 +1,26 @@ +import type { PaginationRequest, PaginationResponse } from 'types/common'; import type { ModelComment, AIModel } from './base'; -import type { CommentSortType } from './enum'; -// AI模型榜单项接口 -export interface AIModelRankingItem { - model: AIModel; - rank: number; - change?: 'up' | 'down' | 'same'; // 排名变化,未来扩展 -} - -// 获取AI模型广场数据请求接口 -export interface GetAIPlazaRequest { - // 模型卡片数据是静态配置(手动维护)的,不需要分页参数 -} - -// 获取AI模型广场数据响应接口 -export interface GetAIPlazaResponse { - models: AIModel[]; // 模型卡片列表,由管理员预定义 - hotRankings: AIModelRankingItem[]; // 热评模型榜单 - clickRankings: AIModelRankingItem[]; // 点击排行榜 -} // 获取模型详情请求接口 -export interface GetModelDetailRequest { - modelId: string; - page?: number; // 评论页码 - limit?: number; // 每页评论数量 - sortBy?: CommentSortType; // 评论排序方式,未来扩展功能 +export interface GetModelsRequest extends PaginationRequest { + // 够了 } // 获取模型详情响应接口 -export interface GetModelDetailResponse { - model: AIModel; - comments: ModelComment[]; // 使用新的ModelComment类型,已经是数组 - totalComments: number; - hasMoreComments: boolean; // 是否还有更多评论,支持无限滚动加载 +export interface GetModelsResponse extends PaginationResponse { + // 够了 } // 批量获取模型评论请求接口 -export interface GetModelCommentsRequest { +export interface GetModelCommentsRequest extends PaginationRequest { modelId: string; - page?: number; // 页码 - limit?: number; // 每页评论数量 - sortBy?: CommentSortType; // 评论排序方式 - parentId?: string; // 父评论ID,用于获取回复 + parentId?: string; // 父评论ID } // 批量获取模型评论响应接口 -export interface GetModelCommentsResponse { - comments: ModelComment[]; // 评论列表 - total: number; // 总数 - page: number; // 当前页码 - limit: number; // 每页数量 - hasMore: boolean; // 是否有更多数据 +export interface GetModelCommentsResponse extends PaginationResponse { + // 够了 } // 发表模型评论请求接口 @@ -63,98 +32,19 @@ export interface CreateModelCommentRequest { // 发表模型评论响应接口 export interface CreateModelCommentResponse { - comment: ModelComment; // 使用新的ModelComment类型 -} - -// 点赞模型评论请求接口 -export interface LikeModelCommentRequest { - commentId: string; -} - -// 点赞模型评论响应接口 -export interface LikeModelCommentResponse { success: boolean; } -// 记录模型点击请求接口 -export interface RecordModelClickRequest { +// 点赞模型请求接口 +export interface LikeModelRequest { modelId: string; } -// 记录模型点击响应接口 -export interface RecordModelClickResponse { - success: boolean; - clickCount: number; // 更新后的总点击数 -} - -// 后面的都暂时不考虑 -// 删除模型评论请求接口 -export interface DeleteModelCommentRequest { - commentId: string; -} - -// 删除模型评论响应接口 -export interface DeleteModelCommentResponse { +// 点赞模型响应接口 +export interface LikeModelResponse { success: boolean; } -// 更新模型评论请求接口 -export interface UpdateModelCommentRequest { - commentId: string; - content?: string; // 更新内容 -} -// 更新模型评论响应接口 -export interface UpdateModelCommentResponse { - success: boolean; - comment: ModelComment; // 返回更新后的评论 -} -// 批量点赞模型评论请求接口 -export interface BatchLikeModelCommentsRequest { - commentIds: string[]; // 评论ID列表 -} - -// 批量点赞模型评论响应接口 -export interface BatchLikeModelCommentsResponse { - results: Array<{ - commentId: string; - success: boolean; - comment?: ModelComment; // 成功时返回更新后的评论 - error?: string; // 失败时的错误信息 - }>; -} - -// 批量删除模型评论请求接口 -export interface BatchDeleteModelCommentsRequest { - commentIds: string[]; // 评论ID列表 -} - -// 批量删除模型评论响应接口 -export interface BatchDeleteModelCommentsResponse { - results: Array<{ - commentId: string; - success: boolean; - error?: string; // 失败时的错误信息 - }>; -} - -// 批量更新模型评论请求接口 -export interface BatchUpdateModelCommentsRequest { - updates: Array<{ - commentId: string; - content?: string; // 更新内容 - rating?: number; // 更新评分 - }>; -} - -// 批量更新模型评论响应接口 -export interface BatchUpdateModelCommentsResponse { - results: Array<{ - commentId: string; - success: boolean; - comment?: ModelComment; // 成功时返回更新后的评论 - error?: string; // 失败时的错误信息 - }>; -} diff --git a/types/model/base.ts b/types/model/base.ts index 0d37bd7..c99f26d 100644 --- a/types/model/base.ts +++ b/types/model/base.ts @@ -1,32 +1,17 @@ -import type { BaseEntity, BaseUser } from '../post/base'; - -// AI模型评论统计信息接口 -export interface ModelCommentStats { - stars: number; // 收藏数 - likes: number; // 点赞数 - comments: number; // 评论数 - replies: number; // 回复数 -} +import type { BaseEntity, BaseStats } from '../common'; // AI模型评论接口 export interface ModelComment extends BaseEntity { modelId: string; // 模型ID - authorId: string; // 作者ID - author: BaseUser; // 作者信息 content: string; // 评论内容 parentId?: string; // 父评论ID,用于嵌套评论 - // 其实没必要 - stats: ModelCommentStats; // 统计信息 } // AI模型接口 -export interface AIModel { - id: string; +export interface AIModel extends BaseEntity, BaseStats { name: string; description: string; avatar?: string; // 模型头像 - tags?: string[]; // 模型标签 - website?: string; // 官方网站 - clickCount?: number; // 点击次数 - likeCount?: number; // 点赞次数 + tags: string[]; // 模型标签(这个反正是静态的,必须有) + website: string; // 官方网站(必须有) } diff --git a/types/model/enum.ts b/types/model/enum.ts deleted file mode 100644 index 5466217..0000000 --- a/types/model/enum.ts +++ /dev/null @@ -1,7 +0,0 @@ -// AI模型评论排序枚举 -// 和post的重复了,而且字段还不一样 -export enum CommentSortType { - LATEST = 'latest', // 最新 - HOTTEST = 'hottest', // 最热 - HIGHEST_RATING = 'highest_rating' // 评分最高 -} diff --git a/types/model/index.ts b/types/model/index.ts index cb387c4..5301537 100644 --- a/types/model/index.ts +++ b/types/model/index.ts @@ -1,4 +1,4 @@ // 导出所有模型相关的类型定义 export * from './base'; export * from './api'; -export * from './enum'; +export * from './types'; diff --git a/types/model/types.ts b/types/model/types.ts new file mode 100644 index 0000000..006b365 --- /dev/null +++ b/types/model/types.ts @@ -0,0 +1,2 @@ +// 只有排序 +// 暂时不排序 \ No newline at end of file diff --git a/types/post/api.ts b/types/post/api.ts index 6f40c82..7b0e98f 100644 --- a/types/post/api.ts +++ b/types/post/api.ts @@ -1,52 +1,26 @@ -import type { BaseEntityContent, Post, PostComment } from './base'; -import type { - PostType, - PostSortBy, - CommentSortBy, - SortOrder -} from './enum'; +import type { Post, PostComment, PostType, PostContent } from './base'; +import type { PaginationRequest, PaginationResponse } from '../common'; // 创建帖子请求接口 -export interface CreatePostRequest extends BaseEntityContent { - type: PostType; // 帖子类型:提问或文章 - images?: string[]; // 图片 - publishedAt?: Date; // 发布时间 +export interface CreatePostRequest { + title: string; + content: PostContent; + type: PostType; } -// 创建帖子响应接口 +// 创建帖子响应接口(整体替换资源,直接返回结果就够了) export interface CreatePostResponse { - post: Post; + success: boolean; } -// 获取帖子列表请求接口 -export interface GetPostsRequest { - page?: number; // 页码 - limit?: number; // 每页数量 - sortBy?: PostSortBy; // 帖子排序方式 - type?: PostType; // 帖子类型:提问或文章 - sortOrder?: SortOrder; // 排序方向 - authorId?: string; - search?: string; +// 获取帖子列表请求接口(简单,但是为了之后的扩展性保持独立) +export interface GetPostsRequest extends PaginationRequest { + // 已继承page和limit字段 } // 获取帖子列表响应接口 -export interface GetPostsResponse { - data: Post[]; // 数据列表 - total: number; // 总数 - page: number; // 当前页码 - limit: number; // 每页数量 - hasMore: boolean; // 是否有更多数据 - sortBy?: PostSortBy; // 帖子排序方式 -} - -// 获取帖子详情请求接口 -export interface GetPostRequest { - postId: string; -} - -// 获取帖子详情响应接口 -export interface GetPostResponse { - post: Post; +export interface GetPostsResponse extends PaginationResponse { + // 已继承所有分页相关字段 } // 点赞帖子请求接口 @@ -71,6 +45,7 @@ export interface BookmarkPostResponse { // 创建评论请求接口 export interface CreateCommentRequest { + // 作者信息让后端去计算,保持纯粹性 postId: string; content: string; parentId?: string; @@ -78,43 +53,25 @@ export interface CreateCommentRequest { // 创建评论响应接口 export interface CreateCommentResponse { - comment: PostComment; + success: boolean; } // 获取评论列表请求接口 -export interface GetCommentsRequest { - page?: number; // 页码 - limit?: number; // 每页数量 - sortOrder?: SortOrder; // 排序方向 +export interface GetCommentsRequest extends PaginationRequest { postId: string; parentId?: string; - sortBy?: CommentSortBy; } // 获取评论列表响应接口 -export interface GetCommentsResponse { - data: PostComment[]; // 数据列表 - total: number; // 总数 - page: number; // 当前页码 - limit: number; // 每页数量 - hasMore: boolean; // 是否有更多数据 +export interface GetCommentsResponse extends PaginationResponse { + // 已继承所有分页相关字段 } -// 点赞评论请求接口 -export interface LikeCommentRequest { - commentId: string; -} - -// 点赞评论响应接口 -export interface LikeCommentResponse { - success: boolean; -} - -// 获取热门帖子请求接口 -export interface GetHotPostsRequest { - limit?: number; // 每页数量 - days?: number; // 统计天数,默认7天 +// 获取热门帖子请求接口(简单,独立,扩展性) +export interface GetHotPostsRequest extends PaginationRequest { + // 已继承limit字段 + // 用不着统计天数 } // 获取热门帖子响应接口 @@ -123,22 +80,18 @@ export interface GetHotPostsResponse { total: number; // 总数 } -// 获取帖子榜单请求接口 -export interface GetPostRankingRequest { - limit?: number; // 每页数量 - period?: 'day' | 'week' | 'month'; // 统计周期 - type?: 'views' | 'likes' | 'comments'; // 排序类型 +// 获取帖子榜单请求接口(简单,独立,扩展性) +export interface GetPostRankingRequest extends PaginationRequest { + // 已继承limit字段 } -// 获取帖子榜单响应接口 +// 获取帖子榜单响应接口(不存在无限滚动的可能) export interface GetPostRankingResponse { data: Post[]; // 数据列表 total: number; // 总数 - period: 'day' | 'week' | 'month'; // 统计周期 - type: 'views' | 'likes' | 'comments'; // 排序类型 } -// 后面全部暂时不考虑 +// 后面全部暂时不会实现 // 删除帖子请求接口 export interface DeletePostRequest { postId: string; diff --git a/types/post/base.ts b/types/post/base.ts index 53c282d..b27e79c 100644 --- a/types/post/base.ts +++ b/types/post/base.ts @@ -1,44 +1,26 @@ -import type {User} from '../user/base'; -import type {PostType} from './enum'; +import type { UserProfile } from '../user'; +import type { BaseEntity, BaseStats } from '../common'; -// 基础实体接口 -export interface BaseEntity { - id: string; // 唯一标识符 - createdAt: Date; // 创建时间 - updatedAt: Date; // 更新时间 -} +export type PostType = 'question' | 'article'; + +// 不搞fallback,统一用原子组件作为fallback +export type PostContent = + | { type: 'text'; value: string } + | { type: 'image'; src: string } + | { type: 'video'; src: string}; -// 用户基础信息接口 -export interface BaseUser { - // 其实应该用profile的 - user: User; -} export interface PostComment extends BaseEntity { - // 这里没带postId,如果后台管理需要,就要带 - // 重复了,而且评论没有数据(点赞、收藏等等) - authorId: string; // 作者ID - author: BaseUser; // 作者信息 - content: string; // 评论内容 + author: UserProfile; // 作者信息 + content: string; // 评论内容(暂时只支持文字评论) parentId?: string; // 父评论ID,用于嵌套评论 } -// 实体内容和统计信息基础接口 -export interface BaseEntityContent { - title?: string; // 标题 - excerpt: string; // 摘要 - tags?: string[]; // 标签数组 - metadata?: Record; // 元数据 - stars: number; // 收藏数 - likes: number; // 点赞数 - comments: number; // 评论数 -} - // 帖子接口 -export interface Post extends BaseEntity, BaseEntityContent { +export interface Post extends BaseEntity, BaseStats{ + title: string; // 标题 + // 没有标签数组 + content: PostContent[]; // 内容(文字、图片等) type: PostType; // 帖子类型:提问或文章 - authorId: string; // 作者ID - author: BaseUser; // 作者信息 - images?: string[]; // 图片数组 - publishedAt?: Date; // 发布时间 + author: UserProfile; // 作者信息 } diff --git a/types/post/enum.ts b/types/post/enum.ts deleted file mode 100644 index c30c058..0000000 --- a/types/post/enum.ts +++ /dev/null @@ -1,29 +0,0 @@ -// 这里应该用type联合的,现在有冗余和风格不统一的问题 -// 排序方式枚举 -export enum SortOrder { - ASC = 'asc', - DESC = 'desc' -} - -// 排序字段枚举 -export enum PostSortBy { - CREATED_AT = 'createdAt', - UPDATED_AT = 'updatedAt', - PUBLISHED_AT = 'publishedAt', - VIEWS = 'views', - LIKES = 'likes', - COMMENTS = 'comments' -} - -// 评论排序字段枚举 -export enum CommentSortBy { - CREATED_AT = 'createdAt', - LIKES = 'likes', - REPLIES = 'replies' -} - -// 帖子类型枚举 -export enum PostType { - QUESTION = 'question', - ARTICLE = 'article' -} diff --git a/types/post/index.ts b/types/post/index.ts index 2b5c3b1..c75d44b 100644 --- a/types/post/index.ts +++ b/types/post/index.ts @@ -1,4 +1,4 @@ // 导出帖子系统所有类型定义 export * from './base'; -export * from './enum'; +export * from './types'; export * from './api'; diff --git a/types/post/types.ts b/types/post/types.ts new file mode 100644 index 0000000..b5c005e --- /dev/null +++ b/types/post/types.ts @@ -0,0 +1,2 @@ +// 只有排序用到type +// 排序暂时不支持,先全部删了 \ No newline at end of file diff --git a/types/upload/api.ts b/types/upload/api.ts new file mode 100644 index 0000000..022f432 --- /dev/null +++ b/types/upload/api.ts @@ -0,0 +1,77 @@ +// 分片上传相关的API接口定义 + +/** + * 文件分片请求接口 + */ +export interface UploadChunkRequest { + /** 文件唯一标识 */ + fileId: string; + /** 分片索引,从0开始 */ + chunkIndex: number; + /** 总分片数量 */ + totalChunks: number; + /** 分片大小 */ + chunkSize: number; + /** 文件总大小 */ + totalSize: number; + /** 文件名称 */ + fileName: string; + /** 文件类型 */ + fileType: string; +} + +/** + * 文件分片响应接口 + */ +export interface UploadChunkResponse { + success: boolean; + message: string; + chunkIndex: number; + uploadedSize: number; + isLastChunk?: boolean; +} + +/** + * 合并分片请求接口 + */ +export interface MergeChunksRequest { + /** 文件唯一标识 */ + fileId: string; + /** 总分片数量 */ + totalChunks: number; + /** 文件名称 */ + fileName: string; + /** 文件类型 */ + fileType: string; + /** 文件总大小 */ + totalSize: number; +} + +/** + * 合并分片响应接口 + */ +export interface MergeChunksResponse { + success: boolean; + message: string; + fileUrl?: string; + fileId: string; + fileSize: number; +} + +/** + * 检查文件上传状态请求接口 + */ +export interface CheckUploadStatusRequest { + /** 文件唯一标识 */ + fileId: string; +} + +/** + * 检查文件上传状态响应接口 + */ +export interface CheckUploadStatusResponse { + success: boolean; + isUploaded: boolean; + uploadedChunks: number[]; + totalChunks?: number; +} diff --git a/types/upload/base.ts b/types/upload/base.ts new file mode 100644 index 0000000..e487e7c --- /dev/null +++ b/types/upload/base.ts @@ -0,0 +1,63 @@ +// 上传相关的基础类型定义 + +/** + * 上传状态枚举 + */ +export enum UploadStatus { + /** 初始状态 */ + INITIAL = 'INITIAL', + /** 上传中 */ + UPLOADING = 'UPLOADING', + /** 暂停 */ + PAUSED = 'PAUSED', + /** 已完成 */ + COMPLETED = 'COMPLETED', + /** 失败 */ + FAILED = 'FAILED' +} + +/** + * 文件分片信息 + */ +export interface FileChunk { + /** 分片索引 */ + index: number; + /** 分片大小 */ + size: number; + /** 分片起始位置 */ + start: number; + /** 分片结束位置 */ + end: number; + /** 是否已上传 */ + uploaded?: boolean; +} + +/** + * 上传任务信息 + */ +export interface UploadTask { + /** 任务ID */ + taskId: string; + /** 文件ID */ + fileId: string; + /** 文件信息 */ + file: File; + /** 上传状态 */ + status: UploadStatus; + /** 已上传大小 */ + uploadedSize: number; + /** 总大小 */ + totalSize: number; + /** 上传进度百分比 */ + progress: number; + /** 分片大小 */ + chunkSize: number; + /** 总分片数 */ + totalChunks: number; + /** 已上传分片索引列表 */ + uploadedChunks: number[]; + /** 创建时间 */ + createdAt: number; + /** 更新时间 */ + updatedAt: number; +} diff --git a/types/upload/index.ts b/types/upload/index.ts new file mode 100644 index 0000000..69aaf90 --- /dev/null +++ b/types/upload/index.ts @@ -0,0 +1,3 @@ +// 导出上传相关的类型定义 +export * from './api'; +export * from './base'; diff --git a/types/user/api.ts b/types/user/api.ts index 545358e..9ab8ef4 100644 --- a/types/user/api.ts +++ b/types/user/api.ts @@ -1,30 +1,99 @@ -import type { User } from './base'; +import type { UserProfile } from './base'; +import type { PaginationRequest } from '../common'; -// 获取热门作者请求接口 -export interface GetHotAuthorsRequest { - limit?: number; // 每页数量 - days?: number; // 统计天数,默认30天 +// 获取当前用户个人资料相关接口(不需要参数,所以没有request) +export interface GetMyProfileResponse { + profile: UserProfile; +} + +// 获取他人档案相关接口 +export interface GetUserProfileRequest { + userId: string; +} + +export interface GetUserProfileResponse { + profile: UserProfile; +} + +// 热门作者相关接口 +export interface GetHotAuthorsRequest extends PaginationRequest { + // 已继承limit字段(page字段默认不用了) } -// 获取热门作者响应接口 export interface GetHotAuthorsResponse { - // 其实应该用profile返回的 - data: User[]; // 数据列表 + data: UserProfile[]; // 数据列表 total: number; // 总数 } -// 获取作者榜单请求接口 -export interface GetAuthorRankingRequest { - limit?: number; // 每页数量 - period?: 'day' | 'week' | 'month'; // 统计周期 - type?: 'posts' | 'views' | 'likes'; // 排序类型 +// 登录注册相关接口 +export interface LoginRequest { + username: string; + password: string; +} + +export interface LoginResponse { + user: UserProfile; + sessionId: string; +} + +export interface RegisterRequest { + username: string; + password: string; +} + +export interface RegisterResponse { + user: UserProfile; + sessionId: string; +} + +export interface ChangePasswordRequest { + oldPassword: string; + newPassword: string; +} + +export interface ChangePasswordResponse { + user: UserProfile; + sessionId: string; +} + +// 通知相关接口 +export type NotificationType = 'like' | 'comment' | 'follow' | 'mention'; + +export interface UserNotification { + id: string; + type: NotificationType; + content: string; + isRead: boolean; + createdAt: Date; +} + +// 个人资料更新相关接口 +export interface UserProfileUpdateRequest { + avatar?: string; + introduction?: string; + level?: string; +} + +export interface UserProfileUpdateResponse { + profile: UserProfile; +} + +// 作者榜单相关接口 +export interface GetAuthorRankingRequest extends PaginationRequest { + // 已继承limit字段(page字段默认不用了) } -// 获取作者榜单响应接口 export interface GetAuthorRankingResponse { - data: User[]; // 数据列表 + data: UserProfile[]; // 数据列表 total: number; // 总数 - // 其实冗余了 - period: 'day' | 'week' | 'month'; // 统计周期 - type: 'posts' | 'views' | 'likes'; // 排序类型 +} + +// 用户搜索相关接口 +export interface UserSearchRequest { + keyword: string; +} + +export interface UserSearchResponse { + users: Array; + total: number; } diff --git a/types/user/base.ts b/types/user/base.ts index d783052..5a04c65 100644 --- a/types/user/base.ts +++ b/types/user/base.ts @@ -1,53 +1,14 @@ -import { NotificationType } from './enum'; - // 最小用户信息 export interface User{ id: string; username: string; } -// 认证 -export interface LoginRequest{ - username: string; - password: string; -} - -export interface LoginResponse{ +export interface UserProfile{ user: User; - sessionId: string; + avatar: string; + introduction: string; + level: string; } -export interface RegisterRequest { - username: string; - email: string; - password: string; -} -export interface RegisterResponse { - user: User; - sessionId: string; -} - -export interface ChangePasswordRequest { // 暂时用不到 - oldPassword: string; - newPassword: string; -} - -// 目前只用得到session -// export interface RefreshTokenRequest { -// sessionId: string; -// } - -// export interface RefreshTokenResponse { -// sessionId: string; -// } - -// 用户通知(暂时用不到) -export interface UserNotification { - id: string; - userId: string; - type: NotificationType; - content: string; - isRead: boolean; - createdAt: Date; -} diff --git a/types/user/enum.ts b/types/user/enum.ts deleted file mode 100644 index c9cec8c..0000000 --- a/types/user/enum.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 通知类型枚举 -export enum NotificationType { - LIKE = 'like', - COMMENT = 'comment', - FOLLOW = 'follow', - MENTION = 'mention' -} - diff --git a/types/user/index.ts b/types/user/index.ts index c206117..8a1785d 100644 --- a/types/user/index.ts +++ b/types/user/index.ts @@ -1,5 +1,2 @@ export * from './base'; -export * from './profile'; -export * from './search'; -export * from './enum'; -export * from './api'; +export * from './api'; \ No newline at end of file diff --git a/types/user/profile.ts b/types/user/profile.ts deleted file mode 100644 index 95c9168..0000000 --- a/types/user/profile.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { User } from './base'; - -// 用户档案以及更新 -export interface UserProfile{ - user: User; - avatar: string; - introduction: string; - level: string; -} - -export interface UserProfileUpdateRequest { - avatar?: string; - introduction?: string; - level?: string; -} - -export interface UserProfileUpdateResponse { - profile: UserProfile; -} - -// 用户关系 -export interface UserRelation { - // 当前用户的id - id: string; - followerId: string[]; - followingId: string[]; -} - -// 当前用户的id由cookie带上 -export interface UserFollowRequest { - userId: string; -} - -export interface UserFollowResponse { - success: boolean; -} - - diff --git a/types/user/search.ts b/types/user/search.ts deleted file mode 100644 index 7ad80e0..0000000 --- a/types/user/search.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { UserProfile } from './profile'; - -// 用户搜索结果 -export interface UserSearchRequest { - keyword: string; -} - -export interface UserSearchResponse { - // 这里逻辑是对的(可惜暂时不打算做) - users: Array - total: number; -} - diff --git a/utils/data.ts b/utils/data.ts index 9d38696..f62a892 100644 --- a/utils/data.ts +++ b/utils/data.ts @@ -3,20 +3,20 @@ * @param func 要节流的函数 * @param wait 等待时间(毫秒) * @param options 选项 - * + * * 解释 * leading 表示首次调用时立即执行函数 * trailing: true 表示最后一次调用后等待 wait 毫秒后执行函数 - * + * * 三种可能 * leading: true, trailing: true 表示首次调用时立即执行函数,最后一次调用后等待 wait 毫秒后补一次执行函数 * 第一次触发(remaining大于wait)->(if分支)立即执行 * 期间再次触发(remaining小于等于wait)->(else if分支)触发定时器,到时间补一次 - * + * * leading: true, trailing: false 表示首次调用时立即执行函数,最后一次调用后不等待 wait 毫秒执行函数 * 第一次触发(remaining大于wait)->(if分支)立即执行 * 期间再次触发(remaining小于等于wait)->不管 - * + * * leading: false, trailing: true 表示首次调用时不立即执行函数,最后一次调用后等待 wait 毫秒后执行函数 * 第一次触发(remaining等于wait)->(else if分支)触发定时器,到时间执行函数 * 期间再次触发->(else if分支)不断重置定时器,但remaining会不断减少 @@ -275,15 +275,15 @@ export const sortBy = ( * @param func 要防抖的函数 * @param wait 等待时间(毫秒) * @param immediate 是否立即执行 - * + * * 两种类型: * 1. 立即执行:第一次触发立即执行,之后触发只重置等待时间 * 2. 非立即执行:触发后等待时间结束才执行,期间触发会重置等待时间 - * + * * immediate=true 立即执行 * 第一次触发(callNow为true)->设定定时器(时间到了消除定时器),并立即执行 * 期间多次触发->不断重置定时器的等待时间 - * + * * immediate=false 非立即执行 * 第一次触发(callNow为false)->设定定时器(时间到了调用函数) * 期间多次触发->不断重置定时器的等待时间