1557492053 1 gadu atpakaļ
vecāks
revīzija
60994ee8f3
100 mainītis faili ar 14988 papildinājumiem un 0 dzēšanām
  1. 11 0
      .bowerrc
  2. 11 0
      .env.sample
  3. 17 0
      .gitignore
  4. 1 0
      .htaccess
  5. 7 0
      404.html
  6. 191 0
      LICENSE
  7. 1 0
      addons/.gitkeep
  8. 1 0
      addons/.htaccess
  9. 1 0
      addons/baidupush/.addonrc
  10. 88 0
      addons/baidupush/Baidupush.php
  11. 62 0
      addons/baidupush/config.php
  12. 15 0
      addons/baidupush/controller/Index.php
  13. 10 0
      addons/baidupush/info.ini
  14. 106 0
      addons/baidupush/library/Push.php
  15. 75 0
      addons/baidupush/library/push/Driver.php
  16. 87 0
      addons/baidupush/library/push/driver/Daily.php
  17. 87 0
      addons/baidupush/library/push/driver/Normal.php
  18. 1 0
      addons/baiduwebtongji/.addonrc
  19. 120 0
      addons/baiduwebtongji/Baiduwebtongji.php
  20. 141 0
      addons/baiduwebtongji/config.php
  21. 15 0
      addons/baiduwebtongji/controller/Index.php
  22. 10 0
      addons/baiduwebtongji/info.ini
  23. 91 0
      addons/baiduwebtongji/library/DataApiConnection.php
  24. 115 0
      addons/baiduwebtongji/library/ReportService.php
  25. 314 0
      addons/baiduwebtongji/library/TongjiApi.php
  26. 1 0
      addons/cms/.addonrc
  27. 1 0
      addons/cms/.gitignore
  28. 216 0
      addons/cms/Cms.php
  29. 11 0
      addons/cms/README.md
  30. 22 0
      addons/cms/bootstrap.js
  31. 255 0
      addons/cms/config.html
  32. 1063 0
      addons/cms/config.php
  33. 222 0
      addons/cms/controller/Ajax.php
  34. 224 0
      addons/cms/controller/Api.php
  35. 146 0
      addons/cms/controller/Archives.php
  36. 78 0
      addons/cms/controller/Base.php
  37. 188 0
      addons/cms/controller/Channel.php
  38. 70 0
      addons/cms/controller/Comment.php
  39. 325 0
      addons/cms/controller/Diyform.php
  40. 33 0
      addons/cms/controller/Go.php
  41. 46 0
      addons/cms/controller/Import.php
  42. 40 0
      addons/cms/controller/Index.php
  43. 92 0
      addons/cms/controller/Order.php
  44. 69 0
      addons/cms/controller/Page.php
  45. 257 0
      addons/cms/controller/Search.php
  46. 243 0
      addons/cms/controller/Sitemap.php
  47. 100 0
      addons/cms/controller/Special.php
  48. 118 0
      addons/cms/controller/Tag.php
  49. 79 0
      addons/cms/controller/User.php
  50. 246 0
      addons/cms/controller/wxapp/Archives.php
  51. 57 0
      addons/cms/controller/wxapp/Base.php
  52. 63 0
      addons/cms/controller/wxapp/Comment.php
  53. 79 0
      addons/cms/controller/wxapp/Common.php
  54. 34 0
      addons/cms/controller/wxapp/Index.php
  55. 55 0
      addons/cms/controller/wxapp/My.php
  56. 249 0
      addons/cms/controller/wxapp/User.php
  57. 1 0
      addons/cms/data/dict.json
  58. 1 0
      addons/cms/data/dict.txt
  59. 274 0
      addons/cms/data/menu.php
  60. 1 0
      addons/cms/data/words.dic
  61. 10 0
      addons/cms/info.ini
  62. 558 0
      addons/cms/install.sql
  63. 91 0
      addons/cms/lang/zh-cn.php
  64. 175 0
      addons/cms/library/Alter.php
  65. 215 0
      addons/cms/library/Bootstrap.php
  66. 18 0
      addons/cms/library/CommentException.php
  67. 173 0
      addons/cms/library/FulltextSearch.php
  68. 181 0
      addons/cms/library/HashMap.php
  69. 51 0
      addons/cms/library/IntCode.php
  70. 76 0
      addons/cms/library/Jssdk.php
  71. 229 0
      addons/cms/library/Order.php
  72. 18 0
      addons/cms/library/OrderException.php
  73. 297 0
      addons/cms/library/SensitiveHelper.php
  74. 679 0
      addons/cms/library/Service.php
  75. 115 0
      addons/cms/library/VicDict.php
  76. 271 0
      addons/cms/library/VicWord.php
  77. 27 0
      addons/cms/library/aip/AipContentCensor.php
  78. 74 0
      addons/cms/library/aip/AipImageCensor.php
  79. 456 0
      addons/cms/library/aip/AipNlp.php
  80. 1155 0
      addons/cms/library/aip/AipOcr.php
  81. 401 0
      addons/cms/library/aip/lib/AipBase.php
  82. 227 0
      addons/cms/library/aip/lib/AipHttpClient.php
  83. 181 0
      addons/cms/library/aip/lib/AipHttpUtil.php
  84. 181 0
      addons/cms/library/aip/lib/AipSampleSigner.php
  85. 19 0
      addons/cms/library/aip/lib/AipSignOption.php
  86. 420 0
      addons/cms/library/hashids/Hashids.php
  87. 24 0
      addons/cms/library/hashids/HashidsException.php
  88. 57 0
      addons/cms/library/hashids/HashidsInterface.php
  89. 20 0
      addons/cms/library/hashids/LICENSE
  90. 123 0
      addons/cms/library/hashids/Math/Bc.php
  91. 123 0
      addons/cms/library/hashids/Math/Gmp.php
  92. 99 0
      addons/cms/library/hashids/Math/MathInterface.php
  93. 49 0
      addons/cms/library/hashids/composer.json
  94. 638 0
      addons/cms/model/Archives.php
  95. 30 0
      addons/cms/model/Autolink.php
  96. 192 0
      addons/cms/model/Block.php
  97. 505 0
      addons/cms/model/Channel.php
  98. 39 0
      addons/cms/model/Collection.php
  99. 254 0
      addons/cms/model/Comment.php
  100. 0 0
      addons/cms/model/Diydata.php

+ 11 - 0
.bowerrc

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

+ 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_

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+/nbproject/
+/thinkphp/
+/vendor/
+/runtime/*
+/public/assets/libs/
+/public/assets/addons/*
+/public/uploads/*
+.idea
+composer.lock
+*.log
+*.css.map
+!.gitkeep
+.env
+.svn
+.vscode
+node_modules
+.user.ini

+ 1 - 0
.htaccess

@@ -0,0 +1 @@
+ 

+ 7 - 0
404.html

@@ -0,0 +1,7 @@
+<html>
+<head><title>404 Not Found</title></head>
+<body>
+<center><h1>404 Not Found</h1></center>
+<hr><center>nginx</center>
+</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.

+ 1 - 0
addons/.gitkeep

@@ -0,0 +1 @@
+

+ 1 - 0
addons/.htaccess

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

+ 1 - 0
addons/baidupush/.addonrc

@@ -0,0 +1 @@
+{"files":["application\/admin\/controller\/Baidupush.php","application\/admin\/view\/baidupush\/index.html","public\/assets\/js\/backend\/baidupush.js"],"license":"regular","licenseto":"72930","licensekey":"4shJFnSbNZ2Ymr9K Nh6dJlti9SVz2bsD+Et5d9PgRkrlH94VrSizk7IvCkA=","domains":["55kaifa.com"],"licensecodes":[],"validations":["0eca98f4692eac6c6eb7e256a43d97f4"],"menus":["baidupush","baidupush\/index","baidupush\/normal","baidupush\/daily"]}

+ 88 - 0
addons/baidupush/Baidupush.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace addons\baidupush;
+
+use addons\baidupush\library\Push;
+use app\common\library\Menu;
+use think\Addons;
+
+/**
+ * 百度推送插件
+ */
+class Baidupush extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        $menu = [
+            [
+                'name'    => 'baidupush',
+                'title'   => '百度推送管理',
+                'icon'    => 'fa fa-paper-plane',
+                'sublist' => [
+                    ['name' => 'baidupush/index', 'title' => '查看'],
+                    ['name' => 'baidupush/normal', 'title' => '普通收录提交'],
+                    ['name' => 'baidupush/daily', 'title' => '快速收录提交'],
+                ]
+            ]
+        ];
+        Menu::create($menu);
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        Menu::delete('baidupush');
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     * @return bool
+     */
+    public function enable()
+    {
+        Menu::enable('baidupush');
+        return true;
+    }
+
+    /**
+     * 插件禁用方法
+     * @return bool
+     */
+    public function disable()
+    {
+        Menu::disable('baidupush');
+        return true;
+    }
+
+    /**
+     * 实现钩子方法
+     * @param mixed $params URL数组
+     * @param null  $extra
+     * @return mixed
+     */
+    public function baidupush($params, $extra = null)
+    {
+        $config = get_addon_config('baidupush');
+        $statusArr = explode(',', $config['status']);
+        $urls = is_string($params) ? [$params] : $params;
+        $extra = $extra ? $extra : 'urls';
+        foreach ($statusArr as $index => $item) {
+            if ($extra == 'urls' || $extra == 'append') {
+                Push::connect(['type' => $item])->realtime($urls);
+            } elseif ($extra == 'del' || $extra == 'delete') {
+                Push::connect(['type' => $item])->delete($urls);
+            }
+        }
+    }
+
+}

+ 62 - 0
addons/baidupush/config.php

@@ -0,0 +1,62 @@
+<?php
+
+return [
+    [
+        'name' => 'daily',
+        'title' => '快速收录',
+        'type' => 'array',
+        'content' => [],
+        'value' => [
+            'site' => 'https://www.55kaifa.com',
+            'token' => 'PsjPJMsw5EIT9Xpp',
+        ],
+        'rule' => '',
+        'msg' => '',
+        'tip' => '请前往百度站长平台获取',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'normal',
+        'title' => '普通收录',
+        'type' => 'array',
+        'content' => [],
+        'value' => [
+            'site' => 'https://www.55kaifa.com',
+            'token' => 'PsjPJMsw5EIT9Xpp',
+        ],
+        'rule' => '',
+        'msg' => '',
+        'tip' => '请前往百度站长平台获取',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'status',
+        'title' => '推送状态',
+        'type' => 'checkbox',
+        'content' => [
+            'normal' => '普通收录',
+            'daily' => '快速收录',
+        ],
+        'value' => 'normal,daily',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => '__tips__',
+        'title' => '温馨提示',
+        'type' => 'string',
+        'content' => [],
+        'value' => '1.普通收录请前往<a href="https://ziyuan.baidu.com/linksubmit/index" target="_blank">百度站长平台(普通收录)</a>获取Site和Token<br>'."\n"
+            .'                      2.快速收录请前往<a href="https://ziyuan.baidu.com/dailysubmit/index" target="_blank">百度站长平台(快速收录)</a>获取Site和Token',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+];

+ 15 - 0
addons/baidupush/controller/Index.php

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

+ 10 - 0
addons/baidupush/info.ini

@@ -0,0 +1,10 @@
+name = baidupush
+title = 百度搜索主动推送
+intro = 百度搜索主动推送,支持普通收录和快速收录
+author = FastAdmin
+website = https://www.fastadmin.net
+version = 1.0.3
+state = 1
+url = /addons/baidupush
+license = regular
+licenseto = 72930

+ 106 - 0
addons/baidupush/library/Push.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace addons\baidupush\library;
+
+use addons\baidupush\library\push\Driver;
+
+/**
+ * Push操作类
+ */
+class Push
+{
+    /**
+     * @var array Push的实例
+     */
+    public static $instance = [];
+
+    /**
+     * @var object 操作句柄
+     */
+    public static $handler;
+
+    /**
+     * 连接Push驱动
+     * @access public
+     * @param array       $options 配置数组
+     * @param bool|string $name    Push连接标识 true 强制重新初始化
+     * @return Driver
+     */
+    public static function connect(array $options = [], $name = false)
+    {
+        $type = !empty($options['type']) ? $options['type'] : 'normal';
+
+        $config = get_addon_config('baidupush');
+
+        $type = strtolower($type);
+
+        $options = array_merge($options, isset($config[$type]) ? $config[$type] : []);
+
+        if (false === $name) {
+            $name = md5(serialize($options));
+        }
+
+        if (true === $name || !isset(self::$instance[$name])) {
+            $class = false === strpos($type, '\\') ?
+                '\\addons\\baidupush\\library\\push\\driver\\' . ucwords($type) :
+                $type;
+
+            if (true === $name) {
+                return new $class($options);
+            }
+
+            self::$instance[$name] = new $class($options);
+        }
+
+        return self::$instance[$name];
+    }
+
+    /**
+     * 自动初始化Push
+     * @access public
+     * @param array $options 配置数组
+     * @return Driver
+     */
+    public static function init(array $options = [])
+    {
+        if (is_null(self::$handler)) {
+            self::$handler = self::connect($options);
+        }
+
+        return self::$handler;
+    }
+
+    /**
+     * 推送实时链接
+     * @access public
+     * @param array $urls URL数组
+     * @return bool
+     */
+    public static function realtime($urls)
+    {
+        return self::init()->realtime($urls);
+    }
+
+    /**
+     * 推送历史链接
+     * @access public
+     * @param array $urls URL数组
+     * @return bool
+     */
+    public static function history($urls)
+    {
+        return self::init()->history($urls);
+    }
+
+    /**
+     * 删除链接
+     * @access public
+     * @param array $urls URL数组
+     * @return mixed
+     */
+    public static function delete($urls)
+    {
+        return self::init()->delete($urls);
+    }
+
+}

+ 75 - 0
addons/baidupush/library/push/Driver.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace addons\baidupush\library\push;
+
+/**
+ * Push基础类
+ */
+abstract class Driver
+{
+    protected $options = [];
+    protected $error = '';
+    protected $data = [
+        "remain"        => 0,
+        "success"       => 0,
+        "not_same_site" => [],
+        "not_valid"     => []
+    ];
+
+    /**
+     * 推送实时链接
+     * @param array $urls URL链接数组
+     * @return bool
+     */
+    abstract public function realtime($urls);
+
+    /**
+     * 推送历史链接
+     * @param array $urls URL链接数组
+     * @return  bool
+     */
+    abstract public function history($urls);
+
+    /**
+     * 删除链接
+     * @param array $urls URL链接数组
+     * @return  boolean
+     */
+    abstract public function delete($urls);
+
+    /**
+     * 获取错误信息
+     * @return string
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    /**
+     * 设置错误信息
+     * @param string $msg
+     */
+    protected function setError($msg)
+    {
+        $this->error = $msg;
+    }
+
+    /**
+     * 获取返回的数据
+     * @return mixed
+     */
+    public function getData()
+    {
+        return $this->data;
+    }
+
+    /**
+     * 设置返回的数据
+     * @param mixed $data
+     */
+    protected function setData($data)
+    {
+        $this->data = array_merge($this->data, $data);
+    }
+}

+ 87 - 0
addons/baidupush/library/push/driver/Daily.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace addons\baidupush\library\push\driver;
+
+use addons\baidupush\library\push\Driver;
+use fast\Http;
+
+/**
+ * 快速收录
+ */
+class Daily extends Driver
+{
+
+    protected $options = [
+        'site'  => '',
+        'token' => '',
+    ];
+
+    /**
+     * 构造函数
+     * @param array $options 参数
+     * @access public
+     */
+    public function __construct($options = [])
+    {
+        if (!empty($options)) {
+            $this->options['site'] = isset($options['site']) ? $options['site'] : '';
+            $this->options['token'] = isset($options['token']) ? $options['token'] : '';
+        }
+    }
+
+    /**
+     * 推送实时链接
+     * @param array $urls URL链接数组
+     * @return bool
+     */
+    public function realtime($urls)
+    {
+        return $this->request($urls, 'urls');
+    }
+
+    /**
+     * 推送历史链接
+     * @param array $urls URL链接数组
+     * @return  bool
+     */
+    public function history($urls)
+    {
+        return $this->realtime($urls);
+    }
+
+    /**
+     * 删除链接
+     * @param array $urls URL链接数组
+     * @return  bool
+     */
+    public function delete($urls)
+    {
+        return $this->request($urls, 'del');
+    }
+
+    protected function request($urls, $type)
+    {
+        $url = "http://data.zz.baidu.com/{$type}?site={$this->options['site']}&token={$this->options['token']}&type=daily";
+        try {
+            $options = [
+                CURLOPT_HTTPHEADER => [
+                    'Content-Type: text/plain'
+                ]
+            ];
+
+            $ret = Http::sendRequest($url, implode("\n", $urls), 'POST', $options);
+            if ($ret['ret']) {
+                $json = (array)json_decode($ret['msg'], true);
+                if (!$json || isset($json['error'])) {
+                    $this->setError($json['message']);
+                    return false;
+                }
+                $this->setData($json);
+                return true;
+            }
+        } catch (\Exception $e) {
+            $this->setError($e->getMessage());
+        }
+        return false;
+    }
+}

+ 87 - 0
addons/baidupush/library/push/driver/Normal.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace addons\baidupush\library\push\driver;
+
+use addons\baidupush\library\push\Driver;
+use fast\Http;
+
+/**
+ * 普通收录
+ */
+class Normal extends Driver
+{
+
+    protected $options = [
+        'site'  => '',
+        'token' => '',
+    ];
+
+    /**
+     * 构造函数
+     * @param array $options 参数
+     * @access public
+     */
+    public function __construct($options = [])
+    {
+        if (!empty($options)) {
+            $this->options['site'] = isset($options['site']) ? $options['site'] : '';
+            $this->options['token'] = isset($options['token']) ? $options['token'] : '';
+        }
+    }
+
+    /**
+     * 推送实时链接
+     * @param array $urls URL链接数组
+     * @return bool
+     */
+    public function realtime($urls)
+    {
+        return $this->request($urls, 'urls');
+    }
+
+    /**
+     * 推送历史链接
+     * @param array $urls URL链接数组
+     * @return  bool
+     */
+    public function history($urls)
+    {
+        return $this->realtime($urls);
+    }
+
+    /**
+     * 删除链接
+     * @param array $urls URL链接数组
+     * @return  bool
+     */
+    public function delete($urls)
+    {
+        return $this->request($urls, 'del');
+    }
+
+    protected function request($urls, $type)
+    {
+        $url = "http://data.zz.baidu.com/{$type}?site={$this->options['site']}&token={$this->options['token']}";
+        try {
+            $options = [
+                CURLOPT_HTTPHEADER => [
+                    'Content-Type: text/plain'
+                ]
+            ];
+
+            $ret = Http::sendRequest($url, implode("\n", $urls), 'POST', $options);
+            if ($ret['ret']) {
+                $json = (array)json_decode($ret['msg'], true);
+                if (!$json || isset($json['error'])) {
+                    $this->setError($json['message']);
+                    return false;
+                }
+                $this->setData($json);
+                return true;
+            }
+        } catch (\Exception $e) {
+            $this->setError($e->getMessage());
+        }
+        return false;
+    }
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
addons/baiduwebtongji/.addonrc


+ 120 - 0
addons/baiduwebtongji/Baiduwebtongji.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace addons\baiduwebtongji;
+
+use app\common\library\Menu;
+use think\Addons;
+
+/**
+ * 百度网站统计插件
+ */
+class Baiduwebtongji extends Addons
+{
+    protected $menu = [
+        [
+            'name'    => 'baiduwebtongji',
+            'title'   => '百度网站统计',
+            'icon'    => 'fa fa-line-chart',
+            'sublist' => [
+                [
+                    'name'    => 'baiduwebtongji/index',
+                    'title'   => '网站概况',
+                    'icon'    => 'fa fa-circle-o',
+                    'sublist' => [
+                        ['name' => 'baiduwebtongji/index/index', 'title' => '查看']
+                    ]
+                ],
+                [
+                    'name'    => 'baiduwebtongji/latest',
+                    'title'   => '实时访客',
+                    'icon'    => 'fa fa-circle-o',
+                    'sublist' => [
+                        ['name' => 'baiduwebtongji/latest/index', 'title' => '查看']
+                    ]
+                ],
+                [
+                    'name'    => 'baiduwebtongji/source',
+                    'title'   => '访客来源',
+                    'icon'    => 'fa fa-circle-o',
+                    'sublist' => [
+                        ['name' => 'baiduwebtongji/source/index', 'title' => '查看']
+                    ]
+                ],
+                [
+                    'name'    => 'baiduwebtongji/toppage',
+                    'title'   => '受访页面',
+                    'icon'    => 'fa fa-circle-o',
+                    'sublist' => [
+                        ['name' => 'baiduwebtongji/toppage/index', 'title' => '查看']
+                    ]
+                ],
+                [
+                    'name'    => 'baiduwebtongji/district',
+                    'title'   => '地域分布',
+                    'icon'    => 'fa fa-circle-o',
+                    'sublist' => [
+                        ['name' => 'baiduwebtongji/district/index', 'title' => '查看']
+                    ]
+                ],
+                [
+                    'name'    => 'baiduwebtongji/searchword',
+                    'title'   => '搜索词',
+                    'icon'    => 'fa fa-circle-o',
+                    'sublist' => [
+                        ['name' => 'baiduwebtongji/searchword/index', 'title' => '查看']
+                    ]
+                ]
+            ]
+        ]
+    ];
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        
+        Menu::create($this->menu);
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        Menu::delete('baiduwebtongji');
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     * @return bool
+     */
+    public function enable()
+    {
+        Menu::enable('baiduwebtongji');
+    }
+
+    /**
+     * 插件禁用方法
+     * @return bool
+     */
+    public function disable()
+    {
+        Menu::disable('baiduwebtongji');
+    }
+
+    /**
+     * 插件升级方法
+     * @return bool
+     */
+    public function upgrade()
+    {
+        //如果菜单有变更则升级菜单
+        Menu::upgrade('baiduwebtongji', $this->menu);
+        return true;
+    }
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 141 - 0
addons/baiduwebtongji/config.php


+ 15 - 0
addons/baiduwebtongji/controller/Index.php

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

+ 10 - 0
addons/baiduwebtongji/info.ini

@@ -0,0 +1,10 @@
+name = baiduwebtongji
+title = 百度网站统计
+intro = 百度网站统计插件
+author = chicharito
+website = https://www.fastadmin.net
+version = 2.1.3
+state = 1
+url = /addons/baiduwebtongji
+license = regular
+licenseto = 72930

+ 91 - 0
addons/baiduwebtongji/library/DataApiConnection.php

@@ -0,0 +1,91 @@
+<?php
+namespace addons\baiduwebtongji\library;
+/**
+ * class DataApiConnection, provide POST method: send POST request to DataApi URL
+ */
+class DataApiConnection {
+    /**
+     * @var string
+     */
+    private $url;
+
+    /**
+     * @var string
+     */
+    private $headers;
+
+    /**
+     * @var string
+     */
+    private $postData;
+    
+    /**
+     * @var string
+     */
+    public $retHead;
+    
+    /**
+     * @var string
+     */
+    public $retBody;
+    
+    /**
+     * @var string
+     */
+    public $retRaw;
+
+    /**
+     * init
+     * @param string $url
+     * @param string $ucid
+     */
+    public function init($url) {
+        $this->url = $url;
+        $this->headers = array('UUID: '.UUID, 'Content-Type:  data/json;charset=UTF-8');
+    }
+
+    /**
+     * generate post data
+     * @param array $data
+     */
+    public function genPostData($data) {
+        $this->postData = json_encode($data);
+    }
+
+    /**
+     * post
+     * @param array $data
+     */
+    public function POST($data) {
+        $this->genPostData($data);
+
+        $curl = curl_init();
+        curl_setopt($curl, CURLOPT_URL, $this->url);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
+        curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
+        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
+        curl_setopt($curl, CURLOPT_AUTOREFERER, 1);
+        curl_setopt($curl, CURLOPT_HTTPHEADER, $this->headers);
+        curl_setopt($curl, CURLOPT_POST, 1);
+        curl_setopt($curl, CURLOPT_POSTFIELDS, $this->postData);
+        curl_setopt($curl, CURLOPT_TIMEOUT, 30);
+        curl_setopt($curl, CURLOPT_HEADER, 0);
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+
+        $tmpRet = curl_exec($curl);
+        if (curl_errno($curl)) {
+            return '[error] CURL ERROR: ' . curl_error($curl) . PHP_EOL;
+        }
+        curl_close($curl);
+        $tmpArray = json_decode($tmpRet, true);
+        if (isset($tmpArray['header']) && isset($tmpArray['body'])) {
+            $this->retHead = $tmpArray['header'];
+            $this->retBody = $tmpArray['body'];
+            $this->retRaw = $tmpRet;
+        }
+        else {
+            return "[error] SERVICE ERROR: {$tmpRet}" . PHP_EOL;
+        }
+    }
+}

+ 115 - 0
addons/baiduwebtongji/library/ReportService.php

@@ -0,0 +1,115 @@
+<?php
+namespace addons\baiduwebtongji\library;
+use addons\baiduwebtongji\library\DataApiConnection;
+/**
+ * class ReportService
+ */
+
+/**
+ * ReportService
+ */
+class ReportService {
+    private $apiUrl;
+    private $userName;
+    private $token;
+    private $ucid;
+    private $password;
+    
+    /**
+     * construct
+     * @param string $apiUrl
+     * @param string $userName
+     * @param string $token
+     * @param string $ucid
+     * @param string $password
+     */
+    public function __construct($config) {
+        define('UUID', $config['uuid']);
+        $this->config=$config;
+        if(ACCOUNT_TYPE==1 || ACCOUNT_TYPE==2){
+            $config_url=$report_url="https://api.baidu.com/json/tongji/v1/ReportService";
+            if(ACCOUNT_TYPE==1){
+                $this->header=[
+                    'username' => $config['username'],
+                    'password' => $config['password'],
+                    'token' => $config['token'],
+                    'account_type' => ACCOUNT_TYPE,
+                ];
+            }elseif(ACCOUNT_TYPE==2){
+                $this->header=[
+                    'userName' => $config['username'],
+                    'accessToken' => $config['token']
+                ];
+            }
+        }elseif(ACCOUNT_TYPE==3){
+            $config_url="https://openapi.baidu.com/rest/2.0/tongji/config/getSiteList?access_token=".$config['token'];
+            $report_url="https://openapi.baidu.com/rest/2.0/tongji/report/getData?access_token=".$config['token'];
+        }
+        $this->config_url = $config_url;
+        $this->report_url = $report_url;
+    }
+    
+    /**
+     * get site list
+     * @return array
+     */
+    public function getSiteList() {
+        if(ACCOUNT_TYPE==3){
+            $res=json_decode(file_get_contents($this->config_url),true);
+            if(isset($res['error_code'])){
+                $tokenArr=json_decode(file_get_contents("https://openapi.baidu.com/oauth/2.0/token?grant_type=refresh_token&refresh_token=".$this->config['refresh_token']."&client_id=".$this->config['apikey']."&client_secret=".$this->config['secretkey']),true);
+                set_addon_config("baiduwebtongji",['token'=>$tokenArr['access_token'],'refresh_token'=>$tokenArr['refresh_token']]);
+                $res=json_decode(file_get_contents("https://openapi.baidu.com/rest/2.0/tongji/config/getSiteList?access_token=".$tokenArr['access_token']),true);
+            }
+            return $res;
+        }else{
+            $apiConnection = new DataApiConnection();
+            $apiConnection->init($this->config_url . '/getSiteList');
+
+            $apiConnectionData = array(
+                'header' => $this->header,
+                'body' => null,
+            );
+            $apiConnection->POST($apiConnectionData);
+            
+            return array(
+                'header' => $apiConnection->retHead,
+                'body' => $apiConnection->retBody,
+                'raw' => $apiConnection->retRaw,
+            );
+        }
+    }
+
+    /**
+     * get data
+     * @param array $parameters
+     * @return array
+     */
+    public function getData($parameters) {
+        if(ACCOUNT_TYPE==3){
+            $str=[];
+            foreach ($parameters as $key => $value) {
+                $str[]=$key."=".$value;
+            }
+            $str=implode("&",$str);
+            $url=$this->report_url."&".$str;
+            $res=json_decode(file_get_contents($url),true);
+            return $res;
+        }else{
+            $apiConnection = new DataApiConnection();
+            $apiConnection->init($this->report_url . '/getData');
+
+            $apiConnectionData = array(
+                'header' => $this->header,
+                'body' => $parameters,
+            );
+            $apiConnection->POST($apiConnectionData);
+            
+            return array(
+                'header' => $apiConnection->retHead,
+                'body' => $apiConnection->retBody,
+                'raw' => $apiConnection->retRaw,
+            );
+        }
+    }
+}

+ 314 - 0
addons/baiduwebtongji/library/TongjiApi.php

@@ -0,0 +1,314 @@
+<?php
+namespace addons\baiduwebtongji\library;
+use addons\baiduwebtongji\library\ReportService;
+/**
+ * 百度网站统计api
+ */
+class TongjiApi {
+	protected $domain;
+	protected $siteId;
+	/**
+	* 参数赋值、登录获取siteId
+	*/
+	public function __construct($config)
+	{
+		// define('LOGIN_URL', 'https://api.baidu.com/sem/common/HolmesLoginService');
+		// define('API_URL', 'https://api.baidu.com/json/tongji/v1/ReportService');
+		define('ACCOUNT_TYPE', $config['account_type']); 
+		// $this->domain=$config['domain'];
+		$this->config=$config;
+		$this->siteId=$config['siteId'];	
+    }
+    /**
+	* 获取数据
+	* @param string $method 查询方法
+	* @param string $mertics 查询参数
+	* @param string $order 排序
+	*/
+	private function getData($param)
+	{
+		$reportService = new ReportService($this->config);
+		// $reportService = new ReportService(API_URL, USERNAME,TOKEN,PASSWORD);
+		if(!$this->siteId){
+			return false;
+			// $ret = $reportService->getSiteList();
+			// $siteList = $ret['body']['data'][0]['list'];
+			// if($siteList){
+			// 	if (count($siteList) > 0) {
+			// 		if($this->domain){
+			// 			foreach ($siteList as $key => $value) {
+			// 				if($this->domain==$value["domain"]){
+			// 					$this->siteId=$value["site_id"];
+			// 					break;
+			// 				}else{
+			// 					continue;
+			// 				}
+			// 			}
+			// 			if(!$this->siteId){
+			// 				$this->siteId = $siteList[0]['site_id'];
+			// 			}
+			// 		}else{
+			// 			$this->siteId = $siteList[0]['site_id'];
+			// 		}
+			// 	}else{
+			// 		return false;
+			// 	}
+			// }else{
+			// 	return false;
+			// }
+		}
+		$param['site_id']=$this->siteId;
+	    $res = $reportService->getData($param);
+	    if(ACCOUNT_TYPE==3){
+	    	return $res;
+	    }else{
+	    	return json_decode($res['raw'],true);
+	    }
+	}
+	public function getSiteList(){
+		// $reportService = new ReportService(API_URL, USERNAME,TOKEN,PASSWORD);
+		$reportService = new ReportService($this->config);
+		$ret = $reportService->getSiteList();
+		if(ACCOUNT_TYPE==3){
+			$siteList = $ret['list'];
+		}else{
+			$siteList = $ret['body']['data'][0]['list'];
+		}
+		$data=[];
+		if($siteList){
+			foreach ($siteList as $key => $value) {
+				$data[]=[
+					'site_id'=>$value["site_id"],
+					'domain'=>$value["domain"]
+				];
+			}
+		}
+		return $data;
+	}
+	/**
+	 * 获取网站今日流量与昨日对比
+	 */
+	public function getOutline()
+	{
+		$param=[
+			'method'		=>	'overview/getOutline',
+		];
+		return $this->getData($param);
+	}
+	/**
+	* 获取今日趋势分析数据
+	*/
+	public function trendTimeA()
+	{
+		$param=[
+			'method'	=>	'trend/time/a',
+			'metrics'	=>	'pv_count,visitor_count',//,ip_count,bounce_ratio,avg_visit_time
+			'start_date'=>	date("Ymd"),
+			'end_date'	=>	date("Ymd"), 
+			'order'		=>	'simple_date_title,asc'
+		];
+		$res=$this->getData($param);
+		return $res;
+	}
+
+	/**
+	 * [getTimeTrendRpt 网站概况]
+	 * @return [type] [description]
+	 */
+	public function getTimeTrendRpt($start_date,$end_date){
+		$param=[
+			'method'		=>	'overview/getTimeTrendRpt',
+			'metrics'		=>	'pv_count,visitor_count',
+			'start_date'	=>	$start_date,
+			'end_date'		=>	$end_date, 
+		];
+		return $this->getData($param);
+	}
+
+	/**
+	* 获取访问地域分布数据
+	*/
+	public function getDistrictRpt($start_date,$end_date)
+	{
+		$param=[
+			'method'=>'overview/getDistrictRpt',
+			'metrics'=>'pv_count,visitor_count',
+			'start_date'=>	$start_date,
+			'end_date'	=>	$end_date, 
+		];
+		return $this->getData($param);
+	}
+
+	/**
+	* 获取访问搜索词、来源网站、入口界面、受访页面、新老访客数据
+	*/
+	public function getCommonTrackRpt($start_date,$end_date)
+	{
+		$param=[
+			'method'=>'overview/getCommonTrackRpt',
+			'metrics'=>'pv_count',
+			'start_date'=>	$start_date,
+			'end_date'	=>	$end_date, 
+		];
+		return $this->getData($param);
+	}
+
+	/**
+	 * 实时访客
+	 * @param  [int] $page        [页码]
+	 * @param  [int] $max_results [每页数量]
+	 * @return [array] [description]
+	 */
+	public function trendLatestA($page,$max_results){
+		$start_index=($page-1)*$max_results;
+		$param=[
+			'method'		=>	'trend/latest/a',
+			'metrics'		=>	'start_time,area,source,access_page,keyword,searchword,ip,visit_time,visit_pages',
+			'max_results'	=> 	$max_results,
+			'start_index'	=>	$start_index
+		];
+		return $this->getData($param);
+	}
+
+	/**
+	 * [sourceAll 全部来源]
+	 * @param  [int] $page        [页码]
+	 * @param  [int] $max_results [每页数量]
+	 * @param  [date] $start_date  [开始时间]
+	 * @param  [date] $end_date    [结束时间]
+	 * @return [array]              [description]
+	 */
+	public function sourceAll($page,$max_results,$start_date,$end_date){
+		$start_index=($page-1)*$max_results;
+		$param=[
+			'method'		=>	'source/all/a',
+			'metrics'		=>	'pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time',
+			'viewType'		=>	'site',
+			'max_results'	=> 	$max_results,
+			'start_index'	=>	$start_index,
+			'start_date'	=>	$start_date,
+			'end_date'		=>	$end_date, 
+		];
+		return $this->getData($param);
+	}
+
+	/**
+	 * [sourceSearchword 搜索词]
+	 * @param  [int] $page        [页码]
+	 * @param  [int] $max_results [每页数量]
+	 * @param  [date] $start_date  [开始时间]
+	 * @param  [date] $end_date    [结束时间]
+	 * @return [array]              [description]
+	 */
+	public function sourceSearchword($page,$max_results,$start_date,$end_date){
+		$start_index=($page-1)*$max_results;
+		$param=[
+			'method'		=>	'source/searchword/a',
+			'metrics'		=>	'pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time',
+			'viewType'		=>	'site',
+			'max_results'	=> 	$max_results,
+			'start_index'	=>	$start_index,
+			'start_date'	=>	$start_date,
+			'end_date'		=>	$end_date, 
+		];
+		return $this->getData($param);
+	}
+
+	/**
+	 * [visitToppage 受访页面]
+	 * @param  [int] $page        [页码]
+	 * @param  [int] $max_results [每页数量]
+	 * @param  [date] $start_date  [开始时间]
+	 * @param  [date] $end_date    [结束时间]
+	 * @return [array]              [description]
+	 */
+	public function visitToppage($page,$max_results,$start_date,$end_date){
+		$start_index=($page-1)*$max_results;
+		$param=[
+			'method'		=>	'visit/toppage/a',
+			'metrics'		=>	'pv_count,visitor_count,ip_count,visit1_count,outward_count,exit_count,exit_ratio,average_stay_time',
+			'max_results'	=> 	$max_results,
+			'start_index'	=>	$start_index,
+			'start_date'	=>	$start_date,
+			'end_date'		=>	$end_date, 
+		];
+		return $this->getData($param);
+	}
+
+	/**
+	 * [visitDistrict 地域分布(按省)]
+	 * @param  [int] $page        [页码]
+	 * @param  [int] $max_results [每页数量]
+	 * @param  [date] $start_date  [开始时间]
+	 * @param  [date] $end_date    [结束时间]
+	 * @return [array]              [description]
+	 */
+	public function visitDistrict($param){
+		$param['method']='visit/district/a';
+		return $this->getData($param);
+	}
+
+	/**
+	 * [visitDistrictTop 地域分布(按城市)]
+	 * @param  [type] $start_date [开始时间]
+	 * @param  [type] $end_date   [结束时间]
+	 * @param  [type] $area       [省份]
+	 * @return [type]             [description]
+	 */
+	public function visitDistrictTop($start_date,$end_date,$area){
+		$param=[
+			'method'		=>	'visit/district/top',
+			'metrics'		=>	'pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time',
+			'start_date'	=>	$start_date,
+			'end_date'		=>	$end_date,
+			'viewType'		=>	'city',
+			'area'			=>	$area
+		];
+		return $this->getData($param);
+	}
+
+	/**
+	 * [visitWorld 地域分布(按国家)]
+	 * @param  [int] $page        [页码]
+	 * @param  [int] $max_results [每页数量]
+	 * @param  [date] $start_date  [开始时间]
+	 * @param  [date] $end_date    [结束时间]
+	 * @return [array]              [description]
+	 */
+	public function visitWorld($start_date,$end_date){
+		$param=[
+			'method'		=>	'visit/world/a',
+			'metrics'		=>	'pv_count,visitor_count,ip_count,bounce_ratio,avg_visit_time',
+			'start_date'	=>	$start_date,
+			'end_date'		=>	$end_date, 
+		];
+		return $this->getData($param);
+	}
+	public function rankGetData($start_date,$end_date){
+		$param=[
+			'method'		=>	'opt/searchwordrank/getData',
+			'metrics'		=>	'',
+			'start_date'	=>	$start_date,
+			'end_date'		=>	$end_date, 
+			'viewType'		=>	'searchword_rank'
+		];
+		return $this->getData($param);
+	}
+	public function rankGetWords(){
+		$param=[
+			'method'		=>	'opt/searchwordrank/getWords',
+		];
+		return $this->getData($param);
+	}
+	public function rankSetWords($words){
+		$param=[
+			'method'	=>	'opt/searchwordrank/setWords',
+			'words'		=>	$words
+		];
+		return $this->getData($param);
+	}
+	public function reportSend($param){
+		$param['method']	=	'home/reportsend/add';
+		return $this->getData($param);
+	}
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
addons/cms/.addonrc


+ 1 - 0
addons/cms/.gitignore

@@ -0,0 +1 @@
+uniapp/unpackage

+ 216 - 0
addons/cms/Cms.php

@@ -0,0 +1,216 @@
+<?php
+
+namespace addons\cms;
+
+use addons\cms\library\FulltextSearch;
+use addons\cms\library\Service;
+use addons\cms\model\Archives;
+use addons\cms\model\Modelx;
+use app\common\library\Auth;
+use app\common\library\Menu;
+use think\Addons;
+use think\Config;
+use think\Db;
+use think\Loader;
+use think\Request;
+
+/**
+ * CMS插件
+ */
+class Cms extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        $menu = include ADDON_PATH . 'cms' . DS . 'data' . DS . 'menu.php';
+        Menu::create($menu);
+
+        //首次安装测试数据
+        if (version_compare(config('fastadmin.version'), '1.3.0', '<') && Db::name("cms_model")->count() == 0) {
+            $this->importTestData();
+        }
+        return true;
+    }
+
+    /**
+     * 导入测试数据
+     */
+    protected function importTestData()
+    {
+        $sqlFile = ADDON_PATH . 'cms' . DS . 'testdata.sql';
+        if (is_file($sqlFile)) {
+            $lines = file($sqlFile);
+            $templine = '';
+            foreach ($lines as $line) {
+                if (substr($line, 0, 2) == '--' || $line == '' || substr($line, 0, 2) == '/*') {
+                    continue;
+                }
+
+                $templine .= $line;
+                if (substr(trim($line), -1, 1) == ';') {
+                    $templine = str_ireplace('__PREFIX__', config('database.prefix'), $templine);
+                    $templine = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $templine);
+                    try {
+                        Db::getPdo()->exec($templine);
+                    } catch (\Exception $e) {
+                        //$e->getMessage();
+                    }
+                    $templine = '';
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        Menu::delete('cms');
+        return true;
+    }
+
+    /**
+     * 插件启用方法
+     */
+    public function enable()
+    {
+        Menu::enable('cms');
+
+        $prefix = Config::get('database.prefix');
+        // 1.4.0表字段升级
+        $modelList = Modelx::whereRaw("FIND_IN_SET('price', `fields`)")->select();
+        foreach ($modelList as $index => $item) {
+            Db::startTrans();
+            try {
+                //更新表数据
+                Db::execute("UPDATE {$prefix}cms_archives a,{$prefix}{$item['table']} e SET a.price = e.price WHERE a.id = e.id");
+                //更新表结构
+                $field = \app\admin\model\cms\Fields::where('source', 'model')->where('name', 'price')->where('source_id', $item['id'])->find();
+                if ($field) {
+                    $field->delete();
+                }
+                Db::commit();
+            } catch (\Exception $e) {
+                Db::rollback();
+            }
+        }
+    }
+
+    /**
+     * 插件禁用方法
+     */
+    public function disable()
+    {
+        Menu::disable('cms');
+    }
+
+    /**
+     * 插件升级方法
+     */
+    public function upgrade()
+    {
+        $menu = include ADDON_PATH . 'cms' . DS . 'data' . DS . 'menu.php';
+        Menu::upgrade('cms', $menu);
+    }
+
+    /**
+     * 应用初始化
+     */
+    public function appInit()
+    {
+        $config = get_addon_config('cms');
+        $hashids_key_length = $config['hashids_key_length'] ?? 10;
+        // 自定义路由变量规则
+        \think\Route::pattern([
+            'diyname' => "/[a-zA-Z0-9\-_\x{4e00}-\x{9fa5}]+/u",
+            'id'      => "\d+",
+            'eid'     => "\w{{$hashids_key_length}}",
+        ]);
+
+        //添加命名空间
+        if (!class_exists('\Hashids\Hashids')) {
+            Loader::addNamespace('Hashids', ADDON_PATH . 'cms' . DS . 'library' . DS . 'hashids' . DS);
+        }
+        $taglib = Config::get('template.taglib_pre_load');
+        Config::set('template.taglib_pre_load', ($taglib ? $taglib . ',' : '') . 'addons\\cms\\taglib\\Cms');
+        Config::set('cms', $config);
+    }
+
+    /**
+     * 脚本替换
+     */
+    public function viewFilter(& $content)
+    {
+        $request = \think\Request::instance();
+        $dispatch = $request->dispatch();
+        if (!$dispatch) {
+            return;
+        }
+
+        if ($request->module() || !isset($dispatch['method'][0]) || $dispatch['method'][0] != '\think\addons\Route') {
+            return;
+        }
+        $addon = isset($dispatch['var']['addon']) ? $dispatch['var']['addon'] : $request->param('addon');
+        if ($addon != 'cms') {
+            return;
+        }
+        $style = '';
+        $script = '';
+        $result = preg_replace_callback("/<(script|style)\s(data\-render=\"(script|style)\")([\s\S]*?)>([\s\S]*?)<\/(script|style)>/i", function ($match) use (&$style, &$script) {
+            if (isset($match[1]) && in_array($match[1], ['style', 'script'])) {
+                ${$match[1]} .= str_replace($match[2], '', $match[0]);
+            }
+            return '';
+        }, $content);
+        $content = preg_replace_callback('/^\s+(\{__STYLE__\}|\{__SCRIPT__\})\s+$/m', function ($matches) use ($style, $script) {
+            return $matches[1] == '{__STYLE__}' ? $style : $script;
+        }, $result ? $result : $content);
+    }
+
+    /**
+     * 会员中心边栏后
+     * @return mixed
+     * @throws \Exception
+     */
+    public function userSidenavAfter()
+    {
+        $request = Request::instance();
+        $controllername = strtolower($request->controller());
+        $actionname = strtolower($request->action());
+        $config = get_addon_config('cms');
+        $sidenav = array_filter(explode(',', $config['usersidenav']));
+        if (!$sidenav) {
+            return '';
+        }
+        $user = Auth::instance()->getUser();
+        $data = [
+            'user'           => $user,
+            'controllername' => $controllername,
+            'actionname'     => $actionname,
+            'sidenav'        => $sidenav
+        ];
+
+        return $this->fetch('view/hook/user_sidenav_after', $data);
+    }
+
+    public function xunsearchConfigInit()
+    {
+        return FulltextSearch::config();
+    }
+
+    public function xunsearchIndexReset($project)
+    {
+        if (!$project['isaddon'] || $project['name'] != 'cms') {
+            return;
+        }
+        return FulltextSearch::reset();
+    }
+
+}

+ 11 - 0
addons/cms/README.md

@@ -0,0 +1,11 @@
+### 插件协议
+在使用FastAdmin付费插件前认真阅读FastAdmin插件使用协议
+https://www.fastadmin.net/page/addon-agreement.html 
+
+### 交流专区
+如果你在使用CMS内容管理系统有遇到什么问题,请到FastAdmin交流专区进行交流
+https://ask.fastadmin.net/zone/cms.html 
+
+### 温馨提示
+此插件为FastAdmin的商业产品,禁止分享、转售、复制CMS插件源码给他人使用,违者将追究法律责任
+

+ 22 - 0
addons/cms/bootstrap.js

@@ -0,0 +1,22 @@
+require.config({
+    paths: {
+        'jquery-colorpicker': '../addons/cms/js/jquery.colorpicker.min',
+        'jquery-autocomplete': '../addons/cms/js/jquery.autocomplete',
+        'jquery-tagsinput': '../addons/cms/js/jquery.tagsinput',
+        'clipboard': '../addons/cms/js/clipboard.min',
+    },
+    shim: {
+        'jquery-colorpicker': {
+            deps: ['jquery'],
+            exports: '$.fn.extend'
+        },
+        'jquery-autocomplete': {
+            deps: ['jquery'],
+            exports: '$.fn.extend'
+        },
+        'jquery-tagsinput': {
+            deps: ['jquery', 'jquery-autocomplete', 'css!../addons/cms/css/jquery.tagsinput.min.css'],
+            exports: '$.fn.extend'
+        }
+    }
+});

+ 255 - 0
addons/cms/config.html

@@ -0,0 +1,255 @@
+<style>
+    #config-form div a.btn {
+        color: #fff;
+        text-decoration: none;
+    }
+    #config-form .alert-info-light .dropdown-menu li a {
+        text-decoration: none;
+    }
+</style>
+<!--@formatter:off-->
+{php}
+$groupList = [
+    'config'=>'system_user_id,sitename,sitelogo,title,keywords,description,indexpagesize,theme,qrcode,wxapp,donateimage,userpage,openedsite,searchtype,autopinyin,baidupush,usersidenav,loadmode,pagemode,indexloadmode,indexpagemode,cachelifetime,urlsuffix',
+    'thumb'=>'default_archives_img,default_channel_img,default_block_img,default_page_img,default_special_img',
+    'wxapp'=>'wxappid,wxappsecret',
+    'rewrite'=>'domain,rewrite,urlsuffix,moduleurlsuffix',
+    'audit'=>'isarchivesaudit,iscommentaudit,audittype,nlptype,aip_appid,aip_apikey,aip_secretkey',
+    'dict'=>'downloadtype,spiders,flagtype,autolinks',
+    'other'=>'archivesratio,score,limitscore,ispaylogin,paytypelist,apikey,archiveseditmode,auditnotice,noticetemplateid,channelallocate,archivesdatalimit,specialdatalimit,pagedatalimit,diyformdatalimit',
+];
+$group = [];
+foreach($groupList as $k=>$v){
+   $item = explode(',', $v);
+   $item = array_flip($item);
+   $group = array_merge($group, array_map(function($value) use($k){return $k;}, $item));
+}
+{/php}
+<form id="config-form" class="edit-form form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+    <div class="alert {$addon.tips.extend|default='alert-info-light'}" style="margin-bottom:10px;">
+        <div style="margin-top:10px;">
+            <a href="{:addon_url('cms/index/index')}" class="btn btn-warning" target="_blank"><i class="fa fa-home"></i> CMS首页</a>
+            <a href="__PUBLIC__index/user/index" class="btn btn-info" target="_blank"><i class="fa fa-user"></i> 会员中心</a>
+            <div class="btn-group">
+                <a class="btn btn-primary" href="{:addon_url('cms/sitemap/index', [], false, false)}/type/all.xml" target="_blank"><i class="fa fa-sitemap fa-fw"></i> Sitemap</a>
+                <a class="btn btn-primary dropdown-toggle" data-toggle="dropdown" href="javascript:;" aria-expanded="true">
+                    <span class="fa fa-caret-down"></span></a>
+                <ul class="dropdown-menu">
+                    <li role="presentation"><a href="{:addon_url('cms/sitemap/index', [], false, false)}/type/all.xml" target="_blank">全部</a></li>
+                    <li role="presentation" class="divider"></li>
+                    <li><a href="{:addon_url('cms/sitemap/index', [], false, false)}/type/archives.xml" target="_blank">文档</a></li>
+                    <li><a href="{:addon_url('cms/sitemap/index', [], false, false)}/type/tags.xml" target="_blank">标签</a></li>
+                    <li><a href="{:addon_url('cms/sitemap/index', [], false, false)}/type/users.xml" target="_blank">会员</a></li>
+                    <li><a href="{:addon_url('cms/sitemap/index', [], false, false)}/type/specials.xml" target="_blank">专题</a></li>
+                    <li><a href="{:addon_url('cms/sitemap/index', [], false, false)}/type/pages.xml" target="_blank">单页</a></li>
+                    <li><a href="{:addon_url('cms/sitemap/index', [], false, false)}/type/diyforms.xml" target="_blank">自定义表单</a></li>
+                </ul>
+            </div>
+        </div>
+    </div>
+
+    <div class="panel panel-default panel-intro">
+        <div class="panel-heading">
+            <ul class="nav nav-tabs nav-group">
+                <li class="active"><a href="#all" data-toggle="tab">全部</a></li>
+                <li><a href="#config" data-toggle="tab">基础</a></li>
+                <li><a href="#thumb" data-toggle="tab">缩略图</a></li>
+                <li><a href="#wxapp" data-toggle="tab">微信小程序</a></li>
+                <li><a href="#rewrite" data-toggle="tab">伪静态</a></li>
+                <li><a href="#audit" data-toggle="tab">审核</a></li>
+                <li><a href="#dict" data-toggle="tab">字典</a></li>
+                <li><a href="#other" data-toggle="tab">其它</a></li>
+                <li class="pull-right"><a href="{:url('cms/ajax/config?name=signin')}" title="签到配置" class="dialogit">签到</a></li>
+                <li class="pull-right"><a href="{:url('cms/ajax/config?name=sms')}" title="短信配置" class="dialogit">短信</a></li>
+                <li class="pull-right"><a href="{:url('cms/ajax/config?name=third')}" title="登录配置" class="dialogit">登录</a></li>
+                <li class="pull-right"><a href="{:url('cms/ajax/config?name=oss')}" title="云存储配置" class="dialogit">云存储</a></li>
+                <li class="pull-right"><a href="{:url('cms/ajax/config?name=epay')}" title="支付配置" class="dialogit">支付</a></li>
+            </ul>
+        </div>
+
+        <div class="panel-body">
+            <div id="myTabContent" class="tab-content">
+                <div class="tab-pane fade active in" id="one">
+
+                    <table class="table table-striped table-config">
+                        <tbody>
+                        {foreach $addon.config as $item}
+                        <tr data-type="{:isset($group[$item['name']])?$group[$item['name']]:'other'}">
+                            <td width="15%">
+                                {$item.title}
+                                {if $item.type=='array' && $item.tip}
+                                <a href="javascript:" class="text-info" data-toggle="popover" data-content="{$item.tip}" data-trigger="click" data-title="配置提示" data-html="true"><i class="fa fa-info-circle"></i></a>
+                                {/if}
+                            </td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        {switch $item.type}
+                                        {case string}
+                                        <input {$item.extend} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control" data-rule="{$item.rule}" data-tip="{$item.tip}"/>
+                                        {/case}
+                                        {case password}
+                                        <input {$item.extend} type="password" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control" data-rule="{$item.rule}" data-tip="{$item.tip}"/>
+                                        {/case}
+                                        {case text}
+                                        <textarea {$item.extend} name="row[{$item.name}]" class="form-control" data-rule="{$item.rule}" rows="5" data-tip="{$item.tip}">{$item.value|htmlentities}</textarea>
+                                        {/case}
+                                        {case array}
+                                        <dl class="fieldlist" data-name="row[{$item.name}]">
+                                            <dd>
+                                                <ins>{:__('Array key')}</ins>
+                                                <ins>{:__('Array value')}</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="row[{$item.name}]" cols="30" rows="5" class="hide">{$item.value|json_encode|htmlentities}</textarea>
+                                        </dl>
+                                        {/case}
+                                        {case date}
+                                        <input {$item.extend} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="YYYY-MM-DD" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                        {/case}
+                                        {case time}
+                                        <input {$item.extend} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="HH:mm:ss" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                        {/case}
+                                        {case datetime}
+                                        <input {$item.extend} type="text" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control datetimepicker" data-date-format="YYYY-MM-DD HH:mm:ss" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                        {/case}
+                                        {case number}
+                                        <input {$item.extend} type="number" name="row[{$item.name}]" value="{$item.value|htmlentities}" class="form-control" data-tip="{$item.tip}" data-rule="{$item.rule}"/>
+                                        {/case}
+                                        {case checkbox}
+                                        {foreach name="item.content" item="vo"}
+                                        <label for="row[{$item.name}][]-{$key}"><input id="row[{$item.name}][]-{$key}" name="row[{$item.name}][]" type="checkbox" value="{$key}" data-tip="{$item.tip}" {in name="key" value="$item.value" }checked{/in} /> {$vo}</label>
+                                        {/foreach}
+                                        <span class="msg-box n-right" for="c-{$item.name}"></span>
+                                        {/case}
+                                        {case radio}
+                                        {foreach name="item.content" item="vo"}
+                                        <label for="row[{$item.name}]-{$key}"><input id="row[{$item.name}]-{$key}" name="row[{$item.name}]" type="radio" value="{$key}" data-tip="{$item.tip}" {in name="key" value="$item.value" }checked{/in} /> {$vo}</label>
+                                        {/foreach}
+                                        <span class="msg-box n-right" for="c-{$item.name}"></span>
+                                        {/case}
+                                        {case value="select" break="0"}{/case}
+                                        {case value="selects"}
+                                        {if $item.name==='spiderfollow'}
+                                        {php}$item['content'] = get_addon_config('cms')['spiders']??[];{/php}
+                                        {/if}
+                                        <select {$item.extend} name="row[{$item.name}]{$item.type=='selects'?'[]':''}" class="form-control selectpicker" data-tip="{$item.tip}" {$item.type=='selects'?'multiple':''}>
+                                            {foreach name="item.content" item="vo"}
+                                            <option value="{$key}" {in name="key" value="$item.value" }selected{/in}>{$vo}</option>
+                                            {/foreach}
+                                        </select>
+                                        {/case}
+                                        {case value="image" break="0"}{/case}
+                                        {case value="images"}
+                                        <div class="form-inline">
+                                            <input id="c-{$item.name}" class="form-control" size="35" name="row[{$item.name}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip}">
+                                            <span><button type="button" id="plupload-{$item.name}" class="btn btn-danger plupload" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}" data-preview-id="p-{$item.name}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                            <span><button type="button" id="fachoose-{$item.name}" class="btn btn-primary fachoose" data-input-id="c-{$item.name}" data-mimetype="image/*" data-multiple="{$item.type=='image'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                            <span class="msg-box n-right" for="c-{$item.name}"></span>
+                                            <ul class="row list-inline plupload-preview" id="p-{$item.name}"></ul>
+                                        </div>
+                                        {/case}
+                                        {case value="file" break="0"}{/case}
+                                        {case value="files"}
+                                        <div class="form-inline">
+                                            <input id="c-{$item.name}" class="form-control" size="35" name="row[{$item.name}]" type="text" value="{$item.value|htmlentities}" data-tip="{$item.tip}">
+                                            <span><button type="button" id="plupload-{$item.name}" class="btn btn-danger plupload" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                            <span><button type="button" id="fachoose-{$item.name}" class="btn btn-primary fachoose" data-input-id="c-{$item.name}" data-multiple="{$item.type=='file'?'false':'true'}"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                        </div>
+                                        {/case}
+                                        {case bool}
+                                        <label for="row[{$item.name}]-yes"><input id="row[{$item.name}]-yes" name="row[{$item.name}]" type="radio" value="1" {$item.value?'checked':''} data-tip="{$item.tip}" /> {:__('Yes')}</label>
+                                        <label for="row[{$item.name}]-no"><input id="row[{$item.name}]-no" name="row[{$item.name}]" type="radio" value="0" {$item.value?'':'checked'} data-tip="{$item.tip}" /> {:__('No')}</label>
+                                        {/case}
+                                        {default /}{$item.value}
+                                        {/switch}
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+
+                            </td>
+                        </tr>
+                        {/foreach}
+                        </tbody>
+                    </table>
+                    <div class="form-group layer-footer">
+                        <label class="control-label col-xs-12 col-sm-2" style="width:15%;"></label>
+                        <div class="col-xs-12 col-sm-8">
+                            <button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
+                            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</form>
+<!--@formatter:on-->
+<script>
+    require.callback = function () {
+        var tabevent = function () {
+            $(document).on("click", ".nav-group li a[data-toggle='tab']", function () {
+                var type = $(this).attr("href").substring(1);
+                if (type == 'all') {
+                    $(".table-config tr").show();
+                } else {
+                    $(".table-config tr").hide();
+                    $(".table-config tr[data-type='" + type + "']").show();
+                }
+            });
+        };
+
+        var customrule = function () {
+            $.validator.config({
+                rules: {
+                    config: function (element) {
+                        if (this.key == "row[archivesratio]") {
+                            var valueArr = this.value.split(/:/);
+                            if (isNaN(valueArr[0]) || isNaN(valueArr[1])) {
+                                return '格式不正确';
+                            }
+                            if (parseFloat(valueArr[0]) + parseFloat(valueArr[1]) != 1) {
+                                return '分成占比相加必须等于1';
+                            }
+                        } else if (this.key == "row[cachelifetime]") {
+                            if (isNaN(this.value) && ['true', 'false'].indexOf(this.value) < 0) {
+                                return "格式不正确,只支持 数字/true";
+                            }
+                        } else if (this.key == "row[theme]") {
+                            if (!/^([a-zA-Z0-9\-_]+)$/.test(this.value)) {
+                                return "只支持字母数字下划线";
+                            }
+                        }
+                        return true;
+                        return $.ajax({
+                            url: 'cms/ajax/check_config_available',
+                            type: 'POST',
+                            data: {name: element.name, value: element.value},
+                            dataType: 'json'
+                        });
+                    },
+                }
+            });
+        };
+        define('backend/addon', ['jquery', 'form'], function ($, Form) {
+            var Controller = {
+                config: function () {
+                    customrule();
+                    Form.api.bindevent($("form[role=form]"));
+                    tabevent();
+                }
+            };
+            return Controller;
+        });
+        define('backend/cms/config', ['jquery', 'form'], function ($, Form) {
+            var Controller = {
+                index: function () {
+                    customrule();
+                    Form.api.bindevent($("form[role=form]"));
+                    tabevent();
+                }
+            };
+            return Controller;
+        });
+    }
+</script>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1063 - 0
addons/cms/config.php


+ 222 - 0
addons/cms/controller/Ajax.php

@@ -0,0 +1,222 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\model\Collection;
+use addons\cms\model\Fields;
+use fast\Tree;
+use think\Config;
+use think\Db;
+
+/**
+ * Ajax控制器
+ * Class Ajax
+ * @package addons\cms\controller
+ */
+class Ajax extends Base
+{
+    protected $noNeedLogin = ["share", "selectpage"];
+
+    public function selectpage()
+    {
+        $id = $this->request->get("id/d", 0);
+        $fieldInfo = Fields::get($id);
+        if (!$fieldInfo) {
+            $this->error("未找到指定字段");
+        }
+        $setting = $fieldInfo['setting'];
+        if (!$setting || !isset($setting['table'])) {
+            $this->error("字段配置不正确");
+        }
+        //搜索关键词,客户端输入以空格分开,这里接收为数组
+        $word = (array)$this->request->request("q_word/a");
+        //当前页
+        $page = $this->request->request("pageNumber");
+        //分页大小
+        $pagesize = $this->request->request("pageSize");
+        //搜索条件
+        $andor = $this->request->request("andOr", "and", "strtoupper");
+        //排序方式
+        $orderby = (array)$this->request->request("orderBy/a");
+        //显示的字段
+        //$field = $this->request->request("showField");
+        $field = $setting['field'];
+        //主键
+        //$primarykey = $this->request->request("keyField");
+        $primarykey = $setting['primarykey'];
+        //主键值
+        $primaryvalue = $this->request->request("keyValue");
+        //搜索字段
+        $searchfield = (array)$this->request->request("searchField/a");
+        $searchfield = [$field, $primarykey];
+        //自定义搜索条件
+        $custom = (array)$this->request->request("custom/a");
+        $custom = isset($setting['conditions']) ? (array)json_decode($setting['conditions'], true) : [];
+        //$custom = array_filter($custom);
+
+        $admin_id = session('admin.id') ?: 0;
+        $user_id = $this->auth->id ?: 0;
+        //如果是管理员需要移除user_id筛选,否则会导致管理员无法筛选列表信息
+        $admin = $this->request->request("admin/d");
+        if ($admin_id && $admin) {
+            unset($custom['user_id']);
+        } else {
+            //如果不是管理员则需要判断是否开放相应的投稿字段
+            if (!in_array($fieldInfo['source'], ['model', 'diyform'])) {
+                $this->error("未开放栏目信息");
+            }
+            if (!$fieldInfo['iscontribute']) {
+                $this->error("未开放字段信息");
+            }
+        }
+
+        //是否返回树形结构
+        $istree = $this->request->request("isTree", 0);
+        $ishtml = $this->request->request("isHtml", 0);
+        if ($istree) {
+            $word = [];
+            $pagesize = 99999;
+        }
+        $order = [];
+        foreach ($orderby as $k => $v) {
+            $order[$v[0]] = $v[1];
+        }
+        $field = $field ? $field : 'name';
+
+        //如果有primaryvalue,说明当前是初始化传值
+        if ($primaryvalue !== null) {
+            $where = [$primarykey => ['in', $primaryvalue]];
+            $where = function ($query) use ($primaryvalue, $custom, $admin_id, $user_id) {
+                $query->where('id', 'in', $primaryvalue);
+                if ($custom && is_array($custom)) {
+                    //替换暂位符
+                    $search = ["{admin_id}", "{user_id}"];
+                    $replace = [$admin_id, $user_id];
+                    foreach ($custom as $k => $v) {
+                        if (is_array($v) && 2 == count($v)) {
+                            $query->where($k, trim($v[0]), str_replace($search, $replace, $v[1]));
+                        } else {
+                            $query->where($k, '=', str_replace($search, $replace, $v));
+                        }
+                    }
+                }
+            };
+            $pagesize = 99999;
+        } else {
+            $where = function ($query) use ($word, $andor, $field, $searchfield, $custom, $admin_id, $user_id) {
+                $logic = $andor == 'AND' ? '&' : '|';
+                $searchfield = is_array($searchfield) ? implode($logic, $searchfield) : $searchfield;
+                $word = array_filter($word);
+                if ($word) {
+                    foreach ($word as $k => $v) {
+                        $query->where(str_replace(',', $logic, $searchfield), "like", "%{$v}%");
+                    }
+                }
+                if ($custom && is_array($custom)) {
+                    //替换暂位符
+                    $search = ["{admin_id}", "{user_id}"];
+                    $replace = [$admin_id, $user_id];
+                    foreach ($custom as $k => $v) {
+                        if (is_array($v) && 2 == count($v)) {
+                            $query->where($k, trim($v[0]), str_replace($search, $replace, $v[1]));
+                        } else {
+                            $query->where($k, '=', str_replace($search, $replace, $v));
+                        }
+                    }
+                }
+            };
+        }
+        $list = [];
+        $total = Db::table($setting['table'])->where($where)->count();
+        if ($total > 0) {
+            $datalist = Db::table($setting['table'])->where($where)
+                ->order($order)
+                ->page($page, $pagesize)
+                ->field($primarykey . "," . $field . ($istree ? ",pid" : ""))
+                ->select();
+            foreach ($datalist as $index => &$item) {
+                unset($item['password'], $item['salt']);
+                $list[] = [
+                    $primarykey => isset($item[$primarykey]) ? $item[$primarykey] : '',
+                    $field      => isset($item[$field]) ? $item[$field] : '',
+                    'pid'       => isset($item['pid']) ? $item['pid'] : 0
+                ];
+            }
+            if ($istree && !$primaryvalue) {
+                $tree = Tree::instance();
+                $tree->init($list, 'pid');
+                $list = $tree->getTreeList($tree->getTreeArray(0), $field);
+                if (!$ishtml) {
+                    foreach ($list as &$item) {
+                        $item = str_replace('&nbsp;', ' ', $item);
+                    }
+                    unset($item);
+                }
+            }
+        }
+        //这里一定要返回有list这个字段,total是可选的,如果total<=list的数量,则会隐藏分页按钮
+        return json(['list' => $list, 'total' => $total]);
+    }
+
+    /**
+     * 添加收藏
+     */
+    public function collection()
+    {
+        if (!$this->auth->isLogin()) {
+            $this->error("请登录后操作", "index/user/login");
+        }
+        $type = $this->request->post("type");
+        $aid = $this->request->post("aid/d");
+        if (!in_array($type, ['archives', 'page', 'special', 'diyform'])) {
+            $this->error("参数不正确");
+        }
+        $model = call_user_func_array(['\\addons\\cms\\model\\' . ucfirst($type), "get"], [$aid]);
+        if (!$model) {
+            $this->error("未找到指定数据");
+        }
+        Db::startTrans();
+        try {
+            $collection = Collection::lock(true)->where(['type' => $type, 'aid' => $aid, 'user_id' => $this->auth->id])->find();
+            if ($collection) {
+                throw new \think\Exception("请勿重复收藏");
+            }
+            $title = $model->title;
+            $url = $model->fullurl;
+            $image = $model->image;
+            $data = [
+                'user_id' => $this->auth->id,
+                'type'    => $type,
+                'aid'     => $aid,
+                'title'   => $title,
+                'url'     => $url,
+                'image'   => $image
+            ];
+            Collection::create($data);
+            Db::commit();
+        } catch (\think\Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        } catch (\Exception $e) {
+            Db::rollback();
+            $this->error("收藏失败");
+        }
+        $this->success("收藏成功");
+    }
+
+    /**
+     * 微信公众号内分享
+     */
+    public function share()
+    {
+        $url = $this->request->request("url", "", "trim");
+        $config = get_addon_config('third');
+        if (!$config) {
+            $this->error("请在后台插件管理安装配置《第三方登录》插件");
+        }
+        $js_sdk = new \addons\cms\library\Jssdk();
+        $data = $js_sdk->getSignedPackage($url);
+        $this->success('', '', $data);
+    }
+
+}

+ 224 - 0
addons/cms/controller/Api.php

@@ -0,0 +1,224 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\Service;
+use addons\cms\model\Archives;
+use addons\cms\model\Channel;
+use addons\cms\model\Diydata;
+use addons\cms\model\Fields;
+use addons\cms\model\Modelx;
+use think\Config;
+use think\Db;
+use think\Exception;
+use think\exception\PDOException;
+
+/**
+ * Api接口控制器
+ *
+ * 仅限用于数据迁移或内部接口,不建议用于对接外部API接口
+ * Class Api
+ * @package addons\cms\controller
+ */
+class Api extends Base
+{
+
+    public function _initialize()
+    {
+        Config::set('default_return_type', 'json');
+
+        $apikey = $this->request->post('apikey');
+        $config = get_addon_config('cms');
+        if (!$config['apikey']) {
+            $this->error('请先在后台配置API密钥');
+        }
+        if ($config['apikey'] != $apikey) {
+            $this->error('密钥不正确');
+        }
+
+        return parent::_initialize();
+    }
+
+    /**
+     * 文档数据写入接口
+     */
+    public function index()
+    {
+        $data = $this->request->post();
+        if (isset($data['user']) && $data['user']) {
+            $user = \app\common\model\User::where('nickname', $data['user'])->find();
+            if ($user) {
+                $data['user_id'] = $user->id;
+            }
+        }
+        //如果有传栏目名称
+        if (isset($data['channel']) && $data['channel']) {
+            $channel = Channel::where('name', $data['channel'])->find();
+            if (!$channel || $channel['status'] != 'normal') {
+                $this->error('栏目未找到');
+            }
+            $data['channel_id'] = $channel->id;
+            unset($data['channel']);
+        } else {
+            $channel_id = $this->request->request('channel_id');
+            $channel = Channel::get($channel_id);
+            if (!$channel || $channel['status'] != 'normal') {
+                $this->error('栏目未找到');
+            }
+        }
+        $model = Modelx::get($channel['model_id']);
+        if (!$model) {
+            $this->error('模型未找到');
+        }
+        $data['model_id'] = $model['id'];
+        $data['content'] = $this->request->post("content", "", "trim");
+        $data['publishtime'] = time();
+        $data['weigh'] = 0;
+
+        Db::startTrans();
+        try {
+            //副表数据插入会在模型事件中完成
+            $archives = new \app\admin\model\cms\Archives;
+            $archives->allowField(true)->save($data);
+            Db::commit();
+            $data = [
+                'id'  => $archives->id,
+                'url' => $archives->fullurl
+            ];
+        } catch (PDOException $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        } catch (Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        $this->success('新增成功', '', $data);
+        return;
+    }
+
+    /**
+     * 读取文章数据
+     */
+    public function archives()
+    {
+        $id = $this->request->request("id/d");
+        $archives = Archives::get($id, ['channel']);
+        if (!$archives || $archives['status'] != 'normal' || $archives['deletetime']) {
+            $this->error("文章未找到");
+        }
+        $channel = Channel::get($archives['channel_id']);
+        if (!$channel) {
+            $this->error("栏目未找到");
+        }
+        $model = Modelx::get($channel['model_id'], [], true);
+        if (!$model) {
+            $this->error("文章模型未找到");
+        }
+        $addon = db($model['table'])->where('id', $archives['id'])->find();
+        if ($addon) {
+            if ($model->fields) {
+                Service::appendTextAndList('model', $model->id, $addon);
+            }
+            $archives->setData($addon);
+        } else {
+            $this->error('文章副表数据未找到');
+        }
+        $content = $archives->content;
+
+        //移除分页数据
+        $content = str_replace("##pagebreak##", "<br>", $content);
+        $archives->content = $content;
+
+        $this->success(__('读取成功'), '', $archives->toArray());
+    }
+
+    /**
+     * 读取文章列表
+     */
+    public function arclist()
+    {
+        $params = [];
+        $model = (int)$this->request->request('model');
+        $channel = (int)$this->request->request('channel');
+        $page = (int)$this->request->request('page');
+        $pagesize = (int)$this->request->request('pagesize');
+        $pagesize = $pagesize ? $pagesize : 10;
+
+        if ($model) {
+            $params['model'] = $model;
+        }
+        if ($channel) {
+            $channelInfo = Channel::get($channel);
+            if (!$channelInfo || $channelInfo['status'] != 'normal') {
+                $channel = -1;
+            }
+            $params['channel'] = $channel;
+        }
+        $page = max(1, $page);
+        $params['limit'] = ($page - 1) * $pagesize . ',' . $pagesize;
+
+        $list = Archives::getArchivesList($params);
+        $list = collection($list)->toArray();
+        foreach ($list as $index => &$item) {
+            $item['url'] = $item['fullurl'];
+            unset($item['imglink'], $item['textlink'], $item['channellink'], $item['taglist'], $item['weigh'], $item['status'], $item['deletetime'], $item['memo'], $item['img']);
+        }
+        $this->success('读取成功', '', $list);
+    }
+
+    /**
+     * 获取栏目列表
+     */
+    public function channel()
+    {
+        $channelList = Channel::where('status', 'normal')
+            ->where('type', 'list')
+            ->order('weigh DESC,id DESC')
+            ->column('id,name');
+        $this->success(__('读取成功'), '', $channelList);
+    }
+
+    /**
+     * 评论数据写入接口
+     */
+    public function comment()
+    {
+        try {
+            $params = $this->request->post();
+            \addons\cms\model\Comment::postComment($params);
+        } catch (Exception $e) {
+            $this->error($e->getMessage());
+        }
+        $this->success(__('评论成功'), '');
+    }
+
+    /**
+     * 自定义表单数据写入接口
+     */
+    public function diyform()
+    {
+        $id = $this->request->request("diyform_id/d");
+        $diyform = \addons\cms\model\Diyform::get($id);
+        if (!$diyform || $diyform['status'] != 'normal') {
+            $this->error("自定义表单未找到");
+        }
+
+        //是否需要登录判断
+        if ($diyform['needlogin'] && !$this->auth->isLogin()) {
+            $this->error("请登录后再操作", "index/user/login");
+        }
+
+        $diydata = new Diydata([], $diyform);
+        if (!$diydata) {
+            $this->error("自定义表未找到");
+        }
+
+        $data = $this->request->request();
+        try {
+            $diydata->allowField(true)->save($data);
+        } catch (Exception $e) {
+            $this->error("数据提交失败");
+        }
+        $this->success("数据提交成功", $diyform['redirecturl'] ? $diyform['redirecturl'] : addon_url('cms/index/index'));
+    }
+}

+ 146 - 0
addons/cms/controller/Archives.php

@@ -0,0 +1,146 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\IntCode;
+use addons\cms\library\Service;
+use addons\cms\model\Archives as ArchivesModel;
+use addons\cms\model\Channel;
+use addons\cms\model\Fields;
+use addons\cms\model\Modelx;
+use addons\cms\model\SpiderLog;
+use think\Config;
+use think\Exception;
+
+/**
+ * 文档控制器
+ * Class Archives
+ * @package addons\cms\controller
+ */
+class Archives extends Base
+{
+    public function index()
+    {
+        $config = get_addon_config('cms');
+        $action = $this->request->post("action");
+        if ($action && $this->request->isPost()) {
+            return $this->$action();
+        }
+        $diyname = $this->request->param('diyname');
+        $eid = $this->request->param('eid');
+        if ($eid) {
+            $diyname = IntCode::decode($eid);
+        }
+        if ($diyname && !is_numeric($diyname)) {
+            $archives = ArchivesModel::with('channel')->where('diyname', $diyname)->find();
+        } else {
+            $id = $diyname ? $diyname : $this->request->param('id', '');
+            $archives = ArchivesModel::get($id, ['channel']);
+        }
+        if (!$archives || ($archives['status'] != 'normal' && (!$archives['user_id'] || $archives['user_id'] != $this->auth->id)) || $archives['deletetime']) {
+            $this->error(__('No specified article found'));
+        }
+        if (!$this->auth->id && !$archives['isguest']) {
+            $this->error(__('Please login first'), 'index/user/login');
+        }
+        $channel = $archives->channel;
+        if (!$channel) {
+            $channel = (new Channel())->get(40);
+//            $this->error(__('No specified channel found'));
+        }
+        $model = Modelx::get($channel['model_id'], [], true);
+        if (!$model) {
+            $this->error(__('No specified model found'));
+        }
+        $addon = db($model['table'])->where('id', $archives['id'])->find();
+        if ($addon) {
+            if ($model->fields) {
+                Service::appendTextAndList('model', $model->id, $addon);
+            }
+            $archives->setData($addon);
+        } else {
+            $this->error(__('No specified addon article found'));
+        }
+
+        SpiderLog::record('archives', $archives['id']);
+
+        Service::appendTextAndList('channel', 0, $channel);
+
+        //PC支持内容分页
+        $page = (int)$this->request->request("page", 1);
+        $page = max(1, $page);
+        $contentArr = array_values(array_filter(explode("##pagebreak##", $archives->content)));
+        $content = $contentArr ? (isset($contentArr[$page - 1]) ? $contentArr[$page - 1] : $contentArr[0]) : '';
+
+        $archives->content = $content . $archives->getPagerHTML($page, count($contentArr));
+
+        $archives->setInc("views", 1);
+        $this->view->assign("__ARCHIVES__", $archives);
+        $this->view->assign("__CHANNEL__", $channel);
+        $this->view->assign("__MODEL__", $model);
+
+        //统计作者文章数和评论数
+        if ($archives->user) {
+            $archives->user->archives = ArchivesModel::where('user_id', $archives->user_id)->where('status', 'normal')->cache(3600)->count();
+            $archives->user->comments = \addons\cms\model\Comment::where('user_id', $archives->user_id)->where('status', 'normal')->cache(3600)->count();
+        }
+
+        //设置TKD
+        Config::set('cms.title', isset($archives['seotitle']) && $archives['seotitle'] ? $archives['seotitle'] : $archives['title']);
+        Config::set('cms.keywords', $archives['keywords']);
+        Config::set('cms.description', $archives['description']);
+        Config::set('cms.image', isset($archives['image']) && $archives['image'] ? cdnurl($archives['image'], true) : '');
+
+        //是否跳转链接
+        if (isset($archives['outlink']) && $archives['outlink']) {
+            $this->redirect($archives['outlink']);
+        }
+        $template = preg_replace('/\.html$/', '', $channel['showtpl']);
+//        var_dump($template);
+        if (!$template) {
+            $this->error('请检查栏目是否配置相应的模板');
+        }
+        return $this->view->fetch('/' . $template);
+    }
+
+    /**
+     * 赞与踩
+     */
+    public function vote()
+    {
+        $id = (int)$this->request->post("id");
+        $type = trim($this->request->post("type", ""));
+        if (!$id || !$type) {
+            $this->error(__('Operation failed'));
+        }
+        $archives = ArchivesModel::get($id);
+        if (!$archives || ($archives['user_id'] != $this->auth->id && $archives['status'] != 'normal') || $archives['deletetime']) {
+            $this->error(__('No specified article found'));
+        }
+        $archives->where('id', $id)->setInc($type === 'like' ? 'likes' : 'dislikes', 1);
+        $archives = ArchivesModel::get($id);
+        $this->success(__('Operation completed'), null, ['likes' => $archives->likes, 'dislikes' => $archives->dislikes, 'likeratio' => $archives->likeratio]);
+    }
+
+    /**
+     * 下载次数
+     */
+    public function download()
+    {
+        $id = (int)$this->request->post("id");
+        if (!$id) {
+            $this->error(__('Operation failed'));
+        }
+        $archives = ArchivesModel::get($id, ['model']);
+        if (!$archives || ($archives['user_id'] != $this->auth->id && $archives['status'] != 'normal') || $archives['deletetime']) {
+            $this->error(__('No specified article found'));
+        }
+        try {
+            $table = $archives->getRelation('model')->getData('table');
+            \think\Db::name($table)->where('id', $id)->setInc('downloads');
+        } catch (Exception $e) {
+            //
+        }
+        $this->success(__('Operation completed'), null);
+    }
+}

+ 78 - 0
addons/cms/controller/Base.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\IntCode;
+use addons\cms\library\Service;
+use addons\cms\model\SpiderLog;
+use app\admin\model\cms\Channel;
+use think\Config;
+use think\Request;
+
+/**
+ * CMS控制器基类
+ */
+class Base extends \think\addons\Controller
+{
+
+    // 初始化
+    public function __construct(Request $request = null)
+    {
+        parent::__construct($request);
+        $config = get_addon_config('cms');
+        // 设定主题模板目录
+        $this->view->engine->config('view_path', $this->view->engine->config('view_path') . $config['theme'] . DS);
+        // 加载自定义标签库
+        //$this->view->engine->config('taglib_pre_load', 'addons\cms\taglib\Cms');
+        // 默认渲染栏目为空
+        $this->view->assign('__CHANNEL__', null);
+        $this->view->assign('isWechat', strpos($this->request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false);
+
+        // 定义CMS首页的URL
+        Config::set('cms.indexurl', addon_url('cms/index/index', [], false));
+        // 定义分页类
+        Config::set('paginate.type', '\\addons\\cms\\library\\Bootstrap');
+        $this->assign("channels", recursion(collection(Channel::where("id", "NOT IN", [22, 44])->order("weigh desc,id desc")->select())->toArray()));
+        $this->assign("baike_channels", recursion(collection(Channel::where("id", "NOT IN", [2, 22, 44])->order("weigh desc,id desc")->select())->toArray()));
+        $this->assign("soft_channels", recursion(collection(Channel::where("id", "NOT IN", [1, 22, 44])->order("weigh desc,id desc")->select())->toArray()));
+        $this->assign("is_ruanjiankaifagongsi", $request->url() === "/ruanjiankaifagongsi.html" || strstr($request->url(), 'ruanjiankaifagongsi'));
+        //判断站点状态
+        if (isset($config['openedsite']) && !in_array('pc', explode(',', $config['openedsite']))) {
+            if ($this->controller != 'order' && $this->action != 'epay') {
+                $this->error('站点已关闭');
+            }
+        }
+    }
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        // 如果请求参数action的值为一个方法名,则直接调用
+        $action = $this->request->post("action");
+        if ($action && $this->request->isPost()) {
+            return $this->$action();
+        }
+    }
+
+    /**
+     * 是否加密ID处理
+     */
+    protected function hashids($name = 'id')
+    {
+        $config = get_addon_config('cms');
+        $getValue = $this->request->get($name);
+        $postValue = $this->request->post($name);
+        if ($config['archiveshashids'] && ($getValue || $postValue)) {
+            if ($getValue) {
+                $getValue = (int)IntCode::decode($getValue);
+                $this->request->get([$name => $getValue]);
+            }
+            if ($postValue) {
+                $postValue = (int)IntCode::decode($postValue);
+                $this->request->post([$name => $postValue]);
+            }
+            $this->request->param('');
+        }
+    }
+
+}

+ 188 - 0
addons/cms/controller/Channel.php

@@ -0,0 +1,188 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\Service;
+use addons\cms\model\Archives;
+use addons\cms\model\Channel as ChannelModel;
+use addons\cms\model\Fields;
+use addons\cms\model\Modelx;
+use addons\cms\model\SpiderLog;
+use think\Cache;
+use think\Config;
+
+/**
+ * 栏目控制器
+ * Class Channel
+ * @package addons\cms\controller
+ */
+class Channel extends Base
+{
+    public function index()
+    {
+        $config = get_addon_config('cms');
+
+        $diyname = $this->request->param('diyname');
+
+        if ($diyname && !is_numeric($diyname)) {
+            $channel = ChannelModel::getByDiyname($diyname);
+        } else {
+            $id = $diyname ? $diyname : $this->request->param('id', '');
+            $channel = ChannelModel::get($id);
+        }
+        if (!$channel || $channel['status'] != 'normal') {
+            $this->error(__('No specified channel found'));
+        }
+
+        $filter = $this->request->get('filter/a', []);
+        $orderby = $this->request->get('orderby', '');
+        $orderway = $this->request->get('orderway', '');
+        $multiple = $this->request->get('multiple/d', 0);
+
+        $orderway = $orderway && in_array(strtolower($orderway), ['asc', 'desc']) ? $orderway : 'desc';
+
+        $params = [];
+        $filter = $this->request->get();
+        $filter = array_diff_key($filter, array_flip(['orderby', 'orderway', 'page', 'multiple']));
+        if (isset($filter['filter'])) {
+            $filter = array_merge($filter, $filter['filter']);
+            unset($filter['filter']);
+        }
+        if ($filter) {
+            $filter = array_filter($filter, 'strlen');
+            $params['filter'] = $filter;
+            $params = $filter;
+        }
+        if ($orderby) {
+            $params['orderby'] = $orderby;
+        }
+        if ($orderway) {
+            $params['orderway'] = $orderway;
+        }
+        if ($multiple) {
+            $params['multiple'] = $multiple;
+        }
+        if ($channel['type'] === 'link') {
+            $this->redirect($channel['outlink']);
+        }
+
+        //加载模型数据
+        $model = Modelx::get($channel['model_id']);
+        if (!$model) {
+            $this->error(__('No specified model found'));
+        }
+
+        //默认排序字段
+        $orders = [
+            ['name' => 'default', 'field' => 'weigh DESC,publishtime DESC', 'title' => __('Default')],
+        ];
+
+        //合并主表筛选字段
+        $orders = array_merge($orders, $model->getOrderFields());
+
+        //获取过滤列表
+        list($filterList, $filter, $params, $fields, $multiValueFields, $fieldsList) = Service::getFilterList('model', $model['id'], $filter, $params, $multiple);
+
+        //获取排序列表
+        list($orderList, $orderby, $orderway) = Service::getOrderList($orderby, $orderway, $orders, $params, $fieldsList);
+
+        //获取过滤的条件和绑定参数
+        list($filterWhere, $filterBind) = Service::getFilterWhereBind($filter, $multiValueFields, $multiple);
+
+        $filterChannel = function ($query) use ($channel) {
+            $query->where(function ($query) use ($channel) {
+                if ($channel['listtype'] <= 2) {
+                    $query->whereOr("channel_id", $channel['id']);
+                }
+                if ($channel['listtype'] == 1 || $channel['listtype'] == 3) {
+                    $query->whereOr('channel_id', 'in', function ($query) use ($channel) {
+                        $query->name("cms_channel")->where('parent_id', $channel['id'])->field("id");
+                    });
+                }
+                if ($channel['listtype'] == 0 || $channel['listtype'] == 4) {
+                    $childrenIds = \addons\cms\model\Channel::getChannelChildrenIds($channel['id'], false);
+                    if ($childrenIds) {
+                        $query->whereOr('channel_id', 'in', $childrenIds);
+                    }
+                }
+            })
+                ->whereOr("(`channel_ids`!='' AND FIND_IN_SET('{$channel['id']}', `channel_ids`))");
+        };
+
+        //模板名称
+        $template = ($this->request->isAjax() ? '/ajax/' : '/') . ($channel["{$channel['type']}tpl"] ?? '');
+        $template = preg_replace('/\.html$/', '', $template);
+
+        $pagelistParams = Service::getPagelistParams($template);
+        //分页大小
+        $pagesize = $pagelistParams['pagesize'] ?? $channel['pagesize'];
+        //过滤条件
+        $filterPagelist = function ($query) use ($pagelistParams) {
+            if (isset($pagelistParams['condition'])) {
+                $query->where($pagelistParams['condition']);
+            }
+        };
+
+        //分页模式
+        $simple = $config['loadmode'] == 'paging' && $config['pagemode'] == 'full' ? false : true;
+
+        //缓存列表总数
+        if (!$simple && ($config['cachelistcount'] ?? false)) {
+            $simple = Archives::with(['channel', 'user'])->alias('a')
+                ->where('a.status', 'normal')
+                ->whereNull('a.deletetime')
+                ->where($filterWhere)
+                ->bind($filterBind)
+                ->where($filterPagelist)
+                ->where($filterChannel)
+                ->where('model_id', $channel->model_id)
+                ->join($model['table'] . ' n', 'a.id=n.id', 'LEFT')
+                ->cache("cms-channel-list-" . $channel['id'] . '-' . md5(serialize($filter)), 3600) //总数缓存1小时
+                ->count();
+        }
+
+        //加载列表数据
+        $pageList = Archives::with(['channel', 'user'])->alias('a')
+            ->where('a.status', 'normal')
+            ->whereNull('a.deletetime')
+            ->where($filterWhere)
+            ->bind($filterBind)
+            ->where($filterPagelist)
+            ->where($filterChannel)
+            ->where('model_id', $channel->model_id)
+            ->join($model['table'] . ' n', 'a.id=n.id', 'LEFT')
+            ->field('a.*')
+            ->field('id,content', true, config('database.prefix') . $model['table'], 'n')
+            ->order($orderby, $orderway)
+            ->paginate($pagesize, $simple);
+
+        Service::appendTextAndList('model', $model->id, $pageList, true);
+
+        Service::appendTextAndList('channel', 0, $channel);
+
+        $pageList->appends(array_filter($params));
+        $this->view->assign("__FILTERLIST__", $filterList);
+        $this->view->assign("__ORDERLIST__", $orderList);
+        $this->view->assign("__PAGELIST__", $pageList);
+        $this->view->assign("__CHANNEL__", $channel);
+
+        SpiderLog::record('channel', $channel['id']);
+
+        //设置TKD
+        Config::set('cms.title', isset($channel['seotitle']) && $channel['seotitle'] ? $channel['seotitle'] : $channel['name']);
+        Config::set('cms.keywords', $channel['keywords']);
+        Config::set('cms.description', $channel['description']);
+        Config::set('cms.image', isset($channel['image']) && $channel['image'] ? cdnurl($channel['image'], true) : '');
+
+        //读取模板
+        if (!$template) {
+            $this->error('请检查栏目是否配置相应的模板');
+        }
+
+        if ($this->request->isAjax()) {
+            $this->success("", "", $this->view->fetch($template));
+        }
+
+        return $this->view->fetch($template);
+    }
+}

+ 70 - 0
addons/cms/controller/Comment.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\CommentException;
+use addons\cms\model\Comment as CommentModel;
+use think\addons\Controller;
+use think\Exception;
+
+/**
+ * 评论控制器
+ * Class Comment
+ * @package addons\cms\controller
+ */
+class Comment extends Base
+{
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $type = $this->request->post("type");
+        if ($type == 'archives') {
+            //检测ID是否加密
+            $this->hashids('aid');
+        }
+    }
+
+    /**
+     * 发表评论
+     */
+    public function post()
+    {
+        try {
+            $params = $this->request->post();
+            CommentModel::postComment($params);
+        } catch (CommentException $e) {
+            if ($e->getCode() == 1) {
+                $this->success($e->getMessage(), null, ['token' => $this->request->token()]);
+            } else {
+                $this->error($e->getMessage(), null, ['token' => $this->request->token()]);
+            }
+        } catch (Exception $e) {
+            $this->error($e->getMessage(), null, ['token' => $this->request->token()]);
+        }
+        $this->success(__('评论成功!'), null, ['token' => $this->request->token()]);
+    }
+
+    /**
+     * 取消评论订阅
+     */
+    public function unsubscribe()
+    {
+        $id = (int)$this->request->param('id');
+        $key = $this->request->param('key');
+        $comment = CommentModel::get($id, 'user');
+        if (!$comment) {
+            $this->error("评论未找到");
+        }
+        if ($key !== md5($comment['id'] . $comment['user']['email'])) {
+            $this->error("无法进行该操作");
+        }
+        if (!$comment['subscribe']) {
+            $this->error("评论已经取消订阅,请勿重复操作");
+        }
+        $comment->subscribe = 0;
+        $comment->save();
+        $this->success('取消评论订阅成功');
+    }
+}

+ 325 - 0
addons/cms/controller/Diyform.php

@@ -0,0 +1,325 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\Service;
+use addons\cms\model\Diydata;
+use addons\cms\model\Diyform as DiyformModel;
+use addons\cms\model\Fields;
+use addons\cms\model\SpiderLog;
+use think\Config;
+use think\Exception;
+use think\Hook;
+use think\Request;
+
+/**
+ * 自定义表单控制器
+ * Class Diyform
+ * @package addons\cms\controller
+ */
+class Diyform extends Base
+{
+
+    protected $diyform = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+
+        $diyname = $this->request->param('diyname');
+        if ($diyname && !is_numeric($diyname)) {
+            $diyform = DiyformModel::getByDiyname($diyname);
+        } else {
+            $id = $diyname ? $diyname : $this->request->get('id', '');
+            $diyform = DiyformModel::get($id);
+        }
+        if (!$diyform || $diyform['status'] != 'normal') {
+            $this->error(__('表单未找到'));
+        }
+        $this->diyform = $diyform;
+        $this->view->assign("__DIYFORM__", $diyform);
+    }
+
+    public function __construct(Request $request = null)
+    {
+        parent::__construct($request);
+
+        $channel = \addons\cms\model\Channel::getChannelByLinktype('diyform', $this->diyform->id);
+        $this->view->assign("__CHANNEL__", $channel);
+    }
+
+    /**
+     * 数据列表
+     * @return string
+     */
+    public function index()
+    {
+        $diyform = $this->diyform;
+        if (!$diyform['isguest'] && !$this->auth->isLogin()) {
+            $this->error("请登录后再操作", "index/user/login");
+        }
+        $config = get_addon_config('cms');
+
+        $filter = $this->request->get('filter/a', []);
+        $orderby = $this->request->get('orderby', '');
+        $orderway = $this->request->get('orderway', '');
+        $multiple = $this->request->get('multiple/d', 0);
+
+        $orderway = $orderway && in_array(strtolower($orderway), ['asc', 'desc']) ? $orderway : 'desc';
+
+        $params = [];
+        $filter = $this->request->get();
+        $filter = array_diff_key($filter, array_flip(['orderby', 'orderway', 'page', 'multiple']));
+        if (isset($filter['filter'])) {
+            $filter = array_merge($filter, $filter['filter']);
+            unset($filter['filter']);
+        }
+        if ($filter) {
+            $params['filter'] = $filter;
+        }
+        if ($orderby) {
+            $params['orderby'] = $orderby;
+        }
+        if ($orderway) {
+            $params['orderway'] = $orderway;
+        }
+        if ($multiple) {
+            $params['multiple'] = $multiple;
+        }
+
+        //默认排序字段
+        $orders = [
+            ['name' => 'default', 'field' => 'createtime DESC,id DESC', 'title' => __('Default')],
+        ];
+
+        //合并特殊筛选字段
+        $orders = array_merge($orders, $diyform->getOrderFields());
+
+        //获取过滤列表
+        list($filterList, $filter, $params, $fields, $multiValueFields, $fieldsList) = Service::getFilterList('diyform', $diyform['id'], $filter, $params, $multiple);
+
+        //获取排序列表
+        list($orderList, $orderby, $orderway) = Service::getOrderList($orderby, $orderway, $orders, $params, $fieldsList);
+
+        //获取过滤的条件和绑定参数
+        list($filterWhere, $filterBind) = Service::getFilterWhereBind($filter, $multiValueFields, $multiple);
+
+        //模板名称
+        $template = preg_replace("/\.html$/i", "", $diyform['listtpl'] ? $diyform['listtpl'] : 'diyform_list');
+        $template = $this->request->get("noframe", "0") ? "diyform_noframe" : $template;
+        $template = '/' . $template;
+
+        $pagelistParams = Service::getPagelistParams($template);
+        //分页大小
+        $pagesize = $pagelistParams['pagesize'] ?? 10;
+        //过滤条件
+        $filterPagelist = function ($query) use ($pagelistParams) {
+            if (isset($pagelistParams['condition'])) {
+                $query->where($pagelistParams['condition']);
+            }
+        };
+
+        $auth = $this->auth;
+        $model = new Diydata([], $diyform);
+        $pageList = $model
+            ->where($filterWhere)
+            ->bind($filterBind)
+            ->where(function ($query) use ($diyform, $auth) {
+                //用户过滤模式
+                //如果是仅用户自己消息可见
+                if ($diyform['usermode'] == 'user') {
+                    $query->where('user_id', $auth->id);
+                }
+            })
+            ->where(function ($query) use ($diyform, $auth) {
+                //状态过滤模式
+                if ($diyform['statusmode'] === 'normal') {
+                    if ($auth->id) {
+                        $query->whereRaw("user_id='" . intval($auth->id) . "' OR status='normal'");
+                    } else {
+                        $query->where('status', 'normal');
+                    }
+                }
+            })
+            ->where($filterPagelist)
+            ->order($orderby, $orderway)
+            ->paginate($pagesize, $config['pagemode'] == 'simple');
+
+        Service::appendTextAndList('diyform', $diyform->id, $pageList, true);
+
+        $this->view->assign("__FILTERLIST__", $filterList);
+        $this->view->assign("__ORDERLIST__", $orderList);
+        $this->view->assign("__PAGELIST__", $pageList);
+
+        SpiderLog::record('diyform', $diyform['id']);
+
+        //设置TKD
+        Config::set('cms.title', $diyform['seotitle'] ?: $diyform['title']);
+        Config::set('cms.keywords', $diyform['keywords']);
+        Config::set('cms.description', $diyform['description']);
+        Config::set('cms.image', isset($diyform['image']) && $diyform['image'] ? cdnurl($diyform['image'], true) : '');
+
+        return $this->view->fetch($template);
+    }
+
+    /**
+     * 查看详情
+     * @return string
+     */
+    public function show()
+    {
+        $diyform = $this->diyform;
+
+        if (!$diyform['isguest'] && !$this->auth->isLogin()) {
+            $this->error("请登录后再操作", "index/user/login");
+        }
+        $id = $this->request->param('id/d');
+        $auth = $this->auth;
+        $model = new Diydata([], $diyform);
+
+        $diydata = $model
+            ->where('id', $id)
+            ->where(function ($query) use ($diyform, $auth) {
+                //用户过滤模式
+                //如果是仅用户自己消息可见
+                if ($diyform['usermode'] == 'user') {
+                    $query->where('user_id', $auth->id);
+                }
+            })
+            ->where(function ($query) use ($diyform, $auth) {
+                //状态过滤模式
+                if ($diyform['statusmode'] === 'normal') {
+                    if ($auth->id) {
+                        $query->whereRaw("user_id='" . intval($auth->id) . "' OR status='normal'");
+                    } else {
+                        $query->where('status', 'normal');
+                    }
+                }
+            })
+            ->find();
+
+        if (!$diydata) {
+            $this->error("数据未找到或正在审核");
+        }
+        $fieldsList = Fields::where('source', 'diyform')->where('source_id', $diyform['id'])
+            ->order('weigh desc,id desc')->column("*", "name");
+
+        Service::appendTextAndList('diyform', $diyform->id, $diydata);
+
+        $this->view->assign('fieldsList', $fieldsList);
+        $this->view->assign("__DIYDATA__", $diydata);
+
+        //设置TKD
+        Config::set('cms.title', $diyform['name'] . '详情');
+        Config::set('cms.keywords', '');
+        Config::set('cms.description', '');
+        Config::set('cms.image', isset($diyform['image']) && $diyform['image'] ? cdnurl($diyform['image'], true) : '');
+
+        //加载模板
+        $template = preg_replace("/\.html$/i", "", $diyform['showtpl'] ? $diyform['showtpl'] : 'diyform_show');
+        return $this->view->fetch('/' . $template);
+    }
+
+    /**
+     * 自定义表单提交
+     */
+    public function post()
+    {
+        $diyform = $this->diyform;
+        $id = $this->request->request("id/d");
+        $diydata = new Diydata([], $diyform);
+        if ($diyform['needlogin'] && !$this->auth->isLogin()) {
+            $this->error("请登录后再操作", "index/user/login");
+        }
+        if ($id) {
+            if (!$this->auth->isLogin()) {
+                $this->error("请登录后再操作", "index/user/login");
+            }
+            $diydata = $diydata->find($id);
+            if (!$diydata) {
+                $this->error("未找到指定数据");
+            }
+            if ($diydata['user_id'] != $this->auth->id) {
+                $this->error("无法进行越权操作");
+            }
+        }
+        if ($this->request->isPost()) {
+            $config = get_addon_config('cms');
+            $this->token();
+
+            //检测是否开启验证码
+            if (isset($diyform['iscaptcha']) && $diyform['iscaptcha']) {
+                $captcha = $this->request->post('captcha');
+                if (!captcha_check($captcha)) {
+                    $this->error("验证码不正确");
+                }
+            }
+            $row = $this->request->post('row/a', '', 'trim,xss_clean');
+            unset($row['id']);
+
+            $fields = DiyformModel::getDiyformFields($diyform['id']);
+            foreach ($fields as $index => $field) {
+                if ($field['isrequire'] && (!isset($row[$field['name']]) || $row[$field['name']] == '')) {
+                    $this->error("{$field['title']}不能为空!");
+                }
+            }
+
+            $row['user_id'] = $this->auth->id;
+            $diydata['status'] = 'hidden';
+            try {
+                $diydata->save($row);
+            } catch (\Exception $e) {
+                $this->error("发生错误:" . $e->getMessage());
+            }
+            //发送通知
+            Service::notice(config('cms.sitename') . '收到新的' . $diyform['name']);
+
+            $redirecturl = $diyform['redirecturl'] ? $diyform['redirecturl'] : $diyform['url'];
+            $this->success($diyform['successtips'] ? $diyform['successtips'] : '提交成功!', $redirecturl);
+        }
+
+        $fields = DiyformModel::getDiyformFields($diyform['id'], $diydata->toArray());
+        $data = [
+            'fields' => $fields
+        ];
+        $diyform['fieldslist'] = $this->fetch('common/fields', $data);
+
+        // 语言检测
+        $lang = $this->request->langset();
+        $lang = preg_match("/^([a-zA-Z\-_]{2,10})\$/i", $lang) ? $lang : 'zh-cn';
+
+        $site = Config::get("site");
+        $upload = \app\common\model\Config::upload();
+        // 上传信息配置后
+        Hook::listen("upload_config_init", $upload);
+
+        // 配置信息
+        $config = [
+            'site'           => array_intersect_key($site, array_flip(['name', 'cdnurl', 'version', 'timezone', 'languages'])),
+            'upload'         => $upload,
+            'modulename'     => 'addons',
+            'controllername' => 'diyform',
+            'actionname'     => 'index',
+            'jsname'         => 'diyform/index',
+            'moduleurl'      => rtrim(url("/index", '', false), '/'),
+            'language'       => $lang
+        ];
+        $config = array_merge($config, Config::get("view_replace_str"));
+
+        Config::set('upload', array_merge(Config::get('upload'), $upload));
+        // 配置信息后
+        Hook::listen("config_init", $config);
+
+        $this->view->assign('diydata', $diydata);
+        $this->view->assign('__DIYDATA__', $diydata);
+        $this->view->assign('jsconfig', $config);
+
+        //设置TKD
+        Config::set('cms.title', $diyform['posttitle']);
+        Config::set('cms.keywords', '');
+        Config::set('cms.description', '');
+
+        $template = preg_replace("/\.html$/i", "", $diyform['posttpl'] ? $diyform['posttpl'] : 'diyform_post');
+        return $this->view->fetch('/' . $template);
+    }
+}

+ 33 - 0
addons/cms/controller/Go.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\model\Autolink;
+use think\Config;
+
+/**
+ * 跳转控制器
+ * Class Go
+ * @package addons\cms\controller
+ */
+class Go extends Base
+{
+    protected $noNeedLogin = ['*'];
+
+    public function index()
+    {
+        $url = $this->request->get("url", "", 'trim,xss_clean');
+        $id = $this->request->get("id/d", "0");
+        if ($id) {
+            $autolink = Autolink::get($id);
+            if ($autolink) {
+                $autolink->setInc("views");
+                $this->redirect($autolink['url']);
+            }
+        }
+
+        Config::set('cms.title', '跳转提示');
+        return $this->view->fetch("/outlink", ['url' => $url]);
+    }
+
+}

+ 46 - 0
addons/cms/controller/Import.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\VicDict;
+
+/**
+ * 导入
+ * Class Import
+ * @package addons\cms\controller
+ */
+class Import extends Base
+{
+    protected $noNeedLogin = ["*"];
+    protected $layout = 'default';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+
+        if (!$this->request->isCli()) {
+            $this->error('只允许在终端进行操作!');
+        }
+    }
+
+    /**
+     * 导入词典
+     */
+    public function dict()
+    {
+        define('_VIC_WORD_DICT_PATH_', ADDON_PATH . 'cms/data/dict.json');
+        $dict = new VicDict('json');
+
+        //添加词语词库 add(词语,词性) 可以是除保留字符(/,\ , \x  ,\i),以外的utf-8编码的任何字符
+        $lines = file(ADDON_PATH . 'cms/data/dict.txt', FILE_IGNORE_NEW_LINES);
+        foreach ($lines as $index => $line) {
+            $lineArr = explode(' ', $line);
+            $dict->add($lineArr[0], 'n');
+        }
+
+        //保存词库
+        $dict->save();
+        echo "done";
+    }
+
+}

+ 40 - 0
addons/cms/controller/Index.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\model\SpiderLog;
+use think\Config;
+
+/**
+ * CMS首页控制器
+ * Class Index
+ * @package addons\cms\controller
+ */
+class Index extends Base
+{
+    public function index()
+    {
+        $config = get_addon_config('cms');
+
+        //设置TKD
+        Config::set('cms.title', $config['title'] ?: __('Home'));
+        Config::set('cms.keywords', $config['keywords']);
+        Config::set('cms.description', $config['description']);
+
+        //首页分页大小
+        $pagesize = $config['indexpagesize'] ?? 10;
+        //首页加载和分页模式
+        $simple = $config['indexloadmode'] == 'paging' && $config['indexpagemode'] == 'full' ? false : true;
+        $simple = $simple ? 'true' : \addons\cms\model\Archives::where('status', 'normal')->cache(true)->count();
+
+        $archivesList = \addons\cms\model\Archives::getArchivesList(['cache' => false, 'paginate' => "{$pagesize},{$simple},page"]);
+        $this->view->assign("__PAGELIST__", $archivesList);
+
+        if ($this->request->isAjax()) {
+            $this->success("", "", $this->view->fetch('ajax/index'));
+        }
+        SpiderLog::record('index', 0);
+        return $this->view->fetch('/index');
+    }
+
+}

+ 92 - 0
addons/cms/controller/Order.php

@@ -0,0 +1,92 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\OrderException;
+use addons\cms\model\Archives;
+use think\Exception;
+
+/**
+ * 订单控制器
+ * Class Order
+ * @package addons\cms\controller
+ */
+class Order extends Base
+{
+
+    /**
+     * 创建订单并发起支付请求
+     */
+    public function submit()
+    {
+        $config = get_addon_config('cms');
+        //是否需要登录后才可以支付
+        if ($config['ispaylogin'] && !$this->auth->isLogin()) {
+            $this->error("请登录后再进行操作!", "index/user/login");
+        }
+
+        $id = $this->request->request('id');
+        $paytype = $this->request->request('paytype');
+        $archives = Archives::get($id);
+        if (!$archives || ($archives['user_id'] != $this->auth->id && $archives['status'] != 'normal') || $archives['deletetime']) {
+            $this->error('未找到指的文档');
+        }
+
+        try {
+            $response = \addons\cms\library\Order::submit($id, $paytype ? $paytype : $config['defaultpaytype']);
+        } catch (OrderException $e) {
+            if ($e->getCode() == 1) {
+                $this->success($e->getMessage(), $archives->url);
+            } else {
+                $this->error($e->getMessage(), $archives->url);
+            }
+        } catch (Exception $e) {
+            $this->error($e->getMessage(), $archives->url);
+        }
+
+        return $response;
+    }
+
+    /**
+     * 企业支付通知和回调
+     */
+    public function epay()
+    {
+        $type = $this->request->param('type');
+        $paytype = $this->request->param('paytype');
+        if ($type == 'notify') {
+            $pay = \addons\epay\library\Service::checkNotify($paytype);
+            if (!$pay) {
+                echo '签名错误';
+                return;
+            }
+            $data = $pay->verify();
+            try {
+                $payamount = $paytype == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
+                \addons\cms\library\Order::settle($data['out_trade_no'], $payamount);
+            } catch (Exception $e) {
+            }
+            echo $pay->success();
+        } else {
+            $pay = \addons\epay\library\Service::checkReturn($paytype);
+            if (!$pay) {
+                $this->error('签名错误');
+            }
+            if ($pay === true) {
+                //微信支付
+                $data = ['out_trade_no' => $this->request->param('orderid')];
+            } else {
+                $data = $pay->verify();
+            }
+
+            $order = \addons\cms\model\Order::getByOrderid($data['out_trade_no']);
+            if (!$order->archives) {
+                $this->error('未找到文档信息!');
+            }
+            //你可以在这里定义你的提示信息,但切记不可在此编写逻辑
+            $this->redirect($order->archives->url);
+            //$this->success("恭喜你!支付成功!", $order->archives->url);
+        }
+        return;
+    }
+}

+ 69 - 0
addons/cms/controller/Page.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\Service;
+use addons\cms\model\Fields;
+use addons\cms\model\Page as PageModel;
+use addons\cms\model\SpiderLog;
+use think\Config;
+
+/**
+ * CMS单页控制器
+ * Class Page
+ * @package addons\cms\controller
+ */
+class Page extends Base
+{
+    public function index()
+    {
+        $diyname = $this->request->param('diyname');
+        if ($diyname && !is_numeric($diyname)) {
+            $page = PageModel::getByDiyname($diyname);
+        } else {
+            $id = $diyname ? $diyname : $this->request->param('id', '');
+            $page = PageModel::get($id);
+        }
+        if (!$page || $page['status'] != 'normal') {
+            $this->error(__('No specified page found'));
+        }
+        $page->setInc('views');
+
+        Service::appendTextAndList('page', 0, $page);
+
+        $this->view->assign("__PAGE__", $page);
+
+        $channel = \addons\cms\model\Channel::getChannelByLinktype('page', $page['id']);
+        $this->view->assign("__CHANNEL__", $channel);
+
+        SpiderLog::record('page', $page['id']);
+
+        //设置TKD
+        Config::set('cms.title', isset($page['seotitle']) && $page['seotitle'] ? $page['seotitle'] : $page['title']);
+        Config::set('cms.keywords', $page['keywords']);
+        Config::set('cms.description', $page['description']);
+        Config::set('cms.image', isset($page['image']) && $page['image'] ? cdnurl($page['image'], true) : '');
+
+        $template = preg_replace("/\.html$/i", "", $page['showtpl'] ? $page['showtpl'] : 'page');
+        return $this->view->fetch('/' . $template);
+    }
+
+    /**
+     * 赞与踩
+     */
+    public function vote()
+    {
+        $id = (int)$this->request->post("id");
+        $type = trim($this->request->post("type", ""));
+        if (!$id || !$type) {
+            $this->error(__('Operation failed'));
+        }
+        $page = \addons\cms\model\Page::get($id);
+        if (!$page) {
+            $this->error(__('No specified page found'));
+        }
+        $page->where('id', $id)->setInc($type === 'like' ? 'likes' : 'dislikes', 1);
+        $page = \addons\cms\model\Page::get($id);
+        $this->success(__('Operation completed'), null, ['likes' => $page->likes, 'dislikes' => $page->dislikes, 'likeratio' => $page->likeratio]);
+    }
+}

+ 257 - 0
addons/cms/controller/Search.php

@@ -0,0 +1,257 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\FulltextSearch;
+use addons\cms\library\Service;
+use addons\cms\model\Archives;
+use addons\cms\model\Modelx;
+use addons\cms\model\SearchLog;
+use think\Config;
+use think\Session;
+
+/**
+ * 搜索控制器
+ * Class Search
+ * @package addons\cms\controller
+ */
+class Search extends Base
+{
+    public function index()
+    {
+        $config = get_addon_config('cms');
+
+        $search = $this->request->request("search", $this->request->request("q", ""));
+        $search = strip_tags($search);
+        $search = mb_substr($search, 0, 100);
+
+        if (!$search) {
+            $this->error("关键字不能为空");
+        }
+        //搜索入库
+        $token = $this->request->request("__searchtoken__");
+        if ($search && $token && $token == Session::get("__searchtoken__")) {
+            $log = SearchLog::getByKeywords($search);
+            if ($log) {
+                $log->setInc("nums");
+            } else {
+                SearchLog::create(['keywords' => $search, 'nums' => 1]);
+            }
+        }
+
+        if ($config['searchtype'] == 'xunsearch') {
+            return $this->xunsearch();
+        }
+
+        $channel = $model = null;
+        $channel_id = $this->request->get('channel_id');
+        $model_id = $this->request->get('model_id');
+        if ($channel_id || $model_id) {
+            $channel = \addons\cms\model\Channel::get($channel_id);
+            $model_id = $channel ? $channel['model_id'] : $model_id;
+
+            //加载模型数据
+            $model = Modelx::get($model_id);
+            if (!$model) {
+                $this->error(__('No specified model found'));
+            }
+        }
+
+        $filterList = [];
+        $orderList = [];
+
+        $orderby = $this->request->get('orderby', '');
+        $orderway = $this->request->get('orderway', '');
+        $orderway = $orderway && in_array(strtolower($orderway), ['asc', 'desc']) ? $orderway : 'desc';
+        $params = ['q' => $search];
+        if ($orderby) {
+            $params['orderby'] = $orderby;
+        }
+        if ($orderway) {
+            $params['orderway'] = $orderway;
+        }
+
+        //默认排序字段
+        $orders = [
+            ['name' => 'default', 'field' => 'weigh', 'title' => __('Default')],
+            ['name' => 'views', 'field' => 'views', 'title' => __('Views')],
+            ['name' => 'id', 'field' => 'id', 'title' => __('Post date')],
+        ];
+
+        //获取排序列表
+        list($orderList, $orderby, $orderway) = Service::getOrderList($orderby, $orderway, $orders, $params);
+
+        //模板名称
+        $template = ($this->request->isAjax() ? '/ajax' : '/') . 'search';
+
+        $pagelistParams = Service::getPagelistParams($template);
+        //分页大小
+        $pagesize = $pagelistParams['pagesize'] ?? 10;
+        //过滤条件
+        $filterPagelist = function ($query) use ($pagelistParams) {
+            if (isset($pagelistParams['condition'])) {
+                $query->where($pagelistParams['condition']);
+            }
+        };
+
+        $pageList = new Archives();
+        if ($model) {
+            $pageList->join($model['table'] . ' n', 'a.id=n.id', 'LEFT')
+                ->field('id,content', true, config('database.prefix') . $model['table'], 'n')
+                ->where('model_id', $model->id);
+        }
+        $pageList = $pageList->with(['channel', 'user'])->alias('a')
+            ->where('a.status', 'normal')
+            ->where(function ($query) use ($search) {
+                $keywordArr = explode(' ', $search);
+                foreach ($keywordArr as $index => $item) {
+                    $query->where('a.title', 'like', '%' . $item . '%');
+                }
+            })
+            ->whereNull('a.deletetime')
+            ->field('a.*')
+            ->where(function ($query) use ($channel) {
+                if ($channel) {
+                    $query->where(function ($query) use ($channel) {
+                        if ($channel['listtype'] <= 2) {
+                            $query->whereOr("channel_id", $channel['id']);
+                        }
+                        if ($channel['listtype'] == 1 || $channel['listtype'] == 3) {
+                            $query->whereOr('channel_id', 'in', function ($query) use ($channel) {
+                                $query->name("cms_channel")->where('parent_id', $channel['id'])->field("id");
+                            });
+                        }
+                        if ($channel['listtype'] == 0 || $channel['listtype'] == 4) {
+                            $childrenIds = \addons\cms\model\Channel::getChannelChildrenIds($channel['id'], false);
+                            if ($childrenIds) {
+                                $query->whereOr('channel_id', 'in', $childrenIds);
+                            }
+                        }
+                    })
+                        ->whereOr("(`channel_ids`!='' AND FIND_IN_SET('{$channel['id']}', `channel_ids`))");
+                }
+            })
+            ->where($filterPagelist)
+            ->order($orderby, $orderway)
+            ->paginate($pagesize, $config['pagemode'] == 'simple');
+
+        $pageList->appends(array_filter($params));
+        $this->view->assign("__FILTERLIST__", $filterList);
+        $this->view->assign("__ORDERLIST__", $orderList);
+        $this->view->assign("__PAGELIST__", $pageList);
+        $this->view->assign("__SEARCHTERM__", $search);
+
+        Config::set('cms.title', __("Search for %s", $search));
+
+        if ($this->request->isAjax()) {
+            $this->success("", "", $this->view->fetch($template));
+        }
+        return $this->view->fetch($template);
+    }
+
+    public function typeahead()
+    {
+        $search = $this->request->post("search", $this->request->post("q", ""));
+        $search = mb_substr($search, 0, 100);
+
+        $list = Archives
+            ::where('status', 'normal')
+            ->whereNull('deletetime')
+            ->where('title', 'like', "%{$search}%")
+            ->order('id', 'desc')
+            ->field('id,title,diyname,channel_id,likes,dislikes,tags,createtime')
+            ->limit(10)
+            ->select();
+        $result = collection($list)->toArray();
+        $result[] = ['id' => 0, 'title' => __('Search more %s', $search), 'url' => addon_url("cms/search/index", [':search' => $search, 'search' => $search])];
+        return json($result);
+    }
+
+    /**
+     * Xunsearch搜索
+     * @return string
+     * @throws \think\Exception
+     */
+    public function xunsearch()
+    {
+        $orderList = [
+            'relevance'       => '默认排序',
+            'createtime_desc' => '发布时间从新到旧',
+            'createtime_asc'  => '发布时间从旧到新',
+            'views_desc'      => '浏览次数从多到少',
+            'views_asc'       => '浏览次数从少到多',
+            'comments_desc'   => '评论次数从多到少',
+            'comments_asc'    => '评论次数从少到多',
+        ];
+
+        $q = $this->request->request('q', $this->request->request('search', ''));
+        $q = strip_tags($q);
+        $q = mb_substr($q, 0, 100);
+
+        $page = $this->request->get('page/d', '1');
+        $order = $this->request->get('order', '');
+        $fulltext = $this->request->get('fulltext/d', '1');
+        $fuzzy = $this->request->get('fuzzy/d', '0');
+        $synonyms = $this->request->get('synonyms/d', '0');
+
+        $order = isset($orderList[$order]) ? $order : 'relevance';
+
+        $total_begin = microtime(true);
+        $search = null;
+        $pagesize = 10;
+        $error = '';
+
+        $result = FulltextSearch::search($q, $page, $pagesize, $order, $fulltext, $fuzzy, $synonyms);
+
+        if (!$result) {
+            $this->error('请检查Xunsearch配置');
+        }
+
+        // 计算总耗时
+        $total_cost = microtime(true) - $total_begin;
+
+        //获取热门搜索
+        $hot = FulltextSearch::hot();
+
+        $data = [
+            'q'           => $q,
+            'error'       => $error,
+            'total'       => $result['total'] ?? 0,
+            'count'       => $result['count'] ?? 0,
+            'search_cost' => $result['microseconds'] ?? 0,
+            'docs'        => $result['list'] ?? [],
+            'pager'       => $result['pager'] ?? '',
+            'corrected'   => $result['corrected'] ?? [],
+            'highlight'   => $result['highlight'] ?? [],
+            'related'     => $result['related'] ?? [],
+            'search'      => $search,
+            'fulltext'    => $fulltext,
+            'synonyms'    => $synonyms,
+            'fuzzy'       => $fuzzy,
+            'order'       => $order,
+            'orderList'   => $orderList,
+            'hot'         => $hot,
+            'total_cost'  => $total_cost,
+        ];
+
+        Config::set('cms.title', __("Search for %s", $q));
+        $this->view->assign("title", $q);
+        $this->view->assign($data);
+        return $this->view->fetch('/xunsearch');
+    }
+
+    public function suggestion()
+    {
+        $q = trim($this->request->get('q', ''));
+        $q = mb_substr($q, 0, 100);
+
+        $terms = [];
+        $config = get_addon_config('cms');
+        if ($config['searchtype'] == 'xunsearch') {
+            $terms = FulltextSearch::suggestion($q);
+        } else {
+            $terms = SearchLog::where("keywords", "LIKE", "{$q}%")->where("nums", ">", 0)->where("status", "normal")->column("keywords");
+        }
+        return json($terms);
+    }
+}

+ 243 - 0
addons/cms/controller/Sitemap.php

@@ -0,0 +1,243 @@
+<?php
+
+namespace addons\cms\controller;
+
+use think\Config;
+use think\Response;
+
+/**
+ * Sitemap控制器
+ * Class Sitemap
+ * @package addons\cms\controller
+ */
+class Sitemap extends Base
+{
+    protected $noNeedLogin = ['*'];
+    protected $options = [
+        'item_key'  => '',
+        'root_node' => 'urlset',
+        'item_node' => 'url',
+        'root_attr' => 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:mobile="http://www.baidu.com/schemas/sitemap-mobile/1/"'
+    ];
+    //默认配置
+    protected $config = [
+        'pagesize' => 5000,
+        'cache'    => 3600
+    ];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $config = get_addon_config('cms');
+        $this->config['pagesize'] = $config['sitemappagesize'] ?? 5000;
+        $this->config['cache'] = $config['sitemapcachelifetime'] ?? 3600;
+        $this->config['cache'] = intval($this->config['cache']) < 0 ? false : $this->config['cache'];
+        Config::set('default_return_type', 'xml');
+    }
+
+    /**
+     * Sitemap集合
+     */
+    public function index()
+    {
+        $pagesize = $this->request->param('pagesize/d', $this->config['pagesize']);
+        $type = $this->request->param('type', '');
+        $type = str_replace('.xml', '', $type);
+        $type = $type === 'all' ? '' : $type;
+        $list = [];
+        $pagesizeStr = $pagesize != $this->config['pagesize'] ? "pagesize/{$pagesize}/" : '';
+        if (!$type || $type == 'channel') {
+            $path = "/addons/cms/sitemap/channels/{$pagesizeStr}page/[PAGE]";
+            $channelsList = \addons\cms\model\Channel::where('status', 'normal')->field('id,name,diyname,createtime')->paginate($pagesize, false, ['path' => $path]);
+            $lastPage = $channelsList->lastPage();
+            foreach ($channelsList->getUrlRange(1, $lastPage) as $index => $item) {
+                $list[] = ['loc' => url($item, '', 'xml', true)];
+            }
+        }
+        if (!$type || $type == 'archives') {
+            $path = "/addons/cms/sitemap/archives/{$pagesizeStr}page/[PAGE]";
+            $archivesList = \addons\cms\model\Archives::with(['channel'])->where('status', 'normal')->field('id,channel_id,diyname,createtime,publishtime')->paginate($pagesize, false, ['path' => $path]);
+            $lastPage = $archivesList->lastPage();
+            foreach ($archivesList->getUrlRange(1, $lastPage) as $index => $item) {
+                $list[] = ['loc' => url($item, '', 'xml', true)];
+            }
+        }
+        if (!$type || $type == 'tags') {
+            $path = "/addons/cms/sitemap/tags/{$pagesizeStr}page/[PAGE]";
+            $tagsList = \addons\cms\model\Tag::where('status', 'normal')->field('id,name')->paginate($pagesize, false, ['path' => $path]);
+            $lastPage = $tagsList->lastPage();
+            foreach ($tagsList->getUrlRange(1, $lastPage) as $index => $item) {
+                $list[] = ['loc' => url($item, '', 'xml', true)];
+            }
+        }
+        if (!$type || $type == 'users') {
+            $path = "/addons/cms/sitemap/users/{$pagesizeStr}page/[PAGE]";
+            $usersList = \addons\cms\model\User::where('status', 'normal')->field('id')->paginate($pagesize, false, ['path' => $path]);
+            $lastPage = $usersList->lastPage();
+            foreach ($usersList->getUrlRange(1, $lastPage) as $index => $item) {
+                $list[] = ['loc' => url($item, '', 'xml', true)];
+            }
+        }
+        if (!$type || $type == 'specials') {
+            $path = "/addons/cms/sitemap/specials/{$pagesizeStr}page/[PAGE]";
+            $usersList = \addons\cms\model\Special::where('status', 'normal')->field('id')->paginate($pagesize, false, ['path' => $path]);
+            $lastPage = $usersList->lastPage();
+            foreach ($usersList->getUrlRange(1, $lastPage) as $index => $item) {
+                $list[] = ['loc' => url($item, '', 'xml', true)];
+            }
+        }
+        if (!$type || $type == 'pages') {
+            $path = "/addons/cms/sitemap/pages/{$pagesizeStr}page/[PAGE]";
+            $usersList = \addons\cms\model\Page::where('status', 'normal')->field('id')->paginate($pagesize, false, ['path' => $path]);
+            $lastPage = $usersList->lastPage();
+            foreach ($usersList->getUrlRange(1, $lastPage) as $index => $item) {
+                $list[] = ['loc' => url($item, '', 'xml', true)];
+            }
+        }
+        if (!$type || $type == 'diyforms') {
+            $path = "/addons/cms/sitemap/diyforms/{$pagesizeStr}page/[PAGE]";
+            $usersList = \addons\cms\model\Diyform::where('status', 'normal')->field('id')->paginate($pagesize, false, ['path' => $path]);
+            $lastPage = $usersList->lastPage();
+            foreach ($usersList->getUrlRange(1, $lastPage) as $index => $item) {
+                $list[] = ['loc' => url($item, '', 'xml', true)];
+            }
+        }
+        $this->options = [
+            'item_key'  => '',
+            'root_node' => 'sitemapindex',
+            'item_node' => 'sitemap',
+            'root_attr' => ''
+        ];
+        return $this->xml($list);
+    }
+
+    /**
+     * 栏目
+     */
+    public function channels()
+    {
+        $pagesize = $this->request->param('pagesize/d', $this->config['pagesize']);
+        $archivesList = \addons\cms\model\Channel::where('status', 'normal')->cache($this->config['cache'])->field('id,name,diyname,createtime')->paginate($pagesize, true);
+        $list = [];
+        foreach ($archivesList as $index => $item) {
+            $list[] = [
+                'loc'      => $item->fullurl,
+                'priority' => 0.6
+            ];
+        }
+        return $this->xml($list);
+    }
+
+    /**
+     * 文章
+     */
+    public function archives()
+    {
+        $pagesize = $this->request->param('pagesize/d', $this->config['pagesize']);
+        $archivesList = \addons\cms\model\Archives::with(['channel'])->where('status', 'normal')->cache($this->config['cache'])->field('id,channel_id,diyname,createtime')->paginate($pagesize, true);
+        $list = [];
+        foreach ($archivesList as $index => $item) {
+            $list[] = [
+                'loc'      => $item->fullurl,
+                'priority' => 0.8
+            ];
+        }
+        return $this->xml($list);
+    }
+
+    /**
+     * 标签
+     */
+    public function tags()
+    {
+        $pagesize = $this->request->param('pagesize/d', $this->config['pagesize']);
+        $tagsList = \addons\cms\model\Tag::where('status', 'normal')->cache($this->config['cache'])->field('id,name')->paginate($pagesize, true);
+        $list = [];
+        foreach ($tagsList as $index => $item) {
+            $list[] = [
+                'loc'      => $item->fullurl,
+                'priority' => 0.6
+            ];
+        }
+        return $this->xml($list);
+    }
+
+    /**
+     * 用户
+     */
+    public function users()
+    {
+        $pagesize = $this->request->param('pagesize/d', $this->config['pagesize']);
+        $userList = \addons\cms\model\User::where('status', 'normal')->cache($this->config['cache'])->field('id')->paginate($pagesize, true);
+        $list = [];
+        foreach ($userList as $index => $item) {
+            $list[] = [
+                'loc'      => $item->fullurl,
+                'priority' => 0.6
+            ];
+        }
+        return $this->xml($list);
+    }
+
+    /**
+     * 专题
+     */
+    public function specials()
+    {
+        $pagesize = $this->request->param('pagesize/d', $this->config['pagesize']);
+        $specialList = \addons\cms\model\Special::where('status', 'normal')->cache($this->config['cache'])->field('id,diyname,createtime')->paginate($pagesize, true);
+        $list = [];
+        foreach ($specialList as $index => $item) {
+            $list[] = [
+                'loc'      => $item->fullurl,
+                'priority' => 0.6
+            ];
+        }
+        return $this->xml($list);
+    }
+
+    /**
+     * 单页
+     */
+    public function pages()
+    {
+        $pagesize = $this->request->param('pagesize/d', $this->config['pagesize']);
+        $specialList = \addons\cms\model\Page::where('status', 'normal')->cache($this->config['cache'])->field('id,diyname,createtime')->paginate($pagesize, true);
+        $list = [];
+        foreach ($specialList as $index => $item) {
+            $list[] = [
+                'loc'      => $item->fullurl,
+                'priority' => 0.6
+            ];
+        }
+        return $this->xml($list);
+    }
+
+    /**
+     * 自定义表单
+     */
+    public function diyforms()
+    {
+        $pagesize = $this->request->param('pagesize/d', $this->config['pagesize']);
+        $specialList = \addons\cms\model\Diyform::where('status', 'normal')->cache($this->config['cache'])->field('id,diyname,createtime')->paginate($pagesize, true);
+        $list = [];
+        foreach ($specialList as $index => $item) {
+            $list[] = [
+                'loc'      => $item->fullurl,
+                'priority' => 0.6
+            ];
+        }
+        return $this->xml($list);
+    }
+
+    /**
+     * 输出XML
+     */
+    protected function xml($data = [])
+    {
+        foreach ($data as $index => &$item) {
+            $item['loc'] = htmlspecialchars($item['loc']);
+        }
+        return Response::create($data, 'xml', 200, [], $this->options);
+    }
+}

+ 100 - 0
addons/cms/controller/Special.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\Service;
+use addons\cms\model\Archives;
+use addons\cms\model\Fields;
+use addons\cms\model\Special as SpecialModel;
+use addons\cms\model\SpiderLog;
+use addons\cms\model\Taggable;
+use think\Config;
+use think\Exception;
+
+/**
+ * 专题控制器
+ * Class Special
+ * @package addons\cms\controller
+ */
+class Special extends Base
+{
+    /**
+     * 专题页面
+     */
+    public function index()
+    {
+        $config = get_addon_config('cms');
+
+        $diyname = $this->request->param('diyname');
+        if ($diyname && !is_numeric($diyname)) {
+            $special = SpecialModel::getByDiyname($diyname);
+        } else {
+            $id = $diyname ? $diyname : $this->request->param('id', '');
+            $special = SpecialModel::get($id);
+        }
+        if (!$special || $special['status'] != 'normal') {
+            $this->error(__('No specified special found'));
+        }
+        $special->setInc("views", 1);
+
+        Service::appendTextAndList('special', 0, $special);
+
+        //模板名称
+        $special['template'] = $special['template'] ? $special['template'] : 'special.html';
+        $template = ($this->request->isAjax() ? '/ajax/' : '/') . $special["template"];
+        $template = preg_replace('/\.html$/', '', $template);
+
+        $pagelistParams = Service::getPagelistParams($template);
+        //分页大小
+        $pagesize = $pagelistParams['pagesize'] ?? 10;
+        //过滤条件
+        $filterPagelist = function ($query) use ($pagelistParams) {
+            if (isset($pagelistParams['condition'])) {
+                $query->where($pagelistParams['condition']);
+            }
+        };
+
+        $archivesList = \addons\cms\model\Archives::with(['channel'])
+            ->where(function ($query) use ($special) {
+                //$query->whereRaw("(`special_ids`!='' AND FIND_IN_SET('{$special->id}', `special_ids`))");
+                //if ($special['tag_ids']) {
+                //    $query->whereOr('id', 'in', function ($query) use ($special) {
+                //        return $query->name("cms_taggable")->where("tag_id", 'in', $special['tag_ids'])->field("archives_id");
+                //    });
+                //}
+                $tableName = (new Taggable)->getTable();
+                $archivesIds = Archives::whereRaw("(`special_ids`!='' AND FIND_IN_SET({$special->id}, `special_ids`))")
+                    ->whereOr("id IN (SELECT archives_id FROM {$tableName} WHERE tag_id IN (" . ($special->tag_ids ? $special->tag_ids : '0') . "))")
+                    ->cache(true)
+                    ->column('id');
+                $archivesIds = array_filter(array_unique($archivesIds));
+                $query->where('id', 'in', $archivesIds);
+            })
+            ->where($filterPagelist)
+            ->where('status', 'normal')
+            ->whereNull('deletetime')
+            ->order('weigh DESC,publishtime DESC')
+            ->paginate($pagesize, $config['pagemode'] == 'simple');
+
+        $this->view->assign("archivesList", $archivesList);
+        $this->view->assign("__PAGELIST__", $archivesList);
+        $this->view->assign("__SPECIAL__", $special);
+
+        $channel = \addons\cms\model\Channel::getChannelByLinktype('special', $special['id']);
+        $this->view->assign("__CHANNEL__", $channel);
+
+        SpiderLog::record('special', $special['id']);
+
+        //设置TKD
+        Config::set('cms.title', isset($special['seotitle']) && $special['seotitle'] ? $special['seotitle'] : $special['title']);
+        Config::set('cms.keywords', $special['keywords']);
+        Config::set('cms.description', $special['description']);
+        Config::set('cms.image', isset($special['image']) && $special['image'] ? cdnurl($special['image'], true) : '');
+
+        if ($this->request->isAjax()) {
+            $this->success("", "", $this->view->fetch($template));
+        }
+        return $this->view->fetch($template);
+    }
+
+}

+ 118 - 0
addons/cms/controller/Tag.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\library\Service;
+use addons\cms\model\Archives;
+use addons\cms\model\SpiderLog;
+use addons\cms\model\Tag as TagModel;
+use addons\cms\model\Taggable;
+use think\Config;
+
+/**
+ * 标签控制器
+ * Class Tag
+ * @package addons\cms\controller
+ */
+class Tag extends Base
+{
+    public function index()
+    {
+        $config = get_addon_config('cms');
+
+        $tag = null;
+        $name = $this->request->param('name');
+        $diyname = $this->request->param('diyname');
+        $name = $name ? $name : $diyname;
+        if ($name && !is_numeric($name)) {
+            $tag = TagModel::getByName($name);
+        } else {
+            $id = $name ? $name : $this->request->param('id', '');
+            $tag = TagModel::get($id);
+        }
+        if (!$tag || $tag['status'] != 'normal') {
+            $this->error(__('No specified tags found'));
+        }
+
+        $filterList = [];
+        $orderList = [];
+
+        $orderby = $this->request->get('orderby', '');
+        $orderway = $this->request->get('orderway', '');
+        $orderway = $orderway && in_array(strtolower($orderway), ['asc', 'desc']) ? $orderway : 'desc';
+
+        $params = [];
+        if ($orderby) {
+            $params['orderby'] = $orderby;
+        }
+        if ($orderway) {
+            $params['orderway'] = $orderway;
+        }
+
+        //默认排序字段
+        $orders = [
+            ['name' => 'default', 'field' => 'weigh', 'title' => __('Default')],
+            ['name' => 'views', 'field' => 'views', 'title' => __('Views')],
+            ['name' => 'id', 'field' => 'id', 'title' => __('Post date')],
+        ];
+
+        //获取排序列表
+        list($orderList, $orderby, $orderway) = Service::getOrderList($orderby, $orderway, $orders, $params);
+
+        //模板名称
+        $template = ($this->request->isAjax() ? '/ajax' : '/') . 'tag';
+
+        $pagelistParams = Service::getPagelistParams($template);
+        //分页大小
+        $pagesize = $pagelistParams['pagesize'] ?? 10;
+        //过滤条件
+        $filterPagelist = function ($query) use ($pagelistParams) {
+            if (isset($pagelistParams['condition'])) {
+                $query->where($pagelistParams['condition']);
+            }
+        };
+
+        $simple = $config['loadmode'] == 'paging' && $config['pagemode'] == 'full' ? false : true;
+        //缓存列表总数
+        if (!$simple && ($config['cachelistcount'] ?? false)) {
+            $simple = Archives::with(['channel'])
+                ->where('status', 'normal')
+                ->where('id', 'in', function ($query) use ($tag) {
+                    return $query->name('cms_taggable')->where('tag_id', $tag['id'])->field('archives_id');
+                })
+                ->where($filterPagelist)
+                ->cache("cms-tag-list-" . $tag['id'], 3600) //总数缓存1小时
+                ->count();
+        }
+
+        $pageList = Archives::with(['channel'])
+            ->where('status', 'normal')
+            ->where('id', 'in', function ($query) use ($tag) {
+                return $query->name('cms_taggable')->where('tag_id', $tag['id'])->field('archives_id');
+            })
+            ->where($filterPagelist)
+            ->order($orderby, $orderway)
+            ->paginate($pagesize, $simple);
+
+        $pageList->appends(array_filter($params));
+        $this->view->assign("__FILTERLIST__", $filterList);
+        $this->view->assign("__ORDERLIST__", $orderList);
+        $this->view->assign("__TAG__", $tag);
+        $this->view->assign("__TAGS__", $tag);
+        $this->view->assign("__PAGELIST__", $pageList);
+
+        if ($this->request->isAjax()) {
+            $this->success("", "", $this->view->fetch($template));
+        }
+
+        SpiderLog::record('tag', $tag['id']);
+
+        //设置TKD
+        Config::set('cms.title', isset($tag['seotitle']) && $tag['seotitle'] ? $tag['seotitle'] : $tag['name']);
+        Config::set('cms.keywords', $tag['keywords']);
+        Config::set('cms.description', $tag['description']);
+        Config::set('cms.image', isset($tag['image']) && $tag['image'] ? cdnurl($tag['image'], true) : '');
+
+        return $this->view->fetch($template);
+    }
+}

+ 79 - 0
addons/cms/controller/User.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace addons\cms\controller;
+
+use addons\cms\model\SpiderLog;
+use think\Config;
+
+/**
+ * 会员个人主页控制器
+ * Class User
+ * @package addons\cms\controller
+ */
+class User extends Base
+{
+
+    public function index()
+    {
+        $config = get_addon_config('cms');
+        if (!$config['userpage']) {
+            $this->error("会员主页功能已关闭");
+        }
+        $user_id = $this->request->param('id');
+        $user = \app\common\model\User::get($user_id);
+        if (!$user) {
+            $this->error("未找到指定会员");
+        }
+        if ($user['status'] == 'hidden') {
+            $this->error("暂时无法浏览");
+        }
+        $pathArr = explode('/', $this->request->pathinfo());
+        $type = end($pathArr);
+        $type = is_numeric($type) || !in_array($type, ['archives', 'comment']) ? 'archives' : $type;
+
+        $pagesize = 10;
+        $page = $this->request->get('page/d', 1);
+        $page = max(1, $page);
+        if ($type == 'archives') {
+            $archivesList = \addons\cms\model\Archives::with(['user', 'channel'])
+                ->where('user_id', $user['id'])
+                ->where('status', 'normal')
+                ->order('id', 'desc')
+                ->paginate($pagesize, $config['pagemode'] == 'simple', ['var_page' => 'page', 'fragment' => '']);
+            $this->view->assign('archivesList', $archivesList);
+            $this->view->assign('__PAGELIST__', $archivesList);
+        } else {
+            $commentList = \addons\cms\model\Comment::with(['user'])
+                ->where('user_id', $user['id'])
+                ->where('status', 'normal')
+                ->order('id', 'desc')
+                ->paginate($pagesize, $config['pagemode'] == 'simple', ['var_page' => 'page', 'fragment' => '']);
+            $collection = $commentList->getCollection();
+            load_relation($collection, 'source');
+            $this->view->assign('commentList', $commentList);
+            $this->view->assign('__PAGELIST__', $commentList);
+        }
+
+        $title = $type == 'archives' ? '的文章' : '的评论';
+        Config::set('cms.title', $user['nickname'] . $title);
+        Config::set('cms.image', isset($user['avatar']) && $user['avatar'] ? cdnurl($user['avatar'], true) : '');
+
+        SpiderLog::record('user', $user['id']);
+
+        $statistics = [
+            'archives' => \addons\cms\model\Archives::where('user_id', $user['id'])->where('status', 'normal')->count(),
+            'comments' => \addons\cms\model\Comment::where('user_id', $user['id'])->where('status', 'normal')->count(),
+        ];
+        $this->view->assign('statistics', $statistics);
+        $this->view->assign('title', ($this->auth->id == $user['id'] ? '我' : 'TA') . $title);
+        $this->view->assign('page', $page);
+        $this->view->assign('type', $type);
+        $this->view->assign('__USER__', $user);
+
+        if ($this->request->isAjax()) {
+            $this->success("", "", $this->view->fetch('ajax/user'));
+        }
+        return $this->view->fetch("/user");
+    }
+
+}

+ 246 - 0
addons/cms/controller/wxapp/Archives.php

@@ -0,0 +1,246 @@
+<?php
+
+namespace addons\cms\controller\wxapp;
+
+use addons\cms\library\Order;
+use addons\cms\library\OrderException;
+use addons\cms\library\Service;
+use addons\cms\model\Archives as ArchivesModel;
+use addons\cms\model\Channel;
+use addons\cms\model\Comment;
+use addons\cms\model\Fields;
+use addons\cms\model\Modelx;
+use addons\epay\library\Collection;
+use addons\third\model\Third;
+use think\Exception;
+
+/**
+ * 文档
+ */
+class Archives extends Base
+{
+    protected $noNeedLogin = ['index', 'detail'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        //检测ID是否加密
+        $this->hashids();
+    }
+
+    /**
+     * 读取文档列表
+     */
+    public function index()
+    {
+        $config = get_addon_config('cms');
+        $params = [];
+        $model = (int)$this->request->request('model');
+        $channel = (int)$this->request->request('channel');
+        $page = (int)$this->request->request('page');
+
+        if ($model) {
+            $params['model'] = $model;
+        }
+        if ($channel) {
+            $params['channel'] = $channel;
+        }
+        $page = max(1, $page);
+        $params['limit'] = ($page - 1) * 10 . ',10';
+        $params['cache'] = 0;
+
+        if ($channel) {
+            $channelInfo = Channel::get($channel);
+            if ($channelInfo) {
+                $channelIds = Channel::where(function ($query) use ($channelInfo) {
+                    if ($channelInfo['listtype'] <= 2) {
+                        $query->whereOr("id", $channelInfo['id']);
+                    }
+                    if ($channelInfo['listtype'] == 1 || $channelInfo['listtype'] == 3) {
+                        $query->whereOr('id', 'in', function ($query) use ($channelInfo) {
+                            $query->name("cms_channel")->where('parent_id', $channelInfo['id'])->field("id");
+                        });
+                    }
+                    if ($channelInfo['listtype'] == 0 || $channelInfo['listtype'] == 4) {
+                        $childrenIds = \addons\cms\model\Channel::getChannelChildrenIds($channelInfo['id'], false);
+                        if ($childrenIds) {
+                            $query->whereOr('id', 'in', $childrenIds);
+                        }
+                    }
+                })->column('id');
+                $params['channel'] = $channelIds;
+            }
+        }
+        $list = ArchivesModel::getArchivesList($params);
+
+        foreach ($list as $index => $item) {
+            if ($item->channel) {
+                $item->channel->visible(explode(',', 'id,parent_id,name,image,diyname,items'));
+            }
+            $item->id = $config['archiveshashids'] ? $item->eid : $item->id;
+        }
+        $list = collection($list)->toArray();
+        foreach ($list as $index => &$item) {
+            $item['url'] = $item['fullurl'];
+            //小程序只显示3张图
+            $item['images_list'] = array_slice(array_filter(explode(',', $item['images'])), 0, 3);
+            unset($item['imglink'], $item['textlink'], $item['channellink'], $item['taglist'], $item['weigh'], $item['status'], $item['deletetime'], $item['memo'], $item['img'], $item['admin_id']);
+        }
+        $this->success('', ['archivesList' => $list]);
+    }
+
+    /**
+     * 文档详情
+     */
+    public function detail()
+    {
+        $action = $this->request->post("action");
+        if ($action && $this->request->isPost()) {
+            return $this->$action();
+        }
+        $diyname = $this->request->param('diyname');
+        if ($diyname && !is_numeric($diyname)) {
+            $archives = ArchivesModel::getByDiyname($diyname);
+        } else {
+            $id = $this->request->param('id', '');
+            $archives = ArchivesModel::get($id);
+        }
+        if (!$archives || ($archives['status'] != 'normal' && (!$archives['user_id'] || $archives['user_id'] != $this->auth->id)) || $archives['deletetime']) {
+            $this->error(__('No specified article found'));
+        }
+        if (!$this->auth->id && !$archives['isguest']) {
+            $this->error(__('Please login first'));
+        }
+        $channel = Channel::get($archives['channel_id']);
+        if (!$channel) {
+            $this->error(__('No specified channel found'));
+        }
+        $model = Modelx::get($channel['model_id']);
+        if (!$model) {
+            $this->error(__('No specified model found'));
+        }
+        $archives->setInc("views", 1);
+        $addon = db($model['table'])->where('id', $archives['id'])->find();
+        if ($addon) {
+            if ($model->fields) {
+                Service::appendTextAndList('model', $model->id, $addon);
+            }
+            $archives->setData($addon);
+        } else {
+            $this->error(__('No specified article addon found'));
+        }
+
+        //小程序付费阅读将不可见
+        $content = $archives->content;
+        if ($archives->is_paid_part_of_content || $archives->ispaid) {
+            $value = $archives->getData('content');
+            $pattern = '/<paid>(.*?)<\/paid>/is';
+            if (preg_match($pattern, $value) && !$archives->ispaid) {
+                $value = preg_replace($pattern, "<div class='alert alert-warning' style='background:#fcf8e3;border:1px solid #faf3cd;color:#c09853;padding:8px;'>付费内容已经隐藏,请付费后查看</div>", $value);
+            }
+            $content = $value;
+        } else {
+            if (!$archives->ispaid) {
+                if (isset($channel['vip']) && $channel['vip'] > $this->auth->vip) {
+                    $paytips = "此文章为付费文章,需要VIP {$channel['vip']}" . ($archives->price > 0 ? "或支付¥{$archives->price}元" : "") . "才能查看";
+                } else {
+                    $paytips = "此文章为付费文章,需要支付¥{$archives->price}元才能查看";
+                }
+                $content = "<div class='alert alert-warning alert-paid' style='background:#fcf8e3;border:1px solid #faf3cd;color:#c09853;padding:8px;'>{$paytips}</div>";
+            }
+        }
+        if (isset($archives['downloadurl'])) {
+            //$archives['downloadurl'] = is_array($archives['downloadurl']) ? $archives['downloadurl'] : (array)json_decode($archives['downloadurl'], true);
+            $archives['downloadurl'] = $archives['downloadurl_list'];
+            unset($archives['downloadurl_text']);
+        }
+        if (!$archives->ispaid && isset($archives['downloadurl'])) {
+            $archives['downloadurl'] = [];
+        }
+
+        //小程序不支持内容页分页
+        $content = str_replace("##pagebreak##", "<br>", $content);
+        $archives->content = $content;
+        $archives->id = $archives->eid;
+        $archives->hidden(['admin_id', 'deletetime', 'status', 'memo', 'weigh', 'special_ids', 'channel']);
+
+        $commentList = Comment::getCommentList(['aid' => $archives['id']]);
+        $commentList = $commentList->getCollection();
+        foreach ($commentList as $index => &$item) {
+            if ($item->user) {
+                $item->user->avatar = cdnurl($item->user->avatar, true);
+                $item->user->visible(explode(',', 'id,nickname,avatar,bio'));
+            }
+            $item->hidden(['ip', 'useragent', 'deletetime', 'aid', 'subscribe', 'status', 'type', 'updatetime']);
+        }
+        $this->request->token();
+        $channel = $channel->toArray();
+        $channel['url'] = $channel['fullurl'];
+        unset($channel['channeltpl'], $channel['listtpl'], $channel['showtpl'], $channel['status'], $channel['weigh'], $channel['parent_id']);
+        $this->success('', ['archivesInfo' => $archives, 'channelInfo' => $channel, 'commentList' => $commentList]);
+    }
+
+    /**
+     * 赞与踩
+     */
+    public function vote()
+    {
+        $id = (int)$this->request->post("id");
+        $type = trim($this->request->post("type", ""));
+        if (!$id || !$type) {
+            $this->error(__('Operation failed'));
+        }
+        $archives = ArchivesModel::get($id);
+        if (!$archives || ($archives['user_id'] != $this->auth->id && $archives['status'] != 'normal') || $archives['deletetime']) {
+            $this->error(__('No specified article found'));
+        }
+        $archives->where('id', $id)->setInc($type === 'like' ? 'likes' : 'dislikes', 1);
+        $archives = ArchivesModel::get($id);
+        $this->success(__('Operation completed'), ['likes' => $archives->likes, 'dislikes' => $archives->dislikes, 'likeratio' => $archives->likeratio]);
+    }
+
+    /**
+     * 提交订单
+     */
+    public function order()
+    {
+        $id = $this->request->post('id/d');
+        $third = Third::where('platform', 'wechat')->where('apptype', 'miniapp')->where('user_id', $this->auth->id)->find();
+        if (!$third) {
+            $this->error("未找到登录用户信息");
+        }
+        $openid = $third['openid'];
+        $archives = \addons\cms\model\Archives::get($id);
+        if (!$archives) {
+            $this->error("文档未找到");
+        }
+        //优先使用余额的方式发起支付
+        $paid = false;
+        try {
+            $response = Order::submit($id, 'balance');
+        } catch (OrderException $e) {
+            if ($e->getCode() == 1) {
+                $paid = true;
+            }
+        } catch (Exception $e) {
+            $this->error($e->getMessage());
+        }
+        if ($paid) {
+            $this->success("余额支付成功", null);
+        }
+
+        //以微信小程序应用内支付的方式发起支付
+        try {
+            $response = Order::submit($id, 'wechat', 'miniapp', $openid);
+        } catch (Exception $e) {
+            $this->error($e->getMessage());
+        }
+
+        //如果是Collection则需要转换为数组
+        $data = $response instanceof Collection ? $response->toArray() : (string)$response;
+
+        $this->success("请求成功", $data);
+
+        return;
+    }
+}

+ 57 - 0
addons/cms/controller/wxapp/Base.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace addons\cms\controller\wxapp;
+
+use addons\cms\library\IntCode;
+use app\common\controller\Api;
+use app\common\library\Auth;
+use think\Config;
+use think\Lang;
+
+class Base extends Api
+{
+    protected $noNeedLogin = [];
+    protected $noNeedRight = ['*'];
+    //设置返回的会员字段
+    protected $allowFields = ['id', 'username', 'nickname', 'mobile', 'avatar', 'score', 'vip', 'level', 'bio', 'balance', 'money'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+
+        Config::set('default_return_type', 'json');
+
+        $config = get_addon_config('cms');
+        Auth::instance()->setAllowFields($this->allowFields);
+
+        //判断站点状态
+        if (isset($config['openedsite']) && !in_array('wxapp', explode(',', $config['openedsite']))) {
+            $this->error('站点已关闭');
+        }
+
+        //这里手动载入语言包
+        Lang::load(ROOT_PATH . '/addons/cms/lang/zh-cn.php');
+        Lang::load(APP_PATH . '/index/lang/zh-cn/user.php');
+    }
+
+    /**
+     * 判断ID是否加密处理
+     */
+    protected function hashids($name = 'id')
+    {
+        $config = get_addon_config('cms');
+        $getValue = $this->request->get($name);
+        $postValue = $this->request->post($name);
+        if ($config['archiveshashids'] && ($getValue || $postValue)) {
+            if ($getValue) {
+                $getValue = (int)IntCode::decode($getValue);
+                $this->request->get([$name => $getValue]);
+            }
+            if ($postValue) {
+                $postValue = (int)IntCode::decode($postValue);
+                $this->request->post([$name => $postValue]);
+            }
+            $this->request->param('');
+        }
+    }
+}

+ 63 - 0
addons/cms/controller/wxapp/Comment.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace addons\cms\controller\wxapp;
+
+use addons\cms\library\CommentException;
+use think\Config;
+use think\Exception;
+
+/**
+ * 评论
+ */
+class Comment extends Base
+{
+    protected $noNeedLogin = ['index'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $type = $this->request->post("type", "archives");
+        if ($type == 'archives') {
+            //检测ID是否加密
+            $this->hashids('aid');
+        }
+    }
+
+    /**
+     * 评论列表
+     */
+    public function index()
+    {
+        $aid = (int)$this->request->post('aid');
+        $page = (int)$this->request->post('page');
+        Config::set('paginate.page', $page);
+        $commentList = \addons\cms\model\Comment::getCommentList(['aid' => $aid]);
+        $commentList = $commentList->getCollection();
+        foreach ($commentList as $index => $item) {
+            if ($item->user) {
+                $item->user->avatar = cdnurl($item->user->avatar, true);
+                $item->user->visible(explode(',', 'id,nickname,avatar,bio'));
+            }
+            $item->hidden(['ip', 'useragent', 'deletetime', 'aid', 'subscribe', 'status', 'type', 'updatetime']);
+        }
+        $this->success('', ['commentList' => $commentList]);
+    }
+
+    /**
+     * 发表评论
+     */
+    public function post()
+    {
+        try {
+            $params = $this->request->post();
+            $comment = \addons\cms\model\Comment::postComment($params);
+            $comment->user->visible(explode(',', 'id,nickname,avatar,email'));
+            $comment->user->avatar = cdnurl($comment->user->avatar, true);
+        } catch (CommentException $e) {
+            $this->success($e->getMessage(), ['token' => $this->request->token()]);
+        } catch (Exception $e) {
+            $this->error($e->getMessage(), ['token' => $this->request->token()]);
+        }
+        $this->success(__('评论成功'), ['token' => $this->request->token()]);
+    }
+}

+ 79 - 0
addons/cms/controller/wxapp/Common.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace addons\cms\controller\wxapp;
+
+use addons\cms\model\Block;
+use addons\cms\model\Channel;
+use think\Config;
+use think\Hook;
+
+/**
+ * 公共
+ */
+class Common extends Base
+{
+    protected $noNeedLogin = '*';
+
+    /**
+     * 初始化
+     */
+    public function init()
+    {
+        //焦点图
+        $bannerList = [];
+        $list = Block::getBlockList(['name' => 'wxappfocus', 'row' => 5]);
+        foreach ($list as $index => $item) {
+            $bannerList[] = ['image' => cdnurl($item['image'], true), 'url' => $item['url'], 'title' => $item['title']];
+        }
+
+        //首页Tab列表
+        $indexTabList = $newsTabList = $productTabList = [['id' => 0, 'title' => '全部']];
+        $channelList = Channel::where('status', 'normal')
+            ->where("FIND_IN_SET('recommend', flag)")
+            ->where('type', 'in', ['list'])
+            ->field('id,parent_id,model_id,name,diyname')
+            ->order('weigh desc,id desc')
+            ->select();
+        foreach ($channelList as $index => $item) {
+            $data = ['id' => $item['id'], 'title' => $item['name']];
+            $indexTabList[] = $data;
+            if ($item['model_id'] == 1) {
+                $newsTabList[] = $data;
+            }
+            if ($item['model_id'] == 2) {
+                $productTabList[] = $data;
+            }
+        }
+
+        //配置信息
+        $upload = Config::get('upload');
+
+        //如果非服务端中转模式需要修改为中转
+        if ($upload['storage'] != 'local' && isset($upload['uploadmode']) && $upload['uploadmode'] != 'server') {
+            //临时修改上传模式为服务端中转
+            set_addon_config($upload['storage'], ["uploadmode" => "server"], false);
+
+            $upload = \app\common\model\Config::upload();
+            // 上传信息配置后
+            Hook::listen("upload_config_init", $upload);
+
+            $upload = Config::set('upload', array_merge(Config::get('upload'), $upload));
+        }
+
+        $upload['cdnurl'] = $upload['cdnurl'] ? $upload['cdnurl'] : cdnurl('', true);
+        $upload['uploadurl'] = preg_match("/^((?:[a-z]+:)?\/\/)(.*)/i", $upload['uploadurl']) ? $upload['uploadurl'] : url($upload['storage'] == 'local' ? '/api/common/upload' : $upload['uploadurl'], '', false, true);
+
+        $config = [
+            'upload' => $upload
+        ];
+
+        $data = [
+            'bannerList'     => $bannerList,
+            'indexTabList'   => $indexTabList,
+            'newsTabList'    => $newsTabList,
+            'productTabList' => $productTabList,
+            'config'         => $config
+        ];
+        $this->success('', $data);
+    }
+}

+ 34 - 0
addons/cms/controller/wxapp/Index.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace addons\cms\controller\wxapp;
+
+use addons\cms\model\Archives;
+use addons\cms\model\Block;
+use addons\cms\model\Channel;
+
+/**
+ * 首页
+ */
+class Index extends Base
+{
+    protected $noNeedLogin = '*';
+
+    /**
+     * 首页
+     */
+    public function index()
+    {
+        $archivesList = Archives::getArchivesList(['cache' => false]);
+        $archivesList = collection($archivesList)->toArray();
+        foreach ($archivesList as $index => &$item) {
+            $item['url'] = $item['fullurl'];
+            //小程序只显示3张图
+            $item['images_list'] = array_slice(array_filter(explode(',', $item['images'])), 0, 3);
+            unset($item['imglink'], $item['textlink'], $item['channellink'], $item['taglist'], $item['weigh'], $item['status'], $item['deletetime'], $item['memo'], $item['img']);
+        }
+        $data = [
+            'archivesList' => $archivesList
+        ];
+        $this->success('', $data);
+    }
+}

+ 55 - 0
addons/cms/controller/wxapp/My.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace addons\cms\controller\wxapp;
+
+use addons\cms\model\Comment;
+use addons\cms\model\Page;
+
+/**
+ * 我的
+ */
+class My extends Base
+{
+    protected $noNeedLogin = ['aboutus'];
+
+    /**
+     * 我发表的评论
+     */
+    public function comment()
+    {
+        $page = (int)$this->request->request('page');
+        $commentList = Comment::
+        with('archives')
+            ->where(['user_id' => $this->auth->id])
+            ->where(['type' => 'archives'])
+            ->where('status', 'normal')
+            ->order('id desc')
+            ->page($page, 10)
+            ->select();
+        foreach ($commentList as $index => &$item) {
+            $item->create_date = human_date($item->createtime);
+            $item->hidden(['ip', 'useragent', 'deletetime', 'aid', 'subscribe', 'status', 'type', 'updatetime']);
+            $item->aid = $item->archives->eid;
+            if ($item->archives) {
+                $item->archives->id = $item['archives']['eid'];
+            }
+        }
+
+        $this->success('', ['commentList' => $commentList]);
+    }
+
+    /**
+     * 关于我们
+     */
+    public function aboutus()
+    {
+        $pageInfo = Page::getByDiyname('aboutus');
+        if (!$pageInfo || $pageInfo['status'] != 'normal') {
+            $this->error(__('单页未找到'));
+        }
+        $pageInfo->image = cdnurl($pageInfo->image, true);
+        $pageInfo->visible(['id', 'title', 'image', 'content', 'createtime']);
+        $pageInfo = $pageInfo->toArray();
+        $this->success('', ['pageInfo' => $pageInfo]);
+    }
+}

+ 249 - 0
addons/cms/controller/wxapp/User.php

@@ -0,0 +1,249 @@
+<?php
+
+namespace addons\cms\controller\wxapp;
+
+use addons\third\library\Service;
+use addons\third\model\Third;
+use app\common\library\Auth;
+use fast\Http;
+use think\Config;
+use think\Validate;
+
+/**
+ * 会员
+ */
+class User extends Base
+{
+    protected $noNeedLogin = ['index', 'login'];
+
+    protected $token = '';
+
+    public function _initialize()
+    {
+        $this->token = $this->request->post('token');
+        if ($this->request->action() == 'login' && $this->token) {
+            $this->request->post(['token' => '']);
+        }
+        parent::_initialize();
+
+        if (!Config::get('fastadmin.usercenter')) {
+            $this->error(__('User center already closed'));
+        }
+    }
+
+
+    /**
+     * 登录
+     */
+    public function login()
+    {
+        if ($this->auth->isLogin()) {
+            $this->success("登录成功", ['userInfo' => $this->getUserInfo()]);
+        }
+        $config = get_addon_config('cms');
+        $code = $this->request->post("code");
+        $rawData = $this->request->post("rawData", '', 'trim');
+        if (!$code || !$rawData) {
+            $this->error("参数不正确");
+        }
+        $third = get_addon_info('third');
+        if (!$third || !$third['state']) {
+            $this->error("请在后台插件管理安装并配置第三方登录插件");
+        }
+        $userInfo = (array)json_decode($rawData, true);
+
+        $params = [
+            'appid'      => $config['wxappid'],
+            'secret'     => $config['wxappsecret'],
+            'js_code'    => $code,
+            'grant_type' => 'authorization_code'
+        ];
+        $result = Http::sendRequest("https://api.weixin.qq.com/sns/jscode2session", $params, 'GET');
+        if ($result['ret']) {
+            $json = (array)json_decode($result['msg'], true);
+            if (isset($json['openid'])) {
+                //如果有传Token
+                if ($this->token) {
+                    $this->auth->init($this->token);
+                    //检测是否登录
+                    if ($this->auth->isLogin()) {
+                        $third = Third::where(['openid' => $json['openid']])
+                            ->where(function ($query) {
+                                $query->where(['platform' => 'wxapp'])
+                                    ->whereOr(['platform' => 'wechat', 'apptype' => 'miniapp']);
+                            })
+                            ->find();
+                        if ($third && $third['user_id'] == $this->auth->id) {
+                            $this->success("登录成功", ['userInfo' => $this->getUserInfo()]);
+                        }
+                    }
+                }
+
+                $platform = 'wechat';
+                $result = [
+                    'openid'        => $json['openid'],
+                    'unionid'       => $json['unionid'] ?? '',
+                    'userinfo'      => [
+                        'nickname' => $userInfo['nickName'],
+                    ],
+                    'access_token'  => $json['session_key'],
+                    'refresh_token' => '',
+                    'expires_in'    => isset($json['expires_in']) ? $json['expires_in'] : 0,
+                    'apptype'       => 'miniapp'
+                ];
+                $extend = ['gender' => $userInfo['gender'], 'nickname' => $userInfo['nickName'], 'avatar' => $userInfo['avatarUrl']];
+                $ret = Service::connect($platform, $result, $extend);
+                if ($ret) {
+                    $this->success("登录成功", ['userInfo' => $this->getUserInfo()]);
+                } else {
+                    $this->error("连接失败");
+                }
+            } else {
+                $this->error("登录失败");
+            }
+        }
+
+        return;
+    }
+
+    /**
+     * 绑定账号
+     */
+    public function bind()
+    {
+        $account = $this->request->post("account");
+        $password = $this->request->post("password");
+        if (!$account || !$password) {
+            $this->error("参数不正确");
+        }
+
+        $account = $this->request->post('account');
+        $password = $this->request->post('password');
+        $rule = [
+            'account'  => 'require|length:3,50',
+            'password' => 'require|length:6,30',
+        ];
+
+        $msg = [
+            'account.require'  => 'Account can not be empty',
+            'account.length'   => 'Account must be 3 to 50 characters',
+            'password.require' => 'Password can not be empty',
+            'password.length'  => 'Password must be 6 to 30 characters',
+        ];
+        $data = [
+            'account'  => $account,
+            'password' => $password,
+        ];
+        $validate = new Validate($rule, $msg);
+        $result = $validate->check($data);
+        if (!$result) {
+            $this->error(__($validate->getError()));
+            return false;
+        }
+        $field = Validate::is($account, 'email') ? 'email' : (Validate::regex($account, '/^1\d{10}$/') ? 'mobile' : 'username');
+        $user = \app\common\model\User::get([$field => $account]);
+        if (!$user) {
+            $this->error('账号未找到');
+        }
+        $third = Third::where(['user_id' => $user->id])
+            ->where(function ($query) {
+                $query->where(['platform' => 'wxapp'])
+                    ->whereOr(['platform' => 'wechat', 'apptype' => 'miniapp']);
+            })
+            ->find();
+        if ($third) {
+            $this->error('账号已经绑定其他小程序账号');
+        }
+
+        $third = Third::where(['user_id' => $this->auth->id])
+            ->where(function ($query) {
+                $query->where(['platform' => 'wxapp'])
+                    ->whereOr(['platform' => 'wechat', 'apptype' => 'miniapp']);
+            })
+            ->find();
+        if (!$third) {
+            $this->error('未找到登录信息');
+        }
+
+        if ($this->auth->login($account, $password)) {
+            $third->user_id = $this->auth->id;
+            $third->save();
+            $this->success("绑定成功", ['userInfo' => $this->getUserInfo()]);
+        } else {
+            $this->error($this->auth->getError());
+        }
+    }
+
+    /**
+     * 个人资料
+     */
+    public function profile()
+    {
+        $user = $this->auth->getUser();
+        $username = $this->request->post('username');
+        $nickname = $this->request->post('nickname');
+        $bio = $this->request->post('bio');
+        $avatar = $this->request->post('avatar');
+        if (!$username || !$nickname) {
+            $this->error("用户名和昵称不能为空");
+        }
+        $exists = \app\common\model\User::where('username', $username)->where('id', '<>', $this->auth->id)->find();
+        if ($exists) {
+            $this->error(__('Username already exists'));
+        }
+        $avatar = str_replace(cdnurl('', true), '', $avatar);
+        $user->username = $username;
+        $user->nickname = $nickname;
+        $user->bio = $bio;
+        $user->avatar = $avatar;
+        $user->save();
+        $this->success('', ['userInfo' => $this->getUserInfo()]);
+    }
+
+    /**
+     * 保存头像
+     */
+    public function avatar()
+    {
+        $user = $this->auth->getUser();
+        $avatar = $this->request->post('avatar');
+        if (!$avatar) {
+            $this->error("头像不能为空");
+        }
+        $avatar = str_replace(cdnurl('', true), '', $avatar);
+        $user->avatar = $avatar;
+        $user->save();
+        $this->success('', ['userInfo' => $this->getUserInfo()]);
+    }
+
+    /**
+     * 退出登录
+     */
+    public function logout()
+    {
+        $this->auth->logout();
+        $this->success();
+    }
+
+    /**
+     * 获取用户信息
+     * @return array
+     */
+    protected function getUserInfo()
+    {
+        $userinfo = $this->auth->getUserInfo();
+        $userinfo['avatar'] = cdnurl($userinfo['avatar'], true);
+        $vip = get_addon_info('vip');
+        $userinfo['is_install_vip'] = ($vip && $vip['state']);
+        if (!$userinfo['is_install_vip']) {//禁用
+            $userinfo['vip'] = 0;
+            $userinfo['vipInfo'] = null;
+        } else {
+            $userinfo['vipInfo'] = \addons\vip\library\Service::getVipInfo($userinfo['id']) ?? null;
+            if (empty($userinfo['vipInfo'])) {
+                $userinfo['vip'] = 0;
+            }
+        }
+        return $userinfo;
+    }
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
addons/cms/data/dict.json


+ 1 - 0
addons/cms/data/dict.txt

@@ -0,0 +1 @@
+请从以下地址下载更新词库:https://raw.githubusercontent.com/lizhichao/VicWord/master/Data/dict.txt

+ 274 - 0
addons/cms/data/menu.php

@@ -0,0 +1,274 @@
+<?php
+
+$menu = [
+    [
+        'name'    => 'cms',
+        'title'   => 'CMS管理',
+        'sublist' => [
+            [
+                'name'    => 'cms/config',
+                'title'   => '站点配置',
+                'icon'    => 'fa fa-gears',
+                'ismenu'  => 1,
+                'weigh'   => '22',
+                'sublist' => [
+                    ['name' => 'cms/config/index', 'title' => '修改'],
+                ],
+            ],
+            [
+                'name'    => 'cms/statistics',
+                'title'   => '统计控制台',
+                'icon'    => 'fa fa-bar-chart',
+                'ismenu'  => 1,
+                'weigh'   => '21',
+                'sublist' => [
+                    ['name' => 'cms/statistics/index', 'title' => '查看'],
+                ],
+            ],
+            [
+                'name'    => 'cms/channel',
+                'title'   => '栏目管理',
+                'icon'    => 'fa fa-list',
+                'weigh'   => '20',
+                'sublist' => [
+                    ['name' => 'cms/channel/index', 'title' => '查看'],
+                    ['name' => 'cms/channel/add', 'title' => '添加'],
+                    ['name' => 'cms/channel/edit', 'title' => '修改'],
+                    ['name' => 'cms/channel/del', 'title' => '删除'],
+                    ['name' => 'cms/channel/multi', 'title' => '批量更新'],
+                    ['name' => 'cms/channel/admin', 'title' => '栏目授权', 'remark' => '分配管理员可管理的栏目数据,此功能需要先开启站点配置栏目授权开关'],
+                ],
+                'remark'  => '用于管理网站的分类、设置导航分类'
+            ],
+            [
+                'name'    => 'cms/archives',
+                'title'   => '内容管理',
+                'icon'    => 'fa fa-file-text-o',
+                'weigh'   => '19',
+                'sublist' => [
+                    ['name' => 'cms/archives/index', 'title' => '查看'],
+                    ['name' => 'cms/archives/content', 'title' => '副表管理', 'remark' => '用于管理模型副表的数据列表,不建议在此进行删除操作'],
+                    ['name' => 'cms/archives/add', 'title' => '添加'],
+                    ['name' => 'cms/archives/edit', 'title' => '修改'],
+                    ['name' => 'cms/archives/del', 'title' => '删除'],
+                    ['name' => 'cms/archives/multi', 'title' => '批量更新'],
+                    ["name" => "cms/archives/recyclebin", "title" => "回收站"],
+                    ["name" => "cms/archives/restore", "title" => "还原"],
+                    ["name" => "cms/archives/destroy", "title" => "真实删除"],
+                ]
+            ],
+            [
+                'name'    => 'cms/modelx',
+                'title'   => '模型管理',
+                'icon'    => 'fa fa-th',
+                'weigh'   => '18',
+                'sublist' => [
+                    ['name' => 'cms/modelx/index', 'title' => '查看'],
+                    ['name' => 'cms/modelx/add', 'title' => '添加'],
+                    ['name' => 'cms/modelx/edit', 'title' => '修改'],
+                    ['name' => 'cms/modelx/del', 'title' => '删除'],
+                    ['name' => 'cms/modelx/duplicate', 'title' => '复制'],
+                    ['name' => 'cms/modelx/multi', 'title' => '批量更新']
+                ],
+                'remark'  => '在线添加修改删除模型,管理模型字段和相关模型数据'
+            ],
+            [
+                'name'    => 'cms/fields',
+                'title'   => '字段管理',
+                'icon'    => 'fa fa-fields',
+                'ismenu'  => 0,
+                'sublist' => [
+                    ['name' => 'cms/fields/index', 'title' => '查看'],
+                    ['name' => 'cms/fields/add', 'title' => '添加'],
+                    ['name' => 'cms/fields/edit', 'title' => '修改'],
+                    ['name' => 'cms/fields/del', 'title' => '删除'],
+                    ['name' => 'cms/fields/duplicate', 'title' => '复制'],
+                    ['name' => 'cms/fields/multi', 'title' => '批量更新'],
+                ],
+                'remark'  => '用于管理模型或表单的字段,灰色为主表字段无法修改'
+            ],
+            [
+                'name'    => 'cms/tag',
+                'title'   => '标签管理',
+                'icon'    => 'fa fa-tags',
+                'weigh'   => '17',
+                'sublist' => [
+                    ['name' => 'cms/tag/index', 'title' => '查看'],
+                    ['name' => 'cms/tag/add', 'title' => '添加'],
+                    ['name' => 'cms/tag/edit', 'title' => '修改'],
+                    ['name' => 'cms/tag/del', 'title' => '删除'],
+                    ['name' => 'cms/tag/multi', 'title' => '批量更新'],
+                ],
+                'remark'  => '用于管理文章关联的标签,标签的添加在添加文章时自动维护,无需手动添加标签'
+            ],
+            [
+                'name'    => 'cms/block',
+                'title'   => '区块管理',
+                'icon'    => 'fa fa-th-large',
+                'weigh'   => '16',
+                'sublist' => [
+                    ['name' => 'cms/block/index', 'title' => '查看'],
+                    ['name' => 'cms/block/add', 'title' => '添加'],
+                    ['name' => 'cms/block/edit', 'title' => '修改'],
+                    ['name' => 'cms/block/del', 'title' => '删除'],
+                    ['name' => 'cms/block/multi', 'title' => '批量更新'],
+                ],
+                'remark'  => '用于管理站点的自定义区块内容,常用于广告、JS脚本、焦点图、片段代码等'
+            ],
+            [
+                'name'    => 'cms/page',
+                'title'   => '单页管理',
+                'icon'    => 'fa fa-file',
+                'weigh'   => '15',
+                'sublist' => [
+                    ['name' => 'cms/page/index', 'title' => '查看'],
+                    ['name' => 'cms/page/add', 'title' => '添加'],
+                    ['name' => 'cms/page/edit', 'title' => '修改'],
+                    ['name' => 'cms/page/del', 'title' => '删除'],
+                    ['name' => 'cms/page/multi', 'title' => '批量更新'],
+                    ["name" => "cms/page/recyclebin", "title" => "回收站"],
+                    ["name" => "cms/page/restore", "title" => "还原"],
+                    ["name" => "cms/page/destroy", "title" => "真实删除"],
+                ],
+                'remark'  => '用于管理网站的单页面'
+            ],
+            [
+                'name'    => 'cms/search_log',
+                'title'   => '搜索记录管理',
+                'icon'    => 'fa fa-history',
+                'weigh'   => '15',
+                'sublist' => [
+                    ['name' => 'cms/search_log/index', 'title' => '查看'],
+                    ['name' => 'cms/search_log/add', 'title' => '添加'],
+                    ['name' => 'cms/search_log/edit', 'title' => '修改'],
+                    ['name' => 'cms/search_log/del', 'title' => '删除'],
+                    ['name' => 'cms/search_log/multi', 'title' => '批量更新'],
+                ],
+                'remark'  => '用于管理网站的搜索记录日志'
+            ],
+            [
+                'name'    => 'cms/comment',
+                'title'   => '评论管理',
+                'icon'    => 'fa fa-comment',
+                'weigh'   => '14',
+                'sublist' => [
+                    ['name' => 'cms/comment/index', 'title' => '查看'],
+                    ['name' => 'cms/comment/add', 'title' => '添加'],
+                    ['name' => 'cms/comment/edit', 'title' => '修改'],
+                    ['name' => 'cms/comment/del', 'title' => '删除'],
+                    ['name' => 'cms/comment/multi', 'title' => '批量更新'],
+                    ["name" => "cms/comment/recyclebin", "title" => "回收站"],
+                    ["name" => "cms/comment/restore", "title" => "还原"],
+                    ["name" => "cms/comment/destroy", "title" => "真实删除"],
+                ],
+                'remark'  => '用于管理用户在网站上发表的评论'
+            ],
+            [
+                'name'    => 'cms/diyform',
+                'title'   => '自定义表单管理',
+                'icon'    => 'fa fa-list',
+                'weigh'   => '13',
+                'sublist' => [
+                    ['name' => 'cms/diyform/index', 'title' => '查看'],
+                    ['name' => 'cms/diyform/add', 'title' => '添加'],
+                    ['name' => 'cms/diyform/edit', 'title' => '修改'],
+                    ['name' => 'cms/diyform/del', 'title' => '删除'],
+                    ['name' => 'cms/diyform/multi', 'title' => '批量更新'],
+                ],
+                'remark'  => '可在线创建自定义表单,管理表单字段和数据列表'
+            ],
+            [
+                'name'    => 'cms/diydata',
+                'title'   => '自定义表单数据管理',
+                'icon'    => 'fa fa-list',
+                'ismenu'  => 0,
+                'weigh'   => '12',
+                'sublist' => [
+                    ['name' => 'cms/diydata/index', 'title' => '查看'],
+                    ['name' => 'cms/diydata/add', 'title' => '添加'],
+                    ['name' => 'cms/diydata/edit', 'title' => '修改'],
+                    ['name' => 'cms/diydata/del', 'title' => '删除'],
+                    ['name' => 'cms/diydata/multi', 'title' => '批量更新'],
+                    ['name' => 'cms/diydata/import', 'title' => '导入'],
+                ],
+                'remark'  => '可在线管理自定义表单的数据列表'
+            ],
+            [
+                'name'    => 'cms/order',
+                'title'   => '订单管理',
+                'icon'    => 'fa fa-cny',
+                'ismenu'  => 1,
+                'weigh'   => '11',
+                'sublist' => [
+                    ['name' => 'cms/order/index', 'title' => '查看'],
+                    ['name' => 'cms/order/add', 'title' => '添加'],
+                    ['name' => 'cms/order/edit', 'title' => '修改'],
+                    ['name' => 'cms/order/del', 'title' => '删除'],
+                    ['name' => 'cms/order/multi', 'title' => '批量更新'],
+                ],
+                'remark'  => '可在线管理付费查看所产生的订单'
+            ],
+            [
+                'name'    => 'cms/special',
+                'title'   => '专题管理',
+                'icon'    => 'fa fa-newspaper-o',
+                'ismenu'  => 1,
+                'weigh'   => '10',
+                'sublist' => [
+                    ['name' => 'cms/special/index', 'title' => '查看'],
+                    ['name' => 'cms/special/add', 'title' => '添加'],
+                    ['name' => 'cms/special/edit', 'title' => '修改'],
+                    ['name' => 'cms/special/del', 'title' => '删除'],
+                    ['name' => 'cms/special/multi', 'title' => '批量更新'],
+                    ["name" => "cms/special/recyclebin", "title" => "回收站"],
+                    ["name" => "cms/special/restore", "title" => "还原"],
+                    ["name" => "cms/special/destroy", "title" => "真实删除"],
+                ],
+                'remark'  => '可在线管理专题列表'
+            ],
+            [
+                'name'    => 'cms/builder',
+                'title'   => '标签生成器',
+                'icon'    => 'fa fa-code',
+                'ismenu'  => 1,
+                'weigh'   => '10',
+                'sublist' => [
+                    ['name' => 'cms/builder/index', 'title' => '生成'],
+                    ['name' => 'cms/builder/parse', 'title' => '解析'],
+                ],
+                'remark'  => '可在线生成模板标签并进行渲染标签'
+            ],
+            [
+                'name'    => 'cms/autolink',
+                'title'   => '自动链接管理',
+                'icon'    => 'fa fa-link',
+                'ismenu'  => 1,
+                'weigh'   => '11',
+                'sublist' => [
+                    ['name' => 'cms/autolink/index', 'title' => '修改'],
+                    ['name' => 'cms/autolink/add', 'title' => '修改'],
+                    ['name' => 'cms/autolink/edit', 'title' => '修改'],
+                    ['name' => 'cms/autolink/del', 'title' => '修改'],
+                    ['name' => 'cms/autolink/multi', 'title' => '批量更新'],
+                ],
+                'remark'  => '管理文章正文内文本自动链接'
+            ],
+            [
+                'name'    => 'cms/spider_log',
+                'title'   => '搜索引擎来访管理',
+                'icon'    => 'fa fa-search',
+                'ismenu'  => 1,
+                'weigh'   => '14',
+                'sublist' => [
+                    ['name' => 'cms/spider_log/index', 'title' => '修改'],
+                    ['name' => 'cms/spider_log/add', 'title' => '修改'],
+                    ['name' => 'cms/spider_log/edit', 'title' => '修改'],
+                    ['name' => 'cms/spider_log/del', 'title' => '修改'],
+                    ['name' => 'cms/spider_log/multi', 'title' => '批量更新'],
+                ],
+                'remark'  => '可在线管理搜索引擎蜘蛛来访记录'
+            ]
+        ]
+    ]
+];
+return $menu;

+ 1 - 0
addons/cms/data/words.dic

@@ -0,0 +1 @@
+一行一个过滤词,第一行请不要修改

+ 10 - 0
addons/cms/info.ini

@@ -0,0 +1,10 @@
+name = cms
+title = CMS内容管理系统
+intro = 基于ThinkPHP5的内容管理系统
+author = FastAdmin
+website = https://www.fastadmin.net
+version = 1.5.23
+state = 1
+url = /addons/cms
+license = regular
+licenseto = 72930

+ 558 - 0
addons/cms/install.sql

@@ -0,0 +1,558 @@
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_addondownload` (
+  `id` int(10) NOT NULL,
+  `content` longtext NOT NULL,
+  `os` set('windows','linux','mac','ubuntu') DEFAULT '' COMMENT '操作系统',
+  `version` varchar(255) DEFAULT '' COMMENT '最新版本',
+  `filesize` varchar(255) DEFAULT '' COMMENT '文件大小',
+  `language` set('zh-cn','en') DEFAULT '' COMMENT '语言',
+  `downloadurl` varchar(1500) DEFAULT '' COMMENT '下载地址',
+  `screenshots` varchar(1500) DEFAULT '' COMMENT '预览截图',
+  `downloads` varchar(10) DEFAULT '0' COMMENT '下载次数',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='下载';
+
+--
+-- 表的结构 `__PREFIX__cms_addonnews`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_addonnews` (
+  `id` int(10) NOT NULL,
+  `content` longtext NOT NULL,
+  `author` varchar(50) DEFAULT '' COMMENT '作者',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='新闻';
+
+--
+-- 表的结构 `__PREFIX__cms_addonproduct`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_addonproduct` (
+  `id` int(10) NOT NULL,
+  `content` longtext NOT NULL,
+  `productdata` varchar(1500) DEFAULT '' COMMENT '产品列表',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品表';
+
+--
+-- 表的结构 `__PREFIX__cms_archives`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_archives` (
+  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `channel_id` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '栏目ID',
+  `channel_ids` varchar(100) DEFAULT '' COMMENT '副栏目ID集合',
+  `model_id` int(10) NOT NULL DEFAULT '0' COMMENT '模型ID',
+  `special_ids` varchar(100) DEFAULT '' COMMENT '专题ID集合',
+  `admin_id` int(10) unsigned DEFAULT '0' COMMENT '管理员ID',
+  `title` varchar(255) DEFAULT '' COMMENT '文章标题',
+  `flag` varchar(100) DEFAULT '' COMMENT '标志',
+  `style` varchar(100) NULL DEFAULT '' COMMENT '样式',
+  `image` varchar(255) DEFAULT '' COMMENT '缩略图',
+  `images` varchar(1500) DEFAULT '' COMMENT '组图',
+  `seotitle` varchar(255) DEFAULT '' COMMENT 'SEO标题',
+  `keywords` varchar(255) DEFAULT '' COMMENT '关键字',
+  `description` varchar(255) DEFAULT '' COMMENT '描述',
+  `tags` varchar(255) DEFAULT '' COMMENT 'TAG',
+  `price` decimal(10,2) unsigned DEFAULT '0.00' COMMENT '价格',
+  `outlink` varchar(255) DEFAULT '' COMMENT '外部链接',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+  `views` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '浏览次数',
+  `comments` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '评论次数',
+  `likes` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '点赞数',
+  `dislikes` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '点踩数',
+  `diyname` varchar(100) DEFAULT '' COMMENT '自定义URL',
+  `isguest` tinyint(1) unsigned DEFAULT '1' COMMENT '是否访客访问',
+  `iscomment` tinyint(1) unsigned DEFAULT '1' COMMENT '是否允许评论',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `publishtime` bigint(16) DEFAULT NULL COMMENT '发布时间',
+  `deletetime` bigint(16) DEFAULT NULL COMMENT '删除时间',
+  `memo` varchar(100) DEFAULT '' COMMENT '备注',
+  `status` enum('normal','hidden','rejected','pulloff') NOT NULL DEFAULT 'normal' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  KEY `diyname` (`diyname`),
+  KEY `channel_id` (`channel_id`),
+  KEY `channel_ids` (`channel_ids`),
+  KEY `weigh` (`weigh`,`publishtime`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='内容表';
+
+--
+-- 表的结构 `__PREFIX__cms_autolink`
+--
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_autolink` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `title` varchar(100) DEFAULT NULL COMMENT '标题',
+  `url` varchar(255) DEFAULT '' COMMENT '链接',
+  `target` enum('self','blank') DEFAULT 'blank' COMMENT '打开方式',
+  `weigh` int(10) DEFAULT '0' COMMENT '排序',
+  `views` int(10) unsigned DEFAULT '0' COMMENT '点击次数',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `status` enum('normal','hidden') DEFAULT 'normal' COMMENT '状态',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='自动链接表';
+
+--
+-- 表的结构 `__PREFIX__cms_block`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_block` (
+  `id` smallint(8) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `type` varchar(30) DEFAULT '' COMMENT '类型',
+  `name` varchar(50) DEFAULT '' COMMENT '名称',
+  `title` varchar(100) DEFAULT '' COMMENT '标题',
+  `image` varchar(255) DEFAULT '' COMMENT '图片',
+  `url` varchar(255) DEFAULT '' COMMENT '链接',
+  `content` mediumtext COMMENT '内容',
+  `parsetpl` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '解析模板标签',
+  `weigh` int(10) NULL DEFAULT 0 COMMENT '权重',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `begintime` bigint(16) DEFAULT NULL COMMENT '开始时间',
+  `endtime` bigint(16) DEFAULT NULL COMMENT '结束时间',
+  `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='区块表';
+
+--
+-- 表的结构 `__PREFIX__cms_channel`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_channel` (
+  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `type` enum('channel','page','link','list') NOT NULL COMMENT '类型',
+  `model_id` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '模型ID',
+  `parent_id` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '父ID',
+  `name` varchar(30) DEFAULT '' COMMENT '名称',
+  `image` varchar(255) DEFAULT '' COMMENT '图片',
+  `flag` varchar(100) DEFAULT '' COMMENT '标志',
+  `seotitle` varchar(255) DEFAULT '' COMMENT 'SEO标题',
+  `keywords` varchar(255) DEFAULT '' COMMENT '关键字',
+  `description` varchar(255) DEFAULT '' COMMENT '描述',
+  `diyname` varchar(100) DEFAULT '' COMMENT '自定义名称',
+  `outlink` varchar(255) DEFAULT '' COMMENT '外部链接',
+  `linktype` varchar(100) DEFAULT '' COMMENT '链接类型',
+  `linkid` int(10) DEFAULT '0' COMMENT '链接ID',
+  `items` mediumint(8) UNSIGNED NOT NULL DEFAULT '0' COMMENT '文章数量',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+  `channeltpl` varchar(100) DEFAULT '' COMMENT '栏目页模板',
+  `listtpl` varchar(100) DEFAULT '' COMMENT '列表页模板',
+  `showtpl` varchar(100) DEFAULT '' COMMENT '详情页模板',
+  `pagesize` smallint(5) NOT NULL DEFAULT '0' COMMENT '分页大小',
+  `vip` tinyint(1) UNSIGNED NULL DEFAULT '0' COMMENT 'VIP',
+  `listtype` tinyint(1) unsigned DEFAULT '0' COMMENT '列表数据类型',
+  `iscontribute` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否可投稿',
+  `isnav` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否导航显示',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `diyname` (`diyname`),
+  KEY `type` (`type`),
+  KEY `weigh` (`weigh`,`id`),
+  KEY `parent_id` (`parent_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='栏目表';
+
+--
+-- 表的结构 `__PREFIX__cms_channel_admin`
+--
+
+CREATE TABLE `__PREFIX__cms_channel_admin` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID',
+  `channel_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '栏目ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `admin_id` (`admin_id`,`channel_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='栏目权限表';
+
+--
+-- 表的结构 `__PREFIX__cms_collection`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_collection` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `type` enum('archives','special','page','diyform') DEFAULT NULL COMMENT '类型',
+  `aid` int(10) unsigned DEFAULT '0' COMMENT '关联ID',
+  `user_id` int(10) DEFAULT NULL COMMENT '会员ID',
+  `title` varchar(255) DEFAULT NULL COMMENT '收藏标题',
+  `image` varchar(255) DEFAULT NULL COMMENT '图片',
+  `url` varchar(255) DEFAULT NULL COMMENT 'URL',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `aid` (`type`,`aid`,`user_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='收藏表';
+
+--
+-- 表的结构 `__PREFIX__cms_comment`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_comment` (
+  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '会员ID',
+  `type` enum('archives','page', 'special') NOT NULL DEFAULT 'archives' COMMENT '类型',
+  `aid` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '关联ID',
+  `pid` int(10) NOT NULL DEFAULT '0' COMMENT '父ID',
+  `content` longtext COMMENT '内容',
+  `comments` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '评论数',
+  `ip` varchar(50) DEFAULT '' COMMENT 'IP',
+  `useragent` varchar(255) DEFAULT '' COMMENT 'User Agent',
+  `subscribe` tinyint(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '订阅',
+  `createtime` bigint(16) UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间',
+  `updatetime` bigint(16) UNSIGNED NOT NULL DEFAULT '0' COMMENT '更新时间',
+  `deletetime` bigint(16) DEFAULT NULL COMMENT '删除时间',
+  `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  KEY `post_id` (`aid`,`pid`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='评论表';
+
+--
+-- 表的结构 `__PREFIX__cms_diyform`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_diyform`(
+  `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
+  `admin_id` int(10) unsigned DEFAULT '0' COMMENT '管理员ID',
+  `name` char(30) DEFAULT '' COMMENT '表单名称',
+  `title` varchar(100) DEFAULT NULL COMMENT '标题',
+  `seotitle` varchar(255) DEFAULT '' COMMENT 'SEO标题',
+  `posttitle` varchar(255) DEFAULT '' COMMENT '发布标题',
+  `keywords` varchar(100) DEFAULT NULL COMMENT '关键字',
+  `description` varchar(255) DEFAULT NULL COMMENT '描述',
+  `table` varchar(50) DEFAULT '' COMMENT '表名',
+  `fields` text COMMENT '字段列表',
+  `isguest` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否访客访问',
+  `needlogin` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否需要登录发布',
+  `isedit` tinyint(1) unsigned DEFAULT '0' COMMENT '是否允许编辑',
+  `iscaptcha` tinyint(1) unsigned DEFAULT '0' COMMENT '是否启用验证码',
+  `successtips` varchar(255) DEFAULT NULL COMMENT '成功提示文字',
+  `redirecturl` varchar(255) DEFAULT NULL COMMENT '成功后跳转链接',
+  `posttpl` varchar(50) DEFAULT '' COMMENT '表单页模板',
+  `listtpl` varchar(50) DEFAULT '' COMMENT '列表页模板',
+  `showtpl` varchar(50) DEFAULT '' COMMENT '详情页模板',
+  `diyname` varchar(100) DEFAULT NULL COMMENT '自定义名称',
+  `usermode` enum('all','user') DEFAULT 'all' COMMENT '用户筛选模式',
+  `statusmode` enum('all','normal','hidden') DEFAULT 'all' COMMENT '状态筛选模式',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `setting` varchar(1500) DEFAULT NULL COMMENT '表单配置',
+  `status` enum('normal','hidden') DEFAULT 'hidden' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  KEY `diyname` (`diyname`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='自定义表单表';
+
+--
+-- 表的结构 `__PREFIX__cms_fields`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_fields` (
+  `id` smallint(5) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `source` varchar(30) DEFAULT '' COMMENT '来源',
+  `source_id` int(10) NOT NULL DEFAULT '0' COMMENT '来源ID',
+  `name` char(30) DEFAULT '' COMMENT '名称',
+  `type` varchar(30) DEFAULT '' COMMENT '类型',
+  `title` varchar(30) DEFAULT '' COMMENT '标题',
+  `content` text COMMENT '内容',
+  `defaultvalue` varchar(100) DEFAULT '' COMMENT '默认值',
+  `rule` varchar(100) DEFAULT '' COMMENT '验证规则',
+  `msg` varchar(100) DEFAULT '' COMMENT '错误消息',
+  `ok` varchar(100) DEFAULT '' COMMENT '成功消息',
+  `tip` varchar(100) DEFAULT '' COMMENT '提示消息',
+  `decimals` tinyint(1) DEFAULT NULL COMMENT '小数点',
+  `length` mediumint(8) DEFAULT NULL COMMENT '长度',
+  `minimum` smallint(6) DEFAULT NULL COMMENT '最小数量',
+  `maximum` smallint(6) UNSIGNED NOT NULL DEFAULT '0' COMMENT '最大数量',
+  `extend` varchar(255) DEFAULT '' COMMENT '扩展信息',
+  `setting` varchar(1500) DEFAULT '' COMMENT '配置信息',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '排序',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `isorder` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否可排序',
+  `iscontribute` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否可投稿',
+  `isfilter` tinyint(1) NOT NULL DEFAULT '0' COMMENT '筛选',
+  `status` enum('normal','hidden') NOT NULL COMMENT '状态',
+  PRIMARY KEY (`id`),
+  KEY `source` (`source`) USING BTREE,
+  KEY `source_id` (`source_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='模型字段表';
+
+--
+-- 表的结构 `__PREFIX__cms_message`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_message` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) DEFAULT NULL COMMENT '会员ID',
+  `name` varchar(50) DEFAULT '' COMMENT '姓名',
+  `telephone` varchar(100) DEFAULT '' COMMENT '电话',
+  `qq` varchar(30) DEFAULT '' COMMENT 'QQ',
+  `content` longtext COMMENT '内容',
+  `os` enum('windows','mac') DEFAULT 'windows' COMMENT '操作系统',
+  `language` set('zh-cn','en') DEFAULT '' COMMENT '语言',
+  `address` varchar(255) DEFAULT '' COMMENT '地区',
+  `category` varchar(255) DEFAULT '' COMMENT '分类',
+  `memo` varchar(255) DEFAULT '' COMMENT '备注',
+  `image` varchar(500) DEFAULT '' COMMENT '图片',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `status` enum('normal','hidden','rejected') DEFAULT 'normal' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  KEY `user_id` (`user_id`),
+  KEY `createtime` (`createtime`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='站内留言';
+
+--
+-- 表的结构 `__PREFIX__cms_friendlink`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_friendlink` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `user_id` int(10) DEFAULT NULL COMMENT '会员ID',
+  `title` varchar(255) DEFAULT '' COMMENT '站点名称',
+  `image` varchar(1500) DEFAULT '' COMMENT '站点Logo',
+  `website` varchar(100) DEFAULT '' COMMENT '站点链接',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `memo` varchar(1500) DEFAULT '' COMMENT '备注',
+  `status` enum('normal','hidden','rejected') DEFAULT 'hidden' COMMENT '状态',
+  `intro` varchar(255) DEFAULT '' COMMENT '站点介绍',
+  PRIMARY KEY (`id`),
+  KEY `user_id` (`user_id`),
+  KEY `createtime` (`createtime`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='友情链接';
+
+--
+-- 表的结构 `__PREFIX__cms_navigation`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_navigation` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `user_id` int(10) DEFAULT NULL COMMENT '会员ID',
+  `title` varchar(255) DEFAULT '' COMMENT '标题',
+  `image` varchar(255) DEFAULT '' COMMENT '图片',
+  `website` varchar(255) DEFAULT '' COMMENT '导航链接',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `memo` varchar(1500) DEFAULT '' COMMENT '备注',
+  `status` enum('normal','hidden','rejected') DEFAULT 'hidden' COMMENT '状态',
+  `intro` varchar(255) DEFAULT '' COMMENT '介绍',
+  PRIMARY KEY (`id`),
+  KEY `user_id` (`user_id`),
+  KEY `createtime` (`createtime`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='热门导航';
+
+--
+-- 表的结构 `__PREFIX__cms_model`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_model` (
+  `id` smallint(5) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `name` char(30) DEFAULT '' COMMENT '模型名称',
+  `table` char(20) DEFAULT '' COMMENT '表名',
+  `fields` text COMMENT '字段列表',
+  `channeltpl` varchar(100) DEFAULT '' COMMENT '栏目页模板',
+  `listtpl` varchar(100) DEFAULT '' COMMENT '列表页模板',
+  `showtpl` varchar(100) DEFAULT '' COMMENT '详情页模板',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `setting` text COMMENT '模型配置',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='内容模型表';
+
+--
+-- 表的结构 `__PREFIX__cms_order`
+--
+
+CREATE TABLE `__PREFIX__cms_order` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `orderid` varchar(50) DEFAULT '' COMMENT '订单ID',
+  `user_id` int(10) unsigned DEFAULT '0' COMMENT '会员ID',
+  `archives_id` int(10) unsigned DEFAULT '0' COMMENT '文档ID',
+  `title` varchar(100) DEFAULT NULL COMMENT '订单标题',
+  `amount` decimal(10,2) unsigned DEFAULT '0.00' COMMENT '订单金额',
+  `payamount` decimal(10,2) unsigned DEFAULT '0.00' COMMENT '支付金额',
+  `paytype` varchar(50) DEFAULT NULL COMMENT '支付类型',
+  `paytime` bigint(16) DEFAULT NULL COMMENT '支付时间',
+  `method` varchar(100) NULL DEFAULT '' COMMENT '支付方法',
+  `ip` varchar(50) DEFAULT NULL COMMENT 'IP地址',
+  `useragent` varchar(255) DEFAULT NULL COMMENT 'UserAgent',
+  `memo` varchar(255) DEFAULT NULL COMMENT '备注',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `status` enum('created','paid','expired') DEFAULT 'created' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  KEY `archives_id` (`archives_id`),
+  KEY `orderid` (`orderid`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
+
+--
+-- 表的结构 `__PREFIX__cms_page`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_page` (
+  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `category_id` int(10) NOT NULL DEFAULT '0' COMMENT '分类ID',
+  `admin_id` int(10) unsigned DEFAULT '0' COMMENT '管理员ID',
+  `type` varchar(50) DEFAULT '' COMMENT '类型',
+  `title` varchar(50) DEFAULT '' COMMENT '标题',
+  `seotitle` varchar(255) DEFAULT '' COMMENT 'SEO标题',
+  `keywords` varchar(255) DEFAULT '' COMMENT '关键字',
+  `description` varchar(255) DEFAULT '' COMMENT '描述',
+  `flag` varchar(100) DEFAULT '' COMMENT '标志',
+  `image` varchar(255) DEFAULT '' COMMENT '头像',
+  `content` longtext COMMENT '内容',
+  `icon` varchar(50) DEFAULT '' COMMENT '图标',
+  `views` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '点击',
+  `likes` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '点赞',
+  `dislikes` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '点踩',
+  `comments` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '评论',
+  `diyname` varchar(100) DEFAULT '' COMMENT '自定义',
+  `showtpl` varchar(50) DEFAULT '' COMMENT '视图模板',
+  `iscomment` tinyint(1) unsigned DEFAULT '1' COMMENT '是否允许评论',
+  `parsetpl` tinyint(1) UNSIGNED NULL DEFAULT '0' COMMENT '解析模板标签',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `deletetime` bigint(16) DEFAULT NULL COMMENT '删除时间',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+  `status` varchar(30) DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  KEY `diyname` (`diyname`),
+  KEY `type` (`type`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='单页表';
+
+--
+-- 表的结构 `__PREFIX__cms_search_log`
+--
+CREATE TABLE `__PREFIX__cms_search_log` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `keywords` varchar(100) CHARACTER SET utf8mb4 DEFAULT '' COMMENT '关键字',
+  `nums` int(10) unsigned DEFAULT '0' COMMENT '搜索次数',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '搜索时间',
+  `status` varchar(50) DEFAULT 'hidden' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `keywords` (`keywords`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='搜索记录表';
+
+--
+-- 表的结构 `__PREFIX__cms_special`
+--
+
+CREATE TABLE `__PREFIX__cms_special` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `admin_id` int(10) unsigned DEFAULT '0' COMMENT '管理员ID',
+  `title` varchar(100) DEFAULT '' COMMENT '标题',
+  `tag_ids` varchar(1500) NULL DEFAULT '' COMMENT '标签ID集合',
+  `flag` varchar(100) DEFAULT '' COMMENT '标志',
+  `label` varchar(50) DEFAULT '' COMMENT '标签',
+  `image` varchar(255) DEFAULT '' COMMENT '图片',
+  `banner` varchar(255) DEFAULT '' COMMENT 'Banner图片',
+  `diyname` varchar(100) DEFAULT '' COMMENT '自定义名称',
+  `seotitle` varchar(255) DEFAULT '' COMMENT 'SEO标题',
+  `keywords` varchar(100) DEFAULT NULL COMMENT '关键字',
+  `description` varchar(255) DEFAULT NULL COMMENT '描述',
+  `intro` varchar(255) DEFAULT NULL COMMENT '专题介绍',
+  `views` int(10) unsigned DEFAULT '0' COMMENT '浏览次数',
+  `comments` int(10) unsigned DEFAULT '0' COMMENT '评论次数',
+  `iscomment` tinyint(1) unsigned DEFAULT '1' COMMENT '是否允许评论',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `deletetime` bigint(16) DEFAULT NULL COMMENT '删除时间',
+  `template` varchar(100) DEFAULT '' COMMENT '专题模板',
+  `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  KEY `diyname` (`diyname`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='专题表';
+
+--
+-- 表的结构 `__PREFIX__cms_spider_log`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_spider_log` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `type` enum('index','archives','page','special','channel','diyform','tag','user') DEFAULT NULL COMMENT '类型',
+  `aid` int(10) DEFAULT '0' COMMENT '关联ID',
+  `name` varchar(50) DEFAULT '' COMMENT '名称',
+  `url` varchar(255) DEFAULT '' COMMENT '来访页面',
+  `nums` int(10) unsigned DEFAULT '0' COMMENT '来访次数',
+  `firsttime` bigint(16) DEFAULT NULL COMMENT '首次来访时间',
+  `lastdata` varchar(100) DEFAULT '' COMMENT '最后5次来访时间',
+  `lasttime` bigint(16) DEFAULT NULL COMMENT '最后来访时间',
+  PRIMARY KEY (`id`),
+  KEY `type` (`type`,`aid`)
+) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COMMENT='搜索引擎来访记录';
+
+--
+-- 表的结构 `__PREFIX__cms_tag`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_tag` (
+  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `name` varchar(50) DEFAULT '' COMMENT '标签名称',
+  `nums` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '文档数量',
+  `seotitle` varchar(100) DEFAULT '' COMMENT 'SEO标题',
+  `keywords` varchar(255) DEFAULT NULL COMMENT '关键字',
+  `description` varchar(255) DEFAULT NULL COMMENT '描述',
+  `views` int(10) DEFAULT NULL COMMENT '浏览次数',
+  `autolink` tinyint(1) unsigned DEFAULT 0 COMMENT '自动内链',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  `updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
+  `status` enum('normal','hidden') DEFAULT 'normal' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`) USING BTREE,
+  KEY `nums` (`nums`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='标签表';
+
+--
+-- 表的结构 `__PREFIX__cms_taggable`
+--
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__cms_taggable` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `tag_id` int(10) DEFAULT NULL COMMENT '标签ID',
+  `archives_id` int(10) DEFAULT NULL COMMENT '文档ID',
+  `createtime` bigint(16) DEFAULT NULL COMMENT '添加时间',
+  PRIMARY KEY (`id`),
+  KEY `tag_id` (`tag_id`),
+  KEY `archives_id` (`archives_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='标签列表';
+
+-- 1.3.5 --
+ALTER TABLE `__PREFIX__cms_tag` ADD COLUMN `autolink` tinyint(1) unsigned DEFAULT 0 COMMENT '自动内链' AFTER `views`;
+
+-- 1.3.6 --
+ALTER TABLE `__PREFIX__cms_order` ADD COLUMN `method` varchar(100) NULL DEFAULT '' COMMENT '支付方法' AFTER `paytime`;
+ALTER TABLE `__PREFIX__cms_page` ADD COLUMN `parsetpl` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '解析模板标签' AFTER `iscomment`;
+ALTER TABLE `__PREFIX__cms_block` ADD COLUMN `parsetpl` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '解析模板标签' AFTER `content`;
+
+-- 1.3.7 --
+ALTER TABLE `__PREFIX__cms_channel` ADD COLUMN `vip` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT 'VIP' AFTER `pagesize`;
+ALTER TABLE `__PREFIX__cms_channel` ADD COLUMN `listtype` tinyint(1) unsigned DEFAULT '0' COMMENT '列表数据类型' AFTER `vip`;
+
+-- 1.4.0 --
+ALTER TABLE `__PREFIX__cms_archives` ADD COLUMN `price` decimal(10, 2) UNSIGNED NULL DEFAULT 0 COMMENT '价格' AFTER `description`,ADD COLUMN `outlink` varchar(255) NULL DEFAULT '' COMMENT '外部链接' AFTER `price`;
+ALTER TABLE `__PREFIX__cms_diyform` ADD COLUMN `iscaptcha` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '是否启用验证码' AFTER `isedit`;
+ALTER TABLE `__PREFIX__cms_archives` ADD INDEX(`channel_id`), ADD INDEX(`channel_ids`), ADD INDEX(`diyname`);
+ALTER TABLE `__PREFIX__cms_channel` ADD INDEX(`type`);
+ALTER TABLE `__PREFIX__cms_page` ADD INDEX(`diyname`);
+ALTER TABLE `__PREFIX__cms_special` ADD INDEX(`diyname`);
+ALTER TABLE `__PREFIX__cms_order` ADD INDEX(`orderid`);
+ALTER TABLE `__PREFIX__cms_fields` ADD COLUMN `filterlist` text NULL COMMENT '筛选列表' AFTER `content`;
+
+-- 1.4.3 --
+ALTER TABLE `__PREFIX__cms_collection` DROP INDEX `aid`,ADD UNIQUE INDEX `aid`(`type`, `aid`, `user_id`) USING BTREE;
+
+-- 1.4.4 --
+ALTER TABLE `__PREFIX__cms_diyform` ADD COLUMN `isguest` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '是否访客访问' AFTER `fields`,MODIFY COLUMN `needlogin` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否需要登录发布' AFTER `isguest`;
+ALTER TABLE `__PREFIX__cms_block` ADD INDEX(`name`);
+ALTER TABLE `__PREFIX__cms_archives` MODIFY COLUMN `channel_ids` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '副栏目ID集合' AFTER `channel_id`,DROP INDEX `channel`,DROP INDEX `channel_id`,DROP INDEX `channel_ids`,DROP INDEX `status`,ADD INDEX `model_id`(`model_id`, `channel_id`, `channel_ids`) USING BTREE;
+
+-- 1.5.0 --
+ALTER TABLE `__PREFIX__cms_archives` MODIFY COLUMN `special_ids` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '专题IDID集合' AFTER `channel_ids`,DROP INDEX `model_id`,DROP INDEX `weigh`,ADD INDEX `channel_id`(`channel_id`),ADD INDEX `channel_ids`(`channel_ids`),ADD INDEX `weigh`(`weight`, `publishtime`) USING BTREE;
+ALTER TABLE `__PREFIX__cms_channel` ADD COLUMN `linktype` varchar(100) NULL DEFAULT '' COMMENT '链接类型' AFTER `outlink`,ADD COLUMN `linkid` int(10) NULL DEFAULT 0 COMMENT '链接ID' AFTER `linktype`;
+ALTER TABLE `__PREFIX__cms_search_log` ADD COLUMN `createtime` bigint(16) NULL COMMENT '搜索时间' AFTER `nums`,ADD COLUMN `status` varchar(50) NULL DEFAULT 'hidden' COMMENT '状态' AFTER `createtime`;
+
+-- 1.5.4 --
+ALTER TABLE `__PREFIX__cms_diyform` ADD COLUMN `posttitle` varchar(255) NULL DEFAULT '' COMMENT '发布标题' AFTER `seotitle`;
+ALTER TABLE `__PREFIX__cms_fields` MODIFY COLUMN `msg` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '错误消息' AFTER `rule`,MODIFY COLUMN `ok` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '成功消息' AFTER `msg`;

+ 91 - 0
addons/cms/lang/zh-cn.php

@@ -0,0 +1,91 @@
+<?php
+
+return [
+    'Home'                                  => '首页',
+    'Default'                               => '默认',
+    'Views'                                 => '浏览次数',
+    'Post date'                             => '发布日期',
+    'All'                                   => '全部',
+    'Hot news'                              => '热门资讯',
+    'Hot tags'                              => '热门标签',
+    'Recommend news'                        => '推荐资讯',
+    'Recommend download'                    => '推荐下载',
+    'Comments'                              => '评论',
+    'Tags'                                  => '标签',
+    'View more'                             => '查看更多',
+    'View detail'                           => '查看详情',
+    'Month'                                 => '月',
+    'Prev'                                  => '上一篇',
+    'Next'                                  => '下一篇',
+    'Comment list'                          => '评论列表',
+    'Favourite'                             => '收藏',
+    'Share'                                 => '分享',
+    'Report'                                => '举报',
+    'Rejected'                              => '已拒绝',
+    'Error report'                          => '错误反馈',
+    'Recently update'                       => '最新更新',
+    'All categories'                        => '所有分类',
+    'Article category'                      => '本文分类',
+    'Article tags'                          => '本文标签',
+    'Article views'                         => '浏览次数',
+    'Article url'                           => '本文链接',
+    'Search for %s'                         => '查找 “%s”',
+    'Search more %s'                        => '查找更多 “%s”',
+    'Empty'                                 => '暂无内容',
+    'Upload'                                => '上传',
+    'Uploading'                             => '上传中',
+    'Choose'                                => '选择',
+    'Array key'                             => '键',
+    'Array value'                           => '值',
+    'No specified article found'            => '未找到指定的文章',
+    'No specified channel found'            => '未找到指定的栏目',
+    'No specified model found'              => '未找到指定的模型',
+    'No specified tags found'               => '未找到指定的标签',
+    'No specified page found'               => '未找到指定的单页',
+    'No specified special found'            => '未找到指定的专题',
+    'No specified addon article found'      => '未找到指定副表数据',
+    'Operation completed'                   => '操作成功!',
+    'Operation failed'                      => '操作失败!',
+    'Unknown data format'                   => '未知的数据格式!',
+    'Network error'                         => '网络错误!',
+    '%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年前',
+    'Verify email'                          => '邮箱验证',
+    'Change password'                       => '修改密码',
+    'Captcha is incorrect'                  => '验证码不正确',
+    'Logged in successful'                  => '登录成功',
+    'Logout successful'                     => '注销成功',
+    'Invalid parameters'                    => '参数不正确',
+    'Change password failure'               => '修改密码失败',
+    'Change password successful'            => '修改密码成功',
+    'Reset password successful'             => '重置密码成功',
+    'Account is locked'                     => '账户已经被锁定',
+    'Password is incorrect'                 => '密码不正确',
+    'Account is incorrect'                  => '账户不正确',
+    'Account not exist'                     => '账户不存在',
+    'Account can not be empty'              => '账户不能为空',
+    'Username or password is incorrect'     => '用户名或密码不正确',
+    'Sign up successful'                    => '注册成功',
+    'Username can not be empty'             => '用户名不能为空',
+    'Username must be 6 to 30 characters'   => '用户名必须6-30个字符',
+    'Password can not be empty'             => '密码不能为空',
+    'Password must be 6 to 30 characters'   => '密码必须6-30个字符',
+    'Mobile is incorrect'                   => '手机格式不正确',
+    'Username already exist'                => '用户名已经存在',
+    'Nickname already exist'                => '昵称已经存在',
+    'Email already exist'                   => '邮箱已经存在',
+    'Mobile already exist'                  => '手机号已经存在',
+    'Username is incorrect'                 => '用户名不正确',
+    'Email is incorrect'                    => '邮箱不正确',
+    'You are not logged in'                 => '你当前还未登录',
+    'You\'ve logged in, do not login again' => '你已经存在,请不要重复登录',
+    'Become VIP %s to browser'              => '需要VIP %s 才能访问',
+    'Become VIP %s to download'             => '需要VIP %s 才能下载',
+    'User not found'                        => '账号未找到',
+    'Category'                              =>  '分类'
+];

+ 175 - 0
addons/cms/library/Alter.php

@@ -0,0 +1,175 @@
+<?php
+
+namespace addons\cms\library;
+
+class Alter
+{
+    protected static $instance = null;
+    protected $config = [];
+    protected $data = [
+        'table'   => '',
+        'oldname' => '',
+        'name'    => '',
+        'type'    => 'VARCHAR',
+        'length'  => '255',
+        'content' => '',
+        'comment' => '',
+        'after'   => '',
+    ];
+
+    public function __construct($options = [])
+    {
+        $this->options = array_merge($this->config, $options);
+    }
+
+    public static function instance($options = [])
+    {
+        if (is_null(self::$instance)) {
+            self::$instance = new static($options);
+        }
+
+        return self::$instance;
+    }
+
+    public function setTable($table)
+    {
+        $this->data['table'] = db()->name($table)->getTable();
+        return $this;
+    }
+
+    public function setType($type)
+    {
+        switch ($type) {
+            case 'checkbox':
+            case 'selects':
+                $this->data['type'] = 'SET';
+                break;
+            case 'radio':
+            case 'select':
+                $this->data['type'] = 'ENUM';
+                break;
+            case 'number':
+                $this->data['type'] = 'INT';
+                break;
+            case 'date':
+            case 'datetime':
+            case 'time':
+                $this->data['type'] = strtoupper($type);
+                break;
+            case 'editor':
+                $this->data['type'] = 'TEXT';
+                break;
+            default:
+                $this->data['type'] = 'VARCHAR';
+                break;
+        }
+        return $this;
+    }
+
+    public function setOldname($oldname)
+    {
+        $this->data['oldname'] = $oldname;
+        return $this;
+    }
+
+    public function setName($name)
+    {
+        $this->data['name'] = $name;
+        return $this;
+    }
+
+    public function setLength($length)
+    {
+        $this->data['length'] = $length;
+        return $this;
+    }
+
+    public function setContent($content)
+    {
+        $this->data['content'] = $content;
+        return $this;
+    }
+
+    public function setComment($comment)
+    {
+        $this->data['comment'] = $comment;
+        return $this;
+    }
+
+    public function setDefaultvalue($defaultvalue)
+    {
+        $this->data['defaultvalue'] = $defaultvalue;
+        return $this;
+    }
+
+    public function setDecimals($decimals)
+    {
+        $this->data['decimals'] = $decimals;
+        return $this;
+    }
+
+    protected function process()
+    {
+        if ($this->data['type'] == 'INT') {
+            if ($this->data['decimals'] > 0) {
+                $this->data['type'] = 'DECIMAL';
+                $this->data['length'] = "({$this->data['length']},{$this->data['decimals']})";
+            } else {
+                $this->data['length'] = "({$this->data['length']})";
+            }
+            $this->data['defaultvalue'] = $this->data['defaultvalue'] == '' ? 'NULL' : $this->data['defaultvalue'];
+        } elseif (in_array($this->data['type'], ['SET', 'ENUM'])) {
+            $content = \app\common\model\Config::decode($this->data['content']);
+            $this->data['length'] = "('" . implode("','", array_keys($content)) . "')";
+            $this->data['defaultvalue'] = in_array($this->data['defaultvalue'], array_keys($content)) ? $this->data['defaultvalue'] : ($this->data['type'] == 'ENUM' ? key($content) : '');
+        } elseif (in_array($this->data['type'], ['DATE', 'TIME', 'DATETIME'])) {
+            $this->data['length'] = '';
+            $this->data['defaultvalue'] = "NULL";
+        } elseif (in_array($this->data['type'], ['TEXT'])) {
+            $this->data['length'] = "(0)";
+            $this->data['defaultvalue'] = 'NULL';
+        } else {
+            $this->data['length'] = "({$this->data['length']})";
+        }
+        $this->data['defaultvalue'] = strtoupper($this->data['defaultvalue']) === 'NULL' ? "NULL" : "'{$this->data['defaultvalue']}'";
+    }
+
+    /**
+     * 获取添加字段的SQL
+     * @return string
+     */
+    public function getAddSql()
+    {
+        $this->process();
+
+        $sql = "ALTER TABLE `{$this->data['table']}` "
+            . "ADD `{$this->data['name']}` {$this->data['type']} {$this->data['length']} "
+            . "DEFAULT {$this->data['defaultvalue']} "
+            . "COMMENT '{$this->data['comment']}' "
+            . ($this->data['after'] ? "AFTER `{$this->data['after']}`" : '');
+        return $sql;
+    }
+
+    public function getModifySql()
+    {
+        $this->process();
+
+        $sql = "ALTER TABLE `{$this->data['table']}` "
+            . ($this->data['oldname'] ? 'CHANGE' : 'MODIFY') . " COLUMN " . ($this->data['oldname'] ? "`{$this->data['oldname']}`" : '') . " `{$this->data['name']}` {$this->data['type']} {$this->data['length']} "
+            . "DEFAULT {$this->data['defaultvalue']} "
+            . "COMMENT '{$this->data['comment']}' "
+            . ($this->data['after'] ? "AFTER `{$this->data['after']}`" : '');
+        return $sql;
+    }
+
+    /**
+     * 获取删除字段的SQL
+     * @return string
+     */
+    public function getDropSql()
+    {
+        $sql = "ALTER TABLE `{$this->data['table']}` "
+            . "DROP `{$this->data['name']}`";
+        return $sql;
+    }
+}

+ 215 - 0
addons/cms/library/Bootstrap.php

@@ -0,0 +1,215 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2017 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: zhangyajun <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+namespace addons\cms\library;
+
+use think\Paginator;
+
+class Bootstrap extends Paginator
+{
+
+    /**
+     * 上一页按钮
+     * @param string $text
+     * @return string
+     */
+    protected function getPreviousButton($text = "&laquo;")
+    {
+        if ($this->currentPage() <= 1) {
+            return $this->getDisabledTextWrapper($text);
+        }
+
+        $url = $this->url(
+            $this->currentPage() - 1
+        );
+
+        return $this->getPageLinkWrapper($url, $text);
+    }
+
+    /**
+     * 下一页按钮
+     * @param string $text
+     * @return string
+     */
+    protected function getNextButton($text = '&raquo;')
+    {
+        if (!$this->hasMore) {
+            return $this->getDisabledTextWrapper($text);
+        }
+
+        $url = $this->url($this->currentPage() + 1);
+
+        return $this->getPageLinkWrapper($url, $text);
+    }
+
+    /**
+     * 页码按钮
+     * @return string
+     */
+    protected function getLinks()
+    {
+        if ($this->simple) {
+            return '';
+        }
+
+        $block = [
+            'first'  => null,
+            'slider' => null,
+            'last'   => null
+        ];
+
+        $side = 3;
+        $window = $side * 2;
+
+        if ($this->lastPage < $window + 6) {
+            $block['first'] = $this->getUrlRange(1, $this->lastPage);
+        } elseif ($this->currentPage <= $window) {
+            $block['first'] = $this->getUrlRange(1, $window + 2);
+            $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
+        } elseif ($this->currentPage > ($this->lastPage - $window)) {
+            $block['first'] = $this->getUrlRange(1, 2);
+            $block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage);
+        } else {
+            $block['first'] = $this->getUrlRange(1, 2);
+            $block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side);
+            $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
+        }
+
+        $html = '';
+
+        if (is_array($block['first'])) {
+            $html .= $this->getUrlLinks($block['first']);
+        }
+
+        if (is_array($block['slider'])) {
+            $html .= $this->getDots();
+            $html .= $this->getUrlLinks($block['slider']);
+        }
+
+        if (is_array($block['last'])) {
+            $html .= $this->getDots();
+            $html .= $this->getUrlLinks($block['last']);
+        }
+
+        return $html;
+    }
+
+    /**
+     * 渲染分页html
+     * @return mixed
+     */
+    public function render($params = null)
+    {
+        if (is_array($params)) {
+            if (isset($params['type'])) {
+                $this->simple = $params['type'] === 'simple';
+            }
+        }
+        if ($this->hasPages()) {
+            if ($this->simple) {
+                return sprintf(
+                    '<ul class="pager">%s %s</ul>',
+                    $this->getPreviousButton(),
+                    $this->getNextButton()
+                );
+            } else {
+                return sprintf(
+                    '<ul class="pagination">%s %s %s</ul>',
+                    $this->getPreviousButton(),
+                    $this->getLinks(),
+                    $this->getNextButton()
+                );
+            }
+        }
+    }
+
+    public function getNextPage()
+    {
+        return $this->currentPage + 1;
+    }
+
+    /**
+     * 生成一个可点击的按钮
+     *
+     * @param  string $url
+     * @param  int    $page
+     * @return string
+     */
+    protected function getAvailablePageWrapper($url, $page)
+    {
+        return '<li><a href="' . htmlentities($url) . '">' . $page . '</a></li>';
+    }
+
+    /**
+     * 生成一个禁用的按钮
+     *
+     * @param  string $text
+     * @return string
+     */
+    protected function getDisabledTextWrapper($text)
+    {
+        return '<li class="disabled"><span>' . $text . '</span></li>';
+    }
+
+    /**
+     * 生成一个激活的按钮
+     *
+     * @param  string $text
+     * @return string
+     */
+    protected function getActivePageWrapper($text)
+    {
+        return '<li class="active"><span>' . $text . '</span></li>';
+    }
+
+    /**
+     * 生成省略号按钮
+     *
+     * @return string
+     */
+    protected function getDots()
+    {
+        return $this->getDisabledTextWrapper('...');
+    }
+
+    /**
+     * 批量生成页码按钮.
+     *
+     * @param  array $urls
+     * @return string
+     */
+    protected function getUrlLinks(array $urls)
+    {
+        $html = '';
+
+        foreach ($urls as $page => $url) {
+            $html .= $this->getPageLinkWrapper($url, $page);
+        }
+
+        return $html;
+    }
+
+    /**
+     * 生成普通页码按钮
+     *
+     * @param  string $url
+     * @param  int    $page
+     * @return string
+     */
+    protected function getPageLinkWrapper($url, $page)
+    {
+        if ($page == $this->currentPage()) {
+            return $this->getActivePageWrapper($page);
+        }
+
+        return $this->getAvailablePageWrapper($url, $page);
+    }
+}

+ 18 - 0
addons/cms/library/CommentException.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace addons\cms\library;
+
+
+use think\Exception;
+use Throwable;
+
+class CommentException extends Exception
+{
+    public function __construct($message = "", $code = 0, $data = [])
+    {
+        $this->message = $message;
+        $this->code = $code;
+        $this->data = $data;
+    }
+
+}

+ 173 - 0
addons/cms/library/FulltextSearch.php

@@ -0,0 +1,173 @@
+<?php
+
+namespace addons\cms\library;
+
+use addons\cms\model\Modelx;
+use addons\xunsearch\library\Xunsearch;
+use think\Config;
+use think\Exception;
+use think\Log;
+use think\View;
+
+class FulltextSearch
+{
+
+    public static function config()
+    {
+        $data = [
+            [
+                'name'   => 'cms',
+                'title'  => 'CMS内容管理系统',
+                'fields' => [
+                    ['name' => 'pid', 'type' => 'id', 'title' => '主键'],
+                    ['name' => 'id', 'type' => 'numeric', 'title' => 'ID'],
+                    ['name' => 'title', 'type' => 'title', 'title' => '标题'],
+                    ['name' => 'content', 'type' => 'body', 'title' => '内容',],
+                    ['name' => 'type', 'type' => 'string', 'title' => '类型', 'index' => 'self'],
+                    ['name' => 'category_id', 'type' => 'numeric', 'title' => '分类ID', 'index' => 'self',],
+                    ['name' => 'user_id', 'type' => 'numeric', 'title' => '会员ID', 'index' => 'self',],
+                    ['name' => 'url', 'type' => 'string', 'title' => '链接',],
+                    ['name' => 'views', 'type' => 'numeric', 'title' => '浏览次数',],
+                    ['name' => 'comments', 'type' => 'numeric', 'title' => '评论次数',],
+                    ['name' => 'createtime', 'type' => 'date', 'title' => '发布时间',],
+                ]
+            ]
+        ];
+        return $data;
+    }
+
+    /**
+     * 重置搜索索引数据库
+     */
+    public static function reset()
+    {
+        \addons\cms\model\Archives::where('status', 'normal')->chunk(100, function ($list) {
+            foreach ($list as $item) {
+                self::add($item);
+            }
+        });
+        return true;
+    }
+
+    /**
+     * 添加索引
+     * @param $row
+     */
+    public static function add($row)
+    {
+        self::update($row, true);
+    }
+
+    /**
+     * 更新索引
+     * @param      $row
+     * @param bool $add
+     */
+    public static function update($row, $add = false)
+    {
+        $info = get_addon_info('xunsearch');
+        if (!$info || !$info['state']) {
+            return;
+        }
+        if (is_numeric($row)) {
+            $row = \addons\cms\model\Archives::get($row);
+            if (!$row) {
+                return;
+            }
+        }
+        if (isset($row['status']) && $row['status'] != 'normal') {
+            self::del($row);
+            return;
+        }
+        $data = [];
+        if ($row instanceof \addons\cms\model\Archives || $row instanceof \app\admin\model\cms\Archives) {
+            $content = '';
+            $model = Modelx::get($row['model_id']);
+            if ($model) {
+                $content = \think\Db::name($model['table'])->where('id', $row['id'])->value("content");
+                $content = $content ? strip_tags($content) : '';
+            }
+            $data['id'] = isset($row['id']) ? $row['id'] : 0;
+            $data['title'] = isset($row['title']) ? $row['title'] : '';
+            $data['category_id'] = isset($row['category_id']) ? $row['category_id'] : 0;
+            $data['user_id'] = isset($row['user_id']) ? $row['user_id'] : 0;
+            $data['content'] = $content;
+            $data['comments'] = isset($row['comments']) ? $row['comments'] : 0;
+            $data['createtime'] = isset($row['createtime']) ? $row['createtime'] : 0;
+            $data['views'] = isset($row['views']) ? $row['views'] : 0;
+            $data['type'] = 'archives';
+            $data['url'] = $row->fullurl;
+        }
+        if ($data) {
+            $data['pid'] = substr($data['type'], 0, 1) . $data['id'];
+            try {
+                Xunsearch::instance('cms')->update($data, $add);
+            } catch (\Exception $e) {
+                Log::record($e->getMessage());
+            }
+        }
+    }
+
+    /**
+     * 删除
+     * @param $row
+     */
+    public static function del($row)
+    {
+        $info = get_addon_info('xunsearch');
+        if (!$info || !$info['state']) {
+            return;
+        }
+        $pid = "a" . (is_numeric($row) ? $row : ($row && isset($row['id']) ? $row['id'] : 0));
+        if ($pid) {
+            try {
+                Xunsearch::instance('cms')->del($pid);
+            } catch (\Exception $e) {
+                Log::record($e->getMessage());
+            }
+        }
+    }
+
+    /**
+     * 获取搜索结果
+     * @return array
+     */
+    public static function search($q, $page = 1, $pagesize = 20, $order = '', $fulltext = true, $fuzzy = false, $synonyms = false)
+    {
+        $info = get_addon_info('xunsearch');
+        if (!$info || !$info['state']) {
+            return [];
+        }
+        return Xunsearch::instance('cms')->search($q, $page, $pagesize, $order, $fulltext, $fuzzy, $synonyms);
+    }
+
+    /**
+     * 获取建议搜索关键字
+     * @param string $q     关键字
+     * @param int    $limit 返回条数
+     * @return array
+     */
+    public static function suggestion($q, $limit = 10)
+    {
+        $info = get_addon_info('xunsearch');
+        if (!$info || !$info['state']) {
+            return [];
+        }
+        return Xunsearch::instance('cms')->suggestion($q, $limit);
+    }
+
+    /**
+     * 获取搜索热门关键字
+     * @return array
+     * @throws \XSException
+     */
+    public static function hot()
+    {
+        $info = get_addon_info('xunsearch');
+        if (!$info || !$info['state']) {
+            return [];
+        }
+        return Xunsearch::instance('cms')->getXS()->search->getHotQuery();
+    }
+
+}

+ 181 - 0
addons/cms/library/HashMap.php

@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * php构建哈希表类.
+ * User: Lustre
+ * Date: 17/3/9
+ * Time: 上午9:10
+ * Url: https://github.com/FireLustre/php-dfa-sensitive
+ **/
+
+namespace addons\cms\library;
+
+class HashMap
+{
+    /**
+     * 哈希表变量
+     *
+     * @var array|null
+     */
+    protected $hashTable = array();
+
+    public function __construct()
+    {
+    }
+
+    /**
+     * 向HashMap中添加一个键值对
+     *
+     * @param $key
+     * @param $value
+     * @return mixed|null
+     */
+    public function put($key, $value)
+    {
+        if (!array_key_exists($key, $this->hashTable)) {
+            $this->hashTable[$key] = $value;
+            return null;
+        }
+        $_temp = $this->hashTable[$key];
+        $this->hashTable[$key] = $value;
+        return $_temp;
+    }
+
+    /**
+     * 根据key获取对应的value
+     *
+     * @param $key
+     * @return mixed|null
+     */
+    public function get($key)
+    {
+        if (array_key_exists($key, $this->hashTable)) {
+            return $this->hashTable[$key];
+        }
+        return null;
+    }
+
+    /**
+     * 删除指定key的键值对
+     *
+     * @param $key
+     * @return mixed|null
+     */
+    public function remove($key)
+    {
+        $temp_table = array();
+        if (array_key_exists($key, $this->hashTable)) {
+            $tempValue = $this->hashTable[$key];
+            while ($curValue = current($this->hashTable)) {
+                if (!(key($this->hashTable) == $key)) {
+                    $temp_table[key($this->hashTable)] = $curValue;
+                }
+                next($this->hashTable);
+            }
+            $this->hashTable = null;
+            $this->hashTable = $temp_table;
+            return $tempValue;
+        }
+        return null;
+    }
+
+    /**
+     * 获取HashMap的所有键值
+     *
+     * @return array
+     */
+    public function keys()
+    {
+        return array_keys($this->hashTable);
+    }
+
+    /**
+     * 获取HashMap的所有value值
+     *
+     * @return array
+     */
+    public function values()
+    {
+        return array_values($this->hashTable);
+    }
+
+    /**
+     * 将一个HashMap的值全部put到当前HashMap中
+     *
+     * @param $map
+     */
+    public function putAll($map)
+    {
+        if (!$map->isEmpty() && $map->size() > 0) {
+            $keys = $map->keys();
+            foreach ($keys as $key) {
+                $this->put($key, $map->get($key));
+            }
+        }
+
+        return;
+    }
+
+    /**
+     * 移除HashMap中所有元素
+     *
+     * @return bool
+     */
+    public function removeAll()
+    {
+        $this->hashTable = null;
+        return true;
+    }
+
+    /**
+     * 判断HashMap中是否包含指定的值
+     *
+     * @param $value
+     * @return bool
+     */
+    public function containsValue($value)
+    {
+        while ($curValue = current($this->H_table)) {
+            if ($curValue == $value) {
+                return true;
+            }
+            next($this->hashTable);
+        }
+        return false;
+    }
+
+    /**
+     * 判断HashMap中是否包含指定的键key
+     *
+     * @param $key
+     * @return bool
+     */
+    public function containsKey($key)
+    {
+        if (array_key_exists($key, $this->hashTable)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 获取HashMap中元素个数
+     *
+     * @return int
+     */
+    public function size()
+    {
+        return count($this->hashTable);
+    }
+
+    /**
+     * 判断HashMap是否为空
+     *
+     * @return bool
+     */
+    public function isEmpty()
+    {
+        return (count($this->hashTable) == 0);
+    }
+}

+ 51 - 0
addons/cms/library/IntCode.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace addons\cms\library;
+
+use Hashids\Hashids;
+
+class IntCode
+{
+    private static $hasids = null;
+
+    /**
+     * 初始化
+     * @access public
+     * @return Hashids
+     */
+    public static function hashids()
+    {
+        if (is_null(self::$hasids)) {
+            $config = get_addon_config('cms');
+            $key = $config['hashids_key'];
+            $length = $config['hashids_key_length'] ?? 10;
+            $key = $key ? $key : config('token.key');
+            self::$hasids = new Hashids($key, $length);
+        }
+        return self::$hasids;
+    }
+
+    /**
+     * 加密
+     * @param $int
+     * @return string
+     */
+    public static function encode($int)
+    {
+        return self::hashids()->encode($int);
+    }
+
+    /**
+     * 解密
+     * @param $str
+     * @return string
+     */
+    public static function decode($str)
+    {
+        $data = self::hashids()->decode($str);
+        if (isset($data[0])) {
+            return $data[0];
+        }
+        return null;
+    }
+}

+ 76 - 0
addons/cms/library/Jssdk.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace addons\cms\library;
+
+use fast\Http;
+use fast\Random;
+use think\Cache;
+
+class Jssdk
+{
+    private $appId;
+    private $appSecret;
+
+    public function __construct()
+    {
+        $config = get_addon_config('third');
+
+        $this->appId = $config['wechat']['app_id'];
+        $this->appSecret = $config['wechat']['app_secret'];
+    }
+
+    public function getSignedPackage($url)
+    {
+        $jsapiTicket = $this->getJsApiTicket();
+        $timestamp = time();
+        $nonceStr = Random::alnum(16);
+        // 这里参数的顺序要按照 key 值 ASCII 码升序排序
+        $string = "jsapi_ticket={$jsapiTicket}&noncestr={$nonceStr}&timestamp={$timestamp}&url={$url}";
+        $signature = sha1($string);
+        $signPackage = array(
+            "appId"     => $this->appId,
+            "nonceStr"  => $nonceStr,
+            "timestamp" => $timestamp,
+            "url"       => $url,
+            "signature" => $signature,
+            "rawString" => $string,
+            "jsticket" => $jsapiTicket,
+        );
+        return $signPackage;
+    }
+
+    private function getJsApiTicket()
+    {
+        $ticket = Cache::get("wechat_jsapi_ticket");
+        if (!$ticket) {
+            $accessToken = $this->getAccessToken();
+            // 如果是企业号用以下 URL 获取 ticket
+            // $url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token={$accessToken}";
+            $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token={$accessToken}";
+            $ret = Http::get($url);
+            $json = (array)json_decode($ret, true);
+            $ticket = isset($json['ticket']) ? $json['ticket'] : '';
+            if ($ticket) {
+                Cache::set('wechat_jsapi_ticket', $ticket, 7200);
+            }
+        }
+        return $ticket;
+    }
+
+    private function getAccessToken()
+    {
+        $token = Cache::get("wechat_access_token");
+        if (!$token) {
+            // 如果是企业号用以下URL获取access_token
+            // $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={$this->appId}&corpsecret={$this->appSecret}";
+            $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$this->appId}&secret={$this->appSecret}";
+            $ret = Http::get($url);
+            $json = (array)json_decode($ret, true);
+            $token = isset($json['access_token']) ? $json['access_token'] : '';
+            if ($token) {
+                Cache::set('wechat_access_token', $token, 7200);
+            }
+        }
+        return $token;
+    }
+}

+ 229 - 0
addons/cms/library/Order.php

@@ -0,0 +1,229 @@
+<?php
+
+namespace addons\cms\library;
+
+use addons\cms\model\Archives;
+use app\common\library\Auth;
+use app\common\model\User;
+use fast\Http;
+use fast\Random;
+use think\Db;
+use think\Exception;
+use think\Hook;
+use think\Request;
+use think\View;
+use Yansongda\Pay\Exceptions\GatewayException;
+
+class Order
+{
+
+    /**
+     * 获取查询条件
+     * @return \Closure
+     */
+    protected static function getQueryCondition()
+    {
+        $condition = function ($query) {
+            $auth = Auth::instance();
+            $user_id = $auth->isLogin() ? $auth->id : 0;
+            $ip = Request::instance()->ip(0, false);
+            $config = get_addon_config('cms');
+            //如果开启支付需要登录,则只判断user_id
+            if ($config['ispaylogin']) {
+                $query->where('user_id', $user_id);
+            } else {
+                if ($user_id) {
+                    $query->whereOr('user_id', $user_id)->whereOr('ip', $ip);
+                } else {
+                    $query->where('user_id', 0)->where('ip', $ip);
+                }
+            }
+        };
+        return $condition;
+    }
+
+    /**
+     * 检查订单
+     * @param int $id 订单号
+     * @return bool
+     */
+    public static function check($id)
+    {
+        $archives = Archives::get($id);
+        if (!$archives) {
+            return false;
+        }
+        $where = [
+            'archives_id' => $id,
+            'status'      => 'paid',
+        ];
+
+        //如果是作者则直接允许查看
+        $auth = Auth::instance();
+        $user_id = $auth->isLogin() ? $auth->id : 0;
+        if ($user_id && $user_id == $archives->user_id) {
+            return true;
+        }
+
+        //匹配已支付订单
+        $order = \addons\cms\model\Order::where($where)->where(self::getQueryCondition())->order('id', 'desc')->find();
+        return $order ? true : false;
+    }
+
+    /**
+     * 发起订单支付
+     *
+     * @param int    $archives_id 文档ID
+     * @param string $paytype     支付类型
+     * @param string $method      支付方法
+     * @param string $openid      Openid
+     * @param string $notifyurl   通知地址
+     * @param string $returnurl   返回地址
+     * @return \addons\epay\library\Collection|\addons\epay\library\RedirectResponse|\addons\epay\library\Response|null
+     * @throws OrderException
+     */
+    public static function submit($archives_id, $paytype = 'wechat', $method = 'web', $openid = '', $notifyurl = '', $returnurl = '')
+    {
+        $archives = Archives::get($archives_id);
+        if (!$archives) {
+            throw new OrderException('文档未找到');
+        }
+
+        $order = \addons\cms\model\Order::where('archives_id', $archives->id)
+            ->where(self::getQueryCondition())
+            ->order('id', 'desc')
+            ->find();
+        if ($order && $order['status'] == 'paid') {
+            throw new OrderException('订单已支付');
+        }
+
+        $auth = Auth::instance();
+        $request = Request::instance();
+        if (!$order || (time() - $order->createtime) > 600 || $order->amount != $archives->price) {
+            $orderid = date("YmdHis") . mt_rand(100000, 999999);
+            $data = [
+                'user_id'     => $auth->id ? $auth->id : 0,
+                'orderid'     => $orderid,
+                'archives_id' => $archives->id,
+                'title'       => "付费阅读",
+                'amount'      => $archives->price,
+                'payamount'   => 0,
+                'paytype'     => $paytype,
+                'method'      => $method,
+                'ip'          => $request->ip(0, false),
+                'useragent'   => substr($request->server('HTTP_USER_AGENT'), 0, 255),
+                'status'      => 'created'
+            ];
+            $order = \addons\cms\model\Order::create($data);
+        } else {
+            //支付方式变更
+            if (($order['method'] && $order['paytype'] == $paytype && $order['method'] != $method)) {
+                $orderid = date("YmdHis") . mt_rand(100000, 999999);
+                $order->save(['orderid' => $orderid]);
+            }
+
+            //更新支付类型和方法
+            $order->save(['paytype' => $paytype, 'method' => $method]);
+
+            if ($order->amount != $archives->price || $order->paytype != $paytype) {
+                $order->amount = $archives->price;
+                $order->paytype = $paytype;
+                $order->save();
+            }
+        }
+
+        //使用余额支付
+        if ($paytype == 'balance') {
+            if (!$auth->id) {
+                throw new OrderException('需要登录后才能够支付');
+            }
+            if ($auth->money < $archives->price) {
+                throw new OrderException('余额不足,无法进行支付');
+            }
+            Db::startTrans();
+            try {
+                User::money(-$archives->price, $auth->id, '购买付费文档:' . $archives['title']);
+                self::settle($order->orderid, $archives->price);
+                Db::commit();
+            } catch (Exception $e) {
+                Db::rollback();
+                throw new OrderException($e->getMessage());
+            }
+            throw new OrderException('余额支付成功', 1);
+        }
+
+        $response = null;
+        $epay = get_addon_info('epay');
+        if ($epay && $epay['state']) {
+            $notifyurl = $notifyurl ? $notifyurl : $request->root(true) . '/addons/cms/order/epay/type/notify/paytype/' . $paytype;
+            $returnurl = $returnurl ? $returnurl : $request->root(true) . '/addons/cms/order/epay/type/return/paytype/' . $paytype . '/orderid/' . $order->orderid;
+
+            //保证取出的金额一致,不一致将导致订单重复错误
+            $amount = sprintf("%.2f", $order->amount);
+            $params = [
+                'amount'    => $amount,
+                'orderid'   => $order->orderid,
+                'type'      => $paytype,
+                'title'     => "支付{$amount}元",
+                'notifyurl' => $notifyurl,
+                'returnurl' => $returnurl,
+                'method'    => $method,
+                'openid'    => $openid
+            ];
+            try {
+                $response = \addons\epay\library\Service::submitOrder($params);
+            } catch (GatewayException $e) {
+                throw new OrderException(config('app_debug') ? $e->getMessage() : "支付失败,请稍后重试");
+            }
+        } else {
+            $result = \think\Hook::listen('cms_order_submit', $order);
+            if (!$result) {
+                throw new OrderException("请在后台安装配置微信支付宝整合插件");
+            }
+        }
+        return $response;
+    }
+
+    /**
+     * 订单结算
+     * @param mixed  $orderid   订单号
+     * @param mixed  $payamount 金额
+     * @param string $memo      备注
+     * @return bool
+     */
+    public static function settle($orderid, $payamount, $memo = '')
+    {
+        $order = \addons\cms\model\Order::getByOrderid($orderid);
+        if (!$order) {
+            return false;
+        }
+        if ($order['status'] != 'paid') {
+            if ($payamount != $order->amount) {
+                \think\Log::write("[cms][pay][{$orderid}][订单支付金额不一致]");
+                return false;
+            }
+
+            //计算收益
+            $config = get_addon_config('cms');
+            list($systemRatio, $userRatio) = explode(':', $config['archivesratio']);
+            Db::startTrans();
+            try {
+                $order->payamount = $payamount;
+                $order->paytime = time();
+                $order->status = 'paid';
+                $order->memo = $memo;
+                $order->save();
+
+                if ($order->payamount == $order->amount) {
+                    User::money($systemRatio * $payamount, $config['system_user_id'], '付费文章收益');
+                    User::money($userRatio * $payamount, $order->archives->user_id, '付费文章收益');
+                }
+                Db::commit();
+            } catch (\Exception $e) {
+                Db::rollback();
+                return false;
+            }
+        }
+        return true;
+    }
+}

+ 18 - 0
addons/cms/library/OrderException.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace addons\cms\library;
+
+
+use think\Exception;
+use Throwable;
+
+class OrderException extends Exception
+{
+    public function __construct($message = "", $code = 0, $data = [])
+    {
+        $this->message = $message;
+        $this->code = $code;
+        $this->data = $data;
+    }
+
+}

+ 297 - 0
addons/cms/library/SensitiveHelper.php

@@ -0,0 +1,297 @@
+<?php
+
+/**
+ * 敏感词类库.
+ * User: Lustre
+ * Date: 17/3/9
+ * Time: 上午9:11
+ * Url: https://github.com/FireLustre/php-dfa-sensitive
+ */
+
+namespace addons\cms\library;
+
+class SensitiveHelper
+{
+    /**
+     * 待检测语句长度
+     *
+     * @var int
+     */
+    protected $contentLength = 0;
+
+    /**
+     * 敏感词单例
+     *
+     * @var object|null
+     */
+    private static $_instance = null;
+
+    /**
+     * 铭感词库树
+     *
+     * @var HashMap|null
+     */
+    protected $wordTree = null;
+
+    /**
+     * 存放待检测语句铭感词
+     *
+     * @var array|null
+     */
+    protected static $badWordList = null;
+
+    /**
+     * 获取单例
+     *
+     * @return self
+     */
+    public static function init()
+    {
+        if (!self::$_instance instanceof self) {
+            self::$_instance = new self();
+        }
+        return self::$_instance;
+    }
+
+    /**
+     * 构建铭感词树【文件模式】
+     *
+     * @param string $filepath
+     * @return $this
+     * @throws \Exception
+     */
+    public function setTreeByFile($filepath = '')
+    {
+        if (!file_exists($filepath)) {
+            throw new \Exception('词库文件不存在');
+        }
+
+        // 词库树初始化
+        $this->wordTree = new HashMap();
+
+        foreach ($this->yieldToReadFile($filepath) as $word) {
+            $this->buildWordToTree(trim($word));
+        }
+
+        return $this;
+    }
+
+
+    /**
+     * 构建铭感词树【数组模式】
+     *
+     * @param null $sensitiveWords
+     * @return $this
+     * @throws \Exception
+     */
+    public function setTree($sensitiveWords = null)
+    {
+        if (empty($sensitiveWords)) {
+            throw new \Exception('词库不能为空');
+        }
+
+        $this->wordTree = new HashMap();
+
+        foreach ($sensitiveWords as $word) {
+            $this->buildWordToTree($word);
+        }
+        return $this;
+    }
+
+    /**
+     * 检测文字中的敏感词
+     *
+     * @param string $content 待检测内容
+     * @param int $matchType 匹配类型 [默认为最小匹配规则]
+     * @param int $wordNum 需要获取的敏感词数量 [默认获取全部]
+     * @return array
+     */
+    public function getBadWord($content, $matchType = 1, $wordNum = 0)
+    {
+        $this->contentLength = mb_strlen($content, 'utf-8');
+        $badWordList = array();
+        for ($length = 0; $length < $this->contentLength; $length++) {
+            $matchFlag = 0;
+            $flag = false;
+            $tempMap = $this->wordTree;
+            for ($i = $length; $i < $this->contentLength; $i++) {
+                $keyChar = mb_substr($content, $i, 1, 'utf-8');
+
+                // 获取指定节点树
+                $nowMap = $tempMap->get($keyChar);
+
+                // 不存在节点树,直接返回
+                if (empty($nowMap)) {
+                    break;
+                }
+
+                // 存在,则判断是否为最后一个
+                $tempMap = $nowMap;
+
+                // 找到相应key,偏移量+1
+                $matchFlag++;
+
+                // 如果为最后一个匹配规则,结束循环,返回匹配标识数
+                if (false === $nowMap->get('ending')) {
+                    continue;
+                }
+
+                $flag = true;
+
+                // 最小规则,直接退出
+                if (1 === $matchType) {
+                    break;
+                }
+            }
+
+            if (!$flag) {
+                $matchFlag = 0;
+            }
+
+            // 找到相应key
+            if ($matchFlag <= 0) {
+                continue;
+            }
+
+            $badWordList[] = mb_substr($content, $length, $matchFlag, 'utf-8');
+
+            // 有返回数量限制
+            if ($wordNum > 0 && count($badWordList) == $wordNum) {
+                return $badWordList;
+            }
+
+            // 需匹配内容标志位往后移
+            $length = $length + $matchFlag - 1;
+        }
+        return $badWordList;
+    }
+
+
+    /**
+     * 替换敏感字字符
+     *
+     * @param $content
+     * @param $replaceChar
+     * @param string $sTag
+     * @param string $eTag
+     * @param int $matchType
+     * @return mixed
+     */
+    public function replace($content, $replaceChar = '', $sTag = '', $eTag = '', $matchType = 1)
+    {
+        if (empty($content)) {
+            throw new \Exception('请填写检测的内容');
+        }
+
+        if (empty(self::$badWordList)) {
+            $badWordList = $this->getBadWord($content, $matchType);
+        } else {
+            $badWordList = self::$badWordList;
+        }
+
+        // 未检测到敏感词,直接返回
+        if (empty($badWordList)) {
+            return $content;
+        }
+
+        foreach ($badWordList as $badWord) {
+            if ($sTag || $eTag) {
+                $replaceChar = $sTag . $badWord . $eTag;
+            }
+            $content = str_replace($badWord, $replaceChar, $content);
+        }
+        return $content;
+    }
+
+    /**
+     * 被检测内容是否合法,合法返回true,非法返回false
+     * @param $content
+     * @return bool
+     */
+    public function islegal($content)
+    {
+        $this->contentLength = mb_strlen($content, 'utf-8');
+
+        for ($length = 0; $length < $this->contentLength; $length++) {
+            $matchFlag = 0;
+
+            $tempMap = $this->wordTree;
+            for ($i = $length; $i < $this->contentLength; $i++) {
+                $keyChar = mb_substr($content, $i, 1, 'utf-8');
+
+                // 获取指定节点树
+                $nowMap = $tempMap->get($keyChar);
+
+                // 不存在节点树,直接返回
+                if (empty($nowMap)) {
+                    break;
+                }
+
+                // 找到相应key,偏移量+1
+                $tempMap = $nowMap;
+                $matchFlag++;
+
+                // 如果为最后一个匹配规则,结束循环,返回匹配标识数
+                if (false === $nowMap->get('ending')) {
+                    continue;
+                }
+
+                return false;
+            }
+
+            // 找到相应key
+            if ($matchFlag <= 0) {
+                continue;
+            }
+
+            // 需匹配内容标志位往后移
+            $length = $length + $matchFlag - 1;
+        }
+        return true;
+    }
+
+    protected function yieldToReadFile($filepath)
+    {
+        $fp = fopen($filepath, 'r');
+        while (!feof($fp)) {
+            yield fgets($fp);
+        }
+        fclose($fp);
+    }
+
+    // 将单个敏感词构建成树结构
+    protected function buildWordToTree($word = '')
+    {
+        if ('' === $word) {
+            return;
+        }
+        $tree = $this->wordTree;
+
+        $wordLength = mb_strlen($word, 'utf-8');
+        for ($i = 0; $i < $wordLength; $i++) {
+            $keyChar = mb_substr($word, $i, 1, 'utf-8');
+
+            // 获取子节点树结构
+            $tempTree = $tree->get($keyChar);
+
+            if ($tempTree) {
+                $tree = $tempTree;
+            } else {
+                // 设置标志位
+                $newTree = new HashMap();
+                $newTree->put('ending', false);
+
+                // 添加到集合
+                $tree->put($keyChar, $newTree);
+                $tree = $newTree;
+            }
+
+            // 到达最后一个节点
+            if ($i == $wordLength - 1) {
+                $tree->put('ending', true);
+            }
+        }
+
+        return;
+    }
+}

+ 679 - 0
addons/cms/library/Service.php

@@ -0,0 +1,679 @@
+<?php
+
+namespace addons\cms\library;
+
+use addons\cms\library\aip\AipContentCensor;
+use addons\cms\library\aip\AipNlp;
+use addons\cms\model\Autolink;
+use addons\cms\model\Diyform;
+use addons\cms\model\Fields;
+use addons\cms\model\Modelx;
+use addons\cms\model\Tag;
+use fast\Http;
+use fast\Random;
+use think\Cache;
+use think\Config;
+use think\Db;
+use think\Exception;
+use think\Hook;
+use think\Model;
+use think\model\Collection;
+
+class Service
+{
+
+    /**
+     * 检测内容是否合法
+     * @param string $content 检测内容
+     * @param string $type    类型
+     * @return bool
+     */
+    public static function isContentLegal($content, $type = null)
+    {
+        $config = get_addon_config('cms');
+        $type = is_null($type) ? $config['audittype'] : $type;
+        if ($type == 'local') {
+            // 敏感词过滤
+            $handle = SensitiveHelper::init()->setTreeByFile(ADDON_PATH . 'cms/data/words.dic');
+            //首先检测是否合法
+            $isLegal = $handle->islegal($content);
+            return $isLegal ? true : false;
+        } elseif ($type == 'baiduyun') {
+            $client = new AipContentCensor($config['aip_appid'], $config['aip_apikey'], $config['aip_secretkey']);
+            $result = $client->textCensorUserDefined($content);
+            if (!isset($result['conclusionType']) || $result['conclusionType'] > 1) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 获取标题的关键字
+     * @param $title
+     * @return array
+     */
+    public static function getContentTags($title)
+    {
+        $arr = [];
+        $config = get_addon_config('cms');
+        if ($config['nlptype'] == 'local') {
+            !defined('_VIC_WORD_DICT_PATH_') && define('_VIC_WORD_DICT_PATH_', ADDON_PATH . 'cms/data/dict.json');
+            $handle = new VicWord('json');
+            $result = $handle->getAutoWord($title);
+            foreach ($result as $index => $item) {
+                $arr[] = $item[0];
+            }
+        } else {
+            $client = new AipNlp($config['aip_appid'], $config['aip_apikey'], $config['aip_secretkey']);
+            $result = $client->lexer($title);
+            if (isset($result['items'])) {
+                foreach ($result['items'] as $index => $item) {
+                    if (!in_array($item['pos'], ['v', 'vd', 'nd', 'a', 'ad', 'an', 'd', 'm', 'q', 'r', 'p', 'c', 'u', 'xc', 'w'])) {
+                        $arr[] = $item['item'];
+                    }
+                }
+            }
+        }
+        foreach ($arr as $index => $item) {
+            if (mb_strlen($item) == 1) {
+                unset($arr[$index]);
+            }
+        }
+        return array_filter(array_unique($arr));
+    }
+
+    /**
+     * 内容关键字自动加链接
+     * 优先顺序为 站点配置自动链接 > 自动链接表 > 标签内链
+     */
+    public static function autolinks($content)
+    {
+        $stages = [];
+
+        //先移除已有的自动链接
+        $content = preg_replace_callback('/\<a\s*data\-rel="autolink".*?\>(.*?)\<\/a\>/i', function ($match) {
+            return $match[1];
+        }, $content);
+
+        //存储所有A标签
+        $content = preg_replace_callback('/\<a(.*?)href\s*=\s*(\'|")(.*?)(\'|")(.*?)\>(.*?)\<\/a\>/i', function ($match) use (&$stages) {
+            $data = [$match[3], $match[5], $match[6]];
+            return '<' . array_push($stages, $data) . '>';
+        }, $content);
+
+        //存在所有HTML标签
+        $content = preg_replace_callback('/(<(?!\d+).*?>)/i', function ($match) use (&$stages) {
+            return '<' . array_push($stages, $match[1]) . '>';
+        }, $content);
+
+        $config = get_addon_config('cms');
+        $limit = $config['autolinks_max_replace'] ?? 2; //单一标签最大替换次数
+        $autolinkArr = [];
+        $tagList = Tag::where('autolink', 1)->cache(true)->where('status', 'normal')->select();
+        foreach ($tagList as $index => $item) {
+            $autolinkArr[$item['name']] = ['text' => $item['name'], 'type' => 'tag', 'url' => $item['fullurl']];
+        }
+        $autolinkList = Autolink::where('status', 'normal')->cache(true)->order('weigh DESC,id DESC')->select();
+        foreach ($autolinkList as $index => $item) {
+            $autolinkArr[$item['title']] = ['text' => $item['title'], 'type' => 'autolink', 'url' => $item['url'], 'target' => $item['target'], 'id' => $item['id']];
+        }
+        foreach ($config['autolinks'] as $text => $url) {
+            $autolinkArr[$text] = ['text' => $text, 'type' => 'config', 'url' => $url];
+        }
+
+        $autolinkArr = array_values($autolinkArr);
+        //字符串长的优先替换
+        usort($autolinkArr, function ($a, $b) {
+            if ($a['text'] == $b['text']) return 0;
+            return (strlen($a['text']) > strlen($b['text'])) ? -1 : 1;
+        });
+
+        //替换链接
+        foreach ($autolinkArr as $index => $item) {
+            $content = preg_replace_callback('/(' . preg_quote($item['text'], '/') . ')/i', function ($match) use ($item, $config, &$stages) {
+                $url = $item['type'] == 'autolink' && isset($item['id']) ? addon_url('cms/go/index', [], $config['urlsuffix'], true) . '?id=' . $item['id'] : $item['url'];
+                $data = [$url, (isset($item['target']) && $item['target'] == 'blank' ? ' target="_blank"' : ''), $match[0]];
+                return '<' . array_push($stages, $data) . '>';
+            }, $content, $limit);
+        }
+
+        return preg_replace_callback('/<(\d+)>/', function ($match) use (&$stages, $config) {
+            $data = $stages[$match[1] - 1];
+            if (!is_array($data)) {
+                return $data;
+            }
+            $url = $data[0];
+            $urlArr = parse_url($url);
+            //站内链接不中转,站外链接中转
+            if (isset($urlArr['host']) && $urlArr['host'] != request()->host() && ($config['redirecturl'] ?? true)) {
+                $url = addon_url('cms/go/index', [], $config['urlsuffix'], true) . '?' . http_build_query(['url' => $url]);
+            }
+            return "<a href=\"{$url}\" {$data[1]}>{$data[2]}</a>";
+        }, $content);
+    }
+
+    /**
+     * 推送消息通知
+     * @param string $content 内容
+     * @param string $type
+     * @param string $template_id
+     */
+    public static function notice($content, $type = null, $template_id = null)
+    {
+        $config = get_addon_config('cms');
+        $type = $type ? $type : $config['auditnotice'];
+        $template_id = $template_id ? $template_id : $config['noticetemplateid'];
+
+        try {
+            if ($type == 'dinghorn') {
+                //钉钉通知插件(dinghorn)
+                Hook::listen('msg_notice', $template_id, [
+                    'content' => $content
+                ]);
+            } elseif ($type == 'vbot') {
+                //企业微信通知(vbot)
+                Hook::listen('vbot_send_msg', $template_id, [
+                    'content' => $content
+                ]);
+            } elseif ($type == 'notice') {
+                //消息通知插件(notice)
+                $params = [
+                    'event'  => $template_id,
+                    'params' => [
+                        'title'   => $content,
+                        'content' => $content,
+                    ]
+                ];
+                Hook::listen('notice_to_data', $params);
+            }
+        } catch (\Exception $e) {
+
+        }
+    }
+
+    /**
+     * 获取表字段信息
+     * @param string $table 表名
+     * @return array
+     */
+    public static function getTableFields($table)
+    {
+        $tagName = "cms-table-fields-{$table}";
+        $fieldlist = Cache::get($tagName);
+        if (!Config::get('app_debug') && $fieldlist) {
+            return $fieldlist;
+        }
+        $dbname = Config::get('database.database');
+        //从数据库中获取表字段信息
+        $sql = "SELECT * FROM `information_schema`.`columns` WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION";
+        //加载主表的列
+        $columnList = Db::query($sql, [$dbname, $table]);
+        $fieldlist = [];
+        foreach ($columnList as $index => $item) {
+            $fieldlist[] = ['name' => $item['COLUMN_NAME'], 'title' => $item['COLUMN_COMMENT'], 'type' => $item['DATA_TYPE']];
+        }
+        Cache::set($tagName, $fieldlist);
+        return $fieldlist;
+    }
+
+    /**
+     * 获取指定类型的自定义字段列表
+     */
+    public static function getCustomFields($source, $source_id, $values = [], $conditions = [])
+    {
+        $fields = Fields::where('source', $source)
+            ->where('source_id', $source_id)
+            ->where($conditions)
+            ->where('status', 'normal')
+            ->order('weigh desc,id desc')
+            ->select();
+        foreach ($fields as $k => $v) {
+            //优先取编辑的值,再次取默认值
+            $v->value = isset($values[$v['name']]) ? $values[$v['name']] : (is_null($v['defaultvalue']) ? '' : $v['defaultvalue']);
+            $v->rule = str_replace(',', '; ', $v->rule);
+            if (in_array($v['type'], ['checkbox', 'lists', 'images'])) {
+                $checked = '';
+                if ($v['minimum'] && $v['maximum']) {
+                    $checked = "{$v['minimum']}~{$v['maximum']}";
+                } elseif ($v['minimum']) {
+                    $checked = "{$v['minimum']}~";
+                } elseif ($v['maximum']) {
+                    $checked = "~{$v['maximum']}";
+                }
+                if ($checked) {
+                    $v->rule .= (';checked(' . $checked . ')');
+                }
+            }
+            if (in_array($v['type'], ['checkbox', 'radio']) && stripos($v->rule, 'required') !== false) {
+                $v->rule = str_replace('required', 'checked', $v->rule);
+            }
+            if (in_array($v['type'], ['selects'])) {
+                $v->extend .= (' ' . 'data-max-options="' . $v['maximum'] . '"');
+            }
+        }
+
+        return $fields;
+    }
+
+    /**
+     * 获取过滤列表
+     * @param string $source    来源类型
+     * @param int    $source_id 来源ID
+     * @param array  $filter    过滤条件
+     * @param array  $params    搜索参数
+     * @param bool   $multiple  是否为复选模式
+     * @return array
+     */
+    public static function getFilterList($source, $source_id, $filter, $params = [], $multiple = false)
+    {
+        $fieldsList = Fields::where('source', $source)
+            ->where('source_id', $source_id)
+            ->where('status', 'normal')
+            ->cache(true)
+            ->select();
+
+        $filterList = [];
+        $multiValueFields = [];
+
+        $fields = [];
+        if (in_array($source, ['model', 'diyform'])) {
+            //查找主表启用过滤搜索的字段
+            $model = $source == 'model' ? Modelx::get($source_id) : Diyform::get($source_id);
+            $setting = $model->setting;
+            if (isset($setting['filterfields'])) {
+                foreach ($setting['filterfields'] as $index => $name) {
+                    $title = isset($setting['titlelist'][$name]) ? $setting['titlelist'][$name] : $name;
+                    $filterlist = isset($setting['filterlist'][$name]) ? $setting['filterlist'][$name] : '';
+                    $filterlist = \app\common\model\Config::decode($filterlist);
+                    if (!$filterlist) {
+                        continue;
+                    }
+                    if (in_array($name, ['special_ids', 'channel_ids', 'images', 'tags', 'keywords'])) {
+                        $multiValueFields[] = $name;
+                    }
+
+                    $fields[] = [
+                        'name'    => $name,
+                        'title'   => $title,
+                        'content' => $filterlist
+                    ];
+                }
+            }
+        }
+        foreach ($fieldsList as $k => $v) {
+            if (!$v['isfilter']) {
+                continue;
+            }
+            $content = isset($v['filter_list']) && $v['filter_list'] ? $v['filter_list'] : $v['content_list'];
+            if (!$content) {
+                continue;
+            }
+            //多选值字段需要做特殊处理
+            if (in_array($v['type'], ['selects', 'checkbox', 'array', 'selectpages'])) {
+                $multiValueFields[] = $v['name'];
+            }
+            $fields[] = [
+                'name'    => $v['name'],
+                'title'   => $v['title'],
+                'content' => $content
+            ];
+        }
+        $filter = array_intersect_key($filter, array_flip(array_column($fields, 'name')));
+        foreach ($fields as $k => $v) {
+            $content = [];
+            $all = ['' => __('All')] + (is_array($v['content']) ? $v['content'] : []);
+            foreach ($all as $m => $n) {
+                $filterArr = isset($filter[$v['name']]) && $filter[$v['name']] !== '' ? ($multiple ? explode(';', $filter[$v['name']]) : [$filter[$v['name']]]) : [];
+                $active = ($m === '' && !$filterArr) || ($m !== '' && in_array($m, $filterArr)) ? true : false;
+                if ($active) {
+                    $current = implode(';', array_diff($filterArr, [$m]));
+                } else {
+                    $current = $multiple ? implode(';', array_merge($filterArr, [$m])) : $m;
+                }
+                $prepare = $m === '' ? array_diff_key($filter, [$v['name'] => $m]) : array_merge($filter, [$v['name'] => $current]);
+                //$url = '?' . http_build_query(array_merge(['filter' => $prepare], array_diff_key($params, ['filter' => ''])));
+                $url = '?' . str_replace(['%2C', '%3B'], [',', ';'], http_build_query(array_merge($prepare, array_intersect_key($params, array_flip(['orderby', 'orderway', 'multiple'])))));
+                $content[] = ['value' => $m, 'title' => $n, 'active' => $active, 'url' => $url];
+            }
+
+            $filterList[] = [
+                'name'    => $v['name'],
+                'title'   => $v['title'],
+                'content' => $content,
+            ];
+        }
+        foreach ($filter as $index => &$item) {
+            $item = is_array($item) ? $item : explode(',', str_replace(';', ',', $item));
+        }
+
+        return [$filterList, $filter, $params, $fields, $multiValueFields, $fieldsList];
+    }
+
+    /**
+     * 获取排序列表
+     * @param string $orderby
+     * @param string $orderway
+     * @param array  $orders
+     * @param array  $params
+     * @param array  $fieldsList
+     * @return array
+     */
+    public static function getOrderList($orderby, $orderway, $orders = [], $params = [], $fieldsList = [])
+    {
+        $lastOrderby = '';
+        $lastOrderway = $orderway && in_array(strtolower($orderway), ['asc', 'desc']) ? $orderway : 'desc';
+
+        foreach ($fieldsList as $index => $field) {
+            if ($field['isorder']) {
+                $orders[] = ['name' => $field['name'], 'field' => $field['name'], 'title' => $field['title']];
+            }
+        }
+
+        $orderby = in_array($orderby, array_map(function ($item) {
+            return $item['name'];
+        }, $orders)) ? $orderby : 'default';
+
+        foreach ($orders as $index => $order) {
+            if ($orderby == $order['name']) {
+                $lastOrderby = $order['field'];
+                break;
+            }
+        }
+
+        $orderList = [];
+        foreach ($orders as $k => $v) {
+            $url = '?' . http_build_query(array_merge($params, ['orderby' => $v['name'], 'orderway' => $v['name'] == $orderby ? ($lastOrderway == 'desc' ? 'asc' : 'desc') : 'desc']));
+            $v['active'] = $orderby == $v['name'] ? true : false;
+            $v['url'] = $url;
+            $orderList[] = $v;
+        }
+
+        return [$orderList, $lastOrderby, $lastOrderway];
+    }
+
+    /**
+     * 获取过滤的最终条件和绑定参数
+     * @param array $filter           过滤条件
+     * @param array $multiValueFields 多值字段
+     * @param bool  $multiple         是否为复选模式
+     * @return array
+     */
+    public static function getFilterWhereBind($filter, $multiValueFields, $multiple = false)
+    {
+        //构造bind数据
+        $bind = [];
+        foreach ($filter as $field => &$item) {
+            if (in_array($field, $multiValueFields)) {
+                $item = !is_array($item) && stripos($item, ',') !== false ? explode(',', $item) : $item;
+                if (is_array($item)) {
+                    foreach ($item as $index => $subitem) {
+                        $bind[$field . $index] = $subitem;
+                    }
+                } else {
+                    $bind[$field] = $item;
+                }
+            }
+        }
+
+        $filterWhere = function ($query) use ($filter, $multiValueFields) {
+            foreach ($filter as $field => $item) {
+                $item = is_array($item) ? $item : [$item];
+                if (in_array($field, $multiValueFields)) {
+                    $query->where(function ($query) use ($field, $item) {
+                        foreach ($item as $subindex => $subitem) {
+                            $query->whereOr("FIND_IN_SET(:" . $field . $subindex . ", `{$field}`)");
+                        }
+                    });
+                } else {
+                    $query->where(function ($query) use ($field, $item) {
+                        foreach ($item as $subindex => $subitem) {
+                            //如果匹配区间,以~分隔
+                            if (preg_match("/[a-zA-Z0-9\.\-]+\~[a-zA-Z0-9\.\-]+/", $subitem)) {
+                                $condition = explode('~', $subitem);
+                                //判断是否时间区间
+                                $op = preg_match("/\d{4}\-\d{1,2}\-\d{1,2}/", $condition[0]) ? 'between time' : 'between';
+                                $query->whereOr($field, $op, $condition);
+                            } else {
+                                $query->whereOr($field, $subitem);
+                            }
+                        }
+                    });
+                }
+            }
+        };
+        return [$filterWhere, $bind];
+    }
+
+    /**
+     * 获取pagelist标签参数
+     * @param string $template
+     * @return array
+     */
+    public static function getPagelistParams($template)
+    {
+        $config = get_addon_config('cms');
+        $templateFile = ADDON_PATH . 'cms' . DS . 'view' . DS . $config['theme'] . $template . '.html';
+        if (!is_file($templateFile)) {
+            return [];
+        }
+        $templateContent = file_get_contents($templateFile);
+        preg_match("/\{cms:pagelist(.*)\}/i", $templateContent, $matches);
+        $attr = [];
+        if ($matches) {
+            $tagAttrText = $matches[1];
+            preg_match_all('/\s+(?>(?P<name>[\w-]+)\s*)=(?>\s*)([\"\'])(?P<value>(?:(?!\\2).)*)\\2/is', $tagAttrText, $matches, PREG_SET_ORDER);
+            foreach ($matches as $match) {
+                $attr[$match['name']] = $match['value'];
+            }
+            unset($matches);
+        }
+        return $attr;
+    }
+
+    /**
+     * 追加_text属性值
+     * @param $fieldsContentList
+     * @param $row
+     */
+    public static function appendTextAttr(&$fieldsContentList, &$row)
+    {
+        //附加列表字段
+        array_walk($fieldsContentList, function ($content, $field) use (&$row) {
+            if (isset($row[$field])) {
+                if (isset($content[$row[$field]])) {
+                    $list = [$row[$field] => $content[$row[$field]]];
+                } else {
+                    $keys = $values = explode(',', $row[$field]);
+                    foreach ($values as $index => &$item) {
+                        $item = isset($content[$item]) ? $content[$item] : $item;
+                    }
+                    $list = array_combine($keys, $values);
+                }
+            } else {
+                $list = [];
+            }
+            $list = array_filter($list);
+            $row[$field . '_text'] = implode(',', $list);
+            $row[$field . '_list'] = $list;
+        });
+    }
+
+    /**
+     * 追加_text和_list后缀数据
+     *
+     * @param string  $source
+     * @param int     $source_id
+     * @param mixed   $row
+     * @param boolean $isMultiArray 是否为二维数组
+     * @return mixed
+     */
+    public static function appendTextAndList($source, $source_id, &$row, $isMultiArray = false)
+    {
+        $list = Fields::where('source', $source)
+            ->where('source_id', $source_id)
+            ->field('id,name,type,content')
+            ->where('status', 'normal')
+            ->cache(true)
+            ->select();
+        $fieldsType = [];
+        $fieldsList = [];
+        $listFields = Fields::getListFields();
+        foreach ($list as $field => $content) {
+            $fieldsType[$content['name']] = $content['type'];
+            if (in_array($content['type'], $listFields)) {
+                $fieldsList[$content['name']] = $content['content_list'];
+            }
+        }
+        $appendFunc = function ($field, $content, &$row) use ($fieldsType) {
+            $fieldType = $fieldsType[$field] ?? '';
+            if (isset($row[$field])) {
+                if (isset($content[$row[$field]])) {
+                    $list = [$row[$field] => $content[$row[$field]]];
+                } else {
+                    $keys = $values = explode(',', $row[$field]);
+                    foreach ($values as $index => &$item) {
+                        $item = isset($content[$item]) ? $content[$item] : $item;
+                    }
+                    $list = array_combine($keys, $values);
+                }
+            } else {
+                $list = [];
+            }
+            $list = array_filter($list);
+            $row[$field . '_text'] = $fieldType == 'array' ? $row[$field] : implode(',', $list);
+            $row[$field . '_list'] = $fieldType == 'array' ? (array)json_decode($row[$field], true) : $list;
+        };
+        foreach ($fieldsList as $field => $content) {
+            if ($isMultiArray) {
+                foreach ($row as $subindex => &$subitem) {
+                    $appendFunc($field, $content, $subitem);
+                }
+            } else {
+                $appendFunc($field, $content, $row);
+            }
+        }
+
+        return $row;
+    }
+
+    /**
+     * 获取自定义字段关联表数据
+     * @param string $source
+     * @param int    $source_id
+     * @param string $field
+     * @param mixed  $key
+     * @return string
+     */
+    public static function getRelationFieldValue($source, $source_id, $field, $key)
+    {
+        $fieldInfo = Fields::where(['source' => $source, 'source_id' => $source_id, 'name' => $field])->cache(true)->find();
+        if (!$fieldInfo) {
+            return '';
+        }
+        $setting = $fieldInfo['setting'];
+        if (!$setting || !isset($setting['table'])) {
+            return '';
+        }
+        //显示的字段
+        $field = $setting['field'];
+        //主键
+        $primarykey = $setting['primarykey'];
+        //主键值
+        $primaryvalue = $key;
+
+        $field = $field ? $field : 'name';
+
+        //如果有primaryvalue,说明当前是初始化传值
+        $where = [$primarykey => ['in', $primaryvalue]];
+
+        $result = [];
+        $datalist = Db::table($setting['table'])->where($where)
+            ->field($primarykey . "," . $field)
+            ->select();
+        foreach ($datalist as $index => &$item) {
+            unset($item['password'], $item['salt']);
+            $result[] = isset($item[$field]) ? $item[$field] : '';
+        }
+        return implode(',', $result);
+    }
+
+    /**
+     * 根据类型获取Model
+     * @param string $type
+     * @param string $source_id
+     * @param array  $with
+     * @return null|\think\Model
+     * @throws Exception
+     */
+    public static function getModelByType($type, $source_id = '', $with = [])
+    {
+        if (!in_array($type, ['page', 'archives', 'special', 'diyform', 'block', 'channel'])) {
+            throw new Exception("未找到指定类型");
+        }
+        $type = ucfirst(strtolower($type));
+        $model = model("\\addons\cms\\model\\{$type}");
+        if (!$model)
+            return null;
+        if ($source_id) {
+            $model = $model->get($source_id, $with);
+        }
+        return $model;
+    }
+
+    /**
+     * 获取缓存标签和时长
+     * @param string $type
+     * @param array  $tag
+     * @return array
+     */
+    public static function getCacheKeyExpire($type, $tag = [])
+    {
+        $config = get_addon_config('cms');
+        $cache = !isset($tag['cache']) ? $config['cachelifetime'] : $tag['cache'];
+        $cache = in_array($cache, ['true', 'false', true, false], true) ? (in_array($cache, ['true', true], true) ? 0 : -1) : (int)$cache;
+        $cacheKey = $cache > -1 ? "cms-taglib-{$type}-" . md5(serialize($tag)) : false;
+        $cacheExpire = $cache > -1 ? $cache : null;
+        return [$cacheKey, $cacheExpire];
+    }
+
+    /**
+     * 获取分页配置参数
+     * @param string $type
+     * @param array  $params
+     * @return array
+     */
+    public static function getPaginateParams($type, $params = [])
+    {
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $paginate = !isset($params['paginate']) ? false : $params['paginate'];
+
+        $paginateArr = explode(',', $paginate);
+        $listRows = is_numeric($paginate) ? $paginate : (is_numeric($paginateArr[0]) ? $paginateArr[0] : $row);
+        $simple = isset($paginateArr[1]) ? $paginateArr[1] : false;
+        $simple = in_array($simple, ['true', 'false', true, false], true) ? (in_array($simple, ['true', true], true) ? true : false) : (int)$simple;
+        $config = [];
+        $config['var_page'] = isset($paginateArr[2]) ? $paginateArr[2] : $type;
+        $config['path'] = isset($paginateArr[3]) ? $paginateArr[3] : '';
+        $config['fragment'] = isset($paginateArr[4]) ? $paginateArr[4] : '';
+        $config['query'] = request()->get();
+        $config['type'] = '\\addons\\cms\\library\\Bootstrap';
+        return [$listRows, $simple, $config];
+    }
+
+    /**
+     * 判断来源是否搜索引擎蜘蛛
+     */
+    public static function isSpider()
+    {
+        $config = get_addon_config('cms');
+        $userAgent = strtolower(request()->server('HTTP_USER_AGENT', ''));
+        $spiders = $config['spiders'] ?? [];
+        foreach ($spiders as $name => $title) {
+            if (stripos($userAgent, $name) !== false) {
+                return $name;
+            }
+        }
+        return '';
+    }
+}

+ 115 - 0
addons/cms/library/VicDict.php

@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * Created by PhpStorm.
+ * User: tanszhe
+ * Date: 2017/12/21
+ * Time: 下午8:16
+ * Url: https://github.com/lizhichao/VicWord
+ */
+
+namespace addons\cms\library;
+
+class VicDict
+{
+    private $word = [];
+    /**
+     * 词典地址
+     * @var string
+     */
+    private $code = 'utf-8';
+
+    private $end = ['\\' => 1];
+
+    private $default_end = ['\\' => 1];
+
+    private $end_key = '\\';
+
+    private $type = 'igb';
+
+    public function __construct($type = 'igb')
+    {
+        $this->type = $type;
+        if (file_exists(_VIC_WORD_DICT_PATH_)) {
+            if ($type == 'igb') {
+                $this->word = igbinary_unserialize(file_get_contents(_VIC_WORD_DICT_PATH_));
+            } else {
+                $this->word = json_decode(file_get_contents(_VIC_WORD_DICT_PATH_), true);
+            }
+        }
+    }
+
+    /**
+     * @param string $word
+     * @param null|string $x 词性
+     * @return bool
+     */
+    public function add($word, $x = null)
+    {
+        $this->end = ['\\x' => $x] + $this->default_end;
+        $word = $this->filter($word);
+        if ($word) {
+            return $this->merge($word);
+        }
+        return false;
+    }
+
+    private function merge($word)
+    {
+        $ar = $this->toArr($word);
+        $br = $ar;
+        $wr = &$this->word;
+        foreach ($ar as $i => $v) {
+            array_shift($br);
+            if (!isset($wr[$v])) {
+                $wr[$v] = $this->dict($br, $this->end);
+                return true;
+            } else {
+                $wr = &$wr[$v];
+            }
+        }
+        if (!isset($wr[$this->end_key])) {
+            foreach ($this->end as $k => $v) {
+                $wr[$k] = $v;
+                $wr[$k] = $v;
+            }
+        }
+        return true;
+    }
+
+    public function save()
+    {
+        if ($this->type == 'igb') {
+            $str = igbinary_serialize($this->word);
+        } else {
+            $str = json_encode($this->word);
+        }
+        return file_put_contents(_VIC_WORD_DICT_PATH_, $str);
+    }
+
+    private function filter($word)
+    {
+        return str_replace(["\n", "\t"], '', trim($word));
+    }
+
+
+    private function dict($arr, $v, $i = 0)
+    {
+        if (isset($arr[$i])) {
+            return [$arr[$i] => $this->dict($arr, $v, $i + 1)];
+        } else {
+            return $v;
+        }
+    }
+
+    private function toArr($str)
+    {
+        $l = mb_strlen($str, $this->code);
+        $r = [];
+        for ($i = 0; $i < $l; $i++) {
+            $r[] = mb_substr($str, $i, 1, $this->code);
+        }
+        return $r;
+    }
+
+}

+ 271 - 0
addons/cms/library/VicWord.php

@@ -0,0 +1,271 @@
+<?php
+
+namespace addons\cms\library;
+/**
+ * Created by PhpStorm.
+ * User: tanszhe
+ * Date: 2017/12/21
+ * Time: 下午8:11
+ * Url: https://github.com/lizhichao/VicWord
+ */
+
+
+class VicWord
+{
+    private $dict = [];
+
+    private $end = '\\';
+
+    private $auto = false;
+
+    private $count = 0;
+
+    /**
+     * @var string 词性
+     */
+    private $x = '\\x';
+
+    public function __construct($type = 'igb')
+    {
+        if (!file_exists(_VIC_WORD_DICT_PATH_)) {
+            return false;
+        }
+        if ($type == 'igb') {
+            $this->dict = igbinary_unserialize(file_get_contents(_VIC_WORD_DICT_PATH_));
+        } else {
+            $this->dict = json_decode(file_get_contents(_VIC_WORD_DICT_PATH_), true);
+        }
+    }
+
+    /**
+     * @param string $path
+     */
+    public function getWord($str)
+    {
+        $this->auto = false;
+        $str = $this->filter($str);
+        return $this->find($str);
+    }
+
+    /**
+     * @param string $path
+     */
+    public function getShortWord($str)
+    {
+        $this->auto = false;
+        $str = $this->filter($str);
+        return $this->shortfind($str);
+    }
+
+    /**
+     * @param string $path
+     */
+    public function getAutoWord($str)
+    {
+        $this->auto = true;
+        $str = $this->filter($str);
+        return $this->autoFind($str, ['long' => 1]);
+    }
+
+
+    private function filter($str)
+    {
+        return strtolower(trim($str));
+    }
+
+
+    private function getD(&$str, $i)
+    {
+        $o = ord($str[$i]);
+        if ($o < 128) {
+            $d = $str[$i];
+        } else {
+            $o = $o >> 4;
+            if ($o == 12) {
+                $d = $str[$i] . $str[++$i];
+            } elseif ($o === 14) {
+                $d = $str[$i] . $str[++$i] . $str[++$i];
+            } elseif ($o == 15) {
+                $d = $str[$i] . $str[++$i] . $str[++$i] . $str[++$i];
+            } else {
+                exit('我不认识的编码');
+            }
+        }
+        return [$d, $i];
+    }
+
+    private function autoFind($str, $auto_info = [])
+    {
+        if ($auto_info['long']) {
+            return $this->find($str, $auto_info);
+        } else {
+            return $this->shortfind($str, $auto_info);
+        }
+    }
+
+    private function reGet(&$r, $auto_info)
+    {
+        $auto_info['c'] = isset($auto_info['c']) ? $auto_info['c']++ : 1;
+        $l = count($r) - 1;
+        $p = [];
+        $str = '';
+        for ($i = $l; $i >= 0; $i--) {
+            $str = $r[$i][0] . $str;
+            $f = $r[$i][3];
+            array_unshift($p, $r[$i]);
+            unset($r[$i]);
+            if ($f == 1) {
+                break;
+            }
+        }
+        $this->count++;
+        $l = strlen($str);
+        if (isset($r[$i - 1])) {
+            $w = $r[$i - 1][1];
+        } else {
+            $w = 0;
+        }
+        if (isset($auto_info['pl']) && $l == $auto_info['pl']) {
+            $r = $p;
+            return false;
+        } elseif ($str && $auto_info['c'] < 3) {
+            $auto_info['pl'] = $l;
+            $auto_info['long'] = !$auto_info['long'];
+            $sr = $this->autoFind($str, $auto_info);
+            $sr = array_map(function ($v) use ($w) {
+                $v[1] += $w;
+                return $v;
+            }, $sr);
+            $r = array_merge($r, $this->getGoodWord($p, $sr));
+        }
+    }
+
+    private function getGoodWord($old, $new)
+    {
+        if (!$new) {
+            return $old;
+        }
+        if ($this->getUnknowCount($old) > $this->getUnknowCount($new)) {
+            return $new;
+        } else {
+            return $old;
+        }
+    }
+
+    private function getUnknowCount($ar)
+    {
+        $i = 0;
+        foreach ($ar as $v) {
+            if ($v[3] == 0) {
+                $i += strlen($v[0]);
+            }
+        }
+        return $i;
+    }
+
+
+    private function find($str, $auto_info = [])
+    {
+        $len = strlen($str);
+        $s = '';
+        $n = '';
+        $j = 0;
+        $r = [];
+        for ($i = 0; $i < $len; $i++) {
+            list($d, $i) = $this->getD($str, $i);
+
+            if (isset($wr[$d])) {
+                $s .= $d;
+                $wr = $wr[$d];
+            } else {
+                if (isset($wr[$this->end])) {
+                    $this->addNotFind($r, $n, $s, $j, $auto_info);
+                    $this->addResult($r, $s, $j, $wr[$this->x]);
+                    $n = '';
+                }
+                $wr = $this->dict;
+                if (isset($wr[$d])) {
+                    $s = $d;
+                    $wr = $wr[$d];
+                } else {
+                    $s = '';
+                }
+            }
+            $n .= $d;
+            $j = $i;
+        }
+        if (isset($wr[$this->end])) {
+            $this->addNotFind($r, $n, $s, $i, $auto_info);
+            $this->addResult($r, $s, $i, $wr[$this->x]);
+        } else {
+            $this->addNotFind($r, $n, '', $i, $auto_info);
+        }
+
+        return $r;
+    }
+
+
+    private function addNotFind(&$r, $n, $s, $i, $auto_info = [])
+    {
+        if ($n !== $s) {
+            $n = str_replace($s, '', $n);
+            $this->addResult($r, $n, $i - strlen($s), null, 0);
+            if ($this->auto) {
+                $this->reGet($r, $auto_info);
+            }
+        }
+    }
+
+
+    private function shortFind($str, $auto_info = [])
+    {
+        $len = strlen($str);
+        $s = '';
+        $n = '';
+        $r = [];
+        for ($i = 0; $i < $len; $i++) {
+            $j = $i;
+            list($d, $i) = $this->getD($str, $i);
+
+            if (isset($wr[$d])) {
+                $s .= $d;
+                $wr = $wr[$d];
+            } else {
+                if (isset($wr[$this->end])) {
+                    $this->addNotFind($r, $n, $s, $j, $auto_info);
+                    $this->addResult($r, $s, $j, $wr[$this->x]);
+                    $n = '';
+                }
+                $wr = $this->dict;
+                if (isset($wr[$d])) {
+                    $s = $d;
+                    $wr = $wr[$d];
+                } else {
+                    $s = '';
+                }
+            }
+
+            $n .= $d;
+
+            if (isset($wr[$this->end])) {
+                $this->addNotFind($r, $n, $s, $i, $auto_info);
+                $this->addResult($r, $s, $i, $wr[$this->x]);
+                $wr = $this->dict;
+                $s = '';
+                $n = '';
+            }
+        }
+        if (isset($wr[$this->end])) {
+            $this->addNotFind($r, $n, $s, $i, $auto_info);
+            $this->addResult($r, $s, $i, $wr[$this->x]);
+        } else {
+            $this->addNotFind($r, $n, '', $i, $auto_info);
+        }
+        return $r;
+    }
+
+    private function addResult(&$r, $k, $i, $x, $find = 1)
+    {
+        $r[] = [$k, $i, $x, $find];
+    }
+}

+ 27 - 0
addons/cms/library/aip/AipContentCensor.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace addons\cms\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* 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.
+*/
+
+/**
+ * 内容审核
+ */
+class AipContentCensor extends AipImageCensor
+{
+
+}

+ 74 - 0
addons/cms/library/aip/AipImageCensor.php

@@ -0,0 +1,74 @@
+<?php
+
+namespace addons\cms\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* 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.
+*/
+
+
+use addons\cms\library\aip\lib\AipBase;
+
+/**
+ * 黄反识别
+ */
+class AipImageCensor extends AipBase
+{
+
+    /**
+     * @var string
+     */
+    private $imageCensorUserDefinedUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/img_censor/v2/user_defined';
+
+    /**
+     * @var string
+     */
+    private $textCensorUserDefinedUrl = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined';
+
+
+    /**
+     * @param string $image 图像
+     * @return array
+     */
+    public function imageCensorUserDefined($image)
+    {
+
+        $data = array();
+
+        $isUrl = substr(trim($image), 0, 4) === 'http';
+        if (!$isUrl) {
+            $data['image'] = base64_encode($image);
+        } else {
+            $data['imgUrl'] = $image;
+        }
+
+        return $this->request($this->imageCensorUserDefinedUrl, $data);
+    }
+
+    /**
+     * @param string $text
+     * @return array
+     */
+    public function textCensorUserDefined($text)
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        return $this->request($this->textCensorUserDefinedUrl, $data);
+    }
+
+}

+ 456 - 0
addons/cms/library/aip/AipNlp.php

@@ -0,0 +1,456 @@
+<?php
+
+namespace addons\cms\library\aip;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* 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.
+*/
+
+use addons\cms\library\aip\lib\AipBase;
+
+class AipNlp extends AipBase
+{
+
+    /**
+     * 词法分析 lexer api url
+     * @var string
+     */
+    private $lexerUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/lexer';
+
+    /**
+     * 词法分析(定制版) lexer_custom api url
+     * @var string
+     */
+    private $lexerCustomUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/lexer_custom';
+
+    /**
+     * 依存句法分析 dep_parser api url
+     * @var string
+     */
+    private $depParserUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/depparser';
+
+    /**
+     * 词向量表示 word_embedding api url
+     * @var string
+     */
+    private $wordEmbeddingUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/word_emb_vec';
+
+    /**
+     * DNN语言模型 dnnlm_cn api url
+     * @var string
+     */
+    private $dnnlmCnUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/dnnlm_cn';
+
+    /**
+     * 词义相似度 word_sim_embedding api url
+     * @var string
+     */
+    private $wordSimEmbeddingUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/word_emb_sim';
+
+    /**
+     * 短文本相似度 simnet api url
+     * @var string
+     */
+    private $simnetUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/simnet';
+
+    /**
+     * 评论观点抽取 comment_tag api url
+     * @var string
+     */
+    private $commentTagUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v2/comment_tag';
+
+    /**
+     * 情感倾向分析 sentiment_classify api url
+     * @var string
+     */
+    private $sentimentClassifyUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/sentiment_classify';
+
+    /**
+     * 文章标签 keyword api url
+     * @var string
+     */
+    private $keywordUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/keyword';
+
+    /**
+     * 文章分类 topic api url
+     * @var string
+     */
+    private $topicUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/topic';
+
+    /**
+     * 文本纠错 ecnet api url
+     * @var string
+     */
+    private $ecnetUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/ecnet';
+
+    /**
+     * 对话情绪识别接口 emotion api url
+     * @var string
+     */
+    private $emotionUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/emotion';
+
+    /**
+     * 新闻摘要接口 news_summary api url
+     * @var string
+     */
+    private $newsSummaryUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/news_summary';
+
+    /**
+     * 地址识别接口 address api url
+     * @var string
+     */
+    private $addressUrl = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/address';
+
+    /**
+     * 格式化结果
+     * @param $content string
+     * @return mixed
+     */
+    protected function proccessResult($content)
+    {
+        return json_decode(mb_convert_encoding($content, 'UTF8', 'GBK'), true, 512, JSON_BIGINT_AS_STRING);
+    }
+
+    /**
+     * 词法分析接口
+     *
+     * @param string $text    - 待分析文本(目前仅支持GBK编码),长度不超过65536字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function lexer($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->lexerUrl, $data);
+    }
+
+    /**
+     * 词法分析(定制版)接口
+     *
+     * @param string $text    - 待分析文本(目前仅支持GBK编码),长度不超过65536字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function lexerCustom($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->lexerCustomUrl, $data);
+    }
+
+    /**
+     * 依存句法分析接口
+     *
+     * @param string $text    - 待分析文本(目前仅支持GBK编码),长度不超过256字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        mode 模型选择。默认值为0,可选值mode=0(对应web模型);mode=1(对应query模型)
+     * @return array
+     */
+    public function depParser($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->depParserUrl, $data);
+    }
+
+    /**
+     * 词向量表示接口
+     *
+     * @param string $word    - 文本内容(GBK编码),最大64字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function wordEmbedding($word, $options = array())
+    {
+
+        $data = array();
+
+        $data['word'] = $word;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->wordEmbeddingUrl, $data);
+    }
+
+    /**
+     * DNN语言模型接口
+     *
+     * @param string $text    - 文本内容(GBK编码),最大512字节,不需要切词
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function dnnlm($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->dnnlmCnUrl, $data);
+    }
+
+    /**
+     * 词义相似度接口
+     *
+     * @param string $word1   - 词1(GBK编码),最大64字节
+     * @param string $word2   - 词1(GBK编码),最大64字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        mode 预留字段,可选择不同的词义相似度模型。默认值为0,目前仅支持mode=0
+     * @return array
+     */
+    public function wordSimEmbedding($word1, $word2, $options = array())
+    {
+
+        $data = array();
+
+        $data['word_1'] = $word1;
+        $data['word_2'] = $word2;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->wordSimEmbeddingUrl, $data);
+    }
+
+    /**
+     * 短文本相似度接口
+     *
+     * @param string $text1   - 待比较文本1(GBK编码),最大512字节
+     * @param string $text2   - 待比较文本2(GBK编码),最大512字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        model 默认为"BOW",可选"BOW"、"CNN"与"GRNN"
+     * @return array
+     */
+    public function simnet($text1, $text2, $options = array())
+    {
+
+        $data = array();
+
+        $data['text_1'] = $text1;
+        $data['text_2'] = $text2;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->simnetUrl, $data);
+    }
+
+    /**
+     * 评论观点抽取接口
+     *
+     * @param string $text    - 评论内容(GBK编码),最大10240字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        type 评论行业类型,默认为4(餐饮美食)
+     * @return array
+     */
+    public function commentTag($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->commentTagUrl, $data);
+    }
+
+    /**
+     * 情感倾向分析接口
+     *
+     * @param string $text    - 文本内容(GBK编码),最大102400字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function sentimentClassify($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->sentimentClassifyUrl, $data);
+    }
+
+    /**
+     * 文章标签接口
+     *
+     * @param string $title   - 篇章的标题,最大80字节
+     * @param string $content - 篇章的正文,最大65535字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function keyword($title, $content, $options = array())
+    {
+
+        $data = array();
+
+        $data['title'] = $title;
+        $data['content'] = $content;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->keywordUrl, $data);
+    }
+
+    /**
+     * 文章分类接口
+     *
+     * @param string $title   - 篇章的标题,最大80字节
+     * @param string $content - 篇章的正文,最大65535字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function topic($title, $content, $options = array())
+    {
+
+        $data = array();
+
+        $data['title'] = $title;
+        $data['content'] = $content;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->topicUrl, $data);
+    }
+
+    /**
+     * 文本纠错接口
+     *
+     * @param string $text    - 待纠错文本,输入限制511字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function ecnet($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->ecnetUrl, $data);
+    }
+
+    /**
+     * 对话情绪识别接口接口
+     *
+     * @param string $text    - 待识别情感文本,输入限制512字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                        scene default(默认项-不区分场景),talk(闲聊对话-如度秘聊天等),task(任务型对话-如导航对话等),customer_service(客服对话-如电信/银行客服等)
+     * @return array
+     */
+    public function emotion($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->emotionUrl, $data);
+    }
+
+    /**
+     * 新闻摘要接口接口
+     *
+     * @param string  $content       - 字符串(限3000字符数以内)字符串仅支持GBK编码,长度需小于3000字符数(即6000字节),请输入前确认字符数没有超限,若字符数超长会返回错误。正文中如果包含段落信息,请使用"\n"分隔,段落信息算法中有重要的作用,请尽量保留
+     * @param integer $maxSummaryLen - 此数值将作为摘要结果的最大长度。例如:原文长度1000字,本参数设置为150,则摘要结果的最大长度是150字;推荐最优区间:200-500字
+     * @param array   $options       - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     *                               title 字符串(限200字符数)字符串仅支持GBK编码,长度需小于200字符数(即400字节),请输入前确认字符数没有超限,若字符数超长会返回错误。标题在算法中具有重要的作用,若文章确无标题,输入参数的“标题”字段为空即可
+     * @return array
+     */
+    public function newsSummary($content, $maxSummaryLen, $options = array())
+    {
+
+        $data = array();
+
+        $data['content'] = $content;
+        $data['max_summary_len'] = $maxSummaryLen;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->newsSummaryUrl, $data);
+    }
+
+    /**
+     * 地址识别接口接口
+     *
+     * @param string $text    - 待识别的文本内容,不超过1000字节
+     * @param array  $options - 可选参数对象,key: value都为string类型
+     * @description options列表:
+     * @return array
+     */
+    public function address($text, $options = array())
+    {
+
+        $data = array();
+
+        $data['text'] = $text;
+
+        $data = array_merge($data, $options);
+        $data = mb_convert_encoding(json_encode($data), 'GBK', 'UTF8');
+
+        return $this->request($this->addressUrl, $data);
+    }
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1155 - 0
addons/cms/library/aip/AipOcr.php


+ 401 - 0
addons/cms/library/aip/lib/AipBase.php

@@ -0,0 +1,401 @@
+<?php
+
+namespace addons\cms\library\aip\lib;
+
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* 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.
+*/
+
+use Exception;
+
+/**
+ * Aip Base 基类
+ */
+class AipBase
+{
+
+    /**
+     * 获取access token url
+     * @var string
+     */
+    protected $accessTokenUrl = 'https://aip.baidubce.com/oauth/2.0/token';
+
+    /**
+     * 反馈接口
+     * @var string
+     */
+    protected $reportUrl = 'https://aip.baidubce.com/rpc/2.0/feedback/v1/report';
+
+    /**
+     * appId
+     * @var string
+     */
+    protected $appId = '';
+
+    /**
+     * apiKey
+     * @var string
+     */
+    protected $apiKey = '';
+
+    /**
+     * secretKey
+     * @var string
+     */
+    protected $secretKey = '';
+
+    /**
+     * 权限
+     * @var array
+     */
+    protected $scope = 'brain_all_scope';
+
+    /**
+     * @param string $appId
+     * @param string $apiKey
+     * @param string $secretKey
+     */
+    public function __construct($appId, $apiKey, $secretKey)
+    {
+        $this->appId = trim($appId);
+        $this->apiKey = trim($apiKey);
+        $this->secretKey = trim($secretKey);
+        $this->isCloudUser = null;
+        $this->client = new AipHttpClient();
+        $this->version = '2_2_2';
+        $this->proxies = array();
+    }
+
+    /**
+     * 查看版本
+     * @return string
+     *
+     */
+    public function getVersion()
+    {
+        return $this->version;
+    }
+
+    /**
+     * 连接超时
+     * @param int $ms 毫秒
+     */
+    public function setConnectionTimeoutInMillis($ms)
+    {
+        $this->client->setConnectionTimeoutInMillis($ms);
+    }
+
+    /**
+     * 响应超时
+     * @param int $ms 毫秒
+     */
+    public function setSocketTimeoutInMillis($ms)
+    {
+        $this->client->setSocketTimeoutInMillis($ms);
+    }
+
+    /**
+     * 代理
+     * @param array $proxy
+     * @return string
+     *
+     */
+    public function setProxies($proxies)
+    {
+        $this->client->setConf($proxies);
+    }
+
+    /**
+     * 处理请求参数
+     * @param  string $url
+     * @param array   $params
+     * @param array   $data
+     * @param array   $headers
+     */
+    protected function proccessRequest($url, &$params, &$data, $headers)
+    {
+        $params['aipSdk'] = 'php';
+        $params['aipSdkVersion'] = $this->version;
+    }
+
+    /**
+     * Api 请求
+     * @param  string $url
+     * @param  mixed  $data
+     * @return mixed
+     */
+    protected function request($url, $data, $headers = array())
+    {
+        try {
+            $result = $this->validate($url, $data);
+            if ($result !== true) {
+                return $result;
+            }
+
+            $params = array();
+            $authObj = $this->auth();
+
+            if ($this->isCloudUser === false) {
+                $params['access_token'] = $authObj['access_token'];
+            }
+
+            // 特殊处理
+            $this->proccessRequest($url, $params, $data, $headers);
+
+            $headers = $this->getAuthHeaders('POST', $url, $params, $headers);
+            $response = $this->client->post($url, $data, $params, $headers);
+
+            $obj = $this->proccessResult($response['content']);
+
+            if (!$this->isCloudUser && isset($obj['error_code']) && $obj['error_code'] == 110) {
+                $authObj = $this->auth(true);
+                $params['access_token'] = $authObj['access_token'];
+                $response = $this->client->post($url, $data, $params, $headers);
+                $obj = $this->proccessResult($response['content']);
+            }
+
+            if (empty($obj) || !isset($obj['error_code'])) {
+                $this->writeAuthObj($authObj);
+            }
+        } catch (Exception $e) {
+            return array(
+                'error_code' => 'SDK108',
+                'error_msg'  => 'connection or read data timeout',
+            );
+        }
+
+        return $obj;
+    }
+
+    /**
+     * Api 多个并发请求
+     * @param  string $url
+     * @param  mixed  $data
+     * @return mixed
+     */
+    protected function multi_request($url, $data)
+    {
+        try {
+            $params = array();
+            $authObj = $this->auth();
+            $headers = $this->getAuthHeaders('POST', $url);
+
+            if ($this->isCloudUser === false) {
+                $params['access_token'] = $authObj['access_token'];
+            }
+
+            $responses = $this->client->multi_post($url, $data, $params, $headers);
+
+            $is_success = false;
+            foreach ($responses as $response) {
+                $obj = $this->proccessResult($response['content']);
+
+                if (empty($obj) || !isset($obj['error_code'])) {
+                    $is_success = true;
+                }
+
+                if (!$this->isCloudUser && isset($obj['error_code']) && $obj['error_code'] == 110) {
+                    $authObj = $this->auth(true);
+                    $params['access_token'] = $authObj['access_token'];
+                    $responses = $this->client->post($url, $data, $params, $headers);
+                    break;
+                }
+            }
+
+            if ($is_success) {
+                $this->writeAuthObj($authObj);
+            }
+
+            $objs = array();
+            foreach ($responses as $response) {
+                $objs[] = $this->proccessResult($response['content']);
+            }
+
+        } catch (Exception $e) {
+            return array(
+                'error_code' => 'SDK108',
+                'error_msg'  => 'connection or read data timeout',
+            );
+        }
+
+        return $objs;
+    }
+
+    /**
+     * 格式检查
+     * @param  string $url
+     * @param  array  $data
+     * @return mix
+     */
+    protected function validate($url, &$data)
+    {
+        return true;
+    }
+
+    /**
+     * 格式化结果
+     * @param $content string
+     * @return mixed
+     */
+    protected function proccessResult($content)
+    {
+        return json_decode($content, true);
+    }
+
+    /**
+     * 返回 access token 路径
+     * @return string
+     */
+    private function getAuthFilePath()
+    {
+        return dirname(__FILE__) . DIRECTORY_SEPARATOR . md5($this->apiKey);
+    }
+
+    /**
+     * 写入本地文件
+     * @param  array $obj
+     * @return void
+     */
+    private function writeAuthObj($obj)
+    {
+        if ($obj === null || (isset($obj['is_read']) && $obj['is_read'] === true)) {
+            return;
+        }
+
+        $obj['time'] = time();
+        $obj['is_cloud_user'] = $this->isCloudUser;
+        @file_put_contents($this->getAuthFilePath(), json_encode($obj));
+    }
+
+    /**
+     * 读取本地缓存
+     * @return array
+     */
+    private function readAuthObj()
+    {
+        $content = @file_get_contents($this->getAuthFilePath());
+        if ($content !== false) {
+            $obj = json_decode($content, true);
+            $this->isCloudUser = $obj['is_cloud_user'];
+            $obj['is_read'] = true;
+            if ($this->isCloudUser || $obj['time'] + $obj['expires_in'] - 30 > time()) {
+                return $obj;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 认证
+     * @param bool $refresh 是否刷新
+     * @return array
+     */
+    private function auth($refresh = false)
+    {
+
+        //非过期刷新
+        if (!$refresh) {
+            $obj = $this->readAuthObj();
+            if (!empty($obj)) {
+                return $obj;
+            }
+        }
+
+        $response = $this->client->get($this->accessTokenUrl, array(
+            'grant_type'    => 'client_credentials',
+            'client_id'     => $this->apiKey,
+            'client_secret' => $this->secretKey,
+        ));
+
+        $obj = json_decode($response['content'], true);
+
+        $this->isCloudUser = !$this->isPermission($obj);
+        return $obj;
+    }
+
+    /**
+     * 判断认证是否有权限
+     * @param  array $authObj
+     * @return boolean
+     */
+    protected function isPermission($authObj)
+    {
+        if (empty($authObj) || !isset($authObj['scope'])) {
+            return false;
+        }
+
+        $scopes = explode(' ', $authObj['scope']);
+
+        return in_array($this->scope, $scopes);
+    }
+
+    /**
+     * @param  string $method HTTP method
+     * @param  string $url
+     * @param  array  $param  参数
+     * @return array
+     */
+    private function getAuthHeaders($method, $url, $params = array(), $headers = array())
+    {
+
+        //不是云的老用户则不用在header中签名 认证
+        if ($this->isCloudUser === false) {
+            return $headers;
+        }
+
+        $obj = parse_url($url);
+        if (!empty($obj['query'])) {
+            foreach (explode('&', $obj['query']) as $kv) {
+                if (!empty($kv)) {
+                    list($k, $v) = explode('=', $kv, 2);
+                    $params[$k] = $v;
+                }
+            }
+        }
+
+        //UTC 时间戳
+        $timestamp = gmdate('Y-m-d\TH:i:s\Z');
+        $headers['Host'] = isset($obj['port']) ? sprintf('%s:%s', $obj['host'], $obj['port']) : $obj['host'];
+        $headers['x-bce-date'] = $timestamp;
+
+        //签名
+        $headers['authorization'] = AipSampleSigner::sign(array(
+            'ak' => $this->apiKey,
+            'sk' => $this->secretKey,
+        ), $method, $obj['path'], $headers, $params, array(
+            'timestamp'     => $timestamp,
+            'headersToSign' => array_keys($headers),
+        ));
+
+        return $headers;
+    }
+
+    /**
+     * 反馈
+     *
+     * @param array $feedbacks
+     * @return array
+     */
+    public function report($feedback)
+    {
+
+        $data = array();
+
+        $data['feedback'] = $feedback;
+
+        return $this->request($this->reportUrl, $data);
+    }
+}

+ 227 - 0
addons/cms/library/aip/lib/AipHttpClient.php

@@ -0,0 +1,227 @@
+<?php
+
+namespace addons\cms\library\aip\lib;
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* 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.
+*/
+
+/**
+ * Http Client
+ */
+class AipHttpClient
+{
+
+    /**
+     * HttpClient
+     * @param array $headers HTTP header
+     */
+    public function __construct($headers = array())
+    {
+        $this->headers = $this->buildHeaders($headers);
+        $this->connectTimeout = 60000;
+        $this->socketTimeout = 60000;
+        $this->conf = array();
+    }
+
+    /**
+     * 连接超时
+     * @param int $ms 毫秒
+     */
+    public function setConnectionTimeoutInMillis($ms)
+    {
+        $this->connectTimeout = $ms;
+    }
+
+    /**
+     * 响应超时
+     * @param int $ms 毫秒
+     */
+    public function setSocketTimeoutInMillis($ms)
+    {
+        $this->socketTimeout = $ms;
+    }
+
+    /**
+     * 配置
+     * @param array $conf
+     */
+    public function setConf($conf)
+    {
+        $this->conf = $conf;
+    }
+
+    /**
+     * 请求预处理
+     * @param resource $ch
+     */
+    public function prepare($ch)
+    {
+        foreach ($this->conf as $key => $value) {
+            curl_setopt($ch, $key, $value);
+        }
+    }
+
+    /**
+     * @param  string $url
+     * @param  array  $data    HTTP POST BODY
+     * @param  array  $param   HTTP URL
+     * @param  array  $headers HTTP header
+     * @return array
+     */
+    public function post($url, $data = array(), $params = array(), $headers = array())
+    {
+        $url = $this->buildUrl($url, $params);
+        $headers = array_merge($this->headers, $this->buildHeaders($headers));
+
+        $ch = curl_init();
+        $this->prepare($ch);
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_POST, 1);
+        curl_setopt($ch, CURLOPT_HEADER, false);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data);
+        curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->socketTimeout);
+        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->connectTimeout);
+        $content = curl_exec($ch);
+        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+        if ($code === 0) {
+            throw new Exception(curl_error($ch));
+        }
+
+        curl_close($ch);
+        return array(
+            'code'    => $code,
+            'content' => $content,
+        );
+    }
+
+    /**
+     * @param  string $url
+     * @param  array  $datas   HTTP POST BODY
+     * @param  array  $param   HTTP URL
+     * @param  array  $headers HTTP header
+     * @return array
+     */
+    public function multi_post($url, $datas = array(), $params = array(), $headers = array())
+    {
+        $url = $this->buildUrl($url, $params);
+        $headers = array_merge($this->headers, $this->buildHeaders($headers));
+
+        $chs = array();
+        $result = array();
+        $mh = curl_multi_init();
+        foreach ($datas as $data) {
+            $ch = curl_init();
+            $chs[] = $ch;
+            $this->prepare($ch);
+            curl_setopt($ch, CURLOPT_URL, $url);
+            curl_setopt($ch, CURLOPT_POST, 1);
+            curl_setopt($ch, CURLOPT_HEADER, false);
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+            curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data);
+            curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->socketTimeout);
+            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->connectTimeout);
+            curl_multi_add_handle($mh, $ch);
+        }
+
+        $running = null;
+        do {
+            curl_multi_exec($mh, $running);
+            usleep(100);
+        } while ($running);
+
+        foreach ($chs as $ch) {
+            $content = curl_multi_getcontent($ch);
+            $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+            $result[] = array(
+                'code'    => $code,
+                'content' => $content,
+            );
+            curl_multi_remove_handle($mh, $ch);
+        }
+        curl_multi_close($mh);
+
+        return $result;
+    }
+
+    /**
+     * @param  string $url
+     * @param  array  $param   HTTP URL
+     * @param  array  $headers HTTP header
+     * @return array
+     */
+    public function get($url, $params = array(), $headers = array())
+    {
+        $url = $this->buildUrl($url, $params);
+        $headers = array_merge($this->headers, $this->buildHeaders($headers));
+
+        $ch = curl_init();
+        $this->prepare($ch);
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_HEADER, false);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+        curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->socketTimeout);
+        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->connectTimeout);
+        $content = curl_exec($ch);
+        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+        if ($code === 0) {
+            throw new Exception(curl_error($ch));
+        }
+
+        curl_close($ch);
+        return array(
+            'code'    => $code,
+            'content' => $content,
+        );
+    }
+
+    /**
+     * 构造 header
+     * @param  array $headers
+     * @return array
+     */
+    private function buildHeaders($headers)
+    {
+        $result = array();
+        foreach ($headers as $k => $v) {
+            $result[] = sprintf('%s:%s', $k, $v);
+        }
+        return $result;
+    }
+
+    /**
+     *
+     * @param  string $url
+     * @param  array  $params 参数
+     * @return string
+     */
+    private function buildUrl($url, $params)
+    {
+        if (!empty($params)) {
+            $str = http_build_query($params);
+            return $url . (strpos($url, '?') === false ? '?' : '&') . $str;
+        } else {
+            return $url;
+        }
+    }
+}

+ 181 - 0
addons/cms/library/aip/lib/AipHttpUtil.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace addons\cms\library\aip\lib;
+/*
+* Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
+*
+* 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.
+*/
+
+/**
+ * BCE Util
+ */
+class AipHttpUtil
+{
+    // 根据RFC 3986,除了:
+    //   1.大小写英文字符
+    //   2.阿拉伯数字
+    //   3.点'.'、波浪线'~'、减号'-'以及下划线'_'
+    // 以外都要编码
+    public static $PERCENT_ENCODED_STRINGS;
+
+    //填充编码数组
+    public static function __init()
+    {
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS = array();
+        for ($i = 0; $i < 256; ++$i) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[$i] = sprintf("%%%02X", $i);
+        }
+
+        //a-z不编码
+        foreach (range('a', 'z') as $ch) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($ch)] = $ch;
+        }
+
+        //A-Z不编码
+        foreach (range('A', 'Z') as $ch) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($ch)] = $ch;
+        }
+
+        //0-9不编码
+        foreach (range('0', '9') as $ch) {
+            AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($ch)] = $ch;
+        }
+
+        //以下4个字符不编码
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('-')] = '-';
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('.')] = '.';
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('_')] = '_';
+        AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord('~')] = '~';
+    }
+
+    /**
+     * 在uri编码中不能对'/'编码
+     * @param  string $path
+     * @return string
+     */
+    public static function urlEncodeExceptSlash($path)
+    {
+        return str_replace("%2F", "/", AipHttpUtil::urlEncode($path));
+    }
+
+    /**
+     * 使用编码数组编码
+     * @param  string $path
+     * @return string
+     */
+    public static function urlEncode($value)
+    {
+        $result = '';
+        for ($i = 0; $i < strlen($value); ++$i) {
+            $result .= AipHttpUtil::$PERCENT_ENCODED_STRINGS[ord($value[$i])];
+        }
+        return $result;
+    }
+
+    /**
+     * 生成标准化QueryString
+     * @param  array $parameters
+     * @return array
+     */
+    public static function getCanonicalQueryString(array $parameters)
+    {
+        //没有参数,直接返回空串
+        if (count($parameters) == 0) {
+            return '';
+        }
+
+        $parameterStrings = array();
+        foreach ($parameters as $k => $v) {
+            //跳过Authorization字段
+            if (strcasecmp('Authorization', $k) == 0) {
+                continue;
+            }
+            if (!isset($k)) {
+                throw new \InvalidArgumentException(
+                    "parameter key should not be null"
+                );
+            }
+            if (isset($v)) {
+                //对于有值的,编码后放在=号两边
+                $parameterStrings[] = AipHttpUtil::urlEncode($k)
+                    . '=' . AipHttpUtil::urlEncode((string)$v);
+            } else {
+                //对于没有值的,只将key编码后放在=号的左边,右边留空
+                $parameterStrings[] = AipHttpUtil::urlEncode($k) . '=';
+            }
+        }
+        //按照字典序排序
+        sort($parameterStrings);
+
+        //使用'&'符号连接它们
+        return implode('&', $parameterStrings);
+    }
+
+    /**
+     * 生成标准化uri
+     * @param  string $path
+     * @return string
+     */
+    public static function getCanonicalURIPath($path)
+    {
+        //空路径设置为'/'
+        if (empty($path)) {
+            return '/';
+        } else {
+            //所有的uri必须以'/'开头
+            if ($path[0] == '/') {
+                return AipHttpUtil::urlEncodeExceptSlash($path);
+            } else {
+                return '/' . AipHttpUtil::urlEncodeExceptSlash($path);
+            }
+        }
+    }
+
+    /**
+     * 生成标准化http请求头串
+     * @param  array $headers
+     * @return array
+     */
+    public static function getCanonicalHeaders($headers)
+    {
+        //如果没有headers,则返回空串
+        if (count($headers) == 0) {
+            return '';
+        }
+
+        $headerStrings = array();
+        foreach ($headers as $k => $v) {
+            //跳过key为null的
+            if ($k === null) {
+                continue;
+            }
+            //如果value为null,则赋值为空串
+            if ($v === null) {
+                $v = '';
+            }
+            //trim后再encode,之后使用':'号连接起来
+            $headerStrings[] = AipHttpUtil::urlEncode(strtolower(trim($k))) . ':' . AipHttpUtil::urlEncode(trim($v));
+        }
+        //字典序排序
+        sort($headerStrings);
+
+        //用'\n'把它们连接起来
+        return implode("\n", $headerStrings);
+    }
+}
+
+AipHttpUtil::__init();
+
+
+

+ 181 - 0
addons/cms/library/aip/lib/AipSampleSigner.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace addons\cms\library\aip\lib;
+
+
+class AipSampleSigner
+{
+
+    const BCE_AUTH_VERSION = "bce-auth-v1";
+    const BCE_PREFIX = 'x-bce-';
+
+    //不指定headersToSign情况下,默认签名http头,包括:
+    //    1.host
+    //    2.content-length
+    //    3.content-type
+    //    4.content-md5
+    public static $defaultHeadersToSign;
+
+    public static function __init()
+    {
+        AipSampleSigner::$defaultHeadersToSign = array(
+            "host",
+            "content-length",
+            "content-type",
+            "content-md5",
+        );
+    }
+
+    /**
+     * 签名
+     * @param  array  $credentials
+     * @param  string $httpMethod
+     * @param  string $path
+     * @param  array  $headers
+     * @param  string $params
+     * @param  array  $options
+     * @return string
+     */
+    public static function sign(
+        array $credentials,
+        $httpMethod,
+        $path,
+        $headers,
+        $params,
+        $options = array()
+    )
+    {
+        //设定签名有效时间
+        if (!isset($options[AipSignOption::EXPIRATION_IN_SECONDS])) {
+            //默认值1800秒
+            $expirationInSeconds = AipSignOption::DEFAULT_EXPIRATION_IN_SECONDS;
+        } else {
+            $expirationInSeconds = $options[AipSignOption::EXPIRATION_IN_SECONDS];
+        }
+
+        //解析ak sk
+        $accessKeyId = $credentials['ak'];
+        $secretAccessKey = $credentials['sk'];
+
+        //设定时间戳,注意:如果自行指定时间戳需要为UTC时间
+        if (!isset($options[AipSignOption::TIMESTAMP])) {
+            //默认值当前时间
+            $timestamp = gmdate('Y-m-d\TH:i:s\Z');
+        } else {
+            $timestamp = $options[AipSignOption::TIMESTAMP];
+        }
+
+        //生成authString
+        $authString = AipSampleSigner::BCE_AUTH_VERSION . '/' . $accessKeyId . '/'
+            . $timestamp . '/' . $expirationInSeconds;
+
+        //使用sk和authString生成signKey
+        $signingKey = hash_hmac('sha256', $authString, $secretAccessKey);
+
+        //生成标准化URI
+        $canonicalURI = AipHttpUtil::getCanonicalURIPath($path);
+
+        //生成标准化QueryString
+        $canonicalQueryString = AipHttpUtil::getCanonicalQueryString($params);
+
+        //填充headersToSign,也就是指明哪些header参与签名
+        $headersToSign = null;
+        if (isset($options[AipSignOption::HEADERS_TO_SIGN])) {
+            $headersToSign = $options[AipSignOption::HEADERS_TO_SIGN];
+        }
+
+        //生成标准化header
+        $canonicalHeader = AipHttpUtil::getCanonicalHeaders(
+            AipSampleSigner::getHeadersToSign($headers, $headersToSign)
+        );
+
+        //整理headersToSign,以';'号连接
+        $signedHeaders = '';
+        if ($headersToSign !== null) {
+            $signedHeaders = strtolower(
+                trim(implode(";", $headersToSign))
+            );
+        }
+
+        //组成标准请求串
+        $canonicalRequest = "$httpMethod\n$canonicalURI\n"
+            . "$canonicalQueryString\n$canonicalHeader";
+
+        //使用signKey和标准请求串完成签名
+        $signature = hash_hmac('sha256', $canonicalRequest, $signingKey);
+
+        //组成最终签名串
+        $authorizationHeader = "$authString/$signedHeaders/$signature";
+
+        return $authorizationHeader;
+    }
+
+    /**
+     * 根据headsToSign过滤应该参与签名的header
+     * @param  array $headers
+     * @param  array $headersToSign
+     * @return array
+     */
+    public static function getHeadersToSign($headers, $headersToSign)
+    {
+
+        //value被trim后为空串的header不参与签名
+        $filter_empty = function ($v) {
+            return trim((string)$v) !== '';
+        };
+        $headers = array_filter($headers, $filter_empty);
+
+        //处理headers的key:去掉前后的空白并转化成小写
+        $trim_and_lower = function ($str) {
+            return strtolower(trim($str));
+        };
+        $temp = array();
+        $process_keys = function ($k, $v) use (&$temp, $trim_and_lower) {
+            $temp[$trim_and_lower($k)] = $v;
+        };
+        array_map($process_keys, array_keys($headers), $headers);
+        $headers = $temp;
+
+        //取出headers的key以备用
+        $header_keys = array_keys($headers);
+
+        $filtered_keys = null;
+        if ($headersToSign !== null) {
+            //如果有headersToSign,则根据headersToSign过滤
+
+            //预处理headersToSign:去掉前后的空白并转化成小写
+            $headersToSign = array_map($trim_and_lower, $headersToSign);
+
+            //只选取在headersToSign里面的header
+            $filtered_keys = array_intersect_key($header_keys, $headersToSign);
+
+        } else {
+            //如果没有headersToSign,则根据默认规则来选取headers
+            $filter_by_default = function ($k) {
+                return AipSampleSigner::isDefaultHeaderToSign($k);
+            };
+            $filtered_keys = array_filter($header_keys, $filter_by_default);
+        }
+
+        //返回需要参与签名的header
+        return array_intersect_key($headers, array_flip($filtered_keys));
+    }
+
+    /**
+     * 检查header是不是默认参加签名的:
+     * 1.是host、content-type、content-md5、content-length之一
+     * 2.以x-bce开头
+     * @param  array $header
+     * @return boolean
+     */
+    public static function isDefaultHeaderToSign($header)
+    {
+        $header = strtolower(trim($header));
+        if (in_array($header, AipSampleSigner::$defaultHeadersToSign)) {
+            return true;
+        }
+        return substr_compare($header, AipSampleSigner::BCE_PREFIX, 0, strlen(AipSampleSigner::BCE_PREFIX)) == 0;
+    }
+}
+
+AipSampleSigner::__init();

+ 19 - 0
addons/cms/library/aip/lib/AipSignOption.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace addons\cms\library\aip\lib;
+
+
+class AipSignOption
+{
+    const EXPIRATION_IN_SECONDS = 'expirationInSeconds';
+
+    const HEADERS_TO_SIGN = 'headersToSign';
+
+    const TIMESTAMP = 'timestamp';
+
+    const DEFAULT_EXPIRATION_IN_SECONDS = 1800;
+
+    const MIN_EXPIRATION_IN_SECONDS = 300;
+
+    const MAX_EXPIRATION_IN_SECONDS = 129600;
+}

+ 420 - 0
addons/cms/library/hashids/Hashids.php

@@ -0,0 +1,420 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids;
+
+use Hashids\Math\Bc;
+use Hashids\Math\Gmp;
+use RuntimeException;
+
+/**
+ * This is the hashids class.
+ *
+ * @author Ivan Akimov <ivan@barreleye.com>
+ * @author Vincent Klaiber <hello@vinkla.com>
+ * @author Johnson Page <jwpage@gmail.com>
+ */
+class Hashids implements HashidsInterface
+{
+    /**
+     * The seps divider.
+     *
+     * @var float
+     */
+    const SEP_DIV = 3.5;
+
+    /**
+     * The guard divider.
+     *
+     * @var float
+     */
+    const GUARD_DIV = 12;
+
+    /**
+     * The alphabet string.
+     *
+     * @var string
+     */
+    protected $alphabet;
+
+    /**
+     * Shuffled alphabets, referenced by alphabet and salt.
+     *
+     * @var array
+     */
+    protected $shuffledAlphabets;
+
+    /**
+     * The seps string.
+     *
+     * @var string
+     */
+    protected $seps = 'cfhistuCFHISTU';
+
+    /**
+     * The guards string.
+     *
+     * @var string
+     */
+    protected $guards;
+
+    /**
+     * The minimum hash length.
+     *
+     * @var int
+     */
+    protected $minHashLength;
+
+    /**
+     * The salt string.
+     *
+     * @var string
+     */
+    protected $salt;
+
+    /**
+     * The math class.
+     *
+     * @var \Hashids\Math\MathInterface
+     */
+    protected $math;
+
+    /**
+     * Create a new hashids instance.
+     *
+     * @param string $salt
+     * @param int $minHashLength
+     * @param string $alphabet
+     *
+     * @throws \Hashids\HashidsException
+     *
+     * @return void
+     */
+    public function __construct($salt = '', $minHashLength = 0, $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
+    {
+        $this->salt = $salt;
+        $this->minHashLength = $minHashLength;
+        $this->alphabet = implode('', array_unique(str_split($alphabet)));
+        $this->math = $this->getMathExtension();
+
+        if (strlen($this->alphabet) < 16) {
+            throw new HashidsException('Alphabet must contain at least 16 unique characters.');
+        }
+
+        if (strpos($this->alphabet, ' ') !== false) {
+            throw new HashidsException('Alphabet can\'t contain spaces.');
+        }
+
+        $alphabetArray = str_split($this->alphabet);
+        $sepsArray = str_split($this->seps);
+
+        $this->seps = implode('', array_intersect($sepsArray, $alphabetArray));
+        $this->alphabet = implode('', array_diff($alphabetArray, $sepsArray));
+        $this->seps = $this->shuffle($this->seps, $this->salt);
+
+        if (!$this->seps || (strlen($this->alphabet) / strlen($this->seps)) > self::SEP_DIV) {
+            $sepsLength = (int) ceil(strlen($this->alphabet) / self::SEP_DIV);
+
+            if ($sepsLength > strlen($this->seps)) {
+                $diff = $sepsLength - strlen($this->seps);
+                $this->seps .= substr($this->alphabet, 0, $diff);
+                $this->alphabet = substr($this->alphabet, $diff);
+            }
+        }
+
+        $this->alphabet = $this->shuffle($this->alphabet, $this->salt);
+        $guardCount = (int) ceil(strlen($this->alphabet) / self::GUARD_DIV);
+
+        if (strlen($this->alphabet) < 3) {
+            $this->guards = substr($this->seps, 0, $guardCount);
+            $this->seps = substr($this->seps, $guardCount);
+        } else {
+            $this->guards = substr($this->alphabet, 0, $guardCount);
+            $this->alphabet = substr($this->alphabet, $guardCount);
+        }
+    }
+
+    /**
+     * Encode parameters to generate a hash.
+     *
+     * @param mixed $numbers
+     *
+     * @return string
+     */
+    public function encode(...$numbers)
+    {
+        $ret = '';
+
+        if (1 === count($numbers) && is_array($numbers[0])) {
+            $numbers = $numbers[0];
+        }
+
+        if (!$numbers) {
+            return $ret;
+        }
+
+        foreach ($numbers as $number) {
+            $isNumber = ctype_digit((string) $number);
+
+            if (!$isNumber) {
+                return $ret;
+            }
+        }
+
+        $alphabet = $this->alphabet;
+        $numbersSize = count($numbers);
+        $numbersHashInt = 0;
+
+        foreach ($numbers as $i => $number) {
+            $numbersHashInt += $this->math->intval($this->math->mod($number, ($i + 100)));
+        }
+
+        $lottery = $ret = $alphabet[$numbersHashInt % strlen($alphabet)];
+        foreach ($numbers as $i => $number) {
+            $alphabet = $this->shuffle($alphabet, substr($lottery.$this->salt.$alphabet, 0, strlen($alphabet)));
+            $ret .= $last = $this->hash($number, $alphabet);
+
+            if ($i + 1 < $numbersSize) {
+                $number %= (ord($last) + $i);
+                $sepsIndex = $this->math->intval($this->math->mod($number, strlen($this->seps)));
+                $ret .= $this->seps[$sepsIndex];
+            }
+        }
+
+        if (strlen($ret) < $this->minHashLength) {
+            $guardIndex = ($numbersHashInt + ord($ret[0])) % strlen($this->guards);
+
+            $guard = $this->guards[$guardIndex];
+            $ret = $guard.$ret;
+
+            if (strlen($ret) < $this->minHashLength) {
+                $guardIndex = ($numbersHashInt + ord($ret[2])) % strlen($this->guards);
+                $guard = $this->guards[$guardIndex];
+
+                $ret .= $guard;
+            }
+        }
+
+        $halfLength = (int) (strlen($alphabet) / 2);
+        while (strlen($ret) < $this->minHashLength) {
+            $alphabet = $this->shuffle($alphabet, $alphabet);
+            $ret = substr($alphabet, $halfLength).$ret.substr($alphabet, 0, $halfLength);
+
+            $excess = strlen($ret) - $this->minHashLength;
+            if ($excess > 0) {
+                $ret = substr($ret, $excess / 2, $this->minHashLength);
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Decode a hash to the original parameter values.
+     *
+     * @param string $hash
+     *
+     * @return array
+     */
+    public function decode($hash)
+    {
+        $ret = [];
+
+        if (!is_string($hash) || !($hash = trim($hash))) {
+            return $ret;
+        }
+
+        $alphabet = $this->alphabet;
+
+        $ret = [];
+
+        $hashBreakdown = str_replace(str_split($this->guards), ' ', $hash);
+        $hashArray = explode(' ', $hashBreakdown);
+
+        $i = count($hashArray) == 3 || count($hashArray) == 2 ? 1 : 0;
+
+        $hashBreakdown = $hashArray[$i];
+
+        if (isset($hashBreakdown[0])) {
+            $lottery = $hashBreakdown[0];
+            $hashBreakdown = substr($hashBreakdown, 1);
+
+            $hashBreakdown = str_replace(str_split($this->seps), ' ', $hashBreakdown);
+            $hashArray = explode(' ', $hashBreakdown);
+
+            foreach ($hashArray as $subHash) {
+                $alphabet = $this->shuffle($alphabet, substr($lottery.$this->salt.$alphabet, 0, strlen($alphabet)));
+                $result = $this->unhash($subHash, $alphabet);
+                if ($this->math->greaterThan($result, PHP_INT_MAX)) {
+                    $ret[] = $this->math->strval($result);
+                } else {
+                    $ret[] = $this->math->intval($result);
+                }
+            }
+
+            if ($this->encode($ret) != $hash) {
+                $ret = [];
+            }
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Encode hexadecimal values and generate a hash string.
+     *
+     * @param string $str
+     *
+     * @return string
+     */
+    public function encodeHex($str)
+    {
+        if (!ctype_xdigit((string) $str)) {
+            return '';
+        }
+
+        $numbers = trim(chunk_split($str, 12, ' '));
+        $numbers = explode(' ', $numbers);
+
+        foreach ($numbers as $i => $number) {
+            $numbers[$i] = hexdec('1'.$number);
+        }
+
+        return call_user_func_array([$this, 'encode'], $numbers);
+    }
+
+    /**
+     * Decode a hexadecimal hash.
+     *
+     * @param string $hash
+     *
+     * @return string
+     */
+    public function decodeHex($hash)
+    {
+        $ret = '';
+        $numbers = $this->decode($hash);
+
+        foreach ($numbers as $i => $number) {
+            $ret .= substr(dechex($number), 1);
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Shuffle alphabet by given salt.
+     *
+     * @param string $alphabet
+     * @param string $salt
+     *
+     * @return string
+     */
+    protected function shuffle($alphabet, $salt)
+    {
+        $key = $alphabet.' '.$salt;
+
+        if (isset($this->shuffledAlphabets[$key])) {
+            return $this->shuffledAlphabets[$key];
+        }
+
+        $saltLength = strlen($salt);
+
+        if (!$saltLength) {
+            return $alphabet;
+        }
+
+        for ($i = strlen($alphabet) - 1, $v = 0, $p = 0; $i > 0; $i--, $v++) {
+            $v %= $saltLength;
+            $p += $int = ord($salt[$v]);
+            $j = ($int + $v + $p) % $i;
+
+            $temp = $alphabet[$j];
+            $alphabet[$j] = $alphabet[$i];
+            $alphabet[$i] = $temp;
+        }
+
+        $this->shuffledAlphabets[$key] = $alphabet;
+
+        return $alphabet;
+    }
+
+    /**
+     * Hash given input value.
+     *
+     * @param string $input
+     * @param string $alphabet
+     *
+     * @return string
+     */
+    protected function hash($input, $alphabet)
+    {
+        $hash = '';
+        $alphabetLength = strlen($alphabet);
+
+        do {
+            $hash = $alphabet[$this->math->intval($this->math->mod($input, $alphabetLength))].$hash;
+
+            $input = $this->math->divide($input, $alphabetLength);
+        } while ($this->math->greaterThan($input, 0));
+
+        return $hash;
+    }
+
+    /**
+     * Unhash given input value.
+     *
+     * @param string $input
+     * @param string $alphabet
+     *
+     * @return int
+     */
+    protected function unhash($input, $alphabet)
+    {
+        $number = 0;
+        $inputLength = strlen($input);
+
+        if ($inputLength && $alphabet) {
+            $alphabetLength = strlen($alphabet);
+            $inputChars = str_split($input);
+
+            foreach ($inputChars as $char) {
+                $position = strpos($alphabet, $char);
+                $number = $this->math->multiply($number, $alphabetLength);
+                $number = $this->math->add($number, $position);
+            }
+        }
+
+        return $number;
+    }
+
+    /**
+     * Get BC Math or GMP extension.
+     *
+     * @codeCoverageIgnore
+     *
+     * @throws \RuntimeException
+     *
+     * @return \Hashids\Math\Bc|\Hashids\Math\Gmp
+     */
+    protected function getMathExtension()
+    {
+        if (extension_loaded('gmp')) {
+            return new Gmp();
+        }
+
+        if (extension_loaded('bcmath')) {
+            return new Bc();
+        }
+
+        throw new RuntimeException('Missing BC Math or GMP extension.');
+    }
+}

+ 24 - 0
addons/cms/library/hashids/HashidsException.php

@@ -0,0 +1,24 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids;
+
+use InvalidArgumentException;
+
+/**
+ * This is the hashids exception class.
+ *
+ * @author Vincent Klaiber <hello@vinkla.com>
+ */
+class HashidsException extends InvalidArgumentException
+{
+    //
+}

+ 57 - 0
addons/cms/library/hashids/HashidsInterface.php

@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids;
+
+/**
+ * This is the hashids interface.
+ *
+ * @author Ivan Akimov <ivan@barreleye.com>
+ * @author Vincent Klaiber <hello@vinkla.com>
+ */
+interface HashidsInterface
+{
+    /**
+     * Encode parameters to generate a hash.
+     *
+     * @param mixed $numbers
+     *
+     * @return string
+     */
+    public function encode(...$numbers);
+
+    /**
+     * Decode a hash to the original parameter values.
+     *
+     * @param string $hash
+     *
+     * @return array
+     */
+    public function decode($hash);
+
+    /**
+     * Encode hexadecimal values and generate a hash string.
+     *
+     * @param string $str
+     *
+     * @return string
+     */
+    public function encodeHex($str);
+
+    /**
+     * Decode a hexadecimal hash.
+     *
+     * @param string $hash
+     *
+     * @return string
+     */
+    public function decodeHex($hash);
+}

+ 20 - 0
addons/cms/library/hashids/LICENSE

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

+ 123 - 0
addons/cms/library/hashids/Math/Bc.php

@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids\Math;
+
+/**
+ * This is the Bc math class.
+ *
+ * @author Vincent Klaiber <hello@vinkla.com>
+ * @author Jakub Kramarz <lenwe@lenwe.net>
+ * @author Johnson Page <jwpage@gmail.com>
+ */
+class Bc implements MathInterface
+{
+    /**
+     * Add two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function add($a, $b)
+    {
+        return bcadd($a, $b, 0);
+    }
+
+    /**
+     * Multiply two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function multiply($a, $b)
+    {
+        return bcmul($a, $b, 0);
+    }
+
+    /**
+     * Divide two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function divide($a, $b)
+    {
+        return bcdiv($a, $b, 0);
+    }
+
+    /**
+     * Compute arbitrary-length integer modulo.
+     *
+     * @param string $n
+     * @param string $d
+     *
+     * @return string
+     */
+    public function mod($n, $d)
+    {
+        return bcmod($n, $d);
+    }
+
+    /**
+     * Compares two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return bool
+     */
+    public function greaterThan($a, $b)
+    {
+        return bccomp($a, $b, 0) > 0;
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP integer.
+     *
+     * @param string $a
+     *
+     * @return int
+     */
+    public function intval($a)
+    {
+        return intval($a);
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP string.
+     *
+     * @param string $a
+     *
+     * @return string
+     */
+    public function strval($a)
+    {
+        return $a;
+    }
+
+    /**
+     * Converts PHP integer to arbitrary-length integer.
+     *
+     * @param int $a
+     *
+     * @return string
+     */
+    public function get($a)
+    {
+        return $a;
+    }
+}

+ 123 - 0
addons/cms/library/hashids/Math/Gmp.php

@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids\Math;
+
+/**
+ * This is the Gmp math class.
+ *
+ * @author Vincent Klaiber <hello@vinkla.com>
+ * @author Jakub Kramarz <lenwe@lenwe.net>
+ * @author Johnson Page <jwpage@gmail.com>
+ */
+class Gmp implements MathInterface
+{
+    /**
+     * Add two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function add($a, $b)
+    {
+        return gmp_add($a, $b);
+    }
+
+    /**
+     * Multiply two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function multiply($a, $b)
+    {
+        return gmp_mul($a, $b);
+    }
+
+    /**
+     * Divide two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function divide($a, $b)
+    {
+        return gmp_div_q($a, $b);
+    }
+
+    /**
+     * Compute arbitrary-length integer modulo.
+     *
+     * @param string $n
+     * @param string $d
+     *
+     * @return string
+     */
+    public function mod($n, $d)
+    {
+        return gmp_mod($n, $d);
+    }
+
+    /**
+     * Compares two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return bool
+     */
+    public function greaterThan($a, $b)
+    {
+        return gmp_cmp($a, $b) > 0;
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP integer.
+     *
+     * @param string $a
+     *
+     * @return int
+     */
+    public function intval($a)
+    {
+        return gmp_intval($a);
+    }
+
+    /**
+     * Converts arbitrary-length integer to PHP string.
+     *
+     * @param string $a
+     *
+     * @return string
+     */
+    public function strval($a)
+    {
+        return gmp_strval($a);
+    }
+
+    /**
+     * Converts PHP integer to arbitrary-length integer.
+     *
+     * @param int $a
+     *
+     * @return string
+     */
+    public function get($a)
+    {
+        return gmp_init($a);
+    }
+}

+ 99 - 0
addons/cms/library/hashids/Math/MathInterface.php

@@ -0,0 +1,99 @@
+<?php
+
+/*
+ * This file is part of Hashids.
+ *
+ * (c) Ivan Akimov <ivan@barreleye.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Hashids\Math;
+
+/**
+ * Interface for different math extensions.
+ *
+ * @author Vincent Klaiber <hello@vinkla.com>
+ * @author Jakub Kramarz <lenwe@lenwe.net>
+ * @author Johnson Page <jwpage@gmail.com>
+ */
+interface MathInterface
+{
+    /**
+     * Add two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function add($a, $b);
+
+    /**
+     * Multiply two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function multiply($a, $b);
+
+    /**
+     * Divide two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function divide($a, $b);
+
+    /**
+     * Compute arbitrary-length integer modulo.
+     *
+     * @param string $n
+     * @param string $d
+     *
+     * @return string
+     */
+    public function mod($n, $d);
+
+    /**
+     * Compares two arbitrary-length integers.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return bool
+     */
+    public function greaterThan($a, $b);
+
+    /**
+     * Converts arbitrary-length integer to PHP integer.
+     *
+     * @param string $a
+     *
+     * @return int
+     */
+    public function intval($a);
+
+    /**
+     * Converts arbitrary-length integer to PHP string.
+     *
+     * @param string $a
+     *
+     * @return string
+     */
+    public function strval($a);
+
+    /**
+     * Converts PHP integer to arbitrary-length integer.
+     *
+     * @param int $a
+     *
+     * @return string
+     */
+    public function get($a);
+}

+ 49 - 0
addons/cms/library/hashids/composer.json

@@ -0,0 +1,49 @@
+{
+    "name": "hashids/hashids",
+    "description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers",
+    "keywords": ["hashids", "hashid", "hash", "ids", "youtube", "bitly", "encode", "decode", "obfuscate"],
+    "homepage": "http://hashids.org/php",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Ivan Akimov",
+            "email": "ivan@barreleye.com",
+            "homepage": "https://twitter.com/IvanAkimov"
+        },
+        {
+            "name": "Vincent Klaiber",
+            "email": "hello@vinkla.com",
+            "homepage": "https://vinkla.com"
+        }
+    ],
+    "require": {
+        "php": "^7.1.3"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^7.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Hashids\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Hashids\\Tests\\": "tests/"
+        }
+    },
+    "config": {
+        "preferred-install": "dist"
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "3.0-dev"
+        }
+    },
+    "suggest": {
+       "ext-bcmath": "Required to use BC Math arbitrary precision mathematics (*).",
+       "ext-gmp": "Required to use GNU multiple precision mathematics (*)."
+     },
+    "minimum-stability": "dev",
+    "prefer-stable": true
+}

+ 638 - 0
addons/cms/model/Archives.php

@@ -0,0 +1,638 @@
+<?php
+
+namespace addons\cms\model;
+
+use addons\cms\library\IntCode;
+use addons\cms\library\Service;
+use app\common\library\Auth;
+use Hashids\Hashids;
+use think\Cache;
+use think\Db;
+use think\Model;
+use traits\model\SoftDelete;
+
+/**
+ * 文章模型
+ */
+class Archives extends Model
+{
+    protected $name = "cms_archives";
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = 'deletetime';
+
+    // 追加属性
+    protected $append = [
+        'url',
+        'fullurl',
+        'likeratio',
+        'taglist',
+        'create_date',
+    ];
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    use SoftDelete;
+
+    /**
+     * 批量设置数据
+     * @param $data
+     * @return $this
+     */
+    public function setData($data)
+    {
+        if (is_object($data)) {
+            $data = get_object_vars($data);
+        }
+        //替换付费内容标签
+        if (isset($data['content'])) {
+            $data['content'] = str_replace(['##paidbegin##', '##paidend##'], ['<paid>', '</paid>'], $data['content']);
+            $data['content'] = str_replace(['$$paidbegin$$', '$$paidend$$'], ['<paid>', '</paid>'], $data['content']);
+        }
+        $this->data = array_merge($this->data, $data);
+        $this->origin = $this->data;
+        return $this;
+    }
+
+    protected static function init()
+    {
+        $config = get_addon_config('cms');
+        self::$config = $config;
+    }
+
+    public function getAttr($name)
+    {
+        //获取自定义字段关联表数据
+        if (!isset($this->data[$name]) && preg_match("/(.*)_value\$/i", $name, $matches)) {
+            $key = $this->data[$matches[1]] ?? '';
+            if (!$key) {
+                return '';
+            }
+            return Service::getRelationFieldValue('model', $this->data['model_id'], $matches[1], $key);
+        }
+        return parent::getAttr($name);
+    }
+
+    /**
+     * 获取加密后的ID
+     * @param $value
+     * @param $data
+     * @return string
+     */
+    public function getEidAttr($value, $data)
+    {
+        $value = $data['id'];
+        if (self::$config['archiveshashids']) {
+            $value = IntCode::encode($value);
+        }
+        return $value;
+    }
+
+    public function getCreateDateAttr($value, $data)
+    {
+        return human_date($data['createtime']);
+    }
+
+    public function getHasimageAttr($value, $data)
+    {
+        return $this->getData("image") ? true : false;
+    }
+
+    public function getIscommentAttr($value, $data)
+    {
+        //优先判断全局评论开关
+        $iscomment = self::$config['iscomment'] ?? 1;
+        if ($iscomment) {
+            $iscomment = $value ? $value : 0;
+        }
+        return $iscomment;
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $value = $value ? $value : self::$config['default_archives_img'];
+        return cdnurl($value, true);
+    }
+
+    public function getImagesAttr($value, $data)
+    {
+        if (!$data['images']) {
+            return '';
+        }
+        $images = explode(',', $data['images']);
+        foreach ($images as $index => &$image) {
+            $image = cdnurl($image, true);
+        }
+        return implode(',', $images);
+    }
+
+    public function getImagesListAttr($value, $data)
+    {
+        if (isset($data['images_list'])) {
+            return $data['images_list'];
+        }
+        $images = $this->getAttr("images");
+        return $images ? array_filter(explode(',', $images)) : [];
+    }
+
+    /**
+     * 获取格式化的内容
+     */
+    public function getContentAttr($value, $data)
+    {
+        static $formatted = false;
+        $value = $data['content'];
+        if (!$formatted) {
+            //如果内容中包含有付费标签
+            $pattern = '/<paid>(.*?)<\/paid>/is';
+            if (preg_match($pattern, $value) && !$this->getAttr('ispaid')) {
+                $paytype = static::$config['defaultpaytype'];
+                $payurl = addon_url('cms/order/submit', ['id' => $data['id'], 'paytype' => $paytype]);
+                if (!isset($data['price']) || $data['price'] <= 0) {
+                    $value = preg_replace($pattern, "<div class='alert alert-warning alert-paid'><a href='javascript:' class=''>内容已经隐藏</a></div>", $value);
+                } else {
+                    $value = preg_replace($pattern, "<div class='alert alert-warning alert-paid'><a href='{$payurl}' class='btn-paynow' data-price='{$data['price']}' data-paytype='{$paytype}'>内容已经隐藏,点击付费后查看</a></div>", $value);
+                }
+            }
+            //实时关键字内链替换
+            $config = get_addon_config('cms');
+            if (isset($config['realtimereplacelink']) && $config['realtimereplacelink']) {
+                $value = Service::autolinks($value);
+            }
+            $formatted = true;
+        }
+        return $value;
+    }
+
+    /**
+     * 获取金额
+     */
+    public function getPriceAttr($value, &$data)
+    {
+        if (isset($data['price'])) {
+            return $data['price'];
+        }
+        $price = 0;
+        if (isset($data['model_id'])) {
+            $model = Modelx::get($data['model_id']);
+            if ($model && in_array('price', $model['fields'])) {
+                $price = Db::name($model['table'])->where('id', $data['id'])->value('price');
+            }
+        }
+        $data['price'] = $price;
+        return $price;
+    }
+
+    /**
+     * 判断是否支付
+     */
+    public function getIspayAttr($value, &$data)
+    {
+        return $this->getAttr('ispaid');
+    }
+
+    /**
+     * 判断是否支付
+     */
+    public function getIspaidAttr($value, &$data)
+    {
+        if (isset($data['ispaid'])) {
+            return $data['ispaid'];
+        }
+
+        $channel = isset($this->channel) ? $this->channel : null;
+        //只有当未设定VIP时才判断付费字段
+        if ($channel && !$channel->vip) {
+            //如果未定义price字段或price字段值为0
+            if (!isset($data['price']) || $data['price'] == 0) {
+                return true;
+            }
+        }
+
+        $isvip = $channel && isset($channel['vip']) && $channel['vip'] && Auth::instance()->vip >= $channel->vip ? true : false;
+        $data['ispaid'] = $isvip || \addons\cms\library\Order::check($data['id']);
+        return $data['ispaid'];
+    }
+
+    /**
+     * 判断是否是部分内容付费
+     */
+    public function getIsPaidPartOfContentAttr($value, $data)
+    {
+        if (isset($data['is_paid_part_of_content'])) {
+            return $data['is_paid_part_of_content'];
+        }
+        $value = isset($this->origin['content']) ? $this->origin['content'] : '';
+        $result = preg_match('/<paid>(.*?)<\/paid>/is', $value);
+        $data['is_paid_part_of_content'] = $result;
+        return $result;
+    }
+
+    /**
+     * 获取下载地址列表
+     */
+    public function getDownloadurlListAttr($value, $data)
+    {
+        $titleArr = isset(self::$config['downloadtype']) ? self::$config['downloadtype'] : [];
+        $downloadurl = is_array($data['downloadurl']) ? $data['downloadurl'] : (array)json_decode($data['downloadurl'], true);
+        $downloadurl = array_filter($downloadurl);
+        $list = [];
+        foreach ($downloadurl as $index => $item) {
+            if (!is_array($item)) {
+                $urlArr = explode(' ', $item);
+                $result['name'] = $index;
+                $result['title'] = isset($titleArr[$index]) ? $titleArr[$index] : '其它';
+                $result['url'] = cdnurl($urlArr[0], true);
+                $result['password'] = isset($urlArr[1]) ? $urlArr[1] : '';
+                $list[] = $result;
+            } elseif (is_array($item) && isset($item['name']) && isset($item['url']) && $item['url']) {
+                $item['url'] = cdnurl($item['url'], true);
+                $result = $item;
+                $result['title'] = isset($titleArr[$item['name']]) ? $titleArr[$item['name']] : '其它';
+                $result['password'] = $result['password'] ?? '';
+                $list[] = $result;
+            }
+        }
+        return $list;
+    }
+
+    public function getTaglistAttr($value, &$data)
+    {
+        if (isset($data['taglist'])) {
+            return $data['taglist'];
+        }
+        $tags = array_filter(explode(",", $data['tags']));
+        $tagList = [];
+        if (stripos(self::$config['rewrite']['tag/index'], ":id") !== false) {
+            $tagList = Tag::where('name', 'in', $tags)->column('name,id');
+        }
+        $tagList = array_change_key_case($tagList, CASE_LOWER);
+        $time = $data['createtime'] ?? time();
+        $list = [];
+        foreach ($tags as $k => $v) {
+            $v_lower = strtolower($v);
+            $vars = [':name' => $v, ':diyname' => $v, ':id' => isset($tagList[$v_lower]) ? $tagList[$v_lower] : '0', ':year' => date("Y", $time), ':month' => date("m", $time), ':day' => date("d", $time)];
+            $list[] = ['name' => $v, 'url' => addon_url('cms/tag/index', $vars)];
+        }
+        $data['taglist'] = $list;
+        return $list;
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data);
+    }
+
+    public function getFullurlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data, true);
+    }
+
+    private function buildUrl($value, $data, $domain = false)
+    {
+        $diyname = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : $data['id'];
+        $catename = isset($this->channel) && $this->channel ? $this->channel->diyname : 'all';
+        $cateid = isset($this->channel) && $this->channel ? $this->channel->id : 0;
+        $time = $data['publishtime'] ?? time();
+        $vars = [
+            ':id'       => $data['id'],
+            ':eid'      => $this->getEidAttr($data['id'], $data),
+            ':diyname'  => $diyname,
+            ':channel'  => $data['channel_id'],
+            ':catename' => $catename,
+            ':cateid'   => $cateid,
+            ':year'     => date("Y", $time),
+            ':month'    => date("m", $time),
+            ':day'      => date("d", $time),
+        ];
+        $suffix = static::$config['moduleurlsuffix']['archives'] ?? static::$config['urlsuffix'];
+        return addon_url('cms/archives/index', $vars, $suffix, $domain);
+    }
+
+    public function getLikeratioAttr($value, $data)
+    {
+        return ($data['dislikes'] > 0 ? min(1, $data['likes'] / ($data['dislikes'] + $data['likes'])) : ($data['likes'] ? 1 : 0.5)) * 100;
+    }
+
+    public function getStyleTextAttr($value, $data)
+    {
+        $color = $this->getAttr("style_color");
+        $color = $color ? $color : "inherit";
+        $color = str_replace(['#', ' '], '', $color);
+        $bold = $this->getAttr("style_bold") ? "bold" : "normal";
+        $attr = ["font-weight:{$bold};"];
+        if (stripos($color, ',') !== false) {
+            list($first, $second) = explode(',', $color);
+            $attr[] = "background-image: -webkit-linear-gradient(0deg, #{$first} 0%, #{$second} 100%);background-image: linear-gradient(90deg, #{$first} 0%, #{$second} 100%);-webkit-background-clip: text;-webkit-text-fill-color: transparent;";
+        } else {
+            $attr[] = "color:#{$color};";
+        }
+
+        return implode('', $attr);
+    }
+
+    public function getStyleBoldAttr($value, $data)
+    {
+        return in_array('b', explode('|', $data['style']));
+    }
+
+    public function getStyleColorAttr($value, $data)
+    {
+        $styleArr = explode('|', $data['style']);
+        foreach ($styleArr as $index => $item) {
+            if (preg_match('/\,|#/', $item)) {
+                return $item;
+            }
+        }
+        return '';
+    }
+
+    /**
+     * 获取内容页分页HTML
+     */
+    public function getPagerHTML($page, $total, $simple = false)
+    {
+        if ($total <= 1) {
+            return '';
+        }
+        $result = \addons\cms\library\Bootstrap::make([], 1, $page, $total, $simple, ['path' => $this->url, 'simple' => $simple]);
+        return "<div class='pager'>" . $result->render() . "</div>";
+    }
+
+    /**
+     * 获取文档列表
+     * @param $params
+     * @return array|false|\PDOStatement|string|\think\Collection
+     */
+    public static function getArchivesList($params)
+    {
+        $type = empty($params['type']) ? '' : $params['type'];
+        $model = !isset($params['model']) ? '' : $params['model'];
+        $channel = !isset($params['channel']) ? '' : $params['channel'];
+        $special = !isset($params['special']) ? '' : $params['special'];
+        $tags = empty($params['tags']) ? '' : $params['tags'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $flag = empty($params['flag']) ? '' : $params['flag'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'weigh' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $limit = empty($params['limit']) ? $row : $params['limit'];
+        $imgwidth = empty($params['imgwidth']) ? '' : $params['imgwidth'];
+        $imgheight = empty($params['imgheight']) ? '' : $params['imgheight'];
+        $addon = empty($params['addon']) ? false : $params['addon'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $paginate = !isset($params['paginate']) ? false : $params['paginate'];
+        $page = !isset($params['page']) ? 1 : (int)$params['page'];
+        $with = !isset($params['with']) ? 'channel' : $params['with'];
+        $where = ['status' => 'normal'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('arclist', $params);
+
+        $where['deletetime'] = ['exp', Db::raw('IS NULL')]; //by erastudio
+        if ($model !== '') {
+            $where['model_id'] = ['in', $model];
+        }
+
+        self::$tagCount++;
+
+        $archivesModel = self::with($with)->alias('a');
+        if ($channel !== '') {
+            if ($type === 'son') {
+                $subQuery = Channel::where('parent_id', 'in', $channel)->field('id')->buildSql();
+                //子级
+                $where['channel_id'] = ['exp', Db::raw(' IN ' . '(' . $subQuery . ')')];
+            } elseif ($type === 'sons') {
+                //所有子级
+                $where['channel_id'] = ['in', Channel::getChannelChildrenIds($channel)];
+            } else {
+                $where['channel_id'] = ['in', $channel];
+            }
+        }
+        //如果有设置标志,则拆分标志信息并构造condition条件
+        if ($flag !== '') {
+            if (stripos($flag, '&') !== false) {
+                $arr = [];
+                foreach (explode('&', $flag) as $k => $v) {
+                    $arr[] = "FIND_IN_SET('{$v}', flag)";
+                }
+                if ($arr) {
+                    $condition .= "(" . implode(' AND ', $arr) . ")";
+                }
+            } else {
+                $condition .= ($condition ? ' AND ' : '');
+                $arr = [];
+                foreach (explode(',', str_replace('|', ',', $flag)) as $k => $v) {
+                    $arr[] = "FIND_IN_SET('{$v}', flag)";
+                }
+                if ($arr) {
+                    $condition .= "(" . implode(' OR ', $arr) . ")";
+                }
+            }
+        }
+        if ($special) {
+            $specialModel = Special::get($special, [], true);
+            if ($specialModel && $specialModel['tag_ids']) {
+                $archivesModel->where("a.id", "in", function ($query) use ($specialModel) {
+                    return $query->name("cms_taggable")->where("tag_id", "in", $specialModel['tag_ids'])->field("archives_id");
+                });
+            }
+        }
+
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+        $order = $orderby == 'weigh' ? $order . ',publishtime DESC' : $order;
+
+        // 如果有筛选标签,则采用子查询
+        if ($tags) {
+            $tagIds = Tag::where('name', 'in', explode(',', $tags))->cache(true)->column("id");
+
+            $archivesModel->where("a.id", "in", function ($query) use ($tagIds) {
+                return $query->name("cms_taggable")->where("tag_id", "in", $tagIds)->field("archives_id");
+            });
+        }
+
+        $modelInfo = null;
+        $prefix = config('database.prefix');
+        $archivesModel
+            ->where($where)
+            ->where($condition)
+            ->field($field, false, $prefix . "cms_archives", "a")
+            ->orderRaw($order);
+        if ($addon && (is_numeric($model) || $channel)) {
+            if ($channel) {
+                //如果channel设置了多个值则只取第一个作为判断
+                $channelArr = explode(',', $channel);
+                $channelinfo = Channel::get($channelArr[0], [], true);
+                $model = $channelinfo ? $channelinfo['model_id'] : $model;
+            }
+            // 查询相关联的模型信息
+            $modelInfo = Modelx::get($model, [], true);
+            if ($modelInfo) {
+                $archivesModel->join($modelInfo['table'] . ' n', 'a.id=n.id', 'LEFT');
+                if ($addon == 'true') {
+                    $archivesModel->field('id,content', true, $prefix . $modelInfo['table'], 'n');
+                } else {
+                    $archivesModel->field($addon, false, $prefix . $modelInfo['table'], 'n');
+                }
+            }
+        }
+
+//        var_dump(request()->url());
+//        if(request()->url()) {
+//
+//        }
+        if ($paginate) {
+            list($listRows, $simple, $config) = Service::getPaginateParams((isset($params['page']) ? 'page' : 'apage' . self::$tagCount), $params);
+            if (isset($params['page'])) {
+                $config['page'] = $page;
+            }
+            $list = $archivesModel->paginate($listRows, $simple, $config);
+        } else {
+            $list = $archivesModel->limit($limit)->cache($cacheKey, $cacheExpire)->select();
+        }
+
+        if ($modelInfo && $modelInfo->fields) {
+            Service::appendTextAndList('model', $modelInfo->id, $list, true);
+        }
+
+        self::render($list, $imgwidth, $imgheight);
+        return $list;
+    }
+
+    /**
+     * 标题高亮搜索结果
+     */
+    public function highlight($title, $keywords = '')
+    {
+        if ($keywords == '') {
+            return $title;
+        }
+        $re = '/(' . str_replace(" ", '|', preg_quote($keywords)) . ')/i';
+        return preg_replace($re, '<span class="highlight">$0</span>', $title);
+    }
+
+    /**
+     * 渲染数据
+     * @param array $list
+     * @param int   $imgwidth
+     * @param int   $imgheight
+     * @return array
+     */
+    public static function render(&$list, $imgwidth, $imgheight)
+    {
+        $width = $imgwidth ? 'width="' . $imgwidth . '"' : '';
+        $height = $imgheight ? 'height="' . $imgheight . '"' : '';
+        foreach ($list as $k => &$v) {
+            $v['textlink'] = '<a href="' . $v['url'] . '">' . $v['title'] . '</a>';
+            $v['channellink'] = '<a href="' . ($v['channel']['url'] ?? '#') . '">' . ($v['channel']['name'] ?? '未知') . '</a>';
+            $v['imglink'] = '<a href="' . $v['url'] . '"><img src="' . $v['image'] . '" ' . $width . ' ' . $height . ' /></a>';
+            $v['img'] = '<img src="' . $v['image'] . '" ' . $width . ' ' . $height . ' />';
+        }
+        return $list;
+    }
+
+    /**
+     * 获取分页列表
+     * @param array $list
+     * @param array $params
+     * @return array
+     */
+    public static function getPageList($list, $params)
+    {
+        $imgwidth = empty($params['imgwidth']) ? '' : $params['imgwidth'];
+        $imgheight = empty($params['imgheight']) ? '' : $params['imgheight'];
+        return self::render($list, $imgwidth, $imgheight);
+    }
+
+    /**
+     * 获取分页过滤
+     * @param array $list
+     * @param array $params
+     * @return array
+     */
+    public static function getPageFilter($list, $params)
+    {
+        $exclude = empty($params['exclude']) ? '' : $params['exclude'];
+        return $list;
+    }
+
+    /**
+     * 获取分页排序
+     * @param array $list
+     * @param array $params
+     * @return array
+     */
+    public static function getPageOrder($list, $params)
+    {
+        $exclude = empty($params['exclude']) ? '' : $params['exclude'];
+        return $list;
+    }
+
+    /**
+     * 获取上一页下一页
+     * @param array $params
+     * @return array
+     */
+    public static function getPrevNext($params = [])
+    {
+        $type = isset($params['type']) ? $params['type'] : 'prev';
+        $channel = isset($params['channel']) ? $params['channel'] : '';
+        $archives = isset($params['archives']) ? $params['archives'] : '';
+        $condition = isset($params['condition']) ? $params['condition'] : '';
+        $model = self::where('id', $type === 'prev' ? '<' : '>', $archives)->where('status', 'normal');
+        if ($channel !== '') {
+            $model->where('channel_id', 'in', $channel);
+        }
+        if (isset($condition)) {
+            $model->where($condition);
+        }
+        $model->order($type === 'prev' ? 'id desc' : 'id asc');
+        $row = $model->find();
+        return $row;
+    }
+
+    /**
+     * 获取SQL查询结果
+     */
+    public static function getQueryList($params)
+    {
+        $sql = isset($params['sql']) ? $params['sql'] : '';
+        $bind = isset($params['bind']) ? explode(',', $params['bind']) : [];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('sql', $params);
+        $list = Cache::get($cacheKey);
+        if (!$list) {
+            $list = Db::query($sql, $bind);
+            Cache::set($cacheKey, $list, $cacheExpire);
+        }
+        return $list;
+    }
+
+    /**
+     * 关联模型
+     */
+    public function user()
+    {
+        return $this->belongsTo("\app\common\model\User", 'user_id', 'id', [], 'LEFT')->setEagerlyType(1);
+    }
+
+    /**
+     * 关联模型
+     */
+    public function model()
+    {
+        return $this->belongsTo("Modelx", 'model_id')->setEagerlyType(1);
+    }
+
+    /**
+     * 关联栏目模型
+     */
+    public function channel()
+    {
+        return $this->belongsTo("Channel", 'channel_id', 'id', [], 'LEFT')->setEagerlyType(1);
+    }
+}

+ 30 - 0
addons/cms/model/Autolink.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace addons\cms\model;
+
+use think\Db;
+use think\Model;
+
+/**
+ * 自动链接模型
+ */
+class Autolink extends Model
+{
+    protected $name = "cms_autolink";
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+    ];
+    protected static $config = [];
+
+    protected static function init()
+    {
+        $config = get_addon_config('cms');
+        self::$config = $config;
+    }
+
+}

+ 192 - 0
addons/cms/model/Block.php

@@ -0,0 +1,192 @@
+<?php
+
+namespace addons\cms\model;
+
+use addons\cms\library\Service;
+use think\Cache;
+use think\Db;
+use think\Model;
+use think\View;
+
+/**
+ * 区块模型
+ */
+class Block extends Model
+{
+    protected $name = "cms_block";
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = '';
+    protected $updateTime = '';
+    // 追加属性
+    protected $append = [
+    ];
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static function init()
+    {
+        $config = get_addon_config('cms');
+        self::$config = $config;
+    }
+
+    public function getAttr($name)
+    {
+        //获取自定义字段关联表数据
+        if (!isset($this->data[$name]) && preg_match("/(.*)_value\$/i", $name, $matches)) {
+            $key = $this->data[$matches[1]] ?? '';
+            if (!$key) {
+                return '';
+            }
+            return Service::getRelationFieldValue('block', 0, $matches[1], $key);
+        }
+        return parent::getAttr($name);
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $value = $value ? $value : self::$config['default_block_img'];
+        return cdnurl($value);
+    }
+
+    public function getContentAttr($value, $data)
+    {
+        if (isset($data['parsetpl']) && $data['parsetpl']) {
+            $view = View::instance();
+            $view->engine->layout(false);
+            return $view->display($data['content']);
+        }
+        return $data['content'];
+    }
+
+    public function getHasimageAttr($value, $data)
+    {
+        return $this->getData("image") ? true : false;
+    }
+
+    /**
+     * 获取区块列表
+     * @param $params
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function getBlockList($params)
+    {
+        $config = get_addon_config('cms');
+        $type = empty($params['type']) ? '' : $params['type'];
+        $name = empty($params['name']) ? '' : $params['name'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'id' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $limit = empty($params['limit']) ? $row : $params['limit'];
+        $cache = !isset($params['cache']) ? $config['cachelifetime'] : $params['cache'];
+        $imgwidth = empty($params['imgwidth']) ? '' : $params['imgwidth'];
+        $imgheight = empty($params['imgheight']) ? '' : $params['imgheight'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $paginate = !isset($params['paginate']) ? false : $params['paginate'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('blocklist', $params);
+
+        self::$tagCount++;
+
+        $where = ['status' => 'normal'];
+        if ($type !== '') {
+            $where['type'] = $type;
+        }
+        if ($name !== '') {
+            $where['name'] = $name;
+        }
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+        $order = $orderby == 'weigh' ? $order . ',id DESC' : $order;
+
+        $blockModel = self::where($where)
+            ->where($condition)
+            ->field($field)
+            ->orderRaw($order);
+
+        if ($paginate) {
+            list($listRows, $simple, $config) = Service::getPaginateParams('bpage' . self::$tagCount, $params);
+            $list = $blockModel->paginate($listRows, $simple, $config);
+        } else {
+            $list = $blockModel->limit($limit)->cache($cacheKey, $cacheExpire)->select();
+        }
+
+        Service::appendTextAndList('block', 0, $list, true);
+
+        self::render($list, $imgwidth, $imgheight);
+        return $list;
+    }
+
+    public static function render(&$list, $imgwidth, $imgheight)
+    {
+        $width = $imgwidth ? 'width="' . $imgwidth . '"' : '';
+        $height = $imgheight ? 'height="' . $imgheight . '"' : '';
+        $time = time();
+        foreach ($list as $k => &$v) {
+            if (($v['begintime'] && $time < $v['begintime']) || ($v['endtime'] && $time > $v['endtime'])) {
+                unset($list[$k]);
+                continue;
+            }
+            $v['textlink'] = '<a href="' . $v['url'] . '">' . $v['title'] . '</a>';
+            $v['imglink'] = '<a href="' . $v['url'] . '"><img src="' . $v['image'] . '" ' . $width . ' ' . $height . ' /></a>';
+            $v['img'] = '<img src="' . $v['image'] . '" ' . $width . ' ' . $height . ' />';
+        }
+        return $list;
+    }
+
+    /**
+     * 获取区块内容
+     * @param $params
+     * @return string
+     */
+    public static function getBlockContent($params)
+    {
+        $fieldName = isset($params['id']) ? 'id' : 'name';
+        $value = isset($params[$fieldName]) ? $params[$fieldName] : '';
+        $field = isset($params['field']) ? $params['field'] : '';
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('blockcontent', $params);
+
+        $row = self::where($fieldName, $value)
+            ->where('status', 'normal')
+            ->cache($cacheKey, $cacheExpire)
+            ->find();
+        $result = '';
+        if ($row) {
+            Service::appendTextAndList('block', 0, $row);
+
+            $content = $row->getData('content');
+            if ($field && isset($row[$field])) {
+                $result = $row->getData($field);
+            } else {
+                if ($content) {
+                    $result = $content;
+                } elseif ($row['image']) {
+                    $result = '<img src="' . $row['image'] . '" class="img-responsive"/>';
+                } else {
+                    $result = $row['title'];
+                }
+                if ($row['url'] && !$content) {
+                    $result = $row['url'] ? '<a href="' . (preg_match("/^https?:\/\/(.*)/i", $row['url']) ? $row['url'] : url($row['url'])) . '" target="_blank">' . $result . '</a>' : $result;
+                }
+            }
+            $row['begintime'] = (int)$row['begintime'];
+            $row['endtime'] = (int)$row['endtime'];
+            if (!$content || ($field && isset($row[$field]))) {
+                return $result;
+            } else {
+                if (!$row['parsetpl']) {
+                    $tagIdentify = "taglib_cms_block_content_" . $row['id'];
+                    Cache::set($tagIdentify, $result);
+                    $result = "{:cache('{$tagIdentify}')}";
+                }
+            }
+            //未开始或过期处理
+            $result = "{if (!{$row['begintime']} || time()>{$row['begintime']})&&(!{$row['endtime']} || time()<{$row['endtime']})}{$result}{/if}";
+        }
+        return $result;
+    }
+}

+ 505 - 0
addons/cms/model/Channel.php

@@ -0,0 +1,505 @@
+<?php
+
+namespace addons\cms\model;
+
+use addons\cms\library\Service;
+use think\Cache;
+use think\Db;
+use think\Model;
+use think\View;
+
+/**
+ * 栏目模型
+ */
+class Channel extends Model
+{
+    protected $name = "cms_channel";
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+
+    // 追加属性
+    protected $append = [
+        'url',
+        'fullurl'
+    ];
+    protected static $config = [];
+
+    protected static $tagCount = 0;
+
+    protected static $parentIds = null;
+
+    protected static $outlinkParentIds = null;
+
+    protected static $navParentIds = null;
+
+    protected static function init()
+    {
+        $config = get_addon_config('cms');
+        self::$config = $config;
+    }
+
+    public function getAttr($name)
+    {
+        //获取自定义字段关联表数据
+        if (!isset($this->data[$name]) && preg_match("/(.*)_value\$/i", $name, $matches)) {
+            $key = $this->data[$matches[1]] ?? '';
+            if (!$key) {
+                return '';
+            }
+            return Service::getRelationFieldValue('channel', 0, $matches[1], $key);
+        }
+        return parent::getAttr($name);
+    }
+
+    public function getUrlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data);
+    }
+
+    public function getFullurlAttr($value, $data)
+    {
+        return $this->buildUrl($value, $data, true);
+    }
+
+    private function buildUrl($value, $data, $domain = false)
+    {
+        $diyname = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : $data['id'];
+        $cateid = $data['id'] ?? 0;
+        $catename = isset($data['diyname']) && $data['diyname'] ? $data['diyname'] : 'all';
+        $time = $data['createtime'] ?? time();
+
+        $vars = [
+            ':id'       => $data['id'],
+            ':diyname'  => $diyname,
+            ':channel'  => $cateid,
+            ':catename' => $catename,
+            ':cateid'   => $cateid,
+            ':year'     => date("Y", $time),
+            ':month'    => date("m", $time),
+            ':day'      => date("d", $time)
+        ];
+        if (isset($data['type']) && isset($data['outlink']) && $data['type'] == 'link') {
+            return $this->getAttr('outlink');
+        }
+        $suffix = static::$config['moduleurlsuffix']['channel'] ?? static::$config['urlsuffix'];
+        return addon_url('cms/channel/index', $vars, $suffix, $domain);
+    }
+
+    public function getImageAttr($value, $data)
+    {
+        $value = $value ? $value : self::$config['default_channel_img'];
+        return cdnurl($value);
+    }
+
+    public function getOutlinkAttr($value, $data)
+    {
+        $indexUrl = $view_replace_str = config('view_replace_str.__PUBLIC__');
+        $indexUrl = rtrim($indexUrl, '/');
+        return str_replace('__INDEX__', $indexUrl, $value);
+    }
+
+    public function getTagcolorAttr($value, $data)
+    {
+        $color = ['primary', 'default', 'success', 'warning', 'danger'];
+        $index = $data['id'] % count($color);
+        return isset($color[$index]) ? $color[$index] : $color[0];
+    }
+
+    public function getHasimageAttr($value, $data)
+    {
+        return $this->getData("image") ? true : false;
+    }
+
+    /**
+     * 判断是否拥有子列表
+     * @param $value
+     * @param $data
+     * @return bool|mixed
+     */
+    public function getHasChildAttr($value, $data)
+    {
+        static $checked = [];
+        if (isset($checked[$data['id']])) {
+            return $checked[$data['id']];
+        }
+        if (is_null(self::$parentIds)) {
+            self::$parentIds = self::where('parent_id', '>', 0)->cache(true)->where('status', 'normal')->column('parent_id');
+        }
+        if (self::$parentIds && in_array($data['id'], self::$parentIds)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 判断导航是否拥有子列表
+     * @param $value
+     * @param $data
+     * @return bool|mixed
+     */
+    public function getHasNavChildAttr($value, $data)
+    {
+        static $checked = [];
+        if (isset($checked[$data['id']])) {
+            return $checked[$data['id']];
+        }
+        if (is_null(self::$navParentIds)) {
+            self::$navParentIds = self::where('parent_id', '>', 0)->cache(true)->where('status', 'normal')->where('isnav', 1)->column('parent_id');
+        }
+        if (self::$navParentIds && in_array($data['id'], self::$navParentIds)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 判断是否当前页面
+     * @param $value
+     * @param $data
+     * @return bool
+     */
+    public function getIsActiveAttr($value, $data)
+    {
+        $url = request()->url();
+        $channel = View::instance()->__CHANNEL__;
+        if (($channel && ($channel['id'] == $this->id || $channel['parent_id'] == $this->id)) || $this->url == $url) {
+            return true;
+        } else {
+            if ($this->has_child) {
+                if (is_null(self::$outlinkParentIds)) {
+                    self::$outlinkParentIds = self::where('type', 'link')->where('status', 'normal')->column('outlink,parent_id');
+                }
+                if (self::$outlinkParentIds && isset(self::$outlinkParentIds[$url]) && self::$outlinkParentIds[$url] == $this->id) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public static function getContributeInfo($archives, $model = null)
+    {
+        // 读取可发布的栏目列表
+        $channel = new Channel();
+        $disabledIds = [];
+        $channelList = collection(
+            $channel->where('status', 'normal')
+                ->order("weigh desc,id desc")
+                ->cache(true)
+                ->select()
+        )->toArray();
+        $channelParents = [];
+        foreach ($channelList as $index => $item) {
+            if ($item['parent_id'] && $item['iscontribute']) {
+                $channelParents[] = $item['parent_id'];
+            }
+        }
+        $channelList = collection(
+            $channel->where('status', 'normal')
+                ->where(function ($query) use ($channelParents, $archives) {
+                    $query->where("iscontribute", 1)->whereOr('id', 'in', $channelParents);
+                    if ($archives) {
+                        $query->whereOr('id', $archives->channel_id);
+                    }
+                })
+                ->order("weigh desc,id desc")
+                ->select()
+        )->toArray();
+        foreach ($channelList as $index => $item) {
+            if (!$item['iscontribute'] && !in_array($item['id'], $channelParents) && (!$archives || $archives->channel_id != $item['id'])) {
+                unset($channelList[$index]);
+            }
+        }
+        foreach ($channelList as $k => $v) {
+            if ($v['type'] == 'link' || (($archives || input('model_id')) && $model && $model['id'] != $v['model_id']) || (!$v['iscontribute'])) {
+                $disabledIds[] = $v['id'];
+            }
+            //if ($v['type'] == 'channel' && !in_array($v['id'], $channelParents)) {
+            //    unset($channelList[$k]);
+            //}
+        }
+        return [$channelList, $disabledIds];
+    }
+
+    /**
+     * 获取栏目所有子级的ID
+     * @param mixed $ids      栏目ID或集合ID
+     * @param bool  $withself 是否包含自身
+     * @return array
+     */
+    public static function getChannelChildrenIds($ids, $withself = true)
+    {
+        $cacheName = 'childrens-' . $ids . '-' . $withself;
+        $result = Cache::get($cacheName);
+        if ($result === false) {
+            $channelList = Channel::where('status', 'normal')
+                ->order('weigh desc,id desc')
+                ->cache(true)
+                ->select();
+
+            $result = [];
+            $tree = \fast\Tree::instance();
+            $tree->init(collection($channelList)->toArray(), 'parent_id');
+            $channelIds = is_array($ids) ? $ids : explode(',', $ids);
+            foreach ($channelIds as $index => $channelId) {
+                $result = array_merge($result, $tree->getChildrenIds($channelId, $withself));
+            }
+            Cache::set($cacheName, $result);
+        }
+        return $result;
+    }
+
+    /**
+     * 获取栏目列表
+     * @param $params
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function getChannelList($params)
+    {
+        $type = empty($params['type']) ? '' : $params['type'];
+        $typeid = !isset($params['typeid']) ? '' : $params['typeid'];
+        $model = !isset($params['model']) ? '' : $params['model'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'weigh' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $limit = empty($params['limit']) ? $row : $params['limit'];
+        $imgwidth = empty($params['imgwidth']) ? '' : $params['imgwidth'];
+        $imgheight = empty($params['imgheight']) ? '' : $params['imgheight'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $paginate = !isset($params['paginate']) ? false : $params['paginate'];
+        $where = ['status' => 'normal'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('channellist', $params);
+
+        self::$tagCount++;
+
+        if ($type === 'top') {
+            //顶级分类
+            $where['parent_id'] = 0;
+        } elseif ($type === 'brother') {
+            $subQuery = self::where('id', 'in', $typeid)->field('parent_id')->buildSql();
+            //同级
+            $where['parent_id'] = ['exp', Db::raw(' IN ' . '(' . $subQuery . ')')];
+        } elseif ($type === 'son') {
+            $subQuery = self::where('parent_id', 'in', $typeid)->field('id')->buildSql();
+            //子级
+            $where['id'] = ['exp', Db::raw(' IN ' . '(' . $subQuery . ')')];
+        } elseif ($type === 'sons') {
+            //所有子级
+            $where['id'] = ['in', self::getChannelChildrenIds($typeid)];
+        } else {
+            if ($typeid !== '') {
+                $where['id'] = ['in', $typeid];
+            }
+        }
+        if ($model !== '') {
+            $where['model_id'] = ['in', $model];
+        }
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+        $order = $orderby == 'weigh' ? $order . ',id DESC' : $order;
+
+        $channelModel = self::where($where)
+            ->where($condition)
+            ->field($field)
+            ->orderRaw($order);
+        if ($paginate) {
+            list($listRows, $simple, $config) = Service::getPaginateParams('cpage' . self::$tagCount, $params);
+            $list = $channelModel->paginate($listRows, $simple, $config);
+        } else {
+            $list = $channelModel->limit($limit)->cache($cacheKey, $cacheExpire)->select();
+        }
+
+        Service::appendTextAndList('channel', 0, $list, true);
+
+        self::render($list, $imgwidth, $imgheight);
+        return $list;
+    }
+
+    /**
+     * 渲染数据
+     * @param array $list
+     * @param int   $imgwidth
+     * @param int   $imgheight
+     * @return array
+     */
+    public static function render(&$list, $imgwidth, $imgheight)
+    {
+        $width = $imgwidth ? 'width="' . $imgwidth . '"' : '';
+        $height = $imgheight ? 'height="' . $imgheight . '"' : '';
+        foreach ($list as $k => &$v) {
+            $v['textlink'] = '<a href="' . $v['url'] . '">' . $v['name'] . '</a>';
+            $v['channellink'] = '<a href="' . $v['url'] . '">' . $v['name'] . '</a>';
+            $v['outlink'] = $v['outlink'];
+            $v['imglink'] = '<a href="' . $v['url'] . '"><img src="' . $v['image'] . '" ' . $width . ' ' . $height . ' /></a>';
+            $v['img'] = '<img src="' . $v['image'] . '" ' . $width . ' ' . $height . ' />';
+        }
+        return $list;
+    }
+
+    /**
+     * 获取面包屑导航
+     * @param array $channel
+     * @param array $archives
+     * @param array $tags
+     * @param array $page
+     * @param array $diyform
+     * @param array $special
+     * @return array
+     */
+    public static function getBreadcrumb($channel, $archives = [], $tags = [], $page = [], $diyform = [], $special = [])
+    {
+        $list = [];
+        $list[] = ['name' => __('Home'), 'url' => addon_url('cms/index/index', [], false)];
+        if ($channel) {
+            if ($channel['parent_id']) {
+                $channelList = self::where('status', 'normal')
+                    ->order('weigh desc,id desc')
+                    ->field('id,name,type,parent_id,diyname,outlink')
+                    ->cache(true)
+                    ->select();
+                //获取栏目的所有上级栏目
+                $parents = \fast\Tree::instance()->init(collection($channelList)->toArray(), 'parent_id')->getParents($channel['id']);
+                foreach ($parents as $k => $v) {
+                    $list[] = ['name' => $v['name'], 'url' => $v['url']];
+                }
+            }
+            $list[] = ['name' => $channel['name'], 'url' => $channel['url']];
+        }
+        if ($archives) {
+            //$list[] = ['name' => $archives['title'], 'url' => $archives['url']];
+        }
+
+        foreach ([$tags, $page, $diyform, $special] as $index => $item) {
+            if ($item && (!$channel || $channel['url'] != $item['url'])) {
+                $list[] = ['name' => $item['title'] ?? $item['name'], 'url' => $item['url']];
+            }
+        }
+        return $list;
+    }
+
+    /**
+     * 获取导航栏目列表HTML
+     * @param       $channel
+     * @param array $params
+     * @return mixed|string
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public static function getNav($channel, $params = [])
+    {
+        $config = get_addon_config('cms');
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $maxLevel = !isset($params['maxlevel']) ? 0 : $params['maxlevel'];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('nav', $params);
+
+        $cacheName = 'nav-' . md5(serialize($params));
+        $result = Cache::get($cacheName);
+        if ($result === false) {
+            $channelList = Channel::where($condition)
+                ->where('status', 'normal')
+                ->order('weigh desc,id desc')
+                ->cache($cacheKey, $cacheExpire)
+                ->select();
+            $tree = \fast\Tree::instance();
+            $tree->init(collection($channelList)->toArray(), 'parent_id');
+            $result = self::getTreeUl($tree, 0, $channel ? $channel['id'] : '', '', 1, $maxLevel);
+            Cache::set($cacheName, $result);
+        }
+        return $result;
+    }
+
+    public static function getTreeUl($tree, $myid, $selectedids = '', $disabledids = '', $level = 1, $maxlevel = 0)
+    {
+        $str = '';
+        $childs = $tree->getChild($myid);
+        if ($childs) {
+            foreach ($childs as $value) {
+                $id = $value['id'];
+                unset($value['child']);
+                $selected = $selectedids && in_array($id, (is_array($selectedids) ? $selectedids : explode(',', $selectedids))) ? 'selected' : '';
+                $disabled = $disabledids && in_array($id, (is_array($disabledids) ? $disabledids : explode(',', $disabledids))) ? 'disabled' : '';
+                $value = array_merge($value, array('selected' => $selected, 'disabled' => $disabled));
+                $value = array_combine(array_map(function ($k) {
+                    return '@' . $k;
+                }, array_keys($value)), $value);
+                $itemtpl = '<li class="@dropdown" value=@id @selected @disabled><a data-toggle="@toggle" data-target="#" href="@url">@name @caret</a> @childlist</li>';
+                $nstr = strtr($itemtpl, $value);
+                $childlist = '';
+                if (!$maxlevel || $level < $maxlevel) {
+                    $childdata = self::getTreeUl($tree, $id, $selectedids, $disabledids, $level + 1, $maxlevel);
+                    $childlist = $childdata ? '<ul class="dropdown-menu" role="menu">' . $childdata . '</ul>' : "";
+                }
+                $str .= strtr($nstr, [
+                    '@childlist' => $childlist,
+                    '@caret'     => $childlist ? ($level == 1 ? '<span class="caret"></span>' : '') : '',
+                    '@dropdown'  => $childlist ? ($level == 1 ? 'dropdown' : 'dropdown-submenu') : '',
+                    '@toggle'    => $childlist ? 'dropdown' : ''
+                ]);
+            }
+        }
+        return $str;
+    }
+
+    public static function getChannelInfo($params)
+    {
+        $config = get_addon_config('cms');
+        $cid = empty($params['cid']) ? '' : $params['cid'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'weigh' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $limit = empty($params['limit']) ? $row : $params['limit'];
+        $imgwidth = empty($params['imgwidth']) ? '' : $params['imgwidth'];
+        $imgheight = empty($params['imgheight']) ? '' : $params['imgheight'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+        $where = [];
+
+        list($cacheKey, $cacheExpire) = Service::getCacheKeyExpire('channelinfo', $params);
+
+        if ($cid !== '') {
+            $where['id'] = $cid;
+        }
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+        $order = $orderby == 'weigh' ? $order . ',id DESC' : $order;
+
+        $data = self::where($where)
+            ->where($condition)
+            ->field($field)
+            ->order($order)
+            ->limit($limit)
+            ->cache($cacheKey, $cacheExpire)
+            ->find();
+        if ($data) {
+            $list = [$data];
+            self::render($list, $imgwidth, $imgheight);
+            return reset($list);
+        } else {
+            return false;
+        }
+    }
+
+    public static function getChannelByLinktype($type, $source_id)
+    {
+        $channel = (new self())->where('linktype', $type)->where('linkid', $source_id)->order('weigh DESC,id DESC')->find();
+        return $channel;
+    }
+
+    public function model()
+    {
+        return $this->belongsTo('Modelx', 'model_id')->setEagerlyType(0);
+    }
+
+    public function parent()
+    {
+        return $this->belongsTo("Channel", "parent_id");
+    }
+
+}

+ 39 - 0
addons/cms/model/Collection.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace addons\cms\model;
+
+use think\Model;
+
+/**
+ * 收藏模型
+ */
+class Collection extends Model
+{
+    protected $name = "cms_collection";
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+
+    // 追加属性
+    protected $append = [
+        'create_date',
+    ];
+    protected static $config = [];
+
+    public function getCreateDateAttr($value, $data)
+    {
+        $value = $value ? $value : (isset($data['createtime']) ? $data['createtime'] : '');
+        return date('Y-m-d H:i:s', $value);
+    }
+
+    /**
+     * 关联模型
+     */
+    public function user()
+    {
+        return $this->belongsTo("\app\common\model\User", 'user_id', 'id', [], 'LEFT')->setEagerlyType(1);
+    }
+
+}

+ 254 - 0
addons/cms/model/Comment.php

@@ -0,0 +1,254 @@
+<?php
+
+namespace addons\cms\model;
+
+use addons\cms\library\CommentException;
+use addons\cms\library\IntCode;
+use addons\cms\library\Service;
+use app\common\library\Auth;
+use app\common\library\Email;
+use app\common\model\User;
+use think\Db;
+use think\Exception;
+use think\Model;
+use think\Validate;
+use traits\model\SoftDelete;
+
+/**
+ * 评论模型
+ */
+class Comment extends Model
+{
+    use SoftDelete;
+    protected $name = "cms_comment";
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = 'deletetime';
+
+    protected static $tagCount = 0;
+
+    // 追加属性
+    protected $append = [
+        'create_date',
+    ];
+
+    public function getCreateDateAttr($value, $data)
+    {
+        return human_date($data['createtime']);
+    }
+
+    /**
+     * 发表评论
+     * @param array $params
+     * @return bool
+     * @throws CommentException
+     * @throws Exception
+     */
+    public static function postComment($params = [])
+    {
+        $config = get_addon_config('cms');
+        $request = request();
+        $useragent = substr($request->server('HTTP_USER_AGENT', ''), 0, 255);
+        $ip = $request->ip(0, false);
+        $auth = Auth::instance();
+
+        if (!$auth->id) {
+            throw new Exception("请登录后发表评论");
+        }
+        if (isset($config['limitscore']['postcomment']) && $auth->score < $config['limitscore']['postcomment']) {
+            throw new Exception("积分必须大于{$config['limitscore']['postcomment']}才可以发表评论");
+        }
+        if (!isset($params['aid']) || !isset($params['content'])) {
+            throw new Exception("内容不能为空");
+        }
+
+        $params['user_id'] = $auth->id;
+        $params['pid'] = $params['pid'] ?? 0;
+        $params['type'] = $params['type'] ?? 'archives';
+        $params['content'] = nl2br($params['content']);
+
+        $archives = $params['type'] == 'archives' ? Archives::get($params['aid']) : ($params['type'] == 'special' ? Special::get($params['aid']) : Page::get($params['aid']));
+        if (!$archives || $archives['status'] == 'hidden') {
+            throw new Exception("文档未找到或正在审核");
+        }
+        if (!$archives['iscomment']) {
+            throw new Exception("文档评论功能已关闭");
+        }
+        $rule = [
+            'type'       => 'require|in:archives,page,special',
+            'pid'        => 'require|number',
+            'user_id'    => 'require|number',
+            'content|内容' => 'require|length:3,250',
+            '__token__'  => 'require|token',
+        ];
+        $message = [
+            'content.length' => '评论最少输入3个字符'
+        ];
+        $validate = new Validate($rule, $message);
+        $result = $validate->check($params);
+        if (!$result) {
+            throw new Exception($validate->getError());
+        }
+
+        //查找最后评论
+        $lastComment = self::where(['type' => $params['type'], 'aid' => $params['aid'], 'ip' => $ip])->order('id', 'desc')->find();
+        if ($lastComment && time() - $lastComment['createtime'] < 30) {
+            throw new Exception("对不起!您发表评论的速度过快!");
+        }
+        if ($lastComment && $lastComment['content'] == $params['content']) {
+            throw new Exception("您可能连续了相同的评论,请不要重复提交");
+        }
+        //审核状态
+        $status = 'normal';
+        if ($config['iscommentaudit'] == 1) {
+            $status = 'hidden';
+        } elseif ($config['iscommentaudit'] == 0) {
+            $status = 'normal';
+        } elseif ($config['iscommentaudit'] == -1) {
+            if (!Service::isContentLegal($params['content'])) {
+                $status = 'hidden';
+            }
+        }
+        $params['ip'] = $ip;
+        $params['useragent'] = $useragent;
+        $params['status'] = $status;
+
+        Db::startTrans();
+        try {
+            $model = new static();
+            $model->allowField(true)->save($params);
+            //评论正常则增加积分和统计
+            if ($status == 'normal') {
+                $archives->setInc('comments');
+                //增加积分
+                $status == 'normal' && User::score($config['score']['postcomment'] ?? 0, $auth->id, '发表评论');
+            }
+            Db::commit();
+        } catch (\Exception $e) {
+            Db::rollback();
+            throw new Exception("发表评论失败");
+        }
+
+        //发送通知
+        if ($status === 'hidden') {
+            Service::notice(config('cms.sitename') . '收到一条待审核评论');
+            throw new CommentException("发表评论成功,但评论需要审核后才会展示", 1);
+        }
+
+        if ($params['pid']) {
+            //查找父评论,是否并发邮件通知
+            $parentComment = self::get($params['pid'], 'user');
+            if ($parentComment && $parentComment['subscribe'] && Validate::is($parentComment->user->email, 'email')) {
+                $domain = $request->domain();
+                $config = get_addon_config('cms');
+                $title = "{$parentComment->user->nickname},您发表在《{$archives['title']}》上的评论有了新回复 - {$config['sitename']}";
+                $archivesurl = $domain . $archives['url'];
+                $unsubscribe_url = addon_url("cms/comment/unsubscribe", ['id' => $parentComment['id'], 'key' => md5($parentComment['id'] . $parentComment->user->email)], true, true);
+                $content = "亲爱的{$parentComment->user->nickname}:<br />您于" . date("Y-m-d H:i:s") .
+                    "在《<a href='{$archivesurl}' target='_blank'>{$archives['title']}</a>》上发表的评论<br /><blockquote>{$parentComment['content']}</blockquote>" .
+                    "<br />{$auth->nickname}发表了回复,内容是<br /><br />您可以<a href='{$archivesurl}'>点击查看评论详情</a>。" .
+                    "<br /><br />如果您不愿意再接收最新评论的通知,<a href='{$unsubscribe_url}'>请点击这里取消</a>";
+                try {
+                    $email = new Email;
+                    $result = $email
+                        ->to($parentComment->user->email)
+                        ->subject($title)
+                        ->message('<div style="min-height:550px; padding: 100px 55px 200px;">' . $content . '</div>')
+                        ->send();
+                } catch (\think\Exception $e) {
+                }
+            }
+        }
+
+        return self::with('user')->find($model->id);
+    }
+
+    /**
+     * 获取评论列表
+     * @param $params
+     * @return \think\Paginator
+     */
+    public static function getCommentList($params)
+    {
+        $type = empty($params['type']) ? 'archives' : $params['type'];
+        $aid = empty($params['aid']) ? 0 : $params['aid'];
+        $pid = empty($params['pid']) ? 0 : $params['pid'];
+        $condition = empty($params['condition']) ? '' : $params['condition'];
+        $field = empty($params['field']) ? '*' : $params['field'];
+        $fragment = empty($params['fragment']) ? 'comments' : $params['fragment'];
+        $row = empty($params['row']) ? 10 : (int)$params['row'];
+        $orderby = empty($params['orderby']) ? 'createtime' : $params['orderby'];
+        $orderway = empty($params['orderway']) ? 'desc' : strtolower($params['orderway']);
+        $pagesize = empty($params['pagesize']) ? $row : $params['pagesize'];
+        $orderway = in_array($orderway, ['asc', 'desc']) ? $orderway : 'desc';
+
+        $aid = is_numeric($aid) ? $aid : IntCode::decode($aid);
+        $pid = is_numeric($pid) ? $pid : IntCode::decode($pid);
+
+        self::$tagCount++;
+
+        $where = ['status' => 'normal'];
+        if ($type) {
+            $where['type'] = $type;
+        }
+        if ($aid) {
+            $where['aid'] = $aid;
+        }
+        if ($pid) {
+            $where['pid'] = $pid;
+        }
+        $order = $orderby == 'rand' ? Db::raw('rand()') : (preg_match("/\,|\s/", $orderby) ? $orderby : "{$orderby} {$orderway}");
+
+        $list = self::with('user')
+            ->where($where)
+            ->where($condition)
+            ->field($field)
+            ->order($order)
+            ->paginate($pagesize, false, ['var_page' => 'cp', 'fragment' => $fragment]);
+        self::render($list);
+        return $list;
+    }
+
+    public static function render(&$list)
+    {
+        return $list;
+    }
+
+    /**
+     * 关联会员模型
+     */
+    public function user()
+    {
+        return $this->belongsTo("app\common\model\User", "user_id", "id", [], "LEFT")->field('id,nickname,avatar,bio,email')->setEagerlyType(1);
+    }
+
+    /**
+     * 关联文章模型
+     */
+    public function archives()
+    {
+        return $this->belongsTo("addons\cms\model\Archives", 'aid', 'id', [], 'LEFT')->field('id,title,image,style,diyname,model_id,channel_id,likes,dislikes,tags,createtime')->setEagerlyType(1);
+    }
+
+    /**
+     * 关联单页模型
+     */
+    public function spage()
+    {
+        return $this->belongsTo("addons\cms\model\Page", 'aid', 'id', [], 'LEFT')->field('id,title,createtime')->setEagerlyType(1);
+    }
+
+    /**
+     * 关联模型
+     */
+    public function source()
+    {
+        $type = $this->getData('type');
+        $modelArr = ['page' => 'Page', 'archives' => 'Archives', 'special' => 'Special'];
+        $model = isset($modelArr[$type]) ? $modelArr[$type] : $modelArr['archives'];
+        return $this->belongsTo($model, "aid");
+    }
+}

+ 0 - 0
addons/cms/model/Diydata.php


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels