213 lines
8.9 KiB
JavaScript
213 lines
8.9 KiB
JavaScript
import { refresh } from './auth.js'
|
|
import { session, replaceTokens, signOut, getSessionRevision } from '../stores/session.js'
|
|
|
|
let refreshPromise = null
|
|
let refreshRevision = -1
|
|
|
|
function requireCurrentSession(revision) {
|
|
if (revision !== getSessionRevision()) throw new Error('登录会话已变更,请重新操作')
|
|
}
|
|
|
|
function expireSession(revision) {
|
|
if (revision !== getSessionRevision()) return
|
|
signOut()
|
|
uni.reLaunch({ url: '/pages/login/login' })
|
|
}
|
|
|
|
function errorFrom(response, fallback) {
|
|
const body = response?.data || {}
|
|
const error = new Error(body.message || fallback)
|
|
error.code = body.code || response?.statusCode
|
|
error.requestId = body.requestId || response?.header?.['x-request-id'] || response?.header?.['X-Request-Id']
|
|
error.traceId = body.traceId || response?.header?.['x-trace-id'] || response?.header?.['X-Trace-Id']
|
|
return error
|
|
}
|
|
|
|
function unauthorized(response) {
|
|
const code = String(response?.data?.code || response?.statusCode || '')
|
|
return response?.statusCode === 401 || code === 'A0401'
|
|
}
|
|
|
|
function successful(response, body) {
|
|
return response?.statusCode === 200 && ['00000', '200'].includes(String(body?.code))
|
|
}
|
|
|
|
async function refreshOverseasToken(revision) {
|
|
requireCurrentSession(revision)
|
|
if (!session.refreshToken) {
|
|
expireSession(revision)
|
|
throw new Error('登录状态已过期,请重新登录')
|
|
}
|
|
if (!refreshPromise || refreshRevision !== revision) {
|
|
refreshRevision = revision
|
|
const pending = refresh(session.refreshToken)
|
|
.then((tokens) => { requireCurrentSession(revision); replaceTokens(tokens); return session.token })
|
|
.catch((error) => { expireSession(revision); throw error })
|
|
.finally(() => { if (refreshPromise === pending) refreshPromise = null })
|
|
refreshPromise = pending
|
|
}
|
|
return refreshPromise
|
|
}
|
|
|
|
async function tokenAfterUnauthorized(failedToken, revision) {
|
|
requireCurrentSession(revision)
|
|
if (session.token && session.token !== failedToken) return session.token
|
|
return refreshOverseasToken(revision)
|
|
}
|
|
|
|
function overseasUrl(path) {
|
|
const base = String(session.apiBase || '').replace(/\/$/, '')
|
|
const nativeRuntime = typeof plus !== 'undefined'
|
|
|| (typeof location !== 'undefined' && location.protocol === 'file:')
|
|
if (nativeRuntime && !/^https?:\/\//i.test(base)) {
|
|
const error = new Error('海外服务地址未配置:App 必须设置绝对 HTTP(S) 地址 VITE_API_BASE_URL')
|
|
error.code = 'PDA_API_BASE_REQUIRED'
|
|
throw error
|
|
}
|
|
const value = `/${String(path || '').replace(/^\/+/, '')}`
|
|
const normalized = /\/(?:api|gateway-api)$/.test(base)
|
|
? value.replace(/^\/api(?=\/)/, '')
|
|
: value
|
|
return `${base}${normalized}`
|
|
}
|
|
|
|
function rawRequest(method, path, data, token) {
|
|
return new Promise((resolve, reject) => {
|
|
uni.request({
|
|
url: overseasUrl(path),
|
|
method,
|
|
data,
|
|
header: { Accept: 'application/json', Authorization: `Bearer ${token}` },
|
|
success: resolve,
|
|
fail: (cause) => reject(Object.assign(new Error(cause.errMsg || '海外服务网络连接失败'), { network: true }))
|
|
})
|
|
})
|
|
}
|
|
|
|
export async function overseasRequest(method, path, data, retry = true) {
|
|
const revision = getSessionRevision()
|
|
console.info('[Overseas PDA API] request', { method, path })
|
|
const attemptedToken = session.token
|
|
let response = await rawRequest(method, path, data, attemptedToken)
|
|
requireCurrentSession(revision)
|
|
if (retry && unauthorized(response)) {
|
|
const replacementToken = await tokenAfterUnauthorized(attemptedToken, revision)
|
|
response = await rawRequest(method, path, data, replacementToken)
|
|
}
|
|
requireCurrentSession(revision)
|
|
if (unauthorized(response)) expireSession(revision)
|
|
const body = response?.data || {}
|
|
console.info('[Overseas PDA API] response', { method, path, statusCode: response?.statusCode, code: body?.code,
|
|
message: body?.message, requestId: body?.requestId || response?.header?.['x-request-id'] || response?.header?.['X-Request-Id'],
|
|
traceId: body?.traceId || response?.header?.['x-trace-id'] || response?.header?.['X-Trace-Id'] })
|
|
if (!successful(response, body)) throw errorFrom(response, '海外作业请求失败')
|
|
return body.data
|
|
}
|
|
|
|
function rawUpload(path, filePath, name, token) {
|
|
const browserFile = typeof Blob !== 'undefined' && filePath instanceof Blob
|
|
if (!browserFile && !String(filePath || '').trim()) {
|
|
const error = new Error('未获取到可上传的照片路径,请重新拍摄')
|
|
error.code = 'PDA_UPLOAD_FILE_PATH_REQUIRED'
|
|
return Promise.reject(error)
|
|
}
|
|
let url
|
|
try { url = overseasUrl(path) } catch (error) { return Promise.reject(error) }
|
|
const h5Runtime = typeof window !== 'undefined' && typeof plus === 'undefined'
|
|
if (h5Runtime && browserFile) return new Promise((resolve, reject) => {
|
|
uni.uploadFile({
|
|
url, files: [{ name, file: filePath }],
|
|
header: { Authorization: `Bearer ${token}` },
|
|
success: resolve,
|
|
fail: (cause) => reject(Object.assign(new Error(cause.errMsg || '海外作业文件上传失败'), { network: true, uploadUrl: url }))
|
|
})
|
|
})
|
|
if (h5Runtime && /^blob:/i.test(filePath)) {
|
|
const selectedFile = fetch(filePath).then((source) => {
|
|
if (!source.ok) throw new Error('无法读取已选择的照片')
|
|
return source.blob()
|
|
})
|
|
return selectedFile.then(async (blob) => {
|
|
const form = new FormData()
|
|
const suffix = String(blob.type || '').split('/')[1] || 'jpg'
|
|
form.append(name, blob, blob.name || `pda-photo.${suffix}`)
|
|
if (typeof XMLHttpRequest !== 'undefined') return new Promise((resolve, reject) => {
|
|
const request = new XMLHttpRequest()
|
|
request.open('POST', url)
|
|
request.setRequestHeader('Authorization', `Bearer ${token}`)
|
|
request.onload = () => resolve({ statusCode: request.status, data: request.responseText, header: {} })
|
|
request.onerror = () => reject(new Error('海外作业文件上传失败'))
|
|
request.send(form)
|
|
})
|
|
const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form })
|
|
return { statusCode: response.status, data: await response.text(), header: Object.fromEntries(response.headers.entries()) }
|
|
})
|
|
.catch((cause) => { throw Object.assign(new Error(cause.message || '海外作业文件上传失败'), { network: true, uploadUrl: url }) })
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
uni.uploadFile({
|
|
url,
|
|
filePath,
|
|
name,
|
|
header: { Authorization: `Bearer ${token}` },
|
|
success: resolve,
|
|
fail: (cause) => reject(Object.assign(new Error(cause.errMsg || '海外作业文件上传失败'), { network: true, uploadUrl: url }))
|
|
})
|
|
})
|
|
}
|
|
|
|
function rawDownload(path, token) {
|
|
return new Promise((resolve, reject) => {
|
|
uni.downloadFile({
|
|
url: overseasUrl(path),
|
|
header: { Authorization: `Bearer ${token}` },
|
|
success: resolve,
|
|
fail: (cause) => reject(Object.assign(new Error(cause.errMsg || '签收照片下载失败'), { network: true }))
|
|
})
|
|
})
|
|
}
|
|
|
|
export async function overseasUpload(path, filePath, name = 'file', retry = true) {
|
|
const revision = getSessionRevision()
|
|
const attemptedToken = session.token
|
|
let response = await rawUpload(path, filePath, name, attemptedToken)
|
|
requireCurrentSession(revision)
|
|
let body = null
|
|
try { body = JSON.parse(response.data || '{}') } catch (_) { /* handled below */ }
|
|
if (retry && unauthorized({ ...response, data: body })) {
|
|
const replacementToken = await tokenAfterUnauthorized(attemptedToken, revision)
|
|
response = await rawUpload(path, filePath, name, replacementToken)
|
|
try { body = JSON.parse(response.data || '{}') } catch (_) { body = null }
|
|
}
|
|
requireCurrentSession(revision)
|
|
if (unauthorized({ ...response, data: body })) expireSession(revision)
|
|
if (!successful(response, body)) throw errorFrom({ ...response, data: body }, '海外作业文件上传失败')
|
|
return body.data
|
|
}
|
|
|
|
export async function overseasDownload(path, options = {}) {
|
|
const revision = getSessionRevision()
|
|
const { retry = true, expectedContentTypePrefix = '' } = options
|
|
const attemptedToken = session.token
|
|
let response = await rawDownload(path, attemptedToken)
|
|
requireCurrentSession(revision)
|
|
if (retry && unauthorized(response)) {
|
|
const replacementToken = await tokenAfterUnauthorized(attemptedToken, revision)
|
|
response = await rawDownload(path, replacementToken)
|
|
}
|
|
requireCurrentSession(revision)
|
|
if (unauthorized(response)) expireSession(revision)
|
|
if (response?.statusCode !== 200 || !response?.tempFilePath) {
|
|
throw errorFrom({ ...response, data: {} }, '签收照片下载失败')
|
|
}
|
|
const contentType = String(response?.header?.['content-type'] || response?.header?.['Content-Type'] || '').toLowerCase()
|
|
// App-Plus downloadFile may omit response headers even when the image body was downloaded successfully.
|
|
// Reject an explicit non-image response, but allow a valid temp file when Content-Type is unavailable.
|
|
if (expectedContentTypePrefix && contentType && !contentType.startsWith(expectedContentTypePrefix.toLowerCase())) {
|
|
throw errorFrom({ ...response, data: {} }, '照片服务返回了无效内容,请稍后重试')
|
|
}
|
|
return response.tempFilePath
|
|
}
|
|
|
|
export function resetOverseasRequestStateForTest() { refreshPromise = null }
|