RESTful – 永夜 https://www.shuijingwanwq.com 没有不值得去解决的问题,也没有不值得去学习的技术! Sun, 17 May 2026 09:36:09 +0000 zh-Hans hourly 1 https://wordpress.org/?v=7.0 Go + Gin 实战 RESTful API:从环境搭建(WSL/Docker/VS Code)到 Git 提交与 GitHub 托管 https://www.shuijingwanwq.com/2026/04/03/9416/ https://www.shuijingwanwq.com/2026/04/03/9416/#respond Fri, 03 Apr 2026 04:11:30 +0000 https://www.shuijingwanwq.com/?p=9416 Post Views: 196

1、参考:使用 Go 和 Gin 构建 RESTful API(Go.dev) https://go.dev/doc/tutorial/web-service-gin — 使用 Gin 构建简单 Web 服务的 Go 官方教程。

2、安装 Go。Docker Compose 一键启动(参考 Hyperf 项目化)。我想要 “写好配置,一条命令启动整个开发环境”,用 docker-compose.yml 最为方便,和 Hyperf 的 Docker Compose 用法几乎一样。创建项目文件夹:web-service-gin



ubuntu@DESKTOP-H4MGQIU:~/wwwroot$ mkdir web-service-gin
ubuntu@DESKTOP-H4MGQIU:~/wwwroot$ cd web-service-gin
ubuntu@DESKTOP-H4MGQIU:~/wwwroot/web-service-gin$ pwd
/home/ubuntu/wwwroot/web-service-gin


3、创建 docker-compose.yml



version: '3.8'

services:
  go:
    image: golang:1.23-alpine
    container_name: go-gin-tutorial
    working_dir: /app
    volumes:
      - ./:/app         # 本地代码挂载
    ports:
      - "8080:8080"     # 映射端口最好加引号
    tty: true
    stdin_open: true
    environment:
      - GOPROXY=https://goproxy.io,direct
    command: tail -f /dev/null   # 容器启动后保持运行状态


4、启动容器(进入 Go 环境), 报错:WSL Ubuntu 里只装了 Docker,没装 Docker Compose。



ubuntu@DESKTOP-H4MGQIU:~/wwwroot/web-service-gin$ docker-compose up -d
Command 'docker-compose' not found, but can be installed with:
sudo snap install docker          # version 28.4.0, or
sudo apt  install docker-compose  # version 1.29.2-1
See 'snap info docker' for additional versions.
ubuntu@DESKTOP-H4MGQIU:~/wwwroot/web-service-gin$ sudo apt update && sudo apt install -y docker-compose
[sudo] password for ubuntu:
Get:1 http://security.ubuntu.com/ubuntu jammy-security InRelease [129 kB]
Hit:2 https://download.docker.com/linux/ubuntu jammy InRelease
Hit:3 http://archive.ubuntu.com/ubuntu jammy InRelease
Hit:4 https://packages.redis.io/deb jammy InRelease
Get:5 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [128 kB]
Get:6 http://archive.ubuntu.com/ubuntu jammy-backports InRelease [127 kB]
Get:7 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages [3328 kB]
Get:8 http://archive.ubuntu.com/ubuntu jammy-updates/universe amd64 c-n-f Metadata [30.4 kB]
Fetched 3743 kB in 7s (575 kB/s)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
62 packages can be upgraded. Run 'apt list --upgradable' to see them.
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  docker-ce docker-ce-cli python3-attr python3-certifi python3-chardet python3-distutils python3-docker python3-dockerpty python3-docopt python3-dotenv python3-idna python3-jsonschema
  python3-lib2to3 python3-pyrsistent python3-requests python3-setuptools python3-texttable python3-urllib3 python3-websocket
Suggested packages:
  cgroupfs-mount | cgroup-lite python-attr-doc python-jsonschema-doc python3-openssl python3-socks python-requests-doc python-setuptools-doc
Recommended packages:
  docker.io
The following NEW packages will be installed:
  docker-compose python3-attr python3-certifi python3-chardet python3-distutils python3-docker python3-dockerpty python3-docopt python3-dotenv python3-idna python3-jsonschema
  python3-lib2to3 python3-pyrsistent python3-requests python3-setuptools python3-texttable python3-urllib3 python3-websocket
The following packages will be upgraded:
  docker-ce docker-ce-cli
2 upgraded, 18 newly installed, 0 to remove and 60 not upgraded.
Need to get 40.4 MB of archives.
After this operation, 7757 kB of additional disk space will be used.
Get:1 https://download.docker.com/linux/ubuntu jammy/stable amd64 docker-ce-cli amd64 5:29.3.1-1~ubuntu.22.04~jammy [16.4 MB]
Get:2 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 python3-lib2to3 all 3.10.8-1~22.04 [77.6 kB]
Get:3 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 python3-distutils all 3.10.8-1~22.04 [139 kB]
Get:4 http://archive.ubuntu.com/ubuntu jammy/main amd64 python3-certifi all 2020.6.20-1 [150 kB]
Get:5 http://archive.ubuntu.com/ubuntu jammy/main amd64 python3-chardet all 4.0.0-1 [98.0 kB]
Get:6 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 python3-idna all 3.3-1ubuntu0.1 [52.1 kB]
Get:7 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 python3-urllib3 all 1.26.5-1~exp1ubuntu0.6 [98.7 kB]
Get:8 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 python3-requests all 2.25.1+dfsg-2ubuntu0.3 [48.8 kB]
Get:9 http://archive.ubuntu.com/ubuntu jammy/universe amd64 python3-websocket all 1.2.3-1 [34.7 kB]
Get:10 http://archive.ubuntu.com/ubuntu jammy/universe amd64 python3-docker all 5.0.3-1 [89.3 kB]
Get:11 http://archive.ubuntu.com/ubuntu jammy/universe amd64 python3-dockerpty all 0.4.1-2 [11.1 kB]
Get:12 http://archive.ubuntu.com/ubuntu jammy/universe amd64 python3-docopt all 0.6.2-4 [26.9 kB]
Get:13 http://archive.ubuntu.com/ubuntu jammy/universe amd64 python3-dotenv all 0.19.2-1 [20.5 kB]
Get:14 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 python3-attr all 21.2.0-1ubuntu1 [43.9 kB]
Get:15 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 python3-setuptools all 59.6.0-1.2ubuntu0.22.04.3 [340 kB]
Get:16 http://archive.ubuntu.com/ubuntu jammy/main amd64 python3-pyrsistent amd64 0.18.1-1build1 [55.5 kB]
Get:17 http://archive.ubuntu.com/ubuntu jammy/main amd64 python3-jsonschema all 3.2.0-0ubuntu2 [43.1 kB]
Get:18 http://archive.ubuntu.com/ubuntu jammy/universe amd64 python3-texttable all 1.6.4-1 [11.4 kB]
Get:19 http://archive.ubuntu.com/ubuntu jammy/universe amd64 docker-compose all 1.29.2-1 [95.8 kB]
Get:20 https://download.docker.com/linux/ubuntu jammy/stable amd64 docker-ce amd64 5:29.3.1-1~ubuntu.22.04~jammy [22.6 MB]
Fetched 40.4 MB in 10s (3969 kB/s)
(Reading database ... 24579 files and directories currently installed.)
Preparing to unpack .../00-docker-ce-cli_5%3a29.3.1-1~ubuntu.22.04~jammy_amd64.deb ...
Unpacking docker-ce-cli (5:29.3.1-1~ubuntu.22.04~jammy) over (5:29.3.0-1~ubuntu.22.04~jammy) ...
Preparing to unpack .../01-docker-ce_5%3a29.3.1-1~ubuntu.22.04~jammy_amd64.deb ...
Unpacking docker-ce (5:29.3.1-1~ubuntu.22.04~jammy) over (5:29.3.0-1~ubuntu.22.04~jammy) ...
Selecting previously unselected package python3-lib2to3.
Preparing to unpack .../02-python3-lib2to3_3.10.8-1~22.04_all.deb ...
Unpacking python3-lib2to3 (3.10.8-1~22.04) ...
Selecting previously unselected package python3-distutils.
Preparing to unpack .../03-python3-distutils_3.10.8-1~22.04_all.deb ...
Unpacking python3-distutils (3.10.8-1~22.04) ...
Selecting previously unselected package python3-certifi.
Preparing to unpack .../04-python3-certifi_2020.6.20-1_all.deb ...
Unpacking python3-certifi (2020.6.20-1) ...
Selecting previously unselected package python3-chardet.
Preparing to unpack .../05-python3-chardet_4.0.0-1_all.deb ...
Unpacking python3-chardet (4.0.0-1) ...
Selecting previously unselected package python3-idna.
Preparing to unpack .../06-python3-idna_3.3-1ubuntu0.1_all.deb ...
Unpacking python3-idna (3.3-1ubuntu0.1) ...
Selecting previously unselected package python3-urllib3.
Preparing to unpack .../07-python3-urllib3_1.26.5-1~exp1ubuntu0.6_all.deb ...
Unpacking python3-urllib3 (1.26.5-1~exp1ubuntu0.6) ...
Selecting previously unselected package python3-requests.
Preparing to unpack .../08-python3-requests_2.25.1+dfsg-2ubuntu0.3_all.deb ...
Unpacking python3-requests (2.25.1+dfsg-2ubuntu0.3) ...
Selecting previously unselected package python3-websocket.
Preparing to unpack .../09-python3-websocket_1.2.3-1_all.deb ...
Unpacking python3-websocket (1.2.3-1) ...
Selecting previously unselected package python3-docker.
Preparing to unpack .../10-python3-docker_5.0.3-1_all.deb ...
Unpacking python3-docker (5.0.3-1) ...
Selecting previously unselected package python3-dockerpty.
Preparing to unpack .../11-python3-dockerpty_0.4.1-2_all.deb ...
Unpacking python3-dockerpty (0.4.1-2) ...
Selecting previously unselected package python3-docopt.
Preparing to unpack .../12-python3-docopt_0.6.2-4_all.deb ...
Unpacking python3-docopt (0.6.2-4) ...
Selecting previously unselected package python3-dotenv.
Preparing to unpack .../13-python3-dotenv_0.19.2-1_all.deb ...
Unpacking python3-dotenv (0.19.2-1) ...
Selecting previously unselected package python3-attr.
Preparing to unpack .../14-python3-attr_21.2.0-1ubuntu1_all.deb ...
Unpacking python3-attr (21.2.0-1ubuntu1) ...
Selecting previously unselected package python3-setuptools.
Preparing to unpack .../15-python3-setuptools_59.6.0-1.2ubuntu0.22.04.3_all.deb ...
Unpacking python3-setuptools (59.6.0-1.2ubuntu0.22.04.3) ...
Selecting previously unselected package python3-pyrsistent:amd64.
Preparing to unpack .../16-python3-pyrsistent_0.18.1-1build1_amd64.deb ...
Unpacking python3-pyrsistent:amd64 (0.18.1-1build1) ...
Selecting previously unselected package python3-jsonschema.
Preparing to unpack .../17-python3-jsonschema_3.2.0-0ubuntu2_all.deb ...
Unpacking python3-jsonschema (3.2.0-0ubuntu2) ...
Selecting previously unselected package python3-texttable.
Preparing to unpack .../18-python3-texttable_1.6.4-1_all.deb ...
Unpacking python3-texttable (1.6.4-1) ...
Selecting previously unselected package docker-compose.
Preparing to unpack .../19-docker-compose_1.29.2-1_all.deb ...
Unpacking docker-compose (1.29.2-1) ...
Setting up python3-dotenv (0.19.2-1) ...
Setting up python3-attr (21.2.0-1ubuntu1) ...
Setting up python3-texttable (1.6.4-1) ...
Setting up python3-docopt (0.6.2-4) ...
Setting up python3-chardet (4.0.0-1) ...
Setting up python3-certifi (2020.6.20-1) ...
Setting up python3-idna (3.3-1ubuntu0.1) ...
Setting up python3-urllib3 (1.26.5-1~exp1ubuntu0.6) ...
Setting up docker-ce-cli (5:29.3.1-1~ubuntu.22.04~jammy) ...
Setting up python3-pyrsistent:amd64 (0.18.1-1build1) ...
Setting up python3-lib2to3 (3.10.8-1~22.04) ...
Setting up python3-websocket (1.2.3-1) ...
Setting up python3-dockerpty (0.4.1-2) ...
Setting up python3-distutils (3.10.8-1~22.04) ...
Setting up python3-setuptools (59.6.0-1.2ubuntu0.22.04.3) ...
Setting up python3-jsonschema (3.2.0-0ubuntu2) ...
Setting up docker-ce (5:29.3.1-1~ubuntu.22.04~jammy) ...
Setting up python3-requests (2.25.1+dfsg-2ubuntu0.3) ...
Setting up python3-docker (5.0.3-1) ...
Setting up docker-compose (1.29.2-1) ...
Processing triggers for man-db (2.10.2-1) ...
ubuntu@DESKTOP-H4MGQIU:~/wwwroot/web-service-gin$ docker-compose --version
docker-compose version 1.29.2, build unknown
ubuntu@DESKTOP-H4MGQIU:~/wwwroot/web-service-gin$ docker-compose up -d
Creating network "web-service-gin_default" with the default driver
Pulling go (golang:1.23-alpine)...
1.23-alpine: Pulling from library/golang
4f4fb700ef54: Pull complete
9824c27679d3: Pull complete
8371a51cbc44: Pull complete
d5791340ef18: Pull complete
d3178a7b2709: Pull complete
28f6f92c256d: Download complete
d1ac4f409af1: Download complete
Digest: sha256:383395b794dffa5b53012a212365d40c8e37109a626ca30d6151c8348d380b5f
Status: Downloaded newer image for golang:1.23-alpine
Creating go-gin-tutorial ... done
ubuntu@DESKTOP-H4MGQIU:~/wwwroot/web-service-gin$ docker-compose exec go sh
/app # ls -la
total 12
drwxr-xr-x    2 1000     1000          4096 Mar 30 03:36 .
drwxr-xr-x    1 root     root          4096 Mar 30 03:40 ..
-rw-r--r--    1 1000     1000           272 Mar 30 03:36 docker-compose.yml


5、创建一个可以管理依赖项的模块。运行该go mod init命令,并指定代码所在模块的路径。



/app # go mod init example/web-service-gin
go: creating new go.mod: module example/web-service-gin


6、打算用 GoLand(Windows)直接编辑 WSL 里的 Go 代码,以配合我的 Docker 环境。我的 Go 代码存在 WSL Ubuntu 的文件夹里,GoLand 可以直接访问 WSL 文件,就像访问本地文件夹一样。现在的代码在这里:~/wwwroot/web-service-gin。在 Windows 里访问这个路径,就是:\wsl$\Ubuntu\home\ubuntu\wwwroot\web-service-gin。 GoLand 打开这个文件夹,打开 GoLand,点 Open(打开),把上面复制的路径直接粘贴到文件夹地址栏,按回车,选择文件夹 → OK。现在就能在 Windows GoLand 里编辑、修改、保存 WSL 里的 Go 代码了。而且修改会实时同步到 Docker 容器里!如图1

在 Windows 里访问这个路径,就是:\wsl$\Ubuntu\home\ubuntu\wwwroot\web-service-gin

7、配置 GoLand 连接 WSL 里的 Docker(关键)
重启后:
文件 → 设置 → 构建、执行、部署 → Docker
左上角 + 添加连接
连接类型选择:WSL(重点:不要选 Docker Desktop)
发行版下拉选择:Ubuntu
底部显示「连接成功」即可
应用 → 确定。如图2

配置 GoLand 连接 WSL 里的 Docker

8、文件 → 设置 → 运行目标,添加 → 选 Docker Compose,点击右上角 +选择:✅ Docker Compose。运行目标(Docker Compose)配置完成后,GoLand 会获得:只管【运行】,不管【代码提示】
它的作用只有:
点 ▶️ 运行时
代码在 Docker 容器里执行。如图3

文件 → 设置 → 运行目标,添加 → 选 Docker Compose

9、项目默认目标:go。如图4

项目默认目标:go

10、代码提示、自动补全、语法识别 必须靠 GOROOT
GoLand 要想识别:
string
int
fmt
函数跳转
自动补全
必须配置:本地 GOROOT这是 IDE 硬性规则。打开 WSL Ubuntu 终端,复制运行:



# 1. 下载 Go 1.23(和容器版本一样)
wget https://dl.google.com/go/go1.23.0.linux-amd64.tar.gz

# 2. 安装到 Linux 标准路径:/usr/local/go
sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz

# 3. 配置环境变量
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc

# 4. 验证安装成功
go version


11、打开 GoLand 设置 → Go → GOROOT
点 + 添加
选择 WSL
自动找到 /usr/local/go
确定 → 应用。如图5

打开 GoLand 设置 → Go → GOROOT

12、创建数据,创建一个名为 main.go 的文件。将在这个文件中编写 Go 代码。



package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

// album represents data about a record album.
type album struct {
	ID     string  `json:"id"`
	Title  string  `json:"title"`
	Artist string  `json:"artist"`
	Price  float64 `json:"price"`
}

