# 登錄
先看效果:

后臺的權限比較大,什么資源都能管。
有的時候,為了安全考慮 很多產品把后臺設計的很復雜。比方說,密碼復雜度、驗證碼、錯誤幾次輸入密碼后,鎖死賬號、子管理員賬號不同權限等。
為了簡單的實現安全,我后臺干脆不用表存管理員賬號,配置中寫死。這樣就不存在說注入的問題。也不會實現多管理員登錄。然后密碼弄稍微復雜的自己能記憶的密碼。 賬號:freelog 密碼:jay2015 寫在admin模塊配置注釋里防止忘了。
由于登錄頁沒有導航這樣公共的模塊,所以所有模板代碼直接寫在一個html里。
我們看下代碼:
~~~
<!DOCTYPE HTML>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js">
<!--<![endif]-->
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{:C('WEB_SITE_TITLE')}</title>
<script type="text/javascript" src="__BOWER__/jquery/dist/jquery.js"></script>
<!--[if lt IE 9]>
<script src="__STATIC__/html5shiv.js"></script>
<script src="__STATIC__/respond.js"></script>
<![endif]-->
<script type="text/javascript" src="__BOWER__/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript">
var url = '';
</script>
<script type="text/javascript" src="__JS__/admin.js"></script>
<link rel="stylesheet" type="text/css" href="__BOWER__/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="__CSS__/admin_custom.css">
</head>
<body>
<!--[if lt IE 8]>
<div class="browsehappy">當前網頁 <strong>不支持</strong> 你正在使用的瀏覽器. 為了正常的訪問, 請 <a href="http://browsehappy.com/">升級你的瀏覽器</a>.</div>
<![endif]-->
<div class="container-fluid">
<div class="row-fluid">
<div class="col-md-5"></div>
<div class="col-md-2">
<form class="login-form" method="post" action="{:U('System/check')}">
<fieldset>
<h3 class="welcome"><i class="login-logo"></i>freelog</h3>
<div class="form-group">
<div class="controls">
<input type="text" name="loginName" placeholder="用戶名" class="form-control" required title="請填寫用戶名">
</div>
</div>
<div class="form-group">
<div class="controls">
<input type="password" name="loginPwd" placeholder="密碼" class="form-control" required title="請填寫密碼">
</div>
</div>
<div class="form-group">
<div class="controls">
<button class="btn-block primary ajax-post" type="submit">登錄</button>
<a class="btn-link" href="/">返回首頁</a>
</div>
</div>
</fieldset>
</form>
</div>
<div class="col-md-5"></div>
</div>
</div>
</body>
</html>
~~~
~~~
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
~~~
這樣的寫法叫瀏覽器條件注釋,專門針對ie低版本的寫法。
~~~
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
~~~
是為了保證兼容性,針對ie8的寫法,意思是裝了chrome的系統,ie打開時用chrome渲染,沒裝繼續用ie的edge引擎渲染。
~~~
<script type="text/javascript" src="__BOWER__/jquery/dist/jquery.js"></script>
<!--[if lt IE 9]>
<script src="__STATIC__/html5shiv.js"></script>
<script src="__STATIC__/respond.js"></script>
<![endif]-->
<script type="text/javascript" src="__BOWER__/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript">
var url = '';
</script>
<script type="text/javascript" src="__JS__/admin.js"></script>
<link rel="stylesheet" type="text/css" href="__BOWER__/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="__CSS__/admin_custom.css">
~~~
引入一些常規的jquery、bootstrap組件和自己后臺用的admin.js。
~~~
<!--[if lt IE 8]>
<div class="browsehappy">當前網頁 <strong>不支持</strong> 你正在使用的瀏覽器. 為了正常的訪問, 請 <a href="http://browsehappy.com/">升級你的瀏覽器</a>.</div>
<![endif]-->
~~~
做了一個ie8向下的提示更換瀏覽器的處理。讓ie6、7去見鬼吧。
~~~
<div class="container-fluid">
<div class="row-fluid">
<div class="col-md-5"></div>
<div class="col-md-2">
<form class="login-form" method="post" action="{:U('System/check')}">
<fieldset>
<h3 class="welcome"><i class="login-logo"></i>freelog</h3>
<div class="form-group">
<div class="controls">
<input type="text" name="loginName" placeholder="用戶名" class="form-control" required title="請填寫用戶名">
</div>
</div>
<div class="form-group">
<div class="controls">
<input type="password" name="loginPwd" placeholder="密碼" class="form-control" required title="請填寫密碼">
</div>
</div>
<div class="form-group">
<div class="controls">
<button class="btn-block primary ajax-post" type="submit">登錄</button>
<a class="btn-link" href="/">返回首頁</a>
</div>
</div>
</fieldset>
</form>
</div>
<div class="col-md-5"></div>
</div>
</div>
~~~
這里,用了bootstrap的網格流動布局。為了讓登錄框居中,
我按照bootstrap默認表單寬度,大概算出它占2列,然后左右留 各5個空白的列。
當屏幕小于5列寬時就會 表單撐開,全寬,達到響應式。

更小寬度也是如此。

由于只是后臺,沒分太細的寬度,如果要匹配不同寬度,需要div上加 col-sm col-sm 之類的類去控制。后臺登錄頁面沒必要做太細。
后臺登錄邏輯和前臺差不多,只不過不用查庫了,直接配置里的鍵去比較表單項。
~~~
/* 登錄驗證 */
public function check() {
//接收數據
$loginName = trim(I('post.loginName'));
$loginPwd = trim(I('post.loginPwd'));
if (C('ADMIN.LOGIN_NAME') == $loginName) {
if (md5($loginPwd) == C('ADMIN.PWD')) {
$user = array(
'admin_id' => 1,
'admin_name' => $loginName,
'login_time' => NOW_TIME, //上次登錄時間
);
//設置登錄SESSION
session(C('USER_AUTH_KEY'), $user);
session(C('USER_AUTH_SIGN_KEY'), user_auth_sign($user));
$this->success('登錄成功', U('System/index'));
} else {
$this->error('密碼錯誤');
}
} else {
$this->error('該管理員不存在');
}
}
~~~
里面的簽名方法復用了OneThink的。
然后注意的一點是 NOW_TIME 常量。每次獲取當前時間戳用time() 多次用就浪費效率了。
所以TP定義了一個常量供框架使用。
`System/check` 比`User/login` 難讓黑客們猜到。
# 導航
登錄過后,就可以看見除了登錄頁面外,每個頁面都有的導航

我是在bootstrap3 默認的帶下拉菜單的導航基礎上加上反轉背景色、加上子菜單箭頭和高亮當前導航實現的效果。
~~~
<div class="navbar navbar-inverse">
<div class="navbar-inner">
<div class="container-fluid">
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="{:U('System/index')}">首頁 <span class="sr-only">(current)</span></a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">資源 <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="{:U('Resource/tags')}">標簽</a></li>
<li><a href="{:U('Resource/pics')}">圖片庫</a></li>
<li><a href="{:U('Resource/files')}">文件庫</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">文章 <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="{:U('Post/text')}">文章</a></li>
<li><a href="{:U('Post/picture')}">圖片</a></li>
<li><a href="{:U('Post/music')}">音樂</a></li>
<li><a href="{:U('Post/video')}">視頻</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">微信 <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="{:U('Weixin/index')}">服務器</a></li>
<li><a href="{:U('Weixin/menu')}">菜單</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">配置 <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="{:U('Config/group?id=1')}">網站設置</a></li>
<li><a href="{:U('Config/index')}">配置列表</a></li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="divider-vertical"></li>
<li><a href="javascript:;">{:get_admin_name()}</a></li>
<li class="divider-vertical"></li>
<li><a href="{:U('System/cleancache')}" class="ajax-get">清除緩存</a></li>
<li class="divider-vertical"></li>
<li><a href="{:U('System/logout')}">登出</a></li>
<li class="divider-vertical"></li>
<li><a href="/">網站</a></li>
</ul>
</div>
<!-- /.nav-collapse -->
</div>
</div>
<!-- /navbar-inner -->
</div>
~~~

高亮代碼實現參考了OneThink的后臺高亮。
先js獲取當前url:
~~~
var url = window.location.pathname + window.location.search;
url = url.replace(/(\/(p)\/\d+)|(&p=\d+)|(\/(id)\/\d+)|(&id=\d+)|(\/(group)\/\d+)|(&group=\d+)/, "");
url = url.replace('.html', '');
~~~
然后admin.js里定義一個高亮菜單的函數:
~~~
function highlight_menu(url) {
$('.nav li').removeClass('active');
$('.nav .dropdown-menu>li>a[href*="'+url+'"]').parent().addClass('active').parents('.dropdown').addClass('active');
$('.nav-collapse .nav li a[href*="'+url+'"]').parent().addClass('active');
}
~~~
admin.js最后, 初始話時調用這個函數。
~~~
$(function(){
//導航子頁面高亮選中
highlight_menu(url);
});
~~~
# 配置
在我遇到typecho 時,我就被它的簡潔所打敗了。它的后臺也是如此。因此,在我開發OneBlog時,我就用OneThink的功能,然后移植typecho的后臺風格,包括登錄頁之類的。
像這樣:

配置我是直接移植OneThink的模板加控制器。前面大家前臺也看到了,同樣的初始化里,讀取數據庫配置,合并到配置數組里去。
后臺的模板代碼我就不帖了,和OneBlog里的差不多。只不過繼承的父模板不一樣。
主要說一下保存。
OneBlog 里的配置,我是copy OneThink里的 控制器方法。
在freelog里,我們不是寫了api了嗎?
我就把那個配置里的保存方法,給他整合到api里面了。其他和OneBlog一樣,列表、編輯頁讀數據什么的。
用過OneThink的開發人員都清楚,OneThink里的配置保存有兩種,一種是所有配置保存,一種是單獨配置項的保存。



全部配置保存的html里保存的是個多維數組,然后提交到 api.php/config/save方法里。
而單獨配置的保存,我還是api的config,只不過提交時表單里帶id,鍵名不是數組了,是配置項的對應鍵,如sort。


因此,api里,config不能像普通表的curd那么去做。
我們回去看api的EmptyController

