3. 支付渠道抽象层设计

支付渠道抽象层设计 学习卡片

为什么需要支付渠道抽象层

前两例分别把支付宝和微信支付接入了项目,但业务代码里如果直接出现 PayTask.payV2()、IWXAPI.sendReq() 这类调用,每加一个渠道就要改一遍下单页、订单页和回调处理。抽象层的目标是把"发起支付"和"处理结果"收敛成两个稳定接口,让业务层只依赖接口,不依赖任何渠道 SDK。

接口与工厂的职责划分

抽象层由三部分组成:统一支付接口(业务层唯一可见的契约)、渠道实现(每个渠道一个类,内部封装各自 SDK)、渠道工厂(根据渠道标识返回对应实现)。业务层拿到的是 IPayChannel,而不是 AlipayChannel 或 WxPayChannel。

业务层(下单页 / 订单页 / 支付结果页) 只调用 IPayChannel.pay() 与 IPayChannel.handleResult(),不 import 任何渠道 SDK IPayChannel(统一支付接口) + pay(activity, PayRequest): void + handleResult(PayResult): PayResult PayChannelFactory getChannel(type): IPayChannel AlipayChannel 封装 PayTask / 同步验签 / 结果映射 WxPayChannel 封装 IWXAPI / prepay_id / 结果映射 新增渠道 实现接口 注册工厂 业务层零改动

接口定义

接口只暴露两个方法:发起支付和处理结果。请求和结果都用统一的数据对象承载,避免业务层接触渠道特有的字段名。

// PayRequest.kt —— 统一支付请求
data class PayRequest(
    val orderId: String,        // 商户订单号
    val amountFen: Long,        // 金额,单位:分
    val subject: String,        // 商品标题
    val channelParams: Map<String, String>  // 渠道特有参数(如微信 prepay_id、支付宝 orderInfo)
)

// PayResult.kt —— 统一支付结果
data class PayResult(
    val orderId: String,
    val status: PayStatus,      // SUCCESS / FAIL / CANCEL / PENDING
    val rawCode: String?,       // 渠道原始返回码,便于排查
    val message: String?
)

enum class PayStatus { SUCCESS, FAIL, CANCEL, PENDING }

// IPayChannel.kt —— 统一支付接口
interface IPayChannel {
    /** 渠道标识,如 "alipay"、"wxpay" */
    val channelType: String

    /** 发起支付,activity 用于调起渠道收银台 */
    fun pay(activity: Activity, request: PayRequest)

    /** 将渠道回调结果映射为统一 PayResult */
    fun handleResult(raw: Any): PayResult
}

渠道实现

每个渠道实现类只做两件事:把 PayRequest 转成自己 SDK 需要的参数,把 SDK 回调转成 PayResult。以下以支付宝为例,微信实现结构完全对称。

// AlipayChannel.kt
class AlipayChannel : IPayChannel {
    override val channelType = "alipay"

    override fun pay(activity: Activity, request: PayRequest) {
        // 支付宝 App 支付需要服务端签好名的 orderInfo 字符串
        val orderInfo = request.channelParams["orderInfo"]
            ?: throw IllegalArgumentException("alipay 缺少 orderInfo")
        PayTask(activity).payV2(orderInfo, true)
    }

    override fun handleResult(raw: Any): PayResult {
        // raw 为支付宝同步返回的 Map<String, String>
        @Suppress("UNCHECKED_CAST")
        val map = raw as Map<String, String>
        val resultStatus = map["resultStatus"]
        val memo = map["memo"]
        val status = when (resultStatus) {
            "9000" -> PayStatus.SUCCESS
            "6001" -> PayStatus.CANCEL
            "8000", "6004" -> PayStatus.PENDING
            else -> PayStatus.FAIL
        }
        return PayResult(
            orderId = map["out_trade_no"] ?: "",
            status = status,
            rawCode = resultStatus,
            message = memo
        )
    }
}
支付宝的 resultStatus=9000 只代表客户端支付动作成功,最终交易状态仍需服务端异步通知确认。抽象层把 PENDING 单独列出,就是为了让业务层知道"客户端结果不等于最终结果"。

渠道工厂

工厂用 Map 注册渠道,新增渠道只需加一行注册代码,业务层完全无感。

// PayChannelFactory.kt
object PayChannelFactory {
    private val registry = mutableMapOf<String, IPayChannel>()

    init {
        register(AlipayChannel())
        register(WxPayChannel())
    }

    fun register(channel: IPayChannel) {
        registry[channel.channelType] = channel
    }

    fun getChannel(type: String): IPayChannel =
        registry[type] ?: throw IllegalArgumentException("未注册的支付渠道: $type")
}

业务层调用方式

业务层只依赖 IPayChannel 和 PayChannelFactory,切换渠道只是换一个字符串。

// 下单页发起支付
val channel = PayChannelFactory.getChannel("alipay")
val request = PayRequest(
    orderId = "ORDER_20240101_001",
    amountFen = 1990L,
    subject = "会员月卡",
    channelParams = mapOf("orderInfo" to orderInfoFromServer)
)
channel.pay(this, request)

// 支付回调页处理结果
val result = channel.handleResult(rawCallbackData)
when (result.status) {
    PayStatus.SUCCESS -> showToast("支付成功,等待服务端确认")
    PayStatus.CANCEL  -> showToast("已取消支付")
    PayStatus.PENDING -> showToast("支付处理中,请稍后查看订单")
    PayStatus.FAIL    -> showToast("支付失败:${result.message}")
}

验证抽象层是否生效

用以下三个检查点确认业务层没有泄漏渠道依赖:

  • 在业务层文件中搜索 import com.alipay 和 import com.tencent.mm,结果应为零。
  • 把 PayChannelFactory.getChannel("alipay") 改成 "wxpay",业务层其余代码不需要任何修改即可编译通过。
  • 新增一个模拟渠道 MockChannel(channelType = "mock",pay() 直接回调成功),注册后业务层用 "mock" 即可跑通全流程,无需真实 SDK。
工厂的 init 块在类加载时执行,如果渠道实现类的构造中做了耗时操作(如初始化 SDK),会拖慢首次调用。建议渠道实现保持无状态、轻量构造,SDK 初始化延迟到 pay() 内部按需执行。