4. 支付订单模型与状态机
订单模型:把“支付结果”从布尔值升级为可审计状态
支付订单不能只用 isPaid: Boolean 表达。真实业务里,一笔订单会经历“待支付 → 支付中 → 成功/失败 → 已退款”等阶段,且每个阶段都有不同的可执行动作与数据字段。下面先定义订单数据模型,再用状态机约束迁移。
数据模型定义
订单模型需要同时承载“业务标识、渠道信息、金额、状态、时间戳、失败原因”六类信息。以下 Kotlin 数据类可直接用于 Android 端本地缓存与服务端同步:
enum class PayStatus {
PENDING, // 待支付:订单已创建,未发起渠道调用
PAYING, // 支付中:已调起渠道,等待结果回调
SUCCESS, // 成功:渠道确认收款
FAILED, // 失败:渠道返回失败或超时关闭
REFUNDED // 已退款:成功后退款完成
}
data class PayOrder(
val orderId: String, // 业务订单号,全局唯一
val channel: String, // "WECHAT" / "ALIPAY"
val amountFen: Long, // 金额,单位:分,避免浮点误差
val status: PayStatus = PayStatus.PENDING,
val prepayId: String? = null, // 渠道预支付标识
val tradeNo: String? = null, // 渠道交易号,成功后写入
val failReason: String? = null,
val createdAt: Long, // 创建时间戳(ms)
val updatedAt: Long // 状态最后变更时间戳(ms)
)
金额统一用“分”存储为
Long,可彻底规避 0.1 + 0.2 类浮点误差;tradeNo 只在 SUCCESS 后才有值,是后续退款与对账的关键字段。状态机:合法迁移与非法迁移
状态机的核心是“白名单迁移”:只有列出的 (from, to) 组合才被允许,其余一律拒绝并抛出异常。这样可以从代码层面阻止“待支付直接跳到已退款”这类脏数据。
状态机实现:白名单 + 守卫
用一张迁移表描述合法路径,配合 transition() 函数做校验。任何不在表中的迁移都会抛出 IllegalStateException,便于在开发期快速暴露问题。
object PayStateMachine {
// 合法迁移白名单:from -> 允许的 to 集合
private val allowed: Map<PayStatus, Set<PayStatus>> = mapOf(
PayStatus.PENDING to setOf(PayStatus.PAYING),
PayStatus.PAYING to setOf(PayStatus.SUCCESS, PayStatus.FAILED),
PayStatus.SUCCESS to setOf(PayStatus.REFUNDED),
PayStatus.FAILED to emptySet(),
PayStatus.REFUNDED to emptySet()
)
fun canTransit(from: PayStatus, to: PayStatus): Boolean =
allowed[from]?.contains(to) == true
fun transition(order: PayOrder, to: PayStatus, now: Long = System.currentTimeMillis()): PayOrder {
require(canTransit(order.status, to)) {
"非法状态迁移: ${order.status} -> $to (orderId=${order.orderId})"
}
return order.copy(status = to, updatedAt = now)
}
}
在支付流程中驱动状态
把状态机嵌入实际支付动作,形成“创建订单 → 发起支付 → 回调处理 → 退款”的闭环。下面演示关键调用点:
// 1. 创建订单:初始为 PENDING
var order = PayOrder(
orderId = "ORD20240521001",
channel = "WECHAT",
amountFen = 1990,
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis()
)
// 2. 调起渠道前:PENDING -> PAYING
order = PayStateMachine.transition(order, PayStatus.PAYING)
// 3. 渠道回调成功:PAYING -> SUCCESS,并写入 tradeNo
order = PayStateMachine.transition(order, PayStatus.SUCCESS)
.copy(tradeNo = "4200001234202405210001")
// 4. 用户申请退款:SUCCESS -> REFUNDED
order = PayStateMachine.transition(order, PayStatus.REFUNDED)
// 5. 非法尝试会被拦截
// PayStateMachine.transition(order, PayStatus.PAYING)
// -> IllegalStateException: 非法状态迁移: REFUNDED -> PAYING
回调可能重复到达。处理回调时应先判断
order.status:若已是 SUCCESS 或 REFUNDED,直接忽略重复通知,避免状态被反复覆盖。验证要点
- 合法路径全覆盖:PENDING→PAYING→SUCCESS→REFUNDED 与 PENDING→PAYING→FAILED 两条主链均可走通。
- 非法路径被拦截:PENDING→REFUNDED、FAILED→SUCCESS、REFUNDED→PAYING 均抛出异常。
- 终态不可逆:FAILED 与 REFUNDED 的出边集合为空,任何迁移尝试都应失败。
- 时间戳更新:每次成功迁移后
updatedAt必须大于createdAt,便于审计与对账。