feat(DOC): 完整地审查了代码,更完整的注释说明

- 无代码修改
This commit is contained in:
tobegold574
2025-11-23 22:26:39 +08:00
parent 4e741a0523
commit c5853847ae
27 changed files with 119 additions and 110 deletions

View File

@@ -3,7 +3,7 @@ import type { ApiClient } from './types';
import type { AxiosError, AxiosResponse, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
import { ApiError } from './errors'; // 直接导入ApiError
// 创建API客户端实例的工厂函数
// 创建API客户端实例的工厂函数直接用axios的requestConfig
const createApiClientInstance = (config?: Partial<AxiosRequestConfig>): ApiClient => {
// 创建axios实例
const instance = axios.create({
@@ -22,8 +22,9 @@ const createApiClientInstance = (config?: Partial<AxiosRequestConfig>): ApiClien
// 响应拦截器数组
const responseInterceptors: Array<number> = [];
// 添加请求拦截器
// 添加请求拦截器(函数声明,非函数实体,接收两个钩子参数)
const addRequestInterceptor = (
// InternalAxiosRequestConfig 是 axios 内部专门用来管理监听器的
onFulfilled?: (_config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>,
onRejected?: (_error: unknown) => unknown
): number => {
@@ -46,12 +47,14 @@ const createApiClientInstance = (config?: Partial<AxiosRequestConfig>): ApiClien
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) {
@@ -60,19 +63,20 @@ const createApiClientInstance = (config?: Partial<AxiosRequestConfig>): ApiClien
}
};
// 设置默认配置
// 设置默认配置(单例的)
// defaults是最底层请求配置
const setDefaults = (_config: Partial<AxiosRequestConfig>): void => {
Object.assign(instance.defaults, _config);
};
// 更新基础URL - 专门用于Runtime层配置
// 更新基础URL,比上面那个更细
const setBaseURL = (baseURL: string): void => {
instance.defaults.baseURL = baseURL;
};
// 创建新实例 - 用于跨域请求等特殊场景
const createInstance = (config?: Partial<AxiosRequestConfig>): ApiClient => {
// 创建新的axios实例
// 创建新的axios实例(工厂模式+原型模式)
const newInstance = axios.create({
...instance.defaults,
...config
@@ -83,6 +87,8 @@ const createApiClientInstance = (config?: Partial<AxiosRequestConfig>): ApiClien
const newResponseInterceptors: Array<number> = [];
// 复制拦截器
// 其实逻辑有很大问题包括闭包问题、handler不会对应、双重断言等等
// 应该不复制,而是重新添加
requestInterceptors.forEach(handler => {
// 由于AxiosInterceptorManager类型定义中没有handlers属性
// 我们使用类型断言来访问内部属性
@@ -107,6 +113,7 @@ const createApiClientInstance = (config?: Partial<AxiosRequestConfig>): ApiClien
}
});
// 同上
responseInterceptors.forEach(handler => {
// 由于AxiosInterceptorManager类型定义中没有handlers属性
// 我们使用类型断言来访问内部属性
@@ -164,14 +171,14 @@ const createApiClientInstance = (config?: Partial<AxiosRequestConfig>): ApiClien
newInstance.defaults.baseURL = baseURL;
},
createInstance: (newConfig?: Partial<AxiosRequestConfig>): ApiClient => {
// 手动处理headers字段的类型转换
// 手动处理headers字段的类型转换这里newInstance实际上是原型
const { headers, ...otherDefaults } = newInstance.defaults;
const configWithTypedHeaders: Partial<AxiosRequestConfig> = {
...otherDefaults,
...newConfig
};
// 只有当headers存在时才添加
// 只有当headers存在时才添加因为defaults和axiosRequestConfig在这里类型不一样
if (headers) {
// 手动转换headers字段确保类型安全
const convertedHeaders: Record<string, string | number | boolean> = {};
@@ -275,5 +282,6 @@ const createApiClient = (config?: Partial<AxiosRequestConfig>) => {
return createApiClientInstance(config);
};
// 导出API客户端和工厂函数
// 导出API客户端单例和工厂函数
// 其实单例没用因为baseurl总归不一样的但是怎么说呢单例也只需要改一个baseurl就好了
export { apiClient, createApiClient };

View File

@@ -13,7 +13,7 @@ export interface IApiError {
}
/**
* API错误类
* API错误类(诶,就是要自定义)
*/
export class ApiError extends Error implements IApiError {
public readonly code: number;
@@ -27,7 +27,7 @@ export class ApiError extends Error implements IApiError {
}
/**
* 从Axios错误创建API错误
* 从Axios错误创建API错误(就是为了简化)
*/
static fromAxiosError(error: AxiosError): ApiError {
if (!error.response) {

View File

@@ -6,7 +6,7 @@ import { chatApi } from './modules/chat';
import { modelApi } from './modules/model';
/**
* API 工厂函数,用于创建和管理 API 实例
* API 工厂函数,用于创建和管理 API 实例(模块化封装)
* 提供统一的 API 访问入口和配置管理
*/
export const createApi = (config?: Partial<AxiosRequestConfig>) => {
@@ -36,5 +36,6 @@ export const createApi = (config?: Partial<AxiosRequestConfig>) => {
export const api = createApi();
// 向后兼容的导出
// 单例还是输出了
export { apiClient } from './client';
export { postApi, userApi, chatApi, modelApi } from './modules';

View File

@@ -20,7 +20,7 @@ export const chatApi = (client: ApiClient) => ({
return client.post('/chat/sessions', data);
},
// 更新聊天会话
// 更新聊天会话(纯充数那个)
updateSession: ({ sessionId, ...data }: UpdateChatSessionRequest): Promise<{ session: ChatSession }> => {
return client.put(`/chat/sessions/${sessionId}`, data);
},
@@ -30,7 +30,7 @@ export const chatApi = (client: ApiClient) => ({
return client.post(`/chat/sessions/${data.sessionId}/messages`, data);
},
// 获取聊天会话列表
// 获取聊天会话列表(不知道写了个什么玩意儿,转什么,应该不用参数的)
getSessions: (params?: GetChatSessionsRequest): Promise<GetChatSessionsResponse> => {
const config: AxiosRequestConfig = params ? { params } : {};
return client.get('/chat/sessions', config);

View File

@@ -4,7 +4,7 @@ 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 };
// 向后兼容的默认实例

View File

@@ -4,8 +4,6 @@ import type {
LoginResponse,
RegisterRequest,
RegisterResponse,
RefreshTokenRequest,
RefreshTokenResponse,
UserProfileUpdateRequest,
UserProfileUpdateResponse,
UserFollowRequest,
@@ -28,11 +26,6 @@ export const userApi = (client: ApiClient) => ({
return client.post('/auth/register', data);
},
// 刷新令牌
refreshToken: (data: RefreshTokenRequest): Promise<RefreshTokenResponse> => {
return client.post('/auth/refresh', data);
},
// 获取用户档案
getProfile: (): Promise<UserProfileUpdateResponse> => {
return client.get('/user/profile');

View File

@@ -27,6 +27,7 @@ export interface ApiClient {
removeResponseInterceptor(handler: number): void;
}
// 后面的除了test里用core里其他地方根本没用上过都用泛型unknown了这是不对的
// API响应接口
export interface ApiResponse<T = unknown> {
success: boolean;

View File

@@ -11,6 +11,7 @@ export class AuthError extends Error {
constructor(code: string, message: string, details?: unknown) {
super(message);
// 这个得删还有test文件里的也要删
this.name = 'AuthError';
this.code = code;
this.details = details;

View File

@@ -19,6 +19,7 @@ export class AuthEventManager {
this.listeners.set(event, []);
}
const eventListeners = this.listeners.get(event);
// 神人判断
if (eventListeners) {
eventListeners.push(listener);
}

View File

@@ -64,6 +64,7 @@ export class DefaultSessionManager {
*/
clearCache(): void {
this.currentUser = null;
// 神人判断,没用接口
// 清除存储适配器中的所有项(如果支持)
if ('clear' in this.storage && typeof this.storage.clear === 'function') {
this.storage.clear();

View File

@@ -13,7 +13,7 @@ import type {
// 事件类型定义
export type AuthEventType = 'login' | 'logout' | 'register' | 'session_expired' | 'session_authenticated' | 'session_logout';
// 事件监听器类型
// 事件监听器类型(就是函数)
export type AuthEventListener = (...args: unknown[]) => void;
// 存储适配器接口 - 仅用于非敏感数据存储不涉及session管理
@@ -77,6 +77,7 @@ export interface SessionManager {
}
// 扩展 Axios 请求配置以支持认证选项
// 其实没必要,因为是服务器端处理过期
declare module 'axios' {
interface AxiosRequestConfig {
skipAuth?: boolean;

View File

@@ -3,11 +3,12 @@
"version": "1.0.0",
"type": "module",
"description": "负责准备视觉层会用到的逻辑函数、常用工具函数封装以及鉴权函数封装。负责维护统一接口。",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"type-check": "tsc --noemit",
"build": "tsc --project tsconfig.json",

View File

@@ -2,21 +2,6 @@
本目录包含了对新认证架构的全面测试套件验证SessionManager、AccessTokenManager、SessionAuthService、ExtendedApiClient和兼容性处理的功能和安全性。
## 测试结构
```
test/
├── auth/
│ ├── session-manager.test.ts # SessionManager测试
│ ├── access-token-manager.test.ts # AccessTokenManager测试
│ ├── session-auth-service.test.ts # SessionAuthService测试
│ ├── compatibility.test.ts # 兼容性处理测试
│ └── integration.test.ts # 集成测试
├── api/
│ └── extended-client.test.ts # ExtendedApiClient测试
└── setup.ts # 测试环境设置
```
## 测试覆盖范围
### SessionManager测试
@@ -26,7 +11,7 @@ test/
- Session清除
- 用户角色检查
### AccessTokenManager测试
### AccessTokenManager测试(已废弃)
- Token生成
- Token获取和缓存
- Token刷新
@@ -45,50 +30,17 @@ test/
- Token注入
- 错误处理
### 兼容性处理测试
### 兼容性处理测试(已废弃)
- 认证模式切换
- Token到Session的迁移
- 混合认证模式
### 集成测试
### 集成测试(并没有)
- 端到端认证流程
- API请求流程
- 事件处理
- 错误处理
## 运行测试
### 运行所有认证架构测试
```bash
npx ts-node scripts/run-auth-tests.ts
```
### 运行特定测试
```bash
# SessionManager测试
npx vitest run test/auth/session-manager.test.ts
# AccessTokenManager测试
npx vitest run test/auth/access-token-manager.test.ts
# SessionAuthService测试
npx vitest run test/auth/session-auth-service.test.ts
# ExtendedApiClient测试
npx vitest run test/api/extended-client.test.ts
# 兼容性测试
npx vitest run test/auth/compatibility.test.ts
# 集成测试
npx vitest run test/auth/integration.test.ts
```
### 运行覆盖率测试
```bash
npx ts-node scripts/run-auth-tests.ts --coverage
```
## 测试环境
测试使用Vitest框架配置了以下环境
@@ -102,16 +54,16 @@ npx ts-node scripts/run-auth-tests.ts --coverage
测试中使用了以下模拟对象:
- ApiClient模拟HTTP客户端
- SessionManager模拟Session管理器
- AccessTokenManager模拟Access Token管理器
- AccessTokenManager模拟Access Token管理器(已废弃)
- AuthEventManager模拟认证事件管理器
- TokenManager模拟Token管理器用于兼容性测试
- TokenManager模拟Token管理器已废弃
## 测试数据
测试使用以下模拟数据:
- 用户信息
- Session信息
- Access Token信息
- Access Token信息(已废弃)
- API响应数据
## 断言
@@ -123,10 +75,3 @@ npx ts-node scripts/run-auth-tests.ts --coverage
- 数据转换
- 安全性检查
## 注意事项
1. 确保在运行测试前已安装所有依赖
2. 测试使用模拟数据,不会影响实际系统
3. 测试覆盖了主要功能和边界情况
4. 测试验证了安全性和错误处理
5. 测试确保了兼容性和平滑迁移

View File

@@ -5,7 +5,7 @@
import { vi } from 'vitest';
// 模拟浏览器API
// 模拟浏览器API(其实用不到)
Object.defineProperty(window, 'localStorage', {
value: {
getItem: vi.fn(),
@@ -26,24 +26,24 @@ Object.defineProperty(window, 'sessionStorage', {
writable: true
});
// 模拟IntersectionObserver
// 模拟IntersectionObserver(其实用不到)
globalThis.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn()
}));
// 模拟ResizeObserver
// 模拟ResizeObserver(其实用不到)
globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn()
}));
// 模拟requestAnimationFrame
// 模拟requestAnimationFrame(其实用不到)
globalThis.requestAnimationFrame = vi.fn((cb: FrameRequestCallback) => setTimeout(cb, 0));
// 模拟cancelAnimationFrame
// 模拟cancelAnimationFrame(其实用不到)
globalThis.cancelAnimationFrame = vi.fn((id: number) => clearTimeout(id));
// 模拟fetch

View File

@@ -13,7 +13,8 @@
"lib": ["es2022", "dom", "dom.iterable"],
// Other Outputs
"sourceMap": true,
// 关了,用的时候老跳找不到源文件
"sourceMap": false,
"declaration": true,
"declarationMap": true,

View File

@@ -3,11 +3,13 @@ import type { ChatMessageType } from './enum';
// 创建聊天会话请求接口
export interface CreateChatSessionRequest {
// 对方的ID因为当前用户的id可以在session里获取cookie自动带上)
participantId: string;
}
// 更新聊天会话请求接口
// 更新聊天会话请求接口(占位)
export interface UpdateChatSessionRequest {
// 只能用于处理后台逻辑
sessionId: string;
}
@@ -57,7 +59,7 @@ export interface MarkMessagesAsReadRequest {
messageIds: string[];
}
// 标记消息已读响应接口
// 标记消息已读响应接口(已读状态只面向接收方)
export interface MarkMessagesAsReadResponse {
success: boolean;
markedMessageIds: string[]; // 成功标记的消息ID

View File

@@ -4,6 +4,7 @@ import type { ChatMessageType, ChatMessageStatus } from './enum';
// 聊天消息接口
export interface ChatMessage {
id: string;
// 聊天会话id下面那个接口的id
sessionId: string;
sender: User;
receiver: User;
@@ -16,6 +17,7 @@ export interface ChatMessage {
fileName?: string;
fileSize?: number;
duration?: number;
// 小图预览
thumbnail?: string;
[key: string]: unknown;
};

View File

@@ -1,4 +1,4 @@
// 聊天消息类型枚举
// 聊天消息类型枚举(暂时用不到的)
export enum ChatMessageType {
TEXT = 'text',
IMAGE = 'image',
@@ -8,7 +8,7 @@ export enum ChatMessageType {
SYSTEM = 'system'
}
// 聊天消息状态枚举
// 聊天消息状态枚举(暂时用不到的)
export enum ChatMessageStatus {
SENDING = 'sending',
SENT = 'sent',

View File

@@ -15,6 +15,7 @@ export interface ModelComment extends BaseEntity {
author: BaseUser; // 作者信息
content: string; // 评论内容
parentId?: string; // 父评论ID用于嵌套评论
// 其实没必要
stats: ModelCommentStats; // 统计信息
}

View File

@@ -1,4 +1,5 @@
// AI模型评论排序枚举
// 和post的重复了而且字段还不一样
export enum CommentSortType {
LATEST = 'latest', // 最新
HOTTEST = 'hottest', // 最热

View File

@@ -10,10 +10,13 @@ export interface BaseEntity {
// 用户基础信息接口
export interface BaseUser {
// 其实应该用profile的
user: User;
}
export interface PostComment extends BaseEntity {
// 这里没带postId如果后台管理需要就要带
// 重复了,而且评论没有数据(点赞、收藏等等)
authorId: string; // 作者ID
author: BaseUser; // 作者信息
content: string; // 评论内容

View File

@@ -1,3 +1,4 @@
// 这里应该用type联合的现在有冗余和风格不统一的问题
// 排序方式枚举
export enum SortOrder {
ASC = 'asc',

View File

@@ -8,6 +8,7 @@ export interface GetHotAuthorsRequest {
// 获取热门作者响应接口
export interface GetHotAuthorsResponse {
// 其实应该用profile返回的
data: User[]; // 数据列表
total: number; // 总数
}
@@ -23,6 +24,7 @@ export interface GetAuthorRankingRequest {
export interface GetAuthorRankingResponse {
data: User[]; // 数据列表
total: number; // 总数
// 其实冗余了
period: 'day' | 'week' | 'month'; // 统计周期
type: 'posts' | 'views' | 'likes'; // 排序类型
}

View File

@@ -33,16 +33,17 @@ export interface ChangePasswordRequest { // 暂时用不到
newPassword: string;
}
export interface RefreshTokenRequest {
sessionId: string;
}
// 目前只用得到session
// export interface RefreshTokenRequest {
// sessionId: string;
// }
export interface RefreshTokenResponse {
sessionId: string;
}
// export interface RefreshTokenResponse {
// sessionId: string;
// }
// 用户通知
export interface UserNotification { // 暂时用不到
// 用户通知(暂时用不到)
export interface UserNotification {
id: string;
userId: string;
type: NotificationType;

View File

@@ -20,11 +20,13 @@ export interface UserProfileUpdateResponse {
// 用户关系
export interface UserRelation {
// 当前用户的id
id: string;
followerId: string[];
followingId: string[];
}
// 当前用户的id由cookie带上
export interface UserFollowRequest {
userId: string;
}

View File

@@ -6,6 +6,7 @@ export interface UserSearchRequest {
}
export interface UserSearchResponse {
// 这里逻辑是对的(可惜暂时不打算做)
users: Array<UserProfile>
total: number;
}

View File

@@ -3,6 +3,23 @@
* @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会不断减少
*/
export function throttle<T extends (...args: unknown[]) => unknown>(
func: T,
@@ -53,6 +70,7 @@ export const deepClone = <T>(obj: T): T => {
return new Date(obj.getTime()) as unknown as T;
}
// 上面是边界情况判断,下面才是真的深拷贝逻辑
// 处理数组
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item)) as unknown as T;
@@ -61,7 +79,9 @@ export const deepClone = <T>(obj: T): T => {
// 处理普通对象
const clonedObj = {} as T;
for (const key in obj) {
// 最安全的一集
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// 递归
clonedObj[key] = deepClone(obj[key]);
}
}
@@ -88,6 +108,7 @@ export function deepEqual(a: unknown, b: unknown): boolean {
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
// 递归,上面比值,下面先确认键一不一样
if (!keysB.includes(key) || !deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
return false;
}
@@ -101,6 +122,8 @@ export function deepEqual(a: unknown, b: unknown): boolean {
* @param obj 源对象
* @param keys 要选取的属性键数组
* @returns 包含指定属性的新对象
* 对象或数组类型属性:拷贝引用
* 基本类型属性:直接赋值
*/
export function pick<T extends Record<string, unknown>, K extends keyof T>(
obj: T,
@@ -142,6 +165,7 @@ export function merge<T extends Record<string, unknown>>(...objects: Partial<T>[
for (const obj of objects) {
if (obj && typeof obj === 'object') {
// 浅拷贝
Object.assign(result, obj);
}
}
@@ -150,7 +174,7 @@ export function merge<T extends Record<string, unknown>>(...objects: Partial<T>[
}
/**
* 将对象转换为查询字符串
* 将对象转换为查询字符串(但是没用到)
* @param obj 要转换的对象
* @returns 查询字符串
*/
@@ -196,6 +220,7 @@ export const unique = <T, K = unknown>(
keyFn?: (item: T) => K
): T[] => {
if (!keyFn) {
// 只适用于简单数组(也是蛮神奇的语法)
return [...new Set(array)];
}
@@ -203,9 +228,11 @@ export const unique = <T, K = unknown>(
return array.filter(item => {
const key = keyFn(item);
if (seen.has(key)) {
// 已存在返回false过滤该元素
return false;
}
seen.add(key);
// 返回true保留该元素
return true;
});
};
@@ -213,7 +240,7 @@ export const unique = <T, K = unknown>(
/**
* 数组分组
* @param array 要分组的数组
* @param keyFn 分组键函数
* @param keyFn 分组键函数(箭头函数实现)
* @returns 分组后的对象
*/
export const groupBy = <T, K extends string | number | symbol>(
@@ -233,7 +260,7 @@ export const groupBy = <T, K extends string | number | symbol>(
/**
* 数组排序
* @param array 要排序的数组
* @param compareFn 比较函数
* @param compareFn 比较函数(箭头函数实现)
* @returns 排序后的新数组
*/
export const sortBy = <T>(
@@ -248,6 +275,18 @@ export const sortBy = <T>(
* @param func 要防抖的函数
* @param wait 等待时间(毫秒)
* @param immediate 是否立即执行
*
* 两种类型:
* 1. 立即执行:第一次触发立即执行,之后触发只重置等待时间
* 2. 非立即执行:触发后等待时间结束才执行,期间触发会重置等待时间
*
* immediate=true 立即执行
* 第一次触发callNow为true->设定定时器(时间到了消除定时器),并立即执行
* 期间多次触发->不断重置定时器的等待时间
*
* immediate=false 非立即执行
* 第一次触发callNow为false->设定定时器(时间到了调用函数)
* 期间多次触发->不断重置定时器的等待时间
*/
export function debounce<T extends (...args: unknown[]) => unknown>(
func: T,