Browse Source

first add

wang jun 3 years ago
commit
74cdc34920
100 changed files with 9896 additions and 0 deletions
  1. 2 0
      .dockerignore
  2. 1 0
      .example.env
  3. 14 0
      .gitignore
  4. 2 0
      CONTRIBUTING.md
  5. 40 0
      Dockerfile
  6. 21 0
      LICENSE
  7. 159 0
      README.md
  8. 8 0
      api/demo/README.md
  9. 13 0
      api/demo/composer.json
  10. 159 0
      api/demo/controller/ArticlesController.php
  11. 25 0
      api/demo/controller/IndexController.php
  12. 5 0
      api/demo/route.php
  13. 61 0
      api/demo/swagger/request/DemoArticlesSave.php
  14. 63 0
      api/demo/swagger/response/DemoArticlesIndexResponse.php
  15. 7 0
      api/demo/vendor/autoload.php
  16. 445 0
      api/demo/vendor/composer/ClassLoader.php
  17. 21 0
      api/demo/vendor/composer/LICENSE
  18. 9 0
      api/demo/vendor/composer/autoload_classmap.php
  19. 9 0
      api/demo/vendor/composer/autoload_namespaces.php
  20. 9 0
      api/demo/vendor/composer/autoload_psr4.php
  21. 52 0
      api/demo/vendor/composer/autoload_real.php
  22. 15 0
      api/demo/vendor/composer/autoload_static.php
  23. 1 0
      api/demo/vendor/composer/installed.json
  24. 431 0
      api/portal/controller/ArticlesController.php
  25. 82 0
      api/portal/controller/CategoriesController.php
  26. 76 0
      api/portal/controller/IndexController.php
  27. 79 0
      api/portal/controller/ListsController.php
  28. 65 0
      api/portal/controller/PagesController.php
  29. 86 0
      api/portal/controller/TagsController.php
  30. 140 0
      api/portal/controller/UserArticlesController.php
  31. 41 0
      api/portal/controller/UserController.php
  32. 324 0
      api/portal/logic/PortalPostModel.php
  33. 88 0
      api/portal/model/PortalCategoryModel.php
  34. 31 0
      api/portal/model/PortalCategoryPostModel.php
  35. 497 0
      api/portal/model/PortalPostModel.php
  36. 30 0
      api/portal/model/PortalTagModel.php
  37. 35 0
      api/portal/model/PortalTagPostModel.php
  38. 21 0
      api/portal/model/RecycleBinModel.php
  39. 46 0
      api/portal/model/UserModel.php
  40. 22 0
      api/portal/route.php
  41. 59 0
      api/portal/service/PortalCategoryService.php
  42. 108 0
      api/portal/service/PortalPostService.php
  43. 100 0
      api/portal/service/PortalTagService.php
  44. 34 0
      api/portal/validate/ArticlesValidate.php
  45. 8 0
      app/demo/README.md
  46. 33 0
      app/demo/api/PageApi.php
  47. 5 0
      app/demo/command.php
  48. 31 0
      app/demo/command/Hello.php
  49. 13 0
      app/demo/composer.json
  50. 27 0
      app/demo/controller/AdminIndexController.php
  51. 32 0
      app/demo/controller/IndexController.php
  52. 19 0
      app/demo/hooks.php
  53. 66 0
      app/demo/lang/zh-cn.php
  54. 13 0
      app/demo/nav.php
  55. 53 0
      app/demo/url.php
  56. 15 0
      app/demo/user_action.php
  57. 11 0
      app/demo/vendor/.gitignore
  58. 67 0
      app/portal/api/CategoryApi.php
  59. 78 0
      app/portal/api/PageApi.php
  60. 462 0
      app/portal/controller/AdminArticleController.php
  61. 370 0
      app/portal/controller/AdminCategoryController.php
  62. 32 0
      app/portal/controller/AdminIndexController.php
  63. 239 0
      app/portal/controller/AdminPageController.php
  64. 166 0
      app/portal/controller/AdminTagController.php
  65. 101 0
      app/portal/controller/ArticleController.php
  66. 21 0
      app/portal/controller/IndexController.php
  67. 39 0
      app/portal/controller/ListController.php
  68. 44 0
      app/portal/controller/PageController.php
  69. 32 0
      app/portal/controller/SearchController.php
  70. 47 0
      app/portal/controller/TagController.php
  71. 113 0
      app/portal/data/portal.sql
  72. 96 0
      app/portal/hooks.php
  73. 13 0
      app/portal/lang/en-us.php
  74. 15 0
      app/portal/lang/en-us/common.php
  75. 21 0
      app/portal/lang/zh-cn.php
  76. 16 0
      app/portal/lang/zh-cn/common.php
  77. 13 0
      app/portal/lang/zh-cn/home.php
  78. 234 0
      app/portal/model/PortalCategoryModel.php
  79. 23 0
      app/portal/model/PortalCategoryPostModel.php
  80. 410 0
      app/portal/model/PortalPostModel.php
  81. 27 0
      app/portal/model/PortalTagModel.php
  82. 23 0
      app/portal/model/PortalTagPostModel.php
  83. 23 0
      app/portal/model/RecycleBinModel.php
  84. 29 0
      app/portal/model/UserModel.php
  85. 14 0
      app/portal/nav.php
  86. 501 0
      app/portal/service/ApiService.php
  87. 297 0
      app/portal/service/PostService.php
  88. 372 0
      app/portal/taglib/Portal.php
  89. 53 0
      app/portal/url.php
  90. 15 0
      app/portal/user_action.php
  91. 30 0
      app/portal/validate/AdminArticleValidate.php
  92. 51 0
      app/portal/validate/AdminPageValidate.php
  93. 55 0
      app/portal/validate/PortalCategoryValidate.php
  94. 1 0
      app/portal/version
  95. 73 0
      composer.json
  96. 1735 0
      composer.lock
  97. 2 0
      data/.gitignore
  98. 29 0
      docker-compose.yml
  99. 12 0
      public/.htaccess
  100. 40 0
      public/api.php

+ 2 - 0
.dockerignore

@@ -0,0 +1,2 @@
+# Created by .ignore support plugin (hsz.mobi)
+.git*

+ 1 - 0
.example.env

@@ -0,0 +1 @@
+APP_DEBUG = true

# APP
APP_DEFAULT_TIMEZONE = Asia/Shanghai

#数据库 DATABASE
DATABASE_DRIVER = mysql
DATABASE_TYPE = mysql
DATABASE_HOSTNAME = 127.0.0.1
DATABASE_DATABASE = test
DATABASE_USERNAME = username
DATABASE_PASSWORD = password
DATABASE_HOSTPORT = 3306
DATABASE_CHARSET = utf8mb4
DATABASE_PREFIX = cmf_
DATABASE_DEBUG = true
DATABASE_AUTHCODE = xxxxx

#语言配置 LANG
LANG_DEFAULT_LANG = zh-cn

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+.buildpath
+.DS_Store
+.project
+.settings
+.vscode
+.idea
+.git
+/build
+/public/assets/dist
+/node_modules
+Vagrantfile
+.vagrant
+*.log
+.env

+ 2 - 0
CONTRIBUTING.md

@@ -0,0 +1,2 @@
+如何贡献我的源代码
+===

+ 40 - 0
Dockerfile

@@ -0,0 +1,40 @@
+FROM php:7.1-apache
+
+MAINTAINER jayknoxqu@gmail.com
+
+#设置容器时区
+ENV TZ Asia/Shanghai
+
+#设置程序入口
+ENV APACHE_DOCUMENT_ROOT /var/www/html/public
+
+RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone \
+    && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
+    && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
+    #添加阿里云的镜像源
+    && mv /etc/apt/sources.list /etc/apt/sources.list.bak \
+    && echo "deb http://mirrors.aliyun.com/debian/ stretch main non-free contrib" >/etc/apt/sources.list \
+    && echo "deb http://mirrors.aliyun.com/debian-security stretch/updates main" >>/etc/apt/sources.list \
+    && echo "deb http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib" >>/etc/apt/sources.list \
+    && echo "deb http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib" >>/etc/apt/sources.list \
+    #安装程序依赖库
+    && apt-get update && apt-get install -y \
+              libfreetype6-dev \
+              libjpeg62-turbo-dev \
+              libpng-dev \
+    #安装 PHP 依赖
+    && docker-php-ext-install pdo_mysql \
+    && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
+    && docker-php-ext-install -j$(nproc) gd \
+    #删除包缓存中的所有包
+    && apt-get clean \
+    && apt-get autoclean \
+    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
+
+#复制代码到PHP容器
+COPY . /var/www/html
+
+# 开启URL重写 并且 添加目录权限
+RUN a2enmod rewrite \
+    && chmod -R 0755 /var/www/html \
+    && chown -R www-data:www-data /var/www/html

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2013-present ThinkCMF (https://www.thinkcmf.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.

+ 159 - 0
README.md

@@ -0,0 +1,159 @@
+ThinkCMF 6.0.4开发版
+===============
+**开发版,请不要用于正式环境!实际项目请下载正式版**
+
+
+### ThinkCMF6.0主要特性
+* 框架协议依旧为`MIT`,让你更自由地飞
+* 基于`ThinkPHP 6.0`重构,核心代码兼容5.1版本,保证老用户最小升级成本
+* API增加Swagger支持
+* 增加`.env`环境配置支持
+
+### 废弃功能
+* 钩子app_begin(使用module_init)
+* 钩子response_send
+* 钩子response_end(使用http_end)
+* 钩子view_filter
+
+### 已完成功能
+- [x] url美化(这是个大大坑)
+- [x] `url()`方法单独维护
+- [x] 后台加密码
+- [x] 插件功能
+- [x] 插件钩子功能
+- [x] 补齐相关钩子(action_begin、module_init)
+- [x] 迁移behavior到listener
+- [x] 应用导航共享
+- [x] 应用后台菜单注解
+- [x] 应用钩子配置
+- [x] 用户操作配置
+- [x] URL 规则配置
+- [x] 插件和应用的command功能
+- [x] 网站安装功能
+- [x] `View::share`
+- [x] 规范所有`Db::name()`为Model调用
+- [x] 单独维护`think-template`,`think-view`
+- [x] 单独维护`cmf-route`
+- [x] API
+- [x] API基顾功能
+- [x] API用户基顾功能
+- [x] 应用第三方库的支持
+- [x] 傻瓜式模板
+- [x] 前台模板切换
+- [x] 后台多模板机制
+- [x] 默认过滤器htmlspecialchars
+- [x] 文件上传
+- [x] 验证码优化
+- [x] Swagger规范
+- [x] 强制所有创建,更新,删除操作为POST请求
+- [x] 增加基础控制器validateFailError()方法
+ 
+### 开发手册
+https://www.thinkcmf.com/docs/cmf6
+
+### Git仓库
+
+1. 码云:https://gitee.com/thinkcmf/ThinkCMF/tree/6.0 主要仓库
+2. GitHub:https://github.com/thinkcmf/thinkcmf/tree/6.0 国际镜像
+
+
+
+### 环境推荐
+> php7.3
+
+> mysql 5.7+
+
+> 打开rewrite
+
+
+### 最低环境要求
+> php7.2.5+
+
+> mysql 5.5+ (mysql5.1安装时选择utf8编码,不支持表情符)
+
+> 打开rewrite
+
+### 安装程序
+
+1. public目录做为网站根目录,入口文件在 public/index.php
+2. 配置好网站,请访问http://你的域名
+
+enjoy your cmf~!  
+
+### Swagger
+#### 开启swagger
+调试模式下访问: http://你的域名/swagger
+
+#### 相关文档
+**OpenAPI** (https://www.openapis.org)  
+**Swagger-PHP** (https://zircote.github.io/swagger-php/)
+
+
+### 待优化功能
+- [ ] 总结数据库和模型统一使用规范
+- [ ] 应用单独配置目录(待定)
+- [ ] 移动Model的逻辑方法到Service里
+
+### 升级指导
+#### 6.0.1升级到6.0.3
+1. composer.json文件里的`autoload.psr-4.themes\\`改为`public/themes`
+2. 安装应用市场包`composer require thinkcmf/cmf-appstore`
+3. `public/themes`,`public/static`静态文件也有更新
+4. 删除`public/themes/admin_simpleboot3/admin`目录下的`app_store`目录
+5. `composer update`
+
+### 更新日志
+#### 6.0.3
+* 自定义分页类
+* 优化后台模板设计
+* 优化后台菜单导入
+* 修复验证器使用错误
+* 修复路由禁用报错
+* 修复插件模板异常类引入错误
+
+#### 6.0.2
+* 增加插件市场支持插件在线安装
+* 增加后台不存在模板文件检测并切换到默认模板
+* 移动swagger功能到插件
+* 优化插件后台权限检查
+* 修复url美化报错
+* 规范env命名,方便编辑器跳转
+* 修正themes命名空间
+* 修复角色删除问题
+* 修复管理员删除问题
+* 修复幻灯片删除问题
+* 优化用户注册
+* 优化后台菜单导入
+
+
+#### 6.0.1
+* 兼容php8.0
+* 升级到`tp6.0.7`
+* 增加插件后台基类`admin_init`钩子
+* 优化cmf版本获取
+* 优化`cmf_clear_cache()`函数
+* 修复插件URL美化报错
+* 修复上传报错
+* 修复`demo应用 page/nav`数据源演示报错
+* 修复导入后台菜单报错
+* 修复url美化问题
+* 修复头像上传报错
+
+
+#### 6.0.0
+* 升级到ThinkPHP6.0
+* API增加Swagger支持
+* 增加`.env`环境配置支持
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 8 - 0
api/demo/README.md

@@ -0,0 +1,8 @@
+ThinkCMF API演示应用
+===============
+
+### 加载第三方库
+支持使用`composer`加载第三方库
+```
+composer require phpoffice/phpspreadsheet
+```

+ 13 - 0
api/demo/composer.json

@@ -0,0 +1,13 @@
+{
+    "name": "thinkcmf/apis-demo",
+    "description": "ThinkCMF demo api",
+    "type": "cmf-api",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "catman",
+            "email": "catman@thinkcmf.com"
+        }
+    ],
+    "require": {}
+}

+ 159 - 0
api/demo/controller/ArticlesController.php

@@ -0,0 +1,159 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+
+namespace api\demo\controller;
+
+use cmf\controller\RestBaseController;
+use OpenApi\Annotations as OA;
+
+/**
+ * Class ArticlesController
+ * @package api\demo\controller
+ */
+class ArticlesController extends RestBaseController
+{
+    /**
+     * @OA\Get(
+     *     tags={"demo"},
+     *     path="/demo/articles",
+     *     @OA\Parameter(
+     *         name="page",
+     *         in="query",
+     *         description="page param",
+     *         required=false,
+     *         @OA\Schema(
+     *             type="string",
+     *         )
+     *     ),
+     *     @OA\Response(
+     *         response="200",
+     *         description="success",
+     *         @OA\JsonContent(
+     *             example={
+     *                 "id": "a3fb6",
+     *                 "name": "sss"
+     *             },
+     *             ref="#/components/schemas/DemoArticlesIndexResponse"
+     *         )
+     *     ),
+     *     @OA\Response(
+     *         response="default",
+     *         description="error operation",
+     *         @OA\JsonContent(ref="#/components/schemas/ErrorResponse")
+     *     ),
+     * )
+     */
+    public function index()
+    {
+        $articles = [
+            ['title' => 'article title1'],
+            ['title' => 'article title2'],
+        ];
+        $this->success('请求成功!', ['articles' => $articles]);
+    }
+
+    /**
+     * @OA\Post(
+     *     tags={"demo"},
+     *     path="/demo/articles",
+     *     @OA\RequestBody(
+     *         required=true,
+     *         description="Created user object",
+     *         @OA\MediaType(
+     *             mediaType="application/x-www-form-urlencoded",
+     *             @OA\Schema(ref="#/components/schemas/DemoArticlesSave")
+     *         )
+     *     ),
+     *     @OA\Response(response="200", description="An example resource"),
+     *     @OA\Response(response="default", description="An example resource")
+     * )
+     */
+    public function save()
+    {
+    }
+
+    /**
+     * @OA\Get(
+     *     tags={"demo"},
+     *     path="/demo/articles/{id}",
+     *     @OA\Parameter(
+     *         name="id",
+     *         in="path",
+     *         description="articles id",
+     *         required=true,
+     *         @OA\Schema(
+     *             type="string",
+     *         )
+     *     ),
+     *     @OA\Response(response="200", description="An example resource"),
+     *     @OA\Response(response="default", description="An example resource")
+     * )
+     */
+    public function read($id)
+    {
+    }
+
+    /**
+     * @OA\Put(
+     *     tags={"demo"},
+     *     path="/demo/articles/{id}",
+     *     @OA\Parameter(
+     *         name="id",
+     *         in="path",
+     *         description="articles id",
+     *         required=true,
+     *         @OA\Schema(
+     *             type="string",
+     *         )
+     *     ),
+     *     @OA\Response(
+     *         response="200",
+     *         description="success",
+     *         @OA\JsonContent(ref="#/components/schemas/SuccessResponse")
+     *     ),
+     *     @OA\Response(
+     *         response="default",
+     *         description="error operation",
+     *         @OA\JsonContent(ref="#/components/schemas/ErrorResponse")
+     *     ),
+     * )
+     */
+    public function update($id)
+    {
+    }
+
+    /**
+     * @OA\Delete(
+     *     tags={"demo"},
+     *     path="/demo/articles/{id}",
+     *     @OA\Parameter(
+     *         name="id",
+     *         in="path",
+     *         description="articles id",
+     *         required=true,
+     *         @OA\Schema(
+     *             type="string",
+     *         )
+     *     ),
+     *     @OA\Response(
+     *         response="200",
+     *         description="success",
+     *         @OA\JsonContent(ref="#/components/schemas/SuccessResponse")
+     *     ),
+     *     @OA\Response(
+     *         response="default",
+     *         description="error operation",
+     *         @OA\JsonContent(ref="#/components/schemas/ErrorResponse")
+     *     ),
+     * )
+     */
+    public function delete($id)
+    {
+    }
+}

+ 25 - 0
api/demo/controller/IndexController.php

@@ -0,0 +1,25 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+
+namespace api\demo\controller;
+
+use cmf\controller\RestBaseController;
+
+/**
+ * Class IndexController
+ * @package api\demo\controller
+ */
+class IndexController extends RestBaseController
+{
+    public function index()
+    {
+        $data = $this->request->param();
+        $this->success('请求成功!', ['test' => 'test', 'data' => $data]);
+    }
+}

+ 5 - 0
api/demo/route.php

@@ -0,0 +1,5 @@
+<?php
+
+use think\facade\Route;
+
+Route::resource('demo/articles', 'demo/Articles');

+ 61 - 0
api/demo/swagger/request/DemoArticlesSave.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace api\demo\swagger\request;
+
+use OpenApi\Annotations as OA;
+
+/**
+ * @OA\Schema(@OA\Xml(name="DemoArticlesSave"))
+ */
+class DemoArticlesSave
+{
+
+    /**
+     * @OA\Property(format="int64")
+     * @var int
+     */
+    public $id;
+
+    /**
+     * @OA\Property()
+     * @var string
+     */
+    public $username;
+
+    /**
+     * @OA\Property()
+     * @var string
+     */
+    public $firstName;
+
+    /**
+     * @OA\Property()
+     * @var string
+     */
+    public $lastName;
+
+    /**
+     * @var string
+     * @OA\Property()
+     */
+    public $email;
+
+    /**
+     * @var string
+     * @OA\Property()
+     */
+    public $password;
+
+    /**
+     * @var string
+     * @OA\Property()
+     */
+    public $phone;
+
+    /**
+     * User Status
+     * @var int
+     * @OA\Property(format="int32")
+     */
+    public $userStatus;
+}

+ 63 - 0
api/demo/swagger/response/DemoArticlesIndexResponse.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace api\demo\swagger\response;
+
+use api\swagger\reponse\SuccessResponse;
+
+use OpenApi\Annotations as OA;
+
+
+/**
+ * @OA\Schema()
+ */
+class DemoArticlesIndexResponse extends SuccessResponse
+{
+
+    /**
+     * @OA\Property(
+     *     type="object",
+     *     ref="#/components/schemas/DemoArticlesIndexResponseData"
+     * )
+     * @var object
+     */
+    public $data;
+
+}
+
+/**
+ * @OA\Schema()
+ */
+class DemoArticlesIndexResponseData
+{
+
+    /**
+     * @OA\Property()
+     * @var int
+     */
+    public $total;
+
+    /**
+     * @OA\Property(
+     *     type="array",
+     *     @OA\Items(ref="#/components/schemas/DemoArticlesIndexResponseDataListItem")
+     * )
+     * @var array
+     */
+    public $list;
+
+}
+
+
+/**
+ * @OA\Schema()
+ */
+class DemoArticlesIndexResponseDataListItem
+{
+    /**
+     * @OA\Property()
+     * @var string
+     */
+    public $name;
+
+
+}

+ 7 - 0
api/demo/vendor/autoload.php

@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInitba11d0d49ab8a6cda13c6c7344f77971::getLoader();

+ 445 - 0
api/demo/vendor/composer/ClassLoader.php

@@ -0,0 +1,445 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ *     $loader = new \Composer\Autoload\ClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->add('Symfony\Component', __DIR__.'/component');
+ *     $loader->add('Symfony',           __DIR__.'/framework');
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see    http://www.php-fig.org/psr/psr-0/
+ * @see    http://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+    // PSR-4
+    private $prefixLengthsPsr4 = array();
+    private $prefixDirsPsr4 = array();
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    private $prefixesPsr0 = array();
+    private $fallbackDirsPsr0 = array();
+
+    private $useIncludePath = false;
+    private $classMap = array();
+    private $classMapAuthoritative = false;
+    private $missingClasses = array();
+    private $apcuPrefix;
+
+    public function getPrefixes()
+    {
+        if (!empty($this->prefixesPsr0)) {
+            return call_user_func_array('array_merge', $this->prefixesPsr0);
+        }
+
+        return array();
+    }
+
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
+    }
+
+    public function getFallbackDirs()
+    {
+        return $this->fallbackDirsPsr0;
+    }
+
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
+    }
+
+    public function getClassMap()
+    {
+        return $this->classMap;
+    }
+
+    /**
+     * @param array $classMap Class to filename map
+     */
+    public function addClassMap(array $classMap)
+    {
+        if ($this->classMap) {
+            $this->classMap = array_merge($this->classMap, $classMap);
+        } else {
+            $this->classMap = $classMap;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
+     *
+     * @param string       $prefix  The prefix
+     * @param array|string $paths   The PSR-0 root directories
+     * @param bool         $prepend Whether to prepend the directories
+     */
+    public function add($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            if ($prepend) {
+                $this->fallbackDirsPsr0 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr0
+                );
+            } else {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
+                    (array) $paths
+                );
+            }
+
+            return;
+        }
+
+        $first = $prefix[0];
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+            return;
+        }
+        if ($prepend) {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixesPsr0[$first][$prefix]
+            );
+        } else {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
+     *
+     * @param string       $prefix  The prefix/namespace, with trailing '\\'
+     * @param array|string $paths   The PSR-4 base directories
+     * @param bool         $prepend Whether to prepend the directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    (array) $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string       $prefix The prefix
+     * @param array|string $paths  The PSR-0 base directories
+     */
+    public function set($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string       $prefix The prefix/namespace, with trailing '\\'
+     * @param array|string $paths  The PSR-4 base directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function setPsr4($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Turns on searching the include path for class files.
+     *
+     * @param bool $useIncludePath
+     */
+    public function setUseIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return bool
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Turns off searching the prefix and fallback directories for classes
+     * that have not been registered with the class map.
+     *
+     * @param bool $classMapAuthoritative
+     */
+    public function setClassMapAuthoritative($classMapAuthoritative)
+    {
+        $this->classMapAuthoritative = $classMapAuthoritative;
+    }
+
+    /**
+     * Should class lookup fail if not found in the current class map?
+     *
+     * @return bool
+     */
+    public function isClassMapAuthoritative()
+    {
+        return $this->classMapAuthoritative;
+    }
+
+    /**
+     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+     *
+     * @param string|null $apcuPrefix
+     */
+    public function setApcuPrefix($apcuPrefix)
+    {
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+    }
+
+    /**
+     * The APCu prefix in use, or null if APCu caching is not enabled.
+     *
+     * @return string|null
+     */
+    public function getApcuPrefix()
+    {
+        return $this->apcuPrefix;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+    }
+
+    /**
+     * Unregisters this instance as an autoloader.
+     */
+    public function unregister()
+    {
+        spl_autoload_unregister(array($this, 'loadClass'));
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param  string    $class The name of the class
+     * @return bool|null True if loaded, null otherwise
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            includeFile($file);
+
+            return true;
+        }
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|false The path if found, false otherwise
+     */
+    public function findFile($class)
+    {
+        // class map lookup
+        if (isset($this->classMap[$class])) {
+            return $this->classMap[$class];
+        }
+        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+            return false;
+        }
+        if (null !== $this->apcuPrefix) {
+            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+            if ($hit) {
+                return $file;
+            }
+        }
+
+        $file = $this->findFileWithExtension($class, '.php');
+
+        // Search for Hack files if we are running on HHVM
+        if (false === $file && defined('HHVM_VERSION')) {
+            $file = $this->findFileWithExtension($class, '.hh');
+        }
+
+        if (null !== $this->apcuPrefix) {
+            apcu_add($this->apcuPrefix.$class, $file);
+        }
+
+        if (false === $file) {
+            // Remember that this class does not exist.
+            $this->missingClasses[$class] = true;
+        }
+
+        return $file;
+    }
+
+    private function findFileWithExtension($class, $ext)
+    {
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            $subPath = $class;
+            while (false !== $lastPos = strrpos($subPath, '\\')) {
+                $subPath = substr($subPath, 0, $lastPos);
+                $search = $subPath . '\\';
+                if (isset($this->prefixDirsPsr4[$search])) {
+                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
+                        if (file_exists($file = $dir . $pathEnd)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+        }
+
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+            return $file;
+        }
+
+        return false;
+    }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+    include $file;
+}

+ 21 - 0
api/demo/vendor/composer/LICENSE

@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+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.
+

+ 9 - 0
api/demo/vendor/composer/autoload_classmap.php

@@ -0,0 +1,9 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);

+ 9 - 0
api/demo/vendor/composer/autoload_namespaces.php

@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);

+ 9 - 0
api/demo/vendor/composer/autoload_psr4.php

@@ -0,0 +1,9 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);

+ 52 - 0
api/demo/vendor/composer/autoload_real.php

@@ -0,0 +1,52 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInitba11d0d49ab8a6cda13c6c7344f77971
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        spl_autoload_register(array('ComposerAutoloaderInitba11d0d49ab8a6cda13c6c7344f77971', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
+        spl_autoload_unregister(array('ComposerAutoloaderInitba11d0d49ab8a6cda13c6c7344f77971', 'loadClassLoader'));
+
+        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
+        if ($useStaticLoader) {
+            require_once __DIR__ . '/autoload_static.php';
+
+            call_user_func(\Composer\Autoload\ComposerStaticInitba11d0d49ab8a6cda13c6c7344f77971::getInitializer($loader));
+        } else {
+            $map = require __DIR__ . '/autoload_namespaces.php';
+            foreach ($map as $namespace => $path) {
+                $loader->set($namespace, $path);
+            }
+
+            $map = require __DIR__ . '/autoload_psr4.php';
+            foreach ($map as $namespace => $path) {
+                $loader->setPsr4($namespace, $path);
+            }
+
+            $classMap = require __DIR__ . '/autoload_classmap.php';
+            if ($classMap) {
+                $loader->addClassMap($classMap);
+            }
+        }
+
+        $loader->register(true);
+
+        return $loader;
+    }
+}

+ 15 - 0
api/demo/vendor/composer/autoload_static.php

@@ -0,0 +1,15 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInitba11d0d49ab8a6cda13c6c7344f77971
+{
+    public static function getInitializer(ClassLoader $loader)
+    {
+        return \Closure::bind(function () use ($loader) {
+
+        }, null, ClassLoader::class);
+    }
+}

+ 1 - 0
api/demo/vendor/composer/installed.json

@@ -0,0 +1 @@
+[]

+ 431 - 0
api/portal/controller/ArticlesController.php

