feat(sdk): 实现动态服务端点配置和环境切换功能
- 移除硬编码的基础URL常量,改为可配置的服务端点 - 添加ServiceEndpointRegistry用于统一管理所有服务端点地址 - 实现ApiClient支持多基础URL的Retrofit实例缓存机制 - 新增XuqmSDK.configureServiceEndpoints等方法用于运行时切换环境 - 为sample-app添加SampleEnvironmentConfig支持本地联调环境切换 - 创建独立的IM、Push、Update SDK模块并集成服务端点配置 - 更新文档说明如何进行联调环境切换操作
这个提交包含在:
父节点
087753075e
当前提交
5a0378d579
17
README.md
17
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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.Query
|
||||
|
||||
const val DEMO_BASE_URL = "https://dev.xuqinmin.com/"
|
||||
const val DEMO_APP_ID = "ak_demo_chat"
|
||||
|
||||
data class DemoResponse<T>(
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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." }
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
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<String, Retrofit>()
|
||||
|
||||
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 <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.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()
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户