没有不值得去解决的问题,也没有不值得去学习的技术!

Designing a Channel Publishing API in the Yii 2 Advanced Template: How Queues and Console Commands Drive the Workflow

Log in to the Penguin accounts respectively, the article has been published successfully

1. Copy the api application to qq and wx. After adjusting the corresponding configurations, the final application directory structure is as follows:

Markdown

common                   公共(所有应用程序共有的文件)
    config/              包含公共配置
    fixtures/            包含公共类的测试夹具
    logics/              包含在接口、前端、后端和控制台中使用的模型逻辑类
    mail/                包含电子邮件的视图文件
    messages/            包含国际化的消息文件
    models/              包含在接口、前端、后端和控制台中使用的模型数据类
    services/            包含在接口、前端、后端和控制台中使用的服务类(多个模型的逻辑类)
    tests/               包含公共类的各种测试
    widgets/             包含公共的小部件
console                  控制台
    config/              包含控制台配置
    controllers/         包含控制台的控制器类(命令)
    migrations/          包含数据库迁移
    models/              包含控制台的模型类
    runtime/             包含运行时生成的文件,例如日志和缓存文件
    services/            包含控制台的服务类
backend                  后端
    assets/              包含应用程序的资源文件(javascript 和 css)
    config/              包含后端配置
    controllers/         包含后端的Web控制器类
    models/              包含后端的模型类
    runtime/             包含运行时生成的文件,例如日志和缓存文件
    services/            包含后端的服务类
    tests/               包含后端应用程序的各种测试
    views/               包含Web应用程序的视图文件
    web/                 Web 应用根目录,包含 Web 入口文件
frontend                 前端
    assets/              包含应用程序的资源文件(javascript 和 css)
    config/              包含前端配置
    controllers/         包含前端的Web控制器类
    models/              包含前端的模型类
    runtime/             包含运行时生成的文件,例如日志和缓存文件
    services/            包含前端的服务类
    tests/               包含前端应用程序的各种测试
    views/               包含Web应用程序的视图文件
    web/                 Web 应用根目录,包含 Web 入口文件
    widgets/             包含前端的小部件
api                      接口(跨渠道)
    behaviors/           包含接口的行为类
    config/              包含接口配置
    controllers/         包含接口的Web控制器类
    fixtures/            包含接口的测试夹具
    messages/            包含国际化的消息文件
    models/              包含接口的模型类
    modules/             包含接口的模块
    rests/               包含接口的 REST API 类
    runtime/             包含运行时生成的文件,例如日志和缓存文件
    services/            包含接口的服务类
    tests/               包含接口应用程序的各种测试
    views/               包含Web应用程序的视图文件
    web/                 Web 应用根目录,包含 Web 入口文件
qq                       接口(企鹅号)
    behaviors/           包含接口的行为类
    config/              包含接口配置
    controllers/         包含接口的Web控制器类
    fixtures/            包含接口的测试夹具
    messages/            包含国际化的消息文件
    models/              包含接口的模型类
    modules/             包含接口的模块
    rests/               包含接口的 REST API 类
    runtime/             包含运行时生成的文件,例如日志和缓存文件
    services/            包含接口的服务类
    tests/               包含接口应用程序的各种测试
    views/               包含Web应用程序的视图文件
    web/                 Web 应用根目录,包含 Web 入口文件
wx                       接口(微信公众帐号)
    behaviors/           包含接口的行为类
    config/              包含接口配置
    controllers/         包含接口的Web控制器类
    fixtures/            包含接口的测试夹具
    messages/            包含国际化的消息文件
    models/              包含接口的模型类
    modules/             包含接口的模块
    rests/               包含接口的 REST API 类
    runtime/             包含运行时生成的文件,例如日志和缓存文件
    services/            包含接口的服务类
    tests/               包含接口应用程序的各种测试
    views/               包含Web应用程序的视图文件
    web/                 Web 应用根目录,包含 Web 入口文件
rpc                      远程过程调用
    assets/              包含应用程序的资源文件(javascript 和 css)
    config/              包含远程过程调用配置
    controllers/         包含远程过程调用的Web控制器类
    messages/            包含国际化的消息文件
    models/              包含远程过程调用的模型类
    modules/             包含远程过程调用的模块
    runtime/             包含运行时生成的文件,例如日志和缓存文件
    services/            包含远程过程调用的服务类
    tests/               包含远程过程调用应用程序的各种测试
    views/               包含Web应用程序的视图文件
    web/                 Web 应用根目录,包含 Web 入口文件
vendor/                  包含相关的第三方软件包
environments/            包含基于环境的覆盖
.gitignore               包含由 git 版本系统忽略的目录列表。如果你需要的东西从来没有到你的源代码存储库,添加它。
composer.json            Composer 配置文件
init                     初始化脚本描述文件
init.bat                 Windows 下的初始化脚本描述文件
LICENSE.md               许可信息。 把你的项目许可证放到这里。特别是开源醒目。
README.md                安装模板的基本信息。请考虑将其替换为有关您的项目及其安装的信息。
requirements.php         安装使用 Yii 需求检查器。
yii                      控制台应用程序引导。
yii.bat                  Windows下的控制台应用程序引导。


2. The Nginx-based single-domain configuration on the Yii 2 advanced project template, reference URL: https://www.shuijingwanwq.com/2018/08/16/2836/ . The api application is for cross-channel, the qq application is for Penguin, and the wx application is for WeChat Official Account. For example: POST http://api.channel-pub-api.localhost/qq/v1/articles/video?group_id=spider , which actually routes the final request to: \qq\rests\article\VideoCreateAction.php

3. Taking the publication of an article of type: video to Penguin as an example to illustrate the overall architecture design, asynchronous task execution is implemented in the Yii2 advanced template based on the Yii2 queue extension, reference URL: https://www.shuijingwanwq.com/2018/10/19/2952/ . Edit \common\config\main-local.php

PHP

        'copyAssetQueue' => [ // 复制资源文件队列
            'class' => 'yii\queue\redis\Queue',
            'redis' => 'redis', // Redis 连接组件或它的配置
            'channel' => 'cpa:queue:copy:asset', // 队列键前缀
            'ttr' => 10 * 60, // 作业处理的最长时间,单位(秒)
            'on afterExec' => ['common\components\queue\CopyAssetEventHandler', 'afterExec'], // 每次成功执行作业后
            'on afterError' => ['common\components\queue\CopyAssetEventHandler', 'afterError'], // 在作业执行期间发生未捕获的异常时
            'as log' => 'yii\queue\LogBehavior',
        ],
        'uploadAssetQueue' => [ // 上传资源文件队列
            'class' => 'yii\queue\redis\Queue',
            'redis' => 'redis', // Redis 连接组件或它的配置
            'channel' => 'cpa:queue:upload:asset', // 队列键前缀
            'ttr' => 2 * 60 * 60, // 作业处理的最长时间,单位(秒)
            'on afterExec' => ['common\components\queue\UploadAssetEventHandler', 'afterExec'], // 每次成功执行作业后
            'on afterError' => ['common\components\queue\UploadAssetEventHandler', 'afterError'], // 在作业执行期间发生未捕获的异常时
            'as log' => 'yii\queue\LogBehavior',
        ],
        'pubArticleQueue' => [ // 发布文章队列
            'class' => 'yii\queue\redis\Queue',
            'redis' => 'redis', // Redis 连接组件或它的配置
            'channel' => 'cpa:queue:pub:article', // 队列键前缀
            'ttr' => 5 * 60, // 作业处理的最长时间,单位(秒)
            'on afterExec' => ['common\components\queue\PubArticleEventHandler', 'afterExec'], // 每次成功执行作业后
            'on afterError' => ['common\components\queue\PubArticleEventHandler', 'afterError'], // 在作业执行期间发生未捕获的异常时
            'as log' => 'yii\queue\LogBehavior',
        ],
        'sourceCallbackQueue' => [ // 来源回调队列
            'class' => 'yii\queue\redis\Queue',
            'redis' => 'redis', // Redis 连接组件或它的配置
            'channel' => 'cpa:queue:source:callback', // 队列键前缀
            'ttr' => 5 * 60, // 作业处理的最长时间,单位(秒)
            'on afterExec' => ['common\components\queue\SourceCallbackEventHandler', 'afterExec'], // 每次成功执行作业后
            'on afterError' => ['common\components\queue\SourceCallbackEventHandler', 'afterError'], // 在作业执行期间发生未捕获的异常时
            'as log' => 'yii\queue\LogBehavior',
        ],

4. Implement the channel publication interface (Penguin): publish an article of type: video to the channel publication, edit \qq\rests\article\Action.php

PHP
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace qq\rests\article;
 
use Yii;
use yii\db\ActiveRecordInterface;
use yii\web\NotFoundHttpException;
use yii\web\ServerErrorHttpException;
 
/**
 * Action is the base class for action classes that implement RESTful API.
 *
 * For more details and usage information on Action, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class Action extends \yii\rest\Action
{
    /**
     * Returns the data model based on the primary key given.
     * If the data model is not found, a 404 HTTP exception will be raised.
     * @param string $id the ID of the model to be loaded. If the model has a composite primary key,
     * the ID must be a string of the primary key values separated by commas.
     * The order of the primary key values should follow that returned by the `primaryKey()` method
     * of the model.
     * @return ActiveRecordInterface the model found
     * @throws NotFoundHttpException if the model cannot be found
     */
    public function findModel($id)
    {
        if ($this->findModel !== null) {
            return call_user_func($this->findModel, $id, $this);
        }

        /* @var $modelClass ActiveRecordInterface */
        $modelClass = $this->modelClass;
        $keys = $modelClass::primaryKey();
        if (count($keys) > 1) {
            $values = explode(',', $id);
            if (count($keys) === count($values)) {
                $model = $modelClass::findOne(array_combine($keys, $values));
            }
        } elseif ($id !== null) {
            $model = $modelClass::findOne($id);
        }

        if (isset($model)) {
            return $model;
        }

        throw new NotFoundHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20002'), ['id' => $id])), 20002);
    }

    /**
     * 处理模型填充与验证
     * @param object $model 模型
     * @param array $requestParams 请求参数
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => true, // 成功
     * ]
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 20004, // 返回码
     *     'message' => '数据验证失败:企鹅号ID(UUID)是无效的。', // 说明
     * ]
     *
     * @throws ServerErrorHttpException
     */
    public static function handleLoadAndValidate($model, $requestParams)
    {
        // 把请求数据填充到模型中
        if (!$model->load($requestParams)) {
            return ['status' => false, 'code' => 40009, 'message' => Yii::t('error', '40009')];
        }
        // 验证模型
        if (!$model->validate()) {
            return self::handleValidateError($model);
        }

        return ['status' => true];
    }

    /**
     * 处理模型错误
     * @param object $model 模型
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 20004, // 返回码
     *     'message' => '数据验证失败:代码是无效的。', // 说明
     * ]
     *
     * @throws ServerErrorHttpException
     */
    public static function handleValidateError($model)
    {
        if ($model->hasErrors()) {
            $response = Yii::$app->getResponse();
            $response->setStatusCode(422, 'Data Validation Failed.');
            foreach ($model->getFirstErrors() as $message) {
                $firstErrors = $message;
                break;
            }
            return ['status' => false, 'code' => 20004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20004'), ['firstErrors' => $firstErrors]))];
        } elseif (!$model->hasErrors()) {
            throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
        }
    }

    /**
     * 处理模型错误
     * @param array $models 模型列表
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 20004, // 返回码
     *     'message' => '数据验证失败:代码是无效的。', // 说明
     * ]
     *
     * @throws ServerErrorHttpException
     */
    public static function handleValidateMultipleError($models)
    {
        foreach ($models as $model) {
            if ($model->hasErrors()) {
                $response = Yii::$app->getResponse();
                $response->setStatusCode(422, 'Data Validation Failed.');
                foreach ($model->getFirstErrors() as $message) {
                    $firstErrors = $message;
                    break;
                }
                return ['status' => false, 'code' => 20004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20004'), ['firstErrors' => $firstErrors]))];
            }
        }

        throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
    }
}


5. Implement the channel publication interface (Penguin): publish an article of type: video to the channel publication, edit \qq\rests\article\VideoCreateAction.php

PHP
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace qq\rests\article;
 
use Yii;
use qq\models\Channel;
use qq\models\QqArticleVideoCreateParam;
use qq\models\ChannelType;
use qq\models\QqTpAppPenguin;
use qq\models\Task;
use qq\models\ArticleType;
use qq\models\QqArticleType;
use qq\models\ArticleCategory;
use qq\models\QqArticle;
use qq\models\QqArticleMultivideos;
use qq\models\redis\qq_auth\QqTpAppPenguinAccessToken as RedisQqAuthQqTpAppPenguinAccessToken;
use qq\services\ChannelService;
use qq\services\ChannelAppSourceService;
use qq\services\ChannelTypeService;
use qq\services\QqCwAppService;
use qq\services\ArticleTypeService;
use qq\services\QqArticleTypeService;
use qq\services\QqArticleCategoryMultivideosService;
use qq\services\QqArticleService;
use yii\base\Model;
use yii\helpers\ArrayHelper;
use yii\web\ServerErrorHttpException;
use yii\web\HttpException;
 
/**
 * 发布文章类型:视频(视频)的文章至渠道发布  /articles/video(article/video-create)
 *
 * 1、请求参数列表
 * (1)channel_app_source_uuids:必填,渠道的应用的来源ID(UUID)
 * (2)source:必填,来源,xContent:内容库;vms:视频管理系统;cms:内容管理系统;spider:自媒体;channel-pub-api:渠道发布接口
 * (3)source_uuid:必填,来源ID(UUID)
 * (4)source_pub_user_id:必填,来源发布用户ID
 * (5)source_callback_url:必填,来源回调地址
 * (6)article_category_id:必填,文章分类ID
 * (7)title:必填,标题
 * (8)author:可选,作者,默认:空字符串
 * (9)source_article_id:必填,来源文章ID
 * (10)media_absolute_url:必填,视频文件的绝对URL
 * (11)tag:必填,视频文章标签,以英文半角逗号分隔,最多5个,每个标签最多8个字
 * (12)apply:可选,是否申请原创文章,0:否;1:是(需要用户具有发表视频原创文章资格否则无效)
 * (13)desc:必填,视频描述
 *
 * 2、输入数据验证规则
 * (1)查询渠道代码,qq:企鹅号是否存在,如果不存在,则返回失败
 * (2)判断渠道代码,qq:企鹅号的状态是否启用,如果未启用,则返回失败
 * (3)必填:channel_app_source_uuids、source、source_uuid、source_pub_user_id、source_callback_url、article_category_id、title、source_article_id、media_absolute_url、tag、desc
 * (4)默认值(''):author
 * (5)默认值(0):apply
 * (6)数组:channel_app_source_uuids
 * (7)存在性:channel_app_source_uuids 必须存在于渠道的应用的来源模型中,且其状态为 1:启用,且渠道的类型代码必须一致
 * (8)如果渠道的类型代码,qq_cw:企鹅号的内容网站应用,查询渠道的类型代码,qq_cw:企鹅号的内容网站应用是否存在,如果不存在,则返回失败
 * (9)如果渠道的类型代码,qq_cw:企鹅号的内容网站应用,判断渠道的类型代码,qq_cw:企鹅号的内容网站应用的状态是否启用,如果未启用,则返回失败
 * (10)存在性:如果渠道的类型代码,qq_cw:企鹅号的内容网站应用,channel_app_source_uuids 必须存在于企鹅号的内容网站应用模型中,且其状态为 1:启用
 * (11)如果渠道的类型代码,qq_tp:企鹅号的第三方服务平台应用,查询渠道的类型代码,qq_tp:企鹅号的第三方服务平台应用是否存在,如果不存在,则返回失败
 * (12)如果渠道的类型代码,qq_tp:企鹅号的第三方服务平台应用,判断渠道的类型代码,qq_tp:企鹅号的第三方服务平台应用的状态是否启用,如果未启用,则返回失败
 * (13)存在性:如果渠道的类型代码,qq_tp:企鹅号的第三方服务平台应用,channel_app_source_uuids 必须存在于企鹅号的第三方服务平台应用的企鹅媒体用户模型中,且其状态为 1:启用
 * (14)范围(['xContent', 'vms', 'cms', 'spider', 'channel-pub-api']):source
 * (15)字符串(最大长度:32):group_id、source
 * (16)字符串(最大长度:64):source_uuid
 * (17)字符串(最大长度:255):source_callback_url
 * (18)查询文章类型代码,video:视频 是否存在,如果不存在,则返回失败
 * (19)判断文章类型代码,video:视频 的状态是否启用,如果未启用,则返回失败
 * (20)查询企鹅号的文章类型代码,multivideos:视频 是否存在,如果不存在,则返回失败
 * (21)判断企鹅号的文章类型代码,multivideos:视频 的状态是否启用,如果未启用,则返回失败
 * (22)存在性:article_category_id 必须存在于文章分类模型中,且其状态为 1:启用
 * (23)查询企鹅号的文章类型(视频)的文章分类的文章分类ID,article_category_id 是否存在,如果不存在,则返回失败
 * (24)判断企鹅号的文章类型(视频)的文章分类的文章分类ID,article_category_id 的状态是否启用,如果未启用,则返回失败
 * (25)整数:source_pub_user_id
 * (26)字符串(最大长度:255):title
 * (27)字符串(最大长度:64):author
 * (28)整数:source_article_id
 * (29)字符串(最大长度:255):tag、desc
 * (30)整数:apply
 * (31)范围([0, 1]):apply
 * (32)存在性:如果渠道的类型代码,qq_tp:企鹅号的第三方服务平台应用,channel_app_source_uuids 必须存在于企鹅号的第三方服务平台应用的访问令牌(Redis)模型中,且其状态为 1:启用
 * (33)用户刷新令牌有效截止时间必须 大于等于 服务器时间
 *
 * 3、操作数据(事务)
 * (1)创建 MySQL 模型(任务)
 * (2)循环创建 MySQL 模型(渠道的应用的任务、企鹅号的内容网站应用的任务/企鹅号的第三方服务平台应用的企鹅媒体用户的任务)
 * (3)创建 MySQL 模型(企鹅号的文章)
 * (4)创建 MySQL 模型(企鹅号的文章类型(视频)的文章)
 * (5)复制来源的资源文件至渠道发布的资源目录,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步)
 *
 * For more details and usage information on VideoCreateAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class VideoCreateAction extends Action
{
    /**
     * @var string the scenario to be assigned to the new model before it is validated and saved.
     */
    public $scenario = Model::SCENARIO_DEFAULT;
    /**
     * @var string the name of the view action. This property is need to create the URL when the model is successfully created.
     */
    public $viewAction = 'view';

    /**
     * Creates a new model.
     * @return array
     * @throws ServerErrorHttpException if there is any error when creating the model
     * @throws \Throwable
     */
    public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }

        // 基于代码查找状态为启用的单个数据模型(渠道)
        $channelEnabledItem = ChannelService::findModelEnabledByCode(Channel::CODE_QQ);

        $requestParams = Yii::$app->getRequest()->getBodyParams();
        /* 判断请求体参数中租户ID是否存在 */
        if (!isset($requestParams['group_id'])) {
            $requestParams = ArrayHelper::merge($requestParams, ['group_id' => Yii::$app->params['groupId']]);
        }

        /* 视频(视频)的文章发布参数 */
        $qqArticleVideoCreateParam = new QqArticleVideoCreateParam();
        // 把请求数据填充到模型中
        if (!$qqArticleVideoCreateParam->load($requestParams, '')) {
            return ['code' => 40009, 'message' => Yii::t('error', '40009')];
        }
        // 验证模型
        if (!$qqArticleVideoCreateParam->validate()) {
            $qqArticleVideoCreateParamResult = self::handleValidateError($qqArticleVideoCreateParam);
            if ($qqArticleVideoCreateParamResult['status'] === false) {
                return ['code' => $qqArticleVideoCreateParamResult['code'], 'message' => $qqArticleVideoCreateParamResult['message']];
            }
        }

        /* 基于文章类型代码定义场景 */
        $this->scenario = QqArticle::SCENARIO_VIDEO_CREATE;

        /* 实例化多个模型 */

        // 渠道的应用的来源
        if (!is_array($requestParams['channel_app_source_uuids'])) {
            return ['code' => 40009, 'message' => Yii::t('error', '40009')];
        }
        // 基于多个UUID返回状态为启用,且渠道的类型代码必须一致的数据模型(渠道的应用的来源)列表
        $channelAppSourceEnabledItems = ChannelAppSourceService::findModelsEnabledByUuids($requestParams['channel_app_source_uuids']);

        // 获取、判断渠道的类型代码,获取应用的数据模型列表
        $channelTypeCode = $channelAppSourceEnabledItems[$requestParams['channel_app_source_uuids'][0]]->channel_type_code;
        if ($channelTypeCode == ChannelType::CODE_QQ_CW) {
            // 基于代码查找状态为启用的单个数据模型(渠道的类型)
            $channelTypeEnabledItem = ChannelTypeService::findModelEnabledByCode(ChannelType::CODE_QQ_CW);
            // 基于多个UUID返回状态为启用的数据模型(企鹅号的内容网站应用)列表
            $qqAppEnabledItems = QqCwAppService::findModelsEnabledByChannelAppSourceUuids($requestParams['channel_app_source_uuids']);
        } else {
            // 基于代码查找状态为启用的单个数据模型(渠道的类型)
            $channelTypeEnabledItem = ChannelTypeService::findModelEnabledByCode(ChannelType::CODE_QQ_TP);
            // 企鹅号的第三方服务平台应用的企鹅媒体用户
            if (!is_array($requestParams['uuid'])) {
                return ['code' => 40009, 'message' => Yii::t('error', '40009')];
            }
            $count = count($requestParams['uuid']);
            // 创建一个初始的 $qqTpAppPenguins 数组包含一个默认的模型
            $qqTpAppPenguins = [new QqTpAppPenguin([
                'scenario' => $this->scenario,
            ])];
            for($i = 1; $i < $count; $i++) {
                $qqTpAppPenguins[] = new QqTpAppPenguin([
                    'scenario' => $this->scenario,
                ]);
            }

            foreach ($requestParams['uuid'] as $key => $uuid) {
                // 转换视频(视频)的文章发布参数,多模型的填充、验证的实现
                $requestParams[$qqTpAppPenguins[0]->formName()][$key]['uuid'] = $uuid;
            }
            // 批量填充模型属性
            if (!Model::loadMultiple($qqTpAppPenguins, $requestParams, $qqTpAppPenguins[0]->formName())) {
                return ['code' => 40009, 'message' => Yii::t('error', '40009')];
            }
            // 批量验证模型
            if (!Model::validateMultiple($qqTpAppPenguins)) {
                $qqTpAppPenguinsResult = self::handleValidateMultipleError($qqTpAppPenguins);
                if ($qqTpAppPenguinsResult['status'] === false) {
                    return ['code' => $qqTpAppPenguinsResult['code'], 'message' => $qqTpAppPenguinsResult['message']];
                }
            }


        }

        // 任务
        $task = new Task([
            'scenario' => Task::SCENARIO_CREATE,
        ]);
        // 转换视频(视频)的文章发布参数,多模型的填充、验证的实现
        $requestParams[$task->formName()] = [
            'group_id' => $requestParams['group_id'],
            'source' => $qqArticleVideoCreateParam->source,
            'source_uuid' => $qqArticleVideoCreateParam->source_uuid,
            'source_pub_user_id' => $qqArticleVideoCreateParam->source_pub_user_id,
            'source_callback_url' => $qqArticleVideoCreateParam->source_callback_url,
        ];
        $taskResult = self::handleLoadAndValidate($task, $requestParams);
        if ($taskResult['status'] === false) {
            return ['code' => $taskResult['code'], 'message' => $taskResult['message']];
        }

        // 基于代码查找状态为启用的单个数据模型(文章类型)
        $articleTypeEnabledItem = ArticleTypeService::findModelEnabledByCode(ArticleType::CODE_VIDEO);

        // 基于代码查找状态为启用的单个数据模型(企鹅号的文章类型)
        $qqArticleTypeEnabledItem = QqArticleTypeService::findModelEnabledByCode(QqArticleType::CODE_MULTIVIDEOS);

        // 文章分类
        $articleCategory = new ArticleCategory([
            'scenario' => $this->scenario,
        ]);
        // 转换视频(视频)的文章发布参数,多模型的填充、验证的实现
        $requestParams[$articleCategory->formName()]['id'] = $qqArticleVideoCreateParam->article_category_id;
        $articleCategoryResult = self::handleLoadAndValidate($articleCategory, $requestParams);
        if ($articleCategoryResult['status'] === false) {
            return ['code' => $articleCategoryResult['code'], 'message' => $articleCategoryResult['message']];
        }

        // 基于文章分类ID查找状态为启用的单个数据模型(企鹅号的文章类型(视频)的文章分类)
        $qqArticleCategoryMultivideosEnabledItem = QqArticleCategoryMultivideosService::findModelEnabledByArticleCategoryId($qqArticleVideoCreateParam->article_category_id);

        // 企鹅号的文章
        $model = new $this->modelClass([
            'scenario' => $this->modelClass::SCENARIO_CREATE,
        ]);
        // 转换视频(视频)的文章发布参数,多模型的填充、验证的实现
        $requestParams[$model->formName()] = [
            'group_id' => $requestParams['group_id'],
            'article_category_id' => $qqArticleVideoCreateParam->article_category_id,
            'title' => $qqArticleVideoCreateParam->title,
            'author' => $qqArticleVideoCreateParam->author,
            'source_article_id' => $qqArticleVideoCreateParam->source_article_id,
        ];
        $modelResult = self::handleLoadAndValidate($model, $requestParams);
        if ($modelResult['status'] === false) {
            return ['code' => $modelResult['code'], 'message' => $modelResult['message']];
        }

        // 企鹅号的文章类型(视频)的文章
        $qqArticleMultivideos = new QqArticleMultivideos([
            'scenario' => QqArticleMultivideos::SCENARIO_CREATE,
        ]);
        // 转换视频(视频)的文章发布参数,多模型的填充、验证的实现
        $requestParams[$qqArticleMultivideos->formName()] = [
            'media' => $qqArticleVideoCreateParam->media_absolute_url,
            'tag' => $qqArticleVideoCreateParam->tag,
            'desc' => $qqArticleVideoCreateParam->desc,
            'apply' => $qqArticleVideoCreateParam->apply,
        ];
        $qqArticleMultivideosResult = self::handleLoadAndValidate($qqArticleMultivideos, $requestParams);
        if ($qqArticleMultivideosResult['status'] === false) {
            return ['code' => $qqArticleMultivideosResult['code'], 'message' => $qqArticleMultivideosResult['message']];
        }

        if ($channelTypeCode == ChannelType::CODE_QQ_TP) {
            // 企鹅号的第三方服务平台应用的访问令牌(Redis)
            // 创建一个初始的 $redisQqAuthQqTpAppAccessTokens 数组包含一个默认的模型
            $redisQqAuthQqTpAppAccessTokens = [new RedisQqAuthQqTpAppPenguinAccessToken([
                'scenario' => $this->scenario,
            ])];
            for($i = 1; $i < $count; $i++) {
                $redisQqAuthQqTpAppAccessTokens[] = new RedisQqAuthQqTpAppPenguinAccessToken([
                    'scenario' => $this->scenario,
                ]);
            }

            foreach ($requestParams['uuid'] as $key => $uuid) {
                // 转换视频(视频)的文章发布参数,多模型的填充、验证的实现
                $requestParams[$redisQqAuthQqTpAppAccessTokens[0]->formName()][$key]['qq_tp_app_penguin_uuid'] = $uuid;
            }
            // 批量填充模型属性
            if (!Model::loadMultiple($redisQqAuthQqTpAppAccessTokens, $requestParams, $redisQqAuthQqTpAppAccessTokens[0]->formName())) {
                return ['code' => 40009, 'message' => Yii::t('error', '40009')];
            }
            // 批量验证模型
            if (!Model::validateMultiple($redisQqAuthQqTpAppAccessTokens)) {
                $redisQqAuthQqTpAppAccessTokensResult = self::handleValidateMultipleError($redisQqAuthQqTpAppAccessTokens);
                if ($redisQqAuthQqTpAppAccessTokensResult['status'] === false) {
                    $qqTpAppPenguinUuids = [];
                    foreach ($redisQqAuthQqTpAppAccessTokens as $redisQqAuthQqTpAppAccessToken) {
                        if ($redisQqAuthQqTpAppAccessToken->hasErrors()) {
                            $qqTpAppPenguinUuids[] = $redisQqAuthQqTpAppAccessToken->qq_tp_app_penguin_uuid;
                        }
                    }
                    if (!empty($qqTpAppPenguinUuids)) {
                        $qqTpAppPenguinUuids = implode(";", $qqTpAppPenguinUuids);
                    }
                    throw new HttpException(302, Yii::t('error', Yii::t('error', Yii::t('error', '40020'), ['qq_tp_app_penguin_uuids' => $qqTpAppPenguinUuids])), 40008);
                }
            }
        }


        /* 操作数据(事务) */

        $qqArticleService = new QqArticleService();

        $result = $qqArticleService->videoCreate($channelEnabledItem, $channelTypeEnabledItem, $channelAppSourceEnabledItems, $qqAppEnabledItems, $articleTypeEnabledItem, $qqArticleTypeEnabledItem, $qqArticleCategoryMultivideosEnabledItem, $task, $model, $qqArticleMultivideos);
        if ($result['status'] === false) {
            throw new ServerErrorHttpException($result['message'], $result['code']);
        }

        return ['code' => 10000, 'message' => Yii::t('success', '30016'), 'data' => $result['data']];
    }
}


6. Implement the channel publication interface (Penguin): publish an article of type: video to the channel publication, edit \common\services\QqArticleService.php

