feat: 完成全部接口替换为新后端接口

This commit is contained in:
R524809
2026-10-19 13:19:07 +08:00
parent 8de06ce66b
commit 997042789d
40 changed files with 1946 additions and 785 deletions
+93 -65
View File
@@ -1,74 +1,102 @@
import { getApiConfig } from '../config/api';
import {
ApiRequestError,
requestApi,
type RequestOptions,
} from './api-transport';
import {
cachedServerUser,
clearSession,
ensureSession,
getAuthContext,
recoverSession,
rejectSession,
} from './auth-session';
export { ApiRequestError } from './api-transport';
interface ApiEnvelope<T> {
code: number;
message: string;
data: T;
requestId?: string;
}
const recoverable = new Set([
'AUTH_REQUIRED',
'AUTH_INVALID',
'AUTH_EXPIRED',
'AUTH_REVOKED',
]);
const blocked = new Set([
'ACCOUNT_NOT_FOUND',
'ACCOUNT_DISABLED',
'WECHAT_LOGIN_BLOCKED',
]);
/** 保留有限的状态用于页面反馈,不把服务器原始响应或凭证输出到界面。 */
export class ApiRequestError extends Error {
constructor(
message: string,
readonly statusCode: number = 0,
) {
super(message);
this.name = 'ApiRequestError';
}
}
/** 新后端统一请求层:校验 HTTP 与业务状态,旧云函数/CDN 请求保持独立。 */
/** 公开接口显式 auth=none;其余接口合并登录,最多恢复并重发一次,不跨账户补发。 */
export async function apiRequest<T>(
path: string,
options: {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
data?: Record<string, unknown>;
header?: Record<string, string>;
} = {},
options: RequestOptions & { auth?: 'required' | 'none' } = {},
): Promise<T> {
const config = getApiConfig();
if (!path.startsWith('/') || path.startsWith('//'))
throw new Error('接口路径不正确');
return new Promise((resolve, reject) => {
wx.request<ApiEnvelope<T>>({
url: `${config.baseUrl}${path}`,
method: options.method || 'GET',
data: options.data,
header: { 'Content-Type': 'application/json', ...options.header },
timeout: config.timeout,
success(res) {
if (res.statusCode < 200 || res.statusCode >= 300) {
reject(
new ApiRequestError(
res.statusCode === 404
? '内容暂不可用'
: '接口请求失败,请稍后重试',
res.statusCode,
),
);
return;
}
if (
!res.data ||
res.data.code !== 0 ||
!Object.prototype.hasOwnProperty.call(res.data, 'data')
) {
reject(
new ApiRequestError(
'接口返回异常,请稍后重试',
res.statusCode,
),
);
return;
}
resolve(res.data.data);
// Authorization 由会话层统一提供;公开请求不附带调用方误传的凭证。
const header = Object.fromEntries(
Object.entries(options.header || {}).filter(
([key]) => key.toLowerCase() !== 'authorization',
),
);
if (options.auth === 'none')
return requestApi(path, { ...options, header }, getApiConfig());
const context = getAuthContext();
const previousUserId = cachedServerUser()?.id;
const initial = await ensureSession(context);
if (previousUserId && initial.user.id !== previousUserId) {
clearSession(false, context);
throw new ApiRequestError(
'账户已变化,请重新操作',
401,
'ACCOUNT_CHANGED',
);
}
const send = async (accessToken: string) => {
if (getAuthContext().key !== context.key)
throw new ApiRequestError('接口环境已变化,请重新操作');
const result = await requestApi<T>(
path,
{
...options,
header: { ...header, Authorization: `Bearer ${accessToken}` },
},
fail() {
reject(
new ApiRequestError('连接失败,请确认后端已启动及网络配置'),
context,
);
if (
getAuthContext().key !== context.key ||
cachedServerUser()?.id !== initial.user.id
)
throw new ApiRequestError('账户状态已变化,请重新操作');
return result;
};
try {
return await send(initial.accessToken);
} catch (error) {
if (!(error instanceof ApiRequestError) || error.statusCode !== 401)
throw error;
if (!recoverable.has(error.reason)) {
if (blocked.has(error.reason))
rejectSession(context, initial.accessToken, true);
throw error;
}
const renewed = await recoverSession(context, initial);
if (renewed.user.id !== initial.user.id) {
clearSession(false, context);
throw new ApiRequestError(
'账户已变化,请重新操作',
401,
'ACCOUNT_CHANGED',
);
}
try {
return await send(renewed.accessToken);
} catch (second) {
if (second instanceof ApiRequestError && second.statusCode === 401)
rejectSession(
context,
renewed.accessToken,
blocked.has(second.reason),
);
},
});
});
throw second;
}
}
}