保持的力量:接口開發(fā)最佳實踐

2018-11-21 21:18 更新

神啊,求你賜給我平靜的心,去接受我無法改變的事;賜給我勇氣,去做我能改變的事;賜給我智慧,去分辨兩者的不同。 --平靜之禱

1.30.1 論保持的力量

追到一個心儀的女生不難,難于如何保持和培養(yǎng)一份真摯的感情;獲得一時的財富也不難,難于如何長久保持收益;創(chuàng)業(yè)的公司很容易博得一時媒體的關(guān)注以及某次天使的投資,但難于如何排除各種障礙、充分利用各方資源發(fā)展成中企業(yè)及至上市公司。

同樣,提供一時的接口很容易,但當我們需要不斷為接口提供升級,以及當我們維護提供一整套接口時,面臨的困難和問題會越來越大。
所以,這是一場持久的戰(zhàn)役。需要我們用穩(wěn)重的心態(tài)、專業(yè)的能力在背后持久支撐、推動。

值得慶幸的是,這些都是問題而不是限制,都是可以被解決的。
以下是結(jié)合 @郭了個浩浩 同學提供的apigee.web_api.pdf文檔,以及我們多年來的項目實際開發(fā)經(jīng)驗為新手提供的一些建議,對老同學相信也會有所幫助。
每個建議通常會包括三部分: 現(xiàn)在主流的做法、PhalApi的做法以及項目的選取。

1.30.2 最佳實踐建議

為了大家查閱和翻看,這里先羅列本章的全部建議:

  • (1)接口風格和協(xié)議的選擇 - HTTP
  • (2)接口域名 - 使用api單獨域名
  • (3)異常處理 - 200/400/500三大接口結(jié)果狀態(tài)碼
  • (4)對外的命名規(guī)則 - 使用小寫加下劃線
  • (5)對內(nèi)的命名規(guī)則 - 使用駝峰法和遵循PEAR命名
  • (6)安全與驗證 - 使用接口簽名和token登錄態(tài)雙重機制
  • (7)返回結(jié)果格式 - JSON
  • (8)URL規(guī)則與路由映射 - 統(tǒng)一service接口服務(wù),可一個文件一個接口
  • (9)SDK包 - 給客戶端自由的調(diào)用空間和自由
  • (10)接口文檔 - 使用markdown快速編寫
  • (11)測試驅(qū)動開發(fā) - 堅持單元測試

1.30.3 建議細說

(1)接口風格和協(xié)議的選擇 - HTTP

目前,后臺接口開發(fā)可以用RESTFull風格,也可以用Web Service;可以用SOAP協(xié)議、RPC協(xié)議,也可以用HTTP協(xié)議;可以用短鏈接,也可以使用長鏈接。如果我們希望繼續(xù)進行劃分,還可以分為同步或異步、單個或批量、是否有SDK包、內(nèi)部接口還是開放接口平臺等。

主流的做法

現(xiàn)在看來,大部分大型的企業(yè)以及大多數(shù)的小公司使用的都是HTTP協(xié)議下的接口開發(fā),部分使用RESTFull,但Web Service較少。如:

PhalApi的做法

我們選取了HTTP的協(xié)議,在于其無論是客戶端接入、開發(fā)調(diào)試,還是部署構(gòu)建上都很容易實現(xiàn),而且也符合主流,因為大家都比較熟悉。
這一點是非常重要的:因為簡單,后臺接口開發(fā)的同學才會更容易上手;因為容易,客戶端接入才會更加無壓力而不用擔心處處受挫。

項目的選取

根據(jù)項目不同的項目背景和需求,可以選擇你合適的風格或者協(xié)議。但是即使出于安全、性能或者其他技術(shù)或非技術(shù)的原因而不采用HTTP協(xié)議的情況下,你也可以在PhalApi原有的接口開發(fā)實現(xiàn)時,輕松擴展你需要的協(xié)議。如使用SOAP,PHPRpc或者swoole下的TCP協(xié)議。其中,部分協(xié)議已有擴展類庫提供支持。

(2)接口域名 - 使用api單獨域名

首先,有一點是可以肯定的。
接口系統(tǒng)應(yīng)該有自己單獨的域名,而不應(yīng)該附屬于網(wǎng)站或者管理后臺。

主流的做法

