diff --git a/broker/src/main/java/com/xiaoleilu/loServer/action/admin/GetUploadUrlAction.java b/broker/src/main/java/com/xiaoleilu/loServer/action/admin/GetUploadUrlAction.java
new file mode 100755
index 00000000..cacc4674
--- /dev/null
+++ b/broker/src/main/java/com/xiaoleilu/loServer/action/admin/GetUploadUrlAction.java
@@ -0,0 +1,126 @@
+/*
+ * This file is part of the Wildfire Chat package.
+ * (c) Heavyrain2012
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+package com.xiaoleilu.loServer.action.admin;
+
+import cn.wildfirechat.common.APIPath;
+import cn.wildfirechat.common.ErrorCode;
+import cn.wildfirechat.pojos.InputGetPresignedUploadUrl;
+import cn.wildfirechat.pojos.OutputPresignedUploadUrl;
+import com.qiniu.util.Auth;
+import com.xiaoleilu.loServer.RestResult;
+import com.xiaoleilu.loServer.action.robot.RobotAction;
+import com.xiaoleilu.loServer.annotation.HttpMethod;
+import com.xiaoleilu.loServer.annotation.Route;
+import com.xiaoleilu.loServer.handler.Request;
+import com.xiaoleilu.loServer.handler.Response;
+import io.moquette.server.config.MediaServerConfig;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.util.internal.StringUtil;
+
+import java.util.UUID;
+
+@Route(APIPath.Get_Presigned_Upload_Url)
+@HttpMethod("POST")
+public class GetUploadUrlAction extends AdminAction {
+
+ @Override
+ public boolean action(Request request, Response response) {
+ if (request.getNettyRequest() instanceof FullHttpRequest) {
+ if(!MediaServerConfig.USER_QINIU) {
+ setResponseContent(RestResult.resultOf(ErrorCode.INVALID_PARAMETER), response);
+ return true;
+ }
+
+ InputGetPresignedUploadUrl inputUserId = getRequestBody(request.getNettyRequest(), InputGetPresignedUploadUrl.class);
+
+ if (StringUtil.isNullOrEmpty(inputUserId.contentType)) {
+ inputUserId.contentType = "application/octet-stream";
+ }
+ String uuid = UUID.randomUUID().toString().replace("-", "");
+ inputUserId.fileName = uuid + inputUserId.fileName;
+
+ int type = inputUserId.mediaType;
+ String bucketName;
+ String bucketDomain;
+ switch (type) {
+ case 0:
+ default:
+ bucketName = MediaServerConfig.QINIU_BUCKET_GENERAL_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_GENERAL_DOMAIN;
+ break;
+ case 1:
+ bucketName = MediaServerConfig.QINIU_BUCKET_IMAGE_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_IMAGE_DOMAIN;
+ break;
+ case 2:
+ bucketName = MediaServerConfig.QINIU_BUCKET_VOICE_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_VOICE_DOMAIN;
+ break;
+ case 3:
+ bucketName = MediaServerConfig.QINIU_BUCKET_VIDEO_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_VIDEO_DOMAIN;
+ break;
+ case 4:
+ bucketName = MediaServerConfig.QINIU_BUCKET_FILE_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_FILE_DOMAIN;
+ break;
+ case 5:
+ bucketName = MediaServerConfig.QINIU_BUCKET_PORTRAIT_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_PORTRAIT_DOMAIN;
+ break;
+ case 6:
+ bucketName = MediaServerConfig.QINIU_BUCKET_FAVORITE_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_FAVORITE_DOMAIN;
+ break;
+ case 7:
+ bucketName = MediaServerConfig.QINIU_BUCKET_STICKER_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_STICKER_DOMAIN;
+ break;
+ case 8:
+ bucketName = MediaServerConfig.QINIU_BUCKET_MOMENTS_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_MOMENTS_DOMAIN;
+ break;
+ case 9:
+ bucketName = MediaServerConfig.QINIU_BUCKET_CUSTOM1_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_CUSTOM1_DOMAIN;
+ break;
+ case 10:
+ bucketName = MediaServerConfig.QINIU_BUCKET_CUSTOM2_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_CUSTOM2_DOMAIN;
+ break;
+ case 11:
+ bucketName = MediaServerConfig.QINIU_BUCKET_CUSTOM3_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_CUSTOM3_DOMAIN;
+ break;
+ case 12:
+ bucketName = MediaServerConfig.QINIU_BUCKET_PAN_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_PAN_DOMAIN;
+ break;
+ }
+
+ if(StringUtil.isNullOrEmpty(bucketName)) {
+ bucketName = MediaServerConfig.QINIU_BUCKET_GENERAL_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_GENERAL_DOMAIN;
+ }
+
+ String key = inputUserId.fileName;
+ Auth auth = Auth.create(MediaServerConfig.QINIU_ACCESS_KEY, MediaServerConfig.QINIU_SECRET_KEY);
+ String token = auth.uploadToken(bucketName, key);
+ String downloadUrl = bucketDomain + "/" + key;
+ String uploadUrl = "https://" + MediaServerConfig.QINIU_SERVER_URL + "?" + token + "?" + key;
+
+ OutputPresignedUploadUrl outputPresignedUploadUrl = new OutputPresignedUploadUrl();
+ outputPresignedUploadUrl.uploadUrl = uploadUrl;
+ outputPresignedUploadUrl.downloadUrl = downloadUrl;
+ outputPresignedUploadUrl.type = 1;
+ setResponseContent(RestResult.ok(outputPresignedUploadUrl), response);
+ }
+ return true;
+ }
+}
diff --git a/broker/src/main/java/com/xiaoleilu/loServer/action/robot/GetUploadUrlAction.java b/broker/src/main/java/com/xiaoleilu/loServer/action/robot/GetUploadUrlAction.java
new file mode 100755
index 00000000..843b0107
--- /dev/null
+++ b/broker/src/main/java/com/xiaoleilu/loServer/action/robot/GetUploadUrlAction.java
@@ -0,0 +1,139 @@
+/*
+ * This file is part of the Wildfire Chat package.
+ * (c) Heavyrain2012
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+package com.xiaoleilu.loServer.action.robot;
+
+import cn.wildfirechat.common.APIPath;
+import cn.wildfirechat.common.ErrorCode;
+import cn.wildfirechat.pojos.InputGetPresignedUploadUrl;
+import cn.wildfirechat.pojos.InputGetUserInfo;
+import cn.wildfirechat.pojos.InputOutputUserInfo;
+import cn.wildfirechat.pojos.OutputPresignedUploadUrl;
+import cn.wildfirechat.proto.ProtoConstants;
+import cn.wildfirechat.proto.WFCMessage;
+import com.qiniu.util.Auth;
+import com.xiaoleilu.loServer.RestResult;
+import com.xiaoleilu.loServer.annotation.HttpMethod;
+import com.xiaoleilu.loServer.annotation.Route;
+import com.xiaoleilu.loServer.handler.Request;
+import com.xiaoleilu.loServer.handler.Response;
+import io.moquette.persistence.MemorySessionStore;
+import io.moquette.server.config.MediaServerConfig;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.util.internal.StringUtil;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+@Route(APIPath.Robot_Get_Presigned_Upload_Url)
+@HttpMethod("POST")
+public class GetUploadUrlAction extends RobotAction {
+
+ @Override
+ public boolean action(Request request, Response response) {
+ if (request.getNettyRequest() instanceof FullHttpRequest) {
+ if(!MediaServerConfig.USER_QINIU) {
+ setResponseContent(RestResult.resultOf(ErrorCode.INVALID_PARAMETER), response);
+ return true;
+ }
+
+ InputGetPresignedUploadUrl inputUserId = getRequestBody(request.getNettyRequest(), InputGetPresignedUploadUrl.class);
+
+ if (StringUtil.isNullOrEmpty(inputUserId.contentType)) {
+ inputUserId.contentType = "application/octet-stream";
+ }
+ String uuid = UUID.randomUUID().toString().replace("-", "");
+ inputUserId.fileName = uuid + inputUserId.fileName;
+
+ int type = inputUserId.mediaType;
+ String bucketName;
+ String bucketDomain;
+ switch (type) {
+ case 0:
+ default:
+ bucketName = MediaServerConfig.QINIU_BUCKET_GENERAL_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_GENERAL_DOMAIN;
+ break;
+ case 1:
+ bucketName = MediaServerConfig.QINIU_BUCKET_IMAGE_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_IMAGE_DOMAIN;
+ break;
+ case 2:
+ bucketName = MediaServerConfig.QINIU_BUCKET_VOICE_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_VOICE_DOMAIN;
+ break;
+ case 3:
+ bucketName = MediaServerConfig.QINIU_BUCKET_VIDEO_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_VIDEO_DOMAIN;
+ break;
+ case 4:
+ bucketName = MediaServerConfig.QINIU_BUCKET_FILE_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_FILE_DOMAIN;
+ break;
+ case 5:
+ bucketName = MediaServerConfig.QINIU_BUCKET_PORTRAIT_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_PORTRAIT_DOMAIN;
+ break;
+ case 6:
+ bucketName = MediaServerConfig.QINIU_BUCKET_FAVORITE_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_FAVORITE_DOMAIN;
+ break;
+ case 7:
+ bucketName = MediaServerConfig.QINIU_BUCKET_STICKER_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_STICKER_DOMAIN;
+ break;
+ case 8:
+ bucketName = MediaServerConfig.QINIU_BUCKET_MOMENTS_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_MOMENTS_DOMAIN;
+ break;
+ case 9:
+ bucketName = MediaServerConfig.QINIU_BUCKET_CUSTOM1_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_CUSTOM1_DOMAIN;
+ break;
+ case 10:
+ bucketName = MediaServerConfig.QINIU_BUCKET_CUSTOM2_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_CUSTOM2_DOMAIN;
+ break;
+ case 11:
+ bucketName = MediaServerConfig.QINIU_BUCKET_CUSTOM3_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_CUSTOM3_DOMAIN;
+ break;
+ case 12:
+ bucketName = MediaServerConfig.QINIU_BUCKET_PAN_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_PAN_DOMAIN;
+ break;
+ }
+
+ if(StringUtil.isNullOrEmpty(bucketName)) {
+ bucketName = MediaServerConfig.QINIU_BUCKET_GENERAL_NAME;
+ bucketDomain = MediaServerConfig.QINIU_BUCKET_GENERAL_DOMAIN;
+ }
+
+ String key = inputUserId.fileName;
+ Auth auth = Auth.create(MediaServerConfig.QINIU_ACCESS_KEY, MediaServerConfig.QINIU_SECRET_KEY);
+ String token = auth.uploadToken(bucketName, key);
+ String downloadUrl = bucketDomain + "/" + key;
+ String uploadUrl = "https://" + MediaServerConfig.QINIU_SERVER_URL + "?" + token + "?" + key;
+
+ OutputPresignedUploadUrl outputPresignedUploadUrl = new OutputPresignedUploadUrl();
+ outputPresignedUploadUrl.uploadUrl = uploadUrl;
+ outputPresignedUploadUrl.downloadUrl = downloadUrl;
+ outputPresignedUploadUrl.type = 1;
+ setResponseContent(RestResult.ok(outputPresignedUploadUrl), response);
+ }
+ return true;
+ }
+}
diff --git a/common/src/main/java/cn/wildfirechat/common/APIPath.java b/common/src/main/java/cn/wildfirechat/common/APIPath.java
index 0c500544..bbb7d493 100644
--- a/common/src/main/java/cn/wildfirechat/common/APIPath.java
+++ b/common/src/main/java/cn/wildfirechat/common/APIPath.java
@@ -127,6 +127,8 @@ public interface APIPath {
String Get_User_Files = "/admin/file/user_files";
String Get_Message_File = "/admin/file/message_file";
+ String Get_Presigned_Upload_Url = "/admin/oss/get_upload_url";
+
String Conference_List = "/admin/conference/list";
String Conference_Exist = "/admin/conference/exist";
String Conference_List_Participant = "/admin/conference/list_participant";
@@ -188,4 +190,6 @@ public interface APIPath {
String Robot_Moments_Update_Profiles_Value = "/robot/moments/profiles/value/push";
String Robot_Conference_Request = "/robot/conference/request";
+
+ String Robot_Get_Presigned_Upload_Url = "/robot/oss/get_upload_url";
}
diff --git a/common/src/main/java/cn/wildfirechat/pojos/InputGetPresignedUploadUrl.java b/common/src/main/java/cn/wildfirechat/pojos/InputGetPresignedUploadUrl.java
new file mode 100755
index 00000000..6dd48012
--- /dev/null
+++ b/common/src/main/java/cn/wildfirechat/pojos/InputGetPresignedUploadUrl.java
@@ -0,0 +1,27 @@
+/*
+ * This file is part of the Wildfire Chat package.
+ * (c) Heavyrain2012
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+package cn.wildfirechat.pojos;
+
+
+/**
+ * 请求预签名上传地址
+ *
+ * 请求封装预签名上传地址,包括:
+ *
+ * - 文件名
+ * - 文件类型
+ * - 媒体类型
+ *
+ *
+ */
+public class InputGetPresignedUploadUrl {
+ public String fileName;
+ public String contentType;
+ public int mediaType;
+}
diff --git a/common/src/main/java/cn/wildfirechat/pojos/OutputPresignedUploadUrl.java b/common/src/main/java/cn/wildfirechat/pojos/OutputPresignedUploadUrl.java
new file mode 100755
index 00000000..2a8b09f1
--- /dev/null
+++ b/common/src/main/java/cn/wildfirechat/pojos/OutputPresignedUploadUrl.java
@@ -0,0 +1,29 @@
+/*
+ * This file is part of the Wildfire Chat package.
+ * (c) Heavyrain2012
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+package cn.wildfirechat.pojos;
+
+
+/**
+ * 预签名上传地址
+ *
+ * 封装预签名上传地址,包括:
+ *
+ * - 上传地址
+ * - 备选上传地址
+ * - 下载地址
+ * - 类型
+ *
+ *
+ */
+public class OutputPresignedUploadUrl {
+ public String uploadUrl;
+ public String backupUploadUrl;
+ public String downloadUrl;
+ public int type;
+}
diff --git a/sdk/src/main/java/cn/wildfirechat/sdk/GeneralAdmin.java b/sdk/src/main/java/cn/wildfirechat/sdk/GeneralAdmin.java
index b151f82f..594a9ad3 100644
--- a/sdk/src/main/java/cn/wildfirechat/sdk/GeneralAdmin.java
+++ b/sdk/src/main/java/cn/wildfirechat/sdk/GeneralAdmin.java
@@ -1,9 +1,26 @@
package cn.wildfirechat.sdk;
import cn.wildfirechat.common.APIPath;
+import cn.wildfirechat.common.ErrorCode;
import cn.wildfirechat.pojos.*;
+import cn.wildfirechat.proto.ProtoConstants;
import cn.wildfirechat.sdk.model.IMResult;
import cn.wildfirechat.sdk.utilities.AdminHttpUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.FileEntity;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.entity.mime.content.FileBody;
+import org.apache.http.entity.mime.content.InputStreamBody;
+import org.apache.http.util.EntityUtils;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URLConnection;
/**
* 通用管理类
@@ -256,4 +273,331 @@ public class GeneralAdmin {
public static IMResult getCustomer() throws Exception {
return AdminHttpUtils.httpJsonPost(APIPath.GET_CUSTOMER, null, String.class);
}
+
+ private static IMResult getPresignedUploadUrl(String fileName, int/*ProtoConstants.MessageMediaType*/ mediaType, String contentType) throws Exception {
+ String path = APIPath.Get_Presigned_Upload_Url;
+
+ InputGetPresignedUploadUrl requestPojo = new InputGetPresignedUploadUrl();
+ requestPojo.fileName = fileName;
+ requestPojo.mediaType = mediaType;
+ requestPojo.contentType = contentType;
+ return AdminHttpUtils.httpJsonPost(path, requestPojo, OutputPresignedUploadUrl.class);
+ }
+
+ /**
+ * 根据文件名获取Content-Type
+ *
+ * @param fileName 文件名
+ * @return Content-Type,如果无法识别则返回 "application/octet-stream"
+ */
+ private static String getContentTypeByFileName(String fileName) {
+ if (fileName == null || fileName.isEmpty()) {
+ return "application/octet-stream";
+ }
+
+ // 首先尝试使用 URLConnection 猜测
+ String contentType = URLConnection.guessContentTypeFromName(fileName);
+
+ // 如果无法识别,使用常见扩展名映射
+ if (contentType == null) {
+ String lowerName = fileName.toLowerCase();
+ if (lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg")) {
+ contentType = "image/jpeg";
+ } else if (lowerName.endsWith(".png")) {
+ contentType = "image/png";
+ } else if (lowerName.endsWith(".gif")) {
+ contentType = "image/gif";
+ } else if (lowerName.endsWith(".bmp")) {
+ contentType = "image/bmp";
+ } else if (lowerName.endsWith(".webp")) {
+ contentType = "image/webp";
+ } else if (lowerName.endsWith(".mp4")) {
+ contentType = "video/mp4";
+ } else if (lowerName.endsWith(".mov")) {
+ contentType = "video/quicktime";
+ } else if (lowerName.endsWith(".avi")) {
+ contentType = "video/x-msvideo";
+ } else if (lowerName.endsWith(".mp3")) {
+ contentType = "audio/mpeg";
+ } else if (lowerName.endsWith(".wav")) {
+ contentType = "audio/wav";
+ } else if (lowerName.endsWith(".pdf")) {
+ contentType = "application/pdf";
+ } else if (lowerName.endsWith(".doc")) {
+ contentType = "application/msword";
+ } else if (lowerName.endsWith(".docx")) {
+ contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+ } else if (lowerName.endsWith(".xls")) {
+ contentType = "application/vnd.ms-excel";
+ } else if (lowerName.endsWith(".xlsx")) {
+ contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+ } else if (lowerName.endsWith(".ppt")) {
+ contentType = "application/vnd.ms-powerpoint";
+ } else if (lowerName.endsWith(".pptx")) {
+ contentType = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
+ } else if (lowerName.endsWith(".txt")) {
+ contentType = "text/plain";
+ } else if (lowerName.endsWith(".html") || lowerName.endsWith(".htm")) {
+ contentType = "text/html";
+ } else if (lowerName.endsWith(".json")) {
+ contentType = "application/json";
+ } else if (lowerName.endsWith(".xml")) {
+ contentType = "application/xml";
+ } else if (lowerName.endsWith(".zip")) {
+ contentType = "application/zip";
+ } else if (lowerName.endsWith(".rar")) {
+ contentType = "application/x-rar-compressed";
+ } else if (lowerName.endsWith(".7z")) {
+ contentType = "application/x-7z-compressed";
+ } else if (lowerName.endsWith(".tar")) {
+ contentType = "application/x-tar";
+ } else if (lowerName.endsWith(".gz")) {
+ contentType = "application/gzip";
+ } else {
+ contentType = "application/octet-stream";
+ }
+ }
+
+ return contentType;
+ }
+
+
+ /**
+ * 上传文件
+ *
+ * 流程:先调用getPresignedUploadUrl获取预签名上传地址,然后直接上传文件。
+ * 上传成功后返回文件的下载地址等信息。
+ *
+ *
+ * @param file 要上传的文件
+ * @return 上传结果,包含下载地址
+ * @throws Exception 上传失败时抛出异常
+ */
+ public static IMResult uploadFile(File file) throws Exception {
+ return uploadFile(file, ProtoConstants.MessageMediaType.FILE, null);
+ }
+ /**
+ * 上传文件
+ *
+ * 流程:先调用getPresignedUploadUrl获取预签名上传地址,然后直接上传文件。
+ * 上传成功后返回文件的下载地址等信息。
+ *
+ *
+ * @param file 要上传的文件
+ * @param mediaType 媒体类型,参考{@link cn.wildfirechat.proto.ProtoConstants.MessageMediaType}
+ * @param contentType 文件Content-Type,例如 "image/jpeg", "application/octet-stream" 等;
+ * 如果为null或空,则根据文件名自动识别
+ * @return 上传结果,包含下载地址
+ * @throws Exception 上传失败时抛出异常
+ */
+ public static IMResult uploadFile(File file, int mediaType, String contentType) throws Exception {
+ if (file == null || !file.exists()) {
+ throw new IllegalArgumentException("文件不能为空或不存在");
+ }
+
+ // 如果未指定Content-Type,根据文件名自动获取
+ if (contentType == null || contentType.isEmpty()) {
+ contentType = getContentTypeByFileName(file.getName());
+ }
+
+ return doUploadFile(file.getName(), mediaType, contentType, file, null);
+ }
+
+ /**
+ * 执行文件上传的公共方法
+ *
+ * 根据服务器类型选择不同的上传方式:
+ *
+ * - type = 1(七牛云):使用 POST + multipart/form-data 表单上传
+ * - type = 其他(阿里云、Minio等):使用 PUT + 二进制流上传
+ *
+ *
+ *
+ * @param fileName 文件名
+ * @param mediaType 媒体类型
+ * @param contentType Content-Type
+ * @param file 文件对象(用于七牛云上传)
+ * @param inputStream 输入流(用于其他类型上传,可为null)
+ * @return 上传结果
+ * @throws Exception 上传失败时抛出异常
+ */
+ private static IMResult doUploadFile(String fileName, int mediaType,
+ String contentType,
+ File file,
+ InputStream inputStream) throws Exception {
+ // 1. 获取预签名上传地址
+ IMResult presignedResult = getPresignedUploadUrl(
+ fileName, mediaType, contentType);
+
+ if (presignedResult.getErrorCode() != ErrorCode.ERROR_CODE_SUCCESS) {
+ IMResult imResult = new IMResult<>();
+ imResult.setCode(presignedResult.code);
+ imResult.setMsg(presignedResult.msg);
+ return imResult;
+ }
+
+ OutputPresignedUploadUrl presignedUrl = presignedResult.getResult();
+ if (presignedUrl == null || presignedUrl.uploadUrl == null) {
+ throw new Exception("预签名上传地址为空");
+ }
+
+ // 2. 根据服务器类型选择上传方式
+ if (presignedUrl.type == 1) {
+ // 七牛云:使用 POST + multipart/form-data 表单上传
+ return uploadToQiniu(presignedUrl, file, inputStream, fileName, contentType);
+ } else {
+ // 其他(阿里云、Minio、腾讯云、华为云、AWS S3等):使用 PUT 上传
+ return uploadToOther(presignedUrl, file, inputStream, contentType);
+ }
+ }
+
+ /**
+ * 上传到七牛云(type = 1)
+ * 使用 POST + multipart/form-data 表单格式
+ */
+ private static IMResult uploadToQiniu(OutputPresignedUploadUrl presignedUrl, File file,
+ InputStream inputStream, String fileName,
+ String contentType) throws Exception {
+ // 解析七牛云上传地址:格式为 "https://host?token?key"
+ String uploadUrl = presignedUrl.uploadUrl;
+ String token;
+ String key;
+
+ int tokenStart = uploadUrl.indexOf('?');
+ int keyStart = uploadUrl.indexOf('?', tokenStart + 1);
+
+ if (tokenStart == -1 || keyStart == -1) {
+ throw new Exception("七牛云上传地址格式错误");
+ }
+
+ String baseUrl = uploadUrl.substring(0, tokenStart);
+ token = uploadUrl.substring(tokenStart + 1, keyStart);
+ key = uploadUrl.substring(keyStart + 1);
+
+ HttpPost httpPost = new HttpPost(baseUrl);
+ try {
+ MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+
+ // 添加 token 和 key 字段
+ builder.addTextBody("token", token);
+ builder.addTextBody("key", key);
+
+ // 添加文件字段
+ if (file != null) {
+ builder.addPart("file", new FileBody(file, ContentType.create(contentType), fileName));
+ } else if (inputStream != null) {
+ builder.addPart("file", new InputStreamBody(inputStream, ContentType.create(contentType), fileName));
+ }
+
+ httpPost.setEntity(builder.build());
+
+ HttpResponse response = AdminHttpUtils.getHttpClient().execute(httpPost);
+ int statusCode = response.getStatusLine().getStatusCode();
+
+ // 消耗响应体
+ if (response.getEntity() != null) {
+ EntityUtils.consumeQuietly(response.getEntity());
+ }
+
+ // 七牛云返回 200 表示成功
+ if (statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_CREATED) {
+ throw new Exception("文件上传到七牛云失败,HTTP状态码:" + statusCode);
+ }
+
+ IMResult imResult = new IMResult<>();
+ imResult.setCode(0);
+ imResult.setResult(presignedUrl.downloadUrl);
+ return imResult;
+ } finally {
+ httpPost.releaseConnection();
+ }
+ }
+
+ /**
+ * 上传到其他对象存储(type != 1)
+ * 使用 PUT + 二进制流
+ */
+ private static IMResult uploadToOther(OutputPresignedUploadUrl presignedUrl, File file,
+ InputStream inputStream, String contentType) throws Exception {
+ HttpPut httpPut = new HttpPut(presignedUrl.uploadUrl);
+ try {
+ // 设置请求实体
+ if (file != null) {
+ FileEntity fileEntity = new FileEntity(file);
+ fileEntity.setContentType(contentType);
+ httpPut.setEntity(fileEntity);
+ } else if (inputStream != null) {
+ InputStreamEntity streamEntity = new InputStreamEntity(inputStream);
+ streamEntity.setContentType(contentType);
+ httpPut.setEntity(streamEntity);
+ }
+
+ HttpResponse response = AdminHttpUtils.getHttpClient().execute(httpPut);
+ int statusCode = response.getStatusLine().getStatusCode();
+
+ // 消耗响应体,确保连接可以被复用
+ if (response.getEntity() != null) {
+ EntityUtils.consumeQuietly(response.getEntity());
+ }
+
+ if (statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_CREATED) {
+ throw new Exception("文件上传失败,HTTP状态码:" + statusCode);
+ }
+
+ IMResult imResult = new IMResult<>();
+ imResult.setCode(0);
+ imResult.setResult(presignedUrl.downloadUrl);
+ return imResult;
+ } finally {
+ httpPut.releaseConnection();
+ }
+ }
+
+ /**
+ * 上传文件(通过输入流)
+ *
+ * 流程:先调用getPresignedUploadUrl获取预签名上传地址,然后直接上传文件。
+ * 上传成功后返回文件的下载地址等信息。
+ *
+ *
+ * @param inputStream 文件输入流
+ * @param fileName 文件名
+ * @return 上传结果,包含下载地址
+ * @throws Exception 上传失败时抛出异常
+ */
+ public static IMResult uploadFile(InputStream inputStream, String fileName) throws Exception {
+ return uploadFile(inputStream, fileName, ProtoConstants.MessageMediaType.FILE, null);
+ }
+ /**
+ * 上传文件(通过输入流)
+ *
+ * 流程:先调用getPresignedUploadUrl获取预签名上传地址,然后直接上传文件。
+ * 上传成功后返回文件的下载地址等信息。
+ *
+ *
+ * 注意:无论上传成功还是失败,此方法都会关闭输入流。
+ *
+ *
+ * @param inputStream 文件输入流(此方法会关闭该流)
+ * @param fileName 文件名
+ * @param mediaType 媒体类型,参考{@link cn.wildfirechat.proto.ProtoConstants.MessageMediaType}
+ * @param contentType 文件Content-Type,例如 "image/jpeg", "application/octet-stream" 等;
+ * 如果为null或空,则根据文件名自动识别
+ * @return 上传结果,包含下载地址
+ * @throws Exception 上传失败时抛出异常
+ */
+ public static IMResult uploadFile(InputStream inputStream, String fileName,
+ int mediaType, String contentType) throws Exception {
+ if (inputStream == null) {
+ throw new IllegalArgumentException("输入流不能为空");
+ }
+
+ // 如果未指定Content-Type,根据文件名自动获取
+ if (contentType == null || contentType.isEmpty()) {
+ contentType = getContentTypeByFileName(fileName);
+ }
+
+ return doUploadFile(fileName, mediaType, contentType, null, inputStream);
+ // 注意:正常流程下的流关闭在 uploadToQiniu 或 uploadToOther 的 finally 块中处理
+ }
}
diff --git a/sdk/src/main/java/cn/wildfirechat/sdk/RobotService.java b/sdk/src/main/java/cn/wildfirechat/sdk/RobotService.java
index 1430245d..d12401bd 100644
--- a/sdk/src/main/java/cn/wildfirechat/sdk/RobotService.java
+++ b/sdk/src/main/java/cn/wildfirechat/sdk/RobotService.java
@@ -4,12 +4,29 @@ import cn.wildfirechat.common.APIPath;
import cn.wildfirechat.common.ErrorCode;
import cn.wildfirechat.pojos.*;
import cn.wildfirechat.pojos.moments.*;
+import cn.wildfirechat.proto.ProtoConstants;
import cn.wildfirechat.sdk.model.IMResult;
import cn.wildfirechat.sdk.utilities.RobotHttpUtils;
import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.FileEntity;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.entity.mime.content.FileBody;
+import org.apache.http.entity.mime.content.InputStreamBody;
+import org.apache.http.util.EntityUtils;
import java.io.Closeable;
+import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.List;
import static cn.wildfirechat.proto.ProtoConstants.ApplicationType.ApplicationType_Robot;
@@ -476,6 +493,333 @@ public class RobotService implements Closeable {
return robotHttpUtils.httpJsonPost(path, requestPojo, Void.class);
}
+ private IMResult getPresignedUploadUrl(String fileName, int/*ProtoConstants.MessageMediaType*/ mediaType, String contentType) throws Exception {
+ String path = APIPath.Robot_Get_Presigned_Upload_Url;
+
+ InputGetPresignedUploadUrl requestPojo = new InputGetPresignedUploadUrl();
+ requestPojo.fileName = fileName;
+ requestPojo.mediaType = mediaType;
+ requestPojo.contentType = contentType;
+ return robotHttpUtils.httpJsonPost(path, requestPojo, OutputPresignedUploadUrl.class);
+ }
+
+ /**
+ * 根据文件名获取Content-Type
+ *
+ * @param fileName 文件名
+ * @return Content-Type,如果无法识别则返回 "application/octet-stream"
+ */
+ private String getContentTypeByFileName(String fileName) {
+ if (fileName == null || fileName.isEmpty()) {
+ return "application/octet-stream";
+ }
+
+ // 首先尝试使用 URLConnection 猜测
+ String contentType = URLConnection.guessContentTypeFromName(fileName);
+
+ // 如果无法识别,使用常见扩展名映射
+ if (contentType == null) {
+ String lowerName = fileName.toLowerCase();
+ if (lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg")) {
+ contentType = "image/jpeg";
+ } else if (lowerName.endsWith(".png")) {
+ contentType = "image/png";
+ } else if (lowerName.endsWith(".gif")) {
+ contentType = "image/gif";
+ } else if (lowerName.endsWith(".bmp")) {
+ contentType = "image/bmp";
+ } else if (lowerName.endsWith(".webp")) {
+ contentType = "image/webp";
+ } else if (lowerName.endsWith(".mp4")) {
+ contentType = "video/mp4";
+ } else if (lowerName.endsWith(".mov")) {
+ contentType = "video/quicktime";
+ } else if (lowerName.endsWith(".avi")) {
+ contentType = "video/x-msvideo";
+ } else if (lowerName.endsWith(".mp3")) {
+ contentType = "audio/mpeg";
+ } else if (lowerName.endsWith(".wav")) {
+ contentType = "audio/wav";
+ } else if (lowerName.endsWith(".pdf")) {
+ contentType = "application/pdf";
+ } else if (lowerName.endsWith(".doc")) {
+ contentType = "application/msword";
+ } else if (lowerName.endsWith(".docx")) {
+ contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+ } else if (lowerName.endsWith(".xls")) {
+ contentType = "application/vnd.ms-excel";
+ } else if (lowerName.endsWith(".xlsx")) {
+ contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+ } else if (lowerName.endsWith(".ppt")) {
+ contentType = "application/vnd.ms-powerpoint";
+ } else if (lowerName.endsWith(".pptx")) {
+ contentType = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
+ } else if (lowerName.endsWith(".txt")) {
+ contentType = "text/plain";
+ } else if (lowerName.endsWith(".html") || lowerName.endsWith(".htm")) {
+ contentType = "text/html";
+ } else if (lowerName.endsWith(".json")) {
+ contentType = "application/json";
+ } else if (lowerName.endsWith(".xml")) {
+ contentType = "application/xml";
+ } else if (lowerName.endsWith(".zip")) {
+ contentType = "application/zip";
+ } else if (lowerName.endsWith(".rar")) {
+ contentType = "application/x-rar-compressed";
+ } else if (lowerName.endsWith(".7z")) {
+ contentType = "application/x-7z-compressed";
+ } else if (lowerName.endsWith(".tar")) {
+ contentType = "application/x-tar";
+ } else if (lowerName.endsWith(".gz")) {
+ contentType = "application/gzip";
+ } else {
+ contentType = "application/octet-stream";
+ }
+ }
+
+ return contentType;
+ }
+
+
+ /**
+ * 上传文件
+ *
+ * 流程:先调用getPresignedUploadUrl获取预签名上传地址,然后直接上传文件。
+ * 上传成功后返回文件的下载地址等信息。
+ *
+ *
+ * @param file 要上传的文件
+ * @return 上传结果,包含下载地址
+ * @throws Exception 上传失败时抛出异常
+ */
+ public IMResult uploadFile(File file) throws Exception {
+ return uploadFile(file, ProtoConstants.MessageMediaType.FILE, null);
+ }
+ /**
+ * 上传文件
+ *
+ * 流程:先调用getPresignedUploadUrl获取预签名上传地址,然后直接上传文件。
+ * 上传成功后返回文件的下载地址等信息。
+ *
+ *
+ * @param file 要上传的文件
+ * @param mediaType 媒体类型,参考{@link cn.wildfirechat.proto.ProtoConstants.MessageMediaType}
+ * @param contentType 文件Content-Type,例如 "image/jpeg", "application/octet-stream" 等;
+ * 如果为null或空,则根据文件名自动识别
+ * @return 上传结果,包含下载地址
+ * @throws Exception 上传失败时抛出异常
+ */
+ public IMResult uploadFile(File file, int mediaType, String contentType) throws Exception {
+ if (file == null || !file.exists()) {
+ throw new IllegalArgumentException("文件不能为空或不存在");
+ }
+
+ // 如果未指定Content-Type,根据文件名自动获取
+ if (contentType == null || contentType.isEmpty()) {
+ contentType = getContentTypeByFileName(file.getName());
+ }
+
+ return doUploadFile(file.getName(), mediaType, contentType, file, null);
+ }
+
+ /**
+ * 执行文件上传的公共方法
+ *
+ * 根据服务器类型选择不同的上传方式:
+ *
+ * - type = 1(七牛云):使用 POST + multipart/form-data 表单上传
+ * - type = 其他(阿里云、Minio等):使用 PUT + 二进制流上传
+ *
+ *
+ *
+ * @param fileName 文件名
+ * @param mediaType 媒体类型
+ * @param contentType Content-Type
+ * @param file 文件对象(用于七牛云上传)
+ * @param inputStream 输入流(用于其他类型上传,可为null)
+ * @return 上传结果
+ * @throws Exception 上传失败时抛出异常
+ */
+ private IMResult doUploadFile(String fileName, int mediaType,
+ String contentType,
+ File file,
+ InputStream inputStream) throws Exception {
+ // 1. 获取预签名上传地址
+ IMResult presignedResult = getPresignedUploadUrl(
+ fileName, mediaType, contentType);
+
+ if (presignedResult.getErrorCode() != ErrorCode.ERROR_CODE_SUCCESS) {
+ IMResult imResult = new IMResult<>();
+ imResult.setCode(presignedResult.code);
+ imResult.setMsg(presignedResult.msg);
+ return imResult;
+ }
+
+ OutputPresignedUploadUrl presignedUrl = presignedResult.getResult();
+ if (presignedUrl == null || presignedUrl.uploadUrl == null) {
+ throw new Exception("预签名上传地址为空");
+ }
+
+ // 2. 根据服务器类型选择上传方式
+ if (presignedUrl.type == 1) {
+ // 七牛云:使用 POST + multipart/form-data 表单上传
+ return uploadToQiniu(presignedUrl, file, inputStream, fileName, contentType);
+ } else {
+ // 其他(阿里云、Minio、腾讯云、华为云、AWS S3等):使用 PUT 上传
+ return uploadToOther(presignedUrl, file, inputStream, contentType);
+ }
+ }
+
+ /**
+ * 上传到七牛云(type = 1)
+ * 使用 POST + multipart/form-data 表单格式
+ */
+ private IMResult uploadToQiniu(OutputPresignedUploadUrl presignedUrl, File file,
+ InputStream inputStream, String fileName,
+ String contentType) throws Exception {
+ // 解析七牛云上传地址:格式为 "https://host?token?key"
+ String uploadUrl = presignedUrl.uploadUrl;
+ String token;
+ String key;
+
+ int tokenStart = uploadUrl.indexOf('?');
+ int keyStart = uploadUrl.indexOf('?', tokenStart + 1);
+
+ if (tokenStart == -1 || keyStart == -1) {
+ throw new Exception("七牛云上传地址格式错误");
+ }
+
+ String baseUrl = uploadUrl.substring(0, tokenStart);
+ token = uploadUrl.substring(tokenStart + 1, keyStart);
+ key = uploadUrl.substring(keyStart + 1);
+
+ HttpPost httpPost = new HttpPost(baseUrl);
+ try {
+ MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+
+ // 添加 token 和 key 字段
+ builder.addTextBody("token", token);
+ builder.addTextBody("key", key);
+
+ // 添加文件字段
+ if (file != null) {
+ builder.addPart("file", new FileBody(file, ContentType.create(contentType), fileName));
+ } else if (inputStream != null) {
+ builder.addPart("file", new InputStreamBody(inputStream, ContentType.create(contentType), fileName));
+ }
+
+ httpPost.setEntity(builder.build());
+
+ HttpResponse response = robotHttpUtils.getHttpClient().execute(httpPost);
+ int statusCode = response.getStatusLine().getStatusCode();
+
+ // 消耗响应体
+ if (response.getEntity() != null) {
+ EntityUtils.consumeQuietly(response.getEntity());
+ }
+
+ // 七牛云返回 200 表示成功
+ if (statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_CREATED) {
+ throw new Exception("文件上传到七牛云失败,HTTP状态码:" + statusCode);
+ }
+
+ IMResult imResult = new IMResult<>();
+ imResult.setCode(0);
+ imResult.setResult(presignedUrl.downloadUrl);
+ return imResult;
+ } finally {
+ httpPost.releaseConnection();
+ }
+ }
+
+ /**
+ * 上传到其他对象存储(type != 1)
+ * 使用 PUT + 二进制流
+ */
+ private IMResult uploadToOther(OutputPresignedUploadUrl presignedUrl, File file,
+ InputStream inputStream, String contentType) throws Exception {
+ HttpPut httpPut = new HttpPut(presignedUrl.uploadUrl);
+ try {
+ // 设置请求实体
+ if (file != null) {
+ FileEntity fileEntity = new FileEntity(file);
+ fileEntity.setContentType(contentType);
+ httpPut.setEntity(fileEntity);
+ } else if (inputStream != null) {
+ InputStreamEntity streamEntity = new InputStreamEntity(inputStream);
+ streamEntity.setContentType(contentType);
+ httpPut.setEntity(streamEntity);
+ }
+
+ HttpResponse response = robotHttpUtils.getHttpClient().execute(httpPut);
+ int statusCode = response.getStatusLine().getStatusCode();
+
+ // 消耗响应体,确保连接可以被复用
+ if (response.getEntity() != null) {
+ EntityUtils.consumeQuietly(response.getEntity());
+ }
+
+ if (statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_CREATED) {
+ throw new Exception("文件上传失败,HTTP状态码:" + statusCode);
+ }
+
+ IMResult imResult = new IMResult<>();
+ imResult.setCode(0);
+ imResult.setResult(presignedUrl.downloadUrl);
+ return imResult;
+ } finally {
+ httpPut.releaseConnection();
+ }
+ }
+
+ /**
+ * 上传文件(通过输入流)
+ *
+ * 流程:先调用getPresignedUploadUrl获取预签名上传地址,然后直接上传文件。
+ * 上传成功后返回文件的下载地址等信息。
+ *
+ *
+ * @param inputStream 文件输入流
+ * @param fileName 文件名
+ * @return 上传结果,包含下载地址
+ * @throws Exception 上传失败时抛出异常
+ */
+ public IMResult uploadFile(InputStream inputStream, String fileName) throws Exception {
+ return uploadFile(inputStream, fileName, ProtoConstants.MessageMediaType.FILE, null);
+ }
+ /**
+ * 上传文件(通过输入流)
+ *
+ * 流程:先调用getPresignedUploadUrl获取预签名上传地址,然后直接上传文件。
+ * 上传成功后返回文件的下载地址等信息。
+ *
+ *
+ * 注意:无论上传成功还是失败,此方法都会关闭输入流。
+ *
+ *
+ * @param inputStream 文件输入流(此方法会关闭该流)
+ * @param fileName 文件名
+ * @param mediaType 媒体类型,参考{@link cn.wildfirechat.proto.ProtoConstants.MessageMediaType}
+ * @param contentType 文件Content-Type,例如 "image/jpeg", "application/octet-stream" 等;
+ * 如果为null或空,则根据文件名自动识别
+ * @return 上传结果,包含下载地址
+ * @throws Exception 上传失败时抛出异常
+ */
+ public IMResult uploadFile(InputStream inputStream, String fileName,
+ int mediaType, String contentType) throws Exception {
+ if (inputStream == null) {
+ throw new IllegalArgumentException("输入流不能为空");
+ }
+
+ // 如果未指定Content-Type,根据文件名自动获取
+ if (contentType == null || contentType.isEmpty()) {
+ contentType = getContentTypeByFileName(fileName);
+ }
+
+ return doUploadFile(fileName, mediaType, contentType, null, inputStream);
+ // 注意:正常流程下的流关闭在 uploadToQiniu 或 uploadToOther 的 finally 块中处理
+ }
+
@Override
public void close() throws IOException {
robotHttpUtils.close();
diff --git a/sdk/src/main/java/cn/wildfirechat/sdk/utilities/AdminHttpUtils.java b/sdk/src/main/java/cn/wildfirechat/sdk/utilities/AdminHttpUtils.java
index e844c4d0..55c85412 100644
--- a/sdk/src/main/java/cn/wildfirechat/sdk/utilities/AdminHttpUtils.java
+++ b/sdk/src/main/java/cn/wildfirechat/sdk/utilities/AdminHttpUtils.java
@@ -46,6 +46,11 @@ public class AdminHttpUtils extends HttpUtils {
// ======================== 线程安全的全局变量 ========================
private static volatile String adminUrl;
private static volatile String adminSecret;
+
+ public static CloseableHttpClient getHttpClient() {
+ return httpClient;
+ }
+
private static volatile CloseableHttpClient httpClient;
diff --git a/sdk/src/main/java/cn/wildfirechat/sdk/utilities/RobotHttpUtils.java b/sdk/src/main/java/cn/wildfirechat/sdk/utilities/RobotHttpUtils.java
index f554761a..6a66899c 100644
--- a/sdk/src/main/java/cn/wildfirechat/sdk/utilities/RobotHttpUtils.java
+++ b/sdk/src/main/java/cn/wildfirechat/sdk/utilities/RobotHttpUtils.java
@@ -120,6 +120,10 @@ public class RobotHttpUtils extends HttpUtils implements Closeable {
return robotSecret;
}
+ public CloseableHttpClient getHttpClient() {
+ return httpClient;
+ }
+
@Override
public void close() throws IOException {
httpClient.close();