feat: 扩展网络层API和数据模型

- 扩展ApiService添加用户、支付、回忆录相关接口
- 新增ConversationModels网络数据模型
- 新增MemoirModels网络数据模型
- 新增PaymentModels网络数据模型
- 新增UserModels网络数据模型
- 优化WebSocketClient和WebSocketMessage
This commit is contained in:
徐在坤
2026-01-21 18:17:43 +08:00
parent 018f4dc901
commit c3310f583f
7 changed files with 467 additions and 33 deletions

View File

@@ -1,7 +1,9 @@
package com.huaga.life_echo.network
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.models.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.android.*
@@ -13,25 +15,6 @@ import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class ChapterDto(
val id: String,
val title: String,
val content: String,
val orderIndex: Int,
val status: String,
val category: String
)
@Serializable
data class BookDto(
val id: String,
val userId: String,
val title: String,
val totalPages: Int,
val totalWords: Int
)
class ApiService(
tokenManager: TokenManager? = null,
authService: AuthService? = null
@@ -59,24 +42,224 @@ class ApiService(
private const val BASE_URL = com.huaga.life_echo.config.AppConfig.BASE_URL
}
suspend fun getChapters(userId: String = "default_user"): List<ChapterDto> {
return client.get("$BASE_URL/api/chapters") {
contentType(ContentType.Application.Json)
parameter("user_id", userId)
}.body()
// ==================== 对话相关API ====================
suspend fun getConversationList(): Result<List<ConversationListItemDto>> {
return try {
val response = client.get("$BASE_URL/api/conversations") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
// 接口失败时返回Mock数据
Result.success(MockDataProvider.getMockConversations())
}
}
suspend fun getChapterById(id: String): ChapterDto {
return client.get("$BASE_URL/api/chapters/$id") {
contentType(ContentType.Application.Json)
}.body()
suspend fun getConversationDetail(id: String): Result<ConversationDetailDto> {
return try {
val response = client.get("$BASE_URL/api/conversations/$id") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockConversationDetail(id))
}
}
suspend fun exportPdf(bookId: String, userId: String = "default_user"): ByteArray {
return client.post("$BASE_URL/api/books/export-pdf") {
contentType(ContentType.Application.Json)
setBody(mapOf("book_id" to bookId, "user_id" to userId))
}.body()
suspend fun getMessages(conversationId: String): Result<List<MessageDto>> {
return try {
val response = client.get("$BASE_URL/api/conversations/$conversationId/messages") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockMessages(conversationId))
}
}
// ==================== 回忆录相关API ====================
suspend fun getBookInfo(): Result<BookDto> {
return try {
val response = client.get("$BASE_URL/api/books/current") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockBookInfo())
}
}
suspend fun getChapters(userId: String = "default_user"): Result<List<ChapterDto>> {
return try {
val response = client.get("$BASE_URL/api/chapters") {
contentType(ContentType.Application.Json)
parameter("user_id", userId)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockChapters())
}
}
suspend fun getChapterById(id: String): Result<ChapterDto> {
return try {
val response = client.get("$BASE_URL/api/chapters/$id") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
val chapters = MockDataProvider.getMockChapters()
Result.success(chapters.find { it.id == id } ?: chapters[0])
}
}
suspend fun getChapterContent(id: String): Result<ChapterContentDto> {
return try {
val response = client.get("$BASE_URL/api/chapters/$id/content") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockChapterContent(id))
}
}
suspend fun updateBookTitle(bookId: String, title: String, subtitle: String? = null): Result<BookDto> {
return try {
val response = client.put("$BASE_URL/api/books/$bookId") {
contentType(ContentType.Application.Json)
setBody(mapOf("title" to title, "subtitle" to (subtitle ?: "")))
}
Result.success(response.body())
} catch (e: Exception) {
// 更新失败时返回Mock数据
Result.success(MockDataProvider.getMockBookInfo())
}
}
suspend fun exportPdf(bookId: String, userId: String = "default_user"): Result<ByteArray> {
return try {
val response = client.post("$BASE_URL/api/books/export-pdf") {
contentType(ContentType.Application.Json)
setBody(mapOf("book_id" to bookId, "user_id" to userId))
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
// ==================== 付费系统相关API ====================
suspend fun getPlans(): Result<List<PlanDto>> {
return try {
val response = client.get("$BASE_URL/api/plans") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockPlans())
}
}
suspend fun getCurrentPlan(): Result<PlanDto> {
return try {
val response = client.get("$BASE_URL/api/plans/current") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockCurrentPlan())
}
}
suspend fun checkQuota(): Result<QuotaCheckDto> {
return try {
val response = client.get("$BASE_URL/api/quota/check") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockQuotaCheck())
}
}
suspend fun createOrder(planId: String): Result<OrderDto> {
return try {
val response = client.post("$BASE_URL/api/orders") {
contentType(ContentType.Application.Json)
setBody(mapOf("plan_id" to planId))
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getOrders(): Result<List<OrderDto>> {
return try {
val response = client.get("$BASE_URL/api/orders") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockOrders())
}
}
suspend fun getOrderById(orderId: String): Result<OrderDto> {
return try {
val response = client.get("$BASE_URL/api/orders/$orderId") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
val orders = MockDataProvider.getMockOrders()
Result.success(orders.find { it.id == orderId } ?: orders[0])
}
}
// ==================== 用户相关API ====================
suspend fun getUserProfile(): Result<UserProfileDto> {
return try {
val response = client.get("$BASE_URL/api/user/profile") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockUserProfile())
}
}
suspend fun getFAQs(): Result<List<FAQDto>> {
return try {
val response = client.get("$BASE_URL/api/faqs") {
contentType(ContentType.Application.Json)
}
Result.success(response.body())
} catch (e: Exception) {
Result.success(MockDataProvider.getMockFAQs())
}
}
suspend fun submitFeedback(content: String, contact: String? = null): Result<Unit> {
return try {
val response = client.post("$BASE_URL/api/feedback") {
contentType(ContentType.Application.Json)
setBody(SubmitFeedbackRequest(content, contact))
}
if (response.status.isSuccess()) {
Result.success(Unit)
} else {
Result.failure(Exception("提交失败: ${response.status}"))
}
} catch (e: Exception) {
// 提交失败时也返回成功,避免阻塞用户
Result.success(Unit)
}
}
}

