个人应用接入使用阿里云盘和百度网盘

一、阿里云盘

官方文档
接入流程 · 语雀流程概述服务端 API 调用流程如下图所示1. 创建账...icon-default.png?t=O83Ahttps://www.yuque.com/aliyundrive/zpfszx/btw0tw

1. 接入授权
1.1. App Key、App Secret和用户授权验证

在通过网盘开发者认证之后,创建个人应用会生成APP ID(对应App Key)和App Secret,后续在组装生成用户授权验证地址时会使用到,同时也需要配置好Scopes(授权范围)以及回调URI(用于授权后跳转到个人应用等),参数细节详见官方文档。

// 生成授权二维码链接
// 此处只填写了必须的参数
func GenerateAuthUrl() string {
	params := url.Values{}
	params.Add("response_type", "code")
	params.Add("client_id", appKey)		
	params.Add("redirect_uri", redirectUri)
	// params.Add("state", state) // 可通过填入随机数来防止CSRF攻击
	params.Add("scope", scope)

	authUrlWithParams := authUrl + "?" + params.Encode()
	return authUrlWithParams
}
1.2. 授权验证方式壹——无后端服务授权模式

阿里云盘通过OAuth2.0 + PKCE的方式,通过提供code_verifier(常通过个人应用进行运算得来)来进行一种无需App Secret的授权验证模式。流程图(Sequence Diagram)

通过code_verifier做参数请求阿里云盘,交换得到的Access Token,默认有效期为30天。

(此次并未作代码实践)

1.3. 授权验证方式贰——扫码授权验证

流程图如下:

实践中,最初通过"github.com/skip2/go-qrcode",把组装的授权验证链接转换为二维码,可以正常工作。

// 生成二维码图片并保存
func SaveQrCode(authUrl string) {
	qrCode, _ := qrcode.New(authUrl, qrcode.Medium)
	err := qrCode.WriteFile(256, "auth_qr_code.png")
	if err != nil {
		log.Fatal(err)
	}
    
	fmt.Println("QR code saved as auth_qr_code.png")
}

后续又使用API获取授权二维码,同样是可以工作的,有效时长默认3分钟。

// 获取授权二维码的链接和用于校验状态的sid
func GetQrCodeFromAli() (string, string, error) {
	reqBody := map[string]interface{}{
		"client_id":     appKey,
		"client_secret": appSecret,
		"scopes":        scopes,
	}

	jsonData, _ := json.Marshal(reqBody)
	req, err := http.NewRequest("POST", apiBase+"/oauth/authorize/qrcode", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", "", err
	}
	req.Header.Set("Content-Type", "application/json")
	defer req.Body.Close()

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", "", err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	var qrCodeResp QrCodeResponse
	_ = json.Unmarshal(body, &qrCodeResp)

	return qrCodeResp.QrCodeUrl, qrCodeResp.Sid, nil
}

// 获取二维码图片
func GetQrCodePic(sid string) {
	// 构建请求 URL
	url := fmt.Sprintf("%s/oauth/qrcode/%s", apiBase, sid)

	// 创建请求
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		log.Fatalf("Failed to create request: %v", err)
	}

	// 发送请求
    client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		log.Fatalf("Failed to send request: %v", err)
	}
	defer resp.Body.Close()

	// 创建本地文件
	filePath := "qrcode.jpeg"
	file, err := os.Create(filePath)
	if err != nil {
		log.Fatalf("Failed to create file: %v", err)
	}
	defer file.Close()

	// 写入文件
	_, err = io.Copy(file, resp.Body)
	if err != nil {
		log.Fatalf("Failed to write to file: %v", err)
	}

	log.Printf("QR code image saved to %s", filePath)
}
1.4. 用code_verifier或code(authCode)交换获取Access Token

上述两种不同的方式在交换获取Access Token的方法是有差异的,无后端模式扫码后会提供code_verifier,而扫码模式将会提供code(或authCode),从官方文档中可见,在传参的时候有一些差异(下方代码是传code)。

// 交换Access Token
func ExchangeToken(code string) (*TokenResponse, error) {
    params := url.Values{}
    params.Add("grant_type", "authorization_code")
    params.Add("code", code)
    params.Add("client_id", appKey)
    params.Add("client_secret", appSecret)
    params.Add("redirect_uri", redirectUri)

    resp, err := http.PostForm(tokenUrl, params)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    var tokenResp TokenResponse
    err = json.Unmarshal(body, &tokenResp)
    if err != nil {
        return nil, err
    }

    if tokenResp.Code != "" {
        return nil, fmt.Errorf("API error: %s", tokenResp.Message)
    }

    return &tokenResp, nil
}

