新增: 抽取 Memoir、Profile、Payment REST 端口
按功能划分的 API 端口接口将仓库和 ViewModel 与 ApiService 解耦。 适配器在底层委托给 ApiService;所有依赖由 AppContainer 统一装配。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
package com.huaga.life_echo.data.repository
|
||||
|
||||
import com.huaga.life_echo.network.ApiService
|
||||
import com.huaga.life_echo.feature.payment.ports.PaymentApiPort
|
||||
import com.huaga.life_echo.network.models.CreatePaymentOrderResponse
|
||||
import com.huaga.life_echo.network.models.CurrentPlanDto
|
||||
import com.huaga.life_echo.network.models.OrderDto
|
||||
@@ -10,62 +10,48 @@ import com.huaga.life_echo.network.models.QuotaCheckDto
|
||||
import com.huaga.life_echo.network.models.TestSubscriptionResponse
|
||||
|
||||
class PaymentRepository(
|
||||
private val apiService: ApiService
|
||||
private val paymentApi: PaymentApiPort
|
||||
) {
|
||||
suspend fun getPlans(): Result<List<PlanDto>> {
|
||||
return apiService.getPlans()
|
||||
return paymentApi.getPlans()
|
||||
}
|
||||
|
||||
suspend fun getCurrentPlan(): Result<CurrentPlanDto> {
|
||||
return apiService.getCurrentPlan()
|
||||
return paymentApi.getCurrentPlan()
|
||||
}
|
||||
|
||||
suspend fun checkQuota(): Result<QuotaCheckDto> {
|
||||
return apiService.checkQuota()
|
||||
return paymentApi.checkQuota()
|
||||
}
|
||||
|
||||
suspend fun createOrder(planId: String): Result<OrderDto> {
|
||||
return apiService.createOrder(planId)
|
||||
return paymentApi.createOrder(planId)
|
||||
}
|
||||
|
||||
suspend fun getOrders(): Result<List<OrderDto>> {
|
||||
return apiService.getOrders()
|
||||
return paymentApi.getOrders()
|
||||
}
|
||||
|
||||
suspend fun getOrderById(orderId: String): Result<OrderDto> {
|
||||
return apiService.getOrderById(orderId)
|
||||
return paymentApi.getOrderById(orderId)
|
||||
}
|
||||
|
||||
// ==================== 支付订单(微信/支付宝) ====================
|
||||
|
||||
/**
|
||||
* 创建支付订单(获取支付参数)
|
||||
*/
|
||||
suspend fun createPaymentOrder(
|
||||
planId: String,
|
||||
paymentMethod: String
|
||||
): Result<CreatePaymentOrderResponse> {
|
||||
return apiService.createPaymentOrder(planId, paymentMethod)
|
||||
return paymentApi.createPaymentOrder(planId, paymentMethod)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付订单状态
|
||||
*/
|
||||
suspend fun getPaymentOrderStatus(orderId: String): Result<PaymentOrderStatusResponse> {
|
||||
return apiService.getPaymentOrderStatus(orderId)
|
||||
return paymentApi.getPaymentOrderStatus(orderId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付订单列表
|
||||
*/
|
||||
suspend fun getPaymentOrders(): Result<List<PaymentOrderStatusResponse>> {
|
||||
return apiService.getPaymentOrders()
|
||||
return paymentApi.getPaymentOrders()
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试订阅开关(仅当服务端开启时可用)
|
||||
*/
|
||||
suspend fun setTestSubscription(action: String, planId: String = "pro"): Result<TestSubscriptionResponse> {
|
||||
return apiService.setTestSubscription(action, planId)
|
||||
return paymentApi.setTestSubscription(action, planId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package com.huaga.life_echo.data.repository
|
||||
|
||||
import com.huaga.life_echo.network.ApiService
|
||||
import com.huaga.life_echo.feature.profile.ports.ProfileApiPort
|
||||
import com.huaga.life_echo.network.models.FAQDto
|
||||
import com.huaga.life_echo.network.models.UserProfileDto
|
||||
|
||||
class ProfileRepository(
|
||||
private val apiService: ApiService
|
||||
private val profileApi: ProfileApiPort
|
||||
) {
|
||||
suspend fun getUserProfile(): Result<UserProfileDto> {
|
||||
return apiService.getUserProfile()
|
||||
return profileApi.getUserProfile()
|
||||
}
|
||||
|
||||
suspend fun getFAQs(): Result<List<FAQDto>> {
|
||||
return apiService.getFAQs()
|
||||
return profileApi.getFAQs()
|
||||
}
|
||||
|
||||
suspend fun submitFeedback(content: String, contact: String?): Result<Unit> {
|
||||
return apiService.submitFeedback(content, contact)
|
||||
return profileApi.submitFeedback(content, contact)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.huaga.life_echo.feature.memoir.adapters
|
||||
|
||||
import com.huaga.life_echo.feature.memoir.ports.MemoirApiPort
|
||||
import com.huaga.life_echo.network.ApiService
|
||||
import com.huaga.life_echo.network.models.BookDto
|
||||
import com.huaga.life_echo.network.models.ChapterContentDto
|
||||
import com.huaga.life_echo.network.models.ChapterDto
|
||||
import com.huaga.life_echo.network.models.MemoirStateDto
|
||||
import com.huaga.life_echo.network.models.TasksStatusDto
|
||||
|
||||
class MemoirApiAdapter(
|
||||
private val apiService: ApiService,
|
||||
) : MemoirApiPort {
|
||||
override suspend fun getBookInfo(): Result<BookDto> = apiService.getBookInfo()
|
||||
override suspend fun getChapters(): Result<List<ChapterDto>> = apiService.getChapters()
|
||||
override suspend fun getChapterById(id: String): Result<ChapterDto> = apiService.getChapterById(id)
|
||||
override suspend fun getChapterContent(id: String): Result<ChapterContentDto> = apiService.getChapterContent(id)
|
||||
override suspend fun updateBookTitle(bookId: String, title: String, subtitle: String?): Result<BookDto> =
|
||||
apiService.updateBookTitle(bookId, title, subtitle)
|
||||
override suspend fun disableChapter(chapterId: String): Result<Unit> = apiService.disableChapter(chapterId)
|
||||
override suspend fun exportPdf(bookId: String): Result<ByteArray> = apiService.exportPdf(bookId)
|
||||
override suspend fun getMemoirState(): Result<MemoirStateDto> = apiService.getMemoirState()
|
||||
override suspend fun getTasksStatus(): Result<TasksStatusDto> = apiService.getTasksStatus()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.huaga.life_echo.feature.memoir.ports
|
||||
|
||||
import com.huaga.life_echo.network.models.BookDto
|
||||
import com.huaga.life_echo.network.models.ChapterContentDto
|
||||
import com.huaga.life_echo.network.models.ChapterDto
|
||||
import com.huaga.life_echo.network.models.MemoirStateDto
|
||||
import com.huaga.life_echo.network.models.TasksStatusDto
|
||||
|
||||
interface MemoirApiPort {
|
||||
suspend fun getBookInfo(): Result<BookDto>
|
||||
suspend fun getChapters(): Result<List<ChapterDto>>
|
||||
suspend fun getChapterById(id: String): Result<ChapterDto>
|
||||
suspend fun getChapterContent(id: String): Result<ChapterContentDto>
|
||||
suspend fun updateBookTitle(bookId: String, title: String, subtitle: String? = null): Result<BookDto>
|
||||
suspend fun disableChapter(chapterId: String): Result<Unit>
|
||||
suspend fun exportPdf(bookId: String): Result<ByteArray>
|
||||
suspend fun getMemoirState(): Result<MemoirStateDto>
|
||||
suspend fun getTasksStatus(): Result<TasksStatusDto>
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.huaga.life_echo.feature.payment.adapters
|
||||
|
||||
import com.huaga.life_echo.feature.payment.ports.PaymentApiPort
|
||||
import com.huaga.life_echo.network.ApiService
|
||||
import com.huaga.life_echo.network.models.CreatePaymentOrderResponse
|
||||
import com.huaga.life_echo.network.models.CurrentPlanDto
|
||||
import com.huaga.life_echo.network.models.OrderDto
|
||||
import com.huaga.life_echo.network.models.PaymentOrderStatusResponse
|
||||
import com.huaga.life_echo.network.models.PlanDto
|
||||
import com.huaga.life_echo.network.models.QuotaCheckDto
|
||||
import com.huaga.life_echo.network.models.TestSubscriptionResponse
|
||||
|
||||
class PaymentApiAdapter(
|
||||
private val apiService: ApiService,
|
||||
) : PaymentApiPort {
|
||||
override suspend fun getPlans(): Result<List<PlanDto>> = apiService.getPlans()
|
||||
override suspend fun getCurrentPlan(): Result<CurrentPlanDto> = apiService.getCurrentPlan()
|
||||
override suspend fun checkQuota(): Result<QuotaCheckDto> = apiService.checkQuota()
|
||||
override suspend fun createOrder(planId: String): Result<OrderDto> = apiService.createOrder(planId)
|
||||
override suspend fun getOrders(): Result<List<OrderDto>> = apiService.getOrders()
|
||||
override suspend fun getOrderById(orderId: String): Result<OrderDto> = apiService.getOrderById(orderId)
|
||||
override suspend fun createPaymentOrder(planId: String, paymentMethod: String): Result<CreatePaymentOrderResponse> =
|
||||
apiService.createPaymentOrder(planId, paymentMethod)
|
||||
override suspend fun getPaymentOrderStatus(orderId: String): Result<PaymentOrderStatusResponse> =
|
||||
apiService.getPaymentOrderStatus(orderId)
|
||||
override suspend fun getPaymentOrders(): Result<List<PaymentOrderStatusResponse>> = apiService.getPaymentOrders()
|
||||
override suspend fun setTestSubscription(action: String, planId: String): Result<TestSubscriptionResponse> =
|
||||
apiService.setTestSubscription(action, planId)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.huaga.life_echo.feature.payment.ports
|
||||
|
||||
import com.huaga.life_echo.network.models.CreatePaymentOrderResponse
|
||||
import com.huaga.life_echo.network.models.CurrentPlanDto
|
||||
import com.huaga.life_echo.network.models.OrderDto
|
||||
import com.huaga.life_echo.network.models.PaymentOrderStatusResponse
|
||||
import com.huaga.life_echo.network.models.PlanDto
|
||||
import com.huaga.life_echo.network.models.QuotaCheckDto
|
||||
import com.huaga.life_echo.network.models.TestSubscriptionResponse
|
||||
|
||||
interface PaymentApiPort {
|
||||
suspend fun getPlans(): Result<List<PlanDto>>
|
||||
suspend fun getCurrentPlan(): Result<CurrentPlanDto>
|
||||
suspend fun checkQuota(): Result<QuotaCheckDto>
|
||||
suspend fun createOrder(planId: String): Result<OrderDto>
|
||||
suspend fun getOrders(): Result<List<OrderDto>>
|
||||
suspend fun getOrderById(orderId: String): Result<OrderDto>
|
||||
suspend fun createPaymentOrder(planId: String, paymentMethod: String): Result<CreatePaymentOrderResponse>
|
||||
suspend fun getPaymentOrderStatus(orderId: String): Result<PaymentOrderStatusResponse>
|
||||
suspend fun getPaymentOrders(): Result<List<PaymentOrderStatusResponse>>
|
||||
suspend fun setTestSubscription(action: String, planId: String = "pro"): Result<TestSubscriptionResponse>
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.huaga.life_echo.feature.profile.adapters
|
||||
|
||||
import com.huaga.life_echo.feature.profile.ports.ProfileApiPort
|
||||
import com.huaga.life_echo.network.ApiService
|
||||
import com.huaga.life_echo.network.models.FAQDto
|
||||
import com.huaga.life_echo.network.models.UserProfileDto
|
||||
|
||||
class ProfileApiAdapter(
|
||||
private val apiService: ApiService,
|
||||
) : ProfileApiPort {
|
||||
override suspend fun getUserProfile(): Result<UserProfileDto> = apiService.getUserProfile()
|
||||
override suspend fun getFAQs(): Result<List<FAQDto>> = apiService.getFAQs()
|
||||
override suspend fun submitFeedback(content: String, contact: String?): Result<Unit> =
|
||||
apiService.submitFeedback(content, contact)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.huaga.life_echo.feature.profile.ports
|
||||
|
||||
import com.huaga.life_echo.network.models.FAQDto
|
||||
import com.huaga.life_echo.network.models.UserProfileDto
|
||||
|
||||
interface ProfileApiPort {
|
||||
suspend fun getUserProfile(): Result<UserProfileDto>
|
||||
suspend fun getFAQs(): Result<List<FAQDto>>
|
||||
suspend fun submitFeedback(content: String, contact: String?): Result<Unit>
|
||||
}
|
||||
@@ -6,18 +6,16 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.huaga.life_echo.data.database.Chapter
|
||||
import com.huaga.life_echo.data.repository.ChapterRepository
|
||||
import com.huaga.life_echo.network.ApiService
|
||||
import com.huaga.life_echo.feature.memoir.ports.MemoirApiPort
|
||||
import com.huaga.life_echo.network.models.MemoirStateDto
|
||||
import com.huaga.life_echo.network.models.TasksStatusDto
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MyMemoirViewModel(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val apiService: ApiService
|
||||
private val memoirApi: MemoirApiPort,
|
||||
) : ViewModel() {
|
||||
|
||||
val chapters = chapterRepository.getAllChapters()
|
||||
@@ -42,7 +40,7 @@ class MyMemoirViewModel(
|
||||
viewModelScope.launch {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
apiService.exportPdf(bookId).fold(
|
||||
memoirApi.exportPdf(bookId).fold(
|
||||
onSuccess = { pdfBytes ->
|
||||
isLoading.value = false
|
||||
onSuccess(pdfBytes)
|
||||
@@ -63,13 +61,11 @@ class MyMemoirViewModel(
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
val result = apiService.getChapters()
|
||||
val result = memoirApi.getChapters()
|
||||
result.fold(
|
||||
onSuccess = { chapterDtosFromApi ->
|
||||
// 保存ChapterDto(包含images信息)
|
||||
chapterDtos.value = chapterDtosFromApi
|
||||
|
||||
// 转换为本地Chapter实体并保存
|
||||
val chapters = chapterDtosFromApi.map { dto ->
|
||||
Chapter(
|
||||
id = dto.id,
|
||||
@@ -102,15 +98,12 @@ class MyMemoirViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载回忆录状态
|
||||
*/
|
||||
fun loadMemoirState() {
|
||||
viewModelScope.launch {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
val result = apiService.getMemoirState()
|
||||
val result = memoirApi.getMemoirState()
|
||||
result.fold(
|
||||
onSuccess = { state ->
|
||||
memoirState.value = state
|
||||
@@ -127,13 +120,10 @@ class MyMemoirViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查任务状态
|
||||
*/
|
||||
fun checkTasksStatus() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = apiService.getTasksStatus()
|
||||
val result = memoirApi.getTasksStatus()
|
||||
result.fold(
|
||||
onSuccess = { status ->
|
||||
tasksStatus.value = status
|
||||
@@ -150,7 +140,7 @@ class MyMemoirViewModel(
|
||||
|
||||
fun loadBookInfo() {
|
||||
viewModelScope.launch {
|
||||
apiService.getBookInfo().fold(
|
||||
memoirApi.getBookInfo().fold(
|
||||
onSuccess = { bookInfo.value = it },
|
||||
onFailure = { }
|
||||
)
|
||||
@@ -160,7 +150,7 @@ class MyMemoirViewModel(
|
||||
fun updateBookTitle(title: String, subtitle: String?) {
|
||||
viewModelScope.launch {
|
||||
bookInfo.value?.let { book ->
|
||||
apiService.updateBookTitle(book.id, title, subtitle).fold(
|
||||
memoirApi.updateBookTitle(book.id, title, subtitle).fold(
|
||||
onSuccess = { bookInfo.value = it },
|
||||
onFailure = { }
|
||||
)
|
||||
@@ -172,20 +162,14 @@ class MyMemoirViewModel(
|
||||
showFullTextReading.value = !showFullTextReading.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除章节(将章节标记为 disabled)
|
||||
* 成功后刷新章节列表并清除选中状态
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun disableChapter(chapterId: String, onSuccess: () -> Unit = {}, onError: (String) -> Unit = {}) {
|
||||
viewModelScope.launch {
|
||||
isLoading.value = true
|
||||
try {
|
||||
apiService.disableChapter(chapterId).fold(
|
||||
memoirApi.disableChapter(chapterId).fold(
|
||||
onSuccess = {
|
||||
// 清除选中状态,回到目录
|
||||
clearSelection()
|
||||
// 刷新章节列表
|
||||
refreshChapters()
|
||||
onSuccess()
|
||||
},
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.huaga.life_echo.ui.viewmodel
|
||||
|
||||
import com.huaga.life_echo.data.database.Chapter
|
||||
import com.huaga.life_echo.data.database.ChapterDao
|
||||
import com.huaga.life_echo.data.repository.ChapterRepository
|
||||
import com.huaga.life_echo.feature.memoir.ports.MemoirApiPort
|
||||
import com.huaga.life_echo.network.models.BookDto
|
||||
import com.huaga.life_echo.network.models.ChapterContentDto
|
||||
import com.huaga.life_echo.network.models.ChapterDto
|
||||
import com.huaga.life_echo.network.models.MemoirStateDto
|
||||
import com.huaga.life_echo.network.models.TasksStatusDto
|
||||
import com.huaga.life_echo.testutil.MainDispatcherRule
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MyMemoirViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun refresh_chapters_uses_memoir_port_instead_of_raw_api_service() =
|
||||
runTest(mainDispatcherRule.dispatcher.scheduler) {
|
||||
val memoirApi = FakeMemoirApiPort(chaptersResult = Result.success(emptyList()))
|
||||
val viewModel = MyMemoirViewModel(
|
||||
chapterRepository = ChapterRepository(chapterDao = FakeChapterDao()),
|
||||
memoirApi = memoirApi,
|
||||
)
|
||||
|
||||
viewModel.refreshChapters()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(1, memoirApi.getChaptersCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun load_book_info_uses_memoir_port() =
|
||||
runTest(mainDispatcherRule.dispatcher.scheduler) {
|
||||
val book = BookDto(id = "b1", title = "Test Book")
|
||||
val memoirApi = FakeMemoirApiPort(bookInfoResult = Result.success(book))
|
||||
val viewModel = MyMemoirViewModel(
|
||||
chapterRepository = ChapterRepository(chapterDao = FakeChapterDao()),
|
||||
memoirApi = memoirApi,
|
||||
)
|
||||
|
||||
viewModel.loadBookInfo()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(book, viewModel.bookInfo.value)
|
||||
}
|
||||
|
||||
private class FakeMemoirApiPort(
|
||||
private val chaptersResult: Result<List<ChapterDto>> = Result.success(emptyList()),
|
||||
private val bookInfoResult: Result<BookDto> = Result.failure(Exception("not set")),
|
||||
) : MemoirApiPort {
|
||||
var getChaptersCalls = 0
|
||||
private set
|
||||
|
||||
override suspend fun getBookInfo(): Result<BookDto> = bookInfoResult
|
||||
override suspend fun getChapters(): Result<List<ChapterDto>> {
|
||||
getChaptersCalls++
|
||||
return chaptersResult
|
||||
}
|
||||
override suspend fun getChapterById(id: String): Result<ChapterDto> =
|
||||
Result.failure(Exception("not implemented"))
|
||||
override suspend fun getChapterContent(id: String): Result<ChapterContentDto> =
|
||||
Result.failure(Exception("not implemented"))
|
||||
override suspend fun updateBookTitle(bookId: String, title: String, subtitle: String?): Result<BookDto> =
|
||||
Result.failure(Exception("not implemented"))
|
||||
override suspend fun disableChapter(chapterId: String): Result<Unit> =
|
||||
Result.failure(Exception("not implemented"))
|
||||
override suspend fun exportPdf(bookId: String): Result<ByteArray> =
|
||||
Result.failure(Exception("not implemented"))
|
||||
override suspend fun getMemoirState(): Result<MemoirStateDto> =
|
||||
Result.failure(Exception("not implemented"))
|
||||
override suspend fun getTasksStatus(): Result<TasksStatusDto> =
|
||||
Result.failure(Exception("not implemented"))
|
||||
}
|
||||
|
||||
private class FakeChapterDao : ChapterDao {
|
||||
override fun getAllChapters() = flowOf(emptyList<Chapter>())
|
||||
override suspend fun getChapterById(id: String): Chapter? = null
|
||||
override suspend fun getChaptersByCategory(category: String): List<Chapter> = emptyList()
|
||||
override suspend fun insertChapter(chapter: Chapter) = Unit
|
||||
override suspend fun insertChapters(chapters: List<Chapter>) = Unit
|
||||
override suspend fun updateChapter(chapter: Chapter) = Unit
|
||||
override suspend fun deleteChapter(chapter: Chapter) = Unit
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user