docs(project): 更新需求与开发进度对比报告并完善Android SDK接口定义

- 添加了完整的XuqmGroup平台需求与开发进度对比报告
- 实现了Android SDK的ImApi接口定义,涵盖群组、好友、黑名单等完整功能
- 定义了IM消息、会话、群组、用户资料等核心数据模型
- 实现了Android SDK的ImSDK核心功能类,包括连接管理和消息处理
这个提交包含在:
XuqmGroup 2026-05-02 12:30:31 +08:00
父节点 15466d4e2b
当前提交 d22d5f7687
共有 10 个文件被更改,包括 637 次插入2 次删除

查看文件

@ -113,8 +113,20 @@
| POST | `/api/im/accounts/import/batch` | 是 | 批量导入账号 |
| DELETE | `/api/im/accounts/{userId}` | 是 | 删除账号 |
| GET | `/api/im/accounts/{userId}/exists` | 是 | 检查账号是否存在 |
| PUT | `/api/im/conversations/{targetId}/hidden` | 是 | 隐藏 / 取消隐藏会话 |
| PUT | `/api/im/conversations/{targetId}/group` | 是 | 设置会话分组 |
| GET | `/api/im/conversation-groups` | 是 | 查询会话分组 |
| GET | `/api/im/conversation-groups/{groupName}` | 是 | 查询会话分组项 |
| DELETE | `/api/im/friends` | 是 | 删除当前用户全部好友 |
| PUT | `/api/im/friends/{friendId}/group` | 是 | 设置好友分组 |
| GET | `/api/im/friends/groups` | 是 | 查询好友分组 |
| GET | `/api/im/friends/groups/{groupName}` | 是 | 查询分组好友 |
| GET | `/api/im/blacklist/check` | 是 | 校验黑名单关系 |
| GET | `/api/im/groups/{groupId}/members` | 是 | 查询群成员列表 |
| GET | `/api/im/groups/{groupId}/members/search` | 是 | 搜索群成员 |
| POST | `/api/im/groups/{groupId}/owner` | 是 | 群主转让 |
| PUT | `/api/im/groups/{groupId}/attributes` | 是 | 设置群扩展属性 |
| POST | `/api/im/groups/{groupId}/attributes/delete` | 是 | 删除群扩展属性 |
| POST | `/api/im/messages/send` | 是 | 发送消息TEXT / IMAGE / AUDIO / VIDEO / FILE / LOCATION / CUSTOM / NOTIFY / RICH_TEXT / CALL_AUDIO / CALL_VIDEO / FORWARD / QUOTE / MERGE |
| GET | `/api/im/messages/search` | 是 | 云端消息搜索 |
| PUT | `/api/im/messages/{id}` | 是 | 编辑自己发送的文本消息 |
@ -257,12 +269,29 @@ curl 'https://dev.xuqinmin.com/api/im/accounts/user_001/exists?appId=ak_demo_cha
curl 'https://dev.xuqinmin.com/api/im/conversations?appId=ak_demo_chat'
curl -X PUT 'https://dev.xuqinmin.com/api/im/conversations/user_002/pinned?appId=ak_demo_chat&chatType=SINGLE&pinned=true'
curl -X PUT 'https://dev.xuqinmin.com/api/im/conversations/user_002/draft?appId=ak_demo_chat&chatType=SINGLE&draft=hello'
curl -X PUT 'https://dev.xuqinmin.com/api/im/conversations/user_002/hidden?appId=ak_demo_chat&chatType=SINGLE&hidden=true'
curl -X PUT 'https://dev.xuqinmin.com/api/im/conversations/user_002/group?appId=ak_demo_chat&chatType=SINGLE&groupName=重要客户'
curl 'https://dev.xuqinmin.com/api/im/conversation-groups?appId=ak_demo_chat'
curl 'https://dev.xuqinmin.com/api/im/conversation-groups/重要客户?appId=ak_demo_chat'
curl -X DELETE 'https://dev.xuqinmin.com/api/im/conversations/user_002?appId=ak_demo_chat&chatType=SINGLE'
curl -X PUT 'https://dev.xuqinmin.com/api/im/friends/user_002/group?appId=ak_demo_chat&groupName=同事'
curl 'https://dev.xuqinmin.com/api/im/friends/groups?appId=ak_demo_chat'
curl 'https://dev.xuqinmin.com/api/im/friends/groups/同事?appId=ak_demo_chat'
curl -X DELETE 'https://dev.xuqinmin.com/api/im/friends?appId=ak_demo_chat'
curl 'https://dev.xuqinmin.com/api/im/groups?appId=ak_demo_chat'
curl 'https://dev.xuqinmin.com/api/im/groups/public?appId=ak_demo_chat&keyword=demo'
curl 'https://dev.xuqinmin.com/api/im/groups/search?appId=ak_demo_chat&keyword=demo&size=20'
curl 'https://dev.xuqinmin.com/api/im/groups/group_001/members?appId=ak_demo_chat'
curl 'https://dev.xuqinmin.com/api/im/groups/group_001/members/search?appId=ak_demo_chat&keyword=demo&size=20'
curl -X POST 'https://dev.xuqinmin.com/api/im/groups/group_001/owner?appId=ak_demo_chat' \
-H 'Content-Type: application/json' \
-d '{"newOwnerId":"user_002"}'
curl -X PUT 'https://dev.xuqinmin.com/api/im/groups/group_001/attributes?appId=ak_demo_chat' \
-H 'Content-Type: application/json' \
-d '{"department":"sales","priority":"high"}'
curl -X POST 'https://dev.xuqinmin.com/api/im/groups/group_001/attributes/delete?appId=ak_demo_chat' \
-H 'Content-Type: application/json' \
-d '{"keys":["priority"]}'
curl 'https://dev.xuqinmin.com/api/im/messages/search?appId=ak_demo_chat&keyword=hello&page=0&size=20'
curl 'https://dev.xuqinmin.com/api/im/admin/webhooks?appId=ak_demo_chat'
curl -X POST 'https://dev.xuqinmin.com/api/im/groups/group_001/join-requests?appId=ak_demo_chat&remark=申请加入'
@ -270,6 +299,7 @@ curl 'https://dev.xuqinmin.com/api/im/groups/group_001/join-requests?appId=ak_de
curl -X POST 'https://dev.xuqinmin.com/api/im/groups/group_001/join-requests/req_001/accept?appId=ak_demo_chat'
curl -X POST 'https://dev.xuqinmin.com/api/im/groups/group_001/join-requests/req_001/reject?appId=ak_demo_chat'
curl 'https://dev.xuqinmin.com/api/im/blacklist?appId=ak_demo_chat'
curl 'https://dev.xuqinmin.com/api/im/blacklist/check?appId=ak_demo_chat&targetUserId=user_002'
curl 'https://dev.xuqinmin.com/api/im/friend-requests?appId=ak_demo_chat&direction=incoming'
```
@ -288,6 +318,15 @@ curl -X POST 'https://dev.xuqinmin.com/api/im/admin/blacklist?appId=ak_demo_chat
-H 'Content-Type: application/json' \
-d '{"userId":"user_001","blockedUserId":"user_002"}'
curl 'https://dev.xuqinmin.com/api/im/admin/groups/group_001/members?appId=ak_demo_chat'
curl -X POST 'https://dev.xuqinmin.com/api/im/admin/groups/group_001/owner?appId=ak_demo_chat' \
-H 'Content-Type: application/json' \
-d '{"newOwnerId":"user_002"}'
curl -X PUT 'https://dev.xuqinmin.com/api/im/admin/groups/group_001/attributes?appId=ak_demo_chat' \
-H 'Content-Type: application/json' \
-d '{"department":"sales"}'
curl -X POST 'https://dev.xuqinmin.com/api/im/admin/groups/group_001/read-receipts?appId=ak_demo_chat' \
-H 'Content-Type: application/json' \
-d '{"messageIds":["msg_001","msg_002"]}'
curl 'https://dev.xuqinmin.com/api/im/admin/groups/group_001/join-requests?appId=ak_demo_chat'
curl -X POST 'https://dev.xuqinmin.com/api/im/admin/groups/group_001/join-requests/req_001/accept?appId=ak_demo_chat'
curl 'https://dev.xuqinmin.com/api/im/admin/keyword-filters?appId=ak_demo_chat'

查看文件

@ -681,6 +681,53 @@ public final class XuqmImServerSdk {
);
}
public void removeAllFriends() {
request(
"DELETE",
buildUri("/api/im/friends", appQuery()),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public FriendLinkView setFriendGroup(String friendId, String groupName) {
Map<String, String> query = appQuery();
if (groupName != null) {
query.put("groupName", groupName);
}
ApiResponse<FriendLinkView> response = request(
"PUT",
buildUri("/api/im/friends/" + encode(friendId) + "/group", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<String> listFriendGroups() {
ApiResponse<List<String>> response = request(
"GET",
buildUri("/api/im/friends/groups", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<String> listFriendsByGroup(String groupName) {
ApiResponse<List<String>> response = request(
"GET",
buildUri("/api/im/friends/groups/" + encode(groupName), appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<FriendRequestView> listFriendRequests(String direction) {
Map<String, String> query = appQuery();
if (direction != null) {
@ -792,6 +839,19 @@ public final class XuqmImServerSdk {
);
}
public BlacklistCheckResult checkBlacklist(String targetUserId) {
Map<String, String> query = appQuery();
query.put("targetUserId", targetUserId);
ApiResponse<BlacklistCheckResult> response = request(
"GET",
buildUri("/api/im/blacklist/check", query),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<GroupView> listGroups() {
ApiResponse<List<GroupView>> response = request(
"GET",
@ -939,6 +999,39 @@ public final class XuqmImServerSdk {
return response.data();
}
public GroupView transferGroupOwner(String groupId, String newOwnerId) {
ApiResponse<GroupView> response = request(
"POST",
buildUri("/api/im/groups/" + encode(groupId) + "/owner", appQuery()),
new TransferOwnerRequest(newOwnerId),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView updateGroupAttributes(String groupId, Map<String, Object> attributes) {
ApiResponse<GroupView> response = request(
"PUT",
buildUri("/api/im/groups/" + encode(groupId) + "/attributes", appQuery()),
attributes == null ? Map.of() : attributes,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView removeGroupAttributes(String groupId, List<String> keys) {
ApiResponse<GroupView> response = request(
"POST",
buildUri("/api/im/groups/" + encode(groupId) + "/attributes/delete", appQuery()),
new AttributeKeysRequest(keys),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView muteGroupMember(String groupId, String userId, long minutes) {
ApiResponse<GroupView> response = request(
"POST",
@ -1117,6 +1210,50 @@ public final class XuqmImServerSdk {
return response.data();
}
public GroupView adminTransferGroupOwner(String groupId, String newOwnerId) {
ApiResponse<GroupView> response = request(
"POST",
buildUri("/api/im/admin/groups/" + encode(groupId) + "/owner", appQuery()),
new TransferOwnerRequest(newOwnerId),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView adminUpdateGroupAttributes(String groupId, Map<String, Object> attributes) {
ApiResponse<GroupView> response = request(
"PUT",
buildUri("/api/im/admin/groups/" + encode(groupId) + "/attributes", appQuery()),
attributes == null ? Map.of() : attributes,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public GroupView adminRemoveGroupAttributes(String groupId, List<String> keys) {
ApiResponse<GroupView> response = request(
"POST",
buildUri("/api/im/admin/groups/" + encode(groupId) + "/attributes/delete", appQuery()),
new AttributeKeysRequest(keys),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<GroupReadReceiptSummary> adminGroupReadReceipts(String groupId, List<String> messageIds) {
ApiResponse<List<GroupReadReceiptSummary>> response = request(
"POST",
buildUri("/api/im/admin/groups/" + encode(groupId) + "/read-receipts", appQuery()),
new GroupReadReceiptRequest(messageIds),
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public void setConversationPinned(String targetId, String chatType, boolean pinned) {
Map<String, String> query = appQuery();
query.put("chatType", chatType);
@ -1170,6 +1307,56 @@ public final class XuqmImServerSdk {
);
}
public void setConversationHidden(String targetId, String chatType, boolean hidden) {
Map<String, String> query = appQuery();
query.put("chatType", chatType);
query.put("hidden", String.valueOf(hidden));
request(
"PUT",
buildUri("/api/im/conversations/" + encode(targetId) + "/hidden", query),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public void setConversationGroup(String targetId, String chatType, String groupName) {
Map<String, String> query = appQuery();
query.put("chatType", chatType);
if (groupName != null) {
query.put("groupName", groupName);
}
request(
"PUT",
buildUri("/api/im/conversations/" + encode(targetId) + "/group", query),
null,
authorizedHeaders(),
new TypeReference<ApiResponse<Void>>() {}
);
}
public List<String> listConversationGroups() {
ApiResponse<List<String>> response = request(
"GET",
buildUri("/api/im/conversation-groups", appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public List<ConversationGroupItem> listConversationGroupItems(String groupName) {
ApiResponse<List<ConversationGroupItem>> response = request(
"GET",
buildUri("/api/im/conversation-groups/" + encode(groupName), appQuery()),
null,
authorizedHeaders(),
new TypeReference<>() {}
);
return response.data();
}
public void deleteConversation(String targetId, String chatType) {
Map<String, String> query = appQuery();
query.put("chatType", chatType);
@ -1586,7 +1773,14 @@ public final class XuqmImServerSdk {
Long lastMsgTime,
int unreadCount,
boolean isMuted,
boolean isPinned
boolean isPinned,
String conversationGroup
) {}
public record ConversationGroupItem(
String targetId,
String chatType,
String groupName
) {}
public record AccountView(
@ -1613,6 +1807,7 @@ public final class XuqmImServerSdk {
String appId,
String userId,
String friendId,
String friendGroup,
Instant createdAt
) {}
@ -1637,6 +1832,13 @@ public final class XuqmImServerSdk {
LocalDateTime createdAt
) {}
public record BlacklistCheckResult(
String targetUserId,
boolean blockedByMe,
boolean blockedByTarget,
boolean eitherBlocked
) {}
public record GroupView(
String id,
String appId,
@ -1646,6 +1848,8 @@ public final class XuqmImServerSdk {
String memberIds,
String adminIds,
String announcement,
String memberInfo,
String extAttributes,
LocalDateTime createdAt
) {
public List<String> memberIdList() {
@ -1657,6 +1861,14 @@ public final class XuqmImServerSdk {
}
}
public record GroupReadReceiptSummary(
String messageId,
String groupId,
int memberCount,
int readCount,
int unreadCount
) {}
public record GroupJoinRequestView(
String id,
String appId,
@ -1899,8 +2111,14 @@ public final class XuqmImServerSdk {
public record SetRoleRequest(String userId, String role) {}
public record TransferOwnerRequest(String newOwnerId) {}
public record AttributeKeysRequest(List<String> keys) {}
public record MuteMemberRequest(String userId, long minutes) {}
public record GroupReadReceiptRequest(List<String> messageIds) {}
public record RequestBatch(List<String> requestIds) {}
public record FriendCheckResult(String userId, boolean isFriend) {}

查看文件

@ -47,4 +47,22 @@ public class BlacklistController {
blacklistService.remove(appId, userId, blockedUserId);
return ResponseEntity.ok(ApiResponse.ok());
}
@GetMapping("/check")
public ResponseEntity<ApiResponse<BlacklistCheckResult>> check(
@AuthenticationPrincipal String userId,
@RequestParam String appId,
@RequestParam String targetUserId) {
boolean blockedByMe = blacklistService.isBlocked(appId, userId, targetUserId);
boolean blockedByTarget = blacklistService.isBlocked(appId, targetUserId, userId);
return ResponseEntity.ok(ApiResponse.success(
new BlacklistCheckResult(targetUserId, blockedByMe, blockedByTarget, blockedByMe || blockedByTarget)));
}
public record BlacklistCheckResult(
String targetUserId,
boolean blockedByMe,
boolean blockedByTarget,
boolean eitherBlocked
) {}
}

查看文件

@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/im")
@ -90,6 +91,52 @@ public class ConversationController {
return ResponseEntity.ok(ApiResponse.ok());
}
@PutMapping("/conversations/{targetId}/hidden")
public ResponseEntity<ApiResponse<Void>> setHidden(
@AuthenticationPrincipal String userId,
@RequestParam String appId,
@PathVariable String targetId,
@RequestParam String chatType,
@RequestParam boolean hidden) {
conversationStateService.setHidden(appId, userId, targetId, chatType, hidden);
return ResponseEntity.ok(ApiResponse.ok());
}
@PutMapping("/conversations/{targetId}/group")
public ResponseEntity<ApiResponse<Void>> setConversationGroup(
@AuthenticationPrincipal String userId,
@RequestParam String appId,
@PathVariable String targetId,
@RequestParam String chatType,
@RequestParam(required = false) String groupName) {
conversationStateService.setConversationGroup(appId, userId, targetId, chatType, groupName);
return ResponseEntity.ok(ApiResponse.ok());
}
@GetMapping("/conversation-groups")
public ResponseEntity<ApiResponse<List<String>>> listConversationGroups(
@AuthenticationPrincipal String userId,
@RequestParam String appId) {
return ResponseEntity.ok(ApiResponse.success(conversationStateService.listConversationGroups(appId, userId)));
}
@GetMapping("/conversation-groups/{groupName}")
public ResponseEntity<ApiResponse<List<Map<String, String>>>> listConversationGroupItems(
@AuthenticationPrincipal String userId,
@RequestParam String appId,
@PathVariable String groupName) {
List<Map<String, String>> items = conversationStateService
.listByConversationGroup(appId, userId, groupName)
.stream()
.map(state -> Map.of(
"targetId", state.getTargetId(),
"chatType", state.getChatType(),
"groupName", state.getConversationGroup()
))
.toList();
return ResponseEntity.ok(ApiResponse.success(items));
}
@DeleteMapping("/conversations/{targetId}")
public ResponseEntity<ApiResponse<Void>> deleteConversation(
@AuthenticationPrincipal String userId,

查看文件

@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@ -72,6 +73,15 @@ public class FriendController {
return ResponseEntity.ok(ApiResponse.success(null));
}
@DeleteMapping
public ResponseEntity<ApiResponse<Void>> removeAllFriends(
@AuthenticationPrincipal String userId,
@RequestParam String appId) {
friendRepository.deleteByAppIdAndUserId(appId, userId);
friendRepository.deleteByAppIdAndFriendId(appId, userId);
return ResponseEntity.ok(ApiResponse.ok());
}
@PostMapping("/batch/remove")
public ResponseEntity<ApiResponse<Void>> removeFriends(
@AuthenticationPrincipal String userId,
@ -87,6 +97,44 @@ public class FriendController {
return ResponseEntity.ok(ApiResponse.success(null));
}
@PutMapping("/{friendId}/group")
public ResponseEntity<ApiResponse<ImFriendEntity>> setFriendGroup(
@AuthenticationPrincipal String userId,
@PathVariable String friendId,
@RequestParam String appId,
@RequestParam(required = false) String groupName) {
ImFriendEntity link = friendRepository.findByAppIdAndUserIdAndFriendId(appId, userId, friendId)
.orElseGet(() -> addFriendLink(appId, userId, friendId));
link.setFriendGroup(normalizeGroup(groupName));
return ResponseEntity.ok(ApiResponse.success(friendRepository.save(link)));
}
@GetMapping("/groups")
public ResponseEntity<ApiResponse<List<String>>> listFriendGroups(
@AuthenticationPrincipal String userId,
@RequestParam String appId) {
List<String> groups = friendRepository.findByAppIdAndUserId(appId, userId).stream()
.map(ImFriendEntity::getFriendGroup)
.filter(group -> group != null && !group.isBlank())
.distinct()
.sorted()
.toList();
return ResponseEntity.ok(ApiResponse.success(groups));
}
@GetMapping("/groups/{groupName}")
public ResponseEntity<ApiResponse<List<String>>> listFriendsByGroup(
@AuthenticationPrincipal String userId,
@RequestParam String appId,
@PathVariable String groupName) {
List<String> friendIds = friendRepository
.findByAppIdAndUserIdAndFriendGroup(appId, userId, normalizeGroup(groupName))
.stream()
.map(ImFriendEntity::getFriendId)
.toList();
return ResponseEntity.ok(ApiResponse.success(friendIds));
}
private ImFriendEntity addFriendLink(String appId, String userId, String friendId) {
ImFriendEntity forward = friendRepository
.findByAppIdAndUserIdAndFriendId(appId, userId, friendId)
@ -113,6 +161,10 @@ public class FriendController {
return friendIds == null ? List.of() : new ArrayList<>(new LinkedHashSet<>(friendIds));
}
private String normalizeGroup(String groupName) {
return groupName == null || groupName.isBlank() ? null : groupName.trim();
}
@PostMapping("/check")
public ResponseEntity<ApiResponse<List<FriendCheckResult>>> checkFriends(
@AuthenticationPrincipal String userId,

查看文件

@ -10,6 +10,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/im/groups")
@ -127,6 +128,33 @@ public class GroupController {
groupService.setRole(groupId, userId, req.userId(), req.role())));
}
@PostMapping("/{groupId}/owner")
public ResponseEntity<ApiResponse<ImGroupEntity>> transferOwner(
@PathVariable String groupId,
@RequestBody TransferOwnerRequest req,
@AuthenticationPrincipal String userId) {
return ResponseEntity.ok(ApiResponse.success(
groupService.transferOwner(groupId, userId, req.newOwnerId())));
}
@PutMapping("/{groupId}/attributes")
public ResponseEntity<ApiResponse<ImGroupEntity>> updateAttributes(
@PathVariable String groupId,
@RequestBody Map<String, Object> attributes,
@AuthenticationPrincipal String userId) {
return ResponseEntity.ok(ApiResponse.success(
groupService.updateExtAttributes(groupId, userId, attributes)));
}
@PostMapping("/{groupId}/attributes/delete")
public ResponseEntity<ApiResponse<ImGroupEntity>> removeAttributes(
@PathVariable String groupId,
@RequestBody AttributeKeysRequest req,
@AuthenticationPrincipal String userId) {
return ResponseEntity.ok(ApiResponse.success(
groupService.removeExtAttributes(groupId, userId, req.keys())));
}
@PostMapping("/{groupId}/mute")
public ResponseEntity<ApiResponse<ImGroupEntity>> muteMember(
@PathVariable String groupId,
@ -214,6 +242,8 @@ public class GroupController {
public record MemberRequest(String userId) {}
public record MemberBatchRequest(List<String> userIds) {}
public record SetRoleRequest(String userId, String role) {}
public record TransferOwnerRequest(String newOwnerId) {}
public record AttributeKeysRequest(List<String> keys) {}
public record MuteMemberRequest(String userId, long minutes) {}
public record RequestBatch(List<String> requestIds) {}
public record ModifyMemberInfoRequest(String nickName) {}

查看文件

@ -408,6 +408,40 @@ public class ImAdminController {
return ResponseEntity.ok(ApiResponse.success(saved));
}
@PostMapping("/groups/{groupId}/owner")
public ResponseEntity<ApiResponse<ImGroupEntity>> transferGroupOwner(
@RequestParam String appId,
@PathVariable String groupId,
@AuthenticationPrincipal String operatorId,
@RequestBody TransferOwnerRequest req) {
ImGroupEntity saved = groupService.adminTransferOwner(appId, groupId, req.newOwnerId());
operationLogService.record(appId, operatorId, "ADMIN_TRANSFER_GROUP_OWNER", "GROUP", groupId, req.newOwnerId());
return ResponseEntity.ok(ApiResponse.success(saved));
}
@PutMapping("/groups/{groupId}/attributes")
public ResponseEntity<ApiResponse<ImGroupEntity>> updateGroupAttributes(
@RequestParam String appId,
@PathVariable String groupId,
@AuthenticationPrincipal String operatorId,
@RequestBody Map<String, Object> attributes) {
ImGroupEntity saved = groupService.adminUpdateExtAttributes(appId, groupId, attributes);
operationLogService.record(appId, operatorId, "ADMIN_UPDATE_GROUP_ATTRIBUTES", "GROUP", groupId, null);
return ResponseEntity.ok(ApiResponse.success(saved));
}
@PostMapping("/groups/{groupId}/attributes/delete")
public ResponseEntity<ApiResponse<ImGroupEntity>> removeGroupAttributes(
@RequestParam String appId,
@PathVariable String groupId,
@AuthenticationPrincipal String operatorId,
@RequestBody AttributeKeysRequest req) {
ImGroupEntity saved = groupService.adminRemoveExtAttributes(appId, groupId, req.keys());
operationLogService.record(appId, operatorId, "ADMIN_REMOVE_GROUP_ATTRIBUTES", "GROUP", groupId,
String.join(",", req.keys() == null ? List.of() : req.keys()));
return ResponseEntity.ok(ApiResponse.success(saved));
}
@PostMapping("/groups/{groupId}/mute")
public ResponseEntity<ApiResponse<ImGroupEntity>> muteGroupMember(
@RequestParam String appId,
@ -419,6 +453,19 @@ public class ImAdminController {
return ResponseEntity.ok(ApiResponse.success(saved));
}
@PostMapping("/groups/{groupId}/read-receipts")
public ResponseEntity<ApiResponse<List<MessageService.GroupReadReceiptSummary>>> groupReadReceipts(
@RequestParam String appId,
@PathVariable String groupId,
@AuthenticationPrincipal String operatorId,
@RequestBody GroupReadReceiptRequest req) {
List<MessageService.GroupReadReceiptSummary> receipts =
messageService.groupReadReceipts(appId, groupId, req.messageIds());
operationLogService.record(appId, operatorId, "QUERY_GROUP_READ_RECEIPTS", "GROUP", groupId,
"count=" + receipts.size());
return ResponseEntity.ok(ApiResponse.success(receipts));
}
@GetMapping("/groups/{groupId}/join-requests")
public ResponseEntity<ApiResponse<List<ImGroupJoinRequestEntity>>> listGroupJoinRequests(
@RequestParam String appId,
@ -632,7 +679,10 @@ public class ImAdminController {
public record BlacklistRequest(String userId, String blockedUserId) {}
public record MemberRequest(String userId) {}
public record SetRoleRequest(String userId, String role) {}
public record TransferOwnerRequest(String newOwnerId) {}
public record AttributeKeysRequest(List<String> keys) {}
public record MuteMemberRequest(String userId, long minutes) {}
public record GroupReadReceiptRequest(List<String> messageIds) {}
public record KickRequest(List<String> userIds) {}
public record BatchSendRequest(List<String> toIds, ImMessageEntity.MsgType msgType, String content) {}
public record SetMsgReadRequest(String userId) {}

查看文件

@ -52,6 +52,30 @@ public class ConversationStateService {
return repository.save(state);
}
@Transactional
public ImConversationStateEntity setHidden(String appId, String userId, String targetId, String chatType, boolean hidden) {
ImConversationStateEntity state = getOrCreate(appId, userId, targetId, chatType);
state.setHidden(hidden);
if (hidden) {
state.setDraft(null);
}
touch(state);
return repository.save(state);
}
@Transactional
public ImConversationStateEntity setConversationGroup(
String appId,
String userId,
String targetId,
String chatType,
String conversationGroup) {
ImConversationStateEntity state = getOrCreate(appId, userId, targetId, chatType);
state.setConversationGroup(normalizeGroup(conversationGroup));
touch(state);
return repository.save(state);
}
@Transactional
public ImConversationStateEntity hideConversation(String appId, String userId, String targetId, String chatType) {
ImConversationStateEntity state = getOrCreate(appId, userId, targetId, chatType);
@ -106,6 +130,7 @@ public class ConversationStateService {
entity.setMuted(false);
entity.setHidden(false);
entity.setDraft(null);
entity.setConversationGroup(null);
entity.setLastReadAt(null);
entity.setCreatedAt(LocalDateTime.now());
entity.setUpdatedAt(LocalDateTime.now());
@ -122,7 +147,27 @@ public class ConversationStateService {
return repository.findByAppIdAndUserIdAndHiddenFalse(appId, userId);
}
public List<String> listConversationGroups(String appId, String userId) {
return repository.findByAppIdAndUserId(appId, userId).stream()
.map(ImConversationStateEntity::getConversationGroup)
.filter(group -> group != null && !group.isBlank())
.distinct()
.sorted()
.toList();
}
public List<ImConversationStateEntity> listByConversationGroup(String appId, String userId, String conversationGroup) {
return repository.findByAppIdAndUserIdAndConversationGroup(appId, userId, normalizeGroup(conversationGroup));
}
private void touch(ImConversationStateEntity entity) {
entity.setUpdatedAt(LocalDateTime.now());
}
private String normalizeGroup(String group) {
if (group == null || group.isBlank()) {
return null;
}
return group.trim();
}
}

查看文件

@ -82,6 +82,7 @@ public class ImGroupService {
group.setMemberIds(toJson(members));
group.setAdminIds(toJson(List.of(creatorId)));
group.setAnnouncement(announcement);
group.setExtAttributes("{}");
group.setCreatedAt(LocalDateTime.now());
ImGroupEntity saved = groupRepository.save(group);
webhookDispatchService.dispatch(appId, "group", "group.created",
@ -466,6 +467,50 @@ public class ImGroupService {
.toList();
}
@Transactional
public ImGroupEntity transferOwner(String groupId, String operatorId, String newOwnerId) {
ImGroupEntity group = get(groupId);
if (!group.getCreatorId().equals(operatorId)) {
throw new BusinessException(403, "只有群主可以转让群");
}
return transferOwnerInternal(group, newOwnerId);
}
@Transactional
public ImGroupEntity adminTransferOwner(String appId, String groupId, String newOwnerId) {
ImGroupEntity group = get(groupId);
ensureAppMatches(group, appId);
return transferOwnerInternal(group, newOwnerId);
}
@Transactional
public ImGroupEntity updateExtAttributes(String groupId, String operatorId, Map<String, Object> attributes) {
ImGroupEntity group = get(groupId);
ensureCanManage(group, operatorId);
return updateExtAttributesInternal(group, attributes);
}
@Transactional
public ImGroupEntity adminUpdateExtAttributes(String appId, String groupId, Map<String, Object> attributes) {
ImGroupEntity group = get(groupId);
ensureAppMatches(group, appId);
return updateExtAttributesInternal(group, attributes);
}
@Transactional
public ImGroupEntity removeExtAttributes(String groupId, String operatorId, List<String> keys) {
ImGroupEntity group = get(groupId);
ensureCanManage(group, operatorId);
return removeExtAttributesInternal(group, keys);
}
@Transactional
public ImGroupEntity adminRemoveExtAttributes(String appId, String groupId, List<String> keys) {
ImGroupEntity group = get(groupId);
ensureAppMatches(group, appId);
return removeExtAttributesInternal(group, keys);
}
@Transactional
public ImGroupEntity adminAddMember(String appId, String groupId, String userId) {
ImGroupEntity group = get(groupId);
@ -606,6 +651,65 @@ public class ImGroupService {
}
}
private ImGroupEntity transferOwnerInternal(ImGroupEntity group, String newOwnerId) {
if (newOwnerId == null || newOwnerId.isBlank()) {
throw new BusinessException(400, "新群主不能为空");
}
List<String> members = new ArrayList<>(memberIds(group));
if (!members.contains(newOwnerId)) {
members.add(newOwnerId);
group.setMemberIds(toJson(members));
}
List<String> admins = new ArrayList<>(adminIds(group));
if (!admins.contains(newOwnerId)) {
admins.add(newOwnerId);
}
group.setCreatorId(newOwnerId);
group.setAdminIds(toJson(admins));
ImGroupEntity saved = groupRepository.save(group);
webhookDispatchService.dispatch(saved.getAppId(), "group", "group.owner_transferred",
java.util.Map.of("groupId", saved.getId(), "newOwnerId", newOwnerId));
return saved;
}
private ImGroupEntity updateExtAttributesInternal(ImGroupEntity group, Map<String, Object> attributes) {
Map<String, Object> current = parseExtAttributes(group.getExtAttributes());
if (attributes != null) {
attributes.forEach((key, value) -> {
if (key != null && !key.isBlank()) {
current.put(key, value);
}
});
}
group.setExtAttributes(toJson(current));
ImGroupEntity saved = groupRepository.save(group);
webhookDispatchService.dispatch(saved.getAppId(), "group", "group.attributes_updated",
java.util.Map.of("groupId", saved.getId(), "attributes", current));
return saved;
}
private ImGroupEntity removeExtAttributesInternal(ImGroupEntity group, List<String> keys) {
Map<String, Object> current = parseExtAttributes(group.getExtAttributes());
for (String key : keys == null ? List.<String>of() : keys) {
if (key != null && !key.isBlank()) {
current.remove(key);
}
}
group.setExtAttributes(toJson(current));
return groupRepository.save(group);
}
private Map<String, Object> parseExtAttributes(String attributesJson) {
try {
if (attributesJson == null || attributesJson.isBlank()) {
return new HashMap<>();
}
return objectMapper.readValue(attributesJson, new TypeReference<>() {});
} catch (Exception e) {
return new HashMap<>();
}
}
@Transactional
public ImGroupJoinRequestEntity adminAcceptJoinRequest(String appId, String groupId, String requestId) {
ImGroupEntity group = get(groupId);

查看文件

@ -473,10 +473,34 @@ public class MessageService {
toEpochMillis(lastMessage != null ? lastMessage.getCreatedAt() : summary.getLastTime()),
(int) unreadCount,
state != null && state.isMuted(),
state != null && state.isPinned()
state != null && state.isPinned(),
state == null ? null : state.getConversationGroup()
);
}
public List<GroupReadReceiptSummary> groupReadReceipts(String appId, String groupId, List<String> messageIds) {
ImGroupEntity group = groupService.get(groupId);
if (!group.getAppId().equals(appId)) {
throw new BusinessException(403, "无权操作");
}
List<String> members = groupService.memberIds(group);
return messageRepository.findAllById(messageIds == null ? List.of() : messageIds).stream()
.filter(message -> appId.equals(message.getAppId()))
.filter(message -> groupId.equals(message.getToId()))
.filter(message -> message.getChatType() == ImMessageEntity.ChatType.GROUP)
.map(message -> {
int readCount = groupReadCount(appId, groupId, message.getCreatedAt(), message.getFromUserId());
return new GroupReadReceiptSummary(
message.getId(),
groupId,
members.size(),
readCount,
Math.max(members.size() - readCount, 0)
);
})
.toList();
}
private long toEpochMillis(LocalDateTime time) {
return time == null ? 0L : time.toInstant(ZoneOffset.UTC).toEpochMilli();
}
@ -628,4 +652,12 @@ public class MessageService {
String content,
ImMessageEntity.MsgStatus status,
LocalDateTime createdAt) {}
public record GroupReadReceiptSummary(
String messageId,
String groupId,
int memberCount,
int readCount,
int unreadCount
) {}
}