2
0
laowu 15 часов назад
Родитель
Сommit
f6088842b8
46 измененных файлов с 3619 добавлено и 789 удалено
  1. 82 0
      app/Console/Commands/CheckStockModeNegative.php
  2. 178 0
      app/Console/Commands/RecordPaidRewardDailyStatistics.php
  3. 13 6
      app/Console/Commands/SuperballUpdatePoolAndStats.php
  4. 7 1
      app/Console/Kernel.php
  5. 919 0
      app/Http/Controllers/Admin/AccountCookieController.php
  6. 42 2
      app/Http/Controllers/Admin/ExtensionNewController.php
  7. 76 152
      app/Http/Controllers/Admin/GlobalController.php
  8. 3 0
      app/Http/Controllers/Admin/MailController.php
  9. 123 0
      app/Http/Controllers/Admin/ProtectLevelController.php
  10. 19 4
      app/Http/Controllers/Admin/RechargeController.php
  11. 25 1
      app/Http/Controllers/Admin/StockModeController.php
  12. 15 4
      app/Http/Controllers/Admin/SuperballController.php
  13. 3 1
      app/Http/Controllers/Admin/WebChannelConfigController.php
  14. 28 26
      app/Http/Controllers/Admin/WithdrawalController.php
  15. 119 81
      app/Http/Controllers/Game/ActivityController.php
  16. 128 0
      app/Http/Controllers/Game/HacksawController.php
  17. 1 1
      app/Http/Controllers/Game/LoginController.php
  18. 168 186
      app/Http/Controllers/Game/PayRechargeController.php
  19. 74 68
      app/Http/Controllers/Game/RechargeController.php
  20. 8 0
      app/Http/Controllers/Game/WebRouteController.php
  21. 11 10
      app/Http/Controllers/Game/WithDrawInfoController.php
  22. 109 60
      app/Http/logic/admin/GlobalLogicController.php
  23. 25 19
      app/Http/logic/admin/WithdrawalLogic.php
  24. 71 50
      app/Models/AccountsInfo.php
  25. 7 5
      app/Models/RecordScoreInfo.php
  26. 77 13
      app/Services/OrderServices.php
  27. 43 0
      app/Services/PaidRewardStatisticsService.php
  28. 212 37
      app/Services/SuperballActivityService.php
  29. 19 2
      app/Services/VipService.php
  30. 39 21
      app/Util.php
  31. 6 2
      resources/views/admin/Withdrawal/verify_finish.blade.php
  32. 619 0
      resources/views/admin/account_cookie/index.blade.php
  33. 53 0
      resources/views/admin/extension_new/subordinate.blade.php
  34. 1 0
      resources/views/admin/game_data/useronline.blade.php
  35. 10 26
      resources/views/admin/global/id_list.blade.php
  36. 76 0
      resources/views/admin/protect_level/add.blade.php
  37. 76 0
      resources/views/admin/protect_level/edit.blade.php
  38. 81 0
      resources/views/admin/protect_level/index.blade.php
  39. 8 3
      resources/views/admin/recharge/list.blade.php
  40. 14 4
      resources/views/admin/stock_mode/index.blade.php
  41. 3 3
      resources/views/admin/superball/prizes.blade.php
  42. 4 0
      resources/views/admin/web_channel_config/add.blade.php
  43. 4 0
      resources/views/admin/web_channel_config/edit.blade.php
  44. 8 0
      resources/views/admin/web_channel_config/index.blade.php
  45. 3 0
      routes/game.php
  46. 9 1
      routes/web.php

+ 82 - 0
app/Console/Commands/CheckStockModeNegative.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Notification\TelegramBot;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+
+class CheckStockModeNegative extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'stock_mode:check_negative';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Check stock-mode rooms (low/mid/high) and send Telegram alert when Stock is negative';
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        // 低/中/高三个房间:SortID 1,2,3
+        $rooms = DB::connection('write')
+            ->table('QPPlatformDB.dbo.RoomStockStatic2')
+            ->where('GameID', 0)
+            ->whereIn('SortID', [1, 2, 3])
+            ->get();
+
+        if ($rooms->isEmpty()) {
+            return 0;
+        }
+
+        $negativeRooms = [];
+        foreach ($rooms as $room) {
+            if ($room->Stock < 0) {
+                // 数据库存储的是 *100 后的值,这里除以 100 便于阅读
+                $negativeRooms[] = [
+                    'sort_id' => $room->SortID,
+                    'stock_raw' => $room->Stock,
+                    'stock' => round($room->Stock / 100, 2),
+                    'level_base' => isset($room->LevelBase) ? round($room->LevelBase / 100, 2) : null,
+                ];
+            }
+        }
+
+        if (empty($negativeRooms)) {
+            return 0;
+        }
+
+        $lines = [];
+        $lines[] = '【库存模式报警】RoomStockStatic2 库存为负';
+        $lines[] = '时间: ' . date('Y-m-d H:i:s');
+        foreach ($negativeRooms as $info) {
+            $lines[] = sprintf(
+                '房间 SortID=%d, Stock=%s (原始=%d), LevelBase=%s',
+                $info['sort_id'],
+                $info['stock'],
+                $info['stock_raw'],
+                $info['level_base'] === null ? '-' : $info['level_base']
+            );
+        }
+
+        try {
+            TelegramBot::getDefault()->sendMsgWithEnv(implode("\n", $lines));
+        } catch (\Throwable $e) {
+            $this->error('Failed to send Telegram alert: ' . $e->getMessage());
+        }
+
+        return 0;
+    }
+}
+

+ 178 - 0
app/Console/Commands/RecordPaidRewardDailyStatistics.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Facade\TableName;
+use App\Models\RecordScoreInfo;
+use Carbon\Carbon;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+
+class RecordPaidRewardDailyStatistics extends Command
+{
+    protected $signature = 'RecordPaidRewardDailyStatistics {day?}';
+
+    protected $description = '统计付费用户各奖励类型每日送出金额';
+
+    public function handle()
+    {
+        $day = $this->argument('day') ?: Carbon::yesterday()->format('Y-m-d');
+
+        $dateId = date('Ymd', strtotime($day));
+
+        $stats = $this->buildStats($day);
+
+        foreach ($stats as $item) {
+            DB::table(TableName::QPRecordDB() . 'RecordPaidRewardDailyStatistics')->updateOrInsert(
+                [
+                    'DateID' => $dateId,
+                    'StatType' => $item['StatType'],
+                ],
+                [
+                    'TotalAmount' => $item['TotalAmount'],
+                    'CreatedAt' => now()->format('Y-m-d H:i:s'),
+                    'UpdatedAt' => now()->format('Y-m-d H:i:s'),
+                ]
+            );
+        }
+
+        $this->info("RecordPaidRewardDailyStatistics done: {$day}");
+
+        return true;
+    }
+
+    private function buildStats(string $day): array
+    {
+        $stats = [];
+        $signIn = $this->signInState($day);
+        $stats[] = $signIn;
+        $bankruptHelp = $this->bankruptHelp($day);
+        $stats[] = $bankruptHelp;
+        $stats[] = $this->fromSuperballPrize($day, 'superball');
+        $stats[] = $this->fromChristmasWheel($day, 'christmas_gift_wheel');
+        $stats[] = $this->fromScoreReason($day, 'bound_phone', 21);
+        $dbStats = DB::table(TableName::QPRecordDB() . 'RecordPaidRewardDailyStatistics')
+            ->lock('with(nolock)')
+            ->where('DateID', date('Ymd', strtotime($day)))
+            ->whereIn('StatType', ['normal_recharge_chips', 'first_recharge_gift_chips', 'bankrupt_gift_chips',
+                'daily_gift_chips', 'vip_inactive_gift_chips', 'free_bonus_gift_chips', 'christmas_gift_chips', 'unknow_chips'])
+            ->select('StatType', 'TotalAmount')
+            ->get()->map(function ($item) {
+                return json_decode(json_encode($item), true);
+            })->toArray();
+        $stats[] = $this->sumAll($dbStats, 'chips_all');
+
+        return $stats;
+    }
+
+    private function sumAll(array $stats, string $type): array
+    {
+        $totalAmount = 0;
+
+        foreach ($stats as $item) {
+            $totalAmount += (int)($item['TotalAmount'] ?? 0);
+        }
+
+        return [
+            'StatType' => $type,
+            'TotalAmount' => $totalAmount,
+        ];
+    }
+
+    private function fromScoreReason(string $day, string $type, int $reason): array
+    {
+        $start = $day . ' 00:00:00';
+        $end = date('Y-m-d H:i:s', strtotime($day . ' +1 day'));
+
+        $row = DB::connection('sqlsrv')
+            ->table(TableName::QPRecordDB() . 'RecordUserScoreChange as r')
+            ->join(TableName::QPAccountsDB() . 'YN_VIPAccount as va', 'va.UserID', '=', 'r.UserID')
+            ->where('va.Recharge', '>', 0)
+            ->where('r.Reason', $reason)
+            ->where('r.UpdateTime', '>=', $start)
+            ->where('r.UpdateTime', '<', $end)
+            ->selectRaw('
+                ISNULL(CAST(SUM(r.ChangeScore) AS BIGINT), 0) as TotalAmount
+            ')
+            ->first();
+
+        return [
+            'StatType' => $type,
+            'TotalAmount' => (int)($row->TotalAmount ?? 0),
+        ];
+    }
+
+    private function fromSuperballPrize(string $day, string $type): array
+    {
+        $row = DB::connection('sqlsrv')
+            ->table(TableName::agent() . 'superball_prize_log as p')
+            ->join(TableName::QPAccountsDB() . 'YN_VIPAccount as va', 'va.UserID', '=', 'p.user_id')
+            ->where('va.Recharge', '>', 0)
+            ->whereDate('p.created_at', $day)
+            ->selectRaw('
+                ISNULL(CAST(SUM(p.total_amount) AS BIGINT), 0) as TotalAmount
+            ')
+            ->first();
+
+        return [
+            'StatType' => $type,
+            'TotalAmount' => (int)($row->TotalAmount ?? 0),
+        ];
+    }
+
+    private function fromChristmasWheel(string $day, string $type): array
+    {
+        $start = $day . ' 00:00:00';
+        $end = date('Y-m-d H:i:s', strtotime($day . ' +1 day'));
+
+        $row = DB::connection('sqlsrv')
+            ->table(TableName::agent() . 'christmas_wheel_history as h')
+            ->join(TableName::QPAccountsDB() . 'YN_VIPAccount as va', 'va.UserID', '=', 'h.UserID')
+            ->where('va.Recharge', '>', 0)
+            ->where('h.created_at', '>=', $start)
+            ->where('h.created_at', '<', $end)
+            ->selectRaw('
+                ISNULL(CAST(SUM(h.reward * 100) AS BIGINT), 0) as TotalAmount
+            ')
+            ->first();
+
+        return [
+            'StatType' => $type,
+            'TotalAmount' => (int)($row->TotalAmount ?? 0),
+        ];
+    }
+
+    private function signInState(string $day)
+    {
+        $res = DB::table('QPRecordDB.dbo.RecordSignIn as h')
+            ->lock('with(nolock)')
+            ->whereDate('SignInDate', $day)
+            ->leftJoin(DB::raw('QPAccountsDB.dbo.YN_VIPAccount as va with(nolock)'),
+                'va.UserID', '=', 'h.UserID')
+            ->where('va.Recharge', '>', 0)
+            ->selectRaw('sum(RewardScore) as TotalAmount')
+            ->first();
+        return [
+            'StatType' => 'sign_in',
+            'TotalAmount' => (int)($res->TotalAmount ?? 0),
+        ];
+    }
+
+    private function bankruptHelp(string $day)
+    {
+        $res = DB::table('QPRecordDB.dbo.RecordUserScoreChange as rus')
+            ->lock('with(nolock)')
+            ->leftJoin(DB::raw('QPAccountsDB.dbo.YN_VIPAccount as va with(nolock)'),
+                'va.UserID', '=', 'rus.UserID')
+            ->whereDate('UpdateTime', $day)
+            ->where('va.Recharge', '>', 0)
+            ->where('rus.Reason', 13)
+            ->selectRaw('sum(ChangeScore) as TotalAmount')
+            ->first();
+        return [
+            'StatType' => 'bankrupt_help',
+            'TotalAmount' => (int)($res->TotalAmount ?? 0),
+        ];
+    }
+}
+

+ 13 - 6
app/Console/Commands/SuperballUpdatePoolAndStats.php

@@ -53,20 +53,25 @@ class SuperballUpdatePoolAndStats extends Command
 
             // 2. 确保当日 superball_daily 记录存在
             DB::connection('write')->transaction(function () use ($dateStr, $poolAmount, $today) {
-                // 确保记录存在(getOrCreateDaily 只在不存在时插入)
-                $this->service->getOrCreateDaily($dateStr);
+                // getOrCreateDaily 已经保证有一条记录(带 lucky_number),此处再加锁更新
+                $daily = $this->service->getOrCreateDaily($dateStr);
 
                 // 3. 计算本次要增加的 completed_count 和 total_balls
                 $hour = (int)$today->format('G'); // 0-23
 
-                if ($hour < 1) {
+                $hour = intval(date('H'));
+                if ($hour < 2) {
                     // 00:00 - 00:59
                     $completedInc = mt_rand(5, 10);
                     $multipliers = [1, 2, 3, 6];
+                } else if($hour<=10){
+                    // 01:00 以后
+                    $completedInc = mt_rand(10, 15);
+                    $multipliers = [6,10];
                 } else {
                     // 01:00 以后
-                    $completedInc = mt_rand(10, 20);
-                    $multipliers = [3,6,10,30];
+                    $completedInc = mt_rand(15, 20);
+                    $multipliers = [10,15];
                 }
 
                 $multiplier = $multipliers[array_rand($multipliers)];
@@ -80,9 +85,11 @@ class SuperballUpdatePoolAndStats extends Command
                         'total_balls' => DB::raw("total_balls + {$ballsInc}"),
                         'updated_at' => now()->format('Y-m-d H:i:s'),
                     ]);
+
+                \Log::info("Superball pool ###$completedInc###$ballsInc");
             });
 
-            \Log::info("Superball pool stats updated for {$dateStr}, pool_amount={$poolAmount}");
+            \Log::info("Superball pool stats updated for {$dateStr}, pool_amount={$poolAmount}"."--");
             return true;
         } catch (\Throwable $e) {
             \Log::error('Superball update pool stats failed: ' . $e->getMessage());

+ 7 - 1
app/Console/Kernel.php

@@ -3,6 +3,7 @@
 namespace App\Console;
 
 use App\Console\Commands\CheckIosAppStore;
+use App\Console\Commands\CheckStockModeNegative;
 use App\Console\Commands\DbQueue;
 use App\Console\Commands\OnlineReport;
 use App\Console\Commands\DecStock;
@@ -13,6 +14,7 @@ use App\Console\Commands\RecordPlatformData;
 use App\Console\Commands\RecordServerGameCount;
 use App\Console\Commands\RecordServerGameCountYesterday;
 use App\Console\Commands\RecordThreeGameYesterday;
+use App\Console\Commands\RecordPaidRewardDailyStatistics;
 use App\Console\Commands\RecordUserScoreChangeStatistics;
 use App\Console\Commands\SuperballUpdatePoolAndStats;
 use Illuminate\Console\Scheduling\Schedule;
@@ -29,9 +31,11 @@ class Kernel extends ConsoleKernel
         ExemptReview::class,
         PayOrder::class,
         Extension::class,
+        CheckStockModeNegative::class,
         RecordPlatformData::class,
         RecordServerGameCount::class,
         RecordServerGameCountYesterday::class,
+        RecordPaidRewardDailyStatistics::class,
         RecordUserScoreChangeStatistics::class,
         DecStock::class,
         CheckIosAppStore::class,
@@ -39,6 +43,7 @@ class Kernel extends ConsoleKernel
         DbQueue::class,
         RecordThreeGameYesterday::class,
         SuperballUpdatePoolAndStats::class,
+
     ];
 
     /**
@@ -52,15 +57,16 @@ class Kernel extends ConsoleKernel
 //        $schedule->command('db_queue')->everyMinute()->description('批量处理redis队列任务');
         $schedule->command('exempt_review')->everyMinute()->description('免审提现');
         $schedule->command('record_server_game_count')->cron('*/15 * * * * ')->description('按天统计游戏人数');
+        $schedule->command('stock_mode:check_negative')->cron('*/5 * * * *')->description('检测库存模式房间库存是否为负并通过 Telegram 报警');
         $schedule->command('online_max')->cron('*/8 * * * * ')->description('最高在线人数统计');
         $schedule->command('record_server_game_count_yesterday')->cron('05 0 * * * ')->description('按天统计游戏人数--今日执行昨日');
         $schedule->command('RecordPlatformData')->cron('10 0 * * * ')->description('数据统计');
+        $schedule->command('RecordPaidRewardDailyStatistics')->cron('15 0 * * * ')->description('付费用户奖励日统计');
         $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('record_three_game_yesterday')->cron('05 0 * * * ')->description('按天统计游戏人数--今日执行昨日');
     }
 

+ 919 - 0
app/Http/Controllers/Admin/AccountCookieController.php

