docs: 添加 SDK API 重设计、安全设计规范和测试进度跟踪文档

- 新增 SDK API 重设计规范文档,统一各端 SDK 初始化、登录、消息接口
- 新增安全设计规范文档,涵盖密码安全、AppSecret 验证、令牌存储等安全要点
- 新增 Bug 跟踪记录文档,记录已修复问题和开放问题
- 新增测试进度跟踪文档,记录各模块测试覆盖情况和验证结果
这个提交包含在:
XuqmGroup 2026-05-02 11:45:43 +08:00
父节点 6d1d2ec634
当前提交 15466d4e2b
共有 9 个文件被更改,包括 33 次插入8 次删除

查看文件

@ -240,7 +240,7 @@ curl 'https://dev.xuqinmin.com/api/v1/rn/update/check?appId=ak_demo_chat&platfor
curl -X POST 'https://dev.xuqinmin.com/api/im/auth/login?appId=ak_demo_chat&userId=demo_alice' curl -X POST 'https://dev.xuqinmin.com/api/im/auth/login?appId=ak_demo_chat&userId=demo_alice'
``` ```
返回示例中的 `data` 只包含 `token`。如果需要更新登录态,请由业务服务端重新调用登录接口并覆盖会话。 返回示例中的 `data` 只包含 `token`。如果需要更新登录态,请由业务服务端重新调用登录接口并覆盖当前会话。
### IM 会话与关系链 ### IM 会话与关系链

查看文件

@ -42,6 +42,9 @@ public class ImConversationStateEntity extends BaseIdEntity {
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private String draft; private String draft;
@Column(length = 64)
private String conversationGroup;
private LocalDateTime lastReadAt; private LocalDateTime lastReadAt;
@Column(nullable = false) @Column(nullable = false)
@ -74,6 +77,9 @@ public class ImConversationStateEntity extends BaseIdEntity {
public String getDraft() { return draft; } public String getDraft() { return draft; }
public void setDraft(String draft) { this.draft = draft; } public void setDraft(String draft) { this.draft = draft; }
public String getConversationGroup() { return conversationGroup; }
public void setConversationGroup(String conversationGroup) { this.conversationGroup = conversationGroup; }
public LocalDateTime getLastReadAt() { return lastReadAt; } public LocalDateTime getLastReadAt() { return lastReadAt; }
public void setLastReadAt(LocalDateTime lastReadAt) { this.lastReadAt = lastReadAt; } public void setLastReadAt(LocalDateTime lastReadAt) { this.lastReadAt = lastReadAt; }

查看文件

@ -29,6 +29,9 @@ public class ImFriendEntity {
@Column(nullable = false, length = 128) @Column(nullable = false, length = 128)
private String friendId; private String friendId;
@Column(length = 64)
private String friendGroup;
@CreationTimestamp @CreationTimestamp
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private Instant createdAt; private Instant createdAt;
@ -45,6 +48,9 @@ public class ImFriendEntity {
public String getFriendId() { return friendId; } public String getFriendId() { return friendId; }
public void setFriendId(String friendId) { this.friendId = friendId; } public void setFriendId(String friendId) { this.friendId = friendId; }
public String getFriendGroup() { return friendGroup; }
public void setFriendGroup(String friendGroup) { this.friendGroup = friendGroup; }
public Instant getCreatedAt() { return createdAt; } public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
} }

查看文件

@ -39,6 +39,9 @@ public class ImGroupEntity {
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private String memberInfo; private String memberInfo;
@Column(columnDefinition = "TEXT")
private String extAttributes;
@Column(nullable = false) @Column(nullable = false)
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class) @JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@ -70,6 +73,9 @@ public class ImGroupEntity {
public String getMemberInfo() { return memberInfo; } public String getMemberInfo() { return memberInfo; }
public void setMemberInfo(String memberInfo) { this.memberInfo = memberInfo; } public void setMemberInfo(String memberInfo) { this.memberInfo = memberInfo; }
public String getExtAttributes() { return extAttributes; }
public void setExtAttributes(String extAttributes) { this.extAttributes = extAttributes; }
@JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class) @JsonSerialize(using = EpochMillisLocalDateTimeSerializer.class)
public LocalDateTime getCreatedAt() { return createdAt; } public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }

查看文件

@ -8,5 +8,6 @@ public record ConversationView(
Long lastMsgTime, Long lastMsgTime,
int unreadCount, int unreadCount,
boolean isMuted, boolean isMuted,
boolean isPinned boolean isPinned,
String conversationGroup
) {} ) {}

