聚合支付系统设计(一)

未经许可,谢绝转载,违者必究

 

产品概述与整体设计

背景

如今,网购已经渗透到人们日常生活中的方方面面,做为网购的载体,互联网电商平台发展如火如荼,支付功能做为其不可或缺的一部分,实现起来,也有各种各样的方案。根据自己有限的认知,我主观上把目前行业内的支付实现方案做以下归类:

  • 持有支付业务许可证,又称支付牌照,自有支付品牌,比如阿里的支付宝、腾讯的微信支付(财付通)、京东的京东支付等;
  • 自建第三方支付聚合平台,对接第三方支付(支付宝、微信支付、中国银联、各大商业银行直连等),为其自有订单提供支付功能;
  • 一些研发资源有限的电商平台,选择市场中直接能提供全套聚合支付的支付平台,省去研发环节,能够以最短时间较低的成本为其平台提供支付功能。

我从开发者的角度,主要针对第二类,讲述怎么去构建商户自己的聚合支付平台,以及投产上线后所需要主意的事项,打造一套简单、稳定、高效的聚合支付平台。

一个完善的聚合支付系统,拥有支付网关、主动对账、退款网关、支付/退款状态查询等功能模块。
我会以LNMP架构为基础,细分成六个章节对每一部分做尽量详细的说明。

支付接口

聚合支付平台的核心,就是怎么合理的去管理接入的各种支付SDK,很多童鞋从官网下载到SDK,几乎不做任何逻辑修改,就直接放到项目的目录中使用,这样做虽然开发成本很低,但弊端颇多,首先要说的就是不易维护,各支付SDK代码结构、风格不一样,后期维护成功高;代码各自为政,没有统一的调用方法;配置分散,无法集中维护系统配置项;无法提供统一有效的日志数据等。因此,我建议首先定义一个Interface,代码如下:

<?php

namespace SuperAvalon\Payment\Interfaces;

/**
 * PaymentHandlerInterface
 *
 * PHP 7.3 compatibility interface
 *
 * @package	    SuperAvalon
 * @subpackage	Payment
 * @category	Payment Libraries
 * @Framework   Lumen/Laravel
 * @author	    Eric <think2017@gmail.com>
 * @Github	    https://github.com/SuperAvalon/Payment/
 * @Composer	https://packagist.org/packages/superavalon/payment
 */
interface PaymentHandlerInterface {

	public function doPay($payParams = array());
	public function doQuery($queryParams = array());
	public function doRefund($refundParams = array());
	public function doRefundQuery($queryParams = array());
	public function payNotifyHandler(&$input);
	public function refundNotifyHandler(&$input);
}
然后,每次接入新的支付方式的过程,其实就是实现该Interface的过程。

通常情况下,一种支付方式有一个class[将其class备注为支付类]来实现,但面对一种支付方式提供了多种支付场景,比如微信(提供了公众号支付、APP支付、扫码支付、H5支付、小程序支付、微信免密代扣等)、中国银联(提供了PC网关支付、WAP支付、APP支付、银联云闪付等),我们该怎么办,我建议针对每种不同的支付场景,都有单独的class来实现,理由如下:

  • 不同的支付场景,程序执行的流程也不一样,比如中国银联PC网关支付,是需要将支付报文通过客户端浏览器表单POST给银联支付网关,跳转至银联支付网页进行支付,而银联APP支付则是通过curl将支付报文提交给银联支付网关,再将其返回的tn码返回给商户APP,商户APP凭该tn码发起支付交易;
  • 对订单系统的订单支付方式展示更加准确,分配给商户不同购物平台(PC端、H5端、APP)的支付方式id是唯一的。如果商户系统不同支付场景所申请的商户号不一样,则需要在推送至财务系统的支付方式也不能重复,否则无法对账;
  • 支付类的代码逻辑只关注于自身的支付逻辑处理,不引入额外的判断流程。

