分类 PHP 下的文章

在实际的项目开发过程中,总会遇到多套程序使用一个公众号的情况。而共用一个公众号,首先会遇到的应该是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'   => '王多鱼爱夏竹冠名代码',
    ],
]);

TP框架的column数据库查询方法是一个非常方便的快捷查询方法,可以用该方法快速的返回结果集中的列,并且可以指定字段作为数据集的数组下标。但是在使用SQL函数后,却异常的返回了索引数组,而不是想要的关联数组。

我想查询本月每天的总营业额,所以使用了以下的方法查询

Db::table('ledger')->where('create_time', 'between', ['2020-01-01', '2020-01-31'])->group('DATE_FORMAT(ledger_date, "%m-%d")')->column('SUM(amount)', 'DATE_FORMAT(ledger_date, "%m-%d")')

我理想中的结果应该是

[
    [01-01] => 100.00,
    [01-02] => 200.00,
    [01-03] => 210.00,
    ...
]

但结果却是

[
    0 => 100.00,
    1 => 200.00,
    2 => 210.00,
    ...
]

这不对劲啊,怎么返回了索引数组,不是关联数组?
然后就看代码,断点调试,终于发现了问题,竟然是一个小小的空格导致的!!!
原来,我使用了SQL函数

DATE_FORMAT(ledger_date, "%m-%d")

然后TP框架在处理SQL列名称的时候,会以,分割列名称,然后使用trim来去除空格

$field = array_map('trim', explode(',', $field));

所以实际去查询的时候,列名称,两边的空格都被去除了,变成了

SELECT DATE_FORMAT(ledger_date,"%m-%d"),SUM(amount) FROM `ledger` ....

但是索引的字段名没变,所以最终

DATE_FORMAT(ledger_date, "%m-%d")

不等于

DATE_FORMAT(ledger_date,"%m-%d")

导致没能生成关联数组

最终的解决方法是,我们使用之前就把逗号两边的空格都去掉,就能得到自己想要的了。

Db::table('ledger')->where('create_time', 'between', ['2020-01-01', '2020-01-31'])->group('DATE_FORMAT(ledger_date,"%m-%d")')->column('SUM(amount)', 'DATE_FORMAT(ledger_date,"%m-%d")')

最近刚好用上模型事件,但手册上对事件的触发条件却没有详细的进行说明。那么,就只能自己进行测试了。

模型事件

首先,从手册上,我们可以知道模型支持以下事件:

事件描述事件方法名
after_read查询后onAfterRead
before_insert新增前onBeforeInsert
after_insert新增后onAfterInsert
before_update更新前onBeforeUpdate
after_update更新后onAfterUpdate
before_write写入前onBeforeWrite
after_write写入后onAfterWrite
before_delete删除前onBeforeDelete
after_delete删除后onAfterDelete
before_restore恢复前onBeforeRestore
after_restore恢复后onAfterRestore

建立模型

为了了解每个事件的触发条件,我们先建立以下模型

模型代码app/model/Users.php

<?php
namespace app\model;

use think\Model;
use think\model\concern\SoftDelete;

class Users extends Model
{
    // 软删除
    use SoftDelete;

    public static function onAfterRead($user) {
        dump('查询后');
    }

    public static function onBeforeInsert($user) {
        dump('新增前');
    }

    public static function onAfterInsert($user) {
        dump('新增后');
    }

    public static function onBeforeUpdate($user) {
        dump('更新前');
    }

    public static function onAfterUpdate($user) {
        dump('更新后');
    }

    public static function onBeforeWrite($user) {
        dump('写入前');
    }

    public static function onAfterWrite($user) {
        dump('写入后');
    }

    public static function onBeforeDelete($user) {
        dump('删除前');
    }

    public static function onAfterDelete($user) {
        dump('删除后');
    }

    public static function onBeforeRestore($user) {
        dump('恢复前');
    }

    public static function onAfterRestore($user) {
        dump('恢复后');
    }
}

数据表

CREATE TABLE `users` (
 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
 `name` varchar(32) NOT NULL COMMENT '姓名',
 `area` varchar(32) NOT NULL COMMENT '区域',
 `address` varchar(64) NOT NULL COMMENT '地址',
 `balance` decimal(9,2) NOT NULL COMMENT '余额',
 `password` varchar(255) NOT NULL COMMENT '密码',
 `status` int(11) NOT NULL COMMENT '状态',
 `last` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后活跃时间',
 `phone` varchar(11) NOT NULL COMMENT '手机号',
 `delete_time` int(10) unsigned DEFAULT NULL COMMENT '删除时间',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'

测试代码

代码

然后通过以下代码去操作数据库

use \app\model\Users;
$data = [
    'name' => 'test',
    'phone' => '13888888888',
    'area' => '0',
    'address' => '广东省韶关市',
    'balance' => '0',
    'password' => '0',
    'status' => '0',
    'last' => '2019-01-01 00:00:00',
];

dump('Users::create($data)');
$users = Users::create($data);

dump('Users::insert($data)');
Users::insert($data);

dump('Users::where("id", $users["id"])->update(["area" => 1])');
Users::where("id", $users["id"])->update(["area" => 1]);

dump('Users::update(["area" => 1], ["id" => $users["id"]])');
Users::update(["area" => 1], ["id" => $users["id"]]);

dump('$users->save(["area" => 2])');
$users->save(["area" => 2]);

dump('Users::where("id", $users["id"])->delete()');
Users::where("id", $users["id"])->delete();

dump('Users::where("id", ">", 0)->find()');
$users = Users::where("id", ">", 0)->find();

dump('Users::destroy($users["id"])');
Users::destroy($users["id"]);

dump('$users->restore()');
$users->restore();

dump('$users->delete()');
$users->delete();

执行结果

执行之后,返回以下结果

^ "Users::create($data)"
^ "写入前"
^ "新增前"
^ "新增后"
^ "写入后"
^ "Users::insert($data)"
^ "Users::where("id", $users["id"])->update(["area" => 1])"
^ "Users::update(["area" => 1], ["id" => $users["id"]])"
^ "写入前"
^ "更新前"
^ "更新后"
^ "写入后"
^ "$users->save(["area" => 2])"
^ "写入前"
^ "更新前"
^ "更新后"
^ "写入后"
^ "Users::where("id", $users["id"])->delete()"
^ "Users::where("id", ">", 0)->find()"
^ "查询后"
^ "Users::destroy($users["id"])"
^ "查询后"
^ "删除前"
^ "删除后"
^ "$users->restore()"
^ "恢复前"
^ "恢复后"
^ "$users->delete()"
^ "删除前"
^ "删除后"

总结

方法查询后新增前新增后更新前更新后写入前写入后删除前删除后恢复前恢复后
create()
insert()
update()
save()
delete()
find()
destroy()
restore()

create()

模型创建数据方法,会触发写入前新增前新增后写入后。使用模型的save()saveAll()来新增方法也会触发这几个事件。

insert()

insert()是Db类的方法,不是模型方法,不会触发模型事件。

update()

update()是Db类的方法,不是模型方法,不会触发模型事件。

如果是模型静态调用update(),则执行的是模型的update方法,而模型的update方法会调用save()方法,所以跟模型的save()方法一样,会触发写入前更新前更新后写入后事件
感谢 @dejavu 的提醒

save()

使用模型的save()方法来更新数据,会触发写入前更新前更新后写入后事件。

delete()

如果是使用模型方法查询出来数据,然后再删除数据,则会触发删除前删除后事件。
如果是直接使用条件删除,则不会触发模型事件。因为直接使用条件删除,这时候的delete()方法不是模型方法。

find()

该查询方法会触发查询后事件

destroy()

该删除数据方法会触发查询后删除前删除后。所以,该方法是先查询出数据,然后再删除该数据。

restore()

该软删除恢复方法会触发恢复前恢复后方法

在接触一些thinkphp新手时,发现总是有一部分人不会使用composer来安装扩展包。于是他们就按照tp3的方式来下载扩展包的压缩包,然后将扩展包解压到项目里面去,结果最后发现用不了,提示类不存在Class 'EasyWeChat\Factory not found`。这里主要下,如何在thinkphp的项目里使用composer来安装扩展包,助力下这部分"迷途的人"。

安装composer

安装composer的方法网上已经很多了,所以这里就不重复去说了。但是要注意电脑里的php版本不要太低,建议使用php7.2
参考方法:https://www.runoob.com/w3cnote/composer-install-and-usage.html

使用composer安装扩展包

现今的9102年,大多数的php扩展包都支持使用composer来进行安装,所以会composer的使用已经算是一项非常必要的技能了,就跟学会复制黏贴一样重要。
下面就以安装PHPMailer为例。

1.获取composer安装命令

打开PHPMailerGitHub,在它的文档里能看到一条composer的命令,一般在支持composer安装的扩展包文档里都会包含这个命令,命令以composer require开头,后面跟着扩展包的“名称”。
命令:

composer require phpmailer/phpmailer

2.打开命令行,并切换到项目目录

首先,这里假设我们的项目放在了E:/wwwroot/www.ll00.cn,打开这个目录能看到config extend public route runtime vendor等目录。
然后打开命令行,输入E:切换到E盘,再输入cd E:/wwwroot/www.ll00.cn切换到项目目录

不要将运行目录切换到public或者vender,我看很多人都犯这样的错误
E:
cd E:/wwwroot/www.ll00.cn

3.执行composer安装命令

安装命令我们已经在第一步获取到了,并且命令行也将运行目录切换到了项目目录里,这时候就可以执行composer命令来安装扩展包了

composer require phpmailer/phpmailer

到这里,如无意外,扩展包就安装好了

使用扩展包

以下是在项目里使用PHPMailer的示例代码

<?php
// 导入 PHPMailer 类到当前命名空间
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

// 实例化PHPMailer
$mail = new PHPMailer(true);

try {
    //Server settings
    $mail->SMTPDebug = SMTP::DEBUG_SERVER;                      // Enable verbose debug output
    $mail->isSMTP();                                            // Send using SMTP
    $mail->Host       = 'smtp1.example.com';                    // Set the SMTP server to send through
    $mail->SMTPAuth   = true;                                   // Enable SMTP authentication
    $mail->Username   = 'user@example.com';                     // SMTP username
    $mail->Password   = 'secret';                               // SMTP password
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;         // Enable TLS encryption; `PHPMailer::ENCRYPTION_SMTPS` also accepted
    $mail->Port       = 587;                                    // TCP port to connect to

    //Recipients
    $mail->setFrom('from@example.com', 'Mailer');
    $mail->addAddress('joe@example.net', 'Joe User');     // Add a recipient
    $mail->addAddress('ellen@example.com');               // Name is optional
    $mail->addReplyTo('info@example.com', 'Information');
    $mail->addCC('cc@example.com');
    $mail->addBCC('bcc@example.com');

    // Content
    $mail->isHTML(true);                                  // Set email format to HTML
    $mail->Subject = 'Here is the subject';
    $mail->Body    = 'This is the HTML message body <b>in bold!</b>';
    $mail->AltBody = 'This is the body in plain text for non-HTML mail clients';

    $mail->send();
    echo 'Message has been sent';
} catch (Exception $e) {
    echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

TP6的文件上传相较于之前的版本有些变化,用法变了,也相对的更灵活了。下面是文件上传的使用示例。
还算说得挺清楚的了,如果还有什么疑问,可以在评论区留言。

前端代码

<!-- 请注意换一下action的提交地址,这里使用了URL生成的助手函数,参考https://www.kancloud.cn/manual/thinkphp6_0/1037508 -->
<form action="{:url('Upload/index')}" method="post" enctype="multipart/form-data">
    <!-- 文件选择按钮 -->
    <input type="file" name="file">
    <!-- 表单提交按钮 -->
    <button type="submit">提交</button>
</form>

配置文件

路径:/config/filesystem.php

<?php

use think\facade\Env;

return [
    // 默认磁盘
    'default' => Env::get('filesystem.driver', 'local'),
    // 磁盘列表
    'disks'   => [
        'local'  => [
            'type' => 'local',
            'root' => app()->getRuntimePath() . 'storage',
        ],
        'public' => [
            // 磁盘类型
            'type'       => 'local',
            // 磁盘路径
            'root'       => app()->getRootPath() . 'public/storage',
            // 磁盘路径对应的外部URL路径
            'url'        => '/storage',
            // 可见性:public=公共,private=私有
            'visibility' => 'public',
        ],
        // 更多的磁盘配置信息
    ],
];

上传处理代码

// 上传文件错误或者文件验证不通过时,都会抛出异常,所以要使用try来捕捉异常
try {
    // 获取上传的文件,如果有上传错误,会抛出异常
    $file = \think\facade\Request::file('file');
    // 如果上传的文件为null,手动抛出一个异常,统一处理异常
    if (null === $file) {
        // 异常代码使用UPLOAD_ERR_NO_FILE常量,方便需要进一步处理异常时使用
        throw new \Exception('请上传文件', UPLOAD_ERR_NO_FILE);
    }

    // 使用验证器验证上传的文件
    validate(['file' => [
        // 限制文件大小(单位b),这里限制为4M
        'fileSize' => 4 * 1024 * 1024,
        // 限制文件后缀,多个后缀以英文逗号分割
        'fileExt'  => 'gif,jpg,png'
        // 更多规则请看“上传验证”的规则,文档地址https://www.kancloud.cn/manual/thinkphp6_0/1037629#_444
    ]])->check(['file' => $file]);

    // 保存路径,实际保存路径为“磁盘路径” + “avatar”
    $path = 'avatar';
    // 文件名规则,默认是当前时间。可以使用哈希算法,如:md5/sha1等,还可以传入匿名函数,详细可以看后面
    $rule = 'md5';
    // 将文件保存public磁盘,文件名为$rule指定的规则。然后将文件路径赋值给$path
    $path = \think\facade\Filesystem::disk('public')->putFile($path, $file, $rule);
    // 拼接URL路径
    $url = \think\facade\Filesystem::getDiskConfig('public', 'url') . '/' . str_replace('\\', '/', $path);
} catch (\Exception $e) {
    // 如果上传时有异常,会执行这里的代码,可以在这里处理异常
    return json([
        'code' => 1,
        'msg'  => $e->getMessage(),
    ]);
}

$info = [
    // 文件路径:avatar/a4/e7b9e4ce42e2097b0df2feb8832d28.jpg
    'path' => $path,
    // URL路径:/storage/avatar/a4/e7b9e4ce42e2097b0df2feb8832d28.jpg
    'url'  => $url,
    // 文件大小(字节)
    'size' => $file->getSize(),
    // 文件名:读书顶个鸟用.jpg
    'name' => $file->getFilename(),
    // 文件MINE:image/jpeg
    'mime' => $file->getMime(),
];
halt($info);

文件名规则

文件名规则支持传入匿名函数、哈希算法和函数名。默认情况下,使用时间来自动生成。

默认算法

默认情况下,文件名是这样生成的

date('Ymd') . DIRECTORY_SEPARATOR . md5((string) microtime(true))

结果是

/storage/avatar/20200117/2801a4c6c49a1e411f58abfa9b4a8f52.jpg

匿名函数

/**
 * @param \think\file\UploadedFile $file 文件
 * @return string 文件名
 */
$rule = function(\think\file\UploadedFile $file) {
    // 获取获取上传文件类型信息:image/jpeg
    $file->getOriginalMime();
    // 获取上传文件名:读书顶个鸟用.jpg
    $file->getOriginalName();
    // 获取文件扩展名:jpg
    $file->extension();
    // 获取文件的哈希散列值
    $file->hash();

    return $file->hash('md5');
};
$path = \think\facade\Filesystem::disk('public')->putFile($path, $file, $rule);

结果是

/storage/avatar/a4e7b9e4ce42e2097b0df2feb8832d28.jpg

哈希算法

支持传入hash_algos()里支持的算法,如md5,sha1,sha256

$rule = 'sha1';
$path = \think\facade\Filesystem::disk('public')->putFile($path, $file, $rule);

结果是

/storage/avatar/ef/c6e7f357b7f97cb7ccfa7c5cfe83bf9819f88f.jpg

使用哈希算法会自动取文件哈希值前两个字符作为目录名

函数名

这里传入time,还可以传入自定义函数名或php内置函数名。但只支持可不传参数的函数

$rule = 'time';
$path = \think\facade\Filesystem::disk('public')->putFile($path, $file, $rule);

结果是

/storage/avatar/1579228212.jpg