Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
corporate-culture-qd
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
王立鹏
corporate-culture-qd
Commits
392ffd3a
You need to sign in or sign up before continuing.
Commit
392ffd3a
authored
Sep 17, 2026
by
王立鹏
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '线上优化/分片上传相关优化' into 'master'
线上优化/分片上传相关优化 See merge request
!38
parents
534f21d2
75af0216
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
89 additions
and
26 deletions
+89
-26
index.ts
src/api/common/index.ts
+58
-18
index.vue
src/components/common/UploadFile/index.vue
+2
-2
index.vue
src/components/common/UploadVideo/index.vue
+3
-2
main.ts
src/main.ts
+9
-2
guards.ts
src/router/guards.ts
+8
-0
user.ts
src/stores/user.ts
+9
-2
No files found.
src/api/common/index.ts
View file @
392ffd3a
import
axios
,
{
type
AxiosRequestConfig
}
from
'axios'
import
CryptoJS
from
'crypto-js'
import
{
useUserStore
}
from
'@/stores'
import
{
storeToRefs
}
from
'pinia'
import
type
{
UploadFileResponseItem
,
ChunkCheckResponse
}
from
'./types'
// 'https://oa.yswg.com.cn:8085'
...
...
@@ -12,24 +11,40 @@ import type { UploadFileResponseItem, ChunkCheckResponse } from './types'
// 20% nas2
// 40% nas3
export
const
uploadBaseUrlPool
=
[
{
url
:
"https://nas1.yswg.top:8088"
,
weight
:
40
},
{
url
:
"https://nas2.yswg.top:8088"
,
weight
:
20
},
{
url
:
"https://nas3.yswg.top:8088"
,
weight
:
40
},
]
;
{
url
:
'https://nas1.yswg.top:8088'
,
weight
:
40
},
{
url
:
'https://nas2.yswg.top:8088'
,
weight
:
20
},
{
url
:
'https://nas3.yswg.top:8088'
,
weight
:
40
},
]
// 获取到nas上传地址
export
const
resolveUploadBaseUrl
=
()
=>
{
const
random
=
Math
.
random
()
*
100
;
if
(
random
<
40
)
return
uploadBaseUrlPool
[
0
]
!
.
url
;
if
(
random
<
60
)
return
uploadBaseUrlPool
[
1
]
!
.
url
;
return
uploadBaseUrlPool
[
2
]
!
.
url
;
}
;
const
random
=
Math
.
random
()
*
100
if
(
random
<
40
)
return
uploadBaseUrlPool
[
0
]
!
.
url
if
(
random
<
60
)
return
uploadBaseUrlPool
[
1
]
!
.
url
return
uploadBaseUrlPool
[
2
]
!
.
url
}
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
=
'视频上传服务暂时不可用,请稍后重试'
/**
* 把上传异常转成能给用户看的文案
* 只有我们自己标记过的服务不可用才原样展示
* 其它像 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
{
const
wordArray
=
CryptoJS
.
lib
.
WordArray
.
create
(
new
Uint8Array
(
buffer
))
...
...
@@ -129,8 +144,7 @@ async function uploadFileByChunks(
},
signal
:
AbortSignal
,
):
Promise
<
UploadFileResponseItem
>
{
const
userStore
=
useUserStore
()
const
{
chunkApiAuthToken
}
=
storeToRefs
(
userStore
)
const
authToken
=
await
ensureChunkApiAuthToken
()
const
{
onProgress
}
=
options
const
fileHash
=
await
fileHashHeadTailMiddle
(
file
)
const
fileSuffix
=
file
.
name
.
includes
(
'.'
)
?
file
.
name
.
slice
(
file
.
name
.
lastIndexOf
(
'.'
))
:
''
...
...
@@ -145,7 +159,7 @@ async function uploadFileByChunks(
fileSize
:
file
.
size
,
fileSuffx
:
fileSuffix
,
},
{
signal
,
headers
:
{
authorization
:
chunkApiAuthToken
.
value
}
},
{
signal
,
headers
:
{
authorization
:
authToken
}
},
)
// 秒传服务检查文件是否存在
...
...
@@ -191,7 +205,7 @@ async function uploadFileByChunks(
signal
,
headers
:
{
'Content-Type'
:
'multipart/form-data'
,
authorization
:
chunkApiAuthToken
.
value
,
authorization
:
authToken
,
},
})
...
...
@@ -233,7 +247,7 @@ async function uploadFileByChunks(
signal
,
headers
:
{
'Content-Type'
:
'multipart/form-data'
,
authorization
:
chunkApiAuthToken
.
value
,
authorization
:
authToken
,
},
},
)
...
...
@@ -298,7 +312,7 @@ export const uploadFile = (
export
const
getTimestamp
=
async
()
=>
{
const
{
data
:
{
data
:
timestamp
},
}
=
await
axios
.
get
(
`
${
resolveUploadBaseUrl
()}
/api/auth/getTime`
)
}
=
await
axios
.
get
(
`
${
resolveUploadBaseUrl
()}
/api/auth/getTime`
,
{
timeout
:
NAS_AUTH_TIMEOUT
}
)
return
timestamp
}
...
...
@@ -308,10 +322,36 @@ export const getChunkApiAuthToken = async (weChatId: string) => {
const
rawStr
=
CHUNK_API_AUTH_SECRET
+
timestamp
const
{
data
:
{
data
},
}
=
await
axios
.
post
(
`
${
resolveUploadBaseUrl
()}
/api/auth/getToken`
,
{
}
=
await
axios
.
post
(
`
${
resolveUploadBaseUrl
()}
/api/auth/getToken`
,
{
timestamp
,
weChatId
,
secret
:
CryptoJS
.
MD5
(
rawStr
).
toString
(),
})
},
{
timeout
:
NAS_AUTH_TIMEOUT
},
)
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
)
}
}
src/components/common/UploadFile/index.vue
View file @
392ffd3a
...
...
@@ -25,7 +25,7 @@
<
script
lang=
"ts"
setup
generic=
"T extends string | string[]"
>
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
{
UploadFileProps
}
from
'./types'
import
{
push
}
from
'notivue'
...
...
@@ -156,7 +156,7 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) =>
}
}
catch
(
error
)
{
console
.
error
(
'上传失败:'
,
error
)
push
.
error
(
'上传失败,请重试'
)
push
.
error
(
resolveUploadErrorMessage
(
error
)
)
const
fileIndex
=
fileList
.
value
.
findIndex
((
file
)
=>
file
.
uid
===
uid
)
if
(
fileIndex
!==
-
1
)
{
...
...
src/components/common/UploadVideo/index.vue
View file @
392ffd3a
...
...
@@ -83,7 +83,7 @@
</
template
>
<
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
{
UploadVideoProps
}
from
'./types'
import
{
getVideoMetadata
}
from
'@/utils'
...
...
@@ -275,7 +275,8 @@ const startUpload = async () => {
push
.
success
(
'视频上传成功!'
)
}
catch
(
error
)
{
uploading
.
value
=
false
uploadError
.
value
=
error
instanceof
Error
?
error
.
message
:
'上传失败,请重试'
console
.
error
(
'视频上传失败'
,
error
)
uploadError
.
value
=
resolveUploadErrorMessage
(
error
)
push
.
error
(
uploadError
.
value
)
}
finally
{
if
(
localVideoUrl
)
{
...
...
src/main.ts
View file @
392ffd3a
...
...
@@ -41,6 +41,13 @@ app.directive('parse-comment', vParseComment)
// 全局组件挂载
app
.
component
(
'SvgIcon'
,
SvgIcon
)
router
.
isReady
().
then
(()
=>
{
// 首次导航的守卫一旦抛错 isReady 会 reject 页面就永远挂不上去(整屏白屏)
// 所以这里兜底 无论首次导航成功与否都要把应用挂载出来
router
.
isReady
()
.
catch
((
error
)
=>
{
console
.
error
(
'首次路由导航失败 仍然挂载应用'
,
error
)
})
.
then
(()
=>
{
app
.
mount
(
'#app'
)
})
})
src/router/guards.ts
View file @
392ffd3a
...
...
@@ -4,6 +4,7 @@ import { saveScrollPosition } from './scrollStorage'
import
{
parseCode
}
from
'@/utils/wxUtil'
import
{
useUserStore
}
from
'@/stores'
import
{
finishPageProgress
,
startPageProgress
}
from
'@/components/common/Progress/usePageProgress'
import
{
push
}
from
'notivue'
// 白名单
const
WHITE_LIST
:
string
[]
=
[
'/aa'
]
...
...
@@ -30,10 +31,17 @@ export function registerRouterGuards(router: Router) {
if
(
code
)
{
// 如果有token 就不需要重新获取用户信息
// if (!userStore.token || !userStore.refreshToken) {
try
{
await
userStore
.
getUserInfoByCode
({
code
,
isCodeLogin
:
0
,
})
}
catch
(
error
)
{
// 守卫里抛错会直接中断导航 页面白屏 什么提示都没有
// 这里兜住 给个提示再照常进页面 后续接口 401 会走重新登录的逻辑
console
.
error
(
'企微 code 登录失败'
,
error
)
push
.
error
(
'登录失败,请刷新页面重试'
)
}
// }
return
{
path
:
to
.
path
,
...
...
src/stores/user.ts
View file @
392ffd3a
...
...
@@ -45,8 +45,15 @@ export const useUserStore = defineStore('user', () => {
setUserInfoAndToken
(
data
)
// 同时获取关于分片的token
const
token
=
await
getChunkApiAuthToken
(
data
.
account
)
setChunkApiAuthToken
(
token
)
// 这个 token 走的是 nas 那边的接口 跟登录、跟首页展示都没有依赖关系
// 所以不 await 直接放后台跑 让首页尽快出来 nas 慢或者挂了也不会拖住登录
// 这里的 catch 不能省 不然 nas 挂掉时会变成未处理的 promise rejection
// 万一没拿到 等真正要分片上传的时候会再补一次(见 ensureChunkApiAuthToken)
getChunkApiAuthToken
(
data
.
account
)
.
then
(
setChunkApiAuthToken
)
.
catch
((
error
)
=>
{
console
.
error
(
'获取分片上传 token 失败 不影响登录'
,
error
)
})
}
const
getNewToken
=
async
()
=>
{
const
{
data
}
=
await
refreshTokenApi
(
refreshToken
.
value
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment