Commit d3d4ac9d by 王立鹏

Merge branch '线上优化/修改分片密钥' into 'master'

线上优化/修改分片密钥

See merge request !42
parents 89c67f28 f63235cf
import axios, { type AxiosRequestConfig } from 'axios'
import CryptoJS from 'crypto-js'
import { useUserStore } from '@/stores'
import service from '@/utils/request'
import type { UploadFileResponseItem, ChunkCheckResponse } from './types'
// 'https://oa.yswg.com.cn:8085'
......@@ -17,7 +18,14 @@ export const uploadBaseUrlPool = [
]
// 获取到nas上传地址
export const resolveUploadBaseUrl = () => {
// 测试环境 NAS 上传地址(本地 / test 打包使用)
const TEST_UPLOAD_BASE_URL = 'http://47.119.149.50:3450'
export const resolveUploadBaseUrl = (isProd = import.meta.env.MODE === 'production') => {
// 本地 和 测试环境
if (!isProd) return TEST_UPLOAD_BASE_URL
// 正式
const random = Math.random() * 100
if (random < 40) return uploadBaseUrlPool[0]!.url
if (random < 60) return uploadBaseUrlPool[1]!.url
......@@ -28,9 +36,6 @@ const OA_UPLOAD_COMMON_BASE_URL = 'http://47.112.96.71:8082'
const CHUNK_UPLOAD_THRESHOLD = 1 * 1024 * 1024
const CHUNK_SIZE = 5 * 1024 * 1024
const MAX_CONCURRENT_UPLOADS = 3
const CHUNK_API_AUTH_SECRET = '4821-7395-1642-8053-2971-5604-9182-4307'
// nas 鉴权接口的超时时间 nas 挂掉时浏览器默认要等 20s 以上 这里主动掐断
const NAS_AUTH_TIMEOUT = 8000
// nas 连不上时统一对外抛这个文案 避免把 ERR_CONNECTION_REFUSED 这种原始错误甩给用户
export const UPLOAD_SERVICE_UNAVAILABLE_MSG = '视频上传服务暂时不可用,请稍后重试'
......@@ -127,6 +132,29 @@ function createChunks(file: File, chunkSize: number) {
return chunks
}
/**
* nas 上传专用的 axios 实例
* 请求:每次随机选一个 nas 节点作为 baseURL 并带上 nas token
* 响应:401(token 过期/无效)时强制换新 token 后重试一次
*/
const nasUploadService = axios.create({})
nasUploadService.interceptors.request.use(async (config) => {
config.baseURL = resolveUploadBaseUrl()
config.headers.set('authorization', await ensureChunkApiAuthToken())
return config
})
nasUploadService.interceptors.response.use(undefined, async (error) => {
const config = error.config as (AxiosRequestConfig & { _retried?: boolean }) | undefined
if (axios.isAxiosError(error) && error.response?.status === 401 && config && !config._retried) {
config._retried = true
await refreshChunkApiAuthToken()
return nasUploadService.request(config)
}
return Promise.reject(error)
})
// 分片上传文件
/**
* 第 1 步:给文件生成一个 hash
......@@ -144,22 +172,23 @@ async function uploadFileByChunks(
},
signal: AbortSignal,
): Promise<UploadFileResponseItem> {
const authToken = await ensureChunkApiAuthToken()
// 先确保有 token 没有的话在计算文件 hash 之前就失败 不用白算
await ensureChunkApiAuthToken()
const { onProgress } = options
const fileHash = await fileHashHeadTailMiddle(file)
const fileSuffix = file.name.includes('.') ? file.name.slice(file.name.lastIndexOf('.')) : ''
// 上传到服务端用的文件名 带上 hash 防止重名覆盖
const uploadFileName = buildUniqueFileName(file.name, fileHash)
const checkResponse = await axios.post<{ data: ChunkCheckResponse }>(
`${resolveUploadBaseUrl()}/mobiles/file-upload/check`,
const checkResponse = await nasUploadService.post<{ data: ChunkCheckResponse }>(
'/mobiles/file-upload/check',
{
hash: fileHash,
fileName: uploadFileName,
fileSize: file.size,
fileSuffx: fileSuffix,
},
{ signal, headers: { authorization: authToken } },
{ signal },
)
// 秒传服务检查文件是否存在
......@@ -201,12 +230,9 @@ async function uploadFileByChunks(
formData.append('filePart', chunk, file.name)
formData.append('chunkNumber', String(index))
await axios.post(`${resolveUploadBaseUrl()}/mobiles/file-upload/chunk`, formData, {
await nasUploadService.post('/mobiles/file-upload/chunk', formData, {
signal,
headers: {
'Content-Type': 'multipart/form-data',
authorization: authToken,
},
headers: { 'Content-Type': 'multipart/form-data' },
})
uploadedSet.add(index)
......@@ -240,15 +266,12 @@ async function uploadFileByChunks(
finishFormData.set('chunkTotalCount', String(totalChunks))
// 上传完成 通知后端文件上传完成
const finishResponse = await axios.post<{ data: { fileUrl: string } }>(
`${resolveUploadBaseUrl()}/mobiles/file-upload/finish`,
const finishResponse = await nasUploadService.post<{ data: { fileUrl: string } }>(
'/mobiles/file-upload/finish',
finishFormData,
{
signal,
headers: {
'Content-Type': 'multipart/form-data',
authorization: authToken,
},
headers: { 'Content-Type': 'multipart/form-data' },
},
)
......@@ -309,29 +332,33 @@ export const uploadFile = (
}
}
export const getTimestamp = async () => {
const {
data: { data: timestamp },
} = await axios.get(`${resolveUploadBaseUrl()}/api/auth/getTime`, { timeout: NAS_AUTH_TIMEOUT })
return timestamp
/**
* 获取 nas 上传接口的 token(由 OA 后端代换,前端不再持有密钥)
* @param forceRefresh 上传服务返回 401/Token无效 时传 true,强制换新 token
*/
export const getChunkApiAuthToken = async (forceRefresh = false) => {
const res = await service.request<{ token: string }>({
url: '/nasUpload/getToken',
method: 'POST',
data: { forceRefresh },
})
return res.data.token
}
export const getChunkApiAuthToken = async (weChatId: string) => {
const timestamp = await getTimestamp()
const rawStr = CHUNK_API_AUTH_SECRET + timestamp
const {
data: { data },
} = await axios.post(
`${resolveUploadBaseUrl()}/api/auth/getToken`,
{
timestamp,
weChatId,
secret: CryptoJS.MD5(rawStr).toString(),
},
{ timeout: NAS_AUTH_TIMEOUT },
)
return data.token as string
// 多个分片并发遇到 401 时 共用同一次刷新
let refreshTokenPromise: Promise<string> | null = null
const refreshChunkApiAuthToken = () => {
if (!refreshTokenPromise) {
refreshTokenPromise = getChunkApiAuthToken(true)
.then((token) => {
useUserStore().setChunkApiAuthToken(token)
return token
})
.finally(() => {
refreshTokenPromise = null
})
}
return refreshTokenPromise
}
/**
......@@ -343,11 +370,8 @@ const ensureChunkApiAuthToken = async () => {
const userStore = useUserStore()
if (userStore.chunkApiAuthToken) return userStore.chunkApiAuthToken
const account = userStore.userInfo?.account
if (!account) throw new Error(UPLOAD_SERVICE_UNAVAILABLE_MSG)
try {
const token = await getChunkApiAuthToken(account)
const token = await getChunkApiAuthToken()
userStore.setChunkApiAuthToken(token)
return token
} catch (error) {
......
......@@ -49,7 +49,7 @@ export const useUserStore = defineStore('user', () => {
// 所以不 await 直接放后台跑 让首页尽快出来 nas 慢或者挂了也不会拖住登录
// 这里的 catch 不能省 不然 nas 挂掉时会变成未处理的 promise rejection
// 万一没拿到 等真正要分片上传的时候会再补一次(见 ensureChunkApiAuthToken)
getChunkApiAuthToken(data.account)
getChunkApiAuthToken()
.then(setChunkApiAuthToken)
.catch((error) => {
console.error('获取分片上传 token 失败 不影响登录', error)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment