Browse Source

优享街第一次上传

18315626215 5 years ago
commit
2db5fdec01
100 changed files with 14749 additions and 0 deletions
  1. 9 0
      .bowerrc
  2. 11 0
      .env.sample
  3. 1 0
      .htaccess
  4. 15 0
      .idea/deployment.xml
  5. 6 0
      .idea/misc.xml
  6. 8 0
      .idea/modules.xml
  7. 15 0
      .idea/webServers.xml
  8. 56 0
      .idea/workspace.xml
  9. 8 0
      .idea/youxiangjie.iml
  10. 1 0
      .user.ini
  11. BIN
      1.0.0.20191101_full.zip
  12. 26 0
      404.html
  13. 191 0
      LICENSE
  14. 92 0
      README.md
  15. 1 0
      application/.htaccess
  16. 13 0
      application/admin/behavior/AdminLog.php
  17. 383 0
      application/admin/command/Addon.php
  18. 68 0
      application/admin/command/Addon/stubs/addon.stub
  19. 40 0
      application/admin/command/Addon/stubs/config.stub
  20. 15 0
      application/admin/command/Addon/stubs/controller.stub
  21. 7 0
      application/admin/command/Addon/stubs/info.stub
  22. 178 0
      application/admin/command/Api.php
  23. 22 0
      application/admin/command/Api/lang/zh-cn.php
  24. 244 0
      application/admin/command/Api/library/Builder.php
  25. 511 0
      application/admin/command/Api/library/Extractor.php
  26. 582 0
      application/admin/command/Api/template/index.html
  27. 1479 0
      application/admin/command/Crud.php
  28. 11 0
      application/admin/command/Crud/stubs/add.stub
  29. 35 0
      application/admin/command/Crud/stubs/controller.stub
  30. 42 0
      application/admin/command/Crud/stubs/controllerindex.stub
  31. 11 0
      application/admin/command/Crud/stubs/edit.stub
  32. 6 0
      application/admin/command/Crud/stubs/html/checkbox.stub
  33. 10 0
      application/admin/command/Crud/stubs/html/fieldlist.stub
  34. 10 0
      application/admin/command/Crud/stubs/html/heading-html.stub
  35. 6 0
      application/admin/command/Crud/stubs/html/radio.stub
  36. 1 0
      application/admin/command/Crud/stubs/html/recyclebin-html.stub
  37. 6 0
      application/admin/command/Crud/stubs/html/select.stub
  38. 5 0
      application/admin/command/Crud/stubs/html/switch.stub
  39. 35 0
      application/admin/command/Crud/stubs/index.stub
  40. 47 0
      application/admin/command/Crud/stubs/javascript.stub
  41. 5 0
      application/admin/command/Crud/stubs/lang.stub
  42. 8 0
      application/admin/command/Crud/stubs/mixins/checkbox.stub
  43. 6 0
      application/admin/command/Crud/stubs/mixins/datetime.stub
  44. 1 0
      application/admin/command/Crud/stubs/mixins/enum.stub
  45. 8 0
      application/admin/command/Crud/stubs/mixins/modelinit.stub
  46. 5 0
      application/admin/command/Crud/stubs/mixins/modelrelationmethod.stub
  47. 8 0
      application/admin/command/Crud/stubs/mixins/multiple.stub
  48. 7 0
      application/admin/command/Crud/stubs/mixins/radio.stub
  49. 60 0
      application/admin/command/Crud/stubs/mixins/recyclebinjs.stub
  50. 7 0
      application/admin/command/Crud/stubs/mixins/select.stub
  51. 40 0
      application/admin/command/Crud/stubs/model.stub
  52. 25 0
      application/admin/command/Crud/stubs/recyclebin.stub
  53. 12 0
      application/admin/command/Crud/stubs/relationmodel.stub
  54. 27 0
      application/admin/command/Crud/stubs/validate.stub
  55. 108 0
      application/admin/command/Install.php
  56. 582 0
      application/admin/command/Install/fastadmin.sql
  57. 1 0
      application/admin/command/Install/install.lock
  58. 323 0
      application/admin/command/Menu.php
  59. 162 0
      application/admin/command/Min.php
  60. 4699 0
      application/admin/command/Min/r.js
  61. 6 0
      application/admin/command/Min/stubs/css.stub
  62. 11 0
      application/admin/command/Min/stubs/js.stub
  63. 196 0
      application/admin/common.php
  64. 8 0
      application/admin/config.php
  65. 364 0
      application/admin/controller/Addon.php
  66. 286 0
      application/admin/controller/Ajax.php
  67. 146 0
      application/admin/controller/Category.php
  68. 56 0
      application/admin/controller/Dashboard.php
  69. 122 0
      application/admin/controller/Index.php
  70. 263 0
      application/admin/controller/auth/Admin.php
  71. 138 0
      application/admin/controller/auth/Adminlog.php
  72. 293 0
      application/admin/controller/auth/Group.php
  73. 149 0
      application/admin/controller/auth/Rule.php
  74. 118 0
      application/admin/controller/general/Attachment.php
  75. 259 0
      application/admin/controller/general/Config.php
  76. 88 0
      application/admin/controller/general/Profile.php
  77. 45 0
      application/admin/controller/user/Group.php
  78. 88 0
      application/admin/controller/user/Rule.php
  79. 75 0
      application/admin/controller/user/User.php
  80. 182 0
      application/admin/lang/zh-cn.php
  81. 93 0
      application/admin/lang/zh-cn/addon.php
  82. 8 0
      application/admin/lang/zh-cn/ajax.php
  83. 9 0
      application/admin/lang/zh-cn/auth/admin.php
  84. 10 0
      application/admin/lang/zh-cn/auth/group.php
  85. 20 0
      application/admin/lang/zh-cn/auth/rule.php
  86. 18 0
      application/admin/lang/zh-cn/category.php
  87. 9 0
      application/admin/lang/zh-cn/config.php
  88. 48 0
      application/admin/lang/zh-cn/dashboard.php
  89. 22 0
      application/admin/lang/zh-cn/general/attachment.php
  90. 65 0
      application/admin/lang/zh-cn/general/config.php
  91. 13 0
      application/admin/lang/zh-cn/general/profile.php
  92. 57 0
      application/admin/lang/zh-cn/index.php
  93. 9 0
      application/admin/lang/zh-cn/user/group.php
  94. 15 0
      application/admin/lang/zh-cn/user/rule.php
  95. 33 0
      application/admin/lang/zh-cn/user/user.php
  96. 523 0
      application/admin/library/Auth.php
  97. 479 0
      application/admin/library/traits/Backend.php
  98. 34 0
      application/admin/model/Admin.php
  99. 69 0
      application/admin/model/AdminLog.php
  100. 21 0
      application/admin/model/AuthGroup.php

+ 9 - 0
.bowerrc

@@ -0,0 +1,9 @@
+{
+  "directory" : "public/assets/libs",
+  "ignoredDependencies": [
+    "file-saver",
+    "html2canvas",
+    "jspdf",
+    "jspdf-autotable"
+  ]
+}

+ 11 - 0
.env.sample

@@ -0,0 +1,11 @@
+[app]
+debug = false
+trace = false
+
+[database]
+hostname = 127.0.0.1
+database = fastadmin
+username = root
+password = root
+hostport = 3306
+prefix = fa_

+ 1 - 0
.htaccess

@@ -0,0 +1 @@
+ 

+ 15 - 0
.idea/deployment.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="PublishConfigData" autoUpload="On explicit save action" serverName="yxj" autoUploadExternalChanges="true">
+    <serverData>
+      <paths name="yxj">
+        <serverdata>
+          <mappings>
+            <mapping deploy="/" local="$PROJECT_DIR$" web="/" />
+          </mappings>
+        </serverdata>
+      </paths>
+    </serverData>
+    <option name="myAutoUpload" value="ON_EXPLICIT_SAVE" />
+  </component>
+</project>

+ 6 - 0
.idea/misc.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="JavaScriptSettings">
+    <option name="languageLevel" value="ES6" />
+  </component>
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/youxiangjie.iml" filepath="$PROJECT_DIR$/.idea/youxiangjie.iml" />
+    </modules>
+  </component>
+</project>

+ 15 - 0
.idea/webServers.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="WebServers">
+    <option name="servers">
+      <webServer id="fb30c637-c48d-41c8-a46d-b30eb43f2303" name="yxj" url="http://47.111.157.216">
+        <fileTransfer host="47.111.157.216" port="21">
+          <advancedOptions>
+            <advancedOptions dataProtectionLevel="Private" shareSSLContext="true" />
+          </advancedOptions>
+          <option name="port" value="21" />
+        </fileTransfer>
+      </webServer>
+    </option>
+  </component>
+</project>

+ 56 - 0
.idea/workspace.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ChangeListManager">
+    <list default="true" id="0c798a10-12e9-4340-984e-11d11f343507" name="Default Changelist" comment="" />
+    <option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
+    <option name="SHOW_DIALOG" value="false" />
+    <option name="HIGHLIGHT_CONFLICTS" value="true" />
+    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+    <option name="LAST_RESOLUTION" value="IGNORE" />
+  </component>
+  <component name="ComposerSettings">
+    <execution>
+      <executable />
+    </execution>
+  </component>
+  <component name="ProjectId" id="1Um3PwqsxizdOTLBw4mBJ8EzGbT" />
+  <component name="PropertiesComponent">
+    <property name="WebServerToolWindowFactoryState" value="false" />
+    <property name="last_opened_file_path" value="$PROJECT_DIR$" />
+    <property name="node.js.detected.package.eslint" value="true" />
+    <property name="node.js.detected.package.tslint" value="true" />
+    <property name="node.js.path.for.package.eslint" value="project" />
+    <property name="node.js.path.for.package.tslint" value="project" />
+    <property name="node.js.selected.package.eslint" value="(autodetect)" />
+    <property name="node.js.selected.package.tslint" value="(autodetect)" />
+  </component>
+  <component name="RunDashboard">
+    <option name="ruleStates">
+      <list>
+        <RuleState>
+          <option name="name" value="ConfigurationTypeDashboardGroupingRule" />
+        </RuleState>
+        <RuleState>
+          <option name="name" value="StatusDashboardGroupingRule" />
+        </RuleState>
+      </list>
+    </option>
+  </component>
+  <component name="SvnConfiguration">
+    <configuration />
+  </component>
+  <component name="TaskManager">
+    <task active="true" id="Default" summary="Default task">
+      <changelist id="0c798a10-12e9-4340-984e-11d11f343507" name="Default Changelist" comment="" />
+      <created>1575947922294</created>
+      <option name="number" value="Default" />
+      <option name="presentableId" value="Default" />
+      <updated>1575947922294</updated>
+      <workItem from="1575947923604" duration="2510000" />
+    </task>
+    <servers />
+  </component>
+  <component name="TypeScriptGeneratedFilesManager">
+    <option name="version" value="1" />
+  </component>
+</project>

+ 8 - 0
.idea/youxiangjie.iml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 1 - 0
.user.ini

@@ -0,0 +1 @@
+open_basedir=/www/wwwroot/yxj/:/tmp/:/proc/

BIN
1.0.0.20191101_full.zip


+ 26 - 0
404.html

@@ -0,0 +1,26 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+<title>404</title>
+<style>
+	body{
+		background-color:#444;
+		font-size:14px;
+	}
+	h3{
+		font-size:60px;
+		color:#eee;
+		text-align:center;
+		padding-top:30px;
+		font-weight:normal;
+	}
+</style>
+</head>
+
+<body>
+<h3>404,您请求的文件不存在!</h3>
+</body>
+</html>

+ 191 - 0
LICENSE

@@ -0,0 +1,191 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "{}" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+   Copyright 2017 Karson
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 92 - 0
README.md

@@ -0,0 +1,92 @@
+FastAdmin是一款基于ThinkPHP5+Bootstrap的极速后台开发框架。
+
+
+## **主要特性**
+
+* 基于`Auth`验证的权限管理系统
+    * 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
+    * 支持单管理员多角色
+    * 支持管理子级数据或个人数据
+* 强大的一键生成功能
+    * 一键生成CRUD,包括控制器、模型、视图、JS、语言包、菜单、回收站等
+    * 一键压缩打包JS和CSS文件,一键CDN静态资源部署
+    * 一键生成控制器菜单和规则
+    * 一键生成API接口文档
+* 完善的前端功能组件开发
+    * 基于`AdminLTE`二次开发
+    * 基于`Bootstrap`开发,自适应手机、平板、PC
+    * 基于`RequireJS`进行JS模块管理,按需加载
+    * 基于`Less`进行样式开发
+    * 基于`Bower`进行前端组件包管理
+* 强大的插件扩展功能,在线安装卸载升级插件
+* 通用的会员模块和API模块
+* 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
+* 二级域名部署支持,同时域名支持绑定到插件
+* 多语言支持,服务端及客户端支持
+* 强大的第三方模块支持([CMS](https://www.fastadmin.net/store/cms.html)、[博客](https://www.fastadmin.net/store/blog.html)、[知识付费问答](https://www.fastadmin.net/store/ask.html))
+* 整合第三方短信接口(阿里云、腾讯云短信)
+* 无缝整合第三方云存储(七牛、阿里云OSS、又拍云)功能
+* 第三方富文本编辑器支持(Summernote、Kindeditor、百度编辑器)
+* 第三方登录(QQ、微信、微博)整合
+* 第三方支付(微信、支付宝)无缝整合,微信支持PC端扫码支付
+* 丰富的插件应用市场
+
+## **安装使用**
+
+https://doc.fastadmin.net
+
+## **在线演示**
+
+https://demo.fastadmin.net
+
+用户名:admin
+
+密 码:123456
+
+提 示:演示站数据无法进行修改,请下载源码安装体验全部功能
+
+## **界面截图**
+![控制台](https://gitee.com/uploads/images/2017/0411/113717_e99ff3e7_10933.png "控制台")
+
+## **问题反馈**
+
+在使用中有任何问题,请使用以下联系方式联系我们
+
+交流社区: https://forum.fastadmin.net
+
+QQ群: [636393962](https://jq.qq.com/?_wv=1027&k=487PNBb)(满) [708784003](https://jq.qq.com/?_wv=1027&k=5ObjtwM)(满) [964776039](https://jq.qq.com/?_wv=1027&k=59qjU2P)(3群)
+
+Email: (karsonzhang#163.com, 把#换成@)
+
+Github: https://github.com/karsonzhang/fastadmin
+
+Gitee: https://gitee.com/karson/fastadmin
+
+## **特别鸣谢**
+
+感谢以下的项目,排名不分先后
+
+ThinkPHP:http://www.thinkphp.cn
+
+AdminLTE:https://adminlte.io
+
+Bootstrap:http://getbootstrap.com
+
+jQuery:http://jquery.com
+
+Bootstrap-table:https://github.com/wenzhixin/bootstrap-table
+
+Nice-validator: https://validator.niceue.com
+
+SelectPage: https://github.com/TerryZ/SelectPage
+
+
+## **版权信息**
+
+FastAdmin遵循Apache2开源协议发布,并提供免费使用。
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有Copyright © 2017-2019 by FastAdmin (https://www.fastadmin.net)
+
+All rights reserved。

+ 1 - 0
application/.htaccess

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

+ 13 - 0
application/admin/behavior/AdminLog.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace app\admin\behavior;
+
+class AdminLog
+{
+    public function run(&$params)
+    {
+        if (request()->isPost()) {
+            \app\admin\model\AdminLog::record();
+        }
+    }
+}

+ 383 - 0
application/admin/command/Addon.php

@@ -0,0 +1,383 @@
+<?php
+
+namespace app\admin\command;
+
+use think\addons\AddonException;
+use think\addons\Service;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Db;
+use think\Exception;
+use think\exception\PDOException;
+
+class Addon extends Command
+{
+
+    protected function configure()
+    {
+        $this
+            ->setName('addon')
+            ->addOption('name', 'a', Option::VALUE_REQUIRED, 'addon name', null)
+            ->addOption('action', 'c', Option::VALUE_REQUIRED, 'action(create/enable/disable/install/uninstall/refresh/upgrade/package/move)', 'create')
+            ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', null)
+            ->addOption('release', 'r', Option::VALUE_OPTIONAL, 'addon release version', null)
+            ->addOption('uid', 'u', Option::VALUE_OPTIONAL, 'fastadmin uid', null)
+            ->addOption('token', 't', Option::VALUE_OPTIONAL, 'fastadmin token', null)
+            ->addOption('local', 'l', Option::VALUE_OPTIONAL, 'local package', null)
+            ->setDescription('Addon manager');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $name = $input->getOption('name') ?: '';
+        $action = $input->getOption('action') ?: '';
+        if (stripos($name, 'addons/') !== false) {
+            $name = explode('/', $name)[1];
+        }
+        //强制覆盖
+        $force = $input->getOption('force');
+        //版本
+        $release = $input->getOption('release') ?: '';
+        //uid
+        $uid = $input->getOption('uid') ?: '';
+        //token
+        $token = $input->getOption('token') ?: '';
+
+        include dirname(__DIR__) . DS . 'common.php';
+
+        if (!$name) {
+            throw new Exception('Addon name could not be empty');
+        }
+        if (!$action || !in_array($action, ['create', 'disable', 'enable', 'install', 'uninstall', 'refresh', 'upgrade', 'package', 'move'])) {
+            throw new Exception('Please input correct action name');
+        }
+
+        // 查询一次SQL,判断连接是否正常
+        Db::execute("SELECT 1");
+
+        $addonDir = ADDON_PATH . $name . DS;
+        switch ($action) {
+            case 'create':
+                //非覆盖模式时如果存在则报错
+                if (is_dir($addonDir) && !$force) {
+                    throw new Exception("addon already exists!\nIf you need to create again, use the parameter --force=true ");
+                }
+                //如果存在先移除
+                if (is_dir($addonDir)) {
+                    rmdirs($addonDir);
+                }
+                mkdir($addonDir, 0755, true);
+                mkdir($addonDir . DS . 'controller', 0755, true);
+                $menuList = \app\common\library\Menu::export($name);
+                $createMenu = $this->getCreateMenu($menuList);
+                $prefix = Config::get('database.prefix');
+                $createTableSql = '';
+                try {
+                    $result = Db::query("SHOW CREATE TABLE `" . $prefix . $name . "`;");
+                    if (isset($result[0]) && isset($result[0]['Create Table'])) {
+                        $createTableSql = $result[0]['Create Table'];
+                    }
+                } catch (PDOException $e) {
+
+                }
+
+                $data = [
+                    'name'               => $name,
+                    'addon'              => $name,
+                    'addonClassName'     => ucfirst($name),
+                    'addonInstallMenu'   => $createMenu ? "\$menu = " . var_export_short($createMenu, "\t") . ";\n\tMenu::create(\$menu);" : '',
+                    'addonUninstallMenu' => $menuList ? 'Menu::delete("' . $name . '");' : '',
+                    'addonEnableMenu'    => $menuList ? 'Menu::enable("' . $name . '");' : '',
+                    'addonDisableMenu'   => $menuList ? 'Menu::disable("' . $name . '");' : '',
+                ];
+                $this->writeToFile("addon", $data, $addonDir . ucfirst($name) . '.php');
+                $this->writeToFile("config", $data, $addonDir . 'config.php');
+                $this->writeToFile("info", $data, $addonDir . 'info.ini');
+                $this->writeToFile("controller", $data, $addonDir . 'controller' . DS . 'Index.php');
+                if ($createTableSql) {
+                    $createTableSql = str_replace("`" . $prefix, '`__PREFIX__', $createTableSql);
+                    file_put_contents($addonDir . 'install.sql', $createTableSql);
+                }
+
+                $output->info("Create Successed!");
+                break;
+            case 'disable':
+            case 'enable':
+                try {
+                    //调用启用、禁用的方法
+                    Service::$action($name, 0);
+                } catch (AddonException $e) {
+                    if ($e->getCode() != -3) {
+                        throw new Exception($e->getMessage());
+                    }
+                    if (!$force) {
+                        //如果有冲突文件则提醒
+                        $data = $e->getData();
+                        foreach ($data['conflictlist'] as $k => $v) {
+                            $output->warning($v);
+                        }
+                        $output->info("Are you sure you want to " . ($action == 'enable' ? 'override' : 'delete') . " all those files?  Type 'yes' to continue: ");
+                        $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
+                        if (trim($line) != 'yes') {
+                            throw new Exception("Operation is aborted!");
+                        }
+                    }
+                    //调用启用、禁用的方法
+                    Service::$action($name, 1);
+                } catch (Exception $e) {
+                    throw new Exception($e->getMessage());
+                }
+                $output->info(ucfirst($action) . " Successed!");
+                break;
+            case 'install':
+                //非覆盖模式时如果存在则报错
+                if (is_dir($addonDir) && !$force) {
+                    throw new Exception("addon already exists!\nIf you need to install again, use the parameter --force=true ");
+                }
+                //如果存在先移除
+                if (is_dir($addonDir)) {
+                    rmdirs($addonDir);
+                }
+                // 获取本地路径
+                $local = $input->getOption('local');
+                try {
+                    Service::install($name, 0, ['version' => $release], $local);
+                } catch (AddonException $e) {
+                    if ($e->getCode() != -3) {
+                        throw new Exception($e->getMessage());
+                    }
+                    if (!$force) {
+                        //如果有冲突文件则提醒
+                        $data = $e->getData();
+                        foreach ($data['conflictlist'] as $k => $v) {
+                            $output->warning($v);
+                        }
+                        $output->info("Are you sure you want to override all those files?  Type 'yes' to continue: ");
+                        $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
+                        if (trim($line) != 'yes') {
+                            throw new Exception("Operation is aborted!");
+                        }
+                    }
+                    Service::install($name, 1, ['version' => $release, 'uid' => $uid, 'token' => $token], $local);
+                } catch (Exception $e) {
+                    throw new Exception($e->getMessage());
+                }
+
+                $output->info("Install Successed!");
+                break;
+            case 'uninstall':
+                //非覆盖模式时如果存在则报错
+                if (!$force) {
+                    throw new Exception("If you need to uninstall addon, use the parameter --force=true ");
+                }
+                try {
+                    Service::uninstall($name, 0);
+                } catch (AddonException $e) {
+                    if ($e->getCode() != -3) {
+                        throw new Exception($e->getMessage());
+                    }
+                    if (!$force) {
+                        //如果有冲突文件则提醒
+                        $data = $e->getData();
+                        foreach ($data['conflictlist'] as $k => $v) {
+                            $output->warning($v);
+                        }
+                        $output->info("Are you sure you want to delete all those files?  Type 'yes' to continue: ");
+                        $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
+                        if (trim($line) != 'yes') {
+                            throw new Exception("Operation is aborted!");
+                        }
+                    }
+                    Service::uninstall($name, 1);
+                } catch (Exception $e) {
+                    throw new Exception($e->getMessage());
+                }
+
+                $output->info("Uninstall Successed!");
+                break;
+            case 'refresh':
+                Service::refresh();
+                $output->info("Refresh Successed!");
+                break;
+            case 'upgrade':
+                Service::upgrade($name, ['version' => $release, 'uid' => $uid, 'token' => $token]);
+                $output->info("Upgrade Successed!");
+                break;
+            case 'package':
+                $infoFile = $addonDir . 'info.ini';
+                if (!is_file($infoFile)) {
+                    throw new Exception(__('Addon info file was not found'));
+                }
+
+                $info = get_addon_info($name);
+                if (!$info) {
+                    throw new Exception(__('Addon info file data incorrect'));
+                }
+                $infoname = isset($info['name']) ? $info['name'] : '';
+                if (!$infoname || !preg_match("/^[a-z]+$/i", $infoname) || $infoname != $name) {
+                    throw new Exception(__('Addon info name incorrect'));
+                }
+
+                $infoversion = isset($info['version']) ? $info['version'] : '';
+                if (!$infoversion || !preg_match("/^\d+\.\d+\.\d+$/i", $infoversion)) {
+                    throw new Exception(__('Addon info version incorrect'));
+                }
+
+                $addonTmpDir = RUNTIME_PATH . 'addons' . DS;
+                if (!is_dir($addonTmpDir)) {
+                    @mkdir($addonTmpDir, 0755, true);
+                }
+                $addonFile = $addonTmpDir . $infoname . '-' . $infoversion . '.zip';
+                if (!class_exists('ZipArchive')) {
+                    throw new Exception(__('ZinArchive not install'));
+                }
+                $zip = new \ZipArchive;
+                $zip->open($addonFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
+
+                $files = new \RecursiveIteratorIterator(
+                    new \RecursiveDirectoryIterator($addonDir), \RecursiveIteratorIterator::LEAVES_ONLY
+                );
+
+                foreach ($files as $name => $file) {
+                    if (!$file->isDir()) {
+                        $filePath = $file->getRealPath();
+                        $relativePath = str_replace(DS, '/', substr($filePath, strlen($addonDir)));
+                        if (!in_array($file->getFilename(), ['.git', '.DS_Store', 'Thumbs.db'])) {
+                            $zip->addFile($filePath, $relativePath);
+                        }
+                    }
+                }
+                $zip->close();
+                $output->info("Package Successed!");
+                break;
+            case 'move':
+                $movePath = [
+                    'adminOnlySelfDir' => ['admin/behavior', 'admin/controller', 'admin/library', 'admin/model', 'admin/validate', 'admin/view'],
+                    'adminAllSubDir' => ['admin/lang'],
+                    'publicDir' => ['public/assets/addons', 'public/assets/js/backend']
+                ];
+                $paths = [];
+                $appPath = str_replace('/', DS, APP_PATH);
+                $rootPath = str_replace('/', DS, ROOT_PATH);
+                foreach ($movePath as $k => $items) {
+                    switch ($k) {
+                        case 'adminOnlySelfDir':
+                            foreach ($items as $v) {
+                                $v = str_replace('/', DS, $v);
+                                $oldPath = $appPath . $v . DS . $name;
+                                $newPath = $rootPath . "addons" . DS . $name . DS . "application" . DS . $v . DS . $name;
+                                $paths[$oldPath] = $newPath;
+                            }
+                            break;
+                        case 'adminAllSubDir':
+                            foreach ($items as $v) {
+                                $v = str_replace('/', DS, $v);
+                                $vPath = $appPath . $v;
+                                $list = scandir($vPath);
+                                foreach ($list as $_v) {
+                                    if (!in_array($_v, ['.', '..']) && is_dir($vPath . DS . $_v)) {
+                                        $oldPath = $appPath . $v . DS . $_v . DS . $name;
+                                        $newPath = $rootPath . "addons" . DS . $name . DS . "application" . DS . $v . DS . $_v . DS . $name;
+                                        $paths[$oldPath] = $newPath;
+                                    }
+                                }
+                            }
+                            break;
+                        case 'publicDir':
+                            foreach ($items as $v) {
+                                $v = str_replace('/', DS, $v);
+                                $oldPath = $rootPath . $v . DS . $name;
+                                $newPath = $rootPath . 'addons' . DS . $name . DS . $v . DS . $name;
+                                $paths[$oldPath] = $newPath;
+                            }
+                            break;
+                    }
+                }
+                foreach ($paths as $oldPath => $newPath) {
+                    if (is_dir($oldPath)) {
+                        if ($force) {
+                            if (is_dir($newPath)) {
+                                $list = scandir($newPath);
+                                foreach ($list as $_v) {
+                                    if (!in_array($_v, ['.', '..'])) {
+                                        $file = $newPath . DS . $_v;
+                                        @chmod($file, 0777);
+                                        @unlink($file);
+                                    }
+                                }
+                                @rmdir($newPath);
+                            }
+                        }
+                        copydirs($oldPath, $newPath);
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * 获取创建菜单的数组
+     * @param array $menu
+     * @return array
+     */
+    protected function getCreateMenu($menu)
+    {
+        $result = [];
+        foreach ($menu as $k => & $v) {
+            $arr = [
+                'name'  => $v['name'],
+                'title' => $v['title'],
+            ];
+            if ($v['icon'] != 'fa fa-circle-o') {
+                $arr['icon'] = $v['icon'];
+            }
+            if ($v['ismenu']) {
+                $arr['ismenu'] = $v['ismenu'];
+            }
+            if (isset($v['childlist']) && $v['childlist']) {
+                $arr['sublist'] = $this->getCreateMenu($v['childlist']);
+            }
+            $result[] = $arr;
+        }
+        return $result;
+    }
+
+    /**
+     * 写入到文件
+     * @param string $name
+     * @param array $data
+     * @param string $pathname
+     * @return mixed
+     */
+    protected function writeToFile($name, $data, $pathname)
+    {
+        $search = $replace = [];
+        foreach ($data as $k => $v) {
+            $search[] = "{%{$k}%}";
+            $replace[] = $v;
+        }
+        $stub = file_get_contents($this->getStub($name));
+        $content = str_replace($search, $replace, $stub);
+
+        if (!is_dir(dirname($pathname))) {
+            mkdir(strtolower(dirname($pathname)), 0755, true);
+        }
+        return file_put_contents($pathname, $content);
+    }
+
+    /**
+     * 获取基础模板
+     * @param string $name
+     * @return string
+     */
+    protected function getStub($name)
+    {
+        return __DIR__ . '/Addon/stubs/' . $name . '.stub';
+    }
+    
+}

+ 68 - 0
application/admin/command/Addon/stubs/addon.stub

@@ -0,0 +1,68 @@
+<?php
+
+namespace addons\{%name%};
+
+use app\common\library\Menu;
+use think\Addons;
+
+/**
+ * 插件
+ */
+class {%addonClassName%} extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        {%addonInstallMenu%}
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        {%addonUninstallMenu%}
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     * @return bool
+     */
+    public function enable()
+    {
+        {%addonEnableMenu%}
+        return true;
+    }
+
+    /**
+     * 插件禁用方法
+     * @return bool
+     */
+    public function disable()
+    {
+        {%addonDisableMenu%}
+        return true;
+    }
+
+    /**
+     * 实现钩子方法
+     * @return mixed
+     */
+    public function testhook($param)
+    {
+        // 调用钩子时候的参数信息
+        print_r($param);
+        // 当前插件的配置信息,配置信息存在当前目录的config.php文件中,见下方
+        print_r($this->getConfig());
+        // 可以返回模板,模板文件默认读取的为插件目录中的文件。模板名不能为空!
+        //return $this->fetch('view/info');
+    }
+
+}

+ 40 - 0
application/admin/command/Addon/stubs/config.stub

@@ -0,0 +1,40 @@
+<?php
+
+return [
+    [
+        //配置唯一标识
+        'name'    => 'usernmae',
+        //显示的标题
+        'title'   => '用户名',
+        //类型
+        'type'    => 'string',
+        //数据字典
+        'content' => [
+        ],
+        //值
+        'value'   => '',
+        //验证规则 
+        'rule'    => 'required',
+        //错误消息
+        'msg'     => '',
+        //提示消息
+        'tip'     => '',
+        //成功消息
+        'ok'      => '',
+        //扩展信息
+        'extend'  => ''
+    ],
+    [
+        'name'    => 'password',
+        'title'   => '密码',
+        'type'    => 'string',
+        'content' => [
+        ],
+        'value'   => '',
+        'rule'    => 'required',
+        'msg'     => '',
+        'tip'     => '',
+        'ok'      => '',
+        'extend'  => ''
+    ],
+];

+ 15 - 0
application/admin/command/Addon/stubs/controller.stub

@@ -0,0 +1,15 @@
+<?php
+
+namespace addons\{%addon%}\controller;
+
+use think\addons\Controller;
+
+class Index extends Controller
+{
+
+    public function index()
+    {
+        $this->error("当前插件暂无前台页面");
+    }
+
+}

+ 7 - 0
application/admin/command/Addon/stubs/info.stub

@@ -0,0 +1,7 @@
+name = {%name%}
+title = 插件名称{%name%}
+intro = FastAdmin插件
+author = yourname
+website = https://www.fastadmin.net
+version = 1.0.0
+state = 1

+ 178 - 0
application/admin/command/Api.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace app\admin\command;
+
+use app\admin\command\Api\library\Builder;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Exception;
+
+class Api extends Command
+{
+    protected function configure()
+    {
+        $site = Config::get('site');
+        $this
+            ->setName('api')
+            ->addOption('url', 'u', Option::VALUE_OPTIONAL, 'default api url', '')
+            ->addOption('module', 'm', Option::VALUE_OPTIONAL, 'module name(admin/index/api)', 'api')
+            ->addOption('output', 'o', Option::VALUE_OPTIONAL, 'output index file name', 'api.html')
+            ->addOption('template', 'e', Option::VALUE_OPTIONAL, '', 'index.html')
+            ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override general file', false)
+            ->addOption('title', 't', Option::VALUE_OPTIONAL, 'document title', $site['name'])
+            ->addOption('author', 'a', Option::VALUE_OPTIONAL, 'document author', $site['name'])
+            ->addOption('class', 'c', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'extend class', null)
+            ->addOption('language', 'l', Option::VALUE_OPTIONAL, 'language', 'zh-cn')
+            ->setDescription('Build Api document from controller');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $apiDir = __DIR__ . DS . 'Api' . DS;
+
+        $force = $input->getOption('force');
+        $url = $input->getOption('url');
+        $language = $input->getOption('language');
+        $language = $language ? $language : 'zh-cn';
+        $langFile = $apiDir . 'lang' . DS . $language . '.php';
+        if (!is_file($langFile)) {
+            throw new Exception('language file not found');
+        }
+        $lang = include_once $langFile;
+        // 目标目录
+        $output_dir = ROOT_PATH . 'public' . DS;
+        $output_file = $output_dir . $input->getOption('output');
+        if (is_file($output_file) && !$force) {
+            throw new Exception("api index file already exists!\nIf you need to rebuild again, use the parameter --force=true ");
+        }
+        // 模板文件
+        $template_dir = $apiDir . 'template' . DS;
+        $template_file = $template_dir . $input->getOption('template');
+        if (!is_file($template_file)) {
+            throw new Exception('template file not found');
+        }
+        // 额外的类
+        $classes = $input->getOption('class');
+        // 标题
+        $title = $input->getOption('title');
+        // 作者
+        $author = $input->getOption('author');
+        // 模块
+        $module = $input->getOption('module');
+
+        $moduleDir = APP_PATH . $module . DS;
+        if (!is_dir($moduleDir)) {
+            throw new Exception('module not found');
+        }
+
+        if (version_compare(PHP_VERSION, '7.0.0', '<')) {
+            if (extension_loaded('Zend OPcache')) {
+                $configuration = opcache_get_configuration();
+                $directives = $configuration['directives'];
+                $configName = request()->isCli() ? 'opcache.enable_cli' : 'opcache.enable';
+                if (!$directives[$configName]) {
+                    throw new Exception("Please make sure {$configName} is turned on, Get help:https://forum.fastadmin.net/d/1321");
+                }
+            } else {
+                throw new Exception("Please make sure opcache already enabled, Get help:https://forum.fastadmin.net/d/1321");
+            }
+        }
+
+        $controllerDir = $moduleDir . Config::get('url_controller_layer') . DS;
+        $files = new \RecursiveIteratorIterator(
+            new \RecursiveDirectoryIterator($controllerDir),
+            \RecursiveIteratorIterator::LEAVES_ONLY
+        );
+
+        foreach ($files as $name => $file) {
+            if (!$file->isDir() && $file->getExtension() == 'php') {
+                $filePath = $file->getRealPath();
+                $classes[] = $this->get_class_from_file($filePath);
+            }
+        }
+        $classes = array_unique(array_filter($classes));
+
+        $config = [
+            'title'       => $title,
+            'author'      => $author,
+            'description' => '',
+            'apiurl'      => $url,
+            'language'    => $language,
+        ];
+        $builder = new Builder($classes);
+        $content = $builder->render($template_file, ['config' => $config, 'lang' => $lang]);
+
+        if (!file_put_contents($output_file, $content)) {
+            throw new Exception('Cannot save the content to ' . $output_file);
+        }
+        $output->info("Build Successed!");
+    }
+
+    /**
+     * get full qualified class name
+     *
+     * @param string $path_to_file
+     * @author JBYRNE http://jarretbyrne.com/2015/06/197/
+     * @return string
+     */
+    protected function get_class_from_file($path_to_file)
+    {
+        //Grab the contents of the file
+        $contents = file_get_contents($path_to_file);
+
+        //Start with a blank namespace and class
+        $namespace = $class = "";
+
+        //Set helper values to know that we have found the namespace/class token and need to collect the string values after them
+        $getting_namespace = $getting_class = false;
+
+        //Go through each token and evaluate it as necessary
+        foreach (token_get_all($contents) as $token) {
+
+            //If this token is the namespace declaring, then flag that the next tokens will be the namespace name
+            if (is_array($token) && $token[0] == T_NAMESPACE) {
+                $getting_namespace = true;
+            }
+
+            //If this token is the class declaring, then flag that the next tokens will be the class name
+            if (is_array($token) && $token[0] == T_CLASS) {
+                $getting_class = true;
+            }
+
+            //While we're grabbing the namespace name...
+            if ($getting_namespace === true) {
+
+                //If the token is a string or the namespace separator...
+                if (is_array($token) && in_array($token[0], [T_STRING, T_NS_SEPARATOR])) {
+
+                    //Append the token's value to the name of the namespace
+                    $namespace .= $token[1];
+                } elseif ($token === ';') {
+
+                    //If the token is the semicolon, then we're done with the namespace declaration
+                    $getting_namespace = false;
+                }
+            }
+
+            //While we're grabbing the class name...
+            if ($getting_class === true) {
+
+                //If the token is a string, it's the name of the class
+                if (is_array($token) && $token[0] == T_STRING) {
+
+                    //Store the token's value as the class name
+                    $class = $token[1];
+
+                    //Got what we need, stope here
+                    break;
+                }
+            }
+        }
+
+        //Build the fully-qualified class name and return it
+        return $namespace ? $namespace . '\\' . $class : $class;
+    }
+}

+ 22 - 0
application/admin/command/Api/lang/zh-cn.php

@@ -0,0 +1,22 @@
+<?php
+
+return [
+    'Info'             => '基础信息',
+    'Sandbox'          => '在线测试',
+    'Sampleoutput'     => '返回示例',
+    'Headers'          => 'Headers',
+    'Parameters'       => '参数',
+    'Body'             => '正文',
+    'Name'             => '名称',
+    'Type'             => '类型',
+    'Required'         => '必选',
+    'Description'      => '描述',
+    'Send'             => '提交',
+    'Reset'            => '重置',
+    'Tokentips'        => 'Token在会员注册或登录后都会返回,WEB端同时存在于Cookie中',
+    'Apiurltips'       => 'API接口URL',
+    'Savetips'         => '点击保存后Token和Api url都将保存在本地Localstorage中',
+    'ReturnHeaders'    => '响应头',
+    'ReturnParameters' => '返回参数',
+    'Response'         => '响应输出',
+];

+ 244 - 0
application/admin/command/Api/library/Builder.php

@@ -0,0 +1,244 @@
+<?php
+
+namespace app\admin\command\Api\library;
+
+use think\Config;
+
+/**
+ * @website https://github.com/calinrada/php-apidoc
+ * @author  Calin Rada <rada.calin@gmail.com>
+ * @author  Karson <karsonzhang@163.com>
+ */
+class Builder
+{
+
+    /**
+     *
+     * @var \think\View
+     */
+    public $view = null;
+
+    /**
+     * parse classes
+     * @var array
+     */
+    protected $classes = [];
+
+    /**
+     *
+     * @param array $classes
+     */
+    public function __construct($classes = [])
+    {
+        $this->classes = array_merge($this->classes, $classes);
+        $this->view = new \think\View(Config::get('template'), Config::get('view_replace_str'));
+    }
+
+    protected function extractAnnotations()
+    {
+        foreach ($this->classes as $class) {
+            $classAnnotation = Extractor::getClassAnnotations($class);
+            // 如果忽略
+            if (isset($classAnnotation['ApiInternal'])) {
+                continue;
+            }
+            Extractor::getClassMethodAnnotations($class);
+        }
+        $allClassAnnotation = Extractor::getAllClassAnnotations();
+        $allClassMethodAnnotation = Extractor::getAllClassMethodAnnotations();
+
+//        foreach ($allClassMethodAnnotation as $className => &$methods) {
+//            foreach ($methods as &$method) {
+//                //权重判断
+//                if ($method && !isset($method['ApiWeigh']) && isset($allClassAnnotation[$className]['ApiWeigh'])) {
+//                    $method['ApiWeigh'] = $allClassAnnotation[$className]['ApiWeigh'];
+//                }
+//            }
+//        }
+//        unset($methods);
+        return [$allClassAnnotation, $allClassMethodAnnotation];
+    }
+
+    protected function generateHeadersTemplate($docs)
+    {
+        if (!isset($docs['ApiHeaders'])) {
+            return [];
+        }
+
+        $headerslist = array();
+        foreach ($docs['ApiHeaders'] as $params) {
+            $tr = array(
+                'name'        => $params['name'],
+                'type'        => $params['type'],
+                'sample'      => isset($params['sample']) ? $params['sample'] : '',
+                'required'    => isset($params['required']) ? $params['required'] : false,
+                'description' => isset($params['description']) ? $params['description'] : '',
+            );
+            $headerslist[] = $tr;
+        }
+
+        return $headerslist;
+    }
+
+    protected function generateParamsTemplate($docs)
+    {
+        if (!isset($docs['ApiParams'])) {
+            return [];
+        }
+
+        $paramslist = array();
+        foreach ($docs['ApiParams'] as $params) {
+            $tr = array(
+                'name'        => $params['name'],
+                'type'        => isset($params['type']) ? $params['type'] : 'string',
+                'sample'      => isset($params['sample']) ? $params['sample'] : '',
+                'required'    => isset($params['required']) ? $params['required'] : true,
+                'description' => isset($params['description']) ? $params['description'] : '',
+            );
+            $paramslist[] = $tr;
+        }
+
+        return $paramslist;
+    }
+
+    protected function generateReturnHeadersTemplate($docs)
+    {
+        if (!isset($docs['ApiReturnHeaders'])) {
+            return [];
+        }
+
+        $headerslist = array();
+        foreach ($docs['ApiReturnHeaders'] as $params) {
+            $tr = array(
+                'name'        => $params['name'],
+                'type'        => 'string',
+                'sample'      => isset($params['sample']) ? $params['sample'] : '',
+                'required'    => isset($params['required']) && $params['required'] ? 'Yes' : 'No',
+                'description' => isset($params['description']) ? $params['description'] : '',
+            );
+            $headerslist[] = $tr;
+        }
+
+        return $headerslist;
+    }
+
+    protected function generateReturnParamsTemplate($st_params)
+    {
+        if (!isset($st_params['ApiReturnParams'])) {
+            return [];
+        }
+
+        $paramslist = array();
+        foreach ($st_params['ApiReturnParams'] as $params) {
+            $tr = array(
+                'name'        => $params['name'],
+                'type'        => isset($params['type']) ? $params['type'] : 'string',
+                'sample'      => isset($params['sample']) ? $params['sample'] : '',
+                'description' => isset($params['description']) ? $params['description'] : '',
+            );
+            $paramslist[] = $tr;
+        }
+
+        return $paramslist;
+    }
+
+    protected function generateBadgeForMethod($data)
+    {
+        $method = strtoupper(is_array($data['ApiMethod'][0]) ? $data['ApiMethod'][0]['data'] : $data['ApiMethod'][0]);
+        $labes = array(
+            'POST'    => 'label-primary',
+            'GET'     => 'label-success',
+            'PUT'     => 'label-warning',
+            'DELETE'  => 'label-danger',
+            'PATCH'   => 'label-default',
+            'OPTIONS' => 'label-info'
+        );
+
+        return isset($labes[$method]) ? $labes[$method] : $labes['GET'];
+    }
+
+    public function parse()
+    {
+        list($allClassAnnotations, $allClassMethodAnnotations) = $this->extractAnnotations();
+
+        $sectorArr = [];
+        foreach ($allClassAnnotations as $index => $allClassAnnotation) {
+            $sector = isset($allClassAnnotation['ApiSector']) ? $allClassAnnotation['ApiSector'][0] : $allClassAnnotation['ApiTitle'][0];
+            $sectorArr[$sector] = isset($allClassAnnotation['ApiWeigh']) ? $allClassAnnotation['ApiWeigh'][0] : 0;
+        }
+        arsort($sectorArr);
+        $routes = include_once CONF_PATH . 'route.php';
+        $subdomain = false;
+        if (config('url_domain_deploy') && isset($routes['__domain__']) && isset($routes['__domain__']['api']) && $routes['__domain__']['api']) {
+            $subdomain = true;
+        }
+        $counter = 0;
+        $section = null;
+        $weigh = 0;
+        $docslist = [];
+        foreach ($allClassMethodAnnotations as $class => $methods) {
+            foreach ($methods as $name => $docs) {
+                if (isset($docs['ApiSector'][0])) {
+                    $section = is_array($docs['ApiSector'][0]) ? $docs['ApiSector'][0]['data'] : $docs['ApiSector'][0];
+                } else {
+                    $section = $class;
+                }
+                if (0 === count($docs)) {
+                    continue;
+                }
+                $route = is_array($docs['ApiRoute'][0]) ? $docs['ApiRoute'][0]['data'] : $docs['ApiRoute'][0];
+                if ($subdomain) {
+                    $route = substr($route, 4);
+                }
+                $docslist[$section][$name] = [
+                    'id'                => $counter,
+                    'method'            => is_array($docs['ApiMethod'][0]) ? $docs['ApiMethod'][0]['data'] : $docs['ApiMethod'][0],
+                    'method_label'      => $this->generateBadgeForMethod($docs),
+                    'section'           => $section,
+                    'route'             => $route,
+                    'title'             => is_array($docs['ApiTitle'][0]) ? $docs['ApiTitle'][0]['data'] : $docs['ApiTitle'][0],
+                    'summary'           => is_array($docs['ApiSummary'][0]) ? $docs['ApiSummary'][0]['data'] : $docs['ApiSummary'][0],
+                    'body'              => isset($docs['ApiBody'][0]) ? is_array($docs['ApiBody'][0]) ? $docs['ApiBody'][0]['data'] : $docs['ApiBody'][0] : '',
+                    'headerslist'       => $this->generateHeadersTemplate($docs),
+                    'paramslist'        => $this->generateParamsTemplate($docs),
+                    'returnheaderslist' => $this->generateReturnHeadersTemplate($docs),
+                    'returnparamslist'  => $this->generateReturnParamsTemplate($docs),
+                    'weigh'             => is_array($docs['ApiWeigh'][0]) ? $docs['ApiWeigh'][0]['data'] : $docs['ApiWeigh'][0],
+                    'return'            => isset($docs['ApiReturn']) ? is_array($docs['ApiReturn'][0]) ? $docs['ApiReturn'][0]['data'] : $docs['ApiReturn'][0] : '',
+                ];
+                $counter++;
+            }
+        }
+
+        //重建排序
+        foreach ($docslist as $index => &$methods) {
+            $methodSectorArr = [];
+            foreach ($methods as $name => $method) {
+                $methodSectorArr[$name] = isset($method['weigh']) ? $method['weigh'] : 0;
+            }
+            arsort($methodSectorArr);
+            $methods = array_merge(array_flip(array_keys($methodSectorArr)), $methods);
+        }
+        $docslist = array_merge(array_flip(array_keys($sectorArr)), $docslist);
+
+        return $docslist;
+    }
+
+    public function getView()
+    {
+        return $this->view;
+    }
+
+    /**
+     * 渲染
+     * @param string $template
+     * @param array  $vars
+     * @return string
+     */
+    public function render($template, $vars = [])
+    {
+        $docslist = $this->parse();
+
+        return $this->view->display(file_get_contents($template), array_merge($vars, ['docslist' => $docslist]));
+    }
+}

+ 511 - 0
application/admin/command/Api/library/Extractor.php

@@ -0,0 +1,511 @@
+<?php
+
+namespace app\admin\command\Api\library;
+
+use Exception;
+
+/**
+ * Class imported from https://github.com/eriknyk/Annotations
+ * @author  Erik Amaru Ortiz https://github.com/eriknyk‎
+ *
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ * @author  Calin Rada <rada.calin@gmail.com>
+ */
+class Extractor
+{
+
+    /**
+     * Static array to store already parsed annotations
+     * @var array
+     */
+    private static $annotationCache;
+
+    private static $classAnnotationCache;
+
+    private static $classMethodAnnotationCache;
+
+    /**
+     * Indicates that annotations should has strict behavior, 'false' by default
+     * @var boolean
+     */
+    private $strict = false;
+
+    /**
+     * Stores the default namespace for Objects instance, usually used on methods like getMethodAnnotationsObjects()
+     * @var string
+     */
+    public $defaultNamespace = '';
+
+    /**
+     * Sets strict variable to true/false
+     * @param bool $value boolean value to indicate that annotations to has strict behavior
+     */
+    public function setStrict($value)
+    {
+        $this->strict = (bool)$value;
+    }
+
+    /**
+     * Sets default namespace to use in object instantiation
+     * @param string $namespace default namespace
+     */
+    public function setDefaultNamespace($namespace)
+    {
+        $this->defaultNamespace = $namespace;
+    }
+
+    /**
+     * Gets default namespace used in object instantiation
+     * @return string $namespace default namespace
+     */
+    public function getDefaultAnnotationNamespace()
+    {
+        return $this->defaultNamespace;
+    }
+
+    /**
+     * Gets all anotations with pattern @SomeAnnotation() from a given class
+     *
+     * @param  string $className class name to get annotations
+     * @return array  self::$classAnnotationCache all annotated elements
+     */
+    public static function getClassAnnotations($className)
+    {
+        if (!isset(self::$classAnnotationCache[$className])) {
+            $class = new \ReflectionClass($className);
+            self::$classAnnotationCache[$className] = self::parseAnnotations($class->getDocComment());
+        }
+
+        return self::$classAnnotationCache[$className];
+    }
+
+    /**
+     * 获取类所有方法的属性配置
+     * @param $className
+     * @return mixed
+     * @throws \ReflectionException
+     */
+    public static function getClassMethodAnnotations($className)
+    {
+        $class = new \ReflectionClass($className);
+
+        foreach ($class->getMethods() as $object) {
+            self::$classMethodAnnotationCache[$className][$object->name] = self::getMethodAnnotations($className, $object->name);
+        }
+
+        return self::$classMethodAnnotationCache[$className];
+    }
+
+    public static function getAllClassAnnotations()
+    {
+        return self::$classAnnotationCache;
+    }
+
+    public static function getAllClassMethodAnnotations()
+    {
+        return self::$classMethodAnnotationCache;
+    }
+
+    /**
+     * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
+     *
+     * @param  string $className class name
+     * @param  string $methodName method name to get annotations
+     * @return array  self::$annotationCache all annotated elements of a method given
+     */
+    public static function getMethodAnnotations($className, $methodName)
+    {
+        if (!isset(self::$annotationCache[$className . '::' . $methodName])) {
+            try {
+                $method = new \ReflectionMethod($className, $methodName);
+                $class = new \ReflectionClass($className);
+                if (!$method->isPublic() || $method->isConstructor()) {
+                    $annotations = array();
+                } else {
+                    $annotations = self::consolidateAnnotations($method, $class);
+                }
+            } catch (\ReflectionException $e) {
+                $annotations = array();
+            }
+
+            self::$annotationCache[$className . '::' . $methodName] = $annotations;
+        }
+
+        return self::$annotationCache[$className . '::' . $methodName];
+    }
+
+    /**
+     * Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
+     * and instance its abcAnnotation class
+     *
+     * @param  string $className class name
+     * @param  string $methodName method name to get annotations
+     * @return array  self::$annotationCache all annotated objects of a method given
+     */
+    public function getMethodAnnotationsObjects($className, $methodName)
+    {
+        $annotations = $this->getMethodAnnotations($className, $methodName);
+        $objects = array();
+
+        $i = 0;
+
+        foreach ($annotations as $annotationClass => $listParams) {
+            $annotationClass = ucfirst($annotationClass);
+            $class = $this->defaultNamespace . $annotationClass . 'Annotation';
+
+            // verify is the annotation class exists, depending if Annotations::strict is true
+            // if not, just skip the annotation instance creation.
+            if (!class_exists($class)) {
+                if ($this->strict) {
+                    throw new Exception(sprintf('Runtime Error: Annotation Class Not Found: %s', $class));
+                } else {
+                    // silent skip & continue
+                    continue;
+                }
+            }
+
+            if (empty($objects[$annotationClass])) {
+                $objects[$annotationClass] = new $class();
+            }
+
+            foreach ($listParams as $params) {
+                if (is_array($params)) {
+                    foreach ($params as $key => $value) {
+                        $objects[$annotationClass]->set($key, $value);
+                    }
+                } else {
+                    $objects[$annotationClass]->set($i++, $params);
+                }
+            }
+        }
+
+        return $objects;
+    }
+
+    private static function consolidateAnnotations($method, $class)
+    {
+        $dockblockClass = $class->getDocComment();
+        $docblockMethod = $method->getDocComment();
+        $methodName = $method->getName();
+
+        $methodAnnotations = self::parseAnnotations($docblockMethod);
+        $classAnnotations = self::parseAnnotations($dockblockClass);
+        if (isset($methodAnnotations['ApiInternal']) || $methodName == '_initialize' || $methodName == '_empty') {
+            return [];
+        }
+
+        $properties = $class->getDefaultProperties();
+        $noNeedLogin = isset($properties['noNeedLogin']) ? is_array($properties['noNeedLogin']) ? $properties['noNeedLogin'] : [$properties['noNeedLogin']] : [];
+        $noNeedRight = isset($properties['noNeedRight']) ? is_array($properties['noNeedRight']) ? $properties['noNeedRight'] : [$properties['noNeedRight']] : [];
+
+        preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $docblockMethod), $methodArr);
+        preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $dockblockClass), $classArr);
+
+        if (!isset($methodAnnotations['ApiMethod'])) {
+            $methodAnnotations['ApiMethod'] = ['get'];
+        }
+        if (!isset($methodAnnotations['ApiWeigh'])) {
+            $methodAnnotations['ApiWeigh'] = [0];
+        }
+        if (!isset($methodAnnotations['ApiSummary'])) {
+            $methodAnnotations['ApiSummary'] = $methodAnnotations['ApiTitle'];
+        }
+
+        if ($methodAnnotations) {
+            foreach ($classAnnotations as $name => $valueClass) {
+                if (count($valueClass) !== 1) {
+                    continue;
+                }
+
+                if ($name === 'ApiRoute') {
+                    if (isset($methodAnnotations[$name])) {
+                        $methodAnnotations[$name] = [rtrim($valueClass[0], '/') . $methodAnnotations[$name][0]];
+                    } else {
+                        $methodAnnotations[$name] = [rtrim($valueClass[0], '/') . '/' . $method->getName()];
+                    }
+                }
+
+                if ($name === 'ApiSector') {
+                    $methodAnnotations[$name] = $valueClass;
+                }
+            }
+        }
+        if (!isset($methodAnnotations['ApiRoute'])) {
+            $urlArr = [];
+            $className = $class->getName();
+
+            list($prefix, $suffix) = explode('\\' . \think\Config::get('url_controller_layer') . '\\', $className);
+            $prefixArr = explode('\\', $prefix);
+            $suffixArr = explode('\\', $suffix);
+            if ($prefixArr[0] == \think\Config::get('app_namespace')) {
+                $prefixArr[0] = '';
+            }
+            $urlArr = array_merge($urlArr, $prefixArr);
+            $urlArr[] = implode('.', array_map(function ($item) {
+                return \think\Loader::parseName($item);
+            }, $suffixArr));
+            $urlArr[] = $method->getName();
+
+            $methodAnnotations['ApiRoute'] = [implode('/', $urlArr)];
+        }
+        if (!isset($methodAnnotations['ApiSector'])) {
+            $methodAnnotations['ApiSector'] = isset($classAnnotations['ApiSector']) ? $classAnnotations['ApiSector'] : $classAnnotations['ApiTitle'];
+        }
+        if (!isset($methodAnnotations['ApiParams'])) {
+            $params = self::parseCustomAnnotations($docblockMethod, 'param');
+            foreach ($params as $k => $v) {
+                $arr = explode(' ', preg_replace("/[\s]+/", " ", $v));
+                $methodAnnotations['ApiParams'][] = [
+                    'name'        => isset($arr[1]) ? str_replace('$', '', $arr[1]) : '',
+                    'nullable'    => false,
+                    'type'        => isset($arr[0]) ? $arr[0] : 'string',
+                    'description' => isset($arr[2]) ? $arr[2] : ''
+                ];
+            }
+        }
+        $methodAnnotations['ApiPermissionLogin'] = [!in_array('*', $noNeedLogin) && !in_array($methodName, $noNeedLogin)];
+        $methodAnnotations['ApiPermissionRight'] = [!in_array('*', $noNeedRight) && !in_array($methodName, $noNeedRight)];
+        return $methodAnnotations;
+    }
+
+    /**
+     * Parse annotations
+     *
+     * @param  string $docblock
+     * @param  string $name
+     * @return array  parsed annotations params
+     */
+    private static function parseCustomAnnotations($docblock, $name = 'param')
+    {
+        $annotations = array();
+
+        $docblock = substr($docblock, 3, -2);
+        if (preg_match_all('/@' . $name . '(?:\s*(?:\(\s*)?(.*?)(?:\s*\))?)??\s*(?:\n|\*\/)/', $docblock, $matches)) {
+            foreach ($matches[1] as $k => $v) {
+                $annotations[] = $v;
+            }
+        }
+        return $annotations;
+    }
+
+    /**
+     * Parse annotations
+     *
+     * @param  string $docblock
+     * @return array  parsed annotations params
+     */
+    private static function parseAnnotations($docblock)
+    {
+        $annotations = array();
+
+        // Strip away the docblock header and footer to ease parsing of one line annotations
+        $docblock = substr($docblock, 3, -2);
+        if (preg_match_all('/@(?<name>[A-Za-z_-]+)[\s\t]*\((?<args>(?:(?!\)).)*)\)\r?/s', $docblock, $matches)) {
+            $numMatches = count($matches[0]);
+            for ($i = 0; $i < $numMatches; ++$i) {
+                $name = $matches['name'][$i];
+                $value = '';
+                // annotations has arguments
+                if (isset($matches['args'][$i])) {
+                    $argsParts = trim($matches['args'][$i]);
+                    if ($name == 'ApiReturn') {
+                        $value = $argsParts;
+                    } elseif ($matches['args'][$i] != '') {
+                        $argsParts = preg_replace("/\{(\w+)\}/", '#$1#', $argsParts);
+                        $value = self::parseArgs($argsParts);
+                        if (is_string($value)) {
+                            $value = preg_replace("/\#(\w+)\#/", '{$1}', $argsParts);
+                        }
+                    }
+                }
+
+                $annotations[$name][] = $value;
+            }
+        }
+        if (stripos($docblock, '@ApiInternal') !== false) {
+            $annotations['ApiInternal'] = [true];
+        }
+        if (!isset($annotations['ApiTitle'])) {
+            preg_match_all("/\*[\s]+(.*)(\\r\\n|\\r|\\n)/U", str_replace('/**', '', $docblock), $matchArr);
+            $title = isset($matchArr[1]) && isset($matchArr[1][0]) ? $matchArr[1][0] : '';
+            $annotations['ApiTitle'] = [$title];
+        }
+
+        return $annotations;
+    }
+
+    /**
+     * Parse individual annotation arguments
+     *
+     * @param  string $content arguments string
+     * @return array  annotated arguments
+     */
+    private static function parseArgs($content)
+    {
+        // Replace initial stars
+        $content = preg_replace('/^\s*\*/m', '', $content);
+
+        $data = array();
+        $len = strlen($content);
+        $i = 0;
+        $var = '';
+        $val = '';
+        $level = 1;
+
+        $prevDelimiter = '';
+        $nextDelimiter = '';
+        $nextToken = '';
+        $composing = false;
+        $type = 'plain';
+        $delimiter = null;
+        $quoted = false;
+        $tokens = array('"', '"', '{', '}', ',', '=');
+
+        while ($i <= $len) {
+            $prev_c = substr($content, $i - 1, 1);
+            $c = substr($content, $i++, 1);
+
+            if ($c === '"' && $prev_c !== "\\") {
+                $delimiter = $c;
+                //open delimiter
+                if (!$composing && empty($prevDelimiter) && empty($nextDelimiter)) {
+                    $prevDelimiter = $nextDelimiter = $delimiter;
+                    $val = '';
+                    $composing = true;
+                    $quoted = true;
+                } else {
+                    // close delimiter
+                    if ($c !== $nextDelimiter) {
+                        throw new Exception(sprintf(
+                            "Parse Error: enclosing error -> expected: [%s], given: [%s]",
+                            $nextDelimiter,
+                            $c
+                        ));
+                    }
+
+                    // validating syntax
+                    if ($i < $len) {
+                        if (',' !== substr($content, $i, 1) && '\\' !== $prev_c) {
+                            throw new Exception(sprintf(
+                                "Parse Error: missing comma separator near: ...%s<--",
+                                substr($content, ($i - 10), $i)
+                            ));
+                        }
+                    }
+
+                    $prevDelimiter = $nextDelimiter = '';
+                    $composing = false;
+                    $delimiter = null;
+                }
+            } elseif (!$composing && in_array($c, $tokens)) {
+                switch ($c) {
+                    case '=':
+                        $prevDelimiter = $nextDelimiter = '';
+                        $level = 2;
+                        $composing = false;
+                        $type = 'assoc';
+                        $quoted = false;
+                        break;
+                    case ',':
+                        $level = 3;
+
+                        // If composing flag is true yet,
+                        // it means that the string was not enclosed, so it is parsing error.
+                        if ($composing === true && !empty($prevDelimiter) && !empty($nextDelimiter)) {
+                            throw new Exception(sprintf(
+                                "Parse Error: enclosing error -> expected: [%s], given: [%s]",
+                                $nextDelimiter,
+                                $c
+                            ));
+                        }
+
+                        $prevDelimiter = $nextDelimiter = '';
+                        break;
+                    case '{':
+                        $subc = '';
+                        $subComposing = true;
+
+                        while ($i <= $len) {
+                            $c = substr($content, $i++, 1);
+
+                            if (isset($delimiter) && $c === $delimiter) {
+                                throw new Exception(sprintf(
+                                    "Parse Error: Composite variable is not enclosed correctly."
+                                ));
+                            }
+
+                            if ($c === '}') {
+                                $subComposing = false;
+                                break;
+                            }
+                            $subc .= $c;
+                        }
+
+                        // if the string is composing yet means that the structure of var. never was enclosed with '}'
+                        if ($subComposing) {
+                            throw new Exception(sprintf(
+                                "Parse Error: Composite variable is not enclosed correctly. near: ...%s'",
+                                $subc
+                            ));
+                        }
+
+                        $val = self::parseArgs($subc);
+                        break;
+                }
+            } else {
+                if ($level == 1) {
+                    $var .= $c;
+                } elseif ($level == 2) {
+                    $val .= $c;
+                }
+            }
+
+            if ($level === 3 || $i === $len) {
+                if ($type == 'plain' && $i === $len) {
+                    $data = self::castValue($var);
+                } else {
+                    $data[trim($var)] = self::castValue($val, !$quoted);
+                }
+
+                $level = 1;
+                $var = $val = '';
+                $composing = false;
+                $quoted = false;
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Try determinate the original type variable of a string
+     *
+     * @param  string $val string containing possibles variables that can be cast to bool or int
+     * @param  boolean $trim indicate if the value passed should be trimmed after to try cast
+     * @return mixed   returns the value converted to original type if was possible
+     */
+    private static function castValue($val, $trim = false)
+    {
+        if (is_array($val)) {
+            foreach ($val as $key => $value) {
+                $val[$key] = self::castValue($value);
+            }
+        } elseif (is_string($val)) {
+            if ($trim) {
+                $val = trim($val);
+            }
+            $val = stripslashes($val);
+            $tmp = strtolower($val);
+
+            if ($tmp === 'false' || $tmp === 'true') {
+                $val = $tmp === 'true';
+            } elseif (is_numeric($val)) {
+                return $val + 0;
+            }
+
+            unset($tmp);
+        }
+
+        return $val;
+    }
+}

+ 582 - 0
application/admin/command/Api/template/index.html

@@ -0,0 +1,582 @@
+<!DOCTYPE html>
+<html lang="{$config.language}">
+    <head>
+        <meta charset="utf-8">
+        <meta http-equiv="X-UA-Compatible" content="IE=edge">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta name="description" content="">
+        <meta name="author" content="{$config.author}">
+        <title>{$config.title}</title>
+
+        <!-- Bootstrap Core CSS -->
+        <link href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
+
+        <!-- Plugin CSS -->
+        <link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
+
+        <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
+        <!--[if lt IE 9]>
+        <script src="https://cdn.staticfile.org/html5shiv/3.7.3/html5shiv.min.js"></script>
+        <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
+        <![endif]-->
+
+        <style type="text/css">
+            body {
+                padding-top: 70px; margin-bottom: 15px;
+                -webkit-font-smoothing: antialiased;
+                -moz-osx-font-smoothing: grayscale;
+                font-family: "Roboto", "SF Pro SC", "SF Pro Display", "SF Pro Icons", "PingFang SC", BlinkMacSystemFont, -apple-system, "Segoe UI", "Microsoft Yahei", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
+                font-weight: 400;
+            }
+            h2        { font-size: 1.6em; }
+            hr        { margin-top: 10px; }
+            .tab-pane { padding-top: 10px; }
+            .mt0      { margin-top: 0px; }
+            .footer   { font-size: 12px; color: #666; }
+            .label    { display: inline-block; min-width: 65px; padding: 0.3em 0.6em 0.3em; }
+            .string   { color: green; }
+            .number   { color: darkorange; }
+            .boolean  { color: blue; }
+            .null     { color: magenta; }
+            .key      { color: red; }
+            .popover  { max-width: 400px; max-height: 400px; overflow-y: auto;}
+            .list-group.panel > .list-group-item {
+            }
+            .list-group-item:last-child {
+                border-radius:0;
+            }
+            h4.panel-title a {
+                font-weight:normal;
+                font-size:14px;
+            }
+            h4.panel-title a .text-muted {
+                font-size:12px;
+                font-weight:normal;
+                font-family: 'Verdana';
+            }
+            #sidebar {
+                width: 220px;
+                position: fixed;
+                margin-left: -240px;
+                overflow-y:auto;
+            }
+            #sidebar > .list-group {
+                margin-bottom:0;
+            }
+            #sidebar > .list-group > a{
+                text-indent:0;
+            }
+            #sidebar .child {
+                border:1px solid #ddd;
+                border-bottom:none;
+            }
+            #sidebar .child > a {
+                border:0;
+            }
+            #sidebar .list-group a.current {
+                background:#f5f5f5;
+            }
+            @media (max-width: 1620px){
+                #sidebar {
+                    margin:0;
+                }
+                #accordion {
+                    padding-left:235px;
+                }
+            }
+            @media (max-width: 768px){
+                #sidebar {
+                    display: none;
+                }
+                #accordion {
+                    padding-left:0px;
+                }
+            }
+            .label-primary {
+                background-color: #248aff;
+            }
+
+        </style>
+    </head>
+    <body>
+        <!-- Fixed navbar -->
+        <div class="navbar navbar-default navbar-fixed-top" role="navigation">
+            <div class="container">
+                <div class="navbar-header">
+                    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+                        <span class="sr-only">Toggle navigation</span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                        <span class="icon-bar"></span>
+                    </button>
+                    <a class="navbar-brand" href="https://www.fastadmin.net" target="_blank">{$config.title}</a>
+                </div>
+                <div class="navbar-collapse collapse">
+                    <form class="navbar-form navbar-right">
+                        <div class="form-group">
+                            Token:
+                        </div>
+                        <div class="form-group">
+                            <input type="text" class="form-control input-sm" data-toggle="tooltip" title="{$lang.Tokentips}" placeholder="token" id="token" />
+                        </div>
+                        <div class="form-group">
+                            Apiurl:
+                        </div>
+                        <div class="form-group">
+                            <input id="apiUrl" type="text" class="form-control input-sm" data-toggle="tooltip" title="{$lang.Apiurltips}" placeholder="https://api.mydomain.com" value="{$config.apiurl}" />
+                        </div>
+                        <div class="form-group">
+                            <button type="button" class="btn btn-success btn-sm" data-toggle="tooltip" title="{$lang.Savetips}" id="save_data">
+                                <span class="glyphicon glyphicon-floppy-disk" aria-hidden="true"></span>
+                            </button>
+                        </div>
+                    </form>
+                </div><!--/.nav-collapse -->
+            </div>
+        </div>
+
+        <div class="container">
+            <!-- menu -->
+            <div id="sidebar">
+                <div class="list-group panel">
+                    {foreach name="docslist" id="docs"}
+                    <a href="#{$key}" class="list-group-item" data-toggle="collapse" data-parent="#sidebar">{$key}  <i class="fa fa-caret-down"></i></a>
+                    <div class="child collapse" id="{$key}">
+                        {foreach name="docs" id="api" }
+                        <a href="javascript:;" data-id="{$api.id}" class="list-group-item">{$api.title}</a>
+                        {/foreach}
+                    </div>
+                    {/foreach}
+                </div>
+            </div>
+            <div class="panel-group" id="accordion">
+                {foreach name="docslist" id="docs"}
+                <h2>{$key}</h2>
+                <hr>
+                {foreach name="docs" id="api" }
+                <div class="panel panel-default">
+                    <div class="panel-heading" id="heading-{$api.id}">
+                        <h4 class="panel-title">
+                            <span class="label {$api.method_label}">{$api.method|strtoupper}</span>
+                            <a data-toggle="collapse" data-parent="#accordion{$api.id}" href="#collapseOne{$api.id}"> {$api.title} <span class="text-muted">{$api.route}</span></a>
+                        </h4>
+                    </div>
+                    <div id="collapseOne{$api.id}" class="panel-collapse collapse">
+                        <div class="panel-body">
+
+                            <!-- Nav tabs -->
+                            <ul class="nav nav-tabs" id="doctab{$api.id}">
+                                <li class="active"><a href="#info{$api.id}" data-toggle="tab">{$lang.Info}</a></li>
+                                <li><a href="#sandbox{$api.id}" data-toggle="tab">{$lang.Sandbox}</a></li>
+                                <li><a href="#sample{$api.id}" data-toggle="tab">{$lang.Sampleoutput}</a></li>
+                            </ul>
+
+                            <!-- Tab panes -->
+                            <div class="tab-content">
+
+                                <div class="tab-pane active" id="info{$api.id}">
+                                    <div class="well">
+                                        {$api.summary}
+                                    </div>
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading"><strong>{$lang.Headers}</strong></div>
+                                        <div class="panel-body">
+                                            {if $api.headerslist}
+                                            <table class="table table-hover">
+                                                <thead>
+                                                    <tr>
+                                                        <th>{$lang.Name}</th>
+                                                        <th>{$lang.Type}</th>
+                                                        <th>{$lang.Required}</th>
+                                                        <th>{$lang.Description}</th>
+                                                    </tr>
+                                                </thead>
+                                                <tbody>
+                                                    {foreach name="api['headerslist']" id="header"}
+                                                    <tr>
+                                                        <td>{$header.name}</td>
+                                                        <td>{$header.type}</td>
+                                                        <td>{$header.required?'是':'否'}</td>
+                                                        <td>{$header.description}</td>
+                                                    </tr>
+                                                    {/foreach}
+                                                </tbody>
+                                            </table>
+                                            {else /}
+                                            无
+                                            {/if}
+                                        </div>
+                                    </div>
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading"><strong>{$lang.Parameters}</strong></div>
+                                        <div class="panel-body">
+                                            {if $api.paramslist}
+                                            <table class="table table-hover">
+                                                <thead>
+                                                    <tr>
+                                                        <th>{$lang.Name}</th>
+                                                        <th>{$lang.Type}</th>
+                                                        <th>{$lang.Required}</th>
+                                                        <th>{$lang.Description}</th>
+                                                    </tr>
+                                                </thead>
+                                                <tbody>
+                                                    {foreach name="api['paramslist']" id="param"}
+                                                    <tr>
+                                                        <td>{$param.name}</td>
+                                                        <td>{$param.type}</td>
+                                                        <td>{:$param.required?'是':'否'}</td>
+                                                        <td>{$param.description}</td>
+                                                    </tr>
+                                                    {/foreach}
+                                                </tbody>
+                                            </table>
+                                            {else /}
+                                            无
+                                            {/if}
+                                        </div>
+                                    </div>
+                                    <div class="panel panel-default">
+                                        <div class="panel-heading"><strong>{$lang.Body}</strong></div>
+                                        <div class="panel-body">
+                                            {$api.body|default='无'}
+                                        </div>
+                                    </div>
+                                </div><!-- #info -->
+
+                                <div class="tab-pane" id="sandbox{$api.id}">
+                                    <div class="row">
+                                        <div class="col-md-12">
+                                            {if $api.headerslist}
+                                            <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>{$lang.Headers}</strong></div>
+                                                <div class="panel-body">
+                                                    <div class="headers">
+                                                        {foreach name="api['headerslist']" id="param"}
+                                                        <div class="form-group">
+                                                            <label class="control-label" for="{$param.name}">{$param.name}</label>
+                                                            <input type="{$param.type}" class="form-control input-sm" id="{$param.name}" {if $param.required}required{/if} placeholder="{$param.description} - Ex: {$param.sample}" name="{$param.name}">
+                                                        </div>
+                                                        {/foreach}
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            {/if}
+                                            <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>{$lang.Parameters}</strong></div>
+                                                <div class="panel-body">
+                                                    <form enctype="application/x-www-form-urlencoded" role="form" action="{$api.route}" method="{$api.method}" name="form{$api.id}" id="form{$api.id}">
+                                                        {if $api.paramslist}
+                                                        {foreach name="api['paramslist']" id="param"}
+                                                        <div class="form-group">
+                                                            <label class="control-label" for="{$param.name}">{$param.name}</label>
+                                                            <input type="{$param.type}" class="form-control input-sm" id="{$param.name}" {if $param.required}required{/if} placeholder="{$param.description}{if $param.sample} - 例: {$param.sample}{/if}" name="{$param.name}">
+                                                        </div>
+                                                        {/foreach}
+                                                        {else /}
+                                                        <div class="form-group">
+                                                            无
+                                                        </div>
+                                                        {/if}
+                                                        <div class="form-group">
+                                                            <button type="submit" class="btn btn-success send" rel="{$api.id}">{$lang.Send}</button>
+                                                            <button type="reset" class="btn btn-info" rel="{$api.id}">{$lang.Reset}</button>
+                                                        </div>
+                                                    </form>
+                                                </div>
+                                            </div>
+                                            <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>{$lang.Response}</strong></div>
+                                                <div class="panel-body">
+                                                    <div class="row">
+                                                        <div class="col-md-12" style="overflow-x:auto">
+                                                            <pre id="response_headers{$api.id}"></pre>
+                                                            <pre id="response{$api.id}"></pre>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="panel panel-default">
+                                                <div class="panel-heading"><strong>{$lang.ReturnParameters}</strong></div>
+                                                <div class="panel-body">
+                                                    {if $api.returnparamslist}
+                                                    <table class="table table-hover">
+                                                        <thead>
+                                                            <tr>
+                                                                <th>{$lang.Name}</th>
+                                                                <th>{$lang.Type}</th>
+                                                                <th>{$lang.Description}</th>
+                                                            </tr>
+                                                        </thead>
+                                                        <tbody>
+                                                            {foreach name="api['returnparamslist']" id="param"}
+                                                            <tr>
+                                                                <td>{$param.name}</td>
+                                                                <td>{$param.type}</td>
+                                                                <td>{$param.description}</td>
+                                                            </tr>
+                                                            {/foreach}
+                                                        </tbody>
+                                                    </table>
+                                                    {else /}
+                                                    无
+                                                    {/if}
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div><!-- #sandbox -->
+
+                                <div class="tab-pane" id="sample{$api.id}">
+                                    <div class="row">
+                                        <div class="col-md-12">
+                                            <pre id="sample_response{$api.id}">{$api.return|default='无'}</pre>
+                                        </div>
+                                    </div>
+                                </div><!-- #sample -->
+
+                            </div><!-- .tab-content -->
+                        </div>
+                    </div>
+                </div>
+                {/foreach}
+                {/foreach}
+            </div>
+
+            <hr>
+
+            <div class="row mt0 footer">
+                <div class="col-md-6" align="left">
+                    Generated on {:date('Y-m-d H:i:s')}
+                </div>
+                <div class="col-md-6" align="right">
+                    <a href="https://www.fastadmin.net" target="_blank">FastAdmin</a>
+                </div>
+            </div>
+
+        </div> <!-- /container -->
+
+        <!-- jQuery -->
+        <script src="https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
+
+        <!-- Bootstrap Core JavaScript -->
+        <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
+
+        <script type="text/javascript">
+            function syntaxHighlight(json) {
+                if (typeof json != 'string') {
+                    json = JSON.stringify(json, undefined, 2);
+                }
+                json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+                return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
+                    var cls = 'number';
+                    if (/^"/.test(match)) {
+                        if (/:$/.test(match)) {
+                            cls = 'key';
+                        } else {
+                            cls = 'string';
+                        }
+                    } else if (/true|false/.test(match)) {
+                        cls = 'boolean';
+                    } else if (/null/.test(match)) {
+                        cls = 'null';
+                    }
+                    return '<span class="' + cls + '">' + match + '</span>';
+                });
+            }
+
+            function prepareStr(str) {
+                try {
+                    return syntaxHighlight(JSON.stringify(JSON.parse(str.replace(/'/g, '"')), null, 2));
+                } catch (e) {
+                    return str;
+                }
+            }
+            var storage = (function () {
+                var uid = new Date;
+                var storage;
+                var result;
+                try {
+                    (storage = window.localStorage).setItem(uid, uid);
+                    result = storage.getItem(uid) == uid;
+                    storage.removeItem(uid);
+                    return result && storage;
+                } catch (exception) {
+                }
+            }());
+
+            $.fn.serializeObject = function ()
+            {
+                var o = {};
+                var a = this.serializeArray();
+                $.each(a, function () {
+                    if (!this.value) {
+                        return;
+                    }
+                    if (o[this.name] !== undefined) {
+                        if (!o[this.name].push) {
+                            o[this.name] = [o[this.name]];
+                        }
+                        o[this.name].push(this.value || '');
+                    } else {
+                        o[this.name] = this.value || '';
+                    }
+                });
+                return o;
+            };
+
+            $(document).ready(function () {
+
+                if (storage) {
+                    storage.getItem('token') && $('#token').val(storage.getItem('token'));
+                    storage.getItem('apiUrl') && $('#apiUrl').val(storage.getItem('apiUrl'));
+                }
+
+                $('[data-toggle="tooltip"]').tooltip({
+                    placement: 'bottom'
+                });
+
+                $(window).on("resize", function(){
+                    $("#sidebar").css("max-height", $(window).height()-80);
+                });
+
+                $(window).trigger("resize");
+
+                $(document).on("click", "#sidebar .list-group > .list-group-item", function(){
+                    $("#sidebar .list-group > .list-group-item").removeClass("current");
+                    $(this).addClass("current");
+                });
+                $(document).on("click", "#sidebar .child a", function(){
+                    var heading = $("#heading-"+$(this).data("id"));
+                    if(!heading.next().hasClass("in")){
+                        $("a", heading).trigger("click");
+                    }
+                    $("html,body").animate({scrollTop:heading.offset().top-70});
+                });
+
+                $('code[id^=response]').hide();
+
+                $.each($('pre[id^=sample_response],pre[id^=sample_post_body]'), function () {
+                    if ($(this).html() == 'NA') {
+                        return;
+                    }
+                    var str = prepareStr($(this).html());
+                    $(this).html(str);
+                });
+
+                $("[data-toggle=popover]").popover({placement: 'right'});
+
+                $('[data-toggle=popover]').on('shown.bs.popover', function () {
+                    var $sample = $(this).parent().find(".popover-content"),
+                            str = $(this).data('content');
+                    if (typeof str == "undefined" || str === "") {
+                        return;
+                    }
+                    var str = prepareStr(str);
+                    $sample.html('<pre>' + str + '</pre>');
+                });
+
+                $('body').on('click', '#save_data', function (e) {
+                    if (storage) {
+                        storage.setItem('token', $('#token').val());
+                        storage.setItem('apiUrl', $('#apiUrl').val());
+                    } else {
+                        alert('Your browser does not support local storage');
+                    }
+                });
+
+                $('body').on('click', '.send', function (e) {
+                    e.preventDefault();
+                    var form = $(this).closest('form');
+                    //added /g to get all the matched params instead of only first
+                    var matchedParamsInRoute = $(form).attr('action').match(/[^{]+(?=\})/g);
+                    var theId = $(this).attr('rel');
+                    //keep a copy of action attribute in order to modify the copy
+                    //instead of the initial attribute
+                    var url = $(form).attr('action');
+                    var method = $(form).prop('method').toLowerCase() || 'get';
+
+                    var formData = new FormData();
+
+                    $(form).find('input').each(function (i, input) {
+                        if ($(input).attr('type').toLowerCase() == 'file') {
+                            formData.append($(input).attr('name'), $(input)[0].files[0]);
+                            method = 'post';
+                        } else {
+                            formData.append($(input).attr('name'), $(input).val())
+                        }
+                    });
+
+                    var index, key, value;
+
+                    if (matchedParamsInRoute) {
+                        var params = {};
+                        formData.forEach(function(value, key){
+                            params[key] = value;
+                        });
+                        for (index = 0; index < matchedParamsInRoute.length; ++index) {
+                            try {
+                                key = matchedParamsInRoute[index];
+                                value = params[key];
+                                if (typeof value == "undefined")
+                                    value = "";
+                                url = url.replace("\{" + key + "\}", value);
+                                formData.delete(key);
+                            } catch (err) {
+                                console.log(err);
+                            }
+                        }
+                    }
+
+                    var headers = {};
+
+                    var token = $('#token').val();
+                    if (token.length > 0) {
+                        headers['token'] = token;
+                    }
+
+                    $("#sandbox" + theId + " .headers input[type=text]").each(function () {
+                        val = $(this).val();
+                        if (val.length > 0) {
+                            headers[$(this).prop('name')] = val;
+                        }
+                    });
+
+                    $.ajax({
+                        url: $('#apiUrl').val() + url,
+                        data: method == 'get' ? $(form).serialize() : formData,
+                        type: method,
+                        dataType: 'json',
+                        contentType: false,
+                        processData: false,
+                        headers: headers,
+                        xhrFields: {
+                            withCredentials: true
+                        },
+                        success: function (data, textStatus, xhr) {
+                            if (typeof data === 'object') {
+                                var str = JSON.stringify(data, null, 2);
+                                $('#response' + theId).html(syntaxHighlight(str));
+                            } else {
+                                $('#response' + theId).html(data || '');
+                            }
+                            $('#response_headers' + theId).html('HTTP ' + xhr.status + ' ' + xhr.statusText + '<br/><br/>' + xhr.getAllResponseHeaders());
+                            $('#response' + theId).show();
+                        },
+                        error: function (xhr, textStatus, error) {
+                            try {
+                                var str = JSON.stringify($.parseJSON(xhr.responseText), null, 2);
+                            } catch (e) {
+                                var str = xhr.responseText;
+                            }
+                            $('#response_headers' + theId).html('HTTP ' + xhr.status + ' ' + xhr.statusText + '<br/><br/>' + xhr.getAllResponseHeaders());
+                            $('#response' + theId).html(syntaxHighlight(str));
+                            $('#response' + theId).show();
+                        }
+                    });
+                    return false;
+                });
+            });
+        </script>
+    </body>
+</html>

+ 1479 - 0
application/admin/command/Crud.php

@@ -0,0 +1,1479 @@
+<?php
+
+namespace app\admin\command;
+
+use fast\Form;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Db;
+use think\Exception;
+use think\exception\ErrorException;
+use think\Lang;
+use think\Loader;
+
+class Crud extends Command
+{
+    protected $stubList = [];
+
+    protected $internalKeywords = [
+        'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor'
+    ];
+    /**
+     * Selectpage搜索字段关联
+     */
+    protected $fieldSelectpageMap = [
+        'nickname' => ['user_id', 'user_ids', 'admin_id', 'admin_ids']
+    ];
+
+    /**
+     * Enum类型识别为单选框的结尾字符,默认会识别为单选下拉列表
+     */
+    protected $enumRadioSuffix = ['data', 'state', 'status'];
+
+    /**
+     * Set类型识别为复选框的结尾字符,默认会识别为多选下拉列表
+     */
+    protected $setCheckboxSuffix = ['data', 'state', 'status'];
+
+    /**
+     * Int类型识别为日期时间的结尾字符,默认会识别为日期文本框
+     */
+    protected $intDateSuffix = ['time'];
+
+    /**
+     * 开关后缀
+     */
+    protected $switchSuffix = ['switch'];
+
+    /**
+     * 富文本后缀
+     */
+    protected $editorSuffix = ['content'];
+
+    /**
+     * 城市后缀
+     */
+    protected $citySuffix = ['city'];
+
+    /**
+     * JSON后缀
+     */
+    protected $jsonSuffix = ['json'];
+
+    /**
+     * Selectpage对应的后缀
+     */
+    protected $selectpageSuffix = ['_id', '_ids'];
+
+    /**
+     * Selectpage多选对应的后缀
+     */
+    protected $selectpagesSuffix = ['_ids'];
+
+    /**
+     * 以指定字符结尾的字段格式化函数
+     */
+    protected $fieldFormatterSuffix = [
+        'status' => ['type' => ['varchar', 'enum'], 'name' => 'status'],
+        'icon'   => 'icon',
+        'flag'   => 'flag',
+        'url'    => 'url',
+        'image'  => 'image',
+        'images' => 'images',
+        'avatar' => 'image',
+        'switch' => 'toggle',
+        'time'   => ['type' => ['int', 'timestamp'], 'name' => 'datetime']
+    ];
+
+    /**
+     * 识别为图片字段
+     */
+    protected $imageField = ['image', 'images', 'avatar', 'avatars'];
+
+    /**
+     * 识别为文件字段
+     */
+    protected $fileField = ['file', 'files'];
+
+    /**
+     * 保留字段
+     */
+    protected $reservedField = ['admin_id'];
+
+    /**
+     * 排除字段
+     */
+    protected $ignoreFields = [];
+
+    /**
+     * 排序字段
+     */
+    protected $sortField = 'weigh';
+
+    /**
+     * 筛选字段
+     * @var string
+     */
+    protected $headingFilterField = 'status';
+
+    /**
+     * 添加时间字段
+     * @var string
+     */
+    protected $createTimeField = 'createtime';
+
+    /**
+     * 更新时间字段
+     * @var string
+     */
+    protected $updateTimeField = 'updatetime';
+
+    /**
+     * 软删除时间字段
+     * @var string
+     */
+    protected $deleteTimeField = 'deletetime';
+
+    /**
+     * 编辑器的Class
+     */
+    protected $editorClass = 'editor';
+
+    /**
+     * langList的key最长字节数
+     */
+    protected $fieldMaxLen = 0;
+
+    protected function configure()
+    {
+        $this
+            ->setName('crud')
+            ->addOption('table', 't', Option::VALUE_REQUIRED, 'table name without prefix', null)
+            ->addOption('controller', 'c', Option::VALUE_OPTIONAL, 'controller name', null)
+            ->addOption('model', 'm', Option::VALUE_OPTIONAL, 'model name', null)
+            ->addOption('fields', 'i', Option::VALUE_OPTIONAL, 'model visible fields', null)
+            ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override or force delete,without tips', null)
+            ->addOption('local', 'l', Option::VALUE_OPTIONAL, 'local model', 1)
+            ->addOption('relation', 'r', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation table name without prefix', null)
+            ->addOption('relationmodel', 'e', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation model name', null)
+            ->addOption('relationforeignkey', 'k', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation foreign key', null)
+            ->addOption('relationprimarykey', 'p', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation primary key', null)
+            ->addOption('relationfields', 's', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation table fields', null)
+            ->addOption('relationmode', 'o', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'relation table mode,hasone or belongsto', null)
+            ->addOption('delete', 'd', Option::VALUE_OPTIONAL, 'delete all files generated by CRUD', null)
+            ->addOption('menu', 'u', Option::VALUE_OPTIONAL, 'create menu when CRUD completed', null)
+            ->addOption('setcheckboxsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate checkbox component with suffix', null)
+            ->addOption('enumradiosuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate radio component with suffix', null)
+            ->addOption('imagefield', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate image component with suffix', null)
+            ->addOption('filefield', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate file component with suffix', null)
+            ->addOption('intdatesuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate date component with suffix', null)
+            ->addOption('switchsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate switch component with suffix', null)
+            ->addOption('citysuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate citypicker component with suffix', null)
+            ->addOption('jsonsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate fieldlist component with suffix', null)
+            ->addOption('selectpagesuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate selectpage component with suffix', null)
+            ->addOption('selectpagessuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate multiple selectpage component with suffix', null)
+            ->addOption('ignorefields', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'ignore fields', null)
+            ->addOption('sortfield', null, Option::VALUE_OPTIONAL, 'sort field', null)
+            ->addOption('headingfilterfield', null, Option::VALUE_OPTIONAL, 'heading filter field', null)
+            ->addOption('editorclass', null, Option::VALUE_OPTIONAL, 'automatically generate editor class', null)
+            ->addOption('db', null, Option::VALUE_OPTIONAL, 'database config name', 'database')
+            ->setDescription('Build CRUD controller and model from table');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $adminPath = dirname(__DIR__) . DS;
+        //数据库
+        $db = $input->getOption('db');
+        //表名
+        $table = $input->getOption('table') ?: '';
+        //自定义控制器
+        $controller = $input->getOption('controller');
+        //自定义模型
+        $model = $input->getOption('model');
+        $model = $model ? $model : $controller;
+        //验证器类
+        $validate = $model;
+        //自定义显示字段
+        $fields = $input->getOption('fields');
+        //强制覆盖
+        $force = $input->getOption('force');
+        //是否为本地model,为0时表示为全局model将会把model放在app/common/model中
+        $local = $input->getOption('local');
+        if (!$table) {
+            throw new Exception('table name can\'t empty');
+        }
+        //是否生成菜单
+        $menu = $input->getOption("menu");
+        //关联表
+        $relation = $input->getOption('relation');
+        //自定义关联表模型
+        $relationModel = $input->getOption('relationmodel');
+        //模式
+        $relationMode = $mode = $input->getOption('relationmode');
+        //外键
+        $relationForeignKey = $input->getOption('relationforeignkey');
+        //主键
+        $relationPrimaryKey = $input->getOption('relationprimarykey');
+        //关联表显示字段
+        $relationFields = $input->getOption('relationfields');
+        //复选框后缀
+        $setcheckboxsuffix = $input->getOption('setcheckboxsuffix');
+        //单选框后缀
+        $enumradiosuffix = $input->getOption('enumradiosuffix');
+        //图片后缀
+        $imagefield = $input->getOption('imagefield');
+        //文件后缀
+        $filefield = $input->getOption('filefield');
+        //日期后缀
+        $intdatesuffix = $input->getOption('intdatesuffix');
+        //开关后缀
+        $switchsuffix = $input->getOption('switchsuffix');
+        //城市后缀
+        $citysuffix = $input->getOption('citysuffix');
+        //JSON配置后缀
+        $jsonsuffix = $input->getOption('jsonsuffix');
+        //selectpage后缀
+        $selectpagesuffix = $input->getOption('selectpagesuffix');
+        //selectpage多选后缀
+        $selectpagessuffix = $input->getOption('selectpagessuffix');
+        //排除字段
+        $ignoreFields = $input->getOption('ignorefields');
+        //排序字段
+        $sortfield = $input->getOption('sortfield');
+        //顶部筛选过滤字段
+        $headingfilterfield = $input->getOption('headingfilterfield');
+        //编辑器Class
+        $editorclass = $input->getOption('editorclass');
+        if ($setcheckboxsuffix) {
+            $this->setCheckboxSuffix = $setcheckboxsuffix;
+        }
+        if ($enumradiosuffix) {
+            $this->enumRadioSuffix = $enumradiosuffix;
+        }
+        if ($imagefield) {
+            $this->imageField = $imagefield;
+        }
+        if ($filefield) {
+            $this->fileField = $filefield;
+        }
+        if ($intdatesuffix) {
+            $this->intDateSuffix = $intdatesuffix;
+        }
+        if ($switchsuffix) {
+            $this->switchSuffix = $switchsuffix;
+        }
+        if ($citysuffix) {
+            $this->citySuffix = $citysuffix;
+        }
+        if ($jsonsuffix) {
+            $this->jsonSuffix = $jsonsuffix;
+        }
+        if ($selectpagesuffix) {
+            $this->selectpageSuffix = $selectpagesuffix;
+        }
+        if ($selectpagessuffix) {
+            $this->selectpagesSuffix = $selectpagessuffix;
+        }
+        if ($ignoreFields) {
+            $this->ignoreFields = $ignoreFields;
+        }
+        if ($editorclass) {
+            $this->editorClass = $editorclass;
+        }
+        if ($sortfield) {
+            $this->sortField = $sortfield;
+        }
+        if ($headingfilterfield) {
+            $this->headingFilterField = $headingfilterfield;
+        }
+
+        $this->reservedField = array_merge($this->reservedField, [$this->createTimeField, $this->updateTimeField, $this->deleteTimeField]);
+
+        $dbconnect = Db::connect($db);
+        $dbname = Config::get($db . '.database');
+        $prefix = Config::get($db . '.prefix');
+
+        //模块
+        $moduleName = 'admin';
+        $modelModuleName = $local ? $moduleName : 'common';
+        $validateModuleName = $local ? $moduleName : 'common';
+
+        //检查主表
+        $modelName = $table = stripos($table, $prefix) === 0 ? substr($table, strlen($prefix)) : $table;
+        $modelTableType = 'table';
+        $modelTableTypeName = $modelTableName = $modelName;
+        $modelTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$modelTableName}'", [], true);
+        if (!$modelTableInfo) {
+            $modelTableType = 'name';
+            $modelTableName = $prefix . $modelName;
+            $modelTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$modelTableName}'", [], true);
+            if (!$modelTableInfo) {
+                throw new Exception("table not found");
+            }
+        }
+        $modelTableInfo = $modelTableInfo[0];
+
+        $relations = [];
+        //检查关联表
+        if ($relation) {
+            $relationArr = $relation;
+            $relations = [];
+
+            foreach ($relationArr as $index => $relationTable) {
+                $relationName = stripos($relationTable, $prefix) === 0 ? substr($relationTable, strlen($prefix)) : $relationTable;
+                $relationTableType = 'table';
+                $relationTableTypeName = $relationTableName = $relationName;
+                $relationTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$relationTableName}'", [], true);
+                if (!$relationTableInfo) {
+                    $relationTableType = 'name';
+                    $relationTableName = $prefix . $relationName;
+                    $relationTableInfo = $dbconnect->query("SHOW TABLE STATUS LIKE '{$relationTableName}'", [], true);
+                    if (!$relationTableInfo) {
+                        throw new Exception("relation table not found");
+                    }
+                }
+                $relationTableInfo = $relationTableInfo[0];
+                $relationModel = isset($relationModel[$index]) ? $relationModel[$index] : '';
+
+                list($relationNamespace, $relationName, $relationFile) = $this->getModelData($modelModuleName, $relationModel, $relationName);
+
+                $relations[] = [
+                    //关联表基础名
+                    'relationName'          => $relationName,
+                    //关联表类命名空间
+                    'relationNamespace'     => $relationNamespace,
+                    //关联模型名
+                    'relationModel'         => $relationModel,
+                    //关联文件
+                    'relationFile'          => $relationFile,
+                    //关联表名称
+                    'relationTableName'     => $relationTableName,
+                    //关联表信息
+                    'relationTableInfo'     => $relationTableInfo,
+                    //关联模型表类型(name或table)
+                    'relationTableType'     => $relationTableType,
+                    //关联模型表类型名称
+                    'relationTableTypeName' => $relationTableTypeName,
+                    //关联模式
+                    'relationFields'        => isset($relationFields[$index]) ? explode(',', $relationFields[$index]) : [],
+                    //关联模式
+                    'relationMode'          => isset($relationMode[$index]) ? $relationMode[$index] : 'belongsto',
+                    //关联表外键
+                    'relationForeignKey'    => isset($relationForeignKey[$index]) ? $relationForeignKey[$index] : Loader::parseName($relationName) . '_id',
+                    //关联表主键
+                    'relationPrimaryKey'    => isset($relationPrimaryKey[$index]) ? $relationPrimaryKey[$index] : '',
+                ];
+            }
+        }
+
+        //根据表名匹配对应的Fontawesome图标
+        $iconPath = ROOT_PATH . str_replace('/', DS, '/public/assets/libs/font-awesome/less/variables.less');
+        $iconName = is_file($iconPath) && stripos(file_get_contents($iconPath), '@fa-var-' . $table . ':') ? 'fa fa-' . $table : 'fa fa-circle-o';
+
+        //控制器
+        list($controllerNamespace, $controllerName, $controllerFile, $controllerArr) = $this->getControllerData($moduleName, $controller, $table);
+        //模型
+        list($modelNamespace, $modelName, $modelFile, $modelArr) = $this->getModelData($modelModuleName, $model, $table);
+        //验证器
+        list($validateNamespace, $validateName, $validateFile, $validateArr) = $this->getValidateData($validateModuleName, $validate, $table);
+
+        //处理基础文件名,取消所有下划线并转换为小写
+        $baseNameArr = $controllerArr;
+        $baseFileName = Loader::parseName(array_pop($baseNameArr), 0);
+        array_push($baseNameArr, $baseFileName);
+        $controllerBaseName = strtolower(implode(DS, $baseNameArr));
+        $controllerUrl = strtolower(implode('/', $baseNameArr));
+
+        //视图文件
+        $viewArr = $controllerArr;
+        $lastValue = array_pop($viewArr);
+        $viewArr[] = Loader::parseName($lastValue, 0);
+        array_unshift($viewArr, 'view');
+        $viewDir = $adminPath . strtolower(implode(DS, $viewArr)) . DS;
+
+        //最终将生成的文件路径
+        $javascriptFile = ROOT_PATH . 'public' . DS . 'assets' . DS . 'js' . DS . 'backend' . DS . $controllerBaseName . '.js';
+        $addFile = $viewDir . 'add.html';
+        $editFile = $viewDir . 'edit.html';
+        $indexFile = $viewDir . 'index.html';
+        $recyclebinFile = $viewDir . 'recyclebin.html';
+        $langFile = $adminPath . 'lang' . DS . Lang::detect() . DS . $controllerBaseName . '.php';
+
+        //是否为删除模式
+        $delete = $input->getOption('delete');
+        if ($delete) {
+            $readyFiles = [$controllerFile, $modelFile, $validateFile, $addFile, $editFile, $indexFile, $recyclebinFile, $langFile, $javascriptFile];
+            foreach ($readyFiles as $k => $v) {
+                $output->warning($v);
+            }
+            if (!$force) {
+                $output->info("Are you sure you want to delete all those files?  Type 'yes' to continue: ");
+                $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
+                if (trim($line) != 'yes') {
+                    throw new Exception("Operation is aborted!");
+                }
+            }
+            foreach ($readyFiles as $k => $v) {
+                if (file_exists($v)) {
+                    unlink($v);
+                }
+                //删除空文件夹
+                switch ($v) {
+                    case $modelFile:
+                        $this->removeEmptyBaseDir($v, $modelArr);
+                        break;
+                    case $validateFile:
+                        $this->removeEmptyBaseDir($v, $validateArr);
+                        break;
+                    case $addFile:
+                    case $editFile:
+                    case $indexFile:
+                    case $recyclebinFile:
+                        $this->removeEmptyBaseDir($v, $viewArr);
+                        break;
+                    default:
+                        $this->removeEmptyBaseDir($v, $controllerArr);
+                }
+            }
+
+            $output->info("Delete Successed");
+            return;
+        }
+
+        //非覆盖模式时如果存在控制器文件则报错
+        if (is_file($controllerFile) && !$force) {
+            throw new Exception("controller already exists!\nIf you need to rebuild again, use the parameter --force=true ");
+        }
+
+        //非覆盖模式时如果存在模型文件则报错
+        if (is_file($modelFile) && !$force) {
+            throw new Exception("model already exists!\nIf you need to rebuild again, use the parameter --force=true ");
+        }
+
+        //非覆盖模式时如果存在验证文件则报错
+        if (is_file($validateFile) && !$force) {
+            throw new Exception("validate already exists!\nIf you need to rebuild again, use the parameter --force=true ");
+        }
+
+        require $adminPath . 'common.php';
+
+        //从数据库中获取表字段信息
+        $sql = "SELECT * FROM `information_schema`.`columns` "
+            . "WHERE TABLE_SCHEMA = ? AND table_name = ? "
+            . "ORDER BY ORDINAL_POSITION";
+        //加载主表的列
+        $columnList = $dbconnect->query($sql, [$dbname, $modelTableName]);
+        $fieldArr = [];
+        foreach ($columnList as $k => $v) {
+            $fieldArr[] = $v['COLUMN_NAME'];
+        }
+
+        // 加载关联表的列
+        foreach ($relations as $index => &$relation) {
+            $relationColumnList = $dbconnect->query($sql, [$dbname, $relation['relationTableName']]);
+
+            $relationFieldList = [];
+            foreach ($relationColumnList as $k => $v) {
+                $relationFieldList[] = $v['COLUMN_NAME'];
+            }
+            if (!$relation['relationPrimaryKey']) {
+                foreach ($relationColumnList as $k => $v) {
+                    if ($v['COLUMN_KEY'] == 'PRI') {
+                        $relation['relationPrimaryKey'] = $v['COLUMN_NAME'];
+                        break;
+                    }
+                }
+            }
+            // 如果主键为空
+            if (!$relation['relationPrimaryKey']) {
+                throw new Exception('Relation Primary key not found!');
+            }
+            // 如果主键不在表字段中
+            if (!in_array($relation['relationPrimaryKey'], $relationFieldList)) {
+                throw new Exception('Relation Primary key not found in table!');
+            }
+            $relation['relationColumnList'] = $relationColumnList;
+            $relation['relationFieldList'] = $relationFieldList;
+        }
+        unset($relation);
+
+        $addList = [];
+        $editList = [];
+        $javascriptList = [];
+        $langList = [];
+        $field = 'id';
+        $order = 'id';
+        $priDefined = false;
+        $priKey = '';
+        $relationPrimaryKey = '';
+        foreach ($columnList as $k => $v) {
+            if ($v['COLUMN_KEY'] == 'PRI') {
+                $priKey = $v['COLUMN_NAME'];
+                break;
+            }
+        }
+        if (!$priKey) {
+            throw new Exception('Primary key not found!');
+        }
+
+        $order = $priKey;
+
+        //如果是关联模型
+        foreach ($relations as $index => &$relation) {
+            if ($relation['relationMode'] == 'hasone') {
+                $relationForeignKey = $relation['relationForeignKey'] ? $relation['relationForeignKey'] : $table . "_id";
+                $relationPrimaryKey = $relation['relationPrimaryKey'] ? $relation['relationPrimaryKey'] : $priKey;
+
+                if (!in_array($relationForeignKey, $relation['relationFieldList'])) {
+                    throw new Exception('relation table [' . $relation['relationTableName'] . '] must be contain field [' . $relationForeignKey . ']');
+                }
+                if (!in_array($relationPrimaryKey, $fieldArr)) {
+                    throw new Exception('table [' . $modelTableName . '] must be contain field [' . $relationPrimaryKey . ']');
+                }
+            } else {
+                $relationForeignKey = $relation['relationForeignKey'] ? $relation['relationForeignKey'] : Loader::parseName($relation['relationName']) . "_id";
+                $relationPrimaryKey = $relation['relationPrimaryKey'] ? $relation['relationPrimaryKey'] : $relation['relationPriKey'];
+                if (!in_array($relationForeignKey, $fieldArr)) {
+                    throw new Exception('table [' . $modelTableName . '] must be contain field [' . $relationForeignKey . ']');
+                }
+                if (!in_array($relationPrimaryKey, $relation['relationFieldList'])) {
+                    throw new Exception('relation table [' . $relation['relationTableName'] . '] must be contain field [' . $relationPrimaryKey . ']');
+                }
+            }
+            $relation['relationForeignKey'] = $relationForeignKey;
+            $relation['relationPrimaryKey'] = $relationPrimaryKey;
+            $relation['relationClassName'] = $modelNamespace != $relation['relationNamespace'] ? $relation['relationNamespace'] . '\\' . $relation['relationName'] : $relation['relationName'];
+        }
+        unset($relation);
+
+        try {
+            Form::setEscapeHtml(false);
+            $setAttrArr = [];
+            $getAttrArr = [];
+            $getEnumArr = [];
+            $appendAttrList = [];
+            $controllerAssignList = [];
+            $headingHtml = '{:build_heading()}';
+            $recyclebinHtml = '';
+
+            //循环所有字段,开始构造视图的HTML和JS信息
+            foreach ($columnList as $k => $v) {
+                $field = $v['COLUMN_NAME'];
+                $itemArr = [];
+                // 这里构建Enum和Set类型的列表数据
+                if (in_array($v['DATA_TYPE'], ['enum', 'set', 'tinyint'])) {
+                    if ($v['DATA_TYPE'] !== 'tinyint') {
+                        $itemArr = substr($v['COLUMN_TYPE'], strlen($v['DATA_TYPE']) + 1, -1);
+                        $itemArr = explode(',', str_replace("'", '', $itemArr));
+                    }
+                    $itemArr = $this->getItemArray($itemArr, $field, $v['COLUMN_COMMENT']);
+                    //如果类型为tinyint且有使用备注数据
+                    if ($itemArr && $v['DATA_TYPE'] == 'tinyint') {
+                        $v['DATA_TYPE'] = 'enum';
+                    }
+                }
+                // 语言列表
+                if ($v['COLUMN_COMMENT'] != '') {
+                    $langList[] = $this->getLangItem($field, $v['COLUMN_COMMENT']);
+                }
+                $inputType = '';
+                //保留字段不能修改和添加
+                if ($v['COLUMN_KEY'] != 'PRI' && !in_array($field, $this->reservedField) && !in_array($field, $this->ignoreFields)) {
+                    $inputType = $this->getFieldType($v);
+
+                    // 如果是number类型时增加一个步长
+                    $step = $inputType == 'number' && $v['NUMERIC_SCALE'] > 0 ? "0." . str_repeat(0, $v['NUMERIC_SCALE'] - 1) . "1" : 0;
+
+                    $attrArr = ['id' => "c-{$field}"];
+                    $cssClassArr = ['form-control'];
+                    $fieldName = "row[{$field}]";
+                    $defaultValue = $v['COLUMN_DEFAULT'];
+                    $editValue = "{\$row.{$field}|htmlentities}";
+                    // 如果默认值非null,则是一个必选项
+                    if ($v['IS_NULLABLE'] == 'NO') {
+                        $attrArr['data-rule'] = 'required';
+                    }
+
+                    if ($inputType == 'select') {
+                        $cssClassArr[] = 'selectpicker';
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        if ($v['DATA_TYPE'] == 'set') {
+                            $attrArr['multiple'] = '';
+                            $fieldName .= "[]";
+                        }
+                        $attrArr['name'] = $fieldName;
+
+                        $this->getEnum($getEnumArr, $controllerAssignList, $field, $itemArr, $v['DATA_TYPE'] == 'set' ? 'multiple' : 'select');
+
+                        $itemArr = $this->getLangArray($itemArr, false);
+                        //添加一个获取器
+                        $this->getAttr($getAttrArr, $field, $v['DATA_TYPE'] == 'set' ? 'multiple' : 'select');
+                        if ($v['DATA_TYPE'] == 'set') {
+                            $this->setAttr($setAttrArr, $field, $inputType);
+                        }
+                        $this->appendAttr($appendAttrList, $field);
+                        $formAddElement = $this->getReplacedStub('html/select', ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => $defaultValue]);
+                        $formEditElement = $this->getReplacedStub('html/select', ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => "\$row.{$field}"]);
+                    } elseif ($inputType == 'datetime') {
+                        $cssClassArr[] = 'datetimepicker';
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        $format = "YYYY-MM-DD HH:mm:ss";
+                        $phpFormat = "Y-m-d H:i:s";
+                        $fieldFunc = '';
+                        switch ($v['DATA_TYPE']) {
+                            case 'year':
+                                $format = "YYYY";
+                                $phpFormat = 'Y';
+                                break;
+                            case 'date':
+                                $format = "YYYY-MM-DD";
+                                $phpFormat = 'Y-m-d';
+                                break;
+                            case 'time':
+                                $format = "HH:mm:ss";
+                                $phpFormat = 'H:i:s';
+                                break;
+                            case 'timestamp':
+                                $fieldFunc = 'datetime';
+                            // no break
+                            case 'datetime':
+                                $format = "YYYY-MM-DD HH:mm:ss";
+                                $phpFormat = 'Y-m-d H:i:s';
+                                break;
+                            default:
+                                $fieldFunc = 'datetime';
+                                $this->getAttr($getAttrArr, $field, $inputType);
+                                $this->setAttr($setAttrArr, $field, $inputType);
+                                $this->appendAttr($appendAttrList, $field);
+                                break;
+                        }
+                        $defaultDateTime = "{:date('{$phpFormat}')}";
+                        $attrArr['data-date-format'] = $format;
+                        $attrArr['data-use-current'] = "true";
+                        $formAddElement = Form::text($fieldName, $defaultDateTime, $attrArr);
+                        $formEditElement = Form::text($fieldName, ($fieldFunc ? "{:\$row.{$field}?{$fieldFunc}(\$row.{$field}):''}" : "{\$row.{$field}{$fieldFunc}}"), $attrArr);
+                    } elseif ($inputType == 'checkbox' || $inputType == 'radio') {
+                        unset($attrArr['data-rule']);
+                        $fieldName = $inputType == 'checkbox' ? $fieldName .= "[]" : $fieldName;
+                        $attrArr['name'] = "row[{$fieldName}]";
+
+                        $this->getEnum($getEnumArr, $controllerAssignList, $field, $itemArr, $inputType);
+                        $itemArr = $this->getLangArray($itemArr, false);
+                        //添加一个获取器
+                        $this->getAttr($getAttrArr, $field, $inputType);
+                        if ($inputType == 'checkbox') {
+                            $this->setAttr($setAttrArr, $field, $inputType);
+                        }
+                        $this->appendAttr($appendAttrList, $field);
+                        $defaultValue = $inputType == 'radio' && !$defaultValue ? key($itemArr) : $defaultValue;
+
+                        $formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => $defaultValue]);
+                        $formEditElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => "\$row.{$field}"]);
+                    } elseif ($inputType == 'textarea') {
+                        $cssClassArr[] = $this->isMatchSuffix($field, $this->editorSuffix) ? $this->editorClass : '';
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        $attrArr['rows'] = 5;
+                        $formAddElement = Form::textarea($fieldName, $defaultValue, $attrArr);
+                        $formEditElement = Form::textarea($fieldName, $editValue, $attrArr);
+                    } elseif ($inputType == 'switch') {
+                        unset($attrArr['data-rule']);
+                        if ($defaultValue === '1' || $defaultValue === 'Y') {
+                            $yes = $defaultValue;
+                            $no = $defaultValue === '1' ? '0' : 'N';
+                        } else {
+                            $no = $defaultValue;
+                            $yes = $defaultValue === '0' ? '1' : 'Y';
+                        }
+                        if (!$itemArr) {
+                            $itemArr = [$yes => 'Yes', $no => 'No'];
+                        }
+                        $stateNoClass = 'fa-flip-horizontal text-gray';
+                        $formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldYes' => $yes, 'fieldNo' => $no, 'attrStr' => Form::attributes($attrArr), 'fieldValue' => $defaultValue, 'fieldSwitchClass' => $defaultValue == $no ? $stateNoClass : '']);
+                        $formEditElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldYes' => $yes, 'fieldNo' => $no, 'attrStr' => Form::attributes($attrArr), 'fieldValue' => "{\$row.{$field}}", 'fieldSwitchClass' => "{eq name=\"\$row.{$field}\" value=\"{$no}\"}fa-flip-horizontal text-gray{/eq}"]);
+                    } elseif ($inputType == 'citypicker') {
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        $attrArr['data-toggle'] = "city-picker";
+                        $formAddElement = sprintf("<div class='control-relative'>%s</div>", Form::input('text', $fieldName, $defaultValue, $attrArr));
+                        $formEditElement = sprintf("<div class='control-relative'>%s</div>", Form::input('text', $fieldName, $editValue, $attrArr));
+                    } elseif ($inputType == 'fieldlist') {
+                        $itemArr = $this->getItemArray($itemArr, $field, $v['COLUMN_COMMENT']);
+                        $itemKey = isset($itemArr['key']) ? ucfirst($itemArr['key']) : 'Key';
+                        $itemValue = isset($itemArr['value']) ? ucfirst($itemArr['value']) : 'Value';
+                        $formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'itemKey' => $itemKey, 'itemValue' => $itemValue, 'fieldValue' => $defaultValue]);
+                        $formEditElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'itemKey' => $itemKey, 'itemValue' => $itemValue, 'fieldValue' => $editValue]);
+                    } else {
+                        $search = $replace = '';
+                        //特殊字段为关联搜索
+                        if ($this->isMatchSuffix($field, $this->selectpageSuffix)) {
+                            $inputType = 'text';
+                            $defaultValue = '';
+                            $attrArr['data-rule'] = 'required';
+                            $cssClassArr[] = 'selectpage';
+                            $selectpageController = str_replace('_', '/', substr($field, 0, strripos($field, '_')));
+                            $attrArr['data-source'] = $selectpageController . "/index";
+                            //如果是类型表需要特殊处理下
+                            if ($selectpageController == 'category') {
+                                $attrArr['data-source'] = 'category/selectpage';
+                                $attrArr['data-params'] = '##replacetext##';
+                                $search = '"##replacetext##"';
+                                $replace = '\'{"custom[type]":"' . $table . '"}\'';
+                            } elseif ($selectpageController == 'admin') {
+                                $attrArr['data-source'] = 'auth/admin/selectpage';
+                            } elseif ($selectpageController == 'user') {
+                                $attrArr['data-source'] = 'user/user/index';
+                            }
+                            if ($this->isMatchSuffix($field, $this->selectpagesSuffix)) {
+                                $attrArr['data-multiple'] = 'true';
+                            }
+                            foreach ($this->fieldSelectpageMap as $m => $n) {
+                                if (in_array($field, $n)) {
+                                    $attrArr['data-field'] = $m;
+                                    break;
+                                }
+                            }
+                        }
+                        //因为有自动完成可输入其它内容
+                        $step = array_intersect($cssClassArr, ['selectpage']) ? 0 : $step;
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        $isUpload = false;
+                        if ($this->isMatchSuffix($field, array_merge($this->imageField, $this->fileField))) {
+                            $isUpload = true;
+                        }
+                        //如果是步长则加上步长
+                        if ($step) {
+                            $attrArr['step'] = $step;
+                        }
+                        //如果是图片加上个size
+                        if ($isUpload) {
+                            $attrArr['size'] = 50;
+                        }
+
+                        $formAddElement = Form::input($inputType, $fieldName, $defaultValue, $attrArr);
+                        $formEditElement = Form::input($inputType, $fieldName, $editValue, $attrArr);
+                        if ($search && $replace) {
+                            $formAddElement = str_replace($search, $replace, $formAddElement);
+                            $formEditElement = str_replace($search, $replace, $formEditElement);
+                        }
+                        //如果是图片或文件
+                        if ($isUpload) {
+                            $formAddElement = $this->getImageUpload($field, $formAddElement);
+                            $formEditElement = $this->getImageUpload($field, $formEditElement);
+                        }
+                    }
+                    //构造添加和编辑HTML信息
+                    $addList[] = $this->getFormGroup($field, $formAddElement);
+                    $editList[] = $this->getFormGroup($field, $formEditElement);
+                }
+
+                //过滤text类型字段
+                if ($v['DATA_TYPE'] != 'text' && $inputType != 'fieldlist') {
+                    //主键
+                    if ($v['COLUMN_KEY'] == 'PRI' && !$priDefined) {
+                        $priDefined = true;
+                        $javascriptList[] = "{checkbox: true}";
+                    }
+                    if ($this->deleteTimeField == $field) {
+                        $recyclebinHtml = $this->getReplacedStub('html/recyclebin-html', ['controllerUrl' => $controllerUrl]);
+                        continue;
+                    }
+                    if (!$fields || in_array($field, explode(',', $fields))) {
+                        //构造JS列信息
+                        $javascriptList[] = $this->getJsColumn($field, $v['DATA_TYPE'], $inputType && in_array($inputType, ['select', 'checkbox', 'radio']) ? '_text' : '', $itemArr);
+                    }
+                    if ($this->headingFilterField && $this->headingFilterField == $field && $itemArr) {
+                        $headingHtml = $this->getReplacedStub('html/heading-html', ['field' => $field, 'fieldName' => Loader::parseName($field, 1, false)]);
+                    }
+                    //排序方式,如果有指定排序字段,否则按主键排序
+                    $order = $field == $this->sortField ? $this->sortField : $order;
+                }
+            }
+
+            //循环关联表,追加语言包和JS列
+            foreach ($relations as $index => $relation) {
+                foreach ($relation['relationColumnList'] as $k => $v) {
+                    // 不显示的字段直接过滤掉
+                    if ($relation['relationFields'] && !in_array($v['COLUMN_NAME'], $relation['relationFields'])) {
+                        continue;
+                    }
+
+                    $relationField = strtolower($relation['relationName']) . "." . $v['COLUMN_NAME'];
+                    // 语言列表
+                    if ($v['COLUMN_COMMENT'] != '') {
+                        $langList[] = $this->getLangItem($relationField, $v['COLUMN_COMMENT']);
+                    }
+
+                    //过滤text类型字段
+                    if ($v['DATA_TYPE'] != 'text') {
+                        //构造JS列信息
+                        $javascriptList[] = $this->getJsColumn($relationField, $v['DATA_TYPE']);
+                    }
+                }
+            }
+
+            //JS最后一列加上操作列
+            $javascriptList[] = str_repeat(" ", 24) . "{field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}";
+            $addList = implode("\n", array_filter($addList));
+            $editList = implode("\n", array_filter($editList));
+            $javascriptList = implode(",\n", array_filter($javascriptList));
+            $langList = implode(",\n", array_filter($langList));
+            //数组等号对齐
+            $langList = array_filter(explode(",\n", $langList . ",\n"));
+            foreach ($langList as &$line) {
+                if (preg_match("/^\s+'([^']+)'\s*=>\s*'([^']+)'\s*/is", $line, $matches)) {
+                    $line = "    '{$matches[1]}'" . str_pad('=>', ($this->fieldMaxLen - strlen($matches[1]) + 3), ' ', STR_PAD_LEFT) . " '{$matches[2]}'";
+                }
+            }
+            unset($line);
+            $langList = implode(",\n", array_filter($langList));
+
+            //表注释
+            $tableComment = $modelTableInfo['Comment'];
+            $tableComment = mb_substr($tableComment, -1) == '表' ? mb_substr($tableComment, 0, -1) . '管理' : $tableComment;
+
+            $modelInit = '';
+            if ($priKey != $order) {
+                $modelInit = $this->getReplacedStub('mixins' . DS . 'modelinit', ['order' => $order]);
+            }
+
+            $data = [
+                'modelConnection'         => $db == 'database' ? '' : "protected \$connection = '{$db}';",
+                'controllerNamespace'     => $controllerNamespace,
+                'modelNamespace'          => $modelNamespace,
+                'validateNamespace'       => $validateNamespace,
+                'controllerUrl'           => $controllerUrl,
+                'controllerName'          => $controllerName,
+                'controllerAssignList'    => implode("\n", $controllerAssignList),
+                'modelName'               => $modelName,
+                'modelTableName'          => $modelTableName,
+                'modelTableType'          => $modelTableType,
+                'modelTableTypeName'      => $modelTableTypeName,
+                'validateName'            => $validateName,
+                'tableComment'            => $tableComment,
+                'iconName'                => $iconName,
+                'pk'                      => $priKey,
+                'order'                   => $order,
+                'table'                   => $table,
+                'tableName'               => $modelTableName,
+                'addList'                 => $addList,
+                'editList'                => $editList,
+                'javascriptList'          => $javascriptList,
+                'langList'                => $langList,
+                'sofeDeleteClassPath'     => in_array($this->deleteTimeField, $fieldArr) ? "use traits\model\SoftDelete;" : '',
+                'softDelete'              => in_array($this->deleteTimeField, $fieldArr) ? "use SoftDelete;" : '',
+                'modelAutoWriteTimestamp' => in_array($this->createTimeField, $fieldArr) || in_array($this->updateTimeField, $fieldArr) ? "'int'" : 'false',
+                'createTime'              => in_array($this->createTimeField, $fieldArr) ? "'{$this->createTimeField}'" : 'false',
+                'updateTime'              => in_array($this->updateTimeField, $fieldArr) ? "'{$this->updateTimeField}'" : 'false',
+                'deleteTime'              => in_array($this->deleteTimeField, $fieldArr) ? "'{$this->deleteTimeField}'" : 'false',
+                'relationSearch'          => $relations ? 'true' : 'false',
+                'relationWithList'        => '',
+                'relationMethodList'      => '',
+                'controllerIndex'         => '',
+                'recyclebinJs'            => '',
+                'headingHtml'             => $headingHtml,
+                'recyclebinHtml'          => $recyclebinHtml,
+                'visibleFieldList'        => $fields ? "\$row->visible(['" . implode("','", array_filter(explode(',', $fields))) . "']);" : '',
+                'appendAttrList'          => implode(",\n", $appendAttrList),
+                'getEnumList'             => implode("\n\n", $getEnumArr),
+                'getAttrList'             => implode("\n\n", $getAttrArr),
+                'setAttrList'             => implode("\n\n", $setAttrArr),
+                'modelInit'               => $modelInit,
+            ];
+
+            //如果使用关联模型
+            if ($relations) {
+                $relationWithList = $relationMethodList = $relationVisibleFieldList = [];
+                foreach ($relations as $index => $relation) {
+                    //需要构造关联的方法
+                    $relation['relationMethod'] = strtolower($relation['relationName']);
+
+                    //关联的模式
+                    $relation['relationMode'] = $relation['relationMode'] == 'hasone' ? 'hasOne' : 'belongsTo';
+
+                    //关联字段
+                    $relation['relationPrimaryKey'] = $relation['relationPrimaryKey'] ? $relation['relationPrimaryKey'] : $priKey;
+
+                    //预载入的方法
+                    $relationWithList[] = $relation['relationMethod'];
+
+                    unset($relation['relationColumnList'], $relation['relationFieldList'], $relation['relationTableInfo']);
+
+                    //构造关联模型的方法
+                    $relationMethodList[] = $this->getReplacedStub('mixins' . DS . 'modelrelationmethod', $relation);
+
+                    //如果设置了显示主表字段,则必须显式将关联表字段显示
+                    if ($fields) {
+                        $relationVisibleFieldList[] = "\$row->visible(['{$relation['relationMethod']}']);";
+                    }
+
+                    //显示的字段
+                    if ($relation['relationFields']) {
+                        $relationVisibleFieldList[] = "\$row->getRelation('" . $relation['relationMethod'] . "')->visible(['" . implode("','", $relation['relationFields']) . "']);";
+                    }
+                }
+
+                $data['relationWithList'] = "->with(['" . implode("','", $relationWithList) . "'])";
+                $data['relationMethodList'] = implode("\n\n", $relationMethodList);
+                $data['relationVisibleFieldList'] = implode("\n\t\t\t\t", $relationVisibleFieldList);
+
+                //需要重写index方法
+                $data['controllerIndex'] = $this->getReplacedStub('controllerindex', $data);
+            } elseif ($fields) {
+                $data = array_merge($data, ['relationWithList' => '', 'relationMethodList' => '', 'relationVisibleFieldList' => '']);
+                //需要重写index方法
+                $data['controllerIndex'] = $this->getReplacedStub('controllerindex', $data);
+            }
+
+            // 生成控制器文件
+            $this->writeToFile('controller', $data, $controllerFile);
+            // 生成模型文件
+            $this->writeToFile('model', $data, $modelFile);
+
+            if ($relations) {
+                foreach ($relations as $i => $relation) {
+                    $relation['modelNamespace'] = $data['modelNamespace'];
+                    if (!is_file($relation['relationFile'])) {
+                        // 生成关联模型文件
+                        $this->writeToFile('relationmodel', $relation, $relation['relationFile']);
+                    }
+                }
+            }
+            // 生成验证文件
+            $this->writeToFile('validate', $data, $validateFile);
+            // 生成视图文件
+            $this->writeToFile('add', $data, $addFile);
+            $this->writeToFile('edit', $data, $editFile);
+            $this->writeToFile('index', $data, $indexFile);
+            if ($recyclebinHtml) {
+                $this->writeToFile('recyclebin', $data, $recyclebinFile);
+                $recyclebinTitle = in_array('title', $fieldArr) ? 'title' : (in_array('name', $fieldArr) ? 'name' : '');
+                $recyclebinTitleJs = $recyclebinTitle ? "\n                        {field: '{$recyclebinTitle}', title: __('" . (ucfirst($recyclebinTitle)) . "'), align: 'left'}," : '';
+                $data['recyclebinJs'] = $this->getReplacedStub('mixins/recyclebinjs', ['recyclebinTitleJs' => $recyclebinTitleJs, 'controllerUrl' => $controllerUrl]);
+            }
+            // 生成JS文件
+            $this->writeToFile('javascript', $data, $javascriptFile);
+            // 生成语言文件
+            $this->writeToFile('lang', $data, $langFile);
+        } catch (ErrorException $e) {
+            throw new Exception("Code: " . $e->getCode() . "\nLine: " . $e->getLine() . "\nMessage: " . $e->getMessage() . "\nFile: " . $e->getFile());
+        }
+
+        //继续生成菜单
+        if ($menu) {
+            exec("php think menu -c {$controllerUrl}");
+        }
+
+        $output->info("Build Successed");
+    }
+
+    protected function getEnum(&$getEnum, &$controllerAssignList, $field, $itemArr = '', $inputType = '')
+    {
+        if (!in_array($inputType, ['datetime', 'select', 'multiple', 'checkbox', 'radio'])) {
+            return;
+        }
+        $fieldList = $this->getFieldListName($field);
+        $methodName = 'get' . ucfirst($fieldList);
+        foreach ($itemArr as $k => &$v) {
+            $v = "__('" . mb_ucfirst($v) . "')";
+        }
+        unset($v);
+        $itemString = $this->getArrayString($itemArr);
+        $getEnum[] = <<<EOD
+    public function {$methodName}()
+    {
+        return [{$itemString}];
+    }
+EOD;
+        $controllerAssignList[] = <<<EOD
+        \$this->view->assign("{$fieldList}", \$this->model->{$methodName}());
+EOD;
+    }
+
+    protected function getAttr(&$getAttr, $field, $inputType = '')
+    {
+        if (!in_array($inputType, ['datetime', 'select', 'multiple', 'checkbox', 'radio'])) {
+            return;
+        }
+        $attrField = ucfirst($this->getCamelizeName($field));
+        $getAttr[] = $this->getReplacedStub("mixins" . DS . $inputType, ['field' => $field, 'methodName' => "get{$attrField}TextAttr", 'listMethodName' => "get{$attrField}List"]);
+    }
+
+    protected function setAttr(&$setAttr, $field, $inputType = '')
+    {
+        if (!in_array($inputType, ['datetime', 'checkbox', 'select'])) {
+            return;
+        }
+        $attrField = ucfirst($this->getCamelizeName($field));
+        if ($inputType == 'datetime') {
+            $return = <<<EOD
+return \$value === '' ? null : (\$value && !is_numeric(\$value) ? strtotime(\$value) : \$value);
+EOD;
+        } elseif (in_array($inputType, ['checkbox', 'select'])) {
+            $return = <<<EOD
+return is_array(\$value) ? implode(',', \$value) : \$value;
+EOD;
+        }
+        $setAttr[] = <<<EOD
+    protected function set{$attrField}Attr(\$value)
+    {
+        $return
+    }
+EOD;
+    }
+
+    protected function appendAttr(&$appendAttrList, $field)
+    {
+        $appendAttrList[] = <<<EOD
+        '{$field}_text'
+EOD;
+    }
+
+    /**
+     * 移除相对的空目录
+     * @param $parseFile
+     * @param $parseArr
+     * @return bool
+     */
+    protected function removeEmptyBaseDir($parseFile, $parseArr)
+    {
+        if (count($parseArr) > 1) {
+            $parentDir = dirname($parseFile);
+            for ($i = 0; $i < count($parseArr); $i++) {
+                try {
+                    $iterator = new \FilesystemIterator($parentDir);
+                    $isDirEmpty = !$iterator->valid();
+                    if ($isDirEmpty) {
+                        rmdir($parentDir);
+                        $parentDir = dirname($parentDir);
+                    } else {
+                        return true;
+                    }
+                } catch (\UnexpectedValueException $e) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 获取控制器相关信息
+     * @param $module
+     * @param $controller
+     * @param $table
+     * @return array
+     */
+    protected function getControllerData($module, $controller, $table)
+    {
+        return $this->getParseNameData($module, $controller, $table, 'controller');
+    }
+
+    /**
+     * 获取模型相关信息
+     * @param $module
+     * @param $model
+     * @param $table
+     * @return array
+     */
+    protected function getModelData($module, $model, $table)
+    {
+        return $this->getParseNameData($module, $model, $table, 'model');
+    }
+
+    /**
+     * 获取验证器相关信息
+     * @param $module
+     * @param $validate
+     * @param $table
+     * @return array
+     */
+    protected function getValidateData($module, $validate, $table)
+    {
+        return $this->getParseNameData($module, $validate, $table, 'validate');
+    }
+
+    /**
+     * 获取已解析相关信息
+     * @param string $module 模块名称
+     * @param string $name   自定义名称
+     * @param string $table  数据表名
+     * @param string $type   解析类型,本例中为controller、model、validate
+     * @return array
+     */
+    protected function getParseNameData($module, $name, $table, $type)
+    {
+        $arr = [];
+        if (!$name) {
+            $parseName = Loader::parseName($table, 1);
+            $parseArr = [$table];
+        } else {
+            $name = str_replace(['.', '/', '\\'], '/', $name);
+            $arr = explode('/', $name);
+            $parseName = ucfirst(array_pop($arr));
+            $parseArr = $arr;
+            array_push($parseArr, $parseName);
+        }
+        //类名不能为内部关键字
+        if (in_array(strtolower($parseName), $this->internalKeywords)) {
+            throw new Exception('Unable to use internal variable:' . $parseName);
+        }
+        $appNamespace = Config::get('app_namespace');
+        $parseNamespace = "{$appNamespace}\\{$module}\\{$type}" . ($arr ? "\\" . implode("\\", $arr) : "");
+        $moduleDir = APP_PATH . $module . DS;
+        $parseFile = $moduleDir . $type . DS . ($arr ? implode(DS, $arr) . DS : '') . $parseName . '.php';
+        return [$parseNamespace, $parseName, $parseFile, $parseArr];
+    }
+
+    /**
+     * 写入到文件
+     * @param string $name
+     * @param array  $data
+     * @param string $pathname
+     * @return mixed
+     */
+    protected function writeToFile($name, $data, $pathname)
+    {
+        foreach ($data as $index => &$datum) {
+            $datum = is_array($datum) ? '' : $datum;
+        }
+        unset($datum);
+        $content = $this->getReplacedStub($name, $data);
+
+        if (!is_dir(dirname($pathname))) {
+            mkdir(dirname($pathname), 0755, true);
+        }
+        return file_put_contents($pathname, $content);
+    }
+
+    /**
+     * 获取替换后的数据
+     * @param string $name
+     * @param array  $data
+     * @return string
+     */
+    protected function getReplacedStub($name, $data)
+    {
+        foreach ($data as $index => &$datum) {
+            $datum = is_array($datum) ? '' : $datum;
+        }
+        unset($datum);
+        $search = $replace = [];
+        foreach ($data as $k => $v) {
+            $search[] = "{%{$k}%}";
+            $replace[] = $v;
+        }
+        $stubname = $this->getStub($name);
+        if (isset($this->stubList[$stubname])) {
+            $stub = $this->stubList[$stubname];
+        } else {
+            $this->stubList[$stubname] = $stub = file_get_contents($stubname);
+        }
+        $content = str_replace($search, $replace, $stub);
+        return $content;
+    }
+
+    /**
+     * 获取基础模板
+     * @param string $name
+     * @return string
+     */
+    protected function getStub($name)
+    {
+        return __DIR__ . DS . 'Crud' . DS . 'stubs' . DS . $name . '.stub';
+    }
+
+    protected function getLangItem($field, $content)
+    {
+        if ($content || !Lang::has($field)) {
+            $this->fieldMaxLen = strlen($field) > $this->fieldMaxLen ? strlen($field) : $this->fieldMaxLen;
+            $content = str_replace(',', ',', $content);
+            if (stripos($content, ':') !== false && stripos($content, ',') && stripos($content, '=') !== false) {
+                list($fieldLang, $item) = explode(':', $content);
+                $itemArr = [$field => $fieldLang];
+                foreach (explode(',', $item) as $k => $v) {
+                    $valArr = explode('=', $v);
+                    if (count($valArr) == 2) {
+                        list($key, $value) = $valArr;
+                        $itemArr[$field . ' ' . $key] = $value;
+                        $this->fieldMaxLen = strlen($field . ' ' . $key) > $this->fieldMaxLen ? strlen($field . ' ' . $key) : $this->fieldMaxLen;
+                    }
+                }
+            } else {
+                $itemArr = [$field => $content];
+            }
+            $resultArr = [];
+            foreach ($itemArr as $k => $v) {
+                $resultArr[] = "    '" . mb_ucfirst($k) . "' => '{$v}'";
+            }
+            return implode(",\n", $resultArr);
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * 读取数据和语言数组列表
+     * @param array   $arr
+     * @param boolean $withTpl
+     * @return array
+     */
+    protected function getLangArray($arr, $withTpl = true)
+    {
+        $langArr = [];
+        foreach ($arr as $k => $v) {
+            $langArr[$k] = is_numeric($k) ? ($withTpl ? "{:" : "") . "__('" . mb_ucfirst($v) . "')" . ($withTpl ? "}" : "") : $v;
+        }
+        return $langArr;
+    }
+
+    /**
+     * 将数据转换成带字符串
+     * @param array $arr
+     * @return string
+     */
+    protected function getArrayString($arr)
+    {
+        if (!is_array($arr)) {
+            return $arr;
+        }
+        $stringArr = [];
+        foreach ($arr as $k => $v) {
+            $is_var = in_array(substr($v, 0, 1), ['$', '_']);
+            if (!$is_var) {
+                $v = str_replace("'", "\'", $v);
+                $k = str_replace("'", "\'", $k);
+            }
+            $stringArr[] = "'" . $k . "' => " . ($is_var ? $v : "'{$v}'");
+        }
+        return implode(", ", $stringArr);
+    }
+
+    protected function getItemArray($item, $field, $comment)
+    {
+        $itemArr = [];
+        $comment = str_replace(',', ',', $comment);
+        if (stripos($comment, ':') !== false && stripos($comment, ',') && stripos($comment, '=') !== false) {
+            list($fieldLang, $item) = explode(':', $comment);
+            $itemArr = [];
+            foreach (explode(',', $item) as $k => $v) {
+                $valArr = explode('=', $v);
+                if (count($valArr) == 2) {
+                    list($key, $value) = $valArr;
+                    $itemArr[$key] = $field . ' ' . $key;
+                }
+            }
+        } else {
+            foreach ($item as $k => $v) {
+                $itemArr[$v] = is_numeric($v) ? $field . ' ' . $v : $v;
+            }
+        }
+        return $itemArr;
+    }
+
+    protected function getFieldType(& $v)
+    {
+        $inputType = 'text';
+        switch ($v['DATA_TYPE']) {
+            case 'bigint':
+            case 'int':
+            case 'mediumint':
+            case 'smallint':
+            case 'tinyint':
+                $inputType = 'number';
+                break;
+            case 'enum':
+            case 'set':
+                $inputType = 'select';
+                break;
+            case 'decimal':
+            case 'double':
+            case 'float':
+                $inputType = 'number';
+                break;
+            case 'longtext':
+            case 'text':
+            case 'mediumtext':
+            case 'smalltext':
+            case 'tinytext':
+                $inputType = 'textarea';
+                break;
+            case 'year':
+            case 'date':
+            case 'time':
+            case 'datetime':
+            case 'timestamp':
+                $inputType = 'datetime';
+                break;
+            default:
+                break;
+        }
+        $fieldsName = $v['COLUMN_NAME'];
+        // 指定后缀说明也是个时间字段
+        if ($this->isMatchSuffix($fieldsName, $this->intDateSuffix)) {
+            $inputType = 'datetime';
+        }
+        // 指定后缀结尾且类型为enum,说明是个单选框
+        if ($this->isMatchSuffix($fieldsName, $this->enumRadioSuffix) && $v['DATA_TYPE'] == 'enum') {
+            $inputType = "radio";
+        }
+        // 指定后缀结尾且类型为set,说明是个复选框
+        if ($this->isMatchSuffix($fieldsName, $this->setCheckboxSuffix) && $v['DATA_TYPE'] == 'set') {
+            $inputType = "checkbox";
+        }
+        // 指定后缀结尾且类型为char或tinyint且长度为1,说明是个Switch复选框
+        if ($this->isMatchSuffix($fieldsName, $this->switchSuffix) && ($v['COLUMN_TYPE'] == 'tinyint(1)' || $v['COLUMN_TYPE'] == 'char(1)') && $v['COLUMN_DEFAULT'] !== '' && $v['COLUMN_DEFAULT'] !== null) {
+            $inputType = "switch";
+        }
+        // 指定后缀结尾城市选择框
+        if ($this->isMatchSuffix($fieldsName, $this->citySuffix) && ($v['DATA_TYPE'] == 'varchar' || $v['DATA_TYPE'] == 'char')) {
+            $inputType = "citypicker";
+        }
+        // 指定后缀结尾JSON配置
+        if ($this->isMatchSuffix($fieldsName, $this->jsonSuffix) && ($v['DATA_TYPE'] == 'varchar' || $v['DATA_TYPE'] == 'text')) {
+            $inputType = "fieldlist";
+        }
+        return $inputType;
+    }
+
+    /**
+     * 判断是否符合指定后缀
+     * @param string $field     字段名称
+     * @param mixed  $suffixArr 后缀
+     * @return boolean
+     */
+    protected function isMatchSuffix($field, $suffixArr)
+    {
+        $suffixArr = is_array($suffixArr) ? $suffixArr : explode(',', $suffixArr);
+        foreach ($suffixArr as $k => $v) {
+            if (preg_match("/{$v}$/i", $field)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 获取表单分组数据
+     * @param string $field
+     * @param string $content
+     * @return string
+     */
+    protected function getFormGroup($field, $content)
+    {
+        $langField = mb_ucfirst($field);
+        return <<<EOD
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('{$langField}')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            {$content}
+        </div>
+    </div>
+EOD;
+    }
+
+    /**
+     * 获取图片模板数据
+     * @param string $field
+     * @param string $content
+     * @return string
+     */
+    protected function getImageUpload($field, $content)
+    {
+        $uploadfilter = $selectfilter = '';
+        if ($this->isMatchSuffix($field, $this->imageField)) {
+            $uploadfilter = ' data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp"';
+            $selectfilter = ' data-mimetype="image/*"';
+        }
+        $multiple = substr($field, -1) == 's' ? ' data-multiple="true"' : ' data-multiple="false"';
+        $preview = ' data-preview-id="p-' . $field . '"';
+        $previewcontainer = $preview ? '<ul class="row list-inline plupload-preview" id="p-' . $field . '"></ul>' : '';
+        return <<<EOD
+<div class="input-group">
+                {$content}
+                <div class="input-group-addon no-border no-padding">
+                    <span><button type="button" id="plupload-{$field}" class="btn btn-danger plupload" data-input-id="c-{$field}"{$uploadfilter}{$multiple}{$preview}><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                    <span><button type="button" id="fachoose-{$field}" class="btn btn-primary fachoose" data-input-id="c-{$field}"{$selectfilter}{$multiple}><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                </div>
+                <span class="msg-box n-right" for="c-{$field}"></span>
+            </div>
+            {$previewcontainer}
+EOD;
+    }
+
+    /**
+     * 获取JS列数据
+     * @param string $field
+     * @param string $datatype
+     * @param string $extend
+     * @param array  $itemArr
+     * @return string
+     */
+    protected function getJsColumn($field, $datatype = '', $extend = '', $itemArr = [])
+    {
+        $lang = mb_ucfirst($field);
+        $formatter = '';
+        foreach ($this->fieldFormatterSuffix as $k => $v) {
+            if (preg_match("/{$k}$/i", $field)) {
+                if (is_array($v)) {
+                    if (in_array($datatype, $v['type'])) {
+                        $formatter = $v['name'];
+                        break;
+                    }
+                } else {
+                    $formatter = $v;
+                    break;
+                }
+            }
+        }
+        $html = str_repeat(" ", 24) . "{field: '{$field}', title: __('{$lang}')";
+
+        if ($datatype == 'set') {
+            $formatter = 'label';
+        }
+        foreach ($itemArr as $k => &$v) {
+            if (substr($v, 0, 3) !== '__(') {
+                $v = "__('" . mb_ucfirst($v) . "')";
+            }
+        }
+        unset($v);
+        $searchList = json_encode($itemArr, JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE);
+        $searchList = str_replace(['":"', '"}', ')","'], ['":', '}', '),"'], $searchList);
+        if ($itemArr) {
+            $html .= ", searchList: " . $searchList;
+        }
+        if (in_array($datatype, ['date', 'datetime']) || $formatter === 'datetime') {
+            $html .= ", operate:'RANGE', addclass:'datetimerange'";
+        } elseif (in_array($datatype, ['float', 'double', 'decimal'])) {
+            $html .= ", operate:'BETWEEN'";
+        }
+        if (in_array($datatype, ['set'])) {
+            $html .= ", operate:'FIND_IN_SET'";
+        }
+        if (in_array($formatter, ['image', 'images'])) {
+            $html .= ", events: Table.api.events.image";
+        }
+        if ($itemArr && !$formatter) {
+            $formatter = 'normal';
+        }
+        if ($formatter) {
+            $html .= ", formatter: Table.api.formatter." . $formatter . "}";
+        } else {
+            $html .= "}";
+        }
+        return $html;
+    }
+
+    protected function getCamelizeName($uncamelized_words, $separator = '_')
+    {
+        $uncamelized_words = $separator . str_replace($separator, " ", strtolower($uncamelized_words));
+        return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator);
+    }
+
+    protected function getFieldListName($field)
+    {
+        return $this->getCamelizeName($field) . 'List';
+    }
+}

+ 11 - 0
application/admin/command/Crud/stubs/add.stub

@@ -0,0 +1,11 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+{%addList%}
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 35 - 0
application/admin/command/Crud/stubs/controller.stub

@@ -0,0 +1,35 @@
+<?php
+
+namespace {%controllerNamespace%};
+
+use app\common\controller\Backend;
+
+/**
+ * {%tableComment%}
+ *
+ * @icon {%iconName%}
+ */
+class {%controllerName%} extends Backend
+{
+    
+    /**
+     * {%modelName%}模型对象
+     * @var \{%modelNamespace%}\{%modelName%}
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \{%modelNamespace%}\{%modelName%};
+{%controllerAssignList%}
+    }
+    
+    /**
+     * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法
+     * 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑
+     * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改
+     */
+    
+{%controllerIndex%}
+}

+ 42 - 0
application/admin/command/Crud/stubs/controllerindex.stub

@@ -0,0 +1,42 @@
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //当前是否为关联查询
+        $this->relationSearch = {%relationSearch%};
+        //设置过滤方法
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->isAjax())
+        {
+            //如果发送的来源是Selectpage,则转发到Selectpage
+            if ($this->request->request('keyField'))
+            {
+                return $this->selectpage();
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                    {%relationWithList%}
+                    ->where($where)
+                    ->order($sort, $order)
+                    ->count();
+
+            $list = $this->model
+                    {%relationWithList%}
+                    ->where($where)
+                    ->order($sort, $order)
+                    ->limit($offset, $limit)
+                    ->select();
+
+            foreach ($list as $row) {
+                {%visibleFieldList%}
+                {%relationVisibleFieldList%}
+            }
+            $list = collection($list)->toArray();
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }

+ 11 - 0
application/admin/command/Crud/stubs/edit.stub

@@ -0,0 +1,11 @@
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+{%editList%}
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 6 - 0
application/admin/command/Crud/stubs/html/checkbox.stub

@@ -0,0 +1,6 @@
+
+            <div class="checkbox">
+            {foreach name="{%fieldList%}" item="vo"}
+            <label for="{%fieldName%}-{$key}"><input id="{%fieldName%}-{$key}" name="{%fieldName%}" type="checkbox" value="{$key}" {in name="key" value="{%selectedValue%}"}checked{/in} /> {$vo}</label> 
+            {/foreach}
+            </div>

+ 10 - 0
application/admin/command/Crud/stubs/html/fieldlist.stub

@@ -0,0 +1,10 @@
+
+            <dl class="fieldlist" data-name="{%fieldName%}">
+                <dd>
+                    <ins>{:__('{%itemKey%}')}</ins>
+                    <ins>{:__('{%itemValue%}')}</ins>
+                </dd>
+                <dd><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+                <textarea name="{%fieldName%}" class="form-control hide" cols="30" rows="5">{%fieldValue%}</textarea>
+            </dl>
+

+ 10 - 0
application/admin/command/Crud/stubs/html/heading-html.stub

@@ -0,0 +1,10 @@
+
+    <div class="panel-heading">
+        {:build_heading(null,FALSE)}
+        <ul class="nav nav-tabs" data-field="{%field%}">
+            <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
+            {foreach name="{%fieldName%}List" item="vo"}
+            <li><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li>
+            {/foreach}
+        </ul>
+    </div>

+ 6 - 0
application/admin/command/Crud/stubs/html/radio.stub

@@ -0,0 +1,6 @@
+
+            <div class="radio">
+            {foreach name="{%fieldList%}" item="vo"}
+            <label for="{%fieldName%}-{$key}"><input id="{%fieldName%}-{$key}" name="{%fieldName%}" type="radio" value="{$key}" {in name="key" value="{%selectedValue%}"}checked{/in} /> {$vo}</label> 
+            {/foreach}
+            </div>

+ 1 - 0
application/admin/command/Crud/stubs/html/recyclebin-html.stub

@@ -0,0 +1 @@
+<a class="btn btn-success btn-recyclebin btn-dialog {:$auth->check('{%controllerUrl%}/recyclebin')?'':'hide'}" href="{%controllerUrl%}/recyclebin" title="{:__('Recycle bin')}"><i class="fa fa-recycle"></i> {:__('Recycle bin')}</a>

+ 6 - 0
application/admin/command/Crud/stubs/html/select.stub

@@ -0,0 +1,6 @@
+            
+            <select {%attrStr%}>
+                {foreach name="{%fieldList%}" item="vo"}
+                    <option value="{$key}" {in name="key" value="{%selectedValue%}"}selected{/in}>{$vo}</option>
+                {/foreach}
+            </select>

+ 5 - 0
application/admin/command/Crud/stubs/html/switch.stub

@@ -0,0 +1,5 @@
+
+            <input {%attrStr%} name="{%fieldName%}" type="hidden" value="{%fieldValue%}">
+            <a href="javascript:;" data-toggle="switcher" class="btn-switcher" data-input-id="c-{%field%}" data-yes="{%fieldYes%}" data-no="{%fieldNo%}" >
+                <i class="fa fa-toggle-on text-success {%fieldSwitchClass%} fa-2x"></i>
+            </a>

+ 35 - 0
application/admin/command/Crud/stubs/index.stub

@@ -0,0 +1,35 @@
+<div class="panel panel-default panel-intro">
+    {%headingHtml%}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
+                        <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('{%controllerUrl%}/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
+                        <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('{%controllerUrl%}/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
+                        <a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('{%controllerUrl%}/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
+                        <a href="javascript:;" class="btn btn-danger btn-import {:$auth->check('{%controllerUrl%}/import')?'':'hide'}" title="{:__('Import')}" id="btn-import-file" data-url="ajax/upload" data-mimetype="csv,xls,xlsx" data-multiple="false"><i class="fa fa-upload"></i> {:__('Import')}</a>
+
+                        <div class="dropdown btn-group {:$auth->check('{%controllerUrl%}/multi')?'':'hide'}">
+                            <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
+                            <ul class="dropdown-menu text-left" role="menu">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
+                            </ul>
+                        </div>
+
+                        {%recyclebinHtml%}
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                           data-operate-edit="{:$auth->check('{%controllerUrl%}/edit')}" 
+                           data-operate-del="{:$auth->check('{%controllerUrl%}/del')}" 
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 47 - 0
application/admin/command/Crud/stubs/javascript.stub

@@ -0,0 +1,47 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: '{%controllerUrl%}/index' + location.search,
+                    add_url: '{%controllerUrl%}/add',
+                    edit_url: '{%controllerUrl%}/edit',
+                    del_url: '{%controllerUrl%}/del',
+                    multi_url: '{%controllerUrl%}/multi',
+                    table: '{%table%}',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: '{%pk%}',
+                sortName: '{%order%}',
+                columns: [
+                    [
+                        {%javascriptList%}
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },{%recyclebinJs%}
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            }
+        }
+    };
+    return Controller;
+});

+ 5 - 0
application/admin/command/Crud/stubs/lang.stub

@@ -0,0 +1,5 @@
+<?php
+
+return [
+{%langList%}
+];

+ 8 - 0
application/admin/command/Crud/stubs/mixins/checkbox.stub

@@ -0,0 +1,8 @@
+
+    public function {%methodName%}($value, $data)
+    {
+        $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
+        $valueArr = explode(',', $value);
+        $list = $this->{%listMethodName%}();
+        return implode(',', array_intersect_key($list, array_flip($valueArr)));
+    }

+ 6 - 0
application/admin/command/Crud/stubs/mixins/datetime.stub

@@ -0,0 +1,6 @@
+
+    public function {%methodName%}($value, $data)
+    {
+        $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
+        return is_numeric($value) ? date("Y-m-d H:i:s", $value) : $value;
+    }

+ 1 - 0
application/admin/command/Crud/stubs/mixins/enum.stub

@@ -0,0 +1 @@
+

+ 8 - 0
application/admin/command/Crud/stubs/mixins/modelinit.stub

@@ -0,0 +1,8 @@
+
+    protected static function init()
+    {
+        self::afterInsert(function ($row) {
+            $pk = $row->getPk();
+            $row->getQuery()->where($pk, $row[$pk])->update(['{%order%}' => $row[$pk]]);
+        });
+    }

+ 5 - 0
application/admin/command/Crud/stubs/mixins/modelrelationmethod.stub

@@ -0,0 +1,5 @@
+
+    public function {%relationMethod%}()
+    {
+        return $this->{%relationMode%}('{%relationClassName%}', '{%relationForeignKey%}', '{%relationPrimaryKey%}', [], 'LEFT')->setEagerlyType(0);
+    }

+ 8 - 0
application/admin/command/Crud/stubs/mixins/multiple.stub

@@ -0,0 +1,8 @@
+
+    public function {%methodName%}($value, $data)
+    {
+        $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
+        $valueArr = explode(',', $value);
+        $list = $this->{%listMethodName%}();
+        return implode(',', array_intersect_key($list, array_flip($valueArr)));
+    }

+ 7 - 0
application/admin/command/Crud/stubs/mixins/radio.stub

@@ -0,0 +1,7 @@
+
+    public function {%methodName%}($value, $data)
+    {
+        $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
+        $list = $this->{%listMethodName%}();
+        return isset($list[$value]) ? $list[$value] : '';
+    }

+ 60 - 0
application/admin/command/Crud/stubs/mixins/recyclebinjs.stub

@@ -0,0 +1,60 @@
+
+        recyclebin: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    'dragsort_url': ''
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: '{%controllerUrl%}/recyclebin' + location.search,
+                pk: 'id',
+                sortName: 'id',
+                columns: [
+                    [
+                        {checkbox: true},
+                        {field: 'id', title: __('Id')},{%recyclebinTitleJs%}
+                        {
+                            field: 'deletetime',
+                            title: __('Deletetime'),
+                            operate: 'RANGE',
+                            addclass: 'datetimerange',
+                            formatter: Table.api.formatter.datetime
+                        },
+                        {
+                            field: 'operate',
+                            width: '130px',
+                            title: __('Operate'),
+                            table: table,
+                            events: Table.api.events.operate,
+                            buttons: [
+                                {
+                                    name: 'Restore',
+                                    text: __('Restore'),
+                                    classname: 'btn btn-xs btn-info btn-ajax btn-restoreit',
+                                    icon: 'fa fa-rotate-left',
+                                    url: '{%controllerUrl%}/restore',
+                                    refresh: true
+                                },
+                                {
+                                    name: 'Destroy',
+                                    text: __('Destroy'),
+                                    classname: 'btn btn-xs btn-danger btn-ajax btn-destroyit',
+                                    icon: 'fa fa-times',
+                                    url: '{%controllerUrl%}/destroy',
+                                    refresh: true
+                                }
+                            ],
+                            formatter: Table.api.formatter.operate
+                        }
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },

+ 7 - 0
application/admin/command/Crud/stubs/mixins/select.stub

@@ -0,0 +1,7 @@
+
+    public function {%methodName%}($value, $data)
+    {
+        $value = $value ? $value : (isset($data['{%field%}']) ? $data['{%field%}'] : '');
+        $list = $this->{%listMethodName%}();
+        return isset($list[$value]) ? $list[$value] : '';
+    }

+ 40 - 0
application/admin/command/Crud/stubs/model.stub

@@ -0,0 +1,40 @@
+<?php
+
+namespace {%modelNamespace%};
+
+use think\Model;
+{%sofeDeleteClassPath%}
+
+class {%modelName%} extends Model
+{
+
+    {%softDelete%}
+
+    {%modelConnection%}
+
+    // 表名
+    protected ${%modelTableType%} = '{%modelTableTypeName%}';
+    
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = {%modelAutoWriteTimestamp%};
+
+    // 定义时间戳字段名
+    protected $createTime = {%createTime%};
+    protected $updateTime = {%updateTime%};
+    protected $deleteTime = {%deleteTime%};
+
+    // 追加属性
+    protected $append = [
+{%appendAttrList%}
+    ];
+    
+{%modelInit%}
+    
+{%getEnumList%}
+
+{%getAttrList%}
+
+{%setAttrList%}
+
+{%relationMethodList%}
+}

+ 25 - 0
application/admin/command/Crud/stubs/recyclebin.stub

@@ -0,0 +1,25 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        {:build_toolbar('refresh')}
+                        <a class="btn btn-info btn-multi btn-disabled disabled {:$auth->check('{%controllerUrl%}/restore')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/restore" data-action="restore"><i class="fa fa-rotate-left"></i> {:__('Restore')}</a>
+                        <a class="btn btn-danger btn-multi btn-disabled disabled {:$auth->check('{%controllerUrl%}/destroy')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/destroy" data-action="destroy"><i class="fa fa-times"></i> {:__('Destroy')}</a>
+                        <a class="btn btn-success btn-restoreall {:$auth->check('{%controllerUrl%}/restore')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/restore" title="{:__('Restore all')}"><i class="fa fa-rotate-left"></i> {:__('Restore all')}</a>
+                        <a class="btn btn-danger btn-destroyall {:$auth->check('{%controllerUrl%}/destroy')?'':'hide'}" href="javascript:;" data-url="{%controllerUrl%}/destroy" title="{:__('Destroy all')}"><i class="fa fa-times"></i> {:__('Destroy all')}</a>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover"
+                           data-operate-restore="{:$auth->check('{%controllerUrl%}/restore')}"
+                           data-operate-destroy="{:$auth->check('{%controllerUrl%}/destroy')}"
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 12 - 0
application/admin/command/Crud/stubs/relationmodel.stub

@@ -0,0 +1,12 @@
+<?php
+
+namespace {%modelNamespace%};
+
+use think\Model;
+
+class {%relationName%} extends Model
+{
+    // 表名
+    protected ${%relationTableType%} = '{%relationTableTypeName%}';
+    
+}

+ 27 - 0
application/admin/command/Crud/stubs/validate.stub

@@ -0,0 +1,27 @@
+<?php
+
+namespace {%validateNamespace%};
+
+use think\Validate;
+
+class {%validateName%} extends Validate
+{
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+    ];
+    /**
+     * 提示消息
+     */
+    protected $message = [
+    ];
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        'add'  => [],
+        'edit' => [],
+    ];
+    
+}

+ 108 - 0
application/admin/command/Install.php

@@ -0,0 +1,108 @@
+<?php
+
+namespace app\admin\command;
+
+use PDO;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Db;
+use think\Exception;
+
+class Install extends Command
+{
+    protected $model = null;
+
+    protected function configure()
+    {
+        $config = Config::get('database');
+        $this
+            ->setName('install')
+            ->addOption('hostname', 'a', Option::VALUE_OPTIONAL, 'mysql hostname', $config['hostname'])
+            ->addOption('hostport', 'o', Option::VALUE_OPTIONAL, 'mysql hostport', $config['hostport'])
+            ->addOption('database', 'd', Option::VALUE_OPTIONAL, 'mysql database', $config['database'])
+            ->addOption('prefix', 'r', Option::VALUE_OPTIONAL, 'table prefix', $config['prefix'])
+            ->addOption('username', 'u', Option::VALUE_OPTIONAL, 'mysql username', $config['username'])
+            ->addOption('password', 'p', Option::VALUE_OPTIONAL, 'mysql password', $config['password'])
+            ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', false)
+            ->setDescription('New installation of FastAdmin');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        // 覆盖安装
+        $force = $input->getOption('force');
+        $hostname = $input->getOption('hostname');
+        $hostport = $input->getOption('hostport');
+        $database = $input->getOption('database');
+        $prefix = $input->getOption('prefix');
+        $username = $input->getOption('username');
+        $password = $input->getOption('password');
+
+        $installLockFile = __DIR__ . "/Install/install.lock";
+        if (is_file($installLockFile) && !$force) {
+            throw new Exception("\nFastAdmin already installed!\nIf you need to reinstall again, use the parameter --force=true ");
+        }
+
+        $sql = file_get_contents(__DIR__ . '/Install/fastadmin.sql');
+
+        $sql = str_replace("`fa_", "`{$prefix}", $sql);
+
+        // 先尝试能否自动创建数据库
+        $config = Config::get('database');
+        $pdo = new PDO("{$config['type']}:host={$hostname}" . ($hostport ? ";port={$hostport}" : ''), $username, $password);
+        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+        $pdo->query("CREATE DATABASE IF NOT EXISTS `{$database}` CHARACTER SET utf8 COLLATE utf8_general_ci;");
+
+        // 连接install命令中指定的数据库
+        $instance = Db::connect([
+            'type' => "{$config['type']}",
+            'hostname' => "{$hostname}",
+            'hostport' => "{$hostport}",
+            'database' => "{$database}",
+            'username' => "{$username}",
+            'password' => "{$password}",
+        ]);
+
+        // 查询一次SQL,判断连接是否正常
+        $instance->execute("SELECT 1");
+
+        // 调用原生PDO对象进行批量查询
+        $instance->getPdo()->exec($sql);
+
+        file_put_contents($installLockFile, 1);
+
+        //后台入口文件
+        $adminFile = ROOT_PATH . 'public' . DS . 'admin.php';
+
+        $dbConfigFile = APP_PATH . 'database.php';
+        $config = @file_get_contents($dbConfigFile);
+        $callback = function ($matches) use ($hostname, $hostport, $username, $password, $database, $prefix) {
+            $field = $matches[1];
+            $replace = $$field;
+            if ($matches[1] == 'hostport' && $hostport == 3306) {
+                $replace = '';
+            }
+            return "'{$matches[1]}'{$matches[2]}=>{$matches[3]}Env::get('database.{$matches[1]}', '{$replace}'),";
+        };
+        $config = preg_replace_callback("/'(hostname|database|username|password|hostport|prefix)'(\s+)=>(\s+)Env::get\((.*)\)\,/", $callback, $config);
+        // 写入数据库配置
+        file_put_contents($dbConfigFile, $config);
+
+        // 修改后台入口
+        if (is_file($adminFile)) {
+            $x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+            $adminName = substr(str_shuffle(str_repeat($x, ceil(10 / strlen($x)))), 1, 10) . '.php';
+            rename($adminFile, ROOT_PATH . 'public' . DS . $adminName);
+            $output->highlight("Admin url:http://www.yoursite.com/{$adminName}");
+        }
+        $output->highlight("Admin username:admin");
+        $output->highlight("Admin password:123456");
+
+        \think\Cache::rm('__menu__');
+
+        $output->info("Install Successed!");
+    }
+}

+ 582 - 0
application/admin/command/Install/fastadmin.sql

@@ -0,0 +1,582 @@
+/*
+ FastAdmin Install SQL
+
+ 官网: https://www.fastadmin.net
+ 演示: https://demo.fastadmin.net
+
+ Date: 2018年05月26日
+*/
+
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for fa_admin
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_admin`;
+CREATE TABLE `fa_admin` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `username` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
+  `nickname` varchar(50) NOT NULL DEFAULT '' COMMENT '昵称',
+  `password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码',
+  `salt` varchar(30) NOT NULL DEFAULT '' COMMENT '密码盐',
+  `avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '头像',
+  `email` varchar(100) NOT NULL DEFAULT '' COMMENT '电子邮箱',
+  `loginfailure` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '失败次数',
+  `logintime` int(10) DEFAULT NULL COMMENT '登录时间',
+  `loginip` varchar(50) DEFAULT NULL COMMENT '登录IP',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `token` varchar(59) NOT NULL DEFAULT '' COMMENT 'Session标识',
+  `status` varchar(30) NOT NULL DEFAULT 'normal' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `username` (`username`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='管理员表';
+
+-- ----------------------------
+-- Records of fa_admin
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_admin` VALUES (1, 'admin', 'Admin', '075eaec83636846f51c152f29b98a2fd', 's4f3', '/assets/img/avatar.png', 'admin@fastadmin.net', 0, 1502029281, '127.0.0.1',1492186163, 1502029281, 'd3992c3b-5ecc-4ecb-9dc2-8997780fcadc', 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_admin_log
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_admin_log`;
+CREATE TABLE `fa_admin_log` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID',
+  `username` varchar(30) NOT NULL DEFAULT '' COMMENT '管理员名字',
+  `url` varchar(1500) NOT NULL DEFAULT '' COMMENT '操作页面',
+  `title` varchar(100) NOT NULL DEFAULT '' COMMENT '日志标题',
+  `content` text NOT NULL COMMENT '内容',
+  `ip` varchar(50) NOT NULL DEFAULT '' COMMENT 'IP',
+  `useragent` varchar(255) NOT NULL DEFAULT '' COMMENT 'User-Agent',
+  `createtime` int(10) DEFAULT NULL COMMENT '操作时间',
+  PRIMARY KEY (`id`),
+  KEY `name` (`username`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='管理员日志表';
+
+-- ----------------------------
+-- Table structure for fa_attachment
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_attachment`;
+CREATE TABLE `fa_attachment` (
+  `id` int(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID',
+  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `url` varchar(255) NOT NULL DEFAULT '' COMMENT '物理路径',
+  `imagewidth` varchar(30) NOT NULL DEFAULT '' COMMENT '宽度',
+  `imageheight` varchar(30) NOT NULL DEFAULT '' COMMENT '高度',
+  `imagetype` varchar(30) NOT NULL DEFAULT '' COMMENT '图片类型',
+  `imageframes` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '图片帧数',
+  `filesize` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '文件大小',
+  `mimetype` varchar(100) NOT NULL DEFAULT '' COMMENT 'mime类型',
+  `extparam` varchar(255) NOT NULL DEFAULT '' COMMENT '透传数据',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建日期',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `uploadtime` int(10) DEFAULT NULL COMMENT '上传时间',
+  `storage` varchar(100) NOT NULL DEFAULT 'local' COMMENT '存储位置',
+  `sha1` varchar(40) NOT NULL DEFAULT '' COMMENT '文件 sha1编码',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='附件表';
+
+-- ----------------------------
+-- Records of fa_attachment
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_attachment` VALUES (1, 1, 0, '/assets/img/qrcode.png', '150', '150', 'png', 0, 21859, 'image/png', '', 1499681848, 1499681848, 1499681848, 'local', '17163603d0263e4838b9387ff2cd4877e8b018f6');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_auth_group
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_auth_group`;
+CREATE TABLE `fa_auth_group` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父组别',
+  `name` varchar(100) NOT NULL DEFAULT '' COMMENT '组名',
+  `rules` text NOT NULL COMMENT '规则ID',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `status` varchar(30) NOT NULL DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='分组表';
+
+-- ----------------------------
+-- Records of fa_auth_group
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_auth_group` VALUES (1, 0, 'Admin group', '*', 1490883540, 149088354, 'normal');
+INSERT INTO `fa_auth_group` VALUES (2, 1, 'Second group', '13,14,16,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,40,41,42,43,44,45,46,47,48,49,50,55,56,57,58,59,60,61,62,63,64,65,1,9,10,11,7,6,8,2,4,5', 1490883540, 1505465692, 'normal');
+INSERT INTO `fa_auth_group` VALUES (3, 2, 'Third group', '1,4,9,10,11,13,14,15,16,17,40,41,42,43,44,45,46,47,48,49,50,55,56,57,58,59,60,61,62,63,64,65,5', 1490883540, 1502205322, 'normal');
+INSERT INTO `fa_auth_group` VALUES (4, 1, 'Second group 2', '1,4,13,14,15,16,17,55,56,57,58,59,60,61,62,63,64,65', 1490883540, 1502205350, 'normal');
+INSERT INTO `fa_auth_group` VALUES (5, 2, 'Third group 2', '1,2,6,7,8,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34', 1490883540, 1502205344, 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_auth_group_access
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_auth_group_access`;
+CREATE TABLE `fa_auth_group_access` (
+  `uid` int(10) unsigned NOT NULL COMMENT '会员ID',
+  `group_id` int(10) unsigned NOT NULL COMMENT '级别ID',
+  UNIQUE KEY `uid_group_id` (`uid`,`group_id`),
+  KEY `uid` (`uid`),
+  KEY `group_id` (`group_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='权限分组表';
+
+-- ----------------------------
+-- Records of fa_auth_group_access
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_auth_group_access` VALUES (1, 1);
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_auth_rule
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_auth_rule`;
+CREATE TABLE `fa_auth_rule` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `type` enum('menu','file') NOT NULL DEFAULT 'file' COMMENT 'menu为菜单,file为权限节点',
+  `pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父ID',
+  `name` varchar(100) NOT NULL DEFAULT '' COMMENT '规则名称',
+  `title` varchar(50) NOT NULL DEFAULT '' COMMENT '规则名称',
+  `icon` varchar(50) NOT NULL DEFAULT '' COMMENT '图标',
+  `condition` varchar(255) NOT NULL DEFAULT '' COMMENT '条件',
+  `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
+  `ismenu` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否为菜单',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+  `status` varchar(30) NOT NULL DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`) USING BTREE,
+  KEY `pid` (`pid`),
+  KEY `weigh` (`weigh`)
+) ENGINE=InnoDB AUTO_INCREMENT=66 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='节点表';
+
+-- ----------------------------
+-- Records of fa_auth_rule
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_auth_rule` VALUES (1, 'file', 0, 'dashboard', 'Dashboard', 'fa fa-dashboard', '', 'Dashboard tips', 1, 1497429920, 1497429920, 143, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (2, 'file', 0, 'general', 'General', 'fa fa-cogs', '', '', 1, 1497429920, 1497430169, 137, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (3, 'file', 0, 'category', 'Category', 'fa fa-leaf', '', 'Category tips', 1, 1497429920, 1497429920, 119, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (4, 'file', 0, 'addon', 'Addon', 'fa fa-rocket', '', 'Addon tips', 1, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (5, 'file', 0, 'auth', 'Auth', 'fa fa-group', '', '', 1, 1497429920, 1497430092, 99, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (6, 'file', 2, 'general/config', 'Config', 'fa fa-cog', '', 'Config tips', 1, 1497429920, 1497430683, 60, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (7, 'file', 2, 'general/attachment', 'Attachment', 'fa fa-file-image-o', '', 'Attachment tips', 1, 1497429920, 1497430699, 53, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (8, 'file', 2, 'general/profile', 'Profile', 'fa fa-user', '', '', 1, 1497429920, 1497429920, 34, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (9, 'file', 5, 'auth/admin', 'Admin', 'fa fa-user', '', 'Admin tips', 1, 1497429920, 1497430320, 118, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (10, 'file', 5, 'auth/adminlog', 'Admin log', 'fa fa-list-alt', '', 'Admin log tips', 1, 1497429920, 1497430307, 113, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (11, 'file', 5, 'auth/group', 'Group', 'fa fa-group', '', 'Group tips', 1, 1497429920, 1497429920, 109, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (12, 'file', 5, 'auth/rule', 'Rule', 'fa fa-bars', '', 'Rule tips', 1, 1497429920, 1497430581, 104, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (13, 'file', 1, 'dashboard/index', 'View', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 136, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (14, 'file', 1, 'dashboard/add', 'Add', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 135, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (15, 'file', 1, 'dashboard/del', 'Delete', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 133, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (16, 'file', 1, 'dashboard/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 134, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (17, 'file', 1, 'dashboard/multi', 'Multi', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 132, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (18, 'file', 6, 'general/config/index', 'View', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 52, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (19, 'file', 6, 'general/config/add', 'Add', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 51, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (20, 'file', 6, 'general/config/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 50, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (21, 'file', 6, 'general/config/del', 'Delete', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 49, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (22, 'file', 6, 'general/config/multi', 'Multi', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 48, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (23, 'file', 7, 'general/attachment/index', 'View', 'fa fa-circle-o', '', 'Attachment tips', 0, 1497429920, 1497429920, 59, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (24, 'file', 7, 'general/attachment/select', 'Select attachment', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 58, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (25, 'file', 7, 'general/attachment/add', 'Add', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 57, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (26, 'file', 7, 'general/attachment/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 56, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (27, 'file', 7, 'general/attachment/del', 'Delete', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 55, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (28, 'file', 7, 'general/attachment/multi', 'Multi', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 54, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (29, 'file', 8, 'general/profile/index', 'View', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 33, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (30, 'file', 8, 'general/profile/update', 'Update profile', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 32, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (31, 'file', 8, 'general/profile/add', 'Add', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 31, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (32, 'file', 8, 'general/profile/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 30, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (33, 'file', 8, 'general/profile/del', 'Delete', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 29, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (34, 'file', 8, 'general/profile/multi', 'Multi', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 28, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (35, 'file', 3, 'category/index', 'View', 'fa fa-circle-o', '', 'Category tips', 0, 1497429920, 1497429920, 142, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (36, 'file', 3, 'category/add', 'Add', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 141, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (37, 'file', 3, 'category/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 140, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (38, 'file', 3, 'category/del', 'Delete', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 139, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (39, 'file', 3, 'category/multi', 'Multi', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 138, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (40, 'file', 9, 'auth/admin/index', 'View', 'fa fa-circle-o', '', 'Admin tips', 0, 1497429920, 1497429920, 117, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (41, 'file', 9, 'auth/admin/add', 'Add', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 116, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (42, 'file', 9, 'auth/admin/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 115, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (43, 'file', 9, 'auth/admin/del', 'Delete', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 114, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (44, 'file', 10, 'auth/adminlog/index', 'View', 'fa fa-circle-o', '', 'Admin log tips', 0, 1497429920, 1497429920, 112, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (45, 'file', 10, 'auth/adminlog/detail', 'Detail', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 111, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (46, 'file', 10, 'auth/adminlog/del', 'Delete', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 110, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (47, 'file', 11, 'auth/group/index', 'View', 'fa fa-circle-o', '', 'Group tips', 0, 1497429920, 1497429920, 108, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (48, 'file', 11, 'auth/group/add', 'Add', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 107, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (49, 'file', 11, 'auth/group/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 106, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (50, 'file', 11, 'auth/group/del', 'Delete', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 105, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (51, 'file', 12, 'auth/rule/index', 'View', 'fa fa-circle-o', '', 'Rule tips', 0, 1497429920, 1497429920, 103, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (52, 'file', 12, 'auth/rule/add', 'Add', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 102, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (53, 'file', 12, 'auth/rule/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 101, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (54, 'file', 12, 'auth/rule/del', 'Delete', 'fa fa-circle-o', '', '', 0, 1497429920, 1497429920, 100, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (55, 'file', 4, 'addon/index', 'View', 'fa fa-circle-o', '', 'Addon tips', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (56, 'file', 4, 'addon/add', 'Add', 'fa fa-circle-o', '', '', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (57, 'file', 4, 'addon/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (58, 'file', 4, 'addon/del', 'Delete', 'fa fa-circle-o', '', '', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (59, 'file', 4, 'addon/local', 'Local install', 'fa fa-circle-o', '', '', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (60, 'file', 4, 'addon/state', 'Update state', 'fa fa-circle-o', '', '', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (61, 'file', 4, 'addon/install', 'Install', 'fa fa-circle-o', '', '', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (62, 'file', 4, 'addon/uninstall', 'Uninstall', 'fa fa-circle-o', '', '', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (63, 'file', 4, 'addon/config', 'Setting', 'fa fa-circle-o', '', '', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (64, 'file', 4, 'addon/refresh', 'Refresh', 'fa fa-circle-o', '', '', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (65, 'file', 4, 'addon/multi', 'Multi', 'fa fa-circle-o', '', '', 0, 1502035509, 1502035509, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (66, 'file', 0, 'user', 'User', 'fa fa-list', '', '', 1, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (67, 'file', 66, 'user/user', 'User', 'fa fa-user', '', '', 1, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (68, 'file', 67, 'user/user/index', 'View', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (69, 'file', 67, 'user/user/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (70, 'file', 67, 'user/user/add', 'Add', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (71, 'file', 67, 'user/user/del', 'Del', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (72, 'file', 67, 'user/user/multi', 'Multi', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (73, 'file', 66, 'user/group', 'User group', 'fa fa-users', '', '', 1, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (74, 'file', 73, 'user/group/add', 'Add', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (75, 'file', 73, 'user/group/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (76, 'file', 73, 'user/group/index', 'View', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (77, 'file', 73, 'user/group/del', 'Del', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (78, 'file', 73, 'user/group/multi', 'Multi', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (79, 'file', 66, 'user/rule', 'User rule', 'fa fa-circle-o', '', '', 1, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (80, 'file', 79, 'user/rule/index', 'View', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (81, 'file', 79, 'user/rule/del', 'Del', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (82, 'file', 79, 'user/rule/add', 'Add', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (83, 'file', 79, 'user/rule/edit', 'Edit', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+INSERT INTO `fa_auth_rule` VALUES (84, 'file', 79, 'user/rule/multi', 'Multi', 'fa fa-circle-o', '', '', 0, 1516374729, 1516374729, 0, 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_category
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_category`;
+CREATE TABLE `fa_category` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父ID',
+  `type` varchar(30) NOT NULL DEFAULT '' COMMENT '栏目类型',
+  `name` varchar(30) NOT NULL DEFAULT '',
+  `nickname` varchar(50) NOT NULL DEFAULT '',
+  `flag` set('hot','index','recommend') NOT NULL DEFAULT '',
+  `image` varchar(100) NOT NULL DEFAULT '' COMMENT '图片',
+  `keywords` varchar(255) NOT NULL DEFAULT '' COMMENT '关键字',
+  `description` varchar(255) NOT NULL DEFAULT '' COMMENT '描述',
+  `diyname` varchar(30) NOT NULL DEFAULT '' COMMENT '自定义名称',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+  `status` varchar(30) NOT NULL DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  KEY `weigh` (`weigh`,`id`),
+  KEY `pid` (`pid`)
+) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='分类表';
+
+-- ----------------------------
+-- Records of fa_category
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_category` VALUES (1, 0, 'page', '官方新闻', 'news', 'recommend', '/assets/img/qrcode.png', '', '', 'news', 1495262190, 1495262190, 1, 'normal');
+INSERT INTO `fa_category` VALUES (2, 0, 'page', '移动应用', 'mobileapp', 'hot', '/assets/img/qrcode.png', '', '', 'mobileapp', 1495262244, 1495262244, 2, 'normal');
+INSERT INTO `fa_category` VALUES (3, 2, 'page', '微信公众号', 'wechatpublic', 'index', '/assets/img/qrcode.png', '', '', 'wechatpublic', 1495262288, 1495262288, 3, 'normal');
+INSERT INTO `fa_category` VALUES (4, 2, 'page', 'Android开发', 'android', 'recommend', '/assets/img/qrcode.png', '', '', 'android', 1495262317, 1495262317, 4, 'normal');
+INSERT INTO `fa_category` VALUES (5, 0, 'page', '软件产品', 'software', 'recommend', '/assets/img/qrcode.png', '', '', 'software', 1495262336, 1499681850, 5, 'normal');
+INSERT INTO `fa_category` VALUES (6, 5, 'page', '网站建站', 'website', 'recommend', '/assets/img/qrcode.png', '', '', 'website', 1495262357, 1495262357, 6, 'normal');
+INSERT INTO `fa_category` VALUES (7, 5, 'page', '企业管理软件', 'company', 'index', '/assets/img/qrcode.png', '', '', 'company', 1495262391, 1495262391, 7, 'normal');
+INSERT INTO `fa_category` VALUES (8, 6, 'page', 'PC端', 'website-pc', 'recommend', '/assets/img/qrcode.png', '', '', 'website-pc', 1495262424, 1495262424, 8, 'normal');
+INSERT INTO `fa_category` VALUES (9, 6, 'page', '移动端', 'website-mobile', 'recommend', '/assets/img/qrcode.png', '', '', 'website-mobile', 1495262456, 1495262456, 9, 'normal');
+INSERT INTO `fa_category` VALUES (10, 7, 'page', 'CRM系统 ', 'company-crm', 'recommend', '/assets/img/qrcode.png', '', '', 'company-crm', 1495262487, 1495262487, 10, 'normal');
+INSERT INTO `fa_category` VALUES (11, 7, 'page', 'SASS平台软件', 'company-sass', 'recommend', '/assets/img/qrcode.png', '', '', 'company-sass', 1495262515, 1495262515, 11, 'normal');
+INSERT INTO `fa_category` VALUES (12, 0, 'test', '测试1', 'test1', 'recommend', '/assets/img/qrcode.png', '', '', 'test1', 1497015727, 1497015727, 12, 'normal');
+INSERT INTO `fa_category` VALUES (13, 0, 'test', '测试2', 'test2', 'recommend', '/assets/img/qrcode.png', '', '', 'test2', 1497015738, 1497015738, 13, 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_config
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_config`;
+CREATE TABLE `fa_config` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `name` varchar(30) NOT NULL DEFAULT '' COMMENT '变量名',
+  `group` varchar(30) NOT NULL DEFAULT '' COMMENT '分组',
+  `title` varchar(100) NOT NULL DEFAULT '' COMMENT '变量标题',
+  `tip` varchar(100) NOT NULL DEFAULT '' COMMENT '变量描述',
+  `type` varchar(30) NOT NULL DEFAULT '' COMMENT '类型:string,text,int,bool,array,datetime,date,file',
+  `value` text NOT NULL COMMENT '变量值',
+  `content` text NOT NULL COMMENT '变量字典数据',
+  `rule` varchar(100) NOT NULL DEFAULT '' COMMENT '验证规则',
+  `extend` varchar(255) NOT NULL DEFAULT '' COMMENT '扩展属性',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='系统配置';
+
+-- ----------------------------
+-- Records of fa_config
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_config` VALUES (1, 'name', 'basic', 'Site name', '请填写站点名称', 'string', 'FastAdmin', '', 'required', '');
+INSERT INTO `fa_config` VALUES (2, 'beian', 'basic', 'Beian', '粤ICP备15000000号-1', 'string', '', '', '', '');
+INSERT INTO `fa_config` VALUES (3, 'cdnurl', 'basic', 'Cdn url', '如果静态资源使用第三方云储存请配置该值', 'string', '', '', '', '');
+INSERT INTO `fa_config` VALUES (4, 'version', 'basic', 'Version', '如果静态资源有变动请重新配置该值', 'string', '1.0.1', '', 'required', '');
+INSERT INTO `fa_config` VALUES (5, 'timezone', 'basic', 'Timezone', '', 'string', 'Asia/Shanghai', '', 'required', '');
+INSERT INTO `fa_config` VALUES (6, 'forbiddenip', 'basic', 'Forbidden ip', '一行一条记录', 'text', '', '', '', '');
+INSERT INTO `fa_config` VALUES (7, 'languages', 'basic', 'Languages', '', 'array', '{\"backend\":\"zh-cn\",\"frontend\":\"zh-cn\"}', '', 'required', '');
+INSERT INTO `fa_config` VALUES (8, 'fixedpage', 'basic', 'Fixed page', '请尽量输入左侧菜单栏存在的链接', 'string', 'dashboard', '', 'required', '');
+INSERT INTO `fa_config` VALUES (9, 'categorytype', 'dictionary', 'Category type', '', 'array', '{\"default\":\"Default\",\"page\":\"Page\",\"article\":\"Article\",\"test\":\"Test\"}', '', '', '');
+INSERT INTO `fa_config` VALUES (10, 'configgroup', 'dictionary', 'Config group', '', 'array', '{\"basic\":\"Basic\",\"email\":\"Email\",\"dictionary\":\"Dictionary\",\"user\":\"User\",\"example\":\"Example\"}', '', '', '');
+INSERT INTO `fa_config` VALUES (11, 'mail_type', 'email', 'Mail type', '选择邮件发送方式', 'select', '1', '[\"Please select\",\"SMTP\",\"Mail\"]', '', '');
+INSERT INTO `fa_config` VALUES (12, 'mail_smtp_host', 'email', 'Mail smtp host', '错误的配置发送邮件会导致服务器超时', 'string', 'smtp.qq.com', '', '', '');
+INSERT INTO `fa_config` VALUES (13, 'mail_smtp_port', 'email', 'Mail smtp port', '(不加密默认25,SSL默认465,TLS默认587)', 'string', '465', '', '', '');
+INSERT INTO `fa_config` VALUES (14, 'mail_smtp_user', 'email', 'Mail smtp user', '(填写完整用户名)', 'string', '10000', '', '', '');
+INSERT INTO `fa_config` VALUES (15, 'mail_smtp_pass', 'email', 'Mail smtp password', '(填写您的密码)', 'string', 'password', '', '', '');
+INSERT INTO `fa_config` VALUES (16, 'mail_verify_type', 'email', 'Mail vertify type', '(SMTP验证方式[推荐SSL])', 'select', '2', '[\"None\",\"TLS\",\"SSL\"]', '', '');
+INSERT INTO `fa_config` VALUES (17, 'mail_from', 'email', 'Mail from', '', 'string', '10000@qq.com', '', '', '');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_ems
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_ems`;
+CREATE TABLE `fa_ems`  (
+  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `event` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '事件',
+  `email` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '邮箱',
+  `code` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '验证码',
+  `times` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '验证次数',
+  `ip` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT 'IP',
+  `createtime` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='邮箱验证码表';
+
+-- ----------------------------
+-- Table structure for fa_sms
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_sms`;
+CREATE TABLE `fa_sms` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `event` varchar(30) NOT NULL DEFAULT '' COMMENT '事件',
+  `mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
+  `code` varchar(10) NOT NULL DEFAULT '' COMMENT '验证码',
+  `times` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '验证次数',
+  `ip` varchar(30) NOT NULL DEFAULT '' COMMENT 'IP',
+  `createtime` int(10) unsigned DEFAULT '0' COMMENT '创建时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='短信验证码表';
+
+-- ----------------------------
+-- Table structure for fa_test
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_test`;
+CREATE TABLE `fa_test` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `admin_id` int(10) NOT NULL DEFAULT '0' COMMENT '管理员ID',
+  `category_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '分类ID(单选)',
+  `category_ids` varchar(100) NOT NULL COMMENT '分类ID(多选)',
+  `week` enum('monday','tuesday','wednesday') NOT NULL COMMENT '星期(单选):monday=星期一,tuesday=星期二,wednesday=星期三',
+  `flag` set('hot','index','recommend') NOT NULL DEFAULT '' COMMENT '标志(多选):hot=热门,index=首页,recommend=推荐',
+  `genderdata` enum('male','female') NOT NULL DEFAULT 'male' COMMENT '性别(单选):male=男,female=女',
+  `hobbydata` set('music','reading','swimming') NOT NULL COMMENT '爱好(多选):music=音乐,reading=读书,swimming=游泳',
+  `title` varchar(50) NOT NULL DEFAULT '' COMMENT '标题',
+  `content` text NOT NULL COMMENT '内容',
+  `image` varchar(100) NOT NULL DEFAULT '' COMMENT '图片',
+  `images` varchar(1500) NOT NULL DEFAULT '' COMMENT '图片组',
+  `attachfile` varchar(100) NOT NULL DEFAULT '' COMMENT '附件',
+  `keywords` varchar(100) NOT NULL DEFAULT '' COMMENT '关键字',
+  `description` varchar(255) NOT NULL DEFAULT '' COMMENT '描述',
+  `city` varchar(100) NOT NULL DEFAULT '' COMMENT '省市',
+  `json` varchar(255) DEFAULT NULL COMMENT '配置:key=名称,value=值',
+  `price` float(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '价格',
+  `views` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '点击',
+  `startdate` date DEFAULT NULL COMMENT '开始日期',
+  `activitytime` datetime DEFAULT NULL COMMENT '活动时间(datetime)',
+  `year` year(4) DEFAULT NULL COMMENT '年',
+  `times` time DEFAULT NULL COMMENT '时间',
+  `refreshtime` int(10) DEFAULT NULL COMMENT '刷新时间(int)',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `deletetime` int(10) DEFAULT NULL COMMENT '删除时间',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+  `switch` tinyint(1) NOT NULL DEFAULT '0' COMMENT '开关',
+  `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态',
+  `state` enum('0','1','2') NOT NULL DEFAULT '1' COMMENT '状态值:0=禁用,1=正常,2=推荐',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='测试表';
+
+-- ----------------------------
+-- Records of fa_test
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_test` VALUES (1, 0, 12, '12,13', 'monday', 'hot,index', 'male', 'music,reading', '我是一篇测试文章', '<p>我是测试内容</p>', '/assets/img/avatar.png', '/assets/img/avatar.png,/assets/img/qrcode.png', '/assets/img/avatar.png', '关键字', '描述', '广西壮族自治区/百色市/平果县', '{\"a\":\"1\",\"b\":\"2\"}', 0.00, 0, '2017-07-10', '2017-07-10 18:24:45', 2017, '18:24:45', 1499682285, 1499682526, 1499682526, NULL, 0, 1, 'normal', '1');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_user
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user`;
+CREATE TABLE `fa_user` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '组别ID',
+  `username` varchar(32) NOT NULL DEFAULT '' COMMENT '用户名',
+  `nickname` varchar(50) NOT NULL DEFAULT '' COMMENT '昵称',
+  `password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码',
+  `salt` varchar(30) NOT NULL DEFAULT '' COMMENT '密码盐',
+  `email` varchar(100) NOT NULL DEFAULT '' COMMENT '电子邮箱',
+  `mobile` varchar(11) NOT NULL DEFAULT '' COMMENT '手机号',
+  `avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '头像',
+  `level` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '等级',
+  `gender` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '性别',
+  `birthday` date COMMENT '生日',
+  `bio` varchar(100) NOT NULL DEFAULT '' COMMENT '格言',
+  `money` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '余额',
+  `score` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '积分',
+  `successions` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '连续登录天数',
+  `maxsuccessions` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '最大连续登录天数',
+  `prevtime` int(10) DEFAULT NULL COMMENT '上次登录时间',
+  `logintime` int(10) DEFAULT NULL COMMENT '登录时间',
+  `loginip` varchar(50) NOT NULL DEFAULT '' COMMENT '登录IP',
+  `loginfailure` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '失败次数',
+  `joinip` varchar(50) NOT NULL DEFAULT '' COMMENT '加入IP',
+  `jointime` int(10) DEFAULT NULL COMMENT '加入时间',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `token` varchar(50) NOT NULL DEFAULT '' COMMENT 'Token',
+  `status` varchar(30) NOT NULL DEFAULT '' COMMENT '状态',
+  `verification` varchar(255) NOT NULL DEFAULT '' COMMENT '验证',
+  PRIMARY KEY (`id`),
+  KEY `username` (`username`),
+  KEY `email` (`email`),
+  KEY `mobile` (`mobile`)
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='会员表';
+
+-- ----------------------------
+-- Records of fa_user
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_user` VALUES (1, 1, 'admin', 'admin', 'c13f62012fd6a8fdf06b3452a94430e5', 'rpR6Bv', 'admin@163.com', '13888888888', '', 0, 0, '2017-04-15', '', 0, 0, 1, 1, 1516170492, 1516171614, '127.0.0.1', 0, '127.0.0.1', 1491461418, 0, 1516171614, '', 'normal','');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_user_group
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user_group`;
+CREATE TABLE `fa_user_group` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `name` varchar(50) DEFAULT '' COMMENT '组名',
+  `rules` text COMMENT '权限节点',
+  `createtime` int(10) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `status` enum('normal','hidden') DEFAULT NULL COMMENT '状态',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='会员组表';
+
+-- ----------------------------
+-- Records of fa_user_group
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_user_group` VALUES (1, '默认组', '1,2,3,4,5,6,7,8,9,10,11,12', 1515386468, 1516168298, 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_user_money_log
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user_money_log`;
+CREATE TABLE `fa_user_money_log` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `money` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变更余额',
+  `before` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变更前余额',
+  `after` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变更后余额',
+  `memo` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='会员余额变动表';
+
+-- ----------------------------
+-- Table structure for fa_user_rule
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user_rule`;
+CREATE TABLE `fa_user_rule` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `pid` int(10) DEFAULT NULL COMMENT '父ID',
+  `name` varchar(50) DEFAULT NULL COMMENT '名称',
+  `title` varchar(50) DEFAULT '' COMMENT '标题',
+  `remark` varchar(100) DEFAULT NULL COMMENT '备注',
+  `ismenu` tinyint(1) DEFAULT NULL COMMENT '是否菜单',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  `weigh` int(10) DEFAULT '0' COMMENT '权重',
+  `status` enum('normal','hidden') DEFAULT NULL COMMENT '状态',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='会员规则表';
+
+-- ----------------------------
+-- Records of fa_user_rule
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_user_rule` VALUES (1, 0, 'index', '前台', '', 1, 1516168079, 1516168079, 1, 'normal');
+INSERT INTO `fa_user_rule` VALUES (2, 0, 'api', 'API接口', '', 1, 1516168062, 1516168062, 2, 'normal');
+INSERT INTO `fa_user_rule` VALUES (3, 1, 'user', '会员模块', '', 1, 1515386221, 1516168103, 12, 'normal');
+INSERT INTO `fa_user_rule` VALUES (4, 2, 'user', '会员模块', '', 1, 1515386221, 1516168092, 11, 'normal');
+INSERT INTO `fa_user_rule` VALUES (5, 3, 'index/user/login', '登录', '', 0, 1515386247, 1515386247, 5, 'normal');
+INSERT INTO `fa_user_rule` VALUES (6, 3, 'index/user/register', '注册', '', 0, 1515386262, 1516015236, 7, 'normal');
+INSERT INTO `fa_user_rule` VALUES (7, 3, 'index/user/index', '会员中心', '', 0, 1516015012, 1516015012, 9, 'normal');
+INSERT INTO `fa_user_rule` VALUES (8, 3, 'index/user/profile', '个人资料', '', 0, 1516015012, 1516015012, 4, 'normal');
+INSERT INTO `fa_user_rule` VALUES (9, 4, 'api/user/login', '登录', '', 0, 1515386247, 1515386247, 6, 'normal');
+INSERT INTO `fa_user_rule` VALUES (10, 4, 'api/user/register', '注册', '', 0, 1515386262, 1516015236, 8, 'normal');
+INSERT INTO `fa_user_rule` VALUES (11, 4, 'api/user/index', '会员中心', '', 0, 1516015012, 1516015012, 10, 'normal');
+INSERT INTO `fa_user_rule` VALUES (12, 4, 'api/user/profile', '个人资料', '', 0, 1516015012, 1516015012, 3, 'normal');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for fa_user_score_log
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user_score_log`;
+CREATE TABLE `fa_user_score_log` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `score` int(10) NOT NULL DEFAULT '0' COMMENT '变更积分',
+  `before` int(10) NOT NULL DEFAULT '0' COMMENT '变更前积分',
+  `after` int(10) NOT NULL DEFAULT '0' COMMENT '变更后积分',
+  `memo` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='会员积分变动表';
+
+-- ----------------------------
+-- Table structure for fa_user_token
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_user_token`;
+CREATE TABLE `fa_user_token` (
+  `token` varchar(50) NOT NULL COMMENT 'Token',
+  `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `expiretime` int(10) DEFAULT NULL COMMENT '过期时间',
+  PRIMARY KEY (`token`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='会员Token表';
+
+-- ----------------------------
+-- Table structure for fa_version
+-- ----------------------------
+DROP TABLE IF EXISTS `fa_version`;
+CREATE TABLE `fa_version`  (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `oldversion` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '旧版本号',
+  `newversion` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '新版本号',
+  `packagesize` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '包大小',
+  `content` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '升级内容',
+  `downloadurl` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '下载地址',
+  `enforce` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '强制更新',
+  `createtime` int(10) NOT NULL DEFAULT 0 COMMENT '创建时间',
+  `updatetime` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间',
+  `weigh` int(10) NOT NULL DEFAULT 0 COMMENT '权重',
+  `status` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT = '版本表';
+
+-- ----------------------------
+-- Table structure for fa_version
+-- ----------------------------
+BEGIN;
+INSERT INTO `fa_version` (`id`, `oldversion`, `newversion`, `packagesize`, `content`, `downloadurl`, `enforce`, `createtime`, `updatetime`, `weigh`, `status`) VALUES
+(1, '1.1.1,2', '1.2.1', '20M', '更新内容', 'https://www.fastadmin.net/download.html', 1, 1520425318, 0, 0, 'normal');
+COMMIT;
+SET FOREIGN_KEY_CHECKS = 1;

+ 1 - 0
application/admin/command/Install/install.lock

@@ -0,0 +1 @@
+1

+ 323 - 0
application/admin/command/Menu.php

@@ -0,0 +1,323 @@
+<?php
+
+namespace app\admin\command;
+
+use app\admin\model\AuthRule;
+use ReflectionClass;
+use ReflectionMethod;
+use think\Cache;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Exception;
+use think\Loader;
+
+class Menu extends Command
+{
+    protected $model = null;
+
+    protected function configure()
+    {
+        $this
+            ->setName('menu')
+            ->addOption('controller', 'c', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 'controller name,use \'all-controller\' when build all menu', null)
+            ->addOption('delete', 'd', Option::VALUE_OPTIONAL, 'delete the specified menu', '')
+            ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force delete menu,without tips', null)
+            ->addOption('equal', 'e', Option::VALUE_OPTIONAL, 'the controller must be equal', null)
+            ->setDescription('Build auth menu from controller');
+        //要执行的controller必须一样,不适用模糊查询
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $this->model = new AuthRule();
+        $adminPath = dirname(__DIR__) . DS;
+        //控制器名
+        $controller = $input->getOption('controller') ?: '';
+        if (!$controller) {
+            throw new Exception("please input controller name");
+        }
+        $force = $input->getOption('force');
+        //是否为删除模式
+        $delete = $input->getOption('delete');
+        //是否控制器完全匹配
+        $equal = $input->getOption('equal');
+
+
+        if ($delete) {
+            if (in_array('all-controller', $controller)) {
+                throw new Exception("could not delete all menu");
+            }
+            $ids = [];
+            $list = $this->model->where(function ($query) use ($controller, $equal) {
+                foreach ($controller as $index => $item) {
+                    if (stripos($item, '_') !== false) {
+                        $item = Loader::parseName($item, 1);
+                    }
+                    if (stripos($item, '/') !== false) {
+                        $controllerArr = explode('/', $item);
+                        end($controllerArr);
+                        $key = key($controllerArr);
+                        $controllerArr[$key] = Loader::parseName($controllerArr[$key]);
+                    } else {
+                        $controllerArr = [Loader::parseName($item)];
+                    }
+                    $item = str_replace('_', '\_', implode('/', $controllerArr));
+                    if ($equal) {
+                        $query->whereOr('name', 'eq', $item);
+                    } else {
+                        $query->whereOr('name', 'like', strtolower($item) . "%");
+                    }
+                }
+            })->select();
+            foreach ($list as $k => $v) {
+                $output->warning($v->name);
+                $ids[] = $v->id;
+            }
+            if (!$ids) {
+                throw new Exception("There is no menu to delete");
+            }
+            if (!$force) {
+                $output->info("Are you sure you want to delete all those menu?  Type 'yes' to continue: ");
+                $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'));
+                if (trim($line) != 'yes') {
+                    throw new Exception("Operation is aborted!");
+                }
+            }
+            AuthRule::destroy($ids);
+
+            Cache::rm("__menu__");
+            $output->info("Delete Successed");
+            return;
+        }
+
+        if (!in_array('all-controller', $controller)) {
+            foreach ($controller as $index => $item) {
+                if (stripos($item, '_') !== false) {
+                    $item = Loader::parseName($item, 1);
+                }
+                if (stripos($item, '/') !== false) {
+                    $controllerArr = explode('/', $item);
+                    end($controllerArr);
+                    $key = key($controllerArr);
+                    $controllerArr[$key] = ucfirst($controllerArr[$key]);
+                } else {
+                    $controllerArr = [ucfirst($item)];
+                }
+                $adminPath = dirname(__DIR__) . DS . 'controller' . DS . implode(DS, $controllerArr) . '.php';
+                if (!is_file($adminPath)) {
+                    $output->error("controller not found");
+                    return;
+                }
+                $this->importRule($item);
+            }
+        } else {
+            $authRuleList = AuthRule::select();
+            //生成权限规则备份文件
+            file_put_contents(RUNTIME_PATH . 'authrule.json', json_encode(collection($authRuleList)->toArray()));
+
+            $this->model->where('id', '>', 0)->delete();
+            $controllerDir = $adminPath . 'controller' . DS;
+            // 扫描新的节点信息并导入
+            $treelist = $this->import($this->scandir($controllerDir));
+        }
+        Cache::rm("__menu__");
+        $output->info("Build Successed!");
+    }
+
+    /**
+     * 递归扫描文件夹
+     * @param string $dir
+     * @return array
+     */
+    public function scandir($dir)
+    {
+        $result = [];
+        $cdir = scandir($dir);
+        foreach ($cdir as $value) {
+            if (!in_array($value, array(".", ".."))) {
+                if (is_dir($dir . DS . $value)) {
+                    $result[$value] = $this->scandir($dir . DS . $value);
+                } else {
+                    $result[] = $value;
+                }
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * 导入规则节点
+     * @param array $dirarr
+     * @param array $parentdir
+     * @return array
+     */
+    public function import($dirarr, $parentdir = [])
+    {
+        $menuarr = [];
+        foreach ($dirarr as $k => $v) {
+            if (is_array($v)) {
+                //当前是文件夹
+                $nowparentdir = array_merge($parentdir, [$k]);
+                $this->import($v, $nowparentdir);
+            } else {
+                //只匹配PHP文件
+                if (!preg_match('/^(\w+)\.php$/', $v, $matchone)) {
+                    continue;
+                }
+                //导入文件
+                $controller = ($parentdir ? implode('/', $parentdir) . '/' : '') . $matchone[1];
+                $this->importRule($controller);
+            }
+        }
+
+        return $menuarr;
+    }
+
+    protected function importRule($controller)
+    {
+        $controller = str_replace('\\', '/', $controller);
+        if (stripos($controller, '/') !== false) {
+            $controllerArr = explode('/', $controller);
+            end($controllerArr);
+            $key = key($controllerArr);
+            $controllerArr[$key] = ucfirst($controllerArr[$key]);
+        } else {
+            $key = 0;
+            $controllerArr = [ucfirst($controller)];
+        }
+        $classSuffix = Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : '';
+        $className = "\\app\\admin\\controller\\" . implode("\\", $controllerArr) . $classSuffix;
+
+        $pathArr = $controllerArr;
+        array_unshift($pathArr, '', 'application', 'admin', 'controller');
+        $classFile = ROOT_PATH . implode(DS, $pathArr) . $classSuffix . ".php";
+        $classContent = file_get_contents($classFile);
+        $uniqueName = uniqid("FastAdmin") . $classSuffix;
+        $classContent = str_replace("class " . $controllerArr[$key] . $classSuffix . " ", 'class ' . $uniqueName . ' ', $classContent);
+        $classContent = preg_replace("/namespace\s(.*);/", 'namespace ' . __NAMESPACE__ . ";", $classContent);
+
+        //临时的类文件
+        $tempClassFile = __DIR__ . DS . $uniqueName . ".php";
+        file_put_contents($tempClassFile, $classContent);
+        $className = "\\app\\admin\\command\\" . $uniqueName;
+        //反射机制调用类的注释和方法名
+        $reflector = new ReflectionClass($className);
+
+        if (isset($tempClassFile)) {
+            //删除临时文件
+            @unlink($tempClassFile);
+        }
+
+        //只匹配公共的方法
+        $methods = $reflector->getMethods(ReflectionMethod::IS_PUBLIC);
+        $classComment = $reflector->getDocComment();
+        //判断是否有启用软删除
+        $softDeleteMethods = ['destroy', 'restore', 'recyclebin'];
+        $withSofeDelete = false;
+        $modelRegexArr = ["/\\\$this\->model\s*=\s*model\(['|\"](\w+)['|\"]\);/", "/\\\$this\->model\s*=\s*new\s+([a-zA-Z\\\]+);/"];
+        $modelRegex = preg_match($modelRegexArr[0], $classContent) ? $modelRegexArr[0] : $modelRegexArr[1];
+        preg_match_all($modelRegex, $classContent, $matches);
+        if (isset($matches[1]) && isset($matches[1][0]) && $matches[1][0]) {
+            \think\Request::instance()->module('admin');
+            $model = model($matches[1][0]);
+            if (in_array('trashed', get_class_methods($model))) {
+                $withSofeDelete = true;
+            }
+        }
+        //忽略的类
+        if (stripos($classComment, "@internal") !== false) {
+            return;
+        }
+        preg_match_all('#(@.*?)\n#s', $classComment, $annotations);
+        $controllerIcon = 'fa fa-circle-o';
+        $controllerRemark = '';
+        //判断注释中是否设置了icon值
+        if (isset($annotations[1])) {
+            foreach ($annotations[1] as $tag) {
+                if (stripos($tag, '@icon') !== false) {
+                    $controllerIcon = substr($tag, stripos($tag, ' ') + 1);
+                }
+                if (stripos($tag, '@remark') !== false) {
+                    $controllerRemark = substr($tag, stripos($tag, ' ') + 1);
+                }
+            }
+        }
+        //过滤掉其它字符
+        $controllerTitle = trim(preg_replace(array('/^\/\*\*(.*)[\n\r\t]/u', '/[\s]+\*\//u', '/\*\s@(.*)/u', '/[\s|\*]+/u'), '', $classComment));
+
+        //导入中文语言包
+        \think\Lang::load(dirname(__DIR__) . DS . 'lang/zh-cn.php');
+
+        //先导入菜单的数据
+        $pid = 0;
+        foreach ($controllerArr as $k => $v) {
+            $key = $k + 1;
+            //驼峰转下划线
+            $controllerNameArr = array_slice($controllerArr, 0, $key);
+            foreach ($controllerNameArr as &$val) {
+                $val = strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $val), "_"));
+            }
+            unset($val);
+            $name = implode('/', $controllerNameArr);
+            $title = (!isset($controllerArr[$key]) ? $controllerTitle : '');
+            $icon = (!isset($controllerArr[$key]) ? $controllerIcon : 'fa fa-list');
+            $remark = (!isset($controllerArr[$key]) ? $controllerRemark : '');
+            $title = $title ? $title : $v;
+            $rulemodel = $this->model->get(['name' => $name]);
+            if (!$rulemodel) {
+                $this->model
+                    ->data(['pid' => $pid, 'name' => $name, 'title' => $title, 'icon' => $icon, 'remark' => $remark, 'ismenu' => 1, 'status' => 'normal'])
+                    ->isUpdate(false)
+                    ->save();
+                $pid = $this->model->id;
+            } else {
+                $pid = $rulemodel->id;
+            }
+        }
+        $ruleArr = [];
+        foreach ($methods as $m => $n) {
+            //过滤特殊的类
+            if (substr($n->name, 0, 2) == '__' || $n->name == '_initialize') {
+                continue;
+            }
+            //未启用软删除时过滤相关方法
+            if (!$withSofeDelete && in_array($n->name, $softDeleteMethods)) {
+                continue;
+            }
+            //只匹配符合的方法
+            if (!preg_match('/^(\w+)' . Config::get('action_suffix') . '/', $n->name, $matchtwo)) {
+                unset($methods[$m]);
+                continue;
+            }
+            $comment = $reflector->getMethod($n->name)->getDocComment();
+            //忽略的方法
+            if (stripos($comment, "@internal") !== false) {
+                continue;
+            }
+            //过滤掉其它字符
+            $comment = preg_replace(array('/^\/\*\*(.*)[\n\r\t]/u', '/[\s]+\*\//u', '/\*\s@(.*)/u', '/[\s|\*]+/u'), '', $comment);
+
+            $title = $comment ? $comment : ucfirst($n->name);
+
+            //获取主键,作为AuthRule更新依据
+            $id = $this->getAuthRulePK($name . "/" . strtolower($n->name));
+
+            $ruleArr[] = array('id' => $id, 'pid' => $pid, 'name' => $name . "/" . strtolower($n->name), 'icon' => 'fa fa-circle-o', 'title' => $title, 'ismenu' => 0, 'status' => 'normal');
+        }
+        $this->model->isUpdate(false)->saveAll($ruleArr);
+    }
+
+    //获取主键
+    protected function getAuthRulePK($name)
+    {
+        if (!empty($name)) {
+            $id = $this->model
+                ->where('name', $name)
+                ->value('id');
+            return $id ? $id : null;
+        }
+    }
+}

+ 162 - 0
application/admin/command/Min.php

@@ -0,0 +1,162 @@
+<?php
+
+namespace app\admin\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Exception;
+
+class Min extends Command
+{
+
+    /**
+     * 路径和文件名配置
+     */
+    protected $options = [
+        'cssBaseUrl'  => 'public/assets/css/',
+        'cssBaseName' => '{module}',
+        'jsBaseUrl'   => 'public/assets/js/',
+        'jsBaseName'  => 'require-{module}',
+    ];
+
+    protected function configure()
+    {
+        $this
+                ->setName('min')
+                ->addOption('module', 'm', Option::VALUE_REQUIRED, 'module name(frontend or backend),use \'all\' when build all modules', null)
+                ->addOption('resource', 'r', Option::VALUE_REQUIRED, 'resource name(js or css),use \'all\' when build all resources', null)
+                ->addOption('optimize', 'o', Option::VALUE_OPTIONAL, 'optimize type(uglify|closure|none)', 'none')
+                ->setDescription('Compress js and css file');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $module = $input->getOption('module') ?: '';
+        $resource = $input->getOption('resource') ?: '';
+        $optimize = $input->getOption('optimize') ?: 'none';
+
+        if (!$module || !in_array($module, ['frontend', 'backend', 'all'])) {
+            throw new Exception('Please input correct module name');
+        }
+        if (!$resource || !in_array($resource, ['js', 'css', 'all'])) {
+            throw new Exception('Please input correct resource name');
+        }
+
+        $moduleArr = $module == 'all' ? ['frontend', 'backend'] : [$module];
+        $resourceArr = $resource == 'all' ? ['js', 'css'] : [$resource];
+
+        $minPath = __DIR__ . DS . 'Min' . DS;
+        $publicPath = ROOT_PATH . 'public' . DS;
+        $tempFile = $minPath . 'temp.js';
+
+        $nodeExec = '';
+
+        if (!$nodeExec) {
+            if (IS_WIN) {
+                // Winsows下请手动配置配置该值,一般将该值配置为 '"C:\Program Files\nodejs\node.exe"',除非你的Node安装路径有变更
+                $nodeExec = 'C:\Program Files\nodejs\node.exe';
+                if (file_exists($nodeExec)) {
+                    $nodeExec = '"' . $nodeExec . '"';
+                } else {
+                    // 如果 '"C:\Program Files\nodejs\node.exe"' 不存在,可能是node安装路径有变更
+                    // 但安装node会自动配置环境变量,直接执行 '"node.exe"' 提高第一次使用压缩打包的成功率
+                    $nodeExec = '"node.exe"';
+                }
+            } else {
+                try {
+                    $nodeExec = exec("which node");
+                    if (!$nodeExec) {
+                        throw new Exception("node environment not found!please install node first!");
+                    }
+                } catch (Exception $e) {
+                    throw new Exception($e->getMessage());
+                }
+            }
+        }
+
+        foreach ($moduleArr as $mod) {
+            foreach ($resourceArr as $res) {
+                $data = [
+                    'publicPath'  => $publicPath,
+                    'jsBaseName'  => str_replace('{module}', $mod, $this->options['jsBaseName']),
+                    'jsBaseUrl'   => $this->options['jsBaseUrl'],
+                    'cssBaseName' => str_replace('{module}', $mod, $this->options['cssBaseName']),
+                    'cssBaseUrl'  => $this->options['cssBaseUrl'],
+                    'jsBasePath'  => str_replace(DS, '/', ROOT_PATH . $this->options['jsBaseUrl']),
+                    'cssBasePath' => str_replace(DS, '/', ROOT_PATH . $this->options['cssBaseUrl']),
+                    'optimize'    => $optimize,
+                    'ds'          => DS,
+                ];
+
+                //源文件
+                $from = $data["{$res}BasePath"] . $data["{$res}BaseName"] . '.' . $res;
+                if (!is_file($from)) {
+                    $output->error("{$res} source file not found!file:{$from}");
+                    continue;
+                }
+                if ($res == "js") {
+                    $content = file_get_contents($from);
+                    preg_match("/require\.config\(\{[\r\n]?[\n]?+(.*?)[\r\n]?[\n]?}\);/is", $content, $matches);
+                    if (!isset($matches[1])) {
+                        $output->error("js config not found!");
+                        continue;
+                    }
+                    $config = preg_replace("/(urlArgs|baseUrl):(.*)\n/", '', $matches[1]);
+                    $data['config'] = $config;
+                }
+                // 生成压缩文件
+                $this->writeToFile($res, $data, $tempFile);
+
+                $output->info("Compress " . $data["{$res}BaseName"] . ".{$res}");
+
+                // 执行压缩
+                $command = "{$nodeExec} \"{$minPath}r.js\" -o \"{$tempFile}\" >> \"{$minPath}node.log\"";
+                if ($output->isDebug()) {
+                    $output->warning($command);
+                }
+                echo exec($command);
+            }
+        }
+
+        if (!$output->isDebug()) {
+            @unlink($tempFile);
+        }
+
+        $output->info("Build Successed!");
+    }
+
+    /**
+     * 写入到文件
+     * @param string $name
+     * @param array $data
+     * @param string $pathname
+     * @return mixed
+     */
+    protected function writeToFile($name, $data, $pathname)
+    {
+        $search = $replace = [];
+        foreach ($data as $k => $v) {
+            $search[] = "{%{$k}%}";
+            $replace[] = $v;
+        }
+        $stub = file_get_contents($this->getStub($name));
+        $content = str_replace($search, $replace, $stub);
+
+        if (!is_dir(dirname($pathname))) {
+            mkdir(strtolower(dirname($pathname)), 0755, true);
+        }
+        return file_put_contents($pathname, $content);
+    }
+
+    /**
+     * 获取基础模板
+     * @param string $name
+     * @return string
+     */
+    protected function getStub($name)
+    {
+        return __DIR__ . DS . 'Min' . DS . 'stubs' . DS . $name . '.stub';
+    }
+}

File diff suppressed because it is too large
+ 4699 - 0
application/admin/command/Min/r.js


+ 6 - 0
application/admin/command/Min/stubs/css.stub

@@ -0,0 +1,6 @@
+({
+  cssIn: "{%cssBasePath%}{%cssBaseName%}.css",
+  out: "{%cssBasePath%}{%cssBaseName%}.min.css",
+  optimizeCss: "default",
+  optimize: "{%optimize%}"
+})

+ 11 - 0
application/admin/command/Min/stubs/js.stub

@@ -0,0 +1,11 @@
+({
+    {%config%}
+    ,
+    optimizeCss: "standard",
+    optimize: "{%optimize%}",   //可使用uglify|closure|none
+    preserveLicenseComments: false,
+    removeCombined: false,
+    baseUrl: "{%jsBasePath%}",    //JS文件所在的基础目录
+    name: "{%jsBaseName%}", //来源文件,不包含后缀
+    out: "{%jsBasePath%}{%jsBaseName%}.min.js"  //目标文件
+});

+ 196 - 0
application/admin/common.php

@@ -0,0 +1,196 @@
+<?php
+
+use app\common\model\Category;
+use fast\Form;
+use fast\Tree;
+use think\Db;
+
+if (!function_exists('build_select')) {
+
+    /**
+     * 生成下拉列表
+     * @param string $name
+     * @param mixed $options
+     * @param mixed $selected
+     * @param mixed $attr
+     * @return string
+     */
+    function build_select($name, $options, $selected = [], $attr = [])
+    {
+        $options = is_array($options) ? $options : explode(',', $options);
+        $selected = is_array($selected) ? $selected : explode(',', $selected);
+        return Form::select($name, $options, $selected, $attr);
+    }
+}
+
+if (!function_exists('build_radios')) {
+
+    /**
+     * 生成单选按钮组
+     * @param string $name
+     * @param array $list
+     * @param mixed $selected
+     * @return string
+     */
+    function build_radios($name, $list = [], $selected = null)
+    {
+        $html = [];
+        $selected = is_null($selected) ? key($list) : $selected;
+        $selected = is_array($selected) ? $selected : explode(',', $selected);
+        foreach ($list as $k => $v) {
+            $html[] = sprintf(Form::label("{$name}-{$k}", "%s {$v}"), Form::radio($name, $k, in_array($k, $selected), ['id' => "{$name}-{$k}"]));
+        }
+        return '<div class="radio">' . implode(' ', $html) . '</div>';
+    }
+}
+
+if (!function_exists('build_checkboxs')) {
+
+    /**
+     * 生成复选按钮组
+     * @param string $name
+     * @param array $list
+     * @param mixed $selected
+     * @return string
+     */
+    function build_checkboxs($name, $list = [], $selected = null)
+    {
+        $html = [];
+        $selected = is_null($selected) ? [] : $selected;
+        $selected = is_array($selected) ? $selected : explode(',', $selected);
+        foreach ($list as $k => $v) {
+            $html[] = sprintf(Form::label("{$name}-{$k}", "%s {$v}"), Form::checkbox($name, $k, in_array($k, $selected), ['id' => "{$name}-{$k}"]));
+        }
+        return '<div class="checkbox">' . implode(' ', $html) . '</div>';
+    }
+}
+
+
+if (!function_exists('build_category_select')) {
+
+    /**
+     * 生成分类下拉列表框
+     * @param string $name
+     * @param string $type
+     * @param mixed $selected
+     * @param array $attr
+     * @param array $header
+     * @return string
+     */
+    function build_category_select($name, $type, $selected = null, $attr = [], $header = [])
+    {
+        $tree = Tree::instance();
+        $tree->init(Category::getCategoryArray($type), 'pid');
+        $categorylist = $tree->getTreeList($tree->getTreeArray(0), 'name');
+        $categorydata = $header ? $header : [];
+        foreach ($categorylist as $k => $v) {
+            $categorydata[$v['id']] = $v['name'];
+        }
+        $attr = array_merge(['id' => "c-{$name}", 'class' => 'form-control selectpicker'], $attr);
+        return build_select($name, $categorydata, $selected, $attr);
+    }
+}
+
+if (!function_exists('build_toolbar')) {
+
+    /**
+     * 生成表格操作按钮栏
+     * @param array $btns 按钮组
+     * @param array $attr 按钮属性值
+     * @return string
+     */
+    function build_toolbar($btns = null, $attr = [])
+    {
+        $auth = \app\admin\library\Auth::instance();
+        $controller = str_replace('.', '/', strtolower(think\Request::instance()->controller()));
+        $btns = $btns ? $btns : ['refresh', 'add', 'edit', 'del', 'import'];
+        $btns = is_array($btns) ? $btns : explode(',', $btns);
+        $index = array_search('delete', $btns);
+        if ($index !== false) {
+            $btns[$index] = 'del';
+        }
+        $btnAttr = [
+            'refresh' => ['javascript:;', 'btn btn-primary btn-refresh', 'fa fa-refresh', '', __('Refresh')],
+            'add'     => ['javascript:;', 'btn btn-success btn-add', 'fa fa-plus', __('Add'), __('Add')],
+            'edit'    => ['javascript:;', 'btn btn-success btn-edit btn-disabled disabled', 'fa fa-pencil', __('Edit'), __('Edit')],
+            'del'     => ['javascript:;', 'btn btn-danger btn-del btn-disabled disabled', 'fa fa-trash', __('Delete'), __('Delete')],
+            'import'  => ['javascript:;', 'btn btn-info btn-import', 'fa fa-upload', __('Import'), __('Import')],
+        ];
+        $btnAttr = array_merge($btnAttr, $attr);
+        $html = [];
+        foreach ($btns as $k => $v) {
+            //如果未定义或没有权限
+            if (!isset($btnAttr[$v]) || ($v !== 'refresh' && !$auth->check("{$controller}/{$v}"))) {
+                continue;
+            }
+            list($href, $class, $icon, $text, $title) = $btnAttr[$v];
+            //$extend = $v == 'import' ? 'id="btn-import-file" data-url="ajax/upload" data-mimetype="csv,xls,xlsx" data-multiple="false"' : '';
+            //$html[] = '<a href="' . $href . '" class="' . $class . '" title="' . $title . '" ' . $extend . '><i class="' . $icon . '"></i> ' . $text . '</a>';
+            if ($v == 'import') {
+                $template = str_replace('/', '_', $controller);
+                $download = '';
+                if (file_exists("./template/{$template}.xlsx")) {
+                    $download .= "<li><a href=\"/template/{$template}.xlsx\" target=\"_blank\">XLSX模版</a></li>";
+                }
+                if (file_exists("./template/{$template}.xls")) {
+                    $download .= "<li><a href=\"/template/{$template}.xls\" target=\"_blank\">XLS模版</a></li>";
+                }
+                if (file_exists("./template/{$template}.csv")) {
+                    $download .= empty($download) ? '' : "<li class=\"divider\"></li>";
+                    $download .= "<li><a href=\"/template/{$template}.csv\" target=\"_blank\">CSV模版</a></li>";
+                }
+                $download .= empty($download) ? '' : "\n                            ";
+                if (!empty($download)) {
+                    $html[] = <<<EOT
+                        <div class="btn-group">
+                            <button type="button" href="{$href}" class="btn btn-info btn-import" title="{$title}" id="btn-import-file" data-url="ajax/upload" data-mimetype="csv,xls,xlsx" data-multiple="false"><i class="{$icon}"></i> {$text}</button>
+                            <button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" title="下载批量导入模版">
+                                <span class="caret"></span>
+                                <span class="sr-only">Toggle Dropdown</span>
+                            </button>
+                            <ul class="dropdown-menu" role="menu">{$download}</ul>
+                        </div>
+EOT;
+                } else {
+                    $html[] = '<a href="' . $href . '" class="' . $class . '" title="' . $title . '" id="btn-import-file" data-url="ajax/upload" data-mimetype="csv,xls,xlsx" data-multiple="false"><i class="' . $icon . '"></i> ' . $text . '</a>';
+                }
+            } else {
+                $html[] = '<a href="' . $href . '" class="' . $class . '" title="' . $title . '"><i class="' . $icon . '"></i> ' . $text . '</a>';
+            }
+        }
+        return implode(' ', $html);
+    }
+}
+
+if (!function_exists('build_heading')) {
+
+    /**
+     * 生成页面Heading
+     *
+     * @param string $path 指定的path
+     * @return string
+     */
+    function build_heading($path = null, $container = true)
+    {
+        $title = $content = '';
+        if (is_null($path)) {
+            $action = request()->action();
+            $controller = str_replace('.', '/', request()->controller());
+            $path = strtolower($controller . ($action && $action != 'index' ? '/' . $action : ''));
+        }
+        // 根据当前的URI自动匹配父节点的标题和备注
+        $data = Db::name('auth_rule')->where('name', $path)->field('title,remark')->find();
+        if ($data) {
+            $title = __($data['title']);
+            $content = __($data['remark']);
+        }
+        if (!$content) {
+            return '';
+        }
+        $result = '<div class="panel-lead"><em>' . $title . '</em>' . $content . '</div>';
+        if ($container) {
+            $result = '<div class="panel-heading">' . $result . '</div>';
+        }
+        return $result;
+    }
+}

+ 8 - 0
application/admin/config.php

@@ -0,0 +1,8 @@
+<?php
+
+//配置文件
+return [
+    'url_common_param'       => true,
+    'url_html_suffix'        => '',
+    'controller_auto_search' => true,
+];

+ 364 - 0
application/admin/controller/Addon.php

@@ -0,0 +1,364 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use fast\Http;
+use think\addons\AddonException;
+use think\addons\Service;
+use think\Cache;
+use think\Config;
+use think\Exception;
+
+/**
+ * 插件管理
+ *
+ * @icon   fa fa-cube
+ * @remark 可在线安装、卸载、禁用、启用插件,同时支持添加本地插件。FastAdmin已上线插件商店 ,你可以发布你的免费或付费插件:<a href="https://www.fastadmin.net/store.html" target="_blank">https://www.fastadmin.net/store.html</a>
+ */
+class Addon extends Backend
+{
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $addons = get_addon_list();
+        foreach ($addons as $k => &$v) {
+            $config = get_addon_config($v['name']);
+            $v['config'] = $config ? 1 : 0;
+            $v['url'] = str_replace($this->request->server('SCRIPT_NAME'), '', $v['url']);
+        }
+        $this->assignconfig(['addons' => $addons]);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 配置
+     */
+    public function config($ids = null)
+    {
+        $name = $this->request->get("name");
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', $ids ? 'id' : 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+        if (!is_dir(ADDON_PATH . $name)) {
+            $this->error(__('Directory not found'));
+        }
+        $info = get_addon_info($name);
+        $config = get_addon_fullconfig($name);
+        if (!$info) {
+            $this->error(__('No Results were found'));
+        }
+        if ($this->request->isPost()) {
+            $params = $this->request->post("row/a", [], 'trim');
+            if ($params) {
+                foreach ($config as $k => &$v) {
+                    if (isset($params[$v['name']])) {
+                        if ($v['type'] == 'array') {
+                            $params[$v['name']] = is_array($params[$v['name']]) ? $params[$v['name']] : (array)json_decode($params[$v['name']], true);
+                            $value = $params[$v['name']];
+                        } else {
+                            $value = is_array($params[$v['name']]) ? implode(',', $params[$v['name']]) : $params[$v['name']];
+                        }
+                        $v['value'] = $value;
+                    }
+                }
+                try {
+                    //更新配置文件
+                    set_addon_fullconfig($name, $config);
+                    Service::refresh();
+                    $this->success();
+                } catch (Exception $e) {
+                    $this->error(__($e->getMessage()));
+                }
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        $tips = [];
+        foreach ($config as $index => &$item) {
+            if ($item['name'] == '__tips__') {
+                $tips = $item;
+                unset($config[$index]);
+            }
+        }
+        $this->view->assign("addon", ['info' => $info, 'config' => $config, 'tips' => $tips]);
+        $configFile = ADDON_PATH . $name . DS . 'config.html';
+        $viewFile = is_file($configFile) ? $configFile : '';
+        return $this->view->fetch($viewFile);
+    }
+
+    /**
+     * 安装
+     */
+    public function install()
+    {
+        $name = $this->request->post("name");
+        $force = (int)$this->request->post("force");
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+        try {
+            $uid = $this->request->post("uid");
+            $token = $this->request->post("token");
+            $version = $this->request->post("version");
+            $faversion = $this->request->post("faversion");
+            $extend = [
+                'uid'       => $uid,
+                'token'     => $token,
+                'version'   => $version,
+                'faversion' => $faversion
+            ];
+            Service::install($name, $force, $extend);
+            $info = get_addon_info($name);
+            $info['config'] = get_addon_config($name) ? 1 : 0;
+            $info['state'] = 1;
+            $this->success(__('Install successful'), null, ['addon' => $info]);
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()), $e->getCode());
+        }
+    }
+
+    /**
+     * 卸载
+     */
+    public function uninstall()
+    {
+        $name = $this->request->post("name");
+        $force = (int)$this->request->post("force");
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+        try {
+            Service::uninstall($name, $force);
+            $this->success(__('Uninstall successful'));
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+    }
+
+    /**
+     * 禁用启用
+     */
+    public function state()
+    {
+        $name = $this->request->post("name");
+        $action = $this->request->post("action");
+        $force = (int)$this->request->post("force");
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+        try {
+            $action = $action == 'enable' ? $action : 'disable';
+            //调用启用、禁用的方法
+            Service::$action($name, $force);
+            Cache::rm('__menu__');
+            $this->success(__('Operate successful'));
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+    }
+
+    /**
+     * 本地上传
+     */
+    public function local()
+    {
+        Config::set('default_return_type', 'json');
+
+        $file = $this->request->file('file');
+        $addonTmpDir = RUNTIME_PATH . 'addons' . DS;
+        if (!is_dir($addonTmpDir)) {
+            @mkdir($addonTmpDir, 0755, true);
+        }
+        $info = $file->rule('uniqid')->validate(['size' => 10240000, 'ext' => 'zip'])->move($addonTmpDir);
+        if ($info) {
+            $tmpName = substr($info->getFilename(), 0, stripos($info->getFilename(), '.'));
+            $tmpAddonDir = ADDON_PATH . $tmpName . DS;
+            $tmpFile = $addonTmpDir . $info->getSaveName();
+            try {
+                Service::unzip($tmpName);
+                unset($info);
+                @unlink($tmpFile);
+                $infoFile = $tmpAddonDir . 'info.ini';
+                if (!is_file($infoFile)) {
+                    throw new Exception(__('Addon info file was not found'));
+                }
+
+                $config = Config::parse($infoFile, '', $tmpName);
+                $name = isset($config['name']) ? $config['name'] : '';
+                if (!$name) {
+                    throw new Exception(__('Addon info file data incorrect'));
+                }
+                if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+                    throw new Exception(__('Addon name incorrect'));
+                }
+
+                $newAddonDir = ADDON_PATH . $name . DS;
+                if (is_dir($newAddonDir)) {
+                    throw new Exception(__('Addon already exists'));
+                }
+
+                //重命名插件文件夹
+                rename($tmpAddonDir, $newAddonDir);
+                try {
+                    //默认禁用该插件
+                    $info = get_addon_info($name);
+                    if ($info['state']) {
+                        $info['state'] = 0;
+                        set_addon_info($name, $info);
+                    }
+
+                    //执行插件的安装方法
+                    $class = get_addon_class($name);
+                    if (class_exists($class)) {
+                        $addon = new $class();
+                        $addon->install();
+                    }
+
+                    //导入SQL
+                    Service::importsql($name);
+
+                    $info['config'] = get_addon_config($name) ? 1 : 0;
+                    $this->success(__('Offline installed tips'), null, ['addon' => $info]);
+                } catch (Exception $e) {
+                    @rmdirs($newAddonDir);
+                    throw new Exception(__($e->getMessage()));
+                }
+            } catch (Exception $e) {
+                unset($info);
+                @unlink($tmpFile);
+                @rmdirs($tmpAddonDir);
+                $this->error(__($e->getMessage()));
+            }
+        } else {
+            // 上传失败获取错误信息
+            $this->error(__($file->getError()));
+        }
+    }
+
+    /**
+     * 更新插件
+     */
+    public function upgrade()
+    {
+        $name = $this->request->post("name");
+        $addonTmpDir = RUNTIME_PATH . 'addons' . DS;
+        if (!$name) {
+            $this->error(__('Parameter %s can not be empty', 'name'));
+        }
+        if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
+            $this->error(__('Addon name incorrect'));
+        }
+        if (!is_dir($addonTmpDir)) {
+            @mkdir($addonTmpDir, 0755, true);
+        }
+        try {
+            $uid = $this->request->post("uid");
+            $token = $this->request->post("token");
+            $version = $this->request->post("version");
+            $faversion = $this->request->post("faversion");
+            $extend = [
+                'uid'       => $uid,
+                'token'     => $token,
+                'version'   => $version,
+                'faversion' => $faversion
+            ];
+            //调用更新的方法
+            Service::upgrade($name, $extend);
+            Cache::rm('__menu__');
+            $this->success(__('Operate successful'));
+        } catch (AddonException $e) {
+            $this->result($e->getData(), $e->getCode(), __($e->getMessage()));
+        } catch (Exception $e) {
+            $this->error(__($e->getMessage()));
+        }
+    }
+
+    /**
+     * 已装插件
+     */
+    public function downloaded()
+    {
+        $offset = (int)$this->request->get("offset");
+        $limit = (int)$this->request->get("limit");
+        $filter = $this->request->get("filter");
+        $search = $this->request->get("search");
+        $search = htmlspecialchars(strip_tags($search));
+        $onlineaddons = Cache::get("onlineaddons");
+        if (!is_array($onlineaddons)) {
+            $onlineaddons = [];
+            $result = Http::sendRequest(config('fastadmin.api_url') . '/addon/index');
+            if ($result['ret']) {
+                $json = (array)json_decode($result['msg'], true);
+                $rows = isset($json['rows']) ? $json['rows'] : [];
+                foreach ($rows as $index => $row) {
+                    $onlineaddons[$row['name']] = $row;
+                }
+            }
+            Cache::set("onlineaddons", $onlineaddons, 600);
+        }
+        $filter = (array)json_decode($filter, true);
+        $addons = get_addon_list();
+        $list = [];
+        foreach ($addons as $k => $v) {
+            if ($search && stripos($v['name'], $search) === false && stripos($v['intro'], $search) === false) {
+                continue;
+            }
+
+            if (isset($onlineaddons[$v['name']])) {
+                $v = array_merge($v, $onlineaddons[$v['name']]);
+            } else {
+                $v['category_id'] = 0;
+                $v['flag'] = '';
+                $v['banner'] = '';
+                $v['image'] = '';
+                $v['donateimage'] = '';
+                $v['demourl'] = '';
+                $v['price'] = __('None');
+                $v['screenshots'] = [];
+                $v['releaselist'] = [];
+            }
+            $v['url'] = addon_url($v['name']);
+            $v['url'] = str_replace($this->request->server('SCRIPT_NAME'), '', $v['url']);
+            $v['createtime'] = filemtime(ADDON_PATH . $v['name']);
+            if ($filter && isset($filter['category_id']) && is_numeric($filter['category_id']) && $filter['category_id'] != $v['category_id']) {
+                continue;
+            }
+            $list[] = $v;
+        }
+        $total = count($list);
+        if ($limit) {
+            $list = array_slice($list, $offset, $limit);
+        }
+        $result = array("total" => $total, "rows" => $list);
+
+        $callback = $this->request->get('callback') ? "jsonp" : "json";
+        return $callback($result);
+    }
+}

+ 286 - 0
application/admin/controller/Ajax.php

@@ -0,0 +1,286 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use fast\Random;
+use think\addons\Service;
+use think\Cache;
+use think\Config;
+use think\Db;
+use think\Lang;
+
+/**
+ * Ajax异步请求接口
+ * @internal
+ */
+class Ajax extends Backend
+{
+
+    protected $noNeedLogin = ['lang'];
+    protected $noNeedRight = ['*'];
+    protected $layout = '';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+
+        //设置过滤方法
+        $this->request->filter(['strip_tags', 'htmlspecialchars']);
+    }
+
+    /**
+     * 加载语言包
+     */
+    public function lang()
+    {
+        header('Content-Type: application/javascript');
+        $controllername = input("controllername");
+        //默认只加载了控制器对应的语言名,你还根据控制器名来加载额外的语言包
+        $this->loadlang($controllername);
+        return jsonp(Lang::get(), 200, [], ['json_encode_param' => JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE]);
+    }
+
+    /**
+     * 上传文件
+     */
+    public function upload()
+    {
+        Config::set('default_return_type', 'json');
+        $file = $this->request->file('file');
+        if (empty($file)) {
+            $this->error(__('No file upload or server upload limit exceeded'));
+        }
+
+        //判断是否已经存在附件
+        $sha1 = $file->hash();
+        $extparam = $this->request->post();
+
+        $upload = Config::get('upload');
+
+        preg_match('/(\d+)(\w+)/', $upload['maxsize'], $matches);
+        $type = strtolower($matches[2]);
+        $typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
+        $size = (int)$upload['maxsize'] * pow(1024, isset($typeDict[$type]) ? $typeDict[$type] : 0);
+        $fileInfo = $file->getInfo();
+        $suffix = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
+        $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
+
+        $mimetypeArr = explode(',', strtolower($upload['mimetype']));
+        $typeArr = explode('/', $fileInfo['type']);
+
+        //禁止上传PHP和HTML文件
+        if (in_array($fileInfo['type'], ['text/x-php', 'text/html']) || in_array($suffix, ['php', 'html', 'htm'])) {
+            $this->error(__('Uploaded file format is limited'));
+        }
+        //验证文件后缀
+        if ($upload['mimetype'] !== '*' &&
+            (
+                !in_array($suffix, $mimetypeArr)
+                || (stripos($typeArr[0] . '/', $upload['mimetype']) !== false && (!in_array($fileInfo['type'], $mimetypeArr) && !in_array($typeArr[0] . '/*', $mimetypeArr)))
+            )
+        ) {
+            $this->error(__('Uploaded file format is limited'));
+        }
+        //验证是否为图片文件
+        $imagewidth = $imageheight = 0;
+        if (in_array($fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp']) || in_array($suffix, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
+            $imgInfo = getimagesize($fileInfo['tmp_name']);
+            if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
+                $this->error(__('Uploaded file is not a valid image'));
+            }
+            $imagewidth = isset($imgInfo[0]) ? $imgInfo[0] : $imagewidth;
+            $imageheight = isset($imgInfo[1]) ? $imgInfo[1] : $imageheight;
+        }
+        $replaceArr = [
+            '{year}'     => date("Y"),
+            '{mon}'      => date("m"),
+            '{day}'      => date("d"),
+            '{hour}'     => date("H"),
+            '{min}'      => date("i"),
+            '{sec}'      => date("s"),
+            '{random}'   => Random::alnum(16),
+            '{random32}' => Random::alnum(32),
+            '{filename}' => $suffix ? substr($fileInfo['name'], 0, strripos($fileInfo['name'], '.')) : $fileInfo['name'],
+            '{suffix}'   => $suffix,
+            '{.suffix}'  => $suffix ? '.' . $suffix : '',
+            '{filemd5}'  => md5_file($fileInfo['tmp_name']),
+        ];
+        $savekey = $upload['savekey'];
+        $savekey = str_replace(array_keys($replaceArr), array_values($replaceArr), $savekey);
+
+        $uploadDir = substr($savekey, 0, strripos($savekey, '/') + 1);
+        $fileName = substr($savekey, strripos($savekey, '/') + 1);
+        //
+        $splInfo = $file->validate(['size' => $size])->move(ROOT_PATH . '/public' . $uploadDir, $fileName);
+        if ($splInfo) {
+            $params = array(
+                'admin_id'    => (int)$this->auth->id,
+                'user_id'     => 0,
+                'filesize'    => $fileInfo['size'],
+                'imagewidth'  => $imagewidth,
+                'imageheight' => $imageheight,
+                'imagetype'   => $suffix,
+                'imageframes' => 0,
+                'mimetype'    => $fileInfo['type'],
+                'url'         => $uploadDir . $splInfo->getSaveName(),
+                'uploadtime'  => time(),
+                'storage'     => 'local',
+                'sha1'        => $sha1,
+                'extparam'    => json_encode($extparam),
+            );
+            $attachment = model("attachment");
+            $attachment->data(array_filter($params));
+            $attachment->save();
+            \think\Hook::listen("upload_after", $attachment);
+            $this->success(__('Upload successful'), null, [
+                'url' => $uploadDir . $splInfo->getSaveName()
+            ]);
+        } else {
+            // 上传失败获取错误信息
+            $this->error($file->getError());
+        }
+    }
+
+    /**
+     * 通用排序
+     */
+    public function weigh()
+    {
+        //排序的数组
+        $ids = $this->request->post("ids");
+        //拖动的记录ID
+        $changeid = $this->request->post("changeid");
+        //操作字段
+        $field = $this->request->post("field");
+        //操作的数据表
+        $table = $this->request->post("table");
+        //主键
+        $pk = $this->request->post("pk");
+        //排序的方式
+        $orderway = $this->request->post("orderway", "", 'strtolower');
+        $orderway = $orderway == 'asc' ? 'ASC' : 'DESC';
+        $sour = $weighdata = [];
+        $ids = explode(',', $ids);
+        $prikey = $pk ? $pk : (Db::name($table)->getPk() ?: 'id');
+        $pid = $this->request->post("pid");
+        //限制更新的字段
+        $field = in_array($field, ['weigh']) ? $field : 'weigh';
+
+        // 如果设定了pid的值,此时只匹配满足条件的ID,其它忽略
+        if ($pid !== '') {
+            $hasids = [];
+            $list = Db::name($table)->where($prikey, 'in', $ids)->where('pid', 'in', $pid)->field("{$prikey},pid")->select();
+            foreach ($list as $k => $v) {
+                $hasids[] = $v[$prikey];
+            }
+            $ids = array_values(array_intersect($ids, $hasids));
+        }
+
+        $list = Db::name($table)->field("$prikey,$field")->where($prikey, 'in', $ids)->order($field, $orderway)->select();
+        foreach ($list as $k => $v) {
+            $sour[] = $v[$prikey];
+            $weighdata[$v[$prikey]] = $v[$field];
+        }
+        $position = array_search($changeid, $ids);
+        $desc_id = $sour[$position];    //移动到目标的ID值,取出所处改变前位置的值
+        $sour_id = $changeid;
+        $weighids = array();
+        $temp = array_values(array_diff_assoc($ids, $sour));
+        foreach ($temp as $m => $n) {
+            if ($n == $sour_id) {
+                $offset = $desc_id;
+            } else {
+                if ($sour_id == $temp[0]) {
+                    $offset = isset($temp[$m + 1]) ? $temp[$m + 1] : $sour_id;
+                } else {
+                    $offset = isset($temp[$m - 1]) ? $temp[$m - 1] : $sour_id;
+                }
+            }
+            $weighids[$n] = $weighdata[$offset];
+            Db::name($table)->where($prikey, $n)->update([$field => $weighdata[$offset]]);
+        }
+        $this->success();
+    }
+
+    /**
+     * 清空系统缓存
+     */
+    public function wipecache()
+    {
+        $type = $this->request->request("type");
+        switch ($type) {
+            case 'all':
+            case 'content':
+                rmdirs(CACHE_PATH, false);
+                Cache::clear();
+                if ($type == 'content')
+                    break;
+            case 'template':
+                rmdirs(TEMP_PATH, false);
+                if ($type == 'template')
+                    break;
+            case 'addons':
+                Service::refresh();
+                if ($type == 'addons')
+                    break;
+        }
+
+        \think\Hook::listen("wipecache_after");
+        $this->success();
+    }
+
+    /**
+     * 读取分类数据,联动列表
+     */
+    public function category()
+    {
+        $type = $this->request->get('type');
+        $pid = $this->request->get('pid');
+        $where = ['status' => 'normal'];
+        $categorylist = null;
+        if ($pid !== '') {
+            if ($type) {
+                $where['type'] = $type;
+            }
+            if ($pid) {
+                $where['pid'] = $pid;
+            }
+
+            $categorylist = Db::name('category')->where($where)->field('id as value,name')->order('weigh desc,id desc')->select();
+        }
+        $this->success('', null, $categorylist);
+    }
+
+    /**
+     * 读取省市区数据,联动列表
+     */
+    public function area()
+    {
+        $params = $this->request->get("row/a");
+        if (!empty($params)) {
+            $province = isset($params['province']) ? $params['province'] : '';
+            $city = isset($params['city']) ? $params['city'] : null;
+        } else {
+            $province = $this->request->get('province');
+            $city = $this->request->get('city');
+        }
+        $where = ['pid' => 0, 'level' => 1];
+        $provincelist = null;
+        if ($province !== '') {
+            if ($province) {
+                $where['pid'] = $province;
+                $where['level'] = 2;
+            }
+            if ($city !== '') {
+                if ($city) {
+                    $where['pid'] = $city;
+                    $where['level'] = 3;
+                }
+                $provincelist = Db::name('area')->where($where)->field('id as value,name')->select();
+            }
+        }
+        $this->success('', null, $provincelist);
+    }
+
+}

+ 146 - 0
application/admin/controller/Category.php

@@ -0,0 +1,146 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use app\common\model\Category as CategoryModel;
+use fast\Tree;
+
+/**
+ * 分类管理
+ *
+ * @icon fa fa-list
+ * @remark 用于统一管理网站的所有分类,分类可进行无限级分类,分类类型请在常规管理->系统配置->字典配置中添加
+ */
+class Category extends Backend
+{
+
+    /**
+     * @var \app\common\model\Category
+     */
+    protected $model = null;
+    protected $categorylist = [];
+    protected $noNeedRight = ['selectpage'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('app\common\model\Category');
+
+        $tree = Tree::instance();
+        $tree->init(collection($this->model->order('weigh desc,id desc')->select())->toArray(), 'pid');
+        $this->categorylist = $tree->getTreeList($tree->getTreeArray(0), 'name');
+        $categorydata = [0 => ['type' => 'all', 'name' => __('None')]];
+        foreach ($this->categorylist as $k => $v) {
+            $categorydata[$v['id']] = $v;
+        }
+        $typeList = CategoryModel::getTypeList();
+        $this->view->assign("flagList", $this->model->getFlagList());
+        $this->view->assign("typeList", $typeList);
+        $this->view->assign("parentList", $categorydata);
+        $this->assignconfig('typeList', $typeList);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //设置过滤方法
+        $this->request->filter(['strip_tags']);
+        if ($this->request->isAjax()) {
+            $search = $this->request->request("search");
+            $type = $this->request->request("type");
+
+            //构造父类select列表选项数据
+            $list = [];
+
+            foreach ($this->categorylist as $k => $v) {
+                if ($search) {
+                    if ($v['type'] == $type && stripos($v['name'], $search) !== false || stripos($v['nickname'], $search) !== false) {
+                        if ($type == "all" || $type == null) {
+                            $list = $this->categorylist;
+                        } else {
+                            $list[] = $v;
+                        }
+                    }
+                } else {
+                    if ($type == "all" || $type == null) {
+                        $list = $this->categorylist;
+                    } elseif ($v['type'] == $type) {
+                        $list[] = $v;
+                    }
+                }
+            }
+
+            $total = count($list);
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = null)
+    {
+        $row = $this->model->get($ids);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        $adminIds = $this->getDataLimitAdminIds();
+        if (is_array($adminIds)) {
+            if (!in_array($row[$this->dataLimitField], $adminIds)) {
+                $this->error(__('You have no permission'));
+            }
+        }
+        if ($this->request->isPost()) {
+            $params = $this->request->post("row/a");
+            if ($params) {
+                $params = $this->preExcludeFields($params);
+
+                if ($params['pid'] != $row['pid']) {
+                    $childrenIds = Tree::instance()->init(collection(\app\common\model\Category::select())->toArray())->getChildrenIds($row['id']);
+                    if (in_array($params['pid'], $childrenIds)) {
+                        $this->error(__('Can not change the parent to child'));
+                    }
+                }
+
+                try {
+                    //是否采用模型验证
+                    if ($this->modelValidate) {
+                        $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                        $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate;
+                        $row->validate($validate);
+                    }
+                    $result = $row->allowField(true)->save($params);
+                    if ($result !== false) {
+                        $this->success();
+                    } else {
+                        $this->error($row->getError());
+                    }
+                } catch (\think\exception\PDOException $e) {
+                    $this->error($e->getMessage());
+                } catch (\think\Exception $e) {
+                    $this->error($e->getMessage());
+                }
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        $this->view->assign("row", $row);
+        return $this->view->fetch();
+    }
+
+
+    /**
+     * Selectpage搜索
+     *
+     * @internal
+     */
+    public function selectpage()
+    {
+        return parent::selectpage();
+    }
+}

+ 56 - 0
application/admin/controller/Dashboard.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use think\Config;
+
+/**
+ * 控制台
+ *
+ * @icon fa fa-dashboard
+ * @remark 用于展示当前系统中的统计数据、统计报表及重要实时数据
+ */
+class Dashboard extends Backend
+{
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $seventtime = \fast\Date::unixtime('day', -7);
+        $paylist = $createlist = [];
+        for ($i = 0; $i < 7; $i++)
+        {
+            $day = date("Y-m-d", $seventtime + ($i * 86400));
+            $createlist[$day] = mt_rand(20, 200);
+            $paylist[$day] = mt_rand(1, mt_rand(1, $createlist[$day]));
+        }
+        $hooks = config('addons.hooks');
+        $uploadmode = isset($hooks['upload_config_init']) && $hooks['upload_config_init'] ? implode(',', $hooks['upload_config_init']) : 'local';
+        $addonComposerCfg = ROOT_PATH . '/vendor/karsonzhang/fastadmin-addons/composer.json';
+        Config::parse($addonComposerCfg, "json", "composer");
+        $config = Config::get("composer");
+        $addonVersion = isset($config['version']) ? $config['version'] : __('Unknown');
+        $this->view->assign([
+            'totaluser'        => 35200,
+            'totalviews'       => 219390,
+            'totalorder'       => 32143,
+            'totalorderamount' => 174800,
+            'todayuserlogin'   => 321,
+            'todayusersignup'  => 430,
+            'todayorder'       => 2324,
+            'unsettleorder'    => 132,
+            'sevendnu'         => '80%',
+            'sevendau'         => '32%',
+            'paylist'          => $paylist,
+            'createlist'       => $createlist,
+            'addonversion'       => $addonVersion,
+            'uploadmode'       => $uploadmode
+        ]);
+
+        return $this->view->fetch();
+    }
+
+}

+ 122 - 0
application/admin/controller/Index.php

@@ -0,0 +1,122 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\model\AdminLog;
+use app\common\controller\Backend;
+use think\Config;
+use think\Hook;
+use think\Validate;
+
+/**
+ * 后台首页
+ * @internal
+ */
+class Index extends Backend
+{
+
+    protected $noNeedLogin = ['login'];
+    protected $noNeedRight = ['index', 'logout'];
+    protected $layout = '';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        //移除HTML标签
+        $this->request->filter('trim,strip_tags,htmlspecialchars');
+    }
+
+    /**
+     * 后台首页
+     */
+    public function index()
+    {
+        //左侧菜单
+        list($menulist, $navlist, $fixedmenu, $referermenu) = $this->auth->getSidebar([
+            'dashboard' => 'hot',
+            'addon'     => ['new', 'red', 'badge'],
+            'auth/rule' => __('Menu'),
+            'general'   => ['new', 'purple'],
+        ], $this->view->site['fixedpage']);
+        $action = $this->request->request('action');
+        if ($this->request->isPost()) {
+            if ($action == 'refreshmenu') {
+                $this->success('', null, ['menulist' => $menulist, 'navlist' => $navlist]);
+            }
+        }
+        $this->view->assign('menulist', $menulist);
+        $this->view->assign('navlist', $navlist);
+        $this->view->assign('fixedmenu', $fixedmenu);
+        $this->view->assign('referermenu', $referermenu);
+        $this->view->assign('title', __('Home'));
+        return $this->view->fetch();
+    }
+
+    /**
+     * 管理员登录
+     */
+    public function login()
+    {
+        $url = $this->request->get('url', 'index/index');
+        if ($this->auth->isLogin()) {
+            $this->success(__("You've logged in, do not login again"), $url);
+        }
+        if ($this->request->isPost()) {
+            $username = $this->request->post('username');
+            $password = $this->request->post('password');
+            $keeplogin = $this->request->post('keeplogin');
+            $token = $this->request->post('__token__');
+            $rule = [
+                'username'  => 'require|length:3,30',
+                'password'  => 'require|length:3,30',
+                '__token__' => 'require|token',
+            ];
+            $data = [
+                'username'  => $username,
+                'password'  => $password,
+                '__token__' => $token,
+            ];
+            if (Config::get('fastadmin.login_captcha')) {
+                $rule['captcha'] = 'require|captcha';
+                $data['captcha'] = $this->request->post('captcha');
+            }
+            $validate = new Validate($rule, [], ['username' => __('Username'), 'password' => __('Password'), 'captcha' => __('Captcha')]);
+            $result = $validate->check($data);
+            if (!$result) {
+                $this->error($validate->getError(), $url, ['token' => $this->request->token()]);
+            }
+            AdminLog::setTitle(__('Login'));
+            $result = $this->auth->login($username, $password, $keeplogin ? 86400 : 0);
+            if ($result === true) {
+                Hook::listen("admin_login_after", $this->request);
+                $this->success(__('Login successful'), $url, ['url' => $url, 'id' => $this->auth->id, 'username' => $username, 'avatar' => $this->auth->avatar]);
+            } else {
+                $msg = $this->auth->getError();
+                $msg = $msg ? $msg : __('Username or password is incorrect');
+                $this->error($msg, $url, ['token' => $this->request->token()]);
+            }
+        }
+
+        // 根据客户端的cookie,判断是否可以自动登录
+        if ($this->auth->autologin()) {
+            $this->redirect($url);
+        }
+        $background = Config::get('fastadmin.login_background');
+        $background = stripos($background, 'http') === 0 ? $background : config('site.cdnurl') . $background;
+        $this->view->assign('background', $background);
+        $this->view->assign('title', __('Login'));
+        Hook::listen("admin_login_init", $this->request);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 注销登录
+     */
+    public function logout()
+    {
+        $this->auth->logout();
+        Hook::listen("admin_logout_after", $this->request);
+        $this->success(__('Logout successful'), 'index/login');
+    }
+
+}

+ 263 - 0
application/admin/controller/auth/Admin.php

@@ -0,0 +1,263 @@
+<?php
+
+namespace app\admin\controller\auth;
+
+use app\admin\model\AuthGroup;
+use app\admin\model\AuthGroupAccess;
+use app\common\controller\Backend;
+use fast\Random;
+use fast\Tree;
+use think\Validate;
+
+/**
+ * 管理员管理
+ *
+ * @icon fa fa-users
+ * @remark 一个管理员可以有多个角色组,左侧的菜单根据管理员所拥有的权限进行生成
+ */
+class Admin extends Backend
+{
+
+    /**
+     * @var \app\admin\model\Admin
+     */
+    protected $model = null;
+    protected $childrenGroupIds = [];
+    protected $childrenAdminIds = [];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('Admin');
+
+        $this->childrenAdminIds = $this->auth->getChildrenAdminIds(true);
+        $this->childrenGroupIds = $this->auth->getChildrenGroupIds(true);
+
+        $groupList = collection(AuthGroup::where('id', 'in', $this->childrenGroupIds)->select())->toArray();
+
+        Tree::instance()->init($groupList);
+        $groupdata = [];
+        if ($this->auth->isSuperAdmin()) {
+            $result = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0));
+            foreach ($result as $k => $v) {
+                $groupdata[$v['id']] = $v['name'];
+            }
+        } else {
+            $result = [];
+            $groups = $this->auth->getGroups();
+            foreach ($groups as $m => $n) {
+                $childlist = Tree::instance()->getTreeList(Tree::instance()->getTreeArray($n['id']));
+                $temp = [];
+                foreach ($childlist as $k => $v) {
+                    $temp[$v['id']] = $v['name'];
+                }
+                $result[__($n['name'])] = $temp;
+            }
+            $groupdata = $result;
+        }
+
+        $this->view->assign('groupdata', $groupdata);
+        $this->assignconfig("admin", ['id' => $this->auth->id]);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if ($this->request->isAjax()) {
+            //如果发送的来源是Selectpage,则转发到Selectpage
+            if ($this->request->request('keyField')) {
+                return $this->selectpage();
+            }
+            $childrenGroupIds = $this->childrenGroupIds;
+            $groupName = AuthGroup::where('id', 'in', $childrenGroupIds)
+                ->column('id,name');
+            $authGroupList = AuthGroupAccess::where('group_id', 'in', $childrenGroupIds)
+                ->field('uid,group_id')
+                ->select();
+
+            $adminGroupName = [];
+            foreach ($authGroupList as $k => $v) {
+                if (isset($groupName[$v['group_id']])) {
+                    $adminGroupName[$v['uid']][$v['group_id']] = $groupName[$v['group_id']];
+                }
+            }
+            $groups = $this->auth->getGroups();
+            foreach ($groups as $m => $n) {
+                $adminGroupName[$this->auth->id][$n['id']] = $n['name'];
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                ->where($where)
+                ->where('id', 'in', $this->childrenAdminIds)
+                ->order($sort, $order)
+                ->count();
+
+            $list = $this->model
+                ->where($where)
+                ->where('id', 'in', $this->childrenAdminIds)
+                ->field(['password', 'salt', 'token'], true)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+            foreach ($list as $k => &$v) {
+                $groups = isset($adminGroupName[$v['id']]) ? $adminGroupName[$v['id']] : [];
+                $v['groups'] = implode(',', array_keys($groups));
+                $v['groups_text'] = implode(',', array_values($groups));
+            }
+            unset($v);
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a");
+            if ($params) {
+                if (!Validate::is($params['password'], '\S{6,16}')) {
+                    $this->error(__("Please input correct password"));
+                }
+                $params['salt'] = Random::alnum();
+                $params['password'] = md5(md5($params['password']) . $params['salt']);
+                $params['avatar'] = '/assets/img/avatar.png'; //设置新管理员默认头像。
+                $result = $this->model->validate('Admin.add')->save($params);
+                if ($result === false) {
+                    $this->error($this->model->getError());
+                }
+                $group = $this->request->post("group/a");
+
+                //过滤不允许的组别,避免越权
+                $group = array_intersect($this->childrenGroupIds, $group);
+                $dataset = [];
+                foreach ($group as $value) {
+                    $dataset[] = ['uid' => $this->model->id, 'group_id' => $value];
+                }
+                model('AuthGroupAccess')->saveAll($dataset);
+                $this->success();
+            }
+            $this->error();
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = null)
+    {
+        $row = $this->model->get(['id' => $ids]);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        if (!in_array($row->id, $this->childrenAdminIds)) {
+            $this->error(__('You have no permission'));
+        }
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a");
+            if ($params) {
+                if ($params['password']) {
+                    if (!Validate::is($params['password'], '\S{6,16}')) {
+                        $this->error(__("Please input correct password"));
+                    }
+                    $params['salt'] = Random::alnum();
+                    $params['password'] = md5(md5($params['password']) . $params['salt']);
+                } else {
+                    unset($params['password'], $params['salt']);
+                }
+                //这里需要针对username和email做唯一验证
+                $adminValidate = \think\Loader::validate('Admin');
+                $adminValidate->rule([
+                    'username' => 'require|regex:\w{3,12}|unique:admin,username,' . $row->id,
+                    'email'    => 'require|email|unique:admin,email,' . $row->id,
+                    'password' => 'regex:\S{32}',
+                ]);
+                $result = $row->validate('Admin.edit')->save($params);
+                if ($result === false) {
+                    $this->error($row->getError());
+                }
+
+                // 先移除所有权限
+                model('AuthGroupAccess')->where('uid', $row->id)->delete();
+
+                $group = $this->request->post("group/a");
+
+                // 过滤不允许的组别,避免越权
+                $group = array_intersect($this->childrenGroupIds, $group);
+
+                $dataset = [];
+                foreach ($group as $value) {
+                    $dataset[] = ['uid' => $row->id, 'group_id' => $value];
+                }
+                model('AuthGroupAccess')->saveAll($dataset);
+                $this->success();
+            }
+            $this->error();
+        }
+        $grouplist = $this->auth->getGroups($row['id']);
+        $groupids = [];
+        foreach ($grouplist as $k => $v) {
+            $groupids[] = $v['id'];
+        }
+        $this->view->assign("row", $row);
+        $this->view->assign("groupids", $groupids);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 删除
+     */
+    public function del($ids = "")
+    {
+        if ($ids) {
+            $ids = array_intersect($this->childrenAdminIds, array_filter(explode(',', $ids)));
+            // 避免越权删除管理员
+            $childrenGroupIds = $this->childrenGroupIds;
+            $adminList = $this->model->where('id', 'in', $ids)->where('id', 'in', function ($query) use ($childrenGroupIds) {
+                $query->name('auth_group_access')->where('group_id', 'in', $childrenGroupIds)->field('uid');
+            })->select();
+            if ($adminList) {
+                $deleteIds = [];
+                foreach ($adminList as $k => $v) {
+                    $deleteIds[] = $v->id;
+                }
+                $deleteIds = array_values(array_diff($deleteIds, [$this->auth->id]));
+                if ($deleteIds) {
+                    $this->model->destroy($deleteIds);
+                    model('AuthGroupAccess')->where('uid', 'in', $deleteIds)->delete();
+                    $this->success();
+                }
+            }
+        }
+        $this->error(__('You have no permission'));
+    }
+
+    /**
+     * 批量更新
+     * @internal
+     */
+    public function multi($ids = "")
+    {
+        // 管理员禁止批量操作
+        $this->error();
+    }
+
+    /**
+     * 下拉搜索
+     */
+    public function selectpage()
+    {
+        $this->dataLimit = 'auth';
+        $this->dataLimitField = 'id';
+        return parent::selectpage();
+    }
+}

+ 138 - 0
application/admin/controller/auth/Adminlog.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace app\admin\controller\auth;
+
+use app\admin\model\AuthGroup;
+use app\common\controller\Backend;
+
+/**
+ * 管理员日志
+ *
+ * @icon fa fa-users
+ * @remark 管理员可以查看自己所拥有的权限的管理员日志
+ */
+class Adminlog extends Backend
+{
+
+    /**
+     * @var \app\admin\model\AdminLog
+     */
+    protected $model = null;
+    protected $childrenGroupIds = [];
+    protected $childrenAdminIds = [];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('AdminLog');
+
+        $this->childrenAdminIds = $this->auth->getChildrenAdminIds(true);
+        $this->childrenGroupIds = $this->auth->getChildrenGroupIds($this->auth->isSuperAdmin() ? true : false);
+
+        $groupName = AuthGroup::where('id', 'in', $this->childrenGroupIds)
+                ->column('id,name');
+
+        $this->view->assign('groupdata', $groupName);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if ($this->request->isAjax())
+        {
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                    ->where($where)
+                    ->where('admin_id', 'in', $this->childrenAdminIds)
+                    ->order($sort, $order)
+                    ->count();
+
+            $list = $this->model
+                    ->where($where)
+                    ->where('admin_id', 'in', $this->childrenAdminIds)
+                    ->order($sort, $order)
+                    ->limit($offset, $limit)
+                    ->select();
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 详情
+     */
+    public function detail($ids)
+    {
+        $row = $this->model->get(['id' => $ids]);
+        if (!$row)
+            $this->error(__('No Results were found'));
+        $this->view->assign("row", $row->toArray());
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     * @internal
+     */
+    public function add()
+    {
+        $this->error();
+    }
+
+    /**
+     * 编辑
+     * @internal
+     */
+    public function edit($ids = NULL)
+    {
+        $this->error();
+    }
+
+    /**
+     * 删除
+     */
+    public function del($ids = "")
+    {
+        if ($ids)
+        {
+            $childrenGroupIds = $this->childrenGroupIds;
+            $adminList = $this->model->where('id', 'in', $ids)->where('admin_id', 'in', function($query) use($childrenGroupIds) {
+                        $query->name('auth_group_access')->field('uid');
+                    })->select();
+            if ($adminList)
+            {
+                $deleteIds = [];
+                foreach ($adminList as $k => $v)
+                {
+                    $deleteIds[] = $v->id;
+                }
+                if ($deleteIds)
+                {
+                    $this->model->destroy($deleteIds);
+                    $this->success();
+                }
+            }
+        }
+        $this->error();
+    }
+
+    /**
+     * 批量更新
+     * @internal
+     */
+    public function multi($ids = "")
+    {
+        // 管理员禁止批量操作
+        $this->error();
+    }
+    
+    public function selectpage()
+    {
+        return parent::selectpage();
+    }
+
+}

+ 293 - 0
application/admin/controller/auth/Group.php

@@ -0,0 +1,293 @@
+<?php
+
+namespace app\admin\controller\auth;
+
+use app\admin\model\AuthGroup;
+use app\common\controller\Backend;
+use fast\Tree;
+
+/**
+ * 角色组
+ *
+ * @icon fa fa-group
+ * @remark 角色组可以有多个,角色有上下级层级关系,如果子角色有角色组和管理员的权限则可以派生属于自己组别下级的角色组或管理员
+ */
+class Group extends Backend
+{
+
+    /**
+     * @var \app\admin\model\AuthGroup
+     */
+    protected $model = null;
+    //当前登录管理员所有子组别
+    protected $childrenGroupIds = [];
+    //当前组别列表数据
+    protected $groupdata = [];
+    //无需要权限判断的方法
+    protected $noNeedRight = ['roletree'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('AuthGroup');
+
+        $this->childrenGroupIds = $this->auth->getChildrenGroupIds(true);
+
+        $groupList = collection(AuthGroup::where('id', 'in', $this->childrenGroupIds)->select())->toArray();
+
+        Tree::instance()->init($groupList);
+        $result = [];
+        if ($this->auth->isSuperAdmin()) {
+            $result = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0));
+        } else {
+            $groups = $this->auth->getGroups();
+            foreach ($groups as $m => $n) {
+                $result = array_merge($result, Tree::instance()->getTreeList(Tree::instance()->getTreeArray($n['pid'])));
+            }
+        }
+        $groupName = [];
+        foreach ($result as $k => $v) {
+            $groupName[$v['id']] = $v['name'];
+        }
+
+        $this->groupdata = $groupName;
+        $this->assignconfig("admin", ['id' => $this->auth->id, 'group_ids' => $this->auth->getGroupIds()]);
+
+        $this->view->assign('groupdata', $this->groupdata);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if ($this->request->isAjax()) {
+            $list = AuthGroup::all(array_keys($this->groupdata));
+            $list = collection($list)->toArray();
+            $groupList = [];
+            foreach ($list as $k => $v) {
+                $groupList[$v['id']] = $v;
+            }
+            $list = [];
+            foreach ($this->groupdata as $k => $v) {
+                if (isset($groupList[$k])) {
+                    $groupList[$k]['name'] = $v;
+                    $list[] = $groupList[$k];
+                }
+            }
+            $total = count($list);
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a", [], 'strip_tags');
+            $params['rules'] = explode(',', $params['rules']);
+            if (!in_array($params['pid'], $this->childrenGroupIds)) {
+                $this->error(__('The parent group can not be its own child'));
+            }
+            $parentmodel = model("AuthGroup")->get($params['pid']);
+            if (!$parentmodel) {
+                $this->error(__('The parent group can not found'));
+            }
+            // 父级别的规则节点
+            $parentrules = explode(',', $parentmodel->rules);
+            // 当前组别的规则节点
+            $currentrules = $this->auth->getRuleIds();
+            $rules = $params['rules'];
+            // 如果父组不是超级管理员则需要过滤规则节点,不能超过父组别的权限
+            $rules = in_array('*', $parentrules) ? $rules : array_intersect($parentrules, $rules);
+            // 如果当前组别不是超级管理员则需要过滤规则节点,不能超当前组别的权限
+            $rules = in_array('*', $currentrules) ? $rules : array_intersect($currentrules, $rules);
+            $params['rules'] = implode(',', $rules);
+            if ($params) {
+                $this->model->create($params);
+                $this->success();
+            }
+            $this->error();
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = null)
+    {
+        $row = $this->model->get(['id' => $ids]);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a", [], 'strip_tags');
+            // 父节点不能是它自身的子节点
+            if (!in_array($params['pid'], $this->childrenGroupIds)) {
+                $this->error(__('The parent group can not be its own child'));
+            }
+            $params['rules'] = explode(',', $params['rules']);
+
+            $parentmodel = model("AuthGroup")->get($params['pid']);
+            if (!$parentmodel) {
+                $this->error(__('The parent group can not found'));
+            }
+            // 父级别的规则节点
+            $parentrules = explode(',', $parentmodel->rules);
+            // 当前组别的规则节点
+            $currentrules = $this->auth->getRuleIds();
+            $rules = $params['rules'];
+            // 如果父组不是超级管理员则需要过滤规则节点,不能超过父组别的权限
+            $rules = in_array('*', $parentrules) ? $rules : array_intersect($parentrules, $rules);
+            // 如果当前组别不是超级管理员则需要过滤规则节点,不能超当前组别的权限
+            $rules = in_array('*', $currentrules) ? $rules : array_intersect($currentrules, $rules);
+            $params['rules'] = implode(',', $rules);
+            if ($params) {
+                $row->save($params);
+                $this->success();
+            }
+            $this->error();
+            return;
+        }
+        $this->view->assign("row", $row);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 删除
+     */
+    public function del($ids = "")
+    {
+        if ($ids) {
+            $ids = explode(',', $ids);
+            $grouplist = $this->auth->getGroups();
+            $group_ids = array_map(function ($group) {
+                return $group['id'];
+            }, $grouplist);
+            // 移除掉当前管理员所在组别
+            $ids = array_diff($ids, $group_ids);
+
+            // 循环判断每一个组别是否可删除
+            $grouplist = $this->model->where('id', 'in', $ids)->select();
+            $groupaccessmodel = model('AuthGroupAccess');
+            foreach ($grouplist as $k => $v) {
+                // 当前组别下有管理员
+                $groupone = $groupaccessmodel->get(['group_id' => $v['id']]);
+                if ($groupone) {
+                    $ids = array_diff($ids, [$v['id']]);
+                    continue;
+                }
+                // 当前组别下有子组别
+                $groupone = $this->model->get(['pid' => $v['id']]);
+                if ($groupone) {
+                    $ids = array_diff($ids, [$v['id']]);
+                    continue;
+                }
+            }
+            if (!$ids) {
+                $this->error(__('You can not delete group that contain child group and administrators'));
+            }
+            $count = $this->model->where('id', 'in', $ids)->delete();
+            if ($count) {
+                $this->success();
+            }
+        }
+        $this->error();
+    }
+
+    /**
+     * 批量更新
+     * @internal
+     */
+    public function multi($ids = "")
+    {
+        // 组别禁止批量操作
+        $this->error();
+    }
+
+    /**
+     * 读取角色权限树
+     *
+     * @internal
+     */
+    public function roletree()
+    {
+        $this->loadlang('auth/group');
+
+        $model = model('AuthGroup');
+        $id = $this->request->post("id");
+        $pid = $this->request->post("pid");
+        $parentGroupModel = $model->get($pid);
+        $currentGroupModel = null;
+        if ($id) {
+            $currentGroupModel = $model->get($id);
+        }
+        if (($pid || $parentGroupModel) && (!$id || $currentGroupModel)) {
+            $id = $id ? $id : null;
+            $ruleList = collection(model('AuthRule')->order('weigh', 'desc')->order('id', 'asc')->select())->toArray();
+            //读取父类角色所有节点列表
+            $parentRuleList = [];
+            if (in_array('*', explode(',', $parentGroupModel->rules))) {
+                $parentRuleList = $ruleList;
+            } else {
+                $parentRuleIds = explode(',', $parentGroupModel->rules);
+                foreach ($ruleList as $k => $v) {
+                    if (in_array($v['id'], $parentRuleIds)) {
+                        $parentRuleList[] = $v;
+                    }
+                }
+            }
+
+            $ruleTree = new Tree();
+            $groupTree = new Tree();
+            //当前所有正常规则列表
+            $ruleTree->init($parentRuleList);
+            //角色组列表
+            $groupTree->init(collection(model('AuthGroup')->where('id', 'in', $this->childrenGroupIds)->select())->toArray());
+
+            //读取当前角色下规则ID集合
+            $adminRuleIds = $this->auth->getRuleIds();
+            //是否是超级管理员
+            $superadmin = $this->auth->isSuperAdmin();
+            //当前拥有的规则ID集合
+            $currentRuleIds = $id ? explode(',', $currentGroupModel->rules) : [];
+
+            if (!$id || !in_array($pid, $this->childrenGroupIds) || !in_array($pid, $groupTree->getChildrenIds($id, true))) {
+                $parentRuleList = $ruleTree->getTreeList($ruleTree->getTreeArray(0), 'name');
+                $hasChildrens = [];
+                foreach ($parentRuleList as $k => $v) {
+                    if ($v['haschild']) {
+                        $hasChildrens[] = $v['id'];
+                    }
+                }
+                $parentRuleIds = array_map(function ($item) {
+                    return $item['id'];
+                }, $parentRuleList);
+                $nodeList = [];
+                foreach ($parentRuleList as $k => $v) {
+                    if (!$superadmin && !in_array($v['id'], $adminRuleIds)) {
+                        continue;
+                    }
+                    if ($v['pid'] && !in_array($v['pid'], $parentRuleIds)) {
+                        continue;
+                    }
+                    $state = array('selected' => in_array($v['id'], $currentRuleIds) && !in_array($v['id'], $hasChildrens));
+                    $nodeList[] = array('id' => $v['id'], 'parent' => $v['pid'] ? $v['pid'] : '#', 'text' => __($v['title']), 'type' => 'menu', 'state' => $state);
+                }
+                $this->success('', null, $nodeList);
+            } else {
+                $this->error(__('Can not change the parent to child'));
+            }
+        } else {
+            $this->error(__('Group not found'));
+        }
+    }
+}

+ 149 - 0
application/admin/controller/auth/Rule.php

@@ -0,0 +1,149 @@
+<?php
+
+namespace app\admin\controller\auth;
+
+use app\admin\model\AuthRule;
+use app\common\controller\Backend;
+use fast\Tree;
+use think\Cache;
+
+/**
+ * 规则管理
+ *
+ * @icon fa fa-list
+ * @remark 规则通常对应一个控制器的方法,同时左侧的菜单栏数据也从规则中体现,通常建议通过控制台进行生成规则节点
+ */
+class Rule extends Backend
+{
+
+    /**
+     * @var \app\admin\model\AuthRule
+     */
+    protected $model = null;
+    protected $rulelist = [];
+    protected $multiFields = 'ismenu,status';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('AuthRule');
+        // 必须将结果集转换为数组
+        $ruleList = collection($this->model->order('weigh', 'desc')->order('id', 'asc')->select())->toArray();
+        foreach ($ruleList as $k => &$v) {
+            $v['title'] = __($v['title']);
+            $v['remark'] = __($v['remark']);
+        }
+        unset($v);
+        Tree::instance()->init($ruleList);
+        $this->rulelist = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0), 'title');
+        $ruledata = [0 => __('None')];
+        foreach ($this->rulelist as $k => &$v) {
+            if (!$v['ismenu']) {
+                continue;
+            }
+            $ruledata[$v['id']] = $v['title'];
+        }
+        unset($v);
+        $this->view->assign('ruledata', $ruledata);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if ($this->request->isAjax()) {
+            $list = $this->rulelist;
+            $total = count($this->rulelist);
+
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a", [], 'strip_tags');
+            if ($params) {
+                if (!$params['ismenu'] && !$params['pid']) {
+                    $this->error(__('The non-menu rule must have parent'));
+                }
+                $result = $this->model->validate()->save($params);
+                if ($result === false) {
+                    $this->error($this->model->getError());
+                }
+                Cache::rm('__menu__');
+                $this->success();
+            }
+            $this->error();
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = null)
+    {
+        $row = $this->model->get(['id' => $ids]);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a", [], 'strip_tags');
+            if ($params) {
+                if (!$params['ismenu'] && !$params['pid']) {
+                    $this->error(__('The non-menu rule must have parent'));
+                }
+                if ($params['pid'] != $row['pid']) {
+                    $childrenIds = Tree::instance()->init(collection(AuthRule::select())->toArray())->getChildrenIds($row['id']);
+                    if (in_array($params['pid'], $childrenIds)) {
+                        $this->error(__('Can not change the parent to child'));
+                    }
+                }
+                //这里需要针对name做唯一验证
+                $ruleValidate = \think\Loader::validate('AuthRule');
+                $ruleValidate->rule([
+                    'name' => 'require|format|unique:AuthRule,name,' . $row->id,
+                ]);
+                $result = $row->validate()->save($params);
+                if ($result === false) {
+                    $this->error($row->getError());
+                }
+                Cache::rm('__menu__');
+                $this->success();
+            }
+            $this->error();
+        }
+        $this->view->assign("row", $row);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 删除
+     */
+    public function del($ids = "")
+    {
+        if ($ids) {
+            $delIds = [];
+            foreach (explode(',', $ids) as $k => $v) {
+                $delIds = array_merge($delIds, Tree::instance()->getChildrenIds($v, true));
+            }
+            $delIds = array_unique($delIds);
+            $count = $this->model->where('id', 'in', $delIds)->delete();
+            if ($count) {
+                Cache::rm('__menu__');
+                $this->success();
+            }
+        }
+        $this->error();
+    }
+}

+ 118 - 0
application/admin/controller/general/Attachment.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace app\admin\controller\general;
+
+use app\common\controller\Backend;
+
+/**
+ * 附件管理
+ *
+ * @icon fa fa-circle-o
+ * @remark 主要用于管理上传到又拍云的数据或上传至本服务的上传数据
+ */
+class Attachment extends Backend
+{
+
+    /**
+     * @var \app\common\model\Attachment
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('Attachment');
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //设置过滤方法
+        $this->request->filter(['strip_tags']);
+        if ($this->request->isAjax()) {
+            $mimetypeQuery = [];
+            $filter = $this->request->request('filter');
+            $filterArr = (array)json_decode($filter, TRUE);
+            if (isset($filterArr['mimetype']) && stripos($filterArr['mimetype'], ',') !== false) {
+                $this->request->get(['filter' => json_encode(array_merge($filterArr, ['mimetype' => '']))]);
+                $mimetypeQuery = function ($query) use ($filterArr) {
+                    $mimetypeArr = explode(',', $filterArr['mimetype']);
+                    foreach ($mimetypeArr as $index => $item) {
+                        $query->whereOr('mimetype', 'like', '%' . $item . '%');
+                    }
+                };
+            }
+
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                ->where($mimetypeQuery)
+                ->where($where)
+                ->order($sort, $order)
+                ->count();
+
+            $list = $this->model
+                ->where($mimetypeQuery)
+                ->where($where)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+            $cdnurl = preg_replace("/\/(\w+)\.php$/i", '', $this->request->root());
+            foreach ($list as $k => &$v) {
+                $v['fullurl'] = ($v['storage'] == 'local' ? $cdnurl : $this->view->config['upload']['cdnurl']) . $v['url'];
+            }
+            unset($v);
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 选择附件
+     */
+    public function select()
+    {
+        if ($this->request->isAjax()) {
+            return $this->index();
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isAjax()) {
+            $this->error();
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 删除附件
+     * @param array $ids
+     */
+    public function del($ids = "")
+    {
+        if ($ids) {
+            \think\Hook::add('upload_delete', function ($params) {
+                $attachmentFile = ROOT_PATH . '/public' . $params['url'];
+                if (is_file($attachmentFile)) {
+                    @unlink($attachmentFile);
+                }
+            });
+            $attachmentlist = $this->model->where('id', 'in', $ids)->select();
+            foreach ($attachmentlist as $attachment) {
+                \think\Hook::listen("upload_delete", $attachment);
+                $attachment->delete();
+            }
+            $this->success();
+        }
+        $this->error(__('Parameter %s can not be empty', 'ids'));
+    }
+
+}

+ 259 - 0
application/admin/controller/general/Config.php

@@ -0,0 +1,259 @@
+<?php
+
+namespace app\admin\controller\general;
+
+use app\common\controller\Backend;
+use app\common\library\Email;
+use app\common\model\Config as ConfigModel;
+use think\Exception;
+use think\Validate;
+
+/**
+ * 系统配置
+ *
+ * @icon fa fa-cogs
+ * @remark 可以在此增改系统的变量和分组,也可以自定义分组和变量,如果需要删除请从数据库中删除
+ */
+class Config extends Backend
+{
+
+    /**
+     * @var \app\common\model\Config
+     */
+    protected $model = null;
+    protected $noNeedRight = ['check', 'rulelist'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('Config');
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $siteList = [];
+        $groupList = ConfigModel::getGroupList();
+        foreach ($groupList as $k => $v) {
+            $siteList[$k]['name'] = $k;
+            $siteList[$k]['title'] = $v;
+            $siteList[$k]['list'] = [];
+        }
+
+        foreach ($this->model->all() as $k => $v) {
+            if (!isset($siteList[$v['group']])) {
+                continue;
+            }
+            $value = $v->toArray();
+            $value['title'] = __($value['title']);
+            if (in_array($value['type'], ['select', 'selects', 'checkbox', 'radio'])) {
+                $value['value'] = explode(',', $value['value']);
+            }
+            $value['content'] = json_decode($value['content'], true);
+            $value['tip'] = htmlspecialchars($value['tip']);
+            $siteList[$v['group']]['list'][] = $value;
+        }
+        $index = 0;
+        foreach ($siteList as $k => &$v) {
+            $v['active'] = !$index ? true : false;
+            $index++;
+        }
+        $this->view->assign('siteList', $siteList);
+        $this->view->assign('typeList', ConfigModel::getTypeList());
+        $this->view->assign('ruleList', ConfigModel::getRegexList());
+        $this->view->assign('groupList', ConfigModel::getGroupList());
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a", [], 'trim');
+            if ($params) {
+                foreach ($params as $k => &$v) {
+                    $v = is_array($v) ? implode(',', $v) : $v;
+                }
+                try {
+                    if (in_array($params['type'], ['select', 'selects', 'checkbox', 'radio', 'array'])) {
+                        $params['content'] = json_encode(ConfigModel::decode($params['content']), JSON_UNESCAPED_UNICODE);
+                    } else {
+                        $params['content'] = '';
+                    }
+                    $result = $this->model->create($params);
+                    if ($result !== false) {
+                        try {
+                            $this->refreshFile();
+                        } catch (Exception $e) {
+                            $this->error($e->getMessage());
+                        }
+                        $this->success();
+                    } else {
+                        $this->error($this->model->getError());
+                    }
+                } catch (Exception $e) {
+                    $this->error($e->getMessage());
+                }
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     * @param null $ids
+     */
+    public function edit($ids = null)
+    {
+        if ($this->request->isPost()) {
+            $this->token();
+            $row = $this->request->post("row/a", [], 'trim');
+            if ($row) {
+                $configList = [];
+                foreach ($this->model->all() as $v) {
+                    if (isset($row[$v['name']])) {
+                        $value = $row[$v['name']];
+                        if (is_array($value) && isset($value['field'])) {
+                            $value = json_encode(ConfigModel::getArrayData($value), JSON_UNESCAPED_UNICODE);
+                        } else {
+                            $value = is_array($value) ? implode(',', $value) : $value;
+                        }
+                        $v['value'] = $value;
+                        $configList[] = $v->toArray();
+                    }
+                }
+                $this->model->allowField(true)->saveAll($configList);
+                try {
+                    $this->refreshFile();
+                } catch (Exception $e) {
+                    $this->error($e->getMessage());
+                }
+                $this->success();
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+    }
+
+    /**
+     * 删除
+     * @param string $ids
+     */
+    public function del($ids = "")
+    {
+        $name = $this->request->post('name');
+        $config = ConfigModel::getByName($name);
+        if ($name && $config) {
+            try {
+                $config->delete();
+                $this->refreshFile();
+            } catch (Exception $e) {
+                $this->error($e->getMessage());
+            }
+            $this->success();
+        } else {
+            $this->error(__('Invalid parameters'));
+        }
+    }
+
+    /**
+     * 刷新配置文件
+     */
+    protected function refreshFile()
+    {
+        $config = [];
+        foreach ($this->model->all() as $k => $v) {
+            $value = $v->toArray();
+            if (in_array($value['type'], ['selects', 'checkbox', 'images', 'files'])) {
+                $value['value'] = explode(',', $value['value']);
+            }
+            if ($value['type'] == 'array') {
+                $value['value'] = (array)json_decode($value['value'], true);
+            }
+            $config[$value['name']] = $value['value'];
+        }
+        file_put_contents(
+            APP_PATH . 'extra' . DS . 'site.php',
+            '<?php' . "\n\nreturn " . var_export($config, true) . ";"
+        );
+    }
+
+    /**
+     * 检测配置项是否存在
+     * @internal
+     */
+    public function check()
+    {
+        $params = $this->request->post("row/a");
+        if ($params) {
+            $config = $this->model->get($params);
+            if (!$config) {
+                return $this->success();
+            } else {
+                return $this->error(__('Name already exist'));
+            }
+        } else {
+            return $this->error(__('Invalid parameters'));
+        }
+    }
+
+    /**
+     * 规则列表
+     * @internal
+     */
+    public function rulelist()
+    {
+        //主键
+        $primarykey = $this->request->request("keyField");
+        //主键值
+        $keyValue = $this->request->request("keyValue", "");
+
+        $keyValueArr = array_filter(explode(',', $keyValue));
+        $regexList = \app\common\model\Config::getRegexList();
+        $list = [];
+        foreach ($regexList as $k => $v) {
+            if ($keyValueArr) {
+                if (in_array($k, $keyValueArr)) {
+                    $list[] = ['id' => $k, 'name' => $v];
+                }
+            } else {
+                $list[] = ['id' => $k, 'name' => $v];
+            }
+        }
+        return json(['list' => $list]);
+    }
+
+    /**
+     * 发送测试邮件
+     * @internal
+     */
+    public function emailtest()
+    {
+        $row = $this->request->post('row/a');
+        $receiver = $this->request->post("receiver");
+        if ($receiver) {
+            if (!Validate::is($receiver, "email")) {
+                $this->error(__('Please input correct email'));
+            }
+            \think\Config::set('site', array_merge(\think\Config::get('site'), $row));
+            $email = new Email;
+            $result = $email
+                ->to($receiver)
+                ->subject(__("This is a test mail"))
+                ->message('<div style="min-height:550px; padding: 100px 55px 200px;">' . __('This is a test mail content') . '</div>')
+                ->send();
+            if ($result) {
+                $this->success();
+            } else {
+                $this->error($email->getError());
+            }
+        } else {
+            return $this->error(__('Invalid parameters'));
+        }
+    }
+}

+ 88 - 0
application/admin/controller/general/Profile.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace app\admin\controller\general;
+
+use app\admin\model\Admin;
+use app\common\controller\Backend;
+use fast\Random;
+use think\Session;
+use think\Validate;
+
+/**
+ * 个人配置
+ *
+ * @icon fa fa-user
+ */
+class Profile extends Backend
+{
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //设置过滤方法
+        $this->request->filter(['strip_tags']);
+        if ($this->request->isAjax()) {
+            $model = model('AdminLog');
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+
+            $total = $model
+                ->where($where)
+                ->where('admin_id', $this->auth->id)
+                ->order($sort, $order)
+                ->count();
+
+            $list = $model
+                ->where($where)
+                ->where('admin_id', $this->auth->id)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 更新个人信息
+     */
+    public function update()
+    {
+        if ($this->request->isPost()) {
+            $this->token();
+            $params = $this->request->post("row/a");
+            $params = array_filter(array_intersect_key(
+                $params,
+                array_flip(array('email', 'nickname', 'password', 'avatar'))
+            ));
+            unset($v);
+            if (!Validate::is($params['email'], "email")) {
+                $this->error(__("Please input correct email"));
+            }
+            if (isset($params['password'])) {
+                if (!Validate::is($params['password'], "/^[\S]{6,16}$/")) {
+                    $this->error(__("Please input correct password"));
+                }
+                $params['salt'] = Random::alnum();
+                $params['password'] = md5(md5($params['password']) . $params['salt']);
+            }
+            $exist = Admin::where('email', $params['email'])->where('id', '<>', $this->auth->id)->find();
+            if ($exist) {
+                $this->error(__("Email already exists"));
+            }
+            if ($params) {
+                $admin = Admin::get($this->auth->id);
+                $admin->save($params);
+                //因为个人资料面板读取的Session显示,修改自己资料后同时更新Session
+                Session::set("admin", $admin->toArray());
+                $this->success();
+            }
+            $this->error();
+        }
+        return;
+    }
+}

+ 45 - 0
application/admin/controller/user/Group.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace app\admin\controller\user;
+
+use app\common\controller\Backend;
+
+/**
+ * 会员组管理
+ *
+ * @icon fa fa-users
+ */
+class Group extends Backend
+{
+
+    /**
+     * @var \app\admin\model\UserGroup
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('UserGroup');
+        $this->view->assign("statusList", $this->model->getStatusList());
+    }
+
+    public function add()
+    {
+        $nodeList = \app\admin\model\UserRule::getTreeList();
+        $this->assign("nodeList", $nodeList);
+        return parent::add();
+    }
+
+    public function edit($ids = NULL)
+    {
+        $row = $this->model->get($ids);
+        if (!$row)
+            $this->error(__('No Results were found'));
+        $rules = explode(',', $row['rules']);
+        $nodeList = \app\admin\model\UserRule::getTreeList($rules);
+        $this->assign("nodeList", $nodeList);
+        return parent::edit($ids);
+    }
+
+}

+ 88 - 0
application/admin/controller/user/Rule.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace app\admin\controller\user;
+
+use app\common\controller\Backend;
+use fast\Tree;
+
+/**
+ * 会员规则管理
+ *
+ * @icon fa fa-circle-o
+ */
+class Rule extends Backend
+{
+
+
+    /**
+     * @var \app\admin\model\UserRule
+     */
+    protected $model = null;
+    protected $rulelist = [];
+    protected $multiFields = 'ismenu,status';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('UserRule');
+        $this->view->assign("statusList", $this->model->getStatusList());
+        // 必须将结果集转换为数组
+        $ruleList = collection($this->model->order('weigh', 'desc')->select())->toArray();
+        foreach ($ruleList as $k => &$v)
+        {
+            $v['title'] = __($v['title']);
+            $v['remark'] = __($v['remark']);
+        }
+        unset($v);
+        Tree::instance()->init($ruleList);
+        $this->rulelist = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0), 'title');
+        $ruledata = [0 => __('None')];
+        foreach ($this->rulelist as $k => &$v)
+        {
+            if (!$v['ismenu'])
+                continue;
+            $ruledata[$v['id']] = $v['title'];
+        }
+        $this->view->assign('ruledata', $ruledata);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        if ($this->request->isAjax())
+        {
+            $list = $this->rulelist;
+            $total = count($this->rulelist);
+
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 删除
+     */
+    public function del($ids = "")
+    {
+        if ($ids)
+        {
+            $delIds = [];
+            foreach (explode(',', $ids) as $k => $v)
+            {
+                $delIds = array_merge($delIds, Tree::instance()->getChildrenIds($v, TRUE));
+            }
+            $delIds = array_unique($delIds);
+            $count = $this->model->where('id', 'in', $delIds)->delete();
+            if ($count)
+            {
+                $this->success();
+            }
+        }
+        $this->error();
+    }
+
+}

+ 75 - 0
application/admin/controller/user/User.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace app\admin\controller\user;
+
+use app\common\controller\Backend;
+
+/**
+ * 会员管理
+ *
+ * @icon fa fa-user
+ */
+class User extends Backend
+{
+
+    protected $relationSearch = true;
+
+
+    /**
+     * @var \app\admin\model\User
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('User');
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //设置过滤方法
+        $this->request->filter(['strip_tags']);
+        if ($this->request->isAjax()) {
+            //如果发送的来源是Selectpage,则转发到Selectpage
+            if ($this->request->request('keyField')) {
+                return $this->selectpage();
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                ->with('group')
+                ->where($where)
+                ->order($sort, $order)
+                ->count();
+            $list = $this->model
+                ->with('group')
+                ->where($where)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+            foreach ($list as $k => $v) {
+                $v->hidden(['password', 'salt']);
+            }
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = NULL)
+    {
+        $row = $this->model->get($ids);
+        if (!$row)
+            $this->error(__('No Results were found'));
+        $this->view->assign('groupList', build_select('row[group_id]', \app\admin\model\UserGroup::column('id,name'), $row['group_id'], ['class' => 'form-control selectpicker']));
+        return parent::edit($ids);
+    }
+
+}

+ 182 - 0
application/admin/lang/zh-cn.php

@@ -0,0 +1,182 @@
+<?php
+
+return [
+    'User id'                                               => '会员ID',
+    'Username'                                              => '用户名',
+    'Nickname'                                              => '昵称',
+    'Password'                                              => '密码',
+    'Sign up'                                               => '注 册',
+    'Sign in'                                               => '登 录',
+    'Sign out'                                              => '注 销',
+    'Keep login'                                            => '保持会话',
+    'Guest'                                                 => '游客',
+    'Welcome'                                               => '%s,你好!',
+    'View'                                                  => '查看',
+    'Add'                                                   => '添加',
+    'Edit'                                                  => '编辑',
+    'Del'                                                   => '删除',
+    'Delete'                                                => '删除',
+    'Import'                                                => '导入',
+    'Export'                                                => '导出',
+    'All'                                                   => '全部',
+    'Detail'                                                => '详情',
+    'Multi'                                                 => '批量更新',
+    'Setting'                                               => '配置',
+    'Move'                                                  => '移动',
+    'Name'                                                  => '名称',
+    'Status'                                                => '状态',
+    'Weigh'                                                 => '权重',
+    'Operate'                                               => '操作',
+    'Warning'                                               => '温馨提示',
+    'Default'                                               => '默认',
+    'Article'                                               => '文章',
+    'Page'                                                  => '单页',
+    'OK'                                                    => '确定',
+    'Apply'                                                 => '应用',
+    'Cancel'                                                => '取消',
+    'Clear'                                                 => '清空',
+    'Custom Range'                                          => '自定义',
+    'Today'                                                 => '今天',
+    'Yesterday'                                             => '昨天',
+    'Last 7 days'                                           => '最近7天',
+    'Last 30 days'                                          => '最近30天',
+    'Last month'                                            => '上月',
+    'This month'                                            => '本月',
+    'Loading'                                               => '加载中',
+    'Money'                                                 => '余额',
+    'Score'                                                 => '积分',
+    'More'                                                  => '更多',
+    'Yes'                                                   => '是',
+    'No'                                                    => '否',
+    'Normal'                                                => '正常',
+    'Hidden'                                                => '隐藏',
+    'Locked'                                                => '锁定',
+    'Submit'                                                => '提交',
+    'Reset'                                                 => '重置',
+    'Execute'                                               => '执行',
+    'Close'                                                 => '关闭',
+    'Choose'                                                => '选择',
+    'Search'                                                => '搜索',
+    'Refresh'                                               => '刷新',
+    'Install'                                               => '安装',
+    'Uninstall'                                             => '卸载',
+    'First'                                                 => '首页',
+    'Previous'                                              => '上一页',
+    'Next'                                                  => '下一页',
+    'Last'                                                  => '末页',
+    'None'                                                  => '无',
+    'Home'                                                  => '主页',
+    'Online'                                                => '在线',
+    'Login'                                                 => '登录',
+    'Logout'                                                => '注销',
+    'Profile'                                               => '个人资料',
+    'Index'                                                 => '首页',
+    'Hot'                                                   => '热门',
+    'Recommend'                                             => '推荐',
+    'Upload'                                                => '上传',
+    'Uploading'                                             => '上传中',
+    'Code'                                                  => '编号',
+    'Message'                                               => '内容',
+    'Line'                                                  => '行号',
+    'File'                                                  => '文件',
+    'Menu'                                                  => '菜单',
+    'Type'                                                  => '类型',
+    'Title'                                                 => '标题',
+    'Content'                                               => '内容',
+    'Append'                                                => '追加',
+    'Select'                                                => '选择',
+    'Memo'                                                  => '备注',
+    'Parent'                                                => '父级',
+    'Params'                                                => '参数',
+    'Permission'                                            => '权限',
+    'Check all'                                             => '选中全部',
+    'Expand all'                                            => '展开全部',
+    'Begin time'                                            => '开始时间',
+    'End time'                                              => '结束时间',
+    'Create time'                                           => '创建时间',
+    'Update time'                                           => '更新时间',
+    'Flag'                                                  => '标志',
+    'Drag to sort'                                          => '拖动进行排序',
+    'Redirect now'                                          => '立即跳转',
+    'Key'                                                   => '键',
+    'Value'                                                 => '值',
+    'Common search'                                         => '普通搜索',
+    'Search %s'                                             => '搜索 %s',
+    'View %s'                                               => '查看 %s',
+    '%d second%s ago'                                       => '%d秒前',
+    '%d minute%s ago'                                       => '%d分钟前',
+    '%d hour%s ago'                                         => '%d小时前',
+    '%d day%s ago'                                          => '%d天前',
+    '%d week%s ago'                                         => '%d周前',
+    '%d month%s ago'                                        => '%d月前',
+    '%d year%s ago'                                         => '%d年前',
+    'Set to normal'                                         => '设为正常',
+    'Set to hidden'                                         => '设为隐藏',
+    'Recycle bin'                                           => '回收站',
+    'Restore'                                               => '还原',
+    'Restore all'                                           => '还原全部',
+    'Destroy'                                               => '销毁',
+    'Destroy all'                                           => '清空回收站',
+    'Nothing need restore'                                  => '没有需要还原的数据',
+    //提示
+    'Go back'                                               => '返回首页',
+    'Jump now'                                              => '立即跳转',
+    'Click to search %s'                                    => '点击搜索 %s',
+    'Click to toggle'                                       => '点击切换',
+    'Operation completed'                                   => '操作成功!',
+    'Operation failed'                                      => '操作失败!',
+    'Unknown data format'                                   => '未知的数据格式!',
+    'Network error'                                         => '网络错误!',
+    'Invalid parameters'                                    => '未知参数',
+    'No results were found'                                 => '记录未找到',
+    'No rows were inserted'                                 => '未插入任何行',
+    'No rows were deleted'                                  => '未删除任何行',
+    'No rows were updated'                                  => '未更新任何行',
+    'Parameter %s can not be empty'                         => '参数%s不能为空',
+    'Are you sure you want to delete the %s selected item?' => '确定删除选中的 %s 项?',
+    'Are you sure you want to delete this item?'            => '确定删除此项?',
+    'Are you sure you want to delete or turncate?'          => '确定删除或清空?',
+    'Are you sure you want to truncate?'                    => '确定清空?',
+    'Token verification error'                              => 'Token验证错误!',
+    'You have no permission'                                => '你没有权限访问',
+    'Please enter your username'                            => '请输入你的用户名',
+    'Please enter your password'                            => '请输入你的密码',
+    'Please login first'                                    => '请登录后操作',
+    'You can upload up to %d file%s'                        => '你最多还可以上传%d个文件',
+    'You can choose up to %d file%s'                        => '你最多还可以选择%d个文件',
+    'An unexpected error occurred'                          => '发生了一个意外错误,程序猿正在紧急处理中',
+    'This page will be re-directed in %s seconds'           => '页面将在 %s 秒后自动跳转',
+    //菜单
+    'Dashboard'                                             => '控制台',
+    'General'                                               => '常规管理',
+    'Category'                                              => '分类管理',
+    'Addon'                                                 => '插件管理',
+    'Auth'                                                  => '权限管理',
+    'Config'                                                => '系统配置',
+    'Attachment'                                            => '附件管理',
+    'Admin'                                                 => '管理员管理',
+    'Admin log'                                             => '管理员日志',
+    'Group'                                                 => '角色组',
+    'Rule'                                                  => '菜单规则',
+    'User'                                                  => '会员管理',
+    'User group'                                            => '会员分组',
+    'User rule'                                             => '会员规则',
+    'Select attachment'                                     => '选择附件',
+    'Update profile'                                        => '更新个人信息',
+    'Local install'                                         => '本地安装',
+    'Update state'                                          => '禁用启用',
+    'Admin group'                                           => '超级管理组',
+    'Second group'                                          => '二级管理组',
+    'Third group'                                           => '三级管理组',
+    'Second group 2'                                        => '二级管理组2',
+    'Third group 2'                                         => '三级管理组2',
+    'Dashboard tips'                                        => '用于展示当前系统中的统计数据、统计报表及重要实时数据',
+    'Config tips'                                           => '可以在此增改系统的变量和分组,也可以自定义分组和变量,如果需要删除请从数据库中删除',
+    'Category tips'                                         => '用于统一管理网站的所有分类,分类可进行无限级分类,分类类型请在常规管理->系统配置->字典配置中添加',
+    'Attachment tips'                                       => '主要用于管理上传到服务器或第三方存储的数据',
+    'Addon tips'                                            => '可在线安装、卸载、禁用、启用插件,同时支持添加本地插件。FastAdmin已上线插件商店 ,你可以发布你的免费或付费插件:<a href="https://www.fastadmin.net/store.html" target="_blank">https://www.fastadmin.net/store.html</a>',
+    'Admin tips'                                            => '一个管理员可以有多个角色组,左侧的菜单根据管理员所拥有的权限进行生成',
+    'Admin log tips'                                        => '管理员可以查看自己所拥有的权限的管理员日志',
+    'Group tips'                                            => '角色组可以有多个,角色有上下级层级关系,如果子角色有角色组和管理员的权限则可以派生属于自己组别的下级角色组或管理员',
+    'Rule tips'                                             => '规则通常对应一个控制器的方法,同时左侧的菜单栏数据也从规则中体现,通常建议通过命令行进行生成规则节点',
+];

+ 93 - 0
application/admin/lang/zh-cn/addon.php

@@ -0,0 +1,93 @@
+<?php
+
+return [
+    'Id'                             => 'ID',
+    'Title'                          => '插件名称',
+    'Value'                          => '配置值',
+    'Array key'                      => '键',
+    'Array value'                    => '值',
+    'File'                           => '文件',
+    'Donate'                         => '打赏作者',
+    'Warmtips'                       => '温馨提示',
+    'Pay now'                        => '立即支付',
+    'Offline install'                => '离线安装',
+    'Refresh addon cache'            => '刷新插件缓存',
+    'Userinfo'                       => '会员信息',
+    'Online store'                   => '在线商店',
+    'Local addon'                    => '本地插件',
+    'Conflict tips'                  => '此插件中发现和现有系统中部分文件发现冲突!以下文件将会被影响,请备份好相关文件后再继续操作',
+    'Login tips'                     => '此处登录账号为<a href="https://www.fastadmin.net" target="_blank">FastAdmin官网账号</a>',
+    'Logined tips'                   => '你好!%s<br />当前你已经登录,将同步保存你的购买记录',
+    'Pay tips'                       => '扫码支付后如果仍然无法立即下载,请不要重复支付,请加<a href="https://jq.qq.com/?_wv=1027&k=487PNBb" target="_blank">QQ群:636393962</a>向管理员反馈',
+    'Pay click tips'                 => '请点击这里在新窗口中进行支付!',
+    'Pay new window tips'            => '请在新弹出的窗口中进行支付,支付完成后再重新点击安装按钮进行安装!',
+    'Uninstall tips'                 => '确认卸载<b>[%s]</b>?<p class="text-danger">卸载将会删除所有插件文件且不可找回!!! 插件如果有创建数据库表请手动删除!!!</p>如有重要数据请备份后再操作!',
+    'Upgrade tips'                   => '确认升级<b>[%s]</b>?<p class="text-danger">如果之前购买插件时未登录,此次升级可能出现购买后才可以下载的提示!!!<br>升级后可能出现部分冗余数据记录,请根据需要移除即可!!!</p>如有重要数据请备份后再操作!',
+    'Offline installed tips'         => '插件安装成功!清除浏览器缓存和框架缓存后生效!',
+    'Online installed tips'          => '插件安装成功!清除浏览器缓存和框架缓存后生效!',
+    'Not login tips'                 => '你当前未登录FastAdmin,登录后将同步已购买的记录,下载时无需二次付费!',
+    'Not installed tips'             => '请安装后再访问插件前台页面!',
+    'Not enabled tips'               => '插件已经禁用,请启用后再访问插件前台页面!',
+    'New version tips'               => '发现新版本:%s 点击查看更新日志',
+    'Store now available tips'       => 'FastAdmin插件市场暂不可用,是否切换到本地插件?',
+    'Switch to the local'            => '切换到本地插件',
+    'try to reload'                  => '重新尝试加载',
+    'Please disable addon first'     => '请先禁用插件再进行升级',
+    'Login now'                      => '立即登录',
+    'Continue install'               => '不登录,继续安装',
+    'View addon home page'           => '查看插件介绍和帮助',
+    'View addon index page'          => '查看插件前台首页',
+    'View addon screenshots'         => '点击查看插件截图',
+    'Click to toggle status'         => '点击切换插件状态',
+    'Click to contact developer'     => '点击与插件开发者取得联系',
+    'My addons'                      => '我购买的插件',
+    'My posts'                       => '我发布的插件',
+    'Index'                          => '前台',
+    'All'                            => '全部',
+    'Uncategoried'                   => '未归类',
+    'Recommend'                      => '推荐',
+    'Hot'                            => '热门',
+    'New'                            => '新',
+    'Paying'                         => '付费',
+    'Free'                           => '免费',
+    'Sale'                           => '折扣',
+    'No image'                       => '暂无缩略图',
+    'Price'                          => '价格',
+    'Downloads'                      => '下载',
+    'Author'                         => '作者',
+    'Identify'                       => '标识',
+    'Homepage'                       => '主页',
+    'Intro'                          => '介绍',
+    'Version'                        => '版本',
+    'New version'                    => '新版本',
+    'Createtime'                     => '添加时间',
+    'Releasetime'                    => '更新时间',
+    'Detail'                         => '插件详情',
+    'Document'                       => '文档',
+    'Demo'                           => '演示',
+    'Feedback'                       => '反馈BUG',
+    'Install'                        => '安装',
+    'Uninstall'                      => '卸载',
+    'Upgrade'                        => '升级',
+    'Setting'                        => '配置',
+    'Disable'                        => '禁用',
+    'Enable'                         => '启用',
+    'Your username or email'         => '你的手机号、用户名或邮箱',
+    'Your password'                  => '你的密码',
+    'Login FastAdmin'                => '登录FastAdmin',
+    'Login'                          => '登录',
+    'Logout'                         => '退出登录',
+    'Register'                       => '注册账号',
+    'You\'re not login'              => '当前未登录',
+    'Continue uninstall'             => '继续卸载',
+    'Continue operate'               => '继续操作',
+    'Install successful'             => '安装成功',
+    'Uninstall successful'           => '卸载成功',
+    'Operate successful'             => '操作成功',
+    'Addon name incorrect'           => '插件名称不正确',
+    'Addon info file was not found'  => '插件配置文件未找到',
+    'Addon info file data incorrect' => '插件配置信息不正确',
+    'Addon already exists'           => '上传的插件已经存在',
+    'Unable to open the zip file'    => '无法打开ZIP文件',
+    'Unable to extract the file'     => '无法解压ZIP文件',
+];

+ 8 - 0
application/admin/lang/zh-cn/ajax.php

@@ -0,0 +1,8 @@
+<?php
+
+return [
+    'No file upload or server upload limit exceeded' => '未上传文件或超出服务器上传限制',
+    'Uploaded file format is limited'                => '上传文件格式受限制',
+    'Uploaded file is not a valid image'             => '上传文件不是有效的图片文件',
+    'Upload successful'                              => '上传成功',
+];

+ 9 - 0
application/admin/lang/zh-cn/auth/admin.php

@@ -0,0 +1,9 @@
+<?php
+
+return [
+    'Group'                         => '所属组别',
+    'Loginfailure'                  => '登录失败次数',
+    'Login time'                    => '最后登录',
+    'Please input correct username' => '用户名只能由3-12位数字、字母、下划线组合',
+    'Please input correct password' => '密码长度必须在6-16位之间,不能包含空格',
+];

+ 10 - 0
application/admin/lang/zh-cn/auth/group.php

@@ -0,0 +1,10 @@
+<?php
+
+return [
+    'The parent group can not be its own child'                            => '父组别不能是自身的子组别',
+    'The parent group can not found'                                       => '父组别未找到',
+    'Group not found'                                                      => '组别未找到',
+    'Can not change the parent to child'                                   => '父组别不能是它的子组别',
+    'Can not change the parent to self'                                    => '父组别不能是它的子组别',
+    'You can not delete group that contain child group and administrators' => '你不能删除含有子组和管理员的组',
+];

+ 20 - 0
application/admin/lang/zh-cn/auth/rule.php

@@ -0,0 +1,20 @@
+<?php
+
+return [
+    'Toggle all'                                                => '显示全部',
+    'Condition'                                                 => '规则条件',
+    'Remark'                                                    => '备注',
+    'Icon'                                                      => '图标',
+    'Alert'                                                     => '警告',
+    'Name'                                                      => '规则',
+    'Controller/Action'                                         => '控制器名/方法名',
+    'Ismenu'                                                    => '菜单',
+    'Search icon'                                               => '搜索图标',
+    'Toggle menu visible'                                       => '点击切换菜单显示',
+    'Toggle sub menu'                                           => '点击切换子菜单',
+    'Menu tips'                                                 => '父级菜单无需匹配控制器和方法,子级菜单请使用控制器名',
+    'Node tips'                                                 => '控制器/方法名,如果有目录请使用 目录名/控制器名/方法名',
+    'The non-menu rule must have parent'                        => '非菜单规则节点必须有父级',
+    'Can not change the parent to child'                        => '父组别不能是它的子组别',
+    'Name only supports letters, numbers, underscore and slash' => 'URL规则只能是小写字母、数字、下划线和/组成',
+];

+ 18 - 0
application/admin/lang/zh-cn/category.php

@@ -0,0 +1,18 @@
+<?php
+
+return [
+    'Id'                                 => 'ID',
+    'Pid'                                => '父ID',
+    'Type'                               => '类型',
+    'All'                                => '全部',
+    'Image'                              => '图片',
+    'Keywords'                           => '关键字',
+    'Description'                        => '描述',
+    'Diyname'                            => '自定义名称',
+    'Createtime'                         => '创建时间',
+    'Updatetime'                         => '更新时间',
+    'Weigh'                              => '权重',
+    'Category warmtips'                  => '温馨提示:栏目类型请前往<b>常规管理</b>-><b>系统配置</b>-><b>字典配置</b>中进行管理',
+    'Can not change the parent to child' => '父组别不能是它的子组别',
+    'Status'                             => '状态'
+];

+ 9 - 0
application/admin/lang/zh-cn/config.php

@@ -0,0 +1,9 @@
+<?php
+
+return [
+    'name'  => '变量名称',
+    'intro' => '描述',
+    'group' => '分组',
+    'type'  => '类型',
+    'value' => '变量值'
+];

+ 48 - 0
application/admin/lang/zh-cn/dashboard.php

@@ -0,0 +1,48 @@
+<?php
+
+return [
+    'Custom'                  => '自定义',
+    'Pid'                     => '父ID',
+    'Type'                    => '栏目类型',
+    'Image'                   => '图片',
+    'Total user'              => '总会员数',
+    'Total view'              => '总访问数',
+    'Total order'             => '总订单数',
+    'Total order amount'      => '总金额',
+    'Today user signup'       => '今日注册',
+    'Today user login'        => '今日登录',
+    'Today order'             => '今日订单',
+    'Unsettle order'          => '未处理订单',
+    'Seven dnu'               => '七日新增',
+    'Seven dau'               => '七日活跃',
+    'Custom zone'             => '这里是你的自定义数据',
+    'Sales'                   => '成交数',
+    'Orders'                  => '订单数',
+    'Real time'               => '实时',
+    'Category count'          => '分类统计',
+    'Category count tips'     => '当前分类总记录数',
+    'Attachment count'        => '附件统计',
+    'Attachment count tips'   => '当前上传的附件数量',
+    'Article count'           => '文章统计',
+    'News count'              => '新闻统计',
+    'Comment count'           => '评论次数',
+    'Like count'              => '点赞次数',
+    'Recent news'             => '最新新闻',
+    'Recent discussion'       => '最新发贴',
+    'Server info'             => '服务器信息',
+    'PHP version'             => 'PHP版本',
+    'Fastadmin version'       => '主框架版本',
+    'Fastadmin addon version' => '插件版本',
+    'Thinkphp version'        => 'ThinkPHP版本',
+    'Sapi name'               => '运行方式',
+    'Debug mode'              => '调试模式',
+    'Software'                => '环境信息',
+    'Upload mode'             => '上传模式',
+    'Upload url'              => '上传URL',
+    'Upload cdn url'          => '上传CDN',
+    'Cdn url'                 => '静态资源CDN',
+    'Timezone'                => '时区',
+    'Language'                => '语言',
+    'View more'               => '查看更多',
+    'Security tips'           => '<i class="fa fa-warning"></i> 安全提示:你正在使用默认的后台登录入口,为了你的网站安全,强烈建议你修改后台登录入口,<a href="https://forum.fastadmin.net/thread/7640" target="_blank">点击查看修改方法</a>',
+];

+ 22 - 0
application/admin/lang/zh-cn/general/attachment.php

@@ -0,0 +1,22 @@
+<?php
+
+return [
+    'Id'                 => 'ID',
+    'Admin_id'           => '管理员ID',
+    'User_id'            => '会员ID',
+    'Url'                => '物理路径',
+    'Imagewidth'         => '宽度',
+    'Imageheight'        => '高度',
+    'Imagetype'          => '图片类型',
+    'Imageframes'        => '图片帧数',
+    'Preview'            => '预览',
+    'Filesize'           => '文件大小',
+    'Mimetype'           => 'Mime类型',
+    'Extparam'           => '透传数据',
+    'Createtime'         => '创建日期',
+    'Uploadtime'         => '上传时间',
+    'Storage'            => '存储引擎',
+    'Upload to third'    => '上传到第三方',
+    'Upload to local'    => '上传到本地',
+    'Upload from editor' => '从编辑器上传'
+];

+ 65 - 0
application/admin/lang/zh-cn/general/config.php

@@ -0,0 +1,65 @@
+<?php
+
+return [
+    'Name'                        => '变量名',
+    'Tip'                         => '提示信息',
+    'Group'                       => '分组',
+    'Type'                        => '类型',
+    'Title'                       => '变量标题',
+    'Value'                       => '变量值',
+    'Basic'                       => '基础配置',
+    'Email'                       => '邮件配置',
+    'Attachment'                  => '附件配置',
+    'Dictionary'                  => '字典配置',
+    'User'                        => '会员配置',
+    'Example'                     => '示例分组',
+    'Extend'                      => '扩展属性',
+    'String'                      => '字符',
+    'Text'                        => '文本',
+    'Editor'                      => '编辑器',
+    'Number'                      => '数字',
+    'Date'                        => '日期',
+    'Time'                        => '时间',
+    'Datetime'                    => '日期时间',
+    'Image'                       => '图片',
+    'Images'                      => '图片(多)',
+    'File'                        => '文件',
+    'Files'                       => '文件(多)',
+    'Select'                      => '列表',
+    'Selects'                     => '列表(多选)',
+    'Switch'                      => '开关',
+    'Checkbox'                    => '复选',
+    'Radio'                       => '单选',
+    'Array'                       => '数组',
+    'Array key'                   => '键名',
+    'Array value'                 => '键值',
+    'Custom'                      => '自定义',
+    'Content'                     => '数据列表',
+    'Rule'                        => '校验规则',
+    'Site name'                   => '站点名称',
+    'Beian'                       => '备案号',
+    'Cdn url'                     => 'CDN地址',
+    'Version'                     => '版本号',
+    'Timezone'                    => '时区',
+    'Forbidden ip'                => '禁止IP',
+    'Languages'                   => '语言',
+    'Fixed page'                  => '后台固定页',
+    'Category type'               => '分类类型',
+    'Config group'                => '配置分组',
+    'Rule tips'                   => '校验规则使用请参考Nice-validator文档',
+    'Extend tips'                 => '扩展属性支持{id}、{name}、{group}、{title}、{value}、{content}、{rule}替换',
+    'Mail type'                   => '邮件发送方式',
+    'Mail smtp host'              => 'SMTP服务器',
+    'Mail smtp port'              => 'SMTP端口',
+    'Mail smtp user'              => 'SMTP用户名',
+    'Mail smtp password'          => 'SMTP密码',
+    'Mail vertify type'           => 'SMTP验证方式',
+    'Mail from'                   => '发件人邮箱',
+    'Name already exist'          => '变量名称已经存在',
+    'Add new config'              => '点击添加新的配置',
+    'Send a test message'         => '发送测试邮件',
+    'This is a test mail content' => '这是一封来自FastAdmin校验邮件,用于校验邮件配置是否正常!',
+    'This is a test mail'         => '这是一封来自FastAdmin的邮件',
+    'Please input your email'     => '请输入测试接收者邮箱',
+    'Please input correct email'  => '请输入正确的邮箱地址',
+];

+ 13 - 0
application/admin/lang/zh-cn/general/profile.php

@@ -0,0 +1,13 @@
+<?php
+
+return [
+    'Url'                                         => '链接',
+    'Userame'                                     => '用户名',
+    'Createtime'                                  => '操作时间',
+    'Click to edit'                               => '点击编辑',
+    'Admin log'                                   => '操作日志',
+    'Leave password blank if dont want to change' => '不修改密码请留空',
+    'Please input correct email'                  => '请输入正确的Email地址',
+    'Please input correct password'               => '密码长度不正确',
+    'Email already exists'                        => '邮箱已经存在',
+];

+ 57 - 0
application/admin/lang/zh-cn/index.php

@@ -0,0 +1,57 @@
+<?php
+
+return [
+    'Title'                                                      => '标题',
+    'Search menu'                                                => '搜索菜单',
+    'Layout Options'                                             => '布局设定',
+    'Fixed Layout'                                               => '固定布局',
+    'You can\'t use fixed and boxed layouts together'            => '盒子模型和固定布局不能同时启作用',
+    'Boxed Layout'                                               => '盒子布局',
+    'Activate the boxed layout'                                  => '盒子布局最大宽度将被限定为1250px',
+    'Toggle Sidebar'                                             => '切换菜单栏',
+    'Toggle the left sidebar\'s state (open or collapse)'        => '切换菜单栏的展示或收起',
+    'Sidebar Expand on Hover'                                    => '菜单栏自动展开',
+    'Let the sidebar mini expand on hover'                       => '鼠标移到菜单栏自动展开',
+    'Toggle Right Sidebar Slide'                                 => '切换右侧操作栏',
+    'Toggle between slide over content and push content effects' => '切换右侧操作栏覆盖或独占',
+    'Toggle Right Sidebar Skin'                                  => '切换右侧操作栏背景',
+    'Toggle between dark and light skins for the right sidebar'  => '将右侧操作栏背景亮色或深色切换',
+    'Show sub menu'                                              => '显示菜单栏子菜单',
+    'Always show sub menu'                                       => '菜单栏子菜单将始终显示',
+    'Disable top menu badge'                                     => '禁用顶部彩色小角标',
+    'Disable top menu badge without left menu'                   => '左边菜单栏的彩色小角标不受影响',
+    'Skins'                                                      => '皮肤',
+    'You\'ve logged in, do not login again'                      => '你已经登录,无需重复登录',
+    'Username or password can not be empty'                      => '用户名密码不能为空',
+    'Username or password is incorrect'                          => '用户名或密码不正确',
+    'Username is incorrect'                                      => '用户名不正确',
+    'Password is incorrect'                                      => '密码不正确',
+    'Admin is forbidden'                                         => '管理员已经被禁止登录',
+    'Please try again after 1 day'                               => '请于1天后再尝试登录',
+    'Login successful'                                           => '登录成功!',
+    'Logout successful'                                          => '退出成功!',
+    'Verification code is incorrect'                             => '验证码不正确',
+    'Wipe cache completed'                                       => '清除缓存成功',
+    'Wipe cache failed'                                          => '清除缓存失败',
+    'Wipe cache'                                                 => '清空缓存',
+    'Wipe all cache'                                             => '一键清除缓存',
+    'Wipe content cache'                                         => '清空内容缓存',
+    'Wipe template cache'                                        => '清除模板缓存',
+    'Wipe addons cache'                                          => '清除插件缓存',
+    'Check for updates'                                          => '检测更新',
+    'Discover new version'                                       => '发现新版本',
+    'Go to download'                                             => '去下载更新',
+    'Currently is the latest version'                            => '当前已经是最新版本',
+    'Ignore this version'                                        => '忽略此次更新',
+    'Do not remind again'                                        => '不再提示',
+    'Your current version'                                       => '你的版本是',
+    'New version'                                                => '新版本',
+    'Release notes'                                              => '更新说明',
+    'Latest news'                                                => '最新消息',
+    'View more'                                                  => '查看更多',
+    'Links'                                                      => '相关链接',
+    'Docs'                                                       => '官方文档',
+    'Forum'                                                      => '交流社区',
+    'QQ qun'                                                     => 'QQ交流群',
+    'Captcha'                                                    => '验证码',
+];

+ 9 - 0
application/admin/lang/zh-cn/user/group.php

@@ -0,0 +1,9 @@
+<?php
+
+return [
+    'Name'       => '组名',
+    'Rules'      => '权限节点',
+    'Createtime' => '添加时间',
+    'Updatetime' => '更新时间',
+    'Status'     => '状态'
+];

+ 15 - 0
application/admin/lang/zh-cn/user/rule.php

@@ -0,0 +1,15 @@
+<?php
+
+return [
+    'Pid'        => '父ID',
+    'Name'       => '规则',
+    'Title'      => '标题',
+    'Remark'     => '备注',
+    'Ismenu'     => '是否菜单',
+    'Createtime' => '创建时间',
+    'Updatetime' => '更新时间',
+    'Menu tips'  => '规则任意,请不可重复,仅做层级显示,无需匹配控制器和方法',
+    'Node tips'  => '模块/控制器/方法名',
+    'Weigh'      => '权重',
+    'Status'     => '状态'
+];

+ 33 - 0
application/admin/lang/zh-cn/user/user.php

@@ -0,0 +1,33 @@
+<?php
+
+return [
+    'Id'             => 'ID',
+    'Group_id'       => '组别ID',
+    'Username'       => '用户名',
+    'Nickname'       => '昵称',
+    'Password'       => '密码',
+    'Salt'           => '密码盐',
+    'Email'          => '电子邮箱',
+    'Mobile'         => '手机号',
+    'Avatar'         => '头像',
+    'Level'          => '等级',
+    'Gender'         => '性别',
+    'Male'           => '男',
+    'FeMale'         => '女',
+    'Birthday'       => '生日',
+    'Bio'            => '格言',
+    'Score'          => '积分',
+    'Successions'    => '连续登录天数',
+    'Maxsuccessions' => '最大连续登录天数',
+    'Prevtime'       => '上次登录时间',
+    'Logintime'      => '登录时间',
+    'Loginip'        => '登录IP',
+    'Loginfailure'   => '失败次数',
+    'Joinip'         => '加入IP',
+    'Jointime'       => '加入时间',
+    'Createtime'     => '创建时间',
+    'Updatetime'     => '更新时间',
+    'Token'          => 'Token',
+    'Status'         => '状态',
+    'Leave password blank if dont want to change' => '不修改密码请留空',
+];

+ 523 - 0
application/admin/library/Auth.php

@@ -0,0 +1,523 @@
+<?php
+
+namespace app\admin\library;
+
+use app\admin\model\Admin;
+use fast\Random;
+use fast\Tree;
+use think\Config;
+use think\Cookie;
+use think\Hook;
+use think\Request;
+use think\Session;
+
+class Auth extends \fast\Auth
+{
+    protected $_error = '';
+    protected $requestUri = '';
+    protected $breadcrumb = [];
+    protected $logined = false; //登录状态
+
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function __get($name)
+    {
+        return Session::get('admin.' . $name);
+    }
+
+    /**
+     * 管理员登录
+     *
+     * @param string $username 用户名
+     * @param string $password 密码
+     * @param int    $keeptime 有效时长
+     * @return  boolean
+     */
+    public function login($username, $password, $keeptime = 0)
+    {
+        $admin = Admin::get(['username' => $username]);
+        if (!$admin) {
+            $this->setError('Username is incorrect');
+            return false;
+        }
+        if ($admin['status'] == 'hidden') {
+            $this->setError('Admin is forbidden');
+            return false;
+        }
+        if (Config::get('fastadmin.login_failure_retry') && $admin->loginfailure >= 10 && time() - $admin->updatetime < 86400) {
+            $this->setError('Please try again after 1 day');
+            return false;
+        }
+        if ($admin->password != md5(md5($password) . $admin->salt)) {
+            $admin->loginfailure++;
+            $admin->save();
+            $this->setError('Password is incorrect');
+            return false;
+        }
+        $admin->loginfailure = 0;
+        $admin->logintime = time();
+        $admin->loginip = request()->ip();
+        $admin->token = Random::uuid();
+        $admin->save();
+        Session::set("admin", $admin->toArray());
+        $this->keeplogin($keeptime);
+        return true;
+    }
+
+    /**
+     * 注销登录
+     */
+    public function logout()
+    {
+        $admin = Admin::get(intval($this->id));
+        if (!$admin) {
+            $admin->token = '';
+            $admin->save();
+        }
+        $this->logined = false; //重置登录状态
+        Session::delete("admin");
+        Cookie::delete("keeplogin");
+        return true;
+    }
+
+    /**
+     * 自动登录
+     * @return boolean
+     */
+    public function autologin()
+    {
+        $keeplogin = Cookie::get('keeplogin');
+        if (!$keeplogin) {
+            return false;
+        }
+        list($id, $keeptime, $expiretime, $key) = explode('|', $keeplogin);
+        if ($id && $keeptime && $expiretime && $key && $expiretime > time()) {
+            $admin = Admin::get($id);
+            if (!$admin || !$admin->token) {
+                return false;
+            }
+            //token有变更
+            if ($key != md5(md5($id) . md5($keeptime) . md5($expiretime) . $admin->token)) {
+                return false;
+            }
+            $ip = request()->ip();
+            //IP有变动
+            if ($admin->loginip != $ip) {
+                return false;
+            }
+            Session::set("admin", $admin->toArray());
+            //刷新自动登录的时效
+            $this->keeplogin($keeptime);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 刷新保持登录的Cookie
+     *
+     * @param int $keeptime
+     * @return  boolean
+     */
+    protected function keeplogin($keeptime = 0)
+    {
+        if ($keeptime) {
+            $expiretime = time() + $keeptime;
+            $key = md5(md5($this->id) . md5($keeptime) . md5($expiretime) . $this->token);
+            $data = [$this->id, $keeptime, $expiretime, $key];
+            Cookie::set('keeplogin', implode('|', $data), 86400 * 30);
+            return true;
+        }
+        return false;
+    }
+
+    public function check($name, $uid = '', $relation = 'or', $mode = 'url')
+    {
+        $uid = $uid ? $uid : $this->id;
+        return parent::check($name, $uid, $relation, $mode);
+    }
+
+    /**
+     * 检测当前控制器和方法是否匹配传递的数组
+     *
+     * @param array $arr 需要验证权限的数组
+     * @return bool
+     */
+    public function match($arr = [])
+    {
+        $request = Request::instance();
+        $arr = is_array($arr) ? $arr : explode(',', $arr);
+        if (!$arr) {
+            return false;
+        }
+
+        $arr = array_map('strtolower', $arr);
+        // 是否存在
+        if (in_array(strtolower($request->action()), $arr) || in_array('*', $arr)) {
+            return true;
+        }
+
+        // 没找到匹配
+        return false;
+    }
+
+    /**
+     * 检测是否登录
+     *
+     * @return boolean
+     */
+    public function isLogin()
+    {
+        if ($this->logined) {
+            return true;
+        }
+        $admin = Session::get('admin');
+        if (!$admin) {
+            return false;
+        }
+        //判断是否同一时间同一账号只能在一个地方登录
+        if (Config::get('fastadmin.login_unique')) {
+            $my = Admin::get($admin['id']);
+            if (!$my || $my['token'] != $admin['token']) {
+                $this->logout();
+                return false;
+            }
+        }
+        //判断管理员IP是否变动
+        if (Config::get('fastadmin.loginip_check')) {
+            if (!isset($admin['loginip']) || $admin['loginip'] != request()->ip()) {
+                $this->logout();
+                return false;
+            }
+        }
+        $this->logined = true;
+        return true;
+    }
+
+    /**
+     * 获取当前请求的URI
+     * @return string
+     */
+    public function getRequestUri()
+    {
+        return $this->requestUri;
+    }
+
+    /**
+     * 设置当前请求的URI
+     * @param string $uri
+     */
+    public function setRequestUri($uri)
+    {
+        $this->requestUri = $uri;
+    }
+
+    public function getGroups($uid = null)
+    {
+        $uid = is_null($uid) ? $this->id : $uid;
+        return parent::getGroups($uid);
+    }
+
+    public function getRuleList($uid = null)
+    {
+        $uid = is_null($uid) ? $this->id : $uid;
+        return parent::getRuleList($uid);
+    }
+
+    public function getUserInfo($uid = null)
+    {
+        $uid = is_null($uid) ? $this->id : $uid;
+
+        return $uid != $this->id ? Admin::get(intval($uid)) : Session::get('admin');
+    }
+
+    public function getRuleIds($uid = null)
+    {
+        $uid = is_null($uid) ? $this->id : $uid;
+        return parent::getRuleIds($uid);
+    }
+
+    public function isSuperAdmin()
+    {
+        return in_array('*', $this->getRuleIds()) ? true : false;
+    }
+
+    /**
+     * 获取管理员所属于的分组ID
+     * @param int $uid
+     * @return array
+     */
+    public function getGroupIds($uid = null)
+    {
+        $groups = $this->getGroups($uid);
+        $groupIds = [];
+        foreach ($groups as $K => $v) {
+            $groupIds[] = (int)$v['group_id'];
+        }
+        return $groupIds;
+    }
+
+    /**
+     * 取出当前管理员所拥有权限的分组
+     * @param boolean $withself 是否包含当前所在的分组
+     * @return array
+     */
+    public function getChildrenGroupIds($withself = false)
+    {
+        //取出当前管理员所有的分组
+        $groups = $this->getGroups();
+        $groupIds = [];
+        foreach ($groups as $k => $v) {
+            $groupIds[] = $v['id'];
+        }
+        $originGroupIds = $groupIds;
+        foreach ($groups as $k => $v) {
+            if (in_array($v['pid'], $originGroupIds)) {
+                $groupIds = array_diff($groupIds, [$v['id']]);
+                unset($groups[$k]);
+            }
+        }
+        // 取出所有分组
+        $groupList = \app\admin\model\AuthGroup::where(['status' => 'normal'])->select();
+        $objList = [];
+        foreach ($groups as $k => $v) {
+            if ($v['rules'] === '*') {
+                $objList = $groupList;
+                break;
+            }
+            // 取出包含自己的所有子节点
+            $childrenList = Tree::instance()->init($groupList)->getChildren($v['id'], true);
+            $obj = Tree::instance()->init($childrenList)->getTreeArray($v['pid']);
+            $objList = array_merge($objList, Tree::instance()->getTreeList($obj));
+        }
+        $childrenGroupIds = [];
+        foreach ($objList as $k => $v) {
+            $childrenGroupIds[] = $v['id'];
+        }
+        if (!$withself) {
+            $childrenGroupIds = array_diff($childrenGroupIds, $groupIds);
+        }
+        return $childrenGroupIds;
+    }
+
+    /**
+     * 取出当前管理员所拥有权限的管理员
+     * @param boolean $withself 是否包含自身
+     * @return array
+     */
+    public function getChildrenAdminIds($withself = false)
+    {
+        $childrenAdminIds = [];
+        if (!$this->isSuperAdmin()) {
+            $groupIds = $this->getChildrenGroupIds(false);
+            $authGroupList = \app\admin\model\AuthGroupAccess::
+            field('uid,group_id')
+                ->where('group_id', 'in', $groupIds)
+                ->select();
+            foreach ($authGroupList as $k => $v) {
+                $childrenAdminIds[] = $v['uid'];
+            }
+        } else {
+            //超级管理员拥有所有人的权限
+            $childrenAdminIds = Admin::column('id');
+        }
+        if ($withself) {
+            if (!in_array($this->id, $childrenAdminIds)) {
+                $childrenAdminIds[] = $this->id;
+            }
+        } else {
+            $childrenAdminIds = array_diff($childrenAdminIds, [$this->id]);
+        }
+        return $childrenAdminIds;
+    }
+
+    /**
+     * 获得面包屑导航
+     * @param string $path
+     * @return array
+     */
+    public function getBreadCrumb($path = '')
+    {
+        if ($this->breadcrumb || !$path) {
+            return $this->breadcrumb;
+        }
+        $path_rule_id = 0;
+        foreach ($this->rules as $rule) {
+            $path_rule_id = $rule['name'] == $path ? $rule['id'] : $path_rule_id;
+        }
+        if ($path_rule_id) {
+            $this->breadcrumb = Tree::instance()->init($this->rules)->getParents($path_rule_id, true);
+            foreach ($this->breadcrumb as $k => &$v) {
+                $v['url'] = url($v['name']);
+                $v['title'] = __($v['title']);
+            }
+        }
+        return $this->breadcrumb;
+    }
+
+    /**
+     * 获取左侧和顶部菜单栏
+     *
+     * @param array  $params URL对应的badge数据
+     * @param string $fixedPage 默认页
+     * @return array
+     */
+    public function getSidebar($params = [], $fixedPage = 'dashboard')
+    {
+        // 边栏开始
+        Hook::listen("admin_sidebar_begin", $params);
+        $colorArr = ['red', 'green', 'yellow', 'blue', 'teal', 'orange', 'purple'];
+        $colorNums = count($colorArr);
+        $badgeList = [];
+        $module = request()->module();
+        // 生成菜单的badge
+        foreach ($params as $k => $v) {
+            $url = $k;
+            if (is_array($v)) {
+                $nums = isset($v[0]) ? $v[0] : 0;
+                $color = isset($v[1]) ? $v[1] : $colorArr[(is_numeric($nums) ? $nums : strlen($nums)) % $colorNums];
+                $class = isset($v[2]) ? $v[2] : 'label';
+            } else {
+                $nums = $v;
+                $color = $colorArr[(is_numeric($nums) ? $nums : strlen($nums)) % $colorNums];
+                $class = 'label';
+            }
+            //必须nums大于0才显示
+            if ($nums) {
+                $badgeList[$url] = '<small class="' . $class . ' pull-right bg-' . $color . '">' . $nums . '</small>';
+            }
+        }
+
+        // 读取管理员当前拥有的权限节点
+        $userRule = $this->getRuleList();
+        $selected = $referer = [];
+        $refererUrl = Session::get('referer');
+        $pinyin = new \Overtrue\Pinyin\Pinyin('Overtrue\Pinyin\MemoryFileDictLoader');
+        // 必须将结果集转换为数组
+        $ruleList = collection(\app\admin\model\AuthRule::where('status', 'normal')
+            ->where('ismenu', 1)
+            ->order('weigh', 'desc')
+            ->cache("__menu__")
+            ->select())->toArray();
+        $indexRuleList = \app\admin\model\AuthRule::where('status', 'normal')
+            ->where('ismenu', 0)
+            ->where('name', 'like', '%/index')
+            ->column('name,pid');
+        $pidArr = array_filter(array_unique(array_map(function ($item) {
+            return $item['pid'];
+        }, $ruleList)));
+        foreach ($ruleList as $k => &$v) {
+            if (!in_array($v['name'], $userRule)) {
+                unset($ruleList[$k]);
+                continue;
+            }
+            $indexRuleName = $v['name'] . '/index';
+            if (isset($indexRuleList[$indexRuleName]) && !in_array($indexRuleName, $userRule)) {
+                unset($ruleList[$k]);
+                continue;
+            }
+            $v['icon'] = $v['icon'] . ' fa-fw';
+            $v['url'] = '/' . $module . '/' . $v['name'];
+            $v['badge'] = isset($badgeList[$v['name']]) ? $badgeList[$v['name']] : '';
+            $v['py'] = $pinyin->abbr($v['title'], '');
+            $v['pinyin'] = $pinyin->permalink($v['title'], '');
+            $v['title'] = __($v['title']);
+            $selected = $v['name'] == $fixedPage ? $v : $selected;
+            $referer = url($v['url']) == $refererUrl ? $v : $referer;
+        }
+        $lastArr = array_diff($pidArr, array_filter(array_unique(array_map(function ($item) {
+            return $item['pid'];
+        }, $ruleList))));
+        foreach ($ruleList as $index => $item) {
+            if (in_array($item['id'], $lastArr)) {
+                unset($ruleList[$index]);
+            }
+        }
+        if ($selected == $referer) {
+            $referer = [];
+        }
+        $selected && $selected['url'] = url($selected['url']);
+        $referer && $referer['url'] = url($referer['url']);
+
+        $select_id = $selected ? $selected['id'] : 0;
+        $menu = $nav = '';
+        if (Config::get('fastadmin.multiplenav')) {
+            $topList = [];
+            foreach ($ruleList as $index => $item) {
+                if (!$item['pid']) {
+                    $topList[] = $item;
+                }
+            }
+            $selectParentIds = [];
+            $tree = Tree::instance();
+            $tree->init($ruleList);
+            if ($select_id) {
+                $selectParentIds = $tree->getParentsIds($select_id, true);
+            }
+            foreach ($topList as $index => $item) {
+                $childList = Tree::instance()->getTreeMenu(
+                    $item['id'],
+                    '<li class="@class" pid="@pid"><a href="@url@addtabs" addtabs="@id" url="@url" py="@py" pinyin="@pinyin"><i class="@icon"></i> <span>@title</span> <span class="pull-right-container">@caret @badge</span></a> @childlist</li>',
+                    $select_id,
+                    '',
+                    'ul',
+                    'class="treeview-menu"'
+                );
+                $current = in_array($item['id'], $selectParentIds);
+                $url = $childList ? 'javascript:;' : url($item['url']);
+                $addtabs = $childList || !$url ? "" : (stripos($url, "?") !== false ? "&" : "?") . "ref=addtabs";
+                $childList = str_replace(
+                    '" pid="' . $item['id'] . '"',
+                    ' treeview ' . ($current ? '' : 'hidden') . '" pid="' . $item['id'] . '"',
+                    $childList
+                );
+                $nav .= '<li class="' . ($current ? 'active' : '') . '"><a href="' . $url . $addtabs . '" addtabs="' . $item['id'] . '" url="' . $url . '"><i class="' . $item['icon'] . '"></i> <span>' . $item['title'] . '</span> <span class="pull-right-container"> </span></a> </li>';
+                $menu .= $childList;
+            }
+        } else {
+            // 构造菜单数据
+            Tree::instance()->init($ruleList);
+            $menu = Tree::instance()->getTreeMenu(
+                0,
+                '<li class="@class"><a href="@url@addtabs" addtabs="@id" url="@url" py="@py" pinyin="@pinyin"><i class="@icon"></i> <span>@title</span> <span class="pull-right-container">@caret @badge</span></a> @childlist</li>',
+                $select_id,
+                '',
+                'ul',
+                'class="treeview-menu"'
+            );
+            if ($selected) {
+                $nav .= '<li role="presentation" id="tab_' . $selected['id'] . '" class="' . ($referer ? '' : 'active') . '"><a href="#con_' . $selected['id'] . '" node-id="' . $selected['id'] . '" aria-controls="' . $selected['id'] . '" role="tab" data-toggle="tab"><i class="' . $selected['icon'] . ' fa-fw"></i> <span>' . $selected['title'] . '</span> </a></li>';
+            }
+            if ($referer) {
+                $nav .= '<li role="presentation" id="tab_' . $referer['id'] . '" class="active"><a href="#con_' . $referer['id'] . '" node-id="' . $referer['id'] . '" aria-controls="' . $referer['id'] . '" role="tab" data-toggle="tab"><i class="' . $referer['icon'] . ' fa-fw"></i> <span>' . $referer['title'] . '</span> </a> <i class="close-tab fa fa-remove"></i></li>';
+            }
+        }
+
+        return [$menu, $nav, $selected, $referer];
+    }
+
+    /**
+     * 设置错误信息
+     *
+     * @param string $error 错误信息
+     * @return Auth
+     */
+    public function setError($error)
+    {
+        $this->_error = $error;
+        return $this;
+    }
+
+    /**
+     * 获取错误信息
+     * @return string
+     */
+    public function getError()
+    {
+        return $this->_error ? __($this->_error) : '';
+    }
+}

+ 479 - 0
application/admin/library/traits/Backend.php

@@ -0,0 +1,479 @@
+<?php
+
+namespace app\admin\library\traits;
+
+use app\admin\library\Auth;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
+use PhpOffice\PhpSpreadsheet\Reader\Xls;
+use PhpOffice\PhpSpreadsheet\Reader\Csv;
+use think\Db;
+use think\Exception;
+use think\exception\PDOException;
+use think\exception\ValidateException;
+
+trait Backend
+{
+
+    /**
+     * 排除前台提交过来的字段
+     * @param $params
+     * @return array
+     */
+    protected function preExcludeFields($params)
+    {
+        if (is_array($this->excludeFields)) {
+            foreach ($this->excludeFields as $field) {
+                if (key_exists($field, $params)) {
+                    unset($params[$field]);
+                }
+            }
+        } else {
+            if (key_exists($this->excludeFields, $params)) {
+                unset($params[$this->excludeFields]);
+            }
+        }
+        return $params;
+    }
+
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //设置过滤方法
+        $this->request->filter(['strip_tags']);
+        if ($this->request->isAjax()) {
+            //如果发送的来源是Selectpage,则转发到Selectpage
+            if ($this->request->request('keyField')) {
+                return $this->selectpage();
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                ->where($where)
+                ->order($sort, $order)
+                ->count();
+
+            $list = $this->model
+                ->where($where)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+
+            $list = collection($list)->toArray();
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 回收站
+     */
+    public function recyclebin()
+    {
+        //设置过滤方法
+        $this->request->filter(['strip_tags']);
+        if ($this->request->isAjax()) {
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                ->onlyTrashed()
+                ->where($where)
+                ->order($sort, $order)
+                ->count();
+
+            $list = $this->model
+                ->onlyTrashed()
+                ->where($where)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $params = $this->request->post("row/a");
+            if ($params) {
+                $params = $this->preExcludeFields($params);
+
+                if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
+                    $params[$this->dataLimitField] = $this->auth->id;
+                }
+                $result = false;
+                Db::startTrans();
+                try {
+                    //是否采用模型验证
+                    if ($this->modelValidate) {
+                        $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                        $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate;
+                        $this->model->validateFailException(true)->validate($validate);
+                    }
+                    $result = $this->model->allowField(true)->save($params);
+                    Db::commit();
+                } catch (ValidateException $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                } catch (PDOException $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                } catch (Exception $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                }
+                if ($result !== false) {
+                    $this->success();
+                } else {
+                    $this->error(__('No rows were inserted'));
+                }
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = null)
+    {
+        $row = $this->model->get($ids);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        $adminIds = $this->getDataLimitAdminIds();
+        if (is_array($adminIds)) {
+            if (!in_array($row[$this->dataLimitField], $adminIds)) {
+                $this->error(__('You have no permission'));
+            }
+        }
+        if ($this->request->isPost()) {
+            $params = $this->request->post("row/a");
+            if ($params) {
+                $params = $this->preExcludeFields($params);
+                $result = false;
+                Db::startTrans();
+                try {
+                    //是否采用模型验证
+                    if ($this->modelValidate) {
+                        $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                        $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate;
+                        $row->validateFailException(true)->validate($validate);
+                    }
+                    $result = $row->allowField(true)->save($params);
+                    Db::commit();
+                } catch (ValidateException $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                } catch (PDOException $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                } catch (Exception $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                }
+                if ($result !== false) {
+                    $this->success();
+                } else {
+                    $this->error(__('No rows were updated'));
+                }
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        $this->view->assign("row", $row);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 删除
+     */
+    public function del($ids = "")
+    {
+        if ($ids) {
+            $pk = $this->model->getPk();
+            $adminIds = $this->getDataLimitAdminIds();
+            if (is_array($adminIds)) {
+                $this->model->where($this->dataLimitField, 'in', $adminIds);
+            }
+            $list = $this->model->where($pk, 'in', $ids)->select();
+
+            $count = 0;
+            Db::startTrans();
+            try {
+                foreach ($list as $k => $v) {
+                    $count += $v->delete();
+                }
+                Db::commit();
+            } catch (PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            } catch (Exception $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($count) {
+                $this->success();
+            } else {
+                $this->error(__('No rows were deleted'));
+            }
+        }
+        $this->error(__('Parameter %s can not be empty', 'ids'));
+    }
+
+    /**
+     * 真实删除
+     */
+    public function destroy($ids = "")
+    {
+        $pk = $this->model->getPk();
+        $adminIds = $this->getDataLimitAdminIds();
+        if (is_array($adminIds)) {
+            $this->model->where($this->dataLimitField, 'in', $adminIds);
+        }
+        if ($ids) {
+            $this->model->where($pk, 'in', $ids);
+        }
+        $count = 0;
+        Db::startTrans();
+        try {
+            $list = $this->model->onlyTrashed()->select();
+            foreach ($list as $k => $v) {
+                $count += $v->delete(true);
+            }
+            Db::commit();
+        } catch (PDOException $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        } catch (Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success();
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+        $this->error(__('Parameter %s can not be empty', 'ids'));
+    }
+
+    /**
+     * 还原
+     */
+    public function restore($ids = "")
+    {
+        $pk = $this->model->getPk();
+        $adminIds = $this->getDataLimitAdminIds();
+        if (is_array($adminIds)) {
+            $this->model->where($this->dataLimitField, 'in', $adminIds);
+        }
+        if ($ids) {
+            $this->model->where($pk, 'in', $ids);
+        }
+        $count = 0;
+        Db::startTrans();
+        try {
+            $list = $this->model->onlyTrashed()->select();
+            foreach ($list as $index => $item) {
+                $count += $item->restore();
+            }
+            Db::commit();
+        } catch (PDOException $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        } catch (Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        if ($count) {
+            $this->success();
+        }
+        $this->error(__('No rows were updated'));
+    }
+
+    /**
+     * 批量更新
+     */
+    public function multi($ids = "")
+    {
+        $ids = $ids ? $ids : $this->request->param("ids");
+        if ($ids) {
+            if ($this->request->has('params')) {
+                parse_str($this->request->post("params"), $values);
+                $values = array_intersect_key($values, array_flip(is_array($this->multiFields) ? $this->multiFields : explode(',', $this->multiFields)));
+                if ($values || $this->auth->isSuperAdmin()) {
+                    $adminIds = $this->getDataLimitAdminIds();
+                    if (is_array($adminIds)) {
+                        $this->model->where($this->dataLimitField, 'in', $adminIds);
+                    }
+                    $count = 0;
+                    Db::startTrans();
+                    try {
+                        $list = $this->model->where($this->model->getPk(), 'in', $ids)->select();
+                        foreach ($list as $index => $item) {
+                            $count += $item->allowField(true)->isUpdate(true)->save($values);
+                        }
+                        Db::commit();
+                    } catch (PDOException $e) {
+                        Db::rollback();
+                        $this->error($e->getMessage());
+                    } catch (Exception $e) {
+                        Db::rollback();
+                        $this->error($e->getMessage());
+                    }
+                    if ($count) {
+                        $this->success();
+                    } else {
+                        $this->error(__('No rows were updated'));
+                    }
+                } else {
+                    $this->error(__('You have no permission'));
+                }
+            }
+        }
+        $this->error(__('Parameter %s can not be empty', 'ids'));
+    }
+
+    /**
+     * 导入
+     */
+    protected function import()
+    {
+        $file = $this->request->request('file');
+        if (!$file) {
+            $this->error(__('Parameter %s can not be empty', 'file'));
+        }
+        $filePath = ROOT_PATH . DS . 'public' . DS . $file;
+        if (!is_file($filePath)) {
+            $this->error(__('No results were found'));
+        }
+        //实例化reader
+        $ext = pathinfo($filePath, PATHINFO_EXTENSION);
+        if (!in_array($ext, ['csv', 'xls', 'xlsx'])) {
+            $this->error(__('Unknown data format'));
+        }
+        if ($ext === 'csv') {
+            $file = fopen($filePath, 'r');
+            $filePath = tempnam(sys_get_temp_dir(), 'import_csv');
+            $fp = fopen($filePath, "w");
+            $n = 0;
+            while ($line = fgets($file)) {
+                $line = rtrim($line, "\n\r\0");
+                $encoding = mb_detect_encoding($line, ['utf-8', 'gbk', 'latin1', 'big5']);
+                if ($encoding != 'utf-8') {
+                    $line = mb_convert_encoding($line, 'utf-8', $encoding);
+                }
+                if ($n == 0 || preg_match('/^".*"$/', $line)) {
+                    fwrite($fp, $line . "\n");
+                } else {
+                    fwrite($fp, '"' . str_replace(['"', ','], ['""', '","'], $line) . "\"\n");
+                }
+                $n++;
+            }
+            fclose($file) || fclose($fp);
+
+            $reader = new Csv();
+        } elseif ($ext === 'xls') {
+            $reader = new Xls();
+        } else {
+            $reader = new Xlsx();
+        }
+
+        //导入文件首行类型,默认是注释,如果需要使用字段名称请使用name
+        $importHeadType = isset($this->importHeadType) ? $this->importHeadType : 'comment';
+
+        $table = $this->model->getQuery()->getTable();
+        $database = \think\Config::get('database.database');
+        $fieldArr = [];
+        $list = db()->query("SELECT COLUMN_NAME,COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND TABLE_SCHEMA = ?", [$table, $database]);
+        foreach ($list as $k => $v) {
+            if ($importHeadType == 'comment') {
+                $fieldArr[$v['COLUMN_COMMENT']] = $v['COLUMN_NAME'];
+            } else {
+                $fieldArr[$v['COLUMN_NAME']] = $v['COLUMN_NAME'];
+            }
+        }
+
+        //加载文件
+        $insert = [];
+        try {
+            if (!$PHPExcel = $reader->load($filePath)) {
+                $this->error(__('Unknown data format'));
+            }
+            $currentSheet = $PHPExcel->getSheet(0);  //读取文件中的第一个工作表
+            $allColumn = $currentSheet->getHighestDataColumn(); //取得最大的列号
+            $allRow = $currentSheet->getHighestRow(); //取得一共有多少行
+            $maxColumnNumber = Coordinate::columnIndexFromString($allColumn);
+            $fields = [];
+            for ($currentRow = 1; $currentRow <= 1; $currentRow++) {
+                for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) {
+                    $val = $currentSheet->getCellByColumnAndRow($currentColumn, $currentRow)->getValue();
+                    $fields[] = $val;
+                }
+            }
+
+            for ($currentRow = 2; $currentRow <= $allRow; $currentRow++) {
+                $values = [];
+                for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) {
+                    $val = $currentSheet->getCellByColumnAndRow($currentColumn, $currentRow)->getValue();
+                    $values[] = is_null($val) ? '' : $val;
+                }
+                $row = [];
+                $temp = array_combine($fields, $values);
+                foreach ($temp as $k => $v) {
+                    if (isset($fieldArr[$k]) && $k !== '') {
+                        $row[$fieldArr[$k]] = $v;
+                    }
+                }
+                if ($row) {
+                    $insert[] = $row;
+                }
+            }
+        } catch (Exception $exception) {
+            $this->error($exception->getMessage());
+        }
+        if (!$insert) {
+            $this->error(__('No rows were updated'));
+        }
+
+        try {
+            //是否包含admin_id字段
+            $has_admin_id = false;
+            foreach ($fieldArr as $name => $key) {
+                if ($key == 'admin_id') {
+                    $has_admin_id = true;
+                    break;
+                }
+            }
+            if ($has_admin_id) {
+                $auth = Auth::instance();
+                foreach ($insert as &$val) {
+                    if (!isset($val['admin_id']) || empty($val['admin_id'])) {
+                        $val['admin_id'] = $auth->isLogin() ? $auth->id : 0;
+                    }
+                }
+            }
+            $this->model->saveAll($insert);
+        } catch (PDOException $exception) {
+            $msg = $exception->getMessage();
+            if (preg_match("/.+Integrity constraint violation: 1062 Duplicate entry '(.+)' for key '(.+)'/is", $msg, $matches)) {
+                $msg = "导入失败,包含【{$matches[1]}】的记录已存在";
+            };
+            $this->error($msg);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+
+        $this->success();
+    }
+}

+ 34 - 0
application/admin/model/Admin.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+use think\Session;
+
+class Admin extends Model
+{
+
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+
+    /**
+     * 重置用户密码
+     * @author baiyouwen
+     */
+    public function resetPassword($uid, $NewPassword)
+    {
+        $passwd = $this->encryptPassword($NewPassword);
+        $ret = $this->where(['id' => $uid])->update(['password' => $passwd]);
+        return $ret;
+    }
+
+    // 密码加密
+    protected function encryptPassword($password, $salt = '', $encrypt = 'md5')
+    {
+        return $encrypt($password . $salt);
+    }
+
+}

+ 69 - 0
application/admin/model/AdminLog.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace app\admin\model;
+
+use app\admin\library\Auth;
+use think\Model;
+
+class AdminLog extends Model
+{
+
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = '';
+    //自定义日志标题
+    protected static $title = '';
+    //自定义日志内容
+    protected static $content = '';
+
+    public static function setTitle($title)
+    {
+        self::$title = $title;
+    }
+
+    public static function setContent($content)
+    {
+        self::$content = $content;
+    }
+
+    public static function record($title = '')
+    {
+        $auth = Auth::instance();
+        $admin_id = $auth->isLogin() ? $auth->id : 0;
+        $username = $auth->isLogin() ? $auth->username : __('Unknown');
+        $content = self::$content;
+        if (!$content) {
+            $content = request()->param('', null, 'trim,strip_tags,htmlspecialchars');
+            foreach ($content as $k => $v) {
+                if (is_string($v) && strlen($v) > 200 || stripos($k, 'password') !== false) {
+                    unset($content[$k]);
+                }
+            }
+        }
+        $title = self::$title;
+        if (!$title) {
+            $title = [];
+            $breadcrumb = Auth::instance()->getBreadcrumb();
+            foreach ($breadcrumb as $k => $v) {
+                $title[] = $v['title'];
+            }
+            $title = implode(' ', $title);
+        }
+        self::create([
+            'title'     => $title,
+            'content'   => !is_scalar($content) ? json_encode($content) : $content,
+            'url'       => substr(request()->url(), 0, 1500),
+            'admin_id'  => $admin_id,
+            'username'  => $username,
+            'useragent' => substr(request()->server('HTTP_USER_AGENT'), 0, 255),
+            'ip'        => request()->ip()
+        ]);
+    }
+
+    public function admin()
+    {
+        return $this->belongsTo('Admin', 'admin_id')->setEagerlyType(0);
+    }
+}

+ 21 - 0
application/admin/model/AuthGroup.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+class AuthGroup extends Model
+{
+
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+
+    public function getNameAttr($value, $data)
+    {
+        return __($value);
+    }
+
+}

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