Commit 75af0216 by lijiabin

fix: 分片上传服务不可用时给出友好提示

- 新增 ensureChunkApiAuthToken 登录时没拿到 token 的话上传前补一次
- nas 鉴权接口加 8s 超时 避免挂掉时干等 20s 以上
- 新增 resolveUploadErrorMessage 统一收敛错误文案
  不再把 Network Error 这类原始报错直接弹给用户

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent ad6b51e0
import axios, { type AxiosRequestConfig } from 'axios' import axios, { type AxiosRequestConfig } from 'axios'
import CryptoJS from 'crypto-js' import CryptoJS from 'crypto-js'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import type { UploadFileResponseItem, ChunkCheckResponse } from './types' import type { UploadFileResponseItem, ChunkCheckResponse } from './types'
// 'https://oa.yswg.com.cn:8085' // 'https://oa.yswg.com.cn:8085'
...@@ -12,24 +11,40 @@ import type { UploadFileResponseItem, ChunkCheckResponse } from './types' ...@@ -12,24 +11,40 @@ import type { UploadFileResponseItem, ChunkCheckResponse } from './types'
// 20% nas2 // 20% nas2
// 40% nas3 // 40% nas3
export const uploadBaseUrlPool = [ export const uploadBaseUrlPool = [
{ url: "https://nas1.yswg.top:8088", weight: 40 }, { url: 'https://nas1.yswg.top:8088', weight: 40 },
{ url: "https://nas2.yswg.top:8088", weight: 20 }, { url: 'https://nas2.yswg.top:8088', weight: 20 },
{ url: "https://nas3.yswg.top:8088", weight: 40 }, { url: 'https://nas3.yswg.top:8088', weight: 40 },
]; ]
// 获取到nas上传地址 // 获取到nas上传地址
export const resolveUploadBaseUrl = () => { export const resolveUploadBaseUrl = () => {
const random = Math.random() * 100; const random = Math.random() * 100
if (random < 40) return uploadBaseUrlPool[0]!.url; if (random < 40) return uploadBaseUrlPool[0]!.url
if (random < 60) return uploadBaseUrlPool[1]!.url; if (random < 60) return uploadBaseUrlPool[1]!.url
return uploadBaseUrlPool[2]!.url; return uploadBaseUrlPool[2]!.url
}; }
const OA_UPLOAD_COMMON_BASE_URL = 'http://47.112.96.71:8082' const OA_UPLOAD_COMMON_BASE_URL = 'http://47.112.96.71:8082'
const CHUNK_UPLOAD_THRESHOLD = 1 * 1024 * 1024 const CHUNK_UPLOAD_THRESHOLD = 1 * 1024 * 1024
const CHUNK_SIZE = 5 * 1024 * 1024 const CHUNK_SIZE = 5 * 1024 * 1024
const MAX_CONCURRENT_UPLOADS = 3 const MAX_CONCURRENT_UPLOADS = 3
const CHUNK_API_AUTH_SECRET = '4821-7395-1642-8053-2971-5604-9182-4307' 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 = '视频上传服务暂时不可用,请稍后重试'
/**
* 把上传异常转成能给用户看的文案
* 只有我们自己标记过的服务不可用才原样展示
* 其它像 Network Error、Request failed with status code 500 这种原始报错一律兜底成通用文案
*/
export const resolveUploadErrorMessage = (error: unknown) => {
if (error instanceof Error && error.message === UPLOAD_SERVICE_UNAVAILABLE_MSG) {
return error.message
}
return '上传失败,请重试'
}
function hashBufferMD5(buffer: ArrayBuffer): string { function hashBufferMD5(buffer: ArrayBuffer): string {
const wordArray = CryptoJS.lib.WordArray.create(new Uint8Array(buffer)) const wordArray = CryptoJS.lib.WordArray.create(new Uint8Array(buffer))
...@@ -129,8 +144,7 @@ async function uploadFileByChunks( ...@@ -129,8 +144,7 @@ async function uploadFileByChunks(
}, },
signal: AbortSignal, signal: AbortSignal,
): Promise<UploadFileResponseItem> { ): Promise<UploadFileResponseItem> {
const userStore = useUserStore() const authToken = await ensureChunkApiAuthToken()
const { chunkApiAuthToken } = storeToRefs(userStore)
const { onProgress } = options const { onProgress } = options
const fileHash = await fileHashHeadTailMiddle(file) const fileHash = await fileHashHeadTailMiddle(file)
const fileSuffix = file.name.includes('.') ? file.name.slice(file.name.lastIndexOf('.')) : '' const fileSuffix = file.name.includes('.') ? file.name.slice(file.name.lastIndexOf('.')) : ''
...@@ -145,7 +159,7 @@ async function uploadFileByChunks( ...@@ -145,7 +159,7 @@ async function uploadFileByChunks(
fileSize: file.size, fileSize: file.size,
fileSuffx: fileSuffix, fileSuffx: fileSuffix,
}, },
{ signal, headers: { authorization: chunkApiAuthToken.value } }, { signal, headers: { authorization: authToken } },
) )
// 秒传服务检查文件是否存在 // 秒传服务检查文件是否存在
...@@ -191,7 +205,7 @@ async function uploadFileByChunks( ...@@ -191,7 +205,7 @@ async function uploadFileByChunks(
signal, signal,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
authorization: chunkApiAuthToken.value, authorization: authToken,
}, },
}) })
...@@ -233,7 +247,7 @@ async function uploadFileByChunks( ...@@ -233,7 +247,7 @@ async function uploadFileByChunks(
signal, signal,
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
authorization: chunkApiAuthToken.value, authorization: authToken,
}, },
}, },
) )
...@@ -298,7 +312,7 @@ export const uploadFile = ( ...@@ -298,7 +312,7 @@ export const uploadFile = (
export const getTimestamp = async () => { export const getTimestamp = async () => {
const { const {
data: { data: timestamp }, data: { data: timestamp },
} = await axios.get(`${resolveUploadBaseUrl()}/api/auth/getTime`) } = await axios.get(`${resolveUploadBaseUrl()}/api/auth/getTime`, { timeout: NAS_AUTH_TIMEOUT })
return timestamp return timestamp
} }
...@@ -308,10 +322,36 @@ export const getChunkApiAuthToken = async (weChatId: string) => { ...@@ -308,10 +322,36 @@ export const getChunkApiAuthToken = async (weChatId: string) => {
const rawStr = CHUNK_API_AUTH_SECRET + timestamp const rawStr = CHUNK_API_AUTH_SECRET + timestamp
const { const {
data: { data }, data: { data },
} = await axios.post(`${resolveUploadBaseUrl()}/api/auth/getToken`, { } = await axios.post(
`${resolveUploadBaseUrl()}/api/auth/getToken`,
{
timestamp, timestamp,
weChatId, weChatId,
secret: CryptoJS.MD5(rawStr).toString(), secret: CryptoJS.MD5(rawStr).toString(),
}) },
{ timeout: NAS_AUTH_TIMEOUT },
)
return data.token as string return data.token as string
} }
/**
* 分片上传前确保拿到 nas 的鉴权 token
* 登录时拿不到 token 不会阻断登录(nas 挂了不该让人进不去系统)
* 所以真正要用的时候在这里补一次 补不到就抛友好文案 让上层弹提示
*/
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)
userStore.setChunkApiAuthToken(token)
return token
} catch (error) {
console.error('获取分片上传 token 失败', error)
throw new Error(UPLOAD_SERVICE_UNAVAILABLE_MSG)
}
}
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<script lang="ts" setup generic="T extends string | string[]"> <script lang="ts" setup generic="T extends string | string[]">
import { Plus } from '@element-plus/icons-vue' import { Plus } from '@element-plus/icons-vue'
import { uploadFile as uploadFileApi } from '@/api' import { uploadFile as uploadFileApi, resolveUploadErrorMessage } from '@/api'
import type { UploadProps, UploadUserFile } from 'element-plus' import type { UploadProps, UploadUserFile } from 'element-plus'
import type { UploadFileProps } from './types' import type { UploadFileProps } from './types'
import { push } from 'notivue' import { push } from 'notivue'
...@@ -156,7 +156,7 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => ...@@ -156,7 +156,7 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) =>
} }
} catch (error) { } catch (error) {
console.error('上传失败:', error) console.error('上传失败:', error)
push.error('上传失败,请重试') push.error(resolveUploadErrorMessage(error))
const fileIndex = fileList.value.findIndex((file) => file.uid === uid) const fileIndex = fileList.value.findIndex((file) => file.uid === uid)
if (fileIndex !== -1) { if (fileIndex !== -1) {
......
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { uploadFile as uploadFileApi } from '@/api/common' import { uploadFile as uploadFileApi, resolveUploadErrorMessage } from '@/api/common'
import type { UploadFile } from 'element-plus' import type { UploadFile } from 'element-plus'
import type { UploadVideoProps } from './types' import type { UploadVideoProps } from './types'
import { getVideoMetadata } from '@/utils' import { getVideoMetadata } from '@/utils'
...@@ -275,7 +275,8 @@ const startUpload = async () => { ...@@ -275,7 +275,8 @@ const startUpload = async () => {
push.success('视频上传成功!') push.success('视频上传成功!')
} catch (error) { } catch (error) {
uploading.value = false uploading.value = false
uploadError.value = error instanceof Error ? error.message : '上传失败,请重试' console.error('视频上传失败', error)
uploadError.value = resolveUploadErrorMessage(error)
push.error(uploadError.value) push.error(uploadError.value)
} finally { } finally {
if (localVideoUrl) { if (localVideoUrl) {
......
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