【业务场景架构实战】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 --> [*]
相关推荐
架构师沉默5 小时前
设计多租户 SaaS 系统,如何做到数据隔离 & 资源配额?
java·后端·架构
kfyty7258 小时前
不依赖第三方,不销毁重建,loveqq 框架如何原生实现动态线程池?
java·架构
刘立军10 小时前
本地大模型编程实战(33)用SSE实现大模型的流式输出
架构·langchain·全栈
一直_在路上10 小时前
Go 语言微服务演进路径:从小型项目到企业级架构
架构·go
天花板之恋12 小时前
Compose之图片加载显示
android jetpack
智能化咨询14 小时前
Kafka架构:构建高吞吐量分布式消息系统的艺术——进阶优化与行业实践
分布式·架构·kafka
七夜zippoe14 小时前
缓存与数据库一致性实战手册:从故障修复到架构演进
数据库·缓存·架构
青鱼入云15 小时前
【面试场景题】支付&金融系统与普通业务系统的一些技术和架构上的区别
面试·金融·架构
gtGsl_16 小时前
深入解析 Apache RocketMQ架构组成与核心组件作用
架构·rocketmq·java-rocketmq