// albums slice to seed record album data.
var albums = []album{
	{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
	{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
	{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func main() {
	router := gin.Default()
	router.GET("/albums", getAlbums)

	router.Run("0.0.0.0:8080") // 监听所有网卡
}

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
	c.IndentedJSON(http.StatusOK, albums)
}


13、现在 IDE 有一些报错的提示:

无法解析符号’github.com’
无法解析符号’gin-gonic’
无法解析符号’gin’
未解析的引用 ‘Default’。如图6

现在 IDE 有一些报错的提示

14、打开 WSL 终端,在 WSL 直接执行:go get github.com/gin-gonic/gin



ubuntu@DESKTOP-H4MGQIU:~/wwwroot/web-service-gin$ go get github.com/gin-gonic/gin
go: downloading github.com/gin-gonic/gin v1.12.0
go: github.com/gin-gonic/gin@v1.12.0 requires go >= 1.25.0; switching to go1.25.8
go: downloading github.com/gin-contrib/sse v1.1.0
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/quic-go/quic-go v0.59.0
go: downloading golang.org/x/net v0.51.0
go: downloading github.com/bytedance/sonic v1.15.0
go: downloading github.com/goccy/go-json v0.10.5
go: downloading github.com/json-iterator/go v1.1.12
go: downloading github.com/goccy/go-yaml v1.19.2
go: downloading github.com/pelletier/go-toml/v2 v2.2.4
go: downloading github.com/ugorji/go/codec v1.3.1
go: downloading go.mongodb.org/mongo-driver/v2 v2.5.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/go-playground/validator/v10 v10.30.1
go: downloading golang.org/x/sys v0.41.0
go: downloading github.com/quic-go/qpack v0.6.0
go: downloading github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd
go: downloading github.com/modern-go/reflect2 v1.0.2
go: downloading github.com/gabriel-vasile/mimetype v1.4.12
go: downloading github.com/go-playground/universal-translator v0.18.1
go: downloading github.com/leodido/go-urn v1.4.0
go: downloading golang.org/x/crypto v0.48.0
go: downloading golang.org/x/text v0.34.0
go: downloading github.com/bytedance/gopkg v0.1.3
go: downloading github.com/cloudwego/base64x v0.1.6
go: downloading golang.org/x/arch v0.22.0
go: downloading github.com/go-playground/locales v0.14.1
go: downloading github.com/klauspost/cpuid/v2 v2.3.0
go: downloading github.com/bytedance/sonic/loader v0.5.0
go: downloading github.com/twitchyliquid64/golang-asm v0.15.1
go: updating go.mod: open /home/ubuntu/wwwroot/web-service-gin/go.mod: permission denied

15、反省一下,感觉现在这样太痛苦了。这种 “容器里装依赖、容器外装依赖” 的用法,是错误、混乱、反人类的!** 容器的优势就是环境统一、隔离、不用乱装东西。现在被用成了:一边容器、一边 WSL,精神分裂!真正正确的开发模式只有两种:只用 WSL 的 Go,不用 Docker 跑开发;全部在 Docker 里,WSL 什么都不装;

16、仔细排查原因,找到了根源,在 文件 → 设置 → 构建、执行、部署 → Docker 中,虚拟机路径:/home/ubuntu/wwwroot/web-service-gin,这个路径是 WSL 中的路径。** 正确的路径 不应该是 WSL 路径!应该是【容器内部的路径】!**。虚拟机路径修改为:/app。如图7

虚拟机路径修改为:/app

17、发现 GOROOT 中仍然不提供 Docker 的选项。GoLand 在「没有 Docker Desktop」的环境下,就是无法直接选择容器内的 GOROOT!最终决定,放弃 GoLand,改用 VS Code + Dev Containers

18、Windows 安装 VS Code,VS Code 扩展商店安装:Dev Containers(微软官方)、WSL、Docker

19、启动容器,报错:ERROR: for go ‘ContainerConfig’,这是老容器 + Python V1 的兼容性问题

<pre class="wp-block-syntaxhighlighter-code">

ubuntu@DESKTOP-H4MGQIU:~/wwwroot/web-service-gin$ docker-compose up -d
Recreating 814a800e96e6_go-gin-tutorial ...

ERROR: for 814a800e96e6_go-gin-tutorial  'ContainerConfig'

ERROR: for go  'ContainerConfig'
Traceback (most recent call last):
  File "/usr/bin/docker-compose", line 33, in <module>
    sys.exit(load_entry_point('docker-compose==1.29.2', 'console_scripts', 'docker-compose')())
  File "/usr/lib/python3/dist-packages/compose/cli/main.py", line 81, in main
    command_func()
  File "/usr/lib/python3/dist-packages/compose/cli/main.py", line 203, in perform_command
    handler(command, command_options)
  File "/usr/lib/python3/dist-packages/compose/metrics/decorator.py", line 18, in wrapper
    result = fn(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/compose/cli/main.py", line 1186, in up
    to_attach = up(False)
  File "/usr/lib/python3/dist-packages/compose/cli/main.py", line 1166, in up
    return self.project.up(
  File "/usr/lib/python3/dist-packages/compose/project.py", line 697, in up
    results, errors = parallel.parallel_execute(
  File "/usr/lib/python3/dist-packages/compose/parallel.py", line 108, in parallel_execute
    raise error_to_reraise
  File "/usr/lib/python3/dist-packages/compose/parallel.py", line 206, in producer
    result = func(obj)
  File "/usr/lib/python3/dist-packages/compose/project.py", line 679, in do
    return service.execute_convergence_plan(
  File "/usr/lib/python3/dist-packages/compose/service.py", line 579, in execute_convergence_plan
    return self._execute_convergence_recreate(
  File "/usr/lib/python3/dist-packages/compose/service.py", line 499, in _execute_convergence_recreate
    containers, errors = parallel_execute(
  File "/usr/lib/python3/dist-packages/compose/parallel.py", line 108, in parallel_execute
    raise error_to_reraise
  File "/usr/lib/python3/dist-packages/compose/parallel.py", line 206, in producer
    result = func(obj)
  File "/usr/lib/python3/dist-packages/compose/service.py", line 494, in recreate
    return self.recreate_container(
  File "/usr/lib/python3/dist-packages/compose/service.py", line 612, in recreate_container
    new_container = self.create_container(
  File "/usr/lib/python3/dist-packages/compose/service.py", line 330, in create_container
    container_options = self._get_container_create_options(
  File "/usr/lib/python3/dist-packages/compose/service.py", line 921, in _get_container_create_options
    container_options, override_options = self._build_container_volume_options(
  File "/usr/lib/python3/dist-packages/compose/service.py", line 960, in _build_container_volume_options
    binds, affinity = merge_volume_bindings(
  File "/usr/lib/python3/dist-packages/compose/service.py", line 1548, in merge_volume_bindings
    old_volumes, old_mounts = get_container_data_volumes(
  File "/usr/lib/python3/dist-packages/compose/service.py", line 1579, in get_container_data_volumes
    container.image_config['ContainerConfig'].get('Volumes') or {}
KeyError: 'ContainerConfig'

</pre>

20、进入目录:~/wwwroot/web-service-gin,执行 :code .。是否信任此文件夹中的文件的作者? 是。如图8

进入目录:~/wwwroot/web-service-gin,执行 :code .。是否信任此文件夹中的文件的作者? 是


ubuntu@DESKTOP-H4MGQIU:~/wwwroot/web-service-gin$ code .
Installing VS Code Server for Linux x64 (cfbea10c5ffb233ea9177d34726e6056e89913dc)
Downloading: 100%
Unpacking: 100%
Unpacked 3690 files and folders to /home/ubuntu/.vscode-server/bin/cfbea10c5ffb233ea9177d34726e6056e89913dc.
Looking for compatibility check script at /home/ubuntu/.vscode-server/bin/cfbea10c5ffb233ea9177d34726e6056e89913dc/bin/helpers/check-requirements.sh
Running compatibility check script
Compatibility check successful (0)


21、删除 .idea 目录,这是 Goland 留下的。准备 Dev Container 配置文件,直接用已有 Docker 镜像,新建:.devcontainer/devcontainer.json


{
  "name": "Go Gin Dev Container",
  "dockerComposeFile": ["../docker-compose.yml"],
  "service": "go",
  "workspaceFolder": "/app",
  "settings": {
    "terminal.integrated.shell.linux": "/bin/sh"
  },
  "extensions": [
    "golang.Go"
  ],
  "forwardPorts": [8080],
  "remoteUser": "root"
}

解释:

dockerComposeFile 指向 docker-compose.yml
service 是希望 VS Code 连接的服务(这里是 go)
workspaceFolder 是代码挂载目录 /app
extensions 自动在容器里安装 Go 插件
forwardPorts 映射容器端口到 VS Code
remoteUser 可选,如果容器里默认用户不是 root

22、VS Code 打开容器
打开 VS Code,进入项目目录
按 F1 → 输入 Dev Containers: Reopen in Container
VS Code 会根据 devcontainer.json 和 docker-compose.yml 启动容器并挂载代码
打开终端,会发现直接在容器里(/app),Go 版本和 Docker 镜像一致

⚡ 提示:如果容器启动慢,可以先用 docker-compose up -d 启动容器,再用 Dev Container 连接。如图9

VS Code 打开容器

23、开发流程建议
用 Dev Container 打开项目
直接在容器终端运行 Go 或 PHP 命令
容器里安装依赖(go mod tidy、composer install 等)
在 VS Code 中调试、运行、测试
不需要关心 WSL 上是否安装 Go/PHP

24、多项目 + 多容器流程图

┌─────────────────────────────────────────────────────────────┐
│ VS Code │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Project-Go │ │ Project-PHP74 │ │ Project-PHP81 │ │
│ │ Workspace │ │ Workspace │ │ Workspace │ │
│ │ (.code-workspace) │ │ (.code-workspace) │ │ (.code-workspace) │ │
│ │ Dev Container │ │ Dev Container │ │ Dev Container │ │
│ │ Service: go │ │ Service: php74 │ │ Service: php81 │ │
│ │ Image: golang:1.23 │ │ Image: php:7.4-fpm │ │ Image: php:8.1-fpm │ │
│ │ Ports: 8080 │ │ Ports: 8000 │ │ Ports: 8001 │ │
│ └─────────────┬───────┘ └─────────────┬───────┘ └─────────────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Docker Container Go │ │ Docker Container │ │ Docker Container │ │
│ │ Version: 1.23 │ │ PHP 7.4 FPM │ │ PHP 8.1 FPM │ │
│ │ /app ← 挂载项目代码 │ │ /app ← 挂载项目代码 │ │ /app ← 挂载项目代码 │ │
│ │ 端口 8080 映射本地 │ │ 端口 8000 映射本地 │ │ 端口 8001 映射本地 │ │
│ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

说明:

  • 每个 VS Code 窗口对应一个独立 Workspace(项目)
  • 每个 Workspace 使用独立 Dev Container 连接对应 Docker 容器
  • 容器内挂载 /app 到对应项目代码,开发和运行环境完全一致
  • 不同项目端口独立映射,本地访问互不冲突

25、以下是 web-service-gin 项目的完整流程图,显示 VS Code、Dev Container、Docker 容器、挂载路径和端口映射,完全贴合本地开发环境。

┌───────────────────────────────────────────────────────────────────────────────┐
│ VS Code Window │
│ (web-service-gin Workspace) │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Dev Container (Go) │ │
│ │ Service: go │ │
│ │ Image: golang:1.23-alpine │ │
│ │ WorkspaceFolder: /app │ │
│ │ Extensions: golang.Go │ │
│ │ ForwardPorts: 8080 │ │
│ │ RemoteUser: root │ │
│ └───────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Docker Container (Go) │ │
│ │ Container Name: go-gin-tutorial │ │
│ │ Go Version: 1.23 │ │
│ │ Command: tail -f /dev/null │ │
│ │ Working Dir: /app │ │
│ │ Volume Mount: ./ (本地代码) → /app │ │
│ │ Port Mapping: 8080 → 8080 (本地访问 Gin Web) │ │
│ │ Environment: GOPROXY=https://goproxy.io,direct │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ 编辑流程:VS Code 编辑器 → Dev Container → Docker 容器运行 → 本地端口访问 Web │
└───────────────────────────────────────────────────────────────────────────────┘

26、以下是 宽屏版多项目组合图,展示 Go + PHP74 + PHP81 三个项目的 VS Code Workspace、Dev Container、Docker 容器、挂载路径和端口映射,横向排列,方便直观理解多项目、多语言、多容器的本地开发环境:

┌───────────────────────────────────────────────────────────────────────────────────────────────┐
│ VS Code Windows │
│ (各自 Workspace 独立窗口) │
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ Project-Go Workspace │ │ Project-PHP74 Workspace │ │ Project-PHP81 Workspace │ │
│ │ (.code-workspace) │ │ (.code-workspace) │ │ (.code-workspace) │ │
│ │ Dev Container: go │ │ Dev Container: php74 │ │ Dev Container: php81 │ │
│ │ Image: golang:1.23-alpine │ │ Image: php:7.4-fpm │ │ Image: php:8.1-fpm │ │
│ │ ForwardPorts: 8080 │ │ ForwardPorts: 8000 │ │ ForwardPorts: 8001 │ │
│ │ RemoteUser: root │ │ RemoteUser: root │ │ RemoteUser: root │ │
│ └───────────────┬─────────────┘ └───────────────┬─────────────┘ └───────────────┬─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ Docker Container Go │ │ Docker Container PHP 7.4 │ │ Docker Container PHP 8.1 │ │
│ │ Name: go-gin-tutorial │ │ Name: php74-dev │ │ Name: php81-dev │ │
│ │ Version: 1.23 │ │ Version: 7.4 FPM │ │ Version: 8.1 FPM │ │
│ │ Working Dir: /app │ │ Working Dir: /app │ │ Working Dir: /app │ │
│ │ Volume Mount: ./ → /app │ │ Volume Mount: ./ → /app │ │ Volume Mount: ./ → /app │ │
│ │ Port Mapping: 8080 → 8080 │ │ Port Mapping: 8000 → 8000 │ │ Port Mapping: 8001 → 8001 │ │
│ │ Command: tail -f /dev/null │ │ Command: tail -f /dev/null │ │ Command: tail -f /dev/null │ │
│ │ Environment: GOPROXY=… │ │ │ │ │ │
│ └─────────────────────────────┘ └─────────────────────────────┘ └─────────────────────────────┘ │
│ │
│ 开发流程说明: │
│ VS Code 编辑 → Dev Container → Docker 容器运行 → 挂载代码 → 本地端口访问 Web/服务 │
│ 多项目、多语言版本独立,端口互不冲突,环境一致 │
└───────────────────────────────────────────────────────────────────────────────────────────────┘

图示说明

  1. VS Code Windows / Workspace
    每个项目对应一个 VS Code 窗口
    .code-workspace 配置独立
    插件、调试、终端完全隔离
  2. Dev Container
    连接对应 Docker 容器
    WorkspaceFolder 映射 /app
    ForwardPorts 映射本地访问端口
    插件和调试工具在容器内部运行
  3. Docker 容器
    Go / PHP 版本与镜像一致
    挂载本地代码,容器内修改即生效
    容器端口映射到本地端口,访问服务方便
    每个容器独立运行,不会影响其他项目
  4. 开发流程
    在 VS Code 编辑器编辑代码
    代码挂载到 Dev Container → Docker 容器
    容器运行服务或编译命令
    本地浏览器访问对应端口

优势

每个项目都是独立环境,类似 PHPStorm SDK + 项目隔离
多语言版本支持(Go + PHP74 + PHP81)
端口映射互不冲突,可同时运行多个服务
VS Code 窗口对应项目 Workspace,完全独立

27、我现在要如何将 APP [开发容器: GO GIN DEV CONTAINER] 转换为一个工作区呢?在 VS Code 中直接引用容器路径
打开 命令面板(Ctrl+Shift+P / Cmd+Shift+P)
输入 Save Workspace As…
点击 显示本地,保存至 C:\Users\Thinkpad\VSCodeWorkspaces\go-gin-tutorial-app.code-workspace。如图10

我现在要如何将 APP [开发容器: GO GIN DEV CONTAINER] 转换为一个工作区呢?

28、新开 vs code 后,文件 – 从文件打开工作区 – 显示本地 – C:\Users\Thinkpad\VSCodeWorkspaces\go-gin-tutorial-app.code-workspace。

29、现在在 vs code 中,问题:could not import github.com/gin-gonic/gin (no required module provides package “github.com/gin-gonic/gin”)。这是 Go Modules 依赖没安装或者未初始化 的典型错误。如图11

现在在 vs code 中,问题:could not import github.com/gin-gonic/gin (no required module provides package "github.com/gin-gonic/gin")

30、运行代码 开始将 Gin 模块作为依赖项进行跟踪。在命令行中,使用该go get 命令将 github.com/gin-gonic/gin 模块添加为模块的依赖项。使用点号参数表示“获取当前目录中代码的依赖项”。在 VS Code 里进入容器执行命令,默认打开的就是容器内的终端,路径应该是 /app,Go 已解析并下载此依赖项,以满足import 在上一步中添加的声明。如图12

在命令行中,使用该go get 命令将 github.com/gin-gonic/gin 模块添加为您的模块的依赖项。


/app # pwd
/app
/app # go get .
go: github.com/gin-gonic/gin@v1.12.0 requires go >= 1.25.0 (running go 1.23.12; GOTOOLCHAIN=local)
/app # 

31、在包含 main.go 的目录中,从命令行运行代码。使用点号参数表示“在当前目录中运行代码”。报错:


/app # go run .
main.go:6:2: no required module provides package github.com/gin-gonic/gin; to add it:
        go get github.com/gin-gonic/gin
/app # 

32、找到了根源,当前容器里的 Go 版本是 1.23.12,Gin v1.12.0 需要 Go ≥ 1.25.0,所以即使执行了 go get . 或 go get github.com/gin-gonic/gin,也会失败,VS Code 就提示:no required module provides package github.com/gin-gonic/gin。

33、编辑 docker-compose.yml,升级 Go 镜像版本,golang:1.23-alpine 修改为 golang:1.26-alpine



version: '3.8'

services:
  go:
    image: golang:1.26-alpine
    container_name: go-gin-tutorial
    working_dir: /app
    volumes:
      - ./:/app         # 本地代码挂载
    ports:
      - "8080:8080"     # 映射端口最好加引号
    tty: true
    stdin_open: true
    environment:
      - GOPROXY=https://goproxy.io,direct
    command: tail -f /dev/null   # 容器启动后保持运行状态


34、容器重建流程(VS Code 内完成)
修改 docker-compose.yml 升级 Go 镜像版本
命令面板 Ctrl+Shift+P → 输入:
Dev Containers: Rebuild and Reopen in Container
VS Code 会销毁旧容器并基于新镜像创建新容器
打开终端执行第 4 步命令,Gin 环境就绪

✅ 工作区 .code-workspace 和本地代码不会丢失
✅ 容器里的 Go 版本和 Gin 依赖都是最新

35、再次执行 go get . 命令,未提示版本问题,正确执行。


/app # go get .
go: downloading github.com/gin-gonic/gin v1.12.0
go: downloading github.com/gin-contrib/sse v1.1.0
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading golang.org/x/net v0.51.0
go: downloading github.com/quic-go/quic-go v0.59.0
go: downloading github.com/go-playground/validator/v10 v10.30.1
go: downloading github.com/goccy/go-yaml v1.19.2
go: downloading github.com/pelletier/go-toml/v2 v2.2.4
go: downloading github.com/bytedance/sonic v1.15.0
go: downloading github.com/ugorji/go/codec v1.3.1
go: downloading github.com/goccy/go-json v0.10.5
go: downloading go.mongodb.org/mongo-driver/v2 v2.5.0
go: downloading github.com/json-iterator/go v1.1.12
go: downloading google.golang.org/protobuf v1.36.10
go: downloading golang.org/x/sys v0.41.0
go: downloading github.com/quic-go/qpack v0.6.0
go: downloading github.com/gabriel-vasile/mimetype v1.4.12
go: downloading github.com/go-playground/universal-translator v0.18.1
go: downloading github.com/leodido/go-urn v1.4.0
go: downloading golang.org/x/crypto v0.48.0
go: downloading golang.org/x/text v0.34.0
go: downloading github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd
go: downloading github.com/modern-go/reflect2 v1.0.2
go: downloading github.com/go-playground/locales v0.14.1
go: downloading github.com/bytedance/gopkg v0.1.3
go: downloading github.com/cloudwego/base64x v0.1.6
go: downloading golang.org/x/arch v0.22.0
go: downloading github.com/klauspost/cpuid/v2 v2.3.0
go: downloading github.com/bytedance/sonic/loader v0.5.0
go: downloading github.com/twitchyliquid64/golang-asm v0.15.1
go: upgraded go 1.23.12 => 1.25.0
go: added github.com/bytedance/gopkg v0.1.3
go: added github.com/bytedance/sonic v1.15.0
go: added github.com/bytedance/sonic/loader v0.5.0
go: added github.com/cloudwego/base64x v0.1.6
go: added github.com/gabriel-vasile/mimetype v1.4.12
go: added github.com/gin-contrib/sse v1.1.0
go: added github.com/gin-gonic/gin v1.12.0
go: added github.com/go-playground/locales v0.14.1
go: added github.com/go-playground/universal-translator v0.18.1
go: added github.com/go-playground/validator/v10 v10.30.1
go: added github.com/goccy/go-json v0.10.5
go: added github.com/goccy/go-yaml v1.19.2
go: added github.com/json-iterator/go v1.1.12
go: added github.com/klauspost/cpuid/v2 v2.3.0
go: added github.com/leodido/go-urn v1.4.0
go: added github.com/mattn/go-isatty v0.0.20
go: added github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd
go: added github.com/modern-go/reflect2 v1.0.2
go: added github.com/pelletier/go-toml/v2 v2.2.4
go: added github.com/quic-go/qpack v0.6.0
go: added github.com/quic-go/quic-go v0.59.0
go: added github.com/twitchyliquid64/golang-asm v0.15.1
go: added github.com/ugorji/go/codec v1.3.1
go: added go.mongodb.org/mongo-driver/v2 v2.5.0
go: added golang.org/x/arch v0.22.0
go: added golang.org/x/crypto v0.48.0
go: added golang.org/x/net v0.51.0
go: added golang.org/x/sys v0.41.0
go: added golang.org/x/text v0.34.0
go: added google.golang.org/protobuf v1.36.10
/app # 

36、在包含 main.go 的目录中,从命令行运行代码。使用点号参数表示“在当前目录中运行代码”。



/app # go run .




代码运行后,就拥有了一个可以向其发送请求的 HTTP 服务器。

37、Windows 浏览器访问:http://localhost:8080/albums ,响应状态:(失败)net::ERR_EMPTY_RESPONSE。修改 main.go,router.Run(“0.0.0.0:8080”) // 监听所有网卡。显示预先提供给服务的数据。如图13

Windows 浏览器访问:http://localhost:8080/albums


[
    {
        "id": "1",
        "title": "Blue Train",
        "artist": "John Coltrane",
        "price": 56.99
    },
    {
        "id": "2",
        "title": "Jeru",
        "artist": "Gerry Mulligan",
        "price": 17.99
    },
    {
        "id": "3",
        "title": "Sarah Vaughan and Clifford Brown",
        "artist": "Sarah Vaughan",
        "price": 39.99
    }
]

68、生成 GitHub 的 Personal Access Token(个人访问令牌)。
我30 秒手把手教生成 Token,一次搞定,以后再也不用输密码!
一步一步生成 GitHub Token
打开 GitHub 右上角头像 → Settings
左侧最下面 → Developer settings
点击 Personal access tokens → Tokens (classic)
点击右上角 Generate new token → Generate new token (classic)
填写:
Note:填 go-gin-learning(随便写,方便自己记)
Expiration:选 90 days 或者 No expiration
Select scopes:勾选 repo(全勾第一项就行)
拉到最下面 → Generate token
复制生成的一串以 ghp_ 开头的代码(只显示一次,关掉就没了!)如图14

生成 GitHub 的 Personal Access Token(个人访问令牌)

69、决定将代码提交至 GitHub,创建仓库:https://github.com/shuijingwan/go-gin-learning 。在容器里的 /app 目录(vs code 下的终端),直接执行下面 全部命令:

<pre class="wp-block-syntaxhighlighter-code">

/app # git init
/bin/sh: git: not found
/app # apk add git
( 1/13) Installing brotli-libs (1.2.0-r0)
( 2/13) Installing c-ares (1.34.6-r0)
( 3/13) Installing libunistring (1.4.1-r0)
( 4/13) Installing libidn2 (2.3.8-r0)
( 5/13) Installing nghttp2-libs (1.68.0-r0)
( 6/13) Installing nghttp3 (1.13.1-r0)
( 7/13) Installing libpsl (0.21.5-r3)
( 8/13) Installing zstd-libs (1.5.7-r2)
( 9/13) Installing libcurl (8.17.0-r1)
(10/13) Installing libexpat (2.7.5-r0)
(11/13) Installing pcre2 (10.47-r0)
(12/13) Installing git (2.52.0-r0)
(13/13) Installing git-init-template (2.52.0-r0)
Executing busybox-1.37.0-r30.trigger
OK: 23.4 MiB in 32 packages
/app # git init
hint: Using 'master' as the name for the initial branch. This default branch name
hint: will change to "main" in Git 3.0. To configure the initial branch name
hint: to use in all of your new repositories, which will suppress this warning,
hint: call:
hint:
hint:   git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint:   git branch -m <name>
hint:
hint: Disable this message with "git config set advice.defaultBranchName false"
Initialized empty Git repository in /app/.git/
/app # echo "# go-gin-learning" >> README.md
/app # cat > .gitignore << 'EOF'
> /bin
> /dist
> /vendor/
> .env
> .env.test
> .idea/
> .vscode/
> .DS_Store
> EOF
/app # git add .
fatal: detected dubious ownership in repository at '/app'
To add an exception for this directory, call:

        git config --global --add safe.directory /app
/app # git config --global --add safe.directory /app
/app # git add .
/app # git commit -m "feat: 初始化Go Gin项目,集成容器开发环境"
Author identity unknown

*** Please tell me who you are.

Run

  git config --global user.email "you@example.com"
  git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.

fatal: unable to auto-detect email address (got 'root@e4dccfba01fb.(none)')
/app # git config --global user.name "shuijingwan"
/app # git config --global user.email "shuijingwanwq@163.com"
/app # git commit -m "feat: 初始化Go Gin项目,集成容器开发环境"
[master (root-commit) 023e682] feat: 初始化Go Gin项目,集成容器开发环境
 7 files changed, 188 insertions(+)
 create mode 100644 .devcontainer/devcontainer.json
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 docker-compose.yml
 create mode 100644 go.mod
 create mode 100644 go.sum
 create mode 100644 main.go
/app # git branch -M main
/app # git remote add origin https://github.com/shuijingwan/go-gin-learning.git
/app # git push -u origin main
Username for 'https://github.com': shuijingwan
Password for 'https://shuijingwan@github.com': 
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 4 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (10/10), 5.21 KiB | 313.00 KiB/s, done.
Total 10 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (1/1), done.
To https://github.com/shuijingwan/go-gin-learning.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.
/app # 
</pre>

70、查看 GitHub 上的仓库目录,发现 .devcontainer 被提交了。现在仓库目录结构如下

/go-gin-learning
├── .devcontainer/ ✅ 应该提交
│ └── devcontainer.json
├── .gitignore ✅ 应该提交
├── README.md ✅ 应该提交
├── docker-compose.yml ✅ 应该提交
├── go.mod
├── go.sum
└── main.go

]]>
https://www.shuijingwanwq.com/2026/04/03/9416/feed/ 0
在 Yii 2.0 中,基于桌面应用端的 RESTful APIs,在移动应用端的复用、覆盖微调的实现 (二) https://www.shuijingwanwq.com/2019/06/04/3302/ https://www.shuijingwanwq.com/2019/06/04/3302/#respond Tue, 04 Jun 2019 08:20:50 +0000 controller->id]]> http://www.shuijingwanwq.com/?p=3302 Post Views: 105

1、获取选题详情接口,选题的操作权限包含了当前用户的所有权限按钮,可针对移动端定制(当状态,2:待审 时,编辑选题的权限,0:无,更新选题的权限,0:无)

2、编辑方法文件:\api\rests\plan\ViewAction.php,获取控制器ID (Yii::$app->controller->id),当其路由为:/v1/plans/2 时,其值为:plan,如图1

获取控制器ID (Yii::$app->controller->id),当其路由为:/v1/plans/2 时,其值为:plan

图1

3、编辑方法文件:\api\rests\plan\ViewAction.php,获取控制器ID (Yii::$app->controller->id),当其路由为:/v1/mobile/plans/2 时,其值为:mobile/plan,如图2

获取控制器ID (Yii::$app->controller->id),当其路由为:/v1/mobile/plans/2 时,其值为:mobile/plan

图2

4、针对移动端定制(当状态,2:待审 时,编辑选题的权限,0:无,更新选题的权限,0:无),可通过判断 控制器ID (Yii::$app->controller->id),分别进行对应的处理,编辑方法文件:\api\rests\plan\ViewAction.php

5、当其路由为:/v1/plans/2 时,$actions[‘edit’] 与 $actions[‘update’] 皆为:1,符合预期,如图3

当其路由为:/v1/plans/2 时,$actions['edit'] 与 $actions['update'] 皆为:1,符合预期

图3

6、当其路由为:/v1/mobile/plans/2 时,$actions[‘edit’] 与 $actions[‘update’] 皆为:0,符合预期,如图4

当其路由为:/v1/mobile/plans/2 时,$actions['edit'] 与 $actions['update'] 皆为:0,符合预期

图4

]]>
https://www.shuijingwanwq.com/2019/06/04/3302/feed/ 0
基于 Yii 2.0 的 RESTful 风格的 API,选题任务的素材实现的代码审查 https://www.shuijingwanwq.com/2019/06/03/3297/ https://www.shuijingwanwq.com/2019/06/03/3297/#respond Mon, 03 Jun 2019 09:50:15 +0000 http://www.shuijingwanwq.com/?p=3297 Post Views: 96 1、要求新来的同事实现时提出的需求如下: (1)已登录,桌面端 \api\rests\resource 修改为: \api\rests\plan_task_resource (2)游客,不做改动 \api\rests\client_resource (3)已登录,移动端 任务管理 – 删除任务素材 任务管理 – 管理后台素材列表 \api\rests\mobile\plan_task_resource (4)已登录,移动端 /v1/mobile/resources/resource 素材上传 \api\rests\mobile\plan_task_resource 2、代码审查,\api\config\urlManager.php,配置:’controller’ => [‘v1/client-resource’], 存在 2 处,规则配置冗余,删除(客户端 — 任务管理 – 素材)
<pre class="wp-block-syntaxhighlighter-code">

        // 客户端 - 素材
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => ['v1/client-resource'],
            'tokens' => ['{id}' => '<id:\\w[\\w,:;]*>'],
            'extraPatterns' => [
                'POST resource' => 'create',
                'POST make-up' => 'makeup',
                'POST del-or-recovery' => 'del-or-recovery',
            ],
        ],

</pre>
<pre class="wp-block-syntaxhighlighter-code">

        // 客户端 -- 任务管理 - 素材
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => ['v1/client-resource'],
            'only' => ['index', 'create', 'update', 'edit', 'list', 'delete'],
            'tokens' => ['{id}' => '<id:\\w[\\w,:;]*>'],
        ],

</pre>
3、\api\config\urlManager.php,配置:’controller’ => [‘v1/client-resource’], 规则调整如下(规范:模式名称与操作名称应保持一致、配置 only 选项来明确列出支持哪些行为): 调整为:
<pre class="wp-block-syntaxhighlighter-code">

        // 客户端 - 素材
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => ['v1/client-resource'],
			'only' => ['resource', 'make-up', 'del-or-recovery'],
            'tokens' => ['{id}' => '<id:\\w[\\w,:;]*>'],
            'extraPatterns' => [
                'POST resource' => 'resource',
                'POST make-up' => 'make-up',
                'POST del-or-recovery' => 'del-or-recovery',
            ],
        ],

</pre>
4、控制器:\api\controllers\ClientResourceController.php,actions() 方法需要调整,yii\rest\ActiveController 默认提供一些动作,如果不需要的话,需要销毁。通过 RESTful APIs 显示数据时,经常需要检查当前用户是否有权限访问和操作所请求的资源, 在 yii\rest\ActiveController 中, 可覆盖 checkAccess() 方法来完成权限检查。=> 的前后,仅需要一个空格,无需对齐。


    /**
     * @inheritdoc
     */
    public function actions()
    {
        $actions = parent::actions();
        $actions['create'] = [
            'class'      => 'api\rests\client_resource\ResourceAction',
            'modelClass' => $this->modelClass,
        ];
        $actions['makeup'] = [
            'class'      => 'api\rests\client_resource\ResourceUpdateAction',
            'modelClass' => $this->modelClass,
        ];
        $actions['del-or-recovery'] = [
            'class'      => 'api\rests\client_resource\DelOrRecoveryAction',
            'modelClass' => $this->modelClass,
        ];
        return $actions;
    }


调整为:


    /**
     * @inheritdoc
     */
    public function actions()
    {
        $actions = parent::actions();
        // 禁用"index"、"view"、"create"、"update"、"delete"、"options"动作
        unset($actions['index'], $actions['view'], $actions['create'], $actions['update'], $actions['delete'], $actions['options']);
        $actions['resource'] = [
            'class' => 'api\rests\client_resource\ResourceAction',
            'modelClass' => $this->modelClass,
			'checkAccess' => [$this, 'checkAccess'],
        ];
        $actions['make-up'] = [
            'class' => 'api\rests\client_resource\ResourceUpdateAction',
            'modelClass' => $this->modelClass,
			'checkAccess' => [$this, 'checkAccess'],
        ];
        $actions['del-or-recovery'] = [
            'class' => 'api\rests\client_resource\DelOrRecoveryAction',
            'modelClass' => $this->modelClass,
			'checkAccess' => [$this, 'checkAccess'],
        ];
        return $actions;
    }


5、模块 v1 中的控制器:\api\modules\v1\controllers\ClientResourceController.php,代码注释与控制器本身未保持一致
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace api\modules\v1\controllers;

/**
 * Menu controller for the `v1` module
 */
class ClientResourceController extends \api\controllers\ClientResourceController
{
    public $modelClass = 'api\modules\v1\models\Resource';
}


</pre>
调整为:
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace api\modules\v1\controllers;

/**
 * ClientResource controller for the `v1` module
 */
class ClientResourceController extends \api\controllers\ClientResourceController
{
    public $modelClass = 'api\modules\v1\models\Resource';
}


</pre>
6、目录:\api\rests\client_resource 中的方法文件,需要重命名: \api\rests\client_resource\ResourceUpdateAction.php 重命名为:\api\rests\client_resource\MakeUpAction.php 7、已登录,桌面端,\api\config\urlManager.php,配置:’controller’ => [‘v1/plan-task-resource’], 数组的 , 后需要一个空格
<pre class="wp-block-syntaxhighlighter-code">

        // 任务管理 - 素材
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => ['v1/plan-task-resource'],
            'only' => ['index','delete','create'],
            'tokens' => ['{id}' => '<id:\\w[\\w,:;]*>'],
            'extraPatterns' => [
                'POST resource' => 'create',
            ],
        ],

</pre>
调整为:
<pre class="wp-block-syntaxhighlighter-code">

        // 任务管理 - 素材
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => ['v1/plan-task-resource'],
            'only' => ['index', 'delete', 'resource'],
            'tokens' => ['{id}' => '<id:\\w[\\w,:;]*>'],
            'extraPatterns' => [
                'POST resource' => 'resource',
            ],
        ],

</pre>
8、控制器:\api\controllers\PlanTaskResourceController.php,actions() 方法需要调整,yii\rest\ActiveController 默认提供一些动作,如果不需要的话,需要销毁。通过 RESTful APIs 显示数据时,经常需要检查当前用户是否有权限访问和操作所请求的资源, 在 yii\rest\ActiveController 中, 可覆盖 checkAccess() 方法来完成权限检查。=> 的前后,仅需要一个空格,无需对齐。


    /**
     * @inheritdoc
     */
    public function actions()
    {
        $actions = parent::actions();
        $actions['delete']['class'] = 'api\rests\plan_task_resource\DeleteAction';
        $actions['index']['class'] = 'api\rests\plan_task_resource\IndexAction';
        $actions['create'] = [
            'class'      => 'api\rests\plan_task_resource\ResourceAction',
            'modelClass' => $this->modelClass,
        ];
        return $actions;
    }


调整为:


    /**
     * @inheritdoc
     */
    public function actions()
    {
        $actions = parent::actions();
        // 禁用"view"、"create"、"update"、"options"动作
        unset($actions['view'], $actions['create'], $actions['update'], $actions['options']);
        $actions['delete']['class'] = 'api\rests\plan_task_resource\DeleteAction';
        $actions['index']['class'] = 'api\rests\plan_task_resource\IndexAction';
        $actions['resource'] = [
            'class' => 'api\rests\plan_task_resource\ResourceAction',
            'modelClass' => $this->modelClass,
			'checkAccess' => [$this, 'checkAccess'],
        ];
        return $actions;
    }


9、模块 v1 中的控制器:\api\modules\v1\controllers\ResourceController.php,类名与文件名未保持一致,代码注释与控制器本身未保持一致。IDE 提示:非控制器类文件,如图1 \api\modules\v1\controllers\ResourceController.php 重命名为:\api\modules\v1\controllers\PlanTaskResourceController.php
IDE 提示:非控制器类文件

图1

<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace api\modules\v1\controllers;

/**
 * Menu controller for the `v1` module
 */
class PlanTaskResourceController extends \api\controllers\PlanTaskResourceController
{
    public $modelClass = 'api\modules\v1\models\Resource';
}


</pre>
调整为:
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace api\modules\v1\controllers;

/**
 * PlanTaskResource controller for the `v1` module
 */
class PlanTaskResourceController extends \api\controllers\PlanTaskResourceController
{
    public $modelClass = 'api\modules\v1\models\Resource';
}


</pre>
10、已登录,移动端,\api\config\urlManager.php,配置:’controller’ => [‘v1/mobile/plan-task-resource’], 数组的 , 后需要一个空格,代码注释未与菜单结构保持一致
<pre class="wp-block-syntaxhighlighter-code">

        // 移动端 - 任务素材
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => ['v1/mobile/plan-task-resource'],
            'only' => ['index','delete','create'],
            'tokens' => ['{id}' => '<id:\\w[\\w,:;]*>'],
            'extraPatterns' => [
                'POST resource' => 'create',
            ],
        ],

</pre>
调整为:
<pre class="wp-block-syntaxhighlighter-code">

        // 移动端 - 任务 - 素材
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => ['v1/mobile/plan-task-resource'],
            'only' => ['index', 'delete', 'resource'],
            'tokens' => ['{id}' => '<id:\\w[\\w,:;]*>'],
            'extraPatterns' => [
                'POST resource' => 'resource',
            ],
        ],

</pre>
11、控制器:\api\controllers\mobile\PlanTaskResourceController.php,actions() 方法需要调整。actions() 方法 与 公共属性 $serializer 之间,需要空出一行
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace api\controllers\mobile;

class PlanTaskResourceController extends \api\controllers\PlanTaskResourceController
{
    public $serializer = [
        'class' => 'api\rests\mobile\plan_task_resource\Serializer',
        'collectionEnvelope' => 'items',
    ];
    /**
     * @inheritdoc
     */
    public function actions()
    {
        $actions = parent::actions();
        $actions['delete']['class'] = 'api\rests\mobile\plan_task_resource\DeleteAction';
        $actions['index']['class'] = 'api\rests\mobile\plan_task_resource\IndexAction';
        $actions['create']['class'] = 'api\rests\mobile\plan_task_resource\ResourceAction';
        return $actions;
    }
}


</pre>
调整为:
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace api\controllers\mobile;

class PlanTaskResourceController extends \api\controllers\PlanTaskResourceController
{
    public $serializer = [
        'class' => 'api\rests\mobile\plan_task_resource\Serializer',
        'collectionEnvelope' => 'items',
    ];

    /**
     * @inheritdoc
     */
    public function actions()
    {
        $actions = parent::actions();
        $actions['delete']['class'] = 'api\rests\mobile\plan_task_resource\DeleteAction';
        $actions['index']['class'] = 'api\rests\mobile\plan_task_resource\IndexAction';
        $actions['resource']['class'] = 'api\rests\mobile\plan_task_resource\ResourceAction';
        return $actions;
    }
}


</pre>
12、控制器:\api\controllers\mobile\ResourceController.php,需要删除 13、模块 v1 中的控制器:\api\modules\v1\controllers\mobile\ResourceController.php,需要删除 14、目录:\api\rests\mobile\plan_task_resource 中的方法文件,需要调整 \api\rests\mobile\plan_task_resource\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 api\rests\mobile\plan_task_resource;


/**
 * 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 \api\rests\plan_task_resource\Action
{

}


</pre>
\api\rests\mobile\plan_task_resource\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 api\rests\mobile\plan_task_resource;

class Action extends \api\rests\plan_task_resource\Action
{

}


</pre>
\api\rests\mobile\plan_task_resource\DeleteAction.php、\api\rests\mobile\plan_task_resource\IndexAction.php、\api\rests\mobile\plan_task_resource\Serializer.php 等 3 个文件,皆需要删除对应的注释 \api\rests\mobile\plan_task_resource\ResourceAction.php
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace api\rests\mobile\plan_task_resource;

use Yii;
use yii\db\ActiveRecord;
use yii\rest\Action;
use yii\web\ServerErrorHttpException;
use api\models\Resource;
use api\models\PlanTask;

class ResourceAction extends \api\rests\plan_task_resource\ResourceAction
{

}


</pre>
\api\rests\mobile\plan_task_resource\ResourceAction.php,未实际使用的文件,其引用需要删除,如图2
未实际使用的文件,其引用需要删除

图2

调整为:
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace api\rests\mobile\plan_task_resource;

class ResourceAction extends \api\rests\plan_task_resource\ResourceAction
{

}


</pre>
15、路由的设计,从某个方面来说是存在问题的,没有充分利用 yii\rest\ActiveController 默认提供的一些动作,但是,由于接口需要向后兼容,路由已经不好调整,只好沿用下去。总结,建议同事些参考一下其他功能的实现,如果也存在上述类似的问题,皆需要逐一解决。]]>
https://www.shuijingwanwq.com/2019/06/03/3297/feed/ 0
在 Yii 2 高级项目模板中,实现 RESTFUL WEB 服务时,消息目录(messages)中配置的最佳实践 https://www.shuijingwanwq.com/2019/01/17/3104/ https://www.shuijingwanwq.com/2019/01/17/3104/#respond Thu, 17 Jan 2019 02:54:25 +0000 http://www.shuijingwanwq.com/?p=3104 Post Views: 70 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 开始,因此,保留原有数组并只想新的数组附加到后面,用 + 运算符,附加的话,在两个数组中存在相同的键名时,第一个数组中的同键名的元素将会被保留,第二个数组中的元素将会被忽略]]>
https://www.shuijingwanwq.com/2019/01/17/3104/feed/ 0
在 Yii 2 高级模板中,渠道发布接口(发布同一篇文章至企鹅号、微信公众帐号等渠道)的架构设计,基于队列、控制台命令推动工作流程 https://www.shuijingwanwq.com/2018/12/05/3044/ https://www.shuijingwanwq.com/2018/12/05/3044/#respond Wed, 05 Dec 2018 09:58:48 +0000 http://www.shuijingwanwq.com/?p=3044 Post Views: 120 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



]]>
https://www.shuijingwanwq.com/2018/12/05/3044/feed/ 0
基于 Yii 2.0,实现 RESTful 风格的 Web Service 服务的 API,请求参数中,仅某一参数支持多个参数值(即数组、列表输入)的数据填充、验证的实现 https://www.shuijingwanwq.com/2018/09/04/2893/ https://www.shuijingwanwq.com/2018/09/04/2893/#respond Tue, 04 Sep 2018 09:08:50 +0000 http://www.shuijingwanwq.com/?p=2893 Post Views: 68 1、POST http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,如图1
POST http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63

图1



{
	"uuid": "e88e79faad9011e8a14554ee75d2ebc1",
	"article_type_code": "standard",
	"article_category_id": 1,
	"title": "标题 - 20180904 - 1",
	"author": "作者 - 20180904 - 1",
	"source": "spider",
	"source_user_id": 1,
	"source_article_id": 1,
	"content": "文章内容 - 20180904 - 1",
	"cover_pic": "/upload/Image/mrtp/2018/08/30/1_8a2fa998c0624c0ebee6eb72c5434e7a.gif",
	"cover_type": 1,
	"tag": "",
	"apply": 0,
	"original_platform": 0,
	"original_url": "",
	"original_author": ""
}


打印请求参数:


Array
(
    [uuid] => e88e79faad9011e8a14554ee75d2ebc1
    [article_type_code] => standard
    [article_category_id] => 1
    [title] => 标题 - 20180904 - 1
    [author] => 作者 - 20180904 - 1
    [source] => spider
    [source_user_id] => 1
    [source_article_id] => 1
    [content] => 文章内容 - 20180904 - 1
    [cover_pic] => /upload/Image/mrtp/2018/08/30/1_8a2fa998c0624c0ebee6eb72c5434e7a.gif
    [cover_type] => 1
    [tag] => 
    [apply] => 0
    [original_platform] => 0
    [original_url] => 
    [original_author] => 
    [group_id] => 015ce30b116ce86058fa6ab4fea4ac63
)



2、POST http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,请求参数 uuid 为数组格式,如图2
POST http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,请求参数 uuid 为数组格式

图2



{
	"uuid": [
		"e88e79faad9011e8a14554ee75d2ebc1",
		"9f359272b00e11e8875654ee75d2ebc1"
	],
	"article_type_code": "standard",
	"article_category_id": 1,
	"title": "标题 - 20180904 - 1",
	"author": "作者 - 20180904 - 1",
	"source": "spider",
	"source_user_id": 1,
	"source_article_id": 1,
	"content": "文章内容 - 20180904 - 1",
	"cover_pic": "/upload/Image/mrtp/2018/08/30/1_8a2fa998c0624c0ebee6eb72c5434e7a.gif",
	"cover_type": 1,
	"tag": "",
	"apply": 0,
	"original_platform": 0,
	"original_url": "",
	"original_author": ""
}


打印请求参数:


Array
(
    [uuid] => Array
        (
            [0] => e88e79faad9011e8a14554ee75d2ebc1
            [1] => 9f359272b00e11e8875654ee75d2ebc1
        )

    [article_type_code] => standard
    [article_category_id] => 1
    [title] => 标题 - 20180904 - 1
    [author] => 作者 - 20180904 - 1
    [source] => spider
    [source_user_id] => 1
    [source_article_id] => 1
    [content] => 文章内容 - 20180904 - 1
    [cover_pic] => /upload/Image/mrtp/2018/08/30/1_8a2fa998c0624c0ebee6eb72c5434e7a.gif
    [cover_type] => 1
    [tag] => 
    [apply] => 0
    [original_platform] => 0
    [original_url] => 
    [original_author] => 
    [group_id] => 015ce30b116ce86058fa6ab4fea4ac63
)




3、查看 \qq\rests\article\CreateAction.php,当 uuid 的值为单个字符串时,其填充与验证代码



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

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

        /* 标准(普通、图文)的文章发布参数 */
        $qqArticleStandardCreateParam = new QqArticleStandardCreateParam();
        // 把请求数据填充到模型中
        if (!$qqArticleStandardCreateParam->load($requestParams, '')) {
            return ['code' => 40009, 'message' => Yii::t('error', '40009')];
        }
        // 验证模型
        if (!$qqArticleStandardCreateParam->validate()) {
            $qqArticleStandardCreateParamResult = self::handleValidateError($qqArticleStandardCreateParam);
            if ($qqArticleStandardCreateParamResult['status'] === false) {
                return ['code' => $qqArticleStandardCreateParamResult['code'], 'message' => $qqArticleStandardCreateParamResult['message']];
            }
        }

        /* 基于文章类型代码定义场景 */
        $this->scenario = 'qq_article_' . $qqArticleStandardCreateParam->article_type_code . '_create';

        /* 实例化多个模型 */

        // 企鹅号的第三方服务平台应用的企鹅媒体用户
        $qqTpAppPenguin = new QqTpAppPenguin([
            'scenario' => $this->scenario,
        ]);
        // 转换标准(普通、图文)的文章发布参数,多模型的填充、验证的实现
        $requestParams[$qqTpAppPenguin->formName()]['uuid'] = $qqArticleStandardCreateParam->uuid;
        $qqTpAppPenguinResult = self::handleLoadAndValidate($qqTpAppPenguin, $requestParams);
        if ($qqTpAppPenguinResult['status'] === false) {
            return ['code' => $qqTpAppPenguinResult['code'], 'message' => $qqTpAppPenguinResult['message']];
        }

    }

    /**
     * 处理模型填充与验证
     * @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.');
        }
    }



4、参考网址:https://www.yiiframework.com/doc/guide/2.0/zh-cn/input-tabular-input ,编辑 \qq\rests\article\CreateAction.php,当 uuid 的值为数组时,其填充与验证代码
<pre class="wp-block-syntaxhighlighter-code">


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

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

        /* 标准(普通、图文)的文章发布参数 */
        $qqArticleStandardCreateParam = new QqArticleStandardCreateParam();
        // 把请求数据填充到模型中
        if (!$qqArticleStandardCreateParam->load($requestParams, '')) {
            return ['code' => 40009, 'message' => Yii::t('error', '40009')];
        }
        // 验证模型
        if (!$qqArticleStandardCreateParam->validate()) {
            $qqArticleStandardCreateParamResult = self::handleValidateError($qqArticleStandardCreateParam);
            if ($qqArticleStandardCreateParamResult['status'] === false) {
                return ['code' => $qqArticleStandardCreateParamResult['code'], 'message' => $qqArticleStandardCreateParamResult['message']];
            }
        }

        /* 基于文章类型代码定义场景 */
        $this->scenario = 'qq_article_' . $qqArticleStandardCreateParam->article_type_code . '_create';

        /* 实例化多个模型 */

        // 企鹅号的第三方服务平台应用的企鹅媒体用户
        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']];
            }
        }

    }

    /**
     * 处理模型填充与验证
     * @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、POST http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,请求参数 uuid 为字符串格式,响应失败,符合预期


{
	"uuid": "e88e79faad9011e8a14554ee75d2ebc1",
	"article_type_code": "standard",
	"article_category_id": 1,
	"title": "标题 - 20180904 - 1",
	"author": "作者 - 20180904 - 1",
	"source": "spider",
	"source_user_id": 1,
	"source_article_id": 1,
	"content": "文章内容 - 20180904 - 1",
	"cover_pic": "/upload/Image/mrtp/2018/08/30/1_8a2fa998c0624c0ebee6eb72c5434e7a.gif",
	"cover_type": 1,
	"tag": "",
	"apply": 0,
	"original_platform": 0,
	"original_url": "",
	"original_author": ""
}




{
    "code": 40009,
    "message": "数据模型填充失败"
}


6、POST http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,请求参数 uuid 为数组格式,只存在1个键值对,其值错误,响应失败,符合预期


{
	"uuid": [
		"e88e79faad9011e8a14554ee75d2ebc10"
	],
	"article_type_code": "standard",
	"article_category_id": 1,
	"title": "标题 - 20180904 - 1",
	"author": "作者 - 20180904 - 1",
	"source": "spider",
	"source_user_id": 1,
	"source_article_id": 1,
	"content": "文章内容 - 20180904 - 1",
	"cover_pic": "/upload/Image/mrtp/2018/08/30/1_8a2fa998c0624c0ebee6eb72c5434e7a.gif",
	"cover_type": 1,
	"tag": "",
	"apply": 0,
	"original_platform": 0,
	"original_url": "",
	"original_author": ""
}




{
    "code": 20004,
    "message": "数据验证失败:企鹅号ID(UUID)是无效的。"
}




SELECT EXISTS(SELECT * FROM `cpa_qq_tp_app_penguin` WHERE (`cpa_qq_tp_app_penguin`.`uuid`='e88e79faad9011e8a14554ee75d2ebc10') AND (`status`=1))


7、POST http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,请求参数 uuid 为数组格式,存在2个键值对,其值皆错误,响应失败,符合预期


{
	"uuid": [
		"e88e79faad9011e8a14554ee75d2ebc15",
		"9f359272b00e11e8875654ee75d2ebc16"
	],
	"article_type_code": "standard",
	"article_category_id": 1,
	"title": "标题 - 20180904 - 1",
	"author": "作者 - 20180904 - 1",
	"source": "spider",
	"source_user_id": 1,
	"source_article_id": 1,
	"content": "文章内容 - 20180904 - 1",
	"cover_pic": "/upload/Image/mrtp/2018/08/30/1_8a2fa998c0624c0ebee6eb72c5434e7a.gif",
	"cover_type": 1,
	"tag": "",
	"apply": 0,
	"original_platform": 0,
	"original_url": "",
	"original_author": ""
}




{
    "code": 20004,
    "message": "数据验证失败:企鹅号ID(UUID)是无效的。"
}




SELECT EXISTS(SELECT * FROM `cpa_qq_tp_app_penguin` WHERE (`cpa_qq_tp_app_penguin`.`uuid`='e88e79faad9011e8a14554ee75d2ebc15') AND (`status`=1))
SELECT EXISTS(SELECT * FROM `cpa_qq_tp_app_penguin` WHERE (`cpa_qq_tp_app_penguin`.`uuid`='9f359272b00e11e8875654ee75d2ebc16') AND (`status`=1))


8、POST http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,请求参数 uuid 为数组格式,存在2个键值对,第1个值正确,第2个值错误,响应失败,符合预期


{
	"uuid": [
		"e88e79faad9011e8a14554ee75d2ebc1",
		"9f359272b00e11e8875654ee75d2ebc16"
	],
	"article_type_code": "standard",
	"article_category_id": 1,
	"title": "标题 - 20180904 - 1",
	"author": "作者 - 20180904 - 1",
	"source": "spider",
	"source_user_id": 1,
	"source_article_id": 1,
	"content": "文章内容 - 20180904 - 1",
	"cover_pic": "/upload/Image/mrtp/2018/08/30/1_8a2fa998c0624c0ebee6eb72c5434e7a.gif",
	"cover_type": 1,
	"tag": "",
	"apply": 0,
	"original_platform": 0,
	"original_url": "",
	"original_author": ""
}




{
    "code": 20004,
    "message": "数据验证失败:企鹅号ID(UUID)是无效的。"
}




SELECT EXISTS(SELECT * FROM `cpa_qq_tp_app_penguin` WHERE (`cpa_qq_tp_app_penguin`.`uuid`='e88e79faad9011e8a14554ee75d2ebc1') AND (`status`=1))
SELECT EXISTS(SELECT * FROM `cpa_qq_tp_app_penguin` WHERE (`cpa_qq_tp_app_penguin`.`uuid`='9f359272b00e11e8875654ee75d2ebc16') AND (`status`=1))


9、POST http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,请求参数 uuid 为数组格式,存在2个键值对,第1个值错误,第2个值正确,响应失败,符合预期


{
	"uuid": [
		"e88e79faad9011e8a14554ee75d2ebc13",
		"9f359272b00e11e8875654ee75d2ebc1"
	],
	"article_type_code": "standard",
	"article_category_id": 1,
	"title": "标题 - 20180904 - 1",
	"author": "作者 - 20180904 - 1",
	"source": "spider",
	"source_user_id": 1,
	"source_article_id": 1,
	"content": "文章内容 - 20180904 - 1",
	"cover_pic": "/upload/Image/mrtp/2018/08/30/1_8a2fa998c0624c0ebee6eb72c5434e7a.gif",
	"cover_type": 1,
	"tag": "",
	"apply": 0,
	"original_platform": 0,
	"original_url": "",
	"original_author": ""
}




{
    "code": 20004,
    "message": "数据验证失败:企鹅号ID(UUID)是无效的。"
}




SELECT EXISTS(SELECT * FROM `cpa_qq_tp_app_penguin` WHERE (`cpa_qq_tp_app_penguin`.`uuid`='e88e79faad9011e8a14554ee75d2ebc13') AND (`status`=1))
SELECT EXISTS(SELECT * FROM `cpa_qq_tp_app_penguin` WHERE (`cpa_qq_tp_app_penguin`.`uuid`='9f359272b00e11e8875654ee75d2ebc1') AND (`status`=1))


10、POST http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,请求参数 uuid 为数组格式,存在2个键值对,其值皆正确,响应成功,符合预期


{
	"uuid": [
		"e88e79faad9011e8a14554ee75d2ebc1",
		"9f359272b00e11e8875654ee75d2ebc1"
	],
	"article_type_code": "standard",
	"article_category_id": 1,
	"title": "标题 - 20180904 - 1",
	"author": "作者 - 20180904 - 1",
	"source": "spider",
	"source_user_id": 1,
	"source_article_id": 1,
	"content": "文章内容 - 20180904 - 1",
	"cover_pic": "/upload/Image/mrtp/2018/08/30/1_8a2fa998c0624c0ebee6eb72c5434e7a.gif",
	"cover_type": 1,
	"tag": "",
	"apply": 0,
	"original_platform": 0,
	"original_url": "",
	"original_author": ""
}




{
    "code": 10000,
    "message": "发布文章类型:标准(普通、图文)的文章成功"
}




SELECT EXISTS(SELECT * FROM `cpa_qq_tp_app_penguin` WHERE (`cpa_qq_tp_app_penguin`.`uuid`='e88e79faad9011e8a14554ee75d2ebc1') AND (`status`=1))
SELECT EXISTS(SELECT * FROM `cpa_qq_tp_app_penguin` WHERE (`cpa_qq_tp_app_penguin`.`uuid`='9f359272b00e11e8875654ee75d2ebc1') AND (`status`=1))


11、可以确定的结论:数组中存在几个键值对,则会执行同样数量的验证过程,不会因为某个值验证失败,后续的值就不再验证,暂时告一段落,后续优化,以实现当某个值验证失败后,后续的值就不再验证,而是直接响应失败才是。]]>
https://www.shuijingwanwq.com/2018/09/04/2893/feed/ 0
基于 yiisoft/yii2-app-advanced,实现 RESTful 风格的 Web Service 服务的 API,请求参数为多模型时,数据填充、验证的实现 https://www.shuijingwanwq.com/2018/09/01/2889/ https://www.shuijingwanwq.com/2018/09/01/2889/#respond Sat, 01 Sep 2018 09:34:44 +0000 http://www.shuijingwanwq.com/?p=2889 Post Views: 94 1、参考网址:https://www.yiiframework.com/doc/guide/2.0/zh-cn/input-multiple-models ,其用于网页表单是合适的,不过 API 应用的请求参数一般并未添加表单名称(尤其是单个模型输入时),因此,不太合适,例:表名为 article_type,字段名为 code,那么请求参数名为 article_type_code,而不是:ArticleType[‘code’],网页表单如图1
参考网址:https://www.yiiframework.com/doc/guide/2.0/zh-cn/input-multiple-models ,其用于网页表单是合适的,不过 API 应用的请求参数一般并未添加表单名称(尤其是单个模型输入时),因此,不太合适,例:表名为 article_type,字段名为 code,那么请求参数名为 article_type_code,而不是:ArticleType['code'],网页表单

图1

2、现在模型数量为7个,分别为:qq_tp_app_penguin(企鹅号的第三方服务平台应用的企鹅媒体用户、QqTpAppPenguin)、article_type(文章类型、ArticleType)、article_category(文章分类、ArticleCategory)、qq_article_category_normal(企鹅号的文章类型(文章)的文章分类、QqArticleCategoryNormal)、article(文章、Article)、qq_article_normal(企鹅号的文章类型(文章)的文章、QqArticleNormal)、qq_tp_app_access_token(企鹅号的第三方服务平台应用的访问令牌(Redis)、RedisQqAuthQqTpAppAccessToken),示例中仅演示前2个模型的填充、验证的实现 3、发布文章类型:标准(普通、图文)的文章至渠道发布 /qq/v1/articles(article/create),请求参数并未严格遵循(表名_字段名)的规则,因为严格遵循的话,会导致字段名过长,且仍然可能存在不容易区分的问题,需要后续手动转换为对应模型的字段


1、请求参数列表
(1)uuid:必填,企鹅号ID(UUID)
(2)article_type_code:必填,文章类型代码,standard:标准(普通)
(3)article_category_id:必填,文章分类ID
(4)title:必填,标题
(5)author:可选,作者,默认:空字符串
(6)source:必填,来源,xContent:内容库;vms:视频管理系统;cms:内容管理系统;spider:自媒体
(7)source_user_id:必填,来源用户ID
(8)source_article_id:必填,来源文章ID
(9)content:必填,文章内容
(10)cover_pic:必填,文章封面图
(11)cover_type:可选,文章封面类型,1:单图;3:三图,默认:1
(12)tag:可选,文章标签,以英文半角逗号分隔,默认:空字符串
(13)apply:可选,是否申请原创文章,0:否;1:是(需要用户具有发表图文原创文章资格否则无效),默认:0
(14)original_platform:可选,原创首发平台,申请原创文章时必填,默认:0
(15)original_url:可选,原创首发链接,申请原创文章时当选择平台不是企鹅号时必填,默认:空字符串
(16)original_author:可选,原创首发作者,申请原创文章时当选择平台不是企鹅号时必填,默认:空字符串

2、输入数据验证规则
(1)必填:uuid、article_type_code、article_category_id、title、source、source_user_id、source_article_id、content、cover_pic
(2)默认值(''):author、tag、original_url、original_author
(3)默认值(1):cover_type
(4)默认值(0):apply、original_platform
(5)比对:article_type_code 其值必须等于 standard
(6)存在性:uuid 必须存在于企鹅号的第三方服务平台应用的企鹅媒体用户模型中,且其状态为 1:启用


4、新建模型文件:\common\logics\QqArticleStandardCreateParam.php,其作用为设置输入数据验证规则中的前4条与第6条、确保所有参数值为存在的,便于转换为对应模型字段时,无需再次判断其值是否存在
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace common\logics;

use Yii;
use yii\base\Model;

/**
 * QqArticleStandardCreateParam
 */
