h5调微信支付 unkonw url_聚合支付系统设计(一)
產品概述與整體設計
背景
如今,網購已經滲透到人們日常生活中的方方面面,做為網購的載體,互聯網電商平臺發展如火如荼,支付功能做為其不可或缺的一部分,實現起來,也有各種各樣的方案。根據自己有限的認知,我主觀上把目前行業內的支付實現方案做以下歸類:
- 持有支付業務許可證,又稱支付牌照,自有支付品牌,比如阿里的支付寶、騰訊的微信支付(財付通)、京東的京東支付等;
- 自建第三方支付聚合平臺,對接第三方支付(支付寶、微信支付、中國銀聯、各大商業銀行直連等),為其自有訂單提供支付功能;
- 一些研發資源有限的電商平臺,選擇市場中直接能提供全套聚合支付的支付平臺,省去研發環節,能夠以最短時間較低的成本為其平臺提供支付功能。
我從開發者的角度,主要針對第二類,講述怎么去構建商戶自己的聚合支付平臺,以及投產上線后所需要主意的事項,打造一套簡單、穩定、高效的聚合支付平臺。
整體設計
一個完善的聚合支付系統,擁有支付網關、主動對賬、退款網關、支付/退款狀態查詢等功能模塊。
我會以LNMP架構為基礎,細分成六個章節對每一部分做盡量詳細的說明。
聚合支付平臺的核心,就是怎么合理的去管理接入的各種支付SDK,很多童鞋從官網下載到SDK,幾乎不做任何邏輯修改,就直接放到項目的目錄中使用,這樣做雖然開發成本很低,但弊端頗多,首先要說的就是不易維護,各支付SDK代碼結構、風格不一樣,后期維護成功高;代碼各自為政,沒有統一的調用方法;配置分散,無法集中維護系統配置項;無法提供統一有效的日志數據等。因此,我建議首先定義一個Interface,如下圖:
<?phpnamespace SuperAvalonPaymentInterfaces;/*** 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抽象類,代碼如下圖:
支付抽象類
<?phpnamespace SuperAvalonPayment;use SuperAvalonPaymentUtilsPaymentUtils; use SuperAvalonPaymentUtilsCommonUtils; use SuperAvalonPaymentInterfacesPaymentHandlerInterface;/*** 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,來實現支付、查詢、退款等功能,代碼參考
<?phpnamespace SuperAvalonPayment;use SuperAvalonPaymentInterfacesPaymentHandlerInterface;/*** 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;} }上面分別提到了 支付Interface、支付類、支付基類三個概念,它們之間的關系是怎樣的,見下圖
對上圖做簡要說明,
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數據解析、各種加密解密函數等第三方支付平臺所需的方法;
總結
以上是生活随笔為你收集整理的h5调微信支付 unkonw url_聚合支付系统设计(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: poi 默认2位小数_odoo小数精确度
- 下一篇: 两个sql交集_如何使用性能分析工具定位