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

Implement Asynchronous Tasks With the Yii2 Queue Extension in the Yii2 Advanced Template: Event Handlers for Successful Execution and Uncaught Exceptions

Attaching an event handler via configuration

1. Attach an event handler via configuration by editing \environments\dev\common\config\main-local.php and \environments\prod\common\config\main-local.php, as shown in Figure 1.

Attaching an event handler via configuration
Figure 1
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' => 5 * 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',
        ],

2. Edit \qq\rests\qq_cw_app\IndexAction.php, which serves as an entry point for pushing to the queue.

PHP

        $data = [
            'channel_id' => 2, // 渠道ID
            'channel_code' => 'wx', // 渠道代码,qq:企鹅号;wx:微信公众帐号
            'channel_type_id' => 3, // 渠道的类型ID
            'channel_type_code' => 'wx', // 渠道的类型代码,qq_cw:企鹅号的内容网站应用;qq_tp:企鹅号的第三方服务平台应用;wx:微信公众帐号应用
            'source' => 'spider', // 来源,xContent:内容库;vms:视频管理系统;cms:内容管理系统;spider:自媒体
            'task_id' => 2, // 任务ID
        ];
        $assets = [
            [
                'type' => 'image',
                'channel_article_id' => 1,
                'absolute_url' => 'http://localhost/channel-pub-api/storage/spider/images/1.png',
            ],
            [
                'type' => 'video',
                'channel_article_id' => 1,
                'absolute_url' => 'http://localhost/channel-pub-api/storage/spider/videos/7月份北上广深等十大城市租金环比上涨 看东方 20180820 高清_高清.mp4',
            ],
        ];
        $assetServiceCopyAssetsAsyncResult = AssetService::copyAssetsAsync($data, $assets);
        print_r($assetServiceCopyAssetsAsyncResult);
        exit;

3. Edit \common\services\AssetService.php to copy the source asset files to the channel publishing asset directory. If the queue job executes successfully, call the corresponding service; otherwise, insert a publish log (asynchronously).

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:自媒体
     *     '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://localhost/channel-pub-api/storage/spider/videos/7月份北上广深等十大城市租金环比上涨 看东方 20180820 高清_高清.mp4', // 来源的资源文件的绝对URL
     *     ],
     * ]
     *
     * @return array $channelPubApiAssetAbsolutePaths 渠道发布的资源文件的相对路径
     * 格式如下:
     * [
     *     [
     *         'type' => 'image',
     *         'channel_article_id' => 1,
     *         'relative_path' => '/2018/09/20/1537439889.2333.1441541478.png',
     *     ],
     *     [
     *         'type' => 'video',
     *         'channel_article_id' => 1,
     *         'relative_path' => '/2018/09/20/1537439889.2403.62871268.mp4',
     *     ],
     * ]
     *
     * @throws Exception execution failed
     */
    public static function copyAssetsAsync($data, $assets)
    {
        $assetData = [];
        $time = time();
        foreach ($assets as $key => $asset) {
            $assetData[] = [
                'channel_id' => $data['channel_id'],
                'channel_code' => $data['channel_code'],
                'channel_type_id' => $data['channel_type_id'],
                'channel_type_code' => $data['channel_type_code'],
                'source' => $data['source'],
                'type' => $asset['type'],
                'absolute_url' => $asset['absolute_url'],
                'relative_path' => '',
                'size' => 0,
                'task_id' => $data['task_id'],
                'channel_article_id' => $asset['channel_article_id'],
                'status' => Asset::STATUS_ENABLED,
                'is_deleted' => Asset::IS_DELETED_NO,
                'created_at' => $time,
                'updated_at' => $time,
                'deleted_at' => Asset::DELETED_AT_DEFAULT,
            ];
        }

        // 批量创建资源
        $asset = new Asset();
        $assetCreateMultipleResult = $asset->createMultiple($assetData);

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

4. Edit \common\jobs\CopyAssetJob.php, the queue job class.

PHP


 * @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;
        }

        // 批量更新资源
        $assetEnabledItem->updateMultiple($assetEnabledItems);
    }
}

5. Edit \common\components\queue\CopyAssetEventHandler.php, the event handler defined in the configuration. For the copy asset files queue, after each successful job execution (afterExec), it will call the corresponding service for subsequent processing, meaning it pushes a job to the upload asset files queue. When an uncaught exception occurs during job execution in the copy asset files queue (afterError), it inserts a publish log and pushes the job to the source callback queue.

PHP


 * @since 1.0
 */
class CopyAssetEventHandler extends Component
{
    /**
     * @param ExecEvent $event
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws ServerErrorHttpException 如果基于任务ID查找状态为启用的资源列表为空,将抛出 500 HTTP 异常
     * @throws Exception execution failed
     */
    public static function afterExec(ExecEvent $event)
    {
        $taskId = $event->job->taskId;
        // 基于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);
        }

        try {
            // 调用相应服务进行后续处理
            $serviceClass = 'common\services\\' . str_replace(' ', '', ucwords(str_replace('_', ' ', $taskEnabledItem->channel_type_code))) . 'AssetService'; // 例:common\services\QqCwAssetService
            $serviceAction = 'copyAssetExecHandler';
            $serviceClass::$serviceAction($taskEnabledItem->id);
        } catch (\Throwable $e) {
            $pubLogData = [];
            $time = time();
            foreach ($channelAppTaskEnabledItems as $channelAppTaskEnabledItem) {
                $pubLogData[] = [
                    'channel_id' => $channelAppTaskEnabledItem['channel_id'],
                    'channel_code' => $channelAppTaskEnabledItem['channel_code'],
                    'channel_type_id' => $channelAppTaskEnabledItem['channel_type_id'],
                    'channel_type_code' => $channelAppTaskEnabledItem['channel_type_code'],
                    'task_id' => $channelAppTaskEnabledItem['task_id'],
                    'channel_app_task_id' => $channelAppTaskEnabledItem['id'],
                    'channel_app_task_uuid' => $channelAppTaskEnabledItem['uuid'],
                    'code' => $e->getCode(),
                    'message' => $e->getMessage(),
                    'data' => Json::encode([]),
                    'status' => PubLog::STATUS_ERROR,
                    'is_deleted' => PubLog::IS_DELETED_NO,
                    'created_at' => $time,
                    'updated_at' => $time,
                    'deleted_at' => PubLog::DELETED_AT_DEFAULT,
                ];
            }

            // 发布任务成功后,调用相应服务失败,插入发布日志,将作业推送至来源回调队列(异步)
            SourceCallbackService::errorAsync($pubLogData);
        }
    }

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

        $pubLogData = [];
        $time = time();
        foreach ($channelAppTaskEnabledItems as $channelAppTaskEnabledItem) {
            $pubLogData[] = [
                'channel_id' => $channelAppTaskEnabledItem['channel_id'],
                'channel_code' => $channelAppTaskEnabledItem['channel_code'],
                'channel_type_id' => $channelAppTaskEnabledItem['channel_type_id'],
                'channel_type_code' => $channelAppTaskEnabledItem['channel_type_code'],
                'task_id' => $channelAppTaskEnabledItem['task_id'],
                'channel_app_task_id' => $channelAppTaskEnabledItem['id'],
                'channel_app_task_uuid' => $channelAppTaskEnabledItem['uuid'],
                'code' => $event->error->getCode(),
                'message' => $event->error->getMessage(),
                'data' => Json::encode([]),
                'status' => PubLog::STATUS_ERROR,
                'is_deleted' => PubLog::IS_DELETED_NO,
                'created_at' => $time,
                'updated_at' => $time,
                'deleted_at' => PubLog::DELETED_AT_DEFAULT,
            ];
        }

        // 发布任务失败后,插入发布日志,将作业推送至来源回调队列(异步)
        SourceCallbackService::errorAsync($pubLogData);
    }
}

