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 登录
|
// 如果工程里集成了 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
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户