@@ -0,0 +1,919 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use App\Models\AccountsInfo;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+
+class AccountCookieController extends Controller
+{
+    public function index(Request $request)
+    {
+        $filters = $this->buildFilters($request);
+
+        $cookieSubQuery = DB::connection('read')->raw("
+            (
+                SELECT *,
+                       ROW_NUMBER() OVER (
+                           PARTITION BY
+                               ISNULL(CAST(UserID AS VARCHAR(50)), '')
+                           ORDER BY CreateTime DESC, ID DESC
+                       ) AS rn
+                FROM QPAccountsDB.dbo.AccountCookie
+            ) ac
+        ");
+
+        $query = DB::connection('read')
+            ->table($cookieSubQuery)
+            ->leftJoin(AccountsInfo::TABLE . ' as ai', 'ai.UserID', '=', 'ac.UserID')
+            ->select([
+                'ac.ID',
+                'ac.UserID',
+                'ai.GameID',
+                'ai.Channel as AccountChannel',
+                'ai.RegisterDate',
+                'ac.UrlSign',
+                'ac.Platform',
+                'ac.CreateTime',
+                'ac.IP',
+                'ac.Locale',
+                'ac.Origin',
+                'ac.FPID',
+                'ac.FF',
+                'ac.ClickUA',
+                'ac.GameUA',
+                'ac.Params',
+                'ac.Cookie',
+            ]);
+
+        $this->applyFilters($query, $filters);
+        $query->where('ac.rn', 1);
+
+        $stats = $this->buildStats(clone $query);
+        $fbclidGroups = $this->buildFbclidGroups(clone $query);
+        $ffGroups = $this->buildDuplicateValueGroups(clone $query, 'FF', 'ff');
+
+        $list = $query
+            ->orderBy('ac.CreateTime', 'desc')
+            ->paginate($filters['page_size'])
+            ->appends($request->query());
+
+        $userIds = $list->getCollection()
+            ->pluck('UserID')
+            ->filter(function ($userId) {
+                return !empty($userId);
+            })
+            ->map(function ($userId) {
+                return (int)$userId;
+            })
+            ->unique()
+            ->values()
+            ->all();
+
+        $payMap = $this->loadPayStats($userIds);
+        $adjustMap = $this->loadAdjustEventStats($userIds, $filters);
+
+        $items = $list->getCollection()->map(function ($row) {
+            $params = $this->decodeJson($row->Params);
+            $cookieMap = $this->parseCookieString($row->Cookie);
+            $uaInfo = $this->analyzeUserAgent($row->ClickUA ?: $row->GameUA);
+            $paramAnalysis = $this->analyzeMarketingParams($params);
+
+            $row->HasFbclid = stripos($row->Params ?? '', 'fbclid') !== false;
+            $row->Fbp = $cookieMap['_fbp'] ?? '';
+            $row->Fbc = $cookieMap['_fbc'] ?? ($params['fbclid'] ?? '');
+            $row->Pixel = $paramAnalysis['primary']['pixel'];
+            $row->UtmSource = $paramAnalysis['primary']['utm_source'];
+            $row->UtmMedium = $paramAnalysis['primary']['utm_medium'];
+            $row->UtmCampaign = $paramAnalysis['primary']['utm_campaign'];
+            $row->ParamChannel = $paramAnalysis['primary']['channel'];
+            $row->ParamCampaign = $paramAnalysis['primary']['campaign'];
+            $row->ParamAdgroup = $paramAnalysis['primary']['adgroup'];
+            $row->ParamCreative = $paramAnalysis['primary']['creative'];
+            $row->UaApp = $uaInfo['app'];
+            $row->UaOs = $uaInfo['os'];
+            $row->UaDevice = $uaInfo['device'];
+            $row->CookieKey = $this->makeCookieKey($row);
+            $row->ParamsDecoded = $params;
+            $row->ParamAnalysis = $paramAnalysis;
+            $row->FbclidValue = $paramAnalysis['primary']['fbclid'];
+            $row->FbclidGroup = null;
+            $row->FFGroup = null;
+            $row->FbclidCookieCheck = $this->validateFbclidCookieConsistency(
+                $paramAnalysis['primary']['fbclid'],
+                $cookieMap['_fbc'] ?? ''
+            );
+
+            return $row;
+        });
+
+        $items = $items->map(function ($row) use ($payMap, $adjustMap, $fbclidGroups, $ffGroups) {
+            $pay = $payMap[(int)($row->UserID ?? 0)] ?? null;
+            $adjust = $adjustMap[(int)($row->UserID ?? 0)] ?? null;
+            $fbclidGroup = $fbclidGroups['rows'][$row->ID] ?? null;
+            $ffGroup = $ffGroups['rows'][$row->ID] ?? null;
+
+            $row->PayOrderCount = $pay->pay_order_count ?? 0;
+            $row->PayAmountSum = $pay->pay_amount_sum ?? 0;
+            $row->PayAmountDisplay = (string)round(((float)($row->PayAmountSum ?? 0)) / 100);
+            $row->LastPayAt = $pay->last_pay_at ?? '';
+            $row->AdjustStatus = $adjust['status'] ?? 'none';
+            $row->AdjustLogs = $adjust['logs'] ?? [];
+            $row->FbclidGroup = $fbclidGroup;
+            $row->FFGroup = $ffGroup;
+
+            return $row;
+        });
+
+        $list->setCollection($items);
+
+        return view('admin.account_cookie.index', [
+            'list' => $list,
+            'filters' => $filters,
+            'stats' => $stats,
+            'fbclidGroups' => $fbclidGroups['groups'],
+            'ffGroups' => $ffGroups['groups'],
+        ]);
+    }
+
+    protected function buildFilters(Request $request)
+    {
+        $gameId = trim((string)$request->input('GameID', ''));
+        $resolvedUserId = '';
+        if ($gameId !== '') {
+            $resolvedUserId = AccountsInfo::query()->where('GameID', $gameId)->value('UserID') ?: '';
+        }
+
+        $dateStartRaw = trim((string)$request->input('date_start', date('Y-m-d')));
+        $dateEndRaw = trim((string)$request->input('date_end', date('Y-m-d')));
+        $registerStartRaw = trim((string)$request->input('register_start', date('Y-m-d')));
+        $registerEndRaw = trim((string)$request->input('register_end', date('Y-m-d')));
+
+        return [
+            'user_id' => trim((string)$request->input('UserID', '')),
+            'game_id' => $gameId,
+            'resolved_user_id' => $resolvedUserId,
+            'url_signs' => $this->splitCsv($request->input('UrlSign', '')),
+            'account_channels' => $this->splitCsv($request->input('AccountChannel', '')),
+            'platform' => trim((string)$request->input('Platform', '')),
+            'date_start_raw' => $dateStartRaw,
+            'date_end_raw' => $dateEndRaw,
+            'register_start_raw' => $registerStartRaw,
+            'register_end_raw' => $registerEndRaw,
+            'date_start' => $this->normalizeDateBoundary($dateStartRaw, false),
+            'date_end' => $this->normalizeDateBoundary($dateEndRaw, true),
+            'register_start' => $this->normalizeDateBoundary($registerStartRaw, false),
+            'register_end' => $this->normalizeDateBoundary($registerEndRaw, true),
+            'has_fbclid' => (int)$request->input('has_fbclid', 1),
+            'origin' => trim((string)$request->input('Origin', '')),
+            'ip' => trim((string)$request->input('IP', '')),
+            'ua' => trim((string)$request->input('UA', '')),
+            'param_category' => trim((string)$request->input('param_category', '')),
+            'param_key' => trim((string)$request->input('param_key', '')),
+            'param_value' => trim((string)$request->input('param_value', '')),
+            'page_size' => max(20, min((int)$request->input('page_size', 100), 500)),
+        ];
+    }
+
+    protected function applyFilters($query, array $filters)
+    {
+        if ($filters['user_id'] !== '') {
+            $query->where('ac.UserID', $filters['user_id']);
+        }
+
+        if ($filters['game_id'] !== '') {
+            if ($filters['resolved_user_id'] !== '') {
+                $query->where('ac.UserID', $filters['resolved_user_id']);
+            } else {
+                $query->whereRaw('1 = 0');
+            }
+        }
+
+        if (!empty($filters['url_signs'])) {
+            $query->whereIn('ac.UrlSign', $filters['url_signs']);
+        }
+
+        if (!empty($filters['account_channels'])) {
+            $query->whereIn('ai.Channel', $filters['account_channels']);
+        }
+
+        if ($filters['platform'] !== '') {
+            $query->where('ac.Platform', $filters['platform']);
+        }
+
+        if ($filters['date_start'] !== '') {
+            $query->where('ac.CreateTime', '>=', $filters['date_start']);
+        }
+        if ($filters['date_end'] !== '') {
+            $query->where('ac.CreateTime', '<=', $filters['date_end']);
+        }
+
+        if ($filters['register_start'] !== '') {
+            $query->where('ai.RegisterDate', '>=', $filters['register_start']);
+        }
+        if ($filters['register_end'] !== '') {
+            $query->where('ai.RegisterDate', '<=', $filters['register_end']);
+        }
+
+        if ($filters['has_fbclid'] === 1) {
+            $query->where('ac.Params', 'like', '%fbclid%');
+        } elseif ($filters['has_fbclid'] === 2) {
+            $query->where('ac.Params', 'not like', '%fbclid%');
+        }
+
+        if ($filters['origin'] !== '') {
+            $query->where('ac.Origin', 'like', '%' . $filters['origin'] . '%');
+        }
+
+        if ($filters['ip'] !== '') {
+            $query->where('ac.IP', 'like', '%' . $filters['ip'] . '%');
+        }
+
+        if ($filters['ua'] !== '') {
+            $query->where(function ($subQuery) use ($filters) {
+                $subQuery->where('ac.ClickUA', 'like', '%' . $filters['ua'] . '%')
+                    ->orWhere('ac.GameUA', 'like', '%' . $filters['ua'] . '%');
+            });
+        }
+
+        if ($filters['param_key'] !== '') {
+            $query->where('ac.Params', 'like', '%"' . $filters['param_key'] . '"%');
+        }
+
+        if ($filters['param_value'] !== '') {
+            $query->where('ac.Params', 'like', '%' . $filters['param_value'] . '%');
+        }
+
+        if ($filters['param_category'] !== '') {
+            $keys = $this->getCategoryKeys($filters['param_category']);
+            if (empty($keys)) {
+                $query->whereRaw('1 = 0');
+            } else {
+                $query->where(function ($subQuery) use ($keys) {
+                    foreach ($keys as $key) {
+                        $subQuery->orWhere('ac.Params', 'like', '%"' . $key . '"%');
+                    }
+                });
+            }
+        }
+    }
+
+    protected function buildStats($query)
+    {
+        $rows = $query->get([
+            'ac.ID',
+            'ac.UserID',
+            'ac.FPID',
+            'ac.FF',
+            'ac.UrlSign',
+            'ac.Platform',
+            'ac.IP',
+            'ac.Origin',
+            'ac.ClickUA',
+            'ac.GameUA',
+            'ac.Params',
+        ]);
+
+        $stats = [
+            'total' => $rows->count(),
+            'unique_users' => $rows->pluck('UserID')->filter()->unique()->count(),
+            'unique_ips' => $rows->pluck('IP')->filter()->unique()->count(),
+            'unique_cookies' => $rows->map(function ($row) {
+                return $this->makeCookieKey($row);
+            })->filter()->unique()->count(),
+            'registered_users' => $rows->filter(function ($row) {
+                return !empty($row->UserID);
+            })->pluck('UserID')->unique()->count(),
+            'paid_users' => 0,
+            'fbclid_count' => 0,
+            'fb_inapp_count' => 0,
+            'ig_inapp_count' => 0,
+            'platforms' => [],
+            'url_signs' => [],
+            'origins' => [],
+            'utm_sources' => [],
+            'param_categories' => [],
+            'param_keys' => [],
+            'param_category_stats' => [],
+            'duplicate_fbclid_groups' => 0,
+            'duplicate_fbclid_rows' => 0,
+            'duplicate_ff_groups' => 0,
+            'duplicate_ff_rows' => 0,
+            'fbclid_cookie_issues' => 0,
+        ];
+
+        $fbclidBuckets = [];
+        $ffBuckets = [];
+
+        foreach ($rows as $row) {
+            $params = $this->decodeJson($row->Params);
+            $uaInfo = $this->analyzeUserAgent($row->ClickUA ?: $row->GameUA);
+            $paramAnalysis = $this->analyzeMarketingParams($params);
+            $cookieMap = $this->parseCookieString($row->Cookie ?? '');
+
+            if (stripos($row->Params ?? '', 'fbclid') !== false) {
+                $stats['fbclid_count']++;
+            }
+            if ($uaInfo['app'] === 'Facebook') {
+                $stats['fb_inapp_count']++;
+            }
+            if ($uaInfo['app'] === 'Instagram') {
+                $stats['ig_inapp_count']++;
+            }
+
+            $this->incrementBucket($stats['platforms'], $row->Platform ?: 'unknown');
+            $this->incrementBucket($stats['url_signs'], (string)($row->UrlSign ?: 'unknown'));
+            $this->incrementBucket($stats['origins'], $row->Origin ?: 'unknown');
+            $this->incrementBucket($stats['utm_sources'], $params['utm_source'] ?? 'unknown');
+
+            foreach ($paramAnalysis['categories'] as $category => $entries) {
+                $label = $this->getParamCategoryLabel($category);
+                $this->incrementBucket($stats['param_categories'], $label);
+
+                if (!isset($stats['param_category_stats'][$category])) {
+                    $stats['param_category_stats'][$category] = [
+                        'label' => $label,
+                        'keys' => [],
+                        'values' => [],
+                    ];
+                }
+
+                foreach ($entries as $entry) {
+                    $this->incrementBucket($stats['param_category_stats'][$category]['keys'], $entry['key']);
+                    $this->incrementBucket($stats['param_keys'], $entry['key']);
+
+                    $valueLabel = $entry['key'] . '=' . $this->truncateParamValue($entry['value']);
+                    $this->incrementBucket($stats['param_category_stats'][$category]['values'], $valueLabel);
+                }
+            }
+
+            $fbclid = $paramAnalysis['primary']['fbclid'];
+            if ($fbclid !== '') {
+                $fbclidBuckets[$fbclid] = ($fbclidBuckets[$fbclid] ?? 0) + 1;
+            }
+
+            $ff = trim((string)($row->FF ?? ''));
+            if ($ff !== '') {
+                $ffBuckets[$ff] = ($ffBuckets[$ff] ?? 0) + 1;
+            }
+
+            $fbclidCookieCheck = $this->validateFbclidCookieConsistency(
+                $paramAnalysis['primary']['fbclid'],
+                $cookieMap['_fbc'] ?? ''
+            );
+            if (!$fbclidCookieCheck['ok']) {
+                $stats['fbclid_cookie_issues']++;
+            }
+        }
+
+        $stats['paid_users'] = $this->loadPaidUserCount(
+            $rows->pluck('UserID')->filter()->map(function ($userId) {
+                return (int)$userId;
+            })->unique()->values()->all()
+        );
+
+        arsort($stats['platforms']);
+        arsort($stats['url_signs']);
+        arsort($stats['origins']);
+        arsort($stats['utm_sources']);
+        arsort($stats['param_categories']);
+        arsort($stats['param_keys']);
+
+        foreach ($stats['param_category_stats'] as &$categoryStats) {
+            arsort($categoryStats['keys']);
+            arsort($categoryStats['values']);
+        }
+        unset($categoryStats);
+
+        foreach ($fbclidBuckets as $count) {
+            if ($count > 1) {
+                $stats['duplicate_fbclid_groups']++;
+                $stats['duplicate_fbclid_rows'] += $count;
+            }
+        }
+
+        foreach ($ffBuckets as $count) {
+            if ($count > 1) {
+                $stats['duplicate_ff_groups']++;
+                $stats['duplicate_ff_rows'] += $count;
+            }
+        }
+
+        return $stats;
+    }
+
+    protected function buildFbclidGroups($query)
+    {
+        $rows = $query->get([
+            'ac.ID',
+            'ac.Params',
+        ]);
+
+        $groupsByFbclid = [];
+
+        foreach ($rows as $row) {
+            $params = $this->decodeJson($row->Params);
+            $fbclid = $this->analyzeMarketingParams($params)['primary']['fbclid'];
+            if ($fbclid === '') {
+                continue;
+            }
+
+            $groupsByFbclid[$fbclid][] = (int)$row->ID;
+        }
+
+        return $this->finalizeDuplicateGroups($groupsByFbclid, 'fbclid');
+    }
+
+    protected function buildDuplicateValueGroups($query, $field, $type)
+    {
+        $rows = $query->get([
+            'ac.ID',
+            'ac.' . $field,
+        ]);
+
+        $groups = [];
+
+        foreach ($rows as $row) {
+            $value = trim((string)($row->{$field} ?? ''));
+            if ($value === '') {
+                continue;
+            }
+
+            $groups[$value][] = (int)$row->ID;
+        }
+
+        return $this->finalizeDuplicateGroups($groups, $type);
+    }
+
+    protected function finalizeDuplicateGroups(array $groupedIds, $type)
+    {
+        $palettes = [
+            'fbclid' => ['#fff3cd', '#d1ecf1', '#d4edda', '#f8d7da', '#e2d9f3', '#fde2b8', '#d6eaf8', '#f5c6cb'],
+            'ff' => ['#e8f5e9', '#e3f2fd', '#fff8e1', '#fce4ec', '#ede7f6', '#e0f7fa', '#f1f8e9', '#fff3e0'],
+        ];
+
+        $palette = $palettes[$type] ?? ['#f5f5f5'];
+
+        $groups = [];
+        $rowMap = [];
+        $groupIndex = 1;
+
+        foreach ($groupedIds as $value => $ids) {
+            $ids = array_values(array_unique($ids));
+            if (count($ids) < 2) {
+                continue;
+            }
+
+            $color = $palette[($groupIndex - 1) % count($palette)];
+            $group = [
+                'index' => $groupIndex,
+                'value' => $value,
+                'count' => count($ids),
+                'color' => $color,
+                'row_ids' => $ids,
+                'type' => $type,
+            ];
+
+            $groups[] = $group;
+
+            foreach ($ids as $id) {
+                $rowMap[$id] = $group;
+            }
+
+            $groupIndex++;
+        }
+
+        return [
+            'groups' => $groups,
+            'rows' => $rowMap,
+        ];
+    }
+
+    protected function validateFbclidCookieConsistency($paramFbclid, $cookieFbc)
+    {
+        $paramFbclid = trim((string)$paramFbclid);
+        $cookieFbc = trim((string)$cookieFbc);
+        $cookieFbclid = $this->extractFbclidFromFbc($cookieFbc);
+
+        if ($paramFbclid === '' && $cookieFbclid === '') {
+            return [
+                'ok' => true,
+                'status' => 'none',
+                'cookie_fbclid' => '',
+                'message' => '',
+            ];
+        }
+
+        if ($paramFbclid !== '' && $cookieFbclid === '') {
+            return [
+                'ok' => false,
+                'status' => 'missing_cookie_fbc',
+                'cookie_fbclid' => '',
+                'message' => 'Params 有 fbclid,但 Cookie 未解析出 _fbc/fbclid',
+            ];
+        }
+
+        if ($paramFbclid === '' && $cookieFbclid !== '') {
+            return [
+                'ok' => false,
+                'status' => 'missing_param_fbclid',
+                'cookie_fbclid' => $cookieFbclid,
+                'message' => 'Cookie 有 _fbc/fbclid,但 Params 缺少 fbclid',
+            ];
+        }
+
+        if ($paramFbclid === $cookieFbclid) {
+            return [
+                'ok' => true,
+                'status' => 'match',
+                'cookie_fbclid' => $cookieFbclid,
+                'message' => '匹配',
+            ];
+        }
+
+        return [
+            'ok' => false,
+            'status' => 'mismatch',
+            'cookie_fbclid' => $cookieFbclid,
+            'message' => 'Params.fbclid 与 Cookie._fbc 中的 fbclid 不一致',
+        ];
+    }
+
+    protected function extractFbclidFromFbc($cookieFbc)
+    {
+        $cookieFbc = trim((string)$cookieFbc);
+        if ($cookieFbc === '') {
+            return '';
+        }
+
+        $parts = explode('.', $cookieFbc, 4);
+        if (count($parts) === 4 && $parts[0] === 'fb' && in_array($parts[1], ['1', '2'], true)) {
+            return trim((string)$parts[3]);
+        }
+
+        return $cookieFbc;
+    }
+
+    protected function incrementBucket(array &$bucket, $key)
+    {
+        $bucket[$key] = ($bucket[$key] ?? 0) + 1;
+    }
+
+    protected function splitCsv($value)
+    {
+        return array_values(array_filter(array_map('trim', explode(',', (string)$value)), function ($item) {
+            return $item !== '';
+        }));
+    }
+
+    protected function normalizeDateBoundary($value, $endOfDay = false)
+    {
+        $value = trim((string)$value);
+        if ($value === '') {
+            return '';
+        }
+
+        $value = substr(str_replace('T', ' ', $value), 0, 10);
+        return $value . ($endOfDay ? ' 23:59:59' : ' 00:00:00');
+    }
+
+    protected function decodeJson($json)
+    {
+        $data = json_decode((string)$json, true);
+        return is_array($data) ? $data : [];
+    }
+
+    protected function analyzeMarketingParams(array $params)
+    {
+        $analysis = [
+            'flat' => [],
+            'categories' => [],
+            'primary' => [
+                'channel' => '',
+                'utm_source' => '',
+                'utm_medium' => '',
+                'utm_campaign' => '',
+                'campaign' => '',
+                'adgroup' => '',
+                'creative' => '',
+                'pixel' => '',
+                'fbclid' => '',
+            ],
+        ];
+
+        foreach ($params as $key => $value) {
+            if (is_array($value) || is_object($value)) {
+                $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+            }
+
+            $key = trim((string)$key);
+            $value = trim((string)$value);
+            if ($key === '' || $value === '') {
+                continue;
+            }
+
+            $category = $this->detectParamCategory($key);
+
+            $analysis['flat'][$key] = $value;
+            $analysis['categories'][$category][] = [
+                'key' => $key,
+                'value' => $value,
+            ];
+        }
+
+        $analysis['primary']['channel'] = $analysis['flat']['c'] ?? ($analysis['flat']['channel'] ?? '');
+        $analysis['primary']['utm_source'] = $analysis['flat']['utm_source'] ?? '';
+        $analysis['primary']['utm_medium'] = $analysis['flat']['utm_medium'] ?? '';
+        $analysis['primary']['utm_campaign'] = $analysis['flat']['utm_campaign'] ?? '';
+        $analysis['primary']['campaign'] = $analysis['flat']['campaign'] ?? ($analysis['flat']['utm_campaign'] ?? '');
+        $analysis['primary']['adgroup'] = $analysis['flat']['adgroup'] ?? ($analysis['flat']['adset'] ?? ($analysis['flat']['utm_term'] ?? ''));
+        $analysis['primary']['creative'] = $analysis['flat']['creative'] ?? ($analysis['flat']['utm_content'] ?? '');
+        $analysis['primary']['pixel'] = $analysis['flat']['pixel'] ?? ($analysis['flat']['pixelID'] ?? ($analysis['flat']['pixel_id'] ?? ''));
+        $analysis['primary']['fbclid'] = $analysis['flat']['fbclid'] ?? '';
+
+        return $analysis;
+    }
+
+    protected function detectParamCategory($key)
+    {
+        $normalizedKey = strtolower(trim((string)$key));
+
+        if (strpos($normalizedKey, 'utm_') === 0) {
+            return 'utm';
+        }
+
+        if (in_array($normalizedKey, ['campaign', 'adgroup', 'adset', 'ad_id', 'creative', 'creative_id', 'placement', 'site_source_name'], true)) {
+            return 'campaign';
+        }
+
+        if (in_array($normalizedKey, ['fbclid', 'gclid', 'ttclid', 'msclkid', 'wbraid', 'gbraid', 'fbc', 'fbp', '_fbc', '_fbp', 'pixel', 'pixelid', 'pixel_id'], true)) {
+            return 'attribution';
+        }
+
+        if (in_array($normalizedKey, ['c', 'channel', 'source', 'media_source', 'utm_source_platform', 'pid', 'af_channel', 'sub_channel'], true)) {
+            return 'channel';
+        }
+
+        if (preg_match('/(_id|id)$/', $normalizedKey)) {
+            return 'identifier';
+        }
+
+        return 'custom';
+    }
+
+    protected function getParamCategoryLabel($category)
+    {
+        $labels = [
+            'utm' => 'UTM',
+            'campaign' => 'Campaign',
+            'attribution' => 'Attribution',
+            'channel' => 'Channel',
+            'identifier' => 'Identifier',
+            'custom' => 'Custom',
+        ];
+
+        return $labels[$category] ?? ucfirst($category);
+    }
+
+    protected function getCategoryKeys($category)
+    {
+        $map = [
+            'utm' => ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'utm_source_platform'],
+            'campaign' => ['campaign', 'adgroup', 'adset', 'ad_id', 'creative', 'creative_id', 'placement', 'site_source_name'],
+            'attribution' => ['fbclid', 'gclid', 'ttclid', 'msclkid', 'wbraid', 'gbraid', 'fbc', 'fbp', '_fbc', '_fbp', 'pixel', 'pixelID', 'pixel_id'],
+            'channel' => ['c', 'channel', 'source', 'media_source', 'pid', 'af_channel', 'sub_channel'],
+            'identifier' => ['utm_id', 'campaign_id', 'adgroup_id', 'adset_id', 'creative_id', 'pixel_id', 'pixelID'],
+        ];
+
+        return $map[$category] ?? [];
+    }
+
+    protected function truncateParamValue($value, $length = 36)
+    {
+        $value = (string)$value;
+        if ($value === '') {
+            return '-';
+        }
+
+        return mb_strlen($value) > $length ? mb_substr($value, 0, $length) . '...' : $value;
+    }
+
+    protected function makeCookieKey($row)
+    {
+        if (!empty($row->FPID)) {
+            return 'fpid:' . $row->FPID;
+        }
+        if (!empty($row->FF)) {
+            return 'ff:' . $row->FF;
+        }
+        if (!empty($row->UserID)) {
+            return 'uid:' . $row->UserID;
+        }
+        return 'row:' . ($row->ID ?? '');
+    }
+
+    protected function parseCookieString($cookieString)
+    {
+        $cookies = [];
+        foreach (explode(';', (string)$cookieString) as $part) {
+            $part = trim($part);
+            if ($part === '' || strpos($part, '=') === false) {
+                continue;
+            }
+            [$name, $value] = explode('=', $part, 2);
+            $cookies[trim($name)] = trim($value);
+        }
+        return $cookies;
+    }
+
+    protected function analyzeUserAgent($ua)
+    {
+        $ua = (string)$ua;
+        $app = 'Browser';
+        if (stripos($ua, 'Instagram') !== false) {
+            $app = 'Instagram';
+        } elseif (stripos($ua, 'FBAN') !== false || stripos($ua, 'FBAV') !== false || stripos($ua, 'Facebook') !== false) {
+            $app = 'Facebook';
+        }
+
+        $os = 'Unknown';
+        if (stripos($ua, 'iPhone') !== false || stripos($ua, 'iPad') !== false || stripos($ua, 'iOS') !== false) {
+            $os = 'iOS';
+        } elseif (stripos($ua, 'Android') !== false) {
+            $os = 'Android';
+        }
+
+        $device = 'Unknown';
+        if (stripos($ua, 'iPhone') !== false) {
+            $device = 'iPhone';
+        } elseif (stripos($ua, 'iPad') !== false) {
+            $device = 'iPad';
+        } elseif (stripos($ua, 'Android') !== false) {
+            $device = 'Android';
+        }
+
+        return compact('app', 'os', 'device');
+    }
+
+    protected function loadPaidUserCount(array $userIds)
+    {
+        if (empty($userIds)) {
+            return 0;
+        }
+
+        return DB::connection('read')
+            ->table('agent.dbo.order')
+            ->whereIn('user_id', $userIds)
+            ->where('pay_status', 1)
+            ->distinct('user_id')
+            ->count('user_id');
+    }
+
+    protected function loadPayStats(array $userIds)
+    {
+        if (empty($userIds)) {
+            return [];
+        }
+
+        return DB::connection('read')
+            ->table('agent.dbo.order')
+            ->whereIn('user_id', $userIds)
+            ->where('pay_status', 1)
+            ->groupBy('user_id')
+            ->selectRaw('user_id, count(*) as pay_order_count, cast(sum(amount) as decimal(18,2)) as pay_amount_sum, max(pay_at) as last_pay_at')
+            ->get()
+            ->keyBy('user_id')
+            ->all();
+    }
+
+    protected function loadAdjustEventStats(array $userIds, array $filters)
+    {
+        if (empty($userIds)) {
+            return [];
+        }
+
+        $result = [];
+        $userIdMap = array_fill_keys(array_map('strval', $userIds), true);
+
+        foreach ($this->resolveAdjustLogFiles($filters) as $logFile) {
+            $handle = @fopen($logFile, 'r');
+            if ($handle === false) {
+                continue;
+            }
+
+            $currentUserId = null;
+
+            while (($line = fgets($handle)) !== false) {
+                $userId = $this->extractAdjustUserId($line);
+                $status = $this->classifyAdjustLine($line);
+
+                if ($userId !== null && isset($userIdMap[(string)$userId])) {
+                    $currentUserId = $userId;
+                } elseif ($userId === null && $currentUserId !== null && in_array($status, ['request', 'success', 'skipped', 'failed'], true)) {
+                    $userId = $currentUserId;
+                } else {
+                    if ($status === 'enter') {
+                        $currentUserId = null;
+                    }
+                    continue;
+                }
+
+                if (!isset($result[$userId])) {
+                    $result[$userId] = ['status' => 'enter', 'logs' => []];
+                }
+
+                if ($status !== null) {
+                    $result[$userId]['status'] = $status;
+                }
+
+                if (count($result[$userId]['logs']) < 50) {
+                    $result[$userId]['logs'][] = trim($line);
+                }
+            }
+
+            fclose($handle);
+        }
+
+        return $result;
+    }
+
+    protected function resolveAdjustLogFiles(array $filters)
+    {
+        $start = substr($filters['date_start'] ?: $filters['register_start'], 0, 10);
+        $end = substr($filters['date_end'] ?: $filters['register_end'], 0, 10);
+        if ($start === '') {
+            $start = date('Y-m-d');
+        }
+        if ($end === '') {
+            $end = $start;
+        }
+
+        $startTs = strtotime($start);
+        $endTs = strtotime($end);
+        if ($startTs === false || $endTs === false) {
+            return [];
+        }
+        if ($endTs < $startTs) {
+            [$startTs, $endTs] = [$endTs, $startTs];
+        }
+
+        $files = [];
+        $days = 0;
+        for ($time = $startTs; $time <= $endTs && $days < 7; $time += 86400, $days++) {
+            $file = storage_path('logs/adjustEvent-' . date('Y-m-d', $time) . '.log');
+            if (file_exists($file)) {
+                $files[] = $file;
+            }
+        }
+
+        return $files;
+    }
+
+    protected function extractAdjustUserId($line)
+    {
+        if (preg_match('/"UserID":"?(\d+)"?/', $line, $matches)) {
+            return (int)$matches[1];
+        }
+        if (preg_match('/"user_id":"?(\d+)"?/', $line, $matches)) {
+            return (int)$matches[1];
+        }
+        return null;
+    }
+
+    protected function classifyAdjustLine($line)
+    {
+        if (strpos($line, 'facebook s2s response') !== false) {
+            return 'success';
+        }
+        if (strpos($line, 'facebook s2s request') !== false) {
+            return 'request';
+        }
+        if (strpos($line, 'facebook s2s skipped') !== false) {
+            return 'skipped';
+        }
+        if (strpos($line, 'facebook s2s failed') !== false) {
+            return 'failed';
+        }
+        if (strpos($line, 'enter:') !== false) {
+            return 'enter';
+        }
+        return null;
+    }
+
+    protected function extractChannelFromCookie($userID, $user = null)
+    {
+        $cookieInfo = \App\Services\ApkService::loadCookie($userID, $user->FPID ?? '', $user->FF ?? '');
+        if (!$cookieInfo || empty($cookieInfo['Params'])) {
+            return null;
+        }
+
+        $params = json_decode($cookieInfo['Params'], true);
+        if (!is_array($params)) {
+            return null;
+        }
+
+        return $params['c'] ?? null;
+    }
+}

+ 42 - 2
app/Http/Controllers/Admin/ExtensionNewController.php

@@ -79,8 +79,8 @@ class ExtensionNewController extends Controller
             'min_commission' => DB::table('system_config')->where('key', 'min_commission')->value('value') ?? 0,
             'max_commission' => DB::table('system_config')->where('key', 'max_commission')->value('value') ?? 0,
             'auto_verify' => DB::connection('write')->table('QPAccountsDB.dbo.SystemStatusInfo')
-                ->where('StatusName', 'AutoVerify')
-                ->value('StatusValue') ?? 0
+                    ->where('StatusName', 'AutoVerify')
+                    ->value('StatusValue') ?? 0
         ];
 
         return view('admin.extension_new.verify', compact('list', 'config'));
@@ -891,4 +891,44 @@ class ExtensionNewController extends Controller
 
         return view('admin.extension_new.bind_list', $result);
     }
+
+    /**
+     * 用户下级查询
+     * @return mixed
+     */
+    public function subordinate(Request $request)
+    {
+        $query = DB::connection('mysql')->table('webgame.AgentUser');
+
+        if (!$request->input('GameID')) {
+            return view('admin.extension_new.subordinate', []);
+        }
+        $userID = DB::table('QPAccountsDB.dbo.AccountsInfo')
+            ->where('GameID', $request->input('GameID'))
+            ->value('UserID');
+        if (!$userID) {
+            return view('admin.extension_new.subordinate', []);
+        }
+        $query->where('AgentUser.Higher1ID', $userID);
+        $list = $query->paginate(15);
+        $userIDs = $list->pluck('UserID')->toArray();
+        if ($userIDs) {
+            $users = DB::table('QPAccountsDB.dbo.AccountsInfo')
+                ->whereIn('UserID', $userIDs)
+                ->get();
+            foreach ($list as $k => $v) {
+                foreach ($users as $k1 => $v1) {
+                    if ($v1->UserID == $v->UserID) {
+                        $list[$k]->GameID = $v1->GameID;
+                        $list[$k]->RegisterDate = $v1->RegisterDate;
+                    }
+                }
+            }
+        }
+
+        return view('admin.extension_new.subordinate', [
+            'request' => $request,
+            'list' => $list,
+        ]);
+    }
 }

+ 76 - 152
app/Http/Controllers/Admin/GlobalController.php

@@ -95,8 +95,8 @@ class GlobalController extends Controller
 
         // 签到总奖励
         $totalSignIn = DB::connection('read')->table('QPAccountsDB.dbo.UserSignInInfo')
-                ->selectRaw('IsNull(sum(TotalReward),0) TotalReward')
-                ->first()->TotalReward / 100 ?? 0;
+            ->selectRaw('IsNull(sum(TotalReward),0) TotalReward')
+            ->first()->TotalReward / 100 ?? 0;
 
         //总付费用户
         $pay_user_count = $order->pay_user_count ?? 0;
@@ -112,9 +112,9 @@ class GlobalController extends Controller
 
         // 累计提现回收金额
         $WithDrawRecovery = DB::connection('read')->table('QPAccountsDB.dbo.OrderWithDraw as ow')
-                ->where('State', 4)
-                ->selectRaw('sum(WithDraw+ServiceFee) as WithDraw')
-                ->first()->WithDraw ?? 0;
+            ->where('State', 4)
+            ->selectRaw('sum(WithDraw+ServiceFee) as WithDraw')
+            ->first()->WithDraw ?? 0;
 
         $AllWorkingScore = number_float(($gameInfo->flowing_water + $gameInfo->Revenue) / 100);
 
@@ -196,10 +196,10 @@ class GlobalController extends Controller
 
         // 签到
         $signIn = DB::connection('read')->table('QPRecordDB.dbo.RecordSignIn')
-                ->where('SignInDate', '>=', date("Y-m-d 00:00:00", strtotime("$start_time")))
-                ->where('SignInDate', '<=', date("Y-m-d 23:59:59", strtotime("$end_time")))
-                ->selectRaw('IsNull(sum(RewardScore),0) RewardScore')
-                ->first()->RewardScore / 100 ?? 0;
+            ->where('SignInDate', '>=', date("Y-m-d 00:00:00", strtotime("$start_time")))
+            ->where('SignInDate', '<=', date("Y-m-d 23:59:59", strtotime("$end_time")))
+            ->selectRaw('IsNull(sum(RewardScore),0) RewardScore')
+            ->first()->RewardScore / 100 ?? 0;
 
 
         // 推广总奖励
@@ -233,13 +233,13 @@ class GlobalController extends Controller
         // 非正式会员人数-- 进入TP试玩场人数
 
         $notMemberNum = DB::connection('read')->table('QPTreasureDB.dbo.RecordUserInout as ri')
-                ->join('QPPlatformDB.dbo.GameRoomInfo as gi', 'ri.ServerID', 'gi.ServerID')
-                ->where('gi.ServerType', 2)
-                ->where('GameID', 1005)
-                ->where('ri.EnterTime', '>=', date("Y-m-d 00:00:00", strtotime("$start_time")))
-                ->where('ri.EnterTime', '<=', date("Y-m-d 23:59:59", strtotime("$end_time")))
-                ->selectRaw('count(distinct(ri.UserID)) count_u')
-                ->first()->count_u ?? 0;
+            ->join('QPPlatformDB.dbo.GameRoomInfo as gi', 'ri.ServerID', 'gi.ServerID')
+            ->where('gi.ServerType', 2)
+            ->where('GameID', 1005)
+            ->where('ri.EnterTime', '>=', date("Y-m-d 00:00:00", strtotime("$start_time")))
+            ->where('ri.EnterTime', '<=', date("Y-m-d 23:59:59", strtotime("$end_time")))
+            ->selectRaw('count(distinct(ri.UserID)) count_u')
+            ->first()->count_u ?? 0;
 
         // 谷歌渠道--付费人数--付费金额
         $google_pay = DB::connection('read')->table('agent.dbo.order as o')
@@ -938,23 +938,23 @@ class GlobalController extends Controller
             $userid = $request->UserID;
 
             $mac = DB::connection('read')->table('QPRecordDB.dbo.RecordUserLogonStatistics')
-                     ->where('UserID', $userid)
-                     ->where('mac', '<>','')
-                     ->pluck('mac')->toArray();
+                ->where('UserID', $userid)
+                ->where('mac', '<>','')
+                ->pluck('mac')->toArray();
             $mac=array_unique($mac);
 
 
             $list=[];
             if($mac) {
                 $userids= DB::connection('read')->table('QPRecordDB.dbo.RecordUserLogonStatistics')
-                         ->whereIn('mac', $mac)
-                         ->pluck('UserID')
-                         ->toArray();
+                    ->whereIn('mac', $mac)
+                    ->pluck('UserID')
+                    ->toArray();
                 $userids=array_unique($userids);
                 $list = DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo')
-                          ->whereIn('UserID', $userids)
-                          ->selectRaw('RegisterDate,LastLogonDate,GameID,Compellation,Channel,NickName,UserID')
-                          ->paginate(100);
+                    ->whereIn('UserID', $userids)
+                    ->selectRaw('RegisterDate,LastLogonDate,GameID,Compellation,Channel,NickName,UserID')
+                    ->paginate(100);
             }
 
 
@@ -1104,10 +1104,10 @@ class GlobalController extends Controller
             $UserID = $request->UserID;
             $cpf=Cpf::getCpf($UserID,1);
             $list = DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo as ai')
-                      ->whereIn('ai.UserID', Cpf::getCpf($UserID))
-                      ->where('IsAndroid', 0)
-                      ->selectRaw('RegisterDate,LastLogonDate,GameID,Compellation,Channel,NickName,ai.UserID')
-                      ->paginate(1000);
+                ->whereIn('ai.UserID', Cpf::getCpf($UserID))
+                ->where('IsAndroid', 0)
+                ->selectRaw('RegisterDate,LastLogonDate,GameID,Compellation,Channel,NickName,ai.UserID')
+                ->paginate(1000);
 
             // 获取总充值和总提现
             $userIds = $list->pluck('UserID')->toArray();
@@ -1179,39 +1179,25 @@ class GlobalController extends Controller
     }
 
 
