PolyChannelSmsWarn.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. <?php
  2. namespace app\crontab;
  3. use think\console\Command;
  4. use think\console\Input;
  5. use think\console\Output;
  6. use think\Db;
  7. use think\Cache;
  8. use app\common\logic\Complex;
  9. use think\Env;
  10. /**
  11. * 聚合渠道游戏通知预警定时器脚本
  12. *
  13. * - 定时任务:每半小时执行一次
  14. * - 预警时间段:8~24 点 之间执行
  15. * - 每天第一次触发时候必通知一次
  16. * - 单次预警间隔3小时
  17. */
  18. class PolyChannelSmsWarn extends Command {
  19. protected function configure() {
  20. $this->setName('PolyChannelSmsWarn')->setDescription('聚合渠道游戏短信预警脚本');
  21. }
  22. protected function execute(Input $input, Output $output) {
  23. $hour = date('H');
  24. // 只在 8~24 点 之间执行
  25. if ( $hour < 8 || $hour > 24 ) {
  26. $output->writeln(date('Y-m-d H:i:s')." 非预警时间段(8~24点),脚本结束\r\n");
  27. return;
  28. }
  29. $list = Db::table('cy_polychannel_game_frozen')->alias('pgf')
  30. ->join('cy_polychannel_deposit pcd', 'pgf.channel_id=pcd.channel_id', 'LEFT')
  31. ->join('nw_complex_channel cc', 'pgf.channel_id=cc.id', 'LEFT')
  32. ->where(['pgf.status' => 1])
  33. // ->where([
  34. // // 'pgf.game_id' => ['in', [891,892]],
  35. // 'pgf.channel_id' => 45
  36. // ])
  37. ->group('pgf.channel_id')
  38. ->field('pgf.channel_id, pcd.mobile, pcd.total_advance, pcd.warn_amount, pcd.over_limit, cc.name as channel_name')
  39. ->select();
  40. $channelNames = array_column($list, 'channel_name');
  41. $output->writeln(date('Y-m-d H:i:s')." start - sum=".count($list)." - 渠道:".implode(',', $channelNames));
  42. if ( !empty($list) ) {
  43. $complexLogic = new Complex;
  44. $redis = Cache::store('redis')->handler();
  45. $noticeExpire = 60*60*3; // 单次预警间隔三小时
  46. foreach ( $list as $row ) {
  47. try {
  48. if ( empty($row['mobile']) ) continue;
  49. if ( !is_object($redis) ) continue;
  50. $totalWarnAmount = bcadd($row['total_advance'] - $row['warn_amount'] + $row['over_limit'], '0', 2); // 剩余预警额度(总预付款-预警金额+可超额度)
  51. $channelTotalMoney = $complexLogic->getChannelGameTotalAmount($row['channel_id']); // 渠道分成总流水
  52. if ( $totalWarnAmount > $channelTotalMoney ) continue;
  53. // 记录第一次执行
  54. $intKey = "wlsh:polyChannelSmsWarn:{$row['channel_id']}";
  55. $initNum = $redis->get($intKey);
  56. if($initNum === false){
  57. $initNum = 0;
  58. $redis->set($intKey, 1, strtotime('tomorrow') - time());
  59. }
  60. // $initNum = 0;
  61. $dayKey = "wlsh:polyChannelSmsWarn:{$row['channel_id']}:".date('md');
  62. $notify_day_count = (int)$redis->get($dayKey);
  63. // 8~24 三小时发送一次; 第一次立即发送;
  64. if ($notify_day_count <= 0 || $initNum == 0) {
  65. $remainAmount = bcadd($row['total_advance'] - $channelTotalMoney, '0',2); // 预付款剩余额度
  66. $output->writeln("## {$row['channel_name']}-渠道, 剩余预警额度: 渠道分成总流水=".$channelTotalMoney.", 预警剩余金额:{$row['warn_amount']}, 剩余额度=".$remainAmount);
  67. // $template = "\n## {$row['channel_name']}-渠道, 预付款余额不足,预警剩余金额:{$row['warn_amount']}, 剩余额度: {$remainAmount} 元;\n请多加关注,并通知相关渠道!\n时间:".date('Y-m-d H:i:s')."\n";
  68. // $this->send_message_dd($template);
  69. // $output->writeln(date('H:i:s')." {$row['channel_id']}_DD 钉钉发送完成");
  70. $template = "**{$row['channel_name']}-渠道**, 预付款余额不足,预警剩余金额:{$row['warn_amount']}, 剩余额度: **{$remainAmount} 元**;\n请多加关注,并通知相关渠道!\n时间:".date('Y-m-d H:i:s')."\n";
  71. $this->send_message_feishu($template);
  72. $output->writeln(date('H:i:s')." {$row['channel_id']}_feishu 飞书发送完成");
  73. $params = ['name' => $row['channel_name'], 'money' => $remainAmount];
  74. $mobileRes = $this->send_message_mobile($row['mobile'], $params);
  75. $output->writeln(date('H:i:s')." {$row['channel_id']}_mobild= {$row['mobile']} 短信发送完成:{$mobileRes}");
  76. $redis->incrby($dayKey, 1);
  77. $redis->expire($dayKey, $noticeExpire);
  78. }
  79. } catch (\Exception $e) {
  80. $output->writeln(date('H:i:s')." {$row["channel_id"]}_{$row["channel_name"]} 处理异常: ".$e->getMessage());
  81. continue;
  82. }
  83. }
  84. }
  85. $output->writeln(date('Y-m-d H:i:s')." end\r\n\n");
  86. }
  87. private function send_message_dd($template){
  88. $ddurl = Env::get('dingtalk.notice_qdyfk_url');
  89. curlDD($template, $ddurl,true);
  90. }
  91. private function send_message_feishu($template){
  92. $feishuUrl = Env::get('feishu.notice_url', '');
  93. // 使用 interactive 类型支持 Markdown 格式
  94. $result = sendFeishuMessage($feishuUrl, $template, 'interactive', '⚠️ 祈盟-渠道预付款警告');
  95. if (!$result['success']) {
  96. throw new \RuntimeException("飞书发送失败: " . $result['error']);
  97. }
  98. return true;
  99. }
  100. private function send_message_mobile($mobile, $params){
  101. // return $this->sendMobileNotifyBatch($mobile, $params);
  102. $mobiles = explode(',', $mobile);
  103. foreach($mobiles as $m){
  104. $this->sendMobileNotify($m, $params);
  105. }
  106. return "OK";
  107. }
  108. // ## 阿里云短信 ##
  109. // 批量发送
  110. private function sendMobileNotifyBatch($mobiles, $params){
  111. $accessKeyId = 'LTAI5tMnwbRaFFNegZABs1i9';
  112. $accessKeySecret = '1AFchDeuPoytXzcCD8HUU4gsMizwn3';
  113. /**
  114. * {
  115. * "PhoneNumberJson": "[\"13121776520\", \"13057807309\"]",
  116. * "SignNameJson": "[\"祈盟\", \"祈盟\"]",
  117. * "TemplateCode": "SMS_495865300",
  118. * "TemplateParamJson": "[{\"name\":\"xxx\", \"money\":\"666\"}, {\"name\":\"xxx\", \"money\":\"666\"}]",
  119. * "SourceIp": "183.158.7.39"
  120. * }
  121. */
  122. $mobilesList = explode(',', $mobiles);
  123. $phoneNumberJson = [];
  124. $signNameJson = [];
  125. $templateParamJson = [];
  126. foreach ($mobilesList as $v){
  127. $phoneNumberJson[] = $v;
  128. $signNameJson[] = "祈盟";
  129. $templateParamJson[] = $params;
  130. }
  131. $paramsData = [
  132. 'PhoneNumberJson' => json_encode($phoneNumberJson),
  133. 'SignNameJson' => json_encode($signNameJson),
  134. 'TemplateCode' => 'SMS_495865300',
  135. 'TemplateParamJson' => json_encode($templateParamJson, JSON_UNESCAPED_UNICODE),
  136. ];
  137. // var_dump(json_encode($paramsData));
  138. $paramsData = array_merge($paramsData, [
  139. "RegionId" => "cn-hangzhou",
  140. "Action" => "SendSms",
  141. "Version" => "2017-05-25",
  142. ]);
  143. $paramsData = array_merge([
  144. "SignatureMethod" => "HMAC-SHA1",
  145. "SignatureNonce" => uniqid(mt_rand(0,0xffff), true),
  146. "SignatureVersion" => "1.0",
  147. "AccessKeyId" => $accessKeyId,
  148. "Timestamp" => gmdate("Y-m-d\TH:i:s\Z"),
  149. "Format" => "JSON",
  150. ], $paramsData);
  151. ksort($paramsData);
  152. $sortedQueryStringTmp = "";
  153. foreach ($paramsData as $key => $value) {
  154. $sortedQueryStringTmp .= "&" . $this->encode($key) . "=" . $this->encode($value);
  155. }
  156. $stringToSign = "POST&%2F&" . $this->encode(substr($sortedQueryStringTmp, 1));
  157. $sign = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&",true));
  158. $signature = $this->encode($sign);
  159. $result = $this->fetchContent("http://dysmsapi.aliyuncs.com/", "POST", "Signature={$signature}{$sortedQueryStringTmp}");
  160. $result = json_decode($result, true);
  161. // dump($result);
  162. return $result['Code'];
  163. }
  164. // 单条发送
  165. private function sendMobileNotify($mobile, $params){
  166. $accessKeyId = 'LTAI5tMnwbRaFFNegZABs1i9';
  167. $accessKeySecret = '1AFchDeuPoytXzcCD8HUU4gsMizwn3';
  168. $paramsData = [
  169. 'PhoneNumbers' => $mobile,
  170. 'SignName' => '祈盟',
  171. 'TemplateCode' => 'SMS_495865300',
  172. 'TemplateParam' => json_encode($params, JSON_UNESCAPED_UNICODE),
  173. ];
  174. $paramsData = array_merge($paramsData, [
  175. "RegionId" => "cn-hangzhou",
  176. "Action" => "SendSms",
  177. "Version" => "2017-05-25",
  178. ]);
  179. $paramsData = array_merge([
  180. "SignatureMethod" => "HMAC-SHA1",
  181. "SignatureNonce" => uniqid(mt_rand(0,0xffff), true),
  182. "SignatureVersion" => "1.0",
  183. "AccessKeyId" => $accessKeyId,
  184. "Timestamp" => gmdate("Y-m-d\TH:i:s\Z"),
  185. "Format" => "JSON",
  186. ], $paramsData);
  187. ksort($paramsData);
  188. $sortedQueryStringTmp = "";
  189. foreach ($paramsData as $key => $value) {
  190. $sortedQueryStringTmp .= "&" . $this->encode($key) . "=" . $this->encode($value);
  191. }
  192. $stringToSign = "POST&%2F&" . $this->encode(substr($sortedQueryStringTmp, 1));
  193. $sign = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&",true));
  194. $signature = $this->encode($sign);
  195. $this->fetchContent("http://dysmsapi.aliyuncs.com/", "POST", "Signature={$signature}{$sortedQueryStringTmp}");
  196. }
  197. private function encode($str)
  198. {
  199. $res = urlencode($str);
  200. $res = preg_replace("/\+/", "%20", $res);
  201. $res = preg_replace("/\*/", "%2A", $res);
  202. $res = preg_replace("/%7E/", "~", $res);
  203. return $res;
  204. }
  205. private function fetchContent($url, $method, $body) {
  206. $ch = curl_init();
  207. if($method == 'POST') {
  208. curl_setopt($ch, CURLOPT_POST, 1);//post提交方式
  209. curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  210. } else {
  211. $url .= '?'.$body;
  212. }
  213. curl_setopt($ch, CURLOPT_URL, $url);
  214. curl_setopt($ch, CURLOPT_TIMEOUT, 5);
  215. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  216. curl_setopt($ch, CURLOPT_HTTPHEADER, array(
  217. "x-sdk-client" => "php/2.0.0"
  218. ));
  219. if(substr($url, 0,5) == 'https') {
  220. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  221. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  222. }
  223. $rtn = curl_exec($ch);
  224. if($rtn === false) {
  225. // 大多由设置等原因引起,一般无法保障后续逻辑正常执行,
  226. // 所以这里触发的是E_USER_ERROR,会终止脚本执行,无法被try...catch捕获,需要用户排查环境、网络等故障
  227. throw new \RuntimeException("[CURL_" . curl_errno($ch) . "]: " . curl_error($ch));
  228. }
  229. curl_close($ch);
  230. return $rtn;
  231. }
  232. }