class QqArticleStandardCreateParam extends Model
{
    public $uuid;
    public $article_type_code;
    public $article_category_id;
    public $title;
    public $author;
    public $source;
    public $source_user_id;
    public $source_article_id;
    public $content;
    public $cover_pic;
    public $cover_type;
    public $tag;
    public $apply;
    public $original_platform;
    public $original_url;
    public $original_author;


    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return [
            [['uuid', 'article_type_code', 'article_category_id', 'title', 'source', 'source_user_id', 'source_article_id', 'content', 'cover_pic'], 'required'],
            [['author', 'tag', 'original_url', 'original_author'], 'default', 'value' => ''],
            [['cover_type'], 'default', 'value' => 1],
            [['apply', 'original_platform'], 'default', 'value' => 0],
            ['article_type_code', 'compare', 'compareValue' => ArticleType::CODE_STANDARD],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function attributeLabels()
    {
        return [
            'uuid' => Yii::t('model/qq-article-standard-create-param', 'Uuid'),
            'article_type_code' => Yii::t('model/qq-article-standard-create-param', 'Article Type Code'),
            'article_category_id' => Yii::t('model/qq-article-standard-create-param', 'Article Category Id'),
            'title' => Yii::t('model/qq-article-standard-create-param', 'Title'),
            'author' => Yii::t('model/qq-article-standard-create-param', 'Author'),
            'source' => Yii::t('model/qq-article-standard-create-param', 'Source'),
            'source_user_id' => Yii::t('model/qq-article-standard-create-param', 'Source User Id'),
            'source_article_id' => Yii::t('model/qq-article-standard-create-param', 'Source Article Id'),
            'content' => Yii::t('model/qq-article-standard-create-param', 'Content'),
            'cover_pic' => Yii::t('model/qq-article-standard-create-param', 'Cover Pic'),
            'cover_type' => Yii::t('model/qq-article-standard-create-param', 'Cover Type'),
            'tag' => Yii::t('model/qq-article-standard-create-param', 'Tag'),
            'apply' => Yii::t('model/qq-article-standard-create-param', 'Apply'),
            'original_platform' => Yii::t('model/qq-article-standard-create-param', 'Original Platform'),
            'original_url' => Yii::t('model/qq-article-standard-create-param', 'Original Url'),
            'original_author' => Yii::t('model/qq-article-standard-create-param', 'Original Author'),
        ];
    }
}


</pre>
5、对应的语言包文件 \common\messages\en-US\model\qq-article-standard-create-param.php
<pre class="wp-block-syntaxhighlighter-code">

<?php /** * Created by PhpStorm. * User: WangQiang * Date: 2018/08/31 * Time: 18:17 */ return [ 'Uuid' => 'Uuid',
    'Article Type Code' => 'Article Type Code',
    'Article Category Id' => 'Article Category Id',
    'Title' => 'Title',
    'Author' => 'Author',
    'Source' => 'Source',
    'Source User Id' => 'Source User Id',
    'Source Article Id' => 'Source Article Id',
    'Content' => 'Content',
    'Cover Pic' => 'Cover Pic',
    'Cover Type' => 'Cover Type',
    'Tag' => 'Tag',
    'Category' => 'Category',
    'Apply' => 'Apply',
    'Original Platform' => 'Original Platform',
    'Original Url' => 'Original Url',
    'Original Author' => 'Original Author',
];

</pre>
\common\messages\zh-CN\model\qq-article-standard-create-param.php
<pre class="wp-block-syntaxhighlighter-code">

<?php /** * Created by PhpStorm. * User: WangQiang * Date: 2018/08/31 * Time: 18:26 */ return [ 'Uuid' => '企鹅号ID(UUID)',
    'Article Type Code' => '文章类型代码',
    'Article Category Id' => '文章分类ID',
    'Title' => '标题',
    'Author' => '作者',
    'Source' => '来源',
    'Source User Id' => '来源用户ID',
    'Source Article Id' => '来源文章ID',
    'Content' => '文章内容',
    'Cover Pic' => '文章封面图',
    'Cover Type' => '文章封面类型,1:单图;3:三图',
    'Tag' => '文章标签,以英文半角逗号分隔',
    'Category' => '文章分类编号,即企鹅号的文章类型(文章)的文章分类ID',
    'Apply' => '是否申请原创文章,0:否;1:是(需要用户具有发表图文原创文章资格否则无效)',
    'Original Platform' => '原创首发平台,申请原创文章时必填',
    'Original Url' => '原创首发链接,申请原创文章时当选择平台不是企鹅号时必填',
    'Original Author' => '原创首发作者,申请原创文章时当选择平台不是企鹅号时必填',
];

</pre>
6、在应用的模型目录中,编辑 \qq\models\Article.php,定义场景
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace qq\models;

/**
 * This is the model class for table "{{%article}}".
 *
 * @see Article
 */
class Article extends \common\logics\Article
{
    const SCENARIO_STANDARD_CREATE = 'qq_article_standard_create';