拿到Access Token之后就可以个人应用就可以调用API来与阿里云盘交互了。

2. 上传文件

想要通过个人应用(通过授权校验后)上传文件到阿里云盘,主要有三个流程:

  1. 创建文件
  2. 文件上传
  3. 上传完毕

不过,一系列的文件操作都需要提供唯一标识用户云盘的drive_id,可以通过一个POST请求来获取。

2.1. 获取drive_id

在官方文档中,没有在这一部分再次说明需要设置Header,不过想要获得drive_id,是通过access_token获取用户信息,要把access_token放到head里。

// 获取用户信息
func GetUserDriveInfo(accessToken string) (*UserDriveInfoResponse, error) {
	req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/user/getDriveInfo", nil)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+accessToken)  // 设置Header

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var userDriveInfoResp UserDriveInfoResponse
	json.Unmarshal(body, &userDriveInfoResp)

	return &userDriveInfoResp, nil
}

有了access_token和drive_id之后,可以开始上传文件的流程了。

2.2. 创建文件

在正式上传文件之前,会在阿里云盘的服务器上创建一个占位符或元数据记录,不是实际的文件内容,而是文件的元数据,包括文件名、大小、类型等信息。最后返回一个唯一的文件标识符(file_id或upload_id)在后续的上传过程中使用。

发送请求时的必要参数有:

// 创建文件
func createFile(accessToken string, driveId string, parentFileID string, fileName string, fileSize int64, checkNameMode string) (*CreateFileResponse, error) {
	reqBody := map[string]interface{}{
		"drive_id":         driveId,
		"parent_file_id":   parentFileID,
		"name":             fileName,
		"type":             "file",
		"check_name_mode":  checkNameMode,
		"size":             fileSize,		   // 如果后续是秒传,这个参数是必须的,此处不是秒传
		"local_created_at": time.Now().Unix(), // 选填参数
	}

	jsonData, _ := json.Marshal(reqBody)

	req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/openFile/create", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	var createFileResp CreateFileResponse
	json.Unmarshal(body, &createFileResp)

	return &createFileResp, nil
}

返回参数上,创建好文件之后,会返回part_info_list[*].upload_url、file_id、upload_id,后续的上传会通过这个链接进行。

2.3. 刷新获取上传链接

除了创建文件之后会返回上传链接,也可以根据创建文件后返回的upload_id来获取上传链接。

// 获取上传链接
func getUploadUrl(accessToken string, driveId string, fileId, uploadId string) ([]struct {
	PartNumber int    `json:"part_number"`
	UploadUrl  string `json:"upload_url"`
}, error) {

	reqBody := map[string]interface{}{  // 都是必填参数
		"drive_id":  driveId,
		"file_id":   fileId,
		"upload_id": uploadId,
		"part_info_list": []map[string]int{
			{"part_number": 1},
		},
	}

	jsonData, _ := json.Marshal(reqBody)

	req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/openFile/getUploadUrl", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+accessToken)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	var createFileResp struct {
		PartInfoList []struct {
			PartNumber int    `json:"part_number"`
			UploadUrl  string `json:"upload_url"`
		} `json:"part_info_list"`
	}
	json.Unmarshal(body, &createFileResp)
	return createFileResp.PartInfoList, nil
}
2.4. 上传文件

主流的上传方式是分片上传,也支持单步上传和文件秒传。

2.4.1. 单片上传

在阿里云盘中,单文件上传最大支持5GB,通过一个向上传链接发送PUT请求。

// 上传文件
func upload(accessToken string, driveId string, dstDir string, filePath string) error {
    // 1. create
    fileSize, _ := GetFileSize(filePath)

    // 创建文件请求的响应结果
    createFileResp, _ := createFile(accessToken, driveId, dstDir, filePath, fileSize, "auto_rename")

    // 2. upload
    file, err := os.Open(filePath)
    if err != nil {
        return err
    }
    defer file.Close()

    uploadUrl, _ := getUploadUrl(accessToken, driveId, createFileResp.FileId, createFileResp.UploadId)

    req, err := http.NewRequest("PUT", uploadUrl[0].UploadUrl, file)
    req.Header.Set("Authorization", "Bearer "+accessToken)
    if err != nil {
        return err
    }

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    // 3. complete
    return completeUpload(accessToken, driveId, createFileResp.FileId, createFileResp.UploadId)
}
2.4.2. 分片上传

