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
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
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
97 additions
and
34 deletions
+97
-34
index.ts
src/api/common/index.ts
+61
-21
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
+10
-3
guards.ts
src/router/guards.ts
+12
-4
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
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
(
timestamp
,
`
${
resolveUploadBaseUrl
()}
/api/auth/getToken`
,
weChatId
,
{
secret
:
CryptoJS
.
MD5
(
rawStr
).
toString
(),
timestamp
,
})
weChatId
,
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
)
}
}
src/components/common/UploadFile/index.vue
View file @
392ffd3a
...
@@ -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
)
{
...
...
src/components/common/UploadVideo/index.vue
View file @
392ffd3a
...
@@ -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
)
{
...
...
src/main.ts
View file @
392ffd3a
...
@@ -41,6 +41,13 @@ app.directive('parse-comment', vParseComment)
...
@@ -41,6 +41,13 @@ app.directive('parse-comment', vParseComment)
// 全局组件挂载
// 全局组件挂载
app
.
component
(
'SvgIcon'
,
SvgIcon
)
app
.
component
(
'SvgIcon'
,
SvgIcon
)
router
.
isReady
().
then
(()
=>
{
// 首次导航的守卫一旦抛错 isReady 会 reject 页面就永远挂不上去(整屏白屏)
app
.
mount
(
'#app'
)
// 所以这里兜底 无论首次导航成功与否都要把应用挂载出来
})
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'
...
@@ -4,6 +4,7 @@ import { saveScrollPosition } from './scrollStorage'
import
{
parseCode
}
from
'@/utils/wxUtil'
import
{
parseCode
}
from
'@/utils/wxUtil'
import
{
useUserStore
}
from
'@/stores'
import
{
useUserStore
}
from
'@/stores'
import
{
finishPageProgress
,
startPageProgress
}
from
'@/components/common/Progress/usePageProgress'
import
{
finishPageProgress
,
startPageProgress
}
from
'@/components/common/Progress/usePageProgress'
import
{
push
}
from
'notivue'
// 白名单
// 白名单
const
WHITE_LIST
:
string
[]
=
[
'/aa'
]
const
WHITE_LIST
:
string
[]
=
[
'/aa'
]
...
@@ -30,10 +31,17 @@ export function registerRouterGuards(router: Router) {
...
@@ -30,10 +31,17 @@ export function registerRouterGuards(router: Router) {
if
(
code
)
{
if
(
code
)
{
// 如果有token 就不需要重新获取用户信息
// 如果有token 就不需要重新获取用户信息
// if (!userStore.token || !userStore.refreshToken) {
// if (!userStore.token || !userStore.refreshToken) {
await
userStore
.
getUserInfoByCode
({
try
{
code
,
await
userStore
.
getUserInfoByCode
({
isCodeLogin
:
0
,
code
,
})
isCodeLogin
:
0
,
})
}
catch
(
error
)
{
// 守卫里抛错会直接中断导航 页面白屏 什么提示都没有
// 这里兜住 给个提示再照常进页面 后续接口 401 会走重新登录的逻辑
console
.
error
(
'企微 code 登录失败'
,
error
)
push
.
error
(
'登录失败,请刷新页面重试'
)
}
// }
// }
return
{
return
{
path
:
to
.
path
,
path
:
to
.
path
,
...
...
src/stores/user.ts
View file @
392ffd3a
...
@@ -45,8 +45,15 @@ export const useUserStore = defineStore('user', () => {
...
@@ -45,8 +45,15 @@ export const useUserStore = defineStore('user', () => {
setUserInfoAndToken
(
data
)
setUserInfoAndToken
(
data
)
// 同时获取关于分片的token
// 同时获取关于分片的token
const
token
=
await
getChunkApiAuthToken
(
data
.
account
)
// 这个 token 走的是 nas 那边的接口 跟登录、跟首页展示都没有依赖关系
setChunkApiAuthToken
(
token
)
// 所以不 await 直接放后台跑 让首页尽快出来 nas 慢或者挂了也不会拖住登录
// 这里的 catch 不能省 不然 nas 挂掉时会变成未处理的 promise rejection
// 万一没拿到 等真正要分片上传的时候会再补一次(见 ensureChunkApiAuthToken)
getChunkApiAuthToken
(
data
.
account
)
.
then
(
setChunkApiAuthToken
)
.
catch
((
error
)
=>
{
console
.
error
(
'获取分片上传 token 失败 不影响登录'
,
error
)
})
}
}
const
getNewToken
=
async
()
=>
{
const
getNewToken
=
async
()
=>
{
const
{
data
}
=
await
refreshTokenApi
(
refreshToken
.
value
)
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