6. Open the URL: http://api.channel-pub-api.localhost/qq/v1/qq-cw-apps?group_id=spider to push a job to the queue.

7. The info command prints information about the queue status. In the copy asset files queue, 1 job is waiting, and in the upload asset files queue, 0 jobs are waiting.

Bash

.\yii copy-asset-queue/info --color=0
.\yii upload-asset-queue/info --color=0

Plaintext

Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0

Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

8. The run command fetches and executes jobs in a loop (the copy asset files queue) until the queue is empty. After the job in the copy asset files queue executes successfully, it pushes the next upload job to the upload asset files queue. In the copy asset files queue, 0 jobs are waiting and 1 job is done. In the upload asset files queue, 1 job is waiting, as shown in Figure 2.

The run command fetches and executes jobs in a loop (the copy asset files queue) until the queue is empty. After the job in the copy asset files queue executes successfully, it pushes the next upload job to the upload asset files queue. In the copy asset files queue, 0 jobs are waiting and 1 job is done. In the upload asset files queue, 1 job is waiting.
Figure 2
Bash

.\yii copy-asset-queue/run --verbose=1 --isolate=1 --color=0
.\yii copy-asset-queue/info
.\yii upload-asset-queue/info --color=0

Plaintext

2018-10-27 17:23:54 [pid: 5216] - Worker is started
2018-10-27 17:23:54 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 5216) - Started
2018-10-27 17:23:55 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 5216) - Done (0.249 s)
2018-10-27 17:23:55 [pid: 5216] - Worker is stopped (0:00:01)

Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1

Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0

9. Edit \common\jobs\CopyAssetJob.php, the queue job class, and deliberately throw an exception to test that when an uncaught exception occurs during job execution in the copy asset files queue (afterError), it inserts a publish log and pushes the job to the source callback queue.

PHP


 * @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);

        throw new ServerErrorHttpException(Yii::t('common/error', Yii::t('common/error', Yii::t('common/error', '35020'), ['task_id' => $this->taskId])), 35020);
        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;
        }

        // 批量更新资源
        $assetEnabledItem->updateMultiple($assetEnabledItems);
    }
}

10. Clear Redis, which clears the data in the queues, then open the URL: http://api.channel-pub-api.localhost/qq/v1/qq-cw-apps?group_id=spider to push a job to the queue.

11. The info command prints information about the queue status. In the copy asset files queue, 1 job is waiting; in the upload asset files queue, 0 jobs are waiting; and in the source callback queue, 0 jobs are waiting.

Bash

.\yii copy-asset-queue/info --color=0
.\yii upload-asset-queue/info --color=0
.\yii source-callback-queue/info --color=0

Plaintext

Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0

Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

12. The run command fetches and executes jobs in a loop (the copy asset files queue) until the queue is empty. After the job in the copy asset files queue fails to execute (because an uncaught exception is thrown in the job), it pushes the next upload job to the upload asset files queue. In the copy asset files queue, 0 jobs are waiting and 1 job is done. In the upload asset files queue, 0 jobs are waiting, and in the source callback queue, 1 job is waiting, as shown in Figure 3.

The run command fetches and executes jobs in a loop (the copy asset files queue) until the queue is empty. After the job in the copy asset files queue fails to execute (because an uncaught exception is thrown in the job), it pushes the next upload job to the upload asset files queue. In the copy asset files queue, 0 jobs are waiting and 1 job is done. In the upload asset files queue, 0 jobs are waiting, and in the source callback queue, 1 job is waiting.
Figure 3
Bash

.\yii copy-asset-queue/run --verbose=1 --isolate=1 --color=0
.\yii copy-asset-queue/info --color=0
.\yii upload-asset-queue/info --color=0
.\yii source-callback-queue/info --color=0