-    //用户单控系统-详细用户列表 修改
+    //用户单控系统-详细用户列表 修改(简化版:只控制金额和整体概率)
     public function dk_userlist_edit(Request $request)
     {
         $UserID = $request->UserID ?? '';
-        $TempID = $request->TempID ?? '';
-
-        $ControlModel = new Control();
         $build_sql = DB::connection('write')->table('QPTreasureDB.dbo.UserScoreControl');
 
         if ($request->isMethod('post')) {
 
             $post = $request->post();
 
-            if (!empty($post['Template'])) {
-                $cc=new ControlController();
-                $Template=(array)$cc->getConfigByID($post['Template']);
-                $post = array_merge($Template, $post);
-            }
-
+            $ControlScore = (float)($post['ControlScore'] ?? 0);
+            $Probability = (float)($post['Probability'] ?? 0);
+            $Remarks = $post['Remarks'] ?? '';
 
-//            if (!empty($post['TPBai']) && empty($post['TpBaiGear'])) {
-//                return apiReturnFail('TP百人额度未设置');
-//            }
-//            if (!empty($post['ABBai']) && empty($post['ABBaiGear'])) {
-//                return apiReturnFail('A&B额度未设置');
-//            }
-//            if (empty($post['Template']) && empty($post['TP']) && empty($post['TPAK47']) && empty($post['TPJOKER']) && empty($post['Rummy5']) && empty($post['Rummy2'])) {
-//                return apiReturnFail('概率没有配置,无法提交!');
-//            }
-            DB::connection('write')->table('QPTreasureDB.dbo.UserControlServer')->where('UserID', $UserID)->delete();
+            // 概率范围 0-100
+            if ($Probability < 0 || $Probability > 100) {
+                return apiReturnFail('概率必须在0-100之间');
+            }
 
-            $ControlScore = $post['ControlScore'];
-            $Remarks = $post['Remarks'];
             $admin_id = session('admin')->id;
             $admin_user = DB::connection('read')->table('agent.dbo.admin_users')->where('id', $admin_id)->first();
             /*管理员彩金控制*/
@@ -1220,37 +1206,21 @@ class GlobalController extends Controller
                 AdminScore::add_score($admin_user, abs($ControlScore), 2);
             }
 
-            /*用户单控记录添加*/
-            $GameData = (new Control())->GameData;
-            foreach ($post as $key => &$value) {
-                if ($key == 'ABBaiGear') {
-                    continue;
-                }
-                if (!isset($GameData[$key])) {
-                    unset($post[$key]);
-                }
-            }
-
-            // 控制分数
+            // 记录单控操作(只记金额和总体概率)
             $prefix = $ControlScore < 0 ? '输' : '赢';
-            // 概率
-            $str = '';
-            foreach ($post as $key => $value) {
-                $str .= $key . ':' . number_float($value) . '% ,';
-            }
-            $content = $prefix . $ControlScore . '/' . rtrim($str, ',');
+            $content = $prefix . $ControlScore . '/ 概率:' . number_float($Probability) . '%';
             $score = DB::connection('read')->table('QPTreasureDB.dbo.GameScoreInfo')->where('UserID', $UserID)->select('Score')->first()->Score ?? 0;
             ControlRecord::record_add($UserID, $admin_id, $content, $score);
-            // 用户控制游戏和场次
-            $ControlModel->userControl($post, $UserID);
+
             $data = [
                 'ControlScore' => (int)($ControlScore * NumConfig::NUM_VALUE),
                 'EffectiveScore' => 0,
                 'ControlKindID' => -1,
                 'Remarks' => $Remarks,
                 'InsertDate' => date('Y-m-d H:i:s'),
-                'ControlRadian' => 0,
-                'UserID'=>$UserID
+                // 概率直接写入 ControlRadian,不再区分游戏
+                'ControlRadian' => $Probability,
+                'UserID' => $UserID
             ];
 
             $build_sql->updateOrInsert(['UserID' => $UserID], $data);
@@ -1263,59 +1233,13 @@ class GlobalController extends Controller
             return $this->json(200, "修改成功");
         }
 
-        $ControlScore = $build_sql->where('UserID', $UserID)->first()->ControlScore ?? '0';
-
-        $GameKindItem = DB::connection('read')->table('QPPlatformDB.dbo.GameKindItem')->pluck('KindID')->toArray();
-
-        $UserControlKind = DB::connection('write')->table('QPTreasureDB.dbo.UserControlKind')
-            ->where('UserID', $UserID)
-            ->selectRaw('(ControlRadian) ControlRadian,KindID')
-            ->pluck('ControlRadian', 'KindID')->toArray();
-
-        $GameData = array_flip($ControlModel->GameData);
-
-
-        foreach ($GameKindItem as $value) {
-            if (!empty($GameData[$value] ?? '')) $UserControlKind[$GameData[$value]] = $UserControlKind[$value] ?? '';
-        }
-
-
-//        $TPBai = $UserControlKind['TPBai'];
-//        $ABBai = $UserControlKind['ABBai'];
-
-//        $substr = substr($TPBai, -3);
-//        $UserControlKind['TPBai'] = (int)$substr;
-//        $UserControlKind['TPBaiGear'] = !empty($TPBai) ? ($TPBai - $substr) / 1000 : '';
-
-//        $substr = substr($ABBai, -3);
-//        $UserControlKind['ABBai'] = (int)$substr;
-//        $UserControlKind['ABBaiGear'] = !empty($ABBai) ? ($ABBai - $substr) / 1000 : '';
+        $row = $build_sql->where('UserID', $UserID)->first();
+        $ControlScore = $row->ControlScore ?? 0;
+        $Probability = $row->ControlRadian ?? 0;
 
-        $amount = DB::connection('write')->table('agent.dbo.order')
-                ->where('user_id', $UserID)
-                ->where('pay_status', 1)
-                ->orderByDesc('pay_at')
-                ->first()->amount ?? '';
+        $ControlScore = $ControlScore / NumConfig::NUM_VALUE;
 
-
-        !empty($amount) && $amount = (int)($amount / 100);
-//        $ControlScore = empty($ControlScore) ? $amount : $ControlScore / NumConfig::NUM_VALUE;
-        $ControlScore =  $ControlScore / NumConfig::NUM_VALUE;
-
-
-//        $list = DB::connection('write')->table('agent.dbo.ControlConfig')->get();
-        $cc=new ControlController();
-        $list=$cc->getList();
-
-
-//        $TpBaiGear = DB::connection('write')->table('agent.dbo.ControlGear')->where('KindID', 2010)->get();
-        $ABBai = DB::connection('write')->table('agent.dbo.ControlGear')->where('KindID', 2012)->get();
-
-        $ControlModel = new Control();
-
-        $GameDataText=$ControlModel->GameDataText;
-
-        $data = compact('UserID', 'UserControlKind', 'ControlScore', 'list', 'TempID', 'ABBai','GameDataText');
+        $data = compact('UserID', 'ControlScore', 'Probability');
 
         return view('admin.global.dk_userlist_edit', $data);
 
@@ -1824,13 +1748,13 @@ class GlobalController extends Controller
 
         // 普通玩家 -- 控制分数
         $ptAccounts = DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo as ai')
-                ->leftJoin('QPAccountsDB.dbo.IDWhiteUser as iu', 'ai.UserID', 'iu.UserID')
-                ->join('QPRecordDB.dbo.RecordUserScoreControl as rc', 'ai.UserID', 'rc.UserID')
-                ->whereNull('iu.UserID')
-                ->where('ControlDate', '>', $now_time)
-                ->where('ControlState', 0)
-                ->selectRaw('sum(abs(ControlScore)) / 100 ControlScore')
-                ->first()->ControlScore ?? 0;
+            ->leftJoin('QPAccountsDB.dbo.IDWhiteUser as iu', 'ai.UserID', 'iu.UserID')
+            ->join('QPRecordDB.dbo.RecordUserScoreControl as rc', 'ai.UserID', 'rc.UserID')
+            ->whereNull('iu.UserID')
+            ->where('ControlDate', '>', $now_time)
+            ->where('ControlState', 0)
+            ->selectRaw('sum(abs(ControlScore)) / 100 ControlScore')
+            ->first()->ControlScore ?? 0;
 
         // 普通玩家 -- 控制赢分数
         $ptWinAccounts = DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo as ai')
@@ -1846,14 +1770,14 @@ class GlobalController extends Controller
 
         // 普通玩家 -- 控制输分数
         $ptLoseAccounts = DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo as ai')
-                ->leftJoin('QPAccountsDB.dbo.IDWhiteUser as iu', 'ai.UserID', 'iu.UserID')
-                ->join('QPRecordDB.dbo.RecordUserScoreControl as rc', 'ai.UserID', 'rc.UserID')
-                ->whereNull('iu.UserID')
-                ->where('ControlDate', '>', $now_time)
-                ->where('ControlState', 0)
-                ->where('ControlScore', '<', 0)
-                ->selectRaw('sum(ControlScore) / 100 ControlScore')
-                ->first()->ControlScore ?? 0;
+            ->leftJoin('QPAccountsDB.dbo.IDWhiteUser as iu', 'ai.UserID', 'iu.UserID')
+            ->join('QPRecordDB.dbo.RecordUserScoreControl as rc', 'ai.UserID', 'rc.UserID')
+            ->whereNull('iu.UserID')
+            ->where('ControlDate', '>', $now_time)
+            ->where('ControlState', 0)
+            ->where('ControlScore', '<', 0)
+            ->selectRaw('sum(ControlScore) / 100 ControlScore')
+            ->first()->ControlScore ?? 0;
 
 
         // +-------------------------------------
@@ -1862,12 +1786,12 @@ class GlobalController extends Controller
 
         // 官方玩家 -- 控制分数
         $gfAccounts = DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo as ai')
-                ->join('QPAccountsDB.dbo.IDWhiteUser as iu', 'ai.UserID', 'iu.UserID')
-                ->join('QPRecordDB.dbo.RecordUserScoreControl as rc', 'ai.UserID', 'rc.UserID')
-                ->where('ControlState', 0)
-                ->where('ControlDate', '>', $now_time)
-                ->selectRaw('sum(abs(ControlScore)) / 100 ControlScore')
-                ->first()->ControlScore ?? 0;
+            ->join('QPAccountsDB.dbo.IDWhiteUser as iu', 'ai.UserID', 'iu.UserID')
+            ->join('QPRecordDB.dbo.RecordUserScoreControl as rc', 'ai.UserID', 'rc.UserID')
+            ->where('ControlState', 0)
+            ->where('ControlDate', '>', $now_time)
+            ->selectRaw('sum(abs(ControlScore)) / 100 ControlScore')
+            ->first()->ControlScore ?? 0;
 
         // 官方玩家 -- 控制赢分数
         $gfWinAccounts = DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo as ai')
@@ -1881,13 +1805,13 @@ class GlobalController extends Controller
 
         // 官方玩家 -- 控制输分数
         $gfLoseAccounts = DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo as ai')
-                ->join('QPAccountsDB.dbo.IDWhiteUser as iu', 'ai.UserID', 'iu.UserID')
-                ->join('QPRecordDB.dbo.RecordUserScoreControl as rc', 'ai.UserID', 'rc.UserID')
-                ->where('ControlState', 0)
-                ->where('ControlDate', '>', $now_time)
-                ->where('ControlScore', '<', 0)
-                ->selectRaw('sum(ControlScore) / 100 ControlScore')
-                ->first()->ControlScore ?? 0;
+            ->join('QPAccountsDB.dbo.IDWhiteUser as iu', 'ai.UserID', 'iu.UserID')
+            ->join('QPRecordDB.dbo.RecordUserScoreControl as rc', 'ai.UserID', 'rc.UserID')
+            ->where('ControlState', 0)
+            ->where('ControlDate', '>', $now_time)
+            ->where('ControlScore', '<', 0)
+            ->selectRaw('sum(ControlScore) / 100 ControlScore')
+            ->first()->ControlScore ?? 0;
 
         $ptTotal = $ptLoseAccounts + ($ptWinAccounts->EffectiveScore ?? 0);
         $gfTotal = $gfLoseAccounts + ($gfWinAccounts->EffectiveScore ?? 0);

+ 3 - 0
app/Http/Controllers/Admin/MailController.php

@@ -150,6 +150,9 @@ class MailController extends Controller
                     return $this->json(500, '彩金额度不足');
                 }
                 $change_score = $value['number'];
+                if (!preg_match('/^\d+$/', $change_score)) {
+                    return $this->json(500, '金额必须是整数');
+                }
 
                 $value['number'] = $value['number'] * 100;
                 $bonus .= implode(',', $value) . ';';

+ 123 - 0
app/Http/Controllers/Admin/ProtectLevelController.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use App\Http\helper\NumConfig;
+use stdClass;
+
+class ProtectLevelController extends Controller
+{
+    protected $table = 'QPAccountsDB.dbo.ProtectLevel';
+
+    // 列表
+    public function index()
+    {
+        $list = DB::connection('read')->table($this->table)
+            ->orderBy('VIP')
+            ->get();
+
+        return view('admin.protect_level.index', compact('list'));
+    }
+
+    // 增加
+    public function add(Request $request)
+    {
+        if ($request->isMethod('get')) {
+            return view('admin.protect_level.add');
+        }
+
+        $data = $request->only([
+            'ID','Recharge','GrantNum','VIP','LevelUpBonus',
+            'MinRecharge','WithdrawLimit','DailyWithdraws',
+            'WithdrawFeeRate','SignAlpha','CustomServiceType',
+            'RechargeExtraSendRate','SuperballNum','BirthdayValue',
+        ]);
+        // 将 GrantNum 存储为原值 * NumConfig::NUM_VALUE
+        if (isset($data['GrantNum'])) {
+            $data['GrantNum'] = intval($data['GrantNum'] * NumConfig::NUM_VALUE);
+        }
+
+        // 简单验证(ID唯一性后面手动检查)
+        $this->validate($request, [
+            'ID'            => 'required|integer',
+            'Recharge'      => 'required|numeric|min:0',
+            'GrantNum'      => 'required|integer|min:0',
+            'VIP'           => 'required|integer|min:0',
+            'LevelUpBonus'  => 'required|integer|min:0',
+            'MinRecharge'   => 'required|integer|min:0',
+            'WithdrawLimit' => 'required|integer|min:0',
+            'DailyWithdraws'=> 'required|integer|min:0|max:255',
+            'WithdrawFeeRate'=> 'required|numeric|min:0',
+            'SignAlpha'     => 'required|integer|min:0',
+            'CustomServiceType' => 'required|integer|in:1,2',
+            'RechargeExtraSendRate' => 'required|numeric|min:0',
+            'SuperballNum'  => 'required|integer|min:0',
+            'BirthdayValue' => 'required|integer|min:0',
+        ]);
+        // 唯一性检查
+        if (DB::connection('read')->table($this->table)->where('ID', $data['ID'])->exists()) {
+            return apiReturnFail('ID already exists');
+        }
+        $data['BirthdayValue'] = $data['BirthdayValue'] * NumConfig::NUM_VALUE;
+        DB::connection('write')->table($this->table)->insert($data);
+        return apiReturnSuc();
+    }
+
+    // 修改
+    public function edit(Request $request, $id)
+    {
+        if ($request->isMethod('get')) {
+            $info = DB::connection('read')->table($this->table)
+                ->where('ID', $id)
+                ->first();
+            return view('admin.protect_level.edit', compact('info'));
+        }
+
+        $post = $request->only([
+            'Recharge','GrantNum','VIP','LevelUpBonus',
+            'MinRecharge','WithdrawLimit','DailyWithdraws',
+            'WithdrawFeeRate','SignAlpha','CustomServiceType',
+            'RechargeExtraSendRate','SuperballNum','BirthdayValue',
+        ]);
+        // GrantNum 乘以系数保存
+        if (isset($post['GrantNum'])) {
+            $post['GrantNum'] = intval($post['GrantNum'] * NumConfig::NUM_VALUE);
+        }
+
+        // 字段校验
+        $this->validate($request, [
+            'Recharge'      => 'required|numeric|min:0',
+            'GrantNum'      => 'required|integer|min:0',
+            'VIP'           => 'required|integer|min:0',
+            'LevelUpBonus'  => 'required|integer|min:0',
+            'MinRecharge'   => 'required|integer|min:0',
+            'WithdrawLimit' => 'required|integer|min:0',
+            'DailyWithdraws'=> 'required|integer|min:0|max:255',
+            'WithdrawFeeRate'=> 'required|numeric|min:0',
+            'SignAlpha'     => 'required|integer|min:0',
+            'CustomServiceType' => 'required|integer|in:1,2',
+            'RechargeExtraSendRate' => 'required|numeric|min:0',
+            'SuperballNum'  => 'required|integer|min:0',
+            'BirthdayValue' => 'required|integer|min:0',
+        ]);
+        $post['BirthdayValue'] = $post['BirthdayValue'] * NumConfig::NUM_VALUE;
+        DB::connection('write')->table($this->table)
+            ->where('ID', $id)
+            ->update($post);
+
+        return apiReturnSuc();
+    }
+
+    // 删除
+    public function delete($id)
+    {
+        DB::connection('write')->table($this->table)
+            ->where('ID', $id)
+            ->delete();
+
+        return apiReturnSuc();
+    }
+}

+ 19 - 4
app/Http/Controllers/Admin/RechargeController.php

@@ -172,13 +172,14 @@ class RechargeController extends Controller
 
         $order ? $orderby = 'g.price desc' : $orderby = 'sum(o.amount) desc';
 
-        // 充值总金额
+        // 充值总金额、手续费汇总(管理员可见)
         $payTotalMoney = DB::connection('write')->table('agent.dbo.order as o')
             ->leftJoin('QPAccountsDB.dbo.AccountsInfo as ai', 'o.user_id', '=', 'ai.UserID')
             ->lock('with(nolock)')
-            ->where($where)->selectRaw('sum(amount) as amount,count(distinct(user_id)) count_u,count(id) count_id')->first();
+            ->where($where)->selectRaw('sum(amount) as amount,sum(ISNULL(payment_fee,0)) as total_payment_fee,count(distinct(user_id)) count_u,count(id) count_id')->first();
 
         $totalMoney = isset($payTotalMoney->amount) ? number_float($payTotalMoney->amount / NumConfig::NUM_VALUE) : 0;
+        $totalPaymentFee = isset($payTotalMoney->total_payment_fee) ? number_float($payTotalMoney->total_payment_fee / NumConfig::NUM_VALUE) : 0;
 
         // 已到账
         if ($recharge_type == 1) {
@@ -249,6 +250,7 @@ class RechargeController extends Controller
                 $val->amount = number_float($val->amount / NumConfig::NUM_VALUE);
                 $val->after_amount = number_float($val->after_amount / NumConfig::NUM_VALUE);
                 $val->score = number_float($val->score / NumConfig::NUM_VALUE);
+                $val->payment_fee = number_float(($val->payment_fee ?? 0) / NumConfig::NUM_VALUE);
 
                 $val->MaxScore = number_float($val->MaxScore / NumConfig::NUM_VALUE);
 
@@ -309,6 +311,7 @@ class RechargeController extends Controller
                 'type_list' => $type_list,
                 'recharge_type' => $recharge_type,
                 'totalMoney' => $totalMoney,
+                'totalPaymentFee' => $totalPaymentFee,
                 'overMoney' => $overMoney,
                 'payOverMoney' => $payOverMoney ?? 0,
                 'payTotalMoney' => $payTotalMoney,
@@ -695,11 +698,18 @@ class RechargeController extends Controller
 
                 $order = DB::table('agent.dbo.order')->where('id', $id)->first();
 
-                if (!$order)
+                if (!$order) {
                     return apiReturnFail('订单不存在!');
+                }
 
-                if ($order->pay_status == 1)
+                if ((int)$order->pay_status === 1) {
                     return apiReturnFail('订单已完成!');
+                }
+
+                // 退款订单禁止补单
+                if ((int)$order->pay_status === 9) {
+                    return apiReturnFail('退款订单不可补单');
+                }
 
                 $payAmt = $order->amount / NumConfig::NUM_VALUE;
                 $order_sn = $order->order_sn;
@@ -821,6 +831,11 @@ class RechargeController extends Controller
                 return apiReturnFail('订单不存在!');
             }
 
+            // 退款订单不允许模拟上报
+            if ((int)$order->pay_status === 9) {
+                return apiReturnFail('退款订单不可模拟上报');
+            }
+
             // 只允许对未成功的订单做模拟上报(已到账的默认认为已正常上报)
             if ((int)$order->pay_status === 1) {
                 return apiReturnFail('仅对未成功订单开放模拟上报');

+ 25 - 1
app/Http/Controllers/Admin/StockModeController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\Admin;
 
 use App\Http\Controllers\Controller;
+use App\Http\helper\NumConfig;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\DB;
 
@@ -32,12 +33,35 @@ class StockModeController extends Controller
             ->where('GameID', 0)
             ->orderBy('SortID')
             ->get();
+        // 今日税收 rtp 中间率
+        $todayID = date('Ymd');
+
+        $gameRoom = DB::connection('read')
+            ->table('QPPlatformDB.dbo.GameRoomInfo')
+            ->select('RevenueRatio')
+            ->first();
+        // 将千分比转换为百分比显示(例如:50 -> 5%)
+        $revenueRatio = $gameRoom ? ($gameRoom->RevenueRatio / 10) : 0;
+
+        $rst2 = DB::Table('QPPlatformDB.dbo.RoomStockDay2')
+            ->lock('with(nolock)')
+            ->selectRaw('SortID, SUM(Winlost) as SumWinlost, SUM(Turnover) as SumTurnover')
+            ->groupBy('SortID')
+            ->where('DateID', $todayID)
+            ->get();
+        foreach ($rst2 as $k => $v) {
+            $rst2[$k]->todayRevenue = $v->SumTurnover / NumConfig::NUM_VALUE * ($revenueRatio / 100);
+            $rst2[$k]->todayRtp = 1 - round($v->SumWinlost / $v->SumTurnover, 4);
+            $rst2[$k]->todayWinRatio = 0; // today 获取中奖率并赋值
+        }
+        $rst2 = $rst2->keyBy('SortID');
 
         return view('admin.stock_mode.index', compact(
             'systemConfig',
             'betMaxLimits',
             'rechargeMinLimits',
-            'roomStocks'
+            'roomStocks',
+            'rst2'
         ));
     }
 

+ 15 - 4
app/Http/Controllers/Admin/SuperballController.php

@@ -24,7 +24,6 @@ class SuperballController extends BaseController
     {
         // 不需要日期筛选,直接按日期倒序分页
         $list = DB::table(TableName::agent() . 'superball_daily')
-            ->lock('with(nolock)')
             ->orderBy('pool_date', 'desc')
             ->paginate(30);
 
@@ -92,7 +91,7 @@ class SuperballController extends BaseController
             ->leftJoin('QPAccountsDB.dbo.AccountsInfo as a', 't.user_id', '=', 'a.UserID')
             ->leftJoin(TableName::agent() . 'superball_user_multiplier as m', 't.user_id', '=', 'm.user_id')
             ->leftJoin(TableName::agent() . 'superball_tier_config as c', 't.tier', '=', 'c.tier')
-            ->leftJoin(DB::raw(TableName::agent() . 'superball_daily as d WITH (NOLOCK)'), 't.task_date', '=', 'd.pool_date')
+            ->leftJoin(TableName::agent() . 'superball_daily as d', 't.task_date', '=', 'd.pool_date')
             ->selectRaw('t.*, a.GameID as game_id, m.multiplier, c.ball_count as tier_ball_count, d.pool_amount, d.total_balls as daily_total_balls');
 
         if ($date) {
@@ -157,7 +156,19 @@ class SuperballController extends BaseController
     public function prizes(Request $request)
     {
         $date = $request->input('date');
-        $userId = (int)$request->input('user_id', 0);
+        $gameId = (int)$request->input('game_id', 0);
+        $userId = 0;
+
+        // 通过 GameID 从 AccountsInfo 获取对应的 UserID
+        if ($gameId > 0) {
+            $userInfo = DB::table('QPAccountsDB.dbo.AccountsInfo')
+                ->where('GameID', $gameId)
+                ->select('UserID')
+                ->first();
+            if ($userInfo) {
+                $userId = $userInfo->UserID;
+            }
+        }
 
         $query = DB::table(TableName::agent() . 'superball_prize_log as p')
             ->leftJoin('QPAccountsDB.dbo.AccountsInfo as a', 'p.user_id', '=', 'a.UserID')
@@ -180,6 +191,6 @@ class SuperballController extends BaseController
             $row->lucky_amount_display = number_float($row->lucky_amount / NumConfig::NUM_VALUE);
         }
 
-        return view('admin.superball.prizes', compact('list', 'date', 'userId'));
+        return view('admin.superball.prizes', compact('list', 'date', 'gameId'));
     }
 }

+ 3 - 1
app/Http/Controllers/Admin/WebChannelConfigController.php

@@ -117,6 +117,7 @@ class WebChannelConfigController
         $data['FullApk'] = $data['FullApk'] ?? '';
         $data['PlatformID'] = $data['PlatformID'] ?? '';
         $data['RegionID'] = $data['RegionID'] ?? '';
+        $data['PlatformToken'] = $data['PlatformToken'] ?? '';
 
         $config = WebChannelConfig::create($data);
 
@@ -153,7 +154,7 @@ class WebChannelConfigController
     {
         $data = $request->all();
         $info = WebChannelConfig::findOrFail($id);
-        
+
         $validator = Validator::make($data, [
             'PackageName' => 'required|string|max:200',
         ]);
@@ -194,6 +195,7 @@ class WebChannelConfigController
         $data['PlatformID'] = $data['PlatformID'] ?? '';
         $data['RegionID'] = $data['RegionID'] ?? '';
         $data['PlatformName'] = $data['PlatformName'] ?? '';
+        $data['PlatformToken'] = $data['PlatformToken'] ?? '';
 
         $oldRegionID = $info->RegionID;
         $oldChannel = $info->Channel;

+ 28 - 26
app/Http/Controllers/Admin/WithdrawalController.php

@@ -177,6 +177,8 @@ class WithdrawalController extends BaseController
             ->where('type', 'cash')
             ->get();
         $list['request'] = $request;
+        $admin = session('admin');
+        $list['viewAll'] = $admin && isset($admin->roles[0]->id) && in_array($admin->roles[0]->id, [1, 12, 2010, 2011]) ? 1 : 0;
         return view('admin.Withdrawal.verify_finish', $list);
     }
 
@@ -476,16 +478,16 @@ class WithdrawalController extends BaseController
 
         // 推广佣金已领取 。
         $info->commission = DB::connection('read')->table('QPRecordDB.dbo.RecordUserScoreStatisticsNew')
-                ->where('ScoreType', 53)
-                ->where('UserID', $UserID)
-                ->selectRaw('IsNull(sum(Score),0) Score')
-                ->first()->Score / NumConfig::NUM_VALUE ?? 0;
+            ->where('ScoreType', 53)
+            ->where('UserID', $UserID)
+            ->selectRaw('IsNull(sum(Score),0) Score')
+            ->first()->Score / NumConfig::NUM_VALUE ?? 0;
 
         // 推广佣金可领取 。
         $waitCommission = DB::connection('read')->table('QPAccountsDB.dbo.UserAgent')
-                ->where('UserID', $UserID)
-                ->selectRaw('IsNull((Balance1 + Balance2),0) as  Balance')
-                ->first()->Balance ?? 0;
+            ->where('UserID', $UserID)
+            ->selectRaw('IsNull((Balance1 + Balance2),0) as  Balance')
+            ->first()->Balance ?? 0;
         $info->waitCommission = $waitCommission > 0 ? $waitCommission / NumConfig::NUM_VALUE : 0;
 
 
@@ -827,7 +829,7 @@ class WithdrawalController extends BaseController
                 ->where('admin_configs.type', 'cash')
                 ->orderBy('admin_configs.id', 'asc')
                 ->get();
-            
+
             return view('admin.Withdrawal.cashier_channel_config', ['list' => $list]);
         } else {
             // POST 提交 - 修改权重
@@ -835,26 +837,26 @@ class WithdrawalController extends BaseController
                 'config.*.sort' => 'required|int',
                 'config.*.status' => 'required|in:1,-1',
             ]);
-            
+
             if ($validator->fails()) {
                 return apiReturnFail($validator->errors()->first());
             }
-            
+
             $config = $request->input('config');
-            
+
             // 验证权重总和
             $opened = array_filter($config, function ($item) {
                 return $item['status'] == 1;
             });
-            
+
             if (empty($opened)) {
                 return apiReturnFail('至少需要开启一个提现渠道');
             }
-            
+
             if (array_sum(array_column($opened, 'sort')) != 100) {
                 return apiReturnFail('权重值分配不正确,开启的渠道权重总和必须为100');
             }
-            
+
             // 更新配置
             foreach ($config as $id => $v) {
                 $v['admin_id'] = session('admin')['id'];
@@ -864,11 +866,11 @@ class WithdrawalController extends BaseController
                 }
                 DB::table('agent.dbo.admin_configs')->where('id', $id)->update($v);
             }
-            
+
             return apiReturnSuc();
         }
     }
-    
+
     // 新增提现渠道
     public function cashier_channel_add(Request $request)
     {
@@ -881,14 +883,14 @@ class WithdrawalController extends BaseController
                 'config_value' => 'required',
                 'status' => 'required|in:1,-1',
             ]);
-            
+
             if ($validator->fails()) {
                 return apiReturnFail($validator->errors()->first());
             }
-            
+
             // 获取最大ID
             $maxId = DB::table('agent.dbo.admin_configs')->max('id');
-            
+
             $data = [
                 'id' => $maxId + 1,
                 'name' => $request->name,
@@ -902,7 +904,7 @@ class WithdrawalController extends BaseController
                 'created_at' => date('Y-m-d H:i:s'),
                 'updated_at' => date('Y-m-d H:i:s'),
             ];
-            
+
             try {
                 DB::table('agent.dbo.admin_configs')->insert($data);
                 return apiReturnSuc();
@@ -911,7 +913,7 @@ class WithdrawalController extends BaseController
             }
         }
     }
-    
+
     // 修改提现渠道
     public function cashier_channel_update(Request $request, $id)
     {
@@ -920,11 +922,11 @@ class WithdrawalController extends BaseController
                 ->where('id', $id)
                 ->where('type', 'cash')
                 ->first();
-            
+
             if (!$info) {
                 return redirect()->back()->with('error', '渠道不存在');
             }
-            
+
             return view('admin.Withdrawal.cashier_channel_update', ['info' => $info]);
         } else {
             // POST 提交
@@ -933,11 +935,11 @@ class WithdrawalController extends BaseController
                 'config_value' => 'required',
                 'status' => 'required|in:1,-1',
             ]);
-            
+
             if ($validator->fails()) {
                 return apiReturnFail($validator->errors()->first());
             }
-            
+
             $data = [
                 'name' => $request->name,
                 'config_key' => $request->config_key ?: $request->name,
@@ -948,7 +950,7 @@ class WithdrawalController extends BaseController
                 'admin_id' => session('admin')['id'],
                 'updated_at' => date('Y-m-d H:i:s'),
             ];
