Day24 - 铁人付外挂实作付款类别(三)- 接收回传资料

完成付款请求之後,接下来是准备好接收金流商回传资讯的 Response 类别,目前外挂的资料夹结构如下:

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

开启 Response.php 档案,输入以下程序码:

<?php
namespace Irp\Gateways;

use Irp\Utility;

defined( 'ABSPATH' ) || exit;

/**
 * Receive response from ironpay.
 */
class Response {

	/**
	 * Initialize and add hooks
	 *
	 * @return void
	 */
	public static function init() {

		// Online Payment listener.
		add_action( 'woocommerce_api_iron-pay', array( __CLASS__, 'receive_response' ) );
		add_action( 'ironpay_online_response', array( __CLASS__, 'valid_response' ) );

		// Offline Payment listener.
		add_action( 'woocommerce_api_iron-pay-offline', array( __CLASS__, 'receive_response' ) );
		add_action( 'ironpay_offline_response', array( __CLASS__, 'valid_response_offline' ) );

	}
}

Response::init();

首先,我们用静态方法 init()来初始化勾点的部分,勾点一共分为两组,分别是即时付款 Online Payment 以及非即时付款 Offline Payment。每一组又个别有两个勾点,我们先看 Online Payment 这一组,第一个勾点是 WooCommerce API 所提供的,也就是前面我们有提到的动态勾点:

add_action( 'woocommerce_api_iron-pay', array( __CLASS__, 'receive_response' ) );

这代表当我们的造访 https://woocommerce.test/wc-api/iron-pay/ 会触发 receive_response 这个方法,这个方法的主要功能为验证当请求这个网址时有接收到金流商回传的参数,并且通过加密演算法的验证。

第二个勾点为 ironpay_online_response,当看到有我们自己命名前缀的勾点时,就代表这是我们自己新增的,新增的方法稍後会实作。

add_action( 'ironpay_online_response', array( __CLASS__, 'valid_response' ) );

这个勾点会在通过我们的请求来源安全性验证後触发 valid_response(),该方法实作更新订单资料、改变库存状态、以及将回传资料写入资料库後进行页面跳转等行为。

有了 Online Payment 勾点的理解之後,Offline Payment 也是一样的逻辑,设定好回传网址後,我们就能在请求类别 Request 里面将这两个网址作为参数传给金流商,通知他们当消费者完成後结帐後会呼叫这两个网址。

通常金流商回有两种呼叫方式,一种是前景呼叫,也就是透过消费者的浏览器跳转到我们提供的网址,不管是即时或是非即时付款,都需要让消费者回到原站会比较合理。

另一种是背景呼叫,常见於非即时付款,因为消费者已经走完结帐流程关闭浏览器了,当消费者隔天完成转帐,需要靠金流商的主机来发起付款完成通知,这种请求也就是俗称的背景呼叫。

不同金流商对於这两种呼叫方式的参数命名都不太相同,前景呼叫有可能叫 return_url 或是 back_url,背景呼叫有可能叫做 notify_url 或是 callback_url,因为这些名称都无法很直观的分辨到底是前景还是背景,所以详读文件的参数名称以及流程图会比较保险些,不然只根据参数名称来传很容易搞错。

接下来是实作 receive_response() 方法:

<?php
namespace Irp\Gateways;

use Irp\Utility;

defined( 'ABSPATH' ) || exit;

/**
 * Receive response from ironpay.
 */
class Response {

	/**
	 * Initialize and add hooks
	 *
	 * @return void
	 */
	public static function init() {
		// 略
	}
	
	/**
	 * Receive response from ironpay
	 *
	 * @return void
	 */
	public static function receive_response() {
		if ( ! empty( $_REQUEST ) ) {
			$params = wc_clean( wp_unslash( $_REQUEST ) );
			$args   = array(
				'authcode'        => $params['authcode'],
				'nonce_str'       => $params['nonce_str'],
				'orderdate'       => $params['orderdate'],
				'orgno'           => $params['orgno'],
				'out_trade_no'    => $params['out_trade_no'],
				'periods'         => $params['periods'],
				'result'          => $params['result'],
				'secondtimestamp' => $params['secondtimestamp'],
				'status'          => $params['status'],
				'total_fee'       => $params['total_fee'],
			);

			$sign = Utility::generate_sign( $args, Utility::get_secret() );
			// $params['sign'] 是金流商回传的,把它拿来跟我们自己算出来的 $sign 做比对
			if ( $sign === $params['sign'] ) {
				if ( current_action() === 'woocommerce_api_iron-pay' ) {
					do_action( 'ironpay_online_response', $params );
				} elseif ( current_action() === 'woocommerce_api_iron-pay-offline' ) {
					do_action( 'ironpay_offline_response', $params );
				}
			}
		}
	}
}

Response::init();

