diff --git a/src/App.vue b/src/App.vue index ddaa7c0..3660b63 100644 --- a/src/App.vue +++ b/src/App.vue @@ -179,7 +179,8 @@ export default { if (lp) { uni.setStorageSync("launchParam", lp) } - const { tryZzdEntryLogin } = await import("@/sheep/utils/request") + const { sanitizeBizSession, tryZzdEntryLogin } = await import("@/sheep/utils/request") + sanitizeBizSession() await tryZzdEntryLogin() const { onZzdAppLaunchTrack } = await import("@/sheep/utils/zzd-aplus-track") await onZzdAppLaunchTrack() diff --git a/src/pages/tabbar/index.vue b/src/pages/tabbar/index.vue index 15940b8..33cd38d 100644 --- a/src/pages/tabbar/index.vue +++ b/src/pages/tabbar/index.vue @@ -224,7 +224,17 @@ const jumpByParam = async () => { } } -const doRetry = () => { +const doRetry = async () => { + try { + const { clearBizSession } = await import("@/sheep/utils/request") + // 清掉脏 token / 错误 userRole,效果接近卸载重装里「清 WebView 登录态」 + clearBizSession({ clearRole: true }) + } catch (e) { + console.warn("[doRetry] clearBizSession", e) + } + jumping = false + bootStatus.value = "loading" + bootMessage.value = "加载中..." void jumpByParam() } @@ -251,7 +261,8 @@ const doModuleJump = (item: any) => { 加载中... {{ bootMessage }} - 重试 + 若多次失败,将清除本地登录缓存后重试 + 清除缓存并重试 diff --git a/src/pages/tabbar/my.vue b/src/pages/tabbar/my.vue index 0aba858..49e1077 100644 --- a/src/pages/tabbar/my.vue +++ b/src/pages/tabbar/my.vue @@ -87,6 +87,7 @@ import { hooks, router, constant } from "@@/index" import { actCancel, userApi } from "@@/api" import { isDevEnv } from "@/sheep/utils/app" import { APPENV } from "@/sheep/constant" +import { ensureBizLogin } from "@/sheep/utils/request" import ModifyUser from "@@/components/modify-user.vue" @@ -167,48 +168,35 @@ const storage = { } const login = { - // 检查应用环境并登录 + // 检查应用环境并登录(浙政钉走 getAuthCode/zzdsso,诗画走 authsh;勿再只认 URL uabaseToken) checkAppInfo: async () => { try { - // 先尝试诗画登录 - const shihuaResult = await login.shihuaLogin() - if (shihuaResult) return - - // 尝试浙政钉登录 - const zhengdingResult = login.zhengdingLogin() - if (zhengdingResult) return - - uni.showToast({ title: "当前登录环境未知", icon: "none" }) + const appInfo = await getAppInfo() + if (appInfo !== APPENV.ZZD && appInfo !== APPENV.SH) { + uni.showToast({ title: "当前登录环境未知", icon: "none" }) + return + } + const ok = await ensureBizLogin() + if (ok) { + storage.getUserInfo() + } } catch (error) { console.error("登录失败:", error) uni.showToast({ title: "登录失败", icon: "none" }) } }, - // 诗画环境登录 + // 保留:开发/兼容旧调用;生产点「请先登录」走 checkAppInfo → ensureBizLogin shihuaLogin: async () => { - const appInfo = await (window as any).CP2.getAppInfo() - if (appInfo.code == 0) return false - - const userInfo = await (window as any).CP2.getUserInfo() - if (userInfo.code == 0) { - uni.showToast({ title: "获取诗画用户信息失败", icon: "none" }) - return false - } else { - const { accountId, sessionId } = userInfo.data - return login.doLogin(1, "", accountId, sessionId) - // return login.doLogin(1, "", "67186e49d63fea141aad6772", "67186e49d63fea141aad6776") - } + const env = await getAppInfo() + if (env !== APPENV.SH) return false + return await ensureBizLogin() }, - // 浙政钉登录 - zhengdingLogin: () => { - const params = new URL(window.location.href).searchParams - const token = params.get("uabaseToken") - if (token) { - return login.doLogin(2, token, "", "") - } - return false + zhengdingLogin: async () => { + const env = await getAppInfo() + if (env !== APPENV.ZZD) return false + return await ensureBizLogin() }, // 执行登录操作 - loginType 1:诗画登录 2:浙政钉登录 diff --git a/src/pages/thirdApp/happyMall.vue b/src/pages/thirdApp/happyMall.vue index a36a369..3cd6782 100644 --- a/src/pages/thirdApp/happyMall.vue +++ b/src/pages/thirdApp/happyMall.vue @@ -3,8 +3,12 @@ 美好商城 + + {{ loginError }} + - 继续跳转 + 继续跳转 + 清除缓存并重试 import { ref } from "vue" import { onLoad } from "@dcloudio/uni-app" -import { hooks } from "@@/index" import { merchantApi } from "@@/api" +import { clearBizSession, ensureBizLogin, recoverBizSession } from "@@/utils/request" // import { filePath } from "@@/utils/file" // const params = reactive({ @@ -28,12 +32,40 @@ import { merchantApi } from "@@/api" // }) const show = ref(false) +const loginError = ref("") + +const ensureLoginAndMaybeOpen = async (autoOpenModal: boolean) => { + loginError.value = "" + try { + const ok = await ensureBizLogin() + if (ok) { + if (autoOpenModal) show.value = true + return + } + loginError.value = "登录失败,可点下方清除缓存后重试" + } catch (e: any) { + loginError.value = (e && e.message) || "登录异常,请重试" + } +} + +const retryLogin = async () => { + loginError.value = "" + try { + clearBizSession({ clearRole: true }) + const ok = await recoverBizSession() + if (ok) { + show.value = true + return + } + loginError.value = "仍无法登录,请关闭后从工作台重新进入" + } catch (e: any) { + loginError.value = (e && e.message) || "登录异常,请重试" + } +} onLoad(async () => { - const isLogin = await hooks.useCheckLogin() - if (isLogin) { - show.value = true - } + // 门户直接跳本页时本地未必有 token,需主动走浙政钉/诗画登录,不能只用 useCheckLogin 静默判断 + await ensureLoginAndMaybeOpen(true) }) // onShow(() => { diff --git a/src/sheep/hooks/use-token.ts b/src/sheep/hooks/use-token.ts index d55b76b..e7838a3 100644 --- a/src/sheep/hooks/use-token.ts +++ b/src/sheep/hooks/use-token.ts @@ -2,14 +2,23 @@ import storageConstant from "../constant/storage-constant" import { jump } from "../router/index" import { isDevEnv } from "../utils/app" +function readBearer(tokenStorage: any): string { + if (!tokenStorage || typeof tokenStorage !== "object" || tokenStorage.data == -1) return "" + const pick = (v: unknown): string => { + if (typeof v === "string" && v.trim()) return v.trim() + if (v && typeof v === "object" && typeof (v as { tokenValue?: unknown }).tokenValue === "string") { + return (v as { tokenValue: string }).tokenValue.trim() + } + return "" + } + const raw = pick(tokenStorage.accessToken) || pick(tokenStorage.access_token) + return raw ? `Bearer ${raw}` : "" +} + export const useToken = () => { const tokenKey = storageConstant.TOKEN const tokenStorage = uni.getStorageSync(tokenKey) - - if (isDevEnv()) { - return tokenStorage?.access_token ? `Bearer ${tokenStorage.access_token}` : "" - } - return tokenStorage?.accessToken?.tokenValue ? `Bearer ${tokenStorage.accessToken.tokenValue}` : "" + return readBearer(tokenStorage) } export const useDefaultHeader = () => { @@ -24,15 +33,15 @@ export const useDefaultHeader = () => { export const useStaticCheckLogin = () => { const tokenKey = storageConstant.TOKEN const tokenStorage = uni.getStorageSync(tokenKey) - if (tokenStorage) return true - return false + return !!readBearer(tokenStorage) } export const useCheckLogin = async (): Promise => { const tokenKey = storageConstant.TOKEN const tokenStorage = uni.getStorageSync(tokenKey) + const loggedIn = !!readBearer(tokenStorage) if (isDevEnv()) { - if (tokenStorage) return true + if (loggedIn) return true uni.showModal({ title: "提示", content: "您还未登录,请先登录", @@ -52,7 +61,7 @@ export const useCheckLogin = async (): Promise => { }) return false } else { - if (tokenStorage) return true + if (loggedIn) return true uni.showToast({ title: "你还未登录", icon: "none" }) return false } diff --git a/src/sheep/utils/app.ts b/src/sheep/utils/app.ts index 02b487f..71a8dce 100644 --- a/src/sheep/utils/app.ts +++ b/src/sheep/utils/app.ts @@ -45,11 +45,16 @@ export const getAppInfo = async (): Promise => { if (cp2?.getAppInfo) { const res = await cp2.getAppInfo() if (res?.code === 0) { - console.log("环境:浙政钉-zzd") - return APPENV.ZZD + // index.html 引入的 jcommind 在普通浏览器也可能 code=0,须同时有浙政钉容器特征 + if (looksLikeZzdContainer()) { + console.log("环境:浙政钉-zzd") + return APPENV.ZZD + } + console.warn("[getAppInfo] CP2.code=0 但无浙政钉容器特征,继续探测") + } else { + console.log("环境:诗画浦江-sh") + return APPENV.SH } - console.log("环境:诗画浦江-sh") - return APPENV.SH } } catch (error) { console.warn("[getAppInfo] CP2.getAppInfo 失败,回退容器探测", error) diff --git a/src/sheep/utils/dingAuthCode.ts b/src/sheep/utils/dingAuthCode.ts index 118742d..65c3365 100644 --- a/src/sheep/utils/dingAuthCode.ts +++ b/src/sheep/utils/dingAuthCode.ts @@ -6,6 +6,8 @@ const JSAPI_LOAD_TIMEOUT_MS = 45000 const DEFAULT_OPEN_JS_URL = "https://g.alicdn.com/dingding/dingtalk-jsapi/3.0.12/dingtalk.open.js" /** 微应用需 dd.config 完成后 ready 才会回调;超时则不再盲调 requestAuthCode */ const DD_READY_TIMEOUT_MS = 30000 +/** gdt getAuthCode 无回调时避免「登录中」遮罩一直盖住页面(白屏感) */ +const GET_AUTH_CODE_TIMEOUT_MS = 20000 let ddErrorCallbackRegistered = false @@ -181,7 +183,15 @@ export function authCodeFromGdtResult(res: unknown): string | null { */ export async function requestZzdAuthCodeViaGdt(corpId: string): Promise { await loadDingTalkOpenJsapi() - const res = await gdtJsapi.getAuthCode({ corpId: (corpId || "").trim() }) + const res = await Promise.race([ + gdtJsapi.getAuthCode({ corpId: (corpId || "").trim() }), + new Promise((_, rej) => + setTimeout( + () => rej(new Error(`getAuthCode 超时(${GET_AUTH_CODE_TIMEOUT_MS / 1000}s),请关闭后重新进入`)), + GET_AUTH_CODE_TIMEOUT_MS, + ), + ), + ]) const code = authCodeFromGdtResult(res) if (!code) throw new Error("getAuthCode 未返回有效 code(请核对 gdt 返回字段)") return code diff --git a/src/sheep/utils/request.ts b/src/sheep/utils/request.ts index 0257e24..a1e55cd 100644 --- a/src/sheep/utils/request.ts +++ b/src/sheep/utils/request.ts @@ -8,11 +8,75 @@ import { APPENV } from "../constant" const TOKEN_KEY = constant.storageConstant.TOKEN const USER_INFO_KEY = constant.storageConstant.USERINFO +const USER_ROLE_KEY = constant.storageConstant.USERROLE const SKIP_AUTH_URLS = ["/thirdparty/thirdPartyOAth2", "/sys/auth/refresh-token", "/auth/oauth2/token"] let refreshTokenPromise: Promise | null = null // 存储刷新token的Promise let isRefreshing = false // 是否正在刷新 let failedQueue: any[] = [] // 队列存储发送失败的请求 +/** App.onLaunch 与页面 ensureBizLogin 并发时复用同一次浙政钉登录,避免双重 showLoading/getAuthCode */ +let zzdLoginPromise: Promise<"ok" | "sms" | "failed"> | null = null +/** 拦截器 refreshToken、「我的」checkAppInfo、页面 ensureBizLogin 并发时复用同一次自动登录 */ +let autoLoginPromise: Promise | null = null +/** 诗画 /shjnsso 并发互斥(与浙政钉 zzdLoginPromise 对称) */ +let shLoginPromise: Promise | null = null + +/** + * 清除本地登录相关缓存(等价于「重装清 WebView」里最关键的几项)。 + * 默认保留 launchParam(入口参数);诗画卡角色时可 clearRole。 + */ +export function clearBizSession(options?: { clearRole?: boolean; clearLaunchParam?: boolean }) { + try { + uni.removeStorageSync(TOKEN_KEY) + } catch { + /* ignore */ + } + try { + uni.removeStorageSync(USER_INFO_KEY) + } catch { + /* ignore */ + } + if (options?.clearRole) { + try { + uni.removeStorageSync(USER_ROLE_KEY) + } catch { + /* ignore */ + } + } + if (options?.clearLaunchParam) { + const fromUrl = getUrlQueryParam("launchParam") + if (!fromUrl) { + try { + uni.removeStorageSync("launchParam") + } catch { + /* ignore */ + } + } + } +} + +/** 设备未信任占位,不是可请求的 token */ +function isDeviceVerifyMarker(tokenStorage: any): boolean { + return !!tokenStorage && typeof tokenStorage === "object" && tokenStorage.data == -1 +} + +/** + * 启动/登录前清理半残 token,避免「有 storage 但带不上 Authorization」导致白屏/进不去。 + * 保留 data=-1(设备核验)标记。 + */ +export function sanitizeBizSession(): void { + const tokenStorage = uni.getStorageSync(TOKEN_KEY) + if (isEmpty(tokenStorage)) return + if (isDeviceVerifyMarker(tokenStorage)) return + if (!getTokenValue(tokenStorage) || isTokenExpires(tokenStorage)) { + clearBizSession() + } +} + +function hasUsableBizToken(tokenStorage: any): boolean { + if (isEmpty(tokenStorage) || isDeviceVerifyMarker(tokenStorage)) return false + return !!getTokenValue(tokenStorage) && !isTokenExpires(tokenStorage) +} // 处理队列中的请求 const processQueue = (error: any, token: any = null) => { @@ -72,21 +136,14 @@ https.interceptors.request.use(async (config) => { // 判断是否需要设置认证头(url 可能是完整地址,故用 includes) if (!SKIP_AUTH_URLS.some((url) => (config.url || "").includes(url))) { - if (!isEmpty(tokenStorage)) { - // token存在,检查是否过期 - if (isTokenExpires(tokenStorage)) { - // 过期,刷新token - const newTokenStorage = await refreshToken() - if (newTokenStorage) { - const tokenValue = getTokenValue(newTokenStorage) - if (tokenValue) config_header.Authorization = tokenValue - } - } else { - const tokenValue = getTokenValue(tokenStorage) - if (tokenValue) config_header.Authorization = tokenValue - } + if (hasUsableBizToken(tokenStorage)) { + const tokenValue = getTokenValue(tokenStorage) + if (tokenValue) config_header.Authorization = tokenValue } else { - // token不存在,刷新token + // 无可用 token(含半残/过期/设备核验占位):清理半残后走自动登录 + if (!isEmpty(tokenStorage) && !isDeviceVerifyMarker(tokenStorage)) { + clearBizSession() + } const newTokenStorage = await refreshToken() if (newTokenStorage) { const tokenValue = getTokenValue(newTokenStorage) @@ -134,7 +191,9 @@ https.interceptors.response.use( if (tokenValue && config.header) config.header.Authorization = tokenValue return https.request(config) } + clearBizSession() } catch (err) { + clearBizSession() return Promise.reject(err) } } @@ -148,21 +207,44 @@ https.interceptors.response.use( }, ) -// 从tokenStorage中读取tokenValue值 +// 从tokenStorage中读取tokenValue值(兼容嵌套 tokenValue / 诗画字符串 accessToken) const getTokenValue = (tokenStorage: any): string | null => { - if (isDevEnv()) { - return tokenStorage?.access_token ? `Bearer ${tokenStorage.access_token}` : null + if (!tokenStorage || typeof tokenStorage !== "object") return null + if (isDeviceVerifyMarker(tokenStorage)) return null + const pick = (v: unknown): string | null => { + if (typeof v === "string" && v.trim()) return v.trim() + if (v && typeof v === "object" && typeof (v as { tokenValue?: unknown }).tokenValue === "string") { + const tv = (v as { tokenValue: string }).tokenValue.trim() + return tv || null + } + return null } - return tokenStorage?.accessToken?.tokenValue ? `Bearer ${tokenStorage.accessToken.tokenValue}` : null + const raw = + pick(tokenStorage.accessToken) || + pick(tokenStorage.access_token) || + (typeof tokenStorage.tokenValue === "string" ? tokenStorage.tokenValue.trim() : null) + return raw ? `Bearer ${raw}` : null } -// 检查tokenStorage中tokenValue是否过期 +// 检查是否过期;无有效 token / 脏 expiresAt 一律视为过期,避免半残数据永远「未过期」 const isTokenExpires = (tokenStorage: any): boolean => { - const { accessToken } = tokenStorage // 线上token字段 - const { access_token } = tokenStorage // 测试token字段 - - const expiresDate = new Date(accessToken?.expiresAt || access_token?.expiresAt).getTime() - return new Date().getTime() >= expiresDate + if (!tokenStorage || typeof tokenStorage !== "object") return true + if (isDeviceVerifyMarker(tokenStorage)) return true + if (!getTokenValue(tokenStorage)) return true + const expiresAt = + (tokenStorage.accessToken && typeof tokenStorage.accessToken === "object" + ? tokenStorage.accessToken.expiresAt + : null) ?? + (tokenStorage.access_token && typeof tokenStorage.access_token === "object" + ? tokenStorage.access_token.expiresAt + : null) + if (expiresAt == null || expiresAt === "") { + // 有 token 但无过期字段(部分诗画落库):先用,等 424 再刷 + return false + } + const expiresDate = new Date(expiresAt).getTime() + if (Number.isNaN(expiresDate)) return true + return Date.now() >= expiresDate } async function refreshToken(): Promise { @@ -211,16 +293,21 @@ async function refreshToken(): Promise { return refreshTokenPromise } -// 根据应用环境自动登录 返回登录结果 +// 根据应用环境自动登录 返回登录结果(全局互斥,避免清缓存后并发打多遍单点) const autoLogin = async (): Promise => { - const appInfo = await getAppInfo() - // 浙政钉环境 - if (appInfo == APPENV.ZZD) return await loginByZzd() - // 诗画环境 - if (appInfo == APPENV.SH) return await loginBySh() - - uni.showToast({ title: "app环境错误", icon: "none" }) - return false + if (autoLoginPromise) return autoLoginPromise + autoLoginPromise = (async () => { + try { + const appInfo = await getAppInfo() + if (appInfo == APPENV.ZZD) return await loginByZzd() + if (appInfo == APPENV.SH) return await loginBySh() + uni.showToast({ title: "app环境错误", icon: "none" }) + return false + } finally { + autoLoginPromise = null + } + })() + return autoLoginPromise } function isZzdNeedSmsVerify(data: unknown): data is { needSmsVerify: true; bindSessionId: string; displayName?: string } { @@ -244,75 +331,87 @@ function goZzdSmsVerify(data: { bindSessionId: string; displayName?: string }) { * @returns ok 已可带 token 请求;sms 已跳转核验页;failed 未登录 */ async function zzdLoginCore(): Promise<"ok" | "sms" | "failed"> { - try { - uni.showLoading({ title: "登录中...", mask: true }) - const tokenStorage = uni.getStorageSync(TOKEN_KEY) - if (!isEmpty(tokenStorage) && !isTokenExpires(tokenStorage) && getTokenValue(tokenStorage)) { - uni.hideLoading() - return "ok" - } + if (zzdLoginPromise) return zzdLoginPromise + zzdLoginPromise = (async () => { + try { + uni.showLoading({ title: "登录中...", mask: true }) + sanitizeBizSession() + const tokenStorage = uni.getStorageSync(TOKEN_KEY) + if (hasUsableBizToken(tokenStorage)) { + return "ok" + } - if (typeof window !== "undefined") { - try { - const { requestZzdAuthCodeViaGdt } = await import("./dingAuthCode") - const corpId = (import.meta.env.VITE_ZZD_CORP_ID || "").trim() - const authCode = await requestZzdAuthCodeViaGdt(corpId) - const res: any = await userApi.auth({ authCode }) - if (res?.code == 0) { - if (isZzdNeedSmsVerify(res.data)) { - goZzdSmsVerify(res.data as { bindSessionId: string; displayName?: string }) - return "sms" + if (typeof window !== "undefined") { + try { + const { requestZzdAuthCodeViaGdt } = await import("./dingAuthCode") + const corpId = (import.meta.env.VITE_ZZD_CORP_ID || "").trim() + const authCode = await requestZzdAuthCodeViaGdt(corpId) + const res: any = await userApi.auth({ authCode }) + if (res?.code == 0) { + if (isZzdNeedSmsVerify(res.data)) { + goZzdSmsVerify(res.data as { bindSessionId: string; displayName?: string }) + return "sms" + } + const d = res.data + if (!d || typeof d !== "object" || (!d.accessToken && !d.refreshToken)) { + clearBizSession() + uni.showToast({ title: "登录返回数据异常,请重试", icon: "none", duration: 4000 }) + return "failed" + } + uni.setStorageSync(TOKEN_KEY, res.data) + const { data: userData } = await userApi.userInfo() + uni.setStorageSync(USER_INFO_KEY, userData) + const { onZzdLoginSuccessTrack } = await import("./zzd-aplus-track") + void onZzdLoginSuccessTrack() + return "ok" } - const d = res.data - if (!d || typeof d !== "object" || (!d.accessToken && !d.refreshToken)) { - uni.showToast({ title: "登录返回数据异常,请重试", icon: "none", duration: 4000 }) - return "failed" + clearBizSession() + if (res?.msg) { + uni.showToast({ title: res.msg, icon: "none", duration: 4000 }) } - uni.setStorageSync(TOKEN_KEY, res.data) - const { data: userData } = await userApi.userInfo() - uni.setStorageSync(USER_INFO_KEY, userData) - const { onZzdLoginSuccessTrack } = await import("./zzd-aplus-track") - void onZzdLoginSuccessTrack() - return "ok" + return "failed" + } catch (e) { + console.warn("[loginByZzd] authCode 登录未成功,尝试 uabaseToken", e) } - if (res?.msg) { - uni.showToast({ title: res.msg, icon: "none", duration: 4000 }) + } + + const token = typeof window !== "undefined" ? getUrlQueryParam("uabaseToken") || null : null + + if (!token) { + clearBizSession() + uni.showToast({ title: "用户凭证已失效,请重新进入智慧后勤应用", icon: "none", duration: 6000 }) + return "failed" + } + + const res = await userApi.auth({ token }) + if (res?.code == 0) { + uni.setStorageSync(TOKEN_KEY, res.data) + const { data: userData } = await userApi.userInfo() + uni.setStorageSync(USER_INFO_KEY, userData) + const { onZzdLoginSuccessTrack } = await import("./zzd-aplus-track") + void onZzdLoginSuccessTrack() + return "ok" + } else { + clearBizSession() + if (res.msg == "token已失效") { + uni.showToast({ title: "用户凭证已失效,请退出并重新进入智慧后勤应用", icon: "none", duration: 6000 }) } return "failed" - } catch (e) { - console.warn("[loginByZzd] authCode 登录未成功,尝试 uabaseToken", e) } - } - - const token = typeof window !== "undefined" ? getUrlQueryParam("uabaseToken") || null : null - - if (!token) { - uni.showToast({ title: "用户凭证已失效,请重新进入智慧后勤应用", icon: "none", duration: 6000 }) + } catch (error: any) { + clearBizSession() + uni.showToast({ title: error, icon: "none" }) return "failed" - } - - const res = await userApi.auth({ token }) - if (res?.code == 0) { - uni.setStorageSync(TOKEN_KEY, res.data) - const { data: userData } = await userApi.userInfo() - uni.setStorageSync(USER_INFO_KEY, userData) - const { onZzdLoginSuccessTrack } = await import("./zzd-aplus-track") - void onZzdLoginSuccessTrack() - return "ok" - } else { - uni.removeStorageSync(TOKEN_KEY) - uni.removeStorageSync(USER_INFO_KEY) - if (res.msg == "token已失效") { - uni.showToast({ title: "用户凭证已失效,请退出并重新进入智慧后勤应用", icon: "none", duration: 6000 }) + } finally { + try { + uni.hideLoading() + } catch { + /* ignore */ } - return "failed" + zzdLoginPromise = null } - } catch (error: any) { - uni.showToast({ title: error, icon: "none" }) - return "failed" - } finally { - uni.hideLoading() - } + })() + return zzdLoginPromise } async function loginByZzd(): Promise { @@ -324,54 +423,74 @@ async function loginByZzd(): Promise { * 从门户/常用入口进入 H5 时主动跑浙政钉单点。非浙政钉环境立即返回 ok,不调用 zzdsso、不碰诗画逻辑。 */ export async function tryZzdEntryLogin(): Promise<"ok" | "sms" | "failed"> { + sanitizeBizSession() const appInfo = await getAppInfo() if (appInfo !== APPENV.ZZD) return "ok" return await zzdLoginCore() } -// 诗画登录 +// 诗画登录(互斥:避免「我的」与请求拦截器各打一遍 /shjnsso) async function loginBySh(): Promise { - try { - uni.showLoading({ title: "登录中...", mask: true }) + if (shLoginPromise) return shLoginPromise + shLoginPromise = (async () => { + try { + uni.showLoading({ title: "登录中...", mask: true }) + sanitizeBizSession() + const tokenStorage = uni.getStorageSync(TOKEN_KEY) + if (hasUsableBizToken(tokenStorage)) { + return true + } - const userInfo = await (window as any).CP2.getUserInfo() - if (userInfo.code == 0 || !userInfo.data.accountId || !userInfo.data.sessionId) { - uni.showToast({ title: "获取诗画用户信息失败", icon: "none" }) - return false - } - - const { accountId, sessionId } = userInfo.data - const response = await userApi.authsh({ accountId, sessionId }) - // const response = await userApi.authsh({ accountId: "67e790d6a95d5a7b507e53dd", sessionId: "68215b03a95d5a4899d20cb4" }) // 测试 - - if (response.code == 0) { - uni.setStorageSync(TOKEN_KEY, { accessToken: response.data.accessToken }) - const { data: userData } = await userApi.userInfo() - uni.setStorageSync(USER_INFO_KEY, userData) - uni.showToast({ title: "登录成功", icon: "none" }) - return true - } else { - uni.removeStorageSync(TOKEN_KEY) - uni.removeStorageSync(USER_INFO_KEY) - if (response.msg == "token已失效") { - uni.showToast({ title: "用户凭证已失效,请退出并重新进入智慧后勤应用", icon: "none", duration: 6000 }) + const userInfo = await (window as any).CP2.getUserInfo() + if (userInfo.code == 0 || !userInfo.data.accountId || !userInfo.data.sessionId) { + uni.showToast({ title: "获取诗画用户信息失败", icon: "none" }) return false } - // 设备未信任:后端 R.failed(-1, msg) 业务码在 code,data 为 null - if (response.code == -1) { - uni.setStorageSync(TOKEN_KEY, { data: -1 }) - } + const { accountId, sessionId } = userInfo.data + const response = await userApi.authsh({ accountId, sessionId }) - uni.showToast({ title: response.msg || "登录失败", icon: "none" }) + if (response.code == 0) { + const at = response.data?.accessToken + // 统一成可被 getTokenValue 识别的结构,避免只落了字符串导致后续读不到 + if (typeof at === "string") { + uni.setStorageSync(TOKEN_KEY, { accessToken: { tokenValue: at } }) + } else { + uni.setStorageSync(TOKEN_KEY, { accessToken: at }) + } + const { data: userData } = await userApi.userInfo() + uni.setStorageSync(USER_INFO_KEY, userData) + uni.showToast({ title: "登录成功", icon: "none" }) + return true + } else { + clearBizSession() + if (response.msg == "token已失效") { + uni.showToast({ title: "用户凭证已失效,请退出并重新进入智慧后勤应用", icon: "none", duration: 6000 }) + return false + } + + // 设备未信任:后端 R.failed(-1, msg) 业务码在 code,data 为 null + if (response.code == -1) { + uni.setStorageSync(TOKEN_KEY, { data: -1 }) + } + + uni.showToast({ title: response.msg || "登录失败", icon: "none" }) + return false + } + } catch (error) { + clearBizSession() + uni.showToast({ title: "登录错误", icon: "none" }) return false + } finally { + try { + uni.hideLoading() + } catch { + /* ignore */ + } + shLoginPromise = null } - } catch (error) { - uni.showToast({ title: "登录错误", icon: "none" }) - return false - } finally { - uni.hideLoading() - } + })() + return shLoginPromise } /** @@ -379,13 +498,28 @@ async function loginBySh(): Promise { * 与「我的」里先登录再扫码一致,保证后续支付接口带用户身份。 */ export async function ensureBizLogin(): Promise { + sanitizeBizSession() const tokenStorage = uni.getStorageSync(TOKEN_KEY) - if (!isEmpty(tokenStorage) && !isTokenExpires(tokenStorage) && getTokenValue(tokenStorage)) { + if (hasUsableBizToken(tokenStorage)) { return true } return await autoLogin() } +/** + * 进不去页时的「清缓存重试」:清 token/用户/诗画角色后重新自动登录。 + * 保留 URL 上的 launchParam。 + */ +export async function recoverBizSession(): Promise { + clearBizSession({ clearRole: true }) + zzdLoginPromise = null + shLoginPromise = null + autoLoginPromise = null + refreshTokenPromise = null + isRefreshing = false + return await autoLogin() +} + // 请求函数 const request = (config: any) => { return https.middleware(config)