From c217e96482a53ed081742efa159f4dc24dc3d596 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Tue, 28 Apr 2026 17:43:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(im):=20=E6=B7=BB=E5=8A=A0=E5=8D=B3?= =?UTF-8?q?=E6=97=B6=E9=80=9A=E8=AE=AF=E6=9C=8D=E5=8A=A1=E7=AB=AFSDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现用户登录认证功能,支持签名验证 - 添加消息发送、撤回及历史记录查询功能 - 实现会话管理功能,包括置顶、静音、已读标记等 - 添加好友系统功能,支持好友申请、同意、删除操作 - 实现群组管理功能,包括创建、解散、成员管理等 - 添加黑名单管理功能 - 实现推送服务集成,支持消息推送注册和发送 - 添加应用版本更新功能,支持Android应用和RN Bundle更新 - 实现关键词过滤和全局禁言管理功能 - 添加管理员消息查询和管理功能 - 集成Webhook配置管理功能 - 实现多服务器URL配置支持,包括基础服务、推送服务和更新服务 - 添加multipart表单数据上传支持,用于文件传输 - 实现统一的API请求处理和响应解析机制 --- .../java/com/xuqm/im/sdk/XuqmImServerSdk.java | 364 ++++++++++++++++++ 1 file changed, 364 insertions(+) diff --git a/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java b/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java index 3bee52b..dbf50c7 100644 --- a/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java +++ b/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java @@ -14,7 +14,10 @@ import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; @@ -31,6 +34,8 @@ public final class XuqmImServerSdk { private final HttpClient httpClient; private final ObjectMapper objectMapper; private final String baseUrl; + private final String pushBaseUrl; + private final String updateBaseUrl; private final String appId; private final String appSecret; private final Supplier bearerTokenSupplier; @@ -41,6 +46,8 @@ public final class XuqmImServerSdk { .registerModule(new JavaTimeModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); this.baseUrl = trimTrailingSlash(Objects.requireNonNull(builder.baseUrl, "baseUrl")); + this.pushBaseUrl = trimTrailingSlash(builder.pushBaseUrl == null ? builder.baseUrl : builder.pushBaseUrl); + this.updateBaseUrl = trimTrailingSlash(builder.updateBaseUrl == null ? builder.baseUrl : builder.updateBaseUrl); this.appId = Objects.requireNonNull(builder.appId, "appId"); this.appSecret = Objects.requireNonNull(builder.appSecret, "appSecret"); this.bearerTokenSupplier = builder.bearerTokenSupplier; @@ -405,6 +412,216 @@ public final class XuqmImServerSdk { return response.data(); } + public void registerPushToken(String userId, String vendor, String token) { + request( + "POST", + buildUri(pushBaseUrl, "/api/push/register", Map.of( + "appId", appId, + "userId", userId, + "vendor", vendor, + "token", token + )), + null, + publicHeaders(), + new TypeReference>() {} + ); + } + + public void sendPush(String userId, String title, String body, String payload) { + Map query = new LinkedHashMap<>(); + query.put("appId", appId); + query.put("userId", userId); + query.put("title", title); + query.put("body", body); + if (payload != null) { + query.put("payload", payload); + } + request( + "POST", + buildUri(pushBaseUrl, "/api/push/send", query), + null, + publicHeaders(), + new TypeReference>() {} + ); + } + + public Map checkAppUpdate(String platform, int currentVersionCode) { + ApiResponse> response = request( + "GET", + buildUri(updateBaseUrl, "/api/v1/updates/app/check", Map.of( + "appId", appId, + "platform", platform, + "currentVersionCode", String.valueOf(currentVersionCode) + )), + null, + publicHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public AppVersionView uploadAppVersion( + String platform, + String versionName, + int versionCode, + String changeLog, + boolean forceUpdate, + Path apkFile) { + Map form = new LinkedHashMap<>(); + form.put("appId", appId); + form.put("platform", platform); + form.put("versionName", versionName); + form.put("versionCode", String.valueOf(versionCode)); + if (changeLog != null) { + form.put("changeLog", changeLog); + } + form.put("forceUpdate", String.valueOf(forceUpdate)); + ApiResponse response = multipartRequest( + "POST", + buildUri(updateBaseUrl, "/api/v1/updates/app/upload", Map.of()), + form, + "apkFile", + apkFile, + authorizedHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public AppVersionView publishAppVersion(String id) { + ApiResponse response = request( + "POST", + buildUri(updateBaseUrl, "/api/v1/updates/app/" + encode(id) + "/publish", Map.of()), + null, + authorizedHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public AppVersionView unpublishAppVersion(String id) { + ApiResponse response = request( + "POST", + buildUri(updateBaseUrl, "/api/v1/updates/app/" + encode(id) + "/unpublish", Map.of()), + null, + authorizedHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public AppVersionView grayAppVersion(String id, boolean enabled, int percent) { + Map body = new LinkedHashMap<>(); + body.put("enabled", enabled); + body.put("percent", percent); + ApiResponse response = request( + "POST", + buildUri(updateBaseUrl, "/api/v1/updates/app/" + encode(id) + "/gray", Map.of()), + body, + authorizedHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public List listAppVersions(String platform) { + ApiResponse> response = request( + "GET", + buildUri(updateBaseUrl, "/api/v1/updates/app/list", Map.of("appId", appId, "platform", platform)), + null, + publicHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public RnBundleView checkRnUpdate(String moduleId, String platform, String currentVersion) { + ApiResponse response = request( + "GET", + buildUri(updateBaseUrl, "/api/v1/rn/update/check", Map.of( + "appId", appId, + "moduleId", moduleId, + "platform", platform, + "currentVersion", currentVersion + )), + null, + publicHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public RnBundleView uploadRnBundle( + String moduleId, + String platform, + String version, + String minCommonVersion, + String note, + Path bundle) { + Map form = new LinkedHashMap<>(); + form.put("appId", appId); + form.put("moduleId", moduleId); + form.put("platform", platform); + form.put("version", version); + if (minCommonVersion != null) { + form.put("minCommonVersion", minCommonVersion); + } + if (note != null) { + form.put("note", note); + } + ApiResponse response = multipartRequest( + "POST", + buildUri(updateBaseUrl, "/api/v1/rn/upload", Map.of()), + form, + "bundle", + bundle, + authorizedHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public RnBundleView publishRnBundle(String id) { + ApiResponse response = request( + "POST", + buildUri(updateBaseUrl, "/api/v1/rn/" + encode(id) + "/publish", Map.of()), + null, + authorizedHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public RnBundleView unpublishRnBundle(String id) { + ApiResponse response = request( + "POST", + buildUri(updateBaseUrl, "/api/v1/rn/" + encode(id) + "/unpublish", Map.of()), + null, + authorizedHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + + public List listRnBundles(String moduleId, String platform) { + Map query = new LinkedHashMap<>(); + query.put("appId", appId); + if (moduleId != null) { + query.put("moduleId", moduleId); + } + if (platform != null) { + query.put("platform", platform); + } + ApiResponse> response = request( + "GET", + buildUri(updateBaseUrl, "/api/v1/rn/list", query), + null, + publicHeaders(), + new TypeReference<>() {} + ); + return response.data(); + } + public List listFriends() { ApiResponse> response = request( "GET", @@ -821,6 +1038,10 @@ public final class XuqmImServerSdk { return Map.of("Authorization", "Bearer " + token); } + private Map publicHeaders() { + return Map.of(); + } + private ApiResponse request( String method, URI uri, @@ -856,6 +1077,66 @@ public final class XuqmImServerSdk { } } + private ApiResponse multipartRequest( + String method, + URI uri, + Map formFields, + String fileFieldName, + Path file, + Map headers, + TypeReference> responseType + ) { + String boundary = "----XuqmBoundary" + UUID.randomUUID().toString().replace("-", ""); + try { + HttpRequest.BodyPublisher bodyPublisher = ofMultipartData(boundary, formFields, fileFieldName, file); + Map mergedHeaders = new LinkedHashMap<>(headers); + mergedHeaders.put("Content-Type", "multipart/form-data; boundary=" + boundary); + HttpRequest.Builder builder = HttpRequest.newBuilder(uri) + .headers(flatten(mergedHeaders)) + .method(method, bodyPublisher); + HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() >= 400) { + throw new ImSdkException("HTTP " + response.statusCode() + ": " + response.body()); + } + ApiResponse apiResponse = objectMapper.readValue(response.body(), responseType); + if (apiResponse.code() != 200) { + throw new ImSdkException(apiResponse.message()); + } + return apiResponse; + } catch (IOException e) { + throw new ImSdkException("Request failed: " + e.getMessage(), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ImSdkException("Request interrupted", e); + } + } + + private HttpRequest.BodyPublisher ofMultipartData( + String boundary, + Map formFields, + String fileFieldName, + Path file + ) throws IOException { + var byteArrays = new ArrayList(); + Charset charset = StandardCharsets.UTF_8; + + for (Map.Entry entry : formFields.entrySet()) { + byteArrays.add(("--" + boundary + "\r\n").getBytes(charset)); + byteArrays.add(("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"\r\n\r\n").getBytes(charset)); + byteArrays.add(entry.getValue().getBytes(charset)); + byteArrays.add("\r\n".getBytes(charset)); + } + + byteArrays.add(("--" + boundary + "\r\n").getBytes(charset)); + byteArrays.add(("Content-Disposition: form-data; name=\"" + fileFieldName + "\"; filename=\"" + file.getFileName() + "\"\r\n").getBytes(charset)); + byteArrays.add(("Content-Type: application/octet-stream\r\n\r\n").getBytes(charset)); + byteArrays.add(Files.readAllBytes(file)); + byteArrays.add("\r\n".getBytes(charset)); + byteArrays.add(("--" + boundary + "--\r\n").getBytes(charset)); + + return HttpRequest.BodyPublishers.ofByteArrays(byteArrays); + } + private URI buildUri(String path, Map query) { StringBuilder builder = new StringBuilder(baseUrl).append(path); if (query != null && !query.isEmpty()) { @@ -876,6 +1157,26 @@ public final class XuqmImServerSdk { return URI.create(builder.toString()); } + private URI buildUri(String base, String path, Map query) { + StringBuilder builder = new StringBuilder(trimTrailingSlash(base)).append(path); + if (query != null && !query.isEmpty()) { + builder.append('?'); + boolean first = true; + for (Map.Entry entry : query.entrySet()) { + String value = entry.getValue(); + if (value == null) { + continue; + } + if (!first) { + builder.append('&'); + } + builder.append(encode(entry.getKey())).append('=').append(encode(value)); + first = false; + } + } + return URI.create(builder.toString()); + } + private Map loginQuery(String userId, String nickname, String avatar, long timestamp, String nonce) { Map query = new LinkedHashMap<>(); query.put("appId", appId); @@ -951,6 +1252,8 @@ public final class XuqmImServerSdk { public static final class Builder { private String baseUrl; + private String pushBaseUrl; + private String updateBaseUrl; private String appId; private String appSecret; private Supplier bearerTokenSupplier; @@ -962,6 +1265,16 @@ public final class XuqmImServerSdk { return this; } + public Builder pushBaseUrl(String pushBaseUrl) { + this.pushBaseUrl = pushBaseUrl; + return this; + } + + public Builder updateBaseUrl(String updateBaseUrl) { + this.updateBaseUrl = updateBaseUrl; + return this; + } + public Builder appId(String appId) { this.appId = appId; return this; @@ -1098,6 +1411,39 @@ public final class XuqmImServerSdk { Long updatedAt ) {} + public record AppVersionView( + String id, + String appId, + String platform, + String versionName, + int versionCode, + String downloadUrl, + String changeLog, + boolean forceUpdate, + String publishStatus, + String appStoreUrl, + String marketUrl, + boolean grayEnabled, + int grayPercent, + Long createdAt + ) {} + + public record RnBundleView( + String id, + String appId, + String moduleId, + String platform, + String version, + String bundleUrl, + String md5, + String minCommonVersion, + String note, + String publishStatus, + boolean grayEnabled, + int grayPercent, + Long createdAt + ) {} + public record HistoryQuery( String msgType, String keyword, @@ -1159,6 +1505,24 @@ public final class XuqmImServerSdk { Boolean enabled ) {} + public record AppVersionUploadRequest( + String appId, + String platform, + String versionName, + int versionCode, + String changeLog, + boolean forceUpdate + ) {} + + public record RnBundleUploadRequest( + String appId, + String moduleId, + String platform, + String version, + String minCommonVersion, + String note + ) {} + public record CreateGroupRequest( String name, List memberIds,