@@ -0,0 +1,431 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+
+namespace api\portal\controller;
+
+use api\portal\service\PortalPostService;
+use api\user\model\UserFavoriteModel;
+use api\user\model\UserLikeModel;
+use cmf\controller\RestBaseController;
+use api\portal\model\PortalPostModel;
+use think\Db;
+
+class ArticlesController extends RestBaseController
+{
+    /**
+     * 文章列表
+     * @throws \think\exception\DbException
+     */
+    public function index()
+    {
+        $params      = $this->request->get();
+        $postService = new PortalPostService();
+        $data        = $postService->postArticles($params);
+        //是否需要关联模型
+        if (!$data->isEmpty()) {
+            if (!empty($params['relation'])) {
+
+                $allowedRelations = allowed_relations(['user', 'categories'], $params['relation']);
+
+                if (!empty($allowedRelations)) {
+                    $data->load('user');
+                    $data->append($allowedRelations);
+                }
+            }
+        }
+        if (empty($this->apiVersion) || $this->apiVersion == '1.0.0') {
+            $response = $data;
+        } else {
+            $response = ['list' => $data];
+        }
+        $this->success('请求成功!', $response);
+    }
+
+    /**
+     * 获取指定的文章
+     * @param $id
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function read($id)
+    {
+        if (intval($id) === 0) {
+            $this->error('无效的文章id!');
+        } else {
+            $postModel = new PortalPostModel();
+            $data      = $postModel->where('id', $id)->find();
+
+            if (empty($data)) {
+                $this->error('文章不存在!');
+            } else {
+                $postModel->where('id', $id)->setInc('post_hits');
+                $url         = cmf_url('portal/Article/index', ['id' => $id, 'cid' => $data['categories'][0]['id']], true, true);
+                $data['url'] = $url;
+                $this->success('请求成功!', $data);
+            }
+
+        }
+    }
+
+    /**
+     * 我的文章列表
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function my()
+    {
+        $params            = $this->request->get();
+        $params['user_id'] = $this->getUserId();
+
+        $postService = new PortalPostService();
+        $data        = $postService->postArticles($params);
+
+        if (empty($this->apiVersion) || $this->apiVersion == '1.0.0') {
+            $response = [$data];
+        } else {
+            $response = ['list' => $data];
+        }
+
+        $this->success('请求成功!', $response);
+    }
+
+    /**
+     * 添加文章
+     * @throws \think\Exception
+     */
+    public function save()
+    {
+        $data            = $this->request->post();
+        $data['user_id'] = $this->getUserId();
+        $result          = $this->validate($data, 'Articles.article');
+        if ($result !== true) {
+            $this->error($result);
+        }
+
+        if (empty($data['published_time'])) {
+            $data['published_time'] = time();
+        }
+        $postModel = new PortalPostModel();
+        $postModel->addArticle($data);
+        $this->success('添加成功!');
+    }
+
+    /**
+     * 更新文章
+     * @param $id
+     * @throws \think\Exception
+     */
+    public function update($id)
+    {
+        $data   = $this->request->put();
+        $result = $this->validate($data, 'Articles.article');
+        if ($result !== true) {
+            $this->error($result);
+        }
+        $postModel = new PortalPostModel();
+        $res       = $postModel->articleFind(['id' => $id, 'user_id' => $this->getUserId()]);
+        if (empty($res)) {
+            $this->error('文章不存在或者已经删除!');
+        }
+
+        $result = $postModel->editArticle($data, $id, $this->getUserId());
+
+        if ($result === false) {
+            $this->error('编辑失败!');
+        } else {
+            $this->success('编辑成功!');
+        }
+    }
+
+    /**
+     * 删除文章
+     * @param $id
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error('无效的文章id');
+        }
+        $postModel = new PortalPostModel();
+        $result    = $postModel->deleteArticle($id, $this->getUserId());
+        if ($result == -1) {
+            $this->error('文章已删除');
+        }
+        if ($result) {
+            $this->success('删除成功!');
+        } else {
+            $this->error('删除失败!');
+        }
+    }
+
+    /**
+     * 批量删除文章
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function deletes()
+    {
+        $ids = $this->request->post('ids/a');
+        if (empty($ids)) {
+            $this->error('文章id不能为空');
+        }
+        $postModel = new PortalPostModel();
+        $result    = $postModel->deleteArticle($ids, $this->getUserId());
+        if ($result == -1) {
+            $this->error('文章已删除');
+        }
+        if ($result) {
+            $this->success('删除成功!');
+        } else {
+            $this->error('删除失败!');
+        }
+    }
+
+    /**
+     * 搜索查询
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function search()
+    {
+        $params = $this->request->get();
+
+        if (!empty($params['keyword'])) {
+            $postService = new PortalPostService();
+            $data        = $postService->postArticles($params);
+
+            if (empty($this->apiVersion) || $this->apiVersion == '1.0.0') {
+                $response = $data;
+            } else {
+                $response = ['list' => $data,];
+            }
+            $this->success('请求成功!', $response);
+        } else {
+            $this->error('搜索关键词不能为空!');
+        }
+
+    }
+
+    /**
+     * 文章点赞
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function doLike()
+    {
+        $userId    = $this->getUserId();
+        $articleId = $this->request->param('id', 0, 'intval');
+
+        $userLikeModel = new UserLikeModel();
+
+        $findLikeCount = $userLikeModel->where([
+            'user_id'   => $userId,
+            'object_id' => $articleId
+        ])->where('table_name', 'portal_post')->count();
+
+        if (empty($findLikeCount)) {
+            $postModel = new PortalPostModel();
+            $article   = $postModel->where('id', $articleId)->field('id,post_title,post_excerpt,more')->find();
+
+            if (empty($article)) {
+                $this->error('文章不存在!');
+            }
+
+            Db::startTrans();
+            try {
+                $postModel->where(['id' => $articleId])->setInc('post_like');
+                $thumbnail = empty($article['more']['thumbnail']) ? '' : $article['more']['thumbnail'];
+                $userLikeModel->insert([
+                    'user_id'     => $userId,
+                    'object_id'   => $articleId,
+                    'table_name'  => 'portal_post',
+                    'title'       => $article['post_title'],
+                    'thumbnail'   => $thumbnail,
+                    'description' => $article['post_excerpt'],
+                    'url'         => json_encode(['action' => 'portal/Article/index', 'param' => ['id' => $articleId, 'cid' => $article['categories'][0]['id']]]),
+                    'create_time' => time()
+                ]);
+                Db::commit();
+            } catch (\Exception $e) {
+                Db::rollback();
+                $this->error('点赞失败!');
+            }
+
+            $likeCount = $postModel->where('id', $articleId)->value('post_like');
+            $this->success("赞好啦!", ['post_like' => $likeCount]);
+        } else {
+            $this->error("您已赞过啦!");
+        }
+    }
+
+    /**
+     * 取消文章点赞
+     */
+    public function cancelLike()
+    {
+        $userId = $this->getUserId();
+
+        $articleId = $this->request->param('id', 0, 'intval');
+
+        $userLikeModel = new UserLikeModel();
+
+        $findLikeCount = $userLikeModel->where([
+            'user_id'   => $userId,
+            'object_id' => $articleId
+        ])->where('table_name', 'portal_post')->count();
+
+        if (!empty($findLikeCount)) {
+            $postModel = new PortalPostModel();
+            Db::startTrans();
+            try {
+                $postModel->where(['id' => $articleId])->setDec('post_like');
+                $userLikeModel->where([
+                    'user_id'   => $userId,
+                    'object_id' => $articleId
+                ])->where('table_name', 'portal_post')->delete();
+                Db::commit();
+            } catch (\Exception $e) {
+                Db::rollback();
+                $this->error('取消点赞失败!');
+            }
+
+            $likeCount = $postModel->where('id', $articleId)->value('post_like');
+            $this->success("取消点赞成功!", ['post_like' => $likeCount]);
+        } else {
+            $this->error("您还没赞过!");
+        }
+    }
+
+    /**
+     * 文章收藏
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function doFavorite()
+    {
+        $userId = $this->getUserId();
+
+        $articleId = $this->request->param('id', 0, 'intval');
+
+        $userFavoriteModel = new UserFavoriteModel();
+
+        $findFavoriteCount = $userFavoriteModel->where([
+            'user_id'   => $userId,
+            'object_id' => $articleId
+        ])->where('table_name', 'portal_post')->count();
+
+        if (empty($findFavoriteCount)) {
+            $postModel = new PortalPostModel();
+            $article   = $postModel->where(['id' => $articleId])->field('id,post_title,post_excerpt,more')->find();
+            if (empty($article)) {
+                $this->error('文章不存在!');
+            }
+
+            Db::startTrans();
+            try {
+                $postModel->where(['id' => $articleId])->setInc('post_favorites');
+                $thumbnail = empty($article['more']['thumbnail']) ? '' : $article['more']['thumbnail'];
+                $userFavoriteModel->insert([
+                    'user_id'     => $userId,
+                    'object_id'   => $articleId,
+                    'table_name'  => 'portal_post',
+                    'thumbnail'   => $thumbnail,
+                    'title'       => $article['post_title'],
+                    'description' => $article['post_excerpt'],
+                    'url'         => json_encode(['action' => 'portal/Article/index', 'param' => ['id' => $articleId, 'cid' => $article['categories'][0]['id']]]),
+                    'create_time' => time()
+                ]);
+                Db::commit();
+            } catch (\Exception $e) {
+                Db::rollback();
+
+                $this->error('收藏失败!');
+            }
+
+            $favoriteCount = $postModel->where('id', $articleId)->value('post_favorites');
+            $this->success("收藏好啦!", ['post_favorites' => $favoriteCount]);
+        } else {
+            $this->error("您已收藏过啦!");
+        }
+    }
+
+    /**
+     * 取消文章收藏
+     */
+    public function cancelFavorite()
+    {
+        $userId = $this->getUserId();
+
+        $articleId = $this->request->param('id', 0, 'intval');
+
+        $userFavoriteModel = new UserFavoriteModel();
+
+        $findFavoriteCount = $userFavoriteModel->where([
+            'user_id'   => $userId,
+            'object_id' => $articleId
+        ])->where('table_name', 'portal_post')->count();
+
+        if (!empty($findFavoriteCount)) {
+            $postModel = new PortalPostModel();
+            Db::startTrans();
+            try {
+                $postModel->where(['id' => $articleId])->setDec('post_favorites');
+                $userFavoriteModel->where([
+                    'user_id'   => $userId,
+                    'object_id' => $articleId
+                ])->where('table_name', 'portal_post')->delete();
+                Db::commit();
+            } catch (\Exception $e) {
+                Db::rollback();
+                $this->error('取消失败!');
+            }
+
+            $favoriteCount = $postModel->where('id', $articleId)->value('post_favorites');
+            $this->success("取消成功!", ['post_favorites' => $favoriteCount]);
+        } else {
+            $this->error("您还没收藏过!");
+        }
+    }
+
+
+    /**
+     * 相关文章列表
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function relatedArticles()
+    {
+        $articleId  = $this->request->param('id', 0, 'intval');
+        $categoryId = Db::name('portal_category_post')->where('post_id', $articleId)->value('category_id');
+
+        $postModel = new PortalPostModel();
+        $articles  = $postModel
+            ->alias('post')
+            ->join('__PORTAL_CATEGORY_POST__ category_post', 'post.id=category_post.post_id')
+            ->where(['post.delete_time' => 0, 'post.post_status' => 1, 'category_post.category_id' => $categoryId])
+            ->orderRaw('rand()')
+            ->limit(5)
+            ->select();
+        if ($articles->isEmpty()){
+            $this->error('没有相关文章!');
+        }
+        $this->success('success', ['list' => $articles]);
+    }
+}

+ 82 - 0
api/portal/controller/CategoriesController.php

@@ -0,0 +1,82 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+
+namespace api\portal\controller;
+
+use api\portal\service\PortalCategoryService;
+use cmf\controller\RestBaseController;
+use api\portal\model\PortalCategoryModel;
+
+class CategoriesController extends RestBaseController
+{
+    /**
+     * 获取分类列表
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function index()
+    {
+
+        $params          = $this->request->get();
+        $categoryService = new PortalCategoryService();
+        $data            = $categoryService->categories($params);
+        if (empty($this->apiVersion) || $this->apiVersion == '1.0.0') {
+            $response = $data;
+        } else {
+            $response = ['list' => $data];
+        }
+
+        $this->success('请求成功!', $response);
+    }
+
+    /**
+     * 显示指定的分类
+     * @param $id
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function read($id)
+    {
+        $categoryModel = new PortalCategoryModel();
+        $data          = $categoryModel
+            ->where('delete_time', 0)
+            ->where('status', 1)
+            ->where('id', $id)
+            ->find();
+        if ($data) {
+            $this->success('请求成功!', $data);
+        } else {
+            $this->error('该分类不存在!');
+        }
+
+    }
+
+    /**
+     * 获取指定分类的子分类列表
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function subCategories()
+    {
+        $id = $this->request->get('category_id', 0, 'intval');
+
+        $categoryModel = new PortalCategoryModel();
+        $categories    = $categoryModel->where(['parent_id' => $id])->select();
+        if (!$categories->isEmpty()) {
+            $this->success('请求成功', ['categories' => $categories]);
+        } else {
+            $this->error('该分类下没有子分类!');
+        }
+
+
+    }
+}

+ 76 - 0
api/portal/controller/IndexController.php

@@ -0,0 +1,76 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+namespace api\portal\controller;
+
+use api\portal\model\PortalPostModel;
+use cmf\controller\RestBaseController;
+use api\portal\model\PortalTagModel;
+
+class IndexController extends RestBaseController
+{
+    protected $tagModel;
+
+    /**
+     * 获取标签列表
+     */
+    public function index()
+    {
+        $this->success('请求成功!', "DD");
+    }
+
+    /**
+     * 获取热门标签列表
+     */
+    public function hotTags()
+    {
+        $params                         = $this->request->get();
+        $params['where']['recommended'] = 1;
+        $data                           = $this->tagModel->getDatas($params);
+
+        if (empty($this->apiVersion) || $this->apiVersion == '1.0.0') {
+            $response = $data;
+        } else {
+            $response = ['list' => $data,];
+        }
+        $this->success('请求成功!', $response);
+    }
+
+    /**
+     * 获取标签文章列表
+     * @param int $id
+     */
+    public function articles($id)
+    {
+        if (intval($id) === 0) {
+            $this->error('无效的标签id!');
+        } else {
+            $params    = $this->request->param();
+            $postModel = new PortalPostModel();
+
+            unset($params['id']);
+
+            $articles = $postModel->paramsFilter($params)->alias('post')
+                ->join('__PORTAL_TAG_POST__ tag_post', 'post.id = tag_post.post_id')
+                ->where(['tag_post.tag_id' => $id])->select();
+
+            if (!empty($params['relation'])) {
+                $allowedRelations = $postModel->allowedRelations($params['relation']);
+                if (!empty($allowedRelations)) {
+                    if (count($articles) > 0) {
+                        $articles->load($allowedRelations);
+                        $articles->append($allowedRelations);
+                    }
+                }
+            }
+
+
+            $this->success('请求成功!', ['articles' => $articles]);
+        }
+    }
+}

+ 79 - 0
api/portal/controller/ListsController.php

@@ -0,0 +1,79 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: wuwu <15093565100@163.com>
+// +----------------------------------------------------------------------
+namespace api\portal\controller;
+
+use api\portal\model\PortalCategoryModel;
+use api\portal\service\PortalPostService;
+use cmf\controller\RestBaseController;
+
+class ListsController extends RestBaseController
+{
+
+    /**
+     * 推荐文章列表
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function recommended()
+    {
+        $param                = $this->request->param();
+        $param['recommended'] = true;
+
+        $portalPostService = new PortalPostService();
+        $articles          = $portalPostService->postArticles($param);
+        //是否需要关联模型
+        if (!$articles->isEmpty()) {
+            if (!empty($param['relation'])) {
+                $allowedRelations = allowed_relations(['user', 'categories'], $param['relation']);
+                if (!empty($allowedRelations)) {
+                    $articles->load($allowedRelations);
+                    $articles->append($allowedRelations);
+                }
+            }
+        }
+        $this->success('ok', ['list' => $articles]);
+    }
+
+    /**
+     * 分类文章列表
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function getCategoryPostLists()
+    {
+        $categoryId = $this->request->param('category_id', 0, 'intval');
+
+        $portalCategoryModel = new  PortalCategoryModel();
+        $findCategory        = $portalCategoryModel->where('id', $categoryId)->find();
+
+        //分类是否存在
+        if (empty($findCategory)) {
+            $this->error('分类不存在!');
+        }
+
+        $param = $this->request->param();
+
+        $portalPostService = new PortalPostService();
+        $articles          = $portalPostService->postArticles($param);
+        //是否需要关联模型
+        if (!$articles->isEmpty()) {
+            if (!empty($param['relation'])) {
+                $allowedRelations = allowed_relations(['user', 'categories'], $param['relation']);
+                if (!empty($allowedRelations)) {
+                    $articles->load($allowedRelations);
+                    $articles->append($allowedRelations);
+                }
+            }
+        }
+        $this->success('ok', ['list' => $articles]);
+    }
+
+}

+ 65 - 0
api/portal/controller/PagesController.php

@@ -0,0 +1,65 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+
+namespace api\portal\controller;
+
+use api\portal\service\PortalPostService;
+use cmf\controller\RestBaseController;
+use api\portal\model\PortalPostModel;
+
+class PagesController extends RestBaseController
+{
+    /**
+     * 页面列表
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function index()
+    {
+        $params      = $this->request->get();
+        $postService = new PortalPostService();
+        $data        = $postService->postArticles($params, true);
+
+        if (empty($this->apiVersion) || $this->apiVersion == '1.0.0') {
+            $response = $data;
+        } else {
+            $response = ['list' => $data,];
+        }
+        $this->success('请求成功!', $response);
+    }
+
+    /**
+     * 获取页面
+     * @param $id
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function read($id)
+    {
+
+        $params    = $this->request->get();
+        $field     = empty($params['field']) ? '*' : $params['field'];
+        $postModel = new PortalPostModel();
+        $data      = $postModel
+            ->field($field)
+            ->where('id', $id)
+            ->where('delete_time', 0)
+            ->where('post_status', 1)
+            ->where('post_type', 2)
+            ->find();
+        if ($data){
+            $this->success('请求成功!', $data);
+        }else{
+            $this->error('文章不存在!');
+        }
+
+    }
+}

+ 86 - 0
api/portal/controller/TagsController.php

@@ -0,0 +1,86 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+namespace api\portal\controller;
+
+use api\portal\model\PortalTagModel;
+use api\portal\service\PortalTagService;
+use cmf\controller\RestBaseController;
+use think\db\Query;
+
+class TagsController extends RestBaseController
+{
+
+    /**
+     * 获取标签列表
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function index()
+    {
+        $params     = $this->request->get();
+        $tagService = new PortalTagService();
+        $data       = $tagService->tagList($params);
+
+        if (empty($this->apiVersion) || $this->apiVersion == '1.0.0') {
+            $response = $data;
+        } else {
+            $response = ['list' => $data,];
+        }
+        if ($data->isEmpty()) {
+            $this->error('没有标签!');
+        }
+        $this->success('请求成功!', $response);
+    }
+
+    /**
+     * 获取热门标签列表
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function hotTags()
+    {
+        $params                         = $this->request->get();
+        $params['where']['recommended'] = 1;
+
+        $tagService = new PortalTagService();
+        $data       = $tagService->tagList($params);
+
+        if (empty($this->apiVersion) || $this->apiVersion == '1.0.0') {
+            $response = $data;
+        } else {
+            $response = ['list' => $data,];
+        }
+        $this->success('请求成功!', $response);
+    }
+
+    /**
+     * 获取标签文章列表
+     * @param $id
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function articles($id)
+    {
+        if (intval($id) === 0) {
+            $this->error('无效的标签id!');
+        } else {
+            $filter       = $this->request->param();
+            $filter['id'] = $id;
+            $tagService   = new PortalTagService();
+            $tag          = $tagService->portalTagArticles($filter);
+            if ($tag) {
+                $this->error('没有相关文章');
+            }
+            $this->success('请求成功!', $tag);
+        }
+    }
+}

+ 140 - 0
api/portal/controller/UserArticlesController.php

@@ -0,0 +1,140 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+namespace api\portal\controller;
+
+use cmf\controller\RestUserBaseController;
+use api\portal\logic\PortalPostModel;
+
+class UserArticlesController extends RestUserBaseController
+{
+    /**
+     * 显示资源列表
+     */
+    public function index()
+    {
+        $params    = $this->request->get();
+        $userId    = $this->getUserId();
+        $postModel = new PortalPostModel();
+        $dates     = $postModel->getUserArticles($userId, $params);
+        $this->success('请求成功!', $dates);
+    }
+
+    /**
+     * 保存新建的资源
+     */
+    public function save()
+    {
+        $dates            = $this->request->post();
+        $dates['user_id'] = $this->getUserId();
+        $result           = $this->validate($dates, 'Articles.article');
+        if ($result !== true) {
+            $this->error($result);
+        }
+        if (empty($dates['published_time'])) {
+            $dates['published_time'] = time();
+        }
+        $postModel = new PortalPostModel();
+        $postModel->addArticle($dates);
+        $this->success('添加成功!');
+    }
+
+    /**
+     * 显示指定的资源
+     * @param $id
+     */
+    public function read($id)
+    {
+        if (empty($id)) {
+            $this->error('无效的文章id');
+        }
+        $params       = $this->request->get();
+        $params['id'] = $id;
+        $userId       = $this->getUserId();
+        $postModel    = new PortalPostModel();
+        $dates        = $postModel->getUserArticles($userId, $params);
+        $this->success('请求成功!', $dates);
+    }
+
+    /**
+     * 保存更新的资源
+     * @param $id
+     */
+    public function update($id)
+    {
+        $data   = $this->request->put();
+        $result = $this->validate($data, 'Articles.article');
+        if ($result !== true) {
+            $this->error($result);
+        }
+        if (empty($id)) {
+            $this->error('无效的文章id');
+        }
+        $postModel = new PortalPostModel();
+        $result    = $postModel->editArticle($data, $id, $this->getUserId());
+        if ($result === false) {
+            $this->error('编辑失败!');
+        } else {
+            $this->success('编辑成功!');
+        }
+    }
+
+    /**
+     * 删除指定资源
+     * @param $id
+     */
+    public function delete($id)
+    {
+        if (empty($id)) {
+            $this->error('无效的文章id');
+        }
+        $postModel = new PortalPostModel();
+        $result    = $postModel->deleteArticle($id, $this->getUserId());
+        if ($result == -1) {
+            $this->error('文章已删除');
+        }
+        if ($result) {
+            $this->success('删除成功!');
+        } else {
+            $this->error('删除失败!');
+        }
+    }
+
+    /**
+     * 批量删除文章
+     */
+    public function deletes()
+    {
+        $ids = $this->request->post('ids/a');
+        if (empty($ids)) {
+            $this->error('文章id不能为空');
+        }
+        $postModel = new PortalPostModel();
+        $result    = $postModel->deleteArticle($ids, $this->getUserId());
+        if ($result == -1) {
+            $this->error('文章已删除');
+        }
+        if ($result) {
+            $this->success('删除成功!');
+        } else {
+            $this->error('删除失败!');
+        }
+    }
+
+    /**
+     * 我的文章列表
+     */
+    public function my()
+    {
+        $params    = $this->request->get();
+        $userId    = $this->getUserId();
+        $postModel = new PortalPostModel();
+        $data      = $postModel->getUserArticles($userId, $params);
+        $this->success('请求成功!', $data);
+    }
+}

+ 41 - 0
api/portal/controller/UserController.php

