Files
pj_h5_cloud/src/sheep/utils/request.ts
T
2026-05-09 15:03:18 +08:00

376 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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