Plaintext

2018-10-27 17:37:10 [pid: 32132] - Worker is started
2018-10-27 17:37:11 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 32132) - Started
2018-10-27 17:37:11 [1] common\jobs\CopyAssetJob (attempt: 1, pid: 32132) - Error (0.232 s)
> yii\web\ServerErrorHttpException: Based on task ID: 2, find the list of enabled resources is empty
2018-10-27 17:37:11 [pid: 32132] - Worker is stopped (0:00:01)

Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 1

Jobs
- waiting: 0
- delayed: 0
- reserved: 0
- done: 0

Jobs
- waiting: 1
- delayed: 0
- reserved: 0
- done: 0

13. View the exception message in the system log table, which is the message field in the log table, to assist subsequent developers in their analysis, as shown in Figure 4.

Viewing the exception message in the system log table, which is the message field in the log table, to assist subsequent developers in their analysis
Figure 4
Plaintext

[1] common\jobs\CopyAssetJob (attempt: 1, PID: 32132) is finished with error: yii\web\ServerErrorHttpException: Based on task ID: 2, find the list of enabled resources is empty in E:\wwwroot\channel-pub-api\common\jobs\CopyAssetJob.php:38
 Stack trace:
 #0 E:\wwwroot\channel-pub-api\vendor\yiisoft\yii2-queue\src\Queue.php(214): common\jobs\CopyAssetJob->execute(Object(yii\queue\redis\Queue))
 #1 E:\wwwroot\channel-pub-api\vendor\yiisoft\yii2-queue\src\cli\Queue.php(162): yii\queue\Queue->handleMessage('1', 'O:24:"common\\jo...', '600', '1')
 #2 E:\wwwroot\channel-pub-api\vendor\yiisoft\yii2-queue\src\cli\Command.php(145): yii\queue\cli\Queue->execute('1', 'O:24:"common\\jo...', '600', '1', '32132')
 #3 [internal function]: yii\queue\cli\Command->actionExec('1', '600', '1', '32132')
 #4 E:\wwwroot\channel-pub-api\vendor\yiisoft\yii2\base\InlineAction.php(57): call_user_func_array(Array, Array)
 #5 E:\wwwroot\channel-pub-api\vendor\yiisoft\yii2\base\Controller.php(157): yii\base\InlineAction->runWithParams(Array)
 #6 E:\wwwroot\channel-pub-api\vendor\yiisoft\yii2\console\Controller.php(148): yii\base\Controller->runAction('exec', Array)
 #7 E:\wwwroot\channel-pub-api\vendor\yiisoft\yii2\base\Module.php(528): yii\console\Controller->runAction('exec', Array)
 #8 E:\wwwroot\channel-pub-api\vendor\yiisoft\yii2\console\Application.php(180): yii\base\Module->runAction('copy-asset-queu...', Array)
 #9 E:\wwwroot\channel-pub-api\vendor\yiisoft\yii2\console\Application.php(147): yii\console\Application->runAction('copy-asset-queu...', Array)
 #10 E:\wwwroot\channel-pub-api\vendor\yiisoft\yii2\base\Application.php(386): yii\console\Application->handleRequest(Object(yii\console\Request))
 #11 E:\wwwroot\channel-pub-api\yii(23): yii\base\Application->run()
 #12 {main}.

14. After the publish job fails, it inserts a publish log and pushes the job to the source callback queue (asynchronously). View the message in the publish log table, which is pub_log; this is business data that will ultimately be displayed to the user, as shown in Figure 5.

After the publish job fails, it inserts a publish log and pushes the job to the source callback queue (asynchronously). View the message in the publish log table, which is pub_log; this is business data that will ultimately be displayed to the user.
Figure 5
Plaintext

Based on task ID: 2, find the list of enabled resources is empty

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

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

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

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

关于我  ·  GitHub  ·  邮件联系