第一章 企业级文件上传架构设计

1.1 为什么需要重新设计文件上传系统?

很多 HarmonyOS NEXT 初学者第一次实现上传,大概率会这样写:


Button('上传文件')
  .onClick(async()=>{

    let result =
      await picker.select()

    http.request(
      "https://xxx.com/upload",
      {
        method:http.RequestMethod.POST,
        extraData:result
      }
    )

  })

看起来:

简单。

直接。

能跑。

但是进入企业项目以后,这种写法马上暴露问题。


真实业务场景

假设开发一个商城 App:

用户发布商品:

需要上传:


商品主图
    |
    |- 图片1  8MB
    |- 图片2  6MB
    |- 图片3  10MB


商品视频

    |
    |- 500MB

如果直接上传:


选择文件

↓

HTTP POST

↓

服务器接收

↓

保存

会出现:


问题一:大文件上传失败

例如:

视频:


500MB

用户上传到:


80%

突然:


网络断开

传统方式:

重新开始。

用户体验:

灾难。


问题二:图片体积过大

手机拍照:


4000 x 3000

8MB

但是服务器真正需要:


1080 x 1080

200KB

如果不处理:

每天大量浪费:

  • 流量
  • CDN成本
  • 存储空间

问题三:OSS 密钥泄露

错误设计:

客户端:


const accessKey =
"xxxxxxxx"


const secret =
"xxxxxxxx"

然后:


APP
 |
 |
 OSS

这是严重安全问题。

因为:

APK 可以反编译。

攻击者拿到:


AccessKey

SecretKey

直接:


删除文件

上传病毒

下载全部数据

问题四:上传逻辑散落

很多项目:

页面1:


uploadAvatar()

页面2:


uploadVideo()

页面3:


uploadFile()

最后:

三个上传逻辑:

三套代码。

维护困难。


所以企业项目需要:

统一上传架构。


1.2 企业级上传整体架构

一个成熟上传系统:

应该如下:


                 用户操作


                    |
                    ↓


              UploadManager


                    |
        ----------------------------

        |             |            |

        ↓             ↓            ↓


   HTTP上传       OSS上传      MinIO上传



        |
        |
----------------------------

文件处理层


        |

        ↓


 文件选择

 Uri解析

 文件读取

 图片压缩

 MD5计算

 分片

 重试

 进度


1.3 分层设计

企业项目推荐:

第一层:UI层

负责:

  • 选择文件
  • 展示进度
  • 展示结果

例如:


UploadPage.ets

不要:

在页面里面写:

HTTP。

OSS。


第二层:业务层

UploadManager

职责:

  • 创建上传任务
  • 管理状态
  • 调度上传

例如:


UploadManager.ets

第三层:上传实现层

不同上传方式:

实现统一接口。

例如:


Uploader

   |
   |
   |------ HttpUploader

   |
   |------ OssUploader

   |
   |------ CosUploader

第四层:文件处理层

包括:


FileUtil

CompressUtil

HashUtil

ChunkUtil

最终:

项目结构:


entry

├── pages

│    └── UploadPage.ets


├── common

│
├── upload

│
│    ├── UploadManager.ets
│
│    ├── UploadTask.ets
│
│    ├── UploadFile.ets
│
│    ├── Uploader.ets
│
│    ├── HttpUploader.ets
│
│    ├── OssUploader.ets
│
│    ├── ChunkUploader.ets
│
│    ├── CompressUtil.ets
│
│    └── Md5Util.ets

1.4 上传生命周期设计

一个文件上传:

实际上是一个状态机。

流程:


WAIT

 |

 ↓

SELECT


 |

 ↓

READ


 |

 ↓

COMPRESS


 |

 ↓

HASH


 |

 ↓

CHECK


 |

 ↓

UPLOAD


 |

 ↓

SUCCESS

失败:


UPLOAD

 |

 ↓

ERROR

 |

 ↓

RETRY

定义状态:

创建:


UploadState.ets

代码:


export enum UploadState {


  /**
   * 等待上传
   */
  WAIT,


  /**
   * 文件读取
   */
  READING,


  /**
   * 图片压缩
   */
  COMPRESS,


  /**
   * 计算文件hash
   */
  HASH,


  /**
   * 上传中
   */
  UPLOADING,


  /**
   * 上传成功
   */
  SUCCESS,


  /**
   * 上传失败
   */
  ERROR,


  /**
   * 用户取消
   */
  CANCEL


}

为什么需要这么多状态?

因为 UI 需要反馈。

例如:

状态:


COMPRESS

显示:


正在优化图片...

状态:


HASH

显示:


正在检查文件...

状态:


UPLOADING

显示:


上传中 65%

1.5 定义上传文件模型

创建:


UploadFile.ets

代码:


export class UploadFile {


 /**
  * 文件Uri
  */
 uri:string=""


 /**
  * 文件名称
  */
 name:string=""


 /**
  * 文件大小
  */
 size:number=0


 /**
  * MIME类型
  */
 mimeType:string=""


 /**
  * MD5值
  */
 md5:string=""


 /**
  * 文件类型
  */
 type:string=""


}

为什么不用:


string

例如:


upload(uri:string)

后面一定会扩展:

需要:


文件大小

文件名称

md5

mime

分片信息

所以提前设计模型。


1.6 上传任务模型

一个文件:

对应一个 Task。

创建:


UploadTask.ets

代码:


import {
 UploadState
} from './UploadState'


import {
 UploadFile
} from './UploadFile'


export class UploadTask {


 /**
  * 任务ID
  */
 id:string=""


 /**
  * 上传文件
  */
 file:UploadFile


 /**
  * 当前状态
  */
 state:
 UploadState=
 UploadState.WAIT


 /**
  * 上传进度
  */
 progress:number=0



 constructor(
    file:UploadFile
 ){

    this.file=file

 }


}

例如:

上传头像:


Task:


id:

10001


file:

avatar.png


state:

UPLOADING


progress:

45

1.7 定义上传接口

核心设计。

所有上传方式:

必须实现:


Uploader

创建:


Uploader.ets

代码:


import {
 UploadTask
} from './UploadTask'


export interface Uploader {



 upload(
   task:UploadTask
 ):Promise<string>




 cancel(
   taskId:string
 ):void



}

以后:

HTTP:


class HttpUploader
implements Uploader

OSS:


class OssUploader
implements Uploader

页面完全无感。


1.8 UploadManager核心实现

创建:


UploadManager.ets

代码:


import {
 UploadTask
} from './UploadTask'


import {
 UploadFile
} from './UploadFile'


import {
 Uploader
} from './Uploader'



export class UploadManager {


 private uploader:Uploader



 constructor(
   uploader:Uploader
 ){

   this.uploader=uploader

 }



 async upload(
    file:UploadFile
 ):Promise<string>{


    let task =
    new UploadTask(file)


    return await
    this.uploader.upload(task)


 }



}

业务调用:


let manager =
new UploadManager(
 new HttpUploader()
)


let url =
await manager.upload(file)

此时:

业务层不知道:

上传到了哪里。

可能:


服务器

OSS

MinIO

COS

都可以。

第二章 HarmonyOS NEXT 文件选择体系详解

在上一章我们完成了企业级上传框架设计。

但是上传之前,还有一个核心问题:

文件从哪里来?

在 HarmonyOS NEXT 中,文件来源和 Android、iOS 有明显区别。

传统开发思维:


用户选择文件

↓

得到文件路径

↓

File对象

↓

上传

但是 HarmonyOS NEXT:


用户选择文件

↓

Picker

↓

Uri

↓

打开Uri

↓

读取数据流

↓

上传

核心:

HarmonyOS 不鼓励应用直接访问用户真实文件路径,而是通过 Uri 进行安全访问。


2.1 HarmonyOS 文件访问模型

HarmonyOS NEXT 文件系统主要分:


应用沙箱文件

↓

公共文件

↓

媒体文件

↓

用户授权文件

2.1.1 应用沙箱

每个应用拥有独立空间。

例如:


/data/storage/el2/base

里面:


files

cache

database

preferences

例如:

用户下载头像:


/data/storage/el2/base/files/avatar.png

特点:

  • APP自己管理
  • 不需要用户授权
  • 其他APP无法访问

2.1.2 公共文件

例如:


Download

Documents

Pictures

Videos

这些属于系统公共空间。

访问:

需要用户授权。


2.1.3 媒体资源

例如:

相册:


照片

视频

音频

HarmonyOS NEXT:

推荐:


photoAccessHelper

而不是直接扫描目录。


2.2 为什么返回 Uri?

例如:

用户选择一张图片。

Android早期:

返回:


/storage/emulated/0/DCIM/a.jpg

HarmonyOS:

返回:


file://media/Photo/1/xxx

为什么?

因为:

安全

如果APP直接获得:


/storage/xxx

APP可以:

  • 遍历用户文件
  • 偷读取照片
  • 获取隐私

Uri机制:

类似:


临时访问许可证

用户选择:

授权一个文件。

APP只能访问:

这个文件。


2.3 文件选择方式

HarmonyOS NEXT 常见:

场景API
图片视频PhotoViewPicker
普通文件DocumentViewPicker
保存文件DocumentSavePicker
拍照Camera
录音AudioCapturer

2.4 PhotoViewPicker 图片选择实战

企业最常见:

上传头像。

流程:


点击按钮

↓

打开相册

↓

用户选择图片

↓

返回Uri

↓

读取图片

↓

压缩

↓

上传

2.4.1 导入模块


import {
 photoAccessHelper
} from '@kit.MediaLibraryKit'

2.4.2 创建选择器

代码:


let photoPicker =
new photoAccessHelper.PhotoViewPicker()

2.4.3 调用选择


let result =
await photoPicker.select({

  MIMEType:
  photoAccessHelper.PhotoViewMIMETypes
  .IMAGE_TYPE,


  maxSelectNumber:1

})

返回:


PhotoSelectResult

结构:


{
 "photoUris":[
   "file://media/Photo/xxx"
 ]
}

完整封装

企业项目:

不要页面直接调用。

创建:


FilePickerManager.ets

代码:


import {
 photoAccessHelper
}
from '@kit.MediaLibraryKit'



export class FilePickerManager {



 static async pickImage()
 :Promise<string>{


 let picker =
 new photoAccessHelper
 .PhotoViewPicker()



 let result =
 await picker.select({


 MIMEType:
 photoAccessHelper
 .PhotoViewMIMETypes
 .IMAGE_TYPE,


 maxSelectNumber:1


 })



 if(
 result.photoUris.length>0
 ){

   return result.photoUris[0]

 }


 return ""

 }



}

页面:


let uri =
await FilePickerManager.pickImage()

页面完全不知道 Picker 细节。


2.5 DocumentViewPicker 文件选择

除了图片:

企业更多是:


PDF

Excel

Word

ZIP

TXT

例如:

OA审批上传合同。


2.5.1 引入


import {
 picker
}
from '@kit.CoreFileKit'

2.5.2 创建


let documentPicker =
new picker.DocumentViewPicker()

2.5.3 选择文件


let result =
await documentPicker.select()

返回:


uri

例如:


file://docs/contract.pdf

封装:


export class DocumentPickerManager {


static async pickFile(){


 let picker =
 new picker.DocumentViewPicker()



 let result =
 await picker.select()



 return result[0]


}



}

2.6 统一文件选择接口

现在:

图片:


PhotoViewPicker

文件:


DocumentViewPicker

如果业务层判断:


if(image){

 PhotoPicker

}else{

 DocumentPicker

}

以后维护很痛苦。

所以统一:


FilePicker

       |
       |

-----------------

ImagePicker

DocumentPicker

VideoPicker

设计:


export interface Picker {


 pick()
 :
 Promise<string>


}

图片实现:


export class ImagePicker
implements Picker{


 async pick(){


 return await
 FilePickerManager.pickImage()


 }


}

文件实现:


export class FilePicker
implements Picker{


async pick(){


return await
DocumentPickerManager.pickFile()


}


}

业务:


let picker =
new ImagePicker()


let uri =
await picker.pick()

2.7 Uri 转换文件信息

拿到:


file://xxx

还不能上传。

需要:


Uri

↓

文件大小

↓

文件名称

↓

MIME

创建:


FileInfoUtil.ets

获取文件大小

使用:


import fileIo
from '@ohos.file.fs'

打开:


let fd =
fileIo.openSync(uri)

获取:


let stat =
fileIo.statSync(fd)

得到:


stat.size

封装:


export class FileInfoUtil {



static getSize(
 uri:string
):number{


 let stat =
 fileIo.statSync(uri)


 return stat.size


}


}

2.8 Uri读取文件流

上传最终需要:


ArrayBuffer

或者

Stream

读取:


let file =
fileIo.openSync(uri)


let buffer =
new ArrayBuffer(size)

读取:


fileIo.readSync(
 file.fd,
 buffer
)

流程:


Uri

↓

open

↓

read

↓

ArrayBuffer

↓

HTTP

↓

服务器

2.9 大文件不要一次读取

错误:


let buffer =
readAll(file)

例如:

视频:


2GB

直接:

OOM。


正确:

流式读取。


文件

↓

1MB

↓

上传

↓

下一块

↓

上传

这就是:

后面章节的:

分片上传


2.10 创建企业统一 FileService

最终:

所有文件处理:

统一入口。

目录:


common/file

    FileService.ets

代码:


export class FileService {


static async getFile(
 uri:string
):Promise<UploadFile>{



 let file =
 new UploadFile()



 file.uri=uri


 file.name=
 this.getName(uri)



 file.size=
 this.getSize(uri)



 return file


}


}

以后:

所有上传:

统一:


let file =
await FileService.getFile(uri)


await uploadManager.upload(file)

2.11 企业项目中的文件选择完整流程

最终:


页面

 |

 ↓

FilePickerManager

 |

 ↓

Uri

 |

 ↓

FileService

 |

 ↓

UploadFile

 |

 ↓

UploadManager

 |

 ↓

Uploader

 |

 ↓

OSS/HTTP

第三章 HarmonyOS NEXT Uri、File、ArrayBuffer 深度解析

3.1 HarmonyOS 文件数据流模型

传统 Web:


<input type=file>

        |

        ↓

File对象

        |

        ↓

Blob

        |

        ↓

ArrayBuffer

        |

        ↓

上传

Android:


Uri

 |

ContentResolver

 |

InputStream

 |

byte[]

 |

上传

HarmonyOS NEXT:


Uri

 |

fileIo

 |

FileDescriptor

 |

ArrayBuffer

 |

HTTP Request

 |

服务器

核心:

HarmonyOS 文件上传,本质:

把 Uri 指向的数据转换成二进制流,然后通过 HTTP 发送。


3.2 Uri 到底是什么?

例如:

PhotoPicker 返回:


file://media/Photo/1/IMG_001.jpg

这个字符串:

不是文件内容。

只是一个:

"文件引用地址"

类似:

数据库:


user_id=1001

它不是用户数据。

只是定位信息。


所以:

不能:


http.request({
    extraData:uri
})

因为上传的是:


file://media/xxx.jpg

而不是:


图片二进制

3.3 获取文件描述符 FD

HarmonyOS:

通过:


fileIo.open()

打开文件。

引入:


import {
 fileIo
}
from '@kit.CoreFileKit'

代码:


let file =
fileIo.openSync(
 uri,
 fileIo.OpenMode.READ_ONLY
)

返回:


File

包含:


{
 fd:number
}

例如:


console.log(file.fd)

输出:


23

这个:

23

就是系统文件描述符。


3.4 获取文件大小

为什么需要大小?

因为读取:

需要知道:


读取多少字节

代码:


let stat =
fileIo.statSync(uri)

返回:


{
 size:2048000,
 mode:0,
 mtime:xxx
}

获取:


let size =
stat.size

单位:

byte。

例如:


2048000 byte

≈2MB

封装:


export class FileUtil {


static getSize(
 uri:string
):number{


 let stat =
 fileIo.statSync(uri)


 return stat.size

}


}

3.5 ArrayBuffer 是什么?

HarmonyOS 二进制处理核心。

简单理解:

字符串:


hello

对应:


ASCII:

68 65 6C 6C 6F

计算机真正处理:

是:

byte数组。


ArrayBuffer:

就是:

连续内存区域。

例如:


let buffer =
new ArrayBuffer(5)

内存:


+----+----+----+----+----+
|00  |00  |00  |00  |00  |
+----+----+----+----+----+

文件:


avatar.jpg

读取:

变成:


FF D8 FF E0 ...

存储:

ArrayBuffer。


3.6 读取整个文件

简单实现:


export function readFile(
 uri:string
):ArrayBuffer{


 let stat =
 fileIo.statSync(uri)


 let size =
 stat.size



 let buffer =
 new ArrayBuffer(size)



 let fd =
 fileIo.openSync(
 uri,
 fileIo.OpenMode.READ_ONLY
 )


 fileIo.readSync(
 fd.fd,
 buffer
 )


 fileIo.closeSync(
 fd
 )


 return buffer

}

调用:


let buffer =
readFile(uri)

得到:


ArrayBuffer

但是!

这个方法:

只适合:

小文件。

例如:

头像:


200KB

可以。


3.7 为什么大文件不能 readAll?

假设:

视频:


2GB

执行:


new ArrayBuffer(
2*1024*1024*1024
)

手机内存:

可能:


8GB

但是:

系统还需要:

  • ArkUI
  • JS Runtime
  • 图片缓存

结果:

容易:


OOM

企业方案:

流式读取。


3.8 分块读取文件

例如:

视频:

500MB

切:


500MB


↓

1MB

1MB

1MB

...

500次

代码:


const CHUNK_SIZE =
1024 * 1024

1MB。


读取:


function readChunk(
 fd:number,
 position:number
){


 let buffer =
 new ArrayBuffer(
 CHUNK_SIZE
 )


 fileIo.readSync(
 fd,
 buffer,
 {
   offset:position
 }
 )


 return buffer

}

上传:


chunk1

↓

chunk2

↓

chunk3

↓

chunk4

这就是:

后面:

OSS Multipart Upload

基础。


3.9 ArrayBuffer 与 Uint8Array

很多 API:

需要:

Uint8Array。

例如:

MD5:


hash.update(
 Uint8Array
)

转换:


let uint8 =
new Uint8Array(
 buffer
)

关系:


ArrayBuffer

      |
      |
      ↓

Uint8Array


区别:

ArrayBuffer

负责:

存储内存。

Uint8Array

负责:

操作每一个 byte。

例如:

修改:


uint8[0]=255

3.10 文件转 Base64

一些接口:

例如:

AI识别:

要求:


{
 image:"base64..."
}

转换:

流程:


Uri

↓

ArrayBuffer

↓

Uint8Array

↓

Base64

代码:


function bufferToBase64(
 buffer:ArrayBuffer
){


 let bytes =
 new Uint8Array(buffer)



 let binary=""


 for(
 let i=0;
 i<bytes.length;
 i++
 ){

 binary +=
 String.fromCharCode(
 bytes[i]
 )

 }


 return encode(
 binary
 )


}

但是:

注意!

Base64 会膨胀:

约:


文件大小

×

1.33

例如:

图片:


3MB

Base64:


4MB

所以:

普通上传:

不要 Base64。

推荐:

multipart/form-data。


3.11 Multipart/form-data 原理

浏览器上传文件:

其实:

也是:

multipart。

格式:


POST /upload


Content-Type:
multipart/form-data;
boundary=xxx



------xxx

Content-Disposition:
form-data;
name="file";
filename="a.png"


(binary)

------xxx--

一个请求:

包含:

多个字段。

例如:


userId

token

file

企业后台:

SpringBoot:


@PostMapping("/upload")
public String upload(
 MultipartFile file
){

}

接收:

就是这里。


3.12 HarmonyOS HTTP上传结构

HarmonyOS:


import http
from '@ohos.net.http'

创建:


let request =
http.createHttp()

请求:


request.request(
 url,
 {
  method:
  http.RequestMethod.POST,


  header:{


  },


  extraData:data

 }
)

但是:

data是什么?

这里就是:

关键。


普通 JSON:


{
name:"test"
}

不能传文件。

文件:

必须:

二进制。

第四章 HarmonyOS NEXT HTTP Multipart 文件上传完整实现

4.1 为什么企业上传优先选择 Multipart?

文件上传常见方案:

方案适用场景推荐程度
Base64 JSON小图片、AI接口⭐⭐
FormData Multipart普通文件上传⭐⭐⭐⭐⭐
OSS直传大文件、视频⭐⭐⭐⭐⭐
分片上传超大文件⭐⭐⭐⭐⭐

企业后台最常见:


客户端

    |
    |
multipart/form-data

    |
    |

业务服务器

    |
    |

OSS

或者:


客户端

    |
    |
OSS直传

    |
    |

业务服务器保存URL

4.2 Multipart 请求结构

一个文件上传请求:


POST /api/file/upload HTTP/1.1

Host: api.xxx.com

Content-Type:
multipart/form-data;
boundary=HarmonyBoundary001


--HarmonyBoundary001

Content-Disposition:
form-data;
name="userId"


10001


--HarmonyBoundary001

Content-Disposition:
form-data;
name="file";
filename="avatar.png"

Content-Type:image/png


<二进制文件>


--HarmonyBoundary001--

这里有三个关键:

1. boundary

分隔符。

例如:


----HarmonyBoundary001

用于告诉服务器:

哪里开始字段。

哪里结束字段。


2. 文件 Header

必须:


Content-Disposition

告诉服务器:

字段名称。

文件名称。


3. 二进制内容

不能:


file://xxx.jpg

必须:


FF D8 FF E0 ...

4.3 HarmonyOS HTTP模块

引入:


import {
 http
} from '@kit.NetworkKit'

创建请求:


let httpRequest =
http.createHttp()

基础请求:


let response =
await httpRequest.request(
    url,
    {

        method:
        http.RequestMethod.POST,


        header:{


        },


        extraData:data

    }
)

4.4 创建 MultipartBuilder

企业项目不要每次拼字符串。

封装:

目录:


upload

 └── MultipartBuilder.ets

代码:


export class MultipartBuilder {


private boundary:string



private chunks:ArrayBuffer[]=[]



constructor(){


 this.boundary =
 "----HarmonyBoundary"
 +
 Date.now()


}




getBoundary(){

 return this.boundary

}



}

boundary示例:


----HarmonyBoundary172233333

保证唯一。


4.5 添加普通参数

例如:

上传接口需要:


userId

token

folder

添加:


addField()

实现:


addField(
 name:string,
 value:string
){


let text =

`--${this.boundary}
Content-Disposition:
form-data;
name="${name}"


${value}
`


}

最终:


userId

↓

10001

4.6 添加文件

核心方法:


addFile()

参数:


addFile(

 fieldName:string,

 fileName:string,

 mimeType:string,

 data:ArrayBuffer

)

实现:


addFile(
 fieldName:string,
 fileName:string,
 mime:string,
 buffer:ArrayBuffer
){


let header =

`--${this.boundary}
Content-Disposition:
form-data;
name="${fieldName}";
filename="${fileName}"

Content-Type:${mime}


`


}

然后拼接:


header

+

binary

4.7 ArrayBuffer拼接

Multipart:

实际上:

多个二进制块组合。

例如:


Header

+

图片

+

End

所以需要:

ArrayBuffer concat。


工具:


export class BufferUtil {


static concat(
 buffers:ArrayBuffer[]
):ArrayBuffer{


 let total=0


 buffers.forEach(
 b=>{
 total+=b.byteLength
 }
 )


 let result =
 new Uint8Array(total)



 let offset=0


 buffers.forEach(
 b=>{


 let arr =
 new Uint8Array(b)


 result.set(
 arr,
 offset
 )


 offset+=arr.length


 })


 return result.buffer


}


}

4.8 完整 MultipartBuilder

最终:


export class MultipartBuilder {


private boundary:string


private buffers:ArrayBuffer[]=[]



constructor(){


 this.boundary=
 "----HarmonyBoundary"
 +
 Date.now()

}



addField(
name:string,
value:string
){


let content=

`--${this.boundary}

Content-Disposition:
form-data;
name="${name}"


${value}

`


this.buffers.push(
 TextEncoderUtil.encode(content)
)


}




addFile(
name:string,
filename:string,
mime:string,
buffer:ArrayBuffer
){



let header=

`--${this.boundary}

Content-Disposition:
form-data;
name="${name}";
filename="${filename}"

Content-Type:${mime}



`



this.buffers.push(
 encode(header)
)


this.buffers.push(
 buffer
)


}



build(){


let end=

`
--${this.boundary}--
`


this.buffers.push(
 encode(end)
)



return BufferUtil.concat(
 this.buffers
)


}


}

4.9 HttpUploader实现

创建:


HttpUploader.ets

代码:


export class HttpUploader
implements Uploader {



async upload(
 task:UploadTask
):Promise<string>{



let builder =
new MultipartBuilder()



builder.addField(
"userId",
"10001"
)



let buffer =
await FileUtil.read(
 task.file.uri
)



builder.addFile(
"file",
task.file.name,
task.file.mimeType,
buffer
)



let body =
builder.build()



let request =
http.createHttp()



let response =
await request.request(


"https://api.xxx.com/upload",


{

method:
http.RequestMethod.POST,


header:{


"Content-Type":

"multipart/form-data;
boundary="
+
builder.getBoundary()



},



extraData:body



}


)



return response.result

}


}

4.10 上传进度监听

普通:


request()

无法满足。

企业使用:


uploadTask

监听:


bytesSent

totalBytes

计算:


progress=

bytesSent / totalBytes *100

例如:


task.progress=
65

ArkUI:


Progress({

value:task.progress

})

4.11 Token认证

企业接口:

必须:


Authorization

例如:


Authorization:

Bearer eyJxxxx

代码:


header:{


"Authorization":

"Bearer "
+
token



}

不要:

把 token 写死。

推荐:


TokenManager

↓

Preferences

↓

自动刷新

4.12 文件类型校验

客户端:

第一层校验。

例如:

只允许图片:


const allow=[

"image/png",

"image/jpeg",

"image/webp"

]

判断:


if(
!allow.includes(
file.mimeType
)
){

throw Error(
"文件类型错误"
)

}

但是注意:

客户端校验不安全。

服务器必须再次校验。


4.13 文件大小限制

例如:

头像:

最大:


5MB

视频:

最大:


500MB

代码:


if(
file.size >
5*1024*1024
){

throw Error(
"文件过大"
)

}

4.14 HTTP上传架构优化

普通:


页面

↓

http.request

生产:


页面

↓

UploadManager

↓

HttpUploader

↓

HttpClient

↓

Server

4.15 HTTP上传存在的问题

Multipart解决了:

普通文件上传。

但是:

大文件:

例如:


1GB视频

依然存在问题:

  1. 内存占用

new ArrayBuffer(1GB)
  1. 网络中断

重新上传
  1. 无法暂停
  2. 无法续传

第五章 HarmonyOS NEXT 大文件分片上传架构

在企业应用中,普通 Multipart 上传只能解决:


几 KB

几 MB

几十 MB

级别文件。

但是:

真实业务经常出现:


视频:
800MB

直播录像:
5GB

企业资料:
2GB

如果仍然:


整个文件

↓

一次HTTP请求

↓

服务器

会出现严重问题。


5.1 为什么需要分片上传?

假设:

视频:


1GB

网络:


10MB/s

理论:


100秒

但是现实:

  • 用户移动网络波动
  • WiFi切换
  • APP进入后台
  • 系统杀进程

例如:

上传:


已经完成:

850MB / 1024MB

突然:


网络断开

普通上传:


重新开始

损失:

850MB流量。


分片上传:

变成:


文件

↓

Chunk1
Chunk2
Chunk3
Chunk4
...

Chunk1024

每个:


1MB

上传:


Chunk1   √

Chunk2   √

Chunk3   √

Chunk4   ×

Chunk5   √

失败:

只重新:


Chunk4

5.2 分片上传整体流程

企业级流程:


客户端


    |
    |
计算文件信息

    |
    |

请求上传初始化

    |
    |

服务器返回 UploadId

    |
    |

文件切片

    |
    |

并发上传 Chunk

    |
    |

记录每个Chunk状态

    |
    |

全部成功

    |
    |

通知服务器合并

    |
    |

返回文件URL

5.3 文件切片原理

假设:

文件:


100MB

设置:


Chunk大小:

5MB

切:


100 / 5

=20片

结果:


chunk0

0MB - 5MB


chunk1

5MB -10MB


chunk2

10MB -15MB


...


chunk19

95MB-100MB

5.4 Chunk模型设计

创建:


ChunkInfo.ets

代码:


export class ChunkInfo {


 /**
  * 分片编号
  */
 index:number=0



 /**
  * 开始位置
  */
 offset:number=0



 /**
  * 分片大小
  */
 size:number=0



 /**
  * 上传状态
  */
 uploaded:boolean=false



 /**
  * ETag
  */
 etag:string=""


}

例如:

第10片:


index:

10


offset:

52428800


size:

1048576


uploaded:

true

5.5 文件切片工具

创建:


ChunkUtil.ets

定义:


const CHUNK_SIZE =
1024 * 1024

1MB。


生成分片:


export class ChunkUtil {



static createChunks(
fileSize:number
):ChunkInfo[]{



let chunks:ChunkInfo[]=[]



let count =
Math.ceil(
fileSize / CHUNK_SIZE
)



for(
let i=0;
i<count;
i++
){



let chunk =
new ChunkInfo()



chunk.index=i


chunk.offset=
i*CHUNK_SIZE



chunk.size=
Math.min(
CHUNK_SIZE,
fileSize -
chunk.offset
)



chunks.push(chunk)



}



return chunks


}



}

例如:

文件:


10MB

输出:


[
{
index:0,
offset:0,
size:1MB
},

{
index:1,
offset:1MB,
size:1MB
}

...

]

5.6 为什么 Chunk 不建议太小?

例如:

100MB文件。

方案1:


1000片

每片100KB

问题:

HTTP请求:

1000次。

消耗:

  • TCP连接
  • Header
  • TLS握手

方案2:


20片

每片5MB

效率更高。


企业经验:

文件类型Chunk大小
图片不用分片
100MB以内1MB
视频5MB~10MB
超大文件10MB~50MB

5.7 UploadId设计

为什么需要 UploadId?

因为:

服务器必须知道:

这些 Chunk:

属于哪个文件。

例如:

上传视频:


video.mp4

初始化:

客户端:


POST /upload/init

发送:


{
"name":"video.mp4",
"size":102400000,
"md5":"xxxx"
}

服务器返回:


{
"uploadId":

"abc123"
}

后续:

每个分片:

带:


uploadId

例如:


POST /upload/chunk

uploadId=abc123

chunkIndex=1

5.8 分片上传接口设计

推荐三个接口。


1. 初始化上传

请求:


POST

/upload/init

参数:


{
"name":"test.mp4",

"size":10000000,

"md5":"xxxxx"
}

返回:


{
"uploadId":"10001"
}

2. 上传分片

请求:


POST

/upload/chunk

参数:


uploadId

chunkIndex

chunkData

返回:


{
"chunkIndex":1,

"etag":"xxxx"
}

3. 合并文件

请求:


POST

/upload/merge

参数:


{
"uploadId":"10001",

"chunks":[

{
"index":0,
"etag":"xxx"
}

]

}

5.9 HarmonyOS读取指定分片

关键:

不要:


readAll()

而是:


offset

+

length

代码:


export function readChunk(

uri:string,

offset:number,

size:number

):ArrayBuffer{



let fd =
fileIo.openSync(
uri,
fileIo.OpenMode.READ_ONLY
)



let buffer =
new ArrayBuffer(size)



fileIo.readSync(

fd.fd,

buffer,

{

offset:offset

}

)



fileIo.closeSync(fd)



return buffer


}

例如:

读取:


第5片

位置:


5MB

大小:


1MB

返回:


ArrayBuffer(1MB)

5.10 分片上传器设计

创建:


ChunkUploader.ets

结构:


export class ChunkUploader {


async upload(

task:UploadTask

){



//1 创建分片

let chunks =
ChunkUtil.createChunks(
task.file.size
)



//2 获取UploadId

let uploadId =
await this.init(task)



//3 上传分片


for(
let chunk of chunks
){


await this.uploadChunk(
uploadId,
chunk
)


}



//4 合并


return await
this.merge(uploadId)


}



}

5.11 并发上传优化

上面的代码:

串行:


chunk1

↓

chunk2

↓

chunk3

速度慢。

企业:

控制并发。

例如:

同时:


3个chunk

效果:


chunk1  上传

chunk2  上传

chunk3  上传


完成后:

chunk4加入

设计:


TaskQueue

例如:


const MAX_UPLOAD=3

队列:


while(queue.length){


 let tasks =
 queue.splice(
 0,
 MAX_UPLOAD
 )


 await Promise.all(
 tasks.map(
 item=>upload(item)
 )
 )


}

5.12 分片状态保存

断点续传核心:

保存:

哪些已经完成。

例如:

本地:


preferences

{

uploadId:

"abc123",

chunks:

[

0,

1,

2,

5

]

}

重新打开APP:

查询:


chunk 0 已完成

chunk 1 已完成

chunk 2 已完成

chunk 3 未完成

继续:


chunk3

第六章 HarmonyOS NEXT 断点续传完整实现

大文件上传最重要的能力:

不是上传。

而是:

上传失败以后,能够继续上传。

企业 App:

  • 云盘
  • 视频平台
  • IM
  • 企业网盘
  • OA系统

几乎都必须支持:

断点续传。


6.1 什么是断点续传?

普通上传:


文件

↓

HTTP

↓

服务器

失败:


重新开始

断点续传:


文件

↓

切片

↓

chunk0  √

chunk1  √

chunk2  √

chunk3  ×


网络恢复


继续:

chunk3

核心思想:

保存:


已经上传成功的部分

6.2 断点续传整体流程

完整流程:


第一次上传


选择文件

    |

计算文件MD5

    |

请求初始化

    |

服务器返回 uploadId

    |

创建chunk

    |

上传chunk

    |

保存chunk状态

    |

完成

异常退出:


重新打开APP


选择同一个文件


    |

计算MD5


    |

查询本地上传记录


    |

恢复uploadId


    |

查询服务器已完成chunk


    |

继续上传剩余chunk

6.3 为什么需要文件MD5?

问题:

如何判断:

是不是同一个文件?

例如:

用户上传:


video.mp4

上传50%。

第二天:

又选择:


video.mp4

系统需要知道:

是不是同一个。

不能靠:


文件名

因为:

两个文件可能:


test.mp4

内容不同

使用:

MD5:


文件内容指纹

例如:

文件A:


video.mp4

MD5:

a8237xxx

文件B:


video.mp4

MD5:

b9288xxx

不同。


6.4 HarmonyOS 文件MD5计算

引入:


import {
 cryptoFramework
}
from '@kit.CryptoArchitectureKit'

但是:

大文件不能:

一次读取。

错误:


let buffer =
readAll(file)

例如:

2GB:

直接爆内存。


正确:

流式计算。

流程:


文件


↓

chunk1

↓

MD5.update()


↓

chunk2

↓

MD5.update()


↓

最终digest

6.5 HashUtil封装

创建:


HashUtil.ets

代码:


export class HashUtil {


static async md5(
 uri:string
):Promise<string>{


 let md5 =
 cryptoFramework
 .createMd(
 "MD5"
 )


 let file =
 fileIo.openSync(
 uri
 )



 let bufferSize =
 1024*1024



 while(true){


 let buffer =
 new ArrayBuffer(
 bufferSize
 )


 let length =
 fileIo.readSync(
 file.fd,
 buffer
 )


 if(length<=0){

 break

 }



 await md5.update(
 {
 data:
 new Uint8Array(buffer,0,length)
 }
 )


 }



 let result =
 await md5.digest()



 return this.hex(
 result.data
 )


}




static hex(
data:ArrayBuffer
){

 let arr =
 new Uint8Array(data)


 return Array.from(
 arr
 )
 .map(
 b=>
 b.toString(16)
 .padStart(2,'0')
 )
 .join('')


}



}

得到:


MD5:

e10adc3949ba59abbe56e057f20f883e

6.6 上传记录模型设计

需要保存:


文件信息

uploadId

已完成chunk

创建时间

状态

创建:


UploadRecord.ets

代码:


export class UploadRecord {


fileMd5:string=""


uploadId:string=""


fileName:string=""


fileSize:number=0


uploadedChunks:number[]=[]


createTime:number=0


}

例如:


{
"fileMd5":
"abc123",

"uploadId":
"upload001",

"uploadedChunks":[
0,
1,
2,
3
]

}

6.7 本地保存上传状态

HarmonyOS:

推荐:

Preferences。

导入:


import {
 preferences
}
from '@kit.ArkData'

创建:


let store =
await preferences.getPreferences(
context,
{
name:"upload"
}
)

保存:


await store.put(
 fileMd5,
 JSON.stringify(record)
)

await store.flush()

读取:


let data =
await store.get(
 fileMd5,
''
)

6.8 ResumeManager设计

创建:


ResumeManager.ets

职责:

  • 保存任务
  • 查询任务
  • 删除任务
  • 恢复任务

代码:


export class ResumeManager {



async save(
record:UploadRecord
){


let store =
await this.getStore()



await store.put(
record.fileMd5,
JSON.stringify(record)
)



await store.flush()


}




async get(
md5:string
){


let store =
await this.getStore()



let value =
await store.get(
md5,
''
)



if(value){

return JSON.parse(
value as string
)

}


return null


}



}

6.9 上传过程中保存状态

上传一个chunk:


async uploadChunk(
chunk:ChunkInfo
){

 await request(chunk)



 record.uploadedChunks
 .push(
 chunk.index
 )


 await resumeManager.save(
 record
 )

}

关键:

不是全部完成才保存。

而是:

每成功一个:

保存一次。


6.10 APP异常退出恢复

场景:

上传:


100个chunk

完成80个

APP:


被系统杀死

重新打开:

流程:


启动APP

↓

读取上传记录

↓

找到未完成任务

↓

展示:

继续上传?

↓

恢复

查询:


let records =
await resumeManager.list()

展示:


视频上传

80%

[继续]

6.11 网络变化监听

移动端:

网络经常变化:


WiFi

↓

4G

↓

无网络

上传应该暂停。


HarmonyOS:

监听网络状态。


import {
 connection
}
from '@kit.NetworkKit'

获取:


let net =
connection.createNetConnection()

监听:


net.on(
'netAvailable',
()=>{

 resume()

}
)

无网络:


pause()

6.12 上传暂停设计

用户点击:


暂停

不是取消。

区别:

暂停:

保存状态。

取消:

删除状态。


Task:

增加:


paused:boolean=false

上传循环:


while(chunks){


if(task.paused){

break

}


upload()

}

6.13 上传取消设计

取消:


删除服务器临时文件

删除本地记录

释放资源

接口:


cancelUpload(
uploadId
)

服务器:


delete uploadId

6.14 秒传设计

大型云盘:

为什么上传几秒完成?

因为:

服务器已经存在。

流程:


计算MD5


↓

服务器查询


↓

存在


↓

直接返回URL

接口:


POST

/upload/check

参数:


{
"md5":"abc123",

"size":100000
}

返回:

存在:


{
exists:true,

url:"https://xxx.com/a.mp4"
}

不存在:


{
exists:false,

uploadId:"xxx"
}

6.15 断点续传核心代码整合

最终:


async upload(
file:UploadFile
){



let md5 =
await HashUtil.md5(
file.uri
)



let exists =
await api.check(
md5
)



if(exists){

return exists.url

}



let uploadId =
await api.init(
file
)



let chunks =
ChunkUtil.createChunks(
file.size
)



for(
let chunk of chunks
){



if(
record.uploadedChunks
.includes(
chunk.index
)
){

continue

}



let data =
FileUtil.readChunk(
file.uri,
chunk.offset,
chunk.size
)



await api.uploadChunk(
uploadId,
chunk.index,
data
)



record.uploadedChunks
.push(
chunk.index
)



await resume.save(
record
)


}



return api.merge(
uploadId
)



}

