feat: 请求层泛型化并收紧默认超时

This commit is contained in:
2026-08-27 21:48:28 +08:00
parent 468bba8670
commit f1fcae0dac
8 changed files with 130 additions and 68 deletions
+6 -2
View File
@@ -22,10 +22,14 @@ describe('reportFragments API', () => {
it('createFragments sends POST with sessionNum and content', async () => { it('createFragments sends POST with sessionNum and content', async () => {
mockedRequest.post.mockResolvedValue({ code: 200, data: { id: 1 } }) mockedRequest.post.mockResolvedValue({ code: 200, data: { id: 1 } })
const result = await createFragments('S001', '学习了递归') const result = await createFragments('S001', '学习了递归')
expect(mockedRequest.post).toHaveBeenCalledWith('/report-fragments', { expect(mockedRequest.post).toHaveBeenCalledWith(
'/report-fragments',
{
sessionNum: 'S001', sessionNum: 'S001',
content: '学习了递归', content: '学习了递归',
}) },
{ timeout: 300000 },
)
expect(result).toEqual({ code: 200, data: { id: 1 } }) expect(result).toEqual({ code: 200, data: { id: 1 } })
}) })
}) })
+4 -2
View File
@@ -58,7 +58,8 @@ describe('studySessions API', () => {
await endSession('S001') await endSession('S001')
expect(mockedRequest.post).toHaveBeenCalledWith( expect(mockedRequest.post).toHaveBeenCalledWith(
'/study-sessions/S001/study-sessions/ended', '/study-sessions/S001/study-sessions/ended',
{ content: '任务结束' } { content: '任务结束' },
{ timeout: 300000 }
) )
}) })
@@ -67,7 +68,8 @@ describe('studySessions API', () => {
await endSession('S001', '自定义结束') await endSession('S001', '自定义结束')
expect(mockedRequest.post).toHaveBeenCalledWith( expect(mockedRequest.post).toHaveBeenCalledWith(
'/study-sessions/S001/study-sessions/ended', '/study-sessions/S001/study-sessions/ended',
{ content: '自定义结束' } { content: '自定义结束' },
{ timeout: 300000 }
) )
}) })
it('abortSession sends POST with confirmation phrase', async () => { it('abortSession sends POST with confirmation phrase', async () => {
+4 -1
View File
@@ -1,8 +1,11 @@
// src/api/reportFragments.ts // src/api/reportFragments.ts
import request from "@/utils/request"; import request from "@/utils/request";
// 残片生成走 AI 聚合,耗时较长,放宽到 5 分钟(默认全局 30s)
const AI_TIMEOUT = { timeout: 300_000 };
export const createFragments = (sessionNum: string, content: string) => { export const createFragments = (sessionNum: string, content: string) => {
return request.post(`/report-fragments`, {sessionNum, content}) return request.post(`/report-fragments`, { sessionNum, content }, AI_TIMEOUT)
} }
export const updateFragments = (id: number, content: string) => { export const updateFragments = (id: number, content: string) => {
+3 -3
View File
@@ -24,15 +24,15 @@ export interface ReviewTaskStats {
} }
export const getReviewFeed = (limit: number = 30, mode: "recent" | "random" | "smart" = "recent") => { export const getReviewFeed = (limit: number = 30, mode: "recent" | "random" | "smart" = "recent") => {
return request.get("/review/feed", { limit, mode }); return request.get<ReviewFeedItem[]>("/review/feed", { limit, mode });
}; };
export const getReviewTaskStats = () => { export const getReviewTaskStats = () => {
return request.get("/review/tasks"); return request.get<ReviewTaskStats[]>("/review/tasks");
}; };
export const getReviewTaskStatsByTask = (taskNum: string) => { export const getReviewTaskStatsByTask = (taskNum: string) => {
return request.get(`/review/tasks/${taskNum}`); return request.get<ReviewTaskStats>(`/review/tasks/${taskNum}`);
}; };
export const getReportDetail = (id: number) => { export const getReportDetail = (id: number) => {
+15 -7
View File
@@ -1,4 +1,5 @@
import request from "@/utils/request"; import request from "@/utils/request";
import type { ApiResponse } from "@/utils/request";
// ============ 标准思维导图 ============ // ============ 标准思维导图 ============
@@ -31,14 +32,17 @@ export interface RecallRecord {
createdTime: string; createdTime: string;
} }
/** 获取/自动生成标准思维导图 */ // AI 聚合类接口耗时较长,统一放宽到 5 分钟(默认全局 30s)
const AI_TIMEOUT = { timeout: 300_000 };
/** 获取/自动生成标准思维导图(首次访问会触发 AI 生成) */
export const getStandardMindMap = (taskNum: string) => { export const getStandardMindMap = (taskNum: string) => {
return request.get(`/review/standard-mind-map/${taskNum}`); return request.get<StandardMindMap>(`/review/standard-mind-map/${taskNum}`, {}, AI_TIMEOUT);
}; };
/** 强制重新生成标准思维导图(全量或增量) */ /** 强制重新生成标准思维导图(全量或增量) */
export const regenerateStandardMindMap = (taskNum: string, mode: "full" | "incremental" = "full") => { export const regenerateStandardMindMap = (taskNum: string, mode: "full" | "incremental" = "full") => {
return request.post(`/review/standard-mind-map/${taskNum}/regenerate?mode=${mode}`); return request.post<StandardMindMap>(`/review/standard-mind-map/${taskNum}/regenerate?mode=${mode}`, null, AI_TIMEOUT);
}; };
/** 用户编辑标准思维导图(大纲文本) */ /** 用户编辑标准思维导图(大纲文本) */
@@ -47,18 +51,22 @@ export const updateStandardMindMap = (taskNum: string, outline: string) => {
}; };
/** 用户提交回忆大纲,与标准导图对比(可选指定起始节点路径) */ /** 用户提交回忆大纲,与标准导图对比(可选指定起始节点路径) */
export const recallCompare = (taskNum: string, recallOutline: string, focusPath?: string) => { export const recallCompare = (
taskNum: string,
recallOutline: string,
focusPath?: string,
): Promise<ApiResponse<StandardMindMap>> => {
const body: Record<string, any> = { recallOutline }; const body: Record<string, any> = { recallOutline };
if (focusPath) body.focusPath = focusPath; if (focusPath) body.focusPath = focusPath;
return request.post(`/review/standard-mind-map/${taskNum}/recall`, body); return request.post(`/review/standard-mind-map/${taskNum}/recall`, body, AI_TIMEOUT);
}; };
/** 在标准导图中查找与内容最匹配的节点 */ /** 在标准导图中查找与内容最匹配的节点 */
export const findNode = (taskNum: string, content: string) => { export const findNode = (taskNum: string, content: string) => {
return request.post(`/review/standard-mind-map/${taskNum}/find-node`, { content }); return request.post<{ path: string }>(`/review/standard-mind-map/${taskNum}/find-node`, { content });
}; };
/** 获取该任务的所有回忆对比记录 */ /** 获取该任务的所有回忆对比记录 */
export const listRecallRecords = (taskNum: string) => { export const listRecallRecords = (taskNum: string) => {
return request.get(`/review/standard-mind-map/${taskNum}/recall-records`); return request.get<RecallRecord[]>(`/review/standard-mind-map/${taskNum}/recall-records`);
}; };
+47 -12
View File
@@ -1,6 +1,40 @@
// src/api/studySession.ts // src/api/studySessions.ts
import request from "@/utils/request"; import request from "@/utils/request";
/** 学习会话详情(start-or-continue / detail 接口返回的会话核心字段) */
export interface SessionDetail {
sessionNum: string;
sessionState: "ONGOING" | "PAUSED" | "ENDED";
taskNum: string;
taskId: number;
taskName?: string;
materialUrl?: string;
startTime?: string;
endTime?: string;
lastStartTime?: string;
actualTime?: number;
effectiveTime?: number;
effectivenessRatio?: number | string;
pointerPosition?: number;
systemMessage?: string;
}
/** 当前活跃会话摘要(跨页面恢复用) */
export interface ActiveSessionInfo {
taskNum: string;
sessionNum: string;
taskName?: string;
}
/** 后端 MyBatis-Plus 分页包装 */
export interface PagedResult<T> {
records: T[];
total: number;
}
// AI 聚合类接口耗时较长,统一放宽到 5 分钟(默认全局 30s)
const AI_TIMEOUT = { timeout: 300_000 };
export const continueSession = (sessionNum: string) => { export const continueSession = (sessionNum: string) => {
return request.post(`/study-sessions/${sessionNum}/study-sessions/continue`); return request.post(`/study-sessions/${sessionNum}/study-sessions/continue`);
}; };
@@ -10,8 +44,9 @@ export const pauseSession = (sessionNum: string, endTime?: Date) => {
return request.post(`/study-sessions/${sessionNum}/study-sessions/pause`, null, { params }); return request.post(`/study-sessions/${sessionNum}/study-sessions/pause`, null, { params });
}; };
/** 结束会话会触发后端报告生成,耗时较长 */
export const endSession = (sessionNum: string, content: string = "任务结束") => { export const endSession = (sessionNum: string, content: string = "任务结束") => {
return request.post(`/study-sessions/${sessionNum}/study-sessions/ended`, { content }); return request.post(`/study-sessions/${sessionNum}/study-sessions/ended`, { content }, AI_TIMEOUT);
}; };
/** 误操作结束学习会话:需输入确认语,严格零数据关闭(删除会话与预期) */ /** 误操作结束学习会话:需输入确认语,严格零数据关闭(删除会话与预期) */
@@ -20,16 +55,16 @@ export const abortSession = (sessionNum: string, confirmation: string) => {
}; };
export const startOrContinueStudySession = (taskNum: string) => { export const startOrContinueStudySession = (taskNum: string) => {
return request.get(`/tasks/${taskNum}/study-sessions/start-or-continue`); return request.get<SessionDetail>(`/tasks/${taskNum}/study-sessions/start-or-continue`);
}; };
export const getSessionDetail = (sessionNum: string) => { export const getSessionDetail = (sessionNum: string) => {
return request.get(`/study-sessions/${sessionNum}`); return request.get<SessionDetail>(`/study-sessions/${sessionNum}`);
}; };
/** 获取会话的学习预期 */ /** 获取会话的学习预期 */
export const getExpectation = (sessionNum: string) => { export const getExpectation = (sessionNum: string) => {
return request.get(`/study-sessions/${sessionNum}/expectation`); return request.get<{ description: string } | null>(`/study-sessions/${sessionNum}/expectation`);
}; };
/** 创建/更新会话的学习预期 */ /** 创建/更新会话的学习预期 */
@@ -37,28 +72,28 @@ export const upsertExpectation = (sessionNum: string, description: string) => {
return request.put(`/study-sessions/${sessionNum}/expectation`, { description }); return request.put(`/study-sessions/${sessionNum}/expectation`, { description });
}; };
/** 获取报告草稿(AI 聚合残片,不可用时为拼接) */ /** 获取报告草稿(AI 聚合残片,不可用时为拼接),生成失败不阻塞手写总结 */
export const getReportDraft = (sessionNum: string) => { export const getReportDraft = (sessionNum: string) => {
return request.get(`/study-sessions/${sessionNum}/report-draft`); return request.get<string>(`/study-sessions/${sessionNum}/report-draft`, {}, AI_TIMEOUT);
}; };
/** 查询当前是否有活跃会话 */ /** 查询当前是否有活跃会话 */
export const getActiveSession = (excludeTaskNum?: string) => { export const getActiveSession = (excludeTaskNum?: string) => {
const params: Record<string, any> = {}; const params: Record<string, any> = {};
if (excludeTaskNum) params.excludeTaskNum = excludeTaskNum; if (excludeTaskNum) params.excludeTaskNum = excludeTaskNum;
return request.get('/study-sessions/active', params); return request.get<ActiveSessionInfo | null>('/study-sessions/active', params);
}; };
/** 分页查询任务的历史残片 */ /** 分页查询任务的历史残片 */
export const getTaskFragments = (taskNum: string, page: number, size: number, keyword?: string) => { export const getTaskFragments = <T = any>(taskNum: string, page: number, size: number, keyword?: string) => {
const params: Record<string, any> = { page, size }; const params: Record<string, any> = { page, size };
if (keyword) params.keyword = keyword; if (keyword) params.keyword = keyword;
return request.get(`/study-sessions/tasks/${taskNum}/fragments`, params); return request.get<PagedResult<T>>(`/study-sessions/tasks/${taskNum}/fragments`, params);
}; };
/** 分页查询任务的历史报告 */ /** 分页查询任务的历史报告 */
export const getTaskReports = (taskNum: string, page: number, size: number, keyword?: string) => { export const getTaskReports = <T = any>(taskNum: string, page: number, size: number, keyword?: string) => {
const params: Record<string, any> = { page, size }; const params: Record<string, any> = { page, size };
if (keyword) params.keyword = keyword; if (keyword) params.keyword = keyword;
return request.get(`/study-sessions/tasks/${taskNum}/reports`, params); return request.get<PagedResult<T>>(`/study-sessions/tasks/${taskNum}/reports`, params);
}; };
+5 -5
View File
@@ -12,7 +12,7 @@ export interface TaskApplication {
} }
export const getTaskApplications = (taskNum: string) => { export const getTaskApplications = (taskNum: string) => {
return request.get(`/tasks/${taskNum}/applications`); return request.get<TaskApplication[]>(`/tasks/${taskNum}/applications`);
}; };
export const createTaskApplication = ( export const createTaskApplication = (
@@ -24,7 +24,7 @@ export const createTaskApplication = (
status?: TaskApplication["status"]; status?: TaskApplication["status"];
}, },
) => { ) => {
return request.post(`/tasks/${taskNum}/applications`, payload); return request.post<TaskApplication>(`/tasks/${taskNum}/applications`, payload);
}; };
export const updateTaskApplication = ( export const updateTaskApplication = (
@@ -36,7 +36,7 @@ export const updateTaskApplication = (
status?: TaskApplication["status"]; status?: TaskApplication["status"];
}, },
) => { ) => {
return request.put(`/tasks/applications/${id}`, payload); return request.put<TaskApplication>(`/tasks/applications/${id}`, payload);
}; };
export const deleteTaskApplication = (id: number) => { export const deleteTaskApplication = (id: number) => {
@@ -54,10 +54,10 @@ export interface PriorityWeights {
} }
export const getPriorityWeights = () => { export const getPriorityWeights = () => {
return request.get("/tasks/priority-weights"); return request.get<PriorityWeights>("/tasks/priority-weights");
}; };
/** 保存权重配置并触发全部任务优先级重算 */ /** 保存权重配置并触发全部任务优先级重算 */
export const savePriorityWeights = (weights: PriorityWeights) => { export const savePriorityWeights = (weights: PriorityWeights) => {
return request.put("/tasks/priority-weights", weights); return request.put<PriorityWeights>("/tasks/priority-weights", weights);
}; };
+40 -30
View File
@@ -2,11 +2,21 @@ import axios from 'axios';
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import router from "@/router"; import router from "@/router";
const basic_url = import.meta.env.VITE_BASE_URL; /** 后端统一响应包装:code === 200 表示业务成功 */
export interface ApiResponse<T = any> {
code: number;
message?: string;
data: T;
}
const baseURL = import.meta.env.VITE_BASE_URL;
// 默认 30s:普通 CRUD 足够;AI 聚合等长耗时接口需在 api 层显式覆写 timeout
const DEFAULT_TIMEOUT = 30_000;
const axiosInstance = axios.create({ const axiosInstance = axios.create({
baseURL: basic_url, baseURL,
timeout: 600000, timeout: DEFAULT_TIMEOUT,
withCredentials: true withCredentials: true
}); });
@@ -51,43 +61,43 @@ const validateResponse = (res: any) => {
return result; return result;
}; };
function get(url: string, params: Record<string, any>): Promise<any>; const get = <T = any>(
function get(url: string): Promise<any>; url: string,
params: Record<string, any> = {},
function get(url: string, params: Record<string, any> = {}) { config: Record<string, any> = {},
return axiosInstance.get(url, { params }) ): Promise<ApiResponse<T>> =>
axiosInstance.get(url, { params, ...config })
.then(validateResponse) .then(validateResponse)
.catch(handleError); .catch(handleError);
}
const post = (url: string, data: any = null, config: any = {}) => { const post = <T = any>(
url: string,
if (config.params) { data: any = null,
// 如果传入 config.params,则作为 URL 参数 config: Record<string, any> = {},
return axiosInstance.post(url, data, { params: config.params, ...config }) ): Promise<ApiResponse<T>> =>
axiosInstance.post(url, data, config)
.then(validateResponse) .then(validateResponse)
.catch(handleError); .catch(handleError);
} else {
// 默认 POST JSON const put = <T = any>(
return axiosInstance.post(url, data, config) url: string,
data: any,
config: Record<string, any> = {},
): Promise<ApiResponse<T>> =>
axiosInstance.put(url, data, config)
.then(validateResponse) .then(validateResponse)
.catch(handleError); .catch(handleError);
}
};
const put = (url: string, data: any, config: any = {}) => { const del = <T = any>(
return axiosInstance.put(url, data, config) url: string,
params: Record<string, any> = {},
config: Record<string, any> = {},
): Promise<ApiResponse<T>> =>
axiosInstance.delete(url, { params, ...config })
.then(validateResponse) .then(validateResponse)
.catch(handleError); .catch(handleError);
};
const del = (url: string, params: Record<string, any> = {}, config: any = {}) => { const requestNotImplemented = () => {
return axiosInstance.delete(url, { params, ...config })
.then(validateResponse)
.catch(handleError);
};
const requestNotImplemented = (method: string) => {
ElMessage.error("该功能暂不可用,请刷新后重试"); ElMessage.error("该功能暂不可用,请刷新后重试");
throw new Error("该功能暂不可用,请刷新后重试"); throw new Error("该功能暂不可用,请刷新后重试");
}; };
@@ -98,7 +108,7 @@ const request = (method: string, url: string, paramsOrData: any) => {
case 'post': return post(url, paramsOrData); case 'post': return post(url, paramsOrData);
case 'put': return put(url, paramsOrData); case 'put': return put(url, paramsOrData);
case 'delete': return del(url, paramsOrData); case 'delete': return del(url, paramsOrData);
default: return requestNotImplemented(method); default: return requestNotImplemented();
} }
}; };