重构: 将 AuthService、ApiService、WebSocketClient 改为适配器
各服务现在通过主构造函数接收各自的客户端提供器; 为在迁移期间保持现有调用点可编译,保留了向后兼容的次构造函数。
This commit is contained in:
@@ -2,6 +2,8 @@ package com.huaga.life_echo.network
|
||||
|
||||
import com.huaga.life_echo.data.auth.TokenManager
|
||||
import com.huaga.life_echo.network.models.*
|
||||
import com.huaga.life_echo.network.runtime.RestClientProfile
|
||||
import com.huaga.life_echo.network.runtime.RestClientProvider
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.android.*
|
||||
@@ -13,83 +15,101 @@ import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
class ApiService(
|
||||
private val provider: RestClientProvider,
|
||||
private val tokenManager: TokenManager? = null,
|
||||
private val authService: AuthService? = null
|
||||
) {
|
||||
private val client by lazy {
|
||||
HttpClient(Android) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
install(Logging) {
|
||||
level = LogLevel.INFO
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 45_000 // 单次请求总超时(如创建订单)
|
||||
connectTimeoutMillis = 15_000 // 连接超时
|
||||
socketTimeoutMillis = 45_000 // 读写超时
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use the provider-based constructor via AppContainer")
|
||||
constructor(
|
||||
tokenManager: TokenManager? = null,
|
||||
authService: AuthService? = null,
|
||||
) : this(
|
||||
provider = createLegacyProvider(tokenManager, authService),
|
||||
tokenManager = tokenManager,
|
||||
authService = authService,
|
||||
)
|
||||
|
||||
// 使用Ktor内置Auth插件:自动添加Bearer token,401时自动刷新并重试
|
||||
if (tokenManager != null && authService != null) {
|
||||
install(Auth) {
|
||||
bearer {
|
||||
loadTokens {
|
||||
val access = tokenManager.getAccessToken()
|
||||
val refresh = tokenManager.getRefreshToken()
|
||||
if (!access.isNullOrBlank() && !refresh.isNullOrBlank()) {
|
||||
BearerTokens(access, refresh)
|
||||
} else null
|
||||
}
|
||||
refreshTokens {
|
||||
val refresh = oldTokens?.refreshToken
|
||||
?: tokenManager.getRefreshToken()
|
||||
if (!refresh.isNullOrBlank()) {
|
||||
val result = authService.refreshToken(refresh)
|
||||
result.fold(
|
||||
onSuccess = { tokenResponse ->
|
||||
tokenManager.saveTokens(
|
||||
tokenResponse.access_token,
|
||||
tokenResponse.refresh_token,
|
||||
tokenManager.getUserId() ?: ""
|
||||
private val client get() = provider.getClient(RestClientProfile.API)
|
||||
|
||||
companion object {
|
||||
private val BASE_URL = com.huaga.life_echo.config.AppConfig.BASE_URL
|
||||
|
||||
private fun createLegacyProvider(
|
||||
tokenManager: TokenManager?,
|
||||
authService: AuthService?,
|
||||
): RestClientProvider = RestClientProvider { profile ->
|
||||
when (profile) {
|
||||
RestClientProfile.AUTH -> HttpClient(Android) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json { ignoreUnknownKeys = true })
|
||||
}
|
||||
install(Logging) { level = LogLevel.INFO }
|
||||
}
|
||||
RestClientProfile.API -> HttpClient(Android) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json { ignoreUnknownKeys = true })
|
||||
}
|
||||
install(Logging) { level = LogLevel.INFO }
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 45_000
|
||||
connectTimeoutMillis = 15_000
|
||||
socketTimeoutMillis = 45_000
|
||||
}
|
||||
if (tokenManager != null && authService != null) {
|
||||
install(Auth) {
|
||||
bearer {
|
||||
loadTokens {
|
||||
val access = tokenManager.getAccessToken()
|
||||
val refresh = tokenManager.getRefreshToken()
|
||||
if (!access.isNullOrBlank() && !refresh.isNullOrBlank()) {
|
||||
BearerTokens(access, refresh)
|
||||
} else null
|
||||
}
|
||||
refreshTokens {
|
||||
val refresh = oldTokens?.refreshToken
|
||||
?: tokenManager.getRefreshToken()
|
||||
if (!refresh.isNullOrBlank()) {
|
||||
val result = authService.refreshToken(refresh)
|
||||
result.fold(
|
||||
onSuccess = { tokenResponse ->
|
||||
tokenManager.saveTokens(
|
||||
tokenResponse.access_token,
|
||||
tokenResponse.refresh_token,
|
||||
tokenManager.getUserId() ?: ""
|
||||
)
|
||||
BearerTokens(
|
||||
tokenResponse.access_token,
|
||||
tokenResponse.refresh_token
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
tokenManager.clearTokens()
|
||||
tokenManager.notifyTokenRefreshFailed()
|
||||
null
|
||||
}
|
||||
)
|
||||
BearerTokens(
|
||||
tokenResponse.access_token,
|
||||
tokenResponse.refresh_token
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
} else {
|
||||
tokenManager.clearTokens()
|
||||
tokenManager.notifyTokenRefreshFailed()
|
||||
null
|
||||
}
|
||||
)
|
||||
} else {
|
||||
tokenManager.clearTokens()
|
||||
tokenManager.notifyTokenRefreshFailed()
|
||||
null
|
||||
}
|
||||
sendWithoutRequest { true }
|
||||
}
|
||||
}
|
||||
sendWithoutRequest { true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val BASE_URL = com.huaga.life_echo.config.AppConfig.BASE_URL
|
||||
}
|
||||
|
||||
// ==================== 对话相关API ====================
|
||||
|
||||
suspend fun createConversation(): Result<CreateConversationResponse> {
|
||||
@@ -138,7 +158,7 @@ class ApiService(
|
||||
|
||||
suspend fun deleteConversation(conversationId: String): Result<Unit> {
|
||||
return try {
|
||||
val response = client.delete("$BASE_URL/api/conversations/$conversationId") {
|
||||
client.delete("$BASE_URL/api/conversations/$conversationId") {
|
||||
contentType(ContentType.Application.Json)
|
||||
}
|
||||
Result.success(Unit)
|
||||
@@ -205,12 +225,9 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除章节(将章节标记为 disabled)
|
||||
*/
|
||||
suspend fun disableChapter(chapterId: String): Result<Unit> {
|
||||
return try {
|
||||
val response = client.delete("$BASE_URL/api/chapters/$chapterId") {
|
||||
client.delete("$BASE_URL/api/chapters/$chapterId") {
|
||||
contentType(ContentType.Application.Json)
|
||||
}
|
||||
Result.success(Unit)
|
||||
@@ -300,13 +317,8 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 支付订单API(微信/支付宝) ====================
|
||||
|
||||
/**
|
||||
* 创建支付订单
|
||||
* 返回微信支付参数或支付宝订单字符串。
|
||||
* 失败时尽量返回后端返回的 detail 信息,便于 debug。
|
||||
*/
|
||||
// ==================== 支付订单API ====================
|
||||
|
||||
suspend fun createPaymentOrder(
|
||||
planId: String,
|
||||
paymentMethod: String
|
||||
@@ -324,14 +336,10 @@ class ApiService(
|
||||
}
|
||||
Result.success(response.body())
|
||||
} catch (e: Exception) {
|
||||
// 超时、网络异常等:保留原始异常信息
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 FastAPI 错误响应中的 detail 字段(字符串或数组),用于展示后端返回的详尽错误信息
|
||||
*/
|
||||
private fun parseApiErrorDetail(errorBody: String?): String {
|
||||
if (errorBody.isNullOrBlank()) return ""
|
||||
return try {
|
||||
@@ -350,9 +358,6 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付订单状态
|
||||
*/
|
||||
suspend fun getPaymentOrderStatus(orderId: String): Result<PaymentOrderStatusResponse> {
|
||||
return try {
|
||||
val response = client.get("$BASE_URL/api/payment/order/$orderId/status") {
|
||||
@@ -364,9 +369,6 @@ class ApiService(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付订单列表(新接口)
|
||||
*/
|
||||
suspend fun getPaymentOrders(): Result<List<PaymentOrderStatusResponse>> {
|
||||
return try {
|
||||
val response = client.get("$BASE_URL/api/payment/orders") {
|
||||
@@ -380,10 +382,6 @@ class ApiService(
|
||||
|
||||
// ==================== 用户相关API ====================
|
||||
|
||||
/**
|
||||
* 测试订阅开关(仅当服务端 ENABLE_TEST_SUBSCRIPTION=1 时可用)
|
||||
* 用于微信支付审核通过前模拟付费通过,体验订阅额度。
|
||||
*/
|
||||
suspend fun setTestSubscription(
|
||||
action: String,
|
||||
planId: String = "pro"
|
||||
@@ -433,14 +431,13 @@ class ApiService(
|
||||
Result.failure(Exception("提交失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 提交失败时也返回成功,避免阻塞用户
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 回忆录状态相关API ====================
|
||||
|
||||
suspend fun getMemoirState(): Result<com.huaga.life_echo.network.models.MemoirStateDto> {
|
||||
suspend fun getMemoirState(): Result<MemoirStateDto> {
|
||||
return try {
|
||||
val response = client.get("$BASE_URL/api/memoir-state") {
|
||||
contentType(ContentType.Application.Json)
|
||||
@@ -453,7 +450,7 @@ class ApiService(
|
||||
|
||||
// ==================== 任务状态相关API ====================
|
||||
|
||||
suspend fun getTasksStatus(): Result<com.huaga.life_echo.network.models.TasksStatusDto> {
|
||||
suspend fun getTasksStatus(): Result<TasksStatusDto> {
|
||||
return try {
|
||||
val response = client.get("$BASE_URL/api/tasks/status") {
|
||||
contentType(ContentType.Application.Json)
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.huaga.life_echo.network
|
||||
|
||||
import com.huaga.life_echo.config.AppConfig
|
||||
import com.huaga.life_echo.network.models.*
|
||||
import com.huaga.life_echo.network.runtime.RestClientProfile
|
||||
import com.huaga.life_echo.network.runtime.RestClientProvider
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.android.Android
|
||||
@@ -24,31 +26,30 @@ import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 认证服务
|
||||
* 处理用户注册、登录、刷新令牌、登出等操作
|
||||
*/
|
||||
class AuthService {
|
||||
private val client = HttpClient(Android) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = false
|
||||
})
|
||||
class AuthService(
|
||||
private val provider: RestClientProvider,
|
||||
) {
|
||||
constructor() : this(
|
||||
RestClientProvider { _ ->
|
||||
HttpClient(Android) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = false
|
||||
})
|
||||
}
|
||||
install(Logging) { level = LogLevel.INFO }
|
||||
}
|
||||
}
|
||||
install(Logging) {
|
||||
level = LogLevel.INFO
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
private val client get() = provider.getClient(RestClientProfile.AUTH)
|
||||
|
||||
companion object {
|
||||
private val BASE_URL = AppConfig.BASE_URL
|
||||
private val AUTH_BASE = "$BASE_URL/api/auth"
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
suspend fun register(
|
||||
phone: String,
|
||||
password: String,
|
||||
@@ -68,7 +69,6 @@ class AuthService {
|
||||
Result.success(tokenResponse)
|
||||
}
|
||||
HttpStatusCode.BadRequest -> {
|
||||
// 尝试解析简单错误响应
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
@@ -77,13 +77,11 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
HttpStatusCode.UnprocessableEntity -> {
|
||||
// FastAPI验证错误(422)- 尝试解析验证错误详情
|
||||
try {
|
||||
val validationError = response.body<ValidationErrorResponse>()
|
||||
val errorMessages = validationError.detail.joinToString(", ") { it.msg }
|
||||
Result.failure(Exception("数据验证失败: $errorMessages"))
|
||||
} catch (e: Exception) {
|
||||
// 如果无法解析为验证错误,尝试解析为简单错误
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
@@ -101,9 +99,6 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
suspend fun login(phone: String, password: String, agreedToTerms: Boolean): Result<TokenResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/login") {
|
||||
@@ -120,7 +115,6 @@ class AuthService {
|
||||
Result.failure(Exception("手机号或密码错误"))
|
||||
}
|
||||
HttpStatusCode.BadRequest -> {
|
||||
// 尝试解析简单错误响应
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
@@ -129,13 +123,11 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
HttpStatusCode.UnprocessableEntity -> {
|
||||
// FastAPI验证错误(422)- 尝试解析验证错误详情
|
||||
try {
|
||||
val validationError = response.body<ValidationErrorResponse>()
|
||||
val errorMessages = validationError.detail.joinToString(", ") { it.msg }
|
||||
Result.failure(Exception("数据验证失败: $errorMessages"))
|
||||
} catch (e: Exception) {
|
||||
// 如果无法解析为验证错误,尝试解析为简单错误
|
||||
try {
|
||||
val error = response.body<ErrorResponse>()
|
||||
Result.failure(Exception(error.detail))
|
||||
@@ -153,9 +145,6 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*/
|
||||
suspend fun refreshToken(refreshToken: String): Result<TokenResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/refresh") {
|
||||
@@ -180,9 +169,6 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
suspend fun logout(accessToken: String, refreshToken: String): Result<Unit> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/logout") {
|
||||
@@ -192,24 +178,15 @@ class AuthService {
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
Result.failure(Exception("未授权"))
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("登出失败: ${response.status}"))
|
||||
}
|
||||
HttpStatusCode.OK -> Result.success(Unit)
|
||||
HttpStatusCode.Unauthorized -> Result.failure(Exception("未授权"))
|
||||
else -> Result.failure(Exception("登出失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
suspend fun getCurrentUser(accessToken: String): Result<UserResponse> {
|
||||
return try {
|
||||
val response = client.get("$AUTH_BASE/me") {
|
||||
@@ -221,21 +198,14 @@ class AuthService {
|
||||
val userResponse = response.body<UserResponse>()
|
||||
Result.success(userResponse)
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
Result.failure(Exception("未授权"))
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("获取用户信息失败: ${response.status}"))
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> Result.failure(Exception("未授权"))
|
||||
else -> Result.failure(Exception("获取用户信息失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传用户头像
|
||||
*/
|
||||
suspend fun uploadAvatar(accessToken: String, imageFile: File): Result<UserResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/me/avatar") {
|
||||
@@ -270,25 +240,14 @@ class AuthService {
|
||||
Result.failure(Exception("上传失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
Result.failure(Exception("未授权"))
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("上传头像失败: ${response.status}"))
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> Result.failure(Exception("未授权"))
|
||||
else -> Result.failure(Exception("上传头像失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 短信验证码相关方法
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*/
|
||||
suspend fun sendVerificationCode(phone: String, purpose: String): Result<SmsResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/sms/send") {
|
||||
@@ -317,19 +276,13 @@ class AuthService {
|
||||
Result.failure(Exception("发送过于频繁,请稍后再试"))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("发送验证码失败: ${response.status}"))
|
||||
}
|
||||
else -> Result.failure(Exception("发送验证码失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码登录/注册(统一接口)
|
||||
* 如果用户不存在,会自动注册(需要提供nickname)
|
||||
*/
|
||||
suspend fun loginWithSms(phone: String, code: String, agreedToTerms: Boolean, nickname: String? = null): Result<TokenResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/login/sms") {
|
||||
@@ -350,18 +303,13 @@ class AuthService {
|
||||
Result.failure(Exception("登录失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("登录失败: ${response.status}"))
|
||||
}
|
||||
else -> Result.failure(Exception("登录失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码注册
|
||||
*/
|
||||
suspend fun registerWithSms(
|
||||
phone: String,
|
||||
code: String,
|
||||
@@ -389,18 +337,13 @@ class AuthService {
|
||||
Result.failure(Exception("注册失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("注册失败: ${response.status}"))
|
||||
}
|
||||
else -> Result.failure(Exception("注册失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
suspend fun resetPassword(phone: String, code: String, newPassword: String): Result<MessageResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/password/reset") {
|
||||
@@ -421,18 +364,13 @@ class AuthService {
|
||||
Result.failure(Exception("重置密码失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("重置密码失败: ${response.status}"))
|
||||
}
|
||||
else -> Result.failure(Exception("重置密码失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码(已登录)
|
||||
*/
|
||||
suspend fun changePassword(accessToken: String, oldPassword: String, newPassword: String): Result<MessageResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/password/change") {
|
||||
@@ -454,21 +392,14 @@ class AuthService {
|
||||
Result.failure(Exception("修改密码失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
Result.failure(Exception("未授权"))
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("修改密码失败: ${response.status}"))
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> Result.failure(Exception("未授权"))
|
||||
else -> Result.failure(Exception("修改密码失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改手机号
|
||||
*/
|
||||
suspend fun changePhone(accessToken: String, newPhone: String, code: String): Result<UserResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/phone/change") {
|
||||
@@ -490,21 +421,14 @@ class AuthService {
|
||||
Result.failure(Exception("修改手机号失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
Result.failure(Exception("未授权"))
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("修改手机号失败: ${response.status}"))
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> Result.failure(Exception("未授权"))
|
||||
else -> Result.failure(Exception("修改手机号失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出所有设备
|
||||
*/
|
||||
suspend fun logoutAll(accessToken: String): Result<MessageResponse> {
|
||||
return try {
|
||||
val response = client.post("$AUTH_BASE/logout/all") {
|
||||
@@ -516,21 +440,14 @@ class AuthService {
|
||||
val messageResponse = response.body<MessageResponse>()
|
||||
Result.success(messageResponse)
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
Result.failure(Exception("未授权"))
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("登出失败: ${response.status}"))
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> Result.failure(Exception("未授权"))
|
||||
else -> Result.failure(Exception("登出失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户昵称
|
||||
*/
|
||||
suspend fun updateNickname(accessToken: String, nickname: String): Result<UserResponse> {
|
||||
return try {
|
||||
val response = client.put("$AUTH_BASE/me/nickname") {
|
||||
@@ -552,12 +469,8 @@ class AuthService {
|
||||
Result.failure(Exception("更新昵称失败: ${response.status}"))
|
||||
}
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
Result.failure(Exception("未授权"))
|
||||
}
|
||||
else -> {
|
||||
Result.failure(Exception("更新昵称失败: ${response.status}"))
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> Result.failure(Exception("未授权"))
|
||||
else -> Result.failure(Exception("更新昵称失败: ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("网络错误: ${e.message}", e))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.huaga.life_echo.network
|
||||
|
||||
import com.huaga.life_echo.network.runtime.RealtimeTransportProvider
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.plugins.websocket.*
|
||||
@@ -12,21 +13,22 @@ import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import android.util.Log
|
||||
|
||||
class WebSocketClient {
|
||||
private val client by lazy {
|
||||
HttpClient(OkHttp) {
|
||||
install(WebSockets)
|
||||
// 注意:WebSocket不需要ContentNegotiation插件,因为消息是手动序列化的
|
||||
// ContentNegotiation插件会导致尝试序列化WebSocket会话对象,引发序列化错误
|
||||
install(Logging) {
|
||||
level = LogLevel.INFO
|
||||
open class WebSocketClient(
|
||||
private val transportProvider: RealtimeTransportProvider,
|
||||
) {
|
||||
constructor() : this(
|
||||
RealtimeTransportProvider {
|
||||
HttpClient(OkHttp) {
|
||||
install(WebSockets)
|
||||
install(Logging) { level = LogLevel.INFO }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
private val client get() = transportProvider.getClient()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var session: DefaultWebSocketSession? = null
|
||||
private val messageFlow = MutableSharedFlow<WebSocketMessage>()
|
||||
@@ -34,9 +36,9 @@ class WebSocketClient {
|
||||
private var isConnected = false
|
||||
private var currentConversationId: String? = null
|
||||
private var currentToken: String? = null
|
||||
private var isGenerating = false // 是否正在生成回复
|
||||
private var currentGenerationJob: Job? = null // 当前生成任务
|
||||
private var onErrorCallback: ((String) -> Unit)? = null // 错误回调
|
||||
private var isGenerating = false
|
||||
private var currentGenerationJob: Job? = null
|
||||
private var onErrorCallback: ((String) -> Unit)? = null
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WebSocketClient"
|
||||
@@ -45,13 +47,12 @@ class WebSocketClient {
|
||||
private const val MAX_RECONNECT_ATTEMPTS = 5
|
||||
}
|
||||
|
||||
suspend fun connect(
|
||||
open suspend fun connect(
|
||||
conversationId: String,
|
||||
token: String? = null,
|
||||
onMessage: (WebSocketMessage) -> Unit,
|
||||
onError: ((String) -> Unit)? = null
|
||||
) {
|
||||
// 如果已连接,先断开
|
||||
if (isConnected) {
|
||||
disconnect()
|
||||
}
|
||||
@@ -68,7 +69,6 @@ class WebSocketClient {
|
||||
Log.d(TAG, "开始连接WebSocket: $url")
|
||||
|
||||
try {
|
||||
// 建立WebSocket连接
|
||||
session = client.webSocketSession {
|
||||
url {
|
||||
takeFrom(url)
|
||||
@@ -80,15 +80,12 @@ class WebSocketClient {
|
||||
currentConversationId = conversationId
|
||||
currentToken = token
|
||||
|
||||
// 启动消息接收协程(在设置isConnected之前)
|
||||
val receiveJob = scope.launch {
|
||||
receiveMessages(onMessage)
|
||||
}
|
||||
|
||||
// 等待一小段时间确保连接建立
|
||||
delay(100)
|
||||
|
||||
// 检查session是否有效
|
||||
if (session == null || session?.isActive != true) {
|
||||
Log.e(TAG, "WebSocket连接失败:session无效")
|
||||
throw Exception("WebSocket连接失败:session无效")
|
||||
@@ -96,7 +93,6 @@ class WebSocketClient {
|
||||
|
||||
Log.d(TAG, "WebSocket session有效,准备发送连接消息")
|
||||
|
||||
// 发送连接消息(不等待确认,由服务器返回connect消息时再设置isConnected)
|
||||
try {
|
||||
sendMessage(WebSocketMessage(
|
||||
type = MessageType.connect,
|
||||
@@ -105,15 +101,12 @@ class WebSocketClient {
|
||||
))
|
||||
Log.d(TAG, "连接消息已发送,等待服务器确认")
|
||||
} catch (e: Exception) {
|
||||
// 发送失败,连接可能有问题
|
||||
Log.e(TAG, "发送连接消息失败: ${e.message}", e)
|
||||
isConnected = false
|
||||
receiveJob.cancel()
|
||||
throw Exception("发送连接消息失败: ${e.message}")
|
||||
}
|
||||
|
||||
// 注意:isConnected将在收到服务器的connect确认消息时设置为true
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "WebSocket连接异常: ${e.message}", e)
|
||||
isConnected = false
|
||||
@@ -126,7 +119,6 @@ class WebSocketClient {
|
||||
|
||||
private suspend fun receiveMessages(onMessage: (WebSocketMessage) -> Unit) {
|
||||
try {
|
||||
// 持续接收消息,直到连接关闭
|
||||
while (true) {
|
||||
val frame = session?.incoming?.receive() as? Frame.Text
|
||||
?: break
|
||||
@@ -137,13 +129,11 @@ class WebSocketClient {
|
||||
|
||||
val message = Json.decodeFromString<WebSocketMessage>(text)
|
||||
|
||||
// 如果收到connect消息,设置连接状态为已连接
|
||||
if (message.type == MessageType.connect) {
|
||||
isConnected = true
|
||||
Log.d(TAG, "WebSocket连接已确认,状态设置为已连接")
|
||||
}
|
||||
|
||||
// 如果收到error消息,设置连接状态为未连接
|
||||
if (message.type == MessageType.error) {
|
||||
isConnected = false
|
||||
val errorMsg = message.getString("message") ?: "未知错误"
|
||||
@@ -155,19 +145,16 @@ class WebSocketClient {
|
||||
onMessage(message)
|
||||
messageFlow.emit(message)
|
||||
} catch (e: Exception) {
|
||||
// JSON解析失败,记录错误但继续接收
|
||||
Log.e(TAG, "消息解析失败: ${e.message}", e)
|
||||
onErrorCallback?.invoke("消息解析失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 连接异常
|
||||
Log.e(TAG, "接收消息异常: ${e.message}", e)
|
||||
isConnected = false
|
||||
val errorMsg = "连接异常: ${e.message}"
|
||||
onErrorCallback?.invoke(errorMsg)
|
||||
|
||||
// 触发重连
|
||||
reconnectJob?.cancel()
|
||||
reconnectJob = scope.launch {
|
||||
Log.d(TAG, "开始重连...")
|
||||
@@ -194,7 +181,6 @@ class WebSocketClient {
|
||||
session?.send(Frame.Text(json))
|
||||
Log.d(TAG, "消息发送成功: ${message.type}")
|
||||
} catch (e: Exception) {
|
||||
// 发送失败,可能连接已断开
|
||||
Log.e(TAG, "发送消息失败: ${e.message}", e)
|
||||
isConnected = false
|
||||
onErrorCallback?.invoke("发送消息失败: ${e.message}")
|
||||
@@ -211,9 +197,6 @@ class WebSocketClient {
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送分段语音(长语音边录边传)
|
||||
*/
|
||||
suspend fun sendAudioSegment(
|
||||
audioBytes: ByteArray,
|
||||
conversationId: String,
|
||||
@@ -250,12 +233,6 @@ class WebSocketClient {
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送完整的音频消息(类似微信语音消息)
|
||||
* @param audioBytes 音频文件字节数组
|
||||
* @param conversationId 对话 ID
|
||||
* @param duration 音频时长(秒)
|
||||
*/
|
||||
suspend fun sendAudioMessage(audioBytes: ByteArray, conversationId: String, duration: Int) {
|
||||
Log.d(TAG, "准备发送音频消息,大小: ${audioBytes.size} 字节, 时长: $duration 秒")
|
||||
if (!isConnected) {
|
||||
@@ -275,9 +252,6 @@ class WebSocketClient {
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅转写:发送音频获取文字,不落库、不触发 Agent,用于「转文字」后客户端再发文本
|
||||
*/
|
||||
suspend fun sendTranscribeOnly(audioBytes: ByteArray, conversationId: String) {
|
||||
Log.d(TAG, "发送仅转写请求,大小: ${audioBytes.size} 字节")
|
||||
if (!isConnected) throw Exception("WebSocket未连接,请先建立连接")
|
||||
@@ -311,9 +285,6 @@ class WebSocketClient {
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前正在生成的回复
|
||||
*/
|
||||
suspend fun cancelGeneration(conversationId: String) {
|
||||
isGenerating = false
|
||||
currentGenerationJob?.cancel()
|
||||
@@ -324,19 +295,13 @@ class WebSocketClient {
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否正在生成回复
|
||||
*/
|
||||
fun isGenerating(): Boolean = isGenerating
|
||||
|
||||
/**
|
||||
* 设置生成状态
|
||||
*/
|
||||
fun setGenerating(generating: Boolean) {
|
||||
isGenerating = generating
|
||||
}
|
||||
|
||||
suspend fun disconnect() {
|
||||
open suspend fun disconnect() {
|
||||
Log.d(TAG, "断开WebSocket连接")
|
||||
isConnected = false
|
||||
isGenerating = false
|
||||
@@ -370,5 +335,5 @@ class WebSocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
fun isConnected(): Boolean = isConnected
|
||||
open fun isConnected(): Boolean = isConnected
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.huaga.life_echo.network
|
||||
|
||||
import com.huaga.life_echo.network.runtime.RestClientProfile
|
||||
import com.huaga.life_echo.network.runtime.RestClientProvider
|
||||
import io.ktor.client.HttpClient
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito
|
||||
|
||||
class ApiServiceProviderTest {
|
||||
|
||||
@Test
|
||||
fun api_service_uses_api_profile_from_provider() {
|
||||
val requestedProfiles = mutableListOf<RestClientProfile>()
|
||||
val provider = RestClientProvider { profile ->
|
||||
requestedProfiles += profile
|
||||
Mockito.mock(HttpClient::class.java)
|
||||
}
|
||||
|
||||
ApiService(
|
||||
provider = provider,
|
||||
tokenManager = null,
|
||||
authService = null
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"ApiService should not eagerly request a client at construction time",
|
||||
emptyList<RestClientProfile>(),
|
||||
requestedProfiles
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.huaga.life_echo.network
|
||||
|
||||
import com.huaga.life_echo.network.runtime.RestClientProfile
|
||||
import com.huaga.life_echo.network.runtime.RestClientProvider
|
||||
import io.ktor.client.HttpClient
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito
|
||||
|
||||
class AuthServiceProviderTest {
|
||||
|
||||
@Test
|
||||
fun auth_service_uses_auth_profile_from_provider_when_constructed() {
|
||||
val requestedProfiles = mutableListOf<RestClientProfile>()
|
||||
val provider = RestClientProvider { profile ->
|
||||
requestedProfiles += profile
|
||||
Mockito.mock(HttpClient::class.java)
|
||||
}
|
||||
|
||||
AuthService(provider = provider)
|
||||
|
||||
assertEquals(
|
||||
"AuthService should not eagerly request a client at construction time",
|
||||
emptyList<RestClientProfile>(),
|
||||
requestedProfiles
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user