    /**
     * {@inheritdoc}
     */
    public function scenarios()
    {
        $scenarios = parent::scenarios();

        return $scenarios;
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        $rules = [
        ];
        $parentRules = parent::rules();

        return ArrayHelper::merge($rules, $parentRules);
    }

    /**
     * {@inheritdoc}
     * @return ArticleQuery the active query used by this AR class.
     */
    public static function find()
    {
        return new ArticleQuery(get_called_class());
    }
}


</pre>
7、公共目录的模型数据层,qq_tp_app_penguin(企鹅号的第三方服务平台应用的企鹅媒体用户、QqTpAppPenguin)、\common\models\QqTpAppPenguin.php,代码如下
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace common\models;

use Yii;

/**
 * This is the model class for table "{{%qq_tp_app_penguin}}".
 *
 * @property int $id
 * @property string $uuid 企鹅号ID(UUID)
 * @property int $qq_tp_app_id 企鹅号的第三方服务平台应用ID
 * @property string $openid 授权第三方用户的企鹅媒体用户唯一标识
 * @property int $status 状态,-1:删除;0:禁用;1:启用
 * @property int $created_at 创建时间
 * @property int $updated_at 更新时间
 */
class QqTpAppPenguin extends \yii\db\ActiveRecord
{
    /**
     * {@inheritdoc}
     */
    public static function tableName()
    {
        return '{{%qq_tp_app_penguin}}';
    }

    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return [
            [['uuid', 'qq_tp_app_id', 'openid'], 'required'],
            [['qq_tp_app_id', 'status', 'created_at', 'updated_at'], 'integer'],
            [['uuid'], 'string', 'max' => 64],
            [['openid'], 'string', 'max' => 32],
            [['uuid'], 'unique'],
            [['openid'], 'unique'],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function attributeLabels()
    {
        return [
            'id' => Yii::t('model/qq-tp-app-penguin', 'ID'),
            'uuid' => Yii::t('model/qq-tp-app-penguin', 'Uuid'),
            'qq_tp_app_id' => Yii::t('model/qq-tp-app-penguin', 'Qq Tp App ID'),
            'openid' => Yii::t('model/qq-tp-app-penguin', 'Openid'),
            'status' => Yii::t('model/qq-tp-app-penguin', 'Status'),
            'created_at' => Yii::t('model/qq-tp-app-penguin', 'Created At'),
            'updated_at' => Yii::t('model/qq-tp-app-penguin', 'Updated At'),
        ];
    }

    /**
     * {@inheritdoc}
     * @return QqTpAppPenguinQuery the active query used by this AR class.
     */
    public static function find()
    {
        return new QqTpAppPenguinQuery(get_called_class());
    }
}


</pre>
8、公共目录的模型逻辑层,qq_tp_app_penguin(企鹅号的第三方服务平台应用的企鹅媒体用户、QqTpAppPenguin)、\common\logics\QqTpAppPenguin.php,代码如下
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace common\logics;

use Yii;
use yii\behaviors\TimestampBehavior;
use yii2tech\ar\softdelete\SoftDeleteBehavior;
use common\behaviors\UUIDBehavior;
use yii\helpers\ArrayHelper;

class QqTpAppPenguin extends \common\models\QqTpAppPenguin
{
    const STATUS_DELETED = -1; //状态:删除
    const STATUS_DISABLED = 0; //状态:禁用
    const STATUS_ENABLED = 1; //状态:启用

    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
            'timestampBehavior' => [
                'class' => TimestampBehavior::className(),
                'attributes' => [
                    self::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
                    self::EVENT_BEFORE_UPDATE => 'updated_at',
                    SoftDeleteBehavior::EVENT_BEFORE_SOFT_DELETE => 'updated_at',
                ]
            ],
            'uuid' => [
                'class' => UUIDBehavior::className(),
                'column' => 'uuid',
            ],
            'softDeleteBehavior' => [
                'class' => SoftDeleteBehavior::className(),
                'softDeleteAttributeValues' => [
                    'status' => self::STATUS_DELETED
                ],
            ],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function scenarios()
    {
        $scenarios = parent::scenarios();

        return $scenarios;
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        $rules = [
        ];
        $parentRules = parent::rules();

        return ArrayHelper::merge($rules, $parentRules);
    }

