Thinkphp 对接阿里云短信 生成二维码

默认分类 PHP thinkphp 二维码 Aliyun 短信 QrCode

记录一下.

对接阿里短信

参考与魔改的官方dome.

定义一个抽象类

文件路径app/lib/sms/SmsAbstract.php

<?php
namespace app\lib\sms;
abstract class SmsAbstract
{
    abstract public function sendVerifyCode($code, $phone);
}

定义阿里云驱动类(主要)

文件路径app/lib/sms/driver/Aliyun.php
这里继承上面的抽象类,此时必须实现sendVerifyCode方法

<?php
namespace app\lib\sms\driver;

use app\lib\sms\SmsAbstract;
use Exception;
use stdClass;
use think\facade\Env;

class Aliyun extends SmsAbstract
{
    public function sendVerifyCode($code, $phone)
    {
        $params = [];

        // 必填:是否启用https
        $security = false;

        // 必填
        $accessKeyId = Env::get('SMS_ALIYUN_ACCESSKEY_ID');
        $accessKeySecret = Env::get('SMS_ALIYUN_ACCESSKEY_SECRET');

        // 必填: 短信接收号码
        $params["PhoneNumbers"] = $phone;

        // 必填: 短信签名
        $params["SignName"] = Env::get('SMS_ALIYUN_SIGNNAME');

        // 必填: 短信模板Code
        $params["TemplateCode"] = Env::get('SMS_ALIYUN_TEMPLATECODE');

        // 可选: 设置模板参数, 假如模板中存在变量需要替换则为必填项
        $params['TemplateParam'] = [
            "code" => $code
        ];

        // *** 需用户填写部分结束, 以下代码若无必要无需更改 ***
        if (!empty($params["TemplateParam"]) && is_array($params["TemplateParam"])) {
            $params["TemplateParam"] = json_encode($params["TemplateParam"], JSON_UNESCAPED_UNICODE);
        }

        $params = array_merge($params, ["RegionId" => "cn-hangzhou", "Action" => "SendSms", "Version" => "2017-05-25"]);
        // 此处可能会抛出异常,注意catch
        return $this->request($accessKeyId, $accessKeySecret, "dysmsapi.aliyuncs.com", $params, $security);
    }

    /**
     * 生成签名并发起请求
     *
     * @param $accessKeyId string AccessKeyId (https://ak-console.aliyun.com/)
     * @param $accessKeySecret string AccessKeySecret
     * @param $domain string API接口所在域名
     * @param $params array API具体参数
     * @param $security boolean 使用https
     * @param string $method boolean 使用GET或POST方法请求,VPC仅支持POST
     * @return bool|stdClass 返回API接口调用结果,当发生错误时返回false
     */
    public function request(string $accessKeyId, string $accessKeySecret, string $domain, array $params, $security = false, $method = 'POST')
    {
        $apiParams = array_merge([
            "SignatureMethod" => "HMAC-SHA1",
            "SignatureNonce" => uniqid(mt_rand(0, 0xffff), true),
            "SignatureVersion" => "1.0",
            "AccessKeyId" => $accessKeyId,
            "Timestamp" => gmdate("Y-m-d\TH:i:s\Z"),
            "Format" => "JSON",
        ], $params);
        ksort($apiParams);

        $sortedQueryStringTmp = "";
        foreach ($apiParams as $key => $value) {
            $sortedQueryStringTmp .= "&" . $this->encode($key) . "=" . $this->encode($value);
        }

        $stringToSign = "${method}&%2F&" . $this->encode(substr($sortedQueryStringTmp, 1));

        $sign = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&", true));

        $signature = $this->encode($sign);

        $url = ($security ? 'https' : 'http') . "://{$domain}/";

        try {
            $content = $this->fetchContent($url, $method, "Signature={$signature}{$sortedQueryStringTmp}");
            return json_decode($content);
        } catch (Exception $e) {
            return false;
        }
    }

    private function encode($str)
    {
        $res = urlencode($str);
        $res = preg_replace("/\+/", "%20", $res);
        $res = preg_replace("/\*/", "%2A", $res);
        $res = preg_replace("/%7E/", "~", $res);
        return $res;
    }

