From 25753010a24ecee30711a12434e25ae629139631 Mon Sep 17 00:00:00 2001
From: zjf <1799650030@qq.com>
Date: Mon, 10 Aug 2026 00:06:20 +0800
Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/App.vue | 3 +-
src/pages/tabbar/index.vue | 15 +-
src/pages/thirdApp/happyMall.vue | 44 ++++-
src/sheep/hooks/use-token.ts | 27 ++-
src/sheep/utils/app.ts | 13 +-
src/sheep/utils/dingAuthCode.ts | 12 +-
src/sheep/utils/request.ts | 291 +++++++++++++++++++++----------
7 files changed, 294 insertions(+), 111 deletions(-)
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/thirdApp/happyMall.vue b/src/pages/thirdApp/happyMall.vue
index da5161f..7778adb 100644
--- a/src/pages/thirdApp/happyMall.vue
+++ b/src/pages/thirdApp/happyMall.vue
@@ -43,8 +43,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({
@@ -69,12 +73,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..eebbed1 100644
--- a/src/sheep/utils/request.ts
+++ b/src/sheep/utils/request.ts
@@ -8,11 +8,71 @@ 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
+
+/**
+ * 清除本地登录相关缓存(等价于「重装清 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 +132,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 +187,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 +203,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 {
@@ -244,75 +322,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,6 +414,7 @@ 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()
@@ -333,6 +424,7 @@ export async function tryZzdEntryLogin(): Promise<"ok" | "sms" | "failed"> {
async function loginBySh(): Promise {
try {
uni.showLoading({ title: "登录中...", mask: true })
+ sanitizeBizSession()
const userInfo = await (window as any).CP2.getUserInfo()
if (userInfo.code == 0 || !userInfo.data.accountId || !userInfo.data.sessionId) {
@@ -345,14 +437,19 @@ async function loginBySh(): Promise {
// const response = await userApi.authsh({ accountId: "67e790d6a95d5a7b507e53dd", sessionId: "68215b03a95d5a4899d20cb4" }) // 测试
if (response.code == 0) {
- uni.setStorageSync(TOKEN_KEY, { accessToken: response.data.accessToken })
+ 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 {
- uni.removeStorageSync(TOKEN_KEY)
- uni.removeStorageSync(USER_INFO_KEY)
+ clearBizSession()
if (response.msg == "token已失效") {
uni.showToast({ title: "用户凭证已失效,请退出并重新进入智慧后勤应用", icon: "none", duration: 6000 })
return false
@@ -367,10 +464,15 @@ async function loginBySh(): Promise {
return false
}
} catch (error) {
+ clearBizSession()
uni.showToast({ title: "登录错误", icon: "none" })
return false
} finally {
- uni.hideLoading()
+ try {
+ uni.hideLoading()
+ } catch {
+ /* ignore */
+ }
}
}
@@ -379,13 +481,26 @@ 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
+ refreshTokenPromise = null
+ isRefreshing = false
+ return await autoLogin()
+}
+
// 请求函数
const request = (config: any) => {
return https.middleware(config)
From 87307392e4a60253dfd0479bb9b2c3dd49550d38 Mon Sep 17 00:00:00 2001
From: zjf <1799650030@qq.com>
Date: Mon, 10 Aug 2026 00:40:42 +0800
Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/tabbar/my.vue | 14 +---
src/sheep/utils/request.ts | 129 +++++++++++++++++++++----------------
2 files changed, 77 insertions(+), 66 deletions(-)
diff --git a/src/pages/tabbar/my.vue b/src/pages/tabbar/my.vue
index 0aba858..7b8e245 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"
@@ -185,20 +186,11 @@ const login = {
}
},
- // 诗画环境登录
+ // 诗画环境登录:走统一 ensureBizLogin,避免与请求拦截器各打一遍 /shjnsso
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")
- }
+ return await ensureBizLogin()
},
// 浙政钉登录
diff --git a/src/sheep/utils/request.ts b/src/sheep/utils/request.ts
index eebbed1..a1e55cd 100644
--- a/src/sheep/utils/request.ts
+++ b/src/sheep/utils/request.ts
@@ -16,6 +16,10 @@ 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」里最关键的几项)。
@@ -289,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 } {
@@ -420,60 +429,68 @@ export async function tryZzdEntryLogin(): Promise<"ok" | "sms" | "failed"> {
return await zzdLoginCore()
}
-// 诗画登录
+// 诗画登录(互斥:避免「我的」与请求拦截器各打一遍 /shjnsso)
async function loginBySh(): Promise {
- try {
- uni.showLoading({ title: "登录中...", mask: true })
- sanitizeBizSession()
-
- 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) {
- const at = response.data?.accessToken
- // 统一成可被 getTokenValue 识别的结构,避免只落了字符串导致后续读不到
- if (typeof at === "string") {
- uni.setStorageSync(TOKEN_KEY, { accessToken: { tokenValue: at } })
- } else {
- uni.setStorageSync(TOKEN_KEY, { accessToken: at })
+ 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 { 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 })
+
+ 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) {
- clearBizSession()
- uni.showToast({ title: "登录错误", icon: "none" })
- return false
- } finally {
- try {
- uni.hideLoading()
- } catch {
- /* ignore */
- }
- }
+ })()
+ return shLoginPromise
}
/**
@@ -496,6 +513,8 @@ export async function ensureBizLogin(): Promise {
export async function recoverBizSession(): Promise {
clearBizSession({ clearRole: true })
zzdLoginPromise = null
+ shLoginPromise = null
+ autoLoginPromise = null
refreshTokenPromise = null
isRefreshing = false
return await autoLogin()
From b787e27d80cc7d71e102d7bc4f150c8a962a60e1 Mon Sep 17 00:00:00 2001
From: zjf <1799650030@qq.com>
Date: Mon, 10 Aug 2026 01:04:02 +0800
Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/tabbar/my.vue | 38 +++++++++++++++++---------------------
1 file changed, 17 insertions(+), 21 deletions(-)
diff --git a/src/pages/tabbar/my.vue b/src/pages/tabbar/my.vue
index 7b8e245..49e1077 100644
--- a/src/pages/tabbar/my.vue
+++ b/src/pages/tabbar/my.vue
@@ -168,39 +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" })
}
},
- // 诗画环境登录:走统一 ensureBizLogin,避免与请求拦截器各打一遍 /shjnsso
+ // 保留:开发/兼容旧调用;生产点「请先登录」走 checkAppInfo → ensureBizLogin
shihuaLogin: async () => {
- const appInfo = await (window as any).CP2.getAppInfo()
- if (appInfo.code == 0) return false
+ 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:浙政钉登录