feat(sdk): 实现动态服务端点配置和环境切换功能

- 移除硬编码的基础URL常量,改为可配置的服务端点
- 添加ServiceEndpointRegistry用于统一管理所有服务端点地址
- 实现ApiClient支持多基础URL的Retrofit实例缓存机制
- 新增XuqmSDK.configureServiceEndpoints等方法用于运行时切换环境
- 为sample-app添加SampleEnvironmentConfig支持本地联调环境切换
- 创建独立的IM、Push、Update SDK模块并集成服务端点配置
- 更新文档说明如何进行联调环境切换操作
这个提交包含在:
XuqmGroup 2026-04-27 19:30:06 +08:00
父节点 087753075e
当前提交 5a0378d579
共有 12 个文件被更改,包括 146 次插入22 次删除

查看文件

@ -75,6 +75,23 @@ XuqmSDK.login(
// 如果工程里集成了 sdk-im,SDK 会自动完成 IM 登录 // 如果工程里集成了 sdk-im,SDK 会自动完成 IM 登录
``` ```
### 3. 切换联调环境
默认使用外网域名。若要本地联调,可在 `Application.onCreate()` 里切换:
```kotlin
XuqmSDK.useLocalServiceEndpoints("10.0.2.2") // Android Emulator
// 物理设备可改成你的电脑局域网 IP
```
如果是 sample-app,也可以直接调用
```kotlin
SampleEnvironmentConfig.useLocalhost("10.0.2.2")
```
切换后,HTTP API 会立即走新端点;如果 IM 已登录,SDK 会自动重新连接。
--- ---
## sdk-core ## sdk-core

查看文件

@ -3,12 +3,14 @@ package com.xuqm.sdk.sample
import android.app.Application import android.app.Application
import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.core.LogLevel import com.xuqm.sdk.core.LogLevel
import com.xuqm.sdk.sample.config.SampleEnvironmentConfig
import com.xuqm.sdk.sample.di.AppDependencies import com.xuqm.sdk.sample.di.AppDependencies
class XuqmSampleApp : Application() { class XuqmSampleApp : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
SampleEnvironmentConfig.useExternal()
AppDependencies.init(this) AppDependencies.init(this)
XuqmSDK.initialize( XuqmSDK.initialize(
context = this, context = this,

查看文件

@ -0,0 +1,34 @@
package com.xuqm.sdk.sample.config
import com.xuqm.sdk.XuqmSDK
data class SampleEnvironment(
val demoBaseUrl: String,
val serviceHost: String? = null,
)
object SampleEnvironmentConfig {
@Volatile
var current: SampleEnvironment = external()
private set
fun external(): SampleEnvironment = SampleEnvironment(
demoBaseUrl = "https://dev.xuqinmin.com/",
serviceHost = null,
)
fun localhost(host: String): SampleEnvironment = SampleEnvironment(
demoBaseUrl = "http://$host:8081/",
serviceHost = host,
)
fun useExternal() {
current = external()
XuqmSDK.useExternalServiceEndpoints()
}
fun useLocalhost(host: String) {
current = localhost(host)
XuqmSDK.useLocalServiceEndpoints(host)
}
}

查看文件

@ -11,7 +11,6 @@ import retrofit2.http.POST
import retrofit2.http.PUT import retrofit2.http.PUT
import retrofit2.http.Query import retrofit2.http.Query
const val DEMO_BASE_URL = "https://dev.xuqinmin.com/"
const val DEMO_APP_ID = "ak_demo_chat" const val DEMO_APP_ID = "ak_demo_chat"
data class DemoResponse<T>( data class DemoResponse<T>(
@ -85,7 +84,10 @@ interface DemoApi {
} }
object DemoApiFactory { object DemoApiFactory {
fun create(tokenProvider: () -> String?): DemoApi { fun create(
baseUrl: String,
tokenProvider: () -> String?,
): DemoApi {
val authInterceptor = Interceptor { chain -> val authInterceptor = Interceptor { chain ->
val request = chain.request().newBuilder().apply { val request = chain.request().newBuilder().apply {
tokenProvider()?.takeIf { it.isNotBlank() }?.let { header("Authorization", "Bearer $it") } tokenProvider()?.takeIf { it.isNotBlank() }?.let { header("Authorization", "Bearer $it") }
@ -98,7 +100,7 @@ object DemoApiFactory {
.build() .build()
return Retrofit.Builder() return Retrofit.Builder()
.baseUrl(DEMO_BASE_URL) .baseUrl(baseUrl)
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build() .build()

查看文件

@ -14,6 +14,7 @@ import com.xuqm.sdk.sample.data.api.RegisterRequest
import com.xuqm.sdk.sample.data.api.ResetPasswordRequest import com.xuqm.sdk.sample.data.api.ResetPasswordRequest
import com.xuqm.sdk.sample.data.api.UpdateProfileRequest import com.xuqm.sdk.sample.data.api.UpdateProfileRequest
import com.xuqm.sdk.sample.data.api.UserData import com.xuqm.sdk.sample.data.api.UserData
import com.xuqm.sdk.sample.config.SampleEnvironmentConfig
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -27,7 +28,11 @@ class AuthRepository(context: Context) {
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
) )
private val api: DemoApi = DemoApiFactory.create(::getDemoToken) private val api: DemoApi
get() = DemoApiFactory.create(
baseUrl = SampleEnvironmentConfig.current.demoBaseUrl,
tokenProvider = ::getDemoToken,
)
fun getDemoToken(): String? = prefs.getString("demo_token", null) fun getDemoToken(): String? = prefs.getString("demo_token", null)
fun getCurrentUserId(): String? = prefs.getString("user_id", null) fun getCurrentUserId(): String? = prefs.getString("user_id", null)

查看文件

@ -3,6 +3,8 @@ package com.xuqm.sdk
import android.content.Context import android.content.Context
import com.xuqm.sdk.auth.TokenStore import com.xuqm.sdk.auth.TokenStore
import com.xuqm.sdk.core.LogLevel import com.xuqm.sdk.core.LogLevel
import com.xuqm.sdk.core.ServiceEndpointRegistry
import com.xuqm.sdk.core.ServiceEndpoints
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.Dispatchers
@ -31,6 +33,20 @@ object XuqmSDK {
initialized = true initialized = true
} }
fun configureServiceEndpoints(endpoints: ServiceEndpoints) {
ServiceEndpointRegistry.configure(endpoints)
loginSession?.let { notifyOptionalModules("onSdkLogin", it) }
}
fun useExternalServiceEndpoints() {
configureServiceEndpoints(ServiceEndpoints())
}
fun useLocalServiceEndpoints(host: String) {
ServiceEndpointRegistry.useLocalhost(host)
loginSession?.let { notifyOptionalModules("onSdkLogin", it) }
}
fun requireInit() { fun requireInit() {
check(initialized) { "XuqmSDK not initialized. Call XuqmSDK.initialize() first." } check(initialized) { "XuqmSDK not initialized. Call XuqmSDK.initialize() first." }
} }

查看文件

@ -1,8 +1,5 @@
package com.xuqm.sdk.core package com.xuqm.sdk.core
const val BASE_URL = "https://dev.xuqinmin.com/"
const val WS_URL = "wss://dev.xuqinmin.com/ws/im"
data class SDKConfig( data class SDKConfig(
val appId: String, val appId: String,
val logLevel: LogLevel = LogLevel.WARN, val logLevel: LogLevel = LogLevel.WARN,

查看文件

@ -0,0 +1,41 @@
package com.xuqm.sdk.core
data class ServiceEndpoints(
val controlBaseUrl: String = "https://dev.xuqinmin.com/",
val imApiBaseUrl: String = "https://im.dev.xuqinmin.com/",
val imWsUrl: String = "wss://im.dev.xuqinmin.com/ws/im",
val pushBaseUrl: String = "https://dev.xuqinmin.com/",
val updateBaseUrl: String = "https://update.dev.xuqinmin.com/",
)
object ServiceEndpointRegistry {
@Volatile
var current: ServiceEndpoints = ServiceEndpoints()
private set
val controlBaseUrl: String get() = current.controlBaseUrl
val imApiBaseUrl: String get() = current.imApiBaseUrl
val imWsUrl: String get() = current.imWsUrl
val pushBaseUrl: String get() = current.pushBaseUrl
val updateBaseUrl: String get() = current.updateBaseUrl
fun configure(endpoints: ServiceEndpoints) {
current = endpoints
}
fun useLocalhost(host: String) {
val normalizedHost = host.removeSuffix("/")
current = ServiceEndpoints(
controlBaseUrl = "http://$normalizedHost:8081/",
imApiBaseUrl = "http://$normalizedHost:8082/",
imWsUrl = "ws://$normalizedHost:8082/ws/im",
pushBaseUrl = "http://$normalizedHost:8081/",
updateBaseUrl = "http://$normalizedHost:8084/",
)
}
fun useExternal() {
current = ServiceEndpoints()
}
}

查看文件

@ -1,7 +1,7 @@
package com.xuqm.sdk.network package com.xuqm.sdk.network
import com.xuqm.sdk.auth.TokenStore import com.xuqm.sdk.auth.TokenStore
import com.xuqm.sdk.core.BASE_URL import com.xuqm.sdk.core.ServiceEndpointRegistry
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 okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -14,8 +14,8 @@ import java.util.concurrent.TimeUnit
object ApiClient { object ApiClient {
private var tokenStore: TokenStore? = null private var tokenStore: TokenStore? = null
lateinit var retrofit: Retrofit private var okHttpClient: OkHttpClient? = null
private set private val retrofitCache = mutableMapOf<String, Retrofit>()
fun init(cfg: SDKConfig, store: TokenStore) { fun init(cfg: SDKConfig, store: TokenStore) {
tokenStore = store tokenStore = store
@ -25,7 +25,7 @@ object ApiClient {
else HttpLoggingInterceptor.Level.NONE else HttpLoggingInterceptor.Level.NONE
} }
val okhttp = OkHttpClient.Builder() okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(logging) .addInterceptor(logging)
@ -40,12 +40,20 @@ object ApiClient {
} }
.build() .build()
retrofit = Retrofit.Builder() synchronized(this) {
.baseUrl(BASE_URL) retrofitCache.clear()
.client(okhttp) }
.addConverterFactory(GsonConverterFactory.create())
.build()
} }
inline fun <reified T> create(): T = retrofit.create(T::class.java) fun <T : Any> create(service: Class<T>, baseUrl: String = ServiceEndpointRegistry.controlBaseUrl): T {
val retrofit = synchronized(this) {
retrofitCache[baseUrl] ?: Retrofit.Builder()
.baseUrl(baseUrl)
.client(requireNotNull(okHttpClient) { "ApiClient not initialized" })
.addConverterFactory(GsonConverterFactory.create())
.build()
.also { retrofitCache[baseUrl] = it }
}
return retrofit.create(service)
}
} }

查看文件

@ -2,7 +2,7 @@ package com.xuqm.sdk.im
import com.xuqm.sdk.XuqmLoginSession 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.ServiceEndpointRegistry
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
@ -20,7 +20,7 @@ import kotlinx.coroutines.withContext
object ImSDK { object ImSDK {
private var client: ImClient? = null private var client: ImClient? = null
private val api: ImApi by lazy { ApiClient.create() } private val api: ImApi get() = ApiClient.create(ImApi::class.java, ServiceEndpointRegistry.imApiBaseUrl)
var currentUserId: String = "" var currentUserId: String = ""
private set private set
@ -131,7 +131,7 @@ object ImSDK {
private fun connectWithToken(token: String) { private fun connectWithToken(token: String) {
XuqmSDK.tokenStore.saveToken(token) XuqmSDK.tokenStore.saveToken(token)
client?.disconnect() client?.disconnect()
client = ImClient(WS_URL, token, XuqmSDK.appId) client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appId)
client?.connect() client?.connect()
} }

查看文件

@ -2,6 +2,7 @@ package com.xuqm.sdk.push
import android.content.Context import android.content.Context
import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.core.ServiceEndpointRegistry
import com.xuqm.sdk.network.ApiClient import com.xuqm.sdk.network.ApiClient
import com.xuqm.sdk.push.api.PushApi import com.xuqm.sdk.push.api.PushApi
import com.xuqm.sdk.utils.DeviceUtils import com.xuqm.sdk.utils.DeviceUtils
@ -11,7 +12,7 @@ import kotlinx.coroutines.launch
object PushSDK { object PushSDK {
private val api: PushApi by lazy { ApiClient.create() } private val api: PushApi get() = ApiClient.create(PushApi::class.java, ServiceEndpointRegistry.pushBaseUrl)
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
fun registerDevice(context: Context, userId: String) { fun registerDevice(context: Context, userId: String) {

查看文件

@ -5,6 +5,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.core.ServiceEndpointRegistry
import com.xuqm.sdk.network.ApiClient import com.xuqm.sdk.network.ApiClient
import com.xuqm.sdk.update.api.UpdateApi import com.xuqm.sdk.update.api.UpdateApi
import com.xuqm.sdk.update.model.RnUpdateInfo import com.xuqm.sdk.update.model.RnUpdateInfo
@ -16,7 +17,7 @@ import java.net.URL
object UpdateSDK { object UpdateSDK {
private val api: UpdateApi by lazy { ApiClient.create() } private val api: UpdateApi get() = ApiClient.create(UpdateApi::class.java, ServiceEndpointRegistry.updateBaseUrl)
private fun normalizeDownloadUrl(rawUrl: String?): String? { private fun normalizeDownloadUrl(rawUrl: String?): String? {
if (rawUrl.isNullOrBlank()) return rawUrl if (rawUrl.isNullOrBlank()) return rawUrl