分类: Web框架

  • 在 Yii 2 高级项目模板中,实现 RESTFUL WEB 服务时,消息目录(messages)中配置的最佳实践

    1、之前的实现如下,公共目录中的配置文件,\common\config\main.php,国际化配置如下:
    
    
        'components' => [
            'i18n' => [
                'translations' => [
                    'common/*' => [
                        'class' => 'yii\i18n\PhpMessageSource',
                        'forceTranslation' => true,
                        'basePath' => '@common/messages',
                        'fileMap' => [
                            'common/app' => 'app.php',
                            'common/error' => 'error.php',
                            'common/success' => 'success.php',
                        ],
                    ],
                    'model/*' => [
                        'class' => 'yii\i18n\PhpMessageSource',
                        'forceTranslation' => true,
                        'basePath' => '@common/messages',
                    ],
                    '*' => [
                        'class' => 'yii\i18n\PhpMessageSource',
                        'forceTranslation' => true,
                        'basePath' => '@app/messages',
                        'fileMap' => [
                            'app' => 'app.php',
                            'error' => 'error.php',
                            'success' => 'success.php',
                        ],
                    ],
                ],
            ],
        ],
    
    
    
    2、公共目录中的消息目录(messages)中的文件如下: \common\messages\zh-CN\error.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?php
    return [
        20000 => 'error',
        201001 => '框架服务控制台HTTP请求失败,状态码:{status_code}',
        201002 => '框架服务控制台HTTP请求失败:{first_error}',
        201003 => '框架服务控制台HTTP请求失败:{message}',
        201004 => '框架服务接口HTTP请求失败,状态码:{status_code}',
        201005 => '框架服务接口HTTP请求失败:{first_error}',
    ];
    
    
    </pre>
    
    \common\messages\zh-CN\error.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?php
    return [
        10000 => 'success',
    ];
    
    
    </pre>
    
    3、接口应用中的消息目录(messages)中的文件如下: \api\messages\zh-CN\error.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?php
    return [
        20000 => 'error',
        224001 => '日志列表为空',
        224002 => '日志ID:{id},不存在',
        224003 => '数据过滤器验证失败:{first_error}',
        224004 => '用户ID:{id},的状态为已禁用',
        224005 => '页面列表为空',
        224006 => '页面ID:{id},不存在',
        224007 => '页面ID:{id},的状态为已删除',
        224008 => '页面ID:{id},的状态为已禁用',
        224009 => '页面ID:{id},的状态为草稿',
        226001 => '用户列表为空',
        226002 => '用户ID:{id},不存在',
        226003 => '用户ID:{id},的状态为已删除',
        226004 => '数据验证失败:{first_error}',
    ];
    
    
    </pre>
    
    \api\messages\zh-CN\success.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?php
    return [
        10000 => 'success',
        124001 => '获取日志列表成功',
        124002 => '获取日志详情成功',
        124003 => '获取页面列表成功',
        124004 => '获取页面详情成功',
        124005 => '创建页面成功',
        124006 => '更新页面成功',
        124007 => '删除页面成功',
        126001 => '获取用户列表成功',
        126002 => '获取用户详情成功',
        126003 => '创建用户成功',
        126004 => '更新用户成功',
        126005 => '删除用户成功',
    ];
    
    
    </pre>
    
    4、公共目录中的 Yii::t() 方法实现如下:
    
    
    	throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '201001'), ['status_code' => $response->getStatusCode()])), 201001);
    
    
    
    5、接口应用中的 Yii::t() 方法实现如下:
    
    
    	return ['code' => 226004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '226004'), ['first_error' => $firstError]))];
    
    
    
    6、之前的实现存在的问题如下: (1)公共目录中的 Yii::t() 方法实现 Yii::t(‘common/error’ 有所冗余,期待 Yii::t(‘error’ 的实现 (2)接口应用中如果要包含公共目录中的要翻译的消息,Yii::t() 方法必须实现 Yii::t(‘common/error’,要包含接口应用中的要翻译的消息,Yii::t() 方法必须实现 Yii::t(‘error’,需要明确得知要翻译的消息的位置,存在很大的出错可能性,且实现不够纯粹,有所混杂 7、基于存在的问题,解决方案调整如下,公共目录中的配置文件,i18n 应用组件配置的调整(消息类别:删除 common/*),\common\config\main.php,国际化配置如下:
    
    
        'components' => [
            'i18n' => [
                'translations' => [
                    'model/*' => [
                        'class' => 'yii\i18n\PhpMessageSource',
                        'forceTranslation' => true,
                        'basePath' => '@common/messages',
                    ],
                    '*' => [
                        'class' => 'yii\i18n\PhpMessageSource',
                        'forceTranslation' => true,
                        'basePath' => '@app/messages',
                        'fileMap' => [
                            'app' => 'app.php',
                            'error' => 'error.php',
                            'success' => 'success.php',
                        ],
                    ],
                ],
            ],
        ],
    
    
    
    8、公共目录中的消息目录(messages)中的文件不做调整,保持原样 9、接口应用中的消息目录(messages)中的文件如下: \api\messages\zh-CN\error.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?php
    $commonMessages = require __DIR__ . '/../../../common/messages/zh-CN/error.php';
    $messages = [
        224001 => '日志列表为空',
        224002 => '日志ID:{id},不存在',
        224003 => '数据过滤器验证失败:{first_error}',
        224004 => '用户ID:{id},的状态为已禁用',
        224005 => '页面列表为空',
        224006 => '页面ID:{id},不存在',
        224007 => '页面ID:{id},的状态为已删除',
        224008 => '页面ID:{id},的状态为已禁用',
        224009 => '页面ID:{id},的状态为草稿',
        226001 => '用户列表为空',
        226002 => '用户ID:{id},不存在',
        226003 => '用户ID:{id},的状态为已删除',
        226004 => '数据验证失败:{first_error}',
    ];
    return $commonMessages + $messages;
    
    
    </pre>
    
    \api\messages\zh-CN\success.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?php
    $commonMessages = require __DIR__ . '/../../../common/messages/zh-CN/success.php';
    $messages = [
        124001 => '获取日志列表成功',
        124002 => '获取日志详情成功',
        124003 => '获取页面列表成功',
        124004 => '获取页面详情成功',
        124005 => '创建页面成功',
        124006 => '更新页面成功',
        124007 => '删除页面成功',
        126001 => '获取用户列表成功',
        126002 => '获取用户详情成功',
        126003 => '创建用户成功',
        126004 => '更新用户成功',
        126005 => '删除用户成功',
    ];
    return $commonMessages + $messages;
    
    
    </pre>
    
    10、公共目录中的 Yii::t() 方法实现如下:
    
    
    	throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '201001'), ['status_code' => $response->getStatusCode()])), 201001);
    
    
    
    11、接口应用中的 Yii::t() 方法实现如下:
    
    
    	return ['code' => 226004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '226004'), ['first_error' => $firstError]))];
    
    
    
    12、之前的实现存在的问题,已经得到解决,如下: (1)公共目录中的 Yii::t() 方法实现 Yii::t(‘error’,仅需要保证对应的 code 存在于公共目录中的消息目录(messages)中 (2)接口应用中如果要包含公共目录中的要翻译的消息,Yii::t() 方法必须实现 Yii::t(‘error’,要包含接口应用中的要翻译的消息,Yii::t() 方法必须实现 Yii::t(‘error’,无需明确得知要翻译的消息的位置,公共目录与接口应用中的消息目录(messages)皆可使用 (3)存在的一个小问题,如果 公共目录中 与 接口应用中 皆存在一个同样的 code,此时接口应用中的值不会覆盖公共目录中的值,原因在于接口应用中消息文件未使用 array_merge 方法,因为数组索引为数字,如果使用 array_merge 方法,数字键名将会被重新编号,索引会从 0 开始,因此,保留原有数组并只想新的数组附加到后面,用 + 运算符,附加的话,在两个数组中存在相同的键名时,第一个数组中的同键名的元素将会被保留,第二个数组中的元素将会被忽略
  • 在 Yii 2 中,当输入数据是通过网址时,为了避免执行冗余的更新SQL,给输入值应用一个过滤器(intval),进而导致衍生出的验证规则失效的分析解决

    在 Yii 2 中,当输入数据是通过网址时,为了避免执行冗余的更新SQL,给输入值应用一个过滤器(intval),进而导致衍生出的验证规则失效的分析解决

    1、在浏览器中打开网址:http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize-update?group_id=spider&channel_app_source_uuid=269f68bc098011e9b1c354ee75d2ebc1&user_name=华栖云1658397962&permission=4&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29tLw%3D%3D ,此时验证规则生效,如图1
    在浏览器中打开网址:http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize-update?group_id=spider&channel_app_source_uuid=269f68bc098011e9b1c354ee75d2ebc1&user_name=华栖云1658397962&permission=4&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29tLw%3D%3D ,此时验证规则生效
    图1
    2、验证规则为:permission:可选,权限,1:同步;2:发布;3:同步与发布
    
    
                /* 更新微博的微连接的网页应用的用户 */
                [['permission'], 'in', 'range' => [self::PERMISSION_SYNC, self::PERMISSION_PUB, self::PERMISSION_SYNC_PUB], 'on' => self::SCENARIO_UPDATE],
    
    
    
    3、此时数据库中 permission 的值为 2,updated_at 的值为 1545961613 ,如图2
    此时数据库中 permission 的值为 2,updated_at 的值为 1545961613
    图2
    4、在浏览器中打开网址:http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize-update?group_id=spider&channel_app_source_uuid=269f68bc098011e9b1c354ee75d2ebc1&user_name=华栖云1658397962&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29tLw%3D%3D ,permission 的值为 2,由于 permission 字段为 SMALLINT(6) 类型,status 字段为 SMALLINT(6) 类型,而通过网址传递的参数为字符串类型,因此,即使 permission 与 status 的值未发生变化,仍然执行了更新 SQL,执行 SQL 语句如下:
    
    
    UPDATE `cpa_weibo_weibo_connect_web_app_user` SET `permission`=2, `status`=1, `updated_at`=1546066986 WHERE `id`=17
    
    
    
    5、调整验证规则,给输入值应用一个过滤器(intval)
    
    
                /* 更新微博的微连接的网页应用的用户 */
                [['permission'], 'in', 'range' => [self::PERMISSION_SYNC, self::PERMISSION_PUB, self::PERMISSION_SYNC_PUB], 'on' => self::SCENARIO_UPDATE],
    			[['permission', 'status'], 'filter', 'filter' => 'intval', 'on' => self::SCENARIO_UPDATE],
    
    
    
    6、在浏览器中打开网址:http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize-update?group_id=spider&channel_app_source_uuid=269f68bc098011e9b1c354ee75d2ebc1&user_name=华栖云1658397962&permission=&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29tLw%3D%3D ,permission 的值为 空字符串,此时验证规则失效,permission 的值更新为 0,执行 SQL 语句如下:
    
    
    UPDATE `cpa_weibo_weibo_connect_web_app_user` SET `permission`=0, `updated_at`=1546067544 WHERE `id`=17
    
    
    
    7、调整验证规则,将过滤器(intval)置于最前
    
    
                /* 更新微博的微连接的网页应用的用户 */
    			[['permission', 'status'], 'filter', 'filter' => 'intval', 'on' => self::SCENARIO_UPDATE],
                [['permission'], 'in', 'range' => [self::PERMISSION_SYNC, self::PERMISSION_PUB, self::PERMISSION_SYNC_PUB], 'on' => self::SCENARIO_UPDATE],
    
    
    
    8、在浏览器中打开网址:http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize-update?group_id=spider&channel_app_source_uuid=269f68bc098011e9b1c354ee75d2ebc1&user_name=华栖云1658397962&permission=&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29tLw%3D%3D ,permission 的值为 空字符串,此时验证规则生效,但是其值已经从字符串变化为 0,期望的是,当其值非“空值”时,才转换整数值,如图3
    在浏览器中打开网址:http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize-update?group_id=spider&channel_app_source_uuid=269f68bc098011e9b1c354ee75d2ebc1&user_name=华栖云1658397962&permission=&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29tLw%3D%3D ,permission 的值为 空字符串,此时验证规则生效,但是其值已经从字符串变化为 0,期望的是,当其值非“空值”时,才转换整数值
    图3
    9、默认情况下,当输入项为空字符串,空数组,或 null 时,会被视为“空值”。在 核心验证器 之中,filter(滤镜)验证器默认会处理空输入。调整 filter(滤镜)验证器,其 yii\base\Validator::skipOnEmpty 属性为 false,调整为 true
    
    
                /* 更新微博的微连接的网页应用的用户 */
    			[['permission', 'status'], 'filter', 'filter' => 'intval', 'skipOnEmpty' => true, 'on' => self::SCENARIO_UPDATE],
                [['permission'], 'in', 'range' => [self::PERMISSION_SYNC, self::PERMISSION_PUB, self::PERMISSION_SYNC_PUB], 'on' => self::SCENARIO_UPDATE],
    
    
    
    10、在浏览器中打开网址:http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize-update?group_id=spider&channel_app_source_uuid=269f68bc098011e9b1c354ee75d2ebc1&user_name=华栖云1658397962&permission=&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29tLw%3D%3D ,permission 的值为 空字符串,此时验证规则生效,数据验证失败:权限不能为空。如图4
    在浏览器中打开网址:http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize-update?group_id=spider&channel_app_source_uuid=269f68bc098011e9b1c354ee75d2ebc1&user_name=华栖云1658397962&permission=&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29tLw%3D%3D ,permission 的值为 空字符串,此时验证规则生效,数据验证失败:权限不能为空。
    图4
    11、此时,2 条规则的顺序可以随意调整了,因为 2 条规则皆是在其值非“空值”时,才会执行验证,推荐置于最后,表示当验证通过后,才转为整数值
    
    
                /* 更新微博的微连接的网页应用的用户 */
                [['permission'], 'in', 'range' => [self::PERMISSION_SYNC, self::PERMISSION_PUB, self::PERMISSION_SYNC_PUB], 'on' => self::SCENARIO_UPDATE],
    			[['permission', 'status'], 'filter', 'filter' => 'intval', 'skipOnEmpty' => true, 'on' => self::SCENARIO_UPDATE],
    
    
    
    12、在浏览器中打开网址:http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize-update?group_id=spider&channel_app_source_uuid=269f68bc098011e9b1c354ee75d2ebc1&user_name=华栖云1658397962&permission=&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29tLw%3D%3D ,permission 的值为 空字符串,此时验证规则生效,数据验证失败:权限不能为空。与步骤 10 的异常响应一致,符合预期  
  • 基于 Rancher,在 1 个 Git 仓库、1 个容器中部署 2 个域名的实现

    基于 Rancher,在 1 个 Git 仓库、1 个容器中部署 2 个域名的实现

    1、现在渠道发布接口包含如下应用:企鹅号、微信公众帐号、微博、控制台命令、跨渠道、授权,其中企鹅号、微信公众帐号、微博、跨渠道需要部署为域名 A,且入方向仅支持内网,其中授权需要部署为域名 B,入方向可支持外网,如图1
    现在渠道发布接口包含如下应用:企鹅号、微信公众帐号、微博、控制台命令、跨渠道、授权,其中企鹅号、微信公众帐号、微博、跨渠道需要部署为域名 A,且入方向仅支持内网,其中授权需要部署为域名 B,入方向可支持外网
    图1
    2、\build\c_files\etc\nginx\conf.d\channel-pub-api.conf,已经支持部署为域名 A,且入方向仅支持内网
    
    
    server {
        listen 80; ## listen for ipv4
        server_name CHANNEL_PUB_API_CFG_NGINX_SERVER_NAME;
        charset utf-8;
    
        root /sobey/www/channel-pub-api;
        index index.php;
    
        location / {
            root /sobey/www/channel-pub-api/api/web;
            try_files $uri $uri/ /api/web/index.php$is_args$args;
    
            # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
            #location ~ ^/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
            #    log_not_found off;
            #    access_log off;
            #    try_files $uri =404;
            #}
    
            location ~ ^/assets/.+\.php(/|$) {
                deny all;
            }
        }
    
        location /qq {
            alias /sobey/www/channel-pub-api/qq/web/;
    
            # redirect to the URL without a trailing slash (uncomment if necessary)
            #location = /qq/ {
            #    return 301 /qq;
            #}
    
            # prevent the directory redirect to the URL with a trailing slash
            location = /qq {
                # if your location is "/qq", try use "/qq/qq/web/index.php$is_args$args"
                # bug ticket: https://trac.nginx.org/nginx/ticket/97
                try_files $uri /qq/qq/web/index.php$is_args$args;
            }
    
            # if your location is "/qq", try use "/qq/qq/web/index.php$is_args$args"
            # bug ticket: https://trac.nginx.org/nginx/ticket/97
            try_files $uri $uri/ /qq/qq/web/index.php$is_args$args;
    
            # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
            #location ~ ^/qq/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
            #    log_not_found off;
            #    access_log off;
            #    try_files $uri =404;
            #}
    
            location ~ ^/qq/assets/.+\.php(/|$) {
                deny all;
            }
        }
    
        location /weibo {
                alias /sobey/www/channel-pub-api/weibo/web/;
    
                # redirect to the URL without a trailing slash (uncomment if necessary)
                #location = /weibo/ {
                #    return 301 /weibo;
                #}
    
                # prevent the directory redirect to the URL with a trailing slash
                location = /weibo {
                    # if your location is "/weibo", try use "/weibo/weibo/web/index.php$is_args$args"
                    # bug ticket: https://trac.nginx.org/nginx/ticket/97
                    try_files $uri /weibo/weibo/web/index.php$is_args$args;
                }
    
                # if your location is "/weibo", try use "/weibo/weibo/web/index.php$is_args$args"
                # bug ticket: https://trac.nginx.org/nginx/ticket/97
                try_files $uri $uri/ /weibo/weibo/web/index.php$is_args$args;
    
                # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
                #location ~ ^/weibo/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
                #    log_not_found off;
                #    access_log off;
                #    try_files $uri =404;
                #}
    
                location ~ ^/weibo/assets/.+\.php(/|$) {
                    deny all;
                }
            }
    
        location /wx {
            alias /sobey/www/channel-pub-api/wx/web/;
    
            # redirect to the URL without a trailing slash (uncomment if necessary)
            #location = /wx/ {
            #    return 301 /wx;
            #}
    
            # prevent the directory redirect to the URL with a trailing slash
            location = /wx {
                # if your location is "/wx", try use "/wx/wx/web/index.php$is_args$args"
                # bug ticket: https://trac.nginx.org/nginx/ticket/97
                try_files $uri /wx/wx/web/index.php$is_args$args;
            }
    
            # if your location is "/wx", try use "/wx/wx/web/index.php$is_args$args"
            # bug ticket: https://trac.nginx.org/nginx/ticket/97
            try_files $uri $uri/ /wx/wx/web/index.php$is_args$args;
    
            # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
            #location ~ ^/wx/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
            #    log_not_found off;
            #    access_log off;
            #    try_files $uri =404;
            #}
    
            location ~ ^/wx/assets/.+\.php(/|$) {
                deny all;
            }
        }
    
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
    	location ~ ^/.+\.php(/|$) {
            rewrite (?!^/((api|qq|weibo|wx)/web|qq|weibo|wx))^ /api/web$uri break;
            rewrite (?!^/qq/web)^/qq(/.+)$ /qq/web$1 break;
            rewrite (?!^/weibo/web)^/weibo(/.+)$ /weibo/web$1 break;
    		rewrite (?!^/wx/web)^/wx(/.+)$ /wx/web$1 break;
    
    		include fastcgi_params;
    		fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    		fastcgi_pass 127.0.0.1:9000;
    		#fastcgi_pass unix:/var/run/php5-fpm.sock;
            try_files $fastcgi_script_name =404;
        }
    
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        location ~ /\. {
            deny all;
        }
    }
    
    
    
    3、Rancher 中端口映射,公开主机端口:8661 映射至私有容器端口:80,如图2
    Rancher 中端口映射,公开主机端口:8661 映射至私有容器端口:80
    图2
    4、域名 A 的访问详情如下
    
    
    企鹅号:https://wjdev2.chinamcloud.com:8661/v1/qq/
    微信公众帐号:https://wjdev2.chinamcloud.com:8661/v1/wx/
    微博:https://wjdev2.chinamcloud.com:8661/v1/weibo/
    跨渠道:https://wjdev2.chinamcloud.com:8661/v1/
    
    
    
    5、编辑 \build\c_files\etc\nginx\conf.d\channel-pub-api.conf,以支持授权需要部署为域名 B,入方向可支持外网
    
    
    server {
        listen CHANNEL_PUB_API_CFG_NGINX_AUTH_LISTEN; ## listen for ipv4
        server_name CHANNEL_PUB_API_CFG_NGINX_AUTH_SERVER_NAME;
        charset utf-8;
    
        root /sobey/www/channel-pub-api/frontend/web;
        index index.php;
        location / {
            # 如果找不到真实存在的文件,把请求分发至 index.php
            try_files $uri $uri/ /index.php$is_args$args;
        }
    
        # uncomment to avoid processing of calls to non-existing static files by Yii
        #location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
        #    try_files $uri =404;
        #}
        #error_page 404 /404.html;
    
        # deny accessing php files for the /assets directory
        location ~ ^/assets/.*\.php$ {
            deny all;
        }
    
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        location ~ \.php$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        location ~* /\. {
            deny all;
        }
    }
    
    server {
        listen CHANNEL_PUB_API_CFG_NGINX_API_LISTEN; ## listen for ipv4
        server_name CHANNEL_PUB_API_CFG_NGINX_API_SERVER_NAME;
        charset utf-8;
    
        root /sobey/www/channel-pub-api;
        index index.php;
    
        location / {
            root /sobey/www/channel-pub-api/api/web;
            try_files $uri $uri/ /api/web/index.php$is_args$args;
    
            # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
            #location ~ ^/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
            #    log_not_found off;
            #    access_log off;
            #    try_files $uri =404;
            #}
    
            location ~ ^/assets/.+\.php(/|$) {
                deny all;
            }
        }
    
        location /qq {
            alias /sobey/www/channel-pub-api/qq/web/;
    
            # redirect to the URL without a trailing slash (uncomment if necessary)
            #location = /qq/ {
            #    return 301 /qq;
            #}
    
            # prevent the directory redirect to the URL with a trailing slash
            location = /qq {
                # if your location is "/qq", try use "/qq/qq/web/index.php$is_args$args"
                # bug ticket: https://trac.nginx.org/nginx/ticket/97
                try_files $uri /qq/qq/web/index.php$is_args$args;
            }
    
            # if your location is "/qq", try use "/qq/qq/web/index.php$is_args$args"
            # bug ticket: https://trac.nginx.org/nginx/ticket/97
            try_files $uri $uri/ /qq/qq/web/index.php$is_args$args;
    
            # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
            #location ~ ^/qq/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
            #    log_not_found off;
            #    access_log off;
            #    try_files $uri =404;
            #}
    
            location ~ ^/qq/assets/.+\.php(/|$) {
                deny all;
            }
        }
    
        location /weibo {
                alias /sobey/www/channel-pub-api/weibo/web/;
    
                # redirect to the URL without a trailing slash (uncomment if necessary)
                #location = /weibo/ {
                #    return 301 /weibo;
                #}
    
                # prevent the directory redirect to the URL with a trailing slash
                location = /weibo {
                    # if your location is "/weibo", try use "/weibo/weibo/web/index.php$is_args$args"
                    # bug ticket: https://trac.nginx.org/nginx/ticket/97
                    try_files $uri /weibo/weibo/web/index.php$is_args$args;
                }
    
                # if your location is "/weibo", try use "/weibo/weibo/web/index.php$is_args$args"
                # bug ticket: https://trac.nginx.org/nginx/ticket/97
                try_files $uri $uri/ /weibo/weibo/web/index.php$is_args$args;
    
                # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
                #location ~ ^/weibo/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
                #    log_not_found off;
                #    access_log off;
                #    try_files $uri =404;
                #}
    
                location ~ ^/weibo/assets/.+\.php(/|$) {
                    deny all;
                }
            }
    
        location /wx {
            alias /sobey/www/channel-pub-api/wx/web/;
    
            # redirect to the URL without a trailing slash (uncomment if necessary)
            #location = /wx/ {
            #    return 301 /wx;
            #}
    
            # prevent the directory redirect to the URL with a trailing slash
            location = /wx {
                # if your location is "/wx", try use "/wx/wx/web/index.php$is_args$args"
                # bug ticket: https://trac.nginx.org/nginx/ticket/97
                try_files $uri /wx/wx/web/index.php$is_args$args;
            }
    
            # if your location is "/wx", try use "/wx/wx/web/index.php$is_args$args"
            # bug ticket: https://trac.nginx.org/nginx/ticket/97
            try_files $uri $uri/ /wx/wx/web/index.php$is_args$args;
    
            # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
            #location ~ ^/wx/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
            #    log_not_found off;
            #    access_log off;
            #    try_files $uri =404;
            #}
    
            location ~ ^/wx/assets/.+\.php(/|$) {
                deny all;
            }
        }
    
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
    	location ~ ^/.+\.php(/|$) {
    		rewrite (?!^/((api|qq|weibo|wx)/web|qq|weibo|wx))^ /api/web$uri break;
    		rewrite (?!^/qq/web)^/qq(/.+)$ /qq/web$1 break;
    		rewrite (?!^/weibo/web)^/weibo(/.+)$ /weibo/web$1 break;
    		rewrite (?!^/wx/web)^/wx(/.+)$ /wx/web$1 break;
    
    		include fastcgi_params;
    		fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    		fastcgi_pass 127.0.0.1:9000;
    		#fastcgi_pass unix:/var/run/php5-fpm.sock;
    		try_files $fastcgi_script_name =404;
        }
    
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        location ~ /\. {
            deny all;
        }
    }
    
    
    
    6、Rancher 中端口映射,公开主机端口:8661 映射至私有容器端口:80,新增 公开主机端口:8662 映射至私有容器端口:81,如图3
    Rancher 中端口映射,公开主机端口:8661 映射至私有容器端口:80,新增 公开主机端口:8662 映射至私有容器端口:81
    图3
    添加环境变量
    
    
    	CHANNEL_PUB_API_CFG_NGINX_API_SERVER_NAME=wjdev2.chinamcloud.com # Nginx 服务器名称(接口域名、建议入方向仅支持内网)
    	CHANNEL_PUB_API_CFG_NGINX_API_LISTEN=80 # Nginx 服务器监听端口(接口域名、建议入方向仅支持内网)
    	CHANNEL_PUB_API_CFG_NGINX_AUTH_SERVER_NAME=wjdev2.chinamcloud.com # Nginx 服务器名称(授权域名、建议入方向可支持外网)
    	CHANNEL_PUB_API_CFG_NGINX_AUTH_LISTEN=81 # Nginx 服务器监听端口(授权域名、建议入方向可支持外网)
    
    
    
    7、查看 /etc/nginx/conf.d/channel-pub-api.conf,环境变量已被替换
    
    
    server {
        listen 81; ## listen for ipv4
        server_name wjdev2.chinamcloud.com;
        charset utf-8;
    
        root /sobey/www/channel-pub-api/frontend/web;
        index index.php;
        location / {
            # 如果找不到真实存在的文件,把请求分发至 index.php
            try_files $uri $uri/ /index.php$is_args$args;
        }
    
        # uncomment to avoid processing of calls to non-existing static files by Yii
        #location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
        #    try_files $uri =404;
        #}
        #error_page 404 /404.html;
    
        # deny accessing php files for the /assets directory
        location ~ ^/assets/.*\.php$ {
            deny all;
        }
    
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        location ~ \.php$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        location ~* /\. {
            deny all;
        }
    }
    
    server {
        listen 80; ## listen for ipv4
        server_name wjdev2.chinamcloud.com;
        charset utf-8;
    
        root /sobey/www/channel-pub-api;
        index index.php;
    
        location / {
            root /sobey/www/channel-pub-api/api/web;
            try_files $uri $uri/ /api/web/index.php$is_args$args;
    
            # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
            #location ~ ^/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
            #    log_not_found off;
            #    access_log off;
            #    try_files $uri =404;
            #}
    
            location ~ ^/assets/.+\.php(/|$) {
                deny all;
            }
        }
    
        location /qq {
            alias /sobey/www/channel-pub-api/qq/web/;
    
            # redirect to the URL without a trailing slash (uncomment if necessary)
            #location = /qq/ {
            #    return 301 /qq;
            #}
    
            # prevent the directory redirect to the URL with a trailing slash
            location = /qq {
                # if your location is "/qq", try use "/qq/qq/web/index.php$is_args$args"
                # bug ticket: https://trac.nginx.org/nginx/ticket/97
                try_files $uri /qq/qq/web/index.php$is_args$args;
            }
    
            # if your location is "/qq", try use "/qq/qq/web/index.php$is_args$args"
            # bug ticket: https://trac.nginx.org/nginx/ticket/97
            try_files $uri $uri/ /qq/qq/web/index.php$is_args$args;
    
            # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
            #location ~ ^/qq/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
            #    log_not_found off;
            #    access_log off;
            #    try_files $uri =404;
            #}
    
            location ~ ^/qq/assets/.+\.php(/|$) {
                deny all;
            }
        }
    
        location /weibo {
                alias /sobey/www/channel-pub-api/weibo/web/;
    
                # redirect to the URL without a trailing slash (uncomment if necessary)
                #location = /weibo/ {
                #    return 301 /weibo;
                #}
    
                # prevent the directory redirect to the URL with a trailing slash
                location = /weibo {
                    # if your location is "/weibo", try use "/weibo/weibo/web/index.php$is_args$args"
                    # bug ticket: https://trac.nginx.org/nginx/ticket/97
                    try_files $uri /weibo/weibo/web/index.php$is_args$args;
                }
    
                # if your location is "/weibo", try use "/weibo/weibo/web/index.php$is_args$args"
                # bug ticket: https://trac.nginx.org/nginx/ticket/97
                try_files $uri $uri/ /weibo/weibo/web/index.php$is_args$args;
    
                # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
                #location ~ ^/weibo/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
                #    log_not_found off;
                #    access_log off;
                #    try_files $uri =404;
                #}
    
                location ~ ^/weibo/assets/.+\.php(/|$) {
                    deny all;
                }
            }
    
        location /wx {
            alias /sobey/www/channel-pub-api/wx/web/;
    
            # redirect to the URL without a trailing slash (uncomment if necessary)
            #location = /wx/ {
            #    return 301 /wx;
            #}
    
            # prevent the directory redirect to the URL with a trailing slash
            location = /wx {
                # if your location is "/wx", try use "/wx/wx/web/index.php$is_args$args"
                # bug ticket: https://trac.nginx.org/nginx/ticket/97
                try_files $uri /wx/wx/web/index.php$is_args$args;
            }
    
            # if your location is "/wx", try use "/wx/wx/web/index.php$is_args$args"
            # bug ticket: https://trac.nginx.org/nginx/ticket/97
            try_files $uri $uri/ /wx/wx/web/index.php$is_args$args;
    
            # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
            #location ~ ^/wx/.+\.(css|js|ico|png|jpe?g|gif|svg|ttf|mp4|mov|swf|pdf|zip|rar)$ {
            #    log_not_found off;
            #    access_log off;
            #    try_files $uri =404;
            #}
    
            location ~ ^/wx/assets/.+\.php(/|$) {
                deny all;
            }
        }
    
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
            location ~ ^/.+\.php(/|$) {
                    rewrite (?!^/((api|qq|weibo|wx)/web|qq|weibo|wx))^ /api/web$uri break;
                    rewrite (?!^/qq/web)^/qq(/.+)$ /qq/web$1 break;
                    rewrite (?!^/weibo/web)^/weibo(/.+)$ /weibo/web$1 break;
                    rewrite (?!^/wx/web)^/wx(/.+)$ /wx/web$1 break;
    
                    include fastcgi_params;
                    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                    fastcgi_pass 127.0.0.1:9000;
                    #fastcgi_pass unix:/var/run/php5-fpm.sock;
                    try_files $fastcgi_script_name =404;
        }
    
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        location ~ /\. {
            deny all;
        }
    }
    
    
    
    8、域名 A、B 的访问详情如下,访问正常,如图4
    域名 A、B 的访问详情如下,访问正常
    图4
    
    
    企鹅号:https://wjdev2.chinamcloud.com:8661/v1/qq/
    微信公众帐号:https://wjdev2.chinamcloud.com:8661/v1/wx/
    微博:https://wjdev2.chinamcloud.com:8661/v1/weibo/
    跨渠道:https://wjdev2.chinamcloud.com:8661/v1/
    授权:https://wjdev2.chinamcloud.com:8662/
    
    
    
  • Yii 2.0 的预定义的 HTTP 异常类的中文翻译与注解(参考:MDN、维基百科)

    Yii 2.0 的预定义的 HTTP 异常类的中文翻译与注解(参考:MDN、维基百科)

    1、yii\web\BadRequestHttpException:状态码 400。

    BadRequestHttpException represents a “Bad Request” HTTP exception with status code 400.

    Use this exception to represent a generic client error. In many cases, there may be an HTTP exception that more precisely describes the error. In that case, consider using the more precise exception to provide the user with additional information.

    See also https://tools.ietf.org/html/rfc7231#section-6.5.1.

    The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).

    中文翻译:
    BadRequestHttpException 表示状态码为 400 的“错误请求”HTTP异常。

    使用此异常来表示通用的客户端错误。在很多情况下,可能存在一个更准确地描述错误信息的 HTTP 异常。在这种情况下,请考虑提供给用户更准确的错误信息。

    参见 https://tools.ietf.org/html/rfc7231#section-6.5.1。

    400(错误请求)状态码表示由于明显的客户端错误(例如,格式错误的请求语法,无效的请求消息或欺骗性路由请求),服务器不能或不会处理该请求。

    注解:
    (1)HTTP 400 Bad Request 响应状态码表示由于语法无效,服务器无法理解该请求。客户端不应该在未经修改的情况下重复此请求。
    (2)错误的请求。可能通过用户方面的多种原因引起的,例如在请求体内有无效的 JSON 数据,无效的操作参数,等等。

    2、yii\web\ConflictHttpException:状态码 409。

    ConflictHttpException represents a “Conflict” HTTP exception with status code 409.

    See also https://tools.ietf.org/html/rfc7231#section-6.5.8.

    The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource. This code is used in situations where the user might be able to resolve the conflict and esubmit the request. The server SHOULD generate a payload that includes enough information for a user to recognize the source of the conflict.

    Conflicts are most likely to occur in response to a PUT request. For example, if versioning were being used and the representation being PUT included changes to a resource that conflict with those made by an earlier (third-party) request, the origin server might use a 409 response to indicate that it can’t complete the request. In this case, the response representation would likely contain information useful for merging the differences based on the revision history.

    中文翻译:
    ConflictHttpException 表示状态码为 409 的“冲突”HTTP异常。

    参见 https://tools.ietf.org/html/rfc7231#section-6.5.8。

    409(冲突)状态码表示由于与目标资源的当前状态冲突而无法完成请求。此代码用于用户可能能够解决冲突并提交请求的情况。服务器应该生成一个有效载荷,其中包含足够的信息供用户识别冲突源。

    冲突最有可能发生在响应PUT请求时。例如,如果正在使用版本控制并且包含PUT的表示更改为与早期(第三方)请求冲突的资源,则源服务器可能使用409响应来指示它无法完成 请求。在这种情况下,响应表示可能包含对基于修订历史合并差异有用的信息。

    注解:
    (1)响应状态码 409 Conflict 表示请求与服务器端目标资源的当前状态相冲突。冲突最有可能发生在对 PUT 请求的响应中。例如,当上传文件的版本比服务器上已存在的要旧,从而导致版本冲突的时候,那么就有可能收到状态码为 409 的响应。
    (2)表示因为请求存在冲突无法处理该请求,例如多个同步更新之间的编辑冲突。

    3、yii\web\ForbiddenHttpException:状态码 403。

    ForbiddenHttpException represents a “Forbidden” HTTP exception with status code 403.

    Use this exception when a user is not allowed to perform the requested action. Using different credentials might or might not allow performing the requested action. If you do not want to expose authorization information to the user, it is valid to respond with a 404 yii\web\NotFoundHttpException.

    See also https://tools.ietf.org/html/rfc7231#section-6.5.3.

    The 403 (Forbidden) status code indicates that the server understood the request but refuses to authorize it. A server that wishes to make public why the request has been forbidden can describe that reason in the response payload (if any).

    If authentication credentials were provided in the request, the server considers them insufficient to grant access. The client SHOULD NOT automatically repeat the request with the same credentials. The client MAY repeat the request with new or different credentials. However, a request might be forbidden for reasons unrelated to the credentials.

    An origin server that wishes to “hide” the current existence of a forbidden target resource MAY instead respond with a status code of 404 (Not Found).

    中文翻译:
    ForbiddenHttpException 表示状态码为 403 的“禁止”HTTP异常。

    如果用户不被允许执行请求的操作,请使用此异常。使用不同的凭据可能允许或者不允许执行请求的操作。如果您不希望向用户公开授权信息,则可以使用 404 yii\web\NotFoundHttpException 进行响应。

    参见 https://tools.ietf.org/html/rfc7231#section-6.5.3。

    403(禁止)状态码表示服务器已经理解请求,但是拒绝执行。如果服务器希望公开请求被禁止的原因,可以在响应有效载荷中描述该原因(如果有的话)。

    如果请求中提供了身份验证凭据,则服务器认为它们不足以授予访问权限。客户端不应该使用相同的凭据自动重复请求。客户端可以使用新的或不同的凭据重复请求。但是,出于与凭证无关的原因,可能会禁止请求。

    如果希望“隐藏”当前存在的禁止目标资源,源服务器可以用状态码 404(未找到)来响应。

    注解:
    (1)状态码 403 Forbidden 代表客户端错误,指的是服务器端有能力处理该请求,但是拒绝授权访问。这个状态类似于 401,但是进入该状态后,不能再继续进行验证。该访问是永久禁止的,并且与应用逻辑密切相关(例如不正确的密码)。
    (2)服务器已经理解请求,但是拒绝执行。与 401 响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个 HEAD 请求,而且服务器希望能够讲清楚为何请求不能被执行,那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个 404 响应,假如它不希望让客户端获得任何信息。
    (3)已经经过身份验证的用户不允许访问指定的 API 末端。

    4、yii\web\GoneHttpException:状态码 410。

    GoneHttpException represents a “Gone” HTTP exception with status code 410.

    Throw a GoneHttpException when a user requests a resource that no longer exists at the requested url. For example, after a record is deleted, future requests for that record should return a 410 GoneHttpException instead of a 404 yii\web\NotFoundHttpException.

    See also https://tools.ietf.org/html/rfc7231#section-6.5.9.

    The 410 (Gone) status code indicates that access to the target resource is no longer available at the origin server and that this condition is likely to be permanent. If the origin server does not know, or has no facility to determine, whether or not the condition is permanent, the status code 404 (Not Found) ought to be used instead.

    The 410 response is primarily intended to assist the task of web maintenance by notifying the recipient that the resource is intentionally unavailable and that the server owners desire that remote links to that resource be removed. Such an event is common for limited-time, promotional services and for resources belonging to individuals no longer associated with the origin server’s site. It is not necessary to mark all permanently unavailable resources as “gone” or to keep the mark for any length of time — that is left to the discretion of the server owner.

    A 410 response is cacheable by default; i.e., unless otherwise indicated by the method definition or explicit cache controls (see Section 4.2.2 of [RFC7234]).

    中文翻译:
    GoneHttpException 表示状态码为 410 的“丢失”HTTP异常。

    当用户请求不再存在于请求的 URL 处的资源时,抛出 GoneHttpException。例如,删除记录后,将来对该记录的请求应返回 410 GoneHttpException 而不是 404 yii\web\NotFoundHttpException。

    参见 https://tools.ietf.org/html/rfc7231#section-6.5.9。

    410(丢失)状态码表示在源服务器上不再可以访问目标资源,并且该条件可能是永久性的。如果源服务器不知道或无法确定条件是否是永久性的,则应该使用状态码 404(未找到)。

    410 响应主要用于通过通知接收方资源是故意不可用的以及服务器所有者希望移除到该资源的远程链接来辅助 web 维护的任务。这种事件对于限时,促销服务以及属于不再与源服务器站点相关联的个人的资源是常见的。没有必要将所有永久不可用的资源标记为“已消失”或将标记保留任何时间长度 – 由服务器所有者自行决定。

    默认情况下,410 响应可缓存,即,除非方法定义或显式高速缓存控制另有说明(参见[RFC7234]的第4.2.2节)。

    注解:
    (1)HTTP 410 丢失 说明请求的内容在服务器上不存在了,同时是永久性的不可用。如果不清楚是否为永久或临时的不可用,应该使用 404 ,410 响应默认会被缓存。
    (2)表示所请求的资源不再可用,将不再可用。当资源被有意地删除并且资源应被清除时,应该使用这个。在收到 410 状态码后,用户应停止再次请求资源。[39]但大多数服务端不会使用此状态码,而是直接使用 404 状态码。

    5、yii\web\MethodNotAllowedHttpException:状态码 405。

    MethodNotAllowedHttpException represents a “Method Not Allowed” HTTP exception with status code 405.

    See also https://tools.ietf.org/html/rfc7231#section-6.5.5.

    The 405 (Method Not Allowed) status code indicates that the method received in the request-line is known by the origin server but not supported by the target resource. The origin server MUST generate an Allow header field in a 405 response containing a list of the target resource’s currently supported methods.

    A 405 response is cacheable by default; i.e., unless otherwise indicated by the method definition or explicit cache controls (see Section 4.2.2 of [RFC7234]).

    中文翻译:
    MethodNotAllowedHttpException 表示状态码为 405 的“方法不允许”HTTP异常。

    参见 https://tools.ietf.org/html/rfc7231#section-6.5.5。

    405(方法不允许)状态码表示请求行中接收的方法由源服务器知道但目标资源不支持。源服务器必须在 405 响应中生成 Allow 头字段,该字段包含目标资源当前支持的方法的列表。

    默认情况下,405 响应可缓存,即,除非方法定义或显式高速缓存控制另有说明(参见[RFC7234]的第4.2.2节)。

    注解:
    (1)状态码 405 Method Not Allowed 表明服务器禁止了使用当前 HTTP 方法的请求。需要注意的是,GET 与 HEAD 两个方法不得被禁止,当然也不得返回状态码 405。
    (2)请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。例如,需要通过 POST 呈现数据的表单上的 GET 请求,或只读资源上的 PUT 请求。
    鉴于 PUT ,DELETE 方法会对服务器上的资源进行写操作,因而绝大部分的网页服务器都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回 405 错误。
    (3)不被允许的方法。请检查 Allow header 允许的 HTTP 方法。

    6、yii\web\NotAcceptableHttpException:状态码 406。

    NotAcceptableHttpException represents a “Not Acceptable” HTTP exception with status code 406.

    Use this exception when the client requests a Content-Type that your application cannot return. Note that, according to the HTTP 1.1 specification, you are not required to respond with this status code in this situation.

    See also https://tools.ietf.org/html/rfc7231#section-6.5.6.

    The 406 (Not Acceptable) status code indicates that the target resource does not have a current representation that would be acceptable to the user agent, according to the proactive negotiation header fields received in the request (Section 5.3), and the server is unwilling to supply a default representation.

    The server SHOULD generate a payload containing a list of available representation characteristics and corresponding resource identifiers from which the user or user agent can choose the one most appropriate. A user agent MAY automatically select the most appropriate choice from that list. However, this specification does not define any standard for such automatic selection, as described in Section 6.4.1.

    中文翻译:
    NotAcceptableHttpException 表示状态码为 406 的“不接受”HTTP异常。

    当客户端请求您的应用程序无法返回的一个 Content-Type 时,请使用此异常。请注意,根据 HTTP 1.1 规范,在这种情况下,您不需要使用此状态码进行响应。

    参见 https://tools.ietf.org/html/rfc7231#section-6.5.6。

    406(Not Acceptable)状态码表示目标资源没有用户代理可接受的当前表示,根据请求中收到的主动协商头字段(第5.3节),并且服务器不愿意提供默认表示。

    服务器应该生成一个有效载荷,其中包含可用的表示特征列表和相应的资源标识符,用户或用户代理可以从中选择最合适的一个。用户代理可以自动从该列表中选择最合适的选项。但是,本规范没有为这种自动选择定义任何标准,如第6.4.1节所述。

    注解:
    (1)HTTP 协议中的 406 Not Acceptable 状态码表示客户端错误,指代服务器端无法提供与 Accept-Charset 以及 Accept-Language 消息头指定的值相匹配的响应。

    在实际应用中,这个错误状态码极少使用:不是给用户返回一个晦涩难懂(且难以更正)的错误状态码,而是将相关的消息头忽略,同时给用户提供一个看得见摸得着的页面。这种做法基于这样一个假设:即便是不能达到用户十分满意,也强于返回错误状态码。

    如果服务器返回了这个错误状态码,那么消息体中应该包含所能提供的资源表现形式的列表,允许用户手动进行选择。
    (2)参见:内容协商,请求的资源的内容特性无法满足请求头中的条件,因而无法生成响应实体,该请求不可接受。
    除非这是一个 HEAD 请求,否则该响应就应当返回一个包含可以让用户或者浏览器从中选择最合适的实体特性以及地址栏表的实体。实体的格式由 Content-Type 头中定义的媒体类型决定。浏览器可以根据格式及自身能力自行作出最佳选择。但是,规范中并没有定义任何作出此类自动选择的标准。

    7、yii\web\NotFoundHttpException:状态码 404。

    NotFoundHttpException represents a “Not Found” HTTP exception with status code 404.

    See also https://tools.ietf.org/html/rfc7231#section-6.5.4.

    The 404 (Not Found) status code indicates that the origin server did not find a current representation for the target resource or is not willing to disclose that one exists. A 404 status code does not indicate whether this lack of representation is temporary or permanent; the 410 (Gone) status code is preferred over 404 if the
    origin server knows, presumably through some configurable means, that the condition is likely to be permanent.

    A 404 response is cacheable by default; i.e., unless otherwise indicated by the method definition or explicit cache controls (see Section 4.2.2 of [RFC7234]).

    中文翻译:
    NotFoundHttpException 表示状态码为 404 的“未找到”HTTP异常。

    参见 https://tools.ietf.org/html/rfc7231#section-6.5.4。

    404(未找到)状态码表示源服务器没有找到目标资源的当前表示,或者不愿意透露存在该目标资源。 404 状态码并不表示缺少表示是暂时的还是永久性的,如果是,410(Gone)状态码优先于 404
    源服务器可能通过一些可配置的方式知道条件可能是永久性的。

    默认情况下,404 响应可缓存,即,除非方法定义或显式高速缓存控制另有说明(参见[RFC7234]的第4.2.2节)。

    注解:
    (1)状态码 404 Not Found 代表客户端错误,指的是服务器端无法找到所请求的资源。返回该响应的链接通常称为坏链(broken link)或死链(dead link),它们会导向链接出错处理(link rot)页面。

    404 状态码并不能说明请求的资源是临时还是永久丢失。如果服务器知道该资源是永久丢失,那么应该返回 410 (Gone) 而不是 404 。
    (2)请求失败,请求所希望得到的资源未被在服务器上发现,但允许用户的后续请求。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用 410 状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404 这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下。

    8、yii\web\ServerErrorHttpException:状态码 500。

    ServerErrorHttpException represents an “Internal Server Error” HTTP exception with status code 500.

    See also https://tools.ietf.org/html/rfc7231#section-6.6.1.

    The 500 (Internal Server Error) status code indicates that the server encountered an unexpected condition that prevented it from fulfilling the request.

    中文翻译:
    ServerErrorHttpException 表示状态码为 500 的“内部服务器错误”HTTP异常。

    参见 https://tools.ietf.org/html/rfc7231#section-6.6.1。

    500(内部服务器错误)状态码表示服务器遇到意外情况,导致服务器无法完成请求。

    注解:
    (1)在 HTTP 协议中,500 Internal Server Error 是表示服务器端错误的响应状态码,意味着所请求的服务器遇到意外的情况并阻止其执行请求。

    这个错误代码是一个通用的“全方位”响应代码。通常服务器管理员对于类似于 500 这样的错误会更加详细地记录相关的请求信息来防止以后同样错误的出现。
    (2)通用错误消息,服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。没有给出具体错误信息。
    (3)内部服务器错误。这可能是由于内部程序错误引起的。

    9、yii\web\TooManyRequestsHttpException:状态码 429。

    TooManyRequestsHttpException represents a “Too Many Requests” HTTP exception with status code 429.

    Use this exception to indicate that a client has made too many requests in a given period of time. For example, you would throw this exception when ‘throttling’ an API user.

    See also https://tools.ietf.org/html/rfc6585#section-4.

    The 429 status code indicates that the user has sent too many requests in a given amount of time (“rate limiting”).

    The response representations SHOULD include details explaining the condition, and MAY include a Retry-After header indicating how long to wait before making a new request.

    Note that this specification does not define how the origin server identifies the user, nor how it counts requests. For example, an origin server that is limiting request rates can do so based upon counts of requests on a per-resource basis, across the entire server,or even among a set of servers. Likewise, it might identify the user by its authentication credentials, or a stateful cookie.

    Responses with the 429 status code MUST NOT be stored by a cache.

    中文翻译:
    TooManyRequestsHttpException 表示状态码为 429 的“太多请求”HTTP异常。

    使用此异常表示客户端在给定的时间段内发出了太多请求。例如,在’限制’API用户时会抛出此异常。

    参见 https://tools.ietf.org/html/rfc6585#section-4。

    429 状态码表示用户在给定的时间内发送了太多请求(“速率限制”)。

    响应表示应该包括解释条件的详细信息,并且可以包括一个 Retry-After 头,指示在发出新请求之前等待多长时间。

    请注意,此规范未定义源服务器如何识别用户,也不定义如何计算请求。例如,限制请求率的源服务器可以基于每个资源,整个服务器或甚至一组服务器中的请求计数来这样做。同样,它可以通过其身份验证凭据或有状态 cookie 来标识用户。

    429 状态码的响应绝不能由缓存存储。

    注解:
    (1)在 HTTP 协议中,响应状态码 429 Too Many Requests 表示在一定的时间内用户发送了太多的请求,即超出了“频次限制”。

    在响应中,可以提供一个 Retry-After 首部来提示用户需要等待多长时间之后再发送新的请求。
    (2)用户在给定的时间内发送了太多的请求。旨在用于网络限速。

    10、yii\web\UnauthorizedHttpException:状态码 401。

    UnauthorizedHttpException represents an “Unauthorized” HTTP exception with status code 401.

    Use this exception to indicate that a client needs to authenticate via WWW-Authenticate header to perform the requested action.

    If the client is already authenticated and is simply not allowed to perform the action, consider using a 403 yii\web\ForbiddenHttpException or 404 yii\web\NotFoundHttpException instead.

    中文翻译:
    UnauthorizedHttpException 表示状态码为 401 的“未认证”HTTP异常。

    使用此异常指示客户端需要通过 WWW-Authenticate 头进行身份验证以执行请求的操作。

    如果客户端已经过身份验证且根本不允许执行操作,请考虑使用 403 yii\web\ForbiddenHttpException 或 404 yii\web\NotFoundHttpException。

    注解:
    (1)状态码 401 Unauthorized 代表客户端错误,指的是由于缺乏目标资源要求的身份验证凭证,发送的请求未得到满足。

    这个状态码会与 WWW-Authenticate 首部一起发送,其中包含有如何进行验证的信息。

    这个状态类似于 403,但是在该情况下,依然可以进行身份验证。
    (2)类似于 403 Forbidden,401 语义即“未认证”,即用户没有必要的凭据。该状态码表示当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。客户端可以重复提交一个包含恰当的 Authorization 头信息的请求。如果当前请求已经包含了 Authorization 证书,那么 401 响应代表着服务器验证已经拒绝了那些证书。如果 401 响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。
    注意:当网站(通常是网站域名)禁止IP地址时,有些网站状态码显示的 401,表示该特定地址被拒绝访问网站。

    11、yii\web\UnsupportedMediaTypeHttpException:状态码 415。

    UnsupportedMediaTypeHttpException represents an “Unsupported Media Type” HTTP exception with status code 415.

    Use this exception when the client sends data in a format that your application does not understand. For example, you would throw this exception if the client POSTs XML data to an action or controller that only accepts JSON.

    See also https://tools.ietf.org/html/rfc7231#section-6.5.13.

    The 415 (Unsupported Media Type) status code indicates that the origin server is refusing to service the request because the payload is in a format not supported by this method on the target resource. The format problem might be due to the request’s indicated Content-Type or Content-Encoding, or as a result of inspecting the data directly.

    中文翻译:
    UnsupportedMediaTypeHttpException 表示状态码为 415 的“不支持的媒体类型”HTTP异常。

    当客户端应用程序无法理解的格式发送数据时,请使用此异常。例如,如果客户端将 XML 数据 POST 到仅接受 JSON 的操作或控制器,则会抛出此异常。

    参见 https://tools.ietf.org/html/rfc7231#section-6.5.13。

    415(不支持的媒体类型)状态码表示源服务器拒绝为请求提供服务,因为有效载荷的格式不是此方法在目标资源上支持的格式。 格式问题可能是由于请求指示了 Content-Type 或Content-Encoding,或者是由于直接检查数据。

    注解:
    (1)415 Unsupported Media Type 是一种 HTTP 协议的错误状态代码,表示服务器由于不支持其有效载荷的格式,从而拒绝接受客户端的请求。

    格式问题的出现有可能源于客户端在 Content-Type 或 Content-Encoding 首部中指定的格式,也可能源于直接对负载数据进行检测的结果。
    (2)对于当前请求的方法和所请求的资源,请求中提交的互联网媒体类型并不是服务器中所支持的格式,因此请求被拒绝。例如,客户端将图像上传格式为 svg,但服务器要求图像使用上传格式为 jpg。

    12、yii\web\RangeNotSatisfiableHttpException:状态码 416。

    RangeNotSatisfiableHttpException represents an exception caused by an improper request of the end-user.

    This exception thrown when the requested range is not satisfiable: the client asked for a portion of the file (byte serving), but the server cannot supply that portion. For example, if the client asked for a part of the file that lies beyond the end of the file.

    Throwing an RangeNotSatisfiableHttpException like in the following example will result in the error page with error 416 to be displayed.

    中文翻译:
    RangeNotSatisfiableHttpException 表示由最终用户的不正确请求引起的异常。

    当请求的范围不可满足时抛出此异常:客户端请求文件的一部分(字节服务),但服务器无法提供该部分。例如,如果客户端要求文件的一部分超出了文件末尾。

    抛出 RangeNotSatisfiableHttpException,如下例所示,将导致错误页面显示 416 错误。

    注解:
    (1)HTTP 416 Range Not Satisfiable 错误状态码意味着服务器无法处理所请求的数据区间。最常见的情况是所请求的数据区间不在文件范围之内,也就是说,Range 首部的值,虽然从语法上来说是没问题的,但是从语义上来说却没有意义。

    416 响应报文包含一个 Content-Range 首部,提示无法满足的数据区间(用星号 * 表示),后面紧跟着一个“/”,再后面是当前资源的长度。例如:Content-Range: */12777

    遇到这一错误状态码时,浏览器一般有两种策略:要么终止操作(例如,一项中断的下载操作被认为是不可恢复的),要么再次请求整个文件。
    (2)简称“Requested Range Not Satisfiable”。客户端已经要求文件的一部分(Byte serving),但服务器不能提供该部分。例如,如果客户端要求文件的一部分超出文件尾端。

    13、yii\web\UnprocessableEntityHttpException:状态码 422。

    UnprocessableEntityHttpException represents an “Unprocessable Entity” HTTP exception with status code 422.

    Use this exception to inform that the server understands the content type of the request entity and the syntax of that request entity is correct but the server was unable to process the contained instructions. For example, to return form validation errors.

    中文翻译:
    UnprocessableEntityHttpException 表示状态码为 422 的“不可处理的实体”HTTP异常。

    使用此异常通知接收方服务器了解请求实体的内容类型,并且该请求实体的语法正确的,但服务器无法处理包含的指令。例如,返回表单验证错误。

    注解:
    (1)HTTP 422 返回码 表示服务器理解请求实体的内容类型,并且请求实体的语法是正确的,但是服务器不能处理所包含的指令。
    (2)请求格式正确,但是由于含有语义错误,无法响应。
    (3)数据验证失败 (例如,响应一个 POST 请求)。 请检查响应体内详细的错误消息。

  • 在 Windows 10 上执行 vagrant ,报错:Vagrant failed to initialize at a very early stage 的解决

    在 Windows 10 上执行 vagrant ,报错:Vagrant failed to initialize at a very early stage 的解决

    1、在 Windows 10 上执行 vagrant ,报错:Vagrant failed to initialize at a very early stage,如图1
    在 Windows 10 上执行 vagrant ,报错:Vagrant failed to initialize at a very early stage
    图1
    
    
    PS E:\wwwroot\channel-pub-api> vagrant
    Vagrant failed to initialize at a very early stage:
    
    The plugins failed to initialize correctly. This may be due to manual
    modifications made within the Vagrant home directory. Vagrant can
    attempt to automatically correct this issue by running:
    
      vagrant plugin repair
    
    If Vagrant was recently updated, this error may be due to incompatible
    versions of dependencies. To fix this problem please remove and re-install
    all plugins. Vagrant can attempt to do this automatically by running:
    
      vagrant plugin expunge --reinstall
    
    Or you may want to try updating the installed plugins to their latest
    versions:
    
      vagrant plugin update
    
    Error message given during initialization: Unable to resolve dependency: user requested 'vagrant-vbguest (= 0.15.1)'
    
    
    
    2、原因应该在于最近更新了 Vagrant,Vagrant 可以通过运行以下命令自动尝试(删除并重新安装 所有插件),依照提示执行命令
    
    
    PS E:\wwwroot\channel-pub-api> vagrant plugin expunge --reinstall
    
    This command permanently deletes all currently installed user plugins. It
    should only be used when a repair command is unable to properly fix the
    system.
    
    Continue? [N]: yes
    
    All user installed plugins have been removed from this Vagrant environment!
    
    Vagrant will now attempt to reinstall user plugins that were removed.
    
    
    
    3、执行如下命令,翻墙后,在 Windows PowerShell 中执行命令仍然报同样的错误
    
    
    PS E:\wwwroot\channel-pub-api> vagrant plugin install vagrant-hostmanager
    Installing the 'vagrant-hostmanager' plugin. This can take a few minutes...
    Vagrant failed to load a configured plugin source. This can be caused
    by a variety of issues including: transient connectivity issues, proxy
    filtering rejecting access to a configured plugin source, or a configured
    plugin source not responding correctly. Please review the error message
    below to help resolve the issue:
    
      SSL_connect SYSCALL returned=5 errno=0 state=SSLv2/v3 read server hello A (https://gems.hashicorp.com/specs.4.8.gz)
    
    Source: https://gems.hashicorp.com/
    
    
    
    4、在 Windows 命令提示符中执行如下命令,执行成功
    
    
    E:\wwwroot\channel-pub-api>vagrant plugin install vagrant-hostmanager
    Installing the 'vagrant-hostmanager' plugin. This can take a few minutes...
    Fetching: vagrant-hostmanager-1.8.9.gem (100%)
    Installed the plugin 'vagrant-hostmanager (1.8.9)'!
    
    
    
    
    5、再次执行 vagrant ,报错:
    
    
    E:\wwwroot\channel-pub-api>vagrant
    Vagrant failed to initialize at a very early stage:
    
    There is a syntax error in the following Vagrantfile. The syntax error
    message is reproduced below for convenience:
    
    E:/wwwroot/channel-pub-api/Vagrantfile:15: syntax error, unexpected tIDENTIFIER, expecting '}'
      weibo:    'channel-pub-api-weibo.te
           ^
    
    
    
    
    6、编辑 Vagrantfile,在 domains 配置中的 14 行缺少结束的英文逗号,添加上,如图2
    编辑 Vagrantfile,在 domains 配置中的 14 行缺少结束的英文逗号,添加上
    图2
    
    
    domains = {
      frontend: 'channel-pub-api-frontend.test',
      backend:  'channel-pub-api-backend.test',
      api:      'channel-pub-api-api.test',
      qq:       'channel-pub-api-qq.test',
      rpc:      'channel-pub-api-rpc.test'
      weibo:    'channel-pub-api-weibo.test',
      wx:       'channel-pub-api-wx.test'
    }
    
    
    
    
    
    domains = {
      frontend: 'channel-pub-api-frontend.test',
      backend:  'channel-pub-api-backend.test',
      api:      'channel-pub-api-api.test',
      qq:       'channel-pub-api-qq.test',
      rpc:      'channel-pub-api-rpc.test',
      weibo:    'channel-pub-api-weibo.test',
      wx:       'channel-pub-api-wx.test'
    }
    
    
    
    7、再次执行 vagrant 正常
    <pre class="wp-block-syntaxhighlighter-code">
    
    PS E:\wwwroot\channel-pub-api> vagrant up
    Vagrant failed to initialize at a very early stage:
    
    There is a syntax error in the following Vagrantfile. The syntax error
    message is reproduced below for convenience:
    
    E:/wwwroot/channel-pub-api/Vagrantfile:15: syntax error, unexpected tIDENTIFIER, expecting '}'
      weibo:    'channel-pub-api-weibo.te
           ^
    PS E:\wwwroot\channel-pub-api> vagrant up
    Installing the 'vagrant-vbguest' plugin. This can take a few minutes...
    Fetching: micromachine-2.0.0.gem (100%)
    Fetching: vagrant-vbguest-0.16.0.gem (100%)
    Installed the plugin 'vagrant-vbguest (0.16.0)'!
    PS E:\wwwroot\channel-pub-api> vagrant
    Usage: vagrant [options] <command> [<args>]
    
        -v, --version                    Print the version and exit.
        -h, --help                       Print this help.
    
    Common commands:
         box             manages boxes: installation, removal, etc.
         cloud           manages everything related to Vagrant Cloud
         destroy         stops and deletes all traces of the vagrant machine
         global-status   outputs status Vagrant environments for this user
         halt            stops the vagrant machine
         help            shows the help for a subcommand
         hostmanager     plugin: vagrant-hostmanager: manages the /etc/hosts file within a multi-machine environment
         init            initializes a new Vagrant environment by creating a Vagrantfile
         login
         package         packages a running vagrant environment into a box
         plugin          manages plugins: install, uninstall, update, etc.
         port            displays information about guest port mappings
         powershell      connects to machine via powershell remoting
         provision       provisions the vagrant machine
         push            deploys code in this environment to a configured destination
         rdp             connects to machine via RDP
         reload          restarts vagrant machine, loads new Vagrantfile configuration
         resume          resume a suspended vagrant machine
         snapshot        manages snapshots: saving, restoring, etc.
         ssh             connects to machine via SSH
         ssh-config      outputs OpenSSH valid configuration to connect to the machine
         status          outputs status of the vagrant machine
         suspend         suspends the machine
         up              starts and provisions the vagrant environment
         upload          upload to machine via communicator
         validate        validates the Vagrantfile
         vbguest         plugin: vagrant-vbguest: install VirtualBox Guest Additions to the machine
         version         prints current and latest Vagrant version
         winrm           executes commands on a machine via WinRM
         winrm-config    outputs WinRM configuration to connect to the machine
    
    For help on any individual command run `vagrant COMMAND -h`
    
    Additional subcommands are available, but are either more advanced
    or not commonly used. To see all subcommands, run the command
    `vagrant list-commands`.
    
    </pre>
    
    8、创建虚拟机,执行如下命令
    
    
    PS E:\wwwroot\channel-pub-api> vagrant up
    ==> channel-pub-api: Machine 'channel-pub-api' has a post `vagrant up` message. This is a message
    ==> channel-pub-api: from the creator of the Vagrantfile, and not from Vagrant itself:
    ==> channel-pub-api:
    ==> channel-pub-api: Frontend URL: http://channel-pub-api-frontend.test
    ==> channel-pub-api: Backend URL: http://channel-pub-api-backend.test
    ==> channel-pub-api: Api URL: http://channel-pub-api-api.test
    ==> channel-pub-api: Qq URL: http://channel-pub-api-qq.test
    ==> channel-pub-api: Rpc URL: http://channel-pub-api-rpc.test
    ==> channel-pub-api: Weibo URL: http://channel-pub-api-weibo.test
    ==> channel-pub-api: Wx URL: http://channel-pub-api-wx.test
    
    
    
    9、等待完成后,在浏览器中访问如下URL即可,符合预期
    
    
    192.168.83.147 channel-pub-api-frontend.test
    192.168.83.147 channel-pub-api-backend.test
    192.168.83.147 channel-pub-api-api.test
    192.168.83.147 channel-pub-api-qq.test
    192.168.83.147 channel-pub-api-rpc.test
    192.168.83.147 channel-pub-api-weibo.test
    192.168.83.147 channel-pub-api-wx.test
    
    
    
  • 在 Yii 2 高级模板中,渠道发布接口(发布同一篇文章至企鹅号、微信公众帐号等渠道)的架构设计,基于队列、控制台命令推动工作流程

    在 Yii 2 高级模板中,渠道发布接口(发布同一篇文章至企鹅号、微信公众帐号等渠道)的架构设计,基于队列、控制台命令推动工作流程

    1、复制 api 应用为 qq、wx,调整相应配置后,最后应用目录结构如下
    
    
    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、在 Yii 2 高级项目模板 上的基于 Nginx 的单域名配置,参考网址:https://www.shuijingwanwq.com/2018/08/16/2836/ ,api 应用为跨渠道、qq 应用为企鹅号、wx 应用为微信公众帐号,例:POST http://api.channel-pub-api.localhost/qq/v1/articles/video?group_id=spider ,实则最终的请求路由至:\qq\rests\article\VideoCreateAction.php 3、以发布文章类型:视频(视频)的文章至企鹅号为例说明整体的架构设计,在 Yii2 高级模板中基于 Yii2 队列扩展实现异步执行任务,参考网址:https://www.shuijingwanwq.com/2018/10/19/2952/ ,编辑 \common\config\main-local.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、实现渠道发布接口(企鹅号):发布文章类型:视频(视频)的文章至渠道发布,编辑 \qq\rests\article\Action.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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.');
        }
    }
    
    
    </pre>
    
    5、实现渠道发布接口(企鹅号):发布文章类型:视频(视频)的文章至渠道发布,编辑 \qq\rests\article\VideoCreateAction.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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']];
        }
    }
    
    
    </pre>
    
    6、实现渠道发布接口(企鹅号):发布文章类型:视频(视频)的文章至渠道发布,编辑 \common\services\QqArticleService.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、实现渠道发布接口(企鹅号):发布文章类型:视频(视频)的文章至渠道发布,复制来源的资源文件至渠道发布的资源目录,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步),编辑 \common\services\AssetService.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 ,执行成功,特意让 media_absolute_url 的值是错误的,以测试复制资源文件队列的作业执行失败后的后续处理;特意让 source_callback_url 的值是错误的,以测试来源回调队列的作业执行失败后的后续处理 请求 Body
    
    
    {
    	"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"
    }
    
    
    
    响应 Body
    
    
    {
        "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、执行 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、查看 4 个队列的状态信息
    
    
    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、队列作业:复制来源的资源文件至渠道发布的资源目录,编辑 \common\jobs\CopyAssetJob.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
        }
    }
    
    </pre>
    
    12、队列事件处理器(每次成功执行作业后):调用相应服务(作业执行成功后)进行后续处理,调用相应服务失败(即服务抛出异常),仅插入系统日志;队列事件处理器(在作业执行期间发生未捕获的异常时):调用相应服务(作业执行失败后)进行后续处理,调用相应服务失败(即服务抛出异常),仅插入系统日志。编辑 \common\components\queue\CopyAssetEventHandler.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
    
        }
    }
    
    </pre>
    
    13、复制资源文件队列的作业执行成功后的后续处理:文件上传,上传资源文件队列任务执行成功后,调用相应服务,否则,插入发布日志(异步)。以企鹅号为例,编辑 \common\services\QqCwAssetService.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、复制资源文件队列的作业执行失败后的后续处理:基于任务ID批量更新任务(场景:复制资源文件队列的作业执行失败后,可发布次数减1,状态,3:发布中(已失败));插入发布日志,将作业推送至来源回调队列(异步)。以企鹅号为例,编辑 \common\services\QqCwAssetService.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、发布任务成功/失败后,插入发布日志,将作业推送至来源回调队列(异步);来源回调队列的作业执行成功后的后续处理;来源回调队列的作业执行失败后的后续处理。编辑 \common\services\SourceCallbackService.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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,
                ]));
            }
        }
    
    
    }
    
    </pre>
    
    16、run 命令获取并执行循环中的任务(复制资源文件队列),直到队列为空
    
    
    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、复制资源文件队列的作业执行失败,执行结果符合预期,发布日志已插入,且已将作业推送至来源回调队列,如图1
    复制资源文件队列的作业执行失败,执行结果符合预期,发布日志已插入,且已将作业推送至来源回调队列
    图1
    
    
    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、run 命令获取并执行循环中的任务(来源回调队列),直到队列为空
    
    
    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、来源回调队列的作业执行失败,执行结果符合预期,发布日志已更新,基于渠道的应用的任务ID更新发布日志(可回调次数减 1;回调状态,2:失败);如果可回调次数大于 0,将任务重新发送到来源回调队列,且延时 1 分钟运行(延时时间在生产环境设置为 10 分钟)。如图2、图3
    来源回调队列的作业执行失败,执行结果符合预期,发布日志已更新,基于渠道的应用的任务ID更新发布日志(可回调次数减 1;回调状态,2:失败)
    图2
    如果可回调次数大于 0,将任务重新发送到来源回调队列,且延时 1 分钟运行(延时时间在生产环境设置为 10 分钟)
    图3
    
    
    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 ,执行成功,特意让 media_absolute_url 的值是错误的,以测试复制资源文件队列的作业执行失败后的后续处理;特意让 source_callback_url 的值是正确的,以测试来源回调队列的作业执行成功后的后续处理 请求 Body
    
    
    {
    	"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"
    }
    
    
    
    响应 Body
    
    
    {
        "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、执行 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、查看 4 个队列的状态信息
    
    
    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、run 命令获取并执行循环中的任务(复制资源文件队列),直到队列为空
    
    
    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、复制资源文件队列的作业执行失败,执行结果符合预期,发布日志已插入,且已将作业推送至来源回调队列,如图4
    复制资源文件队列的作业执行失败,执行结果符合预期,发布日志已插入,且已将作业推送至来源回调队列
    图4
    
    
    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、run 命令获取并执行循环中的任务(来源回调队列),直到队列为空
    
    
    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、来源回调队列的作业执行成功,执行结果符合预期,发布日志已更新,基于渠道的应用的任务ID更新发布日志(可回调次数减 1;回调状态,1:成功)。如图5、图6
    来源回调队列的作业执行成功
    图5
    执行结果符合预期,发布日志已更新,基于渠道的应用的任务ID更新发布日志(可回调次数减 1;回调状态,1:成功)
    图6
    
    
    PS E:\wwwroot\channel-pub-api> ./yii source-callback-queue/info --color=0
    Jobs
    - waiting: 0
    - delayed: 0
    - reserved: 0
    - done: 4
    
    
    
    27、资源服务(企鹅号的内容网站应用的视频文件分片上传,队列任务执行成功后,调用相应服务,否则,插入发布日志)的实现。编辑 \common\services\QqCwAssetService.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
        }
    }
    
    </pre>
    
    28、上传资源文件队列作业的实现,编辑 \common\jobs\UploadAssetJob.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
            }
    
        }
    }
    
    </pre>
    
    29、企鹅号的内容网站应用的视频文件分片上传的具体实现,编辑 \common\services\QqCwVideoMultipartUploadService.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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'];
    
        }
    }
    
    </pre>
    
    30、企鹅号授权的 Access Token 实现,编辑 \common\logics\http\qq_auth\AccessToken.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
            }
        }
    }
    
    </pre>
    
    31、企鹅号接口的视频实现,编辑 \common\logics\http\qq_api\Video.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
            }
    
        }
    }
    
    </pre>
    
    32、企鹅号接口的事务实现,编辑 \common\logics\http\qq_api\Transaction.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
            }
    
        }
    }
    
    </pre>
    
    33、队列(上传资源文件队列)事件处理器的实现,编辑 \common\components\queue\UploadAssetEventHandler.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
            }
        }
    }
    
    </pre>
    
    34、清空之前的测试数据,POST http://api.channel-pub-api.localhost/qq/v1/articles/video?group_id=spider ,执行成功,特意让 media_absolute_url 的值是正确的,以测试复制资源文件队列的作业执行成功后的后续处理,即企鹅号的内容网站应用的视频文件分片上传,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步);且 media_absolute_url 所表示的文件是一个大小为 2.1 GB 的文件,以测试上传资源文件队列的作业执行失败后的后续处理。 请求 Body
    
    
    {
    	"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"
    }
    
    
    
    响应 Body
    
    
    {
        "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、执行 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、查看 4 个队列的状态信息
    
    
    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、run 命令获取并执行循环中的任务(复制资源文件队列),直到队列为空
    
    
    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、复制资源文件队列的作业执行成功,执行结果符合预期,已批量更新资源,且已将作业推送至上传资源文件队列,如图7
    复制资源文件队列的作业执行成功,执行结果符合预期,已批量更新资源,且已将作业推送至上传资源文件队列
    图7
    
    
    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、run 命令获取并执行循环中的任务(上传资源文件队列),直到队列为空,在执行任务的过程中,特意使用翻墙软件(网络质量较差),以测试上传资源文件队列的作业执行失败后的后续处理。基于渠道的应用的任务ID更新企鹅号的内容网站应用的任务(场景:上传资源文件队列的作业执行失败后,可发布次数减1,状态,3:发布中(已失败));插入发布日志,将作业推送至来源回调队列(异步);基于资源ID、企鹅号的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的视频文件分片上传),如果存在,则更新为,3:上传中(已失败)。如图8
    run 命令获取并执行循环中的任务(上传资源文件队列),直到队列为空,在执行任务的过程中,特意使用翻墙软件(网络质量较差),以测试上传资源文件队列的作业执行失败后的后续处理。基于渠道的应用的任务ID更新企鹅号的内容网站应用的任务(场景:上传资源文件队列的作业执行失败后,可发布次数减1,状态,3:发布中(已失败));插入发布日志,将作业推送至来源回调队列(异步);基于资源ID、企鹅号的应用的任务ID查找单个数据模型(企鹅号的内容网站应用的视频文件分片上传),如果存在,则更新为,3:上传中(已失败)
    图8
    
    
    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、清空之前的测试数据,POST http://api.channel-pub-api.localhost/qq/v1/articles/video?group_id=spider ,执行成功,特意让 media_absolute_url 的值是正确的,以测试复制资源文件队列的作业执行成功后的后续处理,即企鹅号的内容网站应用的视频文件分片上传,队列任务执行成功后,调用相应服务,否则,插入发布日志(异步);且 media_absolute_url 所表示的文件是一个大小为 8.44 MB 的文件,以测试上传资源文件队列的作业执行成功后的后续处理 请求 Body
    
    
    {
    	"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"
    }
    
    
    
    响应 Body
    
    
    {
        "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、查看 4 个队列的状态信息
    
    
    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、run 命令获取并执行循环中的任务(复制资源文件队列),直到队列为空
    
    
    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、复制资源文件队列的作业执行成功,执行结果符合预期,已批量更新资源,且已将作业推送至上传资源文件队列
    
    
    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、run 命令获取并执行循环中的任务(上传资源文件队列),直到队列为空,在执行任务的过程中,特意退出翻墙软件(网络质量较好),以测试上传资源文件队列的作业执行成功后的后续处理,暂时无相应处理,方法为空。事务表中已经存在相应记录,如图9
    run 命令获取并执行循环中的任务(上传资源文件队列),直到队列为空,在执行任务的过程中,特意退出翻墙软件(网络质量较好),以测试上传资源文件队列的作业执行成功后的后续处理,暂时无相应处理,方法为空。事务表中已经存在相应记录
    图9
    
    
    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、企鹅号的内容网站应用的视频事务控制器,同步接口的视频事务的实现,编辑 \console\controllers\QqCwTransactionVideoController.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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;
        }
    
    }
    
    
    </pre>
    
    46、企鹅号的内容网站应用的文章事务控制器,同步接口的文章事务的实现,编辑 \console\controllers\QqCwTransactionArticleController.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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;
        }
    
    }
    
    
    </pre>
    
    47、企鹅号的内容网站应用的事务服务的实现,编辑 \common\services\QqCwTransactionService.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
            }
        }
    
    }
    
    </pre>
    
    48、队列作业:发布文章,编辑 \common\jobs\PubArticleJob.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
    
        }
    
    }
    
    </pre>
    
    49、队列事件处理器(每次成功执行作业后):调用相应服务(作业执行成功后)进行后续处理,调用相应服务失败(即服务抛出异常),仅插入系统日志;队列事件处理器(在作业执行期间发生未捕获的异常时):调用相应服务(作业执行失败后)进行后续处理,调用相应服务失败(即服务抛出异常),仅插入系统日志。编辑 \common\components\queue\PubArticleEventHandler.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
        }
    }
    
    </pre>
    
    50、企鹅号的内容网站应用的文章服务的实现,编辑 \common\services\QqCwArticleService.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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.');
        }
    }
    
    </pre>
    
    51、从头开始,测试一遍一切正常的流程,一篇文章同时发布至 2 个企鹅号(且 2 个均成功,在最后一道流程),POST http://api.channel-pub-api.localhost/qq/v1/articles/video?group_id=spider 请求 Body
    
    
    {
    	"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 .. 动画控制器 .. 它可以控制动画的运行状态 .."
    }
    
    
    
    响应 Body
    
    
    {
        "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、执行 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、查看 4 个队列的状态信息
    
    
    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、run 命令获取并执行循环中的任务(复制资源文件队列),直到队列为空,复制资源文件队列的作业执行成功,执行结果符合预期,已将作业推送至上传资源队列
    
    
    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、run 命令获取并执行循环中的任务(上传资源文件队列),直到队列为空,上传资源文件队列的作业执行成功,执行结果符合预期,视频事务记录已经插入至事务表中,如图10
    run 命令获取并执行循环中的任务(上传资源文件队列),直到队列为空,上传资源文件队列的作业执行成功,执行结果符合预期,视频事务记录已经插入至事务表中
    图10
    
    
    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、控制台命令:获取企鹅号的内容网站应用的企鹅号接口的视频事务,同步至企鹅号的内容网站应用的企鹅号的视频事务,执行结果符合预期,已将作业推送至发布文章队列
    
    
    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、run 命令获取并执行循环中的任务(发布文章队列),直到队列为空,发布文章队列的作业执行成功,执行结果符合预期,文章事务记录已经插入至事务表中,如图11
    run 命令获取并执行循环中的任务(发布文章队列),直到队列为空,发布文章队列的作业执行成功,执行结果符合预期,文章事务记录已经插入至事务表中
    图11
    
    
    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、控制台命令:获取企鹅号的内容网站应用的企鹅号接口的文章事务,同步至企鹅号的内容网站应用的企鹅号的文章事务,执行结果符合预期,文章事务记录已经更新至事务表中,且已将作业推送至来源回调队列
    
    
    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、run 命令获取并执行循环中的任务(来源回调队列),直到队列为空,来源回调队列的作业执行成功,执行结果符合预期,已经更新至发布日志表中
    
    
    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、分别登录企鹅号,文章已经发布成功,如图12、13
    分别登录企鹅号,文章已经发布成功
    图12
    分别登录企鹅号,文章已经发布成功
    图13
    61、查看事务表中的信息,打开文章快报链接:http://kuaibao.qq.com/s/20181205V0E6C600 、http://kuaibao.qq.com/s/20181205V0E6DU00 ,结果符合预期,可以正常打开 62、总结,整体程序结构如下:
    
    
    
    0:未实现
    1:已经实现
    2:待实现
    3:不实现
    
    // 复制资源文件队列作业:复制来源的资源文件至渠道发布的资源目录
    \common\jobs\CopyAssetJob.php execute($queue)    1
    
    
    // 资源服务,复制来源的资源文件至渠道发布的资源目录,返回相对路径(同步)
    AssetService.php copyAssetsSync($source, $assets)    1
    
    
    // 复制资源文件队列事件处理器,复制资源文件队列的作业执行成功后
    \common\components\queue\CopyAssetEventHandler.php afterExec(ExecEvent $event)    1
    // 复制资源文件队列事件处理器,复制资源文件队列的作业执行失败后
    \common\components\queue\CopyAssetEventHandler.php afterError(ExecEvent $event)    1
    
    
    // 企鹅号的内容网站应用的资源服务,复制资源文件队列的作业执行成功后的后续处理
    QqCwAssetService.php copyAssetExecHandler($taskId)    1
    // 企鹅号的第三方服务平台应用的资源服务,复制资源文件队列的作业执行成功后的后续处理
    QqTpAssetService.php copyAssetExecHandler($taskId)    0
    // 微信公众帐号应用的资源服务,复制资源文件队列的作业执行成功后的后续处理
    WxAssetService.php copyAssetExecHandler($taskId)    1
    
    
    // 企鹅号的内容网站应用的资源服务,复制资源文件队列的作业执行失败后的后续处理
    QqCwAssetService.php copyAssetErrorHandler($taskId, $eventError)    1
    // 企鹅号的第三方服务平台应用的资源服务,复制资源文件队列的作业执行失败后的后续处理
    QqTpAssetService.php copyAssetErrorHandler($taskId, $eventError)    0
    // 微信公众帐号应用的资源服务,复制资源文件队列的作业执行失败后的后续处理
    WxAssetService.php copyAssetErrorHandler($taskId, $eventError)    1
    
    
    // 上传资源文件队列作业:上传资源文件
    \common\jobs\UploadAssetJob.php execute($queue)    1
    
    
    // 企鹅号的内容网站应用的资源服务,企鹅号的内容网站应用的视频文件分片上传(同步)
    QqCwAssetService.php uploadAssetVideoMultipartSync($assetId, $channelAppTaskId)    1
    // 企鹅号的第三方服务平台应用的资源服务,企鹅号的第三方服务平台应用的视频文件分片上传(同步)
    QqTpAssetService.php uploadAssetVideoMultipartSync($assetId, $channelAppTaskId)    0
    // 微信公众帐号应用的资源服务,微信公众帐号应用的资源上传(同步)
    WxAssetService.php uploadAssetSync($assetId, $channelAppTaskId)    0
    // 微信公众帐号应用的资源服务,企鹅号的内容网站应用的视频文件分片上传(同步)
    WxAssetService.php qqCwUploadAssetVideoMultipartSync($taskId)    3
    
    
    // 上传资源文件队列事件处理器,上传资源文件队列的作业执行成功后
    \common\components\queue\UploadAssetEventHandler.php afterExec(ExecEvent $event)    1
    // 上传资源文件队列事件处理器,上传资源文件队列的作业执行失败后
    \common\components\queue\UploadAssetEventHandler.php afterError(ExecEvent $event)    1
    
    
    // 企鹅号的内容网站应用的资源服务,上传资源文件队列的作业执行成功后的后续处理
    QqCwAssetService.php uploadAssetVideoMultipartExecHandler($assetId, $channelAppTaskId)    1(留空)
    // 企鹅号的第三方服务平台应用的资源服务,上传资源文件队列的作业执行成功后的后续处理
    QqTpAssetService.php uploadAssetVideoMultipartExecHandler($assetId, $channelAppTaskId)    0
    // 微信公众帐号应用的资源服务,上传资源文件队列的作业执行成功后的后续处理
    WxAssetService.php uploadAssetExecHandler($assetId, $channelAppTaskId)    0
    // 微信公众帐号应用的资源服务,企鹅号的内容网站应用的上传资源文件队列的作业执行成功后的后续处理
    WxAssetService.php qqCwUploadAssetVideoMultipartExecHandler($taskId)    3
    
    
    // 企鹅号的内容网站应用的资源服务,上传资源文件队列的作业执行失败后的后续处理
    QqCwAssetService.php uploadAssetVideoMultipartErrorHandler($assetId, $channelAppTaskId, $eventError)    1
    // 企鹅号的第三方服务平台应用的资源服务,上传资源文件队列的作业执行失败后的后续处理
    QqTpAssetService.php uploadAssetVideoMultipartErrorHandler($assetId, $channelAppTaskId, $eventError)    0
    // 微信公众帐号应用的资源服务,上传资源文件队列的作业执行失败后的后续处理
    WxAssetService.php uploadAssetErrorHandler($assetId, $channelAppTaskId, $eventError)    0
    // 微信公众帐号应用的资源服务,企鹅号的内容网站应用的上传资源文件队列的作业执行失败后的后续处理
    WxAssetService.php qqCwUploadAssetVideoMultipartErrorHandler($taskId, $eventError)    1
    
    
    // 企鹅号的内容网站应用的视频事务控制器,同步接口的视频事务
    QqCwTransactionVideoController.php actionSync()    1
    // 企鹅号的第三方服务平台应用的视频事务控制器,同步接口的视频事务
    QqTpTransactionVideoController.php actionSync()    0
    
    
    // 企鹅号的内容网站应用的事务服务,企鹅号的内容网站应用的企鹅号的视频事务成功后的后续处理
    QqCwTransactionService.php videoExecHandler($qqTransactionId)    1
    // 企鹅号的第三方服务平台应用的事务服务,企鹅号的第三方服务平台应用的企鹅号的视频事务成功后的后续处理
    QqTpTransactionService.php videoExecHandler($qqTransactionId)    0
    // 微信公众帐号应用的资源服务,企鹅号的第三方服务平台应用的企鹅号的视频事务成功后的后续处理
    WxAssetService.php qqCwTransactionVideoExecHandler($taskId)    3
    
    
    // 企鹅号的内容网站应用的事务服务,企鹅号的内容网站应用的企鹅号的视频事务失败后的后续处理
    QqCwTransactionService.php videoErrorHandler($qqTransactionId)    1
    // 企鹅号的第三方服务平台应用的事务服务,企鹅号的第三方服务平台应用的企鹅号的视频事务失败后的后续处理
    QqTpTransactionService.php videoErrorHandler($qqTransactionId)    0
    // 微信公众帐号应用的资源服务,企鹅号的第三方服务平台应用的企鹅号的视频事务失败后的后续处理
    WxAssetService.php qqCwTransactionVideoErrorHandler($taskId, $errorCode, $errorMessage)    2
    
    
    // 发布文章队列作业:发布文章
    \common\jobs\PubArticleJob.php execute($queue)    1
    
    
    // 企鹅号的内容网站应用的文章服务,企鹅号的内容网站应用的发布文章类型:视频(视频)的文章至企鹅号平台(同步)
    QqCwArticleService.php pubArticleVideoSync($channelAppTaskId)    1
    // 企鹅号的第三方服务平台应用的文章服务,企鹅号的第三方服务平台应用的发布文章类型:视频(视频)的文章至企鹅号平台(同步)
    QqTpArticleService.php pubArticleVideoSync($channelAppTaskId)    0
    // 微信公众帐号应用的文章服务,微信公众帐号应用的发布文章类型:视频(视频)的文章至微信公众帐号平台(同步)
    WxArticleService.php pubArticleVideoSync($channelAppTaskId)    0
    // 微信公众帐号应用的文章服务,企鹅号的内容网站应用的发布文章类型:视频(视频)的文章至企鹅号平台(同步)
    WxArticleService.php qqCwPubArticleVideoSync($taskId)    3
    
    
    // 发布文章队列事件处理器,发布文章队列的作业执行成功后
    \common\components\queue\PubArticleEventHandler.php afterExec(ExecEvent $event)    1
    // 发布文章队列事件处理器,发布文章队列的作业执行失败后
    \common\components\queue\PubArticleEventHandler.php afterError(ExecEvent $event)    1
    
    
    // 企鹅号的内容网站应用的文章服务,发布文章队列的作业执行成功后的后续处理
    QqCwArticleService.php pubArticleVideoExecHandler($channelAppTaskId)    1(留空)
    // 企鹅号的第三方服务平台应用的文章服务,发布文章队列的作业执行成功后的后续处理
    QqTpArticleService.php pubArticleVideoExecHandler($channelAppTaskId)    0
    // 微信公众帐号应用的文章服务,发布文章队列的作业执行成功后的后续处理
    WxArticleService.php pubArticleVideoExecHandler($channelAppTaskId)    0
    // 微信公众帐号应用的文章服务,企鹅号的内容网站应用的发布文章队列的作业执行成功后的后续处理
    WxArticleService.php qqCwPubArticleVideoExecHandler($taskId)    3
    
    
    // 企鹅号的内容网站应用的文章服务,发布文章队列的作业执行失败后的后续处理
    QqCwArticleService.php pubArticleVideoErrorHandler($channelAppTaskId, $eventError)    1
    // 企鹅号的第三方服务平台应用的文章服务,发布文章队列的作业执行失败后的后续处理
    QqTpArticleService.php pubArticleVideoErrorHandler($channelAppTaskId, $eventError)    0
    // 微信公众帐号应用的文章服务,发布文章队列的作业执行失败后的后续处理
    WxArticleService.php pubArticleVideoErrorHandler($channelAppTaskId, $eventError)    1
    // 微信公众帐号应用的文章服务,企鹅号的内容网站应用的发布文章队列的作业执行失败后的后续处理
    WxArticleService.php qqCwPubArticleVideoErrorHandler($taskId, $eventError)    1
    
    
    // 企鹅号的内容网站应用的文章事务控制器,同步接口的文章事务
    QqCwTransactionArticleController.php actionSync()    1
    // 企鹅号的第三方服务平台应用的文章事务控制器,同步接口的文章事务
    QqTpTransactionArticleController.php actionSync()    0
    
    
    // 企鹅号的内容网站应用的事务服务,企鹅号的内容网站应用的企鹅号的视频文章事务成功后的后续处理
    QqCwTransactionService.php articleMultivideosExecHandler($qqTransactionId)    1
    // 企鹅号的第三方服务平台应用的事务服务,企鹅号的内容网站应用的企鹅号的视频文章事务成功后的后续处理
    QqTpTransactionService.php articleMultivideosExecHandler($qqTransactionId)    0
    // 微信公众帐号应用的文章服务,企鹅号的内容网站应用的企鹅号的视频文章事务成功后的后续处理
    WxArticleService.php qqCwTransactionArticleVideoExecHandler($taskId)    2
    
    
    // 企鹅号的内容网站应用的事务服务,企鹅号的内容网站应用的企鹅号的视频文章事务失败后的后续处理
    QqCwTransactionService.php articleMultivideosErrorHandler($qqTransactionId)    1
    // 企鹅号的第三方服务平台应用的事务服务,企鹅号的内容网站应用的企鹅号的视频文章事务失败后的后续处理
    QqTpTransactionService.php articleMultivideosErrorHandler($qqTransactionId)    0
    // 微信公众帐号应用的文章服务,企鹅号的内容网站应用的企鹅号的视频文章事务失败后的后续处理
    WxArticleService.php qqCwTransactionArticleVideoErrorHandler($taskId, $errorCode, $errorMessage)    2
    
    
    // 来源回调队列作业:来源回调
    \common\jobs\SourceCallbackJob.php execute($queue)    1
    
    
    // 来源回调队列事件处理器,来源回调队列的作业执行成功后
    \common\components\queue\SourceCallbackEventHandler.php afterExec(ExecEvent $event)    1
    // 来源回调队列事件处理器,来源回调队列的作业执行失败后
    \common\components\queue\SourceCallbackEventHandler.php afterError(ExecEvent $event)    1
    
    
    // 来源回调服务,来源回调队列的作业执行成功后的后续处理
    SourceCallbackService.php sourceCallbackExecHandler($channelAppTaskId)    1
    // 来源回调服务,来源回调队列的作业执行失败后的后续处理
    SourceCallbackService.php sourceCallbackErrorHandler($channelAppTaskId)    1
    
    
    
    
  • 在 Yii 2 高级模板中,在 CentOS 7.2 中执行初始化命令,一些应用下的目录权限未设置为 777 的分析解决(根源在于 Docker 部署配置问题)

    在 Yii 2 高级模板中,在 CentOS 7.2 中执行初始化命令,一些应用下的目录权限未设置为 777 的分析解决(根源在于 Docker 部署配置问题)

    1、在 Windows 10 中执行初始化命令,设置了所有应用的目录权限,正常(总计 14 行),如图1
    在 Windows 10 中执行初始化命令,设置了所有应用的目录权限,正常(总计 14 行)
    图1
    
    
    PS E:\wwwroot\channel-pub-api> ./init --env=Development --overwrite=All
    Yii Application Initialization Tool v1.0
    
    
      Start initialization ...
    
          exist api/config/main-local.php
                ...overwrite? [Yes|No|All|Quit]   overwrite api/config/main-local.php
      unchanged api/config/params-local.php
      unchanged api/config/test-local.php
      unchanged api/web/index-test.php
      unchanged api/web/index.php
      unchanged api/web/robots.txt
      overwrite backend/config/main-local.php
      unchanged backend/config/params-local.php
      unchanged backend/config/test-local.php
      unchanged backend/web/index-test.php
      unchanged backend/web/index.php
      unchanged backend/web/robots.txt
      unchanged common/config/main-local.php
      unchanged common/config/params-local.php
      unchanged common/config/test-local.php
      unchanged console/config/main-local.php
      unchanged console/config/params-local.php
      overwrite frontend/config/main-local.php
      unchanged frontend/config/params-local.php
      unchanged frontend/config/test-local.php
      unchanged frontend/web/index-test.php
      unchanged frontend/web/index.php
      unchanged frontend/web/robots.txt
      overwrite qq/config/main-local.php
      unchanged qq/config/params-local.php
      unchanged qq/config/test-local.php
      unchanged qq/web/index-test.php
      unchanged qq/web/index.php
      unchanged qq/web/robots.txt
      overwrite rpc/config/main-local.php
      unchanged rpc/config/params-local.php
      unchanged rpc/config/test-local.php
      unchanged rpc/web/index-test.php
      unchanged rpc/web/index.php
      unchanged rpc/web/robots.txt
      overwrite wx/config/main-local.php
      unchanged wx/config/params-local.php
      unchanged wx/config/test-local.php
      unchanged wx/web/index-test.php
      unchanged wx/web/index.php
      unchanged wx/web/robots.txt
      unchanged yii
      unchanged yii_test
      unchanged yii_test.bat
       generate cookie validation key in backend/config/main-local.php
       generate cookie validation key in frontend/config/main-local.php
       generate cookie validation key in api/config/main-local.php
       generate cookie validation key in rpc/config/main-local.php
       generate cookie validation key in qq/config/main-local.php
       generate cookie validation key in wx/config/main-local.php
          chmod 0777 backend/runtime
          chmod 0777 backend/web/assets
          chmod 0777 frontend/runtime
          chmod 0777 frontend/web/assets
          chmod 0777 api/runtime
          chmod 0777 api/web/assets
          chmod 0777 rpc/runtime
          chmod 0777 rpc/web/assets
          chmod 0777 qq/runtime
          chmod 0777 qq/web/assets
          chmod 0777 wx/runtime
          chmod 0777 wx/web/assets
          chmod 0755 yii
          chmod 0755 yii_test
    
      ... initialization completed.
    
    
    
    
    2、在 CentOS 7.2 中执行初始化命令,一些应用下的目录权限未设置,不正常(总计 8 行),如图2
    在 CentOS 7.2 中执行初始化命令,一些应用下的目录权限未设置,不正常(总计 8 行)
    图2
    
    
    [root@579789e72a51 /]# php /sobey/www/channel-pub-api/init --env=Development --overwrite=All
    Yii Application Initialization Tool v1.0
    
    
      Start initialization ...
    
          exist api/config/main-local.php
                ...overwrite? [Yes|No|All|Quit]   overwrite api/config/main-local.php
      unchanged api/config/params-local.php
      unchanged api/config/test-local.php
      unchanged api/web/index-test.php
      unchanged api/web/index.php
      unchanged api/web/robots.txt
      overwrite backend/config/main-local.php
      unchanged backend/config/params-local.php
      unchanged backend/config/test-local.php
      unchanged backend/web/index-test.php
      unchanged backend/web/index.php
      unchanged backend/web/robots.txt
      unchanged common/config/main-local.php
      unchanged common/config/params-local.php
      unchanged common/config/test-local.php
      unchanged console/config/main-local.php
      unchanged console/config/params-local.php
      overwrite frontend/config/main-local.php
      unchanged frontend/config/params-local.php
      unchanged frontend/config/test-local.php
      unchanged frontend/web/index-test.php
      unchanged frontend/web/index.php
      unchanged frontend/web/robots.txt
      unchanged qq/config/main-local.php
      unchanged qq/config/params-local.php
      unchanged qq/config/test-local.php
      unchanged qq/web/index-test.php
      unchanged qq/web/index.php
      unchanged qq/web/robots.txt
      unchanged rpc/config/main-local.php
      unchanged rpc/config/params-local.php
      unchanged rpc/config/test-local.php
      unchanged rpc/web/index-test.php
      unchanged rpc/web/index.php
      unchanged rpc/web/robots.txt
      unchanged wx/config/main-local.php
      unchanged wx/config/params-local.php
      unchanged wx/config/test-local.php
      unchanged wx/web/index-test.php
      unchanged wx/web/index.php
      unchanged wx/web/robots.txt
      unchanged yii
      unchanged yii_test
      unchanged yii_test.bat
       generate cookie validation key in backend/config/main-local.php
       generate cookie validation key in frontend/config/main-local.php
       generate cookie validation key in api/config/main-local.php
          chmod 0777 backend/runtime
          chmod 0777 backend/web/assets
          chmod 0777 frontend/runtime
          chmod 0777 frontend/web/assets
          chmod 0777 api/runtime
          chmod 0777 api/web/assets
          chmod 0755 yii
          chmod 0755 yii_test
    
      ... initialization completed.
    
    
    
    
    3、进入 /api 目录查看,发现 runtime 的目录权限设置为 777,但是 /qq 目录下的 runtime 的目录权限未设置为 777,如图3
    进入 /api 目录查看,发现 runtime 的目录权限设置为 777,但是 /qq 目录下的 runtime 的目录权限未设置为 777
    图3
    4、在 Windows 10 中编辑 \init,打印 $envs,符合预期
    
    
    PS E:\wwwroot\channel-pub-api> ./init --env=Development --overwrite=All
    Array
    (
        [Development] => Array
            (
                [path] => dev
                [setWritable] => Array
                    (
                        [0] => backend/runtime
                        [1] => backend/web/assets
                        [2] => frontend/runtime
                        [3] => frontend/web/assets
                        [4] => api/runtime
                        [5] => api/web/assets
                        [6] => rpc/runtime
                        [7] => rpc/web/assets
                        [8] => qq/runtime
                        [9] => qq/web/assets
                        [10] => wx/runtime
                        [11] => wx/web/assets
                    )
    
                [setExecutable] => Array
                    (
                        [0] => yii
                        [1] => yii_test
                    )
    
                [setCookieValidationKey] => Array
                    (
                        [0] => backend/config/main-local.php
                        [1] => frontend/config/main-local.php
                        [2] => api/config/main-local.php
                        [3] => rpc/config/main-local.php
                        [4] => qq/config/main-local.php
                        [5] => wx/config/main-local.php
                    )
    
            )
    
        [Production] => Array
            (
                [path] => prod
                [setWritable] => Array
                    (
                        [0] => backend/runtime
                        [1] => backend/web/assets
                        [2] => frontend/runtime
                        [3] => frontend/web/assets
                        [4] => api/runtime
                        [5] => api/web/assets
                        [6] => rpc/runtime
                        [7] => rpc/web/assets
                        [8] => qq/runtime
                        [9] => qq/web/assets
                        [10] => wx/runtime
                        [11] => wx/web/assets
                    )
    
                [setExecutable] => Array
                    (
                        [0] => yii
                    )
    
                [setCookieValidationKey] => Array
                    (
                        [0] => backend/config/main-local.php
                        [1] => frontend/config/main-local.php
                        [2] => api/config/main-local.php
                        [3] => rpc/config/main-local.php
                        [4] => qq/config/main-local.php
                        [5] => wx/config/main-local.php
                    )
    
            )
    
    )
    
    
    
    5、在 CentOS 7.2 中编辑 \init,打印 $env,不符合预期,发现缺少 qq/runtime 等值
    
    
    [root@579789e72a51 /]# php /sobey/www/channel-pub-api/init --env=Development --overwrite=All
    Array
    (
        [Development] => Array
            (
                [path] => dev
                [setWritable] => Array
                    (
                        [0] => backend/runtime
                        [1] => backend/web/assets
                        [2] => frontend/runtime
                        [3] => frontend/web/assets
                        [4] => api/runtime
                        [5] => api/web/assets
                    )
    
                [setExecutable] => Array
                    (
                        [0] => yii
                        [1] => yii_test
                    )
    
                [setCookieValidationKey] => Array
                    (
                        [0] => backend/config/main-local.php
                        [1] => frontend/config/main-local.php
                        [2] => api/config/main-local.php
                    )
    
            )
    
        [Production] => Array
            (
                [path] => prod
                [setWritable] => Array
                    (
                        [0] => backend/runtime
                        [1] => backend/web/assets
                        [2] => frontend/runtime
                        [3] => frontend/web/assets
                        [4] => api/runtime
                        [5] => api/web/assets
                    )
    
                [setExecutable] => Array
                    (
                        [0] => yii
                    )
    
                [setCookieValidationKey] => Array
                    (
                        [0] => backend/config/main-local.php
                        [1] => frontend/config/main-local.php
                        [2] => api/config/main-local.php
                    )
    
            )
    
    )
    
    
    
    6、原因在于 Docker 部署时,是将 /build/c_files/ 目录下的所有文件拷贝至系统根目录,\build\c_files\sobey\www\channel-pub-api\environments\index.php 文件中缺少相应目录配置
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?php
    /**
     * The manifest of files that are local to specific environment.
     * This file returns a list of environments that the application
     * may be installed under. The returned data must be in the following
     * format:
     *
     * ```php
     * return [
     *     'environment name' => [
     *         'path' => 'directory storing the local files',
     *         'skipFiles'  => [
     *             // list of files that should only copied once and skipped if they already exist
     *         ],
     *         'setWritable' => [
     *             // list of directories that should be set writable
     *         ],
     *         'setExecutable' => [
     *             // list of files that should be set executable
     *         ],
     *         'setCookieValidationKey' => [
     *             // list of config files that need to be inserted with automatically generated cookie validation keys
     *         ],
     *         'createSymlink' => [
     *             // list of symlinks to be created. Keys are symlinks, and values are the targets.
     *         ],
     *     ],
     * ];
     * ```
     */
    return [
        'Development' => [
            'path' => 'dev',
            'setWritable' => [
                'backend/runtime',
                'backend/web/assets',
                'frontend/runtime',
                'frontend/web/assets',
                'api/runtime',
                'api/web/assets',
            ],
            'setExecutable' => [
                'yii',
                'yii_test',
            ],
            'setCookieValidationKey' => [
                'backend/config/main-local.php',
                'frontend/config/main-local.php',
                'api/config/main-local.php',
            ],
        ],
        'Production' => [
            'path' => 'prod',
            'setWritable' => [
                'backend/runtime',
                'backend/web/assets',
                'frontend/runtime',
                'frontend/web/assets',
                'api/runtime',
                'api/web/assets',
            ],
            'setExecutable' => [
                'yii',
            ],
            'setCookieValidationKey' => [
                'backend/config/main-local.php',
                'frontend/config/main-local.php',
                'api/config/main-local.php',
            ],
        ],
    ];
    
    
    </pre>
    
    7、复制 \environments\index.php 至 \build\c_files\sobey\www\channel-pub-api\environments\index.php,重新升级,在 CentOS 7.2 中执行初始化命令,符合预期,如图4
    复制 \environments\index.php 至 \build\c_files\sobey\www\channel-pub-api\environments\index.php,重新升级,在 CentOS 7.2 中执行初始化命令,符合预期
    图4
    
    
    [root@579789e72a51 /]# php /sobey/www/channel-pub-api/init --env=Development --overwrite=All
    Yii Application Initialization Tool v1.0
    
    
      Start initialization ...
    
       generate api/config/main-local.php
       generate api/config/params-local.php
       generate api/config/test-local.php
       generate api/web/index-test.php
       generate api/web/index.php
       generate api/web/robots.txt
       generate backend/config/main-local.php
       generate backend/config/params-local.php
       generate backend/config/test-local.php
       generate backend/web/index-test.php
       generate backend/web/index.php
       generate backend/web/robots.txt
       generate common/config/main-local.php
       generate common/config/params-local.php
       generate common/config/test-local.php
       generate console/config/main-local.php
       generate console/config/params-local.php
       generate frontend/config/main-local.php
       generate frontend/config/params-local.php
       generate frontend/config/test-local.php
       generate frontend/web/index-test.php
       generate frontend/web/index.php
       generate frontend/web/robots.txt
       generate qq/config/main-local.php
       generate qq/config/params-local.php
       generate qq/config/test-local.php
       generate qq/web/index-test.php
       generate qq/web/index.php
       generate qq/web/robots.txt
       generate rpc/config/main-local.php
       generate rpc/config/params-local.php
       generate rpc/config/test-local.php
       generate rpc/web/index-test.php
       generate rpc/web/index.php
       generate rpc/web/robots.txt
       generate wx/config/main-local.php
       generate wx/config/params-local.php
       generate wx/config/test-local.php
       generate wx/web/index-test.php
       generate wx/web/index.php
       generate wx/web/robots.txt
       generate yii
       generate yii_test
       generate yii_test.bat
       generate cookie validation key in backend/config/main-local.php
       generate cookie validation key in frontend/config/main-local.php
       generate cookie validation key in api/config/main-local.php
       generate cookie validation key in rpc/config/main-local.php
       generate cookie validation key in qq/config/main-local.php
       generate cookie validation key in wx/config/main-local.php
          chmod 0777 backend/runtime
          chmod 0777 backend/web/assets
          chmod 0777 frontend/runtime
          chmod 0777 frontend/web/assets
          chmod 0777 api/runtime
          chmod 0777 api/web/assets
          chmod 0777 rpc/runtime
          chmod 0777 rpc/web/assets
          chmod 0777 qq/runtime
          chmod 0777 qq/web/assets
          chmod 0777 wx/runtime
          chmod 0777 wx/web/assets
          chmod 0755 yii
          chmod 0755 yii_test
    
      ... initialization completed.
    
    
    
    
    8、进入 /api 目录查看,发现 runtime 的目录权限设置为 777,/qq 目录下的 runtime 的目录权限也设置为 777,如图5
    进入 /api 目录查看,发现 runtime 的目录权限设置为 777,/qq 目录下的 runtime 的目录权限也设置为 777
    图5
  • 在 Yii 2 中基于 yii\db\ActiveQuery::joinWith() 关联声明查询数据,响应字段类型为字符串的分析解决

    在 Yii 2 中基于 yii\db\ActiveQuery::joinWith() 关联声明查询数据,响应字段类型为字符串的分析解决

    1、在 \qq\rests\article_category\StandardIndexAction.php 中
    
    
    	/* @var $modelClass \yii\db\BaseActiveRecord */
    	$modelClass = $this->modelClass;
    
    	$query = $modelClass::find()
    		->joinWith('qqArticleCategoryNormal')
    		->where([
    			$modelClass::tableName() . '.is_deleted' => $modelClass::IS_DELETED_NO,
    			QqArticleCategoryNormal::tableName() . '.is_deleted' => QqArticleCategoryNormal::IS_DELETED_NO,
    		])
    		->asArray()
    		->orderBy([$modelClass::tableName() . '.id' => SORT_DESC]);
    	if (!empty($filter)) {
    		$query->andFilterWhere($filter);
    	}
    
    	// 设置每页资源数量默认为资源总数
    	$count = (int) $query->count($modelClass::tableName() . '.id');
    	if (empty($requestParams['per-page'])) {
    		$requestParams['per-page'] = $count;
    	}
    
    	return Yii::createObject([
    		'class' => ActiveDataProvider::className(),
    		'query' => $query,
    		'pagination' => [
    			'params' => $requestParams,
    			'pageSizeLimit' => [1, $count],
    		],
    		'sort' => [
    			'params' => $requestParams,
    		],
    	]);
    
    
    
    2、期待的响应结果如下:
    
    
    	"items": [
    		{
    			"id": 226,
    			"name": "其他综艺",
    			"parent_id": 0,
    			"status": 1,
    			"is_deleted": 0,
    			"created_at": 1542178227,
    			"updated_at": 1542178227,
    			"deleted_at": 0
    		},
    		{
    			"id": 225,
    			"name": "舞台剧",
    			"parent_id": 0,
    			"status": 1,
    			"is_deleted": 0,
    			"created_at": 1542178227,
    			"updated_at": 1542178227,
    			"deleted_at": 0
    		}
    	]
    
    
    
    3、实际的响应结果如下:所有字段类型皆为字符串,如图1
    实际的响应结果如下:所有字段类型皆为字符串
    图1
    
    
    	"items": [
    		{
    			"id": "67",
    			"name": "跑步",
    			"parent_id": "0",
    			"status": "1",
    			"is_deleted": "0",
    			"created_at": "1542178227",
    			"updated_at": "1542178227",
    			"deleted_at": "0"
    		},
    		{
    			"id": "66",
    			"name": "健身",
    			"parent_id": "0",
    			"status": "1",
    			"is_deleted": "0",
    			"created_at": "1542178227",
    			"updated_at": "1542178227",
    			"deleted_at": "0"
    		}
    	]
    
    
    
    
    4、编辑 \qq\rests\article_category\Serializer.php,打印 $models
    
    
    class Serializer extends \yii\rest\Serializer
    {
        /**
         * Serializes a data provider.
         * @param DataProviderInterface $dataProvider
         * @return array the array representation of the data provider.
         */
        protected function serializeDataProvider($dataProvider)
        {
            if ($this->preserveKeys) {
                $models = $dataProvider->getModels();
            } else {
                $models = array_values($dataProvider->getModels());
            }
            $models = $this->serializeModels($models);
    
            var_dump($models);
            exit;
    	}
    }
    
    
    
    5、打印结果如下,所有字段皆为字符串
    
    
    array(67) {
      [0]=>
      array(9) {
        ["id"]=>
        string(2) "67"
        ["name"]=>
        string(6) "跑步"
        ["parent_id"]=>
        string(1) "0"
        ["status"]=>
        string(1) "1"
        ["is_deleted"]=>
        string(1) "0"
        ["created_at"]=>
        string(10) "1542178227"
        ["updated_at"]=>
        string(10) "1542178227"
        ["deleted_at"]=>
        string(1) "0"
        ["qqArticleCategoryNormal"]=>
        array(8) {
          ["id"]=>
          string(3) "334"
          ["article_category_id"]=>
          string(2) "67"
          ["name"]=>
          string(6) "跑步"
          ["status"]=>
          string(1) "1"
          ["is_deleted"]=>
          string(1) "0"
          ["created_at"]=>
          string(10) "1542178227"
          ["updated_at"]=>
          string(10) "1542178227"
          ["deleted_at"]=>
          string(1) "0"
        }
      }
    }
    
    
    
    6、编辑 \qq\rests\article_category\StandardIndexAction.php,删除 ->asArray(),因为仅需要当前模型的数据,不需要关联模型的数据(即 qqArticleCategoryNormal)
    
    
            /* @var $modelClass \yii\db\BaseActiveRecord */
            $modelClass = $this->modelClass;
    
            $query = $modelClass::find()
                ->joinWith('qqArticleCategoryNormal')
                ->where([
                    $modelClass::tableName() . '.is_deleted' => $modelClass::IS_DELETED_NO,
                    QqArticleCategoryNormal::tableName() . '.is_deleted' => QqArticleCategoryNormal::IS_DELETED_NO,
                ])
                ->orderBy([$modelClass::tableName() . '.id' => SORT_DESC]);
            if (!empty($filter)) {
                $query->andFilterWhere($filter);
            }
    
            // 设置每页资源数量默认为资源总数
            $count = (int) $query->count($modelClass::tableName() . '.id');
            if (empty($requestParams['per-page'])) {
                $requestParams['per-page'] = $count;
            }
    
            return Yii::createObject([
                'class' => ActiveDataProvider::className(),
                'query' => $query,
                'pagination' => [
                    'params' => $requestParams,
                    'pageSizeLimit' => [1, $count],
                ],
                'sort' => [
                    'params' => $requestParams,
                ],
            ]);
    
    
    
    7、实际的响应结果如下:字段类型与数据库中一致,符合预期,如图2
    实际的响应结果如下:字段类型与数据库中一致,符合预期
    图2
    
    
    	"items": [
    		{
    			"id": 67,
    			"name": "跑步",
    			"parent_id": 0,
    			"status": 1,
    			"is_deleted": 0,
    			"created_at": 1542178227,
    			"updated_at": 1542178227,
    			"deleted_at": 0
    		},
    		{
    			"id": 66,
    			"name": "健身",
    			"parent_id": 0,
    			"status": 1,
    			"is_deleted": 0,
    			"created_at": 1542178227,
    			"updated_at": 1542178227,
    			"deleted_at": 0
    		}
    	]
    
    
    
  • 在 Yii 2 中,更新模型时,当某字段不存在时,不更新模型(默认实现),当某字段存在,其值为空(赋值为属性的旧值)时,不更新模型的实现

    在 Yii 2 中,更新模型时,当某字段不存在时,不更新模型(默认实现),当某字段存在,其值为空(赋值为属性的旧值)时,不更新模型的实现

    1、GET http://api.channel-pub-api.localhost/qq/v1/qq-cw-apps/edit/148d4df6eba311e899f654ee75d2ebc1?group_id=spider ,响应如下,基于安全考虑,重置 Client Secret 为空字符串
    
    
    {
        "code": 10000,
        "message": "编辑企鹅号的内容网站应用成功",
        "data": {
            "channel_app_source_uuid": "148d4df6eba311e899f654ee75d2ebc1",
            "penguin_name": "篮球场马达1",
            "penguin_login_qq": "3176058386",
            "penguin_login_wx": "",
            "client_id": "32deed1727f9a23a504d6dda48938de0",
            "client_secret": "",
            "permission": 2,
            "status": 0
        }
    }
    
    
    
    2、因此,PUT http://api.channel-pub-api.localhost/qq/v1/qq-cw-apps/148d4df6eba311e899f654ee75d2ebc1?group_id=spider 时,如果不更新 client_secret 的值,其值为空字符串 Body
    
    
    {
    	"penguin_name": "篮球场马达1",
    	"penguin_login_qq": "3176058386",
    	"penguin_login_wx": "",
    	"client_id": "32deed1727f9a23a504d6dda48938de0",
    	"client_secret": "",
    	"permission": 2,
    	"status": 0
    }
    
    
    
    3、请求响应失败,422,如图1
    请求响应失败,422
    图1
    
    
    {
        "code": 20004,
        "message": "数据验证失败:Client Secret不能为空。"
    }
    
    
    
    4、现在需要实现 当 client_secret 的值为空字符串时,不对 client_secret 执行验证,且不更新 client_secret 的值,编辑验证规则
    
    
        /**
         * @inheritdoc
         */
        public function rules()
        {
            $rules = [
                /* 更新企鹅号的内容网站应用 */
                [['client_secret'], 'validateClientSecret', 'skipOnEmpty' => false, 'on' => self::SCENARIO_UPDATE],
                [['permission'], 'in', 'range' => [self::PERMISSION_SYNC, self::PERMISSION_PUB, self::PERMISSION_SYNC_PUB], 'on' => self::SCENARIO_UPDATE],
                [['status'], 'in', 'range' => [self::STATUS_DISABLED, self::STATUS_ENABLED], 'on' => self::SCENARIO_UPDATE],
            ];
            $parentRules = parent::rules();
    
            return ArrayHelper::merge($rules, $parentRules);
        }
    
        /**
         * Validates the Client Secret.
         * This method serves as the inline validation for Client Secret.
         *
         * @param string $attribute the attribute currently being validated
         * @param array $params the additional name-value pairs given in the rule
         */
        public function validateClientSecret($attribute, $params)
        {
            // 当 Client Secret 为空时,赋值 Client Secret 为原先的值
            if (empty($this->$attribute)) {
                $this->$attribute = $this->getOldAttribute($attribute);
            }
        }
    
    
    
    5、PUT http://api.channel-pub-api.localhost/qq/v1/qq-cw-apps/148d4df6eba311e899f654ee75d2ebc1?group_id=spider 时,贵响应成功,如图2
    PUT http://api.channel-pub-api.localhost/qq/v1/qq-cw-apps/148d4df6eba311e899f654ee75d2ebc1?group_id=spider 时,贵响应成功
    图2
    6、查看 SQL 语句,符合预期,当 client_secret 的值为空字符串时,未更新 client_secret 的值
    
    
    UPDATE `cpa_qq_cw_app` SET `permission`=1, `updated_at`=1542610618 WHERE `id`=6
    
    
    
  • 基于企鹅号的视频文件分片上传的实现流程,包含队列、文件切片、while 循环等

    基于企鹅号的视频文件分片上传的实现流程,包含队列、文件切片、while 循环等

    1、数据库结构的设计,一张资源表,一张企鹅号的视频文件分片上传表,一张企鹅号的事务表,结构如下:
    
    
    28、asset:资源  Asset
    id 			           主键
    channel_id 			   渠道ID
    channel_code	       渠道代码,qq:企鹅号;wx:微信公众帐号
    channel_type_id 	   渠道的类型ID
    channel_type_code      渠道的类型代码,qq_cw:企鹅号的内容网站应用;qq_tp:企鹅号的第三方服务平台应用;wx:微信公众帐号应用
    source                 来源,xContent:内容库;vms:视频管理系统;cms:内容管理系统;spider:自媒体
    type                   资源文件的类型,image:图片;video:视频
    absolute_url           来源的资源文件的绝对URL
    relative_path          渠道发布的资源文件的相对路径
    size                   文件大小,单位(字节)
    task_id                任务ID
    channel_article_id     渠道的文章ID
    status                 状态,0:禁用;1:启用
    is_deleted             是否被删除,0:否;1:是
    created_at             创建时间
    updated_at 	           更新时间
    deleted_at             删除时间
    
    
    
    
    
    29、qq_video_multipart_upload:企鹅号的视频文件分片上传  QqCwVideoMultipartUpload
    id 			                 主键
    asset_id                     资源ID
    qq_app_task_id 		         企鹅号的应用的任务ID
    qq_app_id                    企鹅号的应用ID
    qq_app_type                  企鹅号的应用类型,cw:内容网站应用;tp:第三方服务平台应用
    size                         视频文件大小,单位(字节)
    md5                          视频文件MD5值
    sha                          视频文件SHA-1值
    transaction_id               上传的唯一事务ID
    mediatrunk                   视频 mediatrunk 文件
    start_offset                 分片的起始位置(从0开始计数)
    end_offset                   分片的结束位置
    vid                          视频文件唯一标示ID
    status                       状态,0:禁用;1:待上传;2:上传中;3:上传中(已失败);4:已上传
    is_deleted                   是否被删除,0:否;1:是
    created_at                   创建时间
    updated_at 	                 更新时间
    deleted_at                   删除时间
    
    
    
    
    
    30、qq_transaction:企鹅号的事务  QqTransaction
    id 			                        主键
    group_id                            租户ID
    qq_app_task_id 		                企鹅号的应用的任务ID
    qq_app_id                           企鹅号的应用ID
    qq_app_type                         企鹅号的应用类型,cw:内容网站应用;tp:第三方服务平台应用
    qq_article_id                       企鹅号的文章ID
    qq_video_multipart_upload_id        企鹅号的应用的视频文件分片上传ID
    transaction_id                      事务ID
    type                                类型,1:文章;2:视频
    transaction_ctime                   事务创建时间
    ext_err                             扩展的错误
    transaction_err_msg                 事务的错误信息
    article_abstract                    文章摘要
    article_type                        文章类型,取值:普通文章,图文文章,视频文章,直播文章,RTMP直播文章
    article_type_code                   文章类型代码,normal:文章;multivideos:视频;images:组图;live:直播
    article_url                         文章快报链接
    article_imgurl                      文章封面图
    article_title                       文章标题
    article_pub_flag                    文章发布状态,取值:未发布,发布成功,审核中
    article_pub_time                    文章发布时间
    article_video_title                 视频文章标题
    article_video_desc                  视频文章描述
    article_video_type                  视频文章类型,视频
    article_video_vid                   视频文章的视频唯一ID
    task_id                             任务ID
    status                              状态,0:禁用;1:成功;2:失败;3:处理中
    is_deleted                          是否被删除,0:否;1:是
    created_at                          创建时间
    updated_at 	                        更新时间
    deleted_at                          删除时间
    
    
    
    2、资源的上传是基于队列实现的,因此会先将资源数据存储至资源表,进而入上传资源的队列,再执行上传资源的作业 3、执行 1 次接口请求,此时资源表已经存在资源的相应数据,且入复制资源的队列
    
    
    PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/info --color=0
    Jobs
    - waiting: 1
    - delayed: 0
    - reserved: 0
    - done: 0
    
    
    
    4、执行复制资源队列中的任务命令,会复制相应的资源,且将资源的相对路径存储至资源表中
    
    
    PS E:\wwwroot\channel-pub-api> ./yii copy-asset-queue/run --verbose=1 --isolate=1 --color=0
    2018-11-15 10:05:56 [pid: 23112] - Worker is started
    2018-11-15 10:05:57 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 23112) - Started
    2018-11-15 10:06:08 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 23112) - Done (11.394 s)
    2018-11-15 10:06:08 [pid: 23112] - Worker is stopped (0:00:12)
    
    
    
    5、复制资源队列中的任务执行成功后,会入上传资源的队列
    
    
    PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/info --color=0
    Jobs
    - waiting: 1
    - delayed: 0
    - reserved: 0
    - done: 0
    
    
    
    6、上传资源的代码分为 2 个部分,一为文件切片,需要将视频资源文件切片为100M大小的小文件,\channel-pub-api\common\services\AssetService.php
    
    
        /**
         * 文件切片
         * @param string $fileAbsolutePath 需要切片的文件的绝对路径
         * 格式如下:E:/wwwroot/channel-pub-api/storage/spider/videos/切片文件.mp4
         *
         * @param int $size 104857600,单位为字节
         *
         * 生成文件列表如下:
         * E:/wwwroot/channel-pub-api/storage/spider/videos/切片文件_0.mp4
         * E:/wwwroot/channel-pub-api/storage/spider/videos/切片文件_1.mp4
         */
        public static function cut($fileAbsolutePath, $size)
        {
            // 获取需要切片的文件的路径信息
            $pathInfo = pathinfo($fileAbsolutePath);
            $i = 0;
            $handle = fopen($fileAbsolutePath, "rb");
            while (!feof($handle)) {
                $cutHandle = fopen($pathInfo['dirname'] . '/' . $pathInfo['filename'] . '_' . $i . '.' . $pathInfo['extension'], "wb");
                fwrite($cutHandle, fread($handle, $size));
                fclose($cutHandle);
                unset($cutHandle);
                $i++;
            }
            fclose($handle);
        }
    
    
    
    7、HTTP请求,企鹅号的内容网站应用的视频文件分片上传,\channel-pub-api\common\logics\http\qq_api\Video.php
    <pre class="wp-block-syntaxhighlighter-code">
    
    <?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);
            }
    
        }
    }
    
    </pre>
    
    8、上传视频文件的代码,\channel-pub-api\common\services\QqCwVideoMultipartUploadService.php
    
    
    
        /**
         * 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);
    
                    // 创建企鹅号的事务
                    $qqTransactionServiceCreateData = [
                        'groupId' => $taskEnabledItem->group_id,
                        'qqAppTaskId' => $qqVideoMultipartUploadItem->qq_app_task_id,
                        'qqAppId' => $qqVideoMultipartUploadItem->qq_app_id,
                        'qqAppType' => $qqVideoMultipartUploadItem->qq_app_type,
                        'qqArticleId' => 0,
                        'qqVideoMultipartUploadId' => $qqVideoMultipartUploadItem->id,
                        'transactionId' => $qqVideoMultipartUploadItem->transaction_id,
                        'type' => $qqTransactionService::getTypeByHttpQqApiTransactionType($qqTransactionServiceHttpTransactionInfoResult['transaction_type']),
                        'transactionCtime' => $qqTransactionServiceHttpTransactionInfoResult['transaction_ctime'],
                        'extErr' => $qqTransactionServiceHttpTransactionInfoResult['ext_err'],
                        'transactionErrMsg' => $qqTransactionServiceHttpTransactionInfoResult['transaction_err_msg'],
                        'articleAbstract' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_abstract'],
                        'articleType' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_type'],
                        'articleTypeCode' => '',
                        'articleUrl' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_url'],
                        'articleImgurl' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_imgurl'],
                        'articleTitle' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_title'],
                        'articlePubFlag' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_pub_flag'],
                        'articlePubTime' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_pub_time'],
                        'articleVideoTitle' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['title'],
                        'articleVideoDesc' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['desc'],
                        'articleVideoType' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['type'],
                        'articleVideoVid' => $qqTransactionServiceHttpTransactionInfoResult['article_info']['article_video_info']['vid'],
                        'taskId' => $assetEnabledItem->task_id,
                        'status' => QqTransaction::STATUS_PROCESSING,
                    ];
                    $qqTransactionServiceCreateResult = $qqTransactionService->create($qqTransactionServiceCreateData);
    
                    if ($qqTransactionServiceCreateResult['status'] === false) {
                        throw new ServerErrorHttpException($qqTransactionServiceCreateResult['message'], $qqTransactionServiceCreateResult['code']);
                    }
    
                    $data['status'] = QqVideoMultipartUpload::STATUS_UPLOADED;
                }
                $result = $this->saveModelByData($data);
                if ($result['status'] === false) {
                    throw new ServerErrorHttpException($result['message'], $result['code']);
                }
    
                // 基于资源ID、企鹅号的应用的任务ID获取企鹅号的视频文件分片上传的单个数据模型
                $qqVideoMultipartUploadItem = $this->getModelByAssetIdAndQqAppTaskId($assetId, $qqCwAppTaskPublishItem->id);
                $i++;
            }
    
        }
    
    
    
    9、执行上传资源队列中的任务命令,会切片文件,上传相应的资源,且更新企鹅号的视频文件分片上传表、新增企鹅号的事务
    
    
    PS E:\wwwroot\channel-pub-api> ./yii upload-asset-queue/run --verbose=1 --isolate=1 --color=0
    2018-11-15 11:02:29 [pid: 39632] - Worker is started
    2018-11-15 11:02:30 [1] common\jobs\UploadAssetJob (attempt: 1, pid: 39632) - Started
    2018-11-15 11:06:48 [1] common\jobs\UploadAssetJob (attempt: 1, pid: 39632) - Done (257.816 s)
    2018-11-15 11:06:48 [pid: 39632] - Worker is stopped (0:04:19)
    
    
    
    10、查看生成的切片小文件,由于切片大小为100M,396 MB (415,352,401 字节)的文件切片为4个小的文件,如图1
    查看生成的切片小文件,由于切片大小为100M,396 MB (415,352,401 字节)的文件切片为4个小的文件
    图1
    11、上传成功后,分片的起始位置与分片的结束位置皆等于文件的大小:415352401,如图2
    上传成功后,分片的起始位置与分片的结束位置皆等于文件的大小:415352401
    图2
    12、在企鹅号后台查看我的素材,视频文件已经分片上传成功,如图3
    在企鹅号后台查看我的素材,视频文件已经分片上传成功
    图3