chore: establish V1.1 recovery baseline

This commit is contained in:
2026-09-09 22:05:00 +08:00
commit b46b48eb33
947 changed files with 155471 additions and 0 deletions
+155
View File
@@ -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;
+147
View File
@@ -0,0 +1,147 @@
/**
* API 统一请求层(TypeScript)
*
* 职责:纯 HTTP 客户端,统一处理 Token / 超时 / 错误码 / Session 失效。
*/
import { API_BASE_URL } from '../env';
import type { ApiResponse } from '../types';
export const REQUEST_ERROR = {
NETWORK_ERROR: 'NETWORK_ERROR',
UNAUTHORIZED: 'UNAUTHORIZED',
FORBIDDEN: 'FORBIDDEN',
RATE_LIMITED: 'RATE_LIMITED',
BUSINESS_ERROR: 'BUSINESS_ERROR',
SERVER_ERROR: 'SERVER_ERROR',
MAINTENANCE: 'MAINTENANCE',
} as const;
export interface ApiError {
code: string;
message: string;
requestId: string;
error?: unknown;
}
export class ApiClient {
baseURL: string;
token: string | null = null;
refreshToken: string | null = null;
onSessionExpired: ((reason: any) => void) | null = null;
onMaintenance: ((data: any) => void) | null = null;
constructor() {
this.baseURL = API_BASE_URL;
}
getToken(): string | null {
if (this.token) return this.token;
try {
this.token = wx.getStorageSync('accessToken') as string || null;
this.refreshToken = wx.getStorageSync('refreshToken') as string || null;
} catch { /* ignore */ }
return this.token;
}
getRefreshToken(): string | null {
if (this.refreshToken) return this.refreshToken;
try {
this.refreshToken = wx.getStorageSync('refreshToken') as string || null;
} catch { /* ignore */ }
return this.refreshToken;
}
saveToken(token: string, refreshToken?: string): void {
this.token = token;
this.refreshToken = refreshToken || null;
wx.setStorageSync('accessToken', token);
if (refreshToken) wx.setStorageSync('refreshToken', refreshToken);
}
clearToken(): void {
this.token = null;
this.refreshToken = null;
wx.removeStorageSync('accessToken');
wx.removeStorageSync('refreshToken');
}
generateRequestId(): string {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}
request<T>(options: {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
data?: unknown;
headers?: Record<string, string>;
idempotencyKey?: string;
timeout?: number;
}): Promise<T> {
const { url, method = 'GET', data, headers = {}, idempotencyKey, timeout = 10000 } = options;
const token = this.getToken();
const requestId = this.generateRequestId();
return new Promise((resolve, reject) => {
wx.request({
url: this.baseURL + url,
method,
data,
timeout,
header: {
'Content-Type': 'application/json',
'X-Request-Id': requestId,
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...headers,
},
success: (res: any) => {
if (res.statusCode === 503) {
if (this.onMaintenance) this.onMaintenance(res.data || {});
reject({ code: REQUEST_ERROR.MAINTENANCE, message: '平台正在维护,请稍后再试', requestId });
return;
}
if (res.statusCode === 200) {
const body = (res.data as any) || {};
if (body.success) {
resolve(body.data as T);
} else {
reject({ code: body.code || REQUEST_ERROR.BUSINESS_ERROR, message: body.message, requestId });
}
} else if (res.statusCode === 401) {
this.clearToken();
if (this.onSessionExpired) 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<T>(url: string, data?: unknown, headers?: Record<string, string>): Promise<T> {
return this.request<T>({ url, method: 'GET', data, headers });
}
post<T>(url: string, data?: unknown, headers?: Record<string, string>, idempotencyKey?: string): Promise<T> {
return this.request<T>({ url, method: 'POST', data, headers, idempotencyKey });
}
put<T>(url: string, data?: unknown, headers?: Record<string, string>): Promise<T> {
return this.request<T>({ url, method: 'PUT', data, headers });
}
delete<T>(url: string, data?: unknown, headers?: Record<string, string>): Promise<T> {
return this.request<T>({ url, method: 'DELETE', data, headers });
}
}
export const api = new ApiClient();
export default api;