先定義了其他處理的資源,config。
然后控制器里單獨實現config方法:
~~~
public function config($name = 0){
$model = D('Config');
$result = true;
$data = array();
$code = 404;
$url = '';
switch ($this->_method){
case 'head':
break;
case 'option':
break;
case 'get': // 列出資源
if('list' == $name){
$data = $model->select();
}else{
$id = intval($name);
$data = $model->find($id);
}
if($model->getError() || $model->getDbError()){
$result = false;
}else{
$code = 200;
}
break;
case 'put':
if('save' == $name){
// 批量更新資源
$config = I('put.config');
if(empty($config)){
$result = false;
$data = '表單為空';
}else{
if($config && is_array($config)){
foreach ($config as $name => $value) {
$map = array('name' => $name);
$model->where($map)->setField('value', $value);
}
}
S('DB_CONFIG_DATA',null);
$code = 200;
}
}else{
$puts = $model->create(I('put.'));
if(false === $puts){
$result = false;
$data = $model->getError();
}else{
$id = $puts['id'];
if($find = $model->find($id)){
$result = false !== $model->save($puts);
$code = $result? 200: 404;
}else{
$result = false;
$data = "record not found";
$code = 412;
}
}
}
break;
case 'post': // 新增資源
$posts = $model->create();
if(false == $posts){
$data = $model->getError();
$result = false;
}else{
$id = $model->add();
if(!$id){
$result = false;
}else{
$code = 201;
$data = $id;
$url = '/admin.php/Config/index';
}
}
break;
case 'delete':// 刪除資源
// parse_str(file_get_contents('php://input'), $_DELETE);
// slog($_DELETE);
$id = array_unique((array)I('get.id',0));
slog($id);
if ( empty($id) ) {
$code = 404;
$data = '請選擇要操作的數據';
}else{
$code = 200;
$map = array('id' => array('in', $id) );
if(M('Config')->where($map)->delete()){
S('DB_CONFIG_DATA',null);
//記錄行為
$url = '/admin.php/Config/index';
$data = '刪除成功';
} else {
$code = 412;
$result = false;
$data = '刪除失敗!';
}
}
break;
}
if($result){
$this->success($data, $code, $url);
}else{
$this->error($data, $code, $url);
}
}
~~~
先看更新:
~~~
if('save' == $name){
// 批量更新資源
$config = I('put.config');
if(empty($config)){
$result = false;
$data = '表單為空';
}else{
if($config && is_array($config)){
foreach ($config as $name => $value) {
$map = array('name' => $name);
$model->where($map)->setField('value', $value);
}
}
$code = 200;
}
}else{
$puts = $model->create(I('put.'));
if(false === $puts){
$result = false;
$data = $model->getError();
}else{
$id = $puts['id'];
if($find = $model->find($id)){
$result = false !== $model->save($puts);
$code = $result? 200: 404;
}else{
$result = false;
$data = "record not found";
$code = 412;
}
}
S('DB_CONFIG_DATA',null);
}
~~~
通過$name 是否為save,區分是批量更新還是單個更新。單個更新和之前所有數據表資源的更新一樣。而批量更新,獲取的是 I('put.config') 多維數組,并且通過遍歷去更新每一個:
~~~
foreach ($config as $name => $value) {
$map = array('name' => $name);
$model->where($map)->setField('value', $value);
}
~~~
最后清除一下緩存。
# 資源
資源導航包括了文件、圖片和標簽。
我以文件為例,其他的都差不多,只是模板里個別字段不一樣。