PHP

    /**
     * 发布视频(视频)的文章至渠道发布
     *
     * @param object $channel 渠道
     * @param object $channelType 渠道的类型
     * @param array $channelAppSources 渠道的应用的来源列表
     * @param array $qqApps 企鹅号的应用列表
     * @param object $articleType 文章类型
     * @param object $qqArticleType 企鹅号的文章类型
     * @param object $qqArticleCategoryMultivideos 企鹅号的文章类型(视频)的文章
     * @param object $task 任务
     * 格式如下:
     * [
     *     'group_id' => 'spider', // 租户ID
     *     'source' => 'spider', // 来源,xContent:内容库;vms:视频管理系统;cms:内容管理系统;spider:自媒体;channel-pub-api:渠道发布接口
     *     'source_uuid' => '825e6d5e36468cc4bf536799ce3565cf', // 来源ID(UUID)
     *     'source_pub_user_id' => 1, // 来源发布用户ID
     *     'source_callback_url' => 'http://www.source_callback_url.com', // 来源回调地址
     * ]
     *
     * @param object $qqArticle 企鹅号的文章
     * 格式如下:
     * [
     *     'group_id' => 'spider', // 租户ID
     *     'article_category_id' => 226, // 文章分类ID
     *     'title' => '综艺节目 - 20181121 - 1', // 标题
     *     'author' => '综艺节目 - 20181121 - 1', // 作者
     *     'source_article_id' => 1, //  来源文章ID
     * ]
     *
     * @param object $qqArticleMultivideos 企鹅号的文章类型(视频)的文章
     * 格式如下:
     * [
     *     'media' => 'http://127.0.0.1/channel-pub-api/storage/spider/videos/3d35e17a32fb48cfb76f39a1b1bf33ce_h264_1200k_mp4', // 视频文件
     *     'tag' => '综艺', // 视频文章标签,以英文半角逗号分隔,最多5个,每个标签最多8个字
     *     'desc' => '综艺节目 - 20181121 - 1', // 视频描述
     *     'apply' => 0, // 是否申请原创文章,0:否;1:是(需要用户具有发表视频原创文章资格否则无效)
     * ]
     *
     * @param bool $isCopyAssetsAsync 是否复制来源的资源文件至渠道发布的资源目录,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步),默认为 true
     *
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => true, // 成功
     *     'data' => [ // array
     *         [ // object
     *             'channel_id' => 1, // 渠道ID
     *             'channel_code' => 'qq', // 渠道代码,qq:企鹅号;wx:微信公众帐号
     *             'channel_type_id' => 1, // 渠道的类型ID
     *             'channel_type_code' => 'qq_cw', // 渠道的类型代码,qq_cw:企鹅号的内容网站应用;qq_tp:企鹅号的第三方服务平台应用;wx:微信公众帐号应用
     *             'channel_app_source_id' => 1, // 渠道的应用的来源ID
     *             'channel_app_source_uuid' => 'a3f87610e17011e88f0154ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *             'task_id' => 8, // 任务ID
     *             'status' => 1, // 状态,0:禁用;1:启用
     *             'created_at' => 1541730602, // 创建时间
     *             'updated_at' => 1541730602, // 更新时间
     *             'uuid' => '5ce1f7f2e3c711e8bc2354ee75d2ebc1', // 渠道的应用的任务ID(UUID)
     *             'id' => 13, // ID
     *         ],
     *         [ // object
     *             'channel_id' => 1, // 渠道ID
     *             'channel_code' => 'qq', // 渠道代码,qq:企鹅号;wx:微信公众帐号
     *             'channel_type_id' => 1, // 渠道的类型ID
     *             'channel_type_code' => 'qq_cw', // 渠道的类型代码,qq_cw:企鹅号的内容网站应用;qq_tp:企鹅号的第三方服务平台应用;wx:微信公众帐号应用
     *             'channel_app_source_id' => 2, // 渠道的应用的来源ID
     *             'channel_app_source_uuid' => '2369c1d8e25211e8bbf154ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *             'task_id' => 8, // 任务ID
     *             'status' => 1, // 状态,0:禁用;1:启用
     *             'created_at' => 1541730603, // 创建时间
     *             'updated_at' => 1541730603, // 更新时间
     *             'uuid' => '5ce3a8d6e3c711e8b2bd54ee75d2ebc1', // 渠道的应用的任务ID(UUID)
     *             'id' => 14, // ID
     *         ],
     *     ]
     * ]
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 20004, // 返回码
     *     'message' => '文章类型代码:video,的状态为未启用', // 说明
     * ]
     *
     * @throws \Throwable
     */
    public function videoCreate($channel, $channelType, $channelAppSources, $qqApps, $articleType, $qqArticleType, $qqArticleCategoryMultivideos, $task, $qqArticle, $qqArticleMultivideos, $isCopyAssetsAsync = true)
    {

        if ($isCopyAssetsAsync) {
            $absoluteUrl = $qqArticleMultivideos->media;
        }

        /* 操作数据(事务) */
        $transaction = Yii::$app->db->beginTransaction();
        try {
            /* 创建 MySQL 模型(任务) */
            $taskService = new TaskService();
            $task->channel_id = $channel->id;
            $task->channel_code = $channel->code;
            $task->channel_type_id = $channelType->id;
            $task->channel_type_code = $channelType->code;
            $task->status = Task::STATUS_ENABLED;
            $taskServiceCreateResult = $taskService->create($task, false);
            if ($taskServiceCreateResult['status'] === false) {
                throw new ServerErrorHttpException($taskServiceCreateResult['message'], $taskServiceCreateResult['code']);
            }

            /* 循环创建 MySQL 模型(渠道的应用的任务、企鹅号的内容网站应用的任务/企鹅号的第三方服务平台应用的企鹅媒体用户的任务) */
            $channelAppTasks = [];
            $channelAppTaskService = new ChannelAppTaskService();
            $qqCwAppTaskService = new QqCwAppTaskService();
            foreach ($channelAppSources as $channelAppSource) {
                $channelAppTask = new ChannelAppTask();
                $channelAppTask->channel_id = $channel->id;
                $channelAppTask->channel_code = $channel->code;
                $channelAppTask->channel_type_id = $channelType->id;
                $channelAppTask->channel_type_code = $channelType->code;
                $channelAppTask->channel_app_source_id = $channelAppSource->id;
                $channelAppTask->channel_app_source_uuid = $channelAppSource->uuid;
                $channelAppTask->task_id = $task->id;
                $channelAppTask->status = ChannelAppTask::STATUS_ENABLED;
                $channelAppTaskServiceCreateResult = $channelAppTaskService->create($channelAppTask, false);
                if ($channelAppTaskServiceCreateResult['status'] === false) {
                    throw new ServerErrorHttpException($channelAppTaskServiceCreateResult['message'], $channelAppTaskServiceCreateResult['code']);
                }

                if ($channelType->code == ChannelType::CODE_QQ_CW) {
                    $qqCwAppTask = new QqCwAppTask();
                    $qqCwAppTask->channel_app_task_id = $channelAppTask->id;
                    $qqCwAppTask->channel_app_task_uuid = $channelAppTask->uuid;
                    $qqCwAppTask->qq_cw_app_id = $qqApps[$channelAppSource->uuid]->id;
                    $qqCwAppTask->task_id = $task->id;
                    $qqCwAppTask->status = QqCwAppTask::STATUS_WAIT_PUBLISH;
                    $qqCwAppTaskServiceCreateResult = $qqCwAppTaskService->create($qqCwAppTask, false);
                    if ($qqCwAppTaskServiceCreateResult['status'] === false) {
                        throw new ServerErrorHttpException($qqCwAppTaskServiceCreateResult['message'], $qqCwAppTaskServiceCreateResult['code']);
                    }
                } else {

                }

                $channelAppTask->status = $qqCwAppTask->status;
                $channelAppTasks[] = $channelAppTask;
            }

            /* 创建 MySQL 模型(企鹅号的文章) */
            $qqArticle->qq_app_type = $channelType->code == ChannelType::CODE_QQ_CW ? QqArticle::QQ_APP_TYPE_CW : QqArticle::QQ_APP_TYPE_TP;
            $qqArticle->article_type_id = $articleType->id;
            $qqArticle->qq_article_type_id = $qqArticleType->id;
            $qqArticle->qq_article_category_id = $qqArticleCategoryMultivideos->id;
            $qqArticle->task_id = $task->id;
            $qqArticle->status = QqArticle::STATUS_ENABLED;
            $thisCreateResult = $this->create($qqArticle, false);
            if ($thisCreateResult['status'] === false) {
                throw new ServerErrorHttpException($thisCreateResult['message'], $thisCreateResult['code']);
            }

            /* 创建 MySQL 模型(企鹅号的文章类型(视频)的文章) */
            $qqArticleMultivideosService = new QqArticleMultivideosService();
            $qqArticleMultivideos->qq_article_id = $qqArticle->id;
            $qqArticleMultivideos->category = $qqArticleCategoryMultivideos->id;
            $qqArticleMultivideos->md5 = '';
            $qqArticleMultivideos->media = '';
            $qqArticleMultivideos->vid = '';
            $qqArticleMultivideos->task_id = $task->id;
            $qqArticleMultivideos->status = QqArticleMultivideos::STATUS_ENABLED;
            $qqArticleMultivideosServiceCreateResult = $qqArticleMultivideosService->create($qqArticleMultivideos, false);
            if ($qqArticleMultivideosServiceCreateResult['status'] === false) {
                throw new ServerErrorHttpException($qqArticleMultivideosServiceCreateResult['message'], $qqArticleMultivideosServiceCreateResult['code']);
            }

            $transaction->commit();
        } catch(\Throwable $e) {
            $transaction->rollBack();
            throw $e;
        }

        if ($isCopyAssetsAsync) {
            /* 复制来源的资源文件至渠道发布的资源目录,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步) */
            $assetServiceCopyAssetsAsyncData = [
                'channel_id' => $channel->id,
                'channel_code' => $channel->code,
                'channel_type_id' => $channelType->id,
                'channel_type_code' => $channelType->code,
                'source' => $task->source,
                'task_id' => $task->id,
            ];
            $assets = [
                [
                    'type' => Asset::TYPE_VIDEO,
                    'channel_article_id' => $qqArticle->id,
                    'absolute_url' => $absoluteUrl,
                ],
            ];
            $assetServiceCopyAssetsAsyncResult = AssetService::copyAssetsAsync($assetServiceCopyAssetsAsyncData, $assets);
        }

        return ['status' => true, 'data' => $channelAppTasks];
    }

7. Implement the channel publication interface (Penguin): publish an article of type: video to the channel publication, copy the source resource files to the channel publication’s resource directory. After the queue task executes successfully, call the corresponding service; otherwise, insert a publication log (asynchronously). Edit \common\services\AssetService.php

PHP

    /**
     * 复制来源的资源文件至渠道发布的资源目录,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步)
     * @param array $data 数据
     * 格式如下:
     * [
     *     'channel_id' => 1, // 渠道ID
     *     'channel_code' => 'qq', // 渠道代码,qq:企鹅号;wx:微信公众帐号
     *     'channel_type_id' => 1, // 渠道的类型ID
     *     'channel_type_code' => 'qq_cw', // 渠道的类型代码,qq_cw:企鹅号的内容网站应用;qq_tp:企鹅号的第三方服务平台应用;wx:微信公众帐号应用
     *     'source' => 'spider', // 来源,xContent:内容库;vms:视频管理系统;cms:内容管理系统;spider:自媒体;channel-pub-api:渠道发布接口
     *     'task_id' => 1, // 任务ID
     * ]
     *
     * @param array $assets 来源的资源文件的绝对URL
     * 格式如下:
     * [
     *     [
     *         'type' => 'image', // 资源文件的类型,image:图片;video:视频
     *         'channel_article_id' => 1, // 渠道的文章ID
     *         'absolute_url' => 'http://localhost/spider/storage/spider/images/1.png', // 来源的资源文件的绝对URL
     *     ],
     *     [
     *         'type' => 'video', // 资源文件的类型,image:图片;video:视频
     *         'channel_article_id' => 1, // 渠道的文章ID
     *         'absolute_url' => 'http://127.0.0.1/channel-pub-api/storage/spider/videos/7月份北上广深等十大城市租金环比上涨 看东方 20180820 高清_高清.mp4', // 来源的资源文件的绝对URL
     *     ],
     * ]
     *
     * @throws Exception execution failed
     */
    public static function copyAssetsAsync($data, $assets)
    {
        // file_put_contents('E:/wwwroot/channel-pub-api/qq/runtime/copy-assets-async-data-' . $data['task_id'] . time() . '.txt', print_r($data, true));
        // file_put_contents('E:/wwwroot/channel-pub-api/qq/runtime/copy-assets-async-assets-' . $data['task_id'] . time() . '.txt', print_r($assets, true));
        // 批量创建资源
        static::createMultiple($data, $assets);

        // 将任务发送到队列(复制资源文件队列),通过标准工作人员进行处理
        Yii::$app->copyAssetQueue->push(new CopyAssetJob([
            'taskId' => $data['task_id'],
        ]));
    }

8. POST http://api.channel-pub-api.localhost/qq/v1/articles/video?group_id=spider , executed successfully. I intentionally made the value of media_absolute_url incorrect to test the subsequent processing after the copy resource file queue job fails to execute; I intentionally made the value of source_callback_url incorrect to test the subsequent processing after the source callback queue job fails to execute.

Request Body

JSON

{
	"channel_app_source_uuids": ["bba4e024eba111e89fa754ee75d2ebc1"],
	"source": "spider",
	"source_uuid": "825e6d5e36468cc4bf536799ce3565cf",
	"source_pub_user_id": 1,
	"source_callback_url": "http://www.source_callback_url.com",
	"article_category_id": 226,
	"title": "综艺节目 - 20181123 - 1",
	"author": "综艺节目 - 20181123 - 1",
	"source_article_id": 1,
	"media_absolute_url": "http://127.0.0.1/channel-pub-api/storage/spider/videos/3d35e17a32fb48cfb76f39a1b1bf33ce_h264_1200k_mp4",
	"tag": "综艺",
	"apply": 0,
	"desc": "综艺节目 - 20181123 - 1"
}

Response Body

JSON

{
    "code": 10000,
    "message": "发布文章类型:视频(视频)的文章成功",
    "data": [
        {
            "channel_id": 1,
            "channel_code": "qq",
            "channel_type_id": 1,
            "channel_type_code": "qq_cw",
            "channel_app_source_id": 6,
            "channel_app_source_uuid": "bba4e024eba111e89fa754ee75d2ebc1",
            "task_id": 19,
            "status": 1,
            "created_at": 1542960204,
            "updated_at": 1542960204,
            "uuid": "40e5a938eef611e88d6254ee75d2ebc1",
            "id": 19
        }
    ]
}

9. Execute the SQL statement as follows:

SQL

SELECT * FROM `cpa_channel` WHERE (`code`='qq') AND (`is_deleted`=0)
SELECT * FROM `cpa_channel_app_source` WHERE (`uuid`='bba4e024eba111e89fa754ee75d2ebc1') AND (`is_deleted`=0)
SELECT * FROM `cpa_channel_type` WHERE (`code`='qq_cw') AND (`is_deleted`=0)
SELECT * FROM `cpa_qq_cw_app` WHERE (`channel_app_source_uuid`='bba4e024eba111e89fa754ee75d2ebc1') AND (`is_deleted`=0)
SELECT * FROM `cpa_article_type` WHERE (`code`='video') AND (`is_deleted`=0)
SELECT * FROM `cpa_qq_article_type` WHERE (`code`='multivideos') AND (`is_deleted`=0)
SELECT EXISTS(SELECT * FROM `cpa_article_category` WHERE (`cpa_article_category`.`id`=226) AND ((`is_deleted`=0) AND (`status`=1)))
SELECT * FROM `cpa_qq_article_category_multivideos` WHERE (`article_category_id`=226) AND (`is_deleted`=0)
INSERT INTO `cpa_task` (`group_id`, `source`, `source_uuid`, `source_pub_user_id`, `source_callback_url`, `channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `status`, `created_at`, `updated_at`) VALUES ('spider', 'spider', '825e6d5e36468cc4bf536799ce3565cf', 1, 'http://www.source_callback_url.com', 1, 'qq', 1, 'qq_cw', 1, 1542960204, 1542960204)
INSERT INTO `cpa_channel_app_task` (`channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `channel_app_source_id`, `channel_app_source_uuid`, `task_id`, `status`, `created_at`, `updated_at`, `uuid`) VALUES (1, 'qq', 1, 'qq_cw', 6, 'bba4e024eba111e89fa754ee75d2ebc1', 19, 1, 1542960204, 1542960204, '40e5a938eef611e88d6254ee75d2ebc1')
INSERT INTO `cpa_qq_cw_app_task` (`channel_app_task_id`, `channel_app_task_uuid`, `qq_cw_app_id`, `task_id`, `status`, `created_at`, `updated_at`) VALUES (19, '40e5a938eef611e88d6254ee75d2ebc1', 5, 19, 1, 1542960205, 1542960205)
INSERT INTO `cpa_qq_article` (`group_id`, `article_category_id`, `title`, `author`, `source_article_id`, `qq_app_type`, `article_type_id`, `qq_article_type_id`, `qq_article_category_id`, `task_id`, `status`, `created_at`, `updated_at`) VALUES ('spider', 226, '综艺节目 - 20181123 - 1', '综艺节目 - 20181123 - 1', 1, 'cw', 3, 2, 2808, 19, 1, 1542960205, 1542960205)
INSERT INTO `cpa_qq_article_multivideos` (`media`, `tag`, `desc`, `apply`, `qq_article_id`, `category`, `md5`, `vid`, `task_id`, `status`, `created_at`, `updated_at`) VALUES ('', '综艺', '综艺节目 - 20181123 - 1', 0, 19, 2808, '', '', 19, 1, 1542960205, 1542960205)
INSERT INTO `cpa_asset` (`channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `source`, `type`, `absolute_url`, `relative_path`, `size`, `task_id`, `channel_article_id`, `status`, `is_deleted`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 'qq', 1, 'qq_cw', 'spider', 'video', 'http://127.0.0.1/channel-pub-api/storage/spider/videos/3d35e17a32fb48cfb76f39a1b1bf33ce_h264_1200k_mp4', '', 0, 19, 19, 1, 0, 1542960205, 1542960205, 0)

10. View the status information of the 4 queues

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

11. Queue job: copy source resource files to the channel publication’s resource directory, edit \common\jobs\CopyAssetJob.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/10/22
 * Time: 17:10
 */
 
namespace common\jobs;
 
use Yii;
use common\logics\Asset;
use common\services\TaskService;
use common\services\AssetService;
use yii\web\ServerErrorHttpException;
 
/**
 * 复制来源的资源文件至渠道发布的资源目录
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class CopyAssetJob extends Job
{
    public $taskId;

    /*
     * @throws ServerErrorHttpException 如果基于任务ID查找状态为启用的资源列表为空,将抛出 500 HTTP 异常
     */
    public function execute($queue)
    {
        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($this->taskId);

        // 基于任务ID查找状态为启用的资源列表
        $assetEnabledItems = Asset::findAllEnabledByTaskId($this->taskId);

        if (empty($assetEnabledItems)) {
            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35020'), ['task_id' => $this->taskId])), 35020);
        }

        $source = $taskEnabledItem->source;
        $assets = [];
        foreach ($assetEnabledItems as $assetEnabledItem) {
            $assets[] = [
                'type' => $assetEnabledItem->type,
                'absolute_url' => $assetEnabledItem->absolute_url,
            ];
        }

        // 复制来源的资源文件至渠道发布的资源目录,返回相对路径(同步)
        $assetServiceCopyAssetsSyncResult = AssetService::copyAssetsSync($source, $assets);
        foreach ($assetEnabledItems as $key => $assetEnabledItem) {
            $assetEnabledItem->relative_path = $assetServiceCopyAssetsSyncResult[$key]['relative_path'];
            // 取得文件大小,单位(字节)
            $assetEnabledItem->size = filesize(Yii::$app->params['channelPubApi']['asset'][$assetEnabledItem->type]['basePath'] . $assetServiceCopyAssetsSyncResult[$key]['relative_path']);
            $assetEnabledItems[$key] = $assetEnabledItem;
        }

        // 批量更新资源
        Asset::updateMultiple($assetEnabledItems);
    }
}

12. Queue event handler (after each successful job execution): call the corresponding service (after the job executes successfully) for subsequent processing. If calling the corresponding service fails (i.e., the service throws an exception), only insert a system log. Queue event handler (when an uncaught exception occurs during job execution): call the corresponding service (after the job execution fails) for subsequent processing. If calling the corresponding service fails (i.e., the service throws an exception), only insert a system log. Edit \common\components\queue\CopyAssetEventHandler.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/10/23
 * Time: 14:23
 */
 
namespace common\components\queue;
 
use Yii;
use common\services\TaskService;
use yii\base\Component;
use yii\web\NotFoundHttpException;
use yii\web\UnprocessableEntityHttpException;
use yii\queue\ExecEvent;
 
 
/**
 * Class CopyAssetEventHandler
 * @package common\components\queue
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class CopyAssetEventHandler extends Component
{
    /**
     * @param ExecEvent $event
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     */
    public static function afterExec(ExecEvent $event)
    {
        $taskId = $event->job->taskId;
        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($taskId);

        // 调用相应服务进行后续处理
        $serviceClass = 'common\services\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $taskEnabledItem->channel_type_code))) . 'AssetService'; // 例:common\services\QqCwAssetService
        $serviceAction = 'copyAssetExecHandler';
        $serviceClass::$serviceAction($taskEnabledItem->id);
    }

    /**
     * @param ExecEvent $event
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     */
    public static function afterError(ExecEvent $event)
    {
        $taskId = $event->job->taskId;
        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($taskId);

        // 调用相应服务进行后续处理
        $serviceClass = 'common\services\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $taskEnabledItem->channel_type_code))) . 'AssetService'; // 例:common\services\QqCwAssetService
        $serviceAction = 'copyAssetErrorHandler';
        $serviceClass::$serviceAction($taskEnabledItem->id, $event->error);

    }
}

13. Subsequent processing after the copy resource file queue job executes successfully: file upload. After the upload resource file queue task executes successfully, call the corresponding service; otherwise, insert a publication log (asynchronously). Taking Penguin as an example, edit \common\services\QqCwAssetService.php

PHP

    /**
     * 复制资源文件队列的作业执行成功后的后续处理
     *
     * @param int $taskId 任务ID
     * 格式如下:1
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public static function copyAssetExecHandler($taskId)
    {
        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($taskId);

        // 基于条件查找状态为启用的资源列表
        $assetEnabledWhere = [
            'channel_id' => $taskEnabledItem->channel_id,
            'channel_type_id' => $taskEnabledItem->channel_type_id,
            'type' => Asset::TYPE_VIDEO,
            'task_id' => $taskEnabledItem->id,
        ];
        $assetEnabledItems = Asset::findAllEnabledByWhere($assetEnabledWhere);

        if (empty($assetEnabledItems)) {

        } else {
            // 企鹅号的内容网站应用的视频文件分片上传,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步)
            $assets = [];
            foreach ($assetEnabledItems as $assetEnabledItem) {
                $assets[] = [
                    'job_type' => UploadAssetJob::JOB_TYPE_VIDEO_MULTIPART,
                    'id' => $assetEnabledItem->id,
                ];
            }

            static::uploadAssetVideoMultipartAsync($taskId, $assets);
        }
    }

14. Subsequent processing after the copy resource file queue job fails to execute: batch update tasks based on the task ID (scenario: after the copy resource file queue job fails, the number of publishable times decreases by 1, status, 3: Publishing (Failed)); insert a publication log, and push the job to the source callback queue (asynchronously). Taking Penguin as an example, edit \common\services\QqCwAssetService.php

PHP

    /**
     * 复制资源文件队列的作业执行失败后的后续处理
     *
     * @param int $taskId 任务ID
     * 格式如下:1
     * @param object $eventError 事件错误
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public static function copyAssetErrorHandler($taskId, $eventError)
    {
        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($taskId);

        // 基于任务ID查找状态为启用的资源列表
        $channelAppTaskEnabledItems = ChannelAppTask::findAllEnabledByTaskId($taskId);

        if (empty($channelAppTaskEnabledItems)) {
            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35021'), ['task_id' => $taskId])), 35021);
        }

        // 基于任务ID批量更新企鹅号的内容网站应用的任务(场景:复制资源文件队列的作业执行失败后,可发布次数减1,状态,3:发布中(已失败))
        QqCwAppTask::updateMultiplePublishErrorByTaskId($taskId);

        $pubLogDatas = [];
        foreach ($channelAppTaskEnabledItems as $key => $channelAppTaskEnabledItem) {
            // 基于渠道的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的任务)
            $qqCwAppTaskItem = QqCwAppTaskService::findModelByChannelAppTaskId($channelAppTaskEnabledItem->id);
            $pubLogDatas[$key] = [
                'channel_app_source_uuid' => $channelAppTaskEnabledItem->channel_app_source_uuid,
                'channel_app_task_uuid' => $channelAppTaskEnabledItem->uuid,
                'channel_code' => $channelAppTaskEnabledItem->channel_code,
                'channel_type_code' => $channelAppTaskEnabledItem->channel_type_code,
                'channel_app_task_status' => $qqCwAppTaskItem->status,
            ];
        }

        // 发布任务失败后,插入发布日志,将作业推送至来源回调队列(异步)
        SourceCallbackService::sourceCallbackAsync($channelAppTaskEnabledItems, $eventError->getCode(), $eventError->getMessage(), $pubLogDatas, PubLog::STATUS_ERROR);

        // 基于企鹅号的内容网站应用的任务ID查找状态为启用的单个资源(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联)
        $wxTaskQqCwTaskRelationItem = WxTaskQqCwTaskRelation::findOneEnabledByQqCwTaskId($taskEnabledItem->id);

        if (isset($wxTaskQqCwTaskRelationItem)) {
            WxAssetService::copyAssetErrorHandler($wxTaskQqCwTaskRelationItem->wx_task_id, $eventError);
        }

    }

15. After the publication task succeeds or fails, insert a publication log, and push the job to the source callback queue (asynchronously); subsequent processing after the source callback queue job executes successfully; subsequent processing after the source callback queue job fails to execute. Edit \common\services\SourceCallbackService.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/10/23
 * Time: 19:48
 */
 
namespace common\services;
 
use Yii;
use common\logics\PubLog;
use common\jobs\SourceCallbackJob;
use yii\db\Exception;
use yii\helpers\ArrayHelper;
use yii\helpers\Json;
use yii\web\NotFoundHttpException;
 
/**
 * 来源回调服务
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class SourceCallbackService extends Service
{
    /**
     * 发布任务成功/失败后,插入发布日志,将作业推送至来源回调队列(异步)
     *
     * @param array $channelAppTasks 渠道的应用的任务列表
     * 格式如下:
     * [
     *     [ // object
     *         'id' => 18, // ID
     *         'uuid' => '967f4948ee0211e8a99754ee75d2ebc1', // 渠道的应用的任务ID(UUID)
     *         'channel_id' => 1, // 渠道ID
     *         'channel_code' => 'qq', // 渠道代码,qq:企鹅号;wx:微信公众帐号
     *         'channel_type_id' => 1, // 渠道的类型ID
     *         'channel_type_code' => 'qq_cw', // 渠道的类型代码,qq_cw:企鹅号的内容网站应用;qq_tp:企鹅号的第三方服务平台应用;wx:微信公众帐号应用
     *         'channel_app_source_id' => 6, // 渠道的应用的来源ID
     *         'channel_app_source_uuid' => 'bba4e024eba111e89fa754ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *         'task_id' => 18, // 任务ID
     *         'status' => 1, // 状态,0:禁用;1:启用
     *         'is_deleted' => 0, // 是否被删除,0:否;1:是
     *         'created_at' => 1542855551, // 创建时间
     *         'updated_at' => 1542855551, // 更新时间
     *         'deleted_at' => 0, // 删除时间
     *     ],
     *     [ // object
     *         'id' => 19, // ID
     *         'uuid' => '52f10106ed5511e88f8354ee75d2ebc1', // 渠道的应用的任务ID(UUID)
     *         'channel_id' => 1, // 渠道ID
     *         'channel_code' => 'qq', // 渠道代码,qq:企鹅号;wx:微信公众帐号
     *         'channel_type_id' => 1, // 渠道的类型ID
     *         'channel_type_code' => 'qq_cw', // 渠道的类型代码,qq_cw:企鹅号的内容网站应用;qq_tp:企鹅号的第三方服务平台应用;wx:微信公众帐号应用
     *         'channel_app_source_id' => 6, // 渠道的应用的来源ID
     *         'channel_app_source_uuid' => 'bba4e024eba111e89fa754ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *         'task_id' => 19, // 任务ID
     *         'status' => 1, // 状态,0:禁用;1:启用
     *         'is_deleted' => 0, // 是否被删除,0:否;1:是
     *         'created_at' => 1542855551, // 创建时间
     *         'updated_at' => 1542855551, // 更新时间
     *         'deleted_at' => 0, // 删除时间
     *     ],
     * ]
     *
     * @param int $code 代码
     * @param string $message 说明
     * @param array $datas 数据
     * 格式如下:
     * [
     *     [
     *         'channel_app_source_uuid' => 'bba4e024eba111e89fa754ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *         'channel_app_task_uuid' => '967f4948ee0211e8a99754ee75d2ebc1', // 渠道的应用的任务ID(UUID)
     *         'channel_code' => 'qq', // 渠道代码,qq:企鹅号;wx:微信公众帐号
     *         'channel_type_code' => 'qq_cw', // 渠道的类型代码,qq_cw:企鹅号的内容网站应用;qq_tp:企鹅号的第三方服务平台应用;wx:微信公众帐号应用
     *         'channel_app_task_status' => 3, // 渠道的应用的任务状态,0:禁用;1:待发布;2:发布中;3:发布中(已失败);4:审核中;5:未发布;6:已发布
     *     ],
     *     [
     *         'channel_app_source_uuid' => 'bba4e024eba111e89fa754ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *         'channel_app_task_uuid' => '52f10106ed5511e88f8354ee75d2ebc1', // 渠道的应用的任务ID(UUID)
     *         'channel_code' => 'qq', // 渠道代码,qq:企鹅号;wx:微信公众帐号
     *         'channel_type_code' => 'qq_cw', // 渠道的类型代码,qq_cw:企鹅号的内容网站应用;qq_tp:企鹅号的第三方服务平台应用;wx:微信公众帐号应用
     *         'channel_app_task_status' => 3, // 渠道的应用的任务状态,0:禁用;1:待发布;2:发布中;3:发布中(已失败);4:审核中;5:未发布;6:已发布
     *     ],
     * ]
     *
     * @param int $status 状态,0:禁用;1:成功;2:失败
     *
     * @throws Exception execution failed
     */
    public static function sourceCallbackAsync($channelAppTasks, $code, $message, $datas, $status)
    {
        // 循环创建 MySQL 模型(发布日志)
        $pubLogData = [];
        $time = time();
        foreach ($channelAppTasks as $key => $channelAppTask) {
            $pubLogData[] = [
                'channel_id' => $channelAppTask['channel_id'],
                'channel_code' => $channelAppTask['channel_code'],
                'channel_type_id' => $channelAppTask['channel_type_id'],
                'channel_type_code' => $channelAppTask['channel_type_code'],
                'task_id' => $channelAppTask['task_id'],
                'channel_app_task_id' => $channelAppTask['id'],
                'channel_app_task_uuid' => $channelAppTask['uuid'],
                'code' => $code,
                'message' => $message,
                'data' => Json::encode($datas[$key], 0),
                'have_callback_number' => PubLog::HAVE_CALLBACK_NUMBER_DEFAULT,
                'callback_status' => PubLog::CALLBACK_STATUS_NO,
                'status' => $status,
                'is_deleted' => PubLog::IS_DELETED_NO,
                'created_at' => $time,
                'updated_at' => $time,
                'deleted_at' => PubLog::DELETED_AT_DEFAULT,
            ];
        }

        // 获取渠道的应用的任务ID值列表
        $channelAppTaskIds = ArrayHelper::getColumn($pubLogData, 'channel_app_task_id');

        // 基于多个渠道的应用的任务ID查找资源列表(发布日志)
        $pubLogItems = PubLog::findAllByChannelAppTaskIds($channelAppTaskIds);

        // 遍历资源列表,如果数据中已经存在,则销毁
        if (!empty($pubLogItems)) {
            $pubLogData = ArrayHelper::index($pubLogData, 'channel_app_task_id');
            foreach ($pubLogItems as $pubLogItem) {
                unset($pubLogData[$pubLogItem->channel_app_task_id]);
            }
        }

        if (!empty($pubLogData)) {
            // 批量创建资源
            $pubLog = new PubLog();
            $pubLog->createMultiple($pubLogData);

            // 将任务发送到队列,通过标准工作人员进行处理
            foreach ($pubLogData as $value) {
                Yii::$app->sourceCallbackQueue->push(new SourceCallbackJob([
                    'channelAppTaskId' => $value['channel_app_task_id'],
                ]));
            }
        }
    }

    /**
     * 来源回调队列的作业执行成功后的后续处理
     *
     * @param int $channelAppTaskId 渠道的应用的任务ID
     * 格式如下:1
     */
    public static function sourceCallbackExecHandler($channelAppTaskId)
    {
        // 基于渠道的应用的任务ID更新发布日志(场景:来源回调队列的作业执行成功后,可回调次数减 1;回调状态,1:成功)
        PubLog::updateMultipleCallbackStatusSuccesByChannelAppTaskId($channelAppTaskId);
    }

    /**
     * 来源回调队列的作业执行失败后的后续处理
     *
     * @param int $channelAppTaskId 渠道的应用的任务ID
     * 格式如下:1
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     */
    public static function sourceCallbackErrorHandler($channelAppTaskId)
    {
        // 基于渠道的应用的任务ID更新发布日志(场景:来源回调队列的作业执行失败后,可回调次数减 1;回调状态,2:失败)
        PubLog::updateMultipleCallbackStatusErrorByChannelAppTaskId($channelAppTaskId);

        // 基于渠道的应用的任务ID查找单个数据模型(发布日志)
        $pubLogItem = PubLogService::findModelByChannelAppTaskId($channelAppTaskId);

        // 判断可回调次数,如果大于 0,将任务重新发送到来源回调队列
        if ($pubLogItem->have_callback_number > 0) {
            // 将任务发送到队列,通过标准工作人员进行处理(延时 10 分钟运行)
            Yii::$app->sourceCallbackQueue->delay(Yii::$app->params['sourceCallbackQueue']['delay'])->push(new SourceCallbackJob([
                'channelAppTaskId' => $channelAppTaskId,
            ]));
        }
    }


}

16. The run command fetches and executes tasks in a loop (copy resource file queue) until the queue is empty

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/run --verbose=1 --isolate=1 --color=0
2018-11-23 16:17:05 [pid: 140472] - Worker is started
2018-11-23 16:17:06 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 140472) - Started
2018-11-23 16:17:06 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 140472) - Error (0.274 s)
> yii\web\NotFoundHttpException: Source resource file: 0, does not exist
2018-11-23 16:17:06 [pid: 140472] - Worker is stopped (0:00:01)

17. The copy resource file queue job failed to execute, the execution result meets expectations, the publication log has been inserted, and the job has been pushed to the source callback queue, as shown in Figure 1

The copy resource file queue job failed to execute, the execution result meets expectations, the publication log has been inserted, and the job has been pushed to the source callback queue
Figure 1
Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0

18. The run command fetches and executes tasks in a loop (source callback queue) until the queue is empty

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/run --verbose=1 --isolate=1 --color=0
2018-11-23 16:20:16 [pid: 143548] - Worker is started
2018-11-23 16:20:16 [1] common\jobs\SourceCallbackJob (attempt: 1, pid: 143548) - Started
2018-11-23 16:20:17 [1] common\jobs\SourceCallbackJob (attempt: 1, pid: 143548) - Error (0.745 s)
> yii\httpclient\Exception: Curl error: #6 - Could not resolve host: www.source_callback_url.com
2018-11-23 16:20:17 [pid: 143548] - Worker is stopped (0:00:01)

19. The source callback queue job failed to execute, the execution result meets expectations, the publication log has been updated, and the publication log is updated based on the channel application’s task ID (the number of callback times decreases by 1; callback status, 2: Failed); if the number of callback times is greater than 0, the task is resent to the source callback queue and runs with a delay of 1 minute (the delay time is set to 10 minutes in the production environment). As shown in Figure 2, Figure 3

The source callback queue job failed to execute, the execution result meets expectations, the publication log has been updated, and the publication log is updated based on the channel application's task ID (the number of callback times decreases by 1; callback status, 2: Failed)
Figure 2
If the number of callback times is greater than 0, the task is resent to the source callback queue and runs with a delay of 1 minute (the delay time is set to 10 minutes in the production environment)
Figure 3
Plaintext

PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 1
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/run --verbose=1 --isolate=1 --color=0
2018-11-23 16:23:16 [pid: 141492] - Worker is started
2018-11-23 16:23:16 [2] common\jobs\SourceCallbackJob (attempt: 1, pid: 141492) - Started
2018-11-23 16:23:16 [2] common\jobs\SourceCallbackJob (attempt: 1, pid: 141492) - Error (0.195 s)
> yii\httpclient\Exception: Curl error: #6 - Could not resolve host: www.source_callback_url.com
2018-11-23 16:23:16 [pid: 141492] - Worker is stopped (0:00:00)
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 1
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/run --verbose=1 --isolate=1 --color=0
2018-11-23 16:25:16 [pid: 145080] - Worker is started
2018-11-23 16:25:16 [3] common\jobs\SourceCallbackJob (attempt: 1, pid: 145080) - Started
2018-11-23 16:25:16 [3] common\jobs\SourceCallbackJob (attempt: 1, pid: 145080) - Error (0.226 s)
> yii\httpclient\Exception: Curl error: #6 - Could not resolve host: www.source_callback_url.com
2018-11-23 16:25:16 [pid: 145080] - Worker is stopped (0:00:00)
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 3

20. POST http://api.channel-pub-api.localhost/qq/v1/articles/video?group_id=spider , executed successfully. I intentionally made the value of media_absolute_url incorrect to test the subsequent processing after the copy resource file queue job fails to execute; I intentionally made the value of source_callback_url correct to test the subsequent processing after the source callback queue job executes successfully

Request Body

JSON

{
	"channel_app_source_uuids": ["bba4e024eba111e89fa754ee75d2ebc1"],
	"source": "spider",
	"source_uuid": "825e6d5e36468cc4bf536799ce3565cf",
	"source_pub_user_id": 1,
	"source_callback_url": "http://wjdev.chinamcloud.com:8609/cms/api/thirdPush/callBack",
	"article_category_id": 226,
	"title": "综艺节目 - 20181123 - 2",
	"author": "综艺节目 - 20181123 - 2",
	"source_article_id": 1,
	"media_absolute_url": "http://127.0.0.1/channel-pub-api/storage/spider/videos/3d35e17a32fb48cfb76f39a1b1bf33ce_h264_1200k_mp4",
	"tag": "综艺",
	"apply": 0,
	"desc": "综艺节目 - 20181123 - 2"
}

Response Body

JSON

{
    "code": 10000,
    "message": "发布文章类型:视频(视频)的文章成功",
    "data": [
        {
            "channel_id": 1,
            "channel_code": "qq",
            "channel_type_id": 1,
            "channel_type_code": "qq_cw",
            "channel_app_source_id": 6,
            "channel_app_source_uuid": "bba4e024eba111e89fa754ee75d2ebc1",
            "task_id": 20,
            "status": 1,
            "created_at": 1542961817,
            "updated_at": 1542961817,
            "uuid": "01c9b588eefa11e8a7ba54ee75d2ebc1",
            "id": 20
        }
    ]
}

21. Execute the SQL statement as follows:

SQL

SELECT * FROM `cpa_channel` WHERE (`code`='qq') AND (`is_deleted`=0)
SELECT * FROM `cpa_channel_app_source` WHERE (`uuid`='bba4e024eba111e89fa754ee75d2ebc1') AND (`is_deleted`=0)
SELECT * FROM `cpa_channel_type` WHERE (`code`='qq_cw') AND (`is_deleted`=0)
SELECT * FROM `cpa_qq_cw_app` WHERE (`channel_app_source_uuid`='bba4e024eba111e89fa754ee75d2ebc1') AND (`is_deleted`=0)
SELECT * FROM `cpa_article_type` WHERE (`code`='video') AND (`is_deleted`=0)
SELECT * FROM `cpa_qq_article_type` WHERE (`code`='multivideos') AND (`is_deleted`=0)
SELECT EXISTS(SELECT * FROM `cpa_article_category` WHERE (`cpa_article_category`.`id`=226) AND ((`is_deleted`=0) AND (`status`=1)))
SELECT * FROM `cpa_qq_article_category_multivideos` WHERE (`article_category_id`=226) AND (`is_deleted`=0)
INSERT INTO `cpa_task` (`group_id`, `source`, `source_uuid`, `source_pub_user_id`, `source_callback_url`, `channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `status`, `created_at`, `updated_at`) VALUES ('spider', 'spider', '825e6d5e36468cc4bf536799ce3565cf', 1, 'http://wjdev.chinamcloud.com:8609/cms/api/thirdPush/callBack', 1, 'qq', 1, 'qq_cw', 1, 1542961817, 1542961817)
INSERT INTO `cpa_channel_app_task` (`channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `channel_app_source_id`, `channel_app_source_uuid`, `task_id`, `status`, `created_at`, `updated_at`, `uuid`) VALUES (1, 'qq', 1, 'qq_cw', 6, 'bba4e024eba111e89fa754ee75d2ebc1', 20, 1, 1542961817, 1542961817, '01c9b588eefa11e8a7ba54ee75d2ebc1')
INSERT INTO `cpa_qq_cw_app_task` (`channel_app_task_id`, `channel_app_task_uuid`, `qq_cw_app_id`, `task_id`, `status`, `created_at`, `updated_at`) VALUES (20, '01c9b588eefa11e8a7ba54ee75d2ebc1', 5, 20, 1, 1542961817, 1542961817)
INSERT INTO `cpa_qq_article` (`group_id`, `article_category_id`, `title`, `author`, `source_article_id`, `qq_app_type`, `article_type_id`, `qq_article_type_id`, `qq_article_category_id`, `task_id`, `status`, `created_at`, `updated_at`) VALUES ('spider', 226, '综艺节目 - 20181123 - 2', '综艺节目 - 20181123 - 2', 1, 'cw', 3, 2, 2808, 20, 1, 1542961817, 1542961817)
INSERT INTO `cpa_qq_article_multivideos` (`media`, `tag`, `desc`, `apply`, `qq_article_id`, `category`, `md5`, `vid`, `task_id`, `status`, `created_at`, `updated_at`) VALUES ('', '综艺', '综艺节目 - 20181123 - 2', 0, 20, 2808, '', '', 20, 1, 1542961817, 1542961817)
INSERT INTO `cpa_asset` (`channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `source`, `type`, `absolute_url`, `relative_path`, `size`, `task_id`, `channel_article_id`, `status`, `is_deleted`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 'qq', 1, 'qq_cw', 'spider', 'video', 'http://127.0.0.1/channel-pub-api/storage/spider/videos/3d35e17a32fb48cfb76f39a1b1bf33ce_h264_1200k_mp4', '', 0, 20, 20, 1, 0, 1542961817, 1542961817, 0)

