【业务场景架构实战】4. 支付状态分层流转的设计和实现

"计算机科学中的所有问题都可以通过增加一个 间接层 来解决" ------ David Wheeler

设计目标

  • 层次清晰 :结构遵循 UI → ViewModel → Repository → SDK/Backend,并尽可能精炼。
  • 解耦:将数据和 UI 逻辑遵循同样细的粒度拆分。
  • 数据和控制单向流动:数据从 Data 流向 UI,控制则反之。
  • 状态收束:避免在每一层都引入状态变量。

支付状态流转

支付状态机图

节点表示当时的支付状态,箭头文字则表明状态流转的路径。

  • PaymentResult.XXX 表示支付 SDK 返回值。
  • ApiResult.XXX 是查询业务后台的结果。
stateDiagram-v2 [*] --> Idle Idle --> Created: createOrder() Created --> Paying: startPayment() Paying --> Confirming: PaymentResult.Success Paying --> Cancelled: PaymentResult.Cancel Paying --> Failed: PaymentResult.Failure Confirming --> Paid: ApiResult.Success Confirming --> Failed: ApiResult.Failure Paid --> [*] Failed --> [*] Cancelled --> [*]

支付状态机密封类

根据上述状态机图,借助密封类进行实现。

kotlin 复制代码
// 用密封类对边界进行约束
sealed interface OrderState {
    data object Idle : OrderState
    data object Created : OrderState
    data object Paying : OrderState
    data object Confirming : OrderState // 支付完成后轮询状态
    data object Paid : OrderState // 已落单
    data class Failed(val reason: String) : OrderState
    data object Cancelled : OrderState
}

Repository 统一管理支付状态

Repository 将支付 SDK 的状态和订单后台的状态进行统一收束后,只暴露一个 StateFlow (state)。

  • DataSource :集成支付 SDK 和订单后台,这两者相当于 DataSource 的角色。
  • 对外发射 state :唯一对外暴露的状态,统一通过 reduce 改变状态。
  • 使用封装后的 PaymentSdk :可以看到 sdk.pay() 函数也使用了 Flow 管理状态,将在下文进一步说明。
kotlin 复制代码
class OrderRepository(
    private val paymentSdk: PaymentSdkWrapper,
    private val api: OrderApi
) {
    private val _state = MutableStateFlow<OrderState>(OrderState.Idle)
    val state: StateFlow<OrderState> = _state.asStateFlow()

    suspend fun createOrder(params: Map<String, Any>) {
        reduce(OrderState.Created)
        // 可以在这里调用 api.createOrder(...)
    }

    suspend fun startPayment(orderId: String) {
        reduce(OrderState.Paying)

        paymentSdk.pay(orderId)
            .catch { e -> reduce(OrderState.Failed(e.message ?: "Unknown error")) }
            .collect { result ->
                when (result) {
                    PaymentResult.Success -> confirmOrder(orderId)
                    PaymentResult.Cancel -> reduce(OrderState.Cancelled)
                    is PaymentResult.Failure -> reduce(OrderState.Failed(result.reason))
                }
            }
    }

    private suspend fun confirmOrder(orderId: String) {
        reduce(OrderState.Confirming)

        when (val r = api.confirmOrder(orderId)) {
            is ApiResult.Success -> reduce(OrderState.Paid)
            is ApiResult.Failure -> reduce(OrderState.Failed(r.message))
        }
    }

    private fun reduce(newState: OrderState) {
        _state.value = newState
        println("OrderState -> $newState") // 方便调试日志
    }
}

支付 SDK 及订单后台 API 封装

将支付 SDK 原始的 异步回调 封装为 同步 Flow 方式,利用接口隐藏底层细节,支持替换具体实现。

kotlin 复制代码
// 支付 SDK 支付结果
sealed interface PaymentResult {
    data object Success : PaymentResult
    data object Cancel : PaymentResult
    data class Failure(val reason: String) : PaymentResult
}

interface PaymentSdkWrapper {
    fun pay(orderId: String): Flow<PaymentResult>
}

sealed interface ApiResult {
    data object Success : ApiResult
    data class Failure(val message: String) : ApiResult
}

interface OrderApi {
    suspend fun confirmOrder(orderId: String): ApiResult
}

ViewModel 层:映射为 UIState

ViewModel 只做两件事:

  • Repository 状态映射成 UIState:隐藏支付状态机细节,只提供终端用户关心的 UI 信息。
  • 对外暴露启动支付的入口。
kotlin 复制代码
data class PaymentUiState(
    val isLoading: Boolean = false,
    val message: String? = null,
    val isSuccess: Boolean = false
)

