feat(sdk): 添加认证仓库和登录会话管理功能

- 新增 AuthRepository 处理用户认证和加密存储
- 实现 SDK 登录会话管理和自动通知模块
- 添加 IM SDK 登录集成和会话传递
- 更新 API 响应结构支持 userSig 字段
- 添加文件存储服务和上传功能
- 完善文档说明 SDK 架构和集成方式
这个提交包含在:
XuqmGroup 2026-04-27 19:23:11 +08:00
父节点 00f2ad04b7
当前提交 087753075e
共有 6 个文件被更改,包括 161 次插入32 次删除

查看文件

@ -56,18 +56,23 @@ dependencies {
XuqmSDK.initialize( XuqmSDK.initialize(
context = this, context = this,
appId = "ak_your_app_id", appId = "ak_your_app_id",
debug = BuildConfig.DEBUG logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN
) )
``` ```
### 2. 用户登录后初始化 IM ### 2. 用户登录后初始化 IM
```kotlin ```kotlin
// 调用业务登录接口,拿到 userSig 后再登录 IM // 调用业务登录接口,拿到 userSig 后只需要登录一次 SDK
val userSig = api.getUserSig(userId) val userSig = api.getUserSig(userId)
ImSDK.login(userId = userId, userSig = userSig) XuqmSDK.login(
userId = userId,
userSig = userSig,
nickname = profile.nickname,
avatar = profile.avatar,
)
// Push 设备注册在 PushSDK.initialize() 内部自动完成 // 如果工程里集成了 sdk-im,SDK 会自动完成 IM 登录
``` ```
--- ---
@ -79,18 +84,18 @@ ImSDK.login(userId = userId, userSig = userSig)
| 参数 | 类型 | 说明 | | 参数 | 类型 | 说明 |
|------|------|------| |------|------|------|
| `appId` | String | 应用标识(租户平台获取) | | `appId` | String | 应用标识(租户平台获取) |
| `debug` | Boolean | 开启日志 | | `logLevel` | LogLevel | 日志等级 |
### TokenStore ### TokenStore
基于 `DataStore<Preferences>` 持久化存储。 基于 `EncryptedSharedPreferences` 持久化存储。
```kotlin ```kotlin
// 存储 // 存储
XuqmSDK.tokenStore.save("eyJ...") XuqmSDK.tokenStore.saveToken("eyJ...")
// 读取(协程) // 读取(协程)
val token = XuqmSDK.tokenStore.get() val token = XuqmSDK.tokenStore.getToken()
// 清除(登出) // 清除(登出)
XuqmSDK.tokenStore.clear() XuqmSDK.tokenStore.clear()

查看文件

@ -4,6 +4,7 @@ import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import com.google.gson.annotations.SerializedName
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
@ -36,7 +37,8 @@ data class AuthProfile(
data class AuthResult( data class AuthResult(
val demoToken: String, val demoToken: String,
val imToken: String? = null, @SerializedName(value = "imToken", alternate = ["userSig"])
val userSig: String? = null,
val profile: AuthProfile, val profile: AuthProfile,
) )

查看文件

@ -3,12 +3,12 @@ package com.xuqm.sdk.sample.data.repo
import android.content.Context import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys import androidx.security.crypto.MasterKeys
import com.xuqm.sdk.im.ImSDK import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.sample.data.api.DemoApi
import com.xuqm.sdk.sample.data.api.DEMO_APP_ID
import com.xuqm.sdk.sample.data.api.DemoApiFactory
import com.xuqm.sdk.sample.data.api.AuthResult import com.xuqm.sdk.sample.data.api.AuthResult
import com.xuqm.sdk.sample.data.api.ChangePasswordRequest 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
import com.xuqm.sdk.sample.data.api.DemoApiFactory
import com.xuqm.sdk.sample.data.api.LoginRequest import com.xuqm.sdk.sample.data.api.LoginRequest
import com.xuqm.sdk.sample.data.api.RegisterRequest import com.xuqm.sdk.sample.data.api.RegisterRequest
import com.xuqm.sdk.sample.data.api.ResetPasswordRequest import com.xuqm.sdk.sample.data.api.ResetPasswordRequest
@ -42,6 +42,7 @@ class AuthRepository(context: Context) {
.putString("user_id", profile.userId) .putString("user_id", profile.userId)
.putString("nickname", profile.nickname) .putString("nickname", profile.nickname)
.putString("avatar", profile.avatar) .putString("avatar", profile.avatar)
.putString("user_sig", result.userSig)
.apply() .apply()
} }
@ -51,8 +52,15 @@ class AuthRepository(context: Context) {
val res = api.login(LoginRequest(DEMO_APP_ID, userId, password)) val res = api.login(LoginRequest(DEMO_APP_ID, userId, password))
val data = requireNotNull(res.data) { res.message ?: "Login failed" } val data = requireNotNull(res.data) { res.message ?: "Login failed" }
saveSession(data) saveSession(data)
data.imToken?.takeIf { it.isNotBlank() }?.let { ImSDK.loginWithToken(data.profile.userId, it) } val userSig = requireNotNull(data.userSig?.takeIf { it.isNotBlank() }) {
?: ImSDK.login(data.profile.userId, data.profile.nickname, data.profile.avatar) "Login succeeded but IM credential is missing"
}
XuqmSDK.login(
userId = data.profile.userId,
userSig = userSig,
nickname = data.profile.nickname,
avatar = data.profile.avatar,
)
UserData(data.profile.userId, data.profile.nickname, data.profile.avatar, data.profile.gender) UserData(data.profile.userId, data.profile.nickname, data.profile.avatar, data.profile.gender)
} }
} }
@ -63,8 +71,15 @@ class AuthRepository(context: Context) {
val res = api.register(RegisterRequest(DEMO_APP_ID, userId, password, nickname)) val res = api.register(RegisterRequest(DEMO_APP_ID, userId, password, nickname))
val data = requireNotNull(res.data) { res.message ?: "Register failed" } val data = requireNotNull(res.data) { res.message ?: "Register failed" }
saveSession(data) saveSession(data)
data.imToken?.takeIf { it.isNotBlank() }?.let { ImSDK.loginWithToken(data.profile.userId, it) } val userSig = requireNotNull(data.userSig?.takeIf { it.isNotBlank() }) {
?: ImSDK.login(data.profile.userId, data.profile.nickname, data.profile.avatar) "Register succeeded but IM credential is missing"
}
XuqmSDK.login(
userId = data.profile.userId,
userSig = userSig,
nickname = data.profile.nickname,
avatar = data.profile.avatar,
)
UserData(data.profile.userId, data.profile.nickname, data.profile.avatar, data.profile.gender) UserData(data.profile.userId, data.profile.nickname, data.profile.avatar, data.profile.gender)
} }
} }
@ -94,7 +109,7 @@ class AuthRepository(context: Context) {
} }
fun logout() { fun logout() {
ImSDK.disconnect() XuqmSDK.logout()
prefs.edit().clear().apply() prefs.edit().clear().apply()
} }
} }