@@ -0,0 +1,41 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: wuwu <15093565100@163.com>
+// +----------------------------------------------------------------------
+namespace api\portal\controller;
+
+use api\portal\service\PortalPostService;
+use cmf\controller\RestBaseController;
+
+class UserController extends RestBaseController
+{
+    /**
+     * 会员文章列表
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function articles()
+    {
+        $userId = $this->request->param('user_id', 0, 'intval');
+
+        if (empty($userId)) {
+            $this->error('用户id不能空!');
+        }
+
+        $param             = $this->request->param();
+        $param['user_id']  = $userId;
+        $portalPostService = new PortalPostService();
+        $articles          = $portalPostService->postArticles($param);
+        if ($articles->isEmpty()) {
+            $this->error('没有数据');
+        } else {
+            $this->success('ok', ['list' => $articles]);
+        }
+    }
+
+}

+ 324 - 0
api/portal/logic/PortalPostModel.php

@@ -0,0 +1,324 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+
+namespace api\portal\logic;
+
+use api\portal\model\PortalPostModel as PortalPost;
+use think\Db;
+class PortalPostModel extends PortalPost
+{
+    /**
+     * 获取相关文章
+     * @param int|string|array $postIds 文章id
+     * @return array
+     */
+    public function getRelationPosts($postIds)
+    {
+        $posts = $this->with('articleUser')
+            ->field('id,post_title,user_id,is_top,post_hits,post_like,comment_count,more')
+            ->whereIn('id', $postIds)
+            ->select();
+        foreach ($posts as $post) {
+            $post->appendRelationAttr('articleUser', 'user_nickname');
+        }
+        return $posts;
+    }
+    /**
+     * 获取用户文章
+     */
+    public function getUserArticles($userId, $params)
+    {
+        $where = [
+            'post_type' => 1,
+            'user_id'   => $userId
+        ];
+        if (!empty($params)) {
+            $this->paramsFilter($params);
+        }
+        return $this->where($where)->select();
+    }
+
+    /**
+     * 会员添加文章
+     * @param array $data 文章数据
+     * @return $this
+     */
+    public function addArticle($data)
+    {
+    	//设置图片附件,写入字段过滤
+    	$dataField  =   $this->setMoreField($data);
+    	$data       =   $dataField[0];
+	    array_push($dataField[1],'user_id');
+	    $this->readonly = array_diff(['user_id'],$this->readonly);
+        $this->allowField($dataField[1])->data($data, true)->isUpdate(false)->save();
+        $categories = $this->strToArr($data['categories']);
+        $this->categories()->attach($categories);
+        if (!empty($data['post_keywords']) && is_string($data['post_keywords'])) {
+            //加入标签
+            $data['post_keywords'] = str_replace(',', ',', $data['post_keywords']);
+            $keywords              = explode(',', $data['post_keywords']);
+            $this->addTags($keywords, $this->id);
+        }
+        return $this;
+    }
+
+    /**
+     * 会员文章编辑
+     * @param array $data 文章数据
+     * @param int $id 文章id
+     * @param int $userId 文章所属用户id [可选]
+     * @return boolean   成功 true 失败 false
+     */
+    public function editArticle($data, $id, $userId = '')
+    {
+        if (!empty($userId)) {
+            $isBelong = $this->isuserPost($id, $userId);
+            if ($isBelong === false) {
+                return $isBelong;
+            }
+        }
+	    //设置图片附件,写入字段过滤
+	    $dataField             = $this->setMoreField($data);
+        $data                  = $dataField[0];
+        $data['id']            = $id;
+        $this->allowField($dataField[1])->data($data, true)->isUpdate(true)->save();
+
+        $categories            = $this->strToArr($data['categories']);
+        $oldCategoryIds        = $this->categories()->column('category_id');
+        $sameCategoryIds       = array_intersect($categories, $oldCategoryIds);
+        $needDeleteCategoryIds = array_diff($oldCategoryIds, $sameCategoryIds);
+        $newCategoryIds        = array_diff($categories, $sameCategoryIds);
+        if (!empty($needDeleteCategoryIds)) {
+            $this->categories()->detach($needDeleteCategoryIds);
+        }
+        if (!empty($newCategoryIds)) {
+            $this->categories()->attach(array_values($newCategoryIds));
+        }
+        if (!isset($data['post_keywords'])) {
+	        $keywords = [];
+        } elseif (is_string($data['post_keywords'])) {
+            //加入标签
+            $data['post_keywords'] = str_replace(',', ',', $data['post_keywords']);
+            $keywords              = explode(',', $data['post_keywords']);
+        }
+        $this->addTags($keywords, $data['id']);
+        return $this;
+    }
+
+    /**
+     * 根据文章关键字,增加标签
+     * @param array $keywords 文章关键字数组
+     * @param int $articleId 文章id
+     * @return void
+     */
+    public function addTags($keywords, $articleId)
+    {
+        foreach ($keywords as $key => $value) {
+            $keywords[$key] = trim($value);
+        }
+        $continue = true;
+        $names    = $this->tags()->column('name');
+        if (!empty($keywords) || !empty($names)) {
+            if (!empty($names)) {
+                $sameNames         = array_intersect($keywords, $names);
+                $keywords          = array_diff($keywords, $sameNames);
+                $shouldDeleteNames = array_diff($names, $sameNames);
+                if (!empty($shouldDeleteNames)) {
+                    $tagIdNames = $this->tags()
+                        ->where('name', 'in', $shouldDeleteNames)
+                        ->column('pivot.id', 'tag_id');
+                    $tagIds     = array_keys($tagIdNames);
+                    $tagPostIds = array_values($tagIdNames);
+                    $tagPosts   = DB::name('portal_tag_post')->where('tag_id', 'in', $tagIds)
+                        ->field('id,tag_id,post_id')
+                        ->select();
+                    $keepTagIds = [];
+                    foreach ($tagPosts as $key => $tagPost) {
+                        if ($articleId != $tagPost['post_id']) {
+                            array_push($keepTagIds, $tagPost['tag_id']);
+                        }
+                    }
+                    $keepTagIds         = array_unique($keepTagIds);
+                    $shouldDeleteTagIds = array_diff($tagIds, $keepTagIds);
+                    DB::name('PortalTag')->delete($shouldDeleteTagIds);
+                    DB::name('PortalTagPost')->delete($tagPostIds);
+                }
+            } else {
+                $tagIdNames = DB::name('portal_tag')->where('name', 'in', $keywords)->column('name', 'id');
+                if (!empty($tagIdNames)) {
+                    $tagIds = array_keys($tagIdNames);
+                    $this->tags()->attach($tagIds);
+                    $keywords = array_diff($keywords, array_values($tagIdNames));
+                    if (empty($keywords)) {
+                        $continue = false;
+                    }
+                }
+            }
+            if ($continue) {
+                foreach ($keywords as $key => $value) {
+                    if (!empty($value)) {
+                        $this->tags()->attach(['name' => $value]);
+                    }
+                }
+            }
+        }
+    }
+
+	/**
+	 * 设置缩略图,图片,附件
+	 * 懒人方法
+	 * @param $data 表单数据
+	 */
+	public function setMoreField($data)
+	{
+		$allowField = [
+			'post_title','post_keywords','post_source',
+			'post_excerpt','post_content','more',
+			'published_time'
+		];
+		if (!empty($data['more'])) {
+			$data['more'] = $this->setMoreUrl($data['more']);
+		}
+		if (!empty($data['thumbnail'])) {
+			$data['more']['thumbnail'] = cmf_asset_relative_url($data['thumbnail']);
+		}
+		return [$data,$allowField];
+	}
+
+    /**
+     * 获取图片附件url相对地址
+     * 默认上传名字 *_names  地址 *_urls
+     * @param $annex 上传附件
+     * @return array
+     */
+    public function setMoreUrl($annex)
+    {
+        $more = [];
+        if (!empty($annex)) {
+            foreach ($annex as $key => $value) {
+                $nameArr = $key . '_names';
+                $urlArr  = $key . '_urls';
+                if (is_string($value[$nameArr]) && is_string($value[$urlArr])) {
+                    $more[$key] = [$value[$nameArr], $value[$urlArr]];
+                } elseif (!empty($value[$nameArr]) && !empty($value[$urlArr])) {
+                    $more[$key] = [];
+                    foreach ($value[$urlArr] as $k => $url) {
+                        $url = cmf_asset_relative_url($url);
+                        array_push($more[$key], ['url' => $url, 'name' => $value[$nameArr][$k]]);
+                    }
+                }
+            }
+        }
+        return $more;
+    }
+
+    /**
+     * 删除文章
+     * @param $ids  int|array   文章id
+     * @param int $userId 文章所属用户id  [可选]
+     * @return bool|int 删除结果  true 成功 false 失败  -1 文章不存在
+     */
+    public function deleteArticle($ids, $userId)
+    {
+        $time   = time();
+        $result = false;
+        $where  = [];
+
+        if (!empty($userId)) {
+            if (is_numeric($ids)) {
+                $article = $this->find($ids);
+                if (!empty($article)) {
+                    if ($this->isUserPost($ids, $userId) || $userId == 1) {
+                        $where['id'] = $ids;
+                    }
+                }
+            } else {
+                $ids      = $this->strToArr($ids);
+                $articles = $this->where('id', 'in', $ids)->select();
+                if (!empty($articles)) {
+                    $deleteIds = $this->isUserPosts($ids, $userId);
+                    if (!empty($deleteIds)) {
+                        $where['id'] = ['in', $deleteIds];
+                    }
+                }
+            }
+        } else {
+            if (is_numeric($ids)) {
+                $article = $this->find($ids);
+                if (!empty($article)) {
+                    $where['id'] = $ids;
+                }
+            } else {
+                $ids      = $this->strToArr($ids);
+                $articles = $this->where('id', 'in', $ids)->select();
+                if (!empty($articles)) {
+                    $where['id'] = ['in', $ids];
+                }
+            }
+        }
+        if (empty($article) && empty($articles)) {
+            return -1;
+        }
+        if (!empty($where)) {
+            $result = $this->useGlobalScope(false)
+                ->where($where)
+                ->setField('delete_time', $time);
+        }
+        if ($result) {
+            $data = [
+                'create_time' => $time,
+                'table_name'  => 'portal_post'
+            ];
+            if (!empty($article)) {
+                $data['name'] = $article['post_title'];
+                $article->recycleBin()->save($data);
+            }
+
+            if (!empty($articles)) {
+                foreach ($articles as $article) {
+                    $data['name'] = $article['post_title'];
+                    $article->recycleBin()->save($data);
+                }
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * 判断文章所属用户是否为当前用户,超级管理员除外
+     * @params  int $id     文章id
+     * @param   int $userId 当前用户id
+     * @return  boolean     是 true , 否 false
+     */
+    public function isUserPost($id, $userId)
+    {
+        $postUserId = $this->useGlobalScope(false)
+            ->getFieldById($id, 'user_id');
+        if ($postUserId != $userId || $userId != 1) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 过滤属于当前用户的文章,超级管理员除外
+     * @params  array $ids     文章id的数组
+     * @param   int $userId 当前用户id
+     * @return  array     属于当前用户的文章id
+     */
+    public function isUserPosts($ids, $userId)
+    {
+        $postIds = $this->useGlobalScope(false)
+            ->where('user_id', $userId)
+            ->where('id', 'in', $ids)
+            ->column('id');
+        return array_intersect($ids, $postIds);
+    }
+}

+ 88 - 0
api/portal/model/PortalCategoryModel.php

@@ -0,0 +1,88 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+
+namespace api\portal\model;
+
+use think\Model;
+
+class PortalCategoryModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'portal_category';
+
+    //类型转换
+    protected $type = [
+        'more' => 'array',
+    ];
+
+
+    //模型关联方法
+    protected $relationFilter = ['articles'];
+
+
+    /**
+     * more 自动转化
+     * @param $value
+     * @return array
+     */
+    public function getMoreAttr($value)
+    {
+        $more = json_decode($value, true);
+        if (!empty($more['thumbnail'])) {
+            $more['thumbnail'] = cmf_get_image_url($more['thumbnail']);
+        }
+
+        if (!empty($more['photos'])) {
+            foreach ($more['photos'] as $key => $value) {
+                $more['photos'][$key]['url'] = cmf_get_image_url($value['url']);
+            }
+        }
+        return $more;
+    }
+
+    /**
+     * 关联文章表
+     * @return \think\model\relation\BelongsToMany
+     */
+    public function articles()
+    {
+        return $this->belongsToMany('PortalPostModel', 'portal_category_post', 'post_id', 'category_id');
+    }
+
+    /**
+     * 关联文章分类和文章表
+     * @return \think\model\relation\HasMany
+     */
+    public function PostIds()
+    {
+        return $this->hasMany('PortalCategoryPostModel', 'category_id', 'id');
+    }
+
+    /**
+     * 此类文章id数组
+     * @param string $category_id 分类di
+     * @return array|string|Model|null
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public static function categoryPostIds($category_id)
+    {
+        $ids      = [];
+        $post_ids = self::relation('PostIds')->field(true)->where('id', $category_id)->find();
+        foreach ($post_ids['PostIds'] as $key => $id) {
+            $ids[] = $id['post_id'];
+        }
+        $post_ids['PostIds'] = $ids;
+        return $post_ids;
+    }
+}

+ 31 - 0
api/portal/model/PortalCategoryPostModel.php

@@ -0,0 +1,31 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: wuwu <15093565100@163.com>
+// +----------------------------------------------------------------------
+namespace api\portal\model;
+
+use think\Model;
+
+class PortalCategoryPostModel extends Model
+{
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'portal_category_post';
+
+    /**
+     * 基础查询
+     */
+    protected function base($query)
+    {
+        $query->where('status', 1);
+    }
+}

+ 497 - 0
api/portal/model/PortalPostModel.php

@@ -0,0 +1,497 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+namespace api\portal\model;
+
+use think\Db;
+use think\db\Query;
+use think\Model;
+
+/**
+ * @method getFieldById($id, $string)
+ * @property mixed id
+ */
+class PortalPostModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'portal_post';
+
+    //设置只读字段
+    protected $readonly = ['user_id'];
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = true;
+
+    //类型转换
+    protected $type = [
+        'more' => 'array',
+    ];
+
+    /**
+     *  关联 user表
+     * @return \think\model\relation\BelongsTo
+     */
+    public function user()
+    {
+        return $this->belongsTo('api\portal\model\UserModel', 'user_id');
+    }
+
+    /**
+     * 关联 user表
+     * @return \think\model\relation\BelongsTo
+     */
+    public function articleUser()
+    {
+        return $this->belongsTo('api\portal\model\UserModel', 'user_id')->field('id,user_nickname');
+    }
+
+    /**
+     * 关联分类表
+     * @return \think\model\relation\BelongsToMany
+     */
+    public function categories()
+    {
+        return $this->belongsToMany('api\portal\model\PortalCategoryModel', 'portal_category_post', 'category_id', 'post_id');
+    }
+
+    /**
+     * 关联标签表
+     * @return \think\model\relation\BelongsToMany
+     */
+    public function tags()
+    {
+        return $this->belongsToMany('api\portal\model\PortalTagModel', 'portal_tag_post', 'tag_id', 'post_id');
+    }
+
+    /**
+     * 关联 回收站 表
+     * @return \think\model\relation\HasOne
+     */
+    public function recycleBin()
+    {
+        return $this->hasOne('api\portal\model\RecycleBinModel', 'object_id');
+    }
+
+    /**
+     * published_time   自动转化
+     * @param $value
+     * @return string
+     */
+    public function getPublishedTimeAttr($value)
+    {
+        // 兼容老版本 1.0.0的客户端
+        $apiVersion = request()->header('XX-Api-Version');
+        if (empty($apiVersion)) {
+            return date('Y-m-d H:i:s', $value);
+        } else {
+            return $value;
+        }
+    }
+
+    /**
+     * published_time   自动转化
+     * @param $value
+     * @return int
+     */
+    public function setPublishedTimeAttr($value)
+    {
+        if (is_numeric($value)) {
+            return $value;
+        }
+        return strtotime($value);
+    }
+
+    public function getPostTitleAttr($value)
+    {
+        return htmlspecialchars_decode($value);
+    }
+
+    public function getPostExcerptAttr($value)
+    {
+        return htmlspecialchars_decode($value);
+    }
+
+    /**
+     * post_content 自动转化
+     * @param $value
+     * @return string
+     */
+    public function getPostContentAttr($value)
+    {
+        return cmf_replace_content_file_url(htmlspecialchars_decode($value));
+    }
+
+    /**
+     * post_content 自动转化
+     * @param $value
+     * @return string
+     */
+    public function setPostContentAttr($value)
+    {
+        return htmlspecialchars(cmf_replace_content_file_url(htmlspecialchars_decode($value), true));
+    }
+
+    /**
+     * Thumbnail 自动转化
+     * @param $value
+     * @return array
+     */
+    public function getThumbnailAttr($value)
+    {
+        return cmf_get_image_url($value);
+    }
+
+    /**
+     * more 自动转化
+     * @param $value
+     * @return array
+     */
+    public function getMoreAttr($value)
+    {
+        $more = json_decode($value, true);
+        if (!empty($more['thumbnail'])) {
+            $more['thumbnail'] = cmf_get_image_url($more['thumbnail']);
+        }
+
+        if (!empty($more['audio'])) {
+            $more['audio'] = cmf_get_file_download_url($more['audio']);
+        }
+
+        if (!empty($more['video'])) {
+            $more['video'] = cmf_get_file_download_url($more['video']);
+        }
+
+        if (!empty($more['photos'])) {
+            foreach ($more['photos'] as $key => $value) {
+                $more['photos'][$key]['url'] = cmf_get_image_url($value['url']);
+            }
+        }
+
+        if (!empty($more['files'])) {
+            foreach ($more['files'] as $key => $value) {
+                $more['files'][$key]['url'] = cmf_get_file_download_url($value['url']);
+            }
+        }
+        return $more;
+    }
+
+    /**
+     * 文章查询
+     * @param array $filter 数据
+     * @return array|\PDOStatement|string|Model|null
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function articleFind($filter)
+    {
+        $result = $this
+            ->where(function (Query $query) use ($filter) {
+                if (!empty($filter['id'])) {
+                    $query->where('id', $filter['id']);
+                }
+                if (!empty($filter['user_id'])) {
+                    $query->where('user_id', $filter['user_id']);
+                }
+            })
+            ->where('delete_time', 0)
+            ->where('post_status', 1)
+            ->where('post_type', 1)
+            ->find();
+        return $result;
+    }
+
+    /**
+     * 会员添加文章
+     * @param array $data 文章数据
+     * @return $this
+     * @throws \think\Exception
+     */
+    public function addArticle($data)
+    {
+        if (!empty($data['more'])) {
+            $data['more'] = $this->setMoreUrl($data['more']);
+        }
+        if (!empty($data['thumbnail'])) {
+            $data['more']['thumbnail'] = cmf_asset_relative_url($data['thumbnail']);
+        }
+        $this->allowField(true)->data($data, true)->isUpdate(false)->save();
+        $categories = str_to_arr($data['categories']);
+        //TODO 无法录入多个分类
+        $this->categories()->attach($categories);
+        if (!empty($data['post_keywords']) && is_string($data['post_keywords'])) {
+            //加入标签
+            $data['post_keywords'] = str_replace(',', ',', $data['post_keywords']);
+            $keywords              = explode(',', $data['post_keywords']);
+            $this->addTags($keywords, $this->id);
+        }
+        return $this;
+    }
+
+    /**
+     * 会员文章编辑
+     * @param array  $data   文章数据
+     * @param int    $id     文章id
+     * @param string $userId 文章所属用户id [可选]
+     * @return PortalPostModel|bool
+     * @throws \think\Exception
+     */
+    public function editArticle($data, $id, $userId = '')
+    {
+
+        if (!empty($userId)) {
+            //判断是否属于当前用户的文章
+            $isBelong = $this->isuserPost($id, $userId);
+            if ($isBelong === false) {
+                return $isBelong;
+            }
+        }
+
+        if (!empty($data['more'])) {
+            $data['more'] = $this->setMoreUrl($data['more']);
+        }
+        if (!empty($data['thumbnail'])) {
+            $data['more']['thumbnail'] = cmf_asset_relative_url($data['thumbnail']);
+        }
+        $data['id'] = $id;
+//        $data['post_status'] = empty($data['post_status']) ? 0 : 1;
+//        $data['is_top']      = empty($data['is_top']) ? 0 : 1;
+//        $data['recommended'] = empty($data['recommended']) ? 0 : 1;
+        $this->allowField(true)->data($data, true)->isUpdate(true)->save();
+
+        $categories     = str_to_arr($data['categories']);
+        $oldCategoryIds = $this->categories()->column('category_id');
+
+        $sameCategoryIds       = array_intersect($categories, $oldCategoryIds);
+        $needDeleteCategoryIds = array_diff($oldCategoryIds, $sameCategoryIds);
+        $newCategoryIds        = array_diff($categories, $sameCategoryIds);
+        if (!empty($needDeleteCategoryIds)) {
+            $this->categories()->detach($needDeleteCategoryIds);
+        }
+
+        if (!empty($newCategoryIds)) {
+            $this->categories()->attach(array_values($newCategoryIds));
+        }
+
+        $keywords = [];
+
+        if (!empty($data['post_keywords'])) {
+            if (is_string($data['post_keywords'])) {
+                //加入标签
+                $data['post_keywords'] = str_replace(',', ',', $data['post_keywords']);
+                $keywords              = explode(',', $data['post_keywords']);
+            }
+        }
+
+        $this->addTags($keywords, $data['id']);
+
+        return $this;
+    }
+
+    /**
+     * 根据文章关键字,增加标签
+     * @param array $keywords  文章关键字数组
+     * @param int   $articleId 文章id
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     * @throws \think\exception\PDOException
+     */
+    public function addTags($keywords, $articleId)
+    {
+        foreach ($keywords as $key => $value) {
+            $keywords[$key] = trim($value);
+        }
+        $continue = true;
+        $names    = $this->tags()->column('name');
+        if (!empty($keywords) || !empty($names)) {
+            if (!empty($names)) {
+                $sameNames         = array_intersect($keywords, $names);
+                $keywords          = array_diff($keywords, $sameNames);
+                $shouldDeleteNames = array_diff($names, $sameNames);
+                if (!empty($shouldDeleteNames)) {
+                    $tagIdNames = $this->tags()
+                        ->where('name', 'in', $shouldDeleteNames)
+                        ->column('pivot.id', 'tag_id');
+                    $tagIds     = array_keys($tagIdNames);
+                    $tagPostIds = array_values($tagIdNames);
+                    $tagPosts   = DB::name('portal_tag_post')
+                        ->where('tag_id', 'in', $tagIds)
+                        ->field('id,tag_id,post_id')
+                        ->select();
+                    $keepTagIds = [];
+                    foreach ($tagPosts as $key => $tagPost) {
+                        if ($articleId != $tagPost['post_id']) {
+                            array_push($keepTagIds, $tagPost['tag_id']);
+                        }
+                    }
+                    $keepTagIds         = array_unique($keepTagIds);
+                    $shouldDeleteTagIds = array_diff($tagIds, $keepTagIds);
+                    Db::name('PortalTag')->delete($shouldDeleteTagIds);
+                    Db::name('PortalTagPost')->delete($tagPostIds);
+                }
+            } else {
+                $tagIdNames = DB::name('portal_tag')->where('name', 'in', $keywords)->column('name', 'id');
+                if (!empty($tagIdNames)) {
+                    $tagIds = array_keys($tagIdNames);
+                    $this->tags()->attach($tagIds);
+                    $keywords = array_diff($keywords, array_values($tagIdNames));
+                    if (empty($keywords)) {
+                        $continue = false;
+                    }
+                }
+            }
+            if ($continue) {
+                foreach ($keywords as $key => $value) {
+                    if (!empty($value)) {
+                        $this->tags()->attach(['name' => $value]);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 获取图片附件url相对地址
+     * 默认上传名字 *_names  地址 *_urls
+     * @param array $annex 上传附件
+     * @return array
+     */
+    public function setMoreUrl($annex)
+    {
+        $more = [];
+        if (!empty($annex)) {
+            foreach ($annex as $key => $value) {
+                $nameArr = $key . '_names';
+                $urlArr  = $key . '_urls';
+                if (is_string($value[$nameArr]) && is_string($value[$urlArr])) {
+                    $more[$key] = [$value[$nameArr], $value[$urlArr]];
+                } elseif (!empty($value[$nameArr]) && !empty($value[$urlArr])) {
+                    $more[$key] = [];
+                    foreach ($value[$urlArr] as $k => $url) {
+                        $url = cmf_asset_relative_url($url);
+                        array_push($more[$key], ['url' => $url, 'name' => $value[$nameArr][$k]]);
+                    }
+                }
+            }
+        }
+        return $more;
+    }
+
+    /**
+     * 删除文章
+     * @param  int|array $ids    文章id
+     * @param  string    $userId 文章所属用户id  [可选]
+     * @return bool|int 删除结果  true 成功 false 失败  -1 文章不存在
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function deleteArticle($ids, $userId = '')
+    {
+        $time   = time();
+        $result = false;
+        $where  = [];
+
+        if (!empty($userId)) {
+            if (is_numeric($ids)) {
+                $article = $this->find($ids);
+                if (!empty($article)) {
+                    if ($this->isUserPost($ids, $userId) || $userId == 1) {
+                        $where['id'] = $ids;
+                    }
+                }
+            } else {
+                $ids      = str_to_arr($ids);
+                $articles = $this->where('id', 'in', $ids)->select();
+                if (!empty($articles)) {
+                    $deleteIds = $this->isUserPosts($ids, $userId);
+                    if (!empty($deleteIds)) {
+                        $where['id'] = ['in', $deleteIds];
+                    }
+                }
+            }
+        } else {
+            if (is_numeric($ids)) {
+                $article = $this->find($ids);
+                if (!empty($article)) {
+                    $where['id'] = $ids;
+                }
+            } else {
+                $ids      = str_to_arr($ids);
+                $articles = $this->where('id', 'in', $ids)->select();
+                if (!empty($articles)) {
+                    $where['id'] = ['in', $ids];
+                }
+            }
+        }
+        if (empty($article) && empty($articles)) {
+            return -1;
+        }
+        if (!empty($where)) {
+            $result = $this->useGlobalScope(false)
+                ->where($where)
+                ->setField('delete_time', $time);
+        }
+        if ($result) {
+            $data = [
+                'create_time' => $time,
+                'table_name'  => 'portal_post'
+            ];
+            if (!empty($article)) {
+                $data['name'] = $article['post_title'];
+                $article->recycleBin()->save($data);
+            }
+
+            if (!empty($articles)) {
+                foreach ($articles as $article) {
+                    $data['name'] = $article['post_title'];
+                    $article->recycleBin()->save($data);
+                }
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * 判断文章所属用户是否为当前用户,超级管理员除外
+     * @param   int $id     文章id
+     * @param   int $userId 当前用户id
+     * @return  boolean     是 true , 否 false
+     */
+    public function isUserPost($id, $userId)
+    {
+        $postUserId = $this->getFieldById($id, 'user_id');
+        if ($postUserId == $userId || $userId == 1) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 过滤属于当前用户的文章,超级管理员除外
+     * @param   array $ids    文章id的数组
+     * @param   int   $userId 当前用户id
+     * @return  array     属于当前用户的文章id
+     */
+    public function isUserPosts($ids, $userId)
+    {
+        $postIds = $this
+            ->useGlobalScope(false)
+            ->where('user_id', $userId)
+            ->where('id', 'in', $ids)
+            ->column('id');
+        return array_intersect($ids, $postIds);
+    }
+
+
+}

+ 30 - 0
api/portal/model/PortalTagModel.php

@@ -0,0 +1,30 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+
+namespace api\portal\model;
+
+use think\Model;
+
+class PortalTagModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'portal_tag';
+
+    /**
+     * 关联 文章表
+     * @return \think\model\relation\BelongsToMany
+     */
+    public function articles()
+    {
+        return $this->belongsToMany('PortalPostModel','portal_tag_post','post_id','tag_id')->alias('post');
+    }
+}

+ 35 - 0
api/portal/model/PortalTagPostModel.php

@@ -0,0 +1,35 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+
+namespace api\portal\model;
+
+use think\Model;
+
+class PortalTagPostModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'portal_tag_post';
+
+    /**
+     * 获取指定id相关的文章id数组
+     * @param $post_id  文章id
+     * @return array    相关的文章id
+     */
+    function getRelationPostIds($post_id)
+    {
+        $tagIds  = $this->where('post_id', $post_id)
+            ->column('tag_id');
+        $postIds = $this->whereIn('tag_id', $tagIds)
+            ->column('post_id');
+        return array_unique($postIds);
+    }
+}

+ 21 - 0
api/portal/model/RecycleBinModel.php

@@ -0,0 +1,21 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+namespace api\portal\model;
+
+use think\Model;
+
+class RecycleBinModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'recycle_bin';
+
+}

+ 46 - 0
api/portal/model/UserModel.php

@@ -0,0 +1,46 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+
+namespace api\portal\model;
+
+
+use think\Model;
+
+class UserModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'user';
+
+    //模型关联方法
+    protected $relationFilter = ['user'];
+
+
+    /**
+     * more 自动转化
+     * @param $value
+     * @return string
+     */
+    public function getAvatarAttr($value)
+    {
+        $value = !empty($value) ? cmf_get_image_url($value) : $value;
+        return $value;
+    }
+
+    /**
+     * 关联 user表
+     * @return string 关联数据
+     */
+    public function user()
+    {
+        return $this->belongsTo('UserModel', 'user_id')->setEagerlyType(1);
+    }
+}

+ 22 - 0
api/portal/route.php

@@ -0,0 +1,22 @@
+<?php
+
+use think\facade\Route;
+
+Route::resource('portal/categories', 'portal/Categories');
+Route::get('portal/categories/subCategories', 'portal/Categories/subCategories');
+Route::resource('portal/articles', 'portal/Articles');
+Route::resource('portal/pages', 'portal/Pages');
+Route::resource('portal/userArticles', 'portal/UserArticles');
+
+Route::get('portal/search', 'portal/Articles/search');
+Route::get('portal/articles/my', 'portal/Articles/my');
+Route::get('portal/articles/relatedArticles', 'portal/Articles/relatedArticles');
+Route::post('portal/articles/doLike', 'portal/Articles/doLike');
+Route::post('portal/articles/cancelLike', 'portal/Articles/cancelLike');
+Route::post('portal/articles/doFavorite', 'portal/Articles/doFavorite');
+Route::post('portal/articles/cancelFavorite', 'portal/Articles/cancelFavorite');
+Route::get('portal/tags/:id/articles', 'portal/Tags/articles');
+Route::get('portal/tags', 'portal/Tags/index');
+Route::get('portal/tags/hotTags', 'portal/Tags/hotTags');
+
+Route::post('portal/userArticles/deletes', 'portal/UserArticles/deletes');

+ 59 - 0
api/portal/service/PortalCategoryService.php

@@ -0,0 +1,59 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2018 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// | Date: 2019/01/08
+// | Time:上午 10:32
+// +----------------------------------------------------------------------
+
+
+namespace api\portal\service;
+
+
+use api\portal\model\PortalCategoryModel;
+use think\db\Query;
+
+class PortalCategoryService
+{
+    /**
+     * @param $filter
+     * @return array|\PDOStatement|string|\think\Collection
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function categories($filter)
+    {
+        $categoryModel = new PortalCategoryModel();
+        //条件分解
+        $field = empty($filter['field']) ? '*' : $filter['field'];
+        $order = empty($filter['order']) ? ['-id'] : explode(',', $filter['order']);
+        $page  = empty($filter['page']) ? '' : $filter['page'];
+        $limit = empty($filter['limit']) ? '' : $filter['limit'];
+        if (!empty($page)) {
+            $categoryModel = $categoryModel->page($page);
+        } elseif (!empty($limit)) {
+            $categoryModel = $categoryModel->limit($limit);
+        } else {
+            $categoryModel = $categoryModel->limit(10);
+        }
+        //转化-+为desc、asc
+        $orderArr = order_shift($order);
+
+        $result = $categoryModel
+            ->field($field)
+            ->where('delete_time', 0)
+            ->where('status', 1)
+            ->where(function (Query $query) use ($filter) {
+                if (!empty($filter['ids'])) {
+                    $query->where('id', 'in', $filter['ids']);
+                }
+            })
+            ->order($orderArr)
+            ->select();
+        return $result;
+    }
+}

+ 108 - 0
api/portal/service/PortalPostService.php

@@ -0,0 +1,108 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: pl125 <xskjs888@163.com>
+// +----------------------------------------------------------------------
+namespace api\portal\service;
+
+use api\portal\model\PortalPostModel;
+use think\db\Query;
+
+class PortalPostService
+{
+    //模型关联方法
+    protected $relationFilter = ['user'];
+
+    /**
+     * 文章列表
+     * @param      $filter
+     * @param bool $isPage
+     * @return array|string|\think\Collection
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function postArticles($filter, $isPage = false)
+    {
+        $join = [];
+
+        $field = empty($filter['field']) ? 'a.*' : explode(',', $filter['field']);
+        //转为查询条件
+        if (is_array($field)) {
+            foreach ($field as $key => $value) {
+                $field[$key] = 'a.' . $value;
+            }
+            $field = implode(',', $field);
+        }
+        $page     = empty($filter['page']) ? '' : $filter['page'];
+        $limit    = empty($filter['limit']) ? '' : $filter['limit'];
+        $order    = empty($filter['order']) ? ['-update_time'] : explode(',', $filter['order']);
+        $category = empty($filter['category_id']) ? 0 : intval($filter['category_id']);
+        if (!empty($category)) {
+            array_push($join, ['__PORTAL_CATEGORY_POST__ b', 'a.id = b.post_id']);
+            $field = $field.',b.id AS post_category_id,b.list_order,b.category_id';
+        }
+
+        $orderArr = order_shift($order);
+
+        $portalPostModel = new PortalPostModel();
+
+
+        if (!empty($page)) {
+            $portalPostModel = $portalPostModel->page($page);
+        } elseif (!empty($limit)) {
+            $portalPostModel = $portalPostModel->limit($limit);
+        } else {
+            $portalPostModel = $portalPostModel->limit(10);
+        }
+
+        $articles = $portalPostModel
+            ->alias('a')
+            ->field($field)
+            ->join($join)
+            ->where('a.create_time', '>=', 0)
+            ->where('a.delete_time', 0)
+            ->where('a.post_status', 1)
+            ->where(function (Query $query) use ($filter, $isPage) {
+                if (!empty($filter['user_id'])) {
+                    $query->where('a.user_id', $filter['user_id']);
+                }
+                $category = empty($filter['category_id']) ? 0 : intval($filter['category_id']);
+                if (!empty($category)) {
+                    $query->where('b.category_id', $category);
+                }
+                $startTime = empty($filter['start_time']) ? 0 : strtotime($filter['start_time']);
+                $endTime   = empty($filter['end_time']) ? 0 : strtotime($filter['end_time']);
+                if (!empty($startTime)) {
+                    $query->where('a.published_time', '>=', $startTime);
+                }
+                if (!empty($endTime)) {
+                    $query->where('a.published_time', '<=', $endTime);
+                }
+                $keyword = empty($filter['keyword']) ? '' : $filter['keyword'];
+                if (!empty($keyword)) {
+                    $query->where('a.post_title', 'like', "%$keyword%");
+                }
+                if ($isPage) {
+                    $query->where('a.post_type', 2);
+                } else {
+                    $query->where('a.post_type', 1);
+                }
+                if (!empty($filter['recommended'])) {
+                    $query->where('a.recommended', 1);
+                }
+                if (!empty($filter['ids'])) {
+                    $ids = str_to_arr($filter['ids']);
+                    $query->where('a.id', 'in', $ids);
+                }
+            })
+            ->order($orderArr)
+            ->select();
+
+        return $articles;
+    }
+
+}

+ 100 - 0
api/portal/service/PortalTagService.php

@@ -0,0 +1,100 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2018 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// | Date: 2019/01/09
+// | Time:下午 06:10
+// +----------------------------------------------------------------------
+
+
+namespace api\portal\service;
+
+
+use api\portal\model\PortalPostModel;
+use api\portal\model\PortalTagModel;
+use think\db\Query;
+
+class PortalTagService
+{
+    /**
+     * 获取标签列表
+     * @param array $filter 参数
+     * @return array|\PDOStatement|string|\think\Collection
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function tagList($filter)
+    {
+        $field    = empty($filter['field']) ? '*' : $filter['field'];
+        $page     = empty($filter['page']) ? '' : $filter['page'];
+        $limit    = empty($filter['limit']) ? '' : $filter['limit'];
+        $order    = empty($filter['order']) ? ['-id'] : explode(',', $filter['order']);
+        $orderArr = order_shift($order);
+        $tagModel = new PortalTagModel();
+        if (!empty($page)) {
+            $tagModel = $tagModel->page($page);
+        } elseif (!empty($limit)) {
+            $tagModel = $tagModel->limit($limit);
+        } else {
+            $tagModel = $tagModel->limit(10);
+        }
+
+        $result = $tagModel
+            ->field($field)
+            ->where('status', 1)
+            ->where(function (Query $query) use ($filter) {
+                if (!empty($filter['id'])) {
+                    $query->where('id', $filter['id']);
+                }
+            })
+            ->order($orderArr)
+            ->select();
+        return $result;
+    }
+
+    /**
+     * @param $filter
+     * @return array|\PDOStatement|string|\think\Model|null
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function portalTagArticles($filter)
+    {
+        $tagModel = new PortalTagModel();
+        $tag      = $tagModel
+            ->with([
+                'articles' => function (Query $query) use ($filter) {
+                    $field = empty($filter['field']) ? 'post.*' : explode(',', $filter['field']);
+                    //转为查询条件
+                    if (is_array($field)) {
+                        foreach ($field as $key => $value) {
+                            $field[$key] = 'post.' . $value;
+                        }
+                        $field = implode(',', $field);
+                    }
+                    $page     = empty($filter['page']) ? '' : $filter['page'];
+                    $limit    = empty($filter['limit']) ? '10' : $filter['limit'];
+                    $order    = empty($filter['order']) ? ['-post.id'] : explode(',', $filter['order']);
+                    $orderArr = order_shift($order);
+                    $query->field($field);
+                    if (!empty($page)) {
+                        $query->page($page);
+                    } elseif (!empty($limit)) {
+                        $query->limit($limit);
+                    } else {
+                        $query->limit(10);
+                    }
+                    $query->order($orderArr);
+                    $query->hidden(['pivot']);
+                }
+            ])
+            ->where('id', $filter['id'])
+            ->find();
+        return $tag;
+    }
+}

+ 34 - 0
api/portal/validate/ArticlesValidate.php

@@ -0,0 +1,34 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2017 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// | Date: 2019/01/11
+// | Time:下午 03:24
+// +----------------------------------------------------------------------
+namespace api\portal\validate;
+
+use think\Validate;
+
+class ArticlesValidate extends Validate
+{
+    protected $rule = [
+        'post_title'        =>  'require',
+	    'post_content'      =>  'require',
+	    'categories'        =>  'require'
+    ];
+    protected $message = [
+        'post_title.require'    =>  '文章标题不能为空',
+	    'post_content.require'  =>  '内容不能为空',
+	    'categories.require'    =>  '文章分类不能为空'
+    ];
+
+    protected $scene = [
+        'article'  => [ 'post_title' , 'post_content' , 'categories' ],
+        'page' => ['post_title']
+    ];
+}

+ 8 - 0
app/demo/README.md

@@ -0,0 +1,8 @@
+ThinkCMF 演示应用
+===============
+
+### 加载第三方库
+支持使用`composer`加载第三方库
+```
+composer require phpoffice/phpspreadsheet
+```

+ 33 - 0
app/demo/api/PageApi.php

@@ -0,0 +1,33 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\demo\api;
+
+class PageApi
+{
+    /**
+     * 页面列表 用于导航选择
+     * @return array
+     */
+    public function nav()
+    {
+        return [
+            'rule'  => [
+                'action' => 'demo/Index/index',
+                'param'  => [
+                ]
+            ],//url规则
+            'items' => [
+                ['id' => 1, 'name' => 'test']
+            ] //每个子项item里必须包括id,name,如果想表示层级关系请加上 parent_id
+        ];
+    }
+
+}

+ 5 - 0
app/demo/command.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    // 指令名 =》完整的类名
+    'demo:hello' => 'app\demo\command\Hello'
+];

+ 31 - 0
app/demo/command/Hello.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace app\demo\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+
+class Hello extends Command
+{
+    protected function configure()
+    {
+        $this->setName('demo:hello')
+            ->addArgument('name', Argument::OPTIONAL, "your name")
+            ->addOption('city', '-c', Option::VALUE_REQUIRED, 'city name')
+            ->setDescription('Say App Hello');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $name = trim($input->getArgument('name'));
+        $city = $input->getOption('city');
+        $city = $city ? $city : 'China';
+        $name = $name ? $name : 'ThinkCMF';
+        $output->writeln("Hello, My name is " . $name . '! I\'m from ' . $city);
+    }
+
+
+}

+ 13 - 0
app/demo/composer.json

@@ -0,0 +1,13 @@
+{
+    "name": "thinkcmf/apps-demo",
+    "description": "ThinkCMF demo app",
+    "type": "cmf-app",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "catman",
+            "email": "catman@thinkcmf.com"
+        }
+    ],
+    "require": {}
+}

+ 27 - 0
app/demo/controller/AdminIndexController.php

@@ -0,0 +1,27 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-present http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Released under the MIT License.
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+
+namespace app\demo\controller;
+
+use cmf\controller\AdminBaseController;
+
+class AdminIndexController extends AdminBaseController
+{
+    public function index()
+    {
+        return $this->fetch();
+    }
+
+    public function ws()
+    {
+        return $this->fetch(':ws');
+    }
+}

+ 32 - 0
app/demo/controller/IndexController.php

@@ -0,0 +1,32 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-present http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Released under the MIT License.
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+
+namespace app\demo\controller;
+
+use cmf\controller\HomeBaseController;
+
+class IndexController extends HomeBaseController
+{
+    public function index()
+    {
+        return $this->fetch(':index');
+    }
+
+    public function block()
+    {
+        return $this->fetch();
+    }
+
+    public function ws()
+    {
+        return $this->fetch(':ws');
+    }
+}

+ 19 - 0
app/demo/hooks.php

@@ -0,0 +1,19 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-present http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+return [
+    'demo_hook_test' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '演示钩子', // 钩子名称
+        "description" => "演示钩子", //钩子描述
+        "once"        => 0 // 是否只执行一次
+    ],
+
+];

