From 4dd274a1abf75f553986ebe5200243cb05c2bd54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E8=BF=9B=E6=98=A5?= Date: Mon, 5 Dec 2022 15:48:49 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AD=97=E8=8A=82?= =?UTF-8?q?=E8=B7=B3=E5=8A=A8=E7=81=AB=E5=B1=B1=E5=BC=95=E6=93=8E=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E6=9C=8D=E5=8A=A1=E6=94=AF=E6=8C=81=20(#335)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 38 +++ src/Gateways/VolcengineGateway.php | 311 +++++++++++++++++++++++ tests/Gateways/VolcengineGatewayTest.php | 105 ++++++++ 3 files changed, 454 insertions(+) create mode 100644 src/Gateways/VolcengineGateway.php create mode 100644 tests/Gateways/VolcengineGatewayTest.php diff --git a/README.md b/README.md index 33c8eb2..cbe7a61 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ - [蜘蛛云](https://zzyun.com/) - [融合云信](https://maap.wo.cn/) - [天瑞云](http://cms.tinree.com/) +- [火山引擎](https://console.volcengine.com/sms/) ## 环境需求 @@ -884,6 +885,43 @@ $easySms->send(18888888888, [ ]); ``` +### [火山引擎](https://console.volcengine.com/sms/) + +短信内容使用 `template` + `data` + +```php + 'volcengine' => [ + 'access_key_id' => '', // 平台分配给用户的access_key_id + 'access_key_secret'=>'', // 平台分配给用户的access_key_secret + 'sign_name' => '', // 平台上申请的接口短信签名或者签名ID,可不填,发送短信时data中指定 + 'sms_account' => '', // 消息组帐号,火山短信页面右上角,短信应用括号中的字符串,可不填,发送短信时data中指定 + ], +``` + +发送示例1: + +```php +$easySms->send(18888888888, [ + 'template' => 'SMS_123456', // 模板ID + 'data' => [ + "code" => 1234 // 模板变量 + ], +]); +``` + +发送示例2: +```php +$easySms->send(18888888888, [ + 'template' => 'SMS_123456', // 模板ID + 'data' => [ + "template_param" => ["code" => 1234], // 模板变量参数 + "sign_name" => "yoursignname", // 签名,覆盖配置文件中的sign_name + "sms_account" => "yoursmsaccount", // 消息组帐号,覆盖配置文件中的sms_account + "phone_numbers" => "18888888888,18888888889", // 手机号,批量发送,英文的逗号连接多个手机号,覆盖发送方法中的填入的手机号 + ], +]); +``` + ## :heart: 支持我 [![Sponsor me](https://github.com/overtrue/overtrue/blob/master/sponsor-me.svg?raw=true)](https://github.com/sponsors/overtrue) diff --git a/src/Gateways/VolcengineGateway.php b/src/Gateways/VolcengineGateway.php new file mode 100644 index 0000000..536e7c5 --- /dev/null +++ b/src/Gateways/VolcengineGateway.php @@ -0,0 +1,311 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\EasySms\Gateways; + +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Psr7\Query; +use GuzzleHttp\Psr7\Utils; +use GuzzleHttp\Psr7; +use Overtrue\EasySms\Contracts\MessageInterface; +use Overtrue\EasySms\Contracts\PhoneNumberInterface; +use Overtrue\EasySms\Exceptions\GatewayErrorException; +use Overtrue\EasySms\Support\Config; +use Overtrue\EasySms\Traits\HasHttpRequest; +use Psr\Http\Message\RequestInterface; + +/** + * Class VolcengineGateway. + * + * @see https://www.volcengine.com/docs/6361/66704 + */ +class VolcengineGateway extends Gateway +{ + use HasHttpRequest; + + const ENDPOINT_ACTION = 'SendSms'; + const ENDPOINT_VERSION = '2020-01-01'; + const ENDPOINT_CONTENT_TYPE = 'application/json; charset=utf-8'; + const ENDPOINT_ACCEPT = 'application/json'; + const ENDPOINT_USER_AGENT = 'overtrue/easy-sms'; + const ENDPOINT_SERVICE = 'volcSMS'; + + const Algorithm = 'HMAC-SHA256'; + + const ENDPOINT_DEFAULT_REGION_ID = 'cn-north-1'; + + public static $endpoints = [ + 'cn-north-1' => 'https://sms.volcengineapi.com', + 'ap-singapore-1' => 'https://sms.byteplusapi.com', + ]; + + private $regionId = self::ENDPOINT_DEFAULT_REGION_ID; + protected $requestDate; + + + public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) + { + $data = $message->getData($this); + $signName = !empty($data['sign_name']) ? $data['sign_name'] : $config->get('sign_name'); + $smsAccount = !empty($data['sms_account']) ? $data['sms_account'] : $config->get('sms_account'); + $templateId = $message->getTemplate($this); + $phoneNumbers = !empty($data['phone_numbers']) ? $data['phone_numbers'] : $to->getNumber(); + $templateParam = !empty($data['template_param']) ? $data['template_param'] : $message->getData($this); + + $tag = !empty($data['tag']) ? $data['tag'] : ''; + + $payload = [ + 'SmsAccount' => $smsAccount, // 消息组帐号,火山短信页面右上角,短信应用括号中的字符串 + 'Sign' => $signName, // 短信签名 + 'TemplateID' => $templateId, // 短信模板ID + 'TemplateParam' => json_encode($templateParam), // 短信模板占位符要替换的值 + 'PhoneNumbers' => $phoneNumbers, // 手机号,如果有多个使用英文逗号分割 + ]; + if ($tag) { + $payload['Tag'] = $tag; + } + $queries = [ + 'Action' => self::ENDPOINT_ACTION, + 'Version' => self::ENDPOINT_VERSION, + ]; + + try { + $stack = HandlerStack::create(); + $stack->push($this->signHandle()); + $this->setGuzzleOptions([ + 'headers' => [ + 'Content-Type' => self::ENDPOINT_CONTENT_TYPE, + 'Accept' => self::ENDPOINT_ACCEPT, + 'User-Agent' => self::ENDPOINT_USER_AGENT + ], + 'timeout' => $this->getTimeout(), + 'handler' => $stack, + 'base_uri' => $this->getEndpoint(), + ]); + + $response = $this->request('post', $this->getEndpoint().$this->getCanonicalURI(), [ + 'query' => $queries, + 'json' => $payload, + ]); + if ($response instanceof Psr7\Response) { + $response = json_decode($response->getBody()->getContents(), true); + } + if (isset($response['ResponseMetadata']['Error'])) { // 请求错误参数,如果请求没有错误,则不存在该参数返回 + // 火山引擎错误码格式为:'ZJ'+ 5位数字,比如 ZJ20009,取出数字部分 + preg_match('/\d+/', $response['ResponseMetadata']['Error']['Code'], $matches); + throw new GatewayErrorException($response['ResponseMetadata']['Error']['Code'].":".$response['ResponseMetadata']['Error']['Message'], $matches[0], $response); + } + return $response; + } catch (ClientException $exception) { + $responseContent = $exception->getResponse()->getBody()->getContents(); + $response = json_decode($responseContent, true); + if (isset($response['ResponseMetadata']['Error']) && $error = $response['ResponseMetadata']['Error']) { // 请求错误参数,如果请求没有错误,则不存在该参数返回 + // 火山引擎公共错误码Error与业务错误码略有不同,比如:"Error":{"CodeN":100004,"Code":"MissingRequestInfo","Message":"The request is missing timestamp information."} + // 此处错误码直接取 CodeN + throw new GatewayErrorException($error["CodeN"].":".$error['Message'], $error["CodeN"], $response); + } + throw new GatewayErrorException($responseContent, $exception->getCode(), ['content' => $responseContent]); + } + } + + protected function signHandle() + { + return function (callable $handler) { + return function (RequestInterface $request, array $options) use ($handler) { + $request = $request->withHeader('X-Date', $this->getRequestDate()); + list($canonicalHeaders, $signedHeaders) = $this->getCanonicalHeaders($request); + + $queries = Query::parse($request->getUri()->getQuery()); + $canonicalRequest = $request->getMethod()."\n" + .$this->getCanonicalURI()."\n" + .$this->getCanonicalQueryString($queries)."\n" + .$canonicalHeaders."\n" + .$signedHeaders."\n" + .$this->getPayloadHash($request); + + $stringToSign = $this->getStringToSign($canonicalRequest); + + $signingKey = $this->getSigningKey(); + + $signature = hash_hmac('sha256', $stringToSign, $signingKey); + $parsed = $this->parseRequest($request); + + $parsed['headers']['Authorization'] = self::Algorithm. + ' Credential='.$this->getAccessKeyId().'/'.$this->getCredentialScope().', SignedHeaders='.$signedHeaders.', Signature='.$signature; + + $buildRequest = function () use ($request, $parsed) { + if ($parsed['query']) { + $parsed['uri'] = $parsed['uri']->withQuery(Query::build($parsed['query'])); + } + + return new Psr7\Request( + $parsed['method'], + $parsed['uri'], + $parsed['headers'], + $parsed['body'], + $parsed['version'] + ); + }; + + return $handler($buildRequest(), $options); + }; + }; + } + + private function parseRequest(RequestInterface $request) + { + $uri = $request->getUri(); + return [ + 'method' => $request->getMethod(), + 'path' => $uri->getPath(), + 'query' => Query::parse($uri->getQuery()), + 'uri' => $uri, + 'headers' => $request->getHeaders(), + 'body' => $request->getBody(), + 'version' => $request->getProtocolVersion() + ]; + } + + public function getPayloadHash(RequestInterface $request) + { + if ($request->hasHeader('X-Content-Sha256')) { + return $request->getHeaderLine('X-Content-Sha256'); + } + + return Utils::hash($request->getBody(), 'sha256'); + } + + public function getRegionId() + { + return $this->config->get('region_id', self::ENDPOINT_DEFAULT_REGION_ID); + } + + public function getEndpoint() + { + $regionId = $this->getRegionId(); + if (!in_array($regionId, array_keys(self::$endpoints))) { + $regionId = self::ENDPOINT_DEFAULT_REGION_ID; + } + return static::$endpoints[$regionId]; + } + + public function getRequestDate() + { + return $this->requestDate ?: gmdate('Ymd\THis\Z'); + } + + + /** + * 指代信任状,格式为:YYYYMMDD/region/service/request + * @return string + */ + public function getCredentialScope() + { + return date('Ymd', strtotime($this->getRequestDate())).'/'.$this->getRegionId().'/'.self::ENDPOINT_SERVICE.'/request'; + } + + /** + * 计算签名密钥 + * 在计算签名前,首先从私有访问密钥(Secret Access Key)派生出签名密钥(signing key),而不是直接使用私有访问密钥。具体计算过程如下: + * kSecret = *Your Secret Access Key* + * kDate = HMAC(kSecret, Date) + * kRegion = HMAC(kDate, Region) + * kService = HMAC(kRegion, Service) + * kSigning = HMAC(kService, "request") + * 其中Date精确到日,与RequestDate中YYYYMMDD部分相同。 + * @return string + */ + protected function getSigningKey() + { + $dateKey = hash_hmac('sha256', date("Ymd", strtotime($this->getRequestDate())), $this->getAccessKeySecret(), true); + $regionKey = hash_hmac('sha256', $this->getRegionId(), $dateKey, true); + $serviceKey = hash_hmac('sha256', self::ENDPOINT_SERVICE, $regionKey, true); + return hash_hmac('sha256', 'request', $serviceKey, true); + } + + /** + * 创建签名字符串 + * 签名字符串主要包含请求以及正规化请求的元数据信息,由签名算法、请求日期、信任状和正规化请求哈希值连接组成,伪代码如下: + * StringToSign = Algorithm + '\n' + RequestDate + '\n' + CredentialScope + '\n' + HexEncode(Hash(CanonicalRequest)) + * @return string + */ + public function getStringToSign($canonicalRequest) + { + return self::Algorithm."\n".$this->getRequestDate()."\n".$this->getCredentialScope()."\n".hash('sha256', $canonicalRequest); + } + + /** + * @return string + */ + public function getAccessKeySecret() + { + return $this->config->get('access_key_secret'); + } + + /** + * @return string + */ + public function getAccessKeyId() + { + return $this->config->get('access_key_id'); + } + + /** + * 指代正规化后的Header。 + * 其中伪代码如下: + * CanonicalHeaders = CanonicalHeadersEntry0 + CanonicalHeadersEntry1 + ... + CanonicalHeadersEntryN + * 其中CanonicalHeadersEntry = Lowercase(HeaderName) + ':' + Trimall(HeaderValue) + '\n' + * Lowcase代表将Header的名称全部转化成小写,Trimall表示去掉Header的值的前后多余的空格。 + * 特别注意:最后需要添加"\n"的换行符,header的顺序是以headerName的小写后ascii排序。 + * @return array + */ + public function getCanonicalHeaders(RequestInterface $request) + { + $headers = $request->getHeaders(); + ksort($headers); + $canonicalHeaders = ''; + $signedHeaders = []; + foreach ($headers as $key => $val) { + $lowerKey = strtolower($key); + $canonicalHeaders .= $lowerKey.':'.trim($val[0]).PHP_EOL; + $signedHeaders[] = $lowerKey; + } + $signedHeadersString = implode(';', $signedHeaders); + return [$canonicalHeaders, $signedHeadersString]; + } + + /** + * urlencode(注:同RFC3986方法)每一个querystring参数名称和参数值。 + * 按照ASCII字节顺序对参数名称严格排序,相同参数名的不同参数值需保持请求的原始顺序。 + * 将排序好的参数名称和参数值用=连接,按照排序结果将“参数对”用&连接。 + * 例如:CanonicalQueryString = "Action=ListUsers&Version=2018-01-01" + * @return string + */ + public function getCanonicalQueryString(array $query) + { + ksort($query); + return http_build_query($query); + } + + /** + * 指代正规化后的URI。 + * 如果URI为空,那么使用"/"作为绝对路径。 + * 在火山引擎中绝大多数接口的URI都为"/"。 + * 如果是复杂的path,请通过RFC3986规范进行编码。 + * + * @return string + */ + public function getCanonicalURI() + { + return '/'; + } +} diff --git a/tests/Gateways/VolcengineGatewayTest.php b/tests/Gateways/VolcengineGatewayTest.php new file mode 100644 index 0000000..19fbc72 --- /dev/null +++ b/tests/Gateways/VolcengineGatewayTest.php @@ -0,0 +1,105 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\EasySms\Tests\Gateways; + +use Overtrue\EasySms\Exceptions\GatewayErrorException; +use Overtrue\EasySms\Gateways\VolcengineGateway; +use Overtrue\EasySms\Message; +use Overtrue\EasySms\PhoneNumber; +use Overtrue\EasySms\Support\Config; +use Overtrue\EasySms\Tests\TestCase; + +class VolcengineGatewayTest extends TestCase +{ + public function testSend() + { + $config = [ + 'access_key_id' => 'mock_access_key_id', + 'access_key_secret' => 'mock_access_key_secret', + 'sign_name' => 'mock_sign_name', + 'sms_account' => 'mock_sms_account', + ]; + + $queries = [ + 'Action' => VolcengineGateway::ENDPOINT_ACTION, + 'Version' => VolcengineGateway::ENDPOINT_VERSION, + ]; + + $templateId = 'mock_template_id'; + $phone = '18888888888'; + $templateParam = ['code' => '1234']; + + $params = [ + 'SmsAccount' => $config['sms_account'], + 'Sign' => $config['sign_name'], + 'TemplateID' => $templateId, + 'TemplateParam' => json_encode($templateParam), + 'PhoneNumbers' => $phone + ]; + + + $successReturn = [ + 'ResponseMetadata' => [ + 'RequestId' => 'mock_request_id', + 'Action' => VolcengineGateway::ENDPOINT_ACTION, + 'Version' => VolcengineGateway::ENDPOINT_VERSION, + 'Service' => VolcengineGateway::ENDPOINT_SERVICE, + 'Region' => VolcengineGateway::ENDPOINT_DEFAULT_REGION_ID, + ], + 'Result' => [ + "MessageID" => ["mock_message_id"], + ] + ]; + + $failedReturn = [ + 'ResponseMetadata' => [ + 'RequestId' => 'mock_request_id', + 'Action' => VolcengineGateway::ENDPOINT_ACTION, + 'Version' => VolcengineGateway::ENDPOINT_VERSION, + 'Service' => VolcengineGateway::ENDPOINT_SERVICE, + 'Region' => VolcengineGateway::ENDPOINT_DEFAULT_REGION_ID, + 'Error' => [ + 'Code' => str_repeat("ZJ", rand(1, 3)).rand(10000, 30000), + 'Message' => 'mock_error_message', + ], + ] + ]; + + $gateway = \Mockery::mock(VolcengineGateway::class.'[request]', [$config])->shouldAllowMockingProtectedMethods(); + $gateway->shouldReceive('request') + ->with( + 'post', + VolcengineGateway::$endpoints[VolcengineGateway::ENDPOINT_DEFAULT_REGION_ID].'/', + [ + 'query' => $queries, + 'json' => $params, + ] + ) + ->andReturn($successReturn, $failedReturn) + ->twice(); + + $message = new Message([ + 'template' => $templateId, + 'data' => $templateParam, + ]); + + $this->assertSame($successReturn, $gateway->send(new PhoneNumber($phone), $message, new Config($config))); + + $message = new Message([ + 'template' => $templateId, + 'data' => $templateParam, + ]); + + $this->expectException(GatewayErrorException::class); + $gateway->send(new PhoneNumber($phone), $message, new Config($config)); + } +} From 860bb6abf99a7fb139b574ba52824712ca3378b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Mon, 5 Dec 2022 15:52:10 +0800 Subject: [PATCH 02/11] Fixed #334 --- src/Gateways/QcloudGateway.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gateways/QcloudGateway.php b/src/Gateways/QcloudGateway.php index d0a298d..2aaad18 100644 --- a/src/Gateways/QcloudGateway.php +++ b/src/Gateways/QcloudGateway.php @@ -76,7 +76,7 @@ public function send(PhoneNumberInterface $to, MessageInterface $message, Config 'Host' => self::ENDPOINT_HOST, 'Content-Type' => 'application/json; charset=utf-8', 'X-TC-Action' => self::ENDPOINT_METHOD, - 'X-TC-Region' => self::ENDPOINT_REGION, + 'X-TC-Region' => $this->config->get('region', self::ENDPOINT_REGION), 'X-TC-Timestamp' => $time, 'X-TC-Version' => self::ENDPOINT_VERSION, ], From 034b271a9414a30257c9e8ed8639413a35717a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E8=BF=9B=E6=98=A5?= Date: Mon, 5 Dec 2022 15:59:08 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E7=81=AB=E5=B1=B1?= =?UTF-8?q?=E5=BC=95=E6=93=8Eregion=5Fid=E9=85=8D=E7=BD=AE=20(#336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cbe7a61..b2c0c5e 100644 --- a/README.md +++ b/README.md @@ -892,7 +892,8 @@ $easySms->send(18888888888, [ ```php 'volcengine' => [ 'access_key_id' => '', // 平台分配给用户的access_key_id - 'access_key_secret'=>'', // 平台分配给用户的access_key_secret + 'access_key_secret' => '', // 平台分配给用户的access_key_secret + 'region_id' => 'cn-north-1', // 国内节点 cn-north-1,国外节点 ap-singapore-1,不填或填错,默认使用国内节点 'sign_name' => '', // 平台上申请的接口短信签名或者签名ID,可不填,发送短信时data中指定 'sms_account' => '', // 消息组帐号,火山短信页面右上角,短信应用括号中的字符串,可不填,发送短信时data中指定 ], From ae7cfb39d140e34cf73dfcac75ad62f66f9b5a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=98=E8=AE=A4=E7=94=A8=E6=88=B7?= <1046512080@qq.com> Date: Sun, 15 Jan 2023 13:33:53 +0800 Subject: [PATCH 04/11] Update Messenger.php (#338) --- src/Messenger.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Messenger.php b/src/Messenger.php index c09f03d..97b704c 100644 --- a/src/Messenger.php +++ b/src/Messenger.php @@ -60,6 +60,7 @@ public function send(PhoneNumberInterface $to, MessageInterface $message, array $results[$gateway] = [ 'gateway' => $gateway, 'status' => self::STATUS_SUCCESS, + 'template' => $message->getTemplate($this->easySms->gateway($gateway)), 'result' => $this->easySms->gateway($gateway)->send($to, $message, $config), ]; $isSuccessful = true; @@ -69,12 +70,14 @@ public function send(PhoneNumberInterface $to, MessageInterface $message, array $results[$gateway] = [ 'gateway' => $gateway, 'status' => self::STATUS_FAILURE, + 'template' => $message->getTemplate($this->easySms->gateway($gateway)), 'exception' => $e, ]; } catch (\Throwable $e) { $results[$gateway] = [ 'gateway' => $gateway, 'status' => self::STATUS_FAILURE, + 'template' => $message->getTemplate($this->easySms->gateway($gateway)), 'exception' => $e, ]; } From 7c0d08fd3ab5dead8166b66053056be9fba2dd2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Sun, 29 Jan 2023 09:19:24 +0800 Subject: [PATCH 05/11] Fixed warning #339 --- src/Gateways/AliyunGateway.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gateways/AliyunGateway.php b/src/Gateways/AliyunGateway.php index 804ac6f..c061dd2 100644 --- a/src/Gateways/AliyunGateway.php +++ b/src/Gateways/AliyunGateway.php @@ -79,7 +79,7 @@ public function send(PhoneNumberInterface $to, MessageInterface $message, Config $result = $this->get(self::ENDPOINT_URL, $params); - if ('OK' != $result['Code']) { + if (!empty($result['Code']) && 'OK' != $result['Code']) { throw new GatewayErrorException($result['Message'], $result['Code'], $result); } From 041acd35507b6d040b097305810a244c72622e66 Mon Sep 17 00:00:00 2001 From: "CGI.NET" <1638899+nsnake@users.noreply.github.com> Date: Thu, 9 Mar 2023 16:26:38 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=88=9B=E8=93=9D?= =?UTF-8?q?=E4=BA=91=E6=99=BA=E7=9A=84=E7=BD=91=E5=85=B3=20(#341)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add files via upload add:创蓝云智 * Update README.md * Update README.md --- README.md | 40 ++++++++ src/Gateways/Chuanglanv1Gateway.php | 146 ++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 src/Gateways/Chuanglanv1Gateway.php diff --git a/README.md b/README.md index b2c0c5e..5a50cf5 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ - [百度云](https://cloud.baidu.com/) - [华信短信平台](http://www.ipyy.com/) - [253云通讯(创蓝)](https://www.253.com/) +- [创蓝云智](https://www.chuanglan.com/) - [融云](http://www.rongcloud.cn) - [天毅无线](http://www.85hu.com/) - [阿凡达数据](http://www.avatardata.cn/) @@ -520,6 +521,45 @@ $easySms->send($phone_number, [ ], ``` +### [创蓝云智](https://www.chuanglan.com/) + +普通短信发送内容使用 `content` + +```php + 'chuanglanv1' => [ + 'account' => '', + 'password' => '', + 'needstatus' => false, + 'channel' => \Overtrue\EasySms\Gateways\ChuanglanV1Gateway::CHANNEL_NORMAL_CODE, + ], +``` +发送示例: + +```php +$easySms->send(18888888888, [ + 'content' => xxxxxxx +]); +``` + +变量短信发送内容使用 `template` + `data` + +```php + 'chuanglanv1' => [ + 'account' => '', + 'password' => '', + 'needstatus' => false, + 'channel' => \Overtrue\EasySms\Gateways\ChuanglanV1Gateway::CHANNEL_VARIABLE_CODE, + ], +``` +发送示例: + +```php +$easySms->send(18888888888, [ + 'template' => xxxxxx, // 模板内容 + 'data' => 'phone":"15800000000,1234;15300000000,4321', +]); +``` + ### [融云](http://www.rongcloud.cn) 短信分为两大类,验证类和通知类短信。 发送验证类短信使用 `template` + `data` diff --git a/src/Gateways/Chuanglanv1Gateway.php b/src/Gateways/Chuanglanv1Gateway.php new file mode 100644 index 0000000..528c64e --- /dev/null +++ b/src/Gateways/Chuanglanv1Gateway.php @@ -0,0 +1,146 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Overtrue\EasySms\Gateways; + +use Overtrue\EasySms\Contracts\MessageInterface; +use Overtrue\EasySms\Contracts\PhoneNumberInterface; +use Overtrue\EasySms\Exceptions\GatewayErrorException; +use Overtrue\EasySms\Exceptions\InvalidArgumentException; +use Overtrue\EasySms\Support\Config; +use Overtrue\EasySms\Traits\HasHttpRequest; + +/** + * Class ChuanglanGateway. + * + * @see https://www.chuanglan.com/document/6110e57909fd9600010209de/62b3dc1d272e290001af3e75 + */ +class Chuanglanv1Gateway extends Gateway +{ + use HasHttpRequest; + + /** + * 国际短信 + */ + const INT_URL = 'http://intapi.253.com/send/json'; + + /** + * URL模板 + */ + const ENDPOINT_URL_TEMPLATE = 'https://smssh1.253.com/msg/v1/%s/json'; + + /** + * 支持单发、群发短信 + */ + const CHANNEL_NORMAL_CODE = 'send'; + + /** + * 单号码对应单内容批量下发 + */ + const CHANNEL_VARIABLE_CODE = 'variable'; + + /** + * @param PhoneNumberInterface $to + * @param MessageInterface $message + * @param Config $config + * + * @return array + * + * @throws GatewayErrorException + * @throws InvalidArgumentException + */ + public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) + { + $IDDCode = !empty($to->getIDDCode()) ? $to->getIDDCode() : 86; + + $params = [ + 'account' => $config->get('account'), + 'password' => $config->get('password'), + 'phone' => $to->getNumber(), + ]; + + if (86 != $IDDCode) { + $params['mobile'] = $to->getIDDCode() . $to->getNumber(); + $params['account'] = $config->get('intel_account') ?: $config->get('account'); + $params['password'] = $config->get('intel_password') ?: $config->get('password'); + } + + if (self::CHANNEL_VARIABLE_CODE == $this->getChannel($config, $IDDCode)) { + $params['params'] = $message->getData($this); + $params['msg'] = $this->wrapChannelContent($message->getTemplate($this), $config, $IDDCode); + } else { + $params['msg'] = $this->wrapChannelContent($message->getContent($this), $config, $IDDCode); + } + + $result = $this->postJson($this->buildEndpoint($config, $IDDCode), $params); + + if (!isset($result['code']) || '0' != $result['code']) { + throw new GatewayErrorException(json_encode($result, JSON_UNESCAPED_UNICODE), isset($result['code']) ? $result['code'] : 0, $result); + } + + return $result; + } + + /** + * @param Config $config + * @param int $IDDCode + * + * @return string + * + * @throws InvalidArgumentException + */ + protected function buildEndpoint(Config $config, $IDDCode = 86) + { + $channel = $this->getChannel($config, $IDDCode); + + if (self::INT_URL === $channel) { + return $channel; + } + + return sprintf(self::ENDPOINT_URL_TEMPLATE, $channel); + } + + /** + * @param Config $config + * @param int $IDDCode + * + * @return mixed + * + * @throws InvalidArgumentException + */ + protected function getChannel(Config $config, $IDDCode) + { + if (86 != $IDDCode) { + return self::INT_URL; + } + $channel = $config->get('channel', self::CHANNEL_NORMAL_CODE); + + if (!in_array($channel, [self::CHANNEL_NORMAL_CODE, self::CHANNEL_VARIABLE_CODE])) { + throw new InvalidArgumentException('Invalid channel for ChuanglanGateway.'); + } + + return $channel; + } + + /** + * @param string $content + * @param Config $config + * @param int $IDDCode + * + * @return string|string + * + * @throws InvalidArgumentException + */ + protected function wrapChannelContent($content, Config $config, $IDDCode) + { + return $content; + } +} From 43fd033c04cd4b849727e03d7ca415027ce0308a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Fri, 17 Mar 2023 16:19:17 +0800 Subject: [PATCH 07/11] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8afd9ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 overtrue + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 5171726ce091505cb3e3a9a8c2ad59a084bbfe39 Mon Sep 17 00:00:00 2001 From: "CGI.NET" <1638899+nsnake@users.noreply.github.com> Date: Thu, 23 Mar 2023 17:23:33 +0800 Subject: [PATCH 08/11] Update Chuanglanv1Gateway.php (#342) --- src/Gateways/Chuanglanv1Gateway.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Gateways/Chuanglanv1Gateway.php b/src/Gateways/Chuanglanv1Gateway.php index 528c64e..fd25ceb 100644 --- a/src/Gateways/Chuanglanv1Gateway.php +++ b/src/Gateways/Chuanglanv1Gateway.php @@ -35,12 +35,12 @@ class Chuanglanv1Gateway extends Gateway /** * URL模板 */ - const ENDPOINT_URL_TEMPLATE = 'https://smssh1.253.com/msg/v1/%s/json'; + const ENDPOINT_URL_TEMPLATE = 'https://smssh1.253.com/msg/%s/json'; /** * 支持单发、群发短信 */ - const CHANNEL_NORMAL_CODE = 'send'; + const CHANNEL_NORMAL_CODE = 'v1/send'; /** * 单号码对应单内容批量下发 @@ -63,8 +63,7 @@ public function send(PhoneNumberInterface $to, MessageInterface $message, Config $params = [ 'account' => $config->get('account'), - 'password' => $config->get('password'), - 'phone' => $to->getNumber(), + 'password' => $config->get('password') ]; if (86 != $IDDCode) { @@ -77,6 +76,7 @@ public function send(PhoneNumberInterface $to, MessageInterface $message, Config $params['params'] = $message->getData($this); $params['msg'] = $this->wrapChannelContent($message->getTemplate($this), $config, $IDDCode); } else { + $params['phone'] = $to->getNumber(); $params['msg'] = $this->wrapChannelContent($message->getContent($this), $config, $IDDCode); } From a920c503dd200eeaad259c4619ee97725f8dacdc Mon Sep 17 00:00:00 2001 From: "CGI.NET" <1638899+nsnake@users.noreply.github.com> Date: Mon, 27 Mar 2023 15:16:08 +0800 Subject: [PATCH 09/11] =?UTF-8?q?add=EF=BC=9Areport=E5=8F=82=E6=95=B0=20(#?= =?UTF-8?q?343)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Gateways/Chuanglanv1Gateway.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Gateways/Chuanglanv1Gateway.php b/src/Gateways/Chuanglanv1Gateway.php index fd25ceb..a81fb6a 100644 --- a/src/Gateways/Chuanglanv1Gateway.php +++ b/src/Gateways/Chuanglanv1Gateway.php @@ -63,7 +63,8 @@ public function send(PhoneNumberInterface $to, MessageInterface $message, Config $params = [ 'account' => $config->get('account'), - 'password' => $config->get('password') + 'password' => $config->get('password'), + 'report' => $config->get('needstatus') ?? false ]; if (86 != $IDDCode) { From d8f01fd163b6ecf059335b7ed2f691075476c081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Mon, 27 Mar 2023 15:16:49 +0800 Subject: [PATCH 10/11] Delete .php_cs --- .php_cs | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .php_cs diff --git a/.php_cs b/.php_cs deleted file mode 100644 index 1634080..0000000 --- a/.php_cs +++ /dev/null @@ -1,28 +0,0 @@ - - -This source file is subject to the MIT license that is bundled -with this source code in the file LICENSE. -EOF; - -return PhpCsFixer\Config::create() - ->setRiskyAllowed(true) - ->setRules(array( - '@Symfony' => true, - 'header_comment' => array('header' => $header), - 'array_syntax' => array('syntax' => 'short'), - 'ordered_imports' => true, - 'no_useless_else' => true, - 'no_useless_return' => true, - 'php_unit_construct' => true, - 'php_unit_strict' => true, - )) - ->setFinder( - PhpCsFixer\Finder::create() - ->exclude('vendor') - ->in(__DIR__) - ) -; \ No newline at end of file From 78b9bdaabe08f444589877a47d10595805dba42b Mon Sep 17 00:00:00 2001 From: Hyman Date: Fri, 14 Apr 2023 16:54:03 +0800 Subject: [PATCH 11/11] =?UTF-8?q?Ucloud=E7=BD=91=E5=85=B3=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=A8=A1=E6=9D=BF=E6=8C=87=E5=AE=9A=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=20(#345)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ucloud网关支持模板指定签名 * Ucloud网关支持模板指定签名 --- src/Gateways/UcloudGateway.php | 2 +- tests/Gateways/UcloudGatewayTest.php | 42 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Gateways/UcloudGateway.php b/src/Gateways/UcloudGateway.php index 1eee58a..fddfd3c 100644 --- a/src/Gateways/UcloudGateway.php +++ b/src/Gateways/UcloudGateway.php @@ -68,7 +68,7 @@ protected function buildParams(PhoneNumberInterface $to, MessageInterface $messa $data = $message->getData($this); $params = [ 'Action' => self::ENDPOINT_Action, - 'SigContent' => $config->get('sig_content'), + 'SigContent' => !empty($data['sig_content']) ? $data['sig_content'] : $config->get('sig_content', ''), 'TemplateId' => $message->getTemplate($this), 'PublicKey' => $config->get('public_key'), ]; diff --git a/tests/Gateways/UcloudGatewayTest.php b/tests/Gateways/UcloudGatewayTest.php index 999dfa9..248c509 100644 --- a/tests/Gateways/UcloudGatewayTest.php +++ b/tests/Gateways/UcloudGatewayTest.php @@ -69,4 +69,46 @@ public function testSend() $gateway->send(new PhoneNumber(18888888888), $message, $config); } + + public function testSignContent() + { + $defaultSigContent = 'default_sig_content'; + + $dataSigContent = 'data_sig_content'; + + $config = [ + 'private_key' => '', //私钥 + 'public_key' => '', //公钥 + 'sig_content' => $defaultSigContent, //签名 + 'project_id' => '', + ]; + $easySms = new EasySms($config); + + $phoneNumber = new PhoneNumber('18888888888'); + $gateway = $easySms->gateway('ucloud'); + + $reflectionMethod = new \ReflectionMethod(get_class($gateway), 'buildParams'); + $reflectionMethod->setAccessible(true); + //验证指定签名 + $message = new Message([ + 'template' => '', + 'data' => [ + 'code' => '', // 如果是多个参数可以用数组 + 'mobiles' => '', //同时发送多个手机也可以用数组来,[1111111,11111] + 'sig_content'=>$dataSigContent + ], + ]); + $result = $reflectionMethod->invokeArgs($gateway, [$phoneNumber, $message, $easySms->getConfig()]); + $this->assertSame($dataSigContent, $result['SigContent']); + //验证配置默认签名 + $message = new Message([ + 'template' => '', + 'data' => [ + 'code' => '', // 如果是多个参数可以用数组 + 'mobiles' => '', //同时发送多个手机也可以用数组来,[1111111,11111] + ], + ]); + $result = $reflectionMethod->invokeArgs($gateway, [$phoneNumber, $message, $easySms->getConfig()]); + $this->assertSame($defaultSigContent, $result['SigContent']); + } }