6.16 企业级断点续传优化点

生产环境还需要:

1. 上传速度限制

避免:


上传占满网络

增加:


speedLimit

2. 后台上传

APP进入后台:

继续。

使用:


BackgroundTask

3. 上传任务恢复

APP启动:

自动扫描:


未完成任务

4. 服务端校验

客户端:

不能相信。

服务器:

校验:

  • MD5
  • Size
  • Chunk数量
  • ETag

第七章 HarmonyOS NEXT 图片上传优化实战

图片上传是移动端最常见的文件上传场景。

但是:

图片上传 ≠ 选择图片 + 上传。

企业级图片上传流程:


用户选择图片

↓

读取Uri

↓

解析图片信息

↓

获取EXIF

↓

旋转纠正

↓

尺寸检测

↓

压缩

↓

格式转换

↓

生成缩略图

↓

计算MD5

↓

上传OSS

↓

返回URL

7.1 为什么图片一定要压缩?

手机照片越来越大。

例如:

华为旗舰手机:


RAW照片

20MB+

普通拍照:


4000 x 3000

8MB

但是业务展示:

可能只需要:


1080 x 1080

300KB

如果不压缩:

100万用户:

每天上传:


100万 × 8MB

存储:


8TB/天

CDN:

成本巨大。


7.2 图片处理架构

企业图片Pipeline:


                 Uri


                  |

                  ↓


            ImageSource


                  |

                  ↓


             PixelMap


                  |

        ----------------

        |              |

        ↓              ↓


    Resize        Compress


        |

        ↓


       JPEG


        |

        ↓


      Upload

7.3 HarmonyOS 图片核心对象

HarmonyOS 图片处理:

核心:


ImageSource

PixelMap

ImagePacker

ImageSource

作用:

读取图片源。

例如:


jpg

png

webp

PixelMap

作用:

图片内存对象。

可以:

  • 缩放
  • 裁剪
  • 旋转
  • 修改像素

ImagePacker

作用:

编码。

例如:

PixelMap:

JPEG


7.4 创建图片解析工具

目录:


common/image

    ImageUtil.ets

导入:


import {
 image
}
from '@kit.ImageKit'

7.5 Uri读取ImageSource

代码:


export class ImageUtil {


static async createSource(
uri:string
){


let source =
image.createImageSource(
uri
)


return source


}


}

得到:


ImageSource

7.6 获取图片宽高

为什么需要?

例如:

用户上传:


6000x4000

需要判断:

是否压缩。


代码:


let info =
await source.getImageInfo()


console.log(
info.size.width
)


console.log(
info.size.height
)

返回:


{
width:6000,
height:4000
}

7.7 创建 PixelMap

ImageSource:

不是图片。

需要转换。

代码:


let pixelMap =
await source.createPixelMap()

现在:


ImageSource

↓

PixelMap

PixelMap:

可以操作:


像素

尺寸

方向

颜色

7.8 图片尺寸压缩

目标:

最大宽:


1080

例如:

原图:


4000x3000

比例:


4:3

计算:

宽:


1080

高:


810

缩放参数:


let options =
{

size:{

width:1080,

height:810

}

}

创建:


let pixelMap =
await source.createPixelMap(
options
)

7.9 自动计算缩放比例

不要写死。

工具:


export function calculateSize(
width:number,
height:number
){

const MAX=1080


if(
width<=MAX &&
height<=MAX
){

return {
width,
height
}

}


let scale =
MAX /
Math.max(
width,
height
)


return {

width:
Math.floor(
width*scale
),


height:
Math.floor(
height*scale
)

}


}

例如:

输入:


4000x3000

输出:


1080x810

输入:


800x600

输出:


800x600

无需压缩。


7.10 图片质量压缩

尺寸压缩:

减少:

像素数量。

质量压缩:

减少:

编码质量。

例如:

JPEG:


100%

↓

80%

↓

60%

企业推荐:

头像:


80%

商品图片:


85%

聊天图片:


70%

7.11 ImagePacker编码

PixelMap:

不能直接上传。

需要:

编码。

流程:


PixelMap

↓

ImagePacker

↓

ArrayBuffer

↓

HTTP/OSS

代码:


let packer =
image.createImagePacker()

配置:


let options = {

format:"image/jpeg",

quality:80

}

编码:


let buffer =
await packer.packToData(
pixelMap,
options
)

得到:


ArrayBuffer

可以上传:


upload(buffer)

7.12 图片压缩完整封装

创建:


CompressUtil.ets

代码:


export class CompressUtil {



static async compressImage(
uri:string
):Promise<ArrayBuffer>{



//1 创建source

let source =
image.createImageSource(
uri
)



//2 获取尺寸

let info =
await source.getImageInfo()



let size =
calculateSize(
info.size.width,
info.size.height
)



//3 创建PixelMap

let pixelMap =
await source.createPixelMap(
{

size

}
)





//4 编码


let packer =
image.createImagePacker()



let buffer =
await packer.packToData(
pixelMap,
{

format:
"image/jpeg",

quality:80

}

)



return buffer



}


}

7.13 上传前Pipeline设计

不要:


选择图片

↓

上传

应该:


UploadManager


 |

 ↓


FileProcessor


 |

 --------------------

 |        |          |

 ↓        ↓          ↓


压缩    MD5       校验


 |

 ↓


Uploader

定义:


interface Processor{


process(
file:UploadFile
)
:Promise<UploadFile>


}

图片处理:


class ImageProcessor
implements Processor{


async process(
file
){


let buffer =
await CompressUtil
.compressImage(
file.uri
)


file.buffer=buffer


return file


}


}

7.14 EXIF方向问题

很多图片:

看起来正常。

但是:

上传后:

旋转90度。

原因:

手机照片:

实际像素:


横向保存

但是:

EXIF:

记录:


应该旋转

例如:

照片:


width:

3000


height:

4000

但是:

orientation:


rotate90

上传服务器:

如果忽略:

图片倒置。


处理:

流程:


读取EXIF


↓

orientation


↓

PixelMap.rotate


↓

重新编码

7.15 图片裁剪

常见:

头像。

需求:


正方形

中心裁剪

例如:

原图:


4000x3000

裁剪:


3000x3000

算法:

计算:


left:

(width-height)/2

然后:

PixelMap:

裁剪。


7.16 缩略图生成

列表:

不要加载原图。

例如:

聊天列表:


100张图片

每张:

5MB。

内存:

爆炸。


生成:

thumbnail:


原图

5MB


↓

缩略图

30KB

上传两个:


originalUrl

thumbnailUrl

7.17 微信朋友圈图片方案

类似:

微信:

选择9张。

流程:


9张图片


↓

并行压缩


↓

生成缩略图


↓

上传缩略图


↓

上传原图


↓

返回URL

并发控制:


MAX=3

避免:

一次压缩9张导致:

内存暴涨。


7.18 图片上传最终架构


PhotoViewPicker


        |

        ↓


UploadFile


        |

        ↓


ImageProcessor


        |

        ↓


PixelMap


        |

        ↓


ImagePacker


        |

        ↓


ArrayBuffer


        |

        ↓


OSS Multipart


        |

        ↓


URL

7.19 企业图片上传优化清单

生产环境建议:

优化作用
尺寸压缩降低体积
质量压缩降低存储
EXIF处理避免旋转
缩略图提升列表性能
MD5秒传
OSS CDN加速
WebP减少流量
分片大图支持

8.1 OSS直传核心思想

APP:

不保存:


AccessKey

SecretKey

因为:

一旦泄露:

攻击者可以:

  • 上传垃圾文件
  • 删除文件
  • 下载私密数据

正确:

服务器生成:


STS临时Token

返回:


{
"accessKeyId":"xxx",

"securityToken":"xxx",

"expiration":"xxx",

"endpoint":"oss.xxx.com"
}

APP:

拿这个Token:

上传。


8.2 OSS上传安全模型

完整链路:


用户


 |

 ↓


HarmonyOS APP


 |

 ↓


请求上传权限


 |

 ↓


业务服务器


 |

 ↓


STS服务


 |

 ↓


返回临时凭证


 |

 ↓


APP上传OSS


 |

 ↓


OSS


核心原则:

永远不要把永久密钥放客户端。


8.3 STS临时授权流程

流程:

第一步

APP:

请求:


POST /upload/token

参数:


{
"fileType":"image",

"size":1024000

}

第二步

业务服务器:

调用:


STS AssumeRole

生成:


{
"AccessKeyId":

"TMP.xxx",

"AccessKeySecret":

"xxx",

"SecurityToken":

"xxx"

}

第三步

返回APP:


{
"endpoint":

"https://oss-cn.xxx.com",


"bucket":

"my-bucket",


"objectKey":

"user/avatar/a.png",


"token":

"xxx"

}

8.4 OSS文件路径设计

不要:


/avatar.png

生产环境:

需要目录规划。

推荐:


bucket

 |

 ├── user

 |     └── avatar

 |          └── 10001

 |              └── xxx.jpg


 ├── video

 |      └── 2026

 |          └── 07


 └── document

规则:


业务类型

+

用户ID

+

日期

+

随机ID

例如:


user/avatar/10001/20260731/a8sd91.jpg

优势:

  • 防止文件覆盖
  • 查询方便
  • CDN缓存友好

8.5 HarmonyOS OSS上传模块设计

目录:


upload

 |

 ├── OssUploader.ets

 ├── OssTokenManager.ets

 ├── OssRequest.ets

 └── UploadManager.ets

架构:


UploadManager


        |

        ↓


OssUploader


        |

        ↓


OssRequest


        |

        ↓


OSS

8.6 OssConfig模型

创建:


export class OssConfig {


endpoint:string=""


bucket:string=""


objectKey:string=""


accessKeyId:string=""


accessKeySecret:string=""


securityToken:string=""


}

服务器返回:

转换:


let config =
new OssConfig()

config.endpoint =
data.endpoint

config.bucket =
data.bucket

8.7 OSS HTTP上传原理

OSS本质:

也是HTTP。

请求:


PUT

/bucket/object

Header:


Content-Type:image/jpeg

x-oss-security-token:xxx

Authorization:xxx

Body:


图片二进制

所以:

HarmonyOS:

最终还是:


ArrayBuffer

↓

HTTP PUT

↓

OSS

8.8 OSS签名机制

OSS需要:

证明:

"这个请求是合法用户发起"

所以:

需要签名。

公式:


Signature =
HMAC-SHA1(
AccessKeySecret,
StringToSign
)

StringToSign:

例如:


PUT


image/jpeg


Fri,31 Jul 2026


/xxx/avatar.png

生成:


Authorization

8.9 HarmonyOS OSS Signature实现

创建:


OssSigner.ets

伪代码:


export class OssSigner {


static sign(
secret:string,
content:string
){


let hmac =
crypto.hmacSha1(
secret,
content
)


return Base64.encode(
hmac
)


}


}

最终:

Header:


{

Authorization:

"OSS "

+

accessKeyId

+

":"

+

signature

}

8.10 OSS上传实现

核心:


async upload(
file:UploadFile,
config:OssConfig
){



let request =
http.createHttp()



let response =
await request.request(


config.endpoint
+
"/"
+
config.objectKey,


{


method:
http.RequestMethod.PUT,


header:{


"Content-Type":

file.mimeType,


"x-oss-security-token":

config.securityToken



},


extraData:

file.buffer



}



)



return response


}

8.11 OSS大文件Multipart Upload

普通:

PUT:

适合:


几十MB

大文件:

使用:


Multipart Upload

流程:


初始化


↓

UploadId


↓

上传Part


↓

保存ETag


↓

Complete

8.12 OSS Multipart流程

1 初始化

请求:


POST

?uploads

返回:


<UploadId>

xxxx

</UploadId>

2 上传Part

每片:


PartNumber=1

Data=chunk

返回:


ETag

3 合并

提交:


<Part>

<PartNumber>1</PartNumber>

<ETag>xxx</ETag>

</Part>

OSS:

自动合并。


8.13 HarmonyOS OSS分片上传架构


File


 |

 ↓


ChunkManager


 |

 ↓


PartUploader


 |

 ↓


OSS UploadPart


 |

 ↓


CompleteMultipart

8.14 OSS上传进度

计算:


progress =
uploadedSize /
totalSize

例如:


uploaded:

50MB


total:

100MB

结果:


50%

UI:


Progress({

value:

progress*100

})

8.15 OSS失败重试机制

移动端:

必须重试。

例如:

网络失败:


Part3

失败

不要全部重新上传。


策略:


第一次失败

等待1秒


第二次失败

等待3秒


第三次失败

等待10秒

指数退避:


delay =
2^retry * 1000

8.16 OSS上传状态管理

保存:


{

uploadId:

"xxx",


parts:[

{

number:1,

etag:"xxx"

}

]

}

存储:

HarmonyOS:


Preferences

或者:


RelationalStore数据库

8.17 企业完整上传架构

最终:


                APP


                 |

                 ↓


          UploadManager


                 |

      ----------------------

      |                    |


 HTTPUploader        OssUploader


      |                    |


业务服务器              OSS


支持:

  • 普通上传
  • OSS直传
  • 分片上传
  • 断点续传
  • 秒传
  • 进度监听
  • 暂停恢复

第九章 HarmonyOS NEXT 文件上传企业级 UploadManager 源码设计

在大型 HarmonyOS NEXT 应用中,文件上传不能散落在页面代码中。

错误方式:


Button(){

 onClick(async()=>{

   let result =
   await http.request(
      url
   )

 })

}

问题:

  • 页面耦合网络
  • 无法暂停
  • 无法恢复
  • 无法统一管理
  • 无法统计上传状态

企业级设计:


                 UI层


                  |

                  ↓


             UploadManager


                  |

      -------------------------


      |                       |


HttpUploader            OssUploader


      |                       |


 HTTP服务器              OSS

9.1 UploadManager职责

UploadManager 是上传中心。

负责:

任务管理


创建上传任务

删除任务

查询任务

生命周期


开始

暂停

继续

取消

完成

失败

调度


同时上传几个文件

哪个优先

哪个等待

状态通知

例如:

页面:


上传中 65%

来自:


UploadManager

9.2 上传任务模型设计

创建:


model

 └── UploadTask.ets

代码:


export class UploadTask {


id:string=""


file:UploadFile|null=null



status:
UploadStatus=
UploadStatus.WAITING



progress:number=0



uploadedSize:number=0



totalSize:number=0



retry:number=0



createTime:number=0



}

9.3 上传状态机设计

上传不是简单:


true

false

需要状态。


定义:


export enum UploadStatus {


WAITING="waiting",


UPLOADING="uploading",


PAUSED="paused",


SUCCESS="success",


FAILED="failed",


CANCELLED="cancelled"


}

状态变化:



WAITING


   |

   ↓


UPLOADING


   |

 ----------------

 |              |


SUCCESS       FAILED

暂停:



UPLOADING

    |

    ↓

PAUSED

9.4 UploadTask状态流转

禁止:

非法状态。

例如:


SUCCESS

↓

UPLOADING

不允许。


设计:


class TaskStateMachine{


transition(
task,
next
){



if(
!this.allow(
task.status,
next
)
){

throw Error(
"invalid state"
)

}


task.status=next


}


}

9.5 UploadManager单例设计

整个APP:

一个上传中心。


export class UploadManager {


private static instance:



private tasks:
Map<string,UploadTask>


=new Map()



static getInstance(){



if(!this.instance){

this.instance =
new UploadManager()

}


return this.instance


}



}

使用:


let manager =
UploadManager
.getInstance()

9.6 创建上传任务

方法:


createTask()

代码:


createTask(
file:UploadFile
){



let task =
new UploadTask()



task.id =
uuid()



task.file=file


task.totalSize=
file.size



task.status=
UploadStatus.WAITING



this.tasks.set(
task.id,
task
)



return task


}

返回:


{
id:"task001",

status:"waiting",

progress:0

}

9.7 开始上传

调用:


manager.start(
task.id
)

实现:


async start(
id:string
){



let task =
this.tasks.get(id)



if(!task){

return

}



task.status=
UploadStatus.UPLOADING



await this.execute(
task
)


}

9.8 上传执行器

根据文件类型:

选择:

Uploader。


接口:


export interface Uploader {


upload(
task:UploadTask
)
:
Promise<string>


pause(
task:UploadTask
)


cancel(
task:UploadTask
)



}

实现:


                 Uploader


                    |


      ----------------------------


      |                          |


HttpUploader              OssUploader

9.9 UploadManager调用Uploader

代码:


async execute(
task:UploadTask
){



let uploader =
this.getUploader(
task
)



try{


let url =
await uploader.upload(
task
)



task.status=
UploadStatus.SUCCESS



task.progress=100



task.url=url



this.notify(task)



}catch(e){


task.status=
UploadStatus.FAILED


}

}

9.10 上传队列设计

企业:

不能:

10个视频同时上传。

原因:

  • 占满带宽
  • CPU升高
  • 手机发热

设置:


MAX_RUNNING=3

队列:



任务1  上传


任务2  上传


任务3  上传


任务4  等待


任务5  等待

9.11 TaskQueue实现


export class UploadQueue {


waiting:
UploadTask[]=[]



running:number=0



max:number=3



async add(
task
){


this.waiting.push(task)


this.schedule()


}



schedule(){


while(
this.running<this.max
&&
this.waiting.length
){



let task =
this.waiting.shift()



this.running++



this.start(task)



}



}


}

9.12 上传暂停实现

用户:

点击:


暂停

调用:


manager.pause(
taskId
)

代码:


pause(id:string){



let task =
this.tasks.get(id)



task.status=
UploadStatus.PAUSED



this.uploader.pause(
task
)


}

9.13 上传恢复

继续:


resume(taskId)

流程:



PAUSED


 |

 ↓


读取上传记录


 |

 ↓


找到未完成Chunk


 |

 ↓


继续上传


9.14 上传取消

取消:

不是暂停。

区别:

操作状态保留
暂停PAUSED保留
取消CANCELLED删除

代码:


cancel(id){


let task =
this.tasks.get(id)



task.status=
UploadStatus.CANCELLED



this.uploader.cancel(
task
)



this.tasks.delete(id)



}

9.15 上传事件监听

页面需要:

实时刷新。

例如:


progress:

60%

设计:

EventEmitter。


事件:


upload:start

upload:progress

upload:success

upload:error

upload:cancel

代码:


class UploadEmitter{


listeners=[]



emit(
event,
data
){


this.listeners
.forEach(
fn=>fn(
event,
data
)
)


}


}

9.16 ArkUI绑定上传进度

页面:


@State progress:number=0

监听:


UploadManager
.onProgress(
(task)=>{


this.progress=
task.progress


}

)

UI:


Progress({

value:this.progress

})

效果:



上传中


████████░░

80%

9.17 上传失败自动重试

失败:

不要立即结束。

策略:



第1次失败

↓

1秒


第2次失败

↓

3秒


第3次失败

↓

10秒

代码:


async retry(task){


while(
task.retry<3
){


try{

await upload()

break


}catch(e){


task.retry++


await sleep(
1000*
task.retry
)


}



}


}

9.18 企业级目录结构

推荐:


common


 └── upload


      ├── UploadManager.ets


      ├── UploadTask.ets


      ├── UploadQueue.ets


      ├── UploadStatus.ets


      ├── HttpUploader.ets


      ├── OssUploader.ets


      ├── ResumeManager.ets


      ├── ChunkUploader.ets


      └── UploadEvent.ets

9.19 完整上传链路

最终:


用户选择文件


        ↓


FilePicker


        ↓


FileService


        ↓


UploadManager


        ↓


TaskQueue


        ↓


Uploader


        ↓


HTTP / OSS


        ↓


Progress Event


        ↓


ArkUI刷新


        ↓


完成

第十章 HarmonyOS NEXT 文件上传后台服务与生命周期管理

移动端上传和桌面端最大的区别:

用户不会一直停留在上传页面。

真实场景:

用户上传视频:


开始上传

↓

返回桌面

↓

锁屏

↓

切换APP

↓

重新打开