22. View the status information of the 4 queues

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 3

23. The run command fetches and executes tasks in a loop (copy resource file queue) until the queue is empty

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/run --verbose=1 --isolate=1 --color=0
2018-11-23 16:36:40 [pid: 146364] - Worker is started
2018-11-23 16:36:40 [2] common\jobs\CopyAssetJob (attempt: 1, pid: 146364) - Started
2018-11-23 16:36:41 [2] common\jobs\CopyAssetJob (attempt: 1, pid: 146364) - Error (0.180 s)
> yii\web\NotFoundHttpException: Source resource file: 0, does not exist
2018-11-23 16:36:41 [pid: 146364] - Worker is stopped (0:00:01)

24. The copy resource file queue job failed to execute, the execution result meets expectations, the publication log has been inserted, and the job has been pushed to the source callback queue, as shown in Figure 4

The copy resource file queue job failed to execute, the execution result meets expectations, the publication log has been inserted, and the job has been pushed to the source callback queue
Figure 4
Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 3

25. The run command fetches and executes tasks in a loop (source callback queue) until the queue is empty

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/run --verbose=1 --isolate=1 --color=0
2018-11-23 16:38:53 [pid: 141652] - Worker is started
2018-11-23 16:38:54 [4] common\jobs\SourceCallbackJob (attempt: 1, pid: 141652) - Started
2018-11-23 16:38:54 [4] common\jobs\SourceCallbackJob (attempt: 1, pid: 141652) - Done (0.623 s)
2018-11-23 16:38:55 [pid: 141652] - Worker is stopped (0:00:02)

26. The source callback queue job executed successfully, the execution result meets expectations, the publication log has been updated, and the publication log is updated based on the channel application’s task ID (the number of callback times decreases by 1; callback status, 1: Success). As shown in Figure 5, Figure 6

The source callback queue job executed successfully
Figure 5
The execution result meets expectations, the publication log has been updated, and the publication log is updated based on the channel application's task ID (the number of callback times decreases by 1; callback status, 1: Success)
Figure 6
Plaintext

PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 4

27. Implementation of the resource service (multipart upload of video files for the Penguin content website application; after the queue task executes successfully, call the corresponding service; otherwise, insert a publication log). Edit \common\services\QqCwAssetService.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/10/24
 * Time: 14:01
 */
 
namespace common\services;
 
use Yii;
use common\logics\ChannelType;
use common\logics\ChannelAppTask;
use common\logics\QqCwAppTask;
use common\logics\WxTaskQqCwTaskRelation;
use common\logics\Asset;
use common\logics\PubLog;
use common\jobs\UploadAssetJob;
use yii\helpers\Json;
use yii\web\NotFoundHttpException;
use yii\web\ServerErrorHttpException;
use yii\web\UnprocessableEntityHttpException;
 
/**
 * 企鹅号的内容网站应用的资源服务
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class QqCwAssetService extends Service
{
    /**
     * 复制资源文件队列的作业执行成功后的后续处理
     *
     * @param int $taskId 任务ID
     * 格式如下:1
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public static function copyAssetExecHandler($taskId)
    {
        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($taskId);

        // 基于条件查找状态为启用的资源列表
        $assetEnabledWhere = [
            'channel_id' => $taskEnabledItem->channel_id,
            'channel_type_id' => $taskEnabledItem->channel_type_id,
            'type' => Asset::TYPE_VIDEO,
            'task_id' => $taskEnabledItem->id,
        ];
        $assetEnabledItems = Asset::findAllEnabledByWhere($assetEnabledWhere);

        if (empty($assetEnabledItems)) {

        } else {
            // 企鹅号的内容网站应用的视频文件分片上传,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步)
            $assets = [];
            foreach ($assetEnabledItems as $assetEnabledItem) {
                $assets[] = [
                    'job_type' => UploadAssetJob::JOB_TYPE_VIDEO_MULTIPART,
                    'id' => $assetEnabledItem->id,
                ];
            }

            static::uploadAssetVideoMultipartAsync($taskId, $assets);
        }
    }

    /**
     * 复制资源文件队列的作业执行失败后的后续处理
     *
     * @param int $taskId 任务ID
     * 格式如下:1
     * @param object $eventError 事件错误
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public static function copyAssetErrorHandler($taskId, $eventError)
    {
        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($taskId);

        // 基于任务ID查找状态为启用的资源列表
        $channelAppTaskEnabledItems = ChannelAppTask::findAllEnabledByTaskId($taskId);

        if (empty($channelAppTaskEnabledItems)) {
            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35021'), ['task_id' => $taskId])), 35021);
        }

        // 基于任务ID批量更新企鹅号的内容网站应用的任务(场景:复制资源文件队列的作业执行失败后,可发布次数减1,状态,3:发布中(已失败))
        QqCwAppTask::updateMultiplePublishErrorByTaskId($taskId);

        $pubLogDatas = [];
        foreach ($channelAppTaskEnabledItems as $key => $channelAppTaskEnabledItem) {
            // 基于渠道的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的任务)
            $qqCwAppTaskItem = QqCwAppTaskService::findModelByChannelAppTaskId($channelAppTaskEnabledItem->id);
            $pubLogDatas[$key] = [
                'channel_app_source_uuid' => $channelAppTaskEnabledItem->channel_app_source_uuid,
                'channel_app_task_uuid' => $channelAppTaskEnabledItem->uuid,
                'channel_code' => $channelAppTaskEnabledItem->channel_code,
                'channel_type_code' => $channelAppTaskEnabledItem->channel_type_code,
                'channel_app_task_status' => $qqCwAppTaskItem->status,
            ];
        }

        // 发布任务失败后,插入发布日志,将作业推送至来源回调队列(异步)
        SourceCallbackService::sourceCallbackAsync($channelAppTaskEnabledItems, $eventError->getCode(), $eventError->getMessage(), $pubLogDatas, PubLog::STATUS_ERROR);

        // 基于企鹅号的内容网站应用的任务ID查找状态为启用的单个资源(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联)
        $wxTaskQqCwTaskRelationItem = WxTaskQqCwTaskRelation::findOneEnabledByQqCwTaskId($taskEnabledItem->id);

        if (isset($wxTaskQqCwTaskRelationItem)) {
            WxAssetService::copyAssetErrorHandler($wxTaskQqCwTaskRelationItem->wx_task_id, $eventError);
        }

    }

    /**
     * 上传资源文件队列的作业执行成功后的后续处理
     *
     * @param int $assetId 资源ID
     * 格式如下:1
     *
     * @param int $channelAppTaskId 渠道的应用的任务ID
     * 格式如下:4
     *
     */
    public static function uploadAssetVideoMultipartExecHandler($assetId, $channelAppTaskId)
    {
    }

    /**
     * 上传资源文件队列的作业执行失败后的后续处理
     *
     * @param int $assetId 资源ID
     * 格式如下:1
     *
     * @param int $channelAppTaskId 渠道的应用的任务ID
     * 格式如下:4
     *
     * @param object $eventError 事件错误
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws ServerErrorHttpException
     * @throws \Throwable
     */
    public static function uploadAssetVideoMultipartErrorHandler($assetId, $channelAppTaskId, $eventError)
    {
        // 基于ID查找状态为启用的单个数据模型(资源)
        $assetEnabledItem = AssetService::findModelEnabledById($assetId);

        if ($assetEnabledItem->type != Asset::TYPE_VIDEO) {
            throw new ServerErrorHttpException(Yii::t('common/error', '35040'), 35040);
        }

        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($channelAppTaskId);

        // 基于渠道的应用的任务ID更新企鹅号的内容网站应用的任务(场景:上传资源文件队列的作业执行失败后,可发布次数减1,状态,3:发布中(已失败))
        QqCwAppTask::updatePublishErrorByChannelAppTaskId($channelAppTaskEnabledItem->id);

        $channelAppTaskEnabledItems[] = $channelAppTaskEnabledItem;

        $pubLogDatas = [];
        foreach ($channelAppTaskEnabledItems as $key => $channelAppTaskEnabledItem) {
            // 基于渠道的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的任务)
            $qqCwAppTaskItem = QqCwAppTaskService::findModelByChannelAppTaskId($channelAppTaskEnabledItem->id);
            $pubLogDatas[$key] = [
                'channel_app_source_uuid' => $channelAppTaskEnabledItem->channel_app_source_uuid,
                'channel_app_task_uuid' => $channelAppTaskEnabledItem->uuid,
                'channel_code' => $channelAppTaskEnabledItem->channel_code,
                'channel_type_code' => $channelAppTaskEnabledItem->channel_type_code,
                'channel_app_task_status' => $qqCwAppTaskItem->status,
            ];
        }

        // 发布任务失败后,插入发布日志,将作业推送至来源回调队列(异步)
        SourceCallbackService::sourceCallbackAsync($channelAppTaskEnabledItems, $eventError->getCode(), $eventError->getMessage(), $pubLogDatas, PubLog::STATUS_ERROR);

        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($assetEnabledItem->task_id);

        // 基于企鹅号的内容网站应用的任务ID查找状态为启用的单个资源(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联)
        $wxTaskQqCwTaskRelationItem = WxTaskQqCwTaskRelation::findOneEnabledByQqCwTaskId($taskEnabledItem->id);

        if (isset($wxTaskQqCwTaskRelationItem)) {
            WxAssetService::qqCwUploadAssetVideoMultipartErrorHandler($wxTaskQqCwTaskRelationItem->wx_task_id, $eventError);
        }

        // 判断任务的渠道的类型代码
        if ($taskEnabledItem->channel_type_code != ChannelType::CODE_QQ_CW) {
            throw new ServerErrorHttpException(Yii::t('common/error', '35032'), 35032);
        }

        // 基于渠道的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的任务)
        $qqCwAppTaskItem = QqCwAppTaskService::findModelByChannelAppTaskId($channelAppTaskId);

        // 企鹅号的内容网站应用的视频文件分片上传失败后的后续处理
        $qqCwVideoMultipartUploadService = new QqCwVideoMultipartUploadService();
        $result = $qqCwVideoMultipartUploadService->uploadErrorHandler($assetId, $qqCwAppTaskItem->id);

    }

    /**
     * 企鹅号的内容网站应用的视频文件分片上传,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步)
     *
     * @param int $taskId 任务ID
     * 格式如下:1
     *
     * @param array $assets 多个资源ID
     * 格式如下:
     * [
     *     [
     *         'job_type' => 'video_multipart', // 作业类型,video_multipart:视频文件分片
     *         'id' => 2, // ID
     *     ],
     *     [
     *         'job_type' => 'video_multipart', // 作业类型,video_multipart:视频文件分片
     *         'id' => 4, // ID
     *     ],
     * ]
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     */
    public static function uploadAssetVideoMultipartAsync($taskId, $assets)
    {
        // 基于任务ID查找状态为启用的资源列表
        $channelAppTaskEnabledItems = ChannelAppTask::findAllEnabledByTaskId($taskId);

        foreach ($channelAppTaskEnabledItems as $channelAppTaskEnabledItem) {

            // 基于渠道的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的任务)
            $qqCwAppTaskItem = QqCwAppTaskService::findModelByChannelAppTaskId($channelAppTaskEnabledItem->id);
            // 更新企鹅号的内容网站应用的任务状态,2:发布中
            $qqCwAppTaskItem->status = QqCwAppTask::STATUS_PUBLISH;
            $qqCwAppTaskItem->save();

            // 将任务发送到队列,通过标准工作人员进行处理
            Yii::$app->uploadAssetQueue->push(new UploadAssetJob([
                'channelAppTaskId' => $channelAppTaskEnabledItem->id,
                'assets' => $assets,
            ]));
        }
    }

    /**
     * 企鹅号的内容网站应用的视频文件分片上传(同步)
     *
     * @param int $assetId 资源ID
     * 格式如下:1
     *
     * @param int $channelAppTaskId 渠道的应用的任务ID
     * 格式如下:4
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws ServerErrorHttpException
     * @throws \Throwable
     */
    public static function uploadAssetVideoMultipartSync($assetId, $channelAppTaskId)
    {
        // 基于ID查找状态为启用的单个数据模型(资源)
        $assetEnabledItem = AssetService::findModelEnabledById($assetId);

        if ($assetEnabledItem->type != Asset::TYPE_VIDEO) {
            throw new ServerErrorHttpException(Yii::t('common/error', '35040'), 35040);
        }

        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($channelAppTaskId);

        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($assetEnabledItem->task_id);

        // 判断任务的渠道的类型代码
        if ($taskEnabledItem->channel_type_code != ChannelType::CODE_QQ_CW) {
            throw new ServerErrorHttpException(Yii::t('common/error', '35032'), 35032);
        }

        // 基于渠道的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的任务)
        $qqCwAppTaskItem = QqCwAppTaskService::findModelByChannelAppTaskId($channelAppTaskId);

        // 基于ID查找状态为启用的单个数据模型(企鹅号的内容网站应用)
        $qqCwAppEnabledItem = QqCwAppService::findModelEnabledById($qqCwAppTaskItem->qq_cw_app_id);

        // 企鹅号的内容网站应用的视频文件分片上传
        $qqCwVideoMultipartUploadService = new QqCwVideoMultipartUploadService();
        $result = $qqCwVideoMultipartUploadService->upload($assetId, $qqCwAppTaskItem->id);
    }
}

28. Implementation of the upload resource file queue job, edit \common\jobs\UploadAssetJob.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/10/24
 * Time: 17:17
 */
 
namespace common\jobs;
 
use Yii;
use common\logics\ChannelType;
use common\logics\Asset;
use common\services\TaskService;
use common\services\ChannelAppTaskService;
use yii\helpers\ArrayHelper;
use yii\web\ServerErrorHttpException;
use yii\web\UnprocessableEntityHttpException;
 
/**
 * 上传资源文件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class UploadAssetJob extends Job
{
    const JOB_TYPE_VIDEO_MULTIPART = 'video_multipart'; //作业类型:视频文件分片

    public $channelAppTaskId;
    public $assets;

    /*
     * @throws UnprocessableEntityHttpException
     * @throws ServerErrorHttpException 如果基于任务ID查找状态为启用的资源列表为空,将抛出 500 HTTP 异常
     */
    public function execute($queue)
    {
        // 查找状态为启用的数据模型(渠道的类型)
        $channelTypeEnabledItems = ChannelType::find()->isDeletedNo()->enabled()->all();

        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($this->channelAppTaskId);

        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($channelAppTaskEnabledItem->task_id);

        // 基于任务ID查找状态为启用的资源列表
        $assetEnabledItems = Asset::findAllEnabledByTaskId($taskEnabledItem->id);

        if (empty($assetEnabledItems)) {
            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35020'), ['task_id' => $taskEnabledItem->id])), 35020);
        }

        // 获取基于任务ID查找状态为启用的资源列表的id值列表
        $assetEnabledIds = ArrayHelper::getColumn($assetEnabledItems, 'id');
        // 不属于当前任务ID下的资源ID
        $notBelongIds = [];
        // 作业类型不支持的资源ID
        $jobTypeNotSupportIds = [];
        // 作业类型与渠道的类型代码不匹配的资源ID
        $notMatchChannelTypeCodeIds = [];

        $assets = $this->assets;
        foreach ($this->assets as $assetKey => $asset) {

            $assetJobType = $taskEnabledItem->channel_type_code . '_' . $asset['job_type'];

            // 不属于当前任务ID下的资源ID
            if (!in_array($asset['id'], $assetEnabledIds)) {
                $notBelongIds[] = $asset['id'];
            }

            // 作业类型不支持的资源ID
            if (!in_array($asset['job_type'], [self::JOB_TYPE_VIDEO_MULTIPART])) {
                $jobTypeNotSupportIds[] = $assetJobType;
            }

            // 作业类型与渠道的类型代码不匹配的资源ID
            $matchChannelTypeCode = false;
            foreach ($channelTypeEnabledItems as $itemKey => $channelTypeEnabledItem) {
                $pos = strpos($assetJobType, $channelTypeEnabledItem->code);
                if ($pos !== false) {
                    $assets[$assetKey]['service_class'] = 'common\services\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $channelTypeEnabledItem->code))) . 'AssetService'; // 例:common\services\QqCwAssetService
                    $assets[$assetKey]['service_action'] = 'uploadAsset' . str_replace(' ', '', ucwords(str_replace('_', ' ', $asset['job_type']))) . 'Sync'; // 例:uploadAssetVideoMultipartSync
                    $matchChannelTypeCode = true;
                    break;
                }
            }
            if (!$matchChannelTypeCode) {
                $notMatchChannelTypeCodeIds[] = $assetJobType;
            }
        }

        if (!empty($notBelongIds)) {
            $notBelongIds = implode(",", $notBelongIds);
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35025'), ['not_belong_ids' => $notBelongIds, 'task_id' => $taskEnabledItem->id])), 35025);
        }

        if (!empty($jobTypeNotSupportIds)) {
            $jobTypeNotSupportIds = implode(",", array_unique($jobTypeNotSupportIds));
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35026'), ['job_type_not_support_ids' => $jobTypeNotSupportIds])), 35026);
        }

        if (!empty($notMatchChannelTypeCodeIds)) {
            $notMatchChannelTypeCodeIds = implode(",", array_unique($notMatchChannelTypeCodeIds));
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35027'), ['not_match_channel_type_code_ids' => $notMatchChannelTypeCodeIds])), 35027);
        }

        // 基于作业类型调用相应上传服务
        foreach ($assets as $asset) {
            $serviceAction = $asset['service_action'];
            $asset['service_class']::$serviceAction($asset['id'], $this->channelAppTaskId);
        }

    }
}

29. Specific implementation of multipart upload of video files for the Penguin content website application, edit \common\services\QqCwVideoMultipartUploadService.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/10/29
 * Time: 14:45
 */
 
namespace common\services;
 
use Yii;
use common\logics\QqArticle;
use common\logics\QqTransaction;
use common\logics\QqVideoMultipartUpload;
use common\logics\http\qq_api\Video as HttpQqApiVideo;
use yii\web\ServerErrorHttpException;
 
/**
 * Class QqCwVideoMultipartUploadService
 * @package qq\services
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class QqCwVideoMultipartUploadService extends Service
{
    /**
     * HTTP请求,申请企鹅号的内容网站应用的视频文件分片上传的唯一事务ID
     *
     * @param array $data 数据
     * 格式如下:
     *
     * [
     *     'accessToken' => 'KCK0TIV8NDY44ONAEFI2QW', // 企鹅平台企鹅号应用授权调用凭据
     *     'size' => 9135849, // 视频文件大小,单位(字节)
     *     'md5' => 'd081c619ef7dc62eb2aa29c7c83c6f26', // 视频文件MD5值
     *     'sha' => '28a1076b867bed8202df5b3f656b798148e58ced', // 视频文件SHA-1值
     * ]
     *
     * @return array
     * 格式如下:
     *
     * [
     *     'transaction_id' => '780930255958621794', // 上传的唯一事务ID
     * ]
     *
     * @throws ServerErrorHttpException
     */
    public function httpUploadReady($data)
    {
        /* HTTP请求,申请企鹅号的内容网站应用的视频文件分片上传的唯一事务ID */
        $httpQqApiVideo = new HttpQqApiVideo();
        $uploadReady = $httpQqApiVideo->clientUploadReady($data);

        if ($uploadReady === false) {
            if ($httpQqApiVideo->hasErrors()) {
                foreach ($httpQqApiVideo->getFirstErrors() as $message) {
                    $firstErrors = $message;
                    break;
                }
                throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35042'), ['firstErrors' => $firstErrors])), 35042);
            } elseif (!$httpQqApiVideo->hasErrors()) {
                throw new ServerErrorHttpException('Penguin\'s content website application api HTTP requests fail for unknown reasons.');
            }
        }

        return $uploadReady['data'];
    }

    /**
     * HTTP请求,企鹅号的内容网站应用的视频文件分片上传
     * @param array $data 数据
     * 格式如下:
     *
     * [
     *     'accessToken' => 'LQVHLUQDNEKIDBFARJ1OOA', // 企鹅平台企鹅号应用授权调用凭据
     *     'transactionId' => '780930287703152921', // 上传的唯一事务ID
     *     'mediatrunk' => 'E:/wwwroot/channel-pub-api/storage/channel-pub-api/videos/2018/10/27/1540632234.9842.66697370.mp4', // 视频 mediatrunk 文件
     *     'startOffset' => 0, // 分片的起始位置(从0开始计数)
     * ]
     *
     * @return array
     * 格式如下:
     *
     * [
     *     'end_offset' => 2198151, // 分片的结束位置
     *     'start_offset' => 2198151, // 分片的起始位置
     *     'transaction_id' => 780930255958621794, // 上传的唯一事务ID
     * ]
     *
     * @throws ServerErrorHttpException
     */
    public function httpUploadTrunk($data)
    {
        /* HTTP请求,企鹅号的内容网站应用的视频文件分片上传 */
        $httpQqApiVideo = new HttpQqApiVideo();
        $uploadTrunk = $httpQqApiVideo->clientUploadTrunk($data);

        if ($uploadTrunk === false) {
            if ($httpQqApiVideo->hasErrors()) {
                foreach ($httpQqApiVideo->getFirstErrors() as $message) {
                    $firstErrors = $message;
                    break;
                }
                throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35042'), ['firstErrors' => $firstErrors])), 35042);
            } elseif (!$httpQqApiVideo->hasErrors()) {
                throw new ServerErrorHttpException('Penguin\'s content website application api HTTP requests fail for unknown reasons.');
            }
        }

        return $uploadTrunk['data'];
    }

    /**
     * 企鹅号的内容网站应用的视频文件分片上传
     *
     * @param int $assetId 资源ID
     * 格式如下:1
     *
     * @param int $qqCwAppTaskId 企鹅号的内容网站应用的任务ID
     * 格式如下:6
     *
     * @throws ServerErrorHttpException
     * @throws \Throwable
     */
    public function upload($assetId, $qqCwAppTaskId)
    {
        // 基于ID查找状态为启用的单个数据模型(资源)
        $assetEnabledItem = AssetService::findModelEnabledById($assetId);

        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($assetEnabledItem->task_id);

        // 基于ID查找状态为发布中的单个数据模型(企鹅号的内容网站应用的任务)
        $qqCwAppTaskPublishItem = QqCwAppTaskService::findModelPublishById($qqCwAppTaskId);

        // 基于ID查找状态为启用的单个数据模型(企鹅号的内容网站应用)
        $qqCwAppEnabledItem = QqCwAppService::findModelEnabledById($qqCwAppTaskPublishItem->qq_cw_app_id);

        // 基于企鹅号的内容网站应用ID获取有效的 Access Token
        $qqCwAccessTokenService = new QqCwAccessTokenService();
        $accessTokenValidity = $qqCwAccessTokenService->getAccessTokenValidityByQqCwAppId($qqCwAppEnabledItem->id);

        // 基于资源ID、企鹅号的应用的任务ID获取企鹅号的视频文件分片上传的单个数据模型
        $qqVideoMultipartUploadItem = $this->getModelByAssetIdAndQqAppTaskId($assetId, $qqCwAppTaskPublishItem->id);

        $data = [
            'assetId' => $qqVideoMultipartUploadItem->asset_id,
            'qqAppTaskId' => $qqVideoMultipartUploadItem->qq_app_task_id,
            'qqAppId' => $qqVideoMultipartUploadItem->qq_app_id,
            'qqAppType' => $qqVideoMultipartUploadItem->qq_app_type,
            'size' => $qqVideoMultipartUploadItem->size,
            'md5' => $qqVideoMultipartUploadItem->md5,
            'sha' => $qqVideoMultipartUploadItem->sha,
            'transactionId' => (string) $qqVideoMultipartUploadItem->transaction_id,
            'mediatrunk' => $qqVideoMultipartUploadItem->mediatrunk,
            'startOffset' => $qqVideoMultipartUploadItem->start_offset,
            'endOffset' => $qqVideoMultipartUploadItem->end_offset,
            'vid' => $qqVideoMultipartUploadItem->vid,
            'status' => $qqVideoMultipartUploadItem->status,
        ];

        // 文件切片
        AssetService::cut(Yii::$app->params['channelPubApi']['asset'][$assetEnabledItem->type]['basePath'] . $qqVideoMultipartUploadItem->mediatrunk, HttpQqApiVideo::CUT_SIZE);
        // 获取需要切片的文件的路径信息
        $pathInfo = pathinfo(Yii::$app->params['channelPubApi']['asset'][$assetEnabledItem->type]['basePath'] . $qqVideoMultipartUploadItem->mediatrunk);

        // 判断分片的起始位置与分片的结束位置是否等于视频文件大小,如果相等则中断分片上传,否则继续执行分片上传
        $i = 0;
        while ($qqVideoMultipartUploadItem->start_offset != $qqVideoMultipartUploadItem->size && $qqVideoMultipartUploadItem->end_offset != $qqVideoMultipartUploadItem->size) {
            // file_put_contents('E:/wwwroot/channel-pub-api/qq/runtime/while_' . $assetId . '_' . $qqCwAppId . '_' . $qqVideoMultipartUploadItem->start_offset . '_' . $qqVideoMultipartUploadItem->end_offset . '_' . time() . '.txt', $assetId);
            // HTTP请求,企鹅号的内容网站应用的视频文件分片上传
            $httpUploadTrunkData = [
                'accessToken' => $accessTokenValidity->access_token,
                'transactionId' => $qqVideoMultipartUploadItem->transaction_id,
                'mediatrunk' => $pathInfo['dirname'] . '/' . $pathInfo['filename'] . '_' . $i . '.' . $pathInfo['extension'],
                'startOffset' => $qqVideoMultipartUploadItem->start_offset,
            ];
            $uploadTrunkData = $this->httpUploadTrunk($httpUploadTrunkData);

            // 基于资源ID、企鹅号的内容网站应用ID插入/更新数据至企鹅号的内容网站应用的视频文件分片上传
            $data['startOffset'] = $uploadTrunkData['start_offset'];
            $data['endOffset'] = $uploadTrunkData['end_offset'];
            if ($uploadTrunkData['start_offset'] != $qqVideoMultipartUploadItem->size && $uploadTrunkData['end_offset'] != $qqVideoMultipartUploadItem->size) {
                $data['status'] = QqVideoMultipartUpload::STATUS_UPLOADING;
            } else {

                // HTTP请求,基于上传的唯一事务ID获取事务信息
                $qqTransactionService = new QqTransactionService();
                $qqTransactionServiceHttpTransactionInfoData = [
                    'accessToken' => $accessTokenValidity->access_token,
                    'transactionId' => $qqVideoMultipartUploadItem->transaction_id,
                ];
                $qqTransactionServiceHttpTransactionInfoResult = $qqTransactionService->httpTransactionInfo($qqTransactionServiceHttpTransactionInfoData);

                // 创建企鹅号的事务
                $qqTransaction = new QqTransaction();
                $qqTransaction->attributes = [
                    'group_id' => $taskEnabledItem->group_id,
                    'qq_app_task_id' => $qqVideoMultipartUploadItem->qq_app_task_id,
                    'qq_app_id' => $qqVideoMultipartUploadItem->qq_app_id,
                    'qq_app_type' => $qqVideoMultipartUploadItem->qq_app_type,
                    'qq_article_id' => 0,
                    'qq_video_multipart_upload_id' => $qqVideoMultipartUploadItem->id,
                    'transaction_id' => $qqVideoMultipartUploadItem->transaction_id,
                    'type' => $qqTransactionService::getTypeByHttpQqApiTransactionType($qqTransactionServiceHttpTransactionInfoResult['transaction_type']),
                    'transaction_ctime' => $qqTransactionServiceHttpTransactionInfoResult['transaction_ctime'],
                    'ext_err' => $qqTransactionServiceHttpTransactionInfoResult['ext_err'],
                    'transaction_err_msg' => $qqTransactionServiceHttpTransactionInfoResult['transaction_err_msg'],
                    'article_abstract' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_abstract'],
                    'article_type' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_type'],
                    'article_type_code' => '',
                    'article_url' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_url'],
                    'article_imgurl' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_imgurl'],
                    'article_title' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_title'],
                    'article_pub_flag' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_pub_flag'],
                    'article_pub_time' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_pub_time'],
                    'article_video_title' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['title'],
                    'article_video_desc' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['desc'],
                    'article_video_type' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['type'],
                    'article_video_vid' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['vid'],
                    'task_id' => $assetEnabledItem->task_id,
                    'status' => QqTransaction::STATUS_PROCESSING,
                ];
                $qqTransactionServiceCreateResult = $qqTransactionService->create($qqTransaction);

                if ($qqTransactionServiceCreateResult['status'] === false) {
                    throw new ServerErrorHttpException($qqTransactionServiceCreateResult['message'], $qqTransactionServiceCreateResult['code']);
                }

                $data['status'] = QqVideoMultipartUpload::STATUS_UPLOADED;
            }
            $qqVideoMultipartUploadService = new QqVideoMultipartUploadService();
            $result = $qqVideoMultipartUploadService->saveModelByData($data);
            if ($result['status'] === false) {
                throw new ServerErrorHttpException($result['message'], $result['code']);
            }

            // 基于资源ID、企鹅号的应用的任务ID获取企鹅号的视频文件分片上传的单个数据模型
            $qqVideoMultipartUploadItem = $this->getModelByAssetIdAndQqAppTaskId($assetId, $qqCwAppTaskPublishItem->id);
            $i++;
        }

    }

    /**
     * 企鹅号的内容网站应用的视频文件分片上传失败后的后续处理
     *
     * @param int $assetId 资源ID
     * 格式如下:1
     *
     * @param int $qqCwAppTaskId 企鹅号的应用的任务ID
     * 格式如下:6
     *
     * @throws ServerErrorHttpException
     * @throws \Throwable
     */
    public function uploadErrorHandler($assetId, $qqCwAppTaskId)
    {

        // 基于资源ID、企鹅号的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的视频文件分片上传)
        $qqVideoMultipartUploadItem = QqVideoMultipartUpload::find()->where(['asset_id' => $assetId, 'qq_app_task_id' => $qqCwAppTaskId, 'qq_app_type' => QqArticle::QQ_APP_TYPE_CW])->isDeletedNo()->one();

        if (isset($qqVideoMultipartUploadItem)) {
            $data = [
                'assetId' => $qqVideoMultipartUploadItem->asset_id,
                'qqAppTaskId' => $qqVideoMultipartUploadItem->qq_app_task_id,
                'qqAppId' => $qqVideoMultipartUploadItem->qq_app_id,
                'qqAppType' => $qqVideoMultipartUploadItem->qq_app_type,
                'size' => $qqVideoMultipartUploadItem->size,
                'md5' => $qqVideoMultipartUploadItem->md5,
                'sha' => $qqVideoMultipartUploadItem->sha,
                'transactionId' => (string) $qqVideoMultipartUploadItem->transaction_id,
                'mediatrunk' => $qqVideoMultipartUploadItem->mediatrunk,
                'startOffset' => $qqVideoMultipartUploadItem->start_offset,
                'endOffset' => $qqVideoMultipartUploadItem->end_offset,
                'vid' => $qqVideoMultipartUploadItem->vid,
                'status' => $qqVideoMultipartUploadItem->status,
            ];

            // 判断分片的起始位置与分片的结束位置是否等于视频文件大小,如果不相等则修改为,3:上传中(已失败)
            if ($qqVideoMultipartUploadItem->start_offset != $qqVideoMultipartUploadItem->size || $qqVideoMultipartUploadItem->end_offset != $qqVideoMultipartUploadItem->size) {
                $data['status'] = QqVideoMultipartUpload::STATUS_UPLOADING_ERROR;
                $qqVideoMultipartUploadService = new QqVideoMultipartUploadService();
                $result = $qqVideoMultipartUploadService->saveModelByData($data);
                if ($result['status'] === false) {
                    throw new ServerErrorHttpException($result['message'], $result['code']);
                }
            }
        }

    }

    /**
     * 基于资源ID、企鹅号的应用的任务ID获取企鹅号的视频文件分片上传的单个数据模型
     *
     * @param int $assetId 资源ID
     * 格式如下:1
     *
     * @param int $qqAppTaskId 企鹅号的应用的任务ID
     * 格式如下:6
     *
     * @return object
     * 格式如下:
     *
     * [
     *     'id' => 1, // ID
     *     'qq_app_task_id' => 2, // 企鹅号的应用的任务ID
     *     'qq_app_id' => 6, // 企鹅号的应用ID
     *     'qq_app_type' => 'cw', // 企鹅号的应用类型,cw:内容网站应用;tp:第三方服务平台应用
     *     'channel_app_source_id' => 8, // 渠道的应用的来源ID
     *     'channel_app_source_uuid' => '29e3c876d82811e8a95954ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *     'grant_type' => 'clientcredentials', // 授权类型,clientcredentials:企鹅号应用授权模式
     *     'access_token' => 'LR7WL2MAMTI8DFMIPSGEZG', // 企鹅平台企鹅号应用授权调用凭据
     *     'expires_in' => 7200, // 授权方接口调用凭据有效期,单位(秒)
     *     'expires_at' => 1540797813, // 授权方接口调用凭据有效截止时间
     *     'openid' => '9476dfbaaf799033718b4016f01f9590', // 企鹅平台企鹅号应用对应的企鹅媒体用户唯一标识
     *     'status' => 1, // 状态,0:禁用;1:启用
     *     'is_deleted' => 0, // 是否被删除,0:否;1:是
     *     'created_at' => 1540790913, // 创建时间
     *     'updated_at' => 1540790913, // 更新时间
     *     'deleted_at' => 0, // 删除时间
     * ]
     *
     * @throws ServerErrorHttpException
     * @throws \Throwable
     */
    public function getModelByAssetIdAndQqAppTaskId($assetId, $qqAppTaskId)
    {
        // 基于资源ID、企鹅号的应用的任务ID、企鹅号的应用类型,cw:内容网站应用查找单个数据模型(企鹅号的内容网站应用的视频文件分片上传)
        $qqVideoMultipartUploadItem = QqVideoMultipartUpload::find()->where(['asset_id' => $assetId, 'qq_app_task_id' => $qqAppTaskId, 'qq_app_type' => QqArticle::QQ_APP_TYPE_CW])->isDeletedNo()->one();

        // 返回模型
        if (isset($qqVideoMultipartUploadItem)) {
            return $qqVideoMultipartUploadItem;
        }

        // 基于ID查找状态为发布中的单个数据模型(企鹅号的内容网站应用的任务)
        $qqCwAppTaskPublishItem = QqCwAppTaskService::findModelPublishById($qqAppTaskId);

        // 基于ID查找状态为启用的单个数据模型(企鹅号的内容网站应用)
        $qqCwAppEnabledItem = QqCwAppService::findModelEnabledById($qqCwAppTaskPublishItem->qq_cw_app_id);

        // 基于ID查找状态为启用的单个数据模型(资源)
        $assetEnabledItem = AssetService::findModelEnabledById($assetId);

        // 基于企鹅号的内容网站应用ID获取有效的 Access Token
        $qqCwAccessTokenService = new QqCwAccessTokenService();
        $accessTokenValidity = $qqCwAccessTokenService->getAccessTokenValidityByQqCwAppId($qqCwAppEnabledItem->id);

        // 渠道发布的资源文件的绝对路径
        $assetAbsolutePath = Yii::$app->params['channelPubApi']['asset'][$assetEnabledItem->type]['basePath'] . $assetEnabledItem->relative_path;
        $assetMd5 = md5_file($assetAbsolutePath);
        $assetSha = sha1_file($assetAbsolutePath);

        // HTTP请求,申请企鹅号的内容网站应用的视频文件分片上传的唯一事务ID
        $httpUploadReadyData = [
            'accessToken' => $accessTokenValidity->access_token,
            'size' => $assetEnabledItem->size,
            'md5' => $assetMd5,
            'sha' => $assetSha,
        ];
        $uploadReadyData = $this->httpUploadReady($httpUploadReadyData);

        // 基于资源ID、企鹅号的内容网站应用ID插入/更新数据至企鹅号的内容网站应用的视频文件分片上传
        $data = [
            'assetId' => $assetId,
            'qqAppTaskId' => $qqAppTaskId,
            'qqAppId' => $qqCwAppEnabledItem->id,
            'qqAppType' => QqArticle::QQ_APP_TYPE_CW,
            'size' => $assetEnabledItem->size,
            'md5' => $assetMd5,
            'sha' => $assetSha,
            'transactionId' => (string) $uploadReadyData['transaction_id'],
            'mediatrunk' => $assetEnabledItem->relative_path,
            'startOffset' => 0,
            'endOffset' => 0,
            'vid' => '',
            'status' => QqVideoMultipartUpload::STATUS_WAIT_UPLOAD,
        ];
        $qqVideoMultipartUploadService = new QqVideoMultipartUploadService();
        $result = $qqVideoMultipartUploadService->saveModelByData($data);
        if ($result['status'] === false) {
            throw new ServerErrorHttpException($result['message'], $result['code']);
        }

        return $result['data'];

    }
}

30. Implementation of the Penguin authorized Access Token, edit \common\logics\http\qq_auth\AccessToken.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/08/28
 * Time: 15:33
 */
 
namespace common\logics\http\qq_auth;
 
use Yii;
use yii\web\ServerErrorHttpException;
 
/**
 * 企鹅号授权的 Access Token
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class AccessToken extends Model
{
    /**
     * HTTP请求,企鹅号的内容网站应用通过 Client ID 和 Client Secret 获取 Access Token
     * @param array $data 数据
     * 格式如下:
     *
     * [
     *     'grantType' => 'clientcredentials', // 授权类型,clientcredentials:企鹅号应用授权模式
     *     'clientId' => '41e05276490ed0936a7c947cf82cf285', // Client ID
     *     'clientSecret' => '3a9949ddccf861c3993bad2e21adbae0e863d618', // Client Secret
     * ]
     *
     * @return array|false
     *
     * 格式如下:
     *
     * 企鹅号的内容网站应用授权的 Access Token
     * [
     *     'message' => '', // 说明
     *     'data' => [ // 数据
     *         'access_token' => 'F2RM000PNSU9Q38L8NC_QQ', // 企鹅平台企鹅号应用授权调用凭据
     *         'expires_in' => 7200, // 授权方接口调用凭据有效期,单位(秒)
     *         'openid' => '9476dfbaaf799033718b4016f01f9590', // 企鹅平台企鹅号应用对应的企鹅媒体用户唯一标识
     *     ],
     * ]
     *
     * 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
     * false
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     */
    public function clientAccessToken($data)
    {
        $response = Yii::$app->qqAuthHttp->createRequest()
            ->setMethod('post')
            ->setUrl('accesstoken')
            ->setData([
                'grant_type' => $data['grantType'],
                'client_id' => $data['clientId'],
                'client_secret' => $data['clientSecret'],
            ])
            ->send();
        // 检查响应状态码是否等于20x
        if ($response->isOk) {
            // 检查业务逻辑是否成功
            if ($response->data['code'] === '0') {
                $responseData = ['message' => '', 'data' => $response->data['data']];
                return $responseData;
            } else {
                $this->addError('id', $response->data['msg']);
                return false;
            }
        } else {
            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35038'), ['statusCode' => $response->getStatusCode()])), 35038);
        }
    }
}

31. Implementation of the video interface for the Penguin API, edit \common\logics\http\qq_api\Video.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/10/26
 * Time: 15:33
 */
 
namespace common\logics\http\qq_api;
 
use Yii;
use yii\httpclient\Client;
use yii\web\ServerErrorHttpException;
 
/**
 * 企鹅号接口的视频
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class Video extends Model
{
    const CUT_SIZE = 104857600; //视频分片上传的切片大小:100M

    /**
     * HTTP请求,申请企鹅号的内容网站应用的视频文件分片上传的唯一事务ID
     *
     * @param array $data 数据
     * 格式如下:
     * [
     *     'accessToken' => 'KCK0TIV8NDY44ONAEFI2QW', // 企鹅平台企鹅号应用授权调用凭据
     *     'size' => 9135849, // 视频文件大小,单位(字节)
     *     'md5' => 'd081c619ef7dc62eb2aa29c7c83c6f26', // 视频文件MD5值
     *     'sha' => '28a1076b867bed8202df5b3f656b798148e58ced', // 视频文件SHA-1值
     * ]
     *
     * @return array|false
     * 格式如下:
     * 企鹅号的内容网站应用的视频文件分片上传的唯一事务ID
     * [
     *     'message' => '', // 说明
     *     'data' => [ // 数据
     *         'transaction_id' => '780930255958621794', // 上传的唯一事务ID
     *     ],
     * ]
     *
     * 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
     * false
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     */
    public function clientUploadReady($data)
    {
        $response = Yii::$app->qqApiHttps->createRequest()
            ->setMethod('post')
            ->setUrl('video/clientuploadready')
            ->setData([
                'access_token' => $data['accessToken'],
                'size' => $data['size'],
                'md5' => $data['md5'],
                'sha' => $data['sha'],
            ])
            ->send();
        // file_put_contents('E:/wwwroot/channel-pub-api/qq/runtime/client-upload-ready_' . $data['size'] . '_' . time() . '.txt', $response->data['data']['transaction_id']);
        // 检查响应状态码是否等于20x
        if ($response->isOk) {
            // 检查业务逻辑是否成功
            if ($response->data['code'] === 0) {
                $responseData = ['message' => '', 'data' => $response->data['data']];
                return $responseData;
            } else {
                $this->addError('id', $response->data['msg']);
                return false;
            }
        } else {
            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35041'), ['statusCode' => $response->getStatusCode()])), 35041);
        }
    }

    /**
     * HTTP请求,企鹅号的内容网站应用的视频文件分片上传
     *
     * @param array $data 数据
     * 格式如下:
     * [
     *     'accessToken' => 'LQVHLUQDNEKIDBFARJ1OOA', // 企鹅平台企鹅号应用授权调用凭据
     *     'transactionId' => '780930287703152921', // 上传的唯一事务ID
     *     'mediatrunk' => 'E:/wwwroot/channel-pub-api/storage/channel-pub-api/videos/2018/10/27/1540632234.9842.66697370.mp4', // 视频 mediatrunk 文件
     *     'startOffset' => 0, // 分片的起始位置(从0开始计数)
     * ]
     *
     * @return array|false
     * 格式如下:
     * 企鹅号的内容网站应用的视频文件分片上传
     * [
     *     'message' => '', // 说明
     *     'data' => [ // 数据
     *         'end_offset' => 2198151, // 分片的结束位置
     *         'start_offset' => 2198151, // 分片的起始位置
     *         'transaction_id' => 780930255958621794, // 上传的唯一事务ID
     *     ],
     * ]
     *
     * 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
     * false
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     */
    public function clientUploadTrunk($data)
    {
        $response = Yii::$app->qqApiHttp->createRequest()
            ->setMethod('post')
            ->setUrl('video/clientuploadtrunk?access_token=' . $data['accessToken'] . '&transaction_id=' . $data['transactionId'] . '&start_offset=' . $data['startOffset'])
            ->addFile('mediatrunk', $data['mediatrunk'])
            ->send();
        // file_put_contents('E:/wwwroot/channel-pub-api/qq/runtime/client-upload-trunk_' . $data['transactionId'] . '_' . $data['startOffset'] . '_' . time() . '.txt', $response->data['code']);
        // 检查响应状态码是否等于20x
        if ($response->isOk) {
            // 检查业务逻辑是否成功
            if ($response->data['code'] === 0) {
                $responseData = ['message' => '', 'data' => $response->data['data']];
                return $responseData;
            } elseif ($response->data['code'] === 40027) { // 无效的事务ID
                $this->addError('id', $response->data['code']);
                return false;
            } else {
                $this->addError('id', $response->data['msg']);
                return false;
            }
        } else {
            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35041'), ['statusCode' => $response->getStatusCode()])), 35041);
        }

    }
}

32. Implementation of the transaction interface for the Penguin API, edit \common\logics\http\qq_api\Transaction.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/10/30
 * Time: 18:41
 */
 
namespace common\logics\http\qq_api;
 
use Yii;
use yii\web\ServerErrorHttpException;
 
/**
 * 企鹅号接口的事务
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class Transaction extends Model
{
    const STATUS_SUCCESS = '成功'; //状态:成功
    const STATUS_ERROR = '失败'; //状态:失败
    const STATUS_PROCESSING = '处理中'; //状态:处理中

    const TYPE_ARTICLE = '文章'; //类型:文章
    const TYPE_VIDEO = '视频'; //类型:视频

    const ARTICLE_TYPE_NORMAL = '普通文章'; //文章类型:普通文章
    const ARTICLE_TYPE_IMAGES = '图文文章'; //文章类型:图文文章
    const ARTICLE_TYPE_MULTIVIDEOS = '视频文章'; //文章类型:视频文章
    const ARTICLE_TYPE_LIVE = '直播文章'; //文章类型:直播文章
    const ARTICLE_TYPE_RTMP_LIVE = 'RTMP直播文章'; //文章类型:RTMP直播文章

    const ARTICLE_PUB_FLAG_PUBLISHED = '发布成功'; //文章发布状态:发布成功
    const ARTICLE_PUB_FLAG_UNPUBLISHED = '未发布'; //文章发布状态:未发布
    const ARTICLE_PUB_FLAG_REVIEW = '审核中'; //文章发布状态:审核中

    /**
     * HTTP请求,基于上传的唯一事务ID获取事务信息
     *
     * @param array $data 数据
     * 格式如下:
     * [
     *     'accessToken' => 'LQVHLUQDNEKIDBFARJ1OOA', // 企鹅平台企鹅号应用授权调用凭据
     *     'transactionId' => '780931016953455275', // 上传的唯一事务ID
     * ]
     *
     * @return array|false
     * 格式如下:
     * 企鹅号的内容网站应用的视频文件分片上传的唯一事务ID
     * [
     *     'message' => 'SUCCESS', // 说明
     *     'data' => [ // 数据
     *         'transaction_id' => '780931016953455275', // 唯一事务id
     *         'transaction_status' => '处理中', // 事务处理状态,取值:成功,失败,处理中
     *         'ext_err' => '', //
     *         'transaction_err_msg' => '', //
     *         'transaction_type' => '视频', // 事务类型,取值:文章,视频
     *         'vid' => 'd0776lmbvso', // 视频文件唯一标示ID
     *         'transaction_ctime' => '2018-10-31 19:49:43', // 事务创建时间
     *         'article_info' => [ // 文章信息字段,当事务类型为文章时候有此内容
     *             'article_title' => '', // 文章标题
     *             'article_type' => '', // 文章类型,取值:普通文章,图文文章,视频文章,直播文章,RTMP直播文章
     *             'article_abstract' => '', // 文章摘要
     *             'article_imgurl' => '', // 文章封面图
     *             'article_pub_flag' => '', // 文章发布状态,取值:未发布,发布成功,审核中
     *             'article_pub_time' => '', // 文章发布时间
     *             'article_id' => '', // 上传的唯一事务ID
     *             'article_url' => '', // 文章快报链接
     *             'article_video_info' => [ // 视频文章信息字段,有此内容
     *                 'vid' => '', // 视频唯一id
     *                 'title' => '', // 视频标题
     *                 'desc' => '', // 视频描述
     *                 'type' => '', // 类型,视频
     *             ],
     *             'article_pid' => '', //
     *         ],
     *     ],
     * ]
     *
     * 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
     * false
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     */
    public function clientInfo($data)
    {
        $response = Yii::$app->qqApiHttps->createRequest()
            ->setMethod('get')
            ->setUrl('transaction/infoclient')
            ->setData([
                'access_token' => $data['accessToken'],
                'transaction_id' => $data['transactionId'],
            ])
            ->send();
        // file_put_contents('E:/wwwroot/channel-pub-api/qq/runtime/client-info_' . $data['transactionId'] . time() . '.txt', $response->data['code']);
        // 检查响应状态码是否等于20x
        if ($response->isOk) {
            // 检查业务逻辑是否成功
            if ($response->data['code'] === 0) {
                $responseData = ['message' => $response->data['msg'], 'data' => $response->data['data']];
                return $responseData;
            } else {
                $this->addError('id', $response->data['msg']);
                return false;
            }
        } else {
            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35041'), ['statusCode' => $response->getStatusCode()])), 35041);
        }

    }
}

