jwj 发布的文章

‎aria2 是用于下载文件的实用程序。它支持 HTTP(S)/FTP/SFTP/BitTorrent 和 Metalink 协议。aria2可以从多个来源/协议下载文件,并尝试利用您的最大下载带宽。它支持同时从HTTP(S)/FTP/SFTP和BitTorrent下载文件,而从HTTP(S)/FTP/SFTP下载的数据上传到BitTorrent群。使用 Metalink 块校验和,aria2 在下载文件时自动验证数据块。‎

安装Aria2

sudo apt install aria2

image.png

配置Aria2配置文件

Arai2默认会从$HOME/.aria2/aria2.conf$XDG_CONFIG_HOME/aria2/aria2.conf这两个位置读取配置,为了方便管理,我们可以将配置文件放到/etc/aria2/aria2.conf,以方便管理。

创建/etc/aria2目录,新建并编辑/etc/aria2/aria2.conf配置文件

sudo mkdir /etc/aria2/
sudo vim /etc/aria2/aria2.conf

创建下载文件保存目录

mkdir /home/ubuntu/downloads

把以下配置内容写入到/etc/aria2/aria2.conf

# 设置加密的密钥
rpc-secret=12580
# RPC 开关
enable-rpc=true
# RPC 跨域(WEB 界面需要跨域)
rpc-allow-origin-all=true
# RPC 外部访问(false 的话只监听本地端口)
rpc-listen-all=true
# RPC 端口, 仅当默认端口被占用时修改
rpc-listen-port=6800
# 最大并行下载数(默认值:5)
#max-concurrent-downloads=5
# 单文件并行下载数
split=5
# 断点续传
continue=true
# 同服务器最大连接数
max-connection-per-server=5
# 最小文件分片大小, 下载线程数上限取决于能分出多少片, 对于小文件重要
min-split-size=10M
# 下载速度限制(0代表不限制)(示例值:500K、10M)
max-overall-download-limit=0
# 单文件速度限制
max-download-limit=0
# 上传速度限制
max-overall-upload-limit=0
# 单文件速度限制
max-upload-limit=0
# 断开速度过慢的连接
#lowest-speed-limit=0
# 设置请求头 referer
#referer=*
# 文件保存路径, 默认为当前启动位置
dir=/home/ubuntu/downloads
# 启用磁盘缓存
#disk-cache=0
# 文件分配方法
#file-allocation=prealloc

配置开机启动

sudo nano /etc/systemd/system/aria2.service
[Unit]
Description=aria2 Service
After=network.target

[Service]
Type=simple
User=www
Group=www
Restart=on-failure
RestartSec=5s
ExecStart=aria2c --conf-path=/etc/aria2/aria2.conf

[Install]
WantedBy=multi-user.target

此时,我们尝试下启动服务

sudo systemctl start aria2

然后查看服务状态

sudo systemctl status aria2

如果Active的状态是 active (running) ,则代表服务运行正常。

那我们就可以直接启用服务了,那么服务就会开机时自动启动。

sudo systemctl enable aria2

开放端口

防火墙放通 6800 端口

sudo ufw allow 6800
sudo ufw enable

imageb714790991761b6e.png

参考资料

从ThinkPHP6.0.2升级到ThinkPHP6.0.3后,测试整体网站,发现用了多对多关联关联统计的地方均报错SQLSTATE[42000]: Syntax error or access violation: 1066 Not unique table/alias: 'pivot'。经过排查,系think-orm扩展最近一次的升级,调整了多对多关联导致的错误。

应用代码

下面是关键代码部分

控制器

\app\model\TaskLabel::withCount(['task'])->select()

TaskLabel模型

<?php
namespace app\model;

use think\Model;

/**
 * 任务标签模型
 * @package app\model
 */
class TaskLabel extends Model
{
    // 自动写入时间
    protected $autoWriteTimestamp = 'timestamp';

    // 关闭更新时间
    protected $updateTime = false;