与单步上传相比,分片上传基本就是把一个大文件分为了多个部分,进行了多次单片上传。不过,在参数上有不同。分片上传需要在创建文件后刷新获取上传链接,而在刷新获取上传链接时,part_info_list不再只分为一片。此外,如何分片,也是必不可少的。

// 刷新获取上传链接时,提供的参数有所不同
reqBody := map[string]interface{}{  // 都是必填参数
		"drive_id":  driveId,
		"file_id":   fileId,
		"upload_id": uploadId,
		"part_info_list": []map[string]int{
			{"part_number": 1},
            {"part_number": 2},
            {"part_number": 3},
		},
	}

在返回响应时,会返回多个用于上传的链接(分为3片的文件会分别通过三个链接上传),官方的示例如下:  

2.4.3. 文件秒传

文件秒传,通过把用户的上传的进行HASH匹配,如果存在文件HASH值,就不需要上传而是直接从服务器复制过去。上文提到,如果需要使用秒传能力,在创建时带上秒传所需参数。

如果文件比较大,计算content_hash比较耗时。 可以使用只计算文件前1k的sha1,放入pre_hash字段。如果前1k没有匹配,说明文件无法做秒传。如果匹配到再计算完整的sha1,进行秒传。

(未实践...)

2.5. 上传完毕

这个步骤的目的是:通知阿里云盘服务器所有文件块已上传完成,服务器可以将这些块合并成一个完整的文件。

// completeUpload 完成上传
func completeUpload(accessToken string, driveId string, fileId, uploadId string) error {
	var newFile File

	reqBody := map[string]interface{}{
		"drive_id":  driveId,
		"file_id":   fileId,
		"upload_id": uploadId,
	}

	jsonData, _ := json.Marshal(reqBody)

	req, err := http.NewRequest("POST", apiBase+"/adrive/v1.0/openFile/complete", bytes.NewBuffer(jsonData))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	json.Unmarshal(body, &newFile)
	return nil
}

二、百度网盘

官方文档

获取用户信息icon-default.png?t=O83Ahttps://pan.baidu.com/union/doc/pksg0s9ns

1. 预准备

前置的创建应用、获取App Key、获取App Secret、以及交换获取Access Token不再赘述,大致步骤与阿里云盘相同,只是参数和用来请求的链接不同。下面主要关注不同之处:

1.1. 简化模式授权

此种模式与阿里云盘中无后端服务授权模式类似,不过不像阿里云盘需要通过个人应用运算生成code_verifier。

1.2. 设备码模式授权

流程图如下,通过向硬件设备请求设备码和用户码,作为参数来请求Access Token,使得一些不支持浏览器或输入受限的设备也可以接入。

2. 上传文件

百度网盘上传文件,同样也分为三个步骤:

  1. 预上传
  2. 文件上传(有不需要预上传的单步上传)
  3. 创建文件

与阿里云盘不同的是:百度网盘提供的Access Token已经可以唯一标识用户,所以也没有像阿里云盘那样去获取drive_id,上传文件的时候,常把Access Token用来作为参数。

此外:

2.1. 预上传

在预上传时,如果文件有进行分片,则需要把各个分片的MD5,填入一个字符串数组作为参数;与阿里云盘不同的是,第一个步骤必须填入size这个参数(阿里云盘只有在秒传的时候需要填写size)。

同样的,响应时会返回uploadid用来唯一标识本次传输任务。

2.2. 获取上传域名

在获取上传链接时,百度网盘会返回多个上传链接(为了并行执行多个分片的上传,也可能是负载均衡),使用其中任意一个作为上传链接即可。

// 获取上传 URL
func getUploadUrl(accessToken string, dstDir string, uploadId string) string {
	req, err := http.NewRequest("GET",
		"https://d.pcs.baidu.com/rest/2.0/pcs/file?"+
			"method=locateupload&appid=250528&access_token="+accessToken+
			"&path="+dstDir+"&uploadid="+uploadId+"&upload_version=2.0", nil)
	if err != nil {
		return ""
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "Exec req err"
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)

	var uploadDomainResponse UploadDomainResponse
	json.Unmarshal(body, &uploadDomainResponse)

	return uploadDomainResponse.Servers[0].Server
}

下面是官方文档中的响应示例:

2.3. 文件上传
2.3.1. 单步上传

针对小于2GB的文件,百度网盘提供了无需预上传和创建文件流程的单步上传,请求参数如下:

// 单步上传
func uploadFile(accessToken, localPath, remotePath string) error {
	url := "https://c.pcs.baidu.com/rest/2.0/pcs/file"
	params := map[string]string{
		"method":       "upload",
		"access_token": accessToken,
		"path":         remotePath,
		"ondup":        "newcopy",
	}
	file, err := os.Open(localPath)
	if err != nil {
		return err
	}
	defer file.Close()
	resp, err := grequests.Post(url, &grequests.RequestOptions{
		Params: params,
		Files: []grequests.FileUpload{
			{
				FileContents: file,
			},
		},
	})
	if err != nil {
		return err
	}
	if !resp.Ok {
		return fmt.Errorf("upload failed with status code: %d", resp.StatusCode)
	}
	return nil
}
2.3.2. 分片上传

在请求参数上,百度网盘把参数大多都放在URL的位置,与阿里云盘大多放在Body请求体中不同。此处的分片上传,通过partseq来区分、上传分好的多个分片(也是并行的)。

2.4. 创建文件

创建文件和阿里云盘中完成上传所实现的功能类似,把多个分片,组成一个完整的文件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/906336.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

医院信息化与智能化系统(15)

医院信息化与智能化系统(15) 这里只描述对应过程,和可能遇到的问题及解决办法以及对应的参考链接,并不会直接每一步详细配置 如果你想通过文字描述或代码画流程图,可以试试PlantUML,告诉GPT你的文件结构,让他给你对应…

从比亚迪超越特斯拉,看颠覆全球市场的中国力量

这是比亚迪CEO王传福早年在日本调研电池供应链时发出的感慨。 那时的人们谁也没有想到,比亚迪会从深圳的一家普通的电池供应商开始做起,拼出一条属于自己的“血路”,摇身一变成为名副其实的“电车之王”,并让全球车企仰望。 比亚…

3d 添加辅助坐标器和轨道控制器

1.添加辅助坐标器 使用AxesHelper类来添加坐标轴辅助器,辅助器简单模拟3个坐标轴的对象。红色代表X轴,绿色代表Y轴,蓝色代表Z轴。 // 创建坐标轴辅助器,5是坐标轴的长度 const axesHelper new THREE.AxesHelper(5); // 将坐标轴…

「C/C++」C++标准库之#include<fstream>文件流

✨博客主页何曾参静谧的博客📌文章专栏「C/C」C/C程序设计📚全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasoli…

计算机后台服务-更新下载,重启————未来之窗行业应用跨平台架构

一在教育中应用 1. 提高效率:能够快速收集大量学生的卷子,节省了传统人工收集和整理的时间。 2. 准确性:减少了人工收卷过程中可能出现的错漏和混乱,确保每份卷子都能准确无误地被收集和记录。 3. 即时性:可以实时接收…

奥数与C++小学四年级(第十八题 小球重量)

参考程序代码&#xff1a; #include <iostream> #include <vector>int main() {// 小球的重量std::vector<int> weights {1, 2, 3, 4, 5};// 用来存储可能的结果int a, b, c, d, e, x;// 穷举所有可能的 a, b, c, d, e 的组合for (int i 0; i < weight…

Android启动流程_Init阶段

前言 本文将会介绍 Android 启动流程&#xff0c;将基于 Android 10 代码逻辑介绍原生启动过程。 bootloader 上电 -> 加载 recovery 镜像或者 boot 镜像 -> linux kernel 启动 -> 加载 init 进程 -> 加载 zygote 进程 -> systemserver 进程 -> 系统启动 …

线上3D看车有何优势?

随着Web3D展示技术的不断革新&#xff0c;线上3D看车正逐步成为消费者购车的新选择。这种创新的展示方式不仅提供了更真实、更有趣的互动体验&#xff0c;还带来了诸多优势&#xff0c;让购车过程变得更加便捷、高效。 一、更真实的展示效果 相较于传统的图片和文字描述&…

OpenCv —— 为opencv支持中文,将freetype2库编译进opencv中(附详细编译流程、测试代码)

效果(下面摄像头视频过于老旧 视频效果不好;但文字可添加。) 背景 由于OpenCV本身不支持中文显示,开发者需要借助其他库来实现这一功能。 OpenCV的contrib版本中包含了freetype库,可以通过编译opencv-contrib模块来支持中文显示。这种方法需要在编译OpenCV时特别配置,确保…

打印直角三角形

今天给大家分享一个打印直角三角形的方法 其实直角三角形在终端中的显示是又空格和星号组成的 只要将其看成一个矩形&#xff0c;并找出两者规律便可实现&#xff08;与打印菱形有一定相似的理解&#xff09;&#xff0c;下面我们来分享一个更简便的方法 它的规律是行和列之和…