那么,就有童鞋就会想到了,一个很头疼的问题,代码冗余。大部分第三方支付,虽然提供了不同支付场景,但基础接口都是一样的,只是部分参数不同,或支付流程上面的少许差别。这时候我们就要考虑好以第三方支付平台为单位来封装一个支付抽象类类,实现对第三方支付平台的所有api对接,不涉及到商户系统的业务流程,比如微信支付,我们创建一个WechatDriver抽象类,代码如下图:

支付抽象类

<?php

namespace SuperAvalon\Payment;

use SuperAvalon\Payment\Utils\PaymentUtils;
use SuperAvalon\Payment\Utils\CommonUtils;
use SuperAvalon\Payment\Interfaces\PaymentHandlerInterface;


/**
 * WechatDriver
 * 微信支付底层抽象类
 * PHP 7.3 compatibility interface
 *
 * @package	    SuperAvalon
 * @subpackage	Payment
 * @category	Payment Libraries
 * @Framework   Lumen/Laravel
 * @author	    Eric <think2017@gmail.com>
 * @Github	    https://github.com/SuperAvalon/Payment/
 * @Composer	https://packagist.org/packages/superavalon/payment
 */
abstract class WechatDriver implements PaymentHandlerInterface {
    
    use PaymentUtils, CommonUtils;
    
	protected $_config;
    
	protected $_extra;
    
	/**
	 * Class constructor
	 *
	 * @param	array	$apiConfig	Configuration parameters
	 * @return	void
	 */
	public function __construct(&$apiConfig)
	{
		$this->_config =& $apiConfig;
	}
    
    
    public function getExtraFields()
    {
        return $this->_extra;
    }
    