-            
+
             try {
                 DB::table('agent.dbo.admin_configs')
                     ->where('id', $id)

+ 119 - 81
app/Http/Controllers/Game/ActivityController.php

@@ -191,11 +191,11 @@ class ActivityController extends Controller
     {
         $user=$request->user();
         $UserID=$user->UserID;
-        
+
         // 使用 Redis 进程锁防止并发调用存储过程导致主键冲突
         $lockKey = 'checkin_get_info_' . $UserID;
         $lockAcquired = SetNXLock::getExclusiveLock($lockKey, 5);
-        
+
         if (!$lockAcquired) {
             // 如果获取锁失败,等待一小段时间后重试一次
 //            usleep(100000); // 等待 100ms
@@ -205,7 +205,7 @@ class ActivityController extends Controller
                 return apiReturnFail(['web.checkin.try_again_later', 'Please try again later']);
             }
         }
-        
+
         try {
             $res=DB::connection('sqlsrv')->select("exec QPAccountsDB.dbo.GSP_GP_GetUserSignInInfo $UserID");
             $res2=DB::table('QPAccountsDB.dbo.UserSignInInfo')->where('UserID',$UserID)->first();
@@ -607,7 +607,7 @@ class ActivityController extends Controller
 
         // 获取用户任务数据
         $taskData = $this->getUserTaskData($userId);
-        
+
         // 获取任务配置
         $taskConfig = $this->getTaskConfig();
 
@@ -631,7 +631,7 @@ class ActivityController extends Controller
             $betTarget = $taskData['stage3_bet_target'] ?? 5000;
             $rechargeProgress = $taskData['stage3_recharge_progress'] ?? 0;
             $betProgress = $taskData['stage3_bet_progress'] ?? 0;
-            
+
             // 两个条件都完成才能领取奖励
             if ($rechargeProgress >= $rechargeTarget && $betProgress >= $betTarget) {
                 $stage3_reward_status = 1; // 待领取
@@ -644,7 +644,7 @@ class ActivityController extends Controller
             $taskKey = $taskCfg['id'];
             $progressKey = $taskCfg['progress_key'];
             $completed = $taskData[$taskKey] ?? false;
-            
+
             $stage1Tasks[] = [
                 'id' => $taskCfg['id'],
                 'title' => $taskCfg['title'],
@@ -692,7 +692,7 @@ class ActivityController extends Controller
                     $taskKey = $taskCfg['id'];
                     $progressKey = $taskCfg['progress_key'];
                     $completed = $taskData[$taskKey] ?? false;
-                    
+
                     return [
                         'id' => $taskCfg['id'],
                         'title' => $taskCfg['title'],
@@ -726,20 +726,20 @@ class ActivityController extends Controller
                     $taskKey = $taskCfg['id'];
                     $progressKey = $taskCfg['progress_key'] ?? '';
                     $targetKey = $taskCfg['target_key'] ?? '';
-                    
+
                     // 获取当前进度和目标值
                     $currentProgress = $taskData[$progressKey] ?? 0;
                     $currentTarget = $targetKey ? ($taskData[$targetKey] ?? $taskCfg['target']) : $taskCfg['target'];
-                    
+
                     // 判断是否完成
                     $completed = $currentProgress >= $currentTarget;
-                    
+
                     // 动态生成任务标题(使用模板替换目标值)
                     $title = $taskCfg['title_template'] ?? $taskCfg['title'] ?? '';
                     if (!empty($title) && strpos($title, '{target}') !== false) {
                         $title = str_replace('{target}', $currentTarget, $title);
                     }
-                    
+
                     return [
                         'id' => $taskCfg['id'],
                         'title' => $title,
@@ -817,18 +817,18 @@ class ActivityController extends Controller
             // 阶段3
             elseif ($stage === 3) {
                 $taskId = $request->input('task_id', 'stage3_task1');
-                
+
                 // 检查两个条件是否都完成
-                $rechargeTarget = $taskData['stage3_recharge_target'] ?? 1000;
-                $betTarget = $taskData['stage3_bet_target'] ?? 5000;
+                $rechargeTarget = $taskData['stage3_recharge_target'] ?? 200;
+                $betTarget = $taskData['stage3_bet_target'] ?? 1000;
                 $rechargeProgress = $taskData['stage3_recharge_progress'] ?? 0;
                 $betProgress = $taskData['stage3_bet_progress'] ?? 0;
-                
+
                 if ($rechargeProgress < $rechargeTarget || $betProgress < $betTarget) {
                     Redis::del($redisLockKey);
                     return apiReturnFail(['web.vip_task.task_not_completed', __('messages.web.vip_task.task_not_completed')]); // 任务未完成
                 }
-                
+
                 // 根据循环次数判断奖励:第一次(loop_count=0或不存在)奖励20,之后每次10
                 $currentLoopCount = $taskData['stage3_loop_count'] ?? 0;
                 $rewardAmount = ($currentLoopCount == 0) ? 100 : 50;
@@ -837,21 +837,37 @@ class ActivityController extends Controller
                 $newRechargeTarget = $rechargeTarget + 500;
                 $newBetTarget = $betTarget + 2500;
                 $newLoopCount = $currentLoopCount + 1;
-                
+
+                // 获取最新的总充值和总流水(与 getVipWithdrawTasks 中的统计方式保持一致)
+                $rechargeTotal = DB::table(TableName::QPRecordDB() . 'RecordUserTotalStatistics')
+                    ->where('UserID', $userId)
+                    ->value('Recharge') ?: 0;
+                $totalBetAmount = $taskData['total_bet'] ?? 0;
+
+                // 下一轮目标:固定 100 / 500
+                $newRechargeTarget = 100;
+                $newBetTarget      = 500;
+
                 // 更新任务数据
                 $redisKey = "vip_withdraw_task_{$userId}";
                 $data = Redis::get($redisKey);
                 if ($data) {
                     $taskData = json_decode($data, true);
                     $taskData['stage3_recharge_target'] = $newRechargeTarget;
-                    $taskData['stage3_bet_target'] = $newBetTarget;
-                    $taskData['stage3_loop_count'] = $newLoopCount;
-                    $taskData['stage3_task1'] = false;
-                    $taskData['stage3_task2'] = false;
-                    
+                    $taskData['stage3_bet_target']      = $newBetTarget;
+                    $taskData['stage3_loop_count']      = $newLoopCount;
+                    // 记录当前总充值 / 总流水作为下一轮的基准值
+                    $taskData['stage3_recharge_base']   = $rechargeTotal;
+                    $taskData['stage3_bet_base']        = $totalBetAmount;
+                    // 重置本轮任务状态和进度
+                    $taskData['stage3_task1']           = false;
+                    $taskData['stage3_task2']           = false;
+                    $taskData['stage3_recharge_progress'] = 0;
+                    $taskData['stage3_bet_progress']      = 0;
+
                     // 保存到数据库
                     $this->saveUserTaskData($userId, $taskData);
-                    
+
                     // 更新Redis缓存
                     Redis::setex($redisKey, 86400, json_encode($taskData));
                 }
@@ -908,10 +924,10 @@ class ActivityController extends Controller
                         //return apiReturnSuc($retval);
                     }
                 }catch (\Exception $e){
-                   // TelegramBot::getDefault()->sendProgramNotify("WithDraw Error:", var_export($retval,true).':'.$e->getTraceAsString());
+                    // TelegramBot::getDefault()->sendProgramNotify("WithDraw Error:", var_export($retval,true).':'.$e->getTraceAsString());
                     //return apiReturnSuc($retval);
                 }
-                
+
                 // 记录日志
                 \Log::info('VIP提现任务奖励', [
                     'user_id' => $userId,
@@ -956,7 +972,7 @@ class ActivityController extends Controller
             $dbData = DB::table('agent.dbo.vip_withdraw_tasks')
                 ->where('user_id', $userId)
                 ->first();
-            
+
             if ($dbData) {
                 $taskData = json_decode($dbData->task_data, true);
             } else {
@@ -1159,7 +1175,7 @@ class ActivityController extends Controller
         if (!isset($taskData['sign_in_count'])) {
             $taskData['sign_in_count'] = 0;
         }
-        
+
         // 如存在未充值前的签到标记,且在同一天内完成充值,则补记签到
         $pendingKey = "vip_withdraw_sign_pending_{$userId}";
         $pendingSign = Redis::get($pendingKey);
@@ -1180,7 +1196,7 @@ class ActivityController extends Controller
         if ($pendingSign) {
             Redis::del($pendingKey);
         }
-        
+
         // 如存在“未充值前的签到标记”,且用户已完成充值,则补记签到次数
         $pendingKey = "vip_withdraw_sign_pending_{$userId}";
         if (Redis::exists($pendingKey)) {
@@ -1203,20 +1219,42 @@ class ActivityController extends Controller
         if ($taskData['stage2_completed'] ?? false) {
             // 获取充值总额(从 RecordUserTotalStatistics)
             $rechargeTotal = $userTotalStatistics ? ($userTotalStatistics->Recharge) : 0;
-            $taskData['stage3_recharge_progress'] = $rechargeTotal;
-            
             // 获取下注流水(使用 total_bet)
-            $taskData['stage3_bet_progress'] = $taskData['total_bet'] ?? 0;
-            
-            // 初始化目标值(如果不存在)
-            if (!isset($taskData['stage3_recharge_target'])) {
-                $taskData['stage3_recharge_target'] = 1000;
-            }
-            if (!isset($taskData['stage3_bet_target'])) {
-                $taskData['stage3_bet_target'] = 5000;
+            $totalBetAmount = $taskData['total_bet'] ?? 0;
+
+            $currentLoop = $taskData['stage3_loop_count'] ?? 0;
+
+            if ($currentLoop == 0) {
+                // 第一次:使用总充值 / 总流水,目标 200 / 1000
+                $taskData['stage3_recharge_progress'] = $rechargeTotal;
+                $taskData['stage3_bet_progress'] = $totalBetAmount;
+
+                if (!isset($taskData['stage3_recharge_target'])) {
+                    $taskData['stage3_recharge_target'] = 1000;
+                }
+                if (!isset($taskData['stage3_bet_target'])) {
+                    $taskData['stage3_bet_target'] = 5000;
+                }
+            } else {
+                // 第二轮及之后:只统计本轮新增部分(再充值100、再下注500)
+
+                // 兼容老数据:如果还没有基准值,第一次进入新逻辑时将当前总充值/总流水作为基准
+                if (!isset($taskData['stage3_recharge_base']) || !isset($taskData['stage3_bet_base'])) {
+                    $taskData['stage3_recharge_base'] = $rechargeTotal-100;
+                    $taskData['stage3_bet_base']      = $totalBetAmount-300;
+                }
+
+                $baseRecharge = $taskData['stage3_recharge_base'];
+                $baseBet      = $taskData['stage3_bet_base'];
+
+                $taskData['stage3_recharge_progress'] = max(0, $rechargeTotal - $baseRecharge);
+                $taskData['stage3_bet_progress']      = max(0, $totalBetAmount - $baseBet);
+
+                $taskData['stage3_recharge_target'] = 100;
+                $taskData['stage3_bet_target']      = 500;
             }
         }
-        
+
         // 保留原有的 stage3_current_bet 逻辑(用于兼容)
         if (!isset($taskData['stage3_current_bet'])) {
             $taskData['stage3_current_bet'] = 0;
@@ -1261,7 +1299,7 @@ class ActivityController extends Controller
             $betTarget = $taskData['stage3_bet_target'] ?? 5000;
             $rechargeProgress = $taskData['stage3_recharge_progress'] ?? 0;
             $betProgress = $taskData['stage3_bet_progress'] ?? 0;
-            
+
             $taskData['stage3_task1'] = $rechargeProgress >= $rechargeTarget;
             $taskData['stage3_task2'] = $betProgress >= $betTarget;
         }
@@ -1274,14 +1312,14 @@ class ActivityController extends Controller
     {
         $redisKey = "vip_withdraw_task_{$userId}";
         $data = Redis::get($redisKey);
-        
+
         if ($data) {
             $taskData = json_decode($data, true);
             $taskData[$key] = $value;
-            
+
             // ✅ 保存到数据库(持久化)
             $this->saveUserTaskData($userId, $taskData);
-            
+
             // ✅ 更新Redis缓存(24小时)
             Redis::setex($redisKey, 86400, json_encode($taskData));
         }
@@ -1294,7 +1332,7 @@ class ActivityController extends Controller
     {
         $redisKey = "vip_withdraw_task_{$userId}";
         $data = Redis::get($redisKey);
-        
+
         if ($data) {
             $taskData = json_decode($data, true);
             $taskData['stage3_task1'] = false;
@@ -1302,10 +1340,10 @@ class ActivityController extends Controller
             $taskData['stage3_current_bet'] = 0;
             $taskData['stage3_cycle_start'] = (int)date('Ymd');
             $taskData['stage3_loop_count'] = ($taskData['stage3_loop_count'] ?? 0) + 1;
-            
+
             // ✅ 保存到数据库(持久化)
             $this->saveUserTaskData($userId, $taskData);
-            
+
             // ✅ 更新Redis缓存(24小时)
             Redis::setex($redisKey, 86400, json_encode($taskData));
         }
@@ -1322,29 +1360,29 @@ class ActivityController extends Controller
             ->where('UserID', $userId)
             ->value('Recharge') ?: 0;
         $hasRecharge = $user_recharge > 0;
-        
+
         $pendingKey = "vip_withdraw_sign_pending_{$userId}";
-        
+
         if (!$hasRecharge) {
             // 未充值用户,先把签到标记在Redis中,等待充值后结算(仅限同一天)
             Redis::setex($pendingKey, 86400, date('Ymd')); // 标记签到日期
-            
+
             \Log::info('VIP提现任务-签到记录(待充值)', [
                 'user_id' => $userId,
                 'note'    => '用户尚未充值,签到标记已暂存Redis',
                 'redis_key' => $pendingKey
             ]);
-            
+
             return true;
         }
-        
+
         // 用户已充值,如存在待结算签到标记,检查是否为同一天
         $pendingSign = Redis::get($pendingKey);
         if ($pendingSign && $pendingSign == date('Ymd')) {
             // 同一天内补记签到次数(若尚未计数)
             $redisKey = "vip_withdraw_task_{$userId}";
             $data = Redis::get($redisKey);
-            
+
             if ($data) {
                 $taskData = json_decode($data, true);
                 if (($taskData['sign_in_count'] ?? 0) < 1) {
@@ -1367,57 +1405,57 @@ class ActivityController extends Controller
                 }
                 Redis::setex($redisKey, 86400, json_encode($taskData));
             }
-            
+
             \Log::info('VIP提现任务-签到补记成功(同日充值)', [
                 'user_id' => $userId
             ]);
         }
         Redis::del($pendingKey);
-        
+
         $redisKey = "vip_withdraw_task_{$userId}";
         $data = Redis::get($redisKey);
-        
+
         if ($data) {
             $taskData = json_decode($data, true);
             $taskData['sign_in_count'] = ($taskData['sign_in_count'] ?? 0) + 1;
-            
+
             // ✅ 保存到数据库(持久化)
             $this->saveUserTaskData($userId, $taskData);
-            
+
             // ✅ 更新Redis缓存(24小时)
             Redis::setex($redisKey, 86400, json_encode($taskData));
-            
+
             \Log::info('VIP提现任务-签到触发成功', [
                 'user_id' => $userId,
                 'sign_in_count' => $taskData['sign_in_count']
             ]);
-            
+
             return true;
         } else {
             // 如果Redis没有数据,从数据库加载或初始化
             $dbData = DB::table('agent.dbo.vip_withdraw_tasks')
                 ->where('user_id', $userId)
                 ->first();
-            
+
             if ($dbData) {
                 $taskData = json_decode($dbData->task_data, true);
             } else {
                 $taskData = $this->initUserTaskData($userId);
             }
-            
+
             $taskData['sign_in_count'] = ($taskData['sign_in_count'] ?? 0) + 1;
-            
+
             // ✅ 保存到数据库(持久化)
             $this->saveUserTaskData($userId, $taskData);
-            
+
             // ✅ 更新Redis缓存(24小时)
             Redis::setex($redisKey, 86400, json_encode($taskData));
-            
+
             \Log::info('VIP提现任务-签到触发成功(首次)', [
                 'user_id' => $userId,
                 'sign_in_count' => 1
             ]);
-            
+
             return true;
         }
     }
@@ -1432,7 +1470,7 @@ class ActivityController extends Controller
         $user_recharge = DB::table(TableName::QPAccountsDB() . 'YN_VIPAccount')
             ->where('UserID', $userId)
             ->value('Recharge') ?: 0;
-        
+
         if ($user_recharge <= 0) {
             \Log::info('VIP提现任务-邀请触发失败', [
                 'user_id' => $userId,
@@ -1440,13 +1478,13 @@ class ActivityController extends Controller
             ]);
             return false;
         }
-        
+
         $redisKey = "vip_withdraw_task_{$userId}";
         $data = Redis::get($redisKey);
-        
+
         if ($data) {
             $taskData = json_decode($data, true);
-            
+
             // 检查阶段1是否完成
             if (!isset($taskData['stage1_completed']) || !$taskData['stage1_completed']) {
                 // 阶段1未完成,不计数
@@ -1457,31 +1495,31 @@ class ActivityController extends Controller
                 ]);
                 return false;
             }
-            
+
             // 阶段1已完成,增加邀请计数
             $taskData['invite_count'] = ($taskData['invite_count'] ?? 0) + 1;
-            
+
             // ✅ 保存到数据库(持久化)
             $this->saveUserTaskData($userId, $taskData);
-            
+
             // ✅ 更新Redis缓存(24小时)
             Redis::setex($redisKey, 86400, json_encode($taskData));
-            
+
             \Log::info('VIP提现任务-邀请触发成功', [
                 'user_id' => $userId,
                 'invite_count' => $taskData['invite_count']
             ]);
-            
+
             return true;
         } else {
             // 如果Redis没有数据,从数据库加载
             $dbData = DB::table('agent.dbo.vip_withdraw_tasks')
                 ->where('user_id', $userId)
                 ->first();
-            
+
             if ($dbData) {
                 $taskData = json_decode($dbData->task_data, true);
-                
+
                 // 检查阶段1是否完成
                 if (!isset($taskData['stage1_completed']) || !$taskData['stage1_completed']) {
                     \Log::info('VIP提现任务-邀请触发失败', [
@@ -1490,21 +1528,21 @@ class ActivityController extends Controller
                     ]);
                     return false;
                 }
-                
+
                 // 增加邀请计数
                 $taskData['invite_count'] = ($taskData['invite_count'] ?? 0) + 1;
-                
+
                 // ✅ 保存到数据库(持久化)
                 $this->saveUserTaskData($userId, $taskData);
-                
+
                 // ✅ 更新Redis缓存(24小时)
                 Redis::setex($redisKey, 86400, json_encode($taskData));
-                
+
                 \Log::info('VIP提现任务-邀请触发成功(从数据库加载)', [
                     'user_id' => $userId,
                     'invite_count' => $taskData['invite_count']
                 ]);
-                
+
                 return true;
             } else {
                 // 用户数据不存在或阶段1未完成

+ 128 - 0
app/Http/Controllers/Game/HacksawController.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace App\Http\Controllers\Game;
+
+use App\Game\GameCard;
+use App\Game\GlobalUserInfo;
+use App\Game\LogGamecardClick;
+use App\Game\Services\OuroGameService;
+use App\Models\AccountsInfo;
+use App\Notification\TelegramBot;
+use App\Util;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Controller;
+use Illuminate\Support\Facades\Redis;
+
+class HacksawController extends Controller
+{
+    /** gid => version(与 CDN 静态资源目录一致) */
+    private static $gameVersions = [
+        '1632' => '1.34.2',   // Speed Crash
+        '1624' => '1.43.2',   // Limbo
+        '1590' => '1.30.0',   // Dice
+        '1446' => '1.59.1',   // Baccarat
+        '1416' => '1.30.1',   // Twenty-One
+        '1386' => '1.36.2',   // Colors
+        '1380' => '1.34.1',   // Blocks
+        '1321' => '1.41.0',   // Wheel
+        '1334' => '1.41.1',   // Lines
+        '1148' => '1.27.3',   // Coins
+        '1294' => '1.88.1',   // Plinko
+        '1328' => '1.43.0',   // Hi-Lo
+        '1126' => '1.135.0',  // Mines
+        '1154' => '1.68.1',   // Boxes
+    ];
+
+    /**
+     * Hacksaw 子游戏载入入口
+     * 请求: /game/hacksaw/lunch?gid=1632
+     */
+    public function gameLunch(Request $request)
+    {
+        $gid = (string) $request->input('gid');
+        $user = $request->user();
+        $userid = $user->UserID;
+
+        $version = self::$gameVersions[$gid] ?? null;
+        if ($version === null) {
+            abort(404, 'Hacksaw game version not configured for gid: ' . $gid);
+        }
+
+        GameCard::$enableStateCheck = false;
+        $gamecard = GameCard::where('gid', $gid)->where('brand', 'Hacksaw')->first();
+        if (!$gamecard) {
+            abort(404, 'Game not found');
+        }
+
+        $in_gameid = OuroGameService::getUserInGame($userid, $user->GlobalUID);
+        if ($in_gameid != intval($gamecard->id)) {
+            Util::WriteLog('24680game', compact('in_gameid', 'gamecard', 'user'));
+        }
+
+        $gamecard->increment('play_num', 1);
+        LogGamecardClick::recordClick($gamecard->id, $userid);
+
+        $lang = GlobalUserInfo::getLocale();
+        $supportLang = ['en', 'da', 'de', 'es', 'fi', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'no', 'pl', 'pt', 'ro', 'ru', 'sv', 'th', 'tr', 'vi', 'zh', 'my'];
+        if (!in_array($lang, $supportLang)) {
+            $lang = 'en';
+        }
+
+        $configurls = json_decode(env('CONFIG_GAMES'), true);
+        $configurl = $configurls['hacksaw'] ?? $configurls['hkg'] ?? null;
+        if (!$configurl) {
+            $staticHost = 'static.pgn-nmu2nd.com';
+            $apiHost = 'api.pgn-nmu2nd.com';
+        } else {
+            $staticHost = $configurl['source'] ?? 'static.pgn-nmu2nd.com';
+            $apiHost = $configurl['api'] ?? 'api.pgn-nmu2nd.com';
+        }
+
+        $cdnserver = 'https://' . $staticHost;
+        $apiBase = 'https://' . $apiHost . '/api';
+        $lobbyurl = $cdnserver;
+//        $sign = GlobalUserInfo::genGuuidSign($user);
+        $newToken = base64_encode(random_bytes(20));
+
+        $Currency = env("CONFIG_24680_CURRENCY", "BRL");
+        $CurrencySymbol = env("CONFIG_24680_DOLLAR", "R$");
+
+
+        $data['currency'] = $Currency;
+        $data['dollar'] = $CurrencySymbol;
+
+        $data['limit_room']=0;
+
+        $account = AccountsInfo::where('UserID', $userid)->first();
+        if(!$account){
+            TelegramBot::getDefault()->sendMsgWithEnv("hawksaw_ fail11111:" . json_encode([$request->all(),$data]) );
+            abort(404, 'User not found');
+        }else{
+            $account=$account->toArray();
+        }
+        $data = array_merge($data, $account);
+
+        Redis::setex($newToken, 7200, json_encode($data));
+
+        $params = [
+            'language'     => $lang,
+            'channel'      => 'mobile',
+            'gameid'       => $gid,
+            'mode'         => 2,
+            'token'        => $newToken,
+            'lobbyurl'     => $lobbyurl,
+            'currency'     => env('CONFIG_24680_CURRENCY', 'EUR'),
+            'partner'      => 'demo',
+            'env'          => $apiBase,
+            'realmoneyenv' => $apiBase,
+        ];
+
+        $url = $cdnserver . '/' . $gid . '/' . $version . '/index.html?' . http_build_query($params);
+
+
+        return "<script>
+parent.postMessage({cmd:\"closeLoading\"},\"*\");
+location.href='$url';
+</script>";
+    }
+}

+ 1 - 1
app/Http/Controllers/Game/LoginController.php

@@ -177,7 +177,7 @@ class LoginController extends Controller
 
         $Phone = $this->checkPhone($PhoneNum, $RegisterLocation, $request);
         //有错误返回
-        if (is_array($Phone)){
+        if (is_array($Phone) || is_object($Phone)){
             Log::info(json_encode($Phone),[$Phone,$PhoneCode]);
             return $Phone;
         }

Разница между файлами не показана из-за своего большого размера
+ 168 - 186
app/Http/Controllers/Game/PayRechargeController.php


+ 74 - 68
app/Http/Controllers/Game/RechargeController.php

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Game;
 
 use App\dao\Pay\AccountPayInfo;
 use App\Facade\TableName;
+use App\Game\GlobalUserInfo;
 use App\Http\helper\NumConfig;
 use App\Http\logic\api\CashPayLogic;
 use App\Http\logic\api\RechargeLogic;
@@ -29,6 +30,13 @@ class RechargeController
     public function gear(Request $request)
     {
 
+        $UserID = (int)$request->globalUser->UserID ?? 0;
+        $vip = 0;
+        $user = null;
+        if ($UserID > 0) {
+            $user = GlobalUserInfo::getGameUserInfo('UserID', $UserID);
+            if ($user) $user = GlobalUserInfo::toWebData($user);
+        }
 
         $list = DB::table('agent.dbo.recharge_gear')
             ->where('in_shop', 1)
@@ -37,10 +45,8 @@ class RechargeController
 
         foreach ($list as $item) {
             if (!empty($item->gear)) {
-                $item->gear = Util::filterGearByDevice($item->gear);
+                $item->gear = Util::filterGearByDevice($item->gear,$user);
             }
-            $item->money = intval($item->money);
-            $item->favorable_price = intval($item->favorable_price);
         }
 
         return apiReturnSuc(['list'=>$list,'bonus_show'=>false]);
@@ -62,92 +68,92 @@ class RechargeController
 //        if(Redis::exists($key)) {
 //            $result=json_decode(Redis::get($key));
 //        }else {
-            $first = DB::table('agent.dbo.recharge_gear')
-                ->where('status', 1)
-                ->where('id', $id)
-                ->first();
-
-            if (empty($first)) {
-                return apiReturnFail(['web.payment.payment_error','PayMent Error']);
-            }
+        $first = DB::table('agent.dbo.recharge_gear')
+            ->where('status', 1)
+            ->where('id', $id)
+            ->first();
 
-            $first->favorable_price = $first->favorable_price + $first->give;
+        if (empty($first)) {
+            return apiReturnFail(['web.payment.payment_error','PayMent Error']);
+        }
 
+        $first->favorable_price = $first->favorable_price + $first->give;
 
-            $UserID=$request->UserID;
 
-            if(env('MULTI_COUNTRY',0)==1){
+        $UserID=$request->UserID;
 
+        if(env('MULTI_COUNTRY',0)==1){
 
-                $names = DB::table('agent.dbo.admin_configs')
-                    ->where('type', 'pay_method')
-                    ->where('status', '1')
-                    ->where('country',$request->Country??AccountsInfo::select('BindCountry')->where('UserID',$UserID)->first()->BindCountry)
-                    ->select('id', 'name', 'new_pay_type')
-                    ->orderByDesc('sort')->get()
-                    ->map(function ($value) {
-                        return (array)$value;
-                    })->toArray();
-            }else{
-                $names = DB::table('agent.dbo.admin_configs')
-                    ->where('type', 'pay_method')
-                    ->where('status', '1')
-                    ->select('id', 'name', 'new_pay_type')
-                    ->orderByDesc('sort')->get()
-                    ->map(function ($value) {
-                        return (array)$value;
-                    })->toArray();
-            }
 
+            $names = DB::table('agent.dbo.admin_configs')
+                ->where('type', 'pay_method')
+                ->where('status', '1')
+                ->where('country',$request->Country??AccountsInfo::select('BindCountry')->where('UserID',$UserID)->first()->BindCountry)
+                ->select('id', 'name', 'new_pay_type')
+                ->orderByDesc('sort')->get()
+                ->map(function ($value) {
+                    return (array)$value;
+                })->toArray();
+        }else{
+            $names = DB::table('agent.dbo.admin_configs')
+                ->where('type', 'pay_method')
+                ->where('status', '1')
+                ->select('id', 'name', 'new_pay_type')
+                ->orderByDesc('sort')->get()
+                ->map(function ($value) {
+                    return (array)$value;
+                })->toArray();
+        }
 
 
-            // 渠道
-            if ($ChannelNumber != '') {
-                $switch = Redis::get("recharge_config_switch_{$ChannelNumber}");
-                if ($switch) {
-                    $list = DB::connection('write')->table('QPPlatformDB.dbo.ChannelOpenRecharge as go')
-                        ->join('agent.dbo.admin_configs as cf', 'go.ConfigID', '=', 'cf.id')
-                        ->where('go.Channel', $ChannelNumber)
-                        ->where('go.Status', 1)
-                        ->where('cf.type', 'pay_method')
-                        ->orderByDesc('go.Sort')
-                        ->select('go.Sort', 'ConfigID')
-                        ->pluck('Sort', 'ConfigID')->toArray();
-                }
-            }
 
-            if (!isset($list) || count($list) < 1) {  // 默认配置
-                $list = DB::connection('write')->table('agent.dbo.admin_configs')
-                    ->where('type', 'pay_method')
-                    ->where('status', 1)
-                    ->select('sort as Sort', 'id as ConfigID')
+        // 渠道
+        if ($ChannelNumber != '') {
+            $switch = Redis::get("recharge_config_switch_{$ChannelNumber}");
+            if ($switch) {
+                $list = DB::connection('write')->table('QPPlatformDB.dbo.ChannelOpenRecharge as go')
+                    ->join('agent.dbo.admin_configs as cf', 'go.ConfigID', '=', 'cf.id')
+                    ->where('go.Channel', $ChannelNumber)
+                    ->where('go.Status', 1)
+                    ->where('cf.type', 'pay_method')
+                    ->orderByDesc('go.Sort')
+                    ->select('go.Sort', 'ConfigID')
                     ->pluck('Sort', 'ConfigID')->toArray();
             }
+        }
 
-            $data = [];
+        if (!isset($list) || count($list) < 1) {  // 默认配置
+            $list = DB::connection('write')->table('agent.dbo.admin_configs')
+                ->where('type', 'pay_method')
+                ->where('status', 1)
+                ->select('sort as Sort', 'id as ConfigID')
+                ->pluck('Sort', 'ConfigID')->toArray();
+        }
 
-            foreach ($names as $k => $va) {
-                if (isset($list[$va['id']])) {
-                    $type = $va['new_pay_type'] == 1 ? 3 : 2;
+        $data = [];
 
-                    if ($va['new_pay_type'] == 4) {
-                        $type = 0;
-                    }
-                    // type = 1 sdk跳转 type=2 外跳链接  type=0  默认 type=3 复制信息到APP支付
-                    $data[] = ['id' => $va['id'],
-                        'name' => $va['name'], 'status' => 1, 'type' => $type, 'sort' => $list[$va['id']]];
+        foreach ($names as $k => $va) {
+            if (isset($list[$va['id']])) {
+                $type = $va['new_pay_type'] == 1 ? 3 : 2;
+
+                if ($va['new_pay_type'] == 4) {
+                    $type = 0;
                 }
+                // type = 1 sdk跳转 type=2 外跳链接  type=0  默认 type=3 复制信息到APP支付
+                $data[] = ['id' => $va['id'],
+                    'name' => $va['name'], 'status' => 1, 'type' => $type, 'sort' => $list[$va['id']]];
             }
+        }
 
 //        $first->cpf_first=0;
 //        if($ChannelNumber==103)$first->cpf_first=1;
 
-            $gear = array_values($data);
-            $gear = collect($gear)->sortByDesc('sort')->toArray();
-            $first->kefu_switch = 1;
-            $gear = array_values($gear);
-            $first->gear = \GuzzleHttp\json_encode($gear);
-            $result = apiReturnSuc($first);
+        $gear = array_values($data);
+        $gear = collect($gear)->sortByDesc('sort')->toArray();
+        $first->kefu_switch = 1;
+        $gear = array_values($gear);
+        $first->gear = \GuzzleHttp\json_encode($gear);
+        $result = apiReturnSuc($first);
 //            Redis::set($key,json_encode($result));
 //            Redis::expire($key,60);
 //        }

+ 8 - 0
app/Http/Controllers/Game/WebRouteController.php

@@ -74,6 +74,11 @@ class WebRouteController extends Controller
             $upgradeBonus = SystemStatusInfo::OnlyGetCacheValue('BindPhoneReward') ?? 500;
         }
         $user = GlobalUserInfo::$me;//LoginController::checkLogin($request);
+        if ($user) {
+            Redis::set('user_ua_' . $user->UserID, $request->userAgent());
+            Redis::expireAt('user_ua_' . $user->UserID, time() + 86400);
+        }
+
 
         $hashadd = $request->input("hashadd", "");
         $isreg = 0;
@@ -157,6 +162,8 @@ class WebRouteController extends Controller
             ->where('StatusName', 'Telegram')
             ->first();
 
+        $servicelist = (new ApiController())->getServiceList();
+
 
 //        $chat = "https://m.me/930365713484502";
         // 默认推荐游戏
@@ -265,6 +272,7 @@ class WebRouteController extends Controller
 
 //            'serviceLink' => $chat,
             'serviceLink' => $chat?$chat->StatusString:'https://m.me/930365713484502',
+            'cs'          => $servicelist,
 
             'vipConfig' => VipService::getVipLevelConfig(),
 

+ 11 - 10
app/Http/Controllers/Game/WithDrawInfoController.php

@@ -64,7 +64,8 @@ class WithDrawInfoController
         $list=DB::table('QPAccountsDB.dbo.OrderWithDraw')
             ->where('UserID', $UserID)
             ->selectRaw("CreateDate,OrderId,[State],WithDraw,ServiceFee,PixType")
-            ->paginate(10);
+            ->orderBy('CreateDate', 'desc')
+            ->paginate(100);
         return apiReturnSuc($list);
     }
 
@@ -556,10 +557,10 @@ class WithDrawInfoController
                 if (substr($PixNum, 0, 1) !== '$') {
                     $PixNum = '$' . $PixNum;
                 }
-                
+
                 // 构建 cash.app URL
                 $cashAppUrl = 'https://cash.app/' . $PixNum;
-                
+
                 try {
                     // 使用 stream context 来获取 HTTP 响应头
                     $context = stream_context_create([
@@ -569,21 +570,21 @@ class WithDrawInfoController
                             'ignore_errors' => true
                         ]
                     ]);
-                    
+
                     $htmlContent = @file_get_contents($cashAppUrl, false, $context);
-                    
+
                     // 检查 HTTP 响应头中的状态码
                     if ($htmlContent === false || (isset($http_response_header) && preg_match('/HTTP\/\d\.\d\s+404/', implode("\n", $http_response_header)))) {
                         Util::WriteLog("withdrawInfo", [5, $request->all(), 'Invalid cashapp: ' . $PixNum]);
                         SetNXLock::release($redisKey);
                         return apiReturnFail(['web.withdraw.invalid_cashapp', 'Invalid CashApp account']);
                     }
-                    
+
                     if ($htmlContent !== false) {
                         // 去除空格换行,转换成一行字符串
                         $htmlContent = preg_replace('/\s+/', '', $htmlContent);
                         Util::WriteLog("withdrawInfo", [5, $request->all(), 'CashApp verification response: ' . $htmlContent]);
-                        
+
                         // 检查响应内容中是否包含 "404 Not Found"
                         if (stripos($htmlContent, '404NotFound') !== false) {
                             Util::WriteLog("withdrawInfo", [5, $request->all(), 'Invalid cashapp: ' . $PixNum]);
@@ -596,9 +597,9 @@ class WithDrawInfoController
                     Util::WriteLog("withdrawInfo", [5, $request->all(), 'CashApp verification error: ' . $e->getMessage()]);
                 }
             }
-            
+
             $data = compact('PixType', 'BankUserName',  'PixNum');
-            
+
 
 
         } else {
@@ -624,7 +625,7 @@ class WithDrawInfoController
                     ->where('UserID', $UserID)
                     ->insert($data);
             } catch (\Exception $e) {
-                    Log::error('insert AccountWithDrawInfo failed', ['data' => $data]);
+                Log::error('insert AccountWithDrawInfo failed', ['data' => $data]);
                 Util::WriteLog("withdrawInfo",[6, $request->all(),$data]);
             }
 

+ 109 - 60
app/Http/logic/admin/GlobalLogicController.php

@@ -49,17 +49,17 @@ class GlobalLogicController extends BaseLogicController
 
         // 今日总提现
         $data['today_withdrawal'] = DB::connection('read')->table("QPAccountsDB.dbo.OrderWithDraw as od")
-                ->join('QPAccountsDB.dbo.AccountsRecord as ar', 'od.RecordID', 'ar.RecordID')
-                ->whereDate('update_at', date('Y-m-d'))
-                ->where('State', 2)
-                ->selectRaw('IsNull((sum(WithDraw) + sum(ServiceFee)),0) WithDraw')
-                ->first()->WithDraw / NumConfig::NUM_VALUE ?? 0;
+            ->join('QPAccountsDB.dbo.AccountsRecord as ar', 'od.RecordID', 'ar.RecordID')
+            ->whereDate('update_at', date('Y-m-d'))
+            ->where('State', 2)
+            ->selectRaw('IsNull((sum(WithDraw) + sum(ServiceFee)),0) WithDraw')
+            ->first()->WithDraw / NumConfig::NUM_VALUE ?? 0;
 
         //今日彩金
         $data['cellData'] = DB::connection('read')->table('QPRecordDB.dbo.RecordUserDataStatisticsNew')
-                ->selectRaw('Isnull(SUM(Handsel),0) as Handsel')
-                ->where('DateID', date('Ymd'))
-                ->first()->Handsel / NumConfig::NUM_VALUE ?? 0;
+            ->selectRaw('Isnull(SUM(Handsel),0) as Handsel')
+            ->where('DateID', date('Ymd'))
+            ->first()->Handsel / NumConfig::NUM_VALUE ?? 0;
 
         // 今日杀率 => 被控制输的用户在总活跃用户中的占比
         // ---- 控制输的总用户
@@ -209,26 +209,26 @@ class GlobalLogicController extends BaseLogicController
 
         // 付费玩家整体月卡购买人数
         $monthCardPay = DB::connection('read')->table('QPPlatformDB.dbo.UserMonthCard')
-                ->selectRaw('count(distinct(UserID)) count_U')
-                ->first()->count_U ?? 0;
+            ->selectRaw('count(distinct(UserID)) count_U')
+            ->first()->count_U ?? 0;
 
         // 付费玩家白银月卡购买人数
         $silverPay = DB::connection('read')->table('QPPlatformDB.dbo.UserMonthCard')
-                ->where('CardID', 1)
-                ->selectRaw('count(distinct(UserID)) count_U')
-                ->first()->count_U ?? 0;
+            ->where('CardID', 1)
+            ->selectRaw('count(distinct(UserID)) count_U')
+            ->first()->count_U ?? 0;
 
         // 付费玩家黄金月卡购买人数
         $goldPay = DB::table('QPPlatformDB.dbo.UserMonthCard')
-                ->where('CardID', 2)
-                ->selectRaw('count(distinct(UserID)) count_U')
-                ->first()->count_U ?? 0;
+            ->where('CardID', 2)
+            ->selectRaw('count(distinct(UserID)) count_U')
+            ->first()->count_U ?? 0;
 
         // 付费玩家钻石月卡购买人数
         $diamondPay = DB::connection('read')->table('QPPlatformDB.dbo.UserMonthCard')
-                ->where('CardID', 3)
-                ->selectRaw('count(distinct(UserID)) count_U')
-                ->first()->count_U ?? 0;
+            ->where('CardID', 3)
+            ->selectRaw('count(distinct(UserID)) count_U')
+            ->first()->count_U ?? 0;
 
         $totalRate = $totalPay > 0 ? number_float(($monthCardPay / $totalPay) * NumConfig::NUM_VALUE) . ' %' : 0;
         $silverRate = $totalPay > 0 ? number_float(($silverPay / $totalPay) * NumConfig::NUM_VALUE) . ' %' : 0;
@@ -617,9 +617,9 @@ class GlobalLogicController extends BaseLogicController
 
         // 包名
         $userInfo->PackgeName = DB::connection('read')->table('QPRecordDB.dbo.RecordPackageName')
-                ->where('UserID', $UserID)
-                ->select('PackgeName')
-                ->first()->PackgeName ?? '';
+            ->where('UserID', $UserID)
+            ->select('PackgeName')
+            ->first()->PackgeName ?? '';
 
         // 裂变领取限制开关
         $redis = Redis::connection();
@@ -670,7 +670,8 @@ class GlobalLogicController extends BaseLogicController
             'waitGetEmailScore' => $waitGetEmailScore,
             'score' => $score,
             'insureScore' => $insureScore,
-
+            'todayMaxScore' => $today[0]->MaxScore ?? 0,
+            'todayMaxWinScore' => $today[0]->MaxWinScore ?? 0,
 
         ];
 
@@ -701,7 +702,55 @@ class GlobalLogicController extends BaseLogicController
             $platformData[$pitem]['total'] = (Redis::get($key)?:0)/100;
             $platformData[$pitem]['today'] = (Redis::get($dkey)?:0)/100;
         }
+        // 手机型号
+        $data['mobileBand'] = '';
+        $res = Redis::get('user_ua_' . ($user->UserID ?? ''));
+        if ($res) {
+            $ua = $res ?? '';
+            if (stripos($ua, 'iPhone') !== false) {
+                $data['mobileBand'] = 'iPhone';
+            }
+            if (preg_match('/Android\s[\d\.]+;\s([^)]+)/i', $ua, $match)) {
+                $data['mobileBand'] = 'Android' . trim($match[1]);
+            }
+            if (stripos($ua, 'Windows') !== false) {
+                $data['mobileBand'] = 'PC';
+            }
+        }
+        // 退款信息
+        $data['refund_flag'] = DB::table('agent.dbo.order')
+                ->where(['user_id' => $UserID, 'pay_status' => 9])
+                ->count() > 0;
+        $data['refund_total'] = 0;
+        if ($data['refund_flag']) {
+            $samePhoneUids = [];
+            if ($userInfo->phone) {
+                $samePhoneUids = DB::connection('read')->table('QPAccountsDB.dbo.AccountPhone')
+                    ->where('PhoneNum', $userInfo->phone)->pluck('UserID')->toArray();
+            }
 
+            $sameRegIpUids = DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo')
+                ->where('IsAndroid', 0)
+                ->where('RegisterIP', $userInfo->RegisterIP)
+                ->pluck('UserID')->toArray();
+            $ips = DB::connection('read')->table('QPRecordDB.dbo.RecordUserLogonStatistics')
+                ->where('UserID', $user->UserID ?? 0)
+                ->distinct()
+                ->pluck('LogonIP');
+
+            $sameLoginIpUids = DB::connection('read')->table('QPRecordDB.dbo.RecordUserLogonStatistics')
+                ->whereIn('LogonIP', $ips)
+                ->selectRaw('UserID')
+                ->pluck('UserID')->toArray();
+            $uids = array_unique(array_merge($samePhoneUids, $sameRegIpUids, $sameLoginIpUids));
+            $uids[] = $UserID;
+            if (count($uids) > 0) {
+                $data['refund_total'] = DB::table('agent.dbo.order')->lock('WITH(NOLOCK)')
+                    ->whereIn('user_id', $uids)
+                    ->where('pay_status', 9)
+                    ->sum('amount');
+            }
+        }
 
         return compact('data', 'userInfo', 'registerInviteSwitches', 'gameCount', 'userSource', 'OpenPage','platformData');
     }
@@ -735,11 +784,11 @@ class GlobalLogicController extends BaseLogicController
 
         // 有没有在房间
         $User->ServerName = DB::connection('read')->table('QPTreasureDB.dbo.GameScoreLocker as gsl')
-                ->join('QPPlatformDB.dbo.GameRoomInfo as gri', 'gsl.ServerID', 'gri.ServerID')
-                ->where('UserID', $UserID)
-                ->whereRaw('datediff(hh,CollectDate,getdate())<=5')
-                ->select('ServerName')
-                ->first()->ServerName ?? '';
+            ->join('QPPlatformDB.dbo.GameRoomInfo as gri', 'gsl.ServerID', 'gri.ServerID')
+            ->where('UserID', $UserID)
+            ->whereRaw('datediff(hh,CollectDate,getdate())<=5')
+            ->select('ServerName')
+            ->first()->ServerName ?? '';
 
 
         // 用户标签
@@ -808,16 +857,16 @@ class GlobalLogicController extends BaseLogicController
 
         // 月卡 -- 购买
         $buyMonthCard = DB::connection('read')->table('QPPlatformDB.dbo.MonthCard as mc')
-                ->join('QPPlatformDB.dbo.UserMonthCard as uc', 'mc.CardID', 'uc.CardID')
-                ->where('uc.UserID', $UserID)
-                ->selectRaw('IsNull(sum(Price),0) TotalReward')
-                ->first()->TotalReward / 100 ?? 0;
+            ->join('QPPlatformDB.dbo.UserMonthCard as uc', 'mc.CardID', 'uc.CardID')
+            ->where('uc.UserID', $UserID)
+            ->selectRaw('IsNull(sum(Price),0) TotalReward')
+            ->first()->TotalReward / 100 ?? 0;
 
         // 月卡 -- 已领
         $getMonthCard = DB::connection('read')->table('QPPlatformDB.dbo.UserMonthCard')
-                ->where('UserID', $UserID)
-                ->selectRaw('IsNull(sum(TotalReward),0) Reward')
-                ->first()->Reward / 100 ?? 0;
+            ->where('UserID', $UserID)
+            ->selectRaw('IsNull(sum(TotalReward),0) Reward')
+            ->first()->Reward / 100 ?? 0;
 
         # 保底值
         $StatusValue = 0;
@@ -867,10 +916,10 @@ class GlobalLogicController extends BaseLogicController
 
         // 充值奖金  已领
         $Reward = DB::connection('read')->table('QPRecordDB.dbo.RecordUserScoreChange')
-                ->where('Reason', 53)
-                ->where('UserID', $UserID)
-                ->selectRaw('sum(ChangeScore) Score')
-                ->first()->Score / NumConfig::NUM_VALUE ?? 0;
+            ->where('Reason', 53)
+            ->where('UserID', $UserID)
+            ->selectRaw('sum(ChangeScore) Score')
+            ->first()->Score / NumConfig::NUM_VALUE ?? 0;
 
         // 充值奖金  可领
         $recharge = (new Extensions())->recharge($UserID);
@@ -880,10 +929,10 @@ class GlobalLogicController extends BaseLogicController
 
         // 推广赚金 注册已领
         $reward = DB::connection('read')->table('QPRecordDB.dbo.RecordUserScoreChange')
-                ->where('Reason', 72)
-                ->where('UserID', $UserID)
-                ->selectRaw('sum(ChangeScore) Score')
-                ->first()->Score / NumConfig::NUM_VALUE ?? 0;
+            ->where('Reason', 72)
+            ->where('UserID', $UserID)
+            ->selectRaw('sum(ChangeScore) Score')
+            ->first()->Score / NumConfig::NUM_VALUE ?? 0;
 
         // 推广赚金 注册可领
         $collectable = (new Extensions())->register($UserID)['Register'];
@@ -893,16 +942,16 @@ class GlobalLogicController extends BaseLogicController
         $registerScore['Reward'] = $reward;
 
         $todayReward = DB::connection('read')->table('QPRecordDB.dbo.RecordUserScoreStatisticsNew')
-                ->whereIn('ScoreType', [53, 72])
-                ->where('UserID', $UserID)
-                ->selectRaw('sum(Score) Score')
-                ->first()->Score / NumConfig::NUM_VALUE ?? 0;
+            ->whereIn('ScoreType', [53, 72])
+            ->where('UserID', $UserID)
+            ->selectRaw('sum(Score) Score')
+            ->first()->Score / NumConfig::NUM_VALUE ?? 0;
 
         // 包名
         $PackgeName = DB::connection('read')->table('QPRecordDB.dbo.RecordPackageName')
-                ->where('UserID', $UserID)
-                ->select('PackgeName')
-                ->first()->PackgeName ?? '';
+            ->where('UserID', $UserID)
+            ->select('PackgeName')
+            ->first()->PackgeName ?? '';
 
         // 个人池控制:
         $RecordRechargeControl = DB::connection('read')->table('QPRecordDB.dbo.RecordRechargeControl as rc')
@@ -1091,19 +1140,19 @@ class GlobalLogicController extends BaseLogicController
     {
         $where = [];
         $UserID = DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo')
-                ->where('GameID', $GameID)
-                ->select('UserID')
-                ->first()->UserID ?? '';
+            ->where('GameID', $GameID)
+            ->select('UserID')
+            ->first()->UserID ?? '';
 
         if (!empty($id_list) && empty($start_time)) {
             $withDrawFinal = DB::connection('read')->table('QPAccountsDB.dbo.OrderWithDraw as ow')
-                    ->join('QPAccountsDB.dbo.AccountsInfo as ai', 'ow.UserID', 'ai.UserID')
-                    ->where('ow.State', 2)
-                    ->where('ai.GameID', $GameID)
-                    ->where('finishDate', '>=', date('Y-m-d 00:00:00'))
-                    ->select('finishDate')
-                    ->orderByDesc('finishDate')
-                    ->first()->finishDate ?? '';
+                ->join('QPAccountsDB.dbo.AccountsInfo as ai', 'ow.UserID', 'ai.UserID')
+                ->where('ow.State', 2)
+                ->where('ai.GameID', $GameID)
+                ->where('finishDate', '>=', date('Y-m-d 00:00:00'))
+                ->select('finishDate')
+                ->orderByDesc('finishDate')
+                ->first()->finishDate ?? '';
             if (!empty($withDrawFinal)) {
                 $start_time = date('Y-m-d H:i:s', strtotime($withDrawFinal));
                 $end_time = date('Y-m-d 23:59:59', strtotime($start_time));

+ 25 - 19
app/Http/logic/admin/WithdrawalLogic.php

@@ -157,9 +157,10 @@ class WithdrawalLogic extends BaseLogicController
             AccountWithDrawInfo::orderOverDownExcel($SQL, $where);
 
         } else {
-            $field = ['ai.RegisterIP','ow.locking', 'ar.remarks','ow.remark', 'ow.RecordID', 'ar.RecordID as ar_RecordID', 'wn.state as wn_state', 'ai.NickName', 'ow.AccountsBank', 'ow.BankUserName', 'ow.WithDraw', 'ow.State', 'ow.BankNO', 'ow.ServiceFee', 'OrderId', 'ai.GameID', 'ai.UserID', 'ai.Channel', 'ar.admin_id', 'ow.agent', 'finishDate', 'PixNum', 'PixType', 'ow.EmailAddress', 'ow.PhoneNumber', 'ow.AdhaarNumber', 'ow.IFSCNumber'];
+            $field = ['ai.RegisterIP','ow.locking', 'ar.remarks','ow.remark', 'ow.RecordID', 'ar.RecordID as ar_RecordID', 'wn.state as wn_state', 'ai.NickName', 'ow.AccountsBank', 'ow.BankUserName', 'ow.WithDraw', 'ow.State', 'ow.BankNO', 'ow.ServiceFee', 'ow.withdraw_fee', 'OrderId', 'ai.GameID', 'ai.UserID', 'ai.Channel', 'ar.admin_id', 'ow.agent', 'finishDate', 'PixNum', 'PixType', 'ow.EmailAddress', 'ow.PhoneNumber', 'ow.AdhaarNumber', 'ow.IFSCNumber'];
             $SQL1 = clone $SQL;
             $SQL2 = clone $SQL;
+            $SQL3 = clone $SQL;
             $model = new AccountWithDrawInfo();
 
 
@@ -188,6 +189,10 @@ class WithdrawalLogic extends BaseLogicController
                 $overUserCount->WithDraw = number_float($overUserCount->WithDraw / NumConfig::NUM_VALUE);
             }
 
+            // 手续费汇总(当前筛选条件下)
+            $totalWithdrawFeeRow = $SQL3->where($where)->selectRaw('sum(cast(ISNULL(ow.withdraw_fee,0) as bigint)) as total_withdraw_fee')->lock('with(nolock)')->first();
+            $totalWithdrawFee = isset($totalWithdrawFeeRow->total_withdraw_fee) ? number_float($totalWithdrawFeeRow->total_withdraw_fee / NumConfig::NUM_VALUE) : 0;
+
             $userIDs = [];
             $adminIDs = [];
             $agentIDs = [];
@@ -196,6 +201,7 @@ class WithdrawalLogic extends BaseLogicController
                 $val->actual_arrival = number_float(($val->WithDraw + $val->ServiceFee) / 100); // 实际提现金额
                 $val->ServiceFee = number_float($val->ServiceFee / 100);
                 $val->WithDraw = number_float($val->WithDraw / 100);
+                $val->withdraw_fee_display = number_float(($val->withdraw_fee ?? 0) / NumConfig::NUM_VALUE);
                 $val->sameNameNum=$accountsInfo->sameWithDrawBankName($val->BankUserName);
                 $val->sameEmailNum=$accountsInfo->sameWithDrawEmail($val->EmailAddress);
                 $val->sameMac=$accountsInfo->sameLoginMacCount($val->UserID);
@@ -280,7 +286,7 @@ class WithdrawalLogic extends BaseLogicController
             ->select('PackageName', 'Channel')
             ->pluck('PackageName', 'Channel');
 
-        return compact('applyUserCount', 'overUserCount', 'list', 'payState', 'allChannel', 'Channel', 'GameID', 'withdraw_search', 'withdraw', 'state', 'start_time', 'end_time', 'agent', 'orderID', 'final_start_time', 'final_end_time', 'take_effect', 'withdrawal_administrator', 'isEmpty', 'register_start_time', 'register_end_time', 'PackgeName', 'ChannelPackageName');
+        return compact('applyUserCount', 'overUserCount', 'list', 'totalWithdrawFee', 'payState', 'allChannel', 'Channel', 'GameID', 'withdraw_search', 'withdraw', 'state', 'start_time', 'end_time', 'agent', 'orderID', 'final_start_time', 'final_end_time', 'take_effect', 'withdrawal_administrator', 'isEmpty', 'register_start_time', 'register_end_time', 'PackgeName', 'ChannelPackageName');
     }
 
 
@@ -309,28 +315,28 @@ class WithdrawalLogic extends BaseLogicController
                 //代理订单
                 $value->agent=
                 $this->agent=[(object)[
-                                  "id"           => "6666",
-                                  "name"         => "代理申请为玩家提现",
-                                  "config_key"   => "AgentCashOut",
-                                  "config_value" => "66",
-                                  "type"         => "cash",
-                                  "created_at"   => null,
-                                  "updated_at"   => null,
-                                  "admin_id"     => "11",
-                                  "status"       => "1",
-                                  "sort"         => "0",
-                                  "remarks"      => null,
-                                  "new_pay_type" => "0",
-                                  "pic_num"      => null,
-                                  "pay_error"    => "0",
-                                  "cpf_first"    => "0"
-                              ]];
+                    "id"           => "6666",
+                    "name"         => "代理申请为玩家提现",
+                    "config_key"   => "AgentCashOut",
+                    "config_value" => "66",
+                    "type"         => "cash",
+                    "created_at"   => null,
+                    "updated_at"   => null,
+                    "admin_id"     => "11",
+                    "status"       => "1",
+                    "sort"         => "0",
+                    "remarks"      => null,
+                    "new_pay_type" => "0",
+                    "pic_num"      => null,
+                    "pay_error"    => "0",
+                    "cpf_first"    => "0"
+                ]];
                 continue;
             }
             if(isset($channelConfigs[$value->Channel]) && $channelConfigs[$v->channel]->limit_manual_review_show) {
                 $c = $channelConfigs[$value->Channel];
                 $value->agent = array_filter($this->agent->toArray(), function ($item) use ($c) {
-                   return $item->config_value == $c->agent;
+                    return $item->config_value == $c->agent;
                 });
             }
         }

+ 71 - 50
app/Models/AccountsInfo.php

@@ -468,7 +468,7 @@ class AccountsInfo extends Model
         return DB::connection('read')->table(TableName::QPRecordDB() . 'RecordUserDataStatisticsNew')
             ->where('DateID', date('Ymd'))
             ->whereIn('UserID', $UserIDs)
-            ->selectRaw('Withdraw,Handsel, Recharge, (WinScore + LostScore) Score,ServiceFee,UserID')
+            ->selectRaw('Withdraw,Handsel, Recharge, (WinScore + LostScore) Score,ServiceFee,UserID,MaxScore,MaxWinScore')
             ->get();
     }
 
@@ -545,11 +545,11 @@ class AccountsInfo extends Model
     {
         // 有没有在房间
         return DB::connection('read')->table('QPTreasureDB.dbo.GameScoreLocker as gsl')
-                ->join('QPPlatformDB.dbo.GameRoomInfo as gri', 'gsl.ServerID', 'gri.ServerID')
-                ->where('UserID', $UserID)
-                ->whereRaw('datediff(hh,CollectDate,getdate())<=5')
-                ->select('ServerName')
-                ->first()->ServerName ?? '';
+            ->join('QPPlatformDB.dbo.GameRoomInfo as gri', 'gsl.ServerID', 'gri.ServerID')
+            ->where('UserID', $UserID)
+            ->whereRaw('datediff(hh,CollectDate,getdate())<=5')
+            ->select('ServerName')
+            ->first()->ServerName ?? '';
     }
 
     // 用户未领邮件金额
@@ -601,18 +601,18 @@ class AccountsInfo extends Model
     {
         return 0;
         return DB::connection('read')->table('QPAccountsDB.dbo.AccountsInfo as di')
-                 ->join('QPAccountsDB.dbo.AccountsInfo as dif', 'di.RegisterIP', 'dif.RegisterIP')
-                 ->where('di.UserID', $UserID)
-                 ->selectRaw('count(1) countip')
-                 ->first()->countip;
+            ->join('QPAccountsDB.dbo.AccountsInfo as dif', 'di.RegisterIP', 'dif.RegisterIP')
+            ->where('di.UserID', $UserID)
+            ->selectRaw('count(1) countip')
+            ->first()->countip;
     }
     //关联提现名称
     public function sameWithDrawBankName($name){
         if($name==='')return 0;
         return DB::table(TableName::QPAccountsDB() . 'AccountWithDrawInfo')
-                    ->select('UserID')
-                    ->where("BankUserName",$name)
-                    ->count();
+            ->select('UserID')
+            ->where("BankUserName",$name)
+            ->count();
 
 
 
@@ -621,9 +621,9 @@ class AccountsInfo extends Model
     public function sameWithDrawEmail($email){
         if($email==='')return 0;
         return DB::table(TableName::QPAccountsDB() . 'AccountWithDrawInfo')
-                    ->select('UserID')
-                    ->where("EmailAddress",$email)
-                    ->count();
+            ->select('UserID')
+            ->where("EmailAddress",$email)
+            ->count();
 
     }
     //关联提现名称
@@ -631,37 +631,50 @@ class AccountsInfo extends Model
 
 
         return DB::connection('read')->table('QPAccountsDB.dbo.AccountWithDrawInfo as di')
-                 ->join('QPAccountsDB.dbo.AccountWithDrawInfo as dif', 'di.BankUserName', 'dif.BankUserName')
-                 ->where('di.UserID', $UserID)
-                 ->whereNotNull('di.BankUserName')
-                 ->where('di.BankUserName', '<>', '')
-                 ->selectRaw('count(1) count,di.BankUserName')
-                 ->groupBy('di.BankUserName')
-                 ->first();
+            ->join('QPAccountsDB.dbo.AccountWithDrawInfo as dif', 'di.BankUserName', 'dif.BankUserName')
+            ->where('di.UserID', $UserID)
+            ->whereNotNull('di.BankUserName')
+            ->where('di.BankUserName', '<>', '')
+            ->selectRaw('count(1) count,di.BankUserName')
+            ->groupBy('di.BankUserName')
+            ->first();
 
 
 
     }
     //关联提现mail
     public function sameWithDrawEmailByUserID($UserID){
-        return DB::connection('read')->table('QPAccountsDB.dbo.AccountWithDrawInfo as di')
-                 ->join('QPAccountsDB.dbo.AccountWithDrawInfo as dif', 'di.EmailAddress', 'dif.EmailAddress')
-                 ->where('di.UserID', $UserID)
-                 ->whereNotNull('di.EmailAddress')
-                 ->where('di.EmailAddress', '<>', '')
-                 ->selectRaw('count(1) count,di.EmailAddress')
-                 ->groupBy('di.EmailAddress')
-                 ->first();
+        // 先获取用户的邮箱
+        $user = DB::connection('read')->table('QPAccountsDB.dbo.AccountWithDrawInfo')
+            ->where('UserID', $UserID)
+            ->whereNotNull('EmailAddress')
+            ->where('EmailAddress', '<>', '')
+            ->first(['EmailAddress']);
+
+        $result = json_decode('{"count":null,"EmailAddress":null}');
+        if ($user) {
+            // 再查询相同邮箱的用户数
+            $result = DB::connection('read')->table('QPAccountsDB.dbo.AccountWithDrawInfo')
+                ->where('EmailAddress', $user->EmailAddress)
+                ->selectRaw('COUNT(*) as count, ? as EmailAddress', [$user->EmailAddress])
+                ->first();
+        }
+        return $result;
 
     }
     // 关联登录IP数量
     public function sameLoginIPCount($UserID)
     {
-        return DB::connection('read')->table('QPRecordDB.dbo.RecordUserLogonStatistics as a')
-            ->join('QPRecordDB.dbo.RecordUserLogonStatistics as b', 'a.LogonIP', 'b.LogonIP')
-            ->where('a.UserID', $UserID)
-            ->selectRaw('count(distinct(b.UserID)) countIP')
+        $ips = DB::connection('read')->table('QPRecordDB.dbo.RecordUserLogonStatistics')
+            ->where('UserID', $UserID)
+            ->distinct()
+            ->pluck('LogonIP');
+
+        $count = DB::connection('read')->table('QPRecordDB.dbo.RecordUserLogonStatistics')
+            ->whereIn('LogonIP', $ips)
+            ->selectRaw('count(distinct UserID) countIP')
             ->first()->countIP;
+        return $count;
     }
 
 
@@ -670,16 +683,16 @@ class AccountsInfo extends Model
     {
 
         $mac = DB::connection('read')->table('QPRecordDB.dbo.RecordUserLogonStatistics')
-                 ->where('UserID', $UserID)
-                 ->where('mac', '<>','')
-                  ->selectRaw("distinct(mac) mac")
-                 ->pluck('mac')->toArray();
+            ->where('UserID', $UserID)
+            ->where('mac', '<>','')
+            ->selectRaw("distinct(mac) mac")
+            ->pluck('mac')->toArray();
 
         if($mac) {
             return DB::connection('read')->table('QPRecordDB.dbo.RecordUserLogonStatistics')
-                     ->whereIn('mac', $mac)
-                     ->selectRaw('count(distinct(UserID)) countIP')
-                     ->first()->countIP;
+                ->whereIn('mac', $mac)
+                ->selectRaw('count(distinct(UserID)) countIP')
+                ->first()->countIP;
         }
         return 0;
     }
@@ -688,14 +701,22 @@ class AccountsInfo extends Model
     // 关联银行卡
     public function sameBankNo($UserID)
     {
-        return DB::connection('read')->table('QPAccountsDB.dbo.AccountWithDrawInfo as di')
-            ->join('QPAccountsDB.dbo.AccountWithDrawInfo as dif', 'di.BankNo', 'dif.BankNo')
-            ->where('di.UserID', $UserID)
-            ->whereNotNull('di.BankNo')
-            ->where('di.BankNo', '<>', '')
-            ->selectRaw('count(1) count,di.BankNo')
-            ->groupBy('di.BankNo')
-            ->first();
+        // 步骤1: 获取当前用户的BankNo
+        $user = DB::connection('read')->table('QPAccountsDB.dbo.AccountWithDrawInfo')
+            ->where('UserID', $UserID)
+            ->whereNotNull('BankNo')
+            ->where('BankNo', '<>', '')
+            ->first(['BankNo']);
+
+        // 步骤2: 查询相同BankNo的数量
+        $result = json_decode('{"count":null,"BankNo":null}');
+        if ($user) {
+            $result = DB::connection('read')->table('QPAccountsDB.dbo.AccountWithDrawInfo')
+                ->where('BankNo', $user->BankNo)
+                ->selectRaw('count(*) as count, ? as BankNo', [$user->BankNo])
+                ->first();
+        }
+        return $result;
     }
 
     // 用户彩金

+ 7 - 5
app/Models/RecordScoreInfo.php

@@ -17,7 +17,9 @@ class RecordScoreInfo extends Model
     protected $guarded = [];
 
     // 彩金----21:绑定手机赠送--33:注册赠送--44:签到--45:充值--49:月卡--42:邮件附件(彩金) 51:首充彩金 36:推广充值彩金
-    protected static $Reason = [21, 33, 36, 37, 42, 44, 45, 49, 51, 52, 72];
+    protected static $Reason = [21, 33, 36, 37, 42, 44, 45, 49, 51, 52, 72, 73];
+    /** @var int vip商城充值额外赠送 */
+    const REASON_VIP_SEND_CHIPS = 73;
 
     public static function addScore($user_id, $ChangeScore, $Reason,$currentScore = 0)
     {
@@ -27,10 +29,10 @@ class RecordScoreInfo extends Model
             $AfterScore = $currentScore;
         }else{
             $AfterScore = DB::connection('read')->table('QPTreasureDB.dbo.GameScoreInfo')
-                    ->where('UserID', $user_id)
-                    ->select('Score')
-                    ->lockForUpdate()
-                    ->first()->Score ?? 0;
+                ->where('UserID', $user_id)
+                ->select('Score')
+                ->lockForUpdate()
+                ->first()->Score ?? 0;
         }
 
         if ($ChangeScore > 0 && $Reason > 0) {

+ 77 - 13
app/Services/OrderServices.php

@@ -188,22 +188,24 @@ class  OrderServices
                 }
 
             }else if ($GiftsID == 305) { // 连续未充值 VIP 新礼包
-                // 检查用户是否已经充值过 305 礼包
-                $hasPurchased305 = DB::connection('write')->table('agent.dbo.order')
+                // 305 改为按“轮次”判断:上一轮过期后可再次充值并获得新一轮奖励
+                $latestRecord = DB::connection('write')->table('agent.dbo.inactive_vip_gift_records')
                     ->where('user_id', $user_id)
-                    ->where('GiftsID', 305)
-                    ->where('pay_status', 1)
-                    ->exists();
-                
-                if ($hasPurchased305) {
-                    // 已经充值过 305 礼包,只发放本金(不发放奖励)
+                    ->where('gift_id', 305)
+                    ->orderBy('id', 'desc')
+                    ->first();
+
+                $canStartNewRound = !$latestRecord || (strtotime($latestRecord->expired_at) < time());
+
+                if (!$canStartNewRound) {
+                    // 当前轮次未过期:只发放本金(不发放奖励)
                     $Recharge = $payAmt;
                     $give = 0;
                     $favorable_price = $Recharge + $give;
                     $czReason = 1;
                     $cjReason = 45;
                 } else {
-                    // 首次充值 305 礼包,发放 120% 奖励(固定120%,不从配置读取)
+                    // 开启新一轮 305:发放 120% 立即奖励并创建新的 7 日礼包记录
                     $favorable_price = round($payAmt * 120 / 100, 2);
                     $give = $favorable_price - $payAmt;
                     $Recharge = $payAmt;
@@ -347,8 +349,68 @@ class  OrderServices
             if ($AfterScore) {
                 RecordScoreInfo::addScore($user_id, ($give * NumConfig::NUM_VALUE), $cjReason, $AfterScore); #赠送彩金
             }
+            // vip额外赠送
+            if ($GiftsID == 0) {
+                $userRecharge = $query ?: 0;
+                $VIP = VipService::calculateVipLevel($user_id,$userRecharge);
+                $level = VipService::getVipByField('VIP', $VIP);
+                if ($level && $level->RechargeExtraSendRate > 0) {
+                    $vipSendChips = floor($Recharge * NumConfig::NUM_VALUE * ($level->RechargeExtraSendRate/100));
+                    if ($vipSendChips > 0) {
+                        RecordScoreInfo::addScore($user_id, $vipSendChips, RecordScoreInfo::REASON_VIP_SEND_CHIPS, $AfterScore);
+                        app(PaidRewardStatisticsService::class)
+                            ->incrementRecordByDateIDAndType(date('Ymd'), 'vip_recharge', $vipSendChips);
+                    }
+                }
+            }
+
+            if (in_array($GiftsID, [0, 301, 302, 304, 305, 402]) && $give > 0) {
+                $typeMap= [
+                    0 => 'normal_recharge',
+                    301 => 'first_recharge_gift',
+                    302 => 'bankrupt_gift',
+                    304 => 'daily_gift',
+                    305 => 'vip_inactive_gift',
+                    402 => 'christmas_gift',
+                ];
+                $type = $typeMap[intval($GiftsID)] ?? 'unknown_gift';
+                app(PaidRewardStatisticsService::class)
+                    ->incrementRecordByDateIDAndType(date('Ymd'), $type, $give * NumConfig::NUM_VALUE);
+            }
+            $typeMap= [
+                0 => 'normal_recharge_chips',
+                301 => 'first_recharge_gift_chips',
+                302 => 'bankrupt_gift_chips',
+                304 => 'daily_gift_chips',
+                305 => 'vip_inactive_gift_chips',
+                306 => 'free_bonus_gift_chips',
+                402 => 'christmas_gift_chips',
+            ];
+            $type = $typeMap[intval($GiftsID)] ?? 'unknown_chips';
+            app(PaidRewardStatisticsService::class)
+                ->incrementRecordByDateIDAndType(date('Ymd'), $type, ($Recharge+$give) * NumConfig::NUM_VALUE);
+            app(PaidRewardStatisticsService::class)
+                ->incrementRecordByDateIDAndType(date('Ymd'), 'recharge_real', $Recharge * NumConfig::NUM_VALUE);
+
+
         }
-        $favorable_price =  (int) round($favorable_price * NumConfig::NUM_VALUE);
+        // free bonus 礼包:充值后赠送 InsureScore(单位为分)
+        if (in_array($GiftsID, [306]) && $payAmt > 0) {
+            $gift = DB::table('agent.dbo.recharge_gift')->lock('with(nolock)')
+                ->where(['gift_id' => $GiftsID, 'recommend' => $Recharge])
+                ->first();
+            if ($gift && $gift->task_bonus > 0) {
+                $bonus = (int) round($gift->task_bonus * NumConfig::NUM_VALUE);
+                if ($bonus > 0) {
+                    DB::table('QPTreasureDB.dbo.GameScoreInfo')
+                        ->where('UserID', $user_id)
+                        ->increment('InsureScore', $bonus);
+                    app(PaidRewardStatisticsService::class)
+                        ->incrementRecordByDateIDAndType(date('Ymd'), 'free_bonus_gift', $give * NumConfig::NUM_VALUE);
+                }
+            }
+        }
+        $favorable_price =  (int) round($favorable_price * NumConfig::NUM_VALUE) + ($vipSendChips ?? 0);
         $firstScore = DB::connection('write')->table('QPTreasureDB.dbo.GameScoreInfo')->where('UserID', $user_id)->value('Score');
         $Score = $favorable_price + $firstScore;
 
@@ -439,7 +501,7 @@ class  OrderServices
 //            }
 //        }
 
-        if ($AdId && $payAmt) AfEvent::dispatch([$user_id, $payAmt, $AdId, $eventType]);
+//        if ($AdId && $payAmt) AfEvent::dispatch([$user_id, $payAmt, $AdId, $eventType]);
         try {
             //新邀请
             //(new AgentController())->processDeposit($user_id, $payAmt,$order_sn);
@@ -519,9 +581,11 @@ class  OrderServices
                         'Score' => 0,                         // Score置0
                         'ScoreChange' => 1,                   // ScoreChange设置成1
                         'MaxScore' => 0,                      // MaxScore清0
-                        'MaxWinScore' => 0                    // MaxWinScore清0
+                        'MaxWinScore' => 0,                    // MaxWinScore清0
+                        'PlayTimeCount' => 0,                   //重置游戏时间
+                        'OnLineTimeCount' => $scoreInfo->PlayTimeCount, //赋值免费游戏时间
                     ]);
-
+                //TODO
                 \Log::info('GameScoreInfo切换成功', [
                     'user_id' => $user_id,
                     'original_score' => $scoreInfo->Score,

+ 43 - 0
app/Services/PaidRewardStatisticsService.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Database\QueryException;
+use Illuminate\Support\Facades\DB;
+
+class PaidRewardStatisticsService
+{
+    /**
+     * @param $DateID
+     * @param $type
+     * @param mixed $amount 单位:分
+     * @return mixed
+     */
+    public function incrementRecordByDateIDAndType($DateID, $type, $amount)
+    {
+        $this->createIfNotExist($DateID, $type);
+        return DB::table('QPRecordDB.dbo.RecordPaidRewardDailyStatistics')
+            ->where(['DateID' => $DateID, 'StatType' => $type])
+            ->increment('TotalAmount', $amount);
+    }
+
+    private function createIfNotExist($DateID, $type)
+    {
+        $exist = DB::table('QPRecordDB.dbo.RecordPaidRewardDailyStatistics')
+            ->where(['DateID' => $DateID, 'StatType' => $type])->first();
+        if (!$exist) {
+            try {
+                DB::table('QPRecordDB.dbo.RecordPaidRewardDailyStatistics')->insert([
+                    'DateID' => $DateID,
+                    'StatType' => $type,
+                ]);
+            } catch (QueryException $e) {
+                if (stripos($e->getMessage(), 'duplicate key') === false) {
+                    throw $e;
+                }
+            }
+
+        }
+        return true;
+    }
+}

+ 212 - 37
app/Services/SuperballActivityService.php

@@ -6,8 +6,10 @@ use App\Facade\TableName;
 use App\Game\Services\OuroGameService;
 use App\Http\helper\NumConfig;
 use Carbon\Carbon;
+use Illuminate\Database\QueryException;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Redis;
+use App\Services\VipService;
 
 /**
  * Superball Activity: recharge + turnover task, balls, lucky number, prize pool.
@@ -15,7 +17,7 @@ use Illuminate\Support\Facades\Redis;
  */
 class SuperballActivityService
 {
-    public const TIER_MAX = 'A';
+    public const TIER_MAX = 'S';
     public const MULTIPLIER_MIN = 1.0;
     public const MULTIPLIER_MAX = 3.0;
     public const MULTIPLIER_STEP = 0.5;
@@ -35,6 +37,11 @@ class SuperballActivityService
         $userTask = $this->getUserTask($userId, $today);
         $multiplierRow = $this->getUserMultiplier($userId);
 
+        // VIP 等级与每日免费球数(赠送球数 = VIP 等级)
+        $vipLevel = $this->getUserVipLevel($userId);
+        $level = VipService::getVipByField('VIP', $vipLevel);
+        $vipFreeBalls = $level ? ($level->SuperballNum ?? 0) : 0;
+
         $rechargeToday = $this->getUserRechargeForDate($userId, $today);
         $turnoverToday = $this->getUserTotalBetForDate($userId, $today);
         $turnoverProgress = (int) $turnoverToday;
@@ -45,8 +52,36 @@ class SuperballActivityService
         $rechargeDisplay = $rechargeToday;
         $turnoverDisplay = $turnoverProgress / NumConfig::NUM_VALUE;
         $taskCompleted = $tierConfig && $rechargeDisplay >= $rechargeRequired && $turnoverDisplay >= $turnoverRequired;
-        $canUpgrade = $userTask && $userTask->tier !== self::TIER_MAX && $taskCompleted;
         $canClaim = $taskCompleted && $userTask && (int) $userTask->status === 0;
+        $canUpgrade = $userTask && $userTask->tier !== self::TIER_MAX && $taskCompleted;
+        // 能升级自动升
+        if ($canUpgrade && $userTask->status != 1) {
+            $tierConfigs = $this->getTierConfig();
+            $up = null;
+            foreach (array_reverse($tierConfigs) as $c) {
+                // 已经是最大的档位,直接升级
+                if ($c['tier'] == self::TIER_MAX
+                    && $c['recharge_required'] < $rechargeToday && $c['turnover_required'] < $turnoverDisplay) {
+                    $up = $c;
+                    break;
+                }
+                if ($c['recharge_required'] < $rechargeToday && $c['turnover_required'] < $turnoverDisplay) {
+                    continue;
+                }
+                $up = $c;
+                break;
+            }
+            $res = $this->upgradeTier($userId, $up['tier']);
+            if ($res['success']) {
+                $tierConfig = $this->getTierConfigByTier($res['new_tier']);
+                $userTask = $this->getUserTask($userId, $today);
+                $rechargeRequired = $tierConfig ? (int) $tierConfig->recharge_required : 0;
+                $turnoverRequired = $tierConfig ? (int) $tierConfig->turnover_required : 0;
+                $taskCompleted = $tierConfig && $rechargeDisplay >= $rechargeRequired && $turnoverDisplay >= $turnoverRequired;
+                $canClaim = $taskCompleted && $userTask && (int) $userTask->status === 0;
+            }
+        }
+
 
         $yesterdayBalls = $this->getUserBalls($userId, $yesterday);
         $yesterdayLucky = (int) ($yesterdayDaily->lucky_number ?? 0);
@@ -63,6 +98,8 @@ class SuperballActivityService
             $canClaimYesterday = true;
             $yesterdayPendingPrize = $this->calculateYesterdayPrizeForUser($userId, $yesterday);
         }
+        // 用户昨日未领取球
+        $yesterdayNotClaimedBallCount = $this->getNotClaimBallByUserIdDate($userId, $yesterday);
 
         $todayBasePerBall = 0;
         if ($todayDaily->total_balls > 0 && $todayDaily->pool_amount > 0) {
@@ -86,8 +123,8 @@ class SuperballActivityService
             $todayDisplayCompleted = 0;
             $todayDisplayTotalBalls = 0;
             $todayDisplayBasePerBall = 0;
-            $todayDisplayMyBalls = [];
-            $todayDisplayNumberCounts = [];
+            $todayDisplayMyBalls = $todayMyBallsList;
+            $todayDisplayNumberCounts = $todayNumberCounts;
         } else {
             $todayDisplayPoolAmount = (int) $todayDaily->pool_amount;
             $todayDisplayCompleted = (int) ($todayDaily->completed_count ?? 0);
@@ -99,7 +136,7 @@ class SuperballActivityService
 
         $last7Lucky = $this->getLast7DaysLuckyNumbersPrivate();
 
-        return [
+        $data = [
             'yesterday' => [
                 'pool_amount' => (int) $yesterdayDaily->pool_amount,
                 'pool_amount_display' => (int) $yesterdayDaily->pool_amount / NumConfig::NUM_VALUE,
@@ -118,6 +155,7 @@ class SuperballActivityService
                 'can_claim_yesterday' => $canClaimYesterday,
                 'pending_prize' => $yesterdayPendingPrize,
                 'pending_prize_display' => $yesterdayPendingPrize / NumConfig::NUM_VALUE,
+                'yesterday_not_claimed_ball_count' => $yesterdayNotClaimedBallCount,
             ],
             'today' => [
                 'pool_amount' => $todayDisplayPoolAmount,
@@ -149,9 +187,33 @@ class SuperballActivityService
                 'max' => self::MULTIPLIER_MAX,
                 'step' => self::MULTIPLIER_STEP,
             ],
+            'vip' => [
+                'level' => $vipLevel,
+                'daily_free_balls' => $vipFreeBalls,
+            ],
             'lucky_reward_per_ball' => self::LUCKY_REWARD_PER_BALL,
             'lucky_numbers_7_days' => $last7Lucky,
+            'can_sumbit' => count($todayDisplayMyBalls) < $vipFreeBalls + ($taskCompleted ? $tierConfig->ball_count : 0)
         ];
+
+        return $data;
+    }
+
+    /**
+     * 获取用户 VIP 等级(用于每日赠送球数计算)
+     */
+    protected function getUserVipLevel(int $userId): int
+    {
+        if ($userId <= 0) {
+            return 0;
+        }
+
+        // 从 YN_VIPAccount 读取累计充值金额,交由 VipService 计算 VIP 等级
+        $userRecharge = (int) DB::table('QPAccountsDB.dbo.YN_VIPAccount')
+            ->where('UserID', $userId)
+            ->value('Recharge');
+
+        return (int) VipService::calculateVipLevel($userId, $userRecharge);
     }
 
     /**
@@ -198,7 +260,7 @@ class SuperballActivityService
             return ['success' => false, 'message' => ['web.superball.already_claimed', 'Already claimed']];
         }
 
-        $tierOrder = ['E' => 1, 'D' => 2, 'C' => 3, 'B' => 4, 'A' => 5];
+        $tierOrder = ['E' => 1, 'D' => 2, 'C' => 3, 'B' => 4, 'A' => 5, 'S' => 6];
         $currentOrder = $tierOrder[$task->tier] ?? 0;
         $newOrder = $tierOrder[$newTier] ?? 0;
         if ($newOrder <= $currentOrder) {
@@ -231,8 +293,24 @@ class SuperballActivityService
     {
         $today = Carbon::today()->format('Y-m-d');
         $task = $this->getUserTask($userId, $today);
+        $vipLevel = $this->getUserVipLevel($userId);
+        $level = VipService::getVipByField('VIP', $vipLevel);
+        $vipFreeBalls = $level ? ($level->SuperballNum ?? 0) : 0;
         if (!$task) {
-            return ['success' => false, 'message' => ['web.superball.no_task_today', 'No task for today']];
+            // 昨日未领取奖励
+            $yesterday = Carbon::yesterday()->format('Y-m-d');
+            $yesterdayBallCount = $this->getNotClaimBallByUserIdDate($userId, $yesterday);
+            if ($yesterdayBallCount > 0) {
+                $vipFreeBalls += $yesterdayBallCount;
+            }
+            $this->claimIfHasVipReward($userId, $vipFreeBalls);
+            $this->selectTier($userId, 'E');
+            return [
+                'ball_count' => $vipFreeBalls,
+                'base_ball_count' => 0,
+                'vip_free_balls' => $vipFreeBalls,
+                'message' => 'Claim success, please select numbers for your balls',
+            ];
         }
         if ((int) $task->status === 1) {
             return ['success' => false, 'message' => ['web.superball.already_claimed', 'Already claimed']];
@@ -246,25 +324,40 @@ class SuperballActivityService
         $turnoverToday = $this->getUserTotalBetForDate($userId, $today);
         $rechargeOk = ($rechargeToday) >= (int) $tierConfig->recharge_required;
         $turnoverOk = ($turnoverToday / NumConfig::NUM_VALUE) >= (int) $tierConfig->turnover_required;
+        $complete = 1;
         if (!$rechargeOk || !$turnoverOk) {
-            return ['success' => false, 'message' => ['web.superball.task_not_completed', 'Task not completed']];
+            // 本档没完成
+            $complete = 0;
+        }
+        // 所有任务累加
+        $ballCount = 0;
+        if ($complete) {
+            $ballCount = $tierConfig->ball_count;
+        } else if ($tierConfig->tier !== 'E') { // 没完成获取上一级奖励
+            $idx = $tierConfig->sort_index+1;
+            $configs = $this->getTierConfig();
+            foreach ($configs as $config) {
+                if ($config['sort_index'] == $idx) {
+                    $ballCount += $config['ball_count'];
+                }
+            }
         }
 
-        $ballCount = (int) $tierConfig->ball_count;
+        if ($ballCount < 1) {
+            return ['success' => false, 'message' => ['web.superball.task_not_completed', 'Task not completed']];
+        }
 
-        DB::connection('write')->transaction(function () use ($userId, $today, $task, $ballCount) {
+        DB::connection('write')->transaction(function () use ($userId, $today, $task, $ballCount, $complete) {
             DB::connection('write')->table(TableName::agent() . 'superball_user_task')
                 ->where('user_id', $userId)
                 ->where('task_date', $today)
-                ->update(['status' => 1, 'updated_at' => now()->format('Y-m-d H:i:s')]);
+                ->update(['status' => 1, 'complete' => $complete, 'updated_at' => now()->format('Y-m-d H:i:s')]);
 
-            // Perform atomic increments in a single query to avoid holding an explicit row lock via SELECT ... FOR UPDATE.
-            // This is safe because we only increment if the record already exists (same behavior as before).
             DB::connection('write')->table(TableName::agent() . 'superball_daily')
                 ->where('pool_date', $today)
                 ->update([
-                    'total_balls' => DB::raw("total_balls + {$ballCount}"),
-                    'completed_count' => DB::raw('completed_count + 1'),
+                    'total_balls' => DB::raw('total_balls+' . $ballCount),
+                    'completed_count' => DB::raw('completed_count+1'),
                     'updated_at' => now()->format('Y-m-d H:i:s'),
                 ]);
 
@@ -273,37 +366,76 @@ class SuperballActivityService
 
         return [
             'ball_count' => $ballCount,
+            'base_ball_count' => $ballCount,
+            'vip_free_balls' => $vipFreeBalls,
             'message' => 'Claim success, please select numbers for your balls',
         ];
     }
 
+    public function claimIfHasVipReward(int $userId, $vipFreeBalls): bool
+    {
+        $key = sprintf('claim_vip_reward_%s_%s', $userId, date('Ymd'));
+        if (Redis::exists($key)) {
+            return false;
+        }
+        $today = Carbon::today()->format('Y-m-d');
+        DB::connection('write')->table(TableName::agent() . 'superball_daily')
+            ->where('pool_date', $today)
+            ->update([
+                'total_balls' => DB::raw('total_balls+' . $vipFreeBalls),
+                'updated_at' => now()->format('Y-m-d H:i:s'),
+            ]);
+        Redis::set($key, $vipFreeBalls);
+        Redis::expire($key, 86400);
+        return true;
+    }
+
     /**
      * Submit numbers for all balls (0-9 per ball). Must have claimed and not yet submitted.
      * @return array success: ['success' => true] | failure: ['success' => false, 'message' => [...]]
      */
     public function submitNumbers(int $userId, array $numbers): array
     {
+        $key = sprintf('claim_vip_reward_%s_%s', $userId, date('Ymd'));
+        $vipFreeBalls = (int) Redis::get($key);
+        $vipLevel = $this->getUserVipLevel($userId);
+        $level = VipService::getVipByField('VIP', $vipLevel);
+        $vipFreeBalls2 = $level ? ($level->SuperballNum ?? 0) : 0;
+        $vipFreeBalls = max($vipFreeBalls, $vipFreeBalls2);
         $today = Carbon::today()->format('Y-m-d');
         $task = $this->getUserTask($userId, $today);
-        if (!$task || (int) $task->status !== 1) {
-            return ['success' => false, 'message' => ['web.superball.no_claimed_task', 'No claimed task for today']];
-        }
 
+        $ballCount = 0;
         $tierConfig = $this->getTierConfigByTier($task->tier);
-        if (!$tierConfig) {
-            return ['success' => false, 'message' => ['web.superball.activity_not_found', 'Activity not found']];
-        }
-        $ballCount = (int) $tierConfig->ball_count;
-        if (count($numbers) !== $ballCount) {
-            return ['success' => false, 'message' => ['web.superball.number_count_mismatch', 'Number count mismatch']];
+        if ($task && $task->status == 1) {
+            if ($task->complete == 1) {
+                $ballCount = $tierConfig->ball_count;
+            } else if ($tierConfig->tier !== 'E') { // 没完成获取上一级奖励
+                $idx = $tierConfig->sort_index+1;
+                $configs = $this->getTierConfig();
+                foreach ($configs as $config) {
+                    if ($config['sort_index'] == $idx) {
+                        $ballCount += $config['ball_count'];
+                    }
+                }
+            }
         }
 
         $existing = DB::connection('write')->table(TableName::agent() . 'superball_user_balls')
             ->where('user_id', $userId)
             ->where('ball_date', $today)
             ->count();
-        if ($existing > 0) {
-            return ['success' => false, 'message' => ['web.superball.numbers_already_submitted', 'Numbers already submitted']];
+        // 与领取时保持一致:基础任务球数 + 每日 VIP 免费球数
+        $ballCount = $ballCount + $vipFreeBalls;
+        if ($ballCount < 1) {
+            return ['success' => false, 'message' => ['web.superball.number_count_mismatch', 'no ball']];
+        }
+        if (count($numbers) + $existing > $ballCount) {
+            $remain = $ballCount - $existing;
+            if ($remain < 1) {
+                return ['success' => false, 'message' => ['web.superball.number_count_mismatch', 'Number count mismatch']];
+            }
+            $numbers = array_slice($numbers, 0, $remain);
         }
 
         DB::connection('write')->transaction(function () use ($userId, $today, $numbers) {
@@ -322,7 +454,7 @@ class SuperballActivityService
                 ]);
             }
 
-            // 若用户选择的号码等于当日 lucky_number,则把 superball_daily.lucky_count 累加(使用原子自增,避免锁住整行)
+            // 若用户选择的号码等于当日 lucky_number,则把 superball_daily.lucky_count 累加
             $daily = DB::connection('write')
                 ->table(TableName::agent() . 'superball_daily')
                 ->lock('with(nolock)')
@@ -474,16 +606,23 @@ class SuperballActivityService
             return $row;
         }
         $lucky = mt_rand(0, 9);
-        DB::connection('write')->table(TableName::agent() . 'superball_daily')->insert([
-            'pool_date' => $date,
-            'pool_amount' => 0,
-            'total_balls' => 0,
-            'lucky_number' => $lucky,
-            'completed_count' => 0,
-            'lucky_count' => 0,
-            'created_at' => now()->format('Y-m-d H:i:s'),
-            'updated_at' => now()->format('Y-m-d H:i:s'),
-        ]);
+        try {
+            DB::connection('write')->table(TableName::agent() . 'superball_daily')->insert([
+                'pool_date' => $date,
+                'pool_amount' => 0,
+                'total_balls' => 0,
+                'lucky_number' => $lucky,
+                'completed_count' => 0,
+                'lucky_count' => 0,
+                'created_at' => now()->format('Y-m-d H:i:s'),
+                'updated_at' => now()->format('Y-m-d H:i:s'),
+            ]);
+        } catch (QueryException $e) {
+            // Concurrent getOrCreateDaily: unique index IX_superball_daily_date (pool_date)
+            if (stripos($e->getMessage(), 'duplicate key') === false) {
+                throw $e;
+            }
+        }
         return DB::table(TableName::agent() . 'superball_daily')->where('pool_date', $date)->first();
     }
 
@@ -497,6 +636,41 @@ class SuperballActivityService
             ->update(['pool_amount' => $poolAmountInternal, 'updated_at' => now()->format('Y-m-d H:i:s')]);
     }
 
+    /**
+     * 获取某天未领取的球数
+     * @param $userId
+     * @param $date
+     * @return int
+     */
+    public function getNotClaimBallByUserIdDate($userId, $date) :int
+    {
+        $cacheKey = sprintf('superball_yesterday_not_claim_%d_%s', $userId, $date);
+        if (Redis::exists($cacheKey)) {
+            $ball = Redis::get($cacheKey);
+            return (int) $ball;
+        }
+        $ball = 0;
+        $task = $this->getUserTask($userId, $date);
+        if ($task && $task->status == 0) {
+            $config = $this->getTierConfigByTier($task->tier);
+            if ($task->complete) {
+                $ball = $config->ball_count;
+            } else if ($task->tier != 'E') {
+                $idx = $config->sort_index+1;
+                $configs = $this->getTierConfig();
+                foreach ($configs as $config) {
+                    if ($config['sort_index'] == $idx) {
+                        $ball += $config['ball_count'];
+                    }
+                }
+            }
+        }
+        Redis::set($cacheKey, $ball);
+        Redis::expireAt($cacheKey, strtotime('today +1 day'));
+
+        return $ball;
+    }
+
     // --- private helpers ---
 
     private function getTierConfig(): array
@@ -506,6 +680,7 @@ class SuperballActivityService
             ->get();
         return array_map(function ($r) {
             return [
+                'sort_index' => $r->sort_index,
                 'tier' => $r->tier,
                 'recharge_required' => (int) $r->recharge_required,
                 'turnover_required' => (int) $r->turnover_required,

+ 19 - 2
app/Services/VipService.php

@@ -70,8 +70,8 @@ class VipService
         // 从数据库查询(按MinRecharge升序排列)
         $levels = DB::table('QPAccountsDB.dbo.ProtectLevel')
             ->orderBy('MinRecharge', 'asc')
-            ->select('ID', 'VIP', 'MinRecharge', 'Recharge', 'WithdrawLimit', 'DailyWithdraws', 'GrantNum',
-                'LevelUpBonus', 'RechargeExtraSendRate')
+            ->select('ID', 'VIP', 'MinRecharge', 'Recharge', 'WithdrawLimit', 'DailyWithdraws', 'GrantNum', 'LevelUpBonus',
+                'WithdrawFeeRate', 'SignAlpha', 'CustomServiceType', 'RechargeExtraSendRate', 'SuperballNum', 'BirthdayValue')
             ->get();
 
         // 缓存10分钟
@@ -79,4 +79,21 @@ class VipService
 
         return $levels;
     }
+
+    /**
+     * 通过某个字段获取vip信息
+     * @param $field
+     * @param $value
+     * @return mixed|null
+     */
+    public static function getVipByField($field, $value)
+    {
+        $vipLevels = self::getVipLevelConfig();
+        foreach ($vipLevels as $level) {
+            if ($level->{$field} == $value) {
+                return $level;
+            }
+        }
+        return null;
+    }
 }

+ 39 - 21
app/Util.php

@@ -429,7 +429,7 @@ class Util {
      * @param string|array $gear
      * @return string|array
      */
-    public static function filterGearByDevice($gear)
+    public static function filterGearByDevice($gear,$user=null)
     {
         $isString = false;
         if (is_string($gear)) {
@@ -451,7 +451,7 @@ class Util {
 
         $deviceType = self::getDeviceType();
 
-        $filtered = array_filter($gearArray, function ($item) use ($deviceType) {
+        $filtered = array_filter($gearArray, function ($item) use ($deviceType,$user) {
             $status = $item['status'] ?? 1;
             if ($status != 1) {
                 return true;
@@ -476,6 +476,14 @@ class Util {
                 return false;
             }
 
+            if($value === 2){
+                if(is_array($user)){
+                    if($user['vip'] < 2 || (strtotime($user['RegisterDate']) > time()-86400*2)){
+                        return false;
+                    }
+                }
+            }
+
             return true;
         });
 
@@ -761,33 +769,43 @@ class Util {
         return $string;
     }
 
+    protected static $writefirst=[];
+
     //日志
     public static function WriteLog( $key, $data ) {
         $ymd = date( "Ymd" );
         $date = date( 'Y-m-d H:i:s' );
         $file = storage_path( 'logs' ) . "/{$ymd}_{$key}.log";
-        $ip = IpLocation::getRealIp();
-        $agent=@$_SERVER['HTTP_USER_AGENT']??"";
-        $locale=@$_SERVER['HTTP_ACCEPT_LANGUAGE']??"";
-        $ref = @$_SERVER['HTTP_REFERER'];
-        $url = @$_SERVER['REQUEST_URI'];
-        $host = @$_SERVER['HTTP_HOST'];
-
-        if ( !is_string( $data ) )
-            $data = json_encode( $data, JSON_UNESCAPED_UNICODE );
-        $uid = isset( $GLOBALS['user_id'] ) ? $GLOBALS['user_id'] : 0;
+        $content = "date: {$date}\n";
+        if(!isset(self::$writefirst[$key])) {
+            self::$writefirst[$key] = true;
+            $ip = IpLocation::getRealIp();
+            $agent = @$_SERVER['HTTP_USER_AGENT'] ?? "";
+            $locale = @$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? "";
+            $ref = @$_SERVER['HTTP_REFERER'];
+            $url = @$_SERVER['REQUEST_URI'];
+            $host = @$_SERVER['HTTP_HOST'];
+            $cookie = !empty($_COOKIE) ? json_encode($_COOKIE, JSON_UNESCAPED_UNICODE) : '';
+
+            $uid = isset($GLOBALS['user_id']) ? $GLOBALS['user_id'] : 0;
 
 //        $params = array( 'POST' => $_POST );
-        $params = empty( $_REQUEST ) ? '' : json_encode( $_REQUEST, JSON_UNESCAPED_UNICODE );
+            $params = empty($_REQUEST) ? '' : json_encode($_REQUEST, JSON_UNESCAPED_UNICODE);
+
+
+            $content .= "ip: {$ip}({$uid})   agent:" . $agent . "   locale:" . $locale . "\n";
+            if (!empty($ref))
+                $content .= "referer: {$ref}\n";
+            if (!empty($url))
+                $content .= "request: {$host}{$url}\n";
+            if (!empty($params))
+                $content .= "params: {$params}\n";
+            if (!empty($cookie))
+                $content .= "cookie: {$cookie}\n";
+        }
+        if (!is_string($data))
+            $data = json_encode($data, JSON_UNESCAPED_UNICODE);
 
-        $content = "date: {$date}\n";
-        $content .= "ip: {$ip}({$uid})   agent:".$agent."   locale:".$locale."\n";
-        if ( !empty( $ref ) )
-            $content .= "referer: {$ref}\n";
-        if ( !empty( $url ) )
-            $content .= "request: {$host}{$url}\n";
-        if ( !empty( $params ) )
-            $content .= "params: {$params}\n";
         if ( !empty( $data ) )
             $content .= "content: {$data}\n";
         $content .= "\n";

+ 6 - 2
resources/views/admin/Withdrawal/verify_finish.blade.php

@@ -227,6 +227,7 @@
                                     <th>{{ __('auto.提交时间') }}</th>
                                     <th>{{ __('auto.茶叶申请额度') }}</th>
                                     <th>{{ __('auto.实际到账金额') }}</th>
+                                    <th>{{ __('auto.手续费') }}</th>
                                     <th>{{ __('auto.审核状态') }}</th>
                                     <th>{{ __('auto.茶叶完成情况') }}</th>
                                     <th>{{ __('auto.审核人') }}</th>
@@ -248,8 +249,10 @@
                                         {{ __('auto.实际到账:') }}
                                         {{$overUserCount->userCount ?? 0}} {{ __('auto.人') }}&nbsp;&nbsp;
                                         {{$overUserCount->count ?? 0}} {{ __('auto.笔') }}&nbsp;&nbsp;
-                                        {{$overUserCount->WithDraw ?? 0}}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-
+                                        {{$overUserCount->WithDraw ?? 0}}
+                                        @if(!empty($viewAll))
+                                            &nbsp;&nbsp;{{ __('auto.手续费汇总:') }}{{ $totalWithdrawFee ?? 0 }}
+                                        @endif
                                     </h4>
                                 @endif
 
@@ -275,6 +278,7 @@
                                         <td>{{date('Y-m-d H:i:s',strtotime($item->CreateDate))}}</td>
                                         <td>{{$item->actual_arrival}}</td>
                                         <td>{{$item->WithDraw}}</td>
+                                        <td>{{ $item->withdraw_fee_display ?? 0 }}</td>
                                         <td>
                                             {!! $item->States !!}
                                             @if($item->State == 5)

+ 619 - 0
resources/views/admin/account_cookie/index.blade.php

@@ -0,0 +1,619 @@
+@extends('base.base')
+
+@section('base')
+    <div class="main-panel">
+        <div class="content-wrapper">
+            <div class="page-header">
+                <h3 class="page-title">
+                    <span class="page-title-icon bg-gradient-primary text-white mr-2">
+                        <i class="mdi mdi-cookie"></i>
+                    </span>
+                    AccountCookie 分析
+                </h3>
+            </div>
+
+            <div class="row mb-3">
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['total'] }}</h4>
+                            <small>命中记录</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['unique_cookies'] }}</h4>
+                            <small>唯一 Cookie</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['registered_users'] }}</h4>
+                            <small>注册用户</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['paid_users'] }}</h4>
+                            <small>付费用户</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['fbclid_count'] }}</h4>
+                            <small>含 fbclid</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['unique_ips'] }}</h4>
+                            <small>唯一 IP</small>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row mb-3">
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['unique_users'] }}</h4>
+                            <small>唯一 UserID</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['fb_inapp_count'] }}</h4>
+                            <small>Facebook UA</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['ig_inapp_count'] }}</h4>
+                            <small>Instagram UA</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['duplicate_fbclid_groups'] }}</h4>
+                            <small>重复 fbclid 组</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['duplicate_fbclid_rows'] }}</h4>
+                            <small>重复 fbclid 行</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['duplicate_ff_groups'] }}</h4>
+                            <small>重复 FF 组</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card">
+                        <div class="card-body text-center">
+                            <h4>{{ $stats['duplicate_ff_rows'] }}</h4>
+                            <small>重复 FF 行</small>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-2">
+                    <div class="card border-danger">
+                        <div class="card-body text-center">
+                            <h4 class="text-danger">{{ $stats['fbclid_cookie_issues'] }}</h4>
+                            <small>fbclid/Cookie 异常</small>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <form class="form-inline mb-3" method="get" action="">
+                <div class="form-group mr-2">
+                    <label class="mr-1">UserID</label>
+                    <input type="text" name="UserID" class="form-control" value="{{ $filters['user_id'] }}" style="width: 120px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">GameID</label>
+                    <input type="text" name="GameID" class="form-control" value="{{ $filters['game_id'] }}" style="width: 120px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">UrlSign</label>
+                    <input type="text" name="UrlSign" class="form-control" value="{{ implode(',', $filters['url_signs']) }}" placeholder="110,125" style="width: 140px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">账号渠道</label>
+                    <input type="text" name="AccountChannel" class="form-control" value="{{ implode(',', $filters['account_channels']) }}" placeholder="110,125" style="width: 140px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">Platform</label>
+                    <input type="text" name="Platform" class="form-control" value="{{ $filters['platform'] }}" style="width: 100px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">CreateStart</label>
+                    <input type="date" name="date_start" class="form-control" value="{{ $filters['date_start_raw'] }}" style="width: 160px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">CreateEnd</label>
+                    <input type="date" name="date_end" class="form-control" value="{{ $filters['date_end_raw'] }}" style="width: 160px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">RegStart</label>
+                    <input type="date" name="register_start" class="form-control" value="{{ $filters['register_start_raw'] }}" style="width: 160px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">RegEnd</label>
+                    <input type="date" name="register_end" class="form-control" value="{{ $filters['register_end_raw'] }}" style="width: 160px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">fbclid</label>
+                    <select name="has_fbclid" class="form-control">
+                        <option value="1" @if($filters['has_fbclid'] === 1) selected @endif>仅 fbclid</option>
+                        <option value="0" @if($filters['has_fbclid'] === 0) selected @endif>全部</option>
+                        <option value="2" @if($filters['has_fbclid'] === 2) selected @endif>排除 fbclid</option>
+                    </select>
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">Origin</label>
+                    <input type="text" name="Origin" class="form-control" value="{{ $filters['origin'] }}" style="width: 180px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">IP</label>
+                    <input type="text" name="IP" class="form-control" value="{{ $filters['ip'] }}" style="width: 140px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">UA</label>
+                    <input type="text" name="UA" class="form-control" value="{{ $filters['ua'] }}" style="width: 180px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">参数分类</label>
+                    <select name="param_category" class="form-control">
+                        <option value="" @if($filters['param_category'] === '') selected @endif>全部</option>
+                        <option value="channel" @if($filters['param_category'] === 'channel') selected @endif>Channel</option>
+                        <option value="utm" @if($filters['param_category'] === 'utm') selected @endif>UTM</option>
+                        <option value="campaign" @if($filters['param_category'] === 'campaign') selected @endif>Campaign</option>
+                        <option value="attribution" @if($filters['param_category'] === 'attribution') selected @endif>Attribution</option>
+                        <option value="identifier" @if($filters['param_category'] === 'identifier') selected @endif>Identifier</option>
+                    </select>
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">参数名</label>
+                    <input type="text" name="param_key" class="form-control" value="{{ $filters['param_key'] }}" placeholder="utm_campaign" style="width: 140px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">参数值</label>
+                    <input type="text" name="param_value" class="form-control" value="{{ $filters['param_value'] }}" placeholder="120242..." style="width: 180px;">
+                </div>
+                <div class="form-group mr-2">
+                    <label class="mr-1">每页</label>
+                    <select name="page_size" class="form-control">
+                        <option value="100" @if($filters['page_size'] == 100) selected @endif>100</option>
+                        <option value="200" @if($filters['page_size'] == 200) selected @endif>200</option>
+                        <option value="500" @if($filters['page_size'] == 500) selected @endif>500</option>
+                    </select>
+                </div>
+                <button type="submit" class="btn btn-primary">搜索</button>
+                <a href="/admin/account_cookie/list" class="btn btn-warning ml-2">重置</a>
+            </form>
+
+            <div class="row mb-3">
+                <div class="col-md-3">
+                    <div class="card">
+                        <div class="card-header">Platform 分布</div>
+                        <div class="card-body" style="max-height: 220px; overflow-y: auto;">
+                            <table class="table table-sm">
+                                @foreach(array_slice($stats['platforms'], 0, 10, true) as $key => $count)
+                                    <tr><td>{{ $key }}</td><td class="text-right">{{ $count }}</td></tr>
+                                @endforeach
+                            </table>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="card">
+                        <div class="card-header">UrlSign 分布</div>
+                        <div class="card-body" style="max-height: 220px; overflow-y: auto;">
+                            <table class="table table-sm">
+                                @foreach(array_slice($stats['url_signs'], 0, 10, true) as $key => $count)
+                                    <tr><td>{{ $key }}</td><td class="text-right">{{ $count }}</td></tr>
+                                @endforeach
+                            </table>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="card">
+                        <div class="card-header">utm_source 分布</div>
+                        <div class="card-body" style="max-height: 220px; overflow-y: auto;">
+                            <table class="table table-sm">
+                                @foreach(array_slice($stats['utm_sources'], 0, 10, true) as $key => $count)
+                                    <tr><td>{{ $key }}</td><td class="text-right">{{ $count }}</td></tr>
+                                @endforeach
+                            </table>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-3">
+                    <div class="card">
+                        <div class="card-header">Origin 分布</div>
+                        <div class="card-body" style="max-height: 220px; overflow-y: auto;">
+                            <table class="table table-sm">
+                                @foreach(array_slice($stats['origins'], 0, 10, true) as $key => $count)
+                                    <tr><td style="word-break: break-all;">{{ $key }}</td><td class="text-right">{{ $count }}</td></tr>
+                                @endforeach
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row mb-3">
+                <div class="col-md-4">
+                    <div class="card">
+                        <div class="card-header">投放参数分类</div>
+                        <div class="card-body" style="max-height: 240px; overflow-y: auto;">
+                            <table class="table table-sm">
+                                @forelse(array_slice($stats['param_categories'], 0, 12, true) as $key => $count)
+                                    <tr><td>{{ $key }}</td><td class="text-right">{{ $count }}</td></tr>
+                                @empty
+                                    <tr><td colspan="2" class="text-center">暂无数据</td></tr>
+                                @endforelse
+                            </table>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-4">
+                    <div class="card">
+                        <div class="card-header">投放参数 Key Top</div>
+                        <div class="card-body" style="max-height: 240px; overflow-y: auto;">
+                            <table class="table table-sm">
+                                @forelse(array_slice($stats['param_keys'], 0, 12, true) as $key => $count)
+                                    <tr><td>{{ $key }}</td><td class="text-right">{{ $count }}</td></tr>
+                                @empty
+                                    <tr><td colspan="2" class="text-center">暂无数据</td></tr>
+                                @endforelse
+                            </table>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-4">
+                    <div class="card">
+                        <div class="card-header">识别重点字段</div>
+                        <div class="card-body" style="max-height: 240px; overflow-y: auto; font-size: 13px;">
+                            <div>支持动态提取 Params 中的全部字段</div>
+                            <div>自动归类为 Channel / UTM / Campaign / Attribution / Identifier / Custom</div>
+                            <div style="margin-top: 6px;">可按分类、参数名、参数值组合筛选</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row mb-3">
+                <div class="col-md-6">
+                    <div class="card">
+                        <div class="card-header">重复 fbclid 分组</div>
+                        <div class="card-body" style="max-height: 220px; overflow-y: auto;">
+                            <table class="table table-sm">
+                                <thead>
+                                <tr>
+                                    <th>组</th>
+                                    <th>颜色</th>
+                                    <th>数量</th>
+                                    <th>fbclid</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                @forelse(array_slice($fbclidGroups, 0, 20) as $group)
+                                    <tr>
+                                        <td>#{{ $group['index'] }}</td>
+                                        <td>
+                                            <span style="display:inline-block;width:18px;height:18px;border-radius:3px;border:1px solid #ccc;background:{{ $group['color'] }};"></span>
+                                        </td>
+                                        <td>{{ $group['count'] }}</td>
+                                        <td style="word-break: break-all;">{{ $group['value'] }}</td>
+                                    </tr>
+                                @empty
+                                    <tr>
+                                        <td colspan="4" class="text-center">当前筛选结果没有重复 fbclid。</td>
+                                    </tr>
+                                @endforelse
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-6">
+                    <div class="card">
+                        <div class="card-header">重复 FF 分组</div>
+                        <div class="card-body" style="max-height: 220px; overflow-y: auto;">
+                            <table class="table table-sm">
+                                <thead>
+                                <tr>
+                                    <th>组</th>
+                                    <th>颜色</th>
+                                    <th>数量</th>
+                                    <th>FF</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                @forelse(array_slice($ffGroups, 0, 20) as $group)
+                                    <tr>
+                                        <td>#{{ $group['index'] }}</td>
+                                        <td>
+                                            <span style="display:inline-block;width:18px;height:18px;border-radius:3px;border:1px solid #ccc;background:{{ $group['color'] }};"></span>
+                                        </td>
+                                        <td>{{ $group['count'] }}</td>
+                                        <td style="word-break: break-all;">{{ $group['value'] }}</td>
+                                    </tr>
+                                @empty
+                                    <tr>
+                                        <td colspan="4" class="text-center">当前筛选结果没有重复 FF。</td>
+                                    </tr>
+                                @endforelse
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row mb-3">
+                @forelse($stats['param_category_stats'] as $category => $categoryStats)
+                    <div class="col-md-4">
+                        <div class="card">
+                            <div class="card-header">{{ $categoryStats['label'] }} 详情</div>
+                            <div class="card-body" style="max-height: 260px; overflow-y: auto;">
+                                <div class="mb-2"><strong>Top Keys</strong></div>
+                                <table class="table table-sm mb-3">
+                                    @foreach(array_slice($categoryStats['keys'], 0, 6, true) as $key => $count)
+                                        <tr><td>{{ $key }}</td><td class="text-right">{{ $count }}</td></tr>
+                                    @endforeach
+                                </table>
+                                <div class="mb-2"><strong>Top Values</strong></div>
+                                <table class="table table-sm">
+                                    @foreach(array_slice($categoryStats['values'], 0, 6, true) as $value => $count)
+                                        <tr><td style="word-break: break-all;">{{ $value }}</td><td class="text-right">{{ $count }}</td></tr>
+                                    @endforeach
+                                </table>
+                            </div>
+                        </div>
+                    </div>
+                @empty
+                    <div class="col-md-12">
+                        <div class="alert alert-light border">当前筛选下没有可分析的投放参数。</div>
+                    </div>
+                @endforelse
+            </div>
+
+            <div class="card">
+                <div class="card-header">注册列表 (共 {{ $list->total() }} 条)</div>
+                <div class="card-body table-responsive">
+                    @php
+                        $formatTime = function ($value) {
+                            if (empty($value)) {
+                                return '-';
+                            }
+        
+                            return preg_replace('/^\d{4}-\d{2}-/', '', preg_replace('/\.\d+$/', '', str_replace(' ', "\n", $value)));
+                        };
+                        $formatShort = function ($value, $length = 30) {
+                            $value = (string)$value;
+                            if ($value === '') {
+                                return '-';
+                            }
+                            return mb_strlen($value) > $length ? mb_substr($value, 0, $length) . '...' : $value;
+                        };
+                        $paramLabels = [
+                            'channel' => 'Channel',
+                            'utm' => 'UTM',
+                            'campaign' => 'Campaign',
+                            'attribution' => 'Attribution',
+                            'identifier' => 'Identifier',
+                            'custom' => 'Custom',
+                        ];
+                    @endphp
+                    <table class="table table-bordered table-sm">
+                        <thead>
+                        <tr>
+                            <th>UserID</th>
+                            <th>GameID</th>
+                            <th>账号渠道</th>
+                            <th>UrlSign</th>
+                            <th>CreateTime</th>
+                            <th>RegisterDate</th>
+                            <th>Platform</th>
+                            <th>付费</th>
+                            <th>上报状态</th>
+                            <th>UA分析</th>
+                            <th>IP / Locale</th>
+                            <th>Origin</th>
+                            <th>投放参数</th>
+                            <th>Cookie</th>
+                        </tr>
+                        </thead>
+                        <tbody>
+                        @forelse($list as $row)
+                            @php
+                                $paramDialogSections = [];
+                                foreach (($row->ParamAnalysis['categories'] ?? []) as $category => $entries) {
+                                    $lines = [($paramLabels[$category] ?? ucfirst($category)) . ':'];
+                                    foreach ($entries as $entry) {
+                                        $lines[] = $entry['key'] . '=' . $entry['value'];
+                                    }
+                                    $paramDialogSections[] = implode("\n", $lines);
+                                }
+                                $paramDialogText = implode("\n\n", $paramDialogSections);
+                            @endphp
+                            @php
+                                $rowStyle = '';
+                                if (!empty($row->FFGroup)) {
+                                    $rowStyle = 'background-color: ' . $row->FFGroup['color'] . ';';
+                                } elseif (!empty($row->FbclidGroup)) {
+                                    $rowStyle = 'background-color: ' . $row->FbclidGroup['color'] . ';';
+                                }
+                                if (!empty($row->FbclidGroup)) {
+                                    $rowStyle .= 'border-left: 4px solid #856404;';
+                                }
+                                if (!($row->FbclidCookieCheck['ok'] ?? true)) {
+                                    $rowStyle .= 'border-right: 4px solid #dc3545;';
+                                }
+                            @endphp
+                            <tr @if($rowStyle !== '') style="{{ $rowStyle }}" @endif>
+                                <td>{{ $row->UserID }}</td>
+                                <td>{{ $row->GameID }}</td>
+                                <td>{{ $row->AccountChannel }}</td>
+                                <td>{{ $row->UrlSign }}</td>
+                                <td style="white-space: nowrap;">{{ $formatTime($row->CreateTime) }}</td>
+                                <td style="white-space: nowrap;">{{ $formatTime($row->RegisterDate) }}</td>
+                                <td>{{ $row->Platform }}</td>
+                                <td>
+                                    <div>单数: {{ $row->PayOrderCount }}</div>
+                                    <div>金额: {{ $row->PayAmountDisplay }}</div>
+                                    <div style="white-space: nowrap;">{{ $formatTime($row->LastPayAt) }}</div>
+                                </td>
+                                <td style="min-width: 150px;">
+                                    <div>{{ $row->AdjustStatus }}</div>
+                                    @if(!empty($row->AdjustLogs))
+                                        <div style="margin-top: 6px; font-size: 12px; color: #666; word-break: break-all;">
+                                            {{ $formatShort($row->AdjustLogs[0] ?? '', 24) }}
+                                        </div>
+                                        <a href="javascript:void(0);" onclick='showTextDialog("上报日志 - UserID {{ $row->UserID }}", @json(implode("\n\n", $row->AdjustLogs)))'>查看详情</a>
+                                    @endif
+                                </td>
+                                <td>
+                                    <div>{{ $row->UaApp }}</div>
+                                    <div>{{ $row->UaOs }}</div>
+                                    <div>{{ $row->UaDevice }}</div>
+                                </td>
+                                <td>
+                                    <div>{{ $row->IP }}</div>
+                                    <div>{{ $row->Locale }}</div>
+                                    @if(!empty($row->IP))
+                                        <a href="javascript:void(0);" onclick='lookupIpLocation(this, @json($row->IP))'>查地址</a>
+                                        <div class="ip-location-result" style="margin-top: 4px; font-size: 12px; color: #666; word-break: break-all;"></div>
+                                    @endif
+                                </td>
+                                <td style="max-width: 220px; word-break: break-all;">{{ $row->Origin }}</td>
+                                <td style="min-width: 220px;">
+                                    <div>fbclid: {{ $row->HasFbclid ? 'Y' : 'N' }}</div>
+                                    @if(!empty($row->FbclidGroup))
+                                        <div>
+                                            <span class="badge badge-dark">组 #{{ $row->FbclidGroup['index'] }}</span>
+                                            <span class="badge badge-light">{{ $row->FbclidGroup['count'] }} 条</span>
+                                        </div>
+                                        <div style="word-break: break-all;">{{ $formatShort($row->FbclidValue, 28) }}</div>
+                                    @endif
+                                    @if(!($row->FbclidCookieCheck['ok'] ?? true))
+                                        <div style="margin-top: 4px;">
+                                            <span class="badge badge-danger">{{ $row->FbclidCookieCheck['status'] }}</span>
+                                        </div>
+                                        <div class="text-danger" style="word-break: break-all;">
+                                            {{ $row->FbclidCookieCheck['message'] }}
+                                        </div>
+                                    @endif
+                                    @if(!empty($row->FFGroup))
+                                        <div style="margin-top: 4px;">
+                                            <span class="badge badge-success">FF组 #{{ $row->FFGroup['index'] }}</span>
+                                            <span class="badge badge-light">{{ $row->FFGroup['count'] }} 条</span>
+                                        </div>
+                                        <div style="word-break: break-all;">FF: {{ $formatShort($row->FF, 28) }}</div>
+                                    @endif
+                                    <div>channel: {{ $row->ParamChannel ?: '-' }}</div>
+                                    <div>campaign: {{ $formatShort($row->ParamCampaign ?: '-', 24) }}</div>
+                                    <div>adgroup: {{ $formatShort($row->ParamAdgroup ?: '-', 24) }}</div>
+                                    <div>creative: {{ $formatShort($row->ParamCreative ?: '-', 24) }}</div>
+                                    <div>utm_source: {{ $row->UtmSource ?: '-' }}</div>
+                                    <div>utm_medium: {{ $row->UtmMedium ?: '-' }}</div>
+                                    <div>utm_campaign: {{ $formatShort($row->UtmCampaign ?: '-', 24) }}</div>
+                                    <div>pixel: {{ $row->Pixel ?: '-' }}</div>
+                                    <div>FPID: {{ $row->FPID ?: '-' }}</div>
+                                    <div>FF: {{ $row->FF ?: '-' }}</div>
+                                    @if($paramDialogText !== '')
+                                        <div style="margin-top: 6px;">
+                                            <a href="javascript:void(0);" onclick='showTextDialog("投放参数详情 - UserID {{ $row->UserID }}", @json($paramDialogText))'>查看参数分类</a>
+                                        </div>
+                                    @endif
+                                </td>
+                                <td style="min-width: 220px;">
+                                    <div>fbp: <span style="word-break: break-all;">{{ $formatShort($row->Fbp, 24) }}</span></div>
+                                    <div>fbc: <span style="word-break: break-all;">{{ $formatShort($row->Fbc, 24) }}</span></div>
+                                    @if(!empty($row->FbclidCookieCheck['cookie_fbclid']))
+                                        <div>cookie_fbclid: <span style="word-break: break-all;">{{ $formatShort($row->FbclidCookieCheck['cookie_fbclid'], 24) }}</span></div>
+                                    @endif
+                                    <a href="javascript:void(0);" onclick='showTextDialog("Cookie详情 - UserID {{ $row->UserID }}", @json("fbp: " . ($row->Fbp ?: "-") . "\n\nfbc: " . ($row->Fbc ?: "-") . "\n\nUA: " . ($row->ClickUA ?: $row->GameUA ?: "-")))'>查看详情</a>
+                                </td>
+                            </tr>
+                        @empty
+                            <tr>
+                                <td colspan="14" class="text-center">暂无数据</td>
+                            </tr>
+                        @endforelse
+                        </tbody>
+                    </table>
+                    {!! $list->links() !!}
+                </div>
+            </div>
+        </div>
+    </div>
+    <script>
+        function showTextDialog(title, text) {
+            layer.open({
+                type: 1,
+                title: title,
+                shadeClose: true,
+                shade: 0.4,
+                area: ['720px', '420px'],
+                content: '<div style="padding:16px;max-height:360px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;">' + $('<div>').text(text).html() + '</div>'
+            });
+        }
+
+        var ipLookupCache = {};
+
+        function lookupIpLocation(element, ip) {
+            var $trigger = $(element);
+            var $result = $trigger.siblings('.ip-location-result');
+            if (!ip) {
+                return;
+            }
+
+            if (ipLookupCache[ip]) {
+                $result.text(ipLookupCache[ip]);
+                return;
+            }
+
+            $result.text('查询中...');
+            $.getJSON('https://ipapi.co/' + encodeURIComponent(ip) + '/json/', function (res) {
+                var text = '-';
+                if (res && !res.error) {
+                    text = [res.country_name, res.region, res.city, res.org].filter(Boolean).join(' / ');
+                }
+                ipLookupCache[ip] = text;
+                $result.text(text);
+            }).fail(function () {
+                $result.text('查询失败');
+            });
+        }
+    </script>
+@endsection

