董国庆
2026-06-22 9f9fe547e5f3b0e308c961c3370e7ebd8e0606a3
调试直传-加日志打印
3个文件已修改
216 ■■■■■ 已修改文件
src/utils/upload.ts 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/appVersion/appVersionManage/add.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/appVersion/appVersionManage/index.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/upload.ts
@@ -95,22 +95,44 @@
  url?: string
}
type DirectUploadUrlPayload = string | {
  uploadUrl?: string
  url?: string
  fileUrl?: string
  ossUrl?: string
}
function logDirectUpload(step: string, payload?: unknown) {
  console.log(`[direct-upload] ${step}`, payload)
}
interface CustomDirectUploadRequestOptions extends UploadCustomRequestOptions {
  /** 可选:外部指定 fileId,不传则公共方法内部自动生成 */
  fileId?: string
  /** 可选:允许业务侧覆盖“获取上传地址”的实现 */
  getUploadUrl?: (params: { fileId: string, file: File }) => Promise<string | { uploadUrl?: string, url?: string, fileUrl?: string, ossUrl?: string }>
  getUploadUrl?: (params: { fileId: string, file: File }) => Promise<Service.ResponseResult<DirectUploadUrlPayload> | DirectUploadUrlPayload>
}
/**
 * 统一兼容后端返回的直传地址格式,便于页面只关心上传行为本身。
 * 支持两类返回:
 * 1. 直接返回上传地址字符串 / 对象
 * 2. 返回标准响应结构,上传地址放在 data 字段里
 */
