docs(deploy): 添加部署文档并更新SDK API设计规范

- 新增完整的XuqmGroup部署文档,包含服务器配置、Docker Compose部署策略
- 更新SDK API重设计规范至V2.0,统一各端SDK初始化和登录接口
- 添加安全设计规范文档,涵盖密码安全、AppSecret验证等内容
- 新增离线推送架构设计文档,定义厂商推送集成方案
- 重构SDK登录流程,统一使用userId + userSig鉴权模式
- 移除dbName等外部配置参数,实现零感知平台地址配置
- 完善部署架构图和配置示例文件
这个提交包含在:
XuqmGroup 2026-05-02 11:29:49 +08:00
父节点 553427d44e
当前提交 72a03a65d6
共有 4 个文件被更改,包括 12 次插入29 次删除

查看文件

@ -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(