+ 53 - 0
resources/views/admin/extension_new/subordinate.blade.php

@@ -0,0 +1,53 @@
+@extends('base.base')
+@section('base')
+    <div class="container-fluid">
+        <div class="row">
+            <div class="col-12">
+                <div class="card">
+                    <div class="card-header">
+                        <h3 class="card-title">用户下级查询</h3>
+                    </div>
+                    <div class="card-body">
+                        <form class="form-inline mb-4" method="get" action="">
+                            <div class="form-group mx-sm-3 mb-2">
+                                <label for="UserID" class="mr-2">会员ID:</label>
+                                <input type="text" class="form-control" name="GameID" id="GameID" value="{{ isset($request) ? $request->input('GameID') : '' }}">
+                            </div>
+                            <button type="submit" class="btn btn-primary mb-2 mr-2">搜索</button>
+                            <a href="{{ route('admin.extension_new.bind_list') }}" class="btn btn-warning mb-2">清空</a>
+                        </form>
+
+                        <div class="table-responsive">
+                            <table class="table table-bordered">
+                                <thead>
+                                <tr>
+                                    <th>会员ID</th>
+                                    <th>注册时间</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                @foreach($list??[] as $v)
+                                    <tr>
+                                        <td>
+                                            <a href="/admin/global/id_find?UserID={{$v->UserID}}">
+                                                {{$v->GameID}}
+                                            </a>
+                                        </td>
+                                        <td>{{$v->RegisterDate??""}}</td>
+                                    </tr>
+                                @endforeach
+                                </tbody>
+                            </table>
+                        </div>
+                        <div class="card-footer clearfix">
+                            @if(isset($list))
+                            总共 <b>{{ $list->appends($request->all())->total() }}</b> 条,分为<b>{{ $list->lastPage() }}</b>页
+                            {{ $list->links() }}
+                            @endif
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+@endsection

+ 1 - 0
resources/views/admin/game_data/useronline.blade.php

@@ -53,6 +53,7 @@
 
                             <button onclick="update(5)" class="btn-sm btn btn-gradient-dark">{{ __('auto.充减提') }}</button>
                             @endif
+                            <a href="{{ url('/admin/stock-mode#snapshot-tab') }}" class="btn-sm btn btn-gradient-info" style="margin-left: 10px;">库存快照历史</a>
                             <div id="main" style="width: 100%;height:600px;"></div>
 
                         </div>

+ 10 - 26
resources/views/admin/global/id_list.blade.php

@@ -78,7 +78,7 @@
                             </tr>
                             <tr>
                                 <td>{{ __('auto.会员') }}ID</td>
-                                <td>{{$userInfo->GameID}}</td>
+                                <td>{{$userInfo->GameID}} @if($data['refund_flag']) <b style="color:red;">退款({{round($data['refund_total']/100, 2)}})</b> @endif</td>
                             </tr>
 
                             <tr>
@@ -138,12 +138,12 @@
 
                             <tr>
                                 <td>{{ __('auto.最高分') }}</td>
-                                <td>{{ $userInfo->gameScoreInfo->MaxScore/100 }}</td>
+                                <td>{{ $userInfo->gameScoreInfo->MaxScore/100 }} / {{ $data['todayMaxScore'] }}</td>
                             </tr>
 
                             <tr>
                                 <td>{{ __('auto.最多赢分') }}</td>