	/**
	 * 统一下单接口
	 *
	 * @param array $payParams 
	 * @return array $retval
	 *      code    int         状态码
	 *      type    string      支付凭证类型:prepay_id|code_url|mweb_url
	 *      data    string      支付凭证
	 */
	protected function unified_order($payParams)
	{
		$apiParams = [];
        
		$apiParams['body']		    = $this->_config['mch_name'] . '-' . $payParams['subject'];
		$apiParams['appid']		    = $this->_config['app_id'];
		$apiParams['mch_id']	    = $this->_config['mch_id'];
		$apiParams['total_fee']	    = $payParams['total_fee'];
		$apiParams['trade_type']	= $payParams['trade_type'];
		$apiParams['out_trade_no']  = $payParams['trade_no'];
		$apiParams['notify_url']	= $this->_config['notify_url'];
		$apiParams['nonce_str']	    = $this->create_noncestr(32);
        
        if (isset($payParams['openid'])) {
            $apiParams['openid'] = $payParams['openid'];
        }
        
        if (isset($payParams['scene_info'])) {
            $apiParams['scene_info'] = $payParams['scene_info'];
        }
        
        if ($payParams['trade_type'] == 'NATIVE') {
            $apiParams['product_id'] = $payParams['product_id'];
        }
        
        if (isset($payParams['spbill_create_ip'])) {
            $apiParams['spbill_create_ip'] = $payParams['spbill_create_ip'];
        } else {
            $apiParams['spbill_create_ip'] = $this->get_client_ip();
        }
        
        $retval = [];
        $apiParams['sign'] = $this->unified_sign($apiParams, $this->_config['sign_type']);
        
        $xmlInfo = $this->array_to_xml($apiParams);
        $sResponse = $this->request($this->_config['order_api'], $xmlInfo);
        $aResponse = $this->xml_to_array($sResponse);
        
        $this->write_log(__METHOD__, $payParams['trade_no'], $apiParams, $aResponse);
        
		if ($aResponse && $aResponse['return_code'] == 'SUCCESS' && $aResponse['result_code'] == 'SUCCESS') {
			if ($aResponse['trade_type'] == 'JSAPI' || $aResponse['trade_type'] == 'APP' || $aResponse['trade_type'] == 'WAP') {
				$retval = ['code' => 200, 'data' => ['type' => 'prepay_id', 'value' => $aResponse['prepay_id']]];
			} elseif ($aResponse['trade_type'] == 'NATIVE') {
                $retval = ['code' => 200, 'data' => ['type' => 'code_url', 'value' => $aResponse['code_url']]];
			} elseif ($aResponse['trade_type'] == 'MWEB') {
                $retval = ['code' => 200, 'data' => ['type' => 'mweb_url', 'value' => $aResponse['mweb_url']]];
			}
		}
        
        if (empty($retval)) {
            if (isset($aResponse['err_code']) && isset($aResponse['err_code_des'])) {
                $retval = ['code' => 500, 'api_err_code' => $aResponse['err_code'], 'api_error_msg' => $aResponse['err_code_des']];    
            } elseif (isset($aResponse['return_code']) && isset($aResponse['return_msg'])) {
                $retval = ['code' => 500, 'api_err_code' => $aResponse['return_code'], 'api_error_msg' => $aResponse['return_msg']];    
            }
        }
		
		return $retval;
	}
    
     
	/**
	 * 订单支付状态查询接口
	 *
	 * @param string $tradeNo   订单支付单号 
	 * @return array $retval
	 *      code    int         状态码
	 *      data    string      接口返回报文
	 */
    public function order_query($tradeNo)
    {
		$apiParams = [];
        
        $apiParams['out_trade_no']	= $tradeNo;
		$apiParams['appid']		    = $this->_config['app_id'];
		$apiParams['mch_id']		= $this->_config['mch_id'];
		$apiParams['nonce_str']	    = $this->create_noncestr(32);
        $apiParams['sign']          = $this->unified_sign($apiParams, $this->_config['sign_type']);
        
        $xmlInfo = $this->array_to_xml($apiParams);
        $sResponse = $this->request($this->_config['query_api'], $xmlInfo);
        $aResponse = $this->xml_to_array($sResponse);
        
        $this->write_log(__METHOD__, $tradeNo, $apiParams, $aResponse);
        
        $postSign = $aResponse['sign'];
        unset($aResponse['sign']);
        
        $retval = [];
        $paySign = $this->unified_sign($aResponse, $this->_config['sign_type']);
        
        if (strtolower($paySign) != strtolower($postSign)) {
            return ['code' => 407, 'data' => $aResponse, 'msg' => 'Verify Error.'];
		}
        
        if ($aResponse['return_code'] == 'SUCCESS' && $aResponse['result_code'] == 'SUCCESS') {
           $retval = ['code' => 200, 'data' => $aResponse, 'msg' => 'Success.'];
        } else {
           $retval = ['code' => 403, 'data' => $aResponse, 'msg' => 'Error.'];
        }
        
        return $retval;
    }
    
     
	/**
	 * 微信支付/退款异步通知
	 *
	 * @param string $input     通知报文 
	 * @return array $retval
	 *      code    int         状态码
	 *      data    string      接口返回报文
	 */
	public function notify(&$input)
	{
        $aResponse = (array)simplexml_load_string($input, 'SimpleXMLElement', LIBXML_NOCDATA);
		
        $postSign = $aResponse['sign'];
        unset($aResponse['sign']);
        
        $retval = [];
        $paySign = $this->unified_sign($aResponse, $this->_config['sign_type']);
        
        if (strtolower($paySign) != strtolower($postSign)) {
            return ['code' => 407, 'data' => $aResponse, 'msg' => 'Verify Error.'];
		}

        if ($aResponse['return_code'] == 'SUCCESS' && $aResponse['result_code'] == 'SUCCESS') {
            $retval = ['code' => 200, 'data' => $aResponse, 'msg' => 'Success.'];
        } else {
            $retval = ['code' => 403, 'data' => $aResponse, 'msg' => 'Error.'];
        }
        
        return $retval;
	}
    
    
	/**
	 * 退款申请接口
	 *
	 * @param array $refundParams   退款单数据 
	 *      trade_no    string      订单支付单号
	 *      refund_no    string     订单退款单号
	 *      total_fee    float      订单实付金额
	 *      refund_fee   float      退款申请金额
	 * @return array $retval
	 *      code    int         状态码
	 *      data    string      接口返回报文
	 */
    public function refund($refundParams)
    {        
        $apiParams = [];
        $totalFee = (int)bcmul($refundParams['total_fee'], 100);
        $refundFee = (int)bcmul($refundParams['refund_fee'], 100);

        $apiParams['out_trade_no']	= $refundParams['trade_no'];
        $apiParams['out_refund_no']	= $refundParams['refund_no'];
        $apiParams['total_fee']	    = $totalFee;
        $apiParams['refund_fee']	= $refundFee;
        $apiParams['appid']		    = $this->_config['app_id'];
        $apiParams['mch_id']		= $this->_config['mch_id'];
        $apiParams['nonce_str']	    = $this->create_noncestr(32);
        $apiParams['sign']          = $this->unified_sign($apiParams, $this->_config['sign_type']);

        $certFile = [
            'cert' => storage_path($this->_config['refund_ssl_cert']),
            'key' => storage_path($this->_config['refund_ssl_key']),
        ];
        
        $xmlInfo = $this->array_to_xml($apiParams);
        $sResponse = $this->request($this->_config['refund_api'], $xmlInfo, 10, [], $certFile);
        $aResponse = $this->xml_to_array($sResponse);
        
        $this->write_log(__METHOD__, $refundParams['trade_no'], $apiParams, $aResponse);
       
        $postSign = $aResponse['sign'];
        unset($aResponse['sign']);
        
        $retval = [];
        $paySign = $this->unified_sign($aResponse, $this->_config['sign_type']);
        
        if (strtolower($paySign) != strtolower($postSign)) {
            return ['code' => 407, 'data' => $aResponse, 'msg' => 'Verify Error.'];
		}
        
        if ($aResponse['return_code'] == 'SUCCESS' && $aResponse['result_code'] == 'SUCCESS') {
            $retval = ['code' => 200, 'data' => $aResponse, 'msg' => 'Success.'];
        } else {
            $retval = ['code' => 403, 'data' => $aResponse, 'msg' => 'Error.'];
        }
        
        return $retval;
    }
    
    
	/**
	 * 订单退款状态查询接口
	 *
	 * @param string $refundNo  订单退款单号 
	 * @return array $retval
	 *      code    int         状态码
	 *      data    string      接口返回报文
	 */
    public function refund_query($refundNo)
    {
		$apiParams = [];
        
        $apiParams['out_refund_no']	= $refundNo;
		$apiParams['appid']		    = $this->_config['app_id'];
		$apiParams['mch_id']		= $this->_config['mch_id'];
		$apiParams['nonce_str']	    = $this->create_noncestr(32);
        $apiParams['sign']          = $this->unified_sign($apiParams, $this->_config['sign_type']);
        
        $xmlInfo = $this->array_to_xml($apiParams);
        $sResponse = $this->request($this->_config['refund_query_api'], $xmlInfo);
        $aResponse = $this->xml_to_array($sResponse);
        
        $this->write_log(__METHOD__, $refundNo, $apiParams, $aResponse);
        
        $postSign = $aResponse['sign'];
        unset($aResponse['sign']);
        
        $retval = [];
        $paySign = $this->unified_sign($aResponse, $this->_config['sign_type']);
        
        if (strtolower($paySign) != strtolower($postSign)) {
            return ['code' => 407, 'data' => $aResponse, 'msg' => 'Verify Error.'];
		}
        
        if ($aResponse['return_code'] == 'SUCCESS' && $aResponse['result_code'] == 'SUCCESS') {
            $retval = ['code' => 200, 'data' => $aResponse, 'msg' => 'Success.'];
        } else {
            $retval = ['code' => 403, 'data' => $aResponse, 'msg' => 'Error.'];
        }
        
        return $retval;
    }
    
    
	/**
	 * 下载对账单
	 * @param string $billDate   对账单日期 
	 * @param string $billType   对账单类型 
	 * @return array $retval
	 *      code    int         状态码
	 *      data    string      接口返回报文
	 */
    protected function downloadbill($billDate, $billType)
    {
        $apiParams = [];

        $apiParams['bill_date']	    = $billDate;
        $apiParams['bill_type']	    = $billType;
        $apiParams['appid']		    = $this->_config['app_id'];
        $apiParams['mch_id']		= $this->_config['mch_id'];
        $apiParams['nonce_str']	    = $this->create_noncestr(32);
        $apiParams['sign']          = $this->unified_sign($apiParams, $this->_config['sign_type']);
        
        $xmlInfo = $this->array_to_xml($apiParams);
        $sResponse = $this->request($this->_config['down_bill_api'], $xmlInfo);
        $aResponse = $this->xml_to_array($sResponse);
        
        $postSign = $aResponse['sign'];
        unset($aResponse['sign']);
        
        $retval = [];
        $paySign = $this->unified_sign($aResponse, 'md5');
        
        if (strtolower($paySign) == strtolower($postSign)) {
			if ($aResponse['return_code'] == 'SUCCESS') {
                $retval = ['code' => 200, 'data' => $aResponse];
			}
		}
        
        if (empty($retval)) {
            $retval = ['code' => 500, 'data' => $aResponse];
        }
        
        return $retval;
    }
    
    
	/**
	 * 计算签名
     *
	 * @param	array	$data       签名数据
	 * @param	string	$signType   签名类型    md5/sha1
	 * @return	string  签名结果
	 */
    private function unified_sign($data, $signType = 'md5')
	{
        $strInfo = '';
        ksort($data);
		foreach ($data as $key => $val) {
            if ($val === '') {
                continue;
            }
			if ($strInfo) {
				$strInfo .= "&" . $key . "=" . $val;
			} else {
				$strInfo = $key . "=" . $val;
			}
		}
        
        if (strtolower($signType) == 'md5') {
            return strtoupper(md5($strInfo . '&key=' . $this->_config['secret_key']));
        } elseif (strtolower($signType) == 'sha1') {
            return sha1($strInfo . '&key=' . $this->_config['secret_key']);
        } else {
            return false;
        }
    }
}

