diff --git a/docs/PLATFORM_OVERVIEW.md b/docs/PLATFORM_OVERVIEW.md
index 6cda91b..6d66226 100644
--- a/docs/PLATFORM_OVERVIEW.md
+++ b/docs/PLATFORM_OVERVIEW.md
@@ -83,7 +83,12 @@
| 平台 | Registry | 命令 |
|------|----------|------|
| npm (RN SDK / Vue3 SDK) | https://nexus.xuqinmin.com/repository/npm-hosted/ | `npm publish` |
-| Android Maven | https://nexus.xuqinmin.com/repository/android-hosted/ | `./gradlew publish` |
+| npm download | https://nexus.xuqinmin.com/repository/npm/ | client consumption |
+| Android Maven upload | https://nexus.xuqinmin.com/repository/android-hosted/ | `./gradlew publish` |
+| Android Maven download | https://nexus.xuqinmin.com/repository/android/ | client consumption |
+| Java Maven upload (release) | https://nexus.xuqinmin.com/repository/maven-releases/ | `mvn deploy` |
+| Java Maven upload (snapshot) | https://nexus.xuqinmin.com/repository/maven-snapshots/ | `mvn deploy` |
+| Java Maven download | https://nexus.xuqinmin.com/repository/maven-public/ | client consumption |
| iOS SPM | Git Tag | `git tag x.y.z && git push origin x.y.z` |
| iOS CocoaPods | https://xuqinmin.com/xuqinmin12/xuqm-specs | `pod repo push xuqm-specs XuqmSDK.podspec` |
| HarmonyOS | ohpm (OpenHarmony Package Manager) | `ohpm publish` |
diff --git a/im-sdk/pom.xml b/im-sdk/pom.xml
new file mode 100644
index 0000000..b48c65e
--- /dev/null
+++ b/im-sdk/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+
+
+ com.xuqm
+ xuqmgroup-server-parent
+ 0.1.0-SNAPSHOT
+ ../pom.xml
+
+
+ im-sdk
+ im-sdk
+ Java SDK for XuqmGroup IM service and REST APIs
+
+
+
+ com.xuqm
+ common
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
+
+
+ xuqm-maven-releases
+ https://nexus.xuqinmin.com/repository/maven-releases/
+
+
+ xuqm-maven-snapshots
+ https://nexus.xuqinmin.com/repository/maven-snapshots/
+
+
+
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
new file mode 100644
index 0000000..bd84561
--- /dev/null
+++ b/im-sdk/src/main/java/com/xuqm/im/sdk/XuqmImServerSdk.java
@@ -0,0 +1,1009 @@
+package com.xuqm.im.sdk;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.xuqm.common.model.ApiResponse;
+import com.xuqm.common.security.AppRequestSignatureUtil;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.Supplier;
+
+public final class XuqmImServerSdk {
+
+ private final HttpClient httpClient;
+ private final ObjectMapper objectMapper;
+ private final String baseUrl;
+ private final String appId;
+ private final String appSecret;
+ private final Supplier bearerTokenSupplier;
+
+ private XuqmImServerSdk(Builder builder) {
+ this.httpClient = HttpClient.newHttpClient();
+ this.objectMapper = new ObjectMapper()
+ .registerModule(new JavaTimeModule())
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ this.baseUrl = trimTrailingSlash(Objects.requireNonNull(builder.baseUrl, "baseUrl"));
+ this.appId = Objects.requireNonNull(builder.appId, "appId");
+ this.appSecret = Objects.requireNonNull(builder.appSecret, "appSecret");
+ this.bearerTokenSupplier = builder.bearerTokenSupplier;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public LoginResponse login(String userId, String nickname, String avatar) {
+ long timestamp = System.currentTimeMillis();
+ String nonce = UUID.randomUUID().toString().replace("-", "");
+ String payload = AppRequestSignatureUtil.payload(appId, userId, nickname, avatar, timestamp, nonce);
+ String signature = AppRequestSignatureUtil.sign(appSecret, payload);
+ URI uri = buildUri(
+ "/api/im/auth/login",
+ loginQuery(userId, nickname, avatar, timestamp, nonce)
+ );
+ ApiResponse response = request(
+ "POST",
+ uri,
+ null,
+ Map.of(
+ "X-App-Id", appId,
+ "X-App-Timestamp", String.valueOf(timestamp),
+ "X-App-Nonce", nonce,
+ "X-App-Signature", signature
+ ),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public ImMessage sendMessage(SendMessageRequest request) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/messages/send", Map.of("appId", appId)),
+ request,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public ImMessage revokeMessage(String messageId) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/messages/" + encode(messageId) + "/revoke", Map.of("appId", appId)),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public List listConversations(int size) {
+ ApiResponse> response = request(
+ "GET",
+ buildUri("/api/im/conversations", Map.of("appId", appId, "page", "0", "size", String.valueOf(size))),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public PageResult fetchHistory(String toId, HistoryQuery query) {
+ HistoryQuery effective = query == null ? new HistoryQuery() : query;
+ ApiResponse> response = request(
+ "GET",
+ buildUri(
+ "/api/im/messages/history/" + encode(toId),
+ queryParams(
+ effective.msgType(),
+ effective.keyword(),
+ effective.startTime(),
+ effective.endTime(),
+ effective.page() == null ? 0 : effective.page(),
+ effective.size() == null ? 20 : effective.size()
+ )
+ ),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public PageResult fetchGroupHistory(String groupId, HistoryQuery query) {
+ HistoryQuery effective = query == null ? new HistoryQuery() : query;
+ ApiResponse> response = request(
+ "GET",
+ buildUri(
+ "/api/im/messages/group-history/" + encode(groupId),
+ queryParams(
+ effective.msgType(),
+ effective.keyword(),
+ effective.startTime(),
+ effective.endTime(),
+ effective.page() == null ? 0 : effective.page(),
+ effective.size() == null ? 20 : effective.size()
+ )
+ ),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public List listConversations() {
+ return listConversations(20);
+ }
+
+ public AccountView getProfile(String userId) {
+ ApiResponse response = request(
+ "GET",
+ buildUri("/api/im/accounts/" + encode(userId), appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public AccountView updateProfile(String userId, String nickname, String avatar, String gender) {
+ Map query = appQuery();
+ if (nickname != null) {
+ query.put("nickname", nickname);
+ }
+ if (avatar != null) {
+ query.put("avatar", avatar);
+ }
+ if (gender != null) {
+ query.put("gender", gender);
+ }
+ ApiResponse response = request(
+ "PUT",
+ buildUri("/api/im/accounts/" + encode(userId), query),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public PageResult listUsers(int page, int size) {
+ ApiResponse> response = request(
+ "GET",
+ buildUri("/api/im/admin/users", queryWithPage(page, size)),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public AccountView registerUser(String userId, String nickname, String avatar) {
+ Map body = new LinkedHashMap<>();
+ body.put("userId", userId);
+ if (nickname != null) {
+ body.put("nickname", nickname);
+ }
+ if (avatar != null) {
+ body.put("avatar", avatar);
+ }
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/admin/users", appQuery()),
+ body,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public AccountView updateUserStatus(String userId, String status) {
+ ApiResponse response = request(
+ "PUT",
+ buildUri("/api/im/admin/users/" + encode(userId) + "/status", appQuery()),
+ Map.of("status", status),
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public List searchUsers(String keyword, int size) {
+ ApiResponse> response = request(
+ "GET",
+ buildUri("/api/im/admin/users/search", queryWithSize("keyword", keyword, size)),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public StatsView stats() {
+ ApiResponse response = request(
+ "GET",
+ buildUri("/api/im/admin/stats", appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public List listFriends() {
+ ApiResponse> response = request(
+ "GET",
+ buildUri("/api/im/friends", appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public FriendLinkView addFriend(String friendId) {
+ Map query = appQuery();
+ query.put("friendId", friendId);
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/friends", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public void removeFriend(String friendId) {
+ Map query = appQuery();
+ query.put("friendId", friendId);
+ request(
+ "DELETE",
+ buildUri("/api/im/friends/" + encode(friendId), query),
+ null,
+ authorizedHeaders(),
+ new TypeReference>() {}
+ );
+ }
+
+ public List listFriendRequests(String direction) {
+ Map query = appQuery();
+ if (direction != null) {
+ query.put("direction", direction);
+ }
+ ApiResponse> response = request(
+ "GET",
+ buildUri("/api/im/friend-requests", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public FriendRequestView sendFriendRequest(String toUserId, String remark) {
+ Map query = appQuery();
+ query.put("toUserId", toUserId);
+ if (remark != null) {
+ query.put("remark", remark);
+ }
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/friend-requests", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public FriendRequestView acceptFriendRequest(String requestId) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/friend-requests/" + encode(requestId) + "/accept", appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public FriendRequestView rejectFriendRequest(String requestId) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/friend-requests/" + encode(requestId) + "/reject", appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public List listBlacklist() {
+ ApiResponse> response = request(
+ "GET",
+ buildUri("/api/im/blacklist", appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public BlacklistView addBlacklist(String blockedUserId) {
+ Map query = appQuery();
+ query.put("blockedUserId", blockedUserId);
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/blacklist", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public void removeBlacklist(String blockedUserId) {
+ Map query = appQuery();
+ query.put("blockedUserId", blockedUserId);
+ request(
+ "DELETE",
+ buildUri("/api/im/blacklist", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference>() {}
+ );
+ }
+
+ public List listGroups() {
+ ApiResponse> response = request(
+ "GET",
+ buildUri("/api/im/groups", appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public List listPublicGroups(String keyword) {
+ Map query = appQuery();
+ if (keyword != null) {
+ query.put("keyword", keyword);
+ }
+ ApiResponse> response = request(
+ "GET",
+ buildUri("/api/im/groups/public", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public GroupView getGroup(String groupId) {
+ ApiResponse response = request(
+ "GET",
+ buildUri("/api/im/groups/" + encode(groupId), appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public GroupView createGroup(String name, List memberIds, String groupType) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/groups", appQuery()),
+ new CreateGroupRequest(name, memberIds, groupType),
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public GroupView updateGroup(String groupId, String name, String announcement) {
+ ApiResponse response = request(
+ "PUT",
+ buildUri("/api/im/groups/" + encode(groupId), appQuery()),
+ new UpdateGroupRequest(name, announcement),
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public GroupView addGroupMember(String groupId, String userId) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/groups/" + encode(groupId) + "/members", appQuery()),
+ new MemberRequest(userId),
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public GroupView removeGroupMember(String groupId, String targetUserId) {
+ ApiResponse response = request(
+ "DELETE",
+ buildUri("/api/im/groups/" + encode(groupId) + "/members/" + encode(targetUserId), appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public GroupView setGroupRole(String groupId, String userId, String role) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/groups/" + encode(groupId) + "/roles", appQuery()),
+ new SetRoleRequest(userId, role),
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public GroupView muteGroupMember(String groupId, String userId, long minutes) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/groups/" + encode(groupId) + "/mute", appQuery()),
+ new MuteMemberRequest(userId, minutes),
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public void dismissGroup(String groupId) {
+ request(
+ "DELETE",
+ buildUri("/api/im/groups/" + encode(groupId), appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference>() {}
+ );
+ }
+
+ public GroupJoinRequestView sendGroupJoinRequest(String groupId, String remark) {
+ Map query = appQuery();
+ if (remark != null) {
+ query.put("remark", remark);
+ }
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/groups/" + encode(groupId) + "/join-requests", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public List listGroupJoinRequests(String groupId) {
+ ApiResponse> response = request(
+ "GET",
+ buildUri("/api/im/groups/" + encode(groupId) + "/join-requests", appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public GroupJoinRequestView acceptGroupJoinRequest(String groupId, String requestId) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/" + encode(requestId) + "/accept", appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public GroupJoinRequestView rejectGroupJoinRequest(String groupId, String requestId) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/groups/" + encode(groupId) + "/join-requests/" + encode(requestId) + "/reject", appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public void setConversationPinned(String targetId, String chatType, boolean pinned) {
+ Map query = appQuery();
+ query.put("chatType", chatType);
+ query.put("pinned", String.valueOf(pinned));
+ request(
+ "PUT",
+ buildUri("/api/im/conversations/" + encode(targetId) + "/pinned", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference>() {}
+ );
+ }
+
+ public void setConversationMuted(String targetId, String chatType, boolean muted) {
+ Map query = appQuery();
+ query.put("chatType", chatType);
+ query.put("muted", String.valueOf(muted));
+ request(
+ "PUT",
+ buildUri("/api/im/conversations/" + encode(targetId) + "/muted", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference>() {}
+ );
+ }
+
+ public void markRead(String targetId, String chatType) {
+ Map query = appQuery();
+ query.put("chatType", chatType);
+ request(
+ "PUT",
+ buildUri("/api/im/conversations/" + encode(targetId) + "/read", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference>() {}
+ );
+ }
+
+ public void setDraft(String targetId, String chatType, String draft) {
+ Map query = appQuery();
+ query.put("chatType", chatType);
+ if (draft != null) {
+ query.put("draft", draft);
+ }
+ request(
+ "PUT",
+ buildUri("/api/im/conversations/" + encode(targetId) + "/draft", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference>() {}
+ );
+ }
+
+ public void deleteConversation(String targetId, String chatType) {
+ Map query = appQuery();
+ query.put("chatType", chatType);
+ request(
+ "DELETE",
+ buildUri("/api/im/conversations/" + encode(targetId), query),
+ null,
+ authorizedHeaders(),
+ new TypeReference>() {}
+ );
+ }
+
+ public PageResult queryAdminMessages(
+ String userA,
+ String userB,
+ String msgType,
+ String keyword,
+ LocalDateTime startTime,
+ LocalDateTime endTime,
+ int page,
+ int size) {
+ Map query = appQuery();
+ query.put("userA", userA);
+ query.put("userB", userB);
+ if (msgType != null) {
+ query.put("msgType", msgType);
+ }
+ if (keyword != null) {
+ query.put("keyword", keyword);
+ }
+ if (startTime != null) {
+ query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString());
+ }
+ if (endTime != null) {
+ query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString());
+ }
+ query.put("page", String.valueOf(page));
+ query.put("size", String.valueOf(size));
+ ApiResponse> response = request(
+ "GET",
+ buildUri("/api/im/admin/messages", query),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public ImMessage revokeAdminMessage(String messageId) {
+ ApiResponse response = request(
+ "POST",
+ buildUri("/api/im/admin/messages/" + encode(messageId) + "/revoke", appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference<>() {}
+ );
+ return response.data();
+ }
+
+ public void dismissAdminGroup(String groupId) {
+ request(
+ "DELETE",
+ buildUri("/api/im/admin/groups/" + encode(groupId), appQuery()),
+ null,
+ authorizedHeaders(),
+ new TypeReference>() {}
+ );
+ }
+
+ private Map authorizedHeaders() {
+ String token = bearerTokenSupplier == null ? null : bearerTokenSupplier.get();
+ if (token == null || token.isBlank()) {
+ throw new ImSdkException("Bearer token is required for this call");
+ }
+ return Map.of("Authorization", "Bearer " + token);
+ }
+
+ private ApiResponse request(
+ String method,
+ URI uri,
+ Object body,
+ Map headers,
+ TypeReference> responseType
+ ) {
+ try {
+ HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
+ .header("Content-Type", "application/json")
+ .headers(flatten(headers));
+ if (body != null) {
+ builder.method(method, HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)));
+ } else if ("GET".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) {
+ builder.method(method, HttpRequest.BodyPublishers.noBody());
+ } else {
+ builder.method(method, HttpRequest.BodyPublishers.noBody());
+ }
+ 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 URI buildUri(String path, Map query) {
+ StringBuilder builder = new StringBuilder(baseUrl).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);
+ query.put("userId", userId);
+ query.put("timestamp", String.valueOf(timestamp));
+ query.put("nonce", nonce);
+ if (nickname != null && !nickname.isBlank()) {
+ query.put("nickname", nickname);
+ }
+ if (avatar != null && !avatar.isBlank()) {
+ query.put("avatar", avatar);
+ }
+ return query;
+ }
+
+ private Map appQuery() {
+ Map query = new LinkedHashMap<>();
+ query.put("appId", appId);
+ return query;
+ }
+
+ private Map queryWithPage(int page, int size) {
+ Map query = appQuery();
+ query.put("page", String.valueOf(page));
+ query.put("size", String.valueOf(size));
+ return query;
+ }
+
+ private Map queryWithSize(String key, String value, int size) {
+ Map query = appQuery();
+ query.put(key, value);
+ query.put("size", String.valueOf(size));
+ return query;
+ }
+
+ private Map queryParams(String msgType, String keyword, LocalDateTime startTime, LocalDateTime endTime, int page, int size) {
+ Map query = new LinkedHashMap<>();
+ query.put("appId", appId);
+ if (msgType != null && !msgType.isBlank()) {
+ query.put("msgType", msgType);
+ }
+ if (keyword != null && !keyword.isBlank()) {
+ query.put("keyword", keyword);
+ }
+ if (startTime != null) {
+ query.put("startTime", startTime.toInstant(ZoneOffset.UTC).toString());
+ }
+ if (endTime != null) {
+ query.put("endTime", endTime.toInstant(ZoneOffset.UTC).toString());
+ }
+ query.put("page", String.valueOf(page));
+ query.put("size", String.valueOf(size));
+ return query;
+ }
+
+ private String[] flatten(Map headers) {
+ String[] pairs = new String[headers.size() * 2];
+ int index = 0;
+ for (Map.Entry entry : headers.entrySet()) {
+ pairs[index++] = entry.getKey();
+ pairs[index++] = entry.getValue();
+ }
+ return pairs;
+ }
+
+ private String encode(String value) {
+ return URLEncoder.encode(value, StandardCharsets.UTF_8);
+ }
+
+ private String trimTrailingSlash(String value) {
+ return value.endsWith("/") ? value.substring(0, value.length() - 1) : value;
+ }
+
+ public static final class Builder {
+ private String baseUrl;
+ private String appId;
+ private String appSecret;
+ private Supplier bearerTokenSupplier;
+
+ private Builder() {}
+
+ public Builder baseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ return this;
+ }
+
+ public Builder appId(String appId) {
+ this.appId = appId;
+ return this;
+ }
+
+ public Builder appSecret(String appSecret) {
+ this.appSecret = appSecret;
+ return this;
+ }
+
+ public Builder bearerTokenSupplier(Supplier bearerTokenSupplier) {
+ this.bearerTokenSupplier = bearerTokenSupplier;
+ return this;
+ }
+
+ public XuqmImServerSdk build() {
+ return new XuqmImServerSdk(this);
+ }
+ }
+
+ public record LoginResponse(String token, long expiresAt) {}
+
+ public record ConversationView(
+ String targetId,
+ String chatType,
+ String lastMsgContent,
+ String lastMsgType,
+ Long lastMsgTime,
+ int unreadCount,
+ boolean isMuted,
+ boolean isPinned
+ ) {}
+
+ public record AccountView(
+ String id,
+ String appId,
+ String userId,
+ String nickname,
+ String gender,
+ String avatar,
+ String status,
+ LocalDateTime createdAt
+ ) {}
+
+ public record FriendLinkView(
+ Long id,
+ String appId,
+ String userId,
+ String friendId,
+ Instant createdAt
+ ) {}
+
+ public record FriendRequestView(
+ String id,
+ String appId,
+ String fromUserId,
+ String toUserId,
+ String remark,
+ String status,
+ LocalDateTime createdAt,
+ LocalDateTime reviewedAt
+ ) {}
+
+ public record BlacklistView(
+ String id,
+ String appId,
+ String userId,
+ String blockedUserId,
+ LocalDateTime createdAt
+ ) {}
+
+ public record GroupView(
+ String id,
+ String appId,
+ String name,
+ String groupType,
+ String creatorId,
+ String memberIds,
+ String adminIds,
+ String announcement,
+ LocalDateTime createdAt
+ ) {
+ public List memberIdList() {
+ return parseJsonStringList(memberIds);
+ }
+
+ public List adminIdList() {
+ return parseJsonStringList(adminIds);
+ }
+ }
+
+ public record GroupJoinRequestView(
+ String id,
+ String appId,
+ String groupId,
+ String requesterId,
+ String remark,
+ String status,
+ LocalDateTime createdAt,
+ LocalDateTime reviewedAt
+ ) {}
+
+ public record StatsView(
+ long totalMessages,
+ long totalUsers,
+ long totalGroups,
+ long todayMessages
+ ) {}
+
+ public record HistoryQuery(
+ String msgType,
+ String keyword,
+ LocalDateTime startTime,
+ LocalDateTime endTime,
+ Integer page,
+ Integer size
+ ) {
+ public HistoryQuery() {
+ this(null, null, null, null, null, null);
+ }
+ }
+
+ public record PageResult(
+ List content,
+ long totalElements,
+ int totalPages,
+ int size,
+ int number,
+ int numberOfElements,
+ boolean first,
+ boolean last,
+ boolean empty
+ ) {}
+
+ public record ImMessage(
+ String id,
+ String appId,
+ String fromUserId,
+ String toId,
+ String chatType,
+ String msgType,
+ String content,
+ String status,
+ String mentionedUserIds,
+ Integer groupReadCount,
+ Long createdAt
+ ) {}
+
+ public record SendMessageRequest(
+ String messageId,
+ String toId,
+ String chatType,
+ String msgType,
+ String content,
+ String mentionedUserIds
+ ) {}
+
+ public record CreateGroupRequest(
+ String name,
+ List memberIds,
+ String groupType
+ ) {}
+
+ public record UpdateGroupRequest(
+ String name,
+ String announcement
+ ) {}
+
+ public record MemberRequest(String userId) {}
+
+ public record SetRoleRequest(String userId, String role) {}
+
+ public record MuteMemberRequest(String userId, long minutes) {}
+
+ public static final class ImSdkException extends RuntimeException {
+ public ImSdkException(String message) {
+ super(message);
+ }
+
+ public ImSdkException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ private static List parseJsonStringList(String value) {
+ if (value == null || value.isBlank()) {
+ return List.of();
+ }
+ String trimmed = value.trim();
+ if (!trimmed.startsWith("[")) {
+ return trimmed.isEmpty() ? List.of() : List.of(trimmed.split(","));
+ }
+ try {
+ ObjectMapper mapper = new ObjectMapper();
+ return mapper.readValue(trimmed, new TypeReference>() {});
+ } catch (IOException e) {
+ return new ArrayList<>(List.of(trimmed));
+ }
+ }
+}
diff --git a/im-service/src/main/java/com/xuqm/im/controller/AccountController.java b/im-service/src/main/java/com/xuqm/im/controller/AccountController.java
new file mode 100644
index 0000000..e05b5ed
--- /dev/null
+++ b/im-service/src/main/java/com/xuqm/im/controller/AccountController.java
@@ -0,0 +1,50 @@
+package com.xuqm.im.controller;
+
+import com.xuqm.common.exception.BusinessException;
+import com.xuqm.common.model.ApiResponse;
+import com.xuqm.im.entity.ImAccountEntity;
+import com.xuqm.im.service.ImAccountService;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Objects;
+
+@RestController
+@RequestMapping("/api/im/accounts")
+public class AccountController {
+
+ private final ImAccountService accountService;
+
+ public AccountController(ImAccountService accountService) {
+ this.accountService = accountService;
+ }
+
+ @GetMapping("/{userId}")
+ public ResponseEntity> get(
+ @AuthenticationPrincipal String currentUserId,
+ @RequestParam String appId,
+ @PathVariable String userId) {
+ return ResponseEntity.ok(ApiResponse.success(accountService.getAccount(appId, userId)));
+ }
+
+ @PutMapping("/{userId}")
+ public ResponseEntity> update(
+ @AuthenticationPrincipal String currentUserId,
+ @RequestParam String appId,
+ @PathVariable String userId,
+ @RequestParam(required = false) String nickname,
+ @RequestParam(required = false) String avatar,
+ @RequestParam(required = false) ImAccountEntity.Gender gender) {
+ if (!Objects.equals(currentUserId, userId)) {
+ throw new BusinessException(403, "Only the account owner can update profile");
+ }
+ return ResponseEntity.ok(ApiResponse.success(
+ accountService.updateAccount(appId, userId, nickname, avatar, gender)));
+ }
+}
diff --git a/pom.xml b/pom.xml
index 3c2570b..fd616b8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,7 @@
common
tenant-service
im-service
+ im-sdk
push-service
update-service
demo-service