33. Implementation of the queue (upload resource file queue) event handler, edit \common\components\queue\UploadAssetEventHandler.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/10/23
 * Time: 19:35
 */
 
namespace common\components\queue;
 
use Yii;
use common\logics\ChannelType;
use common\logics\Asset;
use common\services\TaskService;
use common\services\ChannelAppTaskService;
use common\jobs\UploadAssetJob;
use yii\base\Component;
use yii\helpers\ArrayHelper;
use yii\queue\ExecEvent;
use yii\web\NotFoundHttpException;
use yii\web\ServerErrorHttpException;
use yii\web\UnprocessableEntityHttpException;
 
/**
 * Class UploadAssetEventHandler
 * @package common\components\queue
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class UploadAssetEventHandler extends Component
{
    /**
     * @param ExecEvent $event
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws ServerErrorHttpException 如果基于任务ID查找状态为启用的资源列表为空,将抛出 500 HTTP 异常
     */
    public static function afterExec(ExecEvent $event)
    {
        $channelAppTaskId = $event->job->channelAppTaskId;

        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($channelAppTaskId);

        // 查找状态为启用的数据模型(渠道的类型)
        $channelTypeEnabledItems = ChannelType::find()->isDeletedNo()->enabled()->all();

        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($channelAppTaskEnabledItem->task_id);

        // 基于任务ID查找状态为启用的资源列表
        $assetEnabledItems = Asset::findAllEnabledByTaskId($taskEnabledItem->id);

        if (empty($assetEnabledItems)) {
            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35020'), ['task_id' => $taskEnabledItem->id])), 35020);
        }

        // 获取基于任务ID查找状态为启用的资源列表的id值列表
        $assetEnabledIds = ArrayHelper::getColumn($assetEnabledItems, 'id');
        // 不属于当前任务ID下的资源ID
        $notBelongIds = [];
        // 作业类型不支持的资源ID
        $jobTypeNotSupportIds = [];
        // 作业类型与渠道的类型代码不匹配的资源ID
        $notMatchChannelTypeCodeIds = [];

        $assets = $event->job->assets;
        foreach ($event->job->assets as $assetKey => $asset) {

            $assetJobType = $taskEnabledItem->channel_type_code . '_' . $asset['job_type'];

            // 不属于当前任务ID下的资源ID
            if (!in_array($asset['id'], $assetEnabledIds)) {
                $notBelongIds[] = $asset['id'];
            }

            // 作业类型不支持的资源ID
            if (!in_array($asset['job_type'], [UploadAssetJob::JOB_TYPE_VIDEO_MULTIPART])) {
                $jobTypeNotSupportIds[] = $assetJobType;
            }

            // 作业类型与渠道的类型代码不匹配的资源ID
            $matchChannelTypeCode = false;
            foreach ($channelTypeEnabledItems as $itemKey => $channelTypeEnabledItem) {
                $pos = strpos($assetJobType, $channelTypeEnabledItem->code);
                if ($pos !== false) {
                    $assets[$assetKey]['service_class'] = 'common\services\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $channelTypeEnabledItem->code))) . 'AssetService'; // 例:common\services\QqCwAssetService
                    $assets[$assetKey]['service_action'] = 'uploadAsset' . str_replace(' ', '', ucwords(str_replace('_', ' ', $asset['job_type']))) . 'ExecHandler'; // 例:uploadAssetVideoMultipartExecHandler
                    $matchChannelTypeCode = true;
                    break;
                }
            }
            if (!$matchChannelTypeCode) {
                $notMatchChannelTypeCodeIds[] = $assetJobType;
            }
        }

        if (!empty($notBelongIds)) {
            $notBelongIds = implode(",", $notBelongIds);
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35025'), ['not_belong_ids' => $notBelongIds, 'task_id' => $taskEnabledItem->id])), 35025);
        }

        if (!empty($jobTypeNotSupportIds)) {
            $jobTypeNotSupportIds = implode(",", array_unique($jobTypeNotSupportIds));
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35026'), ['job_type_not_support_ids' => $jobTypeNotSupportIds])), 35026);
        }

        if (!empty($notMatchChannelTypeCodeIds)) {
            $notMatchChannelTypeCodeIds = implode(",", array_unique($notMatchChannelTypeCodeIds));
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35027'), ['not_match_channel_type_code_ids' => $notMatchChannelTypeCodeIds])), 35027);
        }

        // 基于作业类型调用相应服务进行后续处理
        foreach ($assets as $asset) {
            $serviceAction = $asset['service_action'];
            $asset['service_class']::$serviceAction($asset['id'], $channelAppTaskId);
        }
    }

    /**
     * @param ExecEvent $event
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws ServerErrorHttpException 如果基于任务ID查找状态为启用的资源列表为空,将抛出 500 HTTP 异常
     */
    public static function afterError(ExecEvent $event)
    {
        $channelAppTaskId = $event->job->channelAppTaskId;

        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($channelAppTaskId);

        // 查找状态为启用的数据模型(渠道的类型)
        $channelTypeEnabledItems = ChannelType::find()->isDeletedNo()->enabled()->all();

        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($channelAppTaskEnabledItem->task_id);

        // 基于任务ID查找状态为启用的资源列表
        $assetEnabledItems = Asset::findAllEnabledByTaskId($taskEnabledItem->id);

        if (empty($assetEnabledItems)) {
            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35020'), ['task_id' => $taskEnabledItem->id])), 35020);
        }

        // 获取基于任务ID查找状态为启用的资源列表的id值列表
        $assetEnabledIds = ArrayHelper::getColumn($assetEnabledItems, 'id');
        // 不属于当前任务ID下的资源ID
        $notBelongIds = [];
        // 作业类型不支持的资源ID
        $jobTypeNotSupportIds = [];
        // 作业类型与渠道的类型代码不匹配的资源ID
        $notMatchChannelTypeCodeIds = [];

        $assets = $event->job->assets;
        foreach ($event->job->assets as $assetKey => $asset) {

            $assetJobType = $taskEnabledItem->channel_type_code . '_' . $asset['job_type'];

            // 不属于当前任务ID下的资源ID
            if (!in_array($asset['id'], $assetEnabledIds)) {
                $notBelongIds[] = $asset['id'];
            }

            // 作业类型不支持的资源ID
            if (!in_array($asset['job_type'], [UploadAssetJob::JOB_TYPE_VIDEO_MULTIPART])) {
                $jobTypeNotSupportIds[] = $assetJobType;
            }

            // 作业类型与渠道的类型代码不匹配的资源ID
            $matchChannelTypeCode = false;
            foreach ($channelTypeEnabledItems as $itemKey => $channelTypeEnabledItem) {
                $pos = strpos($assetJobType, $channelTypeEnabledItem->code);
                if ($pos !== false) {
                    $assets[$assetKey]['service_class'] = 'common\services\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $channelTypeEnabledItem->code))) . 'AssetService'; // 例:common\services\QqCwAssetService
                    $assets[$assetKey]['service_action'] = 'uploadAsset' . str_replace(' ', '', ucwords(str_replace('_', ' ', $asset['job_type']))) . 'ErrorHandler'; // 例:uploadAssetVideoMultipartErrorHandler
                    $matchChannelTypeCode = true;
                    break;
                }
            }
            if (!$matchChannelTypeCode) {
                $notMatchChannelTypeCodeIds[] = $assetJobType;
            }
        }

        if (!empty($notBelongIds)) {
            $notBelongIds = implode(",", $notBelongIds);
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35025'), ['not_belong_ids' => $notBelongIds, 'task_id' => $taskEnabledItem->id])), 35025);
        }

        if (!empty($jobTypeNotSupportIds)) {
            $jobTypeNotSupportIds = implode(",", array_unique($jobTypeNotSupportIds));
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35026'), ['job_type_not_support_ids' => $jobTypeNotSupportIds])), 35026);
        }

        if (!empty($notMatchChannelTypeCodeIds)) {
            $notMatchChannelTypeCodeIds = implode(",", array_unique($notMatchChannelTypeCodeIds));
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35027'), ['not_match_channel_type_code_ids' => $notMatchChannelTypeCodeIds])), 35027);
        }

        // 基于作业类型调用相应服务进行后续处理
        foreach ($assets as $asset) {
            $serviceAction = $asset['service_action'];
            $asset['service_class']::$serviceAction($asset['id'], $channelAppTaskId, $event->error);
        }
    }
}

34. Clear the previous test data, POST http://api.channel-pub-api.localhost/qq/v1/articles/video?group_id=spider , executed successfully. I intentionally made the value of media_absolute_url correct to test the subsequent processing after the copy resource file queue job executes successfully, i.e., the multipart upload of video files for the Penguin content website application. After the queue task executes successfully, call the corresponding service; otherwise, insert a publication log (asynchronously); and the file represented by media_absolute_url is a file with a size of 2.1 GB, to test the subsequent processing after the upload resource file queue job fails to execute.

Request Body

JSON

{
	"channel_app_source_uuids": ["bba4e024eba111e89fa754ee75d2ebc1"],
	"source": "spider",
	"source_uuid": "825e6d5e36468cc4bf536799ce3565cf",
	"source_pub_user_id": 1,
	"source_callback_url": "http://wjdev.chinamcloud.com:8609/cms/api/thirdPush/callBack",
	"article_category_id": 226,
	"title": "综艺节目 - 20181126 - 1",
	"author": "综艺节目 - 20181126 - 1",
	"source_article_id": 1,
	"media_absolute_url": "http://127.0.0.1/channel-pub-api/storage/spider/videos/13f05f8f4633d8b9e9340089be533f7e_h264_500k_mp4.mp4",
	"tag": "综艺",
	"apply": 0,
	"desc": "综艺节目 - 20181126 - 1"
}

Response Body

JSON

{
    "code": 10000,
    "message": "发布文章类型:视频(视频)的文章成功",
    "data": [
        {
            "channel_id": 1,
            "channel_code": "qq",
            "channel_type_id": 1,
            "channel_type_code": "qq_cw",
            "channel_app_source_id": 6,
            "channel_app_source_uuid": "bba4e024eba111e89fa754ee75d2ebc1",
            "task_id": 21,
            "status": 1,
            "created_at": 1543224918,
            "updated_at": 1543224918,
            "uuid": "96cc4ffef15e11e88b6b54ee75d2ebc1",
            "id": 21
        }
    ]
}

35. Execute the SQL statement as follows:

SQL

