四端上传
This commit is contained in:
1 parent
25bbb5f9c8
commit
c6e2cb4d5b
276 files changed
+17812
-3
No files matched your search
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 统一请求层与接口封装。
|
||||
*
|
||||
* 替代旧版本「每个接口手写一遍 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 });
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/** 展示用格式化。 */
|
||||
|
||||
function pad(n) {
|
||||
return n < 10 ? '0' + n : String(n);
|
||||
}
|
||||
|
||||
/** ISO 字符串 → YYYY-MM-DD */
|
||||
function toDateString(value) {
|
||||
if (!value) return '';
|
||||
const d = new Date(value);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
||||
}
|
||||
|
||||
/** 分 → 元(保留两位,去掉多余的 .00) */
|
||||
function fenToYuan(fen) {
|
||||
const n = Number(fen);
|
||||
if (!isFinite(n)) return '0';
|
||||
const yuan = n / 100;
|
||||
return yuan % 1 === 0 ? String(yuan) : yuan.toFixed(2);
|
||||
}
|
||||
|
||||
module.exports = { toDateString, fenToYuan };
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 极简全局状态。
|
||||
*
|
||||
* 替代旧版本在各页面直接读写 wx.getStorageSync 的做法:
|
||||
* 统一从内存读、同步落盘,页面只与这里交互。
|
||||
*/
|
||||
|
||||
const config = require('../config/index');
|
||||
|
||||
const memory = {
|
||||
user: null,
|
||||
siteConfig: null,
|
||||
entitlements: [],
|
||||
};
|
||||
|
||||
const listeners = new Set();
|
||||
|
||||
function notify() {
|
||||
listeners.forEach((fn) => {
|
||||
try {
|
||||
fn();
|
||||
} catch (e) {
|
||||
console.error('[store] 监听器执行失败', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function read(key, fallback) {
|
||||
try {
|
||||
const v = wx.getStorageSync(key);
|
||||
return v === '' || v === undefined || v === null ? fallback : v;
|
||||
} catch (e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const store = {
|
||||
// ── token ────────────────────────────────────────────────────────────
|
||||
getToken: () => read(config.STORAGE.TOKEN, ''),
|
||||
getRefreshToken: () => read(config.STORAGE.REFRESH_TOKEN, ''),
|
||||
|
||||
setTokens(accessToken, refreshToken) {
|
||||
wx.setStorageSync(config.STORAGE.TOKEN, accessToken);
|
||||
if (refreshToken) wx.setStorageSync(config.STORAGE.REFRESH_TOKEN, refreshToken);
|
||||
},
|
||||
|
||||
clearTokens() {
|
||||
wx.removeStorageSync(config.STORAGE.TOKEN);
|
||||
wx.removeStorageSync(config.STORAGE.REFRESH_TOKEN);
|
||||
},
|
||||
|
||||
isLoggedIn: () => !!read(config.STORAGE.TOKEN, ''),
|
||||
|
||||
// ── 用户 ─────────────────────────────────────────────────────────────
|
||||
getUser() {
|
||||
if (memory.user) return memory.user;
|
||||
memory.user = read(config.STORAGE.USER, null);
|
||||
return memory.user;
|
||||
},
|
||||
|
||||
setUser(user) {
|
||||
memory.user = user;
|
||||
wx.setStorageSync(config.STORAGE.USER, user);
|
||||
notify();
|
||||
},
|
||||
|
||||
// ── 站点配置 ─────────────────────────────────────────────────────────
|
||||
getSiteConfig() {
|
||||
if (memory.siteConfig) return memory.siteConfig;
|
||||
memory.siteConfig = read(config.STORAGE.CONFIG, {});
|
||||
return memory.siteConfig;
|
||||
},
|
||||
|
||||
setSiteConfig(cfg) {
|
||||
memory.siteConfig = cfg || {};
|
||||
wx.setStorageSync(config.STORAGE.CONFIG, memory.siteConfig);
|
||||
notify();
|
||||
},
|
||||
|
||||
/** 取配置项,带兜底值,避免配置缺失时页面直接崩掉。 */
|
||||
cfg(key, fallback) {
|
||||
const value = store.getSiteConfig()[key];
|
||||
return value === undefined || value === '' ? (fallback === undefined ? '' : fallback) : value;
|
||||
},
|
||||
|
||||
// ── 已购课程 ─────────────────────────────────────────────────────────
|
||||
getEntitlements() {
|
||||
if (memory.entitlements && memory.entitlements.length) return memory.entitlements;
|
||||
memory.entitlements = read(config.STORAGE.ENTITLEMENTS, []) || [];
|
||||
return memory.entitlements;
|
||||
},
|
||||
|
||||
setEntitlements(list) {
|
||||
memory.entitlements = list || [];
|
||||
wx.setStorageSync(config.STORAGE.ENTITLEMENTS, memory.entitlements);
|
||||
notify();
|
||||
},
|
||||
|
||||
hasCourse(code) {
|
||||
return store.getEntitlements().some((item) => item.code === code);
|
||||
},
|
||||
|
||||
// ── 订阅与清理 ───────────────────────────────────────────────────────
|
||||
subscribe(fn) {
|
||||
listeners.add(fn);
|
||||
return () => listeners.delete(fn);
|
||||
},
|
||||
|
||||
reset() {
|
||||
memory.user = null;
|
||||
memory.siteConfig = null;
|
||||
memory.entitlements = [];
|
||||
store.clearTokens();
|
||||
wx.removeStorageSync(config.STORAGE.USER);
|
||||
wx.removeStorageSync(config.STORAGE.ENTITLEMENTS);
|
||||
notify();
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = store;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const formatTime = timestamp => {
|
||||
const date = new Date(timestamp);
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
|
||||
}
|
||||
|
||||
const formatNumber = n => {
|
||||
n = n.toString()
|
||||
return n[1] ? n : `0${n}`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatTime
|
||||
}
|
||||
Reference in new issue
Block a user