Pārlūkot izejas kodu

init php yii2 system

abiao 5 gadi atpakaļ
vecāks
revīzija
951d7f54c0
100 mainītis faili ar 5069 papildinājumiem un 0 dzēšanām
  1. 3 0
      webApp/.bowerrc
  2. 38 0
      webApp/.gitignore
  3. 69 0
      webApp/Dockerfile
  4. 32 0
      webApp/LICENSE.md
  5. 152 0
      webApp/README.md
  6. 67 0
      webApp/api/behaviors/ResponseFormatBehavior.php
  7. 15 0
      webApp/api/codeception.yml
  8. 3 0
      webApp/api/config/.gitignore
  9. 2 0
      webApp/api/config/bootstrap.php
  10. 97 0
      webApp/api/config/main.php
  11. 5 0
      webApp/api/config/params.php
  12. 9 0
      webApp/api/config/test.php
  13. 14 0
      webApp/api/controllers/ArticleController.php
  14. 69 0
      webApp/api/controllers/PaidController.php
  15. 102 0
      webApp/api/controllers/SiteController.php
  16. 57 0
      webApp/api/controllers/UserController.php
  17. 15 0
      webApp/api/fixtures/UserFixture.php
  18. 22 0
      webApp/api/models/Article.php
  19. 48 0
      webApp/api/models/User.php
  20. 79 0
      webApp/api/models/form/LoginForm.php
  21. 74 0
      webApp/api/models/form/SignupForm.php
  22. 24 0
      webApp/api/modules/v1/Module.php
  23. 13 0
      webApp/api/modules/v1/controllers/ArticleController.php
  24. 61 0
      webApp/api/modules/v1/controllers/PaidController.php
  25. 13 0
      webApp/api/modules/v1/controllers/SiteController.php
  26. 14 0
      webApp/api/modules/v1/controllers/UserController.php
  27. 2 0
      webApp/api/runtime/.gitignore
  28. 11 0
      webApp/api/tests/_bootstrap.php
  29. 14 0
      webApp/api/tests/_data/login_data.php
  30. 2 0
      webApp/api/tests/_output/.gitignore
  31. 1 0
      webApp/api/tests/_support/.gitignore
  32. 26 0
      webApp/api/tests/_support/AcceptanceTester.php
  33. 33 0
      webApp/api/tests/_support/FunctionalTester.php
  34. 25 0
      webApp/api/tests/_support/UnitTester.php
  35. 8 0
      webApp/api/tests/acceptance.suite.yml.example
  36. 28 0
      webApp/api/tests/acceptance/ArticleCest.php
  37. 26 0
      webApp/api/tests/acceptance/_bootstrap.php
  38. 28 0
      webApp/api/tests/acceptance/modules/v1/V1ArticleCest.php
  39. 8 0
      webApp/api/tests/functional.suite.yml.example
  40. 21 0
      webApp/api/tests/functional/ArticleCest.php
  41. 49 0
      webApp/api/tests/functional/PaidCest.php
  42. 63 0
      webApp/api/tests/functional/SiteCest.php
  43. 70 0
      webApp/api/tests/functional/UserCest.php
  44. 27 0
      webApp/api/tests/functional/_bootstrap.php
  45. 28 0
      webApp/api/tests/functional/modules/v1/V1ArticleCest.php
  46. 50 0
      webApp/api/tests/functional/modules/v1/V1PaidCest.php
  47. 64 0
      webApp/api/tests/functional/modules/v1/V1SiteCest.php
  48. 69 0
      webApp/api/tests/functional/modules/v1/V1UserCest.php
  49. 6 0
      webApp/api/tests/unit.suite.yml
  50. 16 0
      webApp/api/tests/unit/_bootstrap.php
  51. 20 0
      webApp/api/tests/unit/models/ArticleCestTest.php
  52. 47 0
      webApp/api/tests/unit/models/UserCestTest.php
  53. 3 0
      webApp/api/web/.gitignore
  54. 9 0
      webApp/api/web/.htaccess
  55. 2 0
      webApp/api/web/assets/.gitignore
  56. BIN
      webApp/api/web/favicon.ico
  57. 2 0
      webApp/api/web/uploads/.gitignore
  58. 152 0
      webApp/backend/actions/CreateAction.php
  59. 99 0
      webApp/backend/actions/DeleteAction.php
  60. 112 0
      webApp/backend/actions/DoAction.php
  61. 79 0
      webApp/backend/actions/IndexAction.php
  62. 95 0
      webApp/backend/actions/SortAction.php
  63. 159 0
      webApp/backend/actions/UpdateAction.php
  64. 68 0
      webApp/backend/actions/ViewAction.php
  65. 72 0
      webApp/backend/actions/helpers/Helper.php
  66. 50 0
      webApp/backend/assets/AppAsset.php
  67. 43 0
      webApp/backend/assets/IndexAsset.php
  68. 38 0
      webApp/backend/assets/UeditorAsset.php
  69. 36 0
      webApp/backend/assets/WebuploaderAsset.php
  70. 65 0
      webApp/backend/behaviors/TimeSearchBehavior.php
  71. 15 0
      webApp/backend/codeception.yml
  72. 178 0
      webApp/backend/components/AccessControl.php
  73. 116 0
      webApp/backend/components/AdminLog.php
  74. 119 0
      webApp/backend/components/CustomLog.php
  75. 96 0
      webApp/backend/components/gii/crud/Generator.php
  76. 20 0
      webApp/backend/components/gii/crud/default/ServiceInterface.php
  77. 157 0
      webApp/backend/components/gii/crud/default/controller.php
  78. 86 0
      webApp/backend/components/gii/crud/default/search.php
  79. 43 0
      webApp/backend/components/gii/crud/default/service.php
  80. 53 0
      webApp/backend/components/gii/crud/default/views/_form.php
  81. 49 0
      webApp/backend/components/gii/crud/default/views/_search.php
  82. 26 0
      webApp/backend/components/gii/crud/default/views/create.php
  83. 80 0
      webApp/backend/components/gii/crud/default/views/index.php
  84. 26 0
      webApp/backend/components/gii/crud/default/views/update.php
  85. 46 0
      webApp/backend/components/gii/crud/default/views/view.php
  86. 15 0
      webApp/backend/components/gii/model/Generator.php
  87. 9 0
      webApp/backend/components/gii/model/form.php
  88. 16 0
      webApp/backend/components/search/SearchEvent.php
  89. 3 0
      webApp/backend/config/.gitignore
  90. 2 0
      webApp/backend/config/bootstrap.php
  91. 193 0
      webApp/backend/config/main.php
  92. 17 0
      webApp/backend/config/params.php
  93. 19 0
      webApp/backend/config/test.php
  94. 127 0
      webApp/backend/controllers/AdController.php
  95. 183 0
      webApp/backend/controllers/AdminUserController.php
  96. 116 0
      webApp/backend/controllers/ArticleController.php
  97. 72 0
      webApp/backend/controllers/AssetsController.php
  98. 159 0
      webApp/backend/controllers/BannerController.php
  99. 105 0
      webApp/backend/controllers/CategoryController.php
  100. 0 0
      webApp/backend/controllers/ClearController.php

+ 3 - 0
webApp/.bowerrc

@@ -0,0 +1,3 @@
+{
+    "directory" : "vendor/bower"
+}

+ 38 - 0
webApp/.gitignore

@@ -0,0 +1,38 @@
+# yii console command
+/yii
+/yii_test
+/yii_test.bat
+
+# phpstorm project files
+.idea
+
+# netbeans project files
+nbproject
+
+# zend studio for eclipse project files
+.buildpath
+.project
+.settings
+
+# windows thumbnail cache
+Thumbs.db
+
+# composer vendor dir
+
+
+# composer itself is not needed
+composer.phar
+
+# Mac DS_Store Files
+.DS_Store
+
+# phpunit itself is not needed
+phpunit.phar
+# local phpunit config
+/phpunit.xml
+composer.lock
+backend/tests/acceptance.suite.yml
+frontend/tests/acceptance.suite.yml
+api/tests/acceptance.suite.yml
+api/tests/functional.suite.yml
+install/install.lock

+ 69 - 0
webApp/Dockerfile

@@ -0,0 +1,69 @@
+ARG PHP_VER=7.4
+
+
+FROM php:${PHP_VER}-fpm
+MAINTAINER liufee job@feehi.com
+
+
+ARG COMPOSER_VER=2.0.0-alpha3
+
+
+RUN apt-get update && apt-get install -y \
+        libfreetype6-dev \
+        libjpeg62-turbo-dev \
+        libpng-dev \
+        libpq-dev \
+        unzip \
+    && docker-php-ext-configure gd --with-freetype --with-jpeg \
+    && docker-php-ext-install -j$(nproc) gd \
+    && docker-php-ext-install pdo mysqli pdo_mysql pdo_pgsql
+
+
+RUN set -eux; \
+    curl --fail --location --retry 3 --output /usr/bin/composer https://getcomposer.org/download/${COMPOSER_VER}/composer.phar \
+    && chmod +x /usr/bin/composer
+
+
+#RUN cd /usr/src \
+    #&& curl --fail --location --retry 3 --output /usr/src/vendor.zip https://resource-1251086492.cos.ap-shanghai.myqcloud.com/vendor.zip \
+    #&& unzip /usr/src/vendor.zip && rm -rf /usr/src/vendor.zip
+
+
+ENV FeehiCMSPath="/usr/local/feehicms"
+ENV DBDSN="sqlite:/data/feehi.db"
+ENV DBUser="root"
+ENV DBPassword=""
+ENV DBCharset="utf8"
+ENV TablePrefix=""
+ENV AdminUsername="admin"
+ENV AdminPassword="123456"
+ENV FrontendUri="//127.0.0.1/"
+ENV Listening="0.0.0.0:80"
+ENV Env="Development"
+
+
+COPY . ${FeehiCMSPath}
+
+
+RUN cd ${FeehiCMSPath} \
+    #&& cp -rf /usr/src/vendor ${FeehiCMSPath} && rm -rf /usr/src/vendor \
+    #&& cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \
+    && composer update -vvv && composer dump-autoload -o \
+    && cp ${FeehiCMSPath}/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh \
+    && chmod +x /usr/local/bin/docker-entrypoint.sh \
+    && rm -rf ${FeehiCMSPath}/install/install.lock \
+
+
+ENV PATH=$PATH:$FeehiCMSPath
+
+
+WORKDIR ${FeehiCMSPath}
+
+
+EXPOSE 80
+
+
+ENTRYPOINT ["/bin/bash", "/usr/local/bin/docker-entrypoint.sh"]
+
+
+CMD ["start"]

+ 32 - 0
webApp/LICENSE.md