支付实体类

有了上面的支付抽象类,针对每一种支付方法,都可以继承该抽象类,并拥有自己的独立的支付流程,比如:微信app支付,我们可以创建一个 WechatAppPayment 支付实体类,支付子类调用抽象类提供的各种底层api,来实现支付、查询、退款等功能,代码参考,

<?php

namespace SuperAvalon\Payment;

use SuperAvalon\Payment\Interfaces\PaymentHandlerInterface;

/**
 * WechatAppPayment
 * 微信app支付中间层
 * PHP 7.3 compatibility interface
 *
 * @package	    SuperAvalon
 * @subpackage	Payment
 * @category	Payment Libraries
 * @Framework   Lumen/Laravel
 * @author	    Eric <think2017@gmail.com>
 * @Github	    https://github.com/SuperAvalon/Payment/
 * @Composer	https://packagist.org/packages/superavalon/payment
 */
class WechatAppPayment extends WechatDriver implements PaymentHandlerInterface {
    
	protected $_config;
    
    protected $_extra = [];
    
	/**
	 * Class constructor
	 *
	 * @param	array	$apiParams	Configuration parameters
	 * @return	void
	 */
	public function __construct(&$apiConfig)
	{
		$this->_config =& $apiConfig;
	}
    
    
	/**
	 * 支付接口
	 *
	 * @param array $tradeData 支付订单参数
	 * @return array response
	 *      code    int         状态码
	 *      msg     string      接口消息
	 *      data    array      支付凭证类型:prepay_id
	 */
	public function doPay($tradeData = array())
	{
		$totalFee = (int)bcmul($tradeData['trade_amount'], 100);
        
        $aResponse = $this->unified_order([
            'trade_type' => 'APP',
            'total_fee' => $totalFee,
            'trade_no' => $tradeData['payment_no'],
            'subject' => $tradeData['subject'] ?? '',
            'pay_body' => $tradeData['pay_body'],
        ]);
        
        if ($aResponse['code'] == 200) {
            return $this->retval(['code' => 200, 'data' => ['prepayid' => $aResponse['data']['value']], 'msg' => 'success.']);
        } else {
            return $this->retval(['code' => 500, 'data' => null, 'msg' => 'unifiedorder error.', 'api_error' => $aResponse['api_error_msg']]);
        }
	}
    
    
	/**
	 * 订单查询接口
	 *
	 * @param array $queryParams 订单查询参数
	 *          string $trade_no 支付单号
	 * @return array 
	 */
    public function doQuery($queryParams = array())
    {
        return $this->order_query($queryParams['trade_no']);
    }
    
    
	/**
	 * 订单退款申请接口
	 *
	 * @param string $refundParams 退款单数据
	 * @return array 
	 */
    public function doRefund($refundParams = array())
    {
        return $this->refund($refundParams);
    }
    
