Files
YelayMaths/yelaymaths/utils/api.js
T
2026-09-29 15:25:40 +08:00

199 lines
6.9 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.
/**
* 统一请求层与接口封装。
*
* 替代旧版本「每个接口手写一遍 wx.request + Promise」的写法。
* 职责:
* - 自动附带 Authorization
* - 统一解包 { code, message, data, requestId }
* - token 失效自动刷新,刷新失败自动重新登录
*/
const config = require('../config/index');
const store = require('./store');
/** 进行中的刷新,避免并发 401 触发多次刷新 */
let refreshing = null;
function staticUrl(path) {
if (!path) return '';
if (/^https?:\/\//i.test(path)) return path;
return config.STATIC_BASE_URL.replace(/\/+$/, '') + '/' + String(path).replace(/^\/+/, '');
}
function normalizeError(body, statusCode) {
if (body && typeof body.code === 'number') {
return { code: body.code, message: body.message || '请求失败', requestId: body.requestId };
}
return { code: -1, message: '服务异常(' + statusCode + ')' };
}
/** 底层请求。不做鉴权重试,返回完整响应体。 */
function rawRequest(options) {
const header = { 'content-type': 'application/json' };
if (options.auth !== false) {
const token = store.getToken();
if (token) header.Authorization = 'Bearer ' + token;
}
return new Promise((resolve, reject) => {
wx.request({
url: config.API_BASE_URL.replace(/\/+$/, '') + options.url,
method: options.method || 'GET',
data: options.data,
header,
timeout: config.TIMEOUT,
success(res) {
resolve({ statusCode: res.statusCode, body: res.data || {} });
},
fail() {
reject({ code: -1, message: '网络异常,请检查网络后重试' });
},
});
});
}
/** 带鉴权的请求。token 失效时刷新一次并重试。 */
function request(options) {
return rawRequest(options).then(({ statusCode, body }) => {
if (body.code === 0) return body.data;
const needLogin = statusCode === 401 || body.code === 2000 || body.code === 2001;
if (needLogin && options.auth !== false && options.retried !== true) {
return refreshAccessToken()
.then(() => request(Object.assign({}, options, { retried: true })))
.catch(() => {
store.reset();
throw { code: 2000, message: '登录已过期,请重新进入小程序' };
});
}
throw normalizeError(body, statusCode);
});
}
function refreshAccessToken() {
if (refreshing) return refreshing;
const refreshToken = store.getRefreshToken();
if (!refreshToken) return Promise.reject({ code: 2000, message: '未登录' });
refreshing = rawRequest({
url: '/v1/auth/refresh',
method: 'POST',
data: { refreshToken },
auth: false,
})
.then(({ body }) => {
if (body.code !== 0) throw { code: 2000, message: '登录已过期' };
store.setTokens(body.data.accessToken);
return body.data.accessToken;
})
.finally(() => {
refreshing = null;
});
return refreshing;
}
// ── 登录 ──────────────────────────────────────────────────────────────────
/** wx.login 换 code,再用 code 换服务端 token。 */
function login() {
return new Promise((resolve, reject) => {
wx.login({
success(res) {
if (!res.code) {
reject({ code: 3002, message: '微信登录失败,请重试' });
return;
}
request({
url: '/v1/auth/wx-login',
method: 'POST',
data: { code: res.code },
auth: false,
})
.then((data) => {
store.setTokens(data.accessToken, data.refreshToken);
store.setUser(data.user);
resolve(data);
})
.catch(reject);
},
fail() {
reject({ code: 3002, message: '微信登录失败,请重试' });
},
});
});
}
function logout() {
return request({ url: '/v1/auth/logout', method: 'POST' }).catch(() => null).then(() => {
store.reset();
});
}
// ── 首页内容 ──────────────────────────────────────────────────────────────
const api = {
staticUrl,
login,
logout,
getConfig: () => request({ url: '/v1/config', auth: false }),
getBanners: () => request({ url: '/v1/banners', auth: false }),
getHomeCourses: () => request({ url: '/v1/home/courses', auth: false }),
getHomeFreeCourses: () => request({ url: '/v1/home/free-courses', auth: false }),
getArticles: (page, pageSize) =>
request({ url: '/v1/articles?page=' + (page || 1) + '&pageSize=' + (pageSize || 10), auth: false }),
getArticle: (id) => request({ url: '/v1/articles/' + id, auth: false }),
getTips: () => request({ url: '/v1/tips', auth: false }),
// ── 课程 ────────────────────────────────────────────────────────────
getCourses: () => request({ url: '/v1/courses' }),
getCourse: (code) => request({ url: '/v1/courses/' + code }),
getLessons: (code) => request({ url: '/v1/courses/' + code + '/lessons' }),
getPlayUrl: (code, chapterId, playId) =>
request({
url:
'/v1/courses/' +
code +
'/play?chapterId=' +
encodeURIComponent(chapterId) +
'&playId=' +
encodeURIComponent(playId),
}),
getFreeLessons: (seriesNo) =>
request({ url: '/v1/free-lessons' + (seriesNo === undefined ? '' : '?seriesNo=' + seriesNo), auth: false }),
// ── 用户 ────────────────────────────────────────────────────────────
getMe: () => request({ url: '/v1/me' }),
updateMe: (payload) => request({ url: '/v1/me', method: 'PATCH', data: payload }),
getEntitlements: () => request({ url: '/v1/me/entitlements' }),
// ── 订单与支付 ──────────────────────────────────────────────────────
createOrder: (courseCode) =>
request({ url: '/v1/orders', method: 'POST', data: { courseCode: courseCode } }),
getOrder: (outTradeNo) => request({ url: '/v1/orders/' + outTradeNo }),
};
/**
* 拉起虚拟支付。
*
* 支付成功只代表「钱付了」,权益由服务端收到发货推送后发放,
* 因此这里不直接开通课程,而是让调用方轮询订单状态。
*/
function requestVirtualPayment(payData) {
return new Promise((resolve, reject) => {
wx.requestVirtualPayment({
signData: payData.signData,
mode: payData.mode,
paySig: payData.paySig,
signature: payData.signature,
success: resolve,
fail: reject,
});
});
}
module.exports = Object.assign(api, { requestVirtualPayment });