Day23 - 铁人付外挂实作付款类别(二)- 发起付款请求

了解 WooCommerce 金流的基本架构後,我们来进行串接的实作,在开始前先回顾一下目前的外挂结构:

iron-pay
├── composer.json
├── composer.lock
├── iron-pay.php
├── src
│   ├── Options.php
│   └── Posts
│       └── ShopOrder
│           └── Metabox.php
└── vendor

首先我们假设铁人付这家金流公司的支付运作模式,以信用卡与虚拟帐号转帐两种付款方式为例,说明实际状况中可能会遇到的方法以及对应的设计方式,理论上同一家金流公司都会采用同一种逻辑,本文为了示范不同情境所以采用不同逻辑,实际情况以金流商提供的技术文件为主。

铁人付信用卡付款流程

建立订单采用 HTTPS 协议, 将订单资料以 POST ( HTTP Method ) 传送,返回结果采用 JSON 格式,返回结果如下:

{
	"status": "200",
	"info": "订单建立成功",
	"data": {
		"html": "https://ironpay.test/payment", 	
		"out_trade_no": "111"
	}
}

当按下结帐按钮时要发起 API 请求,在经过验证请求来源後会回传付款网址,取得该网址後提供给 WC_Payment_Gatewayprocess_payment() 来进行跳转。

铁人付虚拟帐号付款流程

建立订单采用 HTTPS 协议, 将订单资料以 POST ( HTTP Method ) 传送後立即返回取号结果,等消费者转帐完成後金流商由服务器呼叫客户网站的通知付款网址。

新增付款方式

接下来我们先在 src 建立 Gateways 资料夹,并新增 CreditCard.phpVirtualAccount.phpRequest.php 以及 Response.php 四个档案,并另外在 src 资料夹增加一个工具类别 Utility.php

iron-pay
├── composer.json
├── composer.lock
├── iron-pay.php
├── src
│   ├── Gateways
│   │   ├── CreditCard.php
│   │   ├── Request.php
│   │   ├── Response.php
│   │   └── VirtualAccount.php
│   ├── Options.php
│   ├── Posts
│   │   └── ShopOrder
│   │       └── Metabox.php
│   └── Utility.php
└── vendor

CreditCard 与 VirtualAccount 分别为实作信用卡与虚拟帐号的类别,Request 为建立订单的请求类别,Response 负责处理从金流商回传的资料。根据上一篇的架构,CreditCard.php 内容如下:

<?php

namespace Irp\Gateways;

defined( 'ABSPATH' ) || exit;

/**
 * Set payment class
 */
function add_irp_credit_card() {
	class CreditCard extends \WC_Payment_Gateway {

		public function __construct() {
			$this->id                 = 'irp_credit_card';
			$this->icon               = '';
			$this->has_fields         = false;
			$this->method_title       = '铁人付信用卡';
			$this->method_description = '使用铁人付信用卡付款';

			$this->init_form_fields();
			$this->init_settings();

			$this->title       = $this->get_option( 'title' );
			$this->description = $this->get_option( 'description' );

			add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
		}

		public static function register() {
			add_filter( 'woocommerce_payment_gateways', array( __CLASS__, 'add_gateway_class' ) );
		}

		public function add_gateway_class( $methods ) {
			$methods[] = __CLASS__;
			return $methods;
		}

		public function init_form_fields() {
			$this->form_fields = array(
				'enabled'     => array(
					'title'   => '启用/停用',
					'label'   => '启用付款',
					'type'    => 'checkbox',
					'default' => 'no',
				),
				'title'       => array(
					'title' => '付款方式名称',
					'type'  => 'text',
				),
				'description' => array(
					'title' => '付款方式描述',
					'type'  => 'textarea',
					'css'   => 'max-width: 400px;',
				),
			);
		}

		public function process_payment( $order_id ) {
			// 处理付款请求
		}
	}

	CreditCard::register();

}
add_action( 'plugins_loaded', 'Irp\Gateways\add_irp_credit_card' );


