- 微信审核要求交易类小程序提交订单中心 path,之前小程序没有订单页 - 新页面 pages/orders/orders:列表展示商品名 / 状态 / 订单号 / 金额 / 下单时间,接 GET /v1/orders - 我的页新增「我的订单」入口,文案走后台配置 profile.menu.orders - utils/api.js 补 getOrders(),utils/format.js 补 toDateTimeString()
252 lines
8.9 KiB
JavaScript
252 lines
8.9 KiB
JavaScript
/**
|
||
* 统一请求层与接口封装。
|
||
*
|
||
* 替代旧版本「每个接口手写一遍 wx.request + Promise」的写法。
|
||
* 职责:
|
||
* - 自动附带 Authorization
|
||
* - 统一解包 { code, message, data, requestId }
|
||
* - token 失效自动刷新,刷新失败自动重新登录
|
||
*/
|
||
|
||
const config = require('../config/index');
|
||
const store = require('./store');
|
||
|
||
/** 进行中的会话恢复,避免并发 401 触发多次恢复流程 */
|
||
let recovering = null;
|
||
|
||
/**
|
||
* 服务端错误码:账号被封禁。
|
||
* 要和「登录过期」分开:页面据此走自己的提示分支,而不是把人赶去登录。
|
||
* 具体提示什么文字由后台配置决定,这里只管把错误码原样带出去。
|
||
*/
|
||
const ACCOUNT_DISABLED = 2003;
|
||
|
||
function staticUrl(path) {
|
||
if (!path) return '';
|
||
if (/^https?:\/\//i.test(path)) return path;
|
||
return config.STATIC_BASE_URL.replace(/\/+$/, '') + '/' + String(path).replace(/^\/+/, '');
|
||
}
|
||
|
||
/**
|
||
* 下单设备。平台按设备路由支付渠道(iOS 走 Apple 支付,只能用户自己找 App Store 退),
|
||
* 服务端靠这个字段判断一笔单能不能主动退款,所以下单时要一起报上去。
|
||
*/
|
||
function platform() {
|
||
try {
|
||
if (typeof wx.getDeviceInfo === 'function') return wx.getDeviceInfo().platform || 'unknown';
|
||
return wx.getSystemInfoSync().platform || 'unknown';
|
||
} catch (err) {
|
||
return 'unknown';
|
||
}
|
||
}
|
||
|
||
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 recoverSession()
|
||
.then(() => request(Object.assign({}, options, { retried: true })))
|
||
.catch((err) => {
|
||
// 账号异常:把原因原样抛给页面,并且不清本地用户——
|
||
// 清掉的话知识页会把它当成未登录,只剩一片空白,看不出发生了什么
|
||
if (err && err.code === ACCOUNT_DISABLED) throw err;
|
||
store.reset();
|
||
throw { code: 2000, message: '登录已过期,请重新进入小程序' };
|
||
});
|
||
}
|
||
|
||
throw normalizeError(body, statusCode);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 恢复会话。两级兜底:
|
||
* 1. 用 refreshToken 换新 accessToken(快,不打扰用户)
|
||
* 2. refreshToken 也没有或已失效时,重新走一次微信静默登录
|
||
*
|
||
* 第 2 级是必需的:「退出登录」会清空全部 token,
|
||
* 此后任何需要鉴权的操作(例如再次点「登录注册」改昵称)都必须先重新登录,
|
||
* 否则必然 401。
|
||
*/
|
||
function recoverSession() {
|
||
if (recovering) return recovering;
|
||
|
||
recovering = refreshAccessToken()
|
||
.catch(() => login())
|
||
.finally(() => {
|
||
recovering = null;
|
||
});
|
||
|
||
return recovering;
|
||
}
|
||
|
||
function refreshAccessToken() {
|
||
const refreshToken = store.getRefreshToken();
|
||
if (!refreshToken) return Promise.reject({ code: 2000, message: '未登录' });
|
||
|
||
return 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;
|
||
});
|
||
}
|
||
|
||
// ── 登录 ──────────────────────────────────────────────────────────────────
|
||
|
||
/** 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();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 用户主动登录:把数据库里的「已登录」标记置 1。
|
||
*
|
||
* 静默登录(login)不会碰这个标记——它只负责建立身份。只有用户亲自点了登录,
|
||
* 才算「主动使用过登录功能」,购买资格也以这个标记为准,而不是有没有 token。
|
||
*/
|
||
function activeLogin() {
|
||
return request({ url: '/v1/auth/login', method: 'POST' });
|
||
}
|
||
|
||
// ── 首页内容 ──────────────────────────────────────────────────────────────
|
||
|
||
const api = {
|
||
staticUrl,
|
||
login,
|
||
logout,
|
||
activeLogin,
|
||
|
||
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' }),
|
||
|
||
// ── 订单与支付 ──────────────────────────────────────────────────────
|
||
/** 我的订单列表(订单中心用)。 */
|
||
getOrders: () => request({ url: '/v1/orders' }),
|
||
createOrder: (courseCode) =>
|
||
request({
|
||
url: '/v1/orders',
|
||
method: 'POST',
|
||
data: { courseCode: courseCode, terminal: platform() },
|
||
}),
|
||
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, devicePlatform: platform, ACCOUNT_DISABLED });
|