-                                <td>{{ $userInfo->gameScoreInfo->MaxWinScore/100 }}</td>
+                                <td>{{ $userInfo->gameScoreInfo->MaxWinScore/100 }} / {{ $data['todayMaxWinScore'] }}</td>
                             </tr>
 
                             <tr>
@@ -222,7 +222,7 @@
                             <tr>
                                 <td>{{ __('auto.手机型号') }}</td>
                                 <td>
-                                    {{ __('auto.安卓') }}
+                                    {{ $data['mobileBand'] }}
                                 </td>
                             </tr>
                             <tr>
@@ -326,31 +326,15 @@
                                 <th>{{ __('auto.对局数') }}</th>
                             </tr>
                             @foreach($gameCount as $key => $value)
-                            <tr>
-                                <td>{{ $key }}</td>
-                                <td>{{ $value }} </td>
-                            </tr>
-                            @endforeach
-                        </table>
-
-                        <table class="table table-bordered" style="margin-left:5px;width: 20%;float: left">
-
-                            <tr>
-                                <td width="5%" colspan="2">三方数据信息</td>
-                            </tr>
-                            <tr>
-                                <th>平台</th>
-                                <th>输赢(当天/总)</th>
-                            </tr>
-                            @foreach($platformData as $key => $value)
-                                <tr>
-                                    <td>{{ $key }}</td>
-                                    <td>{{ $value['today'] }} / {{ $value['total'] }}</td>
-                                </tr>
+                                @if($value > 0)
+                                    <tr>
+                                        <td>{{ $key }}</td>
+                                        <td>{{ $value }}</td>
+                                    </tr>
+                                @endif
                             @endforeach
                         </table>
 
