AlismsService.php 20 KB

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