上传任务仍然应该:

  • 保持状态
  • 控制资源
  • 恢复执行

10.1 为什么需要后台上传?

普通页面上传:


Page

↓

http.request()

↓

上传

问题:

当页面销毁:


Page disappear

↓

对象释放

↓

请求可能终止

例如:

用户:

点击上传视频。

上传:


60%

然后:

按 Home 键。

如果没有后台能力:


上传失败

企业需要:


UI层

↓

上传服务

↓

后台任务

↓

网络请求

10.2 HarmonyOS应用生命周期

ArkUI页面生命周期:


aboutToAppear()

aboutToDisappear()

但是:

它只负责:

页面。


应用生命周期:


id="m1q0rs"
Ability

    |

    ↓

onForeground()

    |

    ↓

onBackground()

关键:

后台切换:


onBackground()

10.3 上传服务独立化

错误:


Page

 └── upload()

正确:


Page


 |

 ↓


UploadService


 |

 ↓


UploadManager


 |

 ↓


Uploader

页面:

只负责:


开始

暂停

查看状态

上传:

独立运行。


10.4 UploadService设计

目录:


id="gq3s8x"
upload

 └── service

      └── UploadService.ets

代码:


export class UploadService {


private manager:
UploadManager



constructor(){


this.manager =
UploadManager
.getInstance()


}



start(task){

return this.manager
.start(task.id)

}



pause(taskId){

this.manager
.pause(taskId)

}



resume(taskId){

this.manager
.resume(taskId)

}



}

页面:


let service =
new UploadService()


service.start(task)

页面销毁:

上传仍然存在。


10.5 后台任务机制

HarmonyOS:

提供:

BackgroundTask。

作用:

告诉系统:

当前APP:

有重要后台任务。


典型:

  • 文件上传
  • 文件下载
  • 音频播放
  • 导航

流程:


上传开始

↓

申请后台任务

↓

执行上传

↓

完成

↓

释放后台任务

10.6 后台任务生命周期

开始:


requestBackgroundRunning()

结束:


cancelBackgroundRunning()

例如:


class UploadBackground {


async start(){


await requestBackgroundRunning()


}



async stop(){


await cancelBackgroundRunning()


}



}

10.7 上传状态持久化

后台上传:

必须保存状态。

不能:


let task={
 progress:50
}

因为:

内存会丢。


必须:


UploadTask

↓

Preferences

↓

RelationalStore

保存:


{
"id":"001",

"file":"video.mp4",

"progress":50,

"status":"uploading",

"uploadId":"abc"

}

10.8 APP被系统杀死怎么办?

移动端:

系统可能:


低内存

↓

杀死APP

恢复流程:

APP重新启动:


Ability启动

↓

读取UploadRecord

↓

查询服务器状态

↓

恢复任务

代码:


async restore(){


let tasks =
await resumeManager
.getAll()



for(
let task of tasks
){


if(
task.status==="uploading"
){


manager.resume(
task.id
)


}


}


}

10.9 网络状态管理

上传不能:

一直发送。

需要监听:


网络变化

状态:


WiFi

4G

5G

无网络

设计:


enum NetworkType{


WIFI,


CELLULAR,


NONE


}

10.10 WiFi优先上传策略

企业常见:

用户上传大视频。

规则:


图片

↓

任何网络


视频

↓

WiFi优先

例如:


if(
file.size >
500*1024*1024
){


if(
network!==WIFI
){


pause()


}


}

10.11 网络恢复自动继续

断网:


upload

↓

pause

恢复:


network available

↓

resume

监听:


network.onChange(
()=>{


if(
available
){

manager.resumeAll()

}


}

)

10.12 电量策略

视频上传:

非常耗电。

策略:


电量 <20%

↓

暂停大文件上传

模型:


if(
battery.level<20
){


pauseLargeUpload()

}

10.13 上传优先级设计

企业APP:

同时:

头像:


200KB

视频:


2GB

不能:

视频阻塞头像。


增加:

priority。


enum Priority{


HIGH,


NORMAL,


LOW


}

任务:


task.priority=
Priority.HIGH

队列排序:


HIGH

↓

NORMAL

↓

LOW

10.14 上传调度器 Scheduler

设计:


UploadScheduler


        |

        ↓


TaskQueue


        |

        ↓


Worker

Worker数量:

例如:


MAX_WORKER=3

执行:


while(true){


task =
queue.next()


await worker.run(task)


}

10.15 上传Worker设计

代码:


class UploadWorker {


async execute(
task:UploadTask
){


try{


await uploader
.upload(task)



task.status=
SUCCESS



}catch(e){


task.status=
FAILED


}


}

}

10.16 后台上传通知

长时间上传:

需要用户知道。

例如:

系统通知:


正在上传视频

65%

通知内容:


文件:

course.mp4


进度:

65%


10.17 企业上传服务架构

最终:


                 ArkUI


                   |

                   ↓


            UploadService


                   |

                   ↓


          UploadScheduler


                   |

          ----------------


          |              |


       Worker1       Worker2


          |              |


     OSSUploader   HttpUploader


          |

          ↓


        OSS


10.18 完整生命周期

用户:

选择文件:


PhotoPicker

创建:


UploadTask

加入:


Queue

调度:


Worker

上传:


OSS

后台:


继续执行

完成:


通知UI

10.19 生产环境检查列表

上传稳定性

✅ 断点续传
✅ 分片上传
✅ 自动重试
✅ 网络恢复
✅ 后台任务


性能

✅ 限制并发
✅ 图片压缩
✅ 流式读取
✅ 内存控制


安全

✅ STS临时授权
✅ HTTPS
✅ 文件类型校验
✅ 大小限制


用户体验

✅ 进度显示
✅ 暂停
✅ 恢复
✅ 取消
✅ 秒传

第十一章 HarmonyOS NEXT 文件上传安全体系设计

文件上传看似只是:


选择文件

↓

上传服务器

但是在企业系统中:

上传入口往往是攻击入口。

例如:

用户上传:


头像

合同

视频

附件

攻击者可能上传:


恶意脚本

病毒文件

超大文件

伪造格式文件

非法内容

所以企业上传系统必须建立:

客户端 + 服务端 + 存储 + CDN 全链路安全体系。


11.1 文件上传主要安全风险

风险1:文件类型伪造

例如:

前端限制:


只允许 jpg

攻击者:

修改文件名:


virus.exe

↓

photo.jpg

如果服务器只判断:


file.name.endsWith(".jpg")

直接绕过。


风险2:超大文件攻击

攻击者上传:


50GB文件

导致:

  • 带宽耗尽
  • 存储爆炸
  • 服务异常

风险3:恶意文件上传

例如:

上传:


木马

脚本

病毒

然后:

通过URL访问。


风险4:Token泄露

错误:

APP内:


AccessKey

SecretKey

攻击者反编译:

直接获取。


风险5:文件URL泄露

例如:

OSS:


https://bucket.xxx.com/user/a.png

被公开访问。


11.2 企业安全上传架构

推荐:



                 APP


                  |

                  ↓


          上传安全模块


                  |

        -------------------


        |                 |


   Token校验       文件预处理


        |                 |


        ↓                 ↓


              上传网关


                  |

                  ↓


            文件检测服务


                  |

                  ↓


                 OSS


                  |

                  ↓


                 CDN

11.3 客户端安全校验

客户端校验:

目的:

提升体验。

不能作为最终安全。


客户端检查:

文件大小

例如:

头像:


max=5MB

代码:


if(
file.size >
5*1024*1024
){

throw Error(
"文件过大"
)

}

文件扩展名

例如:


.jpg

.png

.webp

但是:

只是第一层。


11.4 MIME Type校验

文件:

包含:


Content-Type

例如:

图片:


image/jpeg

客户端:


if(
!mime.startsWith(
"image/"
)
){

reject()

}

但是:

MIME也可以伪造。


11.5 文件Magic Number检测

真正判断文件类型:

看文件头。

例如:

JPEG:


FF D8 FF

PNG:


89 50 4E 47

PDF:


25 50 44 46

流程:


文件

↓

读取前16字节

↓

Magic Number

↓

判断真实类型

例如:


let header =
buffer.slice(
0,
16
)

服务器:

必须做。


11.6 文件名称安全处理

危险:

用户上传:


../../test.exe

服务器:

不要直接使用:


原文件名

生成:


UUID

+

扩展名

例如:


8f82a.jpg

推荐:


objectKey

=

业务目录

+

用户ID

+

UUID

11.7 OSS STS安全设计

客户端:

获得:

临时权限。

但是:

权限必须最小化。


错误:


{
Action:[
"*"
]
}

正确:


{
Action:[

"oss:PutObject"

],


Resource:[

"user/avatar/*"

]

}

只允许:

上传头像。

不能:

删除文件。


11.8 STS有效时间

不要:


24小时

推荐:


5分钟

15分钟

例如:


{
expiration:

"900"

}

上传完成:

立即失效。


11.9 上传接口防重放

攻击:

抓包:


上传请求

重复发送。


方案:

增加:

nonce。

例如:

请求:


{

timestamp:1720000000,

nonce:"abc123"

}

服务器:

检查:


是否已经使用

11.10 HTTPS传输

上传必须:


HTTPS

禁止:


HTTP

原因:

文件包含:

  • 身份信息
  • 合同
  • 图片
  • 视频

11.11 文件病毒扫描

企业:

上传流程:

增加:

AV扫描。

架构:



APP

↓

OSS临时目录

↓

病毒扫描

↓

通过

↓

正式目录


状态:


pending

↓

scanning

↓

safe

↓

available

11.12 图片内容审核

例如:

用户头像。

需要:

检测:

  • 色情
  • 暴恐
  • 涉政
  • 违规内容

流程:


上传图片

↓

AI审核

↓

通过

↓

展示

11.13 文件访问权限设计

错误:


public-read

所有人访问。


企业:

推荐:

private。

访问:

生成:


Signed URL

例如:

有效:


10分钟

URL:


https://xxx.com/file?a=signature

11.14 防盗链设计

CDN:

检查:

Referer。

或者:

Token。

例如:

请求:


url

+

expire

+

sign

服务器:

验证:


sign是否正确

11.15 上传日志审计

企业必须记录:


谁

什么时候

上传什么

大小

IP

结果

数据库:


upload_log


id

user_id

file_name

file_size

status

created_time

11.16 防止恶意占用存储

限制:

用户配额。

例如:

普通用户:


5GB

VIP:


100GB

上传前:

检查:


当前使用量

+

文件大小

<=

额度

11.17 企业上传安全流程

完整流程:



用户选择文件


↓

客户端基础校验


↓

获取STS


↓

上传临时空间


↓

服务端检测


↓

病毒扫描


↓

内容审核


↓

移动正式目录


↓

生成访问URL


↓

记录日志

第十二章 HarmonyOS NEXT 文件上传性能优化与压测体系

文件上传系统从 Demo 到企业级,最大的区别:

不是:

“能不能上传”。

而是:

在百万用户、高并发、大文件环境下,依然稳定上传。

企业级上传系统需要解决:

  • 上传速度
  • 内存占用
  • CPU消耗
  • 网络波动
  • 并发压力
  • 服务容量
  • 存储成本

12.1 上传性能模型

上传速度由多个因素决定:



实际上传速度

=

min(

客户端读取速度,

网络速度,

HTTP速度,

服务器接收速度,

OSS写入速度

)

例如:

手机:

读取:


200MB/s

网络:


20MB/s

那么:

最大:


20MB/s

所以:

优化不是单点优化。

需要全链路。


12.2 文件读取性能优化

错误:

一次读取整个文件:


let buffer =
readFile(
path
)

问题:

100MB:

还能接受。

2GB:

直接:


内存爆炸

企业方案:

流式读取。



File


↓

1MB


↓

上传


↓

释放


↓

继续读取

12.3 Chunk大小设计

分片大小:

影响:

  • 上传速度
  • 请求数量
  • 内存

太小:

例如:


64KB

问题:

请求太多。

100GB:

需要:

百万请求。


太大:

例如:


1GB

问题:

失败重传成本高。


推荐:

文件大小Chunk
<100MB不用分片
100MB-1GB5MB
1GB-10GB10MB-50MB
10GB以上50MB+

12.4 动态Chunk策略

不要固定。

根据文件大小:

计算。


function getChunkSize(
size:number
){


if(size<100*1024*1024){

return size

}


if(size<1024*1024*1024){

return 5*1024*1024

}


return 20*1024*1024


}

优势:

小文件:

快速完成。

大文件:

稳定。


12.5 上传并发优化

单线程:



chunk1

↓

chunk2

↓

chunk3

速度:

慢。


多线程:



chunk1  --->

chunk2  --->

chunk3  --->

但是:

不是越多越好。


例如:

10个并发:

可能:

  • 手机发热
  • 网络拥堵
  • 电量下降

移动端推荐:



WiFi:

3~5并发


4G:

2~3并发

12.6 并发控制器设计

创建:


ConcurrencyLimiter.ets

代码:


export class ConcurrencyLimiter {


max:number=3


running:number=0


queue:Function[]=[]



async execute(
task:Function
){



if(
this.running>=this.max
){


await new Promise(
resolve=>{

this.queue.push(
resolve
)

}

)

}



this.running++



try{


return await task()



}finally{


this.running--



let next =
this.queue.shift()



if(next){

next()

}


}



}



}

效果:

始终:


最多3个上传

12.7 HTTP连接优化

频繁创建:


http.createHttp()

不好。


原因:

每次:

  • DNS
  • TCP连接
  • TLS握手

优化:

连接复用。



HttpClient

长期存在


减少:


连接建立成本

12.8 Keep-Alive优化

HTTP:

开启:


Connection:

keep-alive

效果:

多个Chunk:

复用连接。


12.9 上传内存优化

图片:

最大问题。

错误:



10张图片

↓

全部解码

↓

上传

内存:

可能:

500MB。


正确:

流水线。



图片1

↓

压缩

↓

上传


释放


图片2

类似:

生产流水线。


12.10 PixelMap释放

HarmonyOS图片对象:

占用Native内存。

完成:

需要释放。

例如:


pixelMap.release()

否则:

大量图片:

可能:

OOM。


12.11 上传队列优化

普通队列:

先进先出:



视频

↓

头像

↓

文本

问题:

头像等待。


优先队列:



HIGH

头像


NORMAL

图片


LOW

视频

12.12 上传速度预测

显示:



剩余:

2分钟

需要:

计算。


公式:



speed

=

uploadedBytes

/

time


remaining

=

remainBytes

/

speed

例如:

已经:


100MB

耗时:

10秒。

速度:


10MB/s

剩余:

900MB。

预计:

90秒。


12.13 失败重试优化

不要:

所有错误都重试。

分类:

网络错误

重试:


YES

权限错误

例如:

403。

不要:


无限重试

文件错误

例如:

损坏。

直接失败。


错误分类:


enum ErrorType{


NETWORK,


AUTH,


FILE,


SERVER


}

12.14 上传压测指标

企业关注:

成功率

公式:



成功上传次数

/

总上传次数

目标:

99.9%


平均速度

例如:



平均:

8MB/s

P95耗时

例如:

90%:

5秒完成。

95%:

10秒完成。


失败率

目标:



<0.1%

12.15 百万用户上传架构

大型应用:

架构:



             用户


              |

              ↓


          API Gateway


              |

      -----------------


      |               |


 Token服务      上传服务


      |               |


      --------OSS------


              |


          CDN加速


12.16 上传服务水平扩展

不要:

单服务器。

应该:



Upload Server 1


Upload Server 2


Upload Server 3


无状态:

服务器:

不保存上传状态。

状态:

放:

  • Redis
  • DB
  • OSS

12.17 Redis管理上传状态

例如:


upload:task:001


{

status:"uploading",

progress:70

}

优势:

多个服务器共享。


12.18 监控体系设计

企业必须监控:

上传数量


QPS

上传速度


MB/s

错误


401

403

500

OSS状态


请求成功率

12.19 链路追踪

一次上传:

生成:

traceId。

例如:


upload-20260731-001

贯穿:



APP

↓

API

↓

STS

↓

OSS

出现问题:

快速定位。


12.20 企业上传最佳实践总结

架构:



UploadManager


↓

Scheduler


↓

Worker


↓

ChunkUploader


↓

OSS

第十三章 HarmonyOS NEXT 文件上传综合实战:打造类似微信/网盘上传框架


13.1 企业上传SDK整体设计

最终效果:

业务页面:

只需要:


UploadManager.upload({

 file,

 type:"image"

})

即可。

内部:

自动完成:



选择文件


↓

任务创建


↓

文件检测


↓

图片处理


↓

计算Hash


↓

判断秒传


↓

获取STS


↓

上传OSS


↓

断点保存


↓

完成回调

13.2 SDK整体架构

推荐目录:


UploadSDK


├── core


│   ├── UploadManager.ets


│   ├── UploadTask.ets


│   ├── UploadQueue.ets


│   ├── UploadScheduler.ets


│


├── uploader


│   ├── BaseUploader.ets


│   ├── HttpUploader.ets


│   ├── OssUploader.ets


│   ├── MultipartUploader.ets


│


├── processor


│   ├── ImageProcessor.ets


│   ├── VideoProcessor.ets


│   ├── FileProcessor.ets


│


├── storage


│   ├── ResumeStore.ets


│   ├── UploadDatabase.ets


│


├── security


│   ├── TokenManager.ets


│   ├── FileValidator.ets


│


└── utils


    ├── HashUtil.ets


    ├── ChunkUtil.ets


13.3 核心接口设计

企业SDK:

必须面向接口。

不要:


new OssUploader()

写死。


定义:


export interface IUploader {


upload(
task:UploadTask
)
:
Promise<UploadResult>



pause(
taskId:string
)


resume(
taskId:string
)


cancel(
taskId:string
)


}

未来支持:


OSS

↓

腾讯COS

↓

AWS S3

↓

自建服务器

只需要:

增加实现。


13.4 UploadTask完整设计

任务:

不是简单对象。

包含:

生命周期。


export class UploadTask {


id:string


file:UploadFile



status:UploadStatus



progress:number=0



speed:number=0



remainingTime:number=0



hash:string



uploadId:string



chunks:Chunk[]



createdAt:number



updatedAt:number


}

例如:

运行时:


{
"id":"task1001",

"status":"uploading",

"progress":65,

"speed":5242880,

"remainingTime":120

}

13.5 UploadManager核心实现

单例:


export class UploadManager {


private static instance:


UploadManager



private uploader:IUploader



private scheduler:
UploadScheduler



static getInstance(){


if(!this.instance){


this.instance=
new UploadManager()


}


return this.instance


}



}

13.6 初始化SDK

App启动:

初始化:


UploadManager.init({

oss:true,


endpoint:

"https://oss.xxx.com"


})

内部:

创建:


TokenManager

ResumeStore

Scheduler

Uploader

13.7 上传入口设计

业务调用:


let task =
UploadManager
.upload({

uri:

"file://xxx.jpg",


type:

"image"


})

内部流程:


async upload(options){


let task =
TaskFactory.create(options)



queue.add(task)



return task.id


}

13.8 TaskFactory任务工厂

为什么需要?

不同文件:

不同策略。

例如:

图片:


压缩

↓

上传

视频:


切片

↓

上传

代码:


class TaskFactory {


create(
options
){


let task=
new UploadTask()



task.id=
uuid()



task.file=
options.file



return task


}


}

13.9 Processor处理链设计

类似:

Java:

责任链。

流程:


File


↓

Validator


↓

Compress


↓

Encrypt


↓

Upload

定义:


interface Processor{


next?:Processor



process(
task
)



}

组合:


validator

.next(
compress
)

.next(
encrypt
)

13.10 图片处理Processor


class ImageProcessor{


async process(
task
){



if(
task.file.type
==="image"
){


task.file.buffer=
await compress(
task.file
)


}


return task


}


}

13.11 视频上传Processor

视频:

通常:

不压缩原文件。

主要:

生成:


封面

缩略图

流程:


video.mp4


↓

extractFrame()


↓

cover.jpg


↓

upload

13.12 ChunkManager设计