    private function fetchContent($url, $method, $body)
    {
        $ch = curl_init();

        if ($method == 'POST') {
            curl_setopt($ch, CURLOPT_POST, 1);//post提交方式
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        } else {
            $url .= '?' . $body;
        }

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array(
            "x-sdk-client" => "php/2.0.0"
        ));

        if (substr($url, 0, 5) == 'https') {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        }

        $rtn = curl_exec($ch);

        if ($rtn === false) {
            // 大多由设置等原因引起,一般无法保障后续逻辑正常执行,
            // 所以这里触发的是E_USER_ERROR,会终止脚本执行,无法被try...catch捕获,需要用户排查环境、网络等故障
            trigger_error("[CURL_" . curl_errno($ch) . "]: " . curl_error($ch), E_USER_ERROR);
        }
        curl_close($ch);

        return $rtn;
    }
}

定义工厂模式类

文件路径app/lib/Sms.php

<?php
namespace app\lib;

use app\lib\sms\SmsAbstract;
use think\Container;
use think\facade\Env;

class Sms
{
    public static function sendCode($code, $phone, $driver = '')
    {
        //获取驱动名字.此处.env里面定义的是ALIYUN.
        $driver = $driver ?: Env::get('SMS_DRIVER');
        //此处拼接完整的驱动类名,以后扩展别的短信平台,只需要再写一个驱动类,然后修改一下.env里面定义的驱动,或者直接使用这个方法的时候通过$driver变量指定
        $class = '\\app\\lib\\sms\\driver\\' . ucfirst(strtolower($driver));
        //此处把类放入使用Thinkphp的容器,相当于单例模式,没有的话会new一个然后保存,有的话直接返回出来.
        $obj = Container::getInstance()->make($class);
        //这里判断是否是抽象类的子类
        if ($obj instanceof SmsAbstract) {
            //抽象类的子类必须实现sendVerifyCode方法,此处可放心调用
            return $obj->sendVerifyCode($code, $phone);
        }
        return false;
    }

    public static function getCode($length = 6): string
    {
        $code = '';
        $chars = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
        while (strlen($code) < $length) {
            shuffle($chars);
            $code .= current($chars);
        }
        return $code;
    }
}

使用方法

<?php
namespace app\controller;

use app\lib\Sms;
use think\facade\Request;
use think\facade\Cache;

class Api
{
    public function send_sms()
    {
        $phone = Request::post('phone');

        if (!preg_match("/^1[3456789]\d{9}$/", $phone)) {
            return $this->error('非法手机号');
        }

        if (!empty(Cache::get('login' . $phone))) {
            return $this->error('请勿频繁请求');
        }

        //生成验证码
        $code = Sms::getCode();
        //发送短信
        Sms::sendCode($code, $phone);

        //验证码存入缓存10分钟
        Cache::set($phone, $code, 600);
        //缓存60秒,防止频繁请求API发送短信
        Cache::set('login' . $phone, 'send sms', 60);

        return $this->success('验证码发送成功');
    }
}

Composer 加载endroid/qr-code 生成二维码

毕竟Composer更优雅一些(相对于单文件生成直接exit结束后续操作)
很简单的东西,但是百度了一圈,好多过时错了,这里记一下

composer require endroid/qr-code

Builder是这个类Endroid\QrCode\Builder\Builder;

use Endroid\QrCode\Builder\Builder;

        //获取数据,这里如果是网址的话,get请求不能直接传,所以解码一下
        $data = Request::get('data', '', 'urldecode');
        if (empty($data)) {
            return Json::error('缺少必要参数{data}');
        }

        //此处返回的是data:image/png;base64,iVBORw0KGgoAAA...这种格式的字符串,前端src直接引用也能显示图片,并不是直接的文件
        //在此链式操作中最后一步是获取生成的数据,有4个方法getDataUri(),getString()直接成文件流设置一下header为图片就能直接返回图片资源,saveToFile()顾名思义应该是保存生成的二维码为图片,getMimeType()获取生成图片的MimeType比如'"image/png"'
        $src = Builder::create()->size(200)->data($data)->build()->getDataUri();
        return Json::success('', ['src' => $src]);

详细别的操作可访问https://github.com/endroid/qr-code

新评论

称呼不能为空
邮箱格式不合法
网站格式不合法
内容不能为空