Home > Backend Development > PHP Tutorial > PHP backend UnionPay payment and refund examples

PHP backend UnionPay payment and refund examples

陈政宽~
Release: 2023-03-11 21:32:01
Original
4132 people have browsed it

This article mainly introduces the PHP back-end UnionPay payment and refund example code. The editor thinks it is quite good, so I will share it with you now and give it as a reference. Let’s follow the editor to take a look.

Statement: This article is based on the latest official UnionPay SDK (2016-08-09 version 5.1.0). If the packages are different, please check whether this is the case. Version

Recently encountered UnionPay payment and related refunds (this article only takes mobile control payment as a premise). The following will write down the problems encountered during the period and the basic process. Before that, go through the official This picture shows some things we need to do for back-end personnel in a payment process

As can be seen from this picture, the back-end is responsible for 1. Platform orders Generate; 2. Push orders to the UnionPay omni-channel platform; 3. Return the tn code to the front-end for payment; 4. Process front-end notifications and asynchronous notifications from the omni-channel platform.

There are three difficulties here, order push, asynchronous notification processing, and order status query.

Download the relevant packages through the official email instructions and put them into the back-end php code. (If you download the payment control, you will probably see only the IOS and Android versions of the SDK. For the back-end , just download any one, PHP code is placed in it); then carefully read the readme.txt file in the SDK, and then follow the following steps:

1. Related parameter configuration

During the docking process, use the test environment in the assets folder of sdk Configuration file and certificate, place them in the sdk folder, and configure the /sdk/SDKconfig.php file to be read correctly Get the acp_sdk.ini configuration file.

Configure the absolute addresses of the four files acpsdk.signCert.path, acpsdk.encryptCert.path, acpsdk.rootCert.path, acpsdk.middleCert.path in the acp_sdk.ini file (just customize the file path ).

During the project development process, errors such as certificate absolute addresses may occur due to different systems or different project addresses. Especially in actual production environments, it is very easy to have different project deployment file addresses, making it impossible to After each update, the certificate address must be changed. I have modified the SDKconfig.php in the SDK to be compatible with different file addresses. Please click to expand and view

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

<?php

namespace com\unionpay\acp\sdk;;

include_once &#39;log.class.php&#39;;

include_once &#39;common.php&#39;;

  

class SDKConfig {

    

  private static $_config = null;

  public static function getSDKConfig(){

    if (SDKConfig::$_config == null ) {

      SDKConfig::$_config = new SDKConfig();

    }

    return SDKConfig::$_config;

  }

    

  private $frontTransUrl;

  private $backTransUrl;

  private $singleQueryUrl;

  private $batchTransUrl;

  private $fileTransUrl;

  private $appTransUrl;

  private $cardTransUrl;

  private $jfFrontTransUrl;

  private $jfBackTransUrl;

  private $jfSingleQueryUrl;

  private $jfCardTransUrl;

  private $jfAppTransUrl;

  private $qrcBackTransUrl;

  private $qrcB2cIssBackTransUrl;

  private $qrcB2cMerBackTransUrl;

    

  private $signMethod;

  private $version;

  private $ifValidateCNName;

  private $ifValidateRemoteCert;

    

  private $signCertPath;

  private $signCertPwd;

  private $validateCertDir;

  private $encryptCertPath;

  private $rootCertPath;

  private $middleCertPath;

  private $frontUrl;

  private $backUrl;

  private $secureKey;

  private $logFilePath;

  private $logLevel;

  

