From 5a0378d5798d083cc9d4a364bd783ca8f9484db6 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Mon, 27 Apr 2026 19:30:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20=E5=AE=9E=E7=8E=B0=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E6=9C=8D=E5=8A=A1=E7=AB=AF=E7=82=B9=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E7=8E=AF=E5=A2=83=E5=88=87=E6=8D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除硬编码的基础URL常量,改为可配置的服务端点 - 添加ServiceEndpointRegistry用于统一管理所有服务端点地址 - 实现ApiClient支持多基础URL的Retrofit实例缓存机制 - 新增XuqmSDK.configureServiceEndpoints等方法用于运行时切换环境 - 为sample-app添加SampleEnvironmentConfig支持本地联调环境切换 - 创建独立的IM、Push、Update SDK模块并集成服务端点配置 - 更新文档说明如何进行联调环境切换操作 --- README.md | 17 ++++++++ .../java/com/xuqm/sdk/sample/XuqmSampleApp.kt | 2 + .../sdk/sample/config/SampleEnvironment.kt | 34 +++++++++++++++ .../com/xuqm/sdk/sample/data/api/DemoApi.kt | 8 ++-- .../sdk/sample/data/repo/AuthRepository.kt | 7 +++- .../src/main/java/com/xuqm/sdk/XuqmSDK.kt | 16 ++++++++ .../main/java/com/xuqm/sdk/core/SDKConfig.kt | 3 -- .../com/xuqm/sdk/core/ServiceEndpoints.kt | 41 +++++++++++++++++++ .../java/com/xuqm/sdk/network/ApiClient.kt | 28 ++++++++----- sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt | 6 +-- .../main/java/com/xuqm/sdk/push/PushSDK.kt | 3 +- .../java/com/xuqm/sdk/update/UpdateSDK.kt | 3 +- 12 files changed, 146 insertions(+), 22 deletions(-) create mode 100644 sample-app/src/main/java/com/xuqm/sdk/sample/config/SampleEnvironment.kt create mode 100644 sdk-core/src/main/java/com/xuqm/sdk/core/ServiceEndpoints.kt diff --git a/README.md b/README.md index 9c6724f..aa23b91 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,23 @@ XuqmSDK.login( // 如果工程里集成了 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 diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/XuqmSampleApp.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/XuqmSampleApp.kt index b5868ef..a77dc5f 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/XuqmSampleApp.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/XuqmSampleApp.kt @@ -3,12 +3,14 @@ package com.xuqm.sdk.sample import android.app.Application import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.core.LogLevel +import com.xuqm.sdk.sample.config.SampleEnvironmentConfig import com.xuqm.sdk.sample.di.AppDependencies class XuqmSampleApp : Application() { override fun onCreate() { super.onCreate() + SampleEnvironmentConfig.useExternal() AppDependencies.init(this) XuqmSDK.initialize( context = this, diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/config/SampleEnvironment.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/config/SampleEnvironment.kt new file mode 100644 index 0000000..c3d46e6 --- /dev/null +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/config/SampleEnvironment.kt @@ -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) + } +} diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/data/api/DemoApi.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/data/api/DemoApi.kt index a54fad5..d57decd 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/data/api/DemoApi.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/data/api/DemoApi.kt @@ -11,7 +11,6 @@ import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Query -const val DEMO_BASE_URL = "https://dev.xuqinmin.com/" const val DEMO_APP_ID = "ak_demo_chat" data class DemoResponse( @@ -85,7 +84,10 @@ interface DemoApi { } object DemoApiFactory { - fun create(tokenProvider: () -> String?): DemoApi { + fun create( + baseUrl: String, + tokenProvider: () -> String?, + ): DemoApi { val authInterceptor = Interceptor { chain -> val request = chain.request().newBuilder().apply { tokenProvider()?.takeIf { it.isNotBlank() }?.let { header("Authorization", "Bearer $it") } @@ -98,7 +100,7 @@ object DemoApiFactory { .build() return Retrofit.Builder() - .baseUrl(DEMO_BASE_URL) + .baseUrl(baseUrl) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/data/repo/AuthRepository.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/data/repo/AuthRepository.kt index 84af95b..12a2750 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/data/repo/AuthRepository.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/data/repo/AuthRepository.kt @@ -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.UpdateProfileRequest import com.xuqm.sdk.sample.data.api.UserData +import com.xuqm.sdk.sample.config.SampleEnvironmentConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -27,7 +28,11 @@ class AuthRepository(context: Context) { 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 getCurrentUserId(): String? = prefs.getString("user_id", null) diff --git a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt index 436d25b..4be1dbb 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt @@ -3,6 +3,8 @@ package com.xuqm.sdk import android.content.Context import com.xuqm.sdk.auth.TokenStore 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.network.ApiClient import kotlinx.coroutines.Dispatchers @@ -31,6 +33,20 @@ object XuqmSDK { 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() { check(initialized) { "XuqmSDK not initialized. Call XuqmSDK.initialize() first." } } diff --git a/sdk-core/src/main/java/com/xuqm/sdk/core/SDKConfig.kt b/sdk-core/src/main/java/com/xuqm/sdk/core/SDKConfig.kt index f973b22..36e45d7 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/core/SDKConfig.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/core/SDKConfig.kt @@ -1,8 +1,5 @@ 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( val appId: String, val logLevel: LogLevel = LogLevel.WARN, diff --git a/sdk-core/src/main/java/com/xuqm/sdk/core/ServiceEndpoints.kt b/sdk-core/src/main/java/com/xuqm/sdk/core/ServiceEndpoints.kt new file mode 100644 index 0000000..899d55d --- /dev/null +++ b/sdk-core/src/main/java/com/xuqm/sdk/core/ServiceEndpoints.kt @@ -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() + } +} diff --git a/sdk-core/src/main/java/com/xuqm/sdk/network/ApiClient.kt b/sdk-core/src/main/java/com/xuqm/sdk/network/ApiClient.kt index ce5aa98..05a4226 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/network/ApiClient.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/network/ApiClient.kt @@ -1,7 +1,7 @@ package com.xuqm.sdk.network 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.SDKConfig import okhttp3.OkHttpClient @@ -14,8 +14,8 @@ import java.util.concurrent.TimeUnit object ApiClient { private var tokenStore: TokenStore? = null - lateinit var retrofit: Retrofit - private set + private var okHttpClient: OkHttpClient? = null + private val retrofitCache = mutableMapOf() fun init(cfg: SDKConfig, store: TokenStore) { tokenStore = store @@ -25,7 +25,7 @@ object ApiClient { else HttpLoggingInterceptor.Level.NONE } - val okhttp = OkHttpClient.Builder() + okHttpClient = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .addInterceptor(logging) @@ -40,12 +40,20 @@ object ApiClient { } .build() - retrofit = Retrofit.Builder() - .baseUrl(BASE_URL) - .client(okhttp) - .addConverterFactory(GsonConverterFactory.create()) - .build() + synchronized(this) { + retrofitCache.clear() + } } - inline fun create(): T = retrofit.create(T::class.java) + fun create(service: Class, 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) + } } diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt index 28da44a..e32d55f 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt @@ -2,7 +2,7 @@ package com.xuqm.sdk.im import com.xuqm.sdk.XuqmLoginSession 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.CreateGroupRequest import com.xuqm.sdk.im.api.ImApi @@ -20,7 +20,7 @@ import kotlinx.coroutines.withContext object ImSDK { 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 = "" private set @@ -131,7 +131,7 @@ object ImSDK { private fun connectWithToken(token: String) { XuqmSDK.tokenStore.saveToken(token) client?.disconnect() - client = ImClient(WS_URL, token, XuqmSDK.appId) + client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appId) client?.connect() } diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt index 4d991ba..f93441a 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt @@ -2,6 +2,7 @@ package com.xuqm.sdk.push import android.content.Context import com.xuqm.sdk.XuqmSDK +import com.xuqm.sdk.core.ServiceEndpointRegistry import com.xuqm.sdk.network.ApiClient import com.xuqm.sdk.push.api.PushApi import com.xuqm.sdk.utils.DeviceUtils @@ -11,7 +12,7 @@ import kotlinx.coroutines.launch 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) fun registerDevice(context: Context, userId: String) { diff --git a/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt b/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt index 219c344..76486b5 100644 --- a/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt +++ b/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.net.Uri import androidx.core.content.FileProvider import com.xuqm.sdk.XuqmSDK +import com.xuqm.sdk.core.ServiceEndpointRegistry import com.xuqm.sdk.network.ApiClient import com.xuqm.sdk.update.api.UpdateApi import com.xuqm.sdk.update.model.RnUpdateInfo @@ -16,7 +17,7 @@ import java.net.URL 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? { if (rawUrl.isNullOrBlank()) return rawUrl