首先我们先检查是否有回传参数,因为不同金流商有可能会用 $_POST 或是 $_GET 回传,所以用 $_REQUEST 可以通用在不同专案之中,wc_clean()wp_unslash() 是 WooCommerce 内建的函式,用来消除斜线或是空格等字元,我们把回传的资料组成一个阵列,在建立勾点之前,我们先要判断回传的演算法是否有符合我们自己算出来的结果,藉此来确保这次的请求是由金流商呼叫。

我们先用 Utility 类别来算出我们的 sign,计算的方法为传入所有参数加上商店金钥,算好後再用它来比对金流商的 sign 是否吻合,如果正确的话才会建立勾点。 current_action() 是判断该方法是在哪个勾点被执行,do_action() 则是建立勾点,第一个参数为勾点名称,第二个为使用该勾点的函式可以拿到的变数。

接下来是做订单处理的 valid_response() 方法:

<?php
namespace Irp\Gateways;

use Irp\Utility;

defined( 'ABSPATH' ) || exit;

/**
 * Receive response from ironpay.
 */
class Response {

	/**
	 * Initialize and add hooks
	 *
	 * @return void
	 */
	public static function init() {
		// 略
	}
	
	/**
	 * Receive response from ironpay
	 *
	 * @return void
	 */
	public static function receive_response() {
		// 略
	}
	
	/**
	 * Receive online payment
	 *
	 * @param array $params The post data received from ironpay.
	 * @return void
	 */
	public static function valid_response( $params ) {
		global $woocommerce;
		$order = new \WC_Order( $params['out_trade_no'] );
		if ( $order ) {
			if ( '0000' === $params['status'] ) {
				$order->payment_complete();
				$order->reduce_order_stock();
			} else {
				$order->update_status( 'on-hold' );
			}
			$woocommerce->cart->empty_cart();
			update_post_meta( $order->get_id(), '_irp_resp_code', $params['status'] );
			update_post_meta( $order->get_id(), '_irp_resp_result', $params['result'] );
			$order->add_order_note( '铁人付交易结果:' . $params['result'], true );
			wp_safe_redirect( $order->get_checkout_order_received_url() );
			exit;
		}
	}
}

Response::init();

我们先取得全域变数 $woocommerce,这个变数等下可以来操作购物车,接着是用金流商回传的订单 ID 来建立订单物件,订单物件有很多方法提供给我们修改订单资料。$params['status'] 为金流商回传的交易代号,通常每个代号会代表不同的结果,所以根据金流文件来判断当代号为 0000 成功时,执行以下的动作:

$order->payment_complete();
$order->reduce_order_stock();
$woocommerce->cart->empty_cart();
update_post_meta( $order->get_id(), '_irp_resp_code', $params['status'] );
update_post_meta( $order->get_id(), '_irp_resp_result', $params['result'] );
$order->add_order_note( '铁人付交易结果:' . $params['result'], true );

分别是将订单状态改完完成、减少库存、清空购物车,以及更新我们的自订栏位 _irp_resp_code_irp_resp_result,最後的 add_order_note() 方法是新增订单备注,回传资料要做哪些事会根据客户需求而有所不同,但大致上不外乎是以上几种流程。

处理完订单後则使用 wp_safe_redirect() 指定跳转页面,$order->get_checkout_order_received_url() 为完成购买页,至於背景呼叫 valid_response_offline() 也差不多,主要差别在於不需做页面跳转,以及可能会需要返回金流商指定的内容:

<?php
namespace Irp\Gateways;

use Irp\Utility;

defined( 'ABSPATH' ) || exit;

/**
 * Receive response from ironpay.
 */
class Response {

	/**
	 * Initialize and add hooks
	 *
	 * @return void
	 */
	public static function init() {
		// 略
	}
	
	/**
	 * Receive response from ironpay
	 *
	 * @return void
	 */
	public static function receive_response() {
		// 略
	}
	
	/**
	 * Receive online payment
	 *
	 * @param array $params The post data received from ironpay.
	 * @return void
	 */
	public static function valid_response( $params ) {
		// 略
	}
	
	/**
	 * Receive offline payment
	 *
	 * @param array $params The post data received from ironpay.
	 * @return void
	 */
	public static function valid_response_offline( $params ) {
		global $woocommerce;
		$order = new \WC_Order( $params['out_trade_no'] );
		if ( $order ) {
			// 略
			wp_send_json( 'success' );
			exit;
		}
	}
}

Response::init();

wp_send_json() 为 WordPress 内建的函式,主要功能是输出 JSON 字串,假设金流商那边希望当呼叫我们的 API 网址时,如果有成功接收的话回传 success 字串,那我们就可以写成 wp_send_json( 'success' ); 来符合金流商的要求。

完整的 Response 类别程序码如下:

namespace Irp\Gateways;

