setName('PolyChannelSmsWarn')->setDescription('聚合渠道游戏短信预警脚本'); } protected function execute(Input $input, Output $output) { $hour = date('H'); // 只在 8~24 点 之间执行 if ( $hour < 8 || $hour > 24 ) { $output->writeln(date('Y-m-d H:i:s')." 非预警时间段(8~24点),脚本结束\r\n"); return; } $list = Db::table('cy_polychannel_game_frozen')->alias('pgf') ->join('cy_polychannel_deposit pcd', 'pgf.channel_id=pcd.channel_id', 'LEFT') ->join('nw_complex_channel cc', 'pgf.channel_id=cc.id', 'LEFT') ->where(['pgf.status' => 1]) // ->where([ // // 'pgf.game_id' => ['in', [891,892]], // 'pgf.channel_id' => 45 // ]) ->group('pgf.channel_id') ->field('pgf.channel_id, pcd.mobile, pcd.total_advance, pcd.warn_amount, pcd.over_limit, cc.name as channel_name') ->select(); $channelNames = array_column($list, 'channel_name'); $output->writeln(date('Y-m-d H:i:s')." start - sum=".count($list)." - 渠道:".implode(',', $channelNames)); if ( !empty($list) ) { $complexLogic = new Complex; $redis = Cache::store('redis')->handler(); $noticeExpire = 60*60*3; // 单次预警间隔三小时 foreach ( $list as $row ) { try { if ( empty($row['mobile']) ) continue; if ( !is_object($redis) ) continue; $totalWarnAmount = bcadd($row['total_advance'] - $row['warn_amount'] + $row['over_limit'], '0', 2); // 剩余预警额度(总预付款-预警金额+可超额度) $channelTotalMoney = $complexLogic->getChannelGameTotalAmount($row['channel_id']); // 渠道分成总流水 if ( $totalWarnAmount > $channelTotalMoney ) continue; // 记录第一次执行 $intKey = "wlsh:polyChannelSmsWarn:{$row['channel_id']}"; $initNum = $redis->get($intKey); if($initNum === false){ $initNum = 0; $redis->set($intKey, 1, strtotime('tomorrow') - time()); } // $initNum = 0; $dayKey = "wlsh:polyChannelSmsWarn:{$row['channel_id']}:".date('md'); $notify_day_count = (int)$redis->get($dayKey); // 8~24 三小时发送一次; 第一次立即发送; if ($notify_day_count <= 0 || $initNum == 0) { $remainAmount = bcadd($row['total_advance'] - $channelTotalMoney, '0',2); // 预付款剩余额度 $output->writeln("## {$row['channel_name']}-渠道, 剩余预警额度: 渠道分成总流水=".$channelTotalMoney.", 预警剩余金额:{$row['warn_amount']}, 剩余额度=".$remainAmount); // $template = "\n## {$row['channel_name']}-渠道, 预付款余额不足,预警剩余金额:{$row['warn_amount']}, 剩余额度: {$remainAmount} 元;\n请多加关注,并通知相关渠道!\n时间:".date('Y-m-d H:i:s')."\n"; // $this->send_message_dd($template); // $output->writeln(date('H:i:s')." {$row['channel_id']}_DD 钉钉发送完成"); $template = "**{$row['channel_name']}-渠道**, 预付款余额不足,预警剩余金额:{$row['warn_amount']}, 剩余额度: **{$remainAmount} 元**;\n请多加关注,并通知相关渠道!\n时间:".date('Y-m-d H:i:s')."\n"; $this->send_message_feishu($template); $output->writeln(date('H:i:s')." {$row['channel_id']}_feishu 飞书发送完成"); $params = ['name' => $row['channel_name'], 'money' => $remainAmount]; $mobileRes = $this->send_message_mobile($row['mobile'], $params); $output->writeln(date('H:i:s')." {$row['channel_id']}_mobild= {$row['mobile']} 短信发送完成:{$mobileRes}"); $redis->incrby($dayKey, 1); $redis->expire($dayKey, $noticeExpire); } } catch (\Exception $e) { $output->writeln(date('H:i:s')." {$row["channel_id"]}_{$row["channel_name"]} 处理异常: ".$e->getMessage()); continue; } } } $output->writeln(date('Y-m-d H:i:s')." end\r\n\n"); } private function send_message_dd($template){ $ddurl = Env::get('dingtalk.notice_qdyfk_url'); curlDD($template, $ddurl,true); } private function send_message_feishu($template){ $feishuUrl = Env::get('feishu.notice_url', ''); // 使用 interactive 类型支持 Markdown 格式 $result = sendFeishuMessage($feishuUrl, $template, 'interactive', '⚠️ 祈盟-渠道预付款警告'); if (!$result['success']) { throw new \RuntimeException("飞书发送失败: " . $result['error']); } return true; } private function send_message_mobile($mobile, $params){ // return $this->sendMobileNotifyBatch($mobile, $params); $mobiles = explode(',', $mobile); foreach($mobiles as $m){ $this->sendMobileNotify($m, $params); } return "OK"; } // ## 阿里云短信 ## // 批量发送 private function sendMobileNotifyBatch($mobiles, $params){ $accessKeyId = 'LTAI5tMnwbRaFFNegZABs1i9'; $accessKeySecret = '1AFchDeuPoytXzcCD8HUU4gsMizwn3'; /** * { * "PhoneNumberJson": "[\"13121776520\", \"13057807309\"]", * "SignNameJson": "[\"祈盟\", \"祈盟\"]", * "TemplateCode": "SMS_495865300", * "TemplateParamJson": "[{\"name\":\"xxx\", \"money\":\"666\"}, {\"name\":\"xxx\", \"money\":\"666\"}]", * "SourceIp": "183.158.7.39" * } */ $mobilesList = explode(',', $mobiles); $phoneNumberJson = []; $signNameJson = []; $templateParamJson = []; foreach ($mobilesList as $v){ $phoneNumberJson[] = $v; $signNameJson[] = "祈盟"; $templateParamJson[] = $params; } $paramsData = [ 'PhoneNumberJson' => json_encode($phoneNumberJson), 'SignNameJson' => json_encode($signNameJson), 'TemplateCode' => 'SMS_495865300', 'TemplateParamJson' => json_encode($templateParamJson, JSON_UNESCAPED_UNICODE), ]; // var_dump(json_encode($paramsData)); $paramsData = array_merge($paramsData, [ "RegionId" => "cn-hangzhou", "Action" => "SendSms", "Version" => "2017-05-25", ]); $paramsData = array_merge([ "SignatureMethod" => "HMAC-SHA1", "SignatureNonce" => uniqid(mt_rand(0,0xffff), true), "SignatureVersion" => "1.0", "AccessKeyId" => $accessKeyId, "Timestamp" => gmdate("Y-m-d\TH:i:s\Z"), "Format" => "JSON", ], $paramsData); ksort($paramsData); $sortedQueryStringTmp = ""; foreach ($paramsData as $key => $value) { $sortedQueryStringTmp .= "&" . $this->encode($key) . "=" . $this->encode($value); } $stringToSign = "POST&%2F&" . $this->encode(substr($sortedQueryStringTmp, 1)); $sign = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&",true)); $signature = $this->encode($sign); $result = $this->fetchContent("http://dysmsapi.aliyuncs.com/", "POST", "Signature={$signature}{$sortedQueryStringTmp}"); $result = json_decode($result, true); // dump($result); return $result['Code']; } // 单条发送 private function sendMobileNotify($mobile, $params){ $accessKeyId = 'LTAI5tMnwbRaFFNegZABs1i9'; $accessKeySecret = '1AFchDeuPoytXzcCD8HUU4gsMizwn3'; $paramsData = [ 'PhoneNumbers' => $mobile, 'SignName' => '祈盟', 'TemplateCode' => 'SMS_495865300', 'TemplateParam' => json_encode($params, JSON_UNESCAPED_UNICODE), ]; $paramsData = array_merge($paramsData, [ "RegionId" => "cn-hangzhou", "Action" => "SendSms", "Version" => "2017-05-25", ]); $paramsData = array_merge([ "SignatureMethod" => "HMAC-SHA1", "SignatureNonce" => uniqid(mt_rand(0,0xffff), true), "SignatureVersion" => "1.0", "AccessKeyId" => $accessKeyId, "Timestamp" => gmdate("Y-m-d\TH:i:s\Z"), "Format" => "JSON", ], $paramsData); ksort($paramsData); $sortedQueryStringTmp = ""; foreach ($paramsData as $key => $value) { $sortedQueryStringTmp .= "&" . $this->encode($key) . "=" . $this->encode($value); } $stringToSign = "POST&%2F&" . $this->encode(substr($sortedQueryStringTmp, 1)); $sign = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&",true)); $signature = $this->encode($sign); $this->fetchContent("http://dysmsapi.aliyuncs.com/", "POST", "Signature={$signature}{$sortedQueryStringTmp}"); } private function encode($str) { $res = urlencode($str); $res = preg_replace("/\+/", "%20", $res); $res = preg_replace("/\*/", "%2A", $res); $res = preg_replace("/%7E/", "~", $res); return $res; } private function fetchContent($url, $method, $body) { $ch = curl_init(); if($method == 'POST') { curl_setopt($ch, CURLOPT_POST, 1);//post提交方式 curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } else { $url .= '?'.$body; } curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_TIMEOUT, 5); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, array( "x-sdk-client" => "php/2.0.0" )); if(substr($url, 0,5) == 'https') { curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); } $rtn = curl_exec($ch); if($rtn === false) { // 大多由设置等原因引起,一般无法保障后续逻辑正常执行, // 所以这里触发的是E_USER_ERROR,会终止脚本执行,无法被try...catch捕获,需要用户排查环境、网络等故障 throw new \RuntimeException("[CURL_" . curl_errno($ch) . "]: " . curl_error($ch)); } curl_close($ch); return $rtn; } }