ltx d17f43135d feat: 完善多角色赛事服务功能
- 新增参赛者、裁判、检录员角色识别及角色化首页与个人中心
- 新增裁判注册、身份认证提示、赛事执裁报名及报名记录查询
- 新增检录工作台,支持检录任务、轮次状态与运动员检录操作
- 新增赛事成绩查询页面及成绩详情展示
- 封装成绩、检录和裁判报名接口,补充请求参数清理逻辑
- 新增检录 Tab 图标、页面路由及本地开发配置
- 新增 Git 忽略规则,排除依赖、构建缓存、日志和本地工具文件
2026-08-07 11:28:38 +08:00

285 lines
9.6 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.

const { getBaseUrl } = require('./constants');
// Token刷新状态和请求队列
let isRefreshing = false;
let requestQueue = [];
function withAuthHeader(needAuth, header, useRefreshTokenOnly = false) {
const app = getApp();
if (!needAuth) return header;
const accessToken = app && app.globalData ? app.globalData.accessToken : '';
const refreshToken = app && app.globalData ? app.globalData.refreshToken : '';
header = header || {};
if (useRefreshTokenOnly && refreshToken) {
// 只使用 refresh token 模式(用于刷新)
header['X-Authorization'] = `Bearer ${refreshToken}`;
} else if (accessToken) {
// 正常模式:使用 access token
header.Authorization = `Bearer ${accessToken}`;
// 同时带上 refresh token,供后端在需要时自动刷新
if (refreshToken) {
header['X-Authorization'] = `Bearer ${refreshToken}`;
}
}
return header;
}
function normalizeError(payload) {
if (!payload) return new Error('Empty response');
// RESTfulResult usually has `errors` and/or `message`
if (typeof payload === 'string') return new Error(payload);
if (payload.errors) return new Error(payload.errors.toString());
if (payload.message) return new Error(payload.message);
return new Error(JSON.stringify(payload));
}
function getHeaderValue(headers, headerName) {
if (!headers || typeof headers !== 'object') return '';
const target = String(headerName || '').toLowerCase();
for (const k in headers) {
if (Object.prototype.hasOwnProperty.call(headers, k) && String(k).toLowerCase() === target) {
const v = headers[k];
return typeof v === 'string' ? v : '';
}
}
return '';
}
function tryUpdateAccessTokenFromResponseHeaders(res) {
// wx.request 的返回头在 res.header(部分环境也可能是 res.headers)
const headers = (res && res.header) || (res && res.headers) || null;
// 从响应头获取新的 token
const newAccessToken = getHeaderValue(headers, 'access-token') || getHeaderValue(headers, 'x-access-token');
const newRefreshToken = getHeaderValue(headers, 'x-access-token') || getHeaderValue(headers, 'x-refresh-token');
if (!newAccessToken && !newRefreshToken) return false;
const app = getApp();
const authData = {};
let hasUpdate = false;
if (newAccessToken && newAccessToken !== 'invalid_token') {
authData.accessToken = newAccessToken;
hasUpdate = true;
}
if (newRefreshToken && newRefreshToken !== 'invalid_token') {
authData.refreshToken = newRefreshToken;
hasUpdate = true;
}
if (hasUpdate) {
if (app && typeof app.setAuth === 'function') {
app.setAuth(authData);
} else {
if (authData.accessToken) {
wx.setStorageSync('accessToken', authData.accessToken);
if (app && app.globalData) app.globalData.accessToken = authData.accessToken;
}
if (authData.refreshToken) {
wx.setStorageSync('refreshToken', authData.refreshToken);
if (app && app.globalData) app.globalData.refreshToken = authData.refreshToken;
}
}
}
// 如果正在刷新Token且获取到了新Token,执行队列中的请求
if (isRefreshing && newAccessToken && newAccessToken !== 'invalid_token') {
isRefreshing = false;
requestQueue.forEach((callback) => callback(newAccessToken));
requestQueue = [];
return true;
}
return hasUpdate;
}
function clearAllTokensAndRedirect() {
wx.clearStorageSync();
const app = getApp();
if (app && app.globalData) {
app.globalData.accessToken = '';
app.globalData.refreshToken = '';
}
wx.showModal({
title: '登录失效',
content: '您的登录已失效,请重新登录。',
showCancel: false,
complete() {
wx.navigateTo({ url: '/subpackages/mine/login/login' });
}
});
}
/**
* 清理请求参数,避免微信请求层把 undefined/null 转成 URL 中的字符串。
* GET 会移除无法被后端模型绑定的空参数;POST 仅移除 undefined,保留合法 null。
*/
function sanitizeRequestData(value, isGet) {
if (value === undefined) return undefined;
if (isGet && (value === null || value === 'undefined' || value === 'null')) return undefined;
if (typeof value === 'number' && !Number.isFinite(value)) return undefined;
if (Array.isArray(value)) {
return value
.map((item) => sanitizeRequestData(item, isGet))
.filter((item) => item !== undefined);
}
if (value && typeof value === 'object') {
const result = {};
Object.keys(value).forEach((key) => {
const cleaned = sanitizeRequestData(value[key], isGet);
if (cleaned !== undefined) result[key] = cleaned;
});
return result;
}
return value;
}
function request({
path,
method = 'GET',
data = {},
needAuth = false,
contentType = 'application/json',
// Internal flag: prevent infinite retry when refresh token也失败
__retry = false,
// Internal flag: 标记这是刷新token的重试请求
__refreshRetry = false
}) {
const baseUrl = getBaseUrl();
const url = `${baseUrl}${path}`;
const upperMethod = method.toUpperCase();
const requestData = sanitizeRequestData(data, upperMethod === 'GET');
// 正常请求:同时带上 access token 和 refresh token
// 刷新重试请求:只使用 refresh token
const header = withAuthHeader(needAuth, {
'content-type': contentType
}, __refreshRetry);
return new Promise((resolve, reject) => {
wx.request({
url,
method: upperMethod,
data: requestData,
header: header,
success: async (res) => {
// 如果后端在 Response Headers 下发新的 token,就替换本地 token
const tokenUpdated = tryUpdateAccessTokenFromResponseHeaders(res);
const statusCode = res && res.data && res.data.statusCode ? res.data.statusCode : 0;
const errors = res && res.data && res.data.errors ? res.data.errors : null;
const payload = res && res.data !== undefined ? res.data : null;
// ── 401 未授权:尝试刷新Token ──
if (statusCode === 401 && needAuth && !__retry) {
// 如果已经在刷新Token,将请求加入队列
if (isRefreshing) {
requestQueue.push((newToken) => {
resolve(request({ path, method, data, needAuth, contentType, __retry: true }));
});
return;
}
// 开始刷新Token
// 重发原请求
isRefreshing = true;
try {
// 检查是否有 refresh token
const app = getApp();
const refreshToken = app && app.globalData ? app.globalData.refreshToken : '';
if (!refreshToken) {
throw new Error('No refresh token available');
}
// 重发请求
await request({
path,
method,
data,
needAuth,
contentType,
__retry: false,
__refreshRetry: true
});
// 刷新成功,现在重试原请求
isRefreshing = false;
resolve(request({ path, method, data, needAuth, contentType, __retry: true }));
return;
} catch (refreshErr) {
// 刷新失败,清除Token并跳转登录
isRefreshing = false;
requestQueue = [];
clearAllTokensAndRedirect();
reject(new Error(errors));
return;
}
}
// ── 401 未授权(重试后仍然失败) ──
if (statusCode === 401) {
clearAllTokensAndRedirect();
reject(new Error(errors));
return;
}
// ── 403 禁止访问 ──
if ( statusCode == 400 || statusCode == 403 ) {
wx.showToast({ title: errors || '请求失败', icon: 'none' });
reject(new Error(errors));
return;
}
// ── 404 资源不存在 ───────────────────────────────────────────
if (statusCode == 404) {
wx.showToast({ title: errors || '请求资源不存在', icon: 'none' });
reject(new Error(errors));
return;
}
// ── 5xx 服务端错误 ───────────────────────────────────────────
if (statusCode >= 500) {
wx.showToast({ title: errors || '服务器异常,请稍后重试', icon: 'none' });
reject(new Error(errors));
return;
}
// ── 其他非 2xx ───────────────────────────────────────────────
if (statusCode !== 0 && (statusCode < 200 || statusCode >= 300)) {
wx.showToast({ title: errors || `请求失败(${statusCode})`, icon: 'none' });
reject(new Error(errors));
return;
}
// ── 2xx 成功:解析业务层 RESTfulResult ────────────────────────
if (payload && typeof payload === 'object' && typeof payload.succeeded === 'boolean') {
if (payload.succeeded) {
resolve(payload.data);
} else {
reject(normalizeError(payload));
}
return;
}
// ── Fallback:直接返回原始 data ───────────────────────────────
resolve(payload);
},
fail(err) {
// 网络请求失败处理
reject(err);
}
});
});
}
module.exports = {
request
};