2.8 Commerce Payment 模块:支付系统
这是与 Commerce Order
几乎是同样重要的模块,电子商务系统最重要的东西就是订单和支付。
作为支付系统,主要的作用是可以接入各种各样的第三方支付网关,完成订单的 付款
退款
功能。 我们先来看一下,目前Drupal社区都提供了哪些支付网关的接入:
https://docs.drupalcommerce.org/commerce2/developer-guide/payments/available-gateways
可以看到官方上记录的支付网关模块已经达到了 114 个,十分惊人,其中包含了中国常用的 支付宝
和 微信支付
。 还有一些没有被官方文档记录的支付网关,也存在一定的数量。
下面我们先来看一下 Commerce Payment
模块的设计。
数据结构
实体
commerce_payment
支付单。内容实体,每一笔支付对应一条记录。commerce_payment_gateway
支付网关实例。配置实体,一个网关类型,可以配置多个网关实例。网关实例的意思是,配置了收款商家账号的网关插件
。commerce_payment_method
支付方法。内容实体,用于保存消费者用户在特定网关插件
的支付账户信息。 比如银行卡支付,一个用户可能拥有很多张银行卡,每一张银行卡就相当于用户的一个支付方法。
需要特别注意的是,commerce_payment
和 commerce_payment_method
两个内容实体,它们是有 bundle 的,但是我们在代码里并没有发现它们对应的 配置实体
。 我们知道,Drupal 核心提供的内容实体 bundle 机制是通过 配置实体
来实现的,这意味着无法通过 php 代码来创建实体。
我们知道,在Drupal7的时代,Entity API是一个社区模块,并不包含在核心,Drupal8的时候才并进核心。 但当我们去看 Entity模块的主页时,会发现上面依然有8.x的版本,这个Entity模块的 drupal8版本 和Drupal8核心的EntityAPI并不是重复的,一是些没有被采纳进核心的EntityAPI特性,有一些人觉得很有用,所以放在这里,方便开发时使用。 这些特性经过社区验证之后,有一点也许也会并进Drupal8核心。
扯得有点远,现在讲回正题,本模块使用了这个社区的Entity模块的一个特性,那就是通过编写插件的方式来创建 Entity Bundle。
我们查看 commerce_payment
和 commerce_payment_method
两个实体类型的定义时,在注解代码中发现一个名为 bundle_plugin_type
的属性, 它的值是一个插件类型ID,这意味着EntityAPI会为每一个该类型的插件创建一个对应的Entity Bundle。所有该用途的插件,都应该实现 Drupal\entity\BundlePlugin\BundlePluginInterface
接口:
<?php
namespace Drupal\entity\BundlePlugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
/**
* Interface for plugins which act as entity bundles.
*/
interface BundlePluginInterface extends PluginInspectionInterface {
/**
* Builds the field definitions for entities of this bundle.
*
* Important:
* Field names must be unique across all bundles.
* It is recommended to prefix them with the bundle name (plugin ID).
*
* @return \Drupal\entity\BundleFieldDefinition[]
* An array of bundle field definitions, keyed by field name.
*/
public function buildFieldDefinitions();
}
该接口定义了一个 buildFieldDefinitions()
方法,该方法用于返回要创建的 Bundle 的自有字段定义。 这里的字段定义应该使用 Drupal\entity\BundleFieldDefinition::create()
方法来创建。
我们从代码中可以看到,commerce_payment
和 commerce_payment_method
两个实体类型对应的Bundle 插件类型为 @CommercePaymentType
和 @CommercePaymentMethodType
。
订单实体字段
本模块在订单实体添加了两个字段:
payment_gateway
记录订单所使用的支付网关实例。payment_method
记录订单所使用的支付方法。
@CommercePaymentGateway
支付网关插件
支付网关插件类型是有一点儿复杂,这是灵活性的兼容性的代价。但是当我们去理清它所支持支付模式时, 会发现其实也不是很复杂,在设计上还是很简约很优雅的。
支付服务商
和 支付网关
阿里支付宝公司
和 腾讯公司
就是中国最大的两个 支付服务商
。
它们分别提供了 支付宝
和 微信支付
两个网络支付产品。 从开发人员的角度看,它们就是 支付网关
,我们需要编写程序来与它们交互。
支付模式
每种支付网关的业务模式可能不太一样,但是主要可以分为以下两类:
-
在站支付
指用户无需离开业务网站,即可进行授权,完成支付。
-
离站支付
指用户必须跳转到支付网关的站点进行授权,完成支付,然后再跳转回业务网站。
此外系统还定义了一种特殊的支付模式,叫 Manual 手动支付
,这种模式表示没有支付网关的支付模式。 这个模式可以用来实现线下现金支付、线下银行转账,等等,这类支付方式。
这三种模式的支付网关插件分别需要实现如下接口:
OnsitePaymentGatewayInterface
OffsitePaymentGatewayInterface
ManualPaymentGatewayInterface
开发者应该细读这三个接口的代码注析,上面把三种模式的流程和接口实现方法都说得很明白,此处不再作翻译。
支持性接口
上文提及的三个接口,实际上还继承了一些支持性接口:
-
SupportsAuthorizationsInterface
支持授权系统把
Authorize
和Capture
定义为在与支付网关交互过程中的两个操作。Authorize
指的是向支付服务商网关发关请求,要求授权支付一定金额。Capture
指的是利用已获得的授权凭证,请求支付服务商网关执行一笔交易,把一定金额从用户的账号,转移到商家的收款账号。有的支付网关是把这两个操作一起完成的,有的支付网关是把这两个操作分开的。 但不管怎么样,系统在
Payment process
这个结账面板提供了一个配置选项,让管理员可以把系统配置为两种方式的任意一种。 如果是Authorize and capture
,两个操作一起完成,如果是Authorize only (requires manual capture after checkout)
, 则需要在结账流程完成后,管理员到订单支付管理页面点击Capture
按钮执行操作。 当然,默认配置是Authorize and capture
,分开操作应该是非常特殊的业务情景才需要用到。如果交易方式是
Authorize only
,此外还有一个Void
操作,在Capture
之前可以点击Void
按钮取消交易。Capture
操作一般是有时效性的,如果超过了这个时效,Authorize
操作创建的交易也会自动失效, 这个时效是用commerce_payment
实体的authorized
和expires
字段记录的。讲回这个接口,其实它只有一个方法,
capturePayment(PaymentInterface $payment, Price $amount = NULL)
, 只有支持把两个操作分开的支付网关,才能实现此接口。 -
SupportsVoidsInterface
支持取消交易参考上文关于
SupportsAuthorizationsInterface
接口的说明。 实现了此接口的支付网关,订单支付管理页面才会出现Void
按钮。 -
SupportsStoredPaymentMethodsInterface
支持保存用户支付方法关于什么是
支付方法
,请参考上文关于commerce_payment_method
内容实体的说明。 如果一个支付网关插件能够支持用户账户信息重用的话,那它可以实现这个接口,使得用户在后续交易中不必再次输入账户信息, 同时,用户还可以在个人中心页面管理自己的支付方法
,添加或删除。 -
SupportsUpdatingStoredPaymentMethodsInterface
支持更新用户支付方法在实上
SupportsStoredPaymentMethodsInterface
接口的基础上,如果实现此接口, 用户还可以在个人中心页面编辑自己已有的支付方法
。 -
SupportsNotificationsInterface
支持异步交易状态通知当请求支付网站执行交易时,可能会顺利成功,也不一定能成功,比如账户余额不足等原因。 有些支付网关支持用HTTP请求的方式,异步向商家服务器发送交易状态变动通知, 可以实现这个接口,在
onNotify(Request $request)
方法中读取这个异步HTTP请求,相应地对本地的数进行更新。 -
SupportsRefundsInterface
支持退款在部分的网关都支持退款,当插件实现了此接口后,网站管理员可以在订单支付管理页面看到一个
Refund
按钮, 点击之后会打开一个退款表单,填写退款金额后,可以请求插件执行退款操作,插件开发者可以在refundPayment(PaymentInterface $payment, Price $amount = NULL)
方法中编写退款业务代码。值得注意的是,它是支持部分退款的,如果网关不支持部分退款,那么开发者可以自行检查用户输入的金额, 用表单错误信息提示用户不能部分退款。
-
HasPaymentInstructionsInterface
支持在支付完成时显示提示信息支付网关实现了此接口,系统就会把提示信息通过
hook_preprocess_commerce_checkout_completion_message()
勾子显示到completion_message
结账面板中。
结账面板
支付是与结账过程紧密相关的,本模块对 Commerce Ckeckout
模块有关键的依赖。 本模块实现了两个结账面板:
-
payment_information
让用户先择订单所使用的支付方案这个面板替换了
billing_information
面板,因为开发者认为billing_information
的信息是支付过程中必要的, 所以他把billing_information
的功能写到了payment_information
中,并把billing_information
删除掉。 这并不太友好,社区很多人都提出了这个问题,相信后面会有所改进。 -
payment_process
执行支付交易用于显示 off-site 模式支付网关插件定义的
offsite-payment
表单,以及执行插件的相关调用。