1. On the previous publishing interface, a single publish action only supported one channel. Therefore, publishing to 3 channels (APP, NetEase, and Weibo) required 3 separate API calls, as shown in Figure 1.

2. On the current publishing interface, a single publish action can publish to multiple channels. The prototype is shown in Figure 2.

3. In the channel publishing API, the publishing of each channel’s task (article) is isolated. It does not support publishing to multiple channels in a single request, as shown in Figure 3.

4. We now need to support publishing to multiple channels in a single request. The channel publishing service provides an API for one-click multi-channel publishing. There are 2 approaches. The first approach is to make HTTP calls to other channels’ publishing APIs from within the API. The second approach is to call other controller actions from within the API’s controller action, because the entry point of an API is essentially a controller action. We ultimately decided to adopt the second approach.
5. Edit /api/controllers/TaskGroupController.php and override the afterAction() method. Run other action IDs inside it.
<?php
/**
* Created by PhpStorm.
* User: Qiang Wang
* Date: 2020/03/16
* Time: 14:47
*/
namespace api\controllers;
use Yii;
use yii\base\InvalidConfigException;
use yii\db\Exception;
use yii\rest\ActiveController;
/**
* Class TaskGroupController
* @package api\controllers
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class TaskGroupController extends ActiveController
{
public $serializer = [
'class' => 'api\rests\task_group\Serializer',
'collectionEnvelope' => 'items',
];
/**
* @inheritdoc
*/
public function actions()
{
$actions = parent::actions();
// 禁用"view"、"update"、"delete"、"options"动作
unset($actions['view'], $actions['update'], $actions['delete'], $actions['options']);
$actions['index']['class'] = 'api\rests\task_group\IndexAction';
$actions['create']['class'] = 'api\rests\task_group\CreateAction';
return $actions;
}
/**
* {@inheritdoc}
* @throws InvalidConfigException if a registered parser does not implement the [[RequestParserInterface]].
* @throws Exception execution failed
*/
public function afterAction($action, $result)
{
if ($action->id == 'create' && $result['code'] === 10000) {
// 返回所有请求参数
$request = Yii::$app->request;
$requestParams = $request->getBodyParams();
// 添加请求参数
$requestParams['task_group']['id'] = $result['data']['id'];
$requestParams['task_group']['uuid'] = $result['data']['uuid'];
$request->setBodyParams($requestParams);
$requestParams = $request->getBodyParams();
// 运行操作ID:index
$this->runAction('index');
}
return parent::afterAction($action, $result);
}
}
6. Requesting IndexAction throws an error: Method Not Allowed. Method Not Allowed. This URL can only handle the following request methods: GET, HEAD. It appears that calling other controller actions from within the API’s controller action is restricted by the request type. For security reasons, we temporarily cannot lift these restrictions. As shown in Figure 4.

{
"name": "Method Not Allowed",
"message": "Method Not Allowed. This URL can only handle the following request methods: GET, HEAD.",
"code": 0,
"status": 405,
"type": "yii\\web\\MethodNotAllowedHttpException"
}
7. Moreover, each channel’s publishing API is distributed across independent applications such as qq and wx. The api application cannot run action IDs belonging to the qq and wx applications. Therefore, we decided to adopt the first approach: making HTTP calls to other channels’ publishing APIs from within the API. As shown in Figure 5.