可以看到除了基本的架构外,我们新增了两个方法,一个是静态方法 reigster() 来管理勾点 woocommerce_payment_gateways 的类别名称注册,另一个 add_gateway_class() 方法来取得类别名称,最後再用 CreditCard::register() 呼叫勾点,就能把新增付款方式的功能包在同一个函式之中。

然後我们依样画葫芦来建立虚拟帐号的付款方式,开启 VirtualAccount.php,贴入以下程序码:

<?php

namespace Irp\Gateways;

defined( 'ABSPATH' ) || exit;

/**
 * Set payment class
 */
function add_irp_virtual_account() {
    class VirtualAccount extends \WC_Payment_Gateway {

        public function __construct() {
            $this->id                 = 'irp_virtual_account';
            $this->icon               = '';
            $this->has_fields         = false;
            $this->method_title       = '铁人付虚拟帐号';
            $this->method_description = '使用铁人付虚拟帐号付款';

            // 以下略
    }

    VirtualAccount::register();

}
add_action( 'plugins_loaded', 'Irp\Gateways\add_irp_virtual_account' );

完成後就能在 WooCommerce 设定页看到铁人付相关的付款方式:

在实务中,如果不同的 Gateway 有相同的属性或方法,可以把共用元素独立成抽象类别,然後让抽象类别继承 WC_Payment_Gateway,要实作的付款方式再继承这个抽象类别,或是把相同的部分拆成 Trait,就能方便管理不同付款方式之间的共有属性与方法。

新增请求类别

请求的部分因为有很多参数要汇整以及带有不同的请求方式,所以我们把请求拆成独立的 Request 类别来处理,然後再於付款方式里面的 process_payment() 来建立实例,并传入 Gateway 类别本身来完成请求,Request 基本架构如下:

<?php

namespace Irp\Gateways;

defined( 'ABSPATH' ) || exit;

class Request {
	/**
	 * The gateway instance
	 *
	 * @var WC_Payment_Gateway
	 */
	protected $gateway;

	/**
	 * Constructor
	 *
	 * @param  WC_Payment_Gateway $gateway The payment gateway instance.
	 */
	public function __construct( $gateway ) {
		$this->gateway = $gateway;
	}	
}

首先,我们要取得 Gateway 实例的相关属性与方法,所以使用 $gateway 来存放付款类别,并在建构式指定给 $gateway 属性。

<?php

namespace Irp\Gateways;

defined( 'ABSPATH' ) || exit;

class Request {
	// 略
	public function __construct( $gateway ) {
		// 略
	}	
	/**
	 * Build transaction args.
	 *
	 * @param WC_Order $order The order object.
	 * @return array
	 */
	public function get_transaction_args( $order ) {
		$args = apply_filters(
			$this->gateway->id . '_transaction_args' ,
			array(
				'nonce_str'       => 'nonce',
				'orgno'           => '商家代号',
				'out_trade_no'    => $order->get_order_number(),
				'secondtimestamp' => time(),
				'total_fee'       => $order->get_total(),
			),
			$order
		);
		return $args;
	}
}

接下来透过 get_transaction_args() 方法来整理要传送给金流商的参数,这边的参数阵列会放在 apply_filters() 里面,这个 WordPress 内建的函式让我们可以自订勾点,第一个参数传勾点名称、第二个传阵列内容,第三个可以让这个勾点带入参数,之後我们就可以用 add_filter( 'irp_credit_card_transaction_args', 'callback', 10, 2 )方式来动态新增参数:

function add_transaction_args( $args, $order ){
	return array_merge(
		$args,
		array(
			'returnurl' => home_url( 'wc-api/iron-pay' ),
			'backurl'   => home_url( 'wc-api/iron-pay-offline' ),
		)
	);
}
add_filter( 'irp_credit_card_transaction_args', 'add_transaction_args', 10, 2 )

因为每个付款方式可能会需要带入不同的参数,所以 Request 这边先写好共用的参数,在付款方式那边就可以使用勾点 Filter 额外新增需要的参数。

Utility 类别定义了一系列静态方法,像是做演算法的杂凑、产出随机字串、Log 方法等等,这里面的方法大部分可以通用在不同的专案之中,像是一种工具箱的概念。接下来是第一种呼叫 API 的方式:

<?php

namespace Irp\Gateways;

defined( 'ABSPATH' ) || exit;

class Request {
	// 略
	public function __construct( $gateway ) {
		// 略
	}	
	/**
	 * Build transaction args.
	 *
	 * @param WC_Order $order The order object.
	 * @return array
	 */
	public function get_transaction_args( $order ) {
		// 略
	}
	
	/**
	 * Request api and get redirect url
	 *
	 * @param WC_Order $order The order object.
	 * @return void
	 */
	public function build_request( $order ) {
		$order   = new WC_Order( $order );
		$options = array(
			'method'  => 'POST',
			'timeout' => 60,
			'body'    => $this->get_transaction_args( $order ),
		);

		$response = wp_remote_request( '金流商 API 请求网址', $options );

		if ( ! is_wp_error( $response ) ) {
			// API 回传资料处理
			$body = json_decode( wp_remote_retrieve_body( $response ), true );
			// 取得跳转网址後返回
			return $body['data']['html']; // 取得跳转网址:'https://ironpay.test/payment';
		} else {
			// API 请求失败的处理
		}
	}
}

这一段需要值得注意的是 wp_remote_request() 这个函式,它封装了 WordPress 的 HTTP 请求物件,透过它可以很方便的发起 HTTP 请求,第一个参数为请求网址,第二个为要传送的参数,我们把金流商所需的资料藉由 get_transaction_args() 取得後放在 body 进行传送,wp_remote_request() 的如果请求正确回传 JSON 时,则用 wp_remote_retrieve_body() 取得 JSON 後进行格式转换,如果请求的结果有误,使用 is_wp_error() 来进行判断。

第二种传送方法是使用表单:

<?php

namespace Irp\Gateways;

defined( 'ABSPATH' ) || exit;

class Request {
	// 略
	public function __construct( $gateway ) {
		// 略
	}	
	/**
	 * Build transaction args.
	 *
	 * @param WC_Order $order The order object.
	 * @return array
	 */
	public function get_transaction_args( $order ) {
		// 略
	}
	
	/**
	 * Request api and get redirect url
	 *
	 * @param WC_Order $order The order object.
	 * @return void
	 */
	public function build_request( $order ) {
		// 略
	}
	
	/**
	 * Generate the form and redirect to IronPay
	 *
	 * @param WC_Order $order The order object.
	 * @return void
	 */
	public function build_request_form( $order ) {

		$order = new \WC_Order( $order );

		try {
			?>
			<div>请稍候重新导向中...</div>
			<form method="post" id="IrpForm" action="<?php echo esc_url( 'API 请求网址' ); ?>" accept="UTF-8" accept-charset="UTF-8">
				<?php
				$fields = $this->get_transaction_args( $order );
				foreach ( $fields as $key => $value ) {
					echo '<input type="hidden" name="' . esc_html( $key ) . '" value="' . esc_html( $value ) . '">';
				}
				?>
			</form>
			<script type="text/javascript">
				document.getElementById('IrpForm').submit();
			</script>
			<?php

		} catch ( Exception $e ) {
			Utility::log( $e->getMessage() . ' ' . $e->getTraceAsString() );
		}
	}
}

准备好两种传送方式後,让我们回到 CreditCard.php,继续信用卡的付款实作 process_payment()

<?php

namespace Irp\Gateways;

defined( 'ABSPATH' ) || exit;

/**
 * Set payment class
 */
function add_irp_credit_card() {
	class CreditCard extends \WC_Payment_Gateway {

		public function __construct() {
			// 略
		}

		public static function register() {
			// 略
		}

		public function add_gateway_class( $methods ) {
			// 略
		}

		public function init_form_fields() {
			// 略
		}

		public function process_payment( $order_id ) {
			$order      = new \WC_Order( $order_id );
			$request    = new Request( $this );
			$return_url = $request->build_request( $order );
			return array(
				'result'   => 'success',
				'redirect' => $return_url,
			);
		}
	}

	CreditCard::register();

}
add_action( 'plugins_loaded', 'Irp\Gateways\add_irp_credit_card' );

我们先建立了 Request 实例并将付款方式类别本身当作参数传进去,然後使用 build_request() 来取得跳转网址,这边记得要先取得 WooCommerce 的 $order 订单物件後传给它,才能在 Request 实例中取得订单资讯,最後返回一个付款网址,并将取得的网址指定给 redirect。

接下来是处理虚拟转帐的付款实作,开启 VirtualAccount.php:

<?php

namespace Irp\Gateways;

defined( 'ABSPATH' ) || exit;

/**
 * Set payment class
 */
function add_irp_virtual_account() {
	class VirtualAccount extends \WC_Payment_Gateway {

		public function __construct() {
			// 略
			add_action( 'woocommerce_receipt_' . $this->id, array( $this, 'receipt_page' ) );
		}

		public static function register() {
			// 略
		}

		public function add_gateway_class( $methods ) {
			// 略
		}

		public function init_form_fields() {
			// 略
		}

		/**
		 * Process payment
		 *
		 * @param string $order_id The order id.
		 * @return array
		 */
		public function process_payment( $order_id ) {
			$order = new \WC_Order( $order_id );
			return array(
				'result'   => 'success',
				'redirect' => $order->get_checkout_payment_url( true ),
			);
		}

		/**
		 * Redirect to IronPay payment page
		 *
		 * @param WC_Order $order The order object.
		 * @return void
		 */
		public function receipt_page( $order ) {
			$request = new Request( $this );
			$request->build_request_form( $order );
		}
	}

	VirtualAccount::register();

}
add_action( 'plugins_loaded', 'Irp\Gateways\add_irp_virtual_account' );

需要注意的地方有三个:

  1. process_payment() 指定的跳转网址为 get_checkout_payment_url(),这是指站内的付款页面

  2. 新增 receipt_page() 方法发起 Request 的前端表单传送方法 build_request_form()

  3. 在建构式新增勾点 woocommerce_receipt_irp_virtual_account 来触发 receipt_page() 方法

这边的运作的逻辑是当消费者按下结帐按钮後,直接跳转去结帐完成页,然後从结帐完成页产生的表单来做资料传送,这样就不用处理 API 的回传结果直接进行页面跳转判断。

当资料传送过去後,接下来就是要准备 WC API 来接收金流商的回传结果,下一篇我们继续处理接收回传资料的部分。

本文同步发表於:https://oberonlai.blog/tw/woocommerce-payment-request/#more


<<:  Day23 燃烧溶解文字

>>:  Day14 Sass 变数有趣的地方

Android 手机 行动电话 小人图示 talkback 无障碍按钮 导览列 捷径 协助工具按钮开关 设定 隐藏 开启

Android 手机 行动电话 小人图示 talkback 无障碍按钮 导览列 捷径 协助工具按钮开...

python os.walk鬼打墙

os.walk 找子目录下特定类型档案,鬼打墙好几天。也写了一两篇po上来,就当是"叠床架...

DAY18 专案进度按钮功能实现-2

class Report(): def content(self): flex_message = ...

Kotlin 语言

https://wolkesau.medium.com/kotlin-语言-5ad3d8f208e4...

生存法则一:在快速变动的环境下生存

承认我们都有一些资讯焦虑 我们生活在快速变动的时代,无时无刻都有新的产业跟名词冒出,数据驱动决策、区...