-
                     </div>
 
                     <div>{{ __('auto.用户控制:') }}

+ 76 - 0
resources/views/admin/protect_level/add.blade.php

@@ -0,0 +1,76 @@
+@extends('base.base')
+@section('base')
+<div class="main-panel">
+    <div class="content-wrapper">
+        <form method="post" class="form-ajax" action="{{ url()->current() }}">
+            @csrf
+            <div class="form-group">
+                <label>ID</label>
+                <input name="ID" class="form-control" value="{{ $info->ID ?? '' }}" {{ isset($info) ? 'readonly' : '' }}>
+            </div>
+            <div class="form-group">
+                <label>充值金额 Recharge</label>
+                <input name="Recharge" class="form-control" value="{{ $info->Recharge ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>救济金 GrantNum</label>
+                <input name="GrantNum" class="form-control" value="{{ isset($info) ? round($info->GrantNum / \App\Http\helper\NumConfig::NUM_VALUE, 2) : '' }}">
+            </div>
+            <div class="form-group">
+                <label>VIP 等级 VIP</label>
+                <input name="VIP" class="form-control" value="{{ $info->VIP ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>升级奖励 LevelUpBonus</label>
+                <input name="LevelUpBonus" class="form-control" value="{{ $info->LevelUpBonus ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>最小充值 MinRecharge</label>
+                <input name="MinRecharge" class="form-control" value="{{ $info->MinRecharge ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>提现限额 WithdrawLimit</label>
+                <input name="WithdrawLimit" class="form-control" value="{{ $info->WithdrawLimit ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>茶叶次数 DailyWithdraws</label>
+                <input name="DailyWithdraws" class="form-control" value="{{ $info->DailyWithdraws ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>提现费率 WithdrawFeeRate</label>
+                <div class="input-group">
+                    <input name="WithdrawFeeRate" class="form-control" value="{{ $info->WithdrawFeeRate ?? '' }}">
+                    <div class="input-group-append"><span class="input-group-text">%</span></div>
+                </div>
+            </div>
+            <div class="form-group">
+                <label>签到系数 SignAlpha</label>
+                <input name="SignAlpha" class="form-control" value="{{ $info->SignAlpha ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>客服类型 CustomServiceType</label>
+                <select name="CustomServiceType" class="form-control">
+                    <option value="1" {{ (isset($info) && $info->CustomServiceType==1) ? 'selected' : '' }}>普通客服</option>
+                    <option value="2" {{ (isset($info) && $info->CustomServiceType==2) ? 'selected' : '' }}>一对一客服</option>
+                </select>
+            </div>
+            <div class="form-group">
+                <label>充值额外赠送 RechargeExtraSendRate</label>
+                <div class="input-group">
+                    <input name="RechargeExtraSendRate" class="form-control" value="{{ $info->RechargeExtraSendRate ?? '' }}">
+                    <div class="input-group-append"><span class="input-group-text">%</span></div>
+                </div>
+            </div>
+            <div class="form-group">
+                <label>SuperballNum</label>
+                <input name="SuperballNum" class="form-control" value="{{ $info->SuperballNum ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>生日礼价值 BirthdayValue</label>
+                <input name="BirthdayValue" class="form-control" value="{{ ($info->BirthdayValue ?? '')/100 }}">
+            </div>
+            <button type="submit" class="btn btn-primary">{{ isset($info) ? '保存' : '添加' }}</button>
+        </form>
+    </div>
+</div>
+@endsection

+ 76 - 0
resources/views/admin/protect_level/edit.blade.php

@@ -0,0 +1,76 @@
+@extends('base.base')
+@section('base')
+<div class="main-panel">
+    <div class="content-wrapper">
+        <form method="post" class="form-ajax" action="{{ url()->current() }}">
+            @csrf
+            <div class="form-group">
+                <label>ID</label>
+                <input name="ID" class="form-control" value="{{ $info->ID ?? '' }}" readonly>
+            </div>
+            <div class="form-group">
+                <label>充值金额 Recharge</label>
+                <input name="Recharge" class="form-control" value="{{ $info->Recharge ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>救济金 GrantNum</label>
+                <input name="GrantNum" class="form-control" value="{{ round($info->GrantNum / \App\Http\helper\NumConfig::NUM_VALUE, 2) }}">
+            </div>
+            <div class="form-group">
+                <label>VIP 等级 VIP</label>
+                <input name="VIP" class="form-control" value="{{ $info->VIP ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>升级奖励 LevelUpBonus</label>
+                <input name="LevelUpBonus" class="form-control" value="{{ $info->LevelUpBonus ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>最小充值 MinRecharge</label>
+                <input name="MinRecharge" class="form-control" value="{{ $info->MinRecharge ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>提现限额 WithdrawLimit</label>
+                <input name="WithdrawLimit" class="form-control" value="{{ $info->WithdrawLimit ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>茶叶次数 DailyWithdraws</label>
+                <input name="DailyWithdraws" class="form-control" value="{{ $info->DailyWithdraws ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>提现费率 WithdrawFeeRate</label>
+                <div class="input-group">
+                    <input name="WithdrawFeeRate" class="form-control" value="{{ $info->WithdrawFeeRate ?? '' }}">
+                    <div class="input-group-append"><span class="input-group-text">%</span></div>
+                </div>
+            </div>
+            <div class="form-group">
+                <label>签到系数 SignAlpha</label>
+                <input name="SignAlpha" class="form-control" value="{{ $info->SignAlpha ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>客服类型 CustomServiceType</label>
+                <select name="CustomServiceType" class="form-control">
+                    <option value="1" {{ ($info->CustomServiceType==1) ? 'selected' : '' }}>普通客服</option>
+                    <option value="2" {{ ($info->CustomServiceType==2) ? 'selected' : '' }}>一对一客服</option>
+                </select>
+            </div>
+            <div class="form-group">
+                <label>充值额外赠送 RechargeExtraSendRate</label>
+                <div class="input-group">
+                    <input name="RechargeExtraSendRate" class="form-control" value="{{ $info->RechargeExtraSendRate ?? '' }}">
+                    <div class="input-group-append"><span class="input-group-text">%</span></div>
+                </div>
+            </div>
+            <div class="form-group">
+                <label>SuperballNum</label>
+                <input name="SuperballNum" class="form-control" value="{{ $info->SuperballNum ?? '' }}">
+            </div>
+            <div class="form-group">
+                <label>生日礼价值 BirthdayValue</label>
+                <input name="BirthdayValue" class="form-control" value="{{ ($info->BirthdayValue ?? '')/100 }}">
+            </div>
+            <button type="submit" class="btn btn-primary">保存</button>
+        </form>
+    </div>
+</div>
+@endsection

+ 81 - 0
resources/views/admin/protect_level/index.blade.php

@@ -0,0 +1,81 @@
+@extends('base.base')
+@section('base')
+<div class="main-panel">
+    <div class="content-wrapper">
+        <div class="page-header">
+            <h3 class="page-title">{{ __('VIP 等级配置') }}</h3>
+        </div>
+
+        <div class="card">
+            <div class="card-body">
+                <button class="btn btn-sm btn-success mb-2" onclick="add()">新增等级</button>
+                <table class="table table-bordered">
+                    <thead>
+                    <tr>
+                        <th>ID</th>
+                        <th>充值金额</th>
+                        <th>救济金金额</th>
+                        <th>VIP等级</th>
+                        <th>最小充值</th>
+                        <th>提现限额</th>
+                        <th>茶叶次数</th>
+                        <th>提现费率百分比</th>
+                        <th>签到系数</th>
+                        <th>客服类型 1 普通 2 一对一</th>
+                        <th>商城充值额外赠送百分比</th>
+                        <th>Superball每日赠送球数</th>
+                        <th>生日礼价值</th>
+                        <th>操作</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    @foreach($list as $row)
+                        <tr>
+                            <td>{{ $row->ID }}</td>
+                            <td>{{ $row->Recharge }}</td>
+                            <td>{{ round($row->GrantNum/\App\Http\helper\NumConfig::NUM_VALUE, 2) }}</td>
+                            <td>{{ $row->VIP }}</td>
+                            <td>{{ $row->MinRecharge }}</td>
+                            <td>{{ $row->WithdrawLimit }}</td>
+                            <td>{{ $row->DailyWithdraws }}</td>
+                            <td>{{ $row->WithdrawFeeRate }} %</td>
+                            <td>{{ $row->SignAlpha }}</td>
+                            <td>{{ $row->CustomServiceType }}</td>
+                            <td>{{ $row->RechargeExtraSendRate }} %</td>
+                            <td>{{ $row->SuperballNum }}</td>
+                            <td>{{ intval($row->BirthdayValue/100) }}</td>
+                            <td>
+                                <button class="btn btn-sm btn-primary" onclick="edit({{ $row->ID }})">{{ __('修改') }}</button>
+                                <button class="btn btn-sm btn-danger" onclick="del({{ $row->ID }})">{{ __('删除') }}</button>
+                            </td>
+                        </tr>
+                    @endforeach
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+
+<script>
+    function add(){
+        layer.open({
+            type:2,shade:0.8,area:['50%','70%'],
+            content:'/admin/protect-level/add'
+        });
+    }
+    function edit(id){
+        layer.open({
+            type:2,shade:0.8,area:['50%','70%'],
+            content:'/admin/protect-level/edit/'+id
+        });
+    }
+    function del(id){
+        myConfirm('确认删除?', function(){
+            myRequest('/admin/protect-level/delete/'+id,'post',{},function(){
+                layer.msg('已删除'); location.reload();
+            });
+        });
+    }
+</script>
+@endsection

+ 8 - 3
resources/views/admin/recharge/list.blade.php

@@ -175,6 +175,7 @@
                                             </svg>
                                         </div>
                                     </th>
+                                    <th width="6%">{{ __('auto.手续费') }}</th>
                                     <th width="8%">{{ __('auto.变化后余额') }}</th>
                                     <th width="8%">{{ __('auto.当前余额') }}</th>
 
@@ -203,6 +204,9 @@
                                         @endif
                                         <h4>{{ __('auto.总金额:') }}{{$totalMoney}} &nbsp;&nbsp; {{$payTotalMoney->count_u ?? 0}}{{ __('auto.人') }}&nbsp;&nbsp;{{$payTotalMoney->count_id ?? 0}}{{ __('auto.笔') }}</h4>
                                         <h4>{{ __('auto.已到账:') }}{{$overMoney}} &nbsp;&nbsp;{{$payOverMoney->count_u ?? 0}}{{ __('auto.人') }}&nbsp;&nbsp;{{$payOverMoney->count_id ?? 0}}{{ __('auto.笔') }}</h4>
+                                        @if(!empty($viewAll))
+                                            <h4>{{ __('auto.手续费汇总:') }}{{ $totalPaymentFee ?? 0 }}</h4>
+                                        @endif
                                     </div>
                                 @endif
 
@@ -213,6 +217,7 @@
                                         <td>{{$v->finished_at}}</td>
                                         <td>{{$v->finished_at ? dateConvert($v->finished_at) : ''}}</td>
                                         <td>{{ $v->amount }}</td>
+                                        <td>{{ $v->payment_fee ?? 0 }}</td>
                                         <td>{{ $v->after_amount }}</td>
                                         <td>{{ $v->score }}</td>
 
@@ -249,17 +254,17 @@
                                         <td>{{$v->created_at}}</td>
                                         <td>{{ dateConvert($v->created_at) }}</td>
                                         <td>
-                                            @if (in_array(session('admin')->roles[0]->id,[1,12])&&$v->pay_status != 1)
+                                            @if (in_array(session('admin')->roles[0]->id,[1,12]) && $v->pay_status != 1 && $v->pay_status != 9)
                                                 <button type="button" class="btn-sm btn-primary"
                                                         onclick="supplement({{$v->id}})">{{ __('auto.补单') }}
                                                 </button>
                                             @endif
-                                            @if (in_array(session('admin')->roles[0]->id,[1,12])&&$v->pay_status == 1)
+                                            @if (in_array(session('admin')->roles[0]->id,[1,12]) && $v->pay_status == 1)
                                                 <button type="button" class="btn-sm btn-warning"
                                                         onclick="setRefund({{$v->id}})">{{ __('auto.标记退款') }}
                                                 </button>
                                             @endif
-                                            @if (in_array(session('admin')->roles[0]->id,[1,12]) && $v->pay_status != 1)
+                                            @if (in_array(session('admin')->roles[0]->id,[1,12]) && $v->pay_status != 1 && $v->pay_status != 9)
                                                 <button type="button" class="btn-sm btn-info"
                                                         onclick="mockFbReport({{$v->id}})">FB模拟上报
                                                 </button>

+ 14 - 4
resources/views/admin/stock_mode/index.blade.php

@@ -195,7 +195,9 @@
                                                 <th>当前库存 (Stock)</th>
                                                 <th>基数 (LevelBase)</th>
                                                 <th>累计税收 (Revenue)</th>
-                                                <th>累计暗税 (RevenueD)</th>
+                                                <th>今日税收</th>
+                                                <th>今日rtp</th>
+                                                <th>中奖率</th>
                                                 <th width="120">操作</th>
                                             </tr>
                                         </thead>
@@ -207,6 +209,9 @@
                                                     $levelBase = ($roomStock->LevelBase ?? 10000) / 100;
                                                     $revenue = ($roomStock->Revenue ?? 0) / 100;
                                                     $revenueD = ($roomStock->RevenueD ?? 0) / 100;
+                                                    $todayRevenue = ($rst2[$sortId]->todayRevenue ?? 0);
+                                                    $todayRtp = ($rst2[$sortId]->todayRtp ?? 0);
+                                                    $todayWinRatio = ($rst2[$sortId]->todayWinRatio ?? 0) * 100;
                                                 @endphp
                                                 <tr>
                                                     <td>
@@ -241,9 +246,9 @@
                                                     <td class="text-right text-muted">
                                                         {{ number_format($revenue, 2) }}
                                                     </td>
-                                                    <td class="text-right text-muted">
-                                                        {{ number_format($revenueD, 2) }}
-                                                    </td>
+                                                    <td class="text-right text-muted">{{ $todayRevenue }}</td>
+                                                    <td class="text-right text-muted">{{ $todayRtp*100 }} %</td>
+                                                    <td class="text-right text-muted">{{ $todayWinRatio }} %</td>
                                                     <td class="text-center">
                                                         <button class="btn btn-sm btn-primary save-room-stock" data-sort-id="{{ $sortId }}">
                                                             <i class="mdi mdi-content-save"></i> 保存
@@ -479,6 +484,11 @@
 
 <script>
 $(function() {
+    // 如果通过哈希访问快照历史,自动切换到对应标签页
+    if (window.location.hash === '#snapshot-tab') {
+        $('a[href="#snapshot-tab"]').tab('show');
+    }
+
     // 保存系统配置
     $('#save-system-config').click(function() {
         const $btn = $(this);

+ 3 - 3
resources/views/admin/superball/prizes.blade.php

@@ -25,8 +25,8 @@
                                 <div>
                                     <span style="padding-left: 10px">结算日期:</span>
                                     <input type="date" name="date" class="form-control" value="{{ $date ?? '' }}" />
-                                    <span style="padding-left: 10px">用户ID:</span>
-                                    <input type="text" name="user_id" class="form-control" style="width: 100px;" value="{{ $userId ?: '' }}" placeholder="UserID" />
+                                    <span style="padding-left: 10px">会员ID:</span>
+                                    <input type="text" name="game_id" class="form-control" style="width: 100px;" value="{{ $gameId ?: '' }}" placeholder="GameID" />
                                 </div>
                                 <div style="margin-top: 10px;">
                                     <input type="submit" class="btn btn-sm btn-gradient-dark btn-icon-text" value="查询"/>&nbsp;&nbsp;
@@ -67,7 +67,7 @@
                             </table>
                             <div class="box-footer clearfix">
                                 共 <b>{{ $list->total() }}</b> 条,共 <b>{{ $list->lastPage() }}</b> 页
-                                {!! $list->appends(['date' => $date, 'user_id' => $userId])->links() !!}
+                                {!! $list->appends(['date' => $date, 'game_id' => $gameId])->links() !!}
                             </div>
                         </div>
                     </div>

+ 4 - 0
resources/views/admin/web_channel_config/add.blade.php

@@ -96,6 +96,10 @@
                                     <label>PlatformID (pixel id)</label>
                                     <input type="text" class="form-control" name="PlatformID">
                                 </div>
+                                <div class="form-group">
+                                    <label>PlatformToken (Conversions API Token)</label>
+                                    <textarea class="form-control" name="PlatformToken" rows="3" placeholder="长文本 Token,保存在 txt 字段中"></textarea>
+                                </div>
                                 <div class="form-group">
                                     <label>BonusArr (REG | MOBILE | EMAIL | PWA)</label>
                                     <div class="row">

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

@@ -107,6 +107,10 @@
                                     <label>PlatformID (eg:pixelid)</label>
                                     <input type="text" class="form-control" name="PlatformID" value="{{$info->PlatformID}}">
                                 </div>
+                                <div class="form-group">
+                                    <label>PlatformToken (Conversions API Token)</label>
+                                    <textarea class="form-control" name="PlatformToken" rows="3" placeholder="长文本 Token,保存在 txt 字段中">{{$info->PlatformToken}}</textarea>
+                                </div>
                                 <div class="form-group">
                                     <label>BonusArr --- {{__('auto.(奖励数量/分)')}} (REG | MOBILE | EMAIL | PWA)</label>
                                     @php $bonusArr = explode('|', $info->BonusArr); @endphp

+ 8 - 0
resources/views/admin/web_channel_config/index.blade.php

@@ -45,6 +45,7 @@
                                     <th>{{ __('auto.备注') }}</th>
                                     <th>StateNo</th>
                                     <th>Platform</th>
+                                    <th>PlatformToken</th>
                                     <th>BonusArr</th>
                                     <th>{{ __('auto.操作') }}</th>
                                 </tr>
@@ -70,6 +71,13 @@
                                         <td>{{$v->Remarks}}</td>
                                         <td>{{$v->StateNo}}</td>
                                         <td>{{$v->PlatformName}} ({{$v->PlatformID}})</td>
+                                        <td>
+                                            @if(!empty($v->PlatformToken))
+                                                <span style="color: #28a745;">{{ __('auto.已配置') }}</span>
+                                            @else
+                                                <span style="color: #dc3545;">{{ __('auto.未配置') }}</span>
+                                            @endif
+                                        </td>
                                         <td>{{$v->BonusArr}}</td>
                                         <td>
                                             <button class="btn btn-sm btn-gradient-dark" onclick="edit({{$v->ID}})">

+ 3 - 0
routes/game.php

@@ -266,6 +266,9 @@ Route::group([
 
     $route->any('/pgsoft/lunch', 'Game\PgSimController@gameLunch');
     $route->any('/jiligame/lunch', 'Game\JiliSimController@gameLunch');
+
+    $route->any('/hacksaw/lunch', 'Game\HacksawController@gameLunch');
+
     //测试
     $route->any('/pg/lunch', 'Game\PgSoftTestController@gameLunch');
 

+ 9 - 1
routes/web.php

@@ -214,6 +214,9 @@ Route::group([
         $route->post('/reward-code/{id}/status', 'Admin\RewardCodeController@updateStatus');
         $route->get('/reward-code/history', 'Admin\RewardCodeController@records');
 
+        $route->get('/account_cookie/list', 'Admin\AccountCookieController@index')->name('admin.account_cookie.index');
+
+
         //充值管理
         $route->get('/recharge/config/cash', 'Admin\WithdrawalController@cashier_channel_config')->name('admin.cashier.config');
         $route->get('/recharge/list/{history?}', 'Admin\RechargeController@rechargeList');
@@ -705,6 +708,7 @@ Route::group([
         $route->any('/extension_new/daily_binding', 'Admin\ExtensionNewController@dailyBinding')->name('admin.extension_new.daily_binding');
         $route->any('/extension_new/reward', 'Admin\ExtensionNewController@reward')->name('admin.extension_new.reward');
         $route->any('/extension_new/bind_list', 'Admin\ExtensionNewController@bind_list')->name('admin.extension_new.bind_list');
+        $route->any('/extension_new/subordinate', 'Admin\ExtensionNewController@subordinate');
 
 
         $route->any('/version/del_version', 'Admin\VersionController@delVersion');
@@ -832,7 +836,11 @@ Route::group([
 
         $route->any('/game_data/user_detail', 'Admin\GameDataController@userDetail');
         $route->any('/user_total/list', 'Admin\GameDataController@userTotalList');
-
+        // vip 等级配置管理
+        $route->get('/protect-level', 'Admin\ProtectLevelController@index');
+        $route->any('/protect-level/add', 'Admin\ProtectLevelController@add');
+        $route->any('/protect-level/edit/{id}', 'Admin\ProtectLevelController@edit');
+        $route->post('/protect-level/delete/{id}', 'Admin\ProtectLevelController@delete');
     });
 
 });

Некоторые файлы не были показаны из-за большого количества измененных файлов