顯然,主流做法也是這樣做的。如:

項目的選取

如果可以,盡量讓接口系統(tǒng)使用獨立的域名,并且使用api作為一級域名。如:

//你的網(wǎng)站為:
http://www.demo.com

//則對應(yīng)的接口為:
http://api.demo.com

(3)異常處理 - 200/400/500三大接口結(jié)果狀態(tài)碼

對于接口的異常處理,在使用HTTP協(xié)議下,可以通過HTTP本身的響應(yīng)狀態(tài)碼來進行區(qū)分。在非HTTP協(xié)議并有SDK包的情況下,異常的處理手段則會更為多樣。

主流的做法

優(yōu)酷接口采用了HTTP響應(yīng)狀態(tài)碼加結(jié)果返回的形式,如:

Request URL:https://openapi.youku.com/v2/videos/show_basic.json
Request Method:GET
Status Code:400 Bad Request

{"error":{"code":1004,"type":"SystemException","description":"Client id null"}}

新浪微博也一樣:

Request URL:https://api.weibo.com/2/statuses/mentions/ids.json
Request Method:GET
Status Code:403 Forbidden

{"error":"auth by Null spi!","error_code":21301,"request":"/2/statuses/mentions/ids.json"}

微信接口則采用了統(tǒng)一200的形式,如:

Request URL:https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=
Request Method:GET
Status Code:200 OK

{"errcode":41002,"errmsg":"appid missing"}

PhalApi的做法

為了與HTTP保持一致性,同時降低不必要的復(fù)雜性,我們采用了200/400/500三大接口結(jié)果狀態(tài)碼。

注意,這里所說的三大狀態(tài)碼,是指接口返回結(jié)果中的狀態(tài)碼,而不是HTTP的響應(yīng)狀態(tài)。
也就是說接口全部的結(jié)果返回都應(yīng)該是200,除非接口服務(wù)有內(nèi)部未捕獲的異常,即:

Status Code:200 OK

返回結(jié)果狀態(tài)碼剛是以下幾種:

//正常返回
{
    "ret": 200,
    "data": {
        //...
    },
    "msg": ""
}

//客戶端非法請求
{
    "ret": 400,
    "data": [],
    "msg": "非法請求:接口服務(wù)Default.Test不存在"
}

//服務(wù)端內(nèi)部錯誤
{
    "ret": 500,
    "data": [],
    "msg": "服務(wù)器運行錯誤: can not connect to database db_demo"
}

####項目的選取
你可以根據(jù)你的需要,擴展400和500這兩系列的錯誤,如401表示登錄失敗等。  
此外,在data里面,你也可以添加一個code來表示業(yè)務(wù)級的操作碼,以及客戶端根據(jù)不同的業(yè)務(wù)場景做出不同和反應(yīng)、交互或引導(dǎo)提示。

(4)對外的命名規(guī)則 - 使用小寫加下劃線

對外的命名,是指外部看得到的命名,如接口參數(shù)的名字,接口返回的結(jié)果節(jié)點名字,以及數(shù)據(jù)庫的表名、字段名。

主流的做法

新浪微博采用了小寫加下劃線的做法,如:

//URL
https://c.api.weibo.com/2/friendships/followers/trend_count.json

//請求參數(shù)
source
access_token

//返回結(jié)果
{
    "uid": 10438,
    "result": [            
        {
            "days": "2012-04-04",
            "follower_count_online":"15",  //粉絲數(shù) 
            "active_follower":"14", //活躍粉絲數(shù)
            "loyal_follower":"0"   //互動粉絲數(shù)            
        },
        ....
    ]
}

Amazon采用了首字母大寫且無下劃線的做法,如:

//Responses
HTTP/1.1 200 OK
Date: Wed, 25 Nov 2009 12:00:00 GMT
Connection: close
Server: AmazonS3

<?xml version="1.0" encoding="UTF-8"?>
<BucketLoggingStatus xmlns="http://doc.s3.amazonaws.com/2006-03-01">
  <LoggingEnabled>
    <TargetBucket>mybucketlogs</TargetBucket>
    <TargetPrefix>mybucket-access_log-/</TargetPrefix>
    <TargetGrants>
      <Grant>
        <Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:type="AmazonCustomerByEmail">
          <EmailAddress>user@company.com</EmailAddress>
        </Grantee>
        <Permission>READ</Permission>
      </Grant>
    </TargetGrants>
  </LoggingEnabled>