8. Edit /api/controllers/TaskGroupController.php and add a filter named taskGroupFilter in the behaviors() method.
<?php
/**
* Created by PhpStorm.
* User: Qiang Wang
* Date: 2020/03/16
* Time: 14:47
*/
namespace api\controllers;
use api\filters\TaskGroupFilter;
use yii\rest\ActiveController;
/**
* Class TaskGroupController
* @package api\controllers
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class TaskGroupController extends ActiveController
{
public $serializer = [
'class' => 'api\rests\task_group\Serializer',
'collectionEnvelope' => 'items',
];
/**
* {@inheritdoc}
*/
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['taskGroupFilter'] = [
'class' => TaskGroupFilter::className(),
'only' => ['create'],
];
return $behaviors;
}
/**
* @inheritdoc
*/
public function actions()
{
$actions = parent::actions();
// 禁用"view"、"update"、"delete"、"options"动作
unset($actions['view'], $actions['update'], $actions['delete'], $actions['options']);
$actions['index']['class'] = 'api\rests\task_group\IndexAction';
$actions['create']['class'] = 'api\rests\task_group\CreateAction';
return $actions;
}
}
9. Task group (i.e., one-click multi-channel publishing) filter. Create /api/filters/TaskGroupFilter.php.
<?php
/**
* Created by PhpStorm.
* User: Qiang Wang
* Date: 2020/03/18
* Time: 16:27
*/
namespace api\filters;
use Yii;
use api\services\TaskGroupService;
use yii\base\ActionFilter;
use yii\base\InvalidConfigException;
use yii\httpclient\Exception;
/**
* 任务组(即一键多渠道发布)过滤器
* @package api\filters
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class TaskGroupFilter extends ActionFilter
{
/**
* {@inheritdoc}
*
* @throws Exception
* @throws InvalidConfigException
*/
public function afterAction($action, $result)
{
if ($result['code'] === 10000) {
// 返回所有请求参数
$request = Yii::$app->request;
$requestParams = $request->getBodyParams();
// 添加请求参数
$requestParams['task_group']['id'] = $result['data']['id'];
$requestParams['task_group']['uuid'] = $result['data']['uuid'];
/* 操作数据(一键发布至多渠道)(同步) */
TaskGroupService::createMultipleSync($requestParams);
}
return parent::afterAction($action, $result);
}
}
10. Implement the one-click multi-channel publishing (synchronous) method in the task group service class. Currently, only Penguin and WeChat are implemented. createMultipleSync($requestParams). Edit /common/services/TaskGroupService.php.
<?php
/**
* Created by PhpStorm.
* User: Qiang Wang
* Date: 2020/03/18
* Time: 10:23
*/
namespace common\services;
use common\logics\ArticleType;
use common\logics\Task;
use common\logics\WxArticle;
use common\logics\WxArticleNews;
use Yii;
use common\logics\Channel;
use common\logics\QqArticleNormal;
use common\logics\http\channel_pub_api\Article as HttpChannelPubApiArticle;
use yii\base\InvalidConfigException;
use yii\httpclient\Exception;
use yii\web\ServerErrorHttpException;
/**
* Class TaskGroupService
* @package common\services
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class TaskGroupService extends Service
{
/**
* 创建任务组
* @param object $model 对象
*
* @param bool $runValidation 保存记录之前是否执行验证 (调用 [[validate()]]),默认为 true
*
* @return array
* 格式如下:
*
* [
* 'status' => true, // 成功
* 'data' => [ // object
* ]
* ]
*
* [
* 'status' => false, // 失败
* 'code' => 202184, // 返回码
* 'message' => '', // 说明
* ]
*
* @throws ServerErrorHttpException
*/
public function create($model, $runValidation = true)
{
if ($model->save($runValidation)) {
return ['status' => true, 'data' => $model];
} elseif ($model->hasErrors()) {
$firstError = '';
foreach ($model->getFirstErrors() as $message) {
$firstError = $message;
break;
}
return ['status' => false, 'code' => 202184, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '202184'), ['first_error' => $firstError]))];
} elseif (!$model->hasErrors()) {
throw new ServerErrorHttpException('Failed to create the object (task group) for unknown reason.');
}
}
/**
* 一键发布至多渠道(同步)
*
* @param array $requestParams 请求参数
*
* @throws Exception
* @throws InvalidConfigException
*/
public static function createMultipleSync($requestParams)
{
// 请求数据
$requestDatas = [];
// 判断渠道:企鹅号是否存在,准备对应渠道的数据
if (isset($requestParams[Channel::CODE_QQ])) {
foreach ($requestParams[Channel::CODE_QQ] as $key => $value) {
$value['source'] = $requestParams['task_group']['source'];
$value['source_uuid'] = $requestParams['task_group']['source_uuid'];
$value['source_pub_user_id'] = $requestParams['task_group']['source_pub_user_id'];
$value['source_callback_url'] = $requestParams['task_group']['source_callback_url'];
$value['task_group_id'] = $requestParams['task_group']['id'];
$value['task_group_uuid'] = $requestParams['task_group']['uuid'];
$value['cover_pics'] = $value['covers'] ?? [];
$value['apply'] = $value['original'] ?? QqArticleNormal::APPLY_DEFAULT;
unset($value['covers'], $value['original']);
$requestDatas[] = [
'url' => 'qq/v1/articles/standard',
'data' => $value,
];
}
}
// 判断渠道:微信公众帐号是否存在,准备对应渠道的数据
if (isset($requestParams[Channel::CODE_WX])) {
foreach ($requestParams[Channel::CODE_WX] as $key => $value) {
$value['app_ids'] = $value['channel_app_source_uuids'] ?? [];
$value['source'] = $requestParams['task_group']['source'];
$value['source_uuid'] = $requestParams['task_group']['source_uuid'];
$value['source_pub_user_id'] = $requestParams['task_group']['source_pub_user_id'];
$value['source_callback_url'] = $requestParams['task_group']['source_callback_url'];
$value['task_group_id'] = $requestParams['task_group']['id'];
$value['task_group_uuid'] = $requestParams['task_group']['uuid'];
$value['thumb'] = $value['covers'][0] ?? '';
$value['show_cover_pic'] = $value['cover_show'] ?? WxArticleNews::SHOW_COVER_PIC_DEFAULT;
$value['url'] = $value['source_url'] ?? '';
unset($value['covers'], $value['cover_show'], $value['source_url']);
$value['code'] = ArticleType::CODE_STANDARD;
$value['type'] = Task::TYPE_PUB;
$value['wx_send_type'] = WxArticle::WX_SEND_TYPE_MASS;
$requestDatas[] = [
'url' => 'wx/v1/wx-articles/article',
'data' => $value,
];
}
}
// 批量发布文章类型:标准(普通、图文)的文章
$httpChannelPubApiArticle = new HttpChannelPubApiArticle();
$httpChannelPubApiArticle->batchPostArticlesStandard(Yii::$app->params['groupId'], $requestDatas);
}
}
11. Implement the HTTP model class, the base class for the channel publishing API. Create /common/logics/http/channel_pub_api/Model.php.
<?php
/**
* Created by PhpStorm.
* User: Qiang Wang
* Date: 2020/03/20
* Time: 13:56
*/
namespace common\logics\http\channel_pub_api;
use Yii;
use yii\httpclient\Client;
use yii\httpclient\CurlTransport;
use yii\web\ServerErrorHttpException;
/**
* 渠道发布接口的基础类
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class Model extends \yii\base\Model
{
private $_httpClient;
/*
* 创建 HTTP 客户端对象
*
* @return object the created object
* @throws InvalidConfigException if a registered parser does not implement the [[RequestParserInterface]].
*/
public function getHttpClient()
{
if (!is_object($this->_httpClient)) {
$this->_httpClient = Yii::createObject([
'class' => Client::className(),
'baseUrl' => Yii::$app->params['channelPubApi']['hostInfo'] . Yii::$app->params['channelPubApi']['baseUrl'],
'requestConfig' => [
'format' => Client::FORMAT_JSON
],
'transport' => CurlTransport::className(),
]);
}
return $this->_httpClient;
}
/*
* 响应对象的处理
*
* @param object $response 响应对象
*
* @return array|bool
*
* 格式如下:
*
* 渠道发布接口的响应信息
* [
* 'message' => '', //说明
* 'data' => [], //数据
* ]
*
* 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
* false
*
* @throws ServerErrorHttpException 如果响应状态码不等于20x
*/
public function responseHandler($response)
{
// 检查响应状态码是否等于20x
$responseCode = $response->data['code'] ?? 0; // 返回码
if ($response->isOk) {
// 检查业务逻辑是否成功
if ($responseCode === 10000) {
return ['message' => $response->data['message'], 'data' => $response->data['data']];
} else {
$this->addError('id', Yii::t('error', Yii::t('error', Yii::t('error', '202186'), ['code' => $responseCode, 'message' => $response->data['message']])));
return !$this->hasErrors();
}
} else {
$responseMessage = $response->data['message'] ?? ''; // 说明
throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '202185'), ['status_code' => $response->statusCode, 'code' => $responseCode, 'message' => $responseMessage])), 202185);
}
}
}
12. Implement the HTTP model class for the channel publishing API’s articles. Batch publish standard article types (normal, image-text). Create /common/logics/http/channel_pub_api/Article.php.
<?php
/**
* Created by PhpStorm.
* User: Qiang Wang
* Date: 2020/03/20
* Time: 14:19
*/
namespace common\logics\http\channel_pub_api;
use Yii;
use yii\base\InvalidConfigException;
use yii\httpclient\Client;
use yii\httpclient\Exception;
/**
* 渠道发布接口的文章
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class Article extends Model
{
/**
* 批量发布文章类型:标准(普通、图文)的文章
*
* @param string $groupId 租户ID
* @param array $requestDatas 请求数据
*
* @throws Exception
* @throws InvalidConfigException
*/
public function batchPostArticlesStandard($groupId, $requestDatas)
{
$client = new Client();
$requests = [];
foreach ($requestDatas as $requestData) {
$requests[] = $this->httpClient->createRequest()
->setMethod('post')
->setUrl($requestData['url'] . '?group_id=' . $groupId)
->setData($requestData['data']);
}
$client->batchSend($requests);
}
}
13. POST request: http://api.channel-pub-api.localhost/v1/task-groups?group_id=015ce30b116ce86058fa6ab4fea4ac63 . Create a task group (i.e., one-click multi-channel publishing). Successful response. As shown in Figure 6.