+ 66 - 0
app/demo/lang/zh-cn.php

@@ -0,0 +1,66 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +---------------------------------------------------------------------
+// | Author: Dean <zxxjjforever@163.com>
+// +----------------------------------------------------------------------
+return [
+    "SAVE"                     => '保存',
+    "CLOSE"                    => '关闭',
+    "OPEN"                     => '开启',
+    "ENABLED"                  => '开启',
+    "DISABLED"                 => '禁用',
+    "DELETE"                   => "删除",
+    "RESTORE"                  => '恢复',
+    "DOWNLOAD"                 => '下载',
+    "YES"                      => '是',
+    "NO"                       => '否',
+    "EDIT"                     => '编辑',
+    "ADD"                      => '添加',
+    "BACK"                     => '返回',
+    'LOGIN'                    => '登录',
+    'PASSWORD'                 => "密码",
+    'USERNAME_OR_EMAIL'        => '用户名或邮箱',
+    'LOGIN_SUCCESS'            => "登录成功!",
+    'PASSWORD_NOT_RIGHT'       => "密码错误!",
+    'PASSWORD_REQUIRED'        => "密码不能为空!",
+    'CAPTCHA_REQUIRED'         => "验证码不能为空!",
+    'CAPTCHA_NOT_RIGHT'        => "验证码错误!",
+    'USERNAME_NOT_EXIST'       => "用户名不存在!",
+    'USERNAME_OR_EMAIL_EMPTY'  => "用户名或邮箱不能为空!",
+    'USE_DISABLED'             => "此用户已被禁用!",
+    'ENTER_VERIFY_CODE'        => '请输入验证码',
+    'HOME'                     => '首页',
+    'LOADING'                  => '正在加载...',
+    'LOGOUT'                   => '退出',
+    "OK"                       => "确定",
+    "STATUS"                   => '状态',
+    "ACTIONS"                  => '操作',
+    "SORT"                     => '排序',
+    "DISPLAY"                  => '显示',
+    "HIDDEN"                   => '隐藏',
+    "HIDE"                     => '隐藏',
+    'DELETE_CONFIRM_MESSAGE'   => '你确定删除吗?',
+    'SETTING'                  => '设置',
+    "ADD_SUCCESS"              => '添加成功!',
+    "ADD_FAILED"               => '添加失败!',
+    "EDIT_SUCCESS"             => '保存成功!',
+    "EDIT_FAILED"              => '保存失败!',
+    "NO_DATA"                  => '没有数据',
+    "MOBILE"                   => '手机',
+    "SENDER_NAME"              => '发件人',
+    "SENDER_EMAIL_ADDRESS"     => '邮箱地址',
+    "SENDER_SMTP_SERVER"       => 'SMTP服务器',
+    "SMTP_MAIL_ADDRESS"        => '发件箱帐号',
+    "SMTP_MAIL_PASSWORD"       => '发件箱密码',
+    "EMAIL_ACTIVATION"         => '邮箱激活',
+    "EMAIL_SUBJECT"            => '邮件标题',
+    "EMAIL_TEMPLATE"           => '邮件模版',
+    "EMAIL_TEMPLATE_HELP_TEXT" => '请用{$link}代替激活链接,{$username}代替用户名'
+
+
+];

+ 13 - 0
app/demo/nav.php

@@ -0,0 +1,13 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-present http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+return [
+    ['name' => '演示所有页面', 'api' => "Page/nav"]
+];

+ 53 - 0
app/demo/url.php

@@ -0,0 +1,53 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-present http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+return [
+    'List/index'    => [
+        'name'   => '演示应用-文章列表',
+        'vars'   => [
+            'id' => [
+                'pattern' => '\d+',
+                'require' => true
+            ]
+        ],
+        'simple' => true
+    ],
+    'Page/index'    => [
+        'name'   => '演示应用-页面页',
+        'vars'   => [
+            'id' => [
+                'pattern' => '\d+',
+                'require' => true
+            ]
+        ],
+        'simple' => true
+    ],
+    'Article/index' => [
+        'name'   => '演示应用-文章页',
+        'vars'   => [
+            'id'  => [
+                'pattern' => '\d+',
+                'require' => true
+            ],
+            'cid' => [
+                'pattern' => '\d+',
+                'require' => false
+            ]
+        ],
+        'simple' => true
+    ],
+    'Search/index'  => [
+        'name'   => '演示应用-搜索页',
+        'vars'   => [
+
+        ],
+        'simple' => false
+    ],
+];

+ 15 - 0
app/demo/user_action.php

@@ -0,0 +1,15 @@
+<?php
+return [
+    'demo_test' => [
+        'name'          => '用户行为演示',//用户操作名称
+        'score'         => 1,//更改积分,可以为负
+        'coin'          => 0,//更改金币,可以为负
+        'cycle_time'    => 1,//周期时间值
+        'cycle_type'    => 1,//周期类型;0:不限;1:按天;2:按小时;3:永久
+        'reward_number' => 1,//奖励次数
+        'url'           => [
+            'action' => 'demo/Test/test',
+            'param'  => ['id' => 1]
+        ],//执行操作的url
+    ]
+];

+ 11 - 0
app/demo/vendor/.gitignore

@@ -0,0 +1,11 @@
+.buildpath
+.DS_Store
+.project
+.settings
+.idea
+.git
+/build
+/public/assets/dist
+/node_modules
+Vagrantfile
+.vagrant

+ 67 - 0
app/portal/api/CategoryApi.php

@@ -0,0 +1,67 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\api;
+
+use app\portal\model\PortalCategoryModel;
+use think\db\Query;
+
+class CategoryApi
+{
+    /**
+     * 分类列表 用于模板设计
+     * @param array $param
+     * @return false|\PDOStatement|string|\think\Collection
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function index($param = [])
+    {
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $where = ['delete_time' => 0];
+
+        //返回的数据必须是数据集或数组,item里必须包括id,name,如果想表示层级关系请加上 parent_id
+        return $portalCategoryModel->where($where)
+            ->where(function (Query $query) use ($param) {
+                if (!empty($param['keyword'])) {
+                    $query->where('name', 'like', "%{$param['keyword']}%");
+                }
+            })->select();
+    }
+
+    /**
+     * 分类列表 用于导航选择
+     * @return array
+     */
+    public function nav()
+    {
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $where = ['delete_time' => 0];
+
+        $categories = $portalCategoryModel->where($where)->select();
+
+        $return = [
+            //'name'  => '文章分类',
+            'rule'  => [
+                'action' => 'portal/List/index',
+                'param'  => [
+                    'id' => 'id'
+                ]
+            ],//url规则
+            'items' => $categories //每个子项item里必须包括id,name,如果想表示层级关系请加上 parent_id
+        ];
+
+        return $return;
+    }
+
+}

+ 78 - 0
app/portal/api/PageApi.php

@@ -0,0 +1,78 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\api;
+
+use app\portal\model\PortalPostModel;
+use think\db\Query;
+
+class PageApi
+{
+    /**
+     * 页面列表 用于模板设计
+     * @param array $param
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public function index($param = [])
+    {
+        $portalPostModel = new PortalPostModel();
+
+        $where = [
+            'post_type'   => 2,
+            'post_status' => 1,
+            'delete_time' => 0
+        ];
+
+        //返回的数据必须是数据集或数组,item里必须包括id,name,如果想表示层级关系请加上 parent_id
+        return $portalPostModel->field('id,post_title AS name')
+            ->where($where)
+            ->where('published_time', '<', time())
+            ->where('published_time', '>', 0)
+            ->where(function (Query $query) use ($param) {
+                if (!empty($param['keyword'])) {
+                    $query->where('post_title', 'like', "%{$param['keyword']}%");
+                }
+            })->select();
+    }
+
+    /**
+     * 页面列表 用于导航选择
+     * @return array
+     */
+    public function nav()
+    {
+        $portalPostModel = new PortalPostModel();
+
+        $where = [
+            'post_type'   => 2,
+            'post_status' => 1,
+            'delete_time' => 0
+        ];
+
+
+        $pages = $portalPostModel->field('id,post_title AS name')
+            ->where('published_time', '<', time())
+            ->where('published_time', '>', 0)
+            ->where($where)->select();
+
+        $return = [
+            'rule'  => [
+                'action' => 'portal/Page/index',
+                'param'  => [
+                    'id' => 'id'
+                ]
+            ],//url规则
+            'items' => $pages //每个子项item里必须包括id,name,如果想表示层级关系请加上 parent_id
+        ];
+
+        return $return;
+    }
+
+}

+ 462 - 0
app/portal/controller/AdminArticleController.php

@@ -0,0 +1,462 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use app\portal\model\PortalCategoryPostModel;
+use app\portal\model\PortalTagPostModel;
+use app\portal\model\RecycleBinModel;
+use cmf\controller\AdminBaseController;
+use app\portal\model\PortalPostModel;
+use app\portal\service\PostService;
+use app\portal\model\PortalCategoryModel;
+use app\admin\model\ThemeModel;
+
+class AdminArticleController extends AdminBaseController
+{
+    /**
+     * 文章列表
+     * @adminMenu(
+     *     'name'   => '文章管理',
+     *     'parent' => 'portal/AdminIndex/default',
+     *     'display'=> true,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章列表',
+     *     'param'  => ''
+     * )
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function index()
+    {
+        $content = hook_one('portal_admin_article_index_view');
+
+        if (!empty($content)) {
+            return $content;
+        }
+
+        $param = $this->request->param();
+
+        $categoryId = $this->request->param('category', 0, 'intval');
+
+        $postService = new PostService();
+        $data        = $postService->adminArticleList($param);
+
+        $data->appends($param);
+
+        $portalCategoryModel = new PortalCategoryModel();
+        $categoryTree        = $portalCategoryModel->adminCategoryTree($categoryId);
+
+        $this->assign('start_time', isset($param['start_time']) ? $param['start_time'] : '');
+        $this->assign('end_time', isset($param['end_time']) ? $param['end_time'] : '');
+        $this->assign('keyword', isset($param['keyword']) ? $param['keyword'] : '');
+        $this->assign('articles', $data->items());
+        $this->assign('category_tree', $categoryTree);
+        $this->assign('category', $categoryId);
+        $this->assign('page', $data->render());
+
+
+        return $this->fetch();
+    }
+
+    /**
+     * 添加文章
+     * @adminMenu(
+     *     'name'   => '添加文章',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '添加文章',
+     *     'param'  => ''
+     * )
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function add()
+    {
+        $content = hook_one('portal_admin_article_add_view');
+
+        if (!empty($content)) {
+            return $content;
+        }
+
+        $themeModel        = new ThemeModel();
+        $articleThemeFiles = $themeModel->getActionThemeFiles('portal/Article/index');
+        $this->assign('article_theme_files', $articleThemeFiles);
+        return $this->fetch();
+    }
+
+    /**
+     * 添加文章提交
+     * @adminMenu(
+     *     'name'   => '添加文章提交',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '添加文章提交',
+     *     'param'  => ''
+     * )
+     */
+    public function addPost()
+    {
+        if ($this->request->isPost()) {
+            $data = $this->request->param();
+
+            //状态只能设置默认值。未发布、未置顶、未推荐
+            $data['post']['post_status'] = 0;
+            $data['post']['is_top']      = 0;
+            $data['post']['recommended'] = 0;
+
+            $post = $data['post'];
+
+            $result = $this->validate($post, 'AdminArticle');
+            if ($result !== true) {
+                $this->error($result);
+            }
+
+            $portalPostModel = new PortalPostModel();
+
+            if (!empty($data['photo_names']) && !empty($data['photo_urls'])) {
+                $data['post']['more']['photos'] = [];
+                foreach ($data['photo_urls'] as $key => $url) {
+                    $photoUrl = cmf_asset_relative_url($url);
+                    array_push($data['post']['more']['photos'], ["url" => $photoUrl, "name" => $data['photo_names'][$key]]);
+                }
+            }
+
+            if (!empty($data['file_names']) && !empty($data['file_urls'])) {
+                $data['post']['more']['files'] = [];
+                foreach ($data['file_urls'] as $key => $url) {
+                    $fileUrl = cmf_asset_relative_url($url);
+                    array_push($data['post']['more']['files'], ["url" => $fileUrl, "name" => $data['file_names'][$key]]);
+                }
+            }
+
+
+            $portalPostModel->adminAddArticle($data['post'], $data['post']['categories']);
+
+            $data['post']['id'] = $portalPostModel->id;
+            $hookParam          = [
+                'is_add'  => true,
+                'article' => $data['post']
+            ];
+            hook('portal_admin_after_save_article', $hookParam);
+
+
+            $this->success('添加成功!', url('AdminArticle/edit', ['id' => $portalPostModel->id]));
+        }
+
+    }
+
+    /**
+     * 编辑文章
+     * @adminMenu(
+     *     'name'   => '编辑文章',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '编辑文章',
+     *     'param'  => ''
+     * )
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function edit()
+    {
+        $content = hook_one('portal_admin_article_edit_view');
+
+        if (!empty($content)) {
+            return $content;
+        }
+
+        $id = $this->request->param('id', 0, 'intval');
+
+        $portalPostModel = new PortalPostModel();
+        $post            = $portalPostModel->where('id', $id)->find();
+        $postCategories  = $post->categories()->alias('a')->column('a.name', 'a.id');
+        $postCategoryIds = implode(',', array_keys($postCategories));
+
+        $themeModel        = new ThemeModel();
+        $articleThemeFiles = $themeModel->getActionThemeFiles('portal/Article/index');
+        $this->assign('article_theme_files', $articleThemeFiles);
+        $this->assign('post', $post);
+        $this->assign('post_categories', $postCategories);
+        $this->assign('post_category_ids', $postCategoryIds);
+
+        return $this->fetch();
+    }
+
+    /**
+     * 编辑文章提交
+     * @adminMenu(
+     *     'name'   => '编辑文章提交',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '编辑文章提交',
+     *     'param'  => ''
+     * )
+     * @throws \think\Exception
+     */
+    public function editPost()
+    {
+
+        if ($this->request->isPost()) {
+            $data = $this->request->param();
+
+            //需要抹除发布、置顶、推荐的修改。
+            unset($data['post']['post_status']);
+            unset($data['post']['is_top']);
+            unset($data['post']['recommended']);
+
+            $post   = $data['post'];
+            $result = $this->validate($post, 'AdminArticle');
+            if ($result !== true) {
+                $this->error($result);
+            }
+
+            $portalPostModel = new PortalPostModel();
+
+            if (!empty($data['photo_names']) && !empty($data['photo_urls'])) {
+                $data['post']['more']['photos'] = [];
+                foreach ($data['photo_urls'] as $key => $url) {
+                    $photoUrl = cmf_asset_relative_url($url);
+                    array_push($data['post']['more']['photos'], ["url" => $photoUrl, "name" => $data['photo_names'][$key]]);
+                }
+            }
+
+            if (!empty($data['file_names']) && !empty($data['file_urls'])) {
+                $data['post']['more']['files'] = [];
+                foreach ($data['file_urls'] as $key => $url) {
+                    $fileUrl = cmf_asset_relative_url($url);
+                    array_push($data['post']['more']['files'], ["url" => $fileUrl, "name" => $data['file_names'][$key]]);
+                }
+            }
+
+            $portalPostModel->adminEditArticle($data['post'], $data['post']['categories']);
+
+            $hookParam = [
+                'is_add'  => false,
+                'article' => $data['post']
+            ];
+            hook('portal_admin_after_save_article', $hookParam);
+
+            $this->success('保存成功!');
+
+        }
+    }
+
+    /**
+     * 文章删除
+     * @adminMenu(
+     *     'name'   => '文章删除',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章删除',
+     *     'param'  => ''
+     * )
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     * @throws \think\exception\PDOException
+     */
+    public function delete()
+    {
+        $param           = $this->request->param();
+        $portalPostModel = new PortalPostModel();
+
+        if (isset($param['id'])) {
+            $id           = $this->request->param('id', 0, 'intval');
+            $result       = $portalPostModel->where('id', $id)->find();
+            $data         = [
+                'object_id'   => $result['id'],
+                'create_time' => time(),
+                'table_name'  => 'portal_post',
+                'name'        => $result['post_title'],
+                'user_id'     => cmf_get_current_admin_id()
+            ];
+            $resultPortal = $portalPostModel
+                ->where('id', $id)
+                ->update(['delete_time' => time()]);
+            if ($resultPortal) {
+                PortalCategoryPostModel::where('post_id', $id)->update(['status' => 0]);
+                PortalTagPostModel::where('post_id', $id)->update(['status' => 0]);
+
+                RecycleBinModel::insert($data);
+            }
+            $this->success("删除成功!", '');
+
+        }
+
+        if (isset($param['ids'])) {
+            $ids     = $this->request->param('ids/a');
+            $recycle = $portalPostModel->where('id', 'in', $ids)->select();
+            $result  = $portalPostModel->where('id', 'in', $ids)->update(['delete_time' => time()]);
+            if ($result) {
+                PortalCategoryPostModel::where('post_id', 'in', $ids)->update(['status' => 0]);
+                PortalTagPostModel::where('post_id', 'in', $ids)->update(['status' => 0]);
+                foreach ($recycle as $value) {
+                    $data = [
+                        'object_id'   => $value['id'],
+                        'create_time' => time(),
+                        'table_name'  => 'portal_post',
+                        'name'        => $value['post_title'],
+                        'user_id'     => cmf_get_current_admin_id()
+                    ];
+                    RecycleBinModel::insert($data);
+                }
+                $this->success("删除成功!", '');
+            }
+        }
+    }
+
+    /**
+     * 文章发布
+     * @adminMenu(
+     *     'name'   => '文章发布',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章发布',
+     *     'param'  => ''
+     * )
+     */
+    public function publish()
+    {
+        $param           = $this->request->param();
+        $portalPostModel = new PortalPostModel();
+
+        if (isset($param['ids']) && isset($param["yes"])) {
+            $ids = $this->request->param('ids/a');
+            $portalPostModel->where('id', 'in', $ids)->update(['post_status' => 1, 'published_time' => time()]);
+            $this->success("发布成功!", '');
+        }
+
+        if (isset($param['ids']) && isset($param["no"])) {
+            $ids = $this->request->param('ids/a');
+            $portalPostModel->where('id', 'in', $ids)->update(['post_status' => 0]);
+            $this->success("取消发布成功!", '');
+        }
+
+    }
+
+    /**
+     * 文章置顶
+     * @adminMenu(
+     *     'name'   => '文章置顶',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章置顶',
+     *     'param'  => ''
+     * )
+     */
+    public function top()
+    {
+        $param           = $this->request->param();
+        $portalPostModel = new PortalPostModel();
+
+        if (isset($param['ids']) && isset($param["yes"])) {
+            $ids = $this->request->param('ids/a');
+
+            $portalPostModel->where('id', 'in', $ids)->update(['is_top' => 1]);
+
+            $this->success("置顶成功!", '');
+
+        }
+
+        if (isset($_POST['ids']) && isset($param["no"])) {
+            $ids = $this->request->param('ids/a');
+
+            $portalPostModel->where('id', 'in', $ids)->update(['is_top' => 0]);
+
+            $this->success("取消置顶成功!", '');
+        }
+    }
+
+    /**
+     * 文章推荐
+     * @adminMenu(
+     *     'name'   => '文章推荐',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章推荐',
+     *     'param'  => ''
+     * )
+     */
+    public function recommend()
+    {
+        $param           = $this->request->param();
+        $portalPostModel = new PortalPostModel();
+
+        if (isset($param['ids']) && isset($param["yes"])) {
+            $ids = $this->request->param('ids/a');
+
+            $portalPostModel->where('id', 'in', $ids)->update(['recommended' => 1]);
+
+            $this->success("推荐成功!", '');
+
+        }
+        if (isset($param['ids']) && isset($param["no"])) {
+            $ids = $this->request->param('ids/a');
+
+            $portalPostModel->where('id', 'in', $ids)->update(['recommended' => 0]);
+
+            $this->success("取消推荐成功!", '');
+
+        }
+    }
+
+    /**
+     * 文章排序
+     * @adminMenu(
+     *     'name'   => '文章排序',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章排序',
+     *     'param'  => ''
+     * )
+     */
+    public function listOrder()
+    {
+        parent::listOrders('portal_category_post');
+        $this->success("排序更新成功!", '');
+    }
+}

+ 370 - 0
app/portal/controller/AdminCategoryController.php

@@ -0,0 +1,370 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use app\admin\model\RouteModel;
+use app\portal\model\PortalCategoryPostModel;
+use app\portal\model\RecycleBinModel;
+use cmf\controller\AdminBaseController;
+use app\portal\model\PortalCategoryModel;
+use app\admin\model\ThemeModel;
+
+
+class AdminCategoryController extends AdminBaseController
+{
+    /**
+     * 文章分类列表
+     * @adminMenu(
+     *     'name'   => '分类管理',
+     *     'parent' => 'portal/AdminIndex/default',
+     *     'display'=> true,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章分类列表',
+     *     'param'  => ''
+     * )
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function index()
+    {
+        $content = hook_one('portal_admin_category_index_view');
+
+        if (!empty($content)) {
+            return $content;
+        }
+
+        $portalCategoryModel = new PortalCategoryModel();
+        $keyword             = $this->request->param('keyword');
+
+        if (empty($keyword)) {
+            $categoryTree = $portalCategoryModel->adminCategoryTableTree();
+            $this->assign('category_tree', $categoryTree);
+        } else {
+            $categories = $portalCategoryModel->where('name', 'like', "%{$keyword}%")
+                ->where('delete_time', 0)->select();
+            $this->assign('categories', $categories);
+        }
+
+        $this->assign('keyword', $keyword);
+
+        return $this->fetch();
+    }
+
+    /**
+     * 添加文章分类
+     * @adminMenu(
+     *     'name'   => '添加文章分类',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '添加文章分类',
+     *     'param'  => ''
+     * )
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function add()
+    {
+        $content = hook_one('portal_admin_category_add_view');
+
+        if (!empty($content)) {
+            return $content;
+        }
+
+        $parentId            = $this->request->param('parent', 0, 'intval');
+        $portalCategoryModel = new PortalCategoryModel();
+        $categoriesTree      = $portalCategoryModel->adminCategoryTree($parentId);
+
+        $themeModel        = new ThemeModel();
+        $listThemeFiles    = $themeModel->getActionThemeFiles('portal/List/index');
+        $articleThemeFiles = $themeModel->getActionThemeFiles('portal/Article/index');
+
+        $this->assign('list_theme_files', $listThemeFiles);
+        $this->assign('article_theme_files', $articleThemeFiles);
+        $this->assign('categories_tree', $categoriesTree);
+        return $this->fetch();
+    }
+
+    /**
+     * 添加文章分类提交
+     * @adminMenu(
+     *     'name'   => '添加文章分类提交',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '添加文章分类提交',
+     *     'param'  => ''
+     * )
+     */
+    public function addPost()
+    {
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $data = $this->request->param();
+
+        $result = $this->validate($data, 'PortalCategory');
+
+        if ($result !== true) {
+            $this->error($result);
+        }
+
+        $result = $portalCategoryModel->addCategory($data);
+
+        if ($result === false) {
+            $this->error('添加失败!');
+        }
+
+        $this->success('添加成功!', url('AdminCategory/index'));
+    }
+
+    /**
+     * 编辑文章分类
+     * @adminMenu(
+     *     'name'   => '编辑文章分类',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '编辑文章分类',
+     *     'param'  => ''
+     * )
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function edit()
+    {
+
+        $content = hook_one('portal_admin_category_edit_view');
+
+        if (!empty($content)) {
+            return $content;
+        }
+
+        $id = $this->request->param('id', 0, 'intval');
+        if ($id > 0) {
+            $portalCategoryModel = new PortalCategoryModel();
+            $category            = $portalCategoryModel->find($id)->toArray();
+
+            $categoriesTree = $portalCategoryModel->adminCategoryTree($category['parent_id'], $id);
+
+            $themeModel        = new ThemeModel();
+            $listThemeFiles    = $themeModel->getActionThemeFiles('portal/List/index');
+            $articleThemeFiles = $themeModel->getActionThemeFiles('portal/Article/index');
+
+            $routeModel = new RouteModel();
+            $alias      = $routeModel->getUrl('portal/List/index', ['id' => $id]);
+
+            $category['alias'] = $alias;
+            $this->assign($category);
+            $this->assign('list_theme_files', $listThemeFiles);
+            $this->assign('article_theme_files', $articleThemeFiles);
+            $this->assign('categories_tree', $categoriesTree);
+            return $this->fetch();
+        } else {
+            $this->error('操作错误!');
+        }
+
+    }
+
+    /**
+     * 编辑文章分类提交
+     * @adminMenu(
+     *     'name'   => '编辑文章分类提交',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '编辑文章分类提交',
+     *     'param'  => ''
+     * )
+     */
+    public function editPost()
+    {
+        $data = $this->request->param();
+
+        $result = $this->validate($data, 'PortalCategory');
+
+        if ($result !== true) {
+            $this->error($result);
+        }
+
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $result = $portalCategoryModel->editCategory($data);
+
+        if ($result === false) {
+            $this->error('保存失败!');
+        }
+
+        $this->success('保存成功!');
+    }
+
+    /**
+     * 文章分类选择对话框
+     * @adminMenu(
+     *     'name'   => '文章分类选择对话框',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章分类选择对话框',
+     *     'param'  => ''
+     * )
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function select()
+    {
+        $ids                 = $this->request->param('ids');
+        $selectedIds         = explode(',', $ids);
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $tpl = <<<tpl
+<tr class='data-item-tr'>
+    <td>
+        <input type='checkbox' class='js-check' data-yid='js-check-y' data-xid='js-check-x' name='ids[]'
+               value='\$id' data-name='\$name' \$checked>
+    </td>
+    <td>\$id</td>
+    <td>\$spacer <a href='\$url' target='_blank'>\$name</a></td>
+</tr>
+tpl;
+
+        $categoryTree = $portalCategoryModel->adminCategoryTableTree($selectedIds, $tpl);
+
+        $categories = $portalCategoryModel->where('delete_time', 0)->select();
+
+        $this->assign('categories', $categories);
+        $this->assign('selectedIds', $selectedIds);
+        $this->assign('categories_tree', $categoryTree);
+        return $this->fetch();
+    }
+
+    /**
+     * 文章分类排序
+     * @adminMenu(
+     *     'name'   => '文章分类排序',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章分类排序',
+     *     'param'  => ''
+     * )
+     */
+    public function listOrder()
+    {
+        parent::listOrders('portal_category');
+        $this->success("排序更新成功!", '');
+    }
+
+    /**
+     * 文章分类显示隐藏
+     * @adminMenu(
+     *     'name'   => '文章分类显示隐藏',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章分类显示隐藏',
+     *     'param'  => ''
+     * )
+     */
+    public function toggle()
+    {
+        $data                = $this->request->param();
+        $portalCategoryModel = new PortalCategoryModel();
+        $ids                 = $this->request->param('ids/a');
+
+        if (isset($data['ids']) && !empty($data["display"])) {
+            $portalCategoryModel->where('id', 'in', $ids)->update(['status' => 1]);
+            $this->success("更新成功!");
+        }
+
+        if (isset($data['ids']) && !empty($data["hide"])) {
+            $portalCategoryModel->where('id', 'in', $ids)->update(['status' => 0]);
+            $this->success("更新成功!");
+        }
+
+    }
+
+    /**
+     * 删除文章分类
+     * @adminMenu(
+     *     'name'   => '删除文章分类',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '删除文章分类',
+     *     'param'  => ''
+     * )
+     */
+    public function delete()
+    {
+        $portalCategoryModel = new PortalCategoryModel();
+        $id                  = $this->request->param('id');
+        //获取删除的内容
+        $findCategory = $portalCategoryModel->where('id', $id)->find();
+
+        if (empty($findCategory)) {
+            $this->error('分类不存在!');
+        }
+        //判断此分类有无子分类(不算被删除的子分类)
+        $categoryChildrenCount = $portalCategoryModel->where(['parent_id' => $id, 'delete_time' => 0])->count();
+
+        if ($categoryChildrenCount > 0) {
+            $this->error('此分类有子类无法删除!');
+        }
+
+        $categoryPostCount = PortalCategoryPostModel::where('category_id', $id)->count();
+
+        if ($categoryPostCount > 0) {
+            $this->error('此分类有文章无法删除!');
+        }
+
+        $data   = [
+            'object_id'   => $findCategory['id'],
+            'create_time' => time(),
+            'table_name'  => 'portal_category',
+            'name'        => $findCategory['name']
+        ];
+        $result = $portalCategoryModel
+            ->where('id', $id)
+            ->update(['delete_time' => time()]);
+        if ($result) {
+            RecycleBinModel::insert($data);
+            $this->success('删除成功!');
+        } else {
+            $this->error('删除失败');
+        }
+    }
+}