</BucketLoggingStatus>

PhalApi的做法

我們提倡使用全部小寫加下劃線的命名,因為這樣更符合客戶端的使用,如:接口參數(shù):

//正確的
&user_id=888

//錯誤的
&userId=888

返回字段:

//正確的
"device_type": "cube",

//錯誤的
"deviceType": "cube",

數(shù)據(jù)庫字段:

//正確的
`user_id` bigint(20) DEFAULT '0' COMMENT '創(chuàng)建者的用戶ID',

//錯誤的
`userId` bigint(20) DEFAULT '0' COMMENT '創(chuàng)建者的用戶ID',

項目的選取

不管是使用全部小寫,還是全部大寫,項目都應(yīng)該保持一致的命名風格,而不是混合凌亂的風格。

(5)對內(nèi)的命名規(guī)則 - 使用駝峰法和遵循PEAR命名

與對外命名對應(yīng)的則是對內(nèi)的命名規(guī)則,這里又回歸到了老生常談的PHP代碼風格。
這里不作過多的說明,只是稍作提及。

PhalApi的做法

我們建議使用PEAR包的命名風格,和駝峰法,如下為一個接口示例:

$ vim ./Api/Default.php 
<?php
/**
 * 默認接口服務(wù)類
 *
 * @author: dogstar <chanzonghuang@gmail.com> 2014-10-04
 */

class Api_Default extends PhalApi_Api {

    public function getRules() {
        return array(
            'index' => array(
                'username'  => array('name' => 'username', 'default' => 'PHPer', ),
            ),
        );
    }

    public function index() {
        return array(
            'title' => 'Default Api',
            'content' => T('Hello {name}, Welcome to use PhalApi!', array('name' => $this->username)),
            'version' => PHALAPI_VERSION,
            'time' => $_SERVER['REQUEST_TIME'],
        );
    }
}

項目的選取

你可以選擇你喜歡的風格,但團隊應(yīng)該保持一致。

即便你不喜歡PhalApi約定的PEAR命名,你也可以自行實現(xiàn)內(nèi)部的類加載機制。
當有多個項目或者多個模塊并存時,可以添加模塊名前綴來作區(qū)分,如:

$ tree
.
├── Demo
│   └── Api
│       └── DUser.php
├── MyApp
│   └── Api
│       └── MUser.php
└── Task
    └── Api
        └── TUser.php