SELECT * FROM `cpa_channel` WHERE (`code`='qq') AND (`is_deleted`=0)
SELECT * FROM `cpa_channel_app_source` WHERE (`uuid`='bba4e024eba111e89fa754ee75d2ebc1') AND (`is_deleted`=0)
SELECT * FROM `cpa_channel_type` WHERE (`code`='qq_cw') AND (`is_deleted`=0)
SELECT * FROM `cpa_qq_cw_app` WHERE (`channel_app_source_uuid`='bba4e024eba111e89fa754ee75d2ebc1') AND (`is_deleted`=0)
SELECT * FROM `cpa_article_type` WHERE (`code`='video') AND (`is_deleted`=0)
SELECT * FROM `cpa_qq_article_type` WHERE (`code`='multivideos') AND (`is_deleted`=0)
SELECT EXISTS(SELECT * FROM `cpa_article_category` WHERE (`cpa_article_category`.`id`=226) AND ((`is_deleted`=0) AND (`status`=1)))
SELECT * FROM `cpa_qq_article_category_multivideos` WHERE (`article_category_id`=226) AND (`is_deleted`=0)
INSERT INTO `cpa_task` (`group_id`, `source`, `source_uuid`, `source_pub_user_id`, `source_callback_url`, `channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `status`, `created_at`, `updated_at`) VALUES ('spider', 'spider', '825e6d5e36468cc4bf536799ce3565cf', 1, 'http://wjdev.chinamcloud.com:8609/cms/api/thirdPush/callBack', 1, 'qq', 1, 'qq_cw', 1, 1543224918, 1543224918)
INSERT INTO `cpa_channel_app_task` (`channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `channel_app_source_id`, `channel_app_source_uuid`, `task_id`, `status`, `created_at`, `updated_at`, `uuid`) VALUES (1, 'qq', 1, 'qq_cw', 6, 'bba4e024eba111e89fa754ee75d2ebc1', 21, 1, 1543224918, 1543224918, '96cc4ffef15e11e88b6b54ee75d2ebc1')
INSERT INTO `cpa_qq_cw_app_task` (`channel_app_task_id`, `channel_app_task_uuid`, `qq_cw_app_id`, `task_id`, `status`, `created_at`, `updated_at`) VALUES (21, '96cc4ffef15e11e88b6b54ee75d2ebc1', 5, 21, 1, 1543224919, 1543224919)
INSERT INTO `cpa_qq_article` (`group_id`, `article_category_id`, `title`, `author`, `source_article_id`, `qq_app_type`, `article_type_id`, `qq_article_type_id`, `qq_article_category_id`, `task_id`, `status`, `created_at`, `updated_at`) VALUES ('spider', 226, '综艺节目 - 20181126 - 1', '综艺节目 - 20181126 - 1', 1, 'cw', 3, 2, 2808, 21, 1, 1543224919, 1543224919)
INSERT INTO `cpa_qq_article_multivideos` (`media`, `tag`, `desc`, `apply`, `qq_article_id`, `category`, `md5`, `vid`, `task_id`, `status`, `created_at`, `updated_at`) VALUES ('', '综艺', '综艺节目 - 20181126 - 1', 0, 21, 2808, '', '', 21, 1, 1543224919, 1543224919)
INSERT INTO `cpa_asset` (`channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `source`, `type`, `absolute_url`, `relative_path`, `size`, `task_id`, `channel_article_id`, `status`, `is_deleted`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 'qq', 1, 'qq_cw', 'spider', 'video', 'http://127.0.0.1/channel-pub-api/storage/spider/videos/13f05f8f4633d8b9e9340089be533f7e_h264_500k_mp4.mp4', '', 0, 21, 21, 1, 0, 1543224919, 1543224919, 0)

36. View the status information of the 4 queues

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

37. The run command fetches and executes tasks in a loop (copy resource file queue) until the queue is empty

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/run --verbose=1 --isolate=1 --color=0
2018-11-26 17:40:44 [pid: 169228] - Worker is started
2018-11-26 17:40:45 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 169228) - Started
2018-11-26 17:41:51 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 169228) - Done (66.431 s)
2018-11-26 17:41:51 [pid: 169228] - Worker is stopped (0:01:07)

38. The copy resource file queue job executed successfully, the execution result meets expectations, resources have been batch updated, and the job has been pushed to the upload resource file queue, as shown in Figure 7

The copy resource file queue job executed successfully, the execution result meets expectations, resources have been batch updated, and the job has been pushed to the upload resource file queue
Figure 7
Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

39. The run command fetches and executes tasks in a loop (upload resource file queue) until the queue is empty. During task execution, I intentionally used VPN software (with poor network quality) to test the subsequent processing after the upload resource file queue job fails to execute. Update the task of the Penguin content website application based on the channel application’s task ID (scenario: after the upload resource file queue job fails, the number of publishable times decreases by 1, status, 3: Publishing (Failed)); insert a publication log, and push the job to the source callback queue (asynchronously); find a single data model (multipart upload of video files for the Penguin content website application) based on the resource ID and the Penguin application’s task ID. If it exists, update it to 3: Uploading (Failed). As shown in Figure 8

The run command fetches and executes tasks in a loop (upload resource file queue) until the queue is empty. During task execution, I intentionally used VPN software (with poor network quality) to test the subsequent processing after the upload resource file queue job fails to execute. Update the task of the Penguin content website application based on the channel application's task ID (scenario: after the upload resource file queue job fails, the number of publishable times decreases by 1, status, 3: Publishing (Failed)); insert a publication log, and push the job to the source callback queue (asynchronously); find a single data model (multipart upload of video files for the Penguin content website application) based on the resource ID and the Penguin application's task ID. If it exists, update it to 3: Uploading (Failed)
Figure 8
Plaintext

PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/run --verbose=1 --isolate=1 --color=0
2018-11-26 17:57:46 [pid: 172564] - Worker is started
2018-11-26 17:57:46 [1] common\jobs\UploadAssetJob (attempt: 1, pid: 172564) - Started
2018-11-26 18:00:43 [1] common\jobs\UploadAssetJob (attempt: 1, pid: 172564) - Error (176.710 s)
> yii\httpclient\Exception: Curl error: #6 - Could not resolve host: api.om.qq.com
2018-11-26 18:00:43 [pid: 172564] - Worker is stopped (0:02:57)
PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0

40. Clear the previous test data, POST http://api.channel-pub-api.localhost/qq/v1/articles/video?group_id=spider , executed successfully. I intentionally made the value of media_absolute_url correct to test the subsequent processing after the copy resource file queue job executes successfully, i.e., the multipart upload of video files for the Penguin content website application. After the queue task executes successfully, call the corresponding service; otherwise, insert a publication log (asynchronously); and the file represented by media_absolute_url is a file with a size of 8.44 MB, to test the subsequent processing after the upload resource file queue job executes successfully

Request Body

JSON

{
	"channel_app_source_uuids": ["bba4e024eba111e89fa754ee75d2ebc1"],
	"source": "spider",
	"source_uuid": "825e6d5e36468cc4bf536799ce3565cf",
	"source_pub_user_id": 1,
	"source_callback_url": "http://wjdev.chinamcloud.com:8609/cms/api/thirdPush/callBack",
	"article_category_id": 226,
	"title": "综艺节目 - 20181126 - 2",
	"author": "综艺节目 - 20181126 - 2",
	"source_article_id": 1,
	"media_absolute_url": "http://127.0.0.1/channel-pub-api/storage/spider/videos/3d35e17a32fb48cfb76f39a1b1bf33ce_h264_1200k_mp4.mp4",
	"tag": "综艺",
	"apply": 0,
	"desc": "综艺节目 - 20181126 - 2"
}

Response Body

JSON

{
    "code": 10000,
    "message": "发布文章类型:视频(视频)的文章成功",
    "data": [
        {
            "channel_id": 1,
            "channel_code": "qq",
            "channel_type_id": 1,
            "channel_type_code": "qq_cw",
            "channel_app_source_id": 6,
            "channel_app_source_uuid": "bba4e024eba111e89fa754ee75d2ebc1",
            "task_id": 22,
            "status": 1,
            "created_at": 1543227265,
            "updated_at": 1543227265,
            "uuid": "0d54f23ef16411e8abbe54ee75d2ebc1",
            "id": 22
        }
    ]
}

41. View the status information of the 4 queues

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

42. The run command fetches and executes tasks in a loop (copy resource file queue) until the queue is empty

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/run --verbose=1 --isolate=1 --color=0
2018-11-26 18:15:48 [pid: 168344] - Worker is started
2018-11-26 18:15:48 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 168344) - Started
2018-11-26 18:15:49 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 168344) - Done (0.440 s)
2018-11-26 18:15:49 [pid: 168344] - Worker is stopped (0:00:01)

43. The copy resource file queue job executed successfully, the execution result meets expectations, resources have been batch updated, and the job has been pushed to the upload resource file queue

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

44. The run command fetches and executes tasks in a loop (upload resource file queue) until the queue is empty. During task execution, I intentionally exited the VPN software (with better network quality) to test the subsequent processing after the upload resource file queue job executes successfully. There is no corresponding processing for now, and the method is empty. The corresponding record already exists in the transaction table, as shown in Figure 9

The run command fetches and executes tasks in a loop (upload resource file queue) until the queue is empty. During task execution, I intentionally exited the VPN software (with better network quality) to test the subsequent processing after the upload resource file queue job executes successfully. There is no corresponding processing for now, and the method is empty. The corresponding record already exists in the transaction table
Figure 9
Plaintext

PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/run --verbose=1 --isolate=1 --color=0
2018-11-26 18:23:08 [pid: 173512] - Worker is started
2018-11-26 18:23:08 [4] common\jobs\UploadAssetJob (attempt: 1, pid: 173512) - Started
2018-11-26 18:23:17 [4] common\jobs\UploadAssetJob (attempt: 1, pid: 173512) - Done (8.575 s)
2018-11-26 18:23:17 [pid: 173512] - Worker is stopped (0:00:09)

45. Video transaction controller for the Penguin content website application, implementation of the video transaction for the synchronous interface, edit \console\controllers\QqCwTransactionVideoController.php

PHP
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace console\controllers;
 
use Yii;
use console\models\Channel;
use console\models\ChannelType;
use console\models\QqArticle;
use console\models\QqTransaction;
use console\services\ChannelService;
use console\services\ChannelTypeService;
use console\services\QqCwAccessTokenService;
use console\services\QqTransactionService;
use console\services\QqCwTransactionService;
use console\services\QqVideoMultipartUploadService;
use yii\console\Controller;
use yii\console\ExitCode;
use yii\web\NotFoundHttpException;
use yii\web\ServerErrorHttpException;
 
/**
 * 企鹅号的内容网站应用的视频事务控制器
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class QqCwTransactionVideoController extends Controller
{
    /**
     * 控制台命令:获取企鹅号的内容网站应用的企鹅号接口的视频事务,同步至企鹅号的内容网站应用的企鹅号的视频事务  qq-cw-transaction-video/sync(qq-cw-transaction-video/sync)
     *
     * 1、输入数据验证规则
     * (1)查询渠道代码,qq:企鹅号是否存在,如果不存在,则返回失败
     * (2)判断渠道代码,qq:企鹅号的状态是否启用,如果未启用,则返回失败
     * (3)如果渠道的类型代码,qq_cw:企鹅号的内容网站应用,查询渠道的类型代码,qq_cw:企鹅号的内容网站应用是否存在,如果不存在,则返回失败
     * (4)如果渠道的类型代码,qq_cw:企鹅号的内容网站应用,判断渠道的类型代码,qq_cw:企鹅号的内容网站应用的状态是否启用,如果未启用,则返回失败
     *
     * 2、操作数据(事务)
     * (1)查询企鹅号的应用类型,cw:内容网站应用 && 类型,2:视频 && 状态,3:处理中的20条记录,基于ID顺序排列(企鹅号的事务)
     * (2)如果企鹅号的事务列表不为空,遍历处理(单个处理后延缓执行 10 秒),如果为空(延缓执行 60 秒),则退出
     * (3)基于ID查找单个数据模型(企鹅号的事务),判断企鹅号的事务状态,如果不为,3:处理中,则跳出本次循环
     * (4)基于企鹅号的内容网站应用ID获取有效的 Access Token
     * (5)HTTP请求,基于上传的唯一事务ID获取事务信息
     * (6)基于企鹅号接口的事务类型获取企鹅号的事务类型
     * (7)基于企鹅号接口的事务状态获取企鹅号的事务状态
     * (8)判断企鹅号的事务状态,如果为,3:处理中,则跳出本次循环
     * (9)判断企鹅号的事务状态,如果为,1:成功
     * (10)基于企鹅号的应用的视频文件分片上传ID查找状态为已上传的单个数据模型(企鹅号的内容网站应用的视频文件分片上传)
     * (11)如果未找到数据模型,将抛出 404 HTTP 异常
     * (12)如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * (13)更新企鹅号的视频文件分片上传的视频文件唯一标示ID
     * (14)更新企鹅号的事务的状态,1:成功
     * (15)判断更新企鹅号的事务时受影响的行数,是否等于 1,如果不等于 1,则跳出本次循环
     * (16)企鹅号的内容网站应用的企鹅号的视频事务成功后的后续处理,例:QqCwTransactionService::videoExecHandler($qqTransactionId)
     * (17)判断企鹅号的事务状态,如果为,2:失败
     * (18)更新企鹅号的事务的状态,2:失败
     * (19)企鹅号的内容网站应用的企鹅号的视频事务失败后的后续处理,例:QqCwTransactionService::videoErrorHandler($qqTransactionId)
     *
     * 3、企鹅号的内容网站应用的企鹅号的视频事务成功后的后续处理
     * (1)基于ID查找状态为成功的单个数据模型(企鹅号的事务)
     * (2)如果未找到数据模型,将抛出 404 HTTP 异常
     * (3)如果找到数据模型,状态未成功,将抛出 422 HTTP 异常
     * (4)判断企鹅号的应用类型,如果不是,cw:内容网站应用,将抛出 422 HTTP 异常
     * (5)判断类型,如果不是,2:视频,将抛出 422 HTTP 异常
     * (6)基于企鹅号的内容网站应用的任务ID查找状态为启用的单个数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联)
     * (7)如果找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),基于微信公众帐号应用的任务ID查找状态为启用的资源列表
     * (8)如果找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),获取微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联的企鹅号的内容网站应用的任务ID值列表
     * (9)如果找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),基于任务ID查询企鹅号的应用类型,cw:内容网站应用 && 类型,2:视频 && 状态,1:成功的资源列表
     * (10)如果找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),判断微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联数量和事务数量是否相等,如果相等,则遍历资源列表,基于ID查找状态为发布中的单个数据模型(企鹅号的内容网站应用的任务),企鹅号的内容网站应用的发布文章类型:视频(视频)的文章至企鹅号平台,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步),调用:WxAssetService::qqCwTransactionVideoExecHandler($taskId)
     * (11)如果未找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),基于ID查找状态为发布中的单个数据模型(企鹅号的内容网站应用的任务)
     * (12)如果未找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),如果未找到数据模型(企鹅号的内容网站应用的任务),将抛出 404 HTTP 异常
     * (13)如果未找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),如果找到数据模型(企鹅号的内容网站应用的任务),状态未发布中,将抛出 422 HTTP 异常
     * (14)企鹅号的内容网站应用的发布文章类型:视频(视频)的文章至企鹅号平台,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步)
     *
     * 4、企鹅号的内容网站应用的企鹅号的视频事务失败后的后续处理
     * (1)基于ID查找状态为失败的单个数据模型(企鹅号的事务)
     * (2)如果未找到数据模型,将抛出 404 HTTP 异常
     * (3)如果找到数据模型,状态未失败,将抛出 422 HTTP 异常
     * (4)判断企鹅号的应用类型,如果不是,cw:内容网站应用,将抛出 422 HTTP 异常
     * (5)判断类型,如果不是,2:视频,将抛出 422 HTTP 异常
     * (6)基于ID查找状态为发布中的单个数据模型(企鹅号的内容网站应用的任务)
     * (7)如果未找到数据模型(企鹅号的内容网站应用的任务),将抛出 404 HTTP 异常
     * (8)如果找到数据模型(企鹅号的内容网站应用的任务),状态未发布中,将抛出 422 HTTP 异常
     * (9)基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
     * (10)如果未找到数据模型,将抛出 404 HTTP 异常
     * (11)如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * (12)基于渠道的应用的任务ID更新企鹅号的内容网站应用的任务(场景:企鹅号的视频事务失败后,可发布次数减1,状态,3:发布中(已失败))
     * (13)发布任务失败后,插入发布日志,将作业推送至来源回调队列(异步)
     * (14)基于企鹅号的内容网站应用的任务ID查找状态为启用的单个资源(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),如果存在,调用:WxAssetService::qqCwTransactionVideoErrorHandler($taskId, $errorCode, $errorMessage)
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws ServerErrorHttpException
     * @throws \Throwable
     */
    public function actionSync()
    {
        // 基于代码查找状态为启用的单个数据模型(渠道)
        $channelEnabledItem = ChannelService::findModelEnabledByCode(Channel::CODE_QQ);

        // 基于代码查找状态为启用的单个数据模型(渠道的类型)
        $channelTypeEnabledItem = ChannelTypeService::findModelEnabledByCode(ChannelType::CODE_QQ_CW);

        // 查询企鹅号的应用类型,cw:内容网站应用 && 类型,2:视频 && 状态,3:处理中的20条记录,基于ID顺序排列
        $qqTransactionVideoItems = QqTransaction::find()->where(['qq_app_type' => QqArticle::QQ_APP_TYPE_CW, 'type' => QqTransaction::TYPE_VIDEO])->isDeletedNo()->processing()->orderBy(['id' => SORT_ASC])->limit(20)->all();

        if ($qqTransactionVideoItems) {
            foreach ($qqTransactionVideoItems as $qqTransactionVideoItem) {
                // 基于ID查找单个数据模型(企鹅号的事务)
                $qqTransactionVideoItem = QqTransaction::findOne($qqTransactionVideoItem->id);
                // 判断企鹅号的事务状态,如果不为,3:处理中,则跳出本次循环
                if($qqTransactionVideoItem->status != QqTransaction::STATUS_PROCESSING){
                    continue;
                }

                // 基于企鹅号的内容网站应用ID获取有效的 Access Token
                $qqCwAccessTokenService = new QqCwAccessTokenService();
                $accessTokenValidity = $qqCwAccessTokenService->getAccessTokenValidityByQqCwAppId($qqTransactionVideoItem->qq_app_id);

                // HTTP请求,基于上传的唯一事务ID获取事务信息
                $qqTransactionService = new QqTransactionService();
                $qqTransactionServiceHttpTransactionInfoData = [
                    'accessToken' => $accessTokenValidity->access_token,
                    'transactionId' => $qqTransactionVideoItem->transaction_id,
                ];
                $qqTransactionServiceHttpTransactionInfoResult = $qqTransactionService->httpTransactionInfo($qqTransactionServiceHttpTransactionInfoData);

                // 基于企鹅号接口的事务状态获取企鹅号的事务状态
                $qqTransactionStatus = QqTransactionService::getStatusByHttpQqApiTransactionStatus($qqTransactionServiceHttpTransactionInfoResult['transaction_status']);

                // 判断企鹅号的事务状态,如果为,3:处理中,则跳出本次循环
                if ($qqTransactionStatus == QqTransaction::STATUS_PROCESSING) {
                    continue;
                }

                /* 判断企鹅号的事务状态 */
                if ($qqTransactionStatus == QqTransaction::STATUS_SUCCESS) { // 状态,1:成功
                    // 基于企鹅号的应用的视频文件分片上传ID查找状态为已上传的单个数据模型(企鹅号的内容网站应用的视频文件分片上传)
                    $qqVideoMultipartUploadItem = QqVideoMultipartUploadService::findModelUploadedById($qqTransactionVideoItem->qq_video_multipart_upload_id);

                    /* 操作数据(事务) */
                    $transaction = Yii::$app->db->beginTransaction();
                    try {
                        // 更新企鹅号的视频文件分片上传的视频文件唯一标示ID
                        $qqVideoMultipartUploadService = new QqVideoMultipartUploadService();
                        $qqVideoMultipartUploadItem->vid = $qqTransactionServiceHttpTransactionInfoResult['vid'];
                        $qqVideoMultipartUploadServiceUpdateResult = $qqVideoMultipartUploadService->update($qqVideoMultipartUploadItem);
                        if ($qqVideoMultipartUploadServiceUpdateResult['status'] === false) {
                            throw new ServerErrorHttpException($qqVideoMultipartUploadServiceUpdateResult['message'], $qqVideoMultipartUploadServiceUpdateResult['code']);
                        }

                        // 更新企鹅号的事务的状态,1:成功
                        $qqTransactionVideoItem->status = QqTransaction::STATUS_SUCCESS;
                        $qqTransactionVideoItemUpdateResult = $qqTransactionVideoItem->update();
                        if ($qqTransactionVideoItemUpdateResult !== false) {

                        } elseif ($qqTransactionVideoItem->hasErrors()) {
                            foreach ($qqTransactionVideoItem->getFirstErrors() as $message) {
                                $firstErrors = $message;
                                break;
                            }
                            throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35087'), ['model' => $qqTransactionVideoItem->formName(), 'first_errors' => $firstErrors])), 35087);
                        } elseif (!$qqTransactionVideoItem->hasErrors()) {
                            throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
                        }

                        $transaction->commit();
                    } catch(\Throwable $e) {
                        $transaction->rollBack();
                        throw $e;
                    }

                    // 判断更新企鹅号的事务时受影响的行数,是否等于 1,如果不等于 1,则跳出本次循环
                    if ($qqTransactionVideoItemUpdateResult != 1) {
                        continue;
                    }

                    // 企鹅号的内容网站应用的企鹅号的视频事务成功后的后续处理
                    QqCwTransactionService::videoExecHandler($qqTransactionVideoItem->id);

                } elseif ($qqTransactionStatus == QqTransaction::STATUS_ERROR) { // 状态,2:失败
                    // 更新企鹅号的事务的状态,2:失败
                    $qqTransactionVideoItem->ext_err = $qqTransactionServiceHttpTransactionInfoResult['ext_err'];
                    $qqTransactionVideoItem->transaction_err_msg = $qqTransactionServiceHttpTransactionInfoResult['transaction_err_msg'];
                    $qqTransactionVideoItem->status = QqTransaction::STATUS_ERROR;
                    $qqTransactionServiceUpdateResult = $qqTransactionService->update($qqTransactionVideoItem);
                    if ($qqTransactionServiceUpdateResult['status'] === false) {
                        throw new ServerErrorHttpException($qqTransactionServiceUpdateResult['message'], $qqTransactionServiceUpdateResult['code']);
                    }

                    // 企鹅号的内容网站应用的企鹅号的视频事务失败后的后续处理
                    QqCwTransactionService::videoErrorHandler($qqTransactionVideoItem->id);
                }
            }

            // 延缓执行 10 秒
            sleep(Yii::$app->params['qqTransaction']['isEmptyNoSleepTime']);
        } else {
            // 延缓执行 60 秒
            sleep(Yii::$app->params['qqTransaction']['isEmptyYesSleepTime']);
        }

        return ExitCode::OK;
    }

}


46. Article transaction controller for the Penguin content website application, implementation of the article transaction for the synchronous interface, edit \console\controllers\QqCwTransactionArticleController.php

PHP
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace console\controllers;
 
use Yii;
use console\models\Channel;
use console\models\ChannelType;
use console\models\QqArticle;
use console\models\QqTransaction;
use console\models\http\qq_api\Transaction as HttpQqApiTransaction;
use console\services\ChannelService;
use console\services\ChannelTypeService;
use console\services\QqCwAccessTokenService;
use console\services\QqTransactionService;
use console\services\QqCwTransactionService;
use yii\console\Controller;
use yii\console\ExitCode;
use yii\web\NotFoundHttpException;
use yii\web\ServerErrorHttpException;
 
/**
 * 企鹅号的内容网站应用的文章事务控制器
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class QqCwTransactionArticleController extends Controller
{
    /**
     * 控制台命令:获取企鹅号的内容网站应用的企鹅号接口的文章事务,同步至企鹅号的内容网站应用的企鹅号的文章事务  qq-cw-transaction-article/sync(qq-cw-transaction-article/sync)
     *
     * 1、输入数据验证规则
     * (1)查询渠道代码,qq:企鹅号是否存在,如果不存在,则返回失败
     * (2)判断渠道代码,qq:企鹅号的状态是否启用,如果未启用,则返回失败
     * (3)如果渠道的类型代码,qq_cw:企鹅号的内容网站应用,查询渠道的类型代码,qq_cw:企鹅号的内容网站应用是否存在,如果不存在,则返回失败
     * (4)如果渠道的类型代码,qq_cw:企鹅号的内容网站应用,判断渠道的类型代码,qq_cw:企鹅号的内容网站应用的状态是否启用,如果未启用,则返回失败
     *
     * 2、操作数据(事务)
     * (1)查询企鹅号的应用类型,cw:内容网站应用 && 类型,1:文章 && 状态,3:处理中 && 文章发布状态:审核中的20条记录,基于ID顺序排列(企鹅号的事务)
     * (2)如果企鹅号的事务列表不为空,遍历处理(单个处理后延缓执行 10 秒),如果为空(延缓执行 60 秒),则退出
     * (3)基于ID查找单个数据模型(企鹅号的事务),判断企鹅号的事务状态,如果不为,3:处理中,则跳出本次循环
     * (4)基于企鹅号的内容网站应用ID获取有效的 Access Token
     * (5)HTTP请求,基于上传的唯一事务ID获取事务信息
     * (6)基于企鹅号接口的事务类型获取企鹅号的事务类型
     * (7)基于企鹅号接口的事务状态获取企鹅号的事务状态
     * (8)判断企鹅号的事务状态,如果为,3:处理中,则跳出本次循环
     * (9)判断企鹅号的事务状态,如果为,1:成功 && 判断企鹅号的事务文章发布状态,如果为,审核中,则跳出本次循环
     * (10)判断企鹅号的事务状态,如果为,1:成功 && 判断企鹅号的事务文章发布状态,如果为,发布成功
     * (11)更新企鹅号的事务
     * (12)判断更新企鹅号的事务时受影响的行数,是否等于 1,如果不等于 1,则跳出本次循环
     * (13)企鹅号的内容网站应用的企鹅号的文章事务成功后,基于文章类型调用相应服务进行后续处理,例:QqCwTransactionService::articleMultivideosExecHandler($qqTransactionId)
     * (14)判断企鹅号的事务状态,如果为,2:失败 || (判断企鹅号的事务状态,如果为,1:成功 && 判断企鹅号的事务文章发布状态,如果为,未发布)
     * (15)更新企鹅号的事务
     * (16)企鹅号的内容网站应用的企鹅号的文章事务失败后,基于文章类型调用相应服务进行后续处理,例:QqCwTransactionService::articleMultivideosErrorHandler($qqTransactionId)
     *
     * 3、企鹅号的内容网站应用的企鹅号的文章事务成功后的后续处理
     * (1)基于ID查找状态为成功的单个数据模型(企鹅号的事务)
     * (2)如果未找到数据模型,将抛出 404 HTTP 异常
     * (3)如果找到数据模型,状态未成功,将抛出 422 HTTP 异常
     * (4)判断企鹅号的应用类型,如果不是,cw:内容网站应用,将抛出 422 HTTP 异常
     * (5)判断类型,如果不是,1:文章,将抛出 422 HTTP 异常
     * (6)基于ID查找状态为审核中的单个数据模型(企鹅号的内容网站应用的任务)
     * (7)如果未找到数据模型(企鹅号的内容网站应用的任务),将抛出 404 HTTP 异常
     * (8)如果找到数据模型(企鹅号的内容网站应用的任务),状态未审核中,将抛出 422 HTTP 异常
     * (9)基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
     * (10)如果未找到数据模型(渠道的应用的任务),将抛出 404 HTTP 异常
     * (11)如果找到数据模型(渠道的应用的任务),状态未启用,将抛出 422 HTTP 异常
     * (12)基于渠道的应用的任务ID更新企鹅号的内容网站应用的任务(场景:企鹅号的文章事务成功后,可发布次数减1,状态,6:已发布)
     * (13)发布任务成功后,插入发布日志,将作业推送至来源回调队列(异步)
     * (14)基于企鹅号的内容网站应用的任务ID查找状态为启用的单个数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联)
     * (15)如果找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),基于微信公众帐号应用的任务ID查找状态为启用的资源列表
     * (16)如果找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),获取微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联的企鹅号的内容网站应用的任务ID值列表
     * (17)如果找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),基于任务ID查询企鹅号的应用类型,cw:内容网站应用 && 类型,1:文章 && 状态,1:成功 && 文章发布状态,发布成功的资源列表
     * (18)如果找到数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联),判断微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联数量和事务数量是否相等,如果相等,调用:WxArticleService::qqCwTransactionArticleVideoExecHandler($taskId)
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws ServerErrorHttpException
     * @throws \Throwable
     */
    public function actionSync()
    {
        // 基于代码查找状态为启用的单个数据模型(渠道)
        $channelEnabledItem = ChannelService::findModelEnabledByCode(Channel::CODE_QQ);

        // 基于代码查找状态为启用的单个数据模型(渠道的类型)
        $channelTypeEnabledItem = ChannelTypeService::findModelEnabledByCode(ChannelType::CODE_QQ_CW);

        // 查询企鹅号的应用类型,cw:内容网站应用 && 类型,1:文章 && 状态,3:处理中 && 文章发布状态:审核中的20条记录,基于ID顺序排列(企鹅号的事务)
        $qqTransactionArticleItems = QqTransaction::find()->where(['qq_app_type' => QqArticle::QQ_APP_TYPE_CW, 'type' => QqTransaction::TYPE_ARTICLE, 'article_pub_flag' => HttpQqApiTransaction::ARTICLE_PUB_FLAG_REVIEW])->isDeletedNo()->processing()->orderBy(['id' => SORT_ASC])->limit(20)->all();

        if ($qqTransactionArticleItems) {
            foreach ($qqTransactionArticleItems as $qqTransactionArticleItem) {
                // 基于ID查找单个数据模型(企鹅号的事务)
                $qqTransactionArticleItem = QqTransaction::findOne($qqTransactionArticleItem->id);
                // 判断企鹅号的事务状态,如果不为,3:处理中,则跳出本次循环
                if($qqTransactionArticleItem->status != QqTransaction::STATUS_PROCESSING){
                    continue;
                }

                // 基于企鹅号的内容网站应用ID获取有效的 Access Token
                $qqCwAccessTokenService = new QqCwAccessTokenService();
                $accessTokenValidity = $qqCwAccessTokenService->getAccessTokenValidityByQqCwAppId($qqTransactionArticleItem->qq_app_id);

                // HTTP请求,基于上传的唯一事务ID获取事务信息
                $qqTransactionService = new QqTransactionService();
                $qqTransactionServiceHttpTransactionInfoData = [
                    'accessToken' => $accessTokenValidity->access_token,
                    'transactionId' => $qqTransactionArticleItem->transaction_id,
                ];
                $qqTransactionServiceHttpTransactionInfoResult = $qqTransactionService->httpTransactionInfo($qqTransactionServiceHttpTransactionInfoData);

                // 基于企鹅号接口的事务状态获取企鹅号的事务状态
                $qqTransactionStatus = QqTransactionService::getStatusByHttpQqApiTransactionStatus($qqTransactionServiceHttpTransactionInfoResult['transaction_status']);

                // 判断企鹅号的事务状态,如果为,3:处理中,则跳出本次循环
                if ($qqTransactionStatus == QqTransaction::STATUS_PROCESSING) {
                    continue;
                }

                // 判断企鹅号的事务状态,如果为,1:成功 && 判断企鹅号的事务文章发布状态,如果为,审核中,则跳出本次循环
                if ($qqTransactionStatus == QqTransaction::STATUS_SUCCESS && $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_pub_flag'] == HttpQqApiTransaction::ARTICLE_PUB_FLAG_REVIEW) {
                    continue;
                }

                // 更新企鹅号的事务
                $qqTransactionArticleItem->type = $qqTransactionService::getTypeByHttpQqApiTransactionType($qqTransactionServiceHttpTransactionInfoResult['transaction_type']);
                $qqTransactionArticleItem->transaction_ctime = $qqTransactionServiceHttpTransactionInfoResult['transaction_ctime'];
                $qqTransactionArticleItem->ext_err = $qqTransactionServiceHttpTransactionInfoResult['ext_err'];
                $qqTransactionArticleItem->transaction_err_msg = $qqTransactionServiceHttpTransactionInfoResult['transaction_err_msg'];
                $qqTransactionArticleItem->article_abstract = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_abstract'];
                $qqTransactionArticleItem->article_type = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_type'];
                $qqTransactionArticleItem->article_url = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_url'];
                $qqTransactionArticleItem->article_imgurl = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_imgurl'];
                $qqTransactionArticleItem->article_title = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_title'];
                $qqTransactionArticleItem->article_pub_flag = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_pub_flag'];
                $qqTransactionArticleItem->article_pub_time = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_pub_time'];
                $qqTransactionArticleItem->article_video_title = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['title'];
                $qqTransactionArticleItem->article_video_desc = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['desc'];
                $qqTransactionArticleItem->article_video_type = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['type'];
                $qqTransactionArticleItem->article_video_vid = $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['vid'];
                $qqTransactionArticleItem->status = $qqTransactionStatus;

                /* 判断企鹅号的事务状态 */
                if ($qqTransactionStatus == QqTransaction::STATUS_SUCCESS && $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_pub_flag'] == HttpQqApiTransaction::ARTICLE_PUB_FLAG_PUBLISHED) { // 状态,1:成功 && 文章发布状态,发布成功
                    // 更新企鹅号的事务
                    $qqTransactionVideoItemUpdateResult = $qqTransactionArticleItem->update();
                    if ($qqTransactionVideoItemUpdateResult !== false) {

                    } elseif ($qqTransactionArticleItem->hasErrors()) {
                        foreach ($qqTransactionArticleItem->getFirstErrors() as $message) {
                            $firstErrors = $message;
                            break;
                        }
                        throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35087'), ['model' => $qqTransactionArticleItem->formName(), 'first_errors' => $firstErrors])), 35087);
                    } elseif (!$qqTransactionArticleItem->hasErrors()) {
                        throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
                    }

                    // 判断更新企鹅号的事务时受影响的行数,是否等于 1,如果不等于 1,则跳出本次循环
                    if ($qqTransactionVideoItemUpdateResult != 1) {
                        continue;
                    }

                    $serviceAction = 'article' . str_replace(' ', '', ucwords(str_replace('_', ' ', $qqTransactionArticleItem->article_type_code))) . 'ExecHandler'; // 例:articleMultivideosExecHandler

                    // 企鹅号的内容网站应用的企鹅号的文章事务成功后,基于文章类型调用相应服务进行后续处理
                    QqCwTransactionService::$serviceAction($qqTransactionArticleItem->id);

                } elseif ($qqTransactionStatus == QqTransaction::STATUS_ERROR || ($qqTransactionStatus == QqTransaction::STATUS_SUCCESS && $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_pub_flag'] == HttpQqApiTransaction::ARTICLE_PUB_FLAG_UNPUBLISHED)) { // 状态,2:失败 || (状态,1:成功 && 文章发布状态,未发布)
                    // 更新企鹅号的事务
                    $qqTransactionServiceUpdateResult = $qqTransactionService->update($qqTransactionArticleItem);
                    if ($qqTransactionServiceUpdateResult['status'] === false) {
                        throw new ServerErrorHttpException($qqTransactionServiceUpdateResult['message'], $qqTransactionServiceUpdateResult['code']);
                    }

                    $serviceAction = 'article' . str_replace(' ', '', ucwords(str_replace('_', ' ', $qqTransactionArticleItem->article_type_code))) . 'ErrorHandler'; // 例:articleMultivideosErrorHandler

                    // 企鹅号的内容网站应用的企鹅号的文章事务失败后,基于文章类型调用相应服务进行后续处理
                    QqCwTransactionService::$serviceAction($qqTransactionArticleItem->id);
                }
            }

            // 延缓执行 10 秒
            sleep(Yii::$app->params['qqTransaction']['isEmptyNoSleepTime']);
        } else {
            // 延缓执行 60 秒
            sleep(Yii::$app->params['qqTransaction']['isEmptyYesSleepTime']);
        }

        return ExitCode::OK;
    }

}


47. Implementation of the transaction service for the Penguin content website application, edit \common\services\QqCwTransactionService.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/11/29
 * Time: 13:19
 */
 
namespace common\services;
 
use Yii;
use common\logics\QqCwAppTask;
use common\logics\QqArticle;
use common\logics\QqTransaction;
use common\logics\WxTaskQqCwTaskRelation;
use common\logics\PubLog;
use common\logics\http\qq_api\Transaction as HttpQqApiTransaction;
use yii\helpers\ArrayHelper;
use yii\web\NotFoundHttpException;
use yii\web\UnprocessableEntityHttpException;
 
/**
 * Class QqCwTransactionService
 * @package common\services
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class QqCwTransactionService extends Service
{
    /**
     * 企鹅号的内容网站应用的企鹅号的视频事务成功后的后续处理
     *
     * @param int $qqTransactionId 企鹅号的事务ID
     * 格式如下:1
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public static function videoExecHandler($qqTransactionId)
    {
        // 基于ID查找状态为成功的单个数据模型(企鹅号的事务)
        $qqTransactionVideoSuccessItem = QqTransactionService::findModelSuccessById($qqTransactionId);

        /* 判断企鹅号的应用类型,如果不是,cw:内容网站应用,将抛出 422 HTTP 异常 */
        if ($qqTransactionVideoSuccessItem->qq_app_type !== QqArticle::QQ_APP_TYPE_CW) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35060'), ['id' => $qqTransactionVideoSuccessItem->id])), 35060);
        }

        /* 判断类型,如果不是,2:视频,将抛出 422 HTTP 异常 */
        if ($qqTransactionVideoSuccessItem->type !== $qqTransactionVideoSuccessItem::TYPE_VIDEO) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35061'), ['id' => $qqTransactionVideoSuccessItem->id])), 35061);
        }

        // 基于企鹅号的内容网站应用的任务ID查找状态为启用的单个数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联)
        $wxTaskQqCwTaskRelationItem = WxTaskQqCwTaskRelation::findOneEnabledByQqCwTaskId($qqTransactionVideoSuccessItem->task_id);
        if (isset($wxTaskQqCwTaskRelationItem)) {
            // 基于微信公众帐号应用的任务ID查找状态为启用的资源列表
            $wxTaskQqCwTaskRelationItems = WxTaskQqCwTaskRelation::findAllEnabledByWxTaskId($wxTaskQqCwTaskRelationItem->wx_task_id);

            // 获取微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联的企鹅号的内容网站应用的任务ID值列表
            $wxTaskQqCwTaskRelationQqCwTaskIds = ArrayHelper::getColumn($wxTaskQqCwTaskRelationItems, 'qq_cw_task_id');
            // 基于任务ID查询企鹅号的应用类型,cw:内容网站应用 && 类型,2:视频 && 状态,1:成功的资源列表
            $qqTransactionVideoSuccessItems = QqTransaction::find()->where(['qq_app_type' => QqArticle::QQ_APP_TYPE_CW, 'type' => QqTransaction::TYPE_VIDEO])->andWhere(['in', 'task_id', $wxTaskQqCwTaskRelationQqCwTaskIds])->isDeletedNo()->success()->all();
            // 判断微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联数量和事务数量是否相等,如果相等,则遍历资源列表
            if (count($wxTaskQqCwTaskRelationQqCwTaskIds) == count($qqTransactionVideoSuccessItems)) {
                foreach ($qqTransactionVideoSuccessItems as $qqTransactionVideoSuccessItem) {
                    // 基于ID查找状态为发布中的单个数据模型(企鹅号的内容网站应用的任务)
                    $QqCwAppTaskPublishItem = QqCwAppTaskService::findModelPublishById($qqTransactionVideoSuccessItem->qq_app_task_id);

                    // 企鹅号的内容网站应用的发布文章类型:视频(视频)的文章至企鹅号平台,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步)
                    QqCwArticleService::pubArticleVideoAsync($QqCwAppTaskPublishItem->channel_app_task_id);
                }
            }

        } else {
            // 基于ID查找状态为发布中的单个数据模型(企鹅号的内容网站应用的任务)
            $QqCwAppTaskPublishItem = QqCwAppTaskService::findModelPublishById($qqTransactionVideoSuccessItem->qq_app_task_id);

            // 企鹅号的内容网站应用的发布文章类型:视频(视频)的文章至企鹅号平台,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步)
            QqCwArticleService::pubArticleVideoAsync($QqCwAppTaskPublishItem->channel_app_task_id);
        }
    }

    /**
     * 企鹅号的内容网站应用的企鹅号的视频事务失败后的后续处理
     *
     * @param int $qqTransactionId 企鹅号的事务ID
     * 格式如下:1
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public static function videoErrorHandler($qqTransactionId)
    {
        // 基于ID查找状态为失败的单个数据模型(企鹅号的事务)
        $qqTransactionVideoErrorItem = QqTransactionService::findModelErrorById($qqTransactionId);

        /* 判断企鹅号的应用类型,如果不是,cw:内容网站应用,将抛出 422 HTTP 异常 */
        if ($qqTransactionVideoErrorItem->qq_app_type !== QqArticle::QQ_APP_TYPE_CW) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35060'), ['id' => $qqTransactionVideoErrorItem->id])), 35060);
        }

        /* 判断类型,如果不是,2:视频,将抛出 422 HTTP 异常 */
        if ($qqTransactionVideoErrorItem->type !== $qqTransactionVideoErrorItem::TYPE_VIDEO) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35061'), ['id' => $qqTransactionVideoErrorItem->id])), 35061);
        }

        // 基于ID查找状态为发布中的单个数据模型(企鹅号的内容网站应用的任务)
        $QqCwAppTaskPublishItem = QqCwAppTaskService::findModelPublishById($qqTransactionVideoErrorItem->qq_app_task_id);

        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($QqCwAppTaskPublishItem->channel_app_task_id);

        // 基于渠道的应用的任务ID更新企鹅号的内容网站应用的任务(场景:企鹅号的视频事务失败后,可发布次数减1,状态,3:发布中(已失败))
        QqCwAppTask::updatePublishErrorByChannelAppTaskId($channelAppTaskEnabledItem->id);

        $channelAppTaskEnabledItems[] = $channelAppTaskEnabledItem;

        $pubLogDatas = [];
        foreach ($channelAppTaskEnabledItems as $key => $channelAppTaskEnabledItem) {
            // 基于渠道的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的任务)
            $qqCwAppTaskItem = QqCwAppTaskService::findModelByChannelAppTaskId($channelAppTaskEnabledItem->id);
            $pubLogDatas[$key] = [
                'channel_app_source_uuid' => $channelAppTaskEnabledItem->channel_app_source_uuid,
                'channel_app_task_uuid' => $channelAppTaskEnabledItem->uuid,
                'channel_code' => $channelAppTaskEnabledItem->channel_code,
                'channel_type_code' => $channelAppTaskEnabledItem->channel_type_code,
                'channel_app_task_status' => $qqCwAppTaskItem->status,
            ];
        }

        $errorCode = 35088;
        $errorMessage = Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35088'), ['error_message' => $qqTransactionVideoErrorItem->transaction_err_msg]));

        // 发布任务失败后,插入发布日志,将作业推送至来源回调队列(异步)
        SourceCallbackService::sourceCallbackAsync($channelAppTaskEnabledItems, $errorCode, $errorMessage, $pubLogDatas, PubLog::STATUS_ERROR);

        // 基于企鹅号的内容网站应用的任务ID查找状态为启用的单个资源(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联)
        $wxTaskQqCwTaskRelationItem = WxTaskQqCwTaskRelation::findOneEnabledByQqCwTaskId($qqTransactionVideoErrorItem->task_id);

        if (isset($wxTaskQqCwTaskRelationItem)) {
            // file_put_contents('E:/wwwroot/channel-pub-api/qq/runtime/video-error-handler-' . $qqTransactionVideoErrorItem->id . '-' . $wxTaskQqCwTaskRelationItem->wx_task_id . '-' . time() . '.txt', '');
            WxAssetService::qqCwTransactionVideoErrorHandler($wxTaskQqCwTaskRelationItem->wx_task_id, $errorCode, $errorMessage);
        }
    }

    /**
     * 企鹅号的内容网站应用的企鹅号的视频文章事务成功后的后续处理
     *
     * @param int $qqTransactionId 企鹅号的事务ID
     * 格式如下:1
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public static function articleMultivideosExecHandler($qqTransactionId)
    {
        // 基于ID查找状态为成功的单个数据模型(企鹅号的事务)
        $qqTransactionArticleSuccessItem = QqTransactionService::findModelSuccessById($qqTransactionId);

        /* 判断企鹅号的应用类型,如果不是,cw:内容网站应用,将抛出 422 HTTP 异常 */
        if ($qqTransactionArticleSuccessItem->qq_app_type !== QqArticle::QQ_APP_TYPE_CW) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35060'), ['id' => $qqTransactionArticleSuccessItem->id])), 35060);
        }

        /* 判断类型,如果不是,1:文章,将抛出 422 HTTP 异常 */
        if ($qqTransactionArticleSuccessItem->type !== $qqTransactionArticleSuccessItem::TYPE_ARTICLE) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35061'), ['id' => $qqTransactionArticleSuccessItem->id])), 35061);
        }

        // 基于ID查找状态为审核中的单个数据模型(企鹅号的内容网站应用的任务)
        $QqCwAppTaskPublishItem = QqCwAppTaskService::findModelReviewById($qqTransactionArticleSuccessItem->qq_app_task_id);

        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($QqCwAppTaskPublishItem->channel_app_task_id);

        // 基于渠道的应用的任务ID更新企鹅号的内容网站应用的任务(场景:企鹅号的文章事务成功后,可发布次数减1,状态,6:已发布)
        QqCwAppTask::updatePublishedByChannelAppTaskId($channelAppTaskEnabledItem->id);

        $channelAppTaskEnabledItems[] = $channelAppTaskEnabledItem;

        $pubLogDatas = [];
        foreach ($channelAppTaskEnabledItems as $key => $channelAppTaskEnabledItem) {
            // 基于渠道的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的任务)
            $qqCwAppTaskItem = QqCwAppTaskService::findModelByChannelAppTaskId($channelAppTaskEnabledItem->id);
            $pubLogDatas[$key] = [
                'channel_app_source_uuid' => $channelAppTaskEnabledItem->channel_app_source_uuid,
                'channel_app_task_uuid' => $channelAppTaskEnabledItem->uuid,
                'channel_code' => $channelAppTaskEnabledItem->channel_code,
                'channel_type_code' => $channelAppTaskEnabledItem->channel_type_code,
                'channel_app_task_status' => $qqCwAppTaskItem->status,
            ];
        }

        $successCode = 25002;
        $successMessage = Yii::t('common/success', 25002);

        // 发布任务成功后,插入发布日志,将作业推送至来源回调队列(异步)
        SourceCallbackService::sourceCallbackAsync($channelAppTaskEnabledItems, $successCode, $successMessage, $pubLogDatas, PubLog::STATUS_SUCCESS);

        // 基于企鹅号的内容网站应用的任务ID查找状态为启用的单个数据模型(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联)
        $wxTaskQqCwTaskRelationItem = WxTaskQqCwTaskRelation::findOneEnabledByQqCwTaskId($qqTransactionArticleSuccessItem->task_id);
        if (isset($wxTaskQqCwTaskRelationItem)) {
            // 基于微信公众帐号应用的任务ID查找状态为启用的资源列表
            $wxTaskQqCwTaskRelationItems = WxTaskQqCwTaskRelation::findAllEnabledByWxTaskId($wxTaskQqCwTaskRelationItem->wx_task_id);

            // 获取微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联的企鹅号的内容网站应用的任务ID值列表
            $wxTaskQqCwTaskRelationQqCwTaskIds = ArrayHelper::getColumn($wxTaskQqCwTaskRelationItems, 'qq_cw_task_id');
            // 基于任务ID查询企鹅号的应用类型,cw:内容网站应用 && 类型,1:文章 && 状态,1:成功 && 文章发布状态,发布成功的资源列表
            $qqTransactionArticleSuccessItems = QqTransaction::find()->where(['qq_app_type' => QqArticle::QQ_APP_TYPE_CW, 'type' => QqTransaction::TYPE_ARTICLE, 'article_pub_flag' => HttpQqApiTransaction::ARTICLE_PUB_FLAG_PUBLISHED])->andWhere(['in', 'task_id', $wxTaskQqCwTaskRelationQqCwTaskIds])->isDeletedNo()->success()->all();
            // 判断微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联数量和事务数量是否相等,如果相等,则遍历资源列表
            if (count($wxTaskQqCwTaskRelationQqCwTaskIds) == count($qqTransactionArticleSuccessItems)) {
                // file_put_contents('E:/wwwroot/channel-pub-api/qq/runtime/article-multivideos-exec-handler-' . $qqTransactionArticleSuccessItem->id . '-' . $wxTaskQqCwTaskRelationItem->wx_task_id . '-' . time() . '.txt', '');
                WxArticleService::qqCwTransactionArticleVideoExecHandler($wxTaskQqCwTaskRelationItem->wx_task_id);
            }

        }
    }

    /**
     * 企鹅号的内容网站应用的企鹅号的视频文章事务失败后的后续处理
     *
     * @param int $qqTransactionId 企鹅号的事务ID
     * 格式如下:1
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \yii\base\Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public static function articleMultivideosErrorHandler($qqTransactionId)
    {
        // 基于ID查找状态为失败的单个数据模型(企鹅号的事务)
        $qqTransactionArticleErrorItem = QqTransactionService::findModelErrorById($qqTransactionId);

        /* 判断企鹅号的应用类型,如果不是,cw:内容网站应用,将抛出 422 HTTP 异常 */
        if ($qqTransactionArticleErrorItem->qq_app_type !== QqArticle::QQ_APP_TYPE_CW) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35060'), ['id' => $qqTransactionArticleErrorItem->id])), 35060);
        }

        /* 判断类型,如果不是,1:文章,将抛出 422 HTTP 异常 */
        if ($qqTransactionArticleErrorItem->type !== $qqTransactionArticleErrorItem::TYPE_ARTICLE) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35061'), ['id' => $qqTransactionArticleErrorItem->id])), 35061);
        }

        // 基于ID查找状态为审核中的单个数据模型(企鹅号的内容网站应用的任务)
        $QqCwAppTaskPublishItem = QqCwAppTaskService::findModelReviewById($qqTransactionArticleErrorItem->qq_app_task_id);

        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($QqCwAppTaskPublishItem->channel_app_task_id);

        // 基于渠道的应用的任务ID更新企鹅号的内容网站应用的任务(场景:企鹅号的文章事务失败后,可发布次数减1,状态,5:未发布)
        QqCwAppTask::updateUnpublishedByChannelAppTaskId($channelAppTaskEnabledItem->id);

        $channelAppTaskEnabledItems[] = $channelAppTaskEnabledItem;

        $pubLogDatas = [];
        foreach ($channelAppTaskEnabledItems as $key => $channelAppTaskEnabledItem) {
            // 基于渠道的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的任务)
            $qqCwAppTaskItem = QqCwAppTaskService::findModelByChannelAppTaskId($channelAppTaskEnabledItem->id);
            $pubLogDatas[$key] = [
                'channel_app_source_uuid' => $channelAppTaskEnabledItem->channel_app_source_uuid,
                'channel_app_task_uuid' => $channelAppTaskEnabledItem->uuid,
                'channel_code' => $channelAppTaskEnabledItem->channel_code,
                'channel_type_code' => $channelAppTaskEnabledItem->channel_type_code,
                'channel_app_task_status' => $qqCwAppTaskItem->status,
            ];
        }

        $errorCode = 35095;
        $errorMessage = Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35095'), ['error_message' => $qqTransactionArticleErrorItem->transaction_err_msg]));

        // 发布任务失败后,插入发布日志,将作业推送至来源回调队列(异步)
        SourceCallbackService::sourceCallbackAsync($channelAppTaskEnabledItems, $errorCode, $errorMessage, $pubLogDatas, PubLog::STATUS_ERROR);

        // 基于企鹅号的内容网站应用的任务ID查找状态为启用的单个资源(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联)
        $wxTaskQqCwTaskRelationItem = WxTaskQqCwTaskRelation::findOneEnabledByQqCwTaskId($qqTransactionArticleErrorItem->task_id);

        if (isset($wxTaskQqCwTaskRelationItem)) {
            // file_put_contents('E:/wwwroot/channel-pub-api/qq/runtime/article-multivideos-error-handler-' . $qqTransactionArticleErrorItem->id . '-' . $wxTaskQqCwTaskRelationItem->wx_task_id . '-' . time() . '.txt', '');
            WxArticleService::qqCwTransactionArticleVideoErrorHandler($wxTaskQqCwTaskRelationItem->wx_task_id, $errorCode, $errorMessage);
        }
    }

}

48. Queue job: publish article, edit \common\jobs\PubArticleJob.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: terryhong
 * Date: 2018/9/7
 * Time: 下午5:11
 */
 
namespace common\jobs;
 
use Yii;
use common\services\TaskService;
use common\services\ChannelAppTaskService;
use common\services\ArticleTypeService;
use yii\web\NotFoundHttpException;
use yii\web\UnprocessableEntityHttpException;
 
/**
 * 发布文章
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class PubArticleJob extends Job
{
    public $channelAppTaskId;

    /*
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws ServerErrorHttpException 如果基于任务ID查找状态为启用的资源列表为空,将抛出 500 HTTP 异常
     */
    public function execute($queue)
    {
        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($this->channelAppTaskId);

        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($channelAppTaskEnabledItem->task_id);

        $serviceClass = 'common\services\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $taskEnabledItem->channel_type_code))) . 'ArticleService'; // 例:common\services\QqCwArticleService

        $articleModel = 'common\logics\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $taskEnabledItem->channel_code))) . 'Article'; // 例:common\logics\QqArticle

        // 基于任务ID查找单个数据模型(渠道的文章)
        $articleModelItem = $articleModel::find()->where(['task_id' => $taskEnabledItem->id])->isDeletedNo()->one();

        // 如果未找到数据模型,将抛出 404 HTTP 异常
        if (!isset($articleModelItem)) {
            throw new NotFoundHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35091'), ['task_id' => $taskEnabledItem->id])), 35091);
        }

        /* 判断状态,如果未启用,将抛出 422 HTTP 异常 */
        if ($articleModelItem->status !== $articleModelItem::STATUS_ENABLED) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35092'), ['task_id' => $taskEnabledItem->id])), 35092);
        }

        // 基于ID查找状态为启用的单个数据模型(文章类型)
        $articleTypeEnabledItem = ArticleTypeService::findModelEnabledById($articleModelItem->article_type_id);

        $serviceAction = 'pubArticle' . str_replace(' ', '', ucwords(str_replace('_', ' ', $articleTypeEnabledItem->code))) . 'Sync'; // 例:pubArticleVideoSync

        $serviceClass::$serviceAction($this->channelAppTaskId);

    }

}

49. Queue event handler (after each successful job execution): call the corresponding service (after the job executes successfully) for subsequent processing. If calling the corresponding service fails (i.e., the service throws an exception), only insert a system log. Queue event handler (when an uncaught exception occurs during job execution): call the corresponding service (after the job execution fails) for subsequent processing. If calling the corresponding service fails (i.e., the service throws an exception), only insert a system log. Edit \common\components\queue\PubArticleEventHandler.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/10/23
 * Time: 19:36
 */
 
namespace common\components\queue;
 
use Yii;
use common\services\TaskService;
use common\services\ChannelAppTaskService;
use common\services\ArticleTypeService;
use yii\base\Component;
use yii\queue\ExecEvent;
use yii\web\NotFoundHttpException;
use yii\web\UnprocessableEntityHttpException;
 
/**
 * Class PubArticleEventHandler
 * @package common\components\queue
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class PubArticleEventHandler extends Component
{
    /**
     * @param ExecEvent $event
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     */
    public static function afterExec(ExecEvent $event)
    {
        $channelAppTaskId = $event->job->channelAppTaskId;

        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($channelAppTaskId);

        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($channelAppTaskEnabledItem->task_id);

        $serviceClass = 'common\services\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $taskEnabledItem->channel_type_code))) . 'ArticleService'; // 例:common\services\QqCwArticleService

        $articleModel = 'common\logics\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $taskEnabledItem->channel_code))) . 'Article'; // 例:common\logics\QqArticle

        // 基于任务ID查找单个数据模型(渠道的文章)
        $articleModelItem = $articleModel::find()->where(['task_id' => $taskEnabledItem->id])->isDeletedNo()->one();

        // 如果未找到数据模型,将抛出 404 HTTP 异常
        if (!isset($articleModelItem)) {
            throw new NotFoundHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35091'), ['task_id' => $taskEnabledItem->id])), 35091);
        }

        /* 判断状态,如果未启用,将抛出 422 HTTP 异常 */
        if ($articleModelItem->status !== $articleModelItem::STATUS_ENABLED) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35092'), ['task_id' => $taskEnabledItem->id])), 35092);
        }

        // 基于ID查找状态为启用的单个数据模型(文章类型)
        $articleTypeEnabledItem = ArticleTypeService::findModelEnabledById($articleModelItem->article_type_id);

        $serviceAction = 'pubArticle' . str_replace(' ', '', ucwords(str_replace('_', ' ', $articleTypeEnabledItem->code))) . 'ExecHandler'; // 例:pubArticleVideoExecHandler

        $serviceClass::$serviceAction($channelAppTaskId);
    }

    /**
     * @param ExecEvent $event
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     */
    public static function afterError(ExecEvent $event)
    {
        $channelAppTaskId = $event->job->channelAppTaskId;

        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($channelAppTaskId);

        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($channelAppTaskEnabledItem->task_id);

        $serviceClass = 'common\services\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $taskEnabledItem->channel_type_code))) . 'ArticleService'; // 例:common\services\QqCwArticleService

        $articleModel = 'common\logics\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $taskEnabledItem->channel_code))) . 'Article'; // 例:common\logics\QqArticle

        // 基于任务ID查找单个数据模型(渠道的文章)
        $articleModelItem = $articleModel::find()->where(['task_id' => $taskEnabledItem->id])->isDeletedNo()->one();

        // 如果未找到数据模型,将抛出 404 HTTP 异常
        if (!isset($articleModelItem)) {
            throw new NotFoundHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35091'), ['task_id' => $taskEnabledItem->id])), 35091);
        }

        /* 判断状态,如果未启用,将抛出 422 HTTP 异常 */
        if ($articleModelItem->status !== $articleModelItem::STATUS_ENABLED) {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35092'), ['task_id' => $taskEnabledItem->id])), 35092);
        }

        // 基于ID查找状态为启用的单个数据模型(文章类型)
        $articleTypeEnabledItem = ArticleTypeService::findModelEnabledById($articleModelItem->article_type_id);

        $serviceAction = 'pubArticle' . str_replace(' ', '', ucwords(str_replace('_', ' ', $articleTypeEnabledItem->code))) . 'ErrorHandler'; // 例:pubArticleVideoErrorHandler

        $serviceClass::$serviceAction($channelAppTaskId, $event->error);
    }
}

50. Implementation of the article service for the Penguin content website application, edit \common\services\QqCwArticleService.php

PHP
<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/11/10
 * Time: 19:47
 */
 
namespace common\services;
 
use Yii;
use common\logics\Channel;
use common\logics\QqArticleVideoCreateParam;
use common\logics\ChannelType;
use common\logics\Task;
use common\logics\QqCwAppTask;
use common\logics\ArticleType;
use common\logics\QqArticleType;
use common\logics\ArticleCategory;
use common\logics\QqArticle;
use common\logics\QqArticleMultivideos;
use common\logics\WxTaskQqCwTaskRelation;
use common\logics\QqTransaction;
use common\logics\PubLog;
use common\logics\http\qq_api\Article as HttpQqApiArticle;
use common\logics\http\qq_api\Transaction as HttpQqApiTransaction;
use common\jobs\PubArticleJob;
use yii\web\NotFoundHttpException;
use yii\web\ServerErrorHttpException;
use yii\web\UnprocessableEntityHttpException;
 
class QqCwArticleService extends Service
{
    /**
     * 发布文章队列的作业执行成功后的后续处理
     *
     * @param int $channelAppTaskId 渠道的应用的任务ID
     * 格式如下:4
     *
     */
    public static function pubArticleVideoExecHandler($channelAppTaskId)
    {
    }
 
    /**
     * 发布文章队列在作业执行失败后的后续处理
     *
     * @param int $channelAppTaskId 渠道的应用的任务ID
     * 格式如下:4
     *
     * @param object $eventError 事件错误
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws ServerErrorHttpException
     * @throws \Throwable
     */
    public static function pubArticleVideoErrorHandler($channelAppTaskId, $eventError)
    {
        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($channelAppTaskId);
 
        // 基于渠道的应用的任务ID更新企鹅号的内容网站应用的任务(场景:发布文章队列的作业执行失败后,可发布次数减1,状态,3:发布中(已失败))
        QqCwAppTask::updatePublishErrorByChannelAppTaskId($channelAppTaskEnabledItem->id);
 
        $channelAppTaskEnabledItems[] = $channelAppTaskEnabledItem;
 
        $pubLogDatas = [];
        foreach ($channelAppTaskEnabledItems as $key => $channelAppTaskEnabledItem) {
            // 基于渠道的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的任务)
            $qqCwAppTaskItem = QqCwAppTaskService::findModelByChannelAppTaskId($channelAppTaskEnabledItem->id);
            $pubLogDatas[$key] = [
                'channel_app_source_uuid' => $channelAppTaskEnabledItem->channel_app_source_uuid,
                'channel_app_task_uuid' => $channelAppTaskEnabledItem->uuid,
                'channel_code' => $channelAppTaskEnabledItem->channel_code,
                'channel_type_code' => $channelAppTaskEnabledItem->channel_type_code,
                'channel_app_task_status' => $qqCwAppTaskItem->status,
            ];
        }
 
        // 发布任务失败后,插入发布日志,将作业推送至来源回调队列(异步)
        SourceCallbackService::sourceCallbackAsync($channelAppTaskEnabledItems, $eventError->getCode(), $eventError->getMessage(), $pubLogDatas, PubLog::STATUS_ERROR);
 
        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($channelAppTaskEnabledItem->task_id);
 
        // 基于企鹅号的内容网站应用的任务ID查找状态为启用的单个资源(微信公众帐号应用的任务与企鹅号的内容网站应用的任务的关联)
        $wxTaskQqCwTaskRelationItem = WxTaskQqCwTaskRelation::findOneEnabledByQqCwTaskId($taskEnabledItem->id);
 
        if (isset($wxTaskQqCwTaskRelationItem)) {
            WxArticleService::qqCwPubArticleVideoErrorHandler($wxTaskQqCwTaskRelationItem->wx_task_id, $eventError);
        }
 
    }
 
    /**
     * 企鹅号的内容网站应用的发布文章类型:视频(视频)的文章至企鹅号平台,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步)
     *
     * @param int $channelAppTaskId 渠道的应用的任务ID
     * 格式如下:37
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     */
    public static function pubArticleVideoAsync($channelAppTaskId)
    {
        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($channelAppTaskId);
 
        // 将任务发送到队列,通过标准工作人员进行处理
        Yii::$app->pubArticleQueue->push(new PubArticleJob([
            'channelAppTaskId' => $channelAppTaskEnabledItem->id,
        ]));
    }
 
    /**
     * 企鹅号的内容网站应用的发布文章类型:视频(视频)的文章至企鹅号平台(同步)
     *
     * @param int $channelAppTaskId 渠道的应用的任务ID
     * 格式如下:37
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws ServerErrorHttpException
     * @throws \Throwable
     */
    public static function pubArticleVideoSync($channelAppTaskId)
    {
        // 基于ID查找状态为启用的单个数据模型(渠道的应用的任务)
        $channelAppTaskEnabledItem = ChannelAppTaskService::findModelEnabledById($channelAppTaskId);
 
        // 基于ID查找状态为启用的单个数据模型(任务)
        $taskEnabledItem = TaskService::findModelEnabledById($channelAppTaskEnabledItem->task_id);
 
        // 基于任务ID查找状态为启用的单个数据模型(企鹅号的文章)
        $qqArticleEnabledItem = QqArticleService::findModelEnabledByTaskId($taskEnabledItem->id);
 
        // 基于企鹅号的文章ID查找状态为启用的单个数据模型(企鹅号的文章类型(视频)的文章)
        $qqArticleMultivideosEnabledItem = QqArticleMultivideosService::findModelEnabledByQqArticleId($qqArticleEnabledItem->id);
 
        // 基于渠道的应用的任务ID查找状态为发布中的单个数据模型(企鹅号的内容网站应用的任务)
        $qqCwAppTaskItem = QqCwAppTaskService::findModelPublishByChannelAppTaskId($channelAppTaskId);
 
        // 基于ID查找状态为启用的单个数据模型(企鹅号的内容网站应用)
        $qqCwAppEnabledItem = QqCwAppService::findModelEnabledById($qqCwAppTaskItem->qq_cw_app_id);
 
        // 基于企鹅号的内容网站应用ID获取有效的 Access Token
        $qqCwAccessTokenService = new QqCwAccessTokenService();
        $accessTokenValidity = $qqCwAccessTokenService->getAccessTokenValidityByQqCwAppId($qqCwAppEnabledItem->id);
 
        // 基于企鹅号的应用类型、类型、企鹅号的应用的任务ID查找状态为成功的单个数据模型(企鹅号的事务)
        $qqTransactionSuccessItem = QqTransactionService::findModelSuccessByQqAppTypeAndTypeAndQqAppTaskId(QqArticle::QQ_APP_TYPE_CW, QqTransaction::TYPE_VIDEO, $qqCwAppTaskItem->id);
 
        // 基于企鹅号的应用的视频文件分片上传ID查找状态为已上传的单个数据模型(企鹅号的内容网站应用的视频文件分片上传)
        $qqCwVideoMultipartUploadUploadedItem = QqVideoMultipartUploadService::findModelUploadedById($qqTransactionSuccessItem->qq_video_multipart_upload_id);
 
        // 子字符串替换,将 ,替换为 空格
        $tag = str_replace(',', ' ', $qqArticleMultivideosEnabledItem->tag);
 
        // HTTP请求,基于企鹅号的内容网站应用的视频文件唯一标示ID发布视频文章
        $httpPubVideoData = [
            'accessToken' => $accessTokenValidity->access_token,
            'title' => $qqArticleEnabledItem->title,
            'tag' => $tag,
            'category' => $qqArticleMultivideosEnabledItem->category,
            'desc' => $qqArticleMultivideosEnabledItem->desc,
            'vid' => $qqCwVideoMultipartUploadUploadedItem->vid,
            'apply' => $qqArticleMultivideosEnabledItem->apply,
        ];
        $pubVideoData = static::httpPubVideo($httpPubVideoData);
        // $pubVideoData = ['transaction_id' => 780935222316202025];
 
        // HTTP请求,基于视频文章的唯一事务ID获取事务信息
        $qqTransactionService = new QqTransactionService();
        $qqTransactionServiceHttpTransactionInfoData = [
            'accessToken' => $accessTokenValidity->access_token,
            'transactionId' => $pubVideoData['transaction_id'],
        ];
        $qqTransactionServiceHttpTransactionInfoResult = $qqTransactionService->httpTransactionInfo($qqTransactionServiceHttpTransactionInfoData);
 
        // 创建企鹅号的事务
        $qqTransaction = new QqTransaction();
        $qqTransaction->attributes = [
            'group_id' => $taskEnabledItem->group_id,
            'qq_app_task_id' => $qqCwVideoMultipartUploadUploadedItem->qq_app_task_id,
            'qq_app_id' => $qqCwVideoMultipartUploadUploadedItem->qq_app_id,
            'qq_app_type' => QqArticle::QQ_APP_TYPE_CW,
            'qq_article_id' => $qqArticleEnabledItem->id,
            'qq_video_multipart_upload_id' => 0,
            'transaction_id' => (string) $pubVideoData['transaction_id'],
            'type' => $qqTransactionService::getTypeByHttpQqApiTransactionType($qqTransactionServiceHttpTransactionInfoResult['transaction_type']),
            'transaction_ctime' => $qqTransactionServiceHttpTransactionInfoResult['transaction_ctime'],
            'ext_err' => $qqTransactionServiceHttpTransactionInfoResult['ext_err'],
            'transaction_err_msg' => $qqTransactionServiceHttpTransactionInfoResult['transaction_err_msg'],
            'article_abstract' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_abstract'],
            'article_type' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_type'],
            'article_type_code' => QqArticleType::CODE_MULTIVIDEOS,
            'article_url' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_url'],
            'article_imgurl' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_imgurl'],
            'article_title' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_title'],
            'article_pub_flag' => HttpQqApiTransaction::ARTICLE_PUB_FLAG_REVIEW,
            'article_pub_time' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_pub_time'],
            'article_video_title' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['title'],
            'article_video_desc' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['desc'],
            'article_video_type' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['type'],
            'article_video_vid' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['vid'],
            'task_id' => $taskEnabledItem->id,
            'status' => QqTransaction::STATUS_PROCESSING,
        ];
        $qqTransactionServiceCreateResult = $qqTransactionService->create($qqTransaction);
        if ($qqTransactionServiceCreateResult['status'] === false) {
            throw new ServerErrorHttpException($qqTransactionServiceCreateResult['message'], $qqTransactionServiceCreateResult['code']);
        }
 
        // 更新企鹅号的内容网站应用的任务状态,4:审核中
        $qqCwAppTaskService = new QqCwAppTaskService();
        $qqCwAppTaskItem->status = QqCwAppTask::STATUS_REVIEW;
        $qqCwAppTaskServiceUpdateResult = $qqCwAppTaskService->update($qqCwAppTaskItem);
        if ($qqCwAppTaskServiceUpdateResult['status'] === false) {
            throw new ServerErrorHttpException($qqCwAppTaskServiceUpdateResult['message'], $qqCwAppTaskServiceUpdateResult['code']);
        }
    }
 
    /**
     * HTTP请求,基于企鹅号的内容网站应用的视频文件唯一标示ID发布视频文章
     * @param array $data 数据
     * 格式如下:
     *
     * [
     *     'accessToken' => 'QVBII-BJMBCYQZ9XYU3OAQ', // 企鹅平台企鹅号应用授权调用凭据
     *     'title' => '标题 - 20180901 - 1', // 视频文章标题
     *     'tag' => '北上广深 租金 上涨', // 视频文章标签
     *     'category' => 100, // 视频分类,即企鹅号的文章类型(视频)的文章分类ID
     *     'desc' => '视频描述', // 视频描述
     *     'vid' => 'j0789dzdjut', // 视频文件唯一标示ID
     *     'apply' => 0, // 是否申请原创文章,0:否;1:是(需要用户具有发表视频原创文章资格否则无效)
     * ]
     *
     * @return array
     * 格式如下:
     *
     * [
     *     'transaction_id' => 780930255958621794, // 视频文章的唯一事务ID
     * ]
     *
     * @throws ServerErrorHttpException
     */
    public static function httpPubVideo($data)
    {
        /* HTTP请求,基于企鹅号的内容网站应用的视频文件唯一标示ID发布视频文章 */
        $httpQqApiArticle = new HttpQqApiArticle();
        $pubVideo = $httpQqApiArticle->clientPubVideo($data);
 
        if ($pubVideo === false) {
            if ($httpQqApiArticle->hasErrors()) {
                foreach ($httpQqApiArticle->getFirstErrors() as $message) {
                    $firstErrors = $message;
                    break;
                }
                throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35042'), ['firstErrors' => $firstErrors])), 35042);
            } elseif (!$httpQqApiArticle->hasErrors()) {
                throw new ServerErrorHttpException('Penguin\'s content website application api HTTP requests fail for unknown reasons.');
            }
        }
 
        return $pubVideo['data'];
    }
 
    /**
     * 发布视频(视频)的文章至渠道发布(提供给其他渠道使用),输入数据验证规则可参考:发布文章类型:视频(视频)的文章至渠道发布  /articles/video(article/video-create)
     *
     * @param array $data 数据
     * 格式如下:
     *
     * [
     *     'source' => 'spider', // 来源,xContent:内容库;vms:视频管理系统;cms:内容管理系统;spider:自媒体;channel-pub-api:渠道发布接口
     *     'source_uuid' => '825e6d5e36468cc4bf536799ce3565cf', // 来源ID(UUID)
     *     'source_pub_user_id' => 1, // 来源发布用户ID
     *     'source_callback_url' => 'source_callback_url', // 来源回调地址
     *     'article_category_id' => 1, // 文章分类ID
     *     'title' => '标题 - 20180901 - 1', // 标题
     *     'author' => '作者 - 20180901 - 1', // 作者
     *     'source_article_id' => 1, // 来源文章ID
     *     'media_absolute_url' => 'http://127.0.0.1/channel-pub-api/storage/spider/videos/7月份北上广深等十大城市租金环比上涨 看东方 20180820 高清_高清.mp4', // 视频文件的绝对URL
     *     'tag' => '北上广深,租金,上涨', // 视频文章标签,以英文半角逗号分隔,最多5个,每个标签最多8个字
     *     'apply' => 0, // 是否申请原创文章,0:否;1:是(需要用户具有发表视频原创文章资格否则无效)
     *     'desc' => '视频描述', // 视频描述
     *     'group_id' => 'spider', // 租户ID
     * ]
     *
     * @param string $channelTypeCode 渠道的类型代码
     *
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => true, // 成功
     *     'code' => 10000, // 返回码
     *     'message' => '发布文章类型:视频(视频)的文章成功', // 说明
     *     'data' => [ // array
     *         [ // object
     *             'channel_id' => 1, // 渠道ID
     *             'channel_code' => 'qq', // 渠道代码,qq:企鹅号;wx:微信公众帐号
     *             'channel_type_id' => 1, // 渠道的类型ID
     *             'channel_type_code' => 'qq_cw', // 渠道的类型代码,qq_cw:企鹅号的内容网站应用;qq_tp:企鹅号的第三方服务平台应用;wx:微信公众帐号应用
     *             'channel_app_source_id' => 1, // 渠道的应用的来源ID
     *             'channel_app_source_uuid' => 'a3f87610e17011e88f0154ee75d2ebc1', // 渠道的应用的来源ID(UUID)
     *             'task_id' => 8, // 任务ID
     *             'status' => 1, // 状态,0:禁用;1:启用
     *             'created_at' => 1541730602, // 创建时间
     *             'updated_at' => 1541730602, // 更新时间
     *             'uuid' => '5ce1f7f2e3c711e8bc2354ee75d2ebc1', // 渠道的应用的任务ID(UUID)
     *             'id' => 13, // ID
     *         ],
     *     ]
     * ]
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 35024, // 返回码
     *     'message' => '文章类型代码:video,的状态为未启用', // 说明
     * ]
     *
     * @throws UnprocessableEntityHttpException
     * @throws ServerErrorHttpException
     * @throws \Throwable
     */
    public function videoCreate($data, $channelTypeCode)
    {
        // 判断渠道的类型代码
        if ($channelTypeCode == ChannelType::CODE_WX) {
            // 基于 Client ID 查找状态为启用的单个数据模型(企鹅号的内容网站应用)
            $qqCwAppEnabledItem = QqCwAppService::findModelEnabledByClientId(Yii::$app->params['qqAuth']['cwApp']['clientId']);
        } else {
            // 基于 Client ID 查找状态为启用的单个数据模型(企鹅号的内容网站应用)
            $qqCwAppEnabledItem = QqCwAppService::findModelEnabledByClientId(Yii::$app->params['qqAuth']['cwApp']['clientId']);
        }
 
        $data['channel_app_source_uuids'] = [$qqCwAppEnabledItem->channel_app_source_uuid];
 
        // 基于代码查找状态为启用的单个数据模型(渠道)
        $channelEnabledItem = ChannelService::findModelEnabledByCode(Channel::CODE_QQ);
 
        /* 视频(视频)的文章发布参数 */
        $qqArticleVideoCreateParam = new QqArticleVideoCreateParam();
        // 把请求数据填充到模型中
        if (!$qqArticleVideoCreateParam->load($data, '')) {
            return ['status' => false, 'code' =>35073, 'message' => Yii::t('common/error', '35073')];
        }
        // 验证模型
        if (!$qqArticleVideoCreateParam->validate()) {
            $qqArticleVideoCreateParamResult = self::handleValidateError($qqArticleVideoCreateParam);
            if ($qqArticleVideoCreateParamResult['status'] === false) {
                return ['status' => false, 'code' =>$qqArticleVideoCreateParamResult['code'], 'message' => $qqArticleVideoCreateParamResult['message']];
            }
        }
 
        /* 基于文章类型代码定义场景 */
        $scenario = QqArticle::SCENARIO_VIDEO_CREATE;
 
        /* 实例化多个模型 */
 
        // 渠道的应用的来源
        if (!is_array($data['channel_app_source_uuids'])) {
            return ['status' => false, 'code' =>35073, 'message' => Yii::t('common/error', '35073')];
        }
        // 基于多个UUID返回状态为启用,且渠道的类型代码必须一致的数据模型(渠道的应用的来源)列表
        $channelAppSourceEnabledItems = ChannelAppSourceService::findModelsEnabledByUuids($data['channel_app_source_uuids']);
 
        // 获取、判断渠道的类型代码,获取应用的数据模型列表
        $channelTypeCode = $channelAppSourceEnabledItems[$data['channel_app_source_uuids'][0]]->channel_type_code;
        if ($channelTypeCode == ChannelType::CODE_QQ_CW) {
            // 基于代码查找状态为启用的单个数据模型(渠道的类型)
            $channelTypeEnabledItem = ChannelTypeService::findModelEnabledByCode(ChannelType::CODE_QQ_CW);
            // 基于多个UUID返回状态为启用的数据模型(企鹅号的内容网站应用)列表
            $qqAppEnabledItems = QqCwAppService::findModelsEnabledByChannelAppSourceUuids($data['channel_app_source_uuids']);
        } else {
            throw new UnprocessableEntityHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35074'), ['channel_type_code' => $channelTypeCode])), 35074);
        }
 
        // 任务
        $task = new Task([
            'scenario' => Task::SCENARIO_CREATE,
        ]);
        // 转换视频(视频)的文章发布参数,多模型的填充、验证的实现
        $data[$task->formName()] = [
            'group_id' => $data['group_id'],
            'source' => $qqArticleVideoCreateParam->source,
            'source_uuid' => $qqArticleVideoCreateParam->source_uuid,
            'source_pub_user_id' => $qqArticleVideoCreateParam->source_pub_user_id,
            'source_callback_url' => $qqArticleVideoCreateParam->source_callback_url,
        ];
        $taskResult = self::handleLoadAndValidate($task, $data);
        if ($taskResult['status'] === false) {
            return ['status' => false, 'code' =>$taskResult['code'], 'message' => $taskResult['message']];
        }
 
        // 基于代码查找状态为启用的单个数据模型(文章类型)
        $articleTypeEnabledItem = ArticleTypeService::findModelEnabledByCode(ArticleType::CODE_VIDEO);
 
        // 基于代码查找状态为启用的单个数据模型(企鹅号的文章类型)
        $qqArticleTypeEnabledItem = QqArticleTypeService::findModelEnabledByCode(QqArticleType::CODE_MULTIVIDEOS);
 
        // 文章分类
        $articleCategory = new ArticleCategory([
            'scenario' => $scenario,
        ]);
        // 转换视频(视频)的文章发布参数,多模型的填充、验证的实现
        $data[$articleCategory->formName()]['id'] = $qqArticleVideoCreateParam->article_category_id;
        $articleCategoryResult = self::handleLoadAndValidate($articleCategory, $data);
        if ($articleCategoryResult['status'] === false) {
            return ['status' => false, 'code' =>$articleCategoryResult['code'], 'message' => $articleCategoryResult['message']];
        }
 
        // 基于文章分类ID查找状态为启用的单个数据模型(企鹅号的文章类型(视频)的文章分类)
        $qqArticleCategoryMultivideosEnabledItem = QqArticleCategoryMultivideosService::findModelEnabledByArticleCategoryId($qqArticleVideoCreateParam->article_category_id);
 
        // 企鹅号的文章
        $model = new QqArticle([
            'scenario' => QqArticle::SCENARIO_CREATE,
        ]);
        // 转换视频(视频)的文章发布参数,多模型的填充、验证的实现
        $data[$model->formName()] = [
            'group_id' => $data['group_id'],
            'article_category_id' => $qqArticleVideoCreateParam->article_category_id,
            'title' => $qqArticleVideoCreateParam->title,
            'author' => $qqArticleVideoCreateParam->author,
            'source_article_id' => $qqArticleVideoCreateParam->source_article_id,
        ];
        $modelResult = self::handleLoadAndValidate($model, $data);
        if ($modelResult['status'] === false) {
            return ['status' => false, 'code' =>$modelResult['code'], 'message' => $modelResult['message']];
        }
 
        // 企鹅号的文章类型(视频)的文章
        $qqArticleMultivideos = new QqArticleMultivideos([
            'scenario' => QqArticleMultivideos::SCENARIO_CREATE,
        ]);
        // 转换视频(视频)的文章发布参数,多模型的填充、验证的实现
        $data[$qqArticleMultivideos->formName()] = [
            'media' => $qqArticleVideoCreateParam->media_absolute_url,
            'tag' => $qqArticleVideoCreateParam->tag,
            'desc' => $qqArticleVideoCreateParam->desc,
            'apply' => $qqArticleVideoCreateParam->apply,
        ];
        $qqArticleMultivideosResult = self::handleLoadAndValidate($qqArticleMultivideos, $data);
        if ($qqArticleMultivideosResult['status'] === false) {
            return ['status' => false, 'code' =>$qqArticleMultivideosResult['code'], 'message' => $qqArticleMultivideosResult['message']];
        }
 
        /* 操作数据(事务) */
 
        $qqArticleService = new QqArticleService();
 
        $result = $qqArticleService->videoCreate($channelEnabledItem, $channelTypeEnabledItem, $channelAppSourceEnabledItems, $qqAppEnabledItems, $articleTypeEnabledItem, $qqArticleTypeEnabledItem, $qqArticleCategoryMultivideosEnabledItem, $task, $model, $qqArticleMultivideos, false);
        if ($result['status'] === false) {
            throw new ServerErrorHttpException($result['message'], $result['code']);
        }
 
        return ['status' => true, 'code' =>10000, 'message' => Yii::t('common/success', '25001'), 'data' => $result['data']];
    }
 
    /**
     * 处理模型填充与验证
     * @param object $model 模型
     * @param array $requestParams 请求参数
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => true, // 成功
     * ]
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 35024, // 返回码
     *     'message' => '数据验证失败:企鹅号ID(UUID)是无效的。', // 说明
     * ]
     *
     * @throws ServerErrorHttpException
     */
    public static function handleLoadAndValidate($model, $requestParams)
    {
        // 把请求数据填充到模型中
        if (!$model->load($requestParams)) {
            return ['status' => false, 'code' => 35073, 'message' => Yii::t('common/error', '35073')];
        }
        // 验证模型
        if (!$model->validate()) {
            return self::handleValidateError($model);
        }
 
        return ['status' => true];
    }
 
    /**
     * 处理模型错误
     * @param object $model 模型
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 35024, // 返回码
     *     'message' => '数据验证失败:代码是无效的。', // 说明
     * ]
     *
     * @throws ServerErrorHttpException
     */
    public static function handleValidateError($model)
    {
        if ($model->hasErrors()) {
            $response = Yii::$app->getResponse();
            $response->setStatusCode(422, 'Data Validation Failed.');
            foreach ($model->getFirstErrors() as $message) {
                $firstErrors = $message;
                break;
            }
            return ['status' => false, 'code' => 35024, 'message' => Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35024'), ['firstErrors' => $firstErrors]))];
        } elseif (!$model->hasErrors()) {
            throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
        }
    }
 
    /**
     * 处理模型错误
     * @param array $models 模型列表
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 35024, // 返回码
     *     'message' => '数据验证失败:代码是无效的。', // 说明
     * ]
     *
     * @throws ServerErrorHttpException
     */
    public static function handleValidateMultipleError($models)
    {
        foreach ($models as $model) {
            if ($model->hasErrors()) {
                $response = Yii::$app->getResponse();
                $response->setStatusCode(422, 'Data Validation Failed.');
                foreach ($model->getFirstErrors() as $message) {
                    $firstErrors = $message;
                    break;
                }
                return ['status' => false, 'code' => 35024, 'message' => Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35024'), ['firstErrors' => $firstErrors]))];
            }
        }
 
        throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
    }
}

51. Starting from scratch, test a fully normal flow where an article is simultaneously published to 2 Penguin accounts (and both succeed in the final process), POST http://api.channel-pub-api.localhost/qq/v1/articles/video?group_id=spider

Request Body

JSON

{
	"channel_app_source_uuids": ["dc74d79ef6ca11e88b4654ee75d2ebc1", "0102512cf6cb11e89b1754ee75d2ebc1"],
	"source": "spider",
	"source_uuid": "825e6d5e36468cc4bf536799ce3565cf",
	"source_pub_user_id": 1,
	"source_callback_url": "http://wjdev.chinamcloud.com:8609/cms/api/thirdPush/callBack",
	"article_category_id": 110,
	"title": "Flutter移动应用:动画的介绍",
	"author": "华栖云秀",
	"source_article_id": 1,
	"media_absolute_url": "http://127.0.0.1/channel-pub-api/storage/spider/videos/flutter-animation-00-00-intro-1940143128.mp4",
	"tag": "Flutter,移动,应用,动画,介绍",
	"apply": 0,
	"desc": "这个课程我们会学一下怎么样创建和使用 Flutter 里的 Animation .. 也就是动画 .. 先了解一下 AnimationController .. 动画控制器 .. 它可以控制动画的运行状态 .."
}

Response Body

JSON

{
    "code": 10000,
    "message": "发布文章类型:视频(视频)的文章成功",
    "data": [
        {
            "channel_id": 1,
            "channel_code": "qq",
            "channel_type_id": 1,
            "channel_type_code": "qq_cw",
            "channel_app_source_id": 11,
            "channel_app_source_uuid": "dc74d79ef6ca11e88b4654ee75d2ebc1",
            "task_id": 81,
            "status": 1,
            "created_at": 1543975716,
            "updated_at": 1543975716,
            "uuid": "ace019b2f83211e8a1e154ee75d2ebc1",
            "id": 91
        },
        {
            "channel_id": 1,
            "channel_code": "qq",
            "channel_type_id": 1,
            "channel_type_code": "qq_cw",
            "channel_app_source_id": 12,
            "channel_app_source_uuid": "0102512cf6cb11e89b1754ee75d2ebc1",
            "task_id": 81,
            "status": 1,
            "created_at": 1543975716,
            "updated_at": 1543975716,
            "uuid": "ace1aff2f83211e888c154ee75d2ebc1",
            "id": 92
        }
    ]
}

52. Execute the SQL statement as follows:

SQL

SELECT * FROM `cpa_channel` WHERE (`code`='qq') AND (`is_deleted`=0)
SELECT * FROM `cpa_channel_app_source` WHERE (`uuid` IN ('dc74d79ef6ca11e88b4654ee75d2ebc1', '0102512cf6cb11e89b1754ee75d2ebc1')) AND (`is_deleted`=0)
SELECT * FROM `cpa_channel_type` WHERE (`code`='qq_cw') AND (`is_deleted`=0)
SELECT * FROM `cpa_qq_cw_app` WHERE (`channel_app_source_uuid` IN ('dc74d79ef6ca11e88b4654ee75d2ebc1', '0102512cf6cb11e89b1754ee75d2ebc1')) AND (`is_deleted`=0)
SELECT * FROM `cpa_article_type` WHERE (`code`='video') AND (`is_deleted`=0)
SELECT * FROM `cpa_qq_article_type` WHERE (`code`='multivideos') AND (`is_deleted`=0)
SELECT EXISTS(SELECT * FROM `cpa_article_category` WHERE (`cpa_article_category`.`id`=110) AND ((`is_deleted`=0) AND (`status`=1)))
SELECT * FROM `cpa_qq_article_category_multivideos` WHERE (`article_category_id`=110) AND (`is_deleted`=0)
INSERT INTO `cpa_task` (`group_id`, `source`, `source_uuid`, `source_pub_user_id`, `source_callback_url`, `channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `status`, `created_at`, `updated_at`) VALUES ('spider', 'spider', '825e6d5e36468cc4bf536799ce3565cf', 1, 'http://wjdev.chinamcloud.com:8609/cms/api/thirdPush/callBack', 1, 'qq', 1, 'qq_cw', 1, 1543975716, 1543975716)
INSERT INTO `cpa_channel_app_task` (`channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `channel_app_source_id`, `channel_app_source_uuid`, `task_id`, `status`, `created_at`, `updated_at`, `uuid`) VALUES (1, 'qq', 1, 'qq_cw', 11, 'dc74d79ef6ca11e88b4654ee75d2ebc1', 81, 1, 1543975716, 1543975716, 'ace019b2f83211e8a1e154ee75d2ebc1')
INSERT INTO `cpa_qq_cw_app_task` (`channel_app_task_id`, `channel_app_task_uuid`, `qq_cw_app_id`, `task_id`, `status`, `created_at`, `updated_at`) VALUES (91, 'ace019b2f83211e8a1e154ee75d2ebc1', 8, 81, 1, 1543975716, 1543975716)
INSERT INTO `cpa_channel_app_task` (`channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `channel_app_source_id`, `channel_app_source_uuid`, `task_id`, `status`, `created_at`, `updated_at`, `uuid`) VALUES (1, 'qq', 1, 'qq_cw', 12, '0102512cf6cb11e89b1754ee75d2ebc1', 81, 1, 1543975716, 1543975716, 'ace1aff2f83211e888c154ee75d2ebc1')
INSERT INTO `cpa_qq_cw_app_task` (`channel_app_task_id`, `channel_app_task_uuid`, `qq_cw_app_id`, `task_id`, `status`, `created_at`, `updated_at`) VALUES (92, 'ace1aff2f83211e888c154ee75d2ebc1', 9, 81, 1, 1543975716, 1543975716)
INSERT INTO `cpa_qq_article` (`group_id`, `article_category_id`, `title`, `author`, `source_article_id`, `qq_app_type`, `article_type_id`, `qq_article_type_id`, `qq_article_category_id`, `task_id`, `status`, `created_at`, `updated_at`) VALUES ('spider', 110, 'Flutter移动应用:动画的介绍', '华栖云秀', 1, 'cw', 3, 2, 807, 81, 1, 1543975716, 1543975716)
INSERT INTO `cpa_qq_article_multivideos` (`media`, `tag`, `desc`, `apply`, `qq_article_id`, `category`, `md5`, `vid`, `task_id`, `status`, `created_at`, `updated_at`) VALUES ('', 'Flutter,移动,应用,动画,介绍', '这个课程我们会学一下怎么样创建和使用 Flutter 里的 Animation .. 也就是动画 .. 先了解一下 AnimationController .. 动画控制器 .. 它可以控制动画的运行状态 ..', 0, 73, 807, '', '', 81, 1, 1543975716, 1543975716)
INSERT INTO `cpa_asset` (`channel_id`, `channel_code`, `channel_type_id`, `channel_type_code`, `source`, `type`, `absolute_url`, `relative_path`, `size`, `task_id`, `channel_article_id`, `status`, `is_deleted`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 'qq', 1, 'qq_cw', 'spider', 'video', 'http://127.0.0.1/channel-pub-api/storage/spider/videos/flutter-animation-00-00-intro-1940143128.mp4', '', 0, 81, 73, 1, 0, 1543975716, 1543975716, 0)