+ 32 - 0
app/portal/controller/AdminIndexController.php

@@ -0,0 +1,32 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use cmf\controller\AdminBaseController;
+
+/**
+ * Class AdminIndexController
+ * @package app\portal\controller
+ * @adminMenuRoot(
+ *     'name'   =>'门户管理',
+ *     'action' =>'default',
+ *     'parent' =>'',
+ *     'display'=> true,
+ *     'order'  => 30,
+ *     'icon'   =>'th',
+ *     'remark' =>'门户管理'
+ * )
+ */
+class AdminIndexController extends AdminBaseController
+{
+
+
+}

+ 239 - 0
app/portal/controller/AdminPageController.php

@@ -0,0 +1,239 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use app\admin\model\RouteModel;
+use cmf\controller\AdminBaseController;
+use app\portal\model\PortalPostModel;
+use app\portal\service\PostService;
+use app\admin\model\ThemeModel;
+
+class AdminPageController extends AdminBaseController
+{
+
+    /**
+     * 页面管理
+     * @adminMenu(
+     *     'name'   => '页面管理',
+     *     'parent' => 'portal/AdminIndex/default',
+     *     'display'=> true,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '页面管理',
+     *     'param'  => ''
+     * )
+     */
+    public function index()
+    {
+        $content = hook_one('portal_admin_page_index_view');
+
+        if (!empty($content)) {
+            return $content;
+        }
+
+        $param = $this->request->param();
+
+        $postService = new PostService();
+        $data        = $postService->adminPageList($param);
+        $data->appends($param);
+
+        $this->assign('keyword', isset($param['keyword']) ? $param['keyword'] : '');
+        $this->assign('pages', $data->items());
+        $this->assign('page', $data->render());
+
+        return $this->fetch();
+    }
+
+    /**
+     * 添加页面
+     * @adminMenu(
+     *     'name'   => '添加页面',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '添加页面',
+     *     'param'  => ''
+     * )
+     */
+    public function add()
+    {
+        $content = hook_one('portal_admin_page_add_view');
+
+        if (!empty($content)) {
+            return $content;
+        }
+
+        $themeModel     = new ThemeModel();
+        $pageThemeFiles = $themeModel->getActionThemeFiles('portal/Page/index');
+        $this->assign('page_theme_files', $pageThemeFiles);
+        return $this->fetch();
+    }
+
+    /**
+     * 添加页面提交
+     * @adminMenu(
+     *     'name'   => '添加页面提交',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '添加页面提交',
+     *     'param'  => ''
+     * )
+     */
+    public function addPost()
+    {
+        $data = $this->request->param();
+
+        $result = $this->validate($data['post'], 'AdminPage');
+        if ($result !== true) {
+            $this->error($result);
+        }
+
+        if (!empty($data['photo_names']) && !empty($data['photo_urls'])) {
+            $data['post']['more']['photos'] = [];
+            foreach ($data['photo_urls'] as $key => $url) {
+                $photoUrl = cmf_asset_relative_url($url);
+                array_push($data['post']['more']['photos'], ["url" => $photoUrl, "name" => $data['photo_names'][$key]]);
+            }
+        }
+
+        if (!empty($data['file_names']) && !empty($data['file_urls'])) {
+            $data['post']['more']['files'] = [];
+            foreach ($data['file_urls'] as $key => $url) {
+                $fileUrl = cmf_asset_relative_url($url);
+                array_push($data['post']['more']['files'], ["url" => $fileUrl, "name" => $data['file_names'][$key]]);
+            }
+        }
+
+        $portalPostModel = new PortalPostModel();
+        $portalPostModel->adminAddPage($data['post']);
+        $this->success(lang('ADD_SUCCESS'), url('AdminPage/edit', ['id' => $portalPostModel->id]));
+
+    }
+
+    /**
+     * 编辑页面
+     * @adminMenu(
+     *     'name'   => '编辑页面',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '编辑页面',
+     *     'param'  => ''
+     * )
+     */
+    public function edit()
+    {
+        $content = hook_one('portal_admin_page_edit_view');
+
+        if (!empty($content)) {
+            return $content;
+        }
+
+        $id = $this->request->param('id', 0, 'intval');
+
+        $portalPostModel = new PortalPostModel();
+        $post            = $portalPostModel->where('id', $id)->find();
+
+        $themeModel     = new ThemeModel();
+        $pageThemeFiles = $themeModel->getActionThemeFiles('portal/Page/index');
+
+        $routeModel         = new RouteModel();
+        $alias              = $routeModel->getUrl('portal/Page/index', ['id' => $id]);
+        $post['post_alias'] = $alias;
+        $this->assign('page_theme_files', $pageThemeFiles);
+        $this->assign('post', $post);
+
+        return $this->fetch();
+    }
+
+    /**
+     * 编辑页面提交
+     * @adminMenu(
+     *     'name'   => '编辑页面提交',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '编辑页面提交',
+     *     'param'  => ''
+     * )
+     */
+    public function editPost()
+    {
+        $data = $this->request->param();
+
+        $result = $this->validate($data['post'], 'AdminPage');
+        if ($result !== true) {
+            $this->error($result);
+        }
+
+        if (!empty($data['photo_names']) && !empty($data['photo_urls'])) {
+            $data['post']['more']['photos'] = [];
+            foreach ($data['photo_urls'] as $key => $url) {
+                $photoUrl = cmf_asset_relative_url($url);
+                array_push($data['post']['more']['photos'], ["url" => $photoUrl, "name" => $data['photo_names'][$key]]);
+            }
+        }
+
+        if (!empty($data['file_names']) && !empty($data['file_urls'])) {
+            $data['post']['more']['files'] = [];
+            foreach ($data['file_urls'] as $key => $url) {
+                $fileUrl = cmf_asset_relative_url($url);
+                array_push($data['post']['more']['files'], ["url" => $fileUrl, "name" => $data['file_names'][$key]]);
+            }
+        }
+
+        $portalPostModel = new PortalPostModel();
+
+        $portalPostModel->adminEditPage($data['post']);
+
+        $this->success(lang('SAVE_SUCCESS'));
+
+    }
+
+    /**
+     * 删除页面
+     * @author    iyting@foxmail.com
+     * @adminMenu(
+     *     'name'   => '删除页面',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '删除页面',
+     *     'param'  => ''
+     * )
+     */
+    public function delete()
+    {
+        $portalPostModel = new PortalPostModel();
+        $data            = $this->request->param();
+
+        $result = $portalPostModel->adminDeletePage($data);
+        if ($result) {
+            $this->success(lang('DELETE_SUCCESS'));
+        } else {
+            $this->error(lang('DELETE_FAILED'));
+        }
+
+    }
+
+}

+ 166 - 0
app/portal/controller/AdminTagController.php

@@ -0,0 +1,166 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author:kane < chengjin005@163.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use app\portal\model\PortalTagModel;
+use app\portal\model\PortalTagPostModel;
+use cmf\controller\AdminBaseController;
+
+/**
+ * Class AdminTagController 标签管理控制器
+ * @package app\portal\controller
+ */
+class AdminTagController extends AdminBaseController
+{
+    /**
+     * 文章标签管理
+     * @adminMenu(
+     *     'name'   => '文章标签',
+     *     'parent' => 'portal/AdminIndex/default',
+     *     'display'=> true,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '文章标签',
+     *     'param'  => ''
+     * )
+     */
+    public function index()
+    {
+        $content = hook_one('portal_admin_tag_index_view');
+
+        if (!empty($content)) {
+            return $content;
+        }
+
+        $portalTagModel = new PortalTagModel();
+        $tags           = $portalTagModel->paginate();
+
+        $this->assign("arrStatus", $portalTagModel::$STATUS);
+        $this->assign("tags", $tags);
+        $this->assign('page', $tags->render());
+        return $this->fetch();
+    }
+
+    /**
+     * 添加文章标签
+     * @adminMenu(
+     *     'name'   => '添加文章标签',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> true,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '添加文章标签',
+     *     'param'  => ''
+     * )
+     */
+    public function add()
+    {
+        $portalTagModel = new PortalTagModel();
+        $this->assign("arrStatus", $portalTagModel::$STATUS);
+        return $this->fetch();
+    }
+
+    /**
+     * 添加文章标签提交
+     * @adminMenu(
+     *     'name'   => '添加文章标签提交',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '添加文章标签提交',
+     *     'param'  => ''
+     * )
+     */
+    public function addPost()
+    {
+
+        $tagData = $this->request->param();
+        $result = $this->validate(
+                $tagData,
+                [
+                    'name'  => 'require|max:20',
+                ],
+                [
+                    'name.require' => '标签名称必填!',
+                    'name.max' => '标签名称超过最大长度!!',
+                ]
+            );
+
+        if (true !== $result) {
+            // 验证失败 输出错误信息
+            $this->error($result);
+        }
+        $portalTagModel = new PortalTagModel();
+        $portalTagModel->save($tagData);
+
+        $this->success(lang("SAVE_SUCCESS"),'index');
+
+    }
+
+    /**
+     * 更新文章标签状态
+     * @adminMenu(
+     *     'name'   => '更新标签状态',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '更新标签状态',
+     *     'param'  => ''
+     * )
+     */
+    public function upStatus()
+    {
+        $intId     = $this->request->param("id");
+        $intStatus = $this->request->param("status");
+        $intStatus = $intStatus ? 1 : 0;
+        if (empty($intId)) {
+            $this->error(lang("NO_ID"));
+        }
+
+        $portalTagModel = new PortalTagModel();
+        $portalTagModel->where("id", $intId)->update(["status" => $intStatus]);
+
+        $this->success(lang("SAVE_SUCCESS"));
+
+    }
+
+    /**
+     * 删除文章标签
+     * @adminMenu(
+     *     'name'   => '删除文章标签',
+     *     'parent' => 'index',
+     *     'display'=> false,
+     *     'hasView'=> false,
+     *     'order'  => 10000,
+     *     'icon'   => '',
+     *     'remark' => '删除文章标签',
+     *     'param'  => ''
+     * )
+     */
+    public function delete()
+    {
+        $intId = $this->request->param("id", 0, 'intval');
+
+        if (empty($intId)) {
+            $this->error(lang("NO_ID"));
+        }
+
+        PortalTagModel::where('id', $intId)->delete();
+        PortalTagPostModel::where('tag_id', $intId)->delete();
+        $this->success(lang("DELETE_SUCCESS"));
+    }
+}

+ 101 - 0
app/portal/controller/ArticleController.php

@@ -0,0 +1,101 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use cmf\controller\HomeBaseController;
+use app\portal\model\PortalCategoryModel;
+use app\portal\service\PostService;
+use app\portal\model\PortalPostModel;
+
+class ArticleController extends HomeBaseController
+{
+    /**
+     * 文章详情
+     * @return mixed
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function index()
+    {
+
+        $portalCategoryModel = new PortalCategoryModel();
+        $postService         = new PostService();
+
+        $articleId  = $this->request->param('id', 0, 'intval');
+        $categoryId = $this->request->param('cid', 0, 'intval');
+        $article    = $postService->publishedArticle($articleId, $categoryId);
+
+        if (empty($article)) {
+            abort(404, '文章不存在!');
+        }
+
+
+        $prevArticle = $postService->publishedPrevArticle($articleId, $categoryId);
+        $nextArticle = $postService->publishedNextArticle($articleId, $categoryId);
+
+        $tplName = 'article';
+
+        if (empty($categoryId)) {
+            $categories = $article['categories'];
+
+            if (count($categories) > 0) {
+                $this->assign('category', $categories[0]);
+            } else {
+                abort(404, '文章未指定分类!');
+            }
+
+        } else {
+            $category = $portalCategoryModel->where('id', $categoryId)->where('status', 1)->find();
+
+            if (empty($category)) {
+                abort(404, '文章不存在!');
+            }
+
+            $this->assign('category', $category);
+
+            $tplName = empty($category["one_tpl"]) ? $tplName : $category["one_tpl"];
+        }
+
+        PortalPostModel::where('id', $articleId)->inc('post_hits')->update();
+
+
+        hook('portal_before_assign_article', $article);
+
+        $this->assign('article', $article);
+        $this->assign('prev_article', $prevArticle);
+        $this->assign('next_article', $nextArticle);
+
+        $tplName = empty($article['more']['template']) ? $tplName : $article['more']['template'];
+
+        return $this->fetch("/$tplName");
+    }
+
+    // 文章点赞
+    public function doLike()
+    {
+        $this->checkUserLogin();
+        $articleId = $this->request->param('id', 0, 'intval');
+
+
+        $canLike = cmf_check_user_action("posts$articleId", 1);
+
+        if ($canLike) {
+            PortalPostModel::where('id', $articleId)->inc('post_like')->update();
+
+            $this->success("赞好啦!");
+        } else {
+            $this->error("您已赞过啦!");
+        }
+    }
+
+}

+ 21 - 0
app/portal/controller/IndexController.php

@@ -0,0 +1,21 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use cmf\controller\HomeBaseController;
+
+class IndexController extends HomeBaseController
+{
+    public function index()
+    {
+        return $this->fetch(':index');
+    }
+}

+ 39 - 0
app/portal/controller/ListController.php

@@ -0,0 +1,39 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use cmf\controller\HomeBaseController;
+use app\portal\model\PortalCategoryModel;
+
+class ListController extends HomeBaseController
+{
+    /***
+     * 文章列表
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function index()
+    {
+        $id                  = $this->request->param('id', 0, 'intval');
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $category = $portalCategoryModel->where('id', $id)->where('status', 1)->find();
+       
+        $this->assign('category', $category);
+
+        $listTpl = empty($category['list_tpl']) ? 'list' : $category['list_tpl'];
+
+        return $this->fetch('/' . $listTpl);
+    }
+
+}

+ 44 - 0
app/portal/controller/PageController.php

@@ -0,0 +1,44 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use cmf\controller\HomeBaseController;
+use app\portal\service\PostService;
+
+class PageController extends HomeBaseController
+{
+    /**
+     * 页面管理
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function index()
+    {
+        $postService = new PostService();
+        $pageId      = $this->request->param('id', 0, 'intval');
+        $page        = $postService->publishedPage($pageId);
+
+        if (empty($page)) {
+            abort(404, ' 页面不存在!');
+        }
+
+        $this->assign('page', $page);
+
+        $more = $page['more'];
+
+        $tplName = empty($more['template']) ? 'page' : $more['template'];
+
+        return $this->fetch("/$tplName");
+    }
+
+}

+ 32 - 0
app/portal/controller/SearchController.php

@@ -0,0 +1,32 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use cmf\controller\HomeBaseController;
+
+class SearchController extends HomeBaseController
+{
+    /**
+     * 搜索
+     * @return mixed
+     */
+    public function index()
+    {
+        $keyword = $this->request->param('keyword');
+
+        if (empty($keyword)) {
+            $this -> error("关键词不能为空!请重新输入!");
+        }
+
+        $this -> assign("keyword", $keyword);
+        return $this->fetch('/search');
+    }
+}

+ 47 - 0
app/portal/controller/TagController.php

@@ -0,0 +1,47 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\controller;
+
+use cmf\controller\HomeBaseController;
+use app\portal\model\PortalTagModel;
+
+class TagController extends HomeBaseController
+{
+    /**
+     * 标签
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function index()
+    {
+        $id             = $this->request->param('id');
+
+        $portalTagModel = new PortalTagModel();
+
+        if(is_numeric($id)){
+            $tag = $portalTagModel->where('id', $id)->where('status', 1)->find();
+        }else{
+            $tag = $portalTagModel->where('name', $id)->where('status', 1)->find();
+        }
+
+
+        if (empty($tag)) {
+            abort(404, '标签不存在!');
+        }
+
+        $this->assign('tag', $tag);
+
+        return $this->fetch('/tag');
+    }
+
+}

+ 113 - 0
app/portal/data/portal.sql

@@ -0,0 +1,113 @@
+
+--
+-- 表的结构 `cmf_portal_category`
+--
+
+CREATE TABLE IF NOT EXISTS `cmf_portal_category` (
+  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '分类id',
+  `parent_id` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '分类父id',
+  `post_count` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '分类文章数',
+  `status` tinyint(3) UNSIGNED NOT NULL DEFAULT '1' COMMENT '状态,1:发布,0:不发布',
+  `delete_time` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '删除时间',
+  `list_order` float NOT NULL DEFAULT '10000' COMMENT '排序',
+  `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '分类名称',
+  `description` varchar(255) NOT NULL DEFAULT '' COMMENT '分类描述',
+  `path` varchar(255) NOT NULL DEFAULT '' COMMENT '分类层级关系路径',
+  `seo_title` varchar(100) NOT NULL DEFAULT '',
+  `seo_keywords` varchar(255) NOT NULL DEFAULT '',
+  `seo_description` varchar(255) NOT NULL DEFAULT '',
+  `list_tpl` varchar(50) NOT NULL DEFAULT '' COMMENT '分类列表模板',
+  `one_tpl` varchar(50) NOT NULL DEFAULT '' COMMENT '分类文章页模板',
+  `more` text COMMENT '扩展属性',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='portal应用 文章分类表';
+
+-- --------------------------------------------------------
+
+--
+-- 表的结构 `cmf_portal_category_post`
+--
+
+CREATE TABLE IF NOT EXISTS `cmf_portal_category_post` (
+  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `post_id` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '文章id',
+  `category_id` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '分类id',
+  `list_order` float NOT NULL DEFAULT '10000' COMMENT '排序',
+  `status` tinyint(3) UNSIGNED NOT NULL DEFAULT '1' COMMENT '状态,1:发布;0:不发布',
+  PRIMARY KEY (`id`),
+  KEY `term_taxonomy_id` (`category_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='portal应用 分类文章对应表';
+
+-- --------------------------------------------------------
+
+--
+-- 表的结构 `cmf_portal_post`
+--
+
+CREATE TABLE IF NOT EXISTS `cmf_portal_post` (
+  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `parent_id` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '父级id',
+  `post_type` tinyint(3) UNSIGNED NOT NULL DEFAULT '1' COMMENT '类型,1:文章;2:页面',
+  `post_format` tinyint(3) UNSIGNED NOT NULL DEFAULT '1' COMMENT '内容格式;1:html;2:md',
+  `user_id` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '发表者用户id',
+  `post_status` tinyint(3) UNSIGNED NOT NULL DEFAULT '1' COMMENT '状态;1:已发布;0:未发布;',
+  `comment_status` tinyint(3) UNSIGNED NOT NULL DEFAULT '1' COMMENT '评论状态;1:允许;0:不允许',
+  `is_top` tinyint(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否置顶;1:置顶;0:不置顶',
+  `recommended` tinyint(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否推荐;1:推荐;0:不推荐',
+  `post_hits` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '查看数',
+  `post_like` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '点赞数',
+  `comment_count` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '评论数',
+  `create_time` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间',
+  `update_time` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '更新时间',
+  `published_time` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '发布时间',
+  `delete_time` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '删除时间',
+  `post_title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'post标题',
+  `post_keywords` varchar(150) NOT NULL DEFAULT '' COMMENT 'seo keywords',
+  `post_excerpt` varchar(500) NOT NULL DEFAULT '' COMMENT 'post摘要',
+  `post_source` varchar(150) NOT NULL DEFAULT '' COMMENT '转载文章的来源',
+  `post_content` text COMMENT '文章内容',
+  `post_content_filtered` text COMMENT '处理过的文章内容',
+  `more` text COMMENT '扩展属性,如缩略图;格式为json',
+  PRIMARY KEY (`id`),
+  KEY `type_status_date` (`post_type`,`post_status`,`create_time`,`id`),
+  KEY `parent_id` (`parent_id`),
+  KEY `user_id` (`user_id`),
+  KEY `create_time` (`create_time`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='portal应用 文章表' ROW_FORMAT=COMPACT;
+
+-- --------------------------------------------------------
+
+--
+-- 表的结构 `cmf_portal_tag`
+--
+
+CREATE TABLE IF NOT EXISTS `cmf_portal_tag` (
+  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '分类id',
+  `status` tinyint(3) UNSIGNED NOT NULL DEFAULT '1' COMMENT '状态,1:发布,0:不发布',
+  `recommended` tinyint(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否推荐;1:推荐;0:不推荐',
+  `post_count` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '标签文章数',
+  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '标签名称',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='portal应用 文章标签表';
+
+-- --------------------------------------------------------
+
+--
+-- 表的结构 `cmf_portal_tag_post`
+--
+
+CREATE TABLE IF NOT EXISTS `cmf_portal_tag_post` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `tag_id` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '标签 id',
+  `post_id` bigint(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '文章 id',
+  `status` tinyint(3) UNSIGNED NOT NULL DEFAULT '1' COMMENT '状态,1:发布;0:不发布',
+  PRIMARY KEY (`id`),
+  KEY `post_id` (`post_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='portal应用 标签文章对应表';
+
+-- --------------------------------------------------------
+
+
+-- 增缩略图字段
+ALTER TABLE `cmf_portal_post` ADD `thumbnail` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '缩略图' AFTER `post_source`;
+ALTER TABLE `cmf_portal_post` ADD `post_favorites` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT '收藏数' AFTER `post_hits`;

+ 96 - 0
app/portal/hooks.php

@@ -0,0 +1,96 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+return [
+    'portal_before_assign_article'    => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '文章显示之前', // 钩子名称
+        "description" => "文章显示之前", //钩子描述
+        "once"        => 0 // 是否只执行一次
+    ],
+    'portal_admin_after_save_article' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '后台文章保存之后', // 钩子名称
+        "description" => "后台文章保存之后", //钩子描述
+        "once"        => 0 // 是否只执行一次
+    ],
+    'portal_admin_article_index_view' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台文章管理列表界面', // 钩子名称
+        "description" => "门户后台文章管理列表界面", //钩子描述
+        "once"        => 1 // 是否只执行一次
+    ],
+    'portal_admin_article_add_view' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台文章添加界面', // 钩子名称
+        "description" => "门户后台文章添加界面", //钩子描述
+        "once"        => 1 // 是否只执行一次
+    ],
+    'portal_admin_article_edit_view' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台文章编辑界面', // 钩子名称
+        "description" => "门户后台文章编辑界面", //钩子描述
+        "once"        => 1 // 是否只执行一次
+    ],
+    'portal_admin_category_index_view' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台文章分类管理列表界面', // 钩子名称
+        "description" => "门户后台文章分类管理列表界面", //钩子描述
+        "once"        => 1 // 是否只执行一次
+    ],
+    'portal_admin_category_add_view' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台文章分类添加界面', // 钩子名称
+        "description" => "门户后台文章分类添加界面", //钩子描述
+        "once"        => 1 // 是否只执行一次
+    ],
+    'portal_admin_category_edit_view' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台文章分类编辑界面', // 钩子名称
+        "description" => "门户后台文章分类编辑界面", //钩子描述
+        "once"        => 1 // 是否只执行一次
+    ],
+    'portal_admin_page_index_view' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台页面管理列表界面', // 钩子名称
+        "description" => "门户后台页面管理列表界面", //钩子描述
+        "once"        => 1 // 是否只执行一次
+    ],
+    'portal_admin_page_add_view' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台页面添加界面', // 钩子名称
+        "description" => "门户后台页面添加界面", //钩子描述
+        "once"        => 1 // 是否只执行一次
+    ],
+    'portal_admin_page_edit_view' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台页面编辑界面', // 钩子名称
+        "description" => "门户后台页面编辑界面", //钩子描述
+        "once"        => 1 // 是否只执行一次
+    ],
+    'portal_admin_tag_index_view' => [
+        "type"        => 2,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台文章标签管理列表界面', // 钩子名称
+        "description" => "门户后台文章标签管理列表界面", //钩子描述
+        "once"        => 1 // 是否只执行一次
+    ],
+    'portal_admin_article_edit_view_right_sidebar' => [
+        "type"        => 4,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台文章添加编辑界面右侧栏', // 钩子名称
+        "description" => "门户后台文章添加编辑界面右侧栏", //钩子描述
+        "once"        => 0 // 是否只执行一次
+    ],
+    'portal_admin_article_edit_view_main' => [
+        "type"        => 4,//钩子类型(默认为应用钩子;2:应用钩子;3:模板钩子;4:后台模板钩子)
+        "name"        => '门户后台文章添加编辑界面主要内容', // 钩子名称
+        "description" => "门户后台文章添加编辑界面主要内容", //钩子描述
+        "once"        => 0 // 是否只执行一次
+    ],
+];

+ 13 - 0
app/portal/lang/en-us.php

@@ -0,0 +1,13 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+return [
+    'ADMIN_CENTER' => 'Admin Center',
+];

+ 15 - 0
app/portal/lang/en-us/common.php

@@ -0,0 +1,15 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+return [
+    'TABLE_PORTAL_CATEGORY'  => 'Article Category',
+    'TABLE_PORTAL_POST'      => 'Article',
+    'TABLE_PORTAL_POST#PAGE' => 'Page'
+];

+ 21 - 0
app/portal/lang/zh-cn.php

@@ -0,0 +1,21 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+return [
+    'ADMIN_TAG_ADD'    => '添加标签',
+    'ADMIN_TAG_DELETE' => '删除标签',
+    'ADMIN_TAG_INDEX'  => '标签列表',
+    'SAVE_SUCCESS'     => '保存成功!',
+    'DELETE_SUCCESS'   => '删除成功!',
+    'ADD_SUCCESS'      => '添加成功',
+    'DELETE_FAILED'    => '删除失败',
+    'ADD_FAILED'       => '添加失败',
+];
+

+ 16 - 0
app/portal/lang/zh-cn/common.php

@@ -0,0 +1,16 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+return [
+    'TABLE_PORTAL_CATEGORY'  => '文章分类',
+    'TABLE_PORTAL_POST'      => '文章',
+    'TABLE_SLIDE'      => '幻灯片',
+    'TABLE_PORTAL_POST#PAGE' => '页面'
+];

+ 13 - 0
app/portal/lang/zh-cn/home.php

@@ -0,0 +1,13 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+/*前台通用语言包*/
+return [
+];

+ 234 - 0
app/portal/model/PortalCategoryModel.php

@@ -0,0 +1,234 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\model;
+
+use app\admin\model\RouteModel;
+use think\Model;
+use tree\Tree;
+use think\db\Query;
+
+class PortalCategoryModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'portal_category';
+
+    protected $type = [
+        'more' => 'array',
+    ];
+
+    public function getArticleTotalCountAttr($value, $data)
+    {
+        $total = PortalCategoryPostModel::where('category_id', $data['id'])->where('status', 1)->count();
+        return $total;
+    }
+
+    /**
+     * 生成分类 select树形结构
+     * @param int $selectId   需要选中的分类 id
+     * @param int $currentCid 需要隐藏的分类 id
+     * @return string
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function adminCategoryTree($selectId = 0, $currentCid = 0)
+    {
+        $categories = $this->order("list_order ASC")
+            ->where('delete_time', 0)
+            ->where(function (Query $query) use ($currentCid) {
+                if (!empty($currentCid)) {
+                    $query->where('id', '<>', $currentCid);
+                }
+            })
+            ->select()->toArray();
+
+        $tree       = new Tree();
+        $tree->icon = ['&nbsp;&nbsp;│', '&nbsp;&nbsp;├─', '&nbsp;&nbsp;└─'];
+        $tree->nbsp = '&nbsp;&nbsp;';
+
+        $newCategories = [];
+        foreach ($categories as $item) {
+            $item['selected'] = $selectId == $item['id'] ? "selected" : "";
+
+            array_push($newCategories, $item);
+        }
+
+        $tree->init($newCategories);
+        $str     = '<option value=\"{$id}\" {$selected}>{$spacer}{$name}</option>';
+        $treeStr = $tree->getTree(0, $str);
+
+        return $treeStr;
+    }
+
+    /**
+     * 分类树形结构
+     * @param int    $currentIds
+     * @param string $tpl
+     * @return string
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function adminCategoryTableTree($currentIds = 0, $tpl = '')
+    {
+//        if (!empty($currentCid)) {
+//            $where['id'] = ['neq', $currentCid];
+//        }
+        $categories = $this->order("list_order ASC")->where('delete_time', 0)->select()->toArray();
+
+        $tree       = new Tree();
+        $tree->icon = ['&nbsp;&nbsp;│', '&nbsp;&nbsp;├─', '&nbsp;&nbsp;└─'];
+        $tree->nbsp = '&nbsp;&nbsp;';
+
+        if (!is_array($currentIds)) {
+            $currentIds = [$currentIds];
+        }
+
+        $newCategories = [];
+        foreach ($categories as $item) {
+            $item['parent_id_node'] = ($item['parent_id']) ? ' class="child-of-node-' . $item['parent_id'] . '"' : '';
+            $item['style']          = empty($item['parent_id']) ? '' : 'display:none;';
+            $item['status_text']    = empty($item['status']) ? '<span class="label label-warning">隐藏</span>' : '<span class="label label-success">显示</span>';
+            $item['checked']        = in_array($item['id'], $currentIds) ? "checked" : "";
+            $item['url']            = cmf_url('portal/List/index', ['id' => $item['id']]);
+            $item['str_action']     = '<a class="btn btn-xs btn-primary" href="' . url("AdminCategory/add", ["parent" => $item['id']]) . '">添加子分类</a>  <a class="btn btn-xs btn-primary" href="' . url("AdminCategory/edit", ["id" => $item['id']]) . '">' . lang('EDIT') . '</a>  <a class="btn btn-xs btn-danger js-ajax-delete" href="' . url("AdminCategory/delete", ["id" => $item['id']]) . '">' . lang('DELETE') . '</a> ';
+            if ($item['status']) {
+                $item['str_action'] .= '<a class="btn btn-xs btn-warning js-ajax-dialog-btn" data-msg="您确定隐藏此分类吗" href="' . url('AdminCategory/toggle', ['ids' => $item['id'], 'hide' => 1]) . '">隐藏</a>';
+            } else {
+                $item['str_action'] .= '<a class="btn btn-xs btn-success js-ajax-dialog-btn" data-msg="您确定显示此分类吗" href="' . url('AdminCategory/toggle', ['ids' => $item['id'], 'display' => 1]) . '">显示</a>';
+            }
+            if ($item['description']) {
+                $item['description'] = '<span title=' . $item['description'] . '>' . mb_substr($item['description'], 0, 50) . "…</span>";
+            }
+            array_push($newCategories, $item);
+        }
+
+        $tree->init($newCategories);
+
+        if (empty($tpl)) {
+            $tpl = " <tr id='node-\$id' \$parent_id_node style='\$style' data-parent_id='\$parent_id' data-id='\$id'>
+                        <td style='padding-left:20px;'><input type='checkbox' class='js-check' data-yid='js-check-y' data-xid='js-check-x' name='ids[]' value='\$id' data-parent_id='\$parent_id' data-id='\$id'></td>
+                        <td><input name='list_orders[\$id]' type='text' size='3' value='\$list_order' class='input-order'></td>
+                        <td>\$id</td>
+                        <td>\$spacer <a href='\$url' target='_blank'>\$name</a></td>
+                        <td>\$description</td>
+                        <td>\$status_text</td>
+                        <td>\$str_action</td>
+                    </tr>";
+        }
+        $treeStr = $tree->getTree(0, $tpl);
+
+        return $treeStr;
+    }
+
+    /**
+     * 添加文章分类
+     * @param $data
+     * @return bool
+     */
+    public function addCategory($data)
+    {
+        $result = true;
+        self::startTrans();
+        try {
+            if (!empty($data['more']['thumbnail'])) {
+                $data['more']['thumbnail'] = cmf_asset_relative_url($data['more']['thumbnail']);
+            }
+            $this->save($data);
+            $id = $this->id;
+            if (empty($data['parent_id'])) {
+
+                $this->where('id', $id)->update(['path' => '0-' . $id]);
+            } else {
+                $parentPath = $this->where('id', intval($data['parent_id']))->value('path');
+                $this->where('id', $id)->update(['path' => "$parentPath-$id"]);
+
+            }
+            self::commit();
+        } catch (\Exception $e) {
+            self::rollback();
+            $result = false;
+        }
+
+        if ($result != false) {
+            //设置别名
+            $routeModel = new RouteModel();
+            if (!empty($data['alias']) && !empty($id)) {
+                $routeModel->setRoute($data['alias'], 'portal/List/index', ['id' => $id], 2, 5000);
+                $routeModel->setRoute($data['alias'] . '/:id', 'portal/Article/index', ['cid' => $id], 2, 4999);
+            }
+            $routeModel->getRoutes(true);
+        }
+
+        return $result;
+    }
+
+    public function editCategory($data)
+    {
+        $result = true;
+
+        $id          = intval($data['id']);
+        $parentId    = intval($data['parent_id']);
+        $oldCategory = $this->where('id', $id)->find();
+
+        if (empty($parentId)) {
+            $newPath = '0-' . $id;
+        } else {
+            $parentPath = $this->where('id', intval($data['parent_id']))->value('path');
+            if ($parentPath === false) {
+                $newPath = false;
+            } else {
+                $newPath = "$parentPath-$id";
+            }
+        }
+
+        if (empty($oldCategory) || empty($newPath)) {
+            $result = false;
+        } else {
+
+            $categoryAlias = $data['alias'];
+            unset($data['alias']);
+            $data['path'] = $newPath;
+            if (!empty($data['more']['thumbnail'])) {
+                $data['more']['thumbnail'] = cmf_asset_relative_url($data['more']['thumbnail']);
+            }
+            $category = $this->where('id', $id)->find();
+            $category->save($data);
+
+            $children = $this->field('id,path')->where('path', 'like', $oldCategory['path'] . "-%")->select();
+            if (!$children->isEmpty()) {
+                foreach ($children as $child) {
+                    $childPath = str_replace($oldCategory['path'] . '-', $newPath . '-', $child['path']);
+                    $this->where('id', $child['id'])->update(['path' => $childPath], ['id' => $child['id']]);
+                }
+            }
+
+            $routeModel = new RouteModel();
+            if (!empty($categoryAlias)) {
+                $routeModel->setRoute($categoryAlias, 'portal/List/index', ['id' => $data['id']], 2, 5000);
+                $routeModel->setRoute($categoryAlias . '/:id', 'portal/Article/index', ['cid' => $data['id']], 2, 4999);
+            } else {
+                $routeModel->deleteRoute('portal/List/index', ['id' => $data['id']]);
+                $routeModel->deleteRoute('portal/Article/index', ['cid' => $data['id']]);
+            }
+
+            $routeModel->getRoutes(true);
+        }
+
+
+        return $result;
+    }
+
+
+}