@@ -0,0 +1,32 @@
+The Feehi CMS is free software. It is released under the terms of
+the following BSD License.
+
+Copyright © 2016 LLC (http://www.feehi.com)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ * Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in
+   the documentation and/or other materials provided with the
+   distribution.
+ * Neither the name of Yii Software LLC nor the names of its
+   contributors may be used to endorse or promote products derived
+   from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.

+ 152 - 0
webApp/README.md

@@ -0,0 +1,152 @@
+FeehiCMS  __[(English)](docs/running_screenshot/README_EN.md)__  首款编写单元测试、功能测试、验收测试的yii2开源系统
+===============================
+
+基于yii2的CMS系统,运行环境与yii2(php>=5.4)一致。FeehiCMS旨在为yii2爱好者提供一个基础功能稳定完善的系统,使开发者更专注于业务功能开发。
+FeehiCMS没有对yii2做任何的修改、封装,但是把yii2的一些优秀特性几乎都用在了FeehiCMS上,虽提供文档,
+但FeehiCMS提倡简洁、快速上手,基于FeehiCMS开发可以无需文档,反倒FeehiCMS为yii2文档提供了最好的实例
+
+[![Latest Stable Version](https://poser.pugx.org/feehi/cms/v/stable)](https://packagist.org/packages/feehi/cms)
+[![License](https://poser.pugx.org/feehi/cms/license)](https://packagist.org/packages/feehi/cms)
+[![Build Status](https://www.travis-ci.org/liufee/cms.svg?branch=master)](https://www.travis-ci.org/liufee/cms)
+
+
+演示站点
+-------
+演示站点后台   **用户名:feehicms 密码123456**
+* 后台 [http://demo.cms.feehi.com/admin](http://demo.cms.feehi.com/admin)
+* 前台 [http://demo.cms.feehi.com](http://demo.cms.feehi.com/)
+* api [http://demo.cms.feehi.com/api/articles](http://demo.cms.feehi.com/api/articles)
+
+
+[更新记录](docs/UPGRADING.md)
+-------
+
+
+帮助
+---------------
+1. 开发文档[http://doc.feehi.com](http://doc.feehi.com)
+
+2. QQ群 936448696
+
+3. 微信 <br> ![微信](http://img-1251086492.cosgz.myqcloud.com/github/wechat.png)
+
+4. Email job@feehi.com
+
+5. [bug反馈](http://www.github.com/liufee/cms/issues)
+
+
+功能
+---------------
+ * 多语言
+ * 单元测试
+ * 功能测试
+ * 验收测试
+ * RBAC权限管理
+ * restful api
+ * 文章管理 
+ * 操作日志
+ * 适配手机
+ 
+ FeehiCMS提供完备的web系统基础通用功能,包括前后台菜单管理,文章标签,广告,banner,缓存,网站设置,seo设置,邮件设置,分类管理,单页...
+ 
+ 
+使用Docker
+-------
+1.下载镜像
+```bash
+    $ docker pull registry.cn-hangzhou.aliyuncs.com/feehi/cms #FQ后建议直接使用docker pull feehi/cms
+```
+    
+2.创建容器
+```bash
+    $ docker run --name feehicms -h feehicms -itd -v /path/to/data:/data -e DBDSN=sqlite:/data/feehi.db -e TablePrefix=feehi_ -e AdminUsername=admin -e AdminPassword=123456 -p 8080:80 feehi/cms
+```
+以上命令将会自动初始化FeehiCMS,并导入数据库(默认数据库为sqlite)  
+如果需要更使用其他数据库,比如mysql,执行:  
+```bash
+    $ docker run --name feehicms -h feehicms -itd -e DBDSN=mysql:host=mysql-ip;dbname=feehi -e DBUser=dbuser -e DBPassword=dbpassword -e TablePrefix=feehi_ -e AdminUsername=admin -e AdminPassword=123456 -p 8080:80 feehi/cms
+```
+如果需要使用postgresql则将DBDSN改为pgsql:host=pgsql-ip  
+  
+也可以仅初始化FeehiCMS,然后通过web在线安装 
+```bash
+    $ docker run --name feehicms -h feehicms -itd -p 8080:80 feehi/cms -o start
+```
+然后访问http://ip:port/install.php,根据提示选择数据库类型,填写数据库用户名、数据库密码、后台管理员用户名、密码完成安装。  
+  
+  
+以上方式启动的容器只能用作开发环境,容器启动命令最终调用为php -S 0.0.0.0:80,如果用作production,可以执行
+```bash
+    $ docker run --name feehicms -h feehicms -itd -p 8080:80 feehi/cms -m start
+```
+容器将启动php-fpm,并监听9000端口,配合nginx使用。nginx配置大致为
+```bash
+    location ~ \.php$ {
+        ...
+        fastcgi_pass fpm-ip:9000;
+        fastcgi_param  SCRIPT_FILENAME  /usr/local/feehicms/frontend/web$fastcgi_script_name;
+        ...
+    }
+```
+**因为yii2会生成js/css,以及新上传的文件(图片)需要nginx webroot使用php fpm容器同一个文件夹:/usr/local/feehicms/frontend/web**
+
+
+安装
+---------------
+前置条件: 如未特别说明,本文档已默认您把php命令加入了环境变量,如果您未把php加入环境变量,请把以下命令中的php替换成/path/to/php
+> 无论是使用归档文件还是composer,都有相应阶段让您填入后台管理用户名、密码
+1. 使用归档文件(简单,适合没有yii2经验者)
+    1. 下载FeehiCMS源码 [点击此处下载最新版](http://resource-1251086492.cossh.myqcloud.com/Feehi_CMS.zip)
+    2. 解压到目录 
+    3. 配置web服务器[web服务器配置](docs/WEBSERVER_CONFIG.md)
+    4. 浏览器打开 http://localhost/install.php 按照提示完成安装(若使用php内置web服务a器则地址为 http://localhost:8080/install.php )
+    5. 完成
+    
+2. 使用composer (`推荐使用此方式安装`) 
+     >composer的安装以及国内镜像设置请点击 [此处](https://developer.aliyun.com/composer)
+     
+     >以下命令默认您已全局安装composer,如果您是局部安装的composer:请使用php /path/to/composer.phar来替换以下命令中的composer
+     
+     1. 使用composer创建FeehiCMS项目
+        
+        ```bash
+            $ composer create-project feehi/cms webApp //此命令创建的FeehiCMS项目不能平滑升级新版本(目录结构简单,目前主力维护版本)
+        ```
+     2. 依次执行以下命令初始化yii2框架以及导入数据库
+         ```bash
+         $ cd webApp
+         $ php ./init --env=Development #初始化yii2框架,线上环境请使用--env=Production
+         $ php ./yii migrate/up --interactive=0 #导入FeehiCMS sql数据库,执行此步骤之前请先到common/config/main-local.php修改成正确的数据库配置
+         ```
+     3. 配置web服务器[web服务器配置](docs/WEBSERVER_CONFIG.md)
+     4. 完成
+ 
+ 
+ 
+运行测试
+-------
+1. 仅运行单元测试,功能测试(不需要配置web服务器)
+ ```bash
+    cd /path/to/webApp
+    vendor/bin/codecept run
+ ```
+2. 运行单元测试,功能测试,验收测试(需要配置完web服务器)
+    1. 分别拷贝backend,frontend,api三个目录下的tests/acceptance.suite.yml.example到各自目录,并均重名为acceptance.suite.yml,且均修改里面的url为各自的访问url地址
+    2. 与上(仅运行单元测试,功能测试)命令一致
+
+
+项目展示
+------------
+* [山东城市服务技师学院](http://www.sdcc.edu.cn/)   
+* [优悦娱乐网](http://www.qqyouyue.com/)  
+* [吉安市食品药品监督管理局](http://www.jamsda.gov.cn/)  
+* [完美娱乐](http://www.qqwanmei.com/)  
+* [房产网](http://www.itufang.cn/)
+* [中丞法拍网](http://www.fapaihouse.com/)  
+* [51前途网](http://www.51uit.com/) 
+* [用友财务软件](http://www.myyonyou.cn/) 
+*  ......
+
+
+[运行效果](docs/running_screenshot)
+---------

+ 67 - 0
webApp/api/behaviors/ResponseFormatBehavior.php

@@ -0,0 +1,67 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2018-08-15 23:30
+ */
+namespace api\behaviors;
+
+use Yii;
+use yii\web\Response;
+
+class ResponseFormatBehavior extends \yii\base\Behavior
+{
+
+    public $negotiate = true;
+
+    public $format = Response::FORMAT_JSON;
+
+    /** @var Response $response */
+    private $_response;
+
+    public function events()
+    {
+        return [
+            Response::EVENT_BEFORE_SEND => [$this, 'beforeSend'],
+        ];
+    }
+
+    public function beforeSend()
+    {
+        $this->_response = Yii::$app->getResponse();
+
+        if( !$this->negotiate ){
+            $this->_response->format = $this->format;
+        }else {
+            $this->negotiate();
+        }
+
+    }
+
+    private function negotiate()
+    {
+        $acceptTypes = Yii::$app->getRequest()->getAcceptableContentTypes();
+        $acceptTypes = array_keys($acceptTypes);
+        foreach ($acceptTypes as $acceptType){
+            switch ($acceptType) {
+                case "text/plain":
+                    $this->_response->format = Response::FORMAT_RAW;
+                    break;
+                case "application/html":
+                case "text/html":
+                case "*/*":
+                    $this->_response->format = $this->format;
+                    break;
+                case "application/json":
+                case "text/json":
+                    $this->_response->format = Response::FORMAT_JSON;
+                    break;
+                case "application/xml":
+                case "text/xml":
+                    $this->_response->format = Response::FORMAT_XML;
+                    break;
+            }
+        }
+    }
+}

+ 15 - 0
webApp/api/codeception.yml

@@ -0,0 +1,15 @@
+namespace: api\tests
+actor: Tester
+paths:
+    tests: tests
+    log: tests/_output
+    data: tests/_data
+    helpers: tests/_support
+settings:
+    bootstrap: _bootstrap.php
+    colors: true
+    memory_limit: 1024M
+modules:
+    config:
+        Yii2:
+            configFile: 'config/test-local.php'

+ 3 - 0
webApp/api/config/.gitignore

@@ -0,0 +1,3 @@
+main-local.php
+params-local.php
+test-local.php

+ 2 - 0
webApp/api/config/bootstrap.php

@@ -0,0 +1,2 @@
+<?php
+Yii::setAlias('@api', dirname(dirname(__DIR__)) . '/api');

+ 97 - 0
webApp/api/config/main.php

@@ -0,0 +1,97 @@
+<?php
+$params = array_merge(
+    require(__DIR__ . '/../../common/config/params.php'),
+    require(__DIR__ . '/../../common/config/params-local.php'),
+    require(__DIR__ . '/params.php')
+);
+
+return [
+    'id' => 'api',
+    'basePath' => dirname(__DIR__),
+    'bootstrap' => ['log'],
+    'controllerNamespace' => 'api\controllers',
+    'components' => [
+        'user' => [
+            'class' => yii\web\User::className(),
+            'identityClass' => api\models\User::className(),
+            'enableSession' => false,
+        ],
+        'log' => [//此项具体详细配置,请访问http://wiki.feehi.com/index.php?title=Yii2_log
+            'traceLevel' => YII_DEBUG ? 3 : 0,
+            'targets' => [
+                [
+                    'class' => yii\log\FileTarget::className(),//当触发levels配置的错误级别时,保存到日志文件
+                    'levels' => ['error', 'warning'],
+                    'logFile' => '@runtime/logs/'.date('Y/m/d') . '.log',
+                ],
+                [
+                    'class' => yii\log\EmailTarget::className(),//当触发levels配置的错误级别时,发送到此些邮箱(请改成自己的邮箱)
+                    'levels' => ['error', 'warning'],
+                    /*'categories' => [//默认匹配所有分类。启用此项后,仅匹配数组中的分类信息会触发邮件提醒(白名单)
+                        'yii\db\*',
+                        'yii\web\HttpException:*',
+                    ],*/
+                    'except' => [//以下配置,除了匹配数组中的分类信息都会触发邮件提醒(黑名单)
+                        'yii\web\HttpException:404',
+                        'yii\web\HttpException:403',
+                        'yii\debug\Module::checkAccess',
+                    ],
+                    'message' => [
+                        'to' => ['admin@feehi.com', 'liufee@126.com'],
+                        'subject' => '来自 Feehi CMS api的新日志消息',
+                    ],
+                ],
+            ],
+        ],
+        'cache' => [
+            'class' => yii\caching\DummyCache::className(),
+            'keyPrefix' => 'api',       // 唯一键前缀
+        ],
+        'urlManager' => [
+            'enablePrettyUrl' => true,
+            'enableStrictParsing' => true,
+            'showScriptName' => false,
+            'rules' => [
+                '' => 'site/index',
+                'login' => 'site/login',
+                'v1/login' => 'v1/site/login',
+                'register' => 'site/register',
+                'v1/register' => 'v1/site/register',
+                'v1' => 'v1/site/index',
+                [
+                    'class' => yii\rest\UrlRule::className(),
+                    'controller' => ['user', 'article', 'paid'],//通过/users,/user/1,/paid/info访问
+                    /*'extraPatterns' => [
+                        'GET search' => 'search',
+                    ],*/
+                ],
+                [
+                    'class' => yii\rest\UrlRule::className(),//v1版本路由,通过/v1/users,/v1/user/1,/v1/paid/info...访问
+                    'controller' => ['v1/site', 'v1/user', 'v1/article', 'v1/paid'],
+                ],
+                '<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
+                '<version:v\d+>/<controller:\w+>/<action:\w+>'=>'<version>/<controller>/<action>',
+            ],
+        ],
+        'request' => [
+            'parsers' => [
+                'application/json' => 'yii\web\JsonParser',
+                'text/json' => 'yii\web\JsonParser',
+            ],
+            'enableCsrfValidation' => false,
+            'enableCookieValidation' => false,
+        ],
+        'response' => [
+            'format' => null,
+            'as format' => [
+                'class' => api\behaviors\ResponseFormatBehavior::className(),
+            ]
+        ],
+    ],
+    'modules' => [
+        'v1' => [
+            'class' => api\modules\v1\Module::className(),
+        ],
+    ],
+    'params' => $params,
+];

+ 5 - 0
webApp/api/config/params.php

@@ -0,0 +1,5 @@
+<?php
+return [
+    'adminEmail' => 'admin@example.com',
+    'user.apiTokenExpire' => 3*24*3600,
+];

+ 9 - 0
webApp/api/config/test.php

@@ -0,0 +1,9 @@
+<?php
+return [
+    'id' => 'app-api-tests',
+    'components' => [
+        'request' => [
+            'cookieValidationKey' => 'test'
+        ],
+    ],
+];

+ 14 - 0
webApp/api/controllers/ArticleController.php

@@ -0,0 +1,14 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-30 18:10
+ */
+namespace api\controllers;
+
+
+class ArticleController extends \yii\rest\ActiveController
+{
+    public $modelClass = "api\models\Article";
+}

+ 69 - 0
webApp/api/controllers/PaidController.php

@@ -0,0 +1,69 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-05-10 22:27
+ */
+
+namespace api\controllers;
+
+
+use yii\filters\auth\CompositeAuth;
+use yii\filters\auth\HttpBasicAuth;
+use yii\filters\auth\HttpBearerAuth;
+use yii\filters\auth\QueryParamAuth;
+use yii\helpers\ArrayHelper;
+use yii\filters\VerbFilter;
+
+class PaidController extends \yii\rest\Controller
+{
+    public function behaviors()
+    {
+        return ArrayHelper::merge(parent::behaviors(), [
+            'authenticator' => [
+                //使用ComopositeAuth混合认证
+                'class' => CompositeAuth::className(),
+                'optional' => [
+                    'info',//无需access-token的action
+                ],
+                'authMethods' => [
+                    HttpBasicAuth::className(),
+                    HttpBearerAuth::className(),
+                    [
+                        'class' => QueryParamAuth::className(),
+                        'tokenParam' => 'access-token',
+                    ]
+                ]
+            ],
+            'verbs' => [
+              'class' => VerbFilter::className(),
+              'actions' => [
+                  'info'  => ['GET'],
+              ],
+          ],
+        ]);
+    }
+
+    /**
+     * 访问路由 /paids 或/paid/index (p.s如果入口在frontend/web/api/index.php则还需在前加上api)
+     *
+     * @return array
+     */
+    public function actionIndex()
+    {
+        return ["我是需要access-token才能访问的接口"];
+    }
+
+    /**
+     * 访问路由 /paid/info (p.s如果入口在frontend/web/api/index.php则还需在前加上api)
+     *
+     * @return array
+     */
+    public function actionInfo()
+    {
+        return ["我不需要access-token也能访问"];
+    }
+
+
+}

+ 102 - 0
webApp/api/controllers/SiteController.php

@@ -0,0 +1,102 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-30 18:10
+ */
+namespace api\controllers;
+
+use Yii;
+use api\models\form\SignupForm;
+use common\models\User;
+use api\models\form\LoginForm;
+use yii\web\IdentityInterface;
+use yii\web\Response;
+
+class SiteController extends \yii\rest\ActiveController
+{
+    public $modelClass = "common\models\Article";
+
+    public function behaviors()
+    {
+        $behaviors = parent::behaviors();
+        $behaviors['contentNegotiator']['formats']['text/html'] = Response::FORMAT_JSON;//默认浏览器打开返回json
+        return $behaviors;
+    }
+
+    public function actions()
+    {
+        return [];
+    }
+
+    public function verbs()
+    {
+        return [
+            'index' => ['GET', 'HEAD'],
+            'login' => ['POST'],
+            'register' => ['POST'],
+        ];
+    }
+
+    public function actionIndex()
+    {
+        return [
+            "feehi api service"
+        ];
+    }
+
+    /**
+     * 登录
+     *
+     * POST /login
+     * {"username":"xxx", "password":"xxxxxx"}
+     *
+     * @return array
+     */
+    public function actionLogin()
+    {
+        $loginForm = new LoginForm();
+        $loginForm->setAttributes( Yii::$app->getRequest()->post() );
+        if ($user = $loginForm->login()) {
+            if ($user instanceof IdentityInterface) {
+                return [
+                    'accessToken' => $user->access_token,
+                    'expiredAt' => Yii::$app->params['user.apiTokenExpire'] + time()
+                ];
+            } else {
+                return $user->errors;
+            }
+        } else {
+            return $loginForm->errors;
+        }
+
+    }
+
+    /**
+     * 注册
+     *
+     * POST /register
+     * {"username":"xxx", "password":"xxxxxxx", "email":"x@x.com"}
+     *
+     * @return array
+     */
+    public function actionRegister()
+    {
+        $signupForm = new SignupForm();
+        $signupForm->setAttributes( Yii::$app->getRequest()->post() );
+        if( ($user = $signupForm->signup()) instanceof User){
+            return [
+                "success" => true,
+                "username" => $user->username,
+                "email" => $user->email
+            ];
+        }else{
+            return [
+                "success" => false,
+                "error" => $signupForm->getErrors()
+            ];
+        }
+    }
+
+}

+ 57 - 0
webApp/api/controllers/UserController.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-30 18:10
+ */
+namespace api\controllers;
+
+use yii\filters\auth\CompositeAuth;
+use yii\filters\auth\HttpBasicAuth;
+use yii\filters\auth\HttpBearerAuth;
+use yii\filters\auth\QueryParamAuth;
+use yii\helpers\ArrayHelper;
+use yii\filters\VerbFilter;
+
+/**
+ * Class UserController
+ * @package api\controllers
+ *
+ * 调用/register注册用户后,再次调用/login登录获取accessToken,再次访问/users?access-token=xxxxxxx访问
+ */
+class UserController extends \yii\rest\ActiveController
+{
+    public $modelClass = "api\models\User";
+
+    public function behaviors()
+    {
+        return ArrayHelper::merge(parent::behaviors(), [
+            'authenticator' => [
+                //使用ComopositeAuth混合认证
+                'class' => CompositeAuth::className(),
+                'optional' => [
+                    'info',//无需access-token的action
+                ],
+                'authMethods' => [
+                    HttpBasicAuth::className(),
+                    HttpBearerAuth::className(),
+                    [
+                        'class' => QueryParamAuth::className(),
+                        'tokenParam' => 'access-token',
+                    ]
+                ]
+            ],
+            'verbs' => [
+                'class' => VerbFilter::className(),
+                'actions' => [
+                    'info'  => ['GET'],
+                ],
+            ],
+        ]);
+    }
+
+    public function actionInfo(){
+        return ["我是user无需token可以访问的info"];
+    }
+}

+ 15 - 0
webApp/api/fixtures/UserFixture.php

@@ -0,0 +1,15 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-07-28 22:45
+ */
+namespace api\fixtures;
+
+use yii\test\ActiveFixture;
+
+class UserFixture extends ActiveFixture
+{
+    public $modelClass = 'api\models\User';
+}

+ 22 - 0
webApp/api/models/Article.php

@@ -0,0 +1,22 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-30 18:10
+ */
+namespace api\models;
+
+class Article extends \common\models\Article
+{
+    public function fields()
+    {
+        return [
+            'title',
+            "description" => "summary",
+            "content" => function($model){
+                return $model->articleContent->content;
+            }
+        ];
+    }
+}

+ 48 - 0
webApp/api/models/User.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-30 19:04
+ */
+
+namespace api\models;
+
+use Yii;
+use yii\web\IdentityInterface;
+use yii\web\UnauthorizedHttpException;
+
+class User extends \common\models\User implements IdentityInterface
+{
+    public function fields()
+    {
+        $fields = parent::fields();
+        unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token'], $fields['access_token']);
+        return $fields;
+    }
+
+    public static function findIdentityByAccessToken($token, $type = null)
+    {
+        if( !self::accessTokenIsValid($token) ){
+            throw new UnauthorizedHttpException("token格式错误或已过期");
+        }
+        return static::findOne(['access_token' => $token]);
+    }
+
+    public function generateAccessToken()
+    {
+        $this->access_token = Yii::$app->security->generateRandomString(32) . time();
+
+    }
+
+    public static function accessTokenIsValid($token)
+    {
+        if (empty($token)) {
+            return false;
+        }
+        $timestamp = (int) substr($token, -10);
+        $expire = Yii::$app->params['user.apiTokenExpire'];
+        return $timestamp + $expire >= time();
+    }
+
+}

+ 79 - 0
webApp/api/models/form/LoginForm.php

@@ -0,0 +1,79 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-05-10 23:39
+ */
+namespace api\models\form;
+
+use api\models\User;
+
+    /**
+     * Login form
+     */
+class LoginForm extends \yii\base\Model
+{
+    public $username;
+    public $password;
+
+    /** @var User */
+    private $_user;
+
+
+    public function rules()
+    {
+        return [
+            [['username', 'password'], 'required'],
+            ['password', 'validatePassword'],
+        ];
+    }
+
+
+    public function validatePassword($attribute, $params)
+    {
+        if (!$this->hasErrors()) {
+            $this->_user = $this->getUser();
+            if (!$this->_user) {
+                $this->addError($attribute, '用户名不存在');
+                return false;
+            }
+            if( !$this->_user->validatePassword($this->password) ){
+                $this->addError($attribute, '密码错误');
+                return false;
+            }
+        }
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'username' => '用户名',
+            'password' => '密码',
+        ];
+    }
+
+    public function login()
+    {
+        if ($this->validate()) {
+            $this->_user->generateAccessToken();
+            if( $this->_user->save(false, ['access_token']) ){
+                return $this->_user;
+            }else{
+                return "false";
+            }
+        } else {
+            return null;
+        }
+    }
+
+    protected function getUser()
+    {
+        if ($this->_user === null) {
+            $this->_user = User::findByUsername($this->username);
+        }
+
+        return $this->_user;
+    }
+
+}

+ 74 - 0
webApp/api/models/form/SignupForm.php

@@ -0,0 +1,74 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-05-11 22:26
+ */
+
+namespace api\models\form;
+
+use Yii;
+use common\models\User;
+
+class SignupForm extends \yii\base\Model
+{
+    public $username;
+
+    public $email;
+
+    public $password;
+
+    public function rules()
+    {
+        return [
+            ['username', 'filter', 'filter' => 'trim'],
+            ['username', 'required'],
+            [
+                'username',
+                'unique',
+                'targetClass' => User::className(),
+                'message' => Yii::t('app', 'This username has already been taken')
+            ],
+            ['username', 'string', 'min' => 2, 'max' => 255],
+
+            ['email', 'filter', 'filter' => 'trim'],
+            ['email', 'required'],
+            ['email', 'email'],
+            ['email', 'string', 'max' => 255],
+            [
+                'email',
+                'unique',
+                'targetClass' => User::className(),
+                'message' => Yii::t('app', 'This email address has already been taken')
+            ],
+
+            ['password', 'required'],
+            ['password', 'string', 'min' => 6],
+        ];
+    }
+
+    public function attributeLabels()
+    {
+        return [
+            'username' => Yii::t('app', 'Username'),
+            'email' => Yii::t('app', 'Email'),
+            'password' => Yii::t('app', 'Password'),
+        ];
+    }
+
+    public function signup()
+    {
+        if (! $this->validate()) {
+            return null;
+        }
+
+        $user = new User();
+        $user->username = $this->username;
+        $user->email = $this->email;
+        $user->setPassword($this->password);
+        $user->generateAuthKey();
+
+        return $user->save() ? $user : null;
+    }
+}

+ 24 - 0
webApp/api/modules/v1/Module.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace api\modules\v1;
+
+/**
+ * v1 module definition class
+ */
+class Module extends \yii\base\Module
+{
+    /**
+     * {@inheritdoc}
+     */
+    public $controllerNamespace = 'api\modules\v1\controllers';
+
+    /**
+     * {@inheritdoc}
+     */
+    public function init()
+    {
+        parent::init();
+
+        // custom initialization code goes here
+    }
+}

+ 13 - 0
webApp/api/modules/v1/controllers/ArticleController.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-05-11 17:33
+ */
+
+namespace api\modules\v1\controllers;
+
+class ArticleController extends \api\controllers\ArticleController
+{
+}

+ 61 - 0
webApp/api/modules/v1/controllers/PaidController.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace api\modules\v1\controllers;
+
+use yii\filters\auth\CompositeAuth;
+use yii\filters\auth\HttpBasicAuth;
+use yii\filters\auth\HttpBearerAuth;
+use yii\filters\auth\QueryParamAuth;
+use yii\helpers\ArrayHelper;
+use yii\filters\VerbFilter;
+
+class PaidController extends \yii\rest\Controller
+{
+    public function behaviors()
+    {
+        return ArrayHelper::merge(parent::behaviors(), [
+            'authenticator' => [
+                //使用ComopositeAuth混合认证
+                'class' => CompositeAuth::className(),
+                'optional' => [
+                    'info',//无需access-token的action
+                ],
+                'authMethods' => [
+                    HttpBasicAuth::className(),
+                    HttpBearerAuth::className(),
+                    [
+                        'class' => QueryParamAuth::className(),
+                        'tokenParam' => 'access-token',
+                    ]
+                ]
+            ],
+            'verbs' => [
+                'class' => VerbFilter::className(),
+                'actions' => [
+                    'info' => ['GET'],
+                ],
+            ],
+        ]);
+    }
+
+    /**
+     * 访问路由 /v1/paids 或/v1/paid/index (p.s如果入口在frontend/web/api/index.php则还需在前加上api)
+     *
+     * @return array
+     */
+    public function actionIndex()
+    {
+        return ["我是v1 paid/index 需要access-token才能访问的接口"];
+    }
+
+    /**
+     * 访问路由 /v1/paid/info (p.s如果入口在frontend/web/api/index.php则还需在前加上api)
+     *
+     * @return array
+     */
+    public function actionInfo()
+    {
+        return ["我是v1 paid/info 我不需要access-token也能访问"];
+    }
+
+}

+ 13 - 0
webApp/api/modules/v1/controllers/SiteController.php

@@ -0,0 +1,13 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-05-11 18:02
+ */
+
+namespace api\modules\v1\controllers;
+
+class SiteController extends \api\controllers\SiteController
+{
+}

+ 14 - 0
webApp/api/modules/v1/controllers/UserController.php

@@ -0,0 +1,14 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-05-11 22:21
+ */
+
+namespace api\modules\v1\controllers;
+
+class UserController extends \api\controllers\UserController
+{
+
+}

+ 2 - 0
webApp/api/runtime/.gitignore

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

+ 11 - 0
webApp/api/tests/_bootstrap.php

@@ -0,0 +1,11 @@
+<?php
+use api\tests\AcceptanceTester;
+
+defined('YII_DEBUG') or define('YII_DEBUG', true);
+defined('YII_ENV') or define('YII_ENV', 'test');
+defined('YII_APP_BASE_PATH') or define('YII_APP_BASE_PATH', __DIR__.'/../../');
+
+require_once YII_APP_BASE_PATH . '/vendor/autoload.php';
+require_once YII_APP_BASE_PATH . '/vendor/yiisoft/yii2/Yii.php';
+require_once YII_APP_BASE_PATH . '/common/config/bootstrap.php';
+require_once __DIR__ . '/../config/bootstrap.php';

+ 14 - 0
webApp/api/tests/_data/login_data.php

@@ -0,0 +1,14 @@
+<?php
+return [
+    [
+        'username' => 'feehi',
+        'auth_key' => 'WczrLDgopD8-J62oqo6bMeMXyOJFFuCJ',
+        // password_0
+        'password_hash' => '$2y$13$4fdb32jM985NvzGX0k/a/uSFQMUcKwle.VCOqz9DBBTqdu4PXbMaq',
+        'password_reset_token' => 'RkD_Jw0_8HEedzLk7MM-ZKEFfYR7VbMr_1392559490',
+        'created_at' => '1392559490',
+        'updated_at' => '1392559490',
+        'email' => 'feehi@feehi.com',
+        'access_token' => "feehiaccesstoken"
+    ],
+];

+ 2 - 0
webApp/api/tests/_output/.gitignore

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

+ 1 - 0
webApp/api/tests/_support/.gitignore

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

+ 26 - 0
webApp/api/tests/_support/AcceptanceTester.php

@@ -0,0 +1,26 @@
+<?php
+namespace api\tests;
+
+/**
+ * Inherited Methods
+ * @method void wantToTest($text)
+ * @method void wantTo($text)
+ * @method void execute($callable)
+ * @method void expectTo($prediction)
+ * @method void expect($prediction)
+ * @method void amGoingTo($argumentation)
+ * @method void am($role)
+ * @method void lookForwardTo($achieveValue)
+ * @method void comment($description)
+ * @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
+ *
+ * @SuppressWarnings(PHPMD)
+*/
+class AcceptanceTester extends \Codeception\Actor
+{
+    use _generated\AcceptanceTesterActions;
+
+   /**
+    * Define custom actions here
+    */
+}

+ 33 - 0
webApp/api/tests/_support/FunctionalTester.php

@@ -0,0 +1,33 @@
+<?php
+namespace api\tests;
+
+/**
+ * Inherited Methods
+ * @method void wantToTest($text)
+ * @method void wantTo($text)
+ * @method void execute($callable)
+ * @method void expectTo($prediction)
+ * @method void expect($prediction)
+ * @method void amGoingTo($argumentation)
+ * @method void am($role)
+ * @method void lookForwardTo($achieveValue)
+ * @method void comment($description)
+ * @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
+ *
+ * @SuppressWarnings(PHPMD)
+ */
+class FunctionalTester extends \Codeception\Actor
+{
+    use _generated\FunctionalTesterActions;
+
+
+    public function seeValidationError($message)
+    {
+        $this->see($message, '.help-block');
+    }
+
+    public function dontSeeValidationError($message)
+    {
+        $this->dontSee($message, '.help-block');
+    }
+}

+ 25 - 0
webApp/api/tests/_support/UnitTester.php

@@ -0,0 +1,25 @@
+<?php
+namespace api\tests;
+
+/**
+ * Inherited Methods
+ * @method void wantToTest($text)
+ * @method void wantTo($text)
+ * @method void execute($callable)
+ * @method void expectTo($prediction)
+ * @method void expect($prediction)
+ * @method void amGoingTo($argumentation)
+ * @method void am($role)
+ * @method void lookForwardTo($achieveValue)
+ * @method void comment($description)
+ * @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
+ *
+ * @SuppressWarnings(PHPMD)
+ */
+class UnitTester extends \Codeception\Actor
+{
+    use _generated\UnitTesterActions;
+   /**
+    * Define custom actions here
+    */
+}

+ 8 - 0
webApp/api/tests/acceptance.suite.yml.example

@@ -0,0 +1,8 @@
+class_name: AcceptanceTester
+modules:
+  enabled:
+  - REST:
+      depends: PhpBrowser
+      url: http://localhost:8888/apitest
+  - Yii2:
+      part: init

+ 28 - 0
webApp/api/tests/acceptance/ArticleCest.php

@@ -0,0 +1,28 @@
+<?php
+namespace api\tests;
+
+use api\tests\AcceptanceTester;
+
+class ArticleCest
+{
+    public function _before(AcceptanceTester $I)
+    {
+    }
+
+    public function _after(AcceptanceTester $I)
+    {
+    }
+
+    public function checkIndex(AcceptanceTester $I)
+    {
+        $I->sendGET('/articles');
+        $I->haveHttpHeader("X-Pagination-Current-Page", 1);
+    }
+
+    public function checkView(AcceptanceTester $I)
+    {
+        $I->sendGET('/articles/1');
+        $I->seeResponseContains("title");
+        $I->seeResponseContains("description");
+    }
+}

+ 26 - 0
webApp/api/tests/acceptance/_bootstrap.php

@@ -0,0 +1,26 @@
+<?php
+
+use api\tests\AcceptanceTester;
+
+/**
+ * Here you can initialize variables via \Codeception\Util\Fixtures class
+ * to store data in global array and use it in Cepts.
+ *
+ * ```php
+ * // Here _bootstrap.php
+ * \Codeception\Util\Fixtures::add('user1', ['name' => 'davert']);
+ * ```
+ *
+ * In Cept
+ *
+ * ```php
+ * \Codeception\Util\Fixtures::get('user1');
+ * ```
+ */
+
+function getToken(AcceptanceTester $I){
+    $I->sendPOST("/login", ["username"=>"feehi", "password"=>123456]);
+    $I->canSeeResponseContains("accessToken");
+    $dt = $I->grabResponse();
+    return json_decode($dt, true)['accessToken'];
+}

+ 28 - 0
webApp/api/tests/acceptance/modules/v1/V1ArticleCest.php

@@ -0,0 +1,28 @@
+<?php
+namespace api\tests;
+
+use api\tests\AcceptanceTester;
+
+class V1ArticleCest
+{
+    public function _before(AcceptanceTester $I)
+    {
+    }
+
+    public function _after(AcceptanceTester $I)
+    {
+    }
+
+    public function checkIndex(AcceptanceTester $I)
+    {
+        $I->sendGET('/v1/articles');
+        $I->haveHttpHeader("X-Pagination-Current-Page", 1);
+    }
+
+    public function checkView(AcceptanceTester $I)
+    {
+        $I->sendGET('/v1/articles/1');
+        $I->seeResponseContains("title");
+        $I->seeResponseContains("description");
+    }
+}

+ 8 - 0
webApp/api/tests/functional.suite.yml.example

@@ -0,0 +1,8 @@
+class_name: FunctionalTester
+modules:
+  enabled:
+  - REST:
+      depends: PhpBrowser
+      url: http://localhost:8888/apitest
+  - Yii2:
+      part: init

+ 21 - 0
webApp/api/tests/functional/ArticleCest.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace api\tests\functional;
+
+use api\tests\FunctionalTester;
+
+class ArticleCest
+{
+    public function checkIndex(FunctionalTester $I)
+    {
+        $I->sendGET('/articles');
+        $I->haveHttpHeader("X-Pagination-Current-Page", 1);
+    }
+
+    public function checkView(FunctionalTester $I)
+    {
+        $I->sendGET('/articles/1');
+        $I->canSeeResponseContains("title");
+        $I->canSeeResponseContains("description");
+    }
+}

+ 49 - 0
webApp/api/tests/functional/PaidCest.php

@@ -0,0 +1,49 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-08-01 23:56
+ */
+
+namespace api\tests\functional;
+
+use api\fixtures\UserFixture;
+use api\tests\FunctionalTester;
+
+class PaidCest
+{
+    public function _fixtures()
+    {
+        return [
+            'user' => [
+                'class' => UserFixture::className(),
+                'dataFile' => codecept_data_dir() . 'login_data.php'
+            ]
+        ];
+    }
+
+    public function _after(FunctionalTester $I)
+    {
+    }
+
+    public function _before(FunctionalTester $I)
+    {
+       $this->token = getTokenFunctional($I);
+    }
+
+    public function checkIndex(FunctionalTester $I)
+    {
+        $I->sendGET('/paid/index');
+        $I->canSeeResponseContains('"status":401');
+
+        $I->sendGET('/paid/index?access-token=' . $this->token);
+        $I->canSeeResponseContains("我是需要access-token才能访问的接口");
+    }
+
+    public function checkInfo(FunctionalTester $I)
+    {
+        $I->sendGET('/paid/info');
+        $I->canSeeResponseContains('我不需要access-token也能访问');
+    }
+}

+ 63 - 0
webApp/api/tests/functional/SiteCest.php

@@ -0,0 +1,63 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-08-02 00:09
+ */
+
+namespace api\tests\functional;
+
+
+use api\fixtures\UserFixture;
+use api\tests\FunctionalTester;
+
+class SiteCest
+{
+    public function _fixtures()
+    {
+        return [
+            'user' => [
+                'class' => UserFixture::className(),
+                'dataFile' => codecept_data_dir() . 'login_data.php'
+            ]
+        ];
+    }
+
+    public function _after(FunctionalTester $I)
+    {
+    }
+
+    public function _before(FunctionalTester $I)
+    {
+        $this->token = getTokenFunctional($I);
+    }
+
+    public function checkLogin(FunctionalTester $I){
+
+        $I->sendPOST("/login", ["username"=>"feehi", "password"=>123456]);
+        $I->canSeeResponseContains("accessToken");
+    }
+
+    public function checkIndex(FunctionalTester $I)
+    {
+        $I->sendGET('/');
+        $I->canSeeResponseContains('feehi api service');
+    }
+
+    public function checkRegister(FunctionalTester $I)
+    {
+
+        $I->sendPOST("/register", ["username"=>"a", "email"=>"afeehi.com", "password"=>123456]);
+        $I->seeResponseContains('"success":false');
+
+        $I->sendPOST("/register", ["username"=>"aa", "email"=>"afeehi.com", "password"=>""]);
+        $I->seeResponseContains('"success":false');
+
+        $I->sendPOST("/register", ["username"=>"aa", "email"=>"afeehi.com", "password"=>""]);
+        $I->seeResponseContains('"success":false');
+
+        $I->sendPOST("/register", ["username"=>uniqid(), "email"=>"a@" . uniqid() . ".com", "password"=>123456]);
+        $I->seeResponseContains('"success":true');
+    }
+}

+ 70 - 0
webApp/api/tests/functional/UserCest.php

@@ -0,0 +1,70 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-08-02 00:10
+ */
+
+namespace api\tests\functional;
+
+
+use api\fixtures\UserFixture;
+use api\tests\FunctionalTester;
+
+class UserCest
+{
+    public function _fixtures()
+    {
+        return [
+            'user' => [
+                'class' => UserFixture::className(),
+                'dataFile' => codecept_data_dir() . 'login_data.php'
+            ]
+        ];
+    }
+
+    public function _after(FunctionalTester $I)
+    {
+    }
+
+
+    public function _before(FunctionalTester $I)
+    {
+        $this->token = getTokenFunctional($I);
+    }
+
+    public function checkUsers(FunctionalTester $I)
+    {
+        $I->sendGET('/users?access-token=' . $this->token);
+        $I->canSeeResponseContains('feehi@feehi.com');
+    }
+
+    public function checkCreateUser(FunctionalTester $I)
+    {
+        $I->sendPOST('/users?access-token=' . $this->token, [
+            "username" => "feehi123",
+            "password" => "123456",
+            "email" => 'admin@feehi.com'
+        ]);
+        $I->canSeeResponseContains('admin@feehi.com');
+    }
+
+    public function checkUser(FunctionalTester $I)
+    {
+        $I->sendGET('/users/1?access-token=' . $this->token);
+        $I->canSeeResponseContains("feehi@feehi.com");
+    }
+
+    public function checkDeleteUser(FunctionalTester $I)
+    {
+        $I->sendDELETE("/users/1?access-token=" . $this->token);
+        $I->canSeeResponseCodeIs(204);
+    }
+
+    public function checkInfo(FunctionalTester $I)
+    {
+        $I->sendGET('/user/info');
+        $I->canSeeResponseContains('我是user无需token可以访问的info');
+    }
+}

+ 27 - 0
webApp/api/tests/functional/_bootstrap.php

@@ -0,0 +1,27 @@
+<?php
+
+use api\tests\FunctionalTester;
+
+/**
+ * Here you can initialize variables via \Codeception\Util\Fixtures class
+ * to store data in global array and use it in Cests.
+ *
+ * ```php
+ * // Here _bootstrap.php
+ * \Codeception\Util\Fixtures::add('user1', ['name' => 'davert']);
+ * ```
+ *
+ * In Cests
+ *
+ * ```php
+ * \Codeception\Util\Fixtures::get('user1');
+ * ```
+ */
+
+function getTokenFunctional(FunctionalTester $I){
+    $I->sendPOST("/login", ["username"=>"feehi", "password"=>123456]);
+    $I->canSeeResponseContains("accessToken");
+    $dt = $I->grabResponse();
+    $token = json_decode($dt, true)['accessToken'];
+    return $token;
+}

+ 28 - 0
webApp/api/tests/functional/modules/v1/V1ArticleCest.php

@@ -0,0 +1,28 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-08-02 00:17
+ */
+
+namespace api\tests\functional;
+
+use api\tests\FunctionalTester;
+
+
+class V1ArticleCest
+{
+    public function checkIndex(FunctionalTester $I)
+    {
+        $I->sendGET('/v1/articles');
+        $I->haveHttpHeader("X-Pagination-Current-Page", 1);
+    }
+
+    public function checkView(FunctionalTester $I)
+    {
+        $I->sendGET('/v1/articles/1');
+        $I->seeResponseContains("title");
+        $I->seeResponseContains("description");
+    }
+}

+ 50 - 0
webApp/api/tests/functional/modules/v1/V1PaidCest.php

@@ -0,0 +1,50 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-08-02 00:19
+ */
+
+namespace api\tests\functional;
+
+
+use api\fixtures\UserFixture;
+use api\tests\FunctionalTester;
+
+class V1PaidCest
+{
+    public function _fixtures()
+    {
+        return [
+            'user' => [
+                'class' => UserFixture::className(),
+                'dataFile' => codecept_data_dir() . 'login_data.php'
+            ]
+        ];
+    }
+
+    public function _after(FunctionalTester $I)
+    {
+    }
+
+    public function _before(FunctionalTester $I)
+    {
+        $this->token = getTokenFunctional($I);
+    }
+
+    public function checkIndex(FunctionalTester $I)
+    {
+        $I->sendGET('/v1/paid/index');
+        $I->canSeeResponseContains('"status":401');
+
+        $I->sendGET('/v1/paid/index?access-token=' . $this->token);
+        $I->canSeeResponseContains("我是v1 paid/index 需要access-token才能访问的接口");
+    }
+
+    public function checkInfo(FunctionalTester $I)
+    {
+        $I->sendGET('/v1/paid/info');
+        $I->canSeeResponseContains('我是v1 paid/info 我不需要access-token也能访问');
+    }
+}

+ 64 - 0
webApp/api/tests/functional/modules/v1/V1SiteCest.php

@@ -0,0 +1,64 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-08-02 00:20
+ */
+
+namespace api\tests\functional;
+
+use api\fixtures\UserFixture;
+use api\tests\FunctionalTester;
+
+
+class V1SiteCest
+{
+    public function _fixtures()
+    {
+        return [
+            'user' => [
+                'class' => UserFixture::className(),
+                'dataFile' => codecept_data_dir() . 'login_data.php'
+            ]
+        ];
+    }
+
+    public function _after(FunctionalTester $I)
+    {
+    }
+
+    public function _before(FunctionalTester $I)
+    {
+        $this->token = getTokenFunctional($I);
+    }
+
+    public function checkLogin(FunctionalTester $I){
+
+        $I->sendPOST("/v1/login", ["username"=>"feehi", "password"=>123456]);
+        $I->canSeeResponseContains("accessToken");
+    }
+
+    public function checkIndex(FunctionalTester $I)
+    {
+        $I->sendGET('/v1');
+        $I->canSeeResponseContains('feehi api service');
+    }
+
+    public function checkRegister(FunctionalTester $I)
+    {
+
+        $I->sendPOST("/v1/register", ["username"=>"a", "email"=>"afeehi.com", "password"=>123456]);
+        $I->seeResponseContains('"success":false');
+
+        $I->sendPOST("/v1/register", ["username"=>"aa", "email"=>"afeehi.com", "password"=>""]);
+        $I->seeResponseContains('"success":false');
+
+        $I->sendPOST("/v1/register", ["username"=>"aa", "email"=>"afeehi.com", "password"=>""]);
+        $I->seeResponseContains('"success":false');
+
+        $I->sendPOST("/v1/register", ["username"=>uniqid(), "email"=>"a@" . uniqid() . ".com", "password"=>123456]);
+        $I->seeResponseContains('"success":true');
+    }
+
+}

+ 69 - 0
webApp/api/tests/functional/modules/v1/V1UserCest.php

@@ -0,0 +1,69 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-08-02 00:21
+ */
+
+namespace api\tests\functional;
+
+use api\fixtures\UserFixture;
+use api\tests\FunctionalTester;
+
+
+class V1UserCest
+{
+    public function _fixtures()
+    {
+        return [
+            'user' => [
+                'class' => UserFixture::className(),
+                'dataFile' => codecept_data_dir() . 'login_data.php'
+            ]
+        ];
+    }
+
+    public function _after(FunctionalTester $I)
+    {
+    }
+
+    public function _before(FunctionalTester $I)
+    {
+        $this->token = getTokenFunctional($I);
+    }
+
+    public function checkUsers(FunctionalTester $I)
+    {
+        $I->sendGET('/v1/users?access-token=' . $this->token);
+        $I->canSeeResponseContains('feehi@feehi.com');
+    }
+
+    public function checkCreateUser(FunctionalTester $I)
+    {
+        $I->sendPOST('/v1/users?access-token=' . $this->token, [
+            "username" => "feehi123",
+            "password" => "123456",
+            "email" => 'admin@feehi.com'
+        ]);
+        $I->canSeeResponseContains('admin@feehi.com');
+    }
+
+    public function checkUser(FunctionalTester $I)
+    {
+        $I->sendGET('/v1/users/1?access-token=' . $this->token);
+        $I->canSeeResponseContains("feehi@feehi.com");
+    }
+
+    public function checkDeleteUser(FunctionalTester $I)
+    {
+        $I->sendDELETE("/v1/users/1?access-token=" . $this->token);
+        $I->canSeeResponseCodeIs(204);
+    }
+
+    public function checkInfo(FunctionalTester $I)
+    {
+        $I->sendGET('/v1/user/info');
+        $I->canSeeResponseContains('我是user无需token可以访问的info');
+    }
+}

+ 6 - 0
webApp/api/tests/unit.suite.yml

@@ -0,0 +1,6 @@
+class_name: UnitTester
+modules:
+    enabled:
+        - Yii2:
+            part: [orm, email, fixtures]
+        - Asserts

+ 16 - 0
webApp/api/tests/unit/_bootstrap.php

@@ -0,0 +1,16 @@
+<?php
+/**
+ * Here you can initialize variables via \Codeception\Util\Fixtures class
+ * to store data in global array and use it in Tests.
+ *
+ * ```php
+ * // Here _bootstrap.php
+ * \Codeception\Util\Fixtures::add('user1', ['name' => 'davert']);
+ * ```
+ *
+ * In Tests
+ *
+ * ```php
+ * \Codeception\Util\Fixtures::get('user1');
+ * ```
+ */

+ 20 - 0
webApp/api/tests/unit/models/ArticleCestTest.php

@@ -0,0 +1,20 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-08-02 00:25
+ */
+
+namespace api\tests\unit\models;
+
+use api\models\Article;
+
+class ArticleCestTest extends \Codeception\Test\Unit
+{
+    public function testFields()
+    {
+        $model = new Article();
+        expect("api article model fields should have title", $model->fields())->contains("title");
+    }
+}

+ 47 - 0
webApp/api/tests/unit/models/UserCestTest.php

@@ -0,0 +1,47 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-08-02 00:31
+ */
+
+namespace api\tests\unit\models;
+
+
+use api\fixtures\UserFixture;
+use api\models\User;
+
+class UserCestTest extends \Codeception\Test\Unit
+{
+    /**
+     * @var \backend\tests\UnitTester
+     */
+    protected $tester;
+
+    public function _fixtures()
+    {
+        return [
+            'user' => [
+                'class' => UserFixture::className(),
+                'dataFile' => codecept_data_dir() . 'login_data.php'
+            ]
+        ];
+    }
+
+    protected function _before()
+    {
+
+    }
+
+    protected function _after()
+    {
+    }
+
+
+    public function testGenerateAccessToken(){
+        $user = new User();
+        $user->generateAccessToken();
+        expect('generate token success', $user->access_token)->notEmpty();
+    }
+}

+ 3 - 0
webApp/api/web/.gitignore

@@ -0,0 +1,3 @@
+/index.php
+/index-test.php
+robots.txt

+ 9 - 0
webApp/api/web/.htaccess

@@ -0,0 +1,9 @@
+<IfModule rewrite_module>
+Options +FollowSymLinks
+IndexIgnore */*
+RewriteEngine On
+ 
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule . index.php
+</IfModule>

+ 2 - 0
webApp/api/web/assets/.gitignore

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

BIN
webApp/api/web/favicon.ico


+ 2 - 0
webApp/api/web/uploads/.gitignore

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

+ 152 - 0
webApp/backend/actions/CreateAction.php

@@ -0,0 +1,152 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-13 00:06
+ */
+namespace backend\actions;
+
+use Yii;
+use Closure;
+use stdClass;
+use backend\actions\helpers\Helper;
+use yii\base\Exception;
+use yii\web\Response;
+use yii\web\UnprocessableEntityHttpException;
+
+/**
+ * backend create
+ * if create occurs error, must return model or error string for display error. return true for successful create.
+ * if GET request, the createResult be a null, POST request the createResult is the value of doCreate closure returns.
+ *
+ * Class CreateAction
+ * @package backend\actions
+ */
+class CreateAction extends \yii\base\Action
+{
+
+    const CREATE_REFERER = "_create_referer";
+
+    /**
+     * @var string|array primary key(s) name
+     */
+    public $primaryKeyIdentity = null;
+
+    /**
+     * @var string primary keys(s) from (GET or POST)
+     */
+    public $primaryKeyFromMethod = "GET";
+
+    /**
+     * @var array|\Closure variables will assigned to view
+     */
+    public $data = [];
+
+    /** @var  string|array success create redirect to url (this value will pass yii::$app->controller->redirect($this->successRedirect) to generate url), default is (GET request) referer url
+     */
+    public $successRedirect = null;
+
+    /**
+     * @var Closure the real create logic, usually will call service layer create method
+     */
+    public $doCreate;
+
+    /**
+     * @var string after success doUpdate tips message showed in page top
+     */
+    public $successTipsMessage = "success";
+
+    /** @var string view template file,default is action id  */
+    public $viewFile = null;
+
+
+    public function init()
+    {
+        parent::init();
+        if( $this->successTipsMessage === "success"){
+            $this->successTipsMessage = Yii::t("app", "success");
+        }
+    }
+
+    /**
+     * create
+     *
+     * @return mixed
+     * @throws UnprocessableEntityHttpException
+     * @throws Exception
+     */
+    public function run()
+    {
+        //according assigned HTTP Method and param name to get value. will be passed to $this->doCreate closure and $this->data closure.Often there is no need to get value on create, so default value is null.
+        $primaryKeys = Helper::getPrimaryKeys($this->primaryKeyIdentity, $this->primaryKeyFromMethod);
+
+        if (Yii::$app->getRequest()->getIsPost()) {//if POST request will execute doCreate.
+            if (!$this->doCreate instanceof Closure) {
+                throw new Exception(__CLASS__ . "::doCreate must be closure");
+            }
+
+            $postData = Yii::$app->getRequest()->post();
+
+            $createData = [];//doCreate closure formal parameter(translate: 传递给doCreate必包的形参)
+
+            if( !empty($primaryKeys) ){
+                foreach ($primaryKeys as $primaryKey) {
+                    array_push($createData, $primaryKey);
+                }
+            }
+
+            array_push($createData, $postData, $this);
+
+            /**
+             * doCreate(primaryKey1, primaryKey2 ..., $_POST, CreateAction)
+             */
+            $createResult = call_user_func_array($this->doCreate, $createData);//call doCreate closure
+
+            if (Yii::$app->getRequest()->getIsAjax()) { //ajax
+                Yii::$app->getResponse()->format = Response::FORMAT_JSON;
+                if ($createResult === true) {//only $createResult is true represent create success
+                    return ['code' => 0, 'msg' => 'success', 'data' => new stdClass()];
+                } else {
+                    throw new UnprocessableEntityHttpException(Helper::getErrorString($createResult));
+                }
+            } else {//not ajax
+                if ($createResult === true) {//only $createResult is true represent create success
+                    Yii::$app->getSession()->setFlash('success', $this->successTipsMessage);
+                    if ($this->successRedirect) return $this->controller->redirect($this->successRedirect);//if $this->successRedirect not empty will redirect to this url
+                    $url = Yii::$app->getSession()->get(self::CREATE_REFERER);
+                    if ($url) return $this->controller->redirect($url);//get an not empty referer will redirect to this url(often, before do create page. also to say: index list page)
+                    return $this->controller->redirect(["index"]);//default is redirect to current controller index action(attention: if current controller has no index action will get a HTTP 404 error)
+                    //if doCreate success will terminated here!!!
+                } else {//besides true, all represent create failed.
+                    Yii::$app->getSession()->setFlash('error', Helper::getErrorString($createResult));//if doCreate error will set a error description string.and continue the current page.
+                }
+            }
+        }
+
+
+        //if GET request or doCreate failed, will display the create page.
+        if (is_array($this->data)) {
+            $data = $this->data;//this data will assigned to view
+        } else if ($this->data instanceof Closure) {
+            $params = [];
+            if( !empty($primaryKeys) ){
+                foreach ($primaryKeys as $primaryKey) {
+                    array_push($params, $primaryKey);
+                }
+            }
+            //GET request just display create page. Only POST request will get a updateResult(returned by doCreate closure)
+            !isset($createResult) && $createResult = null;
+            array_push($params, $createResult, $this);
+            $data = call_user_func_array($this->data, $params);//this data will assigned to view
+        } else {
+            throw new Exception(__CLASS__ . "::data only allows array or closure (with return array)");
+        }
+
+        $this->viewFile === null && $this->viewFile = $this->id;
+
+        Yii::$app->getRequest()->getIsGet() && Yii::$app->getSession()->set(self::CREATE_REFERER, Yii::$app->getRequest()->getReferrer());//set an referer, when success doUpdate may redirect this url
+
+        return $this->controller->render($this->viewFile, $data);
+    }
+}

+ 99 - 0
webApp/backend/actions/DeleteAction.php

@@ -0,0 +1,99 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-13 01:08
+ */
+
+namespace backend\actions;
+
+use Yii;
+use stdClass;
+use Closure;
+use backend\actions\helpers\Helper;
+use yii\base\Exception;
+use yii\web\BadRequestHttpException;
+use yii\web\MethodNotAllowedHttpException;
+use yii\web\Response;
+use yii\web\UnprocessableEntityHttpException;
+
+/**
+ * backend delete
+ * only permit POST request, but can assign value throw query or body for need delete record.
+ *
+ * Class DeleteAction
+ * @package backend\actions
+ */
+class DeleteAction extends \yii\base\Action
+{
+    /**
+     * @var string|array primary key(s) name
+     */
+    public $primaryKeyIdentity = "id";
+
+    /**
+     * @var Closure the real delete logic, usually will call service layer delete method
+     */
+    public $doDelete;
+
+    /**
+     * delete
+     *
+     * @throws BadRequestHttpException
+     * @throws MethodNotAllowedHttpException
+     * @throws UnprocessableEntityHttpException
+     * @throws Exception
+     */
+    public function run()
+    {
+        if (Yii::$app->getRequest()->getIsPost()) {//for safety, delete need POST
+            if( !is_string($this->primaryKeyIdentity) ){
+                throw new Exception(__CLASS__ . "::primaryKeyIdentity only permit string");
+            }
+            $data = Yii::$app->getRequest()->post($this->primaryKeyIdentity, null);
+            if ($data === null) {//不在post参数,则为单个删除
+                $data = Yii::$app->getRequest()->get($this->primaryKeyIdentity, null);
+            }
+
+            if (!$data) {
+                throw new BadRequestHttpException(Yii::t('app', "{$this->primaryKeyIdentity} doesn't exist"));
+            }
+            if( is_string($data) ){
+                if( (strpos($data, "{") === 0 && strpos(strrev($data), "}") === 0) || (strpos($data, "[") === 0 && strpos(strrev($data), "]") === 0) ){
+                    $data = json_decode($data, true);
+                }else{
+                    $data = [$data];
+                }
+            }
+            !isset($data[0]) && $data = [$data];
+
+            $errors = [];
+            foreach ($data as $id){
+                $deleteResult = call_user_func_array($this->doDelete, [$id, $this]);
+                if($deleteResult !== true && $deleteResult !== "" && $deleteResult !== null){
+                    $errors[]= Helper::getErrorString($deleteResult);
+                }
+            }
+
+            if (count($errors) == 0) {
+                if( Yii::$app->getRequest()->getIsAjax() ) {
+                    Yii::$app->getResponse()->format = Response::FORMAT_JSON;
+                    return ['code'=>0, 'msg'=>'success', 'data'=>new stdClass()];
+                }else {
+                    return $this->controller->redirect(Yii::$app->getRequest()->getReferrer());
+                }
+            } else {
+                if( Yii::$app->getRequest()->getIsAjax() ){
+                    Yii::$app->getResponse()->format = Response::FORMAT_JSON;
+                    throw new UnprocessableEntityHttpException(implode("<br>", $errors));
+                }else {
+                    Yii::$app->getSession()->setFlash('error', implode("<br>", $errors));
+                }
+            }
+
+        } else {
+            throw new MethodNotAllowedHttpException(Yii::t('app', "Delete must be POST http method"));
+        }
+    }
+}

+ 112 - 0
webApp/backend/actions/DoAction.php

@@ -0,0 +1,112 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2020-04-03 09:50
+ */
+namespace backend\actions;
+
+use Yii;
+use Closure;
+use stdClass;
+use backend\actions\helpers\Helper;
+use yii\base\Exception;
+use yii\web\Response;
+use yii\web\UnprocessableEntityHttpException;
+
+/**
+ * backend execute action
+ * Often use to for none page display, and only execute action.
+ *
+ * Class DoAction
+ * @package backend\actions
+ */
+class DoAction extends \yii\base\Action
+{
+    /**
+     * @var string|array primary key(s) name
+     */
+    public $primaryKeyIdentity = null;
+
+    /**
+     * @var string primary keys(s) from (GET or POST)
+     */
+    public $primaryKeyFromMethod = "GET";
+
+    /** @var  string|array success do redirect to url (this value will pass yii::$app->controller->redirect($this->successRedirect) to generate url), default is referer url
+     */
+    public $successRedirect = null;
+
+    /**
+     * @var Closure the real do logic
+     */
+    public $do;
+
+    /**
+     * @var string after success doUpdate tips message showed in page top
+     */
+    public $successTipsMessage = "success";
+
+
+    public function init()
+    {
+        parent::init();
+        if( $this->successTipsMessage === "success"){
+            $this->successTipsMessage = Yii::t("app", "success");
+        }
+    }
+
+    /**
+     * do
+     *
+     * @return mixed
+     * @throws UnprocessableEntityHttpException
+     * @throws Exception
+     */
+    public function run()
+    {
+        //according assigned HTTP Method and param name to get value. will be passed to $this->doUpdate closure and $this->data closure.Often use for get value of primary key.
+        $primaryKeys = Helper::getPrimaryKeys($this->primaryKeyIdentity, $this->primaryKeyFromMethod);
+
+        if (!$this->do instanceof Closure) {
+            throw new Exception(__CLASS__ . "::do must be closure");
+        }
+
+        $postData = Yii::$app->getRequest()->post();
+
+        $doData = [];
+
+        if (!empty($primaryKeys)) {
+            foreach ($primaryKeys as $primaryKey) {
+                array_push($doData, $primaryKey);
+            }
+        }
+
+        array_push($doData, $postData, $this);
+
+        /**
+         * do action, function(primaryKey1, primaryKey2 ..., $_POST, DoAction)
+         */
+        $doResult = call_user_func_array($this->do, $doData);//call do closure
+
+        if (Yii::$app->getRequest()->getIsAjax()) { //ajax
+            if ($doResult === true) {//only $doResult is true represent create success
+                Yii::$app->getResponse()->format = Response::FORMAT_JSON;
+                return ['code' => 0, 'msg' => 'success', 'data' => new stdClass()];
+            } else {//not ajax
+                throw new UnprocessableEntityHttpException(Helper::getErrorString($doResult));
+            }
+        } else {
+            if ($doResult === true) {//only $doResult is true represent create success
+                Yii::$app->getSession()->setFlash('success', $this->successTipsMessage);
+                if ($this->successRedirect) return $this->controller->redirect($this->successRedirect);//if $this->successRedirect not empty will redirect to this url
+                $url = Yii::$app->getRequest()->getReferrer();
+                if ($url) return $this->controller->redirect($url);//get an not empty referer will redirect to this url(often, before do page.)
+                return $this->controller->redirect(["index"]);//default is redirect to current controller index action(attention: if current controller has no index action will get a HTTP 404 error)
+            } else {
+                Yii::$app->getSession()->setFlash('error', Helper::getErrorString($doResult));
+            }
+        }
+    }
+}

+ 79 - 0
webApp/backend/actions/IndexAction.php

@@ -0,0 +1,79 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-13 01:00
+ */
+
+namespace backend\actions;
+
+use Yii;
+use Closure;
+use backend\actions\helpers\Helper;
+use yii\base\Exception;
+
+/**
+ * Index list page
+ *
+ * Class IndexAction
+ * @package backend\actions
+ */
+class IndexAction extends \yii\base\Action
+{
+    /**
+     * @var string|array primary key(s) name
+     */
+    public $primaryKeyIdentity = null;
+
+    /**
+     * @var string primary keys(s) from (GET or POST)
+     */
+    public $primaryKeyFromMethod = "GET";
+
+    /**
+     * @var array|\Closure assign to view variables
+     */
+    public $data;
+
+    /** @var $viewFile string template view file path, default is action id */
+    public $viewFile = null;
+
+
+    /**
+     * index list
+     *
+     * @return string
+     * @throws Exception
+     */
+    public function run()
+    {
+        //according assigned HTTP Method and param name to get value. will be passed to $this->>data closure.Often there is no need to get value on index, so default value is null.
+        $primaryKeys = Helper::getPrimaryKeys($this->primaryKeyIdentity, $this->primaryKeyFromMethod);
+
+        $data = $this->data;
+        if( $data instanceof Closure){
+            $params = [];
+            if( !empty($primaryKeys) ){
+                foreach ($primaryKeys as $primaryKey) {
+                    array_push($params, $primaryKey);
+                }
+            }
+            array_push($params, Yii::$app->getRequest()->getQueryParams());
+            array_push($params, $this);
+            //execute closure then assign to view, the closure params like function($_GET, primaryKeyValue1, primaryKeyValue1 ..., IndexAction)
+            $data = call_user_func_array( $this->data, $params );
+            if( !is_array($data) ){
+                throw new Exception("data closure must return array");
+            }
+        }else if (!is_array($data) ){
+            throw new Exception(__CLASS__ . "::data must be array or closure");
+        }
+
+        //default view template is action id
+        $this->viewFile === null && $this->viewFile = $this->id;
+
+        return $this->controller->render($this->viewFile, $data);
+    }
+
+}

+ 95 - 0
webApp/backend/actions/SortAction.php

@@ -0,0 +1,95 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-13 10:00
+ */
+
+namespace backend\actions;
+
+use Yii;
+use stdClass;
+use Closure;
+use backend\actions\helpers\Helper;
+use yii\base\Exception;
+use yii\base\InvalidArgumentException;
+use yii\web\MethodNotAllowedHttpException;
+use yii\web\Response;
+use yii\web\UnprocessableEntityHttpException;
+
+/**
+ * backend sort
+ *
+ * Class SortAction
+ * @package backend\actions
+ */
+class SortAction extends \yii\base\Action
+{
+
+    /**
+     * @var Closure
+     */
+    public $doSort = null;
+
+    /**
+     * @var string after success doUpdate tips message showed in page top
+     */
+    public $successTipsMessage = "success";
+
+
+    public function init()
+    {
+        parent::init();
+        if( $this->successTipsMessage === "success"){
+            $this->successTipsMessage = Yii::t("app", "success");
+        }
+    }
+
+    /**
+     * sort
+     *
+     * @return array|\yii\web\Response
+     * @throws MethodNotAllowedHttpException
+     * @throws UnprocessableEntityHttpException
+     * @throws \yii\base\Exception
+     */
+    public function run()
+    {
+        if (Yii::$app->getRequest()->getIsPost()) {
+            if(!$this->doSort instanceof Closure){
+                throw new Exception(__CLASS__ . "::doSort must be closure");
+            }
+            $post = Yii::$app->getRequest()->post();
+            if (isset($post[Yii::$app->getRequest()->csrfParam])) {
+                unset($post[Yii::$app->getRequest()->csrfParam]);
+            }
+            reset($post);
+            $temp = current($post);
+            $condition = array_keys($temp)[0];
+            $value = $temp[$condition];
+            $condition = json_decode($condition, true);
+            if (!is_array($condition)) throw new InvalidArgumentException("SortColumn generate html must post data like xxx[{pk:'unique'}]=number");
+            $result = call_user_func_array($this->doSort, [$condition, $value, $this]);
+
+            if (Yii::$app->getRequest()->getIsAjax()) {
+                Yii::$app->getResponse()->format = Response::FORMAT_JSON;
+                if( $result === true ){
+                    return ['code'=>0, 'msg'=>'success', 'data'=>new stdClass()];
+                }else{
+                    throw new UnprocessableEntityHttpException(Helper::getErrorString($result));
+                }
+            }else {
+                if ($result === true) {
+                    Yii::$app->getSession()->setFlash('success', $this->successTipsMessage);
+                } else {
+                    Yii::$app->getSession()->setFlash('error', Helper::getErrorString($result));
+                }
+                return $this->controller->goBack();
+            }
+
+        }else{
+            throw new MethodNotAllowedHttpException(Yii::t('app', "Sort must be POST http method"));
+        }
+    }
+}

+ 159 - 0
webApp/backend/actions/UpdateAction.php

@@ -0,0 +1,159 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-13 00:31
+ */
+
+namespace backend\actions;
+
+
+use Yii;
+use stdClass;
+use Closure;
+use backend\actions\helpers\Helper;
+use yii\base\Exception;
+use yii\web\Response;
+use yii\web\UnprocessableEntityHttpException;
+
+/**
+ * backend update
+ * if update occurs error, must return model or error string for display error. return true for successful update.
+ * if GET request, the updateResult be a null, POST request the createResult is the value of doUpdate closure returns.
+ *
+ * Class UpdateAction
+ * @package backend\actions
+ */
+class UpdateAction extends \yii\base\Action
+{
+
+    const UPDATE_REFERER = "_update_referer";
+
+    /**
+     * @var string|array primary key(s) name
+     */
+    public $primaryKeyIdentity = 'id';
+
+    /**
+     * @var string primary keys(s) from (GET or POST)
+     */
+    public $primaryKeyFromMethod = "GET";
+
+    /**
+     * @var array|\Closure variables will assigned to view
+     */
+    public $data;
+
+    /**
+     * @var  string|array success update redirect to url (this value will pass yii::$app->controller->redirect($this->successRedirect) to generate url), default is (GET request) referer url
+     */
+    public $successRedirect;
+
+    /**
+     * @var Closure the real update logic, usually will call service layer update method
+     */
+    public $doUpdate;
+
+    /**
+     * @var string after success doUpdate tips message showed in page top
+     */
+    public $successTipsMessage = "success";
+
+    /**
+     * @var string view template path,default is action id
+     */
+    public $viewFile = null;
+
+
+    public function init()
+    {
+        parent::init();
+        if( $this->successTipsMessage === "success"){
+            $this->successTipsMessage = Yii::t("app", "success");
+        }
+    }
+
+
+    /**
+     * update
+     *
+     * @return array|string
+     * @throws UnprocessableEntityHttpException
+     * @throws Exception
+     */
+    public function run()
+    {
+        //according assigned HTTP Method and param name to get value. will be passed to $this->doUpdate closure and $this->data closure.Often use for get value of primary key.
+        $primaryKeys = Helper::getPrimaryKeys($this->primaryKeyIdentity, $this->primaryKeyFromMethod);
+
+        if (Yii::$app->getRequest()->getIsPost()) {//if POST request will execute doUpdate.
+            if (!$this->doUpdate instanceof Closure) {
+                throw new Exception(__CLASS__ . "::doUpdate must be closure");
+            }
+            $postData = Yii::$app->getRequest()->post();
+
+            $updateData = [];//doUpdate closure formal parameter(translate: 传递给doUpdate必包的形参)
+
+            if( !empty($primaryKeys) ){
+                foreach ($primaryKeys as $primaryKey) {
+                    array_push($updateData, $primaryKey);
+                }
+            }
+
+            array_push($updateData, $postData, $this);
+
+            /**
+             * doUpdate(primaryKey1, primaryKey2 ..., $_POST, UpdateAction)
+             */
+            $updateResult = call_user_func_array($this->doUpdate, $updateData);//call doUpdate closure
+
+            if(  Yii::$app->getRequest()->getIsAjax() ){ //ajax
+                Yii::$app->getResponse()->format = Response::FORMAT_JSON;
+                if( $updateResult === true ){//only $updateResult is true represent update success
+                    return ['code'=>0, 'msg'=>'success', 'data'=>new stdClass()];
+                }else{
+                    throw new UnprocessableEntityHttpException(Helper::getErrorString($updateResult));
+                }
+            }else{//not ajax
+                if( $updateResult === true ){//only $updateResult is true represent update success
+                    Yii::$app->getSession()->setFlash('success', $this->successTipsMessage);
+                    if ($this->successRedirect) return $this->controller->redirect($this->successRedirect);//if $this->successRedirect not empty will redirect to this url
+                    $url = Yii::$app->getSession()->get(self::UPDATE_REFERER);
+                    if ($url) return $this->controller->redirect($url);//get an not empty referer will redirect to this url(often, before do update page. also to say: update page)
+                    return $this->controller->redirect(["index"]);//default is redirect to current controller index action(attention: if current controller has no index action will get a HTTP 404 error)
+                    //if doUpdate success will terminated here!!!
+                }else{//besides true, all represent update failed.
+                    Yii::$app->getSession()->setFlash('error', Helper::getErrorString($updateResult));//if doUpdate error will set a error description string.and continue the current page.
+                }
+            }
+
+        }
+
+        //if GET request or doUpdate failed, will display the update page.
+        if (is_array($this->data)) {
+            $data = $this->data;//this data will assigned to view
+        } elseif ($this->data instanceof Closure) {
+            $params = [];
+            if( !empty($primaryKeys) ){
+                foreach ($primaryKeys as $primaryKey) {
+                    array_push($params, $primaryKey);
+                }
+            }
+            //GET request just display update page. Only POST request will get a updateResult(returned by doUpdate closure)
+            !isset($updateResult) && $updateResult = null;
+            array_push($params, $updateResult, $this);
+            $data = call_user_func_array($this->data, $params);//this data will assigned to view
+        } else {
+            throw new Exception(__CLASS__ . "::data only allows array or closure (with return array)");
+        }
+
+        $this->viewFile === null && $this->viewFile = $this->id;
+
+        Yii::$app->getRequest()->getIsGet() && Yii::$app->getSession()->set(self::UPDATE_REFERER, Yii::$app->getRequest()->getReferrer());//set an referer, when success doUpdate may redirect this url
+
+        return $this->controller->render($this->viewFile, $data);
+    }
+
+
+}

+ 68 - 0
webApp/backend/actions/ViewAction.php

@@ -0,0 +1,68 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-08-13 10:10
+ */
+
+namespace backend\actions;
+
+use Closure;
+use backend\actions\helpers\Helper;
+use yii\base\Exception;
+
+/**
+ * backend view single record
+ *
+ * Class ViewAction
+ * @package backend\actions
+ */
+class ViewAction extends \yii\base\Action
+{
+
+    /**
+     * @var string|array primary key(s) name
+     */
+    public $primaryKeyIdentity = 'id';
+
+    /**
+     * @var string primary keys(s) from (GET or POST)
+     */
+    public $primaryKeyFromMethod = "GET";
+
+    /** @var array|Closure variables will assigned to view */
+    public $data;
+
+    /**
+     * @var string view template file path, default is action id
+     */
+    public $viewFile = 'view';
+
+
+    /**
+     * view detail page
+     *
+     * @return string
+     * @throws Exception
+     */
+    public function run()
+    {
+        if( is_array($this->data) ){
+            $data = $this->data;
+        }else if ($this->data instanceof Closure){
+            //according assigned HTTP Method and param name to get value. will be passed to $this->data closure.Often use for get value of primary key.
+            $primaryKeys = Helper::getPrimaryKeys($this->primaryKeyIdentity, $this->primaryKeyFromMethod);
+            $getDataParams = $primaryKeys;
+            array_push($getDataParams, $this);
+            $data = call_user_func_array($this->data, $getDataParams);
+            if( !is_array($data) ){
+                throw new Exception(__CLASS__ . "::data closure must return array");
+            }
+        }else{
+            throw new Exception(__CLASS__ . "::data only allows array or closure (with return array)");
+        }
+
+        return $this->controller->render($this->viewFile, $data);
+    }
+}

+ 72 - 0
webApp/backend/actions/helpers/Helper.php

@@ -0,0 +1,72 @@
+<?php
+namespace backend\actions\helpers;
+
+use Yii;
+use yii\base\Exception;
+use yii\base\Model;
+
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2020-01-31 21:19
+ */
+
+class Helper
+{
+    public static function getPrimaryKeys($primaryKeyIdentity, $primaryKeyFromMethod)
+    {
+        $primaryKeys = [];
+
+        if( !empty( $primaryKeyIdentity ) ){
+
+            if( is_string($primaryKeyIdentity) ){
+                $primaryKeyIdentity = [$primaryKeyIdentity];
+            }else if( !is_array($primaryKeyIdentity) ){
+                throw new Exception("primaryKeyIdentity must be string or array");
+            }
+
+            foreach ($primaryKeyIdentity as $identity){
+                if( $primaryKeyFromMethod == "GET" ){
+                    $primaryKeys[] =Yii::$app->getRequest()->get($identity, null);
+                }else if( $primaryKeyFromMethod == "POST" ){
+                    $primaryKeys[] = Yii::$app->getRequest()->post($identity, null);
+                }else{
+                    throw new Exception('primaryKeyFromMethod must be GET or POST');
+                }
+            }
+        }
+
+        return $primaryKeys;
+
+    }
+
+    public static function getErrorString($result)
+    {
+        if( !is_array($result) ){
+            $results = [$result];
+        }else{
+            $results = $result;
+        }
+        $error = "";
+        foreach ($results as $result) {
+
+            if ($result instanceof Model) {//if returns a model, will call getErrors() get the error description string
+                $items = $result->getErrors();
+                foreach ($items as $item) {
+                    foreach ($item as $e) {
+                        $error .= $e . "<br>";
+                    }
+                }
+                $error = rtrim($error, "<br>");
+            } else if (is_string($result)) {//if returns a string, they will be the error description
+                $error = $result;
+            } else {
+                throw new Exception("doCreate/doUpdate/doDelete/doSort closure must return boolean, yii\base\Model or string");
+            }
+
+        }
+
+        return $error;
+    }
+}

+ 50 - 0
webApp/backend/assets/AppAsset.php

@@ -0,0 +1,50 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-03-15 21:16
+ */
+
+namespace backend\assets;
+
+
+/**
+ * 重要提示:启用配置后,修改此处的js/css将不会生效
+ * 需要在backend/config/main.php中assetManager.bundles处修改配置
+ * 主要用于测试环境走本地文件,正式环境配置成cdn
+ *
+ * Class AppAsset
+ * @package backend\assets
+ */
+class AppAsset extends \yii\web\AssetBundle
+{
+
+    public $css = [
+        'static/css/bootstrap.min14ed.css?v=3.3.6',
+        //'//cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css',
+        'static/css/font-awesome.min93e3.css?v=4.4.0',
+        'static/css/animate.min.css',
+        'static/css/style.min862f.css?v=4.1.0',
+        'static/js/plugins/layer/laydate/theme/default/laydate.css',
+        //'js/plugins/layer/laydate/skins/default/laydate.css'
+        'static/css/plugins/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css',
+        'static/css/plugins/toastr/toastr.min.css',
+        'static/css/plugins/chosen/chosen.css',
+        'static/css/feehi.css',
+
+    ];
+
+    public $js = [
+        'static/js/feehi.js',
+        'static/js/plugins/layer/laydate/laydate.js',
+        'static/js/plugins/layer/layer.min.js',
+        'static/js/plugins/prettyfile/bootstrap-prettyfile.js',
+        'static/js/plugins/toastr/toastr.min.js',
+        'static/js/plugins/chosen/chosen.jquery.js',
+    ];
+
+    public $depends = [
+        'yii\web\YiiAsset',
+    ];
+}

+ 43 - 0
webApp/backend/assets/IndexAsset.php

@@ -0,0 +1,43 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-03-15 21:16
+ */
+
+namespace backend\assets;
+
+
+/**
+ * 重要提示:启用配置后,修改此处的js/css将不会生效
+ * 需要在backend/config/main.php中assetManager.bundles处修改配置
+ * 主要用于测试环境走本地文件,正式环境配置成cdn
+ *
+ * Class IndexAsset
+ * @package backend\assets
+ */
+class IndexAsset extends \yii\web\AssetBundle
+{
+
+    public $css = [
+        'static/css/bootstrap.min.css',
+        'static/css/font-awesome.min93e3.css?v=4.4.0',
+        'static/css/style.min862f.css?v=4.1.0',
+    ];
+
+    public $js = [
+        "static/js/jquery.min.js?v=2.1.4",
+        "static/js/bootstrap.min.js?v=3.3.6",
+        "static/js/plugins/metisMenu/jquery.metisMenu.js",
+        "static/js/plugins/slimscroll/jquery.slimscroll.min.js",
+        "static/js/plugins/layer/layer.min.js",
+        "static/js/hplus.min.js?v=4.1.0",
+        "static/js/contabs.min.js",
+        "static/js/plugins/pace/pace.min.js",
+    ];
+
+    public $depends = [
+        'yii\web\YiiAsset',
+    ];
+}

+ 38 - 0
webApp/backend/assets/UeditorAsset.php

@@ -0,0 +1,38 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-03-15 21:16
+ */
+
+namespace backend\assets;
+
+
+/**
+ * 重要提示:启用配置后,修改此处的js/css将不会生效
+ * 需要在backend/config/main.php中assetManager.bundles处修改配置
+ * 主要用于测试环境走本地文件,正式环境配置成cdn
+ * Class UeditorAsset
+ * @package backend\assets
+ */
+class UeditorAsset extends \yii\web\AssetBundle
+{
+
+    public $basePath = "@web";
+
+    public $sourcePath = '@backend/web/static/js/plugins/ueditor/';
+
+    public $js = [
+        'ueditor.all.min.js',
+    ];
+
+    public $publishOptions = [
+        'except' => [
+            'php/',
+            'index.html',
+            '.gitignore'
+        ]
+    ];
+
+}

+ 36 - 0
webApp/backend/assets/WebuploaderAsset.php

@@ -0,0 +1,36 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2019-01-06 12:47
+ */
+
+namespace backend\assets;
+
+/**
+ * 重要提示:启用配置后,修改此处的js/css将不会生效
+ * 需要在backend/config/main.php中assetManager.bundles处修改配置
+ * 主要用于测试环境走本地文件,正式环境配置成cdn
+ *
+ * Class WebuploaderAsset
+ * @package backend\assets
+ */
+class WebuploaderAsset extends \yii\web\AssetBundle
+{
+    public $css = [
+    	'css/plugins/webuploader/style.css',
+        'css/plugins/webuploader/webuploader.css',
+    ];
+    public $js = [
+        'js/plugins/webuploader/webuploader.min.js',
+        'js/plugins/webuploader/init.js'
+    ];
+    public $depends = [
+        'yii\bootstrap\BootstrapPluginAsset',
+    ];
+
+    public $basePath = "@web";
+
+    public $sourcePath = '@backend/web/static/';
+}

+ 65 - 0
webApp/backend/behaviors/TimeSearchBehavior.php

@@ -0,0 +1,65 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2018-01-22 17:23
+ */
+namespace backend\behaviors;
+
+use backend\components\search\SearchEvent;
+
+class TimeSearchBehavior extends \yii\base\Behavior
+{
+    public $created_at;
+
+    public $updated_at;
+
+    public $createdAtAttribute = 'created_at';
+
+    public $updatedAtAttribute = 'updated_at';
+
+    public $timeAttributes = [];
+
+    public $delimiter = "~";
+
+    public $format = "int";
+
+
+    public function init()
+    {
+        parent::init();
+        empty($this->timeAttributes) && $this->timeAttributes = [$this->createdAtAttribute => $this->createdAtAttribute, $this->updatedAtAttribute => $this->updatedAtAttribute] ;
+    }
+
+    public function events()
+    {
+        return [
+            SearchEvent::BEFORE_SEARCH => 'beforeSearch'
+        ];
+    }
+
+    public function beforeSearch($event)
+    {
+        /** @var $event \backend\components\search\SearchEvent */
+        foreach ($this->timeAttributes as $filed => $attribute) {
+            if($attribute !== null) $timeAt = $event->sender->{$attribute};
+            if( !empty($timeAt) ){
+                $time = explode($this->delimiter, $timeAt);
+                if( $this->format === 'int' ){
+                    $startAt = strtotime($time[0]);
+                    $endAt = strtotime($time[1]);
+                }else{
+                    $startAt = $time[0];
+                    $endAt = $time[1];
+                }
+                $event->query->andFilterWhere([
+                    'between',
+                    $filed,
+                    $startAt,
+                    $endAt
+                ]);
+            }
+        }
+    }
+}

+ 15 - 0
webApp/backend/codeception.yml

@@ -0,0 +1,15 @@
+namespace: backend\tests
+actor: Tester
+paths:
+    tests: tests
+    log: tests/_output
+    data: tests/_data
+    helpers: tests/_support
+settings:
+    bootstrap: _bootstrap.php
+    colors: true
+    memory_limit: 1024M
+modules:
+    config:
+        Yii2:
+            configFile: 'config/test-local.php'

+ 178 - 0
webApp/backend/components/AccessControl.php

@@ -0,0 +1,178 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-09-10 16:42
+ */
+
+namespace backend\components;
+
+use Yii;
+use yii\web\ForbiddenHttpException;
+use yii\base\Module;
+use yii\web\User;
+use yii\di\Instance;
+
+class AccessControl extends \yii\base\ActionFilter
+{
+    /* @var User */
+    private $_user = 'user';
+
+    public $allowActions = [];
+
+    public $superAdminUserIds = [];
+
+    /**
+     * Get user
+     * @return User
+     * @throws \yii\base\InvalidConfigException
+     */
+    public function getUser()
+    {
+        if (!$this->_user instanceof User) {
+            $this->_user = Instance::ensure($this->_user, User::className());
+        }
+        return $this->_user;
+    }
+
+    /**
+     * Set user
+     * @param User|string $user
+     */
+    public function setUser($user)
+    {
+        $this->_user = $user;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function beforeAction($action)
+    {
+        $actionId = $action->getUniqueId();
+        $user = $this->getUser();
+        if( in_array($user->getId(), $this->superAdminUserIds) ){
+            return true;
+        }
+        if (self::checkRoute('/' . $actionId, Yii::$app->getRequest()->get(), $user)) {
+            return true;
+        }
+        $this->denyAccess($user);
+    }
+
+    /**
+     * Denies the access of the user.
+     * The default implementation will redirect the user to the login page if he is a guest;
+     * if the user is already logged, a 403 HTTP exception will be thrown.
+     * @param  User $user the current user
+     * @throws ForbiddenHttpException if the user is already logged in.
+     */
+    protected function denyAccess($user)
+    {
+        if ($user->getIsGuest()) {
+            $user->loginRequired();
+        } else {
+            throw new ForbiddenHttpException(Yii::t('yii', 'You are not allowed to perform this action.'));
+        }
+    }
+
+    /**
+     * @inheritdoc
+     */
+    protected function isActive($action)
+    {
+        $uniqueId = $action->getUniqueId();
+        if ($uniqueId === Yii::$app->getErrorHandler()->errorAction) {
+            return false;
+        }
+
+        $user = $this->getUser();
+        if($user->getIsGuest())
+        {
+            $loginUrl = null;
+            if(is_array($user->loginUrl) && isset($user->loginUrl[0])){
+                $loginUrl = $user->loginUrl[0];
+            }else if(is_string($user->loginUrl)){
+                $loginUrl = $user->loginUrl;
+            }
+            if(!is_null($loginUrl) && trim($loginUrl,'/') === $uniqueId)
+            {
+                return false;
+            }
+        }
+
+        if ($this->owner instanceof Module) {
+            // convert action uniqueId into an ID relative to the module
+            $mid = $this->owner->getUniqueId();
+            $id = $uniqueId;
+            if ($mid !== '' && strpos($id, $mid . '/') === 0) {
+                $id = substr($id, strlen($mid) + 1);
+            }
+        } else {
+            $id = $action->id;
+        }
+
+        foreach ($this->allowActions as $route) {
+            if (substr($route, -1) === '*') {
+                $route = rtrim($route, "*");
+                if ($route === '' || strpos($id, $route) === 0) {
+                    return false;
+                }
+            } else {
+                if ($id === $route) {
+                    return false;
+                }
+            }
+        }
+
+        if ($action->controller->hasMethod('allowAction') && in_array($action->id, $action->controller->allowAction())) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Check access route for user.
+     * @param string|array $route
+     * @param integer|User $user
+     * @return boolean
+     */
+    public static function checkRoute($route, $params = [], $user = null)
+    {
+        $r = static::normalizeRoute($route);
+
+        if ($user === null) {
+            $user = Yii::$app->getUser();
+        }
+        $userId = $user instanceof User ? $user->getId() : $user;
+
+
+        if ($user->can($r, $params)) {
+            return true;
+        }
+        while (($pos = strrpos($r, '/')) > 0) {
+            $r = substr($r, 0, $pos);
+            if ($user->can($r . '/*', $params)) {
+                return true;
+            }
+        }
+        return $user->can('/*', $params);
+
+    }
+
+    protected static function normalizeRoute($route)
+    {
+        if ($route === '') {
+            return '/' . Yii::$app->controller->getRoute() . ':' . yii::$app->getRequest()->getMethod();
+        } elseif (strncmp($route, '/', 1) === 0) {
+            return $route . ':' . yii::$app->getRequest()->getMethod();
+        } elseif (strpos($route, '/') === false) {
+            return '/' . Yii::$app->controller->getUniqueId() . '/' . $route . ':' . yii::$app->getRequest()->getMethod();
+        } elseif (($mid = Yii::$app->controller->module->getUniqueId()) !== '') {
+            return '/' . $mid . '/' . $route . ':' . yii::$app->getRequest()->getMethod();
+        }
+        return '/' . $route . ':' . yii::$app->getRequest()->getMethod();
+    }
+}

+ 116 - 0
webApp/backend/components/AdminLog.php

@@ -0,0 +1,116 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-03-15 21:16
+ */
+
+namespace backend\components;
+
+use Yii;
+use common\models\AdminLog as AdminLogModel;
+
+class AdminLog extends \yii\base\Event
+{
+
+    /**
+     * when create a record save to database, auto generate a log
+     *
+     * @param $event
+     * @throws \Throwable
+     */
+    public static function create($event)
+    {
+        if ($event->sender->className() !== AdminLogModel::className()) {
+            $desc = '<br>';
+            foreach ($event->sender->getAttributes() as $name => $value) {
+                !is_string( $value ) && $value = print_r($value, true);
+                $desc .= $event->sender->getAttributeLabel($name) . '(' . $name . ') => ' . $value . ',<br>';
+            }
+            $desc = substr($desc, 0, -5);
+            $model = new AdminLogModel();
+            $class = $event->sender->className();
+            $idDes = '';
+            if (isset($event->sender->id)) {
+                $idDes = '{{%ID%}} ' . $event->sender->id;
+            }
+            $model->description = '{{%ADMIN_USER%}} [ ' . Yii::$app->getUser()->getIdentity()->username . ' ] {{%BY%}} ' . $class . ' [ ' . $class::tableName() . ' ] ' . " {{%CREATED%}} {$idDes} {{%RECORD%}}: " . $desc;
+            $model->route = Yii::$app->controller->id . '/' . Yii::$app->controller->action->id;
+            $model->user_id = Yii::$app->getUser()->getId();
+            $model->save();
+        }
+    }
+
+    /**
+     * when delete a record from database, auto generate a log
+     *
+     * @param $event
+     * @throws \Throwable
+     */
+    public static function update($event)
+    {
+        if (! empty($event->changedAttributes)) {
+            $desc = '<br>';
+            $oldAttributes = $event->sender->oldAttributes;
+            foreach ($event->changedAttributes as $name => $value) {
+                if( $oldAttributes[$name] == $value ) continue;
+                !is_string( $value ) && $value = print_r($value, true);
+                $desc .= $event->sender->getAttributeLabel($name) . '(' . $name . ') : ' . $value . '=>' . $event->sender->oldAttributes[$name] . ',<br>';
+            }
+            $desc = substr($desc, 0, -5);
+            $model = new AdminLogModel();
+            $class = $event->sender->className();
+            $idDes = '';
+            if (isset($event->sender->id)) {
+                $idDes = '{{%ID%}} ' . $event->sender->id;
+            }
+            $model->description = '{{%ADMIN_USER%}} [ ' . Yii::$app->getUser()->getIdentity()->username . ' ] {{%BY%}} ' . $class . ' [ ' . $class::tableName() . ' ] ' . " {{%UPDATED%}} {$idDes} {{%RECORD%}}: " . $desc;
+            $model->route = Yii::$app->controller->id . '/' . Yii::$app->controller->action->id;
+            $model->user_id = Yii::$app->getUser()->id;
+            $model->save();
+        }
+    }
+
+    /**
+     * when delete a record from database, auto generate a log
+     *
+     * @param $event
+     * @throws \Throwable
+     */
+    public static function delete($event)
+    {
+        $desc = '<br>';
+        foreach ($event->sender->getAttributes() as $name => $value) {
+            !is_string( $value ) && $value = print_r($value, true);
+            $desc .= $event->sender->getAttributeLabel($name) . '(' . $name . ') => ' . $value . ',<br>';
+        }
+        $desc = substr($desc, 0, -5);
+        $model = new AdminLogModel();
+        $class = $event->sender->className();
+        $idDes = '';
+        if (isset($event->sender->id)) {
+            $idDes = '{{%ID%}} ' . $event->sender->id;
+        }
+        $model->description = '{{%ADMIN_USER%}} [ ' . Yii::$app->getUser()->getIdentity()->username . ' ] {{%BY%}} ' . $class . ' [ ' . $class::tableName() . ' ] ' . " {{%DELETED%}} {$idDes} {{%RECORD%}}: " . $desc;
+        $model->route = Yii::$app->controller->id . '/' . Yii::$app->controller->action->id;
+        $model->user_id = Yii::$app->getUser()->id;
+        $model->save();
+    }
+
+    /**
+     * custom log info
+     *
+     * @param CustomLog $event
+     * @throws yii\base\ErrorException
+     */
+    public static function custom(CustomLog $event)
+    {
+        $model = new AdminLogModel();
+        $model->description = $event->getDescription();
+        $model->route = Yii::$app->controller->id . '/' . Yii::$app->controller->action->id;
+        $model->user_id = Yii::$app->getUser()->getId();
+        $model->save();
+    }
+
+}

+ 119 - 0
webApp/backend/components/CustomLog.php

@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-09-20 23:04
+ */
+namespace backend\components;
+
+use yii;
+use common\models\AdminUser;
+use yii\base\ErrorException;
+
+class CustomLog extends \yii\base\Event
+{
+    const EVENT_AFTER_CREATE = 1;
+
+    const EVENT_AFTER_DELETE = 2;
+
+    const EVENT_CUSTOM = 3;
+
+
+    public $description = null;
+
+    private $adminUserName = null;
+
+    public function init()
+    {
+        parent::init();
+        /** @var AdminUser $identity */
+        $components = Yii::$app->coreComponents();
+        if( !isset($components['user']) ){//cli(console)模式
+            $this->adminUserName = "command(console)";
+        }else {
+            $identity = yii::$app->getUser()->getIdentity();
+            $this->adminUserName = $identity->username;
+        }
+    }
+
+    public function getDescription()
+    {
+        switch ($this->name){
+            case self::EVENT_AFTER_CREATE:
+                $description = $this->create();
+                break;
+            case self::EVENT_AFTER_DELETE:
+                $description = $this->delete();
+                break;
+            case self::EVENT_CUSTOM:
+                $description = $this->custom();
+                break;
+            default:
+                throw new ErrorException("None exists event");
+        }
+        $this->setDescription($description);
+        return $this->description;
+    }
+
+    public function setDescription($description)
+    {
+        $this->description = $description;
+    }
+
+    private function create()
+    {
+        $class = $this->sender->className();
+        $template = $description = '{{%ADMIN_USER%}} [ ' .  $this->adminUserName  . ' ] {{%BY%}} ' . $class . " {{%CREATED%}} {{%RECORD%}}: ";
+        if( $this->description !== null ){
+            return $template . $this->description;
+        }
+        switch ($this->sender->className()){
+            default:
+        $str = "<br>";
+            foreach ($this->sender->activeAttributes() as $field) {
+                $value = $this->sender->$field;
+                if( is_array($value) ) $value = implode(',', $value);
+                $str .= $this->sender->getAttributeLabel($field) . '(' . $field . ') => ' . $value . ',<br>';
+            }
+            $str = substr($str, 0, -5);
+         }
+       return $template . $str;
+    }
+
+    private function delete()
+    {
+        $class = $this->sender->className();
+        $template = '{{%ADMIN_USER%}} [ ' .  $this->adminUserName  . ' ] {{%BY%}} ' . $class . " {{%DELETED%}} {{%RECORD%}}: ";
+        if( $this->description !== null ){
+            return $template . $this->description;
+        }
+        switch ($this->sender->className()){
+            default:
+                $str = "<br>";
+                foreach ($this->sender->activeAttributes() as $field) {
+                    $value = $this->sender->$field;
+                if( is_array($value) ) $value = implode(',', $value);
+                $str .= $this->sender->getAttributeLabel($field) . '(' . $field . ') => ' . $value . ',<br>';
+            }
+            $str = substr($str, 0, -5);
+
+            }
+        return $template . $str;
+    }
+
+    private function custom()
+    {
+        $class= $this->sender->className();
+        $template = '{{%ADMIN_USER%}} [ ' .  $this->adminUserName  . ' ] {{%BY%}} ' . $class;
+        if ($this->description !== null){
+            return $template . $this->description;
+        }
+        switch ($this->sender->className()){
+            default:
+                throw new ErrorException("EVENT_CUSTOM must set description property");
+        }
+    }
+
+}

+ 96 - 0
webApp/backend/components/gii/crud/Generator.php

@@ -0,0 +1,96 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2018-10-08 22:17
+ */
+
+namespace backend\components\gii\crud;
+
+use Yii;
+use ReflectionClass;
+use yii\gii\CodeFile;
+use yii\helpers\StringHelper;
+
+class Generator extends \yii\gii\generators\crud\Generator
+{
+    /**
+     * @param string $attribute
+     * @return string
+     */
+    public function generateActiveSearchField($attribute)
+    {
+        $tableSchema = $this->getTableSchema();
+        if ($tableSchema === false) {
+            return "\$form->field(\$model, '$attribute', ['labelOptions'=>['class'=>'col-sm-4 control-label'], 'size'=>8, 'options'=>['class'=>'col-sm-3']])";
+        }
+
+        $column = $tableSchema->columns[$attribute];
+        if ($column->phpType === 'boolean') {
+            return "\$form->field(\$model, '$attribute')->checkbox()";
+        }
+
+        return "\$form->field(\$model, '$attribute', ['labelOptions'=>['class'=>'col-sm-4 control-label'], 'size'=>8, 'options'=>['class'=>'col-sm-3']])";
+    }
+
+    public function formView()
+    {
+        $class = new ReflectionClass(\yii\gii\generators\crud\Generator::className());
+
+        return dirname($class->getFileName()) . '/form.php';
+    }
+
+    public function generate()
+    {
+        $controllerFile = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->controllerClass, '\\')) . '.php');
+
+        $files = [
+            new CodeFile($controllerFile, $this->render('controller.php')),
+        ];
+
+        if (!empty($this->searchModelClass)) {
+            $searchModel = Yii::getAlias('@' . str_replace('\\', '/', ltrim($this->searchModelClass, '\\') . '.php'));
+            $files[] = new CodeFile($searchModel, $this->render('search.php'));
+        }
+
+        $modelClass = StringHelper::basename($this->modelClass);
+
+        $files[] = new CodeFile(Yii::getAlias("@common/services/") . $modelClass . 'ServiceInterface.php', $this->render("serviceInterface.php"));
+
+        $files[] = new CodeFile(Yii::getAlias("@common/services/") . $modelClass . 'Service.php', $this->render("service.php"));
+
+        $viewPath = $this->getViewPath();
+        $templatePath = $this->getTemplatePath() . '/views';
+        foreach (scandir($templatePath) as $file) {
+            if (empty($this->searchModelClass) && $file === '_search.php') {
+                continue;
+            }
+            if (is_file($templatePath . '/' . $file) && pathinfo($file, PATHINFO_EXTENSION) === 'php') {
+                $files[] = new CodeFile("$viewPath/$file", $this->render("views/$file"));
+            }
+        }
+        $type = Yii::$app->getRequest()->post("generate");
+        if( $type !== null ){
+            $services = require Yii::getAlias("@common/config/") . 'services.php';
+            $key = $modelClass . "Service";
+            if( !isset($services[$key]) ) {
+                $str = file_get_contents(Yii::getAlias("@common/config/") . 'services.php');
+                $lines = explode("\n", $str);
+                foreach ($lines as $key => $line) {
+                    $line = trim($line);
+                    if (empty($line)) {
+                        unset($lines[$key]);
+                    }
+                }
+                $temp[] = "    \\common\services\\" . $modelClass . "ServiceInterface::ServiceName=>[";
+                $temp[] = "        'class' => \\common\services\\" . $modelClass . "Service::className(),";
+                $temp[] = "    ],";
+                array_splice($lines, count($lines) - 1, 0, $temp);
+                file_put_contents(Yii::getAlias("@common/config/") . 'services.php', implode("\n", $lines));
+            }
+        }
+        return $files;
+    }
+
+}

+ 20 - 0
webApp/backend/components/gii/crud/default/ServiceInterface.php

@@ -0,0 +1,20 @@
+<?php
+use yii\helpers\StringHelper;
+
+/* @var $generator yii\gii\generators\crud\Generator */
+/* @var $this yii\web\View */
+
+$modelClass = StringHelper::basename($generator->modelClass);
+$searchModelClass = StringHelper::basename($generator->searchModelClass);
+if ($modelClass === $searchModelClass) {
+    $searchModelAlias = $searchModelClass . 'Search';
+}
+
+echo "<?php\n";
+?>
+namespace common\services;
+
+interface <?=$modelClass?>ServiceInterface extends ServiceInterface
+{
+    const ServiceName = '<?=lcfirst($modelClass)?>Service';
+}

+ 157 - 0
webApp/backend/components/gii/crud/default/controller.php

@@ -0,0 +1,157 @@
+<?php
+/**
+ * This is the FeehiCMS backend template for generating a CRUD controller class file.
+ */
+
+use yii\db\ActiveRecordInterface;
+use yii\helpers\Inflector;
+use yii\helpers\StringHelper;
+
+
+/* @var $this yii\web\View */
+/* @var $generator yii\gii\generators\crud\Generator */
+
+$controllerClass = StringHelper::basename($generator->controllerClass);
+$modelClass = StringHelper::basename($generator->modelClass);
+$searchModelClass = StringHelper::basename($generator->searchModelClass);
+if ($modelClass === $searchModelClass) {
+    $searchModelAlias = $searchModelClass . 'Search';
+}
+
+/* @var $class ActiveRecordInterface */
+$class = $generator->modelClass;
+$pks = $class::primaryKey();
+$urlParams = $generator->generateUrlParams();
+$actionParams = $generator->generateActionParams();
+$actionParamComments = $generator->generateActionParamComments();
+
+echo "<?php\n";
+?>
+
+namespace <?= StringHelper::dirname(ltrim($generator->controllerClass, '\\')) ?>;
+
+use Yii;
+use common\services\<?= $modelClass ?>ServiceInterface;
+use common\services\<?= $modelClass ?>Service;
+use backend\actions\CreateAction;
+use backend\actions\UpdateAction;
+use backend\actions\IndexAction;
+use backend\actions\DeleteAction;
+use backend\actions\SortAction;
+use backend\actions\ViewAction;
+<?php if (empty($generator->searchModelClass)){ ?>
+use yii\data\ActiveDataProvider;
+<?php } ?>
+<?php $category = Yii::t("app", Inflector::pluralize(Inflector::camel2words(StringHelper::basename($generator->modelClass)))); ?>
+<?php
+    $idSign = "";
+    $closureIdParam = "";
+    if( !empty($pks) ) {
+        $idSign = "                'primaryKeyIdentity' => ";
+        if (count($pks) === 1) {
+            if ($pks[0] !== "id") {
+                $idSign .= "'" . $pks[0] . "',\n";
+                $closureIdParam = '$' . $pks[0];
+            }else{
+                $idSign = "";
+                $closureIdParam = '$id';
+            }
+        } else {
+            $idSign .= "[";
+            $i = 0;
+            foreach ($pks as $key) {
+                $idSign .= "'" . $key . "',";
+                if($i > 0){
+                    $closureIdParam .= ' $' . $key . ",";
+                }else{
+                    $closureIdParam .= '$' . $key . ",";
+                }
+
+                $i++;
+            }
+            $idSign = rtrim($idSign, ",");
+            $idSign .= "],\n";
+            $closureIdParam = rtrim($closureIdParam, ",");
+        }
+    }
+?>
+/**
+ * <?= $controllerClass ?> implements the CRUD actions for <?= $modelClass ?> model.
+ */
+class <?= $controllerClass ?> extends \yii\web\<?= StringHelper::basename($generator->baseControllerClass) . "\n" ?>
+{
+    /**
+    * @auth
+    * - item group=未分类 category=<?= $category?> description-get=列表 sort=000 method=get
+    * - item group=未分类 category=<?= $category?> description=创建 sort-get=001 sort-post=002 method=get,post


+    * - item group=未分类 category=<?= $category?> description=修改 sort=003 sort-post=004 method=get,post


+    * - item group=未分类 category=<?= $category?> description-post=删除 sort=005 method=post


+    * - item group=未分类 category=<?= $category?> description-post=排序 sort=006 method=post


+    * - item group=未分类 category=<?= $category?> description-get=查看 sort=007 method=get


+    * @return array
+    */
+    public function actions()
+    {
+        /** @var <?=$modelClass?>ServiceInterface $service */
+        $service = Yii::$app->get(<?=$modelClass?>ServiceInterface::ServiceName);
+        return [
+            'index' => [
+                'class' => IndexAction::className(),
+                'data' => function($query, $indexAction) use($service){
+                    $result = $service->getList($query);
+                    return [
+                        'dataProvider' => $result['dataProvider'],
+                        <?php if( !empty($generator->searchModelClass) ) { ?>'searchModel' => $result['searchModel'],<?php } ?>
+                    ];
+                }
+            ],
+            'create' => [
+                'class' => CreateAction::className(),
+                'doCreate' => function($postData, $createAction) use($service){
+                    return $service->create($postData);
+                },
+                'data' => function($createResultModel, $createAction) use($service){
+                    $model = $createResultModel === null ? $service->newModel() : $createResultModel;
+                    return [
+                        'model' => $model,
+                    ];
+                }
+            ],
+            'update' => [
+                'class' => UpdateAction::className(),
+<?php if(!empty($idSign)){echo $idSign;} ?>
+                'doUpdate' => function(<?php if(!empty($closureIdParam)){echo $closureIdParam;echo ", ";}?>$postData, $updateAction) use($service){
+                    return $service->update(<?=$closureIdParam?>, $postData);
+                },
+                'data' => function(<?php if(!empty($closureIdParam)){echo $closureIdParam;echo ", ";}?>$updateResultModel, $updateAction) use($service){
+                    $model = $updateResultModel === null ? $service->getDetail(<?=$closureIdParam?>) : $updateResultModel;
+                    return [
+                        'model' => $model,
+                    ];
+                }
+            ],
+            'delete' => [
+                'class' => DeleteAction::className(),
+<?php if(!empty($idSign)){echo $idSign;} ?>
+                'doDelete' => function(<?php if(!empty($closureIdParam)){echo $closureIdParam;echo ", ";}?>$deleteAction) use($service){
+                    return $service->delete(<?=$closureIdParam?>);
+                },
+            ],
+            'sort' => [
+                'class' => SortAction::className(),
+                'doSort' => function($id, $sort, $sortAction) use($service){
+                    return $service->sort($id, $sort);
+                },
+            ],
+            'view-layer' => [
+                'class' => ViewAction::className(),
+<?php if(!empty($idSign)){echo $idSign;} ?>
+                'data' => function(<?php if(!empty($closureIdParam)){echo $closureIdParam;echo ", ";}?>$viewAction) use($service){
+                    return [
+                        'model' => $service->getDetail(<?=$closureIdParam?>),
+                    ];
+                },
+            ],
+        ];
+    }
+}

+ 86 - 0
webApp/backend/components/gii/crud/default/search.php

@@ -0,0 +1,86 @@
+<?php
+/**
+ * This is the template for generating CRUD search class of the specified model.
+ */
+
+use yii\helpers\StringHelper;
+
+
+/* @var $this yii\web\View */
+/* @var $generator yii\gii\generators\crud\Generator */
+
+$modelClass = StringHelper::basename($generator->modelClass);
+$searchModelClass = StringHelper::basename($generator->searchModelClass);
+if ($modelClass === $searchModelClass) {
+    $modelAlias = $modelClass . 'Model';
+}
+$rules = $generator->generateSearchRules();
+$labels = $generator->generateSearchLabels();
+$searchAttributes = $generator->getSearchAttributes();
+$searchConditions = $generator->generateSearchConditions();
+
+echo "<?php\n";
+?>
+
+namespace <?= StringHelper::dirname(ltrim($generator->searchModelClass, '\\')) ?>;
+
+use Yii;
+use yii\base\Model;
+use yii\data\ActiveDataProvider;
+use <?= ltrim($generator->modelClass, '\\') . (isset($modelAlias) ? " as $modelAlias" : "") ?>;
+
+/**
+ * <?= $searchModelClass ?> represents the model behind the search form about `<?= $generator->modelClass ?>`.
+ */
+class <?= $searchModelClass ?> extends <?= isset($modelAlias) ? $modelAlias : $modelClass ?> implements \backend\models\search\SearchInterface
+{
+    /**
+     * @inheritdoc
+     */
+    public function rules()
+    {
+        return [
+            <?= implode(",\n            ", $rules) ?>,
+        ];
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function scenarios()
+    {
+        // bypass scenarios() implementation in the parent class
+        return Model::scenarios();
+    }
+
+    /**
+     * Creates data provider instance with search query applied
+     *
+     * @param array $params
+     *
+     * @return ActiveDataProvider
+     */
+    public function search(array $params = [], array $options = [])
+    {
+        $query = <?= isset($modelAlias) ? $modelAlias : $modelClass ?>::find();
+
+        // add conditions that should always apply here
+
+        $dataProvider = new ActiveDataProvider([
+            'query' => $query,
+        ]);
+
+        $this->load($params);
+
+        if (!$this->validate()) {
+            // uncomment the following line if you do not want to return any records when validation fails
+            // $query->where('0=1');
+            return $dataProvider;
+        }
+
+        // grid filtering conditions
+        <?= implode("\n        ", $searchConditions) ?>
+
+        return $dataProvider;
+    }
+}

+ 43 - 0
webApp/backend/components/gii/crud/default/service.php

@@ -0,0 +1,43 @@
+<?php
+use yii\helpers\StringHelper;
+
+/* @var $generator yii\gii\generators\crud\Generator */
+/* @var $this yii\web\View */
+
+$modelClass = StringHelper::basename($generator->modelClass);
+$searchModelClass = StringHelper::basename($generator->searchModelClass);
+if ($modelClass === $searchModelClass) {
+    $searchModelAlias = $searchModelClass . 'Search';
+}
+
+echo "<?php\n";
+?>
+namespace common\services;
+/**
+* This is the template for generating CRUD service class of the specified model.
+*/
+
+<?php if (!empty($generator->searchModelClass)): ?>
+use <?=$generator->searchModelClass . ";\n"?>
+<?php endif; ?>
+use <?= $generator->modelClass . ";\n" ?>
+
+class <?=$modelClass?>Service extends Service implements <?=$modelClass?>ServiceInterface{
+    public function getSearchModel(array $query=[], array $options=[])
+    {
+        <?php if (!empty($generator->searchModelClass)){ ?> return new  <?=$searchModelClass?>();<?php }else { ?>return null;<?php } ?>
+
+    }
+
+    public function getModel($id, array $options = [])
+    {
+        return <?=$modelClass?>::findOne($id);
+    }
+
+    public function newModel(array $options = [])
+    {
+        $model = new <?=$modelClass?>();
+        $model->loadDefaultValues();
+        return $model;
+    }
+}

+ 53 - 0
webApp/backend/components/gii/crud/default/views/_form.php

@@ -0,0 +1,53 @@
+<?php
+
+use yii\helpers\Inflector;
+use yii\helpers\StringHelper;
+
+/* @var $this yii\web\View */
+/* @var $generator yii\gii\generators\crud\Generator */
+
+/* @var $model \yii\db\ActiveRecord */
+$model = new $generator->modelClass();
+$safeAttributes = $model->safeAttributes();
+if (empty($safeAttributes)) {
+    $safeAttributes = $model->attributes();
+}
+
+echo "<?php\n";
+?>
+
+use backend\widgets\ActiveForm;
+
+/* @var $this yii\web\View */
+/* @var $model <?= ltrim($generator->modelClass, '\\') ?> */
+/* @var $form backend\widgets\ActiveForm */
+?>
+<div class="row">
+    <div class="col-sm-12">
+        <div class="ibox">
+            <?="<?= \$this->render('/widgets/_ibox-title') ?>\n"?>
+            <div class="ibox-content">
+                <?="<?php \$form = ActiveForm::begin([
+                    'options' => [
+                        'class' => 'form-horizontal'
+                    ]
+                ]); ?>\n"?>
+                <div class="hr-line-dashed"></div>
+                <?php foreach ($generator->getColumnNames() as $attribute) {
+                    static $i = 0;
+                    if (in_array($attribute, $safeAttributes)) {
+                        if($i==0){
+                            echo "    <?= " . $generator->generateActiveField($attribute) . " ?>\n";
+                            $i++;
+                        }else{
+                            echo "                        <?= " . $generator->generateActiveField($attribute) . " ?>\n";
+                        }
+                        echo '                        <div class="hr-line-dashed"></div>' . "\n\n";
+                    }
+                } ?>
+                <?="        <?= \$form->defaultButtons() ?>\n"?>
+                <?="    <?php ActiveForm::end(); ?>\n";?>
+            </div>
+        </div>
+    </div>
+</div>

+ 49 - 0
webApp/backend/components/gii/crud/default/views/_search.php

@@ -0,0 +1,49 @@
+<?php
+
+use yii\helpers\Inflector;
+use yii\helpers\StringHelper;
+
+/* @var $this yii\web\View */
+/* @var $generator yii\gii\generators\crud\Generator */
+
+echo "<?php\n";
+?>
+
+use yii\helpers\Html;
+use backend\widgets\ActiveForm;
+use yii\helpers\Url;
+
+/* @var $this yii\web\View */
+/* @var $model <?= ltrim($generator->searchModelClass, '\\') ?> */
+/* @var $form yii\widgets\ActiveForm */
+?>
+
+<div class="<?= Inflector::camel2id(StringHelper::basename($generator->modelClass)) ?>-search ibox-heading row search" style="margin-top: 5px;padding-top:5px">
+
+    <?= "<?php " ?>$form = ActiveForm::begin([
+        'action' => ['index'],
+        'method' => 'get',
+    ]); ?>
+
+<?php
+$count = 0;
+foreach ($generator->getColumnNames() as $attribute) {
+    if (++$count < 6) {
+        echo "    <?= " . $generator->generateActiveSearchField($attribute) . " ?>\n\n";
+    } else {
+        echo "    <?php // echo " . $generator->generateActiveSearchField($attribute) . " ?>\n\n";
+    }
+}
+?>
+    <div class="col-sm-3">
+        <div class="col-sm-6">
+            <?= "<?= " ?>Html::submitButton(Yii::t("app", <?= $generator->generateString('Search') ?>), ['class' => 'btn btn-primary btn-block']) ?>
+        </div>
+        <div class="col-sm-6">
+            <?= "<?= " ?>Html::a(Yii::t("app", <?= $generator->generateString('Reset') ?>), Url::to(['index']), ['class' => 'btn btn-default btn-block']) ?>
+        </div>
+    </div>
+
+    <?= "<?php " ?>ActiveForm::end(); ?>
+
+</div>

+ 26 - 0
webApp/backend/components/gii/crud/default/views/create.php

@@ -0,0 +1,26 @@
+<?php
+
+use yii\helpers\Inflector;
+use yii\helpers\StringHelper;
+
+/* @var $this yii\web\View */
+/* @var $generator yii\gii\generators\crud\Generator */
+
+echo "<?php\n";
+?>
+
+use yii\helpers\Url;
+
+
+/* @var $this yii\web\View */
+/* @var $model <?= ltrim($generator->modelClass, '\\') ?> */
+
+$this->params['breadcrumbs'] = [
+    ['label' => yii::t('app', '<?=Inflector::camel2words(StringHelper::basename($generator->modelClass))?>'), 'url' => Url::to(['index'])],
+    ['label' => yii::t('app', 'Create') . yii::t('app', '<?=Inflector::camel2words(StringHelper::basename($generator->modelClass))?>')],
+];
+?>
+<?= "<?= " ?>$this->render('_form', [
+    'model' => $model,
+]) ?>
+

+ 80 - 0
webApp/backend/components/gii/crud/default/views/index.php

@@ -0,0 +1,80 @@
+<?php
+
+use yii\helpers\Inflector;
+use yii\helpers\StringHelper;
+
+/* @var $this yii\web\View */
+/* @var $generator yii\gii\generators\crud\Generator */
+
+$urlParams = $generator->generateUrlParams();
+$nameAttribute = $generator->getNameAttribute();
+
+echo "<?php\n";
+?>
+
+use backend\widgets\Bar;
+use backend\grid\CheckboxColumn;
+use backend\grid\ActionColumn;
+use <?= $generator->indexWidgetType === 'grid' ? "backend\\grid\\GridView" : "yii\\widgets\\ListView" ?>;
+<?= $generator->enablePjax ? 'use yii\widgets\Pjax;' : '' ?>
+
+/* @var $this yii\web\View */
+<?= !empty($generator->searchModelClass) ? "/* @var \$searchModel " . ltrim($generator->searchModelClass, '\\') . " */\n" : '' ?>
+/* @var $dataProvider yii\data\ActiveDataProvider */
+
+$this->title = <?= $generator->generateString(Inflector::pluralize(Inflector::camel2words(StringHelper::basename($generator->modelClass)))) ?>;
+$this->params['breadcrumbs'][] = yii::t('app', '<?=Inflector::camel2words(StringHelper::basename($generator->modelClass))?>');
+?>
+<div class="row">
+    <div class="col-sm-12">
+        <div class="ibox">
+            <?="<?= \$this->render('/widgets/_ibox-title') ?>\n"?>
+            <div class="ibox-content">
+                <?="<?= Bar::widget() ?>\n"?>
+                <?php if( !empty($generator->searchModelClass) ){ echo "<?=\$this->render('_search', ['model' => \$searchModel]); ?>\n";}?>
+    <?= $generator->enablePjax ? '<?php Pjax::begin(); ?>' : '' ?>
+    <?php if ($generator->indexWidgetType === 'grid'): ?>
+        <?= "<?= " ?>GridView::widget([
+                    'dataProvider' => $dataProvider,
+                    <?= !empty($generator->searchModelClass) ? "'filterModel' => \$searchModel,\n                    'columns' => [\n" : "'columns' => [\n"; ?>
+                        ['class' => CheckboxColumn::className()],
+
+<?php
+$count = 0;
+if (($tableSchema = $generator->getTableSchema()) === false) {
+    foreach ($generator->getColumnNames() as $name) {
+        if (++$count < 6) {
+            echo "                       '" . $name . "',\n";
+        } else {
+            echo "                       // '" . $name . "',\n";
+        }
+    }
+} else {
+    foreach ($tableSchema->columns as $column) {
+        $format = $generator->generateColumnFormat($column);
+        if (++$count < 6) {
+            echo "                        '" . $column->name . ($format === 'text' ? "" : ":" . $format) . "',\n";
+        } else {
+            echo "                        // '" . $column->name . ($format === 'text' ? "" : ":" . $format) . "',\n";
+        }
+    }
+}
+?>
+
+                        ['class' => ActionColumn::className(),],
+                    ],
+                ]); ?>
+<?php else: ?>
+    <?= "<?= " ?>ListView::widget([
+        'dataProvider' => $dataProvider,
+        'itemOptions' => ['class' => 'item'],
+        'itemView' => function ($model, $key, $index, $widget) {
+            return Html::a(Html::encode($model-><?= $nameAttribute ?>), ['view', <?= $urlParams ?>]);
+        },
+    ]) ?>
+<?php endif; ?>
+<?= $generator->enablePjax ? '<?php Pjax::end(); ?>' : '' ?>
+            </div>
+        </div>
+    </div>
+</div>

+ 26 - 0
webApp/backend/components/gii/crud/default/views/update.php

@@ -0,0 +1,26 @@
+<?php
+
+use yii\helpers\Inflector;
+use yii\helpers\StringHelper;
+
+/* @var $this yii\web\View */
+/* @var $generator yii\gii\generators\crud\Generator */
+
+$urlParams = $generator->generateUrlParams();
+
+echo "<?php\n";
+?>
+
+use yii\helpers\Url;
+
+/* @var $this yii\web\View */
+/* @var $model <?= ltrim($generator->modelClass, '\\') ?> */
+
+$this->params['breadcrumbs'] = [
+    ['label' => yii::t('app', '<?=Inflector::camel2words(StringHelper::basename($generator->modelClass))?>'), 'url' => Url::to(['index'])],
+    ['label' => yii::t('app', 'Update') . yii::t('app', '<?=Inflector::camel2words(StringHelper::basename($generator->modelClass))?>')],
+];
+?>
+<?= "<?= " ?>$this->render('_form', [
+    'model' => $model,
+]) ?>

+ 46 - 0
webApp/backend/components/gii/crud/default/views/view.php

@@ -0,0 +1,46 @@
+<?php
+
+use yii\helpers\Inflector;
+use yii\helpers\StringHelper;
+
+/* @var $this yii\web\View */
+/* @var $generator yii\gii\generators\crud\Generator */
+
+$urlParams = $generator->generateUrlParams();
+
+echo "<?php\n";
+?>
+
+use yii\helpers\Html;
+use yii\widgets\DetailView;
+
+/* @var $this yii\web\View */
+/* @var $model <?= ltrim($generator->modelClass, '\\') ?> */
+
+$this->title = $model-><?= $generator->getNameAttribute() ?>;
+$this->params['breadcrumbs'][] = ['label' => <?= $generator->generateString(Inflector::pluralize(Inflector::camel2words(StringHelper::basename($generator->modelClass)))) ?>, 'url' => ['index']];
+$this->params['breadcrumbs'][] = $this->title;
+?>
+<div class="<?= Inflector::camel2id(StringHelper::basename($generator->modelClass)) ?>-view">
+
+    <h1><?= "<?= " ?>Html::encode($this->title) ?></h1>
+
+    <?= "<?= " ?>DetailView::widget([
+        'model' => $model,
+        'attributes' => [
+<?php
+if (($tableSchema = $generator->getTableSchema()) === false) {
+    foreach ($generator->getColumnNames() as $name) {
+        echo "            '" . $name . "',\n";
+    }
+} else {
+    foreach ($generator->getTableSchema()->columns as $column) {
+        $format = $generator->generateColumnFormat($column);
+        echo "            '" . $column->name . ($format === 'text' ? "" : ":" . $format) . "',\n";
+    }
+}
+?>
+        ],
+    ]) ?>
+
+</div>

+ 15 - 0
webApp/backend/components/gii/model/Generator.php

@@ -0,0 +1,15 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2020-03-16 22:56
+ */
+namespace backend\components\gii\model;
+
+class Generator extends \yii\gii\generators\model\Generator
+{
+    public $ns = 'common\models';
+    public $queryNs = 'common\models';
+
+}

+ 9 - 0
webApp/backend/components/gii/model/form.php

@@ -0,0 +1,9 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2020-03-16 23:17
+ */
+
+require_once Yii::getAlias("@vendor/yiisoft/yii2-gii/src/generators/model/form.php");

+ 16 - 0
webApp/backend/components/search/SearchEvent.php

@@ -0,0 +1,16 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2018-01-22 17:17
+ */
+namespace backend\components\search;
+
+class SearchEvent extends \yii\base\Event
+{
+    const BEFORE_SEARCH = 1;
+
+    /** @var $query \yii\db\ActiveQuery */
+    public $query;
+}

+ 3 - 0
webApp/backend/config/.gitignore

@@ -0,0 +1,3 @@
+main-local.php
+params-local.php
+test-local.php

+ 2 - 0
webApp/backend/config/bootstrap.php

@@ -0,0 +1,2 @@
+<?php
+Yii::setAlias('@admin', '@frontend/web/admin');

+ 193 - 0
webApp/backend/config/main.php

@@ -0,0 +1,193 @@
+<?php
+$params = array_merge(
+    require(__DIR__ . '/../../common/config/params.php'),
+    require(__DIR__ . '/../../common/config/params-local.php'),
+    require(__DIR__ . '/params.php'),
+    require(__DIR__ . '/params-local.php')
+);
+
+return [
+    'id' => 'app-backend',//应用id,必须唯一
+    'basePath' => dirname(__DIR__),
+    'controllerNamespace' => 'backend\controllers',//控制器命名空间
+    'language' => 'zh-CN',//默认语言
+    'timeZone' => 'Asia/Shanghai',//默认时区
+    'bootstrap' => ['log'],
+    'modules' => [],
+    'components' => [
+        'user' => [
+            'identityClass' => common\models\AdminUser::className(),
+            'enableAutoLogin' => false,
+            'identityCookie' => ['name' => '_backend_identity'],
+            'idParam' => '__backend__id',
+            'returnUrlParam' => '_backend_returnUrl',
+        ],
+        'session' => [
+            'name' => 'BACKEND_FEEHICMS',
+            'timeout' => 1440,//session过期时间,单位为秒
+        ],
+        'log' => [//此项具体详细配置,请访问http://wiki.feehi.com/index.php?title=Yii2_log
+            'traceLevel' => YII_DEBUG ? 3 : 0,
+            'targets' => [
+                [
+                    'class' => yii\log\FileTarget::className(),//当触发levels配置的错误级别时,保存到日志文件
+                    'levels' => ['error', 'warning'],
+                    'logFile' => '@runtime/logs/'.date('Y/m/d') . '.log',
+                ],
+                [
+                    /**
+                    注:此配置可能造成:
+                        1.当打开的页面包含错误时,响应缓慢。若您配置的发件箱不存在或连不上一直等待超时。
+                        2.如果common/config/main.php mail useFileTransport为true时,并不会真发邮件,只把邮件写到runtime目录,很容易造成几十个G吃硬盘。
+                        如您不需要发送邮件提醒建议删除此配置
+                     */
+                    'class' => yii\log\EmailTarget::className(),//当触发levels配置的错误级别时,发送到message to配置的邮箱中(请改成自己的邮箱)
+                    'levels' => ['error', 'warning'],
+                    /*'categories' => [//默认匹配所有分类。启用此项后,仅匹配数组中的分类信息会触发邮件提醒(白名单)
+                        'yii\db\*',
+                        'yii\web\HttpException:*',
+                    ],*/
+                    'except' => [//以下配置,除了匹配数组中的分类信息都会触发邮件提醒(黑名单)
+                        'yii\web\HttpException:404',
+                        'yii\web\HttpException:403',
+                        'yii\debug\Module::checkAccess',
+                    ],
+                    'message' => [
+                        'to' => ['admin@feehi.com', 'liufee@126.com'],//此处修改成自己接收错误的邮箱
+                        'subject' => '来自 Feehi CMS 后台的新日志消息',
+                    ],
+                ],
+            ],
+        ],
+        'errorHandler' => [
+            'errorAction' => 'site/error',
+        ],
+        'request' => [
+            'csrfParam' =>'_csrf_backend',
+        ],
+        'urlManager' => [
+            'enablePrettyUrl' => false,//true 美化路由(注:需要配合web服务器配置伪静态,详见http://doc.feehi.com/install.html), false 不美化路由
+            'showScriptName' => true,//隐藏index.php
+            'enableStrictParsing' => false,
+        ],
+        'i18n' => [
+            'translations' => [//多语言包设置
+                'app*' => [
+                    'class' => yii\i18n\PhpMessageSource::className(),
+                    'basePath' => '@backend/messages',
+                    'sourceLanguage' => 'en-US',
+                    'fileMap' => [
+                        'app' => 'app.php',
+                        'app/error' => 'error.php',
+                    ],
+                ],
+                'menu' => [
+                    'class' => yii\i18n\PhpMessageSource::className(),
+                    'basePath' => '@backend/messages',
+                    'sourceLanguage' => 'zh-CN',
+                    'fileMap' => [
+                        'app' => 'menu.php',
+                        'app/error' => 'error.php',
+                    ],
+                ],
+            ],
+        ],
+        'assetManager' => [
+            'linkAssets' => false,//若为unix like系统这里可以修改成true则创建css js文件软链接到assets而不是拷贝css js到assets目录
+            'bundles' => [
+                yii\widgets\ActiveFormAsset::className() => [
+                    'js' => [
+                    ]
+                ],
+                yii\web\JqueryAsset::className() => [
+                    'js' => [
+                    ],
+                ],
+                yii\web\YiiAsset::className() => [
+                    'js' => [
+                    ],
+                ],
+                yii\validators\ValidationAsset::className() => [
+                    'js' => [
+                    ]
+                ],
+                yii\grid\GridViewAsset::className() => [
+                    'js' => [
+                    ]
+                ],
+                yii\widgets\PjaxAsset::className() => [
+                    'js' => [
+                    ]
+                ],
+                backend\assets\AppAsset::className() => [
+                    'sourcePath' => '@backend/web/static',
+                    'css' => [
+                        'a' => 'css/bootstrap.min14ed.css?v=3.3.6',
+                        'b' => 'css/font-awesome.min93e3.css?v=4.4.0',
+                        'c' => 'css/animate.min.css',
+                        'd' => 'css/style.min862f.css?v=4.1.0',
+                        'f' => 'js/plugins/layer/laydate/theme/default/laydate.css',
+                        'g' => 'css/plugins/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css',
+                        'h' => 'css/plugins/toastr/toastr.min.css',
+                        'i' => 'css/plugins/chosen/chosen.css',
+                        'j' => 'css/feehi.css',
+
+                    ],
+                    'js' => [
+                        'a' => 'js/feehi.js',
+                        'b' => 'js/plugins/layer/laydate/laydate.js',
+                        'c' => 'js/plugins/layer/layer.min.js',
+                        'd' => 'js/plugins/prettyfile/bootstrap-prettyfile.js',
+                        'e' => 'js/plugins/toastr/toastr.min.js',
+                        'f' => 'js/plugins/chosen/chosen.jquery.js',
+                    ],
+                ],
+                backend\assets\IndexAsset::className() => [
+                    'sourcePath' => '@backend/web/static',
+                    'css' => [
+                        'a' => 'css/bootstrap.min.css',
+                        'b' => 'css/font-awesome.min93e3.css?v=4.4.0',
+                        'c' => 'css/style.min862f.css?v=4.1.0',
+                    ],
+                    'js' => [
+                        'a' => "js/jquery.min.js?v=2.1.4",
+                        'b' => "js/bootstrap.min.js?v=3.3.6",
+                        'c' => "js/plugins/metisMenu/jquery.metisMenu.js",
+                        'd' => "js/plugins/slimscroll/jquery.slimscroll.min.js",
+                        'e' => "js/plugins/layer/layer.min.js",
+                        'f' => "js/hplus.min.js?v=4.1.0",
+                        'g' => "js/contabs.min.js",
+                        'h' => "js/plugins/pace/pace.min.js",
+                    ]
+                ],
+                backend\assets\UeditorAsset::className() => [
+                    'sourcePath' => '@backend/web/static/js/plugins/ueditor',
+                    'css' => [
+                        'a' => 'ueditor.all.min.js'
+                    ],
+                ],
+            ]
+        ],
+    ],
+    'on beforeRequest' => [common\components\Feehi::className(), 'backendInit'],
+    'as access' => [
+        'class' => backend\components\AccessControl::className(),
+        'allowActions' => [
+            'site/login',
+            'site/captcha',
+            'site/error',
+            'site/index',
+            'site/main',
+            'site/logout',
+            'site/language',
+            'admin-user/request-password-reset',
+            'admin-user/reset-password',
+            'admin-user/self-update',
+            'assets/*',
+            'debug/*',
+            'gii/*',
+        ],
+        'superAdminUserIds' => [1],//超级管理员用户id,拥有所有权限,不受权限管理的控制
+    ],
+    'params' => $params,
+];

+ 17 - 0
webApp/backend/config/params.php

@@ -0,0 +1,17 @@
+<?php
+return [
+    'supportLanguages' => [//简体中文,繁体中文,英语,法语,俄罗斯语,西班牙语,德语,意大利语,日语,葡萄牙,韩语,荷兰,印度
+        'zh-CN' => '简体中文',
+        'zh-TW' => '繁体中文',
+        'en-US' => 'English',
+        'fr' => 'français',
+        'ru' => 'русский',
+        'es' => 'Español',
+        'de' => 'Deutsche',
+        'it' => 'italiano',
+        'ja' => '日本語',
+        'pt' => 'Português',
+        'ko' => '한국어',
+        'nl' => 'Nederlands',
+    ]
+];

+ 19 - 0
webApp/backend/config/test.php

@@ -0,0 +1,19 @@
+<?php
+return [
+    'id' => 'app-backend-tests',
+    'components' => [
+        'assetManager' => [
+            'basePath' => '@admin/assets',
+        ],
+        'urlManager' => [
+            'showScriptName' => true,
+        ],
+        'user' => [
+            'enableAutoLogin' => true,
+            'autoRenewCookie' => false,
+        ],
+        'request' => [
+            'enableCsrfValidation' => false,
+        ]
+    ],
+];

+ 127 - 0
webApp/backend/controllers/AdController.php

@@ -0,0 +1,127 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-12-05 12:47
+ */
+
+namespace backend\controllers;
+
+use Yii;
+use backend\actions\ViewAction;
+use backend\actions\CreateAction;
+use backend\actions\UpdateAction;
+use backend\actions\IndexAction;
+use backend\actions\DeleteAction;
+use backend\actions\SortAction;
+use common\services\AdServiceInterface;
+
+/**
+ * Advertisement management
+ * - data:
+ *          table options with column `type` equal \common\models\Options::TYPE_AD
+ *          column `value` is a json format, like {"ad":"x.png"}
+ *
+ * Class AdController
+ * @package backend\controllers
+ */
+class AdController extends \yii\web\Controller
+{
+    /**
+     * @auth
+     * - item group=运营管理 category=广告 description-get=列表 sort=620 method=get
+     * - item group=运营管理 category=广告 description-get=查看 sort=621 method=get


+     * - item group=运营管理 category=广告 description=创建 sort-get=622 sort-post=623 method=get,post


+     * - item group=运营管理 category=广告 description=修改 sort-get=624 sort-post=625 method=get,post


+     * - item group=运营管理 category=广告 description-post=删除 sort=626 method=post


+     * - item group=运营管理 category=广告 description-post=排序 sort=627 method=post


+     *
+     * @return array
+     * @throws \yii\base\InvalidConfigException
+     */
+    public function actions()
+    {
+        /** @var AdServiceInterface $service */
+        $service = Yii::$app->get(AdServiceInterface::ServiceName);
+
+        return [
+            'index' => [
+                'class' => IndexAction::className(),
+                'data' => function($query)use($service){
+                    /** @var array $query query params($_GET) */
+                    $result = $service->getList($query);
+                    return [
+                        'dataProvider' => $result['dataProvider'],
+                        'searchModel' => $result['searchModel'],
+                    ];
+                }
+            ],
+            'view-layer' => [
+                'class' => ViewAction::className(),
+                'data' => function($id)use($service){
+                    /** string|int $id primary key value,usually column `id` value  */
+                    return [
+                        'model' => $service->getDetail($id),
+                    ];
+                },
+            ],
+            'create' => [
+                'class' => CreateAction::className(),
+                'doCreate' => function($postData) use($service){
+                    /** @var $postData $_POST data */
+                    return $service->create($postData);
+                },
+                'data' => function($createResultModel,  CreateAction $createAction)use($service){
+                    /**
+                     * same path(`/path/create`) have two HTTP method
+                     *  - GET for display create page
+                     *  - POST execute a create operation(write data to database), then redirect to index or show a create error
+                     *
+                     * if $createResultModel equals null means that is a GET request, need to show create page,
+                     * otherwise means POST request, $createResultModel be the model of created(maybe contains data validation error)
+                     */
+                    $model = $createResultModel === null ? $service->newModel() : $createResultModel;
+                    return [
+                        'model' => $model,
+                    ];
+                }
+            ],
+            'update' => [
+                'class' => UpdateAction::className(),
+                'doUpdate' => function($id, $postData, UpdateAction $updateAction) use($service){
+                    return $service->update($id, $postData);
+                },
+                'data' => function($id, $updateResultModel) use($service){
+                    /**
+                     * same path(`/path/update`) have two HTTP method
+                     *  - GET for display update page
+                     *  - POST execute a update operation(write data to database), then redirect to index or show a update error
+                     *
+                     * if $updateResultModel equals null means that is a GET request, need to show update page,
+                     * otherwise means POST request, $updateResultModel be the model of updated(maybe contains data validation error)
+                     */
+                    $model = $updateResultModel === null ? $service->getDetail($id) : $updateResultModel;
+                    return [
+                        'model' => $model,
+                    ];
+                }
+            ],
+            'delete' => [
+                'class' => DeleteAction::className(),
+                'doDelete' => function($id)use($service){
+                    /** string|int $id primary key value,usually column `id` value  */
+                    return $service->delete($id);
+                },
+            ],
+            'sort' => [
+                'class' => SortAction::className(),
+                'doSort' => function($id, $sort)use($service){
+                    /** string|int $id primary key value,usually column `id` value  */
+                    /** int $sort sort value */
+                    return $service->sort($id, $sort);
+                },
+            ],
+        ];
+    }
+}

+ 183 - 0
webApp/backend/controllers/AdminUserController.php

@@ -0,0 +1,183 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2016-03-31 15:01
+ */
+
+namespace backend\controllers;
+
+use Yii;
+use backend\actions\CreateAction;
+use backend\actions\UpdateAction;
+use common\services\RBACServiceInterface;
+use common\models\AdminUser;
+use common\services\AdminUserServiceInterface;
+use backend\models\form\PasswordResetRequestForm;
+use backend\actions\IndexAction;
+use backend\actions\DeleteAction;
+use backend\actions\SortAction;
+use yii\base\InvalidParamException;
+use yii\web\BadRequestHttpException;
+use backend\actions\ViewAction;
+
+/**
+ * AdminUser management
+ * - data:
+ *          table admin_user
+ *
+ * Class AdminUserController
+ * @package backend\controllers
+ */
+class AdminUserController extends \yii\web\Controller
+{
+
+    /**
+     * @auth
+     * - item group=权限 category=管理员 description-get=列表 sort=520 method=get
+     * - item group=权限 category=管理员 description-get=查看 sort=521 method=get


+     * - item group=权限 category=管理员 description-post=删除 sort=522 method=post


+     * - item group=权限 category=管理员 description-post=排序 sort=523 method=post

+     * - item group=权限 category=管理员 description=创建 sort-get=524 sort-post=525 method=get,post
+     * - item group=权限 category=管理员 description=修改 sort-get=526 sort-post=527 method=get,post
+     * - item rbac=false
+     * - item rbac=false
+     * - item rbac=false
+     * 

+     * @return array
+     * @throws \yii\base\InvalidConfigException
+     */
+    public function actions()
+    {
+        /** @var AdminUserServiceInterface $service */
+        $service = Yii::$app->get(AdminUserServiceInterface::ServiceName);
+        /** @var RBACServiceInterface $rbacService */
+        $rbacService = Yii::$app->get(RBACServiceInterface::ServiceName);
+
+        return [
+            'index' => [
+                'class' => IndexAction::className(),
+                'data' => function($query)use($service){
+                    $result = $service->getList($query);
+                    return [
+                        'dataProvider' => $result['dataProvider'],
+                        'searchModel' => $result['searchModel'],
+                    ];
+                }
+            ],
+            'view-layer' => [
+                'class' => ViewAction::className(),
+                'data' => function($id)use($service){
+                    return [
+                        'model' => $service->getDetail($id),
+                    ];
+                },
+            ],
+            'delete' => [
+                'class' => DeleteAction::className(),
+                'doDelete' => function($id) use($service){
+                    return $service->delete($id);
+                },
+            ],
+            'sort' => [
+                'class' => SortAction::className(),
+                'doSort' => function($id, $sort) use($service){
+                    return $service->sort($id, $sort);
+                },
+            ],
+            'create' => [
+                'class' => CreateAction::className(),
+                'doCreate' => function($postData) use($service, $rbacService){
+                    /** @var AdminUser $model */
+                    return $service->create($postData);
+                },
+                'data' => function()use($service, $rbacService){
+                    return [
+                        'model' => $service->newModel(['scenario' => AdminUserServiceInterface::scenarioCreate]),
+                        'assignModel' => $rbacService->newAssignPermissionModel(),
+                        'permissions' => $rbacService->getPermissionsGroups(),
+                        'roles' => $rbacService->getRoles(),
+                    ];
+                }
+            ],
+            'update' => [
+                'class' => UpdateAction::className(),
+                'doUpdate' => function($id, $postData) use($service){
+                    return $service->update($id, $postData, ['scenario' => AdminUserServiceInterface::scenarioUpdate]);
+                },
+                'data' => function($id, $updateResultModel)use($service, $rbacService){
+                    return [
+                        'model' => $updateResultModel === null ? $service->getDetail($id, ['scenario' => AdminUserServiceInterface::scenarioUpdate]) : $updateResultModel,
+                        'assignModel' => $rbacService->getAssignPermissionDetail($id),
+                        'permissions' => $rbacService->getPermissionsGroups(),
+                        'roles' => $rbacService->getRoles(),
+                    ];
+                }
+            ],
+            'self-update' => [
+                'class' => UpdateAction::className(),
+                'primaryKeyIdentity' => null,
+                'doUpdate' => function($postData) use($service){
+                    return $service->selfUpdate(Yii::$app->getUser()->getId(), $postData, ['scenario' => AdminUserServiceInterface::scenarioSelfUpdate]);
+                },
+                'data' => function($updateResultModel) use($service){
+                    return [
+                        'model' => $updateResultModel === null ? $service->getDetail(Yii::$app->getUser()->getId(), ['scenario' => AdminUserServiceInterface::scenarioSelfUpdate]) : $updateResultModel,
+                    ];
+                },
+                'viewFile' => 'update',
+            ],
+            'request-password-reset' => [
+                'class' => UpdateAction::className(),
+                'primaryKeyIdentity' => null,
+                'successTipsMessage' => Yii::t("app", "Check your email for further instructions."),
+                'doUpdate' => function($postData) use($service){
+                    $result = $service->sendResetPasswordLink($postData);
+                    if( $result === false ){
+                        return 'Sorry, we are unable to reset password for email provided.';
+                    }
+                    return $result;
+                },
+                'data' => function($updateResultModel){
+                    return [
+                        "model" => $updateResultModel === null ? new PasswordResetRequestForm() : $updateResultModel,
+                    ];
+                },
+                'viewFile' => 'requestPasswordResetToken',
+            ],
+            'reset-password' => [
+                'class' => UpdateAction::className(),
+                'primaryKeyIdentity' => 'token',
+                'successTipsMessage' => Yii::t("app", 'New password was saved.'),
+                'doUpdate' => function($token, $postData) use($service) {
+                    return $service->resetPassword($token, $postData);
+                },
+                'data' => function($token, $updateResultModel) use($service) {
+                    if( $updateResultModel === null ){
+                        try {
+                            $model = $service->newResetPasswordForm($token);
+                        }catch (InvalidParamException $e) {
+                                throw new BadRequestHttpException($e->getMessage());
+                        }
+                    }else{
+                        $model = $updateResultModel;
+                    }
+                    return [
+                        'model' => $model,
+                    ];
+                },
+                'successRedirect' => $this->getHomeUrl(),
+                'viewFile' => 'resetPassword'
+            ]
+        ];
+    }
+
+    private function getHomeUrl()
+    {
+        if( Yii::$app->getRequest()->getIsConsoleRequest() ){//when execute ./yii feehi/permission
+            return "/";
+        }
+        return Yii::$app->getHomeUrl();
+    }
+}

+ 116 - 0
webApp/backend/controllers/ArticleController.php

@@ -0,0 +1,116 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2016-03-23 15:13
+ */
+
+namespace backend\controllers;
+
+use Yii;
+use common\services\CategoryServiceInterface;
+use common\models\Article;
+use common\services\ArticleServiceInterface;
+use backend\actions\CreateAction;
+use backend\actions\UpdateAction;
+use backend\actions\IndexAction;
+use backend\actions\ViewAction;
+use backend\actions\DeleteAction;
+use backend\actions\SortAction;
+use yii\helpers\ArrayHelper;
+
+/**
+ * Article management
+ * - data:
+ *          table article article_content
+ * - description:
+ *          article management
+ *
+ * Class ArticleController
+ * @package backend\controllers
+ */
+class ArticleController extends \yii\web\Controller
+{
+
+    /**
+     * @auth
+     * - item group=内容 category=文章 description-get=列表 sort=300 method=get
+     * - item group=内容 category=文章 description-get=查看 sort=301 method=get


+     * - item group=内容 category=文章 description=创建 sort-get=302 sort-post=303 method=get,post


+     * - item group=内容 category=文章 description=修改 sort=304 sort-post=305 method=get,post


+     * - item group=内容 category=文章 description-post=删除 sort=306 method=post


+     * - item group=内容 category=文章 description-post=排序 sort=307 method=post


+     * @return array
+     * @throws \yii\base\InvalidConfigException
+     */
+    public function actions()
+    {
+        /** @var ArticleServiceInterface $service */
+        $service = Yii::$app->get(ArticleServiceInterface::ServiceName);
+        /** @var CategoryServiceInterface $categoryService */
+        $categoryService = Yii::$app->get(CategoryServiceInterface::ServiceName);
+
+        return [
+            'index' => [
+                'class' => IndexAction::className(),
+                'data' => function($query) use($service, $categoryService){
+                    $result = $service->getList($query, ['type'=>Article::ARTICLE]);
+                    return [
+                        'dataProvider' => $result['dataProvider'],
+                        'searchModel' => $result['searchModel'],
+                        'categories' => ArrayHelper::getColumn($categoryService->getLevelCategoriesWithPrefixLevelCharacters(), "prefix_level_name"),
+                        'frontendURLManager' => $service->getFrontendURLManager()
+                    ];
+                }
+            ],
+            'view-layer' => [
+                'class' => ViewAction::className(),
+                'data' => function($id) use($service){
+                    return [
+                        'model' => $service->getDetail($id),
+                    ];
+                },
+            ],
+            'create' => [
+                'class' => CreateAction::className(),
+                'doCreate' => function($postData) use($service){
+                    return $service->create($postData, ['scenario'=>ArticleServiceInterface::ScenarioArticle]);
+                },
+                'data' => function($createResultModel,  CreateAction $createAction) use($service, $categoryService){
+                    return [
+                        'model' => $createResultModel === null ? $service->newModel(['scenario'=>ArticleServiceInterface::ScenarioArticle]) : $createResultModel['articleModel'],
+                        'contentModel' => $createResultModel === null ? $service->newArticleContentModel() : $createResultModel['articleContentModel'] ,
+                        'categories' => ArrayHelper::getColumn($categoryService->getLevelCategoriesWithPrefixLevelCharacters(), "prefix_level_name"),
+                    ];
+                },
+            ],
+            'update' => [
+                'class' => UpdateAction::className(),
+                'doUpdate' => function($id, $postData) use($service){
+                    return $service->update($id, $postData, ['scenario'=>ArticleServiceInterface::ScenarioArticle]);
+                },
+                'data' => function($id, $updateResultModel) use($service, $categoryService){
+                    return [
+                        'model' => $updateResultModel === null ? $service->getDetail($id, ['scenario'=>ArticleServiceInterface::ScenarioArticle]) : $updateResultModel['articleModel'],
+                        'contentModel' => $updateResultModel === null ? $service->getArticleContentDetail($id) : $updateResultModel['articleContentModel'],
+                        'categories' => ArrayHelper::getColumn($categoryService->getLevelCategoriesWithPrefixLevelCharacters(), "prefix_level_name"),
+                    ];
+                }
+            ],
+            'delete' => [
+                'class' => DeleteAction::className(),
+                'doDelete' => function($id) use($service){
+                    return $service->delete($id);
+                },
+            ],
+            'sort' => [
+                'class' => SortAction::className(),
+                'doSort' => function($id, $sort) use($service){
+                    return $service->sort($id, $sort, ['scenario'=>ArticleServiceInterface::ScenarioArticle]);
+                }
+            ],
+        ];
+    }
+
+}

+ 72 - 0
webApp/backend/controllers/AssetsController.php

@@ -0,0 +1,72 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-03-09 22:07
+ */
+
+namespace backend\controllers;
+
+use Yii;
+use backend\widgets\ueditor\UeditorAction;
+use yii\helpers\FileHelper;
+use yii\web\Response;
+use yii\web\UploadedFile;
+
+class AssetsController extends \yii\web\Controller
+{
+
+    public function actions()
+    {
+        return [
+            'ueditor' => [
+                'class' => UeditorAction::className(),
+            ],
+        ];
+    }
+
+    public function actionWebuploader()
+    {
+        Yii::$app->getResponse()->format = Response::FORMAT_JSON;
+        $upload = UploadedFile::getInstanceByName("file");
+        if ($upload !== null) {
+            if( !in_array(strtolower($upload->getExtension()), ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']) ){
+                return [
+                    "code"=>1,
+                    "msg" => Yii::t("app", 'Only picture allowed'),
+                ];
+            }
+            $uploadPath = Yii::getAlias("@uploads/webuploader");
+            if( strpos(strrev($uploadPath), '/') !== 0 ) $uploadPath .= '/';
+            if (! FileHelper::createDirectory($uploadPath)) {
+                return [
+                    'code' => 0,
+                    'msg' => Yii::t('app', "Create directory failed " . $uploadPath)
+                ];
+            }
+            $fullName = isset($options['filename']) ? $uploadPath . uniqid() : $uploadPath . date('YmdHis') . '_' . uniqid() . '.' . $upload->getExtension();
+            if (! $upload->saveAs($fullName)) {
+                return[
+                    'code' => 1,
+                    'msg' => Yii::t('app', 'Upload {attribute} error: ' . $upload->error, ['attribute' => Yii::t('app', "File")])
+                ] ;
+            }
+            $attachment = str_replace(Yii::getAlias('@frontend/web'), '', $fullName);
+            /* @var $cdn \feehi\cdn\TargetInterface */
+            $cdn = Yii::$app->get('cdn');
+            $cdn->upload($fullName, $attachment);
+            return [
+                "code" => 0,
+                "url" => $cdn->getCdnUrl($attachment),
+                "attachment" => $attachment
+            ];
+        }
+        return [
+            "code"=>1,
+            "msg" => Yii::t("app", 'File cannot be empty'),
+        ];
+
+    }
+
+}

+ 159 - 0
webApp/backend/controllers/BannerController.php

@@ -0,0 +1,159 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-12-03 21:58
+ */
+
+namespace backend\controllers;
+
+use Yii;
+use backend\actions\ViewAction;
+use common\services\BannerServiceInterface;
+use backend\actions\IndexAction;
+use backend\actions\SortAction;
+use backend\actions\CreateAction;
+use backend\actions\DeleteAction;
+use backend\actions\UpdateAction;
+
+/**
+ * Banner management
+ * - data:
+ *          table options with column `type` equal \common\models\Options::TYPE_Banner
+ *          column `value` is a json format, like [{"sign":"5a251a3013586","img":"\/uploads\/setting\/banner\/5a251a301280d_1.png","target":"_blank","link":"\/view\/11","sort":"3","status":"1","desc":""}]
+ *          a db row, means a group of banners. such as index banners, detail page banners
+ *
+ * Class BannerController
+ * @package backend\controllers
+ */
+class BannerController extends \yii\web\Controller
+{
+    /**
+     * @auth
+     * - item group=运营管理 category=banner类型 description-get=列表 sort=600 method=get
+     * - item group=运营管理 category=banner类型 description=创建 sort-get=601 sort-post=602 method=get,post


+     * - item group=运营管理 category=banner类型 description=修改 sort-get=603 sort-post=604 method=get,post


+     * - item group=运营管理 category=banner类型 description-post=删除 sort=605 method=post


+     * - item group=运营管理 category=banner description-get=列表 sort=610 method=get


+     * - item group=运营管理 category=banner description=创建 sort-get=611 sort-post=612 method=get,post


+     * - item group=运营管理 category=banner description-get=查看 sort=613 method=get
+     * - item group=运营管理 category=banner description=修改 sort-get=614 sort-post=615 method=get,post


+     * - item group=运营管理 category=banner description-post=排序 sort=616 method=post


+     * - item group=运营管理 category=banner description=删除 sort=617 method=post


+     * @return array
+     * @throws \yii\base\InvalidConfigException
+     */
+    public function actions()
+    {
+        /** @var BannerServiceInterface $service */
+        $service = Yii::$app->get(BannerServiceInterface::ServiceName);
+        return [
+            'index' => [
+                'class' => IndexAction::className(),
+                'data' => function($query) use($service){
+                    $result = $service->getList($query);
+                    return [
+                        'dataProvider' => $result['dataProvider'],
+                        'searchModel' => $result['searchModel']
+                    ];
+                }
+            ],
+            'create' => [
+                'class' => CreateAction::className(),
+                'doCreate' => function($postData) use($service){
+                    return $service->create($postData);
+                },
+                'data' => function($createResultModel) use($service){
+                    $model = $createResultModel === null ? $service->newModel() : $createResultModel;
+                    return [
+                        'model' => $model,
+                    ];
+                }
+            ],
+            'update' => [
+                'class' => UpdateAction::className(),
+                'doUpdate' => function($id, $postData) use($service){
+                    return $service->update($id, $postData);
+                },
+                'data' => function($id, $updateResultModel) use($service){
+                    $model = $updateResultModel === null ? $service->getDetail($id) : $updateResultModel;
+                    return [
+                        'model' => $model,
+                    ];
+                },
+                'successRedirect' => ["banner/index"]
+            ],
+            'delete' => [
+                'class' => DeleteAction::className(),
+                'doDelete' => function($id) use($service){
+                    return $service->delete($id);
+                },
+            ],
+
+            'banners' => [
+                'primaryKeyIdentity' => 'id',
+                'class' => IndexAction::className(),
+                'data' => function($id, $query) use($service){
+                    $result = $service->getBannerList($query);
+                    return [
+                        'dataProvider' => $result['dataProvider'],
+                        'bannerType' => $service->getDetail($id),
+                    ];
+                }
+            ],
+            'banner-create' => [
+                'primaryKeyIdentity' => 'id',
+                'class' => CreateAction::className(),
+                'doCreate' => function($id, $postData) use($service){
+                    return $service->createBanner($id, $postData);
+                },
+                'data' => function($id, $createResultModel) use($service){
+                    $model = $createResultModel === null ? $service->newBannerModel($id) : $createResultModel;
+                    return [
+                        'model' => $model,
+                    ];
+                },
+                'successRedirect' => ['banner/banners', 'id'=>Yii::$app->getRequest()->get('id'), 'sign'=>Yii::$app->getRequest()->get('sign')]
+            ],
+            'banner-view-layer' => [
+                'primaryKeyIdentity' => ['id', 'sign'],
+                'class' => ViewAction::className(),
+                'data' => function($id, $sign) use($service){
+                    return [
+                        'model' => $service->getBannerDetail($id, $sign),
+                    ];
+                },
+                'viewFile' => 'view',
+            ],
+            'banner-update' => [
+                'primaryKeyIdentity' => ['id', 'sign'],
+                'class' => UpdateAction::className(),
+                'doUpdate' => function($id, $sign, $postData) use($service){
+                     return $service->updateBanner($id, $sign, $postData);
+                },
+                'data' => function($id, $sign, $updateResultModel) use($service) {
+                    $model = $updateResultModel === null ? $service->getBannerDetail($id, $sign) : $updateResultModel;
+                    return [
+                        'model' => $model,
+                    ];
+                },
+                'successRedirect' => ['banner/banners', 'id'=>Yii::$app->getRequest()->get('id'), 'sign'=>Yii::$app->getRequest()->get('sign')]
+            ],
+            'banner-sort' => [
+                'class' => SortAction::className(),
+                'doSort' => function($param, $value) use($service){
+                    return $service->sortBanner($param['id'], $param['sign'], $value);
+                },
+            ],
+            'banner-delete' => [
+                'primaryKeyIdentity' => "sign",
+                'class' => DeleteAction::className(),
+                'doDelete' => function($sign) use($service){
+                    $id = Yii::$app->getRequest()->get("id", null);
+                    return $service->deleteBanner($id, $sign);
+                },
+            ],
+        ];
+    }
+}

+ 105 - 0
webApp/backend/controllers/CategoryController.php

@@ -0,0 +1,105 @@
+<?php
+/**
+ * Author: lf
+ * Blog: https://blog.feehi.com
+ * Email: job@feehi.com
+ * Created at: 2017-03-15 21:16
+ */
+
+namespace backend\controllers;
+
+use Yii;
+use backend\actions\ViewAction;
+use common\services\CategoryServiceInterface;
+use backend\actions\CreateAction;
+use backend\actions\UpdateAction;
+use backend\actions\IndexAction;
+use backend\actions\DeleteAction;
+use backend\actions\SortAction;
+use yii\helpers\ArrayHelper;
+
+/**
+ * Category management
+ * - data:
+ *          table category
+ *          column `parent_id` is the parent category id, if equals 0 means first level category
+ *
+ * Class CategoryController
+ * @package backend\controllers
+ */
+class CategoryController extends \yii\web\Controller
+{
+    /**
+     * @auth
+     * - item group=内容 category=分类 description-get=列表 sort=310  method=get
+     * - item group=内容 category=分类 description-get=查看 sort=311 method=get


+     * - item group=内容 category=分类 description=创建 sort-get=312 sort-post=313 method=get,post


+     * - item group=内容 category=分类 description=修改 sort-get=314 sort-post=315 method=get,post


+     * - item group=内容 category=分类 description-post=删除 sort=316 method=post


+     * - item group=内容 category=分类 description-post=排序 sort=317 method=post


+     * @return array
+     * @throws \yii\base\InvalidConfigException
+     */
+    public function actions()
+    {
+        /** @var CategoryServiceInterface $service */
+        $service = Yii::$app->get(CategoryServiceInterface::ServiceName);
+        return [
+            'index' => [
+                'class' => IndexAction::className(),
+                'data' => function() use($service){
+                    return [
+                        "dataProvider" => $service->getCategoryList(),
+                    ];
+                }
+            ],
+            'view-layer' => [
+                'class' => ViewAction::className(),
+                'data' => function($id) use($service){
+                    return [
+                        'model' => $service->getDetail($id),
+                    ];
+                },
+            ],
+            'create' => [
+                'class' => CreateAction::className(),
+                'doCreate' => function($postData) use($service){
+                    return $service->create($postData);
+                },
+                'data' => function($createResultModel) use($service) {
+                    $model = $createResultModel === null ? $service->newModel() : $createResultModel;
+                    return [
+                        'model' => $model,
+                        'categories' => ArrayHelper::getColumn($service->getLevelCategoriesWithPrefixLevelCharacters(), "prefix_level_name"),
+                    ];
+                },
+            ],
+            'update' => [
+                'class' => UpdateAction::className(),
+                'doUpdate' => function($id, $postData) use($service){
+                    return $service->update($id, $postData);
+                },
+                'data' => function($id, $updateResultModel) use($service){
+                    $model = $updateResultModel === null ? $service->getDetail($id) : $updateResultModel;
+                    return [
+                        'model' => $model,
+                        'categories' => ArrayHelper::getColumn($service->getLevelCategoriesWithPrefixLevelCharacters(), "prefix_level_name"),
+                    ];
+                }
+            ],
+            'delete' => [
+                'class' => DeleteAction::className(),
+                'doDelete' => function($id) use($service){
+                    return $service->delete($id);
+                }
+            ],
+            'sort' => [
+                'class' => SortAction::className(),
+                'doSort' => function($id, $sort) use($service){
+                    return $service->sort($id, $sort);
+                },
+            ],
+        ];
+    }
+
+}

+ 0 - 0
webApp/backend/controllers/ClearController.php


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