大文件核心。

创建:


class ChunkManager{


create(
size:number
){


let chunks=[]


let chunkSize=
10*1024*1024



let count=
Math.ceil(
size/chunkSize
)


for(
let i=0;i<count;i++
){


chunks.push({

index:i,

offset:i*chunkSize,

size:chunkSize

})


}


return chunks


}


}

例如:

文件:


1GB

分片:


100个chunk

13.13 MultipartUploader设计

负责:

OSS分片。

流程:


init


↓

uploadPart


↓

complete

代码结构:


class MultipartUploader
implements IUploader{


async upload(task){



let uploadId=
await init(task)



for(
let chunk of task.chunks
){



await uploadPart(
uploadId,
chunk
)



}



return complete(
uploadId
)



}

}

13.14 断点恢复机制

核心:

保存:


{

taskId:"001",

uploadId:"xxx",


completed:[

1,

2,

3

]


}

恢复:


let record =
resumeStore.get(
task.id
)


continueUpload(
record
)

13.15 上传事件系统

SDK:

提供:


task.on(
"progress",

callback

)

事件:


created

waiting

uploading

progress

paused

success

failed

cancelled

实现:


class UploadEvent {


emit(
name,
data
){


}


on(
name,
callback
){


}


}

13.16 ArkUI上传组件封装

业务:

不想写:

监听逻辑。

封装:


UploadProgressView

使用:


UploadProgressView({

taskId:"001"

})

显示:


视频上传中


███████░░░

70%

速度:

8MB/s

剩余:

30秒

13.17 多文件上传

例如:

朋友圈:

9张。

调用:


UploadManager.uploadBatch([

file1,

file2,

file3


])

返回:


[
{
id:"1"
},
{
id:"2"
}
]

内部:

加入队列。


13.18 上传优先级

定义:


enum Priority{


URGENT=10,


HIGH=5,


NORMAL=1


}

例如:

头像:


priority=10

视频:


priority=1

13.19 SDK最终调用体验

业务页面:


const task =
await uploadManager.upload({

file,

priority:

"HIGH"


})


task.onProgress(
(progress)=>{


this.progress=
progress


}

)

业务层:

完全不知道:

  • OSS
  • HTTP
  • Chunk
  • Token
  • Resume

13.20 本章架构总结

最终企业上传SDK:



                 UploadManager


                       |


              UploadScheduler


                       |


              UploadTask Queue


                       |


        ----------------------------


        |                          |


 ImageProcessor             ChunkProcessor


        |                          |


        --------Uploader-----------


                     |


        ----------------------


        |                    |


   HttpUploader        OssUploader


                     |


                   OSS

第十四章 HarmonyOS NEXT 上传SDK数据库持久化设计与断点续传引擎

企业级上传系统最大的区别:

不是上传过程。

而是:

上传任务能够跨越页面、进程、设备状态持续存在。

例如:

用户上传一个:


video.mp4

大小:

5GB

上传到:


70%

此时:

  • APP被杀死
  • 手机重启
  • 网络断开
  • 页面关闭

再次打开:

仍然可以:


继续70%

这需要:

上传任务数据库。


14.1 为什么不能只使用Preferences?

前面简单场景:

Preferences:

可以。

例如:


{
"uploadId":"xxx",
"progress":50
}

但是企业环境:

任务数量:

可能:


1000+

同时存在:

  • 视频上传
  • 图片上传
  • 文档上传

Preferences问题:

1. 查询困难

例如:

查询:


所有正在上传任务

需要:

遍历。


2. 数据结构复杂

一个任务:

包含:


任务信息

分片信息

失败记录

重试次数

速度统计

3. 并发安全

多个上传Worker:

同时更新状态。


企业方案:

使用:


RelationalStore

14.2 数据库设计

设计三张核心表:


upload_task

upload_chunk

upload_log

关系:


upload_task


     1


     |

     |

     N


upload_chunk

14.3 upload_task任务表

字段:

字段说明
id任务ID
file_name文件名
file_uri文件地址
file_size文件大小
file_md5文件Hash
upload_idOSS UploadId
status状态
progress进度
create_time创建时间

SQL:


CREATE TABLE upload_task(

id TEXT PRIMARY KEY,


file_name TEXT,


file_uri TEXT,


file_size INTEGER,


file_md5 TEXT,


upload_id TEXT,


status TEXT,


progress INTEGER,


create_time INTEGER

)

14.4 upload_chunk分片表

一个大文件:

对应多个chunk。

例如:


video.mp4


chunk0

chunk1

chunk2

chunk3

表:


CREATE TABLE upload_chunk(

id INTEGER PRIMARY KEY,


task_id TEXT,


chunk_index INTEGER,


offset INTEGER,


size INTEGER,


etag TEXT,


status TEXT

)

数据:


task_id:

001


chunk_index:

0


status:

success

14.5 upload_log上传日志表

用于:

问题排查。

例如:

为什么失败?


表:


CREATE TABLE upload_log(

id INTEGER PRIMARY KEY,


task_id TEXT,


event TEXT,


message TEXT,


time INTEGER

)

记录:


task001

chunk3 upload failed

network timeout

14.6 HarmonyOS RelationalStore初始化

导入:


import {

relationalStore

}

from '@kit.ArkData'

创建数据库:


let config = {

name:

"upload.db",

securityLevel:

relationalStore.SecurityLevel.S1

}


let store =

await relationalStore.getRdbStore(

context,

config

)

14.7 DatabaseManager封装

创建:


database

 |

 DatabaseManager.ets

代码:


export class DatabaseManager {


private store:


relationalStore.RdbStore



async init(context){


this.store =

await relationalStore.getRdbStore(

context,

{

name:"upload.db",

version:1

}

)

}



}

14.8 保存上传任务

方法:


saveTask()

代码:


async saveTask(
task:UploadTask
){


let value = {


"id":

task.id,


"file_name":

task.file.name,


"file_size":

task.file.size,


"status":

task.status,


"progress":

task.progress


}



await this.store.insert(

"upload_task",

value

)


}

14.9 查询未完成任务

APP启动:

执行:


SELECT *

FROM upload_task

WHERE status != 'success'

代码:


async getUnfinished(){


let result =

await this.store.querySql(

`

SELECT *

FROM upload_task

WHERE status != ?

`,

[

"success"

]

)


return result


}

返回:


[
{
"id":"001",

"status":"uploading",

"progress":70
}
]

14.10 Chunk状态恢复

例如:

文件:

10个分片。

数据库:


chunk0 success

chunk1 success

chunk2 success

chunk3 uploading

chunk4 waiting

恢复:

跳过:


chunk0

chunk1

chunk2

继续:


chunk3

chunk4

查询:


SELECT *

FROM upload_chunk

WHERE

task_id=?

AND

status!='success'

14.11 上传状态机持久化

状态:


enum UploadStatus{


WAITING,


UPLOADING,


PAUSED,


SUCCESS,


FAILED

}

每次变化:

保存:


task.status=

UploadStatus.UPLOADING


await db.update(task)

避免:

内存状态丢失。


14.12 崩溃恢复流程

APP启动:


Ability启动


↓

DatabaseManager.init()


↓

查询upload_task


↓

过滤未完成任务


↓

恢复UploadTask


↓

重新加入Scheduler


↓

继续上传

代码:


async restoreTasks(){


let tasks=

await database

.getUnfinished()



for(
let task of tasks
){


scheduler.add(task)


}


}

14.13 Chunk上传状态更新

上传成功:

不要:

全部完成再保存。

应该:

每个Chunk:

立即更新。


流程:


上传chunk5


↓

OSS返回ETag


↓

更新数据库


↓

继续chunk6

代码:


async completeChunk(
chunk
){


chunk.status=

"success"



await db.updateChunk(
chunk
)


}

14.14 防止重复上传

场景:

两个页面:

同时上传同一个文件。

需要:

任务去重。


根据:


fileMd5

查询:


SELECT *

FROM upload_task

WHERE file_md5=?

存在:

直接返回:

已有任务。


14.15 秒传查询流程

上传前:


计算MD5


↓

查询本地数据库


↓

查询服务器


↓

存在

↓

直接完成

状态:


task.status=

SUCCESS

14.16 数据库事务

批量更新:

例如:

100个chunk。

不要:

一个个提交。


使用事务:


await store.beginTransaction()


try{


updateChunk()


updateTask()


await store.commit()


}catch(e){


await store.rollBack()

}

14.17 数据清理策略

数据库不能无限增长。

规则:

成功任务:

保存:

7天。

失败任务:

保存:

30天。


定时清理:


DELETE FROM upload_task

WHERE

create_time < xxx

14.18 企业级恢复能力

最终支持:

场景恢复
页面关闭
APP后台
APP重启
手机重启
网络断开
系统杀进程

14.19 UploadDatabase最终结构


UploadDatabase


        |


        |


 -----------------------


 |          |           |


Task       Chunk       Log


 |          |


 |          |


任务状态   分片状态


14.20 完整恢复架构



APP启动


  |

  ↓


UploadDatabase


  |

  ↓


查询未完成Task


  |

  ↓


恢复Chunk状态


  |

  ↓


Scheduler重新调度


  |

  ↓


Worker继续上传

第十五章 HarmonyOS NEXT UploadScheduler 并发调度算法实现

企业上传系统中,真正复杂的部分不是 HTTP 请求。

而是:

如何让几十、几百个上传任务,在有限网络、CPU、电量条件下稳定运行。

例如:

用户一次选择:


20张图片

3个视频

5个PDF

如果全部同时上传:

结果:


网络拥堵

手机发热

耗电增加

APP卡顿

所以需要:

UploadScheduler 上传调度器。


15.1 Scheduler职责

UploadScheduler负责:

任务排队


等待上传任务

↓

队列

↓

执行

并发控制

例如:

同时:


最多3个上传

优先级调度

例如:

头像:

优先。

视频:

后台。


状态管理

控制:


waiting

uploading

paused

success

failed

15.2 Scheduler整体结构



              UploadScheduler


                     |


        ---------------------------


        |                         |


 PriorityQueue              WorkerPool


        |                         |


 UploadTask                UploadWorker



15.3 Worker模型

Worker:

就是上传执行单元。

例如:

配置:


MAX_WORKER = 3

代表:

同时:


Worker1

Worker2

Worker3

任务:


Task1

Task2

Task3

执行:


Worker1 ---> Task1

Worker2 ---> Task2

Worker3 ---> Task3

15.4 Worker类设计

创建:


worker

 |

 UploadWorker.ets

代码:


export class UploadWorker {


id:number


busy:boolean=false



constructor(
id:number
){

this.id=id

}



async execute(
task:UploadTask
){


this.busy=true



try{


await task.uploader.upload(
task
)



}catch(e){



console.error(e)


}



finally{


this.busy=false


}



}

}

15.5 WorkerPool设计

管理多个Worker。


export class WorkerPool {


workers:
UploadWorker[]=[]



constructor(
count:number
){


for(
let i=0;i<count;i++
){


this.workers.push(

new UploadWorker(i)

)


}


}



}

初始化:


new WorkerPool(3)

产生:


Worker0

Worker1

Worker2

15.6 获取空闲Worker

方法:


getIdleWorker()

代码:


getIdleWorker(){


return this.workers.find(

worker=>

!worker.busy

)


}

结果:


Worker1 空闲

↓

分配任务

15.7 普通FIFO队列

最简单:


class Queue{


tasks:UploadTask[]=[]



push(task){

this.tasks.push(task)

}



pop(){

return this.tasks.shift()

}


}

问题:

没有优先级。


例如:

队列:


视频10GB

头像100KB

结果:

头像等待。

体验差。


15.8 优先级队列设计

定义:


enum TaskPriority{


HIGH=3,


NORMAL=2,


LOW=1


}

任务:


task.priority=

TaskPriority.HIGH

排序:


tasks.sort(

(a,b)=>

b.priority-a.priority

)

结果:


HIGH

↓

NORMAL

↓

LOW

15.9 PriorityQueue实现


export class PriorityQueue {


private tasks:
UploadTask[]=[]



push(
task:UploadTask
){


this.tasks.push(task)



this.sort()

}



sort(){


this.tasks.sort(

(a,b)=>

b.priority-

a.priority

)


}



pop(){


return this.tasks.shift()


}



}

15.10 Scheduler核心代码


export class UploadScheduler {


queue:
PriorityQueue



pool:
WorkerPool



constructor(){


this.queue=
new PriorityQueue()



this.pool=
new WorkerPool(3)


}



add(
task
){


this.queue.push(task)



this.schedule()


}



schedule(){



let worker=

this.pool.getIdleWorker()



while(
worker
&&
this.queue.length()>0

){



let task=

this.queue.pop()



worker.execute(task)



worker=

this.pool.getIdleWorker()


}



}


}

15.11 并发限制算法

例如:

最大:


3

当前:


Worker1 busy

Worker2 busy

Worker3 busy

新任务:

进入:


waiting queue

状态:


WAITING

↓

UPLOAD

15.12 动态并发调整

固定3个:

不一定最佳。

根据网络:

动态调整。


WiFi:


maxWorker=5

4G:


maxWorker=2

弱网:


maxWorker=1

15.13 网络感知调度

监听:


NetworkChange

例如:


onNetworkChange(type){


if(type==="wifi"){


setWorker(5)


}


if(type==="cellular"){


setWorker(2)


}


}

15.14 大文件公平调度

问题:

一个:

10GB视频。

占满:

所有Worker。


解决:

时间片。

例如:

每次:

上传:


10个chunk

释放Worker。


类似:

CPU调度。


15.15 Chunk级调度

不是:

任务级:


video

↓

上传完成

而是:


video chunk1

image chunk1

pdf chunk1

video chunk2

优势:

公平。


15.16 ChunkScheduler设计


Task


 |

 ↓


ChunkQueue


 |

 ↓


Worker


队列:


chunk1

chunk2

chunk3

Worker:

执行:


一个chunk

15.17 暂停任务调度

用户点击:

暂停。

任务:


UPLOADING

↓

PAUSED

Scheduler:

删除:


running queue

代码:


pause(taskId){


let task=

findTask(taskId)



task.status=

PAUSED



queue.remove(task)


}

15.18 取消任务

取消:

需要:

停止:

  • 网络请求
  • chunk上传
  • 重试

设计:

CancelToken。



class CancelToken{


cancelled=false



cancel(){

this.cancelled=true

}


}

上传:


if(token.cancelled){

throw Error(
"cancel"
)

}

15.19 失败任务重新调度

失败:

进入:

RetryQueue。


流程:


FAILED


↓

等待

↓

retry


↓

UPLOAD

例如:


retryCount < 3

15.20 Scheduler完整流程


用户上传


↓

创建Task


↓

PriorityQueue


↓

Scheduler


↓

Worker空闲?


        |

        |

      是


        ↓


Worker执行


        ↓


上传Chunk


        ↓


更新数据库


        ↓


完成


15.21 企业级调度策略

最终:


UploadScheduler


├── PriorityQueue

├── WorkerPool

├── NetworkManager

├── RetryManager

├── ChunkScheduler

└── TaskStore

15.22 生产参数建议

图片上传


并发:

5

chunk:

1MB

视频上传


并发:

2-3

chunk:

10-20MB

文档上传


并发:

3

chunk:

5MB

第十六章 HarmonyOS NEXT UploadWorker线程模型与大文件分片并行上传

企业级文件上传中,大文件是最复杂的场景。

例如:

用户上传:


video.mp4

大小:

20GB

要求:

  • 上传不中断
  • 支持暂停
  • 支持恢复
  • 支持后台
  • 支持弱网
  • 支持失败重传
  • 不占满内存

核心方案:

文件切片 + Worker池 + Chunk状态管理 + 并行上传。


16.1 大文件上传整体流程

完整流程:


File


 ↓


FileInfo


 ↓


ChunkSplitter


 ↓


Chunk Queue


 ↓


Worker Pool


 ↓


Upload Part


 ↓


OSS


 ↓


Complete Multipart


 ↓


Merge File


16.2 UploadWorker生命周期

一个Worker不是简单函数。

它拥有完整生命周期:


CREATE


 ↓


IDLE


 ↓


RUNNING


 ↓


PAUSED


 ↓


FAILED


 ↓


DESTROY

状态:


export enum WorkerStatus{


IDLE="idle",


RUNNING="running",


PAUSED="paused",


FAILED="failed"


}

16.3 Worker核心模型


export class UploadWorker {


workerId:number


status:
WorkerStatus


currentChunk:
UploadChunk|null



constructor(
id:number
){

this.workerId=id

this.status=
WorkerStatus.IDLE

}



}

16.4 Worker执行任务

Worker获取Chunk:


async run(
chunk:UploadChunk
){


this.status=
WorkerStatus.RUNNING



this.currentChunk=
chunk



try{


await this.uploadChunk(
chunk
)



chunk.status=
"success"



}catch(e){


chunk.status=
"failed"


}


finally{


this.status=
WorkerStatus.IDLE


}


}

16.5 文件分片算法

大文件:

不能一次读取。

例如:

文件:


1GB

分片:


10MB

产生:


100个Chunk

计算:


chunkCount =
Math.ceil(

fileSize /

chunkSize

)

例如:


fileSize:

1073741824


chunkSize:

10485760


chunk:

103

16.6 Chunk模型设计


export class UploadChunk{


index:number


offset:number


size:number


status:string


etag:string



}

示例:


{
"index":5,

"offset":52428800,

"size":10485760,

"status":"uploading"

}

16.7 ChunkSplitter实现


export class ChunkSplitter{


split(
size:number,
chunkSize:number
){


let chunks=[]


let count=
Math.ceil(
size/chunkSize
)


for(
let i=0;
i<count;
i++
){


chunks.push({

index:i,


offset:

i*chunkSize,


size:

Math.min(

chunkSize,

size-i*chunkSize

)


})


}



return chunks


}


}

16.8 分片读取文件

不要:


readAll()

正确:

偏移读取。

例如:

读取:


offset:

50MB


length:

10MB

伪代码:


async readChunk(
file,
chunk
){


return await file.read({

offset:

chunk.offset,


length:

chunk.size


})


}

16.9 Chunk上传流程

单个Chunk:


读取


↓

计算Hash


↓

生成请求


↓

上传


↓

获取ETag


↓

保存状态


代码:


async uploadChunk(
chunk
){


let data=

await readChunk(chunk)



let etag=

await oss.uploadPart(

data

)



chunk.etag=

etag



await database.updateChunk(
chunk
)


}

16.10 OSS Multipart Upload完整流程

OSS:

不是直接:


PUT文件

而是:

第一步

初始化:


POST ?uploads

返回:


{
uploadId:"xxx"
}

第二步

上传Part:


PUT

?partNumber=1

&uploadId=xxx

返回:


ETag

第三步

完成:


POST

?uploadId=xxx

提交:


[
{
partNumber:1,

etag:"xxx"
}
]

16.11 MultipartUploader实现


export class MultipartUploader{


async upload(task){


let uploadId=

await this.initMultipart()



let chunks=

task.chunks



for(
let chunk of chunks
){


await this.uploadPart(

uploadId,

chunk

)


}



return await this.complete(

uploadId

)


}


}

16.12 并行Chunk上传

串行:


chunk1

↓

chunk2

↓

chunk3

速度:

慢。


并行:


Worker1

chunk1


Worker2

chunk2


Worker3

chunk3

速度:

提升。


16.13 ChunkScheduler


class ChunkScheduler{


queue:
UploadChunk[]



workers:
UploadWorker[]



start(){


while(
hasIdleWorker()
&&
queue.length
){


let worker=

getWorker()


let chunk=

queue.shift()



worker.run(chunk)


}


}


}

16.14 并行数量控制

不要无限。

例如:


const MAX_CHUNK_WORKER=4

原因:

手机:

CPU:

有限。

网络:

有限。


推荐:

网络并发
WiFi4-6
5G3-5
4G2-3
弱网1

16.15 上传速度限制

某些场景:

需要限速。

例如:

后台上传:

不能影响用户。


设计:

BandwidthLimiter。


算法:

Token Bucket。


桶:


容量:

10MB

每秒:

增加:


1MB

上传:

消耗。


16.16 BandwidthLimiter实现


class BandwidthLimiter{


capacity:number


tokens:number



consume(size:number){


if(
this.tokens>=size
){


this.tokens-=size


return true

}


return false


}


}

16.17 内存池设计

问题:

多个Chunk同时读取。

例如:

5个Worker。

每个:

20MB。

内存:


100MB

设计:

BufferPool。



class BufferPool{


buffers:ArrayBuffer[]



get(size){

return new ArrayBuffer(size)

}



release(buffer){

this.buffers.push(buffer)

}


}

16.18 Chunk失败恢复

例如:

100个Chunk。

完成:


99%

最后一个失败。


不要重新上传。

只重试:


chunk99

数据库:

记录:


{
"chunk":99,

"status":"failed"

}

16.19 Worker异常处理

异常:

分类。


enum UploadError{


NETWORK,


TIMEOUT,


AUTH,


SPACE,


UNKNOWN


}

处理:

网络:

重试。

权限:

刷新Token。

空间:

失败。


16.20 Token过期处理

大文件:

上传几个小时。

STS:

可能:

15分钟过期。


流程:


上传


↓

401


↓

TokenManager刷新


↓

继续上传

代码:


if(error.code===401){


await tokenManager.refresh()



retry()

}

16.21 大文件上传完整链路



20GB文件


↓

ChunkSplitter


↓

1000 Chunk


↓

ChunkQueue


↓

Worker Pool


↓

OSS UploadPart


↓

保存ETag


↓

CompleteMultipart


↓

生成URL


↓

任务成功


16.22 企业级大文件参数

视频:


Chunk:

20MB


Worker:

4


Retry:

3次

压缩包:


Chunk:

50MB


Worker:

3

移动网络:


Chunk:

5MB


Worker:

2

第十七章 HarmonyOS NEXT 上传SDK ArkUI组件封装与业务层接入

企业项目中,上传能力最终一定要暴露给业务页面。

但是业务开发不应该关心:

  • OSS签名
  • HTTP请求
  • Chunk切片
  • Worker调度
  • 断点恢复
  • Token刷新

业务页面只需要:


选择文件

↓

调用上传

↓

显示状态

↓

处理结果

因此需要封装:

Upload UI Component 层。


17.1 上传组件整体设计

最终:


 id="n8h4m2"

业务页面


    |

    ↓


Upload Components


    |

    ↓


Upload SDK


    |

    ↓


OSS / HTTP

组件层:


components


├── UploadButton.ets


├── UploadProgress.ets


├── UploadList.ets


├── UploadItem.ets


└── UploadPreview.ets

17.2 UploadButton组件设计

作用:

提供统一上传入口。

业务:


UploadButton({

type:"image",

onSuccess:(url)=>{}

})

内部:

负责:

  1. 打开文件选择
  2. 创建任务
  3. 开始上传

17.3 UploadButton实现


@Component
export struct UploadButton {


@State uploading:boolean=false


type:string="file"



onSuccess?:
(url:string)=>void



build(){


Button("选择文件")


.onClick(()=>{


this.selectFile()


})


}



async selectFile(){


let file =

await FilePicker.pick()



let task=

UploadManager.upload({

file:file,

type:this.type

})



task.onSuccess(

(url)=>{


this.onSuccess?.(
url
)


}

)


}


}

17.4 页面使用

业务页面:


UploadButton({

type:"image",


onSuccess:(url)=>{


console.log(url)


}

})

页面无需:


OSS

HTTP

Token

Chunk

17.5 UploadProgress进度组件

上传:

最常见需求:

显示:


正在上传


████████░░


80%

组件:


UploadProgress.ets

输入:


taskId

代码:


@Component
export struct UploadProgress {


taskId:string



@State progress:number=0



aboutToAppear(){


UploadManager

.observe(

this.taskId,

(data)=>{


this.progress=

data.progress


}


)


}



build(){


Progress({

value:this.progress


})


}



}

17.6 UploadItem上传列表

类似:

微信聊天:

多个文件。

显示:


 id="m7x9v3"

photo1.jpg

上传中 60%


video.mp4

等待


doc.pdf

完成

数据:


@State tasks:

UploadTask[]=[]

页面:


ForEach(

this.tasks,

task=>{


UploadItem({

task

})


}

)

17.7 UploadItem状态显示

根据状态:


switch(task.status){


case "waiting":

显示等待



break;



case "uploading":

显示进度



break;



case "success":

显示完成



break;



case "failed":

显示重试按钮



}

17.8 ArkUI状态绑定

核心:

UploadTask变化:

触发:

UI刷新。


错误:

普通对象:


let task={

progress:20

}

变化:

ArkUI不知道。


正确:

使用:


@Observed

17.9 Observable UploadTask

定义:


@Observed
export class UploadTask {


progress:number=0


status:string="waiting"


}

组件:


@ObjectLink

task:UploadTask

变化:


task.progress=80

UI:

自动更新。


17.10 上传列表响应式设计

数据流:


 id="x4p8k2"

UploadManager


      |

      ↓


Observable Store


      |

      ↓


ArkUI


类似:

Redux。


17.11 UploadStore设计

创建:


store


 └── UploadStore.ets

代码:


export class UploadStore {


tasks:

UploadTask[]=[]



add(task){


this.tasks.push(task)


}



update(task){


let old=

this.tasks.find(

x=>x.id===task.id

)


Object.assign(

old,

task

)


}



}

17.12 图片上传完整案例

场景:

用户修改头像。

流程:


 id="f3k7m9"

点击头像


↓

PhotoPicker


↓

ImageProcessor


↓

UploadManager


↓

OSS


↓

更新头像URL


页面:


UploadButton({

type:"avatar",


onSuccess(url){



user.avatar=url



}

})

17.13 IM聊天图片上传

微信类似:

流程:


图片选择


↓

压缩


↓

上传


↓

发送消息


↓

服务器保存URL


消息:


{

type:"image",


url:"https://xxx/a.jpg"

}

注意:

聊天:

不能等待上传完成才显示。

采用:

临时消息。


状态:


sending


↓

uploaded


↓

sent

17.14 视频上传页面设计

直播/短视频:


选择视频


↓

生成封面


↓

压缩


↓

分片上传


↓

审核


↓

发布

UI:

显示:


视频.mp4


上传:

65%


速度:

8MB/s


剩余:

2分钟

17.15 上传暂停按钮

组件:


Button("暂停")


.onClick(()=>{


UploadManager

.pause(

taskId

)


})

恢复:


UploadManager

.resume(

taskId

)

17.16 上传失败重试UI

失败:

显示:


上传失败


[重新上传]

点击:


retry(){


UploadManager

.retry(

taskId

)


}

17.17 上传取消UI

取消:


UploadManager

.cancel(

taskId

)

同时:

清理:

  • 网络请求
  • Chunk记录
  • 临时文件

17.18 企业商城上传案例

商品后台:

上传:

  • 商品图片
  • 商品视频
  • 商品详情附件

流程:


商品编辑页


↓

UploadButton


↓

UploadTask


↓

OSS


↓

返回URL


↓

保存商品

商品数据:


{

name:"手机",


images:[

"https://oss/a.jpg"

]


}

17.19 企业IM上传案例

聊天:

支持:

  • 图片
  • 视频
  • 文件

统一:


UploadManager.upload()

消息:


{

uploadTaskId:"001",

status:"uploading"

}

上传完成:

推送:


{

status:"success",

url:"xxx"

}

17.20 直播APP上传案例

主播上传:

  • 视频
  • 封面
  • 回放

架构:


主播端


↓

UploadSDK


↓

OSS


↓

转码服务


↓

CDN


↓

用户观看

17.21 上传组件最佳实践

组件负责:

✅ 文件选择
✅ UI展示
✅ 用户交互
✅ 状态绑定

SDK负责:

✅ 网络
✅ OSS
✅ 分片
✅ 重试
✅ 恢复

业务负责:

✅ 业务数据保存
✅ 页面逻辑


17.22 最终业务调用效果

业务代码:


const task =

UploadManager.upload({

file,


category:"avatar"


})



UploadProgress({

taskId:

task.id

})

整个业务层:

只有:

几行代码。

第十八章 HarmonyOS NEXT 文件上传SDK完整企业项目落地架构

企业项目中,上传能力通常不是一个页面功能,而是一个基础能力平台

例如一个大型 App:

包含:


商城模块

IM模块

直播模块

社区模块

后台管理模块

这些模块都会上传文件。

如果每个业务自己实现:


商城写一套

IM写一套

直播写一套

最终会产生:

  • 重复开发
  • Bug无法统一修复
  • 上传行为不一致
  • 运维困难

所以企业通常建设:

统一 Upload Platform。


18.1 企业上传平台架构

整体:



                 业务层


    --------------------------------


    商城        IM        直播        社区


                  |


                  ↓


          Upload SDK


                  |


      -------------------------


      |           |            |


  Task管理    上传引擎     安全模块


                  |


      -------------------------


      |                       |


    OSS                  HTTP Server


                  |

                  ↓


              存储系统

18.2 HarmonyOS工程模块设计

推荐:



entry


feature_shop


feature_im


feature_live



common


    |

    └── upload-sdk


其中:

upload-sdk:

独立HAR模块。


18.3 HAR封装上传SDK

HarmonyOS:

推荐:


HAR

(HarmonyOS Archive)

结构:



upload-sdk


├── src/main/ets


│


├── UploadManager.ets


├── OssUploader.ets


├── UploadTask.ets


└── index.ets


业务:

依赖:


{

"dependencies":{


"@company/upload-sdk":

"../upload-sdk"


}

}

18.4 SDK出口设计

不要暴露内部。

例如:

错误:


import {

OssUploader

}

应该:


import {

UploadManager

}

index.ets:


export {

UploadManager

}

from

"./core/UploadManager"



export {

UploadTask

}

from

"./model/UploadTask"

18.5 多业务统一配置

不同业务:

不同策略。

例如:

商城:


{

bucket:"shop",

maxSize:"20MB"

}

直播:


{

bucket:"video",

chunkSize:"20MB"

}

配置中心:


UploadConfig

18.6 UploadConfig设计


export class UploadConfig{


bucket:string


endpoint:string


maxSize:number


chunkSize:number


workerCount:number



}

初始化:


UploadManager.init({

chunkSize:

10*1024*1024,


workerCount:

4

})

18.7 多环境配置

企业:

通常:

三个环境。



开发环境

↓

测试环境

↓

生产环境

配置:


const config={


dev:{


endpoint:"dev-oss"


},



test:{


endpoint:"test-oss"


},



prod:{


endpoint:"prod-oss"


}


}

18.8 上传监控系统

生产环境:

必须知道:

每天:

多少上传?

失败多少?

平均速度?


数据:



upload_start


upload_progress


upload_success


upload_failed


18.9 上传埋点设计

创建:


UploadTracker.ets

事件:


tracker.report({

event:

"upload_success",


taskId,

fileSize,


duration


})

18.10 关键指标

成功率

公式:



success

/

total

目标:


99.9%

平均上传速度

例如:



8.5MB/s

P95耗时

95%用户:

多少时间完成。


失败原因分布

例如:



网络失败 60%


Token过期 20%


服务器异常 20%

18.11 上传日志模型


class UploadLog{


taskId:string


event:string


time:number


extra:any


}

保存:


本地

+

服务器

18.12 灰度发布策略

上传SDK升级:

不能全部上线。


流程:



内部员工


↓

1%用户


↓

10%用户


↓

50%用户


↓

100%

原因:

上传属于核心链路。


18.13 SDK版本管理

例如:


1.0.0


1.1.0


2.0.0

兼容:

旧业务:

继续运行。


18.14 API兼容设计

不要频繁修改:

旧:


upload(file)

新:


upload({

file,

options

})

保留:

旧方法。


18.15 上传异常中心

统一错误码:


enum UploadCode{


NETWORK_ERROR=1001,


TOKEN_EXPIRED=1002,


FILE_TOO_LARGE=1003,


NO_PERMISSION=1004,


SERVER_ERROR=1005


}

业务:

根据错误码处理。


18.16 上传安全中心

统一:



TokenManager


FileValidator


VirusScanner


PermissionChecker

所有业务:

共用。


18.17 上传缓存体系

减少重复上传。

三级缓存:



Memory


↓

Database


↓

OSS

例如:

用户重复上传:

同MD5:

直接返回。


18.18 秒传系统

流程:



计算MD5


↓

发送服务器


↓

查询文件表


↓

存在


↓

返回URL

文件表:


file_hash


file_url


file_size


create_time

18.19 企业文件生命周期管理

OSS:

设置:

临时文件

保存:


24小时

正式文件

保存:


长期

冷文件

转:

低频存储。


18.20 上传平台最终形态

完整:



                Business


                    |


                    ↓


              Upload SDK


                    |


 ------------------------------------------------


 |              |             |                 |


Task        Scheduler     Security        Monitor


 |              |             |                 |


 ------------------------------------------------


                    |


             Storage Adapter


                    |


          -------------------


          |                 |


        OSS             HTTP

第十九章 HarmonyOS NEXT 文件上传源码级优化:内存、线程、性能极限调优

企业级上传系统进入生产环境以后,问题通常不是“上传失败”。

而是:

  • 上传过程中 APP 卡顿
  • 大文件导致 OOM
  • 多任务导致内存暴涨
  • 长时间上传速度下降
  • 后台运行被系统限制
  • 低端设备体验差

因此需要从:

  • ArkTS运行模型
  • 文件IO
  • 内存管理
  • 并发模型
  • 网络层

进行深度优化。


19.1 HarmonyOS 文件上传内存模型

一次上传流程:



File


 ↓


读取Buffer


 ↓


转换ArrayBuffer


 ↓


HTTP Request


 ↓


OSS


如果:

直接读取:


let buffer =
readFileAll()

文件:

2GB

内存:

直接:


2GB

移动设备:

无法接受。


19.2 大文件禁止全量读取

错误:


const data =
file.read()

正确:

分段读取:



文件


0MB

 |

10MB


 |

20MB


 |

30MB


一次:

只保持:


5MB~20MB

19.3 Buffer生命周期设计

Chunk上传流程:



申请Buffer


↓

读取文件


↓

上传


↓

释放Buffer


不要:



chunk1 buffer

chunk2 buffer

chunk3 buffer


全部保存

19.4 Buffer池优化

频繁:

new ArrayBuffer

会造成:

GC压力。


例如:

上传1000个chunk。

如果:

每次:


new ArrayBuffer(
10MB
)

产生:

大量垃圾对象。


设计:

BufferPool。


19.5 BufferPool实现


export class BufferPool {


private pool:
ArrayBuffer[]=[]



get(size:number){


let buffer=

this.pool.pop()



if(buffer){

return buffer

}



return new ArrayBuffer(size)


}



release(
buffer:ArrayBuffer
){


this.pool.push(buffer)


}


}

使用:


let buffer=

pool.get(
10*1024*1024
)



upload(buffer)



pool.release(buffer)

优势:

减少:

内存申请。


19.6 ArrayBuffer与Uint8Array优化

HarmonyOS文件操作:

大量使用:


ArrayBuffer

Uint8Array

关系:



ArrayBuffer

    |

    ↓

Uint8Array

    |

    ↓

二进制数据

转换:


let bytes =
new Uint8Array(buffer)

避免:

重复复制。

错误:


let newBuffer=

copy(buffer)

会:

增加内存。


19.7 零拷贝思想

传统:



文件


↓

Buffer1


↓

Buffer2


↓

HTTP


优化:



文件


↓

共享Buffer


↓

HTTP

减少:

复制。


19.8 图片上传内存优化

图片:

通常:

最大问题。

流程:



原图10MB


↓

解码PixelMap


↓

压缩


↓

重新编码


↓

上传

问题:

PixelMap可能:

几十MB。


处理:

完成后:

立即释放。



pixelMap.release()

19.9 图片压缩策略

不要:

固定压缩。

应该:

根据尺寸。

例如:



头像:

500KB


商品图:

2MB


朋友圈:

5MB

动态:


function getQuality(size){


if(size>10MB){

return 60

}


return 80


}

19.10 视频上传优化

视频:

特点:

  • 时间长
  • 不允许重复

优化:

1. 不读取完整视频

只读取:

chunk。


2. 提前获取Metadata

例如:



duration

width

height

codec

3. 异步生成封面

不要阻塞上传。


19.11 Worker线程模型优化

上传任务:

不要全部:

主线程执行。


主线程:

负责:



UI

事件

状态

Worker:

负责:



文件读取

Hash计算

网络上传

19.12 Worker通信模型

主线程:

发送任务:


worker.postMessage({

taskId:"001"

})

Worker:

返回:


postMessage({

progress:50

})

主线程:

更新:

ArkUI。


19.13 Hash计算优化

秒传:

需要:

MD5。


大文件:

不能:

一次计算。


错误:


md5(
wholeBuffer
)

正确:

流式Hash:



chunk1

↓

update


chunk2

↓

update


chunk3

↓

update


final

类似:


hash.update(buffer)

19.14 上传速度统计优化

不要:

每次进度:

立即刷新UI。

例如:

1秒:

100次。

会:

造成:

UI压力。


限制:



100ms刷新一次

代码:


if(
Date.now()-lastTime>100
){

notify()

}

19.15 网络请求优化

HTTP请求:

优化:

连接复用

开启:


keep-alive

超时控制

不要:

无限等待。

例如:


timeout:

30000

重试策略

指数退避:



1秒

↓

3秒

↓

10秒

19.16 弱网优化

移动网络:

经常:

  • 延迟
  • 丢包
  • 切换网络

策略:

降低并发

WiFi:


5

弱网:


1

增大超时

例如:

普通:

30秒。

弱网:

60秒。


19.17 APP后台上传优化

后台:

系统关注:

资源。


需要:

控制:



CPU

网络

电量

例如:

锁屏:

降低:

worker数量。


19.18 电量感知

监听:



Battery Level

策略:


if(
battery<20
){

pauseLargeUpload()

}

19.19 温度控制

长时间视频上传:

设备发热。


策略:

检测:



thermal state

降低:


worker:

4

↓

1

19.20 大文件上传极限方案

例如:

100GB文件。

架构:



File


↓

Streaming Reader


↓

Chunk Generator


↓

Worker Pool


↓

Multipart Upload


↓

Checkpoint DB


↓

OSS

关键:

永远:

内存:


几十MB

不会:

跟文件大小增长。


19.21 性能参数推荐

普通图片


chunk:

1MB


worker:

3

高清视频


chunk:

20MB


worker:

4

超大文件


chunk:

50MB


worker:

3

19.22 性能优化检查表

内存

✅ 流式读取
✅ Buffer复用
✅ PixelMap释放
✅ 避免复制


CPU

✅ Hash异步
✅ 图片压缩控制
✅ Worker限制


网络

✅ HTTP复用
✅ 分片上传
✅ 动态并发


用户体验

✅ 后台上传
✅ 断点恢复
✅ 速度显示
✅ 暂停继续


19.23 生产级上传性能目标

大型APP:

目标:

上传成功率:


>99.9%

后台恢复:


>99%

内存增长:


<100MB

大文件:


10GB+

稳定上传。

第二十章 HarmonyOS NEXT 文件上传完整源码工程实战

本章开始进入完整工程实现阶段。

目标:

搭建一个可以直接用于企业项目的 HarmonyOS NEXT 上传框架。

最终能力:


 id="upload_arch"

选择文件

↓

创建上传任务

↓

任务队列

↓

分片处理

↓

并发上传

↓

断点保存

↓

OSS存储

↓

返回文件地址

20.1 创建 UploadSDK 工程

推荐工程结构:


UploadSDK


├── entry


├── upload-core


│


├── upload-network


│


├── upload-storage


│


├── upload-ui


│


└── upload-example

模块职责:

模块职责
upload-core任务管理
upload-networkHTTP/OSS
upload-storage数据库
upload-uiArkUI组件
example测试Demo

20.2 upload-core核心模块

目录:


upload-core


