[ 'global' => 'dysmsapi.aliyuncs.com', 'cn-hangzhou' => 'dysmsapi.aliyuncs.com', 'ap-southeast-1' => 'dysmsapi.ap-southeast-1.aliyuncs.com', ], 'dybaseapi' => [ 'global' => 'dybaseapi.aliyuncs.com', 'cn-hangzhou' => '1943695596114318.mns.cn-hangzhou.aliyuncs.com', // http or https; ], ]; /** * @var string $date_time_format 默认时间格式。 */ private $dateTimeFormat = 'Y-m-d\TH:i:s\Z'; /** * @var string $signName 签名。 */ private $signName; /** * @var string $verifyPhoneTemplateCode 短信验证码对应的短信模板 ID 。 */ private $verifyPhoneTemplateCode; /** * @var string $verifyPhoneTemplateField 短信模板变量中的验证码变量名。 */ private $verifyPhoneTemplateField; /** * @var array $baseParams 公共请求参数。 */ private $baseParams = [ 'AccessKeyId' => null, 'Action' => null, 'Format' => 'json', 'RegionId' => 'cn-hangzhou', 'SignatureMethod' => 'HMAC-SHA1', 'SignatureNonce' => null, 'SignatureVersion' => '1.0', 'Timestamp' => null, 'Version' => '2017-05-25', ]; /** * @var array $options 请求参数。 */ private $options = []; /** * AliDysms constructor. * * @param string $access_key_id 访问密钥 ID 。 * @param string $access_key_secret 访问密钥。 */ public function __construct($access_key_id = null, $access_key_secret = null) { // 在这里读取配置文件,初始化配置。 // $this->signName = ''; // $this->accessKeyId = ''; // $this->accessKeySecret = ''; // $this->verifyPhoneTemplateCode = ''; // $this->verifyPhoneTemplateField = ''; if ($access_key_id) { $this->accessKeyId = $access_key_id; } if ($access_key_secret) { $this->accessKeySecret = $access_key_secret; } } /** * 设置 API 的名称。 * * @param string $action API 的名称。 * * @return $this */ public function setAction($action) { $this->action = $action; return $this; } /** * 设置参数。 * * @param string $key 参数名。 * @param mixed $value 参数值。 * * @return $this */ public function setOption($key, $value) { $this->options[$key] = $value; return $this; } /** * 查看是否存在某参数。 * * @param string $key 参数名。 * * @return bool */ public function hasOption($key) { return isset($this->options[$key]); } /** * 批量设置参数。 * * @param array $options 参数数组。 * @param bool $cover 是否覆盖。 * * @return $this */ public function setOptions($options, $cover = false) { if ($cover) { foreach ($options as $k => $v) { $this->options[$k] = $v; } } else { $this->options = array_merge($this->options, $options); } return $this; } /** * 执行请求。 * * @return bool|mixed|string * @throws Exception */ public function execute() { $url = $this->endpoints['dysmsapi']['cn-hangzhou']; $baseParams = $this->baseParams; $baseParams['AccessKeyId'] = $this->accessKeyId; $baseParams['Action'] = $this->action; $baseParams['SignatureNonce'] = md5(uniqid(mt_rand(), true)); $baseParams['Timestamp'] = gmdate($this->dateTimeFormat); // 如果请求参数中包含有公共参数中的字段,则保留请求参数中的字段。 $options = array_merge($this->options, $baseParams); unset($options['Signature']); $options['Signature'] = $this->computeSignature($options); $ch = curl_init(); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_FAILONERROR, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $this->parsePostHttpBody($options)); curl_setopt($ch, CURLOPT_TIMEOUT, 80); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $http_headers = [ 'Date:' . gmdate($this->dateTimeFormat), 'Accept:application/json', 'x-acs-signature-method:HMAC-SHA1', 'x-acs-signature-version:1.0', 'x-acs-region-id:cn-hangzhou', 'x-sdk-client:php/2.0.0', 'Content-MD5:' . base64_encode(md5(json_encode($options), true)), // 'Content-Type:application/octet-stream;charset=utf-8', ]; curl_setopt($ch, CURLOPT_HTTPHEADER, $http_headers); $res = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); $errno = curl_errno($ch); $error = curl_error($ch); curl_close($ch); if ($errno > 0) { throw new Exception($error, $errno); } $res = json_decode($res, true); if (200 != $status || 'OK' != $res['Code']) { throw new Exception('ERR[' . $res['Code'] . ']: ' . $res['Message']); } // 重置请求参数,保证后续再次使用不会遗留前次的参数。 $this->options = []; return $res; } /** * 发送短信。 * * @param array|string $phone_numbers 手机号码,支持多个号码,多个号码字符串以英文半角逗号( , )隔开,支持数组。 * @param string $template_code 短信模板 ID 。 * @param string/array|null $templateParam 短信模板变量对应的实际值,支持 json 字符串,如果传入数组,则进行 json 编码。 * @param string $sign_name 短信签名名称。 * @param null $out_id 外部流水扩展字段。 * * @return bool|mixed|string * @throws Exception */ public function send($phone_numbers, $template_code, $template_param = null, $sign_name = null, $out_id = null) { $this->setAction('SendSms'); if (is_array($phone_numbers)) { $phone_numbers = join(',', $phone_numbers); } $this->setOptions([ 'PhoneNumbers' => $phone_numbers, 'TemplateCode' => $template_code, ]); if ($sign_name) { $this->setOption('SignName', $sign_name); } if (!$this->hasOption('SignName')) { $this->setOption('SignName', $this->signName); } if (is_string($template_param)) { $this->setOption('TemplateParam', $template_param); } elseif (is_array($template_param)) { $this->setOption('TemplateParam', json_encode($template_param, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } if (is_string($out_id)) { $this->setOption('OutId', $out_id); } return $this->execute(); } /** * 发送短信验证码。 * * @param string $phone_number 手机号码。 * @param int $digit 短信验证码位数,默认 6 位。 * @param string $verify_code 验证码。 * @param string $template_code 短信模板 id 。 * @param string $field 验证码参数字段。 * * @return bool|mixed|string * @throws Exception */ public function sendVerifyCode($phone_number, $digit = 6, $verify_code = null, $template_code = null, $field = null) { // 生成指定位数的短信验证码。 if (empty($verify_code)) { $verify_code = ''; for ($i = 0; $i < intval($digit); $i++) $verify_code .= mt_rand(0, 9); } $template_code = empty($template_code) ? $this->verifyPhoneTemplateCode : $template_code; $field = empty($field) ? $this->verifyPhoneTemplateField : $field; $response = $this->send( $phone_number, $template_code, [$field => $verify_code] ); if ($response['code'] === 'OK' && $response['message'] === 'OK') { $response['phone_number'] = $phone_number; $response['verify_code'] = $verify_code; return $response; } return false; } /** * 查询短信发送记录和发送状态。 * * @param string $phone_number 手机号码。 * @param string $send_date 日期格式,格式为 yyyyMMdd ,例如 20181225 ,可查询最近 30 天内的记录。 * @param int $current_page 当前页码。 * @param int $page_size 每页记录数量,取值范围为 1~50 。 * @param null|string $biz_id 发送回执 ID 。 * * @return bool|mixed|string * @throws Exception */ public function getDetails($phone_number, $send_date, $current_page = 1, $page_size = 10, $biz_id = null) { $this->setAction('QuerySendDetails'); $this->setOptions([ 'CurrentPage' => $current_page, 'PageSize' => $page_size, 'PhoneNumber' => $phone_number, 'SendDate' => $send_date, ]); if ($biz_id) { $this->setOption('BizId', $biz_id); } return $this->execute(); } /** * 编辑短信签名。 * * @param string $sign_name 签名名称。 * @param int $sign_source 签名来源。 * @param string $remark 短信签名申请说明。 * @param array $sign_file_list 签名的证明文件。 * * @return bool|mixed|string * @throws Exception */ private function editSign($sign_name, $sign_source, $remark, $sign_file_list = []) { $this->setOptions([ 'SignName' => $sign_name, 'SignSource' => $sign_source, 'Remark' => $remark, ]); if (is_array($sign_file_list)) { $sign_file_list = array_values($sign_file_list); foreach ($sign_file_list as $idx => $sign_file) { $this->setOption('SignFileList.' . ($idx + 1) . '.FileSuffix', $sign_file['file_suffix']); $this->setOption('SignFileList.' . ($idx + 1) . '.FileContents', $sign_file['file_contents']); } } return $this->execute(); } /** * 申请短信签名。 * * @param string $sign_name 签名名称。 * @param int $sign_source 签名来源。其中: * * 0:企事业单位的全称或简称。 * 1:工信部备案网站的全称或简称。 * 2:APP 应用的全称或简称。 * 3:公众号或小程序的全称或简称。 * 4:电商平台店铺名的全称或简称。 * 5:商标名的全称或简称 * * @param string $remark 短信签名申请说明。 * @param array $sign_file_list 签名的证明文件。以数组形式传入,格式如下: * * $sign_file_list = [ * ['file_suffix' => 'jpg','file_contents' => 'R0lGOD...iwAA'], * ['file_suffix' => 'jpg','file_contents' => 'R0lGOD...iwAA'], * ['file_suffix' => 'jpg','file_contents' => 'R0lGOD...iwAA'], * ['file_suffix' => 'jpg','file_contents' => 'R0lGOD...iwAA'], * ]; * * @return bool|mixed|string * @throws Exception */ public function addSign($sign_name, $sign_source, $remark, $sign_file_list = []) { $this->setAction('AddSmsSign'); return $this->editSign($sign_name, $sign_source, $remark, $sign_file_list); } /** * 删除短信签名。 * * @param string $sign_name 短信签名。 * * @return bool|mixed|string * @throws Exception */ public function deleteSign($sign_name) { $this->setAction('DeleteSmsSign'); $this->setOption('SignName', $sign_name); return $this->execute(); } /** * 修改未审核通过的短信签名,并重新提交审核。 * * 参数格式参考 addSign 方法。 * * @param string $sign_name 签名名称。 * @param int $sign_source 签名来源。 * @param string $remark 短信签名申请说明。 * @param array $sign_file_list 签名的证明文件。 * * @return bool|mixed|string * @throws Exception */ public function modifySign($sign_name, $sign_source, $remark, $sign_file_list = []) { $this->setAction('ModifySmsSign'); return $this->editSign($sign_name, $sign_source, $remark, $sign_file_list); } /** * 查询短信签名申请状态。 * * @param string $sign_name 短信签名名称。 * * @return bool|mixed|string * @throws Exception */ public function getSign($sign_name) { $this->setAction('QuerySmsSign'); $this->setOption('SignName', $sign_name); return $this->execute(); } /** * 编辑短信模板。 * * @param string $template_name 模板名称。 * @param int $template_type 短信类型。 * @param string $template_content 模板内容。 * @param string $remark 短信模板申请说明。 * @param string $template_code 短信模板 CODE 。 * * @return bool|mixed|string * @throws Exception */ private function editTemplate($template_name, $template_type, $template_content, $remark, $template_code = null) { $this->setOptions([ 'TemplateName' => $template_name, 'TemplateType' => intval($template_type), 'TemplateContent' => $template_content, 'Remark' => $remark, ]); if ($template_code) { $this->setOption('TemplateCode', $template_code); } return $this->execute(); } /** * 申请短信模板。 * * @param string $template_name 模板名称。 * @param int $template_type 短信类型。其中: * * 0:验证码。 * 1:短信通知。 * 2:推广短信。 * 3:国际/港澳台消息。 * * @param string $template_content 模板内容,长度为 1~500 个字符。 * @param string $remark 短信模板申请说明。 * * @return bool|mixed|string * @throws Exception */ public function addTemplate($template_name, $template_type, $template_content, $remark) { $this->setAction('AddSmsTemplate'); return $this->editTemplate($template_name, $template_type, $template_content, $remark); } /** * 删除短信模板。 * * @param string $template_code 短信模板 CODE 。 * * @return bool|mixed|string * @throws Exception */ public function deleteTemplate($template_code) { $this->setAction('DeleteSmsTemplate'); $this->setOption('TemplateCode', $template_code); return $this->execute(); } /** * 修改未通过审核的短信模板。 * * 参数格式参考 addTemplate 方法。 * * @param string $template_code 短信模板 CODE 。 * @param string $template_name 模板名称。 * @param int $template_type 短信类型。 * @param string $template_content 模板内容,长度为 1~500 个字符。 * @param string $remark 短信模板申请说明。 * * @return bool|mixed|string * @throws Exception */ public function modifyTemplate($template_code, $template_name, $template_type, $template_content, $remark) { $this->setAction('ModifySmsTemplate'); return $this->editTemplate($template_name, $template_type, $template_content, $remark, $template_code); } /** * 查询短信模板的审核状态。 * * @param string $template_code 短信模板 CODE 。 * * @return bool|mixed|string * @throws Exception */ public function getTemplate($template_code) { $this->setAction('QuerySmsTemplate'); $this->setOption('TemplateCode', $template_code); return $this->execute(); } /** * 根据 POP 规则对要签名的字符串进行编码。 * * @param string $str 要编码的字符串。 * * @return string|string[]|null 已编码的字符串。 */ private function percentEncode($str) { $res = urlencode($str); $res = preg_replace('/\+/', '%20', $res); $res = preg_replace('/\*/', '%2A', $res); $res = preg_replace('/%7E/', '~', $res); return $res; } /** * 计算签名。 * * @param array $params 请求参数。 * * @return string 签名。 */ private function computeSignature($params) { global $access_secret; ksort($params); $sourceArr = []; foreach ($params as $k => $v) { $sourceArr[] = $this->percentEncode($k) . '=' . $this->percentEncode($v); } $source = join('&', $sourceArr); $source = 'POST' . '&' . $this->percentEncode('/') . '&' . $this->percentEncode($source); return base64_encode(hash_hmac('sha1', $source, $this->accessKeySecret . '&', true)); } /** * 拼接请求体。 * * @param array $params 请求参数。 * * @return string 请求体字符串。 */ private function parsePostHttpBody($params) { $bodyArr = []; foreach ($params as $k => $v) { $bodyArr[] = $k . '=' . urlencode($v); } return join('&', $bodyArr); } }