    /**
     * {@inheritdoc}
     * @return QqTpAppPenguinQuery the active query used by this AR class.
     */
    public static function find()
    {
        return new QqTpAppPenguinQuery(get_called_class());
    }

}


</pre>
9、在应用的模型目录中,qq_tp_app_penguin(企鹅号的第三方服务平台应用的企鹅媒体用户、QqTpAppPenguin)、\qq\models\QqTpAppPenguin.php,定义具体场景的验证规则,代码如下
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/08/28
 * Time: 15:10
 */

namespace qq\models;

use Yii;
use yii\helpers\ArrayHelper;
use yii\web\ServerErrorHttpException;

/**
 * This is the model class for table "{{%qq_tp_app_penguin}}".
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class QqTpAppPenguin extends \common\logics\QqTpAppPenguin
{
    /**
     * {@inheritdoc}
     */
    public function scenarios()
    {
        $scenarios = parent::scenarios();
        $scenarios[Article::SCENARIO_STANDARD_CREATE] = ['uuid'];

        return $scenarios;
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        $rules = [
            /* 发布文章类型:标准(普通、图文)的文章 */
            [['uuid'], 'required', 'on' => Article::SCENARIO_STANDARD_CREATE],
            ['uuid', 'exist', 'filter' => ['status' => self::STATUS_ENABLED], 'on' => Article::SCENARIO_STANDARD_CREATE],
            [['qq_tp_app_id', 'openid'], 'required'],
        ];
        $parentRules = parent::rules();
        unset($parentRules[0], $parentRules[4]);

        return ArrayHelper::merge($rules, $parentRules);
    }