  function construct(){

  

    //如果想把acp_sdk.ini挪到其他路径的话,请修改下面这行指定绝对路径。

    $configFilePath = dirname(FILE) . "/acp_sdk.ini";

    $certsFilePath = dirname(dirname(FILE)) . "/certs/";

      

    if(!file_exists($configFilePath)){

      $logger = LogUtil::getLogger();

      $logger->LogError("配置文件加载失败,文件路径:[" . $configFilePath . "].请检查启动php的用户是否有读权限。");

      return;

    }

    $ini_array = parse_ini_file($configFilePath, true);

    $sdk_array = $ini_array["acpsdk"];

    $this->frontTransUrl = array_key_exists("acpsdk.frontTransUrl", $sdk_array)?$sdk_array["acpsdk.frontTransUrl"] : null;

    $this->backTransUrl = array_key_exists("acpsdk.backTransUrl", $sdk_array)?$sdk_array["acpsdk.backTransUrl"] : null;

    $this->singleQueryUrl = array_key_exists("acpsdk.singleQueryUrl", $sdk_array)?$sdk_array["acpsdk.singleQueryUrl"] : null;

    $this->batchTransUrl = array_key_exists("acpsdk.batchTransUrl", $sdk_array)?$sdk_array["acpsdk.batchTransUrl"] : null;

    $this->fileTransUrl = array_key_exists("acpsdk.fileTransUrl", $sdk_array)?$sdk_array["acpsdk.fileTransUrl"] : null;

    $this->appTransUrl = array_key_exists("acpsdk.appTransUrl", $sdk_array)?$sdk_array["acpsdk.appTransUrl"] : null;

    $this->cardTransUrl = array_key_exists("acpsdk.cardTransUrl", $sdk_array)?$sdk_array["acpsdk.cardTransUrl"] : null;

    $this->jfFrontTransUrl = array_key_exists("acpsdk.jfFrontTransUrl", $sdk_array)?$sdk_array["acpsdk.jfFrontTransUrl"] : null;

    $this->jfBackTransUrl = array_key_exists("acpsdk.jfBackTransUrl", $sdk_array)?$sdk_array["acpsdk.jfBackTransUrl"] : null;

    $this->jfSingleQueryUrl = array_key_exists("acpsdk.jfSingleQueryUrl", $sdk_array)?$sdk_array["acpsdk.jfSingleQueryUrl"] : null;

    $this->jfCardTransUrl = array_key_exists("acpsdk.jfCardTransUrl", $sdk_array)?$sdk_array["acpsdk.jfCardTransUrl"] : null;

    $this->jfAppTransUrl = array_key_exists("acpsdk.jfAppTransUrl", $sdk_array)?$sdk_array["acpsdk.jfAppTransUrl"] : null;

    $this->qrcBackTransUrl = array_key_exists("acpsdk.qrcBackTransUrl", $sdk_array)?$sdk_array["acpsdk.qrcBackTransUrl"] : null;

    $this->qrcB2cIssBackTransUrl = array_key_exists("acpsdk.qrcB2cIssBackTransUrl", $sdk_array)?$sdk_array["acpsdk.qrcB2cIssBackTransUrl"] : null;

    $this->qrcB2cMerBackTransUrl = array_key_exists("acpsdk.qrcB2cMerBackTransUrl", $sdk_array)?$sdk_array["acpsdk.qrcB2cMerBackTransUrl"] : null;

  

    $this->signMethod = array_key_exists("acpsdk.signMethod", $sdk_array)?$sdk_array["acpsdk.signMethod"] : null;

    $this->version = array_key_exists("acpsdk.version", $sdk_array)?$sdk_array["acpsdk.version"] : null;

    $this->ifValidateCNName = array_key_exists("acpsdk.ifValidateCNName", $sdk_array)?$sdk_array["acpsdk.ifValidateCNName"] : "true";

    $this->ifValidateRemoteCert = array_key_exists("acpsdk.ifValidateRemoteCert", $sdk_array)?$sdk_array["acpsdk.ifValidateRemoteCert"] : "false";

  

    $this->signCertPath = $certsFilePath . (array_key_exists("acpsdk.signCert.path", $sdk_array)?$sdk_array["acpsdk.signCert.path"]: null);

    $this->signCertPwd = array_key_exists("acpsdk.signCert.pwd", $sdk_array)?$sdk_array["acpsdk.signCert.pwd"]: null;

      

    $this->validateCertDir = array_key_exists("acpsdk.validateCert.dir", $sdk_array)? $sdk_array["acpsdk.validateCert.dir"]: null;

    $this->encryptCertPath = $certsFilePath . (array_key_exists("acpsdk.encryptCert.path", $sdk_array)? $sdk_array["acpsdk.encryptCert.path"]: null);

    $this->rootCertPath = $certsFilePath . (array_key_exists("acpsdk.rootCert.path", $sdk_array)? $sdk_array["acpsdk.rootCert.path"]: null);

    $this->middleCertPath = $certsFilePath . (array_key_exists("acpsdk.middleCert.path", $sdk_array)?$sdk_array["acpsdk.middleCert.path"]: null);

      

    $this->frontUrl = array_key_exists("acpsdk.frontUrl", $sdk_array)?$sdk_array["acpsdk.frontUrl"]: null;

    $this->backUrl = array_key_exists("acpsdk.backUrl", $sdk_array)?$sdk_array["acpsdk.backUrl"]: null;

      

    $this->secureKey = array_key_exists("acpsdk.secureKey", $sdk_array)?$sdk_array["acpsdk.secureKey"]: null;

    $this->logFilePath = array_key_exists("acpsdk.log.file.path", $sdk_array)?$sdk_array["acpsdk.log.file.path"]: null;

    $this->logLevel = array_key_exists("acpsdk.log.level", $sdk_array)?$sdk_array["acpsdk.log.level"]: null;

      

  }

  