+ 23 - 0
app/portal/model/PortalCategoryPostModel.php

@@ -0,0 +1,23 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author:kane < chengjin005@163.com>
+// +----------------------------------------------------------------------
+namespace app\portal\model;
+
+use think\Model;
+
+class PortalCategoryPostModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'portal_category_post';
+
+}

+ 410 - 0
app/portal/model/PortalPostModel.php

@@ -0,0 +1,410 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// +----------------------------------------------------------------------
+namespace app\portal\model;
+
+use app\admin\model\RouteModel;
+use think\Model;
+
+/**
+ * @property mixed id
+ */
+class PortalPostModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'portal_post';
+
+    protected $type = [
+        'more' => 'array',
+    ];
+
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = true;
+
+    /**
+     * 关联 user表
+     * @return \think\model\relation\BelongsTo
+     */
+    public function user()
+    {
+        return $this->belongsTo('UserModel', 'user_id');
+    }
+
+    /**
+     * 关联分类表
+     * @return \think\model\relation\BelongsToMany
+     */
+    public function categories()
+    {
+        return $this->belongsToMany('PortalCategoryModel', 'portal_category_post', 'category_id', 'post_id');
+    }
+
+    /**
+     * 关联标签表
+     * @return \think\model\relation\BelongsToMany
+     */
+    public function tags()
+    {
+        return $this->belongsToMany('PortalTagModel', 'portal_tag_post', 'tag_id', 'post_id');
+    }
+
+    /**
+     * post_content 自动转化
+     * @param $value
+     * @return string
+     */
+    public function getPostContentAttr($value)
+    {
+        return cmf_replace_content_file_url(htmlspecialchars_decode($value));
+    }
+
+    /**
+     * post_content 自动转化
+     * @param $value
+     * @return string
+     */
+    public function setPostContentAttr($value)
+    {
+        return htmlspecialchars(cmf_replace_content_file_url(htmlspecialchars_decode($value), true));
+    }
+
+    /**
+     * published_time 自动完成
+     * @param $value
+     * @return false|int
+     */
+    public function setPublishedTimeAttr($value)
+    {
+        return strtotime($value);
+    }
+
+    /**
+     * 后台管理添加文章
+     * @param array        $data       文章数据
+     * @param array|string $categories 文章分类 id
+     * @return $this
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     * @throws \think\exception\PDOException
+     */
+    public function adminAddArticle($data, $categories)
+    {
+        $data['user_id'] = cmf_get_current_admin_id();
+
+        if (!empty($data['more']['thumbnail'])) {
+            $data['more']['thumbnail'] = cmf_asset_relative_url($data['more']['thumbnail']);
+            $data['thumbnail']         = $data['more']['thumbnail'];
+        }
+
+        if (!empty($data['more']['audio'])) {
+            $data['more']['audio'] = cmf_asset_relative_url($data['more']['audio']);
+        }
+
+        if (!empty($data['more']['video'])) {
+            $data['more']['video'] = cmf_asset_relative_url($data['more']['video']);
+        }
+
+        $this->save($data);
+
+        if (is_string($categories)) {
+            $categories = explode(',', $categories);
+        }
+
+        $this->categories()->save($categories);
+
+        $data['post_keywords'] = str_replace(',', ',', $data['post_keywords']);
+
+        $keywords = explode(',', $data['post_keywords']);
+
+        $this->addTags($keywords, $this->id);
+
+        return $this;
+
+    }
+
+    /**
+     * 后台管理编辑文章
+     * @param array        $data       文章数据
+     * @param array|string $categories 文章分类 id
+     * @return $this
+     * @throws \think\Exception
+     */
+    public function adminEditArticle($data, $categories)
+    {
+
+        unset($data['user_id']);
+
+        if (!empty($data['more']['thumbnail'])) {
+            $data['more']['thumbnail'] = cmf_asset_relative_url($data['more']['thumbnail']);
+            $data['thumbnail']         = $data['more']['thumbnail'];
+        }
+
+        if (!empty($data['more']['audio'])) {
+            $data['more']['audio'] = cmf_asset_relative_url($data['more']['audio']);
+        }
+
+        if (!empty($data['more']['video'])) {
+            $data['more']['video'] = cmf_asset_relative_url($data['more']['video']);
+        }
+
+        unset($data['categories']);
+
+        $article = self::find($data['id']);
+
+        $article->save($data);
+
+        if (is_string($categories)) {
+            $categories = explode(',', $categories);
+        }
+
+        $oldCategoryIds        = $article->categories()->column('category_id');
+        $sameCategoryIds       = array_intersect($categories, $oldCategoryIds);
+        $needDeleteCategoryIds = array_diff($oldCategoryIds, $sameCategoryIds);
+        $newCategoryIds        = array_diff($categories, $sameCategoryIds);
+
+        if (!empty($needDeleteCategoryIds)) {
+            $article->categories()->detach($needDeleteCategoryIds);
+        }
+
+        if (!empty($newCategoryIds)) {
+            $article->categories()->attach(array_values($newCategoryIds));
+        }
+
+
+        $data['post_keywords'] = str_replace(',', ',', $data['post_keywords']);
+
+        $keywords = explode(',', $data['post_keywords']);
+
+        $this->addTags($keywords, $data['id']);
+
+        return $this;
+
+    }
+
+    /**
+     * 增加标签
+     * @param $keywords
+     * @param $articleId
+     * @throws \think\Exception
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     * @throws \think\exception\PDOException
+     */
+    public function addTags($keywords, $articleId)
+    {
+        $portalTagModel = new PortalTagModel();
+
+        $tagIds = [];
+
+        $data = [];
+
+        if (!empty($keywords)) {
+
+            $oldTagIds = PortalTagPostModel::where('post_id', $articleId)->column('tag_id');
+
+            foreach ($keywords as $keyword) {
+                $keyword = trim($keyword);
+                if (!empty($keyword)) {
+                    $findTag = $portalTagModel->where('name', $keyword)->find();
+                    if (empty($findTag)) {
+                        $tagId = $portalTagModel->insertGetId([
+                            'name' => $keyword
+                        ]);
+                    } else {
+                        $tagId = $findTag['id'];
+                    }
+
+                    if (!in_array($tagId, $oldTagIds)) {
+                        array_push($data, ['tag_id' => $tagId, 'post_id' => $articleId]);
+                    }
+
+                    array_push($tagIds, $tagId);
+
+                }
+            }
+
+
+            if (empty($tagIds) && !empty($oldTagIds)) {
+                PortalTagPostModel::where('post_id', $articleId)->delete();
+            }
+
+            $sameTagIds = array_intersect($oldTagIds, $tagIds);
+
+            $shouldDeleteTagIds = array_diff($oldTagIds, $sameTagIds);
+
+            if (!empty($shouldDeleteTagIds)) {
+                PortalTagPostModel::where('post_id', $articleId)
+                    ->where('tag_id', 'in', $shouldDeleteTagIds)
+                    ->delete();
+            }
+
+            if (!empty($data)) {
+                PortalTagPostModel::insertAll($data);
+            }
+
+
+        } else {
+            PortalTagPostModel::where('post_id', $articleId)->delete();
+        }
+    }
+
+    /**
+     * @param $data
+     * @return bool
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function adminDeletePage($data)
+    {
+
+        if (isset($data['id'])) {
+            $id = $data['id']; //获取删除id
+
+            $res = $this->where('id', $id)->find();
+
+            if ($res) {
+                $res = json_decode(json_encode($res), true); //转换为数组
+
+                $recycleData = [
+                    'object_id'   => $res['id'],
+                    'create_time' => time(),
+                    'table_name'  => 'portal_post#page',
+                    'name'        => $res['post_title'],
+
+                ];
+
+                PortalPostModel::startTrans(); //开启事务
+                $transStatus = false;
+                try {
+                    PortalPostModel::where('id', $id)->update([
+                        'delete_time' => time()
+                    ]);
+                    RecycleBinModel::insert($recycleData);
+
+                    $transStatus = true;
+                    // 提交事务
+                    PortalPostModel::commit();
+                } catch (\Exception $e) {
+
+                    // 回滚事务
+                    PortalPostModel::rollback();
+                }
+                return $transStatus;
+
+
+            } else {
+                return false;
+            }
+        } elseif (isset($data['ids'])) {
+            $ids = $data['ids'];
+
+            $res = $this->where('id', 'in', $ids)
+                ->select();
+
+            if ($res) {
+                $res = json_decode(json_encode($res), true);
+                foreach ($res as $key => $value) {
+                    $recycleData[$key]['object_id']   = $value['id'];
+                    $recycleData[$key]['create_time'] = time();
+                    $recycleData[$key]['table_name']  = 'portal_post';
+                    $recycleData[$key]['name']        = $value['post_title'];
+
+                }
+
+                PortalPostModel::startTrans(); //开启事务
+                $transStatus = false;
+                try {
+                    PortalPostModel::where('id', 'in', $ids)
+                        ->update([
+                            'delete_time' => time()
+                        ]);
+
+
+                    RecycleBinModel::insertAll($recycleData);
+
+                    $transStatus = true;
+                    // 提交事务
+                    PortalPostModel::commit();
+
+                } catch (\Exception $e) {
+
+                    // 回滚事务
+                    PortalPostModel::rollback();
+
+
+                }
+                return $transStatus;
+
+
+            } else {
+                return false;
+            }
+
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 后台管理添加页面
+     * @param array $data 页面数据
+     * @return $this
+     */
+    public function adminAddPage($data)
+    {
+        $data['user_id'] = cmf_get_current_admin_id();
+
+        if (!empty($data['more']['thumbnail'])) {
+            $data['more']['thumbnail'] = cmf_asset_relative_url($data['more']['thumbnail']);
+        }
+
+        $data['post_status'] = empty($data['post_status']) ? 0 : 1;
+        $data['post_type']   = 2;
+        $this->save($data);
+
+        return $this;
+
+    }
+
+    /**
+     * 后台管理编辑页面
+     * @param array $data 页面数据
+     * @return $this
+     */
+    public function adminEditPage($data)
+    {
+        $data['user_id'] = cmf_get_current_admin_id();
+
+        if (!empty($data['more']['thumbnail'])) {
+            $data['more']['thumbnail'] = cmf_asset_relative_url($data['more']['thumbnail']);
+        }
+
+        $data['post_status'] = empty($data['post_status']) ? 0 : 1;
+        $data['post_type']   = 2;
+
+        $thisPage = PortalPostModel::find($data['id']);
+        $thisPage->save($data);
+
+        $routeModel = new RouteModel();
+        $routeUrl   = $data['post_alias'] ? trim($data['post_alias'], '$') . '$' : '';
+        $routeModel->setRoute($routeUrl, 'portal/Page/index', ['id' => $data['id']], 2, 5000);
+
+        $routeModel->getRoutes(true);
+        return $this;
+    }
+
+}

+ 27 - 0
app/portal/model/PortalTagModel.php

@@ -0,0 +1,27 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author:kane < chengjin005@163.com>
+// +----------------------------------------------------------------------
+namespace app\portal\model;
+
+use think\Model;
+
+class PortalTagModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'portal_tag';
+
+    public static   $STATUS = array(
+        0=>"未启用",
+        1=>"已启用",
+    );
+}

+ 23 - 0
app/portal/model/PortalTagPostModel.php

@@ -0,0 +1,23 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author:kane < chengjin005@163.com>
+// +----------------------------------------------------------------------
+namespace app\portal\model;
+
+use think\Model;
+
+class PortalTagPostModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'portal_tag_post';
+
+}

+ 23 - 0
app/portal/model/RecycleBinModel.php

@@ -0,0 +1,23 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author:kane < chengjin005@163.com>
+// +----------------------------------------------------------------------
+namespace app\portal\model;
+
+use think\Model;
+
+class RecycleBinModel extends Model
+{
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'recycle_bin';
+
+}

+ 29 - 0
app/portal/model/UserModel.php

@@ -0,0 +1,29 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\model;
+
+use think\Model;
+
+class UserModel extends Model
+{
+
+    /**
+     * 模型名称
+     * @var string
+     */
+    protected $name = 'user';
+
+    protected $type = [
+        'more' => 'array',
+    ];
+
+
+}

+ 14 - 0
app/portal/nav.php

@@ -0,0 +1,14 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+return [
+    ['name' => '文章分类', 'api' => "Category/nav"],
+    ['name' => '所有页面', 'api' => "Page/nav"]
+];

+ 501 - 0
app/portal/service/ApiService.php