    /**
     * 任务关联
     * @return \think\model\relation\BelongsToMany
     */
    public function task()
    {
        return $this->belongsToMany(Task::class, TaskLabelPivot::class, 'task_id', 'label_id')
            ->where('status', '>', 0);
    }
}

中间表模型

<?php
namespace app\model;

use think\model\Pivot;

/**
 * 任务标签中间表模型
 * @package app\model
 */
class TaskLabelPivot extends Pivot
{
    // 自动写入时间
    protected $autoWriteTimestamp = 'timestamp';

    // 关闭更新时间
    protected $updateTime = false;
}

任务模型

<?php
namespace app\model;

use think\Model;

/**
 * 任务
 * @package app\model
 */
class Task extends Model
{
    // 自动写入时间
    protected $autoWriteTimestamp = 'timestamp';
}

流程解刨

1.执行withCount方法

\think\db\BaseQuery::withCount()

2.执行withAggregate方法

\think\db\BaseQuery::withAggregate()

3.执行relationCount方法

\think\Model::relationCount()

4.执行task方法

\app\model\TaskLabel::task()

5.触发__call方法

因为where是Query里面的方法,在关联类里面不存在,所以会触发魔术方法

\think\model\Relation::__call()

6.执行baseQuery方法

\think\model\relation\BelongsToMany::baseQuery()

7.执行belongsToManyQuery方法

这方法里面会调用Query类的fieldjoinwhere等方法

\think\model\relation\BelongsToMany::belongsToManyQuery()

8.执行getRelationCountQuery方法

\think\model\relation\BelongsToMany::getRelationCountQuery()

9.执行belongsToManyQuery方法

这方法里面会调用Query类的fieldjoinwhere等方法

\think\model\relation\BelongsToMany::belongsToManyQuery()

问题总结

从执行流程可以看出,\think\model\relation\BelongsToMany::belongsToManyQuery()执行了两次,导致的结果就是join也执行了两次,出现了开头的报错SQLSTATE[42000]: Syntax error or access violation: 1066 Not unique table/alias: 'pivot'

belongsToManyQuery为什么会执行两次呢?
主要原因就是我在关联方法里面使用了where方法,这个方法是Query类的,在关联类里面不存在,所以会触发魔术方法。
而魔术方法里会执行baseQuerybaseQuery又会执行belongsToManyQuerygetRelationCountQuery也会执行belongsToManyQuery。就这样,join重复了。

问题已找到,关联方法后面不能跟着Query类的方法,否则就会出错。目前尚不清楚是框架的问题还是自己的用法问题,但框架问题的可能性大些,毕竟用法按照手册也报错。

解决办法

虽然找问题的时间很长,但最终的解决办法也很简单,只需要将think-orm扩展降级即可。

composer require topthink/think-orm:v2.0.32

好了,散了散了,该干嘛干嘛去。等官方出结果

2020-07-15:目前最新开发版已修复该问题,除了降级,我们还可以使用下列命令升级到最新开发版。

composer require topthink/think-orm:2.0.x-dev

其它

下面是完整的执行流程记录,备份记录下吧

