分块上传录制文件-上传分块

最近更新时间:2024-06-25 09:30:21

我的收藏

接口描述

描述:
企业 secret 鉴权用户可上传文件到录制文件列表,不支持 OAuth。
按照预上传返回的上传事务ID和策略上传对应分块,支持6小时内的断点续传,接口限频200次/分钟。
HTTP 的头部 Content-Type 必须传入 multipart/form-data。
该接口的输入参数是表单数据,不作为签名的部分。
请求方式:POST
鉴权方式:JWT
接口请求域名:
https://api.meeting.qq.com/v1/files/records/upload-part

输入参数

以下请求参数列表仅列出了接口请求参数,HTTP 请求头公共参数请参见签名验证章节的 公共参数说明
参数名称
是否必须
参数类型
参数描述
operator_id
String
操作者 ID。operator_id 必须与 operator_id_type 配合使用。根据 operator_id_type 的值,operator_id 代表不同类型。
operator_id_type
Integer
操作人 ID 类型:
1:userid
upload_id
String
上传事务 ID。
file_size
Integer
文件大小(以字节为单位),需按预上传返回的 block_size 填写(最后一个文件块按照实际大小填写)。
file_seq
Integer
文件块号,从1开始计数。最后一个文件块允许小于 block_size 的值。
file_checksum
String
文件校验和,文件内容 MD5 结果的十六进制表示。
file_content
[ ]Byte
文件二进制内容。

输出参数

成功返回空消息体。

示例

输入示例