@@ -0,0 +1,501 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\service;
+
+use app\portal\model\PortalPostModel;
+use app\portal\model\PortalCategoryModel;
+use think\db\Query;
+use app\portal\model\PortalTagModel;
+
+class ApiService
+{
+    /**
+     * 功能:查询文章列表,支持分页;<br>
+     * 注:此方法查询时关联两个表portal_category_post(category_post),portal_post(post);在指定排序(order),指定查询条件(where)最好指定一下表别名
+     * @param array $param 查询参数<pre>
+     *                     array(
+     *                     'category_ids'=>'',
+     *                     'where'=>'',
+     *                     'limit'=>'',
+     *                     'order'=>'',
+     *                     'page'=>'',
+     *                     'relation'=>''
+     *                     )
+     *                     字段说明:
+     *                     category_ids:文章所在分类,可指定一个或多个分类id,以英文逗号分隔,如1或1,2,3 默认值为全部
+     *                     field:调用指定的字段@return array 包括分页的文章列表<pre>
+     *                     格式:
+     *                     array(
+     *                     "articles"=>array(),//文章列表,array
+     *                     "page"=>"",//生成的分页html,不分页则没有此项
+     *                     "total"=>100, //符合条件的文章总数,不分页则没有此项
+     *                     "total_pages"=>5 // 总页数,不分页则没有此项
+     *                     )</pre>
+     * @todo
+     *                     如只调用posts表里的id和post_title字段可以是post.id,post.post_title; 默认全部,
+     *                     此方法查询时关联两个表portal_category_post(category_post),portal_post(post);
+     *                     所以最好指定一下表名,以防字段冲突
+     *                     limit:数据条数,默认值为10,可以指定从第几条开始,如3,8(表示共调用8条,从第3条开始)
+     *                     order:排序方式,如按posts表里的published_time字段倒序排列:post.published_time desc
+     *                     where:查询条件,字符串形式,和sql语句一样,请在事先做好安全过滤,最好使用第二个参数$where的数组形式进行过滤,此方法查询时关联多个表,所以最好指定一下表名,以防字段冲突,查询条件(只支持数组),格式和thinkPHP的where方法一样,此方法查询时关联多个表,所以最好指定一下表名,以防字段冲突;
+     *                     </pre>
+     */
+    public static function articles($param)
+    {
+        $portalPostModel = new PortalPostModel();
+
+        $where = [
+            'post.post_status' => 1,
+            'post.post_type'   => 1,
+            'post.delete_time' => 0
+        ];
+
+        $wherePublishedTime = function (Query $query) {
+            $query->where('post.published_time', '>', 0)
+                ->where('post.published_time', '<', time());
+        };
+
+        $paramWhere = empty($param['where']) ? '' : $param['where'];
+
+        $limit       = empty($param['limit']) ? 10 : $param['limit'];
+        $order       = empty($param['order']) ? '' : $param['order'];
+        $page        = isset($param['page']) ? $param['page'] : false;
+        $relation    = empty($param['relation']) ? '' : $param['relation'];
+        $categoryIds = empty($param['category_ids']) ? '' : $param['category_ids'];
+
+        $whereCategoryId = null;
+
+        if (!empty($categoryIds)) {
+
+            $field = !empty($param['field']) ? $param['field'] : 'post.*,min(category_post.category_id) as category_id';
+
+            if (!is_array($categoryIds)) {
+                $categoryIds = explode(',', $categoryIds);
+            }
+
+            if (count($categoryIds) == 1) {
+                $whereCategoryId = function (Query $query) use ($categoryIds) {
+                    $query->where('category_post.category_id', $categoryIds[0]);
+                };
+            } else {
+                $whereCategoryId = function (Query $query) use ($categoryIds) {
+                    $query->where('category_post.category_id', 'in', $categoryIds);
+                };
+            }
+        } else {
+
+            $field = !empty($param['field']) ? $param['field'] : 'post.*,min(category_post.category_id) as category_id';
+
+        }
+
+        $articles = $portalPostModel->alias('post')->field($field)
+            ->join('portal_category_post category_post', 'post.id = category_post.post_id')
+            ->where($where)
+            ->where($paramWhere)
+            ->where($whereCategoryId)
+            ->where($wherePublishedTime)
+            ->order($order)
+            ->group('post.id');
+
+        $return = [];
+
+        if (empty($page)) {
+            $length = null;
+            if (strpos($limit, ',')) {
+                list($offset, $length) = explode(',', $limit);
+            } else {
+                $offset = $limit;
+            }
+
+            $articles = $articles->limit($offset, $length)->select();
+
+            if (!empty($relation) && !empty($articles['items'])) {
+                $articles->load($relation);
+            }
+
+            $return['articles'] = $articles;
+        } else {
+
+            if (is_array($page)) {
+                if (empty($page['list_rows'])) {
+                    $page['list_rows'] = 10;
+                }
+
+                $articles = $articles->paginate($page);
+            } else {
+                $articles = $articles->paginate(intval($page));
+            }
+
+            if (!empty($relation) && !empty($articles['items'])) {
+                $articles->load($relation);
+            }
+
+            $articles->appends(request()->get());
+            $articles->appends(request()->post());
+
+            $return['articles']    = $articles->items();
+            $return['page']        = $articles->render();
+            $return['total']       = $articles->total();
+            $return['total_pages'] = $articles->lastPage();
+        }
+
+
+        return $return;
+
+    }
+
+    /**
+     * 功能:查询标签文章列表,支持分页;<br>
+     * 注:此方法查询时关联两个表portal_tag_post(tag_post),portal_post(post);在指定排序(order),指定查询条件(where)最好指定一下表别名
+     * @param array $param 查询参数<pre>
+     *                     array(
+     *                     'tag_id'=>'',
+     *                     'where'=>'',
+     *                     'limit'=>'',
+     *                     'order'=>'',
+     *                     'page'=>'',
+     *                     'relation'=>''
+     *                     )
+     *                     字段说明:
+     *                     field:调用指定的字段@return array 包括分页的文章列表<pre>
+     *                     格式:
+     *                     array(
+     *                     "articles"=>array(),//文章列表,array
+     *                     "page"=>"",//生成的分页html,不分页则没有此项
+     *                     "total"=>100, //符合条件的文章总数,不分页则没有此项
+     *                     "total_pages"=>5 // 总页数,不分页则没有此项
+     *                     )</pre>
+     * @todo
+     *                     如只调用posts表里的id和post_title字段可以是post.id,post.post_title; 默认全部,
+     *                     此方法查询时关联两个表portal_tag_post(category_post),portal_post(post);
+     *                     所以最好指定一下表名,以防字段冲突
+     *                     limit:数据条数,默认值为10,可以指定从第几条开始,如3,8(表示共调用8条,从第3条开始)
+     *                     order:排序方式,如按posts表里的published_time字段倒序排列:post.published_time desc
+     *                     where:查询条件,字符串形式,和sql语句一样,请在事先做好安全过滤,最好使用第二个参数$where的数组形式进行过滤,此方法查询时关联多个表,所以最好指定一下表名,以防字段冲突,查询条件(只支持数组),格式和thinkPHP的where方法一样,此方法查询时关联多个表,所以最好指定一下表名,以防字段冲突;
+     *                     </pre>
+     */
+    public static function tagArticles($param)
+    {
+        
+        $portalPostModel = new PortalPostModel();
+
+        $where = [
+            ['post.post_status' ,'=', 1],
+            ['post.post_type'   ,'=', 1],
+            ['post.delete_time' ,'=', 0]
+        ];
+
+        $paramWhere = empty($param['where']) ? '' : $param['where'];
+
+        $limit    = empty($param['limit']) ? 10 : $param['limit'];
+        $order    = empty($param['order']) ? '' : $param['order'];
+        $page     = isset($param['page']) ? $param['page'] : false;
+        $relation = empty($param['relation']) ? '' : $param['relation'];
+        $tagId    = empty($param['tag_id']) ? '' : $param['tag_id'];
+
+        $articles = $portalPostModel->alias('post');
+        if (empty($tagId)) {
+            return null;
+
+        } else {
+            $field    = !empty($param['field']) ? $param['field'] : 'post.*';
+            $articles = $articles->join('portal_tag_post tag_post', 'post.id = tag_post.post_id');
+            $where[]  = ['tag_post.tag_id', '=', $tagId];
+        }
+
+        $wherePublishedTime = function (Query $query) {
+            $query->where('post.published_time', '>', 0)
+                ->where('post.published_time', '<', time());
+        };
+
+        $articles = $articles->field($field)
+            ->where($where)
+            ->where($paramWhere)
+            ->where($wherePublishedTime)
+            ->order($order);
+
+        $return = [];
+
+        if (empty($page)) {
+            $articles = $articles->limit($limit)->select();
+            if (!empty($relation) && !empty($articles['items'])) {
+                $articles->load($relation);
+            }
+
+            $return['articles'] = $articles;
+        } else {
+
+            if (is_array($page)) {
+                if (empty($page['list_rows'])) {
+                    $page['list_rows'] = 10;
+                }
+
+                $articles = $articles->paginate($page);
+            } else {
+                $articles = $articles->paginate(intval($page));
+            }
+
+            if (!empty($relation) && !empty($articles->items())) {
+                $articles->load($relation);
+            }
+
+            $articles->appends(request()->get());
+            $articles->appends(request()->post());
+
+            $return['articles']    = $articles->items();
+            $return['page']        = $articles->render();
+            $return['total']       = $articles->total();
+            $return['total_pages'] = $articles->lastPage();
+        }
+
+        return $return;
+    }
+
+    /**
+     * 获取指定id的文章
+     * @param int $id
+     * @return array|false|\PDOStatement|string|\think\Model
+     */
+    public static function article($id)
+    {
+        $portalPostModel = new PortalPostModel();
+
+        $where = [
+            'post_status' => 1,
+            'post_type'   => 1,
+            'id'          => $id,
+            'delete_time' => 0
+        ];
+
+        $wherePublishedTime = function (Query $query) {
+            $query->where('published_time', '>', 0)
+                ->where('published_time', '<', time());
+        };
+
+        return $portalPostModel->where($where)
+            ->where($wherePublishedTime)
+            ->find();
+    }
+
+    /**
+     * 获取指定条件的页面列表
+     * @param array $param 查询参数<pre>
+     *                     array(
+     *                     'where'=>'',
+     *                     'order'=>'',
+     *                     )</pre>
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function pages($param)
+    {
+        $paramWhere = empty($param['where']) ? '' : $param['where'];
+
+        $order = empty($param['order']) ? '' : $param['order'];
+
+        $portalPostModel = new PortalPostModel();
+
+        $where = [
+            'post_status' => 1,
+            'post_type'   => 2, //页面
+            'delete_time' => 0
+        ];
+
+        $wherePublishedTime = function (Query $query) {
+            $query->where('published_time', '>', 0)
+                ->where('published_time', '<', time());
+        };
+
+        return $portalPostModel
+            ->where($where)
+            ->where($paramWhere)
+            ->where($wherePublishedTime)
+            ->order($order)
+            ->select();
+    }
+
+    /**
+     * 获取指定id的页面
+     * @param int $id 页面的id
+     * @return array|false|\PDOStatement|string|\think\Model 返回符合条件的页面
+     */
+    public static function page($id)
+    {
+        $portalPostModel = new PortalPostModel();
+
+        $where = [
+            'post_status' => 1,
+            'post_type'   => 2,
+            'id'          => $id,
+            'delete_time' => 0
+        ];
+
+        $wherePublishedTime = function (Query $query) {
+            $query->where('published_time', '>', 0)
+                ->where('published_time', '<', time());
+        };
+
+        return $portalPostModel->where($where)
+            ->where($wherePublishedTime)
+            ->find();
+    }
+
+    /**
+     * 返回指定分类
+     * @param int $id 分类id
+     * @return array 返回符合条件的分类
+     */
+    public static function category($id)
+    {
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $where = [
+            'status'      => 1,
+            'delete_time' => 0,
+            'id'          => $id
+        ];
+
+        return $portalCategoryModel->where($where)->find();
+    }
+
+    /**
+     * 返回指定分类下的子分类
+     * @param int $categoryId 分类id
+     * @param     $field      string  指定查询字段
+     * @return false|\PDOStatement|string|\think\Collection 返回指定分类下的子分类
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     * @throws \think\db\exception\DataNotFoundException
+     */
+    public static function subCategories($categoryId, $field = '*')
+    {
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $where = [
+            'status'      => 1,
+            'delete_time' => 0,
+            'parent_id'   => $categoryId
+        ];
+
+        return $portalCategoryModel->field($field)->where($where)->order('list_order ASC')->select();
+    }
+
+    /**
+     * 返回指定分类下的所有子分类
+     * @param int $categoryId 分类id
+     * @return array 返回指定分类下的所有子分类
+     */
+    public static function allSubCategories($categoryId)
+    {
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $categoryId = intval($categoryId);
+
+        if ($categoryId !== 0) {
+            $category = $portalCategoryModel->field('path')->where('id', $categoryId)->find();
+
+            if (empty($category)) {
+                return [];
+            }
+
+            $categoryPath = $category['path'];
+        } else {
+            $categoryPath = 0;
+        }
+
+        $where = [
+            'status'      => 1,
+            'delete_time' => 0
+        ];
+
+        return $portalCategoryModel->where($where)->whereLike('path', "$categoryPath-%")->select();
+    }
+
+    /**
+     * 返回符合条件的所有分类
+     * @param array $param 查询参数<pre>
+     *                     array(
+     *                     'where'=>'',
+     *                     'order'=>'',
+     *                     )</pre>
+     * @return false|\PDOStatement|string|\think\Collection
+     */
+    public static function categories($param)
+    {
+        $paramWhere = empty($param['where']) ? '' : $param['where'];
+
+        $order = empty($param['order']) ? '' : $param['order'];
+
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $where = [
+            'status'      => 1,
+            'delete_time' => 0,
+        ];
+
+        $temp = $portalCategoryModel
+            ->where($where)
+            ->where($paramWhere)
+            ->order($order);
+
+        if (!empty($param['ids'])) {
+            $temp->whereIn('id', $param['ids']);
+        }
+        return $temp->select();
+    }
+
+    /**
+     * 获取面包屑数据
+     * @param int     $categoryId  当前文章所在分类,或者当前分类的id
+     * @param boolean $withCurrent 是否获取当前分类
+     * @return array 面包屑数据
+     */
+    public static function breadcrumb($categoryId, $withCurrent = false)
+    {
+        $data                = [];
+        $portalCategoryModel = new PortalCategoryModel();
+
+        $path = $portalCategoryModel->where(['id' => $categoryId])->value('path');
+
+        if (!empty($path)) {
+            $parents = explode('-', $path);
+            if (!$withCurrent) {
+                array_pop($parents);
+            }
+
+            if (!empty($parents)) {
+                $data = $portalCategoryModel->where('id', 'in', $parents)->order('path ASC')->select();
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * 返回指定文章的标签
+     * @param int $id 文章ID
+     * @return array 返回符合条件的所有标签
+     */
+    public static function tags($id)
+    {
+        $portalTagModel = new PortalTagModel();
+
+        $where = [
+            'tags.status'      => 1,
+            'tag_post.post_id' => $id
+        ];
+
+        return $portalTagModel->alias('tags')
+            ->where($where)
+            ->join('portal_tag_post tag_post', 'tags.id = tag_post.tag_id')
+            ->select();
+    }
+
+}

+ 297 - 0
app/portal/service/PostService.php

@@ -0,0 +1,297 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// +----------------------------------------------------------------------
+namespace app\portal\service;
+
+use app\portal\model\PortalPostModel;
+use think\db\Query;
+
+class PostService
+{
+    /**
+     * 文章查询
+     * @param $filter
+     * @return \think\Paginator
+     * @throws \think\exception\DbException
+     */
+    public function adminArticleList($filter)
+    {
+        return $this->adminPostList($filter);
+    }
+
+    /**
+     * 页面文章列表
+     * @param $filter
+     * @return \think\Paginator
+     * @throws \think\exception\DbException
+     */
+    public function adminPageList($filter)
+    {
+        return $this->adminPostList($filter, true);
+    }
+
+    /**
+     * 文章查询
+     * @param      $filter
+     * @param bool $isPage
+     * @return \think\Paginator
+     * @throws \think\exception\DbException
+     */
+    public function adminPostList($filter, $isPage = false)
+    {
+
+        $field = 'a.*,u.user_login,u.user_nickname,u.user_email';
+
+        $portalPostModel = new PortalPostModel();
+        $articlesQuery   = $portalPostModel->alias('a');
+        $articlesQuery->join('user u', 'a.user_id = u.id');
+
+        $category = empty($filter['category']) ? 0 : intval($filter['category']);
+        if (!empty($category)) {
+            $articlesQuery->join('portal_category_post b', 'a.id = b.post_id');
+            $field = 'a.*,b.id AS post_category_id,b.list_order,b.category_id,u.user_login,u.user_nickname,u.user_email';
+        }
+
+        $articles = $articlesQuery->field($field)
+            ->where('a.create_time', '>=', 0)
+            ->where('a.delete_time', 0)
+            ->where(function (Query $query) use ($filter, $isPage) {
+
+                $category = empty($filter['category']) ? 0 : intval($filter['category']);
+                if (!empty($category)) {
+                    $query->where('b.category_id', $category);
+                }
+
+                $startTime = empty($filter['start_time']) ? 0 : strtotime($filter['start_time']);
+                $endTime   = empty($filter['end_time']) ? 0 : strtotime($filter['end_time']);
+                if (!empty($startTime)) {
+                    $query->where('a.published_time', '>=', $startTime);
+                }
+                if (!empty($endTime)) {
+                    $query->where('a.published_time', '<=', $endTime);
+                }
+
+                $keyword = empty($filter['keyword']) ? '' : $filter['keyword'];
+                if (!empty($keyword)) {
+                    $query->where('a.post_title', 'like', "%$keyword%");
+                }
+
+                if ($isPage) {
+                    $query->where('a.post_type', 2);
+                } else {
+                    $query->where('a.post_type', 1);
+                }
+            })
+            ->order('update_time', 'DESC')
+            ->paginate(10);
+
+        return $articles;
+
+    }
+
+    /**
+     * 已发布文章查询
+     * @param int $postId     文章id
+     * @param int $categoryId 分类id
+     * @return array|string|\think\Model|null
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function publishedArticle($postId, $categoryId = 0)
+    {
+        $portalPostModel = new PortalPostModel();
+
+        $wherePublishedTime = function (Query $query) {
+            $query->where('post.published_time', '>', 0)
+                ->where('post.published_time', '<', time());
+        };
+
+        if (empty($categoryId)) {
+
+            $where = [
+                'post.post_type'   => 1,
+                'post.post_status' => 1,
+                'post.delete_time' => 0,
+                'post.id'          => $postId
+            ];
+
+            $article = $portalPostModel->alias('post')->field('post.*')
+                ->where($where)
+                ->where($wherePublishedTime)
+                ->find();
+        } else {
+            $where = [
+                'post.post_type'       => 1,
+                'post.post_status'     => 1,
+                'post.delete_time'     => 0,
+                'relation.category_id' => $categoryId,
+                'relation.post_id'     => $postId
+            ];
+
+            $article = $portalPostModel->alias('post')->field('post.*')
+                ->join('portal_category_post relation', 'post.id = relation.post_id')
+                ->where($where)
+                ->where($wherePublishedTime)
+                ->find();
+        }
+
+
+        return $article;
+    }
+
+    /**
+     * 上一篇文章
+     * @param int $postId     文章id
+     * @param int $categoryId 分类id
+     * @return array|string|\think\Model|null
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function publishedPrevArticle($postId, $categoryId = 0)
+    {
+        $portalPostModel = new PortalPostModel();
+
+        $wherePublishedTime = function (Query $query) {
+            $query->where('post.published_time', '>', 0)
+                ->where('post.published_time', '<', time());
+        };
+
+        if (empty($categoryId)) {
+
+            $where = [
+                'post.post_type'   => 1,
+                'post.post_status' => 1,
+                'post.delete_time' => 0,
+            ];
+
+            $article = $portalPostModel
+                ->alias('post')
+                ->field('post.*')
+                ->where($where)
+                ->where('post.id', '<', $postId)
+                ->where($wherePublishedTime)
+                ->order('id', 'DESC')
+                ->find();
+
+        } else {
+            $where = [
+                'post.post_type'       => 1,
+                'post.post_status'     => 1,
+                'post.delete_time'     => 0,
+                'relation.category_id' => $categoryId,
+            ];
+
+            $article = $portalPostModel
+                ->alias('post')
+                ->field('post.*')
+                ->join('portal_category_post relation', 'post.id = relation.post_id')
+                ->where($where)
+                ->where('relation.post_id', '<', $postId)
+                ->where($wherePublishedTime)
+                ->order('id', 'DESC')
+                ->find();
+        }
+
+
+        return $article;
+    }
+
+    /**
+     * 下一篇文章
+     * @param int $postId     文章id
+     * @param int $categoryId 分类id
+     * @return array|string|\think\Model|null
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function publishedNextArticle($postId, $categoryId = 0)
+    {
+        $portalPostModel = new PortalPostModel();
+
+        $wherePublishedTime = function (Query $query) {
+            $query->where('post.published_time', '>', 0)
+                ->where('post.published_time', '<', time());
+        };
+
+        if (empty($categoryId)) {
+
+            $where = [
+                'post.post_type'   => 1,
+                'post.post_status' => 1,
+                'post.delete_time' => 0,
+            ];
+
+            $article = $portalPostModel->alias('post')->field('post.*')
+                ->where($where)
+                ->where('post.id', '>', $postId)
+                ->where($wherePublishedTime)
+                ->order('id', 'ASC')
+                ->find();
+        } else {
+
+
+
+            $where = [
+                'post.post_type'       => 1,
+                'post.post_status'     => 1,
+                'post.delete_time'     => 0,
+                'relation.category_id' => $categoryId,
+
+            ];
+
+            $article = $portalPostModel->alias('post')->field('post.*')
+                ->join('portal_category_post relation', 'post.id = relation.post_id')
+                ->where($where)
+                ->where('relation.post_id', '>', $postId)
+                ->where($wherePublishedTime)
+                ->order('id', 'ASC')
+                ->find();
+        }
+
+
+        return $article;
+    }
+
+    /**
+     * 页面管理查询
+     * @param int $pageId 文章id
+     * @return array|string|\think\Model|null
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\ModelNotFoundException
+     * @throws \think\exception\DbException
+     */
+    public function publishedPage($pageId)
+    {
+
+        $where = [
+            'post_type'   => 2,
+            'post_status' => 1,
+            'delete_time' => 0,
+            'id'          => $pageId
+        ];
+
+        $wherePublishedTime = function (Query $query) {
+            $query->where('published_time', '>', 0)
+                ->where('published_time', '<', time());
+        };
+
+        $portalPostModel = new PortalPostModel();
+        $page            = $portalPostModel
+            ->where($where)
+            ->where($wherePublishedTime)
+            ->find();
+
+        return $page;
+    }
+
+}

+ 372 - 0
app/portal/taglib/Portal.php

@@ -0,0 +1,372 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+namespace app\portal\taglib;
+
+use think\template\TagLib;
+
+class Portal extends TagLib
+{
+    /**
+     * 定义标签列表
+     */
+    protected $tags = [
+        // 标签定义: attr 属性列表 close 是否闭合(0 或者1 默认1) alias 标签别名 level 嵌套层次
+        'articles'         => ['attr' => 'field,where,limit,order,page,relation,returnVarName,pageVarName,categoryIds', 'close' => 1],//非必须属性item
+        'tagarticles'      => ['attr' => 'field,where,limit,order,page,relation,returnVarName,pageVarName,tagId', 'close' => 1],//非必须属性item
+        'page'             => ['attr' => 'id', 'close' => 1],//非必须属性item
+        'breadcrumb'       => ['attr' => 'cid', 'close' => 1],//非必须属性self
+        'categories'       => ['attr' => 'ids,where,order', 'close' => 1],//非必须属性item
+        'category'         => ['attr' => 'id', 'close' => 1],//非必须属性item
+        'subcategories'    => ['attr' => 'categoryId', 'close' => 1],//非必须属性item
+        'allsubcategories' => ['attr' => 'categoryId', 'close' => 1],//非必须属性item
+    ];
+
+    /**
+     * 文章列表标签
+     */
+    public function tagArticles($tag, $content)
+    {
+        $item          = empty($tag['item']) ? 'vo' : $tag['item'];//循环变量名
+        $order         = empty($tag['order']) ? 'post.published_time DESC' : $tag['order'];
+        $relation      = empty($tag['relation']) ? '' : $tag['relation'];
+        $pageVarName   = empty($tag['pageVarName']) ? '__PAGE_VAR_NAME__' : $tag['pageVarName'];
+        $returnVarName = empty($tag['returnVarName']) ? 'articles_data' : $tag['returnVarName'];
+
+        $field = "''";
+        if (!empty($tag['field'])) {
+            if (strpos($tag['field'], '$') === 0) {
+                $field = $tag['field'];
+                $this->autoBuildVar($field);
+            } else {
+                $field = "'{$tag['field']}'";
+            }
+        }
+
+        $where = '""';
+        if (!empty($tag['where']) && strpos($tag['where'], '$') === 0) {
+            $where = $tag['where'];
+        }
+
+        $limit = "''";
+        if (!empty($tag['limit'])) {
+            if (strpos($tag['limit'], '$') === 0) {
+                $limit = $tag['limit'];
+                $this->autoBuildVar($limit);
+            } else {
+                $limit = "'{$tag['limit']}'";
+            }
+        }
+
+        $page = "''";
+        if (!empty($tag['page'])) {
+            if (strpos($tag['page'], '$') === 0) {
+                $page = $tag['page'];
+                $this->autoBuildVar($page);
+            } else {
+                $page = intval($tag['page']);
+                $page = "'{$page}'";
+            }
+        }
+
+        $categoryIds = "''";
+        if (!empty($tag['categoryIds'])) {
+            if (strpos($tag['categoryIds'], '$') === 0) {
+                $categoryIds = $tag['categoryIds'];
+                $this->autoBuildVar($categoryIds);
+            } else {
+                $categoryIds = "'{$tag['categoryIds']}'";
+            }
+        }
+
+        if (!empty($order) && strpos($order, '$') === 0) {
+            $this->autoBuildVar($order);
+        } else {
+            $order = "'{$order}'";
+        }
+
+        $parse = <<<parse
+<?php
+\${$returnVarName} = \app\portal\service\ApiService::articles([
+    'field'   => {$field},
+    'where'   => {$where},
+    'limit'   => {$limit},
+    'order'   => {$order},
+    'page'    => {$page},
+    'relation'=> '{$relation}',
+    'category_ids'=>{$categoryIds}
+]);
+
+\${$pageVarName} = isset(\${$returnVarName}['page'])?\${$returnVarName}['page']:'';
+
+ ?>
+<volist name="{$returnVarName}.articles" id="{$item}">
+{$content}
+</volist>
+parse;
+        return $parse;
+    }
+
+    /**
+     * 标签文章列表标签
+     */
+    public function tagTagArticles($tag, $content)
+    {
+        $item          = empty($tag['item']) ? 'vo' : $tag['item'];//循环变量名
+        $order         = empty($tag['order']) ? 'post.published_time DESC' : $tag['order'];
+        $relation      = empty($tag['relation']) ? '' : $tag['relation'];
+        $pageVarName   = empty($tag['pageVarName']) ? '__PAGE_VAR_NAME__' : $tag['pageVarName'];
+        $returnVarName = empty($tag['returnVarName']) ? 'tag_articles_data' : $tag['returnVarName'];
+
+        $field = "''";
+        if (!empty($tag['field'])) {
+            if (strpos($tag['field'], '$') === 0) {
+                $field = $tag['field'];
+                $this->autoBuildVar($field);
+            } else {
+                $field = "'{$tag['field']}'";
+            }
+        }
+
+        $where = '""';
+        if (!empty($tag['where']) && strpos($tag['where'], '$') === 0) {
+            $where = $tag['where'];
+        }
+
+        $limit = "''";
+        if (!empty($tag['limit'])) {
+            if (strpos($tag['limit'], '$') === 0) {
+                $limit = $tag['limit'];
+                $this->autoBuildVar($limit);
+            } else {
+                $limit = "'{$tag['limit']}'";
+            }
+        }
+
+        $page = "''";
+        if (!empty($tag['page'])) {
+            if (strpos($tag['page'], '$') === 0) {
+                $page = $tag['page'];
+                $this->autoBuildVar($page);
+            } else {
+                $page = intval($tag['page']);
+                $page = "'{$page}'";
+            }
+        }
+
+        $tagId = "''";
+        if (!empty($tag['tagId'])) {
+            if (strpos($tag['tagId'], '$') === 0) {
+                $tagId = $tag['tagId'];
+                $this->autoBuildVar($tagId);
+            } else {
+                $tagId = "'{$tag['tagId']}'";
+            }
+        }
+
+        if (strpos($order, '$') === 0) {
+            $this->autoBuildVar($order);
+        } else {
+            $order = "'{$order}'";
+        }
+
+        $parse = <<<parse
+<?php
+\${$returnVarName} = \app\portal\service\ApiService::tagArticles([
+    'field'   => {$field},
+    'where'   => {$where},
+    'limit'   => {$limit},
+    'order'   => {$order},
+    'page'    => {$page},
+    'relation'=> '{$relation}',
+    'tag_id'=>{$tagId}
+]);
+
+\${$pageVarName} = isset(\${$returnVarName}['page'])?\${$returnVarName}['page']:'';
+
+ ?>
+<volist name="{$returnVarName}.articles" id="{$item}">
+{$content}
+</volist>
+parse;
+        return $parse;
+    }
+
+    /**
+     * 单页文章标签
+     */
+    public function tagPage($tag, $content)
+    {
+        $id = empty($tag['id']) ? 0 : $tag['id'];
+        if (strpos($id, '$') === 0) {
+            $this->autoBuildVar($id);
+        }
+        $returnVarName = empty($tag['item']) ? 'portal_page' : $tag['item'];
+
+        $parse = <<<parse
+<?php
+\${$returnVarName} = \app\portal\service\ApiService::page({$id});
+?>
+{$content}
+parse;
+        return $parse;
+    }
+
+    /**
+     * 面包屑标签
+     */
+    public function tagBreadcrumb($tag, $content)
+    {
+        $cid = empty($tag['cid']) ? '0' : $tag['cid'];
+
+        if (!empty($cid)) {
+            $this->autoBuildVar($cid);
+        }
+
+        $self = isset($tag['self']) ? $tag['self'] : 'false';
+
+        $parse = <<<parse
+<?php
+if(!empty({$cid})){
+    \$__BREADCRUMB_ITEMS__ = \app\portal\service\ApiService::breadcrumb({$cid},{$self});
+?>
+
+<volist name="__BREADCRUMB_ITEMS__" id="vo">
+    {$content}
+</volist>
+
+<?php
+}
+?>
+parse;
+
+        return $parse;
+
+    }
+
+    /**
+     * 文章分类标签
+     */
+    public function tagCategories($tag, $content)
+    {
+        $item          = empty($tag['item']) ? 'vo' : $tag['item'];//循环变量名
+        $order         = empty($tag['order']) ? '' : $tag['order'];
+        $ids           = empty($tag['ids']) ? '""' : $tag['ids'];
+        $returnVarName = 'portal_categories_data';
+        if (strpos($ids, '$') === 0) {
+            $this->autoBuildVar($ids);
+        }
+        $where = '""';
+        if (!empty($tag['where']) && strpos($tag['where'], '$') === 0) {
+            $where = $tag['where'];
+        }
+
+        $parse = <<<parse
+<?php
+\${$returnVarName} = \app\portal\service\ApiService::categories([
+    'where'   => {$where},
+    'order'   => '{$order}',
+    'ids'     => {$ids}
+]);
+
+ ?>
+<volist name="{$returnVarName}" id="{$item}">
+{$content}
+</volist>
+parse;
+        return $parse;
+    }
+
+    /**
+     * 文章分类详情标签
+     * @param array  $tag
+     * @param string $content
+     * @return string
+     */
+    public function tagCategory($tag, $content)
+    {
+        $id = empty($tag['id']) ? 0 : $tag['id'];
+        if (strpos($id, '$') === 0) {
+            $this->autoBuildVar($id);
+        }
+        $returnVarName = empty($tag['item']) ? 'portal_category' : $tag['item'];
+
+        $parse = <<<parse
+<?php
+\${$returnVarName} = \app\portal\service\ApiService::category({$id});
+?>
+{$content}
+parse;
+        return $parse;
+    }
+
+    /**
+     * 文章子分类标签
+     */
+    public function tagSubCategories($tag, $content)
+    {
+        $item          = empty($tag['item']) ? 'vo' : $tag['item'];//循环变量名
+        $returnVarName = 'portal_sub_categories_data';
+
+        $categoryId = "0";
+        if (!empty($tag['categoryId'])) {
+            if (strpos($tag['categoryId'], '$') === 0) {
+                $categoryId = $tag['categoryId'];
+                $this->autoBuildVar($categoryId);
+            } else {
+                $categoryId = intval($tag['categoryId']);
+                $categoryId = "{$categoryId}";
+            }
+        }
+
+        $parse = <<<parse
+<?php
+\${$returnVarName} = \app\portal\service\ApiService::subCategories({$categoryId});
+ 
+ ?>
+<volist name="{$returnVarName}" id="{$item}">
+{$content}
+</volist>
+parse;
+        return $parse;
+    }
+
+
+    /**
+     * 文章分类所有子分类标签
+     */
+    public function tagAllSubCategories($tag, $content)
+    {
+        $item          = empty($tag['item']) ? 'vo' : $tag['item'];//循环变量名
+        $returnVarName = 'portal_all_sub_categories_data';
+
+        $categoryId = "0";
+        if (!empty($tag['categoryId'])) {
+            if (strpos($tag['categoryId'], '$') === 0) {
+                $categoryId = $tag['categoryId'];
+                $this->autoBuildVar($categoryId);
+            } else {
+                $categoryId = intval($tag['categoryId']);
+                $categoryId = "{$categoryId}";
+            }
+        }
+
+        $parse = <<<parse
+<?php
+\${$returnVarName} = \app\portal\service\ApiService::allSubCategories({$categoryId});
+ ?>
+<volist name="{$returnVarName}" id="{$item}">
+{$content}
+</volist>
+parse;
+        return $parse;
+    }
+
+}

+ 53 - 0
app/portal/url.php

@@ -0,0 +1,53 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 老猫 <thinkcmf@126.com>
+// +----------------------------------------------------------------------
+return [
+    'List/index'    => [
+        'name'   => '门户应用-文章列表',
+        'vars'   => [
+            'id' => [
+                'pattern' => '\d+',
+                'require' => true
+            ]
+        ],
+        'simple' => true
+    ],
+    'Page/index'    => [
+        'name'   => '门户应用-页面页',
+        'vars'   => [
+            'id' => [
+                'pattern' => '\d+',
+                'require' => true
+            ]
+        ],
+        'simple' => true
+    ],
+    'Article/index' => [
+        'name'   => '门户应用-文章页',
+        'vars'   => [
+            'id'  => [
+                'pattern' => '\d+',
+                'require' => true
+            ],
+            'cid' => [
+                'pattern' => '\d+',
+                'require' => false
+            ]
+        ],
+        'simple' => true
+    ],
+    'Search/index'  => [
+        'name'   => '门户应用-搜索页',
+        'vars'   => [
+
+        ],
+        'simple' => false
+    ],
+];

+ 15 - 0
app/portal/user_action.php

@@ -0,0 +1,15 @@
+<?php
+return [
+//    'test' => [
+//        'name'          => '用户登录',//用户操作名称
+//        'score'         => 1,//更改积分,可以为负
+//        'coin'          => 0,//更改金币,可以为负
+//        'cycle_time'    => 1,//周期时间值
+//        'cycle_type'    => 1,//周期类型;0:不限;1:按天;2:按小时;3:永久
+//        'reward_number' => 1,//奖励次数
+//        'url'           => [
+//            'action' => 'portal/Test/test',
+//            'param'  => ['id' => 1]
+//        ],//执行操作的url
+//    ]
+];

+ 30 - 0
app/portal/validate/AdminArticleValidate.php

@@ -0,0 +1,30 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// +----------------------------------------------------------------------
+namespace app\portal\validate;
+
+use think\Validate;
+
+class AdminArticleValidate extends Validate
+{
+    protected $rule = [
+        'categories' => 'require',
+        'post_title' => 'require',
+    ];
+    protected $message = [
+        'categories.require' => '请指定文章分类!',
+        'post_title.require' => '文章标题不能为空!',
+    ];
+
+    protected $scene = [
+//        'add'  => ['user_login,user_pass,user_email'],
+//        'edit' => ['user_login,user_email'],
+    ];
+}

+ 51 - 0
app/portal/validate/AdminPageValidate.php

@@ -0,0 +1,51 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// +----------------------------------------------------------------------
+namespace app\portal\validate;
+
+use app\admin\model\RouteModel;
+use think\Validate;
+
+class AdminPageValidate extends Validate
+{
+    protected $rule = [
+        'post_title' => 'require',
+        'post_alias' => 'checkAlias'
+    ];
+    protected $message = [
+        'post_title.require' => '页面标题不能为空',
+    ];
+
+    protected $scene = [
+//        'add'  => ['user_login,user_pass,user_email'],
+//        'edit' => ['user_login,user_email'],
+    ];
+
+    // 自定义验证规则
+    protected function checkAlias($value, $rule, $data)
+    {
+        if (empty($value)) {
+            return true;
+        }
+
+        if (preg_match("/^\d+$/", $value)) {
+            return "别名不能为纯数字!";
+        }
+
+        $routeModel = new RouteModel();
+        $fullUrl    = $routeModel->buildFullUrl('portal/Page/index', ['id' => $data['id']]);
+        if (!$routeModel->existsRoute($value, $fullUrl)) {
+            return true;
+        } else {
+            return "别名已经存在!";
+        }
+
+    }
+}

+ 55 - 0
app/portal/validate/PortalCategoryValidate.php

@@ -0,0 +1,55 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2019 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: 小夏 < 449134904@qq.com>
+// +----------------------------------------------------------------------
+namespace app\portal\validate;
+
+use app\admin\model\RouteModel;
+use think\Validate;
+
+class PortalCategoryValidate extends Validate
+{
+    protected $rule = [
+        'name'  => 'require',
+        'alias' => 'checkAlias',
+    ];
+    protected $message = [
+        'name.require' => '分类名称不能为空',
+    ];
+
+    protected $scene = [
+//        'add'  => ['user_login,user_pass,user_email'],
+//        'edit' => ['user_login,user_email'],
+    ];
+
+    // 自定义验证规则
+    protected function checkAlias($value, $rule, $data)
+    {
+        if (empty($value)) {
+            return true;
+        }
+
+        if (preg_match("/^\d+$/", $value)) {
+            return "别名不能为纯数字!";
+        }
+
+        $routeModel = new RouteModel();
+        if (isset($data['id']) && $data['id'] > 0) {
+            $fullUrl = $routeModel->buildFullUrl('portal/List/index', ['id' => $data['id']]);
+        } else {
+            $fullUrl = $routeModel->getFullUrlByUrl($data['alias']);
+        }
+        if (!$routeModel->existsRoute($value, $fullUrl)) {
+            return true;
+        } else {
+            return "别名已经存在!";
+        }
+
+    }
+}

+ 1 - 0
app/portal/version

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

+ 73 - 0
composer.json

@@ -0,0 +1,73 @@
+{
+    "name": "thinkcmf/thinkcmf",
+    "description": "ThinkCMF based on ThinkPHP 6.0 , it is a free and open source Content Management Framework(CMF)",
+    "type": "project",
+    "keywords": [
+        "cmf",
+        "ThinkCMF",
+        "framework",
+        "ThinkPHP",
+        "ORM"
+    ],
+    "homepage": "http://www.thinkcmf.com/",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "catman",
+            "email": "catman@thinkcmf.com"
+        },
+        {
+            "name": "Xia",
+            "email": "449134904@qq.com"
+        }
+    ],
+    "minimum-stability": "dev",
+    "prefer-stable": true,
+    "require": {
+        "php": ">=7.1.0",
+        "ext-json": "*",
+        "ext-curl": "*",
+        "ext-pdo": "*",
+        "thinkcmf/cmf": "^6.0.0",
+        "thinkcmf/cmf-app": "^6.0.0",
+        "thinkcmf/cmf-route": "^6.0.0",
+        "topthink/framework": "^6.0.0",
+        "thinkcmf/think-view": "~1.0.15",
+        "topthink/think-orm": "^2.0",
+        "topthink/think-captcha": "^3.0",
+        "thinkcmf/cmf-install": "^6.0.0",
+        "thinkcmf/cmf-api": "^6.0.0",
+        "thinkcmf/cmf-appstore": "^1.0",
+        "thinkcmf/cmf-root": "^1.0"
+    },
+    "require-dev": {
+        "symfony/var-dumper": "^4.2",
+        "topthink/think-trace": "^1.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "app\\": "app",
+            "api\\": "api",
+            "plugins\\": "public/plugins",
+            "themes\\": "public/themes"
+        },
+        "psr-0": {
+            "": "extend/"
+        }
+    },
+    "extra": {
+        "think-config": "data/config"
+    },
+    "config": {
+        "preferred-install": "dist",
+        "vendor-dir": "vendor"
+    },
+    "scripts": {
+        "post-autoload-dump": [
+            "@php think service:discover",
+            "@php think vendor:publish"
+        ]
+    },
+    "repositories": {
+    }
+}

+ 1735 - 0
composer.lock

