Răsfoiți Sursa

新群上报上架包变动
添加googleplay包检测
google包添加googlepay支持

Tree 3 zile în urmă
părinte
comite
baa8ea719c

+ 486 - 0
app/Console/Commands/CheckGooglePlayStore.php

@@ -0,0 +1,486 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Notification\TelegramBot;
+use App\Util;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Redis;
+
+/**
+ * 定时检测 Google Play 上架包是否仍可访问(大致对应「未下架」)。
+ *
+ * 规则与 iOS 侧类似:
+ * - online   : HTTP 200 且页面含正常 Play 详情特征
+ * - delisted : HTTP 404,或 200 但为 Play「Not Found」错误页
+ * - unknown  : 超时、429、5xx、页面过短无法判断等
+ *
+ * 连续 delisted / unknown 达阈值才告警;恢复 online 会清计数与已通知标记。
+ */
+class CheckGooglePlayStore extends Command
+{
+    protected $signature = 'google:check-play-store';
+
+    protected $description = '检测 Google Play 包是否疑似下架,告警时 Telegram sendMsgAndImportant';
+
+    /** 展示名 => Play 详情页完整 URL(须含 id= 包名) */
+    protected array $apps = [
+        'google-AFVegas-Nights-228' => 'https://play.google.com/store/apps/details?id=com.oipjuwenkjhde.kjhbnmuiejnd',
+        'google-PrimeAF-Gaming-229' => 'https://play.google.com/store/apps/details?id=com.rewubsbdqkk.koeesowemli',
+        'google-test111' => 'https://play.google.com/store/apps/details?id=com.rewubsbdqkk.123',
+    ];
+
+    protected int $delistedConfirmTimes = 3;
+
+    protected int $unknownConfirmTimes = 3;
+
+    protected int $counterExpireSeconds = 3600;
+
+    protected int $httpMaxAttempts = 3;
+
+    protected int $sleepMinMicroseconds = 200000;
+
+    protected int $sleepMaxMicroseconds = 500000;
+
+    public function handle()
+    {
+        if ($this->apps === []) {
+            $this->warn('未配置 Google Play 检测列表(CheckGooglePlayStore::$apps),跳过。');
+
+            return 0;
+        }
+
+        $env = env('APP_ENV', 'local');
+
+        $results = [];
+        $delistedNotified = [];
+        $unknownNotified = [];
+
+        foreach ($this->apps as $name => $url) {
+            $result = $this->checkSingleApp($name, $url);
+            $results[] = $result;
+
+            Util::WriteLog(
+                'google_play_check',
+                sprintf(
+                    '[%s] %s | %s | %s',
+                    strtoupper($result['status']),
+                    $name,
+                    $url,
+                    $result['reason']
+                )
+            );
+
+            $pkg = $result['package'];
+
+            if ($result['status'] === 'online') {
+                $this->clearCounters($pkg);
+            } elseif ($result['status'] === 'delisted') {
+                $count = $this->increaseCounter($this->getDelistedCounterKey($pkg));
+                $this->clearCounter($this->getUnknownCounterKey($pkg));
+
+                if ($count >= $this->delistedConfirmTimes) {
+                    if (!$this->hasAlreadyNotified('delisted', $pkg)) {
+                        $delistedNotified[] = [
+                            'name'   => $name,
+                            'url'    => $url,
+                            'reason' => $result['reason'],
+                            'count'  => $count,
+                        ];
+                        $this->markNotified('delisted', $pkg);
+                    }
+                }
+            } elseif ($result['status'] === 'unknown') {
+                $count = $this->increaseCounter($this->getUnknownCounterKey($pkg));
+                $this->clearCounter($this->getDelistedCounterKey($pkg));
+
+                if ($count >= $this->unknownConfirmTimes) {
+                    if (!$this->hasAlreadyNotified('unknown', $pkg)) {
+                        $unknownNotified[] = [
+                            'name'   => $name,
+                            'url'    => $url,
+                            'reason' => $result['reason'],
+                            'count'  => $count,
+                        ];
+                        $this->markNotified('unknown', $pkg);
+                    }
+                }
+            }
+
+            usleep(random_int($this->sleepMinMicroseconds, $this->sleepMaxMicroseconds));
+        }
+
+        if (!empty($delistedNotified)) {
+            $this->notifyDelisted($env, $delistedNotified);
+            $this->warn('已发送下架告警: ' . count($delistedNotified) . ' 个');
+        }
+
+        if (!empty($unknownNotified)) {
+            $this->notifyUnknown($env, $unknownNotified);
+            $this->warn('已发送检测异常告警: ' . count($unknownNotified) . ' 个');
+        }
+
+        foreach ($results as $item) {
+            $this->line(sprintf(
+                '[%s] %s - %s',
+                strtoupper($item['status']),
+                $item['name'],
+                $item['reason']
+            ));
+        }
+
+        if (empty($delistedNotified) && empty($unknownNotified)) {
+            $this->info('本次检测完成,无需告警。');
+        }
+
+        return empty($delistedNotified) ? 0 : 1;
+    }
+
+    /**
+     * @return array{name:string,url:string,package:string,status:string,reason:string}
+     */
+    protected function checkSingleApp(string $name, string $playUrl): array
+    {
+        $parsed = $this->parsePlayStoreUrl($playUrl);
+
+        if ($parsed === null) {
+            return [
+                'name'     => $name,
+                'url'      => $playUrl,
+                'package'  => '_badurl_' . md5($playUrl),
+                'status'   => 'unknown',
+                'reason'   => '无法从 URL 解析包名 id=',
+            ];
+        }
+
+        $package = $parsed['package'];
+        $requestUrl = $this->buildDetailsUrl($package);
+
+        $resp = $this->httpGetWithRetry($requestUrl, $this->httpMaxAttempts);
+
+        if (!empty($resp['error'])) {
+            return [
+                'name'     => $name,
+                'url'      => $playUrl,
+                'package'  => $package,
+                'status'   => 'unknown',
+                'reason'   => '请求失败: ' . $resp['error'],
+            ];
+        }
+
+        $code = (int) ($resp['code'] ?? 0);
+        $body = (string) ($resp['body'] ?? '');
+
+        if (in_array($code, [403, 429, 500, 502, 503, 504], true)) {
+            return [
+                'name'     => $name,
+                'url'      => $playUrl,
+                'package'  => $package,
+                'status'   => 'unknown',
+                'reason'   => "HTTP {$code}",
+            ];
+        }
+
+        if ($code === 404) {
+            return [
+                'name'     => $name,
+                'url'      => $playUrl,
+                'package'  => $package,
+                'status'   => 'delisted',
+                'reason'   => 'Play 返回 HTTP 404(包不存在或已下架)',
+            ];
+        }
+
+        if ($code !== 200) {
+            return [
+                'name'     => $name,
+                'url'      => $playUrl,
+                'package'  => $package,
+                'status'   => 'unknown',
+                'reason'   => "HTTP {$code}",
+            ];
+        }
+
+        if ($this->isPlayNotFoundPage($body)) {
+            return [
+                'name'     => $name,
+                'url'      => $playUrl,
+                'package'  => $package,
+                'status'   => 'delisted',
+                'reason'   => 'Play 错误页(Not Found 等)',
+            ];
+        }
+
+        if ($this->isLikelyNormalDetailPage($body)) {
+            $this->clearNotifiedFlags($package);
+
+            return [
+                'name'     => $name,
+                'url'      => $playUrl,
+                'package'  => $package,
+                'status'   => 'online',
+                'reason'   => '详情页正常(含 Play 前端特征)',
+            ];
+        }
+
+        $len = strlen($body);
+
+        if ($len < 15000) {
+            return [
+                'name'     => $name,
+                'url'      => $playUrl,
+                'package'  => $package,
+                'status'   => 'unknown',
+                'reason'   => "HTTP 200 但正文过短({$len} bytes),无法判断",
+            ];
+        }
+
+        return [
+            'name'     => $name,
+            'url'      => $playUrl,
+            'package'  => $package,
+            'status'   => 'unknown',
+            'reason'   => 'HTTP 200 但未识别为正常详情页(页面结构可能变更)',
+        ];
+    }
+
+    protected function isPlayNotFoundPage(string $body): bool
+    {
+        if (stripos($body, '<title>Not Found</title>') !== false) {
+            return true;
+        }
+
+        if (preg_match('/requested\s+URL\s+was\s+not\s+found/i', $body)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    protected function isLikelyNormalDetailPage(string $body): bool
+    {
+        if ($this->isPlayNotFoundPage($body)) {
+            return false;
+        }
+
+        if (strlen($body) < 30000) {
+            return false;
+        }
+
+        // 当前 Play Web 详情页常见内嵌数据;404 短页不含
+        if (strpos($body, 'WIZ_global_data') === false) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 从详情链接解析包名。
+     */
+    protected function parsePlayStoreUrl(string $url): ?array
+    {
+        $parts = parse_url($url);
+        if (empty($parts['host']) || stripos($parts['host'], 'play.google.com') === false) {
+            return null;
+        }
+        $query = $parts['query'] ?? '';
+        if ($query === '') {
+            return null;
+        }
+        parse_str($query, $q);
+        if (empty($q['id']) || !is_string($q['id'])) {
+            return null;
+        }
+        $id = $q['id'];
+        if (!preg_match('/^[a-zA-Z][a-zA-Z0-9._]*$/', $id)) {
+            return null;
+        }
+
+        return ['package' => $id];
+    }
+
+    protected function buildDetailsUrl(string $package): string
+    {
+        return 'https://play.google.com/store/apps/details?id=' . rawurlencode($package) . '&hl=en&gl=US';
+    }
+
+    /**
+     * @return array{code:int,body:string,error:string}
+     */
+    protected function httpGetWithRetry(string $url, int $maxAttempts = 3): array
+    {
+        $attempt = 0;
+        $last = [
+            'code'  => 0,
+            'body'  => '',
+            'error' => '',
+        ];
+
+        while ($attempt < $maxAttempts) {
+            $attempt++;
+
+            $ch = curl_init($url);
+            curl_setopt_array($ch, [
+                CURLOPT_RETURNTRANSFER => true,
+                CURLOPT_FOLLOWLOCATION => true,
+                CURLOPT_TIMEOUT        => 45,
+                CURLOPT_CONNECTTIMEOUT => 15,
+                CURLOPT_SSL_VERIFYPEER => true,
+                CURLOPT_HTTPHEADER     => [
+                    'Accept: text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
+                    'Accept-Language: en-US,en;q=0.9',
+                    'Cache-Control: no-cache',
+                ],
+                CURLOPT_USERAGENT      => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
+            ]);
+
+            $body = curl_exec($ch);
+            $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+            $error = curl_error($ch);
+            curl_close($ch);
+
+            $last = [
+                'code'  => $code,
+                'body'  => is_string($body) ? $body : '',
+                'error' => $error ?: '',
+            ];
+
+            if ($last['error'] === '' && ($code === 200 || $code === 404)) {
+                return $last;
+            }
+
+            $shouldRetry = $last['error'] !== '' || in_array($code, [429, 500, 502, 503, 504], true);
+
+            if (!$shouldRetry || $attempt >= $maxAttempts) {
+                return $last;
+            }
+
+            $sleepSeconds = $attempt === 1 ? 2 : ($attempt === 2 ? 5 : 8);
+            sleep($sleepSeconds);
+        }
+
+        return $last;
+    }
+
+    protected function getDelistedCounterKey(string $package): string
+    {
+        return 'google:play:delisted:' . $package;
+    }
+
+    protected function getUnknownCounterKey(string $package): string
+    {
+        return 'google:play:unknown:' . $package;
+    }
+
+    protected function getDelistedNotifiedKey(string $package): string
+    {
+        return 'google:play:notified:delisted:' . $package;
+    }
+
+    protected function getUnknownNotifiedKey(string $package): string
+    {
+        return 'google:play:notified:unknown:' . $package;
+    }
+
+    protected function increaseCounter(string $key): int
+    {
+        $count = (int) Redis::incr($key);
+        Redis::expire($key, $this->counterExpireSeconds);
+
+        return $count;
+    }
+
+    protected function clearCounters(string $package): void
+    {
+        $this->clearCounter($this->getDelistedCounterKey($package));
+        $this->clearCounter($this->getUnknownCounterKey($package));
+    }
+
+    protected function clearCounter(string $key): void
+    {
+        try {
+            Redis::del($key);
+        } catch (\Throwable $e) {
+            Util::WriteLog('google_play_check', 'Redis del error: ' . $e->getMessage());
+        }
+    }
+
+    protected function hasAlreadyNotified(string $type, string $package): bool
+    {
+        try {
+            $key = $type === 'delisted'
+                ? $this->getDelistedNotifiedKey($package)
+                : $this->getUnknownNotifiedKey($package);
+
+            return (bool) Redis::exists($key);
+        } catch (\Throwable $e) {
+            Util::WriteLog('google_play_check', 'Redis exists error: ' . $e->getMessage());
+
+            return false;
+        }
+    }
+
+    protected function markNotified(string $type, string $package): void
+    {
+        try {
+            $key = $type === 'delisted'
+                ? $this->getDelistedNotifiedKey($package)
+                : $this->getUnknownNotifiedKey($package);
+
+            Redis::setex($key, $this->counterExpireSeconds, 1);
+        } catch (\Throwable $e) {
+            Util::WriteLog('google_play_check', 'Redis setex error: ' . $e->getMessage());
+        }
+    }
+
+    protected function clearNotifiedFlags(string $package): void
+    {
+        try {
+            Redis::del(
+                $this->getDelistedNotifiedKey($package),
+                $this->getUnknownNotifiedKey($package)
+            );
+        } catch (\Throwable $e) {
+            Util::WriteLog('google_play_check', 'Redis clear notified flags error: ' . $e->getMessage());
+        }
+    }
+
+    protected function notifyDelisted(string $env, array $items): void
+    {
+        $lines = ["[{$env}] Google Play 下架/不可访问检测告警"];
+
+        foreach ($items as $item) {
+            $lines[] = '• ' . $item['name'];
+            $lines[] = '  原因: ' . $item['reason'];
+            $lines[] = '  连续次数: ' . $item['count'];
+            $lines[] = '  链接: ' . $item['url'];
+        }
+
+        $this->sendTelegramMessage(implode("\n", $lines));
+    }
+
+    protected function notifyUnknown(string $env, array $items): void
+    {
+        $lines = ["[{$env}] Google Play 检测异常告警"];
+
+        foreach ($items as $item) {
+            $lines[] = '• ' . $item['name'];
+            $lines[] = '  原因: ' . $item['reason'];
+            $lines[] = '  连续次数: ' . $item['count'];
+            $lines[] = '  链接: ' . $item['url'];
+        }
+
+        $this->sendTelegramMessage(implode("\n", $lines));
+    }
+
+    protected function sendTelegramMessage(string $message): void
+    {
+        try {
+            TelegramBot::getDefault()->sendMsgAndImportant($message);
+        } catch (\Throwable $e) {
+            Util::WriteLog('google_play_check', 'Telegram send error: ' . $e->getMessage());
+            $this->error('Telegram 发送失败: ' . $e->getMessage());
+        }
+    }
+}

+ 6 - 4
app/Console/Commands/CheckIosAppStore.php

@@ -33,9 +33,10 @@ class CheckIosAppStore extends Command
 
     /** 要检测的 iOS 包列表:name => url */
     protected array $apps = [
-        'Slots Magnet Revolution' => 'https://apps.apple.com/us/app/slots-magnet-revolution/id6759797102',
-        'Viking Mysteric Slots'   => 'https://apps.apple.com/us/app/viking-mysteric-slots/id6759851510',
-        'Slots Chemistry Pirate'  => 'https://apps.apple.com/us/app/slots-chemistry-pirate/id6759852588',
+        'Slots Magnet Revolution 206' => 'https://apps.apple.com/us/app/slots-magnet-revolution/id6759797102',
+        'Viking Mysteric Slots 207'   => 'https://apps.apple.com/us/app/viking-mysteric-slots/id6759851510',
+        'Slots Chemistry Pirate 208'  => 'https://apps.apple.com/us/app/slots-chemistry-pirate/id6759852588',
+//        'ios test'  => 'https://apps.apple.com/us/app/slots-chemistry-pirate/id67598525',
         // 'ceshi' => 'https://apps.apple.com/us/app/pixiepineslotsgrid/id6758513278',
     ];
 
@@ -493,7 +494,8 @@ class CheckIosAppStore extends Command
     protected function sendTelegramMessage(string $message): void
     {
         try {
-            TelegramBot::getDefault()->sendMsg($message);
+//            TelegramBot::getDefault()->sendMsg($message);
+            TelegramBot::getDefault()->sendMsgAndImportant($message);
         } catch (\Throwable $e) {
             Util::WriteLog('ios_app_store_check', 'Telegram send error: ' . $e->getMessage());
             $this->error('Telegram 发送失败: ' . $e->getMessage());

+ 4 - 1
app/Console/Kernel.php

@@ -2,6 +2,7 @@
 
 namespace App\Console;
 
+use App\Console\Commands\CheckGooglePlayStore;
 use App\Console\Commands\CheckIosAppStore;
 use App\Console\Commands\CheckStockModeNegative;
 use App\Console\Commands\DbQueue;
@@ -39,6 +40,7 @@ class Kernel extends ConsoleKernel
         RecordUserScoreChangeStatistics::class,
         DecStock::class,
         CheckIosAppStore::class,
+        CheckGooglePlayStore::class,
         OnlineReport::class,
         DbQueue::class,
         RecordThreeGameYesterday::class,
@@ -65,7 +67,8 @@ class Kernel extends ConsoleKernel
         $schedule->command('RecordUserScoreChangeStatistics')->cron('03 0 * * * ')->description('用户金额变化明细按天按用户汇总');
         $schedule->command('superball:update-pool-stats')->everyMinute()->description('Superball 每分钟刷新奖池及展示统计');
         $schedule->command('online_report')->everyMinute()->description('每分钟统计曲线');
-        $schedule->command('ios:check-app-store')->everyFiveMinutes()->description('每5分钟检测 iOS App Store 包是否下架');
+        $schedule->command('ios:check-app-store')->everyMinute()->description('每5分钟检测 iOS App Store 包是否下架');
+        $schedule->command('google:check-play-store')->everyMinute()->description('每5分钟检测 Google Play 包是否下架');
 
 //        $schedule->command('record_three_game_yesterday')->cron('05 0 * * * ')->description('按天统计游戏人数--今日执行昨日');
     }

+ 58 - 0
app/Notification/TelegramBot.php

@@ -45,6 +45,9 @@ class TelegramBot
     }
     public function handle(){
         try {
+            $update = json_decode(file_get_contents('php://input'), true);
+
+            Util::WriteLog('telegram_update', json_encode($update, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
             return $this->telegram->handle();
         }catch (TelegramException $e){
             Util::WriteLog('telegram',$e);
@@ -75,6 +78,61 @@ class TelegramBot
             Util::WriteLog('telegram',$e->getMessage());
         }
     }
+
+    /**
+     * 定向发到「重要通知」群(.env:TELEGRAM_IMPORTANT_CHAT_IDS,逗号分隔多个 chat_id)。
+     * 与 sendMsg 的 sendToActiveChats 不同:不依赖 telegram 库里的活跃会话表。
+     */
+    public function sendImportantMsg($str)
+    {
+        $str = substr((string) $str, 0, 4000);
+        if ($str === '' || empty($this->telegram)) {
+            return;
+        }
+        foreach ($this->importantChatIds() as $chatId) {
+            try {
+                \Longman\TelegramBot\Request::sendMessage([
+                    'chat_id' => $chatId,
+                    'text'    => $str,
+                ]);
+            } catch (\Exception $e) {
+                Util::WriteLog('telegram', 'important chat_id=' . $chatId . ' ' . $e->getMessage());
+            }
+        }
+    }
+
+    /** 老群广播 + 重要群各发一份(关键告警用) */
+    public function sendMsgAndImportant($str)
+    {
+        $this->sendMsg($str);
+        $this->sendImportantMsg($str);
+    }
+
+    public function sendProgramNotifyImportant($compName = '', $str = '', $contentArr = [])
+    {
+        $env = env('APP_ENV');
+        $this->sendImportantMsg($env . " $compName 异常 ###\n [$str]\n" . json_encode($contentArr, JSON_PRETTY_PRINT));
+    }
+
+    /**
+     * @return int[]
+     */
+    private function importantChatIds()
+    {
+        $raw = env('TELEGRAM_IMPORTANT_CHAT_IDS', '');
+        if ($raw === null || $raw === '') {
+            return [];
+        }
+        $parts = preg_split('/\s*,\s*/', (string) $raw, -1, PREG_SPLIT_NO_EMPTY);
+        $ids = [];
+        foreach ($parts as $p) {
+            if (is_numeric($p)) {
+                $ids[] = (int) $p;
+            }
+        }
+        return $ids;
+    }
+
     public function sendMsgWithEnv($str){
         if(is_array($str))$str=json_encode($str);
 

+ 12 - 1
app/Util.php

@@ -1,6 +1,7 @@
 <?php
 namespace App;
 
+use App\Game\WebChannelConfig;
 use Exception;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Redis;
@@ -423,7 +424,7 @@ class Util {
 
     /**
      * 根据当前设备类型过滤充值档位的支付方式
-     * Android:隐藏 ApplePay (value/type == 4)
+     * Android:隐藏 ApplePay (value/type == 4);GooglePay (8) 仅当用户 Channel 在 WebChannelConfig 中 PlatformName=apk 时展示
      * iOS:隐藏 GooglePay (value/type == 8)
      *
      * @param string|array $gear
@@ -476,6 +477,16 @@ class Util {
                 return false;
             }
 
+            if ($deviceType === 'android' && $value === 8) {
+                if (!is_array($user) || !isset($user['Channel'])) {
+                    return false;
+                }
+                $channelConfig = WebChannelConfig::getByChannel($user['Channel']);
+                if (!$channelConfig || $channelConfig->PlatformName !== 'apk') {
+                    return false;
+                }
+            }
+
             if($value === 2){
                 if(is_array($user)){
                     if($user['vip'] < 2 || (strtotime($user['RegisterDate']) > time()-86400*2)){