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

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

查看文件

@ -540,6 +540,30 @@ public final class ImSDK {
)
}
public func transferGroupOwner(groupId: String, newOwnerId: String) async throws -> ImGroup {
try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/owner",
method: "POST",
body: ImTransferOwnerRequest(newOwnerId: newOwnerId)
)
}
public func updateGroupAttributes(groupId: String, attributes: [String: ImAttributeValue]) async throws -> ImGroup {
try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/attributes",
method: "PUT",
body: attributes
)
}
public func removeGroupAttributes(groupId: String, keys: [String]) async throws -> ImGroup {
try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)/attributes/delete",
method: "POST",
body: ImAttributeKeysRequest(keys: keys)
)
}
public func dismissGroup(groupId: String) async throws {
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/groups/\(groupId)",
@ -604,6 +628,15 @@ public final class ImSDK {
)
}
public func removeAllFriends() async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/friends",
method: "DELETE",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func removeFriend(friendId: String) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
@ -613,6 +646,35 @@ public final class ImSDK {
)
}
public func setFriendGroup(friendId: String, groupName: String? = nil) async throws {
let config = XuqmSDK.shared.requireConfig()
var items = [URLQueryItem(name: "appId", value: config.appId)]
if let groupName {
items.append(URLQueryItem(name: "groupName", value: groupName))
}
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/friends/\(friendId)/group",
method: "PUT",
queryItems: items
)
}
public func listFriendGroups() async throws -> [String] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/friends/groups",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func listFriendsByGroup(_ groupName: String) async throws -> [String] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/friends/groups/\(groupName)",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func listFriendRequests(direction: String = "incoming") async throws -> [FriendRequest] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
@ -688,6 +750,17 @@ public final class ImSDK {
)
}
public func checkBlacklist(targetUserId: String) async throws -> BlacklistCheckResult {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/blacklist/check",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "targetUserId", value: targetUserId),
]
)
}
public func getProfile(userId: String) async throws -> UserProfile {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
@ -760,6 +833,51 @@ public final class ImSDK {
)
}
public func setConversationHidden(targetId: String, chatType: ChatType, hidden: Bool) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/conversations/\(targetId)/hidden",
method: "PUT",
queryItems: [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "chatType", value: chatType.rawValue),
URLQueryItem(name: "hidden", value: String(hidden)),
]
)
}
public func setConversationGroup(targetId: String, chatType: ChatType, groupName: String? = nil) async throws {
let config = XuqmSDK.shared.requireConfig()
var items = [
URLQueryItem(name: "appId", value: config.appId),
URLQueryItem(name: "chatType", value: chatType.rawValue),
]
if let groupName {
items.append(URLQueryItem(name: "groupName", value: groupName))
}
let _: EmptyResponse = try await ApiClient.shared.request(
path: "/api/im/conversations/\(targetId)/group",
method: "PUT",
queryItems: items
)
}
public func listConversationGroups() async throws -> [String] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/conversation-groups",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func listConversationGroupItems(_ groupName: String) async throws -> [ConversationGroupItem] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/conversation-groups/\(groupName)",
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
)
}
public func setDraft(targetId: String, chatType: ChatType, draft: String) async throws {
let config = XuqmSDK.shared.requireConfig()
let _: EmptyResponse = try await ApiClient.shared.request(
@ -790,6 +908,16 @@ public final class ImSDK {
return conversations.reduce(0) { $0 + $1.unreadCount }
}
public func adminGroupReadReceipts(groupId: String, messageIds: [String]) async throws -> [GroupReadReceiptSummary] {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/admin/groups/\(groupId)/read-receipts",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
body: ImGroupReadReceiptRequest(messageIds: messageIds)
)
}
public func disconnect() {
client?.disconnect()
client = nil

查看文件

@ -91,12 +91,15 @@ public struct ImGroup: Codable, Sendable {
public let memberIds: String
public let adminIds: String
public let announcement: String?
public let memberInfo: String?
public let extAttributes: String?
public let createdAt: Int64
}
public struct ConversationData: Codable, Sendable {
public let targetId: String
public let chatType: ChatType
public let conversationGroup: String?
public let lastMsgContent: String?
public let lastMsgType: String?
public let lastMsgTime: Int64
@ -105,6 +108,12 @@ public struct ConversationData: Codable, Sendable {
public let isPinned: Bool
}
public struct ConversationGroupItem: Codable, Sendable {
public let targetId: String
public let chatType: ChatType
public let groupName: String
}
public struct FriendRequest: Codable, Sendable {
public let id: String
public let appId: String
@ -135,6 +144,21 @@ public struct BlacklistEntry: Codable, Sendable {
public let createdAt: Int64
}
public struct BlacklistCheckResult: Codable, Sendable {
public let targetUserId: String
public let blockedByMe: Bool
public let blockedByTarget: Bool
public let eitherBlocked: Bool
}
public struct GroupReadReceiptSummary: Codable, Sendable {
public let messageId: String
public let groupId: String
public let memberCount: Int
public let readCount: Int
public let unreadCount: Int
}
public struct UserProfile: Codable, Sendable {
public let id: String?
public let appId: String?
@ -187,3 +211,78 @@ public struct ImMuteMemberRequest: Encodable, Sendable {
public let userId: String
public let minutes: Int64
}
public struct ImTransferOwnerRequest: Encodable, Sendable {
public let newOwnerId: String
}
public struct ImAttributeKeysRequest: Encodable, Sendable {
public let keys: [String]
}
public struct ImGroupReadReceiptRequest: Encodable, Sendable {
public let messageIds: [String]
}
public enum ImAttributeValue: Codable, Sendable {
case string(String)
case int(Int)
case double(Double)
case bool(Bool)
case null
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode(Int.self) {
self = .int(value)
} else if let value = try? container.decode(Double.self) {
self = .double(value)
} else {
self = .string(try container.decode(String.self))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let value):
try container.encode(value)
case .int(let value):
try container.encode(value)
case .double(let value):
try container.encode(value)
case .bool(let value):
try container.encode(value)
case .null:
try container.encodeNil()
}
}
}
extension ImAttributeValue: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .string(value)
}
}
extension ImAttributeValue: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self = .int(value)
}
}
extension ImAttributeValue: ExpressibleByFloatLiteral {
public init(floatLiteral value: Double) {
self = .double(value)
}
}
extension ImAttributeValue: ExpressibleByBooleanLiteral {
public init(booleanLiteral value: Bool) {
self = .bool(value)
}
}