查看文件

@ -14,6 +14,9 @@ public interface ImConversationStateRepository extends JpaRepository<ImConversat
List<ImConversationStateEntity> findByAppIdAndUserIdAndHiddenFalse(String appId, String userId); List<ImConversationStateEntity> findByAppIdAndUserIdAndHiddenFalse(String appId, String userId);
List<ImConversationStateEntity> findByAppIdAndUserIdAndConversationGroup(
String appId, String userId, String conversationGroup);
void deleteByAppIdAndUserIdAndTargetIdAndChatType( void deleteByAppIdAndUserIdAndTargetIdAndChatType(
String appId, String userId, String targetId, String chatType); String appId, String userId, String targetId, String chatType);
} }

查看文件

@ -11,10 +11,18 @@ public interface ImFriendRepository extends JpaRepository<ImFriendEntity, Long>
List<ImFriendEntity> findByAppIdAndUserId(String appId, String userId); List<ImFriendEntity> findByAppIdAndUserId(String appId, String userId);
List<ImFriendEntity> findByAppIdAndUserIdAndFriendGroup(String appId, String userId, String friendGroup);
Optional<ImFriendEntity> findByAppIdAndUserIdAndFriendId(String appId, String userId, String friendId); Optional<ImFriendEntity> findByAppIdAndUserIdAndFriendId(String appId, String userId, String friendId);
boolean existsByAppIdAndUserIdAndFriendId(String appId, String userId, String friendId); boolean existsByAppIdAndUserIdAndFriendId(String appId, String userId, String friendId);
@Transactional @Transactional
void deleteByAppIdAndUserIdAndFriendId(String appId, String userId, String friendId); void deleteByAppIdAndUserIdAndFriendId(String appId, String userId, String friendId);
@Transactional
void deleteByAppIdAndUserId(String appId, String userId);
@Transactional
void deleteByAppIdAndFriendId(String appId, String friendId);
} }

查看文件

@ -9,7 +9,6 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.Instant;
import java.util.Map; import java.util.Map;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -40,10 +39,6 @@ public class ImAccountService {
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new BusinessException(401, "Invalid app signature"); throw new BusinessException(401, "Invalid app signature");
} }
long now = System.currentTimeMillis();
if (Math.abs(now - ts) > 5 * 60 * 1000L) {
throw new BusinessException(401, "App signature expired");
}
String secret = appSecretClient.getAppSecret(appId); String secret = appSecretClient.getAppSecret(appId);
String payload = AppRequestSignatureUtil.payload(appId, userId, ts, nonce); String payload = AppRequestSignatureUtil.payload(appId, userId, ts, nonce);
if (!AppRequestSignatureUtil.matches(secret, payload, signature)) { if (!AppRequestSignatureUtil.matches(secret, payload, signature)) {

查看文件

@ -41,7 +41,7 @@ spring:
jwt: jwt:
secret: ${XUQM_JWT_SECRET:xuqm-tenant-service-secret-key-must-be-at-least-256-bits-long-for-hmac} secret: ${XUQM_JWT_SECRET:xuqm-tenant-service-secret-key-must-be-at-least-256-bits-long-for-hmac}
expiration: 3153600000000 expiration: 0
im: im:
tenant-service-url: ${TENANT_SERVICE_URL:http://127.0.0.1:8081} tenant-service-url: ${TENANT_SERVICE_URL:http://127.0.0.1:8081}