class PaymentViewModel(
    private val repo: OrderRepository
) : ViewModel() {

    val uiState: StateFlow<PaymentUiState> = repo.state
        .map { domain ->
            when (domain) {
                OrderState.Idle -> PaymentUiState()
                OrderState.Created -> PaymentUiState(isLoading = true, message = "Order created")
                OrderState.Paying -> PaymentUiState(isLoading = true, message = "Paying...")
                OrderState.Confirming -> PaymentUiState(isLoading = true, message = "Confirming...")
                OrderState.Paid -> PaymentUiState(isSuccess = true, message = "Payment successful")
                is OrderState.Failed -> PaymentUiState(message = "Failed: ${domain.reason}")
                OrderState.Cancelled -> PaymentUiState(message = "Payment cancelled")
            }
        }
        .stateIn(viewModelScope, SharingStarted.Eagerly, PaymentUiState()) // ===> 解释见下文

    fun createOrder(params: Map<String, Any>) {
        viewModelScope.launch { repo.createOrder(params) }
    }

    fun pay(orderId: String) {
        viewModelScope.launch { repo.startPayment(orderId) }
    }
}

.stateIn(viewModelScope, SharingStarted.Eagerly, PaymentUiState() 这段代码的详细说明如下:

  • stateIn:将冷流 Flow 转换为热流 StateFlow,不经订阅就可以启动,并且能记住最新值,当订阅者来到时,可以立即获取到最新值。(与之对应,冷流每一次订阅都会从0开始启动收集)
  • viewModelScope:将流与 VM 生命周期绑定,VM 销毁时,流也自动取消收集。
  • Eagerly :定义了从什么时候开始收集上游 Flow 的策略,常见策略有
    • Eagerly:ViewModel 一创建就立刻开始收集。
    • WhileSubscribed(...):只有当 UI 层订阅时才收集(比较省资源)。
    • Lazily:第一次订阅时才开始。
  • PaymentUiState():初始值。

UI 层:订阅状态并渲染

最后是负责将 UIState 投射为界面状态的 UI 层,这一层只对状态进行渲染,也会支持一些动画、弹窗、视觉效果等,但决不要掺杂业务逻辑。

kotlin 复制代码
class PaymentActivity : AppCompatActivity() {

    private val viewModel: PaymentViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    if (state.isLoading) showLoading(state.message)
                    else hideLoading()

                    state.message?.let { showToast(it) }

                    if (state.isSuccess) navigateToSuccessPage()
                }
            }
        }

        // 举例触发
        viewModel.createOrder(mapOf("amount" to 100))
        viewModel.pay("order-123")
    }
}

交互/UI 视角的状态流程图

描述用户视角的页面状态变化,相比于支付状态机,本图只保留用户能够感知到的状态,并隐藏后台验证等实现细节。可用于向 产品经理、设计师 讲解代码逻辑。

stateDiagram-v2 [*] --> Idle: 页面初始化 Idle --> Loading: 点击下单 Loading --> AwaitingPayment: 订单创建成功 Loading --> Error: 订单创建失败 AwaitingPayment --> Paying: 用户跳转支付 Paying --> Success: 支付成功并确认 Paying --> Failed: 支付失败 Paying --> Cancelled: 用户取消 Error --> [*] Cancelled --> [*] Success --> [*] Failed --> [*]
相关推荐
可触的未来,发芽的智生3 小时前
触摸未来2025.10.04:当神经网络拥有了内在记忆……
人工智能·python·神经网络·算法·架构
夜月yeyue4 小时前
个人写HTOS移植shell
c++·mcu·算法·性能优化·架构·mfc
brzhang5 小时前
为什么 OpenAI 不让 LLM 生成 UI?深度解析 OpenAI Apps SDK 背后的新一代交互范式
前端·后端·架构
brzhang5 小时前
OpenAI Apps SDK ,一个好的 App,不是让用户知道它该怎么用,而是让用户自然地知道自己在做什么。
前端·后端·架构
Lei活在当下5 小时前
【业务场景架构实战】7. 多代智能手表适配:Android APP 表盘编辑页的功能驱动设计
android·设计模式·架构
Jolie_Liang5 小时前
保险业多模态数据融合与智能化运营架构:技术演进、应用实践与发展趋势
大数据·人工智能·架构
aklry7 小时前
elpis之动态组件机制
javascript·vue.js·架构
brzhang7 小时前
高通把Arduino买了,你的“小破板”要变“AI核弹”了?
前端·后端·架构
我星期八休息8 小时前
C++异常处理全面解析:从基础到应用
java·开发语言·c++·人工智能·python·架构
new_daimond8 小时前
微服务网关技术详细介绍
微服务·云原生·架构