_Give it some time, everything will be okay. -- 《那些年我們瘋狂的青春/青春洋溢色彩/This Youth is Crazy》_
## 4.3.1 模擬的業務場景
假設我們需要為“奔跑吧兄弟”綜藝節目開發一套手機App的投票接口,以供用戶在觀看電視的同時,可以進行投票活動參與互動。
下面將以此模擬的業務場景,提供一個接口開發實戰的過程。
## 4.3.2 源代碼下載
[PhalApi-Demo-Vote](http://git.oschina.net/dogstar/PhalApi-Demo-Vote)
## 4.3.3 接口總列表
以下是我們根據業務需求整理出來的接口:
+ 1、用戶可以通過微信、QQ、新浪微博等渠道進行第三方登錄
+ 2、用戶可以創建團隊進行參賽,但隊名不能重復
+ 3、用戶可以對已參賽的團隊進行投票,且每個用戶每天投票最多不能超過3次,支持可配置
+ 4、獲取已參賽團隊的得票排行榜
## 4.3.4 主要涉及技術功能點
+ 1、使用User擴展類庫實現第三方登錄操作
+ 2、使用緩存存放用戶每天投票的次數(為方便起見,使用文件緩存,不落地)
+ 3、對接口進行簽名驗證(為方便起見,固定sign簽名)
+ 4、數據庫的基本操作
+ 5、自動化腳本的使用
## 4.3.4 快速開發流程
(題外音:整個示例的開發,我個人在單元測試驅動開發下,只用了兩個多小時,其中還包括對模板場景的業務構思、建表、編寫單元測試代碼等)。
###(1)創建項目和部署環境
把PhalApi最新的框架代碼下載后,并將User擴展類庫按文檔說明配置后,將項目部署到了以下接口域名:
```
http://api.vote.phalapi.com
```
測試一下:
```javascript
http://api.vote.phalapi.com/vote/?sign=phalapi
//返回
{
"ret": 200,
"data": {
"title": "Hello World!",
"content": "PHPer您好,歡迎使用PhalApi!",
"version": "1.1.4",
"time": 1431796924
},
"msg": ""
}
```
Good! 下面是簡明的開發過程。
###(2)單元測試驅動開發
在定好接口后:
```javascript
<?php
class Api_Act extends PhalApi_Api {
public function joinIn() {
}
public function showList() {
}
public function vote() {
}
}
```
便可使用腳本,快速生成單元測試的骨架代碼:
```javascript
$ cd ./Vote/Tests/Api
$ phalapi_buildtests ../../Api/Act.php Api_Act ../test_env.php > Api_Act_Test.php
```
###(3)快速開發
開發過程此處略,但在單元測試驅動的引導下,很快就產出了以下高質量的代碼:
```javascript
.
├── Api
│?? ├── Act.php
│?? └── Default.php
├── Common
│?? └── SignFilter.php
├── Domain
│?? ├── Team.php
│?? └── Vote.php
├── Model
│?? ├── Team.php
│?? ├── UserVoteRecord.php
│?? └── Vote.php
└── Tests
├── Api
│?? ├── Api_Act_Test.php
│?? └── Api_Default_Test.php
├── Domain
├── Model
├── phpunit.xml
└── test_env.php
```
###(4)單元測試全部通過了!
為了方便大家查看,已省略了部分的調試內容,但保留了測試過程中全部執行的SQL語句,如下:
```javascript
$ phpunit ./Api_Act_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.
[1 - 0.06911s]DELETE FROM phalapi_team WHERE (team_name = 'test team name');<br>
[2 - 0.06487s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[3 - 0.06553s]SELECT COUNT(id) FROM phalapi_team WHERE (team_name = 'test team name');<br>
[4 - 0.0653s]INSERT INTO phalapi_team (team_name) VALUES ('test team name');<br>
[5 - 0.0699s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[6 - 0.06555s]SELECT COUNT(id) FROM phalapi_team WHERE (team_name = 'test team name');<br>
[9 - 0.06778s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[10 - 0.06603s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[11 - 0.06825s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;<br>
[12 - 0.07374s]UPDATE phalapi_vote SET team_id = 3, vote_num = 22 WHERE (team_id = 3);<br>
[13 - 0.07012s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[14 - 0.06682s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[15 - 0.07433s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;<br>
[16 - 0.07283s]UPDATE phalapi_vote SET team_id = 3, vote_num = 23 WHERE (team_id = 3);<br>
[17 - 0.07307s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[18 - 0.07501s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[19 - 0.07135s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;<br>
[20 - 0.07653s]UPDATE phalapi_vote SET team_id = 3, vote_num = 24 WHERE (team_id = 3);<br>
[21 - 0.07215s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[22 - 0.06722s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);<br>
[23 - 0.06506s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');<br>
[24 - 0.06732s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 404);<br>
Time: 1.97 seconds, Memory: 7.00Mb
OK (4 tests, 42 assertions)
```
###(5)運行效果 - Part 1
在我們通過第三方登錄后,我們就可以這樣進行接口操作了。
首先,讓我們添加兩個參賽團隊:
```javascript
//奔跑吧兄弟(藍隊)
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E8%93%9D%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
"ret": 200,
"data": {
"code": 0,
"team_id": "5"
},
"msg": ""
}
//奔跑吧兄弟(紅隊)
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
"ret": 200,
"data": {
"code": 0,
"team_id": "6"
},
"msg": ""
}
```
然后,讓我們進行瘋狂地投票:
```javascript
//第一次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":1},"msg":""}
//第二次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":2},"msg":""}
//第三次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":3},"msg":""}
//第四次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回 - 注意此返回!
{"ret":200,"data":{"code":2,"vote_num":0},"msg":""}
```
最后,看一下排行榜:
```javascript
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.ShowList&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
"ret": 200,
"data": {
"code": 0,
"teams": [
{
"id": 5,
"team_name": "奔跑吧兄弟(藍隊)",
"vote_num": 3
},
{
"id": 6,
"team_name": "奔跑吧兄弟(紅隊)",
"vote_num": 0
}
]
},
"msg": ""
}
```
至此,我們已經可以把接口交付給客戶端同學使用啦!
當然,我們還需要稍微整理輸出WIKI文檔~~~
部分接口返回的結果可能與你實際看到的不一樣,因為數據會在變化而且有單元測試的測試數據。
###(6)運行效果 - Part 2
接口服務,不僅僅需要提供正常的業務功能,還需要考慮到各種客戶端使用的情況,包括非法的請求,或者不合的調用,比如防刷票。
這一部分,主要展示接口在各種異常情況下的響應能力。
####簽名失敗
```javascript
http://api.vote.phalapi.com/vote/?sign=XXX&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
"ret": 400,
"data": [
],
"msg": "非法請求:wrong sign"
}
```
####無登錄態
```javascript
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=XXX&user_id=1
//返回
{
"ret": 401,
"data": [
],
"msg": "非法請求:user need to login again"
}
```
####重復參賽
```javascript
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.JoinIn&team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
"ret": 200,
"data": {
"code": 1,
"team_id": 0
},
"msg": ""
}
```
####當天投票次數已達最大
```javascript
//第四次投票后
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=5&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回 - 注意此返回!
{
"ret": 200,
"data": {
"code": 2,
"vote_num": 0
},
"msg": ""
}
```
####投票的團隊不存在
```javascript
http://api.vote.phalapi.com/vote/?sign=phalapi&service=Act.Vote&team_id=404&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&user_id=1
//返回
{
"ret": 200,
"data": {
"code": 1,
"vote_num": 0
},
"msg": ""
}
```
在進行上面的非法請求后,我們可以同時關注后臺的日志:
```javascript
$ tailf ./Runtime/log/201505/20150517.log
2015-05-17 01:35:32|DEBUG|user not login|{"userId":null,"token":null}
2015-05-17 01:36:01|DEBUG|user can not vote today|{"userId":1,"teamId":5}
2015-05-17 01:37:52|DEBUG|user can not vote today|{"userId":1,"teamId":5}
2015-05-17 01:45:25|DEBUG|user need to login again|{"expiresTime":0,"userId":"1","token":"XXX"}
2015-05-17 01:48:13|DEBUG|user can not vote today|{"userId":1,"teamId":5}
```
###(7)需要的數據庫表
以下為關鍵的表,其他表,可以通過腳本自動生成,然后導入。
```javascript
-- ----------------------------
-- Table structure for `phalapi_team`
-- ----------------------------
DROP TABLE IF EXISTS `phalapi_team`;
CREATE TABLE `phalapi_team` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`team_name` varchar(100) DEFAULT '' COMMENT '隊名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of phalapi_team
-- ----------------------------
INSERT INTO `phalapi_team` VALUES ('3', 'egg team');
INSERT INTO `phalapi_team` VALUES ('5', '奔跑吧兄弟(藍隊)');
INSERT INTO `phalapi_team` VALUES ('6', '奔跑吧兄弟(紅隊)');
INSERT INTO `phalapi_team` VALUES ('17', 'test team name');
INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc877070133c65161d00799e00544', 'weixinName', '******', '4CHqOhe1Jxi3X9HmRfPOXygDnU267eCA', '1431790647', 'phpunit.png');
INSERT INTO `phalapi_user_login_weixin` VALUES ('1', 'wx_122348561111', 'ASDF', '130000000', '1');
INSERT INTO `phalapi_user_session_1` VALUES ('1', '1', '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731', '', '1', '1431790647', '1934382647', null);
```
## 4.3.4 腳本的使用
###(1)創建項目腳本 - phalapi_buildapp
此腳本可用于快速創建項目,其使用如下:
```javascript
$ ./PhalApi/phalapi-buildapp
Usage: ./PhalApi/phalapi-buildapp <app>
```
此腳本會根據Demo示例,快速生成一個新的項目。
###(2)自動生成單元測試骨架腳本 - phalapi_buildtest
這是一個用來自動生成單元測試骨架的腳本,其使用如下:
```javascript
$ phalapi-buildtest
Usage:
php /usr/bin/phalapi-buildtest <file_path> <class_name> [bootstrap] [author = dogstar]
Demo:
php ./build_phpunit_test_tpl.php ./Demo.php Demo > Demo_Test.php
```
然后,我們就可以使用輔助的PhalApiTestRunner::go()進行單元測試:
```javascript
public function testShowList()
{
//Step 1. 構建請求URL
$url = 'service=Act.ShowList&sign=phalapi&user_id=1&token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731';
//Step 2. 執行請求
$rs = PhalApiTestRunner::go($url);
//var_dump($rs);
//Step 3. 驗證
$this->assertNotEmpty($rs);
$this->assertArrayHasKey('code', $rs);
$this->assertArrayHasKey('teams', $rs);
$this->assertEquals(0, $rs['code']);
$this->assertNotEmpty($rs['teams']);
foreach ($rs['teams'] as $team) {
$this->assertArrayHasKey('id', $team);
$this->assertArrayHasKey('team_name', $team);
$this->assertArrayHasKey('vote_num', $team);
$this->assertGreaterThanOrEqual(0, $team['vote_num']);
}
}
```
###(3)自動生成分表建表SQL語句 - phalapi_buildsqls
此腳本用于根據dbs.php配置和./Data/*.sql文件生成完整的建表語句,其使用如下:
```javascript
$ ./PhalApi/phalapi-buildsqls
Usage: ./PhalApi/phalapi-buildsqls <dbs.php> <table> [engine=InnoDB]
```
假設我們需要對vote進行分表,拆分成3個表,可以這樣配置:
```javascript
//$ vim ./Config/dbs.php
'vote' => array(
'prefix' => 'phalapi_',
'key' => 'id',
'map' => array(
array('start' => 0, 'end' => 2, 'db' => 'db_vote'),
),
),
```
然后準備基本的建表語句:
```javascript
//$vim ./Data/vote.sql
`team_id` bigint(20) DEFAULT '0',
`vote_num` int(11) DEFAULT '0',
```
最后,執行生成腳本:
```javascript
$ phalapi-buildsqls ./Config/dbs.php vote
/**
* DB: phalapi_vote
*/
CREATE TABLE `phalapi_vote_0` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`team_id` bigint(20) DEFAULT '0',
`vote_num` int(11) DEFAULT '0',
`ext_data` text COMMENT 'json data here',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `phalapi_vote_1` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`team_id` bigint(20) DEFAULT '0',
`vote_num` int(11) DEFAULT '0',
`ext_data` text COMMENT 'json data here',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `phalapi_vote_2` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`team_id` bigint(20) DEFAULT '0',
`vote_num` int(11) DEFAULT '0',
`ext_data` text COMMENT 'json data here',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
## 4.3.5 在線接口參數查看
可以通過以下鏈接,在線實時查看接口參數:
```javascript
//參賽接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.JoinIn
//投票接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.Vote
//排行榜接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.ShowList
```
其中,投票接口的參數如下:

- 歡迎使用PhalApi!
- 接口,從簡單開始!
- [1.1]-下載與安裝
- [1.2]-創建一個自己的項目
- [1.3]-在線體驗
- [1.4]-文檔、幫助和官網
- [1.10]-對PhalApi框架的抉擇
- [1.11]-快速入門(backup)
- [1.12]-參數規則:接口參數規則配置
- [1.13]-統一的接口請求方式:_sevice=XXX.XXX
- [1.14]-統一的返回格式和結構:ret-data-msg
- [1.15]-數據庫操作:基于NotORM的使用及優化
- [1.16]-配置讀取:內外網環境配置的完美切換
- [1.17]-日記紀錄:簡化版的日記接口
- [1.18]-快速函數:人性化的關懷
- [1.19]-DI服務速查:各資源服務一覽表
- [1.20]-DB操作:數據庫基本操作速查
- [1.21]-類的自動加載:遵循PEAR包的命名規范
- [1.22]-簽名驗證:自定義簽名規則
- [1.23]-請求和響應:GET和POST兩者皆可得及超越JSON格式返回
- [1.24]-緩存策略:更靈活地可配置化的多級緩存
- [1.25]-國際化翻譯:為走向國際化提前做好翻譯準備
- [1.26]-數據安全:數據對稱加密方案
- [1.27]-精益開發:更富表現力的Model層和重量級數據獲取的應對方案
- [1.28]-COOKIE:對COOKIE原生態的支持及記憶加密升級版
- [1.29]-開放與封閉:多入口和統一初始化
- [1.30]-保持的力量:接口開發最佳實踐
- [1.31]-新型計劃任務:以接口形式實現的計劃任務
- [2.11]-核心思想:DI依賴注入-讓資源更可控
- [2.12]-海量數據:可配置的分庫分表
- [2.13]-接口調試:在線SQL語句查看與性能優化
- [2.14]-測試驅動開發:意圖導向編程下的接口開發
- [2.15]-演進:新型計劃任務續篇
- [2.16]-領域驅動設計:應對復雜領域業務的Domain層
- [2.17]-微服務:Api接口服務層
- [2.18]-定制化:資源服務的再實現
- [2.19]-擴展庫:可重用的擴展類庫
- [2.20]-約定編程:架構明顯的編程風格
- [2.21]-服務器統一部署方案簡明版:CentOs---Nginx---php-fpm---MySql-[--Memcached]
- [2.22]-更多工具:精益項目和團隊建設
- [3.1]-擴展類庫:微信開發
- [3.2]-擴展類庫:代理模式下phprpc協議的輕松支持
- [3.3]-擴展類庫:基于PHPMailer的郵件發送
- [3.4]-擴展類庫:優酷開放平臺接口調用
- [3.5]-擴展類庫:七牛云存儲接口調用
- [3.6]-擴展類庫:新型計劃任務
- [3.8]-擴展類庫:用戶、會話和第三方登錄集成
- [3.9]-擴展類庫:swoole支持下的長鏈接和異步任務實現
- [3.11]-擴展類庫:基于FastRoute的快速路由
- [4.2]-開發實戰2:模擬優酷開放平臺接口項目開發
- [4.3]-開發實戰3:一個簡單的小型項目開發(奔跑吧兄弟投票活動)
- [5.1]-架構與思想:PhalApi核心設計和思想解讀
- [5.2]-雜談:扯一些PhalApi的前世和今生
- [5.3]-框架總結:術語表和PHP開發建議
- [5.4]-許可
- [5.5]-聯系和加入我們
- [5.6]-更新日記
- [5.8]-致框架貢獻者:加入PhalApi開源指南
- [6.1]-基于接口查詢語言的SDK包
- [6.2]-SDK包(JAVA版)
- [6.3]-SDK包(PHP版)
- [6.4]-SDK包(Objective-C版)
- [6.5]-SDK包(javascript版)
- [6.6]-SDK包(Ruby版)
- [8.1]-PhalApi視頻教程
- 附錄1:接口文檔參考模板