Tree vor 6 Tagen
Ursprung
Commit
2dc7aa36ee

+ 5 - 5
app/Game/GlobalUserInfo.php

@@ -29,7 +29,7 @@ class GlobalUserInfo extends Model
         if(isset(self::$me)&&!empty(self::$me)&&!empty(self::$me->DefaultLanguage)){
             return substr(self::$me->DefaultLanguage,0,2);
         }else{
-            return substr( @$_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2 );
+            return env('DEFAULT_LOCALE','en');
         }
     }
     public static function getLocaleByUserID($UserID,$default='en')
@@ -215,7 +215,8 @@ class GlobalUserInfo extends Model
                 $user->update(['GameID' => $data['GameID']]);
             }
 
-            $scoreData = self::getScoreDataByUserID($data['UserID']);
+
+            $scoreData = self::getScoreDataByUserID($data['UserID'],!WebChannelConfig::getByChannel($user->Channel)->isRealMoneyReg());
 
             $data['Score'] = $scoreData['Score'];
             $data['InsureScore'] = $scoreData['InsureScore'];
@@ -247,19 +248,18 @@ class GlobalUserInfo extends Model
     }
 
 
-    public static function getScoreDataByUserID($userID)
+    public static function getScoreDataByUserID($userID,$changeInsureScore=true)
     {
         if(!isset(self::$userToScoresData[$userID])) {
             $scoreObj=GameScoreInfo::query()->select(['Score','InsureScore'])->where('UserID', $userID)->first();
             if($scoreObj){
                 self::$userToScoresData[$userID]['Score'] = intval($scoreObj->Score) / NumConfig::NUM_VALUE;
-
                 self::$userToScoresData[$userID]['InsureScore'] = intval($scoreObj->InsureScore) / NumConfig::NUM_VALUE;
                 $user_recharge = DB::table(TableName::QPAccountsDB() . 'YN_VIPAccount')
                     ->where('UserID', $userID)
                     ->value('Recharge') ?: 0;
                 self::$userToScoresData[$userID]['Recharge'] = $user_recharge;
-                if(!$user_recharge){
+                if(!$user_recharge&&$changeInsureScore){
                     self::$userToScoresData[$userID]['Score'] = 0;
                     self::$userToScoresData[$userID]['InsureScore'] = intval($scoreObj->Score) / NumConfig::NUM_VALUE;
                 }

+ 6 - 14
app/Game/WebChannelConfig.php

@@ -8,8 +8,8 @@ use Illuminate\Support\Facades\Redis;
 define('SPECIAL_MODE_GUEST', 1);
 define('SPECIAL_MODE_ZERO_MONEY', 2);//注册不送钱
 define('SPECIAL_DISABLE_PROMOTE_INSTALL', 4);//提示安装
-define('SPECIAL_MODE_PWA_BONUS', 8);//安装app送钱
-define('SPECIAL_MODE_SMS_BONUS', 16);//手机验证送钱
+//define('SPECIAL_MODE_ENABLE_INSURE_SCORE', 8);//直接启用第二货币  GameScoreInfo InsureScore
+define('SPECIAL_MODE_REG_TO_REALMONEY', 16);//默认注册送的钱是真钱不是第二货币
 define('SPECIAL_MODE_MAIL_BONUS', 32);//MAIL验证送钱
 define('SPECIAL_MODE_FIRSTPAY_OFF30', 64);//首冲打开30%bonus
 define('SPECIAL_MODE_DEBUG_EVENT', 128);//是否开启事件日志上保
@@ -71,26 +71,18 @@ class WebChannelConfig extends Model
     {
         return explode('|',$this->BonusArr)[2]??00;
     }
-    public static function GuestOpen($config)
-    {
-        return ($config->SpecialMode&SPECIAL_MODE_GUEST)==SPECIAL_MODE_GUEST;
-    }
-    public function isFistPay30()
+    public function isRealMoneyReg()
     {
-        return ($this->SpecialMode&SPECIAL_MODE_SMS_BONUS)==SPECIAL_MODE_SMS_BONUS;
+        return ($this->SpecialMode&SPECIAL_MODE_REG_TO_REALMONEY)==SPECIAL_MODE_REG_TO_REALMONEY;
     }
-    public function isSmsBonus()
+    public static function GuestOpen($config)
     {
-        return ($this->SpecialMode&SPECIAL_MODE_SMS_BONUS)==SPECIAL_MODE_SMS_BONUS;
+        return ($config->SpecialMode&SPECIAL_MODE_GUEST)==SPECIAL_MODE_GUEST;
     }
     public function isDisablePromote()
     {
         return ($this->SpecialMode&SPECIAL_DISABLE_PROMOTE_INSTALL)==SPECIAL_DISABLE_PROMOTE_INSTALL;
     }
-    public function isPwaBonus()
-    {
-        return ($this->SpecialMode&SPECIAL_MODE_PWA_BONUS)==SPECIAL_MODE_PWA_BONUS;
-    }
     public function isDebugEvent()
     {
         return ($this->SpecialMode&SPECIAL_MODE_DEBUG_EVENT)==SPECIAL_MODE_DEBUG_EVENT;

+ 8 - 2
app/Http/Controllers/Admin/WebChannelConfigController.php

@@ -26,7 +26,7 @@ class WebChannelConfigController
         2 => 'ZERO_MONEY (注册不送钱)',
         4 => 'DISABLE_PROMOTE_INSTALL (提示安装)',
         8 => 'PWA_BONUS (安装app送钱)',
-        16 => 'SMS_BONUS (手机验证送钱)',
+        16 => 'REG_TO_REALMONEY (注册返回就是真金)',
         32 => 'MAIL_BONUS (MAIL验证送钱)',
         64 => 'FIRSTPAY_OFF30 (首冲打开30%bonus)',
         128 => 'DEBUG_EVENT (是否开启事件日志上报)',
@@ -175,7 +175,7 @@ class WebChannelConfigController
     {
         $data = $request->all();
         $info = WebChannelConfig::findOrFail($id);
-
+        
         $validator = Validator::make($data, [
             'PackageName' => 'required|string|max:200',
         ]);
@@ -319,6 +319,12 @@ class WebChannelConfigController
             }
         }
 
+        //同步更新 ChannelPackage
+        $channelNumber = DB::connection('write')->table('QPPlatformDB.dbo.ChannelPackageName')
+            ->where('Channel', $channel);
+        if ($channelNumber->exists()) {
+            $channelNumber->update(['Remarks' => $remark]);
+        }
         // 同步更新 dcat-admin.channel 表
         $dcatChannel = DB::connection('mysql')->table('dcat-admin.channel')->where('channel', $channel);
         if ($dcatChannel->exists()) {

+ 571 - 0
app/Http/Controllers/Admin/WebLogController.php

@@ -0,0 +1,571 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\File;
+
+/**
+ * Web日志行为分析控制器
+ */
+class WebLogController extends Controller
+{
+    /**
+     * 日志列表页面
+     */
+    public function index(Request $request)
+    {
+        // 获取可用日期列表
+        $availableDates = $this->getAvailableDates();
+
+        // 如果没有指定日期,使用最新的日期
+        $defaultDate = !empty($availableDates) ? $availableDates[0]['value'] : date('Ymd');
+
+        $date = $request->input('date', $defaultDate);
+        $channel = $request->input('channel', '');
+        $act = $request->input('act', '');
+        $md5 = $request->input('md5', '');
+        $ip = $request->input('ip', '');
+        $page = $request->input('page', 1);
+        $pageSize = $request->input('pageSize', 100); // 默认100条
+
+        // 读取日志文件
+        $logFile = storage_path("logs/{$date}_weblog.log");
+
+        if (!File::exists($logFile)) {
+            return view('admin.weblog.index', [
+                'logs' => [],
+                'total' => 0,
+                'date' => $date,
+                'channel' => $channel,
+                'act' => $act,
+                'md5' => $md5,
+                'ip' => $ip,
+                'page' => $page,
+                'pageSize' => $pageSize,
+                'availableDates' => $availableDates,
+                'availableChannels' => [],
+                'stats' => [],
+                'error' => "日志文件不存在: {$date}_weblog.log"
+            ]);
+        }
+
+        // 解析日志
+        $logs = $this->parseLogFile($logFile, $channel, $act, $md5, $ip);
+
+        // 标记相同IP的记录组
+        $logs = $this->markIpGroups($logs);
+
+        // 分页
+        $total = count($logs);
+        $logs = array_slice($logs, ($page - 1) * $pageSize, $pageSize);
+
+        // 统计数据
+        $stats = $this->getStatistics($logFile);
+
+        // 获取可用渠道列表(从统计数据中提取)
+        $availableChannels = array_keys($stats['channels']);
+        sort($availableChannels);
+
+        return view('admin.weblog.index', [
+            'logs' => $logs,
+            'total' => $total,
+            'date' => $date,
+            'channel' => $channel,
+            'act' => $act,
+            'md5' => $md5,
+            'ip' => $ip,
+            'page' => $page,
+            'pageSize' => $pageSize,
+            'availableDates' => $availableDates,
+            'availableChannels' => $availableChannels,
+            'stats' => $stats,
+            'error' => null
+        ]);
+    }
+
+    /**
+     * 解析日志文件
+     */
+    private function parseLogFile($logFile, $filterChannel = '', $filterAct = '', $filterMd5 = '', $filterIp = '')
+    {
+        $logs = [];
+        $content = File::get($logFile);
+
+        // 按空行分割日志条目
+        $entries = preg_split('/\n\n+/', $content);
+
+        foreach ($entries as $entry) {
+            if (empty(trim($entry))) continue;
+
+            $lines = explode("\n", $entry);
+            $logData = [
+                'date' => '',
+                'ip' => '',
+                'agent' => '',
+                'locale' => '',
+                'referer' => '',
+                'channel' => '',
+                'act' => '',
+                'md5' => '',
+                'orderid' => '',
+                'remark' => '',
+                'browser' => '',
+                'v' => '',
+                'in_fb' => false,
+                'os' => '',
+                'device' => ''
+            ];
+
+            foreach ($lines as $line) {
+                // 解析日期
+                if (preg_match('/^date:\s*(.+)$/', $line, $matches)) {
+                    $logData['date'] = trim($matches[1]);
+                }
+
+                // 解析IP和Agent
+                if (preg_match('/^ip:\s*([^\s]+).*agent:(.+?)locale:(.+)$/', $line, $matches)) {
+                    $logData['ip'] = trim($matches[1]);
+                    $logData['agent'] = trim($matches[2]);
+                    $logData['locale'] = trim($matches[3]);
+
+                    // 分析UserAgent
+                    $analysis = $this->analyzeUserAgent($logData['agent']);
+                    $logData['in_fb'] = $analysis['in_fb'];
+                    $logData['os'] = $analysis['os'];
+                    $logData['device'] = $analysis['device'];
+                }
+
+                // 解析referer
+                if (preg_match('/^referer:\s*(.+)$/', $line, $matches)) {
+                    $logData['referer'] = trim($matches[1]);
+                }
+
+                // 解析content (JSON)
+                if (preg_match('/^content:\s*(\{.+\})$/', $line, $matches)) {
+                    $json = json_decode($matches[1], true);
+                    if ($json) {
+                        $logData['channel'] = $this->extractChannel($json['host'] ?? '');
+                        $logData['act'] = $json['act'] ?? '';
+                        $logData['md5'] = $json['md5'] ?? '';
+                        $logData['orderid'] = $json['orderid'] ?? '';
+                        $logData['remark'] = $json['remark'] ?? '';
+                        $logData['browser'] = $json['browser'] ?? '';
+                        $logData['v'] = $json['v'] ?? '';
+                    }
+                }
+            }
+
+            // 过滤测试人员IP
+            if (strstr($logData['ip'], '116.86.210.235')) continue;
+
+            // 筛选
+            if ($filterChannel && $logData['channel'] !== $filterChannel) continue;
+            if ($filterAct && $logData['act'] !== $filterAct) continue;
+            if ($filterMd5 && $logData['md5'] !== $filterMd5) continue;
+            if ($filterIp && $logData['ip'] !== $filterIp) continue;
+
+            if (!empty($logData['date'])) {
+                $logs[] = $logData;
+            }
+        }
+
+        // 按时间正序
+        usort($logs, function($a, $b) {
+            return strcmp($a['date'], $b['date']);
+        });
+
+        return $logs;
+    }
+
+    /**
+     * 提取渠道编号 (host最后的数字部分)
+     */
+    private function extractChannel($host)
+    {
+        if (preg_match('/_(\d+)$/', $host, $matches)) {
+            return $matches[1];
+        }
+        return $host;
+    }
+
+    /**
+     * 分析UserAgent
+     */
+    private function analyzeUserAgent($agent)
+    {
+        $result = [
+            'in_fb' => false,
+            'os' => 'Unknown',
+            'device' => 'Unknown'
+        ];
+
+        // 检测是否在Facebook中
+        if (stripos($agent, 'FBAN') !== false ||
+            stripos($agent, 'FBAV') !== false ||
+            stripos($agent, 'IAB') !== false ||
+            stripos($agent, 'FACEBOOK') !== false ||
+            stripos($agent, 'FB_IAB') !== false ||
+            stripos($agent, 'FB4A') !== false) {
+            $result['in_fb'] = true;
+        }
+
+        // 检测iOS设备及型号
+        if (stripos($agent, 'iPhone') !== false) {
+            $result['os'] = 'iOS';
+            // 提取iPhone型号
+            if (preg_match('/iPhone\s*(\d+[,_]\d+)?/i', $agent, $matches)) {
+                if (!empty($matches[1])) {
+                    $result['device'] = 'iPhone ' . str_replace(['_', ','], ['.', '.'], $matches[1]);
+                } else {
+                    $result['device'] = 'iPhone';
+                }
+            } else {
+                $result['device'] = 'iPhone';
+            }
+            // 提取iOS版本
+            if (preg_match('/OS\s+(\d+)[._](\d+)(?:[._](\d+))?/i', $agent, $matches)) {
+                $result['os'] = 'iOS ' . $matches[1] . '.' . $matches[2];
+            }
+        } elseif (stripos($agent, 'iPad') !== false) {
+            $result['os'] = 'iOS';
+            $result['device'] = 'iPad';
+            // 提取iOS版本
+            if (preg_match('/OS\s+(\d+)[._](\d+)(?:[._](\d+))?/i', $agent, $matches)) {
+                $result['os'] = 'iOS ' . $matches[1] . '.' . $matches[2];
+            }
+        }
+        // 检测Android设备及型号
+        elseif (stripos($agent, 'Android') !== false) {
+            // 提取Android版本
+            if (preg_match('/Android\s+([\d.]+)/i', $agent, $matches)) {
+                $result['os'] = 'Android ' . $matches[1];
+            } else {
+                $result['os'] = 'Android';
+            }
+
+            // 提取设备型号(常见厂商)
+            $deviceModel = 'Android';
+
+            // Samsung
+            if (preg_match('/SM-([A-Z0-9]+)/i', $agent, $matches)) {
+                $deviceModel = 'Samsung ' . $matches[1];
+            }
+            // Huawei
+            elseif (preg_match('/(HW-|HUAWEI\s?)([A-Z0-9\-]+)/i', $agent, $matches)) {
+                $deviceModel = 'Huawei ' . $matches[2];
+            }
+            // Xiaomi
+            elseif (preg_match('/(MI\s+|Redmi\s+|POCO\s+)([A-Z0-9\s]+)/i', $agent, $matches)) {
+                $deviceModel = trim($matches[1] . $matches[2]);
+            }
+            // Oppo
+            elseif (preg_match('/OPPO\s+([A-Z0-9]+)/i', $agent, $matches)) {
+                $deviceModel = 'OPPO ' . $matches[1];
+            }
+            // Vivo
+            elseif (preg_match('/vivo\s+([A-Z0-9]+)/i', $agent, $matches)) {
+                $deviceModel = 'vivo ' . $matches[1];
+            }
+            // OnePlus
+            elseif (preg_match('/ONEPLUS\s+([A-Z0-9]+)/i', $agent, $matches)) {
+                $deviceModel = 'OnePlus ' . $matches[1];
+            }
+            // Google Pixel
+            elseif (preg_match('/Pixel\s+([A-Z0-9\s]+)/i', $agent, $matches)) {
+                $deviceModel = 'Pixel ' . trim($matches[1]);
+            }
+            // 其他通用匹配 - 尝试提取Build/之前的型号
+            elseif (preg_match('/;\s*([A-Z0-9\-\s]+)\s+Build\//i', $agent, $matches)) {
+                $model = trim($matches[1]);
+                // 过滤掉Android版本号
+                $model = preg_replace('/Android[\s\d.]+/i', '', $model);
+                if (strlen($model) > 2 && strlen($model) < 30) {
+                    $deviceModel = trim($model);
+                }
+            }
+
+            $result['device'] = $deviceModel;
+        }
+        // Windows
+        elseif (stripos($agent, 'Windows') !== false) {
+            $result['device'] = 'PC';
+            // 提取Windows版本
+            if (preg_match('/Windows NT\s+([\d.]+)/i', $agent, $matches)) {
+                $winVersion = $matches[1];
+                $versionMap = [
+                    '10.0' => 'Windows 10/11',
+                    '6.3' => 'Windows 8.1',
+                    '6.2' => 'Windows 8',
+                    '6.1' => 'Windows 7',
+                    '6.0' => 'Windows Vista',
+                ];
+                $result['os'] = $versionMap[$winVersion] ?? 'Windows NT ' . $winVersion;
+            } else {
+                $result['os'] = 'Windows';
+            }
+        }
+        // macOS
+        elseif (stripos($agent, 'Mac OS') !== false || stripos($agent, 'Macintosh') !== false) {
+            $result['device'] = 'Mac';
+            // 提取macOS版本
+            if (preg_match('/Mac OS X\s+([\d_]+)/i', $agent, $matches)) {
+                $version = str_replace('_', '.', $matches[1]);
+                $result['os'] = 'macOS ' . $version;
+            } else {
+                $result['os'] = 'macOS';
+            }
+        }
+        // Linux
+        elseif (stripos($agent, 'Linux') !== false) {
+            $result['os'] = 'Linux';
+            $result['device'] = 'PC';
+        }
+
+        return $result;
+    }
+
+    /**
+     * 标记相同IP的记录组
+     * 先将日志按IP和时间窗口(5分钟)分组,然后重新排序
+     * 让相同IP在时间相近的记录集中在一起显示
+     */
+    private function markIpGroups($logs)
+    {
+        if (empty($logs)) {
+            return $logs;
+        }
+
+        // 第一步:将日志按IP和时间窗口分组(使用滑动窗口)
+        $ipTimeGroups = [];
+        $logAssigned = []; // 记录每条日志是否已被分配
+
+        foreach ($logs as $index => $log) {
+            if (isset($logAssigned[$index])) {
+                continue; // 已经被分配到组中,跳过
+            }
+
+            $ip = $log['ip'];
+            $time = strtotime($log['date']);
+
+            // 创建新组
+            $newGroup = [
+                'ip' => $ip,
+                'min_time' => $time,
+                'max_time' => $time,
+                'logs' => [$log]
+            ];
+            $logAssigned[$index] = true;
+
+            // 查找其他相同IP且时间在5分钟内的记录
+            foreach ($logs as $idx => $otherLog) {
+                if (isset($logAssigned[$idx]) || $idx === $index) {
+                    continue;
+                }
+
+                if ($otherLog['ip'] === $ip) {
+                    $otherTime = strtotime($otherLog['date']);
+                    // 检查是否在时间窗口内(与组内最小或最大时间相差5分钟内)
+                    if (abs($otherTime - $newGroup['min_time']) <= 300 ||
+                        abs($otherTime - $newGroup['max_time']) <= 300) {
+                        $newGroup['logs'][] = $otherLog;
+                        $newGroup['min_time'] = min($newGroup['min_time'], $otherTime);
+                        $newGroup['max_time'] = max($newGroup['max_time'], $otherTime);
+                        $logAssigned[$idx] = true;
+                    }
+                }
+            }
+
+            $ipTimeGroups[] = $newGroup;
+        }
+
+        // 第二步:按组的最早时间排序
+        usort($ipTimeGroups, function($a, $b) {
+            return $a['min_time'] - $b['min_time'];
+        });
+
+        // 第三步:重组日志,在每个组内按时间排序,并标记分隔线
+        $sortedLogs = [];
+        foreach ($ipTimeGroups as $index => $group) {
+            // 组内按时间排序
+            usort($group['logs'], function($a, $b) {
+                return strcmp($a['date'], $b['date']);
+            });
+
+            // 添加组内的所有日志
+            foreach ($group['logs'] as $logIndex => $log) {
+                // 第一条记录且不是第一组时,添加分隔标记
+                if ($logIndex === 0 && $index > 0) {
+                    $log['show_separator'] = true;
+                } else {
+                    $log['show_separator'] = false;
+                }
+                $sortedLogs[] = $log;
+            }
+        }
+
+        return $sortedLogs;
+    }
+
+    /**
+     * 获取统计数据
+     */
+    private function getStatistics($logFile)
+    {
+        $logs = $this->parseLogFile($logFile);
+
+        $stats = [
+            'total' => count($logs),
+            'channels' => [],
+            'actions' => [],
+            'in_fb_count' => 0,
+            'os_distribution' => [],
+            'unique_users_in_fb' => [],
+            'unique_users_not_in_fb' => []
+        ];
+
+        foreach ($logs as $log) {
+            // 渠道统计
+            if (!isset($stats['channels'][$log['channel']])) {
+                $stats['channels'][$log['channel']] = 0;
+            }
+            $stats['channels'][$log['channel']]++;
+
+            // 行为统计
+            if (!isset($stats['actions'][$log['act']])) {
+                $stats['actions'][$log['act']] = 0;
+            }
+            $stats['actions'][$log['act']]++;
+
+            // FB统计
+            if ($log['in_fb']) {
+                $stats['in_fb_count']++;
+            }
+
+            // OS统计
+            if (!isset($stats['os_distribution'][$log['os']])) {
+                $stats['os_distribution'][$log['os']] = 0;
+            }
+            $stats['os_distribution'][$log['os']]++;
+
+            // 唯一用户(区分FB内外)
+            if (!empty($log['md5'])) {
+                if ($log['in_fb']) {
+                    $stats['unique_users_in_fb'][$log['md5']] = true;
+                } else {
+                    $stats['unique_users_not_in_fb'][$log['md5']] = true;
+                }
+            }
+        }
+
+        // 排序
+        arsort($stats['channels']);
+        arsort($stats['actions']);
+        arsort($stats['os_distribution']);
+
+        $stats['unique_user_count_in_fb'] = count($stats['unique_users_in_fb']);
+        $stats['unique_user_count_not_in_fb'] = count($stats['unique_users_not_in_fb']);
+        $stats['unique_user_count'] = $stats['unique_user_count_in_fb'] + $stats['unique_user_count_not_in_fb'];
+        unset($stats['unique_users_in_fb']);
+        unset($stats['unique_users_not_in_fb']);
+
+        return $stats;
+    }
+
+    /**
+     * 获取可用日期列表
+     */
+    private function getAvailableDates()
+    {
+        $logPath = storage_path('logs');
+        $files = File::glob($logPath . '/*_weblog.log');
+
+        $dates = [];
+        foreach ($files as $file) {
+            $filename = basename($file);
+            if (preg_match('/^(\d{8})_weblog\.log$/', $filename, $matches)) {
+                $dates[] = [
+                    'value' => $matches[1],
+                    'label' => date('Y-m-d', strtotime($matches[1]))
+                ];
+            }
+        }
+
+        // 按日期倒序
+        usort($dates, function($a, $b) {
+            return strcmp($b['value'], $a['value']);
+        });
+
+        return $dates;
+    }
+
+    /**
+     * 导出CSV
+     */
+    public function export(Request $request)
+    {
+        $date = $request->input('date', date('Ymd'));
+        $channel = $request->input('channel', '');
+        $act = $request->input('act', '');
+        $md5 = $request->input('md5', '');
+        $ip = $request->input('ip', '');
+
+        $logFile = storage_path("logs/{$date}_weblog.log");
+
+        if (!File::exists($logFile)) {
+            return redirect()->back()->with('error', '日志文件不存在');
+        }
+
+        $logs = $this->parseLogFile($logFile, $channel, $act, $md5, $ip);
+
+        $filename = "weblog_{$date}.csv";
+        $headers = [
+            'Content-Type' => 'text/csv; charset=UTF-8',
+            'Content-Disposition' => "attachment; filename=\"{$filename}\"",
+        ];
+
+        $callback = function() use ($logs) {
+            $file = fopen('php://output', 'w');
+
+            // UTF-8 BOM
+            fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));
+
+            // CSV表头
+            fputcsv($file, [
+                '日期时间',
+                '渠道',
+                '行为',
+                '用户ID(MD5)',
+                'IP',
+                '是否FB',
+                '操作系统',
+                '设备',
+                '浏览器',
+                'OrderID',
+                '备注'
+            ]);
+
+            // CSV内容
+            foreach ($logs as $log) {
+                fputcsv($file, [
+                    $log['date'],
+                    $log['channel'],
+                    $log['act'],
+                    $log['md5'],
+                    $log['ip'],
+                    $log['in_fb'] ? 'Y' : 'N',
+                    $log['os'],
+                    $log['device'],
+                    $log['browser'],
+                    $log['orderid'],
+                    $log['remark']
+                ]);
+            }
+
+            fclose($file);
+        };
+
+        return response()->stream($callback, 200, $headers);
+    }
+}

+ 403 - 0
app/Http/Controllers/Admin/WebPushController.php

@@ -0,0 +1,403 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Minishlink\WebPush\WebPush;
+use Minishlink\WebPush\Subscription;
+
+/**
+ * Web Push后台管理控制器
+ */
+class WebPushController extends Controller
+{
+    /**
+     * 订阅列表页面
+     */
+    public function subscriptions(Request $request)
+    {
+        $query = DB::table('QPAccountsDB.dbo.WebPushSubscriptions');
+
+        // 筛选条件
+        if ($platform = $request->input('platform')) {
+            $query->where('Platform', $platform);
+        }
+        if ($channel = $request->input('channel')) {
+            $query->where('Channel', $channel);
+        }
+        if ($installed = $request->input('installed')) {
+            $query->where('Installed', $installed);
+        }
+        if ($status = $request->input('status', 1)) {
+            $query->where('Status', $status);
+        }
+        if ($userid = $request->input('userid')) {
+            $query->where('UserID', $userid);
+        }
+
+        $list = $query->orderBy('ID', 'desc')->paginate(20);
+
+        // 统计数据
+        $stats = [
+            'total' => DB::table('QPAccountsDB.dbo.WebPushSubscriptions')->where('Status', 1)->count(),
+            'android' => DB::table('QPAccountsDB.dbo.WebPushSubscriptions')->where('Status', 1)->where('Platform', 'Android')->count(),
+            'ios' => DB::table('QPAccountsDB.dbo.WebPushSubscriptions')->where('Status', 1)->where('Platform', 'iOS')->count(),
+            'installed' => DB::table('QPAccountsDB.dbo.WebPushSubscriptions')->where('Status', 1)->where('Installed', 1)->count(),
+            'today' => DB::table('QPAccountsDB.dbo.WebPushSubscriptions')
+                ->where('Status', 1)
+                ->whereRaw('CONVERT(date, CreatedAt) = CONVERT(date, GETDATE())')
+                ->count(),
+        ];
+
+        return view('admin.webpush.subscriptions', [
+            'list' => $list,
+            'stats' => $stats,
+            'platform' => $request->input('platform', ''),
+            'channel' => $request->input('channel', ''),
+            'installed' => $request->input('installed', ''),
+            'status' => $request->input('status', '1'),
+            'userid' => $request->input('userid', ''),
+        ]);
+    }
+
+    /**
+     * 广播列表页面
+     */
+    public function broadcasts(Request $request)
+    {
+        $query = DB::table('QPAccountsDB.dbo.WebPushBroadcasts');
+
+        if ($status = $request->input('status')) {
+            $query->where('Status', $status);
+        }
+
+        $list = $query->orderBy('ID', 'desc')->paginate(20);
+
+        return view('admin.webpush.broadcasts', [
+            'list' => $list,
+            'status' => $request->input('status', ''),
+        ]);
+    }
+
+    /**
+     * 创建广播页面
+     */
+    public function createBroadcast()
+    {
+        // 获取可选的渠道列表
+        $channels = DB::table('QPAccountsDB.dbo.WebPushSubscriptions')
+            ->select('Channel')
+            ->where('Status', 1)
+            ->distinct()
+            ->pluck('Channel');
+
+        return view('admin.webpush.create_broadcast', [
+            'channels' => $channels,
+        ]);
+    }
+
+    /**
+     * 保存广播
+     */
+    public function storeBroadcast(Request $request)
+    {
+        $request->validate([
+            'title' => 'required|max:255',
+            'body' => 'required',
+        ]);
+
+        try {
+            $title = $request->input('title');
+            $body = $request->input('body');
+            $url = $request->input('url', '');
+            $icon = $request->input('icon', '');
+            $image = $request->input('image', '');
+            $targetType = $request->input('target_type', 'all');
+            $targetValue = $request->input('target_value', []);
+            $filterInstalled = $request->input('filter_installed');
+            $sendNow = $request->input('send_now', false);
+
+            // 计算目标数量
+            $query = DB::table('QPAccountsDB.dbo.WebPushSubscriptions')
+                ->where('Status', 1);
+
+            if ($targetType === 'channel' && !empty($targetValue)) {
+                $query->whereIn('Channel', $targetValue);
+            } elseif ($targetType === 'platform' && !empty($targetValue)) {
+                $query->whereIn('Platform', $targetValue);
+            }
+
+            if ($filterInstalled !== null && $filterInstalled !== '') {
+                $query->where('Installed', $filterInstalled);
+            }
+
+            $totalCount = $query->count();
+
+            if ($totalCount === 0) {
+                return redirect()->back()->with('error', '没有找到符合条件的订阅用户');
+            }
+
+            // 创建广播记录
+            $broadcastId = DB::table('QPAccountsDB.dbo.WebPushBroadcasts')->insertGetId([
+                'Title' => $title,
+                'Body' => $body,
+                'Icon' => $icon,
+                'Image' => $image,
+                'URL' => $url,
+                'TargetType' => $targetType,
+                'TargetValue' => json_encode($targetValue),
+                'FilterInstalled' => $filterInstalled,
+                'Status' => $sendNow ? 'processing' : 'pending',
+                'TotalCount' => $totalCount,
+                'CreatedBy' => session('admin_user')['nickname'] ?? 'Admin',
+                'CreatedAt' => date('Y-m-d H:i:s'),
+                'UpdatedAt' => date('Y-m-d H:i:s')
+            ]);
+
+            // 如果立即发送
+            if ($sendNow) {
+                $this->processBroadcast($broadcastId);
+                return redirect()->route('admin.webpush.broadcasts')->with('success', "广播已发送,ID: {$broadcastId}, 目标用户: {$totalCount}");
+            }
+
+            return redirect()->route('admin.webpush.broadcasts')->with('success', "广播已创建,ID: {$broadcastId}, 目标用户: {$totalCount}");
+
+        } catch (\Exception $e) {
+            Log::error('Create broadcast error: ' . $e->getMessage());
+            return redirect()->back()->with('error', '创建失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 发送广播
+     */
+    public function sendBroadcast($id)
+    {
+        $broadcast = DB::table('QPAccountsDB.dbo.WebPushBroadcasts')
+            ->where('ID', $id)
+            ->first();
+
+        if (!$broadcast) {
+            return redirect()->back()->with('error', '广播不存在');
+        }
+
+        if ($broadcast->Status === 'completed') {
+            return redirect()->back()->with('error', '该广播已发送');
+        }
+
+        $result = $this->processBroadcast($id);
+
+        if ($result['success']) {
+            return redirect()->back()->with('success', $result['message']);
+        } else {
+            return redirect()->back()->with('error', $result['message']);
+        }
+    }
+
+    /**
+     * 禁用订阅
+     */
+    public function disableSubscription($id)
+    {
+        DB::table('QPAccountsDB.dbo.WebPushSubscriptions')
+            ->where('ID', $id)
+            ->update(['Status' => 0, 'UpdatedAt' => date('Y-m-d H:i:s')]);
+
+        return redirect()->back()->with('success', '订阅已禁用');
+    }
+
+    /**
+     * 删除订阅
+     */
+    public function deleteSubscription($id)
+    {
+        DB::table('QPAccountsDB.dbo.WebPushSubscriptions')
+            ->where('ID', $id)
+            ->delete();
+
+        return redirect()->back()->with('success', '订阅已删除');
+    }
+
+    /**
+     * 发送日志
+     */
+    public function sendLogs(Request $request)
+    {
+        $broadcastId = $request->input('broadcast_id');
+
+        $query = DB::table('QPAccountsDB.dbo.WebPushSendLogs as wps')
+            ->leftJoin('QPAccountsDB.dbo.WebPushSubscriptions as sub', 'wps.SubscriptionID', '=', 'sub.ID')
+            ->select('wps.*', 'sub.UserID', 'sub.FPID', 'sub.Platform');
+
+        if ($broadcastId) {
+            $query->where('wps.BroadcastID', $broadcastId);
+        }
+
+        $list = $query->orderBy('wps.ID', 'desc')->paginate(50);
+
+        return view('admin.webpush.send_logs', [
+            'list' => $list,
+            'broadcast_id' => $broadcastId ?? '',
+        ]);
+    }
+
+    /**
+     * 处理广播发送(核心逻辑)
+     */
+    private function processBroadcast($broadcastId)
+    {
+        try {
+            $broadcast = DB::table('QPAccountsDB.dbo.WebPushBroadcasts')
+                ->where('ID', $broadcastId)
+                ->first();
+
+            if (!$broadcast) {
+                return ['success' => false, 'message' => '广播不存在'];
+            }
+
+            // 更新状态为处理中
+            DB::table('QPAccountsDB.dbo.WebPushBroadcasts')
+                ->where('ID', $broadcastId)
+                ->update([
+                    'Status' => 'processing',
+                    'SentAt' => date('Y-m-d H:i:s'),
+                    'UpdatedAt' => date('Y-m-d H:i:s')
+                ]);
+
+            // 获取目标订阅
+            $query = DB::table('QPAccountsDB.dbo.WebPushSubscriptions')
+                ->where('Status', 1);
+
+            if ($broadcast->TargetType === 'channel') {
+                $channels = json_decode($broadcast->TargetValue, true);
+                $query->whereIn('Channel', $channels);
+            } elseif ($broadcast->TargetType === 'platform') {
+                $platforms = json_decode($broadcast->TargetValue, true);
+                $query->whereIn('Platform', $platforms);
+            }
+
+            if ($broadcast->FilterInstalled !== null) {
+                $query->where('Installed', $broadcast->FilterInstalled);
+            }
+
+            $subscriptions = $query->get();
+
+            // Web Push配置
+            $auth = [
+                'VAPID' => [
+                    'subject' => env('VAPID_SUBJECT', 'mailto:admin@example.com'),
+                    'publicKey' => env('VAPID_PUBLIC_KEY', ''),
+                    'privateKey' => env('VAPID_PRIVATE_KEY', ''),
+                ]
+            ];
+
+            $webPush = new WebPush($auth);
+
+            // 准备推送数据
+            $payload = json_encode([
+                'title' => $broadcast->Title,
+                'body' => $broadcast->Body,
+                'icon' => $broadcast->Icon,
+                'image' => $broadcast->Image,
+                'url' => $broadcast->URL,
+                'tag' => 'broadcast-' . $broadcastId,
+                'requireInteraction' => false
+            ]);
+
+            $successCount = 0;
+            $failedCount = 0;
+
+            // 批量发送
+            foreach ($subscriptions as $sub) {
+                try {
+                    $subscriptionInfo = json_decode($sub->SubscriptionInfo, true);
+
+                    $subscription = Subscription::create([
+                        'endpoint' => $subscriptionInfo['endpoint'],
+                        'keys' => $subscriptionInfo['keys'] ?? []
+                    ]);
+
+                    $webPush->queueNotification($subscription, $payload);
+
+                } catch (\Exception $e) {
+                    Log::error("Queue notification error for subscription {$sub->ID}: " . $e->getMessage());
+                }
+            }
+
+            // 发送所有排队的通知
+            foreach ($webPush->flush() as $report) {
+                $endpoint = $report->getEndpoint();
+
+                $subscription = collect($subscriptions)->first(function ($sub) use ($endpoint) {
+                    return $sub->Endpoint === $endpoint;
+                });
+
+                if ($subscription) {
+                    if ($report->isSuccess()) {
+                        $successCount++;
+                        $status = 'success';
+                        $errorMsg = null;
+
+                        // 更新订阅的推送统计
+                        DB::table('QPAccountsDB.dbo.WebPushSubscriptions')
+                            ->where('ID', $subscription->ID)
+                            ->update([
+                                'LastPushAt' => date('Y-m-d H:i:s'),
+                                'PushCount' => DB::raw('PushCount + 1')
+                            ]);
+                    } else {
+                        $failedCount++;
+                        $status = 'failed';
+                        $errorMsg = $report->getReason();
+
+                        // 如果订阅已失效,禁用它
+                        if (in_array($report->getStatusCode(), [404, 410])) {
+                            DB::table('QPAccountsDB.dbo.WebPushSubscriptions')
+                                ->where('ID', $subscription->ID)
+                                ->update(['Status' => 0]);
+                        }
+                    }
+
+                    // 记录发送日志
+                    DB::table('QPAccountsDB.dbo.WebPushSendLogs')->insert([
+                        'BroadcastID' => $broadcastId,
+                        'SubscriptionID' => $subscription->ID,
+                        'Status' => $status,
+                        'ErrorMsg' => $errorMsg,
+                        'SentAt' => date('Y-m-d H:i:s')
+                    ]);
+                }
+            }
+
+            // 更新广播状态
+            DB::table('QPAccountsDB.dbo.WebPushBroadcasts')
+                ->where('ID', $broadcastId)
+                ->update([
+                    'Status' => 'completed',
+                    'SuccessCount' => $successCount,
+                    'FailedCount' => $failedCount,
+                    'UpdatedAt' => date('Y-m-d H:i:s')
+                ]);
+
+            return [
+                'success' => true,
+                'message' => "推送完成! 成功: {$successCount}, 失败: {$failedCount}"
+            ];
+
+        } catch (\Exception $e) {
+            Log::error("Process broadcast error: " . $e->getMessage());
+
+            DB::table('QPAccountsDB.dbo.WebPushBroadcasts')
+                ->where('ID', $broadcastId)
+                ->update([
+                    'Status' => 'failed',
+                    'UpdatedAt' => date('Y-m-d H:i:s')
+                ]);
+
+            return ['success' => false, 'message' => '发送失败: ' . $e->getMessage()];
+        }
+    }
+}

+ 13 - 80
app/Http/Controllers/Admin/WeightConfigController.php

@@ -19,15 +19,8 @@ class WeightConfigController extends Controller
             $totalClicks[$id] = isset($totalRaw[$id]) ? intval($totalRaw[$id]) : 0;
         }
 
-        $showRaw = Redis::hgetall(ApiWeightConfigController::REDIS_KEY_SHOW);
-        $totalShows = [];
-        foreach (ApiWeightConfigController::VALID_IDS as $id) {
-            $totalShows[$id] = isset($showRaw[$id]) ? intval($showRaw[$id]) : 0;
-        }
-
-        $days = 3;
+        $days = 7;
         $dailyClicks = [];
-        $dailyShows = [];
         for ($i = $days - 1; $i >= 0; $i--) {
             $date = date('Y-m-d', strtotime("-{$i} days"));
             $raw = Redis::hgetall(ApiWeightConfigController::REDIS_KEY_CLICKS_DAILY_PREFIX . $date);
@@ -36,34 +29,9 @@ class WeightConfigController extends Controller
                 $row[$id] = isset($raw[$id]) ? intval($raw[$id]) : 0;
             }
             $dailyClicks[] = $row;
-
-            $rawShow = Redis::hgetall(ApiWeightConfigController::REDIS_KEY_SHOW_DAILY_PREFIX . $date);
-            $rowShow = ['date' => $date];
-            foreach (ApiWeightConfigController::VALID_IDS as $id) {
-                $rowShow[$id] = isset($rawShow[$id]) ? intval($rawShow[$id]) : 0;
-            }
-            $dailyShows[] = $rowShow;
         }
 
-        $today = date('Y-m-d');
-        $todayClickRaw = Redis::hgetall(ApiWeightConfigController::REDIS_KEY_CLICKS_DAILY_PREFIX . $today);
-        $todayShowRaw = Redis::hgetall(ApiWeightConfigController::REDIS_KEY_SHOW_DAILY_PREFIX . $today);
-        $todayClicks = [];
-        $todayShows = [];
-        foreach (ApiWeightConfigController::VALID_IDS as $id) {
-            $todayClicks[$id] = isset($todayClickRaw[$id]) ? intval($todayClickRaw[$id]) : 0;
-            $todayShows[$id] = isset($todayShowRaw[$id]) ? intval($todayShowRaw[$id]) : 0;
-        }
-
-        return view('admin.weight_config.index', compact(
-            'config',
-            'totalClicks',
-            'totalShows',
-            'todayClicks',
-            'todayShows',
-            'dailyClicks',
-            'dailyShows'
-        ));
+        return view('admin.weight_config.index', compact('config', 'totalClicks', 'dailyClicks'));
     }
 
     public function update(Request $request)
@@ -93,58 +61,23 @@ class WeightConfigController extends Controller
 
     public function resetStats(Request $request)
     {
-        $type = $request->input('type');
-        if (!$type) {
-            return response()->json(['status' => 'error', 'message' => '缺少操作类型']);
-        }
-        $prefix = config('database.redis.options.prefix', '');
-
-        $stripPrefix = function ($keys) use ($prefix) {
-            if (!$prefix || empty($keys)) {
-                return $keys;
-            }
-            return array_map(function ($k) use ($prefix) {
-                return strpos($k, $prefix) === 0 ? substr($k, strlen($prefix)) : $k;
-            }, $keys);
-        };
-
-        $hdelFieldFromDailyKeys = function (string $dailyPrefix, string $field) use ($stripPrefix) {
-            $keys = Redis::keys($dailyPrefix . '*');
-            foreach ($stripPrefix($keys) as $k) {
-                Redis::hdel($k, $field);
-            }
-        };
-
+        $type = $request->input('type', 'total');
         if ($type === 'all') {
             Redis::del(ApiWeightConfigController::REDIS_KEY_CLICKS);
-            Redis::del(ApiWeightConfigController::REDIS_KEY_SHOW);
-            foreach ([
-                         ApiWeightConfigController::REDIS_KEY_CLICKS_DAILY_PREFIX,
-                         ApiWeightConfigController::REDIS_KEY_SHOW_DAILY_PREFIX,
-                     ] as $dailyPrefix) {
-                $keys = Redis::keys($dailyPrefix . '*');
-                foreach ($stripPrefix($keys) as $k) {
+            $keys = Redis::keys(ApiWeightConfigController::REDIS_KEY_CLICKS_DAILY_PREFIX . '*');
+            if (!empty($keys)) {
+                $prefix = config('database.redis.options.prefix', '');
+                if ($prefix) {
+                    $keys = array_map(function ($k) use ($prefix) {
+                        return strpos($k, $prefix) === 0 ? substr($k, strlen($prefix)) : $k;
+                    }, $keys);
+                }
+                foreach ($keys as $k) {
                     Redis::del($k);
                 }
             }
-        } elseif ($type === 'click_id') {
-            $id = intval($request->input('id'));
-            if (!in_array($id, ApiWeightConfigController::VALID_IDS, true)) {
-                return response()->json(['status' => 'error', 'message' => '无效 id']);
-            }
-            $field = (string) $id;
-            Redis::hdel(ApiWeightConfigController::REDIS_KEY_CLICKS, $field);
-            $hdelFieldFromDailyKeys(ApiWeightConfigController::REDIS_KEY_CLICKS_DAILY_PREFIX, $field);
-        } elseif ($type === 'show_id') {
-            $id = intval($request->input('id'));
-            if (!in_array($id, ApiWeightConfigController::VALID_IDS, true)) {
-                return response()->json(['status' => 'error', 'message' => '无效 id']);
-            }
-            $field = (string) $id;
-            Redis::hdel(ApiWeightConfigController::REDIS_KEY_SHOW, $field);
-            $hdelFieldFromDailyKeys(ApiWeightConfigController::REDIS_KEY_SHOW_DAILY_PREFIX, $field);
         } else {
-            return response()->json(['status' => 'error', 'message' => '未知操作类型']);
+            Redis::del(ApiWeightConfigController::REDIS_KEY_CLICKS);
         }
         return response()->json(['status' => 'success', 'message' => '清除成功']);
     }

+ 1 - 0
resources/views/admin/web_channel_config/edit.blade.php

@@ -102,6 +102,7 @@
                                         <option value="tt" @if($info->PlatformName == 'tt') selected @endif>Tiktok (tt)</option>
                                         <option value="gg" @if($info->PlatformName == 'gg') selected @endif>Google (gg)</option>
                                         <option value="apk" @if($info->PlatformName == 'apk') selected @endif>Apk落地页 (apk)</option>
+                                        <option value="snapchat" @if($info->PlatformName == 'snapchat') selected @endif>snapchat</option>
                                     </select>
                                 </div>
                                 <div class="form-group">

+ 84 - 3
resources/views/admin/web_channel_config/index.blade.php

@@ -35,6 +35,11 @@
 {{--                                <i class="mdi mdi-plus btn-icon-prepend"></i>--}}
 {{--                                {{ __('auto.添加配置') }}--}}
 {{--                            </button>--}}
+                            <button type="button" class="btn btn-sm btn-gradient-primary btn-icon-text" onclick="quickCreateChannel()">
+                                <i class="mdi mdi-plus btn-icon-prepend"></i>
+                                {{ __('auto.快速创建渠道') }}
+                            </button>
+                            <br><br>
                             <table class="table table-bordered">
                                 <thead>
                                 <tr>
@@ -42,6 +47,7 @@
                                     <th>{{ __('auto.渠道号') }}</th>
                                     <th>{{ __('auto.包名') }}</th>
                                     <th>RegionID</th>
+                                    <th>{{ __('auto.网站地址') }}</th>
                                     <th>{{ __('auto.备注') }}</th>
                                     <th>StateNo</th>
                                     <th>Platform</th>
@@ -61,14 +67,35 @@
                                                 $regionInfo = $regionMap[$v->RegionID] ?? null;
                                             @endphp
                                             @if($regionInfo && $regionInfo['has_logo'])
-                                                <div style="background-color: #000; display: inline-block; padding: 2px;">
-                                                    <img src="{{$regionInfo['logo']}}" style="max-height: 30px; width: auto;" alt="Logo" title="{{$v->RegionID}}">
+                                                <div style="background-color: #000; display: inline-block; padding: 2px; width: 100%; text-align: center;">
+                                                    @if(!empty($regionInfo['is_svg']))
+                                                        <object data="{{$regionInfo['logo']}}" type="image/svg+xml" style="max-height: 30px; width: 100%; height: auto; pointer-events: none;" title="{{$v->RegionID}}">
+                                                            <img src="{{$regionInfo['logo']}}" style="max-height: 30px; width: auto;" alt="Logo" title="{{$v->RegionID}}">
+                                                        </object>
+                                                    @else
+                                                        <img src="{{$regionInfo['logo']}}" style="max-height: 30px; width: auto;" alt="Logo" title="{{$v->RegionID}}">
+                                                    @endif
                                                 </div>
                                             @else
                                                 <span style="color: {{$regionInfo['color'] ?? ''}}">{{$v->RegionID}}</span>
                                             @endif
                                         </td>
-                                        <td>{{$v->Remarks}}</td>
+                                        <td>
+                                            @php
+                                                $channelUrl = $channelUrls[$v->ID] ?? '';
+                                            @endphp
+                                            @if($channelUrl)
+                                                <a href="javascript:void(0);"
+                                                   onclick="copyToClipboard('{{$channelUrl}}', this)"
+                                                   style="color: #007bff; text-decoration: none; cursor: pointer;"
+                                                   title="{{ __('auto.点击复制') }}">
+                                                    {{$channelUrl}}
+                                                </a>
+                                            @else
+                                                <span style="color: #999;">-</span>
+                                            @endif
+                                        </td>
+                                        <td contentEditable="true" onblur="remarks(this, {{$v->ID}})" style="cursor: text;">{{$v->Remarks}}</td>
                                         <td>{{$v->StateNo}}</td>
                                         <td>{{$v->PlatformName}} ({{$v->PlatformID}})</td>
                                         <td>
@@ -120,5 +147,59 @@
                 content: '/admin/web_channel_config/update/' + id
             });
         }
+
+        function copyToClipboard(text, element) {
+            // 创建临时文本域
+            var tempInput = document.createElement('textarea');
+            tempInput.value = text;
+            document.body.appendChild(tempInput);
+            tempInput.select();
+
+            try {
+                // 执行复制
+                document.execCommand('copy');
+
+                // 显示复制成功提示
+                layer.msg('{{ __('auto.复制成功') }}', {icon: 1, time: 1000});
+
+                // 短暂改变链接颜色表示已复制
+                var originalColor = element.style.color;
+                element.style.color = '#28a745';
+                setTimeout(function() {
+                    element.style.color = originalColor;
+                }, 300);
+            } catch (err) {
+                layer.msg('{{ __('auto.复制失败') }}', {icon: 2});
+            }
+
+            // 移除临时文本域
+            document.body.removeChild(tempInput);
+        }
+
+        // 快速创建渠道
+        function quickCreateChannel() {
+            layer.open({
+                type: 2,
+                title: '{{ __('auto.快速创建渠道') }}',
+                shadeClose: true,
+                shade: 0.8,
+                area: ['60%', '70%'],
+                content: '/admin/channel/quick_create_channel',
+                end: function() {
+                    window.location.reload();
+                }
+            });
+        }
+
+        // 修改备注
+        function remarks(obj, id) {
+            var remark = $(obj).html();
+            myRequest("/admin/web_channel_config/remarks/" + id, "post", {remark}, function (res) {
+                layer.msg(res.msg);
+                setTimeout(function () {
+                    // window.location.reload();
+                }, 1500);
+            });
+        }
     </script>
 @endsection

+ 25 - 3
resources/views/admin/web_region_config/index.blade.php

@@ -44,14 +44,36 @@
                                         <td>{{$v->DomainUrl}}</td>
                                         <td>
                                             @if($v->LogoUrl)
-                                                <div style="background-color: #000; display: inline-block; padding: 5px;">
-                                                    <img src="{{$v->DomainUrl}}{{$v->LogoUrl}}" style="max-width: 150px; height: auto; border-radius: 0;" alt="Logo">
+                                                @php
+                                                    $logoUrl = $v->DomainUrl . $v->LogoUrl;
+                                                    $isSvg = strtolower(pathinfo($v->LogoUrl, PATHINFO_EXTENSION)) === 'svg';
+                                                @endphp
+                                                <div style="background-color: #000; display: inline-block; padding: 5px; width: 100%; text-align: center;">
+                                                    @if($isSvg)
+                                                        <object data="{{$logoUrl}}" type="image/svg+xml" style="max-width: 150px; width: 100%; height: auto; pointer-events: none;">
+                                                            <img src="{{$logoUrl}}" style="max-width: 150px; height: auto;" alt="Logo">
+                                                        </object>
+                                                    @else
+                                                        <img src="{{$logoUrl}}" style="max-width: 150px; height: auto; border-radius: 0;" alt="Logo">
+                                                    @endif
                                                 </div>
                                             @endif
                                         </td>
                                         <td>
                                             @if($v->IconUrl)
-                                                <img src="{{$v->DomainUrl}}{{$v->IconUrl}}" style="width: 192px; height: 192px; border-radius: 0;" alt="Icon">
+                                                @php
+                                                    $iconUrl = $v->DomainUrl . $v->IconUrl;
+                                                    $isIconSvg = strtolower(pathinfo($v->IconUrl, PATHINFO_EXTENSION)) === 'svg';
+                                                @endphp
+                                                @if($isIconSvg)
+                                                    <div style="width: 192px; height: 192px; display: flex; align-items: center; justify-content: center;">
+                                                        <object data="{{$iconUrl}}" type="image/svg+xml" style="width: 100%; height: 100%; pointer-events: none;">
+                                                            <img src="{{$iconUrl}}" style="width: 192px; height: 192px;" alt="Icon">
+                                                        </object>
+                                                    </div>
+                                                @else
+                                                    <img src="{{$iconUrl}}" style="width: 192px; height: 192px; border-radius: 0;" alt="Icon">
+                                                @endif
                                             @endif
                                         </td>
                                         <td>{{$v->GroupID}}</td>