  public function get($property_name)

  {

    if(isset($this->$property_name))

    {

      return($this->$property_name);

    }

    else

    {

      return(NULL);

    }

  }

}

Copy after login

2. Omni-channel products Order push

Please click to view the relevant code

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

use com\unionpay\acp\sdk\AcpService;

use com\unionpay\acp\sdk\LogUtil;

use com\unionpay\acp\sdk\SDKConfig;

  

  /**

   * 银联支付下单

   *

   * @param $orders

   * @param $orders_type

   * @return array

   */

  public function unionPay($orders, $orders_type = 0)

  {

    include_once dirname(dirname(dirname(FILE))) . &#39;/Model/unionpay-sdk/sdk/acp_service.php&#39;;

    $config = new SDKConfig();

    $AcpService = new AcpService();

    $log = LogUtil::getLogger();

    $time = date(&#39;YmdHis&#39;, time());

    $params = array(

  

      //以下信息非特殊情况不需要改动

      &#39;version&#39; => $config->getSDKConfig()->version,         //版本号

      &#39;encoding&#39; => &#39;utf-8&#39;,         //编码方式

      &#39;txnType&#39; => &#39;01&#39;,           //交易类型

      &#39;txnSubType&#39; => &#39;01&#39;,         //交易子类

      &#39;bizType&#39; => &#39;000201&#39;,         //业务类型

      &#39;frontUrl&#39; => $config->getSDKConfig()->frontUrl, //前台通知地址

      &#39;backUrl&#39; => $this->getURL(&#39;api_pay_unionpay_call_back&#39;),  //后台通知地址

      &#39;signMethod&#39; => $config->getSDKConfig()->signMethod,         //签名方法

      &#39;channelType&#39; => &#39;08&#39;,         //渠道类型,07-PC,08-手机

      &#39;accessType&#39; => &#39;0&#39;,        //接入类型

      &#39;currencyCode&#39; => &#39;156&#39;,      //交易币种,境内商户固定156

  

      //TODO 以下信息需要填写

      &#39;merId&#39; => $this->getParameter(&#39;mer_id&#39;),   //商户代码,请改自己的测试商户号

      &#39;orderId&#39; => $orders["order_no"],  //商户订单号,8-32位数字字母,不能含“-”或“_”

      &#39;txnTime&#39; => $time, //订单发送时间,格式为YYYYMMDDhhmmss,取北京时间

      &#39;txnAmt&#39; => $orders[&#39;total_price&#39;] * 100,  //交易金额,单位分

    );

  

    $AcpService->sign ( $params ); // 签名

    $url = $config->getSDKConfig()->appTransUrl;

  

    $result_arr = $AcpService->post ($params, $url);

  

    if(count($result_arr)<=0) { //没收到200应答的情况 $log->LogInfo(&#39;没收到200应答的情况&#39;);

    }

  

//    $this->printResult ($url, $params, $result_arr ); //页面打印请求应答数据

  

    if (!$AcpService->validate ($result_arr) ){

      $log->LogInfo(&#39;应答报文验签失败&#39;);

    }

    if ($result_arr["respCode"] == "00"){

      //成功

      return array(&#39;txn_time&#39;=>$time, &#39;tn&#39;=>$result_arr["tn"]);

//      echo "后续请将此tn传给手机开发,由他们用此tn调起控件后完成支付。

\n";

//      echo "手机端demo默认从仿真获取tn,仿真只返回一个tn,如不想修改手机和后台间的通讯方式,【此页面请修改代码为只输出tn】。

\n";

    } else {

      //其他应答码做以失败处理

      return array(&#39;txn_time&#39;=>$time, &#39;tn&#39;=>0);

      //echo "失败:" . $result_arr["respMsg"] . "。

\n";

  

    }

  }

Copy after login

Please note that the txnTime format should not be transmitted incorrectly. There should be no problems in the test environment, and the obtained tn will be returned. APP can make payment

3. Asynchronous notification processing and order transaction status query

The main function of this step is to process UnionPay transaction success information, and try to Avoid problems caused by unhandled callbacks.

Let’s talk about asynchronous notification processing first. This step is the main basis for order status modification. There are no actual difficulties, just make sure there are no problems with the relevant parameters

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

/**

   * 银联回调

   *

   * @param Request $request

   * @return array|Response

   */

  public function unionPayCallBackAction(Request $request)

  {

    if ($request->get(&#39;type&#39;) == 1){//前台通知-进行订单状态查询

      $query = $this->unionPayQuery($request, array(), 1);

  

      return new JsonResponse($query);

    }

  

    require_once dirname(dirname(dirname(FILE))) . "/Model/unionpay-sdk/sdk/acp_service.php";

    $log = LogUtil::getLogger();

    $AcpService = new AcpService();

  

  

    if ($request->request->has(&#39;signature&#39;) && $AcpService->validate($_POST)) {

      $order_no = $request->request->get(&#39;orderId&#39;);

      $respCode = $request->request->get(&#39;respCode&#39;);

      $total = $request->request->get(&#39;txnAmt&#39;); // 交易金额

      if ($respCode === &#39;00&#39; || $respCode === &#39;A6&#39;) {

        $trade_no = $request->request->get(&#39;origQryId&#39;)?:&#39;UN&#39; . date(&#39;YmdHis&#39;, time()) . substr(implode(NULL, array_map(&#39;ord&#39;, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);

        $this->dispose($order_no, $trade_no, 4);//订单交易处理-请根据实际情况自行编写

      }

    } else {

      if (!$request->request->has(&#39;signature&#39;)) {

        $log->LogInfo(&#39;签名为空&#39;);

      } else {

        $log->LogInfo(&#39;验签失败&#39;);

      }

    }

  

    exit;

  }

Copy after login

Order transaction status query

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

   do{//循环查询,直到获取到退款订单的queryID

      sleep($number * 2);

      $query = $this->unionPayQuery(&#39;&#39;, $orders);

      $number += 1;

    }while($query[&#39;errorCode&#39;] != 0 || empty($query[&#39;result_arr&#39;]["queryId"]));

  

public function unionPayQuery($request, $orders)

  {

    require_once dirname(dirname(dirname(FILE))) . "/Model/unionpay-sdk/sdk/acp_service.php";

    $config = new SDKConfig();

    $AcpService = new AcpService();

    $log = LogUtil::getLogger();

    $params = array(

      //以下信息非特殊情况不需要改动

      &#39;version&#39; => $config->getSDKConfig()->version,    //版本号

      &#39;encoding&#39; => &#39;utf-8&#39;,     //编码方式

      &#39;signMethod&#39; => $config->getSDKConfig()->signMethod,     //签名方法

      &#39;txnType&#39; => &#39;00&#39;,       //交易类型

      &#39;txnSubType&#39; => &#39;00&#39;,     //交易子类

      &#39;bizType&#39; => &#39;000000&#39;,     //业务类型

      &#39;accessType&#39; => &#39;0&#39;,    //接入类型

      &#39;channelType&#39; => &#39;07&#39;,     //渠道类型

  

      //TODO 以下信息需要填写

      &#39;orderId&#39; => $orders[&#39;order_no&#39;],  //请修改被查询的交易的订单号,8-32位数字字母,不能含“-”或“_”

      &#39;merId&#39; => $this->getParameter(&#39;mer_id&#39;),   //商户代码,请改自己的测试商户号

      &#39;txnTime&#39; => date(&#39;YmdHis&#39;, time()), //请修改被查询的交易的订单发送时间,格式为YYYYMMDDhhmmss

    );

  

    $AcpService->sign ( $params ); // 签名

    $url = $config->getSDKConfig()->singleQueryUrl;

  

    $result_arr = $AcpService->post ( $params, $url);

    if(count($result_arr)<=0) { //没收到200应答的情况 $log->LogInfo(&#39;没收到200应答的情况&#39;);

    }

  

    if (!$AcpService->validate ($result_arr) ){

      $log->LogInfo(&#39;应答报文验签失败&#39;);

    }

    if ($result_arr["respCode"] == "00"){

      if ($result_arr["origRespCode"] == "00"){

        //交易成功

        $trade_no = &#39;UN&#39; . date(&#39;YmdHis&#39;, time()) . substr(implode(NULL, array_map(&#39;ord&#39;, str_split(substr(uniqid(), 7, 13), 1))), 0, 8);

        $this->dispose($orders[&#39;order_no&#39;], $trade_no, 4);

        $result = array(&#39;errorCode&#39;=>0, &#39;message&#39;=>&#39;交易成功&#39;, &#39;result_arr&#39;=>$result_arr);

  

      } else if ($result_arr["origRespCode"] == "03"

        || $result_arr["origRespCode"] == "04"

        || $result_arr["origRespCode"] == "05"){

        //后续需发起交易状态查询交易确定交易状态

  

        $result = array(&#39;errorCode&#39;=>2, &#39;message&#39;=>&#39;交易处理中&#39;, &#39;result_arr&#39;=>$result_arr);

  

      } else {

        //其他应答码做以失败处理

  

        echo "交易失败:" . $result_arr["origRespMsg"] . "。

\n";

  

        $result = array(&#39;errorCode&#39;=>1, &#39;message&#39;=>"交易失败:" . $result_arr["origRespMsg"] . ".", &#39;result_arr&#39;=>$result_arr);

      }

    } else if ($result_arr["respCode"] == "03"

      || $result_arr["respCode"] == "04"

      || $result_arr["respCode"] == "05" ){

      //后续需发起交易状态查询交易确定交易状态

  

      $result = array(&#39;errorCode&#39;=>2, &#39;message&#39;=>"处理超时,请稍后查询.", &#39;result_arr&#39;=>$result_arr);

    } else {

      //其他应答码做以失败处理

  

  

      $result = array(&#39;errorCode&#39;=>1, &#39;message&#39;=>"失败:" . $result_arr["respMsg"] . ".", &#39;result_arr&#39;=>$result_arr);

    }

  

    return $result;

  }

Copy after login

That's it. If there is no order for the project, the online refund will be completed.

Order refund related

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

public function refundUnionPay($orders)

  {

    require_once(dirname(dirname(FILE)) . "/Model/unionpay-sdk/sdk/acp_service.php");

  

    set_time_limit(100);

  

    $config = new SDKConfig();

    $AcpService = new AcpService();

    $log = LogUtil::getLogger();

    $number = 0;

    do{//循环查询,直到获取到退款订单的queryID

      sleep($number * 2);

      $query = $this->unionPayQuery(&#39;&#39;, $orders);

      $number += 1;

    }while($query[&#39;errorCode&#39;] != 0 || empty($query[&#39;result_arr&#39;]["queryId"]));

    

  

    if ($query[&#39;errorCode&#39;] != 0) {

      return array(&#39;errorCode&#39;=>1, &#39;message&#39;=>&#39;订单未成交,无法退款&#39;);

    }

    $params = array(

  

      //以下信息非特殊情况不需要改动

      &#39;version&#39; => $config->getSDKConfig()->version,      //版本号

      &#39;encoding&#39; => &#39;utf-8&#39;,       //编码方式

      &#39;signMethod&#39; => $config->getSDKConfig()->signMethod,       //签名方法

      &#39;txnType&#39; => &#39;04&#39;,         //交易类型

      &#39;txnSubType&#39; => &#39;00&#39;,       //交易子类

      &#39;bizType&#39; => &#39;000201&#39;,       //业务类型

      &#39;accessType&#39; => &#39;0&#39;,      //接入类型

      &#39;channelType&#39; => &#39;07&#39;,       //渠道类型

      &#39;backUrl&#39; => $config->getSDKConfig()->backUrl, //后台通知地址

  

      //TODO 以下信息需要填写

      &#39;orderId&#39; => "T" . $orders[&#39;order_no&#39;],   //商户订单号,8-32位数字字母,不能含“-”或“_”,可以自行定制规则,重新产生-此处为在退款订单前拼接 T

      &#39;merId&#39; => $this->getParameter(&#39;mer_id&#39;),     //商户代码,请改成自己的商户号

      &#39;origQryId&#39; => $query[&#39;result_arr&#39;]["queryId"], //原消费的queryId,可以从查询接口或者通知接口中获取

      &#39;txnTime&#39; => date(&#39;YmdHis&#39;, time()),    //订单发送时间,格式为YYYYMMDDhhmmss,重新产生,不同于原消费

      &#39;txnAmt&#39; => $orders[&#39;total_price&#39;] * 100,   //交易金额,退货总金额需要小于等于原消费

    );

  

    $AcpService->sign ( $params ); // 签名

    $url = $config->getSDKConfig()->backTransUrl;

  

    $result_arr = $AcpService->post ( $params, $url);

    if(count($result_arr)<=0) { //没收到200应答的情况 return array(&#39;errorCode&#39;=>1, &#39;message&#39;=>"没收到应答.");

    }

  

    if (!$AcpService->validate ($result_arr) ){

      return array(&#39;errorCode&#39;=>1, &#39;message&#39;=>"应答报文验签失败.");

    }

  

    if ($result_arr["respCode"] == "00"){

      //交易已受理,等待接收后台通知更新订单状态,如果通知长时间未收到也可发起交易状态查询

      return array(&#39;errorCode&#39;=>0, &#39;message&#39;=>"受理成功.");

  

    } else if ($result_arr["respCode"] == "03"

      || $result_arr["respCode"] == "04"

      || $result_arr["respCode"] == "05" ){

      //后续需发起交易状态查询交易确定交易状态

      return array(&#39;errorCode&#39;=>1, &#39;message&#39;=>"处理超时,请稍微查询.");

    } else {

      //其他应答码做以失败处理

  

      return array(&#39;errorCode&#39;=>1, &#39;message&#39;=>"失败:" . $result_arr["respMsg"] . ".");

    }

  }

Copy after login

Just perform relevant operations based on the return status value. Please implement the actual logic code yourself

Switch production environment

The project relationship is temporarily unavailable - follow-up supplement

To be continued. . . .

The above is the detailed content of PHP backend UnionPay payment and refund examples. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template