$ head */*/*
==> Demo/Api/DUser.php <==
<?php
class Api_DUser extends PhalApi_Api {
}

==> MyApp/Api/MUser.php <==
<?php
class Api_MUser extends PhalApi_Api {
}

==> Task/Api/TUser.php <==
<?php
class Api_TUser extends PhalApi_Api {
}

其它的Domain層和Model層等也類似,這樣可以避免類名沖突,或者IDE開發(fā)環(huán)境下的混淆。

(6)安全與驗證 - 使用接口簽名和token登錄態(tài)雙重機制

既然采用HTTP協(xié)議,那么安全方面就需要接口自身進行保證。

所幸,現(xiàn)在可用的加密手段有多種選擇。
對于接口簽名,我們可以使用非對稱的驗簽方式,如md5;也可以用對稱的方式,如RSA。
最后,為每一個接入的客戶端分配app_key和app_secrect即可。

當然,更好的安全是接口系統(tǒng)再提供登錄態(tài)的驗證,即通常所說的token。這兩者的相合,會為接口增加更好的安全保障。

主流的做法

  • 七牛云存儲,采用Access Key/Secret Key,并且在需要時添加相應(yīng)的憑證
  • 微信公眾號,采用由AppID(應(yīng)用ID)和AppSecret(應(yīng)用密鑰)生成的ACCESS_TOKEN
  • 優(yōu)酷開放平臺,采用應(yīng)用Key client_id

PhalApi的做法

我們不提供具體的接口簽名方案,是因為把這種決策移交給項目應(yīng)用本身進行定制。
而定制也是非常簡單的,只需要簡單的兩步即可:

  • 1、實現(xiàn)過濾器接口 PhalApi_Filter::check();
  • 2、注冊過濾器服務(wù) DI()->filter;

對于token,雖然框架沒有提供內(nèi)置的實現(xiàn),但可以從PhalApi的擴展類庫尋找這種支持,這一點已經(jīng)User擴展類庫支持。

項目的選取

正如PhalApi提供的自由空間,項目可以自行實現(xiàn)接口簽名,和根據(jù)需要是否采用User擴展類庫,或者自行實現(xiàn)token的處理。

(7)返回結(jié)果格式 - JSON

[1.14.1 統(tǒng)一返回的格式]一節(jié)中,已經(jīng)對JSON的返回格式作了說明,這里不再贅述,也只是稍作提及。

主流的做法

目前采用了JSON的格式返回的有:

  • 新浪微博
  • 優(yōu)酷開放平臺
  • 騰訊開放平臺
  • 微信接口

采用了XML格式返回的有:

  • Amazon

PhalApi的做法

我們默認采了JSON的格式返回。

項目的選取

項目可以輕松擴展成其他格式的返回。

(8)URL規(guī)則與路由映射 - 統(tǒng)一service接口服務(wù),可一個文件一個接口

先從項目內(nèi)部的文件劃分說起,通常最為常見的情況是,很多開發(fā)人員都喜歡把很多很多很多接口都塞到一個接口文件里面。

這樣的文件,通常會有2K到3K左右。
我覺得這是一種極端,而且是一種不好的極端。因為文件過大的話,會帶來很多問題。

但與之對立的有另一種做法,即一個文件,一個接口。
這一點,在我之前就職的一家出名的游戲公司中得到了廣泛的認可和遵循。如:

//?service=UserInfo.Go
<?php
class Api_UserInfo extends PhalApi_Api {

     public function go() {
           //TODO
     }
}

//?service=GroupInfo.Go
<?php
class Api_GroupInfo extends PhalApi_Api {

     public function go() {
           //TODO
     }
}

雖然也是一種極端,但卻很好地做到了接口隔離,即不用擔心修改此接口的實現(xiàn)而影響到其他接口服務(wù)。

最后,我們再來聊URL規(guī)則,就更順暢了。如果我們采用一個文件對應(yīng)一個接口,則我們可以省略Action(全部都為go()方法),簡寫成:?service=XXX。
再進一步,我們可以利用接口服務(wù)器(如Nginx)的規(guī)則Rewrite來提供更好的URL規(guī)則,同時盡量隱藏我們的接口內(nèi)部實現(xiàn)細節(jié),如:

//原始地
http://api.demo.com/?service=UserInfo.Go

//簡化地
http://api.demo.com/?service=UserInfo

//再進一步
http://api.demo.com/UserInfo

//或者
http://api.demo.com/UserInfo.json

還有一點需要關(guān)注的就是接口的版本,當有v1,v2,v3等不同的版本時,我們也需要在接口URL中體現(xiàn)這些版本的不同。

主流的做法

PhalApi的做法

目前而言,PhalApi在URL規(guī)則和路由這塊還比較欠缺,沒有像其他網(wǎng)站一樣提供強大的路由支持。
但我們在代碼實現(xiàn)的層面,可以提供不同的入口,以開放給不同的終端(內(nèi)部的或者外部的), 以及不同的版本支持。如:

$ tree Public/
Public/
├── v1
│   └── index.php
├── v2
│   └── index.php
└── v3
    └── index.php

3 directories, 3 files

則對應(yīng)的版本URL則可以為:

//v1版本
http://api.demo.com/v1/?service=Default.Index

//v2版本
http://api.demo.com/v2/?service=Default.Index

//v3版本
http://api.demo.com/v3/?service=Default.Index

項目的選取

項目可以結(jié)合不同的入口,以及接口服務(wù)器的URL規(guī)則Rewrite作一些自定的URL路由。

(9)SDK包 - 給客戶端自由的調(diào)用空間和自由

目前移動開發(fā)主要有iOS、Android、Windowns Phone、網(wǎng)站等不同的終端,各種終端又有不同的語言,如果我們需要提供SDK包,不僅僅需要考慮到縱向的版本升級,還需要維護橫向的多樣性。
而且,如果我們使用的是HTTP協(xié)議,則不必要擔心這些維護的成本,同時給客戶端提供一個自由的空間進行調(diào)用 -- 即客戶端可以自己編寫本身的接口客戶端。

主流的做法

很多國內(nèi)的開放平臺接口都是不提供SDK包的,但有些安全度高的則會,如支持寶。

以下是一些提供了SDK的平臺 :

PhalApi的做法

我們暫時沒有提供SDK包,但對于PHP,有一個簡單的客戶端類,可見: [1.13]-統(tǒng)一的接口請求方式

項目的選取

出于公司產(chǎn)品簇的項目考慮,項目可以內(nèi)部提供SDK給同類的客戶端使用,如分為iOS版的客戶端SDK,以及Android版的客戶端SDK。

小故事:與SDK包的一個真實的痛苦經(jīng)歷

有一點是非常重要的,千萬不要讓不懂PHP語言的人去開發(fā)提供PHP的SDK包,更不要使用所謂的工具自動轉(zhuǎn)換生成SDK包代碼。
在我曾經(jīng)做過的一個項目中,因為需要接入一個接口系統(tǒng),而這個接口是由專業(yè)的JAVA團隊維護的,但他們對PHP語言則是非常薄弱,以致他們使用了工具來生成PHP語言的SDK包。

這就導(dǎo)致了我在接入一個簡單的接口時,卻開發(fā)聯(lián)調(diào)耗費了兩天、測試聯(lián)調(diào)時耗費了在接口調(diào)用超時問題排查上。
而最后找到的原因卻是因為app_key不對而導(dǎo)致服務(wù)端異常,而在SDK包卻隱藏了這一異常錯誤信息,反而給出了time out超時的提示,嚴重誤導(dǎo)了排查的方向!
而當我嘗試深入去調(diào)試SDK時,得到卻又是既沒有code又沒有message的異常!最讓人難以忍受的是,他們提供的SDK包竟然和JAVA的企業(yè)系統(tǒng)一樣復(fù)雜的結(jié)構(gòu)(正如他們是使用工具來生成轉(zhuǎn)換的)!

想象一下,PHP代碼下有\(zhòng)com\sina\webo\sdk\Constants.php這樣類似JAVA的文件結(jié)構(gòu),PHP的同學會作何感想?用JAVA的世界的方式來開發(fā)PHP,顯然是走不通的??!
而執(zhí)意要走的話,到最后就是各種接入的痛苦,稍微按奈不住的同學難免就會因為情緒問題而大開爭論了。而這一切,只是因為非PHP人員使用了自動生成工具。
我覺得,這是一種不負責任的做法,希望大家不要效仿。

(10)接口文檔 - 使用markdown快速編寫

(場外音:通過沐浴法理清了頭緒,繼續(xù)回來執(zhí)筆編寫)。
就我個人經(jīng)歷而言,markdown就是一個開始你會拒絕,接著你會越來越喜歡,到最后會愛不釋手的一個工具。

如果你或者你的團隊還在使用郵件或者work文檔來傳遞共享接口文檔,那就太不應(yīng)該了;如果你正在使用某個WIKI系統(tǒng)進行文檔的維護但卻不喜歡它的編輯或者展示方式時,你可以嘗試使用一下markdown。
正如你現(xiàn)在正在查看的文檔也是通過markdown編寫的。

主流的做法

作為開放接口平臺,文檔肯定是以網(wǎng)站的形式提供。但很多時候,對于我們內(nèi)部的接口或者小項目來說,顯然這樣的成本太大了。
接口,從簡單開始。
我們理應(yīng)一直堅持這一點,所以文檔也是一樣,我們應(yīng)該尋求一種在內(nèi)部快速共享最新接口文檔的途徑。如:

  • 1、使用內(nèi)部WIKI
  • 2、使用開源中國或者其他站點的WIKI(這時可以通過在線編輯或者GIT更新)

項目的選取

你可以根據(jù)項目的需要,或者公司以往的做法,但至少不要再使用郵件或者word文檔。

(11)測試驅(qū)動開發(fā) - 堅持單元測試

單元測試,在PhalApi里面不只一次提到了,這里再次進行說明,是希望能引起大家的關(guān)注,去嘗試體驗一下。

我們都知道,在開發(fā)一個新功能時、新接口時,修復(fù)一個BUG或者作一些大的調(diào)整或者重構(gòu)工作,我們是毫無壓力的,而且這時的成本很低,僅在于開發(fā)人員本身的時間和精力的消耗。
當提測后進入測試階段,測試人員發(fā)現(xiàn)一個BUG后,有些團隊會以禪道或者Bugzilla或其他方式來紀錄和追蹤BUG。這時我們開發(fā)會覺得一個這么小的問題還需要去紀錄、去登記很不值得。然后,我們應(yīng)當注意到這時修復(fù)一個BUG會涉及到測試人員資源的開銷。
當進入了回歸測試階段,特別是多系統(tǒng)交互、跨團隊合作時,一個BUG就會從一個人傳到另一個人,從這個團隊流到那個團隊,這時成本就會逐漸增大。

最后,上線后,當一個奇怪的問題出現(xiàn)后,我們需要定位原因就更加困難重重了。
我曾經(jīng)就經(jīng)歷這樣一番:有用戶發(fā)現(xiàn)游戲的道具減少了。我們一開始以為是某些運營配置、或者數(shù)據(jù)以及用戶的等級限制所引發(fā)的,但在排除了各種業(yè)務(wù)的問題后,到最后卻發(fā)現(xiàn)是PHP中array使用“+”運算而引發(fā)的血案!
在正常情況下,我們都知道array_merge()函數(shù)對于數(shù)值的下標則會追加并重新生成下標序列,即會合并;而數(shù)組+則會去掉相同下標的元素。

但實際情況下,線上BUG所產(chǎn)生的影響不在于排查和修復(fù)的時間成本,而在于在這段時間內(nèi)所損失的金額、數(shù)據(jù)等成本。

當然,從測試的角度上看,測試并不能保證我們的系統(tǒng)沒有BUG,只能說暫時未發(fā)現(xiàn)BUG。
單元測試也一樣,作為開發(fā)人員,我們應(yīng)當在最低成本的時期就及時發(fā)現(xiàn)我們直覺覺得可能會出現(xiàn)的問題并進行修復(fù)。
對我們親手所編寫的代碼負責,并且用客觀的方式來證明我們的代碼目前未發(fā)現(xiàn)問題,而不是主觀認為“我寫的代碼沒有問題”。更不應(yīng)該一次又一次地犯下各種低級或者重復(fù)的錯誤,而讓團隊其他成員對我們喪失信任。

PhalApi一直很注重單元測試,也很注重自動化,為了減輕大家重復(fù)編寫單元測試骨架代碼的痛苦,我們提供了一個可以生成單元測試代碼的腳本。
假設(shè)我們有這么一個類:

<?php

class Api_Default extends PhalApi_Api {

        public function index() {
             //TODO
        }
}

那么,我們可以這樣生成測試代碼:

$ cd .//Demo/Tests
$ phalapi-buildtest ../Api/Default.php Api_Default ./test_env.php 
<?php
/**
 * PhpUnderControl_ApiDefault_Test
 *
 * 針對 ../Api/Default.php Api_Default 類的PHPUnit單元測試
 *
 * @author: dogstar 20150514
 */

require_once dirname(__FILE__) . '/test_env.php';

if (!class_exists('Api_Default')) {
    require dirname(__FILE__) . '/../Api/Default.php';
}

class PhpUnderControl_ApiDefault_Test extends PHPUnit_Framework_TestCase
{
    public $apiDefault;

    protected function setUp()
    {
        parent::setUp();

        $this->apiDefault = new Api_Default();
    }

    protected function tearDown()
    {
    }

    /**
     * @group testGetRules
     */ 
    public function testGetRules()
    {
        $rs = $this->apiDefault->getRules();
    }

    /**
     * @group testIndex
     */ 
    public function testIndex()
    {
        $rs = $this->apiDefault->index();
    }

}

溫馨提示:

  1. 可以先執(zhí)行:ln -s /path/to/PhalApi/phalapi-buildtest /usr/bin/phalapi-buildtest
  2. test_env.php為測試環(huán)境初始化文件,可以在里面引用init.php文件,并作一些調(diào)整
  3. 輸出的測試代碼可以重定向到./Demo/Tests/Api/Api_Default_Test.php,讓測試代碼與產(chǎn)品代碼對齊

最后,我們就可以這樣執(zhí)行單元測試了:

$ phpunit ./Api_Default_Test.php 

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號