	/**
	 * 订单退款状态查询接口
	 *
	 * @param string $refundNo 订单退款单号
	 * @return array 
	 */
    public function doRefundQuery($queryParams = array())
    {
        return $this->refund_query($queryParams['refund_no']);
    }
    
	/**
	 * 支付通知报文解析验签
	 *
	 * @param string $tradeNo 通知报文
	 * @return array 
	 */
    public function payNotifyHandler(&$input)
    {
        return $this->notify($input);
    }
    
	/**
	 * 退款通知报文解析验签
	 *
	 * @param string $input 通知报文
	 * @return array 
	 */
    public function refundNotifyHandler(&$input)
    {
        return;
    }
}

UML类图 

上面分别提到了 支付Interface、支付抽象类、支付实体类,它们之间的关系是怎样的,见下图

以下UML类图,只以微信、银联部分类为基础,可参考
 

对上图做简要说明,

PaymentHandlerInterface是所有支付类的接口,系统所有支付功能类都需要实现它;

Wechat_driver、Unionpay_driver为对接第三方支付接口的支付抽象类,需要实现第三方支付接口的所有API交互,为支付功能类提供功能方法;

Wechat_app_driver、Wechat_mweb_driver、Wechat_native_driver、Unionpay_app_driver、Unionpay_wap_driver为系统支付功能类,调用抽象类的各基础方法,为系统提供支付、查询、退款、退款查询等功能;

