Files
danyang-match/miniapp/utils/api.js
T

156 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;