截止撰稿之時,ThinkPHP6.0已經進入RC3階段。按ThinkPHP做者流年的計劃,ThinkPHP6.0預計將會在今年秋季擇機發布正式版,RC3將多是正式版以前的最後一個或者倒數第二個RC版本,這也就意味着,ThinkPHP6.0已經日趨完善穩定,是一個值得嘗試的候選版本了。php
故此,隨之有着重大變化的think-queue擴展也已經升級發佈了接近正式版的3.0.2。那麼已經用think-queue上了生產環境的小夥伴們確定很想知道,think-queuehtml
3.0是否已經能夠嚐鮮了?本期小編將帶你們用think-queue 3.0.2的定時隊列來打造一個定時扣費系統來告訴你答案。linux
PS:本人文中若有錯誤或者不失之處,還請海涵,歡迎各位大牛隨時批評指導。laravel
使用think-queue隊列,必須具有如下條件:git
1:一個基於liunx系統的server,windows亦可但不推薦;github
2:redis服務端。建議5.0版本,web
參考文章:https://www.jianshu.com/p/fe6...;redis
新手推薦使用寶塔面板,https://www.bt.cn/download/li... ,以省去精力和減小配置編譯形成的服務端各類常見問題。thinkphp
3:composer包管理器數據庫
可參考:https://www.phpcomposer.com;
推薦鏡像源:https://mirrors.aliyun.com/co...,在此感謝阿里雲的貢獻。
4:一個redis客戶端,Windows開發者推薦在如下項目裏選擇使用
https://github.com/uglide/Red...;
https://github.com/qishibo/An...;
https://github.com/cinience/R...;
以及windows自帶的Windows PowerShell。
對於think-queue不是很熟悉和了解的,請務必先閱讀下面這邊教程後再回來繼續閱讀:
https://github.com/coolseven/...
1:首選建立thinkphp6.0新的項目,參考https://www.kancloud.cn/manua...
composer create-project topthink/think=6.0.x-dev tp
2:使用think-queue 3.0.2
https://packagist.org/package...
composer require topthink/think-queue
也能夠項目根目錄下composer.json文件添加配置項
"require": { "php": ">=7.1.0", "topthink/framework": "6.0.*-dev", "topthink/think-view": "^1.0", "symfony/var-dumper":"^4.2", "topthink/think-queue": "^3.0" },
3:檢查是否安裝成功
在項目根目錄下運行
php think
看到
就表示think-queue已經安裝成功。
接下來就要進行下一步:建立項目的數據庫,結構我已經準備好了
用戶會員表
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用戶名', `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '暱稱', `realname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '姓名', `password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密碼', `create_time` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '註冊時間', `update_time` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '更新時間', `login_time` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '登錄時間', `login_count` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '登錄次數', `login_ip` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '登陸ip', `vip` tinyint(2) UNSIGNED NULL DEFAULT 0 COMMENT 'vip等級', `vip_join` int(10) UNSIGNED NULL DEFAULT 0 COMMENT 'vip加入時間', `vip_time` int(10) UNSIGNED NULL DEFAULT 0 COMMENT 'vip過時時間', `ip` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '註冊ip', `status` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '狀態1:正常2:禁用3:臨時', `lock_uid` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '封禁人', `lock_time` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '封禁時間', `lock_tips` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '封禁緣由', `back_time` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '解封時間', `group` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '身份1:普通2:管理員3:代理4:合做方5:渠道商', `group_time` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '身份過時', `safe_level` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '安全等級', `safe_code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密保安全碼', `safe_token` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '密保令牌1:未2:是', `safe_device` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '密保設備1:未2:是', `safe_phone` bigint(11) UNSIGNED NULL DEFAULT 0 COMMENT '密保手機', `safe_email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密保郵箱', `verify_code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '找回校驗碼', `verify_lock` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '找回鎖定', `verify_time` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '校驗碼過時', `money` decimal(10, 3) UNSIGNED NOT NULL DEFAULT 0.000 COMMENT '餘額', `give` decimal(10, 3) UNSIGNED NOT NULL DEFAULT 0.000 COMMENT '增送', `brokerage` decimal(10, 3) UNSIGNED NOT NULL DEFAULT 0.000 COMMENT '佣金', `server` int(10) UNSIGNED NOT NULL DEFAULT 10 COMMENT '服務器', `gold` tinyint(8) UNSIGNED NOT NULL DEFAULT 0 COMMENT '金幣', `credits` tinyint(8) UNSIGNED NOT NULL DEFAULT 0 COMMENT '積分', `union_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '微信', `unionid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'QQ', `inviters` tinyint(5) UNSIGNED NOT NULL DEFAULT 0 COMMENT '邀請次數', `inviter_id` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '邀請人', PRIMARY KEY (`id`) USING BTREE, INDEX `id`(`id`, `username`, `realname`, `vip`, `group`, `money`, `create_time`) USING BTREE COMMENT '聯合索引' ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用戶表' ROW_FORMAT = Dynamic;
日誌表
DROP TABLE IF EXISTS `logs`; CREATE TABLE `logs` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `uid` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用戶id', `subid` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '主機id', `op_id` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '操做人id', `type` tinyint(2) UNSIGNED NOT NULL DEFAULT 1 COMMENT '類型1:會員2:管理3:系統5:財務', `time` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '時間', `ip` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '登陸ip', `content` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '日誌內容', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_uid_type_time`(`uid`, `type`, `time`, `op_id`, `subid`) USING BTREE COMMENT '聯合索引' ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '日誌表' ROW_FORMAT = Dynamic;
用戶主機表
DROP TABLE IF EXISTS `server`; CREATE TABLE `server` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `uid` int(10) UNSIGNED NOT NULL COMMENT '所屬用戶', `status` int(2) UNSIGNED NOT NULL DEFAULT 2 COMMENT '狀態1:已中止2:運行中3:已過時4:需續費5:已刪除6:異常', `time` int(10) UNSIGNED NOT NULL COMMENT '建立時間', `op` int(4) UNSIGNED NULL DEFAULT 0 COMMENT '操做人', `op_time` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '操做時間', `subid` bigint(11) UNSIGNED NULL DEFAULT 0 COMMENT '實例id', `ip_address` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'IPv4', `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT 'root密碼', `snapshotid` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '快照id', `port` int(10) UNSIGNED NULL DEFAULT 22 COMMENT '端口', `ips` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '高防IP', `enable_ipv6` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'IPv6', `ipv6` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'IPv6地址', `dcid` int(5) UNSIGNED NULL DEFAULT 0 COMMENT '位置', `osid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '操做系統', `arch` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '系統類型', `vpsplanid` double(32, 0) UNSIGNED NULL DEFAULT 0 COMMENT '配置規格', `hostname` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '自定義名稱', `ddos` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'DDOS', `appid` int(6) UNSIGNED NULL DEFAULT 0 COMMENT '預裝應用', `destroy` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '刪除時間', `month` int(3) UNSIGNED NULL DEFAULT 0 COMMENT '購買時長', `deduction` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '扣費次數', `money` decimal(10, 3) UNSIGNED NULL DEFAULT 0.000 COMMENT '費用', `deduction_time` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '扣費時間', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '主機表' ROW_FORMAT = Compact;
流水錶
DROP TABLE IF EXISTS `account`; CREATE TABLE `account` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `uid` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用戶id', `type` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '類型1:收入2:支出', `class` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '種類1:餘額', `money` decimal(10, 3) UNSIGNED NULL DEFAULT 0.000 COMMENT '金額', `way` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '來源1:系統2:充值3:提現4:主機', `style` tinyint(2) UNSIGNED NOT NULL DEFAULT 1 COMMENT '分類1:系統2:支付寶3:微信4:卡密', `timestamp` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '時間鎖', `subid` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '實例id', `rechargeid` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '充值id', `trade` char(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '流水號', `card_id` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '卡密id', `op` tinyint(2) UNSIGNED NOT NULL DEFAULT 1 COMMENT '操做人', `content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '內容', `time` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '時間', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `idx_timestamp`(`timestamp`) USING BTREE COMMENT '時間鎖', INDEX `idx_uid_type_class_way_style_subid_rechargeid`(`uid`, `type`, `class`, `way`, `style`, `subid`, `rechargeid`, `trade`, `money`) USING BTREE COMMENT '聯合索引' ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '流水錶' ROW_FORMAT = Dynamic;
系統會員註冊登陸代碼部分略過,請本身補充,也能夠在文章末尾下載本教程對應實例代碼。
本教程think-queue隊列使用的是redis驅動方式,請務必先配置好redis,配置文件在config文件夾下面的queue.php,別弄錯了
return [ 'default' => 'redis', 'connections' => [ 'sync' => [ 'driver' => 'sync', ], 'database' => [ 'driver' => 'database', 'queue' => 'default', 'table' => 'jobs', ], 'redis' => [ 'driver' => 'redis', 'queue' => 'default', 'host' => '127.0.0.1', 'port' => 6379, 'password' => 'Xun166123', 'select' => 1, 'timeout' => 0, 'persistent' => false, ], ], 'failed' => [ 'type' => 'none', 'table' => 'failed_jobs', ], ];
接下來添加隊列
/* * 隊列任務 * @author zakeear <zakeear@86dede.com> * @version v0.1.5 * @time 2019-06-10 */ namespace app\queue\controller; use think\Exception; use think\facade\Db; use think\facade\Queue; class Host{ /** * 添加隊列 * @access public * @param int $subid 主機id * @param string $type 任務名 * @param int $times 延時秒數 * @throws \think\Exception */ public function addTask(int $subid=0,string $type='server',int $times=0){ $server=Db::name('server')->where(['subid'=>$subid])->find(); if(!$server){ exit; } switch($type){ case 'server': $jobHandlerClassName='app\queue\job\Money@fire'; $jobDataArr=['submit'=>time(),'doit'=>time()+$times,'subid'=>$server['subid'],'hostname'=>$server['hostname']]; $jobQueueName="Money"; break; case 'destroy': $jobHandlerClassName='app\queue\job\Destroy@fire'; $jobDataArr=['submit'=>time(),'doit'=>time()+$times,'subid'=>$server['subid'],'hostname'=>$server['hostname']]; $jobQueueName="Destroy"; break; default: break; } if($times==0){ $isPushed=Queue::push($jobHandlerClassName,$jobDataArr,$jobQueueName); }else{ $isPushed=Queue::later($times,$jobHandlerClassName,$jobDataArr,$jobQueueName); } } }
而後是消費者類
/* * 主機扣費類 * @author zakeear <zakeear@86dede.com> * @version v0.2.0 * @time 2019-06-13 */ namespace app\queue\job; use think\queue\Job; use think\facade\Db; class Money{ public function fire(Job $job,$data){ //job $isJobDone=$this->doJob($job,$data); $attempts=$job->attempts()+1; if($isJobDone){ print('<info>['.date('Y-m-d H:i:s',time())."] 主機".$data['hostname']."扣費任務完成,任務銷燬</info>\n"); $job->delete(); }else{ $release=strtotime(date('Y-m-d H:',time()).'00')+3599+date('i',$data['submit'])*60+date('s',$data['submit'])-time(); print('<info>['.date('Y-m-d H:i:s',time())."] ".$release."秒後執行主機".$data['hostname']."第".$attempts."次扣費任務</info>\n"); $job->release($release); } } private function doJob($job,$data){ //job $attempts=$job->attempts(); print('<info>['.date('Y-m-d H:i:s',time())."] 主機".$data['hostname']."第".$attempts."次扣費</info>\n"); //主機 $server=Db::name('server')->field('id,uid,subid,month,money,hostname,deduction')->where(['subid'=>$data['subid'],'status'=>2])->find(); if(!$server){ print('<info>['.date('Y-m-d H:i:s')."] 主機".$data['hostname']."已經不存在或者被刪除!"."\n"); return true; } //日誌 $logs=new \app\common\logic\Logs(); //配置 $this->config = Db::name('config')->field('rate,month,is_buy,vultr_api,vultr_keys,web_name,web_icon,time')->where(['status'=>1])->order('time','desc')->find(); if($server['deduction']==$this->config['month']){ //扣費 $logs->database($server['uid'],5,'','主機【'.$server['hostname'].'】達到月付限額,本月再也不扣費',1,$server['subid']); //計數 Db::name('server')->where(['subid'=>$data['subid']])->update(['deduction'=>0,'deduction_time'=>0]); //job $attempts=$attempts-1; print('<info>['.date('Y-m-d H:i:s')."] 主機".$data['hostname']."已經達到月付上限".$attempts."次\n"); //隊列 $job=new \app\queue\controller\Host(); //刪除 $job->addTask($data['subid'],'destroy',0); //建立 $release=\app\Timer::nextMonth()[0]+date('i',$data['submit'])*60+date('s',$data['submit'])-time();//下月從新計費 $job->addTask($data['subid'],'server',$release); //返回 return true; } //用戶 $user=Db::name('user')->field('id,money')->where(['id'=>$server['uid']])->find(); if($user['money']<$server['money']){ //日誌 $logs->database($server['uid'],5,'','主機【'.$server['hostname'].'】不足於支付:【'.$server['money'].'】',1,$server['subid']); //刪除 Db::name('server')->where(['subid'=>$data['subid']])->update(['status'=>5,'destroy'=>time()]); //日誌 $logs->database($server['uid'],5,'','主機【'.$server['hostname'].'】刪除',1,$server['subid']); //隊列 $job=new \app\queue\controller\Host(); //刪除 $job->addTask($data['subid'],'destroy',0); //job print('<info>['.date('Y-m-d H:i:s')."] 用戶餘額不足於支付".$user['money']."元\n"); //返回 return true; } //費用 $money=new \app\common\logic\Money(); //扣費 $money->hostDec($server['uid'],1,$server['money'],1,$server['subid'],'主機【'.$server['hostname'].'】支付費用'); //日誌 $logs->database($server['uid'],5,'','主機【'.$server['hostname'].'】支付費用:【'.$server['money'].'】',1,$server['subid']); //計數 Db::name('server')->where(['subid'=>$data['subid']])->inc('deduction',1)->update(['deduction_time'=>time()]); } }
建立好後,文件目錄對應結構以下:
參考此文章
https://segmentfault.com/a/11...
或者使用寶塔的計劃任務
添加如下腳本
#檢查php Money 隊列腳本是否啓動 php_count=`ps -ef | grep Money | grep -v "grep" | wc -l` if [ $php_count == 0 ];then echo '----php Money queue start' `sudo -H -u www bash -c 'nohup php /www/wwwroot/www.demo.com/think queue:listen --queue Money > /www/wwwroot/www.demo.com/logs/Money.txt 2>&1 &'` else echo '----php Money queue ok' fi #檢查php DestroyQueue 隊列腳本是否啓動 php_count=`ps -ef | grep Destroy | grep -v "grep" | wc -l` if [ $php_count == 0 ];then echo '----php Destroy queue start' `sudo -H -u www bash -c 'nohup php /www/wwwroot/www.demo.com/think queue:listen --queue Destroy > /www/wwwroot/www.demo.com/logs/Destroy.txt 2>&1 &'` else echo '----php Destroy queue ok' fi
如圖:
think-queue 目前爲止還未實現subscribe功能,這裏利用了think-queue的延時隊列來實現定時任務,當消費者類裏的任務完成之後,不return true,使用延時拋回給隊列就好,該隊列會一直存在不會被刪除,也就變相的實現定時任務了。
不過到這裏會有一個疑問,如何準確告訴隊列須要延時多久?
代碼以下:
$release=strtotime(date('Y-m-d H:',time()).'00')+3599+date('i',$data['submit'])*60+date('s',$data['submit'])-time(); print('<info>['.date('Y-m-d H:i:s',time())."] ".$release."秒後執行主機".$data['hostname']."第".$attempts."次扣費任務</info>\n"); $job->release($release);
這裏就很關鍵了,須要你本身計算出來,假如一分鐘一次,按常理$job->release(60)就搞定了,可是忘記了消費者類運行自己是須要時間的,起碼幾百毫秒是要的。遇到任務多的時候,可能1-2秒才能完成,加入第一次任務在15:00:01開始消費任務,消費完成又花了1秒,消費完成後你再$job->release(60)那麼下次執行消費隊列就是15:01:02,那麼第三次執行就是15:01:03,依次類推,60次任務後,中間出現了有長達一分鐘沒有扣費的狀況,這對須要定時扣費的項目來講就是bug或者災難。這裏咱們經過動態計算來決定延時多少秒後,就解決了這個問題,常常生產環境長達2個月的觀察,偏差是先後2秒。這裏留個引子,若是控制到先後不超過1秒呢?假如這時候任務隊列太多,堆積了成千上百條隊列再排隊了之後,又該如何處理?下一期咱們可能會爲大家講解如何使用think-queue實現任務調度來打造一個支持高併發的訂單下單系統。
文章裏缺失的代碼部分,請前往https://github.com/zakeear/man查找或者自行結合業務進行修改。
在https://github.com/coolseven/...,thinkphp的隊列核心是本身編寫的,laravel的隊列核心依賴於symfony/process這個composer包。翻閱think-queue 3.0的源碼後發現
正好契合了thinkphp6.0的理念,全面擁抱composer!由此也帶來了think-queue 3.0和think-queue 2.0最大的一個區別,think-queue 3.0須要註冊服務,think-queue 2.0不須要,這點差別會引起一個小編在使用過程當中遇到的極端問題,windows下安裝好的think-queue 3.0 到了liunx上,php think queue無效。若是有同類問題,解決把辦法是在liunx上安裝thinkphp6.0和think-queue 3.0後下載到windows下使用便可,這點習慣在windows下開發的小夥伴要注意。
另一點,使用過think-queue 2.0的同窗會發現,think-queue 3.0要想正常使用必須依賴於php的兩個內置函數,而這2個內置函數太對於敏感,運維通常會禁用,小編推薦的寶塔也會默認禁用掉它們,think-queue 3.0更換到symfony/process這個包之後,就再也不依賴這2個內置函數,從這點來講因此3.0要比2.0更加安全。
https://www.kancloud.cn/think... ThinkPHP開發者週刊是Thinkphp生態的重要一環,流年已經獨自一人更新維護了1年多,爲廣大phper提供了一個學習和認識php圈子裏優秀的項目、書籍、開發者的渠道,目前該週刊已經轉由志願者維護,不遠的未來,將會交由社區維護。目前有好的文章、項目、書籍和案例,歡迎你們投稿。投稿地址:QQ羣:780179357
本教程實有倉促,文中若有遺漏和錯誤歡迎指正。本教程完整實例源碼已經託管到giehub:https://github.com/zakeear/man