模板resource/files.html代碼:
~~~
<extend name="Public/base" />
<block name="body">
<div class="main-title location">
<h2>文件</h2>
</div>
<div class="data-table table-striped"></div>
<div class="typecho-table-wrap">
<table class="table table-hover" id="del_table">
<thead>
<tr>
<th>ID</th>
<th>原始名</th>
<th>后綴</th>
<th>路徑</th>
<th>大小</th>
<th>上傳時間</th>
</tr>
</thead>
<tbody>
<notempty name="list">
<volist name="list" id="file">
<tr>
<td>{$file.id}</td>
<td>{$file.savename}</td>
<td>{$file.ext}</td>
<td><a href="{$file.path}" target="_blank">{$file.path}</a></td>
<td>{$file.size|format_bytes}</td>
<td>{$file.create_time|date="Y-m-d h:i:s",###}</td>
</tr>
</volist>
<else/>
<td colspan="6" class="text-center"> aOh! 暫時還沒有內容! </td>
</notempty>
</tbody>
</table>
</div>
<!-- 分頁 -->
<div class="pagination">
<div class="pull-right">
{$_page}
</div>
</div>
</block>
~~~
處理了大小的格式化顯示,和創建時間格式化。
控制器代碼也簡單:
~~~
<?php
namespace Admin\Controller;
use Think\Controller;
class ResourceController extends CommonController {
//標簽
public function tags(){
/* 查詢條件初始化 */
$map = array('status' => 1);
if(isset($_GET['title'])){
$map['title'] = array('like', '%'.(string)I('title').'%');
}
$this->meta_title = '標簽';
$this->_list(array('source' => 'Tags', 'map' => $map, 'order' => '`id`'));
}
//圖片
public function pics(){
/* 查詢條件初始化 */
$map = array('status' => 1);
$this->meta_title = '圖片';
$this->_list(array('source' => 'Picture', 'map' => $map, 'order' => '`id`'));
}
public function files(){
/* 查詢條件初始化 */
$map = array('status' => 1);
$this->meta_title = '文件';
$this->_list(array('source' => 'File', 'map' => $map, 'order' => '`id`'));
}
}
~~~
圖片和標簽只不過換了資源名,并且標簽加了title搜索。
這一切復用了common控制器里的_list方法。
因為文件、圖片、標簽都是用戶產生,本來后臺就不應該有刪除的行為。因為刪除了文章里關聯的數據就會有問題。
顯示出來是為了方便審查,萬一用戶上傳了黃色圖片和音頻怎么辦。
讀取時按照status=1處理的。到時候加刪除也簡單,通過刪除鏈接和ajax類更新該記錄為status=0就好了。
# 文章
文章,為了方便管理,將四種類型通過菜單區分管理列表。

為了實現動態菜單,
控制器里方法也是通過空操縱實現:
~~~
<?php
namespace Admin\Controller;
use Think\Controller;
class PostController extends CommonController {
public function _empty($action = 'text'){
if(!in_array($action, array('text', 'picture', 'video', 'music')))
$this->error('錯誤的文章類型');
/* 查詢條件初始化 */
$map = array();
$map['type'] = $action;
if($search = I('get.title', '', 'trim')){
$map['_string'] = "`title` LIKE '%{$search}%' OR `description` LIKE '%{$search}' OR FIND_IN_SET('{$search}', tags)";
}
$this->meta_title = '文章管理';
$this->_list(array('source' => 'Post', 'map' => $map, 'order' => '`id`', 'tpl'=>strtolower($action)));
}
}
~~~
不過比之前標簽搜索,提供多了一些字段,如描述、和標簽。模板也是動態的。如文章模板。
~~~
<extend name="Public/base" />
<block name="body">
<div class="main-title location">
<h2>文章</h2>
</div>
<div class="data-table table-striped">
<div class="clearfix">
<!-- 高級搜索 -->
<div class="search-form pull-right clearfix form-inline">
<div class="input-group mb10">
<form action="{:U()}" method="GET">
<input type="text" name="title" class="form-control text-s" value="{:I('title')}" placeholder="請輸入標簽名稱">
<span class="input-group-btn">
<button class="btn btn-default"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></button>
</span>
</form>
</div><!-- /input-group -->
</div>
</div>
<div class="typecho-table-wrap">
<table class="table table-hover" id="del_table">
<thead>
<tr>
<th>ID</th>
<th>標題</th>
<th>描述</th>
<th>作者</th>
<th>標簽</th>
<th>瀏覽數</th>
<th>最后更新時間</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<notempty name="list">
<volist name="list" id="post">
<tr>
<td>{$post.id}</td>
<td>{$post.title}</td>
<td>{$post.description}</td>
<td>{$post.author}</td>
<td>{$post.tags}</td>
<td>{$post.views}</td>
<td>{$post.update_at}</td>
<td><a href="/api.php/post?id={$post.id}" class="ajax-delete confirm">刪除</a></td>
</tr>
</volist>
<else/>
<td colspan="6" class="text-center"> aOh! 暫時還沒有內容! </td>
</notempty>
</tbody>
</table>
</div>
<!-- 分頁 -->
<div class="pagination">
<div class="pull-right">
{$_page}
</div>
</div>
</block>
~~~
這里為了以后擴展方便預留了。每個類別文章沒用一個模板,是為了以后實現不同的預覽和編輯著想,先只顯示基礎數據吧。
至于搜索,一個get表單搞定:
~~~
<form action="{:U()}" method="GET">
<input type="text" name="title" class="form-control text-s" value="{:I('title')}" placeholder="請輸入標簽名稱">
<span class="input-group-btn">
<button class="btn btn-default"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></button>
</span>
</form>
~~~
至于刪除,和前臺一樣,交給api、ajax-delete。
`<a href="/api.php/post?id={$post.id}" class="ajax-delete confirm">刪除</a>`
# 微信
本來微信想做很多功能,實現像公眾平臺一樣的功能(多媒體管理,消息查看)。可是后來嘗試發現,個人開發者未認證用戶,沒那些高級接口的權限。
所以干脆,只做了2個功能頁:微信服務器ip列表,和微信平臺(iframe實現,要怎么管理自己登錄去吧)。
## 服務器ip

直接看控制器 :
~~~
<?php
namespace Admin\Controller;
use Think\Controller;
use Com\Wechat;
use Com\WechatAuth;
class WeixinController extends CommonController {
//初始化方法
protected function _initialize() {
if(!C('WEIXIN.APPID')){
$this->error('請先配置好微信');
}
}
//首頁
public function index(){
$wechatauth = new WechatAuth(C('WEIXIN.APPID'), C('WEIXIN.SECRET'), C(''));
$access_token = $wechatauth->getAccessToken();
$result = $wechatauth->getServerIp($access_token);
if(isset($result['errcode']))
$this->error($result['errmsg']);
slog($result);
$this->assign('ip', $result['ip_list']);
$this->display();
}
//菜單
public function menu(){
$this->display();
}
}
~~~
weixin/index.html 代碼:
~~~
<extend name="Public/base" />
<block name="body">
<div class="col-md-4"></div>
<div class="col-md-4">
<table class="table table-hover">
<thead><th>微信服務器IP:</th></thead>
<tbody>
<volist name="ip" id="vo">
<tr><td>{$vo}</td></tr>
</volist>
</tbody>
</table>
</div>
<div class="col-md-4"></div>
</block>
~~~
我也是盲人摸象,摸索出WechatAuth類的使用。并且,自己擴展了一個getServerIp的方法:
~~~
public function getServerIp($token){
return $this->api('getcallbackip', array('access_token'=>$token));
}
~~~
后面微信開發里會細講。
## 菜單
沒啥技術含量,但是思路不錯:
~~~
<extend name="Public/base" />
<block name="body">
<iframe width="100%" height="700" src="https://mp.weixin.qq.com/"></iframe>
</block>
~~~
- 序
- 前言
- 內容簡介
- 目錄
- 基礎知識
- 起步
- 控制器
- 模型
- 模板
- 命名空間
- 進階知識
- 路由
- 配置
- 緩存
- 權限
- 擴展
- 國際化
- 安全
- 單元測試
- 拿來主義
- 調試方法
- 調試的步驟
- 調試工具
- 顯示trace信息
- 開啟調試和關閉調試的區別
- netbeans+xdebug
- Socketlog
- PHP常見錯誤
- 小黃鴨調試法,每個程序員都要知道的
- 應用場景
- 第三方登錄
- 圖片處理
- 博客
- SAE
- REST實踐
- Cli
- ajax分頁
- barcode條形碼
- excel
- 發郵件
- 漢字轉全拼和首字母,支持帶聲調
- 中文分詞
- 瀏覽器useragent解析
- freelog項目實戰
- 需求分析
- 數據庫設計
- 編碼實踐
- 前端實現
- rest接口
- 文章發布
- 文件上傳
- 視頻播放
- 音樂播放
- 圖片幻燈片展示
- 注冊和登錄
- 個人資料更新
- 第三方登錄的使用
- 后臺
- 微信的開發
- 首頁及個人主頁
- 列表
- 歸檔
- 搜索
- 分頁
- 總結經驗
- 自我提升
- 進行小項目的鍛煉
- 對現有輪子的重構和移植
- 寫技術博客
- 制作視頻教程
- 學習PHP的知識和新特性
- 和同行直接溝通、交流
- 學好英語,走向國際
- 如何參與
- 瀏覽官網和極思維還有看云
- 回答ThinkPHP新手的問題
- 嘗試發現ThinkPHP的bug,告訴官方人員或者push request
- 開發能提高效率的ThinkPHP工具
- 嘗試翻譯官方文檔
- 幫新手入門
- 創造基于ThinkPHP的產品,進行連帶推廣
- 展望未來
- OneThink
- ThinkPHP4
- 附錄