53. View the status information of the 4 queues

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

54. The run command fetches and executes tasks in a loop (copy resource file queue) until the queue is empty. The copy resource file queue job executed successfully, the execution result meets expectations, and the job has been pushed to the upload resource queue

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/run --verbose=1 --isolate=1 --color=0
2018-12-05 10:10:36 [pid: 22424] - Worker is started
2018-12-05 10:10:37 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 22424) - Started
2018-12-05 10:10:37 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 22424) - Done (0.261 s)
2018-12-05 10:10:37 [pid: 22424] - Worker is stopped (0:00:01)
PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 2
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

55. The run command fetches and executes tasks in a loop (upload resource file queue) until the queue is empty. The upload resource file queue job executed successfully, the execution result meets expectations, and the video transaction record has been inserted into the transaction table, as shown in Figure 10

The run command fetches and executes tasks in a loop (upload resource file queue) until the queue is empty. The upload resource file queue job executed successfully, the execution result meets expectations, and the video transaction record has been inserted into the transaction table
Figure 10
Plaintext

PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/run --verbose=1 --isolate=1 --color=0
2018-12-05 10:11:52 [pid: 20084] - Worker is started
2018-12-05 10:11:52 [1] common\jobs\UploadAssetJob (attempt: 1, pid: 20084) - Started
2018-12-05 10:11:58 [1] common\jobs\UploadAssetJob (attempt: 1, pid: 20084) - Done (5.706 s)
2018-12-05 10:11:58 [2] common\jobs\UploadAssetJob (attempt: 1, pid: 20084) - Started
2018-12-05 10:12:03 [2] common\jobs\UploadAssetJob (attempt: 1, pid: 20084) - Done (5.028 s)
2018-12-05 10:12:04 [pid: 20084] - Worker is stopped (0:00:12)
PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