    /**
     * {@inheritdoc}
     * @return QqTpAppPenguinQuery the active query used by this AR class.
     */
    public static function find()
    {
        return new QqTpAppPenguinQuery(get_called_class());
    }
}


</pre>
10、编辑方法文件 \qq\rests\article\CreateAction.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\base\Model;
use qq\models\QqArticleStandardCreateParam;
use qq\models\ArticleType;
use qq\models\Article;
use qq\models\QqTpAppPenguin;
use qq\services\QqArticleService;
use yii\helpers\Url;
use yii\helpers\ArrayHelper;
use yii\web\ServerErrorHttpException;

/**
 * 发布文章类型:标准(普通、图文)的文章至渠道发布
 *
 * 1、请求参数列表
 * (1)uuid:必填,企鹅号ID(UUID)
 * (2)article_type_code:必填,文章类型代码,standard:标准(普通)
 * (3)article_category_id:必填,文章分类ID
 * (4)title:必填,标题
 * (5)author:可选,作者,默认:空字符串
 * (6)source:必填,来源,xContent:内容库;vms:视频管理系统;cms:内容管理系统;spider:自媒体
 * (7)source_user_id:必填,来源用户ID
 * (8)source_article_id:必填,来源文章ID
 * (9)content:必填,文章内容
 * (10)cover_pic:必填,文章封面图
 * (11)cover_type:可选,文章封面类型,1:单图;3:三图,默认:1
 * (12)tag:可选,文章标签,以英文半角逗号分隔,默认:空字符串
 * (13)apply:可选,是否申请原创文章,0:否;1:是(需要用户具有发表图文原创文章资格否则无效),默认:0
 * (14)original_platform:可选,原创首发平台,申请原创文章时必填,默认:0
 * (15)original_url:可选,原创首发链接,申请原创文章时当选择平台不是企鹅号时必填,默认:空字符串
 * (16)original_author:可选,原创首发作者,申请原创文章时当选择平台不是企鹅号时必填,默认:空字符串
 *
 * 2、输入数据验证规则
 * (1)必填:uuid、article_type_code、article_category_id、title、source、source_user_id、source_article_id、content、cover_pic
 * (2)默认值(''):author、tag、original_url、original_author
 * (3)默认值(1):cover_type
 * (4)默认值(0):apply、original_platform
 * (5)比对:article_type_code 其值必须等于 standard
 * (6)存在性:uuid 必须存在于企鹅号的第三方服务平台应用的企鹅媒体用户模型中,且其状态为 1:启用
 *
 * For more details and usage information on CreateAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class CreateAction 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 \yii\db\ActiveRecordInterface the model newly created
     * @throws ServerErrorHttpException if there is any error when creating the model
     */
    public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }

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

        /* 标准(普通、图文)的文章发布参数 */
        $qqArticleStandardCreateParam = new QqArticleStandardCreateParam();
        // 把请求数据填充到模型中
        if (!$qqArticleStandardCreateParam->load($requestParams, '')) {
            return ['code' => 40009, 'message' => Yii::t('error', '40009')];
        }
        // 验证模型
        if (!$qqArticleStandardCreateParam->validate()) {
            $qqArticleStandardCreateParamResult = self::handleValidateError($qqArticleStandardCreateParam);
            if ($qqArticleStandardCreateParamResult['status'] === false) {
                return ['code' => $qqArticleStandardCreateParamResult['code'], 'message' => $qqArticleStandardCreateParamResult['message']];
            }
        }

        /* 基于文章类型代码定义场景 */
        $this->scenario = 'qq_article_' . $qqArticleStandardCreateParam->article_type_code . '_create';

        /* 实例化多个模型 */
        // 企鹅号的第三方服务平台应用的企鹅媒体用户
        $qqTpAppPenguin = new QqTpAppPenguin([
            'scenario' => $this->scenario,
        ]);
        // 转换标准(普通、图文)的文章发布参数,多模型的填充、验证的实现
        $requestParams[$qqTpAppPenguin->formName()]['uuid'] = $qqArticleStandardCreateParam->uuid;
        $qqTpAppPenguinResult = self::handleLoadAndValidate($qqTpAppPenguin, $requestParams);
        if ($qqTpAppPenguinResult['status'] === false) {
            return ['code' => $qqTpAppPenguinResult['code'], 'message' => $qqTpAppPenguinResult['message']];
        }

        return ['code' => 10000, 'message' => Yii::t('app', '10003'), 'data' => ''];
    }

    /**
     * 处理模型填充与验证
     * @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.');
        }
    }
}


</pre>
11、Post http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,响应:数据验证失败:企鹅号ID(UUID)不能为空。,符合预期


{
}




{
    "code": 20004,
    "message": "数据验证失败:企鹅号ID(UUID)不能为空。"
}


12、Post http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,缺少参数:original_author 时,打印:$qqArticleStandardCreateParam,original_author 的值为空字符串,符合预期


{
	"uuid": "e88e79faad9011e8a14554ee75d2ebc1",
	"article_type_code": "standard",
	"article_category_id": 1,
	"title": "标题 - 20180901 - 1",
	"author": "作者 - 20180901 - 1",
	"source": "spider",
	"source_user_id": 1,
	"source_article_id": 1,
	"content": "文章内容 - 20180901 - 1",
	"cover_pic": "http://b.hiphotos.baidu.com/image/pic/item/1e30e924b899a901a5be490c10950a7b0208f505.jpg",
	"cover_type": 1,
	"tag": "",
	"apply": 0,
	"original_platform": 0,
	"original_url": ""
}




qq\models\QqArticleStandardCreateParam Object
(
    [uuid] => e88e79faad9011e8a14554ee75d2ebc1
    [article_type_code] => standard
    [article_category_id] => 1
    [title] => 标题 - 20180901 - 1
    [author] => 作者 - 20180901 - 1
    [source] => spider
    [source_user_id] => 1
    [source_article_id] => 1
    [content] => 文章内容 - 20180901 - 1
    [cover_pic] => http://b.hiphotos.baidu.com/image/pic/item/1e30e924b899a901a5be490c10950a7b0208f505.jpg
    [cover_type] => 1
    [tag] => 
    [apply] => 0
    [original_platform] => 0
    [original_url] => 
    [original_author] => 
    [_errors:yii\base\Model:private] => Array
        (
        )
)


13、Post http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,参数:uuid 不存在于企鹅号的第三方服务平台应用的企鹅媒体用户模型中,响应:数据验证失败:企鹅号ID(UUID)是无效的。,符合预期,如图2
Post http://api.channel-pub-api.localhost/qq/v1/articles?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,参数:uuid 不存在于企鹅号的第三方服务平台应用的企鹅媒体用户模型中,响应:数据验证失败:企鹅号ID(UUID)是无效的。,符合预期

图2



{
	"uuid": "e88e79faad9011e8a14554ee75d2ebc10",
	"article_type_code": "standard",
	"article_category_id": 1,
	"title": "标题 - 20180901 - 1",
	"author": "作者 - 20180901 - 1",
	"source": "spider",
	"source_user_id": 1,
	"source_article_id": 1,
	"content": "文章内容 - 20180901 - 1",
	"cover_pic": "http://b.hiphotos.baidu.com/image/pic/item/1e30e924b899a901a5be490c10950a7b0208f505.jpg",
	"cover_type": 1,
	"tag": "",
	"apply": 0,
	"original_platform": 0,
	"original_url": "",
	"original_author": ""
}




{
    "code": 20004,
    "message": "数据验证失败:企鹅号ID(UUID)是无效的。"
}


14、查看日志,其验证过程中的 SQL 语句


	SELECT EXISTS(SELECT * FROM `cpa_qq_tp_app_penguin` WHERE (`cpa_qq_tp_app_penguin`.`uuid`='e88e79faad9011e8a14554ee75d2ebc10') AND (`status`=1))


15、后续其他模型的验证可以沿用 uuid 的验证方案,便可以实现请求参数为多模型时,数据的填充、验证,\qq\rests\article\CreateAction.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\base\Model;
use qq\models\QqArticleStandardCreateParam;
use qq\models\ArticleCategory;
use qq\models\QqArticleCategoryNormal;
use qq\models\Article;
use qq\models\QqTpAppPenguin;
use qq\models\QqArticleNormal;
use qq\models\redis\qq_auth\QqTpAppAccessToken as RedisQqAuthQqTpAppAccessToken;
use qq\services\QqArticleService;
use yii\helpers\Url;
use yii\helpers\ArrayHelper;
use yii\web\ServerErrorHttpException;
use yii\web\HttpException;

/**
 * 发布文章类型:标准(普通、图文)的文章至渠道发布
 *
 * 1、请求参数列表
 * (1)uuid:必填,企鹅号ID(UUID)
 * (2)article_type_code:必填,文章类型代码,standard:标准(普通)
 * (3)article_category_id:必填,文章分类ID
 * (4)title:必填,标题
 * (5)author:可选,作者,默认:空字符串
 * (6)source:必填,来源,xContent:内容库;vms:视频管理系统;cms:内容管理系统;spider:自媒体
 * (7)source_user_id:必填,来源用户ID
 * (8)source_article_id:必填,来源文章ID
 * (9)content:必填,文章内容
 * (10)cover_pic:必填,文章封面图
 * (11)cover_type:可选,文章封面类型,1:单图;3:三图,默认:1
 * (12)tag:可选,文章标签,以英文半角逗号分隔,默认:空字符串
 * (13)apply:可选,是否申请原创文章,0:否;1:是(需要用户具有发表图文原创文章资格否则无效),默认:0
 * (14)original_platform:可选,原创首发平台,申请原创文章时必填,默认:0
 * (15)original_url:可选,原创首发链接,申请原创文章时当选择平台不是企鹅号时必填,默认:空字符串
 * (16)original_author:可选,原创首发作者,申请原创文章时当选择平台不是企鹅号时必填,默认:空字符串
 *
 * 2、输入数据验证规则
 * (1)必填:uuid、article_type_code、article_category_id、title、source、source_user_id、source_article_id、content、cover_pic
 * (2)默认值(''):author、tag、original_url、original_author
 * (3)默认值(1):cover_type
 * (4)默认值(0):apply、original_platform
 * (5)比对:article_type_code 其值必须等于 standard
 * (6)存在性:uuid 必须存在于企鹅号的第三方服务平台应用的企鹅媒体用户模型中,且其状态为 1:启用
 * (7)存在性:article_category_id 必须存在于文章分类模型中,且其状态为 1:启用
 * (8)存在性:article_category_id 必须存在于企鹅号的文章类型(文章)的文章分类模型中,且其状态为 1:启用
 * (9)字符串(最大长度:32):group_id、source
 * (10)字符串(最大长度:64):author
 * (11)字符串(最大长度:255):title
 * (12)整数:source_user_id、source_article_id
 * (13)范围(['xContent', 'vms', 'cms', 'spider']):source
 * (14)字符串:content
 * (15)字符串(最大长度:64):original_author
 * (16)字符串(最大长度:255):cover_pic、tag、original_url
 * (17)整数:cover_type、apply、original_platform
 * (18)范围([1, 3]):cover_type
 * (19)范围([0, 1]):apply
 * (20)范围([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]):original_platform
 * (21)网址:original_url
 * (22)必填:original_platform(当 apply 等于 1 时)
 * (23)范围([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]):original_platform(当 apply 等于 1 时)
 * (24)必填:original_url(当 apply 等于 1 且 original_platform 不等于 1 时)
 * (25)必填:original_author(当 apply 等于 1 且 original_platform 不等于 1 时)
 * (26)存在性:uuid 必须存在于企鹅号的第三方服务平台应用的访问令牌(Redis)模型中,且其状态为 1:启用
 * (27)用户刷新令牌有效截止时间必须 大于等于 服务器时间
 *
 * 3、操作数据(事务)
 * (1)插入数据
 *
 * For more details and usage information on CreateAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class CreateAction 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 \yii\db\ActiveRecordInterface the model newly created
     * @throws ServerErrorHttpException if there is any error when creating the model
     */
    public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }

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

        /* 标准(普通、图文)的文章发布参数 */
        $qqArticleStandardCreateParam = new QqArticleStandardCreateParam();
        // 把请求数据填充到模型中
        if (!$qqArticleStandardCreateParam->load($requestParams, '')) {
            return ['code' => 40009, 'message' => Yii::t('error', '40009')];
        }
        // 验证模型
        if (!$qqArticleStandardCreateParam->validate()) {
            $qqArticleStandardCreateParamResult = self::handleValidateError($qqArticleStandardCreateParam);
            if ($qqArticleStandardCreateParamResult['status'] === false) {
                return ['code' => $qqArticleStandardCreateParamResult['code'], 'message' => $qqArticleStandardCreateParamResult['message']];
            }
        }

        /* 基于文章类型代码定义场景 */
        $this->scenario = 'qq_article_' . $qqArticleStandardCreateParam->article_type_code . '_create';

        /* 实例化多个模型 */

        // 企鹅号的第三方服务平台应用的企鹅媒体用户
        $qqTpAppPenguin = new QqTpAppPenguin([
            'scenario' => $this->scenario,
        ]);
        // 转换标准(普通、图文)的文章发布参数,多模型的填充、验证的实现
        $requestParams[$qqTpAppPenguin->formName()]['uuid'] = $qqArticleStandardCreateParam->uuid;
        $qqTpAppPenguinResult = self::handleLoadAndValidate($qqTpAppPenguin, $requestParams);
        if ($qqTpAppPenguinResult['status'] === false) {
            return ['code' => $qqTpAppPenguinResult['code'], 'message' => $qqTpAppPenguinResult['message']];
        }

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

        // 如果当前场景为:qq_article_standard_create
        if ($this->scenario == Article::SCENARIO_STANDARD_CREATE) {
            // 文章分类
            $qqArticleCategoryNormal = new QqArticleCategoryNormal([
                'scenario' => $this->scenario,
            ]);
            // 转换标准(普通、图文)的文章发布参数,多模型的填充、验证的实现
            $requestParams[$qqArticleCategoryNormal->formName()]['article_category_id'] = $qqArticleStandardCreateParam->article_category_id;
            $qqArticleCategoryNormalResult = self::handleLoadAndValidate($qqArticleCategoryNormal, $requestParams);
            if ($qqArticleCategoryNormalResult['status'] === false) {
                return ['code' => $qqArticleCategoryNormalResult['code'], 'message' => $qqArticleCategoryNormalResult['message']];
            }
        }

        // 文章
        $model = new $this->modelClass([
            'scenario' => $this->scenario,
        ]);
        // 转换标准(普通、图文)的文章发布参数,多模型的填充、验证的实现
        $requestParams[$model->formName()] = [
            'group_id' => $requestParams['group_id'],
            'title' => $qqArticleStandardCreateParam->title,
            'author' => $qqArticleStandardCreateParam->author,
            'source' => $qqArticleStandardCreateParam->source,
            'source_user_id' => $qqArticleStandardCreateParam->source_user_id,
            'source_article_id' => $qqArticleStandardCreateParam->source_article_id,
        ];
        $modelResult = self::handleLoadAndValidate($model, $requestParams);
        if ($modelResult['status'] === false) {
            return ['code' => $modelResult['code'], 'message' => $modelResult['message']];
        }

        // 如果当前场景为:qq_article_standard_create
        if ($this->scenario == Article::SCENARIO_STANDARD_CREATE) {
            // 企鹅号的文章类型(文章)的文章
            $qqArticleNormal = new QqArticleNormal([
                'scenario' => $this->scenario,
            ]);
            // 转换标准(普通、图文)的文章发布参数,多模型的填充、验证的实现
            $requestParams[$qqArticleNormal->formName()] = [
                'content' => $qqArticleStandardCreateParam->content,
                'cover_pic' => $qqArticleStandardCreateParam->cover_pic,
                'cover_type' => $qqArticleStandardCreateParam->cover_type,
                'tag' => $qqArticleStandardCreateParam->tag,
                'apply' => $qqArticleStandardCreateParam->apply,
                'original_platform' => $qqArticleStandardCreateParam->original_platform,
                'original_url' => $qqArticleStandardCreateParam->original_url,
                'original_author' => $qqArticleStandardCreateParam->original_author,
            ];
            $qqArticleNormalResult = self::handleLoadAndValidate($qqArticleNormal, $requestParams);
            if ($qqArticleNormalResult['status'] === false) {
                return ['code' => $qqArticleNormalResult['code'], 'message' => $qqArticleNormalResult['message']];
            }
        }

        // 企鹅号的第三方服务平台应用的访问令牌(Redis)
        $redisQqAuthQqTpAppAccessToken = new RedisQqAuthQqTpAppAccessToken([
            'scenario' => $this->scenario,
        ]);
        // 转换标准(普通、图文)的文章发布参数,多模型的填充、验证的实现
        $requestParams[$redisQqAuthQqTpAppAccessToken->formName()]['qq_tp_app_penguin_uuid'] = $qqArticleStandardCreateParam->uuid;
        $redisQqAuthQqTpAppAccessTokenResult = self::handleLoadAndValidate($redisQqAuthQqTpAppAccessToken, $requestParams);
        if ($redisQqAuthQqTpAppAccessTokenResult['status'] === false) {
            throw new HttpException(302, Yii::t('error', '40008'), 40008);
        }

        return ['code' => 10000, 'message' => Yii::t('app', '10003'), 'data' => ''];
    }

    /**
     * 处理模型填充与验证
     * @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.');
        }
    }
}


</pre>
]]>
https://www.shuijingwanwq.com/2018/09/01/2889/feed/ 0
在 Yii 2 中,当处理一个 RESTful API 请求时,支持新的响应格式:text/html,且仅支持 HTML 格式 https://www.shuijingwanwq.com/2018/08/27/2881/ https://www.shuijingwanwq.com/2018/08/27/2881/#respond Mon, 27 Aug 2018 10:17:24 +0000 http://www.shuijingwanwq.com/?p=2881 Post Views: 252 1、在浏览器打开网址:http://api.channel-pub-api.localhost/qq/v1/oauth2/authorize?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,响应 XML 格式,因为 RESTful APIs 同时支持JSON和XML格式。但不支持 HTML 格式。如图1
在浏览器打开网址:http://api.channel-pub-api.localhost/qq/v1/oauth2/authorize?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,响应 XML 格式,因为 RESTful APIs 同时支持JSON和XML格式。但不支持 HTML 格式

图1

2、在 Postman 中打开,Accept 的值为:application/json; version=0.0,响应 JSON 格式,如图2
在 Postman 中打开,Accept 的值为:application/json; version=0.0,响应 JSON 格式

图2



{
    "name": "Unprocessable entity",
    "message": "数据验证失败:第三方服务平台授权后重定向的回调链接不能为空",
    "code": 40004,
    "status": 422,
    "type": "yii\\web\\UnprocessableEntityHttpException"
}


3、现在需要增加对于 HTML 格式的支持,且仅支持 HTML 格式,因为是一个页面,而不是一个接口,最后还会跳转至对应的回调链接页面,编辑 \qq\rests\oauth2\AuthorizeAction.php,设置 format 属性,format 属性指定 data 中数据格式化后的样式
<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\oauth2;

use Yii;
use yii\base\Model;
use yii\web\Response;
use yii\base\DynamicModel;
use yii\web\ServerErrorHttpException;
use yii\web\UnprocessableEntityHttpException;

/**
 * 第三方服务平台授权(引导用户进入授权页面登录同意授权)
 *
 * 1、请求参数列表
 * (1)redirect_uri:必填,第三方服务平台授权后重定向的回调链接
 *
 * 2、输入数据验证规则
 * (1)必填:redirect_uri
 * (2)网址:redirect_uri
 *
 * 3、操作数据
 * (1)302 跳转至 https://auth.om.qq.com/omoauth2/authorize?response_type=code&client_id=626d0ce988bf5fddb3d9dd9ce627b2ba&state=STATE&redirect_uri=http%3A%2F%2Fapi.channel-pub-api-localhost.chinamcloud.com%2Fqq%2Fv1%2Foauth2%2Faccess-token%3Fgroup_id%3D015ce30b116ce86058fa6ab4fea4ac63%26redirect_uri%3Dhttp%3A%2F%2Fwww.zmt.com
 *
 * For more details and usage information on AuthorizeAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class AuthorizeAction 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';

    /**
     * Authorizes a new model.
     * @return \yii\db\ActiveRecordInterface the model newly created
     * @throws ServerErrorHttpException if there is any error when creating the model
     */
    public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }

        $request = Yii::$app->request;
        $get = $request->get();
        $redirect_uri = $request->get('redirect_uri');

        Yii::$app->response->format = Response::FORMAT_HTML;

        // 临时验证
        $model = DynamicModel::validateData(compact('redirect_uri'), [
            [['redirect_uri'], 'required'],
            [['redirect_uri'], 'url'],
        ]);

        if ($model->hasErrors()) {
            foreach ($model->getFirstErrors() as $message) {
                $firstErrors = $message;
                break;
            }
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20004'), ['firstErrors' => $firstErrors])), 20004);
        }

        // 包含 host info 的整个 URL,编码 URL 字符串
        $redirectUri = urlencode($request->hostInfo . $request->baseUrl . '/v1/oauth2/access-token?group_id=' . Yii::$app->params['groupId'] . '&redirect_uri=' . $redirect_uri);

        /* 浏览器跳转:引导用户进入授权页面登录同意授权,获取 code */
        Yii::$app->response->redirect(Yii::$app->params['qqAuth']['hostInfo'] . Yii::$app->params['qqAuth']['baseUrl'] . '/authorize?response_type=code&client_id=' . Yii::$app->params['qqAuth']['tpApp']['clientId'] . '&state=STATE&redirect_uri=' . $redirectUri);
    }
}