function normalizeDirectUploadUrlResult(raw: string | { uploadUrl?: string, url?: string, fileUrl?: string, ossUrl?: string } | null | undefined): DirectUploadUrlResult | null {
function normalizeDirectUploadUrlResult(raw: Service.ResponseResult<DirectUploadUrlPayload> | DirectUploadUrlPayload | null | undefined): DirectUploadUrlResult | null {
  if (!raw)
    return null
  if (typeof raw === 'string') {
    const uploadUrl = raw.trim()
  const payload = (
    typeof raw === 'object'
      && raw !== null
      && 'data' in raw
      ? raw.data
      : raw
  )
  if (typeof payload === 'string') {
    const uploadUrl = payload.trim()
    if (!uploadUrl)
      return null
    return {
@@ -119,11 +141,11 @@
    }
  }
  const uploadUrl = String(raw.uploadUrl || raw.url || raw.fileUrl || raw.ossUrl || '').trim()
  const uploadUrl = String(payload?.uploadUrl || payload?.url || payload?.fileUrl || payload?.ossUrl || '').trim()
  if (!uploadUrl)
    return null
  const fileUrl = String(raw.url || raw.fileUrl || raw.ossUrl || '').trim()
  const fileUrl = String(payload?.url || payload?.fileUrl || payload?.ossUrl || '').trim()
  return {
    uploadUrl,
    url: fileUrl || uploadUrl.split('?')[0] || '',
@@ -139,24 +161,46 @@
  uploadUrl,
  file,
  onProgress,
  fileId,
}: {
  uploadUrl: string
  file: File
  onProgress?: (e: { percent: number }) => void
  fileId?: string
}) {
  return new Promise<void>((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    logDirectUpload('xhr open', {
      fileId,
      fileName: file.name,
      method: 'PUT',
      uploadUrl,
    })
    xhr.open('PUT', uploadUrl, true)
    xhr.upload.onprogress = (event) => {
      if (!event.lengthComputable)
        return
      const percent = Math.min(100, Math.round((event.loaded / event.total) * 100))
      logDirectUpload('upload progress', {
        fileId,
        fileName: file.name,
        loaded: event.loaded,
        total: event.total,
        percent,
      })
      onProgress?.({
        percent: Math.min(100, Math.round((event.loaded / event.total) * 100)),
        percent,
      })
    }
    xhr.onload = () => {
      logDirectUpload('xhr load', {
        fileId,
        fileName: file.name,
        status: xhr.status,
        responseText: xhr.responseText,
      })
      if (xhr.status >= 200 && xhr.status < 300) {
        onProgress?.({ percent: 100 })
        resolve()
@@ -166,13 +210,28 @@
    }
    xhr.onerror = () => {
      logDirectUpload('xhr error', {
        fileId,
        fileName: file.name,
        status: xhr.status,
      })
      reject(new Error('Network error'))
    }
    xhr.onabort = () => {
      logDirectUpload('xhr abort', {
        fileId,
        fileName: file.name,
      })
      reject(new Error('Upload aborted'))
    }
    logDirectUpload('xhr send', {
      fileId,
      fileName: file.name,
      size: file.size,
      type: file.type,
    })
    xhr.send(file)
  })
}
@@ -192,7 +251,16 @@
}: CustomDirectUploadRequestOptions) {
  try {
    const rawFile = file.file
    logDirectUpload('custom request start', {
      fileName: file.name,
      hasRawFile: Boolean(rawFile),
      source: getUploadUrl ? 'custom getUploadUrl' : 'default getUploadOSSUrl',
    })
    if (!rawFile) {
      logDirectUpload('custom request missing raw file', {
        fileName: file.name,
        file,
      })
      file.status = 'error'
      onError()
      window.$message.error('未获取到上传文件')
@@ -201,11 +269,27 @@
    // 直传链路里 fileId 由前端先生成,后续保存业务表单时直接提交它
    const nextFileId = String(fileId || uuidv4())
    const uploadUrlResult = normalizeDirectUploadUrlResult(
      await (getUploadUrl
    logDirectUpload('generated file id', {
      fileId: nextFileId,
      fileName: rawFile.name,
      size: rawFile.size,
      type: rawFile.type,
    })
    const rawUploadUrlResponse = await (getUploadUrl
        ? getUploadUrl({ fileId: nextFileId, file: rawFile })
        : getUploadOSSUrl({ fileId: nextFileId })),
    )
        : getUploadOSSUrl({ fileId: nextFileId }))
    logDirectUpload('get upload url response', {
      fileId: nextFileId,
      fileName: rawFile.name,
      response: rawUploadUrlResponse,
    })
    const uploadUrlResult = normalizeDirectUploadUrlResult(rawUploadUrlResponse)
    logDirectUpload('normalized upload url', {
      fileId: nextFileId,
      uploadUrl: uploadUrlResult?.uploadUrl,
      fileUrl: uploadUrlResult?.url,
    })
    if (!uploadUrlResult?.uploadUrl) {
      file.status = 'error'
@@ -214,15 +298,30 @@
      return undefined
    }
    logDirectUpload('start upload', {
      fileId: nextFileId,
      fileName: rawFile.name,
      uploadUrl: uploadUrlResult.uploadUrl,
    })
    await uploadFileToSignedUrl({
      uploadUrl: uploadUrlResult.uploadUrl,
      file: rawFile,
      onProgress,
      fileId: nextFileId,
    })
    logDirectUpload('upload success', {
      fileId: nextFileId,
      fileName: rawFile.name,
      fileUrl: uploadUrlResult.url,
    })
    // 回写 n-upload 所需状态,保持页面文件列表展示正常
    file.status = 'finished'
    file.url = uploadUrlResult.url || ''
    logDirectUpload('call onFinish', {
      fileId: nextFileId,
      fileName: rawFile.name,
    })
    onFinish()
    return {
@@ -233,6 +332,10 @@
    }
  }
  catch (err) {
    logDirectUpload('upload failed', {
      error: err,
      fileName: file.name,
    })
    file.status = 'error'
    onError()
    window.$message.error('文件上传出错')
@@ -253,11 +356,24 @@
  // const { maxSize = 5, allowedTypes = ['image/jpeg', 'image/png', 'image/jpg'] } = options
  const { maxSize = 5, allowedTypes } = options
  const file = data.file.file
  logDirectUpload('before upload check start', {
    fileName: data.file.name,
    hasRawFile: Boolean(file),
    size: file?.size,
    type: file?.type,
    maxSize,
    allowedTypes,
  })
  if (!file)
    return false
  // 检查文件类型
  if (allowedTypes && allowedTypes?.length > 0 && !allowedTypes.includes(file.type)) {
    logDirectUpload('before upload check failed: type', {
      fileName: data.file.name,
      fileType: file.type,
      allowedTypes,
    })
    window.$message.error(`只能上传 ${allowedTypes.join('/')} 格式的文件`)
    return false
  }
@@ -265,9 +381,19 @@
  // 检查文件大小
  const isLtSize = file.size / 1024 / 1024 < maxSize
  if (!isLtSize) {
    logDirectUpload('before upload check failed: size', {
      fileName: data.file.name,
      size: file.size,
      maxSize,
    })
    window.$message.error(`文件大小不能超过 ${maxSize}MB`)
    return false
  }
  logDirectUpload('before upload check passed', {
    fileName: data.file.name,
    size: file.size,
    type: file.type,
  })
  return true
}
src/views/system/appVersion/appVersionManage/add.vue
@@ -6,6 +6,10 @@
import { beforeUploadCheck, customDirectUploadRequest } from '@/utils/upload'
import { releaseAppVersion } from '@/service/api/appVersion'
function logAppVersionAdd(step: string, payload?: unknown) {
  console.log(`[app-version/add] ${step}`, payload)
}
const router = useRouter()
const message = useMessage()
const formRef = ref<FormInst | null>(null)
@@ -45,8 +49,13 @@
async function customRequestAndroid(options: any) {
  androidUploading.value = true
  logAppVersionAdd('android upload triggered', {
    fileName: options?.file?.name,
    file: options?.file,
  })
  const originalOnError = options.onError
  options.onError = () => {
    logAppVersionAdd('android upload onError callback')
    formModel.value.androidFiles = []
    originalOnError?.()
  }
@@ -54,9 +63,11 @@
    // 安卓包改为前端直传:先拿上传地址,再将文件直接传到 OSS
    const res = await customDirectUploadRequest(options as UploadCustomRequestOptions)
    if (!res) {
      logAppVersionAdd('android upload result empty')
      formModel.value.androidFiles = []
      return
    }
    logAppVersionAdd('android upload result', res)
    formModel.value.androidFiles = [{
      id: String(res.id || ''),
      name: res.name || 'android.apk',
@@ -65,14 +76,23 @@
    }]
  }
  finally {
    logAppVersionAdd('android upload finished', {
      uploading: false,
      fileList: formModel.value.androidFiles,
    })
    androidUploading.value = false
  }
}
async function customRequestIos(options: any) {
  iosUploading.value = true
  logAppVersionAdd('ios upload triggered', {
    fileName: options?.file?.name,
    file: options?.file,
  })
  const originalOnError = options.onError
  options.onError = () => {
    logAppVersionAdd('ios upload onError callback')
    formModel.value.iosFiles = []
    originalOnError?.()
  }
@@ -80,9 +100,11 @@
    // iOS 包改为前端直传:上传完成后将 fileId 存入文件项 id 字段
    const res = await customDirectUploadRequest(options as UploadCustomRequestOptions)
    if (!res) {
      logAppVersionAdd('ios upload result empty')
      formModel.value.iosFiles = []
      return
    }
    logAppVersionAdd('ios upload result', res)
    formModel.value.iosFiles = [{
      id: String(res.id || ''),
      name: res.name || 'ios.ipa',
@@ -91,12 +113,22 @@
    }]
  }
  finally {
    logAppVersionAdd('ios upload finished', {
      uploading: false,
      fileList: formModel.value.iosFiles,
    })
    iosUploading.value = false
  }
}
async function handleConfirm() {
  await formRef.value?.validate(async (errors) => {
    logAppVersionAdd('handle confirm validate result', {
      errors,
      androidUploading: androidUploading.value,
      iosUploading: iosUploading.value,
      version: formModel.value.version,
    })
    if (errors) {
      message.error('请完善必填信息')
      return
@@ -123,8 +155,10 @@
      updateDescription: formModel.value.updateNote,
      forceUpdateFlag: formModel.value.isForceUpdate ? 1 : 0,
    }
    logAppVersionAdd('release request params', params)
    const { isSuccess } = await releaseAppVersion(params)
    logAppVersionAdd('release response', { isSuccess, params })
    if (isSuccess) {
      message.success('发布成功')
      window.sessionStorage.setItem('appVersionManageNeedRefresh', '1')
src/views/system/appVersion/appVersionManage/index.vue
@@ -14,6 +14,10 @@
import Pagination from '@/components/common/Pagination.vue'
import OperationLogModal from '@/components/custom/OperationLogModal.vue'
function logAppVersionUpload(step: string, payload?: unknown) {
  console.log(`[app-version/index] ${step}`, payload)
}
const listData = ref<AppVersionItem[]>([])
const loading = ref(false)
const showForm = ref(false)
@@ -189,6 +193,12 @@
async function handleConfirm() {
  await formRef.value?.validate(async (errors) => {
    logAppVersionUpload('handle confirm validate result', {
      errors,
      androidUploading: androidUploading.value,
      iosUploading: iosUploading.value,
      version: formModel.value.version,
    })
    if (errors) {
      window.$message?.error('请完善必填信息')
      return
@@ -215,8 +225,10 @@
      updateDescription: formModel.value.updateNote,
      forceUpdateFlag: formModel.value.isForceUpdate ? 1 : 0,
    }
    logAppVersionUpload('release request params', params)
    const { isSuccess } = await releaseAppVersion(params)
    logAppVersionUpload('release response', { isSuccess, params })
    if (isSuccess) {
      window.$message?.success('发布成功')
      showForm.value = false
@@ -241,8 +253,13 @@
async function customRequestAndroid(options: any) {
  androidUploading.value = true
  logAppVersionUpload('android upload triggered', {
    fileName: options?.file?.name,
    file: options?.file,
  })
  const originalOnError = options.onError
  options.onError = () => {
    logAppVersionUpload('android upload onError callback')
    formModel.value.androidFiles = []
    originalOnError?.()
  }
@@ -250,9 +267,11 @@
    // 安卓包改为前端直传:先换取上传地址,再直接上传到 OSS
    const res = await customDirectUploadRequest(options as UploadCustomRequestOptions)
    if (!res) {
      logAppVersionUpload('android upload result empty')
      formModel.value.androidFiles = []
    }
    else {
      logAppVersionUpload('android upload result', res)
      formModel.value.androidFiles = [{
        id: String(res.id || ''),
        name: res.name || 'android.apk',
@@ -262,14 +281,23 @@
    }
  }
  finally {
    logAppVersionUpload('android upload finished', {
      uploading: false,
      fileList: formModel.value.androidFiles,
    })
    androidUploading.value = false
  }
}
async function customRequestIos(options: any) {
  iosUploading.value = true
  logAppVersionUpload('ios upload triggered', {
    fileName: options?.file?.name,
    file: options?.file,
  })
  const originalOnError = options.onError
  options.onError = () => {
    logAppVersionUpload('ios upload onError callback')
    formModel.value.iosFiles = []
    originalOnError?.()
  }
@@ -277,9 +305,11 @@
    // iOS 包改为前端直传:上传成功后将 fileId 存到文件项 id 字段
    const res = await customDirectUploadRequest(options as UploadCustomRequestOptions)
    if (!res) {
      logAppVersionUpload('ios upload result empty')
      formModel.value.iosFiles = []
    }
    else {
      logAppVersionUpload('ios upload result', res)
      formModel.value.iosFiles = [{
        id: String(res.id || ''),
        name: res.name || 'ios.ipa',
@@ -289,6 +319,10 @@
    }
  }
  finally {
    logAppVersionUpload('ios upload finished', {
      uploading: false,
      fileList: formModel.value.iosFiles,
    })
    iosUploading.value = false
  }
}