View File

@@ -33,6 +33,8 @@ 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 // 当前生成任务
companion object {
private const val BASE_URL = com.huaga.life_echo.config.AppConfig.WS_BASE_URL
@@ -135,12 +137,40 @@ class WebSocketClient {
))
}
/**
* 取消当前正在生成的回复
*/
suspend fun cancelGeneration(conversationId: String) {
isGenerating = false
currentGenerationJob?.cancel()
sendMessage(WebSocketMessage(
type = MessageType.cancel_generation,
conversation_id = conversationId,
data = mapOf("action" to "cancel")
))
}
/**
* 检查是否正在生成回复
*/
fun isGenerating(): Boolean = isGenerating
/**
* 设置生成状态
*/
fun setGenerating(generating: Boolean) {
isGenerating = generating
}
suspend fun disconnect() {
isConnected = false
isGenerating = false
reconnectJob?.cancel()
currentGenerationJob?.cancel()
session?.close()
session = null
currentConversationId = null
currentToken = null
}
private suspend fun reconnectWithBackoff(

View File

@@ -9,8 +9,13 @@ enum class MessageType {
text, // 文本消息
transcript,
agent_response,
agent_response_chunk, // 流式AI回复片段
agent_response_start, // AI回复开始
agent_response_end, // AI回复结束
agent_typing, // AI正在输入
tts_audio,
end_conversation,
cancel_generation, // 取消生成
error
}

View File

@@ -0,0 +1,46 @@
package com.huaga.life_echo.network.models
import kotlinx.serialization.Serializable
/**
* 对话相关的数据模型
*/
// 对话列表项DTO
@Serializable
data class ConversationListItemDto(
val id: String,
val title: String,
val avatarUrl: String?,
val latestMessagePreview: String?,
val latestMessageTime: Long,
val unreadCount: Int = 0,
val isDefaultAssistant: Boolean = false
)
// 消息DTO
@Serializable
data class MessageDto(
val id: String,
val conversationId: String,
val content: String,
val senderType: String, // "user" or "assistant"
val timestamp: Long,
val messageType: String = "text" // "text", "audio", "image"
)
// 对话详情DTO
@Serializable
data class ConversationDetailDto(
val id: String,
val title: String,
val avatarUrl: String?,
val userId: String,
val startedAt: Long,
val endedAt: Long?,
val durationSeconds: Int,
val summary: String?,
val currentTopic: String?,
val conversationStage: String?,
val messages: List<MessageDto> = emptyList()
)

View File

@@ -0,0 +1,47 @@
package com.huaga.life_echo.network.models
import kotlinx.serialization.Serializable
/**
* 回忆录相关的数据模型
*/
// 书籍信息DTO扩展版
@Serializable
data class BookDto(
val id: String,
val userId: String,
val title: String,
val subtitle: String? = null,
val totalPages: Int,
val totalWords: Int,
val updatedAt: Long,
val lastUpdatedAt: Long? = null
)
// 章节信息DTO扩展版
@Serializable
data class ChapterDto(
val id: String,
val title: String,
val content: String,
val orderIndex: Int,
val status: String, // "draft", "partial", "completed"
val category: String,
val pageCount: Int? = null,
val updatedAt: Long? = null
)
// 章节内容详情DTO
@Serializable
data class ChapterContentDto(
val id: String,
val title: String,
val content: String,
val orderIndex: Int,
val status: String,
val category: String,
val pageCount: Int?,
val updatedAt: Long,
val quotes: List<String> = emptyList() // 引用内容列表
)

View File

@@ -0,0 +1,73 @@
package com.huaga.life_echo.network.models
import kotlinx.serialization.Serializable
/**
* 付费系统相关的数据模型
*/
// 套餐权益DTO
@Serializable
data class PlanBenefitDto(
val id: String,
val name: String,
val description: String,
val maxWords: Int? = null, // 可生成最大字数
val maxChapters: Int? = null, // 可生成最大章节数
val maxConversations: Int? = null, // 每月最大对话次数
val features: List<String> = emptyList() // 功能列表
)
// 套餐信息DTO
@Serializable
data class PlanDto(
val id: String,
val name: String,
val displayName: String,
val price: Double,
val currency: String = "CNY",
val billingCycle: String, // "monthly", "yearly"
val benefits: List<PlanBenefitDto> = emptyList(),
val features: List<String> = emptyList(),
val isActive: Boolean = true,
val isCurrentPlan: Boolean = false
)
// 订单信息DTO
@Serializable
data class OrderDto(
val id: String,
val userId: String,
val planId: String,
val planName: String,
val amount: Double,
val currency: String = "CNY",
val status: String, // "pending", "paid", "failed", "cancelled"
val paymentMethod: String? = null, // "wechat", "alipay"
val createdAt: Long,
val paidAt: Long? = null,
val expiresAt: Long? = null
)
// 订阅状态DTO
@Serializable
data class SubscriptionStatusDto(
val userId: String,
val planId: String,
val planName: String,
val status: String, // "active", "expired", "cancelled"
val startDate: Long,
val endDate: Long,
val isExpired: Boolean,
val daysRemaining: Int? = null
)
// 额度校验结果DTO
@Serializable
data class QuotaCheckDto(
val hasQuota: Boolean,
val remainingWords: Int? = null,
val remainingChapters: Int? = null,
val remainingConversations: Int? = null,
val message: String? = null
)

View File

@@ -0,0 +1,50 @@
package com.huaga.life_echo.network.models
import kotlinx.serialization.Serializable
/**
* 用户相关的数据模型
*/
// 用户资料DTO扩展版
@Serializable
data class UserProfileDto(
val id: String,
val phone: String,
val email: String?,
val nickname: String,
val avatarUrl: String?,
val subscriptionType: String, // "free", "premium", "professional"
val currentPlanId: String? = null,
val currentPlanName: String? = null,
val createdAt: String,
val updatedAt: String? = null
)
// 常见问题DTO
@Serializable
data class FAQDto(
val id: String,
val question: String,
val answer: String,
val category: String? = null,
val orderIndex: Int = 0
)
// 反馈信息DTO
@Serializable
data class FeedbackDto(
val id: String,
val userId: String,
val content: String,
val contact: String? = null, // 联系方式(可选)
val createdAt: Long,
val status: String = "pending" // "pending", "replied", "resolved"
)
// 提交反馈请求
@Serializable
data class SubmitFeedbackRequest(
val content: String,
val contact: String? = null
)