</pre>
4、在浏览器打开网址:http://api.channel-pub-api.localhost/qq/v1/oauth2/authorize?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,响应 HTML 格式,符合预期,如图3
在浏览器打开网址:http://api.channel-pub-api.localhost/qq/v1/oauth2/authorize?group_id=015ce30b116ce86058fa6ab4fea4ac63 ,响应 HTML 格式,符合预期

图3

5、在 Postman 中打开,Accept 的值为:application/json; version=0.0,响应 HTML 格式,符合预期,如图4
在 Postman 中打开,Accept 的值为:application/json; version=0.0,响应 HTML 格式,符合预期

图4

]]>
https://www.shuijingwanwq.com/2018/08/27/2881/feed/ 0
在 Yii 2 高级项目模板 上的基于 Nginx 的单域名配置 https://www.shuijingwanwq.com/2018/08/16/2836/ https://www.shuijingwanwq.com/2018/08/16/2836/#respond Thu, 16 Aug 2018 10:02:52 +0000 http://www.shuijingwanwq.com/?p=2836 Post Views: 138 1、现阶段的目录结构中有3个应用,分别为:frontend、backend、api,其域名分别配置为 :http://www.channel-pub-api.localhost/ 、http://www.channel-pub-api.localhost/backend 、http://www.channel-pub-api.localhost/api 2、编辑 hosts 文件


# channel-pub-api
127.0.0.1 channel-pub-api.localhost
127.0.0.1 www.channel-pub-api.localhost


3、参考:https://github.com/mickgeek/yii2-advanced-one-domain-config/blob/master/vhosts/nginx.conf ,编辑 \frontend\config\main.php


return [
    'components' => [
        'request' => [
            'baseUrl' => '',
            'csrfParam' => '_csrf-frontend',
        ],
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules' => [
            ],
        ],
    ],
];


4、编辑 \backend\config\main.php,设置 baseUrl,分离 Session 和 Cookie


return [
    'homeUrl' => '/backend',
    'components' => [
        'request' => [
            'baseUrl' => '/backend',
            'csrfParam' => '_csrf-backend',
            'csrfCookie' => [
                'httpOnly' => true,
                'path' => '/backend',
            ],
        ],
        'user' => [
            'identityClass' => 'backend\models\User',
            'enableAutoLogin' => true,
            'identityCookie' => [
                'name' => '_identity-backend',
                'path' => '/backend',
                'httpOnly' => true
            ],
        ],
        'session' => [
            // this is the name of the session cookie used for login on the backend
            'name' => 'advanced-backend',
            'cookieParams' => [
                'path' => '/backend',
            ],
        ],
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules' => [
            ],
        ],
    ],
];


5、编辑 \api\config\main.php


return [
    'homeUrl' => '/api',
    'components' => [
        'request' => [
            'baseUrl' => '/api',
            'csrfParam' => '_csrf-api',
            'parsers' => [
                'application/json' => 'yii\web\JsonParser',
            ]
        ],
        'urlManager' => require __DIR__ . '/urlManager.php',
    ],
];


6、编辑 \api\config\urlManager.php


return [
    'class' => yii\web\UrlManager::class,
    'enablePrettyUrl' => true,
    'enableStrictParsing' => true,
    'showScriptName' => false,
    'rules' => [
    ],
];


7、编辑 \environments\dev\frontend\web\robots.txt、\environments\prod\frontend\web\robots.txt 编辑前:


User-agent: *
Disallow: /


编辑后:


User-agent: *
Disallow: /frontend/web
Disallow: /backend/web
Disallow: /api/web


8、编辑 channel-pub-api.conf 文件


## FRONTEND ##
server {
    charset utf-8;
    client_max_body_size 128M;

    listen 80; ## listen for ipv4
    #listen [::]:80 default_server ipv6only=on; ## listen for ipv6

    server_name www.channel-pub-api.localhost;

    root E:/wwwroot/channel-pub-api;
    index index.php;

    access_log  logs/www.channel-pub-api.localhost.access.log;
    error_log   logs/www.channel-pub-api.localhost.error.log;

	location / {
        root E:/wwwroot/channel-pub-api/frontend/web;
        try_files $uri $uri/ /frontend/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 /backend {
        alias E:/wwwroot/channel-pub-api/backend/web/;

        # redirect to the URL without a trailing slash (uncomment if necessary)
        #location = /backend/ {
        #    return 301 /backend;
        #}

        # prevent the directory redirect to the URL with a trailing slash
        location = /backend {
            # if your location is "/backend", try use "/backend/backend/web/index.php$is_args$args"
            # bug ticket: https://trac.nginx.org/nginx/ticket/97
            try_files $uri /backend/backend/web/index.php$is_args$args;
        }

        # if your location is "/backend", try use "/backend/backend/web/index.php$is_args$args"
        # bug ticket: https://trac.nginx.org/nginx/ticket/97
        try_files $uri $uri/ /backend/backend/web/index.php$is_args$args;

        # omit static files logging, and if they don't exist, avoid processing by Yii (uncomment if necessary)
        #location ~ ^/backend/.+\.(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 ~ ^/backend/assets/.+\.php(/|$) {
            deny all;
        }
    }

	location /api {
        alias E:/wwwroot/channel-pub-api/api/web/;

        # redirect to the URL without a trailing slash (uncomment if necessary)
        #location = /api/ {
        #    return 301 /api;
        #}

        # prevent the directory redirect to the URL with a trailing slash
        location = /api {
            # if your location is "/api", try use "/api/api/web/index.php$is_args$args"
            # bug ticket: https://trac.nginx.org/nginx/ticket/97
            try_files $uri /api/api/web/index.php$is_args$args;
        }

        # if your location is "/api", try use "/api/api/web/index.php$is_args$args"
        # bug ticket: https://trac.nginx.org/nginx/ticket/97
        try_files $uri $uri/ /api/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 ~ ^/api/.+\.(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 ~ ^/api/assets/.+\.php(/|$) {
            deny all;
        }
    }

	location ~ ^/.+\.php(/|$) {
        rewrite (?!^/((frontend|backend|api)/web|backend|api))^ /frontend/web$uri break;
        rewrite (?!^/backend/web)^/backend(/.+)$ /backend/web$1 break;
		rewrite (?!^/api/web)^/api(/.+)$ /api/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;
    }

    location ~ /\. {
        deny all;
    }
}

## MISC ##

### WWW Redirect ###
server {
    listen       80;
    server_name  channel-pub-api.localhost;
    return       301 http://www.channel-pub-api.localhost$request_uri;
}



9、打开网址:http://www.channel-pub-api.localhost/css/site.css ,前端静态资源可以成功访问,如图1
打开网址:http://www.channel-pub-api.localhost/css/site.css ,前端静态资源可以成功访问

图1

10、打开网址:http://www.channel-pub-api.localhost/site/signup ,前端路由符合预期,成功响应,如图2
打开网址:http://www.channel-pub-api.localhost/site/signup ,前端路由符合预期,成功响应

图2

11、打开网址:http://www.channel-pub-api.localhost/debug/default/index ,前端路由的 Debug 符合预期,成功响应,如图3
打开网址:http://www.channel-pub-api.localhost/debug/default/index ,前端路由的 Debug 符合预期,成功响应

图3

12、打开网址:http://www.channel-pub-api.localhost/backend/css/site.css ,后端静态资源可以成功访问,如图4
打开网址:http://www.channel-pub-api.localhost/backend/css/site.css ,后端静态资源可以成功访问

图4

13、打开网址:http://www.channel-pub-api.localhost/backend/site/login ,后端路由符合预期,成功响应,如图5
打开网址:http://www.channel-pub-api.localhost/backend/site/login ,后端路由符合预期,成功响应

图5

14、打开网址:http://www.channel-pub-api.localhost/backend/debug/default/index ,后端路由的 Debug 符合预期,成功响应,如图6
打开网址:http://www.channel-pub-api.localhost/backend/debug/default/index ,后端路由的 Debug 符合预期,成功响应

图6

15、打开网址:http://www.channel-pub-api.localhost/api/code/css/main.css ,接口静态资源可以成功访问,如图7
打开网址:http://www.channel-pub-api.localhost/api/code/css/main.css ,接口静态资源可以成功访问

图7

16、打开网址:http://www.channel-pub-api.localhost/api/v1/users ,接口路由符合预期,成功响应,如图8
打开网址:http://www.channel-pub-api.localhost/api/v1/users ,接口路由符合预期,成功响应

图8

17、打开网址:http://www.channel-pub-api.localhost/api/debug/default/index ,接口路由的 Debug 符合预期,成功响应,如图9
http://www.channel-pub-api.localhost/api/debug/default/index

图9

]]>
https://www.shuijingwanwq.com/2018/08/16/2836/feed/ 0
基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建远程过程调用应用(实现基于 Hprose 2.0 for PHP 的 RPC 服务端),新建 rpc 目录、配置和环境、测试、Vagrant等的支持 https://www.shuijingwanwq.com/2018/08/15/2829/ https://www.shuijingwanwq.com/2018/08/15/2829/#respond Wed, 15 Aug 2018 01:48:49 +0000 http://www.shuijingwanwq.com/?p=2829 Post Views: 134