{
"source": {
"channel_app_source_uuids": [
"55f62c8c67fc11ea809554ee75d2ebc1",
"36cf694a67fc11eaa79d54ee75d2ebc1",
"29473624681311eaab8e54ee75d2ebc1"
],
"title": "北京小汤山医院启用,设千张床位",
"content": "3月16日晚拍摄的北京小汤山医院局部(无人机照片)。记者从北京市疫情防控领导小组获悉,为做好境外输入人员疫情防控工作,3月16日起,启用北京小汤山医院。医院设床位1000余张,将主要用于境外来(返)京人员中需筛查人员、疑似病例及轻型、普通型确诊患者的筛查和治疗。",
"source": "spider",
"source_uuid": "825e6d5e36468cc4bf536799ce3565cf",
"source_pub_user_id": 1,
"source_callback_url": "http://scms.wjdev.chinamcloud.cn/api/thirdPush/callBack",
"task_group_source_article_id": 1,
"baijia_source_article_id": 2,
"baijia_author": "鞠焕宗",
"baijia_covers": [
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRG00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRH00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRI00AN0001NOS.jpg"
],
"baijia_original": 0,
"baijia_original_url": "http://news.163.com/photoview/00AN0001/2307424.html",
"netease_source_article_id": 3,
"netease_author": "鞠焕宗",
"netease_article_category_id": 417,
"netease_covers": [
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRG00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRH00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRI00AN0001NOS.jpg"
],
"netease_cover_type": "threeImg",
"netease_original": 0,
"qq_source_article_id": 4,
"qq_author": "鞠焕宗",
"qq_article_category_id": 24,
"qq_tag": "北京,疫情,防控,境外,输入,小汤山,医院",
"qq_covers": [
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRG00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRH00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRI00AN0001NOS.jpg"
],
"qq_cover_type": 3,
"qq_original": 0,
"qq_original_platform": 0,
"qq_original_url": "",
"qq_original_author": "",
"weibo_source_article_id": 5,
"weibo_summary": "记者从北京市疫情防控领导小组获悉,为做好境外输入人员疫情防控工作,3月16日起,启用北京小汤山医院。医院设床位1000余张,将主要用于境外来(返)京人员中需筛查人员、疑似病例及轻型、普通型确诊患者的筛查和治疗。",
"weibo_link_content": "记者从北京市疫情防控领导小组获悉,为做好境外输入人员疫情防控工作,3月16日起,启用北京小汤山医院。医院设床位1000余张,将主要用于境外来(返)京人员中需筛查人员、疑似病例及轻型、普通型确诊患者的筛查和治疗。",
"weibo_covers": [
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRG00AN0001NOS.jpg"
],
"wx_source_article_id": 6,
"wx_author": "鞠焕宗",
"wx_covers": [
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRG00AN0001NOS.jpg"
],
"wx_cover_show": 1,
"wx_description": "记者从北京市疫情防控领导小组获悉,为做好境外输入人员疫情防控工作,3月16日起,启用北京小汤山医院。医院设床位1000余张,将主要用于境外来(返)京人员中需筛查人员、疑似病例及轻型、普通型确诊患者的筛查和治疗。",
"wx_source_url": "http://news.163.com/photoview/00AN0001/2307424.html",
"toutiao_source_article_id": 7,
"toutiao_source_url": "http://news.163.com/photoview/00AN0001/2307424.html"
},
"task_group": {
"source": "spider",
"source_uuid": "825e6d5e36468cc4bf536799ce3565cf",
"source_pub_user_id": 1,
"source_callback_url": "http://scms.wjdev.chinamcloud.cn/api/thirdPush/callBack",
"source_article_id": 1
},
"baijia": [
{
"channel_app_source_uuids": [
"29473624681311eaab8e54ee75d2ebc1"
],
"title": "北京小汤山医院启用,设千张床位",
"content": "3月16日晚拍摄的北京小汤山医院局部(无人机照片)。记者从北京市疫情防控领导小组获悉,为做好境外输入人员疫情防控工作,3月16日起,启用北京小汤山医院。医院设床位1000余张,将主要用于境外来(返)京人员中需筛查人员、疑似病例及轻型、普通型确诊患者的筛查和治疗。",
"source_article_id": 2,
"author": "鞠焕宗",
"covers": [
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRG00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRH00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRI00AN0001NOS.jpg"
],
"original": 0,
"original_url": "http://news.163.com/photoview/00AN0001/2307424.html"
}
],
"netease": [
{
"channel_app_source_uuids": [
"29473624681311eaab8e54ee75d2ebc1"
],
"title": "北京小汤山医院启用,设千张床位",
"content": "3月16日晚拍摄的北京小汤山医院局部(无人机照片)。记者从北京市疫情防控领导小组获悉,为做好境外输入人员疫情防控工作,3月16日起,启用北京小汤山医院。医院设床位1000余张,将主要用于境外来(返)京人员中需筛查人员、疑似病例及轻型、普通型确诊患者的筛查和治疗。",
"source_article_id": 3,
"author": "鞠焕宗",
"article_category_id": 417,
"covers": [
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRG00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRH00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRI00AN0001NOS.jpg"
],
"cover_type": "threeImg",
"original": 0
}
],
"qq": [
{
"channel_app_source_uuids": [
"55f62c8c67fc11ea809554ee75d2ebc1"
],
"title": "北京小汤山医院启用,设千张床位",
"content": "3月16日晚拍摄的北京小汤山医院局部(无人机照片)。记者从北京市疫情防控领导小组获悉,为做好境外输入人员疫情防控工作,3月16日起,启用北京小汤山医院。医院设床位1000余张,将主要用于境外来(返)京人员中需筛查人员、疑似病例及轻型、普通型确诊患者的筛查和治疗。",
"source_article_id": 4,
"author": "鞠焕宗",
"article_category_id": 24,
"tag": "北京,疫情,防控,境外,输入,小汤山,医院",
"covers": [
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRG00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRH00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRI00AN0001NOS.jpg"
],
"cover_type": 3,
"original": 0,
"original_platform": 0,
"original_url": "",
"original_author": ""
},
{
"channel_app_source_uuids": [
"36cf694a67fc11eaa79d54ee75d2ebc1"
],
"title": "北京小汤山医院启用,设千张床位",
"content": "3月16日晚拍摄的北京小汤山医院局部(无人机照片)。记者从北京市疫情防控领导小组获悉,为做好境外输入人员疫情防控工作,3月16日起,启用北京小汤山医院。医院设床位1000余张,将主要用于境外来(返)京人员中需筛查人员、疑似病例及轻型、普通型确诊患者的筛查和治疗。",
"source_article_id": 4,
"author": "鞠焕宗",
"article_category_id": 24,
"tag": "北京,疫情,防控,境外,输入,小汤山,医院",
"covers": [
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRJ00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRH00AN0001NOS.jpg",
"http://pic-bucket.ws.126.net/photo/0001/2020-03-17/F7TNMMRI00AN0001NOS.jpg"
],
"cover_type": 3,
"original": 0,
"original_platform": 0,
"original_url": "",
"original_author": ""
}
],
"weibo": [
{
"channel_app_source_uuids": [
"渠道的应用的来源ID(UUID)"
],
"title": "北京小汤山医院启用,设千张床位",
"content": "内容",
"source_article_id": "来源文章ID",
"summary": "导语",
"link_content": "链接内容",
"covers": [
"封面图"
]
}
],
"wx": [
{
"channel_app_source_uuids": [
"渠道的应用的来源ID(UUID)"
],
"title": "北京小汤山医院启用,设千张床位",
"content": "内容",
"source_article_id": "来源文章ID",
"author": "作者",
"covers": [
"封面图"
],
"cover_show": "封面图是否显示",
"description": "描述",
"source_url": "来源链接"
}
],
"toutiao": [
{
"channel_app_source_uuids": [
"渠道的应用的来源ID(UUID)"
],
"title": "北京小汤山医院启用,设千张床位",
"content": "内容",
"source_article_id": "来源文章ID",
"source_url": "来源链接"
}
]
}
{
"code": 10000,
"message": "已发布文章至渠道发布,待发布至多渠道,请稍候",
"data": {
"source_article_id": 1,
"is_deleted": 0,
"deleted_at": 0,
"status": 1,
"created_at": 1584758252,
"updated_at": 1584758252,
"uuid": "eba334aa6b1c11ea8d7154ee75d2ebc1",
"id": 63
}
}
14. View the Log Messages of the api application. It made 2 requests to Penguin and 1 request to WeChat. As shown in Figure 7.

15. View the Log Messages of the qq application. It made 2 requests to Penguin. As shown in Figure 8.

16. View the Log Messages of the wx application. It made 1 request to WeChat. Figure 9.

17. View the data in the MySQL table. It wrote 2 records for Penguin and 1 record for WeChat, which meets expectations. Other channels can be handled similarly following the same rules. As shown in Figure 10.