Java
Python
Go
cURL
import com.alibaba.fastjson.JSONObject; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.*; public class UploadPartRecordFileExample { private static final String yourAppId = "yourAppId"; private static final String yourSdkId = "yourSdkId"; private static final String yourSecretId = "yourSecretId"; private static final String yourSecretKey = "yourSecretKey"; private static final String prodHost = "https://api.meeting.qq.com"; private static final String uploadRecordFilePreparePath = "/v1/files/records/upload-prepare"; // 分块上传录制文件-预上传路径 private static final String uploadRecordFilePartPath = "/v1/files/records/upload-part"; // 分块上传录制文件-上传分块路径 private static final String uploadRecordFileFinishPath = "/v1/files/records/upload-finish"; // 分块上传录制文件-完成上传路径 private static final String uploadRecordFilePartMethod = "POST"; public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeyException { byte[] fileContent = FileUtils.readFileToByteArray(new File("your_file_path")); String jobId = uploadPartRecordFile("operatorId", 1, "fileName", "video", fileContent, 1, true); if (!Objects.equals(jobId, "")) { System.out.println("Job ID: " + jobId); } } public static String uploadPartRecordFile(String operatorId, int operatorIdType, String fileName, String fileType, byte[] fileContent, int speakNumber, boolean aiRecord) throws IOException, NoSuchAlgorithmException, InvalidKeyException { CloseableHttpClient httpClient = HttpClients.createDefault(); // 1、预上传 HttpPost uploadRecordFilePrepareReq = createUploadRecordFilePrepareRequest(operatorId, operatorIdType, Base64.getEncoder().encodeToString(fileName.getBytes(StandardCharsets.UTF_8)), fileType, fileContent.length); CloseableHttpResponse uploadRecordFilePrepareResp = httpClient.execute(uploadRecordFilePrepareReq); String uploadRecordFilePrepareRespBody = ""; try { if (uploadRecordFilePrepareResp.getStatusLine().getStatusCode() != 200) { System.out.println("resp.status: " + uploadRecordFilePrepareResp.getStatusLine().getStatusCode()); System.out.println("resp.headers: " + Arrays.toString(uploadRecordFilePrepareResp.getAllHeaders())); System.out.println("resp.body: " + EntityUtils.toString(uploadRecordFilePrepareResp.getEntity())); return ""; } System.out.println("resp.headers: " + Arrays.toString(uploadRecordFilePrepareResp.getAllHeaders())); uploadRecordFilePrepareRespBody = EntityUtils.toString(uploadRecordFilePrepareResp.getEntity()); System.out.println("resp.body: " + uploadRecordFilePrepareRespBody); } finally { uploadRecordFilePrepareResp.close(); } JSONObject uploadRecordFilePrepareRespEntity = JSONObject.parseObject(uploadRecordFilePrepareRespBody); System.out.println("===================upload record file prepare success!\\n"); String uploadId = uploadRecordFilePrepareRespEntity.getString("upload_id"); int blockSize = uploadRecordFilePrepareRespEntity.getIntValue("block_size"); int blockNum = uploadRecordFilePrepareRespEntity.getIntValue("block_num"); // 2、分页上传 for (int fileSeq = 1; fileSeq <= blockNum; fileSeq++) { byte[] filePartContent; if (fileSeq != blockNum) { filePartContent = Arrays.copyOfRange(fileContent, (fileSeq - 1) * blockSize, fileSeq * blockSize); } else { filePartContent = Arrays.copyOfRange(fileContent, (fileSeq - 1) * blockSize, fileContent.length); } // 上传分片 HttpPost uploadPartRecordFileReq = createUploadPartRecordFileRequest(operatorId, operatorIdType, fileName, uploadId, fileSeq, filePartContent); CloseableHttpResponse uploadPartRecordFileResp = httpClient.execute(uploadPartRecordFileReq); try { if (uploadPartRecordFileResp.getStatusLine().getStatusCode() != 200) { System.out.println("resp.status: " + uploadPartRecordFileResp.getStatusLine().getStatusCode()); System.out.println("resp.headers: " + Arrays.toString(uploadPartRecordFileResp.getAllHeaders())); System.out.println("resp.body: " + EntityUtils.toString(uploadPartRecordFileResp.getEntity())); System.out.printf("===================uploadRecordFilePart_%d error\\n", fileSeq); return ""; } else { System.out.println("resp.headers: " + Arrays.toString(uploadPartRecordFileResp.getAllHeaders())); } } finally { uploadPartRecordFileResp.close(); } System.out.printf("===================uploadRecordFilePart_%d finish\\n", fileSeq); } System.out.println("===================uploadRecordFilePart_all finish"); // 完成上传 HttpPost uploadRecordFileFinishReq = createUploadRecordFileFinishRequest(operatorId, operatorIdType, uploadId, speakNumber, aiRecord); CloseableHttpResponse uploadRecordFileFinishResp = httpClient.execute(uploadRecordFileFinishReq); try { if (uploadRecordFileFinishResp.getStatusLine().getStatusCode() != 200) { System.out.println("resp.status: " + uploadRecordFileFinishResp.getStatusLine().getStatusCode()); System.out.println("resp.headers: " + Arrays.toString(uploadRecordFileFinishResp.getAllHeaders())); System.out.println("resp.body: " + EntityUtils.toString(uploadRecordFileFinishResp.getEntity())); System.out.println("===================uploadRecordFilePart finish error==================="); return ""; } System.out.println("resp.headers: " + Arrays.toString(uploadRecordFileFinishResp.getAllHeaders())); String uploadRecordFileFinishRespBody = EntityUtils.toString(uploadRecordFileFinishResp.getEntity()); System.out.println("resp.body: " + uploadRecordFileFinishRespBody); JSONObject uploadRecordFilePrepareRsp = JSONObject.parseObject(uploadRecordFileFinishRespBody); return uploadRecordFilePrepareRsp.getString("job_id"); } finally { uploadRecordFileFinishResp.close(); } } private static HttpPost createUploadRecordFilePrepareRequest(String operatorId, int operatorIdType, String fileName, String fileType, int fileSize) throws NoSuchAlgorithmException, InvalidKeyException { UploadRecordFilePrepareReq uploadRecordFilePrepareReq = new UploadRecordFilePrepareReq(operatorId, operatorIdType, fileName, fileType, fileSize); String requestBody = JSONObject.toJSONString(uploadRecordFilePrepareReq); HttpEntity entity = new StringEntity(requestBody, ContentType.APPLICATION_JSON); HttpPost req = new HttpPost(prodHost + uploadRecordFilePreparePath); req.setEntity(entity); String nonce = Integer.toString(new Random().nextInt(99999999) + 1); String timestamp = Long.toString(System.currentTimeMillis() / 1000); req.setHeader("Content-Type", ContentType.APPLICATION_JSON.toString()); req.setHeader("AppId", yourAppId); req.setHeader("SdkId", yourSdkId); req.setHeader("X-TC-Key", yourSecretId); req.setHeader("X-TC-Nonce", nonce); req.setHeader("X-TC-Timestamp", timestamp); req.setHeader("X-TC-Registered", "1"); req.setHeader("X-TC-Signature", generateSignature(uploadRecordFilePartMethod, nonce, timestamp, uploadRecordFilePreparePath, requestBody)); return req; } private static HttpPost createUploadPartRecordFileRequest(String operatorId, int operatorIdType, String fileName, String uploadId, int fileSeq, byte[] filePartContent) throws NoSuchAlgorithmException, InvalidKeyException { String fileChecksum = DigestUtils.md5Hex(filePartContent); MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.addTextBody("operator_id", operatorId); builder.addTextBody("operator_id_type", Integer.toString(operatorIdType)); builder.addTextBody("upload_id", uploadId); builder.addTextBody("file_seq", Integer.toString(fileSeq)); builder.addTextBody("file_size", Integer.toString(filePartContent.length)); builder.addTextBody("file_checksum", fileChecksum); builder.addBinaryBody("file_content", new ByteArrayInputStream(filePartContent), ContentType.DEFAULT_BINARY, fileName); HttpEntity httpEntity = builder.build(); HttpPost req = new HttpPost(prodHost + uploadRecordFilePartPath); req.setEntity(httpEntity); String nonce = Integer.toString(new Random().nextInt(99999999) + 1); String timestamp = Long.toString(System.currentTimeMillis() / 1000); req.setHeader("Content-Type", httpEntity.getContentType().getValue()); req.setHeader("AppId", yourAppId); req.setHeader("SdkId", yourSdkId); req.setHeader("X-TC-Key", yourSecretId); req.setHeader("X-TC-Nonce", nonce); req.setHeader("X-TC-Timestamp", timestamp); req.setHeader("X-TC-Registered", "1"); req.setHeader("X-TC-Signature", generateSignature(uploadRecordFilePartMethod, nonce, timestamp, uploadRecordFilePartPath, "")); return req; } private static HttpPost createUploadRecordFileFinishRequest(String operatorId, int operatorIdType, String uploadId, int speakNumber, boolean aiRecord) throws NoSuchAlgorithmException, InvalidKeyException { UploadRecordFileFinishReq uploadRecordFileFinishReq = new UploadRecordFileFinishReq(operatorId, operatorIdType, uploadId, speakNumber, aiRecord); String requestBody = JSONObject.toJSONString(uploadRecordFileFinishReq); HttpEntity entity = new StringEntity(requestBody, ContentType.APPLICATION_JSON); HttpPost req = new HttpPost(prodHost + uploadRecordFileFinishPath); req.setEntity(entity); String nonce = Integer.toString(new Random().nextInt(99999999) + 1); String timestamp = Long.toString(System.currentTimeMillis() / 1000); req.setHeader("Content-Type", ContentType.APPLICATION_JSON.toString()); req.setHeader("AppId", yourAppId); req.setHeader("SdkId", yourSdkId); req.setHeader("X-TC-Key", yourSecretId); req.setHeader("X-TC-Nonce", nonce); req.setHeader("X-TC-Timestamp", timestamp); req.setHeader("X-TC-Registered", "1"); req.setHeader("X-TC-Signature", generateSignature(uploadRecordFilePartMethod, nonce, timestamp, uploadRecordFileFinishPath, requestBody)); return req; } private static String generateSignature(String method, String nonce, String timestamp, String requestUri, String requestBody) throws NoSuchAlgorithmException, InvalidKeyException { // 实现签名逻辑 String headSignStr = String.format("X-TC-Key=%s&X-TC-Nonce=%s&X-TC-Timestamp=%s", yourSecretId, nonce, timestamp); String signStr = String.format("%s\\n%s\\n%s\\n%s", method, headSignStr, requestUri, requestBody); Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec(yourSecretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(secretKeySpec); byte[] hash = mac.doFinal(signStr.getBytes(StandardCharsets.UTF_8)); String sha = bytesToHex(hash); return Base64.getEncoder().encodeToString(sha.getBytes(StandardCharsets.UTF_8)); } private static String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } }

import base64 import hashlib import hmac import json import random import time import requests from requests_toolbelt import MultipartEncoder your_app_id = "yourAppId" # 企业ID your_sdk_id = "yourSdkId" # 应用ID your_secret_id = "yourSecretId" # 应用密钥ID your_secret_key = "yourSecretKey" # 应用密钥KEY prod_host = "https://api.meeting.qq.com" upload_record_file_prepare_path = "/v1/files/records/upload-prepare" # 分块上传录制文件-预上传路径 upload_record_file_part_path = "/v1/files/records/upload-part" # 分块上传录制文件-上传分块路径 upload_record_file_finish_path = "/v1/files/records/upload-finish" # 分块上传录制文件-完成上传路径 upload_record_file_part_method = "POST" # 分块上传录制文件-上传分块方法 # upload_part_record_file 分块上传文件到用户的录制文件列表 def upload_part_record_file(operator_id, operator_id_type, file_name, file_type, file_content, speak_number, ai_record): # 1、预上传 prepare_request = create_upload_record_file_prepare_request(operator_id, operator_id_type, file_name, file_type, len(file_content)) prepare_response = requests.request(prepare_request.method, prepare_request.url, data=prepare_request.data, headers=prepare_request.headers) try: file_prepare_response_entity = parse_upload_record_file_prepare_response(prepare_response) except Exception as e: print("预上传失败: ", e) return # 2、上传分块 upload_id = str(file_prepare_response_entity.get("upload_id")) block_size = int(file_prepare_response_entity.get("block_size")) block_num = int(file_prepare_response_entity.get("block_num")) file_seq = 1 while file_seq <= block_num: if file_seq != block_num: file_part_content = file_content[(file_seq - 1) * block_size: file_seq * block_size] else: file_part_content = file_content[(file_seq - 1) * block_size:] part_upload_request = create_upload_record_file_part_request(operator_id, operator_id_type, file_name, upload_id, file_seq, file_part_content) part_upload_response = requests.request(part_upload_request.method, part_upload_request.url, data=part_upload_request.data, headers=part_upload_request.headers) try: parse_upload_record_part_response(part_upload_response, file_seq) except Exception as e: print("上传分块失败: ", e) return file_seq = file_seq + 1 # 3、完成上传 finish_request = create_upload_record_file_finish_request(operator_id, operator_id_type, upload_id, speak_number, ai_record) finish_response = requests.request(finish_request.method, finish_request.url, data=finish_request.data, headers=finish_request.headers) try: finish_response_entity = parse_upload_record_file_finish_response(finish_response) except Exception as e: print("完成上传失败: ", e) return return finish_response_entity.get("job_id") def create_upload_record_file_prepare_request(operator_id, operator_id_type, file_name, file_type, file_size): bodyEntity = { "operator_id": operator_id, "operator_id_type": str(operator_id_type), "file_name": base64.b64encode(file_name.encode()).decode(), "file_type": file_type, "file_size": file_size, } bodyStr = json.dumps(bodyEntity) random.seed(time.time()) nonce = random.randint(1, 100000000) timestamp = int(time.time()) url = prod_host + upload_record_file_prepare_path headers = { "Content-Type": "application/json", "AppId": your_app_id, "SdkId": your_sdk_id, "X-TC-Key": your_secret_id, "X-TC-Nonce": str(nonce), "X-TC-Timestamp": str(timestamp), "X-TC-Registered": "1", } # gen the signature signature = gen_jwt_signature(upload_record_file_part_method, str(nonce), str(timestamp), upload_record_file_prepare_path, bodyStr) headers["X-TC-Signature"] = signature return requests.Request(upload_record_file_part_method, url, data=bodyStr, headers=headers) def parse_upload_record_file_prepare_response(prepare_response): if prepare_response.status_code != 200: print("预上传失败") print("prepare_response.status:{status_code}\\n".format(status_code=prepare_response.status_code)) print("prepare_response.headers:", prepare_response.headers, "\\n") print("prepare_response.body:", prepare_response.text, "\\n") raise Exception("预上传失败:", prepare_response.text, "\\n") else: print("预上传成功") print("prepare_response.headers:", prepare_response.headers, "\\n") print("prepare_response.body:", prepare_response.text, "\\n") return json.loads(prepare_response.text) def create_upload_record_file_part_request(operator_id, operator_id_type, file_name, upload_id, file_seq, file_part_content): multipart_data = MultipartEncoder( fields={ "operator_id": operator_id, "operator_id_type": str(operator_id_type), "file_size": str(len(file_part_content)), "file_checksum": hashlib.md5(file_part_content).hexdigest(), "upload_id": upload_id, "file_seq": str(file_seq), 'file_content': (file_name, file_part_content, "application/octet-stream") } ) random.seed(time.time()) nonce = random.randint(1, 100000000) timestamp = int(time.time()) url = prod_host + upload_record_file_part_path headers = { "Content-Type": multipart_data.content_type, "AppId": your_app_id, "SdkId": your_sdk_id, "X-TC-Key": your_secret_id, "X-TC-Nonce": str(nonce), "X-TC-Timestamp": str(timestamp), "X-TC-Registered": "1", } # gen the signature signature = gen_jwt_signature(upload_record_file_part_method, str(nonce), str(timestamp), upload_record_file_part_path, "") headers["X-TC-Signature"] = signature return requests.Request(upload_record_file_part_method, url, data=multipart_data, headers=headers) def parse_upload_record_part_response(part_upload_response, file_seq): if part_upload_response.status_code != 200: print("分块上传失败,part:{file_seq}==================\\n".format(file_seq=file_seq)) print("prepare_response.status:{status_code}\\n".format(status_code=part_upload_response.status_code)) print("prepare_response.headers:", part_upload_response.headers, "\\n") print("prepare_response.body:", part_upload_response.text, "\\n") raise Exception("分块上传失败:", part_upload_response.text) else: print("分块上传成功,part:{file_seq}==================\\n".format(file_seq=file_seq)) print("prepare_response.headers:", part_upload_response.headers, "\\n") def create_upload_record_file_finish_request(operator_id, operator_id_type, upload_id, speak_number, ai_record): bodyEntity = { "operator_id": operator_id, "operator_id_type": str(operator_id_type), "upload_id": upload_id, "speak_number": speak_number, "ai_record": ai_record, } bodyStr = json.dumps(bodyEntity) random.seed(time.time()) nonce = random.randint(1, 100000000) timestamp = int(time.time()) url = prod_host + upload_record_file_finish_path headers = { "Content-Type": "application/json", "AppId": your_app_id, "SdkId": your_sdk_id, "X-TC-Key": your_secret_id, "X-TC-Nonce": str(nonce), "X-TC-Timestamp": str(timestamp), "X-TC-Registered": "1", } # gen the signature signature = gen_jwt_signature(upload_record_file_part_method, str(nonce), str(timestamp), upload_record_file_finish_path, bodyStr) headers["X-TC-Signature"] = signature return requests.Request(upload_record_file_part_method, url, data=bodyStr, headers=headers) def parse_upload_record_file_finish_response(finish_response): if finish_response.status_code != 200: print("完成上传失败") print("prepare_response.status:{status_code}\\n".format(status_code=finish_response.status_code)) print("prepare_response.headers:", finish_response.headers, "\\n") print("prepare_response.body:", finish_response.text, "\\n") raise Exception("预上传失败:", finish_response.text, "\\n") else: print("完成上传成功") print("prepare_response.headers:", finish_response.headers, "\\n") print("prepare_response.body:", finish_response.text, "\\n") return json.loads(finish_response.text) def gen_jwt_signature(method: str, nonce: str, timestamp: str, request_uri: str, request_body: str): head_sign_str = "{0}\\nX-TC-Key={1}&X-TC-Nonce={2}&X-TC-Timestamp={3}\\n{4}\\n{5}".format( method, your_secret_id, nonce, timestamp, request_uri, request_body) signature = hmac.new(your_secret_key.encode(), head_sign_str.encode(), hashlib.sha256).hexdigest() return base64.b64encode(signature.encode()).decode()


import ( "bytes" "context" "crypto/hmac" "crypto/md5" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "math/rand" "mime/multipart" "net/http" "strconv" "strings" "time" ) const ( yourAppId = "yourAppId" // 企业ID yourSdkId = "yourSdkId" // 应用ID yourSecretId = "yourSecretId" // 应用密钥ID yourSecretKey = "yourSecretKey" // 应用密钥KEY prodHost = "https://api.meeting.qq.com" uploadRecordFilePreparePath = "/v1/files/records/upload-prepare" // 分块上传录制文件-预上传路径 uploadRecordFilePartPath = "/v1/files/records/upload-part" // 分块上传录制文件-上传分块路径 uploadRecordFileFinishPath = "/v1/files/records/upload-finish" // 分块上传录制文件-完成上传路径 uploadRecordFilePartMethod = "POST" ) // UploadRecordFilePrepareReq 分块上传录制文件-预上传请求结构体 type UploadRecordFilePrepareReq struct { OperatorId string `json:"operator_id"` // 操作人ID OperatorIdType int `json:"operator_id_type"` // 操作人ID类型 FileName string `json:"file_name"` // 文件名 FileType string `json:"file_type"` // 文件类型 FileSize int `json:"file_size"` // 文件大小 } // UploadRecordFilePrepareRsp 分块上传录制文件-预上传响应结构体 type UploadRecordFilePrepareRsp struct { UploadId string `json:"upload_id"` // 上传事务ID BlockSize int `json:"block_size"` // 分块大小策略(以字节为单位) BlockNum int `json:"block_num"` // 分块数量 } // UploadRecordFilePartRsp 分块上传录制文件-上传分块响应结构体 type UploadRecordFilePartRsp struct { } // UploadRecordFileFinishReq 分块上传录制文件-完成上传请求结构体 type UploadRecordFileFinishReq struct { OperatorId string `json:"operator_id"` // 操作人ID OperatorIdType int `json:"operator_id_type"` // 操作人ID类型 UploadId string `json:"upload_id"` // 上传事务ID SpeakNumber int `json:"speak_number"` // 上传文件中的发言人数:传具体数值代表几人发言,最多支持12人,其中0代表多人发言 AiRecord bool `json:"ai_record"` // 自动生成智能转写和智能纪要:true:自动生成(默认);false:不生成; } // UploadRecordFileFinishRsp 分块上传录制文件-完成上传响应结构体 type UploadRecordFileFinishRsp struct { JobId string `json:"job_id"` // 任务ID } // UploadPartRecordFile 分块上传文件到用户的录制文件列表 // @description 一次性上传不大于20MB的文件,接口限频200次/分钟。如果需要上传大于 20M 文件请使用分块上传。 /** * @param operatorId 操作者ID; * @param operatorIdType 操作人ID类型; * @param fileName 文件名+后缀,最多支持30个字符,base64编码; * @param fileType 文件类型,支持以下枚举值:1、voice:音频,支持上传m4a、aiff、wma、ogg、aac、amr、wav、mp3格式;2、video:视频,支持上传mp4、flv、mov、ogg、avi、wmv、m4v、3gp、mpeg格式; * @param fileContent 文件二进制内容; * @param speakNumber 上传文件中的发言人数:传具体数值代表几人发言,最多支持12人,其中0代表多人发言; * @param aiRecord 自动生成智能转写和智能纪要:true:自动生成(默认),false:不生成; * @return jobId 上传录制文件的任务ID; * @return err 错误原因; */ func UploadPartRecordFile(ctx context.Context, operatorId string, operatorIdType int, fileName, fileType string, fileContent []byte, speakNumber int, aiRecord bool) (jobId string, err error) { // 预上传 uploadRecordFilePrepareRespEntity, err := uploadRecordFilePrepare(ctx, operatorId, operatorIdType, fileName, fileType, fileContent) if err != nil { fmt.Println(err) return } fmt.Printf("uploadRecordFilePrepareRespEntity: %+v\\n", uploadRecordFilePrepareRespEntity) // 上传分块 blockNum := uploadRecordFilePrepareRespEntity.BlockNum blockSize := uploadRecordFilePrepareRespEntity.BlockSize uploadId := uploadRecordFilePrepareRespEntity.UploadId for fileSeq := 1; fileSeq <= blockNum; fileSeq++ { filePartContent := make([]byte, 0) if fileSeq != blockNum { filePartContent = fileContent[(fileSeq-1)*blockSize : fileSeq*blockSize] } else { filePartContent = fileContent[(fileSeq-1)*blockSize:] } err = uploadRecordFilePart(ctx, operatorId, operatorIdType, fileName, uploadId, fileSeq, filePartContent) if err != nil { fmt.Println(err) return } fmt.Printf("===================uploadRecordFilePart_%d finish===================\\n", fileSeq) } fmt.Printf("===================uploadRecordFilePart all finish===================\\n") // 完成上传 uploadRecordFileFinishRespEntity, err := uploadRecordFileFinish(ctx, operatorId, operatorIdType, uploadId, speakNumber, aiRecord) if err != nil { fmt.Println(err) return } fmt.Printf("uploadRecordFileFinisheRespEntity: %+v\\n", uploadRecordFileFinishRespEntity) return uploadRecordFileFinishRespEntity.JobId, err } // createUploadRecordFilePrepareRequest 创建分块上传录制文件-预上传请求 func createUploadRecordFilePrepareRequest(ctx context.Context, operatorId string, operatorIdType int, fileName, fileType string, fileSize int) *http.Request { reqBody := &UploadRecordFilePrepareReq{ OperatorId: operatorId, OperatorIdType: operatorIdType, FileName: base64.StdEncoding.EncodeToString([]byte(fileName)), FileType: fileType, FileSize: fileSize, } reqBodyStr, _ := json.Marshal(reqBody) bodyBuf := strings.NewReader(string(reqBodyStr)) req, _ := http.NewRequest(uploadRecordFilePartMethod, prodHost+uploadRecordFilePreparePath, bodyBuf) req.Header.Set("Content-Type", "application/json") req.Header.Set("AppId", yourAppId) req.Header.Set("SdkId", yourSdkId) req.Header.Set("X-TC-Key", yourSecretId) rand.Seed(time.Now().UnixNano()) nonce := rand.Intn(100000000) + 1 timestamp := time.Now().Unix() req.Header.Set("X-TC-Nonce", fmt.Sprintf("%d", nonce)) req.Header.Set("X-TC-Timestamp", fmt.Sprintf("%d", timestamp)) req.Header.Set("X-TC-Registered", "1") // gen the signature signature := GenJwtSignature(ctx, uploadRecordFilePartMethod, strconv.Itoa(nonce), strconv.Itoa(int(timestamp)), uploadRecordFilePreparePath, string(reqBodyStr)) req.Header.Set("X-TC-Signature", signature) return req } // uploadRecordFilePrepare 预上传录制文件 func uploadRecordFilePrepare(ctx context.Context, operatorId string, operatorIdType int, fileName, fileType string, fileContent []byte) (respEntity *UploadRecordFilePrepareRsp, err error) { // 1、Prepare the request req := createUploadRecordFilePrepareRequest(ctx, operatorId, operatorIdType, fileName, fileType, len(fileContent)) // 2、Send the request resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println(err) return } defer resp.Body.Close() // 3、Parse the response respEntity = &UploadRecordFilePrepareRsp{} err = parseResponse(resp, respEntity) if err != nil { fmt.Println(err) return } return respEntity, nil } // createUploadRecordFilePartRequest 创建分块上传录制文件-上传分块请求 func createUploadRecordFilePartRequest(ctx context.Context, operatorId string, operatorIdType int, fileName, uploadId string, fileSeq int, filePartContent []byte) *http.Request { fileSize := len(filePartContent) md5Content := md5.Sum(filePartContent) fileCheckSum := hex.EncodeToString(md5Content[:]) bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf) _ = bodyWriter.WriteField("operator_id", operatorId) _ = bodyWriter.WriteField("operator_id_type", strconv.Itoa(operatorIdType)) _ = bodyWriter.WriteField("upload_id", uploadId) _ = bodyWriter.WriteField("file_size", strconv.Itoa(fileSize)) _ = bodyWriter.WriteField("file_seq", strconv.Itoa(fileSeq)) _ = bodyWriter.WriteField("file_checksum", fileCheckSum) formFile, _ := bodyWriter.CreateFormFile("file_content", fileName) _, _ = io.Copy(formFile, bytes.NewReader(filePartContent)) _ = bodyWriter.Close() rand.Seed(time.Now().UnixNano()) nonce := rand.Intn(100000000) + 1 timestamp := time.Now().Unix() req, _ := http.NewRequest(uploadRecordFilePartMethod, prodHost+uploadRecordFilePartPath, bodyBuf) req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) req.Header.Set("AppId", yourAppId) req.Header.Set("SdkId", yourSdkId) req.Header.Set("X-TC-Key", yourSecretId) req.Header.Set("X-TC-Nonce", fmt.Sprintf("%d", nonce)) req.Header.Set("X-TC-Timestamp", fmt.Sprintf("%d", timestamp)) req.Header.Set("X-TC-Registered", "1") // gen the signature signature := GenJwtSignature(ctx, uploadRecordFilePartMethod, strconv.Itoa(nonce), strconv.Itoa(int(timestamp)), uploadRecordFilePartPath, "") req.Header.Set("X-TC-Signature", signature) return req } // uploadRecordFilePart 分块上传录制文件 func uploadRecordFilePart(ctx context.Context, operatorId string, operatorIdType int, fileName, uploadId string, fileSeq int, filePartContent []byte) (err error) { // 1、Prepare the request req := createUploadRecordFilePartRequest(ctx, operatorId, operatorIdType, fileName, uploadId, fileSeq, filePartContent) // 2、Send the request resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println(err) return } defer resp.Body.Close() // 3、Parse the response respEntity := &UploadRecordFilePartRsp{} err = parseResponse(resp, respEntity) if err != nil { fmt.Println(err) return } return nil } // createUploadRecordFileFinishRequest 创建分块上传录制文件-完成上传请求 func createUploadRecordFileFinishRequest(ctx context.Context, operatorId string, operatorIdType int, uploadId string, speakNumber int, aiRecord bool) *http.Request { reqBody := &UploadRecordFileFinishReq{ OperatorId: operatorId, OperatorIdType: operatorIdType, UploadId: uploadId, SpeakNumber: speakNumber, AiRecord: aiRecord, } reqBodyStr, _ := json.Marshal(reqBody) bodyBuf := strings.NewReader(string(reqBodyStr)) req, _ := http.NewRequest(uploadRecordFilePartMethod, prodHost+uploadRecordFileFinishPath, bodyBuf) req.Header.Set("Content-Type", "application/json") req.Header.Set("AppId", yourAppId) req.Header.Set("SdkId", yourSdkId) req.Header.Set("X-TC-Key", yourSecretId) rand.Seed(time.Now().UnixNano()) nonce := rand.Intn(100000000) + 1 timestamp := time.Now().Unix() req.Header.Set("X-TC-Nonce", fmt.Sprintf("%d", nonce)) req.Header.Set("X-TC-Timestamp", fmt.Sprintf("%d", timestamp)) req.Header.Set("X-TC-Registered", "1") // gen the signature signature := GenJwtSignature(ctx, uploadRecordFilePartMethod, strconv.Itoa(nonce), strconv.Itoa(int(timestamp)), uploadRecordFileFinishPath, string(reqBodyStr)) req.Header.Set("X-TC-Signature", signature) return req } // uploadRecordFileFinish 分块上传录制文件-完成上传 func uploadRecordFileFinish(ctx context.Context, operatorId string, operatorIdType int, uploadId string, speakNumber int, aiRecord bool) (respEntity *UploadRecordFileFinishRsp, err error) { // 1、Prepare the request req := createUploadRecordFileFinishRequest(ctx, operatorId, operatorIdType, uploadId, speakNumber, aiRecord) // 2、Send the request resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println(err) return } defer resp.Body.Close() // 3、Parse the response respEntity = &UploadRecordFileFinishRsp{} err = parseResponse(resp, respEntity) if err != nil { fmt.Println(err) return } return respEntity, nil } //GenJwtSignature 生成签名 /** * @param httpMethod http请求方法 GET/POST/PUT等 * @param headerNonce X-TC-Nonce请求头,随机数 * @param headerTimestamp X-TC-Timestamp请求头,当前时间的秒级时间戳 * @param requestUri 请求uri,eg:/v1/meetings * @param requestBody 请求体,没有的设为空串 * @return 签名,需要设置在请求头X-TC-Signature中 */ func GenJwtSignature(ctx context.Context, method string, nonce string, timestamp string, requestUri string, requestBody string) string { headSignStr := fmt.Sprintf("X-TC-Key=%s&X-TC-Nonce=%s&X-TC-Timestamp=%s", yourSecretId, nonce, timestamp) signStr := fmt.Sprintf("%s\\n%s\\n%s\\n%s", method, headSignStr, requestUri, requestBody) h := hmac.New(sha256.New, []byte(yourSecretKey)) h.Write([]byte(signStr)) sha := hex.EncodeToString(h.Sum([]byte{})) signBase64 := base64.StdEncoding.EncodeToString([]byte(sha)) return signBase64 } func parseResponse(resp *http.Response, respEntity interface{}) (err error) { respBody, err := io.ReadAll(resp.Body) if err != nil { return err } fmt.Printf("resp status: %s\\n", resp.Status) fmt.Println("resp headers:") for k, v := range resp.Header { fmt.Printf("%s: %s\\n", k, v) } fmt.Printf("respBody: %s\\n", string(respBody)) if err = json.Unmarshal(respBody, respEntity); err != nil { return err } fmt.Printf("resp entity: %+v\\n", respEntity) return nil }


curl --location 'https://api.meeting.qq.com/v1/files/records/upload-part' \\
--header 'Content-Type: multipart/form-data' \\
--header 'SdkId: xxxxxx' \\
--header 'AppId: xxxxxx' \\
--header 'X-TC-Key: hNYX1K5MxxxxxxxxxdrHh9aGkumq' \\
--header 'X-TC-Timestamp: 1712736948' \\
--header 'X-TC-Nonce: xxxxxx' \\
--header 'X-TC-Signature: xxxxxx' \\
--form 'operator_id_type="1"' \\
--form 'operator_id="xxxxxx"' \\
--form 'upload_id="K3JxxxxeK9Wd"' \\
--form 'file_seq="2"' \\
--form 'file_size="747216"' \\
--form 'file_checksum="bae13479e3fb65df1684db1c351749b6"' \\
--form 'file_content=@"path-to-file"'

输出示例

{}