feat: 扩展网络层API和数据模型
- 扩展ApiService添加用户、支付、回忆录相关接口 - 新增ConversationModels网络数据模型 - 新增MemoirModels网络数据模型 - 新增PaymentModels网络数据模型 - 新增UserModels网络数据模型 - 优化WebSocketClient和WebSocketMessage
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
@@ -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() // 引用内容列表
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user