array(9) {
  [0]=>
  array(5) {
    ["file"]=>
    string(63) "/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php"
    ["line"]=>
    int(687)
    ["function"]=>
    string(18) "belongsToManyQuery"
    ["class"]=>
    string(34) "think\model\relation\BelongsToMany"
    ["type"]=>
    string(2) "->"
  }
  [1]=>
  array(5) {
    ["file"]=>
    string(49) "/vendor/topthink/think-orm/src/model/Relation.php"
    ["line"]=>
    int(249)
    ["function"]=>
    string(9) "baseQuery"
    ["class"]=>
    string(34) "think\model\relation\BelongsToMany"
    ["type"]=>
    string(2) "->"
  }
  [2]=>
  array(5) {
    ["file"]=>
    string(24) "/app/model/TaskLabel.php"
    ["line"]=>
    int(25)
    ["function"]=>
    string(6) "__call"
    ["class"]=>
    string(20) "think\model\Relation"
    ["type"]=>
    string(2) "->"
  }
  [3]=>
  array(5) {
    ["file"]=>
    string(61) "/vendor/topthink/think-orm/src/model/concern/RelationShip.php"
    ["line"]=>
    int(392)
    ["function"]=>
    string(4) "task"
    ["class"]=>
    string(19) "app\model\TaskLabel"
    ["type"]=>
    string(2) "->"
  }
  [4]=>
  array(5) {
    ["file"]=>
    string(64) "/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php"
    ["line"]=>
    int(273)
    ["function"]=>
    string(13) "relationCount"
    ["class"]=>
    string(11) "think\Model"
    ["type"]=>
    string(2) "->"
  }
  [5]=>
  array(5) {
    ["file"]=>
    string(64) "/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php"
    ["line"]=>
    int(325)
    ["function"]=>
    string(13) "withAggregate"
    ["class"]=>
    string(18) "think\db\BaseQuery"
    ["type"]=>
    string(2) "->"
  }
  [6]=>
  array(3) {
    ["function"]=>
    string(9) "withCount"
    ["class"]=>
    string(18) "think\db\BaseQuery"
    ["type"]=>
    string(2) "->"
  }
  [7]=>
  array(3) {
    ["file"]=>
    string(40) "/vendor/topthink/think-orm/src/Model.php"
    ["line"]=>
    int(1047)
    ["function"]=>
    string(20) "call_user_func_array"
  }
  [8]=>
  array(5) {
    ["file"]=>
    string(24) "/app/controller/Task.php"
    ["line"]=>
    int(573)
    ["function"]=>
    string(12) "__callStatic"
    ["class"]=>
    string(11) "think\Model"
    ["type"]=>
    string(2) "::"
  }
}
array(7) {
  [0]=>
  array(5) {
    ["file"]=>
    string(63) "/vendor/topthink/think-orm/src/model/relation/BelongsToMany.php"
    ["line"]=>
    int(379)
    ["function"]=>
    string(18) "belongsToManyQuery"
    ["class"]=>
    string(34) "think\model\relation\BelongsToMany"
    ["type"]=>
    string(2) "->"
  }
  [1]=>
  array(5) {
    ["file"]=>
    string(61) "/vendor/topthink/think-orm/src/model/concern/RelationShip.php"
    ["line"]=>
    int(392)
    ["function"]=>
    string(21) "getRelationCountQuery"
    ["class"]=>
    string(34) "think\model\relation\BelongsToMany"
    ["type"]=>
    string(2) "->"
  }
  [2]=>
  array(5) {
    ["file"]=>
    string(64) "/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php"
    ["line"]=>
    int(273)
    ["function"]=>
    string(13) "relationCount"
    ["class"]=>
    string(11) "think\Model"
    ["type"]=>
    string(2) "->"
  }
  [3]=>
  array(5) {
    ["file"]=>
    string(64) "/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php"
    ["line"]=>
    int(325)
    ["function"]=>
    string(13) "withAggregate"
    ["class"]=>
    string(18) "think\db\BaseQuery"
    ["type"]=>
    string(2) "->"
  }
  [4]=>
  array(3) {
    ["function"]=>
    string(9) "withCount"
    ["class"]=>
    string(18) "think\db\BaseQuery"
    ["type"]=>
    string(2) "->"
  }
  [5]=>
  array(3) {
    ["file"]=>
    string(40) "/vendor/topthink/think-orm/src/Model.php"
    ["line"]=>
    int(1047)
    ["function"]=>
    string(20) "call_user_func_array"
  }
  [6]=>
  array(5) {
    ["file"]=>
    string(24) "/app/controller/Task.php"
    ["line"]=>
    int(573)
    ["function"]=>
    string(12) "__callStatic"
    ["class"]=>
    string(11) "think\Model"
    ["type"]=>
    string(2) "::"
  }
}