查看文件

@ -0,0 +1,9 @@
package com.xuqm.sdk
data class XuqmLoginSession(
val appId: String,
val userId: String,
val userSig: String,
val nickname: String? = null,
val avatar: String? = null,
)

查看文件

@ -5,6 +5,8 @@ import com.xuqm.sdk.auth.TokenStore
import com.xuqm.sdk.core.LogLevel import com.xuqm.sdk.core.LogLevel
import com.xuqm.sdk.core.SDKConfig import com.xuqm.sdk.core.SDKConfig
import com.xuqm.sdk.network.ApiClient import com.xuqm.sdk.network.ApiClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object XuqmSDK { object XuqmSDK {
@ -15,6 +17,8 @@ object XuqmSDK {
private set private set
private var initialized = false private var initialized = false
@Volatile
private var loginSession: XuqmLoginSession? = null
fun initialize( fun initialize(
context: Context, context: Context,
@ -32,4 +36,72 @@ object XuqmSDK {
} }
val appId: String get() = config.appId val appId: String get() = config.appId
val currentLoginSession: XuqmLoginSession?
get() = loginSession
suspend fun login(
userId: String,
userSig: String,
nickname: String? = null,
avatar: String? = null,
): XuqmLoginSession = withContext(Dispatchers.IO) {
requireInit()
val session = XuqmLoginSession(
appId = appId,
userId = userId,
userSig = userSig,
nickname = nickname,
avatar = avatar,
)
loginSession = session
tokenStore.saveToken(userSig)
notifyOptionalModules("onSdkLogin", session)
session
}
fun logout() {
val session = loginSession
loginSession = null
tokenStore.clear()
if (session != null) {
notifyOptionalModulesSync("onSdkLogout")
}
}
private fun notifyOptionalModules(methodName: String, session: XuqmLoginSession) {
listOf(
"com.xuqm.sdk.im.ImSDK",
"com.xuqm.sdk.push.PushSDK",
"com.xuqm.sdk.update.UpdateSDK",
).forEach { className ->
runCatching {
val clazz = Class.forName(className)
val instance = clazz.getField("INSTANCE").get(null)
val method = clazz.methods.firstOrNull { method ->
method.name == methodName &&
method.parameterTypes.size == 1 &&
method.parameterTypes[0].name == XuqmLoginSession::class.java.name
} ?: return@runCatching
method.invoke(instance, session)
}
}
}
private fun notifyOptionalModulesSync(methodName: String) {
listOf(
"com.xuqm.sdk.im.ImSDK",
"com.xuqm.sdk.push.PushSDK",
"com.xuqm.sdk.update.UpdateSDK",
).forEach { className ->
runCatching {
val clazz = Class.forName(className)
val instance = clazz.getField("INSTANCE").get(null)
val method = clazz.methods.firstOrNull { method ->
method.name == methodName && method.parameterTypes.isEmpty()
} ?: return@runCatching
method.invoke(instance)
}
}
}
} }

查看文件

@ -1,11 +1,11 @@
package com.xuqm.sdk.im package com.xuqm.sdk.im
import com.xuqm.sdk.XuqmLoginSession
import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.core.WS_URL import com.xuqm.sdk.core.WS_URL
import com.xuqm.sdk.im.api.AddMemberRequest import com.xuqm.sdk.im.api.AddMemberRequest
import com.xuqm.sdk.im.api.CreateGroupRequest import com.xuqm.sdk.im.api.CreateGroupRequest
import com.xuqm.sdk.im.api.ImApi import com.xuqm.sdk.im.api.ImApi
import com.xuqm.sdk.im.api.LoginRequest
import com.xuqm.sdk.im.api.SetMutedRequest import com.xuqm.sdk.im.api.SetMutedRequest
import com.xuqm.sdk.im.api.SetPinnedRequest import com.xuqm.sdk.im.api.SetPinnedRequest
import com.xuqm.sdk.im.api.UpdateGroupRequest import com.xuqm.sdk.im.api.UpdateGroupRequest
@ -25,22 +25,32 @@ object ImSDK {
var currentUserId: String = "" var currentUserId: String = ""
private set private set
suspend fun login(userId: String, nickname: String? = null, avatar: String? = null) = init {
withContext(Dispatchers.IO) { XuqmSDK.currentLoginSession?.let { onSdkLogin(it) }
XuqmSDK.requireInit()
val res = api.login(LoginRequest(XuqmSDK.appId, userId, nickname, avatar))
val token = requireNotNull(res.data?.token) { "IM login failed: ${res.message}" }
currentUserId = userId
connectWithToken(token)
} }
suspend fun loginWithToken(userId: String, token: String) = suspend fun login(
userId: String,
userSig: String,
nickname: String? = null,
avatar: String? = null,
) = withContext(Dispatchers.IO) {
XuqmSDK.requireInit()
currentUserId = userId
connectWithToken(userSig)
}
suspend fun loginWithUserSig(userId: String, userSig: String) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
XuqmSDK.requireInit() XuqmSDK.requireInit()
currentUserId = userId currentUserId = userId
connectWithToken(token) connectWithToken(userSig)
} }
@Deprecated("Use loginWithUserSig(userId, userSig) instead.")
suspend fun loginWithToken(userId: String, token: String) =
loginWithUserSig(userId, token)
fun sendMessage(toId: String, chatType: String, msgType: String, content: String) { fun sendMessage(toId: String, chatType: String, msgType: String, content: String) {
client?.sendMessage(toId, chatType, msgType, content) client?.sendMessage(toId, chatType, msgType, content)
} }
@ -105,10 +115,17 @@ object ImSDK {
fun removeListener(listener: ImEventListener) = client?.removeListener(listener) fun removeListener(listener: ImEventListener) = client?.removeListener(listener)
fun disconnect() { fun disconnect() {
client?.disconnect() disconnectInternal(clearTokenStore = true)
client = null }
currentUserId = ""
XuqmSDK.tokenStore.clear() fun onSdkLogin(session: XuqmLoginSession) {
XuqmSDK.requireInit()
currentUserId = session.userId
connectWithToken(session.userSig)
}
fun onSdkLogout() {
disconnectInternal(clearTokenStore = false)
} }
private fun connectWithToken(token: String) { private fun connectWithToken(token: String) {
@ -117,4 +134,13 @@ object ImSDK {
client = ImClient(WS_URL, token, XuqmSDK.appId) client = ImClient(WS_URL, token, XuqmSDK.appId)
client?.connect() client?.connect()
} }
private fun disconnectInternal(clearTokenStore: Boolean) {
client?.disconnect()
client = null
currentUserId = ""
if (clearTokenStore) {
XuqmSDK.tokenStore.clear()
}
}
} }