└── ets


    ├── manager


    │    UploadManager.ets


    │


    ├── model


    │    UploadTask.ets


    │


    ├── scheduler


    │    UploadScheduler.ets


    │


    └── worker


         UploadWorker.ets

20.3 UploadTask任务模型

文件:


UploadTask.ets

代码:


export enum UploadStatus {


WAITING="waiting",


UPLOADING="uploading",


PAUSED="paused",


SUCCESS="success",


FAILED="failed"


}



@Observed
export class UploadTask {


id:string



fileName:string



fileSize:number



progress:number=0



status:

UploadStatus=
UploadStatus.WAITING



uploadId:string=""


speed:number=0



constructor(
id:string,
fileName:string,
size:number
){


this.id=id


this.fileName=fileName


this.fileSize=size


}


}

20.4 UploadManager核心入口

所有业务:

通过这里调用。

文件:


UploadManager.ets

代码:


export class UploadManager {


private static instance:

UploadManager



private scheduler:

UploadScheduler



private constructor(){


this.scheduler=

new UploadScheduler()


}



static getInstance(){


if(!this.instance){


this.instance=

new UploadManager()


}



return this.instance


}



upload(file){

let task=

new UploadTask(

Date.now()
.toString(),

file.name,

file.size

)



this.scheduler.add(task)



return task


}


}

20.5 业务调用方式

页面:


let task =

UploadManager
.getInstance()
.upload(file)

返回:


{
"id":"173456789",

"status":"waiting",

"progress":0

}

20.6 UploadScheduler实现

负责:

任务调度。

文件:


UploadScheduler.ets

代码:


export class UploadScheduler {


tasks:
UploadTask[]=[]



workers:
UploadWorker[]=[]



constructor(){


for(
let i=0;i<3;i++
){


this.workers.push(

new UploadWorker(i)

)


}


}



add(task:UploadTask){


this.tasks.push(task)



this.dispatch()


}



dispatch(){


let worker=

this.workers.find(

w=>!w.busy

)



if(
worker
&&
this.tasks.length>0

){


let task=

this.tasks.shift()



worker.execute(task)



}



}


}

20.7 UploadWorker实现

文件:


UploadWorker.ets

代码:


export class UploadWorker {


id:number


busy:boolean=false



constructor(
id:number
){

this.id=id

}



async execute(
task:UploadTask
){


this.busy=true



task.status=

UploadStatus.UPLOADING



try{


await UploadEngine.upload(
task
)



task.status=

UploadStatus.SUCCESS



}catch(e){



task.status=

UploadStatus.FAILED



}



this.busy=false


}


}

20.8 UploadEngine上传核心

目录:


engine


UploadEngine.ets

职责:

连接:

  • 文件读取
  • 分片
  • 上传器

代码:


export class UploadEngine {



static async upload(
task:UploadTask
){


let chunks=

ChunkManager.create(

task.fileSize

)



for(
let chunk of chunks
){


await this.uploadChunk(
chunk,
task
)



}



}



}

20.9 Chunk模型

文件:


Chunk.ets

代码:


export class UploadChunk{


index:number


offset:number


size:number



status:string="waiting"



etag:string=""


}

20.10 ChunkManager

负责:

文件切片。


export class ChunkManager {



static create(
size:number
){


let result=[]


let chunkSize=

10*
1024*
1024



let count=

Math.ceil(

size/chunkSize

)



for(
let i=0;i<count;i++
){


result.push({

index:i,


offset:

i*chunkSize,


size:

Math.min(

chunkSize,

size-i*chunkSize

)

})


}



return result


}


}

20.11 OSS上传接口设计

不要绑定OSS。

定义:


interface StorageAdapter{


init(task)


uploadPart(
chunk
)


complete(
task
)


}

支持:

未来:


OSS

COS

S3

MinIO

20.12 OSS实现

文件:


OssAdapter.ets

代码:


export class OssAdapter
implements StorageAdapter{


async init(task){


return await Http.post(

"/multipart/init",

task

)

}



async uploadPart(chunk){


return await Http.upload(

chunk

)

}



async complete(task){


return await Http.post(

"/multipart/complete",

task

)

}



}

20.13 HTTP网络层封装

目录:


upload-network


HttpClient.ets

代码:


export class HttpClient {


async post(
url,
data
){


return await request.post({

url,

data

})


}



async upload(
url,
data
){


return await request.uploadFile({

url,

files:data

})


}



}

20.14 上传进度监听

增加:

EventEmitter。



export class UploadEvent {


private listeners=[]



on(callback){


this.listeners.push(callback)


}



emit(data){


this.listeners.forEach(

fn=>

fn(data)

)


}


}

任务:

绑定:


task.event.on(

(progress)=>{


console.log(progress)

}

)

20.15 ArkUI上传页面

示例:

图片上传。



@Entry
@Component
struct UploadPage {


@State progress:number=0



build(){


Column(){



Button("选择图片")


.onClick(()=>{


this.startUpload()


})



Progress({

value:this.progress


})



}


}



async startUpload(){


let file=

await Picker.select()



let task=

UploadManager

.getInstance()

.upload(file)



task.event.on(

data=>{


this.progress=

data.progress


}

)


}


}

20.16 完整调用链



UploadPage


↓

UploadManager


↓

UploadScheduler


↓

UploadWorker


↓

UploadEngine


↓

ChunkManager


↓

OssAdapter


↓

OSS

第二十一章 HarmonyOS NEXT 上传SDK生产级完善:断点续传数据库 + OSS Multipart完整实现

企业上传系统和普通上传最大的区别:

普通上传:


选择文件

↓

上传

↓

完成

生产级上传:


创建任务

↓

保存任务

↓

上传分片

↓

保存分片状态

↓

APP退出

↓

重新启动

↓

恢复任务

↓

继续上传

核心:

上传状态必须持久化。


21.1 上传任务持久化架构

整体:



UploadTask


      |


      ↓


UploadDatabase


      |


 -------------------


 |                 |


task表          chunk表


 |                 |


恢复             分片恢复


21.2 RelationalStore数据库初始化

HarmonyOS NEXT:

使用:


@kit.ArkData

导入:


import {

relationalStore

}

from '@kit.ArkData';

创建数据库:


export class UploadDatabase {


private store:

relationalStore.RdbStore



async init(context){


let config:

relationalStore.StoreConfig={


name:

"upload.db",


securityLevel:

relationalStore.SecurityLevel.S1


}



this.store=

await relationalStore.getRdbStore(

context,

config

)


}


}

21.3 创建upload_task表

任务表:

保存:

上传整体状态。


字段:



id

file_name

file_path

file_size

file_hash

upload_id

status

progress

create_time

update_time

SQL:


CREATE TABLE upload_task(

id TEXT PRIMARY KEY,


file_name TEXT,


file_path TEXT,


file_size INTEGER,


file_hash TEXT,


upload_id TEXT,


status TEXT,


progress INTEGER,


create_time INTEGER,


update_time INTEGER

)

21.4 创建upload_chunk表

分片状态:


CREATE TABLE upload_chunk(

id INTEGER PRIMARY KEY,


task_id TEXT,


chunk_index INTEGER,


chunk_size INTEGER,


chunk_offset INTEGER,


etag TEXT,


status TEXT

)

例如:

任务:


video.mp4

分片:



0 success

1 success

2 uploading

3 waiting

21.5 UploadTask保存

新增任务:


async insertTask(
task:UploadTask
){


let values:


relationalStore.ValuesBucket={



"id":

task.id,



"file_name":

task.fileName,



"file_size":

task.fileSize,



"status":

task.status



}



await this.store.insert(

"upload_task",

values

)


}

21.6 保存Chunk

创建:

100个分片。

循环:

保存。


async insertChunks(
taskId,
chunks
){


for(
let chunk of chunks
){



await this.store.insert(

"upload_chunk",

{


"task_id":

taskId,


"chunk_index":

chunk.index,


"status":

"waiting"


}


)


}


}

21.7 更新上传进度

上传过程中:


task.progress=60


database.updateTask(

task

)

SQL:


UPDATE upload_task

SET progress=?

WHERE id=?

21.8 更新Chunk状态

某个分片成功:


chunk.status=

"success"


chunk.etag=

etag

保存:


updateChunk(chunk)

数据库:



chunk1

success

etag:

xxxx


21.9 APP启动恢复流程

APP重新打开:



Ability启动


↓

UploadDatabase初始化


↓

查询未完成任务


↓

恢复UploadTask


↓

读取Chunk状态


↓

继续上传


21.10 查询未完成任务

SQL:


SELECT *

FROM upload_task

WHERE status != 'success'

代码:


async getRunningTasks(){


return await this.store.querySql(

`

SELECT *

FROM upload_task

WHERE status != ?

`,

[

"success"

]


)


}

21.11 恢复Chunk

查询:


SELECT *

FROM upload_chunk

WHERE task_id=?

AND status!='success'

返回:


[

{

index:3,

status:"waiting"

},

{

index:4,

status:"waiting"

}

]

只上传:

未完成部分。


21.12 OSS Multipart完整流程

大文件:

不能:

一次PUT。


流程:



CreateMultipartUpload


↓

UploadPart


↓

ListParts


↓

CompleteMultipartUpload


21.13 初始化Multipart

请求:


POST

/object?uploads

返回:


{

uploadId:

"CAFE123456"

}

保存:

数据库。


task.uploadId=

response.uploadId


updateTask(task)

21.14 上传Part

请求:


PUT

/object

?partNumber=1

&uploadId=xxx

返回:


ETag:

"abc123"

保存:


chunk.etag=

etag


updateChunk(chunk)

21.15 Complete Multipart

所有分片完成:

收集:


[

{

partNumber:1,

etag:"xxx"

},


{

partNumber:2,

etag:"yyy"

}

]

发送:


POST

?uploadId=xxx

成功:


task.status=

SUCCESS

21.16 秒传系统设计

大厂:

基本都有。

核心:

文件Hash。


流程:



用户选择文件


↓

计算Hash


↓

服务器查询


↓

存在


↓

返回URL


↓

完成

21.17 文件Hash计算

不要:

一次读取。

错误:


md5(file)

正确:

流式计算:



chunk1

↓

hash.update


chunk2

↓

hash.update


chunk3

↓

final


伪代码:


let hash=

new MD5()



for(
chunk of chunks
){


hash.update(chunk)


}



let result=

hash.digest()

21.18 秒传接口设计

客户端:


POST /file/check

参数:


{

hash:"xxx",

size:102400

}

服务器返回:

存在:


{

exist:true,

url:"https://xxx"

}

不存在:


{

exist:false

}

21.19 断点续传完整流程



开始上传


↓

生成UploadId


↓

保存数据库


↓

上传Chunk


↓

保存ETag


↓

APP关闭


↓

再次打开


↓

读取UploadId


↓

读取完成Chunk


↓

继续上传


↓

Complete


21.20 崩溃恢复案例

上传:


10GB视频

进度:


80%

突然:


APP被系统杀死

重新打开:

数据库:


{

progress:80,


uploadId:"xxx",


completed:[

0,

1,

2,

...

80

]

}

继续:


81%

↓

100%

21.21 生产级恢复能力

支持:

场景恢复
页面关闭
APP退出
系统杀死
网络断开
Token过期
手机重启

21.22 当前上传SDK能力

现在已经具备:



UploadManager


+

Scheduler


+

Worker


+

Chunk


+

Database


+

Multipart


+

Resume


+

Fast Upload

第二十二章 HarmonyOS NEXT 文件上传安全体系设计

企业文件上传系统中,上传链路属于高风险入口。

因为用户上传的数据:

可能包含:

  • 图片
  • 视频
  • 文档
  • 压缩包
  • 配置文件
  • 用户隐私数据

如果安全设计不足,会产生:

  • 恶意文件上传
  • 存储桶泄露
  • Token泄露
  • 文件覆盖攻击
  • 木马传播
  • 数据盗取

因此企业级上传系统必须建立:

客户端安全 + 网络安全 + 存储安全 + 服务端安全 四层体系。


22.1 文件上传安全整体架构

完整安全链路:



用户文件


 ↓


客户端校验


 ↓


安全Token


 ↓


HTTPS传输


 ↓


上传服务


 ↓


病毒检测


 ↓


OSS隔离存储


 ↓


审核发布


22.2 客户端安全职责

客户端主要负责:

第一道防线。

包括:



文件类型检测

文件大小限制

文件名称过滤

Hash计算

权限控制

Token管理

但是:

客户端不能作为最终安全依据。

原因:

客户端代码:

可以被:

  • 反编译
  • 修改
  • Hook

最终安全:

必须依赖服务器。


22.3 文件类型校验

危险场景:

用户上传:


virus.exe

然后:

修改:


avatar.jpg

仅判断:

文件名:

不安全。

错误:


if(file.name.endsWith(".jpg")){


allow()


}

22.4 MIME类型检测

读取:

文件头。

例如:

JPEG:


FF D8 FF

PNG:


89 50 4E 47

称为:

Magic Number。


22.5 FileValidator设计

创建:


FileValidator.ets

代码:


export class FileValidator {


validate(file){



this.checkSize(file)



this.checkType(file)



this.checkName(file)



return true



}



}

22.6 文件大小限制

例如:

头像:


5MB

视频:


2GB

配置:


const limit={


image:

5*1024*1024,


video:

2*1024*1024*1024


}

检查:


if(
file.size>limit
){

throw Error(
"file too large"
)

}

22.7 文件名安全过滤

危险:


../../config.json

攻击:

路径穿越。


过滤:


function sanitizeName(
name
){


return name.replace(

/[^a-zA-Z0-9._-]/g,

""

)


}

22.8 禁止危险后缀

例如:



.exe

.sh

.bat

.apk

.js

黑名单:


const deny=[

".exe",

".sh",

".bat"

]

22.9 上传Token安全设计

客户端:

不能保存:

永久AccessKey。

错误:



AccessKey

SecretKey

写死APP

原因:

APK/HAP:

可以分析。


正确:

使用:

STS临时Token。


22.10 STS Token流程



APP


 ↓


业务服务器


 ↓


申请临时权限


 ↓


STS Token


 ↓


OSS上传


Token:

包含:


{

accessKey:"xxx",

expire:

3600,

policy:"upload-only"

}

22.11 Token生命周期

例如:

有效:


15分钟

过期:

自动刷新。


流程:



上传


↓

401


↓

刷新Token


↓

继续Chunk


22.12 OSS权限隔离

不要:

所有文件:

同一个Bucket。


推荐:



avatar-bucket


video-bucket


document-bucket


temp-bucket

权限:

不同。


22.13 临时目录隔离

上传:

不要直接:

正式目录。


流程:



upload/temp/


↓

审核


↓

move


↓

publish/

例如:



temp/video001.mp4


↓

media/video001.mp4

22.14 HTTPS安全

所有上传:

必须:


HTTPS

禁止:


http://

防止:

  • 中间人攻击
  • Token窃取
  • 文件篡改

22.15 证书校验

高级场景:

Certificate Pinning。


客户端保存:

服务器证书指纹。


连接:

比较。


防止:

代理抓包。


22.16 上传签名机制

请求:

增加签名。

例如:


{

fileHash:"xxx",

timestamp:123456,


sign:"abcdef"

}

服务器:

验证:



hash

+

timestamp

+

secret

22.17 防止重放攻击

攻击:

重复发送:

同一个上传请求。


解决:

加入:

timestamp。


例如:

允许:


5分钟

超过:

拒绝。


22.18 文件病毒扫描

企业:

上传完成后:

不能立即开放。


流程:



Upload


↓

Scan


↓

Safe?


↓

Publish


状态:


enum FileStatus{


UPLOADING,


SCANNING,


SAFE,


BLOCKED


}

22.19 图片安全处理

图片可能:

包含:

EXIF信息。

例如:

GPS位置。


上传前:

清理:



EXIF


GPS


Camera Info

保护:

用户隐私。


22.20 视频安全处理

视频:

可能包含:

隐藏信息。

处理:



重新编码


↓

生成新文件


↓

发布

22.21 数据库安全

本地:

上传记录。

包含:

  • 文件路径
  • Token信息
  • UploadId

不能明文保存。


敏感字段:

加密。

例如:



uploadId

token

22.22 本地安全存储

HarmonyOS:

使用:

安全存储。

例如:


Preferences

+

加密

不要:


localStorage

保存Token。


22.23 上传安全日志

记录:



用户

文件Hash

时间

结果

IP

用于:

审计。


22.24 企业安全架构

最终:



              Client


                |


        FileValidator


                |


          HTTPS + Sign


                |


        Upload Gateway


                |


      -------------------


      |                 |


   Scanner           OSS


      |                 |


      -------------------


                |


          File Service

第二十三章 HarmonyOS NEXT 文件上传云端架构设计:OSS、CDN、文件服务平台

企业应用中,文件上传完成并不代表结束。

真正完整的链路:



用户上传文件


↓

上传服务


↓

对象存储 OSS


↓

文件处理服务


↓

CDN分发


↓

用户访问


23.1 为什么需要文件服务平台

简单项目:


APP

↓

OSS

即可。

但是企业:

会出现:

  • 文件权限复杂
  • 多业务共享
  • 图片需要压缩
  • 视频需要转码
  • 文件需要审核
  • CDN需要加速

所以需要:

File Service 文件服务中心。


23.2 企业文件平台整体架构



                HarmonyOS APP


                      |


                      ↓


              Upload Gateway


                      |


        ---------------------------


        |                         |


 File Service              Upload Service


        |                         |


        -----------+-------------


                   |


                  OSS


                   |


        ----------------


        |              |


      CDN          Processing


23.3 Upload Gateway上传网关

作用:

统一入口。

所有上传:

经过:


api.company.com/upload

负责:

  • 身份认证
  • Token校验
  • 限流
  • 路由

请求:


POST /upload/create

参数:


{

fileName:

"test.jpg",


size:

102400,


hash:

"xxxx"


}

返回:


{

taskId:

"10001",


uploadToken:

"xxx"


}

23.4 文件服务中心设计

核心:

File Service。

管理:



文件元数据

权限

生命周期

访问地址

业务关系

数据库:



file_info


file_permission


file_relation


23.5 file_info文件表

设计:


CREATE TABLE file_info(

id BIGINT,


hash VARCHAR(64),


name VARCHAR(255),


size BIGINT,


type VARCHAR(50),


url TEXT,


status VARCHAR(20),


create_time DATETIME


)

数据:


{

"id":10001,


"name":"avatar.png",


"size":204800,


"status":"safe"

}

23.6 文件和业务关联

同一个文件:

可能:

多个业务使用。

例如:

商品图片:

商城使用。

社区帖子:

也使用。


不要复制文件。

设计:

file_relation。



CREATE TABLE file_relation(

file_id BIGINT,


biz_type VARCHAR(50),


biz_id VARCHAR(64)


)

例如:



file_id:

10001


biz:

product


id:

8888

23.7 秒传系统云端设计

客户端:

计算:

Hash。

服务器:

查询:


SELECT *

FROM file_info

WHERE hash=?

存在:

返回:


{

exist:true,


url:"xxx"

}

不存在:

创建上传任务。


23.8 OSS目录规划

不要:

全部放根目录。

推荐:



bucket


├── avatar


│


├── video


│


├── image


│


├── document


│


└── temp


23.9 文件命名策略

不要:

使用用户文件名。

例如:


test.jpg

可能:

覆盖。


推荐:

UUID。

例如:



20260731/

a8f923fd.jpg

规则:



业务

+

日期

+

UUID

23.10 CDN加速架构

用户访问:

不要:

直接OSS。


错误:



用户

↓

OSS

正确:



用户


↓

CDN节点


↓

OSS源站

优势:

  • 降低延迟
  • 减少OSS压力
  • 节省成本

23.11 文件URL生成

三种方式。


公开URL

例如:

头像。



https://cdn.com/a.jpg

私有URL

需要签名。

例如:



https://cdn.com/a.jpg?token=xxx

临时URL

有效时间:



10分钟

23.12 图片处理服务

上传:

原图。

后台:

生成:



原图


↓

缩略图


↓

WebP


↓

水印

例如:



original.jpg