开启WSL

以管理员身份启动 Windows PowerShell,输入以下命令

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

命令执行可能需要几分钟,系统设置完成后会提示是否重启,我们输入“Y”重启系统以完成设置

下载子系统

WIndows获取发行版子系统下载链接。
然后再命令行执行下载,文件大概四五百兆

Invoke-WebRequest -Uri https://aka.ms/wslubuntu2004 -OutFile Ubuntu.appx -UseBasicParsing

安装子系统

Add-AppxPackage .\Ubuntu.appx

安装好之后,打开开始菜单会看到一个新的应用程序,打开它即可打开子系统。
第一次运行需要配置用户和密码,按照提示设置用户和密码即可。

资料参考:https://blog.irain.in/archives/Windows_Server_2019_Subsystem.html

准备工作

创建相关目录和文件

mkdir -p ./CA/{private,newcerts} && cd ./CA
echo 01 > serial
touch index.txt index.txt.attr

OpenSSL配置文件

编辑openssl.cnf

vi openssl.cnf

将以下内容复制到openssl.cnf文件

[ ca ]
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir             = ../CA                 # Where everything is kept
certs           = $dir/certs            # Where the issued certs are kept
crl_dir         = $dir/crl              # Where the issued crl are kept
new_certs_dir   = $dir/newcerts         # default place for new certs.
database        = $dir/index.txt        # database index file.
serial          = $dir/serial           # The current serial number
RANDFILE        = $dir/private/.rand    # private random number file
#unique_subject  = no                   # Set to 'no' to allow creation of
                                        # several ctificates with same subject.

# The root key and root certificate.
private_key     = $dir/private/cakey.pem# The private key
certificate     = $dir/cacert.pem       # The CA certificate

# For certificate revocation lists.
crlnumber         = $dir/crlnumber      # the current crl number
crl               = $dir/crl.pem        # The current CRL
crl_extensions    = crl_ext

# SHA-1 is deprecated, so use SHA-2 instead.
preserve          = no                  # keep passed DN ordering
default_md        = sha256              # use SHA-256 by default
default_days      = 365                 # how long to certify for
default_crl_days  = 30                  # how long before next CRL

# A few difference way of specifying how similar the request should look
# For type CA, the listed attributes must be the same, and the optional
# and supplied fields are just that :-)
policy            = policy_match

