docs(deploy): 添加部署文档并更新SDK API设计规范
- 新增完整的XuqmGroup部署文档,包含服务器配置、Docker Compose部署策略 - 更新SDK API重设计规范至V2.0,统一各端SDK初始化和登录接口 - 添加安全设计规范文档,涵盖密码安全、AppSecret验证等内容 - 新增离线推送架构设计文档,定义厂商推送集成方案 - 重构SDK登录流程,统一使用userId + userSig鉴权模式 - 移除dbName等外部配置参数,实现零感知平台地址配置 - 完善部署架构图和配置示例文件
这个提交包含在:
父节点
553427d44e
当前提交
72a03a65d6
@ -148,7 +148,7 @@ val service = RetrofitFactory.create(MyApiService::class.java)
|
||||
- 会话置顶/免打扰/已读/草稿/删除同步服务端
|
||||
- IM 连接状态提示
|
||||
- SDK 登录态恢复后自动重连
|
||||
- IM token 自动续签,过期前会静默刷新并重连
|
||||
- IM 登录态更新后自动重连,不在 SDK 侧做静默续签
|
||||
- 群聊支持 `@userId` 提及,并写入 `mentionedUserIds`
|
||||
- 关系链支持好友申请、接受/拒绝、黑名单
|
||||
- 支持图片 / 视频 / 音频 / 文件消息,文件通过独立文件服务上传后再发 IM
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
| 字段 | 内容 |
|
||||
|------|------|
|
||||
| **测试目的** | 验证 UserSig 鉴权模式下的登录与登出 |
|
||||
| **测试步骤** | 1. 调用 `XuqmSDK.login(userId, userSig, nickname, avatar, userSigExpiresAt)` <br> 2. 观察 `ImSDK.onSdkLogin` 是否自动触发 WebSocket 连接 <br> 3. 监听 `ImEventListener.onConnected()` <br> 4. 调用 `XuqmSDK.logout()` <br> 5. 确认 `ImSDK.onSdkLogout` 断开 WebSocket 并清空 Token |
|
||||
| **测试步骤** | 1. 调用 `XuqmSDK.login(userId, userSig)` <br> 2. 观察 `ImSDK.onSdkLogin` 是否自动触发 WebSocket 连接 <br> 3. 监听 `ImEventListener.onConnected()` <br> 4. 调用 `XuqmSDK.logout()` <br> 5. 确认 `ImSDK.onSdkLogout` 断开 WebSocket 并清空 Token |
|
||||
| **预期结果** | 1. 登录返回 `XuqmLoginSession` <br> 2. WebSocket 建立 101 连接并 STOMP CONNECTED <br> 3. `onConnected()` 回调触发 <br> 4. 登出后 `connectionState` 变为 `Disconnected` <br> 5. `TokenStore` 被清空 |
|
||||
| **实际结果** | 通过 |
|
||||
| **通过状态** | ✅ |
|
||||
@ -108,13 +108,13 @@
|
||||
|
||||
---
|
||||
|
||||
### TC-08 UserSig 续签测试(新增)
|
||||
### TC-08 UserSig 登录态更新测试(新增)
|
||||
|
||||
| 字段 | 内容 |
|
||||
|------|------|
|
||||
| **测试目的** | 验证 UserSig 即将过期时的静默续签回调机制 |
|
||||
| **测试步骤** | 1. 登录时传入 `userSigExpiresAt`(如当前时间 + 6 分钟) <br> 2. 设置 `XuqmSDK.setUserSigRefreshListener { ... }` <br> 3. 等待 1 分钟后观察 `UserSigRefresher` 检查逻辑 <br> 4. 在回调中获取新 UserSig 并重新调用 `XuqmSDK.login()` <br> 5. 验证旧定时器被停止,新定时器启动 |
|
||||
| **预期结果** | 1. `UserSigRefresher.start(expiryTimeMs)` 启动 <br> 2. 到期前 5 分钟触发 `onUserSigRefreshRequired()` <br> 3. 回调在主线程执行 <br> 4. 重新登录后 WebSocket 使用新 Token 连接 <br> 5. 无内存泄漏,定时器正确替换 |
|
||||
| **测试目的** | 验证业务侧重新签发登录态后,SDK 能覆盖旧会话并重连 |
|
||||
| **测试步骤** | 1. 登录后保持 WebSocket 连接 <br> 2. 业务服务端重新签发新的 UserSig <br> 3. 重新调用 `XuqmSDK.login()` <br> 4. 观察 `ImSDK.onSdkLogin` 与 `ImEventListener.onConnected()` <br> 5. 确认旧会话被替换 |
|
||||
| **预期结果** | 1. 新登录态生效 <br> 2. WebSocket 使用新登录态重连 <br> 3. 不存在 SDK 侧定时续签逻辑 <br> 4. 旧会话被覆盖 <br> 5. 无内存泄漏 |
|
||||
| **实际结果** | 待测试 |
|
||||
| **通过状态** | ⬜ |
|
||||
|
||||
@ -143,5 +143,5 @@
|
||||
| TC-05 | 会话列表/置顶/静音测试 | ⬜ 待测试 |
|
||||
| TC-06 | Push 设备注册测试 | ⬜ 待测试 |
|
||||
| TC-07 | 版本更新检查测试 | ⬜ 待测试 |
|
||||
| TC-08 | UserSig 续签测试 | ⬜ 待测试 |
|
||||
| TC-08 | UserSig 登录态更新测试 | ⬜ 待测试 |
|
||||
| TC-09 | 多厂商 Push 检测测试 | ⬜ 待测试 |
|
||||
|
||||
@ -6,7 +6,6 @@ import okhttp3.logging.HttpLoggingInterceptor
|
||||
import com.xuqm.sdk.sample.BuildConfig
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
@ -38,19 +37,10 @@ data class AuthProfile(
|
||||
|
||||
data class AuthResult(
|
||||
val demoToken: String,
|
||||
val demoTokenExpiresAt: Long? = null,
|
||||
@SerializedName(value = "imToken", alternate = ["userSig"])
|
||||
val userSig: String? = null,
|
||||
val imTokenExpiresAt: Long? = null,
|
||||
val imToken: String,
|
||||
val profile: AuthProfile,
|
||||
)
|
||||
|
||||
data class ImRefreshResult(
|
||||
@SerializedName(value = "imToken", alternate = ["userSig"])
|
||||
val userSig: String,
|
||||
val imTokenExpiresAt: Long? = null,
|
||||
)
|
||||
|
||||
data class UserData(
|
||||
val userId: String,
|
||||
val nickname: String,
|
||||
@ -74,9 +64,6 @@ interface DemoApi {
|
||||
@POST("api/demo/auth/register")
|
||||
suspend fun register(@Body request: RegisterRequest): DemoResponse<AuthResult>
|
||||
|
||||
@POST("api/demo/auth/refresh-im")
|
||||
suspend fun refreshImToken(@Query("appId") appId: String = DEMO_APP_ID): DemoResponse<ImRefreshResult>
|
||||
|
||||
@POST("api/demo/auth/reset-password")
|
||||
suspend fun resetPassword(@Body request: ResetPasswordRequest): DemoResponse<Unit>
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import com.xuqm.sdk.XuqmSDK
|
||||
import com.xuqm.sdk.sample.data.api.AuthResult
|
||||
import com.xuqm.sdk.sample.data.api.ImRefreshResult
|
||||
import com.xuqm.sdk.sample.data.api.ChangePasswordRequest
|
||||
import com.xuqm.sdk.sample.data.api.DEMO_APP_ID
|
||||
import com.xuqm.sdk.sample.data.api.DemoApi
|
||||
@ -55,22 +54,19 @@ class AuthRepository(context: Context) {
|
||||
val demoToken = getDemoToken().orEmpty()
|
||||
val userId = getCurrentUserId().orEmpty()
|
||||
val userSig = getCurrentUserSig().orEmpty()
|
||||
val demoTokenExpiresAt = prefs.getLong("demo_token_expires_at", 0L)
|
||||
return demoToken.isNotBlank() &&
|
||||
userId.isNotBlank() &&
|
||||
userSig.isNotBlank() &&
|
||||
demoTokenExpiresAt > System.currentTimeMillis()
|
||||
userSig.isNotBlank()
|
||||
}
|
||||
|
||||
private fun saveSession(result: AuthResult) {
|
||||
val profile = result.profile
|
||||
prefs.edit()
|
||||
.putString("demo_token", result.demoToken)
|
||||
.putLong("demo_token_expires_at", result.demoTokenExpiresAt ?: 0L)
|
||||
.putString("user_id", profile.userId)
|
||||
.putString("nickname", profile.nickname)
|
||||
.putString("avatar", profile.avatar)
|
||||
.putString("user_sig", result.userSig)
|
||||
.putString("user_sig", result.imToken)
|
||||
.apply()
|
||||
}
|
||||
|
||||
@ -80,7 +76,7 @@ class AuthRepository(context: Context) {
|
||||
val res = api.login(LoginRequest(DEMO_APP_ID, userId, password))
|
||||
val data = requireNotNull(res.data) { res.message ?: "Login failed" }
|
||||
saveSession(data)
|
||||
val userSig = requireNotNull(data.userSig?.takeIf { it.isNotBlank() }) {
|
||||
val userSig = requireNotNull(data.imToken.takeIf { it.isNotBlank() }) {
|
||||
"Login succeeded but IM credential is missing"
|
||||
}
|
||||
XuqmSDK.login(
|
||||
@ -98,7 +94,7 @@ class AuthRepository(context: Context) {
|
||||
val res = api.register(RegisterRequest(DEMO_APP_ID, userId, password, nickname))
|
||||
val data = requireNotNull(res.data) { res.message ?: "Register failed" }
|
||||
saveSession(data)
|
||||
val userSig = requireNotNull(data.userSig?.takeIf { it.isNotBlank() }) {
|
||||
val userSig = requireNotNull(data.imToken.takeIf { it.isNotBlank() }) {
|
||||
"Register succeeded but IM credential is missing"
|
||||
}
|
||||
XuqmSDK.login(
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户