feat: 扩展前端网络层服务

- 扩展ApiService添加新接口
- 优化AuthService认证服务
- 增强WebSocketClient连接和消息处理
- 更新AuthModels和ConversationModels数据模型
This commit is contained in:
iammm0
2026-01-23 14:02:42 +08:00
parent 4a8f1a3b88
commit 09c50bc320
5 changed files with 293 additions and 43 deletions

View File

@@ -1,7 +1,6 @@
package com.huaga.life_echo.network package com.huaga.life_echo.network
import com.huaga.life_echo.data.auth.TokenManager import com.huaga.life_echo.data.auth.TokenManager
import com.huaga.life_echo.data.mock.MockDataProvider
import com.huaga.life_echo.network.interceptors.AuthInterceptorPlugin import com.huaga.life_echo.network.interceptors.AuthInterceptorPlugin
import com.huaga.life_echo.network.models.* import com.huaga.life_echo.network.models.*
import io.ktor.client.* import io.ktor.client.*
@@ -44,6 +43,17 @@ class ApiService(
// ==================== 对话相关API ==================== // ==================== 对话相关API ====================
suspend fun createConversation(): Result<CreateConversationResponse> {
return try {
val response = client.post("$BASE_URL/api/conversations") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getConversationList(): Result<List<ConversationListItemDto>> { suspend fun getConversationList(): Result<List<ConversationListItemDto>> {
return try { return try {
val response = client.get("$BASE_URL/api/conversations") { val response = client.get("$BASE_URL/api/conversations") {
@@ -51,8 +61,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
// 接口失败时返回Mock数据 Result.failure(e)
Result.success(MockDataProvider.getMockConversations())
} }
} }
@@ -63,7 +72,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockConversationDetail(id)) Result.failure(e)
} }
} }
@@ -74,7 +83,33 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockMessages(conversationId)) Result.failure(e)
}
}
suspend fun deleteConversation(conversationId: String): Result<Unit> {
return try {
val response = client.delete("$BASE_URL/api/conversations/$conversationId") {
contentType(ContentType.Application.Json)
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun organizeConversation(conversationId: String): Result<Unit> {
return try {
val response = client.post("$BASE_URL/api/conversations/$conversationId/organize") {
contentType(ContentType.Application.Json)
}
if (response.status.isSuccess()) {
Result.success(Unit)
} else {
Result.failure(Exception("整理失败: ${response.status}"))
}
} catch (e: Exception) {
Result.failure(e)
} }
} }
@@ -87,7 +122,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockBookInfo()) Result.failure(e)
} }
} }
@@ -98,7 +133,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockChapters()) Result.failure(e)
} }
} }
@@ -109,8 +144,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
val chapters = MockDataProvider.getMockChapters() Result.failure(e)
Result.success(chapters.find { it.id == id } ?: chapters[0])
} }
} }
@@ -121,7 +155,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockChapterContent(id)) Result.failure(e)
} }
} }
@@ -133,8 +167,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
// 更新失败时返回Mock数据 Result.failure(e)
Result.success(MockDataProvider.getMockBookInfo())
} }
} }
@@ -159,7 +192,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockPlans()) Result.failure(e)
} }
} }
@@ -170,7 +203,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockCurrentPlan()) Result.failure(e)
} }
} }
@@ -181,7 +214,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockQuotaCheck()) Result.failure(e)
} }
} }
@@ -204,7 +237,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockOrders()) Result.failure(e)
} }
} }
@@ -215,8 +248,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
val orders = MockDataProvider.getMockOrders() Result.failure(e)
Result.success(orders.find { it.id == orderId } ?: orders[0])
} }
} }
@@ -229,7 +261,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockUserProfile()) Result.failure(e)
} }
} }
@@ -240,7 +272,7 @@ class ApiService(
} }
Result.success(response.body()) Result.success(response.body())
} catch (e: Exception) { } catch (e: Exception) {
Result.success(MockDataProvider.getMockFAQs()) Result.failure(e)
} }
} }

View File

@@ -7,21 +7,27 @@ import com.huaga.life_echo.network.models.RefreshTokenRequest
import com.huaga.life_echo.network.models.RegisterRequest import com.huaga.life_echo.network.models.RegisterRequest
import com.huaga.life_echo.network.models.TokenResponse import com.huaga.life_echo.network.models.TokenResponse
import com.huaga.life_echo.network.models.UserResponse import com.huaga.life_echo.network.models.UserResponse
import com.huaga.life_echo.network.models.ValidationErrorResponse
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.engine.android.Android import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.forms.*
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.header import io.ktor.client.request.header
import io.ktor.client.request.post import io.ktor.client.request.post
import io.ktor.client.request.setBody import io.ktor.client.request.setBody
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType import io.ktor.http.contentType
import io.ktor.http.Headers
import io.ktor.http.append
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File
/** /**
* 认证服务 * 认证服务
@@ -66,8 +72,29 @@ class AuthService {
Result.success(tokenResponse) Result.success(tokenResponse)
} }
HttpStatusCode.BadRequest -> { HttpStatusCode.BadRequest -> {
val error = response.body<ErrorResponse>() // 尝试解析简单错误响应
Result.failure(Exception(error.detail)) try {
val error = response.body<ErrorResponse>()
Result.failure(Exception(error.detail))
} catch (e: Exception) {
Result.failure(Exception("注册失败: ${response.status}"))
}
}
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))
} catch (e2: Exception) {
Result.failure(Exception("请求格式错误: ${response.status}"))
}
}
} }
else -> { else -> {
Result.failure(Exception("注册失败: ${response.status}")) Result.failure(Exception("注册失败: ${response.status}"))
@@ -97,8 +124,29 @@ class AuthService {
Result.failure(Exception("手机号或密码错误")) Result.failure(Exception("手机号或密码错误"))
} }
HttpStatusCode.BadRequest -> { HttpStatusCode.BadRequest -> {
val error = response.body<ErrorResponse>() // 尝试解析简单错误响应
Result.failure(Exception(error.detail)) try {
val error = response.body<ErrorResponse>()
Result.failure(Exception(error.detail))
} catch (e: Exception) {
Result.failure(Exception("请求错误: ${response.status}"))
}
}
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))
} catch (e2: Exception) {
Result.failure(Exception("请求格式错误: ${response.status}"))
}
}
} }
else -> { else -> {
Result.failure(Exception("登录失败: ${response.status}")) Result.failure(Exception("登录失败: ${response.status}"))
@@ -188,4 +236,53 @@ class AuthService {
Result.failure(Exception("网络错误: ${e.message}", e)) 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") {
header("Authorization", "Bearer $accessToken")
setBody(
MultiPartFormDataContent(
formData {
append(
key = "file",
headers = Headers.build {
append(HttpHeaders.ContentType, "image/jpeg")
append(HttpHeaders.ContentDisposition, "form-data; name=\"file\"; filename=\"${imageFile.name}\"")
}
) {
imageFile.readBytes()
}
}
)
)
}
when (response.status) {
HttpStatusCode.OK -> {
val userResponse = response.body<UserResponse>()
Result.success(userResponse)
}
HttpStatusCode.BadRequest -> {
try {
val error = response.body<ErrorResponse>()
Result.failure(Exception(error.detail))
} catch (e: Exception) {
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))
}
}
} }

View File

@@ -1,7 +1,7 @@
package com.huaga.life_echo.network package com.huaga.life_echo.network
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.android.* import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.websocket.* import io.ktor.client.plugins.websocket.*
import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.* import io.ktor.client.plugins.logging.*
@@ -15,9 +15,10 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import android.util.Log
class WebSocketClient { class WebSocketClient {
private val client = HttpClient(Android) { private val client = HttpClient(OkHttp) {
install(WebSockets) install(WebSockets)
install(ContentNegotiation) { install(ContentNegotiation) {
json(Json { json(Json {
@@ -39,8 +40,10 @@ class WebSocketClient {
private var currentToken: String? = null private var currentToken: String? = null
private var isGenerating = false // 是否正在生成回复 private var isGenerating = false // 是否正在生成回复
private var currentGenerationJob: Job? = null // 当前生成任务 private var currentGenerationJob: Job? = null // 当前生成任务
private var onErrorCallback: ((String) -> Unit)? = null // 错误回调
companion object { companion object {
private const val TAG = "WebSocketClient"
private const val BASE_URL = com.huaga.life_echo.config.AppConfig.WS_BASE_URL private const val BASE_URL = com.huaga.life_echo.config.AppConfig.WS_BASE_URL
private const val RECONNECT_DELAY_MS = 3000L private const val RECONNECT_DELAY_MS = 3000L
private const val MAX_RECONNECT_ATTEMPTS = 5 private const val MAX_RECONNECT_ATTEMPTS = 5
@@ -49,8 +52,14 @@ class WebSocketClient {
suspend fun connect( suspend fun connect(
conversationId: String, conversationId: String,
token: String? = null, token: String? = null,
onMessage: (WebSocketMessage) -> Unit onMessage: (WebSocketMessage) -> Unit,
onError: ((String) -> Unit)? = null
) { ) {
// 如果已连接,先断开
if (isConnected) {
disconnect()
}
val baseUrl = "$BASE_URL/ws/conversation/$conversationId" val baseUrl = "$BASE_URL/ws/conversation/$conversationId"
val url = if (token != null) { val url = if (token != null) {
"$baseUrl?token=$token" "$baseUrl?token=$token"
@@ -58,61 +67,141 @@ class WebSocketClient {
baseUrl baseUrl
} }
onErrorCallback = onError
Log.d(TAG, "开始连接WebSocket: $url")
try { try {
// 建立WebSocket连接
session = client.webSocketSession { session = client.webSocketSession {
url { url {
takeFrom(url) takeFrom(url)
} }
} }
Log.d(TAG, "WebSocket session创建成功")
currentConversationId = conversationId currentConversationId = conversationId
currentToken = token currentToken = token
isConnected = true
// 启动消息接收协程 // 启动消息接收协程在设置isConnected之前
scope.launch { val receiveJob = scope.launch {
receiveMessages(onMessage) receiveMessages(onMessage)
} }
// 发送连接消息 // 等待一小段时间确保连接建立
sendMessage(WebSocketMessage( delay(100)
type = MessageType.connect,
conversation_id = conversationId, // 检查session是否有效
data = buildJsonObject { put("status", JsonPrimitive("connected")) } if (session == null || session?.isActive != true) {
)) Log.e(TAG, "WebSocket连接失败session无效")
throw Exception("WebSocket连接失败session无效")
}
Log.d(TAG, "WebSocket session有效准备发送连接消息")
// 发送连接消息不等待确认由服务器返回connect消息时再设置isConnected
try {
sendMessage(WebSocketMessage(
type = MessageType.connect,
conversation_id = conversationId,
data = buildJsonObject { put("status", JsonPrimitive("connected")) }
))
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) { } catch (e: Exception) {
Log.e(TAG, "WebSocket连接异常: ${e.message}", e)
isConnected = false isConnected = false
session?.close()
session = null
onErrorCallback?.invoke("连接失败: ${e.message}")
throw e throw e
} }
} }
private suspend fun receiveMessages(onMessage: (WebSocketMessage) -> Unit) { private suspend fun receiveMessages(onMessage: (WebSocketMessage) -> Unit) {
try { try {
while (isConnected) { // 持续接收消息,直到连接关闭
while (true) {
val frame = session?.incoming?.receive() as? Frame.Text val frame = session?.incoming?.receive() as? Frame.Text
?: break ?: break
val message = Json.decodeFromString<WebSocketMessage>(frame.readText()) try {
onMessage(message) val text = frame.readText()
messageFlow.emit(message) Log.d(TAG, "收到WebSocket消息: $text")
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") ?: "未知错误"
Log.e(TAG, "收到错误消息: $errorMsg")
onErrorCallback?.invoke(errorMsg)
}
Log.d(TAG, "处理消息类型: ${message.type}")
onMessage(message)
messageFlow.emit(message)
} catch (e: Exception) {
// JSON解析失败记录错误但继续接收
Log.e(TAG, "消息解析失败: ${e.message}", e)
onErrorCallback?.invoke("消息解析失败: ${e.message}")
}
} }
} catch (_: Exception) { } catch (e: Exception) {
// 连接异常
Log.e(TAG, "接收消息异常: ${e.message}", e)
isConnected = false isConnected = false
val errorMsg = "连接异常: ${e.message}"
onErrorCallback?.invoke(errorMsg)
// 触发重连 // 触发重连
reconnectJob?.cancel() reconnectJob?.cancel()
reconnectJob = scope.launch { reconnectJob = scope.launch {
Log.d(TAG, "开始重连...")
reconnectWithBackoff(onMessage) reconnectWithBackoff(onMessage)
} }
} }
} }
suspend fun sendMessage(message: WebSocketMessage) { suspend fun sendMessage(message: WebSocketMessage) {
if (!isConnected && message.type != MessageType.connect) {
Log.w(TAG, "尝试发送消息但未连接: ${message.type}")
throw Exception("WebSocket未连接无法发送消息")
}
if (session == null || session?.isActive != true) {
Log.w(TAG, "WebSocket会话无效")
isConnected = false
throw Exception("WebSocket会话无效")
}
try { try {
val json = Json.encodeToString(WebSocketMessage.serializer(), message) val json = Json.encodeToString(WebSocketMessage.serializer(), message)
Log.d(TAG, "发送消息: ${message.type}, 内容: $json")
session?.send(Frame.Text(json)) session?.send(Frame.Text(json))
Log.d(TAG, "消息发送成功: ${message.type}")
} catch (e: Exception) { } catch (e: Exception) {
// 处理发送失败 // 发送失败,可能连接已断开
Log.e(TAG, "发送消息失败: ${e.message}", e)
isConnected = false
onErrorCallback?.invoke("发送消息失败: ${e.message}")
throw e throw e
} }
} }
@@ -127,6 +216,12 @@ class WebSocketClient {
} }
suspend fun sendTextMessage(text: String, conversationId: String) { suspend fun sendTextMessage(text: String, conversationId: String) {
Log.d(TAG, "准备发送文本消息: $text")
if (!isConnected) {
Log.w(TAG, "WebSocket未连接无法发送文本消息")
throw Exception("WebSocket未连接请先建立连接")
}
sendMessage(WebSocketMessage( sendMessage(WebSocketMessage(
type = MessageType.text, type = MessageType.text,
conversation_id = conversationId, conversation_id = conversationId,
@@ -168,6 +263,7 @@ class WebSocketClient {
} }
suspend fun disconnect() { suspend fun disconnect() {
Log.d(TAG, "断开WebSocket连接")
isConnected = false isConnected = false
isGenerating = false isGenerating = false
reconnectJob?.cancel() reconnectJob?.cancel()
@@ -176,6 +272,7 @@ class WebSocketClient {
session = null session = null
currentConversationId = null currentConversationId = null
currentToken = null currentToken = null
Log.d(TAG, "WebSocket连接已断开")
} }
private suspend fun reconnectWithBackoff( private suspend fun reconnectWithBackoff(

View File

@@ -48,8 +48,23 @@ data class UserResponse(
val created_at: String val created_at: String
) )
// 错误响应 // 错误响应 - 支持简单字符串格式
@Serializable @Serializable
data class ErrorResponse( data class ErrorResponse(
val detail: String val detail: String
) )
// FastAPI验证错误详情
@Serializable
data class ValidationErrorDetail(
val type: String,
val loc: List<String>,
val msg: String,
val input: String? = null
)
// FastAPI验证错误响应422
@Serializable
data class ValidationErrorResponse(
val detail: List<ValidationErrorDetail>
)

View File

@@ -29,6 +29,15 @@ data class MessageDto(
val messageType: String = "text" // "text", "audio", "image" val messageType: String = "text" // "text", "audio", "image"
) )
// 创建对话响应DTO
@Serializable
data class CreateConversationResponse(
val id: String,
val user_id: String,
val started_at: String,
val status: String
)
// 对话详情DTO // 对话详情DTO
@Serializable @Serializable
data class ConversationDetailDto( data class ConversationDetailDto(