small.jpg


thumb.jpg

23.13 图片处理流程



Upload Success


↓

Image Service


↓

Resize


↓

Compress


↓

Store


↓

CDN

23.14 视频处理服务

视频:

上传完成:

进入任务队列。


流程:



video.mp4


↓

Transcoding


↓

480P


↓

720P


↓

1080P


↓

M3U8


↓

CDN

23.15 视频转码状态


enum VideoStatus{


UPLOADED,


PROCESSING,


SUCCESS,


FAILED


}

APP:

显示:



视频处理中...

23.16 文件审核流程

企业:

UGC场景。

例如:

社区。

流程:



上传


↓

审核


↓

通过


↓

发布

状态:



WAIT_SCAN


SAFE


BLOCK

23.17 文件权限系统

例如:

企业网盘。

权限:



owner


reader


writer


admin

表:


file_permission

file_id

user_id

role

23.18 文件删除策略

不要:

直接删除OSS。


采用:

软删除。

状态:



ACTIVE


↓

DELETED


↓

CLEAN

定时任务:

清理。


23.19 文件生命周期

完整:



上传


↓

存储


↓

访问


↓

低频


↓

归档


↓

删除

23.20 企业文件平台能力

最终:



File Platform


├── Upload


├── Storage


├── CDN


├── Image Service


├── Video Service


├── Permission


├── Audit


└── Monitor

23.21 HarmonyOS客户端最终接入

业务:

只关心:


UploadManager.upload({

file

})

返回:


{

fileId:"10001",


url:"https://cdn.xxx.com/a.jpg"

}

23.22 典型企业应用

电商

商品:

  • 图片
  • 视频
  • 详情附件

IM

聊天:

  • 图片
  • 文件
  • 视频

直播

主播:

  • 视频
  • 封面
  • 回放

企业网盘

  • 大文件
  • 分片
  • 权限

23.23 完整云端链路



HarmonyOS NEXT


      |

      ↓


Upload SDK


      |

      ↓


Upload Gateway


      |

      ↓


File Service


      |

      ↓


OSS


      |

      ↓


Processing


      |

      ↓


CDN


      |

      ↓


用户访问

第二十四章 HarmonyOS NEXT 文件上传项目实战:商城商品图片上传系统

电商商城是文件上传最典型的业务场景。

商品中心通常需要:

  • 商品主图
  • 商品轮播图
  • 商品详情图
  • 商品视频
  • 商品规格附件

一个商品可能:


id="u4m7qx"
商品A


图片:

10张


视频:

1个


详情附件:

多个

24.1 商品图片上传业务架构

完整流程:


id="p8n3mv"

商家后台


↓

选择商品图片


↓

图片预处理


↓

Upload SDK


↓

文件服务


↓

OSS


↓

图片处理


↓

CDN


↓

商品发布

24.2 商品图片数据模型

商品:


id="m7x2qw"
{

productId:"10001",


name:"手机",


images:[


{

fileId:"f001",


url:"https://cdn.xxx/1.jpg",


sort:1


},


{

fileId:"f002",


url:"https://cdn.xxx/2.jpg",


sort:2


}


]

}

24.3 商品图片表设计

数据库:


id="k9q5mw"
CREATE TABLE product_image(

id BIGINT,


product_id BIGINT,


file_id BIGINT,


url VARCHAR(500),


sort INT,


create_time DATETIME

)

24.4 商品图片上传流程

用户点击:


id="c5m8zx"

添加图片


执行:


id="x8q3mv"

PhotoPicker


↓

选择图片


↓

ImageProcessor


↓

UploadManager


↓

返回URL


↓

保存商品

24.5 HarmonyOS图片选择

使用:

PhotoViewPicker。


示例:


id="r7m2qx"
import {

photoAccessHelper

}

from '@kit.MediaLibraryKit';

创建:


id="h4v8mz"
let picker =

new photoAccessHelper.PhotoViewPicker()

选择:


id="n6x3qp"
let result =

await picker.select({

MIMEType:

'image/*'

})

返回:


id="b8m5qw"
{

uri:

"file://xxx.jpg"

}

24.6 图片上传前处理

不要直接上传原图。

流程:


id="z9q4mv"

原图片


↓

读取PixelMap


↓

压缩


↓

转换格式


↓

上传

24.7 商品图片压缩策略

不同用途:

不同质量。

例如:

主图


id="m3x8qp"

尺寸:

800x800


质量:

85%

详情图


id="q7v2mz"

尺寸:

1200px


质量:

80%

24.8 ImageProcessor设计

目录:


id="v5m9qx"
image


ImageProcessor.ets

代码:


id="w8q2mk"
export class ImageProcessor {


static async compress(
file
){


let pixelMap=

await ImageSource.create(

file

)



let result=

await ImagePacker.pack(

pixelMap,

{

quality:80

}

)



return result


}



}

24.9 多图片上传

商品:

一次:

10张图片。

不能:

串行。


错误:


id="f3x7mv"

图片1

↓

完成


图片2

↓

完成


图片3

↓

完成

耗时:

过长。


正确:

并发上传。


id="h8m4qx"

图片1

图片2

图片3


同时上传


24.10 商品多文件任务模型

创建:

UploadBatch。


id="r6q9mv"
class UploadBatch{


id:string


tasks:

UploadTask[]=[]



progress:number=0


}


24.11 批量上传管理


id="n4x7pz"
uploadImages(files){



let tasks=[]



files.forEach(file=>{


tasks.push(

UploadManager.upload(file)

)


})



return tasks



}

24.12 上传列表UI设计

页面:


id="x2m8qw"

商品图片


+ 添加图片



[图片1]

上传完成



[图片2]

上传中 60%



[图片3]

等待

ArkUI:


id="q8m3vz"
ForEach(

tasks,

task=>{


UploadItem({

task

})


}

)

24.13 商品图片排序

商城:

支持拖动排序。

例如:


id="w7m4qx"

图片A

图片B

图片C


拖动


图片C

图片A

图片B

保存:

sort字段。


24.14 上传完成回调

上传成功:


id="c9m5xp"
task.onSuccess=

(url)=>{


product.images.push({

url:url

})


}

最终:

提交商品。


24.15 商品草稿保存

企业后台:

不能要求:

全部上传完成才能保存。


支持:

草稿。

状态:


id="k3q8mv"

DRAFT


↓

UPLOADING


↓

READY


↓

PUBLISHED

24.16 上传失败处理

例如:

第5张失败。

不要:

全部失败。


显示:


id="m8x4pz"

图片1 √


图片2 √


图片3 √


图片4 √


图片5 ×


[重新上传]

24.17 商品视频上传

商品视频:

特点:

大。

使用:

Multipart。

流程:


id="v7m2qx"

video.mp4


↓

切片


↓

上传


↓

转码


↓

生成播放地址

24.18 商品视频封面生成

上传视频后:

服务器:

抽取第一帧。

生成:


id="p9x5mw"

video-cover.jpg

商品:

保存:


id="z6q2mv"
{

video:

"video.mp4",


cover:

"cover.jpg"

}

24.19 OSS目录设计

推荐:


id="h3m8qx"

product/


├── 2026/


│


├── 07/


│


└── productId/


      |

      ├── image/


      |

      └── video/

例如:


id="r5x8mv"

product/2026/07/10001/image/a001.jpg

24.20 CDN访问

上传:

OSS地址:


id="w2q7mz"

oss.xxx.com/a.jpg

转换:

CDN:


id="n8m3qx"

cdn.xxx.com/a.jpg

用户访问:

CDN。


24.21 商品图片安全

上传:

检查:


id="x6q9mv"

文件类型

文件大小

图片内容

禁止:

伪装图片。


24.22 商品后台完整代码结构


id="m4x7qw"

product


├── ProductCreatePage.ets


├── ProductImagePicker.ets


├── ProductUploadList.ets


├── ProductVideoUpload.ets


└── ProductService.ets

24.23 ProductImagePicker组件


id="c7m2vx"
@Component
export struct ProductImagePicker {


tasks:

UploadTask[]=[]



build(){


Column(){


Button(
"添加图片"
)


ForEach(

this.tasks,

task=>{


UploadItem({

task

})


}

)


}


}



}

24.24 商品发布完整链路

最终:


id="u9m5qx"

商家选择图片


↓

图片压缩


↓

UploadSDK


↓

OSS


↓

图片服务


↓

CDN


↓

保存商品


↓

商品上线

第二十四章 HarmonyOS NEXT 文件上传项目实战:商城商品图片上传系统

电商商城是文件上传最典型的业务场景。

商品中心通常需要:

  • 商品主图
  • 商品轮播图
  • 商品详情图
  • 商品视频
  • 商品规格附件

一个商品可能:


id="u4m7qx"
商品A


图片:

10张


视频:

1个


详情附件:

多个

24.1 商品图片上传业务架构

完整流程:


id="p8n3mv"

商家后台


↓

选择商品图片


↓

图片预处理


↓

Upload SDK


↓

文件服务


↓

OSS


↓

图片处理


↓

CDN


↓

商品发布

24.2 商品图片数据模型

商品:


id="m7x2qw"
{

productId:"10001",


name:"手机",


images:[


{

fileId:"f001",


url:"https://cdn.xxx/1.jpg",


sort:1


},


{

fileId:"f002",


url:"https://cdn.xxx/2.jpg",


sort:2


}


]

}

24.3 商品图片表设计

数据库:


id="k9q5mw"
CREATE TABLE product_image(

id BIGINT,


product_id BIGINT,


file_id BIGINT,


url VARCHAR(500),


sort INT,


create_time DATETIME

)

24.4 商品图片上传流程

用户点击:


id="c5m8zx"

添加图片


执行:


id="x8q3mv"

PhotoPicker


↓

选择图片


↓

ImageProcessor


↓

UploadManager


↓

返回URL


↓

保存商品

24.5 HarmonyOS图片选择

使用:

PhotoViewPicker。


示例:


id="r7m2qx"
import {

photoAccessHelper

}

from '@kit.MediaLibraryKit';

创建:


id="h4v8mz"
let picker =

new photoAccessHelper.PhotoViewPicker()

选择:


id="n6x3qp"
let result =

await picker.select({

MIMEType:

'image/*'

})

返回:


id="b8m5qw"
{

uri:

"file://xxx.jpg"

}

24.6 图片上传前处理

不要直接上传原图。

流程:


id="z9q4mv"

原图片


↓

读取PixelMap


↓

压缩


↓

转换格式


↓

上传

24.7 商品图片压缩策略

不同用途:

不同质量。

例如:

主图


id="m3x8qp"

尺寸:

800x800


质量:

85%

详情图


id="q7v2mz"

尺寸:

1200px


质量:

80%

24.8 ImageProcessor设计

目录:


id="v5m9qx"
image


ImageProcessor.ets

代码:


id="w8q2mk"
export class ImageProcessor {


static async compress(
file
){


let pixelMap=

await ImageSource.create(

file

)



let result=

await ImagePacker.pack(

pixelMap,

{

quality:80

}

)



return result


}



}

24.9 多图片上传

商品:

一次:

10张图片。

不能:

串行。


错误:


id="f3x7mv"

图片1

↓

完成


图片2

↓

完成


图片3

↓

完成

耗时:

过长。


正确:

并发上传。


id="h8m4qx"

图片1

图片2

图片3


同时上传


24.10 商品多文件任务模型

创建:

UploadBatch。


id="r6q9mv"
class UploadBatch{


id:string


tasks:

UploadTask[]=[]



progress:number=0


}


24.11 批量上传管理


id="n4x7pz"
uploadImages(files){



let tasks=[]



files.forEach(file=>{


tasks.push(

UploadManager.upload(file)

)


})



return tasks



}

24.12 上传列表UI设计

页面:


id="x2m8qw"

商品图片


+ 添加图片



[图片1]

上传完成



[图片2]

上传中 60%



[图片3]

等待

ArkUI:


id="q8m3vz"
ForEach(

tasks,

task=>{


UploadItem({

task

})


}

)

24.13 商品图片排序

商城:

支持拖动排序。

例如:


id="w7m4qx"

图片A

图片B

图片C


拖动


图片C

图片A

图片B

保存:

sort字段。


24.14 上传完成回调

上传成功:


id="c9m5xp"
task.onSuccess=

(url)=>{


product.images.push({

url:url

})


}

最终:

提交商品。


24.15 商品草稿保存

企业后台:

不能要求:

全部上传完成才能保存。


支持:

草稿。

状态:


id="k3q8mv"

DRAFT


↓

UPLOADING


↓

READY


↓

PUBLISHED

24.16 上传失败处理

例如:

第5张失败。

不要:

全部失败。


显示:


id="m8x4pz"

图片1 √


图片2 √


图片3 √


图片4 √


图片5 ×


[重新上传]

24.17 商品视频上传

商品视频:

特点:

大。

使用:

Multipart。

流程:


id="v7m2qx"

video.mp4


↓

切片


↓

上传


↓

转码


↓

生成播放地址

24.18 商品视频封面生成

上传视频后:

服务器:

抽取第一帧。

生成:


id="p9x5mw"

video-cover.jpg

商品:

保存:


id="z6q2mv"
{

video:

"video.mp4",


cover:

"cover.jpg"

}

24.19 OSS目录设计

推荐:


id="h3m8qx"

product/


├── 2026/


│


├── 07/


│


└── productId/


      |

      ├── image/


      |

      └── video/

例如:


id="r5x8mv"

product/2026/07/10001/image/a001.jpg

24.20 CDN访问

上传:

OSS地址:


id="w2q7mz"

oss.xxx.com/a.jpg

转换:

CDN:


id="n8m3qx"

cdn.xxx.com/a.jpg

用户访问:

CDN。


24.21 商品图片安全

上传:

检查:


id="x6q9mv"

文件类型

文件大小

图片内容

禁止:

伪装图片。


24.22 商品后台完整代码结构


id="m4x7qw"

product


├── ProductCreatePage.ets


├── ProductImagePicker.ets


├── ProductUploadList.ets


├── ProductVideoUpload.ets


└── ProductService.ets

24.23 ProductImagePicker组件


id="c7m2vx"
@Component
export struct ProductImagePicker {


tasks:

UploadTask[]=[]



build(){


Column(){


Button(
"添加图片"
)


ForEach(

this.tasks,

task=>{


UploadItem({

task

})


}

)


}


}



}

24.24 商品发布完整链路

最终:


id="u9m5qx"

商家选择图片


↓

图片压缩


↓

UploadSDK


↓

OSS


↓

图片服务


↓

CDN


↓

保存商品


↓

商品上线

第二十五章 HarmonyOS NEXT 文件上传项目实战:IM即时通讯图片/文件上传系统

即时通讯(IM)是文件上传复杂度最高的业务之一。

因为 IM 上传不是简单:


选择文件

↓

上传

↓

发送

而是:



用户选择图片


↓

立即显示消息气泡


↓

后台上传文件


↓

上传成功


↓

发送正式消息


↓

对方收到

用户体验必须类似微信、Telegram、企业微信。


25.1 IM文件上传整体架构

完整链路:




          用户A


            |


            ↓


       HarmonyOS APP


            |


   -------------------


   |                 |


消息模块          上传模块


   |                 |


   ↓                 ↓


WebSocket        Upload SDK


   |                 |


   ↓                 ↓


IM Server          OSS


   |


   ↓


用户B

25.2 IM上传核心特点

相比商城:

IM多了:

1. 即时性

用户点击发送:

马上看到消息。


2. 状态同步

需要:



发送中


上传中


发送成功


失败


3. 离线恢复

APP关闭:

重新打开:

继续发送。


25.3 消息状态设计

定义:



export enum MessageStatus{


CREATING,


UPLOADING,


UPLOADED,


SENDING,


SENT,


FAILED


}

状态流:




CREATING


↓

UPLOADING


↓

UPLOADED


↓

SENDING


↓

SENT


失败:



UPLOADING


↓

FAILED


25.4 IM消息数据模型

Message:


export class Message{


id:string



conversationId:string



type:string



content:string



fileId:string



status:

MessageStatus



createTime:number


}

图片消息:



{

type:"image",


fileId:"f10001",


url:"cdn.xxx.com/a.jpg"


}

25.5 图片发送流程

用户:

点击图片。

流程:



PhotoPicker


↓

生成本地消息


↓

显示聊天气泡


↓

压缩图片


↓

上传OSS


↓

更新消息


↓

发送服务器

25.6 为什么先显示消息

错误:

等待上传完成:



选择图片


↓

等待30秒


↓

显示消息

用户感觉:

卡顿。


正确:

乐观更新。


类似微信:

立即显示:



[图片]

上传中...

25.7 创建临时消息

选择图片后:

立即创建。


let message=

new Message()



message.id=

generateUUID()



message.status=

MessageStatus.UPLOADING



message.localPath=

file.uri

保存:

本地数据库。


25.8 IM本地消息数据库

表:

message。



CREATE TABLE message(

id TEXT PRIMARY KEY,


conversation_id TEXT,


type TEXT,


content TEXT,


file_id TEXT,


status TEXT,


create_time INTEGER

)

25.9 上传绑定消息

UploadTask:

增加:

messageId。




class UploadTask{


id:string


messageId:string


progress:number


}

关系:




Message


 |

 |

UploadTask


 |

 |

File

25.10 图片压缩

聊天图片:

不需要原图。

策略:



原图:

8MB


↓

压缩


↓

500KB

优势:

  • 上传快
  • 节省流量
  • 降低服务器成本

25.11 IM图片处理

流程:



Image


↓

Resize


↓

Compress


↓

Remove EXIF


↓

Upload

25.12 Upload SDK调用

聊天模块:

不关心OSS。

代码:



let task=

UploadManager

.upload({

file,

type:"chat_image"

})


监听:



task.onProgress(

p=>{


message.progress=p


}

)

25.13 上传完成更新消息

成功:



task.onSuccess(

result=>{


message.fileId=

result.fileId



message.status=

MessageStatus.UPLOADED



sendMessage(message)



}

)

25.14 WebSocket发送消息

上传成功:

发送:



{

type:"image",


fileId:"10001",


url:"cdn.xxx/a.jpg"

}

服务器:

转发。


25.15 接收方处理

收到:



{

type:"image",

url:"xxx"

}

显示:



图片

↓

加载CDN

↓

展示

25.16 图片缓存系统

聊天大量图片:

必须缓存。

三级:




Memory Cache


      ↓


Disk Cache


      ↓


CDN


25.17 ImageCache设计



class ImageCache{


memory:


Map<string,PixelMap>



get(url){


return this.memory.get(url)

}


}

25.18 大文件发送

例如:

发送:

1GB视频。

不能:

普通上传。


采用:

Multipart。

流程:



视频


↓

Chunk


↓

Upload SDK


↓

OSS


↓

生成文件ID


↓

发送消息

25.19 文件消息模型



{

type:"file",


name:"demo.zip",


size:1024000000,


fileId:"f20001"


}

25.20 文件上传进度展示

聊天气泡:



demo.zip


██████░░░░


60%


状态:

绑定:

UploadTask。


25.21 断网恢复

场景:

上传:

80%。

网络断开。


保存:



{

uploadId:"xxx",


completedChunks:[

1,

2,

3

]


}

恢复:

继续上传。


25.22 APP重启恢复

启动:

查询:



SELECT *

FROM message

WHERE status!='SENT'

恢复:



未发送消息


↓

重新上传


↓

重新发送

25.23 失败重试

用户点击:

重新发送。


代码:



retryMessage(id){



let msg=

findMessage(id)



UploadManager

.resume(

msg.taskId

)


}

25.24 视频消息流程

短视频:



录制


↓

生成缩略图


↓

压缩


↓

上传


↓

转码


↓

发送消息

25.25 视频转码状态同步

服务器:

返回:



{

status:

"processing"

}

客户端:

显示:



视频处理中...

完成:

推送:



{

status:

"ready"

}

25.26 IM上传架构总结

完整:



Message


 |

 |

UploadTask


 |

 |

UploadSDK


 |

 |

OSS


 |

 |

FileService


 |

 |

WebSocket


 |

 |

Receiver
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