diff --git a/admin/src/api/file.ts b/admin/src/api/file.ts index 2d60dd2c1..9f81ff782 100644 --- a/admin/src/api/file.ts +++ b/admin/src/api/file.ts @@ -100,6 +100,7 @@ export interface OssCredentialsResponse { host?: string cdn_domain?: string key_prefix?: string + object_key?: string max_size?: number duration?: number expired_time?: number @@ -111,8 +112,10 @@ export interface OssCredentialsResponse { } } +export type OssDirectUploadType = 'video' | 'voice' | 'desktop_package' + /** 申请 STS 临时凭证 */ -export function getOssCredentials(params: { type: 'video' }) { +export function getOssCredentials(params: { type: OssDirectUploadType; name?: string }) { return request.post({ url: '/upload/ossCredentials', params @@ -121,7 +124,7 @@ export function getOssCredentials(params: { type: 'video' }) { /** 直传完成回执:写 file 表 + HEAD 校验 */ export function confirmOssUpload(params: { - type: 'video' + type: OssDirectUploadType key: string name: string size: number diff --git a/admin/src/components/upload/index.vue b/admin/src/components/upload/index.vue index eead4783d..98b3a5b62 100644 --- a/admin/src/components/upload/index.vue +++ b/admin/src/components/upload/index.vue @@ -51,8 +51,10 @@ import useAppStore from '@/stores/modules/app' import useUserStore from '@/stores/modules/user' import feedback from '@/utils/feedback' import { + DirectUploadApiError, DirectUploadFallbackError, - uploadVideoDirectToCos + uploadDirectToCos, + type DirectUploadType } from '@/utils/oss-direct-upload' export default defineComponent({ @@ -83,7 +85,7 @@ export default defineComponent({ type: Boolean, default: false }, - // 视频直传到 OSS(绕开服务器中转,仅 type=video 生效) + // 直传到对象存储,绕开服务器中转 direct: { type: Boolean, default: false @@ -102,8 +104,10 @@ export default defineComponent({ const visible = ref(false) const fileList = ref([]) - // 仅 video/voice + direct 时才接管 http-request - const useDirect = computed(() => props.direct && ['video', 'voice'].includes(props.type)) + const directTypes: DirectUploadType[] = ['video', 'voice', 'desktop_package'] + const useDirect = computed( + () => props.direct && directTypes.includes(props.type as DirectUploadType) + ) const handleProgress = () => { visible.value = true @@ -131,7 +135,10 @@ export default defineComponent({ fileList.value = [] emit('allSuccess') } - feedback.msgError(`${file.name}文件上传失败`) + if (!(event instanceof DirectUploadApiError)) { + const message = event instanceof Error ? event.message : '' + feedback.msgError(message || `${file.name}文件上传失败`) + } uploadRefs.value?.abort(file) visible.value = false emit('change', file) @@ -153,18 +160,20 @@ export default defineComponent({ return '.wmv,.avi,.mpg,.mpeg,.3gp,.mov,.mp4,.flv,.rmvb,.mkv' case 'voice': return '.mp3,.wav,.wma,.m4a,.aac,.amr' + case 'desktop_package': + return '.exe,.zip' default: return '*' } }) - // 走 COS 直传:成功时模拟老接口的响应 envelope,失败/降级时回到默认 XHR + // 走 COS 直传:成功时模拟老接口的响应 envelope const httpRequest = async (options: UploadRequestOptions) => { visible.value = true try { - const data = await uploadVideoDirectToCos({ + const data = await uploadDirectToCos({ file: options.file, - type: props.type as any, + type: props.type as DirectUploadType, cid: Number((options.data as any)?.cid ?? 0), onProgress(info) { // 触发 ElUpload 内部进度(保持与默认上传一致的体验) @@ -178,6 +187,12 @@ export default defineComponent({ ;(options as any).onSuccess?.({ code: RequestCodeEnum.SUCCESS, msg: 'ok', data }) } catch (err: any) { if (err instanceof DirectUploadFallbackError) { + if (props.type === 'desktop_package') { + ;(options as any).onError?.( + new Error('当前未启用腾讯云 COS,安装包无法直传,请配置 COS 后重试') + ) + return + } feedback.msgWarning('当前存储不支持直传,已切换为普通上传') await defaultXhrUpload(options) return diff --git a/admin/src/utils/oss-direct-upload.ts b/admin/src/utils/oss-direct-upload.ts index 90637d1f1..1cd284624 100644 --- a/admin/src/utils/oss-direct-upload.ts +++ b/admin/src/utils/oss-direct-upload.ts @@ -3,10 +3,11 @@ import COS from 'cos-js-sdk-v5' import { confirmOssUpload, getOssCredentials, - type OssCredentialsResponse + type OssCredentialsResponse, + type OssDirectUploadType } from '@/api/file' -export type DirectUploadType = 'video' +export type DirectUploadType = OssDirectUploadType export interface DirectUploadProgress { /** 0-100 */ @@ -37,8 +38,17 @@ export interface DirectUploadOptions { const SLICE_SIZE = 5 * 1024 * 1024 // 5MB const ASYNC_LIMIT = 3 +async function callDirectUploadApi(request: () => Promise): Promise { + try { + return await request() + } catch (error) { + // request 拦截器已经展示过接口/网络错误,上传组件只负责收口失败状态 + throw new DirectUploadApiError(error) + } +} + function buildKey(prefix: string, file: File): string { - const ext = (file.name.split('.').pop() || 'mp4').toLowerCase() + const ext = (file.name.split('.').pop() || 'bin').toLowerCase() const ts = Date.now() const rand = Math.random().toString(36).slice(2, 10) return `${prefix}${ts}-${rand}.${ext}` @@ -48,8 +58,13 @@ function buildKey(prefix: string, file: File): string { * 直传到腾讯云 COS(含 STS 凭证申请、分片上传、回执) * 不支持降级 / fallback=true 时抛错,由调用方决定走老链路。 */ -export async function uploadVideoDirectToCos(options: DirectUploadOptions): Promise { - const credentials: OssCredentialsResponse = await getOssCredentials({ type: options.type }) +export async function uploadDirectToCos(options: DirectUploadOptions): Promise { + const credentials: OssCredentialsResponse = await callDirectUploadApi(() => + getOssCredentials({ + type: options.type, + name: options.file.name + }) + ) if (credentials.fallback) { const handled = options.onFallback?.(credentials.provider) ?? false @@ -66,7 +81,7 @@ export async function uploadVideoDirectToCos(options: DirectUploadOptions): Prom if (credentials.max_size && options.file.size > credentials.max_size) { const mb = Math.round(credentials.max_size / 1024 / 1024) - throw new Error(`视频体积超出上限(${mb}MB)`) + throw new Error(`文件体积超出上限(${mb}MB)`) } const cred = credentials.credentials @@ -85,7 +100,7 @@ export async function uploadVideoDirectToCos(options: DirectUploadOptions): Prom } }) - const key = buildKey(credentials.key_prefix, options.file) + const key = credentials.object_key || buildKey(credentials.key_prefix, options.file) await new Promise((resolve, reject) => { cos.uploadFile( @@ -117,14 +132,16 @@ export async function uploadVideoDirectToCos(options: DirectUploadOptions): Prom ) }) - const confirmed = await confirmOssUpload({ - type: options.type, - key, - name: options.file.name, - size: options.file.size, - content_type: options.file.type || '', - cid: options.cid ?? 0 - }) + const confirmed = await callDirectUploadApi(() => + confirmOssUpload({ + type: options.type, + key, + name: options.file.name, + size: options.file.size, + content_type: options.file.type || '', + cid: options.cid ?? 0 + }) + ) options.onProgress?.({ percent: 100, loaded: options.file.size, total: options.file.size, speed: 0 }) @@ -140,3 +157,14 @@ export class DirectUploadFallbackError extends Error { this.provider = provider } } + +/** 请求层已经展示过错误,避免 ElUpload 再弹一条通用失败提示。 */ +export class DirectUploadApiError extends Error { + readonly originalError: unknown + + constructor(error: unknown) { + super('') + this.name = 'DirectUploadApiError' + this.originalError = error + } +} diff --git a/admin/src/views/order/index.vue b/admin/src/views/order/index.vue index 0a882618e..d71c8bd3e 100644 --- a/admin/src/views/order/index.vue +++ b/admin/src/views/order/index.vue @@ -677,6 +677,28 @@ + + + + + +