chore: establish V1.1 recovery baseline
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* API 统一请求层(规范 §131)
|
||||
*
|
||||
* 职责:纯 HTTP 客户端,统一处理 Token / 超时 / 错误码 / Session 失效。
|
||||
* 不含任何业务域方法(域方法在 services/ 层,规范 §130 页面不直接调用底层 request)。
|
||||
*/
|
||||
const { BUSINESS_LIMITS, REQUEST_ERROR } = require('../constants');
|
||||
const { API_BASE_URL } = require('../env');
|
||||
|
||||
class ApiClient {
|
||||
constructor() {
|
||||
this.baseURL = API_BASE_URL;
|
||||
this.token = null;
|
||||
this._refreshToken = null;
|
||||
/** @type {((reason: any) => void)|null} Session 失效回调,由 AuthStore 注入 */
|
||||
this.onSessionExpired = null;
|
||||
/** @type {((config: object) => void)|null} 维护态拦截回调,由 AppStore 注入 */
|
||||
this.onMaintenance = null;
|
||||
}
|
||||
|
||||
/** 读取本地存储的 access token */
|
||||
getToken() {
|
||||
if (this.token) return this.token;
|
||||
try {
|
||||
this.token = wx.getStorageSync('accessToken') || null;
|
||||
this._refreshToken = wx.getStorageSync('refreshToken') || null;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return this.token;
|
||||
}
|
||||
|
||||
/** 读取本地存储的 refresh token(供 auth service 刷新使用) */
|
||||
getRefreshToken() {
|
||||
if (this._refreshToken) return this._refreshToken;
|
||||
try {
|
||||
this._refreshToken = wx.getStorageSync('refreshToken') || null;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return this._refreshToken;
|
||||
}
|
||||
|
||||
/** 持久化 token(登录/刷新成功后调用) */
|
||||
saveToken(token, refreshToken) {
|
||||
this.token = token;
|
||||
this._refreshToken = refreshToken;
|
||||
wx.setStorageSync('accessToken', token);
|
||||
wx.setStorageSync('refreshToken', refreshToken);
|
||||
}
|
||||
|
||||
/** 清除 token(登出/Session 失效后调用) */
|
||||
clearToken() {
|
||||
this.token = null;
|
||||
this._refreshToken = null;
|
||||
wx.removeStorageSync('accessToken');
|
||||
wx.removeStorageSync('refreshToken');
|
||||
}
|
||||
|
||||
/** 生成请求 ID(用于链路追踪) */
|
||||
generateRequestId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
* @param {{url: string, method?: string, data?: any, headers?: object, idempotencyKey?: string, timeout?: number}} options
|
||||
*/
|
||||
request(options) {
|
||||
const { url, method = 'GET', data, headers = {}, idempotencyKey, timeout } = options;
|
||||
const token = this.getToken();
|
||||
const requestId = this.generateRequestId();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: this.baseURL + url,
|
||||
method,
|
||||
data,
|
||||
timeout: timeout || BUSINESS_LIMITS.REQUEST_TIMEOUT_MS,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...headers,
|
||||
},
|
||||
success: (res) => {
|
||||
// 维护态拦截(规范 §77)
|
||||
if (res.statusCode === 503) {
|
||||
if (typeof this.onMaintenance === 'function') this.onMaintenance(res.data || {});
|
||||
reject({
|
||||
code: REQUEST_ERROR.MAINTENANCE,
|
||||
message: '平台正在维护,请稍后再试',
|
||||
requestId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (res.statusCode === 200) {
|
||||
const body = res.data || {};
|
||||
if (body.success) {
|
||||
resolve(body.data);
|
||||
} else {
|
||||
reject({
|
||||
code: body.code || REQUEST_ERROR.BUSINESS_ERROR,
|
||||
message: body.message,
|
||||
requestId,
|
||||
});
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
this.clearToken();
|
||||
if (typeof this.onSessionExpired === 'function') this.onSessionExpired();
|
||||
reject({ code: REQUEST_ERROR.UNAUTHORIZED, message: '请重新登录', requestId });
|
||||
} else if (res.statusCode === 403) {
|
||||
reject({ code: REQUEST_ERROR.FORBIDDEN, message: '无权限访问', requestId });
|
||||
} else if (res.statusCode === 429) {
|
||||
reject({ code: REQUEST_ERROR.RATE_LIMITED, message: '请求过于频繁,请稍后重试', requestId });
|
||||
} else {
|
||||
reject({ code: REQUEST_ERROR.SERVER_ERROR, message: '服务暂时不可用', requestId });
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject({
|
||||
code: REQUEST_ERROR.NETWORK_ERROR,
|
||||
message: '网络连接失败,请检查网络后重试',
|
||||
requestId,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get(url, data, headers) {
|
||||
return this.request({ url, method: 'GET', data, headers });
|
||||
}
|
||||
|
||||
post(url, data, headers, idempotencyKey) {
|
||||
return this.request({ url, method: 'POST', data, headers, idempotencyKey });
|
||||
}
|
||||
|
||||
put(url, data, headers, idempotencyKey) {
|
||||
return this.request({ url, method: 'PUT', data, headers, idempotencyKey });
|
||||
}
|
||||
|
||||
patch(url, data, headers, idempotencyKey) {
|
||||
return this.request({ url, method: 'PATCH', data, headers, idempotencyKey });
|
||||
}
|
||||
|
||||
delete(url, data, headers, idempotencyKey) {
|
||||
return this.request({ url, method: 'DELETE', data, headers, idempotencyKey });
|
||||
}
|
||||
}
|
||||
|
||||
const apiClient = new ApiClient();
|
||||
module.exports = apiClient;
|
||||
Reference in New Issue
Block a user