python-函数前一行加@xxxx的含义参数的约束条件检查装饰器

在sklearn中看到红框中的函数&#xff0c;于是好奇是什么东西&#xff0c;查到python-函数前一行加xxxx的含义 于是找到函数定义&#xff1a;def validate_params(parameter_constraints, *, prefer_skip_nested_validation): 但是&#xff0c;里面没有定义func参数 于是再看…

科技资讯|谷歌Play应用商店有望支持 XR 头显,AR / VR设备有望得到发展

据 Android Authority 报道&#xff0c;谷歌似乎正在为其 Play 商店增加对 XR 头显的支持。该媒体在 Play 商店的代码中发现了相关的线索&#xff0c;包括一个代表头显的小图标以及对“XR 头显”的提及。 谷歌也可能改变了此前拒绝将 Play 商店引入 Meta Quest 头显的决定。今…

百度SEO分析实用指南 提升网站搜索排名的有效策略

内容概要 在数字化时代&#xff0c;搜索引擎优化&#xff08;SEO&#xff09;已经成为提升网站曝光度的关键工具。本指南将带您了解SEO的基本知识&#xff0c;帮助您在复杂的网络环境中立足。我们将从关键词优化开始&#xff0c;重点讲解如何选择合适的关键词来提高搜索引擎排…

基于vue框架的的考研网上辅导系统ao9z7(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;学生,公告信息,课程分类,考研资料,考研视频,课程信息,院校建议,教师 开题报告内容 基于Vue框架的考研网上辅导系统开题报告 一、研究背景与意义 随着高等教育的普及和就业竞争的加剧&#xff0c;考研已成为众多大学生提升学历、增强就…

flutter 用PUT的方式传输文件不带分隔符

最近有个需求&#xff0c;需要在flutter中用put的方式传输固件到设备上&#xff0c;本来以为用dio或者http这两个框架就能轻松完成的&#xff0c;结果发现&#xff0c;这两个框架传输过去的文件都会修改了源文件&#xff0c;把请求头的分隔符也带进去了&#xff0c;设备无法识别…

Javaweb 实验4 xml

我发现了有些人喜欢静静看博客不聊天呐&#xff0c; 但是ta会点赞。 这样的人呢帅气低调有内涵&#xff0c; 美丽大方很优雅。 说的就是你&#xff0c; 不用再怀疑哦 实验四 XML 目的&#xff1a; 安装和使用XML的开发环境认识XML的不同类型掌握XML文档的基本语法了解D…

基于SSM+VUE历史车轮网站JAVA|VUE|Springboot计算机毕业设计源代码+数据库+LW文档+开题报告+答辩稿+部署教+代码讲解

源代码数据库LW文档&#xff08;1万字以上&#xff09;开题报告答辩稿 部署教程代码讲解代码时间修改教程 一、开发工具、运行环境、开发技术 开发工具 1、操作系统&#xff1a;Window操作系统 2、开发工具&#xff1a;IntelliJ IDEA或者Eclipse 3、数据库存储&#xff1a…

【果实种子识别】Python+深度学习+人工智能+CNN卷积神经网络算法+TensorFlow+算法模型训练

一、介绍 果实种子识别系统&#xff0c;使用Python语言进行开发&#xff0c;通过TensorFlow搭建卷积神经网络算法模型&#xff0c;对10种坚果果实&#xff08;‘杏仁’, ‘巴西坚果’, ‘腰果’, ‘椰子’, ‘榛子’, ‘夏威夷果’, ‘山核桃’, ‘松子’, ‘开心果’, ‘核桃…

云服务器哪家好?(跨境电商云服务器推荐)

跨境电商的发展势头愈发迅猛&#xff0c;对云服务器的需求也在与日俱增。挑选一个合适的云服务器&#xff0c;能够为跨境电商提供稳定的运行环境&#xff0c;确保网站具备快速响应能力&#xff0c;提升用户体验&#xff0c;进而增强商业竞争力。 以下是一些比较好的云服务器提供…

CentOS 文件系统扩容与缩容

一、 概述 理解Linux文件系统的管理&#xff0c;需要了解以下的一张图&#xff1a; 一般使用LVM (Logical Volume Manager) 管理磁盘存储&#xff0c;该工具允许用户更灵活地分配和管理存储空间。主要有以下几个概念&#xff1a; PV&#xff08;Physical Volume&#xff0c;物…