Common_utils、Payment_utils为系统工具类,Common_utils可提供诸如curl封装、日志函数、dns查询等系统可以通用的方法,Payment_utils可封装xml数据解析、各种加密解密函数等第三方支付平台所需的方法;

不定时更新,原著文章,谢绝转载,

 

  • 19
    点赞
  • 108
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Java聚合支付系统源码是指使用Java语言开发的一个聚合支付系统的源代码。聚合支付系统是一种整合多种支付渠道的系统,通过一个平台接入多个支付渠道,实现多种支付方式的统一管理和使用。该系统的源码包括了系统的各个模块的实现代码。 Java聚合支付系统源码通常涵盖了以下几个方面的内容: 1. 支付渠道的接入和集成:通过源码可以了解和学习如何接入各种支付渠道,包括支付宝、微信支付、银联支付等。源码中会包含各种支付接口的封装和调用示例,以及接口回调的处理代码。 2. 支付流程的实现:聚合支付系统的核心功能是整合多个支付渠道,使得用户可以灵活选择支付方式。源码中会包含支付流程的具体实现,包括用户选择支付方式、提交支付请求、回调处理以及支付结果的返回等。 3. 订单管理和支付状态的管理:聚合支付系统需要对订单和支付状态进行管理和记录。源码中会包含订单创建、查询、更新等操作的代码,以及支付状态更新和查询的代码。 4. 安全和风控机制的实现:支付系统需要考虑安全和风险控制,源码中会包含支付数据的加密处理、防止订单重复提交的机制以及异常情况的处理代码。 Java聚合支付系统源码的实现需要使用Java的开发框架和技术,如Spring、MyBatis等。同时,也需要了解支付渠道的接口规范和开发文档。 通过阅读和理解Java聚合支付系统源码,可以了解和学习到支付系统的整体架构和设计思路,从而可以自行开发和定制符合自身需求的聚合支付系统

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值