chenhao 5 years ago
parent
commit
b60bf9d6f9
100 changed files with 31345 additions and 1 deletions
  1. 0 1
      .gitignore
  2. 189 0
      runtime/temp/18483e3f1cb99c0d39cff757b79197e9.php
  3. 4 0
      thinkphp/.gitignore
  4. 1 0
      thinkphp/.htaccess
  5. 47 0
      thinkphp/.travis.yml
  6. 119 0
      thinkphp/CONTRIBUTING.md
  7. 32 0
      thinkphp/LICENSE.txt
  8. 114 0
      thinkphp/README.md
  9. 65 0
      thinkphp/base.php
  10. 12 0
      thinkphp/codecov.yml
  11. 35 0
      thinkphp/composer.json
  12. 20 0
      thinkphp/console.php
  13. 298 0
      thinkphp/convention.php
  14. 589 0
      thinkphp/helper.php
  15. 136 0
      thinkphp/lang/zh-cn.php
  16. 672 0
      thinkphp/library/think/App.php
  17. 235 0
      thinkphp/library/think/Build.php
  18. 247 0
      thinkphp/library/think/Cache.php
  19. 457 0
      thinkphp/library/think/Collection.php
  20. 214 0
      thinkphp/library/think/Config.php
  21. 863 0
      thinkphp/library/think/Console.php
  22. 229 0
      thinkphp/library/think/Controller.php
  23. 268 0
      thinkphp/library/think/Cookie.php
  24. 180 0
      thinkphp/library/think/Db.php
  25. 252 0
      thinkphp/library/think/Debug.php
  26. 39 0
      thinkphp/library/think/Env.php
  27. 136 0
      thinkphp/library/think/Error.php
  28. 55 0
      thinkphp/library/think/Exception.php
  29. 478 0
      thinkphp/library/think/File.php
  30. 148 0
      thinkphp/library/think/Hook.php
  31. 610 0
      thinkphp/library/think/Image.php
  32. 265 0
      thinkphp/library/think/Lang.php
  33. 677 0
      thinkphp/library/think/Loader.php
  34. 237 0
      thinkphp/library/think/Log.php
  35. 2334 0
      thinkphp/library/think/Model.php
  36. 409 0
      thinkphp/library/think/Paginator.php
  37. 1205 0
      thinkphp/library/think/Process.php
  38. 1679 0
      thinkphp/library/think/Request.php
  39. 332 0
      thinkphp/library/think/Response.php
  40. 1645 0
      thinkphp/library/think/Route.php
  41. 366 0
      thinkphp/library/think/Session.php
  42. 1139 0
      thinkphp/library/think/Template.php
  43. 333 0
      thinkphp/library/think/Url.php
  44. 1335 0
      thinkphp/library/think/Validate.php
  45. 239 0
      thinkphp/library/think/View.php
  46. 231 0
      thinkphp/library/think/cache/Driver.php
  47. 268 0
      thinkphp/library/think/cache/driver/File.php
  48. 187 0
      thinkphp/library/think/cache/driver/Lite.php
  49. 177 0
      thinkphp/library/think/cache/driver/Memcache.php
  50. 187 0
      thinkphp/library/think/cache/driver/Memcached.php
  51. 188 0
      thinkphp/library/think/cache/driver/Redis.php
  52. 199 0
      thinkphp/library/think/cache/driver/Sqlite.php
  53. 152 0
      thinkphp/library/think/cache/driver/Wincache.php
  54. 155 0
      thinkphp/library/think/cache/driver/Xcache.php
  55. 24 0
      thinkphp/library/think/config/driver/Ini.php
  56. 24 0
      thinkphp/library/think/config/driver/Json.php
  57. 31 0
      thinkphp/library/think/config/driver/Xml.php
  58. 470 0
      thinkphp/library/think/console/Command.php
  59. 464 0
      thinkphp/library/think/console/Input.php
  60. 19 0
      thinkphp/library/think/console/LICENSE
  61. 222 0
      thinkphp/library/think/console/Output.php
  62. 1 0
      thinkphp/library/think/console/bin/README.md
  63. BIN
      thinkphp/library/think/console/bin/hiddeninput.exe
  64. 56 0
      thinkphp/library/think/console/command/Build.php
  65. 63 0
      thinkphp/library/think/console/command/Clear.php
  66. 69 0
      thinkphp/library/think/console/command/Help.php
  67. 74 0
      thinkphp/library/think/console/command/Lists.php
  68. 110 0
      thinkphp/library/think/console/command/Make.php
  69. 50 0
      thinkphp/library/think/console/command/make/Controller.php
  70. 36 0
      thinkphp/library/think/console/command/make/Model.php
  71. 10 0
      thinkphp/library/think/console/command/make/stubs/controller.plain.stub
  72. 85 0
      thinkphp/library/think/console/command/make/stubs/controller.stub
  73. 10 0
      thinkphp/library/think/console/command/make/stubs/model.stub
  74. 294 0
      thinkphp/library/think/console/command/optimize/Autoload.php
  75. 93 0
      thinkphp/library/think/console/command/optimize/Config.php
  76. 75 0
      thinkphp/library/think/console/command/optimize/Route.php
  77. 118 0
      thinkphp/library/think/console/command/optimize/Schema.php
  78. 115 0
      thinkphp/library/think/console/input/Argument.php
  79. 375 0
      thinkphp/library/think/console/input/Definition.php
  80. 190 0
      thinkphp/library/think/console/input/Option.php
  81. 340 0
      thinkphp/library/think/console/output/Ask.php
  82. 319 0
      thinkphp/library/think/console/output/Descriptor.php
  83. 198 0
      thinkphp/library/think/console/output/Formatter.php
  84. 211 0
      thinkphp/library/think/console/output/Question.php
  85. 149 0
      thinkphp/library/think/console/output/descriptor/Console.php
  86. 52 0
      thinkphp/library/think/console/output/driver/Buffer.php
  87. 373 0
      thinkphp/library/think/console/output/driver/Console.php
  88. 33 0
      thinkphp/library/think/console/output/driver/Nothing.php
  89. 116 0
      thinkphp/library/think/console/output/formatter/Stack.php
  90. 189 0
      thinkphp/library/think/console/output/formatter/Style.php
  91. 163 0
      thinkphp/library/think/console/output/question/Choice.php
  92. 57 0
      thinkphp/library/think/console/output/question/Confirmation.php
  93. 99 0
      thinkphp/library/think/controller/Rest.php
  94. 51 0
      thinkphp/library/think/controller/Yar.php
  95. 895 0
      thinkphp/library/think/db/Builder.php
  96. 1070 0
      thinkphp/library/think/db/Connection.php
  97. 48 0
      thinkphp/library/think/db/Expression.php
  98. 2998 0
      thinkphp/library/think/db/Query.php
  99. 134 0
      thinkphp/library/think/db/builder/Mysql.php
  100. 89 0
      thinkphp/library/think/db/builder/Pgsql.php

+ 0 - 1
.gitignore

@@ -1,4 +1,3 @@
 .idea
 composer.lock
 *.log
-thinkphp

+ 189 - 0
runtime/temp/18483e3f1cb99c0d39cff757b79197e9.php

@@ -0,0 +1,189 @@
+<?php if (!defined('THINK_PATH')) exit(); /*a:1:{s:78:"E:\PHPstudy\PHPTutorial\WWW\shibie\public/../application/admin\view\login.html";i:1585617946;}*/ ?>
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+    <title>后台登录</title>
+    <link rel="stylesheet" href="/static/admin/js/layui/css/layui.css">
+    <link rel="stylesheet" href="/static/admin/css/admin.css">
+    <link rel="stylesheet" href="/static/admin/css/login.css">
+    <!--<link id="layuicss-layer" rel="stylesheet" href="/static/admin/css/layer.css" media="all">-->
+    <script src="http://static.geetest.com/static/tools/gt.js"></script>
+    <script>
+        /*保证登录页面是顶级页面*/
+        if(window.top!==window.self){window.top.location=window.location};
+    </script>
+</head>
+<body layadmin-themealias="default">
+<div class="layadmin-user-login layadmin-user-display-show" id="LAY-user-login" >
+    <div class="layadmin-user-login-main">
+        <div class="layadmin-user-login-box layadmin-user-login-header">
+            <h2>图像搜索后台</h2>
+            <p>后台管理系统</p>
+        </div>
+        <div class="layadmin-user-login-box layadmin-user-login-body">
+            <form class="layui-form" onsubmit="return false;">
+                <div class="layui-form-item">
+                    <label class="layadmin-user-login-icon layui-icon layui-icon-username"
+                           for="LAY-user-login-username"></label>
+                    <input type="text" name="username" id="LAY-user-login-username" lay-verify="required" placeholder="用户名" class="layui-input">
+                </div>
+                <div class="layui-form-item">
+                    <label class="layadmin-user-login-icon layui-icon layui-icon-password"
+                           for="LAY-user-login-password"></label>
+                    <input type="password" name="password" id="LAY-user-login-password" lay-verify="required" placeholder="密码" class="layui-input">
+                </div>
+                <div class="layui-form-item">
+                    <div class="layui-row">
+                        <?php if(config('verify_type') == 1): ?>
+                        <div class="layui-col-xs7">
+                            <label class="layadmin-user-login-icon layui-icon layui-icon-vercode"
+                                   for="LAY-user-login-vercode"></label>
+                            <input type="text" name="vercode" id="LAY-user-login-vercode" lay-verify="required" placeholder="图形验证码" class="layui-input">
+                        </div>
+                        <div class="layui-col-xs5">
+                            <div style="margin-left: 10px;">
+                                <img src="<?php echo url('checkVerify'); ?>" onclick="javascript:this.src='<?php echo url('checkVerify'); ?>?tm='+Math.random();" style="float:right;cursor: pointer" id="verify"/>
+                            </div>
+                        </div>
+                        <?php elseif(config('verify_type') == 2): ?>
+                        <div class="layui-col-xs12" id="geeFa">
+                            <div id="embed-captcha"></div>
+                        </div>
+                        <p id="wait">正在加载验证码.... <i class="layui-icon layui-icon-loading-1 layui-icon layui-anim layui-anim-rotate layui-anim-loop"></i></p>
+                        <?php endif; ?>
+                    </div>
+                </div>
+                <div class="layui-form-item" style="margin-bottom: 20px;">
+                    <!--<input type="checkbox" name="remember" lay-skin="primary" title="记住密码">-->
+                    <!--<div class="layui-unselect layui-form-checkbox" lay-skin="primary"><span>记住密码</span><i-->
+                            <!--class="layui-icon layui-icon-ok"></i></div>-->
+                    <!--<a href="javascript:;" class="layadmin-user-jump-change layadmin-link" style="margin-top: 7px;">忘记密码?</a>-->
+                </div>
+                <div class="layui-form-item">
+                    <button class="layui-btn layui-btn-fluid" lay-submit="" lay-filter="LAY-login">登 录</button>
+                </div>
+            </form>
+        </div>
+    </div>
+
+    <div class="layui-trans layadmin-user-login-footer">
+        <p>© 2018 <a href="http://www.layui.com/" target="_blank">layui.com</a></p>
+    </div>
+</div>
+<script src="/static/admin/js/layui/layui.all.js"></script>
+<script src="/static/admin/js/jquery.min.js"></script>
+<script src="/static/admin/js/wk.js"></script>
+<script>
+    $('#LAY-user-login-username').focus();
+    var handlerEmbed = function (captchaObj) {
+        $("#embed-submit").click(function (e) {
+            var validate = captchaObj.getValidate();
+            if (!validate) {
+                $("#notice")[0].className = "show";
+                setTimeout(function () {
+                    $("#notice")[0].className = "hide";
+                }, 2000);
+                e.preventDefault();
+            }
+        });
+        // 将验证码加到id为captcha的元素里
+        captchaObj.appendTo("#embed-captcha");
+        captchaObj.onReady(function () {
+            $("#wait")[0].className = "layui-hide";
+        });
+        // 更多接口参考:http://www.geetest.com/install/sections/idx-client-sdk.html
+    };
+    var geeCheck = function(){
+        $.ajax({
+            // 获取id,challenge,success(是否启用failback)
+            url: "<?php echo url('getVerify',array('t'=>time())); ?>", // 加随机数防止缓存
+            type: "get",
+            dataType: "json",
+            success: function (data) {
+                // 使用initGeetest接口
+                // 参数1:配置参数
+                // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它做appendTo之类的事件
+                initGeetest({
+                    gt: data.gt,
+                    challenge: data.challenge,
+                    product: "float", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效
+                    offline: !data.success, // 表示用户后台检测极验服务器是否宕机,一般不需要关注
+                    width:"100%",
+                }, handlerEmbed);
+            }
+        });
+    }
+    if('<?php echo config('verify_type'); ?>' == 2){
+        geeCheck();
+    }
+    layui.use('form', function(){
+        var form = layui.form;
+        //监听提交
+        form.on('submit(LAY-login)', function(data){
+            $(".layui-btn").addClass('layui-disabled').attr('disabled','disabled').html('登录中... &nbsp;<i class="layui-icon layui-icon-loading-1 layui-icon layui-anim layui-anim-rotate layui-anim-loop"></i>');
+            $.ajax({
+                url:"<?php echo url('doLogin'); ?>",
+                type:'post',
+                dataType:'json',
+                data:data.field,
+                success:function(res){
+                    if(res.code == 1){
+                        setTimeout(function(){
+                            location.href=res.url;
+                        },1500);
+                    }else{
+                        layer.msg(res.msg,{icon:2,time:1500,anim: 6},function(index){
+                            if('<?php echo config('verify_type'); ?>' == 2 && res.code != -3){
+                                $("#wait").removeClass('layui-hide');
+                                $('#embed-captcha').remove();
+                                $('#geeFa').append('<div id="embed-captcha"></div>')
+                                geeCheck();
+                            }else if('<?php echo config('verify_type'); ?>' == 1){
+                                $('#verify').attr("src","<?php echo url('checkVerify'); ?>?tm="+Math.random());
+                            }
+                            switch(res.code)
+                            {
+                                case -1:
+                                    $('#LAY-user-login-username').focus();
+                                    break;
+                                case -2:
+                                    $('#LAY-user-login-password').focus();
+                                    break;
+                                case -4:
+                                    $('#LAY-user-login-vercode').focus();
+                                    break;
+                            }
+                            layer.close(index);
+                        })
+                        $(".layui-btn").removeClass('layui-disabled').removeAttr('disabled').text("登 录");
+                        return false;
+                    }
+                }
+                ,error:function(event, xhr, options, exc){
+                    switch (event.status) {
+                        case 403:
+                            wk.error('403:禁止访问...');
+                            break;
+                        case 404:
+                            wk.error('404:请求服务器出错...');
+                            break;
+                        case 500:
+                            wk.error('500:服务器错误...');
+                            break;
+                    }
+                    $(".layui-btn").removeClass('layui-disabled').removeAttr('disabled').text("登 录");
+                }
+            })
+        });
+    });
+    //防止页面后退
+    history.pushState(null, null, document.URL);
+    window.addEventListener('popstate', function () {
+        history.pushState(null, null, document.URL);
+    });
+
+</script>
+</body>
+</html>

+ 4 - 0
thinkphp/.gitignore

@@ -0,0 +1,4 @@
+/composer.lock
+/vendor
+.idea
+.DS_Store

+ 1 - 0
thinkphp/.htaccess

@@ -0,0 +1 @@
+deny from all

+ 47 - 0
thinkphp/.travis.yml

@@ -0,0 +1,47 @@
+sudo: false
+
+language: php
+
+services:
+  - memcached
+  - mongodb
+  - mysql
+  - postgresql
+  - redis-server
+
+matrix:
+  fast_finish: true
+  include:
+    - php: 5.4
+    - php: 5.5
+    - php: 5.6
+    - php: 7.0
+    - php: hhvm
+  allow_failures:
+    - php: hhvm
+
+cache:
+  directories:
+    - $HOME/.composer/cache
+
+before_install:
+  - composer self-update
+  - mysql -e "create database IF NOT EXISTS test;" -uroot
+  - psql -c 'DROP DATABASE IF EXISTS test;' -U postgres
+  - psql -c 'create database test;' -U postgres
+
+install:
+  - ./tests/script/install.sh
+
+script:
+  ## LINT
+  - find . -path ./vendor -prune -o -type f -name \*.php -exec php -l {} \;
+  ## PHP Copy/Paste Detector
+  - vendor/bin/phpcpd --verbose --exclude vendor ./ || true
+  ## PHPLOC
+  - vendor/bin/phploc --exclude vendor ./
+  ## PHPUNIT
+  - vendor/bin/phpunit --coverage-clover=coverage.xml --configuration=phpunit.xml
+
+after_success:
+  - bash <(curl -s https://codecov.io/bash)

+ 119 - 0
thinkphp/CONTRIBUTING.md

@@ -0,0 +1,119 @@
+如何贡献我的源代码
+===
+
+此文档介绍了 ThinkPHP 团队的组成以及运转机制,您提交的代码将给 ThinkPHP 项目带来什么好处,以及如何才能加入我们的行列。
+
+## 通过 Github 贡献代码
+
+ThinkPHP 目前使用 Git 来控制程序版本,如果你想为 ThinkPHP 贡献源代码,请先大致了解 Git 的使用方法。我们目前把项目托管在 GitHub 上,任何 GitHub 用户都可以向我们贡献代码。
+
+参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。
+
+我们希望你贡献的代码符合:
+
+* ThinkPHP 的编码规范
+* 适当的注释,能让其他人读懂
+* 遵循 Apache2 开源协议
+
+**如果想要了解更多细节或有任何疑问,请继续阅读下面的内容**
+
+### 注意事项
+
+* 本项目代码格式化标准选用 [**PSR-2**](http://www.kancloud.cn/thinkphp/php-fig-psr/3141);
+* 类名和类文件名遵循 [**PSR-4**](http://www.kancloud.cn/thinkphp/php-fig-psr/3144);
+* 对于 Issues 的处理,请使用诸如 `fix #xxx(Issue ID)` 的 commit title 直接关闭 issue。
+* 系统会自动在 PHP 5.4 5.5 5.6 7.0 和 HHVM 上测试修改,其中 HHVM 下的测试容许报错,请确保你的修改符合 PHP 5.4 ~ 5.6 和 PHP 7.0 的语法规范;
+* 管理员不会合并造成 CI faild 的修改,若出现 CI faild 请检查自己的源代码或修改相应的[单元测试文件](tests);
+
+## GitHub Issue
+
+GitHub 提供了 Issue 功能,该功能可以用于:
+
+* 提出 bug
+* 提出功能改进
+* 反馈使用体验
+
+该功能不应该用于:
+
+ * 提出修改意见(涉及代码署名和修订追溯问题)
+ * 不友善的言论
+
+## 快速修改
+
+**GitHub 提供了快速编辑文件的功能**
+
+1. 登录 GitHub 帐号;
+2. 浏览项目文件,找到要进行修改的文件;
+3. 点击右上角铅笔图标进行修改;
+4. 填写 `Commit changes` 相关内容(Title 必填);
+5. 提交修改,等待 CI 验证和管理员合并。
+
+**若您需要一次提交大量修改,请继续阅读下面的内容**
+
+## 完整流程
+
+1. `fork`本项目;
+2. 克隆(`clone`)你 `fork` 的项目到本地;
+3. 新建分支(`branch`)并检出(`checkout`)新分支;
+4. 添加本项目到你的本地 git 仓库作为上游(`upstream`);
+5. 进行修改,若你的修改包含方法或函数的增减,请记得修改[单元测试文件](tests);
+6. 变基(衍合 `rebase`)你的分支到上游 master 分支;
+7. `push` 你的本地仓库到 GitHub;
+8. 提交 `pull request`;
+9. 等待 CI 验证(若不通过则重复 5~7,不需要重新提交 `pull request`,GitHub 会自动更新你的 `pull request`);
+10. 等待管理员处理,并及时 `rebase` 你的分支到上游 master 分支(若上游 master 分支有修改)。
+
+*若有必要,可以 `git push -f` 强行推送 rebase 后的分支到自己的 `fork`*
+
+*绝对不可以使用 `git push -f` 强行推送修改到上游*
+
+### 注意事项
+
+* 若对上述流程有任何不清楚的地方,请查阅 GIT 教程,如 [这个](http://backlogtool.com/git-guide/cn/);
+* 对于代码**不同方面**的修改,请在自己 `fork` 的项目中**创建不同的分支**(原因参见`完整流程`第9条备注部分);
+* 变基及交互式变基操作参见 [Git 交互式变基](http://pakchoi.me/2015/03/17/git-interactive-rebase/)
+
+## 推荐资源
+
+### 开发环境
+
+* XAMPP for Windows 5.5.x
+* WampServer (for Windows)
+* upupw Apache PHP5.4 ( for Windows)
+
+或自行安装
+
+- Apache / Nginx
+- PHP 5.4 ~ 5.6
+- MySQL / MariaDB
+
+*Windows 用户推荐添加 PHP bin 目录到 PATH,方便使用 composer*
+
+*Linux 用户自行配置环境, Mac 用户推荐使用内置 Apache 配合 Homebrew 安装 PHP 和 MariaDB*
+
+### 编辑器
+
+Sublime Text 3 + phpfmt 插件
+
+phpfmt 插件参数
+
+```json
+{
+	"autocomplete": true,
+	"enable_auto_align": true,
+	"format_on_save": true,
+	"indent_with_space": true,
+	"psr1_naming": false,
+	"psr2": true,
+	"version": 4
+}
+```
+
+或其他 编辑器 / IDE 配合 PSR2 自动格式化工具
+
+### Git GUI
+
+* SourceTree
+* GitHub Desktop
+
+或其他 Git 图形界面客户端

+ 32 - 0
thinkphp/LICENSE.txt

@@ -0,0 +1,32 @@
+
+ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
+版权所有Copyright © 2006-2017 by ThinkPHP (http://thinkphp.cn)
+All rights reserved。
+ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
+
+Apache Licence是著名的非盈利开源组织Apache采用的协议。
+该协议和BSD类似,鼓励代码共享和尊重原作者的著作权,
+允许代码修改,再作为开源或商业软件发布。需要满足
+的条件: 
+1. 需要给代码的用户一份Apache Licence ;
+2. 如果你修改了代码,需要在被修改的文件中说明;
+3. 在延伸的代码中(修改和有源代码衍生的代码中)需要
+带有原来代码中的协议,商标,专利声明和其他原来作者规
+定需要包含的说明;
+4. 如果再发布的产品中包含一个Notice文件,则在Notice文
+件中需要带有本协议内容。你可以在Notice中增加自己的
+许可,但不可以表现为对Apache Licence构成更改。 
+具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.

+ 114 - 0
thinkphp/README.md

@@ -0,0 +1,114 @@
+ThinkPHP 5.0
+===============
+
+[![StyleCI](https://styleci.io/repos/48530411/shield?style=flat&branch=master)](https://styleci.io/repos/48530411)
+[![Build Status](https://travis-ci.org/top-think/framework.svg?branch=master)](https://travis-ci.org/top-think/framework)
+[![codecov.io](http://codecov.io/github/top-think/framework/coverage.svg?branch=master)](http://codecov.io/github/github/top-think/framework?branch=master)
+[![Total Downloads](https://poser.pugx.org/topthink/framework/downloads)](https://packagist.org/packages/topthink/framework)
+[![Latest Stable Version](https://poser.pugx.org/topthink/framework/v/stable)](https://packagist.org/packages/topthink/framework)
+[![Latest Unstable Version](https://poser.pugx.org/topthink/framework/v/unstable)](https://packagist.org/packages/topthink/framework)
+[![License](https://poser.pugx.org/topthink/framework/license)](https://packagist.org/packages/topthink/framework)
+
+ThinkPHP5在保持快速开发和大道至简的核心理念不变的同时,PHP版本要求提升到5.4,优化核心,减少依赖,基于全新的架构思想和命名空间实现,是ThinkPHP突破原有框架思路的颠覆之作,其主要特性包括:
+
+ + 基于命名空间和众多PHP新特性
+ + 核心功能组件化
+ + 强化路由功能
+ + 更灵活的控制器
+ + 重构的模型和数据库类
+ + 配置文件可分离
+ + 重写的自动验证和完成
+ + 简化扩展机制
+ + API支持完善
+ + 改进的Log类
+ + 命令行访问支持
+ + REST支持
+ + 引导文件支持
+ + 方便的自动生成定义
+ + 真正惰性加载
+ + 分布式环境支持
+ + 支持Composer
+ + 支持MongoDb
+
+> ThinkPHP5的运行环境要求PHP5.4以上。
+
+详细开发文档参考 [ThinkPHP5完全开发手册](http://www.kancloud.cn/manual/thinkphp5) 以及[ThinkPHP5入门系列教程](http://www.kancloud.cn/special/thinkphp5_quickstart)
+
+## 目录结构
+
+初始的目录结构如下:
+
+~~~
+www  WEB部署目录(或者子目录)
+├─application           应用目录
+│  ├─common             公共模块目录(可以更改)
+│  ├─module_name        模块目录
+│  │  ├─config.php      模块配置文件
+│  │  ├─common.php      模块函数文件
+│  │  ├─controller      控制器目录
+│  │  ├─model           模型目录
+│  │  ├─view            视图目录
+│  │  └─ ...            更多类库目录
+│  │
+│  ├─command.php        命令行工具配置文件
+│  ├─common.php         公共函数文件
+│  ├─config.php         公共配置文件
+│  ├─route.php          路由配置文件
+│  ├─tags.php           应用行为扩展定义文件
+│  └─database.php       数据库配置文件
+│
+├─public                WEB目录(对外访问目录)
+│  ├─index.php          入口文件
+│  ├─router.php         快速测试文件
+│  └─.htaccess          用于apache的重写
+│
+├─thinkphp              框架系统目录
+│  ├─lang               语言文件目录
+│  ├─library            框架类库目录
+│  │  ├─think           Think类库包目录
+│  │  └─traits          系统Trait目录
+│  │
+│  ├─tpl                系统模板目录
+│  ├─base.php           基础定义文件
+│  ├─console.php        控制台入口文件
+│  ├─convention.php     框架惯例配置文件
+│  ├─helper.php         助手函数文件
+│  ├─phpunit.xml        phpunit配置文件
+│  └─start.php          框架入口文件
+│
+├─extend                扩展类库目录
+├─runtime               应用的运行时目录(可写,可定制)
+├─vendor                第三方类库目录(Composer依赖库)
+├─build.php             自动生成定义文件(参考)
+├─composer.json         composer 定义文件
+├─LICENSE.txt           授权说明文件
+├─README.md             README 文件
+├─think                 命令行入口文件
+~~~
+
+> router.php用于php自带webserver支持,可用于快速测试
+> 切换到public目录后,启动命令:php -S localhost:8888  router.php
+> 上面的目录结构和名称是可以改变的,这取决于你的入口文件和配置参数。
+
+## 命名规范
+
+ThinkPHP5的命名规范遵循`PSR-2`规范以及`PSR-4`自动加载规范。
+
+## 参与开发
+注册并登录 Github 帐号, fork 本项目并进行改动。
+
+更多细节参阅 [CONTRIBUTING.md](CONTRIBUTING.md)
+
+## 版权信息
+
+ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有Copyright © 2006-2018 by ThinkPHP (http://thinkphp.cn)
+
+All rights reserved。
+
+ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
+
+更多细节参阅 [LICENSE.txt](LICENSE.txt)

+ 65 - 0
thinkphp/base.php

@@ -0,0 +1,65 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+define('THINK_VERSION', '5.0.21');
+define('THINK_START_TIME', microtime(true));
+define('THINK_START_MEM', memory_get_usage());
+define('EXT', '.php');
+define('DS', DIRECTORY_SEPARATOR);
+defined('THINK_PATH') or define('THINK_PATH', __DIR__ . DS);
+define('LIB_PATH', THINK_PATH . 'library' . DS);
+define('CORE_PATH', LIB_PATH . 'think' . DS);
+define('TRAIT_PATH', LIB_PATH . 'traits' . DS);
+defined('APP_PATH') or define('APP_PATH', dirname($_SERVER['SCRIPT_FILENAME']) . DS);
+defined('ROOT_PATH') or define('ROOT_PATH', dirname(realpath(APP_PATH)) . DS);
+defined('EXTEND_PATH') or define('EXTEND_PATH', ROOT_PATH . 'extend' . DS);
+defined('VENDOR_PATH') or define('VENDOR_PATH', ROOT_PATH . 'vendor' . DS);
+defined('RUNTIME_PATH') or define('RUNTIME_PATH', ROOT_PATH . 'runtime' . DS);
+defined('LOG_PATH') or define('LOG_PATH', RUNTIME_PATH . 'log' . DS);
+defined('CACHE_PATH') or define('CACHE_PATH', RUNTIME_PATH . 'cache' . DS);
+defined('TEMP_PATH') or define('TEMP_PATH', RUNTIME_PATH . 'temp' . DS);
+defined('CONF_PATH') or define('CONF_PATH', APP_PATH); // 配置文件目录
+defined('CONF_EXT') or define('CONF_EXT', EXT); // 配置文件后缀
+defined('ENV_PREFIX') or define('ENV_PREFIX', 'PHP_'); // 环境变量的配置前缀
+
+// 环境常量
+define('IS_CLI', PHP_SAPI == 'cli' ? true : false);
+define('IS_WIN', strpos(PHP_OS, 'WIN') !== false);
+
+// 载入Loader类
+require CORE_PATH . 'Loader.php';
+
+// 加载环境变量配置文件
+if (is_file(ROOT_PATH . '.env')) {
+    $env = parse_ini_file(ROOT_PATH . '.env', true);
+
+    foreach ($env as $key => $val) {
+        $name = ENV_PREFIX . strtoupper($key);
+
+        if (is_array($val)) {
+            foreach ($val as $k => $v) {
+                $item = $name . '_' . strtoupper($k);
+                putenv("$item=$v");
+            }
+        } else {
+            putenv("$name=$val");
+        }
+    }
+}
+
+// 注册自动加载
+\think\Loader::register();
+
+// 注册错误和异常处理机制
+\think\Error::register();
+
+// 加载惯例配置文件
+\think\Config::set(include THINK_PATH . 'convention' . EXT);

+ 12 - 0
thinkphp/codecov.yml

@@ -0,0 +1,12 @@
+comment:
+  layout: header, changes, diff
+coverage:
+  ignore:
+  - base.php
+  - helper.php
+  - convention.php
+  - lang/zh-cn.php
+  - start.php
+  - console.php
+  status:
+    patch: false

+ 35 - 0
thinkphp/composer.json

@@ -0,0 +1,35 @@
+{
+    "name": "topthink/framework",
+    "description": "the new thinkphp framework",
+    "type": "think-framework",
+    "keywords": [
+        "framework",
+        "thinkphp",
+        "ORM"
+    ],
+    "homepage": "http://thinkphp.cn/",
+    "license": "Apache-2.0",
+    "authors": [
+        {
+            "name": "liu21st",
+            "email": "liu21st@gmail.com"
+        }
+    ],
+    "require": {
+        "php": ">=5.4.0",
+        "topthink/think-installer": "~1.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "4.8.*",
+        "johnkary/phpunit-speedtrap": "^1.0",
+        "mikey179/vfsStream": "~1.6",
+        "phploc/phploc": "2.*",
+        "sebastian/phpcpd": "2.*",
+        "phpdocumentor/reflection-docblock": "^2.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "think\\": "library/think"
+        }
+    }
+}

+ 20 - 0
thinkphp/console.php

@@ -0,0 +1,20 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2017 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+// ThinkPHP 引导文件
+// 加载基础文件
+require __DIR__ . '/base.php';
+
+// 执行应用
+App::initCommon();
+Console::init();

+ 298 - 0
thinkphp/convention.php

@@ -0,0 +1,298 @@
+<?php
+
+return [
+    // +----------------------------------------------------------------------
+    // | 应用设置
+    // +----------------------------------------------------------------------
+    // 默认Host地址
+    'app_host'               => '',
+    // 应用调试模式
+    'app_debug'              => false,
+    // 应用Trace
+    'app_trace'              => false,
+    // 应用模式状态
+    'app_status'             => '',
+    // 是否支持多模块
+    'app_multi_module'       => true,
+    // 入口自动绑定模块
+    'auto_bind_module'       => false,
+    // 注册的根命名空间
+    'root_namespace'         => [],
+    // 扩展函数文件
+    'extra_file_list'        => [THINK_PATH . 'helper' . EXT],
+    // 默认输出类型
+    'default_return_type'    => 'html',
+    // 默认AJAX 数据返回格式,可选json xml ...
+    'default_ajax_return'    => 'json',
+    // 默认JSONP格式返回的处理方法
+    'default_jsonp_handler'  => 'jsonpReturn',
+    // 默认JSONP处理方法
+    'var_jsonp_handler'      => 'callback',
+    // 默认时区
+    'default_timezone'       => 'PRC',
+    // 是否开启多语言
+    'lang_switch_on'         => false,
+    // 默认全局过滤方法 用逗号分隔多个
+    'default_filter'         => '',
+    // 默认语言
+    'default_lang'           => 'zh-cn',
+    // 应用类库后缀
+    'class_suffix'           => false,
+    // 控制器类后缀
+    'controller_suffix'      => false,
+
+    // +----------------------------------------------------------------------
+    // | 模块设置
+    // +----------------------------------------------------------------------
+
+    // 默认模块名
+    'default_module'         => 'index',
+    // 禁止访问模块
+    'deny_module_list'       => ['common'],
+    // 默认控制器名
+    'default_controller'     => 'Index',
+    // 默认操作名
+    'default_action'         => 'index',
+    // 默认验证器
+    'default_validate'       => '',
+    // 默认的空控制器名
+    'empty_controller'       => 'Error',
+    // 操作方法前缀
+    'use_action_prefix'      => false,
+    // 操作方法后缀
+    'action_suffix'          => '',
+    // 自动搜索控制器
+    'controller_auto_search' => false,
+
+    // +----------------------------------------------------------------------
+    // | URL设置
+    // +----------------------------------------------------------------------
+
+    // PATHINFO变量名 用于兼容模式
+    'var_pathinfo'           => 's',
+    // 兼容PATH_INFO获取
+    'pathinfo_fetch'         => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
+    // pathinfo分隔符
+    'pathinfo_depr'          => '/',
+    // HTTPS代理标识
+    'https_agent_name'       => '',
+    // URL伪静态后缀
+    'url_html_suffix'        => 'html',
+    // URL普通方式参数 用于自动生成
+    'url_common_param'       => false,
+    // URL参数方式 0 按名称成对解析 1 按顺序解析
+    'url_param_type'         => 0,
+    // 是否开启路由
+    'url_route_on'           => true,
+    // 路由配置文件(支持配置多个)
+    'route_config_file'      => ['route'],
+    // 路由使用完整匹配
+    'route_complete_match'   => false,
+    // 是否强制使用路由
+    'url_route_must'         => false,
+    // 域名部署
+    'url_domain_deploy'      => false,
+    // 域名根,如thinkphp.cn
+    'url_domain_root'        => '',
+    // 是否自动转换URL中的控制器和操作名
+    'url_convert'            => true,
+    // 默认的访问控制器层
+    'url_controller_layer'   => 'controller',
+    // 表单请求类型伪装变量
+    'var_method'             => '_method',
+    // 表单ajax伪装变量
+    'var_ajax'               => '_ajax',
+    // 表单pjax伪装变量
+    'var_pjax'               => '_pjax',
+    // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
+    'request_cache'          => false,
+    // 请求缓存有效期
+    'request_cache_expire'   => null,
+    // 全局请求缓存排除规则
+    'request_cache_except'   => [],
+
+    // +----------------------------------------------------------------------
+    // | 模板设置
+    // +----------------------------------------------------------------------
+
+    'template'               => [
+        // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写
+        'auto_rule'    => 1,
+        // 模板引擎类型 支持 php think 支持扩展
+        'type'         => 'Think',
+        // 视图基础目录,配置目录为所有模块的视图起始目录
+        'view_base'    => '',
+        // 当前模板的视图目录 留空为自动获取
+        'view_path'    => '',
+        // 模板后缀
+        'view_suffix'  => 'html',
+        // 模板文件名分隔符
+        'view_depr'    => DS,
+        // 模板引擎普通标签开始标记
+        'tpl_begin'    => '{',
+        // 模板引擎普通标签结束标记
+        'tpl_end'      => '}',
+        // 标签库标签开始标记
+        'taglib_begin' => '{',
+        // 标签库标签结束标记
+        'taglib_end'   => '}',
+    ],
+
+    // 视图输出字符串内容替换
+    'view_replace_str'       => [],
+    // 默认跳转页面对应的模板文件
+    'dispatch_success_tmpl'  => THINK_PATH . 'tpl' . DS . 'dispatch_jump.tpl',
+    'dispatch_error_tmpl'    => THINK_PATH . 'tpl' . DS . 'dispatch_jump.tpl',
+
+    // +----------------------------------------------------------------------
+    // | 异常及错误设置
+    // +----------------------------------------------------------------------
+
+    // 异常页面的模板文件
+    'exception_tmpl'         => THINK_PATH . 'tpl' . DS . 'think_exception.tpl',
+
+    // 错误显示信息,非调试模式有效
+    'error_message'          => '页面错误!请稍后再试~',
+    // 显示错误信息
+    'show_error_msg'         => false,
+    // 异常处理handle类 留空使用 \think\exception\Handle
+    'exception_handle'       => '',
+    // 是否记录trace信息到日志
+    'record_trace'           => false,
+
+    // +----------------------------------------------------------------------
+    // | 日志设置
+    // +----------------------------------------------------------------------
+
+    'log'                    => [
+        // 日志记录方式,内置 file socket 支持扩展
+        'type'  => 'File',
+        // 日志保存目录
+        'path'  => LOG_PATH,
+        // 日志记录级别
+        'level' => [],
+    ],
+
+    // +----------------------------------------------------------------------
+    // | Trace设置 开启 app_trace 后 有效
+    // +----------------------------------------------------------------------
+    'trace'                  => [
+        // 内置Html Console 支持扩展
+        'type' => 'Html',
+    ],
+
+    // +----------------------------------------------------------------------
+    // | 缓存设置
+    // +----------------------------------------------------------------------
+
+    'cache'                  => [
+        // 驱动方式
+        'type'   => 'File',
+        // 缓存保存目录
+        'path'   => CACHE_PATH,
+        // 缓存前缀
+        'prefix' => '',
+        // 缓存有效期 0表示永久缓存
+        'expire' => 0,
+    ],
+
+    // +----------------------------------------------------------------------
+    // | 会话设置
+    // +----------------------------------------------------------------------
+
+    'session'                => [
+        'id'             => '',
+        // SESSION_ID的提交变量,解决flash上传跨域
+        'var_session_id' => '',
+        // SESSION 前缀
+        'prefix'         => 'think',
+        // 驱动方式 支持redis memcache memcached
+        'type'           => '',
+        // 是否自动开启 SESSION
+        'auto_start'     => true,
+        'httponly'       => true,
+        'secure'         => false,
+    ],
+
+    // +----------------------------------------------------------------------
+    // | Cookie设置
+    // +----------------------------------------------------------------------
+    'cookie'                 => [
+        // cookie 名称前缀
+        'prefix'    => '',
+        // cookie 保存时间
+        'expire'    => 0,
+        // cookie 保存路径
+        'path'      => '/',
+        // cookie 有效域名
+        'domain'    => '',
+        //  cookie 启用安全传输
+        'secure'    => false,
+        // httponly设置
+        'httponly'  => '',
+        // 是否使用 setcookie
+        'setcookie' => true,
+    ],
+
+    // +----------------------------------------------------------------------
+    // | 数据库设置
+    // +----------------------------------------------------------------------
+
+    'database'               => [
+        // 数据库类型
+        'type'            => 'mysql',
+        // 数据库连接DSN配置
+        'dsn'             => '',
+        // 服务器地址
+        'hostname'        => '127.0.0.1',
+        // 数据库名
+        'database'        => '',
+        // 数据库用户名
+        'username'        => 'root',
+        // 数据库密码
+        'password'        => '',
+        // 数据库连接端口
+        'hostport'        => '',
+        // 数据库连接参数
+        'params'          => [],
+        // 数据库编码默认采用utf8
+        'charset'         => 'utf8',
+        // 数据库表前缀
+        'prefix'          => '',
+        // 数据库调试模式
+        'debug'           => false,
+        // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
+        'deploy'          => 0,
+        // 数据库读写是否分离 主从式有效
+        'rw_separate'     => false,
+        // 读写分离后 主服务器数量
+        'master_num'      => 1,
+        // 指定从服务器序号
+        'slave_no'        => '',
+        // 是否严格检查字段是否存在
+        'fields_strict'   => true,
+        // 数据集返回类型
+        'resultset_type'  => 'array',
+        // 自动写入时间戳字段
+        'auto_timestamp'  => false,
+        // 时间字段取出后的默认时间格式
+        'datetime_format' => 'Y-m-d H:i:s',
+        // 是否需要进行SQL性能分析
+        'sql_explain'     => false,
+    ],
+
+    //分页配置
+    'paginate'               => [
+        'type'      => 'bootstrap',
+        'var_page'  => 'page',
+        'list_rows' => 15,
+    ],
+
+    //控制台配置
+    'console'                => [
+        'name'    => 'Think Console',
+        'version' => '0.1',
+        'user'    => null,
+    ],
+
+];

+ 589 - 0
thinkphp/helper.php

@@ -0,0 +1,589 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+//------------------------
+// ThinkPHP 助手函数
+//-------------------------
+
+use think\Cache;
+use think\Config;
+use think\Cookie;
+use think\Db;
+use think\Debug;
+use think\exception\HttpException;
+use think\exception\HttpResponseException;
+use think\Lang;
+use think\Loader;
+use think\Log;
+use think\Model;
+use think\Request;
+use think\Response;
+use think\Session;
+use think\Url;
+use think\View;
+
+if (!function_exists('load_trait')) {
+    /**
+     * 快速导入Traits PHP5.5以上无需调用
+     * @param string    $class trait库
+     * @param string    $ext 类库后缀
+     * @return boolean
+     */
+    function load_trait($class, $ext = EXT)
+    {
+        return Loader::import($class, TRAIT_PATH, $ext);
+    }
+}
+
+if (!function_exists('exception')) {
+    /**
+     * 抛出异常处理
+     *
+     * @param string    $msg  异常消息
+     * @param integer   $code 异常代码 默认为0
+     * @param string    $exception 异常类
+     *
+     * @throws Exception
+     */
+    function exception($msg, $code = 0, $exception = '')
+    {
+        $e = $exception ?: '\think\Exception';
+        throw new $e($msg, $code);
+    }
+}
+
+if (!function_exists('debug')) {
+    /**
+     * 记录时间(微秒)和内存使用情况
+     * @param string            $start 开始标签
+     * @param string            $end 结束标签
+     * @param integer|string    $dec 小数位 如果是m 表示统计内存占用
+     * @return mixed
+     */
+    function debug($start, $end = '', $dec = 6)
+    {
+        if ('' == $end) {
+            Debug::remark($start);
+        } else {
+            return 'm' == $dec ? Debug::getRangeMem($start, $end) : Debug::getRangeTime($start, $end, $dec);
+        }
+    }
+}
+
+if (!function_exists('lang')) {
+    /**
+     * 获取语言变量值
+     * @param string    $name 语言变量名
+     * @param array     $vars 动态变量值
+     * @param string    $lang 语言
+     * @return mixed
+     */
+    function lang($name, $vars = [], $lang = '')
+    {
+        return Lang::get($name, $vars, $lang);
+    }
+}
+
+if (!function_exists('config')) {
+    /**
+     * 获取和设置配置参数
+     * @param string|array  $name 参数名
+     * @param mixed         $value 参数值
+     * @param string        $range 作用域
+     * @return mixed
+     */
+    function config($name = '', $value = null, $range = '')
+    {
+        if (is_null($value) && is_string($name)) {
+            return 0 === strpos($name, '?') ? Config::has(substr($name, 1), $range) : Config::get($name, $range);
+        } else {
+            return Config::set($name, $value, $range);
+        }
+    }
+}
+
+if (!function_exists('input')) {
+    /**
+     * 获取输入数据 支持默认值和过滤
+     * @param string    $key 获取的变量名
+     * @param mixed     $default 默认值
+     * @param string    $filter 过滤方法
+     * @return mixed
+     */
+    function input($key = '', $default = null, $filter = '')
+    {
+        if (0 === strpos($key, '?')) {
+            $key = substr($key, 1);
+            $has = true;
+        }
+        if ($pos = strpos($key, '.')) {
+            // 指定参数来源
+            list($method, $key) = explode('.', $key, 2);
+            if (!in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) {
+                $key    = $method . '.' . $key;
+                $method = 'param';
+            }
+        } else {
+            // 默认为自动判断
+            $method = 'param';
+        }
+        if (isset($has)) {
+            return request()->has($key, $method, $default);
+        } else {
+            return request()->$method($key, $default, $filter);
+        }
+    }
+}
+
+if (!function_exists('widget')) {
+    /**
+     * 渲染输出Widget
+     * @param string    $name Widget名称
+     * @param array     $data 传入的参数
+     * @return mixed
+     */
+    function widget($name, $data = [])
+    {
+        return Loader::action($name, $data, 'widget');
+    }
+}
+
+if (!function_exists('model')) {
+    /**
+     * 实例化Model
+     * @param string    $name Model名称
+     * @param string    $layer 业务层名称
+     * @param bool      $appendSuffix 是否添加类名后缀
+     * @return \think\Model
+     */
+    function model($name = '', $layer = 'model', $appendSuffix = false)
+    {
+        return Loader::model($name, $layer, $appendSuffix);
+    }
+}
+
+if (!function_exists('validate')) {
+    /**
+     * 实例化验证器
+     * @param string    $name 验证器名称
+     * @param string    $layer 业务层名称
+     * @param bool      $appendSuffix 是否添加类名后缀
+     * @return \think\Validate
+     */
+    function validate($name = '', $layer = 'validate', $appendSuffix = false)
+    {
+        return Loader::validate($name, $layer, $appendSuffix);
+    }
+}
+
+if (!function_exists('db')) {
+    /**
+     * 实例化数据库类
+     * @param string        $name 操作的数据表名称(不含前缀)
+     * @param array|string  $config 数据库配置参数
+     * @param bool          $force 是否强制重新连接
+     * @return \think\db\Query
+     */
+    function db($name = '', $config = [], $force = false)
+    {
+        return Db::connect($config, $force)->name($name);
+    }
+}
+
+if (!function_exists('controller')) {
+    /**
+     * 实例化控制器 格式:[模块/]控制器
+     * @param string    $name 资源地址
+     * @param string    $layer 控制层名称
+     * @param bool      $appendSuffix 是否添加类名后缀
+     * @return \think\Controller
+     */
+    function controller($name, $layer = 'controller', $appendSuffix = false)
+    {
+        return Loader::controller($name, $layer, $appendSuffix);
+    }
+}
+
+if (!function_exists('action')) {
+    /**
+     * 调用模块的操作方法 参数格式 [模块/控制器/]操作
+     * @param string        $url 调用地址
+     * @param string|array  $vars 调用参数 支持字符串和数组
+     * @param string        $layer 要调用的控制层名称
+     * @param bool          $appendSuffix 是否添加类名后缀
+     * @return mixed
+     */
+    function action($url, $vars = [], $layer = 'controller', $appendSuffix = false)
+    {
+        return Loader::action($url, $vars, $layer, $appendSuffix);
+    }
+}
+
+if (!function_exists('import')) {
+    /**
+     * 导入所需的类库 同java的Import 本函数有缓存功能
+     * @param string    $class 类库命名空间字符串
+     * @param string    $baseUrl 起始路径
+     * @param string    $ext 导入的文件扩展名
+     * @return boolean
+     */
+    function import($class, $baseUrl = '', $ext = EXT)
+    {
+        return Loader::import($class, $baseUrl, $ext);
+    }
+}
+
+if (!function_exists('vendor')) {
+    /**
+     * 快速导入第三方框架类库 所有第三方框架的类库文件统一放到 系统的Vendor目录下面
+     * @param string    $class 类库
+     * @param string    $ext 类库后缀
+     * @return boolean
+     */
+    function vendor($class, $ext = EXT)
+    {
+        return Loader::import($class, VENDOR_PATH, $ext);
+    }
+}
+
+if (!function_exists('dump')) {
+    /**
+     * 浏览器友好的变量输出
+     * @param mixed     $var 变量
+     * @param boolean   $echo 是否输出 默认为true 如果为false 则返回输出字符串
+     * @param string    $label 标签 默认为空
+     * @return void|string
+     */
+    function dump($var, $echo = true, $label = null)
+    {
+        return Debug::dump($var, $echo, $label);
+    }
+}
+
+if (!function_exists('url')) {
+    /**
+     * Url生成
+     * @param string        $url 路由地址
+     * @param string|array  $vars 变量
+     * @param bool|string   $suffix 生成的URL后缀
+     * @param bool|string   $domain 域名
+     * @return string
+     */
+    function url($url = '', $vars = '', $suffix = true, $domain = false)
+    {
+        return Url::build($url, $vars, $suffix, $domain);
+    }
+}
+
+if (!function_exists('session')) {
+    /**
+     * Session管理
+     * @param string|array  $name session名称,如果为数组表示进行session设置
+     * @param mixed         $value session值
+     * @param string        $prefix 前缀
+     * @return mixed
+     */
+    function session($name, $value = '', $prefix = null)
+    {
+        if (is_array($name)) {
+            // 初始化
+            Session::init($name);
+        } elseif (is_null($name)) {
+            // 清除
+            Session::clear('' === $value ? null : $value);
+        } elseif ('' === $value) {
+            // 判断或获取
+            return 0 === strpos($name, '?') ? Session::has(substr($name, 1), $prefix) : Session::get($name, $prefix);
+        } elseif (is_null($value)) {
+            // 删除
+            return Session::delete($name, $prefix);
+        } else {
+            // 设置
+            return Session::set($name, $value, $prefix);
+        }
+    }
+}
+
+if (!function_exists('cookie')) {
+    /**
+     * Cookie管理
+     * @param string|array  $name cookie名称,如果为数组表示进行cookie设置
+     * @param mixed         $value cookie值
+     * @param mixed         $option 参数
+     * @return mixed
+     */
+    function cookie($name, $value = '', $option = null)
+    {
+        if (is_array($name)) {
+            // 初始化
+            Cookie::init($name);
+        } elseif (is_null($name)) {
+            // 清除
+            Cookie::clear($value);
+        } elseif ('' === $value) {
+            // 获取
+            return 0 === strpos($name, '?') ? Cookie::has(substr($name, 1), $option) : Cookie::get($name, $option);
+        } elseif (is_null($value)) {
+            // 删除
+            return Cookie::delete($name);
+        } else {
+            // 设置
+            return Cookie::set($name, $value, $option);
+        }
+    }
+}
+
+if (!function_exists('cache')) {
+    /**
+     * 缓存管理
+     * @param mixed     $name 缓存名称,如果为数组表示进行缓存设置
+     * @param mixed     $value 缓存值
+     * @param mixed     $options 缓存参数
+     * @param string    $tag 缓存标签
+     * @return mixed
+     */
+    function cache($name, $value = '', $options = null, $tag = null)
+    {
+        if (is_array($options)) {
+            // 缓存操作的同时初始化
+            $cache = Cache::connect($options);
+        } elseif (is_array($name)) {
+            // 缓存初始化
+            return Cache::connect($name);
+        } else {
+            $cache = Cache::init();
+        }
+
+        if (is_null($name)) {
+            return $cache->clear($value);
+        } elseif ('' === $value) {
+            // 获取缓存
+            return 0 === strpos($name, '?') ? $cache->has(substr($name, 1)) : $cache->get($name);
+        } elseif (is_null($value)) {
+            // 删除缓存
+            return $cache->rm($name);
+        } elseif (0 === strpos($name, '?') && '' !== $value) {
+            $expire = is_numeric($options) ? $options : null;
+            return $cache->remember(substr($name, 1), $value, $expire);
+        } else {
+            // 缓存数据
+            if (is_array($options)) {
+                $expire = isset($options['expire']) ? $options['expire'] : null; //修复查询缓存无法设置过期时间
+            } else {
+                $expire = is_numeric($options) ? $options : null; //默认快捷缓存设置过期时间
+            }
+            if (is_null($tag)) {
+                return $cache->set($name, $value, $expire);
+            } else {
+                return $cache->tag($tag)->set($name, $value, $expire);
+            }
+        }
+    }
+}
+
+if (!function_exists('trace')) {
+    /**
+     * 记录日志信息
+     * @param mixed     $log log信息 支持字符串和数组
+     * @param string    $level 日志级别
+     * @return void|array
+     */
+    function trace($log = '[think]', $level = 'log')
+    {
+        if ('[think]' === $log) {
+            return Log::getLog();
+        } else {
+            Log::record($log, $level);
+        }
+    }
+}
+
+if (!function_exists('request')) {
+    /**
+     * 获取当前Request对象实例
+     * @return Request
+     */
+    function request()
+    {
+        return Request::instance();
+    }
+}
+
+if (!function_exists('response')) {
+    /**
+     * 创建普通 Response 对象实例
+     * @param mixed      $data   输出数据
+     * @param int|string $code   状态码
+     * @param array      $header 头信息
+     * @param string     $type
+     * @return Response
+     */
+    function response($data = [], $code = 200, $header = [], $type = 'html')
+    {
+        return Response::create($data, $type, $code, $header);
+    }
+}
+
+if (!function_exists('view')) {
+    /**
+     * 渲染模板输出
+     * @param string    $template 模板文件
+     * @param array     $vars 模板变量
+     * @param array     $replace 模板替换
+     * @param integer   $code 状态码
+     * @return \think\response\View
+     */
+    function view($template = '', $vars = [], $replace = [], $code = 200)
+    {
+        return Response::create($template, 'view', $code)->replace($replace)->assign($vars);
+    }
+}
+
+if (!function_exists('json')) {
+    /**
+     * 获取\think\response\Json对象实例
+     * @param mixed   $data 返回的数据
+     * @param integer $code 状态码
+     * @param array   $header 头部
+     * @param array   $options 参数
+     * @return \think\response\Json
+     */
+    function json($data = [], $code = 200, $header = [], $options = [])
+    {
+        return Response::create($data, 'json', $code, $header, $options);
+    }
+}
+
+if (!function_exists('jsonp')) {
+    /**
+     * 获取\think\response\Jsonp对象实例
+     * @param mixed   $data    返回的数据
+     * @param integer $code    状态码
+     * @param array   $header 头部
+     * @param array   $options 参数
+     * @return \think\response\Jsonp
+     */
+    function jsonp($data = [], $code = 200, $header = [], $options = [])
+    {
+        return Response::create($data, 'jsonp', $code, $header, $options);
+    }
+}
+
+if (!function_exists('xml')) {
+    /**
+     * 获取\think\response\Xml对象实例
+     * @param mixed   $data    返回的数据
+     * @param integer $code    状态码
+     * @param array   $header  头部
+     * @param array   $options 参数
+     * @return \think\response\Xml
+     */
+    function xml($data = [], $code = 200, $header = [], $options = [])
+    {
+        return Response::create($data, 'xml', $code, $header, $options);
+    }
+}
+
+if (!function_exists('redirect')) {
+    /**
+     * 获取\think\response\Redirect对象实例
+     * @param mixed         $url 重定向地址 支持Url::build方法的地址
+     * @param array|integer $params 额外参数
+     * @param integer       $code 状态码
+     * @param array         $with 隐式传参
+     * @return \think\response\Redirect
+     */
+    function redirect($url = [], $params = [], $code = 302, $with = [])
+    {
+        if (is_integer($params)) {
+            $code   = $params;
+            $params = [];
+        }
+        return Response::create($url, 'redirect', $code)->params($params)->with($with);
+    }
+}
+
+if (!function_exists('abort')) {
+    /**
+     * 抛出HTTP异常
+     * @param integer|Response      $code 状态码 或者 Response对象实例
+     * @param string                $message 错误信息
+     * @param array                 $header 参数
+     */
+    function abort($code, $message = null, $header = [])
+    {
+        if ($code instanceof Response) {
+            throw new HttpResponseException($code);
+        } else {
+            throw new HttpException($code, $message, null, $header);
+        }
+    }
+}
+
+if (!function_exists('halt')) {
+    /**
+     * 调试变量并且中断输出
+     * @param mixed      $var 调试变量或者信息
+     */
+    function halt($var)
+    {
+        dump($var);
+        throw new HttpResponseException(new Response);
+    }
+}
+
+if (!function_exists('token')) {
+    /**
+     * 生成表单令牌
+     * @param string $name 令牌名称
+     * @param mixed  $type 令牌生成方法
+     * @return string
+     */
+    function token($name = '__token__', $type = 'md5')
+    {
+        $token = Request::instance()->token($name, $type);
+        return '<input type="hidden" name="' . $name . '" value="' . $token . '" />';
+    }
+}
+
+if (!function_exists('load_relation')) {
+    /**
+     * 延迟预载入关联查询
+     * @param mixed $resultSet 数据集
+     * @param mixed $relation 关联
+     * @return array
+     */
+    function load_relation($resultSet, $relation)
+    {
+        $item = current($resultSet);
+        if ($item instanceof Model) {
+            $item->eagerlyResultSet($resultSet, $relation);
+        }
+        return $resultSet;
+    }
+}
+
+if (!function_exists('collection')) {
+    /**
+     * 数组转换为数据集对象
+     * @param array $resultSet 数据集数组
+     * @return \think\model\Collection|\think\Collection
+     */
+    function collection($resultSet)
+    {
+        $item = current($resultSet);
+        if ($item instanceof Model) {
+            return \think\model\Collection::make($resultSet);
+        } else {
+            return \think\Collection::make($resultSet);
+        }
+    }
+}

+ 136 - 0
thinkphp/lang/zh-cn.php

@@ -0,0 +1,136 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+// 核心中文语言包
+return [
+    // 系统错误提示
+    'Undefined variable'                                        => '未定义变量',
+    'Undefined index'                                           => '未定义数组索引',
+    'Undefined offset'                                          => '未定义数组下标',
+    'Parse error'                                               => '语法解析错误',
+    'Type error'                                                => '类型错误',
+    'Fatal error'                                               => '致命错误',
+    'syntax error'                                              => '语法错误',
+
+    // 框架核心错误提示
+    'dispatch type not support'                                 => '不支持的调度类型',
+    'method param miss'                                         => '方法参数错误',
+    'method not exists'                                         => '方法不存在',
+    'module not exists'                                         => '模块不存在',
+    'controller not exists'                                     => '控制器不存在',
+    'class not exists'                                          => '类不存在',
+    'property not exists'                                       => '类的属性不存在',
+    'template not exists'                                       => '模板文件不存在',
+    'illegal controller name'                                   => '非法的控制器名称',
+    'illegal action name'                                       => '非法的操作名称',
+    'url suffix deny'                                           => '禁止的URL后缀访问',
+    'Route Not Found'                                           => '当前访问路由未定义',
+    'Undefined db type'                                         => '未定义数据库类型',
+    'variable type error'                                       => '变量类型错误',
+    'PSR-4 error'                                               => 'PSR-4 规范错误',
+    'not support total'                                         => '简洁模式下不能获取数据总数',
+    'not support last'                                          => '简洁模式下不能获取最后一页',
+    'error session handler'                                     => '错误的SESSION处理器类',
+    'not allow php tag'                                         => '模板不允许使用PHP语法',
+    'not support'                                               => '不支持',
+    'redisd master'                                             => 'Redisd 主服务器错误',
+    'redisd slave'                                              => 'Redisd 从服务器错误',
+    'must run at sae'                                           => '必须在SAE运行',
+    'memcache init error'                                       => '未开通Memcache服务,请在SAE管理平台初始化Memcache服务',
+    'KVDB init error'                                           => '没有初始化KVDB,请在SAE管理平台初始化KVDB服务',
+    'fields not exists'                                         => '数据表字段不存在',
+    'where express error'                                       => '查询表达式错误',
+    'not support data'                                          => '不支持的数据表达式',
+    'no data to update'                                         => '没有任何数据需要更新',
+    'miss data to insert'                                       => '缺少需要写入的数据',
+    'miss complex primary data'                                 => '缺少复合主键数据',
+    'miss update condition'                                     => '缺少更新条件',
+    'model data Not Found'                                      => '模型数据不存在',
+    'table data not Found'                                      => '表数据不存在',
+    'delete without condition'                                  => '没有条件不会执行删除操作',
+    'miss relation data'                                        => '缺少关联表数据',
+    'tag attr must'                                             => '模板标签属性必须',
+    'tag error'                                                 => '模板标签错误',
+    'cache write error'                                         => '缓存写入失败',
+    'sae mc write error'                                        => 'SAE mc 写入错误',
+    'route name not exists'                                     => '路由标识不存在(或参数不够)',
+    'invalid request'                                           => '非法请求',
+    'bind attr has exists'                                      => '模型的属性已经存在',
+    'relation data not exists'                                  => '关联数据不存在',
+    'relation not support'                                      => '关联不支持',
+    'chunk not support order'                                   => 'Chunk不支持调用order方法',
+    'closure not support cache(true)'                           => '使用闭包查询不支持cache(true),请指定缓存Key',
+
+    // 上传错误信息
+    'unknown upload error'                                      => '未知上传错误!',
+    'file write error'                                          => '文件写入失败!',
+    'upload temp dir not found'                                 => '找不到临时文件夹!',
+    'no file to uploaded'                                       => '没有文件被上传!',
+    'only the portion of file is uploaded'                      => '文件只有部分被上传!',
+    'upload File size exceeds the maximum value'                => '上传文件大小超过了最大值!',
+    'upload write error'                                        => '文件上传保存错误!',
+    'has the same filename: {:filename}'                        => '存在同名文件:{:filename}',
+    'upload illegal files'                                      => '非法上传文件',
+    'illegal image files'                                       => '非法图片文件',
+    'extensions to upload is not allowed'                       => '上传文件后缀不允许',
+    'mimetype to upload is not allowed'                         => '上传文件MIME类型不允许!',
+    'filesize not match'                                        => '上传文件大小不符!',
+    'directory {:path} creation failed'                         => '目录 {:path} 创建失败!',
+
+    // Validate Error Message
+    ':attribute require'                                        => ':attribute不能为空',
+    ':attribute must be numeric'                                => ':attribute必须是数字',
+    ':attribute must be integer'                                => ':attribute必须是整数',
+    ':attribute must be float'                                  => ':attribute必须是浮点数',
+    ':attribute must be bool'                                   => ':attribute必须是布尔值',
+    ':attribute not a valid email address'                      => ':attribute格式不符',
+    ':attribute not a valid mobile'                             => ':attribute格式不符',
+    ':attribute must be a array'                                => ':attribute必须是数组',
+    ':attribute must be yes,on or 1'                            => ':attribute必须是yes、on或者1',
+    ':attribute not a valid datetime'                           => ':attribute不是一个有效的日期或时间格式',
+    ':attribute not a valid file'                               => ':attribute不是有效的上传文件',
+    ':attribute not a valid image'                              => ':attribute不是有效的图像文件',
+    ':attribute must be alpha'                                  => ':attribute只能是字母',
+    ':attribute must be alpha-numeric'                          => ':attribute只能是字母和数字',
+    ':attribute must be alpha-numeric, dash, underscore'        => ':attribute只能是字母、数字和下划线_及破折号-',
+    ':attribute not a valid domain or ip'                       => ':attribute不是有效的域名或者IP',
+    ':attribute must be chinese'                                => ':attribute只能是汉字',
+    ':attribute must be chinese or alpha'                       => ':attribute只能是汉字、字母',
+    ':attribute must be chinese,alpha-numeric'                  => ':attribute只能是汉字、字母和数字',
+    ':attribute must be chinese,alpha-numeric,underscore, dash' => ':attribute只能是汉字、字母、数字和下划线_及破折号-',
+    ':attribute not a valid url'                                => ':attribute不是有效的URL地址',
+    ':attribute not a valid ip'                                 => ':attribute不是有效的IP地址',
+    ':attribute must be dateFormat of :rule'                    => ':attribute必须使用日期格式 :rule',
+    ':attribute must be in :rule'                               => ':attribute必须在 :rule 范围内',
+    ':attribute be notin :rule'                                 => ':attribute不能在 :rule 范围内',
+    ':attribute must between :1 - :2'                           => ':attribute只能在 :1 - :2 之间',
+    ':attribute not between :1 - :2'                            => ':attribute不能在 :1 - :2 之间',
+    'size of :attribute must be :rule'                          => ':attribute长度不符合要求 :rule',
+    'max size of :attribute must be :rule'                      => ':attribute长度不能超过 :rule',
+    'min size of :attribute must be :rule'                      => ':attribute长度不能小于 :rule',
+    ':attribute cannot be less than :rule'                      => ':attribute日期不能小于 :rule',
+    ':attribute cannot exceed :rule'                            => ':attribute日期不能超过 :rule',
+    ':attribute not within :rule'                               => '不在有效期内 :rule',
+    'access IP is not allowed'                                  => '不允许的IP访问',
+    'access IP denied'                                          => '禁止的IP访问',
+    ':attribute out of accord with :2'                          => ':attribute和确认字段:2不一致',
+    ':attribute cannot be same with :2'                         => ':attribute和比较字段:2不能相同',
+    ':attribute must greater than or equal :rule'               => ':attribute必须大于等于 :rule',
+    ':attribute must greater than :rule'                        => ':attribute必须大于 :rule',
+    ':attribute must less than or equal :rule'                  => ':attribute必须小于等于 :rule',
+    ':attribute must less than :rule'                           => ':attribute必须小于 :rule',
+    ':attribute must equal :rule'                               => ':attribute必须等于 :rule',
+    ':attribute has exists'                                     => ':attribute已存在',
+    ':attribute not conform to the rules'                       => ':attribute不符合指定规则',
+    'invalid Request method'                                    => '无效的请求类型',
+    'invalid token'                                             => '令牌数据无效',
+    'not conform to the rules'                                  => '规则错误',
+];

+ 672 - 0
thinkphp/library/think/App.php

@@ -0,0 +1,672 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\exception\ClassNotFoundException;
+use think\exception\HttpException;
+use think\exception\HttpResponseException;
+use think\exception\RouteNotFoundException;
+
+/**
+ * App 应用管理
+ * @author liu21st <liu21st@gmail.com>
+ */
+class App
+{
+    /**
+     * @var bool 是否初始化过
+     */
+    protected static $init = false;
+
+    /**
+     * @var string 当前模块路径
+     */
+    public static $modulePath;
+
+    /**
+     * @var bool 应用调试模式
+     */
+    public static $debug = true;
+
+    /**
+     * @var string 应用类库命名空间
+     */
+    public static $namespace = 'app';
+
+    /**
+     * @var bool 应用类库后缀
+     */
+    public static $suffix = false;
+
+    /**
+     * @var bool 应用路由检测
+     */
+    protected static $routeCheck;
+
+    /**
+     * @var bool 严格路由检测
+     */
+    protected static $routeMust;
+
+    /**
+     * @var array 请求调度分发
+     */
+    protected static $dispatch;
+
+    /**
+     * @var array 额外加载文件
+     */
+    protected static $file = [];
+
+    /**
+     * 执行应用程序
+     * @access public
+     * @param  Request $request 请求对象
+     * @return Response
+     * @throws Exception
+     */
+    public static function run(Request $request = null)
+    {
+        $request = is_null($request) ? Request::instance() : $request;
+
+        try {
+            $config = self::initCommon();
+
+            // 模块/控制器绑定
+            if (defined('BIND_MODULE')) {
+                BIND_MODULE && Route::bind(BIND_MODULE);
+            } elseif ($config['auto_bind_module']) {
+                // 入口自动绑定
+                $name = pathinfo($request->baseFile(), PATHINFO_FILENAME);
+                if ($name && 'index' != $name && is_dir(APP_PATH . $name)) {
+                    Route::bind($name);
+                }
+            }
+
+            $request->filter($config['default_filter']);
+
+            // 默认语言
+            Lang::range($config['default_lang']);
+            // 开启多语言机制 检测当前语言
+            $config['lang_switch_on'] && Lang::detect();
+            $request->langset(Lang::range());
+
+            // 加载系统语言包
+            Lang::load([
+                THINK_PATH . 'lang' . DS . $request->langset() . EXT,
+                APP_PATH . 'lang' . DS . $request->langset() . EXT,
+            ]);
+
+            // 监听 app_dispatch
+            Hook::listen('app_dispatch', self::$dispatch);
+            // 获取应用调度信息
+            $dispatch = self::$dispatch;
+
+            // 未设置调度信息则进行 URL 路由检测
+            if (empty($dispatch)) {
+                $dispatch = self::routeCheck($request, $config);
+            }
+
+            // 记录当前调度信息
+            $request->dispatch($dispatch);
+
+            // 记录路由和请求信息
+            if (self::$debug) {
+                Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
+                Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
+                Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
+            }
+
+            // 监听 app_begin
+            Hook::listen('app_begin', $dispatch);
+
+            // 请求缓存检查
+            $request->cache(
+                $config['request_cache'],
+                $config['request_cache_expire'],
+                $config['request_cache_except']
+            );
+
+            $data = self::exec($dispatch, $config);
+        } catch (HttpResponseException $exception) {
+            $data = $exception->getResponse();
+        }
+
+        // 清空类的实例化
+        Loader::clearInstance();
+
+        // 输出数据到客户端
+        if ($data instanceof Response) {
+            $response = $data;
+        } elseif (!is_null($data)) {
+            // 默认自动识别响应输出类型
+            $type = $request->isAjax() ?
+            Config::get('default_ajax_return') :
+            Config::get('default_return_type');
+
+            $response = Response::create($data, $type);
+        } else {
+            $response = Response::create();
+        }
+
+        // 监听 app_end
+        Hook::listen('app_end', $response);
+
+        return $response;
+    }
+
+    /**
+     * 初始化应用,并返回配置信息
+     * @access public
+     * @return array
+     */
+    public static function initCommon()
+    {
+        if (empty(self::$init)) {
+            if (defined('APP_NAMESPACE')) {
+                self::$namespace = APP_NAMESPACE;
+            }
+
+            Loader::addNamespace(self::$namespace, APP_PATH);
+
+            // 初始化应用
+            $config       = self::init();
+            self::$suffix = $config['class_suffix'];
+
+            // 应用调试模式
+            self::$debug = Env::get('app_debug', Config::get('app_debug'));
+
+            if (!self::$debug) {
+                ini_set('display_errors', 'Off');
+            } elseif (!IS_CLI) {
+                // 重新申请一块比较大的 buffer
+                if (ob_get_level() > 0) {
+                    $output = ob_get_clean();
+                }
+
+                ob_start();
+
+                if (!empty($output)) {
+                    echo $output;
+                }
+
+            }
+
+            if (!empty($config['root_namespace'])) {
+                Loader::addNamespace($config['root_namespace']);
+            }
+
+            // 加载额外文件
+            if (!empty($config['extra_file_list'])) {
+                foreach ($config['extra_file_list'] as $file) {
+                    $file = strpos($file, '.') ? $file : APP_PATH . $file . EXT;
+                    if (is_file($file) && !isset(self::$file[$file])) {
+                        include $file;
+                        self::$file[$file] = true;
+                    }
+                }
+            }
+
+            // 设置系统时区
+            date_default_timezone_set($config['default_timezone']);
+
+            // 监听 app_init
+            Hook::listen('app_init');
+
+            self::$init = true;
+        }
+
+        return Config::get();
+    }
+
+    /**
+     * 初始化应用或模块
+     * @access public
+     * @param string $module 模块名
+     * @return array
+     */
+    private static function init($module = '')
+    {
+        // 定位模块目录
+        $module = $module ? $module . DS : '';
+
+        // 加载初始化文件
+        if (is_file(APP_PATH . $module . 'init' . EXT)) {
+            include APP_PATH . $module . 'init' . EXT;
+        } elseif (is_file(RUNTIME_PATH . $module . 'init' . EXT)) {
+            include RUNTIME_PATH . $module . 'init' . EXT;
+        } else {
+            // 加载模块配置
+            $config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT);
+
+            // 读取数据库配置文件
+            $filename = CONF_PATH . $module . 'database' . CONF_EXT;
+            Config::load($filename, 'database');
+
+            // 读取扩展配置文件
+            if (is_dir(CONF_PATH . $module . 'extra')) {
+                $dir   = CONF_PATH . $module . 'extra';
+                $files = scandir($dir);
+                foreach ($files as $file) {
+                    if ('.' . pathinfo($file, PATHINFO_EXTENSION) === CONF_EXT) {
+                        $filename = $dir . DS . $file;
+                        Config::load($filename, pathinfo($file, PATHINFO_FILENAME));
+                    }
+                }
+            }
+
+            // 加载应用状态配置
+            if ($config['app_status']) {
+                Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT);
+            }
+
+            // 加载行为扩展文件
+            if (is_file(CONF_PATH . $module . 'tags' . EXT)) {
+                Hook::import(include CONF_PATH . $module . 'tags' . EXT);
+            }
+
+            // 加载公共文件
+            $path = APP_PATH . $module;
+            if (is_file($path . 'common' . EXT)) {
+                include $path . 'common' . EXT;
+            }
+
+            // 加载当前模块语言包
+            if ($module) {
+                Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT);
+            }
+        }
+
+        return Config::get();
+    }
+
+    /**
+     * 设置当前请求的调度信息
+     * @access public
+     * @param array|string  $dispatch 调度信息
+     * @param string        $type     调度类型
+     * @return void
+     */
+    public static function dispatch($dispatch, $type = 'module')
+    {
+        self::$dispatch = ['type' => $type, $type => $dispatch];
+    }
+
+    /**
+     * 执行函数或者闭包方法 支持参数调用
+     * @access public
+     * @param string|array|\Closure $function 函数或者闭包
+     * @param array                 $vars     变量
+     * @return mixed
+     */
+    public static function invokeFunction($function, $vars = [])
+    {
+        $reflect = new \ReflectionFunction($function);
+        $args    = self::bindParams($reflect, $vars);
+
+        // 记录执行信息
+        self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');
+
+        return $reflect->invokeArgs($args);
+    }
+
+    /**
+     * 调用反射执行类的方法 支持参数绑定
+     * @access public
+     * @param string|array $method 方法
+     * @param array        $vars   变量
+     * @return mixed
+     */
+    public static function invokeMethod($method, $vars = [])
+    {
+        if (is_array($method)) {
+            $class   = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
+            $reflect = new \ReflectionMethod($class, $method[1]);
+        } else {
+            // 静态方法
+            $reflect = new \ReflectionMethod($method);
+        }
+
+        $args = self::bindParams($reflect, $vars);
+
+        self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');
+
+        return $reflect->invokeArgs(isset($class) ? $class : null, $args);
+    }
+
+    /**
+     * 调用反射执行类的实例化 支持依赖注入
+     * @access public
+     * @param string $class 类名
+     * @param array  $vars  变量
+     * @return mixed
+     */
+    public static function invokeClass($class, $vars = [])
+    {
+        $reflect     = new \ReflectionClass($class);
+        $constructor = $reflect->getConstructor();
+        $args        = $constructor ? self::bindParams($constructor, $vars) : [];
+
+        return $reflect->newInstanceArgs($args);
+    }
+
+    /**
+     * 绑定参数
+     * @access private
+     * @param \ReflectionMethod|\ReflectionFunction $reflect 反射类
+     * @param array                                 $vars    变量
+     * @return array
+     */
+    private static function bindParams($reflect, $vars = [])
+    {
+        // 自动获取请求变量
+        if (empty($vars)) {
+            $vars = Config::get('url_param_type') ?
+            Request::instance()->route() :
+            Request::instance()->param();
+        }
+
+        $args = [];
+        if ($reflect->getNumberOfParameters() > 0) {
+            // 判断数组类型 数字数组时按顺序绑定参数
+            reset($vars);
+            $type = key($vars) === 0 ? 1 : 0;
+
+            foreach ($reflect->getParameters() as $param) {
+                $args[] = self::getParamValue($param, $vars, $type);
+            }
+        }
+
+        return $args;
+    }
+
+    /**
+     * 获取参数值
+     * @access private
+     * @param \ReflectionParameter  $param 参数
+     * @param array                 $vars  变量
+     * @param string                $type  类别
+     * @return array
+     */
+    private static function getParamValue($param, &$vars, $type)
+    {
+        $name  = $param->getName();
+        $class = $param->getClass();
+
+        if ($class) {
+            $className = $class->getName();
+            $bind      = Request::instance()->$name;
+
+            if ($bind instanceof $className) {
+                $result = $bind;
+            } else {
+                if (method_exists($className, 'invoke')) {
+                    $method = new \ReflectionMethod($className, 'invoke');
+
+                    if ($method->isPublic() && $method->isStatic()) {
+                        return $className::invoke(Request::instance());
+                    }
+                }
+
+                $result = method_exists($className, 'instance') ?
+                $className::instance() :
+                new $className;
+            }
+        } elseif (1 == $type && !empty($vars)) {
+            $result = array_shift($vars);
+        } elseif (0 == $type && isset($vars[$name])) {
+            $result = $vars[$name];
+        } elseif ($param->isDefaultValueAvailable()) {
+            $result = $param->getDefaultValue();
+        } else {
+            throw new \InvalidArgumentException('method param miss:' . $name);
+        }
+
+        return $result;
+    }
+
+    /**
+     * 执行调用分发
+     * @access protected
+     * @param array $dispatch 调用信息
+     * @param array $config   配置信息
+     * @return Response|mixed
+     * @throws \InvalidArgumentException
+     */
+    protected static function exec($dispatch, $config)
+    {
+        switch ($dispatch['type']) {
+            case 'redirect': // 重定向跳转
+                $data = Response::create($dispatch['url'], 'redirect')
+                    ->code($dispatch['status']);
+                break;
+            case 'module': // 模块/控制器/操作
+                $data = self::module(
+                    $dispatch['module'],
+                    $config,
+                    isset($dispatch['convert']) ? $dispatch['convert'] : null
+                );
+                break;
+            case 'controller': // 执行控制器操作
+                $vars = array_merge(Request::instance()->param(), $dispatch['var']);
+                $data = Loader::action(
+                    $dispatch['controller'],
+                    $vars,
+                    $config['url_controller_layer'],
+                    $config['controller_suffix']
+                );
+                break;
+            case 'method': // 回调方法
+                $vars = array_merge(Request::instance()->param(), $dispatch['var']);
+                $data = self::invokeMethod($dispatch['method'], $vars);
+                break;
+            case 'function': // 闭包
+                $data = self::invokeFunction($dispatch['function']);
+                break;
+            case 'response': // Response 实例
+                $data = $dispatch['response'];
+                break;
+            default:
+                throw new \InvalidArgumentException('dispatch type not support');
+        }
+
+        return $data;
+    }
+
+    /**
+     * 执行模块
+     * @access public
+     * @param array $result  模块/控制器/操作
+     * @param array $config  配置参数
+     * @param bool  $convert 是否自动转换控制器和操作名
+     * @return mixed
+     * @throws HttpException
+     */
+    public static function module($result, $config, $convert = null)
+    {
+        if (is_string($result)) {
+            $result = explode('/', $result);
+        }
+
+        $request = Request::instance();
+
+        if ($config['app_multi_module']) {
+            // 多模块部署
+            $module    = strip_tags(strtolower($result[0] ?: $config['default_module']));
+            $bind      = Route::getBind('module');
+            $available = false;
+
+            if ($bind) {
+                // 绑定模块
+                list($bindModule) = explode('/', $bind);
+
+                if (empty($result[0])) {
+                    $module    = $bindModule;
+                    $available = true;
+                } elseif ($module == $bindModule) {
+                    $available = true;
+                }
+            } elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module)) {
+                $available = true;
+            }
+
+            // 模块初始化
+            if ($module && $available) {
+                // 初始化模块
+                $request->module($module);
+                $config = self::init($module);
+
+                // 模块请求缓存检查
+                $request->cache(
+                    $config['request_cache'],
+                    $config['request_cache_expire'],
+                    $config['request_cache_except']
+                );
+            } else {
+                throw new HttpException(404, 'module not exists:' . $module);
+            }
+        } else {
+            // 单一模块部署
+            $module = '';
+            $request->module($module);
+        }
+
+        // 设置默认过滤机制
+        $request->filter($config['default_filter']);
+
+        // 当前模块路径
+        App::$modulePath = APP_PATH . ($module ? $module . DS : '');
+
+        // 是否自动转换控制器和操作名
+        $convert = is_bool($convert) ? $convert : $config['url_convert'];
+
+        // 获取控制器名
+        $controller = strip_tags($result[1] ?: $config['default_controller']);
+        $controller = $convert ? strtolower($controller) : $controller;
+
+        // 获取操作名
+        $actionName = strip_tags($result[2] ?: $config['default_action']);
+        if (!empty($config['action_convert'])) {
+            $actionName = Loader::parseName($actionName, 1);
+        } else {
+            $actionName = $convert ? strtolower($actionName) : $actionName;
+        }
+
+        // 设置当前请求的控制器、操作
+        $request->controller(Loader::parseName($controller, 1))->action($actionName);
+
+        // 监听module_init
+        Hook::listen('module_init', $request);
+
+        try {
+            $instance = Loader::controller(
+                $controller,
+                $config['url_controller_layer'],
+                $config['controller_suffix'],
+                $config['empty_controller']
+            );
+        } catch (ClassNotFoundException $e) {
+            throw new HttpException(404, 'controller not exists:' . $e->getClass());
+        }
+
+        // 获取当前操作名
+        $action = $actionName . $config['action_suffix'];
+
+        $vars = [];
+        if (is_callable([$instance, $action])) {
+            // 执行操作方法
+            $call = [$instance, $action];
+            // 严格获取当前操作方法名
+            $reflect    = new \ReflectionMethod($instance, $action);
+            $methodName = $reflect->getName();
+            $suffix     = $config['action_suffix'];
+            $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
+            $request->action($actionName);
+
+        } elseif (is_callable([$instance, '_empty'])) {
+            // 空操作
+            $call = [$instance, '_empty'];
+            $vars = [$actionName];
+        } else {
+            // 操作不存在
+            throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
+        }
+
+        Hook::listen('action_begin', $call);
+
+        return self::invokeMethod($call, $vars);
+    }
+
+    /**
+     * URL路由检测(根据PATH_INFO)
+     * @access public
+     * @param  \think\Request $request 请求实例
+     * @param  array          $config  配置信息
+     * @return array
+     * @throws \think\Exception
+     */
+    public static function routeCheck($request, array $config)
+    {
+        $path   = $request->path();
+        $depr   = $config['pathinfo_depr'];
+        $result = false;
+
+        // 路由检测
+        $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
+        if ($check) {
+            // 开启路由
+            if (is_file(RUNTIME_PATH . 'route.php')) {
+                // 读取路由缓存
+                $rules = include RUNTIME_PATH . 'route.php';
+                is_array($rules) && Route::rules($rules);
+            } else {
+                $files = $config['route_config_file'];
+                foreach ($files as $file) {
+                    if (is_file(CONF_PATH . $file . CONF_EXT)) {
+                        // 导入路由配置
+                        $rules = include CONF_PATH . $file . CONF_EXT;
+                        is_array($rules) && Route::import($rules);
+                    }
+                }
+            }
+
+            // 路由检测(根据路由定义返回不同的URL调度)
+            $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
+            $must   = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];
+
+            if ($must && false === $result) {
+                // 路由无效
+                throw new RouteNotFoundException();
+            }
+        }
+
+        // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
+        if (false === $result) {
+            $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
+        }
+
+        return $result;
+    }
+
+    /**
+     * 设置应用的路由检测机制
+     * @access public
+     * @param  bool $route 是否需要检测路由
+     * @param  bool $must  是否强制检测路由
+     * @return void
+     */
+    public static function route($route, $must = false)
+    {
+        self::$routeCheck = $route;
+        self::$routeMust  = $must;
+    }
+}

+ 235 - 0
thinkphp/library/think/Build.php

@@ -0,0 +1,235 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+class Build
+{
+    /**
+     * 根据传入的 build 资料创建目录和文件
+     * @access public
+     * @param  array  $build     build 列表
+     * @param  string $namespace 应用类库命名空间
+     * @param  bool   $suffix    类库后缀
+     * @return void
+     * @throws Exception
+     */
+    public static function run(array $build = [], $namespace = 'app', $suffix = false)
+    {
+        // 锁定
+        $lock = APP_PATH . 'build.lock';
+
+        // 如果锁定文件不可写(不存在)则进行处理,否则表示已经有程序在处理了
+        if (!is_writable($lock)) {
+            if (!touch($lock)) {
+                throw new Exception(
+                    '应用目录[' . APP_PATH . ']不可写,目录无法自动生成!<BR>请手动生成项目目录~',
+                    10006
+                );
+            }
+
+            foreach ($build as $module => $list) {
+                if ('__dir__' == $module) {
+                    // 创建目录列表
+                    self::buildDir($list);
+                } elseif ('__file__' == $module) {
+                    // 创建文件列表
+                    self::buildFile($list);
+                } else {
+                    // 创建模块
+                    self::module($module, $list, $namespace, $suffix);
+                }
+            }
+
+            // 解除锁定
+            unlink($lock);
+        }
+    }
+
+    /**
+     * 创建目录
+     * @access protected
+     * @param  array $list 目录列表
+     * @return void
+     */
+    protected static function buildDir($list)
+    {
+        foreach ($list as $dir) {
+            // 目录不存在则创建目录
+            !is_dir(APP_PATH . $dir) && mkdir(APP_PATH . $dir, 0755, true);
+        }
+    }
+
+    /**
+     * 创建文件
+     * @access protected
+     * @param  array $list 文件列表
+     * @return void
+     */
+    protected static function buildFile($list)
+    {
+        foreach ($list as $file) {
+            // 先创建目录
+            if (!is_dir(APP_PATH . dirname($file))) {
+                mkdir(APP_PATH . dirname($file), 0755, true);
+            }
+
+            // 再创建文件
+            if (!is_file(APP_PATH . $file)) {
+                file_put_contents(
+                    APP_PATH . $file,
+                    'php' == pathinfo($file, PATHINFO_EXTENSION) ? "<?php\n" : ''
+                );
+            }
+        }
+    }
+
+    /**
+     * 创建模块
+     * @access public
+     * @param  string $module    模块名
+     * @param  array  $list      build 列表
+     * @param  string $namespace 应用类库命名空间
+     * @param  bool   $suffix    类库后缀
+     * @return void
+     */
+    public static function module($module = '', $list = [], $namespace = 'app', $suffix = false)
+    {
+        $module = $module ?: '';
+
+        // 创建模块目录
+        !is_dir(APP_PATH . $module) && mkdir(APP_PATH . $module);
+
+        // 如果不是 runtime 目录则需要创建配置文件和公共文件、创建模块的默认页面
+        if (basename(RUNTIME_PATH) != $module) {
+            self::buildCommon($module);
+            self::buildHello($module, $namespace, $suffix);
+        }
+
+        // 未指定文件和目录,则创建默认的模块目录和文件
+        if (empty($list)) {
+            $list = [
+                '__file__' => ['config.php', 'common.php'],
+                '__dir__'  => ['controller', 'model', 'view'],
+            ];
+        }
+
+        // 创建子目录和文件
+        foreach ($list as $path => $file) {
+            $modulePath = APP_PATH . $module . DS;
+
+            if ('__dir__' == $path) {
+                // 生成子目录
+                foreach ($file as $dir) {
+                    self::checkDirBuild($modulePath . $dir);
+                }
+            } elseif ('__file__' == $path) {
+                // 生成(空白)文件
+                foreach ($file as $name) {
+                    if (!is_file($modulePath . $name)) {
+                        file_put_contents(
+                            $modulePath . $name,
+                            'php' == pathinfo($name, PATHINFO_EXTENSION) ? "<?php\n" : ''
+                        );
+                    }
+                }
+            } else {
+                // 生成相关 MVC 文件
+                foreach ($file as $val) {
+                    $val      = trim($val);
+                    $filename = $modulePath . $path . DS . $val . ($suffix ? ucfirst($path) : '') . EXT;
+                    $space    = $namespace . '\\' . ($module ? $module . '\\' : '') . $path;
+                    $class    = $val . ($suffix ? ucfirst($path) : '');
+
+                    switch ($path) {
+                        case 'controller': // 控制器
+                            $content = "<?php\nnamespace {$space};\n\nclass {$class}\n{\n\n}";
+                            break;
+                        case 'model': // 模型
+                            $content = "<?php\nnamespace {$space};\n\nuse think\Model;\n\nclass {$class} extends Model\n{\n\n}";
+                            break;
+                        case 'view': // 视图
+                            $filename = $modulePath . $path . DS . $val . '.html';
+                            self::checkDirBuild(dirname($filename));
+                            $content = '';
+                            break;
+                        default:
+                            // 其他文件
+                            $content = "<?php\nnamespace {$space};\n\nclass {$class}\n{\n\n}";
+                    }
+
+                    if (!is_file($filename)) {
+                        file_put_contents($filename, $content);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 创建模块的欢迎页面
+     * @access protected
+     * @param  string $module    模块名
+     * @param  string $namespace 应用类库命名空间
+     * @param  bool   $suffix    类库后缀
+     * @return void
+     */
+    protected static function buildHello($module, $namespace, $suffix = false)
+    {
+        $filename = APP_PATH . ($module ? $module . DS : '') .
+            'controller' . DS . 'Index' .
+            ($suffix ? 'Controller' : '') . EXT;
+
+        if (!is_file($filename)) {
+            $module = $module ? $module . '\\' : '';
+            $suffix = $suffix ? 'Controller' : '';
+            $content = str_replace(
+                ['{$app}', '{$module}', '{layer}', '{$suffix}'],
+                [$namespace, $module, 'controller', $suffix],
+                file_get_contents(THINK_PATH . 'tpl' . DS . 'default_index.tpl')
+            );
+
+            self::checkDirBuild(dirname($filename));
+            file_put_contents($filename, $content);
+        }
+    }
+
+    /**
+     * 创建模块的公共文件
+     * @access protected
+     * @param  string $module 模块名
+     * @return void
+     */
+    protected static function buildCommon($module)
+    {
+        $config = CONF_PATH . ($module ? $module . DS : '') . 'config.php';
+
+        self::checkDirBuild(dirname($config));
+
+        if (!is_file($config)) {
+            file_put_contents($config, "<?php\n//配置文件\nreturn [\n\n];");
+        }
+
+        $common = APP_PATH . ($module ? $module . DS : '') . 'common.php';
+        if (!is_file($common)) file_put_contents($common, "<?php\n");
+    }
+
+    /**
+     * 创建目录
+     * @access protected
+     * @param  string $dirname 目录名称
+     * @return void
+     */
+    protected static function checkDirBuild($dirname)
+    {
+        !is_dir($dirname) && mkdir($dirname, 0755, true);
+    }
+}

+ 247 - 0
thinkphp/library/think/Cache.php

@@ -0,0 +1,247 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\cache\Driver;
+
+class Cache
+{
+    /**
+     * @var array 缓存的实例
+     */
+    public static $instance = [];
+
+    /**
+     * @var int 缓存读取次数
+     */
+    public static $readTimes = 0;
+
+    /**
+     * @var int 缓存写入次数
+     */
+    public static $writeTimes = 0;
+
+    /**
+     * @var object 操作句柄
+     */
+    public static $handler;
+
+    /**
+     * 连接缓存驱动
+     * @access public
+     * @param  array       $options 配置数组
+     * @param  bool|string $name    缓存连接标识 true 强制重新连接
+     * @return Driver
+     */
+    public static function connect(array $options = [], $name = false)
+    {
+        $type = !empty($options['type']) ? $options['type'] : 'File';
+
+        if (false === $name) {
+            $name = md5(serialize($options));
+        }
+
+        if (true === $name || !isset(self::$instance[$name])) {
+            $class = false === strpos($type, '\\') ?
+            '\\think\\cache\\driver\\' . ucwords($type) :
+            $type;
+
+            // 记录初始化信息
+            App::$debug && Log::record('[ CACHE ] INIT ' . $type, 'info');
+
+            if (true === $name) {
+                return new $class($options);
+            }
+
+            self::$instance[$name] = new $class($options);
+        }
+
+        return self::$instance[$name];
+    }
+
+    /**
+     * 自动初始化缓存
+     * @access public
+     * @param  array $options 配置数组
+     * @return Driver
+     */
+    public static function init(array $options = [])
+    {
+        if (is_null(self::$handler)) {
+            if (empty($options) && 'complex' == Config::get('cache.type')) {
+                $default = Config::get('cache.default');
+                // 获取默认缓存配置,并连接
+                $options = Config::get('cache.' . $default['type']) ?: $default;
+            } elseif (empty($options)) {
+                $options = Config::get('cache');
+            }
+
+            self::$handler = self::connect($options);
+        }
+
+        return self::$handler;
+    }
+
+    /**
+     * 切换缓存类型 需要配置 cache.type 为 complex
+     * @access public
+     * @param  string $name 缓存标识
+     * @return Driver
+     */
+    public static function store($name = '')
+    {
+        if ('' !== $name && 'complex' == Config::get('cache.type')) {
+            return self::connect(Config::get('cache.' . $name), strtolower($name));
+        }
+
+        return self::init();
+    }
+
+    /**
+     * 判断缓存是否存在
+     * @access public
+     * @param  string $name 缓存变量名
+     * @return bool
+     */
+    public static function has($name)
+    {
+        self::$readTimes++;
+
+        return self::init()->has($name);
+    }
+
+    /**
+     * 读取缓存
+     * @access public
+     * @param  string $name    缓存标识
+     * @param  mixed  $default 默认值
+     * @return mixed
+     */
+    public static function get($name, $default = false)
+    {
+        self::$readTimes++;
+
+        return self::init()->get($name, $default);
+    }
+
+    /**
+     * 写入缓存
+     * @access public
+     * @param  string   $name   缓存标识
+     * @param  mixed    $value  存储数据
+     * @param  int|null $expire 有效时间 0为永久
+     * @return boolean
+     */
+    public static function set($name, $value, $expire = null)
+    {
+        self::$writeTimes++;
+
+        return self::init()->set($name, $value, $expire);
+    }
+
+    /**
+     * 自增缓存(针对数值缓存)
+     * @access public
+     * @param  string $name 缓存变量名
+     * @param  int    $step 步长
+     * @return false|int
+     */
+    public static function inc($name, $step = 1)
+    {
+        self::$writeTimes++;
+
+        return self::init()->inc($name, $step);
+    }
+
+    /**
+     * 自减缓存(针对数值缓存)
+     * @access public
+     * @param  string $name 缓存变量名
+     * @param  int    $step 步长
+     * @return false|int
+     */
+    public static function dec($name, $step = 1)
+    {
+        self::$writeTimes++;
+
+        return self::init()->dec($name, $step);
+    }
+
+    /**
+     * 删除缓存
+     * @access public
+     * @param  string $name 缓存标识
+     * @return boolean
+     */
+    public static function rm($name)
+    {
+        self::$writeTimes++;
+
+        return self::init()->rm($name);
+    }
+
+    /**
+     * 清除缓存
+     * @access public
+     * @param  string $tag 标签名
+     * @return boolean
+     */
+    public static function clear($tag = null)
+    {
+        self::$writeTimes++;
+
+        return self::init()->clear($tag);
+    }
+
+    /**
+     * 读取缓存并删除
+     * @access public
+     * @param  string $name 缓存变量名
+     * @return mixed
+     */
+    public static function pull($name)
+    {
+        self::$readTimes++;
+        self::$writeTimes++;
+
+        return self::init()->pull($name);
+    }
+
+    /**
+     * 如果不存在则写入缓存
+     * @access public
+     * @param  string $name   缓存变量名
+     * @param  mixed  $value  存储数据
+     * @param  int    $expire 有效时间 0为永久
+     * @return mixed
+     */
+    public static function remember($name, $value, $expire = null)
+    {
+        self::$readTimes++;
+
+        return self::init()->remember($name, $value, $expire);
+    }
+
+    /**
+     * 缓存标签
+     * @access public
+     * @param  string       $name    标签名
+     * @param  string|array $keys    缓存标识
+     * @param  bool         $overlay 是否覆盖
+     * @return Driver
+     */
+    public static function tag($name, $keys = null, $overlay = false)
+    {
+        return self::init()->tag($name, $keys, $overlay);
+    }
+
+}

+ 457 - 0
thinkphp/library/think/Collection.php

@@ -0,0 +1,457 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: zhangyajun <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use ArrayAccess;
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
+use JsonSerializable;
+
+class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
+{
+    /**
+     * @var array 数据
+     */
+    protected $items = [];
+
+    /**
+     * Collection constructor.
+     * @access public
+     * @param  array $items 数据
+     */
+    public function __construct($items = [])
+    {
+        $this->items = $this->convertToArray($items);
+    }
+
+    /**
+     * 创建 Collection 实例
+     * @access public
+     * @param  array $items 数据
+     * @return static
+     */
+    public static function make($items = [])
+    {
+        return new static($items);
+    }
+
+    /**
+     * 判断数据是否为空
+     * @access public
+     * @return bool
+     */
+    public function isEmpty()
+    {
+        return empty($this->items);
+    }
+
+    /**
+     * 将数据转成数组
+     * @access public
+     * @return array
+     */
+    public function toArray()
+    {
+        return array_map(function ($value) {
+            return ($value instanceof Model || $value instanceof self) ?
+                $value->toArray() :
+                $value;
+        }, $this->items);
+    }
+
+    /**
+     * 获取全部的数据
+     * @access public
+     * @return array
+     */
+    public function all()
+    {
+        return $this->items;
+    }
+
+    /**
+     * 交换数组中的键和值
+     * @access public
+     * @return static
+     */
+    public function flip()
+    {
+        return new static(array_flip($this->items));
+    }
+
+    /**
+     * 返回数组中所有的键名组成的新 Collection 实例
+     * @access public
+     * @return static
+     */
+    public function keys()
+    {
+        return new static(array_keys($this->items));
+    }
+
+    /**
+     * 合并数组并返回一个新的 Collection 实例
+     * @access public
+     * @param  mixed $items 新的数据
+     * @return static
+     */
+    public function merge($items)
+    {
+        return new static(array_merge($this->items, $this->convertToArray($items)));
+    }
+
+    /**
+     * 比较数组,返回差集生成的新 Collection 实例
+     * @access public
+     * @param  mixed $items 做比较的数据
+     * @return static
+     */
+    public function diff($items)
+    {
+        return new static(array_diff($this->items, $this->convertToArray($items)));
+    }
+
+    /**
+     * 比较数组,返回交集组成的 Collection 新实例
+     * @access public
+     * @param  mixed $items 比较数据
+     * @return static
+     */
+    public function intersect($items)
+    {
+        return new static(array_intersect($this->items, $this->convertToArray($items)));
+    }
+
+    /**
+     * 返回并删除数据中的的最后一个元素(出栈)
+     * @access public
+     * @return mixed
+     */
+    public function pop()
+    {
+        return array_pop($this->items);
+    }
+
+    /**
+     * 返回并删除数据中首个元素
+     * @access public
+     * @return mixed
+     */
+    public function shift()
+    {
+        return array_shift($this->items);
+    }
+
+    /**
+     * 在数组开头插入一个元素
+     * @access public
+     * @param mixed $value 值
+     * @param mixed $key   键名
+     * @return void
+     */
+    public function unshift($value, $key = null)
+    {
+        if (is_null($key)) {
+            array_unshift($this->items, $value);
+        } else {
+            $this->items = [$key => $value] + $this->items;
+        }
+    }
+
+    /**
+     * 在数组结尾插入一个元素
+     * @access public
+     * @param  mixed $value 值
+     * @param  mixed $key   键名
+     * @return void
+     */
+    public function push($value, $key = null)
+    {
+        if (is_null($key)) {
+            $this->items[] = $value;
+        } else {
+            $this->items[$key] = $value;
+        }
+    }
+
+    /**
+     * 通过使用用户自定义函数,以字符串返回数组
+     * @access public
+     * @param  callable $callback 回调函数
+     * @param  mixed    $initial  初始值
+     * @return mixed
+     */
+    public function reduce(callable $callback, $initial = null)
+    {
+        return array_reduce($this->items, $callback, $initial);
+    }
+
+    /**
+     * 以相反的顺序创建一个新的 Collection 实例
+     * @access public
+     * @return static
+     */
+    public function reverse()
+    {
+        return new static(array_reverse($this->items));
+    }
+
+    /**
+     * 把数据分割为新的数组块
+     * @access public
+     * @param  int  $size         分隔长度
+     * @param  bool $preserveKeys 是否保持原数据索引
+     * @return static
+     */
+    public function chunk($size, $preserveKeys = false)
+    {
+        $chunks = [];
+
+        foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) {
+            $chunks[] = new static($chunk);
+        }
+
+        return new static($chunks);
+    }
+
+    /**
+     * 给数据中的每个元素执行回调
+     * @access public
+     * @param  callable $callback 回调函数
+     * @return $this
+     */
+    public function each(callable $callback)
+    {
+        foreach ($this->items as $key => $item) {
+            $result = $callback($item, $key);
+
+            if (false === $result) {
+                break;
+            }
+
+            if (!is_object($item)) {
+                $this->items[$key] = $result;
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * 用回调函数过滤数据中的元素
+     * @access public
+     * @param callable|null $callback 回调函数
+     * @return static
+     */
+    public function filter(callable $callback = null)
+    {
+        return new static(array_filter($this->items, $callback ?: null));
+    }
+
+    /**
+     * 返回数据中指定的一列
+     * @access public
+     * @param mixed $columnKey 键名
+     * @param null  $indexKey  作为索引值的列
+     * @return array
+     */
+    public function column($columnKey, $indexKey = null)
+    {
+        if (function_exists('array_column')) {
+            return array_column($this->items, $columnKey, $indexKey);
+        }
+
+        $result = [];
+        foreach ($this->items as $row) {
+            $key    = $value    = null;
+            $keySet = $valueSet = false;
+
+            if (null !== $indexKey && array_key_exists($indexKey, $row)) {
+                $key    = (string) $row[$indexKey];
+                $keySet = true;
+            }
+
+            if (null === $columnKey) {
+                $valueSet = true;
+                $value    = $row;
+            } elseif (is_array($row) && array_key_exists($columnKey, $row)) {
+                $valueSet = true;
+                $value    = $row[$columnKey];
+            }
+
+            if ($valueSet) {
+                if ($keySet) {
+                    $result[$key] = $value;
+                } else {
+                    $result[] = $value;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * 对数据排序,并返回排序后的数据组成的新 Collection 实例
+     * @access public
+     * @param  callable|null $callback 回调函数
+     * @return static
+     */
+    public function sort(callable $callback = null)
+    {
+        $items = $this->items;
+        $callback = $callback ?: function ($a, $b) {
+            return $a == $b ? 0 : (($a < $b) ? -1 : 1);
+        };
+
+        uasort($items, $callback);
+        return new static($items);
+    }
+
+    /**
+     * 将数据打乱后组成新的 Collection 实例
+     * @access public
+     * @return static
+     */
+    public function shuffle()
+    {
+        $items = $this->items;
+
+        shuffle($items);
+        return new static($items);
+    }
+
+    /**
+     * 截取数据并返回新的 Collection 实例
+     * @access public
+     * @param  int  $offset       起始位置
+     * @param  int  $length       截取长度
+     * @param  bool $preserveKeys 是否保持原先的键名
+     * @return static
+     */
+    public function slice($offset, $length = null, $preserveKeys = false)
+    {
+        return new static(array_slice($this->items, $offset, $length, $preserveKeys));
+    }
+
+    /**
+     * 指定的键是否存在
+     * @access public
+     * @param  mixed $offset 键名
+     * @return bool
+     */
+    public function offsetExists($offset)
+    {
+        return array_key_exists($offset, $this->items);
+    }
+
+    /**
+     * 获取指定键对应的值
+     * @access public
+     * @param  mixed $offset 键名
+     * @return mixed
+     */
+    public function offsetGet($offset)
+    {
+        return $this->items[$offset];
+    }
+
+    /**
+     * 设置键值
+     * @access public
+     * @param  mixed $offset 键名
+     * @param  mixed $value  值
+     * @return void
+     */
+    public function offsetSet($offset, $value)
+    {
+        if (is_null($offset)) {
+            $this->items[] = $value;
+        } else {
+            $this->items[$offset] = $value;
+        }
+    }
+
+    /**
+     * 删除指定键值
+     * @access public
+     * @param  mixed $offset 键名
+     * @return void
+     */
+    public function offsetUnset($offset)
+    {
+        unset($this->items[$offset]);
+    }
+
+    /**
+     * 统计数据的个数
+     * @access public
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->items);
+    }
+
+    /**
+     * 获取数据的迭代器
+     * @access public
+     * @return ArrayIterator
+     */
+    public function getIterator()
+    {
+        return new ArrayIterator($this->items);
+    }
+
+    /**
+     * 将数据反序列化成数组
+     * @access public
+     * @return array
+     */
+    public function jsonSerialize()
+    {
+        return $this->toArray();
+    }
+
+    /**
+     * 转换当前数据集为 JSON 字符串
+     * @access public
+     * @param  integer $options json 参数
+     * @return string
+     */
+    public function toJson($options = JSON_UNESCAPED_UNICODE)
+    {
+        return json_encode($this->toArray(), $options);
+    }
+
+    /**
+     * 将数据转换成字符串
+     * @access public
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->toJson();
+    }
+
+    /**
+     * 将数据转换成数组
+     * @access protected
+     * @param  mixed $items 数据
+     * @return array
+     */
+    protected function convertToArray($items)
+    {
+        return $items instanceof self ? $items->all() : (array) $items;
+    }
+}

+ 214 - 0
thinkphp/library/think/Config.php

@@ -0,0 +1,214 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+class Config
+{
+    /**
+     * @var array 配置参数
+     */
+    private static $config = [];
+
+    /**
+     * @var string 参数作用域
+     */
+    private static $range = '_sys_';
+
+    /**
+     * 设定配置参数的作用域
+     * @access public
+     * @param  string $range 作用域
+     * @return void
+     */
+    public static function range($range)
+    {
+        self::$range = $range;
+
+        if (!isset(self::$config[$range])) self::$config[$range] = [];
+    }
+
+    /**
+     * 解析配置文件或内容
+     * @access public
+     * @param  string $config 配置文件路径或内容
+     * @param  string $type   配置解析类型
+     * @param  string $name   配置名(如设置即表示二级配置)
+     * @param  string $range  作用域
+     * @return mixed
+     */
+    public static function parse($config, $type = '', $name = '', $range = '')
+    {
+        $range = $range ?: self::$range;
+
+        if (empty($type)) $type = pathinfo($config, PATHINFO_EXTENSION);
+
+        $class = false !== strpos($type, '\\') ?
+            $type :
+            '\\think\\config\\driver\\' . ucwords($type);
+
+        return self::set((new $class())->parse($config), $name, $range);
+    }
+
+    /**
+     * 加载配置文件(PHP格式)
+     * @access public
+     * @param  string $file  配置文件名
+     * @param  string $name  配置名(如设置即表示二级配置)
+     * @param  string $range 作用域
+     * @return mixed
+     */
+    public static function load($file, $name = '', $range = '')
+    {
+        $range = $range ?: self::$range;
+
+        if (!isset(self::$config[$range])) self::$config[$range] = [];
+
+        if (is_file($file)) {
+            $name = strtolower($name);
+            $type = pathinfo($file, PATHINFO_EXTENSION);
+
+            if ('php' == $type) {
+                return self::set(include $file, $name, $range);
+            }
+
+            if ('yaml' == $type && function_exists('yaml_parse_file')) {
+                return self::set(yaml_parse_file($file), $name, $range);
+            }
+
+            return self::parse($file, $type, $name, $range);
+        }
+
+        return self::$config[$range];
+    }
+
+    /**
+     * 检测配置是否存在
+     * @access public
+     * @param  string $name 配置参数名(支持二级配置 . 号分割)
+     * @param  string $range  作用域
+     * @return bool
+     */
+    public static function has($name, $range = '')
+    {
+        $range = $range ?: self::$range;
+
+        if (!strpos($name, '.')) {
+            return isset(self::$config[$range][strtolower($name)]);
+        }
+
+        // 二维数组设置和获取支持
+        $name = explode('.', $name, 2);
+        return isset(self::$config[$range][strtolower($name[0])][$name[1]]);
+    }
+
+    /**
+     * 获取配置参数 为空则获取所有配置
+     * @access public
+     * @param  string $name 配置参数名(支持二级配置 . 号分割)
+     * @param  string $range  作用域
+     * @return mixed
+     */
+    public static function get($name = null, $range = '')
+    {
+        $range = $range ?: self::$range;
+
+        // 无参数时获取所有
+        if (empty($name) && isset(self::$config[$range])) {
+            return self::$config[$range];
+        }
+
+        // 非二级配置时直接返回
+        if (!strpos($name, '.')) {
+            $name = strtolower($name);
+            return isset(self::$config[$range][$name]) ? self::$config[$range][$name] : null;
+        }
+
+        // 二维数组设置和获取支持
+        $name    = explode('.', $name, 2);
+        $name[0] = strtolower($name[0]);
+
+        if (!isset(self::$config[$range][$name[0]])) {
+            // 动态载入额外配置
+            $module = Request::instance()->module();
+            $file   = CONF_PATH . ($module ? $module . DS : '') . 'extra' . DS . $name[0] . CONF_EXT;
+
+            is_file($file) && self::load($file, $name[0]);
+        }
+
+        return isset(self::$config[$range][$name[0]][$name[1]]) ?
+            self::$config[$range][$name[0]][$name[1]] :
+            null;
+    }
+
+    /**
+     * 设置配置参数 name 为数组则为批量设置
+     * @access public
+     * @param  string|array $name  配置参数名(支持二级配置 . 号分割)
+     * @param  mixed        $value 配置值
+     * @param  string       $range 作用域
+     * @return mixed
+     */
+    public static function set($name, $value = null, $range = '')
+    {
+        $range = $range ?: self::$range;
+
+        if (!isset(self::$config[$range])) self::$config[$range] = [];
+
+        // 字符串则表示单个配置设置
+        if (is_string($name)) {
+            if (!strpos($name, '.')) {
+                self::$config[$range][strtolower($name)] = $value;
+            } else {
+                // 二维数组
+                $name = explode('.', $name, 2);
+                self::$config[$range][strtolower($name[0])][$name[1]] = $value;
+            }
+
+            return $value;
+        }
+
+        // 数组则表示批量设置
+        if (is_array($name)) {
+            if (!empty($value)) {
+                self::$config[$range][$value] = isset(self::$config[$range][$value]) ?
+                    array_merge(self::$config[$range][$value], $name) :
+                    $name;
+
+                return self::$config[$range][$value];
+            }
+
+            return self::$config[$range] = array_merge(
+                self::$config[$range], array_change_key_case($name)
+            );
+        }
+
+        // 为空直接返回已有配置
+        return self::$config[$range];
+    }
+
+    /**
+     * 重置配置参数
+     * @access public
+     * @param  string $range 作用域
+     * @return void
+     */
+    public static function reset($range = '')
+    {
+        $range = $range ?: self::$range;
+
+        if (true === $range) {
+            self::$config = [];
+        } else {
+            self::$config[$range] = [];
+        }
+    }
+}

+ 863 - 0
thinkphp/library/think/Console.php

@@ -0,0 +1,863 @@
+<?php
+// +----------------------------------------------------------------------
+// | TopThink [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2015 http://www.topthink.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: zhangyajun <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\console\Command;
+use think\console\command\Help as HelpCommand;
+use think\console\Input;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Definition as InputDefinition;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+use think\console\output\driver\Buffer;
+
+class Console
+{
+    /**
+     * @var string 命令名称
+     */
+    private $name;
+
+    /**
+     * @var string 命令版本
+     */
+    private $version;
+
+    /**
+     * @var Command[] 命令
+     */
+    private $commands = [];
+
+    /**
+     * @var bool 是否需要帮助信息
+     */
+    private $wantHelps = false;
+
+    /**
+     * @var bool 是否捕获异常
+     */
+    private $catchExceptions = true;
+
+    /**
+     * @var bool 是否自动退出执行
+     */
+    private $autoExit = true;
+
+    /**
+     * @var InputDefinition 输入定义
+     */
+    private $definition;
+
+    /**
+     * @var string 默认执行的命令
+     */
+    private $defaultCommand;
+
+    /**
+     * @var array 默认提供的命令
+     */
+    private static $defaultCommands = [
+        "think\\console\\command\\Help",
+        "think\\console\\command\\Lists",
+        "think\\console\\command\\Build",
+        "think\\console\\command\\Clear",
+        "think\\console\\command\\make\\Controller",
+        "think\\console\\command\\make\\Model",
+        "think\\console\\command\\optimize\\Autoload",
+        "think\\console\\command\\optimize\\Config",
+        "think\\console\\command\\optimize\\Route",
+        "think\\console\\command\\optimize\\Schema",
+    ];
+
+    /**
+     * Console constructor.
+     * @access public
+     * @param  string     $name    名称
+     * @param  string     $version 版本
+     * @param null|string $user    执行用户
+     */
+    public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN', $user = null)
+    {
+        $this->name    = $name;
+        $this->version = $version;
+
+        if ($user) {
+            $this->setUser($user);
+        }
+
+        $this->defaultCommand = 'list';
+        $this->definition     = $this->getDefaultInputDefinition();
+
+        foreach ($this->getDefaultCommands() as $command) {
+            $this->add($command);
+        }
+    }
+
+    /**
+     * 设置执行用户
+     * @param $user
+     */
+    public function setUser($user)
+    {
+        $user = posix_getpwnam($user);
+        if ($user) {
+            posix_setuid($user['uid']);
+            posix_setgid($user['gid']);
+        }
+    }
+
+    /**
+     * 初始化 Console
+     * @access public
+     * @param  bool $run 是否运行 Console
+     * @return int|Console
+     */
+    public static function init($run = true)
+    {
+        static $console;
+
+        if (!$console) {
+            $config = Config::get('console');
+            // 实例化 console
+            $console = new self($config['name'], $config['version'], $config['user']);
+
+            // 读取指令集
+            if (is_file(CONF_PATH . 'command' . EXT)) {
+                $commands = include CONF_PATH . 'command' . EXT;
+
+                if (is_array($commands)) {
+                    foreach ($commands as $command) {
+                        class_exists($command) &&
+                        is_subclass_of($command, "\\think\\console\\Command") &&
+                        $console->add(new $command());  // 注册指令
+                    }
+                }
+            }
+        }
+
+        return $run ? $console->run() : $console;
+    }
+
+    /**
+     * 调用命令
+     * @access public
+     * @param  string $command
+     * @param  array  $parameters
+     * @param  string $driver
+     * @return Output
+     */
+    public static function call($command, array $parameters = [], $driver = 'buffer')
+    {
+        $console = self::init(false);
+
+        array_unshift($parameters, $command);
+
+        $input  = new Input($parameters);
+        $output = new Output($driver);
+
+        $console->setCatchExceptions(false);
+        $console->find($command)->run($input, $output);
+
+        return $output;
+    }
+
+    /**
+     * 执行当前的指令
+     * @access public
+     * @return int
+     * @throws \Exception
+     */
+    public function run()
+    {
+        $input  = new Input();
+        $output = new Output();
+
+        $this->configureIO($input, $output);
+
+        try {
+            $exitCode = $this->doRun($input, $output);
+        } catch (\Exception $e) {
+            if (!$this->catchExceptions) throw $e;
+
+            $output->renderException($e);
+
+            $exitCode = $e->getCode();
+
+            if (is_numeric($exitCode)) {
+                $exitCode = ((int) $exitCode) ?: 1;
+            } else {
+                $exitCode = 1;
+            }
+        }
+
+        if ($this->autoExit) {
+            if ($exitCode > 255) $exitCode = 255;
+
+            exit($exitCode);
+        }
+
+        return $exitCode;
+    }
+
+    /**
+     * 执行指令
+     * @access public
+     * @param  Input  $input  输入
+     * @param  Output $output 输出
+     * @return int
+     */
+    public function doRun(Input $input, Output $output)
+    {
+        // 获取版本信息
+        if (true === $input->hasParameterOption(['--version', '-V'])) {
+            $output->writeln($this->getLongVersion());
+
+            return 0;
+        }
+
+        $name = $this->getCommandName($input);
+
+        // 获取帮助信息
+        if (true === $input->hasParameterOption(['--help', '-h'])) {
+            if (!$name) {
+                $name  = 'help';
+                $input = new Input(['help']);
+            } else {
+                $this->wantHelps = true;
+            }
+        }
+
+        if (!$name) {
+            $name  = $this->defaultCommand;
+            $input = new Input([$this->defaultCommand]);
+        }
+
+        return $this->doRunCommand($this->find($name), $input, $output);
+    }
+
+    /**
+     * 设置输入参数定义
+     * @access public
+     * @param  InputDefinition $definition 输入定义
+     * @return $this;
+     */
+    public function setDefinition(InputDefinition $definition)
+    {
+        $this->definition = $definition;
+
+        return $this;
+    }
+
+    /**
+     * 获取输入参数定义
+     * @access public
+     * @return InputDefinition
+     */
+    public function getDefinition()
+    {
+        return $this->definition;
+    }
+
+    /**
+     * 获取帮助信息
+     * @access public
+     * @return string
+     */
+    public function getHelp()
+    {
+        return $this->getLongVersion();
+    }
+
+    /**
+     * 设置是否捕获异常
+     * @access public
+     * @param bool $boolean 是否捕获
+     * @return $this
+     */
+    public function setCatchExceptions($boolean)
+    {
+        $this->catchExceptions = (bool) $boolean;
+
+        return $this;
+    }
+
+    /**
+     * 设置是否自动退出
+     * @access public
+     * @param bool $boolean 是否自动退出
+     * @return $this
+     */
+    public function setAutoExit($boolean)
+    {
+        $this->autoExit = (bool) $boolean;
+
+        return $this;
+    }
+
+    /**
+     * 获取名称
+     * @access public
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * 设置名称
+     * @access public
+     * @param  string $name 名称
+     * @return $this
+     */
+    public function setName($name)
+    {
+        $this->name = $name;
+
+        return $this;
+    }
+
+    /**
+     * 获取版本
+     * @access public
+     * @return string
+     */
+    public function getVersion()
+    {
+        return $this->version;
+    }
+
+    /**
+     * 设置版本
+     * @access public
+     * @param  string $version 版本信息
+     * @return $this
+     */
+    public function setVersion($version)
+    {
+        $this->version = $version;
+
+        return $this;
+    }
+
+    /**
+     * 获取完整的版本号
+     * @access public
+     * @return string
+     */
+    public function getLongVersion()
+    {
+        if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) {
+            return sprintf(
+                '<info>%s</info> version <comment>%s</comment>',
+                $this->getName(),
+                $this->getVersion()
+            );
+        }
+
+        return '<info>Console Tool</info>';
+    }
+
+    /**
+     * 注册一个指令
+     * @access public
+     * @param string $name 指令名称
+     * @return Command
+     */
+    public function register($name)
+    {
+        return $this->add(new Command($name));
+    }
+
+    /**
+     * 批量添加指令
+     * @access public
+     * @param  Command[] $commands 指令实例
+     * @return $this
+     */
+    public function addCommands(array $commands)
+    {
+        foreach ($commands as $command) $this->add($command);
+
+        return $this;
+    }
+
+    /**
+     * 添加一个指令
+     * @access public
+     * @param  Command $command 命令实例
+     * @return Command|bool
+     */
+    public function add(Command $command)
+    {
+        if (!$command->isEnabled()) {
+            $command->setConsole(null);
+            return false;
+        }
+
+        $command->setConsole($this);
+
+        if (null === $command->getDefinition()) {
+            throw new \LogicException(
+                sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))
+            );
+        }
+
+        $this->commands[$command->getName()] = $command;
+
+        foreach ($command->getAliases() as $alias) {
+            $this->commands[$alias] = $command;
+        }
+
+        return $command;
+    }
+
+    /**
+     * 获取指令
+     * @access public
+     * @param  string $name 指令名称
+     * @return Command
+     * @throws \InvalidArgumentException
+     */
+    public function get($name)
+    {
+        if (!isset($this->commands[$name])) {
+            throw new \InvalidArgumentException(
+                sprintf('The command "%s" does not exist.', $name)
+            );
+        }
+
+        $command = $this->commands[$name];
+
+        if ($this->wantHelps) {
+            $this->wantHelps = false;
+
+            /** @var HelpCommand $helpCommand */
+            $helpCommand = $this->get('help');
+            $helpCommand->setCommand($command);
+
+            return $helpCommand;
+        }
+
+        return $command;
+    }
+
+    /**
+     * 某个指令是否存在
+     * @access public
+     * @param  string $name 指令名称
+     * @return bool
+     */
+    public function has($name)
+    {
+        return isset($this->commands[$name]);
+    }
+
+    /**
+     * 获取所有的命名空间
+     * @access public
+     * @return array
+     */
+    public function getNamespaces()
+    {
+        $namespaces = [];
+
+        foreach ($this->commands as $command) {
+            $namespaces = array_merge(
+                $namespaces, $this->extractAllNamespaces($command->getName())
+            );
+
+            foreach ($command->getAliases() as $alias) {
+                $namespaces = array_merge(
+                    $namespaces, $this->extractAllNamespaces($alias)
+                );
+            }
+        }
+
+        return array_values(array_unique(array_filter($namespaces)));
+    }
+
+    /**
+     * 查找注册命名空间中的名称或缩写
+     * @access public
+     * @param string $namespace
+     * @return string
+     * @throws \InvalidArgumentException
+     */
+    public function findNamespace($namespace)
+    {
+        $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
+            return preg_quote($matches[1]) . '[^:]*';
+        }, $namespace);
+
+        $allNamespaces = $this->getNamespaces();
+        $namespaces    = preg_grep('{^' . $expr . '}', $allNamespaces);
+
+        if (empty($namespaces)) {
+            $message = sprintf(
+                'There are no commands defined in the "%s" namespace.', $namespace
+            );
+
+            if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
+                if (1 == count($alternatives)) {
+                    $message .= "\n\nDid you mean this?\n    ";
+                } else {
+                    $message .= "\n\nDid you mean one of these?\n    ";
+                }
+
+                $message .= implode("\n    ", $alternatives);
+            }
+
+            throw new \InvalidArgumentException($message);
+        }
+
+        $exact = in_array($namespace, $namespaces, true);
+
+        if (count($namespaces) > 1 && !$exact) {
+            throw new \InvalidArgumentException(
+                sprintf(
+                    'The namespace "%s" is ambiguous (%s).',
+                    $namespace,
+                    $this->getAbbreviationSuggestions(array_values($namespaces)))
+            );
+        }
+
+        return $exact ? $namespace : reset($namespaces);
+    }
+
+    /**
+     * 查找指令
+     * @access public
+     * @param  string $name 名称或者别名
+     * @return Command
+     * @throws \InvalidArgumentException
+     */
+    public function find($name)
+    {
+        $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
+            return preg_quote($matches[1]) . '[^:]*';
+        }, $name);
+
+        $allCommands = array_keys($this->commands);
+        $commands    = preg_grep('{^' . $expr . '}', $allCommands);
+
+        if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) {
+            if (false !== ($pos = strrpos($name, ':'))) {
+                $this->findNamespace(substr($name, 0, $pos));
+            }
+
+            $message = sprintf('Command "%s" is not defined.', $name);
+
+            if ($alternatives = $this->findAlternatives($name, $allCommands)) {
+                if (1 == count($alternatives)) {
+                    $message .= "\n\nDid you mean this?\n    ";
+                } else {
+                    $message .= "\n\nDid you mean one of these?\n    ";
+                }
+                $message .= implode("\n    ", $alternatives);
+            }
+
+            throw new \InvalidArgumentException($message);
+        }
+
+        if (count($commands) > 1) {
+            $commandList = $this->commands;
+            $commands    = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) {
+                $commandName = $commandList[$nameOrAlias]->getName();
+
+                return $commandName === $nameOrAlias || !in_array($commandName, $commands);
+            });
+        }
+
+        $exact = in_array($name, $commands, true);
+        if (count($commands) > 1 && !$exact) {
+            $suggestions = $this->getAbbreviationSuggestions(array_values($commands));
+
+            throw new \InvalidArgumentException(
+                sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)
+            );
+        }
+
+        return $this->get($exact ? $name : reset($commands));
+    }
+
+    /**
+     * 获取所有的指令
+     * @access public
+     * @param  string $namespace 命名空间
+     * @return Command[]
+     */
+    public function all($namespace = null)
+    {
+        if (null === $namespace) return $this->commands;
+
+        $commands = [];
+
+        foreach ($this->commands as $name => $command) {
+            $ext = $this->extractNamespace($name, substr_count($namespace, ':') + 1);
+
+            if ($ext === $namespace) $commands[$name] = $command;
+        }
+
+        return $commands;
+    }
+
+    /**
+     * 获取可能的指令名
+     * @access public
+     * @param  array $names 指令名
+     * @return array
+     */
+    public static function getAbbreviations($names)
+    {
+        $abbrevs = [];
+        foreach ($names as $name) {
+            for ($len = strlen($name); $len > 0; --$len) {
+                $abbrev             = substr($name, 0, $len);
+                $abbrevs[$abbrev][] = $name;
+            }
+        }
+
+        return $abbrevs;
+    }
+
+    /**
+     * 配置基于用户的参数和选项的输入和输出实例
+     * @access protected
+     * @param  Input  $input  输入实例
+     * @param  Output $output 输出实例
+     * @return void
+     */
+    protected function configureIO(Input $input, Output $output)
+    {
+        if (true === $input->hasParameterOption(['--ansi'])) {
+            $output->setDecorated(true);
+        } elseif (true === $input->hasParameterOption(['--no-ansi'])) {
+            $output->setDecorated(false);
+        }
+
+        if (true === $input->hasParameterOption(['--no-interaction', '-n'])) {
+            $input->setInteractive(false);
+        }
+
+        if (true === $input->hasParameterOption(['--quiet', '-q'])) {
+            $output->setVerbosity(Output::VERBOSITY_QUIET);
+        } else {
+            if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) {
+                $output->setVerbosity(Output::VERBOSITY_DEBUG);
+            } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) {
+                $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE);
+            } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) {
+                $output->setVerbosity(Output::VERBOSITY_VERBOSE);
+            }
+        }
+    }
+
+    /**
+     * 执行指令
+     * @access protected
+     * @param  Command $command 指令实例
+     * @param  Input   $input   输入实例
+     * @param  Output  $output  输出实例
+     * @return int
+     * @throws \Exception
+     */
+    protected function doRunCommand(Command $command, Input $input, Output $output)
+    {
+        return $command->run($input, $output);
+    }
+
+    /**
+     * 获取指令的名称
+     * @access protected
+     * @param  Input $input 输入实例
+     * @return string
+     */
+    protected function getCommandName(Input $input)
+    {
+        return $input->getFirstArgument();
+    }
+
+    /**
+     * 获取默认输入定义
+     * @access protected
+     * @return InputDefinition
+     */
+    protected function getDefaultInputDefinition()
+    {
+        return new InputDefinition([
+            new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
+            new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
+            new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'),
+            new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
+            new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'),
+            new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
+            new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
+            new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
+        ]);
+    }
+
+    /**
+     * 获取默认命令
+     * @access protected
+     * @return Command[]
+     */
+    protected function getDefaultCommands()
+    {
+        $defaultCommands = [];
+
+        foreach (self::$defaultCommands as $class) {
+            if (class_exists($class) && is_subclass_of($class, "think\\console\\Command")) {
+                $defaultCommands[] = new $class();
+            }
+        }
+
+        return $defaultCommands;
+    }
+
+    /**
+     * 添加默认指令
+     * @access public
+     * @param  array $classes 指令
+     * @return void
+     */
+    public static function addDefaultCommands(array $classes)
+    {
+        self::$defaultCommands = array_merge(self::$defaultCommands, $classes);
+    }
+
+    /**
+     * 获取可能的建议
+     * @access private
+     * @param  array $abbrevs
+     * @return string
+     */
+    private function getAbbreviationSuggestions($abbrevs)
+    {
+        return sprintf(
+            '%s, %s%s',
+            $abbrevs[0],
+            $abbrevs[1],
+            count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''
+        );
+    }
+
+    /**
+     * 返回指令的命名空间部分
+     * @access public
+     * @param  string $name  指令名称
+     * @param  string $limit 部分的命名空间的最大数量
+     * @return string
+     */
+    public function extractNamespace($name, $limit = null)
+    {
+        $parts = explode(':', $name);
+        array_pop($parts);
+
+        return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit));
+    }
+
+    /**
+     * 查找可替代的建议
+     * @access private
+     * @param string             $name       指令名称
+     * @param array|\Traversable $collection 建议集合
+     * @return array
+     */
+    private function findAlternatives($name, $collection)
+    {
+        $threshold       = 1e3;
+        $alternatives    = [];
+        $collectionParts = [];
+
+        foreach ($collection as $item) {
+            $collectionParts[$item] = explode(':', $item);
+        }
+
+        foreach (explode(':', $name) as $i => $subname) {
+            foreach ($collectionParts as $collectionName => $parts) {
+                $exists = isset($alternatives[$collectionName]);
+
+                if (!isset($parts[$i]) && $exists) {
+                    $alternatives[$collectionName] += $threshold;
+                    continue;
+                } elseif (!isset($parts[$i])) {
+                    continue;
+                }
+
+                $lev = levenshtein($subname, $parts[$i]);
+
+                if ($lev <= strlen($subname) / 3 ||
+                    '' !== $subname &&
+                    false !== strpos($parts[$i], $subname)
+                ) {
+                    $alternatives[$collectionName] = $exists ?
+                        $alternatives[$collectionName] + $lev :
+                        $lev;
+                } elseif ($exists) {
+                    $alternatives[$collectionName] += $threshold;
+                }
+            }
+        }
+
+        foreach ($collection as $item) {
+            $lev = levenshtein($name, $item);
+
+            if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
+                $alternatives[$item] = isset($alternatives[$item]) ?
+                    $alternatives[$item] - $lev :
+                    $lev;
+            }
+        }
+
+        $alternatives = array_filter($alternatives, function ($lev) use ($threshold) {
+            return $lev < 2 * $threshold;
+        });
+
+        asort($alternatives);
+
+        return array_keys($alternatives);
+    }
+
+    /**
+     * 设置默认的指令
+     * @access public
+     * @param string $commandName 指令名称
+     * @return $this
+     */
+    public function setDefaultCommand($commandName)
+    {
+        $this->defaultCommand = $commandName;
+
+        return $this;
+    }
+
+    /**
+     * 返回所有的命名空间
+     * @access private
+     * @param  string $name 指令名称
+     * @return array
+     */
+    private function extractAllNamespaces($name)
+    {
+        $namespaces = [];
+
+        foreach (explode(':', $name, -1) as $part) {
+            if (count($namespaces)) {
+                $namespaces[] = end($namespaces) . ':' . $part;
+            } else {
+                $namespaces[] = $part;
+            }
+        }
+
+        return $namespaces;
+    }
+
+}

+ 229 - 0
thinkphp/library/think/Controller.php

@@ -0,0 +1,229 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\exception\ValidateException;
+use traits\controller\Jump;
+
+Loader::import('controller/Jump', TRAIT_PATH, EXT);
+
+class Controller
+{
+    use Jump;
+
+    /**
+     * @var \think\View 视图类实例
+     */
+    protected $view;
+
+    /**
+     * @var \think\Request Request 实例
+     */
+    protected $request;
+
+    /**
+     * @var bool 验证失败是否抛出异常
+     */
+    protected $failException = false;
+
+    /**
+     * @var bool 是否批量验证
+     */
+    protected $batchValidate = false;
+
+    /**
+     * @var array 前置操作方法列表
+     */
+    protected $beforeActionList = [];
+
+    /**
+     * 构造方法
+     * @access public
+     * @param Request $request Request 对象
+     */
+    public function __construct(Request $request = null)
+    {
+        $this->view    = View::instance(Config::get('template'), Config::get('view_replace_str'));
+        $this->request = is_null($request) ? Request::instance() : $request;
+
+        // 控制器初始化
+        $this->_initialize();
+
+        // 前置操作方法
+        if ($this->beforeActionList) {
+            foreach ($this->beforeActionList as $method => $options) {
+                is_numeric($method) ?
+                $this->beforeAction($options) :
+                $this->beforeAction($method, $options);
+            }
+        }
+    }
+
+    /**
+     * 初始化操作
+     * @access protected
+     */
+    protected function _initialize()
+    {
+    }
+
+    /**
+     * 前置操作
+     * @access protected
+     * @param  string $method  前置操作方法名
+     * @param  array  $options 调用参数 ['only'=>[...]] 或者 ['except'=>[...]]
+     * @return void
+     */
+    protected function beforeAction($method, $options = [])
+    {
+        if (isset($options['only'])) {
+            if (is_string($options['only'])) {
+                $options['only'] = explode(',', $options['only']);
+            }
+
+            if (!in_array($this->request->action(), $options['only'])) {
+                return;
+            }
+        } elseif (isset($options['except'])) {
+            if (is_string($options['except'])) {
+                $options['except'] = explode(',', $options['except']);
+            }
+
+            if (in_array($this->request->action(), $options['except'])) {
+                return;
+            }
+        }
+
+        call_user_func([$this, $method]);
+    }
+
+    /**
+     * 加载模板输出
+     * @access protected
+     * @param  string $template 模板文件名
+     * @param  array  $vars     模板输出变量
+     * @param  array  $replace  模板替换
+     * @param  array  $config   模板参数
+     * @return mixed
+     */
+    protected function fetch($template = '', $vars = [], $replace = [], $config = [])
+    {
+        return $this->view->fetch($template, $vars, $replace, $config);
+    }
+
+    /**
+     * 渲染内容输出
+     * @access protected
+     * @param  string $content 模板内容
+     * @param  array  $vars    模板输出变量
+     * @param  array  $replace 替换内容
+     * @param  array  $config  模板参数
+     * @return mixed
+     */
+    protected function display($content = '', $vars = [], $replace = [], $config = [])
+    {
+        return $this->view->display($content, $vars, $replace, $config);
+    }
+
+    /**
+     * 模板变量赋值
+     * @access protected
+     * @param  mixed $name  要显示的模板变量
+     * @param  mixed $value 变量的值
+     * @return $this
+     */
+    protected function assign($name, $value = '')
+    {
+        $this->view->assign($name, $value);
+
+        return $this;
+    }
+
+    /**
+     * 初始化模板引擎
+     * @access protected
+     * @param array|string $engine 引擎参数
+     * @return $this
+     */
+    protected function engine($engine)
+    {
+        $this->view->engine($engine);
+
+        return $this;
+    }
+
+    /**
+     * 设置验证失败后是否抛出异常
+     * @access protected
+     * @param bool $fail 是否抛出异常
+     * @return $this
+     */
+    protected function validateFailException($fail = true)
+    {
+        $this->failException = $fail;
+
+        return $this;
+    }
+
+    /**
+     * 验证数据
+     * @access protected
+     * @param  array        $data     数据
+     * @param  string|array $validate 验证器名或者验证规则数组
+     * @param  array        $message  提示信息
+     * @param  bool         $batch    是否批量验证
+     * @param  mixed        $callback 回调方法(闭包)
+     * @return array|string|true
+     * @throws ValidateException
+     */
+    protected function validate($data, $validate, $message = [], $batch = false, $callback = null)
+    {
+        if (is_array($validate)) {
+            $v = Loader::validate();
+            $v->rule($validate);
+        } else {
+            // 支持场景
+            if (strpos($validate, '.')) {
+                list($validate, $scene) = explode('.', $validate);
+            }
+
+            $v = Loader::validate($validate);
+
+            !empty($scene) && $v->scene($scene);
+        }
+
+        // 批量验证
+        if ($batch || $this->batchValidate) {
+            $v->batch(true);
+        }
+
+        // 设置错误信息
+        if (is_array($message)) {
+            $v->message($message);
+        }
+
+        // 使用回调验证
+        if ($callback && is_callable($callback)) {
+            call_user_func_array($callback, [$v, &$data]);
+        }
+
+        if (!$v->check($data)) {
+            if ($this->failException) {
+                throw new ValidateException($v->getError());
+            }
+
+            return $v->getError();
+        }
+
+        return true;
+    }
+}

+ 268 - 0
thinkphp/library/think/Cookie.php

@@ -0,0 +1,268 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+class Cookie
+{
+    /**
+     * @var array cookie 设置参数
+     */
+    protected static $config = [
+        'prefix'    => '', // cookie 名称前缀
+        'expire'    => 0, // cookie 保存时间
+        'path'      => '/', // cookie 保存路径
+        'domain'    => '', // cookie 有效域名
+        'secure'    => false, //  cookie 启用安全传输
+        'httponly'  => false, // httponly 设置
+        'setcookie' => true, // 是否使用 setcookie
+    ];
+
+    /**
+     * @var bool 是否完成初始化了
+     */
+    protected static $init;
+
+    /**
+     * Cookie初始化
+     * @access public
+     * @param  array $config 配置参数
+     * @return void
+     */
+    public static function init(array $config = [])
+    {
+        if (empty($config)) {
+            $config = Config::get('cookie');
+        }
+
+        self::$config = array_merge(self::$config, array_change_key_case($config));
+
+        if (!empty(self::$config['httponly'])) {
+            ini_set('session.cookie_httponly', 1);
+        }
+
+        self::$init = true;
+    }
+
+    /**
+     * 设置或者获取 cookie 作用域(前缀)
+     * @access public
+     * @param  string $prefix 前缀
+     * @return string|
+     */
+    public static function prefix($prefix = '')
+    {
+        if (empty($prefix)) {
+            return self::$config['prefix'];
+        }
+
+        return self::$config['prefix'] = $prefix;
+    }
+
+    /**
+     * Cookie 设置、获取、删除
+     * @access public
+     * @param  string $name   cookie 名称
+     * @param  mixed  $value  cookie 值
+     * @param  mixed  $option 可选参数 可能会是 null|integer|string
+     * @return void
+     */
+    public static function set($name, $value = '', $option = null)
+    {
+        !isset(self::$init) && self::init();
+
+        // 参数设置(会覆盖黙认设置)
+        if (!is_null($option)) {
+            if (is_numeric($option)) {
+                $option = ['expire' => $option];
+            } elseif (is_string($option)) {
+                parse_str($option, $option);
+            }
+
+            $config = array_merge(self::$config, array_change_key_case($option));
+        } else {
+            $config = self::$config;
+        }
+
+        $name = $config['prefix'] . $name;
+
+        // 设置 cookie
+        if (is_array($value)) {
+            array_walk_recursive($value, 'self::jsonFormatProtect', 'encode');
+            $value = 'think:' . json_encode($value);
+        }
+
+        $expire = !empty($config['expire']) ?
+        $_SERVER['REQUEST_TIME'] + intval($config['expire']) :
+        0;
+
+        if ($config['setcookie']) {
+            setcookie(
+                $name, $value, $expire, $config['path'], $config['domain'],
+                $config['secure'], $config['httponly']
+            );
+        }
+
+        $_COOKIE[$name] = $value;
+    }
+
+    /**
+     * 永久保存 Cookie 数据
+     * @access public
+     * @param  string $name   cookie 名称
+     * @param  mixed  $value  cookie 值
+     * @param  mixed  $option 可选参数 可能会是 null|integer|string
+     * @return void
+     */
+    public static function forever($name, $value = '', $option = null)
+    {
+        if (is_null($option) || is_numeric($option)) {
+            $option = [];
+        }
+
+        $option['expire'] = 315360000;
+
+        self::set($name, $value, $option);
+    }
+
+    /**
+     * 判断是否有 Cookie 数据
+     * @access public
+     * @param  string      $name   cookie 名称
+     * @param  string|null $prefix cookie 前缀
+     * @return bool
+     */
+    public static function has($name, $prefix = null)
+    {
+        !isset(self::$init) && self::init();
+
+        $prefix = !is_null($prefix) ? $prefix : self::$config['prefix'];
+
+        return isset($_COOKIE[$prefix . $name]);
+    }
+
+    /**
+     * 获取 Cookie 的值
+     * @access public
+     * @param string      $name   cookie 名称
+     * @param string|null $prefix cookie 前缀
+     * @return mixed
+     */
+    public static function get($name = '', $prefix = null)
+    {
+        !isset(self::$init) && self::init();
+
+        $prefix = !is_null($prefix) ? $prefix : self::$config['prefix'];
+        $key    = $prefix . $name;
+
+        if ('' == $name) {
+            // 获取全部
+            if ($prefix) {
+                $value = [];
+
+                foreach ($_COOKIE as $k => $val) {
+                    if (0 === strpos($k, $prefix)) {
+                        $value[$k] = $val;
+                    }
+
+                }
+            } else {
+                $value = $_COOKIE;
+            }
+        } elseif (isset($_COOKIE[$key])) {
+            $value = $_COOKIE[$key];
+
+            if (0 === strpos($value, 'think:')) {
+                $value = json_decode(substr($value, 6), true);
+                array_walk_recursive($value, 'self::jsonFormatProtect', 'decode');
+            }
+        } else {
+            $value = null;
+        }
+
+        return $value;
+    }
+
+    /**
+     * 删除 Cookie
+     * @access public
+     * @param  string      $name   cookie 名称
+     * @param  string|null $prefix cookie 前缀
+     * @return void
+     */
+    public static function delete($name, $prefix = null)
+    {
+        !isset(self::$init) && self::init();
+
+        $config = self::$config;
+        $prefix = !is_null($prefix) ? $prefix : $config['prefix'];
+        $name   = $prefix . $name;
+
+        if ($config['setcookie']) {
+            setcookie(
+                $name, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'],
+                $config['domain'], $config['secure'], $config['httponly']
+            );
+        }
+
+        // 删除指定 cookie
+        unset($_COOKIE[$name]);
+    }
+
+    /**
+     * 清除指定前缀的所有 cookie
+     * @access public
+     * @param  string|null $prefix cookie 前缀
+     * @return void
+     */
+    public static function clear($prefix = null)
+    {
+        if (empty($_COOKIE)) {
+            return;
+        }
+
+        !isset(self::$init) && self::init();
+
+        // 要删除的 cookie 前缀,不指定则删除 config 设置的指定前缀
+        $config = self::$config;
+        $prefix = !is_null($prefix) ? $prefix : $config['prefix'];
+
+        if ($prefix) {
+            foreach ($_COOKIE as $key => $val) {
+                if (0 === strpos($key, $prefix)) {
+                    if ($config['setcookie']) {
+                        setcookie(
+                            $key, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'],
+                            $config['domain'], $config['secure'], $config['httponly']
+                        );
+                    }
+
+                    unset($_COOKIE[$key]);
+                }
+            }
+        }
+    }
+
+    /**
+     * json 转换时的格式保护
+     * @access protected
+     * @param  mixed  $val  要转换的值
+     * @param  string $key  键名
+     * @param  string $type 转换类别
+     * @return void
+     */
+    protected static function jsonFormatProtect(&$val, $key, $type = 'encode')
+    {
+        if (!empty($val) && true !== $val) {
+            $val = 'decode' == $type ? urldecode($val) : urlencode($val);
+        }
+    }
+}

+ 180 - 0
thinkphp/library/think/Db.php

@@ -0,0 +1,180 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\db\Connection;
+use think\db\Query;
+
+/**
+ * Class Db
+ * @package think
+ * @method Query table(string $table) static 指定数据表(含前缀)
+ * @method Query name(string $name) static 指定数据表(不含前缀)
+ * @method Query where(mixed $field, string $op = null, mixed $condition = null) static 查询条件
+ * @method Query join(mixed $join, mixed $condition = null, string $type = 'INNER') static JOIN查询
+ * @method Query union(mixed $union, boolean $all = false) static UNION查询
+ * @method Query limit(mixed $offset, integer $length = null) static 查询LIMIT
+ * @method Query order(mixed $field, string $order = null) static 查询ORDER
+ * @method Query cache(mixed $key = null , integer $expire = null) static 设置查询缓存
+ * @method mixed value(string $field) static 获取某个字段的值
+ * @method array column(string $field, string $key = '') static 获取某个列的值
+ * @method Query view(mixed $join, mixed $field = null, mixed $on = null, string $type = 'INNER') static 视图查询
+ * @method mixed find(mixed $data = null) static 查询单个记录
+ * @method mixed select(mixed $data = null) static 查询多个记录
+ * @method integer insert(array $data, boolean $replace = false, boolean $getLastInsID = false, string $sequence = null) static 插入一条记录
+ * @method integer insertGetId(array $data, boolean $replace = false, string $sequence = null) static 插入一条记录并返回自增ID
+ * @method integer insertAll(array $dataSet) static 插入多条记录
+ * @method integer update(array $data) static 更新记录
+ * @method integer delete(mixed $data = null) static 删除记录
+ * @method boolean chunk(integer $count, callable $callback, string $column = null) static 分块获取数据
+ * @method mixed query(string $sql, array $bind = [], boolean $master = false, bool $pdo = false) static SQL查询
+ * @method integer execute(string $sql, array $bind = [], boolean $fetch = false, boolean $getLastInsID = false, string $sequence = null) static SQL执行
+ * @method Paginator paginate(integer $listRows = 15, mixed $simple = null, array $config = []) static 分页查询
+ * @method mixed transaction(callable $callback) static 执行数据库事务
+ * @method void startTrans() static 启动事务
+ * @method void commit() static 用于非自动提交状态下面的查询提交
+ * @method void rollback() static 事务回滚
+ * @method boolean batchQuery(array $sqlArray) static 批处理执行SQL语句
+ * @method string quote(string $str) static SQL指令安全过滤
+ * @method string getLastInsID($sequence = null) static 获取最近插入的ID
+ */
+class Db
+{
+    /**
+     * @var Connection[] 数据库连接实例
+     */
+    private static $instance = [];
+
+    /**
+     * @var int 查询次数
+     */
+    public static $queryTimes = 0;
+
+    /**
+     * @var int 执行次数
+     */
+    public static $executeTimes = 0;
+
+    /**
+     * 数据库初始化,并取得数据库类实例
+     * @access public
+     * @param  mixed       $config 连接配置
+     * @param  bool|string $name   连接标识 true 强制重新连接
+     * @return Connection
+     * @throws Exception
+     */
+    public static function connect($config = [], $name = false)
+    {
+        if (false === $name) {
+            $name = md5(serialize($config));
+        }
+
+        if (true === $name || !isset(self::$instance[$name])) {
+            // 解析连接参数 支持数组和字符串
+            $options = self::parseConfig($config);
+
+            if (empty($options['type'])) {
+                throw new \InvalidArgumentException('Undefined db type');
+            }
+
+            $class = false !== strpos($options['type'], '\\') ?
+            $options['type'] :
+            '\\think\\db\\connector\\' . ucwords($options['type']);
+
+            // 记录初始化信息
+            if (App::$debug) {
+                Log::record('[ DB ] INIT ' . $options['type'], 'info');
+            }
+
+            if (true === $name) {
+                $name = md5(serialize($config));
+            }
+
+            self::$instance[$name] = new $class($options);
+        }
+
+        return self::$instance[$name];
+    }
+
+    /**
+     * 清除连接实例
+     * @access public
+     * @return void
+     */
+    public static function clear()
+    {
+        self::$instance = [];
+    }
+
+    /**
+     * 数据库连接参数解析
+     * @access private
+     * @param  mixed $config 连接参数
+     * @return array
+     */
+    private static function parseConfig($config)
+    {
+        if (empty($config)) {
+            $config = Config::get('database');
+        } elseif (is_string($config) && false === strpos($config, '/')) {
+            $config = Config::get($config); // 支持读取配置参数
+        }
+
+        return is_string($config) ? self::parseDsn($config) : $config;
+    }
+
+    /**
+     * DSN 解析
+     * 格式: mysql://username:passwd@localhost:3306/DbName?param1=val1&param2=val2#utf8
+     * @access private
+     * @param  string $dsnStr 数据库 DSN 字符串解析
+     * @return array
+     */
+    private static function parseDsn($dsnStr)
+    {
+        $info = parse_url($dsnStr);
+
+        if (!$info) {
+            return [];
+        }
+
+        $dsn = [
+            'type'     => $info['scheme'],
+            'username' => isset($info['user']) ? $info['user'] : '',
+            'password' => isset($info['pass']) ? $info['pass'] : '',
+            'hostname' => isset($info['host']) ? $info['host'] : '',
+            'hostport' => isset($info['port']) ? $info['port'] : '',
+            'database' => !empty($info['path']) ? ltrim($info['path'], '/') : '',
+            'charset'  => isset($info['fragment']) ? $info['fragment'] : 'utf8',
+        ];
+
+        if (isset($info['query'])) {
+            parse_str($info['query'], $dsn['params']);
+        } else {
+            $dsn['params'] = [];
+        }
+
+        return $dsn;
+    }
+
+    /**
+     * 调用驱动类的方法
+     * @access public
+     * @param  string $method 方法名
+     * @param  array  $params 参数
+     * @return mixed
+     */
+    public static function __callStatic($method, $params)
+    {
+        return call_user_func_array([self::connect(), $method], $params);
+    }
+}

+ 252 - 0
thinkphp/library/think/Debug.php

@@ -0,0 +1,252 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\exception\ClassNotFoundException;
+use think\response\Redirect;
+
+class Debug
+{
+    /**
+     * @var array 区间时间信息
+     */
+    protected static $info = [];
+
+    /**
+     * @var array 区间内存信息
+     */
+    protected static $mem = [];
+
+    /**
+     * 记录时间(微秒)和内存使用情况
+     * @access public
+     * @param  string $name  标记位置
+     * @param  mixed  $value 标记值(留空则取当前 time 表示仅记录时间 否则同时记录时间和内存)
+     * @return void
+     */
+    public static function remark($name, $value = '')
+    {
+        self::$info[$name] = is_float($value) ? $value : microtime(true);
+
+        if ('time' != $value) {
+            self::$mem['mem'][$name]  = is_float($value) ? $value : memory_get_usage();
+            self::$mem['peak'][$name] = memory_get_peak_usage();
+        }
+    }
+
+    /**
+     * 统计某个区间的时间(微秒)使用情况 返回值以秒为单位
+     * @access public
+     * @param  string  $start 开始标签
+     * @param  string  $end   结束标签
+     * @param  integer $dec   小数位
+     * @return string
+     */
+    public static function getRangeTime($start, $end, $dec = 6)
+    {
+        if (!isset(self::$info[$end])) {
+            self::$info[$end] = microtime(true);
+        }
+
+        return number_format((self::$info[$end] - self::$info[$start]), $dec);
+    }
+
+    /**
+     * 统计从开始到统计时的时间(微秒)使用情况 返回值以秒为单位
+     * @access public
+     * @param  integer $dec 小数位
+     * @return string
+     */
+    public static function getUseTime($dec = 6)
+    {
+        return number_format((microtime(true) - THINK_START_TIME), $dec);
+    }
+
+    /**
+     * 获取当前访问的吞吐率情况
+     * @access public
+     * @return string
+     */
+    public static function getThroughputRate()
+    {
+        return number_format(1 / self::getUseTime(), 2) . 'req/s';
+    }
+
+    /**
+     * 记录区间的内存使用情况
+     * @access public
+     * @param  string  $start 开始标签
+     * @param  string  $end   结束标签
+     * @param  integer $dec   小数位
+     * @return string
+     */
+    public static function getRangeMem($start, $end, $dec = 2)
+    {
+        if (!isset(self::$mem['mem'][$end])) {
+            self::$mem['mem'][$end] = memory_get_usage();
+        }
+
+        $size = self::$mem['mem'][$end] - self::$mem['mem'][$start];
+        $a    = ['B', 'KB', 'MB', 'GB', 'TB'];
+        $pos  = 0;
+
+        while ($size >= 1024) {
+            $size /= 1024;
+            $pos++;
+        }
+
+        return round($size, $dec) . " " . $a[$pos];
+    }
+
+    /**
+     * 统计从开始到统计时的内存使用情况
+     * @access public
+     * @param  integer $dec 小数位
+     * @return string
+     */
+    public static function getUseMem($dec = 2)
+    {
+        $size = memory_get_usage() - THINK_START_MEM;
+        $a    = ['B', 'KB', 'MB', 'GB', 'TB'];
+        $pos  = 0;
+
+        while ($size >= 1024) {
+            $size /= 1024;
+            $pos++;
+        }
+
+        return round($size, $dec) . " " . $a[$pos];
+    }
+
+    /**
+     * 统计区间的内存峰值情况
+     * @access public
+     * @param  string  $start 开始标签
+     * @param  string  $end   结束标签
+     * @param  integer $dec   小数位
+     * @return string
+     */
+    public static function getMemPeak($start, $end, $dec = 2)
+    {
+        if (!isset(self::$mem['peak'][$end])) {
+            self::$mem['peak'][$end] = memory_get_peak_usage();
+        }
+
+        $size = self::$mem['peak'][$end] - self::$mem['peak'][$start];
+        $a    = ['B', 'KB', 'MB', 'GB', 'TB'];
+        $pos  = 0;
+
+        while ($size >= 1024) {
+            $size /= 1024;
+            $pos++;
+        }
+
+        return round($size, $dec) . " " . $a[$pos];
+    }
+
+    /**
+     * 获取文件加载信息
+     * @access public
+     * @param  bool $detail 是否显示详细
+     * @return integer|array
+     */
+    public static function getFile($detail = false)
+    {
+        $files = get_included_files();
+
+        if ($detail) {
+            $info = [];
+
+            foreach ($files as $file) {
+                $info[] = $file . ' ( ' . number_format(filesize($file) / 1024, 2) . ' KB )';
+            }
+
+            return $info;
+        }
+
+        return count($files);
+    }
+
+    /**
+     * 浏览器友好的变量输出
+     * @access public
+     * @param  mixed       $var   变量
+     * @param  boolean     $echo  是否输出(默认为 true,为 false 则返回输出字符串)
+     * @param  string|null $label 标签(默认为空)
+     * @param  integer     $flags htmlspecialchars 的标志
+     * @return null|string
+     */
+    public static function dump($var, $echo = true, $label = null, $flags = ENT_SUBSTITUTE)
+    {
+        $label = (null === $label) ? '' : rtrim($label) . ':';
+
+        ob_start();
+        var_dump($var);
+        $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', ob_get_clean());
+
+        if (IS_CLI) {
+            $output = PHP_EOL . $label . $output . PHP_EOL;
+        } else {
+            if (!extension_loaded('xdebug')) {
+                $output = htmlspecialchars($output, $flags);
+            }
+
+            $output = '<pre>' . $label . $output . '</pre>';
+        }
+
+        if ($echo) {
+            echo($output);
+            return;
+        }
+
+        return $output;
+    }
+
+    /**
+     * 调试信息注入到响应中
+     * @access public
+     * @param  Response $response 响应实例
+     * @param  string   $content  返回的字符串
+     * @return void
+     */
+    public static function inject(Response $response, &$content)
+    {
+        $config = Config::get('trace');
+        $type   = isset($config['type']) ? $config['type'] : 'Html';
+        $class  = false !== strpos($type, '\\') ? $type : '\\think\\debug\\' . ucwords($type);
+
+        unset($config['type']);
+
+        if (!class_exists($class)) {
+            throw new ClassNotFoundException('class not exists:' . $class, $class);
+        }
+
+        /** @var \think\debug\Console|\think\debug\Html $trace */
+        $trace = new $class($config);
+
+        if ($response instanceof Redirect) {
+            // TODO 记录
+        } else {
+            $output = $trace->output($response, Log::getLog());
+
+            if (is_string($output)) {
+                // trace 调试信息注入
+                $pos = strripos($content, '</body>');
+                if (false !== $pos) {
+                    $content = substr($content, 0, $pos) . $output . substr($content, $pos);
+                } else {
+                    $content = $content . $output;
+                }
+            }
+        }
+    }
+}

+ 39 - 0
thinkphp/library/think/Env.php

@@ -0,0 +1,39 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+class Env
+{
+    /**
+     * 获取环境变量值
+     * @access public
+     * @param  string $name    环境变量名(支持二级 . 号分割)
+     * @param  string $default 默认值
+     * @return mixed
+     */
+    public static function get($name, $default = null)
+    {
+        $result = getenv(ENV_PREFIX . strtoupper(str_replace('.', '_', $name)));
+
+        if (false !== $result) {
+            if ('false' === $result) {
+                $result = false;
+            } elseif ('true' === $result) {
+                $result = true;
+            }
+
+            return $result;
+        }
+
+        return $default;
+    }
+}

+ 136 - 0
thinkphp/library/think/Error.php

@@ -0,0 +1,136 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://zjzit.cn>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\console\Output as ConsoleOutput;
+use think\exception\ErrorException;
+use think\exception\Handle;
+use think\exception\ThrowableError;
+
+class Error
+{
+    /**
+     * 注册异常处理
+     * @access public
+     * @return void
+     */
+    public static function register()
+    {
+        error_reporting(E_ALL);
+        set_error_handler([__CLASS__, 'appError']);
+        set_exception_handler([__CLASS__, 'appException']);
+        register_shutdown_function([__CLASS__, 'appShutdown']);
+    }
+
+    /**
+     * 异常处理
+     * @access public
+     * @param  \Exception|\Throwable $e 异常
+     * @return void
+     */
+    public static function appException($e)
+    {
+        if (!$e instanceof \Exception) {
+            $e = new ThrowableError($e);
+        }
+
+        $handler = self::getExceptionHandler();
+        $handler->report($e);
+
+        if (IS_CLI) {
+            $handler->renderForConsole(new ConsoleOutput, $e);
+        } else {
+            $handler->render($e)->send();
+        }
+    }
+
+    /**
+     * 错误处理
+     * @access public
+     * @param  integer $errno      错误编号
+     * @param  integer $errstr     详细错误信息
+     * @param  string  $errfile    出错的文件
+     * @param  integer $errline    出错行号
+     * @return void
+     * @throws ErrorException
+     */
+    public static function appError($errno, $errstr, $errfile = '', $errline = 0)
+    {
+        $exception = new ErrorException($errno, $errstr, $errfile, $errline);
+
+        // 符合异常处理的则将错误信息托管至 think\exception\ErrorException
+        if (error_reporting() & $errno) {
+            throw $exception;
+        }
+
+        self::getExceptionHandler()->report($exception);
+    }
+
+    /**
+     * 异常中止处理
+     * @access public
+     * @return void
+     */
+    public static function appShutdown()
+    {
+        // 将错误信息托管至 think\ErrorException
+        if (!is_null($error = error_get_last()) && self::isFatal($error['type'])) {
+            self::appException(new ErrorException(
+                $error['type'], $error['message'], $error['file'], $error['line']
+            ));
+        }
+
+        // 写入日志
+        Log::save();
+    }
+
+    /**
+     * 确定错误类型是否致命
+     * @access protected
+     * @param  int $type 错误类型
+     * @return bool
+     */
+    protected static function isFatal($type)
+    {
+        return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]);
+    }
+
+    /**
+     * 获取异常处理的实例
+     * @access public
+     * @return Handle
+     */
+    public static function getExceptionHandler()
+    {
+        static $handle;
+
+        if (!$handle) {
+            // 异常处理 handle
+            $class = Config::get('exception_handle');
+
+            if ($class && is_string($class) && class_exists($class) &&
+                is_subclass_of($class, "\\think\\exception\\Handle")
+            ) {
+                $handle = new $class;
+            } else {
+                $handle = new Handle;
+
+                if ($class instanceof \Closure) {
+                    $handle->setRender($class);
+                }
+
+            }
+        }
+
+        return $handle;
+    }
+}

+ 55 - 0
thinkphp/library/think/Exception.php

@@ -0,0 +1,55 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://zjzit.cn>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+class Exception extends \Exception
+{
+    /**
+     * @var array 保存异常页面显示的额外 Debug 数据
+     */
+    protected $data = [];
+
+    /**
+     * 设置异常额外的 Debug 数据
+     * 数据将会显示为下面的格式
+     *
+     * Exception Data
+     * --------------------------------------------------
+     * Label 1
+     *   key1      value1
+     *   key2      value2
+     * Label 2
+     *   key1      value1
+     *   key2      value2
+     *
+     * @access protected
+     * @param  string $label 数据分类,用于异常页面显示
+     * @param  array  $data  需要显示的数据,必须为关联数组
+     * @return void
+     */
+    final protected function setData($label, array $data)
+    {
+        $this->data[$label] = $data;
+    }
+
+    /**
+     * 获取异常额外 Debug 数据
+     * 主要用于输出到异常页面便于调试
+     * @access public
+     * @return array
+     */
+    final public function getData()
+    {
+        return $this->data;
+    }
+
+}

+ 478 - 0
thinkphp/library/think/File.php

@@ -0,0 +1,478 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use SplFileObject;
+
+class File extends SplFileObject
+{
+    /**
+     * @var string 错误信息
+     */
+    private $error = '';
+
+    /**
+     * @var string 当前完整文件名
+     */
+    protected $filename;
+
+    /**
+     * @var string 上传文件名
+     */
+    protected $saveName;
+
+    /**
+     * @var string 文件上传命名规则
+     */
+    protected $rule = 'date';
+
+    /**
+     * @var array 文件上传验证规则
+     */
+    protected $validate = [];
+
+    /**
+     * @var bool 单元测试
+     */
+    protected $isTest;
+
+    /**
+     * @var array 上传文件信息
+     */
+    protected $info;
+
+    /**
+     * @var array 文件 hash 信息
+     */
+    protected $hash = [];
+
+    /**
+     * File constructor.
+     * @access public
+     * @param  string $filename 文件名称
+     * @param  string $mode     访问模式
+     */
+    public function __construct($filename, $mode = 'r')
+    {
+        parent::__construct($filename, $mode);
+        $this->filename = $this->getRealPath() ?: $this->getPathname();
+    }
+
+    /**
+     * 设置是否是单元测试
+     * @access public
+     * @param  bool $test 是否是测试
+     * @return $this
+     */
+    public function isTest($test = false)
+    {
+        $this->isTest = $test;
+
+        return $this;
+    }
+
+    /**
+     * 设置上传信息
+     * @access public
+     * @param  array $info 上传文件信息
+     * @return $this
+     */
+    public function setUploadInfo($info)
+    {
+        $this->info = $info;
+
+        return $this;
+    }
+
+    /**
+     * 获取上传文件的信息
+     * @access public
+     * @param  string $name 信息名称
+     * @return array|string
+     */
+    public function getInfo($name = '')
+    {
+        return isset($this->info[$name]) ? $this->info[$name] : $this->info;
+    }
+
+    /**
+     * 获取上传文件的文件名
+     * @access public
+     * @return string
+     */
+    public function getSaveName()
+    {
+        return $this->saveName;
+    }
+
+    /**
+     * 设置上传文件的保存文件名
+     * @access public
+     * @param  string $saveName 保存名称
+     * @return $this
+     */
+    public function setSaveName($saveName)
+    {
+        $this->saveName = $saveName;
+
+        return $this;
+    }
+
+    /**
+     * 获取文件的哈希散列值
+     * @access public
+     * @param  string $type 类型
+     * @return string
+     */
+    public function hash($type = 'sha1')
+    {
+        if (!isset($this->hash[$type])) {
+            $this->hash[$type] = hash_file($type, $this->filename);
+        }
+
+        return $this->hash[$type];
+    }
+
+    /**
+     * 检查目录是否可写
+     * @access protected
+     * @param  string $path 目录
+     * @return boolean
+     */
+    protected function checkPath($path)
+    {
+        if (is_dir($path) || mkdir($path, 0755, true)) {
+            return true;
+        }
+
+        $this->error = ['directory {:path} creation failed', ['path' => $path]];
+
+        return false;
+    }
+
+    /**
+     * 获取文件类型信息
+     * @access public
+     * @return string
+     */
+    public function getMime()
+    {
+        $finfo = finfo_open(FILEINFO_MIME_TYPE);
+
+        return finfo_file($finfo, $this->filename);
+    }
+
+    /**
+     * 设置文件的命名规则
+     * @access public
+     * @param  string $rule 文件命名规则
+     * @return $this
+     */
+    public function rule($rule)
+    {
+        $this->rule = $rule;
+
+        return $this;
+    }
+
+    /**
+     * 设置上传文件的验证规则
+     * @access public
+     * @param  array $rule 验证规则
+     * @return $this
+     */
+    public function validate(array $rule = [])
+    {
+        $this->validate = $rule;
+
+        return $this;
+    }
+
+    /**
+     * 检测是否合法的上传文件
+     * @access public
+     * @return bool
+     */
+    public function isValid()
+    {
+        return $this->isTest ? is_file($this->filename) : is_uploaded_file($this->filename);
+    }
+
+    /**
+     * 检测上传文件
+     * @access public
+     * @param  array $rule 验证规则
+     * @return bool
+     */
+    public function check($rule = [])
+    {
+        $rule = $rule ?: $this->validate;
+
+        /* 检查文件大小 */
+        if (isset($rule['size']) && !$this->checkSize($rule['size'])) {
+            $this->error = 'filesize not match';
+            return false;
+        }
+
+        /* 检查文件 Mime 类型 */
+        if (isset($rule['type']) && !$this->checkMime($rule['type'])) {
+            $this->error = 'mimetype to upload is not allowed';
+            return false;
+        }
+
+        /* 检查文件后缀 */
+        if (isset($rule['ext']) && !$this->checkExt($rule['ext'])) {
+            $this->error = 'extensions to upload is not allowed';
+            return false;
+        }
+
+        /* 检查图像文件 */
+        if (!$this->checkImg()) {
+            $this->error = 'illegal image files';
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 检测上传文件后缀
+     * @access public
+     * @param  array|string $ext 允许后缀
+     * @return bool
+     */
+    public function checkExt($ext)
+    {
+        if (is_string($ext)) {
+            $ext = explode(',', $ext);
+        }
+
+        $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION));
+
+        return in_array($extension, $ext);
+    }
+
+    /**
+     * 检测图像文件
+     * @access public
+     * @return bool
+     */
+    public function checkImg()
+    {
+        $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION));
+
+        // 如果上传的不是图片,或者是图片而且后缀确实符合图片类型则返回 true
+        return !in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) || in_array($this->getImageType($this->filename), [1, 2, 3, 4, 6, 13]);
+    }
+
+    /**
+     * 判断图像类型
+     * @access protected
+     * @param  string $image 图片名称
+     * @return bool|int
+     */
+    protected function getImageType($image)
+    {
+        if (function_exists('exif_imagetype')) {
+            return exif_imagetype($image);
+        }
+
+        try {
+            $info = getimagesize($image);
+            return $info ? $info[2] : false;
+        } catch (\Exception $e) {
+            return false;
+        }
+    }
+
+    /**
+     * 检测上传文件大小
+     * @access public
+     * @param  integer $size 最大大小
+     * @return bool
+     */
+    public function checkSize($size)
+    {
+        return $this->getSize() <= $size;
+    }
+
+    /**
+     * 检测上传文件类型
+     * @access public
+     * @param  array|string $mime 允许类型
+     * @return bool
+     */
+    public function checkMime($mime)
+    {
+        $mime = is_string($mime) ? explode(',', $mime) : $mime;
+
+        return in_array(strtolower($this->getMime()), $mime);
+    }
+
+    /**
+     * 移动文件
+     * @access public
+     * @param  string      $path     保存路径
+     * @param  string|bool $savename 保存的文件名 默认自动生成
+     * @param  boolean     $replace  同名文件是否覆盖
+     * @return false|File
+     */
+    public function move($path, $savename = true, $replace = true)
+    {
+        // 文件上传失败,捕获错误代码
+        if (!empty($this->info['error'])) {
+            $this->error($this->info['error']);
+            return false;
+        }
+
+        // 检测合法性
+        if (!$this->isValid()) {
+            $this->error = 'upload illegal files';
+            return false;
+        }
+
+        // 验证上传
+        if (!$this->check()) {
+            return false;
+        }
+
+        $path = rtrim($path, DS) . DS;
+        // 文件保存命名规则
+        $saveName = $this->buildSaveName($savename);
+        $filename = $path . $saveName;
+
+        // 检测目录
+        if (false === $this->checkPath(dirname($filename))) {
+            return false;
+        }
+
+        // 不覆盖同名文件
+        if (!$replace && is_file($filename)) {
+            $this->error = ['has the same filename: {:filename}', ['filename' => $filename]];
+            return false;
+        }
+
+        /* 移动文件 */
+        if ($this->isTest) {
+            rename($this->filename, $filename);
+        } elseif (!move_uploaded_file($this->filename, $filename)) {
+            $this->error = 'upload write error';
+            return false;
+        }
+
+        // 返回 File 对象实例
+        $file = new self($filename);
+        $file->setSaveName($saveName)->setUploadInfo($this->info);
+
+        return $file;
+    }
+
+    /**
+     * 获取保存文件名
+     * @access protected
+     * @param  string|bool $savename 保存的文件名 默认自动生成
+     * @return string
+     */
+    protected function buildSaveName($savename)
+    {
+        // 自动生成文件名
+        if (true === $savename) {
+            if ($this->rule instanceof \Closure) {
+                $savename = call_user_func_array($this->rule, [$this]);
+            } else {
+                switch ($this->rule) {
+                    case 'date':
+                        $savename = date('Ymd') . DS . md5(microtime(true));
+                        break;
+                    default:
+                        if (in_array($this->rule, hash_algos())) {
+                            $hash     = $this->hash($this->rule);
+                            $savename = substr($hash, 0, 2) . DS . substr($hash, 2);
+                        } elseif (is_callable($this->rule)) {
+                            $savename = call_user_func($this->rule);
+                        } else {
+                            $savename = date('Ymd') . DS . md5(microtime(true));
+                        }
+                }
+            }
+        } elseif ('' === $savename || false === $savename) {
+            $savename = $this->getInfo('name');
+        }
+
+        if (!strpos($savename, '.')) {
+            $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
+        }
+
+        return $savename;
+    }
+
+    /**
+     * 获取错误代码信息
+     * @access private
+     * @param  int $errorNo 错误号
+     * @return $this
+     */
+    private function error($errorNo)
+    {
+        switch ($errorNo) {
+            case 1:
+            case 2:
+                $this->error = 'upload File size exceeds the maximum value';
+                break;
+            case 3:
+                $this->error = 'only the portion of file is uploaded';
+                break;
+            case 4:
+                $this->error = 'no file to uploaded';
+                break;
+            case 6:
+                $this->error = 'upload temp dir not found';
+                break;
+            case 7:
+                $this->error = 'file write error';
+                break;
+            default:
+                $this->error = 'unknown upload error';
+        }
+
+        return $this;
+    }
+
+    /**
+     * 获取错误信息(支持多语言)
+     * @access public
+     * @return string
+     */
+    public function getError()
+    {
+        if (is_array($this->error)) {
+            list($msg, $vars) = $this->error;
+        } else {
+            $msg  = $this->error;
+            $vars = [];
+        }
+
+        return Lang::has($msg) ? Lang::get($msg, $vars) : $msg;
+    }
+
+    /**
+     * 魔法方法,获取文件的 hash 值
+     * @access public
+     * @param  string $method 方法名
+     * @param  mixed  $args   调用参数
+     * @return string
+     */
+    public function __call($method, $args)
+    {
+        return $this->hash($method);
+    }
+}

+ 148 - 0
thinkphp/library/think/Hook.php

@@ -0,0 +1,148 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+class Hook
+{
+    /**
+     * @var array 标签
+     */
+    private static $tags = [];
+
+    /**
+     * 动态添加行为扩展到某个标签
+     * @access public
+     * @param  string $tag      标签名称
+     * @param  mixed  $behavior 行为名称
+     * @param  bool   $first    是否放到开头执行
+     * @return void
+     */
+    public static function add($tag, $behavior, $first = false)
+    {
+        isset(self::$tags[$tag]) || self::$tags[$tag] = [];
+
+        if (is_array($behavior) && !is_callable($behavior)) {
+            if (!array_key_exists('_overlay', $behavior) || !$behavior['_overlay']) {
+                unset($behavior['_overlay']);
+                self::$tags[$tag] = array_merge(self::$tags[$tag], $behavior);
+            } else {
+                unset($behavior['_overlay']);
+                self::$tags[$tag] = $behavior;
+            }
+        } elseif ($first) {
+            array_unshift(self::$tags[$tag], $behavior);
+        } else {
+            self::$tags[$tag][] = $behavior;
+        }
+    }
+
+    /**
+     * 批量导入插件
+     * @access public
+     * @param  array   $tags      插件信息
+     * @param  boolean $recursive 是否递归合并
+     * @return void
+     */
+    public static function import(array $tags, $recursive = true)
+    {
+        if ($recursive) {
+            foreach ($tags as $tag => $behavior) {
+                self::add($tag, $behavior);
+            }
+        } else {
+            self::$tags = $tags + self::$tags;
+        }
+    }
+
+    /**
+     * 获取插件信息
+     * @access public
+     * @param  string $tag 插件位置(留空获取全部)
+     * @return array
+     */
+    public static function get($tag = '')
+    {
+        if (empty($tag)) {
+            return self::$tags;
+        }
+
+        return array_key_exists($tag, self::$tags) ? self::$tags[$tag] : [];
+    }
+
+    /**
+     * 监听标签的行为
+     * @access public
+     * @param  string $tag    标签名称
+     * @param  mixed  $params 传入参数
+     * @param  mixed  $extra  额外参数
+     * @param  bool   $once   只获取一个有效返回值
+     * @return mixed
+     */
+    public static function listen($tag, &$params = null, $extra = null, $once = false)
+    {
+        $results = [];
+
+        foreach (static::get($tag) as $key => $name) {
+            $results[$key] = self::exec($name, $tag, $params, $extra);
+
+            // 如果返回 false,或者仅获取一个有效返回则中断行为执行
+            if (false === $results[$key] || (!is_null($results[$key]) && $once)) {
+                break;
+            }
+        }
+
+        return $once ? end($results) : $results;
+    }
+
+    /**
+     * 执行某个行为
+     * @access public
+     * @param  mixed  $class  要执行的行为
+     * @param  string $tag    方法名(标签名)
+     * @param  mixed  $params 传人的参数
+     * @param  mixed  $extra  额外参数
+     * @return mixed
+     */
+    public static function exec($class, $tag = '', &$params = null, $extra = null)
+    {
+        App::$debug && Debug::remark('behavior_start', 'time');
+
+        $method = Loader::parseName($tag, 1, false);
+
+        if ($class instanceof \Closure) {
+            $result = call_user_func_array($class, [ & $params, $extra]);
+            $class  = 'Closure';
+        } elseif (is_array($class)) {
+            list($class, $method) = $class;
+
+            $result = (new $class())->$method($params, $extra);
+            $class  = $class . '->' . $method;
+        } elseif (is_object($class)) {
+            $result = $class->$method($params, $extra);
+            $class  = get_class($class);
+        } elseif (strpos($class, '::')) {
+            $result = call_user_func_array($class, [ & $params, $extra]);
+        } else {
+            $obj    = new $class();
+            $method = ($tag && is_callable([$obj, $method])) ? $method : 'run';
+            $result = $obj->$method($params, $extra);
+        }
+
+        if (App::$debug) {
+            Debug::remark('behavior_end', 'time');
+            Log::record('[ BEHAVIOR ] Run ' . $class . ' @' . $tag . ' [ RunTime:' . Debug::getRangeTime('behavior_start', 'behavior_end') . 's ]', 'info');
+        }
+
+        return $result;
+    }
+
+}

+ 610 - 0
thinkphp/library/think/Image.php

@@ -0,0 +1,610 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\image\Exception as ImageException;
+use think\image\gif\Gif;
+
+class Image
+{
+
+    /* 缩略图相关常量定义 */
+    const THUMB_SCALING   = 1; //常量,标识缩略图等比例缩放类型
+    const THUMB_FILLED    = 2; //常量,标识缩略图缩放后填充类型
+    const THUMB_CENTER    = 3; //常量,标识缩略图居中裁剪类型
+    const THUMB_NORTHWEST = 4; //常量,标识缩略图左上角裁剪类型
+    const THUMB_SOUTHEAST = 5; //常量,标识缩略图右下角裁剪类型
+    const THUMB_FIXED     = 6; //常量,标识缩略图固定尺寸缩放类型
+    /* 水印相关常量定义 */
+    const WATER_NORTHWEST = 1; //常量,标识左上角水印
+    const WATER_NORTH     = 2; //常量,标识上居中水印
+    const WATER_NORTHEAST = 3; //常量,标识右上角水印
+    const WATER_WEST      = 4; //常量,标识左居中水印
+    const WATER_CENTER    = 5; //常量,标识居中水印
+    const WATER_EAST      = 6; //常量,标识右居中水印
+    const WATER_SOUTHWEST = 7; //常量,标识左下角水印
+    const WATER_SOUTH     = 8; //常量,标识下居中水印
+    const WATER_SOUTHEAST = 9; //常量,标识右下角水印
+    /* 翻转相关常量定义 */
+    const FLIP_X = 1; //X轴翻转
+    const FLIP_Y = 2; //Y轴翻转
+
+    /**
+     * 图像资源对象
+     *
+     * @var resource
+     */
+    protected $im;
+
+    /** @var  Gif */
+    protected $gif;
+
+    /**
+     * 图像信息,包括 width, height, type, mime, size
+     *
+     * @var array
+     */
+    protected $info;
+
+    protected function __construct(\SplFileInfo $file)
+    {
+        //获取图像信息
+        $info = @getimagesize($file->getPathname());
+
+        //检测图像合法性
+        if (false === $info || (IMAGETYPE_GIF === $info[2] && empty($info['bits']))) {
+            throw new ImageException('Illegal image file');
+        }
+
+        //设置图像信息
+        $this->info = [
+            'width'  => $info[0],
+            'height' => $info[1],
+            'type'   => image_type_to_extension($info[2], false),
+            'mime'   => $info['mime'],
+        ];
+
+        //打开图像
+        if ('gif' == $this->info['type']) {
+            $this->gif = new Gif($file->getPathname());
+            $this->im  = @imagecreatefromstring($this->gif->image());
+        } else {
+            $fun      = "imagecreatefrom{$this->info['type']}";
+            $this->im = @$fun($file->getPathname());
+        }
+
+        if (empty($this->im)) {
+            throw new ImageException('Failed to create image resources!');
+        }
+
+    }
+
+    /**
+     * 打开一个图片文件
+     * @param \SplFileInfo|string $file
+     * @return Image
+     */
+    public static function open($file)
+    {
+        if (is_string($file)) {
+            $file = new \SplFileInfo($file);
+        }
+        if (!$file->isFile()) {
+            throw new ImageException('image file not exist');
+        }
+        return new self($file);
+    }
+
+    /**
+     * 保存图像
+     * @param string      $pathname  图像保存路径名称
+     * @param null|string $type      图像类型
+     * @param int         $quality   图像质量
+     * @param bool        $interlace 是否对JPEG类型图像设置隔行扫描
+     * @return $this
+     */
+    public function save($pathname, $type = null, $quality = 80, $interlace = true)
+    {
+        //自动获取图像类型
+        if (is_null($type)) {
+            $type = $this->info['type'];
+        } else {
+            $type = strtolower($type);
+        }
+        //保存图像
+        if ('jpeg' == $type || 'jpg' == $type) {
+            //JPEG图像设置隔行扫描
+            imageinterlace($this->im, $interlace);
+            imagejpeg($this->im, $pathname, $quality);
+        } elseif ('gif' == $type && !empty($this->gif)) {
+            $this->gif->save($pathname);
+        } elseif ('png' == $type) {
+            //设定保存完整的 alpha 通道信息
+            imagesavealpha($this->im, true);
+            //ImagePNG生成图像的质量范围从0到9的
+            imagepng($this->im, $pathname, min((int) ($quality / 10), 9));
+        } else {
+            $fun = 'image' . $type;
+            $fun($this->im, $pathname);
+        }
+
+        return $this;
+    }
+
+    /**
+     * 返回图像宽度
+     * @return int 图像宽度
+     */
+    public function width()
+    {
+        return $this->info['width'];
+    }
+
+    /**
+     * 返回图像高度
+     * @return int 图像高度
+     */
+    public function height()
+    {
+        return $this->info['height'];
+    }
+
+    /**
+     * 返回图像类型
+     * @return string 图像类型
+     */
+    public function type()
+    {
+        return $this->info['type'];
+    }
+
+    /**
+     * 返回图像MIME类型
+     * @return string 图像MIME类型
+     */
+    public function mime()
+    {
+        return $this->info['mime'];
+    }
+
+    /**
+     * 返回图像尺寸数组 0 - 图像宽度,1 - 图像高度
+     * @return array 图像尺寸
+     */
+    public function size()
+    {
+        return [$this->info['width'], $this->info['height']];
+    }
+
+    /**
+     * 旋转图像
+     * @param int $degrees 顺时针旋转的度数
+     * @return $this
+     */
+    public function rotate($degrees = 90)
+    {
+        do {
+            $img = imagerotate($this->im, -$degrees, imagecolorallocatealpha($this->im, 0, 0, 0, 127));
+            imagedestroy($this->im);
+            $this->im = $img;
+        } while (!empty($this->gif) && $this->gifNext());
+
+        $this->info['width']  = imagesx($this->im);
+        $this->info['height'] = imagesy($this->im);
+
+        return $this;
+    }
+
+    /**
+     * 翻转图像
+     * @param integer $direction 翻转轴,X或者Y
+     * @return $this
+     */
+    public function flip($direction = self::FLIP_X)
+    {
+        //原图宽度和高度
+        $w = $this->info['width'];
+        $h = $this->info['height'];
+
+        do {
+
+            $img = imagecreatetruecolor($w, $h);
+
+            switch ($direction) {
+                case self::FLIP_X:
+                    for ($y = 0; $y < $h; $y++) {
+                        imagecopy($img, $this->im, 0, $h - $y - 1, 0, $y, $w, 1);
+                    }
+                    break;
+                case self::FLIP_Y:
+                    for ($x = 0; $x < $w; $x++) {
+                        imagecopy($img, $this->im, $w - $x - 1, 0, $x, 0, 1, $h);
+                    }
+                    break;
+                default:
+                    throw new ImageException('不支持的翻转类型');
+            }
+
+            imagedestroy($this->im);
+            $this->im = $img;
+
+        } while (!empty($this->gif) && $this->gifNext());
+
+        return $this;
+    }
+
+    /**
+     * 裁剪图像
+     *
+     * @param  integer $w      裁剪区域宽度
+     * @param  integer $h      裁剪区域高度
+     * @param  integer $x      裁剪区域x坐标
+     * @param  integer $y      裁剪区域y坐标
+     * @param  integer $width  图像保存宽度
+     * @param  integer $height 图像保存高度
+     *
+     * @return $this
+     */
+    public function crop($w, $h, $x = 0, $y = 0, $width = null, $height = null)
+    {
+        //设置保存尺寸
+        empty($width) && $width   = $w;
+        empty($height) && $height = $h;
+        do {
+            //创建新图像
+            $img = imagecreatetruecolor($width, $height);
+            // 调整默认颜色
+            $color = imagecolorallocate($img, 255, 255, 255);
+            imagefill($img, 0, 0, $color);
+            //裁剪
+            imagecopyresampled($img, $this->im, 0, 0, $x, $y, $width, $height, $w, $h);
+            imagedestroy($this->im); //销毁原图
+            //设置新图像
+            $this->im = $img;
+        } while (!empty($this->gif) && $this->gifNext());
+        $this->info['width']  = (int) $width;
+        $this->info['height'] = (int) $height;
+        return $this;
+    }
+
+    /**
+     * 生成缩略图
+     *
+     * @param  integer $width  缩略图最大宽度
+     * @param  integer $height 缩略图最大高度
+     * @param int      $type   缩略图裁剪类型
+     *
+     * @return $this
+     */
+    public function thumb($width, $height, $type = self::THUMB_SCALING)
+    {
+        //原图宽度和高度
+        $w = $this->info['width'];
+        $h = $this->info['height'];
+        /* 计算缩略图生成的必要参数 */
+        switch ($type) {
+            /* 等比例缩放 */
+            case self::THUMB_SCALING:
+                //原图尺寸小于缩略图尺寸则不进行缩略
+                if ($w < $width && $h < $height) {
+                    return $this;
+                }
+                //计算缩放比例
+                $scale = min($width / $w, $height / $h);
+                //设置缩略图的坐标及宽度和高度
+                $x      = $y      = 0;
+                $width  = $w * $scale;
+                $height = $h * $scale;
+                break;
+            /* 居中裁剪 */
+            case self::THUMB_CENTER:
+                //计算缩放比例
+                $scale = max($width / $w, $height / $h);
+                //设置缩略图的坐标及宽度和高度
+                $w = $width / $scale;
+                $h = $height / $scale;
+                $x = ($this->info['width'] - $w) / 2;
+                $y = ($this->info['height'] - $h) / 2;
+                break;
+            /* 左上角裁剪 */
+            case self::THUMB_NORTHWEST:
+                //计算缩放比例
+                $scale = max($width / $w, $height / $h);
+                //设置缩略图的坐标及宽度和高度
+                $x = $y = 0;
+                $w = $width / $scale;
+                $h = $height / $scale;
+                break;
+            /* 右下角裁剪 */
+            case self::THUMB_SOUTHEAST:
+                //计算缩放比例
+                $scale = max($width / $w, $height / $h);
+                //设置缩略图的坐标及宽度和高度
+                $w = $width / $scale;
+                $h = $height / $scale;
+                $x = $this->info['width'] - $w;
+                $y = $this->info['height'] - $h;
+                break;
+            /* 填充 */
+            case self::THUMB_FILLED:
+                //计算缩放比例
+                if ($w < $width && $h < $height) {
+                    $scale = 1;
+                } else {
+                    $scale = min($width / $w, $height / $h);
+                }
+                //设置缩略图的坐标及宽度和高度
+                $neww = $w * $scale;
+                $newh = $h * $scale;
+                $x    = $this->info['width'] - $w;
+                $y    = $this->info['height'] - $h;
+                $posx = ($width - $w * $scale) / 2;
+                $posy = ($height - $h * $scale) / 2;
+                do {
+                    //创建新图像
+                    $img = imagecreatetruecolor($width, $height);
+                    // 调整默认颜色
+                    $color = imagecolorallocate($img, 255, 255, 255);
+                    imagefill($img, 0, 0, $color);
+                    //裁剪
+                    imagecopyresampled($img, $this->im, $posx, $posy, $x, $y, $neww, $newh, $w, $h);
+                    imagedestroy($this->im); //销毁原图
+                    $this->im = $img;
+                } while (!empty($this->gif) && $this->gifNext());
+                $this->info['width']  = (int) $width;
+                $this->info['height'] = (int) $height;
+                return $this;
+            /* 固定 */
+            case self::THUMB_FIXED:
+                $x = $y = 0;
+                break;
+            default:
+                throw new ImageException('不支持的缩略图裁剪类型');
+        }
+        /* 裁剪图像 */
+        return $this->crop($w, $h, $x, $y, $width, $height);
+    }
+
+    /**
+     * 添加水印
+     *
+     * @param  string $source 水印图片路径
+     * @param int     $locate 水印位置
+     * @param int     $alpha  透明度
+     * @return $this
+     */
+    public function water($source, $locate = self::WATER_SOUTHEAST, $alpha = 100)
+    {
+        if (!is_file($source)) {
+            throw new ImageException('水印图像不存在');
+        }
+        //获取水印图像信息
+        $info = getimagesize($source);
+        if (false === $info || (IMAGETYPE_GIF === $info[2] && empty($info['bits']))) {
+            throw new ImageException('非法水印文件');
+        }
+        //创建水印图像资源
+        $fun   = 'imagecreatefrom' . image_type_to_extension($info[2], false);
+        $water = $fun($source);
+        //设定水印图像的混色模式
+        imagealphablending($water, true);
+        /* 设定水印位置 */
+        switch ($locate) {
+            /* 右下角水印 */
+            case self::WATER_SOUTHEAST:
+                $x = $this->info['width'] - $info[0];
+                $y = $this->info['height'] - $info[1];
+                break;
+            /* 左下角水印 */
+            case self::WATER_SOUTHWEST:
+                $x = 0;
+                $y = $this->info['height'] - $info[1];
+                break;
+            /* 左上角水印 */
+            case self::WATER_NORTHWEST:
+                $x = $y = 0;
+                break;
+            /* 右上角水印 */
+            case self::WATER_NORTHEAST:
+                $x = $this->info['width'] - $info[0];
+                $y = 0;
+                break;
+            /* 居中水印 */
+            case self::WATER_CENTER:
+                $x = ($this->info['width'] - $info[0]) / 2;
+                $y = ($this->info['height'] - $info[1]) / 2;
+                break;
+            /* 下居中水印 */
+            case self::WATER_SOUTH:
+                $x = ($this->info['width'] - $info[0]) / 2;
+                $y = $this->info['height'] - $info[1];
+                break;
+            /* 右居中水印 */
+            case self::WATER_EAST:
+                $x = $this->info['width'] - $info[0];
+                $y = ($this->info['height'] - $info[1]) / 2;
+                break;
+            /* 上居中水印 */
+            case self::WATER_NORTH:
+                $x = ($this->info['width'] - $info[0]) / 2;
+                $y = 0;
+                break;
+            /* 左居中水印 */
+            case self::WATER_WEST:
+                $x = 0;
+                $y = ($this->info['height'] - $info[1]) / 2;
+                break;
+            default:
+                /* 自定义水印坐标 */
+                if (is_array($locate)) {
+                    list($x, $y) = $locate;
+                } else {
+                    throw new ImageException('不支持的水印位置类型');
+                }
+        }
+        do {
+            //添加水印
+            $src = imagecreatetruecolor($info[0], $info[1]);
+            // 调整默认颜色
+            $color = imagecolorallocate($src, 255, 255, 255);
+            imagefill($src, 0, 0, $color);
+            imagecopy($src, $this->im, 0, 0, $x, $y, $info[0], $info[1]);
+            imagecopy($src, $water, 0, 0, 0, 0, $info[0], $info[1]);
+            imagecopymerge($this->im, $src, $x, $y, 0, 0, $info[0], $info[1], $alpha);
+            //销毁零时图片资源
+            imagedestroy($src);
+        } while (!empty($this->gif) && $this->gifNext());
+        //销毁水印资源
+        imagedestroy($water);
+        return $this;
+    }
+
+    /**
+     * 图像添加文字
+     *
+     * @param  string  $text   添加的文字
+     * @param  string  $font   字体路径
+     * @param  integer $size   字号
+     * @param  string  $color  文字颜色
+     * @param int      $locate 文字写入位置
+     * @param  integer $offset 文字相对当前位置的偏移量
+     * @param  integer $angle  文字倾斜角度
+     *
+     * @return $this
+     * @throws ImageException
+     */
+    public function text($text, $font, $size, $color = '#00000000',
+        $locate = self::WATER_SOUTHEAST, $offset = 0, $angle = 0) {
+
+        if (!is_file($font)) {
+            throw new ImageException("不存在的字体文件:{$font}");
+        }
+        //获取文字信息
+        $info = imagettfbbox($size, $angle, $font, $text);
+        $minx = min($info[0], $info[2], $info[4], $info[6]);
+        $maxx = max($info[0], $info[2], $info[4], $info[6]);
+        $miny = min($info[1], $info[3], $info[5], $info[7]);
+        $maxy = max($info[1], $info[3], $info[5], $info[7]);
+        /* 计算文字初始坐标和尺寸 */
+        $x = $minx;
+        $y = abs($miny);
+        $w = $maxx - $minx;
+        $h = $maxy - $miny;
+        /* 设定文字位置 */
+        switch ($locate) {
+            /* 右下角文字 */
+            case self::WATER_SOUTHEAST:
+                $x += $this->info['width'] - $w;
+                $y += $this->info['height'] - $h;
+                break;
+            /* 左下角文字 */
+            case self::WATER_SOUTHWEST:
+                $y += $this->info['height'] - $h;
+                break;
+            /* 左上角文字 */
+            case self::WATER_NORTHWEST:
+                // 起始坐标即为左上角坐标,无需调整
+                break;
+            /* 右上角文字 */
+            case self::WATER_NORTHEAST:
+                $x += $this->info['width'] - $w;
+                break;
+            /* 居中文字 */
+            case self::WATER_CENTER:
+                $x += ($this->info['width'] - $w) / 2;
+                $y += ($this->info['height'] - $h) / 2;
+                break;
+            /* 下居中文字 */
+            case self::WATER_SOUTH:
+                $x += ($this->info['width'] - $w) / 2;
+                $y += $this->info['height'] - $h;
+                break;
+            /* 右居中文字 */
+            case self::WATER_EAST:
+                $x += $this->info['width'] - $w;
+                $y += ($this->info['height'] - $h) / 2;
+                break;
+            /* 上居中文字 */
+            case self::WATER_NORTH:
+                $x += ($this->info['width'] - $w) / 2;
+                break;
+            /* 左居中文字 */
+            case self::WATER_WEST:
+                $y += ($this->info['height'] - $h) / 2;
+                break;
+            default:
+                /* 自定义文字坐标 */
+                if (is_array($locate)) {
+                    list($posx, $posy) = $locate;
+                    $x += $posx;
+                    $y += $posy;
+                } else {
+                    throw new ImageException('不支持的文字位置类型');
+                }
+        }
+        /* 设置偏移量 */
+        if (is_array($offset)) {
+            $offset        = array_map('intval', $offset);
+            list($ox, $oy) = $offset;
+        } else {
+            $offset = intval($offset);
+            $ox     = $oy     = $offset;
+        }
+        /* 设置颜色 */
+        if (is_string($color) && 0 === strpos($color, '#')) {
+            $color = str_split(substr($color, 1), 2);
+            $color = array_map('hexdec', $color);
+            if (empty($color[3]) || $color[3] > 127) {
+                $color[3] = 0;
+            }
+        } elseif (!is_array($color)) {
+            throw new ImageException('错误的颜色值');
+        }
+        do {
+            /* 写入文字 */
+            $col = imagecolorallocatealpha($this->im, $color[0], $color[1], $color[2], $color[3]);
+            imagettftext($this->im, $size, $angle, $x + $ox, $y + $oy, $col, $font, $text);
+        } while (!empty($this->gif) && $this->gifNext());
+        return $this;
+    }
+
+    /**
+     * 切换到GIF的下一帧并保存当前帧
+     */
+    protected function gifNext()
+    {
+        ob_start();
+        ob_implicit_flush(0);
+        imagegif($this->im);
+        $img = ob_get_clean();
+        $this->gif->image($img);
+        $next = $this->gif->nextImage();
+        if ($next) {
+            imagedestroy($this->im);
+            $this->im = imagecreatefromstring($next);
+            return $next;
+        } else {
+            imagedestroy($this->im);
+            $this->im = imagecreatefromstring($this->gif->image());
+            return false;
+        }
+    }
+
+    /**
+     * 析构方法,用于销毁图像资源
+     */
+    public function __destruct()
+    {
+        empty($this->im) || imagedestroy($this->im);
+    }
+
+}

+ 265 - 0
thinkphp/library/think/Lang.php

@@ -0,0 +1,265 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+class Lang
+{
+    /**
+     * @var array 语言数据
+     */
+    private static $lang = [];
+
+    /**
+     * @var string 语言作用域
+     */
+    private static $range = 'zh-cn';
+
+    /**
+     * @var string 语言自动侦测的变量
+     */
+    protected static $langDetectVar = 'lang';
+
+    /**
+     * @var string 语言 Cookie 变量
+     */
+    protected static $langCookieVar = 'think_var';
+
+    /**
+     * @var int 语言 Cookie 的过期时间
+     */
+    protected static $langCookieExpire = 3600;
+
+    /**
+     * @var array 允许语言列表
+     */
+    protected static $allowLangList = [];
+
+    /**
+     * @var array Accept-Language 转义为对应语言包名称 系统默认配置
+     */
+    protected static $acceptLanguage = ['zh-hans-cn' => 'zh-cn'];
+
+    /**
+     * 设定当前的语言
+     * @access public
+     * @param  string $range 语言作用域
+     * @return string
+     */
+    public static function range($range = '')
+    {
+        if ($range) {
+            self::$range = $range;
+        }
+
+        return self::$range;
+    }
+
+    /**
+     * 设置语言定义(不区分大小写)
+     * @access public
+     * @param  string|array  $name  语言变量
+     * @param  string        $value 语言值
+     * @param  string        $range 语言作用域
+     * @return mixed
+     */
+    public static function set($name, $value = null, $range = '')
+    {
+        $range = $range ?: self::$range;
+
+        if (!isset(self::$lang[$range])) {
+            self::$lang[$range] = [];
+        }
+
+        if (is_array($name)) {
+            return self::$lang[$range] = array_change_key_case($name) + self::$lang[$range];
+        }
+
+        return self::$lang[$range][strtolower($name)] = $value;
+    }
+
+    /**
+     * 加载语言定义(不区分大小写)
+     * @access public
+     * @param  array|string $file 语言文件
+     * @param  string $range      语言作用域
+     * @return mixed
+     */
+    public static function load($file, $range = '')
+    {
+        $range = $range ?: self::$range;
+        $file  = is_string($file) ? [$file] : $file;
+
+        if (!isset(self::$lang[$range])) {
+            self::$lang[$range] = [];
+        }
+
+        $lang = [];
+
+        foreach ($file as $_file) {
+            if (is_file($_file)) {
+                // 记录加载信息
+                App::$debug && Log::record('[ LANG ] ' . $_file, 'info');
+
+                $_lang = include $_file;
+
+                if (is_array($_lang)) {
+                    $lang = array_change_key_case($_lang) + $lang;
+                }
+            }
+        }
+
+        if (!empty($lang)) {
+            self::$lang[$range] = $lang + self::$lang[$range];
+        }
+
+        return self::$lang[$range];
+    }
+
+    /**
+     * 获取语言定义(不区分大小写)
+     * @access public
+     * @param  string|null $name  语言变量
+     * @param  string      $range 语言作用域
+     * @return mixed
+     */
+    public static function has($name, $range = '')
+    {
+        $range = $range ?: self::$range;
+
+        return isset(self::$lang[$range][strtolower($name)]);
+    }
+
+    /**
+     * 获取语言定义(不区分大小写)
+     * @access public
+     * @param  string|null $name  语言变量
+     * @param  array       $vars  变量替换
+     * @param  string      $range 语言作用域
+     * @return mixed
+     */
+    public static function get($name = null, $vars = [], $range = '')
+    {
+        $range = $range ?: self::$range;
+
+        // 空参数返回所有定义
+        if (empty($name)) {
+            return self::$lang[$range];
+        }
+
+        $key   = strtolower($name);
+        $value = isset(self::$lang[$range][$key]) ? self::$lang[$range][$key] : $name;
+
+        // 变量解析
+        if (!empty($vars) && is_array($vars)) {
+            /**
+             * Notes:
+             * 为了检测的方便,数字索引的判断仅仅是参数数组的第一个元素的key为数字0
+             * 数字索引采用的是系统的 sprintf 函数替换,用法请参考 sprintf 函数
+             */
+            if (key($vars) === 0) {
+                // 数字索引解析
+                array_unshift($vars, $value);
+                $value = call_user_func_array('sprintf', $vars);
+            } else {
+                // 关联索引解析
+                $replace = array_keys($vars);
+                foreach ($replace as &$v) {
+                    $v = "{:{$v}}";
+                }
+                $value = str_replace($replace, $vars, $value);
+            }
+
+        }
+
+        return $value;
+    }
+
+    /**
+     * 自动侦测设置获取语言选择
+     * @access public
+     * @return string
+     */
+    public static function detect()
+    {
+        $langSet = '';
+
+        if (isset($_GET[self::$langDetectVar])) {
+            // url 中设置了语言变量
+            $langSet = strtolower($_GET[self::$langDetectVar]);
+        } elseif (isset($_COOKIE[self::$langCookieVar])) {
+            // Cookie 中设置了语言变量
+            $langSet = strtolower($_COOKIE[self::$langCookieVar]);
+        } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
+            // 自动侦测浏览器语言
+            preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches);
+            $langSet     = strtolower($matches[1]);
+            $acceptLangs = Config::get('header_accept_lang');
+
+            if (isset($acceptLangs[$langSet])) {
+                $langSet = $acceptLangs[$langSet];
+            } elseif (isset(self::$acceptLanguage[$langSet])) {
+                $langSet = self::$acceptLanguage[$langSet];
+            }
+        }
+
+        // 合法的语言
+        if (empty(self::$allowLangList) || in_array($langSet, self::$allowLangList)) {
+            self::$range = $langSet ?: self::$range;
+        }
+
+        return self::$range;
+    }
+
+    /**
+     * 设置语言自动侦测的变量
+     * @access public
+     * @param  string $var 变量名称
+     * @return void
+     */
+    public static function setLangDetectVar($var)
+    {
+        self::$langDetectVar = $var;
+    }
+
+    /**
+     * 设置语言的 cookie 保存变量
+     * @access public
+     * @param  string $var 变量名称
+     * @return void
+     */
+    public static function setLangCookieVar($var)
+    {
+        self::$langCookieVar = $var;
+    }
+
+    /**
+     * 设置语言的 cookie 的过期时间
+     * @access public
+     * @param  string $expire 过期时间
+     * @return void
+     */
+    public static function setLangCookieExpire($expire)
+    {
+        self::$langCookieExpire = $expire;
+    }
+
+    /**
+     * 设置允许的语言列表
+     * @access public
+     * @param  array $list 语言列表
+     * @return void
+     */
+    public static function setAllowLangList($list)
+    {
+        self::$allowLangList = $list;
+    }
+}

+ 677 - 0
thinkphp/library/think/Loader.php

@@ -0,0 +1,677 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\exception\ClassNotFoundException;
+
+class Loader
+{
+    /**
+     * @var array 实例数组
+     */
+    protected static $instance = [];
+
+    /**
+     * @var array 类名映射
+     */
+    protected static $classMap = [];
+
+    /**
+     * @var array 命名空间别名
+     */
+    protected static $namespaceAlias = [];
+
+    /**
+     * @var array PSR-4 命名空间前缀长度映射
+     */
+    private static $prefixLengthsPsr4 = [];
+
+    /**
+     * @var array PSR-4 的加载目录
+     */
+    private static $prefixDirsPsr4 = [];
+
+    /**
+     * @var array PSR-4 加载失败的回退目录
+     */
+    private static $fallbackDirsPsr4 = [];
+
+    /**
+     * @var array PSR-0 命名空间前缀映射
+     */
+    private static $prefixesPsr0 = [];
+
+    /**
+     * @var array PSR-0 加载失败的回退目录
+     */
+    private static $fallbackDirsPsr0 = [];
+
+    /**
+     * @var array 需要加载的文件
+     */
+    private static $files = [];
+
+    /**
+     * 自动加载
+     * @access public
+     * @param  string $class 类名
+     * @return bool
+     */
+    public static function autoload($class)
+    {
+        // 检测命名空间别名
+        if (!empty(self::$namespaceAlias)) {
+            $namespace = dirname($class);
+            if (isset(self::$namespaceAlias[$namespace])) {
+                $original = self::$namespaceAlias[$namespace] . '\\' . basename($class);
+                if (class_exists($original)) {
+                    return class_alias($original, $class, false);
+                }
+            }
+        }
+
+        if ($file = self::findFile($class)) {
+            // 非 Win 环境不严格区分大小写
+            if (!IS_WIN || pathinfo($file, PATHINFO_FILENAME) == pathinfo(realpath($file), PATHINFO_FILENAME)) {
+                __include_file($file);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 查找文件
+     * @access private
+     * @param  string $class 类名
+     * @return bool|string
+     */
+    private static function findFile($class)
+    {
+        // 类库映射
+        if (!empty(self::$classMap[$class])) {
+            return self::$classMap[$class];
+        }
+
+        // 查找 PSR-4
+        $logicalPathPsr4 = strtr($class, '\\', DS) . EXT;
+        $first           = $class[0];
+
+        if (isset(self::$prefixLengthsPsr4[$first])) {
+            foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach (self::$prefixDirsPsr4[$prefix] as $dir) {
+                        if (is_file($file = $dir . DS . substr($logicalPathPsr4, $length))) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // 查找 PSR-4 fallback dirs
+        foreach (self::$fallbackDirsPsr4 as $dir) {
+            if (is_file($file = $dir . DS . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // 查找 PSR-0
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespace class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+            . strtr(substr($logicalPathPsr4, $pos + 1), '_', DS);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DS) . EXT;
+        }
+
+        if (isset(self::$prefixesPsr0[$first])) {
+            foreach (self::$prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (is_file($file = $dir . DS . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // 查找 PSR-0 fallback dirs
+        foreach (self::$fallbackDirsPsr0 as $dir) {
+            if (is_file($file = $dir . DS . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // 找不到则设置映射为 false 并返回
+        return self::$classMap[$class] = false;
+    }
+
+    /**
+     * 注册 classmap
+     * @access public
+     * @param  string|array $class 类名
+     * @param  string       $map   映射
+     * @return void
+     */
+    public static function addClassMap($class, $map = '')
+    {
+        if (is_array($class)) {
+            self::$classMap = array_merge(self::$classMap, $class);
+        } else {
+            self::$classMap[$class] = $map;
+        }
+    }
+
+    /**
+     * 注册命名空间
+     * @access public
+     * @param  string|array $namespace 命名空间
+     * @param  string       $path      路径
+     * @return void
+     */
+    public static function addNamespace($namespace, $path = '')
+    {
+        if (is_array($namespace)) {
+            foreach ($namespace as $prefix => $paths) {
+                self::addPsr4($prefix . '\\', rtrim($paths, DS), true);
+            }
+        } else {
+            self::addPsr4($namespace . '\\', rtrim($path, DS), true);
+        }
+    }
+
+    /**
+     * 添加 PSR-0 命名空间
+     * @access private
+     * @param  array|string $prefix  空间前缀
+     * @param  array        $paths   路径
+     * @param  bool         $prepend 预先设置的优先级更高
+     * @return void
+     */
+    private static function addPsr0($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            self::$fallbackDirsPsr0 = $prepend ?
+            array_merge((array) $paths, self::$fallbackDirsPsr0) :
+            array_merge(self::$fallbackDirsPsr0, (array) $paths);
+        } else {
+            $first = $prefix[0];
+
+            if (!isset(self::$prefixesPsr0[$first][$prefix])) {
+                self::$prefixesPsr0[$first][$prefix] = (array) $paths;
+            } else {
+                self::$prefixesPsr0[$first][$prefix] = $prepend ?
+                array_merge((array) $paths, self::$prefixesPsr0[$first][$prefix]) :
+                array_merge(self::$prefixesPsr0[$first][$prefix], (array) $paths);
+            }
+        }
+    }
+
+    /**
+     * 添加 PSR-4 空间
+     * @access private
+     * @param  array|string $prefix  空间前缀
+     * @param  string       $paths   路径
+     * @param  bool         $prepend 预先设置的优先级更高
+     * @return void
+     */
+    private static function addPsr4($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            self::$fallbackDirsPsr4 = $prepend ?
+            array_merge((array) $paths, self::$fallbackDirsPsr4) :
+            array_merge(self::$fallbackDirsPsr4, (array) $paths);
+
+        } elseif (!isset(self::$prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException(
+                    "A non-empty PSR-4 prefix must end with a namespace separator."
+                );
+            }
+
+            self::$prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            self::$prefixDirsPsr4[$prefix]                = (array) $paths;
+
+        } else {
+            self::$prefixDirsPsr4[$prefix] = $prepend ?
+            // Prepend directories for an already registered namespace.
+            array_merge((array) $paths, self::$prefixDirsPsr4[$prefix]) :
+            // Append directories for an already registered namespace.
+            array_merge(self::$prefixDirsPsr4[$prefix], (array) $paths);
+        }
+    }
+
+    /**
+     * 注册命名空间别名
+     * @access public
+     * @param  array|string $namespace 命名空间
+     * @param  string       $original  源文件
+     * @return void
+     */
+    public static function addNamespaceAlias($namespace, $original = '')
+    {
+        if (is_array($namespace)) {
+            self::$namespaceAlias = array_merge(self::$namespaceAlias, $namespace);
+        } else {
+            self::$namespaceAlias[$namespace] = $original;
+        }
+    }
+
+    /**
+     * 注册自动加载机制
+     * @access public
+     * @param  callable $autoload 自动加载处理方法
+     * @return void
+     */
+    public static function register($autoload = null)
+    {
+        // 注册系统自动加载
+        spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true);
+
+        // Composer 自动加载支持
+        if (is_dir(VENDOR_PATH . 'composer')) {
+            if (PHP_VERSION_ID >= 50600 && is_file(VENDOR_PATH . 'composer' . DS . 'autoload_static.php')) {
+                require VENDOR_PATH . 'composer' . DS . 'autoload_static.php';
+
+                $declaredClass = get_declared_classes();
+                $composerClass = array_pop($declaredClass);
+
+                foreach (['prefixLengthsPsr4', 'prefixDirsPsr4', 'fallbackDirsPsr4', 'prefixesPsr0', 'fallbackDirsPsr0', 'classMap', 'files'] as $attr) {
+                    if (property_exists($composerClass, $attr)) {
+                        self::${$attr} = $composerClass::${$attr};
+                    }
+                }
+            } else {
+                self::registerComposerLoader();
+            }
+        }
+
+        // 注册命名空间定义
+        self::addNamespace([
+            'think'    => LIB_PATH . 'think' . DS,
+            'behavior' => LIB_PATH . 'behavior' . DS,
+            'traits'   => LIB_PATH . 'traits' . DS,
+        ]);
+
+        // 加载类库映射文件
+        if (is_file(RUNTIME_PATH . 'classmap' . EXT)) {
+            self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT));
+        }
+
+        self::loadComposerAutoloadFiles();
+
+        // 自动加载 extend 目录
+        self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS);
+    }
+
+    /**
+     * 注册 composer 自动加载
+     * @access private
+     * @return void
+     */
+    private static function registerComposerLoader()
+    {
+        if (is_file(VENDOR_PATH . 'composer/autoload_namespaces.php')) {
+            $map = require VENDOR_PATH . 'composer/autoload_namespaces.php';
+            foreach ($map as $namespace => $path) {
+                self::addPsr0($namespace, $path);
+            }
+        }
+
+        if (is_file(VENDOR_PATH . 'composer/autoload_psr4.php')) {
+            $map = require VENDOR_PATH . 'composer/autoload_psr4.php';
+            foreach ($map as $namespace => $path) {
+                self::addPsr4($namespace, $path);
+            }
+        }
+
+        if (is_file(VENDOR_PATH . 'composer/autoload_classmap.php')) {
+            $classMap = require VENDOR_PATH . 'composer/autoload_classmap.php';
+            if ($classMap) {
+                self::addClassMap($classMap);
+            }
+        }
+
+        if (is_file(VENDOR_PATH . 'composer/autoload_files.php')) {
+            self::$files = require VENDOR_PATH . 'composer/autoload_files.php';
+        }
+    }
+
+    // 加载composer autofile文件
+    public static function loadComposerAutoloadFiles()
+    {
+        foreach (self::$files as $fileIdentifier => $file) {
+            if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+                __require_file($file);
+
+                $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+            }
+        }
+    }
+
+    /**
+     * 导入所需的类库 同 Java 的 Import 本函数有缓存功能
+     * @access public
+     * @param  string $class   类库命名空间字符串
+     * @param  string $baseUrl 起始路径
+     * @param  string $ext     导入的文件扩展名
+     * @return bool
+     */
+    public static function import($class, $baseUrl = '', $ext = EXT)
+    {
+        static $_file = [];
+        $key          = $class . $baseUrl;
+        $class        = str_replace(['.', '#'], [DS, '.'], $class);
+
+        if (isset($_file[$key])) {
+            return true;
+        }
+
+        if (empty($baseUrl)) {
+            list($name, $class) = explode(DS, $class, 2);
+
+            if (isset(self::$prefixDirsPsr4[$name . '\\'])) {
+                // 注册的命名空间
+                $baseUrl = self::$prefixDirsPsr4[$name . '\\'];
+            } elseif ('@' == $name) {
+                // 加载当前模块应用类库
+                $baseUrl = App::$modulePath;
+            } elseif (is_dir(EXTEND_PATH . $name)) {
+                $baseUrl = EXTEND_PATH . $name . DS;
+            } else {
+                // 加载其它模块的类库
+                $baseUrl = APP_PATH . $name . DS;
+            }
+        } elseif (substr($baseUrl, -1) != DS) {
+            $baseUrl .= DS;
+        }
+
+        // 如果类存在则导入类库文件
+        if (is_array($baseUrl)) {
+            foreach ($baseUrl as $path) {
+                if (is_file($filename = $path . DS . $class . $ext)) {
+                    break;
+                }
+            }
+        } else {
+            $filename = $baseUrl . $class . $ext;
+        }
+
+        if (!empty($filename) &&
+            is_file($filename) &&
+            (!IS_WIN || pathinfo($filename, PATHINFO_FILENAME) == pathinfo(realpath($filename), PATHINFO_FILENAME))
+        ) {
+            __include_file($filename);
+            $_file[$key] = true;
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 实例化(分层)模型
+     * @access public
+     * @param  string $name         Model名称
+     * @param  string $layer        业务层名称
+     * @param  bool   $appendSuffix 是否添加类名后缀
+     * @param  string $common       公共模块名
+     * @return object
+     * @throws ClassNotFoundException
+     */
+    public static function model($name = '', $layer = 'model', $appendSuffix = false, $common = 'common')
+    {
+        $uid = $name . $layer;
+
+        if (isset(self::$instance[$uid])) {
+            return self::$instance[$uid];
+        }
+
+        list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix);
+
+        if (class_exists($class)) {
+            $model = new $class();
+        } else {
+            $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class);
+
+            if (class_exists($class)) {
+                $model = new $class();
+            } else {
+                throw new ClassNotFoundException('class not exists:' . $class, $class);
+            }
+        }
+
+        return self::$instance[$uid] = $model;
+    }
+
+    /**
+     * 实例化(分层)控制器 格式:[模块名/]控制器名
+     * @access public
+     * @param  string $name         资源地址
+     * @param  string $layer        控制层名称
+     * @param  bool   $appendSuffix 是否添加类名后缀
+     * @param  string $empty        空控制器名称
+     * @return object
+     * @throws ClassNotFoundException
+     */
+    public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
+    {
+        list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix);
+
+        if (class_exists($class)) {
+            return App::invokeClass($class);
+        }
+
+        if ($empty) {
+            $emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix);
+
+            if (class_exists($emptyClass)) {
+                return new $emptyClass(Request::instance());
+            }
+        }
+
+        throw new ClassNotFoundException('class not exists:' . $class, $class);
+    }
+
+    /**
+     * 实例化验证类 格式:[模块名/]验证器名
+     * @access public
+     * @param  string $name         资源地址
+     * @param  string $layer        验证层名称
+     * @param  bool   $appendSuffix 是否添加类名后缀
+     * @param  string $common       公共模块名
+     * @return object|false
+     * @throws ClassNotFoundException
+     */
+    public static function validate($name = '', $layer = 'validate', $appendSuffix = false, $common = 'common')
+    {
+        $name = $name ?: Config::get('default_validate');
+
+        if (empty($name)) {
+            return new Validate;
+        }
+
+        $uid = $name . $layer;
+        if (isset(self::$instance[$uid])) {
+            return self::$instance[$uid];
+        }
+
+        list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix);
+
+        if (class_exists($class)) {
+            $validate = new $class;
+        } else {
+            $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class);
+
+            if (class_exists($class)) {
+                $validate = new $class;
+            } else {
+                throw new ClassNotFoundException('class not exists:' . $class, $class);
+            }
+        }
+
+        return self::$instance[$uid] = $validate;
+    }
+
+    /**
+     * 解析模块和类名
+     * @access protected
+     * @param  string $name         资源地址
+     * @param  string $layer        验证层名称
+     * @param  bool   $appendSuffix 是否添加类名后缀
+     * @return array
+     */
+    protected static function getModuleAndClass($name, $layer, $appendSuffix)
+    {
+        if (false !== strpos($name, '\\')) {
+            $module = Request::instance()->module();
+            $class  = $name;
+        } else {
+            if (strpos($name, '/')) {
+                list($module, $name) = explode('/', $name, 2);
+            } else {
+                $module = Request::instance()->module();
+            }
+
+            $class = self::parseClass($module, $layer, $name, $appendSuffix);
+        }
+
+        return [$module, $class];
+    }
+
+    /**
+     * 数据库初始化 并取得数据库类实例
+     * @access public
+     * @param  mixed       $config 数据库配置
+     * @param  bool|string $name   连接标识 true 强制重新连接
+     * @return \think\db\Connection
+     */
+    public static function db($config = [], $name = false)
+    {
+        return Db::connect($config, $name);
+    }
+
+    /**
+     * 远程调用模块的操作方法 参数格式 [模块/控制器/]操作
+     * @access public
+     * @param  string       $url          调用地址
+     * @param  string|array $vars         调用参数 支持字符串和数组
+     * @param  string       $layer        要调用的控制层名称
+     * @param  bool         $appendSuffix 是否添加类名后缀
+     * @return mixed
+     */
+    public static function action($url, $vars = [], $layer = 'controller', $appendSuffix = false)
+    {
+        $info   = pathinfo($url);
+        $action = $info['basename'];
+        $module = '.' != $info['dirname'] ? $info['dirname'] : Request::instance()->controller();
+        $class  = self::controller($module, $layer, $appendSuffix);
+
+        if ($class) {
+            if (is_scalar($vars)) {
+                if (strpos($vars, '=')) {
+                    parse_str($vars, $vars);
+                } else {
+                    $vars = [$vars];
+                }
+            }
+
+            return App::invokeMethod([$class, $action . Config::get('action_suffix')], $vars);
+        }
+
+        return false;
+    }
+
+    /**
+     * 字符串命名风格转换
+     * type 0 将 Java 风格转换为 C 的风格 1 将 C 风格转换为 Java 的风格
+     * @access public
+     * @param  string  $name    字符串
+     * @param  integer $type    转换类型
+     * @param  bool    $ucfirst 首字母是否大写(驼峰规则)
+     * @return string
+     */
+    public static function parseName($name, $type = 0, $ucfirst = true)
+    {
+        if ($type) {
+            $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) {
+                return strtoupper($match[1]);
+            }, $name);
+
+            return $ucfirst ? ucfirst($name) : lcfirst($name);
+        }
+
+        return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_"));
+    }
+
+    /**
+     * 解析应用类的类名
+     * @access public
+     * @param  string $module       模块名
+     * @param  string $layer        层名 controller model ...
+     * @param  string $name         类名
+     * @param  bool   $appendSuffix 是否添加类名后缀
+     * @return string
+     */
+    public static function parseClass($module, $layer, $name, $appendSuffix = false)
+    {
+
+        $array = explode('\\', str_replace(['/', '.'], '\\', $name));
+        $class = self::parseName(array_pop($array), 1);
+        $class = $class . (App::$suffix || $appendSuffix ? ucfirst($layer) : '');
+        $path  = $array ? implode('\\', $array) . '\\' : '';
+
+        return App::$namespace . '\\' .
+            ($module ? $module . '\\' : '') .
+            $layer . '\\' . $path . $class;
+    }
+
+    /**
+     * 初始化类的实例
+     * @access public
+     * @return void
+     */
+    public static function clearInstance()
+    {
+        self::$instance = [];
+    }
+}
+
+// 作用范围隔离
+
+/**
+ * include
+ * @param  string $file 文件路径
+ * @return mixed
+ */
+function __include_file($file)
+{
+    return include $file;
+}
+
+/**
+ * require
+ * @param  string $file 文件路径
+ * @return mixed
+ */
+function __require_file($file)
+{
+    return require $file;
+}

+ 237 - 0
thinkphp/library/think/Log.php

@@ -0,0 +1,237 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\exception\ClassNotFoundException;
+
+/**
+ * Class Log
+ * @package think
+ *
+ * @method void log($msg) static 记录一般日志
+ * @method void error($msg) static 记录错误日志
+ * @method void info($msg) static 记录一般信息日志
+ * @method void sql($msg) static 记录 SQL 查询日志
+ * @method void notice($msg) static 记录提示日志
+ * @method void alert($msg) static 记录报警日志
+ */
+class Log
+{
+    const LOG    = 'log';
+    const ERROR  = 'error';
+    const INFO   = 'info';
+    const SQL    = 'sql';
+    const NOTICE = 'notice';
+    const ALERT  = 'alert';
+    const DEBUG  = 'debug';
+
+    /**
+     * @var array 日志信息
+     */
+    protected static $log = [];
+
+    /**
+     * @var array 配置参数
+     */
+    protected static $config = [];
+
+    /**
+     * @var array 日志类型
+     */
+    protected static $type = ['log', 'error', 'info', 'sql', 'notice', 'alert', 'debug'];
+
+    /**
+     * @var log\driver\File|log\driver\Test|log\driver\Socket 日志写入驱动
+     */
+    protected static $driver;
+
+    /**
+     * @var string 当前日志授权 key
+     */
+    protected static $key;
+
+    /**
+     * 日志初始化
+     * @access public
+     * @param  array $config 配置参数
+     * @return void
+     */
+    public static function init($config = [])
+    {
+        $type  = isset($config['type']) ? $config['type'] : 'File';
+        $class = false !== strpos($type, '\\') ? $type : '\\think\\log\\driver\\' . ucwords($type);
+
+        self::$config = $config;
+        unset($config['type']);
+
+        if (class_exists($class)) {
+            self::$driver = new $class($config);
+        } else {
+            throw new ClassNotFoundException('class not exists:' . $class, $class);
+        }
+
+        // 记录初始化信息
+        App::$debug && Log::record('[ LOG ] INIT ' . $type, 'info');
+    }
+
+    /**
+     * 获取日志信息
+     * @access public
+     * @param  string $type 信息类型
+     * @return array|string
+     */
+    public static function getLog($type = '')
+    {
+        return $type ? self::$log[$type] : self::$log;
+    }
+
+    /**
+     * 记录调试信息
+     * @access public
+     * @param  mixed  $msg  调试信息
+     * @param  string $type 信息类型
+     * @return void
+     */
+    public static function record($msg, $type = 'log')
+    {
+        self::$log[$type][] = $msg;
+
+        // 命令行下面日志写入改进
+        IS_CLI && self::save();
+    }
+
+    /**
+     * 清空日志信息
+     * @access public
+     * @return void
+     */
+    public static function clear()
+    {
+        self::$log = [];
+    }
+
+    /**
+     * 设置当前日志记录的授权 key
+     * @access public
+     * @param  string $key 授权 key
+     * @return void
+     */
+    public static function key($key)
+    {
+        self::$key = $key;
+    }
+
+    /**
+     * 检查日志写入权限
+     * @access public
+     * @param  array $config 当前日志配置参数
+     * @return bool
+     */
+    public static function check($config)
+    {
+        return !self::$key || empty($config['allow_key']) || in_array(self::$key, $config['allow_key']);
+    }
+
+    /**
+     * 保存调试信息
+     * @access public
+     * @return bool
+     */
+    public static function save()
+    {
+        // 没有需要保存的记录则直接返回
+        if (empty(self::$log)) {
+            return true;
+        }
+
+        is_null(self::$driver) && self::init(Config::get('log'));
+
+        // 检测日志写入权限
+        if (!self::check(self::$config)) {
+            return false;
+        }
+
+        if (empty(self::$config['level'])) {
+            // 获取全部日志
+            $log = self::$log;
+            if (!App::$debug && isset($log['debug'])) {
+                unset($log['debug']);
+            }
+        } else {
+            // 记录允许级别
+            $log = [];
+            foreach (self::$config['level'] as $level) {
+                if (isset(self::$log[$level])) {
+                    $log[$level] = self::$log[$level];
+                }
+            }
+        }
+
+        if ($result = self::$driver->save($log)) {
+            self::$log = [];
+        }
+
+        Hook::listen('log_write_done', $log);
+
+        return $result;
+    }
+
+    /**
+     * 实时写入日志信息 并支持行为
+     * @access public
+     * @param  mixed  $msg   调试信息
+     * @param  string $type  信息类型
+     * @param  bool   $force 是否强制写入
+     * @return bool
+     */
+    public static function write($msg, $type = 'log', $force = false)
+    {
+        $log = self::$log;
+
+        // 如果不是强制写入,而且信息类型不在可记录的类别中则直接返回 false 不做记录
+        if (true !== $force && !empty(self::$config['level']) && !in_array($type, self::$config['level'])) {
+            return false;
+        }
+
+        // 封装日志信息
+        $log[$type][] = $msg;
+
+        // 监听 log_write
+        Hook::listen('log_write', $log);
+
+        is_null(self::$driver) && self::init(Config::get('log'));
+
+        // 写入日志
+        if ($result = self::$driver->save($log)) {
+            self::$log = [];
+        }
+
+        return $result;
+    }
+
+    /**
+     * 静态方法调用
+     * @access public
+     * @param  string $method 调用方法
+     * @param  mixed  $args   参数
+     * @return void
+     */
+    public static function __callStatic($method, $args)
+    {
+        if (in_array($method, self::$type)) {
+            array_push($args, $method);
+
+            call_user_func_array('\\think\\Log::record', $args);
+        }
+    }
+
+}

+ 2334 - 0
thinkphp/library/think/Model.php

@@ -0,0 +1,2334 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use BadMethodCallException;
+use InvalidArgumentException;
+use think\db\Query;
+use think\exception\ValidateException;
+use think\model\Collection as ModelCollection;
+use think\model\Relation;
+use think\model\relation\BelongsTo;
+use think\model\relation\BelongsToMany;
+use think\model\relation\HasMany;
+use think\model\relation\HasManyThrough;
+use think\model\relation\HasOne;
+use think\model\relation\MorphMany;
+use think\model\relation\MorphOne;
+use think\model\relation\MorphTo;
+
+/**
+ * Class Model
+ * @package think
+ * @mixin Query
+ */
+abstract class Model implements \JsonSerializable, \ArrayAccess
+{
+    // 数据库查询对象池
+    protected static $links = [];
+    // 数据库配置
+    protected $connection = [];
+    // 父关联模型对象
+    protected $parent;
+    // 数据库查询对象
+    protected $query;
+    // 当前模型名称
+    protected $name;
+    // 数据表名称
+    protected $table;
+    // 当前类名称
+    protected $class;
+    // 回调事件
+    private static $event = [];
+    // 错误信息
+    protected $error;
+    // 字段验证规则
+    protected $validate;
+    // 数据表主键 复合主键使用数组定义 不设置则自动获取
+    protected $pk;
+    // 数据表字段信息 留空则自动获取
+    protected $field = [];
+    // 数据排除字段
+    protected $except = [];
+    // 数据废弃字段
+    protected $disuse = [];
+    // 只读字段
+    protected $readonly = [];
+    // 显示属性
+    protected $visible = [];
+    // 隐藏属性
+    protected $hidden = [];
+    // 追加属性
+    protected $append = [];
+    // 数据信息
+    protected $data = [];
+    // 原始数据
+    protected $origin = [];
+    // 关联模型
+    protected $relation = [];
+
+    // 保存自动完成列表
+    protected $auto = [];
+    // 新增自动完成列表
+    protected $insert = [];
+    // 更新自动完成列表
+    protected $update = [];
+    // 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型
+    protected $autoWriteTimestamp;
+    // 创建时间字段
+    protected $createTime = 'create_time';
+    // 更新时间字段
+    protected $updateTime = 'update_time';
+    // 时间字段取出后的默认时间格式
+    protected $dateFormat;
+    // 字段类型或者格式转换
+    protected $type = [];
+    // 是否为更新数据
+    protected $isUpdate = false;
+    // 是否强制更新所有数据
+    protected $force = false;
+    // 更新条件
+    protected $updateWhere;
+    // 验证失败是否抛出异常
+    protected $failException = false;
+    // 全局查询范围
+    protected $useGlobalScope = true;
+    // 是否采用批量验证
+    protected $batchValidate = false;
+    // 查询数据集对象
+    protected $resultSetType;
+    // 关联自动写入
+    protected $relationWrite;
+
+    /**
+     * 初始化过的模型.
+     *
+     * @var array
+     */
+    protected static $initialized = [];
+
+    /**
+     * 是否从主库读取(主从分布式有效)
+     * @var array
+     */
+    protected static $readMaster;
+
+    /**
+     * 构造方法
+     * @access public
+     * @param array|object $data 数据
+     */
+    public function __construct($data = [])
+    {
+        if (is_object($data)) {
+            $this->data = get_object_vars($data);
+        } else {
+            $this->data = $data;
+        }
+
+        if ($this->disuse) {
+            // 废弃字段
+            foreach ((array) $this->disuse as $key) {
+                if (array_key_exists($key, $this->data)) {
+                    unset($this->data[$key]);
+                }
+            }
+        }
+
+        // 记录原始数据
+        $this->origin = $this->data;
+
+        // 当前类名
+        $this->class = get_called_class();
+
+        if (empty($this->name)) {
+            // 当前模型名
+            $name       = str_replace('\\', '/', $this->class);
+            $this->name = basename($name);
+            if (Config::get('class_suffix')) {
+                $suffix     = basename(dirname($name));
+                $this->name = substr($this->name, 0, -strlen($suffix));
+            }
+        }
+
+        if (is_null($this->autoWriteTimestamp)) {
+            // 自动写入时间戳
+            $this->autoWriteTimestamp = $this->getQuery()->getConfig('auto_timestamp');
+        }
+
+        if (is_null($this->dateFormat)) {
+            // 设置时间戳格式
+            $this->dateFormat = $this->getQuery()->getConfig('datetime_format');
+        }
+
+        if (is_null($this->resultSetType)) {
+            $this->resultSetType = $this->getQuery()->getConfig('resultset_type');
+        }
+        // 执行初始化操作
+        $this->initialize();
+    }
+
+    /**
+     * 是否从主库读取数据(主从分布有效)
+     * @access public
+     * @param  bool     $all 是否所有模型生效
+     * @return $this
+     */
+    public function readMaster($all = false)
+    {
+        $model = $all ? '*' : $this->class;
+
+        static::$readMaster[$model] = true;
+        return $this;
+    }
+
+    /**
+     * 创建模型的查询对象
+     * @access protected
+     * @return Query
+     */
+    protected function buildQuery()
+    {
+        // 合并数据库配置
+        if (!empty($this->connection)) {
+            if (is_array($this->connection)) {
+                $connection = array_merge(Config::get('database'), $this->connection);
+            } else {
+                $connection = $this->connection;
+            }
+        } else {
+            $connection = [];
+        }
+
+        $con = Db::connect($connection);
+        // 设置当前模型 确保查询返回模型对象
+        $queryClass = $this->query ?: $con->getConfig('query');
+        $query      = new $queryClass($con, $this);
+
+        if (isset(static::$readMaster['*']) || isset(static::$readMaster[$this->class])) {
+            $query->master(true);
+        }
+
+        // 设置当前数据表和模型名
+        if (!empty($this->table)) {
+            $query->setTable($this->table);
+        } else {
+            $query->name($this->name);
+        }
+
+        if (!empty($this->pk)) {
+            $query->pk($this->pk);
+        }
+
+        return $query;
+    }
+
+    /**
+     * 创建新的模型实例
+     * @access public
+     * @param  array|object $data 数据
+     * @param  bool         $isUpdate 是否为更新
+     * @param  mixed        $where 更新条件
+     * @return Model
+     */
+    public function newInstance($data = [], $isUpdate = false, $where = null)
+    {
+        return (new static($data))->isUpdate($isUpdate, $where);
+    }
+
+    /**
+     * 获取当前模型的查询对象
+     * @access public
+     * @param bool      $buildNewQuery  创建新的查询对象
+     * @return Query
+     */
+    public function getQuery($buildNewQuery = false)
+    {
+        if ($buildNewQuery) {
+            return $this->buildQuery();
+        } elseif (!isset(self::$links[$this->class])) {
+            // 创建模型查询对象
+            self::$links[$this->class] = $this->buildQuery();
+        }
+
+        return self::$links[$this->class];
+    }
+
+    /**
+     * 获取当前模型的数据库查询对象
+     * @access public
+     * @param bool $useBaseQuery 是否调用全局查询范围
+     * @param bool $buildNewQuery 创建新的查询对象
+     * @return Query
+     */
+    public function db($useBaseQuery = true, $buildNewQuery = true)
+    {
+        $query = $this->getQuery($buildNewQuery);
+
+        // 全局作用域
+        if ($useBaseQuery && method_exists($this, 'base')) {
+            call_user_func_array([$this, 'base'], [ & $query]);
+        }
+
+        // 返回当前模型的数据库查询对象
+        return $query;
+    }
+
+    /**
+     *  初始化模型
+     * @access protected
+     * @return void
+     */
+    protected function initialize()
+    {
+        $class = get_class($this);
+        if (!isset(static::$initialized[$class])) {
+            static::$initialized[$class] = true;
+            static::init();
+        }
+    }
+
+    /**
+     * 初始化处理
+     * @access protected
+     * @return void
+     */
+    protected static function init()
+    {
+    }
+
+    /**
+     * 设置父关联对象
+     * @access public
+     * @param Model $model  模型对象
+     * @return $this
+     */
+    public function setParent($model)
+    {
+        $this->parent = $model;
+        return $this;
+    }
+
+    /**
+     * 获取父关联对象
+     * @access public
+     * @return Model
+     */
+    public function getParent()
+    {
+        return $this->parent;
+    }
+
+    /**
+     * 设置数据对象值
+     * @access public
+     * @param mixed $data  数据或者属性名
+     * @param mixed $value 值
+     * @return $this
+     */
+    public function data($data, $value = null)
+    {
+        if (is_string($data)) {
+            $this->data[$data] = $value;
+        } else {
+            // 清空数据
+            $this->data = [];
+            if (is_object($data)) {
+                $data = get_object_vars($data);
+            }
+            if (true === $value) {
+                // 数据对象赋值
+                foreach ($data as $key => $value) {
+                    $this->setAttr($key, $value, $data);
+                }
+            } else {
+                $this->data = $data;
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * 获取对象原始数据 如果不存在指定字段返回false
+     * @access public
+     * @param string $name 字段名 留空获取全部
+     * @return mixed
+     * @throws InvalidArgumentException
+     */
+    public function getData($name = null)
+    {
+        if (is_null($name)) {
+            return $this->data;
+        } elseif (array_key_exists($name, $this->data)) {
+            return $this->data[$name];
+        } elseif (array_key_exists($name, $this->relation)) {
+            return $this->relation[$name];
+        } else {
+            throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name);
+        }
+    }
+
+    /**
+     * 是否需要自动写入时间字段
+     * @access public
+     * @param bool $auto
+     * @return $this
+     */
+    public function isAutoWriteTimestamp($auto)
+    {
+        $this->autoWriteTimestamp = $auto;
+        return $this;
+    }
+
+    /**
+     * 更新是否强制写入数据 而不做比较
+     * @access public
+     * @param bool $force
+     * @return $this
+     */
+    public function force($force = true)
+    {
+        $this->force = $force;
+        return $this;
+    }
+
+    /**
+     * 修改器 设置数据对象值
+     * @access public
+     * @param string $name  属性名
+     * @param mixed  $value 属性值
+     * @param array  $data  数据
+     * @return $this
+     */
+    public function setAttr($name, $value, $data = [])
+    {
+        if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
+            // 自动写入的时间戳字段
+            $value = $this->autoWriteTimestamp($name);
+        } else {
+            // 检测修改器
+            $method = 'set' . Loader::parseName($name, 1) . 'Attr';
+            if (method_exists($this, $method)) {
+                $value = $this->$method($value, array_merge($this->data, $data), $this->relation);
+            } elseif (isset($this->type[$name])) {
+                // 类型转换
+                $value = $this->writeTransform($value, $this->type[$name]);
+            }
+        }
+
+        // 设置数据对象属性
+        $this->data[$name] = $value;
+        return $this;
+    }
+
+    /**
+     * 获取当前模型的关联模型数据
+     * @access public
+     * @param string $name 关联方法名
+     * @return mixed
+     */
+    public function getRelation($name = null)
+    {
+        if (is_null($name)) {
+            return $this->relation;
+        } elseif (array_key_exists($name, $this->relation)) {
+            return $this->relation[$name];
+        } else {
+            return;
+        }
+    }
+
+    /**
+     * 设置关联数据对象值
+     * @access public
+     * @param string $name  属性名
+     * @param mixed  $value 属性值
+     * @return $this
+     */
+    public function setRelation($name, $value)
+    {
+        $this->relation[$name] = $value;
+        return $this;
+    }
+
+    /**
+     * 自动写入时间戳
+     * @access public
+     * @param string $name 时间戳字段
+     * @return mixed
+     */
+    protected function autoWriteTimestamp($name)
+    {
+        if (isset($this->type[$name])) {
+            $type = $this->type[$name];
+            if (strpos($type, ':')) {
+                list($type, $param) = explode(':', $type, 2);
+            }
+            switch ($type) {
+                case 'datetime':
+                case 'date':
+                    $format = !empty($param) ? $param : $this->dateFormat;
+                    $value  = $this->formatDateTime(time(), $format);
+                    break;
+                case 'timestamp':
+                case 'integer':
+                default:
+                    $value = time();
+                    break;
+            }
+        } elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
+            'datetime',
+            'date',
+            'timestamp',
+        ])
+        ) {
+            $value = $this->formatDateTime(time(), $this->dateFormat);
+        } else {
+            $value = $this->formatDateTime(time(), $this->dateFormat, true);
+        }
+        return $value;
+    }
+
+    /**
+     * 时间日期字段格式化处理
+     * @access public
+     * @param mixed $time      时间日期表达式
+     * @param mixed $format    日期格式
+     * @param bool  $timestamp 是否进行时间戳转换
+     * @return mixed
+     */
+    protected function formatDateTime($time, $format, $timestamp = false)
+    {
+        if (false !== strpos($format, '\\')) {
+            $time = new $format($time);
+        } elseif (!$timestamp && false !== $format) {
+            $time = date($format, $time);
+        }
+        return $time;
+    }
+
+    /**
+     * 数据写入 类型转换
+     * @access public
+     * @param mixed        $value 值
+     * @param string|array $type  要转换的类型
+     * @return mixed
+     */
+    protected function writeTransform($value, $type)
+    {
+        if (is_null($value)) {
+            return;
+        }
+
+        if (is_array($type)) {
+            list($type, $param) = $type;
+        } elseif (strpos($type, ':')) {
+            list($type, $param) = explode(':', $type, 2);
+        }
+        switch ($type) {
+            case 'integer':
+                $value = (int) $value;
+                break;
+            case 'float':
+                if (empty($param)) {
+                    $value = (float) $value;
+                } else {
+                    $value = (float) number_format($value, $param, '.', '');
+                }
+                break;
+            case 'boolean':
+                $value = (bool) $value;
+                break;
+            case 'timestamp':
+                if (!is_numeric($value)) {
+                    $value = strtotime($value);
+                }
+                break;
+            case 'datetime':
+                $format = !empty($param) ? $param : $this->dateFormat;
+                $value  = is_numeric($value) ? $value : strtotime($value);
+                $value  = $this->formatDateTime($value, $format);
+                break;
+            case 'object':
+                if (is_object($value)) {
+                    $value = json_encode($value, JSON_FORCE_OBJECT);
+                }
+                break;
+            case 'array':
+                $value = (array) $value;
+            case 'json':
+                $option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE;
+                $value  = json_encode($value, $option);
+                break;
+            case 'serialize':
+                $value = serialize($value);
+                break;
+
+        }
+        return $value;
+    }
+
+    /**
+     * 获取器 获取数据对象的值
+     * @access public
+     * @param string $name 名称
+     * @return mixed
+     * @throws InvalidArgumentException
+     */
+    public function getAttr($name)
+    {
+        try {
+            $notFound = false;
+            $value    = $this->getData($name);
+        } catch (InvalidArgumentException $e) {
+            $notFound = true;
+            $value    = null;
+        }
+
+        // 检测属性获取器
+        $method = 'get' . Loader::parseName($name, 1) . 'Attr';
+        if (method_exists($this, $method)) {
+            $value = $this->$method($value, $this->data, $this->relation);
+        } elseif (isset($this->type[$name])) {
+            // 类型转换
+            $value = $this->readTransform($value, $this->type[$name]);
+        } elseif (in_array($name, [$this->createTime, $this->updateTime])) {
+            if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
+                'datetime',
+                'date',
+                'timestamp',
+            ])
+            ) {
+                $value = $this->formatDateTime(strtotime($value), $this->dateFormat);
+            } else {
+                $value = $this->formatDateTime($value, $this->dateFormat);
+            }
+        } elseif ($notFound) {
+            $relation = Loader::parseName($name, 1, false);
+            if (method_exists($this, $relation)) {
+                $modelRelation = $this->$relation();
+                // 不存在该字段 获取关联数据
+                $value = $this->getRelationData($modelRelation);
+                // 保存关联对象值
+                $this->relation[$name] = $value;
+            } else {
+                throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name);
+            }
+        }
+        return $value;
+    }
+
+    /**
+     * 获取关联模型数据
+     * @access public
+     * @param Relation        $modelRelation 模型关联对象
+     * @return mixed
+     * @throws BadMethodCallException
+     */
+    protected function getRelationData(Relation $modelRelation)
+    {
+        if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
+            $value = $this->parent;
+        } else {
+            // 首先获取关联数据
+            if (method_exists($modelRelation, 'getRelation')) {
+                $value = $modelRelation->getRelation();
+            } else {
+                throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation');
+            }
+        }
+        return $value;
+    }
+
+    /**
+     * 数据读取 类型转换
+     * @access public
+     * @param mixed        $value 值
+     * @param string|array $type  要转换的类型
+     * @return mixed
+     */
+    protected function readTransform($value, $type)
+    {
+        if (is_null($value)) {
+            return;
+        }
+
+        if (is_array($type)) {
+            list($type, $param) = $type;
+        } elseif (strpos($type, ':')) {
+            list($type, $param) = explode(':', $type, 2);
+        }
+        switch ($type) {
+            case 'integer':
+                $value = (int) $value;
+                break;
+            case 'float':
+                if (empty($param)) {
+                    $value = (float) $value;
+                } else {
+                    $value = (float) number_format($value, $param, '.', '');
+                }
+                break;
+            case 'boolean':
+                $value = (bool) $value;
+                break;
+            case 'timestamp':
+                if (!is_null($value)) {
+                    $format = !empty($param) ? $param : $this->dateFormat;
+                    $value  = $this->formatDateTime($value, $format);
+                }
+                break;
+            case 'datetime':
+                if (!is_null($value)) {
+                    $format = !empty($param) ? $param : $this->dateFormat;
+                    $value  = $this->formatDateTime(strtotime($value), $format);
+                }
+                break;
+            case 'json':
+                $value = json_decode($value, true);
+                break;
+            case 'array':
+                $value = empty($value) ? [] : json_decode($value, true);
+                break;
+            case 'object':
+                $value = empty($value) ? new \stdClass() : json_decode($value);
+                break;
+            case 'serialize':
+                try {
+                    $value = unserialize($value);
+                } catch (\Exception $e) {
+                    $value = null;
+                }
+                break;
+            default:
+                if (false !== strpos($type, '\\')) {
+                    // 对象类型
+                    $value = new $type($value);
+                }
+        }
+        return $value;
+    }
+
+    /**
+     * 设置需要追加的输出属性
+     * @access public
+     * @param array $append   属性列表
+     * @param bool  $override 是否覆盖
+     * @return $this
+     */
+    public function append($append = [], $override = false)
+    {
+        $this->append = $override ? $append : array_merge($this->append, $append);
+        return $this;
+    }
+
+    /**
+     * 设置附加关联对象的属性
+     * @access public
+     * @param string       $relation 关联方法
+     * @param string|array $append   追加属性名
+     * @return $this
+     * @throws Exception
+     */
+    public function appendRelationAttr($relation, $append)
+    {
+        if (is_string($append)) {
+            $append = explode(',', $append);
+        }
+
+        $relation = Loader::parseName($relation, 1, false);
+
+        // 获取关联数据
+        if (isset($this->relation[$relation])) {
+            $model = $this->relation[$relation];
+        } else {
+            $model = $this->getRelationData($this->$relation());
+        }
+
+        if ($model instanceof Model) {
+            foreach ($append as $key => $attr) {
+                $key = is_numeric($key) ? $attr : $key;
+                if (isset($this->data[$key])) {
+                    throw new Exception('bind attr has exists:' . $key);
+                } else {
+                    $this->data[$key] = $model->getAttr($attr);
+                }
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * 设置需要隐藏的输出属性
+     * @access public
+     * @param array $hidden   属性列表
+     * @param bool  $override 是否覆盖
+     * @return $this
+     */
+    public function hidden($hidden = [], $override = false)
+    {
+        $this->hidden = $override ? $hidden : array_merge($this->hidden, $hidden);
+        return $this;
+    }
+
+    /**
+     * 设置需要输出的属性
+     * @access public
+     * @param array $visible
+     * @param bool  $override 是否覆盖
+     * @return $this
+     */
+    public function visible($visible = [], $override = false)
+    {
+        $this->visible = $override ? $visible : array_merge($this->visible, $visible);
+        return $this;
+    }
+
+    /**
+     * 解析隐藏及显示属性
+     * @access protected
+     * @param array $attrs  属性
+     * @param array $result 结果集
+     * @param bool  $visible
+     * @return array
+     */
+    protected function parseAttr($attrs, &$result, $visible = true)
+    {
+        $array = [];
+        foreach ($attrs as $key => $val) {
+            if (is_array($val)) {
+                if ($visible) {
+                    $array[] = $key;
+                }
+                $result[$key] = $val;
+            } elseif (strpos($val, '.')) {
+                list($key, $name) = explode('.', $val);
+                if ($visible) {
+                    $array[] = $key;
+                }
+                $result[$key][] = $name;
+            } else {
+                $array[] = $val;
+            }
+        }
+        return $array;
+    }
+
+    /**
+     * 转换子模型对象
+     * @access protected
+     * @param Model|ModelCollection $model
+     * @param                  $visible
+     * @param                  $hidden
+     * @param                  $key
+     * @return array
+     */
+    protected function subToArray($model, $visible, $hidden, $key)
+    {
+        // 关联模型对象
+        if (isset($visible[$key])) {
+            $model->visible($visible[$key]);
+        } elseif (isset($hidden[$key])) {
+            $model->hidden($hidden[$key]);
+        }
+        return $model->toArray();
+    }
+
+    /**
+     * 转换当前模型对象为数组
+     * @access public
+     * @return array
+     */
+    public function toArray()
+    {
+        $item    = [];
+        $visible = [];
+        $hidden  = [];
+
+        $data = array_merge($this->data, $this->relation);
+
+        // 过滤属性
+        if (!empty($this->visible)) {
+            $array = $this->parseAttr($this->visible, $visible);
+            $data  = array_intersect_key($data, array_flip($array));
+        } elseif (!empty($this->hidden)) {
+            $array = $this->parseAttr($this->hidden, $hidden, false);
+            $data  = array_diff_key($data, array_flip($array));
+        }
+
+        foreach ($data as $key => $val) {
+            if ($val instanceof Model || $val instanceof ModelCollection) {
+                // 关联模型对象
+                $item[$key] = $this->subToArray($val, $visible, $hidden, $key);
+            } elseif (is_array($val) && reset($val) instanceof Model) {
+                // 关联模型数据集
+                $arr = [];
+                foreach ($val as $k => $value) {
+                    $arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
+                }
+                $item[$key] = $arr;
+            } else {
+                // 模型属性
+                $item[$key] = $this->getAttr($key);
+            }
+        }
+        // 追加属性(必须定义获取器)
+        if (!empty($this->append)) {
+            foreach ($this->append as $key => $name) {
+                if (is_array($name)) {
+                    // 追加关联对象属性
+                    $relation   = $this->getAttr($key);
+                    $item[$key] = $relation->append($name)->toArray();
+                } elseif (strpos($name, '.')) {
+                    list($key, $attr) = explode('.', $name);
+                    // 追加关联对象属性
+                    $relation   = $this->getAttr($key);
+                    $item[$key] = $relation->append([$attr])->toArray();
+                } else {
+                    $relation = Loader::parseName($name, 1, false);
+                    if (method_exists($this, $relation)) {
+                        $modelRelation = $this->$relation();
+                        $value         = $this->getRelationData($modelRelation);
+
+                        if (method_exists($modelRelation, 'getBindAttr')) {
+                            $bindAttr = $modelRelation->getBindAttr();
+                            if ($bindAttr) {
+                                foreach ($bindAttr as $key => $attr) {
+                                    $key = is_numeric($key) ? $attr : $key;
+                                    if (isset($this->data[$key])) {
+                                        throw new Exception('bind attr has exists:' . $key);
+                                    } else {
+                                        $item[$key] = $value ? $value->getAttr($attr) : null;
+                                    }
+                                }
+                                continue;
+                            }
+                        }
+                        $item[$name] = $value;
+                    } else {
+                        $item[$name] = $this->getAttr($name);
+                    }
+                }
+            }
+        }
+        return !empty($item) ? $item : [];
+    }
+
+    /**
+     * 转换当前模型对象为JSON字符串
+     * @access public
+     * @param integer $options json参数
+     * @return string
+     */
+    public function toJson($options = JSON_UNESCAPED_UNICODE)
+    {
+        return json_encode($this->toArray(), $options);
+    }
+
+    /**
+     * 移除当前模型的关联属性
+     * @access public
+     * @return $this
+     */
+    public function removeRelation()
+    {
+        $this->relation = [];
+        return $this;
+    }
+
+    /**
+     * 转换当前模型数据集为数据集对象
+     * @access public
+     * @param array|\think\Collection $collection 数据集
+     * @return \think\Collection
+     */
+    public function toCollection($collection)
+    {
+        if ($this->resultSetType) {
+            if ('collection' == $this->resultSetType) {
+                $collection = new ModelCollection($collection);
+            } elseif (false !== strpos($this->resultSetType, '\\')) {
+                $class      = $this->resultSetType;
+                $collection = new $class($collection);
+            }
+        }
+        return $collection;
+    }
+
+    /**
+     * 关联数据一起更新
+     * @access public
+     * @param mixed $relation 关联
+     * @return $this
+     */
+    public function together($relation)
+    {
+        if (is_string($relation)) {
+            $relation = explode(',', $relation);
+        }
+        $this->relationWrite = $relation;
+        return $this;
+    }
+
+    /**
+     * 获取模型对象的主键
+     * @access public
+     * @param string $name 模型名
+     * @return mixed
+     */
+    public function getPk($name = '')
+    {
+        if (!empty($name)) {
+            $table = $this->getQuery()->getTable($name);
+            return $this->getQuery()->getPk($table);
+        } elseif (empty($this->pk)) {
+            $this->pk = $this->getQuery()->getPk();
+        }
+        return $this->pk;
+    }
+
+    /**
+     * 判断一个字段名是否为主键字段
+     * @access public
+     * @param string $key 名称
+     * @return bool
+     */
+    protected function isPk($key)
+    {
+        $pk = $this->getPk();
+        if (is_string($pk) && $pk == $key) {
+            return true;
+        } elseif (is_array($pk) && in_array($key, $pk)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 保存当前数据对象
+     * @access public
+     * @param array  $data     数据
+     * @param array  $where    更新条件
+     * @param string $sequence 自增序列名
+     * @return integer|false
+     */
+    public function save($data = [], $where = [], $sequence = null)
+    {
+        if (is_string($data)) {
+            $sequence = $data;
+            $data     = [];
+        }
+
+        if (!empty($data)) {
+            // 数据自动验证
+            if (!$this->validateData($data)) {
+                return false;
+            }
+            // 数据对象赋值
+            foreach ($data as $key => $value) {
+                $this->setAttr($key, $value, $data);
+            }
+            if (!empty($where)) {
+                $this->isUpdate    = true;
+                $this->updateWhere = $where;
+            }
+        }
+
+        // 自动关联写入
+        if (!empty($this->relationWrite)) {
+            $relation = [];
+            foreach ($this->relationWrite as $key => $name) {
+                if (is_array($name)) {
+                    if (key($name) === 0) {
+                        $relation[$key] = [];
+                        foreach ($name as $val) {
+                            if (isset($this->data[$val])) {
+                                $relation[$key][$val] = $this->data[$val];
+                                unset($this->data[$val]);
+                            }
+                        }
+                    } else {
+                        $relation[$key] = $name;
+                    }
+                } elseif (isset($this->relation[$name])) {
+                    $relation[$name] = $this->relation[$name];
+                } elseif (isset($this->data[$name])) {
+                    $relation[$name] = $this->data[$name];
+                    unset($this->data[$name]);
+                }
+            }
+        }
+
+        // 数据自动完成
+        $this->autoCompleteData($this->auto);
+
+        // 事件回调
+        if (false === $this->trigger('before_write', $this)) {
+            return false;
+        }
+        $pk = $this->getPk();
+        if ($this->isUpdate) {
+            // 自动更新
+            $this->autoCompleteData($this->update);
+
+            // 事件回调
+            if (false === $this->trigger('before_update', $this)) {
+                return false;
+            }
+
+            // 获取有更新的数据
+            $data = $this->getChangedData();
+
+            if (empty($data) || (count($data) == 1 && is_string($pk) && isset($data[$pk]))) {
+                // 关联更新
+                if (isset($relation)) {
+                    $this->autoRelationUpdate($relation);
+                }
+                return 0;
+            } elseif ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
+                // 自动写入更新时间
+                $data[$this->updateTime]       = $this->autoWriteTimestamp($this->updateTime);
+                $this->data[$this->updateTime] = $data[$this->updateTime];
+            }
+
+            if (empty($where) && !empty($this->updateWhere)) {
+                $where = $this->updateWhere;
+            }
+
+            // 保留主键数据
+            foreach ($this->data as $key => $val) {
+                if ($this->isPk($key)) {
+                    $data[$key] = $val;
+                }
+            }
+
+            $array = [];
+
+            foreach ((array) $pk as $key) {
+                if (isset($data[$key])) {
+                    $array[$key] = $data[$key];
+                    unset($data[$key]);
+                }
+            }
+
+            if (!empty($array)) {
+                $where = $array;
+            }
+
+            // 检测字段
+            $allowFields = $this->checkAllowField(array_merge($this->auto, $this->update));
+
+            // 模型更新
+            if (!empty($allowFields)) {
+                $result = $this->getQuery()->where($where)->strict(false)->field($allowFields)->update($data);
+            } else {
+                $result = $this->getQuery()->where($where)->update($data);
+            }
+
+            // 关联更新
+            if (isset($relation)) {
+                $this->autoRelationUpdate($relation);
+            }
+
+            // 更新回调
+            $this->trigger('after_update', $this);
+
+        } else {
+            // 自动写入
+            $this->autoCompleteData($this->insert);
+
+            // 自动写入创建时间和更新时间
+            if ($this->autoWriteTimestamp) {
+                if ($this->createTime && !isset($this->data[$this->createTime])) {
+                    $this->data[$this->createTime] = $this->autoWriteTimestamp($this->createTime);
+                }
+                if ($this->updateTime && !isset($this->data[$this->updateTime])) {
+                    $this->data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
+                }
+            }
+
+            if (false === $this->trigger('before_insert', $this)) {
+                return false;
+            }
+
+            // 检测字段
+            $allowFields = $this->checkAllowField(array_merge($this->auto, $this->insert));
+            if (!empty($allowFields)) {
+                $result = $this->getQuery()->strict(false)->field($allowFields)->insert($this->data, false, false, $sequence);
+            } else {
+                $result = $this->getQuery()->insert($this->data, false, false, $sequence);
+            }
+
+            // 获取自动增长主键
+            if ($result && $insertId = $this->getQuery()->getLastInsID($sequence)) {
+                foreach ((array) $pk as $key) {
+                    if (!isset($this->data[$key]) || '' == $this->data[$key]) {
+                        $this->data[$key] = $insertId;
+                    }
+                }
+            }
+
+            // 关联写入
+            if (isset($relation)) {
+                foreach ($relation as $name => $val) {
+                    $method = Loader::parseName($name, 1, false);
+                    $this->$method()->save($val);
+                }
+            }
+
+            // 标记为更新
+            $this->isUpdate = true;
+
+            // 新增回调
+            $this->trigger('after_insert', $this);
+        }
+        // 写入回调
+        $this->trigger('after_write', $this);
+
+        // 重新记录原始数据
+        $this->origin = $this->data;
+
+        return $result;
+    }
+
+    protected function checkAllowField($auto = [])
+    {
+        if (true === $this->field) {
+            $this->field = $this->getQuery()->getTableInfo('', 'fields');
+            $field       = $this->field;
+        } elseif (!empty($this->field)) {
+            $field = array_merge($this->field, $auto);
+            if ($this->autoWriteTimestamp) {
+                array_push($field, $this->createTime, $this->updateTime);
+            }
+        } elseif (!empty($this->except)) {
+            $fields      = $this->getQuery()->getTableInfo('', 'fields');
+            $field       = array_diff($fields, (array) $this->except);
+            $this->field = $field;
+        } else {
+            $field = [];
+        }
+
+        if ($this->disuse) {
+            // 废弃字段
+            $field = array_diff($field, (array) $this->disuse);
+        }
+        return $field;
+    }
+
+    protected function autoRelationUpdate($relation)
+    {
+        foreach ($relation as $name => $val) {
+            if ($val instanceof Model) {
+                $val->save();
+            } else {
+                unset($this->data[$name]);
+                $model = $this->getAttr($name);
+                if ($model instanceof Model) {
+                    $model->save($val);
+                }
+            }
+        }
+    }
+
+    /**
+     * 获取变化的数据 并排除只读数据
+     * @access public
+     * @return array
+     */
+    public function getChangedData()
+    {
+        if ($this->force) {
+            $data = $this->data;
+        } else {
+            $data = array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
+                if ((empty($a) || empty($b)) && $a !== $b) {
+                    return 1;
+                }
+                return is_object($a) || $a != $b ? 1 : 0;
+            });
+        }
+
+        if (!empty($this->readonly)) {
+            // 只读字段不允许更新
+            foreach ($this->readonly as $key => $field) {
+                if (isset($data[$field])) {
+                    unset($data[$field]);
+                }
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * 字段值(延迟)增长
+     * @access public
+     * @param string  $field    字段名
+     * @param integer $step     增长值
+     * @param integer $lazyTime 延时时间(s)
+     * @return integer|true
+     * @throws Exception
+     */
+    public function setInc($field, $step = 1, $lazyTime = 0)
+    {
+        // 更新条件
+        $where = $this->getWhere();
+
+        $result = $this->getQuery()->where($where)->setInc($field, $step, $lazyTime);
+        if (true !== $result) {
+            $this->data[$field] += $step;
+        }
+
+        return $result;
+    }
+
+    /**
+     * 字段值(延迟)增长
+     * @access public
+     * @param string  $field    字段名
+     * @param integer $step     增长值
+     * @param integer $lazyTime 延时时间(s)
+     * @return integer|true
+     * @throws Exception
+     */
+    public function setDec($field, $step = 1, $lazyTime = 0)
+    {
+        // 更新条件
+        $where  = $this->getWhere();
+        $result = $this->getQuery()->where($where)->setDec($field, $step, $lazyTime);
+        if (true !== $result) {
+            $this->data[$field] -= $step;
+        }
+
+        return $result;
+    }
+
+    /**
+     * 获取更新条件
+     * @access protected
+     * @return mixed
+     */
+    protected function getWhere()
+    {
+        // 删除条件
+        $pk = $this->getPk();
+
+        if (is_string($pk) && isset($this->data[$pk])) {
+            $where = [$pk => $this->data[$pk]];
+        } elseif (!empty($this->updateWhere)) {
+            $where = $this->updateWhere;
+        } else {
+            $where = null;
+        }
+        return $where;
+    }
+
+    /**
+     * 保存多个数据到当前数据对象
+     * @access public
+     * @param array   $dataSet 数据
+     * @param boolean $replace 是否自动识别更新和写入
+     * @return array|false
+     * @throws \Exception
+     */
+    public function saveAll($dataSet, $replace = true)
+    {
+        if ($this->validate) {
+            // 数据批量验证
+            $validate = $this->validate;
+            foreach ($dataSet as $data) {
+                if (!$this->validateData($data, $validate)) {
+                    return false;
+                }
+            }
+        }
+
+        $result = [];
+        $db     = $this->getQuery();
+        $db->startTrans();
+        try {
+            $pk = $this->getPk();
+            if (is_string($pk) && $replace) {
+                $auto = true;
+            }
+            foreach ($dataSet as $key => $data) {
+                if ($this->isUpdate || (!empty($auto) && isset($data[$pk]))) {
+                    $result[$key] = self::update($data, [], $this->field);
+                } else {
+                    $result[$key] = self::create($data, $this->field);
+                }
+            }
+            $db->commit();
+            return $this->toCollection($result);
+        } catch (\Exception $e) {
+            $db->rollback();
+            throw $e;
+        }
+    }
+
+    /**
+     * 设置允许写入的字段
+     * @access public
+     * @param string|array $field 允许写入的字段 如果为true只允许写入数据表字段
+     * @return $this
+     */
+    public function allowField($field)
+    {
+        if (is_string($field)) {
+            $field = explode(',', $field);
+        }
+        $this->field = $field;
+        return $this;
+    }
+
+    /**
+     * 设置排除写入的字段
+     * @access public
+     * @param string|array $field 排除允许写入的字段
+     * @return $this
+     */
+    public function except($field)
+    {
+        if (is_string($field)) {
+            $field = explode(',', $field);
+        }
+        $this->except = $field;
+        return $this;
+    }
+
+    /**
+     * 设置只读字段
+     * @access public
+     * @param mixed $field 只读字段
+     * @return $this
+     */
+    public function readonly($field)
+    {
+        if (is_string($field)) {
+            $field = explode(',', $field);
+        }
+        $this->readonly = $field;
+        return $this;
+    }
+
+    /**
+     * 是否为更新数据
+     * @access public
+     * @param bool  $update
+     * @param mixed $where
+     * @return $this
+     */
+    public function isUpdate($update = true, $where = null)
+    {
+        $this->isUpdate = $update;
+        if (!empty($where)) {
+            $this->updateWhere = $where;
+        }
+        return $this;
+    }
+
+    /**
+     * 数据自动完成
+     * @access public
+     * @param array $auto 要自动更新的字段列表
+     * @return void
+     */
+    protected function autoCompleteData($auto = [])
+    {
+        foreach ($auto as $field => $value) {
+            if (is_integer($field)) {
+                $field = $value;
+                $value = null;
+            }
+
+            if (!isset($this->data[$field])) {
+                $default = null;
+            } else {
+                $default = $this->data[$field];
+            }
+
+            $this->setAttr($field, !is_null($value) ? $value : $default);
+        }
+    }
+
+    /**
+     * 删除当前的记录
+     * @access public
+     * @return integer
+     */
+    public function delete()
+    {
+        if (false === $this->trigger('before_delete', $this)) {
+            return false;
+        }
+
+        // 删除条件
+        $where = $this->getWhere();
+
+        // 删除当前模型数据
+        $result = $this->getQuery()->where($where)->delete();
+
+        // 关联删除
+        if (!empty($this->relationWrite)) {
+            foreach ($this->relationWrite as $key => $name) {
+                $name  = is_numeric($key) ? $name : $key;
+                $model = $this->getAttr($name);
+                if ($model instanceof Model) {
+                    $model->delete();
+                }
+            }
+        }
+
+        $this->trigger('after_delete', $this);
+        // 清空原始数据
+        $this->origin = [];
+
+        return $result;
+    }
+
+    /**
+     * 设置自动完成的字段( 规则通过修改器定义)
+     * @access public
+     * @param array $fields 需要自动完成的字段
+     * @return $this
+     */
+    public function auto($fields)
+    {
+        $this->auto = $fields;
+        return $this;
+    }
+
+    /**
+     * 设置字段验证
+     * @access public
+     * @param array|string|bool $rule  验证规则 true表示自动读取验证器类
+     * @param array             $msg   提示信息
+     * @param bool              $batch 批量验证
+     * @return $this
+     */
+    public function validate($rule = true, $msg = [], $batch = false)
+    {
+        if (is_array($rule)) {
+            $this->validate = [
+                'rule' => $rule,
+                'msg'  => $msg,
+            ];
+        } else {
+            $this->validate = true === $rule ? $this->name : $rule;
+        }
+        $this->batchValidate = $batch;
+        return $this;
+    }
+
+    /**
+     * 设置验证失败后是否抛出异常
+     * @access public
+     * @param bool $fail 是否抛出异常
+     * @return $this
+     */
+    public function validateFailException($fail = true)
+    {
+        $this->failException = $fail;
+        return $this;
+    }
+
+    /**
+     * 自动验证数据
+     * @access protected
+     * @param array $data  验证数据
+     * @param mixed $rule  验证规则
+     * @param bool  $batch 批量验证
+     * @return bool
+     */
+    protected function validateData($data, $rule = null, $batch = null)
+    {
+        $info = is_null($rule) ? $this->validate : $rule;
+
+        if (!empty($info)) {
+            if (is_array($info)) {
+                $validate = Loader::validate();
+                $validate->rule($info['rule']);
+                $validate->message($info['msg']);
+            } else {
+                $name = is_string($info) ? $info : $this->name;
+                if (strpos($name, '.')) {
+                    list($name, $scene) = explode('.', $name);
+                }
+                $validate = Loader::validate($name);
+                if (!empty($scene)) {
+                    $validate->scene($scene);
+                }
+            }
+            $batch = is_null($batch) ? $this->batchValidate : $batch;
+
+            if (!$validate->batch($batch)->check($data)) {
+                $this->error = $validate->getError();
+                if ($this->failException) {
+                    throw new ValidateException($this->error);
+                } else {
+                    return false;
+                }
+            }
+            $this->validate = null;
+        }
+        return true;
+    }
+
+    /**
+     * 返回模型的错误信息
+     * @access public
+     * @return string|array
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    /**
+     * 注册回调方法
+     * @access public
+     * @param string   $event    事件名
+     * @param callable $callback 回调方法
+     * @param bool     $override 是否覆盖
+     * @return void
+     */
+    public static function event($event, $callback, $override = false)
+    {
+        $class = get_called_class();
+        if ($override) {
+            self::$event[$class][$event] = [];
+        }
+        self::$event[$class][$event][] = $callback;
+    }
+
+    /**
+     * 触发事件
+     * @access protected
+     * @param string $event  事件名
+     * @param mixed  $params 传入参数(引用)
+     * @return bool
+     */
+    protected function trigger($event, &$params)
+    {
+        if (isset(self::$event[$this->class][$event])) {
+            foreach (self::$event[$this->class][$event] as $callback) {
+                if (is_callable($callback)) {
+                    $result = call_user_func_array($callback, [ & $params]);
+                    if (false === $result) {
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 写入数据
+     * @access public
+     * @param array      $data  数据数组
+     * @param array|true $field 允许字段
+     * @return $this
+     */
+    public static function create($data = [], $field = null)
+    {
+        $model = new static();
+        if (!empty($field)) {
+            $model->allowField($field);
+        }
+        $model->isUpdate(false)->save($data, []);
+        return $model;
+    }
+
+    /**
+     * 更新数据
+     * @access public
+     * @param array      $data  数据数组
+     * @param array      $where 更新条件
+     * @param array|true $field 允许字段
+     * @return $this
+     */
+    public static function update($data = [], $where = [], $field = null)
+    {
+        $model = new static();
+        if (!empty($field)) {
+            $model->allowField($field);
+        }
+        $result = $model->isUpdate(true)->save($data, $where);
+        return $model;
+    }
+
+    /**
+     * 查找单条记录
+     * @access public
+     * @param mixed        $data  主键值或者查询条件(闭包)
+     * @param array|string $with  关联预查询
+     * @param bool         $cache 是否缓存
+     * @return static|null
+     * @throws exception\DbException
+     */
+    public static function get($data, $with = [], $cache = false)
+    {
+        if (is_null($data)) {
+            return;
+        }
+
+        if (true === $with || is_int($with)) {
+            $cache = $with;
+            $with  = [];
+        }
+        $query = static::parseQuery($data, $with, $cache);
+        return $query->find($data);
+    }
+
+    /**
+     * 查找所有记录
+     * @access public
+     * @param mixed        $data  主键列表或者查询条件(闭包)
+     * @param array|string $with  关联预查询
+     * @param bool         $cache 是否缓存
+     * @return static[]|false
+     * @throws exception\DbException
+     */
+    public static function all($data = null, $with = [], $cache = false)
+    {
+        if (true === $with || is_int($with)) {
+            $cache = $with;
+            $with  = [];
+        }
+        $query = static::parseQuery($data, $with, $cache);
+        return $query->select($data);
+    }
+
+    /**
+     * 分析查询表达式
+     * @access public
+     * @param mixed  $data  主键列表或者查询条件(闭包)
+     * @param string $with  关联预查询
+     * @param bool   $cache 是否缓存
+     * @return Query
+     */
+    protected static function parseQuery(&$data, $with, $cache)
+    {
+        $result = self::with($with)->cache($cache);
+        if (is_array($data) && key($data) !== 0) {
+            $result = $result->where($data);
+            $data   = null;
+        } elseif ($data instanceof \Closure) {
+            call_user_func_array($data, [ & $result]);
+            $data = null;
+        } elseif ($data instanceof Query) {
+            $result = $data->with($with)->cache($cache);
+            $data   = null;
+        }
+        return $result;
+    }
+
+    /**
+     * 删除记录
+     * @access public
+     * @param mixed $data 主键列表 支持闭包查询条件
+     * @return integer 成功删除的记录数
+     */
+    public static function destroy($data)
+    {
+        $model = new static();
+        $query = $model->db();
+        if (empty($data) && 0 !== $data) {
+            return 0;
+        } elseif (is_array($data) && key($data) !== 0) {
+            $query->where($data);
+            $data = null;
+        } elseif ($data instanceof \Closure) {
+            call_user_func_array($data, [ & $query]);
+            $data = null;
+        }
+        $resultSet = $query->select($data);
+        $count     = 0;
+        if ($resultSet) {
+            foreach ($resultSet as $data) {
+                $result = $data->delete();
+                $count += $result;
+            }
+        }
+        return $count;
+    }
+
+    /**
+     * 命名范围
+     * @access public
+     * @param string|array|\Closure $name 命名范围名称 逗号分隔
+     * @internal  mixed                 ...$params 参数调用
+     * @return Query
+     */
+    public static function scope($name)
+    {
+        $model  = new static();
+        $query  = $model->db();
+        $params = func_get_args();
+        array_shift($params);
+        array_unshift($params, $query);
+        if ($name instanceof \Closure) {
+            call_user_func_array($name, $params);
+        } elseif (is_string($name)) {
+            $name = explode(',', $name);
+        }
+        if (is_array($name)) {
+            foreach ($name as $scope) {
+                $method = 'scope' . trim($scope);
+                if (method_exists($model, $method)) {
+                    call_user_func_array([$model, $method], $params);
+                }
+            }
+        }
+        return $query;
+    }
+
+    /**
+     * 设置是否使用全局查询范围
+     * @param bool $use 是否启用全局查询范围
+     * @access public
+     * @return Query
+     */
+    public static function useGlobalScope($use)
+    {
+        $model = new static();
+        return $model->db($use);
+    }
+
+    /**
+     * 根据关联条件查询当前模型
+     * @access public
+     * @param string  $relation 关联方法名
+     * @param mixed   $operator 比较操作符
+     * @param integer $count    个数
+     * @param string  $id       关联表的统计字段
+     * @return Relation|Query
+     */
+    public static function has($relation, $operator = '>=', $count = 1, $id = '*')
+    {
+        $relation = (new static())->$relation();
+        if (is_array($operator) || $operator instanceof \Closure) {
+            return $relation->hasWhere($operator);
+        }
+        return $relation->has($operator, $count, $id);
+    }
+
+    /**
+     * 根据关联条件查询当前模型
+     * @access public
+     * @param  string $relation 关联方法名
+     * @param  mixed  $where    查询条件(数组或者闭包)
+     * @param  mixed  $fields   字段
+     * @return Relation|Query
+     */
+    public static function hasWhere($relation, $where = [], $fields = null)
+    {
+        return (new static())->$relation()->hasWhere($where, $fields);
+    }
+
+    /**
+     * 解析模型的完整命名空间
+     * @access public
+     * @param string $model 模型名(或者完整类名)
+     * @return string
+     */
+    protected function parseModel($model)
+    {
+        if (false === strpos($model, '\\')) {
+            $path = explode('\\', get_called_class());
+            array_pop($path);
+            array_push($path, Loader::parseName($model, 1));
+            $model = implode('\\', $path);
+        }
+        return $model;
+    }
+
+    /**
+     * 查询当前模型的关联数据
+     * @access public
+     * @param string|array $relations 关联名
+     * @return $this
+     */
+    public function relationQuery($relations)
+    {
+        if (is_string($relations)) {
+            $relations = explode(',', $relations);
+        }
+
+        foreach ($relations as $key => $relation) {
+            $subRelation = '';
+            $closure     = null;
+            if ($relation instanceof \Closure) {
+                // 支持闭包查询过滤关联条件
+                $closure  = $relation;
+                $relation = $key;
+            }
+            if (is_array($relation)) {
+                $subRelation = $relation;
+                $relation    = $key;
+            } elseif (strpos($relation, '.')) {
+                list($relation, $subRelation) = explode('.', $relation, 2);
+            }
+            $method                = Loader::parseName($relation, 1, false);
+            $this->data[$relation] = $this->$method()->getRelation($subRelation, $closure);
+        }
+        return $this;
+    }
+
+    /**
+     * 预载入关联查询 返回数据集
+     * @access public
+     * @param array  $resultSet 数据集
+     * @param string $relation  关联名
+     * @return array
+     */
+    public function eagerlyResultSet(&$resultSet, $relation)
+    {
+        $relations = is_string($relation) ? explode(',', $relation) : $relation;
+        foreach ($relations as $key => $relation) {
+            $subRelation = '';
+            $closure     = false;
+            if ($relation instanceof \Closure) {
+                $closure  = $relation;
+                $relation = $key;
+            }
+            if (is_array($relation)) {
+                $subRelation = $relation;
+                $relation    = $key;
+            } elseif (strpos($relation, '.')) {
+                list($relation, $subRelation) = explode('.', $relation, 2);
+            }
+            $relation = Loader::parseName($relation, 1, false);
+            $this->$relation()->eagerlyResultSet($resultSet, $relation, $subRelation, $closure);
+        }
+    }
+
+    /**
+     * 预载入关联查询 返回模型对象
+     * @access public
+     * @param Model  $result   数据对象
+     * @param string $relation 关联名
+     * @return Model
+     */
+    public function eagerlyResult(&$result, $relation)
+    {
+        $relations = is_string($relation) ? explode(',', $relation) : $relation;
+
+        foreach ($relations as $key => $relation) {
+            $subRelation = '';
+            $closure     = false;
+            if ($relation instanceof \Closure) {
+                $closure  = $relation;
+                $relation = $key;
+            }
+            if (is_array($relation)) {
+                $subRelation = $relation;
+                $relation    = $key;
+            } elseif (strpos($relation, '.')) {
+                list($relation, $subRelation) = explode('.', $relation, 2);
+            }
+            $relation = Loader::parseName($relation, 1, false);
+            $this->$relation()->eagerlyResult($result, $relation, $subRelation, $closure);
+        }
+    }
+
+    /**
+     * 关联统计
+     * @access public
+     * @param Model        $result   数据对象
+     * @param string|array $relation 关联名
+     * @return void
+     */
+    public function relationCount(&$result, $relation)
+    {
+        $relations = is_string($relation) ? explode(',', $relation) : $relation;
+
+        foreach ($relations as $key => $relation) {
+            $closure = false;
+            if ($relation instanceof \Closure) {
+                $closure  = $relation;
+                $relation = $key;
+            } elseif (is_string($key)) {
+                $name     = $relation;
+                $relation = $key;
+            }
+            $relation = Loader::parseName($relation, 1, false);
+            $count    = $this->$relation()->relationCount($result, $closure);
+            if (!isset($name)) {
+                $name = Loader::parseName($relation) . '_count';
+            }
+            $result->setAttr($name, $count);
+        }
+    }
+
+    /**
+     * 获取模型的默认外键名
+     * @access public
+     * @param string $name 模型名
+     * @return string
+     */
+    protected function getForeignKey($name)
+    {
+        if (strpos($name, '\\')) {
+            $name = basename(str_replace('\\', '/', $name));
+        }
+        return Loader::parseName($name) . '_id';
+    }
+
+    /**
+     * HAS ONE 关联定义
+     * @access public
+     * @param string $model      模型名
+     * @param string $foreignKey 关联外键
+     * @param string $localKey   当前模型主键
+     * @param array  $alias      别名定义(已经废弃)
+     * @param string $joinType   JOIN类型
+     * @return HasOne
+     */
+    public function hasOne($model, $foreignKey = '', $localKey = '', $alias = [], $joinType = 'INNER')
+    {
+        // 记录当前关联信息
+        $model      = $this->parseModel($model);
+        $localKey   = $localKey ?: $this->getPk();
+        $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+        return new HasOne($this, $model, $foreignKey, $localKey, $joinType);
+    }
+
+    /**
+     * BELONGS TO 关联定义
+     * @access public
+     * @param string $model      模型名
+     * @param string $foreignKey 关联外键
+     * @param string $localKey   关联主键
+     * @param array  $alias      别名定义(已经废弃)
+     * @param string $joinType   JOIN类型
+     * @return BelongsTo
+     */
+    public function belongsTo($model, $foreignKey = '', $localKey = '', $alias = [], $joinType = 'INNER')
+    {
+        // 记录当前关联信息
+        $model      = $this->parseModel($model);
+        $foreignKey = $foreignKey ?: $this->getForeignKey($model);
+        $localKey   = $localKey ?: (new $model)->getPk();
+        $trace      = debug_backtrace(false, 2);
+        $relation   = Loader::parseName($trace[1]['function']);
+        return new BelongsTo($this, $model, $foreignKey, $localKey, $joinType, $relation);
+    }
+
+    /**
+     * HAS MANY 关联定义
+     * @access public
+     * @param string $model      模型名
+     * @param string $foreignKey 关联外键
+     * @param string $localKey   当前模型主键
+     * @return HasMany
+     */
+    public function hasMany($model, $foreignKey = '', $localKey = '')
+    {
+        // 记录当前关联信息
+        $model      = $this->parseModel($model);
+        $localKey   = $localKey ?: $this->getPk();
+        $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+        return new HasMany($this, $model, $foreignKey, $localKey);
+    }
+
+    /**
+     * HAS MANY 远程关联定义
+     * @access public
+     * @param string $model      模型名
+     * @param string $through    中间模型名
+     * @param string $foreignKey 关联外键
+     * @param string $throughKey 关联外键
+     * @param string $localKey   当前模型主键
+     * @return HasManyThrough
+     */
+    public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '')
+    {
+        // 记录当前关联信息
+        $model      = $this->parseModel($model);
+        $through    = $this->parseModel($through);
+        $localKey   = $localKey ?: $this->getPk();
+        $foreignKey = $foreignKey ?: $this->getForeignKey($this->name);
+        $throughKey = $throughKey ?: $this->getForeignKey($through);
+        return new HasManyThrough($this, $model, $through, $foreignKey, $throughKey, $localKey);
+    }
+
+    /**
+     * BELONGS TO MANY 关联定义
+     * @access public
+     * @param string $model      模型名
+     * @param string $table      中间表名
+     * @param string $foreignKey 关联外键
+     * @param string $localKey   当前模型关联键
+     * @return BelongsToMany
+     */
+    public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '')
+    {
+        // 记录当前关联信息
+        $model      = $this->parseModel($model);
+        $name       = Loader::parseName(basename(str_replace('\\', '/', $model)));
+        $table      = $table ?: Loader::parseName($this->name) . '_' . $name;
+        $foreignKey = $foreignKey ?: $name . '_id';
+        $localKey   = $localKey ?: $this->getForeignKey($this->name);
+        return new BelongsToMany($this, $model, $table, $foreignKey, $localKey);
+    }
+
+    /**
+     * MORPH  MANY 关联定义
+     * @access public
+     * @param string       $model 模型名
+     * @param string|array $morph 多态字段信息
+     * @param string       $type  多态类型
+     * @return MorphMany
+     */
+    public function morphMany($model, $morph = null, $type = '')
+    {
+        // 记录当前关联信息
+        $model = $this->parseModel($model);
+        if (is_null($morph)) {
+            $trace = debug_backtrace(false, 2);
+            $morph = Loader::parseName($trace[1]['function']);
+        }
+        $type = $type ?: get_class($this);
+        if (is_array($morph)) {
+            list($morphType, $foreignKey) = $morph;
+        } else {
+            $morphType  = $morph . '_type';
+            $foreignKey = $morph . '_id';
+        }
+        return new MorphMany($this, $model, $foreignKey, $morphType, $type);
+    }
+
+    /**
+     * MORPH  One 关联定义
+     * @access public
+     * @param string       $model 模型名
+     * @param string|array $morph 多态字段信息
+     * @param string       $type  多态类型
+     * @return MorphOne
+     */
+    public function morphOne($model, $morph = null, $type = '')
+    {
+        // 记录当前关联信息
+        $model = $this->parseModel($model);
+        if (is_null($morph)) {
+            $trace = debug_backtrace(false, 2);
+            $morph = Loader::parseName($trace[1]['function']);
+        }
+        $type = $type ?: get_class($this);
+        if (is_array($morph)) {
+            list($morphType, $foreignKey) = $morph;
+        } else {
+            $morphType  = $morph . '_type';
+            $foreignKey = $morph . '_id';
+        }
+        return new MorphOne($this, $model, $foreignKey, $morphType, $type);
+    }
+
+    /**
+     * MORPH TO 关联定义
+     * @access public
+     * @param string|array $morph 多态字段信息
+     * @param array        $alias 多态别名定义
+     * @return MorphTo
+     */
+    public function morphTo($morph = null, $alias = [])
+    {
+        $trace    = debug_backtrace(false, 2);
+        $relation = Loader::parseName($trace[1]['function']);
+
+        if (is_null($morph)) {
+            $morph = $relation;
+        }
+        // 记录当前关联信息
+        if (is_array($morph)) {
+            list($morphType, $foreignKey) = $morph;
+        } else {
+            $morphType  = $morph . '_type';
+            $foreignKey = $morph . '_id';
+        }
+        return new MorphTo($this, $morphType, $foreignKey, $alias, $relation);
+    }
+
+    public function __call($method, $args)
+    {
+        $query = $this->db(true, false);
+        if (method_exists($this, 'scope' . $method)) {
+            // 动态调用命名范围
+            $method = 'scope' . $method;
+            array_unshift($args, $query);
+            call_user_func_array([$this, $method], $args);
+            return $this;
+        } else {
+            return call_user_func_array([$query, $method], $args);
+        }
+    }
+
+    public static function __callStatic($method, $args)
+    {
+        $model = new static();
+        $query = $model->db();
+        if (method_exists($model, 'scope' . $method)) {
+            // 动态调用命名范围
+            $method = 'scope' . $method;
+            array_unshift($args, $query);
+
+            call_user_func_array([$model, $method], $args);
+            return $query;
+        } else {
+            return call_user_func_array([$query, $method], $args);
+        }
+    }
+
+    /**
+     * 修改器 设置数据对象的值
+     * @access public
+     * @param string $name  名称
+     * @param mixed  $value 值
+     * @return void
+     */
+    public function __set($name, $value)
+    {
+        $this->setAttr($name, $value);
+    }
+
+    /**
+     * 获取器 获取数据对象的值
+     * @access public
+     * @param string $name 名称
+     * @return mixed
+     */
+    public function __get($name)
+    {
+        return $this->getAttr($name);
+    }
+
+    /**
+     * 检测数据对象的值
+     * @access public
+     * @param string $name 名称
+     * @return boolean
+     */
+    public function __isset($name)
+    {
+        try {
+            if (array_key_exists($name, $this->data) || array_key_exists($name, $this->relation)) {
+                return true;
+            } else {
+                $this->getAttr($name);
+                return true;
+            }
+        } catch (InvalidArgumentException $e) {
+            return false;
+        }
+
+    }
+
+    /**
+     * 销毁数据对象的值
+     * @access public
+     * @param string $name 名称
+     * @return void
+     */
+    public function __unset($name)
+    {
+        unset($this->data[$name], $this->relation[$name]);
+    }
+
+    public function __toString()
+    {
+        return $this->toJson();
+    }
+
+    // JsonSerializable
+    public function jsonSerialize()
+    {
+        return $this->toArray();
+    }
+
+    // ArrayAccess
+    public function offsetSet($name, $value)
+    {
+        $this->setAttr($name, $value);
+    }
+
+    public function offsetExists($name)
+    {
+        return $this->__isset($name);
+    }
+
+    public function offsetUnset($name)
+    {
+        $this->__unset($name);
+    }
+
+    public function offsetGet($name)
+    {
+        return $this->getAttr($name);
+    }
+
+    /**
+     * 解序列化后处理
+     */
+    public function __wakeup()
+    {
+        $this->initialize();
+    }
+
+    /**
+     * 模型事件快捷方法
+     * @param      $callback
+     * @param bool $override
+     */
+    protected static function beforeInsert($callback, $override = false)
+    {
+        self::event('before_insert', $callback, $override);
+    }
+
+    protected static function afterInsert($callback, $override = false)
+    {
+        self::event('after_insert', $callback, $override);
+    }
+
+    protected static function beforeUpdate($callback, $override = false)
+    {
+        self::event('before_update', $callback, $override);
+    }
+
+    protected static function afterUpdate($callback, $override = false)
+    {
+        self::event('after_update', $callback, $override);
+    }
+
+    protected static function beforeWrite($callback, $override = false)
+    {
+        self::event('before_write', $callback, $override);
+    }
+
+    protected static function afterWrite($callback, $override = false)
+    {
+        self::event('after_write', $callback, $override);
+    }
+
+    protected static function beforeDelete($callback, $override = false)
+    {
+        self::event('before_delete', $callback, $override);
+    }
+
+    protected static function afterDelete($callback, $override = false)
+    {
+        self::event('after_delete', $callback, $override);
+    }
+
+}

+ 409 - 0
thinkphp/library/think/Paginator.php

@@ -0,0 +1,409 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: zhangyajun <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use ArrayAccess;
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
+use JsonSerializable;
+use Traversable;
+
+abstract class Paginator implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
+{
+    /** @var bool 是否为简洁模式 */
+    protected $simple = false;
+
+    /** @var Collection 数据集 */
+    protected $items;
+
+    /** @var integer 当前页 */
+    protected $currentPage;
+
+    /** @var  integer 最后一页 */
+    protected $lastPage;
+
+    /** @var integer|null 数据总数 */
+    protected $total;
+
+    /** @var  integer 每页的数量 */
+    protected $listRows;
+
+    /** @var bool 是否有下一页 */
+    protected $hasMore;
+
+    /** @var array 一些配置 */
+    protected $options = [
+        'var_page' => 'page',
+        'path'     => '/',
+        'query'    => [],
+        'fragment' => '',
+    ];
+
+    /** @var mixed simple模式下的下个元素 */
+    protected $nextItem;
+
+    public function __construct($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = [])
+    {
+        $this->options = array_merge($this->options, $options);
+
+        $this->options['path'] = '/' != $this->options['path'] ? rtrim($this->options['path'], '/') : $this->options['path'];
+
+        $this->simple   = $simple;
+        $this->listRows = $listRows;
+
+        if (!$items instanceof Collection) {
+            $items = Collection::make($items);
+        }
+
+        if ($simple) {
+            $this->currentPage = $this->setCurrentPage($currentPage);
+            $this->hasMore     = count($items) > ($this->listRows);
+            if ($this->hasMore) {
+                $this->nextItem = $items->slice($this->listRows, 1);
+            }
+            $items = $items->slice(0, $this->listRows);
+        } else {
+            $this->total       = $total;
+            $this->lastPage    = (int) ceil($total / $listRows);
+            $this->currentPage = $this->setCurrentPage($currentPage);
+            $this->hasMore     = $this->currentPage < $this->lastPage;
+        }
+        $this->items = $items;
+    }
+
+    /**
+     * @param       $items
+     * @param       $listRows
+     * @param null  $currentPage
+     * @param bool  $simple
+     * @param null  $total
+     * @param array $options
+     * @return Paginator
+     */
+    public static function make($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = [])
+    {
+        return new static($items, $listRows, $currentPage, $total, $simple, $options);
+    }
+
+    protected function setCurrentPage($currentPage)
+    {
+        if (!$this->simple && $currentPage > $this->lastPage) {
+            return $this->lastPage > 0 ? $this->lastPage : 1;
+        }
+
+        return $currentPage;
+    }
+
+    /**
+     * 获取页码对应的链接
+     *
+     * @param $page
+     * @return string
+     */
+    protected function url($page)
+    {
+        if ($page <= 0) {
+            $page = 1;
+        }
+
+        if (strpos($this->options['path'], '[PAGE]') === false) {
+            $parameters = [$this->options['var_page'] => $page];
+            $path       = $this->options['path'];
+        } else {
+            $parameters = [];
+            $path       = str_replace('[PAGE]', $page, $this->options['path']);
+        }
+        if (count($this->options['query']) > 0) {
+            $parameters = array_merge($this->options['query'], $parameters);
+        }
+        $url = $path;
+        if (!empty($parameters)) {
+            $url .= '?' . http_build_query($parameters, null, '&');
+        }
+        return $url . $this->buildFragment();
+    }
+
+    /**
+     * 自动获取当前页码
+     * @param string $varPage
+     * @param int    $default
+     * @return int
+     */
+    public static function getCurrentPage($varPage = 'page', $default = 1)
+    {
+        $page = (int) Request::instance()->param($varPage);
+
+        if (filter_var($page, FILTER_VALIDATE_INT) !== false && $page >= 1) {
+            return $page;
+        }
+
+        return $default;
+    }
+
+    /**
+     * 自动获取当前的path
+     * @return string
+     */
+    public static function getCurrentPath()
+    {
+        return Request::instance()->baseUrl();
+    }
+
+    public function total()
+    {
+        if ($this->simple) {
+            throw new \DomainException('not support total');
+        }
+        return $this->total;
+    }
+
+    public function listRows()
+    {
+        return $this->listRows;
+    }
+
+    public function currentPage()
+    {
+        return $this->currentPage;
+    }
+
+    public function lastPage()
+    {
+        if ($this->simple) {
+            throw new \DomainException('not support last');
+        }
+        return $this->lastPage;
+    }
+
+    /**
+     * 数据是否足够分页
+     * @return boolean
+     */
+    public function hasPages()
+    {
+        return !(1 == $this->currentPage && !$this->hasMore);
+    }
+
+    /**
+     * 创建一组分页链接
+     *
+     * @param  int $start
+     * @param  int $end
+     * @return array
+     */
+    public function getUrlRange($start, $end)
+    {
+        $urls = [];
+
+        for ($page = $start; $page <= $end; $page++) {
+            $urls[$page] = $this->url($page);
+        }
+
+        return $urls;
+    }
+
+    /**
+     * 设置URL锚点
+     *
+     * @param  string|null $fragment
+     * @return $this
+     */
+    public function fragment($fragment)
+    {
+        $this->options['fragment'] = $fragment;
+        return $this;
+    }
+
+    /**
+     * 添加URL参数
+     *
+     * @param  array|string $key
+     * @param  string|null  $value
+     * @return $this
+     */
+    public function appends($key, $value = null)
+    {
+        if (!is_array($key)) {
+            $queries = [$key => $value];
+        } else {
+            $queries = $key;
+        }
+
+        foreach ($queries as $k => $v) {
+            if ($k !== $this->options['var_page']) {
+                $this->options['query'][$k] = $v;
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * 构造锚点字符串
+     *
+     * @return string
+     */
+    protected function buildFragment()
+    {
+        return $this->options['fragment'] ? '#' . $this->options['fragment'] : '';
+    }
+
+    /**
+     * 渲染分页html
+     * @return mixed
+     */
+    abstract public function render();
+
+    public function items()
+    {
+        return $this->items->all();
+    }
+
+    public function getCollection()
+    {
+        return $this->items;
+    }
+
+    public function isEmpty()
+    {
+        return $this->items->isEmpty();
+    }
+
+    /**
+     * 给每个元素执行个回调
+     *
+     * @param  callable $callback
+     * @return $this
+     */
+    public function each(callable $callback)
+    {
+        foreach ($this->items as $key => $item) {
+            $result = $callback($item, $key);
+            if (false === $result) {
+                break;
+            } elseif (!is_object($item)) {
+                $this->items[$key] = $result;
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * Retrieve an external iterator
+     * @return Traversable An instance of an object implementing <b>Iterator</b> or
+     * <b>Traversable</b>
+     */
+    public function getIterator()
+    {
+        return new ArrayIterator($this->items->all());
+    }
+
+    /**
+     * Whether a offset exists
+     * @param mixed $offset
+     * @return bool
+     */
+    public function offsetExists($offset)
+    {
+        return $this->items->offsetExists($offset);
+    }
+
+    /**
+     * Offset to retrieve
+     * @param mixed $offset
+     * @return mixed
+     */
+    public function offsetGet($offset)
+    {
+        return $this->items->offsetGet($offset);
+    }
+
+    /**
+     * Offset to set
+     * @param mixed $offset
+     * @param mixed $value
+     */
+    public function offsetSet($offset, $value)
+    {
+        $this->items->offsetSet($offset, $value);
+    }
+
+    /**
+     * Offset to unset
+     * @param mixed $offset
+     * @return void
+     * @since 5.0.0
+     */
+    public function offsetUnset($offset)
+    {
+        $this->items->offsetUnset($offset);
+    }
+
+    /**
+     * Count elements of an object
+     */
+    public function count()
+    {
+        return $this->items->count();
+    }
+
+    public function __toString()
+    {
+        return (string) $this->render();
+    }
+
+    public function toArray()
+    {
+        if ($this->simple) {
+            return [
+                'per_page'     => $this->listRows,
+                'current_page' => $this->currentPage,
+                'has_more'     => $this->hasMore,
+                'next_item'    => $this->nextItem,
+                'data'         => $this->items->toArray(),
+            ];
+        } else {
+            return [
+                'total'        => $this->total,
+                'per_page'     => $this->listRows,
+                'current_page' => $this->currentPage,
+                'last_page'    => $this->lastPage,
+                'data'         => $this->items->toArray(),
+            ];
+        }
+
+    }
+
+    /**
+     * Specify data which should be serialized to JSON
+     */
+    public function jsonSerialize()
+    {
+        return $this->toArray();
+    }
+
+    public function __call($name, $arguments)
+    {
+        $collection = $this->getCollection();
+
+        $result = call_user_func_array([$collection, $name], $arguments);
+
+        if ($result === $collection) {
+            return $this;
+        }
+
+        return $result;
+    }
+
+}

+ 1205 - 0
thinkphp/library/think/Process.php

@@ -0,0 +1,1205 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\process\exception\Failed as ProcessFailedException;
+use think\process\exception\Timeout as ProcessTimeoutException;
+use think\process\pipes\Pipes;
+use think\process\pipes\Unix as UnixPipes;
+use think\process\pipes\Windows as WindowsPipes;
+use think\process\Utils;
+
+class Process
+{
+
+    const ERR = 'err';
+    const OUT = 'out';
+
+    const STATUS_READY      = 'ready';
+    const STATUS_STARTED    = 'started';
+    const STATUS_TERMINATED = 'terminated';
+
+    const STDIN  = 0;
+    const STDOUT = 1;
+    const STDERR = 2;
+
+    const TIMEOUT_PRECISION = 0.2;
+
+    private $callback;
+    private $commandline;
+    private $cwd;
+    private $env;
+    private $input;
+    private $starttime;
+    private $lastOutputTime;
+    private $timeout;
+    private $idleTimeout;
+    private $options;
+    private $exitcode;
+    private $fallbackExitcode;
+    private $processInformation;
+    private $outputDisabled = false;
+    private $stdout;
+    private $stderr;
+    private $enhanceWindowsCompatibility = true;
+    private $enhanceSigchildCompatibility;
+    private $process;
+    private $status                       = self::STATUS_READY;
+    private $incrementalOutputOffset      = 0;
+    private $incrementalErrorOutputOffset = 0;
+    private $tty;
+    private $pty;
+
+    private $useFileHandles = false;
+
+    /** @var Pipes */
+    private $processPipes;
+
+    private $latestSignal;
+
+    private static $sigchild;
+
+    /**
+     * @var array
+     */
+    public static $exitCodes = [
+        0   => 'OK',
+        1   => 'General error',
+        2   => 'Misuse of shell builtins',
+        126 => 'Invoked command cannot execute',
+        127 => 'Command not found',
+        128 => 'Invalid exit argument',
+        // signals
+        129 => 'Hangup',
+        130 => 'Interrupt',
+        131 => 'Quit and dump core',
+        132 => 'Illegal instruction',
+        133 => 'Trace/breakpoint trap',
+        134 => 'Process aborted',
+        135 => 'Bus error: "access to undefined portion of memory object"',
+        136 => 'Floating point exception: "erroneous arithmetic operation"',
+        137 => 'Kill (terminate immediately)',
+        138 => 'User-defined 1',
+        139 => 'Segmentation violation',
+        140 => 'User-defined 2',
+        141 => 'Write to pipe with no one reading',
+        142 => 'Signal raised by alarm',
+        143 => 'Termination (request to terminate)',
+        // 144 - not defined
+        145 => 'Child process terminated, stopped (or continued*)',
+        146 => 'Continue if stopped',
+        147 => 'Stop executing temporarily',
+        148 => 'Terminal stop signal',
+        149 => 'Background process attempting to read from tty ("in")',
+        150 => 'Background process attempting to write to tty ("out")',
+        151 => 'Urgent data available on socket',
+        152 => 'CPU time limit exceeded',
+        153 => 'File size limit exceeded',
+        154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
+        155 => 'Profiling timer expired',
+        // 156 - not defined
+        157 => 'Pollable event',
+        // 158 - not defined
+        159 => 'Bad syscall',
+    ];
+
+    /**
+     * 构造方法
+     * @param string         $commandline 指令
+     * @param string|null    $cwd         工作目录
+     * @param array|null     $env         环境变量
+     * @param string|null    $input       输入
+     * @param int|float|null $timeout     超时时间
+     * @param array          $options     proc_open的选项
+     * @throws \RuntimeException
+     * @api
+     */
+    public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = [])
+    {
+        if (!function_exists('proc_open')) {
+            throw new \RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
+        }
+
+        $this->commandline = $commandline;
+        $this->cwd         = $cwd;
+
+        if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DS)) {
+            $this->cwd = getcwd();
+        }
+        if (null !== $env) {
+            $this->setEnv($env);
+        }
+
+        $this->input = $input;
+        $this->setTimeout($timeout);
+        $this->useFileHandles               = '\\' === DS;
+        $this->pty                          = false;
+        $this->enhanceWindowsCompatibility  = true;
+        $this->enhanceSigchildCompatibility = '\\' !== DS && $this->isSigchildEnabled();
+        $this->options                      = array_replace([
+            'suppress_errors' => true,
+            'binary_pipes'    => true,
+        ], $options);
+    }
+
+    public function __destruct()
+    {
+        $this->stop();
+    }
+
+    public function __clone()
+    {
+        $this->resetProcessData();
+    }
+
+    /**
+     * 运行指令
+     * @param callback|null $callback
+     * @return int
+     */
+    public function run($callback = null)
+    {
+        $this->start($callback);
+
+        return $this->wait();
+    }
+
+    /**
+     * 运行指令
+     * @param callable|null $callback
+     * @return self
+     * @throws \RuntimeException
+     * @throws ProcessFailedException
+     */
+    public function mustRun($callback = null)
+    {
+        if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) {
+            throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
+        }
+
+        if (0 !== $this->run($callback)) {
+            throw new ProcessFailedException($this);
+        }
+
+        return $this;
+    }
+
+    /**
+     * 启动进程并写到 STDIN 输入后返回。
+     * @param callable|null $callback
+     * @throws \RuntimeException
+     * @throws \RuntimeException
+     * @throws \LogicException
+     */
+    public function start($callback = null)
+    {
+        if ($this->isRunning()) {
+            throw new \RuntimeException('Process is already running');
+        }
+        if ($this->outputDisabled && null !== $callback) {
+            throw new \LogicException('Output has been disabled, enable it to allow the use of a callback.');
+        }
+
+        $this->resetProcessData();
+        $this->starttime = $this->lastOutputTime = microtime(true);
+        $this->callback  = $this->buildCallback($callback);
+        $descriptors     = $this->getDescriptors();
+
+        $commandline = $this->commandline;
+
+        if ('\\' === DS && $this->enhanceWindowsCompatibility) {
+            $commandline = 'cmd /V:ON /E:ON /C "(' . $commandline . ')';
+            foreach ($this->processPipes->getFiles() as $offset => $filename) {
+                $commandline .= ' ' . $offset . '>' . Utils::escapeArgument($filename);
+            }
+            $commandline .= '"';
+
+            if (!isset($this->options['bypass_shell'])) {
+                $this->options['bypass_shell'] = true;
+            }
+        }
+
+        $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options);
+
+        if (!is_resource($this->process)) {
+            throw new \RuntimeException('Unable to launch a new process.');
+        }
+        $this->status = self::STATUS_STARTED;
+
+        if ($this->tty) {
+            return;
+        }
+
+        $this->updateStatus(false);
+        $this->checkTimeout();
+    }
+
+    /**
+     * 重启进程
+     * @param callable|null $callback
+     * @return Process
+     * @throws \RuntimeException
+     * @throws \RuntimeException
+     */
+    public function restart($callback = null)
+    {
+        if ($this->isRunning()) {
+            throw new \RuntimeException('Process is already running');
+        }
+
+        $process = clone $this;
+        $process->start($callback);
+
+        return $process;
+    }
+
+    /**
+     * 等待要终止的进程
+     * @param callable|null $callback
+     * @return int
+     */
+    public function wait($callback = null)
+    {
+        $this->requireProcessIsStarted(__FUNCTION__);
+
+        $this->updateStatus(false);
+        if (null !== $callback) {
+            $this->callback = $this->buildCallback($callback);
+        }
+
+        do {
+            $this->checkTimeout();
+            $running = '\\' === DS ? $this->isRunning() : $this->processPipes->areOpen();
+            $close   = '\\' !== DS || !$running;
+            $this->readPipes(true, $close);
+        } while ($running);
+
+        while ($this->isRunning()) {
+            usleep(1000);
+        }
+
+        if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
+            throw new \RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig']));
+        }
+
+        return $this->exitcode;
+    }
+
+    /**
+     * 获取PID
+     * @return int|null
+     * @throws \RuntimeException
+     */
+    public function getPid()
+    {
+        if ($this->isSigchildEnabled()) {
+            throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.');
+        }
+
+        $this->updateStatus(false);
+
+        return $this->isRunning() ? $this->processInformation['pid'] : null;
+    }
+
+    /**
+     * 将一个 POSIX 信号发送到进程中
+     * @param int $signal
+     * @return Process
+     */
+    public function signal($signal)
+    {
+        $this->doSignal($signal, true);
+
+        return $this;
+    }
+
+    /**
+     * 禁用从底层过程获取输出和错误输出。
+     * @return Process
+     */
+    public function disableOutput()
+    {
+        if ($this->isRunning()) {
+            throw new \RuntimeException('Disabling output while the process is running is not possible.');
+        }
+        if (null !== $this->idleTimeout) {
+            throw new \LogicException('Output can not be disabled while an idle timeout is set.');
+        }
+
+        $this->outputDisabled = true;
+
+        return $this;
+    }
+
+    /**
+     * 开启从底层过程获取输出和错误输出。
+     * @return Process
+     * @throws \RuntimeException
+     */
+    public function enableOutput()
+    {
+        if ($this->isRunning()) {
+            throw new \RuntimeException('Enabling output while the process is running is not possible.');
+        }
+
+        $this->outputDisabled = false;
+
+        return $this;
+    }
+
+    /**
+     * 输出是否禁用
+     * @return bool
+     */
+    public function isOutputDisabled()
+    {
+        return $this->outputDisabled;
+    }
+
+    /**
+     * 获取当前的输出管道
+     * @return string
+     * @throws \LogicException
+     * @throws \LogicException
+     * @api
+     */
+    public function getOutput()
+    {
+        if ($this->outputDisabled) {
+            throw new \LogicException('Output has been disabled.');
+        }
+
+        $this->requireProcessIsStarted(__FUNCTION__);
+
+        $this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true);
+
+        return $this->stdout;
+    }
+
+    /**
+     * 以增量方式返回的输出结果。
+     * @return string
+     */
+    public function getIncrementalOutput()
+    {
+        $this->requireProcessIsStarted(__FUNCTION__);
+
+        $data = $this->getOutput();
+
+        $latest = substr($data, $this->incrementalOutputOffset);
+
+        if (false === $latest) {
+            return '';
+        }
+
+        $this->incrementalOutputOffset = strlen($data);
+
+        return $latest;
+    }
+
+    /**
+     * 清空输出
+     * @return Process
+     */
+    public function clearOutput()
+    {
+        $this->stdout                  = '';
+        $this->incrementalOutputOffset = 0;
+
+        return $this;
+    }
+
+    /**
+     * 返回当前的错误输出的过程 (STDERR)。
+     * @return string
+     */
+    public function getErrorOutput()
+    {
+        if ($this->outputDisabled) {
+            throw new \LogicException('Output has been disabled.');
+        }
+
+        $this->requireProcessIsStarted(__FUNCTION__);
+
+        $this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true);
+
+        return $this->stderr;
+    }
+
+    /**
+     * 以增量方式返回 errorOutput
+     * @return string
+     */
+    public function getIncrementalErrorOutput()
+    {
+        $this->requireProcessIsStarted(__FUNCTION__);
+
+        $data = $this->getErrorOutput();
+
+        $latest = substr($data, $this->incrementalErrorOutputOffset);
+
+        if (false === $latest) {
+            return '';
+        }
+
+        $this->incrementalErrorOutputOffset = strlen($data);
+
+        return $latest;
+    }
+
+    /**
+     * 清空 errorOutput
+     * @return Process
+     */
+    public function clearErrorOutput()
+    {
+        $this->stderr                       = '';
+        $this->incrementalErrorOutputOffset = 0;
+
+        return $this;
+    }
+
+    /**
+     * 获取退出码
+     * @return null|int
+     */
+    public function getExitCode()
+    {
+        if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) {
+            throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
+        }
+
+        $this->updateStatus(false);
+
+        return $this->exitcode;
+    }
+
+    /**
+     * 获取退出文本
+     * @return null|string
+     */
+    public function getExitCodeText()
+    {
+        if (null === $exitcode = $this->getExitCode()) {
+            return;
+        }
+
+        return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
+    }
+
+    /**
+     * 检查是否成功
+     * @return bool
+     */
+    public function isSuccessful()
+    {
+        return 0 === $this->getExitCode();
+    }
+
+    /**
+     * 是否未捕获的信号已被终止子进程
+     * @return bool
+     */
+    public function hasBeenSignaled()
+    {
+        $this->requireProcessIsTerminated(__FUNCTION__);
+
+        if ($this->isSigchildEnabled()) {
+            throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
+        }
+
+        $this->updateStatus(false);
+
+        return $this->processInformation['signaled'];
+    }
+
+    /**
+     * 返回导致子进程终止其执行的数。
+     * @return int
+     */
+    public function getTermSignal()
+    {
+        $this->requireProcessIsTerminated(__FUNCTION__);
+
+        if ($this->isSigchildEnabled()) {
+            throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
+        }
+
+        $this->updateStatus(false);
+
+        return $this->processInformation['termsig'];
+    }
+
+    /**
+     * 检查子进程信号是否已停止
+     * @return bool
+     */
+    public function hasBeenStopped()
+    {
+        $this->requireProcessIsTerminated(__FUNCTION__);
+
+        $this->updateStatus(false);
+
+        return $this->processInformation['stopped'];
+    }
+
+    /**
+     * 返回导致子进程停止其执行的数。
+     * @return int
+     */
+    public function getStopSignal()
+    {
+        $this->requireProcessIsTerminated(__FUNCTION__);
+
+        $this->updateStatus(false);
+
+        return $this->processInformation['stopsig'];
+    }
+
+    /**
+     * 检查是否正在运行
+     * @return bool
+     */
+    public function isRunning()
+    {
+        if (self::STATUS_STARTED !== $this->status) {
+            return false;
+        }
+
+        $this->updateStatus(false);
+
+        return $this->processInformation['running'];
+    }
+
+    /**
+     * 检查是否已开始
+     * @return bool
+     */
+    public function isStarted()
+    {
+        return self::STATUS_READY != $this->status;
+    }
+
+    /**
+     * 检查是否已终止
+     * @return bool
+     */
+    public function isTerminated()
+    {
+        $this->updateStatus(false);
+
+        return self::STATUS_TERMINATED == $this->status;
+    }
+
+    /**
+     * 获取当前的状态
+     * @return string
+     */
+    public function getStatus()
+    {
+        $this->updateStatus(false);
+
+        return $this->status;
+    }
+
+    /**
+     * 终止进程
+     */
+    public function stop()
+    {
+        if ($this->isRunning()) {
+            if ('\\' === DS && !$this->isSigchildEnabled()) {
+                exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode);
+                if ($exitCode > 0) {
+                    throw new \RuntimeException('Unable to kill the process');
+                }
+            } else {
+                $pids = preg_split('/\s+/', `ps -o pid --no-heading --ppid {$this->getPid()}`);
+                foreach ($pids as $pid) {
+                    if (is_numeric($pid)) {
+                        posix_kill($pid, 9);
+                    }
+                }
+            }
+        }
+
+        $this->updateStatus(false);
+        if ($this->processInformation['running']) {
+            $this->close();
+        }
+
+        return $this->exitcode;
+    }
+
+    /**
+     * 添加一行输出
+     * @param string $line
+     */
+    public function addOutput($line)
+{
+        $this->lastOutputTime = microtime(true);
+        $this->stdout .= $line;
+    }
+
+    /**
+     * 添加一行错误输出
+     * @param string $line
+     */
+    public function addErrorOutput($line)
+{
+        $this->lastOutputTime = microtime(true);
+        $this->stderr .= $line;
+    }
+
+    /**
+     * 获取被执行的指令
+     * @return string
+     */
+    public function getCommandLine()
+{
+        return $this->commandline;
+    }
+
+    /**
+     * 设置指令
+     * @param string $commandline
+     * @return self
+     */
+    public function setCommandLine($commandline)
+{
+        $this->commandline = $commandline;
+
+        return $this;
+    }
+
+    /**
+     * 获取超时时间
+     * @return float|null
+     */
+    public function getTimeout()
+{
+        return $this->timeout;
+    }
+
+    /**
+     * 获取idle超时时间
+     * @return float|null
+     */
+    public function getIdleTimeout()
+{
+        return $this->idleTimeout;
+    }
+
+    /**
+     * 设置超时时间
+     * @param int|float|null $timeout
+     * @return self
+     */
+    public function setTimeout($timeout)
+{
+        $this->timeout = $this->validateTimeout($timeout);
+
+        return $this;
+    }
+
+    /**
+     * 设置idle超时时间
+     * @param int|float|null $timeout
+     * @return self
+     */
+    public function setIdleTimeout($timeout)
+{
+        if (null !== $timeout && $this->outputDisabled) {
+            throw new \LogicException('Idle timeout can not be set while the output is disabled.');
+        }
+
+        $this->idleTimeout = $this->validateTimeout($timeout);
+
+        return $this;
+    }
+
+    /**
+     * 设置TTY
+     * @param bool $tty
+     * @return self
+     */
+    public function setTty($tty)
+{
+        if ('\\' === DS && $tty) {
+            throw new \RuntimeException('TTY mode is not supported on Windows platform.');
+        }
+        if ($tty && (!file_exists('/dev/tty') || !is_readable('/dev/tty'))) {
+            throw new \RuntimeException('TTY mode requires /dev/tty to be readable.');
+        }
+
+        $this->tty = (bool) $tty;
+
+        return $this;
+    }
+
+    /**
+     * 检查是否是tty模式
+     * @return bool
+     */
+    public function isTty()
+{
+        return $this->tty;
+    }
+
+    /**
+     * 设置pty模式
+     * @param bool $bool
+     * @return self
+     */
+    public function setPty($bool)
+{
+        $this->pty = (bool) $bool;
+
+        return $this;
+    }
+
+    /**
+     * 是否是pty模式
+     * @return bool
+     */
+    public function isPty()
+{
+        return $this->pty;
+    }
+
+    /**
+     * 获取工作目录
+     * @return string|null
+     */
+    public function getWorkingDirectory()
+{
+        if (null === $this->cwd) {
+            return getcwd() ?: null;
+        }
+
+        return $this->cwd;
+    }
+
+    /**
+     * 设置工作目录
+     * @param string $cwd
+     * @return self
+     */
+    public function setWorkingDirectory($cwd)
+{
+        $this->cwd = $cwd;
+
+        return $this;
+    }
+
+    /**
+     * 获取环境变量
+     * @return array
+     */
+    public function getEnv()
+{
+        return $this->env;
+    }
+
+    /**
+     * 设置环境变量
+     * @param array $env
+     * @return self
+     */
+    public function setEnv(array $env)
+{
+        $env = array_filter($env, function ($value) {
+            return !is_array($value);
+        });
+
+        $this->env = [];
+        foreach ($env as $key => $value) {
+            $this->env[(binary) $key] = (binary) $value;
+        }
+
+        return $this;
+    }
+
+    /**
+     * 获取输入
+     * @return null|string
+     */
+    public function getInput()
+{
+        return $this->input;
+    }
+
+    /**
+     * 设置输入
+     * @param mixed $input
+     * @return self
+     */
+    public function setInput($input)
+{
+        if ($this->isRunning()) {
+            throw new \LogicException('Input can not be set while the process is running.');
+        }
+
+        $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input);
+
+        return $this;
+    }
+
+    /**
+     * 获取proc_open的选项
+     * @return array
+     */
+    public function getOptions()
+{
+        return $this->options;
+    }
+
+    /**
+     * 设置proc_open的选项
+     * @param array $options
+     * @return self
+     */
+    public function setOptions(array $options)
+{
+        $this->options = $options;
+
+        return $this;
+    }
+
+    /**
+     * 是否兼容windows
+     * @return bool
+     */
+    public function getEnhanceWindowsCompatibility()
+{
+        return $this->enhanceWindowsCompatibility;
+    }
+
+    /**
+     * 设置是否兼容windows
+     * @param bool $enhance
+     * @return self
+     */
+    public function setEnhanceWindowsCompatibility($enhance)
+{
+        $this->enhanceWindowsCompatibility = (bool) $enhance;
+
+        return $this;
+    }
+
+    /**
+     * 返回是否 sigchild 兼容模式激活
+     * @return bool
+     */
+    public function getEnhanceSigchildCompatibility()
+{
+        return $this->enhanceSigchildCompatibility;
+    }
+
+    /**
+     * 激活 sigchild 兼容性模式。
+     * @param bool $enhance
+     * @return self
+     */
+    public function setEnhanceSigchildCompatibility($enhance)
+{
+        $this->enhanceSigchildCompatibility = (bool) $enhance;
+
+        return $this;
+    }
+
+    /**
+     * 是否超时
+     */
+    public function checkTimeout()
+{
+        if (self::STATUS_STARTED !== $this->status) {
+            return;
+        }
+
+        if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
+            $this->stop();
+
+            throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_GENERAL);
+        }
+
+        if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
+            $this->stop();
+
+            throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_IDLE);
+        }
+    }
+
+    /**
+     * 是否支持pty
+     * @return bool
+     */
+    public static function isPtySupported()
+{
+        static $result;
+
+        if (null !== $result) {
+            return $result;
+        }
+
+        if ('\\' === DS) {
+            return $result = false;
+        }
+
+        $proc = @proc_open('echo 1', [['pty'], ['pty'], ['pty']], $pipes);
+        if (is_resource($proc)) {
+            proc_close($proc);
+
+            return $result = true;
+        }
+
+        return $result = false;
+    }
+
+    /**
+     * 创建所需的 proc_open 的描述符
+     * @return array
+     */
+    private function getDescriptors()
+{
+        if ('\\' === DS) {
+            $this->processPipes = WindowsPipes::create($this, $this->input);
+        } else {
+            $this->processPipes = UnixPipes::create($this, $this->input);
+        }
+        $descriptors = $this->processPipes->getDescriptors($this->outputDisabled);
+
+        if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
+
+            $descriptors = array_merge($descriptors, [['pipe', 'w']]);
+
+            $this->commandline = '(' . $this->commandline . ') 3>/dev/null; code=$?; echo $code >&3; exit $code';
+        }
+
+        return $descriptors;
+    }
+
+    /**
+     * 建立 wait () 使用的回调。
+     * @param callable|null $callback
+     * @return callable
+     */
+    protected function buildCallback($callback)
+{
+        $out      = self::OUT;
+        $callback = function ($type, $data) use ($callback, $out) {
+            if ($out == $type) {
+                $this->addOutput($data);
+            } else {
+                $this->addErrorOutput($data);
+            }
+
+            if (null !== $callback) {
+                call_user_func($callback, $type, $data);
+            }
+        };
+
+        return $callback;
+    }
+
+    /**
+     * 更新状态
+     * @param bool $blocking
+     */
+    protected function updateStatus($blocking)
+{
+        if (self::STATUS_STARTED !== $this->status) {
+            return;
+        }
+
+        $this->processInformation = proc_get_status($this->process);
+        $this->captureExitCode();
+
+        $this->readPipes($blocking, '\\' === DS ? !$this->processInformation['running'] : true);
+
+        if (!$this->processInformation['running']) {
+            $this->close();
+        }
+    }
+
+    /**
+     * 是否开启 '--enable-sigchild'
+     * @return bool
+     */
+    protected function isSigchildEnabled()
+{
+        if (null !== self::$sigchild) {
+            return self::$sigchild;
+        }
+
+        if (!function_exists('phpinfo')) {
+            return self::$sigchild = false;
+        }
+
+        ob_start();
+        phpinfo(INFO_GENERAL);
+
+        return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
+    }
+
+    /**
+     * 验证是否超时
+     * @param int|float|null $timeout
+     * @return float|null
+     */
+    private function validateTimeout($timeout)
+{
+        $timeout = (float) $timeout;
+
+        if (0.0 === $timeout) {
+            $timeout = null;
+        } elseif ($timeout < 0) {
+            throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
+        }
+
+        return $timeout;
+    }
+
+    /**
+     * 读取pipes
+     * @param bool $blocking
+     * @param bool $close
+     */
+    private function readPipes($blocking, $close)
+{
+        $result = $this->processPipes->readAndWrite($blocking, $close);
+
+        $callback = $this->callback;
+        foreach ($result as $type => $data) {
+            if (3 == $type) {
+                $this->fallbackExitcode = (int) $data;
+            } else {
+                $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
+            }
+        }
+    }
+
+    /**
+     * 捕获退出码
+     */
+    private function captureExitCode()
+{
+        if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) {
+            $this->exitcode = $this->processInformation['exitcode'];
+        }
+    }
+
+    /**
+     * 关闭资源
+     * @return int 退出码
+     */
+    private function close()
+{
+        $this->processPipes->close();
+        if (is_resource($this->process)) {
+            $exitcode = proc_close($this->process);
+        } else {
+            $exitcode = -1;
+        }
+
+        $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1);
+        $this->status   = self::STATUS_TERMINATED;
+
+        if (-1 === $this->exitcode && null !== $this->fallbackExitcode) {
+            $this->exitcode = $this->fallbackExitcode;
+        } elseif (-1 === $this->exitcode && $this->processInformation['signaled']
+            && 0 < $this->processInformation['termsig']
+        ) {
+            $this->exitcode = 128 + $this->processInformation['termsig'];
+        }
+
+        return $this->exitcode;
+    }
+
+    /**
+     * 重置数据
+     */
+    private function resetProcessData()
+{
+        $this->starttime                    = null;
+        $this->callback                     = null;
+        $this->exitcode                     = null;
+        $this->fallbackExitcode             = null;
+        $this->processInformation           = null;
+        $this->stdout                       = null;
+        $this->stderr                       = null;
+        $this->process                      = null;
+        $this->latestSignal                 = null;
+        $this->status                       = self::STATUS_READY;
+        $this->incrementalOutputOffset      = 0;
+        $this->incrementalErrorOutputOffset = 0;
+    }
+
+    /**
+     * 将一个 POSIX 信号发送到进程中。
+     * @param int  $signal
+     * @param bool $throwException
+     * @return bool
+     */
+    private function doSignal($signal, $throwException)
+{
+        if (!$this->isRunning()) {
+            if ($throwException) {
+                throw new \LogicException('Can not send signal on a non running process.');
+            }
+
+            return false;
+        }
+
+        if ($this->isSigchildEnabled()) {
+            if ($throwException) {
+                throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.');
+            }
+
+            return false;
+        }
+
+        if (true !== @proc_terminate($this->process, $signal)) {
+            if ($throwException) {
+                throw new \RuntimeException(sprintf('Error while sending signal `%s`.', $signal));
+            }
+
+            return false;
+        }
+
+        $this->latestSignal = $signal;
+
+        return true;
+    }
+
+    /**
+     * 确保进程已经开启
+     * @param string $functionName
+     */
+    private function requireProcessIsStarted($functionName)
+{
+        if (!$this->isStarted()) {
+            throw new \LogicException(sprintf('Process must be started before calling %s.', $functionName));
+        }
+    }
+
+    /**
+     * 确保进程已经终止
+     * @param string $functionName
+     */
+    private function requireProcessIsTerminated($functionName)
+{
+        if (!$this->isTerminated()) {
+            throw new \LogicException(sprintf('Process must be terminated before calling %s.', $functionName));
+        }
+    }
+}

+ 1679 - 0
thinkphp/library/think/Request.php

@@ -0,0 +1,1679 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+class Request
+{
+    /**
+     * @var object 对象实例
+     */
+    protected static $instance;
+
+    protected $method;
+    /**
+     * @var string 域名(含协议和端口)
+     */
+    protected $domain;
+
+    /**
+     * @var string URL地址
+     */
+    protected $url;
+
+    /**
+     * @var string 基础URL
+     */
+    protected $baseUrl;
+
+    /**
+     * @var string 当前执行的文件
+     */
+    protected $baseFile;
+
+    /**
+     * @var string 访问的ROOT地址
+     */
+    protected $root;
+
+    /**
+     * @var string pathinfo
+     */
+    protected $pathinfo;
+
+    /**
+     * @var string pathinfo(不含后缀)
+     */
+    protected $path;
+
+    /**
+     * @var array 当前路由信息
+     */
+    protected $routeInfo = [];
+
+    /**
+     * @var array 环境变量
+     */
+    protected $env;
+
+    /**
+     * @var array 当前调度信息
+     */
+    protected $dispatch = [];
+    protected $module;
+    protected $controller;
+    protected $action;
+    // 当前语言集
+    protected $langset;
+
+    /**
+     * @var array 请求参数
+     */
+    protected $param   = [];
+    protected $get     = [];
+    protected $post    = [];
+    protected $request = [];
+    protected $route   = [];
+    protected $put;
+    protected $session = [];
+    protected $file    = [];
+    protected $cookie  = [];
+    protected $server  = [];
+    protected $header  = [];
+
+    /**
+     * @var array 资源类型
+     */
+    protected $mimeType = [
+        'xml'   => 'application/xml,text/xml,application/x-xml',
+        'json'  => 'application/json,text/x-json,application/jsonrequest,text/json',
+        'js'    => 'text/javascript,application/javascript,application/x-javascript',
+        'css'   => 'text/css',
+        'rss'   => 'application/rss+xml',
+        'yaml'  => 'application/x-yaml,text/yaml',
+        'atom'  => 'application/atom+xml',
+        'pdf'   => 'application/pdf',
+        'text'  => 'text/plain',
+        'image' => 'image/png,image/jpg,image/jpeg,image/pjpeg,image/gif,image/webp,image/*',
+        'csv'   => 'text/csv',
+        'html'  => 'text/html,application/xhtml+xml,*/*',
+    ];
+
+    protected $content;
+
+    // 全局过滤规则
+    protected $filter;
+    // Hook扩展方法
+    protected static $hook = [];
+    // 绑定的属性
+    protected $bind = [];
+    // php://input
+    protected $input;
+    // 请求缓存
+    protected $cache;
+    // 缓存是否检查
+    protected $isCheckCache;
+    /**
+     * 是否合并Param
+     * @var bool
+     */
+    protected $mergeParam = false;
+
+    /**
+     * 构造函数
+     * @access protected
+     * @param array $options 参数
+     */
+    protected function __construct($options = [])
+    {
+        foreach ($options as $name => $item) {
+            if (property_exists($this, $name)) {
+                $this->$name = $item;
+            }
+        }
+        if (is_null($this->filter)) {
+            $this->filter = Config::get('default_filter');
+        }
+
+        // 保存 php://input
+        $this->input = file_get_contents('php://input');
+    }
+
+    public function __call($method, $args)
+    {
+        if (array_key_exists($method, self::$hook)) {
+            array_unshift($args, $this);
+            return call_user_func_array(self::$hook[$method], $args);
+        } else {
+            throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
+        }
+    }
+
+    /**
+     * Hook 方法注入
+     * @access public
+     * @param string|array  $method 方法名
+     * @param mixed         $callback callable
+     * @return void
+     */
+    public static function hook($method, $callback = null)
+    {
+        if (is_array($method)) {
+            self::$hook = array_merge(self::$hook, $method);
+        } else {
+            self::$hook[$method] = $callback;
+        }
+    }
+
+    /**
+     * 初始化
+     * @access public
+     * @param array $options 参数
+     * @return \think\Request
+     */
+    public static function instance($options = [])
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new static($options);
+        }
+        return self::$instance;
+    }
+
+    /**
+     * 销毁当前请求对象
+     * @access public
+     * @return void
+     */
+    public static function destroy()
+    {
+        if (!is_null(self::$instance)) {
+            self::$instance = null;
+        }
+    }
+
+    /**
+     * 创建一个URL请求
+     * @access public
+     * @param string    $uri URL地址
+     * @param string    $method 请求类型
+     * @param array     $params 请求参数
+     * @param array     $cookie
+     * @param array     $files
+     * @param array     $server
+     * @param string    $content
+     * @return \think\Request
+     */
+    public static function create($uri, $method = 'GET', $params = [], $cookie = [], $files = [], $server = [], $content = null)
+    {
+        $server['PATH_INFO']      = '';
+        $server['REQUEST_METHOD'] = strtoupper($method);
+        $info                     = parse_url($uri);
+        if (isset($info['host'])) {
+            $server['SERVER_NAME'] = $info['host'];
+            $server['HTTP_HOST']   = $info['host'];
+        }
+        if (isset($info['scheme'])) {
+            if ('https' === $info['scheme']) {
+                $server['HTTPS']       = 'on';
+                $server['SERVER_PORT'] = 443;
+            } else {
+                unset($server['HTTPS']);
+                $server['SERVER_PORT'] = 80;
+            }
+        }
+        if (isset($info['port'])) {
+            $server['SERVER_PORT'] = $info['port'];
+            $server['HTTP_HOST']   = $server['HTTP_HOST'] . ':' . $info['port'];
+        }
+        if (isset($info['user'])) {
+            $server['PHP_AUTH_USER'] = $info['user'];
+        }
+        if (isset($info['pass'])) {
+            $server['PHP_AUTH_PW'] = $info['pass'];
+        }
+        if (!isset($info['path'])) {
+            $info['path'] = '/';
+        }
+        $options                      = [];
+        $options[strtolower($method)] = $params;
+        $queryString                  = '';
+        if (isset($info['query'])) {
+            parse_str(html_entity_decode($info['query']), $query);
+            if (!empty($params)) {
+                $params      = array_replace($query, $params);
+                $queryString = http_build_query($params, '', '&');
+            } else {
+                $params      = $query;
+                $queryString = $info['query'];
+            }
+        } elseif (!empty($params)) {
+            $queryString = http_build_query($params, '', '&');
+        }
+        if ($queryString) {
+            parse_str($queryString, $get);
+            $options['get'] = isset($options['get']) ? array_merge($get, $options['get']) : $get;
+        }
+
+        $server['REQUEST_URI']  = $info['path'] . ('' !== $queryString ? '?' . $queryString : '');
+        $server['QUERY_STRING'] = $queryString;
+        $options['cookie']      = $cookie;
+        $options['param']       = $params;
+        $options['file']        = $files;
+        $options['server']      = $server;
+        $options['url']         = $server['REQUEST_URI'];
+        $options['baseUrl']     = $info['path'];
+        $options['pathinfo']    = '/' == $info['path'] ? '/' : ltrim($info['path'], '/');
+        $options['method']      = $server['REQUEST_METHOD'];
+        $options['domain']      = isset($info['scheme']) ? $info['scheme'] . '://' . $server['HTTP_HOST'] : '';
+        $options['content']     = $content;
+        self::$instance         = new self($options);
+        return self::$instance;
+    }
+
+    /**
+     * 设置或获取当前包含协议的域名
+     * @access public
+     * @param string $domain 域名
+     * @return string
+     */
+    public function domain($domain = null)
+    {
+        if (!is_null($domain)) {
+            $this->domain = $domain;
+            return $this;
+        } elseif (!$this->domain) {
+            $this->domain = $this->scheme() . '://' . $this->host();
+        }
+        return $this->domain;
+    }
+
+    /**
+     * 设置或获取当前完整URL 包括QUERY_STRING
+     * @access public
+     * @param string|true $url URL地址 true 带域名获取
+     * @return string
+     */
+    public function url($url = null)
+    {
+        if (!is_null($url) && true !== $url) {
+            $this->url = $url;
+            return $this;
+        } elseif (!$this->url) {
+            if (IS_CLI) {
+                $this->url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
+            } elseif (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
+                $this->url = $_SERVER['HTTP_X_REWRITE_URL'];
+            } elseif (isset($_SERVER['REQUEST_URI'])) {
+                $this->url = $_SERVER['REQUEST_URI'];
+            } elseif (isset($_SERVER['ORIG_PATH_INFO'])) {
+                $this->url = $_SERVER['ORIG_PATH_INFO'] . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
+            } else {
+                $this->url = '';
+            }
+        }
+        return true === $url ? $this->domain() . $this->url : $this->url;
+    }
+
+    /**
+     * 设置或获取当前URL 不含QUERY_STRING
+     * @access public
+     * @param string $url URL地址
+     * @return string
+     */
+    public function baseUrl($url = null)
+    {
+        if (!is_null($url) && true !== $url) {
+            $this->baseUrl = $url;
+            return $this;
+        } elseif (!$this->baseUrl) {
+            $str           = $this->url();
+            $this->baseUrl = strpos($str, '?') ? strstr($str, '?', true) : $str;
+        }
+        return true === $url ? $this->domain() . $this->baseUrl : $this->baseUrl;
+    }
+
+    /**
+     * 设置或获取当前执行的文件 SCRIPT_NAME
+     * @access public
+     * @param string $file 当前执行的文件
+     * @return string
+     */
+    public function baseFile($file = null)
+    {
+        if (!is_null($file) && true !== $file) {
+            $this->baseFile = $file;
+            return $this;
+        } elseif (!$this->baseFile) {
+            $url = '';
+            if (!IS_CLI) {
+                $script_name = basename($_SERVER['SCRIPT_FILENAME']);
+                if (basename($_SERVER['SCRIPT_NAME']) === $script_name) {
+                    $url = $_SERVER['SCRIPT_NAME'];
+                } elseif (basename($_SERVER['PHP_SELF']) === $script_name) {
+                    $url = $_SERVER['PHP_SELF'];
+                } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $script_name) {
+                    $url = $_SERVER['ORIG_SCRIPT_NAME'];
+                } elseif (($pos = strpos($_SERVER['PHP_SELF'], '/' . $script_name)) !== false) {
+                    $url = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $script_name;
+                } elseif (isset($_SERVER['DOCUMENT_ROOT']) && strpos($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']) === 0) {
+                    $url = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $_SERVER['SCRIPT_FILENAME']));
+                }
+            }
+            $this->baseFile = $url;
+        }
+        return true === $file ? $this->domain() . $this->baseFile : $this->baseFile;
+    }
+
+    /**
+     * 设置或获取URL访问根地址
+     * @access public
+     * @param string $url URL地址
+     * @return string
+     */
+    public function root($url = null)
+    {
+        if (!is_null($url) && true !== $url) {
+            $this->root = $url;
+            return $this;
+        } elseif (!$this->root) {
+            $file = $this->baseFile();
+            if ($file && 0 !== strpos($this->url(), $file)) {
+                $file = str_replace('\\', '/', dirname($file));
+            }
+            $this->root = rtrim($file, '/');
+        }
+        return true === $url ? $this->domain() . $this->root : $this->root;
+    }
+
+    /**
+     * 获取当前请求URL的pathinfo信息(含URL后缀)
+     * @access public
+     * @return string
+     */
+    public function pathinfo()
+    {
+        if (is_null($this->pathinfo)) {
+            if (isset($_GET[Config::get('var_pathinfo')])) {
+                // 判断URL里面是否有兼容模式参数
+                $_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')];
+                unset($_GET[Config::get('var_pathinfo')]);
+            } elseif (IS_CLI) {
+                // CLI模式下 index.php module/controller/action/params/...
+                $_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
+            }
+
+            // 分析PATHINFO信息
+            if (!isset($_SERVER['PATH_INFO'])) {
+                foreach (Config::get('pathinfo_fetch') as $type) {
+                    if (!empty($_SERVER[$type])) {
+                        $_SERVER['PATH_INFO'] = (0 === strpos($_SERVER[$type], $_SERVER['SCRIPT_NAME'])) ?
+                        substr($_SERVER[$type], strlen($_SERVER['SCRIPT_NAME'])) : $_SERVER[$type];
+                        break;
+                    }
+                }
+            }
+            $this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/');
+        }
+        return $this->pathinfo;
+    }
+
+    /**
+     * 获取当前请求URL的pathinfo信息(不含URL后缀)
+     * @access public
+     * @return string
+     */
+    public function path()
+    {
+        if (is_null($this->path)) {
+            $suffix   = Config::get('url_html_suffix');
+            $pathinfo = $this->pathinfo();
+            if (false === $suffix) {
+                // 禁止伪静态访问
+                $this->path = $pathinfo;
+            } elseif ($suffix) {
+                // 去除正常的URL后缀
+                $this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
+            } else {
+                // 允许任何后缀访问
+                $this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo);
+            }
+        }
+        return $this->path;
+    }
+
+    /**
+     * 当前URL的访问后缀
+     * @access public
+     * @return string
+     */
+    public function ext()
+    {
+        return pathinfo($this->pathinfo(), PATHINFO_EXTENSION);
+    }
+
+    /**
+     * 获取当前请求的时间
+     * @access public
+     * @param bool $float 是否使用浮点类型
+     * @return integer|float
+     */
+    public function time($float = false)
+    {
+        return $float ? $_SERVER['REQUEST_TIME_FLOAT'] : $_SERVER['REQUEST_TIME'];
+    }
+
+    /**
+     * 当前请求的资源类型
+     * @access public
+     * @return false|string
+     */
+    public function type()
+    {
+        $accept = $this->server('HTTP_ACCEPT');
+        if (empty($accept)) {
+            return false;
+        }
+
+        foreach ($this->mimeType as $key => $val) {
+            $array = explode(',', $val);
+            foreach ($array as $k => $v) {
+                if (stristr($accept, $v)) {
+                    return $key;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 设置资源类型
+     * @access public
+     * @param string|array  $type 资源类型名
+     * @param string        $val 资源类型
+     * @return void
+     */
+    public function mimeType($type, $val = '')
+    {
+        if (is_array($type)) {
+            $this->mimeType = array_merge($this->mimeType, $type);
+        } else {
+            $this->mimeType[$type] = $val;
+        }
+    }
+
+    /**
+     * 当前的请求类型
+     * @access public
+     * @param bool $method  true 获取原始请求类型
+     * @return string
+     */
+    public function method($method = false)
+    {
+        if (true === $method) {
+            // 获取原始请求类型
+            return $this->server('REQUEST_METHOD') ?: 'GET';
+        } elseif (!$this->method) {
+            if (isset($_POST[Config::get('var_method')])) {
+                $this->method = strtoupper($_POST[Config::get('var_method')]);
+                $this->{$this->method}($_POST);
+            } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
+                $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
+            } else {
+                $this->method = $this->server('REQUEST_METHOD') ?: 'GET';
+            }
+        }
+        return $this->method;
+    }
+
+    /**
+     * 是否为GET请求
+     * @access public
+     * @return bool
+     */
+    public function isGet()
+    {
+        return $this->method() == 'GET';
+    }
+
+    /**
+     * 是否为POST请求
+     * @access public
+     * @return bool
+     */
+    public function isPost()
+    {
+        return $this->method() == 'POST';
+    }
+
+    /**
+     * 是否为PUT请求
+     * @access public
+     * @return bool
+     */
+    public function isPut()
+    {
+        return $this->method() == 'PUT';
+    }
+
+    /**
+     * 是否为DELTE请求
+     * @access public
+     * @return bool
+     */
+    public function isDelete()
+    {
+        return $this->method() == 'DELETE';
+    }
+
+    /**
+     * 是否为HEAD请求
+     * @access public
+     * @return bool
+     */
+    public function isHead()
+    {
+        return $this->method() == 'HEAD';
+    }
+
+    /**
+     * 是否为PATCH请求
+     * @access public
+     * @return bool
+     */
+    public function isPatch()
+    {
+        return $this->method() == 'PATCH';
+    }
+
+    /**
+     * 是否为OPTIONS请求
+     * @access public
+     * @return bool
+     */
+    public function isOptions()
+    {
+        return $this->method() == 'OPTIONS';
+    }
+
+    /**
+     * 是否为cli
+     * @access public
+     * @return bool
+     */
+    public function isCli()
+    {
+        return PHP_SAPI == 'cli';
+    }
+
+    /**
+     * 是否为cgi
+     * @access public
+     * @return bool
+     */
+    public function isCgi()
+    {
+        return strpos(PHP_SAPI, 'cgi') === 0;
+    }
+
+    /**
+     * 获取当前请求的参数
+     * @access public
+     * @param string|array  $name 变量名
+     * @param mixed         $default 默认值
+     * @param string|array  $filter 过滤方法
+     * @return mixed
+     */
+    public function param($name = '', $default = null, $filter = '')
+    {
+        if (empty($this->mergeParam)) {
+            $method = $this->method(true);
+            // 自动获取请求变量
+            switch ($method) {
+                case 'POST':
+                    $vars = $this->post(false);
+                    break;
+                case 'PUT':
+                case 'DELETE':
+                case 'PATCH':
+                    $vars = $this->put(false);
+                    break;
+                default:
+                    $vars = [];
+            }
+            // 当前请求参数和URL地址中的参数合并
+            $this->param      = array_merge($this->param, $this->get(false), $vars, $this->route(false));
+            $this->mergeParam = true;
+        }
+        if (true === $name) {
+            // 获取包含文件上传信息的数组
+            $file = $this->file();
+            $data = is_array($file) ? array_merge($this->param, $file) : $this->param;
+            return $this->input($data, '', $default, $filter);
+        }
+        return $this->input($this->param, $name, $default, $filter);
+    }
+
+    /**
+     * 设置获取路由参数
+     * @access public
+     * @param string|array  $name 变量名
+     * @param mixed         $default 默认值
+     * @param string|array  $filter 过滤方法
+     * @return mixed
+     */
+    public function route($name = '', $default = null, $filter = '')
+    {
+        if (is_array($name)) {
+            $this->param        = [];
+            return $this->route = array_merge($this->route, $name);
+        }
+        return $this->input($this->route, $name, $default, $filter);
+    }
+
+    /**
+     * 设置获取GET参数
+     * @access public
+     * @param string|array  $name 变量名
+     * @param mixed         $default 默认值
+     * @param string|array  $filter 过滤方法
+     * @return mixed
+     */
+    public function get($name = '', $default = null, $filter = '')
+    {
+        if (empty($this->get)) {
+            $this->get = $_GET;
+        }
+        if (is_array($name)) {
+            $this->param      = [];
+            return $this->get = array_merge($this->get, $name);
+        }
+        return $this->input($this->get, $name, $default, $filter);
+    }
+
+    /**
+     * 设置获取POST参数
+     * @access public
+     * @param string        $name 变量名
+     * @param mixed         $default 默认值
+     * @param string|array  $filter 过滤方法
+     * @return mixed
+     */
+    public function post($name = '', $default = null, $filter = '')
+    {
+        if (empty($this->post)) {
+            $content = $this->input;
+            if (empty($_POST) && false !== strpos($this->contentType(), 'application/json')) {
+                $this->post = (array) json_decode($content, true);
+            } else {
+                $this->post = $_POST;
+            }
+        }
+        if (is_array($name)) {
+            $this->param       = [];
+            return $this->post = array_merge($this->post, $name);
+        }
+        return $this->input($this->post, $name, $default, $filter);
+    }
+
+    /**
+     * 设置获取PUT参数
+     * @access public
+     * @param string|array      $name 变量名
+     * @param mixed             $default 默认值
+     * @param string|array      $filter 过滤方法
+     * @return mixed
+     */
+    public function put($name = '', $default = null, $filter = '')
+    {
+        if (is_null($this->put)) {
+            $content = $this->input;
+            if (false !== strpos($this->contentType(), 'application/json')) {
+                $this->put = (array) json_decode($content, true);
+            } else {
+                parse_str($content, $this->put);
+            }
+        }
+        if (is_array($name)) {
+            $this->param      = [];
+            return $this->put = is_null($this->put) ? $name : array_merge($this->put, $name);
+        }
+
+        return $this->input($this->put, $name, $default, $filter);
+    }
+
+    /**
+     * 设置获取DELETE参数
+     * @access public
+     * @param string|array      $name 变量名
+     * @param mixed             $default 默认值
+     * @param string|array      $filter 过滤方法
+     * @return mixed
+     */
+    public function delete($name = '', $default = null, $filter = '')
+    {
+        return $this->put($name, $default, $filter);
+    }
+
+    /**
+     * 设置获取PATCH参数
+     * @access public
+     * @param string|array      $name 变量名
+     * @param mixed             $default 默认值
+     * @param string|array      $filter 过滤方法
+     * @return mixed
+     */
+    public function patch($name = '', $default = null, $filter = '')
+    {
+        return $this->put($name, $default, $filter);
+    }
+
+    /**
+     * 获取request变量
+     * @param string        $name 数据名称
+     * @param string        $default 默认值
+     * @param string|array  $filter 过滤方法
+     * @return mixed
+     */
+    public function request($name = '', $default = null, $filter = '')
+    {
+        if (empty($this->request)) {
+            $this->request = $_REQUEST;
+        }
+        if (is_array($name)) {
+            $this->param          = [];
+            return $this->request = array_merge($this->request, $name);
+        }
+        return $this->input($this->request, $name, $default, $filter);
+    }
+
+    /**
+     * 获取session数据
+     * @access public
+     * @param string|array  $name 数据名称
+     * @param string        $default 默认值
+     * @param string|array  $filter 过滤方法
+     * @return mixed
+     */
+    public function session($name = '', $default = null, $filter = '')
+    {
+        if (empty($this->session)) {
+            $this->session = Session::get();
+        }
+        if (is_array($name)) {
+            return $this->session = array_merge($this->session, $name);
+        }
+        return $this->input($this->session, $name, $default, $filter);
+    }
+
+    /**
+     * 获取cookie参数
+     * @access public
+     * @param string|array  $name 数据名称
+     * @param string        $default 默认值
+     * @param string|array  $filter 过滤方法
+     * @return mixed
+     */
+    public function cookie($name = '', $default = null, $filter = '')
+    {
+        if (empty($this->cookie)) {
+            $this->cookie = Cookie::get();
+        }
+        if (is_array($name)) {
+            return $this->cookie = array_merge($this->cookie, $name);
+        } elseif (!empty($name)) {
+            $data = Cookie::has($name) ? Cookie::get($name) : $default;
+        } else {
+            $data = $this->cookie;
+        }
+
+        // 解析过滤器
+        $filter = $this->getFilter($filter, $default);
+
+        if (is_array($data)) {
+            array_walk_recursive($data, [$this, 'filterValue'], $filter);
+            reset($data);
+        } else {
+            $this->filterValue($data, $name, $filter);
+        }
+        return $data;
+    }
+
+    /**
+     * 获取server参数
+     * @access public
+     * @param string|array  $name 数据名称
+     * @param string        $default 默认值
+     * @param string|array  $filter 过滤方法
+     * @return mixed
+     */
+    public function server($name = '', $default = null, $filter = '')
+    {
+        if (empty($this->server)) {
+            $this->server = $_SERVER;
+        }
+        if (is_array($name)) {
+            return $this->server = array_merge($this->server, $name);
+        }
+        return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
+    }
+
+    /**
+     * 获取上传的文件信息
+     * @access public
+     * @param string|array $name 名称
+     * @return null|array|\think\File
+     */
+    public function file($name = '')
+    {
+        if (empty($this->file)) {
+            $this->file = isset($_FILES) ? $_FILES : [];
+        }
+        if (is_array($name)) {
+            return $this->file = array_merge($this->file, $name);
+        }
+        $files = $this->file;
+        if (!empty($files)) {
+            // 处理上传文件
+            $array = [];
+            foreach ($files as $key => $file) {
+                if (is_array($file['name'])) {
+                    $item  = [];
+                    $keys  = array_keys($file);
+                    $count = count($file['name']);
+                    for ($i = 0; $i < $count; $i++) {
+                        if (empty($file['tmp_name'][$i]) || !is_file($file['tmp_name'][$i])) {
+                            continue;
+                        }
+                        $temp['key'] = $key;
+                        foreach ($keys as $_key) {
+                            $temp[$_key] = $file[$_key][$i];
+                        }
+                        $item[] = (new File($temp['tmp_name']))->setUploadInfo($temp);
+                    }
+                    $array[$key] = $item;
+                } else {
+                    if ($file instanceof File) {
+                        $array[$key] = $file;
+                    } else {
+                        if (empty($file['tmp_name']) || !is_file($file['tmp_name'])) {
+                            continue;
+                        }
+                        $array[$key] = (new File($file['tmp_name']))->setUploadInfo($file);
+                    }
+                }
+            }
+            if (strpos($name, '.')) {
+                list($name, $sub) = explode('.', $name);
+            }
+            if ('' === $name) {
+                // 获取全部文件
+                return $array;
+            } elseif (isset($sub) && isset($array[$name][$sub])) {
+                return $array[$name][$sub];
+            } elseif (isset($array[$name])) {
+                return $array[$name];
+            }
+        }
+        return;
+    }
+
+    /**
+     * 获取环境变量
+     * @param string|array  $name 数据名称
+     * @param string        $default 默认值
+     * @param string|array  $filter 过滤方法
+     * @return mixed
+     */
+    public function env($name = '', $default = null, $filter = '')
+    {
+        if (empty($this->env)) {
+            $this->env = $_ENV;
+        }
+        if (is_array($name)) {
+            return $this->env = array_merge($this->env, $name);
+        }
+        return $this->input($this->env, false === $name ? false : strtoupper($name), $default, $filter);
+    }
+
+    /**
+     * 设置或者获取当前的Header
+     * @access public
+     * @param string|array  $name header名称
+     * @param string        $default 默认值
+     * @return string
+     */
+    public function header($name = '', $default = null)
+    {
+        if (empty($this->header)) {
+            $header = [];
+            if (function_exists('apache_request_headers') && $result = apache_request_headers()) {
+                $header = $result;
+            } else {
+                $server = $this->server ?: $_SERVER;
+                foreach ($server as $key => $val) {
+                    if (0 === strpos($key, 'HTTP_')) {
+                        $key          = str_replace('_', '-', strtolower(substr($key, 5)));
+                        $header[$key] = $val;
+                    }
+                }
+                if (isset($server['CONTENT_TYPE'])) {
+                    $header['content-type'] = $server['CONTENT_TYPE'];
+                }
+                if (isset($server['CONTENT_LENGTH'])) {
+                    $header['content-length'] = $server['CONTENT_LENGTH'];
+                }
+            }
+            $this->header = array_change_key_case($header);
+        }
+        if (is_array($name)) {
+            return $this->header = array_merge($this->header, $name);
+        }
+        if ('' === $name) {
+            return $this->header;
+        }
+        $name = str_replace('_', '-', strtolower($name));
+        return isset($this->header[$name]) ? $this->header[$name] : $default;
+    }
+
+    /**
+     * 获取变量 支持过滤和默认值
+     * @param array         $data 数据源
+     * @param string|false  $name 字段名
+     * @param mixed         $default 默认值
+     * @param string|array  $filter 过滤函数
+     * @return mixed
+     */
+    public function input($data = [], $name = '', $default = null, $filter = '')
+    {
+        if (false === $name) {
+            // 获取原始数据
+            return $data;
+        }
+        $name = (string) $name;
+        if ('' != $name) {
+            // 解析name
+            if (strpos($name, '/')) {
+                list($name, $type) = explode('/', $name);
+            } else {
+                $type = 's';
+            }
+            // 按.拆分成多维数组进行判断
+            foreach (explode('.', $name) as $val) {
+                if (isset($data[$val])) {
+                    $data = $data[$val];
+                } else {
+                    // 无输入数据,返回默认值
+                    return $default;
+                }
+            }
+            if (is_object($data)) {
+                return $data;
+            }
+        }
+
+        // 解析过滤器
+        $filter = $this->getFilter($filter, $default);
+
+        if (is_array($data)) {
+            array_walk_recursive($data, [$this, 'filterValue'], $filter);
+            reset($data);
+        } else {
+            $this->filterValue($data, $name, $filter);
+        }
+
+        if (isset($type) && $data !== $default) {
+            // 强制类型转换
+            $this->typeCast($data, $type);
+        }
+        return $data;
+    }
+
+    /**
+     * 设置或获取当前的过滤规则
+     * @param mixed $filter 过滤规则
+     * @return mixed
+     */
+    public function filter($filter = null)
+    {
+        if (is_null($filter)) {
+            return $this->filter;
+        } else {
+            $this->filter = $filter;
+        }
+    }
+
+    protected function getFilter($filter, $default)
+    {
+        if (is_null($filter)) {
+            $filter = [];
+        } else {
+            $filter = $filter ?: $this->filter;
+            if (is_string($filter) && false === strpos($filter, '/')) {
+                $filter = explode(',', $filter);
+            } else {
+                $filter = (array) $filter;
+            }
+        }
+
+        $filter[] = $default;
+        return $filter;
+    }
+
+    /**
+     * 递归过滤给定的值
+     * @param mixed     $value 键值
+     * @param mixed     $key 键名
+     * @param array     $filters 过滤方法+默认值
+     * @return mixed
+     */
+    private function filterValue(&$value, $key, $filters)
+    {
+        $default = array_pop($filters);
+        foreach ($filters as $filter) {
+            if (is_callable($filter)) {
+                // 调用函数或者方法过滤
+                $value = call_user_func($filter, $value);
+            } elseif (is_scalar($value)) {
+                if (false !== strpos($filter, '/')) {
+                    // 正则过滤
+                    if (!preg_match($filter, $value)) {
+                        // 匹配不成功返回默认值
+                        $value = $default;
+                        break;
+                    }
+                } elseif (!empty($filter)) {
+                    // filter函数不存在时, 则使用filter_var进行过滤
+                    // filter为非整形值时, 调用filter_id取得过滤id
+                    $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
+                    if (false === $value) {
+                        $value = $default;
+                        break;
+                    }
+                }
+            }
+        }
+        return $this->filterExp($value);
+    }
+
+    /**
+     * 过滤表单中的表达式
+     * @param string $value
+     * @return void
+     */
+    public function filterExp(&$value)
+    {
+        // 过滤查询特殊字符
+        if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT LIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOT EXISTS|NOTEXISTS|EXISTS|NOT NULL|NOTNULL|NULL|BETWEEN TIME|NOT BETWEEN TIME|NOTBETWEEN TIME|NOTIN|NOT IN|IN)$/i', $value)) {
+            $value .= ' ';
+        }
+        // TODO 其他安全过滤
+    }
+
+    /**
+     * 强制类型转换
+     * @param string $data
+     * @param string $type
+     * @return mixed
+     */
+    private function typeCast(&$data, $type)
+    {
+        switch (strtolower($type)) {
+            // 数组
+            case 'a':
+                $data = (array) $data;
+                break;
+            // 数字
+            case 'd':
+                $data = (int) $data;
+                break;
+            // 浮点
+            case 'f':
+                $data = (float) $data;
+                break;
+            // 布尔
+            case 'b':
+                $data = (boolean) $data;
+                break;
+            // 字符串
+            case 's':
+            default:
+                if (is_scalar($data)) {
+                    $data = (string) $data;
+                } else {
+                    throw new \InvalidArgumentException('variable type error:' . gettype($data));
+                }
+        }
+    }
+
+    /**
+     * 是否存在某个请求参数
+     * @access public
+     * @param string    $name 变量名
+     * @param string    $type 变量类型
+     * @param bool      $checkEmpty 是否检测空值
+     * @return mixed
+     */
+    public function has($name, $type = 'param', $checkEmpty = false)
+    {
+        if (empty($this->$type)) {
+            $param = $this->$type();
+        } else {
+            $param = $this->$type;
+        }
+        // 按.拆分成多维数组进行判断
+        foreach (explode('.', $name) as $val) {
+            if (isset($param[$val])) {
+                $param = $param[$val];
+            } else {
+                return false;
+            }
+        }
+        return ($checkEmpty && '' === $param) ? false : true;
+    }
+
+    /**
+     * 获取指定的参数
+     * @access public
+     * @param string|array  $name 变量名
+     * @param string        $type 变量类型
+     * @return mixed
+     */
+    public function only($name, $type = 'param')
+    {
+        $param = $this->$type();
+        if (is_string($name)) {
+            $name = explode(',', $name);
+        }
+        $item = [];
+        foreach ($name as $key) {
+            if (isset($param[$key])) {
+                $item[$key] = $param[$key];
+            }
+        }
+        return $item;
+    }
+
+    /**
+     * 排除指定参数获取
+     * @access public
+     * @param string|array  $name 变量名
+     * @param string        $type 变量类型
+     * @return mixed
+     */
+    public function except($name, $type = 'param')
+    {
+        $param = $this->$type();
+        if (is_string($name)) {
+            $name = explode(',', $name);
+        }
+        foreach ($name as $key) {
+            if (isset($param[$key])) {
+                unset($param[$key]);
+            }
+        }
+        return $param;
+    }
+
+    /**
+     * 当前是否ssl
+     * @access public
+     * @return bool
+     */
+    public function isSsl()
+    {
+        $server = array_merge($_SERVER, $this->server);
+        if (isset($server['HTTPS']) && ('1' == $server['HTTPS'] || 'on' == strtolower($server['HTTPS']))) {
+            return true;
+        } elseif (isset($server['REQUEST_SCHEME']) && 'https' == $server['REQUEST_SCHEME']) {
+            return true;
+        } elseif (isset($server['SERVER_PORT']) && ('443' == $server['SERVER_PORT'])) {
+            return true;
+        } elseif (isset($server['HTTP_X_FORWARDED_PROTO']) && 'https' == $server['HTTP_X_FORWARDED_PROTO']) {
+            return true;
+        } elseif (Config::get('https_agent_name') && isset($server[Config::get('https_agent_name')])) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 当前是否Ajax请求
+     * @access public
+     * @param bool $ajax  true 获取原始ajax请求
+     * @return bool
+     */
+    public function isAjax($ajax = false)
+    {
+        $value  = $this->server('HTTP_X_REQUESTED_WITH', '', 'strtolower');
+        $result = ('xmlhttprequest' == $value) ? true : false;
+        if (true === $ajax) {
+            return $result;
+        } else {
+            $result =  $this->param(Config::get('var_ajax')) ? true : $result;
+            $this->mergeParam = false;
+            return $result;
+        }
+    }
+
+    /**
+     * 当前是否Pjax请求
+     * @access public
+     * @param bool $pjax  true 获取原始pjax请求
+     * @return bool
+     */
+    public function isPjax($pjax = false)
+    {
+        $result = !is_null($this->server('HTTP_X_PJAX')) ? true : false;
+        if (true === $pjax) {
+            return $result;
+        } else {
+            $result = $this->param(Config::get('var_pjax')) ? true : $result;
+            $this->mergeParam = false;
+            return $result;
+        }
+    }
+
+    /**
+     * 获取客户端IP地址
+     * @param integer   $type 返回类型 0 返回IP地址 1 返回IPV4地址数字
+     * @param boolean   $adv 是否进行高级模式获取(有可能被伪装)
+     * @return mixed
+     */
+    public function ip($type = 0, $adv = true)
+    {
+        $type      = $type ? 1 : 0;
+        static $ip = null;
+        if (null !== $ip) {
+            return $ip[$type];
+        }
+
+        $httpAgentIp = Config::get('http_agent_ip');
+
+        if ($httpAgentIp && isset($_SERVER[$httpAgentIp])) {
+            $ip = $_SERVER[$httpAgentIp];
+        } elseif ($adv) {
+            if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+                $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
+                $pos = array_search('unknown', $arr);
+                if (false !== $pos) {
+                    unset($arr[$pos]);
+                }
+                $ip = trim(current($arr));
+            } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
+                $ip = $_SERVER['HTTP_CLIENT_IP'];
+            } elseif (isset($_SERVER['REMOTE_ADDR'])) {
+                $ip = $_SERVER['REMOTE_ADDR'];
+            }
+        } elseif (isset($_SERVER['REMOTE_ADDR'])) {
+            $ip = $_SERVER['REMOTE_ADDR'];
+        }
+        // IP地址合法验证
+        $long = sprintf("%u", ip2long($ip));
+        $ip   = $long ? [$ip, $long] : ['0.0.0.0', 0];
+        return $ip[$type];
+    }
+
+    /**
+     * 检测是否使用手机访问
+     * @access public
+     * @return bool
+     */
+    public function isMobile()
+    {
+        if (isset($_SERVER['HTTP_VIA']) && stristr($_SERVER['HTTP_VIA'], "wap")) {
+            return true;
+        } elseif (isset($_SERVER['HTTP_ACCEPT']) && strpos(strtoupper($_SERVER['HTTP_ACCEPT']), "VND.WAP.WML")) {
+            return true;
+        } elseif (isset($_SERVER['HTTP_X_WAP_PROFILE']) || isset($_SERVER['HTTP_PROFILE'])) {
+            return true;
+        } elseif (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/(blackberry|configuration\/cldc|hp |hp-|htc |htc_|htc-|iemobile|kindle|midp|mmp|motorola|mobile|nokia|opera mini|opera |Googlebot-Mobile|YahooSeeker\/M1A1-R2D2|android|iphone|ipod|mobi|palm|palmos|pocket|portalmmm|ppc;|smartphone|sonyericsson|sqh|spv|symbian|treo|up.browser|up.link|vodafone|windows ce|xda |xda_)/i', $_SERVER['HTTP_USER_AGENT'])) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 当前URL地址中的scheme参数
+     * @access public
+     * @return string
+     */
+    public function scheme()
+    {
+        return $this->isSsl() ? 'https' : 'http';
+    }
+
+    /**
+     * 当前请求URL地址中的query参数
+     * @access public
+     * @return string
+     */
+    public function query()
+    {
+        return $this->server('QUERY_STRING');
+    }
+
+    /**
+     * 当前请求的host
+     * @access public
+     * @param bool $strict  true 仅仅获取HOST
+     * @return string
+     */
+    public function host($strict = false)
+    {
+        if (isset($_SERVER['HTTP_X_REAL_HOST'])) {
+            $host = $_SERVER['HTTP_X_REAL_HOST'];
+        } else {
+            $host = $this->server('HTTP_HOST');
+        }
+
+        return true === $strict && strpos($host, ':') ? strstr($host, ':', true) : $host;
+    }
+
+    /**
+     * 当前请求URL地址中的port参数
+     * @access public
+     * @return integer
+     */
+    public function port()
+    {
+        return $this->server('SERVER_PORT');
+    }
+
+    /**
+     * 当前请求 SERVER_PROTOCOL
+     * @access public
+     * @return integer
+     */
+    public function protocol()
+    {
+        return $this->server('SERVER_PROTOCOL');
+    }
+
+    /**
+     * 当前请求 REMOTE_PORT
+     * @access public
+     * @return integer
+     */
+    public function remotePort()
+    {
+        return $this->server('REMOTE_PORT');
+    }
+
+    /**
+     * 当前请求 HTTP_CONTENT_TYPE
+     * @access public
+     * @return string
+     */
+    public function contentType()
+    {
+        $contentType = $this->server('CONTENT_TYPE');
+        if ($contentType) {
+            if (strpos($contentType, ';')) {
+                list($type) = explode(';', $contentType);
+            } else {
+                $type = $contentType;
+            }
+            return trim($type);
+        }
+        return '';
+    }
+
+    /**
+     * 获取当前请求的路由信息
+     * @access public
+     * @param array $route 路由名称
+     * @return array
+     */
+    public function routeInfo($route = [])
+    {
+        if (!empty($route)) {
+            $this->routeInfo = $route;
+        } else {
+            return $this->routeInfo;
+        }
+    }
+
+    /**
+     * 设置或者获取当前请求的调度信息
+     * @access public
+     * @param array  $dispatch 调度信息
+     * @return array
+     */
+    public function dispatch($dispatch = null)
+    {
+        if (!is_null($dispatch)) {
+            $this->dispatch = $dispatch;
+        }
+        return $this->dispatch;
+    }
+
+    /**
+     * 设置或者获取当前的模块名
+     * @access public
+     * @param string $module 模块名
+     * @return string|Request
+     */
+    public function module($module = null)
+    {
+        if (!is_null($module)) {
+            $this->module = $module;
+            return $this;
+        } else {
+            return $this->module ?: '';
+        }
+    }
+
+    /**
+     * 设置或者获取当前的控制器名
+     * @access public
+     * @param string $controller 控制器名
+     * @return string|Request
+     */
+    public function controller($controller = null)
+    {
+        if (!is_null($controller)) {
+            $this->controller = $controller;
+            return $this;
+        } else {
+            return $this->controller ?: '';
+        }
+    }
+
+    /**
+     * 设置或者获取当前的操作名
+     * @access public
+     * @param string $action 操作名
+     * @return string|Request
+     */
+    public function action($action = null)
+    {
+        if (!is_null($action) && !is_bool($action)) {
+            $this->action = $action;
+            return $this;
+        } else {
+            $name = $this->action ?: '';
+            return true === $action ? $name : strtolower($name);
+        }
+    }
+
+    /**
+     * 设置或者获取当前的语言
+     * @access public
+     * @param string $lang 语言名
+     * @return string|Request
+     */
+    public function langset($lang = null)
+    {
+        if (!is_null($lang)) {
+            $this->langset = $lang;
+            return $this;
+        } else {
+            return $this->langset ?: '';
+        }
+    }
+
+    /**
+     * 设置或者获取当前请求的content
+     * @access public
+     * @return string
+     */
+    public function getContent()
+    {
+        if (is_null($this->content)) {
+            $this->content = $this->input;
+        }
+        return $this->content;
+    }
+
+    /**
+     * 获取当前请求的php://input
+     * @access public
+     * @return string
+     */
+    public function getInput()
+    {
+        return $this->input;
+    }
+
+    /**
+     * 生成请求令牌
+     * @access public
+     * @param string $name 令牌名称
+     * @param mixed  $type 令牌生成方法
+     * @return string
+     */
+    public function token($name = '__token__', $type = 'md5')
+    {
+        $type  = is_callable($type) ? $type : 'md5';
+        $token = call_user_func($type, $_SERVER['REQUEST_TIME_FLOAT']);
+        if ($this->isAjax()) {
+            header($name . ': ' . $token);
+        }
+        Session::set($name, $token);
+        return $token;
+    }
+
+    /**
+     * 设置当前地址的请求缓存
+     * @access public
+     * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id
+     * @param mixed  $expire 缓存有效期
+     * @param array  $except 缓存排除
+     * @param string $tag    缓存标签
+     * @return void
+     */
+    public function cache($key, $expire = null, $except = [], $tag = null)
+    {
+        if (!is_array($except)) {
+            $tag    = $except;
+            $except = [];
+        }
+
+        if (false !== $key && $this->isGet() && !$this->isCheckCache) {
+            // 标记请求缓存检查
+            $this->isCheckCache = true;
+            if (false === $expire) {
+                // 关闭当前缓存
+                return;
+            }
+            if ($key instanceof \Closure) {
+                $key = call_user_func_array($key, [$this]);
+            } elseif (true === $key) {
+                foreach ($except as $rule) {
+                    if (0 === stripos($this->url(), $rule)) {
+                        return;
+                    }
+                }
+                // 自动缓存功能
+                $key = '__URL__';
+            } elseif (strpos($key, '|')) {
+                list($key, $fun) = explode('|', $key);
+            }
+            // 特殊规则替换
+            if (false !== strpos($key, '__')) {
+                $key = str_replace(['__MODULE__', '__CONTROLLER__', '__ACTION__', '__URL__', ''], [$this->module, $this->controller, $this->action, md5($this->url(true))], $key);
+            }
+
+            if (false !== strpos($key, ':')) {
+                $param = $this->param();
+                foreach ($param as $item => $val) {
+                    if (is_string($val) && false !== strpos($key, ':' . $item)) {
+                        $key = str_replace(':' . $item, $val, $key);
+                    }
+                }
+            } elseif (strpos($key, ']')) {
+                if ('[' . $this->ext() . ']' == $key) {
+                    // 缓存某个后缀的请求
+                    $key = md5($this->url());
+                } else {
+                    return;
+                }
+            }
+            if (isset($fun)) {
+                $key = $fun($key);
+            }
+
+            if (strtotime($this->server('HTTP_IF_MODIFIED_SINCE')) + $expire > $_SERVER['REQUEST_TIME']) {
+                // 读取缓存
+                $response = Response::create()->code(304);
+                throw new \think\exception\HttpResponseException($response);
+            } elseif (Cache::has($key)) {
+                list($content, $header) = Cache::get($key);
+                $response               = Response::create($content)->header($header);
+                throw new \think\exception\HttpResponseException($response);
+            } else {
+                $this->cache = [$key, $expire, $tag];
+            }
+        }
+    }
+
+    /**
+     * 读取请求缓存设置
+     * @access public
+     * @return array
+     */
+    public function getCache()
+    {
+        return $this->cache;
+    }
+
+    /**
+     * 设置当前请求绑定的对象实例
+     * @access public
+     * @param string|array $name 绑定的对象标识
+     * @param mixed  $obj 绑定的对象实例
+     * @return mixed
+     */
+    public function bind($name, $obj = null)
+    {
+        if (is_array($name)) {
+            $this->bind = array_merge($this->bind, $name);
+        } else {
+            $this->bind[$name] = $obj;
+        }
+    }
+
+    public function __set($name, $value)
+    {
+        $this->bind[$name] = $value;
+    }
+
+    public function __get($name)
+    {
+        return isset($this->bind[$name]) ? $this->bind[$name] : null;
+    }
+
+    public function __isset($name)
+    {
+        return isset($this->bind[$name]);
+    }
+}

+ 332 - 0
thinkphp/library/think/Response.php

@@ -0,0 +1,332 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\response\Json as JsonResponse;
+use think\response\Jsonp as JsonpResponse;
+use think\response\Redirect as RedirectResponse;
+use think\response\View as ViewResponse;
+use think\response\Xml as XmlResponse;
+
+class Response
+{
+    // 原始数据
+    protected $data;
+
+    // 当前的contentType
+    protected $contentType = 'text/html';
+
+    // 字符集
+    protected $charset = 'utf-8';
+
+    //状态
+    protected $code = 200;
+
+    // 输出参数
+    protected $options = [];
+    // header参数
+    protected $header = [];
+
+    protected $content = null;
+
+    /**
+     * 构造函数
+     * @access   public
+     * @param mixed $data    输出数据
+     * @param int   $code
+     * @param array $header
+     * @param array $options 输出参数
+     */
+    public function __construct($data = '', $code = 200, array $header = [], $options = [])
+    {
+        $this->data($data);
+        if (!empty($options)) {
+            $this->options = array_merge($this->options, $options);
+        }
+        $this->contentType($this->contentType, $this->charset);
+        $this->header = array_merge($this->header, $header);
+        $this->code   = $code;
+    }
+
+    /**
+     * 创建Response对象
+     * @access public
+     * @param mixed  $data    输出数据
+     * @param string $type    输出类型
+     * @param int    $code
+     * @param array  $header
+     * @param array  $options 输出参数
+     * @return Response|JsonResponse|ViewResponse|XmlResponse|RedirectResponse|JsonpResponse
+     */
+    public static function create($data = '', $type = '', $code = 200, array $header = [], $options = [])
+    {
+        $class = false !== strpos($type, '\\') ? $type : '\\think\\response\\' . ucfirst(strtolower($type));
+        if (class_exists($class)) {
+            $response = new $class($data, $code, $header, $options);
+        } else {
+            $response = new static($data, $code, $header, $options);
+        }
+
+        return $response;
+    }
+
+    /**
+     * 发送数据到客户端
+     * @access public
+     * @return mixed
+     * @throws \InvalidArgumentException
+     */
+    public function send()
+    {
+        // 监听response_send
+        Hook::listen('response_send', $this);
+
+        // 处理输出数据
+        $data = $this->getContent();
+
+        // Trace调试注入
+        if (Env::get('app_trace', Config::get('app_trace'))) {
+            Debug::inject($this, $data);
+        }
+
+        if (200 == $this->code) {
+            $cache = Request::instance()->getCache();
+            if ($cache) {
+                $this->header['Cache-Control'] = 'max-age=' . $cache[1] . ',must-revalidate';
+                $this->header['Last-Modified'] = gmdate('D, d M Y H:i:s') . ' GMT';
+                $this->header['Expires']       = gmdate('D, d M Y H:i:s', $_SERVER['REQUEST_TIME'] + $cache[1]) . ' GMT';
+                Cache::tag($cache[2])->set($cache[0], [$data, $this->header], $cache[1]);
+            }
+        }
+
+        if (!headers_sent() && !empty($this->header)) {
+            // 发送状态码
+            http_response_code($this->code);
+            // 发送头部信息
+            foreach ($this->header as $name => $val) {
+                if (is_null($val)) {
+                    header($name);
+                } else {
+                    header($name . ':' . $val);
+                }
+            }
+        }
+
+        echo $data;
+
+        if (function_exists('fastcgi_finish_request')) {
+            // 提高页面响应
+            fastcgi_finish_request();
+        }
+
+        // 监听response_end
+        Hook::listen('response_end', $this);
+
+        // 清空当次请求有效的数据
+        if (!($this instanceof RedirectResponse)) {
+            Session::flush();
+        }
+    }
+
+    /**
+     * 处理数据
+     * @access protected
+     * @param mixed $data 要处理的数据
+     * @return mixed
+     */
+    protected function output($data)
+    {
+        return $data;
+    }
+
+    /**
+     * 输出的参数
+     * @access public
+     * @param mixed $options 输出参数
+     * @return $this
+     */
+    public function options($options = [])
+    {
+        $this->options = array_merge($this->options, $options);
+        return $this;
+    }
+
+    /**
+     * 输出数据设置
+     * @access public
+     * @param mixed $data 输出数据
+     * @return $this
+     */
+    public function data($data)
+    {
+        $this->data = $data;
+        return $this;
+    }
+
+    /**
+     * 设置响应头
+     * @access public
+     * @param string|array $name  参数名
+     * @param string       $value 参数值
+     * @return $this
+     */
+    public function header($name, $value = null)
+    {
+        if (is_array($name)) {
+            $this->header = array_merge($this->header, $name);
+        } else {
+            $this->header[$name] = $value;
+        }
+        return $this;
+    }
+
+    /**
+     * 设置页面输出内容
+     * @param $content
+     * @return $this
+     */
+    public function content($content)
+    {
+        if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([
+            $content,
+            '__toString',
+        ])
+        ) {
+            throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content)));
+        }
+
+        $this->content = (string) $content;
+
+        return $this;
+    }
+
+    /**
+     * 发送HTTP状态
+     * @param integer $code 状态码
+     * @return $this
+     */
+    public function code($code)
+    {
+        $this->code = $code;
+        return $this;
+    }
+
+    /**
+     * LastModified
+     * @param string $time
+     * @return $this
+     */
+    public function lastModified($time)
+    {
+        $this->header['Last-Modified'] = $time;
+        return $this;
+    }
+
+    /**
+     * Expires
+     * @param string $time
+     * @return $this
+     */
+    public function expires($time)
+    {
+        $this->header['Expires'] = $time;
+        return $this;
+    }
+
+    /**
+     * ETag
+     * @param string $eTag
+     * @return $this
+     */
+    public function eTag($eTag)
+    {
+        $this->header['ETag'] = $eTag;
+        return $this;
+    }
+
+    /**
+     * 页面缓存控制
+     * @param string $cache 状态码
+     * @return $this
+     */
+    public function cacheControl($cache)
+    {
+        $this->header['Cache-control'] = $cache;
+        return $this;
+    }
+
+    /**
+     * 页面输出类型
+     * @param string $contentType 输出类型
+     * @param string $charset     输出编码
+     * @return $this
+     */
+    public function contentType($contentType, $charset = 'utf-8')
+    {
+        $this->header['Content-Type'] = $contentType . '; charset=' . $charset;
+        return $this;
+    }
+
+    /**
+     * 获取头部信息
+     * @param string $name 头部名称
+     * @return mixed
+     */
+    public function getHeader($name = '')
+    {
+        if (!empty($name)) {
+            return isset($this->header[$name]) ? $this->header[$name] : null;
+        } else {
+            return $this->header;
+        }
+    }
+
+    /**
+     * 获取原始数据
+     * @return mixed
+     */
+    public function getData()
+    {
+        return $this->data;
+    }
+
+    /**
+     * 获取输出数据
+     * @return mixed
+     */
+    public function getContent()
+    {
+        if (null == $this->content) {
+            $content = $this->output($this->data);
+
+            if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([
+                $content,
+                '__toString',
+            ])
+            ) {
+                throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content)));
+            }
+
+            $this->content = (string) $content;
+        }
+        return $this->content;
+    }
+
+    /**
+     * 获取状态码
+     * @return integer
+     */
+    public function getCode()
+    {
+        return $this->code;
+    }
+}

+ 1645 - 0
thinkphp/library/think/Route.php

@@ -0,0 +1,1645 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\exception\HttpException;
+
+class Route
+{
+    // 路由规则
+    private static $rules = [
+        'get'     => [],
+        'post'    => [],
+        'put'     => [],
+        'delete'  => [],
+        'patch'   => [],
+        'head'    => [],
+        'options' => [],
+        '*'       => [],
+        'alias'   => [],
+        'domain'  => [],
+        'pattern' => [],
+        'name'    => [],
+    ];
+
+    // REST路由操作方法定义
+    private static $rest = [
+        'index'  => ['get', '', 'index'],
+        'create' => ['get', '/create', 'create'],
+        'edit'   => ['get', '/:id/edit', 'edit'],
+        'read'   => ['get', '/:id', 'read'],
+        'save'   => ['post', '', 'save'],
+        'update' => ['put', '/:id', 'update'],
+        'delete' => ['delete', '/:id', 'delete'],
+    ];
+
+    // 不同请求类型的方法前缀
+    private static $methodPrefix = [
+        'get'    => 'get',
+        'post'   => 'post',
+        'put'    => 'put',
+        'delete' => 'delete',
+        'patch'  => 'patch',
+    ];
+
+    // 子域名
+    private static $subDomain = '';
+    // 域名绑定
+    private static $bind = [];
+    // 当前分组信息
+    private static $group = [];
+    // 当前子域名绑定
+    private static $domainBind;
+    private static $domainRule;
+    // 当前域名
+    private static $domain;
+    // 当前路由执行过程中的参数
+    private static $option = [];
+
+    /**
+     * 注册变量规则
+     * @access public
+     * @param string|array $name 变量名
+     * @param string       $rule 变量规则
+     * @return void
+     */
+    public static function pattern($name = null, $rule = '')
+    {
+        if (is_array($name)) {
+            self::$rules['pattern'] = array_merge(self::$rules['pattern'], $name);
+        } else {
+            self::$rules['pattern'][$name] = $rule;
+        }
+    }
+
+    /**
+     * 注册子域名部署规则
+     * @access public
+     * @param string|array $domain  子域名
+     * @param mixed        $rule    路由规则
+     * @param array        $option  路由参数
+     * @param array        $pattern 变量规则
+     * @return void
+     */
+    public static function domain($domain, $rule = '', $option = [], $pattern = [])
+    {
+        if (is_array($domain)) {
+            foreach ($domain as $key => $item) {
+                self::domain($key, $item, $option, $pattern);
+            }
+        } elseif ($rule instanceof \Closure) {
+            // 执行闭包
+            self::setDomain($domain);
+            call_user_func_array($rule, []);
+            self::setDomain(null);
+        } elseif (is_array($rule)) {
+            self::setDomain($domain);
+            self::group('', function () use ($rule) {
+                // 动态注册域名的路由规则
+                self::registerRules($rule);
+            }, $option, $pattern);
+            self::setDomain(null);
+        } else {
+            self::$rules['domain'][$domain]['[bind]'] = [$rule, $option, $pattern];
+        }
+    }
+
+    private static function setDomain($domain)
+    {
+        self::$domain = $domain;
+    }
+
+    /**
+     * 设置路由绑定
+     * @access public
+     * @param mixed  $bind 绑定信息
+     * @param string $type 绑定类型 默认为module 支持 namespace class controller
+     * @return mixed
+     */
+    public static function bind($bind, $type = 'module')
+    {
+        self::$bind = ['type' => $type, $type => $bind];
+    }
+
+    /**
+     * 设置或者获取路由标识
+     * @access public
+     * @param string|array $name  路由命名标识 数组表示批量设置
+     * @param array        $value 路由地址及变量信息
+     * @return array
+     */
+    public static function name($name = '', $value = null)
+    {
+        if (is_array($name)) {
+            return self::$rules['name'] = $name;
+        } elseif ('' === $name) {
+            return self::$rules['name'];
+        } elseif (!is_null($value)) {
+            self::$rules['name'][strtolower($name)][] = $value;
+        } else {
+            $name = strtolower($name);
+            return isset(self::$rules['name'][$name]) ? self::$rules['name'][$name] : null;
+        }
+    }
+
+    /**
+     * 读取路由绑定
+     * @access public
+     * @param string $type 绑定类型
+     * @return mixed
+     */
+    public static function getBind($type)
+    {
+        return isset(self::$bind[$type]) ? self::$bind[$type] : null;
+    }
+
+    /**
+     * 导入配置文件的路由规则
+     * @access public
+     * @param array  $rule 路由规则
+     * @param string $type 请求类型
+     * @return void
+     */
+    public static function import(array $rule, $type = '*')
+    {
+        // 检查域名部署
+        if (isset($rule['__domain__'])) {
+            self::domain($rule['__domain__']);
+            unset($rule['__domain__']);
+        }
+
+        // 检查变量规则
+        if (isset($rule['__pattern__'])) {
+            self::pattern($rule['__pattern__']);
+            unset($rule['__pattern__']);
+        }
+
+        // 检查路由别名
+        if (isset($rule['__alias__'])) {
+            self::alias($rule['__alias__']);
+            unset($rule['__alias__']);
+        }
+
+        // 检查资源路由
+        if (isset($rule['__rest__'])) {
+            self::resource($rule['__rest__']);
+            unset($rule['__rest__']);
+        }
+
+        self::registerRules($rule, strtolower($type));
+    }
+
+    // 批量注册路由
+    protected static function registerRules($rules, $type = '*')
+    {
+        foreach ($rules as $key => $val) {
+            if (is_numeric($key)) {
+                $key = array_shift($val);
+            }
+            if (empty($val)) {
+                continue;
+            }
+            if (is_string($key) && 0 === strpos($key, '[')) {
+                $key = substr($key, 1, -1);
+                self::group($key, $val);
+            } elseif (is_array($val)) {
+                self::setRule($key, $val[0], $type, $val[1], isset($val[2]) ? $val[2] : []);
+            } else {
+                self::setRule($key, $val, $type);
+            }
+        }
+    }
+
+    /**
+     * 注册路由规则
+     * @access public
+     * @param string|array $rule    路由规则
+     * @param string       $route   路由地址
+     * @param string       $type    请求类型
+     * @param array        $option  路由参数
+     * @param array        $pattern 变量规则
+     * @return void
+     */
+    public static function rule($rule, $route = '', $type = '*', $option = [], $pattern = [])
+    {
+        $group = self::getGroup('name');
+
+        if (!is_null($group)) {
+            // 路由分组
+            $option  = array_merge(self::getGroup('option'), $option);
+            $pattern = array_merge(self::getGroup('pattern'), $pattern);
+        }
+
+        $type = strtolower($type);
+
+        if (strpos($type, '|')) {
+            $option['method'] = $type;
+            $type             = '*';
+        }
+        if (is_array($rule) && empty($route)) {
+            foreach ($rule as $key => $val) {
+                if (is_numeric($key)) {
+                    $key = array_shift($val);
+                }
+                if (is_array($val)) {
+                    $route    = $val[0];
+                    $option1  = array_merge($option, $val[1]);
+                    $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []);
+                } else {
+                    $option1  = null;
+                    $pattern1 = null;
+                    $route    = $val;
+                }
+                self::setRule($key, $route, $type, !is_null($option1) ? $option1 : $option, !is_null($pattern1) ? $pattern1 : $pattern, $group);
+            }
+        } else {
+            self::setRule($rule, $route, $type, $option, $pattern, $group);
+        }
+
+    }
+
+    /**
+     * 设置路由规则
+     * @access public
+     * @param string|array $rule    路由规则
+     * @param string       $route   路由地址
+     * @param string       $type    请求类型
+     * @param array        $option  路由参数
+     * @param array        $pattern 变量规则
+     * @param string       $group   所属分组
+     * @return void
+     */
+    protected static function setRule($rule, $route, $type = '*', $option = [], $pattern = [], $group = '')
+    {
+        if (is_array($rule)) {
+            $name = $rule[0];
+            $rule = $rule[1];
+        } elseif (is_string($route)) {
+            $name = $route;
+        }
+        if (!isset($option['complete_match'])) {
+            if (Config::get('route_complete_match')) {
+                $option['complete_match'] = true;
+            } elseif ('$' == substr($rule, -1, 1)) {
+                // 是否完整匹配
+                $option['complete_match'] = true;
+            }
+        } elseif (empty($option['complete_match']) && '$' == substr($rule, -1, 1)) {
+            // 是否完整匹配
+            $option['complete_match'] = true;
+        }
+
+        if ('$' == substr($rule, -1, 1)) {
+            $rule = substr($rule, 0, -1);
+        }
+
+        if ('/' != $rule || $group) {
+            $rule = trim($rule, '/');
+        }
+        $vars = self::parseVar($rule);
+        if (isset($name)) {
+            $key    = $group ? $group . ($rule ? '/' . $rule : '') : $rule;
+            $suffix = isset($option['ext']) ? $option['ext'] : null;
+            self::name($name, [$key, $vars, self::$domain, $suffix]);
+        }
+        if (isset($option['modular'])) {
+            $route = $option['modular'] . '/' . $route;
+        }
+        if ($group) {
+            if ('*' != $type) {
+                $option['method'] = $type;
+            }
+            if (self::$domain) {
+                self::$rules['domain'][self::$domain]['*'][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern];
+            } else {
+                self::$rules['*'][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern];
+            }
+        } else {
+            if ('*' != $type && isset(self::$rules['*'][$rule])) {
+                unset(self::$rules['*'][$rule]);
+            }
+            if (self::$domain) {
+                self::$rules['domain'][self::$domain][$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern];
+            } else {
+                self::$rules[$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern];
+            }
+            if ('*' == $type) {
+                // 注册路由快捷方式
+                foreach (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as $method) {
+                    if (self::$domain && !isset(self::$rules['domain'][self::$domain][$method][$rule])) {
+                        self::$rules['domain'][self::$domain][$method][$rule] = true;
+                    } elseif (!self::$domain && !isset(self::$rules[$method][$rule])) {
+                        self::$rules[$method][$rule] = true;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 设置当前执行的参数信息
+     * @access public
+     * @param array $options 参数信息
+     * @return mixed
+     */
+    protected static function setOption($options = [])
+    {
+        self::$option[] = $options;
+    }
+
+    /**
+     * 获取当前执行的所有参数信息
+     * @access public
+     * @return array
+     */
+    public static function getOption()
+    {
+        return self::$option;
+    }
+
+    /**
+     * 获取当前的分组信息
+     * @access public
+     * @param string $type 分组信息名称 name option pattern
+     * @return mixed
+     */
+    public static function getGroup($type)
+    {
+        if (isset(self::$group[$type])) {
+            return self::$group[$type];
+        } else {
+            return 'name' == $type ? null : [];
+        }
+    }
+
+    /**
+     * 设置当前的路由分组
+     * @access public
+     * @param string $name    分组名称
+     * @param array  $option  分组路由参数
+     * @param array  $pattern 分组变量规则
+     * @return void
+     */
+    public static function setGroup($name, $option = [], $pattern = [])
+    {
+        self::$group['name']    = $name;
+        self::$group['option']  = $option ?: [];
+        self::$group['pattern'] = $pattern ?: [];
+    }
+
+    /**
+     * 注册路由分组
+     * @access public
+     * @param string|array   $name    分组名称或者参数
+     * @param array|\Closure $routes  路由地址
+     * @param array          $option  路由参数
+     * @param array          $pattern 变量规则
+     * @return void
+     */
+    public static function group($name, $routes, $option = [], $pattern = [])
+    {
+        if (is_array($name)) {
+            $option = $name;
+            $name   = isset($option['name']) ? $option['name'] : '';
+        }
+        // 分组
+        $currentGroup = self::getGroup('name');
+        if ($currentGroup) {
+            $name = $currentGroup . ($name ? '/' . ltrim($name, '/') : '');
+        }
+        if (!empty($name)) {
+            if ($routes instanceof \Closure) {
+                $currentOption  = self::getGroup('option');
+                $currentPattern = self::getGroup('pattern');
+                self::setGroup($name, array_merge($currentOption, $option), array_merge($currentPattern, $pattern));
+                call_user_func_array($routes, []);
+                self::setGroup($currentGroup, $currentOption, $currentPattern);
+                if ($currentGroup != $name) {
+                    self::$rules['*'][$name]['route']   = '';
+                    self::$rules['*'][$name]['var']     = self::parseVar($name);
+                    self::$rules['*'][$name]['option']  = $option;
+                    self::$rules['*'][$name]['pattern'] = $pattern;
+                }
+            } else {
+                $item          = [];
+                $completeMatch = Config::get('route_complete_match');
+                foreach ($routes as $key => $val) {
+                    if (is_numeric($key)) {
+                        $key = array_shift($val);
+                    }
+                    if (is_array($val)) {
+                        $route    = $val[0];
+                        $option1  = array_merge($option, isset($val[1]) ? $val[1] : []);
+                        $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []);
+                    } else {
+                        $route = $val;
+                    }
+
+                    $options  = isset($option1) ? $option1 : $option;
+                    $patterns = isset($pattern1) ? $pattern1 : $pattern;
+                    if ('$' == substr($key, -1, 1)) {
+                        // 是否完整匹配
+                        $options['complete_match'] = true;
+                        $key                       = substr($key, 0, -1);
+                    } elseif ($completeMatch) {
+                        $options['complete_match'] = true;
+                    }
+                    $key    = trim($key, '/');
+                    $vars   = self::parseVar($key);
+                    $item[] = ['rule' => $key, 'route' => $route, 'var' => $vars, 'option' => $options, 'pattern' => $patterns];
+                    // 设置路由标识
+                    $suffix = isset($options['ext']) ? $options['ext'] : null;
+                    self::name($route, [$name . ($key ? '/' . $key : ''), $vars, self::$domain, $suffix]);
+                }
+                self::$rules['*'][$name] = ['rule' => $item, 'route' => '', 'var' => [], 'option' => $option, 'pattern' => $pattern];
+            }
+
+            foreach (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as $method) {
+                if (!isset(self::$rules[$method][$name])) {
+                    self::$rules[$method][$name] = true;
+                } elseif (is_array(self::$rules[$method][$name])) {
+                    self::$rules[$method][$name] = array_merge(self::$rules['*'][$name], self::$rules[$method][$name]);
+                }
+            }
+
+        } elseif ($routes instanceof \Closure) {
+            // 闭包注册
+            $currentOption  = self::getGroup('option');
+            $currentPattern = self::getGroup('pattern');
+            self::setGroup('', array_merge($currentOption, $option), array_merge($currentPattern, $pattern));
+            call_user_func_array($routes, []);
+            self::setGroup($currentGroup, $currentOption, $currentPattern);
+        } else {
+            // 批量注册路由
+            self::rule($routes, '', '*', $option, $pattern);
+        }
+    }
+
+    /**
+     * 注册路由
+     * @access public
+     * @param string|array $rule    路由规则
+     * @param string       $route   路由地址
+     * @param array        $option  路由参数
+     * @param array        $pattern 变量规则
+     * @return void
+     */
+    public static function any($rule, $route = '', $option = [], $pattern = [])
+    {
+        self::rule($rule, $route, '*', $option, $pattern);
+    }
+
+    /**
+     * 注册GET路由
+     * @access public
+     * @param string|array $rule    路由规则
+     * @param string       $route   路由地址
+     * @param array        $option  路由参数
+     * @param array        $pattern 变量规则
+     * @return void
+     */
+    public static function get($rule, $route = '', $option = [], $pattern = [])
+    {
+        self::rule($rule, $route, 'GET', $option, $pattern);
+    }
+
+    /**
+     * 注册POST路由
+     * @access public
+     * @param string|array $rule    路由规则
+     * @param string       $route   路由地址
+     * @param array        $option  路由参数
+     * @param array        $pattern 变量规则
+     * @return void
+     */
+    public static function post($rule, $route = '', $option = [], $pattern = [])
+    {
+        self::rule($rule, $route, 'POST', $option, $pattern);
+    }
+
+    /**
+     * 注册PUT路由
+     * @access public
+     * @param string|array $rule    路由规则
+     * @param string       $route   路由地址
+     * @param array        $option  路由参数
+     * @param array        $pattern 变量规则
+     * @return void
+     */
+    public static function put($rule, $route = '', $option = [], $pattern = [])
+    {
+        self::rule($rule, $route, 'PUT', $option, $pattern);
+    }
+
+    /**
+     * 注册DELETE路由
+     * @access public
+     * @param string|array $rule    路由规则
+     * @param string       $route   路由地址
+     * @param array        $option  路由参数
+     * @param array        $pattern 变量规则
+     * @return void
+     */
+    public static function delete($rule, $route = '', $option = [], $pattern = [])
+    {
+        self::rule($rule, $route, 'DELETE', $option, $pattern);
+    }
+
+    /**
+     * 注册PATCH路由
+     * @access public
+     * @param string|array $rule    路由规则
+     * @param string       $route   路由地址
+     * @param array        $option  路由参数
+     * @param array        $pattern 变量规则
+     * @return void
+     */
+    public static function patch($rule, $route = '', $option = [], $pattern = [])
+    {
+        self::rule($rule, $route, 'PATCH', $option, $pattern);
+    }
+
+    /**
+     * 注册资源路由
+     * @access public
+     * @param string|array $rule    路由规则
+     * @param string       $route   路由地址
+     * @param array        $option  路由参数
+     * @param array        $pattern 变量规则
+     * @return void
+     */
+    public static function resource($rule, $route = '', $option = [], $pattern = [])
+    {
+        if (is_array($rule)) {
+            foreach ($rule as $key => $val) {
+                if (is_array($val)) {
+                    list($val, $option, $pattern) = array_pad($val, 3, []);
+                }
+                self::resource($key, $val, $option, $pattern);
+            }
+        } else {
+            if (strpos($rule, '.')) {
+                // 注册嵌套资源路由
+                $array = explode('.', $rule);
+                $last  = array_pop($array);
+                $item  = [];
+                foreach ($array as $val) {
+                    $item[] = $val . '/:' . (isset($option['var'][$val]) ? $option['var'][$val] : $val . '_id');
+                }
+                $rule = implode('/', $item) . '/' . $last;
+            }
+            // 注册资源路由
+            foreach (self::$rest as $key => $val) {
+                if ((isset($option['only']) && !in_array($key, $option['only']))
+                    || (isset($option['except']) && in_array($key, $option['except']))) {
+                    continue;
+                }
+                if (isset($last) && strpos($val[1], ':id') && isset($option['var'][$last])) {
+                    $val[1] = str_replace(':id', ':' . $option['var'][$last], $val[1]);
+                } elseif (strpos($val[1], ':id') && isset($option['var'][$rule])) {
+                    $val[1] = str_replace(':id', ':' . $option['var'][$rule], $val[1]);
+                }
+                $item           = ltrim($rule . $val[1], '/');
+                $option['rest'] = $key;
+                self::rule($item . '$', $route . '/' . $val[2], $val[0], $option, $pattern);
+            }
+        }
+    }
+
+    /**
+     * 注册控制器路由 操作方法对应不同的请求后缀
+     * @access public
+     * @param string $rule    路由规则
+     * @param string $route   路由地址
+     * @param array  $option  路由参数
+     * @param array  $pattern 变量规则
+     * @return void
+     */
+    public static function controller($rule, $route = '', $option = [], $pattern = [])
+    {
+        foreach (self::$methodPrefix as $type => $val) {
+            self::$type($rule . '/:action', $route . '/' . $val . ':action', $option, $pattern);
+        }
+    }
+
+    /**
+     * 注册别名路由
+     * @access public
+     * @param string|array $rule   路由别名
+     * @param string       $route  路由地址
+     * @param array        $option 路由参数
+     * @return void
+     */
+    public static function alias($rule = null, $route = '', $option = [])
+    {
+        if (is_array($rule)) {
+            self::$rules['alias'] = array_merge(self::$rules['alias'], $rule);
+        } else {
+            self::$rules['alias'][$rule] = $option ? [$route, $option] : $route;
+        }
+    }
+
+    /**
+     * 设置不同请求类型下面的方法前缀
+     * @access public
+     * @param string $method 请求类型
+     * @param string $prefix 类型前缀
+     * @return void
+     */
+    public static function setMethodPrefix($method, $prefix = '')
+    {
+        if (is_array($method)) {
+            self::$methodPrefix = array_merge(self::$methodPrefix, array_change_key_case($method));
+        } else {
+            self::$methodPrefix[strtolower($method)] = $prefix;
+        }
+    }
+
+    /**
+     * rest方法定义和修改
+     * @access public
+     * @param string|array $name     方法名称
+     * @param array|bool   $resource 资源
+     * @return void
+     */
+    public static function rest($name, $resource = [])
+    {
+        if (is_array($name)) {
+            self::$rest = $resource ? $name : array_merge(self::$rest, $name);
+        } else {
+            self::$rest[$name] = $resource;
+        }
+    }
+
+    /**
+     * 注册未匹配路由规则后的处理
+     * @access public
+     * @param string $route  路由地址
+     * @param string $method 请求类型
+     * @param array  $option 路由参数
+     * @return void
+     */
+    public static function miss($route, $method = '*', $option = [])
+    {
+        self::rule('__miss__', $route, $method, $option, []);
+    }
+
+    /**
+     * 注册一个自动解析的URL路由
+     * @access public
+     * @param string $route 路由地址
+     * @return void
+     */
+    public static function auto($route)
+    {
+        self::rule('__auto__', $route, '*', [], []);
+    }
+
+    /**
+     * 获取或者批量设置路由定义
+     * @access public
+     * @param mixed $rules 请求类型或者路由定义数组
+     * @return array
+     */
+    public static function rules($rules = '')
+    {
+        if (is_array($rules)) {
+            self::$rules = $rules;
+        } elseif ($rules) {
+            return true === $rules ? self::$rules : self::$rules[strtolower($rules)];
+        } else {
+            $rules = self::$rules;
+            unset($rules['pattern'], $rules['alias'], $rules['domain'], $rules['name']);
+            return $rules;
+        }
+    }
+
+    /**
+     * 检测子域名部署
+     * @access public
+     * @param Request $request      Request请求对象
+     * @param array   $currentRules 当前路由规则
+     * @param string  $method       请求类型
+     * @return void
+     */
+    public static function checkDomain($request, &$currentRules, $method = 'get')
+    {
+        // 域名规则
+        $rules = self::$rules['domain'];
+        // 开启子域名部署 支持二级和三级域名
+        if (!empty($rules)) {
+            $host = $request->host(true);
+            if (isset($rules[$host])) {
+                // 完整域名或者IP配置
+                $item = $rules[$host];
+            } else {
+                $rootDomain = Config::get('url_domain_root');
+                if ($rootDomain) {
+                    // 配置域名根 例如 thinkphp.cn 163.com.cn 如果是国家级域名 com.cn net.cn 之类的域名需要配置
+                    $domain = explode('.', rtrim(stristr($host, $rootDomain, true), '.'));
+                } else {
+                    $domain = explode('.', $host, -2);
+                }
+                // 子域名配置
+                if (!empty($domain)) {
+                    // 当前子域名
+                    $subDomain       = implode('.', $domain);
+                    self::$subDomain = $subDomain;
+                    $domain2         = array_pop($domain);
+                    if ($domain) {
+                        // 存在三级域名
+                        $domain3 = array_pop($domain);
+                    }
+                    if ($subDomain && isset($rules[$subDomain])) {
+                        // 子域名配置
+                        $item = $rules[$subDomain];
+                    } elseif (isset($rules['*.' . $domain2]) && !empty($domain3)) {
+                        // 泛三级域名
+                        $item      = $rules['*.' . $domain2];
+                        $panDomain = $domain3;
+                    } elseif (isset($rules['*']) && !empty($domain2)) {
+                        // 泛二级域名
+                        if ('www' != $domain2) {
+                            $item      = $rules['*'];
+                            $panDomain = $domain2;
+                        }
+                    }
+                }
+            }
+            if (!empty($item)) {
+                if (isset($panDomain)) {
+                    // 保存当前泛域名
+                    $request->route(['__domain__' => $panDomain]);
+                }
+                if (isset($item['[bind]'])) {
+                    // 解析子域名部署规则
+                    list($rule, $option, $pattern) = $item['[bind]'];
+                    if (!empty($option['https']) && !$request->isSsl()) {
+                        // https检测
+                        throw new HttpException(404, 'must use https request:' . $host);
+                    }
+
+                    if (strpos($rule, '?')) {
+                        // 传入其它参数
+                        $array  = parse_url($rule);
+                        $result = $array['path'];
+                        parse_str($array['query'], $params);
+                        if (isset($panDomain)) {
+                            $pos = array_search('*', $params);
+                            if (false !== $pos) {
+                                // 泛域名作为参数
+                                $params[$pos] = $panDomain;
+                            }
+                        }
+                        $_GET = array_merge($_GET, $params);
+                    } else {
+                        $result = $rule;
+                    }
+
+                    if (0 === strpos($result, '\\')) {
+                        // 绑定到命名空间 例如 \app\index\behavior
+                        self::$bind = ['type' => 'namespace', 'namespace' => $result];
+                    } elseif (0 === strpos($result, '@')) {
+                        // 绑定到类 例如 @app\index\controller\User
+                        self::$bind = ['type' => 'class', 'class' => substr($result, 1)];
+                    } else {
+                        // 绑定到模块/控制器 例如 index/user
+                        self::$bind = ['type' => 'module', 'module' => $result];
+                    }
+                    self::$domainBind = true;
+                } else {
+                    self::$domainRule = $item;
+                    $currentRules     = isset($item[$method]) ? $item[$method] : $item['*'];
+                }
+            }
+        }
+    }
+
+    /**
+     * 检测URL路由
+     * @access public
+     * @param Request $request     Request请求对象
+     * @param string  $url         URL地址
+     * @param string  $depr        URL分隔符
+     * @param bool    $checkDomain 是否检测域名规则
+     * @return false|array
+     */
+    public static function check($request, $url, $depr = '/', $checkDomain = false)
+    {
+        //检查解析缓存
+        if (!App::$debug && Config::get('route_check_cache')) {
+            $key = self::getCheckCacheKey($request);
+            if (Cache::has($key)) {
+                list($rule, $route, $pathinfo, $option, $matches) = Cache::get($key);
+                return self::parseRule($rule, $route, $pathinfo, $option, $matches, true);
+            }
+        }
+
+        // 分隔符替换 确保路由定义使用统一的分隔符
+        $url = str_replace($depr, '|', $url);
+
+        if (isset(self::$rules['alias'][$url]) || isset(self::$rules['alias'][strstr($url, '|', true)])) {
+            // 检测路由别名
+            $result = self::checkRouteAlias($request, $url, $depr);
+            if (false !== $result) {
+                return $result;
+            }
+        }
+        $method = strtolower($request->method());
+        // 获取当前请求类型的路由规则
+        $rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];
+        // 检测域名部署
+        if ($checkDomain) {
+            self::checkDomain($request, $rules, $method);
+        }
+        // 检测URL绑定
+        $return = self::checkUrlBind($url, $rules, $depr);
+        if (false !== $return) {
+            return $return;
+        }
+        if ('|' != $url) {
+            $url = rtrim($url, '|');
+        }
+        $item = str_replace('|', '/', $url);
+        if (isset($rules[$item])) {
+            // 静态路由规则检测
+            $rule = $rules[$item];
+            if (true === $rule) {
+                $rule = self::getRouteExpress($item);
+            }
+            if (!empty($rule['route']) && self::checkOption($rule['option'], $request)) {
+                self::setOption($rule['option']);
+                return self::parseRule($item, $rule['route'], $url, $rule['option']);
+            }
+        }
+
+        // 路由规则检测
+        if (!empty($rules)) {
+            return self::checkRoute($request, $rules, $url, $depr);
+        }
+        return false;
+    }
+
+    private static function getRouteExpress($key)
+    {
+        return self::$domainRule ? self::$domainRule['*'][$key] : self::$rules['*'][$key];
+    }
+
+    /**
+     * 检测路由规则
+     * @access private
+     * @param Request $request
+     * @param array   $rules   路由规则
+     * @param string  $url     URL地址
+     * @param string  $depr    URL分割符
+     * @param string  $group   路由分组名
+     * @param array   $options 路由参数(分组)
+     * @return mixed
+     */
+    private static function checkRoute($request, $rules, $url, $depr = '/', $group = '', $options = [])
+    {
+        foreach ($rules as $key => $item) {
+            if (true === $item) {
+                $item = self::getRouteExpress($key);
+            }
+            if (!isset($item['rule'])) {
+                continue;
+            }
+            $rule    = $item['rule'];
+            $route   = $item['route'];
+            $vars    = $item['var'];
+            $option  = $item['option'];
+            $pattern = $item['pattern'];
+
+            // 检查参数有效性
+            if (!self::checkOption($option, $request)) {
+                continue;
+            }
+
+            if (isset($option['ext'])) {
+                // 路由ext参数 优先于系统配置的URL伪静态后缀参数
+                $url = preg_replace('/\.' . $request->ext() . '$/i', '', $url);
+            }
+
+            if (is_array($rule)) {
+                // 分组路由
+                $pos = strpos(str_replace('<', ':', $key), ':');
+                if (false !== $pos) {
+                    $str = substr($key, 0, $pos);
+                } else {
+                    $str = $key;
+                }
+                if (is_string($str) && $str && 0 !== stripos(str_replace('|', '/', $url), $str)) {
+                    continue;
+                }
+                self::setOption($option);
+                $result = self::checkRoute($request, $rule, $url, $depr, $key, $option);
+                if (false !== $result) {
+                    return $result;
+                }
+            } elseif ($route) {
+                if ('__miss__' == $rule || '__auto__' == $rule) {
+                    // 指定特殊路由
+                    $var    = trim($rule, '__');
+                    ${$var} = $item;
+                    continue;
+                }
+                if ($group) {
+                    $rule = $group . ($rule ? '/' . ltrim($rule, '/') : '');
+                }
+
+                self::setOption($option);
+                if (isset($options['bind_model']) && isset($option['bind_model'])) {
+                    $option['bind_model'] = array_merge($options['bind_model'], $option['bind_model']);
+                }
+                $result = self::checkRule($rule, $route, $url, $pattern, $option, $depr);
+                if (false !== $result) {
+                    return $result;
+                }
+            }
+        }
+        if (isset($auto)) {
+            // 自动解析URL地址
+            return self::parseUrl($auto['route'] . '/' . $url, $depr);
+        } elseif (isset($miss)) {
+            // 未匹配所有路由的路由规则处理
+            return self::parseRule('', $miss['route'], $url, $miss['option']);
+        }
+        return false;
+    }
+
+    /**
+     * 检测路由别名
+     * @access private
+     * @param Request $request
+     * @param string  $url  URL地址
+     * @param string  $depr URL分隔符
+     * @return mixed
+     */
+    private static function checkRouteAlias($request, $url, $depr)
+    {
+        $array = explode('|', $url);
+        $alias = array_shift($array);
+        $item  = self::$rules['alias'][$alias];
+
+        if (is_array($item)) {
+            list($rule, $option) = $item;
+            $action              = $array[0];
+            if (isset($option['allow']) && !in_array($action, explode(',', $option['allow']))) {
+                // 允许操作
+                return false;
+            } elseif (isset($option['except']) && in_array($action, explode(',', $option['except']))) {
+                // 排除操作
+                return false;
+            }
+            if (isset($option['method'][$action])) {
+                $option['method'] = $option['method'][$action];
+            }
+        } else {
+            $rule = $item;
+        }
+        $bind = implode('|', $array);
+        // 参数有效性检查
+        if (isset($option) && !self::checkOption($option, $request)) {
+            // 路由不匹配
+            return false;
+        } elseif (0 === strpos($rule, '\\')) {
+            // 路由到类
+            return self::bindToClass($bind, substr($rule, 1), $depr);
+        } elseif (0 === strpos($rule, '@')) {
+            // 路由到控制器类
+            return self::bindToController($bind, substr($rule, 1), $depr);
+        } else {
+            // 路由到模块/控制器
+            return self::bindToModule($bind, $rule, $depr);
+        }
+    }
+
+    /**
+     * 检测URL绑定
+     * @access private
+     * @param string $url   URL地址
+     * @param array  $rules 路由规则
+     * @param string $depr  URL分隔符
+     * @return mixed
+     */
+    private static function checkUrlBind(&$url, &$rules, $depr = '/')
+    {
+        if (!empty(self::$bind)) {
+            $type = self::$bind['type'];
+            $bind = self::$bind[$type];
+            // 记录绑定信息
+            App::$debug && Log::record('[ BIND ] ' . var_export($bind, true), 'info');
+            // 如果有URL绑定 则进行绑定检测
+            switch ($type) {
+                case 'class':
+                    // 绑定到类
+                    return self::bindToClass($url, $bind, $depr);
+                case 'controller':
+                    // 绑定到控制器类
+                    return self::bindToController($url, $bind, $depr);
+                case 'namespace':
+                    // 绑定到命名空间
+                    return self::bindToNamespace($url, $bind, $depr);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 绑定到类
+     * @access public
+     * @param string $url   URL地址
+     * @param string $class 类名(带命名空间)
+     * @param string $depr  URL分隔符
+     * @return array
+     */
+    public static function bindToClass($url, $class, $depr = '/')
+    {
+        $url    = str_replace($depr, '|', $url);
+        $array  = explode('|', $url, 2);
+        $action = !empty($array[0]) ? $array[0] : Config::get('default_action');
+        if (!empty($array[1])) {
+            self::parseUrlParams($array[1]);
+        }
+        return ['type' => 'method', 'method' => [$class, $action], 'var' => []];
+    }
+
+    /**
+     * 绑定到命名空间
+     * @access public
+     * @param string $url       URL地址
+     * @param string $namespace 命名空间
+     * @param string $depr      URL分隔符
+     * @return array
+     */
+    public static function bindToNamespace($url, $namespace, $depr = '/')
+    {
+        $url    = str_replace($depr, '|', $url);
+        $array  = explode('|', $url, 3);
+        $class  = !empty($array[0]) ? $array[0] : Config::get('default_controller');
+        $method = !empty($array[1]) ? $array[1] : Config::get('default_action');
+        if (!empty($array[2])) {
+            self::parseUrlParams($array[2]);
+        }
+        return ['type' => 'method', 'method' => [$namespace . '\\' . Loader::parseName($class, 1), $method], 'var' => []];
+    }
+
+    /**
+     * 绑定到控制器类
+     * @access public
+     * @param string $url        URL地址
+     * @param string $controller 控制器名 (支持带模块名 index/user )
+     * @param string $depr       URL分隔符
+     * @return array
+     */
+    public static function bindToController($url, $controller, $depr = '/')
+    {
+        $url    = str_replace($depr, '|', $url);
+        $array  = explode('|', $url, 2);
+        $action = !empty($array[0]) ? $array[0] : Config::get('default_action');
+        if (!empty($array[1])) {
+            self::parseUrlParams($array[1]);
+        }
+        return ['type' => 'controller', 'controller' => $controller . '/' . $action, 'var' => []];
+    }
+
+    /**
+     * 绑定到模块/控制器
+     * @access public
+     * @param string $url        URL地址
+     * @param string $controller 控制器类名(带命名空间)
+     * @param string $depr       URL分隔符
+     * @return array
+     */
+    public static function bindToModule($url, $controller, $depr = '/')
+    {
+        $url    = str_replace($depr, '|', $url);
+        $array  = explode('|', $url, 2);
+        $action = !empty($array[0]) ? $array[0] : Config::get('default_action');
+        if (!empty($array[1])) {
+            self::parseUrlParams($array[1]);
+        }
+        return ['type' => 'module', 'module' => $controller . '/' . $action];
+    }
+
+    /**
+     * 路由参数有效性检查
+     * @access private
+     * @param array   $option  路由参数
+     * @param Request $request Request对象
+     * @return bool
+     */
+    private static function checkOption($option, $request)
+    {
+        if ((isset($option['method']) && is_string($option['method']) && false === stripos($option['method'], $request->method()))
+            || (isset($option['ajax']) && $option['ajax'] && !$request->isAjax()) // Ajax检测
+             || (isset($option['ajax']) && !$option['ajax'] && $request->isAjax()) // 非Ajax检测
+             || (isset($option['pjax']) && $option['pjax'] && !$request->isPjax()) // Pjax检测
+             || (isset($option['pjax']) && !$option['pjax'] && $request->isPjax()) // 非Pjax检测
+             || (isset($option['ext']) && false === stripos('|' . $option['ext'] . '|', '|' . $request->ext() . '|')) // 伪静态后缀检测
+             || (isset($option['deny_ext']) && false !== stripos('|' . $option['deny_ext'] . '|', '|' . $request->ext() . '|'))
+            || (isset($option['domain']) && !in_array($option['domain'], [$_SERVER['HTTP_HOST'], self::$subDomain])) // 域名检测
+             || (isset($option['https']) && $option['https'] && !$request->isSsl()) // https检测
+             || (isset($option['https']) && !$option['https'] && $request->isSsl()) // https检测
+             || (!empty($option['before_behavior']) && false === Hook::exec($option['before_behavior'])) // 行为检测
+             || (!empty($option['callback']) && is_callable($option['callback']) && false === call_user_func($option['callback'])) // 自定义检测
+        ) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 检测路由规则
+     * @access private
+     * @param string $rule    路由规则
+     * @param string $route   路由地址
+     * @param string $url     URL地址
+     * @param array  $pattern 变量规则
+     * @param array  $option  路由参数
+     * @param string $depr    URL分隔符(全局)
+     * @return array|false
+     */
+    private static function checkRule($rule, $route, $url, $pattern, $option, $depr)
+    {
+        // 检查完整规则定义
+        if (isset($pattern['__url__']) && !preg_match(0 === strpos($pattern['__url__'], '/') ? $pattern['__url__'] : '/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) {
+            return false;
+        }
+        // 检查路由的参数分隔符
+        if (isset($option['param_depr'])) {
+            $url = str_replace(['|', $option['param_depr']], [$depr, '|'], $url);
+        }
+
+        $len1 = substr_count($url, '|');
+        $len2 = substr_count($rule, '/');
+        // 多余参数是否合并
+        $merge = !empty($option['merge_extra_vars']);
+        if ($merge && $len1 > $len2) {
+            $url = str_replace('|', $depr, $url);
+            $url = implode('|', explode($depr, $url, $len2 + 1));
+        }
+
+        if ($len1 >= $len2 || strpos($rule, '[')) {
+            if (!empty($option['complete_match'])) {
+                // 完整匹配
+                if (!$merge && $len1 != $len2 && (false === strpos($rule, '[') || $len1 > $len2 || $len1 < $len2 - substr_count($rule, '['))) {
+                    return false;
+                }
+            }
+            $pattern = array_merge(self::$rules['pattern'], $pattern);
+            if (false !== $match = self::match($url, $rule, $pattern)) {
+                // 匹配到路由规则
+                return self::parseRule($rule, $route, $url, $option, $match);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 解析模块的URL地址 [模块/控制器/操作?]参数1=值1&参数2=值2...
+     * @access public
+     * @param string $url        URL地址
+     * @param string $depr       URL分隔符
+     * @param bool   $autoSearch 是否自动深度搜索控制器
+     * @return array
+     */
+    public static function parseUrl($url, $depr = '/', $autoSearch = false)
+    {
+
+        if (isset(self::$bind['module'])) {
+            $bind = str_replace('/', $depr, self::$bind['module']);
+            // 如果有模块/控制器绑定
+            $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
+        }
+        $url              = str_replace($depr, '|', $url);
+        list($path, $var) = self::parseUrlPath($url);
+        $route            = [null, null, null];
+        if (isset($path)) {
+            // 解析模块
+            $module = Config::get('app_multi_module') ? array_shift($path) : null;
+            if ($autoSearch) {
+                // 自动搜索控制器
+                $dir    = APP_PATH . ($module ? $module . DS : '') . Config::get('url_controller_layer');
+                $suffix = App::$suffix || Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : '';
+                $item   = [];
+                $find   = false;
+                foreach ($path as $val) {
+                    $item[] = $val;
+                    $file   = $dir . DS . str_replace('.', DS, $val) . $suffix . EXT;
+                    $file   = pathinfo($file, PATHINFO_DIRNAME) . DS . Loader::parseName(pathinfo($file, PATHINFO_FILENAME), 1) . EXT;
+                    if (is_file($file)) {
+                        $find = true;
+                        break;
+                    } else {
+                        $dir .= DS . Loader::parseName($val);
+                    }
+                }
+                if ($find) {
+                    $controller = implode('.', $item);
+                    $path       = array_slice($path, count($item));
+                } else {
+                    $controller = array_shift($path);
+                }
+            } else {
+                // 解析控制器
+                $controller = !empty($path) ? array_shift($path) : null;
+            }
+            // 解析操作
+            $action = !empty($path) ? array_shift($path) : null;
+            // 解析额外参数
+            self::parseUrlParams(empty($path) ? '' : implode('|', $path));
+            // 封装路由
+            $route = [$module, $controller, $action];
+            // 检查地址是否被定义过路由
+            $name  = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action);
+            $name2 = '';
+            if (empty($module) || isset($bind) && $module == $bind) {
+                $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action);
+            }
+
+            if (isset(self::$rules['name'][$name]) || isset(self::$rules['name'][$name2])) {
+                throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
+            }
+        }
+        return ['type' => 'module', 'module' => $route];
+    }
+
+    /**
+     * 解析URL的pathinfo参数和变量
+     * @access private
+     * @param string $url URL地址
+     * @return array
+     */
+    private static function parseUrlPath($url)
+    {
+        // 分隔符替换 确保路由定义使用统一的分隔符
+        $url = str_replace('|', '/', $url);
+        $url = trim($url, '/');
+        $var = [];
+        if (false !== strpos($url, '?')) {
+            // [模块/控制器/操作?]参数1=值1&参数2=值2...
+            $info = parse_url($url);
+            $path = explode('/', $info['path']);
+            parse_str($info['query'], $var);
+        } elseif (strpos($url, '/')) {
+            // [模块/控制器/操作]
+            $path = explode('/', $url);
+        } else {
+            $path = [$url];
+        }
+        return [$path, $var];
+    }
+
+    /**
+     * 检测URL和规则路由是否匹配
+     * @access private
+     * @param string $url     URL地址
+     * @param string $rule    路由规则
+     * @param array  $pattern 变量规则
+     * @return array|false
+     */
+    private static function match($url, $rule, $pattern)
+    {
+        $m2 = explode('/', $rule);
+        $m1 = explode('|', $url);
+
+        $var = [];
+        foreach ($m2 as $key => $val) {
+            // val中定义了多个变量 <id><name>
+            if (false !== strpos($val, '<') && preg_match_all('/<(\w+(\??))>/', $val, $matches)) {
+                $value   = [];
+                $replace = [];
+                foreach ($matches[1] as $name) {
+                    if (strpos($name, '?')) {
+                        $name      = substr($name, 0, -1);
+                        $replace[] = '(' . (isset($pattern[$name]) ? $pattern[$name] : '\w+') . ')?';
+                    } else {
+                        $replace[] = '(' . (isset($pattern[$name]) ? $pattern[$name] : '\w+') . ')';
+                    }
+                    $value[] = $name;
+                }
+                $val = str_replace($matches[0], $replace, $val);
+                if (preg_match('/^' . $val . '$/', isset($m1[$key]) ? $m1[$key] : '', $match)) {
+                    array_shift($match);
+                    foreach ($value as $k => $name) {
+                        if (isset($match[$k])) {
+                            $var[$name] = $match[$k];
+                        }
+                    }
+                    continue;
+                } else {
+                    return false;
+                }
+            }
+
+            if (0 === strpos($val, '[:')) {
+                // 可选参数
+                $val      = substr($val, 1, -1);
+                $optional = true;
+            } else {
+                $optional = false;
+            }
+            if (0 === strpos($val, ':')) {
+                // URL变量
+                $name = substr($val, 1);
+                if (!$optional && !isset($m1[$key])) {
+                    return false;
+                }
+                if (isset($m1[$key]) && isset($pattern[$name])) {
+                    // 检查变量规则
+                    if ($pattern[$name] instanceof \Closure) {
+                        $result = call_user_func_array($pattern[$name], [$m1[$key]]);
+                        if (false === $result) {
+                            return false;
+                        }
+                    } elseif (!preg_match(0 === strpos($pattern[$name], '/') ? $pattern[$name] : '/^' . $pattern[$name] . '$/', $m1[$key])) {
+                        return false;
+                    }
+                }
+                $var[$name] = isset($m1[$key]) ? $m1[$key] : '';
+            } elseif (!isset($m1[$key]) || 0 !== strcasecmp($val, $m1[$key])) {
+                return false;
+            }
+        }
+        // 成功匹配后返回URL中的动态变量数组
+        return $var;
+    }
+
+    /**
+     * 解析规则路由
+     * @access private
+     * @param string $rule      路由规则
+     * @param string $route     路由地址
+     * @param string $pathinfo  URL地址
+     * @param array  $option    路由参数
+     * @param array  $matches   匹配的变量
+     * @param bool   $fromCache 通过缓存解析
+     * @return array
+     */
+    private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = [], $fromCache = false)
+    {
+        $request = Request::instance();
+
+        //保存解析缓存
+        if (Config::get('route_check_cache') && !$fromCache) {
+            try {
+                $key = self::getCheckCacheKey($request);
+                Cache::tag('route_check')->set($key, [$rule, $route, $pathinfo, $option, $matches]);
+            } catch (\Exception $e) {
+
+            }
+        }
+
+        // 解析路由规则
+        if ($rule) {
+            $rule = explode('/', $rule);
+            // 获取URL地址中的参数
+            $paths = explode('|', $pathinfo);
+            foreach ($rule as $item) {
+                $fun = '';
+                if (0 === strpos($item, '[:')) {
+                    $item = substr($item, 1, -1);
+                }
+                if (0 === strpos($item, ':')) {
+                    $var           = substr($item, 1);
+                    $matches[$var] = array_shift($paths);
+                } else {
+                    // 过滤URL中的静态变量
+                    array_shift($paths);
+                }
+            }
+        } else {
+            $paths = explode('|', $pathinfo);
+        }
+
+        // 获取路由地址规则
+        if (is_string($route) && isset($option['prefix'])) {
+            // 路由地址前缀
+            $route = $option['prefix'] . $route;
+        }
+        // 替换路由地址中的变量
+        if (is_string($route) && !empty($matches)) {
+            foreach ($matches as $key => $val) {
+                if (false !== strpos($route, ':' . $key)) {
+                    $route = str_replace(':' . $key, $val, $route);
+                }
+            }
+        }
+
+        // 绑定模型数据
+        if (isset($option['bind_model'])) {
+            $bind = [];
+            foreach ($option['bind_model'] as $key => $val) {
+                if ($val instanceof \Closure) {
+                    $result = call_user_func_array($val, [$matches]);
+                } else {
+                    if (is_array($val)) {
+                        $fields    = explode('&', $val[1]);
+                        $model     = $val[0];
+                        $exception = isset($val[2]) ? $val[2] : true;
+                    } else {
+                        $fields    = ['id'];
+                        $model     = $val;
+                        $exception = true;
+                    }
+                    $where = [];
+                    $match = true;
+                    foreach ($fields as $field) {
+                        if (!isset($matches[$field])) {
+                            $match = false;
+                            break;
+                        } else {
+                            $where[$field] = $matches[$field];
+                        }
+                    }
+                    if ($match) {
+                        $query  = strpos($model, '\\') ? $model::where($where) : Loader::model($model)->where($where);
+                        $result = $query->failException($exception)->find();
+                    }
+                }
+                if (!empty($result)) {
+                    $bind[$key] = $result;
+                }
+            }
+            $request->bind($bind);
+        }
+
+        if (!empty($option['response'])) {
+            Hook::add('response_send', $option['response']);
+        }
+
+        // 解析额外参数
+        self::parseUrlParams(empty($paths) ? '' : implode('|', $paths), $matches);
+        // 记录匹配的路由信息
+        $request->routeInfo(['rule' => $rule, 'route' => $route, 'option' => $option, 'var' => $matches]);
+
+        // 检测路由after行为
+        if (!empty($option['after_behavior'])) {
+            if ($option['after_behavior'] instanceof \Closure) {
+                $result = call_user_func_array($option['after_behavior'], []);
+            } else {
+                foreach ((array) $option['after_behavior'] as $behavior) {
+                    $result = Hook::exec($behavior, '');
+                    if (!is_null($result)) {
+                        break;
+                    }
+                }
+            }
+            // 路由规则重定向
+            if ($result instanceof Response) {
+                return ['type' => 'response', 'response' => $result];
+            } elseif (is_array($result)) {
+                return $result;
+            }
+        }
+
+        if ($route instanceof \Closure) {
+            // 执行闭包
+            $result = ['type' => 'function', 'function' => $route];
+        } elseif (0 === strpos($route, '/') || strpos($route, '://')) {
+            // 路由到重定向地址
+            $result = ['type' => 'redirect', 'url' => $route, 'status' => isset($option['status']) ? $option['status'] : 301];
+        } elseif (false !== strpos($route, '\\')) {
+            // 路由到方法
+            list($path, $var) = self::parseUrlPath($route);
+            $route            = str_replace('/', '@', implode('/', $path));
+            $method           = strpos($route, '@') ? explode('@', $route) : $route;
+            $result           = ['type' => 'method', 'method' => $method, 'var' => $var];
+        } elseif (0 === strpos($route, '@')) {
+            // 路由到控制器
+            $route             = substr($route, 1);
+            list($route, $var) = self::parseUrlPath($route);
+            $result            = ['type' => 'controller', 'controller' => implode('/', $route), 'var' => $var];
+            $request->action(array_pop($route));
+            $request->controller($route ? array_pop($route) : Config::get('default_controller'));
+            $request->module($route ? array_pop($route) : Config::get('default_module'));
+            App::$modulePath = APP_PATH . (Config::get('app_multi_module') ? $request->module() . DS : '');
+        } else {
+            // 路由到模块/控制器/操作
+            $result = self::parseModule($route, isset($option['convert']) ? $option['convert'] : false);
+        }
+        // 开启请求缓存
+        if ($request->isGet() && isset($option['cache'])) {
+            $cache = $option['cache'];
+            if (is_array($cache)) {
+                list($key, $expire, $tag) = array_pad($cache, 3, null);
+            } else {
+                $key    = str_replace('|', '/', $pathinfo);
+                $expire = $cache;
+                $tag    = null;
+            }
+            $request->cache($key, $expire, $tag);
+        }
+        return $result;
+    }
+
+    /**
+     * 解析URL地址为 模块/控制器/操作
+     * @access private
+     * @param string $url     URL地址
+     * @param bool   $convert 是否自动转换URL地址
+     * @return array
+     */
+    private static function parseModule($url, $convert = false)
+    {
+        list($path, $var) = self::parseUrlPath($url);
+        $action           = array_pop($path);
+        $controller       = !empty($path) ? array_pop($path) : null;
+        $module           = Config::get('app_multi_module') && !empty($path) ? array_pop($path) : null;
+        $method           = Request::instance()->method();
+        if (Config::get('use_action_prefix') && !empty(self::$methodPrefix[$method])) {
+            // 操作方法前缀支持
+            $action = 0 !== strpos($action, self::$methodPrefix[$method]) ? self::$methodPrefix[$method] . $action : $action;
+        }
+        // 设置当前请求的路由变量
+        Request::instance()->route($var);
+        // 路由到模块/控制器/操作
+        return ['type' => 'module', 'module' => [$module, $controller, $action], 'convert' => $convert];
+    }
+
+    /**
+     * 解析URL地址中的参数Request对象
+     * @access private
+     * @param string $url 路由规则
+     * @param array  $var 变量
+     * @return void
+     */
+    private static function parseUrlParams($url, &$var = [])
+    {
+        if ($url) {
+            if (Config::get('url_param_type')) {
+                $var += explode('|', $url);
+            } else {
+                preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) {
+                    $var[$match[1]] = strip_tags($match[2]);
+                }, $url);
+            }
+        }
+        // 设置当前请求的参数
+        Request::instance()->route($var);
+    }
+
+    // 分析路由规则中的变量
+    private static function parseVar($rule)
+    {
+        // 提取路由规则中的变量
+        $var = [];
+        foreach (explode('/', $rule) as $val) {
+            $optional = false;
+            if (false !== strpos($val, '<') && preg_match_all('/<(\w+(\??))>/', $val, $matches)) {
+                foreach ($matches[1] as $name) {
+                    if (strpos($name, '?')) {
+                        $name     = substr($name, 0, -1);
+                        $optional = true;
+                    } else {
+                        $optional = false;
+                    }
+                    $var[$name] = $optional ? 2 : 1;
+                }
+            }
+
+            if (0 === strpos($val, '[:')) {
+                // 可选参数
+                $optional = true;
+                $val      = substr($val, 1, -1);
+            }
+            if (0 === strpos($val, ':')) {
+                // URL变量
+                $name       = substr($val, 1);
+                $var[$name] = $optional ? 2 : 1;
+            }
+        }
+        return $var;
+    }
+
+    /**
+     * 获取路由解析缓存的key
+     * @param Request $request
+     * @return string
+     */
+    private static function getCheckCacheKey(Request $request)
+    {
+        static $key;
+
+        if (empty($key)) {
+            if ($callback = Config::get('route_check_cache_key')) {
+                $key = call_user_func($callback, $request);
+            } else {
+                $key = "{$request->host(true)}|{$request->method()}|{$request->path()}";
+            }
+        }
+
+        return $key;
+    }
+}

+ 366 - 0
thinkphp/library/think/Session.php

@@ -0,0 +1,366 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\exception\ClassNotFoundException;
+
+class Session
+{
+    protected static $prefix = '';
+    protected static $init   = null;
+
+    /**
+     * 设置或者获取session作用域(前缀)
+     * @param string $prefix
+     * @return string|void
+     */
+    public static function prefix($prefix = '')
+    {
+        empty(self::$init) && self::boot();
+        if (empty($prefix) && null !== $prefix) {
+            return self::$prefix;
+        } else {
+            self::$prefix = $prefix;
+        }
+    }
+
+    /**
+     * session初始化
+     * @param array $config
+     * @return void
+     * @throws \think\Exception
+     */
+    public static function init(array $config = [])
+    {
+        if (empty($config)) {
+            $config = Config::get('session');
+        }
+        // 记录初始化信息
+        App::$debug && Log::record('[ SESSION ] INIT ' . var_export($config, true), 'info');
+        $isDoStart = false;
+        if (isset($config['use_trans_sid'])) {
+            ini_set('session.use_trans_sid', $config['use_trans_sid'] ? 1 : 0);
+        }
+
+        // 启动session
+        if (!empty($config['auto_start']) && PHP_SESSION_ACTIVE != session_status()) {
+            ini_set('session.auto_start', 0);
+            $isDoStart = true;
+        }
+
+        if (isset($config['prefix']) && ('' === self::$prefix || null === self::$prefix)) {
+            self::$prefix = $config['prefix'];
+        }
+        if (isset($config['var_session_id']) && isset($_REQUEST[$config['var_session_id']])) {
+            session_id($_REQUEST[$config['var_session_id']]);
+        } elseif (isset($config['id']) && !empty($config['id'])) {
+            session_id($config['id']);
+        }
+        if (isset($config['name'])) {
+            session_name($config['name']);
+        }
+        if (isset($config['path'])) {
+            session_save_path($config['path']);
+        }
+        if (isset($config['domain'])) {
+            ini_set('session.cookie_domain', $config['domain']);
+        }
+        if (isset($config['expire'])) {
+            ini_set('session.gc_maxlifetime', $config['expire']);
+            ini_set('session.cookie_lifetime', $config['expire']);
+        }
+        if (isset($config['secure'])) {
+            ini_set('session.cookie_secure', $config['secure']);
+        }
+        if (isset($config['httponly'])) {
+            ini_set('session.cookie_httponly', $config['httponly']);
+        }
+        if (isset($config['use_cookies'])) {
+            ini_set('session.use_cookies', $config['use_cookies'] ? 1 : 0);
+        }
+        if (isset($config['cache_limiter'])) {
+            session_cache_limiter($config['cache_limiter']);
+        }
+        if (isset($config['cache_expire'])) {
+            session_cache_expire($config['cache_expire']);
+        }
+        if (!empty($config['type'])) {
+            // 读取session驱动
+            $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\session\\driver\\' . ucwords($config['type']);
+
+            // 检查驱动类
+            if (!class_exists($class) || !session_set_save_handler(new $class($config))) {
+                throw new ClassNotFoundException('error session handler:' . $class, $class);
+            }
+        }
+        if ($isDoStart) {
+            session_start();
+            self::$init = true;
+        } else {
+            self::$init = false;
+        }
+    }
+
+    /**
+     * session自动启动或者初始化
+     * @return void
+     */
+    public static function boot()
+    {
+        if (is_null(self::$init)) {
+            self::init();
+        } elseif (false === self::$init) {
+            if (PHP_SESSION_ACTIVE != session_status()) {
+                session_start();
+            }
+            self::$init = true;
+        }
+    }
+
+    /**
+     * session设置
+     * @param string        $name session名称
+     * @param mixed         $value session值
+     * @param string|null   $prefix 作用域(前缀)
+     * @return void
+     */
+    public static function set($name, $value = '', $prefix = null)
+    {
+        empty(self::$init) && self::boot();
+
+        $prefix = !is_null($prefix) ? $prefix : self::$prefix;
+        if (strpos($name, '.')) {
+            // 二维数组赋值
+            list($name1, $name2) = explode('.', $name);
+            if ($prefix) {
+                $_SESSION[$prefix][$name1][$name2] = $value;
+            } else {
+                $_SESSION[$name1][$name2] = $value;
+            }
+        } elseif ($prefix) {
+            $_SESSION[$prefix][$name] = $value;
+        } else {
+            $_SESSION[$name] = $value;
+        }
+    }
+
+    /**
+     * session获取
+     * @param string        $name session名称
+     * @param string|null   $prefix 作用域(前缀)
+     * @return mixed
+     */
+    public static function get($name = '', $prefix = null)
+    {
+        empty(self::$init) && self::boot();
+        $prefix = !is_null($prefix) ? $prefix : self::$prefix;
+        if ('' == $name) {
+            // 获取全部的session
+            $value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION;
+        } elseif ($prefix) {
+            // 获取session
+            if (strpos($name, '.')) {
+                list($name1, $name2) = explode('.', $name);
+                $value               = isset($_SESSION[$prefix][$name1][$name2]) ? $_SESSION[$prefix][$name1][$name2] : null;
+            } else {
+                $value = isset($_SESSION[$prefix][$name]) ? $_SESSION[$prefix][$name] : null;
+            }
+        } else {
+            if (strpos($name, '.')) {
+                list($name1, $name2) = explode('.', $name);
+                $value               = isset($_SESSION[$name1][$name2]) ? $_SESSION[$name1][$name2] : null;
+            } else {
+                $value = isset($_SESSION[$name]) ? $_SESSION[$name] : null;
+            }
+        }
+        return $value;
+    }
+
+    /**
+     * session获取并删除
+     * @param string        $name session名称
+     * @param string|null   $prefix 作用域(前缀)
+     * @return mixed
+     */
+    public static function pull($name, $prefix = null)
+    {
+        $result = self::get($name, $prefix);
+        if ($result) {
+            self::delete($name, $prefix);
+            return $result;
+        } else {
+            return;
+        }
+    }
+
+    /**
+     * session设置 下一次请求有效
+     * @param string        $name session名称
+     * @param mixed         $value session值
+     * @param string|null   $prefix 作用域(前缀)
+     * @return void
+     */
+    public static function flash($name, $value)
+    {
+        self::set($name, $value);
+        if (!self::has('__flash__.__time__')) {
+            self::set('__flash__.__time__', $_SERVER['REQUEST_TIME_FLOAT']);
+        }
+        self::push('__flash__', $name);
+    }
+
+    /**
+     * 清空当前请求的session数据
+     * @return void
+     */
+    public static function flush()
+    {
+        if (self::$init) {
+            $item = self::get('__flash__');
+
+            if (!empty($item)) {
+                $time = $item['__time__'];
+                if ($_SERVER['REQUEST_TIME_FLOAT'] > $time) {
+                    unset($item['__time__']);
+                    self::delete($item);
+                    self::set('__flash__', []);
+                }
+            }
+        }
+    }
+
+    /**
+     * 删除session数据
+     * @param string|array  $name session名称
+     * @param string|null   $prefix 作用域(前缀)
+     * @return void
+     */
+    public static function delete($name, $prefix = null)
+    {
+        empty(self::$init) && self::boot();
+        $prefix = !is_null($prefix) ? $prefix : self::$prefix;
+        if (is_array($name)) {
+            foreach ($name as $key) {
+                self::delete($key, $prefix);
+            }
+        } elseif (strpos($name, '.')) {
+            list($name1, $name2) = explode('.', $name);
+            if ($prefix) {
+                unset($_SESSION[$prefix][$name1][$name2]);
+            } else {
+                unset($_SESSION[$name1][$name2]);
+            }
+        } else {
+            if ($prefix) {
+                unset($_SESSION[$prefix][$name]);
+            } else {
+                unset($_SESSION[$name]);
+            }
+        }
+    }
+
+    /**
+     * 清空session数据
+     * @param string|null   $prefix 作用域(前缀)
+     * @return void
+     */
+    public static function clear($prefix = null)
+    {
+        empty(self::$init) && self::boot();
+        $prefix = !is_null($prefix) ? $prefix : self::$prefix;
+        if ($prefix) {
+            unset($_SESSION[$prefix]);
+        } else {
+            $_SESSION = [];
+        }
+    }
+
+    /**
+     * 判断session数据
+     * @param string        $name session名称
+     * @param string|null   $prefix
+     * @return bool
+     */
+    public static function has($name, $prefix = null)
+    {
+        empty(self::$init) && self::boot();
+        $prefix = !is_null($prefix) ? $prefix : self::$prefix;
+        if (strpos($name, '.')) {
+            // 支持数组
+            list($name1, $name2) = explode('.', $name);
+            return $prefix ? isset($_SESSION[$prefix][$name1][$name2]) : isset($_SESSION[$name1][$name2]);
+        } else {
+            return $prefix ? isset($_SESSION[$prefix][$name]) : isset($_SESSION[$name]);
+        }
+    }
+
+    /**
+     * 添加数据到一个session数组
+     * @param  string  $key
+     * @param  mixed   $value
+     * @return void
+     */
+    public static function push($key, $value)
+    {
+        $array = self::get($key);
+        if (is_null($array)) {
+            $array = [];
+        }
+        $array[] = $value;
+        self::set($key, $array);
+    }
+
+    /**
+     * 启动session
+     * @return void
+     */
+    public static function start()
+    {
+        session_start();
+        self::$init = true;
+    }
+
+    /**
+     * 销毁session
+     * @return void
+     */
+    public static function destroy()
+    {
+        if (!empty($_SESSION)) {
+            $_SESSION = [];
+        }
+        session_unset();
+        session_destroy();
+        self::$init = null;
+    }
+
+    /**
+     * 重新生成session_id
+     * @param bool $delete 是否删除关联会话文件
+     * @return void
+     */
+    public static function regenerate($delete = false)
+    {
+        session_regenerate_id($delete);
+    }
+
+    /**
+     * 暂停session
+     * @return void
+     */
+    public static function pause()
+    {
+        // 暂停session
+        session_write_close();
+        self::$init = false;
+    }
+}

+ 1139 - 0
thinkphp/library/think/Template.php

@@ -0,0 +1,1139 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\exception\TemplateNotFoundException;
+use think\template\TagLib;
+
+/**
+ * ThinkPHP分离出来的模板引擎
+ * 支持XML标签和普通标签的模板解析
+ * 编译型模板引擎 支持动态缓存
+ */
+class Template
+{
+    // 模板变量
+    protected $data = [];
+    // 引擎配置
+    protected $config = [
+        'view_path'          => '', // 模板路径
+        'view_base'          => '',
+        'view_suffix'        => 'html', // 默认模板文件后缀
+        'view_depr'          => DS,
+        'cache_suffix'       => 'php', // 默认模板缓存后缀
+        'tpl_deny_func_list' => 'echo,exit', // 模板引擎禁用函数
+        'tpl_deny_php'       => false, // 默认模板引擎是否禁用PHP原生代码
+        'tpl_begin'          => '{', // 模板引擎普通标签开始标记
+        'tpl_end'            => '}', // 模板引擎普通标签结束标记
+        'strip_space'        => false, // 是否去除模板文件里面的html空格与换行
+        'tpl_cache'          => true, // 是否开启模板编译缓存,设为false则每次都会重新编译
+        'compile_type'       => 'file', // 模板编译类型
+        'cache_prefix'       => '', // 模板缓存前缀标识,可以动态改变
+        'cache_time'         => 0, // 模板缓存有效期 0 为永久,(以数字为值,单位:秒)
+        'layout_on'          => false, // 布局模板开关
+        'layout_name'        => 'layout', // 布局模板入口文件
+        'layout_item'        => '{__CONTENT__}', // 布局模板的内容替换标识
+        'taglib_begin'       => '{', // 标签库标签开始标记
+        'taglib_end'         => '}', // 标签库标签结束标记
+        'taglib_load'        => true, // 是否使用内置标签库之外的其它标签库,默认自动检测
+        'taglib_build_in'    => 'cx', // 内置标签库名称(标签使用不必指定标签库名称),以逗号分隔 注意解析顺序
+        'taglib_pre_load'    => '', // 需要额外加载的标签库(须指定标签库名称),多个以逗号分隔
+        'display_cache'      => false, // 模板渲染缓存
+        'cache_id'           => '', // 模板缓存ID
+        'tpl_replace_string' => [],
+        'tpl_var_identify'   => 'array', // .语法变量识别,array|object|'', 为空时自动识别
+    ];
+
+    private $literal     = [];
+    private $includeFile = []; // 记录所有模板包含的文件路径及更新时间
+    protected $storage;
+
+    /**
+     * 构造函数
+     * @access public
+     * @param array $config
+     */
+    public function __construct(array $config = [])
+    {
+        $this->config['cache_path'] = TEMP_PATH;
+        $this->config               = array_merge($this->config, $config);
+
+        $this->config['taglib_begin_origin'] = $this->config['taglib_begin'];
+        $this->config['taglib_end_origin']   = $this->config['taglib_end'];
+
+        $this->config['taglib_begin'] = preg_quote($this->config['taglib_begin'], '/');
+        $this->config['taglib_end']   = preg_quote($this->config['taglib_end'], '/');
+        $this->config['tpl_begin']    = preg_quote($this->config['tpl_begin'], '/');
+        $this->config['tpl_end']      = preg_quote($this->config['tpl_end'], '/');
+
+        // 初始化模板编译存储器
+        $type          = $this->config['compile_type'] ? $this->config['compile_type'] : 'File';
+        $class         = false !== strpos($type, '\\') ? $type : '\\think\\template\\driver\\' . ucwords($type);
+        $this->storage = new $class();
+    }
+
+    /**
+     * 模板变量赋值
+     * @access public
+     * @param mixed $name
+     * @param mixed $value
+     * @return void
+     */
+    public function assign($name, $value = '')
+    {
+        if (is_array($name)) {
+            $this->data = array_merge($this->data, $name);
+        } else {
+            $this->data[$name] = $value;
+        }
+    }
+
+    /**
+     * 模板引擎参数赋值
+     * @access public
+     * @param mixed $name
+     * @param mixed $value
+     */
+    public function __set($name, $value)
+    {
+        $this->config[$name] = $value;
+    }
+
+    /**
+     * 模板引擎配置项
+     * @access public
+     * @param array|string $config
+     * @return string|void|array
+     */
+    public function config($config)
+    {
+        if (is_array($config)) {
+            $this->config = array_merge($this->config, $config);
+        } elseif (isset($this->config[$config])) {
+            return $this->config[$config];
+        } else {
+            return;
+        }
+    }
+
+    /**
+     * 模板变量获取
+     * @access public
+     * @param  string $name 变量名
+     * @return mixed
+     */
+    public function get($name = '')
+    {
+        if ('' == $name) {
+            return $this->data;
+        } else {
+            $data = $this->data;
+            foreach (explode('.', $name) as $key => $val) {
+                if (isset($data[$val])) {
+                    $data = $data[$val];
+                } else {
+                    $data = null;
+                    break;
+                }
+            }
+            return $data;
+        }
+    }
+
+    /**
+     * 渲染模板文件
+     * @access public
+     * @param string    $template 模板文件
+     * @param array     $vars 模板变量
+     * @param array     $config 模板参数
+     * @return void
+     */
+    public function fetch($template, $vars = [], $config = [])
+    {
+        if ($vars) {
+            $this->data = $vars;
+        }
+        if ($config) {
+            $this->config($config);
+        }
+        if (!empty($this->config['cache_id']) && $this->config['display_cache']) {
+            // 读取渲染缓存
+            $cacheContent = Cache::get($this->config['cache_id']);
+            if (false !== $cacheContent) {
+                echo $cacheContent;
+                return;
+            }
+        }
+        $template = $this->parseTemplateFile($template);
+        if ($template) {
+            $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.');
+            if (!$this->checkCache($cacheFile)) {
+                // 缓存无效 重新模板编译
+                $content = file_get_contents($template);
+                $this->compiler($content, $cacheFile);
+            }
+            // 页面缓存
+            ob_start();
+            ob_implicit_flush(0);
+            // 读取编译存储
+            $this->storage->read($cacheFile, $this->data);
+            // 获取并清空缓存
+            $content = ob_get_clean();
+            if (!empty($this->config['cache_id']) && $this->config['display_cache']) {
+                // 缓存页面输出
+                Cache::set($this->config['cache_id'], $content, $this->config['cache_time']);
+            }
+            echo $content;
+        }
+    }
+
+    /**
+     * 渲染模板内容
+     * @access public
+     * @param string    $content 模板内容
+     * @param array     $vars 模板变量
+     * @param array     $config 模板参数
+     * @return void
+     */
+    public function display($content, $vars = [], $config = [])
+    {
+        if ($vars) {
+            $this->data = $vars;
+        }
+        if ($config) {
+            $this->config($config);
+        }
+        $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.');
+        if (!$this->checkCache($cacheFile)) {
+            // 缓存无效 模板编译
+            $this->compiler($content, $cacheFile);
+        }
+        // 读取编译存储
+        $this->storage->read($cacheFile, $this->data);
+    }
+
+    /**
+     * 设置布局
+     * @access public
+     * @param mixed     $name 布局模板名称 false 则关闭布局
+     * @param string    $replace 布局模板内容替换标识
+     * @return Template
+     */
+    public function layout($name, $replace = '')
+    {
+        if (false === $name) {
+            // 关闭布局
+            $this->config['layout_on'] = false;
+        } else {
+            // 开启布局
+            $this->config['layout_on'] = true;
+            // 名称必须为字符串
+            if (is_string($name)) {
+                $this->config['layout_name'] = $name;
+            }
+            if (!empty($replace)) {
+                $this->config['layout_item'] = $replace;
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * 检查编译缓存是否有效
+     * 如果无效则需要重新编译
+     * @access private
+     * @param string $cacheFile 缓存文件名
+     * @return boolean
+     */
+    private function checkCache($cacheFile)
+    {
+        // 未开启缓存功能
+        if (!$this->config['tpl_cache']) {
+            return false;
+        }
+        // 缓存文件不存在
+        if (!is_file($cacheFile)) {
+            return false;
+        }
+        // 读取缓存文件失败
+        if (!$handle = @fopen($cacheFile, "r")) {
+            return false;
+        }
+        // 读取第一行
+        preg_match('/\/\*(.+?)\*\//', fgets($handle), $matches);
+        if (!isset($matches[1])) {
+            return false;
+        }
+        $includeFile = unserialize($matches[1]);
+        if (!is_array($includeFile)) {
+            return false;
+        }
+        // 检查模板文件是否有更新
+        foreach ($includeFile as $path => $time) {
+            if (is_file($path) && filemtime($path) > $time) {
+                // 模板文件如果有更新则缓存需要更新
+                return false;
+            }
+        }
+        // 检查编译存储是否有效
+        return $this->storage->check($cacheFile, $this->config['cache_time']);
+    }
+
+    /**
+     * 检查编译缓存是否存在
+     * @access public
+     * @param string $cacheId 缓存的id
+     * @return boolean
+     */
+    public function isCache($cacheId)
+    {
+        if ($cacheId && $this->config['display_cache']) {
+            // 缓存页面输出
+            return Cache::has($cacheId);
+        }
+        return false;
+    }
+
+    /**
+     * 编译模板文件内容
+     * @access private
+     * @param string    $content 模板内容
+     * @param string    $cacheFile 缓存文件名
+     * @return void
+     */
+    private function compiler(&$content, $cacheFile)
+    {
+        // 判断是否启用布局
+        if ($this->config['layout_on']) {
+            if (false !== strpos($content, '{__NOLAYOUT__}')) {
+                // 可以单独定义不使用布局
+                $content = str_replace('{__NOLAYOUT__}', '', $content);
+            } else {
+                // 读取布局模板
+                $layoutFile = $this->parseTemplateFile($this->config['layout_name']);
+                if ($layoutFile) {
+                    // 替换布局的主体内容
+                    $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile));
+                }
+            }
+        } else {
+            $content = str_replace('{__NOLAYOUT__}', '', $content);
+        }
+
+        // 模板解析
+        $this->parse($content);
+        if ($this->config['strip_space']) {
+            /* 去除html空格与换行 */
+            $find    = ['~>\s+<~', '~>(\s+\n|\r)~'];
+            $replace = ['><', '>'];
+            $content = preg_replace($find, $replace, $content);
+        }
+        // 优化生成的php代码
+        $content = preg_replace('/\?>\s*<\?php\s(?!echo\b)/s', '', $content);
+        // 模板过滤输出
+        $replace = $this->config['tpl_replace_string'];
+        $content = str_replace(array_keys($replace), array_values($replace), $content);
+        // 添加安全代码及模板引用记录
+        $content = '<?php if (!defined(\'THINK_PATH\')) exit(); /*' . serialize($this->includeFile) . '*/ ?>' . "\n" . $content;
+        // 编译存储
+        $this->storage->write($cacheFile, $content);
+        $this->includeFile = [];
+        return;
+    }
+
+    /**
+     * 模板解析入口
+     * 支持普通标签和TagLib解析 支持自定义标签库
+     * @access public
+     * @param string $content 要解析的模板内容
+     * @return void
+     */
+    public function parse(&$content)
+    {
+        // 内容为空不解析
+        if (empty($content)) {
+            return;
+        }
+        // 替换literal标签内容
+        $this->parseLiteral($content);
+        // 解析继承
+        $this->parseExtend($content);
+        // 解析布局
+        $this->parseLayout($content);
+        // 检查include语法
+        $this->parseInclude($content);
+        // 替换包含文件中literal标签内容
+        $this->parseLiteral($content);
+        // 检查PHP语法
+        $this->parsePhp($content);
+
+        // 获取需要引入的标签库列表
+        // 标签库只需要定义一次,允许引入多个一次
+        // 一般放在文件的最前面
+        // 格式:<taglib name="html,mytag..." />
+        // 当TAGLIB_LOAD配置为true时才会进行检测
+        if ($this->config['taglib_load']) {
+            $tagLibs = $this->getIncludeTagLib($content);
+            if (!empty($tagLibs)) {
+                // 对导入的TagLib进行解析
+                foreach ($tagLibs as $tagLibName) {
+                    $this->parseTagLib($tagLibName, $content);
+                }
+            }
+        }
+        // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀
+        if ($this->config['taglib_pre_load']) {
+            $tagLibs = explode(',', $this->config['taglib_pre_load']);
+            foreach ($tagLibs as $tag) {
+                $this->parseTagLib($tag, $content);
+            }
+        }
+        // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀
+        $tagLibs = explode(',', $this->config['taglib_build_in']);
+        foreach ($tagLibs as $tag) {
+            $this->parseTagLib($tag, $content, true);
+        }
+        // 解析普通模板标签 {$tagName}
+        $this->parseTag($content);
+
+        // 还原被替换的Literal标签
+        $this->parseLiteral($content, true);
+        return;
+    }
+
+    /**
+     * 检查PHP语法
+     * @access private
+     * @param string $content 要解析的模板内容
+     * @return void
+     * @throws \think\Exception
+     */
+    private function parsePhp(&$content)
+    {
+        // 短标签的情况要将<?标签用echo方式输出 否则无法正常输出xml标识
+        $content = preg_replace('/(<\?(?!php|=|$))/i', '<?php echo \'\\1\'; ?>' . "\n", $content);
+        // PHP语法检查
+        if ($this->config['tpl_deny_php'] && false !== strpos($content, '<?php')) {
+            throw new Exception('not allow php tag', 11600);
+        }
+        return;
+    }
+
+    /**
+     * 解析模板中的布局标签
+     * @access private
+     * @param string $content 要解析的模板内容
+     * @return void
+     */
+    private function parseLayout(&$content)
+    {
+        // 读取模板中的布局标签
+        if (preg_match($this->getRegex('layout'), $content, $matches)) {
+            // 替换Layout标签
+            $content = str_replace($matches[0], '', $content);
+            // 解析Layout标签
+            $array = $this->parseAttr($matches[0]);
+            if (!$this->config['layout_on'] || $this->config['layout_name'] != $array['name']) {
+                // 读取布局模板
+                $layoutFile = $this->parseTemplateFile($array['name']);
+                if ($layoutFile) {
+                    $replace = isset($array['replace']) ? $array['replace'] : $this->config['layout_item'];
+                    // 替换布局的主体内容
+                    $content = str_replace($replace, $content, file_get_contents($layoutFile));
+                }
+            }
+        } else {
+            $content = str_replace('{__NOLAYOUT__}', '', $content);
+        }
+        return;
+    }
+
+    /**
+     * 解析模板中的include标签
+     * @access private
+     * @param  string $content 要解析的模板内容
+     * @return void
+     */
+    private function parseInclude(&$content)
+    {
+        $regex = $this->getRegex('include');
+        $func  = function ($template) use (&$func, &$regex, &$content) {
+            if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) {
+                foreach ($matches as $match) {
+                    $array = $this->parseAttr($match[0]);
+                    $file  = $array['file'];
+                    unset($array['file']);
+                    // 分析模板文件名并读取内容
+                    $parseStr = $this->parseTemplateName($file);
+                    foreach ($array as $k => $v) {
+                        // 以$开头字符串转换成模板变量
+                        if (0 === strpos($v, '$')) {
+                            $v = $this->get(substr($v, 1));
+                        }
+                        $parseStr = str_replace('[' . $k . ']', $v, $parseStr);
+                    }
+                    $content = str_replace($match[0], $parseStr, $content);
+                    // 再次对包含文件进行模板分析
+                    $func($parseStr);
+                }
+                unset($matches);
+            }
+        };
+        // 替换模板中的include标签
+        $func($content);
+        return;
+    }
+
+    /**
+     * 解析模板中的extend标签
+     * @access private
+     * @param  string $content 要解析的模板内容
+     * @return void
+     */
+    private function parseExtend(&$content)
+    {
+        $regex  = $this->getRegex('extend');
+        $array  = $blocks  = $baseBlocks  = [];
+        $extend = '';
+        $func   = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) {
+            if (preg_match($regex, $template, $matches)) {
+                if (!isset($array[$matches['name']])) {
+                    $array[$matches['name']] = 1;
+                    // 读取继承模板
+                    $extend = $this->parseTemplateName($matches['name']);
+                    // 递归检查继承
+                    $func($extend);
+                    // 取得block标签内容
+                    $blocks = array_merge($blocks, $this->parseBlock($template));
+                    return;
+                }
+            } else {
+                // 取得顶层模板block标签内容
+                $baseBlocks = $this->parseBlock($template, true);
+                if (empty($extend)) {
+                    // 无extend标签但有block标签的情况
+                    $extend = $template;
+                }
+            }
+        };
+
+        $func($content);
+        if (!empty($extend)) {
+            if ($baseBlocks) {
+                $children = [];
+                foreach ($baseBlocks as $name => $val) {
+                    $replace = $val['content'];
+                    if (!empty($children[$name])) {
+                        // 如果包含有子block标签
+                        foreach ($children[$name] as $key) {
+                            $replace = str_replace($baseBlocks[$key]['begin'] . $baseBlocks[$key]['content'] . $baseBlocks[$key]['end'], $blocks[$key]['content'], $replace);
+                        }
+                    }
+                    if (isset($blocks[$name])) {
+                        // 带有{__block__}表示与所继承模板的相应标签合并,而不是覆盖
+                        $replace = str_replace(['{__BLOCK__}', '{__block__}'], $replace, $blocks[$name]['content']);
+                        if (!empty($val['parent'])) {
+                            // 如果不是最顶层的block标签
+                            $parent = $val['parent'];
+                            if (isset($blocks[$parent])) {
+                                $blocks[$parent]['content'] = str_replace($blocks[$name]['begin'] . $blocks[$name]['content'] . $blocks[$name]['end'], $replace, $blocks[$parent]['content']);
+                            }
+                            $blocks[$name]['content'] = $replace;
+                            $children[$parent][]      = $name;
+                            continue;
+                        }
+                    } elseif (!empty($val['parent'])) {
+                        // 如果子标签没有被继承则用原值
+                        $children[$val['parent']][] = $name;
+                        $blocks[$name]              = $val;
+                    }
+                    if (!$val['parent']) {
+                        // 替换模板中的顶级block标签
+                        $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend);
+                    }
+                }
+            }
+            $content = $extend;
+            unset($blocks, $baseBlocks);
+        }
+        return;
+    }
+
+    /**
+     * 替换页面中的literal标签
+     * @access private
+     * @param  string   $content 模板内容
+     * @param  boolean  $restore 是否为还原
+     * @return void
+     */
+    private function parseLiteral(&$content, $restore = false)
+    {
+        $regex = $this->getRegex($restore ? 'restoreliteral' : 'literal');
+        if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
+            if (!$restore) {
+                $count = count($this->literal);
+                // 替换literal标签
+                foreach ($matches as $match) {
+                    $this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2]));
+                    $content         = str_replace($match[0], "<!--###literal{$count}###-->", $content);
+                    $count++;
+                }
+            } else {
+                // 还原literal标签
+                foreach ($matches as $match) {
+                    $content = str_replace($match[0], $this->literal[$match[1]], $content);
+                }
+                // 清空literal记录
+                $this->literal = [];
+            }
+            unset($matches);
+        }
+        return;
+    }
+
+    /**
+     * 获取模板中的block标签
+     * @access private
+     * @param  string   $content 模板内容
+     * @param  boolean  $sort 是否排序
+     * @return array
+     */
+    private function parseBlock(&$content, $sort = false)
+    {
+        $regex  = $this->getRegex('block');
+        $result = [];
+        if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
+            $right = $keys = [];
+            foreach ($matches as $match) {
+                if (empty($match['name'][0])) {
+                    if (count($right) > 0) {
+                        $tag                  = array_pop($right);
+                        $start                = $tag['offset'] + strlen($tag['tag']);
+                        $length               = $match[0][1] - $start;
+                        $result[$tag['name']] = [
+                            'begin'   => $tag['tag'],
+                            'content' => substr($content, $start, $length),
+                            'end'     => $match[0][0],
+                            'parent'  => count($right) ? end($right)['name'] : '',
+                        ];
+                        $keys[$tag['name']] = $match[0][1];
+                    }
+                } else {
+                    // 标签头压入栈
+                    $right[] = [
+                        'name'   => $match[2][0],
+                        'offset' => $match[0][1],
+                        'tag'    => $match[0][0],
+                    ];
+                }
+            }
+            unset($right, $matches);
+            if ($sort) {
+                // 按block标签结束符在模板中的位置排序
+                array_multisort($keys, $result);
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * 搜索模板页面中包含的TagLib库
+     * 并返回列表
+     * @access private
+     * @param  string $content 模板内容
+     * @return array|null
+     */
+    private function getIncludeTagLib(&$content)
+    {
+        // 搜索是否有TagLib标签
+        if (preg_match($this->getRegex('taglib'), $content, $matches)) {
+            // 替换TagLib标签
+            $content = str_replace($matches[0], '', $content);
+            return explode(',', $matches['name']);
+        }
+        return;
+    }
+
+    /**
+     * TagLib库解析
+     * @access public
+     * @param  string   $tagLib 要解析的标签库
+     * @param  string   $content 要解析的模板内容
+     * @param  boolean  $hide 是否隐藏标签库前缀
+     * @return void
+     */
+    public function parseTagLib($tagLib, &$content, $hide = false)
+    {
+        if (false !== strpos($tagLib, '\\')) {
+            // 支持指定标签库的命名空间
+            $className = $tagLib;
+            $tagLib    = substr($tagLib, strrpos($tagLib, '\\') + 1);
+        } else {
+            $className = '\\think\\template\\taglib\\' . ucwords($tagLib);
+        }
+        /** @var Taglib $tLib */
+        $tLib = new $className($this);
+        $tLib->parseTag($content, $hide ? '' : $tagLib);
+        return;
+    }
+
+    /**
+     * 分析标签属性
+     * @access public
+     * @param  string   $str 属性字符串
+     * @param  string   $name 不为空时返回指定的属性名
+     * @return array
+     */
+    public function parseAttr($str, $name = null)
+    {
+        $regex = '/\s+(?>(?P<name>[\w-]+)\s*)=(?>\s*)([\"\'])(?P<value>(?:(?!\\2).)*)\\2/is';
+        $array = [];
+        if (preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) {
+            foreach ($matches as $match) {
+                $array[$match['name']] = $match['value'];
+            }
+            unset($matches);
+        }
+        if (!empty($name) && isset($array[$name])) {
+            return $array[$name];
+        } else {
+            return $array;
+        }
+    }
+
+    /**
+     * 模板标签解析
+     * 格式: {TagName:args [|content] }
+     * @access private
+     * @param  string $content 要解析的模板内容
+     * @return void
+     */
+    private function parseTag(&$content)
+    {
+        $regex = $this->getRegex('tag');
+        if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
+            foreach ($matches as $match) {
+                $str  = stripslashes($match[1]);
+                $flag = substr($str, 0, 1);
+                switch ($flag) {
+                    case '$':
+                        // 解析模板变量 格式 {$varName}
+                        // 是否带有?号
+                        if (false !== $pos = strpos($str, '?')) {
+                            $array = preg_split('/([!=]={1,2}|(?<!-)[><]={0,1})/', substr($str, 0, $pos), 2, PREG_SPLIT_DELIM_CAPTURE);
+                            $name  = $array[0];
+                            $this->parseVar($name);
+                            $this->parseVarFunction($name);
+
+                            $str = trim(substr($str, $pos + 1));
+                            $this->parseVar($str);
+                            $first = substr($str, 0, 1);
+                            if (strpos($name, ')')) {
+                                // $name为对象或是自动识别,或者含有函数
+                                if (isset($array[1])) {
+                                    $this->parseVar($array[2]);
+                                    $name .= $array[1] . $array[2];
+                                }
+                                switch ($first) {
+                                    case '?':
+                                        $str = '<?php echo (' . $name . ') ? ' . $name . ' : ' . substr($str, 1) . '; ?>';
+                                        break;
+                                    case '=':
+                                        $str = '<?php if(' . $name . ') echo ' . substr($str, 1) . '; ?>';
+                                        break;
+                                    default:
+                                        $str = '<?php echo ' . $name . '?' . $str . '; ?>';
+                                }
+                            } else {
+                                if (isset($array[1])) {
+                                    $this->parseVar($array[2]);
+                                    $express = $name . $array[1] . $array[2];
+                                } else {
+                                    $express = false;
+                                }
+                                // $name为数组
+                                switch ($first) {
+                                    case '?':
+                                        // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx
+                                        $str = '<?php echo ' . ($express ?: 'isset(' . $name . ')') . '?' . $name . ':' . substr($str, 1) . '; ?>';
+                                        break;
+                                    case '=':
+                                        // {$varname?='xxx'} $varname为真时才输出xxx
+                                        $str = '<?php if(' . ($express ?: '!empty(' . $name . ')') . ') echo ' . substr($str, 1) . '; ?>';
+                                        break;
+                                    case ':':
+                                        // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx
+                                        $str = '<?php echo ' . ($express ?: '!empty(' . $name . ')') . '?' . $name . $str . '; ?>';
+                                        break;
+                                    default:
+                                        $str = '<?php echo ' . ($express ?: '!empty(' . $name . ')') . '?' . $str . '; ?>';
+                                }
+                            }
+                        } else {
+                            $this->parseVar($str);
+                            $this->parseVarFunction($str);
+                            $str = '<?php echo ' . $str . '; ?>';
+                        }
+                        break;
+                    case ':':
+                        // 输出某个函数的结果
+                        $str = substr($str, 1);
+                        $this->parseVar($str);
+                        $str = '<?php echo ' . $str . '; ?>';
+                        break;
+                    case '~':
+                        // 执行某个函数
+                        $str = substr($str, 1);
+                        $this->parseVar($str);
+                        $str = '<?php ' . $str . '; ?>';
+                        break;
+                    case '-':
+                    case '+':
+                        // 输出计算
+                        $this->parseVar($str);
+                        $str = '<?php echo ' . $str . '; ?>';
+                        break;
+                    case '/':
+                        // 注释标签
+                        $flag2 = substr($str, 1, 1);
+                        if ('/' == $flag2 || ('*' == $flag2 && substr(rtrim($str), -2) == '*/')) {
+                            $str = '';
+                        }
+                        break;
+                    default:
+                        // 未识别的标签直接返回
+                        $str = $this->config['tpl_begin'] . $str . $this->config['tpl_end'];
+                        break;
+                }
+                $content = str_replace($match[0], $str, $content);
+            }
+            unset($matches);
+        }
+        return;
+    }
+
+    /**
+     * 模板变量解析,支持使用函数
+     * 格式: {$varname|function1|function2=arg1,arg2}
+     * @access public
+     * @param  string $varStr 变量数据
+     * @return void
+     */
+    public function parseVar(&$varStr)
+    {
+        $varStr = trim($varStr);
+        if (preg_match_all('/\$[a-zA-Z_](?>\w*)(?:[:\.][0-9a-zA-Z_](?>\w*))+/', $varStr, $matches, PREG_OFFSET_CAPTURE)) {
+            static $_varParseList = [];
+            while ($matches[0]) {
+                $match = array_pop($matches[0]);
+                //如果已经解析过该变量字串,则直接返回变量值
+                if (isset($_varParseList[$match[0]])) {
+                    $parseStr = $_varParseList[$match[0]];
+                } else {
+                    if (strpos($match[0], '.')) {
+                        $vars  = explode('.', $match[0]);
+                        $first = array_shift($vars);
+                        if ('$Think' == $first) {
+                            // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出
+                            $parseStr = $this->parseThinkVar($vars);
+                        } elseif ('$Request' == $first) {
+                            // 获取Request请求对象参数
+                            $method = array_shift($vars);
+                            if (!empty($vars)) {
+                                $params = implode('.', $vars);
+                                if ('true' != $params) {
+                                    $params = '\'' . $params . '\'';
+                                }
+                            } else {
+                                $params = '';
+                            }
+                            $parseStr = '\think\Request::instance()->' . $method . '(' . $params . ')';
+                        } else {
+                            switch ($this->config['tpl_var_identify']) {
+                                case 'array': // 识别为数组
+                                    $parseStr = $first . '[\'' . implode('\'][\'', $vars) . '\']';
+                                    break;
+                                case 'obj': // 识别为对象
+                                    $parseStr = $first . '->' . implode('->', $vars);
+                                    break;
+                                default: // 自动判断数组或对象
+                                    $parseStr = '(is_array(' . $first . ')?' . $first . '[\'' . implode('\'][\'', $vars) . '\']:' . $first . '->' . implode('->', $vars) . ')';
+                            }
+                        }
+                    } else {
+                        $parseStr = str_replace(':', '->', $match[0]);
+                    }
+                    $_varParseList[$match[0]] = $parseStr;
+                }
+                $varStr = substr_replace($varStr, $parseStr, $match[1], strlen($match[0]));
+            }
+            unset($matches);
+        }
+        return;
+    }
+
+    /**
+     * 对模板中使用了函数的变量进行解析
+     * 格式 {$varname|function1|function2=arg1,arg2}
+     * @access public
+     * @param  string $varStr 变量字符串
+     * @return void
+     */
+    public function parseVarFunction(&$varStr)
+    {
+        if (false == strpos($varStr, '|')) {
+            return;
+        }
+        static $_varFunctionList = [];
+        $_key                    = md5($varStr);
+        //如果已经解析过该变量字串,则直接返回变量值
+        if (isset($_varFunctionList[$_key])) {
+            $varStr = $_varFunctionList[$_key];
+        } else {
+            $varArray = explode('|', $varStr);
+            // 取得变量名称
+            $name = array_shift($varArray);
+            // 对变量使用函数
+            $length = count($varArray);
+            // 取得模板禁止使用函数列表
+            $template_deny_funs = explode(',', $this->config['tpl_deny_func_list']);
+            for ($i = 0; $i < $length; $i++) {
+                $args = explode('=', $varArray[$i], 2);
+                // 模板函数过滤
+                $fun = trim($args[0]);
+                switch ($fun) {
+                    case 'default': // 特殊模板函数
+                        if (false === strpos($name, '(')) {
+                            $name = '(isset(' . $name . ') && (' . $name . ' !== \'\')?' . $name . ':' . $args[1] . ')';
+                        } else {
+                            $name = '(' . $name . ' ?: ' . $args[1] . ')';
+                        }
+                        break;
+                    default: // 通用模板函数
+                        if (!in_array($fun, $template_deny_funs)) {
+                            if (isset($args[1])) {
+                                if (strstr($args[1], '###')) {
+                                    $args[1] = str_replace('###', $name, $args[1]);
+                                    $name    = "$fun($args[1])";
+                                } else {
+                                    $name = "$fun($name,$args[1])";
+                                }
+                            } else {
+                                if (!empty($args[0])) {
+                                    $name = "$fun($name)";
+                                }
+                            }
+                        }
+                }
+            }
+            $_varFunctionList[$_key] = $name;
+            $varStr                  = $name;
+        }
+        return;
+    }
+
+    /**
+     * 特殊模板变量解析
+     * 格式 以 $Think. 打头的变量属于特殊模板变量
+     * @access public
+     * @param  array $vars 变量数组
+     * @return string
+     */
+    public function parseThinkVar($vars)
+    {
+        $type  = strtoupper(trim(array_shift($vars)));
+        $param = implode('.', $vars);
+        if ($vars) {
+            switch ($type) {
+                case 'SERVER':
+                    $parseStr = '\\think\\Request::instance()->server(\'' . $param . '\')';
+                    break;
+                case 'GET':
+                    $parseStr = '\\think\\Request::instance()->get(\'' . $param . '\')';
+                    break;
+                case 'POST':
+                    $parseStr = '\\think\\Request::instance()->post(\'' . $param . '\')';
+                    break;
+                case 'COOKIE':
+                    $parseStr = '\\think\\Cookie::get(\'' . $param . '\')';
+                    break;
+                case 'SESSION':
+                    $parseStr = '\\think\\Session::get(\'' . $param . '\')';
+                    break;
+                case 'ENV':
+                    $parseStr = '\\think\\Request::instance()->env(\'' . $param . '\')';
+                    break;
+                case 'REQUEST':
+                    $parseStr = '\\think\\Request::instance()->request(\'' . $param . '\')';
+                    break;
+                case 'CONST':
+                    $parseStr = strtoupper($param);
+                    break;
+                case 'LANG':
+                    $parseStr = '\\think\\Lang::get(\'' . $param . '\')';
+                    break;
+                case 'CONFIG':
+                    $parseStr = '\\think\\Config::get(\'' . $param . '\')';
+                    break;
+                default:
+                    $parseStr = '\'\'';
+                    break;
+            }
+        } else {
+            switch ($type) {
+                case 'NOW':
+                    $parseStr = "date('Y-m-d g:i a',time())";
+                    break;
+                case 'VERSION':
+                    $parseStr = 'THINK_VERSION';
+                    break;
+                case 'LDELIM':
+                    $parseStr = '\'' . ltrim($this->config['tpl_begin'], '\\') . '\'';
+                    break;
+                case 'RDELIM':
+                    $parseStr = '\'' . ltrim($this->config['tpl_end'], '\\') . '\'';
+                    break;
+                default:
+                    if (defined($type)) {
+                        $parseStr = $type;
+                    } else {
+                        $parseStr = '';
+                    }
+            }
+        }
+        return $parseStr;
+    }
+
+    /**
+     * 分析加载的模板文件并读取内容 支持多个模板文件读取
+     * @access private
+     * @param  string $templateName 模板文件名
+     * @return string
+     */
+    private function parseTemplateName($templateName)
+    {
+        $array    = explode(',', $templateName);
+        $parseStr = '';
+        foreach ($array as $templateName) {
+            if (empty($templateName)) {
+                continue;
+            }
+            if (0 === strpos($templateName, '$')) {
+                //支持加载变量文件名
+                $templateName = $this->get(substr($templateName, 1));
+            }
+            $template = $this->parseTemplateFile($templateName);
+            if ($template) {
+                // 获取模板文件内容
+                $parseStr .= file_get_contents($template);
+            }
+        }
+        return $parseStr;
+    }
+
+    /**
+     * 解析模板文件名
+     * @access private
+     * @param  string $template 文件名
+     * @return string|false
+     */
+    private function parseTemplateFile($template)
+    {
+        if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+            if (strpos($template, '@')) {
+                list($module, $template) = explode('@', $template);
+            }
+            if (0 !== strpos($template, '/')) {
+                $template = str_replace(['/', ':'], $this->config['view_depr'], $template);
+            } else {
+                $template = str_replace(['/', ':'], $this->config['view_depr'], substr($template, 1));
+            }
+            if ($this->config['view_base']) {
+                $module = isset($module) ? $module : Request::instance()->module();
+                $path   = $this->config['view_base'] . ($module ? $module . DS : '');
+            } else {
+                $path = isset($module) ? APP_PATH . $module . DS . basename($this->config['view_path']) . DS : $this->config['view_path'];
+            }
+            $template = realpath($path . $template . '.' . ltrim($this->config['view_suffix'], '.'));
+        }
+
+        if (is_file($template)) {
+            // 记录模板文件的更新时间
+            $this->includeFile[$template] = filemtime($template);
+            return $template;
+        } else {
+            throw new TemplateNotFoundException('template not exists:' . $template, $template);
+        }
+    }
+
+    /**
+     * 按标签生成正则
+     * @access private
+     * @param  string $tagName 标签名
+     * @return string
+     */
+    private function getRegex($tagName)
+    {
+        $regex = '';
+        if ('tag' == $tagName) {
+            $begin = $this->config['tpl_begin'];
+            $end   = $this->config['tpl_end'];
+            if (strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1) {
+                $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>[^' . $end . ']*))' . $end;
+            } else {
+                $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>(?:(?!' . $end . ').)*))' . $end;
+            }
+        } else {
+            $begin  = $this->config['taglib_begin'];
+            $end    = $this->config['taglib_end'];
+            $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false;
+            switch ($tagName) {
+                case 'block':
+                    if ($single) {
+                        $regex = $begin . '(?:' . $tagName . '\b(?>(?:(?!name=).)*)\bname=([\'\"])(?P<name>[\$\w\-\/\.]+)\\1(?>[^' . $end . ']*)|\/' . $tagName . ')' . $end;
+                    } else {
+                        $regex = $begin . '(?:' . $tagName . '\b(?>(?:(?!name=).)*)\bname=([\'\"])(?P<name>[\$\w\-\/\.]+)\\1(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end;
+                    }
+                    break;
+                case 'literal':
+                    if ($single) {
+                        $regex = '(' . $begin . $tagName . '\b(?>[^' . $end . ']*)' . $end . ')';
+                        $regex .= '(?:(?>[^' . $begin . ']*)(?>(?!' . $begin . '(?>' . $tagName . '\b[^' . $end . ']*|\/' . $tagName . ')' . $end . ')' . $begin . '[^' . $begin . ']*)*)';
+                        $regex .= '(' . $begin . '\/' . $tagName . $end . ')';
+                    } else {
+                        $regex = '(' . $begin . $tagName . '\b(?>(?:(?!' . $end . ').)*)' . $end . ')';
+                        $regex .= '(?:(?>(?:(?!' . $begin . ').)*)(?>(?!' . $begin . '(?>' . $tagName . '\b(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end . ')' . $begin . '(?>(?:(?!' . $begin . ').)*))*)';
+                        $regex .= '(' . $begin . '\/' . $tagName . $end . ')';
+                    }
+                    break;
+                case 'restoreliteral':
+                    $regex = '<!--###literal(\d+)###-->';
+                    break;
+                case 'include':
+                    $name = 'file';
+                case 'taglib':
+                case 'layout':
+                case 'extend':
+                    if (empty($name)) {
+                        $name = 'name';
+                    }
+                    if ($single) {
+                        $regex = $begin . $tagName . '\b(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P<name>[\$\w\-\/\.\:@,\\\\]+)\\1(?>[^' . $end . ']*)' . $end;
+                    } else {
+                        $regex = $begin . $tagName . '\b(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P<name>[\$\w\-\/\.\:@,\\\\]+)\\1(?>(?:(?!' . $end . ').)*)' . $end;
+                    }
+                    break;
+            }
+        }
+        return '/' . $regex . '/is';
+    }
+}

+ 333 - 0
thinkphp/library/think/Url.php

@@ -0,0 +1,333 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+class Url
+{
+    // 生成URL地址的root
+    protected static $root;
+    protected static $bindCheck;
+
+    /**
+     * URL生成 支持路由反射
+     * @param string            $url 路由地址
+     * @param string|array      $vars 参数(支持数组和字符串)a=val&b=val2... ['a'=>'val1', 'b'=>'val2']
+     * @param string|bool       $suffix 伪静态后缀,默认为true表示获取配置值
+     * @param boolean|string    $domain 是否显示域名 或者直接传入域名
+     * @return string
+     */
+    public static function build($url = '', $vars = '', $suffix = true, $domain = false)
+    {
+        if (false === $domain && Route::rules('domain')) {
+            $domain = true;
+        }
+        // 解析URL
+        if (0 === strpos($url, '[') && $pos = strpos($url, ']')) {
+            // [name] 表示使用路由命名标识生成URL
+            $name = substr($url, 1, $pos - 1);
+            $url  = 'name' . substr($url, $pos + 1);
+        }
+        if (false === strpos($url, '://') && 0 !== strpos($url, '/')) {
+            $info = parse_url($url);
+            $url  = !empty($info['path']) ? $info['path'] : '';
+            if (isset($info['fragment'])) {
+                // 解析锚点
+                $anchor = $info['fragment'];
+                if (false !== strpos($anchor, '?')) {
+                    // 解析参数
+                    list($anchor, $info['query']) = explode('?', $anchor, 2);
+                }
+                if (false !== strpos($anchor, '@')) {
+                    // 解析域名
+                    list($anchor, $domain) = explode('@', $anchor, 2);
+                }
+            } elseif (strpos($url, '@') && false === strpos($url, '\\')) {
+                // 解析域名
+                list($url, $domain) = explode('@', $url, 2);
+            }
+        }
+
+        // 解析参数
+        if (is_string($vars)) {
+            // aaa=1&bbb=2 转换成数组
+            parse_str($vars, $vars);
+        }
+
+        if ($url) {
+            $rule = Route::name(isset($name) ? $name : $url . (isset($info['query']) ? '?' . $info['query'] : ''));
+            if (is_null($rule) && isset($info['query'])) {
+                $rule = Route::name($url);
+                // 解析地址里面参数 合并到vars
+                parse_str($info['query'], $params);
+                $vars = array_merge($params, $vars);
+                unset($info['query']);
+            }
+        }
+        if (!empty($rule) && $match = self::getRuleUrl($rule, $vars)) {
+            // 匹配路由命名标识
+            $url = $match[0];
+            // 替换可选分隔符
+            $url = preg_replace(['/(\W)\?$/', '/(\W)\?/'], ['', '\1'], $url);
+            if (!empty($match[1])) {
+                $domain = $match[1];
+            }
+            if (!is_null($match[2])) {
+                $suffix = $match[2];
+            }
+        } elseif (!empty($rule) && isset($name)) {
+            throw new \InvalidArgumentException('route name not exists:' . $name);
+        } else {
+            // 检查别名路由
+            $alias      = Route::rules('alias');
+            $matchAlias = false;
+            if ($alias) {
+                // 别名路由解析
+                foreach ($alias as $key => $val) {
+                    if (is_array($val)) {
+                        $val = $val[0];
+                    }
+                    if (0 === strpos($url, $val)) {
+                        $url        = $key . substr($url, strlen($val));
+                        $matchAlias = true;
+                        break;
+                    }
+                }
+            }
+            if (!$matchAlias) {
+                // 路由标识不存在 直接解析
+                $url = self::parseUrl($url, $domain);
+            }
+            if (isset($info['query'])) {
+                // 解析地址里面参数 合并到vars
+                parse_str($info['query'], $params);
+                $vars = array_merge($params, $vars);
+            }
+        }
+
+        // 检测URL绑定
+        if (!self::$bindCheck) {
+            $type = Route::getBind('type');
+            if ($type) {
+                $bind = Route::getBind($type);
+                if ($bind && 0 === strpos($url, $bind)) {
+                    $url = substr($url, strlen($bind) + 1);
+                }
+            }
+        }
+        // 还原URL分隔符
+        $depr = Config::get('pathinfo_depr');
+        $url  = str_replace('/', $depr, $url);
+
+        // URL后缀
+        $suffix = in_array($url, ['/', '']) ? '' : self::parseSuffix($suffix);
+        // 锚点
+        $anchor = !empty($anchor) ? '#' . $anchor : '';
+        // 参数组装
+        if (!empty($vars)) {
+            // 添加参数
+            if (Config::get('url_common_param')) {
+                $vars = http_build_query($vars);
+                $url .= $suffix . '?' . $vars . $anchor;
+            } else {
+                $paramType = Config::get('url_param_type');
+                foreach ($vars as $var => $val) {
+                    if ('' !== trim($val)) {
+                        if ($paramType) {
+                            $url .= $depr . urlencode($val);
+                        } else {
+                            $url .= $depr . $var . $depr . urlencode($val);
+                        }
+                    }
+                }
+                $url .= $suffix . $anchor;
+            }
+        } else {
+            $url .= $suffix . $anchor;
+        }
+        // 检测域名
+        $domain = self::parseDomain($url, $domain);
+        // URL组装
+        $url = $domain . rtrim(self::$root ?: Request::instance()->root(), '/') . '/' . ltrim($url, '/');
+
+        self::$bindCheck = false;
+        return $url;
+    }
+
+    // 直接解析URL地址
+    protected static function parseUrl($url, &$domain)
+    {
+        $request = Request::instance();
+        if (0 === strpos($url, '/')) {
+            // 直接作为路由地址解析
+            $url = substr($url, 1);
+        } elseif (false !== strpos($url, '\\')) {
+            // 解析到类
+            $url = ltrim(str_replace('\\', '/', $url), '/');
+        } elseif (0 === strpos($url, '@')) {
+            // 解析到控制器
+            $url = substr($url, 1);
+        } else {
+            // 解析到 模块/控制器/操作
+            $module  = $request->module();
+            $domains = Route::rules('domain');
+            if (true === $domain && 2 == substr_count($url, '/')) {
+                $current = $request->host();
+                $match   = [];
+                $pos     = [];
+                foreach ($domains as $key => $item) {
+                    if (isset($item['[bind]']) && 0 === strpos($url, $item['[bind]'][0])) {
+                        $pos[$key] = strlen($item['[bind]'][0]) + 1;
+                        $match[]   = $key;
+                        $module    = '';
+                    }
+                }
+                if ($match) {
+                    $domain = current($match);
+                    foreach ($match as $item) {
+                        if (0 === strpos($current, $item)) {
+                            $domain = $item;
+                        }
+                    }
+                    self::$bindCheck = true;
+                    $url             = substr($url, $pos[$domain]);
+                }
+            } elseif ($domain) {
+                if (isset($domains[$domain]['[bind]'][0])) {
+                    $bindModule = $domains[$domain]['[bind]'][0];
+                    if ($bindModule && !in_array($bindModule[0], ['\\', '@'])) {
+                        $module = '';
+                    }
+                }
+            }
+            $module = $module ? $module . '/' : '';
+
+            $controller = $request->controller();
+            if ('' == $url) {
+                // 空字符串输出当前的 模块/控制器/操作
+                $action = $request->action();
+            } else {
+                $path       = explode('/', $url);
+                $action     = array_pop($path);
+                $controller = empty($path) ? $controller : array_pop($path);
+                $module     = empty($path) ? $module : array_pop($path) . '/';
+            }
+            if (Config::get('url_convert')) {
+                $action     = strtolower($action);
+                $controller = Loader::parseName($controller);
+            }
+            $url = $module . $controller . '/' . $action;
+        }
+        return $url;
+    }
+
+    // 检测域名
+    protected static function parseDomain(&$url, $domain)
+    {
+        if (!$domain) {
+            return '';
+        }
+        $request    = Request::instance();
+        $rootDomain = Config::get('url_domain_root');
+        if (true === $domain) {
+            // 自动判断域名
+            $domain = Config::get('app_host') ?: $request->host();
+
+            $domains = Route::rules('domain');
+            if ($domains) {
+                $route_domain = array_keys($domains);
+                foreach ($route_domain as $domain_prefix) {
+                    if (0 === strpos($domain_prefix, '*.') && strpos($domain, ltrim($domain_prefix, '*.')) !== false) {
+                        foreach ($domains as $key => $rule) {
+                            $rule = is_array($rule) ? $rule[0] : $rule;
+                            if (is_string($rule) && false === strpos($key, '*') && 0 === strpos($url, $rule)) {
+                                $url    = ltrim($url, $rule);
+                                $domain = $key;
+                                // 生成对应子域名
+                                if (!empty($rootDomain)) {
+                                    $domain .= $rootDomain;
+                                }
+                                break;
+                            } elseif (false !== strpos($key, '*')) {
+                                if (!empty($rootDomain)) {
+                                    $domain .= $rootDomain;
+                                }
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+        } else {
+            if (empty($rootDomain)) {
+                $host       = Config::get('app_host') ?: $request->host();
+                $rootDomain = substr_count($host, '.') > 1 ? substr(strstr($host, '.'), 1) : $host;
+            }
+            if (substr_count($domain, '.') < 2 && !strpos($domain, $rootDomain)) {
+                $domain .= '.' . $rootDomain;
+            }
+        }
+        if (false !== strpos($domain, '://')) {
+            $scheme = '';
+        } else {
+            $scheme = $request->isSsl() || Config::get('is_https') ? 'https://' : 'http://';
+        }
+        return $scheme . $domain;
+    }
+
+    // 解析URL后缀
+    protected static function parseSuffix($suffix)
+    {
+        if ($suffix) {
+            $suffix = true === $suffix ? Config::get('url_html_suffix') : $suffix;
+            if ($pos = strpos($suffix, '|')) {
+                $suffix = substr($suffix, 0, $pos);
+            }
+        }
+        return (empty($suffix) || 0 === strpos($suffix, '.')) ? $suffix : '.' . $suffix;
+    }
+
+    // 匹配路由地址
+    public static function getRuleUrl($rule, &$vars = [])
+    {
+        foreach ($rule as $item) {
+            list($url, $pattern, $domain, $suffix) = $item;
+            if (empty($pattern)) {
+                return [rtrim($url, '$'), $domain, $suffix];
+            }
+            $type = Config::get('url_common_param');
+            foreach ($pattern as $key => $val) {
+                if (isset($vars[$key])) {
+                    $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key . '', '<' . $key . '>'], $type ? $vars[$key] : urlencode($vars[$key]), $url);
+                    unset($vars[$key]);
+                    $result = [$url, $domain, $suffix];
+                } elseif (2 == $val) {
+                    $url    = str_replace(['/[:' . $key . ']', '[:' . $key . ']', '<' . $key . '?>'], '', $url);
+                    $result = [$url, $domain, $suffix];
+                } else {
+                    break;
+                }
+            }
+            if (isset($result)) {
+                return $result;
+            }
+        }
+        return false;
+    }
+
+    // 指定当前生成URL地址的root
+    public static function root($root)
+    {
+        self::$root = $root;
+        Request::instance()->root($root);
+    }
+}

+ 1335 - 0
thinkphp/library/think/Validate.php

@@ -0,0 +1,1335 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+use think\exception\ClassNotFoundException;
+
+class Validate
+{
+    // 实例
+    protected static $instance;
+
+    // 自定义的验证类型
+    protected static $type = [];
+
+    // 验证类型别名
+    protected $alias = [
+        '>' => 'gt', '>=' => 'egt', '<' => 'lt', '<=' => 'elt', '=' => 'eq', 'same' => 'eq',
+    ];
+
+    // 当前验证的规则
+    protected $rule = [];
+
+    // 验证提示信息
+    protected $message = [];
+    // 验证字段描述
+    protected $field = [];
+
+    // 验证规则默认提示信息
+    protected static $typeMsg = [
+        'require'     => ':attribute require',
+        'number'      => ':attribute must be numeric',
+        'integer'     => ':attribute must be integer',
+        'float'       => ':attribute must be float',
+        'boolean'     => ':attribute must be bool',
+        'email'       => ':attribute not a valid email address',
+        'array'       => ':attribute must be a array',
+        'accepted'    => ':attribute must be yes,on or 1',
+        'date'        => ':attribute not a valid datetime',
+        'file'        => ':attribute not a valid file',
+        'image'       => ':attribute not a valid image',
+        'alpha'       => ':attribute must be alpha',
+        'alphaNum'    => ':attribute must be alpha-numeric',
+        'alphaDash'   => ':attribute must be alpha-numeric, dash, underscore',
+        'activeUrl'   => ':attribute not a valid domain or ip',
+        'chs'         => ':attribute must be chinese',
+        'chsAlpha'    => ':attribute must be chinese or alpha',
+        'chsAlphaNum' => ':attribute must be chinese,alpha-numeric',
+        'chsDash'     => ':attribute must be chinese,alpha-numeric,underscore, dash',
+        'url'         => ':attribute not a valid url',
+        'ip'          => ':attribute not a valid ip',
+        'dateFormat'  => ':attribute must be dateFormat of :rule',
+        'in'          => ':attribute must be in :rule',
+        'notIn'       => ':attribute be notin :rule',
+        'between'     => ':attribute must between :1 - :2',
+        'notBetween'  => ':attribute not between :1 - :2',
+        'length'      => 'size of :attribute must be :rule',
+        'max'         => 'max size of :attribute must be :rule',
+        'min'         => 'min size of :attribute must be :rule',
+        'after'       => ':attribute cannot be less than :rule',
+        'before'      => ':attribute cannot exceed :rule',
+        'expire'      => ':attribute not within :rule',
+        'allowIp'     => 'access IP is not allowed',
+        'denyIp'      => 'access IP denied',
+        'confirm'     => ':attribute out of accord with :2',
+        'different'   => ':attribute cannot be same with :2',
+        'egt'         => ':attribute must greater than or equal :rule',
+        'gt'          => ':attribute must greater than :rule',
+        'elt'         => ':attribute must less than or equal :rule',
+        'lt'          => ':attribute must less than :rule',
+        'eq'          => ':attribute must equal :rule',
+        'unique'      => ':attribute has exists',
+        'regex'       => ':attribute not conform to the rules',
+        'method'      => 'invalid Request method',
+        'token'       => 'invalid token',
+        'fileSize'    => 'filesize not match',
+        'fileExt'     => 'extensions to upload is not allowed',
+        'fileMime'    => 'mimetype to upload is not allowed',
+    ];
+
+    // 当前验证场景
+    protected $currentScene = null;
+
+    // 正则表达式 regex = ['zip'=>'\d{6}',...]
+    protected $regex = [];
+
+    // 验证场景 scene = ['edit'=>'name1,name2,...']
+    protected $scene = [];
+
+    // 验证失败错误信息
+    protected $error = [];
+
+    // 批量验证
+    protected $batch = false;
+
+    /**
+     * 构造函数
+     * @access public
+     * @param array $rules 验证规则
+     * @param array $message 验证提示信息
+     * @param array $field 验证字段描述信息
+     */
+    public function __construct(array $rules = [], $message = [], $field = [])
+    {
+        $this->rule    = array_merge($this->rule, $rules);
+        $this->message = array_merge($this->message, $message);
+        $this->field   = array_merge($this->field, $field);
+    }
+
+    /**
+     * 实例化验证
+     * @access public
+     * @param array     $rules 验证规则
+     * @param array     $message 验证提示信息
+     * @param array     $field 验证字段描述信息
+     * @return Validate
+     */
+    public static function make($rules = [], $message = [], $field = [])
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new self($rules, $message, $field);
+        }
+        return self::$instance;
+    }
+
+    /**
+     * 添加字段验证规则
+     * @access protected
+     * @param string|array  $name  字段名称或者规则数组
+     * @param mixed         $rule  验证规则
+     * @return Validate
+     */
+    public function rule($name, $rule = '')
+    {
+        if (is_array($name)) {
+            $this->rule = array_merge($this->rule, $name);
+        } else {
+            $this->rule[$name] = $rule;
+        }
+        return $this;
+    }
+
+    /**
+     * 注册验证(类型)规则
+     * @access public
+     * @param string    $type  验证规则类型
+     * @param mixed     $callback callback方法(或闭包)
+     * @return void
+     */
+    public static function extend($type, $callback = null)
+    {
+        if (is_array($type)) {
+            self::$type = array_merge(self::$type, $type);
+        } else {
+            self::$type[$type] = $callback;
+        }
+    }
+
+    /**
+     * 设置验证规则的默认提示信息
+     * @access protected
+     * @param string|array  $type  验证规则类型名称或者数组
+     * @param string        $msg  验证提示信息
+     * @return void
+     */
+    public static function setTypeMsg($type, $msg = null)
+    {
+        if (is_array($type)) {
+            self::$typeMsg = array_merge(self::$typeMsg, $type);
+        } else {
+            self::$typeMsg[$type] = $msg;
+        }
+    }
+
+    /**
+     * 设置提示信息
+     * @access public
+     * @param string|array  $name  字段名称
+     * @param string        $message 提示信息
+     * @return Validate
+     */
+    public function message($name, $message = '')
+    {
+        if (is_array($name)) {
+            $this->message = array_merge($this->message, $name);
+        } else {
+            $this->message[$name] = $message;
+        }
+        return $this;
+    }
+
+    /**
+     * 设置验证场景
+     * @access public
+     * @param string|array  $name  场景名或者场景设置数组
+     * @param mixed         $fields 要验证的字段
+     * @return Validate
+     */
+    public function scene($name, $fields = null)
+    {
+        if (is_array($name)) {
+            $this->scene = array_merge($this->scene, $name);
+        }if (is_null($fields)) {
+            // 设置当前场景
+            $this->currentScene = $name;
+        } else {
+            // 设置验证场景
+            $this->scene[$name] = $fields;
+        }
+        return $this;
+    }
+
+    /**
+     * 判断是否存在某个验证场景
+     * @access public
+     * @param string $name 场景名
+     * @return bool
+     */
+    public function hasScene($name)
+    {
+        return isset($this->scene[$name]);
+    }
+
+    /**
+     * 设置批量验证
+     * @access public
+     * @param bool $batch  是否批量验证
+     * @return Validate
+     */
+    public function batch($batch = true)
+    {
+        $this->batch = $batch;
+        return $this;
+    }
+
+    /**
+     * 数据自动验证
+     * @access public
+     * @param array     $data  数据
+     * @param mixed     $rules  验证规则
+     * @param string    $scene 验证场景
+     * @return bool
+     */
+    public function check($data, $rules = [], $scene = '')
+    {
+        $this->error = [];
+
+        if (empty($rules)) {
+            // 读取验证规则
+            $rules = $this->rule;
+        }
+
+        // 分析验证规则
+        $scene = $this->getScene($scene);
+        if (is_array($scene)) {
+            // 处理场景验证字段
+            $change = [];
+            $array  = [];
+            foreach ($scene as $k => $val) {
+                if (is_numeric($k)) {
+                    $array[] = $val;
+                } else {
+                    $array[]    = $k;
+                    $change[$k] = $val;
+                }
+            }
+        }
+
+        foreach ($rules as $key => $item) {
+            // field => rule1|rule2... field=>['rule1','rule2',...]
+            if (is_numeric($key)) {
+                // [field,rule1|rule2,msg1|msg2]
+                $key  = $item[0];
+                $rule = $item[1];
+                if (isset($item[2])) {
+                    $msg = is_string($item[2]) ? explode('|', $item[2]) : $item[2];
+                } else {
+                    $msg = [];
+                }
+            } else {
+                $rule = $item;
+                $msg  = [];
+            }
+            if (strpos($key, '|')) {
+                // 字段|描述 用于指定属性名称
+                list($key, $title) = explode('|', $key);
+            } else {
+                $title = isset($this->field[$key]) ? $this->field[$key] : $key;
+            }
+
+            // 场景检测
+            if (!empty($scene)) {
+                if ($scene instanceof \Closure && !call_user_func_array($scene, [$key, $data])) {
+                    continue;
+                } elseif (is_array($scene)) {
+                    if (!in_array($key, $array)) {
+                        continue;
+                    } elseif (isset($change[$key])) {
+                        // 重载某个验证规则
+                        $rule = $change[$key];
+                    }
+                }
+            }
+
+            // 获取数据 支持二维数组
+            $value = $this->getDataValue($data, $key);
+
+            // 字段验证
+            if ($rule instanceof \Closure) {
+                // 匿名函数验证 支持传入当前字段和所有字段两个数据
+                $result = call_user_func_array($rule, [$value, $data]);
+            } else {
+                $result = $this->checkItem($key, $value, $rule, $data, $title, $msg);
+            }
+
+            if (true !== $result) {
+                // 没有返回true 则表示验证失败
+                if (!empty($this->batch)) {
+                    // 批量验证
+                    if (is_array($result)) {
+                        $this->error = array_merge($this->error, $result);
+                    } else {
+                        $this->error[$key] = $result;
+                    }
+                } else {
+                    $this->error = $result;
+                    return false;
+                }
+            }
+        }
+        return !empty($this->error) ? false : true;
+    }
+
+    /**
+     * 根据验证规则验证数据
+     * @access protected
+     * @param  mixed     $value 字段值
+     * @param  mixed     $rules 验证规则
+     * @return bool
+     */
+    protected function checkRule($value, $rules)
+    {
+        if ($rules instanceof \Closure) {
+            return call_user_func_array($rules, [$value]);
+        } elseif (is_string($rules)) {
+            $rules = explode('|', $rules);
+        }
+
+        foreach ($rules as $key => $rule) {
+            if ($rule instanceof \Closure) {
+                $result = call_user_func_array($rule, [$value]);
+            } else {
+                // 判断验证类型
+                list($type, $rule) = $this->getValidateType($key, $rule);
+
+                $callback = isset(self::$type[$type]) ? self::$type[$type] : [$this, $type];
+
+                $result = call_user_func_array($callback, [$value, $rule]);
+            }
+
+            if (true !== $result) {
+                return $result;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * 验证单个字段规则
+     * @access protected
+     * @param string    $field  字段名
+     * @param mixed     $value  字段值
+     * @param mixed     $rules  验证规则
+     * @param array     $data  数据
+     * @param string    $title  字段描述
+     * @param array     $msg  提示信息
+     * @return mixed
+     */
+    protected function checkItem($field, $value, $rules, $data, $title = '', $msg = [])
+    {
+        // 支持多规则验证 require|in:a,b,c|... 或者 ['require','in'=>'a,b,c',...]
+        if (is_string($rules)) {
+            $rules = explode('|', $rules);
+        }
+        $i = 0;
+        foreach ($rules as $key => $rule) {
+            if ($rule instanceof \Closure) {
+                $result = call_user_func_array($rule, [$value, $data]);
+                $info   = is_numeric($key) ? '' : $key;
+            } else {
+                // 判断验证类型
+                list($type, $rule, $info) = $this->getValidateType($key, $rule);
+
+                // 如果不是require 有数据才会行验证
+                if (0 === strpos($info, 'require') || (!is_null($value) && '' !== $value)) {
+                    // 验证类型
+                    $callback = isset(self::$type[$type]) ? self::$type[$type] : [$this, $type];
+                    // 验证数据
+                    $result = call_user_func_array($callback, [$value, $rule, $data, $field, $title]);
+                } else {
+                    $result = true;
+                }
+            }
+
+            if (false === $result) {
+                // 验证失败 返回错误信息
+                if (isset($msg[$i])) {
+                    $message = $msg[$i];
+                    if (is_string($message) && strpos($message, '{%') === 0) {
+                        $message = Lang::get(substr($message, 2, -1));
+                    }
+                } else {
+                    $message = $this->getRuleMsg($field, $title, $info, $rule);
+                }
+                return $message;
+            } elseif (true !== $result) {
+                // 返回自定义错误信息
+                if (is_string($result) && false !== strpos($result, ':')) {
+                    $result = str_replace([':attribute', ':rule'], [$title, (string) $rule], $result);
+                }
+                return $result;
+            }
+            $i++;
+        }
+        return $result;
+    }
+
+    /**
+     * 获取当前验证类型及规则
+     * @access public
+     * @param  mixed     $key
+     * @param  mixed     $rule
+     * @return array
+     */
+    protected function getValidateType($key, $rule)
+    {
+        // 判断验证类型
+        if (!is_numeric($key)) {
+            return [$key, $rule, $key];
+        }
+
+        if (strpos($rule, ':')) {
+            list($type, $rule) = explode(':', $rule, 2);
+            if (isset($this->alias[$type])) {
+                // 判断别名
+                $type = $this->alias[$type];
+            }
+            $info = $type;
+        } elseif (method_exists($this, $rule)) {
+            $type = $rule;
+            $info = $rule;
+            $rule = '';
+        } else {
+            $type = 'is';
+            $info = $rule;
+        }
+
+        return [$type, $rule, $info];
+    }
+
+    /**
+     * 验证是否和某个字段的值一致
+     * @access protected
+     * @param mixed     $value 字段值
+     * @param mixed     $rule  验证规则
+     * @param array     $data  数据
+     * @param string    $field 字段名
+     * @return bool
+     */
+    protected function confirm($value, $rule, $data, $field = '')
+    {
+        if ('' == $rule) {
+            if (strpos($field, '_confirm')) {
+                $rule = strstr($field, '_confirm', true);
+            } else {
+                $rule = $field . '_confirm';
+            }
+        }
+        return $this->getDataValue($data, $rule) === $value;
+    }
+
+    /**
+     * 验证是否和某个字段的值是否不同
+     * @access protected
+     * @param mixed $value 字段值
+     * @param mixed $rule  验证规则
+     * @param array $data  数据
+     * @return bool
+     */
+    protected function different($value, $rule, $data)
+    {
+        return $this->getDataValue($data, $rule) != $value;
+    }
+
+    /**
+     * 验证是否大于等于某个值
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @param array     $data  数据
+     * @return bool
+     */
+    protected function egt($value, $rule, $data)
+    {
+        $val = $this->getDataValue($data, $rule);
+        return !is_null($val) && $value >= $val;
+    }
+
+    /**
+     * 验证是否大于某个值
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @param array     $data  数据
+     * @return bool
+     */
+    protected function gt($value, $rule, $data)
+    {
+        $val = $this->getDataValue($data, $rule);
+        return !is_null($val) && $value > $val;
+    }
+
+    /**
+     * 验证是否小于等于某个值
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @param array     $data  数据
+     * @return bool
+     */
+    protected function elt($value, $rule, $data)
+    {
+        $val = $this->getDataValue($data, $rule);
+        return !is_null($val) && $value <= $val;
+    }
+
+    /**
+     * 验证是否小于某个值
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @param array     $data  数据
+     * @return bool
+     */
+    protected function lt($value, $rule, $data)
+    {
+        $val = $this->getDataValue($data, $rule);
+        return !is_null($val) && $value < $val;
+    }
+
+    /**
+     * 验证是否等于某个值
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function eq($value, $rule)
+    {
+        return $value == $rule;
+    }
+
+    /**
+     * 验证字段值是否为有效格式
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param string    $rule  验证规则
+     * @param array     $data  验证数据
+     * @return bool
+     */
+    protected function is($value, $rule, $data = [])
+    {
+        switch ($rule) {
+            case 'require':
+                // 必须
+                $result = !empty($value) || '0' == $value;
+                break;
+            case 'accepted':
+                // 接受
+                $result = in_array($value, ['1', 'on', 'yes']);
+                break;
+            case 'date':
+                // 是否是一个有效日期
+                $result = false !== strtotime($value);
+                break;
+            case 'alpha':
+                // 只允许字母
+                $result = $this->regex($value, '/^[A-Za-z]+$/');
+                break;
+            case 'alphaNum':
+                // 只允许字母和数字
+                $result = $this->regex($value, '/^[A-Za-z0-9]+$/');
+                break;
+            case 'alphaDash':
+                // 只允许字母、数字和下划线 破折号
+                $result = $this->regex($value, '/^[A-Za-z0-9\-\_]+$/');
+                break;
+            case 'chs':
+                // 只允许汉字
+                $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}]+$/u');
+                break;
+            case 'chsAlpha':
+                // 只允许汉字、字母
+                $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z]+$/u');
+                break;
+            case 'chsAlphaNum':
+                // 只允许汉字、字母和数字
+                $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9]+$/u');
+                break;
+            case 'chsDash':
+                // 只允许汉字、字母、数字和下划线_及破折号-
+                $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9\_\-]+$/u');
+                break;
+            case 'activeUrl':
+                // 是否为有效的网址
+                $result = checkdnsrr($value);
+                break;
+            case 'ip':
+                // 是否为IP地址
+                $result = $this->filter($value, [FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6]);
+                break;
+            case 'url':
+                // 是否为一个URL地址
+                $result = $this->filter($value, FILTER_VALIDATE_URL);
+                break;
+            case 'float':
+                // 是否为float
+                $result = $this->filter($value, FILTER_VALIDATE_FLOAT);
+                break;
+            case 'number':
+                $result = is_numeric($value);
+                break;
+            case 'integer':
+                // 是否为整型
+                $result = $this->filter($value, FILTER_VALIDATE_INT);
+                break;
+            case 'email':
+                // 是否为邮箱地址
+                $result = $this->filter($value, FILTER_VALIDATE_EMAIL);
+                break;
+            case 'boolean':
+                // 是否为布尔值
+                $result = in_array($value, [true, false, 0, 1, '0', '1'], true);
+                break;
+            case 'array':
+                // 是否为数组
+                $result = is_array($value);
+                break;
+            case 'file':
+                $result = $value instanceof File;
+                break;
+            case 'image':
+                $result = $value instanceof File && in_array($this->getImageType($value->getRealPath()), [1, 2, 3, 6]);
+                break;
+            case 'token':
+                $result = $this->token($value, '__token__', $data);
+                break;
+            default:
+                if (isset(self::$type[$rule])) {
+                    // 注册的验证规则
+                    $result = call_user_func_array(self::$type[$rule], [$value]);
+                } else {
+                    // 正则验证
+                    $result = $this->regex($value, $rule);
+                }
+        }
+        return $result;
+    }
+
+    // 判断图像类型
+    protected function getImageType($image)
+    {
+        if (function_exists('exif_imagetype')) {
+            return exif_imagetype($image);
+        } else {
+            try {
+                $info = getimagesize($image);
+                return $info ? $info[2] : false;
+            } catch (\Exception $e) {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * 验证是否为合格的域名或者IP 支持A,MX,NS,SOA,PTR,CNAME,AAAA,A6, SRV,NAPTR,TXT 或者 ANY类型
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function activeUrl($value, $rule)
+    {
+        if (!in_array($rule, ['A', 'MX', 'NS', 'SOA', 'PTR', 'CNAME', 'AAAA', 'A6', 'SRV', 'NAPTR', 'TXT', 'ANY'])) {
+            $rule = 'MX';
+        }
+        return checkdnsrr($value, $rule);
+    }
+
+    /**
+     * 验证是否有效IP
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则 ipv4 ipv6
+     * @return bool
+     */
+    protected function ip($value, $rule)
+    {
+        if (!in_array($rule, ['ipv4', 'ipv6'])) {
+            $rule = 'ipv4';
+        }
+        return $this->filter($value, [FILTER_VALIDATE_IP, 'ipv6' == $rule ? FILTER_FLAG_IPV6 : FILTER_FLAG_IPV4]);
+    }
+
+    /**
+     * 验证上传文件后缀
+     * @access protected
+     * @param mixed     $file  上传文件
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function fileExt($file, $rule)
+    {
+        if (is_array($file)) {
+            foreach ($file as $item) {
+                if (!($item instanceof File) || !$item->checkExt($rule)) {
+                    return false;
+                }
+            }
+            return true;
+        } elseif ($file instanceof File) {
+            return $file->checkExt($rule);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 验证上传文件类型
+     * @access protected
+     * @param mixed     $file  上传文件
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function fileMime($file, $rule)
+    {
+        if (is_array($file)) {
+            foreach ($file as $item) {
+                if (!($item instanceof File) || !$item->checkMime($rule)) {
+                    return false;
+                }
+            }
+            return true;
+        } elseif ($file instanceof File) {
+            return $file->checkMime($rule);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 验证上传文件大小
+     * @access protected
+     * @param mixed     $file  上传文件
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function fileSize($file, $rule)
+    {
+        if (is_array($file)) {
+            foreach ($file as $item) {
+                if (!($item instanceof File) || !$item->checkSize($rule)) {
+                    return false;
+                }
+            }
+            return true;
+        } elseif ($file instanceof File) {
+            return $file->checkSize($rule);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 验证图片的宽高及类型
+     * @access protected
+     * @param mixed     $file  上传文件
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function image($file, $rule)
+    {
+        if (!($file instanceof File)) {
+            return false;
+        }
+        if ($rule) {
+            $rule                        = explode(',', $rule);
+            list($width, $height, $type) = getimagesize($file->getRealPath());
+            if (isset($rule[2])) {
+                $imageType = strtolower($rule[2]);
+                if ('jpeg' == $imageType) {
+                    $imageType = 'jpg';
+                }
+                if (image_type_to_extension($type, false) != $imageType) {
+                    return false;
+                }
+            }
+
+            list($w, $h) = $rule;
+            return $w == $width && $h == $height;
+        } else {
+            return in_array($this->getImageType($file->getRealPath()), [1, 2, 3, 6]);
+        }
+    }
+
+    /**
+     * 验证请求类型
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function method($value, $rule)
+    {
+        $method = Request::instance()->method();
+        return strtoupper($rule) == $method;
+    }
+
+    /**
+     * 验证时间和日期是否符合指定格式
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function dateFormat($value, $rule)
+    {
+        $info = date_parse_from_format($rule, $value);
+        return 0 == $info['warning_count'] && 0 == $info['error_count'];
+    }
+
+    /**
+     * 验证是否唯一
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则 格式:数据表,字段名,排除ID,主键名
+     * @param array     $data  数据
+     * @param string    $field  验证字段名
+     * @return bool
+     */
+    protected function unique($value, $rule, $data, $field)
+    {
+        if (is_string($rule)) {
+            $rule = explode(',', $rule);
+        }
+        if (false !== strpos($rule[0], '\\')) {
+            // 指定模型类
+            $db = new $rule[0];
+        } else {
+            try {
+                $db = Loader::model($rule[0]);
+            } catch (ClassNotFoundException $e) {
+                $db = Db::name($rule[0]);
+            }
+        }
+        $key = isset($rule[1]) ? $rule[1] : $field;
+
+        if (strpos($key, '^')) {
+            // 支持多个字段验证
+            $fields = explode('^', $key);
+            foreach ($fields as $key) {
+                $map[$key] = $data[$key];
+            }
+        } elseif (strpos($key, '=')) {
+            parse_str($key, $map);
+        } else {
+            $map[$key] = $data[$field];
+        }
+
+        $pk = isset($rule[3]) ? $rule[3] : $db->getPk();
+        if (is_string($pk)) {
+            if (isset($rule[2])) {
+                $map[$pk] = ['neq', $rule[2]];
+            } elseif (isset($data[$pk])) {
+                $map[$pk] = ['neq', $data[$pk]];
+            }
+        }
+        if ($db->where($map)->field($pk)->find()) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 使用行为类验证
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @param array     $data  数据
+     * @return mixed
+     */
+    protected function behavior($value, $rule, $data)
+    {
+        return Hook::exec($rule, '', $data);
+    }
+
+    /**
+     * 使用filter_var方式验证
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function filter($value, $rule)
+    {
+        if (is_string($rule) && strpos($rule, ',')) {
+            list($rule, $param) = explode(',', $rule);
+        } elseif (is_array($rule)) {
+            $param = isset($rule[1]) ? $rule[1] : null;
+            $rule  = $rule[0];
+        } else {
+            $param = null;
+        }
+        return false !== filter_var($value, is_int($rule) ? $rule : filter_id($rule), $param);
+    }
+
+    /**
+     * 验证某个字段等于某个值的时候必须
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @param array     $data  数据
+     * @return bool
+     */
+    protected function requireIf($value, $rule, $data)
+    {
+        list($field, $val) = explode(',', $rule);
+        if ($this->getDataValue($data, $field) == $val) {
+            return !empty($value) || '0' == $value;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 通过回调方法验证某个字段是否必须
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @param array     $data  数据
+     * @return bool
+     */
+    protected function requireCallback($value, $rule, $data)
+    {
+        $result = call_user_func_array($rule, [$value, $data]);
+        if ($result) {
+            return !empty($value) || '0' == $value;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 验证某个字段有值的情况下必须
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @param array     $data  数据
+     * @return bool
+     */
+    protected function requireWith($value, $rule, $data)
+    {
+        $val = $this->getDataValue($data, $rule);
+        if (!empty($val)) {
+            return !empty($value) || '0' == $value;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 验证是否在范围内
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function in($value, $rule)
+    {
+        return in_array($value, is_array($rule) ? $rule : explode(',', $rule));
+    }
+
+    /**
+     * 验证是否不在某个范围
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function notIn($value, $rule)
+    {
+        return !in_array($value, is_array($rule) ? $rule : explode(',', $rule));
+    }
+
+    /**
+     * between验证数据
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function between($value, $rule)
+    {
+        if (is_string($rule)) {
+            $rule = explode(',', $rule);
+        }
+        list($min, $max) = $rule;
+        return $value >= $min && $value <= $max;
+    }
+
+    /**
+     * 使用notbetween验证数据
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function notBetween($value, $rule)
+    {
+        if (is_string($rule)) {
+            $rule = explode(',', $rule);
+        }
+        list($min, $max) = $rule;
+        return $value < $min || $value > $max;
+    }
+
+    /**
+     * 验证数据长度
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function length($value, $rule)
+    {
+        if (is_array($value)) {
+            $length = count($value);
+        } elseif ($value instanceof File) {
+            $length = $value->getSize();
+        } else {
+            $length = mb_strlen((string) $value);
+        }
+
+        if (strpos($rule, ',')) {
+            // 长度区间
+            list($min, $max) = explode(',', $rule);
+            return $length >= $min && $length <= $max;
+        } else {
+            // 指定长度
+            return $length == $rule;
+        }
+    }
+
+    /**
+     * 验证数据最大长度
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function max($value, $rule)
+    {
+        if (is_array($value)) {
+            $length = count($value);
+        } elseif ($value instanceof File) {
+            $length = $value->getSize();
+        } else {
+            $length = mb_strlen((string) $value);
+        }
+        return $length <= $rule;
+    }
+
+    /**
+     * 验证数据最小长度
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function min($value, $rule)
+    {
+        if (is_array($value)) {
+            $length = count($value);
+        } elseif ($value instanceof File) {
+            $length = $value->getSize();
+        } else {
+            $length = mb_strlen((string) $value);
+        }
+        return $length >= $rule;
+    }
+
+    /**
+     * 验证日期
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function after($value, $rule)
+    {
+        return strtotime($value) >= strtotime($rule);
+    }
+
+    /**
+     * 验证日期
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function before($value, $rule)
+    {
+        return strtotime($value) <= strtotime($rule);
+    }
+
+    /**
+     * 验证有效期
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return bool
+     */
+    protected function expire($value, $rule)
+    {
+        if (is_string($rule)) {
+            $rule = explode(',', $rule);
+        }
+        list($start, $end) = $rule;
+        if (!is_numeric($start)) {
+            $start = strtotime($start);
+        }
+
+        if (!is_numeric($end)) {
+            $end = strtotime($end);
+        }
+        return $_SERVER['REQUEST_TIME'] >= $start && $_SERVER['REQUEST_TIME'] <= $end;
+    }
+
+    /**
+     * 验证IP许可
+     * @access protected
+     * @param string    $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return mixed
+     */
+    protected function allowIp($value, $rule)
+    {
+        return in_array($_SERVER['REMOTE_ADDR'], is_array($rule) ? $rule : explode(',', $rule));
+    }
+
+    /**
+     * 验证IP禁用
+     * @access protected
+     * @param string    $value  字段值
+     * @param mixed     $rule  验证规则
+     * @return mixed
+     */
+    protected function denyIp($value, $rule)
+    {
+        return !in_array($_SERVER['REMOTE_ADDR'], is_array($rule) ? $rule : explode(',', $rule));
+    }
+
+    /**
+     * 使用正则验证数据
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则 正则规则或者预定义正则名
+     * @return mixed
+     */
+    protected function regex($value, $rule)
+    {
+        if (isset($this->regex[$rule])) {
+            $rule = $this->regex[$rule];
+        }
+        if (0 !== strpos($rule, '/') && !preg_match('/\/[imsU]{0,4}$/', $rule)) {
+            // 不是正则表达式则两端补上/
+            $rule = '/^' . $rule . '$/';
+        }
+        return is_scalar($value) && 1 === preg_match($rule, (string) $value);
+    }
+
+    /**
+     * 验证表单令牌
+     * @access protected
+     * @param mixed     $value  字段值
+     * @param mixed     $rule  验证规则
+     * @param array     $data  数据
+     * @return bool
+     */
+    protected function token($value, $rule, $data)
+    {
+        $rule = !empty($rule) ? $rule : '__token__';
+        if (!isset($data[$rule]) || !Session::has($rule)) {
+            // 令牌数据无效
+            return false;
+        }
+
+        // 令牌验证
+        if (isset($data[$rule]) && Session::get($rule) === $data[$rule]) {
+            // 防止重复提交
+            Session::delete($rule); // 验证完成销毁session
+            return true;
+        }
+        // 开启TOKEN重置
+        Session::delete($rule);
+        return false;
+    }
+
+    // 获取错误信息
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    /**
+     * 获取数据值
+     * @access protected
+     * @param array $data 数据
+     * @param string $key 数据标识 支持二维
+     * @return mixed
+     */
+    protected function getDataValue($data, $key)
+    {
+        if (is_numeric($key)) {
+            $value = $key;
+        } elseif (strpos($key, '.')) {
+            // 支持二维数组验证
+            list($name1, $name2) = explode('.', $key);
+            $value               = isset($data[$name1][$name2]) ? $data[$name1][$name2] : null;
+        } else {
+            $value = isset($data[$key]) ? $data[$key] : null;
+        }
+        return $value;
+    }
+
+    /**
+     * 获取验证规则的错误提示信息
+     * @access protected
+     * @param string    $attribute  字段英文名
+     * @param string    $title  字段描述名
+     * @param string    $type  验证规则名称
+     * @param mixed     $rule  验证规则数据
+     * @return string
+     */
+    protected function getRuleMsg($attribute, $title, $type, $rule)
+    {
+        if (isset($this->message[$attribute . '.' . $type])) {
+            $msg = $this->message[$attribute . '.' . $type];
+        } elseif (isset($this->message[$attribute][$type])) {
+            $msg = $this->message[$attribute][$type];
+        } elseif (isset($this->message[$attribute])) {
+            $msg = $this->message[$attribute];
+        } elseif (isset(self::$typeMsg[$type])) {
+            $msg = self::$typeMsg[$type];
+        } elseif (0 === strpos($type, 'require')) {
+            $msg = self::$typeMsg['require'];
+        } else {
+            $msg = $title . Lang::get('not conform to the rules');
+        }
+
+        if (is_string($msg) && 0 === strpos($msg, '{%')) {
+            $msg = Lang::get(substr($msg, 2, -1));
+        } elseif (Lang::has($msg)) {
+            $msg = Lang::get($msg);
+        }
+
+        if (is_string($msg) && is_scalar($rule) && false !== strpos($msg, ':')) {
+            // 变量替换
+            if (is_string($rule) && strpos($rule, ',')) {
+                $array = array_pad(explode(',', $rule), 3, '');
+            } else {
+                $array = array_pad([], 3, '');
+            }
+            $msg = str_replace(
+                [':attribute', ':rule', ':1', ':2', ':3'],
+                [$title, (string) $rule, $array[0], $array[1], $array[2]],
+                $msg);
+        }
+        return $msg;
+    }
+
+    /**
+     * 获取数据验证的场景
+     * @access protected
+     * @param string $scene  验证场景
+     * @return array
+     */
+    protected function getScene($scene = '')
+    {
+        if (empty($scene)) {
+            // 读取指定场景
+            $scene = $this->currentScene;
+        }
+
+        if (!empty($scene) && isset($this->scene[$scene])) {
+            // 如果设置了验证适用场景
+            $scene = $this->scene[$scene];
+            if (is_string($scene)) {
+                $scene = explode(',', $scene);
+            }
+        } else {
+            $scene = [];
+        }
+        return $scene;
+    }
+
+    public static function __callStatic($method, $params)
+    {
+        $class = self::make();
+        if (method_exists($class, $method)) {
+            return call_user_func_array([$class, $method], $params);
+        } else {
+            throw new \BadMethodCallException('method not exists:' . __CLASS__ . '->' . $method);
+        }
+    }
+}

+ 239 - 0
thinkphp/library/think/View.php

@@ -0,0 +1,239 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think;
+
+class View
+{
+    // 视图实例
+    protected static $instance;
+    // 模板引擎实例
+    public $engine;
+    // 模板变量
+    protected $data = [];
+    // 用于静态赋值的模板变量
+    protected static $var = [];
+    // 视图输出替换
+    protected $replace = [];
+
+    /**
+     * 构造函数
+     * @access public
+     * @param array $engine  模板引擎参数
+     * @param array $replace  字符串替换参数
+     */
+    public function __construct($engine = [], $replace = [])
+    {
+        // 初始化模板引擎
+        $this->engine($engine);
+        // 基础替换字符串
+        $request = Request::instance();
+        $base    = $request->root();
+        $root    = strpos($base, '.') ? ltrim(dirname($base), DS) : $base;
+        if ('' != $root) {
+            $root = '/' . ltrim($root, '/');
+        }
+        $baseReplace = [
+            '__ROOT__'   => $root,
+            '__URL__'    => $base . '/' . $request->module() . '/' . Loader::parseName($request->controller()),
+            '__STATIC__' => $root . '/static',
+            '__CSS__'    => $root . '/static/css',
+            '__JS__'     => $root . '/static/js',
+        ];
+        $this->replace = array_merge($baseReplace, (array) $replace);
+    }
+
+    /**
+     * 初始化视图
+     * @access public
+     * @param array $engine  模板引擎参数
+     * @param array $replace  字符串替换参数
+     * @return object
+     */
+    public static function instance($engine = [], $replace = [])
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new self($engine, $replace);
+        }
+        return self::$instance;
+    }
+
+    /**
+     * 模板变量静态赋值
+     * @access public
+     * @param mixed $name  变量名
+     * @param mixed $value 变量值
+     * @return void
+     */
+    public static function share($name, $value = '')
+    {
+        if (is_array($name)) {
+            self::$var = array_merge(self::$var, $name);
+        } else {
+            self::$var[$name] = $value;
+        }
+    }
+
+    /**
+     * 模板变量赋值
+     * @access public
+     * @param mixed $name  变量名
+     * @param mixed $value 变量值
+     * @return $this
+     */
+    public function assign($name, $value = '')
+    {
+        if (is_array($name)) {
+            $this->data = array_merge($this->data, $name);
+        } else {
+            $this->data[$name] = $value;
+        }
+        return $this;
+    }
+
+    /**
+     * 设置当前模板解析的引擎
+     * @access public
+     * @param array|string $options 引擎参数
+     * @return $this
+     */
+    public function engine($options = [])
+    {
+        if (is_string($options)) {
+            $type    = $options;
+            $options = [];
+        } else {
+            $type = !empty($options['type']) ? $options['type'] : 'Think';
+        }
+
+        $class = false !== strpos($type, '\\') ? $type : '\\think\\view\\driver\\' . ucfirst($type);
+        if (isset($options['type'])) {
+            unset($options['type']);
+        }
+        $this->engine = new $class($options);
+        return $this;
+    }
+
+    /**
+     * 配置模板引擎
+     * @access private
+     * @param string|array  $name 参数名
+     * @param mixed         $value 参数值
+     * @return $this
+     */
+    public function config($name, $value = null)
+    {
+        $this->engine->config($name, $value);
+        return $this;
+    }
+
+    /**
+     * 解析和获取模板内容 用于输出
+     * @param string    $template 模板文件名或者内容
+     * @param array     $vars     模板输出变量
+     * @param array     $replace 替换内容
+     * @param array     $config     模板参数
+     * @param bool      $renderContent     是否渲染内容
+     * @return string
+     * @throws Exception
+     */
+    public function fetch($template = '', $vars = [], $replace = [], $config = [], $renderContent = false)
+    {
+        // 模板变量
+        $vars = array_merge(self::$var, $this->data, $vars);
+
+        // 页面缓存
+        ob_start();
+        ob_implicit_flush(0);
+
+        // 渲染输出
+        try {
+            $method = $renderContent ? 'display' : 'fetch';
+            // 允许用户自定义模板的字符串替换
+            $replace = array_merge($this->replace, $replace, (array) $this->engine->config('tpl_replace_string'));
+            $this->engine->config('tpl_replace_string', $replace);
+            $this->engine->$method($template, $vars, $config);
+        } catch (\Exception $e) {
+            ob_end_clean();
+            throw $e;
+        }
+
+        // 获取并清空缓存
+        $content = ob_get_clean();
+        // 内容过滤标签
+        Hook::listen('view_filter', $content);
+        return $content;
+    }
+
+    /**
+     * 视图内容替换
+     * @access public
+     * @param string|array  $content 被替换内容(支持批量替换)
+     * @param string        $replace    替换内容
+     * @return $this
+     */
+    public function replace($content, $replace = '')
+    {
+        if (is_array($content)) {
+            $this->replace = array_merge($this->replace, $content);
+        } else {
+            $this->replace[$content] = $replace;
+        }
+        return $this;
+    }
+
+    /**
+     * 渲染内容输出
+     * @access public
+     * @param string $content 内容
+     * @param array  $vars    模板输出变量
+     * @param array  $replace 替换内容
+     * @param array  $config     模板参数
+     * @return mixed
+     */
+    public function display($content, $vars = [], $replace = [], $config = [])
+    {
+        return $this->fetch($content, $vars, $replace, $config, true);
+    }
+
+    /**
+     * 模板变量赋值
+     * @access public
+     * @param string    $name  变量名
+     * @param mixed     $value 变量值
+     */
+    public function __set($name, $value)
+    {
+        $this->data[$name] = $value;
+    }
+
+    /**
+     * 取得模板显示变量的值
+     * @access protected
+     * @param string $name 模板变量
+     * @return mixed
+     */
+    public function __get($name)
+    {
+        return $this->data[$name];
+    }
+
+    /**
+     * 检测模板变量是否设置
+     * @access public
+     * @param string $name 模板变量名
+     * @return bool
+     */
+    public function __isset($name)
+    {
+        return isset($this->data[$name]);
+    }
+}

+ 231 - 0
thinkphp/library/think/cache/Driver.php

@@ -0,0 +1,231 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\cache;
+
+/**
+ * 缓存基础类
+ */
+abstract class Driver
+{
+    protected $handler = null;
+    protected $options = [];
+    protected $tag;
+
+    /**
+     * 判断缓存是否存在
+     * @access public
+     * @param string $name 缓存变量名
+     * @return bool
+     */
+    abstract public function has($name);
+
+    /**
+     * 读取缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @param mixed  $default 默认值
+     * @return mixed
+     */
+    abstract public function get($name, $default = false);
+
+    /**
+     * 写入缓存
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param mixed     $value  存储数据
+     * @param int       $expire  有效时间 0为永久
+     * @return boolean
+     */
+    abstract public function set($name, $value, $expire = null);
+
+    /**
+     * 自增缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    abstract public function inc($name, $step = 1);
+
+    /**
+     * 自减缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    abstract public function dec($name, $step = 1);
+
+    /**
+     * 删除缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return boolean
+     */
+    abstract public function rm($name);
+
+    /**
+     * 清除缓存
+     * @access public
+     * @param string $tag 标签名
+     * @return boolean
+     */
+    abstract public function clear($tag = null);
+
+    /**
+     * 获取实际的缓存标识
+     * @access public
+     * @param string $name 缓存名
+     * @return string
+     */
+    protected function getCacheKey($name)
+    {
+        return $this->options['prefix'] . $name;
+    }
+
+    /**
+     * 读取缓存并删除
+     * @access public
+     * @param string $name 缓存变量名
+     * @return mixed
+     */
+    public function pull($name)
+    {
+        $result = $this->get($name, false);
+        if ($result) {
+            $this->rm($name);
+            return $result;
+        } else {
+            return;
+        }
+    }
+
+    /**
+     * 如果不存在则写入缓存
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param mixed     $value  存储数据
+     * @param int       $expire  有效时间 0为永久
+     * @return mixed
+     */
+    public function remember($name, $value, $expire = null)
+    {
+        if (!$this->has($name)) {
+            $time = time();
+            while ($time + 5 > time() && $this->has($name . '_lock')) {
+                // 存在锁定则等待
+                usleep(200000);
+            }
+
+            try {
+                // 锁定
+                $this->set($name . '_lock', true);
+                if ($value instanceof \Closure) {
+                    $value = call_user_func($value);
+                }
+                $this->set($name, $value, $expire);
+                // 解锁
+                $this->rm($name . '_lock');
+            } catch (\Exception $e) {
+                // 解锁
+                $this->rm($name . '_lock');
+                throw $e;
+            } catch (\throwable $e) {
+                $this->rm($name . '_lock');
+                throw $e;
+            }
+        } else {
+            $value = $this->get($name);
+        }
+        return $value;
+    }
+
+    /**
+     * 缓存标签
+     * @access public
+     * @param string        $name 标签名
+     * @param string|array  $keys 缓存标识
+     * @param bool          $overlay 是否覆盖
+     * @return $this
+     */
+    public function tag($name, $keys = null, $overlay = false)
+    {
+        if (is_null($name)) {
+
+        } elseif (is_null($keys)) {
+            $this->tag = $name;
+        } else {
+            $key = 'tag_' . md5($name);
+            if (is_string($keys)) {
+                $keys = explode(',', $keys);
+            }
+            $keys = array_map([$this, 'getCacheKey'], $keys);
+            if ($overlay) {
+                $value = $keys;
+            } else {
+                $value = array_unique(array_merge($this->getTagItem($name), $keys));
+            }
+            $this->set($key, implode(',', $value), 0);
+        }
+        return $this;
+    }
+
+    /**
+     * 更新标签
+     * @access public
+     * @param string $name 缓存标识
+     * @return void
+     */
+    protected function setTagItem($name)
+    {
+        if ($this->tag) {
+            $key       = 'tag_' . md5($this->tag);
+            $this->tag = null;
+            if ($this->has($key)) {
+                $value   = explode(',', $this->get($key));
+                $value[] = $name;
+                $value   = implode(',', array_unique($value));
+            } else {
+                $value = $name;
+            }
+            $this->set($key, $value, 0);
+        }
+    }
+
+    /**
+     * 获取标签包含的缓存标识
+     * @access public
+     * @param string $tag 缓存标签
+     * @return array
+     */
+    protected function getTagItem($tag)
+    {
+        $key   = 'tag_' . md5($tag);
+        $value = $this->get($key);
+        if ($value) {
+            return array_filter(explode(',', $value));
+        } else {
+            return [];
+        }
+    }
+
+    /**
+     * 返回句柄对象,可执行其它高级方法
+     *
+     * @access public
+     * @return object
+     */
+    public function handler()
+    {
+        return $this->handler;
+    }
+}

+ 268 - 0
thinkphp/library/think/cache/driver/File.php

@@ -0,0 +1,268 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+/**
+ * 文件类型缓存类
+ * @author    liu21st <liu21st@gmail.com>
+ */
+class File extends Driver
+{
+    protected $options = [
+        'expire'        => 0,
+        'cache_subdir'  => true,
+        'prefix'        => '',
+        'path'          => CACHE_PATH,
+        'data_compress' => false,
+    ];
+
+    protected $expire;
+
+    /**
+     * 构造函数
+     * @param array $options
+     */
+    public function __construct($options = [])
+    {
+        if (!empty($options)) {
+            $this->options = array_merge($this->options, $options);
+        }
+        if (substr($this->options['path'], -1) != DS) {
+            $this->options['path'] .= DS;
+        }
+        $this->init();
+    }
+
+    /**
+     * 初始化检查
+     * @access private
+     * @return boolean
+     */
+    private function init()
+    {
+        // 创建项目缓存目录
+        if (!is_dir($this->options['path'])) {
+            if (mkdir($this->options['path'], 0755, true)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 取得变量的存储文件名
+     * @access protected
+     * @param  string $name 缓存变量名
+     * @param  bool   $auto 是否自动创建目录
+     * @return string
+     */
+    protected function getCacheKey($name, $auto = false)
+    {
+        $name = md5($name);
+        if ($this->options['cache_subdir']) {
+            // 使用子目录
+            $name = substr($name, 0, 2) . DS . substr($name, 2);
+        }
+        if ($this->options['prefix']) {
+            $name = $this->options['prefix'] . DS . $name;
+        }
+        $filename = $this->options['path'] . $name . '.php';
+        $dir      = dirname($filename);
+
+        if ($auto && !is_dir($dir)) {
+            mkdir($dir, 0755, true);
+        }
+        return $filename;
+    }
+
+    /**
+     * 判断缓存是否存在
+     * @access public
+     * @param string $name 缓存变量名
+     * @return bool
+     */
+    public function has($name)
+    {
+        return $this->get($name) ? true : false;
+    }
+
+    /**
+     * 读取缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @param mixed  $default 默认值
+     * @return mixed
+     */
+    public function get($name, $default = false)
+    {
+        $filename = $this->getCacheKey($name);
+        if (!is_file($filename)) {
+            return $default;
+        }
+        $content      = file_get_contents($filename);
+        $this->expire = null;
+        if (false !== $content) {
+            $expire = (int) substr($content, 8, 12);
+            if (0 != $expire && time() > filemtime($filename) + $expire) {
+                return $default;
+            }
+            $this->expire = $expire;
+            $content      = substr($content, 32);
+            if ($this->options['data_compress'] && function_exists('gzcompress')) {
+                //启用数据压缩
+                $content = gzuncompress($content);
+            }
+            $content = unserialize($content);
+            return $content;
+        } else {
+            return $default;
+        }
+    }
+
+    /**
+     * 写入缓存
+     * @access public
+     * @param string            $name 缓存变量名
+     * @param mixed             $value  存储数据
+     * @param integer|\DateTime $expire  有效时间(秒)
+     * @return boolean
+     */
+    public function set($name, $value, $expire = null)
+    {
+        if (is_null($expire)) {
+            $expire = $this->options['expire'];
+        }
+        if ($expire instanceof \DateTime) {
+            $expire = $expire->getTimestamp() - time();
+        }
+        $filename = $this->getCacheKey($name, true);
+        if ($this->tag && !is_file($filename)) {
+            $first = true;
+        }
+        $data = serialize($value);
+        if ($this->options['data_compress'] && function_exists('gzcompress')) {
+            //数据压缩
+            $data = gzcompress($data, 3);
+        }
+        $data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
+        $result = file_put_contents($filename, $data);
+        if ($result) {
+            isset($first) && $this->setTagItem($filename);
+            clearstatcache();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 自增缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function inc($name, $step = 1)
+    {
+        if ($this->has($name)) {
+            $value  = $this->get($name) + $step;
+            $expire = $this->expire;
+        } else {
+            $value  = $step;
+            $expire = 0;
+        }
+
+        return $this->set($name, $value, $expire) ? $value : false;
+    }
+
+    /**
+     * 自减缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function dec($name, $step = 1)
+    {
+        if ($this->has($name)) {
+            $value  = $this->get($name) - $step;
+            $expire = $this->expire;
+        } else {
+            $value  = -$step;
+            $expire = 0;
+        }
+
+        return $this->set($name, $value, $expire) ? $value : false;
+    }
+
+    /**
+     * 删除缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return boolean
+     */
+    public function rm($name)
+    {
+        $filename = $this->getCacheKey($name);
+        try {
+            return $this->unlink($filename);
+        } catch (\Exception $e) {
+        }
+    }
+
+    /**
+     * 清除缓存
+     * @access public
+     * @param string $tag 标签名
+     * @return boolean
+     */
+    public function clear($tag = null)
+    {
+        if ($tag) {
+            // 指定标签清除
+            $keys = $this->getTagItem($tag);
+            foreach ($keys as $key) {
+                $this->unlink($key);
+            }
+            $this->rm('tag_' . md5($tag));
+            return true;
+        }
+        $files = (array) glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DS : '') . '*');
+        foreach ($files as $path) {
+            if (is_dir($path)) {
+                $matches = glob($path . '/*.php');
+                if (is_array($matches)) {
+                    array_map('unlink', $matches);
+                }
+                rmdir($path);
+            } else {
+                unlink($path);
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 判断文件是否存在后,删除
+     * @param $path
+     * @return bool
+     * @author byron sampson <xiaobo.sun@qq.com>
+     * @return boolean
+     */
+    private function unlink($path)
+    {
+        return is_file($path) && unlink($path);
+    }
+
+}

+ 187 - 0
thinkphp/library/think/cache/driver/Lite.php

@@ -0,0 +1,187 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+/**
+ * 文件类型缓存类
+ * @author    liu21st <liu21st@gmail.com>
+ */
+class Lite extends Driver
+{
+    protected $options = [
+        'prefix' => '',
+        'path'   => '',
+        'expire' => 0, // 等于 10*365*24*3600(10年)
+    ];
+
+    /**
+     * 构造函数
+     * @access public
+     *
+     * @param array $options
+     */
+    public function __construct($options = [])
+    {
+        if (!empty($options)) {
+            $this->options = array_merge($this->options, $options);
+        }
+        if (substr($this->options['path'], -1) != DS) {
+            $this->options['path'] .= DS;
+        }
+
+    }
+
+    /**
+     * 取得变量的存储文件名
+     * @access protected
+     * @param string $name 缓存变量名
+     * @return string
+     */
+    protected function getCacheKey($name)
+    {
+        return $this->options['path'] . $this->options['prefix'] . md5($name) . '.php';
+    }
+
+    /**
+     * 判断缓存是否存在
+     * @access public
+     * @param string $name 缓存变量名
+     * @return mixed
+     */
+    public function has($name)
+    {
+        return $this->get($name) ? true : false;
+    }
+
+    /**
+     * 读取缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @param mixed  $default 默认值
+     * @return mixed
+     */
+    public function get($name, $default = false)
+    {
+        $filename = $this->getCacheKey($name);
+        if (is_file($filename)) {
+            // 判断是否过期
+            $mtime = filemtime($filename);
+            if ($mtime < time()) {
+                // 清除已经过期的文件
+                unlink($filename);
+                return $default;
+            }
+            return include $filename;
+        } else {
+            return $default;
+        }
+    }
+
+    /**
+     * 写入缓存
+     * @access   public
+     * @param string            $name 缓存变量名
+     * @param mixed             $value  存储数据
+     * @param integer|\DateTime $expire  有效时间(秒)
+     * @return bool
+     */
+    public function set($name, $value, $expire = null)
+    {
+        if (is_null($expire)) {
+            $expire = $this->options['expire'];
+        }
+        if ($expire instanceof \DateTime) {
+            $expire = $expire->getTimestamp();
+        } else {
+            $expire = 0 === $expire ? 10 * 365 * 24 * 3600 : $expire;
+            $expire = time() + $expire;
+        }
+        $filename = $this->getCacheKey($name);
+        if ($this->tag && !is_file($filename)) {
+            $first = true;
+        }
+        $ret = file_put_contents($filename, ("<?php return " . var_export($value, true) . ";"));
+        // 通过设置修改时间实现有效期
+        if ($ret) {
+            isset($first) && $this->setTagItem($filename);
+            touch($filename, $expire);
+        }
+        return $ret;
+    }
+
+    /**
+     * 自增缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function inc($name, $step = 1)
+    {
+        if ($this->has($name)) {
+            $value = $this->get($name) + $step;
+        } else {
+            $value = $step;
+        }
+        return $this->set($name, $value, 0) ? $value : false;
+    }
+
+    /**
+     * 自减缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function dec($name, $step = 1)
+    {
+        if ($this->has($name)) {
+            $value = $this->get($name) - $step;
+        } else {
+            $value = -$step;
+        }
+        return $this->set($name, $value, 0) ? $value : false;
+    }
+
+    /**
+     * 删除缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return boolean
+     */
+    public function rm($name)
+    {
+        return unlink($this->getCacheKey($name));
+    }
+
+    /**
+     * 清除缓存
+     * @access   public
+     * @param string $tag 标签名
+     * @return bool
+     */
+    public function clear($tag = null)
+    {
+        if ($tag) {
+            // 指定标签清除
+            $keys = $this->getTagItem($tag);
+            foreach ($keys as $key) {
+                unlink($key);
+            }
+            $this->rm('tag_' . md5($tag));
+            return true;
+        }
+        array_map("unlink", glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DS : '') . '*.php'));
+    }
+}

+ 177 - 0
thinkphp/library/think/cache/driver/Memcache.php

@@ -0,0 +1,177 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+class Memcache extends Driver
+{
+    protected $options = [
+        'host'       => '127.0.0.1',
+        'port'       => 11211,
+        'expire'     => 0,
+        'timeout'    => 0, // 超时时间(单位:毫秒)
+        'persistent' => true,
+        'prefix'     => '',
+    ];
+
+    /**
+     * 构造函数
+     * @param array $options 缓存参数
+     * @access public
+     * @throws \BadFunctionCallException
+     */
+    public function __construct($options = [])
+    {
+        if (!extension_loaded('memcache')) {
+            throw new \BadFunctionCallException('not support: memcache');
+        }
+        if (!empty($options)) {
+            $this->options = array_merge($this->options, $options);
+        }
+        $this->handler = new \Memcache;
+        // 支持集群
+        $hosts = explode(',', $this->options['host']);
+        $ports = explode(',', $this->options['port']);
+        if (empty($ports[0])) {
+            $ports[0] = 11211;
+        }
+        // 建立连接
+        foreach ((array) $hosts as $i => $host) {
+            $port = isset($ports[$i]) ? $ports[$i] : $ports[0];
+            $this->options['timeout'] > 0 ?
+            $this->handler->addServer($host, $port, $this->options['persistent'], 1, $this->options['timeout']) :
+            $this->handler->addServer($host, $port, $this->options['persistent'], 1);
+        }
+    }
+
+    /**
+     * 判断缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return bool
+     */
+    public function has($name)
+    {
+        $key = $this->getCacheKey($name);
+        return false !== $this->handler->get($key);
+    }
+
+    /**
+     * 读取缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @param mixed  $default 默认值
+     * @return mixed
+     */
+    public function get($name, $default = false)
+    {
+        $result = $this->handler->get($this->getCacheKey($name));
+        return false !== $result ? $result : $default;
+    }
+
+    /**
+     * 写入缓存
+     * @access public
+     * @param string            $name 缓存变量名
+     * @param mixed             $value  存储数据
+     * @param integer|\DateTime $expire  有效时间(秒)
+     * @return bool
+     */
+    public function set($name, $value, $expire = null)
+    {
+        if (is_null($expire)) {
+            $expire = $this->options['expire'];
+        }
+        if ($expire instanceof \DateTime) {
+            $expire = $expire->getTimestamp() - time();
+        }
+        if ($this->tag && !$this->has($name)) {
+            $first = true;
+        }
+        $key = $this->getCacheKey($name);
+        if ($this->handler->set($key, $value, 0, $expire)) {
+            isset($first) && $this->setTagItem($key);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 自增缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function inc($name, $step = 1)
+    {
+        $key = $this->getCacheKey($name);
+        if ($this->handler->get($key)) {
+            return $this->handler->increment($key, $step);
+        }
+        return $this->handler->set($key, $step);
+    }
+
+    /**
+     * 自减缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function dec($name, $step = 1)
+    {
+        $key   = $this->getCacheKey($name);
+        $value = $this->handler->get($key) - $step;
+        $res   = $this->handler->set($key, $value);
+        if (!$res) {
+            return false;
+        } else {
+            return $value;
+        }
+    }
+
+    /**
+     * 删除缓存
+     * @param    string  $name 缓存变量名
+     * @param bool|false $ttl
+     * @return bool
+     */
+    public function rm($name, $ttl = false)
+    {
+        $key = $this->getCacheKey($name);
+        return false === $ttl ?
+        $this->handler->delete($key) :
+        $this->handler->delete($key, $ttl);
+    }
+
+    /**
+     * 清除缓存
+     * @access public
+     * @param string $tag 标签名
+     * @return bool
+     */
+    public function clear($tag = null)
+    {
+        if ($tag) {
+            // 指定标签清除
+            $keys = $this->getTagItem($tag);
+            foreach ($keys as $key) {
+                $this->handler->delete($key);
+            }
+            $this->rm('tag_' . md5($tag));
+            return true;
+        }
+        return $this->handler->flush();
+    }
+}

+ 187 - 0
thinkphp/library/think/cache/driver/Memcached.php

@@ -0,0 +1,187 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+class Memcached extends Driver
+{
+    protected $options = [
+        'host'     => '127.0.0.1',
+        'port'     => 11211,
+        'expire'   => 0,
+        'timeout'  => 0, // 超时时间(单位:毫秒)
+        'prefix'   => '',
+        'username' => '', //账号
+        'password' => '', //密码
+        'option'   => [],
+    ];
+
+    /**
+     * 构造函数
+     * @param array $options 缓存参数
+     * @access public
+     */
+    public function __construct($options = [])
+    {
+        if (!extension_loaded('memcached')) {
+            throw new \BadFunctionCallException('not support: memcached');
+        }
+        if (!empty($options)) {
+            $this->options = array_merge($this->options, $options);
+        }
+        $this->handler = new \Memcached;
+        if (!empty($this->options['option'])) {
+            $this->handler->setOptions($this->options['option']);
+        }
+        // 设置连接超时时间(单位:毫秒)
+        if ($this->options['timeout'] > 0) {
+            $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->options['timeout']);
+        }
+        // 支持集群
+        $hosts = explode(',', $this->options['host']);
+        $ports = explode(',', $this->options['port']);
+        if (empty($ports[0])) {
+            $ports[0] = 11211;
+        }
+        // 建立连接
+        $servers = [];
+        foreach ((array) $hosts as $i => $host) {
+            $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1];
+        }
+        $this->handler->addServers($servers);
+        if ('' != $this->options['username']) {
+            $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
+            $this->handler->setSaslAuthData($this->options['username'], $this->options['password']);
+        }
+    }
+
+    /**
+     * 判断缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return bool
+     */
+    public function has($name)
+    {
+        $key = $this->getCacheKey($name);
+        return $this->handler->get($key) ? true : false;
+    }
+
+    /**
+     * 读取缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @param mixed  $default 默认值
+     * @return mixed
+     */
+    public function get($name, $default = false)
+    {
+        $result = $this->handler->get($this->getCacheKey($name));
+        return false !== $result ? $result : $default;
+    }
+
+    /**
+     * 写入缓存
+     * @access public
+     * @param string            $name 缓存变量名
+     * @param mixed             $value  存储数据
+     * @param integer|\DateTime $expire  有效时间(秒)
+     * @return bool
+     */
+    public function set($name, $value, $expire = null)
+    {
+        if (is_null($expire)) {
+            $expire = $this->options['expire'];
+        }
+        if ($expire instanceof \DateTime) {
+            $expire = $expire->getTimestamp() - time();
+        }
+        if ($this->tag && !$this->has($name)) {
+            $first = true;
+        }
+        $key    = $this->getCacheKey($name);
+        $expire = 0 == $expire ? 0 : $_SERVER['REQUEST_TIME'] + $expire;
+        if ($this->handler->set($key, $value, $expire)) {
+            isset($first) && $this->setTagItem($key);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 自增缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function inc($name, $step = 1)
+    {
+        $key = $this->getCacheKey($name);
+        if ($this->handler->get($key)) {
+            return $this->handler->increment($key, $step);
+        }
+        return $this->handler->set($key, $step);
+    }
+
+    /**
+     * 自减缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function dec($name, $step = 1)
+    {
+        $key   = $this->getCacheKey($name);
+        $value = $this->handler->get($key) - $step;
+        $res   = $this->handler->set($key, $value);
+        if (!$res) {
+            return false;
+        } else {
+            return $value;
+        }
+    }
+
+    /**
+     * 删除缓存
+     * @param    string  $name 缓存变量名
+     * @param bool|false $ttl
+     * @return bool
+     */
+    public function rm($name, $ttl = false)
+    {
+        $key = $this->getCacheKey($name);
+        return false === $ttl ?
+        $this->handler->delete($key) :
+        $this->handler->delete($key, $ttl);
+    }
+
+    /**
+     * 清除缓存
+     * @access public
+     * @param string $tag 标签名
+     * @return bool
+     */
+    public function clear($tag = null)
+    {
+        if ($tag) {
+            // 指定标签清除
+            $keys = $this->getTagItem($tag);
+            $this->handler->deleteMulti($keys);
+            $this->rm('tag_' . md5($tag));
+            return true;
+        }
+        return $this->handler->flush();
+    }
+}

+ 188 - 0
thinkphp/library/think/cache/driver/Redis.php

@@ -0,0 +1,188 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+/**
+ * Redis缓存驱动,适合单机部署、有前端代理实现高可用的场景,性能最好
+ * 有需要在业务层实现读写分离、或者使用RedisCluster的需求,请使用Redisd驱动
+ *
+ * 要求安装phpredis扩展:https://github.com/nicolasff/phpredis
+ * @author    尘缘 <130775@qq.com>
+ */
+class Redis extends Driver
+{
+    protected $options = [
+        'host'       => '127.0.0.1',
+        'port'       => 6379,
+        'password'   => '',
+        'select'     => 0,
+        'timeout'    => 0,
+        'expire'     => 0,
+        'persistent' => false,
+        'prefix'     => '',
+    ];
+
+    /**
+     * 构造函数
+     * @param array $options 缓存参数
+     * @access public
+     */
+    public function __construct($options = [])
+    {
+        if (!extension_loaded('redis')) {
+            throw new \BadFunctionCallException('not support: redis');
+        }
+        if (!empty($options)) {
+            $this->options = array_merge($this->options, $options);
+        }
+        $this->handler = new \Redis;
+        if ($this->options['persistent']) {
+            $this->handler->pconnect($this->options['host'], $this->options['port'], $this->options['timeout'], 'persistent_id_' . $this->options['select']);
+        } else {
+            $this->handler->connect($this->options['host'], $this->options['port'], $this->options['timeout']);
+        }
+
+        if ('' != $this->options['password']) {
+            $this->handler->auth($this->options['password']);
+        }
+
+        if (0 != $this->options['select']) {
+            $this->handler->select($this->options['select']);
+        }
+    }
+
+    /**
+     * 判断缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return bool
+     */
+    public function has($name)
+    {
+        return $this->handler->exists($this->getCacheKey($name));
+    }
+
+    /**
+     * 读取缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @param mixed  $default 默认值
+     * @return mixed
+     */
+    public function get($name, $default = false)
+    {
+        $value = $this->handler->get($this->getCacheKey($name));
+        if (is_null($value) || false === $value) {
+            return $default;
+        }
+
+        try {
+            $result = 0 === strpos($value, 'think_serialize:') ? unserialize(substr($value, 16)) : $value;
+        } catch (\Exception $e) {
+            $result = $default;
+        }
+
+        return $result;
+    }
+
+    /**
+     * 写入缓存
+     * @access public
+     * @param string            $name 缓存变量名
+     * @param mixed             $value  存储数据
+     * @param integer|\DateTime $expire  有效时间(秒)
+     * @return boolean
+     */
+    public function set($name, $value, $expire = null)
+    {
+        if (is_null($expire)) {
+            $expire = $this->options['expire'];
+        }
+        if ($expire instanceof \DateTime) {
+            $expire = $expire->getTimestamp() - time();
+        }
+        if ($this->tag && !$this->has($name)) {
+            $first = true;
+        }
+        $key   = $this->getCacheKey($name);
+        $value = is_scalar($value) ? $value : 'think_serialize:' . serialize($value);
+        if ($expire) {
+            $result = $this->handler->setex($key, $expire, $value);
+        } else {
+            $result = $this->handler->set($key, $value);
+        }
+        isset($first) && $this->setTagItem($key);
+        return $result;
+    }
+
+    /**
+     * 自增缓存(针对数值缓存)
+     * @access public
+     * @param  string    $name 缓存变量名
+     * @param  int       $step 步长
+     * @return false|int
+     */
+    public function inc($name, $step = 1)
+    {
+        $key = $this->getCacheKey($name);
+
+        return $this->handler->incrby($key, $step);
+    }
+
+    /**
+     * 自减缓存(针对数值缓存)
+     * @access public
+     * @param  string    $name 缓存变量名
+     * @param  int       $step 步长
+     * @return false|int
+     */
+    public function dec($name, $step = 1)
+    {
+        $key = $this->getCacheKey($name);
+
+        return $this->handler->decrby($key, $step);
+    }
+
+    /**
+     * 删除缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return boolean
+     */
+    public function rm($name)
+    {
+        return $this->handler->delete($this->getCacheKey($name));
+    }
+
+    /**
+     * 清除缓存
+     * @access public
+     * @param string $tag 标签名
+     * @return boolean
+     */
+    public function clear($tag = null)
+    {
+        if ($tag) {
+            // 指定标签清除
+            $keys = $this->getTagItem($tag);
+            foreach ($keys as $key) {
+                $this->handler->delete($key);
+            }
+            $this->rm('tag_' . md5($tag));
+            return true;
+        }
+        return $this->handler->flushDB();
+    }
+
+}

+ 199 - 0
thinkphp/library/think/cache/driver/Sqlite.php

@@ -0,0 +1,199 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+/**
+ * Sqlite缓存驱动
+ * @author    liu21st <liu21st@gmail.com>
+ */
+class Sqlite extends Driver
+{
+    protected $options = [
+        'db'         => ':memory:',
+        'table'      => 'sharedmemory',
+        'prefix'     => '',
+        'expire'     => 0,
+        'persistent' => false,
+    ];
+
+    /**
+     * 构造函数
+     * @param array $options 缓存参数
+     * @throws \BadFunctionCallException
+     * @access public
+     */
+    public function __construct($options = [])
+    {
+        if (!extension_loaded('sqlite')) {
+            throw new \BadFunctionCallException('not support: sqlite');
+        }
+        if (!empty($options)) {
+            $this->options = array_merge($this->options, $options);
+        }
+        $func          = $this->options['persistent'] ? 'sqlite_popen' : 'sqlite_open';
+        $this->handler = $func($this->options['db']);
+    }
+
+    /**
+     * 获取实际的缓存标识
+     * @access public
+     * @param string $name 缓存名
+     * @return string
+     */
+    protected function getCacheKey($name)
+    {
+        return $this->options['prefix'] . sqlite_escape_string($name);
+    }
+
+    /**
+     * 判断缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return bool
+     */
+    public function has($name)
+    {
+        $name   = $this->getCacheKey($name);
+        $sql    = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . $_SERVER['REQUEST_TIME'] . ') LIMIT 1';
+        $result = sqlite_query($this->handler, $sql);
+        return sqlite_num_rows($result);
+    }
+
+    /**
+     * 读取缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @param mixed  $default 默认值
+     * @return mixed
+     */
+    public function get($name, $default = false)
+    {
+        $name   = $this->getCacheKey($name);
+        $sql    = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . $_SERVER['REQUEST_TIME'] . ') LIMIT 1';
+        $result = sqlite_query($this->handler, $sql);
+        if (sqlite_num_rows($result)) {
+            $content = sqlite_fetch_single($result);
+            if (function_exists('gzcompress')) {
+                //启用数据压缩
+                $content = gzuncompress($content);
+            }
+            return unserialize($content);
+        }
+        return $default;
+    }
+
+    /**
+     * 写入缓存
+     * @access public
+     * @param string            $name 缓存变量名
+     * @param mixed             $value  存储数据
+     * @param integer|\DateTime $expire  有效时间(秒)
+     * @return boolean
+     */
+    public function set($name, $value, $expire = null)
+    {
+        $name  = $this->getCacheKey($name);
+        $value = sqlite_escape_string(serialize($value));
+        if (is_null($expire)) {
+            $expire = $this->options['expire'];
+        }
+        if ($expire instanceof \DateTime) {
+            $expire = $expire->getTimestamp();
+        } else {
+            $expire = (0 == $expire) ? 0 : (time() + $expire); //缓存有效期为0表示永久缓存
+        }
+        if (function_exists('gzcompress')) {
+            //数据压缩
+            $value = gzcompress($value, 3);
+        }
+        if ($this->tag) {
+            $tag       = $this->tag;
+            $this->tag = null;
+        } else {
+            $tag = '';
+        }
+        $sql = 'REPLACE INTO ' . $this->options['table'] . ' (var, value, expire, tag) VALUES (\'' . $name . '\', \'' . $value . '\', \'' . $expire . '\', \'' . $tag . '\')';
+        if (sqlite_query($this->handler, $sql)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 自增缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function inc($name, $step = 1)
+    {
+        if ($this->has($name)) {
+            $value = $this->get($name) + $step;
+        } else {
+            $value = $step;
+        }
+        return $this->set($name, $value, 0) ? $value : false;
+    }
+
+    /**
+     * 自减缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function dec($name, $step = 1)
+    {
+        if ($this->has($name)) {
+            $value = $this->get($name) - $step;
+        } else {
+            $value = -$step;
+        }
+        return $this->set($name, $value, 0) ? $value : false;
+    }
+
+    /**
+     * 删除缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return boolean
+     */
+    public function rm($name)
+    {
+        $name = $this->getCacheKey($name);
+        $sql  = 'DELETE FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\'';
+        sqlite_query($this->handler, $sql);
+        return true;
+    }
+
+    /**
+     * 清除缓存
+     * @access public
+     * @param string $tag 标签名
+     * @return boolean
+     */
+    public function clear($tag = null)
+    {
+        if ($tag) {
+            $name = sqlite_escape_string($tag);
+            $sql  = 'DELETE FROM ' . $this->options['table'] . ' WHERE tag=\'' . $name . '\'';
+            sqlite_query($this->handler, $sql);
+            return true;
+        }
+        $sql = 'DELETE FROM ' . $this->options['table'];
+        sqlite_query($this->handler, $sql);
+        return true;
+    }
+}

+ 152 - 0
thinkphp/library/think/cache/driver/Wincache.php

@@ -0,0 +1,152 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+/**
+ * Wincache缓存驱动
+ * @author    liu21st <liu21st@gmail.com>
+ */
+class Wincache extends Driver
+{
+    protected $options = [
+        'prefix' => '',
+        'expire' => 0,
+    ];
+
+    /**
+     * 构造函数
+     * @param array $options 缓存参数
+     * @throws \BadFunctionCallException
+     * @access public
+     */
+    public function __construct($options = [])
+    {
+        if (!function_exists('wincache_ucache_info')) {
+            throw new \BadFunctionCallException('not support: WinCache');
+        }
+        if (!empty($options)) {
+            $this->options = array_merge($this->options, $options);
+        }
+    }
+
+    /**
+     * 判断缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return bool
+     */
+    public function has($name)
+    {
+        $key = $this->getCacheKey($name);
+        return wincache_ucache_exists($key);
+    }
+
+    /**
+     * 读取缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @param mixed  $default 默认值
+     * @return mixed
+     */
+    public function get($name, $default = false)
+    {
+        $key = $this->getCacheKey($name);
+        return wincache_ucache_exists($key) ? wincache_ucache_get($key) : $default;
+    }
+
+    /**
+     * 写入缓存
+     * @access public
+     * @param string            $name 缓存变量名
+     * @param mixed             $value  存储数据
+     * @param integer|\DateTime $expire  有效时间(秒)
+     * @return boolean
+     */
+    public function set($name, $value, $expire = null)
+    {
+        if (is_null($expire)) {
+            $expire = $this->options['expire'];
+        }
+        if ($expire instanceof \DateTime) {
+            $expire = $expire->getTimestamp() - time();
+        }
+        $key = $this->getCacheKey($name);
+        if ($this->tag && !$this->has($name)) {
+            $first = true;
+        }
+        if (wincache_ucache_set($key, $value, $expire)) {
+            isset($first) && $this->setTagItem($key);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 自增缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function inc($name, $step = 1)
+    {
+        $key = $this->getCacheKey($name);
+        return wincache_ucache_inc($key, $step);
+    }
+
+    /**
+     * 自减缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function dec($name, $step = 1)
+    {
+        $key = $this->getCacheKey($name);
+        return wincache_ucache_dec($key, $step);
+    }
+
+    /**
+     * 删除缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return boolean
+     */
+    public function rm($name)
+    {
+        return wincache_ucache_delete($this->getCacheKey($name));
+    }
+
+    /**
+     * 清除缓存
+     * @access public
+     * @param string $tag 标签名
+     * @return boolean
+     */
+    public function clear($tag = null)
+    {
+        if ($tag) {
+            $keys = $this->getTagItem($tag);
+            foreach ($keys as $key) {
+                wincache_ucache_delete($key);
+            }
+            $this->rm('tag_' . md5($tag));
+            return true;
+        } else {
+            return wincache_ucache_clear();
+        }
+    }
+
+}

+ 155 - 0
thinkphp/library/think/cache/driver/Xcache.php

@@ -0,0 +1,155 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\cache\driver;
+
+use think\cache\Driver;
+
+/**
+ * Xcache缓存驱动
+ * @author    liu21st <liu21st@gmail.com>
+ */
+class Xcache extends Driver
+{
+    protected $options = [
+        'prefix' => '',
+        'expire' => 0,
+    ];
+
+    /**
+     * 构造函数
+     * @param array $options 缓存参数
+     * @access public
+     * @throws \BadFunctionCallException
+     */
+    public function __construct($options = [])
+    {
+        if (!function_exists('xcache_info')) {
+            throw new \BadFunctionCallException('not support: Xcache');
+        }
+        if (!empty($options)) {
+            $this->options = array_merge($this->options, $options);
+        }
+    }
+
+    /**
+     * 判断缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return bool
+     */
+    public function has($name)
+    {
+        $key = $this->getCacheKey($name);
+        return xcache_isset($key);
+    }
+
+    /**
+     * 读取缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @param mixed  $default 默认值
+     * @return mixed
+     */
+    public function get($name, $default = false)
+    {
+        $key = $this->getCacheKey($name);
+        return xcache_isset($key) ? xcache_get($key) : $default;
+    }
+
+    /**
+     * 写入缓存
+     * @access public
+     * @param string            $name 缓存变量名
+     * @param mixed             $value  存储数据
+     * @param integer|\DateTime $expire  有效时间(秒)
+     * @return boolean
+     */
+    public function set($name, $value, $expire = null)
+    {
+        if (is_null($expire)) {
+            $expire = $this->options['expire'];
+        }
+        if ($expire instanceof \DateTime) {
+            $expire = $expire->getTimestamp() - time();
+        }
+        if ($this->tag && !$this->has($name)) {
+            $first = true;
+        }
+        $key = $this->getCacheKey($name);
+        if (xcache_set($key, $value, $expire)) {
+            isset($first) && $this->setTagItem($key);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 自增缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function inc($name, $step = 1)
+    {
+        $key = $this->getCacheKey($name);
+        return xcache_inc($key, $step);
+    }
+
+    /**
+     * 自减缓存(针对数值缓存)
+     * @access public
+     * @param string    $name 缓存变量名
+     * @param int       $step 步长
+     * @return false|int
+     */
+    public function dec($name, $step = 1)
+    {
+        $key = $this->getCacheKey($name);
+        return xcache_dec($key, $step);
+    }
+
+    /**
+     * 删除缓存
+     * @access public
+     * @param string $name 缓存变量名
+     * @return boolean
+     */
+    public function rm($name)
+    {
+        return xcache_unset($this->getCacheKey($name));
+    }
+
+    /**
+     * 清除缓存
+     * @access public
+     * @param string $tag 标签名
+     * @return boolean
+     */
+    public function clear($tag = null)
+    {
+        if ($tag) {
+            // 指定标签清除
+            $keys = $this->getTagItem($tag);
+            foreach ($keys as $key) {
+                xcache_unset($key);
+            }
+            $this->rm('tag_' . md5($tag));
+            return true;
+        }
+        if (function_exists('xcache_unset_by_prefix')) {
+            return xcache_unset_by_prefix($this->options['prefix']);
+        } else {
+            return false;
+        }
+    }
+}

+ 24 - 0
thinkphp/library/think/config/driver/Ini.php

@@ -0,0 +1,24 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\config\driver;
+
+class Ini
+{
+    public function parse($config)
+    {
+        if (is_file($config)) {
+            return parse_ini_file($config, true);
+        } else {
+            return parse_ini_string($config, true);
+        }
+    }
+}

+ 24 - 0
thinkphp/library/think/config/driver/Json.php

@@ -0,0 +1,24 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\config\driver;
+
+class Json
+{
+    public function parse($config)
+    {
+        if (is_file($config)) {
+            $config = file_get_contents($config);
+        }
+        $result = json_decode($config, true);
+        return $result;
+    }
+}

+ 31 - 0
thinkphp/library/think/config/driver/Xml.php

@@ -0,0 +1,31 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\config\driver;
+
+class Xml
+{
+    public function parse($config)
+    {
+        if (is_file($config)) {
+            $content = simplexml_load_file($config);
+        } else {
+            $content = simplexml_load_string($config);
+        }
+        $result = (array) $content;
+        foreach ($result as $key => $val) {
+            if (is_object($val)) {
+                $result[$key] = (array) $val;
+            }
+        }
+        return $result;
+    }
+}

+ 470 - 0
thinkphp/library/think/console/Command.php

@@ -0,0 +1,470 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console;
+
+use think\Console;
+use think\console\input\Argument;
+use think\console\input\Definition;
+use think\console\input\Option;
+
+class Command
+{
+
+    /** @var  Console */
+    private $console;
+    private $name;
+    private $aliases = [];
+    private $definition;
+    private $help;
+    private $description;
+    private $ignoreValidationErrors          = false;
+    private $consoleDefinitionMerged         = false;
+    private $consoleDefinitionMergedWithArgs = false;
+    private $code;
+    private $synopsis = [];
+    private $usages   = [];
+
+    /** @var  Input */
+    protected $input;
+
+    /** @var  Output */
+    protected $output;
+
+    /**
+     * 构造方法
+     * @param string|null $name 命令名称,如果没有设置则比如在 configure() 里设置
+     * @throws \LogicException
+     * @api
+     */
+    public function __construct($name = null)
+    {
+        $this->definition = new Definition();
+
+        if (null !== $name) {
+            $this->setName($name);
+        }
+
+        $this->configure();
+
+        if (!$this->name) {
+            throw new \LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this)));
+        }
+    }
+
+    /**
+     * 忽略验证错误
+     */
+    public function ignoreValidationErrors()
+    {
+        $this->ignoreValidationErrors = true;
+    }
+
+    /**
+     * 设置控制台
+     * @param Console $console
+     */
+    public function setConsole(Console $console = null)
+    {
+        $this->console = $console;
+    }
+
+    /**
+     * 获取控制台
+     * @return Console
+     * @api
+     */
+    public function getConsole()
+    {
+        return $this->console;
+    }
+
+    /**
+     * 是否有效
+     * @return bool
+     */
+    public function isEnabled()
+    {
+        return true;
+    }
+
+    /**
+     * 配置指令
+     */
+    protected function configure()
+    {
+    }
+
+    /**
+     * 执行指令
+     * @param Input  $input
+     * @param Output $output
+     * @return null|int
+     * @throws \LogicException
+     * @see setCode()
+     */
+    protected function execute(Input $input, Output $output)
+    {
+        throw new \LogicException('You must override the execute() method in the concrete command class.');
+    }
+
+    /**
+     * 用户验证
+     * @param Input  $input
+     * @param Output $output
+     */
+    protected function interact(Input $input, Output $output)
+    {
+    }
+
+    /**
+     * 初始化
+     * @param Input  $input  An InputInterface instance
+     * @param Output $output An OutputInterface instance
+     */
+    protected function initialize(Input $input, Output $output)
+    {
+    }
+
+    /**
+     * 执行
+     * @param Input  $input
+     * @param Output $output
+     * @return int
+     * @throws \Exception
+     * @see setCode()
+     * @see execute()
+     */
+    public function run(Input $input, Output $output)
+    {
+        $this->input  = $input;
+        $this->output = $output;
+
+        $this->getSynopsis(true);
+        $this->getSynopsis(false);
+
+        $this->mergeConsoleDefinition();
+
+        try {
+            $input->bind($this->definition);
+        } catch (\Exception $e) {
+            if (!$this->ignoreValidationErrors) {
+                throw $e;
+            }
+        }
+
+        $this->initialize($input, $output);
+
+        if ($input->isInteractive()) {
+            $this->interact($input, $output);
+        }
+
+        $input->validate();
+
+        if ($this->code) {
+            $statusCode = call_user_func($this->code, $input, $output);
+        } else {
+            $statusCode = $this->execute($input, $output);
+        }
+
+        return is_numeric($statusCode) ? (int) $statusCode : 0;
+    }
+
+    /**
+     * 设置执行代码
+     * @param callable $code callable(InputInterface $input, OutputInterface $output)
+     * @return Command
+     * @throws \InvalidArgumentException
+     * @see execute()
+     */
+    public function setCode(callable $code)
+    {
+        if (!is_callable($code)) {
+            throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.');
+        }
+
+        if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) {
+            $r = new \ReflectionFunction($code);
+            if (null === $r->getClosureThis()) {
+                $code = \Closure::bind($code, $this);
+            }
+        }
+
+        $this->code = $code;
+
+        return $this;
+    }
+
+    /**
+     * 合并参数定义
+     * @param bool $mergeArgs
+     */
+    public function mergeConsoleDefinition($mergeArgs = true)
+    {
+        if (null === $this->console
+            || (true === $this->consoleDefinitionMerged
+                && ($this->consoleDefinitionMergedWithArgs || !$mergeArgs))
+        ) {
+            return;
+        }
+
+        if ($mergeArgs) {
+            $currentArguments = $this->definition->getArguments();
+            $this->definition->setArguments($this->console->getDefinition()->getArguments());
+            $this->definition->addArguments($currentArguments);
+        }
+
+        $this->definition->addOptions($this->console->getDefinition()->getOptions());
+
+        $this->consoleDefinitionMerged = true;
+        if ($mergeArgs) {
+            $this->consoleDefinitionMergedWithArgs = true;
+        }
+    }
+
+    /**
+     * 设置参数定义
+     * @param array|Definition $definition
+     * @return Command
+     * @api
+     */
+    public function setDefinition($definition)
+    {
+        if ($definition instanceof Definition) {
+            $this->definition = $definition;
+        } else {
+            $this->definition->setDefinition($definition);
+        }
+
+        $this->consoleDefinitionMerged = false;
+
+        return $this;
+    }
+
+    /**
+     * 获取参数定义
+     * @return Definition
+     * @api
+     */
+    public function getDefinition()
+    {
+        return $this->definition;
+    }
+
+    /**
+     * 获取当前指令的参数定义
+     * @return Definition
+     */
+    public function getNativeDefinition()
+    {
+        return $this->getDefinition();
+    }
+
+    /**
+     * 添加参数
+     * @param string $name        名称
+     * @param int    $mode        类型
+     * @param string $description 描述
+     * @param mixed  $default     默认值
+     * @return Command
+     */
+    public function addArgument($name, $mode = null, $description = '', $default = null)
+    {
+        $this->definition->addArgument(new Argument($name, $mode, $description, $default));
+
+        return $this;
+    }
+
+    /**
+     * 添加选项
+     * @param string $name        选项名称
+     * @param string $shortcut    别名
+     * @param int    $mode        类型
+     * @param string $description 描述
+     * @param mixed  $default     默认值
+     * @return Command
+     */
+    public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null)
+    {
+        $this->definition->addOption(new Option($name, $shortcut, $mode, $description, $default));
+
+        return $this;
+    }
+
+    /**
+     * 设置指令名称
+     * @param string $name
+     * @return Command
+     * @throws \InvalidArgumentException
+     */
+    public function setName($name)
+    {
+        $this->validateName($name);
+
+        $this->name = $name;
+
+        return $this;
+    }
+
+    /**
+     * 获取指令名称
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * 设置描述
+     * @param string $description
+     * @return Command
+     */
+    public function setDescription($description)
+    {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    /**
+     *  获取描述
+     * @return string
+     */
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    /**
+     * 设置帮助信息
+     * @param string $help
+     * @return Command
+     */
+    public function setHelp($help)
+    {
+        $this->help = $help;
+
+        return $this;
+    }
+
+    /**
+     * 获取帮助信息
+     * @return string
+     */
+    public function getHelp()
+    {
+        return $this->help;
+    }
+
+    /**
+     * 描述信息
+     * @return string
+     */
+    public function getProcessedHelp()
+    {
+        $name = $this->name;
+
+        $placeholders = [
+            '%command.name%',
+            '%command.full_name%',
+        ];
+        $replacements = [
+            $name,
+            $_SERVER['PHP_SELF'] . ' ' . $name,
+        ];
+
+        return str_replace($placeholders, $replacements, $this->getHelp());
+    }
+
+    /**
+     * 设置别名
+     * @param string[] $aliases
+     * @return Command
+     * @throws \InvalidArgumentException
+     */
+    public function setAliases($aliases)
+    {
+        if (!is_array($aliases) && !$aliases instanceof \Traversable) {
+            throw new \InvalidArgumentException('$aliases must be an array or an instance of \Traversable');
+        }
+
+        foreach ($aliases as $alias) {
+            $this->validateName($alias);
+        }
+
+        $this->aliases = $aliases;
+
+        return $this;
+    }
+
+    /**
+     * 获取别名
+     * @return array
+     */
+    public function getAliases()
+    {
+        return $this->aliases;
+    }
+
+    /**
+     * 获取简介
+     * @param bool $short 是否简单的
+     * @return string
+     */
+    public function getSynopsis($short = false)
+    {
+        $key = $short ? 'short' : 'long';
+
+        if (!isset($this->synopsis[$key])) {
+            $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
+        }
+
+        return $this->synopsis[$key];
+    }
+
+    /**
+     * 添加用法介绍
+     * @param string $usage
+     * @return $this
+     */
+    public function addUsage($usage)
+    {
+        if (0 !== strpos($usage, $this->name)) {
+            $usage = sprintf('%s %s', $this->name, $usage);
+        }
+
+        $this->usages[] = $usage;
+
+        return $this;
+    }
+
+    /**
+     * 获取用法介绍
+     * @return array
+     */
+    public function getUsages()
+    {
+        return $this->usages;
+    }
+
+    /**
+     * 验证指令名称
+     * @param string $name
+     * @throws \InvalidArgumentException
+     */
+    private function validateName($name)
+    {
+        if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
+            throw new \InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
+        }
+    }
+}

+ 464 - 0
thinkphp/library/think/console/Input.php

@@ -0,0 +1,464 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console;
+
+use think\console\input\Argument;
+use think\console\input\Definition;
+use think\console\input\Option;
+
+class Input
+{
+
+    /**
+     * @var Definition
+     */
+    protected $definition;
+
+    /**
+     * @var Option[]
+     */
+    protected $options = [];
+
+    /**
+     * @var Argument[]
+     */
+    protected $arguments = [];
+
+    protected $interactive = true;
+
+    private $tokens;
+    private $parsed;
+
+    public function __construct($argv = null)
+    {
+        if (null === $argv) {
+            $argv = $_SERVER['argv'];
+            // 去除命令名
+            array_shift($argv);
+        }
+
+        $this->tokens = $argv;
+
+        $this->definition = new Definition();
+    }
+
+    protected function setTokens(array $tokens)
+    {
+        $this->tokens = $tokens;
+    }
+
+    /**
+     * 绑定实例
+     * @param Definition $definition A InputDefinition instance
+     */
+    public function bind(Definition $definition)
+    {
+        $this->arguments  = [];
+        $this->options    = [];
+        $this->definition = $definition;
+
+        $this->parse();
+    }
+
+    /**
+     * 解析参数
+     */
+    protected function parse()
+    {
+        $parseOptions = true;
+        $this->parsed = $this->tokens;
+        while (null !== $token = array_shift($this->parsed)) {
+            if ($parseOptions && '' == $token) {
+                $this->parseArgument($token);
+            } elseif ($parseOptions && '--' == $token) {
+                $parseOptions = false;
+            } elseif ($parseOptions && 0 === strpos($token, '--')) {
+                $this->parseLongOption($token);
+            } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
+                $this->parseShortOption($token);
+            } else {
+                $this->parseArgument($token);
+            }
+        }
+    }
+
+    /**
+     * 解析短选项
+     * @param string $token 当前的指令.
+     */
+    private function parseShortOption($token)
+    {
+        $name = substr($token, 1);
+
+        if (strlen($name) > 1) {
+            if ($this->definition->hasShortcut($name[0])
+                && $this->definition->getOptionForShortcut($name[0])->acceptValue()
+            ) {
+                $this->addShortOption($name[0], substr($name, 1));
+            } else {
+                $this->parseShortOptionSet($name);
+            }
+        } else {
+            $this->addShortOption($name, null);
+        }
+    }
+
+    /**
+     * 解析短选项
+     * @param string $name 当前指令
+     * @throws \RuntimeException
+     */
+    private function parseShortOptionSet($name)
+    {
+        $len = strlen($name);
+        for ($i = 0; $i < $len; ++$i) {
+            if (!$this->definition->hasShortcut($name[$i])) {
+                throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i]));
+            }
+
+            $option = $this->definition->getOptionForShortcut($name[$i]);
+            if ($option->acceptValue()) {
+                $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));
+
+                break;
+            } else {
+                $this->addLongOption($option->getName(), null);
+            }
+        }
+    }
+
+    /**
+     * 解析完整选项
+     * @param string $token 当前指令
+     */
+    private function parseLongOption($token)
+    {
+        $name = substr($token, 2);
+
+        if (false !== $pos = strpos($name, '=')) {
+            $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1));
+        } else {
+            $this->addLongOption($name, null);
+        }
+    }
+
+    /**
+     * 解析参数
+     * @param string $token 当前指令
+     * @throws \RuntimeException
+     */
+    private function parseArgument($token)
+    {
+        $c = count($this->arguments);
+
+        if ($this->definition->hasArgument($c)) {
+            $arg = $this->definition->getArgument($c);
+
+            $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token;
+
+        } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) {
+            $arg = $this->definition->getArgument($c - 1);
+
+            $this->arguments[$arg->getName()][] = $token;
+        } else {
+            throw new \RuntimeException('Too many arguments.');
+        }
+    }
+
+    /**
+     * 添加一个短选项的值
+     * @param string $shortcut 短名称
+     * @param mixed  $value    值
+     * @throws \RuntimeException
+     */
+    private function addShortOption($shortcut, $value)
+    {
+        if (!$this->definition->hasShortcut($shortcut)) {
+            throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut));
+        }
+
+        $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
+    }
+
+    /**
+     * 添加一个完整选项的值
+     * @param string $name  选项名
+     * @param mixed  $value 值
+     * @throws \RuntimeException
+     */
+    private function addLongOption($name, $value)
+    {
+        if (!$this->definition->hasOption($name)) {
+            throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name));
+        }
+
+        $option = $this->definition->getOption($name);
+
+        if (false === $value) {
+            $value = null;
+        }
+
+        if (null !== $value && !$option->acceptValue()) {
+            throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value));
+        }
+
+        if (null === $value && $option->acceptValue() && count($this->parsed)) {
+            $next = array_shift($this->parsed);
+            if (isset($next[0]) && '-' !== $next[0]) {
+                $value = $next;
+            } elseif (empty($next)) {
+                $value = '';
+            } else {
+                array_unshift($this->parsed, $next);
+            }
+        }
+
+        if (null === $value) {
+            if ($option->isValueRequired()) {
+                throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name));
+            }
+
+            if (!$option->isArray()) {
+                $value = $option->isValueOptional() ? $option->getDefault() : true;
+            }
+        }
+
+        if ($option->isArray()) {
+            $this->options[$name][] = $value;
+        } else {
+            $this->options[$name] = $value;
+        }
+    }
+
+    /**
+     * 获取第一个参数
+     * @return string|null
+     */
+    public function getFirstArgument()
+    {
+        foreach ($this->tokens as $token) {
+            if ($token && '-' === $token[0]) {
+                continue;
+            }
+
+            return $token;
+        }
+        return;
+    }
+
+    /**
+     * 检查原始参数是否包含某个值
+     * @param string|array $values 需要检查的值
+     * @return bool
+     */
+    public function hasParameterOption($values)
+    {
+        $values = (array) $values;
+
+        foreach ($this->tokens as $token) {
+            foreach ($values as $value) {
+                if ($token === $value || 0 === strpos($token, $value . '=')) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 获取原始选项的值
+     * @param string|array $values  需要检查的值
+     * @param mixed        $default 默认值
+     * @return mixed The option value
+     */
+    public function getParameterOption($values, $default = false)
+    {
+        $values = (array) $values;
+        $tokens = $this->tokens;
+
+        while (0 < count($tokens)) {
+            $token = array_shift($tokens);
+
+            foreach ($values as $value) {
+                if ($token === $value || 0 === strpos($token, $value . '=')) {
+                    if (false !== $pos = strpos($token, '=')) {
+                        return substr($token, $pos + 1);
+                    }
+
+                    return array_shift($tokens);
+                }
+            }
+        }
+
+        return $default;
+    }
+
+    /**
+     * 验证输入
+     * @throws \RuntimeException
+     */
+    public function validate()
+    {
+        if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) {
+            throw new \RuntimeException('Not enough arguments.');
+        }
+    }
+
+    /**
+     * 检查输入是否是交互的
+     * @return bool
+     */
+    public function isInteractive()
+    {
+        return $this->interactive;
+    }
+
+    /**
+     * 设置输入的交互
+     * @param bool
+     */
+    public function setInteractive($interactive)
+    {
+        $this->interactive = (bool) $interactive;
+    }
+
+    /**
+     * 获取所有的参数
+     * @return Argument[]
+     */
+    public function getArguments()
+    {
+        return array_merge($this->definition->getArgumentDefaults(), $this->arguments);
+    }
+
+    /**
+     * 根据名称获取参数
+     * @param string $name 参数名
+     * @return mixed
+     * @throws \InvalidArgumentException
+     */
+    public function getArgument($name)
+    {
+        if (!$this->definition->hasArgument($name)) {
+            throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+        }
+
+        return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name)
+            ->getDefault();
+    }
+
+    /**
+     * 设置参数的值
+     * @param string $name  参数名
+     * @param string $value 值
+     * @throws \InvalidArgumentException
+     */
+    public function setArgument($name, $value)
+    {
+        if (!$this->definition->hasArgument($name)) {
+            throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+        }
+
+        $this->arguments[$name] = $value;
+    }
+
+    /**
+     * 检查是否存在某个参数
+     * @param string|int $name 参数名或位置
+     * @return bool
+     */
+    public function hasArgument($name)
+    {
+        return $this->definition->hasArgument($name);
+    }
+
+    /**
+     * 获取所有的选项
+     * @return Option[]
+     */
+    public function getOptions()
+    {
+        return array_merge($this->definition->getOptionDefaults(), $this->options);
+    }
+
+    /**
+     * 获取选项值
+     * @param string $name 选项名称
+     * @return mixed
+     * @throws \InvalidArgumentException
+     */
+    public function getOption($name)
+    {
+        if (!$this->definition->hasOption($name)) {
+            throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
+        }
+
+        return isset($this->options[$name]) ? $this->options[$name] : $this->definition->getOption($name)->getDefault();
+    }
+
+    /**
+     * 设置选项值
+     * @param string      $name  选项名
+     * @param string|bool $value 值
+     * @throws \InvalidArgumentException
+     */
+    public function setOption($name, $value)
+    {
+        if (!$this->definition->hasOption($name)) {
+            throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
+        }
+
+        $this->options[$name] = $value;
+    }
+
+    /**
+     * 是否有某个选项
+     * @param string $name 选项名
+     * @return bool
+     */
+    public function hasOption($name)
+    {
+        return $this->definition->hasOption($name) && isset($this->options[$name]);
+    }
+
+    /**
+     * 转义指令
+     * @param string $token
+     * @return string
+     */
+    public function escapeToken($token)
+    {
+        return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token);
+    }
+
+    /**
+     * 返回传递给命令的参数的字符串
+     * @return string
+     */
+    public function __toString()
+    {
+        $tokens = array_map(function ($token) {
+            if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) {
+                return $match[1] . $this->escapeToken($match[2]);
+            }
+
+            if ($token && '-' !== $token[0]) {
+                return $this->escapeToken($token);
+            }
+
+            return $token;
+        }, $this->tokens);
+
+        return implode(' ', $tokens);
+    }
+}

+ 19 - 0
thinkphp/library/think/console/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2004-2016 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 222 - 0
thinkphp/library/think/console/Output.php

@@ -0,0 +1,222 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console;
+
+use Exception;
+use think\console\output\Ask;
+use think\console\output\Descriptor;
+use think\console\output\driver\Buffer;
+use think\console\output\driver\Console;
+use think\console\output\driver\Nothing;
+use think\console\output\Question;
+use think\console\output\question\Choice;
+use think\console\output\question\Confirmation;
+
+/**
+ * Class Output
+ * @package think\console
+ *
+ * @see     \think\console\output\driver\Console::setDecorated
+ * @method void setDecorated($decorated)
+ *
+ * @see     \think\console\output\driver\Buffer::fetch
+ * @method string fetch()
+ *
+ * @method void info($message)
+ * @method void error($message)
+ * @method void comment($message)
+ * @method void warning($message)
+ * @method void highlight($message)
+ * @method void question($message)
+ */
+class Output
+{
+    const VERBOSITY_QUIET        = 0;
+    const VERBOSITY_NORMAL       = 1;
+    const VERBOSITY_VERBOSE      = 2;
+    const VERBOSITY_VERY_VERBOSE = 3;
+    const VERBOSITY_DEBUG        = 4;
+
+    const OUTPUT_NORMAL = 0;
+    const OUTPUT_RAW    = 1;
+    const OUTPUT_PLAIN  = 2;
+
+    private $verbosity = self::VERBOSITY_NORMAL;
+
+    /** @var Buffer|Console|Nothing */
+    private $handle = null;
+
+    protected $styles = [
+        'info',
+        'error',
+        'comment',
+        'question',
+        'highlight',
+        'warning'
+    ];
+
+    public function __construct($driver = 'console')
+    {
+        $class = '\\think\\console\\output\\driver\\' . ucwords($driver);
+
+        $this->handle = new $class($this);
+    }
+
+    public function ask(Input $input, $question, $default = null, $validator = null)
+    {
+        $question = new Question($question, $default);
+        $question->setValidator($validator);
+
+        return $this->askQuestion($input, $question);
+    }
+
+    public function askHidden(Input $input, $question, $validator = null)
+    {
+        $question = new Question($question);
+
+        $question->setHidden(true);
+        $question->setValidator($validator);
+
+        return $this->askQuestion($input, $question);
+    }
+
+    public function confirm(Input $input, $question, $default = true)
+    {
+        return $this->askQuestion($input, new Confirmation($question, $default));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function choice(Input $input, $question, array $choices, $default = null)
+    {
+        if (null !== $default) {
+            $values  = array_flip($choices);
+            $default = $values[$default];
+        }
+
+        return $this->askQuestion($input, new Choice($question, $choices, $default));
+    }
+
+    protected function askQuestion(Input $input, Question $question)
+    {
+        $ask    = new Ask($input, $this, $question);
+        $answer = $ask->run();
+
+        if ($input->isInteractive()) {
+            $this->newLine();
+        }
+
+        return $answer;
+    }
+
+    protected function block($style, $message)
+    {
+        $this->writeln("<{$style}>{$message}</$style>");
+    }
+
+    /**
+     * 输出空行
+     * @param int $count
+     */
+    public function newLine($count = 1)
+    {
+        $this->write(str_repeat(PHP_EOL, $count));
+    }
+
+    /**
+     * 输出信息并换行
+     * @param string $messages
+     * @param int    $type
+     */
+    public function writeln($messages, $type = self::OUTPUT_NORMAL)
+    {
+        $this->write($messages, true, $type);
+    }
+
+    /**
+     * 输出信息
+     * @param string $messages
+     * @param bool   $newline
+     * @param int    $type
+     */
+    public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
+    {
+        $this->handle->write($messages, $newline, $type);
+    }
+
+    public function renderException(\Exception $e)
+    {
+        $this->handle->renderException($e);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setVerbosity($level)
+    {
+        $this->verbosity = (int) $level;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getVerbosity()
+    {
+        return $this->verbosity;
+    }
+
+    public function isQuiet()
+    {
+        return self::VERBOSITY_QUIET === $this->verbosity;
+    }
+
+    public function isVerbose()
+    {
+        return self::VERBOSITY_VERBOSE <= $this->verbosity;
+    }
+
+    public function isVeryVerbose()
+    {
+        return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity;
+    }
+
+    public function isDebug()
+    {
+        return self::VERBOSITY_DEBUG <= $this->verbosity;
+    }
+
+    public function describe($object, array $options = [])
+    {
+        $descriptor = new Descriptor();
+        $options    = array_merge([
+            'raw_text' => false,
+        ], $options);
+
+        $descriptor->describe($this, $object, $options);
+    }
+
+    public function __call($method, $args)
+    {
+        if (in_array($method, $this->styles)) {
+            array_unshift($args, $method);
+            return call_user_func_array([$this, 'block'], $args);
+        }
+
+        if ($this->handle && method_exists($this->handle, $method)) {
+            return call_user_func_array([$this->handle, $method], $args);
+        } else {
+            throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
+        }
+    }
+
+}

+ 1 - 0
thinkphp/library/think/console/bin/README.md

@@ -0,0 +1 @@
+console 工具使用 hiddeninput.exe 在 windows 上隐藏密码输入,该二进制文件由第三方提供,相关源码和其他细节可以在 [Hidden Input](https://github.com/Seldaek/hidden-input) 找到。

BIN
thinkphp/library/think/console/bin/hiddeninput.exe


+ 56 - 0
thinkphp/library/think/console/command/Build.php

@@ -0,0 +1,56 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+
+class Build extends Command
+{
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function configure()
+    {
+        $this->setName('build')
+            ->setDefinition([
+                new Option('config', null, Option::VALUE_OPTIONAL, "build.php path"),
+                new Option('module', null, Option::VALUE_OPTIONAL, "module name"),
+            ])
+            ->setDescription('Build Application Dirs');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        if ($input->hasOption('module')) {
+            \think\Build::module($input->getOption('module'));
+            $output->writeln("Successed");
+            return;
+        }
+
+        if ($input->hasOption('config')) {
+            $build = include $input->getOption('config');
+        } else {
+            $build = include APP_PATH . 'build.php';
+        }
+        if (empty($build)) {
+            $output->writeln("Build Config Is Empty");
+            return;
+        }
+        \think\Build::run($build);
+        $output->writeln("Successed");
+
+    }
+}

+ 63 - 0
thinkphp/library/think/console/command/Clear.php

@@ -0,0 +1,63 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+namespace think\console\command;
+
+use think\Cache;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+
+class Clear extends Command
+{
+    protected function configure()
+    {
+        // 指令配置
+        $this
+            ->setName('clear')
+            ->addArgument('type', Argument::OPTIONAL, 'type to clear', null)
+            ->addOption('path', 'd', Option::VALUE_OPTIONAL, 'path to clear', null)
+            ->setDescription('Clear runtime file');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $path = $input->getOption('path') ?: RUNTIME_PATH;
+
+        $type = $input->getArgument('type');
+
+        if ($type == 'route') {
+            Cache::clear('route_check');
+        } else {
+            if (is_dir($path)) {
+                $this->clearPath($path);
+            }
+        }
+
+        $output->writeln("<info>Clear Successed</info>");
+    }
+
+    protected function clearPath($path)
+    {
+        $path  = realpath($path) . DS;
+        $files = scandir($path);
+        if ($files) {
+            foreach ($files as $file) {
+                if ('.' != $file && '..' != $file && is_dir($path . $file)) {
+                    $this->clearPath($path . $file);
+                } elseif ('.gitignore' != $file && is_file($path . $file)) {
+                    unlink($path . $file);
+                }
+            }
+        }
+    }
+}

+ 69 - 0
thinkphp/library/think/console/command/Help.php

@@ -0,0 +1,69 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+
+class Help extends Command
+{
+
+    private $command;
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function configure()
+    {
+        $this->ignoreValidationErrors();
+
+        $this->setName('help')->setDefinition([
+            new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'),
+            new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
+        ])->setDescription('Displays help for a command')->setHelp(<<<EOF
+The <info>%command.name%</info> command displays help for a given command:
+
+  <info>php %command.full_name% list</info>
+
+To display the list of available commands, please use the <info>list</info> command.
+EOF
+        );
+    }
+
+    /**
+     * Sets the command.
+     * @param Command $command The command to set
+     */
+    public function setCommand(Command $command)
+    {
+        $this->command = $command;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function execute(Input $input, Output $output)
+    {
+        if (null === $this->command) {
+            $this->command = $this->getConsole()->find($input->getArgument('command_name'));
+        }
+
+        $output->describe($this->command, [
+            'raw_text' => $input->getOption('raw'),
+        ]);
+
+        $this->command = null;
+    }
+}

+ 74 - 0
thinkphp/library/think/console/command/Lists.php

@@ -0,0 +1,74 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Option as InputOption;
+use think\console\input\Definition as InputDefinition;
+
+class Lists extends Command
+{
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function configure()
+    {
+        $this->setName('list')->setDefinition($this->createDefinition())->setDescription('Lists commands')->setHelp(<<<EOF
+The <info>%command.name%</info> command lists all commands:
+
+  <info>php %command.full_name%</info>
+
+You can also display the commands for a specific namespace:
+
+  <info>php %command.full_name% test</info>
+
+It's also possible to get raw list of commands (useful for embedding command runner):
+
+  <info>php %command.full_name% --raw</info>
+EOF
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getNativeDefinition()
+    {
+        return $this->createDefinition();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function execute(Input $input, Output $output)
+    {
+        $output->describe($this->getConsole(), [
+            'raw_text'  => $input->getOption('raw'),
+            'namespace' => $input->getArgument('namespace'),
+        ]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    private function createDefinition()
+    {
+        return new InputDefinition([
+            new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
+            new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list')
+        ]);
+    }
+}

+ 110 - 0
thinkphp/library/think/console/command/Make.php

@@ -0,0 +1,110 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 刘志淳 <chun@engineer.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\command;
+
+use think\App;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+
+abstract class Make extends Command
+{
+
+    protected $type;
+
+    abstract protected function getStub();
+
+    protected function configure()
+    {
+        $this->addArgument('name', Argument::REQUIRED, "The name of the class");
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+
+        $name = trim($input->getArgument('name'));
+
+        $classname = $this->getClassName($name);
+
+        $pathname = $this->getPathName($classname);
+
+        if (is_file($pathname)) {
+            $output->writeln('<error>' . $this->type . ' already exists!</error>');
+            return false;
+        }
+
+        if (!is_dir(dirname($pathname))) {
+            mkdir(strtolower(dirname($pathname)), 0755, true);
+        }
+
+        file_put_contents($pathname, $this->buildClass($classname));
+
+        $output->writeln('<info>' . $this->type . ' created successfully.</info>');
+
+    }
+
+    protected function buildClass($name)
+    {
+        $stub = file_get_contents($this->getStub());
+
+        $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\');
+
+        $class = str_replace($namespace . '\\', '', $name);
+
+        return str_replace(['{%className%}', '{%namespace%}', '{%app_namespace%}'], [
+            $class,
+            $namespace,
+            App::$namespace,
+        ], $stub);
+
+    }
+
+    protected function getPathName($name)
+    {
+        $name = str_replace(App::$namespace . '\\', '', $name);
+
+        return APP_PATH . str_replace('\\', '/', $name) . '.php';
+    }
+
+    protected function getClassName($name)
+    {
+        $appNamespace = App::$namespace;
+
+        if (strpos($name, $appNamespace . '\\') === 0) {
+            return $name;
+        }
+
+        if (Config::get('app_multi_module')) {
+            if (strpos($name, '/')) {
+                list($module, $name) = explode('/', $name, 2);
+            } else {
+                $module = 'common';
+            }
+        } else {
+            $module = null;
+        }
+
+        if (strpos($name, '/') !== false) {
+            $name = str_replace('/', '\\', $name);
+        }
+
+        return $this->getNamespace($appNamespace, $module) . '\\' . $name;
+    }
+
+    protected function getNamespace($appNamespace, $module)
+    {
+        return $module ? ($appNamespace . '\\' . $module) : $appNamespace;
+    }
+
+}

+ 50 - 0
thinkphp/library/think/console/command/make/Controller.php

@@ -0,0 +1,50 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 刘志淳 <chun@engineer.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\Config;
+use think\console\command\Make;
+use think\console\input\Option;
+
+class Controller extends Make
+{
+
+    protected $type = "Controller";
+
+    protected function configure()
+    {
+        parent::configure();
+        $this->setName('make:controller')
+            ->addOption('plain', null, Option::VALUE_NONE, 'Generate an empty controller class.')
+            ->setDescription('Create a new resource controller class');
+    }
+
+    protected function getStub()
+    {
+        if ($this->input->getOption('plain')) {
+            return __DIR__ . '/stubs/controller.plain.stub';
+        }
+
+        return __DIR__ . '/stubs/controller.stub';
+    }
+
+    protected function getClassName($name)
+    {
+        return parent::getClassName($name) . (Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : '');
+    }
+
+    protected function getNamespace($appNamespace, $module)
+    {
+        return parent::getNamespace($appNamespace, $module) . '\controller';
+    }
+
+}

+ 36 - 0
thinkphp/library/think/console/command/make/Model.php

@@ -0,0 +1,36 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 刘志淳 <chun@engineer.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\command\make;
+
+use think\console\command\Make;
+
+class Model extends Make
+{
+    protected $type = "Model";
+
+    protected function configure()
+    {
+        parent::configure();
+        $this->setName('make:model')
+            ->setDescription('Create a new model class');
+    }
+
+    protected function getStub()
+    {
+        return __DIR__ . '/stubs/model.stub';
+    }
+
+    protected function getNamespace($appNamespace, $module)
+    {
+        return parent::getNamespace($appNamespace, $module) . '\model';
+    }
+}

+ 10 - 0
thinkphp/library/think/console/command/make/stubs/controller.plain.stub

@@ -0,0 +1,10 @@
+<?php
+
+namespace {%namespace%};
+
+use think\Controller;
+
+class {%className%} extends Controller
+{
+    //
+}

+ 85 - 0
thinkphp/library/think/console/command/make/stubs/controller.stub

@@ -0,0 +1,85 @@
+<?php
+
+namespace {%namespace%};
+
+use think\Controller;
+use think\Request;
+
+class {%className%} extends Controller
+{
+    /**
+     * 显示资源列表
+     *
+     * @return \think\Response
+     */
+    public function index()
+    {
+        //
+    }
+
+    /**
+     * 显示创建资源表单页.
+     *
+     * @return \think\Response
+     */
+    public function create()
+    {
+        //
+    }
+
+    /**
+     * 保存新建的资源
+     *
+     * @param  \think\Request  $request
+     * @return \think\Response
+     */
+    public function save(Request $request)
+    {
+        //
+    }
+
+    /**
+     * 显示指定的资源
+     *
+     * @param  int  $id
+     * @return \think\Response
+     */
+    public function read($id)
+    {
+        //
+    }
+
+    /**
+     * 显示编辑资源表单页.
+     *
+     * @param  int  $id
+     * @return \think\Response
+     */
+    public function edit($id)
+    {
+        //
+    }
+
+    /**
+     * 保存更新的资源
+     *
+     * @param  \think\Request  $request
+     * @param  int  $id
+     * @return \think\Response
+     */
+    public function update(Request $request, $id)
+    {
+        //
+    }
+
+    /**
+     * 删除指定资源
+     *
+     * @param  int  $id
+     * @return \think\Response
+     */
+    public function delete($id)
+    {
+        //
+    }
+}

+ 10 - 0
thinkphp/library/think/console/command/make/stubs/model.stub

@@ -0,0 +1,10 @@
+<?php
+
+namespace {%namespace%};
+
+use think\Model;
+
+class {%className%} extends Model
+{
+    //
+}

+ 294 - 0
thinkphp/library/think/console/command/optimize/Autoload.php

@@ -0,0 +1,294 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+namespace think\console\command\optimize;
+
+use think\App;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+
+class Autoload extends Command
+{
+
+    protected function configure()
+    {
+        $this->setName('optimize:autoload')
+            ->setDescription('Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+
+        $classmapFile = <<<EOF
+<?php
+/**
+ * 类库映射
+ */
+
+return [
+
+EOF;
+
+        $namespacesToScan = [
+            App::$namespace . '\\' => realpath(rtrim(APP_PATH)),
+            'think\\'              => LIB_PATH . 'think',
+            'behavior\\'           => LIB_PATH . 'behavior',
+            'traits\\'             => LIB_PATH . 'traits',
+            ''                     => realpath(rtrim(EXTEND_PATH)),
+        ];
+
+        $root_namespace = Config::get('root_namespace');
+        foreach ($root_namespace as $namespace => $dir) {
+            $namespacesToScan[$namespace . '\\'] = realpath($dir);
+        }
+
+        krsort($namespacesToScan);
+        $classMap = [];
+        foreach ($namespacesToScan as $namespace => $dir) {
+
+            if (!is_dir($dir)) {
+                continue;
+            }
+
+            $namespaceFilter = $namespace === '' ? null : $namespace;
+            $classMap        = $this->addClassMapCode($dir, $namespaceFilter, $classMap);
+        }
+
+        ksort($classMap);
+        foreach ($classMap as $class => $code) {
+            $classmapFile .= '    ' . var_export($class, true) . ' => ' . $code;
+        }
+        $classmapFile .= "];\n";
+
+        if (!is_dir(RUNTIME_PATH)) {
+            @mkdir(RUNTIME_PATH, 0755, true);
+        }
+
+        file_put_contents(RUNTIME_PATH . 'classmap' . EXT, $classmapFile);
+
+        $output->writeln('<info>Succeed!</info>');
+    }
+
+    protected function addClassMapCode($dir, $namespace, $classMap)
+    {
+        foreach ($this->createMap($dir, $namespace) as $class => $path) {
+
+            $pathCode = $this->getPathCode($path) . ",\n";
+
+            if (!isset($classMap[$class])) {
+                $classMap[$class] = $pathCode;
+            } elseif ($classMap[$class] !== $pathCode && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($classMap[$class] . ' ' . $path, '\\', '/'))) {
+                $this->output->writeln(
+                    '<warning>Warning: Ambiguous class resolution, "' . $class . '"' .
+                    ' was found in both "' . str_replace(["',\n"], [
+                        '',
+                    ], $classMap[$class]) . '" and "' . $path . '", the first will be used.</warning>'
+                );
+            }
+        }
+        return $classMap;
+    }
+
+    protected function getPathCode($path)
+    {
+
+        $baseDir    = '';
+        $libPath    = $this->normalizePath(realpath(LIB_PATH));
+        $appPath    = $this->normalizePath(realpath(APP_PATH));
+        $extendPath = $this->normalizePath(realpath(EXTEND_PATH));
+        $rootPath   = $this->normalizePath(realpath(ROOT_PATH));
+        $path       = $this->normalizePath($path);
+
+        if ($libPath !== null && strpos($path, $libPath . '/') === 0) {
+            $path    = substr($path, strlen(LIB_PATH));
+            $baseDir = 'LIB_PATH';
+        } elseif ($appPath !== null && strpos($path, $appPath . '/') === 0) {
+            $path    = substr($path, strlen($appPath) + 1);
+            $baseDir = 'APP_PATH';
+        } elseif ($extendPath !== null && strpos($path, $extendPath . '/') === 0) {
+            $path    = substr($path, strlen($extendPath) + 1);
+            $baseDir = 'EXTEND_PATH';
+        } elseif ($rootPath !== null && strpos($path, $rootPath . '/') === 0) {
+            $path    = substr($path, strlen($rootPath) + 1);
+            $baseDir = 'ROOT_PATH';
+        }
+
+        if ($path !== false) {
+            $baseDir .= " . ";
+        }
+
+        return $baseDir . (($path !== false) ? var_export($path, true) : "");
+    }
+
+    protected function normalizePath($path)
+    {
+        if ($path === false) {
+            return;
+        }
+        $parts    = [];
+        $path     = strtr($path, '\\', '/');
+        $prefix   = '';
+        $absolute = false;
+
+        if (preg_match('{^([0-9a-z]+:(?://(?:[a-z]:)?)?)}i', $path, $match)) {
+            $prefix = $match[1];
+            $path   = substr($path, strlen($prefix));
+        }
+
+        if (substr($path, 0, 1) === '/') {
+            $absolute = true;
+            $path     = substr($path, 1);
+        }
+
+        $up = false;
+        foreach (explode('/', $path) as $chunk) {
+            if ('..' === $chunk && ($absolute || $up)) {
+                array_pop($parts);
+                $up = !(empty($parts) || '..' === end($parts));
+            } elseif ('.' !== $chunk && '' !== $chunk) {
+                $parts[] = $chunk;
+                $up      = '..' !== $chunk;
+            }
+        }
+
+        return $prefix . ($absolute ? '/' : '') . implode('/', $parts);
+    }
+
+    protected function createMap($path, $namespace = null)
+    {
+        if (is_string($path)) {
+            if (is_file($path)) {
+                $path = [new \SplFileInfo($path)];
+            } elseif (is_dir($path)) {
+
+                $objects = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::SELF_FIRST);
+
+                $path = [];
+
+                /** @var \SplFileInfo $object */
+                foreach ($objects as $object) {
+                    if ($object->isFile() && $object->getExtension() == 'php') {
+                        $path[] = $object;
+                    }
+                }
+            } else {
+                throw new \RuntimeException(
+                    'Could not scan for classes inside "' . $path .
+                    '" which does not appear to be a file nor a folder'
+                );
+            }
+        }
+
+        $map = [];
+
+        /** @var \SplFileInfo $file */
+        foreach ($path as $file) {
+            $filePath = $file->getRealPath();
+
+            if (pathinfo($filePath, PATHINFO_EXTENSION) != 'php') {
+                continue;
+            }
+
+            $classes = $this->findClasses($filePath);
+
+            foreach ($classes as $class) {
+                if (null !== $namespace && 0 !== strpos($class, $namespace)) {
+                    continue;
+                }
+
+                if (!isset($map[$class])) {
+                    $map[$class] = $filePath;
+                } elseif ($map[$class] !== $filePath && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($map[$class] . ' ' . $filePath, '\\', '/'))) {
+                    $this->output->writeln(
+                        '<warning>Warning: Ambiguous class resolution, "' . $class . '"' .
+                        ' was found in both "' . $map[$class] . '" and "' . $filePath . '", the first will be used.</warning>'
+                    );
+                }
+            }
+        }
+
+        return $map;
+    }
+
+    protected function findClasses($path)
+    {
+        $extraTypes = '|trait';
+
+        $contents = @php_strip_whitespace($path);
+        if (!$contents) {
+            if (!file_exists($path)) {
+                $message = 'File at "%s" does not exist, check your classmap definitions';
+            } elseif (!is_readable($path)) {
+                $message = 'File at "%s" is not readable, check its permissions';
+            } elseif ('' === trim(file_get_contents($path))) {
+                return [];
+            } else {
+                $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted';
+            }
+            $error = error_get_last();
+            if (isset($error['message'])) {
+                $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message'];
+            }
+            throw new \RuntimeException(sprintf($message, $path));
+        }
+
+        if (!preg_match('{\b(?:class|interface' . $extraTypes . ')\s}i', $contents)) {
+            return [];
+        }
+
+        // strip heredocs/nowdocs
+        $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents);
+        // strip strings
+        $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents);
+        // strip leading non-php code if needed
+        if (substr($contents, 0, 2) !== '<?') {
+            $contents = preg_replace('{^.+?<\?}s', '<?', $contents, 1, $replacements);
+            if ($replacements === 0) {
+                return [];
+            }
+        }
+        // strip non-php blocks in the file
+        $contents = preg_replace('{\?>.+<\?}s', '?><?', $contents);
+        // strip trailing non-php code if needed
+        $pos = strrpos($contents, '?>');
+        if (false !== $pos && false === strpos(substr($contents, $pos), '<?')) {
+            $contents = substr($contents, 0, $pos);
+        }
+
+        preg_match_all('{
+            (?:
+                 \b(?<![\$:>])(?P<type>class|interface' . $extraTypes . ') \s++ (?P<name>[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+)
+               | \b(?<![\$:>])(?P<ns>namespace) (?P<nsname>\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;]
+            )
+        }ix', $contents, $matches);
+
+        $classes   = [];
+        $namespace = '';
+
+        for ($i = 0, $len = count($matches['type']); $i < $len; $i++) {
+            if (!empty($matches['ns'][$i])) {
+                $namespace = str_replace([' ', "\t", "\r", "\n"], '', $matches['nsname'][$i]) . '\\';
+            } else {
+                $name = $matches['name'][$i];
+                if ($name[0] === ':') {
+                    $name = 'xhp' . substr(str_replace(['-', ':'], ['_', '__'], $name), 1);
+                } elseif ($matches['type'][$i] === 'enum') {
+                    $name = rtrim($name, ':');
+                }
+                $classes[] = ltrim($namespace . $name, '\\');
+            }
+        }
+
+        return $classes;
+    }
+
+}

+ 93 - 0
thinkphp/library/think/console/command/optimize/Config.php

@@ -0,0 +1,93 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+namespace think\console\command\optimize;
+
+use think\Config as ThinkConfig;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+
+class Config extends Command
+{
+    /** @var  Output */
+    protected $output;
+
+    protected function configure()
+    {
+        $this->setName('optimize:config')
+            ->addArgument('module', Argument::OPTIONAL, 'Build module config cache .')
+            ->setDescription('Build config and common file cache.');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        if ($input->getArgument('module')) {
+            $module = $input->getArgument('module') . DS;
+        } else {
+            $module = '';
+        }
+
+        $content = '<?php ' . PHP_EOL . $this->buildCacheContent($module);
+
+        if (!is_dir(RUNTIME_PATH . $module)) {
+            @mkdir(RUNTIME_PATH . $module, 0755, true);
+        }
+
+        file_put_contents(RUNTIME_PATH . $module . 'init' . EXT, $content);
+
+        $output->writeln('<info>Succeed!</info>');
+    }
+
+    protected function buildCacheContent($module)
+    {
+        $content = '';
+        $path    = realpath(APP_PATH . $module) . DS;
+
+        if ($module) {
+            // 加载模块配置
+            $config = ThinkConfig::load(CONF_PATH . $module . 'config' . CONF_EXT);
+
+            // 读取数据库配置文件
+            $filename = CONF_PATH . $module . 'database' . CONF_EXT;
+            ThinkConfig::load($filename, 'database');
+
+            // 加载应用状态配置
+            if ($config['app_status']) {
+                $config = ThinkConfig::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT);
+            }
+            // 读取扩展配置文件
+            if (is_dir(CONF_PATH . $module . 'extra')) {
+                $dir   = CONF_PATH . $module . 'extra';
+                $files = scandir($dir);
+                foreach ($files as $file) {
+                    if (strpos($file, CONF_EXT)) {
+                        $filename = $dir . DS . $file;
+                        ThinkConfig::load($filename, pathinfo($file, PATHINFO_FILENAME));
+                    }
+                }
+            }
+        }
+
+        // 加载行为扩展文件
+        if (is_file(CONF_PATH . $module . 'tags' . EXT)) {
+            $content .= '\think\Hook::import(' . (var_export(include CONF_PATH . $module . 'tags' . EXT, true)) . ');' . PHP_EOL;
+        }
+
+        // 加载公共文件
+        if (is_file($path . 'common' . EXT)) {
+            $content .= substr(php_strip_whitespace($path . 'common' . EXT), 5) . PHP_EOL;
+        }
+
+        $content .= '\think\Config::set(' . var_export(ThinkConfig::get(), true) . ');';
+        return $content;
+    }
+}

+ 75 - 0
thinkphp/library/think/console/command/optimize/Route.php

@@ -0,0 +1,75 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+namespace think\console\command\optimize;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+
+class Route extends Command
+{
+    /** @var  Output */
+    protected $output;
+
+    protected function configure()
+    {
+        $this->setName('optimize:route')
+            ->setDescription('Build route cache.');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+
+        if (!is_dir(RUNTIME_PATH)) {
+            @mkdir(RUNTIME_PATH, 0755, true);
+        }
+
+        file_put_contents(RUNTIME_PATH . 'route.php', $this->buildRouteCache());
+        $output->writeln('<info>Succeed!</info>');
+    }
+
+    protected function buildRouteCache()
+    {
+        $files = \think\Config::get('route_config_file');
+        foreach ($files as $file) {
+            if (is_file(CONF_PATH . $file . CONF_EXT)) {
+                $config = include CONF_PATH . $file . CONF_EXT;
+                if (is_array($config)) {
+                    \think\Route::import($config);
+                }
+            }
+        }
+        $rules = \think\Route::rules(true);
+        array_walk_recursive($rules, [$this, 'buildClosure']);
+        $content = '<?php ' . PHP_EOL . 'return ';
+        $content .= var_export($rules, true) . ';';
+        $content = str_replace(['\'[__start__', '__end__]\''], '', stripcslashes($content));
+        return $content;
+    }
+
+    protected function buildClosure(&$value)
+    {
+        if ($value instanceof \Closure) {
+            $reflection = new \ReflectionFunction($value);
+            $startLine  = $reflection->getStartLine();
+            $endLine    = $reflection->getEndLine();
+            $file       = $reflection->getFileName();
+            $item       = file($file);
+            $content    = '';
+            for ($i = $startLine - 1; $i <= $endLine - 1; $i++) {
+                $content .= $item[$i];
+            }
+            $start = strpos($content, 'function');
+            $end   = strrpos($content, '}');
+            $value = '[__start__' . substr($content, $start, $end - $start + 1) . '__end__]';
+        }
+    }
+}

+ 118 - 0
thinkphp/library/think/console/command/optimize/Schema.php

@@ -0,0 +1,118 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+namespace think\console\command\optimize;
+
+use think\App;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Db;
+
+class Schema extends Command
+{
+    /** @var  Output */
+    protected $output;
+
+    protected function configure()
+    {
+        $this->setName('optimize:schema')
+            ->addOption('config', null, Option::VALUE_REQUIRED, 'db config .')
+            ->addOption('db', null, Option::VALUE_REQUIRED, 'db name .')
+            ->addOption('table', null, Option::VALUE_REQUIRED, 'table name .')
+            ->addOption('module', null, Option::VALUE_REQUIRED, 'module name .')
+            ->setDescription('Build database schema cache.');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        if (!is_dir(RUNTIME_PATH . 'schema')) {
+            @mkdir(RUNTIME_PATH . 'schema', 0755, true);
+        }
+        $config = [];
+        if ($input->hasOption('config')) {
+            $config = $input->getOption('config');
+        }
+        if ($input->hasOption('module')) {
+            $module = $input->getOption('module');
+            // 读取模型
+            $path = APP_PATH . $module . DS . 'model';
+            $list = is_dir($path) ? scandir($path) : [];
+            $app  = App::$namespace;
+            foreach ($list as $file) {
+                if (0 === strpos($file, '.')) {
+                    continue;
+                }
+                $class = '\\' . $app . '\\' . $module . '\\model\\' . pathinfo($file, PATHINFO_FILENAME);
+                $this->buildModelSchema($class);
+            }
+            $output->writeln('<info>Succeed!</info>');
+            return;
+        } elseif ($input->hasOption('table')) {
+            $table = $input->getOption('table');
+            if (!strpos($table, '.')) {
+                $dbName = Db::connect($config)->getConfig('database');
+            }
+            $tables[] = $table;
+        } elseif ($input->hasOption('db')) {
+            $dbName = $input->getOption('db');
+            $tables = Db::connect($config)->getTables($dbName);
+        } elseif (!\think\Config::get('app_multi_module')) {
+            $app  = App::$namespace;
+            $path = APP_PATH . 'model';
+            $list = is_dir($path) ? scandir($path) : [];
+            foreach ($list as $file) {
+                if (0 === strpos($file, '.')) {
+                    continue;
+                }
+                $class = '\\' . $app . '\\model\\' . pathinfo($file, PATHINFO_FILENAME);
+                $this->buildModelSchema($class);
+            }
+            $output->writeln('<info>Succeed!</info>');
+            return;
+        } else {
+            $tables = Db::connect($config)->getTables();
+        }
+
+        $db = isset($dbName) ? $dbName . '.' : '';
+        $this->buildDataBaseSchema($tables, $db, $config);
+
+        $output->writeln('<info>Succeed!</info>');
+    }
+
+    protected function buildModelSchema($class)
+    {
+        $reflect = new \ReflectionClass($class);
+        if (!$reflect->isAbstract() && $reflect->isSubclassOf('\think\Model')) {
+            $table   = $class::getTable();
+            $dbName  = $class::getConfig('database');
+            $content = '<?php ' . PHP_EOL . 'return ';
+            $info    = $class::getConnection()->getFields($table);
+            $content .= var_export($info, true) . ';';
+            file_put_contents(RUNTIME_PATH . 'schema' . DS . $dbName . '.' . $table . EXT, $content);
+        }
+    }
+
+    protected function buildDataBaseSchema($tables, $db, $config)
+    {
+        if ('' == $db) {
+            $dbName = Db::connect($config)->getConfig('database') . '.';
+        } else {
+            $dbName = $db;
+        }
+        foreach ($tables as $table) {
+            $content = '<?php ' . PHP_EOL . 'return ';
+            $info    = Db::connect($config)->getFields($db . $table);
+            $content .= var_export($info, true) . ';';
+            file_put_contents(RUNTIME_PATH . 'schema' . DS . $dbName . $table . EXT, $content);
+        }
+    }
+}

+ 115 - 0
thinkphp/library/think/console/input/Argument.php

@@ -0,0 +1,115 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+class Argument
+{
+
+    const REQUIRED = 1;
+    const OPTIONAL = 2;
+    const IS_ARRAY = 4;
+
+    private $name;
+    private $mode;
+    private $default;
+    private $description;
+
+    /**
+     * 构造方法
+     * @param string $name        参数名
+     * @param int    $mode        参数类型: self::REQUIRED 或者 self::OPTIONAL
+     * @param string $description 描述
+     * @param mixed  $default     默认值 (仅 self::OPTIONAL 类型有效)
+     * @throws \InvalidArgumentException
+     */
+    public function __construct($name, $mode = null, $description = '', $default = null)
+    {
+        if (null === $mode) {
+            $mode = self::OPTIONAL;
+        } elseif (!is_int($mode) || $mode > 7 || $mode < 1) {
+            throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode));
+        }
+
+        $this->name        = $name;
+        $this->mode        = $mode;
+        $this->description = $description;
+
+        $this->setDefault($default);
+    }
+
+    /**
+     * 获取参数名
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * 是否必须
+     * @return bool
+     */
+    public function isRequired()
+    {
+        return self::REQUIRED === (self::REQUIRED & $this->mode);
+    }
+
+    /**
+     * 该参数是否接受数组
+     * @return bool
+     */
+    public function isArray()
+    {
+        return self::IS_ARRAY === (self::IS_ARRAY & $this->mode);
+    }
+
+    /**
+     * 设置默认值
+     * @param mixed $default 默认值
+     * @throws \LogicException
+     */
+    public function setDefault($default = null)
+    {
+        if (self::REQUIRED === $this->mode && null !== $default) {
+            throw new \LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.');
+        }
+
+        if ($this->isArray()) {
+            if (null === $default) {
+                $default = [];
+            } elseif (!is_array($default)) {
+                throw new \LogicException('A default value for an array argument must be an array.');
+            }
+        }
+
+        $this->default = $default;
+    }
+
+    /**
+     * 获取默认值
+     * @return mixed
+     */
+    public function getDefault()
+    {
+        return $this->default;
+    }
+
+    /**
+     * 获取描述
+     * @return string
+     */
+    public function getDescription()
+    {
+        return $this->description;
+    }
+}

+ 375 - 0
thinkphp/library/think/console/input/Definition.php

@@ -0,0 +1,375 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+class Definition
+{
+
+    /**
+     * @var Argument[]
+     */
+    private $arguments;
+
+    private $requiredCount;
+    private $hasAnArrayArgument = false;
+    private $hasOptional;
+
+    /**
+     * @var Option[]
+     */
+    private $options;
+    private $shortcuts;
+
+    /**
+     * 构造方法
+     * @param array $definition
+     * @api
+     */
+    public function __construct(array $definition = [])
+    {
+        $this->setDefinition($definition);
+    }
+
+    /**
+     * 设置指令的定义
+     * @param array $definition 定义的数组
+     */
+    public function setDefinition(array $definition)
+    {
+        $arguments = [];
+        $options   = [];
+        foreach ($definition as $item) {
+            if ($item instanceof Option) {
+                $options[] = $item;
+            } else {
+                $arguments[] = $item;
+            }
+        }
+
+        $this->setArguments($arguments);
+        $this->setOptions($options);
+    }
+
+    /**
+     * 设置参数
+     * @param Argument[] $arguments 参数数组
+     */
+    public function setArguments($arguments = [])
+    {
+        $this->arguments          = [];
+        $this->requiredCount      = 0;
+        $this->hasOptional        = false;
+        $this->hasAnArrayArgument = false;
+        $this->addArguments($arguments);
+    }
+
+    /**
+     * 添加参数
+     * @param Argument[] $arguments 参数数组
+     * @api
+     */
+    public function addArguments($arguments = [])
+    {
+        if (null !== $arguments) {
+            foreach ($arguments as $argument) {
+                $this->addArgument($argument);
+            }
+        }
+    }
+
+    /**
+     * 添加一个参数
+     * @param Argument $argument 参数
+     * @throws \LogicException
+     */
+    public function addArgument(Argument $argument)
+    {
+        if (isset($this->arguments[$argument->getName()])) {
+            throw new \LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName()));
+        }
+
+        if ($this->hasAnArrayArgument) {
+            throw new \LogicException('Cannot add an argument after an array argument.');
+        }
+
+        if ($argument->isRequired() && $this->hasOptional) {
+            throw new \LogicException('Cannot add a required argument after an optional one.');
+        }
+
+        if ($argument->isArray()) {
+            $this->hasAnArrayArgument = true;
+        }
+
+        if ($argument->isRequired()) {
+            ++$this->requiredCount;
+        } else {
+            $this->hasOptional = true;
+        }
+
+        $this->arguments[$argument->getName()] = $argument;
+    }
+
+    /**
+     * 根据名称或者位置获取参数
+     * @param string|int $name 参数名或者位置
+     * @return Argument 参数
+     * @throws \InvalidArgumentException
+     */
+    public function getArgument($name)
+    {
+        if (!$this->hasArgument($name)) {
+            throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+        }
+
+        $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments;
+
+        return $arguments[$name];
+    }
+
+    /**
+     * 根据名称或位置检查是否具有某个参数
+     * @param string|int $name 参数名或者位置
+     * @return bool
+     * @api
+     */
+    public function hasArgument($name)
+    {
+        $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments;
+
+        return isset($arguments[$name]);
+    }
+
+    /**
+     * 获取所有的参数
+     * @return Argument[] 参数数组
+     */
+    public function getArguments()
+    {
+        return $this->arguments;
+    }
+
+    /**
+     * 获取参数数量
+     * @return int
+     */
+    public function getArgumentCount()
+    {
+        return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments);
+    }
+
+    /**
+     * 获取必填的参数的数量
+     * @return int
+     */
+    public function getArgumentRequiredCount()
+    {
+        return $this->requiredCount;
+    }
+
+    /**
+     * 获取参数默认值
+     * @return array
+     */
+    public function getArgumentDefaults()
+    {
+        $values = [];
+        foreach ($this->arguments as $argument) {
+            $values[$argument->getName()] = $argument->getDefault();
+        }
+
+        return $values;
+    }
+
+    /**
+     * 设置选项
+     * @param Option[] $options 选项数组
+     */
+    public function setOptions($options = [])
+    {
+        $this->options   = [];
+        $this->shortcuts = [];
+        $this->addOptions($options);
+    }
+
+    /**
+     * 添加选项
+     * @param Option[] $options 选项数组
+     * @api
+     */
+    public function addOptions($options = [])
+    {
+        foreach ($options as $option) {
+            $this->addOption($option);
+        }
+    }
+
+    /**
+     * 添加一个选项
+     * @param Option $option 选项
+     * @throws \LogicException
+     * @api
+     */
+    public function addOption(Option $option)
+    {
+        if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) {
+            throw new \LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
+        }
+
+        if ($option->getShortcut()) {
+            foreach (explode('|', $option->getShortcut()) as $shortcut) {
+                if (isset($this->shortcuts[$shortcut])
+                    && !$option->equals($this->options[$this->shortcuts[$shortcut]])
+                ) {
+                    throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut));
+                }
+            }
+        }
+
+        $this->options[$option->getName()] = $option;
+        if ($option->getShortcut()) {
+            foreach (explode('|', $option->getShortcut()) as $shortcut) {
+                $this->shortcuts[$shortcut] = $option->getName();
+            }
+        }
+    }
+
+    /**
+     * 根据名称获取选项
+     * @param string $name 选项名
+     * @return Option
+     * @throws \InvalidArgumentException
+     * @api
+     */
+    public function getOption($name)
+    {
+        if (!$this->hasOption($name)) {
+            throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name));
+        }
+
+        return $this->options[$name];
+    }
+
+    /**
+     * 根据名称检查是否有这个选项
+     * @param string $name 选项名
+     * @return bool
+     * @api
+     */
+    public function hasOption($name)
+    {
+        return isset($this->options[$name]);
+    }
+
+    /**
+     * 获取所有选项
+     * @return Option[]
+     * @api
+     */
+    public function getOptions()
+    {
+        return $this->options;
+    }
+
+    /**
+     * 根据名称检查某个选项是否有短名称
+     * @param string $name 短名称
+     * @return bool
+     */
+    public function hasShortcut($name)
+    {
+        return isset($this->shortcuts[$name]);
+    }
+
+    /**
+     * 根据短名称获取选项
+     * @param string $shortcut 短名称
+     * @return Option
+     */
+    public function getOptionForShortcut($shortcut)
+    {
+        return $this->getOption($this->shortcutToName($shortcut));
+    }
+
+    /**
+     * 获取所有选项的默认值
+     * @return array
+     */
+    public function getOptionDefaults()
+    {
+        $values = [];
+        foreach ($this->options as $option) {
+            $values[$option->getName()] = $option->getDefault();
+        }
+
+        return $values;
+    }
+
+    /**
+     * 根据短名称获取选项名
+     * @param string $shortcut 短名称
+     * @return string
+     * @throws \InvalidArgumentException
+     */
+    private function shortcutToName($shortcut)
+    {
+        if (!isset($this->shortcuts[$shortcut])) {
+            throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut));
+        }
+
+        return $this->shortcuts[$shortcut];
+    }
+
+    /**
+     * 获取该指令的介绍
+     * @param bool $short 是否简洁介绍
+     * @return string
+     */
+    public function getSynopsis($short = false)
+    {
+        $elements = [];
+
+        if ($short && $this->getOptions()) {
+            $elements[] = '[options]';
+        } elseif (!$short) {
+            foreach ($this->getOptions() as $option) {
+                $value = '';
+                if ($option->acceptValue()) {
+                    $value = sprintf(' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : '');
+                }
+
+                $shortcut   = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : '';
+                $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value);
+            }
+        }
+
+        if (count($elements) && $this->getArguments()) {
+            $elements[] = '[--]';
+        }
+
+        foreach ($this->getArguments() as $argument) {
+            $element = '<' . $argument->getName() . '>';
+            if (!$argument->isRequired()) {
+                $element = '[' . $element . ']';
+            } elseif ($argument->isArray()) {
+                $element .= ' (' . $element . ')';
+            }
+
+            if ($argument->isArray()) {
+                $element .= '...';
+            }
+
+            $elements[] = $element;
+        }
+
+        return implode(' ', $elements);
+    }
+}

+ 190 - 0
thinkphp/library/think/console/input/Option.php

@@ -0,0 +1,190 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\input;
+
+class Option
+{
+
+    const VALUE_NONE     = 1;
+    const VALUE_REQUIRED = 2;
+    const VALUE_OPTIONAL = 4;
+    const VALUE_IS_ARRAY = 8;
+
+    private $name;
+    private $shortcut;
+    private $mode;
+    private $default;
+    private $description;
+
+    /**
+     * 构造方法
+     * @param string       $name        选项名
+     * @param string|array $shortcut    短名称,多个用|隔开或者使用数组
+     * @param int          $mode        选项类型(可选类型为 self::VALUE_*)
+     * @param string       $description 描述
+     * @param mixed        $default     默认值 (类型为 self::VALUE_REQUIRED 或者 self::VALUE_NONE 的时候必须为null)
+     * @throws \InvalidArgumentException
+     */
+    public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null)
+    {
+        if (0 === strpos($name, '--')) {
+            $name = substr($name, 2);
+        }
+
+        if (empty($name)) {
+            throw new \InvalidArgumentException('An option name cannot be empty.');
+        }
+
+        if (empty($shortcut)) {
+            $shortcut = null;
+        }
+
+        if (null !== $shortcut) {
+            if (is_array($shortcut)) {
+                $shortcut = implode('|', $shortcut);
+            }
+            $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-'));
+            $shortcuts = array_filter($shortcuts);
+            $shortcut  = implode('|', $shortcuts);
+
+            if (empty($shortcut)) {
+                throw new \InvalidArgumentException('An option shortcut cannot be empty.');
+            }
+        }
+
+        if (null === $mode) {
+            $mode = self::VALUE_NONE;
+        } elseif (!is_int($mode) || $mode > 15 || $mode < 1) {
+            throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode));
+        }
+
+        $this->name        = $name;
+        $this->shortcut    = $shortcut;
+        $this->mode        = $mode;
+        $this->description = $description;
+
+        if ($this->isArray() && !$this->acceptValue()) {
+            throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.');
+        }
+
+        $this->setDefault($default);
+    }
+
+    /**
+     * 获取短名称
+     * @return string
+     */
+    public function getShortcut()
+    {
+        return $this->shortcut;
+    }
+
+    /**
+     * 获取选项名
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * 是否可以设置值
+     * @return bool 类型不是 self::VALUE_NONE 的时候返回true,其他均返回false
+     */
+    public function acceptValue()
+    {
+        return $this->isValueRequired() || $this->isValueOptional();
+    }
+
+    /**
+     * 是否必须
+     * @return bool 类型是 self::VALUE_REQUIRED 的时候返回true,其他均返回false
+     */
+    public function isValueRequired()
+    {
+        return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode);
+    }
+
+    /**
+     * 是否可选
+     * @return bool 类型是 self::VALUE_OPTIONAL 的时候返回true,其他均返回false
+     */
+    public function isValueOptional()
+    {
+        return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode);
+    }
+
+    /**
+     * 选项值是否接受数组
+     * @return bool 类型是 self::VALUE_IS_ARRAY 的时候返回true,其他均返回false
+     */
+    public function isArray()
+    {
+        return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode);
+    }
+
+    /**
+     * 设置默认值
+     * @param mixed $default 默认值
+     * @throws \LogicException
+     */
+    public function setDefault($default = null)
+    {
+        if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) {
+            throw new \LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.');
+        }
+
+        if ($this->isArray()) {
+            if (null === $default) {
+                $default = [];
+            } elseif (!is_array($default)) {
+                throw new \LogicException('A default value for an array option must be an array.');
+            }
+        }
+
+        $this->default = $this->acceptValue() ? $default : false;
+    }
+
+    /**
+     * 获取默认值
+     * @return mixed
+     */
+    public function getDefault()
+    {
+        return $this->default;
+    }
+
+    /**
+     * 获取描述文字
+     * @return string
+     */
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    /**
+     * 检查所给选项是否是当前这个
+     * @param Option $option
+     * @return bool
+     */
+    public function equals(Option $option)
+    {
+        return $option->getName() === $this->getName()
+        && $option->getShortcut() === $this->getShortcut()
+        && $option->getDefault() === $this->getDefault()
+        && $option->isArray() === $this->isArray()
+        && $option->isValueRequired() === $this->isValueRequired()
+        && $option->isValueOptional() === $this->isValueOptional();
+    }
+}

+ 340 - 0
thinkphp/library/think/console/output/Ask.php

@@ -0,0 +1,340 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+use think\console\Input;
+use think\console\Output;
+use think\console\output\question\Choice;
+use think\console\output\question\Confirmation;
+
+class Ask
+{
+    private static $stty;
+
+    private static $shell;
+
+    /** @var  Input */
+    protected $input;
+
+    /** @var  Output */
+    protected $output;
+
+    /** @var  Question */
+    protected $question;
+
+    public function __construct(Input $input, Output $output, Question $question)
+    {
+        $this->input    = $input;
+        $this->output   = $output;
+        $this->question = $question;
+    }
+
+    public function run()
+    {
+        if (!$this->input->isInteractive()) {
+            return $this->question->getDefault();
+        }
+
+        if (!$this->question->getValidator()) {
+            return $this->doAsk();
+        }
+
+        $that = $this;
+
+        $interviewer = function () use ($that) {
+            return $that->doAsk();
+        };
+
+        return $this->validateAttempts($interviewer);
+    }
+
+    protected function doAsk()
+    {
+        $this->writePrompt();
+
+        $inputStream  = STDIN;
+        $autocomplete = $this->question->getAutocompleterValues();
+
+        if (null === $autocomplete || !$this->hasSttyAvailable()) {
+            $ret = false;
+            if ($this->question->isHidden()) {
+                try {
+                    $ret = trim($this->getHiddenResponse($inputStream));
+                } catch (\RuntimeException $e) {
+                    if (!$this->question->isHiddenFallback()) {
+                        throw $e;
+                    }
+                }
+            }
+
+            if (false === $ret) {
+                $ret = fgets($inputStream, 4096);
+                if (false === $ret) {
+                    throw new \RuntimeException('Aborted');
+                }
+                $ret = trim($ret);
+            }
+        } else {
+            $ret = trim($this->autocomplete($inputStream));
+        }
+
+        $ret = strlen($ret) > 0 ? $ret : $this->question->getDefault();
+
+        if ($normalizer = $this->question->getNormalizer()) {
+            return $normalizer($ret);
+        }
+
+        return $ret;
+    }
+
+    private function autocomplete($inputStream)
+    {
+        $autocomplete = $this->question->getAutocompleterValues();
+        $ret          = '';
+
+        $i          = 0;
+        $ofs        = -1;
+        $matches    = $autocomplete;
+        $numMatches = count($matches);
+
+        $sttyMode = shell_exec('stty -g');
+
+        shell_exec('stty -icanon -echo');
+
+        while (!feof($inputStream)) {
+            $c = fread($inputStream, 1);
+
+            if ("\177" === $c) {
+                if (0 === $numMatches && 0 !== $i) {
+                    --$i;
+                    $this->output->write("\033[1D");
+                }
+
+                if ($i === 0) {
+                    $ofs        = -1;
+                    $matches    = $autocomplete;
+                    $numMatches = count($matches);
+                } else {
+                    $numMatches = 0;
+                }
+
+                $ret = substr($ret, 0, $i);
+            } elseif ("\033" === $c) {
+                $c .= fread($inputStream, 2);
+
+                if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
+                    if ('A' === $c[2] && -1 === $ofs) {
+                        $ofs = 0;
+                    }
+
+                    if (0 === $numMatches) {
+                        continue;
+                    }
+
+                    $ofs += ('A' === $c[2]) ? -1 : 1;
+                    $ofs = ($numMatches + $ofs) % $numMatches;
+                }
+            } elseif (ord($c) < 32) {
+                if ("\t" === $c || "\n" === $c) {
+                    if ($numMatches > 0 && -1 !== $ofs) {
+                        $ret = $matches[$ofs];
+                        $this->output->write(substr($ret, $i));
+                        $i = strlen($ret);
+                    }
+
+                    if ("\n" === $c) {
+                        $this->output->write($c);
+                        break;
+                    }
+
+                    $numMatches = 0;
+                }
+
+                continue;
+            } else {
+                $this->output->write($c);
+                $ret .= $c;
+                ++$i;
+
+                $numMatches = 0;
+                $ofs        = 0;
+
+                foreach ($autocomplete as $value) {
+                    if (0 === strpos($value, $ret) && $i !== strlen($value)) {
+                        $matches[$numMatches++] = $value;
+                    }
+                }
+            }
+
+            $this->output->write("\033[K");
+
+            if ($numMatches > 0 && -1 !== $ofs) {
+                $this->output->write("\0337");
+                $this->output->highlight(substr($matches[$ofs], $i));
+                $this->output->write("\0338");
+            }
+        }
+
+        shell_exec(sprintf('stty %s', $sttyMode));
+
+        return $ret;
+    }
+
+    protected function getHiddenResponse($inputStream)
+    {
+        if ('\\' === DIRECTORY_SEPARATOR) {
+            $exe = __DIR__ . '/../bin/hiddeninput.exe';
+
+            $value = rtrim(shell_exec($exe));
+            $this->output->writeln('');
+
+            if (isset($tmpExe)) {
+                unlink($tmpExe);
+            }
+
+            return $value;
+        }
+
+        if ($this->hasSttyAvailable()) {
+            $sttyMode = shell_exec('stty -g');
+
+            shell_exec('stty -echo');
+            $value = fgets($inputStream, 4096);
+            shell_exec(sprintf('stty %s', $sttyMode));
+
+            if (false === $value) {
+                throw new \RuntimeException('Aborted');
+            }
+
+            $value = trim($value);
+            $this->output->writeln('');
+
+            return $value;
+        }
+
+        if (false !== $shell = $this->getShell()) {
+            $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword';
+            $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
+            $value   = rtrim(shell_exec($command));
+            $this->output->writeln('');
+
+            return $value;
+        }
+
+        throw new \RuntimeException('Unable to hide the response.');
+    }
+
+    protected function validateAttempts($interviewer)
+    {
+        /** @var \Exception $error */
+        $error    = null;
+        $attempts = $this->question->getMaxAttempts();
+        while (null === $attempts || $attempts--) {
+            if (null !== $error) {
+                $this->output->error($error->getMessage());
+            }
+
+            try {
+                return call_user_func($this->question->getValidator(), $interviewer());
+            } catch (\Exception $error) {
+            }
+        }
+
+        throw $error;
+    }
+
+    /**
+     * 显示问题的提示信息
+     */
+    protected function writePrompt()
+    {
+        $text    = $this->question->getQuestion();
+        $default = $this->question->getDefault();
+
+        switch (true) {
+            case null === $default:
+                $text = sprintf(' <info>%s</info>:', $text);
+
+                break;
+
+            case $this->question instanceof Confirmation:
+                $text = sprintf(' <info>%s (yes/no)</info> [<comment>%s</comment>]:', $text, $default ? 'yes' : 'no');
+
+                break;
+
+            case $this->question instanceof Choice && $this->question->isMultiselect():
+                $choices = $this->question->getChoices();
+                $default = explode(',', $default);
+
+                foreach ($default as $key => $value) {
+                    $default[$key] = $choices[trim($value)];
+                }
+
+                $text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, implode(', ', $default));
+
+                break;
+
+            case $this->question instanceof Choice:
+                $choices = $this->question->getChoices();
+                $text    = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, $choices[$default]);
+
+                break;
+
+            default:
+                $text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, $default);
+        }
+
+        $this->output->writeln($text);
+
+        if ($this->question instanceof Choice) {
+            $width = max(array_map('strlen', array_keys($this->question->getChoices())));
+
+            foreach ($this->question->getChoices() as $key => $value) {
+                $this->output->writeln(sprintf("  [<comment>%-${width}s</comment>] %s", $key, $value));
+            }
+        }
+
+        $this->output->write(' > ');
+    }
+
+    private function getShell()
+    {
+        if (null !== self::$shell) {
+            return self::$shell;
+        }
+
+        self::$shell = false;
+
+        if (file_exists('/usr/bin/env')) {
+            $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
+            foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) {
+                if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
+                    self::$shell = $sh;
+                    break;
+                }
+            }
+        }
+
+        return self::$shell;
+    }
+
+    private function hasSttyAvailable()
+    {
+        if (null !== self::$stty) {
+            return self::$stty;
+        }
+
+        exec('stty 2>&1', $output, $exitcode);
+
+        return self::$stty = $exitcode === 0;
+    }
+}

+ 319 - 0
thinkphp/library/think/console/output/Descriptor.php

@@ -0,0 +1,319 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+use think\Console;
+use think\console\Command;
+use think\console\input\Argument as InputArgument;
+use think\console\input\Definition as InputDefinition;
+use think\console\input\Option as InputOption;
+use think\console\Output;
+use think\console\output\descriptor\Console as ConsoleDescription;
+
+class Descriptor
+{
+
+    /**
+     * @var Output
+     */
+    protected $output;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function describe(Output $output, $object, array $options = [])
+    {
+        $this->output = $output;
+
+        switch (true) {
+            case $object instanceof InputArgument:
+                $this->describeInputArgument($object, $options);
+                break;
+            case $object instanceof InputOption:
+                $this->describeInputOption($object, $options);
+                break;
+            case $object instanceof InputDefinition:
+                $this->describeInputDefinition($object, $options);
+                break;
+            case $object instanceof Command:
+                $this->describeCommand($object, $options);
+                break;
+            case $object instanceof Console:
+                $this->describeConsole($object, $options);
+                break;
+            default:
+                throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
+        }
+    }
+
+    /**
+     * 输出内容
+     * @param string $content
+     * @param bool   $decorated
+     */
+    protected function write($content, $decorated = false)
+    {
+        $this->output->write($content, false, $decorated ? Output::OUTPUT_NORMAL : Output::OUTPUT_RAW);
+    }
+
+    /**
+     * 描述参数
+     * @param InputArgument $argument
+     * @param array         $options
+     * @return void
+     */
+    protected function describeInputArgument(InputArgument $argument, array $options = [])
+    {
+        if (null !== $argument->getDefault()
+            && (!is_array($argument->getDefault())
+                || count($argument->getDefault()))
+        ) {
+            $default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault()));
+        } else {
+            $default = '';
+        }
+
+        $totalWidth   = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName());
+        $spacingWidth = $totalWidth - strlen($argument->getName()) + 2;
+
+        $this->writeText(sprintf("  <info>%s</info>%s%s%s", $argument->getName(), str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + <info> + </info> + 2 spaces
+            preg_replace('/\s*\R\s*/', PHP_EOL . str_repeat(' ', $totalWidth + 17), $argument->getDescription()), $default), $options);
+    }
+
+    /**
+     * 描述选项
+     * @param InputOption $option
+     * @param array       $options
+     * @return void
+     */
+    protected function describeInputOption(InputOption $option, array $options = [])
+    {
+        if ($option->acceptValue() && null !== $option->getDefault()
+            && (!is_array($option->getDefault())
+                || count($option->getDefault()))
+        ) {
+            $default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($option->getDefault()));
+        } else {
+            $default = '';
+        }
+
+        $value = '';
+        if ($option->acceptValue()) {
+            $value = '=' . strtoupper($option->getName());
+
+            if ($option->isValueOptional()) {
+                $value = '[' . $value . ']';
+            }
+        }
+
+        $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]);
+        $synopsis   = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : '    ', sprintf('--%s%s', $option->getName(), $value));
+
+        $spacingWidth = $totalWidth - strlen($synopsis) + 2;
+
+        $this->writeText(sprintf("  <info>%s</info>%s%s%s%s", $synopsis, str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + <info> + </info> + 2 spaces
+            preg_replace('/\s*\R\s*/', "\n" . str_repeat(' ', $totalWidth + 17), $option->getDescription()), $default, $option->isArray() ? '<comment> (multiple values allowed)</comment>' : ''), $options);
+    }
+
+    /**
+     * 描述输入
+     * @param InputDefinition $definition
+     * @param array           $options
+     * @return void
+     */
+    protected function describeInputDefinition(InputDefinition $definition, array $options = [])
+    {
+        $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
+        foreach ($definition->getArguments() as $argument) {
+            $totalWidth = max($totalWidth, strlen($argument->getName()));
+        }
+
+        if ($definition->getArguments()) {
+            $this->writeText('<comment>Arguments:</comment>', $options);
+            $this->writeText("\n");
+            foreach ($definition->getArguments() as $argument) {
+                $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth]));
+                $this->writeText("\n");
+            }
+        }
+
+        if ($definition->getArguments() && $definition->getOptions()) {
+            $this->writeText("\n");
+        }
+
+        if ($definition->getOptions()) {
+            $laterOptions = [];
+
+            $this->writeText('<comment>Options:</comment>', $options);
+            foreach ($definition->getOptions() as $option) {
+                if (strlen($option->getShortcut()) > 1) {
+                    $laterOptions[] = $option;
+                    continue;
+                }
+                $this->writeText("\n");
+                $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
+            }
+            foreach ($laterOptions as $option) {
+                $this->writeText("\n");
+                $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
+            }
+        }
+    }
+
+    /**
+     * 描述指令
+     * @param Command $command
+     * @param array   $options
+     * @return void
+     */
+    protected function describeCommand(Command $command, array $options = [])
+    {
+        $command->getSynopsis(true);
+        $command->getSynopsis(false);
+        $command->mergeConsoleDefinition(false);
+
+        $this->writeText('<comment>Usage:</comment>', $options);
+        foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
+            $this->writeText("\n");
+            $this->writeText('  ' . $usage, $options);
+        }
+        $this->writeText("\n");
+
+        $definition = $command->getNativeDefinition();
+        if ($definition->getOptions() || $definition->getArguments()) {
+            $this->writeText("\n");
+            $this->describeInputDefinition($definition, $options);
+            $this->writeText("\n");
+        }
+
+        if ($help = $command->getProcessedHelp()) {
+            $this->writeText("\n");
+            $this->writeText('<comment>Help:</comment>', $options);
+            $this->writeText("\n");
+            $this->writeText(' ' . str_replace("\n", "\n ", $help), $options);
+            $this->writeText("\n");
+        }
+    }
+
+    /**
+     * 描述控制台
+     * @param Console $console
+     * @param array   $options
+     * @return void
+     */
+    protected function describeConsole(Console $console, array $options = [])
+    {
+        $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
+        $description        = new ConsoleDescription($console, $describedNamespace);
+
+        if (isset($options['raw_text']) && $options['raw_text']) {
+            $width = $this->getColumnWidth($description->getCommands());
+
+            foreach ($description->getCommands() as $command) {
+                $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options);
+                $this->writeText("\n");
+            }
+        } else {
+            if ('' != $help = $console->getHelp()) {
+                $this->writeText("$help\n\n", $options);
+            }
+
+            $this->writeText("<comment>Usage:</comment>\n", $options);
+            $this->writeText("  command [options] [arguments]\n\n", $options);
+
+            $this->describeInputDefinition(new InputDefinition($console->getDefinition()->getOptions()), $options);
+
+            $this->writeText("\n");
+            $this->writeText("\n");
+
+            $width = $this->getColumnWidth($description->getCommands());
+
+            if ($describedNamespace) {
+                $this->writeText(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
+            } else {
+                $this->writeText('<comment>Available commands:</comment>', $options);
+            }
+
+            // add commands by namespace
+            foreach ($description->getNamespaces() as $namespace) {
+                if (!$describedNamespace && ConsoleDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
+                    $this->writeText("\n");
+                    $this->writeText(' <comment>' . $namespace['id'] . '</comment>', $options);
+                }
+
+                foreach ($namespace['commands'] as $name) {
+                    $this->writeText("\n");
+                    $spacingWidth = $width - strlen($name);
+                    $this->writeText(sprintf("  <info>%s</info>%s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name)
+                            ->getDescription()), $options);
+                }
+            }
+
+            $this->writeText("\n");
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    private function writeText($content, array $options = [])
+    {
+        $this->write(isset($options['raw_text'])
+            && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true);
+    }
+
+    /**
+     * 格式化
+     * @param mixed $default
+     * @return string
+     */
+    private function formatDefaultValue($default)
+    {
+        return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+    }
+
+    /**
+     * @param Command[] $commands
+     * @return int
+     */
+    private function getColumnWidth(array $commands)
+    {
+        $width = 0;
+        foreach ($commands as $command) {
+            $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width;
+        }
+
+        return $width + 2;
+    }
+
+    /**
+     * @param InputOption[] $options
+     * @return int
+     */
+    private function calculateTotalWidthForOptions($options)
+    {
+        $totalWidth = 0;
+        foreach ($options as $option) {
+            $nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + --
+
+            if ($option->acceptValue()) {
+                $valueLength = 1 + strlen($option->getName()); // = + value
+                $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
+
+                $nameLength += $valueLength;
+            }
+            $totalWidth = max($totalWidth, $nameLength);
+        }
+
+        return $totalWidth;
+    }
+}

+ 198 - 0
thinkphp/library/think/console/output/Formatter.php

@@ -0,0 +1,198 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+namespace think\console\output;
+
+use think\console\output\formatter\Stack as StyleStack;
+use think\console\output\formatter\Style;
+
+class Formatter
+{
+
+    private $decorated = false;
+    private $styles    = [];
+    private $styleStack;
+
+    /**
+     * 转义
+     * @param string $text
+     * @return string
+     */
+    public static function escape($text)
+    {
+        return preg_replace('/([^\\\\]?)</is', '$1\\<', $text);
+    }
+
+    /**
+     * 初始化命令行输出格式
+     */
+    public function __construct()
+    {
+        $this->setStyle('error', new Style('white', 'red'));
+        $this->setStyle('info', new Style('green'));
+        $this->setStyle('comment', new Style('yellow'));
+        $this->setStyle('question', new Style('black', 'cyan'));
+        $this->setStyle('highlight', new Style('red'));
+        $this->setStyle('warning', new Style('black', 'yellow'));
+
+        $this->styleStack = new StyleStack();
+    }
+
+    /**
+     * 设置外观标识
+     * @param bool $decorated 是否美化文字
+     */
+    public function setDecorated($decorated)
+    {
+        $this->decorated = (bool) $decorated;
+    }
+
+    /**
+     * 获取外观标识
+     * @return bool
+     */
+    public function isDecorated()
+    {
+        return $this->decorated;
+    }
+
+    /**
+     * 添加一个新样式
+     * @param string $name  样式名
+     * @param Style  $style 样式实例
+     */
+    public function setStyle($name, Style $style)
+    {
+        $this->styles[strtolower($name)] = $style;
+    }
+
+    /**
+     * 是否有这个样式
+     * @param string $name
+     * @return bool
+     */
+    public function hasStyle($name)
+    {
+        return isset($this->styles[strtolower($name)]);
+    }
+
+    /**
+     * 获取样式
+     * @param string $name
+     * @return Style
+     * @throws \InvalidArgumentException
+     */
+    public function getStyle($name)
+    {
+        if (!$this->hasStyle($name)) {
+            throw new \InvalidArgumentException(sprintf('Undefined style: %s', $name));
+        }
+
+        return $this->styles[strtolower($name)];
+    }
+
+    /**
+     * 使用所给的样式格式化文字
+     * @param string $message 文字
+     * @return string
+     */
+    public function format($message)
+    {
+        $offset   = 0;
+        $output   = '';
+        $tagRegex = '[a-z][a-z0-9_=;-]*';
+        preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE);
+        foreach ($matches[0] as $i => $match) {
+            $pos  = $match[1];
+            $text = $match[0];
+
+            if (0 != $pos && '\\' == $message[$pos - 1]) {
+                continue;
+            }
+
+            $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset));
+            $offset = $pos + strlen($text);
+
+            if ($open = '/' != $text[1]) {
+                $tag = $matches[1][$i][0];
+            } else {
+                $tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : '';
+            }
+
+            if (!$open && !$tag) {
+                // </>
+                $this->styleStack->pop();
+            } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) {
+                $output .= $this->applyCurrentStyle($text);
+            } elseif ($open) {
+                $this->styleStack->push($style);
+            } else {
+                $this->styleStack->pop($style);
+            }
+        }
+
+        $output .= $this->applyCurrentStyle(substr($message, $offset));
+
+        return str_replace('\\<', '<', $output);
+    }
+
+    /**
+     * @return StyleStack
+     */
+    public function getStyleStack()
+    {
+        return $this->styleStack;
+    }
+
+    /**
+     * 根据字符串创建新的样式实例
+     * @param string $string
+     * @return Style|bool
+     */
+    private function createStyleFromString($string)
+    {
+        if (isset($this->styles[$string])) {
+            return $this->styles[$string];
+        }
+
+        if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) {
+            return false;
+        }
+
+        $style = new Style();
+        foreach ($matches as $match) {
+            array_shift($match);
+
+            if ('fg' == $match[0]) {
+                $style->setForeground($match[1]);
+            } elseif ('bg' == $match[0]) {
+                $style->setBackground($match[1]);
+            } else {
+                try {
+                    $style->setOption($match[1]);
+                } catch (\InvalidArgumentException $e) {
+                    return false;
+                }
+            }
+        }
+
+        return $style;
+    }
+
+    /**
+     * 从堆栈应用样式到文字
+     * @param string $text 文字
+     * @return string
+     */
+    private function applyCurrentStyle($text)
+    {
+        return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text;
+    }
+}

+ 211 - 0
thinkphp/library/think/console/output/Question.php

@@ -0,0 +1,211 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output;
+
+class Question
+{
+
+    private $question;
+    private $attempts;
+    private $hidden         = false;
+    private $hiddenFallback = true;
+    private $autocompleterValues;
+    private $validator;
+    private $default;
+    private $normalizer;
+
+    /**
+     * 构造方法
+     * @param string $question 问题
+     * @param mixed  $default  默认答案
+     */
+    public function __construct($question, $default = null)
+    {
+        $this->question = $question;
+        $this->default  = $default;
+    }
+
+    /**
+     * 获取问题
+     * @return string
+     */
+    public function getQuestion()
+    {
+        return $this->question;
+    }
+
+    /**
+     * 获取默认答案
+     * @return mixed
+     */
+    public function getDefault()
+    {
+        return $this->default;
+    }
+
+    /**
+     * 是否隐藏答案
+     * @return bool
+     */
+    public function isHidden()
+    {
+        return $this->hidden;
+    }
+
+    /**
+     * 隐藏答案
+     * @param bool $hidden
+     * @return Question
+     */
+    public function setHidden($hidden)
+    {
+        if ($this->autocompleterValues) {
+            throw new \LogicException('A hidden question cannot use the autocompleter.');
+        }
+
+        $this->hidden = (bool) $hidden;
+
+        return $this;
+    }
+
+    /**
+     * 不能被隐藏是否撤销
+     * @return bool
+     */
+    public function isHiddenFallback()
+    {
+        return $this->hiddenFallback;
+    }
+
+    /**
+     * 设置不能被隐藏的时候的操作
+     * @param bool $fallback
+     * @return Question
+     */
+    public function setHiddenFallback($fallback)
+    {
+        $this->hiddenFallback = (bool) $fallback;
+
+        return $this;
+    }
+
+    /**
+     * 获取自动完成
+     * @return null|array|\Traversable
+     */
+    public function getAutocompleterValues()
+    {
+        return $this->autocompleterValues;
+    }
+
+    /**
+     * 设置自动完成的值
+     * @param null|array|\Traversable $values
+     * @return Question
+     * @throws \InvalidArgumentException
+     * @throws \LogicException
+     */
+    public function setAutocompleterValues($values)
+    {
+        if (is_array($values) && $this->isAssoc($values)) {
+            $values = array_merge(array_keys($values), array_values($values));
+        }
+
+        if (null !== $values && !is_array($values)) {
+            if (!$values instanceof \Traversable || $values instanceof \Countable) {
+                throw new \InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.');
+            }
+        }
+
+        if ($this->hidden) {
+            throw new \LogicException('A hidden question cannot use the autocompleter.');
+        }
+
+        $this->autocompleterValues = $values;
+
+        return $this;
+    }
+
+    /**
+     * 设置答案的验证器
+     * @param null|callable $validator
+     * @return Question The current instance
+     */
+    public function setValidator($validator)
+    {
+        $this->validator = $validator;
+
+        return $this;
+    }
+
+    /**
+     * 获取验证器
+     * @return null|callable
+     */
+    public function getValidator()
+    {
+        return $this->validator;
+    }
+
+    /**
+     * 设置最大重试次数
+     * @param null|int $attempts
+     * @return Question
+     * @throws \InvalidArgumentException
+     */
+    public function setMaxAttempts($attempts)
+    {
+        if (null !== $attempts && $attempts < 1) {
+            throw new \InvalidArgumentException('Maximum number of attempts must be a positive value.');
+        }
+
+        $this->attempts = $attempts;
+
+        return $this;
+    }
+
+    /**
+     * 获取最大重试次数
+     * @return null|int
+     */
+    public function getMaxAttempts()
+    {
+        return $this->attempts;
+    }
+
+    /**
+     * 设置响应的回调
+     * @param string|\Closure $normalizer
+     * @return Question
+     */
+    public function setNormalizer($normalizer)
+    {
+        $this->normalizer = $normalizer;
+
+        return $this;
+    }
+
+    /**
+     * 获取响应回调
+     * The normalizer can ba a callable (a string), a closure or a class implementing __invoke.
+     * @return string|\Closure
+     */
+    public function getNormalizer()
+    {
+        return $this->normalizer;
+    }
+
+    protected function isAssoc($array)
+    {
+        return (bool) count(array_filter(array_keys($array), 'is_string'));
+    }
+}

+ 149 - 0
thinkphp/library/think/console/output/descriptor/Console.php

@@ -0,0 +1,149 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output\descriptor;
+
+use think\Console as ThinkConsole;
+use think\console\Command;
+
+class Console
+{
+
+    const GLOBAL_NAMESPACE = '_global';
+
+    /**
+     * @var ThinkConsole
+     */
+    private $console;
+
+    /**
+     * @var null|string
+     */
+    private $namespace;
+
+    /**
+     * @var array
+     */
+    private $namespaces;
+
+    /**
+     * @var Command[]
+     */
+    private $commands;
+
+    /**
+     * @var Command[]
+     */
+    private $aliases;
+
+    /**
+     * 构造方法
+     * @param ThinkConsole $console
+     * @param string|null  $namespace
+     */
+    public function __construct(ThinkConsole $console, $namespace = null)
+    {
+        $this->console   = $console;
+        $this->namespace = $namespace;
+    }
+
+    /**
+     * @return array
+     */
+    public function getNamespaces()
+    {
+        if (null === $this->namespaces) {
+            $this->inspectConsole();
+        }
+
+        return $this->namespaces;
+    }
+
+    /**
+     * @return Command[]
+     */
+    public function getCommands()
+    {
+        if (null === $this->commands) {
+            $this->inspectConsole();
+        }
+
+        return $this->commands;
+    }
+
+    /**
+     * @param string $name
+     * @return Command
+     * @throws \InvalidArgumentException
+     */
+    public function getCommand($name)
+    {
+        if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) {
+            throw new \InvalidArgumentException(sprintf('Command %s does not exist.', $name));
+        }
+
+        return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name];
+    }
+
+    private function inspectConsole()
+    {
+        $this->commands   = [];
+        $this->namespaces = [];
+
+        $all = $this->console->all($this->namespace ? $this->console->findNamespace($this->namespace) : null);
+        foreach ($this->sortCommands($all) as $namespace => $commands) {
+            $names = [];
+
+            /** @var Command $command */
+            foreach ($commands as $name => $command) {
+                if (!$command->getName()) {
+                    continue;
+                }
+
+                if ($command->getName() === $name) {
+                    $this->commands[$name] = $command;
+                } else {
+                    $this->aliases[$name] = $command;
+                }
+
+                $names[] = $name;
+            }
+
+            $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names];
+        }
+    }
+
+    /**
+     * @param array $commands
+     * @return array
+     */
+    private function sortCommands(array $commands)
+    {
+        $namespacedCommands = [];
+        foreach ($commands as $name => $command) {
+            $key = $this->console->extractNamespace($name, 1);
+            if (!$key) {
+                $key = self::GLOBAL_NAMESPACE;
+            }
+
+            $namespacedCommands[$key][$name] = $command;
+        }
+        ksort($namespacedCommands);
+
+        foreach ($namespacedCommands as &$commandsSet) {
+            ksort($commandsSet);
+        }
+        // unset reference to keep scope clear
+        unset($commandsSet);
+
+        return $namespacedCommands;
+    }
+}

+ 52 - 0
thinkphp/library/think/console/output/driver/Buffer.php

@@ -0,0 +1,52 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output\driver;
+
+use think\console\Output;
+
+class Buffer
+{
+    /**
+     * @var string
+     */
+    private $buffer = '';
+
+    public function __construct(Output $output)
+    {
+        // do nothing
+    }
+
+    public function fetch()
+    {
+        $content      = $this->buffer;
+        $this->buffer = '';
+        return $content;
+    }
+
+    public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL)
+    {
+        $messages = (array) $messages;
+
+        foreach ($messages as $message) {
+            $this->buffer .= $message;
+        }
+        if ($newline) {
+            $this->buffer .= "\n";
+        }
+    }
+
+    public function renderException(\Exception $e)
+    {
+        // do nothing
+    }
+
+}

+ 373 - 0
thinkphp/library/think/console/output/driver/Console.php

@@ -0,0 +1,373 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output\driver;
+
+use think\console\Output;
+use think\console\output\Formatter;
+
+class Console
+{
+
+    /** @var  Resource */
+    private $stdout;
+
+    /** @var  Formatter */
+    private $formatter;
+
+    private $terminalDimensions;
+
+    /** @var  Output */
+    private $output;
+
+    public function __construct(Output $output)
+    {
+        $this->output    = $output;
+        $this->formatter = new Formatter();
+        $this->stdout    = $this->openOutputStream();
+        $decorated       = $this->hasColorSupport($this->stdout);
+        $this->formatter->setDecorated($decorated);
+    }
+
+    public function getFormatter()
+    {
+        return $this->formatter;
+    }
+
+    public function setDecorated($decorated)
+    {
+        $this->formatter->setDecorated($decorated);
+    }
+
+    public function write($messages, $newline = false, $type = Output::OUTPUT_NORMAL, $stream = null)
+    {
+        if (Output::VERBOSITY_QUIET === $this->output->getVerbosity()) {
+            return;
+        }
+
+        $messages = (array) $messages;
+
+        foreach ($messages as $message) {
+            switch ($type) {
+                case Output::OUTPUT_NORMAL:
+                    $message = $this->formatter->format($message);
+                    break;
+                case Output::OUTPUT_RAW:
+                    break;
+                case Output::OUTPUT_PLAIN:
+                    $message = strip_tags($this->formatter->format($message));
+                    break;
+                default:
+                    throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type));
+            }
+
+            $this->doWrite($message, $newline, $stream);
+        }
+    }
+
+    public function renderException(\Exception $e)
+    {
+        $stderr    = $this->openErrorStream();
+        $decorated = $this->hasColorSupport($stderr);
+        $this->formatter->setDecorated($decorated);
+
+        do {
+            $title = sprintf('  [%s]  ', get_class($e));
+
+            $len = $this->stringWidth($title);
+
+            $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX;
+
+            if (defined('HHVM_VERSION') && $width > 1 << 31) {
+                $width = 1 << 31;
+            }
+            $lines = [];
+            foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) {
+                foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
+
+                    $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $line)) + 4;
+                    $lines[]    = [$line, $lineLength];
+
+                    $len = max($lineLength, $len);
+                }
+            }
+
+            $messages   = ['', ''];
+            $messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
+            $messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title))));
+            foreach ($lines as $line) {
+                $messages[] = sprintf('<error>  %s  %s</error>', $line[0], str_repeat(' ', $len - $line[1]));
+            }
+            $messages[] = $emptyLine;
+            $messages[] = '';
+            $messages[] = '';
+
+            $this->write($messages, true, Output::OUTPUT_NORMAL, $stderr);
+
+            if (Output::VERBOSITY_VERBOSE <= $this->output->getVerbosity()) {
+                $this->write('<comment>Exception trace:</comment>', true, Output::OUTPUT_NORMAL, $stderr);
+
+                // exception related properties
+                $trace = $e->getTrace();
+                array_unshift($trace, [
+                    'function' => '',
+                    'file'     => $e->getFile() !== null ? $e->getFile() : 'n/a',
+                    'line'     => $e->getLine() !== null ? $e->getLine() : 'n/a',
+                    'args'     => [],
+                ]);
+
+                for ($i = 0, $count = count($trace); $i < $count; ++$i) {
+                    $class    = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
+                    $type     = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
+                    $function = $trace[$i]['function'];
+                    $file     = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
+                    $line     = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
+
+                    $this->write(sprintf(' %s%s%s() at <info>%s:%s</info>', $class, $type, $function, $file, $line), true, Output::OUTPUT_NORMAL, $stderr);
+                }
+
+                $this->write('', true, Output::OUTPUT_NORMAL, $stderr);
+                $this->write('', true, Output::OUTPUT_NORMAL, $stderr);
+            }
+        } while ($e = $e->getPrevious());
+
+    }
+
+    /**
+     * 获取终端宽度
+     * @return int|null
+     */
+    protected function getTerminalWidth()
+    {
+        $dimensions = $this->getTerminalDimensions();
+
+        return $dimensions[0];
+    }
+
+    /**
+     * 获取终端高度
+     * @return int|null
+     */
+    protected function getTerminalHeight()
+    {
+        $dimensions = $this->getTerminalDimensions();
+
+        return $dimensions[1];
+    }
+
+    /**
+     * 获取当前终端的尺寸
+     * @return array
+     */
+    public function getTerminalDimensions()
+    {
+        if ($this->terminalDimensions) {
+            return $this->terminalDimensions;
+        }
+
+        if ('\\' === DS) {
+            if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) {
+                return [(int) $matches[1], (int) $matches[2]];
+            }
+            if (preg_match('/^(\d+)x(\d+)$/', $this->getMode(), $matches)) {
+                return [(int) $matches[1], (int) $matches[2]];
+            }
+        }
+
+        if ($sttyString = $this->getSttyColumns()) {
+            if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
+                return [(int) $matches[2], (int) $matches[1]];
+            }
+            if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
+                return [(int) $matches[2], (int) $matches[1]];
+            }
+        }
+
+        return [null, null];
+    }
+
+    /**
+     * 获取stty列数
+     * @return string
+     */
+    private function getSttyColumns()
+    {
+        if (!function_exists('proc_open')) {
+            return;
+        }
+
+        $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
+        $process        = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
+        if (is_resource($process)) {
+            $info = stream_get_contents($pipes[1]);
+            fclose($pipes[1]);
+            fclose($pipes[2]);
+            proc_close($process);
+
+            return $info;
+        }
+        return;
+    }
+
+    /**
+     * 获取终端模式
+     * @return string <width>x<height> 或 null
+     */
+    private function getMode()
+    {
+        if (!function_exists('proc_open')) {
+            return;
+        }
+
+        $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
+        $process        = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
+        if (is_resource($process)) {
+            $info = stream_get_contents($pipes[1]);
+            fclose($pipes[1]);
+            fclose($pipes[2]);
+            proc_close($process);
+
+            if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
+                return $matches[2] . 'x' . $matches[1];
+            }
+        }
+        return;
+    }
+
+    private function stringWidth($string)
+    {
+        if (!function_exists('mb_strwidth')) {
+            return strlen($string);
+        }
+
+        if (false === $encoding = mb_detect_encoding($string)) {
+            return strlen($string);
+        }
+
+        return mb_strwidth($string, $encoding);
+    }
+
+    private function splitStringByWidth($string, $width)
+    {
+        if (!function_exists('mb_strwidth')) {
+            return str_split($string, $width);
+        }
+
+        if (false === $encoding = mb_detect_encoding($string)) {
+            return str_split($string, $width);
+        }
+
+        $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
+        $lines      = [];
+        $line       = '';
+        foreach (preg_split('//u', $utf8String) as $char) {
+            if (mb_strwidth($line . $char, 'utf8') <= $width) {
+                $line .= $char;
+                continue;
+            }
+            $lines[] = str_pad($line, $width);
+            $line    = $char;
+        }
+        if (strlen($line)) {
+            $lines[] = count($lines) ? str_pad($line, $width) : $line;
+        }
+
+        mb_convert_variables($encoding, 'utf8', $lines);
+
+        return $lines;
+    }
+
+    private function isRunningOS400()
+    {
+        $checks = [
+            function_exists('php_uname') ? php_uname('s') : '',
+            getenv('OSTYPE'),
+            PHP_OS,
+        ];
+        return false !== stripos(implode(';', $checks), 'OS400');
+    }
+
+    /**
+     * 当前环境是否支持写入控制台输出到stdout.
+     *
+     * @return bool
+     */
+    protected function hasStdoutSupport()
+    {
+        return false === $this->isRunningOS400();
+    }
+
+    /**
+     * 当前环境是否支持写入控制台输出到stderr.
+     *
+     * @return bool
+     */
+    protected function hasStderrSupport()
+    {
+        return false === $this->isRunningOS400();
+    }
+
+    /**
+     * @return resource
+     */
+    private function openOutputStream()
+    {
+        if (!$this->hasStdoutSupport()) {
+            return fopen('php://output', 'w');
+        }
+        return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w');
+    }
+
+    /**
+     * @return resource
+     */
+    private function openErrorStream()
+    {
+        return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w');
+    }
+
+    /**
+     * 将消息写入到输出。
+     * @param string $message 消息
+     * @param bool   $newline 是否另起一行
+     * @param null   $stream
+     */
+    protected function doWrite($message, $newline, $stream = null)
+    {
+        if (null === $stream) {
+            $stream = $this->stdout;
+        }
+        if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) {
+            throw new \RuntimeException('Unable to write output.');
+        }
+
+        fflush($stream);
+    }
+
+    /**
+     * 是否支持着色
+     * @param $stream
+     * @return bool
+     */
+    protected function hasColorSupport($stream)
+    {
+        if (DIRECTORY_SEPARATOR === '\\') {
+            return
+            '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD
+            || false !== getenv('ANSICON')
+            || 'ON' === getenv('ConEmuANSI')
+            || 'xterm' === getenv('TERM');
+        }
+
+        return function_exists('posix_isatty') && @posix_isatty($stream);
+    }
+
+}

+ 33 - 0
thinkphp/library/think/console/output/driver/Nothing.php

@@ -0,0 +1,33 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output\driver;
+
+use think\console\Output;
+
+class Nothing
+{
+
+    public function __construct(Output $output)
+    {
+        // do nothing
+    }
+
+    public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL)
+    {
+        // do nothing
+    }
+
+    public function renderException(\Exception $e)
+    {
+        // do nothing
+    }
+}

+ 116 - 0
thinkphp/library/think/console/output/formatter/Stack.php

@@ -0,0 +1,116 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output\formatter;
+
+class Stack
+{
+
+    /**
+     * @var Style[]
+     */
+    private $styles;
+
+    /**
+     * @var Style
+     */
+    private $emptyStyle;
+
+    /**
+     * 构造方法
+     * @param Style|null $emptyStyle
+     */
+    public function __construct(Style $emptyStyle = null)
+    {
+        $this->emptyStyle = $emptyStyle ?: new Style();
+        $this->reset();
+    }
+
+    /**
+     * 重置堆栈
+     */
+    public function reset()
+    {
+        $this->styles = [];
+    }
+
+    /**
+     * 推一个样式进入堆栈
+     * @param Style $style
+     */
+    public function push(Style $style)
+    {
+        $this->styles[] = $style;
+    }
+
+    /**
+     * 从堆栈中弹出一个样式
+     * @param Style|null $style
+     * @return Style
+     * @throws \InvalidArgumentException
+     */
+    public function pop(Style $style = null)
+    {
+        if (empty($this->styles)) {
+            return $this->emptyStyle;
+        }
+
+        if (null === $style) {
+            return array_pop($this->styles);
+        }
+
+        /**
+         * @var int   $index
+         * @var Style $stackedStyle
+         */
+        foreach (array_reverse($this->styles, true) as $index => $stackedStyle) {
+            if ($style->apply('') === $stackedStyle->apply('')) {
+                $this->styles = array_slice($this->styles, 0, $index);
+
+                return $stackedStyle;
+            }
+        }
+
+        throw new \InvalidArgumentException('Incorrectly nested style tag found.');
+    }
+
+    /**
+     * 计算堆栈的当前样式。
+     * @return Style
+     */
+    public function getCurrent()
+    {
+        if (empty($this->styles)) {
+            return $this->emptyStyle;
+        }
+
+        return $this->styles[count($this->styles) - 1];
+    }
+
+    /**
+     * @param Style $emptyStyle
+     * @return Stack
+     */
+    public function setEmptyStyle(Style $emptyStyle)
+    {
+        $this->emptyStyle = $emptyStyle;
+
+        return $this;
+    }
+
+    /**
+     * @return Style
+     */
+    public function getEmptyStyle()
+    {
+        return $this->emptyStyle;
+    }
+}

+ 189 - 0
thinkphp/library/think/console/output/formatter/Style.php

@@ -0,0 +1,189 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output\formatter;
+
+class Style
+{
+
+    private static $availableForegroundColors = [
+        'black'   => ['set' => 30, 'unset' => 39],
+        'red'     => ['set' => 31, 'unset' => 39],
+        'green'   => ['set' => 32, 'unset' => 39],
+        'yellow'  => ['set' => 33, 'unset' => 39],
+        'blue'    => ['set' => 34, 'unset' => 39],
+        'magenta' => ['set' => 35, 'unset' => 39],
+        'cyan'    => ['set' => 36, 'unset' => 39],
+        'white'   => ['set' => 37, 'unset' => 39],
+    ];
+    private static $availableBackgroundColors = [
+        'black'   => ['set' => 40, 'unset' => 49],
+        'red'     => ['set' => 41, 'unset' => 49],
+        'green'   => ['set' => 42, 'unset' => 49],
+        'yellow'  => ['set' => 43, 'unset' => 49],
+        'blue'    => ['set' => 44, 'unset' => 49],
+        'magenta' => ['set' => 45, 'unset' => 49],
+        'cyan'    => ['set' => 46, 'unset' => 49],
+        'white'   => ['set' => 47, 'unset' => 49],
+    ];
+    private static $availableOptions = [
+        'bold'       => ['set' => 1, 'unset' => 22],
+        'underscore' => ['set' => 4, 'unset' => 24],
+        'blink'      => ['set' => 5, 'unset' => 25],
+        'reverse'    => ['set' => 7, 'unset' => 27],
+        'conceal'    => ['set' => 8, 'unset' => 28],
+    ];
+
+    private $foreground;
+    private $background;
+    private $options = [];
+
+    /**
+     * 初始化输出的样式
+     * @param string|null $foreground 字体颜色
+     * @param string|null $background 背景色
+     * @param array       $options    格式
+     * @api
+     */
+    public function __construct($foreground = null, $background = null, array $options = [])
+    {
+        if (null !== $foreground) {
+            $this->setForeground($foreground);
+        }
+        if (null !== $background) {
+            $this->setBackground($background);
+        }
+        if (count($options)) {
+            $this->setOptions($options);
+        }
+    }
+
+    /**
+     * 设置字体颜色
+     * @param string|null $color 颜色名
+     * @throws \InvalidArgumentException
+     * @api
+     */
+    public function setForeground($color = null)
+    {
+        if (null === $color) {
+            $this->foreground = null;
+
+            return;
+        }
+
+        if (!isset(static::$availableForegroundColors[$color])) {
+            throw new \InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors))));
+        }
+
+        $this->foreground = static::$availableForegroundColors[$color];
+    }
+
+    /**
+     * 设置背景色
+     * @param string|null $color 颜色名
+     * @throws \InvalidArgumentException
+     * @api
+     */
+    public function setBackground($color = null)
+    {
+        if (null === $color) {
+            $this->background = null;
+
+            return;
+        }
+
+        if (!isset(static::$availableBackgroundColors[$color])) {
+            throw new \InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors))));
+        }
+
+        $this->background = static::$availableBackgroundColors[$color];
+    }
+
+    /**
+     * 设置字体格式
+     * @param string $option 格式名
+     * @throws \InvalidArgumentException When the option name isn't defined
+     * @api
+     */
+    public function setOption($option)
+    {
+        if (!isset(static::$availableOptions[$option])) {
+            throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions))));
+        }
+
+        if (!in_array(static::$availableOptions[$option], $this->options)) {
+            $this->options[] = static::$availableOptions[$option];
+        }
+    }
+
+    /**
+     * 重置字体格式
+     * @param string $option 格式名
+     * @throws \InvalidArgumentException
+     */
+    public function unsetOption($option)
+    {
+        if (!isset(static::$availableOptions[$option])) {
+            throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions))));
+        }
+
+        $pos = array_search(static::$availableOptions[$option], $this->options);
+        if (false !== $pos) {
+            unset($this->options[$pos]);
+        }
+    }
+
+    /**
+     * 批量设置字体格式
+     * @param array $options
+     */
+    public function setOptions(array $options)
+    {
+        $this->options = [];
+
+        foreach ($options as $option) {
+            $this->setOption($option);
+        }
+    }
+
+    /**
+     * 应用样式到文字
+     * @param string $text 文字
+     * @return string
+     */
+    public function apply($text)
+    {
+        $setCodes   = [];
+        $unsetCodes = [];
+
+        if (null !== $this->foreground) {
+            $setCodes[]   = $this->foreground['set'];
+            $unsetCodes[] = $this->foreground['unset'];
+        }
+        if (null !== $this->background) {
+            $setCodes[]   = $this->background['set'];
+            $unsetCodes[] = $this->background['unset'];
+        }
+        if (count($this->options)) {
+            foreach ($this->options as $option) {
+                $setCodes[]   = $option['set'];
+                $unsetCodes[] = $option['unset'];
+            }
+        }
+
+        if (0 === count($setCodes)) {
+            return $text;
+        }
+
+        return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes));
+    }
+}

+ 163 - 0
thinkphp/library/think/console/output/question/Choice.php

@@ -0,0 +1,163 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output\question;
+
+use think\console\output\Question;
+
+class Choice extends Question
+{
+
+    private $choices;
+    private $multiselect  = false;
+    private $prompt       = ' > ';
+    private $errorMessage = 'Value "%s" is invalid';
+
+    /**
+     * 构造方法
+     * @param string $question 问题
+     * @param array  $choices  选项
+     * @param mixed  $default  默认答案
+     */
+    public function __construct($question, array $choices, $default = null)
+    {
+        parent::__construct($question, $default);
+
+        $this->choices = $choices;
+        $this->setValidator($this->getDefaultValidator());
+        $this->setAutocompleterValues($choices);
+    }
+
+    /**
+     * 可选项
+     * @return array
+     */
+    public function getChoices()
+    {
+        return $this->choices;
+    }
+
+    /**
+     * 设置可否多选
+     * @param bool $multiselect
+     * @return self
+     */
+    public function setMultiselect($multiselect)
+    {
+        $this->multiselect = $multiselect;
+        $this->setValidator($this->getDefaultValidator());
+
+        return $this;
+    }
+
+    public function isMultiselect()
+    {
+        return $this->multiselect;
+    }
+
+    /**
+     * 获取提示
+     * @return string
+     */
+    public function getPrompt()
+    {
+        return $this->prompt;
+    }
+
+    /**
+     * 设置提示
+     * @param string $prompt
+     * @return self
+     */
+    public function setPrompt($prompt)
+    {
+        $this->prompt = $prompt;
+
+        return $this;
+    }
+
+    /**
+     * 设置错误提示信息
+     * @param string $errorMessage
+     * @return self
+     */
+    public function setErrorMessage($errorMessage)
+    {
+        $this->errorMessage = $errorMessage;
+        $this->setValidator($this->getDefaultValidator());
+
+        return $this;
+    }
+
+    /**
+     * 获取默认的验证方法
+     * @return callable
+     */
+    private function getDefaultValidator()
+    {
+        $choices      = $this->choices;
+        $errorMessage = $this->errorMessage;
+        $multiselect  = $this->multiselect;
+        $isAssoc      = $this->isAssoc($choices);
+
+        return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) {
+            // Collapse all spaces.
+            $selectedChoices = str_replace(' ', '', $selected);
+
+            if ($multiselect) {
+                // Check for a separated comma values
+                if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) {
+                    throw new \InvalidArgumentException(sprintf($errorMessage, $selected));
+                }
+                $selectedChoices = explode(',', $selectedChoices);
+            } else {
+                $selectedChoices = [$selected];
+            }
+
+            $multiselectChoices = [];
+            foreach ($selectedChoices as $value) {
+                $results = [];
+                foreach ($choices as $key => $choice) {
+                    if ($choice === $value) {
+                        $results[] = $key;
+                    }
+                }
+
+                if (count($results) > 1) {
+                    throw new \InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results)));
+                }
+
+                $result = array_search($value, $choices);
+
+                if (!$isAssoc) {
+                    if (!empty($result)) {
+                        $result = $choices[$result];
+                    } elseif (isset($choices[$value])) {
+                        $result = $choices[$value];
+                    }
+                } elseif (empty($result) && array_key_exists($value, $choices)) {
+                    $result = $value;
+                }
+
+                if (empty($result)) {
+                    throw new \InvalidArgumentException(sprintf($errorMessage, $value));
+                }
+                array_push($multiselectChoices, $result);
+            }
+
+            if ($multiselect) {
+                return $multiselectChoices;
+            }
+
+            return current($multiselectChoices);
+        };
+    }
+}

+ 57 - 0
thinkphp/library/think/console/output/question/Confirmation.php

@@ -0,0 +1,57 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace think\console\output\question;
+
+use think\console\output\Question;
+
+class Confirmation extends Question
+{
+
+    private $trueAnswerRegex;
+
+    /**
+     * 构造方法
+     * @param string $question        问题
+     * @param bool   $default         默认答案
+     * @param string $trueAnswerRegex 验证正则
+     */
+    public function __construct($question, $default = true, $trueAnswerRegex = '/^y/i')
+    {
+        parent::__construct($question, (bool) $default);
+
+        $this->trueAnswerRegex = $trueAnswerRegex;
+        $this->setNormalizer($this->getDefaultNormalizer());
+    }
+
+    /**
+     * 获取默认的答案回调
+     * @return callable
+     */
+    private function getDefaultNormalizer()
+    {
+        $default = $this->getDefault();
+        $regex   = $this->trueAnswerRegex;
+
+        return function ($answer) use ($default, $regex) {
+            if (is_bool($answer)) {
+                return $answer;
+            }
+
+            $answerIsTrue = (bool) preg_match($regex, $answer);
+            if (false === $default) {
+                return $answer && $answerIsTrue;
+            }
+
+            return !$answer || $answerIsTrue;
+        };
+    }
+}

+ 99 - 0
thinkphp/library/think/controller/Rest.php

@@ -0,0 +1,99 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\controller;
+
+use think\App;
+use think\Request;
+use think\Response;
+
+abstract class Rest
+{
+
+    protected $method; // 当前请求类型
+    protected $type; // 当前资源类型
+    // 输出类型
+    protected $restMethodList    = 'get|post|put|delete';
+    protected $restDefaultMethod = 'get';
+    protected $restTypeList      = 'html|xml|json|rss';
+    protected $restDefaultType   = 'html';
+    protected $restOutputType    = [ // REST允许输出的资源类型列表
+        'xml'  => 'application/xml',
+        'json' => 'application/json',
+        'html' => 'text/html',
+    ];
+
+    /**
+     * 构造函数 取得模板对象实例
+     * @access public
+     */
+    public function __construct()
+    {
+        // 资源类型检测
+        $request = Request::instance();
+        $ext     = $request->ext();
+        if ('' == $ext) {
+            // 自动检测资源类型
+            $this->type = $request->type();
+        } elseif (!preg_match('/(' . $this->restTypeList . ')$/i', $ext)) {
+            // 资源类型非法 则用默认资源类型访问
+            $this->type = $this->restDefaultType;
+        } else {
+            $this->type = $ext;
+        }
+        // 请求方式检测
+        $method = strtolower($request->method());
+        if (!preg_match('/(' . $this->restMethodList . ')$/i', $method)) {
+            // 请求方式非法 则用默认请求方法
+            $method = $this->restDefaultMethod;
+        }
+        $this->method = $method;
+    }
+
+    /**
+     * REST 调用
+     * @access public
+     * @param string $method 方法名
+     * @return mixed
+     * @throws \Exception
+     */
+    public function _empty($method)
+    {
+        if (method_exists($this, $method . '_' . $this->method . '_' . $this->type)) {
+            // RESTFul方法支持
+            $fun = $method . '_' . $this->method . '_' . $this->type;
+        } elseif ($this->method == $this->restDefaultMethod && method_exists($this, $method . '_' . $this->type)) {
+            $fun = $method . '_' . $this->type;
+        } elseif ($this->type == $this->restDefaultType && method_exists($this, $method . '_' . $this->method)) {
+            $fun = $method . '_' . $this->method;
+        }
+        if (isset($fun)) {
+            return App::invokeMethod([$this, $fun]);
+        } else {
+            // 抛出异常
+            throw new \Exception('error action :' . $method);
+        }
+    }
+
+    /**
+     * 输出返回数据
+     * @access protected
+     * @param mixed     $data 要返回的数据
+     * @param String    $type 返回类型 JSON XML
+     * @param integer   $code HTTP状态码
+     * @return Response
+     */
+    protected function response($data, $type = 'json', $code = 200)
+    {
+        return Response::create($data, $type)->code($code);
+    }
+
+}

+ 51 - 0
thinkphp/library/think/controller/Yar.php

@@ -0,0 +1,51 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\controller;
+
+/**
+ * ThinkPHP Yar控制器类
+ */
+abstract class Yar
+{
+
+    /**
+     * 构造函数
+     * @access public
+     */
+    public function __construct()
+    {
+        //控制器初始化
+        if (method_exists($this, '_initialize')) {
+            $this->_initialize();
+        }
+
+        //判断扩展是否存在
+        if (!extension_loaded('yar')) {
+            throw new \Exception('not support yar');
+        }
+
+        //实例化Yar_Server
+        $server = new \Yar_Server($this);
+        // 启动server
+        $server->handle();
+    }
+
+    /**
+     * 魔术方法 有不存在的操作的时候执行
+     * @access public
+     * @param string $method 方法名
+     * @param array $args 参数
+     * @return mixed
+     */
+    public function __call($method, $args)
+    {}
+}

+ 895 - 0
thinkphp/library/think/db/Builder.php

@@ -0,0 +1,895 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\db;
+
+use PDO;
+use think\Exception;
+
+abstract class Builder
+{
+    // connection对象实例
+    protected $connection;
+    // 查询对象实例
+    protected $query;
+
+    // 数据库表达式
+    protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'not like' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'exp' => 'EXP', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN', 'exists' => 'EXISTS', 'notexists' => 'NOT EXISTS', 'not exists' => 'NOT EXISTS', 'null' => 'NULL', 'notnull' => 'NOT NULL', 'not null' => 'NOT NULL', '> time' => '> TIME', '< time' => '< TIME', '>= time' => '>= TIME', '<= time' => '<= TIME', 'between time' => 'BETWEEN TIME', 'not between time' => 'NOT BETWEEN TIME', 'notbetween time' => 'NOT BETWEEN TIME'];
+
+    // SQL表达式
+    protected $selectSql    = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT%%LOCK%%COMMENT%';
+    protected $insertSql    = '%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
+    protected $insertAllSql = '%INSERT% INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
+    protected $updateSql    = 'UPDATE %TABLE% SET %SET% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%';
+    protected $deleteSql    = 'DELETE FROM %TABLE% %USING% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+    /**
+     * 构造函数
+     * @access public
+     * @param Connection    $connection 数据库连接对象实例
+     * @param Query         $query      数据库查询对象实例
+     */
+    public function __construct(Connection $connection, Query $query)
+    {
+        $this->connection = $connection;
+        $this->query      = $query;
+    }
+
+    /**
+     * 获取当前的连接对象实例
+     * @access public
+     * @return Connection
+     */
+    public function getConnection()
+    {
+        return $this->connection;
+    }
+
+    /**
+     * 获取当前的Query对象实例
+     * @access public
+     * @return Query
+     */
+    public function getQuery()
+    {
+        return $this->query;
+    }
+
+    /**
+     * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写)
+     * @access protected
+     * @param string $sql sql语句
+     * @return string
+     */
+    protected function parseSqlTable($sql)
+    {
+        return $this->query->parseSqlTable($sql);
+    }
+
+    /**
+     * 数据分析
+     * @access protected
+     * @param array     $data 数据
+     * @param array     $options 查询参数
+     * @return array
+     * @throws Exception
+     */
+    protected function parseData($data, $options)
+    {
+        if (empty($data)) {
+            return [];
+        }
+
+        // 获取绑定信息
+        $bind = $this->query->getFieldsBind($options['table']);
+        if ('*' == $options['field']) {
+            $fields = array_keys($bind);
+        } else {
+            $fields = $options['field'];
+        }
+
+        $result = [];
+        foreach ($data as $key => $val) {
+            $item = $this->parseKey($key, $options, true);
+            if ($val instanceof Expression) {
+                $result[$item] = $val->getValue();
+                continue;
+            } elseif (is_object($val) && method_exists($val, '__toString')) {
+                // 对象数据写入
+                $val = $val->__toString();
+            }
+            if (false === strpos($key, '.') && !in_array($key, $fields, true)) {
+                if ($options['strict']) {
+                    throw new Exception('fields not exists:[' . $key . ']');
+                }
+            } elseif (is_null($val)) {
+                $result[$item] = 'NULL';
+            } elseif (is_array($val) && !empty($val)) {
+                switch (strtolower($val[0])) {
+                    case 'inc':
+                        $result[$item] = $item . '+' . floatval($val[1]);
+                        break;
+                    case 'dec':
+                        $result[$item] = $item . '-' . floatval($val[1]);
+                        break;
+                    case 'exp':
+                        throw new Exception('not support data:[' . $val[0] . ']');
+                }
+            } elseif (is_scalar($val)) {
+                // 过滤非标量数据
+                if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) {
+                    $result[$item] = $val;
+                } else {
+                    $key = str_replace('.', '_', $key);
+                    $this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
+                    $result[$item] = ':data__' . $key;
+                }
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * 字段名分析
+     * @access protected
+     * @param string $key
+     * @param array  $options
+     * @return string
+     */
+    protected function parseKey($key, $options = [], $strict = false)
+    {
+        return $key;
+    }
+
+    /**
+     * value分析
+     * @access protected
+     * @param mixed     $value
+     * @param string    $field
+     * @return string|array
+     */
+    protected function parseValue($value, $field = '')
+    {
+        if (is_string($value)) {
+            $value = strpos($value, ':') === 0 && $this->query->isBind(substr($value, 1)) ? $value : $this->connection->quote($value);
+        } elseif (is_array($value)) {
+            $value = array_map([$this, 'parseValue'], $value);
+        } elseif (is_bool($value)) {
+            $value = $value ? '1' : '0';
+        } elseif (is_null($value)) {
+            $value = 'null';
+        }
+        return $value;
+    }
+
+    /**
+     * field分析
+     * @access protected
+     * @param mixed     $fields
+     * @param array     $options
+     * @return string
+     */
+    protected function parseField($fields, $options = [])
+    {
+        if ('*' == $fields || empty($fields)) {
+            $fieldsStr = '*';
+        } elseif (is_array($fields)) {
+            // 支持 'field1'=>'field2' 这样的字段别名定义
+            $array = [];
+            foreach ($fields as $key => $field) {
+                if ($field instanceof Expression) {
+                    $array[] = $field->getValue();
+                } elseif (!is_numeric($key)) {
+                    $array[] = $this->parseKey($key, $options) . ' AS ' . $this->parseKey($field, $options, true);
+                } else {
+                    $array[] = $this->parseKey($field, $options);
+                }
+            }
+            $fieldsStr = implode(',', $array);
+        }
+        return $fieldsStr;
+    }
+
+    /**
+     * table分析
+     * @access protected
+     * @param mixed $tables
+     * @param array $options
+     * @return string
+     */
+    protected function parseTable($tables, $options = [])
+    {
+        $item = [];
+        foreach ((array) $tables as $key => $table) {
+            if (!is_numeric($key)) {
+                $key    = $this->parseSqlTable($key);
+                $item[] = $this->parseKey($key) . ' ' . (isset($options['alias'][$table]) ? $this->parseKey($options['alias'][$table]) : $this->parseKey($table));
+            } else {
+                $table = $this->parseSqlTable($table);
+                if (isset($options['alias'][$table])) {
+                    $item[] = $this->parseKey($table) . ' ' . $this->parseKey($options['alias'][$table]);
+                } else {
+                    $item[] = $this->parseKey($table);
+                }
+            }
+        }
+        return implode(',', $item);
+    }
+
+    /**
+     * where分析
+     * @access protected
+     * @param mixed $where   查询条件
+     * @param array $options 查询参数
+     * @return string
+     */
+    protected function parseWhere($where, $options)
+    {
+        $whereStr = $this->buildWhere($where, $options);
+        if (!empty($options['soft_delete'])) {
+            // 附加软删除条件
+            list($field, $condition) = $options['soft_delete'];
+
+            $binds    = $this->query->getFieldsBind($options['table']);
+            $whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : '';
+            $whereStr = $whereStr . $this->parseWhereItem($field, $condition, '', $options, $binds);
+        }
+        return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
+    }
+
+    /**
+     * 生成查询条件SQL
+     * @access public
+     * @param mixed     $where
+     * @param array     $options
+     * @return string
+     */
+    public function buildWhere($where, $options)
+    {
+        if (empty($where)) {
+            $where = [];
+        }
+
+        if ($where instanceof Query) {
+            return $this->buildWhere($where->getOptions('where'), $options);
+        }
+
+        $whereStr = '';
+        $binds    = $this->query->getFieldsBind($options['table']);
+        foreach ($where as $key => $val) {
+            $str = [];
+            foreach ($val as $field => $value) {
+                if ($value instanceof Expression) {
+                    $str[] = ' ' . $key . ' ( ' . $value->getValue() . ' )';
+                } elseif ($value instanceof \Closure) {
+                    // 使用闭包查询
+                    $query = new Query($this->connection);
+                    call_user_func_array($value, [ & $query]);
+                    $whereClause = $this->buildWhere($query->getOptions('where'), $options);
+                    if (!empty($whereClause)) {
+                        $str[] = ' ' . $key . ' ( ' . $whereClause . ' )';
+                    }
+                } elseif (strpos($field, '|')) {
+                    // 不同字段使用相同查询条件(OR)
+                    $array = explode('|', $field);
+                    $item  = [];
+                    foreach ($array as $k) {
+                        $item[] = $this->parseWhereItem($k, $value, '', $options, $binds);
+                    }
+                    $str[] = ' ' . $key . ' ( ' . implode(' OR ', $item) . ' )';
+                } elseif (strpos($field, '&')) {
+                    // 不同字段使用相同查询条件(AND)
+                    $array = explode('&', $field);
+                    $item  = [];
+                    foreach ($array as $k) {
+                        $item[] = $this->parseWhereItem($k, $value, '', $options, $binds);
+                    }
+                    $str[] = ' ' . $key . ' ( ' . implode(' AND ', $item) . ' )';
+                } else {
+                    // 对字段使用表达式查询
+                    $field = is_string($field) ? $field : '';
+                    $str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key, $options, $binds);
+                }
+            }
+
+            $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($key) + 1) : implode(' ', $str);
+        }
+
+        return $whereStr;
+    }
+
+    // where子单元分析
+    protected function parseWhereItem($field, $val, $rule = '', $options = [], $binds = [], $bindName = null)
+    {
+        // 字段分析
+        $key = $field ? $this->parseKey($field, $options, true) : '';
+
+        // 查询规则和条件
+        if (!is_array($val)) {
+            $val = is_null($val) ? ['null', ''] : ['=', $val];
+        }
+        list($exp, $value) = $val;
+
+        // 对一个字段使用多个查询条件
+        if (is_array($exp)) {
+            $item = array_pop($val);
+            // 传入 or 或者 and
+            if (is_string($item) && in_array($item, ['AND', 'and', 'OR', 'or'])) {
+                $rule = $item;
+            } else {
+                array_push($val, $item);
+            }
+            foreach ($val as $k => $item) {
+                $bindName = 'where_' . str_replace('.', '_', $field) . '_' . $k;
+                $str[]    = $this->parseWhereItem($field, $item, $rule, $options, $binds, $bindName);
+            }
+            return '( ' . implode(' ' . $rule . ' ', $str) . ' )';
+        }
+
+        // 检测操作符
+        if (!in_array($exp, $this->exp)) {
+            $exp = strtolower($exp);
+            if (isset($this->exp[$exp])) {
+                $exp = $this->exp[$exp];
+            } else {
+                throw new Exception('where express error:' . $exp);
+            }
+        }
+        $bindName = $bindName ?: 'where_' . $rule . '_' . str_replace(['.', '-'], '_', $field);
+        if (preg_match('/\W/', $bindName)) {
+            // 处理带非单词字符的字段名
+            $bindName = md5($bindName);
+        }
+
+        if ($value instanceof Expression) {
+
+        } elseif (is_object($value) && method_exists($value, '__toString')) {
+            // 对象数据写入
+            $value = $value->__toString();
+        }
+
+        $bindType = isset($binds[$field]) ? $binds[$field] : PDO::PARAM_STR;
+        if (is_scalar($value) && array_key_exists($field, $binds) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && strpos($exp, 'TIME') === false) {
+            if (strpos($value, ':') !== 0 || !$this->query->isBind(substr($value, 1))) {
+                if ($this->query->isBind($bindName)) {
+                    $bindName .= '_' . str_replace('.', '_', uniqid('', true));
+                }
+                $this->query->bind($bindName, $value, $bindType);
+                $value = ':' . $bindName;
+            }
+        }
+
+        $whereStr = '';
+        if (in_array($exp, ['=', '<>', '>', '>=', '<', '<='])) {
+            // 比较运算
+            if ($value instanceof \Closure) {
+                $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value);
+            } else {
+                $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field);
+            }
+        } elseif ('LIKE' == $exp || 'NOT LIKE' == $exp) {
+            // 模糊匹配
+            if (is_array($value)) {
+                foreach ($value as $item) {
+                    $array[] = $key . ' ' . $exp . ' ' . $this->parseValue($item, $field);
+                }
+                $logic = isset($val[2]) ? $val[2] : 'AND';
+                $whereStr .= '(' . implode($array, ' ' . strtoupper($logic) . ' ') . ')';
+            } else {
+                $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field);
+            }
+        } elseif ('EXP' == $exp) {
+            // 表达式查询
+            if ($value instanceof Expression) {
+                $whereStr .= '( ' . $key . ' ' . $value->getValue() . ' )';
+            } else {
+                throw new Exception('where express error:' . $exp);
+            }
+        } elseif (in_array($exp, ['NOT NULL', 'NULL'])) {
+            // NULL 查询
+            $whereStr .= $key . ' IS ' . $exp;
+        } elseif (in_array($exp, ['NOT IN', 'IN'])) {
+            // IN 查询
+            if ($value instanceof \Closure) {
+                $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value);
+            } else {
+                $value = array_unique(is_array($value) ? $value : explode(',', $value));
+                if (array_key_exists($field, $binds)) {
+                    $bind  = [];
+                    $array = [];
+                    $i     = 0;
+                    foreach ($value as $v) {
+                        $i++;
+                        if ($this->query->isBind($bindName . '_in_' . $i)) {
+                            $bindKey = $bindName . '_in_' . uniqid() . '_' . $i;
+                        } else {
+                            $bindKey = $bindName . '_in_' . $i;
+                        }
+                        $bind[$bindKey] = [$v, $bindType];
+                        $array[]        = ':' . $bindKey;
+                    }
+                    $this->query->bind($bind);
+                    $zone = implode(',', $array);
+                } else {
+                    $zone = implode(',', $this->parseValue($value, $field));
+                }
+                $whereStr .= $key . ' ' . $exp . ' (' . (empty($zone) ? "''" : $zone) . ')';
+            }
+        } elseif (in_array($exp, ['NOT BETWEEN', 'BETWEEN'])) {
+            // BETWEEN 查询
+            $data = is_array($value) ? $value : explode(',', $value);
+            if (array_key_exists($field, $binds)) {
+                if ($this->query->isBind($bindName . '_between_1')) {
+                    $bindKey1 = $bindName . '_between_1' . uniqid();
+                    $bindKey2 = $bindName . '_between_2' . uniqid();
+                } else {
+                    $bindKey1 = $bindName . '_between_1';
+                    $bindKey2 = $bindName . '_between_2';
+                }
+                $bind = [
+                    $bindKey1 => [$data[0], $bindType],
+                    $bindKey2 => [$data[1], $bindType],
+                ];
+                $this->query->bind($bind);
+                $between = ':' . $bindKey1 . ' AND :' . $bindKey2;
+            } else {
+                $between = $this->parseValue($data[0], $field) . ' AND ' . $this->parseValue($data[1], $field);
+            }
+            $whereStr .= $key . ' ' . $exp . ' ' . $between;
+        } elseif (in_array($exp, ['NOT EXISTS', 'EXISTS'])) {
+            // EXISTS 查询
+            if ($value instanceof \Closure) {
+                $whereStr .= $exp . ' ' . $this->parseClosure($value);
+            } else {
+                $whereStr .= $exp . ' (' . $value . ')';
+            }
+        } elseif (in_array($exp, ['< TIME', '> TIME', '<= TIME', '>= TIME'])) {
+            $whereStr .= $key . ' ' . substr($exp, 0, 2) . ' ' . $this->parseDateTime($value, $field, $options, $bindName, $bindType);
+        } elseif (in_array($exp, ['BETWEEN TIME', 'NOT BETWEEN TIME'])) {
+            if (is_string($value)) {
+                $value = explode(',', $value);
+            }
+
+            $whereStr .= $key . ' ' . substr($exp, 0, -4) . $this->parseDateTime($value[0], $field, $options, $bindName . '_between_1', $bindType) . ' AND ' . $this->parseDateTime($value[1], $field, $options, $bindName . '_between_2', $bindType);
+        }
+        return $whereStr;
+    }
+
+    // 执行闭包子查询
+    protected function parseClosure($call, $show = true)
+    {
+        $query = new Query($this->connection);
+        call_user_func_array($call, [ & $query]);
+        return $query->buildSql($show);
+    }
+
+    /**
+     * 日期时间条件解析
+     * @access protected
+     * @param string    $value
+     * @param string    $key
+     * @param array     $options
+     * @param string    $bindName
+     * @param integer   $bindType
+     * @return string
+     */
+    protected function parseDateTime($value, $key, $options = [], $bindName = null, $bindType = null)
+    {
+        // 获取时间字段类型
+        if (strpos($key, '.')) {
+            list($table, $key) = explode('.', $key);
+            if (isset($options['alias']) && $pos = array_search($table, $options['alias'])) {
+                $table = $pos;
+            }
+        } else {
+            $table = $options['table'];
+        }
+        $type = $this->query->getTableInfo($table, 'type');
+        if (isset($type[$key])) {
+            $info = $type[$key];
+        }
+        if (isset($info)) {
+            if (is_string($value)) {
+                $value = strtotime($value) ?: $value;
+            }
+
+            if (preg_match('/(datetime|timestamp)/is', $info)) {
+                // 日期及时间戳类型
+                $value = date('Y-m-d H:i:s', $value);
+            } elseif (preg_match('/(date)/is', $info)) {
+                // 日期及时间戳类型
+                $value = date('Y-m-d', $value);
+            }
+        }
+        $bindName = $bindName ?: $key;
+
+        if ($this->query->isBind($bindName)) {
+            $bindName .= '_' . str_replace('.', '_', uniqid('', true));
+        }
+
+        $this->query->bind($bindName, $value, $bindType);
+        return ':' . $bindName;
+    }
+
+    /**
+     * limit分析
+     * @access protected
+     * @param mixed $limit
+     * @return string
+     */
+    protected function parseLimit($limit)
+    {
+        return (!empty($limit) && false === strpos($limit, '(')) ? ' LIMIT ' . $limit . ' ' : '';
+    }
+
+    /**
+     * join分析
+     * @access protected
+     * @param array $join
+     * @param array $options 查询条件
+     * @return string
+     */
+    protected function parseJoin($join, $options = [])
+    {
+        $joinStr = '';
+        if (!empty($join)) {
+            foreach ($join as $item) {
+                list($table, $type, $on) = $item;
+                $condition               = [];
+                foreach ((array) $on as $val) {
+                    if ($val instanceof Expression) {
+                        $condition[] = $val->getValue();
+                    } elseif (strpos($val, '=')) {
+                        list($val1, $val2) = explode('=', $val, 2);
+                        $condition[]       = $this->parseKey($val1, $options) . '=' . $this->parseKey($val2, $options);
+                    } else {
+                        $condition[] = $val;
+                    }
+                }
+
+                $table = $this->parseTable($table, $options);
+                $joinStr .= ' ' . $type . ' JOIN ' . $table . ' ON ' . implode(' AND ', $condition);
+            }
+        }
+        return $joinStr;
+    }
+
+    /**
+     * order分析
+     * @access protected
+     * @param mixed $order
+     * @param array $options 查询条件
+     * @return string
+     */
+    protected function parseOrder($order, $options = [])
+    {
+        if (empty($order)) {
+            return '';
+        }
+
+        $array = [];
+        foreach ($order as $key => $val) {
+            if ($val instanceof Expression) {
+                $array[] = $val->getValue();
+            } elseif ('[rand]' == $val) {
+                $array[] = $this->parseRand();
+            } else {
+                if (is_numeric($key)) {
+                    list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' ');
+                } else {
+                    $sort = $val;
+                }
+                $sort    = strtoupper($sort);
+                $sort    = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : '';
+                $array[] = $this->parseKey($key, $options, true) . $sort;
+            }
+        }
+        $order = implode(',', $array);
+
+        return !empty($order) ? ' ORDER BY ' . $order : '';
+    }
+
+    /**
+     * group分析
+     * @access protected
+     * @param mixed $group
+     * @return string
+     */
+    protected function parseGroup($group)
+    {
+        return !empty($group) ? ' GROUP BY ' . $this->parseKey($group) : '';
+    }
+
+    /**
+     * having分析
+     * @access protected
+     * @param string $having
+     * @return string
+     */
+    protected function parseHaving($having)
+    {
+        return !empty($having) ? ' HAVING ' . $having : '';
+    }
+
+    /**
+     * comment分析
+     * @access protected
+     * @param string $comment
+     * @return string
+     */
+    protected function parseComment($comment)
+    {
+        if (false !== strpos($comment, '*/')) {
+            $comment = strstr($comment, '*/', true);
+        }
+        return !empty($comment) ? ' /* ' . $comment . ' */' : '';
+    }
+
+    /**
+     * distinct分析
+     * @access protected
+     * @param mixed $distinct
+     * @return string
+     */
+    protected function parseDistinct($distinct)
+    {
+        return !empty($distinct) ? ' DISTINCT ' : '';
+    }
+
+    /**
+     * union分析
+     * @access protected
+     * @param mixed $union
+     * @return string
+     */
+    protected function parseUnion($union)
+    {
+        if (empty($union)) {
+            return '';
+        }
+        $type = $union['type'];
+        unset($union['type']);
+        foreach ($union as $u) {
+            if ($u instanceof \Closure) {
+                $sql[] = $type . ' ' . $this->parseClosure($u);
+            } elseif (is_string($u)) {
+                $sql[] = $type . ' ( ' . $this->parseSqlTable($u) . ' )';
+            }
+        }
+        return ' ' . implode(' ', $sql);
+    }
+
+    /**
+     * index分析,可在操作链中指定需要强制使用的索引
+     * @access protected
+     * @param mixed $index
+     * @return string
+     */
+    protected function parseForce($index)
+    {
+        if (empty($index)) {
+            return '';
+        }
+
+        return sprintf(" FORCE INDEX ( %s ) ", is_array($index) ? implode(',', $index) : $index);
+    }
+
+    /**
+     * 设置锁机制
+     * @access protected
+     * @param bool|string $lock
+     * @return string
+     */
+    protected function parseLock($lock = false)
+    {
+        if (is_bool($lock)) {
+            return $lock ? ' FOR UPDATE ' : '';
+        } elseif (is_string($lock)) {
+            return ' ' . trim($lock) . ' ';
+        }
+    }
+
+    /**
+     * 生成查询SQL
+     * @access public
+     * @param array $options 表达式
+     * @return string
+     */
+    public function select($options = [])
+    {
+        $sql = str_replace(
+            ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
+            [
+                $this->parseTable($options['table'], $options),
+                $this->parseDistinct($options['distinct']),
+                $this->parseField($options['field'], $options),
+                $this->parseJoin($options['join'], $options),
+                $this->parseWhere($options['where'], $options),
+                $this->parseGroup($options['group']),
+                $this->parseHaving($options['having']),
+                $this->parseOrder($options['order'], $options),
+                $this->parseLimit($options['limit']),
+                $this->parseUnion($options['union']),
+                $this->parseLock($options['lock']),
+                $this->parseComment($options['comment']),
+                $this->parseForce($options['force']),
+            ], $this->selectSql);
+        return $sql;
+    }
+
+    /**
+     * 生成insert SQL
+     * @access public
+     * @param array     $data 数据
+     * @param array     $options 表达式
+     * @param bool      $replace 是否replace
+     * @return string
+     */
+    public function insert(array $data, $options = [], $replace = false)
+    {
+        // 分析并处理数据
+        $data = $this->parseData($data, $options);
+        if (empty($data)) {
+            return 0;
+        }
+        $fields = array_keys($data);
+        $values = array_values($data);
+
+        $sql = str_replace(
+            ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
+            [
+                $replace ? 'REPLACE' : 'INSERT',
+                $this->parseTable($options['table'], $options),
+                implode(' , ', $fields),
+                implode(' , ', $values),
+                $this->parseComment($options['comment']),
+            ], $this->insertSql);
+
+        return $sql;
+    }
+
+    /**
+     * 生成insertall SQL
+     * @access public
+     * @param array     $dataSet 数据集
+     * @param array     $options 表达式
+     * @param bool      $replace 是否replace
+     * @return string
+     * @throws Exception
+     */
+    public function insertAll($dataSet, $options = [], $replace = false)
+    {
+        // 获取合法的字段
+        if ('*' == $options['field']) {
+            $fields = array_keys($this->query->getFieldsType($options['table']));
+        } else {
+            $fields = $options['field'];
+        }
+
+        foreach ($dataSet as $data) {
+            foreach ($data as $key => $val) {
+                if (!in_array($key, $fields, true)) {
+                    if ($options['strict']) {
+                        throw new Exception('fields not exists:[' . $key . ']');
+                    }
+                    unset($data[$key]);
+                } elseif (is_null($val)) {
+                    $data[$key] = 'NULL';
+                } elseif (is_scalar($val)) {
+                    $data[$key] = $this->parseValue($val, $key);
+                } elseif (is_object($val) && method_exists($val, '__toString')) {
+                    // 对象数据写入
+                    $data[$key] = $val->__toString();
+                } else {
+                    // 过滤掉非标量数据
+                    unset($data[$key]);
+                }
+            }
+            $value    = array_values($data);
+            $values[] = 'SELECT ' . implode(',', $value);
+
+            if (!isset($insertFields)) {
+                $insertFields = array_keys($data);
+            }
+        }
+
+        foreach ($insertFields as $field) {
+            $fields[] = $this->parseKey($field, $options, true);
+        }
+
+        return str_replace(
+            ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
+            [
+                $replace ? 'REPLACE' : 'INSERT',
+                $this->parseTable($options['table'], $options),
+                implode(' , ', $insertFields),
+                implode(' UNION ALL ', $values),
+                $this->parseComment($options['comment']),
+            ], $this->insertAllSql);
+    }
+
+    /**
+     * 生成select insert SQL
+     * @access public
+     * @param array     $fields 数据
+     * @param string    $table 数据表
+     * @param array     $options 表达式
+     * @return string
+     */
+    public function selectInsert($fields, $table, $options)
+    {
+        if (is_string($fields)) {
+            $fields = explode(',', $fields);
+        }
+
+        $fields = array_map([$this, 'parseKey'], $fields);
+        $sql    = 'INSERT INTO ' . $this->parseTable($table, $options) . ' (' . implode(',', $fields) . ') ' . $this->select($options);
+        return $sql;
+    }
+
+    /**
+     * 生成update SQL
+     * @access public
+     * @param array     $data 数据
+     * @param array     $options 表达式
+     * @return string
+     */
+    public function update($data, $options)
+    {
+        $table = $this->parseTable($options['table'], $options);
+        $data  = $this->parseData($data, $options);
+        if (empty($data)) {
+            return '';
+        }
+        foreach ($data as $key => $val) {
+            $set[] = $key . '=' . $val;
+        }
+
+        $sql = str_replace(
+            ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
+            [
+                $this->parseTable($options['table'], $options),
+                implode(',', $set),
+                $this->parseJoin($options['join'], $options),
+                $this->parseWhere($options['where'], $options),
+                $this->parseOrder($options['order'], $options),
+                $this->parseLimit($options['limit']),
+                $this->parseLock($options['lock']),
+                $this->parseComment($options['comment']),
+            ], $this->updateSql);
+
+        return $sql;
+    }
+
+    /**
+     * 生成delete SQL
+     * @access public
+     * @param array $options 表达式
+     * @return string
+     */
+    public function delete($options)
+    {
+        $sql = str_replace(
+            ['%TABLE%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
+            [
+                $this->parseTable($options['table'], $options),
+                !empty($options['using']) ? ' USING ' . $this->parseTable($options['using'], $options) . ' ' : '',
+                $this->parseJoin($options['join'], $options),
+                $this->parseWhere($options['where'], $options),
+                $this->parseOrder($options['order'], $options),
+                $this->parseLimit($options['limit']),
+                $this->parseLock($options['lock']),
+                $this->parseComment($options['comment']),
+            ], $this->deleteSql);
+
+        return $sql;
+    }
+}

+ 1070 - 0
thinkphp/library/think/db/Connection.php

@@ -0,0 +1,1070 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\db;
+
+use PDO;
+use PDOStatement;
+use think\Db;
+use think\db\exception\BindParamException;
+use think\Debug;
+use think\Exception;
+use think\exception\PDOException;
+use think\Log;
+
+/**
+ * Class Connection
+ * @package think
+ * @method Query table(string $table) 指定数据表(含前缀)
+ * @method Query name(string $name) 指定数据表(不含前缀)
+ *
+ */
+abstract class Connection
+{
+
+    /** @var PDOStatement PDO操作实例 */
+    protected $PDOStatement;
+
+    /** @var string 当前SQL指令 */
+    protected $queryStr = '';
+    // 返回或者影响记录数
+    protected $numRows = 0;
+    // 事务指令数
+    protected $transTimes = 0;
+    // 错误信息
+    protected $error = '';
+
+    /** @var PDO[] 数据库连接ID 支持多个连接 */
+    protected $links = [];
+
+    /** @var PDO 当前连接ID */
+    protected $linkID;
+    protected $linkRead;
+    protected $linkWrite;
+
+    // 查询结果类型
+    protected $fetchType = PDO::FETCH_ASSOC;
+    // 字段属性大小写
+    protected $attrCase = PDO::CASE_LOWER;
+    // 监听回调
+    protected static $event = [];
+    // 使用Builder类
+    protected $builder;
+    // 数据库连接参数配置
+    protected $config = [
+        // 数据库类型
+        'type'            => '',
+        // 服务器地址
+        'hostname'        => '',
+        // 数据库名
+        'database'        => '',
+        // 用户名
+        'username'        => '',
+        // 密码
+        'password'        => '',
+        // 端口
+        'hostport'        => '',
+        // 连接dsn
+        'dsn'             => '',
+        // 数据库连接参数
+        'params'          => [],
+        // 数据库编码默认采用utf8
+        'charset'         => 'utf8',
+        // 数据库表前缀
+        'prefix'          => '',
+        // 数据库调试模式
+        'debug'           => false,
+        // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
+        'deploy'          => 0,
+        // 数据库读写是否分离 主从式有效
+        'rw_separate'     => false,
+        // 读写分离后 主服务器数量
+        'master_num'      => 1,
+        // 指定从服务器序号
+        'slave_no'        => '',
+        // 模型写入后自动读取主服务器
+        'read_master'     => false,
+        // 是否严格检查字段是否存在
+        'fields_strict'   => true,
+        // 数据返回类型
+        'result_type'     => PDO::FETCH_ASSOC,
+        // 数据集返回类型
+        'resultset_type'  => 'array',
+        // 自动写入时间戳字段
+        'auto_timestamp'  => false,
+        // 时间字段取出后的默认时间格式
+        'datetime_format' => 'Y-m-d H:i:s',
+        // 是否需要进行SQL性能分析
+        'sql_explain'     => false,
+        // Builder类
+        'builder'         => '',
+        // Query类
+        'query'           => '\\think\\db\\Query',
+        // 是否需要断线重连
+        'break_reconnect' => false,
+    ];
+
+    // PDO连接参数
+    protected $params = [
+        PDO::ATTR_CASE              => PDO::CASE_NATURAL,
+        PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
+        PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,
+        PDO::ATTR_STRINGIFY_FETCHES => false,
+        PDO::ATTR_EMULATE_PREPARES  => false,
+    ];
+
+    // 绑定参数
+    protected $bind = [];
+
+    /**
+     * 构造函数 读取数据库配置信息
+     * @access public
+     * @param array $config 数据库配置数组
+     */
+    public function __construct(array $config = [])
+    {
+        if (!empty($config)) {
+            $this->config = array_merge($this->config, $config);
+        }
+    }
+
+    /**
+     * 获取新的查询对象
+     * @access protected
+     * @return Query
+     */
+    protected function getQuery()
+    {
+        $class = $this->config['query'];
+        return new $class($this);
+    }
+
+    /**
+     * 获取当前连接器类对应的Builder类
+     * @access public
+     * @return string
+     */
+    public function getBuilder()
+    {
+        if (!empty($this->builder)) {
+            return $this->builder;
+        } else {
+            return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
+        }
+    }
+
+    /**
+     * 调用Query类的查询方法
+     * @access public
+     * @param string    $method 方法名称
+     * @param array     $args 调用参数
+     * @return mixed
+     */
+    public function __call($method, $args)
+    {
+        return call_user_func_array([$this->getQuery(), $method], $args);
+    }
+
+    /**
+     * 解析pdo连接的dsn信息
+     * @access protected
+     * @param array $config 连接信息
+     * @return string
+     */
+    abstract protected function parseDsn($config);
+
+    /**
+     * 取得数据表的字段信息
+     * @access public
+     * @param string $tableName
+     * @return array
+     */
+    abstract public function getFields($tableName);
+
+    /**
+     * 取得数据库的表信息
+     * @access public
+     * @param string $dbName
+     * @return array
+     */
+    abstract public function getTables($dbName);
+
+    /**
+     * SQL性能分析
+     * @access protected
+     * @param string $sql
+     * @return array
+     */
+    abstract protected function getExplain($sql);
+
+    /**
+     * 对返数据表字段信息进行大小写转换出来
+     * @access public
+     * @param array $info 字段信息
+     * @return array
+     */
+    public function fieldCase($info)
+    {
+        // 字段大小写转换
+        switch ($this->attrCase) {
+            case PDO::CASE_LOWER:
+                $info = array_change_key_case($info);
+                break;
+            case PDO::CASE_UPPER:
+                $info = array_change_key_case($info, CASE_UPPER);
+                break;
+            case PDO::CASE_NATURAL:
+            default:
+                // 不做转换
+        }
+        return $info;
+    }
+
+    /**
+     * 获取数据库的配置参数
+     * @access public
+     * @param string $config 配置名称
+     * @return mixed
+     */
+    public function getConfig($config = '')
+    {
+        return $config ? $this->config[$config] : $this->config;
+    }
+
+    /**
+     * 设置数据库的配置参数
+     * @access public
+     * @param string|array      $config 配置名称
+     * @param mixed             $value 配置值
+     * @return void
+     */
+    public function setConfig($config, $value = '')
+    {
+        if (is_array($config)) {
+            $this->config = array_merge($this->config, $config);
+        } else {
+            $this->config[$config] = $value;
+        }
+    }
+
+    /**
+     * 连接数据库方法
+     * @access public
+     * @param array         $config 连接参数
+     * @param integer       $linkNum 连接序号
+     * @param array|bool    $autoConnection 是否自动连接主数据库(用于分布式)
+     * @return PDO
+     * @throws Exception
+     */
+    public function connect(array $config = [], $linkNum = 0, $autoConnection = false)
+    {
+        if (!isset($this->links[$linkNum])) {
+            if (!$config) {
+                $config = $this->config;
+            } else {
+                $config = array_merge($this->config, $config);
+            }
+            // 连接参数
+            if (isset($config['params']) && is_array($config['params'])) {
+                $params = $config['params'] + $this->params;
+            } else {
+                $params = $this->params;
+            }
+            // 记录当前字段属性大小写设置
+            $this->attrCase = $params[PDO::ATTR_CASE];
+
+            // 数据返回类型
+            if (isset($config['result_type'])) {
+                $this->fetchType = $config['result_type'];
+            }
+            try {
+                if (empty($config['dsn'])) {
+                    $config['dsn'] = $this->parseDsn($config);
+                }
+                if ($config['debug']) {
+                    $startTime = microtime(true);
+                }
+                $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params);
+                if ($config['debug']) {
+                    // 记录数据库连接信息
+                    Log::record('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn'], 'sql');
+                }
+            } catch (\PDOException $e) {
+                if ($autoConnection) {
+                    Log::record($e->getMessage(), 'error');
+                    return $this->connect($autoConnection, $linkNum);
+                } else {
+                    throw $e;
+                }
+            }
+        }
+        return $this->links[$linkNum];
+    }
+
+    /**
+     * 释放查询结果
+     * @access public
+     */
+    public function free()
+    {
+        $this->PDOStatement = null;
+    }
+
+    /**
+     * 获取PDO对象
+     * @access public
+     * @return \PDO|false
+     */
+    public function getPdo()
+    {
+        if (!$this->linkID) {
+            return false;
+        } else {
+            return $this->linkID;
+        }
+    }
+
+    /**
+     * 执行查询 返回数据集
+     * @access public
+     * @param string        $sql sql指令
+     * @param array         $bind 参数绑定
+     * @param bool          $master 是否在主服务器读操作
+     * @param bool          $pdo 是否返回PDO对象
+     * @return mixed
+     * @throws PDOException
+     * @throws \Exception
+     */
+    public function query($sql, $bind = [], $master = false, $pdo = false)
+    {
+        $this->initConnect($master);
+        if (!$this->linkID) {
+            return false;
+        }
+
+        // 记录SQL语句
+        $this->queryStr = $sql;
+        if ($bind) {
+            $this->bind = $bind;
+        }
+
+        Db::$queryTimes++;
+        try {
+            // 调试开始
+            $this->debug(true);
+
+            // 释放前次的查询结果
+            if (!empty($this->PDOStatement)) {
+                $this->free();
+            }
+            // 预处理
+            if (empty($this->PDOStatement)) {
+                $this->PDOStatement = $this->linkID->prepare($sql);
+            }
+            // 是否为存储过程调用
+            $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
+            // 参数绑定
+            if ($procedure) {
+                $this->bindParam($bind);
+            } else {
+                $this->bindValue($bind);
+            }
+            // 执行查询
+            $this->PDOStatement->execute();
+            // 调试结束
+            $this->debug(false, '', $master);
+            // 返回结果集
+            return $this->getResult($pdo, $procedure);
+        } catch (\PDOException $e) {
+            if ($this->isBreak($e)) {
+                return $this->close()->query($sql, $bind, $master, $pdo);
+            }
+            throw new PDOException($e, $this->config, $this->getLastsql());
+        } catch (\Throwable $e) {
+            if ($this->isBreak($e)) {
+                return $this->close()->query($sql, $bind, $master, $pdo);
+            }
+            throw $e;
+        } catch (\Exception $e) {
+            if ($this->isBreak($e)) {
+                return $this->close()->query($sql, $bind, $master, $pdo);
+            }
+            throw $e;
+        }
+    }
+
+    /**
+     * 执行语句
+     * @access public
+     * @param  string        $sql sql指令
+     * @param  array         $bind 参数绑定
+     * @param  Query         $query 查询对象
+     * @return int
+     * @throws PDOException
+     * @throws \Exception
+     */
+    public function execute($sql, $bind = [], Query $query = null)
+    {
+        $this->initConnect(true);
+        if (!$this->linkID) {
+            return false;
+        }
+
+        // 记录SQL语句
+        $this->queryStr = $sql;
+        if ($bind) {
+            $this->bind = $bind;
+        }
+
+        Db::$executeTimes++;
+        try {
+            // 调试开始
+            $this->debug(true);
+
+            //释放前次的查询结果
+            if (!empty($this->PDOStatement) && $this->PDOStatement->queryString != $sql) {
+                $this->free();
+            }
+            // 预处理
+            if (empty($this->PDOStatement)) {
+                $this->PDOStatement = $this->linkID->prepare($sql);
+            }
+            // 是否为存储过程调用
+            $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
+            // 参数绑定
+            if ($procedure) {
+                $this->bindParam($bind);
+            } else {
+                $this->bindValue($bind);
+            }
+            // 执行语句
+            $this->PDOStatement->execute();
+            // 调试结束
+            $this->debug(false, '', true);
+
+            if ($query && !empty($this->config['deploy']) && !empty($this->config['read_master'])) {
+                $query->readMaster();
+            }
+
+            $this->numRows = $this->PDOStatement->rowCount();
+            return $this->numRows;
+        } catch (\PDOException $e) {
+            if ($this->isBreak($e)) {
+                return $this->close()->execute($sql, $bind, $query);
+            }
+            throw new PDOException($e, $this->config, $this->getLastsql());
+        } catch (\Throwable $e) {
+            if ($this->isBreak($e)) {
+                return $this->close()->execute($sql, $bind, $query);
+            }
+            throw $e;
+        } catch (\Exception $e) {
+            if ($this->isBreak($e)) {
+                return $this->close()->execute($sql, $bind, $query);
+            }
+            throw $e;
+        }
+    }
+
+    /**
+     * 根据参数绑定组装最终的SQL语句 便于调试
+     * @access public
+     * @param string    $sql 带参数绑定的sql语句
+     * @param array     $bind 参数绑定列表
+     * @return string
+     */
+    public function getRealSql($sql, array $bind = [])
+    {
+        if (is_array($sql)) {
+            $sql = implode(';', $sql);
+        }
+
+        foreach ($bind as $key => $val) {
+            $value = is_array($val) ? $val[0] : $val;
+            $type  = is_array($val) ? $val[1] : PDO::PARAM_STR;
+            if (PDO::PARAM_STR == $type) {
+                $value = $this->quote($value);
+            } elseif (PDO::PARAM_INT == $type) {
+                $value = (float) $value;
+            }
+            // 判断占位符
+            $sql = is_numeric($key) ?
+            substr_replace($sql, $value, strpos($sql, '?'), 1) :
+            str_replace(
+                [':' . $key . ')', ':' . $key . ',', ':' . $key . ' ', ':' . $key . PHP_EOL],
+                [$value . ')', $value . ',', $value . ' ', $value . PHP_EOL],
+                $sql . ' ');
+        }
+        return rtrim($sql);
+    }
+
+    /**
+     * 参数绑定
+     * 支持 ['name'=>'value','id'=>123] 对应命名占位符
+     * 或者 ['value',123] 对应问号占位符
+     * @access public
+     * @param array $bind 要绑定的参数列表
+     * @return void
+     * @throws BindParamException
+     */
+    protected function bindValue(array $bind = [])
+    {
+        foreach ($bind as $key => $val) {
+            // 占位符
+            $param = is_numeric($key) ? $key + 1 : ':' . $key;
+            if (is_array($val)) {
+                if (PDO::PARAM_INT == $val[1] && '' === $val[0]) {
+                    $val[0] = 0;
+                }
+                $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]);
+            } else {
+                $result = $this->PDOStatement->bindValue($param, $val);
+            }
+            if (!$result) {
+                throw new BindParamException(
+                    "Error occurred  when binding parameters '{$param}'",
+                    $this->config,
+                    $this->getLastsql(),
+                    $bind
+                );
+            }
+        }
+    }
+
+    /**
+     * 存储过程的输入输出参数绑定
+     * @access public
+     * @param array $bind 要绑定的参数列表
+     * @return void
+     * @throws BindParamException
+     */
+    protected function bindParam($bind)
+    {
+        foreach ($bind as $key => $val) {
+            $param = is_numeric($key) ? $key + 1 : ':' . $key;
+            if (is_array($val)) {
+                array_unshift($val, $param);
+                $result = call_user_func_array([$this->PDOStatement, 'bindParam'], $val);
+            } else {
+                $result = $this->PDOStatement->bindValue($param, $val);
+            }
+            if (!$result) {
+                $param = array_shift($val);
+                throw new BindParamException(
+                    "Error occurred  when binding parameters '{$param}'",
+                    $this->config,
+                    $this->getLastsql(),
+                    $bind
+                );
+            }
+        }
+    }
+
+    /**
+     * 获得数据集数组
+     * @access protected
+     * @param bool   $pdo 是否返回PDOStatement
+     * @param bool   $procedure 是否存储过程
+     * @return PDOStatement|array
+     */
+    protected function getResult($pdo = false, $procedure = false)
+    {
+        if ($pdo) {
+            // 返回PDOStatement对象处理
+            return $this->PDOStatement;
+        }
+        if ($procedure) {
+            // 存储过程返回结果
+            return $this->procedure();
+        }
+        $result        = $this->PDOStatement->fetchAll($this->fetchType);
+        $this->numRows = count($result);
+        return $result;
+    }
+
+    /**
+     * 获得存储过程数据集
+     * @access protected
+     * @return array
+     */
+    protected function procedure()
+    {
+        $item = [];
+        do {
+            $result = $this->getResult();
+            if ($result) {
+                $item[] = $result;
+            }
+        } while ($this->PDOStatement->nextRowset());
+        $this->numRows = count($item);
+        return $item;
+    }
+
+    /**
+     * 执行数据库事务
+     * @access public
+     * @param callable $callback 数据操作方法回调
+     * @return mixed
+     * @throws PDOException
+     * @throws \Exception
+     * @throws \Throwable
+     */
+    public function transaction($callback)
+    {
+        $this->startTrans();
+        try {
+            $result = null;
+            if (is_callable($callback)) {
+                $result = call_user_func_array($callback, [$this]);
+            }
+            $this->commit();
+            return $result;
+        } catch (\Exception $e) {
+            $this->rollback();
+            throw $e;
+        } catch (\Throwable $e) {
+            $this->rollback();
+            throw $e;
+        }
+    }
+
+    /**
+     * 启动事务
+     * @access public
+     * @return bool|mixed
+     * @throws \Exception
+     */
+    public function startTrans()
+    {
+        $this->initConnect(true);
+        if (!$this->linkID) {
+            return false;
+        }
+
+        ++$this->transTimes;
+        try {
+            if (1 == $this->transTimes) {
+                $this->linkID->beginTransaction();
+            } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
+                $this->linkID->exec(
+                    $this->parseSavepoint('trans' . $this->transTimes)
+                );
+            }
+
+        } catch (\PDOException $e) {
+            if ($this->isBreak($e)) {
+                return $this->close()->startTrans();
+            }
+            throw $e;
+        } catch (\Exception $e) {
+            if ($this->isBreak($e)) {
+                return $this->close()->startTrans();
+            }
+            throw $e;
+        } catch (\Error $e) {
+            if ($this->isBreak($e)) {
+                return $this->close()->startTrans();
+            }
+            throw $e;
+        }
+    }
+
+    /**
+     * 用于非自动提交状态下面的查询提交
+     * @access public
+     * @return void
+     * @throws PDOException
+     */
+    public function commit()
+    {
+        $this->initConnect(true);
+
+        if (1 == $this->transTimes) {
+            $this->linkID->commit();
+        }
+
+        --$this->transTimes;
+    }
+
+    /**
+     * 事务回滚
+     * @access public
+     * @return void
+     * @throws PDOException
+     */
+    public function rollback()
+    {
+        $this->initConnect(true);
+
+        if (1 == $this->transTimes) {
+            $this->linkID->rollBack();
+        } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
+            $this->linkID->exec(
+                $this->parseSavepointRollBack('trans' . $this->transTimes)
+            );
+        }
+
+        $this->transTimes = max(0, $this->transTimes - 1);
+    }
+
+    /**
+     * 是否支持事务嵌套
+     * @return bool
+     */
+    protected function supportSavepoint()
+    {
+        return false;
+    }
+
+    /**
+     * 生成定义保存点的SQL
+     * @param $name
+     * @return string
+     */
+    protected function parseSavepoint($name)
+    {
+        return 'SAVEPOINT ' . $name;
+    }
+
+    /**
+     * 生成回滚到保存点的SQL
+     * @param $name
+     * @return string
+     */
+    protected function parseSavepointRollBack($name)
+    {
+        return 'ROLLBACK TO SAVEPOINT ' . $name;
+    }
+
+    /**
+     * 批处理执行SQL语句
+     * 批处理的指令都认为是execute操作
+     * @access public
+     * @param array $sqlArray SQL批处理指令
+     * @return boolean
+     */
+    public function batchQuery($sqlArray = [], $bind = [], Query $query = null)
+    {
+        if (!is_array($sqlArray)) {
+            return false;
+        }
+        // 自动启动事务支持
+        $this->startTrans();
+        try {
+            foreach ($sqlArray as $sql) {
+                $this->execute($sql, $bind, $query);
+            }
+            // 提交事务
+            $this->commit();
+        } catch (\Exception $e) {
+            $this->rollback();
+            throw $e;
+        }
+
+        return true;
+    }
+
+    /**
+     * 获得查询次数
+     * @access public
+     * @param boolean $execute 是否包含所有查询
+     * @return integer
+     */
+    public function getQueryTimes($execute = false)
+    {
+        return $execute ? Db::$queryTimes + Db::$executeTimes : Db::$queryTimes;
+    }
+
+    /**
+     * 获得执行次数
+     * @access public
+     * @return integer
+     */
+    public function getExecuteTimes()
+    {
+        return Db::$executeTimes;
+    }
+
+    /**
+     * 关闭数据库(或者重新连接)
+     * @access public
+     * @return $this
+     */
+    public function close()
+    {
+        $this->linkID    = null;
+        $this->linkWrite = null;
+        $this->linkRead  = null;
+        $this->links     = [];
+        return $this;
+    }
+
+    /**
+     * 是否断线
+     * @access protected
+     * @param \PDOException|\Exception  $e 异常对象
+     * @return bool
+     */
+    protected function isBreak($e)
+    {
+        if (!$this->config['break_reconnect']) {
+            return false;
+        }
+
+        $info = [
+            'server has gone away',
+            'no connection to the server',
+            'Lost connection',
+            'is dead or not enabled',
+            'Error while sending',
+            'decryption failed or bad record mac',
+            'server closed the connection unexpectedly',
+            'SSL connection has been closed unexpectedly',
+            'Error writing data to the connection',
+            'Resource deadlock avoided',
+            'failed with errno',
+        ];
+
+        $error = $e->getMessage();
+
+        foreach ($info as $msg) {
+            if (false !== stripos($error, $msg)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 获取最近一次查询的sql语句
+     * @access public
+     * @return string
+     */
+    public function getLastSql()
+    {
+        return $this->getRealSql($this->queryStr, $this->bind);
+    }
+
+    /**
+     * 获取最近插入的ID
+     * @access public
+     * @param string  $sequence     自增序列名
+     * @return string
+     */
+    public function getLastInsID($sequence = null)
+    {
+        return $this->linkID->lastInsertId($sequence);
+    }
+
+    /**
+     * 获取返回或者影响的记录数
+     * @access public
+     * @return integer
+     */
+    public function getNumRows()
+    {
+        return $this->numRows;
+    }
+
+    /**
+     * 获取最近的错误信息
+     * @access public
+     * @return string
+     */
+    public function getError()
+    {
+        if ($this->PDOStatement) {
+            $error = $this->PDOStatement->errorInfo();
+            $error = $error[1] . ':' . $error[2];
+        } else {
+            $error = '';
+        }
+        if ('' != $this->queryStr) {
+            $error .= "\n [ SQL语句 ] : " . $this->getLastsql();
+        }
+        return $error;
+    }
+
+    /**
+     * SQL指令安全过滤
+     * @access public
+     * @param string $str SQL字符串
+     * @param bool   $master 是否主库查询
+     * @return string
+     */
+    public function quote($str, $master = true)
+    {
+        $this->initConnect($master);
+        return $this->linkID ? $this->linkID->quote($str) : $str;
+    }
+
+    /**
+     * 数据库调试 记录当前SQL及分析性能
+     * @access protected
+     * @param boolean $start 调试开始标记 true 开始 false 结束
+     * @param string  $sql 执行的SQL语句 留空自动获取
+     * @param boolean $master 主从标记
+     * @return void
+     */
+    protected function debug($start, $sql = '', $master = false)
+    {
+        if (!empty($this->config['debug'])) {
+            // 开启数据库调试模式
+            if ($start) {
+                Debug::remark('queryStartTime', 'time');
+            } else {
+                // 记录操作结束时间
+                Debug::remark('queryEndTime', 'time');
+                $runtime = Debug::getRangeTime('queryStartTime', 'queryEndTime');
+                $sql     = $sql ?: $this->getLastsql();
+                $result  = [];
+                // SQL性能分析
+                if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) {
+                    $result = $this->getExplain($sql);
+                }
+                // SQL监听
+                $this->trigger($sql, $runtime, $result, $master);
+            }
+        }
+    }
+
+    /**
+     * 监听SQL执行
+     * @access public
+     * @param callable $callback 回调方法
+     * @return void
+     */
+    public function listen($callback)
+    {
+        self::$event[] = $callback;
+    }
+
+    /**
+     * 触发SQL事件
+     * @access protected
+     * @param string    $sql SQL语句
+     * @param float     $runtime SQL运行时间
+     * @param mixed     $explain SQL分析
+     * @param  bool     $master 主从标记
+     * @return void
+     */
+    protected function trigger($sql, $runtime, $explain = [], $master = false)
+    {
+        if (!empty(self::$event)) {
+            foreach (self::$event as $callback) {
+                if (is_callable($callback)) {
+                    call_user_func_array($callback, [$sql, $runtime, $explain, $master]);
+                }
+            }
+        } else {
+            // 未注册监听则记录到日志中
+            if ($this->config['deploy']) {
+                // 分布式记录当前操作的主从
+                $master = $master ? 'master|' : 'slave|';
+            } else {
+                $master = '';
+            }
+
+            Log::record('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $runtime . 's ]', 'sql');
+            if (!empty($explain)) {
+                Log::record('[ EXPLAIN : ' . var_export($explain, true) . ' ]', 'sql');
+            }
+        }
+    }
+
+    /**
+     * 初始化数据库连接
+     * @access protected
+     * @param boolean $master 是否主服务器
+     * @return void
+     */
+    protected function initConnect($master = true)
+    {
+        if (!empty($this->config['deploy'])) {
+            // 采用分布式数据库
+            if ($master || $this->transTimes) {
+                if (!$this->linkWrite) {
+                    $this->linkWrite = $this->multiConnect(true);
+                }
+                $this->linkID = $this->linkWrite;
+            } else {
+                if (!$this->linkRead) {
+                    $this->linkRead = $this->multiConnect(false);
+                }
+                $this->linkID = $this->linkRead;
+            }
+        } elseif (!$this->linkID) {
+            // 默认单数据库
+            $this->linkID = $this->connect();
+        }
+    }
+
+    /**
+     * 连接分布式服务器
+     * @access protected
+     * @param boolean $master 主服务器
+     * @return PDO
+     */
+    protected function multiConnect($master = false)
+    {
+        $_config = [];
+        // 分布式数据库配置解析
+        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
+            $_config[$name] = explode(',', $this->config[$name]);
+        }
+
+        // 主服务器序号
+        $m = floor(mt_rand(0, $this->config['master_num'] - 1));
+
+        if ($this->config['rw_separate']) {
+            // 主从式采用读写分离
+            if ($master) // 主服务器写入
+            {
+                $r = $m;
+            } elseif (is_numeric($this->config['slave_no'])) {
+                // 指定服务器读
+                $r = $this->config['slave_no'];
+            } else {
+                // 读操作连接从服务器 每次随机连接的数据库
+                $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1));
+            }
+        } else {
+            // 读写操作不区分服务器 每次随机连接的数据库
+            $r = floor(mt_rand(0, count($_config['hostname']) - 1));
+        }
+        $dbMaster = false;
+        if ($m != $r) {
+            $dbMaster = [];
+            foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
+                $dbMaster[$name] = isset($_config[$name][$m]) ? $_config[$name][$m] : $_config[$name][0];
+            }
+        }
+        $dbConfig = [];
+        foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) {
+            $dbConfig[$name] = isset($_config[$name][$r]) ? $_config[$name][$r] : $_config[$name][0];
+        }
+        return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
+    }
+
+    /**
+     * 析构方法
+     * @access public
+     */
+    public function __destruct()
+    {
+        // 释放查询
+        if ($this->PDOStatement) {
+            $this->free();
+        }
+        // 关闭连接
+        $this->close();
+    }
+}

+ 48 - 0
thinkphp/library/think/db/Expression.php

@@ -0,0 +1,48 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\db;
+
+class Expression
+{
+    /**
+     * 查询表达式
+     *
+     * @var string
+     */
+    protected $value;
+
+    /**
+     * 创建一个查询表达式
+     *
+     * @param  string  $value
+     * @return void
+     */
+    public function __construct($value)
+    {
+        $this->value = $value;
+    }
+
+    /**
+     * 获取表达式
+     *
+     * @return string
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    public function __toString()
+    {
+        return (string) $this->value;
+    }
+}

+ 2998 - 0
thinkphp/library/think/db/Query.php

@@ -0,0 +1,2998 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\db;
+
+use PDO;
+use think\App;
+use think\Cache;
+use think\Collection;
+use think\Config;
+use think\Db;
+use think\db\exception\BindParamException;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\ModelNotFoundException;
+use think\Exception;
+use think\exception\DbException;
+use think\exception\PDOException;
+use think\Loader;
+use think\Model;
+use think\model\Relation;
+use think\model\relation\OneToOne;
+use think\Paginator;
+
+class Query
+{
+    // 数据库Connection对象实例
+    protected $connection;
+    // 数据库Builder对象实例
+    protected $builder;
+    // 当前模型类名称
+    protected $model;
+    // 当前数据表名称(含前缀)
+    protected $table = '';
+    // 当前数据表名称(不含前缀)
+    protected $name = '';
+    // 当前数据表主键
+    protected $pk;
+    // 当前数据表前缀
+    protected $prefix = '';
+    // 查询参数
+    protected $options = [];
+    // 参数绑定
+    protected $bind = [];
+    // 数据表信息
+    protected static $info = [];
+    // 回调事件
+    private static $event = [];
+    // 读取主库
+    private static $readMaster = [];
+
+    /**
+     * 构造函数
+     * @access public
+     * @param Connection $connection 数据库对象实例
+     * @param Model      $model      模型对象
+     */
+    public function __construct(Connection $connection = null, $model = null)
+    {
+        $this->connection = $connection ?: Db::connect([], true);
+        $this->prefix     = $this->connection->getConfig('prefix');
+        $this->model      = $model;
+        // 设置当前连接的Builder对象
+        $this->setBuilder();
+    }
+
+    /**
+     * 利用__call方法实现一些特殊的Model方法
+     * @access public
+     * @param string $method 方法名称
+     * @param array  $args   调用参数
+     * @return mixed
+     * @throws DbException
+     * @throws Exception
+     */
+    public function __call($method, $args)
+    {
+        if (strtolower(substr($method, 0, 5)) == 'getby') {
+            // 根据某个字段获取记录
+            $field         = Loader::parseName(substr($method, 5));
+            $where[$field] = $args[0];
+            return $this->where($where)->find();
+        } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') {
+            // 根据某个字段获取记录的某个值
+            $name         = Loader::parseName(substr($method, 10));
+            $where[$name] = $args[0];
+            return $this->where($where)->value($args[1]);
+        } else {
+            throw new Exception('method not exist:' . __CLASS__ . '->' . $method);
+        }
+    }
+
+    /**
+     * 获取当前的数据库Connection对象
+     * @access public
+     * @return Connection
+     */
+    public function getConnection()
+    {
+        return $this->connection;
+    }
+
+    /**
+     * 切换当前的数据库连接
+     * @access public
+     * @param mixed $config
+     * @return $this
+     */
+    public function connect($config)
+    {
+        $this->connection = Db::connect($config);
+        $this->setBuilder();
+        $this->prefix = $this->connection->getConfig('prefix');
+        return $this;
+    }
+
+    /**
+     * 设置当前的数据库Builder对象
+     * @access protected
+     * @return void
+     */
+    protected function setBuilder()
+    {
+        $class         = $this->connection->getBuilder();
+        $this->builder = new $class($this->connection, $this);
+    }
+
+    /**
+     * 获取当前的模型对象实例
+     * @access public
+     * @return Model|null
+     */
+    public function getModel()
+    {
+        return $this->model;
+    }
+
+    /**
+     * 设置后续从主库读取数据
+     * @access public
+     * @param  bool $allTable
+     * @return void
+     */
+    public function readMaster($allTable = false)
+    {
+        if ($allTable) {
+            $table = '*';
+        } else {
+            $table = isset($this->options['table']) ? $this->options['table'] : $this->getTable();
+        }
+
+        static::$readMaster[$table] = true;
+
+        return $this;
+    }
+
+    /**
+     * 获取当前的builder实例对象
+     * @access public
+     * @return Builder
+     */
+    public function getBuilder()
+    {
+        return $this->builder;
+    }
+
+    /**
+     * 指定默认的数据表名(不含前缀)
+     * @access public
+     * @param string $name
+     * @return $this
+     */
+    public function name($name)
+    {
+        $this->name = $name;
+        return $this;
+    }
+
+    /**
+     * 指定默认数据表名(含前缀)
+     * @access public
+     * @param string $table 表名
+     * @return $this
+     */
+    public function setTable($table)
+    {
+        $this->table = $table;
+        return $this;
+    }
+
+    /**
+     * 得到当前或者指定名称的数据表
+     * @access public
+     * @param string $name
+     * @return string
+     */
+    public function getTable($name = '')
+    {
+        if ($name || empty($this->table)) {
+            $name      = $name ?: $this->name;
+            $tableName = $this->prefix;
+            if ($name) {
+                $tableName .= Loader::parseName($name);
+            }
+        } else {
+            $tableName = $this->table;
+        }
+        return $tableName;
+    }
+
+    /**
+     * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写)
+     * @access public
+     * @param string $sql sql语句
+     * @return string
+     */
+    public function parseSqlTable($sql)
+    {
+        if (false !== strpos($sql, '__')) {
+            $prefix = $this->prefix;
+            $sql    = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) use ($prefix) {
+                return $prefix . strtolower($match[1]);
+            }, $sql);
+        }
+        return $sql;
+    }
+
+    /**
+     * 执行查询 返回数据集
+     * @access public
+     * @param string      $sql    sql指令
+     * @param array       $bind   参数绑定
+     * @param boolean     $master 是否在主服务器读操作
+     * @param bool|string $class  指定返回的数据集对象
+     * @return mixed
+     * @throws BindParamException
+     * @throws PDOException
+     */
+    public function query($sql, $bind = [], $master = false, $class = false)
+    {
+        return $this->connection->query($sql, $bind, $master, $class);
+    }
+
+    /**
+     * 执行语句
+     * @access public
+     * @param string $sql  sql指令
+     * @param array  $bind 参数绑定
+     * @return int
+     * @throws BindParamException
+     * @throws PDOException
+     */
+    public function execute($sql, $bind = [])
+    {
+        return $this->connection->execute($sql, $bind, $this);
+    }
+
+    /**
+     * 获取最近插入的ID
+     * @access public
+     * @param string $sequence 自增序列名
+     * @return string
+     */
+    public function getLastInsID($sequence = null)
+    {
+        return $this->connection->getLastInsID($sequence);
+    }
+
+    /**
+     * 获取最近一次查询的sql语句
+     * @access public
+     * @return string
+     */
+    public function getLastSql()
+    {
+        return $this->connection->getLastSql();
+    }
+
+    /**
+     * 执行数据库事务
+     * @access public
+     * @param callable $callback 数据操作方法回调
+     * @return mixed
+     */
+    public function transaction($callback)
+    {
+        return $this->connection->transaction($callback);
+    }
+
+    /**
+     * 启动事务
+     * @access public
+     * @return void
+     */
+    public function startTrans()
+    {
+        $this->connection->startTrans();
+    }
+
+    /**
+     * 用于非自动提交状态下面的查询提交
+     * @access public
+     * @return void
+     * @throws PDOException
+     */
+    public function commit()
+    {
+        $this->connection->commit();
+    }
+
+    /**
+     * 事务回滚
+     * @access public
+     * @return void
+     * @throws PDOException
+     */
+    public function rollback()
+    {
+        $this->connection->rollback();
+    }
+
+    /**
+     * 批处理执行SQL语句
+     * 批处理的指令都认为是execute操作
+     * @access public
+     * @param array $sql SQL批处理指令
+     * @return boolean
+     */
+    public function batchQuery($sql = [], $bind = [])
+    {
+        return $this->connection->batchQuery($sql, $bind);
+    }
+
+    /**
+     * 获取数据库的配置参数
+     * @access public
+     * @param string $name 参数名称
+     * @return boolean
+     */
+    public function getConfig($name = '')
+    {
+        return $this->connection->getConfig($name);
+    }
+
+    /**
+     * 得到分表的的数据表名
+     * @access public
+     * @param array  $data  操作的数据
+     * @param string $field 分表依据的字段
+     * @param array  $rule  分表规则
+     * @return string
+     */
+    public function getPartitionTableName($data, $field, $rule = [])
+    {
+        // 对数据表进行分区
+        if ($field && isset($data[$field])) {
+            $value = $data[$field];
+            $type  = $rule['type'];
+            switch ($type) {
+                case 'id':
+                    // 按照id范围分表
+                    $step = $rule['expr'];
+                    $seq  = floor($value / $step) + 1;
+                    break;
+                case 'year':
+                    // 按照年份分表
+                    if (!is_numeric($value)) {
+                        $value = strtotime($value);
+                    }
+                    $seq = date('Y', $value) - $rule['expr'] + 1;
+                    break;
+                case 'mod':
+                    // 按照id的模数分表
+                    $seq = ($value % $rule['num']) + 1;
+                    break;
+                case 'md5':
+                    // 按照md5的序列分表
+                    $seq = (ord(substr(md5($value), 0, 1)) % $rule['num']) + 1;
+                    break;
+                default:
+                    if (function_exists($type)) {
+                        // 支持指定函数哈希
+                        $seq = (ord(substr($type($value), 0, 1)) % $rule['num']) + 1;
+                    } else {
+                        // 按照字段的首字母的值分表
+                        $seq = (ord($value{0}) % $rule['num']) + 1;
+                    }
+            }
+            return $this->getTable() . '_' . $seq;
+        } else {
+            // 当设置的分表字段不在查询条件或者数据中
+            // 进行联合查询,必须设定 partition['num']
+            $tableName = [];
+            for ($i = 0; $i < $rule['num']; $i++) {
+                $tableName[] = 'SELECT * FROM ' . $this->getTable() . '_' . ($i + 1);
+            }
+
+            $tableName = '( ' . implode(" UNION ", $tableName) . ') AS ' . $this->name;
+            return $tableName;
+        }
+    }
+
+    /**
+     * 得到某个字段的值
+     * @access public
+     * @param string $field   字段名
+     * @param mixed  $default 默认值
+     * @param bool   $force   强制转为数字类型
+     * @return mixed
+     */
+    public function value($field, $default = null, $force = false)
+    {
+        $result = false;
+        if (empty($this->options['fetch_sql']) && !empty($this->options['cache'])) {
+            // 判断查询缓存
+            $cache = $this->options['cache'];
+            if (empty($this->options['table'])) {
+                $this->options['table'] = $this->getTable();
+            }
+            $key    = is_string($cache['key']) ? $cache['key'] : md5($this->connection->getConfig('database') . '.' . $field . serialize($this->options) . serialize($this->bind));
+            $result = Cache::get($key);
+        }
+        if (false === $result) {
+            if (isset($this->options['field'])) {
+                unset($this->options['field']);
+            }
+            $pdo = $this->field($field)->limit(1)->getPdo();
+            if (is_string($pdo)) {
+                // 返回SQL语句
+                return $pdo;
+            }
+            $result = $pdo->fetchColumn();
+            if ($force) {
+                $result += 0;
+            }
+
+            if (isset($cache) && false !== $result) {
+                // 缓存数据
+                $this->cacheData($key, $result, $cache);
+            }
+        } else {
+            // 清空查询条件
+            $this->options = [];
+        }
+        return false !== $result ? $result : $default;
+    }
+
+    /**
+     * 得到某个列的数组
+     * @access public
+     * @param string $field 字段名 多个字段用逗号分隔
+     * @param string $key   索引
+     * @return array
+     */
+    public function column($field, $key = '')
+    {
+        $result = false;
+        if (empty($this->options['fetch_sql']) && !empty($this->options['cache'])) {
+            // 判断查询缓存
+            $cache = $this->options['cache'];
+            if (empty($this->options['table'])) {
+                $this->options['table'] = $this->getTable();
+            }
+            $guid   = is_string($cache['key']) ? $cache['key'] : md5($this->connection->getConfig('database') . '.' . $field . serialize($this->options) . serialize($this->bind));
+            $result = Cache::get($guid);
+        }
+        if (false === $result) {
+            if (isset($this->options['field'])) {
+                unset($this->options['field']);
+            }
+            if (is_null($field)) {
+                $field = '*';
+            } elseif ($key && '*' != $field) {
+                $field = $key . ',' . $field;
+            }
+            $pdo = $this->field($field)->getPdo();
+            if (is_string($pdo)) {
+                // 返回SQL语句
+                return $pdo;
+            }
+            if (1 == $pdo->columnCount()) {
+                $result = $pdo->fetchAll(PDO::FETCH_COLUMN);
+            } else {
+                $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC);
+                if ($resultSet) {
+                    $fields = array_keys($resultSet[0]);
+                    $count  = count($fields);
+                    $key1   = array_shift($fields);
+                    $key2   = $fields ? array_shift($fields) : '';
+                    $key    = $key ?: $key1;
+                    if (strpos($key, '.')) {
+                        list($alias, $key) = explode('.', $key);
+                    }
+                    foreach ($resultSet as $val) {
+                        if ($count > 2) {
+                            $result[$val[$key]] = $val;
+                        } elseif (2 == $count) {
+                            $result[$val[$key]] = $val[$key2];
+                        } elseif (1 == $count) {
+                            $result[$val[$key]] = $val[$key1];
+                        }
+                    }
+                } else {
+                    $result = [];
+                }
+            }
+            if (isset($cache) && isset($guid)) {
+                // 缓存数据
+                $this->cacheData($guid, $result, $cache);
+            }
+        } else {
+            // 清空查询条件
+            $this->options = [];
+        }
+        return $result;
+    }
+
+    /**
+     * COUNT查询
+     * @access public
+     * @param string $field 字段名
+     * @return integer|string
+     */
+    public function count($field = '*')
+    {
+        if (isset($this->options['group'])) {
+            // 支持GROUP
+            $options = $this->getOptions();
+            $subSql  = $this->options($options)->field('count(' . $field . ')')->bind($this->bind)->buildSql();
+            return $this->table([$subSql => '_group_count_'])->value('COUNT(*) AS tp_count', 0, true);
+        }
+
+        return $this->value('COUNT(' . $field . ') AS tp_count', 0, true);
+    }
+
+    /**
+     * SUM查询
+     * @access public
+     * @param string $field 字段名
+     * @return float|int
+     */
+    public function sum($field)
+    {
+        return $this->value('SUM(' . $field . ') AS tp_sum', 0, true);
+    }
+
+    /**
+     * MIN查询
+     * @access public
+     * @param string $field 字段名
+     * @param bool   $force   强制转为数字类型
+     * @return mixed
+     */
+    public function min($field, $force = true)
+    {
+        return $this->value('MIN(' . $field . ') AS tp_min', 0, $force);
+    }
+
+    /**
+     * MAX查询
+     * @access public
+     * @param string $field 字段名
+     * @param bool   $force   强制转为数字类型
+     * @return mixed
+     */
+    public function max($field, $force = true)
+    {
+        return $this->value('MAX(' . $field . ') AS tp_max', 0, $force);
+    }
+
+    /**
+     * AVG查询
+     * @access public
+     * @param string $field 字段名
+     * @return float|int
+     */
+    public function avg($field)
+    {
+        return $this->value('AVG(' . $field . ') AS tp_avg', 0, true);
+    }
+
+    /**
+     * 设置记录的某个字段值
+     * 支持使用数据库字段和方法
+     * @access public
+     * @param string|array $field 字段名
+     * @param mixed        $value 字段值
+     * @return integer
+     */
+    public function setField($field, $value = '')
+    {
+        if (is_array($field)) {
+            $data = $field;
+        } else {
+            $data[$field] = $value;
+        }
+        return $this->update($data);
+    }
+
+    /**
+     * 字段值(延迟)增长
+     * @access public
+     * @param string  $field    字段名
+     * @param integer $step     增长值
+     * @param integer $lazyTime 延时时间(s)
+     * @return integer|true
+     * @throws Exception
+     */
+    public function setInc($field, $step = 1, $lazyTime = 0)
+    {
+        $condition = !empty($this->options['where']) ? $this->options['where'] : [];
+        if (empty($condition)) {
+            // 没有条件不做任何更新
+            throw new Exception('no data to update');
+        }
+        if ($lazyTime > 0) {
+            // 延迟写入
+            $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition) . serialize($this->bind));
+            $step = $this->lazyWrite('inc', $guid, $step, $lazyTime);
+            if (false === $step) {
+                // 清空查询条件
+                $this->options = [];
+                return true;
+            }
+        }
+        return $this->setField($field, ['inc', $step]);
+    }
+
+    /**
+     * 字段值(延迟)减少
+     * @access public
+     * @param string  $field    字段名
+     * @param integer $step     减少值
+     * @param integer $lazyTime 延时时间(s)
+     * @return integer|true
+     * @throws Exception
+     */
+    public function setDec($field, $step = 1, $lazyTime = 0)
+    {
+        $condition = !empty($this->options['where']) ? $this->options['where'] : [];
+        if (empty($condition)) {
+            // 没有条件不做任何更新
+            throw new Exception('no data to update');
+        }
+        if ($lazyTime > 0) {
+            // 延迟写入
+            $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition) . serialize($this->bind));
+            $step = $this->lazyWrite('dec', $guid, $step, $lazyTime);
+            if (false === $step) {
+                // 清空查询条件
+                $this->options = [];
+                return true;
+            }
+            return $this->setField($field, ['inc', $step]);
+        }
+        return $this->setField($field, ['dec', $step]);
+    }
+
+    /**
+     * 延时更新检查 返回false表示需要延时
+     * 否则返回实际写入的数值
+     * @access protected
+     * @param string  $type     自增或者自减
+     * @param string  $guid     写入标识
+     * @param integer $step     写入步进值
+     * @param integer $lazyTime 延时时间(s)
+     * @return false|integer
+     */
+    protected function lazyWrite($type, $guid, $step, $lazyTime)
+    {
+        if (!Cache::has($guid . '_time')) {
+            // 计时开始
+            Cache::set($guid . '_time', $_SERVER['REQUEST_TIME'], 0);
+            Cache::$type($guid, $step);
+        } elseif ($_SERVER['REQUEST_TIME'] > Cache::get($guid . '_time') + $lazyTime) {
+            // 删除缓存
+            $value = Cache::$type($guid, $step);
+            Cache::rm($guid);
+            Cache::rm($guid . '_time');
+            return 0 === $value ? false : $value;
+        } else {
+            // 更新缓存
+            Cache::$type($guid, $step);
+        }
+        return false;
+    }
+
+    /**
+     * 查询SQL组装 join
+     * @access public
+     * @param mixed  $join      关联的表名
+     * @param mixed  $condition 条件
+     * @param string $type      JOIN类型
+     * @return $this
+     */
+    public function join($join, $condition = null, $type = 'INNER')
+    {
+        if (empty($condition)) {
+            // 如果为组数,则循环调用join
+            foreach ($join as $key => $value) {
+                if (is_array($value) && 2 <= count($value)) {
+                    $this->join($value[0], $value[1], isset($value[2]) ? $value[2] : $type);
+                }
+            }
+        } else {
+            $table = $this->getJoinTable($join);
+
+            $this->options['join'][] = [$table, strtoupper($type), $condition];
+        }
+        return $this;
+    }
+
+    /**
+     * 获取Join表名及别名 支持
+     * ['prefix_table或者子查询'=>'alias'] 'prefix_table alias' 'table alias'
+     * @access public
+     * @param array|string $join
+     * @return array|string
+     */
+    protected function getJoinTable($join, &$alias = null)
+    {
+        // 传入的表名为数组
+        if (is_array($join)) {
+            $table = $join;
+            $alias = array_shift($join);
+        } else {
+            $join = trim($join);
+            if (false !== strpos($join, '(')) {
+                // 使用子查询
+                $table = $join;
+            } else {
+                $prefix = $this->prefix;
+                if (strpos($join, ' ')) {
+                    // 使用别名
+                    list($table, $alias) = explode(' ', $join);
+                } else {
+                    $table = $join;
+                    if (false === strpos($join, '.') && 0 !== strpos($join, '__')) {
+                        $alias = $join;
+                    }
+                }
+                if ($prefix && false === strpos($table, '.') && 0 !== strpos($table, $prefix) && 0 !== strpos($table, '__')) {
+                    $table = $this->getTable($table);
+                }
+            }
+            if (isset($alias) && $table != $alias) {
+                $table = [$table => $alias];
+            }
+        }
+        return $table;
+    }
+
+    /**
+     * 查询SQL组装 union
+     * @access public
+     * @param mixed   $union
+     * @param boolean $all
+     * @return $this
+     */
+    public function union($union, $all = false)
+    {
+        $this->options['union']['type'] = $all ? 'UNION ALL' : 'UNION';
+
+        if (is_array($union)) {
+            $this->options['union'] = array_merge($this->options['union'], $union);
+        } else {
+            $this->options['union'][] = $union;
+        }
+        return $this;
+    }
+
+    /**
+     * 指定查询字段 支持字段排除和指定数据表
+     * @access public
+     * @param mixed   $field
+     * @param boolean $except    是否排除
+     * @param string  $tableName 数据表名
+     * @param string  $prefix    字段前缀
+     * @param string  $alias     别名前缀
+     * @return $this
+     */
+    public function field($field, $except = false, $tableName = '', $prefix = '', $alias = '')
+    {
+        if (empty($field)) {
+            return $this;
+        } elseif ($field instanceof Expression) {
+            $this->options['field'][] = $field;
+            return $this;
+        }
+
+        if (is_string($field)) {
+            if (preg_match('/[\<\'\"\(]/', $field)) {
+                return $this->fieldRaw($field);
+            }
+            $field = array_map('trim', explode(',', $field));
+        }
+        if (true === $field) {
+            // 获取全部字段
+            $fields = $this->getTableInfo($tableName ?: (isset($this->options['table']) ? $this->options['table'] : ''), 'fields');
+            $field  = $fields ?: ['*'];
+        } elseif ($except) {
+            // 字段排除
+            $fields = $this->getTableInfo($tableName ?: (isset($this->options['table']) ? $this->options['table'] : ''), 'fields');
+            $field  = $fields ? array_diff($fields, $field) : $field;
+        }
+        if ($tableName) {
+            // 添加统一的前缀
+            $prefix = $prefix ?: $tableName;
+            foreach ($field as $key => $val) {
+                if (is_numeric($key)) {
+                    $val = $prefix . '.' . $val . ($alias ? ' AS ' . $alias . $val : '');
+                }
+                $field[$key] = $val;
+            }
+        }
+
+        if (isset($this->options['field'])) {
+            $field = array_merge((array) $this->options['field'], $field);
+        }
+        $this->options['field'] = array_unique($field);
+        return $this;
+    }
+
+    /**
+     * 表达式方式指定查询字段
+     * @access public
+     * @param  string $field    字段名
+     * @param  array  $bind     参数绑定
+     * @return $this
+     */
+    public function fieldRaw($field, array $bind = [])
+    {
+        $this->options['field'][] = $this->raw($field);
+
+        if ($bind) {
+            $this->bind($bind);
+        }
+
+        return $this;
+    }
+
+    /**
+     * 设置数据
+     * @access public
+     * @param mixed $field 字段名或者数据
+     * @param mixed $value 字段值
+     * @return $this
+     */
+    public function data($field, $value = null)
+    {
+        if (is_array($field)) {
+            $this->options['data'] = isset($this->options['data']) ? array_merge($this->options['data'], $field) : $field;
+        } else {
+            $this->options['data'][$field] = $value;
+        }
+        return $this;
+    }
+
+    /**
+     * 字段值增长
+     * @access public
+     * @param string|array $field 字段名
+     * @param integer      $step  增长值
+     * @return $this
+     */
+    public function inc($field, $step = 1)
+    {
+        $fields = is_string($field) ? explode(',', $field) : $field;
+        foreach ($fields as $field) {
+            $this->data($field, ['inc', $step]);
+        }
+        return $this;
+    }
+
+    /**
+     * 字段值减少
+     * @access public
+     * @param string|array $field 字段名
+     * @param integer      $step  增长值
+     * @return $this
+     */
+    public function dec($field, $step = 1)
+    {
+        $fields = is_string($field) ? explode(',', $field) : $field;
+        foreach ($fields as $field) {
+            $this->data($field, ['dec', $step]);
+        }
+        return $this;
+    }
+
+    /**
+     * 使用表达式设置数据
+     * @access public
+     * @param string $field 字段名
+     * @param string $value 字段值
+     * @return $this
+     */
+    public function exp($field, $value)
+    {
+        $this->data($field, $this->raw($value));
+        return $this;
+    }
+
+    /**
+     * 使用表达式设置数据
+     * @access public
+     * @param  mixed $value 表达式
+     * @return Expression
+     */
+    public function raw($value)
+    {
+        return new Expression($value);
+    }
+
+    /**
+     * 指定JOIN查询字段
+     * @access public
+     * @param string|array $table 数据表
+     * @param string|array $field 查询字段
+     * @param mixed        $on    JOIN条件
+     * @param string       $type  JOIN类型
+     * @return $this
+     */
+    public function view($join, $field = true, $on = null, $type = 'INNER')
+    {
+        $this->options['view'] = true;
+        if (is_array($join) && key($join) === 0) {
+            foreach ($join as $key => $val) {
+                $this->view($val[0], $val[1], isset($val[2]) ? $val[2] : null, isset($val[3]) ? $val[3] : 'INNER');
+            }
+        } else {
+            $fields = [];
+            $table  = $this->getJoinTable($join, $alias);
+
+            if (true === $field) {
+                $fields = $alias . '.*';
+            } else {
+                if (is_string($field)) {
+                    $field = explode(',', $field);
+                }
+                foreach ($field as $key => $val) {
+                    if (is_numeric($key)) {
+                        $fields[]                   = $alias . '.' . $val;
+                        $this->options['map'][$val] = $alias . '.' . $val;
+                    } else {
+                        if (preg_match('/[,=\.\'\"\(\s]/', $key)) {
+                            $name = $key;
+                        } else {
+                            $name = $alias . '.' . $key;
+                        }
+                        $fields[$name]              = $val;
+                        $this->options['map'][$val] = $name;
+                    }
+                }
+            }
+            $this->field($fields);
+            if ($on) {
+                $this->join($table, $on, $type);
+            } else {
+                $this->table($table);
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * 设置分表规则
+     * @access public
+     * @param array  $data  操作的数据
+     * @param string $field 分表依据的字段
+     * @param array  $rule  分表规则
+     * @return $this
+     */
+    public function partition($data, $field, $rule = [])
+    {
+        $this->options['table'] = $this->getPartitionTableName($data, $field, $rule);
+        return $this;
+    }
+
+    /**
+     * 指定AND查询条件
+     * @access public
+     * @param mixed $field     查询字段
+     * @param mixed $op        查询表达式
+     * @param mixed $condition 查询条件
+     * @return $this
+     */
+    public function where($field, $op = null, $condition = null)
+    {
+        $param = func_get_args();
+        array_shift($param);
+        $this->parseWhereExp('AND', $field, $op, $condition, $param);
+        return $this;
+    }
+
+    /**
+     * 指定OR查询条件
+     * @access public
+     * @param mixed $field     查询字段
+     * @param mixed $op        查询表达式
+     * @param mixed $condition 查询条件
+     * @return $this
+     */
+    public function whereOr($field, $op = null, $condition = null)
+    {
+        $param = func_get_args();
+        array_shift($param);
+        $this->parseWhereExp('OR', $field, $op, $condition, $param);
+        return $this;
+    }
+
+    /**
+     * 指定XOR查询条件
+     * @access public
+     * @param mixed $field     查询字段
+     * @param mixed $op        查询表达式
+     * @param mixed $condition 查询条件
+     * @return $this
+     */
+    public function whereXor($field, $op = null, $condition = null)
+    {
+        $param = func_get_args();
+        array_shift($param);
+        $this->parseWhereExp('XOR', $field, $op, $condition, $param);
+        return $this;
+    }
+
+    /**
+     * 指定表达式查询条件
+     * @access public
+     * @param  string $where  查询条件
+     * @param  array  $bind   参数绑定
+     * @param  string $logic  查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereRaw($where, $bind = [], $logic = 'AND')
+    {
+        $this->options['where'][$logic][] = $this->raw($where);
+
+        if ($bind) {
+            $this->bind($bind);
+        }
+
+        return $this;
+    }
+
+    /**
+     * 指定表达式查询条件 OR
+     * @access public
+     * @param  string $where  查询条件
+     * @param  array  $bind   参数绑定
+     * @return $this
+     */
+    public function whereOrRaw($where, $bind = [])
+    {
+        return $this->whereRaw($where, $bind, 'OR');
+    }
+
+    /**
+     * 指定Null查询条件
+     * @access public
+     * @param mixed  $field 查询字段
+     * @param string $logic 查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereNull($field, $logic = 'AND')
+    {
+        $this->parseWhereExp($logic, $field, 'null', null, [], true);
+        return $this;
+    }
+
+    /**
+     * 指定NotNull查询条件
+     * @access public
+     * @param mixed  $field 查询字段
+     * @param string $logic 查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereNotNull($field, $logic = 'AND')
+    {
+        $this->parseWhereExp($logic, $field, 'notnull', null, [], true);
+        return $this;
+    }
+
+    /**
+     * 指定Exists查询条件
+     * @access public
+     * @param mixed  $condition 查询条件
+     * @param string $logic     查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereExists($condition, $logic = 'AND')
+    {
+        $this->options['where'][strtoupper($logic)][] = ['exists', $condition];
+        return $this;
+    }
+
+    /**
+     * 指定NotExists查询条件
+     * @access public
+     * @param mixed  $condition 查询条件
+     * @param string $logic     查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereNotExists($condition, $logic = 'AND')
+    {
+        $this->options['where'][strtoupper($logic)][] = ['not exists', $condition];
+        return $this;
+    }
+
+    /**
+     * 指定In查询条件
+     * @access public
+     * @param mixed  $field     查询字段
+     * @param mixed  $condition 查询条件
+     * @param string $logic     查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereIn($field, $condition, $logic = 'AND')
+    {
+        $this->parseWhereExp($logic, $field, 'in', $condition, [], true);
+        return $this;
+    }
+
+    /**
+     * 指定NotIn查询条件
+     * @access public
+     * @param mixed  $field     查询字段
+     * @param mixed  $condition 查询条件
+     * @param string $logic     查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereNotIn($field, $condition, $logic = 'AND')
+    {
+        $this->parseWhereExp($logic, $field, 'not in', $condition, [], true);
+        return $this;
+    }
+
+    /**
+     * 指定Like查询条件
+     * @access public
+     * @param mixed  $field     查询字段
+     * @param mixed  $condition 查询条件
+     * @param string $logic     查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereLike($field, $condition, $logic = 'AND')
+    {
+        $this->parseWhereExp($logic, $field, 'like', $condition, [], true);
+        return $this;
+    }
+
+    /**
+     * 指定NotLike查询条件
+     * @access public
+     * @param mixed  $field     查询字段
+     * @param mixed  $condition 查询条件
+     * @param string $logic     查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereNotLike($field, $condition, $logic = 'AND')
+    {
+        $this->parseWhereExp($logic, $field, 'not like', $condition, [], true);
+        return $this;
+    }
+
+    /**
+     * 指定Between查询条件
+     * @access public
+     * @param mixed  $field     查询字段
+     * @param mixed  $condition 查询条件
+     * @param string $logic     查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereBetween($field, $condition, $logic = 'AND')
+    {
+        $this->parseWhereExp($logic, $field, 'between', $condition, [], true);
+        return $this;
+    }
+
+    /**
+     * 指定NotBetween查询条件
+     * @access public
+     * @param mixed  $field     查询字段
+     * @param mixed  $condition 查询条件
+     * @param string $logic     查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereNotBetween($field, $condition, $logic = 'AND')
+    {
+        $this->parseWhereExp($logic, $field, 'not between', $condition, [], true);
+        return $this;
+    }
+
+    /**
+     * 指定Exp查询条件
+     * @access public
+     * @param mixed  $field     查询字段
+     * @param mixed  $condition 查询条件
+     * @param string $logic     查询逻辑 and or xor
+     * @return $this
+     */
+    public function whereExp($field, $condition, $logic = 'AND')
+    {
+        $this->parseWhereExp($logic, $field, 'exp', $this->raw($condition), [], true);
+        return $this;
+    }
+
+    /**
+     * 设置软删除字段及条件
+     * @access public
+     * @param false|string  $field     查询字段
+     * @param mixed         $condition 查询条件
+     * @return $this
+     */
+    public function useSoftDelete($field, $condition = null)
+    {
+        if ($field) {
+            $this->options['soft_delete'] = [$field, $condition ?: ['null', '']];
+        }
+        return $this;
+    }
+
+    /**
+     * 分析查询表达式
+     * @access public
+     * @param string                $logic     查询逻辑 and or xor
+     * @param string|array|\Closure $field     查询字段
+     * @param mixed                 $op        查询表达式
+     * @param mixed                 $condition 查询条件
+     * @param array                 $param     查询参数
+     * @param  bool                 $strict    严格模式
+     * @return void
+     */
+    protected function parseWhereExp($logic, $field, $op, $condition, $param = [], $strict = false)
+    {
+        $logic = strtoupper($logic);
+        if ($field instanceof \Closure) {
+            $this->options['where'][$logic][] = is_string($op) ? [$op, $field] : $field;
+            return;
+        }
+
+        if (is_string($field) && !empty($this->options['via']) && !strpos($field, '.')) {
+            $field = $this->options['via'] . '.' . $field;
+        }
+
+        if ($field instanceof Expression) {
+            return $this->whereRaw($field, is_array($op) ? $op : []);
+        } elseif ($strict) {
+            // 使用严格模式查询
+            $where[$field] = [$op, $condition];
+
+            // 记录一个字段多次查询条件
+            $this->options['multi'][$logic][$field][] = $where[$field];
+        } elseif (is_string($field) && preg_match('/[,=\>\<\'\"\(\s]/', $field)) {
+            $where[] = ['exp', $this->raw($field)];
+            if (is_array($op)) {
+                // 参数绑定
+                $this->bind($op);
+            }
+        } elseif (is_null($op) && is_null($condition)) {
+            if (is_array($field)) {
+                // 数组批量查询
+                $where = $field;
+                foreach ($where as $k => $val) {
+                    $this->options['multi'][$logic][$k][] = $val;
+                }
+            } elseif ($field && is_string($field)) {
+                // 字符串查询
+                $where[$field]                            = ['null', ''];
+                $this->options['multi'][$logic][$field][] = $where[$field];
+            }
+        } elseif (is_array($op)) {
+            $where[$field] = $param;
+        } elseif (in_array(strtolower($op), ['null', 'notnull', 'not null'])) {
+            // null查询
+            $where[$field] = [$op, ''];
+
+            $this->options['multi'][$logic][$field][] = $where[$field];
+        } elseif (is_null($condition)) {
+            // 字段相等查询
+            $where[$field] = ['eq', $op];
+
+            $this->options['multi'][$logic][$field][] = $where[$field];
+        } else {
+            if ('exp' == strtolower($op)) {
+                $where[$field] = ['exp', $this->raw($condition)];
+                // 参数绑定
+                if (isset($param[2]) && is_array($param[2])) {
+                    $this->bind($param[2]);
+                }
+            } else {
+                $where[$field] = [$op, $condition];
+            }
+            // 记录一个字段多次查询条件
+            $this->options['multi'][$logic][$field][] = $where[$field];
+        }
+
+        if (!empty($where)) {
+            if (!isset($this->options['where'][$logic])) {
+                $this->options['where'][$logic] = [];
+            }
+            if (is_string($field) && $this->checkMultiField($field, $logic)) {
+                $where[$field] = $this->options['multi'][$logic][$field];
+            } elseif (is_array($field)) {
+                foreach ($field as $key => $val) {
+                    if ($this->checkMultiField($key, $logic)) {
+                        $where[$key] = $this->options['multi'][$logic][$key];
+                    }
+                }
+            }
+            $this->options['where'][$logic] = array_merge($this->options['where'][$logic], $where);
+        }
+    }
+
+    /**
+     * 检查是否存在一个字段多次查询条件
+     * @access public
+     * @param string $field 查询字段
+     * @param string $logic 查询逻辑 and or xor
+     * @return bool
+     */
+    private function checkMultiField($field, $logic)
+    {
+        return isset($this->options['multi'][$logic][$field]) && count($this->options['multi'][$logic][$field]) > 1;
+    }
+
+    /**
+     * 去除某个查询条件
+     * @access public
+     * @param string $field 查询字段
+     * @param string $logic 查询逻辑 and or xor
+     * @return $this
+     */
+    public function removeWhereField($field, $logic = 'AND')
+    {
+        $logic = strtoupper($logic);
+        if (isset($this->options['where'][$logic][$field])) {
+            unset($this->options['where'][$logic][$field]);
+            unset($this->options['multi'][$logic][$field]);
+        }
+        return $this;
+    }
+
+    /**
+     * 去除查询参数
+     * @access public
+     * @param string|bool $option 参数名 true 表示去除所有参数
+     * @return $this
+     */
+    public function removeOption($option = true)
+    {
+        if (true === $option) {
+            $this->options = [];
+        } elseif (is_string($option) && isset($this->options[$option])) {
+            unset($this->options[$option]);
+        }
+        return $this;
+    }
+
+    /**
+     * 指定查询数量
+     * @access public
+     * @param mixed $offset 起始位置
+     * @param mixed $length 查询数量
+     * @return $this
+     */
+    public function limit($offset, $length = null)
+    {
+        if (is_null($length) && strpos($offset, ',')) {
+            list($offset, $length) = explode(',', $offset);
+        }
+        $this->options['limit'] = intval($offset) . ($length ? ',' . intval($length) : '');
+        return $this;
+    }
+
+    /**
+     * 指定分页
+     * @access public
+     * @param mixed $page     页数
+     * @param mixed $listRows 每页数量
+     * @return $this
+     */
+    public function page($page, $listRows = null)
+    {
+        if (is_null($listRows) && strpos($page, ',')) {
+            list($page, $listRows) = explode(',', $page);
+        }
+        $this->options['page'] = [intval($page), intval($listRows)];
+        return $this;
+    }
+
+    /**
+     * 分页查询
+     * @param int|array $listRows 每页数量 数组表示配置参数
+     * @param int|bool  $simple   是否简洁模式或者总记录数
+     * @param array     $config   配置参数
+     *                            page:当前页,
+     *                            path:url路径,
+     *                            query:url额外参数,
+     *                            fragment:url锚点,
+     *                            var_page:分页变量,
+     *                            list_rows:每页数量
+     *                            type:分页类名
+     * @return \think\Paginator
+     * @throws DbException
+     */
+    public function paginate($listRows = null, $simple = false, $config = [])
+    {
+        if (is_int($simple)) {
+            $total  = $simple;
+            $simple = false;
+        }
+        if (is_array($listRows)) {
+            $config   = array_merge(Config::get('paginate'), $listRows);
+            $listRows = $config['list_rows'];
+        } else {
+            $config   = array_merge(Config::get('paginate'), $config);
+            $listRows = $listRows ?: $config['list_rows'];
+        }
+
+        /** @var Paginator $class */
+        $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\paginator\\driver\\' . ucwords($config['type']);
+        $page  = isset($config['page']) ? (int) $config['page'] : call_user_func([
+            $class,
+            'getCurrentPage',
+        ], $config['var_page']);
+
+        $page = $page < 1 ? 1 : $page;
+
+        $config['path'] = isset($config['path']) ? $config['path'] : call_user_func([$class, 'getCurrentPath']);
+
+        if (!isset($total) && !$simple) {
+            $options = $this->getOptions();
+
+            unset($this->options['order'], $this->options['limit'], $this->options['page'], $this->options['field']);
+
+            $bind    = $this->bind;
+            $total   = $this->count();
+            $results = $this->options($options)->bind($bind)->page($page, $listRows)->select();
+        } elseif ($simple) {
+            $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select();
+            $total   = null;
+        } else {
+            $results = $this->page($page, $listRows)->select();
+        }
+        return $class::make($results, $listRows, $page, $total, $simple, $config);
+    }
+
+    /**
+     * 指定当前操作的数据表
+     * @access public
+     * @param mixed $table 表名
+     * @return $this
+     */
+    public function table($table)
+    {
+        if (is_string($table)) {
+            if (strpos($table, ')')) {
+                // 子查询
+            } elseif (strpos($table, ',')) {
+                $tables = explode(',', $table);
+                $table  = [];
+                foreach ($tables as $item) {
+                    list($item, $alias) = explode(' ', trim($item));
+                    if ($alias) {
+                        $this->alias([$item => $alias]);
+                        $table[$item] = $alias;
+                    } else {
+                        $table[] = $item;
+                    }
+                }
+            } elseif (strpos($table, ' ')) {
+                list($table, $alias) = explode(' ', $table);
+
+                $table = [$table => $alias];
+                $this->alias($table);
+            }
+        } else {
+            $tables = $table;
+            $table  = [];
+            foreach ($tables as $key => $val) {
+                if (is_numeric($key)) {
+                    $table[] = $val;
+                } else {
+                    $this->alias([$key => $val]);
+                    $table[$key] = $val;
+                }
+            }
+        }
+        $this->options['table'] = $table;
+        return $this;
+    }
+
+    /**
+     * USING支持 用于多表删除
+     * @access public
+     * @param mixed $using
+     * @return $this
+     */
+    public function using($using)
+    {
+        $this->options['using'] = $using;
+        return $this;
+    }
+
+    /**
+     * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc'])
+     * @access public
+     * @param string|array $field 排序字段
+     * @param string       $order 排序
+     * @return $this
+     */
+    public function order($field, $order = null)
+    {
+        if (empty($field)) {
+            return $this;
+        } elseif ($field instanceof Expression) {
+            $this->options['order'][] = $field;
+            return $this;
+        }
+
+        if (is_string($field)) {
+            if (!empty($this->options['via'])) {
+                $field = $this->options['via'] . '.' . $field;
+            }
+            if (strpos($field, ',')) {
+                $field = array_map('trim', explode(',', $field));
+            } else {
+                $field = empty($order) ? $field : [$field => $order];
+            }
+        } elseif (!empty($this->options['via'])) {
+            foreach ($field as $key => $val) {
+                if (is_numeric($key)) {
+                    $field[$key] = $this->options['via'] . '.' . $val;
+                } else {
+                    $field[$this->options['via'] . '.' . $key] = $val;
+                    unset($field[$key]);
+                }
+            }
+        }
+        if (!isset($this->options['order'])) {
+            $this->options['order'] = [];
+        }
+        if (is_array($field)) {
+            $this->options['order'] = array_merge($this->options['order'], $field);
+        } else {
+            $this->options['order'][] = $field;
+        }
+
+        return $this;
+    }
+
+    /**
+     * 表达式方式指定Field排序
+     * @access public
+     * @param  string $field 排序字段
+     * @param  array  $bind  参数绑定
+     * @return $this
+     */
+    public function orderRaw($field, array $bind = [])
+    {
+        $this->options['order'][] = $this->raw($field);
+
+        if ($bind) {
+            $this->bind($bind);
+        }
+
+        return $this;
+    }
+
+    /**
+     * 查询缓存
+     * @access public
+     * @param mixed             $key    缓存key
+     * @param integer|\DateTime $expire 缓存有效期
+     * @param string            $tag    缓存标签
+     * @return $this
+     */
+    public function cache($key = true, $expire = null, $tag = null)
+    {
+        // 增加快捷调用方式 cache(10) 等同于 cache(true, 10)
+        if ($key instanceof \DateTime || (is_numeric($key) && is_null($expire))) {
+            $expire = $key;
+            $key    = true;
+        }
+
+        if (false !== $key) {
+            $this->options['cache'] = ['key' => $key, 'expire' => $expire, 'tag' => $tag];
+        }
+        return $this;
+    }
+
+    /**
+     * 指定group查询
+     * @access public
+     * @param string $group GROUP
+     * @return $this
+     */
+    public function group($group)
+    {
+        $this->options['group'] = $group;
+        return $this;
+    }
+
+    /**
+     * 指定having查询
+     * @access public
+     * @param string $having having
+     * @return $this
+     */
+    public function having($having)
+    {
+        $this->options['having'] = $having;
+        return $this;
+    }
+
+    /**
+     * 指定查询lock
+     * @access public
+     * @param bool|string $lock 是否lock
+     * @return $this
+     */
+    public function lock($lock = false)
+    {
+        $this->options['lock']   = $lock;
+        $this->options['master'] = true;
+        return $this;
+    }
+
+    /**
+     * 指定distinct查询
+     * @access public
+     * @param string $distinct 是否唯一
+     * @return $this
+     */
+    public function distinct($distinct)
+    {
+        $this->options['distinct'] = $distinct;
+        return $this;
+    }
+
+    /**
+     * 指定数据表别名
+     * @access public
+     * @param mixed $alias 数据表别名
+     * @return $this
+     */
+    public function alias($alias)
+    {
+        if (is_array($alias)) {
+            foreach ($alias as $key => $val) {
+                if (false !== strpos($key, '__')) {
+                    $table = $this->parseSqlTable($key);
+                } else {
+                    $table = $key;
+                }
+                $this->options['alias'][$table] = $val;
+            }
+        } else {
+            if (isset($this->options['table'])) {
+                $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table'];
+                if (false !== strpos($table, '__')) {
+                    $table = $this->parseSqlTable($table);
+                }
+            } else {
+                $table = $this->getTable();
+            }
+
+            $this->options['alias'][$table] = $alias;
+        }
+        return $this;
+    }
+
+    /**
+     * 指定强制索引
+     * @access public
+     * @param string $force 索引名称
+     * @return $this
+     */
+    public function force($force)
+    {
+        $this->options['force'] = $force;
+        return $this;
+    }
+
+    /**
+     * 查询注释
+     * @access public
+     * @param string $comment 注释
+     * @return $this
+     */
+    public function comment($comment)
+    {
+        $this->options['comment'] = $comment;
+        return $this;
+    }
+
+    /**
+     * 获取执行的SQL语句
+     * @access public
+     * @param boolean $fetch 是否返回sql
+     * @return $this
+     */
+    public function fetchSql($fetch = true)
+    {
+        $this->options['fetch_sql'] = $fetch;
+        return $this;
+    }
+
+    /**
+     * 不主动获取数据集
+     * @access public
+     * @param bool $pdo 是否返回 PDOStatement 对象
+     * @return $this
+     */
+    public function fetchPdo($pdo = true)
+    {
+        $this->options['fetch_pdo'] = $pdo;
+        return $this;
+    }
+
+    /**
+     * 设置从主服务器读取数据
+     * @access public
+     * @return $this
+     */
+    public function master()
+    {
+        $this->options['master'] = true;
+        return $this;
+    }
+
+    /**
+     * 设置是否严格检查字段名
+     * @access public
+     * @param bool $strict 是否严格检查字段
+     * @return $this
+     */
+    public function strict($strict = true)
+    {
+        $this->options['strict'] = $strict;
+        return $this;
+    }
+
+    /**
+     * 设置查询数据不存在是否抛出异常
+     * @access public
+     * @param bool $fail 数据不存在是否抛出异常
+     * @return $this
+     */
+    public function failException($fail = true)
+    {
+        $this->options['fail'] = $fail;
+        return $this;
+    }
+
+    /**
+     * 设置自增序列名
+     * @access public
+     * @param string $sequence 自增序列名
+     * @return $this
+     */
+    public function sequence($sequence = null)
+    {
+        $this->options['sequence'] = $sequence;
+        return $this;
+    }
+
+    /**
+     * 指定数据表主键
+     * @access public
+     * @param string $pk 主键
+     * @return $this
+     */
+    public function pk($pk)
+    {
+        $this->pk = $pk;
+        return $this;
+    }
+
+    /**
+     * 查询日期或者时间
+     * @access public
+     * @param string       $field 日期字段名
+     * @param string|array $op    比较运算符或者表达式
+     * @param string|array $range 比较范围
+     * @return $this
+     */
+    public function whereTime($field, $op, $range = null)
+    {
+        if (is_null($range)) {
+            if (is_array($op)) {
+                $range = $op;
+            } else {
+                // 使用日期表达式
+                switch (strtolower($op)) {
+                    case 'today':
+                    case 'd':
+                        $range = ['today', 'tomorrow'];
+                        break;
+                    case 'week':
+                    case 'w':
+                        $range = ['this week 00:00:00', 'next week 00:00:00'];
+                        break;
+                    case 'month':
+                    case 'm':
+                        $range = ['first Day of this month 00:00:00', 'first Day of next month 00:00:00'];
+                        break;
+                    case 'year':
+                    case 'y':
+                        $range = ['this year 1/1', 'next year 1/1'];
+                        break;
+                    case 'yesterday':
+                        $range = ['yesterday', 'today'];
+                        break;
+                    case 'last week':
+                        $range = ['last week 00:00:00', 'this week 00:00:00'];
+                        break;
+                    case 'last month':
+                        $range = ['first Day of last month 00:00:00', 'first Day of this month 00:00:00'];
+                        break;
+                    case 'last year':
+                        $range = ['last year 1/1', 'this year 1/1'];
+                        break;
+                    default:
+                        $range = $op;
+                }
+            }
+            $op = is_array($range) ? 'between' : '>';
+        }
+        $this->where($field, strtolower($op) . ' time', $range);
+        return $this;
+    }
+
+    /**
+     * 获取数据表信息
+     * @access public
+     * @param mixed  $tableName 数据表名 留空自动获取
+     * @param string $fetch     获取信息类型 包括 fields type bind pk
+     * @return mixed
+     */
+    public function getTableInfo($tableName = '', $fetch = '')
+    {
+        if (!$tableName) {
+            $tableName = $this->getTable();
+        }
+        if (is_array($tableName)) {
+            $tableName = key($tableName) ?: current($tableName);
+        }
+
+        if (strpos($tableName, ',')) {
+            // 多表不获取字段信息
+            return false;
+        } else {
+            $tableName = $this->parseSqlTable($tableName);
+        }
+
+        // 修正子查询作为表名的问题
+        if (strpos($tableName, ')')) {
+            return [];
+        }
+
+        list($guid) = explode(' ', $tableName);
+        $db         = $this->getConfig('database');
+        if (!isset(self::$info[$db . '.' . $guid])) {
+            if (!strpos($guid, '.')) {
+                $schema = $db . '.' . $guid;
+            } else {
+                $schema = $guid;
+            }
+            // 读取缓存
+            if (!App::$debug && is_file(RUNTIME_PATH . 'schema/' . $schema . '.php')) {
+                $info = include RUNTIME_PATH . 'schema/' . $schema . '.php';
+            } else {
+                $info = $this->connection->getFields($guid);
+            }
+            $fields = array_keys($info);
+            $bind   = $type   = [];
+            foreach ($info as $key => $val) {
+                // 记录字段类型
+                $type[$key] = $val['type'];
+                $bind[$key] = $this->getFieldBindType($val['type']);
+                if (!empty($val['primary'])) {
+                    $pk[] = $key;
+                }
+            }
+            if (isset($pk)) {
+                // 设置主键
+                $pk = count($pk) > 1 ? $pk : $pk[0];
+            } else {
+                $pk = null;
+            }
+            self::$info[$db . '.' . $guid] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk];
+        }
+        return $fetch ? self::$info[$db . '.' . $guid][$fetch] : self::$info[$db . '.' . $guid];
+    }
+
+    /**
+     * 获取当前数据表的主键
+     * @access public
+     * @param string|array $options 数据表名或者查询参数
+     * @return string|array
+     */
+    public function getPk($options = '')
+    {
+        if (!empty($this->pk)) {
+            $pk = $this->pk;
+        } else {
+            $pk = $this->getTableInfo(is_array($options) ? $options['table'] : $options, 'pk');
+        }
+        return $pk;
+    }
+
+    // 获取当前数据表字段信息
+    public function getTableFields($table = '')
+    {
+        return $this->getTableInfo($table ?: $this->getOptions('table'), 'fields');
+    }
+
+    // 获取当前数据表字段类型
+    public function getFieldsType($table = '')
+    {
+        return $this->getTableInfo($table ?: $this->getOptions('table'), 'type');
+    }
+
+    // 获取当前数据表绑定信息
+    public function getFieldsBind($table = '')
+    {
+        $types = $this->getFieldsType($table);
+        $bind  = [];
+        if ($types) {
+            foreach ($types as $key => $type) {
+                $bind[$key] = $this->getFieldBindType($type);
+            }
+        }
+        return $bind;
+    }
+
+    /**
+     * 获取字段绑定类型
+     * @access public
+     * @param string $type 字段类型
+     * @return integer
+     */
+    protected function getFieldBindType($type)
+    {
+        if (0 === strpos($type, 'set') || 0 === strpos($type, 'enum')) {
+            $bind = PDO::PARAM_STR;
+        } elseif (preg_match('/(int|double|float|decimal|real|numeric|serial|bit)/is', $type)) {
+            $bind = PDO::PARAM_INT;
+        } elseif (preg_match('/bool/is', $type)) {
+            $bind = PDO::PARAM_BOOL;
+        } else {
+            $bind = PDO::PARAM_STR;
+        }
+        return $bind;
+    }
+
+    /**
+     * 参数绑定
+     * @access public
+     * @param mixed   $key   参数名
+     * @param mixed   $value 绑定变量值
+     * @param integer $type  绑定类型
+     * @return $this
+     */
+    public function bind($key, $value = false, $type = PDO::PARAM_STR)
+    {
+        if (is_array($key)) {
+            $this->bind = array_merge($this->bind, $key);
+        } else {
+            $this->bind[$key] = [$value, $type];
+        }
+        return $this;
+    }
+
+    /**
+     * 检测参数是否已经绑定
+     * @access public
+     * @param string $key 参数名
+     * @return bool
+     */
+    public function isBind($key)
+    {
+        return isset($this->bind[$key]);
+    }
+
+    /**
+     * 查询参数赋值
+     * @access protected
+     * @param array $options 表达式参数
+     * @return $this
+     */
+    protected function options(array $options)
+    {
+        $this->options = $options;
+        return $this;
+    }
+
+    /**
+     * 获取当前的查询参数
+     * @access public
+     * @param string $name 参数名
+     * @return mixed
+     */
+    public function getOptions($name = '')
+    {
+        if ('' === $name) {
+            return $this->options;
+        } else {
+            return isset($this->options[$name]) ? $this->options[$name] : null;
+        }
+    }
+
+    /**
+     * 设置关联查询JOIN预查询
+     * @access public
+     * @param string|array $with 关联方法名称
+     * @return $this
+     */
+    public function with($with)
+    {
+        if (empty($with)) {
+            return $this;
+        }
+
+        if (is_string($with)) {
+            $with = explode(',', $with);
+        }
+
+        $first = true;
+
+        /** @var Model $class */
+        $class = $this->model;
+        foreach ($with as $key => $relation) {
+            $subRelation = '';
+            $closure     = false;
+            if ($relation instanceof \Closure) {
+                // 支持闭包查询过滤关联条件
+                $closure    = $relation;
+                $relation   = $key;
+                $with[$key] = $key;
+            } elseif (is_array($relation)) {
+                $subRelation = $relation;
+                $relation    = $key;
+            } elseif (is_string($relation) && strpos($relation, '.')) {
+                $with[$key]                   = $relation;
+                list($relation, $subRelation) = explode('.', $relation, 2);
+            }
+
+            /** @var Relation $model */
+            $relation = Loader::parseName($relation, 1, false);
+            $model    = $class->$relation();
+            if ($model instanceof OneToOne && 0 == $model->getEagerlyType()) {
+                $model->eagerly($this, $relation, $subRelation, $closure, $first);
+                $first = false;
+            } elseif ($closure) {
+                $with[$key] = $closure;
+            }
+        }
+        $this->via();
+        if (isset($this->options['with'])) {
+            $this->options['with'] = array_merge($this->options['with'], $with);
+        } else {
+            $this->options['with'] = $with;
+        }
+        return $this;
+    }
+
+    /**
+     * 关联统计
+     * @access public
+     * @param string|array $relation 关联方法名
+     * @param bool         $subQuery 是否使用子查询
+     * @return $this
+     */
+    public function withCount($relation, $subQuery = true)
+    {
+        if (!$subQuery) {
+            $this->options['with_count'] = $relation;
+        } else {
+            $relations = is_string($relation) ? explode(',', $relation) : $relation;
+            if (!isset($this->options['field'])) {
+                $this->field('*');
+            }
+            foreach ($relations as $key => $relation) {
+                $closure = false;
+                if ($relation instanceof \Closure) {
+                    $closure  = $relation;
+                    $relation = $key;
+                }
+                $relation = Loader::parseName($relation, 1, false);
+                $count    = '(' . $this->model->$relation()->getRelationCountQuery($closure) . ')';
+                $this->field([$count => Loader::parseName($relation) . '_count']);
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * 关联预加载中 获取关联指定字段值
+     * example:
+     * Model::with(['relation' => function($query){
+     *     $query->withField("id,name");
+     * }])
+     *
+     * @param string | array $field 指定获取的字段
+     * @return $this
+     */
+    public function withField($field)
+    {
+        $this->options['with_field'] = $field;
+        return $this;
+    }
+
+    /**
+     * 设置当前字段添加的表别名
+     * @access public
+     * @param string $via
+     * @return $this
+     */
+    public function via($via = '')
+    {
+        $this->options['via'] = $via;
+        return $this;
+    }
+
+    /**
+     * 设置关联查询
+     * @access public
+     * @param string|array $relation 关联名称
+     * @return $this
+     */
+    public function relation($relation)
+    {
+        if (empty($relation)) {
+            return $this;
+        }
+        if (is_string($relation)) {
+            $relation = explode(',', $relation);
+        }
+        if (isset($this->options['relation'])) {
+            $this->options['relation'] = array_merge($this->options['relation'], $relation);
+        } else {
+            $this->options['relation'] = $relation;
+        }
+        return $this;
+    }
+
+    /**
+     * 把主键值转换为查询条件 支持复合主键
+     * @access public
+     * @param array|string $data    主键数据
+     * @param mixed        $options 表达式参数
+     * @return void
+     * @throws Exception
+     */
+    protected function parsePkWhere($data, &$options)
+    {
+        $pk = $this->getPk($options);
+        // 获取当前数据表
+        $table = is_array($options['table']) ? key($options['table']) : $options['table'];
+        if (!empty($options['alias'][$table])) {
+            $alias = $options['alias'][$table];
+        }
+        if (is_string($pk)) {
+            $key = isset($alias) ? $alias . '.' . $pk : $pk;
+            // 根据主键查询
+            if (is_array($data)) {
+                $where[$key] = isset($data[$pk]) ? $data[$pk] : ['in', $data];
+            } else {
+                $where[$key] = strpos($data, ',') ? ['IN', $data] : $data;
+            }
+        } elseif (is_array($pk) && is_array($data) && !empty($data)) {
+            // 根据复合主键查询
+            foreach ($pk as $key) {
+                if (isset($data[$key])) {
+                    $attr         = isset($alias) ? $alias . '.' . $key : $key;
+                    $where[$attr] = $data[$key];
+                } else {
+                    throw new Exception('miss complex primary data');
+                }
+            }
+        }
+
+        if (!empty($where)) {
+            if (isset($options['where']['AND'])) {
+                $options['where']['AND'] = array_merge($options['where']['AND'], $where);
+            } else {
+                $options['where']['AND'] = $where;
+            }
+        }
+        return;
+    }
+
+    /**
+     * 插入记录
+     * @access public
+     * @param mixed   $data         数据
+     * @param boolean $replace      是否replace
+     * @param boolean $getLastInsID 返回自增主键
+     * @param string  $sequence     自增序列名
+     * @return integer|string
+     */
+    public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null)
+    {
+        // 分析查询表达式
+        $options = $this->parseExpress();
+        $data    = array_merge($options['data'], $data);
+        // 生成SQL语句
+        $sql = $this->builder->insert($data, $options, $replace);
+        // 获取参数绑定
+        $bind = $this->getBind();
+        if ($options['fetch_sql']) {
+            // 获取实际执行的SQL语句
+            return $this->connection->getRealSql($sql, $bind);
+        }
+
+        // 执行操作
+        $result = 0 === $sql ? 0 : $this->execute($sql, $bind, $this);
+        if ($result) {
+            $sequence  = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null);
+            $lastInsId = $this->getLastInsID($sequence);
+            if ($lastInsId) {
+                $pk = $this->getPk($options);
+                if (is_string($pk)) {
+                    $data[$pk] = $lastInsId;
+                }
+            }
+            $options['data'] = $data;
+            $this->trigger('after_insert', $options);
+
+            if ($getLastInsID) {
+                return $lastInsId;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * 插入记录并获取自增ID
+     * @access public
+     * @param mixed   $data     数据
+     * @param boolean $replace  是否replace
+     * @param string  $sequence 自增序列名
+     * @return integer|string
+     */
+    public function insertGetId(array $data, $replace = false, $sequence = null)
+    {
+        return $this->insert($data, $replace, true, $sequence);
+    }
+
+    /**
+     * 批量插入记录
+     * @access public
+     * @param mixed     $dataSet 数据集
+     * @param boolean   $replace  是否replace
+     * @param integer   $limit   每次写入数据限制
+     * @return integer|string
+     */
+    public function insertAll(array $dataSet, $replace = false, $limit = null)
+    {
+        // 分析查询表达式
+        $options = $this->parseExpress();
+        if (!is_array(reset($dataSet))) {
+            return false;
+        }
+
+        // 生成SQL语句
+        if (is_null($limit)) {
+            $sql = $this->builder->insertAll($dataSet, $options, $replace);
+        } else {
+            $array = array_chunk($dataSet, $limit, true);
+            foreach ($array as $item) {
+                $sql[] = $this->builder->insertAll($item, $options, $replace);
+            }
+        }
+
+        // 获取参数绑定
+        $bind = $this->getBind();
+        if ($options['fetch_sql']) {
+            // 获取实际执行的SQL语句
+            return $this->connection->getRealSql($sql, $bind);
+        } elseif (is_array($sql)) {
+            // 执行操作
+            return $this->batchQuery($sql, $bind, $this);
+        } else {
+            // 执行操作
+            return $this->execute($sql, $bind, $this);
+        }
+    }
+
+    /**
+     * 通过Select方式插入记录
+     * @access public
+     * @param string $fields 要插入的数据表字段名
+     * @param string $table  要插入的数据表名
+     * @return integer|string
+     * @throws PDOException
+     */
+    public function selectInsert($fields, $table)
+    {
+        // 分析查询表达式
+        $options = $this->parseExpress();
+        // 生成SQL语句
+        $table = $this->parseSqlTable($table);
+        $sql   = $this->builder->selectInsert($fields, $table, $options);
+        // 获取参数绑定
+        $bind = $this->getBind();
+        if ($options['fetch_sql']) {
+            // 获取实际执行的SQL语句
+            return $this->connection->getRealSql($sql, $bind);
+        } else {
+            // 执行操作
+            return $this->execute($sql, $bind, $this);
+        }
+    }
+
+    /**
+     * 更新记录
+     * @access public
+     * @param mixed $data 数据
+     * @return integer|string
+     * @throws Exception
+     * @throws PDOException
+     */
+    public function update(array $data = [])
+    {
+        $options = $this->parseExpress();
+        $data    = array_merge($options['data'], $data);
+        $pk      = $this->getPk($options);
+        if (isset($options['cache']) && is_string($options['cache']['key'])) {
+            $key = $options['cache']['key'];
+        }
+
+        if (empty($options['where'])) {
+            // 如果存在主键数据 则自动作为更新条件
+            if (is_string($pk) && isset($data[$pk])) {
+                $where[$pk] = $data[$pk];
+                if (!isset($key)) {
+                    $key = 'think:' . $options['table'] . '|' . $data[$pk];
+                }
+                unset($data[$pk]);
+            } elseif (is_array($pk)) {
+                // 增加复合主键支持
+                foreach ($pk as $field) {
+                    if (isset($data[$field])) {
+                        $where[$field] = $data[$field];
+                    } else {
+                        // 如果缺少复合主键数据则不执行
+                        throw new Exception('miss complex primary data');
+                    }
+                    unset($data[$field]);
+                }
+            }
+            if (!isset($where)) {
+                // 如果没有任何更新条件则不执行
+                throw new Exception('miss update condition');
+            } else {
+                $options['where']['AND'] = $where;
+            }
+        } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) {
+            $key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind);
+        }
+
+        // 生成UPDATE SQL语句
+        $sql = $this->builder->update($data, $options);
+        // 获取参数绑定
+        $bind = $this->getBind();
+        if ($options['fetch_sql']) {
+            // 获取实际执行的SQL语句
+            return $this->connection->getRealSql($sql, $bind);
+        } else {
+            // 检测缓存
+            if (isset($key) && Cache::get($key)) {
+                // 删除缓存
+                Cache::rm($key);
+            } elseif (!empty($options['cache']['tag'])) {
+                Cache::clear($options['cache']['tag']);
+            }
+            // 执行操作
+            $result = '' == $sql ? 0 : $this->execute($sql, $bind, $this);
+            if ($result) {
+                if (is_string($pk) && isset($where[$pk])) {
+                    $data[$pk] = $where[$pk];
+                } elseif (is_string($pk) && isset($key) && strpos($key, '|')) {
+                    list($a, $val) = explode('|', $key);
+                    $data[$pk]     = $val;
+                }
+                $options['data'] = $data;
+                $this->trigger('after_update', $options);
+            }
+            return $result;
+        }
+    }
+
+    /**
+     * 执行查询但只返回PDOStatement对象
+     * @access public
+     * @return \PDOStatement|string
+     */
+    public function getPdo()
+    {
+        // 分析查询表达式
+        $options = $this->parseExpress();
+        // 生成查询SQL
+        $sql = $this->builder->select($options);
+        // 获取参数绑定
+        $bind = $this->getBind();
+        if ($options['fetch_sql']) {
+            // 获取实际执行的SQL语句
+            return $this->connection->getRealSql($sql, $bind);
+        }
+        // 执行查询操作
+        return $this->query($sql, $bind, $options['master'], true);
+    }
+
+    /**
+     * 查找记录
+     * @access public
+     * @param array|string|Query|\Closure $data
+     * @return Collection|false|\PDOStatement|string
+     * @throws DbException
+     * @throws ModelNotFoundException
+     * @throws DataNotFoundException
+     */
+    public function select($data = null)
+    {
+        if ($data instanceof Query) {
+            return $data->select();
+        } elseif ($data instanceof \Closure) {
+            call_user_func_array($data, [ & $this]);
+            $data = null;
+        }
+        // 分析查询表达式
+        $options = $this->parseExpress();
+
+        if (false === $data) {
+            // 用于子查询 不查询只返回SQL
+            $options['fetch_sql'] = true;
+        } elseif (!is_null($data)) {
+            // 主键条件分析
+            $this->parsePkWhere($data, $options);
+        }
+
+        $resultSet = false;
+        if (empty($options['fetch_sql']) && !empty($options['cache'])) {
+            // 判断查询缓存
+            $cache = $options['cache'];
+            unset($options['cache']);
+            $key       = is_string($cache['key']) ? $cache['key'] : md5($this->connection->getConfig('database') . '.' . serialize($options) . serialize($this->bind));
+            $resultSet = Cache::get($key);
+        }
+        if (false === $resultSet) {
+            // 生成查询SQL
+            $sql = $this->builder->select($options);
+            // 获取参数绑定
+            $bind = $this->getBind();
+            if ($options['fetch_sql']) {
+                // 获取实际执行的SQL语句
+                return $this->connection->getRealSql($sql, $bind);
+            }
+
+            $options['data'] = $data;
+            if ($resultSet = $this->trigger('before_select', $options)) {
+            } else {
+                // 执行查询操作
+                $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']);
+
+                if ($resultSet instanceof \PDOStatement) {
+                    // 返回PDOStatement对象
+                    return $resultSet;
+                }
+            }
+
+            if (isset($cache) && false !== $resultSet) {
+                // 缓存数据集
+                $this->cacheData($key, $resultSet, $cache);
+            }
+        }
+
+        // 数据列表读取后的处理
+        if (!empty($this->model)) {
+            // 生成模型对象
+            if (count($resultSet) > 0) {
+                foreach ($resultSet as $key => $result) {
+                    /** @var Model $model */
+                    $model = $this->model->newInstance($result);
+                    $model->isUpdate(true);
+
+                    // 关联查询
+                    if (!empty($options['relation'])) {
+                        $model->relationQuery($options['relation']);
+                    }
+                    // 关联统计
+                    if (!empty($options['with_count'])) {
+                        $model->relationCount($model, $options['with_count']);
+                    }
+                    $resultSet[$key] = $model;
+                }
+                if (!empty($options['with'])) {
+                    // 预载入
+                    $model->eagerlyResultSet($resultSet, $options['with']);
+                }
+                // 模型数据集转换
+                $resultSet = $model->toCollection($resultSet);
+            } else {
+                $resultSet = $this->model->toCollection($resultSet);
+            }
+        } elseif ('collection' == $this->connection->getConfig('resultset_type')) {
+            // 返回Collection对象
+            $resultSet = new Collection($resultSet);
+        }
+        // 返回结果处理
+        if (!empty($options['fail']) && count($resultSet) == 0) {
+            $this->throwNotFound($options);
+        }
+        return $resultSet;
+    }
+
+    /**
+     * 缓存数据
+     * @access public
+     * @param string    $key    缓存标识
+     * @param mixed     $data   缓存数据
+     * @param array     $config 缓存参数
+     */
+    protected function cacheData($key, $data, $config = [])
+    {
+        if (isset($config['tag'])) {
+            Cache::tag($config['tag'])->set($key, $data, $config['expire']);
+        } else {
+            Cache::set($key, $data, $config['expire']);
+        }
+    }
+
+    /**
+     * 生成缓存标识
+     * @access public
+     * @param mixed     $value   缓存数据
+     * @param array     $options 缓存参数
+     * @param array     $bind    绑定参数
+     * @return string
+     */
+    protected function getCacheKey($value, $options, $bind = [])
+    {
+        if (is_scalar($value)) {
+            $data = $value;
+        } elseif (is_array($value) && is_string($value[0]) && 'eq' == strtolower($value[0])) {
+            $data = $value[1];
+        }
+        $prefix = $this->connection->getConfig('database') . '.';
+
+        if (isset($data)) {
+            return 'think:' . $prefix . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data;
+        }
+
+        try {
+            return md5($prefix . serialize($options) . serialize($bind));
+        } catch (\Exception $e) {
+            throw new Exception('closure not support cache(true)');
+        }
+    }
+
+    /**
+     * 查找单条记录
+     * @access public
+     * @param array|string|Query|\Closure $data
+     * @return array|false|\PDOStatement|string|Model
+     * @throws DbException
+     * @throws ModelNotFoundException
+     * @throws DataNotFoundException
+     */
+    public function find($data = null)
+    {
+        if ($data instanceof Query) {
+            return $data->find();
+        } elseif ($data instanceof \Closure) {
+            call_user_func_array($data, [ & $this]);
+            $data = null;
+        }
+        // 分析查询表达式
+        $options = $this->parseExpress();
+        $pk      = $this->getPk($options);
+        if (!is_null($data)) {
+            // AR模式分析主键条件
+            $this->parsePkWhere($data, $options);
+        } elseif (!empty($options['cache']) && true === $options['cache']['key'] && is_string($pk) && isset($options['where']['AND'][$pk])) {
+            $key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind);
+        }
+
+        $options['limit'] = 1;
+        $result           = false;
+        if (empty($options['fetch_sql']) && !empty($options['cache'])) {
+            // 判断查询缓存
+            $cache = $options['cache'];
+            if (true === $cache['key'] && !is_null($data) && !is_array($data)) {
+                $key = 'think:' . $this->connection->getConfig('database') . '.' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data;
+            } elseif (is_string($cache['key'])) {
+                $key = $cache['key'];
+            } elseif (!isset($key)) {
+                $key = md5($this->connection->getConfig('database') . '.' . serialize($options) . serialize($this->bind));
+            }
+            $result = Cache::get($key);
+        }
+        if (false === $result) {
+            // 生成查询SQL
+            $sql = $this->builder->select($options);
+            // 获取参数绑定
+            $bind = $this->getBind();
+            if ($options['fetch_sql']) {
+                // 获取实际执行的SQL语句
+                return $this->connection->getRealSql($sql, $bind);
+            }
+            if (is_string($pk)) {
+                if (!is_array($data)) {
+                    if (isset($key) && strpos($key, '|')) {
+                        list($a, $val) = explode('|', $key);
+                        $item[$pk]     = $val;
+                    } else {
+                        $item[$pk] = $data;
+                    }
+                    $data = $item;
+                }
+            }
+            $options['data'] = $data;
+            // 事件回调
+            if ($result = $this->trigger('before_find', $options)) {
+            } else {
+                // 执行查询
+                $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']);
+
+                if ($resultSet instanceof \PDOStatement) {
+                    // 返回PDOStatement对象
+                    return $resultSet;
+                }
+                $result = isset($resultSet[0]) ? $resultSet[0] : null;
+            }
+
+            if (isset($cache) && $result) {
+                // 缓存数据
+                $this->cacheData($key, $result, $cache);
+            }
+        }
+
+        // 数据处理
+        if (!empty($result)) {
+            if (!empty($this->model)) {
+                // 返回模型对象
+                $result = $this->model->newInstance($result);
+                $result->isUpdate(true, isset($options['where']['AND']) ? $options['where']['AND'] : null);
+                // 关联查询
+                if (!empty($options['relation'])) {
+                    $result->relationQuery($options['relation']);
+                }
+                // 预载入查询
+                if (!empty($options['with'])) {
+                    $result->eagerlyResult($result, $options['with']);
+                }
+                // 关联统计
+                if (!empty($options['with_count'])) {
+                    $result->relationCount($result, $options['with_count']);
+                }
+            }
+        } elseif (!empty($options['fail'])) {
+            $this->throwNotFound($options);
+        }
+        return $result;
+    }
+
+    /**
+     * 查询失败 抛出异常
+     * @access public
+     * @param array $options 查询参数
+     * @throws ModelNotFoundException
+     * @throws DataNotFoundException
+     */
+    protected function throwNotFound($options = [])
+    {
+        if (!empty($this->model)) {
+            $class = get_class($this->model);
+            throw new ModelNotFoundException('model data Not Found:' . $class, $class, $options);
+        } else {
+            $table = is_array($options['table']) ? key($options['table']) : $options['table'];
+            throw new DataNotFoundException('table data not Found:' . $table, $table, $options);
+        }
+    }
+
+    /**
+     * 查找多条记录 如果不存在则抛出异常
+     * @access public
+     * @param array|string|Query|\Closure $data
+     * @return array|\PDOStatement|string|Model
+     * @throws DbException
+     * @throws ModelNotFoundException
+     * @throws DataNotFoundException
+     */
+    public function selectOrFail($data = null)
+    {
+        return $this->failException(true)->select($data);
+    }
+
+    /**
+     * 查找单条记录 如果不存在则抛出异常
+     * @access public
+     * @param array|string|Query|\Closure $data
+     * @return array|\PDOStatement|string|Model
+     * @throws DbException
+     * @throws ModelNotFoundException
+     * @throws DataNotFoundException
+     */
+    public function findOrFail($data = null)
+    {
+        return $this->failException(true)->find($data);
+    }
+
+    /**
+     * 分批数据返回处理
+     * @access public
+     * @param integer  $count    每次处理的数据数量
+     * @param callable $callback 处理回调方法
+     * @param string   $column   分批处理的字段名
+     * @param string   $order    排序规则
+     * @return boolean
+     * @throws \LogicException
+     */
+    public function chunk($count, $callback, $column = null, $order = 'asc')
+    {
+        $options = $this->getOptions();
+        if (empty($options['table'])) {
+            $options['table'] = $this->getTable();
+        }
+        $column = $column ?: $this->getPk($options);
+
+        if (isset($options['order'])) {
+            if (App::$debug) {
+                throw new \LogicException('chunk not support call order');
+            }
+            unset($options['order']);
+        }
+        $bind = $this->bind;
+        if (is_array($column)) {
+            $times = 1;
+            $query = $this->options($options)->page($times, $count);
+        } else {
+            if (strpos($column, '.')) {
+                list($alias, $key) = explode('.', $column);
+            } else {
+                $key = $column;
+            }
+            $query = $this->options($options)->limit($count);
+        }
+        $resultSet = $query->order($column, $order)->select();
+
+        while (count($resultSet) > 0) {
+            if ($resultSet instanceof Collection) {
+                $resultSet = $resultSet->all();
+            }
+
+            if (false === call_user_func($callback, $resultSet)) {
+                return false;
+            }
+
+            if (is_array($column)) {
+                $times++;
+                $query = $this->options($options)->page($times, $count);
+            } else {
+                $end    = end($resultSet);
+                $lastId = is_array($end) ? $end[$key] : $end->getData($key);
+                $query  = $this->options($options)
+                    ->limit($count)
+                    ->where($column, 'asc' == strtolower($order) ? '>' : '<', $lastId);
+            }
+
+            $resultSet = $query->bind($bind)->order($column, $order)->select();
+        }
+
+        return true;
+    }
+
+    /**
+     * 获取绑定的参数 并清空
+     * @access public
+     * @return array
+     */
+    public function getBind()
+    {
+        $bind       = $this->bind;
+        $this->bind = [];
+        return $bind;
+    }
+
+    /**
+     * 创建子查询SQL
+     * @access public
+     * @param bool $sub
+     * @return string
+     * @throws DbException
+     */
+    public function buildSql($sub = true)
+    {
+        return $sub ? '( ' . $this->select(false) . ' )' : $this->select(false);
+    }
+
+    /**
+     * 删除记录
+     * @access public
+     * @param mixed $data 表达式 true 表示强制删除
+     * @return int
+     * @throws Exception
+     * @throws PDOException
+     */
+    public function delete($data = null)
+    {
+        // 分析查询表达式
+        $options = $this->parseExpress();
+        $pk      = $this->getPk($options);
+        if (isset($options['cache']) && is_string($options['cache']['key'])) {
+            $key = $options['cache']['key'];
+        }
+
+        if (!is_null($data) && true !== $data) {
+            if (!isset($key) && !is_array($data)) {
+                // 缓存标识
+                $key = 'think:' . $options['table'] . '|' . $data;
+            }
+            // AR模式分析主键条件
+            $this->parsePkWhere($data, $options);
+        } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) {
+            $key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind);
+        }
+
+        if (true !== $data && empty($options['where'])) {
+            // 如果条件为空 不进行删除操作 除非设置 1=1
+            throw new Exception('delete without condition');
+        }
+        // 生成删除SQL语句
+        $sql = $this->builder->delete($options);
+        // 获取参数绑定
+        $bind = $this->getBind();
+        if ($options['fetch_sql']) {
+            // 获取实际执行的SQL语句
+            return $this->connection->getRealSql($sql, $bind);
+        }
+
+        // 检测缓存
+        if (isset($key) && Cache::get($key)) {
+            // 删除缓存
+            Cache::rm($key);
+        } elseif (!empty($options['cache']['tag'])) {
+            Cache::clear($options['cache']['tag']);
+        }
+        // 执行操作
+        $result = $this->execute($sql, $bind, $this);
+        if ($result) {
+            if (!is_array($data) && is_string($pk) && isset($key) && strpos($key, '|')) {
+                list($a, $val) = explode('|', $key);
+                $item[$pk]     = $val;
+                $data          = $item;
+            }
+            $options['data'] = $data;
+            $this->trigger('after_delete', $options);
+        }
+        return $result;
+    }
+
+    /**
+     * 分析表达式(可用于查询或者写入操作)
+     * @access protected
+     * @return array
+     */
+    protected function parseExpress()
+    {
+        $options = $this->options;
+
+        // 获取数据表
+        if (empty($options['table'])) {
+            $options['table'] = $this->getTable();
+        }
+
+        if (!isset($options['where'])) {
+            $options['where'] = [];
+        } elseif (isset($options['view'])) {
+            // 视图查询条件处理
+            foreach (['AND', 'OR'] as $logic) {
+                if (isset($options['where'][$logic])) {
+                    foreach ($options['where'][$logic] as $key => $val) {
+                        if (array_key_exists($key, $options['map'])) {
+                            $options['where'][$logic][$options['map'][$key]] = $val;
+                            unset($options['where'][$logic][$key]);
+                        }
+                    }
+                }
+            }
+
+            if (isset($options['order'])) {
+                // 视图查询排序处理
+                if (is_string($options['order'])) {
+                    $options['order'] = explode(',', $options['order']);
+                }
+                foreach ($options['order'] as $key => $val) {
+                    if (is_numeric($key)) {
+                        if (strpos($val, ' ')) {
+                            list($field, $sort) = explode(' ', $val);
+                            if (array_key_exists($field, $options['map'])) {
+                                $options['order'][$options['map'][$field]] = $sort;
+                                unset($options['order'][$key]);
+                            }
+                        } elseif (array_key_exists($val, $options['map'])) {
+                            $options['order'][$options['map'][$val]] = 'asc';
+                            unset($options['order'][$key]);
+                        }
+                    } elseif (array_key_exists($key, $options['map'])) {
+                        $options['order'][$options['map'][$key]] = $val;
+                        unset($options['order'][$key]);
+                    }
+                }
+            }
+        }
+
+        if (!isset($options['field'])) {
+            $options['field'] = '*';
+        }
+
+        if (!isset($options['data'])) {
+            $options['data'] = [];
+        }
+
+        if (!isset($options['strict'])) {
+            $options['strict'] = $this->getConfig('fields_strict');
+        }
+
+        foreach (['master', 'lock', 'fetch_pdo', 'fetch_sql', 'distinct'] as $name) {
+            if (!isset($options[$name])) {
+                $options[$name] = false;
+            }
+        }
+
+        if (isset(static::$readMaster['*']) || (is_string($options['table']) && isset(static::$readMaster[$options['table']]))) {
+            $options['master'] = true;
+        }
+
+        foreach (['join', 'union', 'group', 'having', 'limit', 'order', 'force', 'comment'] as $name) {
+            if (!isset($options[$name])) {
+                $options[$name] = '';
+            }
+        }
+
+        if (isset($options['page'])) {
+            // 根据页数计算limit
+            list($page, $listRows) = $options['page'];
+            $page                  = $page > 0 ? $page : 1;
+            $listRows              = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20);
+            $offset                = $listRows * ($page - 1);
+            $options['limit']      = $offset . ',' . $listRows;
+        }
+
+        $this->options = [];
+        return $options;
+    }
+
+    /**
+     * 注册回调方法
+     * @access public
+     * @param string   $event    事件名
+     * @param callable $callback 回调方法
+     * @return void
+     */
+    public static function event($event, $callback)
+    {
+        self::$event[$event] = $callback;
+    }
+
+    /**
+     * 触发事件
+     * @access protected
+     * @param string $event   事件名
+     * @param mixed  $params  额外参数
+     * @return bool
+     */
+    protected function trigger($event, $params = [])
+    {
+        $result = false;
+        if (isset(self::$event[$event])) {
+            $callback = self::$event[$event];
+            $result   = call_user_func_array($callback, [$params, $this]);
+        }
+        return $result;
+    }
+}

+ 134 - 0
thinkphp/library/think/db/builder/Mysql.php

@@ -0,0 +1,134 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\db\builder;
+
+use think\db\Builder;
+use think\Exception;
+
+/**
+ * mysql数据库驱动
+ */
+class Mysql extends Builder
+{
+
+    protected $insertAllSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES %DATA% %COMMENT%';
+    protected $updateSql    = 'UPDATE %TABLE% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%';
+
+    /**
+     * 生成insertall SQL
+     * @access public
+     * @param array     $dataSet 数据集
+     * @param array     $options 表达式
+     * @param bool      $replace 是否replace
+     * @return string
+     * @throws Exception
+     */
+    public function insertAll($dataSet, $options = [], $replace = false)
+    {
+        // 获取合法的字段
+        if ('*' == $options['field']) {
+            $fields = array_keys($this->query->getFieldsType($options['table']));
+        } else {
+            $fields = $options['field'];
+        }
+
+        foreach ($dataSet as $data) {
+            foreach ($data as $key => $val) {
+                if (!in_array($key, $fields, true)) {
+                    if ($options['strict']) {
+                        throw new Exception('fields not exists:[' . $key . ']');
+                    }
+                    unset($data[$key]);
+                } elseif (is_null($val)) {
+                    $data[$key] = 'NULL';
+                } elseif (is_scalar($val)) {
+                    $data[$key] = $this->parseValue($val, $key);
+                } elseif (is_object($val) && method_exists($val, '__toString')) {
+                    // 对象数据写入
+                    $data[$key] = $val->__toString();
+                } else {
+                    // 过滤掉非标量数据
+                    unset($data[$key]);
+                }
+            }
+            $value    = array_values($data);
+            $values[] = '( ' . implode(',', $value) . ' )';
+
+            if (!isset($insertFields)) {
+                $insertFields = array_map([$this, 'parseKey'], array_keys($data));
+            }
+        }
+
+        return str_replace(
+            ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
+            [
+                $replace ? 'REPLACE' : 'INSERT',
+                $this->parseTable($options['table'], $options),
+                implode(' , ', $insertFields),
+                implode(' , ', $values),
+                $this->parseComment($options['comment']),
+            ], $this->insertAllSql);
+    }
+
+    /**
+     * 字段和表名处理
+     * @access protected
+     * @param mixed  $key
+     * @param array  $options
+     * @return string
+     */
+    protected function parseKey($key, $options = [], $strict = false)
+    {
+        if (is_numeric($key)) {
+            return $key;
+        } elseif ($key instanceof Expression) {
+            return $key->getValue();
+        }
+
+        $key = trim($key);
+        if (strpos($key, '$.') && false === strpos($key, '(')) {
+            // JSON字段支持
+            list($field, $name) = explode('$.', $key);
+            return 'json_extract(' . $field . ', \'$.' . $name . '\')';
+        } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {
+            list($table, $key) = explode('.', $key, 2);
+            if ('__TABLE__' == $table) {
+                $table = $this->query->getTable();
+            }
+            if (isset($options['alias'][$table])) {
+                $table = $options['alias'][$table];
+            }
+        }
+
+        if ('*' != $key && ($strict || !preg_match('/[,\'\"\*\(\)`.\s]/', $key))) {
+            $key = '`' . $key . '`';
+        }
+        if (isset($table)) {
+            if (strpos($table, '.')) {
+                $table = str_replace('.', '`.`', $table);
+            }
+            $key = '`' . $table . '`.' . $key;
+        }
+        return $key;
+    }
+
+    /**
+     * 随机排序
+     * @access protected
+     * @return string
+     */
+    protected function parseRand()
+    {
+        return 'rand()';
+    }
+
+}

+ 89 - 0
thinkphp/library/think/db/builder/Pgsql.php

@@ -0,0 +1,89 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+
+namespace think\db\builder;
+
+use think\db\Builder;
+
+/**
+ * Pgsql数据库驱动
+ */
+class Pgsql extends Builder
+{
+    protected $insertSql    = 'INSERT INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
+    protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%';
+
+    /**
+     * limit分析
+     * @access protected
+     * @param mixed $limit
+     * @return string
+     */
+    public function parseLimit($limit)
+    {
+        $limitStr = '';
+        if (!empty($limit)) {
+            $limit = explode(',', $limit);
+            if (count($limit) > 1) {
+                $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' ';
+            } else {
+                $limitStr .= ' LIMIT ' . $limit[0] . ' ';
+            }
+        }
+        return $limitStr;
+    }
+
+    /**
+     * 字段和表名处理
+     * @access protected
+     * @param mixed  $key
+     * @param array  $options
+     * @return string
+     */
+    protected function parseKey($key, $options = [], $strict = false)
+    {
+        if (is_numeric($key)) {
+            return $key;
+        } elseif ($key instanceof Expression) {
+            return $key->getValue();
+        }
+
+        $key = trim($key);
+        if (strpos($key, '$.') && false === strpos($key, '(')) {
+            // JSON字段支持
+            list($field, $name) = explode('$.', $key);
+            $key                = $field . '->>\'' . $name . '\'';
+        } elseif (strpos($key, '.')) {
+            list($table, $key) = explode('.', $key, 2);
+            if ('__TABLE__' == $table) {
+                $table = $this->query->getTable();
+            }
+            if (isset($options['alias'][$table])) {
+                $table = $options['alias'][$table];
+            }
+        }
+        if (isset($table)) {
+            $key = $table . '.' . $key;
+        }
+        return $key;
+    }
+
+    /**
+     * 随机排序
+     * @access protected
+     * @return string
+     */
+    protected function parseRand()
+    {
+        return 'RANDOM()';
+    }
+
+}

Some files were not shown because too many files changed in this diff