56. Console command: fetch the video transaction of the Penguin API for the Penguin content website application, and synchronize it to the video transaction of the Penguin for the Penguin content website application. The execution result meets expectations, and the job has been pushed to the publish article queue

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii qq-cw-transaction-video/sync
PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 2
- delayed: 0
- reserved: 0
- done: 0
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

57. The run command fetches and executes tasks in a loop (publish article queue) until the queue is empty. The publish article queue job executed successfully, the execution result meets expectations, and the article transaction record has been inserted into the transaction table, as shown in Figure 11

The run command fetches and executes tasks in a loop (publish article queue) until the queue is empty. The publish article queue job executed successfully, the execution result meets expectations, and the article transaction record has been inserted into the transaction table
Figure 11
Plaintext

PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/run --verbose=1 --isolate=1 --color=0
2018-12-05 10:13:56 [pid: 19856] - Worker is started
2018-12-05 10:13:56 [1] common\jobs\PubArticleJob (attempt: 1, pid: 19856) - Started
2018-12-05 10:13:58 [1] common\jobs\PubArticleJob (attempt: 1, pid: 19856) - Done (2.107 s)
2018-12-05 10:13:59 [2] common\jobs\PubArticleJob (attempt: 1, pid: 19856) - Started
2018-12-05 10:14:01 [2] common\jobs\PubArticleJob (attempt: 1, pid: 19856) - Done (2.107 s)
2018-12-05 10:14:01 [pid: 19856] - Worker is stopped (0:00:05)
PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

58. Console command: fetch the article transaction of the Penguin API for the Penguin content website application, and synchronize it to the article transaction of the Penguin for the Penguin content website application. The execution result meets expectations, the article transaction record has been updated in the transaction table, and the job has been pushed to the source callback queue

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii qq-cw-transaction-article/sync
PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 2
- delayed: 0
- reserved: 0
- done: 0

59. The run command fetches and executes tasks in a loop (source callback queue) until the queue is empty. The source callback queue job executed successfully, the execution result meets expectations, and it has been updated in the publication log table

Plaintext

PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/run --verbose=1 --isolate=1 --color=0
2018-12-05 10:49:54 [pid: 25376] - Worker is started
2018-12-05 10:49:54 [1] common\jobs\SourceCallbackJob (attempt: 1, pid: 25376) - Started
2018-12-05 10:50:06 [1] common\jobs\SourceCallbackJob (attempt: 1, pid: 25376) - Error (11.899 s)
> yii\httpclient\Exception: Curl error: #6 - Could not resolve host: wjdev.chinamcloud.com
2018-12-05 10:50:07 [2] common\jobs\SourceCallbackJob (attempt: 1, pid: 25376) - Started
2018-12-05 10:50:07 [2] common\jobs\SourceCallbackJob (attempt: 1, pid: 25376) - Done (0.424 s)
2018-12-05 10:50:07 [pid: 25376] - Worker is stopped (0:00:13)
PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1
PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii pub-article-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 1
- reserved: 0
- done: 2
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/run --verbose=1 --isolate=1 --color=0
2018-12-05 11:00:10 [pid: 25616] - Worker is started
2018-12-05 11:00:10 [3] common\jobs\SourceCallbackJob (attempt: 1, pid: 25616) - Started
2018-12-05 11:00:14 [3] common\jobs\SourceCallbackJob (attempt: 1, pid: 25616) - Done (3.800 s)
2018-12-05 11:00:14 [pid: 25616] - Worker is stopped (0:00:04)
PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 3

60. Log in to the Penguin accounts respectively, the article has been published successfully, as shown in Figure 12, 13

Log in to the Penguin accounts respectively, the article has been published successfully
Figure 12
Log in to the Penguin accounts respectively, the article has been published successfully
Figure 13

61. View the information in the transaction table, open the article quick report links: http://kuaibao.qq.com/s/20181205V0E6C600 , http://kuaibao.qq.com/s/20181205V0E6DU00 , the result meets expectations and they can be opened normally

62. Summary, the overall program structure is as follows:

Plaintext
0: Not implemented
1: Implemented
2: To be implemented
3: Not to be implemented

// Copy resource file queue job: copy source resource files to the channel publication's resource directory
\common\jobs\CopyAssetJob.php execute($queue)    1


// Resource service, copy source resource files to the channel publication's resource directory, return relative path (synchronous)
AssetService.php copyAssetsSync($source, $assets)    1


// Copy resource file queue event handler, after the copy resource file queue job executes successfully
\common\components\queue\CopyAssetEventHandler.php afterExec(ExecEvent $event)    1
// Copy resource file queue event handler, after the copy resource file queue job fails to execute
\common\components\queue\CopyAssetEventHandler.php afterError(ExecEvent $event)    1


// Resource service for the Penguin content website application, subsequent processing after the copy resource file queue job executes successfully
QqCwAssetService.php copyAssetExecHandler($taskId)    1
// Resource service for the Penguin third-party service platform application, subsequent processing after the copy resource file queue job executes successfully
QqTpAssetService.php copyAssetExecHandler($taskId)    0
// Resource service for the WeChat Official Account application, subsequent processing after the copy resource file queue job executes successfully
WxAssetService.php copyAssetExecHandler($taskId)    1


// Resource service for the Penguin content website application, subsequent processing after the copy resource file queue job fails to execute
QqCwAssetService.php copyAssetErrorHandler($taskId, $eventError)    1
// Resource service for the Penguin third-party service platform application, subsequent processing after the copy resource file queue job fails to execute
QqTpAssetService.php copyAssetErrorHandler($taskId, $eventError)    0
// Resource service for the WeChat Official Account application, subsequent processing after the copy resource file queue job fails to execute
WxAssetService.php copyAssetErrorHandler($taskId, $eventError)    1


// Upload resource file queue job: upload resource file
\common\jobs\UploadAssetJob.php execute($queue)    1


// Resource service for the Penguin content website application, multipart upload of video files for the Penguin content website application (synchronous)
QqCwAssetService.php uploadAssetVideoMultipartSync($assetId, $channelAppTaskId)    1
// Resource service for the Penguin third-party service platform application, multipart upload of video files for the Penguin third-party service platform application (synchronous)
QqTpAssetService.php uploadAssetVideoMultipartSync($assetId, $channelAppTaskId)    0
// Resource service for the WeChat Official Account application, resource upload for the WeChat Official Account application (synchronous)
WxAssetService.php uploadAssetSync($assetId, $channelAppTaskId)    0
// Resource service for the WeChat Official Account application, multipart upload of video files for the Penguin content website application (synchronous)
WxAssetService.php qqCwUploadAssetVideoMultipartSync($taskId)    3


// Upload resource file queue event handler, after the upload resource file queue job executes successfully
\common\components\queue\UploadAssetEventHandler.php afterExec(ExecEvent $event)    1
// Upload resource file queue event handler, after the upload resource file queue job fails to execute
\common\components\queue\UploadAssetEventHandler.php afterError(ExecEvent $event)    1


// Resource service for the Penguin content website application, subsequent processing after the upload resource file queue job executes successfully
QqCwAssetService.php uploadAssetVideoMultipartExecHandler($assetId, $channelAppTaskId)    1(left blank)
// Resource service for the Penguin third-party service platform application, subsequent processing after the upload resource file queue job executes successfully
QqTpAssetService.php uploadAssetVideoMultipartExecHandler($assetId, $channelAppTaskId)    0
// Resource service for the WeChat Official Account application, subsequent processing after the upload resource file queue job executes successfully
WxAssetService.php uploadAssetExecHandler($assetId, $channelAppTaskId)    0
// Resource service for the WeChat Official Account application, subsequent processing after the upload resource file queue job of the Penguin content website application executes successfully
WxAssetService.php qqCwUploadAssetVideoMultipartExecHandler($taskId)    3


// Resource service for the Penguin content website application, subsequent processing after the upload resource file queue job fails to execute
QqCwAssetService.php uploadAssetVideoMultipartErrorHandler($assetId, $channelAppTaskId, $eventError)    1
// Resource service for the Penguin third-party service platform application, subsequent processing after the upload resource file queue job fails to execute
QqTpAssetService.php uploadAssetVideoMultipartErrorHandler($assetId, $channelAppTaskId, $eventError)    0
// Resource service for the WeChat Official Account application, subsequent processing after the upload resource file queue job fails to execute
WxAssetService.php uploadAssetErrorHandler($assetId, $channelAppTaskId, $eventError)    0
// Resource service for the WeChat Official Account application, subsequent processing after the upload resource file queue job of the Penguin content website application fails to execute
WxAssetService.php qqCwUploadAssetVideoMultipartErrorHandler($taskId, $eventError)    1


// Video transaction controller for the Penguin content website application, video transaction for the synchronous interface
QqCwTransactionVideoController.php actionSync()    1
// Video transaction controller for the Penguin third-party service platform application, video transaction for the synchronous interface
QqTpTransactionVideoController.php actionSync()    0


// Transaction service for the Penguin content website application, subsequent processing after the Penguin video transaction for the Penguin content website application succeeds
QqCwTransactionService.php videoExecHandler($qqTransactionId)    1
// Transaction service for the Penguin third-party service platform application, subsequent processing after the Penguin video transaction for the Penguin third-party service platform application succeeds
QqTpTransactionService.php videoExecHandler($qqTransactionId)    0
// Resource service for the WeChat Official Account application, subsequent processing after the Penguin video transaction for the Penguin third-party service platform application succeeds
WxAssetService.php qqCwTransactionVideoExecHandler($taskId)    3


// Transaction service for the Penguin content website application, subsequent processing after the Penguin video transaction for the Penguin content website application fails
QqCwTransactionService.php videoErrorHandler($qqTransactionId)    1
// Transaction service for the Penguin third-party service platform application, subsequent processing after the Penguin video transaction for the Penguin third-party service platform application fails
QqTpTransactionService.php videoErrorHandler($qqTransactionId)    0
// Resource service for the WeChat Official Account application, subsequent processing after the Penguin video transaction for the Penguin third-party service platform application fails
WxAssetService.php qqCwTransactionVideoErrorHandler($taskId, $errorCode, $errorMessage)    2


// Publish article queue job: publish article
\common\jobs\PubArticleJob.php execute($queue)    1


// Article service for the Penguin content website application, publish an article of type: video to the Penguin platform for the Penguin content website application (synchronous)
QqCwArticleService.php pubArticleVideoSync($channelAppTaskId)    1
// Article service for the Penguin third-party service platform application, publish an article of type: video to the Penguin platform for the Penguin third-party service platform application (synchronous)
QqTpArticleService.php pubArticleVideoSync($channelAppTaskId)    0
// Article service for the WeChat Official Account application, publish an article of type: video to the WeChat Official Account platform for the WeChat Official Account application (synchronous)
WxArticleService.php pubArticleVideoSync($channelAppTaskId)    0
// Article service for the WeChat Official Account application, publish an article of type: video to the Penguin platform for the Penguin content website application (synchronous)
WxArticleService.php qqCwPubArticleVideoSync($taskId)    3


// Publish article queue event handler, after the publish article queue job executes successfully
\common\components\queue\PubArticleEventHandler.php afterExec(ExecEvent $event)    1
// Publish article queue event handler, after the publish article queue job fails to execute
\common\components\queue\PubArticleEventHandler.php afterError(ExecEvent $event)    1


// Article service for the Penguin content website application, subsequent processing after the publish article queue job executes successfully
QqCwArticleService.php pubArticleVideoExecHandler($channelAppTaskId)    1(left blank)
// Article service for the Penguin third-party service platform application, subsequent processing after the publish article queue job executes successfully
QqTpArticleService.php pubArticleVideoExecHandler($channelAppTaskId)    0
// Article service for the WeChat Official Account application, subsequent processing after the publish article queue job executes successfully
WxArticleService.php pubArticleVideoExecHandler($channelAppTaskId)    0
// Article service for the WeChat Official Account application, subsequent processing after the publish article queue job of the Penguin content website application executes successfully
WxArticleService.php qqCwPubArticleVideoExecHandler($taskId)    3


// Article service for the Penguin content website application, subsequent processing after the publish article queue job fails to execute
QqCwArticleService.php pubArticleVideoErrorHandler($channelAppTaskId, $eventError)    1
// Article service for the Penguin third-party service platform application, subsequent processing after the publish article queue job fails to execute
QqTpArticleService.php pubArticleVideoErrorHandler($channelAppTaskId, $eventError)    0
// Article service for the WeChat Official Account application, subsequent processing after the publish article queue job fails to execute
WxArticleService.php pubArticleVideoErrorHandler($channelAppTaskId, $eventError)    1
// Article service for the WeChat Official Account application, subsequent processing after the publish article queue job of the Penguin content website application fails to execute
WxArticleService.php qqCwPubArticleVideoErrorHandler($taskId, $eventError)    1


// Article transaction controller for the Penguin content website application, article transaction for the synchronous interface
QqCwTransactionArticleController.php actionSync()    1
// Article transaction controller for the Penguin third-party service platform application, article transaction for the synchronous interface
QqTpTransactionArticleController.php actionSync()    0


// Transaction service for the Penguin content website application, subsequent processing after the Penguin video article transaction for the Penguin content website application succeeds
QqCwTransactionService.php articleMultivideosExecHandler($qqTransactionId)    1
// Transaction service for the Penguin third-party service platform application, subsequent processing after the Penguin video article transaction for the Penguin content website application succeeds
QqTpTransactionService.php articleMultivideosExecHandler($qqTransactionId)    0
// Article service for the WeChat Official Account application, subsequent processing after the Penguin video article transaction for the Penguin content website application succeeds
WxArticleService.php qqCwTransactionArticleVideoExecHandler($taskId)    2


// Transaction service for the Penguin content website application, subsequent processing after the Penguin video article transaction for the Penguin content website application fails
QqCwTransactionService.php articleMultivideosErrorHandler($qqTransactionId)    1
// Transaction service for the Penguin third-party service platform application, subsequent processing after the Penguin video article transaction for the Penguin content website application fails
QqTpTransactionService.php articleMultivideosErrorHandler($qqTransactionId)    0
// Article service for the WeChat Official Account application, subsequent processing after the Penguin video article transaction for the Penguin content website application fails
WxArticleService.php qqCwTransactionArticleVideoErrorHandler($taskId, $errorCode, $errorMessage)    2


// Source callback queue job: source callback
\common\jobs\SourceCallbackJob.php execute($queue)    1


// Source callback queue event handler, after the source callback queue job executes successfully
\common\components\queue\SourceCallbackEventHandler.php afterExec(ExecEvent $event)    1
// Source callback queue event handler, after the source callback queue job fails to execute
\common\components\queue\SourceCallbackEventHandler.php afterError(ExecEvent $event)    1


// Source callback service, subsequent processing after the source callback queue job executes successfully
SourceCallbackService.php sourceCallbackExecHandler($channelAppTaskId)    1
// Source callback service, subsequent processing after the source callback queue job fails to execute
SourceCallbackService.php sourceCallbackErrorHandler($channelAppTaskId)    1


关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系