use Irp\Utility;

defined( 'ABSPATH' ) || exit;

/**
 * Receive response from ironpay.
 */
class Response {

	/**
	 * Initialize and add hooks
	 *
	 * @return void
	 */
	public static function init() {

		// Online Payment listener.
		add_action( 'woocommerce_api_iron-pay', array( __CLASS__, 'receive_response' ) );
		add_action( 'ironpay_online_response', array( __CLASS__, 'valid_response' ) );

		// Offline Payment listener.
		add_action( 'woocommerce_api_iron-pay-offline', array( __CLASS__, 'receive_response' ) );
		add_action( 'ironpay_offline_response', array( __CLASS__, 'valid_response_offline' ) );

	}

	/**
	 * Receive response from ironpay
	 *
	 * @return void
	 */
	public static function receive_response() {

		if ( ! empty( $_REQUEST ) ) {
			$params = wc_clean( wp_unslash( $_REQUEST ) );
			$args   = array(
				'authcode'        => $params['authcode'],
				'nonce_str'       => $params['nonce_str'],
				'orderdate'       => $params['orderdate'],
				'orgno'           => $params['orgno'],
				'out_trade_no'    => $params['out_trade_no'],
				'periods'         => $params['periods'],
				'result'          => $params['result'],
				'secondtimestamp' => $params['secondtimestamp'],
				'status'          => $params['status'],
				'total_fee'       => $params['total_fee'],
			);

			$sign = Utility::generate_sign( $args, Utility::get_secret() );

			if ( $sign === $params['sign'] ) {
				if ( current_action() === 'woocommerce_api_iron-pay' ) {
					do_action( 'ironpay_online_response', $params );
				} elseif ( current_action() === 'woocommerce_api_iron-pay-offline' ) {
					do_action( 'ironpay_offline_response', $params );
				}
			}
		}
	}

	/**
	 * Receive online payment
	 *
	 * @param array $params The post data received from ironpay.
	 * @return void
	 */
	public static function valid_response( $params ) {
		global $woocommerce;
		$order = new \WC_Order( $params['out_trade_no'] );
		if ( $order ) {
			if ( '0000' === $status ) {
				$order->payment_complete();
				$order->reduce_order_stock();
			} else {
				$order->update_status( 'on-hold' );
			}
			$woocommerce->cart->empty_cart();
			update_post_meta( $order->get_id(), '_irp_resp_code', $params['status'] );
			update_post_meta( $order->get_id(), '_irp_resp_result', $params['result'] );
			$order->add_order_note( '铁人付交易结果:' . $params['result'], true );
			wp_safe_redirect( $order->get_checkout_order_received_url() );
			exit;
		}
	}

	/**
	 * Receive offline payment
	 *
	 * @param array $params The post data received from ironpay.
	 * @return void
	 */
	public static function valid_response_offline( $params ) {
		global $woocommerce;
		$order = new \WC_Order( $params['out_trade_no'] );
		if ( $order ) {
			if ( '0000' === $status ) {
				$order->payment_complete( $buysafe_no );
				$order->reduce_order_stock();	
			}
			$woocommerce->cart->empty_cart();
			update_post_meta( $order->get_id(), '_irp_resp_code', $params['status'] );
			update_post_meta( $order->get_id(), '_irp_resp_result', $params['result'] );
			$order->add_order_note( '铁人付交易结果:' . $params['result'], true );
			wp_send_json( 'success' );
			exit;
		}
	}
}

Response::init();

这样就算是完成接收金流商回传资料的工作了,剩下的就是根据金流商的规格去做调整,WooCommerce 金流串接就能大功告成了,但在交给客户测试验收前,我们先自己来做单元测试,下一篇来介绍如何写测试以及说明应该要测试哪些类别与方法。

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


<<:  CSS微动画 - Loading来了!小精灵?这个小家伙吃蛋

>>:  [Day11] Face Detection - 使用OpenCV & Dlib:Dlib HOG + Linear SVM

Day-8:Rails的CRUD

CRUD系虾米? CRUD即为Create、Read、Update、Delete等四项基本资料库操作...

Day23:安全性和演算法-混成密码系统(Hybrid Cryptosystem)

前言 前面两天已经提到共同金钥密码系统以及公开金钥密码系统,今天要来探讨,结合两种密码系统,并且弥补...

Day 29 - AWS Lambda 接收参数查询 Dynamodb

Day 29 - AWS Lambda 接收参数查询 Dynamodb Day 28 - AWS L...

用Stack 制作Queue

记录学习内容。 以下内容和截图大多引用文章。 还不了解,内容可能有错误。 Queue 可以用 Sta...

[Day21] NLP会用到的模型(四)-Seq2Seq

一. Sequence to Sequence 在说明transformer之前,先介绍一下何谓Se...