@@ -0,0 +1,1735 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "59184daa1ad25d00ea33dc617ac9d2d5",
+    "packages": [
+        {
+            "name": "chamilo/pclzip",
+            "version": "v2.8.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/chamilo/pclzip.git",
+                "reference": "b94b7a190e186a31bd37f21be3a83a48c7d6b49a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/chamilo/pclzip/zipball/b94b7a190e186a31bd37f21be3a83a48c7d6b49a",
+                "reference": "b94b7a190e186a31bd37f21be3a83a48c7d6b49a",
+                "shasum": ""
+            },
+            "replace": {
+                "pclzip/pclzip": "^2.8"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "pclzip.lib.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1"
+            ],
+            "authors": [
+                {
+                    "name": "Vincent Blavet"
+                }
+            ],
+            "description": "A PHP library that offers compression and extraction functions for Zip formatted archives",
+            "homepage": "https://github.com/chamilo/pclzip",
+            "keywords": [
+                "php",
+                "zip"
+            ],
+            "support": {
+                "issues": "https://github.com/chamilo/pclzip/issues",
+                "source": "https://github.com/chamilo/pclzip/tree/v2.8.4"
+            },
+            "time": "2017-11-28T22:14:11+00:00"
+        },
+        {
+            "name": "electrolinux/phpquery",
+            "version": "0.9.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/electrolinux/phpquery.git",
+                "reference": "6cb8afcfe8cd4ce45f2f8c27d561383037c27a3a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/electrolinux/phpquery/zipball/6cb8afcfe8cd4ce45f2f8c27d561383037c27a3a",
+                "reference": "6cb8afcfe8cd4ce45f2f8c27d561383037c27a3a",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "phpQuery/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Tobiasz Cudnik",
+                    "email": "tobiasz.cudnik@gmail.com",
+                    "homepage": "https://github.com/TobiaszCudnik",
+                    "role": "Developer"
+                },
+                {
+                    "name": "didier Belot",
+                    "role": "Packager"
+                }
+            ],
+            "description": "phpQuery is a server-side, chainable, CSS3 selector driven Document Object Model (DOM) API based on jQuery JavaScript Library",
+            "homepage": "http://code.google.com/p/phpquery/",
+            "support": {
+                "source": "https://github.com/electrolinux/phpquery/tree/0.9.6"
+            },
+            "time": "2013-03-21T12:39:33+00:00"
+        },
+        {
+            "name": "ezyang/htmlpurifier",
+            "version": "v4.13.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ezyang/htmlpurifier.git",
+                "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
+                "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.2"
+            },
+            "require-dev": {
+                "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "HTMLPurifier": "library/"
+                },
+                "files": [
+                    "library/HTMLPurifier.composer.php"
+                ],
+                "exclude-from-classmap": [
+                    "/library/HTMLPurifier/Language/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-or-later"
+            ],
+            "authors": [
+                {
+                    "name": "Edward Z. Yang",
+                    "email": "admin@htmlpurifier.org",
+                    "homepage": "http://ezyang.com"
+                }
+            ],
+            "description": "Standards compliant HTML filter written in PHP",
+            "homepage": "http://htmlpurifier.org/",
+            "keywords": [
+                "html"
+            ],
+            "support": {
+                "issues": "https://github.com/ezyang/htmlpurifier/issues",
+                "source": "https://github.com/ezyang/htmlpurifier/tree/master"
+            },
+            "time": "2020-06-29T00:56:53+00:00"
+        },
+        {
+            "name": "league/flysystem",
+            "version": "1.1.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/flysystem.git",
+                "reference": "18634df356bfd4119fe3d6156bdb990c414c14ea"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/18634df356bfd4119fe3d6156bdb990c414c14ea",
+                "reference": "18634df356bfd4119fe3d6156bdb990c414c14ea",
+                "shasum": ""
+            },
+            "require": {
+                "ext-fileinfo": "*",
+                "league/mime-type-detection": "^1.3",
+                "php": "^7.2.5 || ^8.0"
+            },
+            "conflict": {
+                "league/flysystem-sftp": "<1.0.6"
+            },
+            "require-dev": {
+                "phpspec/prophecy": "^1.11.1",
+                "phpunit/phpunit": "^8.5.8"
+            },
+            "suggest": {
+                "ext-ftp": "Allows you to use FTP server storage",
+                "ext-openssl": "Allows you to use FTPS server storage",
+                "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2",
+                "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
+                "league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
+                "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
+                "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
+                "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
+                "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
+                "league/flysystem-webdav": "Allows you to use WebDAV storage",
+                "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter",
+                "spatie/flysystem-dropbox": "Allows you to use Dropbox storage",
+                "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "League\\Flysystem\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Frank de Jonge",
+                    "email": "info@frenky.net"
+                }
+            ],
+            "description": "Filesystem abstraction: Many filesystems, one API.",
+            "keywords": [
+                "Cloud Files",
+                "WebDAV",
+                "abstraction",
+                "aws",
+                "cloud",
+                "copy.com",
+                "dropbox",
+                "file systems",
+                "files",
+                "filesystem",
+                "filesystems",
+                "ftp",
+                "rackspace",
+                "remote",
+                "s3",
+                "sftp",
+                "storage"
+            ],
+            "support": {
+                "issues": "https://github.com/thephpleague/flysystem/issues",
+                "source": "https://github.com/thephpleague/flysystem/tree/1.1.5"
+            },
+            "funding": [
+                {
+                    "url": "https://offset.earth/frankdejonge",
+                    "type": "other"
+                }
+            ],
+            "time": "2021-08-17T13:49:42+00:00"
+        },
+        {
+            "name": "league/flysystem-cached-adapter",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/flysystem-cached-adapter.git",
+                "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-cached-adapter/zipball/d1925efb2207ac4be3ad0c40b8277175f99ffaff",
+                "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff",
+                "shasum": ""
+            },
+            "require": {
+                "league/flysystem": "~1.0",
+                "psr/cache": "^1.0.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "~0.9",
+                "phpspec/phpspec": "^3.4",
+                "phpunit/phpunit": "^5.7",
+                "predis/predis": "~1.0",
+                "tedivm/stash": "~0.12"
+            },
+            "suggest": {
+                "ext-phpredis": "Pure C implemented extension for PHP"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "League\\Flysystem\\Cached\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "frankdejonge",
+                    "email": "info@frenky.net"
+                }
+            ],
+            "description": "An adapter decorator to enable meta-data caching.",
+            "support": {
+                "issues": "https://github.com/thephpleague/flysystem-cached-adapter/issues",
+                "source": "https://github.com/thephpleague/flysystem-cached-adapter/tree/master"
+            },
+            "time": "2020-07-25T15:56:04+00:00"
+        },
+        {
+            "name": "league/mime-type-detection",
+            "version": "1.7.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thephpleague/mime-type-detection.git",
+                "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
+                "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
+                "shasum": ""
+            },
+            "require": {
+                "ext-fileinfo": "*",
+                "php": "^7.2 || ^8.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^2.18",
+                "phpstan/phpstan": "^0.12.68",
+                "phpunit/phpunit": "^8.5.8 || ^9.3"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "League\\MimeTypeDetection\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Frank de Jonge",
+                    "email": "info@frankdejonge.nl"
+                }
+            ],
+            "description": "Mime-type detection for Flysystem",
+            "support": {
+                "issues": "https://github.com/thephpleague/mime-type-detection/issues",
+                "source": "https://github.com/thephpleague/mime-type-detection/tree/1.7.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/frankdejonge",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/league/flysystem",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-01-18T20:58:21+00:00"
+        },
+        {
+            "name": "mindplay/annotations",
+            "version": "1.3.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-annotations/php-annotations.git",
+                "reference": "7e1547259a6aa7e3abc3832207499943614e9d13"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-annotations/php-annotations/zipball/7e1547259a6aa7e3abc3832207499943614e9d13",
+                "reference": "7e1547259a6aa7e3abc3832207499943614e9d13",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "require-dev": {
+                "phpunit/php-code-coverage": "~1.2.1",
+                "phpunit/php-file-iterator": ">=1.3.0@stable"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "mindplay\\annotations\\": "src\\annotations"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-3.0+"
+            ],
+            "authors": [
+                {
+                    "name": "Rasmus Schultz",
+                    "email": "rasmus@mindplay.dk"
+                }
+            ],
+            "description": "Industrial-strength annotations for PHP",
+            "homepage": "http://blog.mindplay.dk/",
+            "keywords": [
+                "annotations",
+                "framework"
+            ],
+            "support": {
+                "issues": "https://github.com/php-annotations/php-annotations/issues",
+                "source": "https://github.com/php-annotations/php-annotations/tree/1.3.2"
+            },
+            "time": "2021-01-21T11:42:37+00:00"
+        },
+        {
+            "name": "phpmailer/phpmailer",
+            "version": "v6.5.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPMailer/PHPMailer.git",
+                "reference": "dd803df5ad7492e1b40637f7ebd258fee5ca7355"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/dd803df5ad7492e1b40637f7ebd258fee5ca7355",
+                "reference": "dd803df5ad7492e1b40637f7ebd258fee5ca7355",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-filter": "*",
+                "ext-hash": "*",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+                "doctrine/annotations": "^1.2",
+                "php-parallel-lint/php-console-highlighter": "^0.5.0",
+                "php-parallel-lint/php-parallel-lint": "^1.3",
+                "phpcompatibility/php-compatibility": "^9.3.5",
+                "roave/security-advisories": "dev-latest",
+                "squizlabs/php_codesniffer": "^3.6.0",
+                "yoast/phpunit-polyfills": "^1.0.0"
+            },
+            "suggest": {
+                "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+                "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+                "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+                "psr/log": "For optional PSR-3 debug logging",
+                "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication",
+                "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PHPMailer\\PHPMailer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-only"
+            ],
+            "authors": [
+                {
+                    "name": "Marcus Bointon",
+                    "email": "phpmailer@synchromedia.co.uk"
+                },
+                {
+                    "name": "Jim Jagielski",
+                    "email": "jimjag@gmail.com"
+                },
+                {
+                    "name": "Andy Prevost",
+                    "email": "codeworxtech@users.sourceforge.net"
+                },
+                {
+                    "name": "Brent R. Matzelle"
+                }
+            ],
+            "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+            "support": {
+                "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+                "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.5.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Synchro",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-08-18T09:14:16+00:00"
+        },
+        {
+            "name": "psr/cache",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/cache.git",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Cache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for caching libraries",
+            "keywords": [
+                "cache",
+                "psr",
+                "psr-6"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/cache/tree/master"
+            },
+            "time": "2016-08-06T20:24:11+00:00"
+        },
+        {
+            "name": "psr/container",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/container.git",
+                "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
+                "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Container\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common Container Interface (PHP FIG PSR-11)",
+            "homepage": "https://github.com/php-fig/container",
+            "keywords": [
+                "PSR-11",
+                "container",
+                "container-interface",
+                "container-interop",
+                "psr"
+            ],
+            "support": {
+                "issues": "https://github.com/php-fig/container/issues",
+                "source": "https://github.com/php-fig/container/tree/1.1.1"
+            },
+            "time": "2021-03-05T17:36:06+00:00"
+        },
+        {
+            "name": "psr/log",
+            "version": "1.1.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+                "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "Psr/Log/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/log/tree/1.1.4"
+            },
+            "time": "2021-05-03T11:20:27+00:00"
+        },
+        {
+            "name": "psr/simple-cache",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/simple-cache.git",
+                "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+                "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\SimpleCache\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interfaces for simple caching",
+            "keywords": [
+                "cache",
+                "caching",
+                "psr",
+                "psr-16",
+                "simple-cache"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/simple-cache/tree/master"
+            },
+            "time": "2017-10-23T01:57:42+00:00"
+        },
+        {
+            "name": "thinkcmf/cmf",
+            "version": "v6.0.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thinkcmf/cmf-core.git",
+                "reference": "084e2fae6562484b205ceb4401320000df985b63"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thinkcmf/cmf-core/zipball/084e2fae6562484b205ceb4401320000df985b63",
+                "reference": "084e2fae6562484b205ceb4401320000df985b63",
+                "shasum": ""
+            },
+            "require": {
+                "electrolinux/phpquery": "^0.9.6",
+                "ezyang/htmlpurifier": "^4.9",
+                "mindplay/annotations": "^1.3",
+                "phpmailer/phpmailer": "~6.0",
+                "thinkcmf/cmf-extend": "~5.1.0",
+                "topthink/framework": "~6.0.0",
+                "topthink/think-captcha": "~3.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "think\\captcha\\CaptchaService"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "cmf\\": "src"
+                },
+                "files": [
+                    "src/common.php"
+                ],
+                "classmap": [
+                    "src/App.php",
+                    "src/console/command/VendorPublish.php",
+                    "src/captcha/Captcha.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "catman",
+                    "email": "catman@thinkcmf.com"
+                }
+            ],
+            "description": "The ThinkCMF Core Package",
+            "support": {
+                "issues": "https://github.com/thinkcmf/cmf-core/issues",
+                "source": "https://github.com/thinkcmf/cmf-core/tree/v6.0.7"
+            },
+            "time": "2021-09-17T22:40:18+00:00"
+        },
+        {
+            "name": "thinkcmf/cmf-api",
+            "version": "v6.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thinkcmf/cmf-api.git",
+                "reference": "4f31eeb81a6d7337eba587c216c22159cf55d37c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thinkcmf/cmf-api/zipball/4f31eeb81a6d7337eba587c216c22159cf55d37c",
+                "reference": "4f31eeb81a6d7337eba587c216c22159cf55d37c",
+                "shasum": ""
+            },
+            "require": {
+                "thinkcmf/cmf": "^6.0.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "api\\": "src"
+                },
+                "files": []
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "catman",
+                    "email": "catman@thinkcmf.com"
+                }
+            ],
+            "description": "The ThinkCMF 5.1 Core Api Package",
+            "support": {
+                "issues": "https://github.com/thinkcmf/cmf-api/issues",
+                "source": "https://github.com/thinkcmf/cmf-api/tree/v6.0.3"
+            },
+            "time": "2021-09-12T20:40:10+00:00"
+        },
+        {
+            "name": "thinkcmf/cmf-app",
+            "version": "v6.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thinkcmf/cmf-app.git",
+                "reference": "50048f2ffef66fdb3698662231e0f00392c5a021"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thinkcmf/cmf-app/zipball/50048f2ffef66fdb3698662231e0f00392c5a021",
+                "reference": "50048f2ffef66fdb3698662231e0f00392c5a021",
+                "shasum": ""
+            },
+            "require": {
+                "thinkcmf/cmf": "^6.0.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "app\\": "src"
+                },
+                "files": []
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "catman",
+                    "email": "catman@thinkcmf.com"
+                }
+            ],
+            "description": "The ThinkCMF App Package",
+            "support": {
+                "issues": "https://github.com/thinkcmf/cmf-app/issues",
+                "source": "https://github.com/thinkcmf/cmf-app/tree/v6.0.5"
+            },
+            "time": "2021-09-17T22:42:58+00:00"
+        },
+        {
+            "name": "thinkcmf/cmf-appstore",
+            "version": "v1.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thinkcmf/cmf-appstore.git",
+                "reference": "8becf22e6a2a87822f9aea5f59a6a7bebc52658f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thinkcmf/cmf-appstore/zipball/8becf22e6a2a87822f9aea5f59a6a7bebc52658f",
+                "reference": "8becf22e6a2a87822f9aea5f59a6a7bebc52658f",
+                "shasum": ""
+            },
+            "require": {
+                "chamilo/pclzip": "^2.8"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "app\\admin\\": "src"
+                },
+                "files": []
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "catman",
+                    "email": "catman@thinkcmf.com"
+                }
+            ],
+            "description": "The ThinkCMF App Store Package",
+            "support": {
+                "issues": "https://github.com/thinkcmf/cmf-appstore/issues",
+                "source": "https://github.com/thinkcmf/cmf-appstore/tree/v1.0.4"
+            },
+            "time": "2021-04-03T14:09:15+00:00"
+        },
+        {
+            "name": "thinkcmf/cmf-extend",
+            "version": "v5.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thinkcmf/cmf-extend.git",
+                "reference": "500ac89f30b9352dbac4f3f13c88212d8b2a9618"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thinkcmf/cmf-extend/zipball/500ac89f30b9352dbac4f3f13c88212d8b2a9618",
+                "reference": "500ac89f30b9352dbac4f3f13c88212d8b2a9618",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "dir\\": "src/dir",
+                    "tree\\": "src/tree",
+                    "wxapp\\": "src/wxapp"
+                },
+                "files": []
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "catman",
+                    "email": "catman@thinkcmf.com"
+                }
+            ],
+            "description": "The ThinkCMF extend Package",
+            "support": {
+                "issues": "https://github.com/thinkcmf/cmf-extend/issues",
+                "source": "https://github.com/thinkcmf/cmf-extend/tree/v5.1.1"
+            },
+            "time": "2021-02-10T07:02:12+00:00"
+        },
+        {
+            "name": "thinkcmf/cmf-install",
+            "version": "v6.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thinkcmf/cmf-install.git",
+                "reference": "7c12b4e87eb7585e74d3f0398b898bebb34e5ce5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thinkcmf/cmf-install/zipball/7c12b4e87eb7585e74d3f0398b898bebb34e5ce5",
+                "reference": "7c12b4e87eb7585e74d3f0398b898bebb34e5ce5",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "app\\": "src"
+                },
+                "files": []
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "catman",
+                    "email": "catman@thinkcmf.com"
+                }
+            ],
+            "description": "The ThinkCMF Install Package",
+            "support": {
+                "issues": "https://github.com/thinkcmf/cmf-install/issues",
+                "source": "https://github.com/thinkcmf/cmf-install/tree/v6.0.3"
+            },
+            "time": "2021-04-20T11:18:24+00:00"
+        },
+        {
+            "name": "thinkcmf/cmf-root",
+            "version": "v1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thinkcmf/cmf-root.git",
+                "reference": "070f83e79273697e58b283c7a95bdc66eb583e75"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thinkcmf/cmf-root/zipball/070f83e79273697e58b283c7a95bdc66eb583e75",
+                "reference": "070f83e79273697e58b283c7a95bdc66eb583e75",
+                "shasum": ""
+            },
+            "require": {
+                "composer-plugin-api": "^1.0||^2.0"
+            },
+            "require-dev": {
+                "composer/composer": "^1.0||^2.0"
+            },
+            "type": "composer-plugin",
+            "extra": {
+                "class": "cmf\\composer\\RootDirPlugin"
+            },
+            "autoload": {
+                "psr-4": {
+                    "cmf\\composer\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "catman",
+                    "email": "catman@thinkcmf.com"
+                }
+            ],
+            "description": "The files in ThinkCMF root dir",
+            "support": {
+                "issues": "https://github.com/thinkcmf/cmf-root/issues",
+                "source": "https://github.com/thinkcmf/cmf-root/tree/v1.0.2"
+            },
+            "time": "2021-09-29T00:17:19+00:00"
+        },
+        {
+            "name": "thinkcmf/cmf-route",
+            "version": "v6.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thinkcmf/cmf-route.git",
+                "reference": "706276d67a5a6ae68d6399107169d03e205aeaf1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thinkcmf/cmf-route/zipball/706276d67a5a6ae68d6399107169d03e205aeaf1",
+                "reference": "706276d67a5a6ae68d6399107169d03e205aeaf1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "think\\": "src"
+                },
+                "classmap": [
+                    "src/Route.php",
+                    "src/Http.php",
+                    "src/route/Rule.php",
+                    "src/route/Url.php",
+                    "src/route/dispatch/Controller.php",
+                    "src/route/dispatch/Url.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                },
+                {
+                    "name": "catman",
+                    "email": "catman@thinkcmf.com"
+                }
+            ],
+            "description": "thinkcmf6 route",
+            "support": {
+                "issues": "https://github.com/thinkcmf/cmf-route/issues",
+                "source": "https://github.com/thinkcmf/cmf-route/tree/v6.0.3"
+            },
+            "time": "2021-03-22T10:33:16+00:00"
+        },
+        {
+            "name": "thinkcmf/think-template",
+            "version": "v2.0.9",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thinkcmf/think-template.git",
+                "reference": "9d52df5f7ab5e1904b3dfa42b6ad49f5ea398c01"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thinkcmf/think-template/zipball/9d52df5f7ab5e1904b3dfa42b6ad49f5ea398c01",
+                "reference": "9d52df5f7ab5e1904b3dfa42b6ad49f5ea398c01",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0",
+                "psr/simple-cache": "^1.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "think\\": "src"
+                },
+                "classmap": [
+                    "src/View.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                }
+            ],
+            "description": "the php template engine",
+            "support": {
+                "source": "https://github.com/thinkcmf/think-template/tree/v2.0.9"
+            },
+            "time": "2021-02-10T06:11:43+00:00"
+        },
+        {
+            "name": "thinkcmf/think-view",
+            "version": "v1.0.15",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thinkcmf/think-view.git",
+                "reference": "1cd8abf6639e5fe4e25f7f244152c9f67779bf67"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thinkcmf/think-view/zipball/1cd8abf6639e5fe4e25f7f244152c9f67779bf67",
+                "reference": "1cd8abf6639e5fe4e25f7f244152c9f67779bf67",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0",
+                "thinkcmf/think-template": "~2.0.8"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "think\\view\\driver\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                }
+            ],
+            "description": "thinkphp template driver",
+            "support": {
+                "source": "https://github.com/thinkcmf/think-view/tree/v1.0.15"
+            },
+            "time": "2020-12-31T05:13:44+00:00"
+        },
+        {
+            "name": "topthink/framework",
+            "version": "v6.0.9",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/framework.git",
+                "reference": "0b5fb453f0e533de3af3a1ab6a202510b61be617"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/framework/zipball/0b5fb453f0e533de3af3a1ab6a202510b61be617",
+                "reference": "0b5fb453f0e533de3af3a1ab6a202510b61be617",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "league/flysystem": "^1.1.4",
+                "league/flysystem-cached-adapter": "^1.0",
+                "php": ">=7.2.5",
+                "psr/container": "~1.0",
+                "psr/log": "~1.0",
+                "psr/simple-cache": "^1.0",
+                "topthink/think-helper": "^3.1.1",
+                "topthink/think-orm": "^2.0"
+            },
+            "require-dev": {
+                "mikey179/vfsstream": "^1.6",
+                "mockery/mockery": "^1.2",
+                "phpunit/phpunit": "^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [],
+                "psr-4": {
+                    "think\\": "src/think/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                },
+                {
+                    "name": "yunwuxin",
+                    "email": "448901948@qq.com"
+                }
+            ],
+            "description": "The ThinkPHP Framework.",
+            "homepage": "http://thinkphp.cn/",
+            "keywords": [
+                "framework",
+                "orm",
+                "thinkphp"
+            ],
+            "support": {
+                "issues": "https://github.com/top-think/framework/issues",
+                "source": "https://github.com/top-think/framework/tree/v6.0.9"
+            },
+            "time": "2021-07-22T03:24:49+00:00"
+        },
+        {
+            "name": "topthink/think-captcha",
+            "version": "v3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-captcha.git",
+                "reference": "1eef3717c1bcf4f5bbe2d1a1c704011d330a8b55"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-captcha/zipball/1eef3717c1bcf4f5bbe2d1a1c704011d330a8b55",
+                "reference": "1eef3717c1bcf4f5bbe2d1a1c704011d330a8b55",
+                "shasum": ""
+            },
+            "require": {
+                "topthink/framework": "^6.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "think\\captcha\\CaptchaService"
+                    ],
+                    "config": {
+                        "captcha": "src/config.php"
+                    }
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "think\\captcha\\": "src/"
+                },
+                "files": [
+                    "src/helper.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "yunwuxin",
+                    "email": "448901948@qq.com"
+                }
+            ],
+            "description": "captcha package for thinkphp",
+            "support": {
+                "issues": "https://github.com/top-think/think-captcha/issues",
+                "source": "https://github.com/top-think/think-captcha/tree/v3.0.3"
+            },
+            "time": "2020-05-19T10:55:45+00:00"
+        },
+        {
+            "name": "topthink/think-helper",
+            "version": "v3.1.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-helper.git",
+                "reference": "f98e3ad44acd27ae85a4d923b1bdfd16c6d8d905"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-helper/zipball/f98e3ad44acd27ae85a4d923b1bdfd16c6d8d905",
+                "reference": "f98e3ad44acd27ae85a4d923b1bdfd16c6d8d905",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "think\\": "src"
+                },
+                "files": [
+                    "src/helper.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "yunwuxin",
+                    "email": "448901948@qq.com"
+                }
+            ],
+            "description": "The ThinkPHP6 Helper Package",
+            "support": {
+                "issues": "https://github.com/top-think/think-helper/issues",
+                "source": "https://github.com/top-think/think-helper/tree/v3.1.5"
+            },
+            "time": "2021-06-21T06:17:31+00:00"
+        },
+        {
+            "name": "topthink/think-orm",
+            "version": "v2.0.44",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-orm.git",
+                "reference": "5d3d5c1ebf8bfccf34bacd90edb42989b16ea409"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-orm/zipball/5d3d5c1ebf8bfccf34bacd90edb42989b16ea409",
+                "reference": "5d3d5c1ebf8bfccf34bacd90edb42989b16ea409",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "ext-pdo": "*",
+                "php": ">=7.1.0",
+                "psr/log": "~1.0",
+                "psr/simple-cache": "^1.0",
+                "topthink/think-helper": "^3.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7|^8|^9.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "think\\": "src"
+                },
+                "files": [
+                    "stubs/load_stubs.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                }
+            ],
+            "description": "think orm",
+            "keywords": [
+                "database",
+                "orm"
+            ],
+            "support": {
+                "issues": "https://github.com/top-think/think-orm/issues",
+                "source": "https://github.com/top-think/think-orm/tree/v2.0.44"
+            },
+            "time": "2021-07-21T02:22:31+00:00"
+        }
+    ],
+    "packages-dev": [
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.23.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6",
+                "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-27T12:26:48+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php72",
+            "version": "v1.23.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php72.git",
+                "reference": "9a142215a36a3888e30d0a9eeea9766764e96976"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976",
+                "reference": "9a142215a36a3888e30d0a9eeea9766764e96976",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php72\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php72/tree/v1.23.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-27T09:17:38+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php80",
+            "version": "v1.23.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php80.git",
+                "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be",
+                "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php80\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ion Bazan",
+                    "email": "ion.bazan@gmail.com"
+                },
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-07-28T13:41:28+00:00"
+        },
+        {
+            "name": "symfony/var-dumper",
+            "version": "v4.4.30",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/var-dumper.git",
+                "reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c",
+                "reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php72": "~1.5",
+                "symfony/polyfill-php80": "^1.16"
+            },
+            "conflict": {
+                "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
+                "symfony/console": "<3.4"
+            },
+            "require-dev": {
+                "ext-iconv": "*",
+                "symfony/console": "^3.4|^4.0|^5.0",
+                "symfony/process": "^4.4|^5.0",
+                "twig/twig": "^1.43|^2.13|^3.0.4"
+            },
+            "suggest": {
+                "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
+                "ext-intl": "To show region name in time zone dump",
+                "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
+            },
+            "bin": [
+                "Resources/bin/var-dump-server"
+            ],
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "Resources/functions/dump.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Component\\VarDumper\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides mechanisms for walking through any arbitrary PHP variable",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "debug",
+                "dump"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/var-dumper/tree/v4.4.30"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-08-04T20:31:23+00:00"
+        },
+        {
+            "name": "topthink/think-trace",
+            "version": "v1.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/top-think/think-trace.git",
+                "reference": "9a9fa8f767b6c66c5a133ad21ca1bc96ad329444"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/top-think/think-trace/zipball/9a9fa8f767b6c66c5a133ad21ca1bc96ad329444",
+                "reference": "9a9fa8f767b6c66c5a133ad21ca1bc96ad329444",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.0",
+                "topthink/framework": "^6.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "think": {
+                    "services": [
+                        "think\\trace\\Service"
+                    ],
+                    "config": {
+                        "trace": "src/config.php"
+                    }
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "think\\trace\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "liu21st",
+                    "email": "liu21st@gmail.com"
+                }
+            ],
+            "description": "thinkphp debug trace",
+            "support": {
+                "issues": "https://github.com/top-think/think-trace/issues",
+                "source": "https://github.com/top-think/think-trace/tree/v1.4"
+            },
+            "time": "2020-06-29T05:27:28+00:00"
+        }
+    ],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": true,
+    "prefer-lowest": false,
+    "platform": {
+        "php": ">=7.1.0",
+        "ext-json": "*",
+        "ext-curl": "*",
+        "ext-pdo": "*"
+    },
+    "platform-dev": [],
+    "plugin-api-version": "2.0.0"
+}

+ 2 - 0
data/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 29 - 0
docker-compose.yml

@@ -0,0 +1,29 @@
+version: "2"
+
+services:
+  mysql:
+    image: mysql:8.0.13
+    restart: always
+    command: --default-authentication-plugin=mysql_native_password
+    container_name: thinkcmf-db
+    environment:
+      - MYSQL_ROOT_PASSWORD=Root@123
+      - MYSQL_USER=thinkcmf
+      - MYSQL_PASSWORD=Thinkcmf@123
+      - MYSQL_DATABASE=thinkcmf
+      - TZ=Asia/Shanghai
+    volumes:
+      - ./mysql:/var/lib/mysql
+    ports:
+      - "3306:3306"
+      - "80:80"
+    expose:
+      - 80
+
+  thinkcmf:
+    build: .
+    network_mode: service:mysql
+    restart: always
+    container_name: thinkcmf-web
+    depends_on:
+      - mysql

+ 12 - 0
public/.htaccess

@@ -0,0 +1,12 @@
+<IfModule mod_rewrite.c>
+  Options +FollowSymlinks -Multiviews
+  RewriteEngine On
+
+  RewriteCond %{REQUEST_FILENAME} !-d
+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteRule ^api/?(.*)$ api.php?s=$1 [QSA,PT,L]
+
+  RewriteCond %{REQUEST_FILENAME} !-d
+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteRule ^(.*)$ index.php?s=$1 [QSA,PT,L]
+</IfModule>

+ 40 - 0
public/api.php

@@ -0,0 +1,40 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkCMF [ WE CAN DO IT MORE SIMPLE ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2013-2020 http://www.thinkcmf.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Author: 老猫 <zxxjjforever@163.com>
+// +----------------------------------------------------------------------
+// [ 应用入口文件 ]
+namespace think;
+
+// 调试模式开关 已经移到.env文件中,APP_DEBUG = true
+//define('APP_DEBUG', true);
+
+// 定义CMF根目录,可更改此目录
+define('CMF_ROOT', dirname(__DIR__) . '/');
+
+// 定义CMF数据目录,可更改此目录
+define('CMF_DATA', CMF_ROOT . 'data/');
+
+// 定义应用目录
+define('APP_PATH', CMF_ROOT . 'app/');
+
+// 定义网站入口目录
+define('WEB_ROOT', __DIR__ . '/');
+
+// 定义命名空间
+define('APP_NAMESPACE', 'api');
+
+require CMF_ROOT . 'vendor/autoload.php';
+
+// 执行HTTP应用并响应
+$http = (new App())->http;
+
+$response = $http->run();
+
+$response->send();
+
+$http->end($response);
+

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