376 lines
12 KiB
TypeScript
376 lines
12 KiB
TypeScript
import Request from "luch-request"
|
||
import { router, constant } from "@@/index"
|
||
import { isEmpty } from "radash"
|
||
import { getAppInfo, getUrlQueryParam, isDevEnv } from "@@/utils/app"
|
||
import { userApi } from "../api"
|
||
import showVerifyDeviceImpModel from "../components/verifyDeviceImpModel/verifyDeviceImpModel"
|
||
import { APPENV } from "../constant"
|
||
|
||
const TOKEN_KEY = constant.storageConstant.TOKEN
|
||
const USER_INFO_KEY = constant.storageConstant.USERINFO
|
||
const SKIP_AUTH_URLS = ["/thirdparty/thirdPartyOAth2", "/sys/auth/refresh-token", "/auth/oauth2/token"]
|
||
|
||
let refreshTokenPromise: Promise<any> | null = null // 存储刷新token的Promise
|
||
let isRefreshing = false // 是否正在刷新
|
||
let failedQueue: any[] = [] // 队列存储发送失败的请求
|
||
|
||
// 处理队列中的请求
|
||
const processQueue = (error: any, token: any = null) => {
|
||
failedQueue.forEach((prom) => {
|
||
if (error) {
|
||
prom.reject(error)
|
||
} else {
|
||
prom.resolve(token)
|
||
}
|
||
})
|
||
if (!error) {
|
||
failedQueue = []
|
||
}
|
||
}
|
||
|
||
// 创建https请求实例
|
||
const https = new Request({
|
||
baseURL: import.meta.env.VITE_API_URL,
|
||
timeout: 15000,
|
||
method: "GET",
|
||
header: {
|
||
Accept: "*/*",
|
||
"Content-Type": "application/json;charset=UTF-8",
|
||
},
|
||
withCredentials: true,
|
||
custom: {
|
||
showSuccess: false, // 成功提醒 默认使用后端返回值
|
||
successMsg: "", // 成功消息
|
||
showError: true, // 显示失败消息 默认显示
|
||
errorMsg: "", // 失败提醒 默认使用后端返回信息
|
||
loadingMsg: "加载中", // loading提醒文字
|
||
auth: false, // 需要授权才能请求 默认放开
|
||
},
|
||
})
|
||
|
||
// 生产环境协议安全检查
|
||
const validateProductionSecurity = () => {
|
||
if (isDevEnv()) return
|
||
|
||
const apiUrl = import.meta.env.VITE_API_URL
|
||
if (apiUrl && !apiUrl.startsWith("https://")) {
|
||
uni.showToast({ title: "生产环境安全错误: 必须使用 HTTPS 协议", icon: "error" })
|
||
throw new Error("生产环境禁止使用 HTTP 协议")
|
||
}
|
||
}
|
||
|
||
https.interceptors.request.use(async (config) => {
|
||
// 生产环境协议检查
|
||
// validateProductionSecurity()
|
||
|
||
const tokenStorage = uni.getStorageSync(TOKEN_KEY)
|
||
const config_header: any = config.header || {}
|
||
|
||
// 设置租户请求头
|
||
config_header["TENANT-ID"] = "1"
|
||
if (import.meta.env.VITE_IS_TOC === "true") config_header["CLIENT-TOC"] = "Y"
|
||
|
||
// 判断是否需要设置认证头
|
||
if (!SKIP_AUTH_URLS.some((url) => config.url?.startsWith(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
|
||
}
|
||
} else {
|
||
// token不存在,刷新token
|
||
const newTokenStorage = await refreshToken()
|
||
if (newTokenStorage) {
|
||
const tokenValue = getTokenValue(newTokenStorage)
|
||
if (tokenValue) config_header.Authorization = tokenValue
|
||
}
|
||
}
|
||
}
|
||
|
||
return config
|
||
}, Promise.reject)
|
||
|
||
// 响应拦截器
|
||
https.interceptors.response.use(
|
||
async (response) => {
|
||
// 账号密码登录响应结果特殊处理
|
||
if (response.config.url === "/auth/oauth2/token") {
|
||
return Promise.resolve({
|
||
code: 0,
|
||
data: response.data,
|
||
msg: null,
|
||
})
|
||
}
|
||
|
||
return Promise.resolve(response.data)
|
||
},
|
||
async (error) => {
|
||
// 处理 424 错误(token过期)
|
||
if (error.statusCode === 424) {
|
||
if (isDevEnv()) {
|
||
// 开发环境直接清除token并跳转登录
|
||
uni.removeStorageSync(TOKEN_KEY)
|
||
uni.removeStorageSync(USER_INFO_KEY)
|
||
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" })
|
||
router.jump("/pages/user/login")
|
||
} else {
|
||
try {
|
||
const newTokenStorage = await refreshToken()
|
||
if (newTokenStorage) {
|
||
const config = error.config
|
||
const tokenValue = getTokenValue(newTokenStorage)
|
||
if (tokenValue && config.header) config.header.Authorization = tokenValue
|
||
return https.request(config)
|
||
}
|
||
} catch (err) {
|
||
return Promise.reject(err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 显示错误信息
|
||
if (error?.data?.msg) {
|
||
uni.showToast({ title: error.data.msg, icon: "none", mask: true })
|
||
}
|
||
return Promise.resolve(error.data)
|
||
},
|
||
)
|
||
|
||
// 从tokenStorage中读取tokenValue值
|
||
const getTokenValue = (tokenStorage: any): string | null => {
|
||
if (isDevEnv()) {
|
||
return tokenStorage?.access_token ? `Bearer ${tokenStorage.access_token}` : null
|
||
}
|
||
return tokenStorage?.accessToken?.tokenValue ? `Bearer ${tokenStorage.accessToken.tokenValue}` : null
|
||
}
|
||
|
||
// 检查tokenStorage中tokenValue是否过期
|
||
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
|
||
}
|
||
|
||
async function refreshToken(): Promise<any> {
|
||
if (refreshTokenPromise) return refreshTokenPromise
|
||
|
||
if (!isRefreshing) {
|
||
isRefreshing = true
|
||
refreshTokenPromise = (async () => {
|
||
try {
|
||
const loginResult = await autoLogin()
|
||
if (loginResult) {
|
||
const newTokenStorage = uni.getStorageSync(TOKEN_KEY)
|
||
processQueue(null, newTokenStorage)
|
||
return newTokenStorage
|
||
} else {
|
||
// 检查是否需要设备验证
|
||
const tokenStorage = uni.getStorageSync(TOKEN_KEY)
|
||
if (tokenStorage?.data == -1) {
|
||
// 显示设备验证弹窗
|
||
await showVerifyDeviceImpModel(async () => {
|
||
// 验证完成后重新登录
|
||
const newLoginResult = await autoLogin()
|
||
if (newLoginResult) {
|
||
const newTokenStorage = uni.getStorageSync(TOKEN_KEY)
|
||
processQueue(null, newTokenStorage)
|
||
return newTokenStorage
|
||
} else {
|
||
processQueue(new Error("登录失败"), null)
|
||
throw new Error("登录失败")
|
||
}
|
||
})
|
||
} else {
|
||
processQueue(new Error("登录失败"), null)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
processQueue(error, null)
|
||
throw error
|
||
} finally {
|
||
isRefreshing = false
|
||
refreshTokenPromise = null
|
||
}
|
||
})()
|
||
}
|
||
|
||
return refreshTokenPromise
|
||
}
|
||
|
||
// 根据应用环境自动登录 返回登录结果
|
||
const autoLogin = async (): Promise<boolean> => {
|
||
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
|
||
}
|
||
|
||
function isZzdNeedSmsVerify(data: unknown): data is { needSmsVerify: true; bindSessionId: string; displayName?: string } {
|
||
if (!data || typeof data !== "object") return false
|
||
const o = data as Record<string, unknown>
|
||
const need =
|
||
o.needSmsVerify === true || o.needSmsVerify === "true" || o.needSmsVerify === 1 || o.needSmsVerify === "1"
|
||
const sid = o.bindSessionId
|
||
const sidStr = typeof sid === "string" ? sid : sid != null ? String(sid) : ""
|
||
return need && sidStr.length > 0
|
||
}
|
||
|
||
function goZzdSmsVerify(data: { bindSessionId: string; displayName?: string }) {
|
||
const sid = typeof data.bindSessionId === "string" ? data.bindSessionId : String(data.bindSessionId || "")
|
||
const name = typeof data.displayName === "string" ? data.displayName : ""
|
||
const q = `bindSessionId=${encodeURIComponent(sid)}&displayName=${encodeURIComponent(name)}`
|
||
uni.reLaunch({ url: `/pages/user/zzdSmsVerify?${q}` })
|
||
}
|
||
|
||
/**
|
||
* 浙政钉专用:校验本地 token → 无效则 getAuthCode→zzdsso → 需绑手机则去短信页 → 否则换 token。
|
||
* @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 (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)) {
|
||
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)
|
||
return "ok"
|
||
}
|
||
if (res?.msg) {
|
||
uni.showToast({ title: res.msg, icon: "none", duration: 4000 })
|
||
}
|
||
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 })
|
||
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)
|
||
return "ok"
|
||
} else {
|
||
uni.removeStorageSync(TOKEN_KEY)
|
||
uni.removeStorageSync(USER_INFO_KEY)
|
||
if (res.msg == "token已失效") {
|
||
uni.showToast({ title: "用户凭证已失效,请退出并重新进入智慧后勤应用", icon: "none", duration: 6000 })
|
||
}
|
||
return "failed"
|
||
}
|
||
} catch (error: any) {
|
||
uni.showToast({ title: error, icon: "none" })
|
||
return "failed"
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
}
|
||
|
||
async function loginByZzd(): Promise<boolean> {
|
||
const r = await zzdLoginCore()
|
||
return r === "ok"
|
||
}
|
||
|
||
/**
|
||
* 从门户/常用入口进入 H5 时主动跑浙政钉单点。非浙政钉环境立即返回 ok,不调用 zzdsso、不碰诗画逻辑。
|
||
*/
|
||
export async function tryZzdEntryLogin(): Promise<"ok" | "sms" | "failed"> {
|
||
const appInfo = await getAppInfo()
|
||
if (appInfo !== APPENV.ZZD) return "ok"
|
||
return await zzdLoginCore()
|
||
}
|
||
|
||
// 诗画登录
|
||
async function loginBySh(): Promise<boolean> {
|
||
try {
|
||
uni.showLoading({ title: "登录中...", mask: 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 })
|
||
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) {
|
||
uni.showToast({ title: "登录错误", icon: "none" })
|
||
return false
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
}
|
||
|
||
// 请求函数
|
||
const request = (config: any) => {
|
||
return https.middleware(config)
|
||
}
|
||
|
||
export default request
|