
这篇文章我不想只讲“上传功能怎么做出来”而是把我这次做多附件上传中心时真正踩到的工程边界梳理清楚为什么单个上传接口一旦遇到后台切换、网络抖动、超大文件就会很快失控以及我最后是怎么把断点续传、失败重试、并发队列和后台调度收成一套能上线的结构。一、最开始的问题不是上传失败而是上传逻辑太“直”了我最早做这个页面时想法其实特别直接用户选文件点上传Request Kit 发请求成功了就改状态失败了就弹提示。刚跑 Demo 的时候这套逻辑看起来没什么问题。可一旦把场景从“上传一个 3MB 图片”换成“连续上传 5 个附件其中还混着 1GB 的视频、几十 MB 的文档、网络中途断一下、用户切到后台再回来”问题马上就全出来了文件一多多个任务互相抢占某个大文件失败以后整个队列状态会被拖乱页面退出再回来任务状态不连续网络恢复以后不知道该从头传还是从上次分片继续某个任务一直失败会不会把剩下所有任务都卡住用户看到“失败”两个字但根本不知道失败在哪一段。也就是说表面看是“上传中心”本质上已经不是一个按钮功能了而是一套任务调度系统。二、我后来先把任务拆成了“状态机”页面才开始好维护如果上传任务只有“成功”和“失败”两个状态这种功能几乎一定会越写越乱。因为真实业务里一个上传任务至少会经历这些阶段WAITING已经入队等待执行UPLOADING正在上传PAUSED手动暂停或网络暂时不可用RETRYING发生异常进入自动重试FAILED超过重试上限正式失败SUCCESS上传完成。我后面把每个文件都抽成一条UploadTask里面除了文件信息还会记录当前状态当前进度已上传分片数失败次数最大重试次数最近错误信息本地断点位置。这样做最大的变化是页面不再凭感觉拼 UI而是完全跟着任务状态走。最基础的一版任务模型我最后收成了下面这种结构exportenumUploadStatus{WAITINGWAITING,UPLOADINGUPLOADING,PAUSEDPAUSED,RETRYINGRETRYING,FAILEDFAILED,SUCCESSSUCCESS}exportinterfaceUploadTask{id:stringfilePath:stringfileName:stringfileSize:numberchunkSize:numberuploadedChunk:numbertotalChunk:numberprogress:numberretryCount:numbermaxRetry:numberstatus:UploadStatus errorMessage?:string}这段代码看起来只是多了几个字段但它解决的是一个更底层的问题上传任务终于有了统一的生命周期。三、真正让我稳下来的不是接口而是“上传队列”一开始我也试过最省事的做法用户选了几个文件就起几个并发上传。结果非常快问题就来了。大文件占住带宽小文件一直排队网络一差多个任务同时重试页面上明明只坏了一项但用户看到的却像全部都卡住了。后来我就不再让页面直接发请求而是加了一层UploadManager。这层只做三件事维护任务队列控制最大并发数统一管理重试和状态回调。也就是说页面负责“发起上传”真正执行上传的是队列管理器。我最后保留的核心逻辑大概是这样classUploadManager{privatequeue:UploadTask[][]privaterunningCount:number0privatemaxConcurrent:number3addTask(task:UploadTask){this.queue.push(task)this.schedule()}privateschedule(){while(this.runningCountthis.maxConcurrent){constnextTaskthis.queue.find(itemitem.statusUploadStatus.WAITING)if(!nextTask){break}this.runningCount1nextTask.statusUploadStatus.UPLOADINGthis.uploadTask(nextTask).finally((){this.runningCount-1this.schedule()})}}}这里最关键的不是while这几行而是思路变了上传不是页面事件而是队列事件页面点击“上传”只是在往队列里塞任务调度权在管理器手里不在按钮手里。这一点想通以后很多以前很乱的问题一下就有了归宿。四、断点续传不是一个按钮而是一段“已上传分片”的记录真正让我觉得这个功能像样起来的是把断点续传补完整。用户看到的只是“继续上传”四个字但工程上它背后至少要回答三件事已经传到哪个分片了下次恢复时从哪里接着走失败后重试是重试当前分片还是重试整个文件。我最后没有做整文件一次性上传而是把大文件按分片处理。每个分片完成以后立即更新uploadedChunkprogresslastCallbackTime本地持久化断点位置这样做的好处非常直接如果第 385 个分片超时恢复时就不必从第 1 个分片重新开始而是从384的下一个位置继续。这也是为什么在上传详情页里我刻意把“总分片数”“断点位置”“重试次数”“最后回调时间”都显式展示出来。你看这张图其实就是在讲这套链路的几个关键证据红圈标出的“总分片数”和“断点位置”告诉我们恢复到底从哪一段开始“失败原因”不是一句泛泛提示而是具体到网络超时“重试次数”和“最后回调时间”能让排查不再停留在猜测层面。很多上传功能看起来也有失败页但如果没有这些字段开发者很难判断问题到底卡在哪一步。五、自动重试真正难的不是“重试一下”而是别把队列拖崩自动重试听起来像一句话真正落地时却很讲分寸。如果所有失败都立刻重试网络一抖整个队列会同时重试某个必然失败的任务会反复耗资源用户会觉得页面一直在转但又不知道什么时候结束。所以我最后给重试定了三条规则只对可恢复错误重试例如网络超时、瞬时连接失败限制最大重试次数超过上限进入FAILED重试前做延迟退避避免多个任务瞬间同时重试。比如第 1 次重试等 2 秒第 2 次等 4 秒第 3 次等 8 秒。这样一来网络刚刚恢复时队列不会一下子全部撞上去。页面上我也把这件事讲清楚了失败任务不是彻底结束而是会先进入“可重试”的状态再决定是系统自动兜底还是用户手动触发。这张图里红色标注圈出来的点其实刚好就是用户最关心、开发者也最该先排查的几个位置“并发 3” 对应的是队列调度能力“重试”按钮对应失败任务的人工接管入口“继续上传”对应断点续传能力。读者看一眼就能理解这不是一个简单的文件列表而是一套带调度、带恢复、带失败兜底的上传中心。六、后台切走以后还能继续是因为我把后台任务和上传链路接在了一起多附件上传真正的考题很多时候不是前台而是后台。因为用户上传视频、压缩包这类大文件时几乎不可能一直盯着页面看。他切出去回消息、查文档、锁屏都是很正常的操作。如果这时候上传能力完全依赖当前页面生命周期用户体验一定很差。所以我后面又补上了Background Tasks Kit的调度逻辑让上传任务在应用退到后台后仍然能维持可控执行。在工程结构上我没有把后台任务另写成一套上传逻辑而是只让后台能力负责“保活与调度”真正的任务状态还是统一落在UploadManager上。也就是说前台和后台不是两套上传系统它们共享同一套任务队列后台只是改变任务执行时机而不改变任务模型。这个收法很重要。否则很容易出现前台一个状态、后台一个状态、页面回来又对不上的情况。七、整个工程为什么能稳下来我觉得关键不是某个 API而是“证据可见”后面我回头看这次实战真正让我满意的不是“上传成功率提高了”而是整个系统开始有了可解释性。比如在 DevEco 的开发视角里我会同时看三块东西左边项目结构里页面、管理器、后台任务是不是分层清楚中间代码里队列状态、重试入口、后台调度是不是收口在少数几个方法里底部日志里任务回调、重试日志、队列状态是不是连续可追踪。这张图对我来说就很像这次工程的缩影红色箭头标注“队列状态”说明上传中心不再是散点逻辑“重试入口”说明错误恢复是有结构的“后台任务调度”说明页面生命周期之外任务还能被系统托住底部日志把“回调—重试—队列状态”串成了一条完整证据链。这也是我这次最强烈的体会上传功能一旦进入工程化阶段最值钱的不是单次成功而是状态可见、过程可追。八、本文小记如果让我用一句话总结这次实战我会写成多附件上传中心的难点不在“传”而在“调度”。真正决定体验的从来不是那个“选择文件”按钮而是队列是不是稳分片是不是能续失败是不是能判重试是不是有边界后台是不是还能接着跑。对我自己来说这次做完以后有几个判断基本算是固定下来了任务一定要先抽成状态机上传一定要收进队列管理器断点续传一定要落到可持久化的分片进度上自动重试一定要有错误类型和次数边界后台调度一定要和前台任务共用一套状态来源。如果你现在也在做 HarmonyOS 的文件上传类功能我的建议不是“先把上传接口接上”而是先想清楚你的上传中心究竟只是一个页面还是一套任务系统。想清楚这一点后面的工程结构会完全不一样。