bug修复1122345

8 阅读4分钟

下面给你一套不改旧上传组件内部、只在“使用层/业务层”加壳的整体方案:解决你这类批量 + 重试 + 清空覆盖导致的“文件名是 A、内容却是别的文件”的偶发串档问题。

核心思想就两点: 1. 批次隔离(batchId):每次“清空覆盖/重新开始一轮上传”,让旧请求回调全部失效 2. 尝试隔离(attempt):同一个文件发生重试时,让旧尝试回调失效(防旧回调覆盖新结果)

再配一个稳定唯一的 clientId(你自己生成,不依赖旧组件 uid),保证写回定位永远不会错。

你这个 bug 为什么会发生(前端视角)

你现在的链路大概是: • 列表里每项有 uid、name、fileId • 上传成功回调里:find(uid) → 写回 fileId • 支持重试 • 支持“重复上传=清空 list 全部覆盖”

问题在于:异步回调可能在“你认为已经结束/被覆盖”之后才回来。

典型时序(最常见): • 第一轮上传 A(attempt=1)发出请求 • 你点了“重试”或清空覆盖开始第二轮 • 第二轮更快成功,把 A 的 fileId 写成 id2 • 过一会第一轮的旧请求也成功回来了,把 fileId 又写成 id1 • UI 仍显示 A 的名字(本地 name 没变),但 fileId 已被旧回调覆盖 → 下载内容就错

所以:你只要能让“旧回调没有写回资格”,问题就会消失。

整体逻辑(你照这个做)

数据结构(业务层维护)

每个文件 item 加这些字段(不影响旧组件): • clientId:你自己生成的唯一ID • uid:旧组件给的也行(可留可不用) • file:原始 File • name:显示名(本地) • fileId:后端返回 • status:pending/uploading/success/fail • attempt:该 item 的上传版本号(用于重试隔离)

另外全局维护: • currentBatchId:当前上传批次号(用于清空覆盖隔离)

核心代码(可直接落地)

这是一层“UploadManager”壳,你只要在页面里用它去驱动旧组件的上传即可。

// UploadManager.ts (或直接写在你的页面 composable 里)

type UploadItem = { clientId: string uid?: string | number file: File name: string status: 'pending' | 'uploading' | 'success' | 'fail' fileId?: string attempt: number // 可选:调试指纹,方便定位串档 fingerprint?: string }

export function createUploadManager(uploadApi: (file: File) => Promise<{ id: string; name?: string }>) { let currentBatchId = 0 const list: UploadItem[] = []

/** 开启新一轮:用于“清空覆盖/重新上传” */ function startNewBatch() { currentBatchId += 1 list.splice(0, list.length) // 清空(保留响应式引用也行) return currentBatchId }

/** 添加文件(选择文件后调用) */ function addFiles(files: File[]) { const items = files.map((file) => { const clientId = (crypto as any).randomUUID?.() ?? ${Date.now()}-${Math.random()} const item: UploadItem = { clientId, file, name: file.name, status: 'pending', fileId: undefined, attempt: 0, fingerprint: ${file.name}|${file.size}|${file.lastModified}, } return item }) list.push(...items) return items }

/** 上传单个(支持重试):不改旧组件,只在外层控制写回资格 */ async function uploadOne(item: UploadItem) { const myBatch = currentBatchId

item.attempt += 1
const myAttempt = item.attempt

item.status = 'uploading'

try {
  const res = await uploadApi(item.file)

  // 关键1:批次不一致 => 说明清空覆盖/新一轮开始了,这是旧回调,丢弃
  if (myBatch !== currentBatchId) return

  // 关键2:尝试号不一致 => 说明该文件又被重试/重新发起了,这是旧回调,丢弃
  if (myAttempt !== item.attempt) return

  item.fileId = res.id
  // 如果后端返回 name,可以更新展示名(可选)
  // item.name = res.name || item.name

  item.status = 'success'
} catch (e) {
  if (myBatch !== currentBatchId) return
  if (myAttempt !== item.attempt) return

  item.status = 'fail'
  throw e
}

}

/** 串行上传(你说你们本质是顺序调用,这里保证严格 await) */ async function uploadAllSerial() { const myBatch = currentBatchId for (const item of list) { // 批次变了就停止(避免旧批次继续跑) if (myBatch !== currentBatchId) return // 跳过已成功 if (item.status === 'success') continue await uploadOne(item) } }

/** 重试某一个 */ async function retry(item: UploadItem) { await uploadOne(item) }

/** 只读暴露列表 */ function getList() { return list }

return { startNewBatch, addFiles, uploadOne, uploadAllSerial, retry, getList, getCurrentBatchId: () => currentBatchId, } }

你在页面里怎么用(不改旧组件)

假设旧组件给你的是: • 选择文件事件:onSelect(files) • 上传方法你自己调:uploadApi(file)(你已有)

使用方式:

const mgr = createUploadManager(uploadApi)

// “覆盖上传 / 重新上传”按钮 function onOverwriteUpload(newFiles: File[]) { mgr.startNewBatch() // ✅ 批次 +1 + 清空 mgr.addFiles(newFiles) // ✅ 建立新列表 mgr.uploadAllSerial() // ✅ 严格串行上传 }

// 普通追加上传 function onAppendUpload(newFiles: File[]) { mgr.addFiles(newFiles) mgr.uploadAllSerial() }

// 单个重试 function onRetry(item) { mgr.retry(item) }

这套为什么能治“偶发串内容”

  1. batchId:解决“清空覆盖”导致旧回调污染新列表

清空只是 UI 操作,网络请求不会自动消失。 batchId 相当于“这一轮上传的身份证”。旧轮回调回来发现身份证过期,就不允许写回。

  1. attempt:解决“重试”导致旧回调覆盖新结果

同一个文件重试时,旧请求可能晚回来。 attempt 相当于“第几次尝试”。只有最新尝试才有写回资格。

  1. clientId:解决“uid 不稳定/复用/丢失”导致 find 命中错对象

你不依赖组件内部 uid,不担心重复/undefined/重建列表。

建议你加的 2 行调试日志(不泄露业务)

在写回 fileId 前打一条:

console.log('[UPLOAD_OK]', item.clientId, item.fingerprint, 'attempt', myAttempt, 'batch', myBatch, 'id', res.id)

点击下载时打一条:

console.log('[DOWNLOAD_CLICK]', item.clientId, item.fingerprint, 'id', item.fileId)

一旦再出现问题,你能一眼看出是不是“旧回调写回”或“id 被覆盖”。

如果你告诉我:你们“重试”是怎么触发的(自动重试?按钮重试?失败后自动重试N次?),我可以把 uploadOne 改成带退避重试且同样受 attempt/batchId 保护的版本。