# For the CA policy
[ policy_match ]
countryName             = match
stateOrProvinceName     = match
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional
vi root.conf
[ req ]
default_bits        = 2048
default_keyfile     = r.pem
default_md          = sha256
string_mask         = nombstr
distinguished_name  = req_distinguished_name
req_extensions      = req_ext
x509_extensions     = x509_ext

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = Guangdong
localityName                = Locality Name (eg, city)
localityName_default        = Shaoguan
organizationName            = Organization Name (eg, company)
organizationName_default    = jwj
commonName                  = Common Name (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = jwj

[ x509_ext ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints       = CA:TRUE,pathlen:3
keyUsage               = digitalSignature, keyEncipherment, keyCertSign, cRLSign

[ req_ext ]
subjectKeyIdentifier = hash
basicConstraints     = CA:TRUE
keyUsage             = digitalSignature, keyEncipherment, keyCertSign, cRLSign

参数含义:

字段 值
countryName 国家名缩写
stateOrProvinceName 州或省
localityName 地点,如城市
organizationName 组织名
commonName 商标(证书上显示的 CA 名称)

  • xxx_default 设置该字段默认值,这样等一下生成证书时就不用手动填写信息,直接回车使用默认值就行了。

生成 CA 根密钥:

openssl genrsa -out ./private/cakey.pem 2048
Generating RSA private key, 2048 bit long modulus
......................................+++
........................+++
e is 65537 (0x10001)

自签发 CA 根证书:

openssl req -new -x509 -key ./private/cakey.pem -out ./cacert.pem -days 7300 -config ./root.conf
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [CN]:
State or Province Name (full name) [GuangDong]:
Locality Name (eg, city) [ShaoGuan]:
Organization Name (eg, company) [jwj]:
Common Name (e.g. server FQDN or YOUR name) [My CA]:

将 PEM 格式证书转为常用的 DER 格式:

openssl x509 -inform PEM -in ./cacert.pem -outform DER -out ./CA.cer

用 CA 证书签发 SSL 证书
创建文件夹方便管理:

mkdir ../i0w.cn && cd ../i0w.cn

创建用户证书配置文件:

vi server.conf
[ req ]
default_bits        = 2048
default_keyfile     = r.pem
default_md          = sha256
string_mask         = nombstr
distinguished_name  = req_distinguished_name
req_extensions      = req_ext
x509_extensions     = x509_ext

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = Guangdong
localityName                = Locality Name (eg, city)
localityName_default        = Shaoguan
organizationName            = Organization Name (eg, company)
organizationName_default    = jwj
commonName                  = Common Name (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = localhost

[ x509_ext ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints       = CA:FALSE
keyUsage               = digitalSignature, keyEncipherment
subjectAltName         = @alt_names

[ req_ext ]
subjectKeyIdentifier = hash
basicConstraints     = CA:FALSE
keyUsage             = digitalSignature, keyEncipherment
subjectAltName       = @alt_names

[ alt_names ]
DNS.1   = localhost
DNS.2   = i0w.cn
DNS.3   = *.i0w.cn
IP.1    = 127.0.0.1
IP.2    = 192.168.0.111
IP.3    = 192.168.1.111

注意:

  1. 在 [ alt_names ] 下填写要签发证书的域名或 IP,支持通配符;
  2. Firefox 下出现 MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY,原因是 basicConstraints 被设置成了 CA:TRUE,改为 CA:FALSE 即可。

生成用户 RSA 密钥:

openssl genrsa -out ./server.key 2048
Generating RSA private key, 2048 bit long modulus
.......................................................................................+++
................+++
e is 65537 (0x10001)

生成用户证书请求:

openssl req -new -key ./server.key -out ./server.csr -config ./server.conf
Generating RSA private key, 2048 bit long modulus
.......................................................................................+++
................+++
e is 65537 (0x10001)
[root@huawei i0w.cn]# openssl req -new -key ./server.key -out ./server.csr -config ./server.conf
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [CN]:
State or Province Name (full name) [GuangDong]:
Locality Name (eg, city) [ShaoGuang]:
Organization Name (eg, company) [jwj]:
Common Name (e.g. server FQDN or YOUR name) [*.i0w.cn]:

签发用户证书:

openssl ca -config ../CA/openssl.cnf -in ./server.csr -out ./server.crt -days 3650 -extensions x509_ext -extfile ./server.conf
Using configuration from ../CA/openssl.cnf
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
countryName           :PRINTABLE:'CN'
stateOrProvinceName   :PRINTABLE:'GuangDong'
localityName          :PRINTABLE:'ShaoGuang'
organizationName      :PRINTABLE:'jwj'
commonName            :T61STRING:'*.i0w.cn'
Certificate is to be certified until Jun 10 01:39:26 2030 GMT (3650 days)
Sign the certificate? [y/n]:y


1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated

附上证书签发目录结构:

$ tree
.
├── demoCA
│   ├── CA.cer              # CA 证书(DER 格式)
│   ├── cacert.pem          # CA 证书(PEM 格式)
│   ├── index.txt           # 签发记录数据库
│   ├── index.txt.attr
│   ├── index.txt.old
│   ├── newcerts
│   │   └── 01.pem
│   ├── private
│   │   └── cakey.pem       # CA 私钥
│   ├── serial
│   └── serial.old
├── 2heng.xin
│   ├── 2heng.xin.crt       # 用户证书
│   ├── 2heng.xin.csr
│   └── 2heng.xin.key       # 用户证书私钥
├── root.conf               # CA 配置文件
└── server.conf             # 用户配置文件

参考来源:
https://2heng.xin/2018/12/16/your-own-ca-with-openssl/
https://blog.csdn.net/cuitone/article/details/87966042

在实际的项目开发过程中,总会遇到多套程序使用一个公众号的情况。而共用一个公众号,首先会遇到的应该是access_token问题了,两个程序互相的去获取access_token,导致被“挤下线”。
这次我遇到的情况比较简单,两套程序都是自己负责开发的,所以比较好处理,只需要修改代码,让它们共用access_token即可。

什么是access_token?

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。

为什么会被“挤下线”呢?

access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效

怎么解决被“挤下线”呢?

建议公众号开发者使用中控服务器统一获取和刷新access_token,其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务

详细想法

微信那边建议的是,使用中控服务器统一获取和刷新access_token,但要去搞一套中控程序,那就太麻烦了,还是用现有的实在点。
这里想到的是用redis来储存access_token,然后A程序从redis获取access_token
如果access_token不存在或已过期,则A程序就从微信服务器获取access_token,然后更新到redis里去。
这样,B程序去resis获取到的access_token就是最新的了,不用再去微信哪里获取,导致A程序获取到的access_token失效。
当然A跟B的顺序不是固定的,谁先发现access_token过期,就谁去更新。然后,如果刚好并发,也有极有可能会出问题(出问题再说)。

ThinkPHP配置

因为要用TP的缓存,所以要在缓存配置里增加redis配置,这样才能用redis来储存缓存内容。
首先,按照手册配置缓存配置,增加Redis的配置。

<?php
use think\facade\Env;

// +----------------------------------------------------------------------
// | 缓存设置
// +----------------------------------------------------------------------

return [
    // 默认缓存驱动
    'default' => Env::get('cache.driver', 'file'),

    // 缓存连接方式配置
    'stores'  => [
        'file' => [
            // 驱动方式
            'type'       => 'File',
            // 缓存保存目录
            'path'       => '',
            // 缓存前缀
            'prefix'     => '',
            // 缓存有效期 0表示永久缓存
            'expire'     => 0,
            // 缓存标签前缀
            'tag_prefix' => 'tag:',
            // 序列化机制 例如 ['serialize', 'unserialize']
            'serialize'  => [],
        ],
        /*** 下面这些是新加的 ***/
        // Redis缓存驱动
        'redis'   =>  [
            // 驱动方式
            'type'   => 'redis',
            // 服务器地址
            'host'       => '127.0.0.1',
        ],
        /*** 上面这些是新加的 ***/
    ],
];

EasyWeChat使用示例

虽然详细想法里写了很多,但其中很多工作都EasyWeChatThinkPHP完成了,所以代码很简单。
下面是简单的使用示例:

// 初始化EasyWeChat,app_id和secret经过脱敏,不要zhao'chao
$wechat = \EasyWeChat\Factory::officialAccount([
    'app_id'    => 'wx4202f388888',
    'secret'    => 'b06e645090bb1bd0aefc6588888',
]);

// 就这么简单的一句,就可以使用Redis储存AccessToken了
$wechat->access_token->setCache(\think\facade\Cache::store('redis'));

// 发模板消息
$wechat->template_message->send([
    'touser'      => 'dsadasdasdas',
    'template_id' => 'vN_nAl6UiLbCnCT_-lwPoGgaMZUXvY0G72Rr3C-5k6o',
    'url'         => 'https://blog.ll00.cn',
    'data'        => [
        // 头部
        'first'    => '您有新的待审批通行证,请尽快处理!',
        // 申请内容
        'keyword1' => '钞票',
        // 预约时间
        'keyword2' => '2020年6月6日',
        // 申请人
        'keyword3' => '西虹市首富',
        // 申请时间
        'keyword4' => '2020年5月20日 13时14分',
        // 底部
        'remark'   => '王多鱼爱夏竹冠名代码',
    ],
]);