“mitao”
2026-05-06 2225a5d9243e7f8ece8b57e781534b21acd1ab00
fix:token续期
1个文件已添加
12个文件已修改
635 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/controller/AppAuthController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/service/AppSettingsService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/App.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/bindPhone/bindPhone.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/clearCache/clearCache.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/login/login.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/login/service.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/settings/utils.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/auth-session.js 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/request.js 310 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/controller/AppAuthController.java
@@ -110,4 +110,12 @@
        appAuthService.logout();
        return ApiResponse.ok(null);
    }
    /**
     * 刷新当前 APP 登录态有效期。
     */
    @PostMapping("/refresh-token")
    public ApiResponse<AppLoginResponse> refreshToken() {
        return ApiResponse.ok(appAuthService.refreshToken());
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java
@@ -12,7 +12,6 @@
import com.tcm.training.admin.app.auth.vo.AppLoginUserVO;
import com.tcm.training.admin.app.auth.vo.AppPasswordResetVerifyResponse;
import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
import com.tcm.training.admin.app.auth.exception.InviteCodeRequiredException;
import com.tcm.training.admin.app.share.service.AppShareService;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.mapper.UserMapper;
@@ -36,7 +35,6 @@
    private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?![0-9]+$)(?![a-zA-Z]+$)(?![^0-9a-zA-Z]+$).{8,32}$");
    private static final String PASSWORD_RESET_TICKET_CACHE_PREFIX = "app:password-reset:ticket:";
    private static final long PASSWORD_RESET_TICKET_EXPIRE_MINUTES = 10L;
    private final UserMapper userMapper;
    private final AppSmsCodeService appSmsCodeService;
    private final BCryptPasswordEncoder passwordEncoder;
