Alisms.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. <?php
  2. namespace crmeb\services;
  3. /**
  4. * 阿里云 - 短信服务功能模块。
  5. * Class Alisms
  6. */
  7. class Alisms
  8. {
  9. /**
  10. * @var string $accessKeyId 访问密钥 ID 。
  11. */
  12. private $accessKeyId;
  13. /**
  14. * @var string $accessKeySecret 访问密钥。
  15. */
  16. private $accessKeySecret;
  17. /**
  18. * 支持的 API 列表:
  19. * 短信发送接口:
  20. * SendSms 发送短信。
  21. * SendBatchSms 批量发送短信。
  22. * 短信查询接口:
  23. * QuerySendDetails 查询短信发送的状态。
  24. * 签名申请接口:
  25. * AddSmsSign 调用短信 AddSmsSign 申请短信签名。
  26. * DeleteSmsSign 调用接口 DeleteSmsSign 删除短信签名。
  27. * QuerySmsSign 调用接口 QuerySmsSign 查询短信签名申请状态。
  28. * ModifySmsSign 调用接口 ModifySmsSign 修改未审核通过的短信签名,并重新提交审核。
  29. * 模板申请接口:
  30. * ModifySmsTemplate 调用接口 ModifySmsTemplate 修改未通过审核的短信模板。
  31. * QuerySmsTemplate 调用接口 QuerySmsTemplate 查询短信模板的审核状态。
  32. * AddSmsTemplate 调用接口 AddSmsTemplate 申请短信模板。
  33. * DeleteSmsTemplate 调用接口 DeleteSmsTemplate 删除短信模板。
  34. * 回执消息:
  35. * SmsReport 订阅 SmsReport 短信状态报告,获取短信发送状态。
  36. * SmsUp 订阅 SmsUp 上行短信消息,获取终端用户回复短信的内容。
  37. * SignSmsReport 订阅签名审核状态消息( SignSmsReport ),获取指定签名的审核状态。
  38. * TemplateSmsReport 订阅模板审核状态消息( TemplateSmsReport ),获取指定模板的审核状态。
  39. *
  40. * @var string $action API 的名称。
  41. */
  42. private $action;
  43. /**
  44. * @var array $endpoints 阿里云公网服务地址。
  45. */
  46. private $endpoints = [
  47. 'dysmsapi' => [
  48. 'global' => 'dysmsapi.aliyuncs.com',
  49. 'cn-hangzhou' => 'dysmsapi.aliyuncs.com',
  50. 'ap-southeast-1' => 'dysmsapi.ap-southeast-1.aliyuncs.com',
  51. ],
  52. 'dybaseapi' => [
  53. 'global' => 'dybaseapi.aliyuncs.com',
  54. 'cn-hangzhou' => '1943695596114318.mns.cn-hangzhou.aliyuncs.com', // http or https;
  55. ],
  56. ];
  57. /**
  58. * @var string $date_time_format 默认时间格式。
  59. */
  60. private $dateTimeFormat = 'Y-m-d\TH:i:s\Z';
  61. /**
  62. * @var string $signName 签名。
  63. */
  64. private $signName;
  65. /**
  66. * @var string $verifyPhoneTemplateCode 短信验证码对应的短信模板 ID 。
  67. */
  68. private $verifyPhoneTemplateCode;
  69. /**
  70. * @var string $verifyPhoneTemplateField 短信模板变量中的验证码变量名。
  71. */
  72. private $verifyPhoneTemplateField;
  73. /**
  74. * @var array $baseParams 公共请求参数。
  75. */
  76. private $baseParams = [
  77. 'AccessKeyId' => null,
  78. 'Action' => null,
  79. 'Format' => 'json',
  80. 'RegionId' => 'cn-hangzhou',
  81. 'SignatureMethod' => 'HMAC-SHA1',
  82. 'SignatureNonce' => null,
  83. 'SignatureVersion' => '1.0',
  84. 'Timestamp' => null,
  85. 'Version' => '2017-05-25',
  86. ];
  87. /**
  88. * @var array $options 请求参数。
  89. */
  90. private $options = [];
  91. /**
  92. * AliDysms constructor.
  93. *
  94. * @param string $access_key_id 访问密钥 ID 。
  95. * @param string $access_key_secret 访问密钥。
  96. */
  97. public function __construct($access_key_id = null, $access_key_secret = null)
  98. {
  99. // 在这里读取配置文件,初始化配置。
  100. // $this->signName = '';
  101. // $this->accessKeyId = '';
  102. // $this->accessKeySecret = '';
  103. // $this->verifyPhoneTemplateCode = '';
  104. // $this->verifyPhoneTemplateField = '';
  105. if ($access_key_id) {
  106. $this->accessKeyId = $access_key_id;
  107. }
  108. if ($access_key_secret) {
  109. $this->accessKeySecret = $access_key_secret;
  110. }
  111. }
  112. /**
  113. * 设置 API 的名称。
  114. *
  115. * @param string $action API 的名称。
  116. *
  117. * @return $this
  118. */
  119. public function setAction($action)
  120. {
  121. $this->action = $action;
  122. return $this;
  123. }
  124. /**
  125. * 设置参数。
  126. *
  127. * @param string $key 参数名。
  128. * @param mixed $value 参数值。
  129. *
  130. * @return $this
  131. */
  132. public function setOption($key, $value)
  133. {
  134. $this->options[$key] = $value;
  135. return $this;
  136. }
  137. /**
  138. * 查看是否存在某参数。
  139. *
  140. * @param string $key 参数名。
  141. *
  142. * @return bool
  143. */
  144. public function hasOption($key)
  145. {
  146. return isset($this->options[$key]);
  147. }
  148. /**
  149. * 批量设置参数。
  150. *
  151. * @param array $options 参数数组。
  152. * @param bool $cover 是否覆盖。
  153. *
  154. * @return $this
  155. */
  156. public function setOptions($options, $cover = false)
  157. {
  158. if ($cover) {
  159. foreach ($options as $k => $v) {
  160. $this->options[$k] = $v;
  161. }
  162. } else {
  163. $this->options = array_merge($this->options, $options);
  164. }
  165. return $this;
  166. }
  167. /**
  168. * 执行请求。
  169. *
  170. * @return bool|mixed|string
  171. * @throws \Exception
  172. */
  173. public function execute()
  174. {
  175. $url = $this->endpoints['dysmsapi']['cn-hangzhou'];
  176. $baseParams = $this->baseParams;
  177. $baseParams['AccessKeyId'] = $this->accessKeyId;
  178. $baseParams['Action'] = $this->action;
  179. $baseParams['SignatureNonce'] = md5(uniqid(mt_rand(), true));
  180. $baseParams['Timestamp'] = gmdate($this->dateTimeFormat);
  181. // 如果请求参数中包含有公共参数中的字段,则保留请求参数中的字段。
  182. $options = array_merge($this->options, $baseParams);
  183. unset($options['Signature']);
  184. $options['Signature'] = $this->computeSignature($options);
  185. $ch = curl_init();
  186. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  187. curl_setopt($ch, CURLOPT_URL, $url);
  188. curl_setopt($ch, CURLOPT_FAILONERROR, false);
  189. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  190. curl_setopt($ch, CURLOPT_POSTFIELDS, $this->parsePostHttpBody($options));
  191. curl_setopt($ch, CURLOPT_TIMEOUT, 80);
  192. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
  193. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  194. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  195. $http_headers = [
  196. 'Date:' . gmdate($this->dateTimeFormat),
  197. 'Accept:application/json',
  198. 'x-acs-signature-method:HMAC-SHA1',
  199. 'x-acs-signature-version:1.0',
  200. 'x-acs-region-id:cn-hangzhou',
  201. 'x-sdk-client:php/2.0.0',
  202. 'Content-MD5:' . base64_encode(md5(json_encode($options), true)),
  203. // 'Content-Type:application/octet-stream;charset=utf-8',
  204. ];
  205. curl_setopt($ch, CURLOPT_HTTPHEADER, $http_headers);
  206. $res = curl_exec($ch);
  207. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  208. $errno = curl_errno($ch);
  209. $error = curl_error($ch);
  210. curl_close($ch);
  211. if ($errno > 0) {
  212. throw new \Exception($error, $errno);
  213. }
  214. $res = json_decode($res, true);
  215. if (200 != $status || 'OK' != $res['Code']) {
  216. throw new \Exception('ERR[' . $res['Code'] . ']: ' . $res['Message']);
  217. }
  218. // 重置请求参数,保证后续再次使用不会遗留前次的参数。
  219. $this->options = [];
  220. return $res;
  221. }
  222. /**
  223. * 发送短信。
  224. *
  225. * @param array|string $phone_numbers 手机号码,支持多个号码,多个号码字符串以英文半角逗号( , )隔开,支持数组。
  226. * @param string $template_code 短信模板 ID 。
  227. * @param string/array|null $templateParam 短信模板变量对应的实际值,支持 json 字符串,如果传入数组,则进行 json 编码。
  228. * @param string $sign_name 短信签名名称。
  229. * @param null $out_id 外部流水扩展字段。
  230. *
  231. * @return bool|mixed|string
  232. * @throws \Exception
  233. */
  234. public function send($phone_numbers, $template_code, $template_param = null, $sign_name = null, $out_id = null)
  235. {
  236. $this->setAction('SendSms');
  237. if (is_array($phone_numbers)) {
  238. $phone_numbers = join(',', $phone_numbers);
  239. }
  240. $this->setOptions([
  241. 'PhoneNumbers' => $phone_numbers,
  242. 'TemplateCode' => $template_code,
  243. ]);
  244. if ($sign_name) {
  245. $this->setOption('SignName', $sign_name);
  246. }
  247. if (!$this->hasOption('SignName')) {
  248. $this->setOption('SignName', $this->signName);
  249. }
  250. if (is_string($template_param)) {
  251. $this->setOption('TemplateParam', $template_param);
  252. } elseif (is_array($template_param)) {
  253. $this->setOption('TemplateParam', json_encode($template_param, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
  254. }
  255. if (is_string($out_id)) {
  256. $this->setOption('OutId', $out_id);
  257. }
  258. return $this->execute();
  259. }
  260. /**
  261. * 发送短信验证码。
  262. *
  263. * @param string $phone_number 手机号码。
  264. * @param int $digit 短信验证码位数,默认 6 位。
  265. * @param string $verify_code 验证码。
  266. * @param string $template_code 短信模板 id 。
  267. * @param string $field 验证码参数字段。
  268. *
  269. * @return bool|mixed|string
  270. * @throws \Exception
  271. */
  272. public function sendVerifyCode($phone_number, $digit = 6, $verify_code = null, $template_code = null, $field = null)
  273. {
  274. // 生成指定位数的短信验证码。
  275. if (empty($verify_code)) {
  276. $verify_code = '';
  277. for ($i = 0; $i < intval($digit); $i++) $verify_code .= mt_rand(0, 9);
  278. }
  279. $template_code = empty($template_code) ? $this->verifyPhoneTemplateCode : $template_code;
  280. $field = empty($field) ? $this->verifyPhoneTemplateField : $field;
  281. $response = $this->send(
  282. $phone_number,
  283. $template_code,
  284. [$field => $verify_code]
  285. );
  286. if ($response['code'] === 'OK' && $response['message'] === 'OK') {
  287. $response['phone_number'] = $phone_number;
  288. $response['verify_code'] = $verify_code;
  289. return $response;
  290. }
  291. return false;
  292. }
  293. /**
  294. * 查询短信发送记录和发送状态。
  295. *
  296. * @param string $phone_number 手机号码。
  297. * @param string $send_date 日期格式,格式为 yyyyMMdd ,例如 20181225 ,可查询最近 30 天内的记录。
  298. * @param int $current_page 当前页码。
  299. * @param int $page_size 每页记录数量,取值范围为 1~50 。
  300. * @param null|string $biz_id 发送回执 ID 。
  301. *
  302. * @return bool|mixed|string
  303. * @throws \Exception
  304. */
  305. public function getDetails($phone_number, $send_date, $current_page = 1, $page_size = 10, $biz_id = null)
  306. {
  307. $this->setAction('QuerySendDetails');
  308. $this->setOptions([
  309. 'CurrentPage' => $current_page,
  310. 'PageSize' => $page_size,
  311. 'PhoneNumber' => $phone_number,
  312. 'SendDate' => $send_date,
  313. ]);
  314. if ($biz_id) {
  315. $this->setOption('BizId', $biz_id);
  316. }
  317. return $this->execute();
  318. }
  319. /**
  320. * 编辑短信签名。
  321. *
  322. * @param string $sign_name 签名名称。
  323. * @param int $sign_source 签名来源。
  324. * @param string $remark 短信签名申请说明。
  325. * @param array $sign_file_list 签名的证明文件。
  326. *
  327. * @return bool|mixed|string
  328. * @throws \Exception
  329. */
  330. private function editSign($sign_name, $sign_source, $remark, $sign_file_list = [])
  331. {
  332. $this->setOptions([
  333. 'SignName' => $sign_name,
  334. 'SignSource' => $sign_source,
  335. 'Remark' => $remark,
  336. ]);
  337. if (is_array($sign_file_list)) {
  338. $sign_file_list = array_values($sign_file_list);
  339. foreach ($sign_file_list as $idx => $sign_file) {
  340. $this->setOption('SignFileList.' . ($idx + 1) . '.FileSuffix', $sign_file['file_suffix']);
  341. $this->setOption('SignFileList.' . ($idx + 1) . '.FileContents', $sign_file['file_contents']);
  342. }
  343. }
  344. return $this->execute();
  345. }
  346. /**
  347. * 申请短信签名。
  348. *
  349. * @param string $sign_name 签名名称。
  350. * @param int $sign_source 签名来源。其中:
  351. *
  352. * 0:企事业单位的全称或简称。
  353. * 1:工信部备案网站的全称或简称。
  354. * 2:APP 应用的全称或简称。
  355. * 3:公众号或小程序的全称或简称。
  356. * 4:电商平台店铺名的全称或简称。
  357. * 5:商标名的全称或简称
  358. *
  359. * @param string $remark 短信签名申请说明。
  360. * @param array $sign_file_list 签名的证明文件。以数组形式传入,格式如下:
  361. *
  362. * $sign_file_list = [
  363. * ['file_suffix' => 'jpg','file_contents' => 'R0lGOD...iwAA'],
  364. * ['file_suffix' => 'jpg','file_contents' => 'R0lGOD...iwAA'],
  365. * ['file_suffix' => 'jpg','file_contents' => 'R0lGOD...iwAA'],
  366. * ['file_suffix' => 'jpg','file_contents' => 'R0lGOD...iwAA'],
  367. * ];
  368. *
  369. * @return bool|mixed|string
  370. * @throws \Exception
  371. */
  372. public function addSign($sign_name, $sign_source, $remark, $sign_file_list = [])
  373. {
  374. $this->setAction('AddSmsSign');
  375. return $this->editSign($sign_name, $sign_source, $remark, $sign_file_list);
  376. }
  377. /**
  378. * 删除短信签名。
  379. *
  380. * @param string $sign_name 短信签名。
  381. *
  382. * @return bool|mixed|string
  383. * @throws \Exception
  384. */
  385. public function deleteSign($sign_name)
  386. {
  387. $this->setAction('DeleteSmsSign');
  388. $this->setOption('SignName', $sign_name);
  389. return $this->execute();
  390. }
  391. /**
  392. * 修改未审核通过的短信签名,并重新提交审核。
  393. *
  394. * 参数格式参考 addSign 方法。
  395. *
  396. * @param string $sign_name 签名名称。
  397. * @param int $sign_source 签名来源。
  398. * @param string $remark 短信签名申请说明。
  399. * @param array $sign_file_list 签名的证明文件。
  400. *
  401. * @return bool|mixed|string
  402. * @throws Exception
  403. */
  404. public function modifySign($sign_name, $sign_source, $remark, $sign_file_list = [])
  405. {
  406. $this->setAction('ModifySmsSign');
  407. return $this->editSign($sign_name, $sign_source, $remark, $sign_file_list);
  408. }
  409. /**
  410. * 查询短信签名申请状态。
  411. *
  412. * @param string $sign_name 短信签名名称。
  413. *
  414. * @return bool|mixed|string
  415. * @throws \Exception
  416. */
  417. public function getSign($sign_name)
  418. {
  419. $this->setAction('QuerySmsSign');
  420. $this->setOption('SignName', $sign_name);
  421. return $this->execute();
  422. }
  423. /**
  424. * 编辑短信模板。
  425. *
  426. * @param string $template_name 模板名称。
  427. * @param int $template_type 短信类型。
  428. * @param string $template_content 模板内容。
  429. * @param string $remark 短信模板申请说明。
  430. * @param string $template_code 短信模板 CODE 。
  431. *
  432. * @return bool|mixed|string
  433. * @throws \Exception
  434. */
  435. private function editTemplate($template_name, $template_type, $template_content, $remark, $template_code = null)
  436. {
  437. $this->setOptions([
  438. 'TemplateName' => $template_name,
  439. 'TemplateType' => intval($template_type),
  440. 'TemplateContent' => $template_content,
  441. 'Remark' => $remark,
  442. ]);
  443. if ($template_code) {
  444. $this->setOption('TemplateCode', $template_code);
  445. }
  446. return $this->execute();
  447. }
  448. /**
  449. * 申请短信模板。
  450. *
  451. * @param string $template_name 模板名称。
  452. * @param int $template_type 短信类型。其中:
  453. *
  454. * 0:验证码。
  455. * 1:短信通知。
  456. * 2:推广短信。
  457. * 3:国际/港澳台消息。
  458. *
  459. * @param string $template_content 模板内容,长度为 1~500 个字符。
  460. * @param string $remark 短信模板申请说明。
  461. *
  462. * @return bool|mixed|string
  463. * @throws \Exception
  464. */
  465. public function addTemplate($template_name, $template_type, $template_content, $remark)
  466. {
  467. $this->setAction('AddSmsTemplate');
  468. return $this->editTemplate($template_name, $template_type, $template_content, $remark);
  469. }
  470. /**
  471. * 删除短信模板。
  472. *
  473. * @param string $template_code 短信模板 CODE 。
  474. *
  475. * @return bool|mixed|string
  476. * @throws Exception
  477. */
  478. public function deleteTemplate($template_code)
  479. {
  480. $this->setAction('DeleteSmsTemplate');
  481. $this->setOption('TemplateCode', $template_code);
  482. return $this->execute();
  483. }
  484. /**
  485. * 修改未通过审核的短信模板。
  486. *
  487. * 参数格式参考 addTemplate 方法。
  488. *
  489. * @param string $template_code 短信模板 CODE 。
  490. * @param string $template_name 模板名称。
  491. * @param int $template_type 短信类型。
  492. * @param string $template_content 模板内容,长度为 1~500 个字符。
  493. * @param string $remark 短信模板申请说明。
  494. *
  495. * @return bool|mixed|string
  496. * @throws \Exception
  497. */
  498. public function modifyTemplate($template_code, $template_name, $template_type, $template_content, $remark)
  499. {
  500. $this->setAction('ModifySmsTemplate');
  501. return $this->editTemplate($template_name, $template_type, $template_content, $remark, $template_code);
  502. }
  503. /**
  504. * 查询短信模板的审核状态。
  505. *
  506. * @param string $template_code 短信模板 CODE 。
  507. *
  508. * @return bool|mixed|string
  509. * @throws \Exception
  510. */
  511. public function getTemplate($template_code)
  512. {
  513. $this->setAction('QuerySmsTemplate');
  514. $this->setOption('TemplateCode', $template_code);
  515. return $this->execute();
  516. }
  517. /**
  518. * 根据 POP 规则对要签名的字符串进行编码。
  519. *
  520. * @param string $str 要编码的字符串。
  521. *
  522. * @return string|string[]|null 已编码的字符串。
  523. */
  524. private function percentEncode($str)
  525. {
  526. $res = urlencode($str);
  527. $res = preg_replace('/\+/', '%20', $res);
  528. $res = preg_replace('/\*/', '%2A', $res);
  529. $res = preg_replace('/%7E/', '~', $res);
  530. return $res;
  531. }
  532. /**
  533. * 计算签名。
  534. *
  535. * @param array $params 请求参数。
  536. *
  537. * @return string 签名。
  538. */
  539. private function computeSignature($params)
  540. {
  541. global $access_secret;
  542. ksort($params);
  543. $sourceArr = [];
  544. foreach ($params as $k => $v) {
  545. $sourceArr[] = $this->percentEncode($k) . '=' . $this->percentEncode($v);
  546. }
  547. $source = join('&', $sourceArr);
  548. $source = 'POST' . '&' . $this->percentEncode('/') . '&' . $this->percentEncode($source);
  549. return base64_encode(hash_hmac('sha1', $source, $this->accessKeySecret . '&', true));
  550. }
  551. /**
  552. * 拼接请求体。
  553. *
  554. * @param array $params 请求参数。
  555. *
  556. * @return string 请求体字符串。
  557. */
  558. private function parsePostHttpBody($params)
  559. {
  560. $bodyArr = [];
  561. foreach ($params as $k => $v) {
  562. $bodyArr[] = $k . '=' . urlencode($v);
  563. }
  564. return join('&', $bodyArr);
  565. }
  566. }