分片上传使用说明
管理端大文件(安装包 .ipa/.apk/.wgt、视频等)通过 admin/src/components/upload-chunk/ 上传,走标准 S3 multipart 协议:前端 @aws-sdk/lib-storage 直传,不经过后端"中转上传",天然带并发 / 重试 / 进度回调 / 中断。业务不要自己写 axios 上传。
普通图片 / 小文件请改用 单文件上传。 后端存储/接口详见 后端-存储引擎。业务表只保存
file_hash_id(统一文件哈希主键),访问地址通过/api/uploads/<id>动态拼装。
一、组件形态
- 外层是虚线拖拽区(点击 / 拖入均可),显示"文件名(大小)"。
- 上方独立两段进度条:MD5 计算进度 + 上传进度(
formatSize显示已传 / 总量)。 - 底部按钮:
开始上传/取消上传;额外具名插槽#actions="{ file, uploading }"允许自定义按钮组。 - 秒传命中会走独立提示(
instantHit),不再显示上传进度条。
二、上传流程
选择文件 → SparkMD5(10MB 分块,进度实时反映)
→ POST {endpoint}/checkInstant 秒传预检
├─ 命中:直接 emit('change', { instant:true, fileHashId, location:url })
└─ 未命中:
POST {endpoint}/generateKey 拿 S3 Key
→ @aws-sdk/lib-storage Upload(Bucket=props.bucket, Key, Body=file, queueSize, partSize)
→ upload.done()
→ POST {endpoint}/registerHash 落 x_common_file_hash(供下次秒传)
→ emit('change', { fileHashId, key, location:url, instant:false })任何一步失败或用户点"取消上传"(upload.abort())都会走 catch:AbortError 提示"已取消"、其它 emit('error', err)。
三、S3 客户端与鉴权
admin/src/utils/s3Client.ts 提供单例:
ts
const s3Client = new S3Client({
region: 'us-east-1',
endpoint: `${location.protocol}//${location.host}/api/admin/upload_chunk`,
forcePathStyle: true, // 路径风格,不启用 virtual-host 风格
credentials: { accessKeyId: 'dummy', secretAccessKey: 'dummy' }
})- 通过 middleware 在
finalizeRequest阶段:注入token: <JWT>头、剥离 AWS SigV4 相关头(authorization/x-amz-date/x-amz-content-sha256: UNSIGNED-PAYLOAD),后端以token鉴权。 - 切换 MinIO 或其他 S3 兼容服务时,前端代码零改动,只改
s3Client.ts的endpoint/credentials。
四、Props
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
bucket | string | 'files' | S3 Bucket 名(后端目前仅支持固定桶) |
accept | string | '' | <input accept>,通常用 ext 代替 |
ext | string[] | [] | 允许扩展名,如 ['ipa','apk']、['wgt'];非空时会覆盖 accept |
endpoint | string | '/api/admin/upload_chunk' | S3 兼容前缀,同时是管理接口前缀(checkInstant / generateKey / registerHash 拼在它下面) |
partSize | number | 5 * 1024 * 1024 | 单片大小(5 MB) |
queueSize | number | 3 | 分片并发数 |
五、Emits / Expose
change(result: ChunkUploadResult)
ts
// admin/src/components/upload-chunk/type.ts
interface ChunkUploadResult {
md5: string
key: string // S3 Key(秒传命中时为空串)
fileName: string
fileSize: number
fileHashId: string|null // ⭐ 业务表要存的字段;秒传命中或注册失败都可能为 null
instant: boolean // 是否走了秒传
location?: string // 完整访问 URL(可直接展示)
}error(err: Error)
上传任一阶段失败触发;AbortError 走"已取消"分支,不会 emit。
defineExpose({ startUpload, cancelUpload, triggerFileInput })
外部可编程"开始 / 取消 / 弹文件选择"。
六、用法
安装包发布(.ipa/.apk)
vue
<!-- views/fabu/components/upload.vue 内 -->
<upload-chunk :ext="['ipa', 'apk']" @change="handleUploaded" @error="handleError" />
<script setup lang="ts">
import UploadChunk from '@/components/upload-chunk'
import type { ChunkUploadResult } from '@/components/upload-chunk'
const handleUploaded = async (r: ChunkUploadResult) => {
if (!r.fileHashId) { feedback.msgError('上传异常,未获取到文件标识'); return }
// 通知后端按 file_hash_id 解析安装包元信息(BundleId/版本/图标…)
await fabuVersionUpload({ file_hash_id: r.fileHashId, file_name: r.fileName })
}
</script>热更包(.wgt)
vue
<!-- views/fabu/version/wgt.vue -->
<upload-chunk :ext="['wgt']" @change="handleUploaded" @error="handleUploadError" />调试页 + 自定义 actions 插槽
views/dev_tools/uploadChunk.vue 演示了 bucket="files" 显式写法与 #actions 插槽:
vue
<UploadChunk ref="uploadChunkRef" bucket="files" @change="handleUploadResult" @error="handleError">
<template #actions="{ file }">
<span v-if="file" class="text-info mr-2">已选:{{ file.name }}</span>
</template>
</UploadChunk>七、后端接口
| 用途 | 方法 | 路径 | 请求 |
|---|---|---|---|
| 秒传预检 | POST | /api/admin/upload_chunk/checkInstant | { file_md5, file_name } |
| 生成 S3 Key | POST | /api/admin/upload_chunk/generateKey | { file_name } → { key } |
| 上传完注册哈希 | POST | /api/admin/upload_chunk/registerHash | { file_md5, file_key, file_name, file_size } → { file_hash_id, url } |
| S3 multipart 全套(Init / UploadPart / Complete / Abort / ListParts) | PUT / POST / GET / DELETE | /api/admin/upload_chunk/{bucket}/{key} | 标准 S3 RESTful,forcePathStyle 路径风格 |
| 文件流访问 | GET | /api/uploads/<file_hash_id>[/<fileName>] | 自动 MIME + 长缓存 |
鉴权统一走请求头 token: <JWT>(由 s3Client middleware 注入)。
分片临时目录由
storage/cron_cleanup.go每小时清理一次,超过ChunkExpireHour(默认 24h)的未完成任务会被删除。
八、常见坑
- 业务表存
file_hash_id,别存 URL:URL 由ossDomain + /api/uploads/<id>动态拼(见stores/modules/app.ts),换域名/协议时不用回补数据。 fileHashId可能为null:秒传命中但后端未返 id、或registerHash失败都会为空;业务侧必须显式判空再走后续接口(fabu/components/upload.vue就是这么处理的)。- 秒传的前提是历史曾成功
registerHash同 MD5:首次上传完不调registerHash,下次同 MD5 也不会秒传;组件已在成功路径里内置调用,业务无需重复。 - 走的是真 S3 协议:
s3Client.ts通过 middleware 剥离 AWS 签名头并注入token;不要给请求加自定义业务头(后端只识别token),也不要启用 SigV4 校验。 ext会覆盖accept:ext.length > 0时acceptValue用ext拼.png,.jpg形式;只设accept才会走原生属性。partSize与浏览器内存:单片过小 → 分片数暴涨;过大 → 一片读入ArrayBuffer占内存。默认 5 MB / 并发 3 是常见甜点,几百 MB ~ 数 GB 场景无需改。- 中断是
upload.abort():与 单文件上传 的uploadSeq++不同,这里是真中断(AWS SDK 内部会 AbortController 取消后续分片),已 in-flight 的分片会由后端AbortMultipartUpload清理。 - MD5 计算占用主线程:
SparkMD5走FileReader.readAsArrayBuffer,GB 级文件会明显卡顿;如需极限优化可迁到 Web Worker(upload-chunk/index.vue::calcMD5一处即可)。
九、相关代码位置
- 前端组件与客户端:
admin/src/components/upload-chunk/index.vue(组件实现)admin/src/components/upload-chunk/index.ts(barrel 出口)admin/src/components/upload-chunk/type.ts(ChunkUploadResult)admin/src/utils/s3Client.ts(S3 客户端与 token 注入)admin/src/utils/file.ts(formatSize)
- 后端接口:
server/app/controller/admin_ctl/common_controller/upload_chunk.go;驱动与存储:server/plugin/storage/{storage_interface.go,storage_factory.go,storage_local.go,storage_oss.go,presign.go,cron_cleanup.go}。 - 现网调用点:
admin/src/views/fabu/components/upload.vue(.ipa/.apk发布)admin/src/views/fabu/version/wgt.vue(.wgt热更)admin/src/views/dev_tools/uploadChunk.vue(分片调试页)