@@ -115,7 +113,6 @@
        AppWechatUserProfile profile = appWechatAuthClient.resolveUserProfile(request);
        AppUser user = findUserByWechatOpenid(profile.getOpenid());
        if (user == null) {
            requireInviteCodeForThirdPartyRegister(request.getInviteCode());
            user = buildWechatUser(profile);
            bindInviteRelationIfAbsent(user, request.getInviteCode());
            userMapper.insert(user);
@@ -136,7 +133,6 @@
        AppAppleUserProfile profile = appAppleAuthClient.resolveUserProfile(request);
        AppUser user = findUserByAppleSubject(profile.getSubject());
        if (user == null) {
            requireInviteCodeForThirdPartyRegister(request.getInviteCode());
            user = buildAppleUser(profile);
            bindInviteRelationIfAbsent(user, request.getInviteCode());
            userMapper.insert(user);
@@ -187,11 +183,28 @@
    }
    /**
     * 刷新当前 APP 登录态,并重新签发一枚新的 token。
     */
    public AppLoginResponse refreshToken() {
        Long userId = appAuthSessionService.requireLoginUserId();
        AppUser user = requireEnabledUserById(userId);
        // 当前 Sa-Token 版本不支持按 token 原地续期,这里直接为当前用户重新签发新 token。
        appAuthSessionService.login(user.getId());
        return buildLoginResponse(user, appAuthSessionService.getTokenInfo());
    }
    /**
     * 为成功登录的 APP 用户建立登录态并拼装统一响应。
     */
    private AppLoginResponse createLoginResponse(AppUser user) {
        appAuthSessionService.login(user.getId());
        SaTokenInfo tokenInfo = appAuthSessionService.getTokenInfo();
        return buildLoginResponse(user, appAuthSessionService.getTokenInfo());
    }
    /**
     * 统一构造 APP 登录响应,保证登录与续期接口返回结构一致。
     */
    private AppLoginResponse buildLoginResponse(AppUser user, SaTokenInfo tokenInfo) {
        return new AppLoginResponse(
                tokenInfo.getTokenValue(),
                "Bearer",
@@ -235,6 +248,18 @@
     */
    private AppUser requireEnabledUserByPhone(String phone) {
        AppUser user = requireExistingUserByPhone(phone);
        requireEnabledUser(user);
        return user;
    }
    /**
     * 按用户主键查询并校验 APP 用户状态。
     */
    private AppUser requireEnabledUserById(Long userId) {
        AppUser user = userMapper.selectById(userId);
        if (user == null || user.getDeleted() == null || user.getDeleted().intValue() != 0) {
            throw new IllegalArgumentException("用户不存在");
        }
        requireEnabledUser(user);
        return user;
    }
@@ -284,15 +309,6 @@
    }
    /**
     * 第三方授权首次创建账号时必须携带医生邀请码。
     */
    private void requireInviteCodeForThirdPartyRegister(String inviteCode) {
        if (normalizeInviteCode(inviteCode) == null) {
            throw new InviteCodeRequiredException();
        }
    }
    /**
     * 用户尚未绑定诊所关系时,根据医生邀请码补齐上下级关系。
     */
    private void bindInviteRelationIfAbsent(AppUser user, String inviteCode) {
@@ -325,13 +341,14 @@
    private void applyWechatProfile(AppUser user, AppWechatUserProfile profile, boolean syncLocalProfile) {
        String nickname = trimToNull(profile.getNickname());
        String avatar = trimToNull(profile.getAvatar());
        String currentNickname = trimToNull(user.getNickname());
        user.setWechatOpenid(profile.getOpenid());
        user.setWechatNickname(nickname);
        user.setWechatAvatar(avatar);
        user.setWechatBoundAt(LocalDateTime.now());
        if (syncLocalProfile || trimToNull(user.getNickname()) == null) {
        if (shouldRefreshLocalNickname(currentNickname, profile.getOpenid(), nickname, syncLocalProfile)) {
            user.setNickname(nickname == null ? profile.getOpenid() : nickname);
        }
        if (syncLocalProfile || trimToNull(user.getAvatar()) == null) {
@@ -343,6 +360,40 @@
    }
    /**
     * 仅在首次建号、昵称为空、openid 兜底值或历史乱码场景下刷新本地昵称,避免覆盖用户手动修改结果。
     */
    private boolean shouldRefreshLocalNickname(String currentNickname,
                                               String openid,
                                               String latestWechatNickname,
                                               boolean syncLocalProfile) {
        if (syncLocalProfile) {
            return true;
        }
        if (trimToNull(latestWechatNickname) == null) {
            return false;
        }
        String normalizedCurrentNickname = trimToNull(currentNickname);
        if (normalizedCurrentNickname == null) {
            return true;
        }
        String normalizedOpenid = trimToNull(openid);
        if (normalizedOpenid != null && normalizedOpenid.equals(normalizedCurrentNickname)) {
            return true;
        }
        return containsNicknameMojibake(normalizedCurrentNickname);
    }
    /**
     * 识别历史微信昵称乱码的常见特征,限定为已知乱码模式,避免误伤正常昵称。
     */
    private boolean containsNicknameMojibake(String nickname) {
        return nickname.contains("ð")
                || nickname.contains("Ã")
                || nickname.contains("â")
                || nickname.contains("�");
    }
    /**
     * 将苹果资料回填到本地用户,并在首次自动建号时补齐基础展示昵称。
     */
    private void applyAppleProfile(AppUser user, AppAppleUserProfile profile, boolean syncLocalProfile) {
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthSessionService.java
@@ -47,6 +47,13 @@
    }
    /**
     * 获取当前请求携带的 APP token 值。
     */
    public String getTokenValue() {
        return StpAppUserUtil.getTokenValue();
    }
    /**
     * 注销当前 APP 登录态。
     */
    public void logout() {
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java
@@ -49,6 +49,20 @@
    }
    /**
     * 获取底层 APP 登录态逻辑对象,供需要调用 Sa-Token 高级能力的服务复用。
     */
    public static StpLogic getStpLogic() {
        return STP_LOGIC;
    }
    /**
     * 获取当前请求携带的 token 值。
     */
    public static String getTokenValue() {
        return STP_LOGIC.getTokenValue();
    }
    /**
     * 注销当前 APP 登录态。
     */
    public static void logout() {
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/service/AppSettingsService.java
@@ -10,6 +10,7 @@
import com.tcm.training.admin.app.auth.service.AppSmsCodeService;
import com.tcm.training.admin.app.auth.service.AppWechatAuthClient;
import com.tcm.training.admin.app.auth.service.AppWechatUserProfile;
import com.tcm.training.admin.app.auth.exception.InviteCodeRequiredException;
import com.tcm.training.admin.app.settings.dto.AppSettingsNotificationUpdateRequest;
import com.tcm.training.admin.app.settings.dto.AppSettingsPasswordUpdateRequest;
import com.tcm.training.admin.app.settings.dto.AppSettingsPhoneBindRequest;
@@ -168,6 +169,7 @@
        }
        AppUser occupiedUser = findUserByPhone(newPhone);
        validatePhoneBindTarget(user, occupiedUser);
        requireInviteCodeWhenDoctorRelationMissing(user, occupiedUser, request.getInviteCode());
        appSmsCodeService.validatePhoneChangeCode(newPhone, code);
        AppUser loginUser = occupiedUser == null
                ? bindPhoneToCurrentUser(user, newPhone, request.getInviteCode())
@@ -208,10 +210,34 @@
        requireEnabledUser(targetUser);
        copyThirdPartyBinding(currentUser, targetUser);
        fillMissingProfile(targetUser, currentUser);
        inheritDoctorRelationIfAbsent(targetUser, currentUser);
        bindInviteRelationIfAbsent(targetUser, inviteCode);
        userMapper.updateById(targetUser);
        disableMergedTemporaryUser(currentUser);
        return targetUser;
    }
    /**
     * 当临时账号和目标手机号账号都没有医生关系时,绑定手机号阶段必须携带邀请码。
     */
    private void requireInviteCodeWhenDoctorRelationMissing(AppUser currentUser, AppUser targetUser, String inviteCode) {
        if (hasDoctorRelation(currentUser) || hasDoctorRelation(targetUser)) {
            return;
        }
        if (normalizeInviteCode(inviteCode) == null) {
            throw new InviteCodeRequiredException();
        }
    }
    /**
     * 合并到已有手机号账号时,优先继承临时账号上已存在的医生关系。
     */
    private void inheritDoctorRelationIfAbsent(AppUser targetUser, AppUser sourceUser) {
        if (hasDoctorRelation(targetUser) || !hasDoctorRelation(sourceUser)) {
            return;
        }
        targetUser.setClinicId(sourceUser.getClinicId());
        targetUser.setSuperiorClinicUserId(sourceUser.getSuperiorClinicUserId());
    }
    /**
@@ -487,6 +513,13 @@
    }
    /**
     * 判断账号是否已绑定医生上下级关系。
     */
    private boolean hasDoctorRelation(AppUser user) {
        return user != null && (user.getClinicId() != null || user.getSuperiorClinicUserId() != null);
    }
    /**
     * 为绑定后的用户签发新的 APP 登录态。
     */
    private AppLoginResponse createLoginResponse(AppUser user) {
traditionalApp/App.vue
@@ -7,6 +7,7 @@
        extractBindTicket,
        extractInviteCode
    } from '@/utils/utils.js'
    import { hasBoundPhone, hasStoredSession } from '@/utils/auth-session.js'
    import { cacheInviteCode } from '@/utils/invite-cache.js'
    import { initJPushLifecycle } from '@/utils/push.js'
    import {
@@ -70,6 +71,9 @@
            return
        }
        lastHandledInvitePayload = payloadKey
        if (hasStoredSession() && hasBoundPhone()) {
            return
        }
        const currentPages = getCurrentPages()
        const currentRoute = currentPages && currentPages.length > 0 ? currentPages[currentPages.length - 1].route : ''
        const targetParams = []
traditionalApp/pages/bindPhone/bindPhone.vue
@@ -45,6 +45,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { bindSettingsPhone, sendBindPhoneCode } from './service.js'
import { saveAuthSession } from '@/utils/auth-session.js'
import { clearCachedInviteCode, getCachedInviteCode } from '@/utils/invite-cache.js'
interface ApiResponse<T> {
@@ -180,6 +181,10 @@
            code: normalizedCode,
            inviteCode: getCachedInviteCode()
        })
        if (shouldShowInviteRequiredGuide(response)) {
            showInviteRequiredGuide()
            return
        }
        if (!isSuccessResponse(response)) {
            return
        }
@@ -233,17 +238,11 @@
 * 绑定成功后保存后端返回的新登录态,确保后续请求切到正式账号。
 */
const saveLoginSession = (loginData: AppLoginPayload | undefined, fallbackPhone: string) => {
    const userInfo = loginData?.user || {}
    const tokenValue = normalizeOptionalText(loginData?.token)
    if (tokenValue) {
        const authToken = `${loginData?.tokenType || 'Bearer'} ${tokenValue}`.trim()
        uni.setStorageSync('token', authToken)
    }
    uni.setStorageSync('appUserInfo', {
        ...userInfo,
        phone: normalizeOptionalText(userInfo.phone) || fallbackPhone
    saveAuthSession(loginData, {
        userOverrides: {
            phone: normalizeOptionalText(loginData?.user?.phone) || fallbackPhone
        }
    })
    uni.setStorageSync('showLogoutModal', 1)
}
/**
@@ -254,6 +253,30 @@
}
/**
 * 判断是否需要展示缺少邀请码引导弹窗。
 */
const shouldShowInviteRequiredGuide = (response: ApiResponse<unknown> | undefined) => {
    return !!response && response.code === 506 && normalizeOptionalText(response.msg || response.message) === '暂无邀请码,请联系管理员获取'
}
/**
 * 展示缺少邀请码弹窗,关闭后跳转到邀请码说明详情。
 */
const showInviteRequiredGuide = () => {
    uni.showModal({
        title: '提示',
        content: '暂无邀请码,请联系管理员获取',
        showCancel: false,
        confirmText: '我知道了',
        success: () => {
            uni.navigateTo({
                url: '/pages/help/help?type=invite'
            })
        }
    })
}
/**
 * 将可选文本归一化为空字符串。
 */
const normalizeOptionalText = (value: unknown) => {
traditionalApp/pages/clearCache/clearCache.vue
@@ -99,6 +99,7 @@
    return new Promise<void>((resolve) => {
        const token = uni.getStorageSync('token')
        const appUserInfo = uni.getStorageSync('appUserInfo')
        const tokenExpireAt = uni.getStorageSync('tokenExpireAt')
        const loginRedirectMeta = uni.getStorageSync('loginRedirectMeta')
        const showLogoutModal = uni.getStorageSync('showLogoutModal')
        const inviteCodeCacheKey = getInviteCodeCacheKey()
@@ -110,6 +111,9 @@
        if (appUserInfo) {
            uni.setStorageSync('appUserInfo', appUserInfo)
        }
        if (tokenExpireAt) {
            uni.setStorageSync('tokenExpireAt', tokenExpireAt)
        }
        if (loginRedirectMeta) {
            uni.setStorageSync('loginRedirectMeta', loginRedirectMeta)
        }
traditionalApp/pages/login/login.vue
@@ -61,6 +61,7 @@
    import { onLoad } from '@dcloudio/uni-app'
    import { computed, onUnmounted, ref } from 'vue'
    import { appleLogin, passwordLogin, sendSmsCode, smsLogin, wechatLogin } from './service.js'
    import { saveAuthSession } from '@/utils/auth-session.js'
    import { syncJPushAliasWithCurrentUser } from '@/utils/push.js'
    import { clearCachedInviteCode, getCachedInviteCode } from '@/utils/invite-cache.js'
@@ -654,18 +655,15 @@
        if (!loginData) {
            return
        }
        const authToken = `${loginData.tokenType || 'Bearer'} ${loginData.token || ''}`.trim()
        uni.setStorageSync('token', authToken)
        uni.setStorageSync('appUserInfo', loginData.user || {})
        uni.setStorageSync('showLogoutModal', 1)
        saveAuthSession(loginData, {
            redirectMeta: {
                inviteUserId: inviteUserId.value,
                inviteType: inviteType.value
            }
        })
        if (loginData.user?.clinicId) {
            clearCachedInviteCode()
        }
        // 先保留来源参数,后续如需登录后回跳可直接复用。
        uni.setStorageSync('loginRedirectMeta', {
            inviteUserId: inviteUserId.value,
            inviteType: inviteType.value
        })
        syncJPushAliasWithCurrentUser()
    }
traditionalApp/pages/login/service.js
@@ -36,6 +36,13 @@
}
/**
 * 刷新当前 APP 登录态有效期。
 */
export const refreshAuthToken = () => {
    return request.post('/api/app/auth/refresh-token', {}, false, false)
}
/**
 * 确认上级用户邀请。
 */
export const confirmSuperior = (params) => {
traditionalApp/pages/settings/utils.js
@@ -1,5 +1,6 @@
import { normalizeOptionalText } from '@/utils/utils.js'
import { clearJPushAliasBinding } from '@/utils/push.js'
import { clearAuthSession } from '@/utils/auth-session.js'
/**
 * 判断接口响应是否成功。
@@ -65,10 +66,7 @@
 * 清理登录态缓存,保持与请求拦截器的处理一致。
 */
export const clearSettingsLoginSession = () => {
    uni.removeStorageSync('token')
    uni.removeStorageSync('appUserInfo')
    uni.removeStorageSync('loginRedirectMeta')
    uni.setStorageSync('showLogoutModal', 0)
    clearAuthSession()
    clearJPushAliasBinding()
}
traditionalApp/utils/auth-session.js
New file
@@ -0,0 +1,102 @@
const TOKEN_KEY = 'token'
const USER_KEY = 'appUserInfo'
const TOKEN_EXPIRE_AT_KEY = 'tokenExpireAt'
const LOGOUT_MODAL_KEY = 'showLogoutModal'
const REDIRECT_META_KEY = 'loginRedirectMeta'
/**
 * 读取当前缓存中的 APP token。
 */
export function getStoredToken() {
    const token = uni.getStorageSync(TOKEN_KEY)
    return typeof token === 'string' ? token.trim() : ''
}
/**
 * 判断当前是否存在本地登录态。
 */
export function hasStoredSession() {
    return !!getStoredToken()
}
/**
 * 读取当前缓存中的 APP 用户信息。
 */
export function getStoredAppUser() {
    const user = uni.getStorageSync(USER_KEY)
    return user && typeof user === 'object' ? user : {}
}
/**
 * 判断当前缓存用户是否已完成手机号绑定。
 */
export function hasBoundPhone() {
    const user = getStoredAppUser()
    return typeof user.phone === 'string' && user.phone.trim() !== ''
}
/**
 * 从登录响应中持久化 APP 登录态。
 */
export function saveAuthSession(loginData, extras = {}) {
    if (!loginData || typeof loginData !== 'object') {
        return
    }
    const tokenValue = typeof loginData.token === 'string' ? loginData.token.trim() : ''
    if (tokenValue) {
        const tokenType = typeof loginData.tokenType === 'string' && loginData.tokenType.trim() ? loginData.tokenType.trim() : 'Bearer'
        uni.setStorageSync(TOKEN_KEY, `${tokenType} ${tokenValue}`.trim())
    }
    const userInfo = loginData.user && typeof loginData.user === 'object' ? loginData.user : {}
    uni.setStorageSync(USER_KEY, {
        ...userInfo,
        ...extras.userOverrides,
    })
    uni.setStorageSync(LOGOUT_MODAL_KEY, 1)
    persistTokenExpireAt(loginData.expiresIn)
    if (extras.redirectMeta) {
        uni.setStorageSync(REDIRECT_META_KEY, extras.redirectMeta)
    }
}
/**
 * 根据 expiresIn 持久化 token 预计过期时间戳。
 */
export function persistTokenExpireAt(expiresIn) {
    const expireSeconds = Number(expiresIn)
    if (!Number.isFinite(expireSeconds) || expireSeconds <= 0) {
        uni.removeStorageSync(TOKEN_EXPIRE_AT_KEY)
        return
    }
    uni.setStorageSync(TOKEN_EXPIRE_AT_KEY, Date.now() + expireSeconds * 1000)
}
/**
 * 读取当前 token 过期时间戳。
 */
export function getTokenExpireAt() {
    const rawValue = Number(uni.getStorageSync(TOKEN_EXPIRE_AT_KEY) || 0)
    return Number.isFinite(rawValue) && rawValue > 0 ? rawValue : 0
}
/**
 * 判断当前 token 是否即将过期。
 */
export function willTokenExpireWithin(bufferMs = 0) {
    const expireAt = getTokenExpireAt()
    if (!expireAt) {
        return false
    }
    return expireAt - Date.now() <= bufferMs
}
/**
 * 清理本地 APP 登录态缓存。
 */
export function clearAuthSession() {
    uni.removeStorageSync(TOKEN_KEY)
    uni.removeStorageSync(USER_KEY)
    uni.removeStorageSync(TOKEN_EXPIRE_AT_KEY)
    uni.removeStorageSync(REDIRECT_META_KEY)
    uni.setStorageSync(LOGOUT_MODAL_KEY, 0)
}
traditionalApp/utils/request.js
@@ -1,21 +1,33 @@
const GET = 'GET';
const POST = 'POST';
const PUT = 'PUT';
const FORM = 'FORM';
const DELETE = 'DELETE';
import config from '@/config/index.js'
import { clearJPushAliasBinding } from '@/utils/push.js'
const baseURL = config.BASE_URL;
import {
    clearAuthSession,
    getStoredToken,
    hasBoundPhone,
    saveAuthSession,
    willTokenExpireWithin,
} from '@/utils/auth-session.js'
const waitingList = [] //等待队列
const excutingList = [] //执行队列
const GET = 'GET'
const POST = 'POST'
const PUT = 'PUT'
const FORM = 'FORM'
const DELETE = 'DELETE'
const baseURL = config.BASE_URL
const TOKEN_REFRESH_BUFFER_MS = 30 * 60 * 1000
const SUCCESS_CODES = [0, 200]
const UNAUTHORIZED_CODES = [401, 504, 505, 510]
const waitingList = []
const excutingList = []
let appAuthRedirecting = false
let refreshTokenPromise = null
/**
 * 判断当前响应码是否属于成功态。
 */
function isSuccessCode(code) {
    return code === 0 || code === 200
    return SUCCESS_CODES.includes(code)
}
/**
@@ -32,104 +44,111 @@
}
/**
 * 读取当前语言请求头值。
 */
function resolveLangHeader() {
    return uni.getStorageSync('locale') === 'zh-Hans' ? 'zh_CN' : 'zh-tw'
}
/**
 * 构造当前请求头。
 */
function buildHeaders(type) {
    return {
        'content-type': type ? 'application/x-www-form-urlencoded;charset=UTF-8' : 'application/json',
        Authorization: getStoredToken(),
        lang: resolveLangHeader(),
    }
}
/**
 * 统一展示普通业务错误提示。
 */
function showErrorToast(payload) {
    uni.showToast({
        title: resolveMessage(payload),
        duration: 2000,
        icon: 'none',
        mask: true,
    })
}
/**
 * 统一发起请求,并兼容当前仓库 `code=0` 的后端成功协议。
 */
function request(method, url, data, type, lodingFlag = true) {
    if (lodingFlag) {
        uni.showLoading({
            mask: true,
        })
    }
    return new Promise(function(resolve, reject) {
        let token = uni.getStorageSync('token')
        let header = {
            'content-type': type ? 'application/x-www-form-urlencoded;charset=UTF-8' : 'application/json',
            'Authorization': token,
            'lang': uni.getStorageSync('locale') === 'zh-Hans' ? 'zh_CN' : 'zh-tw',
        }
        const requestTask = uni.request({
            url: baseURL + url,
            method: method,
            data: data,
            header: header,
            success: (res) => {
                if (lodingFlag) {
                    uni.hideLoading()
                }
                const payload = res.data || {}
                if (isSuccessCode(payload.code)) {
                    resolve(payload);
                    return
                }
                if (payload.code == 502) {
                    resolve(payload);
                    uni.showToast({
                        title: resolveMessage(payload),
                        duration: 2000,
                        icon: 'none',
                        mask: true,
                    })
                    return
                }
                if (payload.code == 1 || payload.code == 500) {
                    uni.showToast({
                        title: resolveMessage(payload),
                        duration: 2000,
                        icon: 'none',
                        mask: true,
                    })
                    resolve(payload)
                    return
                }
                if (payload.code == 401 || payload.code == 510 || payload.code == 504 || payload.code == 505) {
                    handleUnauthorized(payload)
                    resolve(payload)
                    return
                }
                if (payload.code == 501) {
                    uni.showToast({
                        title: resolveMessage(payload),
                        duration: 2000,
                        icon: 'none',
                        mask: true,
                    })
                    resolve(payload)
                    return
                }
                if (payload.code == 506) {
                    resolve(payload);
                    return
                }
                uni.showToast({
                    title: resolveMessage(payload),
                    duration: 2000,
                    icon: 'none',
    return new Promise((resolve, reject) => {
        const executeRequest = () => {
            if (lodingFlag) {
                uni.showLoading({
                    mask: true,
                })
                resolve(payload)
            },
            fail(err) {
                if (lodingFlag) {
                    uni.hideLoading()
                }
                reject(err)
            },
            complete: () => {
                const excutingIndex = excutingList.findIndex(item => item === requestTask)
                excutingList.splice(excutingIndex, 1)
                const [apiFn] = waitingList
                if (excutingList.length === 0 && typeof apiFn === 'function') {
                    apiFn()
                    waitingList.splice(0, 1)
                }
            },
        })
        excutingList.push(requestTask)
            }
            const requestTask = uni.request({
                url: `${baseURL}${url}`,
                method,
                data,
                header: buildHeaders(type),
                success: (res) => {
                    if (lodingFlag) {
                        uni.hideLoading()
                    }
                    const payload = res.data || {}
                    if (isSuccessCode(payload.code)) {
                        resolve(payload)
                        return
                    }
                    if (payload.code === 502 || payload.code === 501) {
                        showErrorToast(payload)
                        resolve(payload)
                        return
                    }
                    if (payload.code === 1 || payload.code === 500) {
                        showErrorToast(payload)
                        resolve(payload)
                        return
                    }
                    if (UNAUTHORIZED_CODES.includes(payload.code)) {
                        handleUnauthorized(payload)
                        resolve(payload)
                        return
                    }
                    if (payload.code === 506) {
                        resolve(payload)
                        return
                    }
                    showErrorToast(payload)
                    resolve(payload)
                },
                fail: (error) => {
                    if (lodingFlag) {
                        uni.hideLoading()
                    }
                    reject(error)
                },
                complete: () => {
                    const excutingIndex = excutingList.findIndex((item) => item === requestTask)
                    if (excutingIndex >= 0) {
                        excutingList.splice(excutingIndex, 1)
                    }
                    const [apiFn] = waitingList
                    if (excutingList.length === 0 && typeof apiFn === 'function') {
                        apiFn()
                        waitingList.splice(0, 1)
                    }
                },
            })
            excutingList.push(requestTask)
        }
        ensureSessionFresh(url)
            .then(() => {
                executeRequest()
            })
            .catch((error) => {
                reject(error)
            })
    })
}
/**
@@ -151,7 +170,7 @@
    })
    setTimeout(() => {
        uni.reLaunch({
            url: '/pages/login/login'
            url: '/pages/login/login',
        })
        setTimeout(() => {
            appAuthRedirecting = false
@@ -163,10 +182,7 @@
 * 清除 APP 登录态缓存。
 */
function clearLoginSession() {
    uni.removeStorageSync('token')
    uni.removeStorageSync('appUserInfo')
    uni.removeStorageSync('loginRedirectMeta')
    uni.setStorageSync('showLogoutModal', 0)
    clearAuthSession()
    clearJPushAliasBinding()
}
@@ -182,19 +198,83 @@
}
/**
 * 清除请求队列。
 * 清除请求队列,避免登录失效后旧请求继续回写界面状态。
 */
function clearAllRequest() {
    if (excutingList.length > 0) {
        for (var i = 0; i < excutingList.length; i++) {
            const item = excutingList[i]
            if (item && item.abort) {
                item.abort()
            }
    excutingList.forEach((item) => {
        if (item && typeof item.abort === 'function') {
            item.abort()
        }
        excutingList.length = 0
        waitingList.length = 0
    })
    excutingList.length = 0
    waitingList.length = 0
}
/**
 * 在请求发送前按阈值串行刷新 APP token,避免多个接口同时触发续期。
 */
function ensureSessionFresh(url) {
    if (!shouldAttemptTokenRefresh(url)) {
        return Promise.resolve()
    }
    if (refreshTokenPromise) {
        return refreshTokenPromise
    }
    refreshTokenPromise = refreshAppToken().finally(() => {
        refreshTokenPromise = null
    })
    return refreshTokenPromise
}
/**
 * 判断当前请求是否需要在发送前尝试刷新 token。
 */
function shouldAttemptTokenRefresh(url) {
    const token = getStoredToken()
    if (!token || !hasBoundPhone()) {
        return false
    }
    if (url === '/api/app/auth/refresh-token' || url === '/api/v1/app/auth/refresh-token') {
        return false
    }
    const routeStr = getCurrentRoute()
    if (routeStr === 'pages/login/login' || routeStr === 'pages/register/register' || routeStr === 'pages/bindPhone/bindPhone') {
        return false
    }
    return willTokenExpireWithin(TOKEN_REFRESH_BUFFER_MS)
}
/**
 * 调用后端续期接口并回写本地 token 与用户信息。
 */
function refreshAppToken() {
    return new Promise((resolve, reject) => {
        uni.request({
            url: `${baseURL}/api/app/auth/refresh-token`,
            method: POST,
            data: {},
            header: {
                'content-type': 'application/json',
                Authorization: getStoredToken(),
                lang: resolveLangHeader(),
            },
            success: (res) => {
                const payload = res.data || {}
                if (isSuccessCode(payload.code) && payload.data) {
                    saveAuthSession(payload.data)
                    resolve(payload)
                    return
                }
                if (UNAUTHORIZED_CODES.includes(payload.code)) {
                    handleUnauthorized(payload)
                }
                reject(payload)
            },
            fail: (error) => {
                reject(error)
            },
        })
    })
}
/**