1、在 api 目录中实现 页面 的相应 RESTful 风格的 Web Service 服务的 API,然后再调整为 RPC 客户端

2、页面 的相应 API 全部实现后,在 Postman 中执行 POST 请求,响应正常,如图1

页面 的相应 API 全部实现后,在 Postman 中执行 POST 请求,响应正常

图1


{
	"title":"title-20180731-1",
	"body":"body-20180731-1"
}



{
    "code": 10000,
    "message": "创建页面成功",
    "data": {
        "title": "title-20180731-1",
        "body": "body-20180731-1",
        "slug": "title-20180731-1",
        "created_at": 1533015414,
        "updated_at": 1533015414,
        "id": 1
    }
}


3、在 Postman 中执行 PUT /pages/1 请求,响应正常


{
	"slug":"slug-20180731-1",
	"title":"title-20180731-1",
	"body":"body-20180731-1",
	"status":2
}



{
    "code": 10000,
    "message": "更新页面成功",
    "data": {
        "id": 1,
        "slug": "slug-20180731-1",
        "title": "title-20180731-1",
        "body": "body-20180731-1",
        "view": 0,
        "status": 2,
        "created_at": 1533015414,
        "updated_at": 1533015770
    }
}


4、在 Postman 中执行 GET /pages 请求,响应正常


{
    "code": 10000,
    "message": "获取页面列表成功",
    "data": {
        "items": [
            {
                "id": 1,
                "slug": "slug-20180731-1",
                "title": "title-20180731-1",
                "body": "body-20180731-1",
                "view": 0,
                "status": 2,
                "created_at": 1533015414,
                "updated_at": 1533015770
            }
        ],
        "_links": {
            "self": {
                "href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/pages?page=1"
            }
        },
        "_meta": {
            "totalCount": 1,
            "pageCount": 1,
            "currentPage": 1,
            "perPage": 20
        }
    }
}


5、在 Postman 中执行 GET /pages/1 请求,响应正常


{
    "code": 10000,
    "message": "获取用户详情成功",
    "data": {
        "id": 1,
        "slug": "slug-20180731-1",
        "title": "title-20180731-1",
        "body": "body-20180731-1",
        "view": 0,
        "status": 2,
        "created_at": 1533015414,
        "updated_at": 1533015770
    }
}


6、在 Postman 中执行 DELETE /pages/1 请求,响应正常


{
    "code": 10000,
    "message": "删除页面成功"
}


7、在 Postman 中执行 GET /pages 请求,响应正常


filter[title][like]:title
filter[slug][like]:2018
filter[status]:2



{
    "code": 10000,
    "message": "获取页面列表成功",
    "data": {
        "items": [
            {
                "id": 1,
                "slug": "slug-20180731-1",
                "title": "title-20180731-1",
                "body": "body-20180731-1",
                "view": 0,
                "status": 2,
                "created_at": 1533015414,
                "updated_at": 1533019394
            }
        ],
        "_links": {
            "self": {
                "href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/pages?filter%5Btitle%5D%5Blike%5D=title&filter%5Bslug%5D%5Blike%5D=2018&filter%5Bstatus%5D=2&page=1"
            }
        },
        "_meta": {
            "totalCount": 1,
            "pageCount": 1,
            "currentPage": 1,
            "perPage": 20
        }
    }
}


8、使用 composer 命令行安装 Hprose


composer require "hprose/hprose-yii:^2.0"



./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing hprose/hprose (v2.0.35): Downloading (100%)
  - Installing hprose/hprose-yii (v2.0.0): Downloading (100%)
hprose/hprose suggests installing ext-hprose (Faster serialize and unserialize hprose extension.)
hprose/hprose-yii suggests installing ext-hprose (Faster serialize and unserialize hprose extension.)
Writing lock file
Generating autoload files


9、添加 RPC 应用程序(rpc),复制 frontend 至 rpc, environments/dev/frontend 至 environments/dev/rpc 以及 environments/prod/frontend 至 environments/prod/rpc

10、删除目录 \rpc\runtime\debug 下的所有文件,删除目录 \rpc\tests\_output 下除.gitignore之外的所有文件,删除 \rpc\runtime\logs\app.log,在目录 /rpc 中调整命名空间和路径以 rpc 开头(替换 frontend 为 rpc),如图2

删除目录 \rpc\runtime\debug 下的所有文件,删除目录 \rpc\tests\_output 下除.gitignore之外的所有文件,删除 \rpc\runtime\logs\app.log,在目录 /rpc 中调整命名空间和路径以 rpc 开头(替换 frontend 为 rpc)

图2

11、在 common\config\bootstrap.php 中添加 Yii::setAlias(‘rpc’, dirname(dirname(__DIR__)) . ‘/rpc’)

12、编辑environments/index.php,搜索此文件有6处frontend,则相应复制6份,替换为rpc

13、\vagrant\nginx\app.conf,新增虚拟主机 rpc

14、编辑 \vagrant\nginx\log\.gitignore,配置 rpc 的日志文件为忽略

15、编辑 \Vagrantfile,新增 rpc 相关的配置

16、基于 Nginx 配置 Web 服务器,配置虚拟主机:www、backend、api、rpc,编辑 C:\nginx-1.10.1\conf\vhosts\github-shuijingwan-yii2-app-advanced.conf


## FRONTEND ##
server {
    charset utf-8;
    client_max_body_size 128M;

    listen 80; ## listen for ipv4
    #listen [::]:80 default_server ipv6only=on; ## listen for ipv6

    server_name www.github-shuijingwan-yii2-app-advanced.localhost;
    root        E:/wwwroot/github-shuijingwan-yii2-app-advanced/frontend/web;
    index       index.php;

    access_log  logs/www.github-shuijingwan-yii2-app-advanced.localhost.access.log;
    error_log   logs/www.github-shuijingwan-yii2-app-advanced.localhost.error.log;

	location / {
		# Redirect everything that isn't a real file to 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;
	}

	location ~ \.php$ {
		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 $uri =404;
	}

	location ~* /\. {
		deny all;
	}
}

## BACKEND ##
server {
    charset utf-8;
    client_max_body_size 128M;

    listen 80; ## listen for ipv4
    #listen [::]:80 default_server ipv6only=on; ## listen for ipv6

    server_name backend.github-shuijingwan-yii2-app-advanced.localhost;
    root        E:/wwwroot/github-shuijingwan-yii2-app-advanced/backend/web;
    index       index.php;

    access_log  logs/backend.github-shuijingwan-yii2-app-advanced.localhost.access.log;
    error_log   logs/backend.github-shuijingwan-yii2-app-advanced.localhost.error.log;

	location / {
		# Redirect everything that isn't a real file to 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;
	}

	location ~ \.php$ {
		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 $uri =404;
	}

	location ~* /\. {
		deny all;
	}
}

## API ##
server {
    charset utf-8;
    client_max_body_size 128M;

    listen 80; ## listen for ipv4
    #listen [::]:80 default_server ipv6only=on; ## listen for ipv6

    server_name api.github-shuijingwan-yii2-app-advanced.localhost;
    root        E:/wwwroot/github-shuijingwan-yii2-app-advanced/api/web;
    index       index.php;

    access_log  logs/api.github-shuijingwan-yii2-app-advanced.localhost.access.log;
    error_log   logs/api.github-shuijingwan-yii2-app-advanced.localhost.error.log;

	location / {
		# Redirect everything that isn't a real file to 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;
	}

	location ~ \.php$ {
		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 $uri =404;
	}

	location ~* /\. {
		deny all;
	}
}

## RPC ##
server {
    charset utf-8;
    client_max_body_size 128M;

    listen 80; ## listen for ipv4
    #listen [::]:80 default_server ipv6only=on; ## listen for ipv6

    server_name rpc.github-shuijingwan-yii2-app-advanced.localhost;
    root        E:/wwwroot/github-shuijingwan-yii2-app-advanced/rpc/web;
    index       index.php;

    access_log  logs/rpc.github-shuijingwan-yii2-app-advanced.localhost.access.log;
    error_log   logs/rpc.github-shuijingwan-yii2-app-advanced.localhost.error.log;

	location / {
		# Redirect everything that isn't a real file to 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;
	}

	location ~ \.php$ {
		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 $uri =404;
	}

	location ~* /\. {
		deny all;
	}
}

## MISC ##

### WWW Redirect ###
server {
    listen       80;
    server_name  github-shuijingwan-yii2-app-advanced.localhost;
    return       301 http://www.github-shuijingwan-yii2-app-advanced.localhost$request_uri;
}



17、打开 Windows PowerShell,执行 init 命令并选择 dev 作为环境,api应用所需环境配置文件自动生成,如图3

打开 Windows PowerShell,执行 init 命令并选择 dev 作为环境,api应用所需环境配置文件自动生成

图3


.\init
0
yes
All


18、打开:http://rpc.github-shuijingwan-yii2-app-advanced.localhost/ ,符合预期

19、编辑 codeception.yml 配置包含所有应用程序的测试

20、由于控制台应用程序使用不同的请求组件,因此编辑 \common\config\test.php,注释 request 组件(在 Web 应用程序中需取消注释)


    'components' => [
        'user' => [
            'class' => 'yii\web\User',
            'identityClass' => 'common\models\User',
        ],
        /*
        'request' => [
            'cookieValidationKey' => 'test',
        ],
        */
    ],


21、执行测试的迁移命令,如图4

执行测试的迁移命令

图4


./yii_test migrate --migrationPath=@yii/log/migrations/
./yii_test migrate


22、构建测试套件,然后运行所有的样例测试
注:执行迁移命令(./yii_test migrate)后,需取消注释 request 组件,否则报错:


 Test  ..\common\tests\unit\models\LoginFormTest.php:testLoginCorrect

  [yii\base\InvalidConfigException] yii\web\Request::cookieValidationKey must be configured with a secret key.



vendor/bin/codecept build
vendor/bin/codecept run


23、编辑 README.md,在目录结构中新增 rpc

&lt;p align="center">
    &lt;a href="https://github.com/yiisoft" target="_blank">
        &lt;img src="https://avatars0.githubusercontent.com/u/993323" height="100px">
    &lt;/a>
    &lt;h1 align="center">Yii 2 Advanced Project Template&lt;/h1>
    &lt;br>
&lt;/p>
 
Yii 2 Advanced Project Template is a skeleton [Yii 2](http://www.yiiframework.com/) application best for
developing complex Web applications with multiple tiers.
 
The template includes four tiers: api, front end, back end, and console, each of which
is a separate Yii application.
 
The template is designed to work in a team development environment. It supports
deploying the application in different environments.
 
Documentation is at [docs/guide-zh-CN/README.md](docs/guide-zh-CN/README.md).
 
[![Latest Stable Version](https://img.shields.io/packagist/v/yiisoft/yii2-app-advanced.svg)](https://packagist.org/packages/yiisoft/yii2-app-advanced)
[![Total Downloads](https://img.shields.io/packagist/dt/yiisoft/yii2-app-advanced.svg)](https://packagist.org/packages/yiisoft/yii2-app-advanced)
[![Build Status](https://travis-ci.org/yiisoft/yii2-app-advanced.svg?branch=master)](https://travis-ci.org/yiisoft/yii2-app-advanced)
 
## TABLE OF CONTENTS
- [Basic info](docs/guide-zh-CN/README.md)
- [Installation](docs/guide-zh-CN/start-installation.md)
    - [Manual installation](docs/guide-zh-CN/start-installation.md)
    - [Vagrant installation](docs/guide-zh-CN/start-installation.md#使用vagrant安装)
- [Testing](docs/guide-zh-CN/start-testing.md)
 
DIRECTORY STRUCTURE
-------------------
 
```
common
    config/              contains shared configurations
    fixtures/            contains fixtures for common classes
    logics/              contains logic classes used in both backend and frontend and api and console
    mail/                contains view files for e-mails
    messages/            contains message files for I18N
    models/              contains model classes used in both backend and frontend and api and console
    tests/               contains various tests for common classes
    widgets/             contains common widgets
console
    config/              contains console configurations
    controllers/         contains console controllers (commands)
    migrations/          contains database migrations
    models/              contains console-specific model classes
    runtime/             contains files generated during runtime
backend
    assets/              contains application assets such as JavaScript and CSS
    config/              contains backend configurations
    controllers/         contains Web controller classes
    models/              contains backend-specific model classes
    runtime/             contains files generated during runtime
    tests/               contains various tests for backend application    
    views/               contains view files for the Web application
    web/                 contains the entry script and Web resources
frontend
    assets/              contains application assets such as JavaScript and CSS
    config/              contains frontend configurations
    controllers/         contains Web controller classes
    models/              contains frontend-specific model classes
    runtime/             contains files generated during runtime
    tests/               contains various tests for frontend application
    views/               contains view files for the Web application
    web/                 contains the entry script and Web resources
    widgets/             contains frontend widgets
api
    behaviors/           contains behaviors for api application
    config/              contains api configurations
    controllers/         contains Web controller classes
    fixtures/            contains fixtures for api application
    models/              contains api-specific model classes
    modules/             contains modules for api application
    rests/               contains rests for api application
    runtime/             contains files generated during runtime
    tests/               contains various tests for api application
    views/               contains view files for the Web application
    web/                 contains the entry script and Web resources
rpc
    assets/              contains application assets such as JavaScript and CSS
    config/              contains rpc configurations
    controllers/         contains Web controller classes
    models/              contains rpc-specific model classes
    runtime/             contains files generated during runtime
    tests/               contains various tests for rpc application
    views/               contains view files for the Web application
    web/                 contains the entry script and Web resources
    widgets/             contains rpc widgets
vendor/                  contains dependent 3rd-party packages
environments/            contains environment-based overrides
.gitignore               contains a list of directories ignored by git version system. If you need something never get to your source code repository, add it there.
composer.json            Composer config described in Configuring Composer.
init                     initialization script described in Configuration and environments.
init.bat                 same for Windows.
LICENSE.md               license info. Put your project license there. Especially when opensourcing.
README.md                basic info about installing template. Consider replacing it with information about your project and its installation.
requirements.php         Yii requirements checker.
yii                      console application bootstrap.
yii.bat                  same for Windows.
```
 
 

24、测试使用Vagrant安装,是否支持 rpc,参考网址:https://github.com/yiisoft/yii2-app-advanced/blob/master/docs/guide-zh-CN/start-installation.md ,使用Vagrant安装(Windows 用户手册)

25、编辑如下代码到 hosts 文件


# github-shuijingwan-yii2-app-advanced Vagrant
192.168.83.137 y2aa-frontend.test y2aa-backend.test y2aa-api.test y2aa-rpc.test


26、打开Windows PowerShell,切换路径至项目根目录,并且执行如下命令(需要翻墙,可能需要执行多次),期间有报错,参考第20步骤处理,报错:The name of your virtual machine couldn’t be set because VirtualBox
is reporting another VM with that name already exists. ,打开 VirtualBox,删除虚拟机 ubuntu-16.04-amd64_1533104831432_66987,删除目录 F:\VirtualBox VMs\ubuntu-16.04-amd64_1533105278434_57210


vagrant plugin install vagrant-hostmanager
vagrant up


27、等待完成后,在浏览器中访问如下URL即可,符合预期

frontend: http://y2aa-frontend.test
backend: http://y2aa-backend.test
api: http://y2aa-api.test
rpc: http://y2aa-rpc.test

]]>
https://www.shuijingwanwq.com/2018/08/15/2829/feed/ 0